メインコンテンツまでスキップ

Luaスクリプティング:例とパターン

このページには、EliteMobs Luaパワーの完全な動作例に加えて、実用的なパターン、ベストプラクティス、ヒントが含まれています。各例には、何をしているか、なぜそうしているかの説明が付いています。

Luaパワーが初めての方は、はじめにから始めてください。完全なAPIの詳細については、APIリファレンスボスとエンティティワールドと環境ゾーンとターゲティングEnumを参照してください。

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はコーンの原点(ボス自身、1ブロック上にオフセット)を設定し、Target2は目的地(20ブロック以内の最も近いプレイヤー)を設定します。radiusはコーンの広がりを制御します。

  2. パーティクル生成 -- cone:full_target(0.4)はコーン内のすべての位置に40%のカバレッジで解決するターゲットハンドルを返します。パーティクル仕様はEliteScriptパーティクルと同じフィールド名を使用します:particleamountspeed

  3. ダメージ -- context.script:damage(cone:full_target(), 1.0, 1.5)はコーン全体内のすべての生きているエンティティにヒットします。最初の数値(1.0)はベースダメージ量で、2番目(1.5)はプレイヤーに適用されるダメージ倍率です。

EliteScriptフィールド名

context.scriptに渡されるゾーンとターゲットのテーブルはEliteScriptフィールド名targetTypeshapeTargetTarget2rangeoffsetcoverage)を使用します。完全なリストはEliteScriptゾーンEliteScriptターゲットを参照してください。


例:ステート+スケジューラ攻撃ループ

この例で学べること: 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_spawncontext.state.startedfalseに、context.state.loop_task_idnilに設定します。stateテーブルはこのボスインスタンスの全生存期間にわたって永続します。
  2. 戦闘ガード -- on_enter_combatはループ開始前にcontext.state.startedをチェックし、複数の重複ループを防ぎます。
  3. スケジューラパターン -- context.scheduler:run_every(100, callback)は100ティック(5秒)ごとにコールバックを実行します。コールバックはフレッシュなコンテキストを受け取ります。
  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)はアトミックにチェックと設定を行います。
  3. ファイアーティック -- context.player:set_fire_ticks(60)はプレイヤーを60ティック(3秒)間燃やします。
  4. パーティクル -- context.world:spawn_particle_at_location(location, spec)は位置にパーティクルを生成します。
  5. メッセージ -- context.player:send_message(text)はカラーコード付きチャットメッセージを送信します。
  6. グローバルクールダウン -- context.cooldowns:set_global(40)はこのボスの全パワーを40ティックのクールダウンに設定します。

例:ネイティブLuaゾーンを使用したゾーンベースAoEパワー

この例で学べること: ネイティブLuaゾーンを作成・クエリして、エリア内のプレイヤーにダメージを与える方法。

完全なパワーファイル(クリックで展開)
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_idnilに初期化します。
  2. 繰り返し攻撃 -- on_enter_combatは60ティック(3秒)ごとに繰り返しタスクを開始します。
  3. ゾーン定義 -- zone_defテーブルはネイティブLuaゾーン構文を使用します。kindはシェイプを、radiusはサイズを、originはコールバック実行時のボスの現在位置を設定します。
  4. エンティティクエリ -- tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" })は球体内の全プレイヤーテーブルのLua配列を返します。
  5. ダメージ付与 -- victim:deal_custom_damage(4.0)はボスに帰属する4ポイントのダメージを与えます。
  6. パーティクルフィードバック -- 各犠牲者に紫色のダストパーティクル、ボスの位置にアンビエントパーティクルを表示します。
  7. クリーンアップ -- on_exit_combatは繰り返しタスクをキャンセルし、ステートをクリアします。
ネイティブゾーン vs. スクリプトユーティリティゾーン

この例はネイティブLuaゾーンcontext.zones:get_entities_in_zone())を使用します。スクリプトユーティリティ(context.script:zone(...))はEliteScriptフィールド名(shapeTargetTarget2)を使用します。どちらも動作します -- シンプルな形状にはネイティブゾーンを、EliteScriptの高度なターゲット解決が必要な場合はcontext.scriptを使用してください。


例:マルチフェーズボスメカニクス

