Lua 脚本:示例与模式
本页提供完整的 EliteMobs Lua 能力示例及说明。将每个示例保存为独立的 .lua 文件,附加到测试 Boss;编辑后运行 /em reload,再生成一个新的 Boss。slam、roar、frenzy 和 transform 等动画名必须存在于 Boss 的自定义模型中,它们不是内置动画。没有自定义模型的 Boss 会跳过这些动画调用。下文时间均按 20 TPS 计算。
如果你是 Lua 能力的新手,请从入门指南开始。NPC 脚本请参阅 NPC 脚本。完整 Boss 能力 API 详情请参阅 API 参考、Boss 与实体、世界与环境、区域与目标选择和枚举。
示例:使用脚本工具进行基于区域的目标选择
本示例教授: 如何使用 context.script 从 Lua 创建 EliteScript 风格的区域几何,生成粒子并对区域内的实体造成伤害。
完整能力文件(点击展开)
return {
api_version = 1,
on_boss_damaged_by_player = function(context)
local cone = context.script:zone({
shape = "CONE",
Target = {
targetType = "SELF",
offset = "0,1,0"
},
Target2 = {
targetType = "NEARBY_PLAYERS",
range = 20
},
radius = 5
})
context.script:spawn_particles(
cone:full_target(0.4),
{
particle = "FLAME",
amount = 1,
speed = 0.05
}
)
context.script:damage(cone:full_target(), 1.0, 1.5)
end
}
说明
- 区域创建 --
context.script:zone(...)使用与 EliteScript 区域相同的字段名。Target将圆锥顶点设在 Boss 上方一格。解析出的第一个Target2位置决定底面中心,因此也决定圆锥长度;radius = 5设置底面半径。NEARBY_PLAYERS搜索沿各轴延伸 20 格的长方体,不保证返回最近的玩家。在此伤害钩子中,要瞄准攻击者可使用DIRECT_TARGET。没有找到玩家就不会生成圆锥;两个端点必须位于同一世界且位置不同。 - 粒子生成 --
cone:full_target(0.4)返回目标句柄,每次调用以 40% 概率随机选择区域中的各个点。不保证点数恰好占 40%,也不会筛选伤害接收者。粒子配置使用 EliteScript 的particle、amount和speed字段。粒子工具会将采样区域位置沿 X、Z 轴各偏移半格,因此粒子并不精确表示实体命中边界。 - 伤害 --
context.script:damage(cone:full_target(), 1.0, 1.5)命中圆锥内解析出的玩家,因为区域默认过滤器是PLAYER。第一个数字(1.0)是提交的基础伤害,第二个(1.5)是对玩家应用的 EliteMobs 伤害倍率。最终伤害由战斗处理流程决定。
此示例在每次分派的命中事件中执行,没有本地或全局冷却,粒子与伤害之间也没有预警延迟。与其他攻击组合时,请显式检查冷却。每次静态区域查询都会重新解析目标;Lua 区域句柄不会自动应用 YAML 动作条件或目标缓存。
传递给 context.script 的区域和目标表使用 EliteScript 字段名(targetType、shape、Target、Target2、range、offset、coverage)。完整列表请参阅 EliteScript 区域和 EliteScript 目标。
示例:状态 + 调度器攻击循环
本示例教授: 使用 context.state 跟踪运行时状态,context.scheduler 执行重复任务,以及正确的战斗进入/退出生命周期。
完整能力文件(点击展开)
local function pick_action(context)
local roll = math.random(1, 2)
if roll == 1 then
context.boss:play_model_animation("slam")
else
context.boss:play_model_animation("roar")
end
end
return {
api_version = 1,
on_spawn = function(context)
context.state.started = false
context.state.loop_task_id = nil
end,
on_enter_combat = function(context)
if context.state.started then
return
end
context.state.started = true
context.state.loop_task_id = context.scheduler:run_every(100, function(loop_context)
if loop_context.boss.exists then
pick_action(loop_context)
end
end)
end,
on_exit_combat = function(context)
if context.state.loop_task_id ~= nil then
context.scheduler:cancel_task(context.state.loop_task_id)
context.state.loop_task_id = nil
end
context.state.started = false
end
}
说明
- 状态初始化 --
on_spawn将context.state.started设为false,context.state.loop_task_id设为nil。state表在此 Boss 上此能力运行时的各个钩子之间保留。其他能力拥有独立的状态表。 - 战斗守卫 --
on_enter_combat在启动循环前检查context.state.started,防止多个重叠循环。 - 调度器模式 --
context.scheduler:run_every(100, callback)以零延迟安排首次调用,之后每 100 tick(5 秒)重复一次,并不会先等待五秒。若需要首次延迟,使用run_repeating(100, 100, callback)。回调接收不含触发事件或玩家的新上下文,因此loop_context.boss是新快照。将返回的数字任务 ID 保存到状态中。此例只选择动画,本身不造成伤害。 - 退出时清理 --
on_exit_combat使用保存的任务 ID 取消重复任务并重置状态。不清理的话,调度器会在战斗结束后继续运行。
在 on_exit_combat 中取消仅限战斗的重复任务。否则战斗结束后它们仍会被调度,直到被取消或能力运行时关闭。暂停的运行时会跳过回调但保留任务。未捕获的回调错误会关闭运行时并取消其拥有的任务。
示例:命中时的火焰效果
本示例教授: 简单的"命中时应用效果"能力 -- 战斗能力最常见的模式。
完整能力文件(点击展开)
return {
api_version = 1,
on_player_damaged_by_boss = function(context)
-- Guard: the player may be nil in edge cases
if context.player == nil then
return
end
if not context.cooldowns:global_ready() then
return
end
-- Check and set a 60-tick (3 second) local cooldown in one call
if not context.cooldowns:check_local("fire_touch", 60) then
return
end
-- Coordinate with other powers that check the shared cooldown
context.cooldowns:set_global(40)
-- Set the player on fire for 60 ticks (3 seconds)
context.player:set_fire_ticks(60)
-- Visual feedback: spawn flame particles at the player's location
context.world:spawn_particle_at_location(
context.player.current_location,
{ particle = "FLAME", amount = 20, speed = 0.1 }
)
-- Tell the player what happened
context.player:send_message("&cThe boss's touch burns!")
end
}
说明
- Nil 检查 --
context.player来自当前事件的玩家参与者或直接目标。使用前务必检查,尤其是在将代码改用于其他钩子时。调度器回调不会继承此玩家。 - 本地冷却 -- 检查
global_ready()后,context.cooldowns:check_local("fire_touch", 60)检查该键,可用时立即设置 60 tick 冷却;否则返回false并退出钩子。此键由同一 Boss 上的能力共享,不同 Boss 相互独立。组合示例时请添加能力名称前缀。 - 燃烧时间 --
context.player:set_fire_ticks(60)先检查玩家是否有效、存活、在线、与来源位于同一世界,以及在副本实例内是否为当前参与者,然后将 Bukkit 燃烧时间设为 60 tick(3 秒)。它替换现有时长,而不是增加 60 tick。其他游戏机制可能灭火;消息和粒子不能证明效果已获准应用。 - 粒子 --
context.world:spawn_particle_at_location(location, spec)在指定位置生成粒子。配置表接受particle(Bukkit 粒子枚举名)、amount和speed。 - 消息 --
context.player:send_message(text)发送彩色聊天消息。&c(红色)等标准 Minecraft 颜色代码会自动处理。 - 全局冷却 --
context.cooldowns:set_global(40)在应用效果前将 Boss 的共享冷却设为 40 tick(2 秒)。此例先检查它,再消耗本地冷却。其他 Lua 钩子和回调若也应等待,必须调用global_ready();设置全局冷却不会暂停所有 Lua 回调。
示例:使用原生 Lua 区域的基于区域的 AoE 能力
本示例教授: 创建和查询原生 Lua 区域,对区域内的玩家造成伤害。
完整能力文件(点击展开)
return {
api_version = 1,
on_spawn = function(context)
context.state.aoe_task_id = nil
end,
on_enter_combat = function(context)
-- Prevent duplicate loops
if context.state.aoe_task_id ~= nil then
return
end
context.state.aoe_task_id = context.scheduler:run_every(60, function(tick_context)
-- Make sure the boss is still alive
if not tick_context.boss.exists then
return
end
if not tick_context.cooldowns:global_ready() then
return
end
-- Check and set this boss's named cooldown before applying effects
if not tick_context.cooldowns:check_local("pulse_aoe", 60) then
return
end
tick_context.cooldowns:set_global(60)
-- Build a sphere zone centered on the boss's current position
local zone_def = {
kind = "sphere",
radius = 8,
origin = tick_context.boss:get_location()
}
-- Find all players inside the sphere
local victims = tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" })
-- Damage and show particles on each player found
for i = 1, #victims do
local victim = victims[i]
victim:deal_custom_damage(4.0)
tick_context.world:spawn_particle_at_location(
victim.current_location,
{ particle = "DUST", amount = 15, speed = 0, red = 128, green = 0, blue = 255 }
)
end
-- Spawn a particle burst at the boss; this does not draw the sphere
tick_context.world:spawn_particle_at_location(
tick_context.boss:get_location(),
{ particle = "WITCH", amount = 40, speed = 0.1 }
)
end)
end,
on_exit_combat = function(context)
if context.state.aoe_task_id ~= nil then
context.scheduler:cancel_task(context.state.aoe_task_id)
context.state.aoe_task_id = nil
end
end
}
说明
- 状态设置 --
on_spawn将aoe_task_id初始化为nil。这里将保存重复任务的调度器 ID。 - 重复攻击 --
on_enter_combat启动首次延迟为零、之后每 60 tick(3 秒)重复的任务。检查任务 ID 可防止重复循环。每次回调先检查全局冷却,再检查并设置pulse_aoe,然后在效果前设置 60 tick 全局冷却。这仅与检查这些键的能力协调,不会自动阻止所有脚本。 - 区域定义 --
zone_def使用原生 Lua 区域语法。kind指定形状("sphere"),radius设置大小,origin在回调执行时设为 Boss 的当前位置,因此区域会跟随 Boss 移动。 - 实体查询 --
tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" })根据形状与实体的相交判定返回从 1 开始的 Lua 数组,而不是精确检查脚部位置距离。查询搜索 Boss 当前世界,不按玩家游戏模式或遭遇参与情况过滤。过滤器接受"players"、"elites"、"mobs"或"living"(默认),并排除 Boss 自身。 - 造成伤害 --
victim:deal_custom_damage(4.0)通过BossCustomAttackDamage提交归属于 Boss 的 4 点基础伤害。它跳过无敌、死亡的目标和非生存或冒险模式玩家。在精英攻击玩家的路径上,它绕过正常伤害公式和格挡减伤,并清除原版护甲减伤。伤害监听器、取消事件和伤害吸收仍可改变实际生命损失。工具在提交后重置目标的原生伤害冷却;它与context.script:damage使用不同路径。 - 粒子反馈 -- 即使伤害被拒绝,仍会在每个查询到的玩家保存的位置显示紫色尘埃。Boss 位置的
WITCH粒子只是一次粒子喷发,不会描绘圆环或球体边界。这些简化粒子表默认位置散布为零,尘埃大小固定为1。 - 清理 --
on_exit_combat取消重复任务并清理状态,与前一个示例相同。
此示例使用原生 Lua 区域(context.zones:get_entities_in_zone()),接受包含 kind、radius、origin 等字段的简单表。脚本工具(context.script:zone(...))使用 EliteScript 字段名,如 shape、Target 和 Target2。简单形状使用原生区域,需要高级目标解析时使用 context.script。
示例:多阶段 Boss 机制
本示例教授: 使用状态跟踪 Boss 阶段并在生命值阈值处切换行为。
这些是 Lua 状态阶段,不是会替换 Boss 定义的 EliteMobs YAML phases:。此处不会替换或治疗 Boss。之后回血也不会返回阶段 1;离开战斗仍保留阶段 2,直到能力运行时被重新创建。
完整能力文件(点击展开)
local function phase_one_attack(context)
-- One damage pulse per callback; the animation does not delay it
context.boss:play_model_animation("slam")
local zone_def = {
kind = "sphere",
radius = 5,
origin = context.boss:get_location()
}
local targets = context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #targets do
targets[i]:deal_custom_damage(3.0)
end
context.world:spawn_particle_at_location(
context.boss:get_location(),
{ particle = "EXPLOSION", amount = 3, speed = 0 }
)
end
local function phase_two_attack(context)
-- One pulse per callback, repeated more frequently in phase 2
context.boss:play_model_animation("frenzy")
local zone_def = {
kind = "sphere",
radius = 8,
origin = context.boss:get_location()
}
local targets = context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #targets do
targets[i]:deal_custom_damage(2.0)
targets[i]:apply_potion_effect("SLOWNESS", 40, 1)
end
context.world:spawn_particle_at_location(
context.boss:get_location(),
{ particle = "DUST", amount = 30, speed = 0.2, red = 255, green = 0, blue = 0 }
)
context.world:play_sound_at_location(
context.boss:get_location(),
"entity.wither.ambient",
1.0, 1.5
)
end
local function start_attack_loop(context)
if context.state.attack_task_id ~= nil then return end
local phase = context.state.phase or 1
local interval = phase == 2 and 40 or 100
local attack = phase == 2 and phase_two_attack or phase_one_attack
context.state.attack_task_id = context.scheduler:run_every(interval, function(tick_context)
if not tick_context.boss.exists or not tick_context.boss.is_in_combat then return end
if tick_context.cooldowns:check_local("phase_attack", interval) then
attack(tick_context)
end
end)
end
return {
api_version = 1,
on_spawn = function(context)
context.state.phase = 1
context.state.attack_task_id = nil
end,
on_enter_combat = function(context)
start_attack_loop(context)
end,
on_game_tick = function(context)
-- Only check phase transition every 20 ticks (1 second) to stay lightweight
if not context.boss.is_in_combat then return end
if not context.cooldowns:check_local("phase_check", 20) then
return
end
-- Skip if already in phase 2
if context.state.phase ~= 1 then
return
end
-- Check health ratio
local health_ratio = context.boss.health / context.boss.maximum_health
if health_ratio <= 0.5 then
-- Transition to phase 2
context.state.phase = 2
context.log:info("Boss entering phase 2 at " .. tostring(math.floor(health_ratio * 100)) .. "% health")
-- Cancel the old attack loop
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end
-- Play transition effects
context.boss:play_model_animation("transform")
-- Announce the phase change to nearby players
local nearby = context.players.nearby_players(40)
for i = 1, #nearby do
nearby[i]:send_message("&4&lThe boss enters a frenzy!")
nearby[i]:show_title("&4Phase 2", "&cThe boss is enraged!", 10, 40, 10)
end
start_attack_loop(context)
end
end,
on_exit_combat = function(context)
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end
end
}
说明
- 状态初始化 --
on_spawn将phase设为1,attack_task_id设为nil。这些状态属于此能力运行时,不与其他能力共享,也不会跨重载保存。 - 攻击循环 --
on_enter_combat通过start_attack_loop恢复当前阶段:阶段 1 间隔为 100 tick,阶段 2 为 40。两者首次回调均为零延迟。每次攻击仍需等待phase_attack冷却结束;切换阶段不会清除旧冷却,因此某次回调可能跳过攻击。此例不检查全局冷却。阶段 1 向半径 5 格球体内的玩家提交 3 点自定义伤害,并播放动画和粒子,不会等待动画时长。 - on_game_tick 中的阶段检查 --
on_game_tick在非战斗状态下立即返回,然后检查20 tick的冷却("phase_check")。如果 Boss 已进入阶段2,也会提前返回。 - 生命值阈值 --
context.boss.health / context.boss.maximum_health得到当前生命值比例。降至 50% 或以下时开始过渡。 - 阶段过渡 -- 取消旧循环,播放变身动画,附近玩家收到消息和标题,启动更快的新循环(每 40 tick 而非 100)。
- 阶段 2 攻击 --
phase_two_attack使用半径 8 格的球体,每次成功回调向每个查询到的玩家提交一次 2 点伤害。它尝试施加 40 tick 的缓慢 II(amplifier = 1),受上述状态效果参与检查限制,然后显示红色尘埃并播放凋灵声音。它不会在一次回调中安排多次命中。 - 日志 --
context.log:info(...)写入服务器控制台。 - 清理 --
on_exit_combat取消当前活跃的攻击循环,无论 Boss 处于哪个阶段。
活动且未暂停的运行时每个服务器 tick 接收一次 on_game_tick(20 TPS 时每秒 20 次)。请用冷却限制高开销操作,例如 check_local("phase_check", 20)。这是定期检查生命值,致命一击可能在阶段检查之前移除 Boss。超出执行预算会关闭该能力运行时并取消其任务。
AI 生成提示
要让 AI 可靠地生成 Lua 能力,请在提示中包含:
- 精确的钩子名称 -- 如
on_player_damaged_by_boss,而不是“当 Boss 击中玩家时”。 - 原生 Lua 区域或脚本工具 -- 明确选择。原生区域使用
context.zones:get_entities_in_zone(zone_def, opts),字段为kind、radius、origin。脚本工具使用context.script:zone(...),字段为 EliteScript 的shape、Target、Target2等。 - 本地和全局冷却 -- 指定带能力名称前缀的键及 tick 时长。
context.cooldowns:check_local(key, ticks)检查并设置 Boss 的命名冷却。要与全局冷却协调,必须在消耗本地键前显式检查global_ready(),并在效果前设置全局时长。 - 自定义模型动画名 -- 如
context.boss:play_model_animation("slam")。AI 无法猜出模型的动画名,需要明确提供。 - 目标选择 -- 指定使用
context.player(事件玩家)、context.players.nearby_players(range)还是区域查询。 - 效果类型 -- 伤害(
deal_custom_damage)、药水(apply_potion_effect)、燃烧(set_fire_ticks)、速度(set_velocity_vector、apply_push_vector)等。 - 使用正确包装器的方法名 -- 提供适用的 Boss 或共享 API 参考,不要编造别名。
- 脚本工具使用 EliteScript 字段名 -- 使用
targetType、shape、Target、Target2、range、offset、coverage,而不是 Lua 风格名称。 - 调度器时间和上下文 -- 指定首次回调是立即执行还是延迟执行。
run_every首次延迟为零,run_repeating接受独立的延迟参数。使用回调的新上下文,它不含触发玩家或事件。后续需要玩家 UUID 或位置时,请显式保存。
良好的提示示例
编写一个 Lua 能力,在
on_enter_combat中启动首次延迟 80 tick、间隔 80 tick 的重复任务。每次回调创建以 Boss 为中心、半径 6 的原生 Lua 球体,查询玩家并向每人提交 2.0 自定义伤害。先检查全局冷却,再以 80 tick 检查并设置本地键"my_power:pulse",然后在效果前将全局冷却设为 80。在on_exit_combat中取消任务。在每个查询到的玩家位置生成 DUST 粒子(red=0, green=255, blue=100)。
应当加入的额外约束
- “返回一个包含
api_version = 1的表。” - “在
on_spawn中初始化所有状态字段。” - “用
if not tick_context.boss.exists then return end保护调度器回调。” - “在
on_exit_combat中取消所有重复任务。” - “不要编造方法名,只使用 API 参考中的方法。”
- “用
context.cooldowns:check_local(key, ticks)一次完成检查和设置。”
QC 检查清单
部署 Lua 能力之前,请检查:
- 文件返回恰好一个包含
api_version = 1的表。 - 钩子名称与钩子列表中的条目精确匹配,例如
on_player_damaged_by_boss,而不是on_player_hit。 - 在可能没有玩家的钩子中,使用
context.player前用== nil检查。 context.state字段在on_spawn中初始化。- 每个仅限战斗的重复任务,无论通过
run_every还是run_repeating创建,都在on_exit_combat中取消,且首次延迟符合意图。 - 调度器回调使用自己的上下文,不假设存在触发玩家或事件。
- 冷却键包含能力名称前缀,时长以 tick 为单位。全局协调包含显式的可用性检查。
on_game_tick钩子用冷却检查保护重逻辑。- 所有方法名存在于 API 参考中,没有编造别名。
- 脚本工具表使用 EliteScript 字段名(
targetType、shape、Target等),而不是 Lua 风格名称。 - 原生区域定义使用
kind、radius、origin、destination等。 - 能力不在钩子或回调中调用阻塞或耗时操作。
- 粒子配置使用有效的 Bukkit 粒子枚举名(UPPER_CASE),例如
"FLAME"、"DUST"、"EXPLOSION"。 - 重载修改后的文件,生成新的 Boss,在测试服务器上检查可见行为和控制台输出。文件解析成功不能证明攻击正常运行。
最佳实践
-
从小钩子开始并验证。 写一个发送日志消息的
on_spawn。确认它触发后再继续构建。 -
保持辅助函数为局部。 在返回表上方声明
local function pick_action(context)之类的辅助函数,让依赖关系明确。每个能力实例本来就拥有独立的 Lua 运行时。 -
将几何放入脚本工具。 如果需要锥体、旋转射线、平移射线或动画区域,使用带有 EliteScript 字段名的
context.script:zone(...)。脚本工具复用 EliteScript 区域引擎。 -
使用
context.state存储运行时状态。 它属于一个 Boss 上的一个能力运行时,并在该运行时的钩子之间保留。不与其他能力共享,也不跨重载保存。 -
使用命名的本地冷却键。 使用
"fire_touch"或"aoe_pulse"等描述性键,而不是简单数字。显式键由同一 Boss 的所有能力共享;添加能力名前缀,如"my_power:fire_touch",以防冲突。 -
保持
on_game_tick轻量。 用冷却限制高开销操作,并为执行预算留出充足余量。 -
完成时取消重复任务。 仅用于战斗的循环应在
on_exit_combat中取消。运行时关闭会自动取消其拥有的调度器任务,但战斗结束本身不会取消它们。 -
在调度器回调中使用新上下文。
run_every、run_repeating和run_after回调收到的新上下文不含调度它们的事件或玩家。使用此参数,并显式保存所需 UUID 或位置;外部上下文可能含过时快照。 -
用
context.log:info()记录状态转换。 开发时记录阶段切换、冷却开始和调度器启停。部署前删除频繁日志:Boss 的context.log:debug()也以 info 级别写入,只是前缀不同。 -
复用相关 EliteScript 参考。 区域、目标和相对向量介绍共享解析器。独立 Lua 工具不会自动求值 YAML 条件、等待或动作级缓存;请在 Lua 中编写检查。继承目标和落地位置需要 YAML 动作上下文,而这些句柄不具备该上下文。
常见初学者错误
-
在调度器回调中使用外部
context。 外部上下文捕获了钩子运行时的快照。在run_every或run_after回调内,始终使用回调自身的参数(例如tick_context),它会给你一个新鲜的快照。 -
忘记取消重复任务。 如果你在
on_enter_combat中启动了run_every但从未取消,任务会一直运行直到 Boss 从服务器移除,即使战斗已结束。 -
不在
on_spawn中初始化状态。 如果你在on_game_tick中读取context.state.phase但从未在on_spawn中设置它,它将是nil,你的比较将表现异常。 -
不检查 nil 就使用
context.player。 确认哪些钩子会提供玩家。调度器回调默认没有玩家,缺少检查可能关闭能力运行时。 -
在脚本工具调用中使用 Lua 风格字段名。 使用文档中的 EliteScript 字段,如
targetType、Target、Target2和shape。不支持target_type、zone_shape等下划线变体。区域解析器接受小写target和target2,但这不代表任意 Lua 风格别名都有效。 -
在
on_game_tick中不加冷却就运行重逻辑。 多个 Boss 上的全世界实体查询和密集粒子几何开销很大。限制这些工作的频率,并让每个回调保持在执行预算内。 -
混用包装器 API。 EliteMobs 的 Boss 专用包装器使用
entity:teleport_to_location(loc)和player:set_velocity_vector(vec)。共享 MagmaCore 包装器还有其他别名。从其他插件示例借用方法前,先检查当前上下文的参考文档。 -
使用
context.boss.health设置生命值。context.boss.health是只读快照。要治愈 Boss,使用context.boss:restore_health(amount)。 -
忘记
api_version = 1。 返回的表必须包含此字段,否则 EliteMobs 不会加载该能力。
