跳至主要內容

Lua 腳本:鉤子與生命週期

webapp_banner.jpg

本頁涵蓋 Lua 能力可以定義的每個鉤子、鉤子執行順序、每個 Boss 如何取得自己的隔離執行階段,以及沙盒中可用的標準函式庫函式。

如果你還沒有編寫過 Lua 能力,請先從入門指南開始。

Boss 能力鉤子

本頁記載的是位於 plugins/EliteMobs/powers/ 的 Boss Lua 能力的鉤子。NPC Lua 腳本使用它們自己的 plugins/EliteMobs/npc_scripts/ 資料夾,以及 NPC 專屬的鉤子,例如 on_npc_interact 與 on_npc_proximity_enter;請見 NPC 腳本。


鉤子參考​

每個 Lua 能力檔案都會回傳一個表。該表中的每個鍵(除了 api_version 與 priority 之外)都必須是下方所列鉤子之一。每當對應的遊戲事件觸發時,執行階段就會呼叫相符的函式。

鉤子觸發時機context.player 是否可用?
on_spawn精英怪物生成時否
on_game_tick執行階段時鐘啟用且未暫停時,每個伺服器刻執行一次;20 TPS 下通常間隔 50 毫秒否
on_boss_damagedBoss 受到任何來源的傷害否
on_boss_damaged_by_playerBoss 受到玩家的傷害是
on_reinforcement_damaged_by_player此 Boss 召喚的增援受到玩家傷害;在召喚者的能力上執行是
on_boss_damaged_by_eliteBoss 受到另一個精英怪物的傷害否
on_player_damaged_by_boss某位玩家受到此 Boss 的傷害是
on_enter_combatBoss 進入戰鬥是
on_exit_combatBoss 離開戰鬥否
on_healBoss 治療時否
on_boss_target_changedEliteMobs 回報玩家目標事件,包含首次取得目標;不涵蓋每次目標變更或失去目標是
on_deathBoss 死亡否
on_phase_switch階段型 Boss 切換到新階段否
on_mind_action原生 Mind 向附加能力請求動作時;請參閱動作上下文否
on_zone_enter某個實體進入被監看的區域是(若該實體為玩家)
on_zone_leave某個實體離開被監看的區域是(若該實體為玩家)

當 context.player 標示為「否」時,存取它會回傳 nil。使用前請務必進行 nil 檢查。

on_boss_damaged 與更具體的傷害鉤子使用不同事件路徑。一次攻擊可能觸發兩者,不應視為互斥。目標鉤子無法透過這個 Lua 事件表取消,也不會回報先前的目標。

Lua 區域不會觸發 on_zone_enter / on_zone_leave

這兩個鉤子會被載入器接受並驗證,但 Lua 能力中沒有任何東西能讓它們觸發。它們只會由 EliteScript 的區域監聽器派發,而該監聽器是在同一個 Boss 上的 YAML eliteScript: 於其 Events: 中列出 ZoneEnterEvent 或 ZoneLeaveEvent 時才會啟動。只有 Lua 能力的 Boss 會毫無怨言地載入含有這些鉤子的檔案,然後永遠不會呼叫它們。

Lua 監看器是另一套機制:context.zones:watch_zone(...) 與 context.script:zone(...):watch(...) 會直接呼叫你傳給它們的 on_enter / on_leave 回呼,而永遠不會經過這些頂層鉤子。除非你是刻意要把 Lua 能力與 YAML 區域腳本搭配使用,否則請使用監看器。

YAML 區域事件會送達該 Boss 的所有相符能力。事件包含進入或離開的實體,但沒有來源區域或腳本的識別碼。因此,多個 YAML 監聽器可能為同一實體傳送多個事件。

NPC Lua 腳本則不同。它們的 context.zones 來自 MagmaCore,其 watch 會派發 on_zone_enter / on_zone_leave。請參閱 NPC 腳本。

典型的多鉤子能力​

單一個 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 an explosion particle 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_reinforcement_damaged_by_player、on_boss_damaged_by_elite 與 on_player_damaged_by_boss。

欄位 / 方法類型說明
event.damage_amountdouble建立此事件表時的傷害快照
event.get_damage_amount()double讀取經過先前修改後的目前傷害
event.damage_causestringSpigot DamageCause 名稱(例如 "ENTITY_ATTACK"、"PROJECTILE")
event.damager實體表底層事件為實體傷害時的原始 Bukkit 傷害來源,on_boss_damaged 也包含這種情況。投射物命中時是投射物本身,不是射手。
event.projectileentity 表投射物實體,前提是傷害來源為投射物。
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

請用點號呼叫 event.set_damage_amount(n) 與 event.multiply_damage_amount(n)。這兩個 setter 直接讀取第一個引數,不會移除冒號呼叫傳入的 self。event:transfer_damage(target, amount) 支援冒號呼叫。

