跳到主要内容

Lua 脚本:NPC 脚本

webapp_banner.jpg

EliteMobs NPC Lua 脚本是独立的 .lua 文件,可以挂接到 NPC 配置上。它们与 Boss Lua 能力是分开的:Boss 能力位于 plugins/EliteMobs/powers/,而 NPC 脚本位于 plugins/EliteMobs/npc_scripts/

NPC 脚本现在与 Boss 能力、FreeMinecraftModels 道具以及 FMM 物品运行在同一个统一的 MagmaCore 脚本运行时上。这意味着 NPC 脚本可以获得完整的共享脚本接口 —— context.world(包括 strike_lightning)、context.zonescontext.schedulercontext.cooldownscontext.logcontext.eventcontext.player —— 外加一个 NPC 专属的 context.npc 表。凡是 MagmaCore 向脚本暴露的内容,在这里同样可用。

实验性功能

NPC Lua 脚本仍处于实验阶段。NPC 专属的钩子以及 context.npc 辅助方法可能会发生变化。共享表(context.worldcontext.zonescontext.schedulercontext.cooldownscontext.logcontext.eventcontext.player)与脚本引擎Lua API 参考中记录的是同一批。


文件位置

在以下位置创建 NPC 脚本文件:

plugins/
EliteMobs/
npc_scripts/
wave.lua

子文件夹被递归扫描。但脚本是仅按文件名注册的,因此 npc_scripts/wave.luanpc_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


脚本结构

每个 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在 NPC 有效期间的每个服务器 tick 运行。请保持它非常轻量。
on_npc_interactfunction玩家与该 NPC 交互时运行。
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 邻近钩子使用 NPC 配置中的 activationRadius 值。

钩子触发时机
on_npc_proximity_enter玩家从 NPC 激活半径之外移动到半径之内。
on_npc_proximity_leave玩家从 NPC 激活半径之内移动到半径之外。

这些钩子由服务端的邻近扫描器按 NPC、按玩家分别跟踪。站在某个 NPC 附近不会阻止另一个 NPC 触发它自己的进入事件,而持续待在半径之内也不会反复刷屏式地触发进入事件。

正常的问候、对话与任务指示行为仍会照常运行。Lua 钩子是在这些行为之上叠加的。


共享脚本接口

由于 NPC 脚本运行在统一运行时上,每个 NPC 钩子也会收到 FreeMinecraftModels 脚本所使用的那些共享 MagmaCore 上下文表。EliteMobs Boss 能力运行在同一个运行时上,但其中若干表使用的是 Boss 专属的变体。完整的方法列表请参阅 Lua API 参考脚本引擎

作用
context.world世界效果与查询:strike_lightningspawn_particleplay_soundset_block_atplace_temporary_blockspawn_entityspawn_fireworkget_nearby_entitiesget_nearby_playersraycast 等等。坐标形式(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))—— 每个都会返回一个数字句柄。watch(handle, on_enter, on_leave) 开始跟踪(这些回调触发的是你的 on_zone_enter / on_zone_leave 钩子,而不是你传入的那些函数);unwatch(handle) 停止跟踪。
context.schedulerrun_later(ticks, fn)run_repeating(delay, interval, fn)cancel(task_id)
context.cooldowns共享的 MagmaCore 冷却:local_readylocal_remainingcheck_localset_localglobal_readyset_global
context.loginfo(msg)warn(msg)error(msg) —— 写入服务器控制台。
context.event当前的 Bukkit 事件(存在时)。参见下文。
context.player触发交互的玩家(存在时)。参见下文。
context.state一个普通的 Lua 表,在该 NPC 被移除之前,对这个 NPC 脚本实例始终持续存在。

示例:交互时降下雷击

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 or locationnil朝向一个方向向量,或转向某个位置/玩家位置。
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 时传送。

若 NPC 没有配置巡逻,或请求无法被接受,移动方法会返回 false。请参阅 NPC 与首领巡逻


context.player

context.playeron_npc_interacton_npc_proximity_enteron_npc_proximity_leave 中可用。在不涉及玩家的生命周期钩子中,它为 nil

它就是共享的 MagmaCore 玩家包装对象 —— 与 Boss 能力和 FMM 脚本使用的是同一个完整的生物/玩家表,因此它暴露的内容远不止基础项(生命值、药水效果、send_messageshow_titleshow_action_barget_held_item、射线检测等等)。完整列表请参阅 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.playercontext.npc:get_nearby_players(...) 返回的包装对象,以及 FreeMinecraftModels 道具与物品脚本所看到的那些:

字段类型说明
is_eliteboolean若 EliteMobs 将该实体作为精英追踪则为 true
is_custom_bossboolean若它是自定义 Boss 则为 true(当 is_elitefalse 时始终为 false
is_significant_bossboolean对于生命值倍率大于 1 的自定义 Boss 为 true —— 实用的"这是一个真正的 Boss,而不是增援"判断
elitetable 或 nil仅存在于精英上。见下文

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 能力实体包装对象上,后者由独立的 Boss 端表构建器生成 —— 那一套请参阅 Boss 与实体


context.event

当钩子没有对应的 Bukkit 事件时,context.eventnil。存在时,它就是共享的 MagmaCore 事件表:

字段 / 方法说明
is_cancelled底层事件是否已被取消(仅对可取消事件有意义)。
cancel()在事件可取消时取消它。
uncancel()在事件可取消时撤销取消。
player事件的触发者(例如进行交互的玩家),以玩家包装对象的形式给出(存在时)。

对于交互/邻近触发的玩家,请优先使用 context.player(这些钩子中它一定会被设置)。


状态、调度器与冷却

context.state 是一个普通的 Lua 表,在该 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, functioninterval ticks 运行一次(初始延迟为 0)。返回一个任务 ID。
cancel(task_id) / cancel_task(task_id)number取消一个自有任务。

调度器回调会收到一个全新的 context。它们不会收到原始的 context.playercontext.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启动全局冷却。
统一的冷却 API

NPC 脚本现在使用共享的 MagmaCore 冷却参数顺序(check_local(key?, duration)),与 Boss 能力和 FreeMinecraftModels 脚本一致。更早的实验性 NPC 版本使用的是 check_local(duration, key?) —— 请把旧脚本更新为共享的参数顺序。


示例:靠近时挥手

这个脚本让 NPC 面向进入范围的玩家,并播放 wave 自定义模型动画。只要玩家一直停留在半径之内,邻近进入事件对每个 NPC/玩家组合已经只会触发一次;这里的冷却是为了避免快速的离开/再进入循环导致动画过于频繁地重播。

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 钩子保持精简。对每个定义了它的 NPC 脚本实例,它每秒会运行 20 次。(没有声明 on_game_tick 的脚本永远不会被 tick。)
  • 对于邻近相关的行为,请优先使用 on_npc_proximity_enteron_npc_proximity_leave,而不是每 tick 轮询附近玩家。
  • 使用 context.cooldowns:check_local(...) 来限制动画、音效和粒子爆发的频率。
  • 当某个行为不需要每 tick 执行时,使用 context.scheduler:run_repeating(...) 并设置一个合理的间隔。
  • 避免在 Lua 中进行大范围搜索。context.npc:get_nearby_players(radius) 用于小范围的局部检查没有问题,但大范围扫描应当留在插件运行时中完成。

相关页面