跳至主要内容

Lua 腳本:範例與模式

本頁包含 FreeMinecraftModels 道具腳本的完整可運行範例,以及實用模式和最佳實踐。每個範例都附有詳細說明,解釋其功能和原因。

如果你剛接觸道具腳本,請從入門指南開始。完整的 API 詳情請參閱 Prop API


範例:無敵道具

本範例教你: 最簡單的實用腳本——取消傷害,使道具無法被破壞。

這是 FreeMinecraftModels 自帶的預製腳本。

完整腳本檔案(點擊展開)
return {
api_version = 1,

on_left_click = function(context)
if context.event then
context.event.cancel()
end
end
}

逐步講解

  1. 鉤子選擇 -- on_left_click 在玩家攻擊(左鍵點擊)道具時觸發。底層是道具支撐盔甲架上的 EntityDamageByEntityEvent

  2. 事件守衛 -- context.event 在此鉤子中應該始終存在,但添加守衛檢查是一種好習慣。

  3. 取消 -- 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
}

逐步講解

  1. 檔案作用域常數 -- OPEN_ANIMATIONCLOSE_ANIMATION 定義在 return 表的上方。這樣可以方便地針對使用不同動畫名稱的不同模型檔案進行修改。

  2. 狀態初始化 -- on_spawn 設定 context.state.is_open = false。狀態在此道具實例的所有鉤子之間保持。

  3. 無敵 -- on_left_click 鉤子取消傷害,防止門被意外破壞。

  4. 切換邏輯 -- on_right_click 檢查 context.state.is_open,停止當前動畫,播放相應的動畫,翻轉狀態,並播放聲音。在 play_animation() 之前呼叫 stop_animation() 確保乾淨的過渡。

  5. 聲音回饋 -- 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

-- Watch for enter/leave
context.zones:watch(
handle,
function(player)
-- on_enter
context.state.players_in_zone = (context.state.players_in_zone or 0) + 1
end,
function(player)
-- on_leave
context.state.players_in_zone = math.max(0, (context.state.players_in_zone or 0) - 1)
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
-- Red particles when players are inside
tick_context.world:spawn_particle("DUST", 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_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
}

逐步講解

  1. 常數 -- ZONE_RADIUSPARTICLE_INTERVAL 位於檔案作用域,便於調整。

  2. 狀態初始化 -- on_spawn 在執行其他操作之前,將所有狀態欄位設定為 nil / 0

  3. 區域建立 -- context.zones:create_sphere() 建立以道具為中心的球形區域。回傳的控制代碼是一個數字 ID,用於後續參照該區域。

  4. 區域監聽 -- context.zones:watch() 註冊玩家進入和離開的回呼。回呼增減儲存在 context.state 中的計數器。

  5. 粒子迴圈 -- 重複任務每半秒在道具周圍以環形生成粒子。粒子類型根據區域內是否有玩家而變化。

  6. 清理 -- on_destroy 取消重複任務並取消區域監聽。雖然在道具被移除時兩者都會自動清理,但明確清理是最佳實踐。

粒子效能

每個 tick 生成大量粒子可能影響效能。使用合理的間隔(10-20 tick),並保持粒子數量較低。上面的範例使用 PARTICLE_INTERVAL = 10(每秒兩次),每環僅 12 個粒子。


範例:發聲道具

本範例教你: 互動時播放聲音、使用 state 和排程器實現冷卻行為,以及防止連續快速互動。

完整腳本檔案(點擊展開)
local SOUND_NAME = "BLOCK_NOTE_BLOCK_HARP"
local COOLDOWN_TICKS = 40 -- 2 seconds between sounds

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)
-- Prevent spam
if context.state.on_cooldown 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)

-- Set cooldown
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}

逐步講解

  1. 冷卻模式 -- 由於 FMM 道具腳本沒有像 EliteMobs 那樣的內建 context.cooldowns API,此範例使用 context.state.on_cooldownscheduler:run_later() 實現簡單的冷卻。播放聲音時旗標設為 true,延遲任務在 COOLDOWN_TICKS 後將其重設。

  2. 聲音播放 -- context.world:play_sound() 接受 UPPER_CASE 的 Bukkit Sound 列舉名稱、座標、音量和音調。

  3. 粒子回饋 -- 聲音播放時,道具上方會出現音符粒子,提供視覺提示。

  4. 無敵 -- on_left_click 鉤子照常取消傷害。

  5. 排程器回呼上下文 -- run_later 回呼接收 later_context,一個新的上下文。我們使用 later_context.state(而不是 context.state)來重設冷卻旗標。由於狀態是共享的,兩者指向同一個表——但使用回呼的上下文參數是正確的習慣。

