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

Luaスクリプティング:NPCスクリプト

webapp_banner.jpg

EliteMobsのNPC Luaスクリプトは、NPC設定にアタッチする独立した .lua ファイルです。ボスのLuaパワーとは別物で、ボスパワーは plugins/EliteMobs/powers/ に、NPCスクリプトは plugins/EliteMobs/npc_scripts/ に置かれます。

NPCスクリプトは、ボスパワーやFreeMinecraftModelsのプロップと共通のMagmaCoreランタイム上で動作します。context.world、context.zones、context.scheduler、context.cooldowns、context.log などの共通サービスと、NPC専用の context.npc テーブルを受け取ります。プレイヤーやイベントのデータはフックによって異なります。ボスパワーには独自のテーブルもあるため、NPCスクリプトでは共有APIの引数仕様を使用してください。

実験的機能

NPC Luaスクリプトはまだ実験的です。NPC固有のフックと context.npc のヘルパーは変更される可能性があります。共有テーブル(context.world、context.zones、context.scheduler、context.cooldowns、context.log、context.event、context.player)は、スクリプティングエンジンとLua APIリファレンスで文書化されているものと同一です。


ファイルの配置場所​

NPCスクリプトファイルは次の場所に作成します:

plugins/
EliteMobs/
npc_scripts/
wave.lua

サブフォルダも再帰的にスキャンされます。ただしスクリプトはファイル名のみで登録されるため、npc_scripts/wave.lua と npc_scripts/town/wave.lua は衝突します。ツリー全体でベース名が重複しないようにしてください。

検出時には大文字と小文字を区別せず、絶対パスの一定の順序に従って同じベース名から1つを選び、コピーの内容が異なる場合は採用・除外したパスをログに記録します。採用したファイルが壊れていても、除外した重複ファイルへの切り替えは行いません。設定から参照するときは、採用されたファイル名の大文字と小文字を正確に合わせてください。

NPC設定内での .lua 拡張子は省略可能です。- wave と - wave.lua はどちらも wave.lua に解決されます。NPC設定が存在しないスクリプトを参照している場合、EliteMobsは警告をログに出力し、NPC自体は通常どおりスポーンします。


NPCへのスクリプトのアタッチ​

NPC設定に scripts: リストを追加します:

scripts:
- wave.lua

1体のNPCに複数のスクリプトをアタッチできます:

scripts:
- wave.lua
- greeting_particles.lua

スクリプトは優先度順に実行されます。priority の値が小さいものが先で、省略時は 0 です。同じ優先度では scripts: リストの順序を維持します。これはNPCイベントの実行順であり、個別に予約したタスクの順序ではありません。


スクリプトの構造​

すべてのNPCスクリプトは、1つのテーブルを返す必要があります:

return {
api_version = 1,
priority = 0,

on_spawn = function(context)
context.state.spawned = true
context.npc:play_model_animation("idle")
end
}

受け付けられるトップレベルフィールドは次のものだけです:

フィールド型備考
api_versionnumber必須。1 である必要があります。
prioritynumber任意。値が小さいものが先に実行されます。
on_spawnfunctionNPCのスポーン後に実行されます。
on_removefunction通常のNPC削除中に、スクリプト終了と実体削除より前に実行されます。
on_game_tickfunctionランタイムが初期化され、クロックが有効になった後、サーバーティックごとに実行されます。処理は極力軽くしてください。
on_npc_interactfunction通常のインタラクト処理がスクリプトの段階に達したときに実行されます。下記の制限を参照してください。
on_npc_proximity_enterfunctionプレイヤーがこのNPCのアクティベーション半径に入ったときに1回実行されます。
on_npc_proximity_leavefunctionプレイヤーがこのNPCのアクティベーション半径から出たときに1回実行されます。
on_zone_enterfunctionこのスクリプトが監視しているゾーンにプレイヤーが入ったときに実行されます(context.zones を参照)。
on_zone_leavefunction監視中のゾーンからプレイヤーが出たときに実行されます。

ゾーン監視が追跡するのはプレイヤーだけです。モブやその他のエンティティが on_zone_enter / on_zone_leave を発火させることはありません。

不明なトップレベルキーはスクリプト読み込み時に拒否されます。ヘルパー関数は、返却するテーブルより上で local 関数として宣言してください。

