Lua 脚本:NPC 脚本
EliteMobs NPC Lua 脚本是独立的 .lua 文件,可以挂接到 NPC 配置上。它们与 Boss Lua 能力是分开的:Boss 能力位于 plugins/EliteMobs/powers/,而 NPC 脚本位于 plugins/EliteMobs/npc_scripts/。
NPC 脚本与 Boss 能力和 FreeMinecraftModels 道具使用共享的 MagmaCore 脚本运行时。它们会收到 context.world、context.zones、context.scheduler、context.cooldowns、context.log 等共享服务,以及 NPC 专属的 context.npc 表。玩家与事件数据取决于钩子。Boss 能力为若干表提供了自己的版本,因此编写 NPC 脚本时请使用共享 API 的参数签名。
NPC Lua 脚本仍处于实验阶段。NPC 专属的钩子以及 context.npc 辅助方法可能会发生变化。共享表(context.world、context.zones、context.scheduler、context.cooldowns、context.log、context.event、context.player)与脚本引擎和 Lua API 参考中记录的是同一批。
文件位置
在以下位置创建 NPC 脚本文件:
plugins/
EliteMobs/
npc_scripts/
wave.lua
子文件夹会被递归扫描。但脚本是仅按文件名注册的,因此 npc_scripts/wave.lua 与 npc_scripts/town/wave.lua 会发生冲突 —— 请让整个目录树中的文件基名保持唯一。
发现文件时,会忽略大小写,按稳定的绝对路径顺序为每个基名选择一个文件,并在副本内容不同时记录选中和跳过的路径。选中的文件出错时,不会回退到跳过的重复文件。配置中的引用仍须与选中文件名的大小写完全一致。
在 NPC 配置中,.lua 扩展名是可选的:- wave 和 - wave.lua 都会解析为 wave.lua。如果 NPC 配置引用了一个不存在的脚本,EliteMobs 会记录一条警告,NPC 仍然会正常生成。
将脚本挂接到 NPC
在 NPC 配置中添加一个 scripts: 列表:
scripts:
- wave.lua
可以为一个 NPC 挂接多个脚本:
scripts:
- wave.lua
- greeting_particles.lua
脚本按优先级顺序运行。priority 值越小越先运行,省略时默认为 0。优先级相同时保留 scripts: 列表中的顺序。这决定的是 NPC 事件的分派顺序,而非独立调度任务的顺序。
脚本结构
每个 NPC 脚本都必须返回一个表:
return {
api_version = 1,
priority = 0,
on_spawn = function(context)
context.state.spawned = true
context.npc:play_model_animation("idle")
end
}
只接受以下这些顶层字段:
| 字段 | 类型 | 说明 |
|---|---|---|
api_version | number | 必填。必须为 1。 |
priority | number | 可选。值越小越先运行。 |
on_spawn | function | 在 NPC 生成之后运行。 |
on_remove | function | 在正常移除 NPC 时运行,早于脚本关闭和实体移除。 |
on_game_tick | function | 脚本运行时初始化且时钟启动后,每个服务器 tick 运行。请保持轻量。 |
on_npc_interact | function | 普通交互处理到达脚本步骤时运行;限制见下文。 |
on_npc_proximity_enter | function | 玩家进入该 NPC 的激活半径时运行一次。 |
on_npc_proximity_leave | function | 玩家离开该 NPC 的激活半径时运行一次。 |
on_zone_enter | function | 玩家进入该脚本正在监视的区域时运行(参见 context.zones)。 |
on_zone_leave | function | 玩家离开被监视的区域时运行。 |
区域监视只跟踪玩家 —— 怪物和其他实体永远不会触发 on_zone_enter / on_zone_leave。
未知的顶层键会在脚本加载期间被拒绝。辅助函数应当在返回的表之上声明为 local 函数。
加载器会执行文件级代码进行验证,随后每个 NPC 脚本实例又会在自己的沙箱中执行一次。不要在文件级初始化中使用钩子上下文。每个脚本的全局变量和 context.state 都独立,同一 NPC 上的脚本也不例外。
NPC 运行时采用延迟初始化。目前 NPC 宿主会创建实例并分派 on_spawn,但不会调用运行时的主动初始化方法 start()。因此,只有 on_game_tick 的文件不会自行开始 tick。请定义 on_spawn = function(context) end,让它在生成时初始化,或由另一个已声明的事件钩子稍后初始化。
Lua 错误会关闭受影响的脚本实例,并取消它拥有的工作。之后的 NPC 事件会丢弃这个已关闭的实例,而不是重建它。请重新加载修正后的定义,并创建新的 NPC 脚本实例。状态不会跨移除或重启保存;on_remove 是 NPC 移除钩子,不是通用错误或关闭回调。
邻近钩子
NPC 邻近钩子使用 NPC 配置中的 activationRadius 值。
| 钩子 | 触发时机 |
|---|---|
on_npc_proximity_enter | 玩家从 NPC 激活半径之外移动到半径之内。 |
on_npc_proximity_leave | 玩家从 NPC 激活半径之内移动到半径之外。 |
服务端扫描器每 100 ticks 检查一次各 NPC/玩家组合,20 TPS 时通常为五秒,距离按当前 NPC 实体周围的球形范围计算。第一次扫描时已经在范围内也算进入。两次扫描之间的短暂访问可能漏掉。靠近一个 NPC 不会阻止另一个 NPC 的进入事件,持续检测到在范围内也不会重复触发。
正在巡逻移动或被编辑的 NPC 会被跳过。因此,先前检测到的组合可能在开始移动时触发离开,恢复检测时触发进入。移除 NPC 或玩家下线不保证触发离开钩子:分派需要被跟踪的 NPC 和在线玩家都存在。NPC 集合或在线玩家集合为空时,会清除跟踪而不发送离开事件。activationRadius 小于等于零时禁用邻近激活;任务提供者独立的 30 格标记范围不会扩大 Lua 激活范围。
正常的问候、对话与任务指示行为仍会照常运行。Lua 钩子是在这些行为之上叠加的。
共享脚本接口
由于 NPC 脚本运行在统一运行时上,每个 NPC 钩子也会收到 FreeMinecraftModels 脚本所使用的那些共享 MagmaCore 上下文表。EliteMobs Boss 能力运行在同一个运行时上,但其中若干表使用的是 Boss 专属的变体。完整的方法列表请参阅 Lua API 参考与脚本引擎:
| 表 | 作用 |
|---|---|
context.world | 世界效果与查询:strike_lightning、spawn_particle、play_sound、set_block_at、place_temporary_block、spawn_entity、spawn_firework、get_nearby_entities、get_nearby_players、raycast 等等。坐标形式(strike_lightning(x, y, z))与位置表形式(strike_lightning_at_location(loc))都被接受。 |
context.zones | 用 create_sphere(x, y, z, radius)、create_cylinder(x, y, z, radius, height) 或 create_cuboid(x, y, z, xSize, ySize, zSize) 创建空间区域。返回数字句柄,所有者没有世界时返回 nil。watch(handle, on_enter, on_leave) 在对应参数为函数时启用顶层钩子,不会调用传入的函数本身。unwatch(handle) 删除区域并停止跟踪。 |
context.scheduler | run_later(ticks, fn)、run_repeating(delay, interval, fn)、cancel(task_id)。 |
context.cooldowns | 共享的 MagmaCore 冷却:local_ready、local_remaining、check_local、set_local、global_ready、set_global。 |
context.log | info(msg)、warn(msg)、error(msg) —— 写入服务器控制台。 |
context.event | 钩子提供事件或触发者时的相应数据。参见下文。 |
context.player | 触发交互的玩家(存在时)。参见下文。 |
context.state | 一个普通的 Lua 表,在该 NPC 被移除之前,对这个 NPC 脚本实例始终持续存在。 |
共享区域采用创建时 NPC 所在世界的绝对坐标,不会跟随移动的 NPC。创建时便会注册每 tick 扫描,检查所有者当前世界中的玩家,即使尚未通过 watch 设置进入/离开处理。为要启用的每个方向传入一个函数,并在返回的脚本表中实现对应的 on_zone_enter 或 on_zone_leave。这些钩子收到 context.player 和触发者事件表,但没有区域标识符。unwatch 会彻底删除句柄;再次监视需要另建区域。第一次扫描时已在区域内的玩家算作进入,而离线玩家无法收到离开回调。
示例:交互时降下雷击
return {
api_version = 1,
on_npc_interact = function(context)
-- NPC scripts can now reach the full world API.
context.world:strike_lightning_at_location(context.npc:get_location())
end
}
context.npc
context.npc 在每个 NPC 钩子中都可用。
字段
| 字段 | 类型 | 说明 |
|---|---|---|
name | string | 配置中的 NPC 显示名称。 |
filename | string | NPC 配置文件名。 |
uuid | string | 运行时的 NPC UUID。 |
activation_radius | number | 配置的激活半径。 |
current_location | location table | 当支撑实体存在时的位置快照。 |
entity_type | string | 当支撑实体存在时的 Bukkit 实体类型。 |
方法
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
is_valid() | - | boolean | NPC 是否仍然拥有一个有效的支撑实体。 |
get_location() | - | location table | 当前 NPC 位置;若实体不可用,则返回生成位置。 |
get_eye_location() | - | location table | 当前的视线位置,兜底为生成位置。 |
get_activation_radius() | - | number | 当前配置的激活半径。 |
get_nearby_players(radius) | number | table | NPC 所在世界中球形距离范围内的玩家包装对象。不筛选游戏模式;请使用非负半径。 |
face_direction_or_location(target) | vector、location 或 entity table | nil | 朝向方向或同一世界的位置。带 world 或 current_location 的表视为位置,只有坐标时视为方向。巡逻正在移动时不做任何事。 |
say_greeting(player?) | player、UUID、名称或 nil | nil | 发送配置好的问候语。可用时默认发给触发的玩家。 |
say_dialog(player?) | player、UUID、名称或 nil | nil | 发送配置好的对话。可用时默认发给触发的玩家。 |
say_farewell(player?) | player、UUID、名称或 nil | nil | 发送配置好的告别文本。可用时默认发给触发的玩家。 |
play_model_animation(name) | string | nil | 若存在自定义模型动画则播放它。否则是安全的空操作。 |
patrol_pause() | - | boolean | 暂停已配置的巡逻。 |
patrol_resume() | - | boolean | 取消脚本等待或临时移动并恢复巡逻。 |
walk_to(x, y, z) | 三个数字 | boolean | 走到相对于设定生成点的偏移,然后恢复巡逻。自动求解长距离路线。 |
hold(x, y, z) | 三个数字 | boolean | 走到相对于设定原点的偏移并停留。 |
teleport(x, y, z) | 三个数字 | boolean | 传送到相对于设定原点的偏移,要求目标区域进行实体 tick。 |
remove() | - | nil | 通过原因 OTHER 的正常移除流程移除当前 NPC 实体,不删除预设的放置数据。不要从 on_remove 调用,否则会再次进入移除流程。 |
若 NPC 没有配置巡逻控制器,或请求无法被接受,移动方法会返回 false。路线配置和长距离行为请参阅 NPC 与首领巡逻。
context.player
context.player 在 on_npc_interact、on_npc_proximity_enter、on_npc_proximity_leave 以及监视区域的进入/离开钩子中可用。在不涉及玩家的生命周期钩子中,它为 nil。
这是 FMM 脚本也使用的共享 MagmaCore 玩家包装对象。它提供生命值、药水效果、send_message、show_title、show_action_bar、get_held_item、射线检测等。Boss 能力有自己的玩家包装对象,其方法列表不能与此互换。完整共享列表请参阅 Lua API 参考。常用项如下:
| 字段 / 方法 | 说明 |
|---|---|
name | 玩家名称。 |
uuid | 玩家 UUID。 |
current_location | 每次访问字段时实时读取玩家位置。先前保存的位置表仍是当时的快照。 |
get_eye_location() | 当前玩家视线位置。 |
send_message(text) | 发送一条聊天消息。支持颜色代码。 |
在共享的辅助函数中使用 context.player 之前,请始终进行 nil 检查。
entity_type 是小写的在共享的 MagmaCore 实体表上,entity_type 是 Bukkit 名称的小写形式("player"、"zombie")。只有 context.npc.entity_type 和 EliteMobs 的 Boss 能力实体表使用大写形式。如果一个脚本必须同时兼容两者,请进行不区分大小写的比较。
EliteMobs 向每一张共享实体表添加的字段
EliteMobs 运行时会为共享 MagmaCore 实体表添加字段,包括 context.player、context.npc:get_nearby_players(...) 返回的包装对象,以及 FreeMinecraftModels 道具脚本的包装对象:
| 字段 | 类型 | 说明 |
|---|---|---|
is_elite | boolean | 若 EliteMobs 将该实体作为精英追踪则为 true |
is_custom_boss | boolean | 若它是自定义 Boss 则为 true(当 is_elite 为 false 时始终为 false) |
is_significant_boss | boolean | 生命值倍率大于 1 的自定义 Boss 为 true。不检查是否作为增援召唤。 |
elite | table 或 nil | 仅存在于精英上。见下文 |
elite_player | table 或 nil | 在 EliteMobs 构建的共享玩家包装对象上,若玩家的精英背包已加载,则提供 tier。每次访问都新建表,保存的表仍保留原来的 tier。 |
can_receive_hostile_effect(actor_uuid) | boolean | 根据共享敌对目标规则和 EliteMobs 魔法武器授权,检查玩家触发者。请传入有效 UUID 字符串。 |
elite 子表:
| 字段 / 方法 | 类型 | 说明 |
|---|---|---|
elite.level | number | 精英等级 |
elite.name | string 或 nil | 精英显示名称 |
elite.health | number | 精英当前生命值(实时读取) |
elite.max_health | number | 精英最大生命值(实时读取) |
elite.is_custom_boss | boolean | 与顶层同名字段的值相同 |
elite.health_multiplier | number | 配置的生命值倍率 |
elite.damage_multiplier | number | 配置的伤害倍率 |
elite:remove() | — | 让该精英消失 |
-- Warn the approaching player if a real boss is loose near this NPC
on_npc_proximity_enter = function(context)
if context.player == nil then return end
local here = context.npc:get_location()
local nearby = context.world:get_nearby_entities(here.x, here.y, here.z, 40)
for i = 1, #nearby do
if nearby[i].is_significant_boss then
context.player:send_message("&cA boss is nearby: " .. tostring(nearby[i].elite.name))
return
end
end
end
EliteMobs 的 Boss 能力包装对象使用独立的构建器和不同的字段集。部分名称重合,例如 is_elite,但共享的嵌套 elite 表不能与 Boss 能力的方法互换。相关列表请参阅 Boss 与实体。FMM 提供自己的 EliteMobs 适配器;不要假定 EliteMobs 构建的共享包装对象上的每个附加字段都存在于 FMM 中。
context.event
只有钩子既没有事件也没有触发者时,context.event 才为 nil。它是共享的 MagmaCore 事件表:
| 字段 / 方法 | 说明 |
|---|---|
is_cancelled | 创建表时的取消状态快照,不可取消的事件为 false。调用 cancel() 或 uncancel() 后不会更新。 |
cancel() | 仅在底层事件可取消时存在,用于取消该事件。 |
uncancel() | 仅在底层事件可取消时存在,用于清除取消状态。 |
player | 事件的触发者(例如进行交互的玩家),以玩家包装对象的形式给出(存在时)。 |
对于交互/邻近触发的玩家,请优先使用 context.player(这些钩子中它一定会被设置)。
目前 on_npc_interact 只提供玩家,不提供 Bukkit 事件。因此其事件表有 player 和 is_cancelled: false,却没有 cancel 或 uncancel 方法。NPC 交互脚本不能通过此表取消普通 NPC 菜单。
交互流程会忽略已取消的 Bukkit 交互,阻止使用命名牌或与正在编辑的 NPC 交互,并为每个玩家施加一 tick 的交互去重。若任务对话目标处理了本次交互,就会在 Lua 钩子之前返回。否则钩子先于普通 NPC 菜单/动作分派运行。邻近事件也不可取消;区域钩子只提供触发者,没有 Bukkit 事件。这些路径同样不提供 cancel() 或 uncancel()。
状态、调度器与冷却
context.state 属于这个独立的 NPC 脚本实例,持续到实例结束。同一 NPC 上其他脚本的状态与它分开。
context.scheduler 是共享的 MagmaCore 调度器。MagmaCore 的名称和 EliteMobs 的 run_after / run_every 名称都可以使用 —— 它们是同一行为的别名:
| 方法 | 参数 | 说明 |
|---|---|---|
run_later(ticks, callback) / run_after(ticks, callback) | number, function | 延迟之后运行一次。返回一个任务 ID。 |
run_repeating(delay, interval, callback) | number, number, function | 在初始延迟之后重复运行。返回一个任务 ID。 |
run_every(interval, callback) | number, function | 每 interval ticks 运行一次(初始延迟为 0)。返回一个任务 ID。 |
cancel(task_id) / cancel_task(task_id) | number | 取消一个自有任务。 |
调度器回调会收到一个全新的 context。它们不会收到原始的 context.player 或 context.event。当 NPC 被移除时,所有自有任务都会被自动取消。
context.cooldowns 是共享的 MagmaCore 冷却表:
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
local_ready(key?) | string | boolean | 本地冷却结束时返回 true。 |
local_remaining(key?) | string | number | 剩余的 ticks,就绪时为 0。 |
check_local(key?, duration) | string, number | boolean | 若就绪,则开始冷却并返回 true。 |
set_local(duration, key?) | number, string | nil | 设置或重置冷却。 |
global_ready() | - | boolean | 共享的全局冷却就绪时返回 true。 |
set_global(duration) | number | nil | 启动全局冷却。 |
check_local 的时长始终是第二个参数,默认键应写作 check_local(nil, 60)。set_local 则把时长放在第一位。默认键包含脚本文件名。同一 NPC 适配器上的脚本共享显式本地键和全局冷却存储;除非有意协调,否则请使用不同的键。时长按单调时钟的经过时间计算,每个名义 tick 为 50 ms,不是数服务器 tick;非正时长会清除冷却。设置全局冷却不会自动阻止钩子或 check_local,请自行检查 global_ready()。
NPC 脚本现在使用共享的 MagmaCore 冷却参数顺序(check_local(key?, duration)),与 Boss 能力和 FreeMinecraftModels 脚本一致。更早的实验性 NPC 版本使用的是 check_local(duration, key?) —— 请把旧脚本更新为共享的参数顺序。
示例:靠近时挥手
这个脚本让 NPC 面向检测到进入的玩家,并播放 wave 自定义模型动画。邻近进入取决于五秒扫描,而不是每个移动数据包。带键的冷却会在重复进入通知出现时限制重播,但不会提高扫描频率。
return {
api_version = 1,
priority = 0,
on_npc_proximity_enter = function(context)
if context.player == nil then return end
if context.cooldowns:check_local("wave:" .. context.player.uuid, 60) then
context.npc:face_direction_or_location(context.player.current_location)
context.npc:play_model_animation("wave")
end
end
}
当 NPC 没有自定义模型,或该模型没有对应动画时,play_model_animation(name) 会安全地什么都不做。
性能建议
- 保持
on_game_tick钩子精简。初始化后,其时钟每个服务器 tick 运行,通常每秒 20 次。没有此钩子的脚本仍可能产生每 tick 区域扫描或显式调度的工作。 - 对于邻近相关的行为,请优先使用
on_npc_proximity_enter和on_npc_proximity_leave,而不是每 tick 轮询附近玩家。 - 使用
context.cooldowns:check_local(...)来限制动画、音效和粒子爆发的频率。 - 当某个行为不需要每 tick 执行时,使用
context.scheduler:run_repeating(...)并设置一个合理的间隔。 - 避免在 Lua 中进行大范围搜索。
context.npc:get_nearby_players(radius)用于小范围的局部检查没有问题,但大范围扫描应当留在插件运行时中完成。
相关页面
- 创建 NPC —— NPC 配置字段,包括
activationRadius - Lua 入门指南 —— Boss Lua 能力
- 脚本引擎 —— 共享的 Lua 概念与统一运行时
- Lua API 参考 ——
context.world、context.player、context.zones等的完整方法列表
