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

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

このページには、FreeMinecraftModelsのプロップおよびアイテムスクリプトの完全に動作するサンプルに加え、実践的なパターンとベストプラクティスが含まれています。各サンプルには、何をするのか、なぜそうするのかを説明するウォークスルーが付いています。

スクリプティングが初めての場合は、はじめにから始めてください。APIの詳細については、プロップ&アイテムAPIを参照してください。


サンプル:無敵プロップ

学べること: 最もシンプルで実用的なスクリプト -- ダメージをキャンセルして、プロップが壊されないようにします。

これはFreeMinecraftModelsに同梱されている既製スクリプトです。

スクリプトファイル全体(クリックで展開)
return {
api_version = 1,

on_left_click = function(context)
if context.event then
context.event.cancel()
end
end
}

ウォークスルー

  1. フックの選択 -- on_left_clickはプレイヤーがプロップを殴る(左クリックする)と発火します。内部的には、これはプロップを支えるアーマースタンドに対するEntityDamageByEntityEventです。

  2. イベントガード -- context.eventはこのフックでは常に存在するはずですが、ガードしておくのは良い習慣です。

  3. キャンセル -- 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)
-- Make the door invulnerable
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
}

ウォークスルー

  1. ファイルスコープの定数 -- OPEN_ANIMATIONCLOSE_ANIMATIONはreturnテーブルの上で定義されています。これにより、異なるアニメーション名を使うかもしれない異なるモデルファイルに合わせて簡単に変更できます。

  2. 状態の初期化 -- on_spawncontext.state.is_open = falseを設定します。状態はこのプロップインスタンスのすべてのフックにわたって保持されます。

  3. 無敵化 -- on_left_clickフックがダメージをキャンセルするので、ドアが誤って壊されることはありません。

  4. トグルロジック -- on_right_clickcontext.state.is_openをチェックし、現在のアニメーションを停止し、適切なアニメーションを再生し、状態を反転させ、サウンドを再生します。play_animation()の前のstop_animation()呼び出しにより、クリーンな遷移が保証されます。

  5. サウンドフィードバック -- context.world:play_sound()はプロップの位置でBukkit Sound列挙名を再生します。サウンド名はUPPER_CASEの列挙名でなければなりません。

アニメーション名

"open""close"のようなアニメーション名は、モデルファイルで定義されているものと一致する必要があります。アニメーションが見つからない場合、play_animation()falseを返し、何も起こりません。正確なアニメーション名についてはモデルファイルを確認してください。


サンプル:パーティクル付き近接トリガー

学べること: ゾーンの作成、入退場イベントのゾーン監視、パーティクル効果、そしてクリーンアップ。

スクリプトファイル全体(クリックで展開)
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)
-- Make invulnerable
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
}

ウォークスルー

  1. 定数 -- ZONE_RADIUSPARTICLE_INTERVALは、簡単に調整できるようファイルスコープにあります。

  2. 状態の初期化 -- on_spawnは、他の処理を行う前にすべての状態フィールドをnil / 0に設定します。

  3. ゾーンの作成 -- context.zones:create_sphere()はプロップを中心とする球ゾーンを作成します。返されるハンドルは、後でこのゾーンを参照するために使う数値IDです。

  4. ゾーンの監視 -- context.zones:watch(handle, function() end, function() end)on_zone_enterおよびon_zone_leaveフックを有効にします。これらのフックはcontext.stateに保存されたカウンタを増減させます。プレイヤー固有の挙動が必要な場合に備え、これらは横断したプレイヤーをcontext.player / context.event.playerとして受け取ります。

  5. パーティクルループ -- 繰り返しタスクが0.5秒ごとにプロップの周りにリング状のパーティクルをスポーンします。パーティクルの種類は、プレイヤーがゾーン内にいるかどうかに基づいて変わります。

  6. クリーンアップ -- on_destroyは繰り返しタスクをキャンセルし、ゾーンの監視を解除します。どちらもプロップが削除されると自動的にクリーンアップされますが、明示的なクリーンアップはベストプラクティスです。

