跳到主要内容

Lua 脚本:示例与模式

本页包含 FreeMinecraftModels 道具脚本和物品脚本的完整可运行示例,以及实用模式和最佳实践。每个示例都附有逐步讲解,说明其功能和原因。

如果您刚接触脚本,请从 入门指南 开始。完整的 API 详情请参阅 道具与物品 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() 创建一个以道具为中心的球形区域。返回的句柄是一个数字 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 取消重复任务并取消区域监听。虽然在道具被移除时两者都会自动清理,但显式清理是一种最佳实践。

粒子性能

每刻生成大量粒子会影响性能。请使用合理的间隔(10-20 刻)并保持较低的粒子数量。上面的示例使用 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.statescheduler: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 表示动画从头开始播放,不从之前的动画混合过渡。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 刻是 30 秒,等级 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. 优雅回退 -- 如果未安装 EliteMobs 或 Boss 文件不存在,spawn_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. 方向锥 -- 玩家的偏航角被转换为方向向量(dir_xdir_z)。玩家前方 70 度的锥形决定了波的扩散范围。

  3. 扩张的波 -- scheduler:run_repeating() 每 2 刻运行一次。每刻波的半径增长 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 刻。玻璃破碎声随波前推进。

  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() 将其递减。当充能归零时,法杖播放失败声并拒绝激活。

  3. 治疗 -- 脚本不是直接设置生命值,而是施加 2 秒的再生 II。这与 Minecraft 的生命系统配合得更自然,并能与其他效果正确叠加。

  4. 事件取消 -- context.event:cancel() 防止右键与附近方块交互(放置方块、开门等)。

  5. UI 反馈 -- show_action_bar() 告诉玩家其操作的结果和剩余充能。针对冷却、充能耗尽和成功治疗的不同消息让物品手感灵敏。

获取自定义物品

使用 /fmm giveitem <id> 来获取一个被正确标记的自定义物品。通过其他方式创建的物品可能缺少 fmm_item_id PDC 键,这意味着脚本不会为它们激活。


最佳实践

  • 从一个极小的钩子开始并验证。 写一个仅发送日志消息的 on_spawn。确认它触发。然后再继续构建。

  • 保持辅助函数为局部。local function toggle_door(context) 这样的辅助函数应声明在 return 表的上方。这能让它们远离全局作用域。

  • on_spawn 中初始化所有状态。 如果你在 on_right_click 中读取 context.state.is_open 却从未在 on_spawn 中设置它,它将是 nil,你的比较可能会出现意外行为。

  • 完成后取消重复任务。 每个 run_repeating 都应在 on_destroy 中有对应的 cancel。泄漏的任务会浪费 CPU。

  • 使用全新的调度器回调上下文。 调度器回调会接收一个全新的 context 参数。在回调内部请始终使用该参数,而不是外层的 context

  • 保持 on_game_tick 轻量。 如果你定义了这个钩子,它会在每个服务器刻运行(每秒 20 次)。请把昂贵的工作用 context.cooldowns 或调度器间隔加以限制。

  • 默认让道具无敌。 除非你希望道具可被破坏,否则请在每个脚本中包含 on_left_click 的伤害取消。

  • Bukkit 枚举使用 UPPER_CASE。 声音名称和粒子名称必须使用 Bukkit 枚举常量格式(例如 "FLAME",而不是 "flame")。


初学者常见错误

  • 在调度器回调内使用外层 context 外层 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 参数,而不是外层的 context
  7. on_game_tick 钩子用检查来限制昂贵的工作。
  8. 所有方法名都存在于 道具与物品 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
  • 只使用有文档记载的方法名 -- 如果它不在道具 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长矛沿瞄准方向发出火焰光束(潜行+右键)
goblin_shield.lua盾牌格挡时形成玻璃穹顶(被动反应)
goblin_iron_sword.lua铁剑蓄力的黑暗之环(潜行+右键)
无需模型

这些脚本适用于外观为原版的物品。每个脚本只需一个带 material: 字段的 YML 配置 —— 不需要 .bbmodel 文件。例如,要启用金剑脚本,可创建一个这样的 YML 配置:

isEnabled: true
material: GOLDEN_SWORD
scripts:
- goblin_golden_sword.lua

后续步骤