跳至主要內容

Lua 腳本:範例與模式

本頁包含 EliteMobs Lua 能力的完整工作範例,以及實用模式、最佳實踐和技巧。每個範例都附有其功能和原因的說明。

如果你是 Lua 能力的新手,請從入門指南開始。完整 API 詳情請參閱 API 參考Boss 與實體世界與環境區域與目標選擇列舉

webapp_banner.jpg


範例:使用腳本工具進行基於區域的目標選擇

本範例教授: 如何使用 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
}

說明

  1. 區域建立 -- context.script:zone(...) 使用與 EliteScript 區域相同的欄位名稱建立錐形。Target 設定錐形原點(Boss 本身,向上偏移1格),Target2 設定目的地(20格內最近的玩家)。radius 控制錐形的張開程度。
  2. 粒子生成 -- cone:full_target(0.4) 傳回一個以 40% 覆蓋率解析為錐形內所有位置的目標控制碼。
  3. 傷害 -- context.script:damage(cone:full_target(), 1.0, 1.5) 命中完整錐形內的所有生物實體。
EliteScript 欄位名稱

傳遞給 context.script 的區域和目標表使用 EliteScript 欄位名稱targetTypeshapeTargetTarget2rangeoffsetcoverage)。


範例:狀態 + 排程器攻擊迴圈

本範例教授: 使用 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
}

說明

  1. 狀態初始化 -- on_spawn 設定初始狀態值。state 表在此 Boss 實例的整個生命週期中持續存在。
  2. 戰鬥守衛 -- on_enter_combat 在啟動迴圈前檢查狀態,防止多個重疊迴圈。
  3. 排程器模式 -- context.scheduler:run_every(100, callback) 每 100 tick 執行回呼。回呼接收新鮮上下文
  4. 退出時清理 -- on_exit_combat 取消重複任務並重設狀態。
始終取消排程器任務

如果在 on_enter_combat 中啟動了重複任務,務必在 on_exit_combat 中取消它。


範例:命中時的火焰效果

完整能力檔案(點擊展開)
return {
api_version = 1,

on_player_damaged_by_boss = function(context)
-- Guard: the player may be nil in edge cases
if context.player == nil then
return
end

-- Check and set a 60-tick (3 second) local cooldown in one call
if not context.cooldowns:check_local("fire_touch", 60) then
return
end

-- Set the player on fire for 60 ticks (3 seconds)
context.player:set_fire_ticks(60)

-- Visual feedback: spawn flame particles at the player's location
context.world:spawn_particle_at_location(
context.player.current_location,
{ particle = "FLAME", amount = 20, speed = 0.1 }
)

-- Tell the player what happened
context.player:send_message("&cThe boss's touch burns!")

-- Set the global power cooldown so other powers on this boss
-- don't all fire at the same instant
context.cooldowns:set_global(40)
end
}

說明

  1. Nil 防護 -- context.player 是一個惰性鍵,會解析為該事件所涉及的玩家。在少數邊界情況下(例如玩家在事件觸發與鉤子執行之間離線),它可能為 nil。使用前務必先做防護。
  2. 本地冷卻 -- context.cooldowns:check_local("fire_touch", 60) 會原子性地做兩件事:檢查冷卻鍵 "fire_touch" 是否就緒,若就緒則立即把冷卻設為 60 tick。若冷卻尚未就緒,它會傳回 false 並提早結束函式。鍵 "fire_touch" 的作用範圍是此 Boss 實例 -- 擁有相同能力的其他 Boss 有各自獨立的冷卻。
  3. 著火 tick -- context.player:set_fire_ticks(60) 讓玩家著火 60 個遊戲 tick(3 秒)。這會直接呼叫底層的 Bukkit 方法。
  4. 粒子 -- context.world:spawn_particle_at_location(location, spec) 在特定位置生成粒子。規格表接受 particle(Bukkit 粒子列舉名稱)、amountspeed
  5. 訊息 -- context.player:send_message(text) 會送出帶顏色代碼的聊天訊息。像 &c(紅色)這類標準 Minecraft 顏色代碼會自動生效。
  6. 全域冷卻 -- context.cooldowns:set_global(40) 會讓此 Boss 上的所有能力進入 40 tick(2 秒)的冷卻。這可避免多個能力同時觸發。

範例:使用原生 Lua 區域的基於區域的 AoE 能力