パーティクルのパフォーマンス

毎tick多数のパーティクルをスポーンすると、パフォーマンスに影響する可能性があります。適切な間隔(10〜20 tick)を使用し、パーティクル数を低く保ってください。上記の例ではPARTICLE_INTERVAL = 10(毎秒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)
-- Make invulnerable
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
}

ウォークスルー

  1. クールダウンパターン -- context.cooldowns:check_local("sound", COOLDOWN_TICKS)"sound"クールダウンが準備できているかをチェックし、準備できていれば即座に開始します。これによりクリックハンドラを小さく保ち、フラグをリセットするためだけにスケジューラタスクを作成することを避けられます。

  2. サウンド再生 -- context.world:play_sound()はBukkit Sound列挙名(UPPER_CASE)、座標、音量、ピッチを受け取ります。

  3. パーティクルフィードバック -- サウンド再生時にプロップの上に音符パーティクルが現れ、視覚的な合図を与えます。

  4. 無敵化 -- on_left_clickフックがいつも通りダメージをキャンセルします。

クールダウンの実装

通常のクールダウンにはcontext.cooldownsを使用してください。context.statescheduler: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
}

ウォークスルー

  1. アニメーションの自動開始 -- on_spawnはループする"idle"アニメーションを即座に再生します。blendのfalseは、前のアニメーションからブレンドせずに新しく開始することを意味します。loopのtrueは無限に繰り返すことを意味します。

  2. 環境パーティクル -- 繰り返しタスクが2秒ごとにプロップの上にエンチャントテーブルのパーティクルをスポーンし、魔法のような環境効果を作り出します。

  3. クリーンアップ -- on_destroyがパーティクルタスクをキャンセルします。


サンプル:座れる椅子

学べること: 右クリックでプレイヤーをプロップに乗せ、左クリックで降ろし、context.event.playerを使ってインタラクションしたプレイヤーを取得します。

スクリプトファイル全体(クリックで展開)
return {
api_version = 1,

on_right_click = function(context)
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 damage so the chair is invulnerable
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
}

ウォークスルー

  1. 右クリックで座る -- on_right_clickcontext.event.playerからプレイヤーを取得し、すでに搭乗者かどうかをチェックし(二重搭乗を避けるため)、context.prop:mount(player)を呼び出してプロップのアーマースタンドに座らせます。

  2. 左クリックで立つ -- on_left_clickはダメージイベントをキャンセルし(無敵化)、殴ったプレイヤーが現在搭乗者かどうかをチェックします。そうであれば、context.prop:dismount(player)が降ろします。

  3. 搭乗者のチェック -- context.prop:get_passengers()はエンティティテーブルの配列を返します。UUIDを比較して、リスト内でインタラクションしたプレイヤーを見つけます。

  4. サウンドフィードバック -- 座るときに木材設置音、立つときに木材破壊音が再生され、触覚的なフィードバックを与えます。


サンプル:祝福の祠

学べること: プレイヤーの手持ちアイテムのチェック、アイテムの消費、ランダムなポーション効果の付与、クールダウン管理、そしてパーティクル/サウンドのフィードバック。

スクリプトファイル全体(クリックで展開)
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)
-- Make invulnerable
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
}

