跳到主要内容

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)
-- Guard: the player may be nil in edge cases
if context.player == nil 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

-- 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!")

-- Set the global power cooldown so other powers on this boss
-- don't all fire at the same instant
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)
-- 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

-- Check a local cooldown so this doesn't stack with other effects
if not tick_context.cooldowns:check_local("pulse_aoe", 60) then
return
end

-- 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 visual ring particles at the boss
tick_context.world:spawn_particle_at_location(
tick_context.boss:get_location(),
{ particle = "WITCH", amount = 40, speed = 0.1 }
)

-- Set global cooldown
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)
-- Slow, heavy slam
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)
-- Fast, frantic multi-hit
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
context.state.phase_switched = false
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.state.phase_switched = true

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
}

说明

  1. 状态初始化 -- on_spawnphase 设为 1attack_task_id 设为 nilphase_switched 设为 false
  2. 攻击循环 -- on_enter_combat 通过 start_attack_loop 恢复当前阶段:阶段1每100 tick攻击一次,阶段2每40 tick攻击一次。离开并重新进入战斗会保留阶段。阶段1的攻击创建半径5格的球形区域,对其中的玩家造成伤害,并播放带爆炸粒子的重击动画。
  3. on_game_tick 中的阶段检查 -- on_game_tick 在非战斗状态下立即返回,然后检查20 tick的冷却("phase_check")。如果 Boss 已进入阶段2,也会提前返回。
  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次)。请用冷却限制高开销操作,例如 check_local("phase_check", 20)。超出执行预算会禁用能力。


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)。

应当加入的额外约束

  • "Return a single table with api_version = 1."
  • "Initialize all state fields in on_spawn."
  • "Guard scheduler callbacks with if not tick_context.boss.exists then return end."
  • "Cancel all repeating tasks in on_exit_combat."
  • "Do not invent method names -- only use methods from the API Reference page."
  • "Use context.cooldowns:check_local(key, ticks) for combined check-and-set."

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 轻量。 用冷却限制高开销操作,并为执行预算留出充足余量。

  • 完成时取消重复任务。 每个 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 不会加载该能力。


后续步骤