Lua 脚本:示例与模式
本页介绍 FreeMinecraftModels 道具脚本的示例、写法和注意事项。使用前请准备示例引用的模型和动画,并在服务器上验证效果,再将其加入地图。
示例:无敌道具
学习内容: 用最小的脚本取消左键点击的默认伤害。
将此示例保存为脚本文件夹中的 invulnerable.lua,重新加载脚本后再将它绑定到道具。
完整脚本文件(点击展开)
return {
api_version = 1,
on_left_click = function(context)
if context.event then
context.event.cancel()
end
end
}
逐步讲解
- 玩家攻击道具时触发
on_left_click。Lua 事件是 FMM 默认道具伤害处理使用的取消标志。 - 此钩子应当始终提供
context.event,但先检查是否存在仍是好习惯。 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
}
逐步讲解
OPEN_ANIMATION和CLOSE_ANIMATION定义在返回的表外,方便按模型的动画名称调整。on_spawn将context.state.is_open初始化为false。状态在这个道具的同一脚本绑定内跨钩子保存,同一道具上的其他脚本各有自己的状态。on_left_click取消本次点击的默认伤害。on_right_click请求停止和播放相应动画,翻转标志并播放声音。标志记录请求的状态,不代表动画已播放。blend = true将动画排入队列,停止请求不能中断共享引擎正在播放的death。详见动画。此例不改变屏障碰撞,也不保证门口可以通行。- 声音接受不区分大小写的 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
}
逐步讲解
ZONE_RADIUS和PARTICLE_INTERVAL是文件级常量,方便调整。on_spawn先将状态字段初始化为nil或0。context.zones:create_sphere()在道具生成时的位置创建固定球形区域,数字句柄指向该区域。粒子环读取道具当前位置,因此应搭配静止道具使用,或在移动道具时更新区域。context.zones:watch(handle, function() end, function() end)启用on_zone_enter和on_zone_leave。钩子修改context.state中的计数器,也通过context.player/context.event.player提供穿过边界的玩家,便于编写玩家专属逻辑。- 重复任务每 10 个服务器刻生成一圈粒子,在 20 TPS 下为半秒。区域内是否有玩家决定粒子类型。
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
}
逐步讲解
context.cooldowns:check_local("sound", COOLDOWN_TICKS)检查冷却,可用时立即启动。同一道具的此脚本绑定由所有玩家共享冷却。这里每刻代表 50 ms 的经过时间;调度器延迟则按实际服务器刻计数。- 声音接受不区分大小写的 Bukkit 枚举名称、
minecraft:block.note_block.harp等有效键及自定义资源包声音键。客户端必须有对应声音。 - 播放声音时,道具上方出现音符粒子作为提示。
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
}
逐步讲解
on_spawn立即启动"idle"。blend 为false表示立即切换。内置idle状态始终循环,不受传入的 loop 参数影响。- 重复任务每 2 秒在道具上方生成附魔台粒子,营造魔法效果。
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
}
逐步讲解
on_right_click从context.event.player获取玩家,确认尚未就座后,通过context.prop:mount(player)请求第一个空闲m_挂载点座位。on_left_click取消这次点击的默认伤害,若玩家正在乘坐,则调用context.prop:dismount(player)将其移下。context.prop:get_passengers()返回实体表数组,通过 UUID 找到交互玩家。- 声音伴随排队的就座和起身请求,并非确认操作完成。右键钩子取消 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
}
逐步讲解
player:get_held_item()返回主手物品的type、amount、display_name,空手则返回nil。held.type是大写 Bukkit 材质名,因此与"GOLD_INGOT"比较。player:consume_held_item(1)将主手物品移除任务排入队列,执行时才读取主手,不返回付款结果。本例不是原子交易。玩家换手持物品可能改变消耗对象,而祝福立即施加。- 文件级
BLESSINGS表列出正面效果,math.random(#BLESSINGS)随机选择。player:add_potion_effect(effect, duration, amplifier)施加效果,600刻为 30 秒,amplifier1为 II 级。 - 此脚本绑定的所有玩家共享一个标志,600 个服务器刻后重置,在 20 TPS 下为 30 秒。按经过时间计时的冷却可使用发声道具示例中的
context.cooldowns。 - 附魔和快乐村民粒子配合信标激活声,表现祝福效果。
示例:诅咒神龛
学习内容: 负面药水效果、雷击、生成实体,以及根据供奉物品选择分支。
完整脚本文件(点击展开)
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
}
逐步讲解
- 脚本检查
player:get_held_item(),无金锭时惩罚,有金锭时奖励。 context.world:strike_lightning(x, y, z)在player.current_location处产生会造成伤害的真实雷击。context.world:spawn_entity("zombie", x, y, z)在神龛周围生成一圈普通僵尸。移除道具不会移除这些生物。本例不检查地形,也不限制仍存活的数量。player:add_potion_effect("poison", 400, 1)施加 20 秒的中毒 II。效果名称是对应 BukkitPotionEffectType的小写名称。- 奖励分支将主手消耗排入队列并施加随机正面效果。与祝福神龛一样,这只是演示,不是付款和奖励的原子交易。
- 两个分支都设置同一脚本绑定内所有玩家共享的标志,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
}
逐步讲解
context.state.is_spinning防止重复的旋转请求,开始时设置,定时停止时清除。play_animation(SPIN_ANIMATION, true, false)通过队列请求非循环动画。停止任务在点击后 100 个服务器刻执行,不是从实际播放开始计时。队列忙碌可能缩短可见旋转,甚至使其无法显示;共享引擎也不会停止正在播放的death。本例在动画请求返回false时仍设置标志。BLOCK_CHAIN_PLACE提供机械启动声,BLOCK_CHAIN_FALL提供停止声,可调整音调。run_later回调中的状态和世界访问均使用later_context,不使用外层context。
"spin" 必须与模型定义一致。若模型使用 "rotate" 或 "turn",请修改 SPIN_ANIMATION。
示例:惊吓道具
学习内容: 区域触发、长冷却,以及组合声音、粒子和动画制造惊吓。
完整脚本文件(点击展开)
local SCARE_RADIUS = 3
local COOLDOWN_TICKS = 1200 -- 60 seconds between scares
local SCARE_ANIMATION = "jumpscare"
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
context.state.zone_handle = nil
local loc = context.prop.current_location
if loc == nil then return end
-- Create a small sphere zone around the prop
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, SCARE_RADIUS)
context.state.zone_handle = handle
-- Watch for players entering the zone. The actual scare logic
-- lives in on_zone_enter below.
context.zones:watch(handle, function() end, nil)
end,
on_zone_enter = function(context)
if context.state.on_cooldown then
return
end
local scare_loc = context.prop.current_location
if scare_loc == nil then return end
-- Play the jumpscare animation
context.prop:stop_animation()
context.prop:play_animation(SCARE_ANIMATION, false, false)
-- Scary sound
context.world:play_sound(
"ENTITY_GHAST_SCREAM",
scare_loc.x, scare_loc.y, scare_loc.z,
1.0, 0.7
)
-- Burst of smoke particles
context.world:spawn_particle(
"CAMPFIRE_SIGNAL_SMOKE",
scare_loc.x, scare_loc.y + 1, scare_loc.z,
20, 0.5, 0.5, 0.5, 0.05
)
-- Set cooldown so it does not trigger again immediately
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end,
on_left_click = function(context)
-- 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
}
逐步讲解
context.zones:create_sphere()在生成位置创建半径 3 格的固定区域,context.zones:watch(handle, function() end, nil)启用on_zone_enter。移动道具时也要移动或重建区域。on_zone_enter播放"jumpscare"动画、恶魂尖叫并生成营火烟雾,制造突然的惊吓。- 所有玩家共享此脚本绑定的标志。1200 个服务器刻后重置,在 20 TPS 下为 60 秒;之后下一次进入区域可再次触发。
context.zones:watch()第三个参数为nil,表示不处理玩家离开。on_destroy取消区域监听。移除道具时也会自动清理,但显式清理是好习惯。
把道具藏在拐角或暗处。半径 3 格让玩家靠近后才触发,可按需调整 SCARE_RADIUS 和 COOLDOWN_TICKS。
示例:哥布林刷怪道具
学习内容: 通过 prop:spawn_elitemobs_boss() 在交互时生成自定义 Boss,并处理 EliteMobs 不可用的情况。
完整脚本文件(点击展开)
local BOSS_FILE = "goblin_warrior.yml"
local COOLDOWN_TICKS = 200 -- 10 seconds between spawns
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- 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
}
逐步讲解
context.prop:spawn_elitemobs_boss(filename, x, y, z)在指定坐标生成 EliteMobs 自定义 Boss。文件名须匹配 EliteMobscustombosses文件夹中的.yml文件。- 集成功能无法返回活着的 Boss 时返回
nil,包括插件不可用、配置缺失或生成失败。示例消息建议检查 EliteMobs,不代表已确定具体原因。 - 请求的位置在道具上方一格。这不是地形或碰撞检查,请为 Boss 选择合适的空地。
- 同一脚本绑定的所有玩家共享每 200 个服务器刻一次的尝试,在 20 TPS 下为 10 秒。它不限制存活 Boss 数,道具消失也不会移除 Boss。用于重复玩法时应增加遭遇数量等限制。
- 成功时使用火焰和唤魔者召唤声,失败时使用烟雾和灭火声,让玩家无需查看控制台也能区分结果。
"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 检查清单
部署道具脚本前请检查:
- 文件只返回一个包含
api_version = 1的表。 - 所有钩子名准确匹配道具钩子列表。附魔钩子属于另一个运行环境。
- 在已知点击钩子以外取消事件前,检查
context.event和context.event.cancel。 context.state字段在on_spawn初始化。- 每个
scheduler:run_repeating(...)都在on_destroy有对应scheduler:cancel(...)。 - 调度回调使用自己的上下文参数,不使用外层
context。 on_game_tick对高开销工作设有条件检查。- 方法名均存在于道具 API参考中,没有编造的别名。
- 粒子名接受
FLAME和flame,API 统一大小写后查找 Bukkit 粒子。声音接受不区分大小写的 Bukkit 枚举名称、minecraft:block.note_block.harp等有效键及自定义资源包声音键。客户端必须有对应声音。 - 钩子和回调内没有阻塞或长时间运行的操作。
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: 已不能启用物品脚本。