ウォークスルー

  1. アイテムのチェック -- player:get_held_item()はメインハンドのアイテムについてtypeamountdisplay_nameを持つテーブルを返します(空の場合はnil)。held.typeはBukkitマテリアル名(大文字)なので、"GOLD_INGOT"と比較します。

  2. アイテムの消費 -- player:consume_held_item(1)はプレイヤーのメインハンドのスタックからアイテムを1つ取り除きます。

  3. ランダムなバフ -- ファイルスコープのBLESSINGSテーブルは利用可能な正の効果を列挙しています。math.random(#BLESSINGS)がランダムに1つ選びます。player:add_potion_effect(effect, duration, amplifier)がそれを付与します -- 600 tickは30秒、amplifier 1はレベルIIです。

  4. クールダウン -- 音を発するプロップのサンプルと同じ、ブール値フラグ+スケジューラのパターンです。30秒のクールダウンが祠の連打を防ぎます。

  5. フィードバック -- エンチャントと幸せな村人のパーティクル、加えてビーコン起動音が「神聖な祝福」の雰囲気を作り出します。


サンプル:呪われた祠

学べること: 負のポーション効果、落雷、エンティティのスポーン、そしてプレイヤーの供物に基づく分岐ロジック。

スクリプトファイル全体(クリックで展開)
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)
-- Make invulnerable
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
}

ウォークスルー

  1. 供物による分岐 -- スクリプトはplayer:get_held_item()をチェックし、2つの経路のいずれかを取ります:プレイヤーが金を持っていない場合は罰、持っている場合は報酬です。

  2. 落雷 -- context.world:strike_lightning(x, y, z)はプレイヤーの位置に本物の(ダメージを与える)雷を落とします。プレイヤーの位置はplayer.current_locationから読み取られます。

  3. ゾンビのスポーン -- context.world:spawn_entity("zombie", x, y, z)はバニラのゾンビをスポーンします。ループは三角法を使って、祠の周りの円形に均等に配置します。

  4. 負のポーション効果 -- player:add_potion_effect("poison", 400, 1)は20秒間の毒IIを付与します。効果名はBukkitのPotionEffectType名と一致する小文字の文字列です。

  5. 報酬の経路 -- 金が供えられると、祠はインゴットを1つ消費し、ランダムな正の効果を付与します。これは祝福の祠の挙動を反映しています。

  6. クールダウン -- どちらの分岐を取ったかに関わらず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)
-- Make invulnerable
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
}

ウォークスルー

  1. 状態ガード -- context.state.is_spinningは複数の重複する回転リクエストを防ぎます。フラグは回転が開始されたときに設定され、スケジュールされた停止が発火したときにクリアされます。

  2. タイマー付きアニメーション -- play_animation(SPIN_ANIMATION, true, false)はアニメーションを一度だけ再生します(ループなし)。scheduler:run_later(100, ...)呼び出しが、アニメーション自体がより長いまたはループする場合に備えて、ちょうど5秒後にアニメーションを停止します。

  3. 機械的なサウンド -- BLOCK_CHAIN_PLACEはカチッとした機械的な開始音を、BLOCK_CHAIN_FALLは巻き戻るような停止音を与えます。ピッチはお好みで調整してください。

  4. コールバックコンテキスト -- run_laterコールバックは、すべての状態とワールドへのアクセスにlater_contextcontextではなく)を使用します。これがスケジューラコールバックの正しいパターンです。

アニメーション名

"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)
-- Make invulnerable
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
}

ウォークスルー

  1. 近接ゾーン -- context.zones:create_sphere()は半径3ブロックのゾーンを作成します。context.zones:watch(handle, function() end, nil)on_zone_enterを有効にし、これは任意のプレイヤーが中に踏み込むと発火します。

  2. 驚かし効果 -- on_zone_enterフックは"jumpscare"アニメーション、ガストの叫び声、そしてキャンプファイアの煙パーティクルをスポーンします。この組み合わせが突然のびっくりする効果を作り出します。

  3. 60秒のクールダウン -- ブール値フラグのパターンが、驚かしが繰り返しトリガーされるのを防ぎます。一度トリガーされると、プロップは60秒間(COOLDOWN_TICKS = 1200)沈黙し、その後再武装します。

  4. 退場フックなし -- context.zones:watch()の3番目の引数がnilであることは、プレイヤーがゾーンを離れるタイミングを気にしないことを意味します。

  5. クリーンアップ -- on_destroyがゾーンの監視を解除します。ゾーンはプロップが削除されると自動的にクリーンアップされますが、明示的なクリーンアップはベストプラクティスです。