在玩家傷害精英的鉤子中,event.entity 代表受傷的精英。在 on_reinforcement_damaged_by_player 中,context.boss 仍是召喚者,context.player 是攻擊玩家。鉤子接收增援原本可修改的傷害事件。

這些鉤子也提供 event.is_damage_transfer 與 event:transfer_damage(target, amount)。目標必須是另一隻存活的精英,且能力所屬 Boss 有權影響它。數量必須有限且非負。事件已取消、數量為零或巢狀轉移會回傳 false。有效呼叫提交一次獨立且歸屬於玩家的攻擊,並回傳 true;不會扣除原本攻擊的傷害,也不保證其他監聽器允許新攻擊。若要分攤而非複製傷害,請自行用 event.set_damage_amount(...) 修改原傷害。轉移傷害不能遞迴再次轉移。

生成鉤子​

適用於 on_spawn。

欄位 / 方法類型說明
event.spawn_reasonstringSpigot SpawnReason 名稱
event.cancel_event()—取消該生成

死亡鉤子​

適用於 on_death。

欄位 / 方法類型說明
event.entityentity 表正在死亡的實體

區域鉤子​

適用於 on_zone_enter 與 on_zone_leave。

欄位 / 方法類型說明
event.entityentity 表進入或離開區域的實體

由 Lua 建立的區域監看器不會填入 context.event;它們會將進入/離開的實體直接傳遞給其回呼。

可取消的事件(一般情況)​

底層遊戲事件可取消的鉤子提供 event.cancel_event()。on_game_tick、on_mind_action 與一般排程回呼的 context.event 為 nil。on_heal 等鉤子有底層事件,但此表不提供治療專用欄位。呼叫前請確認文件列出的成員。

Boss 專用事件表不提供共用包裝器的 cancel()、uncancel() 或 is_cancelled。取消事件會停止向同一 Boss 的後續能力派發,但不會退出目前的 Lua 函式。若不應執行函式後續程式碼,請加上 return。

關於完整的 entity 表欄位,請見 Boss 與實體。關於傷害原因與生成原因的值,請見列舉與值。


鉤子執行順序​

Boss 的事件派發器依執行順序處理附加的腳本能力。Lua 能力的 priority 欄位決定其位置:

  • 數值較低者先執行(預設為 0)。
  • 相同優先級保留 Boss 能力的插入順序。不要假設不同載入路徑下都有某個固定順序。
return {
api_version = 1,
priority = -10, -- runs before most other powers

on_boss_damaged_by_player = function(context)
-- Record this power's last attacker before its later hooks run
context.state.last_attacker = context.player.uuid
end
}

YAML EliteScript 能力以優先級 0 參與同一派發器。負數 Lua 優先級在它們之前執行,正數在之後。這只為一隻 Boss 的一次事件排序,不控制獨立 Bukkit 事件或排程任務的順序。已取消的事件會停止派發,因此不一定每個能力都會收到事件。增援自己的能力執行後,只有事件仍未取消時,直接召喚者才會收到增援傷害鉤子。


執行階段模型​

每個能力與 Boss 組合一個執行階段​

每個附加的 Lua 能力都為每隻 Boss 擁有獨立執行階段。啟動時,EliteMobs 在全新的沙盒中執行已載入的原始碼並保存回傳表。Boss 消失或被移除時,執行階段關閉。因此,擁有兩個 Lua 能力的 Boss 有兩套獨立狀態表與環境。

這代表:

  • 檔案區域變數、函式及自行定義的 Lua 全域變數,僅屬於該 Boss 上該能力的執行階段。
  • 回傳表的鉤子函式絕不會在不同 Boss 之間共用。

載入器也會執行一次檔案以驗證回傳表。檔案頂層初始化應獨立於鉤子的上下文。僅使用逐刻鉤子的能力不需要 on_spawn 就能啟動時鐘。執行階段初始化與生成回呼是兩個獨立步驟。

狀態隔離​

每個執行階段都有自己的 context.state 表,與同一 Boss 的其他能力及其他 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 消失時:

  1. 執行階段會呼叫 shutdown()。
  2. 每個歸其所有的任務——無論是一次性的(run_after)還是重複性的(run_every)——都會被自動取消。
  3. 所有區域監看都會被清除。
  4. 此能力仍持有的 AI、警覺性、縮放與無敵修改會被還原,它設定的治療狀態會被清除,其追蹤火球控制器也會停止。

執行階段會在移除 Boss 時清理自己擁有的排程任務。部分 Java 遊戲輔助功能會啟動生命週期獨立的任務,請查閱對應說明。一般遊戲中不再需要的重複任務仍應取消:

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 消失或執行階段關閉時,逐刻工作及其擁有的區域監看任務會被取消。

