Lua-скриптинг: Примеры и паттерны
Эта страница содержит полные рабочие примеры скриптов пропсов FreeMinecraftModels, а также практические паттерны и лучшие практики. Каждый пример включает разбор, объясняющий что он делает и почему.
Если вы новичок в скриптинге пропсов, начните с Начало работы. Полные детали API см. в Prop API.
Пример: Неуязвимый пропс
Чему учит: Простейший полезный скрипт — отмена урона, чтобы пропс нельзя было сломать.
Это готовый скрипт, поставляемый с FreeMinecraftModels.
Полный файл скрипта (нажмите, чтобы развернуть)
return {
api_version = 1,
on_left_click = function(context)
if context.event then
context.event.cancel()
end
end
}
Разбор
-
Выбор хука —
on_left_clickсрабатывает, когда игрок бьёт (левый клик) по пропсу. Под капотом этоEntityDamageByEntityEventна стойке для брони, лежащей в основе пропса. -
Проверка события —
context.eventвсегда должен присутствовать в этом хуке, но проверка — хорошая практика. -
Отмена —
context.event.cancel()отменяет событие урона, что предотвращает повреждение и уничтожение стойки для брони.
Использование
Добавьте в конфиг .yml вашего пропса:
isEnabled: true
scripts:
- invulnerable.lua
Пример: Интерактивная дверь
Чему учит: Переключение состояния по правому клику, воспроизведение и остановка анимаций, использование context.state для отслеживания открытия/закрытия двери.
Полный файл скрипта (нажмите, чтобы развернуть)
local OPEN_ANIMATION = "open"
local CLOSE_ANIMATION = "close"
return {
api_version = 1,
on_spawn = function(context)
context.state.is_open = false
end,
on_left_click = function(context)
-- Делаем дверь неуязвимой
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
}
Разбор
-
Константы на уровне файла —
OPEN_ANIMATIONиCLOSE_ANIMATIONопределены над возвращаемой таблицей. Это упрощает их изменение для разных файлов моделей, которые могут использовать другие имена анимаций. -
Инициализация состояния —
on_spawnустанавливаетcontext.state.is_open = false. Состояние сохраняется между всеми хуками для данного экземпляра пропса. -
Неуязвимость — Хук
on_left_clickотменяет урон, чтобы дверь нельзя было случайно сломать. -
Логика переключения —
on_right_clickпроверяетcontext.state.is_open, останавливает текущую анимацию, воспроизводит соответствующую анимацию, переключает состояние и воспроизводит звук. Вызовstop_animation()передplay_animation()обеспечивает чистые переходы. -
Звуковая обратная связь —
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
}
Разбор
-
Константы —
ZONE_RADIUSиPARTICLE_INTERVALна уровне файла для удобной настройки. -
Инициализация состояния —
on_spawnустанавливает все поля состояния вnil/0перед началом работы. -
Создание зоны —
context.zones:create_sphere()создаёт сферическую зону с центром в пропсе. Возвращённый идентификатор — числовой ID для ссылки на эту зону позже. -
Наблюдение за зоной —
context.zones:watch()регистрирует обратные вызовы для входа и выхода игрока. Обратные вызовы увеличивают и уменьшают счётчик, хранящийся вcontext.state. -
Цикл частиц — Повторяющаяся задача создаёт частицы кольцом вокруг пропса каждые полсекунды. Тип частиц меняется в зависимости от наличия игроков в зоне.
-
Очистка —
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
}
Разбор
-
Паттерн кулдауна — Поскольку скрипты пропсов FMM не имеют встроенного API
context.cooldowns, как EliteMobs, пример реализует простой кулдаун с помощьюcontext.state.on_cooldownиscheduler:run_later(). Флаг устанавливается вtrueпри воспроизведении звука, и отложенная задача сбрасывает его черезCOOLDOWN_TICKS. -
Воспроизведение звука —
context.world:play_sound()принимает имя перечисления Bukkit Sound в ВЕРХНЕМ_РЕГИСТРЕ, координаты, громкость и высоту тона. -
Визуальная обратная связь — Частицы нот появляются над пропсом при воспроизведении звука, давая визуальный сигнал.
-
Неуязвимость — Хук
on_left_clickотменяет урон как обычно. -
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
}
Разбор
-
Автозапуск анимации —
on_spawnнемедленно запускает зацикленную анимацию"idle".falseдля blend означает запуск с нуля без перехода от предыдущей анимации.trueдля loop означает бесконечное повторение. -
Фоновые частицы — Повторяющаяся задача создаёт частицы стола зачарования над пропсом каждые 2 секунды, создавая магический фоновый эффект.
-
Очистка —
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
}
Разбор
-
Правый клик для посадки —
on_right_clickполучает игрока изcontext.event.player, проверяет, является ли он уже пассажиром (чтобы избежать двойной посадки), и вызываетcontext.prop:mount(player)для посадки на стойку для брони пропса. -
Левый клик для вставания —
on_left_clickотменяет событие урона (неуязвимость), затем проверяет, является ли бьющий игрок пассажиром. Если да,context.prop:dismount(player)высаживает его. -
Проверка пассажиров —
context.prop:get_passengers()возвращает массив таблиц сущностей. Мы сравниваем UUID, чтобы найти взаимодействующего игрока в списке. -
Звуковая обратная связь — Звук размещения дерева при посадке и звук разрушения дерева при вставании дают тактильную обратную связь.
Пример: Святилище благословения
Чему учит: Проверка предмета в руке игрока, потребление предметов, применение случайных эффектов зелий, управление кулдауном, обратная связь частицами и звуком.
Полный файл скрипта (нажмите, чтобы развернуть)
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
}
Разбор
-
Проверка предмета —
player:get_held_item()возвращает таблицу сtype,amountиdisplay_nameдля предмета в основной руке (илиnil, если рука пуста). Мы сравниваемheld.typeс"gold_ingot"(имя материала в нижнем регистре). -
Потребление предмета —
player:consume_held_item(1)удаляет один предмет из стака в основной руке игрока. -
Случайный бафф — Таблица
BLESSINGSна уровне файла перечисляет доступные положительные эффекты.math.random(#BLESSINGS)выбирает один случайным образом.player:add_potion_effect(effect, duration, amplifier)применяет его —600тиков — это 30 секунд, усилитель1— это уровень II. -
Кулдаун — Тот же паттерн «булев флаг плюс планировщик» из примера звукоизлучающего пропса. 30-секундный кулдаун предотвращает спам святилища.
-
Обратная связь — Частицы зачарования и 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
}
Разбор
-
Ветвление по подношению — Скрипт проверяет
player:get_held_item()и идёт по одному из двух путей: наказание, если у игрока нет золота, или награда, если есть. -
Удар молнии —
context.world:strike_lightning(x, y, z)бьёт настоящей (наносящей урон) молнией в позицию игрока. Позиция игрока считывается изplayer.current_location. -
Спавн зомби —
context.world:spawn_entity("zombie", x, y, z)спавнит ванильных зомби. Цикл распределяет их равномерно по кругу вокруг святилища с помощью тригонометрии. -
Негативные эффекты зелий —
player:add_potion_effect("poison", 400, 1)применяет 20 секунд Яда II. Имена эффектов — строки в нижнем регистре, соответствующие именамPotionEffectTypeBukkit. -
Путь награды — Когда золото предложено, святилище потребляет один слиток и применяет случайный положительный эффект, зеркалируя поведение святилища благословения.
-
Кулдаун — 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
}
Разбор
-
Защита состоянием —
context.state.is_spinningпредотвращает множественные перекрывающиеся запросы вращения. Флаг устанавливается при начале вращения и сбрасывается при запланированной остановке. -
Анимация по таймеру —
play_animation(SPIN_ANIMATION, true, false)воспроизводит анимацию один раз (без зацикливания). Вызовscheduler:run_later(100, ...)останавливает анимацию ровно через 5 секунд на случай, если сама анимация длиннее или зациклена. -
Механические звуки —
BLOCK_CHAIN_PLACEдаёт щёлкающий/механический звук запуска;BLOCK_CHAIN_FALLдаёт звук замедления остановки. Настройте высоту тона по вкусу. -
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
}
Разбор
-
Зона приближения —
context.zones:create_sphere()создаёт зону радиусом 3 блока.context.zones:watch()регистрирует обратный вызов on-enter, который срабатывает, когда любой игрок входит внутрь. -
Эффекты испуга — Обратный вызов on-enter воспроизводит анимацию
"jumpscare", крик гаста и создаёт дымовые частицы костра. Комбинация создаёт внезапный, пугающий эффект. -
60-секундный кулдаун — Паттерн с булевым флагом предотвращает повторное срабатывание. После срабатывания пропс замолкает на 60 секунд (
COOLDOWN_TICKS = 1200), затем перевзводится. -
Без обратного вызова выхода —
nilвторой аргументcontext.zones:watch()означает, что нам не важно, когда игроки покидают зону. -
Очистка —
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
}
Разбор
-
Спавн босса —
context.prop:spawn_elitemobs_boss(filename, x, y, z)спавнит кастомного босса EliteMobs по заданным координатам. Имя файла должно совпадать с файлом.ymlв папкеcustombossesEliteMobs. -
Корректный запасной вариант —
spawn_elitemobs_boss()возвращаетnil, если EliteMobs не установлен или файл босса не существует. Скрипт обрабатывает это предупреждением в логе, эффектом частиц неудачи и сообщением игроку, объясняющим проблему. -
Смещение спавна — Босс спавнится на
loc.y + 1(на один блок выше пропса), чтобы предотвратить вклинивание босса в пропс или землю. -
Кулдаун — 10-секундный кулдаун предотвращает заполнение области воинами-гоблинами. Настройте
COOLDOWN_TICKSпод ваши игровые потребности. -
Визуальное/звуковое различие — Успех использует частицы огня и звук призыва Вызывателя для драматичного спавна. Неудача использует дым и звук тушения огня для чёткого эффекта «шипения», чтобы игрок понял, что что-то пошло не так, не проверяя консоль.
Имя файла босса (например, "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и т.д.) допускаются как ключи в возвращаемой таблице.
Чеклист контроля качества
Используйте этот чеклист для проверки скрипта пропса перед развёртыванием:
- Файл возвращает ровно одну таблицу с
api_version = 1. - Каждое имя хука точно совпадает с записью в списке хуков.
context.eventпроверяется с помощьюif context.event thenперед вызовомcancel().- Поля
context.stateинициализированы вon_spawn. - Каждый вызов
scheduler:run_repeating(...)имеет соответствующийscheduler:cancel(...)вon_destroy. - Обратные вызовы планировщика используют собственный параметр context обратного вызова, а не внешний
context. - Хуки
on_game_tickзащищают ресурсоёмкую работу проверкой. - Все имена методов существуют в справочнике Prop API — без придуманных псевдонимов.
- Имена звуков и частиц используют ВЕРХНИЙ_РЕГИСТР перечислений Bukkit.
- Скрипт не вызывает блокирующих или длительных операций внутри хука или обратного вызова.
Советы по генерации с помощью ИИ
Если вы хотите, чтобы ИИ надёжно генерировал скрипты пропсов, убедитесь, что запрос включает:
- Точное имя хука — например,
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.
Следующие шаги
- Начало работы — структура файлов, хуки, первый скрипт, шаблоны
- Prop API — полный справочник API для всех таблиц context
- Устранение неполадок — типичные проблемы, советы по отладке