Перейти к основному содержимому

Lua-скрипты: примеры и приёмы

Здесь приведены примеры скриптов пропсов FreeMinecraftModels с пояснениями. Подготовьте модели и анимации, на которые ссылаются примеры, и проверьте их на сервере перед использованием в карте.

Если вы только начинаете, прочитайте руководство по началу работы. Описание методов находится в API пропсов.


Пример: неуязвимый пропс​

Чему учит: Минимальный скрипт для отмены стандартного урона от левого клика.

Сохраните пример как invulnerable.lua в папке скриптов и перезагрузите скрипты перед привязкой к пропсу.

Полный файл скрипта (нажмите, чтобы развернуть)
return {
api_version = 1,

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

Разбор​

  1. on_left_click срабатывает при ударе игрока по пропсу. Lua-событие содержит флаг отмены стандартной обработки урона FMM.
  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)
-- 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
}

Разбор​

  1. Константы OPEN_ANIMATION и CLOSE_ANIMATION находятся над возвращаемой таблицей, чтобы их было удобно менять под имена анимаций модели.
  2. on_spawn задаёт context.state.is_open = false. Состояние сохраняется между хуками одной привязки скрипта к пропсу. Другие скрипты того же пропса имеют отдельное состояние.
  3. on_left_click отменяет стандартный урон от этого клика.
  4. on_right_click запрашивает остановку и нужную анимацию, переключает флаг и проигрывает звук. Флаг отражает запрос, а не подтверждённое воспроизведение. blend = true ставит анимацию в очередь. Остановка не прерывает активную анимацию death общего движка. См. анимации. Пример не меняет коллизию барьеров и не гарантирует проходимость дверного проёма.
  5. Звуки принимают имена enum 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
}

Разбор​

  1. ZONE_RADIUS и PARTICLE_INTERVAL вынесены на уровень файла для настройки.
  2. on_spawn сначала присваивает полям состояния nil или 0.
  3. context.zones:create_sphere() создаёт неподвижную сферу в позиции пропса при спавне. Числовой handle ссылается на эту зону. Кольцо частиц читает текущую позицию пропса, поэтому используйте неподвижный пропс либо обновляйте зону при перемещении.
  4. context.zones:watch(handle, function() end, function() end) включает on_zone_enter и on_zone_leave. Они меняют счётчик в context.state и получают пересекающего границу игрока через context.player / context.event.player, если нужна логика для конкретного игрока.
  5. Задача создаёт кольцо частиц каждые 10 серверных тиков, то есть раз в полсекунды при 20 TPS. Тип частиц зависит от наличия игроков в зоне.
  6. on_destroy отменяет задачу и отслеживание зоны. При удалении пропса система также очищает их автоматически, но явная очистка полезна.
Производительность частиц

Много частиц каждый тик создают нагрузку. Используйте интервал от 10 до 20 тиков и ограничивайте количество. Здесь PARTICLE_INTERVAL = 10: при 20 TPS кольцо из 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
}

Разбор​

  1. context.cooldowns:check_local("sound", COOLDOWN_TICKS) проверяет готовность и сразу запускает кулдаун. Его разделяют все игроки, использующие эту привязку скрипта к пропсу. Тик кулдауна равен 50 мс прошедшего времени; задержки планировщика считают реальные серверные тики.
  2. Звуки принимают имена enum Bukkit без учёта регистра, допустимые ключи вроде minecraft:block.note_block.harp и ключи собственных ресурспаков. Клиенту нужен соответствующий звук в ресурспаке.
  3. Вместе со звуком над пропсом появляются частицы нот.
  4. on_left_click отменяет стандартный урон от этого клика.
Реализация кулдауна

Для обычного кулдауна используйте context.cooldowns. context.state вместе с scheduler:run_later() нужны, когда по завершении задержки требуется собственное изменение состояния.


Пример: анимированный фоновый пропс​

Чему учит: Запуск циклической анимации при спавне и создание частиц с интервалом в тиках.

Полный файл скрипта (нажмите, чтобы развернуть)
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 переключает анимацию немедленно. Встроенное состояние idle повторяется независимо от переданного аргумента loop.
  2. Повторяющаяся задача создаёт над пропсом частицы стола зачарований каждые 2 секунды для фонового эффекта.
  3. 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
}

Разбор​

  1. on_right_click получает context.event.player, проверяет, не сидит ли игрок уже, и через context.prop:mount(player) запрашивает первое свободное место в точках крепления m_.
  2. on_left_click отменяет стандартный урон этого клика и, если ударивший игрок сидит, вызывает context.prop:dismount(player).
  3. context.prop:get_passengers() возвращает массив таблиц сущностей. UUID позволяет найти нужного игрока.
  4. Звуки сопровождают поставленные в очередь запросы посадки и высадки, а не подтверждают их выполнение. Хук правого клика отменяет стандартную посадку 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
}

