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

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)
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
}

解説

  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)
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
}

解説

  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)
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
}

解説

  1. ステート初期化 -- on_spawnphase1attack_task_idnilphase_switchedfalseに設定します。
  2. フェーズ1攻撃ループ -- on_enter_combatは100ティックごとにphase_one_attackを実行する繰り返しタスクを開始します。
  3. on_game_tickでのフェーズチェック -- on_game_tickは毎サーバーティックに発火するため、まず20ティックのクールダウンをチェックします。
  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は毎サーバーティック(1秒間に20回)実行されます。check_local("phase_check", 20)のように、常にクールダウンチェックの後ろに重い処理を隠してください。フックが50msを超えると、EliteMobsは自動的にパワーを無効にします。


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)を生成します。


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を軽量に保つ。 常にクールダウンチェックの背後に保護する。ロジックが毎ティック実行される場合、50ms未満で完了しなければパワーは無効化される。

  • 繰り返しタスクが終わったらキャンセルする。 すべての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はパワーをロードしない。


次のステップ