冷卻實作

FMM 道具腳本沒有 EliteMobs 的 context.cooldowns API。請使用此處展示的模式:在 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
}

逐步講解

  1. 自動啟動動畫 -- on_spawn 立即播放循環 "idle" 動畫。blend 的 false 表示不與之前的動畫混合直接開始。loop 的 true 表示無限重複。

  2. 氛圍粒子 -- 重複任務每 2 秒在道具上方生成附魔台粒子,營造魔法氛圍效果。

  3. 清理 -- 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
}

逐步講解

  1. 右鍵坐下 -- on_right_clickcontext.event.player 取得玩家,檢查是否已經是乘客(避免重複騎乘),然後呼叫 context.prop:mount(player) 將其坐在道具的盔甲架上。

  2. 左鍵站起 -- on_left_click 取消傷害事件(無敵),然後檢查攻擊的玩家是否目前是乘客。如果是,context.prop:dismount(player) 將其彈出。

  3. 乘客檢查 -- context.prop:get_passengers() 回傳實體表的陣列。我們比較 UUID 來在列表中找到互動的玩家。

  4. 聲音回饋 -- 坐下時播放木頭放置聲,站起時播放木頭破碎聲,提供觸覺回饋。


範例:祝福神殿

本範例教你: 檢查玩家手持物品、消耗物品、施加隨機藥水效果、冷卻管理,以及粒子/聲音回饋。

完整腳本檔案(點擊展開)
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
}

逐步講解

  1. 物品檢查 -- player:get_held_item() 回傳主手物品的 typeamountdisplay_name 表(如果為空則回傳 nil)。我們將 held.type"gold_ingot"(小寫材質名稱)比較。

  2. 物品消耗 -- player:consume_held_item(1) 從玩家主手堆疊中移除一個物品。

  3. 隨機增益 -- 檔案作用域的 BLESSINGS 表列出可用的正面效果。math.random(#BLESSINGS) 隨機選擇一個。player:add_potion_effect(effect, duration, amplifier) 施加效果——600 tick 為 30 秒,amplifier 1 為等級 II。

  4. 冷卻 -- 與發聲道具範例相同的布林旗標加排程器模式。30 秒冷卻防止刷神殿。

  5. 回饋 -- 附魔和快樂村民粒子加上信標啟動聲音,營造「神聖祝福」的感覺。


範例:詛咒神殿

本範例教你: 負面藥水效果、閃電劈擊、實體生成,以及基於玩家供品的分支邏輯。

完整腳本檔案(點擊展開)
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
}

逐步講解

  1. 基於供品的分支 -- 腳本檢查 player:get_held_item() 並走兩條路徑之一:如果玩家沒有金錠則懲罰,如果有則獎勵。

  2. 閃電劈擊 -- context.world:strike_lightning(x, y, z) 在玩家位置劈下真實(有傷害的)閃電。玩家的位置從 player.current_location 讀取。

  3. 殭屍生成 -- context.world:spawn_entity("zombie", x, y, z) 生成原版殭屍。迴圈使用三角函數將它們均勻分佈在神殿周圍的圓上。

  4. 負面藥水效果 -- player:add_potion_effect("poison", 400, 1) 施加 20 秒的毒藥 II。效果名稱為匹配 Bukkit PotionEffectType 名稱的小寫字串。

  5. 獎勵路徑 -- 提供金錠時,神殿消耗一個金錠並施加隨機正面效果,與祝福神殿的行為相同。

  6. 冷卻 -- 無論走哪條分支都會套用 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
}

逐步講解

  1. 狀態守衛 -- context.state.is_spinning 防止多個重疊的旋轉請求。旗標在旋轉開始時設定,在排程的停止觸發時清除。

  2. 定時動畫 -- play_animation(SPIN_ANIMATION, true, false) 播放一次動畫(不循環)。scheduler:run_later(100, ...) 在恰好 5 秒後停止動畫,以防動畫本身更長或循環。

  3. 機械音效 -- BLOCK_CHAIN_PLACE 提供點擊/機械啟動聲;BLOCK_CHAIN_FALL 提供減速停止聲。可根據喜好調整音調。

  4. 回呼上下文 -- 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