Разбор​

  1. player:get_held_item() возвращает type, amount, display_name предмета в основной руке либо nil для пустой руки. held.type содержит имя материала Bukkit в верхнем регистре, поэтому сравнивается с "GOLD_INGOT".
  2. player:consume_held_item(1) ставит удаление из основной руки в очередь. Рука читается при выполнении задачи; результат оплаты не возвращается. Пример не выполняет атомарный обмен: смена предмета может изменить то, что будет потрачено, а благословение применяется сразу.
  3. Таблица BLESSINGS на уровне файла перечисляет положительные эффекты. math.random(#BLESSINGS) выбирает один, а player:add_potion_effect(effect, duration, amplifier) накладывает его. 600 тиков соответствуют 30 секундам, amplifier 1 задаёт уровень II.
  4. Флаг общий для всех игроков этой привязки скрипта. Он сбрасывается через 600 серверных тиков, то есть 30 секунд при 20 TPS. Для кулдауна по прошедшему времени используйте context.cooldowns, как в звуковом примере.
  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)
-- 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
}

Разбор​

  1. Скрипт проверяет player:get_held_item(): отсутствие золота ведёт к наказанию, наличие к награде.
  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) накладывает Отравление II на 20 секунд. Имена эффектов соответствуют PotionEffectType Bukkit в нижнем регистре.
  5. Ветка награды ставит расход предмета в основной руке в очередь и накладывает случайный положительный эффект. Как и святилище благословения, это демонстрация, а не атомарная операция оплаты и выдачи награды.
  6. Обе ветки задают общий флаг для всех игроков этой привязки скрипта. Сброс происходит через 600 серверных тиков, то есть 30 секунд при 20 TPS.
Производительность спавна сущностей

Одновременный спавн нескольких сущностей может нагружать сервер. Ограничьтесь числом от 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
}

Разбор​

  1. context.state.is_spinning предотвращает повторные запросы вращения. Флаг задаётся при запуске и сбрасывается при запланированной остановке.
  2. play_animation(SPIN_ANIMATION, true, false) запрашивает нециклическую анимацию через очередь. Остановка выполняется через 100 серверных тиков после клика, а не после начала воспроизведения. Занятая очередь может сократить видимое вращение или не дать ему начаться. Общий движок не останавливает активную анимацию death. Пример задаёт флаг даже при результате false от запроса анимации.
  3. BLOCK_CHAIN_PLACE даёт механический звук запуска, а BLOCK_CHAIN_FALL звук остановки. Высоту тона можно настроить.
  4. Коллбэк 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
}

Разбор​

  1. context.zones:create_sphere() создаёт неподвижную зону радиусом 3 блока в позиции спавна. context.zones:watch(handle, function() end, nil) включает on_zone_enter. Перемещая пропс, перемещайте или пересоздавайте и зону.
  2. on_zone_enter проигрывает "jumpscare", крик гаста и создаёт дым от костра для внезапного пугающего эффекта.
  3. Все игроки разделяют флаг этой привязки скрипта. Он сбрасывается через 1200 серверных тиков, то есть 60 секунд при 20 TPS. После этого следующий вход в зону может снова запустить эффект.
  4. Третий аргумент nil у context.zones:watch() означает, что выход игроков не обрабатывается.
  5. 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
}

Разбор​

  1. context.prop:spawn_elitemobs_boss(filename, x, y, z) создаёт босса EliteMobs в указанных координатах. Имя должно соответствовать .yml в папке custombosses EliteMobs.
  2. Метод возвращает nil, если интеграция не может вернуть живого босса: например, плагин недоступен, конфигурация отсутствует или спавн завершился неудачей. Сообщение примера предлагает проверить EliteMobs, но не определяет точную причину.
  3. Запрошенная позиция находится на один блок выше пропса. Это не проверка ландшафта или коллизии. Выберите свободное место, подходящее боссу.
  4. Привязка скрипта разрешает одну попытку каждые 200 серверных тиков, то есть 10 секунд при 20 TPS, общую для всех игроков. Ограничения числа живых боссов нет, и исчезновение пропса не удаляет созданных боссов. Для повторяемого игрового события добавьте необходимые ограничения.
  5. При успехе используются пламя и звук призыва заклинателя; при неудаче дым и тушение огня. Игрок видит результат, не открывая консоль.
Интеграция с EliteMobs

Имя вроде "goblin_warrior.yml" должно соответствовать существующей конфигурации босса. При распространении карты или подземелья со скриптом приложите конфигурацию босса и укажите зависимость от EliteMobs.


Эффекты предметов​

Примеры скриптов экипированных предметов больше не поддерживаются. Для текущих эффектов используйте определения пользовательских предметов и собственные зачарования.