驚かしのデザイン

最も効果的なジャンプスケアのためには、プロップを角の周りや暗い場所に隠してください。半径3ブロックにより、プレイヤーが十分に近づいてから驚かしがトリガーされることが保証されます。SCARE_RADIUSCOOLDOWN_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)
-- Make invulnerable
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
}

ウォークスルー

  1. ボスのスポーン -- context.prop:spawn_elitemobs_boss(filename, x, y, z)は指定された座標にEliteMobsのカスタムボスをスポーンします。ファイル名はEliteMobsのcustombossesフォルダ内の.ymlファイルと一致する必要があります。

  2. 優雅なフォールバック -- spawn_elitemobs_boss()は、EliteMobsがインストールされていないかボスファイルが存在しない場合にnilを返します。スクリプトは警告ログメッセージ、不発のパーティクル効果、そして失敗を説明するプレイヤーメッセージでこれを処理します。

  3. スポーンオフセット -- ボスはloc.y + 1(プロップの1ブロック上)にスポーンされ、ボスがプロップや地面にめり込むのを防ぎます。

  4. クールダウン -- 10秒のクールダウンが、プレイヤーがゴブリン戦士でエリアを溢れさせるのを防ぎます。ゲームプレイのニーズに応じてCOOLDOWN_TICKSを調整してください。

  5. 視覚/音声による区別 -- 成功では炎パーティクルとエヴォーカーの召喚音を使い、劇的なスポーンを演出します。失敗では煙と火消しの音を使い、明確な「不発」効果を出すので、プレイヤーはコンソールを確認しなくても何かがうまくいかなかったことが分かります。

EliteMobs統合

ボスのファイル名(例:"goblin_warrior.yml")は、EliteMobs内の既存のカスタムボス設定に対応している必要があります。このスクリプトを使うマップやダンジョンを配布する場合は、ボス設定ファイルを含め、EliteMobsへの依存関係を文書化してください。


サンプル:フロストショックウェイブの剣(アイテムスクリプト)

学べること: on_equipでの状態初期化、能力をトリガーするon_shift_right_click、拡大する波のためのscheduler:run_repeating()、ターゲット探索のためのget_nearby_entities()、エンティティの戦闘メソッド、パーティクル/サウンド効果、get_highest_block_y()による地形改変、UIフィードバックのためのshow_action_bar()、そして非生物エンティティに対するガードを示す、完全なアイテムスクリプト。

スクリプトファイル全体(クリックで展開)
-- Frost Shockwave Sword
-- Shift + Right-click to release a horizontal frost wave

local WAVE_MAX_RADIUS = 12
local WAVE_SPEED = 2
local WAVE_TICK_INTERVAL = 2
local WAVE_DAMAGE = 4.0
local WAVE_KNOCKBACK = 1.2
local WAVE_CONE_ANGLE = 70