context.zones:watch(
handle,
function(player)
-- on_enter: trigger the scare
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,
nil -- no on_leave callback needed
)
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
}

逐步講解

  1. 接近區域 -- context.zones:create_sphere() 建立 3 格半徑的區域。context.zones:watch() 註冊進入回呼,當任何玩家踏入時觸發。

  2. 驚嚇效果 -- 進入回呼播放 "jumpscare" 動畫、惡魂尖叫聲,並生成營火煙霧粒子。組合起來營造突然的驚嚇效果。

  3. 60 秒冷卻 -- 布林旗標模式防止驚嚇重複觸發。觸發後,道具靜默 60 秒(COOLDOWN_TICKS = 1200),然後重新啟用。

  4. 無離開回呼 -- context.zones:watch() 的第二個參數為 nil 表示我們不關心玩家何時離開區域。

  5. 清理 -- on_destroy 取消區域監視。雖然在道具被移除時區域會自動清理,但明確清理是最佳實踐。

驚嚇設計

為了最佳驚嚇效果,將道具隱藏在角落後面或黑暗區域中。3 格半徑確保玩家靠近後才觸發驚嚇。根據喜好調整 SCARE_RADIUSCOOLDOWN_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
}

逐步講解

  1. Boss 生成 -- context.prop:spawn_elitemobs_boss(filename, x, y, z) 在指定座標生成 EliteMobs 自訂 Boss。檔名必須匹配 EliteMobs custombosses 資料夾中的 .yml 檔案。

  2. 優雅回退 -- spawn_elitemobs_boss() 在 EliteMobs 未安裝或 Boss 檔案不存在時回傳 nil。腳本透過警告日誌訊息、熄滅粒子效果和玩家訊息來處理失敗。

  3. 生成偏移 -- Boss 在 loc.y + 1(道具上方一格)生成,防止 Boss 卡入道具或地面。

  4. 冷卻 -- 10 秒冷卻防止玩家用哥布林戰士淹沒區域。根據你的遊戲需求調整 COOLDOWN_TICKS

  5. 視覺/音效區分 -- 成功使用火焰粒子和喚魔者召喚聲產生戲劇性的生成效果。失敗使用煙霧和滅火聲產生清晰的「熄滅」效果,讓玩家知道出了問題而不需要查看主控台。

EliteMobs 整合

Boss 檔名(例如 "goblin_warrior.yml")必須對應 EliteMobs 中現有的自訂 Boss 配置。如果你正在分發使用此腳本的地圖或地城,請包含 Boss 配置檔案並記錄 EliteMobs 依賴。


最佳實踐

  • 從小鉤子開始並驗證。 寫一個只發送日誌訊息的 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 次)。將開銷大的工作放在基於狀態的冷卻檢查之後。

  • 預設使道具無敵。 除非你希望道具可以被破壞,否則在每個腳本中都包含 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_spawnon_right_click 等)作為鍵。


QC 檢查清單

在部署道具腳本之前,使用此檢查清單進行驗證:

  1. 檔案恰好回傳一個包含 api_version = 1 的表。
  2. 每個鉤子名稱與鉤子列表中的條目精確匹配。
  3. 在呼叫 cancel() 之前,透過 if context.event then 檢查了 context.event
  4. context.state 欄位在 on_spawn 中初始化。
  5. 每個 scheduler:run_repeating(...) 呼叫在 on_destroy 中有對應的 scheduler:cancel(...)
  6. 排程器回呼使用回呼自身的上下文參數,而不是外部的 context
  7. on_game_tick 鉤子將開銷大的工作放在檢查之後。
  8. 所有方法名稱存在於 Prop API 參考中——沒有編造的別名。
  9. 聲音和粒子名稱使用 UPPER_CASE 的 Bukkit 列舉名稱。
  10. 腳本不在鉤子或回呼中呼叫任何阻塞或長時間執行的操作。

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.state 和 scheduler:run_later 實現點擊之間 2 秒的冷卻。


下一步

  • 入門指南 -- 檔案結構、鉤子、首個腳本講解、範本
  • Prop API -- 所有上下文表的完整 API 參考
  • 疑難排解 -- 常見問題、除錯技巧