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

Lua-скриптинг: Примеры и паттерны

Эта страница содержит полные рабочие примеры скриптов пропсов FreeMinecraftModels, а также практические паттерны и лучшие практики. Каждый пример включает разбор, объясняющий что он делает и почему.

Если вы новичок в скриптинге пропсов, начните с Начало работы. Полные детали API см. в Prop 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)
-- Делаем дверь неуязвимой
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
-- Закрываем дверь
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
-- Открываем дверь
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 проверяет context.state.is_open, останавливает текущую анимацию, воспроизводит соответствующую анимацию, переключает состояние и воспроизводит звук. Вызов stop_animation() перед play_animation() обеспечивает чистые переходы.

  5. Звуковая обратная связьcontext.world:play_sound() воспроизводит звук по имени перечисления Bukkit Sound в местоположении пропса. Имена звуков должны быть в ВЕРХНЕМ_РЕГИСТРЕ.

Имена анимаций

Имена анимаций вроде "open" и "close" должны совпадать с определёнными в файле модели. Если анимация не найдена, play_animation() возвращает false и ничего не происходит. Проверьте файл модели на точные имена анимаций.


Пример: Триггер приближения с частицами

Чему учит: Создание зон, наблюдение за зонами для событий входа/выхода, эффекты частиц и очистка.

Полный файл скрипта (нажмите, чтобы развернуть)
local ZONE_RADIUS = 8
local PARTICLE_INTERVAL = 10 -- тики между всплесками частиц

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

-- Создаём сферическую зону вокруг пропса
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, ZONE_RADIUS)
context.state.zone_handle = handle

-- Наблюдаем за входом/выходом
context.zones:watch(
handle,
function(player)
-- on_enter
context.state.players_in_zone = (context.state.players_in_zone or 0) + 1
end,
function(player)
-- on_leave
context.state.players_in_zone = math.max(0, (context.state.players_in_zone or 0) - 1)
end
)

-- Запускаем повторяющийся эффект частиц на границе зоны
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

-- Создаём частицы кольцом на границе зоны
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
-- Красные частицы, когда игроки внутри
tick_context.world:spawn_particle("DUST", px, prop_loc.y + 0.5, pz, 1, 0, 0, 0, 0)
else
-- Зелёные частицы, когда зона пуста
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)
-- Делаем неуязвимым
if context.event then
context.event.cancel()
end
end,

on_destroy = function(context)
-- Очищаем повторяющуюся задачу
if context.state.particle_task then
context.scheduler:cancel(context.state.particle_task)
context.state.particle_task = nil
end
-- Очищаем наблюдение за зоной
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() создаёт сферическую зону с центром в пропсе. Возвращённый идентификатор — числовой ID для ссылки на эту зону позже.

  4. Наблюдение за зонойcontext.zones:watch() регистрирует обратные вызовы для входа и выхода игрока. Обратные вызовы увеличивают и уменьшают счётчик, хранящийся в context.state.

  5. Цикл частиц — Повторяющаяся задача создаёт частицы кольцом вокруг пропса каждые полсекунды. Тип частиц меняется в зависимости от наличия игроков в зоне.

  6. Очисткаon_destroy отменяет повторяющуюся задачу и прекращает наблюдение за зоной. Хотя обе очищаются автоматически при удалении пропса, явная очистка — лучшая практика.

Производительность частиц

Создание большого количества частиц каждый тик может влиять на производительность. Используйте разумный интервал (10-20 тиков) и держите количество частиц низким. Пример выше использует PARTICLE_INTERVAL = 10 (дважды в секунду) с только 12 частицами на кольцо.


Пример: Звукоизлучающий пропс

Чему учит: Воспроизведение звуков при взаимодействии, поведение кулдауна с использованием состояния и планировщика, предотвращение быстрых повторных взаимодействий.

Полный файл скрипта (нажмите, чтобы развернуть)
local SOUND_NAME = "BLOCK_NOTE_BLOCK_HARP"
local COOLDOWN_TICKS = 40 -- 2 секунды между звуками