return {
api_version = 1,

on_equip = function(context)
context.state.wave_active = false
end,

on_shift_right_click = function(context)
if context.state.wave_active then return end
local loc = context.player.current_location
local yaw_rad = math.rad(loc.yaw)
local dir_x = -math.sin(yaw_rad)
local dir_z = math.cos(yaw_rad)

context.event:cancel()
context.state.wave_active = true
context.state.hit_entities = {}
context.state.destroyed_blocks = {}

local current_radius = 1
local origin_x, origin_y, origin_z = loc.x, loc.y, loc.z
local player_uuid = context.player.uuid

context.player:show_action_bar("&b&lFrost Shockwave!", 40)
context.world:play_sound("ENTITY_PLAYER_ATTACK_SWEEP", origin_x, origin_y, origin_z, 1.5, 0.5)

local task_id = context.scheduler:run_repeating(0, WAVE_TICK_INTERVAL, function(tick_context)
if current_radius > WAVE_MAX_RADIUS then
tick_context.state.wave_active = false
tick_context.scheduler:cancel(tick_context.state.wave_task)
return
end

local half_angle = math.rad(WAVE_CONE_ANGLE / 2)
local steps = math.floor(current_radius * 8)
for i = 0, steps do
local angle = (i / steps) * 2 * math.pi
local px = origin_x + math.cos(angle) * current_radius
local pz = origin_z + math.sin(angle) * current_radius
local to_x, to_z = px - origin_x, pz - origin_z
local to_len = math.sqrt(to_x * to_x + to_z * to_z)
if to_len > 0 then
local dot = (to_x * dir_x + to_z * dir_z) / to_len
if dot >= math.cos(half_angle) then
local ground_y = tick_context.world:get_highest_block_y(math.floor(px), math.floor(pz))
tick_context.world:spawn_particle("SNOWFLAKE", px, ground_y + 1.0, pz, 3, 0.3, 0.2, 0.3, 0.02)
local bx, bz = math.floor(px), math.floor(pz)
local block_key = bx .. "," .. bz
if not tick_context.state.destroyed_blocks[block_key] then
tick_context.state.destroyed_blocks[block_key] = true
local block_type = tick_context.world:get_block_at(bx, ground_y, bz)
if block_type ~= "bedrock" and block_type ~= "obsidian" and block_type ~= "barrier" then
tick_context.world:set_block_at(bx, ground_y, bz, "AIR")
end
end
end
end
end

local entities = tick_context.world:get_nearby_entities(origin_x, origin_y, origin_z, current_radius + 1)
for _, entity in ipairs(entities) do
if entity.uuid ~= player_uuid and not tick_context.state.hit_entities[entity.uuid] then
local eloc = entity.current_location
local dx, dz = eloc.x - origin_x, eloc.z - origin_z
local dist = math.sqrt(dx * dx + dz * dz)
if dist >= current_radius - 2 and dist <= current_radius + 1 and dist > 0 then
local dot = (dx * dir_x + dz * dir_z) / dist
if dot >= math.cos(half_angle) and entity.damage then
tick_context.state.hit_entities[entity.uuid] = true
entity:damage(WAVE_DAMAGE)
entity:push((dx/dist)*WAVE_KNOCKBACK, 0.3, (dz/dist)*WAVE_KNOCKBACK)
if entity.is_alive then entity:add_potion_effect("SLOWNESS", 60, 1) end
tick_context.world:spawn_particle("SNOWFLAKE", eloc.x, eloc.y+1, eloc.z, 10, 0.5, 0.5, 0.5, 0.1)
end
end
end
end

tick_context.world:play_sound("BLOCK_GLASS_BREAK",
origin_x + dir_x * current_radius, origin_y,
origin_z + dir_z * current_radius, 0.8, 1.5)
current_radius = current_radius + WAVE_SPEED
end)

context.state.wave_task = task_id
end
}

ウォークスルー

  1. 状態の初期化 -- on_equipwave_active = falseを設定し、複数の重複するショックウェイブを防ぎます。このフックはプレイヤーが剣を装備したときに発火します。

  2. 方向性のあるコーン -- プレイヤーのyawが方向ベクトル(dir_xdir_z)に変換されます。プレイヤーの正面70度のコーンが波の広がりを決定します。

  3. 拡大する波 -- scheduler:run_repeating()が2 tickごとに実行されます。各tickで波の半径がWAVE_SPEEDだけ大きくなります。パーティクルはリング状にスポーンされますが、コーンの角度内のみです。

  4. 地形の破壊 -- get_highest_block_y()が各地点の地面のレベルを見つけます。表面のブロックが破壊され(AIRに設定)、岩盤/黒曜石/バリアは除外されます。destroyed_blocksテーブルが二重処理を防ぎます。

  5. エンティティのターゲティング -- get_nearby_entities()が原点付近のすべてのエンティティを見つけます。スクリプトは各エンティティが現在の波の帯の中、コーンの中にあり、まだヒットしていないことをチェックします。if entity.damage thenのガードは極めて重要です -- get_nearby_entities()は生物だけでなくすべてのエンティティ(アーマースタンド、アイテムなど)を返します。

  6. 戦闘効果 -- ヒットしたエンティティはダメージを受け、波の方向にノックバックされ、3秒間の鈍化IIを受けます。

  7. プレイヤーフィードバック -- show_action_bar()が40 tickの間「Frost Shockwave!」を表示します。ガラスの割れる音が波の前面とともに進みます。

  8. クリーンアップ -- 半径がWAVE_MAX_RADIUSを超えると、繰り返しタスクは自身をキャンセルし、wave_activeフラグをリセットします。

