跳到主要内容

Lua 脚本:示例与模式

本页包含 EliteMobs Lua 能力的完整工作示例,以及实用模式、最佳实践和技巧。每个示例都附有其功能和原因的说明。

如果你是 Lua 能力的新手,请从入门指南开始。完整 API 详情请参阅 API 参考Boss 与实体世界与环境区域与目标选择枚举

webapp_banner.jpg


示例:使用脚本工具进行基于区域的目标选择

本示例教授: 如何使用 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
}

说明

  1. 区域创建 -- context.script:zone(...) 使用与 EliteScript 区域相同的字段名创建锥形。Target 设置锥形原点(Boss 本身,向上偏移1格),Target2 设置目的地(20格内最近的玩家)。radius 控制锥形的张开程度。
  2. 粒子生成 -- cone:full_target(0.4) 返回一个以 40% 覆盖率解析为锥形内所有位置的目标句柄。
  3. 伤害 -- context.script:damage(cone:full_target(), 1.0, 1.5) 命中完整锥形内的所有生物实体。第一个数字(1.0)是基础伤害量,第二个(1.5)是应用于玩家的伤害倍率。
EliteScript 字段名

传递给 context.script 的区域和目标表使用 EliteScript 字段名targetTypeshapeTargetTarget2rangeoffsetcoverage)。完整列表请参阅 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
}

说明

  1. 状态初始化 -- on_spawncontext.state.started 设为 falsecontext.state.loop_task_id 设为 nilstate 表在此 Boss 实例的整个生命周期中持续存在。
  2. 战斗守卫 -- on_enter_combat 在启动循环前检查 context.state.started,防止多个重叠循环。
  3. 调度器模式 -- context.scheduler:run_every(100, callback) 每 100 tick(5秒)执行回调。回调接收新鲜上下文
  4. 退出时清理 -- on_exit_combat 取消重复任务并重置状态。
始终取消调度器任务

如果在 on_enter_combat 中启动了重复任务,务必在 on_exit_combat 中取消它。忘记取消会留下一个后台任务,直到 Boss 消失才停止运行。


示例:命中时的火焰效果

本示例教授: 简单的"命中时应用效果"能力 -- 战斗能力最常见的模式。

完整能力文件(点击展开)
return {
api_version = 1,
on_player_damaged_by_boss = function(context)
if context.player == nil then return end
if not context.cooldowns:check_local("fire_touch", 60) then return end
context.player:set_fire_ticks(60)
context.world:spawn_particle_at_location(context.player.current_location, { particle = "FLAME", amount = 20, speed = 0.1 })
context.player:send_message("&cThe boss's touch burns!")
context.cooldowns:set_global(40)
end
}

说明

  1. Nil 守卫 -- context.player 在极少数边缘情况下可能为 nil。使用前务必检查。
  2. 本地冷却 -- context.cooldowns:check_local("fire_touch", 60) 原子性地检查和设置冷却。
  3. 燃烧 tick -- context.player:set_fire_ticks(60) 让玩家燃烧 60 tick(3秒)。
  4. 粒子 -- context.world:spawn_particle_at_location(location, spec) 在位置生成粒子。
  5. 消息 -- context.player:send_message(text) 发送带颜色代码的聊天消息。
  6. 全局冷却 -- context.cooldowns:set_global(40) 将此 Boss 的所有能力设为 40 tick 冷却。

示例:使用原生 Lua 区域的基于区域的 AoE 能力

本示例教授: 创建和查询原生 Lua 区域,对区域内的玩家造成伤害。