この例で学べること: ステートを使用してボスのフェーズを追跡し、体力の閾値で行動を切り替える方法。

完全なパワーファイル(クリックで展開)
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_spawnphase1attack_task_idnilphase_switchedfalseに設定します。
  2. 攻撃ループ -- on_enter_combatstart_attack_loop で現在のフェーズを再開します。フェーズ1は100ティックごと、フェーズ2は40ティックごとに攻撃します。戦闘を離れて再開してもフェーズは維持されます。フェーズ1の攻撃は半径5ブロックの球を作り、内部のプレイヤーにダメージを与え、爆発パーティクル付きの打撃アニメーションを再生します。
  3. on_game_tick でのフェーズ確認 -- on_game_tick は戦闘中でなければ即座に終了し、その後20ティックのクールダウン("phase_check")を確認します。すでにフェーズ2の場合も早期終了します。
  4. 体力閾値 -- context.boss.health / context.boss.maximum_healthで現在の体力を割合で取得します。50%以下になるとトランジションが始まります。
  5. フェーズトランジション -- 古い攻撃ループをキャンセルし、変身アニメーションを再生し、近くのプレイヤーにメッセージとタイトルを送信し、より速い新しい攻撃ループを開始します(100ティックではなく40ティックごと)。
  6. フェーズ2攻撃 -- より大きな球体(8ブロック)、ヒットあたりのダメージはやや少ないがはるかに頻繁に発火、鈍化ポーション効果を適用、赤いダストパーティクルとウィザーサウンドを使用します。
  7. ログ -- context.log:info(...)はサーバーコンソールに書き込みます。
  8. クリーンアップ -- on_exit_combatはボスがどのフェーズにいるかに関係なく、現在アクティブな攻撃ループをキャンセルします。
on_game_tickを軽量に保つ

on_game_tick はサーバーの毎ティック(毎秒20回)実行されます。check_local("phase_check", 20) のようなクールダウンで重い処理を制限してください。実行予算を超えるとパワーが無効になります。


AI生成のヒント

AIにLuaパワーを確実に生成させたい場合、プロンプトに以下を含めてください:

  • 正確なフック名 -- 例:on_player_damaged_by_boss、「ボスがプレイヤーを攻撃したとき」ではない。
  • ネイティブLuaゾーンかスクリプトユーティリティか -- どちらかを指定。
  • ローカルおよびグローバルクールダウン -- キー名とティック単位の期間を指定。
  • カスタムモデルアニメーション名 -- AIは推測できません。提供してください。
  • ターゲット選択 -- context.playercontext.players.nearby_players(range)、またはゾーンクエリのいずれかを指定。
  • エフェクトタイプ -- ダメージ、ポーション、ファイアー、ベロシティなど。
  • ドキュメント化されたメソッド名のみ使用する。
  • スクリプトユーティリティの仕様はEliteScriptフィールド名を使用する。
  • スケジューラコールバックはフレッシュなコンテキストを受け取る。

良いプロンプト例

on_enter_combatを使用して80ティックごとに繰り返しタスクを開始するLuaパワーを書いてください。各ティックで、ボスを中心とするネイティブ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チェックリスト

  1. ファイルはapi_version = 1を含む1つのテーブルのみを返す。
  2. すべてのフック名はフックリストのエントリと正確に一致する。
  3. context.playerはnilになりうるフックで使用前に== nilでガードされている。
  4. context.stateフィールドはon_spawnで初期化されている。
  5. すべてのcontext.scheduler:run_every(...)にはon_exit_combatで対応するcontext.scheduler:cancel_task(...)がある。
  6. スケジューラコールバックは外側のcontextではなく、コールバック自身のコンテキストパラメータを使用している。
  7. クールダウンキーは説明的な文字列で、期間はティック単位。
  8. on_game_tickフックはクールダウンチェックの後ろに重い処理を隠している。
  9. すべてのメソッド名はAPIリファレンスに存在する。
  10. スクリプトユーティリティテーブルはEliteScriptフィールド名を使用している。
  11. ネイティブゾーン定義はkindradiusorigindestinationなどを使用している。
  12. パワーはフックやコールバック内でブロッキングまたは長時間実行操作を呼び出していない。
  13. パーティクル仕様は有効なBukkitパーティクルenum名をUPPER_CASEで使用している。