エンティティ型の安全性

entity:damage()entity:push()entity:add_potion_effect()を呼び出す前に、必ずif entity.damage thenをチェックしてください。get_nearby_entities()メソッドは、これらのメソッドを持たないアーマースタンド、ドロップしたアイテム、経験値オーブなどの非生物を含む、範囲内のすべてのエンティティを返します。


サンプル:ヒーリングワンド(アイテムスクリプト)

学べること: 右クリック起動、クールダウン管理、使用回数制限のためのitem:consume()メソッド、そしてポーション効果による回復を示す、よりシンプルなアイテムスクリプト。

スクリプトファイル全体(クリックで展開)
-- Healing Wand
-- Right-click to heal yourself. Consumes one charge per use.

local HEAL_AMOUNT = 6.0 -- 3 hearts
local COOLDOWN_TICKS = 60 -- 3 seconds
local REGEN_DURATION = 40 -- 2 seconds of regen
local REGEN_AMPLIFIER = 1 -- Regeneration II

return {
api_version = 1,

on_right_click = function(context)
-- Check remaining charges
local uses = context.item:get_uses()
if uses <= 0 then
context.player:show_action_bar("&c&lOut of charges!", 40)
context.world:play_sound("BLOCK_FIRE_EXTINGUISH",
context.player.current_location.x,
context.player.current_location.y,
context.player.current_location.z,
0.8, 1.5)
return
end

-- Check cooldown
if not context.cooldowns:check_local("heal", COOLDOWN_TICKS) then
context.player:show_action_bar("&cWand recharging...", 20)
return
end

-- Cancel the event so we don't interact with blocks
if context.event then
context.event:cancel()
end

-- Consume one charge
context.item:set_uses(uses - 1)

-- Heal the player
local loc = context.player.current_location
context.player:add_potion_effect("REGENERATION", REGEN_DURATION, REGEN_AMPLIFIER)

-- Visual and audio feedback
context.world:spawn_particle("HEART", loc.x, loc.y + 1.5, loc.z, 8, 0.4, 0.3, 0.4, 0)
context.world:spawn_particle("HAPPY_VILLAGER", loc.x, loc.y + 1, loc.z, 15, 0.5, 0.5, 0.5, 0)
context.world:play_sound("ENTITY_PLAYER_LEVELUP", loc.x, loc.y, loc.z, 0.7, 1.8)

-- UI feedback
context.player:show_action_bar("&a&lHealed! &7(" .. (uses - 1) .. " charges left)", 40)
end
}

ウォークスルー

  1. クールダウンパターン -- context.cooldowns:check_local("heal", COOLDOWN_TICKS)が、再充填クールダウンのチェックと開始を1回の呼び出しで行います。アイテムのローカルクールダウンは、装備と装備解除のサイクルを跨いで保持されます。

  2. チャージシステム -- context.item:get_uses()がアイテムのPDCからカスタムの使用回数カウンタを読み取ります。各使用でset_uses()によりそれを減らします。チャージが0に達すると、ワンドは不発音を鳴らし、起動を拒否します。

  3. 回復 -- 直接HPを設定するのではなく、スクリプトは2秒間のリジェネレーションIIを付与します。これはMinecraftのHPシステムとより自然に連携し、他の効果と適切にスタックします。

  4. イベントのキャンセル -- context.event:cancel()が、右クリックが近くのブロックとインタラクションする(ブロックの設置、ドアを開けるなど)のを防ぎます。

  5. UIフィードバック -- show_action_bar()が、アクションの結果と残りチャージをプレイヤーに伝えます。クールダウン、チャージ切れ、回復成功で異なるメッセージにすることで、アイテムが反応的に感じられます。