完整能力文件(点击展开)
return {
api_version = 1,
on_spawn = function(context) context.state.aoe_task_id = nil end,
on_enter_combat = function(context)
if context.state.aoe_task_id ~= nil then return end
context.state.aoe_task_id = context.scheduler:run_every(60, function(tick_context)
if not tick_context.boss.exists then return end
if not tick_context.cooldowns:check_local("pulse_aoe", 60) then return end
local zone_def = { kind = "sphere", radius = 8, origin = tick_context.boss:get_location() }
local victims = tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #victims do
victims[i]:deal_custom_damage(4.0)
tick_context.world:spawn_particle_at_location(victims[i].current_location, { particle = "DUST", amount = 15, speed = 0, red = 128, green = 0, blue = 255 })
end
tick_context.world:spawn_particle_at_location(tick_context.boss:get_location(), { particle = "SPELL_MOB", amount = 40, speed = 0.1 })
tick_context.cooldowns:set_global(60)
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
}

说明

  1. 状态设置 -- on_spawnaoe_task_id 初始化为 nil
  2. 重复攻击 -- on_enter_combat 每 60 tick(3秒)启动一个重复任务。
  3. 区域定义 -- zone_def 表使用原生 Lua 区域语法,包含 kindradiusorigin
  4. 实体查询 -- 返回球体内所有玩家表的 Lua 数组。
  5. 造成伤害 -- victim:deal_custom_damage(4.0) 造成 4 点归属于 Boss 的伤害。
  6. 粒子反馈 -- 每个受害者显示紫色尘埃粒子,Boss 位置显示环境粒子。
  7. 清理 -- on_exit_combat 取消任务并清理状态。
原生区域 vs. 脚本工具区域

此示例使用原生 Lua 区域context.zones:get_entities_in_zone())。脚本工具(context.script:zone(...))使用 EliteScript 字段名如 shapeTargetTarget2。两者都有效 -- 简单形状使用原生区域,需要 EliteScript 高级目标解析时使用 context.script


示例:多阶段 Boss 机制

本示例教授: 使用状态跟踪 Boss 阶段并在生命值阈值处切换行为。

完整能力文件(点击展开)
local function phase_one_attack(context)
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)
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

return {
api_version = 1,
on_spawn = function(context)
context.state.phase = 1
context.state.attack_task_id = nil
context.state.phase_switched = false
end,
on_enter_combat = function(context)
if context.state.attack_task_id ~= nil then return end
context.state.attack_task_id = context.scheduler:run_every(100, function(tick_context)
if not tick_context.boss.exists then return end
if not tick_context.cooldowns:check_local("phase_attack", 100) then return end
phase_one_attack(tick_context)
end)
end,
on_game_tick = function(context)
if not context.cooldowns:check_local("phase_check", 20) then return end
if context.state.phase ~= 1 then return end
local health_ratio = context.boss.health / context.boss.maximum_health
if health_ratio <= 0.5 then
context.state.phase = 2
context.state.phase_switched = true
context.log:info("Boss entering phase 2 at " .. tostring(math.floor(health_ratio * 100)) .. "% health")
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end
context.boss:play_model_animation("transform")
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
context.state.attack_task_id = context.scheduler:run_every(40, function(tick_context)
if not tick_context.boss.exists then return end
if not tick_context.cooldowns:check_local("phase_attack", 40) then return end
phase_two_attack(tick_context)
end)
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
}

说明

  1. 状态初始化 -- on_spawnphase 设为 1attack_task_id 设为 nilphase_switched 设为 false
  2. 阶段1攻击循环 -- on_enter_combat 启动每 100 tick 执行 phase_one_attack 的重复任务。
  3. on_game_tick 中的阶段检查 -- 检查 20 tick 的冷却以避免每 tick 执行重逻辑。
  4. 生命值阈值 -- 降至 50% 或以下时开始过渡。
  5. 阶段过渡 -- 取消旧循环,播放变身动画,附近玩家收到消息和标题,启动更快的新循环(每 40 tick 而非 100)。
  6. 阶段2攻击 -- 更大的球体(8格),每次伤害略低但频率更高,应用缓慢药水效果,使用红色尘埃粒子和凋灵声音。
  7. 日志 -- context.log:info(...) 写入服务器控制台。
  8. 清理 -- on_exit_combat 取消当前活跃的攻击循环,无论 Boss 处于哪个阶段。
保持 on_game_tick 轻量

on_game_tick 每个服务器 tick 执行一次(每秒20次)。始终用冷却检查保护重逻辑。如果你的钩子超过 50ms,EliteMobs 将自动禁用该能力。


AI 生成提示

要让 AI 可靠地生成 Lua 能力,确保提示包含:精确的钩子名称,原生 Lua 区域或脚本工具(指明哪个),带键名和 tick 持续时间的本地和全局冷却,自定义模型动画名称,目标选择,效果类型,以及仅使用已文档化的方法名称。

良好的提示示例

编写一个 Lua 能力,使用 on_enter_combat 每 80 tick 启动一个重复任务。每次 tick,创建一个以 Boss 为中心的原生 Lua 球体区域(半径6),查询其中的玩家,并对每个玩家造成 2.0 自定义伤害。使用本地冷却键 "pulse",持续时间 80。在 on_exit_combat 中取消任务。在每个受害者位置生成 DUST 粒子(red=0, green=255, blue=100)。