ローダーは検証時にファイル直下のコードを評価し、その後、各NPCスクリプトインスタンスが個別のサンドボックスで再評価します。ファイル直下の初期化でフックのコンテキストを使用しないでください。同じNPCに付けたスクリプトも、それぞれ独立したグローバル変数と context.state を持ちます。

NPCランタイムは遅延初期化されます。現在のNPCホストはインスタンスを作成して on_spawn を送りますが、即時初期化する start() は呼びません。そのため、on_game_tick だけのファイルは自動ではティックを開始しません。on_spawn = function(context) end を定義してスポーン時に初期化するか、宣言済みの別のイベントフックで後から初期化してください。

Luaエラーが発生すると、該当インスタンスを閉じ、所有する処理をキャンセルします。その後のNPCイベントは閉じたインスタンスを破棄し、再作成しません。修正した定義を再読み込みして、新しいNPCスクリプトインスタンスを作成してください。状態は削除や再起動をまたいで保存されません。on_remove はNPC削除用であり、一般的なエラー・終了コールバックではありません。


近接フック​

NPCの近接フックは、NPCの activationRadius 設定値を使用します。

フック発火するタイミング
on_npc_proximity_enterプレイヤーがNPCのアクティベーション半径の外から内へ移動したとき。
on_npc_proximity_leaveプレイヤーがNPCのアクティベーション半径の内から外へ移動したとき。

サーバー側のスキャナーは、現在のNPC実体からの球形距離を使い、各NPCとプレイヤーの組を 100ティックごと(20 TPSで通常5秒)に確認します。初回スキャン時にすでに範囲内にいる場合も進入として扱います。スキャン間の短い訪問は見逃すことがあります。あるNPCの近くにいても別のNPCの進入イベントを妨げず、範囲内で検出され続けても進入を繰り返しません。

巡回中や編集中のNPCはスキップします。そのため、検出済みの組で移動開始時に退出、検出再開時に進入が発生する場合があります。NPCの削除やログアウト時の退出フックは保証されません。送出には追跡対象NPCとオンラインのプレイヤーの両方が必要です。NPCまたはオンラインプレイヤーの集合が空になると、退出イベントなしで追跡を消去します。activationRadius が0以下なら近接アクティベーションは無効です。クエスト提供NPCの別の30ブロックのマーカー範囲は、Luaの範囲を広げません。

通常の挨拶、ダイアログ、クエストインジケーターの挙動はそのまま動作します。Luaフックはその上に振る舞いを追加するものです。


共有スクリプティング面​

NPCスクリプトは統合ランタイム上で動作するため、すべてのNPCフックはFreeMinecraftModelsスクリプトが使うのと同じ共有MagmaCoreコンテキストテーブルも受け取ります。EliteMobsのボスパワーも同じランタイム上で動作しますが、いくつかのテーブルについてはボス固有のバリアントを使います。完全なメソッド一覧はLua APIリファレンスとスクリプティングエンジンにあります:

テーブル役割
context.worldワールドへの効果とクエリ:strike_lightning、spawn_particle、play_sound、set_block_at、place_temporary_block、spawn_entity、spawn_firework、get_nearby_entities、get_nearby_players、raycast など。座標形式(strike_lightning(x, y, z))とロケーションテーブル形式(strike_lightning_at_location(loc))の両方が使えます。
context.zonescreate_sphere(x, y, z, radius)、create_cylinder(x, y, z, radius, height)、create_cuboid(x, y, z, xSize, ySize, zSize) で空間ゾーンを作成します。数値ハンドルを返し、所有者のワールドがなければ nil です。watch(handle, on_enter, on_leave) は関数引数が指定された方向のトップレベルフックを有効にします。渡した関数自体は呼びません。unwatch(handle) はゾーンを削除して追跡を停止します。
context.schedulerrun_later(ticks, fn)、run_repeating(delay, interval, fn)、cancel(task_id)。
context.cooldowns共有MagmaCoreクールダウン:local_ready、local_remaining、check_local、set_local、global_ready、set_global。
context.loginfo(msg)、warn(msg)、error(msg) — サーバーコンソールに出力します。
context.eventフックがイベントまたは実行者を渡す場合のデータ。下記を参照。
context.playerインタラクト/トリガーしたプレイヤー(存在する場合)。下記を参照。
context.stateこのNPCスクリプトインスタンスに対して、NPCが削除されるまで保持される素のLuaテーブル。