Рекомендации​

  • Начинайте с малого и проверяйте. Напишите один on_spawn с сообщением в журнал, убедитесь в его срабатывании и затем расширяйте скрипт.
  • Делайте вспомогательные функции локальными. Объявляйте local function toggle_door(context) над возвращаемой таблицей, вне глобальной области видимости.
  • Инициализируйте состояние в on_spawn. Если читать context.state.is_open в on_right_click без начального присваивания, значение будет nil, а сравнение может дать неожиданный результат.
  • Отменяйте ненужные повторяющиеся задачи. У каждого run_repeating должен быть соответствующий cancel в on_destroy, чтобы не тратить CPU.
  • Используйте собственный контекст коллбэка. Планировщик передаёт свежий параметр контекста. Внутри коллбэка используйте его, а не внешний context.
  • Ограничивайте работу в on_game_tick. Он выполняется каждый серверный тик, 20 раз в секунду при 20 TPS. Затратные действия ограничивайте через context.cooldowns или интервал планировщика.
  • Явно управляйте уроном левого клика. Если удар не должен наносить стандартный урон пропсу, отменяйте его в on_left_click. Это не блокирует все источники урона.
  • Частицы принимают FLAME и flame: API нормализует регистр перед поиском частицы Bukkit. Звуки принимают имена enum Bukkit без учёта регистра, допустимые ключи вроде minecraft:block.note_block.harp и ключи собственных ресурспаков. Звук должен быть в ресурспаке клиента.

Частые ошибки новичков​

  • Внешний context внутри коллбэка. Он содержит снимок момента вызова хука. Используйте собственный параметр коллбэка.
  • Забытый повторяющийся таймер. Запущенный в on_spawn и не отменённый run_repeating работает до удаления пропса.
  • Неинициализированное состояние. Чтение context.state.x до присваивания возвращает nil и может незаметно нарушить логику.
  • Неверное имя анимации. Если play_animation("open") возвращает false, проверьте точное имя в модели.
  • Частицы принимают FLAME и flame: API нормализует регистр перед поиском частицы Bukkit. Звуки принимают имена enum Bukkit без учёта регистра, допустимые ключи вроде minecraft:block.note_block.harp и ключи собственных ресурспаков. Звук должен быть в ресурспаке клиента.
  • Отсутствие api_version = 1. Возвращаемая таблица должна содержать поле, иначе FMM не загрузит скрипт.
  • Вспомогательные функции в возвращаемой таблице. Помимо api_version и необязательного priority, допускаются только поддерживаемые имена хуков со значениями-функциями. Помощники объявляются локально вне таблицы. FMM выполняет привязанные скрипты в порядке списка конфигурации; priority его не меняет.

Проверка качества​

Перед использованием скрипта проверьте следующее:

  1. Файл возвращает ровно одну таблицу с api_version = 1.
  2. Имена хуков точно соответствуют списку хуков пропса. Хуки зачарований относятся к отдельной среде выполнения.
  3. Вне известного хука клика перед отменой проверены и context.event, и context.event.cancel.
  4. Поля context.state инициализируются в on_spawn.
  5. Каждому scheduler:run_repeating(...) соответствует scheduler:cancel(...) в on_destroy.
  6. Коллбэки используют свой параметр контекста, а не внешний context.
  7. Затратная работа в on_game_tick ограничена проверкой.
  8. Все методы существуют в API пропсов, выдуманных псевдонимов нет.
  9. Частицы принимают FLAME и flame: API нормализует регистр перед поиском частицы Bukkit. Звуки принимают имена enum Bukkit без учёта регистра, допустимые ключи вроде minecraft:block.note_block.harp и ключи собственных ресурспаков. Звук должен быть в ресурспаке клиента.
  10. В хуках и коллбэках нет блокирующих или длительных операций.

Советы по генерации через ИИ​

Укажите в запросе:

  • Точное имя хука, например on_right_click, а не только описание клика игрока.
  • Имена анимаций из модели. ИИ не может их угадать.
  • Имена enum звуков, например "BLOCK_NOTE_BLOCK_HARP", а не просто звук арфы.
  • Имена enum частиц, например "FLAME", а не просто огонь.
  • Нужно ли наносить стандартный урон левым кликом. Для отмены используются on_left_click и context.event.cancel().
  • Требование использовать проверенные методы. Сначала сверяйтесь с API пропсов; если нужная операция не описана, проверьте текущий исходный код среды выполнения, не придумывая псевдоним.

Пример хорошего запроса​

Напиши скрипт пропса FMM, который проигрывает "activate" при правом клике, отменяет стандартный урон левого клика, создаёт частицы FLAME в позиции пропса и проигрывает BLOCK_LEVER_CLICK. Между кликами нужен кулдаун 2 секунды через context.cooldowns:check_local("activate", 40).


Совместимость контента предметов​

Старые наборы предметов с scripts: требуют обновлённых определений предметов и зачарований. FMM пропускает устаревшие определения и оставляет файлы без изменений. Добавление material: больше не активирует скрипт предмета.

Следующие шаги​