Lua 腳本:鉤子與生命週期
本頁涵蓋 Lua 能力可以定義的每個鉤子、鉤子執行順序、每個 Boss 如何取得自己的隔離執行階段,以及沙盒中可用的標準函式庫函式。
如果你還沒有編寫過 Lua 能力,請先從入門指南開始。
本頁記載的是位於 plugins/EliteMobs/powers/ 的 Boss Lua 能力的鉤子。NPC Lua 腳本使用它們自己的 plugins/EliteMobs/npc_scripts/ 資料夾,以及 NPC 專屬的鉤子,例如 on_npc_interact 與 on_npc_proximity_enter;請見 NPC 腳本。
鉤子參考
每個 Lua 能力檔案都會回傳一個表。該表中的每個鍵(除了 api_version 與 priority 之外)都必須是下方所列鉤子之一。每當對應的遊戲事件觸發時,執行階段就會呼叫相符的函式。
| Hook | 觸發時機 | context.player 是否可用? |
|---|---|---|
on_spawn | 精英怪物生成時 | 否 |
on_game_tick | 在執行階段時鐘作用期間,每個伺服器刻(50 毫秒)觸發一次 | 否 |
on_boss_damaged | Boss 受到任何來源的傷害 | 否 |
on_boss_damaged_by_player | Boss 受到玩家的傷害 | 是 |
on_boss_damaged_by_elite | Boss 受到另一個精英怪物的傷害 | 否 |
on_player_damaged_by_boss | 某位玩家受到此 Boss 的傷害 | 是 |
on_enter_combat | Boss 進入戰鬥 | 是 |
on_exit_combat | Boss 離開戰鬥 | 否 |
on_heal | Boss 治療時 | 否 |
on_boss_target_changed | Boss 切換其目標 | 是 |
on_death | Boss 死亡 | 否 |
on_phase_switch | 階段型 Boss 切換到新階段 | 否 |
on_zone_enter | 某個實體進入被監看的區域 | 是(若該實體為玩家) |
on_zone_leave | 某個實體離開被監看的區域 | 是(若該實體為玩家) |
當 context.player 標示為「否」時,存取它會回傳 nil。使用前請務必進行 nil 檢查。
頂層的 on_zone_enter 與 on_zone_leave 鉤子是由 EliteScript/ScriptZone 事件觸發的。由 context.zones:watch_zone(...) 與 context.script:zone(...):watch(...) 所建立的 Lua 監看器,會直接呼叫它們自己的 on_enter / on_leave 回呼,而不會調用這些頂層鉤子。
典型的多鉤子能力
單一個 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_damaged、on_boss_damaged_by_player、on_boss_damaged_by_elite 與 on_player_damaged_by_boss。
| 欄位 / 方法 | 類型 | 說明 |
|---|---|---|
event.damage_amount | double | 原始傷害值 |
event.damage_cause | string | Spigot DamageCause 名稱(例如 "ENTITY_ATTACK"、"PROJECTILE") |
event.damager | entity 表 | 造成傷害的實體。僅存在於「由實體造成傷害」的鉤子中。 |
event.projectile | entity 表 | 投射物實體,前提是傷害來源為投射物。 |
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_reason | string | Spigot SpawnReason 名稱 |
event.cancel_event() | — | 取消該生成 |
死亡鉤子
適用於 on_death。
| 欄位 / 方法 | 類型 | 說明 |
|---|---|---|
event.entity | entity 表 | 正在死亡的實體 |
區域鉤子
適用於 on_zone_enter 與 on_zone_leave。
| 欄位 / 方法 | 類型 | 說明 |
|---|---|---|
event.entity | entity 表 | 進入或離開區域的實體 |
由 Lua 建立的區域監看器不會填入 context.event;它們會將進入/離開的實體直接傳遞給其回呼。
可取消的事件(一般情況)
任何其底層遊戲事件為可取消的鉤子,都會公開 event.cancel_event()。如果某個鉤子的 context.event 為 nil(例如 on_game_tick、on_heal),則代表沒有可供互動的底層事件。
關於完整的 entity 表欄位,請見 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 消失時:
- 執行階段會呼叫
shutdown()。 - 每個歸其所有的任務——無論是一次性的(
run_after)還是重複性的(run_every)——都會被自動取消。 - 所有區域監看都會被清除。
你永遠不需要在 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
}
逐刻時鐘的行為
只有當能力定義了 on_game_tick 鉤子時,某個 Lua 能力實例的內部刻時鐘才會運作。透過 context.zones:watch_zone(...) 或 context.script:zone(...):watch(...) 建立的區域監看,會建立它們自己歸其所有的重複性任務,而不會啟用該能力頂層的 on_game_tick 鉤子。
如果一個能力既沒有 on_game_tick,也沒有區域監看器,那麼就不會產生任何逐刻的工作。當 Boss 消失或執行階段關閉時,逐刻工作與區域監看器任務都會被自動取消。
錯誤與效能行為
EliteMobs 對 Lua 能力施加了嚴格的錯誤與效能限制:
例外
如果某個鉤子函式或排程回呼拋出了 Lua 錯誤(或某個 API 呼叫浮現了 Java 例外),該能力會針對該 Boss 實例立即被停用。執行階段會被關閉,所有歸其所有的任務都會被取消。
該錯誤會被記錄到伺服器主控台,連同該能力的檔名、行號,以及當時正在執行的鉤子:
[Lua] Error in 'frost_cone.lua' at line 35 during 'on_boss_damaged_by_player':
[Lua] -> ...explanation of what went wrong...
[Lua] -> Script has been disabled for this entity to prevent further errors.
執行預算
每次鉤子調用與每次回呼調用都會被計時。如果單一次呼叫耗時超過 50 毫秒,該能力就會被停用,並附帶一則主控台警告:
[Lua] my_power.lua took 73ms in 'on_game_tick' (limit: 50ms) — script disabled to prevent lag.
這可防止失控的腳本凍結伺服器。要維持在預算之內:
- 避免在鉤子內使用無界限的迴圈。使用
context.scheduler:run_every(...)將工作分散到各刻。 - 讓
on_game_tick處理器保持輕量——它們每一刻都會執行。 - 將繁重的初始化移到
on_spawn或on_enter_combat中,而不要每刻都重複執行。
Lua 沙盒
Lua 能力在一個沙盒化的 LuaJ 環境中執行。數個可能存取檔案系統或 Java 執行階段的全域變數已被移除。
已移除的全域變數
以下標準 Lua 全域變數被設為 nil 且無法使用:
| 已移除 | 原因 |
|---|---|
debug | 公開內部 VM 狀態 |
dofile | 檔案系統存取 |
io | 檔案系統存取 |
load | 任意程式碼載入 |
loadfile | 檔案系統存取 |
luajava | 直接存取 Java 類別 |
module | 模組系統(不需要) |
os | 作業系統存取 |
package | 模組系統(不需要) |
require | 模組系統 / 檔案系統存取 |
可用的標準函式庫
Lua 標準函式庫的其餘一切都能正常運作:
| 類別 | 函式 |
|---|---|
| Math | math.abs、math.ceil、math.floor、math.max、math.min、math.random、math.sin、math.cos、math.sqrt、math.pi,以及所有其他 math.* 函式 |
| String | string.byte、string.char、string.find、string.format、string.gsub、string.len、string.lower、string.match、string.rep、string.sub、string.upper,以及所有其他 string.* 函式 |
| Table | table.insert、table.remove、table.sort、table.concat,以及所有其他 table.* 函式 |
| Iterators | pairs、ipairs、next |
| Type | type、tostring、tonumber、select、unpack |
| Error handling | pcall、xpcall、error、assert |
| Other | print、rawget、rawset、rawequal、rawlen、setmetatable、getmetatable |
print 會寫到伺服器主控台,但用於輸出時請優先使用 context.log:info(msg) 或 context.log:warn(msg)。這些都會以能力名稱作為前綴,讓追溯訊息出自哪個能力變得更容易。
em 輔助命名空間
em 表在檔案載入時(任何鉤子執行之前)即可使用。它提供用於建構整個 API 所使用的 location 表、vector 表與區域定義的輔助建構子。
| 函式 | 用途 |
|---|---|
em.create_location(x, y, z [, world, yaw, pitch]) | 建立一個 location 表,可選擇加上世界名稱、yaw 與 pitch |
em.create_vector(x, y, z) | 建立一個 vector 表 |
em.zone.create_sphere_zone(radius) | 建立一個球體區域定義 |
em.zone.create_dome_zone(radius) | 建立一個半球(dome)區域定義 |
em.zone.create_cylinder_zone(radius, height) | 建立一個圓柱區域定義 |
em.zone.create_cuboid_zone(x, y, z) | 建立一個長方體(cuboid)區域定義 |
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
}
關於區域形狀、篩選條件、監看器與目標選取模式的完整說明,請見區域與目標選取。