return {
api_version = 1,

on_spawn = function(context)
context.state.on_cooldown = false
end,

on_left_click = function(context)
-- Делаем неуязвимым
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
-- Предотвращаем спам
if context.state.on_cooldown then
return
end

local loc = context.prop.current_location
if loc == nil then return end

-- Воспроизводим звук
context.world:play_sound(SOUND_NAME, loc.x, loc.y, loc.z, 1.0, 1.0)

-- Показываем частицы
context.world:spawn_particle("NOTE", loc.x, loc.y + 1.5, loc.z, 5, 0.3, 0.3, 0.3, 0)

-- Устанавливаем кулдаун
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}

Разбор

  1. Паттерн кулдауна — Поскольку скрипты пропсов FMM не имеют встроенного API context.cooldowns, как EliteMobs, пример реализует простой кулдаун с помощью context.state.on_cooldown и scheduler:run_later(). Флаг устанавливается в true при воспроизведении звука, и отложенная задача сбрасывает его через COOLDOWN_TICKS.

  2. Воспроизведение звукаcontext.world:play_sound() принимает имя перечисления Bukkit Sound в ВЕРХНЕМ_РЕГИСТРЕ, координаты, громкость и высоту тона.

  3. Визуальная обратная связь — Частицы нот появляются над пропсом при воспроизведении звука, давая визуальный сигнал.

  4. Неуязвимость — Хук on_left_click отменяет урон как обычно.

  5. Context обратного вызова планировщика — Обратный вызов run_later получает later_context — новый context. Мы используем later_context.state (а не context.state) для сброса флага кулдауна. Поскольку состояние общее, оба указывают на одну и ту же таблицу — но использование параметра context обратного вызова — правильная привычка.

Реализация кулдауна

Скрипты пропсов FMM не имеют API context.cooldowns из EliteMobs. Используйте паттерн, показанный здесь: булев флаг в context.state в сочетании с scheduler:run_later() для его сброса. Это даёт полный контроль над длительностью и поведением кулдауна.


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

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

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

on_spawn = function(context)
-- Запускаем анимацию покоя немедленно, зацикленно
context.prop:play_animation("idle", false, true)

-- Испускаем фоновые частицы каждые 40 тиков (2 секунды)
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". false для blend означает запуск с нуля без перехода от предыдущей анимации. true для loop означает бесконечное повторение.

  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

-- Проверяем, сидит ли игрок уже на этом пропсе
local passengers = context.prop:get_passengers()
for i = 1, #passengers do
if passengers[i].uuid == player.uuid then
-- Игрок уже сидит, ничего не делаем по правому клику
return
end
end

-- Сажаем игрока на стул
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)
-- Отменяем урон, чтобы стул был неуязвим
if context.event then
context.event.cancel()
end

local player = context.event and context.event.player
if not player then return end

-- Проверяем, сидит ли игрок, и высаживаем его
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) для посадки на стойку для брони пропса.

  2. Левый клик для вставанияon_left_click отменяет событие урона (неуязвимость), затем проверяет, является ли бьющий игрок пассажиром. Если да, context.prop:dismount(player) высаживает его.

  3. Проверка пассажировcontext.prop:get_passengers() возвращает массив таблиц сущностей. Мы сравниваем UUID, чтобы найти взаимодействующего игрока в списке.

  4. Звуковая обратная связь — Звук размещения дерева при посадке и звук разрушения дерева при вставании дают тактильную обратную связь.


Пример: Святилище благословения

Чему учит: Проверка предмета в руке игрока, потребление предметов, применение случайных эффектов зелий, управление кулдауном, обратная связь частицами и звуком.

Полный файл скрипта (нажмите, чтобы развернуть)
local COOLDOWN_TICKS = 600  -- 30 секунд между использованиями

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)
-- Делаем неуязвимым
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

-- Проверяем кулдаун
if context.state.on_cooldown then
player:send_message("&eСвятилище перезаряжается... Пожалуйста, подождите.")
return
end

-- Проверяем, держит ли игрок золотой слиток
local held = player:get_held_item()
if not held or held.type ~= "gold_ingot" then
player:send_message("&eСвятилище требует золотое подношение...")
return
end

-- Потребляем один золотой слиток
player:consume_held_item(1)

-- Воспроизводим эффекты благословения
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

