跳至主要內容

Lua 腳本:鉤子與生命週期

webapp_banner.jpg

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

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

Boss 能力鉤子

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


鉤子參考

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

Hook觸發時機context.player 是否可用?
on_spawn精英怪物生成時
on_game_tick在執行階段時鐘作用期間,每個伺服器刻(50 毫秒)觸發一次
on_boss_damagedBoss 受到任何來源的傷害
on_boss_damaged_by_playerBoss 受到玩家的傷害
on_boss_damaged_by_eliteBoss 受到另一個精英怪物的傷害
on_player_damaged_by_boss某位玩家受到此 Boss 的傷害
on_enter_combatBoss 進入戰鬥
on_exit_combatBoss 離開戰鬥
on_healBoss 治療時
on_boss_target_changedBoss 切換其目標
on_deathBoss 死亡
on_phase_switch階段型 Boss 切換到新階段
on_zone_enter某個實體進入被監看的區域是(若該實體為玩家)
on_zone_leave某個實體離開被監看的區域是(若該實體為玩家)

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

區域鉤子的來源

頂層的 on_zone_enteron_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_damagedon_boss_damaged_by_playeron_boss_damaged_by_eliteon_player_damaged_by_boss

欄位 / 方法類型說明
event.damage_amountdouble原始傷害值
event.damage_causestringSpigot DamageCause 名稱(例如 "ENTITY_ATTACK""PROJECTILE"
event.damagerentity 表造成傷害的實體。僅存在於「由實體造成傷害」的鉤子中。
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

生成鉤子

適用於 on_spawn

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

死亡鉤子

適用於 on_death

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

區域鉤子

適用於 on_zone_enteron_zone_leave

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

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

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

任何其底層遊戲事件為可取消的鉤子,都會公開 event.cancel_event()。如果某個鉤子的 context.eventnil(例如 on_game_tickon_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 消失時:

  1. 執行階段會呼叫 shutdown()
  2. 每個歸其所有的任務——無論是一次性的(run_after)還是重複性的(run_every)——都會被自動取消。
  3. 所有區域監看都會被清除。

你永遠不需要在 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_spawnon_enter_combat 中,而不要每刻都重複執行。

Lua 沙盒

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

已移除的全域變數

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

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

可用的標準函式庫

Lua 標準函式庫的其餘一切都能正常運作:

類別函式
Mathmath.absmath.ceilmath.floormath.maxmath.minmath.randommath.sinmath.cosmath.sqrtmath.pi,以及所有其他 math.* 函式
Stringstring.bytestring.charstring.findstring.formatstring.gsubstring.lenstring.lowerstring.matchstring.repstring.substring.upper,以及所有其他 string.* 函式
Tabletable.inserttable.removetable.sorttable.concat,以及所有其他 table.* 函式
Iteratorspairsipairsnext
Typetypetostringtonumberselectunpack
Error handlingpcallxpcallerrorassert
Otherprintrawgetrawsetrawequalrawlensetmetatablegetmetatable
提示

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
}

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


後續步驟