完整能力檔案(點擊展開)
return {
api_version = 1,

on_spawn = function(context)
context.state.aoe_task_id = nil
end,

on_enter_combat = function(context)
-- Prevent duplicate loops
if context.state.aoe_task_id ~= nil then
return
end

context.state.aoe_task_id = context.scheduler:run_every(60, function(tick_context)
-- Make sure the boss is still alive
if not tick_context.boss.exists then
return
end

-- Check a local cooldown so this doesn't stack with other effects
if not tick_context.cooldowns:check_local("pulse_aoe", 60) then
return
end

-- Build a sphere zone centered on the boss's current position
local zone_def = {
kind = "sphere",
radius = 8,
origin = tick_context.boss:get_location()
}

-- Find all players inside the sphere
local victims = tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" })

-- Damage and show particles on each player found
for i = 1, #victims do
local victim = victims[i]
victim:deal_custom_damage(4.0)

tick_context.world:spawn_particle_at_location(
victim.current_location,
{ particle = "DUST", amount = 15, speed = 0, red = 128, green = 0, blue = 255 }
)
end

-- Spawn visual ring particles at the boss
tick_context.world:spawn_particle_at_location(
tick_context.boss:get_location(),
{ particle = "WITCH", amount = 40, speed = 0.1 }
)

-- Set global cooldown
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
}

說明

  1. 狀態設定 -- on_spawnaoe_task_id 初始化為 nil。這個任務 ID 會保存重複排程的參考。
  2. 重複攻擊 -- on_enter_combat 會啟動一個每 60 tick(3 秒)執行一次的重複任務。開頭的防護可避免在重新進入戰鬥時啟動第二個迴圈。
  3. 區域定義 -- zone_def 表使用原生 Lua 區域語法。kind 欄位指定形狀("sphere"),radius 設定大小,origin 則設為回呼執行當下 Boss 的位置。這表示該區域會隨 Boss 移動而跟隨。
  4. 查詢實體 -- tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" }) 會傳回球體內所有玩家表的 Lua 陣列。filter 選項接受 "players""elites""mobs""living"(預設)。
  5. 造成傷害 -- victim:deal_custom_damage(4.0) 造成 4 點歸屬於該 Boss 的傷害。這使用 EliteMobs 的自訂傷害系統,會考量護甲與其他戰鬥修正。
  6. 粒子回饋 -- 每位受害者身上會出現紫色塵埃粒子,Boss 位置也會出現環境粒子,以視覺方式標示這次脈衝。
  7. 清理 -- on_exit_combat 會取消該重複任務並清除狀態,做法與前一個範例相同。
原生區域 vs. 腳本工具區域

此範例使用原生 Lua 區域context.zones:get_entities_in_zone())。腳本工具(context.script:zone(...))使用 EliteScript 欄位名稱。兩者都有效 -- 簡單形狀使用原生區域,需要高級目標解析時使用 context.script


範例:多階段 Boss 機制

完整能力檔案(點擊展開)
local function phase_one_attack(context)
-- Slow, heavy slam
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)
-- Fast, frantic multi-hit
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

local function start_attack_loop(context)
if context.state.attack_task_id ~= nil then return end
local phase = context.state.phase or 1
local interval = phase == 2 and 40 or 100
local attack = phase == 2 and phase_two_attack or phase_one_attack
context.state.attack_task_id = context.scheduler:run_every(interval, function(tick_context)
if not tick_context.boss.exists or not tick_context.boss.is_in_combat then return end
if tick_context.cooldowns:check_local("phase_attack", interval) then
attack(tick_context)
end
end)
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)
start_attack_loop(context)
end,

on_game_tick = function(context)
-- Only check phase transition every 20 ticks (1 second) to stay lightweight
if not context.boss.is_in_combat then return end
if not context.cooldowns:check_local("phase_check", 20) then
return
end

-- Skip if already in phase 2
if context.state.phase ~= 1 then
return
end

-- Check health ratio
local health_ratio = context.boss.health / context.boss.maximum_health

if health_ratio <= 0.5 then
-- Transition to phase 2
context.state.phase = 2
context.state.phase_switched = true

context.log:info("Boss entering phase 2 at " .. tostring(math.floor(health_ratio * 100)) .. "% health")

-- Cancel the old attack loop
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end

-- Play transition effects
context.boss:play_model_animation("transform")

-- Announce the phase change to nearby players
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

start_attack_loop(context)
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
}

說明

  1. 狀態初始化 -- on_spawnphase 設為 1attack_task_id 設為 nilphase_switched 設為 false。這些值會在此 Boss 實例的所有鉤子之間保留。
  2. 攻擊迴圈 -- on_enter_combat 透過 start_attack_loop 恢復目前階段:階段1每100 tick攻擊一次,階段2每40 tick攻擊一次。離開並重新進入戰鬥會保留階段。階段1的攻擊建立半徑5格的球形區域,對其中的玩家造成傷害,並播放帶爆炸粒子的重擊動畫。
  3. on_game_tick 中的階段檢查 -- on_game_tick 在非戰鬥狀態下立即返回,然後檢查20 tick的冷卻("phase_check")。若 Boss 已進入階段2,也會提早返回。
  4. 生命值門檻 -- context.boss.health / context.boss.maximum_health 會給出目前生命值的比例。當它降到 50% 或以下時,轉換就會開始。
  5. 階段轉換 -- 舊的攻擊迴圈會被取消、播放變身動畫、附近玩家會收到訊息與標題,並啟動一個更快的新攻擊迴圈(每 40 tick 而非 100 tick)。
  6. 第 2 階段攻擊 -- phase_two_attack 使用更大的球體(8 格),每次命中的傷害略低但發動頻率高得多,會施加緩速藥水效果,並使用紅色塵埃粒子與凋零音效營造不同的感受。
  7. 記錄 -- context.log:info(...) 會寫入伺服器主控台,這在開發期間除錯階段轉換時非常寶貴。
  8. 清理 -- on_exit_combat 會取消目前作用中的任何攻擊迴圈,無論 Boss 處於哪個階段。
