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 tick 執行一次(初始延遲為 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 | 剩餘 tick 數,就緒時為 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等的完整方法清單