共有ゾーンは作成時のNPCのワールド内の絶対座標を使い、移動するNPCを追いません。作成すると、watch で進入・退出処理を付ける前でも、所有者の現在のワールドにいるプレイヤーの毎ティックスキャンが登録されます。有効にする方向ごとに関数を渡し、返すスクリプトテーブルに対応する on_zone_enter または on_zone_leave を実装してください。これらのフックは context.player と実行者のイベントテーブルを受け取りますが、ゾーン識別子は受け取りません。unwatch はハンドルを完全に削除するため、再監視するには別のゾーンを作成します。初回スキャンですでに内側にいるプレイヤーも進入扱いで、オフラインのプレイヤーには退出コールバックを送れません。

例:インタラクト時に雷を落とす​

return {
api_version = 1,

on_npc_interact = function(context)
-- NPC scripts can now reach the full world API.
context.world:strike_lightning_at_location(context.npc:get_location())
end
}

context.npc​

context.npc はすべてのNPCフックで利用できます。

フィールド​

フィールド型備考
namestring設定で指定したNPCの表示名。
filenamestringNPC設定のファイル名。
uuidstring実行時のNPCのUUID。
activation_radiusnumber設定されたアクティベーション半径。
current_locationlocation table実体となるエンティティが存在する場合の位置スナップショット。
entity_typestring実体となるエンティティが存在する場合のBukkitエンティティタイプ。

メソッド​

メソッド引数戻り値備考
is_valid()-booleanNPCがまだ有効な実体エンティティを保持しているかどうか。
get_location()-location table現在のNPCの位置。エンティティが利用できない場合はスポーン位置。
get_eye_location()-location table現在の目線位置。利用できない場合はスポーン位置にフォールバック。
get_activation_radius()-number現在設定されているアクティベーション半径。
get_nearby_players(radius)numbertableNPCのワールド内で球形距離に含まれるプレイヤーのラッパー。ゲームモードのフィルターはありません。半径は0以上にしてください。
face_direction_or_location(target)vector, location or entity tablenil方向または同じワールド内の位置を向きます。world または current_location があるテーブルは位置、座標だけなら方向として扱います。巡回移動中は何もしません。
say_greeting(player?)player, UUID, name, or nilnil設定された挨拶を送信します。利用可能な場合はトリガーしたプレイヤーが既定値になります。
say_dialog(player?)player, UUID, name, or nilnil設定されたダイアログを送信します。利用可能な場合はトリガーしたプレイヤーが既定値になります。
say_farewell(player?)player, UUID, name, or nilnil設定された別れの言葉を送信します。利用可能な場合はトリガーしたプレイヤーが既定値になります。
play_model_animation(name)stringnilカスタムモデルのアニメーションが存在する場合に再生します。存在しない場合は安全に何もしません。
patrol_pause()-boolean設定された巡回を一時停止します。
patrol_resume()-booleanスクリプトの待機・一時移動を解除して巡回を再開します。
walk_to(x, y, z)3 個の数値boolean設定されたスポーン位置からのオフセットまで歩き、巡回を再開します。長距離経路は自動で解決します。
hold(x, y, z)3 個の数値boolean設定上の原点からのオフセットまで歩いて待機します。
teleport(x, y, z)3 個の数値boolean設定上の原点からのオフセットへ、目的地でエンティティのティックが有効ならテレポートします。
remove()-nil理由 OTHER の通常削除で現在のNPC実体を削除します。設定上の配置は消しません。削除処理を再度呼ぶため、on_remove 内から呼ばないでください。

NPCに巡回コントローラーが設定されていない場合や要求を受理できない場合、移動メソッドは false を返します。経路設定と長距離移動についてはNPC・ボスの巡回を参照してください。


context.player​

context.player は on_npc_interact、on_npc_proximity_enter、on_npc_proximity_leave、監視ゾーンの進入・退出フックで利用できます。プレイヤーが関与しないライフサイクルフックでは nil です。

これはFMMスクリプトでも使う共有MagmaCoreプレイヤーラッパーです。体力、ポーション効果、send_message、show_title、show_action_bar、get_held_item、レイキャストなどを公開します。ボスパワーは独自のラッパーを使うため、そのメソッド一覧とは互換ではありません。共有APIの全一覧はLua APIリファレンスを参照してください。よく使う項目は次のとおりです:

フィールド/メソッド備考
nameプレイヤー名。
uuidプレイヤーのUUID。
current_locationフィールドにアクセスするたびに現在の位置を読み取ります。保存済みの位置テーブルは取得時のスナップショットのままです。
get_eye_location()現在のプレイヤーの目線位置。
send_message(text)チャットメッセージを送信します。カラーコードに対応。

