跳到主要内容

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() 在道具生成时的位置创建固定球形区域,数字句柄指向该区域。粒子环读取道具当前位置,因此应搭配静止道具使用,或在移动道具时更新区域。
  4. context.zones:watch(handle, function() end, function() end) 启用 on_zone_enter 和 on_zone_leave。钩子修改 context.state 中的计数器,也通过 context.player / context.event.player 提供穿过边界的玩家,便于编写玩家专属逻辑。
  5. 重复任务每 10 个服务器刻生成一圈粒子,在 20 TPS 下为半秒。区域内是否有玩家决定粒子类型。
  6. on_destroy 取消重复任务和区域监听。移除道具时系统也会自动清理,但显式清理是好习惯。
粒子性能

每刻生成大量粒子会影响性能。可使用 10 到 20 刻的间隔,并控制数量。本例 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) 检查冷却,可用时立即启动。同一道具的此脚本绑定由所有玩家共享冷却。这里每刻代表 50 ms 的经过时间;调度器延迟则按实际服务器刻计数。
  2. 声音接受不区分大小写的 Bukkit 枚举名称、minecraft:block.note_block.harp 等有效键及自定义资源包声音键。客户端必须有对应声音。
  3. 播放声音时,道具上方出现音符粒子作为提示。
  4. on_left_click 取消本次点击的默认伤害。
冷却实现

普通冷却使用 context.cooldowns。只有在延迟结束时需要自定义状态变更,才使用 context.state 配合 scheduler:run_later()。


示例:动画环境道具​

学习内容: 生成时启动循环动画,并按刻间隔生成环境粒子。

完整脚本文件(点击展开)
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 刻为 30 秒,amplifier 1 为 II 级。
  4. 此脚本绑定的所有玩家共享一个标志,600 个服务器刻后重置,在 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 个服务器刻后重置,在 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 个服务器刻执行,不是从实际播放开始计时。队列忙碌可能缩短可见旋转,甚至使其无法显示;共享引擎也不会停止正在播放的 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 个服务器刻后重置,在 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 个服务器刻一次的尝试,在 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 轻量。 它每个服务器刻执行,在 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: 已不能启用物品脚本。

后续步骤​