跳至主要內容

Lua 腳本:範例與模式

本頁介紹 FreeMinecraftModels 道具腳本的範例、寫法與注意事項。請準備範例引用的模型和動畫,先在伺服器驗證效果,再加入地圖。

初次撰寫腳本請先閱讀入門指南。完整介面見道具 API。


範例:無敵道具​

學習內容: 用最小的腳本取消左鍵點擊的預設傷害。

將範例儲存為腳本資料夾中的 invulnerable.lua,重新載入腳本後再綁定至道具。

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

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

逐步講解​

  1. 玩家攻擊道具時觸發 on_left_click。Lua 事件是 FMM 預設道具傷害處理使用的取消旗標。
  2. 此鉤子應當總是提供 context.event,但先確認是否存在仍是好習慣。
  3. context.event.cancel() 阻止 FMM 套用這次點擊的預設傷害,不會讓道具免疫所有傷害來源。

用法​

在道具的 .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)
-- Prevent default left-click damage
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_ANIMATION 和 CLOSE_ANIMATION 定義在回傳的表外,方便依模型的動畫名稱調整。
  2. on_spawn 將 context.state.is_open 初始化為 false。狀態在此道具的同一腳本綁定內跨鉤子保留,同一道具的其他腳本各有自己的狀態。
  3. on_left_click 取消這次點擊的預設傷害。
  4. on_right_click 請求停止和播放對應動畫,切換旗標並播放音效。旗標記錄請求的狀態,不代表動畫已播放。blend = true 將動畫排入佇列;停止請求不能中斷共用引擎正在播放的 death。詳見動畫。此例不改變屏障碰撞,也不保證門口可以通行。
  5. 音效接受不區分大小寫的 Bukkit 列舉名稱、minecraft:block.note_block.harp 等有效鍵及自訂資源包音效鍵。用戶端資源包必須包含對應音效。
動畫名稱

"open"、"close" 必須符合模型定義。找不到時 play_animation() 回傳 false,但本例仍會切換旗標並播放音效。若狀態取決於請求是否被接受,請檢查回傳值。即使回傳 true,也不代表用戶端已顯示動畫。


範例:帶粒子的鄰近觸發器​

學習內容: 建立區域、監看進入和離開、產生粒子,以及清理資源。

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

逐步講解​

  1. ZONE_RADIUS 和 PARTICLE_INTERVAL 是檔案層級的常數,方便調整。
  2. on_spawn 先將狀態欄位初始化為 nil 或 0。
  3. context.zones:create_sphere() 在道具生成時的位置建立固定球形區域,數值 handle 指向該區域。粒子環讀取道具目前位置,因此應搭配靜止道具使用,或在移動道具時更新區域。
  4. context.zones:watch(handle, function() end, function() end) 啟用 on_zone_enter 和 on_zone_leave。鉤子調整 context.state 中的計數器,也透過 context.player / context.event.player 提供穿越邊界的玩家,可用來撰寫玩家專屬邏輯。
  5. 重複任務每 10 個伺服器 tick 產生一圈粒子,20 TPS 時為半秒。區域內有無玩家決定粒子類型。
  6. on_destroy 取消重複任務和區域監看。移除道具時系統也會自動清理,但明確清理仍是好習慣。
粒子效能

每個 tick 產生大量粒子會影響效能。可使用 10 到 20 ticks 的間隔,並控制數量。本例 PARTICLE_INTERVAL = 10,20 TPS 時每秒兩次,每圈 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)
-- Prevent default left-click damage
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
}

逐步講解​

  1. context.cooldowns:check_local("sound", COOLDOWN_TICKS) 檢查冷卻,可用時立即啟動。同一道具的此腳本綁定由所有玩家共用冷卻。每個 tick 代表 50 ms 的經過時間;排程器延遲則計算實際伺服器 tick。
  2. 音效接受不區分大小寫的 Bukkit 列舉名稱、minecraft:block.note_block.harp 等有效鍵及自訂資源包音效鍵。用戶端必須有對應音效。
  3. 播放音效時,道具上方出現音符粒子作為提示。
  4. 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
}

逐步講解​

  1. on_spawn 立即啟動 "idle"。blend 為 false 表示立即切換。內建 idle 狀態始終循環,不受傳入的 loop 引數影響。
  2. 重複任務每 2 秒在道具上方產生附魔台粒子,營造魔法效果。
  3. on_destroy 取消粒子任務。

範例:可坐椅子​

學習內容: 右鍵就座、左鍵起身,以及透過 context.event.player 取得互動玩家。

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