保持 on_game_tick 輕量

on_game_tick 每個伺服器 tick執行一次(每秒20次)。請用冷卻限制高負荷操作,例如 check_local("phase_check", 20)。超出執行預算會停用能力。


AI 生成提示

要讓 AI 可靠地生成 Lua 能力,確保提示包含:精確的鉤子名稱,原生 Lua 區域或腳本工具(指明哪個),帶鍵名和 tick 持續時間的本地和全域冷卻,自訂模型動畫名稱,目標選擇,效果類型,以及僅使用已文件化的方法名稱。

良好的提示範例

寫一個 Lua 能力,使用 on_enter_combat 啟動一個每 80 tick 執行一次的重複任務。每次執行時,建立一個以 Boss 為中心的原生 Lua 球體區域(半徑 6),查詢其中的玩家,並對每位玩家造成 2.0 點自訂傷害。使用本地冷卻鍵 "pulse",持續時間 80。在 on_exit_combat 中取消該任務。在每位受害者身上生成 DUST 粒子(red=0、green=255、blue=100)。

應額外納入的限制

  • 「傳回單一個包含 api_version = 1 的表。」
  • 「在 on_spawn 中初始化所有狀態欄位。」
  • 「以 if not tick_context.boss.exists then return end 保護排程回呼。」
  • 「在 on_exit_combat 中取消所有重複任務。」
  • 「不要自行發明方法名稱 -- 只使用 API 參考頁面上的方法。」
  • 「使用 context.cooldowns:check_local(key, ticks) 進行合併的檢查與設定。」

QC 檢查清單(供人工或 AI 審查)

  1. 檔案傳回恰好一個包含 api_version = 1 的表。
  2. 鉤子名稱與鉤子列表精確匹配。
  3. context.player 在使用前用 == nil 保護。
  4. context.state 欄位在 on_spawn 中初始化。
  5. 每個 run_everyon_exit_combat 中有對應的 cancel_task
  6. 排程器回呼使用回呼自身的上下文參數。
  7. 冷卻鍵是描述性字串,持續時間以 tick 為單位。
  8. on_game_tick 鉤子用冷卻檢查保護重邏輯。
  9. 所有方法名存在於 API 參考中。
  10. 腳本工具表使用 EliteScript 欄位名稱。
  11. 原生區域定義使用 kindradiusorigindestination 等。
  12. 能力不在鉤子或回呼中呼叫阻塞操作。
  13. 粒子規格使用有效的 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 輕量。 用冷卻限制高負荷操作,並為執行預算保留充足餘裕。

  • 完成時取消重複任務。 每個 run_every 必須在 on_exit_combat(可能還有 on_death)中有對應的 cancel_task。洩漏的任務浪費 CPU 並可能在 Boss 消失後導致空參考錯誤。

  • 在排程器回呼中使用新鮮上下文。 排程器回呼(run_everyrun_after)接收新鮮上下文作為參數。始終使用該參數 -- 而非外部 context -- 因為外部上下文可能包含過時的快照。

  • context.log:info() 記錄狀態轉換。 在開發期間,為階段切換、冷卻開始和排程器啟動/停止新增日誌。部署前刪除或更改為 context.log:debug()

  • 重用現有的 EliteScript 文件。 區域目標相對向量條件 頁面記錄了腳本工具接受的相同欄位名。不要在你的 Lua 能力中重複該資訊 -- 只需參考它。


常見初學者錯誤

  • 在排程器回呼中使用外部 context 外部上下文擷取了鉤子執行時的快照。在 run_everyrun_after 回呼內,始終使用回呼自身的參數(例如 tick_context),它會給你一個新鮮的快照。

  • 忘記取消重複任務。 如果你在 on_enter_combat 中啟動了 run_every 但從未取消,任務會一直執行直到 Boss 從伺服器移除,即使戰鬥已結束。

  • 不在 on_spawn 中初始化狀態。 如果你在 on_game_tick 中讀取 context.state.phase 但從未在 on_spawn 中設定它,它將是 nil,你的比較將表現異常。

  • 沒有 nil 守衛就檢查 context.playeron_player_damaged_by_boss 等鉤子中,玩家幾乎總是可用的 -- 但「幾乎總是」不是「總是」。一個缺失的 nil 守衛可以使能力崩潰。

  • 在腳本工具呼叫中使用 Lua 風格欄位名。 腳本工具期望 targetTypeTargetTarget2shape -- 而非 target_typetargettarget2zone_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 不會載入該能力。


後續步驟