Lua 脚本:钩子与生命周期
本页涵盖 Lua 能力可以定义的每个钩子、钩子执行顺序、每个 Boss 如何获得自己的隔离运行时,以及沙盒中可用的标准库函数。
如果你还没有编写过 Lua 能力,请先从入门指南开始。
钩子参考
每个 Lua 能力文件都会返回一个表。该表中的每个键(除了 api_version 和 priority)都必须是下面列出的钩子之一。运行时在相应的游戏事件触发时会调用匹配的函数。
| 钩子 | 触发时机 | context.player 是否可用? |
|---|---|---|
on_spawn | 精英怪物生成时 | 否 |
on_game_tick | 运行时时钟活动期间每个服务器 tick(50 毫秒)执行一次 | 否 |
on_boss_damaged | Boss 受到任何来源的伤害时 | 否 |
on_boss_damaged_by_player | Boss 受到玩家的伤害时 | 是 |
on_boss_damaged_by_elite | Boss 受到另一个精英怪物的伤害时 | 否 |
on_player_damaged_by_boss | 玩家受到此 Boss 的伤害时 | 是 |
on_enter_combat | Boss 进入战斗时 | 是 |
on_exit_combat | Boss 脱离战斗时 | 否 |
on_heal | Boss 治疗时 | 否 |
on_boss_target_changed | Boss 切换目标时 | 是 |
on_death | Boss 死亡时 | 否 |
on_phase_switch | 阶段 Boss 切换到新阶段时 | 否 |
on_zone_enter | 实体进入被监视的区域时 | 是(如果该实体是玩家) |
on_zone_leave | 实体离开被监视的区域时 | 是(如果该实体是玩家) |
当 context.player 列为"否"时,访问它会返回 nil。使用前请始终进行 nil 检查。
典型的多钩子能力
单个 Lua 能力可以根据需要定义任意数量的钩子。下面是一个同时使用三个钩子的骨架示例:
return {
api_version = 1,
on_enter_combat = function(context)
-- Initialize per-fight state when combat begins
context.state.hit_count = 0
context.log:info("Combat started!")
end,
on_boss_damaged_by_player = function(context)
-- Track hits and trigger an ability every 5th hit
context.state.hit_count = (context.state.hit_count or 0) + 1
if context.state.hit_count % 5 ~= 0 then
return
end
if not context.cooldowns.check_local("counter_attack", 100) then
return
end
-- Fire a projectile back at the player
local origin = context.boss:get_location()
origin:add(0, 1, 0)
context.boss:summon_projectile(
"SMALL_FIREBALL", origin, context.player:get_location(), 1.5
)
end,
on_death = function(context)
-- Spawn a firework on death
context.world:spawn_particle_at_location(
context.boss:get_location(), "EXPLOSION_EMITTER", 1
)
end
}
事件数据(context.event)
某些钩子会接收一个 context.event 表,其中包含触发该钩子的游戏事件的相关数据。可用的字段取决于正在运行的钩子。
伤害钩子
适用于 on_boss_damaged、on_boss_damaged_by_player、on_boss_damaged_by_elite 和 on_player_damaged_by_boss。
| 字段 / 方法 | 类型 | 说明 |
|---|---|---|
event.damage_amount | double | 原始伤害值 |
event.damage_cause | string | Spigot DamageCause 名称(如 "ENTITY_ATTACK"、"PROJECTILE") |
event.damager | 实体表 | 造成伤害的实体。仅在由实体造成伤害的钩子中存在。 |
event.projectile | 实体表 | 如果造成伤害的是投射物,则为该投射物实体。 |
event.set_damage_amount(n) | — | 将伤害覆盖为固定值 |
event.multiply_damage_amount(n) | — | 将当前伤害乘以 n |
event.cancel_event() | — | 完全取消伤害事件 |
on_boss_damaged_by_player = function(context)
-- Halve all projectile damage
if context.event.damage_cause == "PROJECTILE" then
context.event.multiply_damage_amount(0.5)
end
end
生成钩子
适用于 on_spawn。
| 字段 / 方法 | 类型 | 说明 |
|---|---|---|
event.spawn_reason | string | Spigot SpawnReason 名称 |
event.cancel_event() | — | 取消生成 |
死亡钩子
适用于 on_death。
| 字段 / 方法 | 类型 | 说明 |
|---|---|---|
event.entity | 实体表 | 正在死亡的实体 |
区域钩子
适用于 on_zone_enter 和 on_zone_leave。
| 字段 / 方法 | 类型 | 说明 |
|---|---|---|
event.entity | 实体表 | 进入或离开区域的实体 |
可取消事件(通用)
任何底层游戏事件可取消的钩子都会暴露 event.cancel_event()。如果某个钩子的 context.event 为 nil(如 on_game_tick、on_heal),则没有底层事件可以交互。
有关完整的实体表字段,请参阅 Boss 与实体。有关伤害原因和生成原因的值,请参阅枚举与值。
钩子执行顺序
当一个 Boss 附加了多个 Lua 能力时,每个能力的钩子都会为同一事件被调用。执行顺序由 priority 字段决定:
- 值越小越先执行(默认为
0)。 - 相同优先级的能力按加载顺序执行(实际上是未指定的)。
return {
api_version = 1,
priority = -10, -- runs before most other powers
on_boss_damaged_by_player = function(context)
-- This runs early, so other powers see any state changes we make
context.state.last_attacker = context.player.uuid
end
}
优先级仅影响同一 Boss 上 Lua 能力之间的排序,不会与 EliteScript 的执行顺序产生交互。
运行时模型
每个 Boss 一个运行时
每个 Boss 实体都会获得自己独立的 Lua 运行时实例。当 Boss 生成时,EliteMobs 加载 Lua 源代码,在全新的沙盒环境中执行它,并存储返回的表。当 Boss 消失或被移除时,运行时会关闭。
这意味着:
- 文件执行期间设置的全局 Lua 变量(如使用
local function定义的辅助函数)对该 Boss 是私有的。 - 返回表的钩子函数永远不会在 Boss 之间共享。
状态隔离
每个运行时都有自己的 context.state 表。一个 Boss 的状态对其他所有 Boss 完全不可见,即使它们共享同一个 Lua 能力文件。使用 context.state 来存储计数器、标志、计时器或跨钩子所需的任何按 Boss 存储的数据。
return {
api_version = 1,
on_boss_damaged_by_player = function(context)
-- Each boss tracks its own enrage counter independently
context.state.enrage_hits = (context.state.enrage_hits or 0) + 1
if context.state.enrage_hits >= 20 then
context.boss:apply_potion_effect("SPEED", 200, 2)
end
end
}
计划任务所有权
通过 context.scheduler 创建的所有任务都归创建它们的运行时所有。当 Boss 消失时:
- 运行时调用
shutdown()。 - 所有已拥有的任务——无论是一次性(
run_after)还是重复(run_every)——都会被自动取消。 - 所有区域监视都会被清除。
你永远不需要在 Boss 移除时手动清理计划任务。但是,你仍然应该在正常游戏过程中不再需要重复任务时取消它们,以避免不必要的开销:
return {
api_version = 1,
on_enter_combat = function(context)
local pulse_count = 0
local task_id
task_id = context.scheduler:run_every(20, function(tick_context)
pulse_count = pulse_count + 1
if pulse_count > 10 or not tick_context.boss.exists then
tick_context.scheduler:cancel_task(task_id)
return
end
tick_context.world:spawn_particle_at_location(
tick_context.boss:get_location(),
{ particle = "FLAME", amount = 20, speed = 0.1 }
)
end)
end
}
每 Tick 时钟行为
Lua 能力实例的内部 tick 时钟仅在有理由运行时才会运行。具体来说,时钟在以下任一条件满足时注册:
- 能力定义了
on_game_tick钩子,或者 - 能力通过
context.zones:watch_zone(...)创建了至少一个区域监视。
如果两个条件都不满足,则不会产生每 tick 的开销。当 Boss 消失或运行时关闭时,时钟会自动注销。
错误和性能行为
EliteMobs 对 Lua 能力实施严格的错误和性能限制:
异常
如果钩子函数或计划回调抛出 Lua 错误(或 API 调用中出现 Java 异常),该能力会被立即禁用。运行时会关闭,所有已拥有的任务会被取消。
错误会连同能力的文件名一起记录到服务器控制台:
[EliteMobs] Lua power frost_cone.lua failed while handling on_boss_damaged_by_player.
执行预算
每次钩子调用和每次回调调用都会被计时。如果单次调用超过 50 毫秒,该能力会被禁用并在控制台输出警告:
[EliteMobs] Lua power my_power.lua exceeded the 50ms execution budget on on_game_tick and was disabled.
这可以防止失控的脚本冻结服务器。为了保持在预算范围内:
- 避免在钩子内使用无界循环。使用
context.scheduler:run_every(...)将工作分散到多个 tick。 - 保持
on_game_tick处理程序轻量——它们每个 tick 都会运行。 - 将繁重的初始化移到
on_spawn或on_enter_combat中,而不是每个 tick 都重复执行。
Lua 沙盒
Lua 能力在沙盒化的 LuaJ 环境中运行。多个可能访问文件系统或 Java 运行时的全局变量已被移除。
已移除的全局变量
以下标准 Lua 全局变量被设置为 nil,无法使用:
| 已移除 | 原因 |
|---|---|
debug | 暴露内部 VM 状态 |
dofile | 文件系统访问 |
io | 文件系统访问 |
load | 任意代码加载 |
loadfile | 文件系统访问 |
luajava | 直接 Java 类访问 |
module | 模块系统(不需要) |
os | 操作系统访问 |
package | 模块系统(不需要) |
require | 模块系统 / 文件系统访问 |
可用的标准库
Lua 标准库的其他所有内容均正常工作:
| 类别 | 函数 |
|---|---|
| Math | math.abs、math.ceil、math.floor、math.max、math.min、math.random、math.sin、math.cos、math.sqrt、math.pi,以及所有其他 math.* 函数 |
| String | string.byte、string.char、string.find、string.format、string.gsub、string.len、string.lower、string.match、string.rep、string.sub、string.upper,以及所有其他 string.* 函数 |
| Table | table.insert、table.remove、table.sort、table.concat,以及所有其他 table.* 函数 |
| 迭代器 | pairs、ipairs、next |
| 类型 | type、tostring、tonumber、select、unpack |
| 错误处理 | pcall、xpcall、error、assert |
| 其他 | print、rawget、rawset、rawequal、rawlen、setmetatable、getmetatable |
print 会写入服务器控制台,但建议使用 context.log:info(msg) 或 context.log:warn(msg) 进行输出。这些会带有能力名称前缀,更容易追踪是哪个能力产生的消息。
em 辅助命名空间
em 表在文件加载时(任何钩子运行之前)就可用。它提供辅助构造函数,用于构建整个 API 中使用的位置表、向量表和区域定义。
| 函数 | 用途 |
|---|---|
em.create_location(x, y, z [, world, yaw, pitch]) | 创建带有可选世界名称、偏航和俯仰的位置表 |
em.create_vector(x, y, z) | 创建向量表 |
em.zone.create_sphere_zone(radius) | 创建球形区域定义 |
em.zone.create_dome_zone(radius) | 创建穹顶区域定义 |
em.zone.create_cylinder_zone(radius, height) | 创建圆柱体区域定义 |
em.zone.create_cuboid_zone(x, y, z) | 创建长方体区域定义 |
em.zone.create_cone_zone(length, radius) | 创建锥形区域定义 |
em.zone.create_static_ray_zone(length, thickness) | 创建静态射线区域定义 |
em.zone.create_rotating_ray_zone(length, point_radius, animation_duration) | 创建旋转射线区域定义 |
em.zone.create_translating_ray_zone(length, point_radius, animation_duration) | 创建平移射线区域定义 |
区域构造器返回可链式调用的表,支持 :set_center(loc)(或根据区域类型使用 :set_origin(loc) / :set_destination(loc))。它们可以在文件顶部或钩子内使用:
-- At file scope: create a reusable zone shape
local blast_zone = em.zone.create_sphere_zone(5)
return {
api_version = 1,
on_boss_damaged_by_player = function(context)
-- Anchor the zone to the boss's current location at call time
blast_zone:set_center(context.boss:get_location())
local entities = context.zones:get_entities_in_zone(blast_zone)
for i = 1, #entities do
if entities[i].type == "PLAYER" then
entities[i]:apply_potion_effect("SLOWNESS", 60, 1)
end
end
end
}
有关区域形状、过滤器、监视器和目标模式的完整说明,请参阅区域与目标。
