跳到主要内容

Lua 脚本:NPC 脚本

webapp_banner.jpg

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_versionnumber必填。必须为 1。
prioritynumber可选。值越小越先运行。
on_spawnfunction在 NPC 生成之后运行。
on_removefunction在正常移除 NPC 时运行,早于脚本关闭和实体移除。
on_game_tickfunction脚本运行时初始化且时钟启动后,每个服务器 tick 运行。请保持轻量。
on_npc_interactfunction普通交互处理到达脚本步骤时运行;限制见下文。
on_npc_proximity_enterfunction玩家进入该 NPC 的激活半径时运行一次。
on_npc_proximity_leavefunction玩家离开该 NPC 的激活半径时运行一次。
on_zone_enterfunction玩家进入该脚本正在监视的区域时运行(参见 context.zones)。
on_zone_leavefunction玩家离开被监视的区域时运行。

区域监视只跟踪玩家 —— 怪物和其他实体永远不会触发 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.schedulerrun_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.loginfo(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 钩子中都可用。

字段​

字段类型说明
namestring配置中的 NPC 显示名称。
filenamestringNPC 配置文件名。
uuidstring运行时的 NPC UUID。
activation_radiusnumber配置的激活半径。
current_locationlocation table当支撑实体存在时的位置快照。
entity_typestring当支撑实体存在时的 Bukkit 实体类型。

方法​

方法参数返回值说明
is_valid()-booleanNPC 是否仍然拥有一个有效的支撑实体。
get_location()-location table当前 NPC 位置;若实体不可用,则返回生成位置。
get_eye_location()-location table当前的视线位置,兜底为生成位置。
get_activation_radius()-number当前配置的激活半径。
get_nearby_players(radius)numbertableNPC 所在世界中球形距离范围内的玩家包装对象。不筛选游戏模式;请使用非负半径。
face_direction_or_location(target)vector、location 或 entity tablenil朝向方向或同一世界的位置。带 world 或 current_location 的表视为位置,只有坐标时视为方向。巡逻正在移动时不做任何事。
say_greeting(player?)player、UUID、名称或 nilnil发送配置好的问候语。可用时默认发给触发的玩家。
say_dialog(player?)player、UUID、名称或 nilnil发送配置好的对话。可用时默认发给触发的玩家。
say_farewell(player?)player、UUID、名称或 nilnil发送配置好的告别文本。可用时默认发给触发的玩家。
play_model_animation(name)stringnil若存在自定义模型动画则播放它。否则是安全的空操作。
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_eliteboolean若 EliteMobs 将该实体作为精英追踪则为 true
is_custom_bossboolean若它是自定义 Boss 则为 true(当 is_elite 为 false 时始终为 false)
is_significant_bossboolean生命值倍率大于 1 的自定义 Boss 为 true。不检查是否作为增援召唤。
elitetable 或 nil仅存在于精英上。见下文
elite_playertable 或 nil在 EliteMobs 构建的共享玩家包装对象上,若玩家的精英背包已加载,则提供 tier。每次访问都新建表,保存的表仍保留原来的 tier。
can_receive_hostile_effect(actor_uuid)boolean根据共享敌对目标规则和 EliteMobs 魔法武器授权,检查玩家触发者。请传入有效 UUID 字符串。

elite 子表:

字段 / 方法类型说明
elite.levelnumber精英等级
elite.namestring 或 nil精英显示名称
elite.healthnumber精英当前生命值(实时读取)
elite.max_healthnumber精英最大生命值(实时读取)
elite.is_custom_bossboolean与顶层同名字段的值相同
elite.health_multipliernumber配置的生命值倍率
elite.damage_multipliernumber配置的伤害倍率
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?)stringboolean本地冷却结束时返回 true。
local_remaining(key?)stringnumber剩余的 ticks,就绪时为 0。
check_local(key?, duration)string, numberboolean若就绪,则开始冷却并返回 true。
set_local(duration, key?)number, stringnil设置或重置冷却。
global_ready()-boolean共享的全局冷却就绪时返回 true。
set_global(duration)numbernil启动全局冷却。

check_local 的时长始终是第二个参数,默认键应写作 check_local(nil, 60)。set_local 则把时长放在第一位。默认键包含脚本文件名。同一 NPC 适配器上的脚本共享显式本地键和全局冷却存储;除非有意协调,否则请使用不同的键。时长按单调时钟的经过时间计算,每个名义 tick 为 50 ms,不是数服务器 tick;非正时长会清除冷却。设置全局冷却不会自动阻止钩子或 check_local,请自行检查 global_ready()。

统一的冷却 API

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) 用于小范围的局部检查没有问题,但大范围扫描应当留在插件运行时中完成。