ベストプラクティス

  • 小さなフックから始めて検証する。 単一のon_spawnでログメッセージを送信するものを書く。発火を確認してから構築を進める。

  • ヘルパー関数はローカルに保つ。 local function pick_action(context)のようにリターンテーブルの上に宣言する。これによりグローバルスコープから除外され、同じランタイムにロードされた他のLuaパワーとの衝突を防ぐ。

  • ジオメトリはスクリプトユーティリティに入れる。 コーン、回転レイ、平行移動レイ、アニメーションゾーンが必要な場合は、EliteScriptフィールド名でcontext.script:zone(...)を使用する。スクリプトユーティリティは実績のあるEliteScriptゾーンエンジンを再利用する。

  • ランタイムステートにはcontext.stateを使用する。 Luaグローバル変数は使わない。context.stateは単一のボスインスタンスにスコープされ、そのボスの存続期間中フック間で永続する。

  • 名前付きローカルクールダウンキーを使用する。 単なる数値の代わりに、"fire_touch""aoe_pulse"のような説明的なキーを使用する。これによりデバッグが容易になり、同じパワー内の異なるクールダウン間の偶発的な衝突を防ぐ。

  • on_game_tick を軽量に保つ。 クールダウンで重い処理を制限し、実行予算に十分な余裕を持たせてください。

  • 繰り返しタスクが終わったらキャンセルする。 すべてのrun_everyにはon_exit_combat(場合によってはon_death)に対応するcancel_taskが必要。リークしたタスクはCPUを浪費し、ボスのデスポーン後にnull参照エラーを引き起こす可能性がある。

  • スケジューラコールバックではフレッシュなコンテキストを使用する。 スケジューラコールバック(run_everyrun_after)はパラメータとしてフレッシュなコンテキストを受け取る。常にそのパラメータを使用する -- 外側のcontextではなく -- 外側のコンテキストは古いスナップショットを保持している可能性がある。

  • context.log:info()でステートの遷移をログする。 開発中はフェーズ切り替え、クールダウン開始、スケジューラの開始/停止のログを追加する。デプロイ前に削除するかcontext.log:debug()に変更する。

  • 既存のEliteScriptドキュメントを再利用する。 ゾーンターゲット相対ベクトル条件のページは、スクリプトユーティリティが受け入れるのと同じフィールド名を文書化している。Luaパワー内でその情報を複製せず、参照するだけにする。


よくある初心者の間違い

  • スケジューラコールバック内で外側のcontextを使用する。 外側のコンテキストはフックが実行された時点のスナップショットをキャプチャする。run_everyrun_afterコールバック内では、常にコールバック独自のパラメータ(例:tick_context)を使用する。これによりフレッシュなスナップショットが得られる。

  • 繰り返しタスクのキャンセルを忘れる。 on_enter_combatrun_everyを開始してもキャンセルしなければ、戦闘終了後もボスがサーバーから削除されるまでタスクが実行され続ける。

  • on_spawnでステートを初期化しない。 on_game_tickcontext.state.phaseを読み取るがon_spawnで設定しなかった場合、nilになり比較が予期しない動作をする。

  • nilガードなしでcontext.playerをチェックする。 on_player_damaged_by_bossのようなフックでは、プレイヤーはほぼ常に利用可能だが、「ほぼ常に」は「常に」ではない。1つのnilガードの欠如がパワーをクラッシュさせる可能性がある。

  • スクリプトユーティリティ呼び出しでLuaスタイルのフィールド名を使用する。 スクリプトユーティリティはtargetTypeTargetTarget2shapeを期待する -- target_typetargettarget2zone_shapeではない。一致しない名前は黙って結果を生成しない。

  • クールダウンゲートなしでon_game_tickで重い処理を実行する。 このフックはサーバーの毎ティックで発火する。多くのボスにわたって毎秒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は読み取り専用のスナップショット。ボスを回復するにはcontext.boss:restore_health(amount)を使用する。

  • api_version = 1を忘れる。 返却テーブルにはこのフィールドが含まれている必要があり、そうでなければEliteMobsはパワーをロードしない。


次のステップ