跳至主要內容

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
}

逐步講解

  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

-- 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
}

逐步講解

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

  2. 狀態初始化 -- on_spawn 在做任何其他事情之前,先將所有狀態欄位設為 nil / 0

  3. 區域建立 -- context.zones:create_sphere() 建立一個以道具為中心的球形區域。回傳的 handle 是一個數值 ID,用於之後引用此區域。

  4. 區域監看 -- context.zones:watch(handle, function() end, function() end) 啟用 on_zone_enteron_zone_leave 鉤子。這些鉤子會遞增和遞減儲存在 context.state 中的計數器;如果你想要針對特定玩家的行為,它們也會以 context.player / context.event.player 接收穿越的玩家。

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

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

逐步講解

  1. 冷卻模式 -- context.cooldowns:check_local("sound", COOLDOWN_TICKS) 檢查 "sound" 冷卻是否就緒,如果就緒,會立即啟動它。這讓點擊處理器保持精簡,並避免僅為了重設旗標而建立排程器任務。

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

  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 表示它會重新開始,不會從前一個動畫混合。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 是大寫的 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. 冷卻 -- 與發聲道具範例相同的布林旗標加排程器模式。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. 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
}

逐步講解

  1. 鄰近區域 -- context.zones:create_sphere() 建立一個半徑 3 格的區域。context.zones:watch(handle, function() end, nil) 啟用 on_zone_enter,當任何玩家踏入時觸發。

  2. 驚嚇效果 -- on_zone_enter 鉤子播放 "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_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
}

逐步講解

  1. 狀態初始化 -- on_equip 設定 wave_active = false 以防止多個重疊的衝擊波。此鉤子在玩家裝備劍時觸發。

  2. 方向圓錐 -- 玩家的 yaw 被轉換為方向向量(dir_xdir_z)。玩家前方的 70 度圓錐決定波的擴散範圍。

  3. 擴張波 -- scheduler:run_repeating() 每 2 ticks 執行一次。每個 tick,波半徑增加 WAVE_SPEED。粒子會在環形中生成,但僅在圓錐角度內。

  4. 地形破壞 -- get_highest_block_y() 在每個點找出地面高度。表面方塊會被破壞(設為 AIR),排除基岩/黑曜石/屏障。destroyed_blocks 表防止重複處理。

  5. 實體目標 -- get_nearby_entities() 找出原點附近的所有實體。腳本檢查每個實體是否在當前波帶內、在圓錐內,且尚未被擊中。if entity.damage then 守衛至關重要——get_nearby_entities() 回傳所有實體(盔甲架、物品等),而非僅有生物。

  6. 戰鬥效果 -- 被擊中的實體會受到傷害、被往波的方向擊退,並獲得 3 秒的緩慢 II。

  7. 玩家回饋 -- show_action_bar() 顯示「Frost Shockwave!」40 ticks。玻璃破碎聲隨波前推進。

  8. 清理 -- 當半徑超過 WAVE_MAX_RADIUS 時,重複任務會取消自己並重設 wave_active 旗標。

實體類型安全

在呼叫 entity:damage()entity:push()entity:add_potion_effect() 之前,務必檢查 if entity.damage thenget_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
}

逐步講解

  1. 冷卻模式 -- context.cooldowns:check_local("heal", COOLDOWN_TICKS) 在一次呼叫中檢查並啟動充能冷卻。物品本地冷卻會在裝備和卸下循環之間持續存在。

  2. 充能系統 -- context.item:get_uses() 從物品的 PDC 讀取自訂使用計數器。每次使用透過 set_uses() 遞減。當充能歸 0 時,魔杖播放熄滅聲並拒絕啟動。

  3. 治療 -- 腳本不直接設定生命值,而是套用 2 秒的恢復 II。這與 Minecraft 的生命值系統更自然地配合,並能與其他效果正確堆疊。

  4. 事件取消 -- context.event:cancel() 防止右鍵與附近的方塊互動(放置方塊、開門等)。

  5. 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_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 與 Item 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.cooldowns:check_local("activate", 40) 實現點擊之間 2 秒的冷卻。


哥布林物品集合

一組以哥布林武器為主題的 10 個範例物品腳本可作為可下載的範例內容取得。它們並未捆綁在基礎外掛 jar 中——外掛本身在首次執行時唯一會寫入 scripts/ 的腳本是四個預製道具腳本:invulnerable.luapickupable.luastorage_single.luastorage_double.lua。下方每個哥布林腳本都展示了不同的戰鬥機制、粒子效果和 API 使用模式。

腳本物品效果
goblin_golden_sword.lua金劍25% 機率橫掃 -- 對所有附近敵人造成傷害並推開
goblin_iron_axe.lua鐵斧20% 機率尖刺 -- 將目標向上發射並帶有滴水石視覺效果
goblin_bow.lua將命中的怪物拉向玩家
goblin_crossbow.lua33% 機率煙火齊射
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

下一步