跳到主要内容

Lua 脚本:钩子与生命周期

webapp_banner.jpg

本页涵盖 Lua 能力可以定义的每个钩子、钩子执行顺序、每个 Boss 如何获得自己的隔离运行时,以及沙盒中可用的标准库函数。

如果你还没有编写过 Lua 能力,请先从入门指南开始。


钩子参考

每个 Lua 能力文件都会返回一个表。该表中的每个键(除了 api_versionpriority)都必须是下面列出的钩子之一。运行时在相应的游戏事件触发时会调用匹配的函数。

钩子触发时机context.player 是否可用?
on_spawn精英怪物生成时
on_game_tick运行时时钟活动期间每个服务器 tick(50 毫秒)执行一次
on_boss_damagedBoss 受到任何来源的伤害时
on_boss_damaged_by_playerBoss 受到玩家的伤害时
on_boss_damaged_by_eliteBoss 受到另一个精英怪物的伤害时
on_player_damaged_by_boss玩家受到此 Boss 的伤害时
on_enter_combatBoss 进入战斗时
on_exit_combatBoss 脱离战斗时
on_healBoss 治疗时
on_boss_target_changedBoss 切换目标时
on_deathBoss 死亡时
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_damagedon_boss_damaged_by_playeron_boss_damaged_by_eliteon_player_damaged_by_boss

字段 / 方法类型说明
event.damage_amountdouble原始伤害值
event.damage_causestringSpigot 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_reasonstringSpigot SpawnReason 名称
event.cancel_event()取消生成

死亡钩子

适用于 on_death

字段 / 方法类型说明
event.entity实体表正在死亡的实体

区域钩子

适用于 on_zone_enteron_zone_leave

字段 / 方法类型说明
event.entity实体表进入或离开区域的实体

可取消事件(通用)

任何底层游戏事件可取消的钩子都会暴露 event.cancel_event()。如果某个钩子的 context.eventnil(如 on_game_tickon_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 消失时:

  1. 运行时调用 shutdown()
  2. 所有已拥有的任务——无论是一次性(run_after)还是重复(run_every)——都会被自动取消。
  3. 所有区域监视都会被清除。

你永远不需要在 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_spawnon_enter_combat 中,而不是每个 tick 都重复执行。

Lua 沙盒

Lua 能力在沙盒化的 LuaJ 环境中运行。多个可能访问文件系统或 Java 运行时的全局变量已被移除。

已移除的全局变量

以下标准 Lua 全局变量被设置为 nil,无法使用:

已移除原因
debug暴露内部 VM 状态
dofile文件系统访问
io文件系统访问
load任意代码加载
loadfile文件系统访问
luajava直接 Java 类访问
module模块系统(不需要)
os操作系统访问
package模块系统(不需要)
require模块系统 / 文件系统访问

可用的标准库

Lua 标准库的其他所有内容均正常工作:

类别函数
Mathmath.absmath.ceilmath.floormath.maxmath.minmath.randommath.sinmath.cosmath.sqrtmath.pi,以及所有其他 math.* 函数
Stringstring.bytestring.charstring.findstring.formatstring.gsubstring.lenstring.lowerstring.matchstring.repstring.substring.upper,以及所有其他 string.* 函数
Tabletable.inserttable.removetable.sorttable.concat,以及所有其他 table.* 函数
迭代器pairsipairsnext
类型typetostringtonumberselectunpack
错误处理pcallxpcallerrorassert
其他printrawgetrawsetrawequalrawlensetmetatablegetmetatable
提示

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
}

有关区域形状、过滤器、监视器和目标模式的完整说明,请参阅区域与目标


后续步骤