Lua 脚本:示例与模式
本页包含 FreeMinecraftModels 道具脚本的完整可运行示例,以及实用模式和最佳实践。每个示例都附有详细说明,解释其功能和原因。
如果您刚接触道具脚本,请从入门指南开始。完整的 API 详情请参阅道具 API。
示例:无敌道具
本示例教您: 最简单的实用脚本——取消伤害,使道具无法被破坏。
这是 FreeMinecraftModels 自带的预制脚本。
完整脚本文件(点击展开)
return {
api_version = 1,
on_left_click = function(context)
if context.event then
context.event.cancel()
end
end
}
逐步讲解
-
钩子选择 --
on_left_click在玩家攻击(左键点击)道具时触发。底层是道具支撑盔甲架上的EntityDamageByEntityEvent。 -
事件守卫 --
context.event在此钩子中应该始终存在,但添加守卫检查是一种好习惯。 -
取消 --
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
}
逐步讲解
-
文件作用域常量 --
OPEN_ANIMATION和CLOSE_ANIMATION定义在 return 表的上方。这样可以方便地针对使用不同动画名称的不同模型文件进行修改。 -
状态初始化 --
on_spawn设置context.state.is_open = false。状态在此道具实例的所有钩子之间保持。 -
无敌 --
on_left_click钩子取消伤害,防止门被意外破坏。 -
切换逻辑 --
on_right_click检查context.state.is_open,停止当前动画,播放相应的动画,翻转状态,并播放声音。在play_animation()之前调用stop_animation()确保干净的过渡。 -
声音反馈 --
context.world:play_sound()在道具位置播放 Bukkit Sound 枚举名称。声音名称必须使用 UPPER_CASE 枚举名。
"open" 和 "close" 等动画名称必须与模型文件中定义的一致。如果找不到动画,play_animation() 返回 false 且不会执行任何操作。请检查您的模型文件以确认准确的动画名称。
示例:带粒子效果的接近触发器
本示例教您: 区域创建、区域进入/离开事件监听、粒子效果和清理。
完整脚本文件(点击展开)
local ZONE_RADIUS = 8
local PARTICLE_INTERVAL = 10 -- ticks between particle bursts
return {
api_version = 1,
on_spawn = function(context)
context.state.zone_handle = nil
context.state.particle_task = nil
context.state.players_in_zone = 0
local loc = context.prop.current_location
if loc == nil then return end
-- Create a sphere zone around the prop
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, ZONE_RADIUS)
context.state.zone_handle = handle
-- Watch for enter/leave
context.zones:watch(
handle,
function(player)
-- on_enter
context.state.players_in_zone = (context.state.players_in_zone or 0) + 1
end,
function(player)
-- on_leave
context.state.players_in_zone = math.max(0, (context.state.players_in_zone or 0) - 1)
end
)
-- Start a repeating particle effect at the zone boundary
context.state.particle_task = context.scheduler:run_repeating(0, PARTICLE_INTERVAL, function(tick_context)
local prop_loc = tick_context.prop.current_location
if prop_loc == nil then return end
-- Spawn particles in a ring at the zone boundary
for angle = 0, 350, 30 do
local rad = math.rad(angle)
local px = prop_loc.x + math.cos(rad) * ZONE_RADIUS
local pz = prop_loc.z + math.sin(rad) * ZONE_RADIUS
if (tick_context.state.players_in_zone or 0) > 0 then
-- Red particles when players are inside
tick_context.world:spawn_particle("DUST", px, prop_loc.y + 0.5, pz, 1, 0, 0, 0, 0)
else
-- Green particles when zone is empty
tick_context.world:spawn_particle("HAPPY_VILLAGER", px, prop_loc.y + 0.5, pz, 1, 0, 0, 0, 0)
end
end
end)
end,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_destroy = function(context)
-- Clean up the repeating task
if context.state.particle_task then
context.scheduler:cancel(context.state.particle_task)
context.state.particle_task = nil
end
-- Clean up the zone watch
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}
逐步讲解
-
常量 --
ZONE_RADIUS和PARTICLE_INTERVAL位于文件作用域,便于调整。 -
状态初始化 --
on_spawn在执行其他操作之前,将所有状态字段设置为nil/0。 -
区域创建 --
context.zones:create_sphere()创建以道具为中心的球形区域。返回的句柄是一个数字 ID,用于后续引用该区域。 -
区域监听 --
context.zones:watch()注册玩家进入和离开的回调。回调增减存储在context.state中的计数器。 -
粒子循环 -- 重复任务每半秒在道具周围以环形生成粒子。粒子类型根据区域内是否有玩家而变化。
-
清理 --
on_destroy取消重复任务并取消区域监听。虽然在道具被移除时两者都会自动清理,但显式清理是最佳实践。
每个刻生成大量粒子可能影响性能。使用合理的间隔(10-20 刻),并保持粒子数量较低。上面的示例使用 PARTICLE_INTERVAL = 10(每秒两次),每环仅 12 个粒子。
示例:发声道具
本示例教您: 交互时播放声音、使用 state 和 scheduler 实现冷却行为,以及防止连续快速交互。
完整脚本文件(点击展开)
local SOUND_NAME = "BLOCK_NOTE_BLOCK_HARP"
local COOLDOWN_TICKS = 40 -- 2 seconds between sounds
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
-- Prevent spam
if context.state.on_cooldown then
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Play the sound
context.world:play_sound(SOUND_NAME, loc.x, loc.y, loc.z, 1.0, 1.0)
-- Show some particles
context.world:spawn_particle("NOTE", loc.x, loc.y + 1.5, loc.z, 5, 0.3, 0.3, 0.3, 0)
-- Set cooldown
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}
逐步讲解
-
冷却模式 -- 由于 FMM 道具脚本没有像 EliteMobs 那样的内置
context.cooldownsAPI,此示例使用context.state.on_cooldown和scheduler:run_later()实现简单的冷却。播放声音时标记设为true,延迟任务在COOLDOWN_TICKS后将其重置。 -
声音播放 --
context.world:play_sound()接受 UPPER_CASE 的 Bukkit Sound 枚举名称、坐标、音量和音调。 -
粒子反馈 -- 声音播放时,道具上方会出现音符粒子,提供视觉提示。
-
无敌 --
on_left_click钩子照常取消伤害。 -
调度器回调上下文 --
run_later回调接收later_context,一个新的上下文。我们使用later_context.state(而不是context.state)来重置冷却标记。由于状态是共享的,两者指向同一个表——但使用回调的上下文参数是正确的习惯。
FMM 道具脚本没有 EliteMobs 的 context.cooldowns API。请使用此处展示的模式:在 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表示不与之前的动画混合直接开始。loop 的true表示无限重复。 -
氛围粒子 -- 重复任务每 2 秒在道具上方生成附魔台粒子,营造魔法氛围效果。
-
清理 --
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
}
逐步讲解
-
右键坐下 --
on_right_click从context.event.player获取玩家,检查他们是否已经是乘客(避免重复挂载),然后调用context.prop:mount(player)将其坐在道具的盔甲架上。 -
左键站起 --
on_left_click取消伤害事件(无敌),然后检查攻击的玩家是否当前是乘客。如果是,context.prop:dismount(player)将其弹出。 -
乘客检查 --
context.prop:get_passengers()返回实体表数组。我们比较 UUID 来在列表中找到交互的玩家。 -
声音反馈 -- 坐下时播放木头放置声音,站起时播放木头破坏声音,提供触觉反馈。
示例:祝福神殿
本示例教您: 检查玩家手持物品、消耗物品、施加随机药水效果、冷却管理以及粒子/声音反馈。
完整脚本文件(点击展开)
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
}
逐步讲解
-
物品检查 --
player:get_held_item()返回主手物品的type、amount和display_name表(如果为空则返回nil)。我们将held.type与"gold_ingot"(小写材料名称)进行比较。 -
物品消耗 --
player:consume_held_item(1)从玩家主手中移除一个物品。 -
随机增益 -- 文件作用域的
BLESSINGS表列出了可用的正面效果。math.random(#BLESSINGS)随机选择一个。player:add_potion_effect(effect, duration, amplifier)应用效果——600刻为 30 秒,amplifier1为 II 级。 -
冷却 -- 与发声道具示例中相同的布尔标记加调度器模式。30 秒的冷却防止频繁使用神殿。
-
反馈 -- 附魔和村民高兴粒子加上信标激活声音营造出"神圣祝福"的感觉。
示例:诅咒神殿
本示例教您: 负面药水效果、闪电打击、实体生成以及基于玩家供品的分支逻辑。
完整脚本文件(点击展开)
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
}
逐步讲解
-
基于供品的分支 -- 脚本检查
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。效果名称是与 Bukkit 的PotionEffectType名称匹配的小写字符串。 -
奖励路径 -- 当提供金子时,神殿消耗一个金锭并施加随机正面效果,与祝福神殿的行为相同。
-
冷却 -- 无论走哪条分支,都会施加 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
}
逐步讲解
-
状态守卫 --
context.state.is_spinning防止多个重叠的旋转请求。旋转开始时设置标记,调度的停止触发时清除标记。 -
定时动画 --
play_animation(SPIN_ANIMATION, true, false)播放一次动画(不循环)。scheduler:run_later(100, ...)调用在恰好 5 秒后停止动画,以防动画本身更长或正在循环。 -
机械声音 --
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
context.zones:watch(
handle,
function(player)
-- on_enter: trigger the scare
if context.state.on_cooldown then
return
end
local scare_loc = context.prop.current_location
if scare_loc == nil then return end
-- Play the jumpscare animation
context.prop:stop_animation()
context.prop:play_animation(SCARE_ANIMATION, false, false)
-- Scary sound
context.world:play_sound(
"ENTITY_GHAST_SCREAM",
scare_loc.x, scare_loc.y, scare_loc.z,
1.0, 0.7
)
-- Burst of smoke particles
context.world:spawn_particle(
"CAMPFIRE_SIGNAL_SMOKE",
scare_loc.x, scare_loc.y + 1, scare_loc.z,
20, 0.5, 0.5, 0.5, 0.05
)
-- Set cooldown so it does not trigger again immediately
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end,
nil -- no on_leave callback needed
)
end,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_destroy = function(context)
-- Clean up the zone watch
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}
逐步讲解
-
接近区域 --
context.zones:create_sphere()创建 3 格半径的区域。context.zones:watch()注册进入回调,当任何玩家踏入时触发。 -
惊吓效果 -- 进入回调播放
"jumpscare"动画、恶魂尖叫声音,并生成营火烟雾粒子。这些组合创造出突然的、令人惊恐的效果。 -
60 秒冷却 -- 布尔标记模式防止惊吓反复触发。一旦触发,道具会沉默 60 秒(
COOLDOWN_TICKS = 1200),然后重新启用。 -
无离开回调 --
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)
-- 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
}
逐步讲解
-
Boss 生成 --
context.prop:spawn_elitemobs_boss(filename, x, y, z)在给定坐标生成一个 EliteMobs 自定义 Boss。文件名必须与 EliteMobs 的custombosses文件夹中的.yml文件匹配。 -
优雅降级 --
spawn_elitemobs_boss()在 EliteMobs 未安装或 Boss 文件不存在时返回nil。脚本通过警告日志消息、熄灭粒子效果和玩家消息来处理这种情况。 -
生成偏移 -- Boss 生成在
loc.y + 1(道具上方一格),防止 Boss 卡入道具或地面。 -
冷却 -- 10 秒的冷却防止玩家在区域内淹没哥布林战士。可根据您的游戏需求调整
COOLDOWN_TICKS。 -
视觉/音频区分 -- 成功时使用火焰粒子和唤魔者召唤声音来实现戏剧性的生成效果。失败时使用烟雾和灭火声音来清晰地表示"熄灭"效果,让玩家知道出了问题而无需检查控制台。
Boss 文件名(例如 "goblin_warrior.yml")必须对应 EliteMobs 中现有的自定义 Boss 配置。如果您正在分发使用此脚本的地图或地下城,请包含 Boss 配置文件并记录 EliteMobs 的依赖。
最佳实践
-
从小钩子开始并验证。 写一个只发送日志消息的
on_spawn。确认它触发了,然后在此基础上构建。 -
保持辅助函数为局部。 在 return 表上方声明辅助函数,如
local function toggle_door(context)。这使它们不会进入全局作用域。 -
在
on_spawn中初始化所有状态。 如果您在on_right_click中读取context.state.is_open但从未在on_spawn中设置它,它将是nil,您的比较可能会产生意外行为。 -
完成后取消重复任务。 每个
run_repeating都应在on_destroy中有对应的cancel。泄露的任务会浪费 CPU。 -
使用调度器回调的新上下文。 调度器回调接收新的上下文参数。始终在回调内使用该参数,而不是外部的
context。 -
保持
on_game_tick轻量。 如果您定义了此钩子,它每个服务器刻都会运行(每秒 20 次)。将开销大的工作放在基于状态的冷却检查之后。 -
默认使道具无敌。 除非您希望道具可以被破坏,否则在每个脚本中都包含
on_left_click的伤害取消。 -
Bukkit 枚举使用 UPPER_CASE。 声音名称和粒子名称必须使用 Bukkit 枚举常量格式(例如
"FLAME",而不是"flame")。
常见新手错误
-
在调度器回调中使用外部
context。 外部上下文捕获的是钩子运行时的快照。在回调内部,始终使用回调自身的参数。 -
忘记取消重复任务。 如果您在
on_spawn中启动了run_repeating但从未取消它,任务会一直运行直到道具被移除。 -
未在
on_spawn中初始化状态。 在设置之前读取context.state.x会返回nil,这可能会无声地破坏您的逻辑。 -
错误的动画名称。 如果
play_animation("open")返回false,说明动画名称与模型文件中的不匹配。检查模型以确认准确的名称。 -
声音/粒子名称使用小写。
"flame"不起作用——使用"FLAME"。API 内部会将粒子转换为 UPPER_CASE,但 Sound 枚举名称必须精确匹配。 -
忘记
api_version = 1。 返回的表必须包含此字段,否则 FMM 将不会加载该脚本。 -
在返回的表中放置非钩子函数。 辅助函数必须在
return语句上方声明。返回表中只允许使用钩子名称(on_spawn、on_right_click等)作为键。
QC 检查清单
在部署道具脚本之前,使用此检查清单进行验证:
- 文件恰好返回一个包含
api_version = 1的表。 - 每个钩子名称与钩子列表中的条目精确匹配。
- 在调用
cancel()之前,通过if context.event then检查了context.event。 context.state字段在on_spawn中初始化。- 每个
scheduler:run_repeating(...)调用在on_destroy中有对应的scheduler:cancel(...)。 - 调度器回调使用回调自身的上下文参数,而不是外部的
context。 on_game_tick钩子将开销大的工作放在检查之后。- 所有方法名称存在于道具 API 参考中——没有编造的别名。
- 声音和粒子名称使用 UPPER_CASE 的 Bukkit 枚举名称。
- 脚本不在钩子或回调中调用任何阻塞或长时间运行的操作。
AI 生成提示
如果您想让 AI 可靠地生成道具脚本,请确保提示词中包含:
- 准确的钩子名称 -- 例如
on_right_click,而不是"当玩家点击道具时"。 - 来自模型文件的动画名称 -- AI 无法猜测这些名称;请提供它们。
- Sound 枚举名称 -- 例如
"BLOCK_NOTE_BLOCK_HARP",而不是"竖琴声"。 - Particle 枚举名称 -- 例如
"FLAME",而不是"火焰粒子"。 - 道具是否应该无敌 -- 如果是,请包含带有
context.event.cancel()的on_left_click。 - 只使用文档中记录的方法名称 -- 如果不在道具 API 页面上,它就不存在。
良好的提示词示例
编写一个 FMM 道具脚本,右键点击时播放 "activate" 动画,使道具无敌,点击时在道具位置生成 FLAME 粒子,播放 BLOCK_LEVER_CLICK 声音,并使用 context.state 和 scheduler:run_later 实现点击之间 2 秒的冷却。