カスタムアイテムの入手

適切にタグ付けされたカスタムアイテムを入手するには/fmm giveitem <id>を使用してください。他の手段で作成されたアイテムはfmm_item_id PDCキーを欠いている可能性があり、その場合スクリプトはそれらに対して起動しません。


ベストプラクティス

  • 小さなフックから始めて検証する。 ログメッセージを送る単一のon_spawnを書きます。それが発火することを確認します。そこから組み立てます。

  • ヘルパー関数はローカルに保つ。 local function toggle_door(context)のようなヘルパーはreturnテーブルの上で宣言します。これによりグローバルスコープから外れます。

  • すべての状態をon_spawnで初期化する。 on_right_clickcontext.state.is_openを読み取るのにon_spawnで設定しなかった場合、それはnilになり、比較が予期しない挙動をする可能性があります。

  • 完了したら繰り返しタスクをキャンセルする。 すべてのrun_repeatingには、on_destroyに対応するcancelが必要です。漏れたタスクはCPUを浪費します。

  • 新しいスケジューラコールバックコンテキストを使う。 スケジューラコールバックは新しいコンテキストパラメータを受け取ります。コールバック内では、外側のcontextではなく、常にそのパラメータを使用してください。

  • on_game_tickを軽量に保つ。 このフックを定義すると、サーバーtickごと(毎秒20回)に実行されます。コストの高い処理はcontext.cooldownsやスケジューラの間隔の後ろにゲートしてください。

  • プロップはデフォルトで無敵にする。 プロップを壊せるようにしたい場合を除き、すべてのスクリプトにon_left_clickのダメージキャンセルを含めてください。

  • Bukkit列挙にはUPPER_CASEを使う。 サウンド名とパーティクル名はBukkit列挙定数の形式(例:"flame"ではなく"FLAME")を使用しなければなりません。


初心者によくある間違い

  • スケジューラコールバック内で外側のcontextを使う。 外側のコンテキストはフックが実行された時点のスナップショットをキャプチャします。コールバック内では、常にコールバック自身のパラメータを使用してください。

  • 繰り返しタスクのキャンセルを忘れる。 on_spawnrun_repeatingを開始したのにキャンセルしなかった場合、そのタスクはプロップが削除されるまで実行され続けます。

  • on_spawnで状態を初期化しない。 context.state.xを設定する前に読み取るとnilが返り、ロジックが静かに壊れる可能性があります。

  • 間違ったアニメーション名。 play_animation("open")falseを返す場合、アニメーション名がモデルファイル内のものと一致していません。正確な名前についてはモデルを確認してください。

  • 小文字のサウンド/パーティクル名。 "flame"は機能しません -- "FLAME"を使用してください。APIはパーティクルについては内部でUPPER_CASEに変換しますが、Sound列挙名は正確でなければなりません。

  • api_version = 1を忘れる。 返されるテーブルにはこのフィールドが含まれていなければなりません。さもないとFMMはスクリプトをロードしません。

  • フックでない関数を返されるテーブル内に入れる。 ヘルパー関数はreturn文の上で宣言しなければなりません。返されるテーブルのキーとして許可されるのは、フック名(on_spawnon_right_clickなど)のみです。


QCチェックリスト