共有ヘルパー関数の中で使う前に、必ず context.player のnilチェックを行ってください。

ここでの entity_type は小文字です

共有MagmaCoreのエンティティテーブルでは、entity_type は Bukkit 名を小文字にしたものです("player"、"zombie")。大文字形式を使うのは context.npc.entity_type と EliteMobs のボスパワーのエンティティテーブルだけです。両方を扱う必要があるスクリプトでは、大文字小文字を区別せずに比較してください。

すべての共有エンティティテーブルに EliteMobs が追加するフィールド​

EliteMobsの動作中は、context.player、context.npc:get_nearby_players(...) が返すラッパー、FreeMinecraftModelsのプロップスクリプトのラッパーなど、共有MagmaCoreエンティティテーブルに追加のフィールドを提供します。

フィールド型備考
is_elitebooleanEliteMobs がそのエンティティをエリートとして追跡していれば true
is_custom_bossbooleanカスタムボスであれば true(is_elite が false のときは常に false)
is_significant_bossboolean体力倍率が 1 を超えるカスタムボスなら true。増援として召喚されたかどうかは調べません。
elitetable または nilエリートにのみ存在します。下記を参照
elite_playertable または nilEliteMobsが構築した共有プレイヤーラッパーで、エリートインベントリが読み込み済みなら tier を公開します。アクセスごとに新しいテーブルを作るため、保存済みのテーブルのtierは古い値のままです。
can_receive_hostile_effect(actor_uuid)booleanプレイヤーの実行者について、共有の敵対対象ルールとEliteMobsの魔法武器の許可を確認します。有効なUUID文字列を渡してください。

elite サブテーブル:

フィールド/メソッド型備考
elite.levelnumberエリートのレベル
elite.namestring または nilエリートの表示名
elite.healthnumber現在のエリートの体力(リアルタイムで読み取り)
elite.max_healthnumberエリートの最大体力(リアルタイムで読み取り)
elite.is_custom_bossbooleanトップレベルのフィールドと同じ値
elite.health_multipliernumber設定された体力倍率
elite.damage_multipliernumber設定されたダメージ倍率
elite:remove()—エリートをデスポーンさせます
-- Warn the approaching player if a real boss is loose near this NPC
on_npc_proximity_enter = function(context)
if context.player == nil then return end
local here = context.npc:get_location()
local nearby = context.world:get_nearby_entities(here.x, here.y, here.z, 40)
for i = 1, #nearby do
if nearby[i].is_significant_boss then
context.player:send_message("&cA boss is nearby: " .. tostring(nearby[i].elite.name))
return
end
end
end

EliteMobsのボスパワーラッパーは別のビルダーとフィールド群を使います。is_elite など一部の名前は共通ですが、共有の入れ子の elite テーブルとボスパワーのメソッドは互換ではありません。ボスとエンティティを参照してください。FMMには独自のEliteMobsアダプターがあるため、EliteMobsが構築した共有ラッパーの追加フィールドがすべてFMMにもあるとは限りません。


context.event​

context.event が nil になるのは、フックにイベントも実行者もない場合だけです。これは共有MagmaCoreイベントテーブルです:

フィールド/メソッド備考
is_cancelledテーブル作成時のキャンセル状態。キャンセル不可のイベントでは false です。cancel() / uncancel() 後も更新されません。
cancel()元のイベントがキャンセル可能な場合だけ存在し、そのイベントをキャンセルします。
uncancel()元のイベントがキャンセル可能な場合だけ存在し、キャンセルを解除します。
playerイベントの実行者(例:インタラクトしたプレイヤー)をプレイヤーラッパーとして。存在する場合のみ。

インタラクト/近接のプレイヤーについては context.player を使うことを推奨します(これらのフックでは必ず設定されます)。

現在、on_npc_interact はBukkitイベントを渡さず、プレイヤーだけを渡します。そのイベントテーブルには player と is_cancelled: false があり、cancel / uncancel はありません。このテーブルで通常のNPCメニューをキャンセルすることはできません。

インタラクト処理はキャンセル済みのBukkitイベントを無視し、名札の使用と編集中NPCへの操作を防ぎ、プレイヤーごとに1ティックの連続操作を抑制します。クエストの会話目標が操作を処理した場合は、Luaフックより前に終了します。それ以外では、通常のNPCメニュー・アクションより先にフックが動きます。近接イベントもキャンセル不可で、ゾーンフックはBukkitイベントなしで実行者を渡します。これらにも cancel() / uncancel() はありません。


