Lua 腳本:範例與模式
本頁包含 FreeMinecraftModels 道具和物品腳本的完整可運行範例,以及實用模式和最佳實踐。每個範例都附有詳細說明,解釋其功能和原因。
如果你剛接觸腳本,請從入門指南開始。完整的 API 詳情請參閱 Prop 與 Item API。
範例:無敵道具
本範例教你: 最簡單的實用腳本——取消傷害,使道具無法被破壞。
這是 FreeMinecraftModels 自帶的預製腳本。
完整腳本檔案(點擊展開)
return {
api_version = 1,
on_left_click = function(context)
if context.event then
context.event.cancel()
end
end
}
逐步講解
-
鉤子選擇 --
on_left_click在玩家攻擊(左鍵點擊)道具時觸發。底層是道具支撐盔甲架上的EntityDamageByEntityEvent。 -
事件守衛 --
context.event在此鉤子中應該始終存在,但添加守衛檢查是一種好習慣。 -
取消 --
context.event.cancel()取消傷害事件,防止盔甲架受到傷害和被摧毀。
用法
在你的道具 .yml 配置中新增:
isEnabled: true
scripts:
- invulnerable.lua
範例:互動式門
本範例教你: 右鍵切換狀態、播放和停止動畫,以及使用 context.state 追蹤門是開啟還是關閉狀態。
完整腳本檔案(點擊展開)
local OPEN_ANIMATION = "open"
local CLOSE_ANIMATION = "close"
return {
api_version = 1,
on_spawn = function(context)
context.state.is_open = false
end,
on_left_click = function(context)
-- Make the door invulnerable
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
local loc = context.prop.current_location
if context.state.is_open then
-- Close the door
context.prop:stop_animation()
context.prop:play_animation(CLOSE_ANIMATION, true, false)
context.state.is_open = false
if loc then
context.world:play_sound(
"BLOCK_WOODEN_DOOR_CLOSE",
loc.x, loc.y, loc.z,
1.0, 1.0
)
end
else
-- Open the door
context.prop:stop_animation()
context.prop:play_animation(OPEN_ANIMATION, true, false)
context.state.is_open = true
if loc then
context.world:play_sound(
"BLOCK_WOODEN_DOOR_OPEN",
loc.x, loc.y, loc.z,
1.0, 1.0
)
end
end
end
}
逐步講解
-
檔案作用域的常數 --
OPEN_ANIMATION和CLOSE_ANIMATION定義在 return 表之上。這讓使用不同動畫名稱的不同模型檔案可以輕鬆變更它們。 -
狀態初始化 --
on_spawn設定context.state.is_open = false。狀態會在此道具實例的所有鉤子之間持續存在。 -
無敵 --
on_left_click鉤子取消傷害,使門不會被意外破壞。 -
切換邏輯 --
on_right_click檢查context.state.is_open,停止任何當前動畫,播放適當的動畫,翻轉狀態,並播放音效。在play_animation()之前呼叫stop_animation()可確保乾淨的過渡。 -
音效回饋 --
context.world:play_sound()在道具位置播放 Bukkit Sound 列舉名稱。聲音名稱必須是 UPPER_CASE 列舉名稱。
像 "open" 和 "close" 這樣的動畫名稱必須與模型檔案中定義的相符。如果找不到動畫,play_animation() 會回傳 false 且什麼都不會發生。請檢查你的模型檔案以確認準確的動畫名稱。
範例:帶粒子的鄰近觸發器
本範例教你: 區域建立、監看區域的進入/離開事件、粒子效果,以及清理。
完整腳本檔案(點擊展開)
local ZONE_RADIUS = 8
local PARTICLE_INTERVAL = 10 -- ticks between particle bursts
return {
api_version = 1,
on_spawn = function(context)
context.state.zone_handle = nil
context.state.particle_task = nil
context.state.players_in_zone = 0
local loc = context.prop.current_location
if loc == nil then return end
-- Create a sphere zone around the prop
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, ZONE_RADIUS)
context.state.zone_handle = handle
-- Pass placeholder functions to enable on_zone_enter and on_zone_leave.
-- The actual logic lives in the hooks below.
context.zones:watch(handle, function() end, function() end)
-- Start a repeating particle effect at the zone boundary
context.state.particle_task = context.scheduler:run_repeating(0, PARTICLE_INTERVAL, function(tick_context)
local prop_loc = tick_context.prop.current_location
if prop_loc == nil then return end
-- Spawn particles in a ring at the zone boundary
for angle = 0, 350, 30 do
local rad = math.rad(angle)
local px = prop_loc.x + math.cos(rad) * ZONE_RADIUS
local pz = prop_loc.z + math.sin(rad) * ZONE_RADIUS
if (tick_context.state.players_in_zone or 0) > 0 then
-- Flame particles when players are inside
tick_context.world:spawn_particle("FLAME", px, prop_loc.y + 0.5, pz, 1, 0, 0, 0, 0)
else
-- Green particles when zone is empty
tick_context.world:spawn_particle("HAPPY_VILLAGER", px, prop_loc.y + 0.5, pz, 1, 0, 0, 0, 0)
end
end
end)
end,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_zone_enter = function(context)
context.state.players_in_zone = (context.state.players_in_zone or 0) + 1
end,
on_zone_leave = function(context)
context.state.players_in_zone = math.max(0, (context.state.players_in_zone or 0) - 1)
end,
on_destroy = function(context)
-- Clean up the repeating task
if context.state.particle_task then
context.scheduler:cancel(context.state.particle_task)
context.state.particle_task = nil
end
-- Clean up the zone watch
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}
逐步講解
-
常數 --
ZONE_RADIUS和PARTICLE_INTERVAL位於檔案作用域,方便調整。 -
狀態初始化 --
on_spawn在做任何其他事情之前,先將所有狀態欄位設為nil/0。 -
區域建立 --
context.zones:create_sphere()建立一個以道具為中心的球形區域。回傳的 handle 是一個數值 ID,用於之後引用此區域。 -
區域監看 --
context.zones:watch(handle, function() end, function() end)啟用on_zone_enter和on_zone_leave鉤子。這些鉤子會遞增和遞減儲存在context.state中的計數器;如果你想要針對特定玩家的行為,它們也會以context.player/context.event.player接收穿越的玩家。 -
粒子迴圈 -- 一個重複任務每半秒在道具周圍的環形中生成粒子。粒子類型會根據玩家是否在區域內而改變。
-
清理 --
on_destroy取消重複任務並停止監看區域。雖然兩者都會在道具被移除時自動清理,但明確清理是一種最佳實踐。
每個 tick 生成大量粒子可能影響效能。請使用合理的間隔(10-20 ticks)並保持粒子數量較低。上方的範例使用 PARTICLE_INTERVAL = 10(每秒兩次),每個環形只有 12 個粒子。
範例:發聲道具
本範例教你: 在互動時播放音效、使用 context.cooldowns,以及防止快速連續互動。
完整腳本檔案(點擊展開)
local SOUND_NAME = "BLOCK_NOTE_BLOCK_HARP"
local COOLDOWN_TICKS = 40 -- 2 seconds between sounds
return {
api_version = 1,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
-- Prevent spam
if not context.cooldowns:check_local("sound", COOLDOWN_TICKS) then
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Play the sound
context.world:play_sound(SOUND_NAME, loc.x, loc.y, loc.z, 1.0, 1.0)
-- Show some particles
context.world:spawn_particle("NOTE", loc.x, loc.y + 1.5, loc.z, 5, 0.3, 0.3, 0.3, 0)
end
}
逐步講解
-
冷卻模式 --
context.cooldowns:check_local("sound", COOLDOWN_TICKS)檢查"sound"冷卻是否就緒,如果就緒,會立即啟動它。這讓點擊處理器保持精簡,並避免僅為了重設旗標而建立排程器任務。 -
音效播放 --
context.world:play_sound()接受 UPPER_CASE 的 Bukkit Sound 列舉名稱、座標、音量和音調。 -
粒子回饋 -- 音效播放時音符粒子會出現在道具上方,提供視覺提示。
-
無敵 --
on_left_click鉤子照常取消傷害。
一般冷卻請使用 context.cooldowns。當你需要在延遲結束時進行自訂狀態變更時,才保留 context.state 加上 scheduler:run_later() 的做法。
範例:動畫環境道具
本範例教你: 在生成時啟動循環動畫,並搭配基於 tick 的粒子發射器。
完整腳本檔案(點擊展開)
return {
api_version = 1,
on_spawn = function(context)
-- Start the idle animation immediately, looping
context.prop:play_animation("idle", false, true)
-- Emit ambient particles every 40 ticks (2 seconds)
context.state.ambient_task = context.scheduler:run_repeating(0, 40, function(tick_context)
local loc = tick_context.prop.current_location
if loc == nil then return end
tick_context.world:spawn_particle(
"ENCHANT",
loc.x, loc.y + 1, loc.z,
10, 0.5, 0.5, 0.5, 0.05
)
end)
end,
on_left_click = function(context)
if context.event then
context.event.cancel()
end
end,
on_destroy = function(context)
if context.state.ambient_task then
context.scheduler:cancel(context.state.ambient_task)
end
end
}
逐步講解
-
自動啟動動畫 --
on_spawn立即播放循環的"idle"動畫。blend 為false表示它會重新開始,不會從前一個動畫混合。loop 為true表示它會無限重複。 -
環境粒子 -- 一個重複任務每 2 秒在道具上方生成附魔台粒子,營造神奇的環境效果。
-
清理 --
on_destroy取消粒子任務。
範例:可坐椅子
本範例教你: 右鍵將玩家乘坐到道具上、左鍵讓玩家下來,以及使用 context.event.player 取得互動的玩家。
完整腳本檔案(點擊展開)
return {
api_version = 1,
on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end
-- Check if the player is already sitting on this prop
local passengers = context.prop:get_passengers()
for i = 1, #passengers do
if passengers[i].uuid == player.uuid then
-- Player is already seated, do nothing on right-click
return
end
end
-- Mount the player on the chair
context.prop:mount(player)
local loc = context.prop.current_location
if loc then
context.world:play_sound("BLOCK_WOOD_PLACE", loc.x, loc.y, loc.z, 0.8, 1.2)
end
end,
on_left_click = function(context)
-- Cancel damage so the chair is invulnerable
if context.event then
context.event.cancel()
end
local player = context.event and context.event.player
if not player then return end
-- Check if the player is sitting and dismount them
local passengers = context.prop:get_passengers()
for i = 1, #passengers do
if passengers[i].uuid == player.uuid then
context.prop:dismount(player)
local loc = context.prop.current_location
if loc then
context.world:play_sound("BLOCK_WOOD_BREAK", loc.x, loc.y, loc.z, 0.8, 1.0)
end
return
end
end
end
}
逐步講解
-
右鍵坐下 --
on_right_click從context.event.player取得玩家,檢查他們是否已經是乘客(以避免重複乘坐),然後呼叫context.prop:mount(player)讓他們坐到道具的盔甲架上。 -
左鍵起身 --
on_left_click取消傷害事件(無敵),然後檢查攻擊的玩家是否目前是乘客。如果是,context.prop:dismount(player)會讓他們下來。 -
乘客檢查 --
context.prop:get_passengers()回傳實體表的陣列。我們比較 UUID 以在列表中找到互動的玩家。 -
音效回饋 -- 坐下時播放放置木頭的聲音,起身時播放破壞木頭的聲音,提供觸覺回饋。
範例:祝福神壇
本範例教你: 檢查玩家手持的物品、消耗物品、套用隨機藥水效果、冷卻管理,以及粒子/音效回饋。
完整腳本檔案(點擊展開)
local COOLDOWN_TICKS = 600 -- 30 seconds between uses
local BLESSINGS = {
{ effect = "speed", name = "Swiftness" },
{ effect = "strength", name = "Strength" },
{ effect = "regeneration", name = "Regeneration" },
{ effect = "resistance", name = "Resistance" },
{ effect = "jump_boost", name = "Leap" },
{ effect = "haste", name = "Haste" },
}
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end
-- Check cooldown
if context.state.on_cooldown then
player:send_message("&eThe shrine is recharging... Please wait.")
return
end
-- Check if the player is holding a gold ingot
local held = player:get_held_item()
if not held or held.type ~= "GOLD_INGOT" then
player:send_message("&eThe shrine demands a gold offering...")
return
end
-- Consume one gold ingot
player:consume_held_item(1)
-- Play blessing effects
local loc = context.prop.current_location
if loc then
context.world:spawn_particle("ENCHANT", loc.x, loc.y + 1.5, loc.z, 30, 0.5, 0.5, 0.5, 0.5)
context.world:spawn_particle("HAPPY_VILLAGER", loc.x, loc.y + 1, loc.z, 10, 0.3, 0.3, 0.3, 0)
context.world:play_sound("BLOCK_BEACON_ACTIVATE", loc.x, loc.y, loc.z, 1.0, 1.5)
end
-- Apply a random blessing
local chosen = BLESSINGS[math.random(#BLESSINGS)]
player:add_potion_effect(chosen.effect, 600, 1) -- 30 seconds, level II
player:send_message("&aThe shrine blesses you with " .. chosen.name .. "!")
-- Set cooldown
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}
逐步講解
-
物品檢查 --
player:get_held_item()回傳一個包含主手物品type、amount和display_name的表(如果為空則回傳nil)。held.type是大寫的 Bukkit 材質名稱,所以我們將它與"GOLD_INGOT"比較。 -
物品消耗 --
player:consume_held_item(1)從玩家主手的堆疊中移除一個物品。 -
隨機增益 -- 檔案作用域的
BLESSINGS表列出可用的正面效果。math.random(#BLESSINGS)隨機挑選一個。player:add_potion_effect(effect, duration, amplifier)套用它——600ticks 是 30 秒,amplifier1是等級 II。 -
冷卻 -- 與發聲道具範例相同的布林旗標加排程器模式。30 秒冷卻防止神壇被濫用。
-
回饋 -- 附魔和開心村民粒子加上烽火台啟動聲營造「神聖祝福」的感覺。
範例:詛咒神壇
本範例教你: 負面藥水效果、閃電打擊、實體生成,以及根據玩家貢品的分支邏輯。
完整腳本檔案(點擊展開)
local COOLDOWN_TICKS = 600 -- 30 seconds between uses
local BUFFS = {
{ effect = "speed", name = "Swiftness" },
{ effect = "strength", name = "Strength" },
{ effect = "regeneration", name = "Regeneration" },
{ effect = "resistance", name = "Resistance" },
}
local CURSES = {
{ effect = "slowness", name = "Slowness" },
{ effect = "weakness", name = "Weakness" },
{ effect = "poison", name = "Poison" },
{ effect = "mining_fatigue", name = "Mining Fatigue" },
}
local ZOMBIE_COUNT = 4
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end
-- Check cooldown
if context.state.on_cooldown then
player:send_message("&7The dark shrine pulses with residual energy...")
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Check if the player is holding a gold ingot
local held = player:get_held_item()
if not held or held.type ~= "GOLD_INGOT" then
-- No offering -- punish the player!
player:send_message("&4The shrine demands tribute! You dare approach empty-handed?!")
-- Strike lightning on the player
local player_loc = player.current_location
if player_loc then
context.world:strike_lightning(player_loc.x, player_loc.y, player_loc.z)
end
-- Spawn a horde of zombies around the shrine
for i = 1, ZOMBIE_COUNT do
local angle = math.rad((360 / ZOMBIE_COUNT) * i)
local spawn_x = loc.x + math.cos(angle) * 3
local spawn_z = loc.z + math.sin(angle) * 3
context.world:spawn_entity("zombie", spawn_x, loc.y, spawn_z)
end
-- Apply a random curse
local chosen_curse = CURSES[math.random(#CURSES)]
player:add_potion_effect(chosen_curse.effect, 400, 1) -- 20 seconds, level II
player:send_message("&cThe shrine curses you with " .. chosen_curse.name .. "!")
-- Ominous effects
context.world:spawn_particle("SMOKE", loc.x, loc.y + 1, loc.z, 30, 0.5, 0.5, 0.5, 0.05)
context.world:play_sound("ENTITY_WITHER_AMBIENT", loc.x, loc.y, loc.z, 1.0, 0.5)
else
-- Gold offered -- reward the player
player:consume_held_item(1)
-- Apply a random buff
local chosen_buff = BUFFS[math.random(#BUFFS)]
player:add_potion_effect(chosen_buff.effect, 600, 1) -- 30 seconds, level II
player:send_message("&aThe dark shrine accepts your offering. You are blessed with " .. chosen_buff.name .. "!")
-- Positive feedback
context.world:spawn_particle("ENCHANT", loc.x, loc.y + 1.5, loc.z, 30, 0.5, 0.5, 0.5, 0.5)
context.world:play_sound("BLOCK_BEACON_ACTIVATE", loc.x, loc.y, loc.z, 1.0, 0.8)
end
-- Set cooldown regardless of path
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}
逐步講解
-
依貢品分支 -- 腳本檢查
player:get_held_item()並採取兩條路徑之一:玩家沒有金子時懲罰,有金子時獎勵。 -
閃電打擊 --
context.world:strike_lightning(x, y, z)在玩家位置打擊真實(會造成傷害)的閃電。玩家的位置從player.current_location讀取。 -
殭屍生成 --
context.world:spawn_entity("zombie", x, y, z)生成原版殭屍。迴圈使用三角函數將牠們平均分佈在神壇周圍的圓圈中。 -
負面藥水效果 --
player:add_potion_effect("poison", 400, 1)套用 20 秒的中毒 II。效果名稱是符合 BukkitPotionEffectType名稱的小寫字串。 -
獎勵路徑 -- 當提供金子時,神壇消耗一個錠並套用隨機正面效果,與祝福神壇的行為相呼應。
-
冷卻 -- 無論採取哪個分支都會套用 30 秒冷卻,防止快速連續的懲罰或獎勵。
一次生成多個實體可能影響伺服器效能。請保持數量較低(4-6),並考慮在許多玩家同時使用神壇時加入每位玩家的冷卻。
範例:旋轉地球儀
本範例教你: 在互動時播放定時動畫、排程動畫停止,以及機械音效。
完整腳本檔案(點擊展開)
local SPIN_ANIMATION = "spin"
local SPIN_DURATION = 100 -- 5 seconds in ticks
return {
api_version = 1,
on_spawn = function(context)
context.state.is_spinning = false
end,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
-- Prevent starting a new spin while already spinning
if context.state.is_spinning then
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Start the spin animation (non-looping)
context.prop:play_animation(SPIN_ANIMATION, true, false)
context.state.is_spinning = true
-- Play a mechanical clicking sound
context.world:play_sound("BLOCK_CHAIN_PLACE", loc.x, loc.y, loc.z, 1.0, 1.5)
-- Schedule the animation to stop after 5 seconds
context.scheduler:run_later(SPIN_DURATION, function(later_context)
later_context.prop:stop_animation()
later_context.state.is_spinning = false
local stop_loc = later_context.prop.current_location
if stop_loc then
later_context.world:play_sound("BLOCK_CHAIN_FALL", stop_loc.x, stop_loc.y, stop_loc.z, 1.0, 0.8)
end
end)
end
}
逐步講解
-
狀態守衛 --
context.state.is_spinning防止多個重疊的旋轉請求。旗標在旋轉開始時設定,並在排程的停止觸發時清除。 -
定時動畫 --
play_animation(SPIN_ANIMATION, true, false)播放動畫一次(不循環)。scheduler:run_later(100, ...)呼叫會在剛好 5 秒後停止動畫,以防動畫本身更長或正在循環。 -
機械音效 --
BLOCK_CHAIN_PLACE提供點擊/機械的開始聲;BLOCK_CHAIN_FALL提供逐漸停下的結束聲。可依喜好調整音調。 -
回呼上下文 --
run_later回呼對所有狀態和世界存取使用later_context(而不是context)。這是排程器回呼的正確模式。
"spin" 動畫名稱必須與你的模型檔案中定義的相符。如果你的模型使用不同的名稱(例如 "rotate"、"turn"),請相應更新 SPIN_ANIMATION 常數。
範例:驚嚇道具
本範例教你: 鄰近區域觸發器、帶有長冷卻的一次性驚嚇效果,以及結合音效/粒子/動畫以產生戲劇性衝擊。
完整腳本檔案(點擊展開)
local SCARE_RADIUS = 3
local COOLDOWN_TICKS = 1200 -- 60 seconds between scares
local SCARE_ANIMATION = "jumpscare"
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
context.state.zone_handle = nil
local loc = context.prop.current_location
if loc == nil then return end
-- Create a small sphere zone around the prop
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, SCARE_RADIUS)
context.state.zone_handle = handle
-- Watch for players entering the zone. The actual scare logic
-- lives in on_zone_enter below.
context.zones:watch(handle, function() end, nil)
end,
on_zone_enter = function(context)
if context.state.on_cooldown then
return
end
local scare_loc = context.prop.current_location
if scare_loc == nil then return end
-- Play the jumpscare animation
context.prop:stop_animation()
context.prop:play_animation(SCARE_ANIMATION, false, false)
-- Scary sound
context.world:play_sound(
"ENTITY_GHAST_SCREAM",
scare_loc.x, scare_loc.y, scare_loc.z,
1.0, 0.7
)
-- Burst of smoke particles
context.world:spawn_particle(
"CAMPFIRE_SIGNAL_SMOKE",
scare_loc.x, scare_loc.y + 1, scare_loc.z,
20, 0.5, 0.5, 0.5, 0.05
)
-- Set cooldown so it does not trigger again immediately
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_destroy = function(context)
-- Clean up the zone watch
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}
逐步講解
-
鄰近區域 --
context.zones:create_sphere()建立一個半徑 3 格的區域。context.zones:watch(handle, function() end, nil)啟用on_zone_enter,當任何玩家踏入時觸發。 -
驚嚇效果 --
on_zone_enter鉤子播放"jumpscare"動畫、惡魂尖叫聲,並生成營火煙霧粒子。這個組合營造突如其來、令人受驚的效果。 -
60 秒冷卻 -- 布林旗標模式防止驚嚇重複觸發。一旦觸發,道具會沉默 60 秒(
COOLDOWN_TICKS = 1200),然後重新就緒。 -
無離開鉤子 --
context.zones:watch()的第三個引數為nil,表示我們不關心玩家何時離開區域。 -
清理 --
on_destroy停止監看區域。雖然區域會在道具被移除時自動清理,但明確清理是一種最佳實踐。
為了達到最佳驚嚇效果,請將道具藏在轉角處或黑暗區域。3 格的半徑確保玩家在驚嚇觸發前已經很靠近。可依喜好調整 SCARE_RADIUS 和 COOLDOWN_TICKS。
範例:哥布林生成器道具
本範例教你: 使用 prop:spawn_elitemobs_boss() 從道具互動生成自訂 Boss,並在未安裝 EliteMobs 時優雅回退。
完整腳本檔案(點擊展開)
local BOSS_FILE = "goblin_warrior.yml"
local COOLDOWN_TICKS = 200 -- 10 seconds between spawns
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end
-- Prevent spawn spam
if context.state.on_cooldown then
player:send_message("&7The spawner is recharging...")
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Try to spawn the EliteMobs boss
local boss = context.prop:spawn_elitemobs_boss(BOSS_FILE, loc.x, loc.y + 1, loc.z)
if boss then
-- Success -- play spawn effects
context.world:spawn_particle("FLAME", loc.x, loc.y + 1, loc.z, 20, 0.5, 0.5, 0.5, 0.05)
context.world:play_sound("ENTITY_EVOKER_PREPARE_SUMMON", loc.x, loc.y, loc.z, 1.0, 1.0)
player:send_message("&cA goblin warrior emerges!")
else
-- EliteMobs is not installed or the boss file was not found
context.log:warn("Could not spawn boss '" .. BOSS_FILE .. "' -- is EliteMobs installed?")
player:send_message("&7The spawner fizzles... (EliteMobs not available)")
-- Fizzle particles as visual feedback
context.world:spawn_particle("SMOKE", loc.x, loc.y + 1, loc.z, 10, 0.3, 0.3, 0.3, 0.02)
context.world:play_sound("BLOCK_FIRE_EXTINGUISH", loc.x, loc.y, loc.z, 0.8, 1.2)
end
-- Set cooldown
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}
逐步講解
-
Boss 生成 --
context.prop:spawn_elitemobs_boss(filename, x, y, z)在指定座標生成 EliteMobs 自訂 Boss。檔名必須匹配 EliteMobscustombosses資料夾中的.yml檔案。 -
優雅回退 --
spawn_elitemobs_boss()在 EliteMobs 未安裝或 Boss 檔案不存在時回傳nil。腳本透過警告日誌訊息、熄滅粒子效果和玩家訊息來處理失敗。 -
生成偏移 -- Boss 在
loc.y + 1(道具上方一格)生成,防止 Boss 卡入道具或地面。 -
冷卻 -- 10 秒冷卻防止玩家用哥布林戰士淹沒區域。根據你的遊戲需求調整
COOLDOWN_TICKS。 -
視覺/音效區分 -- 成功使用火焰粒子和喚魔者召喚聲產生戲劇性的生成效果。失敗使用煙霧和滅火聲產生清晰的「熄滅」效果,讓玩家知道出了問題而不需要查看主控台。
Boss 檔名(例如 "goblin_warrior.yml")必須對應 EliteMobs 中現有的自訂 Boss 配置。如果你正在分發使用此腳本的地圖或地城,請包含 Boss 配置檔案並記錄 EliteMobs 依賴。
範例:寒霜衝擊波劍(物品腳本)
本範例教你: 一個完整的物品腳本,展示 on_equip 狀態初始化、用 on_shift_right_click 觸發能力、用 scheduler:run_repeating() 製作擴張波、用 get_nearby_entities() 尋找目標、實體戰鬥方法、粒子/音效效果、用 get_highest_block_y() 修改地形、用 show_action_bar() 提供 UI 回饋,以及防範非生物實體。
完整腳本檔案(點擊展開)
-- Frost Shockwave Sword
-- Shift + Right-click to release a horizontal frost wave
local WAVE_MAX_RADIUS = 12
local WAVE_SPEED = 2
local WAVE_TICK_INTERVAL = 2
local WAVE_DAMAGE = 4.0
local WAVE_KNOCKBACK = 1.2
local WAVE_CONE_ANGLE = 70
return {
api_version = 1,
on_equip = function(context)
context.state.wave_active = false
end,
on_shift_right_click = function(context)
if context.state.wave_active then return end
local loc = context.player.current_location
local yaw_rad = math.rad(loc.yaw)
local dir_x = -math.sin(yaw_rad)
local dir_z = math.cos(yaw_rad)
context.event:cancel()
context.state.wave_active = true
context.state.hit_entities = {}
context.state.destroyed_blocks = {}
local current_radius = 1
local origin_x, origin_y, origin_z = loc.x, loc.y, loc.z
local player_uuid = context.player.uuid
context.player:show_action_bar("&b&lFrost Shockwave!", 40)
context.world:play_sound("ENTITY_PLAYER_ATTACK_SWEEP", origin_x, origin_y, origin_z, 1.5, 0.5)
local task_id = context.scheduler:run_repeating(0, WAVE_TICK_INTERVAL, function(tick_context)
if current_radius > WAVE_MAX_RADIUS then
tick_context.state.wave_active = false
tick_context.scheduler:cancel(tick_context.state.wave_task)
return
end
local half_angle = math.rad(WAVE_CONE_ANGLE / 2)
local steps = math.floor(current_radius * 8)
for i = 0, steps do
local angle = (i / steps) * 2 * math.pi
local px = origin_x + math.cos(angle) * current_radius
local pz = origin_z + math.sin(angle) * current_radius
local to_x, to_z = px - origin_x, pz - origin_z
local to_len = math.sqrt(to_x * to_x + to_z * to_z)
if to_len > 0 then
local dot = (to_x * dir_x + to_z * dir_z) / to_len
if dot >= math.cos(half_angle) then
local ground_y = tick_context.world:get_highest_block_y(math.floor(px), math.floor(pz))
tick_context.world:spawn_particle("SNOWFLAKE", px, ground_y + 1.0, pz, 3, 0.3, 0.2, 0.3, 0.02)
local bx, bz = math.floor(px), math.floor(pz)
local block_key = bx .. "," .. bz
if not tick_context.state.destroyed_blocks[block_key] then
tick_context.state.destroyed_blocks[block_key] = true
local block_type = tick_context.world:get_block_at(bx, ground_y, bz)
if block_type ~= "bedrock" and block_type ~= "obsidian" and block_type ~= "barrier" then
tick_context.world:set_block_at(bx, ground_y, bz, "AIR")
end
end
end
end
end
local entities = tick_context.world:get_nearby_entities(origin_x, origin_y, origin_z, current_radius + 1)
for _, entity in ipairs(entities) do
if entity.uuid ~= player_uuid and not tick_context.state.hit_entities[entity.uuid] then
local eloc = entity.current_location
local dx, dz = eloc.x - origin_x, eloc.z - origin_z
local dist = math.sqrt(dx * dx + dz * dz)
if dist >= current_radius - 2 and dist <= current_radius + 1 and dist > 0 then
local dot = (dx * dir_x + dz * dir_z) / dist
if dot >= math.cos(half_angle) and entity.damage then
tick_context.state.hit_entities[entity.uuid] = true
entity:damage(WAVE_DAMAGE)
entity:push((dx/dist)*WAVE_KNOCKBACK, 0.3, (dz/dist)*WAVE_KNOCKBACK)
if entity.is_alive then entity:add_potion_effect("SLOWNESS", 60, 1) end
tick_context.world:spawn_particle("SNOWFLAKE", eloc.x, eloc.y+1, eloc.z, 10, 0.5, 0.5, 0.5, 0.1)
end
end
end
end
tick_context.world:play_sound("BLOCK_GLASS_BREAK",
origin_x + dir_x * current_radius, origin_y,
origin_z + dir_z * current_radius, 0.8, 1.5)
current_radius = current_radius + WAVE_SPEED
end)
context.state.wave_task = task_id
end
}
逐步講解
-
狀態初始化 --
on_equip設定wave_active = false以防止多個重疊的衝擊波。此鉤子在玩家裝備劍時觸發。 -
方向圓錐 -- 玩家的 yaw 被轉換為方向向量(
dir_x、dir_z)。玩家前方的 70 度圓錐決定波的擴散範圍。 -
擴張波 --
scheduler:run_repeating()每 2 ticks 執行一次。每個 tick,波半徑增加WAVE_SPEED。粒子會在環形中生成,但僅在圓錐角度內。 -
地形破壞 --
get_highest_block_y()在每個點找出地面高度。表面方塊會被破壞(設為 AIR),排除基岩/黑曜石/屏障。destroyed_blocks表防止重複處理。 -
實體目標 --
get_nearby_entities()找出原點附近的所有實體。腳本檢查每個實體是否在當前波帶內、在圓錐內,且尚未被擊中。if entity.damage then守衛至關重要——get_nearby_entities()回傳所有實體(盔甲架、物品等),而非僅有生物。 -
戰鬥效果 -- 被擊中的實體會受到傷害、被往波的方向擊退,並獲得 3 秒的緩慢 II。
-
玩家回饋 --
show_action_bar()顯示「Frost Shockwave!」40 ticks。玻璃破碎聲隨波前推進。 -
清理 -- 當半徑超過
WAVE_MAX_RADIUS時,重複任務會取消自己並重設wave_active旗標。
在呼叫 entity:damage()、entity:push() 或 entity:add_potion_effect() 之前,務必檢查 if entity.damage then。get_nearby_entities() 方法回傳範圍內的所有實體,包括盔甲架、掉落物和經驗球等沒有這些方法的非生物實體。
範例:治療魔杖(物品腳本)
本範例教你: 一個較簡單的物品腳本,展示右鍵啟動、冷卻管理、用於有限次數的 item:consume() 方法,以及用藥水效果治療。
完整腳本檔案(點擊展開)
-- Healing Wand
-- Right-click to heal yourself. Consumes one charge per use.
local HEAL_AMOUNT = 6.0 -- 3 hearts
local COOLDOWN_TICKS = 60 -- 3 seconds
local REGEN_DURATION = 40 -- 2 seconds of regen
local REGEN_AMPLIFIER = 1 -- Regeneration II
return {
api_version = 1,
on_right_click = function(context)
-- Check remaining charges
local uses = context.item:get_uses()
if uses <= 0 then
context.player:show_action_bar("&c&lOut of charges!", 40)
context.world:play_sound("BLOCK_FIRE_EXTINGUISH",
context.player.current_location.x,
context.player.current_location.y,
context.player.current_location.z,
0.8, 1.5)
return
end
-- Check cooldown
if not context.cooldowns:check_local("heal", COOLDOWN_TICKS) then
context.player:show_action_bar("&cWand recharging...", 20)
return
end
-- Cancel the event so we don't interact with blocks
if context.event then
context.event:cancel()
end
-- Consume one charge
context.item:set_uses(uses - 1)
-- Heal the player
local loc = context.player.current_location
context.player:add_potion_effect("REGENERATION", REGEN_DURATION, REGEN_AMPLIFIER)
-- Visual and audio feedback
context.world:spawn_particle("HEART", loc.x, loc.y + 1.5, loc.z, 8, 0.4, 0.3, 0.4, 0)
context.world:spawn_particle("HAPPY_VILLAGER", loc.x, loc.y + 1, loc.z, 15, 0.5, 0.5, 0.5, 0)
context.world:play_sound("ENTITY_PLAYER_LEVELUP", loc.x, loc.y, loc.z, 0.7, 1.8)
-- UI feedback
context.player:show_action_bar("&a&lHealed! &7(" .. (uses - 1) .. " charges left)", 40)
end
}
逐步講解
-
冷卻模式 --
context.cooldowns:check_local("heal", COOLDOWN_TICKS)在一次呼叫中檢查並啟動充能冷卻。物品本地冷卻會在裝備和卸下循環之間持續存在。 -
充能系統 --
context.item:get_uses()從物品的 PDC 讀取自訂使用計數器。每次使用透過set_uses()遞減。當充能歸 0 時,魔杖播放熄滅聲並拒絕啟動。 -
治療 -- 腳本不直接設定生命值,而是套用 2 秒的恢復 II。這與 Minecraft 的生命值系統更自然地配合,並能與其他效果正確堆疊。
-
事件取消 --
context.event:cancel()防止右鍵與附近的方塊互動(放置方塊、開門等)。 -
UI 回饋 --
show_action_bar()告訴玩家其操作的結果和剩餘充能。針對冷卻、充能耗盡和成功治療的不同訊息讓物品感覺有回應。
使用 /fmm giveitem <id> 取得正確標記的自訂物品。透過其他方式建立的物品可能缺少 fmm_item_id PDC 鍵,這表示腳本不會為它們啟動。
最佳實踐
-
從小鉤子開始並驗證。 寫一個只發送日誌訊息的
on_spawn。確認它觸發了,然後在此基礎上建構。 -
保持輔助函式為區域變數。 在 return 表上方宣告輔助函式,如
local function toggle_door(context)。這使它們不會進入全域作用域。 -
在
on_spawn中初始化所有狀態。 如果你在on_right_click中讀取context.state.is_open但從未在on_spawn中設定它,它將是nil,你的比較可能會產生意外行為。 -
完成後取消重複任務。 每個
run_repeating都應在on_destroy中有對應的cancel。洩漏的任務會浪費 CPU。 -
使用排程器回呼的新上下文。 排程器回呼接收新的上下文參數。始終在回呼內使用該參數,而不是外部的
context。 -
保持
on_game_tick輕量。 如果你定義了此鉤子,它每個伺服器 tick 都會執行(每秒 20 次)。將開銷大的工作放在context.cooldowns或排程器間隔之後。 -
預設使道具無敵。 除非你希望道具可以被破壞,否則在每個腳本中都包含
on_left_click的傷害取消。 -
Bukkit 列舉使用 UPPER_CASE。 聲音名稱和粒子名稱必須使用 Bukkit 列舉常數格式(例如
"FLAME",而不是"flame")。
常見新手錯誤
-
在排程器回呼中使用外部
context。 外部上下文捕獲的是鉤子執行時的快照。在回呼內部,始終使用回呼自身的參數。 -
忘記取消重複任務。 如果你在
on_spawn中啟動了run_repeating但從未取消它,任務會一直執行直到道具被移除。 -
未在
on_spawn中初始化狀態。 在設定之前讀取context.state.x會回傳nil,這可能會無聲地破壞你的邏輯。 -
錯誤的動畫名稱。 如果
play_animation("open")回傳false,說明動畫名稱與模型檔案中的不符。檢查模型以確認準確的名稱。 -
聲音/粒子名稱使用小寫。
"flame"不起作用——使用"FLAME"。API 內部會將粒子轉換為 UPPER_CASE,但 Sound 列舉名稱必須精確匹配。 -
忘記
api_version = 1。 回傳的表必須包含此欄位,否則 FMM 將不會載入該腳本。 -
在回傳的表中放置非鉤子函式。 輔助函式必須在
return陳述式上方宣告。回傳表中只允許使用鉤子名稱(on_spawn、on_right_click等)作為鍵。
QC 檢查清單
在部署道具腳本之前,使用此檢查清單進行驗證:
- 檔案恰好回傳一個包含
api_version = 1的表。 - 每個鉤子名稱與道具鉤子列表或物品鉤子列表中的條目精確匹配。
- 在呼叫
cancel()之前,透過if context.event then檢查了context.event。 context.state欄位在on_spawn中初始化。- 每個
scheduler:run_repeating(...)呼叫在on_destroy中有對應的scheduler:cancel(...)。 - 排程器回呼使用回呼自身的上下文參數,而不是外部的
context。 on_game_tick鉤子將開銷大的工作放在檢查之後。- 所有方法名稱存在於 Prop 與 Item API 參考中——沒有編造的別名。
- 聲音和粒子名稱使用 UPPER_CASE 的 Bukkit 列舉名稱。
- 腳本不在鉤子或回呼中呼叫任何阻塞或長時間執行的操作。
AI 生成提示
如果你想讓 AI 可靠地生成道具腳本,請確保提示詞中包含:
- 準確的鉤子名稱 -- 例如
on_right_click,而不是「當玩家點擊道具時」。 - 來自模型檔案的動畫名稱 -- AI 無法猜測這些名稱;請提供它們。
- Sound 列舉名稱 -- 例如
"BLOCK_NOTE_BLOCK_HARP",而不是「豎琴聲」。 - Particle 列舉名稱 -- 例如
"FLAME",而不是「火焰粒子」。 - 道具是否應該無敵 -- 如果是,請包含帶有
context.event.cancel()的on_left_click。 - 只使用文件中記錄的方法名稱 -- 如果不在 Prop API 頁面上,它就不存在。
良好的提示詞範例
撰寫一個 FMM 道具腳本,右鍵點擊時播放 "activate" 動畫,使道具無敵,點擊時在道具位置生成 FLAME 粒子,播放 BLOCK_LEVER_CLICK 聲音,並使用 context.cooldowns:check_local("activate", 40) 實現點擊之間 2 秒的冷卻。
哥布林物品集合
一組以哥布林武器為主題的 10 個範例物品腳本可作為可下載的範例內容取得。它們並未捆綁在基礎外掛 jar 中——外掛本身在首次執行時唯一會寫入 scripts/ 的腳本是四個預製道具腳本:invulnerable.lua、pickupable.lua、storage_single.lua 和 storage_double.lua。下方每個哥布林腳本都展示了不同的戰鬥機制、粒子效果和 API 使用模式。
| 腳本 | 物品 | 效果 |
|---|---|---|
goblin_golden_sword.lua | 金劍 | 25% 機率橫掃 -- 對所有附近敵人造成傷害並推開 |
goblin_iron_axe.lua | 鐵斧 | 20% 機率尖刺 -- 將目標向上發射並帶有滴水石視覺效果 |
goblin_bow.lua | 弓 | 將命中的怪物拉向玩家 |
goblin_crossbow.lua | 弩 | 33% 機率煙火齊射 |
goblin_trident.lua | 三叉戟 | 冰穹包覆目標 3 秒 |
goblin_iron_hoe.lua | 鐵鋤 | 15% 機率毒雲 |
goblin_mace.lua | 錘 | 從天空降下隕石(右鍵) |
goblin_spear.lua | 矛 | 沿瞄準方向發射火焰光束(shift+右鍵) |
goblin_shield.lua | 盾 | 格擋時產生玻璃穹頂(反應式) |
goblin_iron_sword.lua | 鐵劍 | 蓄力黑暗環(shift+右鍵) |
這些腳本可搭配外觀為原版的物品使用。每個只需要一個帶有 material: 欄位的 YML 配置——不需要 .bbmodel 檔案。例如,要啟用金劍腳本,請建立如下的 YML 配置:
isEnabled: true
material: GOLDEN_SWORD
scripts:
- goblin_golden_sword.lua
下一步
- 入門指南 | Prop 與 Item API | 疑難排解
- API 參考 | 腳本引擎