-- Применяем случайное благословение
local chosen = BLESSINGS[math.random(#BLESSINGS)]
player:add_potion_effect(chosen.effect, 600, 1) -- 30 секунд, уровень II
player:send_message("&aСвятилище благословляет вас " .. chosen.name .. "!")

-- Устанавливаем кулдаун
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 с "gold_ingot" (имя материала в нижнем регистре).

  2. Потребление предметаplayer:consume_held_item(1) удаляет один предмет из стака в основной руке игрока.

  3. Случайный бафф — Таблица BLESSINGS на уровне файла перечисляет доступные положительные эффекты. math.random(#BLESSINGS) выбирает один случайным образом. player:add_potion_effect(effect, duration, amplifier) применяет его — 600 тиков — это 30 секунд, усилитель 1 — это уровень II.

  4. Кулдаун — Тот же паттерн «булев флаг плюс планировщик» из примера звукоизлучающего пропса. 30-секундный кулдаун предотвращает спам святилища.

  5. Обратная связь — Частицы зачарования и happy-villager плюс звук активации маяка создают ощущение «божественного благословения».


Пример: Проклятое святилище

Чему учит: Негативные эффекты зелий, удары молнии, спавн сущностей и ветвящаяся логика на основе подношения игрока.

Полный файл скрипта (нажмите, чтобы развернуть)
local COOLDOWN_TICKS = 600  -- 30 секунд между использованиями

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)
-- Делаем неуязвимым
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

-- Проверяем кулдаун
if context.state.on_cooldown then
player:send_message("&7Тёмное святилище пульсирует остаточной энергией...")
return
end

local loc = context.prop.current_location
if loc == nil then return end

-- Проверяем, держит ли игрок золотой слиток
local held = player:get_held_item()
if not held or held.type ~= "gold_ingot" then
-- Нет подношения — наказываем игрока!
player:send_message("&4Святилище требует дань! Ты осмелился подойти с пустыми руками?!")

-- Бьём молнией по игроку
local player_loc = player.current_location
if player_loc then
context.world:strike_lightning(player_loc.x, player_loc.y, player_loc.z)
end

-- Спавним орду зомби вокруг святилища
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

-- Применяем случайное проклятие
local chosen_curse = CURSES[math.random(#CURSES)]
player:add_potion_effect(chosen_curse.effect, 400, 1) -- 20 секунд, уровень II
player:send_message("&cСвятилище проклинает вас " .. chosen_curse.name .. "!")

-- Зловещие эффекты
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
-- Золото предложено — награждаем игрока
player:consume_held_item(1)

-- Применяем случайный бафф
local chosen_buff = BUFFS[math.random(#BUFFS)]
player:add_potion_effect(chosen_buff.effect, 600, 1) -- 30 секунд, уровень II
player:send_message("&aТёмное святилище принимает подношение. Вы благословлены " .. chosen_buff.name .. "!")

-- Позитивная обратная связь
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

-- Устанавливаем кулдаун независимо от пути
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) применяет 20 секунд Яда II. Имена эффектов — строки в нижнем регистре, соответствующие именам PotionEffectType Bukkit.

  5. Путь награды — Когда золото предложено, святилище потребляет один слиток и применяет случайный положительный эффект, зеркалируя поведение святилища благословения.

  6. Кулдаун — 30-секундный кулдаун применяется независимо от выбранной ветки, предотвращая быстрое наказание или награждение.

Производительность спавна сущностей

Спавн нескольких сущностей одновременно может влиять на производительность сервера. Держите количество низким (4-6) и рассмотрите добавление кулдауна на игрока, если много игроков одновременно используют святилище.


Пример: Вращающийся глобус

Чему учит: Воспроизведение анимации по таймеру при взаимодействии, планирование остановки анимации и механические звуковые эффекты.

Полный файл скрипта (нажмите, чтобы развернуть)
local SPIN_ANIMATION = "spin"
local SPIN_DURATION = 100 -- 5 секунд в тиках

return {
api_version = 1,

on_spawn = function(context)
context.state.is_spinning = false
end,

on_left_click = function(context)
-- Делаем неуязвимым
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
-- Предотвращаем запуск нового вращения во время текущего
if context.state.is_spinning then
return
end

local loc = context.prop.current_location
if loc == nil then return end

-- Запускаем анимацию вращения (без зацикливания)
context.prop:play_animation(SPIN_ANIMATION, true, false)
context.state.is_spinning = true

-- Воспроизводим механический щёлкающий звук
context.world:play_sound("BLOCK_CHAIN_PLACE", loc.x, loc.y, loc.z, 1.0, 1.5)

-- Планируем остановку анимации через 5 секунд
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. Context обратного вызова — Обратный вызов run_later использует later_context (а не context) для всего доступа к состоянию и миру. Это правильный паттерн для обратных вызовов планировщика.

Имена анимаций

Имя анимации "spin" должно совпадать с определённым в файле модели. Если ваша модель использует другое имя (например, "rotate", "turn"), обновите константу SPIN_ANIMATION соответственно.


Пример: Пропс-скример

Чему учит: Триггеры зоны приближения, одноразовые эффекты испуга с длинным кулдауном, комбинирование звука/частиц/анимации для драматического эффекта.

Полный файл скрипта (нажмите, чтобы развернуть)
local SCARE_RADIUS = 3
local COOLDOWN_TICKS = 1200 -- 60 секунд между испугами
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

-- Создаём маленькую сферическую зону вокруг пропса
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, SCARE_RADIUS)
context.state.zone_handle = handle

-- Наблюдаем за входом игроков в зону
context.zones:watch(
handle,
function(player)
-- on_enter: запускаем испуг
if context.state.on_cooldown then
return
end

local scare_loc = context.prop.current_location
if scare_loc == nil then return end

-- Воспроизводим анимацию скримера
context.prop:stop_animation()
context.prop:play_animation(SCARE_ANIMATION, false, false)

-- Страшный звук
context.world:play_sound(
"ENTITY_GHAST_SCREAM",
scare_loc.x, scare_loc.y, scare_loc.z,
1.0, 0.7
)

-- Всплеск дымовых частиц
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
)

-- Устанавливаем кулдаун, чтобы не срабатывал снова сразу
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end,
nil -- обратный вызов on_leave не нужен
)
end,

on_left_click = function(context)
-- Делаем неуязвимым
if context.event then
context.event.cancel()
end
end,

on_destroy = function(context)
-- Очищаем наблюдение за зоной
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() регистрирует обратный вызов on-enter, который срабатывает, когда любой игрок входит внутрь.

  2. Эффекты испуга — Обратный вызов on-enter воспроизводит анимацию "jumpscare", крик гаста и создаёт дымовые частицы костра. Комбинация создаёт внезапный, пугающий эффект.

  3. 60-секундный кулдаун — Паттерн с булевым флагом предотвращает повторное срабатывание. После срабатывания пропс замолкает на 60 секунд (COOLDOWN_TICKS = 1200), затем перевзводится.

  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 секунд между спавнами

return {
api_version = 1,

on_spawn = function(context)
context.state.on_cooldown = false
end,

on_left_click = function(context)
-- Делаем неуязвимым
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

-- Предотвращаем спам спавна
if context.state.on_cooldown then
player:send_message("&7Спавнер перезаряжается...")
return
end

local loc = context.prop.current_location
if loc == nil then return end

-- Пытаемся заспавнить босса EliteMobs
local boss = context.prop:spawn_elitemobs_boss(BOSS_FILE, loc.x, loc.y + 1, loc.z)

if boss then
-- Успех — воспроизводим эффекты спавна
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("&cВоин-гоблин появляется!")
else
-- EliteMobs не установлен или файл босса не найден
context.log:warn("Could not spawn boss '" .. BOSS_FILE .. "' -- is EliteMobs installed?")
player:send_message("&7Спавнер шипит... (EliteMobs недоступен)")

-- Частицы неудачи как визуальная обратная связь
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

-- Устанавливаем кулдаун
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. Корректный запасной вариантspawn_elitemobs_boss() возвращает nil, если EliteMobs не установлен или файл босса не существует. Скрипт обрабатывает это предупреждением в логе, эффектом частиц неудачи и сообщением игроку, объясняющим проблему.

  3. Смещение спавна — Босс спавнится на loc.y + 1 (на один блок выше пропса), чтобы предотвратить вклинивание босса в пропс или землю.

  4. Кулдаун — 10-секундный кулдаун предотвращает заполнение области воинами-гоблинами. Настройте COOLDOWN_TICKS под ваши игровые потребности.

  5. Визуальное/звуковое различие — Успех использует частицы огня и звук призыва Вызывателя для драматичного спавна. Неудача использует дым и звук тушения огня для чёткого эффекта «шипения», чтобы игрок понял, что что-то пошло не так, не проверяя консоль.

Интеграция с EliteMobs

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


Лучшие практики

  • Начните с маленького хука и проверьте. Напишите один on_spawn, который отправляет сообщение в лог. Подтвердите, что он срабатывает. Затем стройте дальше.

  • Держите вспомогательные функции локальными. Объявляйте помощников вроде local function toggle_door(context) над возвращаемой таблицей. Это держит их вне глобальной области видимости.

  • Инициализируйте всё состояние в on_spawn. Если вы читаете context.state.is_open в on_right_click, но никогда не устанавливаете его в on_spawn, оно будет nil, и ваши сравнения могут вести себя неожиданно.

  • Отменяйте повторяющиеся задачи, когда закончите. Каждый run_repeating должен иметь соответствующий cancel в on_destroy. Утечённые задачи тратят CPU.

  • Используйте свежий context обратных вызовов планировщика. Обратные вызовы планировщика получают свежий параметр context. Всегда используйте этот параметр внутри обратного вызова, а не внешний context.

  • Держите on_game_tick лёгким. Если вы определяете этот хук, он запускается каждый серверный тик (20 раз в секунду). Защищайте ресурсоёмкую работу проверкой кулдауна на основе состояния.

  • Делайте пропсы неуязвимыми по умолчанию. Если вы не хотите, чтобы пропс можно было сломать, включайте отмену урона в on_left_click в каждый скрипт.

  • Используйте ВЕРХНИЙ_РЕГИСТР для перечислений Bukkit. Имена звуков и частиц должны использовать формат констант перечислений Bukkit (например, "FLAME", а не "flame").


Типичные ошибки начинающих

  • Использование внешнего context внутри обратного вызова планировщика. Внешний context захватывает снимок на момент выполнения хука. Внутри обратных вызовов всегда используйте собственный параметр обратного вызова.

  • Забытая отмена повторяющихся задач. Если вы запустили run_repeating в on_spawn, но никогда не отменили, задача работает до удаления пропса.

  • Нет инициализации состояния в on_spawn. Чтение context.state.x до его установки возвращает nil, что может молча сломать вашу логику.

  • Неправильные имена анимаций. Если play_animation("open") возвращает false, имя анимации не совпадает с определённым в файле модели. Проверьте модель на точные имена.

  • Имена звуков/частиц в нижнем регистре. "flame" не работает — используйте "FLAME". API внутренне конвертирует в ВЕРХНИЙ_РЕГИСТР для частиц, но имена перечислений Sound должны быть точными.

  • Забытый api_version = 1. Возвращаемая таблица должна включать это поле, или FMM не загрузит скрипт.

  • Размещение функций внутри возвращаемой таблицы, которые не являются хуками. Вспомогательные функции должны быть объявлены над оператором return. Только имена хуков (on_spawn, on_right_click и т.д.) допускаются как ключи в возвращаемой таблице.


Чеклист контроля качества

Используйте этот чеклист для проверки скрипта пропса перед развёртыванием:

  1. Файл возвращает ровно одну таблицу с api_version = 1.
  2. Каждое имя хука точно совпадает с записью в списке хуков.
  3. context.event проверяется с помощью if context.event then перед вызовом cancel().
  4. Поля context.state инициализированы в on_spawn.
  5. Каждый вызов scheduler:run_repeating(...) имеет соответствующий scheduler:cancel(...) в on_destroy.
  6. Обратные вызовы планировщика используют собственный параметр context обратного вызова, а не внешний context.
  7. Хуки on_game_tick защищают ресурсоёмкую работу проверкой.
  8. Все имена методов существуют в справочнике Prop API — без придуманных псевдонимов.
  9. Имена звуков и частиц используют ВЕРХНИЙ_РЕГИСТР перечислений Bukkit.
  10. Скрипт не вызывает блокирующих или длительных операций внутри хука или обратного вызова.

Советы по генерации с помощью ИИ

Если вы хотите, чтобы ИИ надёжно генерировал скрипты пропсов, убедитесь, что запрос включает:

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

Хороший пример запроса

Напиши скрипт пропса FMM, который воспроизводит анимацию "activate" по правому клику, делает пропс неуязвимым, создаёт частицы FLAME в местоположении пропса при клике, воспроизводит звук BLOCK_LEVER_CLICK и имеет 2-секундный кулдаун между кликами, используя context.state и scheduler:run_later.


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