状態、スケジューラー、クールダウン​

context.state は個々のNPCスクリプトインスタンスに属し、そのインスタンスの終了まで保持されます。同じNPC上の別のスクリプトの状態とは独立しています。

context.scheduler は共有MagmaCoreのスケジューラーです。MagmaCoreの名前とEliteMobsの run_after / run_every の名前はどちらも使えます — 同じ動作のエイリアスです:

メソッド引数備考
run_later(ticks, callback) / run_after(ticks, callback)number, function遅延後に1回実行します。タスクIDを返します。
run_repeating(delay, interval, callback)number, number, function初期遅延の後、繰り返し実行します。タスクIDを返します。
run_every(interval, callback)number, functioninterval ティックごとに実行します(初期遅延は0)。タスクIDを返します。
cancel(task_id) / cancel_task(task_id)number所有しているタスクをキャンセルします。

スケジューラーのコールバックは新しいコンテキストを受け取ります。元の context.player や context.event は受け取りません。所有しているタスクは、NPCが削除された時点ですべて自動的にキャンセルされます。

context.cooldowns は共有MagmaCoreのクールダウンテーブルです:

メソッド引数戻り値備考
local_ready(key?)stringbooleanローカルクールダウンが明けている場合にtrue。
local_remaining(key?)stringnumber残りティック数。準備完了の場合は 0。
check_local(key?, duration)string, numberboolean準備完了ならクールダウンを開始してtrueを返します。
set_local(duration, key?)number, stringnilクールダウンを設定またはリセットします。
global_ready()-boolean共有グローバルクールダウンが準備完了ならtrue。
set_global(duration)numbernilグローバルクールダウンを開始します。

check_local の時間は常に第2引数です。既定キーには check_local(nil, 60) を使います。一方、set_local は時間が第1引数です。既定キーにはスクリプトのファイル名が含まれます。明示したローカルキーとグローバルクールダウンの保存先は同じNPCアダプター上のスクリプト間で共通なので、連動させる場合以外は異なるキーを使ってください。時間はサーバーティック数ではなく、名目上の1ティックを50 msとして単調増加時計の経過時間で測ります。0以下の時間はクールダウンを消去します。グローバルクールダウンを設定しても、フックや check_local は自動では止まりません。global_ready() を明示的に確認してください。

統一クールダウンAPI

NPCスクリプトは現在、ボスパワーやFreeMinecraftModelsスクリプトと同じ、共有MagmaCoreのクールダウン引数順(check_local(key?, duration))を使用します。以前の実験的なNPCビルドでは check_local(duration, key?) でした — 古いスクリプトは共有の引数順に更新してください。


例:接近時に手を振る​

このスクリプトは、進入を検出したプレイヤーの方をNPCが向き、カスタムモデルの wave アニメーションを再生します。進入の判定は移動パケットごとではなく5秒間隔のスキャンです。キー付きクールダウンは進入通知が繰り返された際の再生を制限しますが、スキャン頻度は増やしません。

return {
api_version = 1,
priority = 0,

on_npc_proximity_enter = function(context)
if context.player == nil then return end

if context.cooldowns:check_local("wave:" .. context.player.uuid, 60) then
context.npc:face_direction_or_location(context.player.current_location)
context.npc:play_model_animation("wave")
end
end
}

play_model_animation(name) は、NPCにカスタムモデルがない場合や、モデルにそのアニメーションがない場合、安全に何も行いません。


パフォーマンスのガイドライン​

  • on_game_tick フックは軽くしてください。初期化後はサーバーティックごと、通常は毎秒20回動きます。このフックがなくても、毎ティックのゾーンスキャンや明示的に予約した処理が発生する場合があります。
  • 近接に関する挙動には、毎ティック周囲のプレイヤーをポーリングするのではなく、on_npc_proximity_enter と on_npc_proximity_leave を使ってください。
  • アニメーション、サウンド、パーティクルの発生を制御するには context.cooldowns:check_local(...) を使ってください。
  • 毎ティック実行する必要のない挙動には、適切な間隔を指定した context.scheduler:run_repeating(...) を使ってください。
  • Lua内での大規模な検索は避けてください。context.npc:get_nearby_players(radius) は小規模なローカルチェックには問題ありませんが、広範囲のスキャンはプラグインランタイム側に留めるべきです。