Luaスクリプティング:サンプルとパターン
このページでは、FreeMinecraftModelsのプロップスクリプトの例と書き方を解説します。参照するモデルやアニメーションを用意し、マップに組み込む前にサーバーで動作を確認してください。
初めての場合ははじめにを、APIの詳細はプロップAPIを参照してください。
サンプル:無敵プロップ
学べること: 左クリックによる標準のダメージ処理をキャンセルする最小限のスクリプトです。
スクリプトフォルダにinvulnerable.luaとして保存し、スクリプトを再読み込みしてからプロップに割り当ててください。
スクリプトファイル全体(クリックで展開)
return {
api_version = 1,
on_left_click = function(context)
if context.event then
context.event.cancel()
end
end
}
解説
-
on_left_clickはプレイヤーがプロップを殴ると発火します。Luaイベントは、FMMが標準のプロップダメージ処理に使うキャンセルフラグです。 -
イベントガード --
context.eventはこのフックでは常に存在するはずですが、ガードしておくのは良い習慣です。 -
context.event.cancel()はそのクリックの標準ダメージを防ぎます。すべてのダメージ源に対して無敵になるわけではありません。
使い方
プロップの.yml設定に追加します:
isEnabled: true
scripts:
- invulnerable.lua
サンプル:インタラクティブなドア
学べること: 右クリックで状態を切り替え、アニメーションを再生・停止し、context.stateを使ってドアが開いているか閉じているかを追跡します。
スクリプトファイル全体(クリックで展開)
local OPEN_ANIMATION = "open"
local CLOSE_ANIMATION = "close"
return {
api_version = 1,
on_spawn = function(context)
context.state.is_open = false
end,
on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
local loc = context.prop.current_location
if context.state.is_open then
-- Close the door
context.prop:stop_animation()
context.prop:play_animation(CLOSE_ANIMATION, true, false)
context.state.is_open = false
if loc then
context.world:play_sound(
"BLOCK_WOODEN_DOOR_CLOSE",
loc.x, loc.y, loc.z,
1.0, 1.0
)
end
else
-- Open the door
context.prop:stop_animation()
context.prop:play_animation(OPEN_ANIMATION, true, false)
context.state.is_open = true
if loc then
context.world:play_sound(
"BLOCK_WOODEN_DOOR_OPEN",
loc.x, loc.y, loc.z,
1.0, 1.0
)
end
end
end
}
解説
-
ファイルスコープの定数 --
OPEN_ANIMATIONとCLOSE_ANIMATIONはreturnテーブルの上で定義されています。これにより、異なるアニメーション名を使うかもしれない異なるモデルファイルに合わせて簡単に変更できます。 -
on_spawnがcontext.state.is_open = falseを設定します。状態はこのプロップへのスクリプト割り当て単位でフック間に保持されます。同じプロップに割り当てた別のスクリプトとは共有しません。 -
on_left_clickはそのクリックによる標準ダメージをキャンセルします。 -
on_right_clickは停止と対応するアニメーションを要求し、フラグを切り替えて音を鳴らします。フラグは要求した状態を表し、再生の確認ではありません。blend = trueはアニメーションをキューに入れます。停止要求は共有エンジンの実行中のdeathを中断しません。アニメーションを参照してください。この例はバリアの衝突を変えず、ドアを通れることも保証しません。 -
サウンドには大文字小文字を問わない Bukkit の列挙名、
minecraft:block.note_block.harpなどの有効なキー、独自リソースパックのキーを使用できます。クライアントのリソースパックにそのサウンドが必要です。
"open"や"close"はモデルの定義名と一致させてください。見つからなければplay_animation()はfalseを返しますが、この例はフラグを切り替え、音も鳴らします。要求の受理に合わせて状態を変えるなら戻り値を確認してください。trueでも画面上の再生を確認したことにはなりません。
サンプル:パーティクル付き近接トリガー
学べること: ゾーンの作成、入退場イベントのゾーン監視、パーティクル効果、そしてクリーンアップ。
スクリプトファイル全体(クリックで展開)
local ZONE_RADIUS = 8
local PARTICLE_INTERVAL = 10 -- ticks between particle bursts
return {
api_version = 1,
on_spawn = function(context)
context.state.zone_handle = nil
context.state.particle_task = nil
context.state.players_in_zone = 0
local loc = context.prop.current_location
if loc == nil then return end
-- Create a sphere zone around the prop
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, ZONE_RADIUS)
context.state.zone_handle = handle
-- Pass placeholder functions to enable on_zone_enter and on_zone_leave.
-- The actual logic lives in the hooks below.
context.zones:watch(handle, function() end, function() end)
-- Start a repeating particle effect at the zone boundary
context.state.particle_task = context.scheduler:run_repeating(0, PARTICLE_INTERVAL, function(tick_context)
local prop_loc = tick_context.prop.current_location
if prop_loc == nil then return end
-- Spawn particles in a ring at the zone boundary
for angle = 0, 350, 30 do
local rad = math.rad(angle)
local px = prop_loc.x + math.cos(rad) * ZONE_RADIUS
local pz = prop_loc.z + math.sin(rad) * ZONE_RADIUS
if (tick_context.state.players_in_zone or 0) > 0 then
-- Flame particles when players are inside
tick_context.world:spawn_particle("FLAME", px, prop_loc.y + 0.5, pz, 1, 0, 0, 0, 0)
else
-- Green particles when zone is empty
tick_context.world:spawn_particle("HAPPY_VILLAGER", px, prop_loc.y + 0.5, pz, 1, 0, 0, 0, 0)
end
end
end)
end,
on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,
on_zone_enter = function(context)
context.state.players_in_zone = (context.state.players_in_zone or 0) + 1
end,
on_zone_leave = function(context)
context.state.players_in_zone = math.max(0, (context.state.players_in_zone or 0) - 1)
end,
on_destroy = function(context)
-- Clean up the repeating task
if context.state.particle_task then
context.scheduler:cancel(context.state.particle_task)
context.state.particle_task = nil
end
-- Clean up the zone watch
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}
解説
-
定数 --
ZONE_RADIUSとPARTICLE_INTERVALは、簡単に調整できるようファイルスコープにあります。 -
状態の初期化 --
on_spawnは、他の処理を行う前にすべての状態フィールドをnil/0に設定します。 -
context.zones:create_sphere()はスポーン時の位置に固定の球形ゾーンを作ります。数値ハンドルはそのゾーンを参照します。パーティクルの輪は現在位置を読むため、動かないプロップで使うか、移動時にゾーンも更新してください。 -
ゾーンの監視 --
context.zones:watch(handle, function() end, function() end)はon_zone_enterおよびon_zone_leaveフックを有効にします。これらのフックはcontext.stateに保存されたカウンタを増減させます。プレイヤー固有の挙動が必要な場合に備え、これらは横断したプレイヤーをcontext.player/context.event.playerとして受け取ります。 -
繰り返しタスクがサーバーの10 tickごとに輪状のパーティクルを出します。20 TPSなら0.5秒です。ゾーン内のプレイヤーの有無で種類が変わります。
-
クリーンアップ --
on_destroyは繰り返しタスクをキャンセルし、ゾーンの監視を解除します。どちらもプロップが削除されると自動的にクリーンアップされますが、明示的なクリーンアップはベストプラクティスです。
毎tick大量にパーティクルを出すと負荷がかかります。10〜20 tick程度の間隔を空け、数を抑えてください。この例はPARTICLE_INTERVAL = 10で、20 TPSなら毎秒2回、輪ごとに12個です。
サンプル:音を発するプロップ
学べること: インタラクション時のサウンド再生、context.cooldownsの使用、そして連続的なインタラクションの防止。
スクリプトファイル全体(クリックで展開)
local SOUND_NAME = "BLOCK_NOTE_BLOCK_HARP"
local COOLDOWN_TICKS = 40 -- 2 seconds between sounds
return {
api_version = 1,
on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
-- Prevent spam
if not context.cooldowns:check_local("sound", COOLDOWN_TICKS) then
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Play the sound
context.world:play_sound(SOUND_NAME, loc.x, loc.y, loc.z, 1.0, 1.0)
-- Show some particles
context.world:spawn_particle("NOTE", loc.x, loc.y + 1.5, loc.z, 5, 0.3, 0.3, 0.3, 0)
end
}
解説
-
context.cooldowns:check_local("sound", COOLDOWN_TICKS)は利用可能かを調べ、可能なら即座にクールダウンを開始します。このプロップへの同じスクリプト割り当てを使う全プレイヤーが共有します。1 tickを50 msとして経過時間を測ります。スケジューラの遅延は実際のサーバーtickを数えます。 -
サウンドには大文字小文字を問わない Bukkit の列挙名、
minecraft:block.note_block.harpなどの有効なキー、独自リソースパックのキーを使用できます。クライアントのリソースパックにそのサウンドが必要です。 -
パーティクルフィードバック -- サウンド再生時にプロップの上に音符パーティクルが現れ、視覚的な合図を与えます。
-
on_left_clickはそのクリックによる標準ダメージをキャンセルします。
通常のクールダウンにはcontext.cooldownsを使用してください。context.stateとscheduler:run_later()の組み合わせは、遅延の最後にカスタムの状態変更が必要な場合のために取っておきます。
サンプル:アニメーションする環境プロップ
学べること: スポーン時にループアニメーションを開始し、tickベースのパーティクルエミッターを備えます。
スクリプトファイル全体(クリックで展開)
return {
api_version = 1,
on_spawn = function(context)
-- Start the idle animation immediately, looping
context.prop:play_animation("idle", false, true)
-- Emit ambient particles every 40 ticks (2 seconds)
context.state.ambient_task = context.scheduler:run_repeating(0, 40, function(tick_context)
local loc = tick_context.prop.current_location
if loc == nil then return end
tick_context.world:spawn_particle(
"ENCHANT",
loc.x, loc.y + 1, loc.z,
10, 0.5, 0.5, 0.5, 0.05
)
end)
end,
on_left_click = function(context)
if context.event then
context.event.cancel()
end
end,
on_destroy = function(context)
if context.state.ambient_task then
context.scheduler:cancel(context.state.ambient_task)
end
end
}
解説
-
on_spawnは"idle"アニメーションを即座に開始します。blendのfalseで即時に切り替わります。組み込みのidle状態は、渡したloop引数に関係なく繰り返されます。 -
環境パーティクル -- 繰り返しタスクが2秒ごとにプロップの上にエンチャントテーブルのパーティクルをスポーンし、魔法のような環境効果を作り出します。
-
クリーンアップ --
on_destroyがパーティクルタスクをキャンセルします。
サンプル:座れる椅子
学べること: 右クリックでプレイヤーをプロップに乗せ、左クリックで降ろし、context.event.playerを使ってインタラクションしたプレイヤーを取得します。
スクリプトファイル全体(クリックで展開)
return {
api_version = 1,
on_right_click = function(context)
-- Handle seating here instead of also using the default mount fallback
if context.event then
context.event.cancel()
end
local player = context.event and context.event.player
if not player then return end
-- Check if the player is already sitting on this prop
local passengers = context.prop:get_passengers()
for i = 1, #passengers do
if passengers[i].uuid == player.uuid then
-- Player is already seated, do nothing on right-click
return
end
end
-- Mount the player on the chair
context.prop:mount(player)
local loc = context.prop.current_location
if loc then
context.world:play_sound("BLOCK_WOOD_PLACE", loc.x, loc.y, loc.z, 0.8, 1.2)
end
end,
on_left_click = function(context)
-- Cancel default left-click damage
if context.event then
context.event.cancel()
end
local player = context.event and context.event.player
if not player then return end
-- Check if the player is sitting and dismount them
local passengers = context.prop:get_passengers()
for i = 1, #passengers do
if passengers[i].uuid == player.uuid then
context.prop:dismount(player)
local loc = context.prop.current_location
if loc then
context.world:play_sound("BLOCK_WOOD_BREAK", loc.x, loc.y, loc.z, 0.8, 1.0)
end
return
end
end
end
}
解説
-
on_right_clickはcontext.event.playerを取得し、すでに座っていないか確認してからcontext.prop:mount(player)を呼びます。最初の空いているm_マウントポイントの座席を要求します。 -
on_left_clickはそのクリックの標準ダメージをキャンセルします。殴ったプレイヤーが座っていればcontext.prop:dismount(player)で降ろします。 -
搭乗者のチェック --
context.prop:get_passengers()はエンティティテーブルの配列を返します。UUIDを比較して、リスト内でインタラクションしたプレイヤーを見つけます。 -
音はキューに入れた乗降要求に伴うもので、乗降の完了を確認するものではありません。右クリックフックはFMMの標準の着席処理をキャンセルし、二重に着席を試みることを防ぎます。モデルには空いている
m_マウントポイントが必要です。
サンプル:祝福の祠
学べること: プレイヤーの手持ちアイテムのチェック、アイテムの消費、ランダムなポーション効果の付与、クールダウン管理、そしてパーティクル/サウンドのフィードバック。
スクリプトファイル全体(クリックで展開)
local COOLDOWN_TICKS = 600 -- 30 seconds between uses
local BLESSINGS = {
{ effect = "speed", name = "Swiftness" },
{ effect = "strength", name = "Strength" },
{ effect = "regeneration", name = "Regeneration" },
{ effect = "resistance", name = "Resistance" },
{ effect = "jump_boost", name = "Leap" },
{ effect = "haste", name = "Haste" },
}
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end
-- Check cooldown
if context.state.on_cooldown then
player:send_message("&eThe shrine is recharging... Please wait.")
return
end
-- Check if the player is holding a gold ingot
local held = player:get_held_item()
if not held or held.type ~= "GOLD_INGOT" then
player:send_message("&eThe shrine demands a gold offering...")
return
end
-- Consume one gold ingot
player:consume_held_item(1)
-- Play blessing effects
local loc = context.prop.current_location
if loc then
context.world:spawn_particle("ENCHANT", loc.x, loc.y + 1.5, loc.z, 30, 0.5, 0.5, 0.5, 0.5)
context.world:spawn_particle("HAPPY_VILLAGER", loc.x, loc.y + 1, loc.z, 10, 0.3, 0.3, 0.3, 0)
context.world:play_sound("BLOCK_BEACON_ACTIVATE", loc.x, loc.y, loc.z, 1.0, 1.5)
end
-- Apply a random blessing
local chosen = BLESSINGS[math.random(#BLESSINGS)]
player:add_potion_effect(chosen.effect, 600, 1) -- 30 seconds, level II
player:send_message("&aThe shrine blesses you with " .. chosen.name .. "!")
-- Set cooldown
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}
解説
-
アイテムのチェック --
player:get_held_item()はメインハンドのアイテムについてtype、amount、display_nameを持つテーブルを返します(空の場合はnil)。held.typeはBukkitマテリアル名(大文字)なので、"GOLD_INGOT"と比較します。 -
player:consume_held_item(1)はメインハンドからの削除をキューに入れ、タスクの実行時にその手のアイテムを読みます。支払い結果は返しません。この例は一体の取引処理ではありません。持ち替えると消費対象が変わる一方、祝福は即座に付与されます。 -
ランダムなバフ -- ファイルスコープの
BLESSINGSテーブルは利用可能な正の効果を列挙しています。math.random(#BLESSINGS)がランダムに1つ選びます。player:add_potion_effect(effect, duration, amplifier)がそれを付与します --600tickは30秒、amplifier1はレベルIIです。 -
同じスクリプト割り当ての全プレイヤーがフラグを共有します。サーバーの600 tick後、20 TPSなら30秒後に解除されます。経過時間で測るなら、音を発するプロップの例のように
context.cooldownsを使ってください。 -
フィードバック -- エンチャントと幸せな村人のパーティクル、加えてビーコン起動音が「神聖な祝福」の雰囲気を作り出します。
サンプル:呪われた祠
学べること: 負のポーション効果、落雷、エンティティのスポーン、そしてプレイヤーの供物に基づく分岐ロジック。
スクリプトファイル全体(クリックで展開)
local COOLDOWN_TICKS = 600 -- 30 seconds between uses
local BUFFS = {
{ effect = "speed", name = "Swiftness" },
{ effect = "strength", name = "Strength" },
{ effect = "regeneration", name = "Regeneration" },
{ effect = "resistance", name = "Resistance" },
}
local CURSES = {
{ effect = "slowness", name = "Slowness" },
{ effect = "weakness", name = "Weakness" },
{ effect = "poison", name = "Poison" },
{ effect = "mining_fatigue", name = "Mining Fatigue" },
}
local ZOMBIE_COUNT = 4
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end
-- Check cooldown
if context.state.on_cooldown then
player:send_message("&7The dark shrine pulses with residual energy...")
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Check if the player is holding a gold ingot
local held = player:get_held_item()
if not held or held.type ~= "GOLD_INGOT" then
-- No offering -- punish the player!
player:send_message("&4The shrine demands tribute! You dare approach empty-handed?!")
-- Strike lightning on the player
local player_loc = player.current_location
if player_loc then
context.world:strike_lightning(player_loc.x, player_loc.y, player_loc.z)
end
-- Spawn a horde of zombies around the shrine
for i = 1, ZOMBIE_COUNT do
local angle = math.rad((360 / ZOMBIE_COUNT) * i)
local spawn_x = loc.x + math.cos(angle) * 3
local spawn_z = loc.z + math.sin(angle) * 3
context.world:spawn_entity("zombie", spawn_x, loc.y, spawn_z)
end
-- Apply a random curse
local chosen_curse = CURSES[math.random(#CURSES)]
player:add_potion_effect(chosen_curse.effect, 400, 1) -- 20 seconds, level II
player:send_message("&cThe shrine curses you with " .. chosen_curse.name .. "!")
-- Ominous effects
context.world:spawn_particle("SMOKE", loc.x, loc.y + 1, loc.z, 30, 0.5, 0.5, 0.5, 0.05)
context.world:play_sound("ENTITY_WITHER_AMBIENT", loc.x, loc.y, loc.z, 1.0, 0.5)
else
-- Gold offered -- reward the player
player:consume_held_item(1)
-- Apply a random buff
local chosen_buff = BUFFS[math.random(#BUFFS)]
player:add_potion_effect(chosen_buff.effect, 600, 1) -- 30 seconds, level II
player:send_message("&aThe dark shrine accepts your offering. You are blessed with " .. chosen_buff.name .. "!")
-- Positive feedback
context.world:spawn_particle("ENCHANT", loc.x, loc.y + 1.5, loc.z, 30, 0.5, 0.5, 0.5, 0.5)
context.world:play_sound("BLOCK_BEACON_ACTIVATE", loc.x, loc.y, loc.z, 1.0, 0.8)
end
-- Set cooldown regardless of path
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}
解説
-
供物による分岐 -- スクリプトは
player:get_held_item()をチェックし、2つの経路のいずれかを取ります:プレイヤーが金を持っていない場合は罰、持っている場合は報酬です。 -
落雷 --
context.world:strike_lightning(x, y, z)はプレイヤーの位置に本物の(ダメージを与える)雷を落とします。プレイヤーの位置はplayer.current_locationから読み取られます。 -
context.world:spawn_entity("zombie", x, y, z)は祠の周りに通常のゾンビを円状にスポーンします。プロップを削除してもゾンビは消えません。この例は地形を確認せず、生存数にも上限を設けません。 -
負のポーション効果 --
player:add_potion_effect("poison", 400, 1)は20秒間の毒IIを付与します。効果名はBukkitのPotionEffectType名と一致する小文字の文字列です。 -
メインハンドの消費をキューに入れ、ランダムなバフを付与します。祝福の祠と同様に、支払いと報酬を一体で確定する取引処理ではありません。
-
どちらの分岐も、このスクリプト割り当ての全プレイヤーが共有するフラグを設定します。サーバーの600 tick後、20 TPSなら30秒後に解除されます。
一度に複数のエンティティをスポーンすると、サーバーパフォーマンスに影響する可能性があります。数を少なく保ち(4〜6)、多くのプレイヤーが同時に祠を使う場合はプレイヤーごとのクールダウンの追加を検討してください。
サンプル:回転する地球儀
学べること: インタラクション時にタイマー付きアニメーションを再生し、アニメーション停止をスケジュールし、機械的な効果音を鳴らします。
スクリプトファイル全体(クリックで展開)
local SPIN_ANIMATION = "spin"
local SPIN_DURATION = 100 -- 5 seconds in ticks
return {
api_version = 1,
on_spawn = function(context)
context.state.is_spinning = false
end,
on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
-- Prevent starting a new spin while already spinning
if context.state.is_spinning then
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Start the spin animation (non-looping)
context.prop:play_animation(SPIN_ANIMATION, true, false)
context.state.is_spinning = true
-- Play a mechanical clicking sound
context.world:play_sound("BLOCK_CHAIN_PLACE", loc.x, loc.y, loc.z, 1.0, 1.5)
-- Schedule the animation to stop after 5 seconds
context.scheduler:run_later(SPIN_DURATION, function(later_context)
later_context.prop:stop_animation()
later_context.state.is_spinning = false
local stop_loc = later_context.prop.current_location
if stop_loc then
later_context.world:play_sound("BLOCK_CHAIN_FALL", stop_loc.x, stop_loc.y, stop_loc.z, 1.0, 0.8)
end
end)
end
}
解説
-
状態ガード --
context.state.is_spinningは複数の重複する回転リクエストを防ぎます。フラグは回転が開始されたときに設定され、スケジュールされた停止が発火したときにクリアされます。 -
play_animation(SPIN_ANIMATION, true, false)は繰り返さないアニメーションをキュー経由で要求します。停止要求は再生開始からではなく、クリックからサーバーの100 tick後に実行されます。キューが混んでいれば回転が短くなったり、表示されなかったりします。共有エンジンの実行中のdeathは停止できません。この例はアニメーション要求がfalseを返してもフラグを設定します。 -
機械的なサウンド --
BLOCK_CHAIN_PLACEはカチッとした機械的な開始音を、BLOCK_CHAIN_FALLは巻き戻るような停止音を与えます。ピッチはお好みで調整してください。 -
コールバックコンテキスト --
run_laterコールバックは、すべての状態とワールドへのアクセスにlater_context(contextではなく)を使用します。これがスケジューラコールバックの正しいパターンです。
"spin"というアニメーション名は、モデルファイルで定義されているものと一致する必要があります。モデルが異なる名前(例:"rotate"、"turn")を使っている場合は、それに応じてSPIN_ANIMATION定数を更新してください。
サンプル:ジャンプスケアプロップ
学べること: 近接ゾーントリガー、長いクールダウンを伴うワンショットの驚かし効果、そしてサウンド/パーティクル/アニメーションを組み合わせた劇的なインパクト。
スクリプトファイル全体(クリックで展開)
local SCARE_RADIUS = 3
local COOLDOWN_TICKS = 1200 -- 60 seconds between scares
local SCARE_ANIMATION = "jumpscare"
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
context.state.zone_handle = nil
local loc = context.prop.current_location
if loc == nil then return end
-- Create a small sphere zone around the prop
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, SCARE_RADIUS)
context.state.zone_handle = handle
-- Watch for players entering the zone. The actual scare logic
-- lives in on_zone_enter below.
context.zones:watch(handle, function() end, nil)
end,
on_zone_enter = function(context)
if context.state.on_cooldown then
return
end
local scare_loc = context.prop.current_location
if scare_loc == nil then return end
-- Play the jumpscare animation
context.prop:stop_animation()
context.prop:play_animation(SCARE_ANIMATION, false, false)
-- Scary sound
context.world:play_sound(
"ENTITY_GHAST_SCREAM",
scare_loc.x, scare_loc.y, scare_loc.z,
1.0, 0.7
)
-- Burst of smoke particles
context.world:spawn_particle(
"CAMPFIRE_SIGNAL_SMOKE",
scare_loc.x, scare_loc.y + 1, scare_loc.z,
20, 0.5, 0.5, 0.5, 0.05
)
-- Set cooldown so it does not trigger again immediately
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end,
on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,
on_destroy = function(context)
-- Clean up the zone watch
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}
解説
-
context.zones:create_sphere()はスポーン位置に半径3ブロックの固定ゾーンを作ります。context.zones:watch(handle, function() end, nil)がon_zone_enterを有効にします。プロップを動かす場合はゾーンも動かすか作り直してください。 -
驚かし効果 --
on_zone_enterフックは"jumpscare"アニメーション、ガストの叫び声、そしてキャンプファイアの煙パーティクルをスポーンします。この組み合わせが突然のびっくりする効果を作り出します。 -
このスクリプト割り当ての全プレイヤーがフラグを共有します。サーバーの1200 tick後、20 TPSなら60秒後に解除され、その後のゾーンへの入場で再び発動できます。
-
退場フックなし --
context.zones:watch()の3番目の引数がnilであることは、プレイヤーがゾーンを離れるタイミングを気にしないことを意味します。 -
クリーンアップ --
on_destroyがゾーンの監視を解除します。ゾーンはプロップが削除されると自動的にクリーンアップされますが、明示的なクリーンアップはベストプラクティスです。
最も効果的なジャンプスケアのためには、プロップを角の周りや暗い場所に隠してください。半径3ブロックにより、プレイヤーが十分に近づいてから驚かしがトリガーされることが保証されます。SCARE_RADIUSとCOOLDOWN_TICKSはお好みで調整してください。
サンプル:ゴブリンスポナープロップ
学べること: prop:spawn_elitemobs_boss()を使って、プロップのインタラクションからカスタムボスをスポーンし、EliteMobsがインストールされていない場合に優雅にフォールバックします。
スクリプトファイル全体(クリックで展開)
local BOSS_FILE = "goblin_warrior.yml"
local COOLDOWN_TICKS = 200 -- 10 seconds between spawns
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end
-- Prevent spawn spam
if context.state.on_cooldown then
player:send_message("&7The spawner is recharging...")
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Try to spawn the EliteMobs boss
local boss = context.prop:spawn_elitemobs_boss(BOSS_FILE, loc.x, loc.y + 1, loc.z)
if boss then
-- Success -- play spawn effects
context.world:spawn_particle("FLAME", loc.x, loc.y + 1, loc.z, 20, 0.5, 0.5, 0.5, 0.05)
context.world:play_sound("ENTITY_EVOKER_PREPARE_SUMMON", loc.x, loc.y, loc.z, 1.0, 1.0)
player:send_message("&cA goblin warrior emerges!")
else
-- EliteMobs is not installed or the boss file was not found
context.log:warn("Could not spawn boss '" .. BOSS_FILE .. "' -- is EliteMobs installed?")
player:send_message("&7The spawner fizzles... (EliteMobs not available)")
-- Fizzle particles as visual feedback
context.world:spawn_particle("SMOKE", loc.x, loc.y + 1, loc.z, 10, 0.3, 0.3, 0.3, 0.02)
context.world:play_sound("BLOCK_FIRE_EXTINGUISH", loc.x, loc.y, loc.z, 0.8, 1.2)
end
-- Set cooldown
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}
解説
-
ボスのスポーン --
context.prop:spawn_elitemobs_boss(filename, x, y, z)は指定された座標にEliteMobsのカスタムボスをスポーンします。ファイル名はEliteMobsのcustombossesフォルダ内の.ymlファイルと一致する必要があります。 -
spawn_elitemobs_boss()は連携機能が生きたボスを返せない場合にnilを返します。プラグインが利用できない場合、設定がない場合、スポーン失敗などが該当します。例のメッセージはEliteMobsの確認を促しますが、原因を特定するものではありません。 -
要求する位置はプロップの1ブロック上です。地形や衝突は確認しないため、ボスに合う空いた場所を選んでください。
-
このスクリプト割り当てはサーバーの200 tickに1回、20 TPSなら10秒に1回の試行を許可し、全プレイヤーが共有します。生存するボス数に上限はなく、プロップが消えてもボスは残ります。繰り返し遊べる仕組みに使うなら遭遇数などの制限を追加してください。
-
視覚/音声による区別 -- 成功では炎パーティクルとエヴォーカーの召喚音を使い、劇的なスポーンを演出します。失敗では煙と火消しの音を使い、明確な「不発」効果を出すので、プレイヤーはコンソールを確認しなくても何かがうまくいかなかったことが分かります。
ボスのファイル名(例:"goblin_warrior.yml")は、EliteMobs内の既存のカスタムボス設定に対応している必要があります。このスクリプトを使うマップやダンジョンを配布する場合は、ボス設定ファイルを含め、EliteMobsへの依存関係を文書化してください。
アイテムの効果
装備アイテム用スクリプトの例は廃止されました。現在のアイテム効果にはカスタムアイテム定義と独自エンチャントを使ってください。
ベストプラクティス
-
小さなフックから始めて検証する。 ログメッセージを送る単一の
on_spawnを書きます。それが発火することを確認します。そこから組み立てます。 -
ヘルパー関数はローカルに保つ。
local function toggle_door(context)のようなヘルパーはreturnテーブルの上で宣言します。これによりグローバルスコープから外れます。 -
すべての状態を
on_spawnで初期化する。on_right_clickでcontext.state.is_openを読み取るのにon_spawnで設定しなかった場合、それはnilになり、比較が予期しない挙動をする可能性があります。 -
完了したら繰り返しタスクをキャンセルする。 すべての
run_repeatingには、on_destroyに対応するcancelが必要です。漏れたタスクはCPUを浪費します。 -
新しいスケジューラコールバックコンテキストを使う。 スケジューラコールバックは新しいコンテキストパラメータを受け取ります。コールバック内では、外側の
contextではなく、常にそのパラメータを使用してください。 -
on_game_tickを軽量に保つ。 サーバーの毎tick、20 TPSなら毎秒20回実行されます。重い処理はcontext.cooldownsやスケジューラの間隔で頻度を制限してください。 -
左クリックのダメージを明示的に制御する。 殴ったときに標準ダメージを与えたくない場合は
on_left_clickでキャンセルしてください。すべてのダメージ源を防ぐものではありません。 -
パーティクル名は
FLAMEとflameの両方を受け付けます。API が大文字小文字を正規化して Bukkit のパーティクルを検索します。 サウンドには大文字小文字を問わない Bukkit の列挙名、minecraft:block.note_block.harpなどの有効なキー、独自リソースパックのキーを使用できます。クライアントのリソースパックにそのサウンドが必要です。
初心者によくある間違い
-
スケジューラコールバック内で外側の
contextを使う。 外側のコンテキストはフックが実行された時点のスナップショットをキャプチャします。コールバック内では、常にコールバック自身のパラメータを使用してください。 -
繰り返しタスクのキャンセルを忘れる。
on_spawnでrun_repeatingを開始したのにキャンセルしなかった場合、そのタスクはプロップが削除されるまで実行され続けます。 -
on_spawnで状態を初期化しない。context.state.xを設定する前に読み取るとnilが返り、ロジックが静かに壊れる可能性があります。 -
間違ったアニメーション名。
play_animation("open")がfalseを返す場合、アニメーション名がモデルファイル内のものと一致していません。正確な名前についてはモデルを確認してください。 -
パーティクル名は
FLAMEとflameの両方を受け付けます。API が大文字小文字を正規化して Bukkit のパーティクルを検索します。 サウンドには大文字小文字を問わない Bukkit の列挙名、minecraft:block.note_block.harpなどの有効なキー、独自リソースパックのキーを使用できます。クライアントのリソースパックにそのサウンドが必要です。 -
api_version = 1を忘れる。 返されるテーブルにはこのフィールドが含まれていなければなりません。さもないとFMMはスクリプトをロードしません。 -
返すテーブルにヘルパー関数を入れる。
api_versionと省略可能なpriority以外は、対応するフック名と関数だけを指定できます。ヘルパーはテーブルの外でローカル関数として定義してください。FMMは設定リストの順にスクリプトを実行し、priorityで並べ替えません。
QCチェックリスト
プロップスクリプトをデプロイする前に検証するため、このチェックリストを使用してください:
- ファイルは
api_version = 1を持つテーブルをちょうど1つ返している。 - すべてのフック名がプロップフック一覧と正確に一致している。エンチャントのフックは別の実行環境に属する。
- 既知のクリックフック以外でキャンセルするときは、
context.eventとcontext.event.cancelの両方を確認している。 context.stateフィールドはon_spawnで初期化されている。- すべての
scheduler:run_repeating(...)呼び出しに、on_destroyに対応するscheduler:cancel(...)がある。 - スケジューラコールバックは、外側の
contextではなく、コールバック自身のコンテキストパラメータを使用している。 on_game_tickフックは、コストの高い処理をチェックの後ろにゲートしている。- すべてのメソッド名がプロップAPIリファレンスに存在し、架空の別名を使っていない。
- パーティクル名は
FLAMEとflameの両方を受け付けます。API が大文字小文字を正規化して Bukkit のパーティクルを検索します。 サウンドには大文字小文字を問わない Bukkit の列挙名、minecraft:block.note_block.harpなどの有効なキー、独自リソースパックのキーを使用できます。クライアントのリソースパックにそのサウンドが必要です。 - スクリプトはフックやコールバック内でブロッキングや長時間実行される操作を呼び出していない。
AI生成のヒント
AIにプロップスクリプトを確実に生成させたい場合は、プロンプトに以下を含めてください:
- 正確なフック名 -- 例:「プレイヤーがプロップをクリックしたとき」ではなく
on_right_click。 - モデルファイルのアニメーション名 -- AIはこれを推測できません。提供してください。
- サウンド列挙名 -- 例:「ハープの音」ではなく
"BLOCK_NOTE_BLOCK_HARP"。 - パーティクル列挙名 -- 例:「火のパーティクル」ではなく
"FLAME"。 - 左クリックで標準ダメージを与えるか。防ぐ場合は
on_left_clickとcontext.event.cancel()を使います。 - 確認済みのメソッド名を使うこと。プロップAPIリファレンスを優先し、必要な操作が載っていなければ現在の実装を確認してください。別名を創作してはいけません。
良いプロンプトの例
右クリックで「activate」アニメーションを再生し、左クリックの標準ダメージをキャンセルするFMMプロップスクリプトを書いてください。クリック時にプロップの位置にFLAMEパーティクルを出し、BLOCK_LEVER_CLICKを鳴らしてください。context.cooldowns:check_local("activate", 40)でクリック間に2秒のクールダウンを設けてください。
アイテムコンテンツの互換性
scripts:を使う古いアイテムパックは、アイテム定義とエンチャントコンテンツの更新が必要です。FMMは廃止済みの定義を読み飛ばし、ファイル自体は変更しません。material:を追加してもアイテムスクリプトは動作しません。