暫停執行階段會保留 Lua 狀態及其擁有的任務,但跳過工作。Boss 事件路徑也會檢查能力是否受到抑制。移除 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.

執行預算​

Lua 能力執行時受到一個硬性執行預算的約束,而該預算是在 Lua VM 內部強制執行的,不只是事後測量。每個鉤子、每個排程回呼,甚至是檔案的初次求值,都在該預算之內執行:

限制數值
每個進入點的執行緒 CPU 時間50 ms
無法測量 CPU 時間時的備用經過時間250 ms
每個進入點的位元組碼指令數250,000

VM 會計算指令數,並每 1024 條指令檢查一次時間。錯誤會指出超出的限制:

Lua instruction budget exceeded (250000 instruction limit)
Lua CPU-time budget exceeded (50ms current-thread CPU limit)
Lua elapsed-time fallback budget exceeded (250ms fallback; current-thread CPU time unavailable)

無限 Lua 迴圈會在執行途中被中斷。巢狀呼叫共用進入點的預算。VM 無法中斷正在執行的 Java API 呼叫,呼叫返回後也沒有額外的 50 毫秒實際時間限制。因此,預算並不能保證高負荷的世界操作安全。

要維持在預算之內:

  • 避免在鉤子內使用無界限的迴圈。使用 context.scheduler:run_every(...) 將工作分散到各刻。
  • 讓 on_game_tick 處理器保持輕量——它們每一刻都會執行。
  • 將繁重的初始化移到 on_spawn 或 on_enter_combat 中,而不要每刻都重複執行。
  • 請記得,在大範圍區域上進行粒子迴圈是燒掉 250,000 個指令最常見的方式。請使用 coverage 來取樣較少的位置。

Lua 沙盒​

Lua 能力在一個沙盒化的 LuaJ 環境中執行。數個可能存取檔案系統或 Java 執行階段的全域變數已被移除。

已移除的全域變數​

以下標準 Lua 全域變數被設為 nil 且無法使用:

已移除原因
debug公開內部 VM 狀態
dofile檔案系統存取
io檔案系統存取
load任意程式碼載入
loadfile檔案系統存取
luajava直接存取 Java 類別
module模組系統(不需要)
os作業系統存取
package模組系統(不需要)
require模組系統 / 檔案系統存取

VM 的除錯鉤子在內部仍會被安裝 —— 執行預算就是靠它來計算指令數 —— 但 debug 表本身會在那之後立即被設為 nil,因此腳本無法存取它。

可用的標準函式庫​

沙盒保留 LuaJ 的標準函式庫,包含以下常用函式:

類別函式
Mathmath.abs、math.ceil、math.floor、math.max、math.min、math.random、math.sin、math.cos、math.sqrt、math.pi,以及所有其他 math.* 函式
Stringstring.byte、string.char、string.find、string.format、string.gsub、string.len、string.lower、string.match、string.rep、string.sub、string.upper,以及所有其他 string.* 函式
Tabletable.insert、table.remove、table.sort、table.concat,以及所有其他 table.* 函式
迭代器pairs、ipairs、next
型別type、tostring、tonumber、select、unpack
錯誤處理pcall、xpcall、error、assert
其他print、rawget、rawset、rawequal、rawlen、setmetatable、getmetatable

字串共用一個鎖定的 metatable。getmetatable("") 會回傳 false,而且即使腳本在自己的 string 表中新增或取代函式,("text"):upper() 這類字串方法呼叫仍一律使用標準字串函式。

提示

print 會寫入主控台,但建議使用 context.log:info(msg) 或 context.log:warn(msg)。Boss 日誌不會自動包含能力檔名。多個能力可能同時記錄時,請在訊息中加上檔名。


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)建立一個平移射線區域定義

em.location 提供共用的唯讀世界查詢:is_in_dungeon、is_protected、owned_by、owners、kinds_at 與 has_kind。第一個引數是位置表。位置缺漏、不是表、缺少世界或世界未載入時,回傳 false 或空表。缺少的座標預設為零;座標型別無效仍可能引發錯誤。為這些查詢建立位置時,請明確指定世界。簽章與回傳結構見 Lua API 參考。

-- Skip a destructive mechanic inside claims or protected regions
if em.location.is_protected(context.player:get_location()) then return end

建造權限檢查並沒有對應的 Lua 綁定。em.location.is_protected 只告訴你是否有任何保護外掛聲明了該位置;它不會解析個別玩家的建造權限。

區域建構器會回傳可串連的表,附帶 :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].is_player then
entities[i]:apply_potion_effect("SLOWNESS", 60, 1)
end
end
end
}

關於區域形狀、篩選條件、監看器與目標選取模式的完整說明,請見區域與目標選取。


後續步驟​