QC 检查清单

  1. 文件返回恰好一个包含 api_version = 1 的表。
  2. 钩子名称与钩子列表中的条目精确匹配。
  3. context.player 在使用前用 == nil 保护。
  4. context.state 字段在 on_spawn 中初始化。
  5. 每个 run_everyon_exit_combat 中有对应的 cancel_task
  6. 调度器回调使用回调自身的上下文参数,而非外部 context
  7. 冷却键是描述性字符串,持续时间以 tick 为单位。
  8. on_game_tick 钩子用冷却检查保护重逻辑。
  9. 所有方法名存在于 API 参考中。
  10. 脚本工具表使用 EliteScript 字段名。
  11. 原生区域定义使用 kindradiusorigindestination 等。
  12. 能力不在钩子或回调中调用阻塞操作。
  13. 粒子规格使用有效的 Bukkit 粒子枚举名(UPPER_CASE)。

最佳实践

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

  • 保持辅助函数为局部。 在返回表上方声明如 local function pick_action(context) 的辅助函数。这使它们不在全局作用域中,避免与同一运行时中加载的其他 Lua 能力冲突。

  • 将几何放入脚本工具。 如果需要锥体、旋转射线、平移射线或动画区域,使用带有 EliteScript 字段名的 context.script:zone(...)。脚本工具重用了经过实战检验的 EliteScript 区域引擎。

  • 使用 context.state 存储运行时状态。 不要使用 Lua 全局变量。context.state 限定于单个 Boss 实例,并在该 Boss 的生命周期内跨钩子持久化。

  • 使用命名的本地冷却键。 不要使用简单数字,使用描述性键如 "fire_touch""aoe_pulse"。这使调试更容易,并防止同一能力中不同冷却之间的意外冲突。

  • 保持 on_game_tick 轻量。 始终用冷却检查保护。如果你的逻辑每个 tick 都运行,必须在远少于 50ms 内完成,否则能力将被禁用。

  • 完成时取消重复任务。 每个 run_every 必须在 on_exit_combat(可能还有 on_death)中有对应的 cancel_task。泄漏的任务浪费 CPU 并可能在 Boss 消失后导致空引用错误。

  • 在调度器回调中使用新鲜上下文。 调度器回调(run_everyrun_after)接收新鲜上下文作为参数。始终使用该参数 -- 而非外部 context -- 因为外部上下文可能包含过时的快照。

  • context.log:info() 记录状态转换。 在开发期间,为阶段切换、冷却开始和调度器启动/停止添加日志。部署前删除或更改为 context.log:debug()

  • 重用现有的 EliteScript 文档。 区域目标相对向量条件 页面记录了脚本工具接受的相同字段名。不要在你的 Lua 能力中重复该信息 -- 只需引用它。


常见初学者错误

  • 在调度器回调中使用外部 context 外部上下文捕获了钩子运行时的快照。在 run_everyrun_after 回调内,始终使用回调自身的参数(例如 tick_context),它会给你一个新鲜的快照。

  • 忘记取消重复任务。 如果你在 on_enter_combat 中启动了 run_every 但从未取消,任务会一直运行直到 Boss 从服务器移除,即使战斗已结束。

  • 不在 on_spawn 中初始化状态。 如果你在 on_game_tick 中读取 context.state.phase 但从未在 on_spawn 中设置它,它将是 nil,你的比较将表现异常。

  • 没有 nil 守卫就检查 context.playeron_player_damaged_by_boss 等钩子中,玩家几乎总是可用的 -- 但"几乎总是"不是"总是"。一个缺失的 nil 守卫可以使能力崩溃。

  • 在脚本工具调用中使用 Lua 风格字段名。 脚本工具期望 targetTypeTargetTarget2shape -- 而非 target_typetargettarget2zone_shape。不匹配的名称会悄无声息地不产生结果。

  • on_game_tick 中没有冷却门控就运行重逻辑。 这个钩子每个服务器 tick 都会触发。即使是在许多 Boss 上每秒重复 20 次的简单算术也会积累。

  • 编造方法名。 如果方法未在 API 参考中列出,则不存在。常见错误包括写 entity:teleport(loc) 而非 entity:teleport_to_location(loc),或 player:set_velocity(vec) 而非 player:set_velocity_vector(vec)

  • 使用 context.boss.health 设置生命值。 context.boss.health 是只读快照。要治愈 Boss,使用 context.boss:restore_health(amount)

  • 忘记 api_version = 1 返回的表必须包含此字段,否则 EliteMobs 不会加载该能力。


后续步骤