プロップスクリプトをデプロイする前に検証するため、このチェックリストを使用してください:

  1. ファイルはapi_version = 1を持つテーブルをちょうど1つ返している。
  2. すべてのフック名が、プロップフックリストまたはアイテムフックリストのエントリと正確に一致している。
  3. context.eventcancel()を呼び出す前にif context.event thenでガードされている。
  4. context.stateフィールドはon_spawnで初期化されている。
  5. すべてのscheduler:run_repeating(...)呼び出しに、on_destroyに対応するscheduler:cancel(...)がある。
  6. スケジューラコールバックは、外側のcontextではなく、コールバック自身のコンテキストパラメータを使用している。
  7. on_game_tickフックは、コストの高い処理をチェックの後ろにゲートしている。
  8. すべてのメソッド名がプロップ&アイテムAPIリファレンスに存在する -- 創作したエイリアスはない。
  9. サウンド名とパーティクル名はUPPER_CASEのBukkit列挙名を使用している。
  10. スクリプトはフックやコールバック内でブロッキングや長時間実行される操作を呼び出していない。

AI生成のヒント

AIにプロップスクリプトを確実に生成させたい場合は、プロンプトに以下を含めてください:

  • 正確なフック名 -- 例:「プレイヤーがプロップをクリックしたとき」ではなくon_right_click
  • モデルファイルのアニメーション名 -- AIはこれを推測できません。提供してください。
  • サウンド列挙名 -- 例:「ハープの音」ではなく"BLOCK_NOTE_BLOCK_HARP"
  • パーティクル列挙名 -- 例:「火のパーティクル」ではなく"FLAME"
  • プロップを無敵にすべきかどうか -- そうであれば、context.event.cancel()を伴うon_left_clickを含めてください。
  • 文書化されたメソッド名のみを使う -- プロップAPIページにないなら、それは存在しません。

良いプロンプトの例

右クリックされたときに「activate」アニメーションを再生し、プロップを無敵にし、クリック時にプロップの位置にFLAMEパーティクルをスポーンし、BLOCK_LEVER_CLICKサウンドを再生し、context.cooldowns:check_local("activate", 40)を使ってクリック間に2秒のクールダウンを持つFMMプロップスクリプトを書いてください。


ゴブリンアイテムコレクション

ゴブリンの武器をテーマにした10個のサンプルアイテムスクリプトのセットが、ダウンロード可能なサンプルコンテンツとして利用できます。これらはベースプラグインのjarには同梱されていません -- プラグイン自体が初回実行時にscripts/へ書き込むのは、4つの既製プロップスクリプト(invulnerable.luapickupable.luastorage_single.luastorage_double.lua)のみです。以下の各ゴブリンスクリプトは、異なる戦闘メカニクス、パーティクル効果、そしてAPI使用パターンを示しています。

スクリプトアイテム効果
goblin_golden_sword.lua金の剣25%の確率でなぎ払い -- 近くのすべての敵にダメージを与えて押しのける
goblin_iron_axe.lua鉄の斧20%の確率でスパイク -- 鍾乳石のビジュアルでターゲットを上方に打ち上げる
goblin_bow.lua命中したモブをプレイヤーの方へ引き寄せる
goblin_crossbow.luaクロスボウ33%の確率で花火の連射
goblin_trident.luaトライデント氷のドームが3秒間ターゲットを包む
goblin_iron_hoe.lua鉄のクワ15%の確率で毒の雲
goblin_mace.luaメイス空から隕石が落下(右クリック)
goblin_spear.lua照準に沿った炎のビーム(shift+右クリック)
goblin_shield.lua防御時にガラスのドーム(リアクティブ)
goblin_iron_sword.lua鉄の剣闇のリングのチャージ(shift+右クリック)
モデル不要

これらのスクリプトはバニラの見た目のアイテムで動作します。各スクリプトはmaterial:フィールドを持つYML設定のみが必要で、.bbmodelファイルは不要です。例えば、金の剣のスクリプトを有効にするには、次のようなYML設定を作成します:

isEnabled: true
material: GOLDEN_SWORD
scripts:
- goblin_golden_sword.lua

次のステップ