on_right_click = function(context)
-- Handle seating here instead of also using the default mount fallback
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 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 default left-click damage
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_click 從 context.event.player 取得玩家,確認尚未就座後,透過 context.prop:mount(player) 請求第一個空閒 m_ 掛載點座位。
  2. on_left_click 取消這次點擊的預設傷害,若玩家正在乘坐,則呼叫 context.prop:dismount(player) 讓玩家下來。
  3. context.prop:get_passengers() 回傳實體表陣列,透過 UUID 找到互動玩家。
  4. 音效伴隨排入佇列的就座和起身請求,不代表操作已完成。右鍵鉤子取消 FMM 預設就座處理,避免再次嘗試。模型必須有空閒 m_ 掛載點。

範例:祝福神壇​

學習內容: 檢查和消耗手持物品、隨機藥水效果、冷卻及粒子和音效回饋。

完整腳本檔案(點擊展開)
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)
-- Prevent default left-click damage
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() 回傳主手物品的 type、amount、display_name,空手則回傳 nil。held.type 是大寫 Bukkit 材質名稱,因此與 "GOLD_INGOT" 比較。
  2. player:consume_held_item(1) 將主手物品移除任務排入佇列,執行時才讀取主手,不回傳付款結果。本例不是原子交易。玩家更換手持物品可能改變消耗對象,而祝福立即套用。
  3. 檔案層級的 BLESSINGS 表列出正面效果,math.random(#BLESSINGS) 隨機選擇。player:add_potion_effect(effect, duration, amplifier) 套用效果,600 ticks 為 30 秒,amplifier 1 為 II 級。
  4. 此腳本綁定的所有玩家共用旗標,600 個伺服器 tick 後重設,20 TPS 時為 30 秒。按經過時間計算的冷卻可使用發聲道具範例中的 context.cooldowns。
  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)
-- Prevent default left-click damage
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. 兩個分支都設定同一腳本綁定內所有玩家共用的旗標,600 個伺服器 tick 後重設,20 TPS 時為 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)
-- Prevent default left-click damage
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) 透過佇列請求非循環動畫。停止任務在點擊後 100 個伺服器 tick 執行,不是從實際播放開始計時。佇列忙碌可能縮短可見旋轉,甚至使它無法顯示;共用引擎也不會停止正在播放的 death。本例在動畫請求回傳 false 時仍設定旗標。
  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. 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)
-- Prevent default left-click damage
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(handle, function() end, nil) 啟用 on_zone_enter。移動道具時也要移動或重建區域。
  2. on_zone_enter 播放 "jumpscare" 動畫、惡魂尖叫並產生營火煙霧,製造突然的驚嚇。
  3. 所有玩家共用此腳本綁定的旗標。1200 個伺服器 tick 後重設,20 TPS 時為 60 秒;之後下一次進入區域可再次觸發。
  4. context.zones:watch() 第三個引數為 nil,表示不處理玩家離開。
  5. 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)
-- Prevent default left-click damage
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. context.prop:spawn_elitemobs_boss(filename, x, y, z) 在指定座標生成 EliteMobs 自訂 Boss。檔名須符合 EliteMobs custombosses 資料夾中的 .yml 檔案。
  2. 整合功能無法回傳活著的 Boss 時回傳 nil,包括外掛無法使用、設定缺失或生成失敗。範例訊息建議檢查 EliteMobs,不代表已確定具體原因。
  3. 請求的位置在道具上方一格。這不是地形或碰撞檢查,請為 Boss 選擇合適的空地。
  4. 同一腳本綁定的所有玩家共用每 200 個伺服器 tick 一次的嘗試,20 TPS 時為 10 秒。它不限制存活 Boss 數,道具消失也不會移除 Boss。用於重複遊玩時應增加遭遇數量等限制。
  5. 成功時使用火焰與喚魔者召喚聲,失敗時使用煙霧與滅火聲,讓玩家不用查看主控台也能區分結果。
EliteMobs 整合

"goblin_warrior.yml" 等檔名必須對應既有自訂 Boss 設定。散布使用此腳本的地圖或地城時,請附帶 Boss 設定並註明 EliteMobs 相依需求。


物品效果​

裝備物品腳本範例已停用。目前的物品效果請使用自訂物品定義和自訂附魔。

