Lua 腳本:範例與模式
本頁包含 EliteMobs Lua 能力的完整工作範例,以及實用模式、最佳實踐和技巧。每個範例都附有其功能和原因的說明。
如果你是 Lua 能力的新手,請從入門指南開始。完整 API 詳情請參閱 API 參考、Boss 與實體、世界與環境、區域與目標選擇和列舉。
範例:使用腳本工具進行基於區域的目標選擇
本範例教授: 如何使用 context.script 從 Lua 建立 EliteScript 風格的區域幾何,生成粒子並對區域內的實體造成傷害。
完整能力檔案(點擊展開)
return {
api_version = 1,
on_boss_damaged_by_player = function(context)
local cone = context.script:zone({
shape = "CONE",
Target = { targetType = "SELF", offset = "0,1,0" },
Target2 = { targetType = "NEARBY_PLAYERS", range = 20 },
radius = 5
})
context.script:spawn_particles(cone:full_target(0.4), { particle = "FLAME", amount = 1, speed = 0.05 })
context.script:damage(cone:full_target(), 1.0, 1.5)
end
}
說明
- 區域建立 --
context.script:zone(...)使用與 EliteScript 區域相同的欄位名稱建立錐形。Target設定錐形原點(Boss 本身,向上偏移1格),Target2設定目的地(20格內最近的玩家)。radius控制錐形的張開程度。 - 粒子生成 --
cone:full_target(0.4)傳回一個以 40% 覆蓋率解析為錐形內所有位置的目標控制碼。 - 傷害 --
context.script:damage(cone:full_target(), 1.0, 1.5)命中完整錐形內的所有生物實體。
傳遞給 context.script 的區域和目標表使用 EliteScript 欄位名稱(targetType、shape、Target、Target2、range、offset、coverage)。
範例:狀態 + 排程器攻擊迴圈
本範例教授: 使用 context.state 追蹤執行時狀態,context.scheduler 執行重複任務,以及正確的戰鬥進入/退出生命週期。
完整能力檔案(點擊展開)
local function pick_action(context)
local roll = math.random(1, 2)
if roll == 1 then context.boss:play_model_animation("slam")
else context.boss:play_model_animation("roar") end
end
return {
api_version = 1,
on_spawn = function(context)
context.state.started = false
context.state.loop_task_id = nil
end,
on_enter_combat = function(context)
if context.state.started then return end
context.state.started = true
context.state.loop_task_id = context.scheduler:run_every(100, function(loop_context)
if loop_context.boss.exists then pick_action(loop_context) end
end)
end,
on_exit_combat = function(context)
if context.state.loop_task_id ~= nil then
context.scheduler:cancel_task(context.state.loop_task_id)
context.state.loop_task_id = nil
end
context.state.started = false
end
}
說明
- 狀態初始化 --
on_spawn設定初始狀態值。state表在此 Boss 實例的整個生命週期中持續存在。 - 戰鬥守衛 --
on_enter_combat在啟動迴圈前檢查狀態,防止多個重疊迴圈。 - 排程器模式 --
context.scheduler:run_every(100, callback)每 100 tick 執行回呼。回呼接收新鮮上下文。 - 退出時清理 --
on_exit_combat取消重複任務並重設狀態。
如果在 on_enter_combat 中啟動了重複任務,務必在 on_exit_combat 中取消它。
範例:命中時的火焰效果
完整能力檔案(點擊展開)
return {
api_version = 1,
on_player_damaged_by_boss = function(context)
if context.player == nil then return end
if not context.cooldowns:check_local("fire_touch", 60) then return end
context.player:set_fire_ticks(60)
context.world:spawn_particle_at_location(context.player.current_location, { particle = "FLAME", amount = 20, speed = 0.1 })
context.player:send_message("&cThe boss's touch burns!")
context.cooldowns:set_global(40)
end
}
範例:使用原生 Lua 區域的基於區域的 AoE 能力
完整能力檔案(點擊展開)
return {
api_version = 1,
on_spawn = function(context) context.state.aoe_task_id = nil end,
on_enter_combat = function(context)
if context.state.aoe_task_id ~= nil then return end
context.state.aoe_task_id = context.scheduler:run_every(60, function(tick_context)
if not tick_context.boss.exists then return end
if not tick_context.cooldowns:check_local("pulse_aoe", 60) then return end
local zone_def = { kind = "sphere", radius = 8, origin = tick_context.boss:get_location() }
local victims = tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #victims do
victims[i]:deal_custom_damage(4.0)
tick_context.world:spawn_particle_at_location(victims[i].current_location, { particle = "DUST", amount = 15, speed = 0, red = 128, green = 0, blue = 255 })
end
tick_context.world:spawn_particle_at_location(tick_context.boss:get_location(), { particle = "SPELL_MOB", amount = 40, speed = 0.1 })
tick_context.cooldowns:set_global(60)
end)
end,
on_exit_combat = function(context)
if context.state.aoe_task_id ~= nil then
context.scheduler:cancel_task(context.state.aoe_task_id)
context.state.aoe_task_id = nil
end
end
}
此範例使用原生 Lua 區域(context.zones:get_entities_in_zone())。腳本工具(context.script:zone(...))使用 EliteScript 欄位名稱。兩者都有效 -- 簡單形狀使用原生區域,需要高級目標解析時使用 context.script。
範例:多階段 Boss 機制
完整能力檔案(點擊展開)
local function phase_one_attack(context)
context.boss:play_model_animation("slam")
local zone_def = { kind = "sphere", radius = 5, origin = context.boss:get_location() }
local targets = context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #targets do targets[i]:deal_custom_damage(3.0) end
context.world:spawn_particle_at_location(context.boss:get_location(), { particle = "EXPLOSION", amount = 3, speed = 0 })
end
local function phase_two_attack(context)
context.boss:play_model_animation("frenzy")
local zone_def = { kind = "sphere", radius = 8, origin = context.boss:get_location() }
local targets = context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #targets do
targets[i]:deal_custom_damage(2.0)
targets[i]:apply_potion_effect("SLOWNESS", 40, 1)
end
context.world:spawn_particle_at_location(context.boss:get_location(), { particle = "DUST", amount = 30, speed = 0.2, red = 255, green = 0, blue = 0 })
context.world:play_sound_at_location(context.boss:get_location(), "entity.wither.ambient", 1.0, 1.5)
end
return {
api_version = 1,
on_spawn = function(context)
context.state.phase = 1
context.state.attack_task_id = nil
context.state.phase_switched = false
end,
on_enter_combat = function(context)
if context.state.attack_task_id ~= nil then return end
context.state.attack_task_id = context.scheduler:run_every(100, function(tick_context)
if not tick_context.boss.exists then return end
if not tick_context.cooldowns:check_local("phase_attack", 100) then return end
phase_one_attack(tick_context)
end)
end,
on_game_tick = function(context)
if not context.cooldowns:check_local("phase_check", 20) then return end
if context.state.phase ~= 1 then return end
local health_ratio = context.boss.health / context.boss.maximum_health
if health_ratio <= 0.5 then
context.state.phase = 2
context.state.phase_switched = true
context.log:info("Boss entering phase 2 at " .. tostring(math.floor(health_ratio * 100)) .. "% health")
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end
context.boss:play_model_animation("transform")
local nearby = context.players.nearby_players(40)
for i = 1, #nearby do
nearby[i]:send_message("&4&lThe boss enters a frenzy!")
nearby[i]:show_title("&4Phase 2", "&cThe boss is enraged!", 10, 40, 10)
end
context.state.attack_task_id = context.scheduler:run_every(40, function(tick_context)
if not tick_context.boss.exists then return end
if not tick_context.cooldowns:check_local("phase_attack", 40) then return end
phase_two_attack(tick_context)
end)
end
end,
on_exit_combat = function(context)
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end
end
}
on_game_tick 每個伺服器 tick 執行一次(每秒20次)。始終用冷卻檢查保護重邏輯。如果你的鉤子超過 50ms,EliteMobs 將自動停用該能力。
AI 生成提示
要讓 AI 可靠地生成 Lua 能力,確保提示包含:精確的鉤子名稱,原生 Lua 區域或腳本工具(指明哪個),帶鍵名和 tick 持續時間的本地和全域冷卻,自訂模型動畫名稱,目標選擇,效果類型,以及僅使用已文件化的方法名稱。
QC 檢查清單
- 檔案傳回恰好一個包含
api_version = 1的表。 - 鉤子名稱與鉤子列表精確匹配。
context.player在使用前用== nil保護。context.state欄位在on_spawn中初始化。- 每個
run_every在on_exit_combat中有對應的cancel_task。 - 排程器回呼使用回呼自身的上下文參數。
- 冷卻鍵是描述性字串,持續時間以 tick 為單位。
on_game_tick鉤子用冷卻檢查保護重邏輯。- 所有方法名存在於 API 參考中。
- 腳本工具表使用 EliteScript 欄位名稱。
- 原生區域定義使用
kind、radius、origin、destination等。 - 能力不在鉤子或回呼中呼叫阻塞操作。
- 粒子規格使用有效的 Bukkit 粒子列舉名(UPPER_CASE)。
最佳實踐
-
從小鉤子開始並驗證。 寫一個發送日誌訊息的
on_spawn。確認它觸發後再繼續建構。 -
保持輔助函式為區域性。 在返回表上方宣告如
local function pick_action(context)的輔助函式。這使它們不在全域作用域中,避免與同一執行時中載入的其他 Lua 能力衝突。 -
將幾何放入腳本工具。 如果需要錐體、旋轉射線、平移射線或動畫區域,使用帶有 EliteScript 欄位名的
context.script:zone(...)。腳本工具重用了經過實戰檢驗的 EliteScript 區域引擎。 -
使用
context.state儲存執行時狀態。 不要使用 Lua 全域變數。context.state限定於單一 Boss 實例,並在該 Boss 的生命週期內跨鉤子持久化。 -
使用命名的本地冷卻鍵。 不要使用簡單數字,使用描述性鍵如
"fire_touch"或"aoe_pulse"。這使除錯更容易,並防止同一能力中不同冷卻之間的意外衝突。 -
保持
on_game_tick輕量。 始終用冷卻檢查保護。如果你的邏輯每個 tick 都執行,必須在遠少於 50ms 內完成,否則能力將被停用。 -
完成時取消重複任務。 每個
run_every必須在on_exit_combat(可能還有on_death)中有對應的cancel_task。洩漏的任務浪費 CPU 並可能在 Boss 消失後導致空參考錯誤。 -
在排程器回呼中使用新鮮上下文。 排程器回呼(
run_every、run_after)接收新鮮上下文作為參數。始終使用該參數 -- 而非外部context-- 因為外部上下文可能包含過時的快照。 -
用
context.log:info()記錄狀態轉換。 在開發期間,為階段切換、冷卻開始和排程器啟動/停止新增日誌。部署前刪除或更改為context.log:debug()。 -
重用現有的 EliteScript 文件。 區域、目標、相對向量 和 條件 頁面記錄了腳本工具接受的相同欄位名。不要在你的 Lua 能力中重複該資訊 -- 只需參考它。
常見初學者錯誤
-
在排程器回呼中使用外部
context。 外部上下文擷取了鉤子執行時的快照。在run_every或run_after回呼內,始終使用回呼自身的參數(例如tick_context),它會給你一個新鮮的快照。 -
忘記取消重複任務。 如果你在
on_enter_combat中啟動了run_every但從未取消,任務會一直執行直到 Boss 從伺服器移除,即使戰鬥已結束。 -
不在
on_spawn中初始化狀態。 如果你在on_game_tick中讀取context.state.phase但從未在on_spawn中設定它,它將是nil,你的比較將表現異常。 -
沒有 nil 守衛就檢查
context.player。 在on_player_damaged_by_boss等鉤子中,玩家幾乎總是可用的 -- 但「幾乎總是」不是「總是」。一個缺失的 nil 守衛可以使能力崩潰。 -
在腳本工具呼叫中使用 Lua 風格欄位名。 腳本工具期望
targetType、Target、Target2、shape-- 而非target_type、target、target2、zone_shape。不匹配的名稱會悄無聲息地不產生結果。 -
在
on_game_tick中沒有冷卻門控就執行重邏輯。 這個鉤子每個伺服器 tick 都會觸發。即使是在許多 Boss 上每秒重複 20 次的簡單算術也會累積。 -
編造方法名。 如果方法未在 API 參考中列出,則不存在。常見錯誤包括寫
entity:teleport(loc)而非entity:teleport_to_location(loc),或player:set_velocity(vec)而非player:set_velocity_vector(vec)。 -
使用
context.boss.health設定生命值。context.boss.health是唯讀快照。要治癒 Boss,使用context.boss:restore_health(amount)。 -
忘記
api_version = 1。 返回的表必須包含此欄位,否則 EliteMobs 不會載入該能力。