最佳實踐​

  • 從小鉤子開始驗證。 先寫一個輸出日誌的 on_spawn,確認觸發後再擴充。
  • 輔助函式保持區域作用域。 在回傳表外宣告 local function toggle_door(context),避免污染全域作用域。
  • 在 on_spawn 初始化狀態。 若未設定 context.state.is_open 就在 on_right_click 讀取,它會是 nil,比較結果可能不符預期。
  • 及時取消重複任務。 每個 run_repeating 應在 on_destroy 有對應的 cancel,避免浪費 CPU。
  • 使用回呼自己的上下文。 排程器會提供新的 context 引數,回呼內使用它,不要擷取外部 context。
  • 保持 on_game_tick 輕量。 它每個伺服器 tick 執行,20 TPS 時每秒 20 次。用 context.cooldowns 或排程間隔限制高負荷工作。
  • 明確控制左鍵傷害。 不希望攻擊造成預設道具傷害時,在 on_left_click 中取消;這並不阻止所有傷害來源。
  • 粒子名稱接受 FLAME 和 flame,API 統一大小寫後查找 Bukkit 粒子。音效接受不區分大小寫的 Bukkit 列舉名稱、minecraft:block.note_block.harp 等有效鍵及自訂資源包音效鍵。用戶端必須有對應音效。

常見新手錯誤​

  • 在排程回呼中使用外部 context。 它保留鉤子執行時的快照,應使用回呼自己的引數。
  • 忘記取消重複任務。 on_spawn 啟動的 run_repeating 若不取消,會持續到道具移除。
  • 未初始化狀態。 設定前讀取 context.state.x 會得到 nil,可能使邏輯出錯而沒有明顯提示。
  • 動畫名稱錯誤。 play_animation("open") 回傳 false 時,請檢查模型中的正確名稱。
  • 粒子名稱接受 FLAME 和 flame,API 統一大小寫後查找 Bukkit 粒子。音效接受不區分大小寫的 Bukkit 列舉名稱、minecraft:block.note_block.harp 等有效鍵及自訂資源包音效鍵。用戶端必須有對應音效。
  • 忘記 api_version = 1。 回傳的表必須包含該欄位,否則 FMM 不載入腳本。
  • 把輔助函式放進回傳表。 除 api_version 和可選的 priority 外,只能放入支援的鉤子名稱及函式值。輔助函式應在表外以區域函式定義。FMM 按設定清單順序執行綁定腳本,priority 不會改變順序。

QC 檢查清單​

部署道具腳本前請檢查:

  1. 檔案只回傳一個包含 api_version = 1 的表。
  2. 所有鉤子名稱準確符合道具鉤子列表。附魔鉤子屬於另一個執行環境。
  3. 在已知點擊鉤子以外取消事件前,檢查 context.event 和 context.event.cancel。
  4. context.state 欄位在 on_spawn 初始化。
  5. 每個 scheduler:run_repeating(...) 都在 on_destroy 有對應的 scheduler:cancel(...)。
  6. 排程回呼使用自己的上下文引數,不使用外部 context。
  7. on_game_tick 對高負荷工作設有條件檢查。
  8. 方法名稱均存在於道具 API參考中,沒有編造的別名。
  9. 粒子名稱接受 FLAME 和 flame,API 統一大小寫後查找 Bukkit 粒子。音效接受不區分大小寫的 Bukkit 列舉名稱、minecraft:block.note_block.harp 等有效鍵及自訂資源包音效鍵。用戶端必須有對應音效。
  10. 鉤子與回呼內沒有阻塞或長時間執行的操作。

AI 生成提示​

請在產生道具腳本的提示詞中提供:

  • 準確的鉤子名稱,例如 on_right_click,而不只是「玩家點擊時」。
  • 模型中的動畫名稱,AI 無法猜測。
  • 音效列舉名稱,例如 "BLOCK_NOTE_BLOCK_HARP",而不只是「豎琴聲」。
  • 粒子列舉名稱,例如 "FLAME",而不只是「火焰粒子」。
  • 左鍵是否造成預設傷害。需要阻止時使用 on_left_click 和 context.event.cancel()。
  • 只使用已驗證的方法名稱。優先參照道具 API;需要的操作未列出時,應檢查目前執行環境的原始碼,不要編造別名。

良好的提示詞範例​

撰寫一個 FMM 道具腳本,右鍵時播放 "activate" 動畫,取消左鍵預設傷害,點擊時在道具位置產生 FLAME 粒子並播放 BLOCK_LEVER_CLICK,用 context.cooldowns:check_local("activate", 40) 設定兩次點擊間的 2 秒冷卻。


物品內容相容性​

使用 scripts: 的舊物品包需要更新物品定義和附魔內容。FMM 跳過這些已停用的定義,保留原檔不變。加入 material: 已不能啟用物品腳本。

下一步​