Lua-скрипты: примеры и приёмы
Здесь приведены примеры скриптов пропсов FreeMinecraftModels с пояснениями. Подготовьте модели и анимации, на которые ссылаются примеры, и проверьте их на сервере перед использованием в карте.
Если вы только начинаете, прочитайте руководство по началу работы. Описание методов находится в API пропсов.
Пример: неуязвимый пропс
Чему учит: Минимальный скрипт для отмены стандартного урона от левого клика.
Сохраните пример как invulnerable.lua в папке скриптов и перезагрузите скрипты перед привязкой к пропсу.
Полный файл скрипта (нажмите, чтобы развернуть)
return {
api_version = 1,
on_left_click = function(context)
if context.event then
context.event.cancel()
end
end
}
Разбор
on_left_clickсрабатывает при ударе игрока по пропсу. Lua-событие содержит флаг отмены стандартной обработки урона FMM.- В этом хуке должен присутствовать
context.event, но проверять его наличие полезно. context.event.cancel()предотвращает стандартный урон от этого клика. Это не общая неуязвимость ко всем источникам урона.
Использование
Добавьте в конфигурацию .yml пропса:
isEnabled: true
scripts:
- invulnerable.lua
Пример: интерактивная дверь
Чему учит: Переключение по правому клику, управление анимациями и хранение состояния двери в context.state.
Полный файл скрипта (нажмите, чтобы развернуть)
local OPEN_ANIMATION = "open"
local CLOSE_ANIMATION = "close"
return {
api_version = 1,
on_spawn = function(context)
context.state.is_open = false
end,
on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
local loc = context.prop.current_location
if context.state.is_open then
-- Close the door
context.prop:stop_animation()
context.prop:play_animation(CLOSE_ANIMATION, true, false)
context.state.is_open = false
if loc then
context.world:play_sound(
"BLOCK_WOODEN_DOOR_CLOSE",
loc.x, loc.y, loc.z,
1.0, 1.0
)
end
else
-- Open the door
context.prop:stop_animation()
context.prop:play_animation(OPEN_ANIMATION, true, false)
context.state.is_open = true
if loc then
context.world:play_sound(
"BLOCK_WOODEN_DOOR_OPEN",
loc.x, loc.y, loc.z,
1.0, 1.0
)
end
end
end
}
Разбор
- Константы
OPEN_ANIMATIONиCLOSE_ANIMATIONнаходятся над возвращаемой таблицей, чтобы их было удобно менять под имена анимаций модели. on_spawnзадаётcontext.state.is_open = false. Состояние сохраняется между хуками одной привязки скрипта к пропсу. Другие скрипты того же пропса имеют отдельное состояние.on_left_clickотменяет стандартный урон от этого клика.on_right_clickзапрашивает остановку и нужную анимацию, переключает флаг и проигрывает звук. Флаг отражает запрос, а не подтверждённое воспроизведение.blend = trueставит анимацию в очередь. Остановка не прерывает активную анимациюdeathобщего движка. См. анимации. Пример не меняет коллизию барьеров и не гарантирует проходимость дверного проёма.- Звуки принимают имена 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
}
Разбор
ZONE_RADIUSиPARTICLE_INTERVALвынесены на уровень файла для настройки.on_spawnсначала присваивает полям состоянияnilили0.context.zones:create_sphere()создаёт неподвижную сферу в позиции пропса при спавне. Числовой handle ссылается на эту зону. Кольцо частиц читает текущую позицию пропса, поэтому используйте неподвижный пропс либо обновляйте зону при перемещении.context.zones:watch(handle, function() end, function() end)включаетon_zone_enterиon_zone_leave. Они меняют счётчик вcontext.stateи получают пересекающего границу игрока черезcontext.player/context.event.player, если нужна логика для конкретного игрока.- Задача создаёт кольцо частиц каждые 10 серверных тиков, то есть раз в полсекунды при 20 TPS. Тип частиц зависит от наличия игроков в зоне.
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
}
Разбор
context.cooldowns:check_local("sound", COOLDOWN_TICKS)проверяет готовность и сразу запускает кулдаун. Его разделяют все игроки, использующие эту привязку скрипта к пропсу. Тик кулдауна равен 50 мс прошедшего времени; задержки планировщика считают реальные серверные тики.- Звуки принимают имена enum Bukkit без учёта регистра, допустимые ключи вроде
minecraft:block.note_block.harpи ключи собственных ресурспаков. Клиенту нужен соответствующий звук в ресурспаке. - Вместе со звуком над пропсом появляются частицы нот.
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
}
Разбор
on_spawnсразу запускает"idle". Значение blendfalseпереключает анимацию немедленно. Встроенное состояниеidleповторяется независимо от переданного аргумента loop.- Повторяющаяся задача создаёт над пропсом частицы стола зачарований каждые 2 секунды для фонового эффекта.
on_destroyотменяет задачу частиц.
Пример: стул для сидения
Чему учит: Посадка правым кликом, высадка левым и получение игрока через context.event.player.
Полный файл скрипта (нажмите, чтобы развернуть)
return {
api_version = 1,
on_right_click = function(context)
-- Handle seating here instead of also using the default mount fallback
if context.event then
context.event.cancel()
end
local player = context.event and context.event.player
if not player then return end
-- Check if the player is already sitting on this prop
local passengers = context.prop:get_passengers()
for i = 1, #passengers do
if passengers[i].uuid == player.uuid then
-- Player is already seated, do nothing on right-click
return
end
end
-- Mount the player on the chair
context.prop:mount(player)
local loc = context.prop.current_location
if loc then
context.world:play_sound("BLOCK_WOOD_PLACE", loc.x, loc.y, loc.z, 0.8, 1.2)
end
end,
on_left_click = function(context)
-- Cancel default left-click damage
if context.event then
context.event.cancel()
end
local player = context.event and context.event.player
if not player then return end
-- Check if the player is sitting and dismount them
local passengers = context.prop:get_passengers()
for i = 1, #passengers do
if passengers[i].uuid == player.uuid then
context.prop:dismount(player)
local loc = context.prop.current_location
if loc then
context.world:play_sound("BLOCK_WOOD_BREAK", loc.x, loc.y, loc.z, 0.8, 1.0)
end
return
end
end
end
}
Разбор
on_right_clickполучаетcontext.event.player, проверяет, не сидит ли игрок уже, и черезcontext.prop:mount(player)запрашивает первое свободное место в точках крепленияm_.on_left_clickотменяет стандартный урон этого клика и, если ударивший игрок сидит, вызываетcontext.prop:dismount(player).context.prop:get_passengers()возвращает массив таблиц сущностей. UUID позволяет найти нужного игрока.- Звуки сопровождают поставленные в очередь запросы посадки и высадки, а не подтверждают их выполнение. Хук правого клика отменяет стандартную посадку FMM, чтобы избежать второй попытки. Модели нужна свободная точка
m_.
Пример: святилище благословения
Чему учит: Проверка и расход предмета в руке, случайные эффекты зелий, кулдаун, частицы и звуки.
Полный файл скрипта (нажмите, чтобы развернуть)
local COOLDOWN_TICKS = 600 -- 30 seconds between uses
local BLESSINGS = {
{ effect = "speed", name = "Swiftness" },
{ effect = "strength", name = "Strength" },
{ effect = "regeneration", name = "Regeneration" },
{ effect = "resistance", name = "Resistance" },
{ effect = "jump_boost", name = "Leap" },
{ effect = "haste", name = "Haste" },
}
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end
-- Check cooldown
if context.state.on_cooldown then
player:send_message("&eThe shrine is recharging... Please wait.")
return
end
-- Check if the player is holding a gold ingot
local held = player:get_held_item()
if not held or held.type ~= "GOLD_INGOT" then
player:send_message("&eThe shrine demands a gold offering...")
return
end
-- Consume one gold ingot
player:consume_held_item(1)
-- Play blessing effects
local loc = context.prop.current_location
if loc then
context.world:spawn_particle("ENCHANT", loc.x, loc.y + 1.5, loc.z, 30, 0.5, 0.5, 0.5, 0.5)
context.world:spawn_particle("HAPPY_VILLAGER", loc.x, loc.y + 1, loc.z, 10, 0.3, 0.3, 0.3, 0)
context.world:play_sound("BLOCK_BEACON_ACTIVATE", loc.x, loc.y, loc.z, 1.0, 1.5)
end
-- Apply a random blessing
local chosen = BLESSINGS[math.random(#BLESSINGS)]
player:add_potion_effect(chosen.effect, 600, 1) -- 30 seconds, level II
player:send_message("&aThe shrine blesses you with " .. chosen.name .. "!")
-- Set cooldown
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}
Разбор
player:get_held_item()возвращаетtype,amount,display_nameпредмета в основной руке либоnilдля пустой руки.held.typeсодержит имя материала Bukkit в верхнем регистре, поэтому сравнивается с"GOLD_INGOT".player:consume_held_item(1)ставит удаление из основной руки в очередь. Рука читается при выполнении задачи; результат оплаты не возвращается. Пример не выполняет атомарный обмен: смена предмета может изменить то, что будет потрачено, а благословение применяется сразу.- Таблица
BLESSINGSна уровне файла перечисляет положительные эффекты.math.random(#BLESSINGS)выбирает один, аplayer:add_potion_effect(effect, duration, amplifier)накладывает его.600тиков соответствуют 30 секундам, amplifier1задаёт уровень II. - Флаг общий для всех игроков этой привязки скрипта. Он сбрасывается через 600 серверных тиков, то есть 30 секунд при 20 TPS. Для кулдауна по прошедшему времени используйте
context.cooldowns, как в звуковом примере. - Частицы зачарований и счастливого жителя со звуком активации маяка показывают благословение.
Пример: проклятое святилище
Чему учит: Отрицательные эффекты зелий, молния, спавн сущностей и выбор действий по подношению.
Полный файл скрипта (нажмите, чтобы развернуть)
local COOLDOWN_TICKS = 600 -- 30 seconds between uses
local BUFFS = {
{ effect = "speed", name = "Swiftness" },
{ effect = "strength", name = "Strength" },
{ effect = "regeneration", name = "Regeneration" },
{ effect = "resistance", name = "Resistance" },
}
local CURSES = {
{ effect = "slowness", name = "Slowness" },
{ effect = "weakness", name = "Weakness" },
{ effect = "poison", name = "Poison" },
{ effect = "mining_fatigue", name = "Mining Fatigue" },
}
local ZOMBIE_COUNT = 4
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end
-- Check cooldown
if context.state.on_cooldown then
player:send_message("&7The dark shrine pulses with residual energy...")
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Check if the player is holding a gold ingot
local held = player:get_held_item()
if not held or held.type ~= "GOLD_INGOT" then
-- No offering -- punish the player!
player:send_message("&4The shrine demands tribute! You dare approach empty-handed?!")
-- Strike lightning on the player
local player_loc = player.current_location
if player_loc then
context.world:strike_lightning(player_loc.x, player_loc.y, player_loc.z)
end
-- Spawn a horde of zombies around the shrine
for i = 1, ZOMBIE_COUNT do
local angle = math.rad((360 / ZOMBIE_COUNT) * i)
local spawn_x = loc.x + math.cos(angle) * 3
local spawn_z = loc.z + math.sin(angle) * 3
context.world:spawn_entity("zombie", spawn_x, loc.y, spawn_z)
end
-- Apply a random curse
local chosen_curse = CURSES[math.random(#CURSES)]
player:add_potion_effect(chosen_curse.effect, 400, 1) -- 20 seconds, level II
player:send_message("&cThe shrine curses you with " .. chosen_curse.name .. "!")
-- Ominous effects
context.world:spawn_particle("SMOKE", loc.x, loc.y + 1, loc.z, 30, 0.5, 0.5, 0.5, 0.05)
context.world:play_sound("ENTITY_WITHER_AMBIENT", loc.x, loc.y, loc.z, 1.0, 0.5)
else
-- Gold offered -- reward the player
player:consume_held_item(1)
-- Apply a random buff
local chosen_buff = BUFFS[math.random(#BUFFS)]
player:add_potion_effect(chosen_buff.effect, 600, 1) -- 30 seconds, level II
player:send_message("&aThe dark shrine accepts your offering. You are blessed with " .. chosen_buff.name .. "!")
-- Positive feedback
context.world:spawn_particle("ENCHANT", loc.x, loc.y + 1.5, loc.z, 30, 0.5, 0.5, 0.5, 0.5)
context.world:play_sound("BLOCK_BEACON_ACTIVATE", loc.x, loc.y, loc.z, 1.0, 0.8)
end
-- Set cooldown regardless of path
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}
Разбор
- Скрипт проверяет
player:get_held_item(): отсутствие золота ведёт к наказанию, наличие к награде. 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)накладывает Отравление II на 20 секунд. Имена эффектов соответствуютPotionEffectTypeBukkit в нижнем регистре.- Ветка награды ставит расход предмета в основной руке в очередь и накладывает случайный положительный эффект. Как и святилище благословения, это демонстрация, а не атомарная операция оплаты и выдачи награды.
- Обе ветки задают общий флаг для всех игроков этой привязки скрипта. Сброс происходит через 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
}
Разбор
context.state.is_spinningпредотвращает повторные запросы вращения. Флаг задаётся при запуске и сбрасывается при запланированной остановке.play_animation(SPIN_ANIMATION, true, false)запрашивает нециклическую анимацию через очередь. Остановка выполняется через 100 серверных тиков после клика, а не после начала воспроизведения. Занятая очередь может сократить видимое вращение или не дать ему начаться. Общий движок не останавливает активную анимациюdeath. Пример задаёт флаг даже при результатеfalseот запроса анимации.BLOCK_CHAIN_PLACEдаёт механический звук запуска, аBLOCK_CHAIN_FALLзвук остановки. Высоту тона можно настроить.- Коллбэк
run_laterобращается к состоянию и миру черезlater_context, а не внешнийcontext.
Имя "spin" должно совпадать с моделью. Если она использует "rotate" или "turn", измените SPIN_ANIMATION.
Пример: пропс-джампскейр
Чему учит: Срабатывание при входе в зону, долгий кулдаун и сочетание анимации, звука и частиц.
Полный файл скрипта (нажмите, чтобы развернуть)
local SCARE_RADIUS = 3
local COOLDOWN_TICKS = 1200 -- 60 seconds between scares
local SCARE_ANIMATION = "jumpscare"
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
context.state.zone_handle = nil
local loc = context.prop.current_location
if loc == nil then return end
-- Create a small sphere zone around the prop
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, SCARE_RADIUS)
context.state.zone_handle = handle
-- Watch for players entering the zone. The actual scare logic
-- lives in on_zone_enter below.
context.zones:watch(handle, function() end, nil)
end,
on_zone_enter = function(context)
if context.state.on_cooldown then
return
end
local scare_loc = context.prop.current_location
if scare_loc == nil then return end
-- Play the jumpscare animation
context.prop:stop_animation()
context.prop:play_animation(SCARE_ANIMATION, false, false)
-- Scary sound
context.world:play_sound(
"ENTITY_GHAST_SCREAM",
scare_loc.x, scare_loc.y, scare_loc.z,
1.0, 0.7
)
-- Burst of smoke particles
context.world:spawn_particle(
"CAMPFIRE_SIGNAL_SMOKE",
scare_loc.x, scare_loc.y + 1, scare_loc.z,
20, 0.5, 0.5, 0.5, 0.05
)
-- Set cooldown so it does not trigger again immediately
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end,
on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,
on_destroy = function(context)
-- Clean up the zone watch
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}
Разбор
context.zones:create_sphere()создаёт неподвижную зону радиусом 3 блока в позиции спавна.context.zones:watch(handle, function() end, nil)включаетon_zone_enter. Перемещая пропс, перемещайте или пересоздавайте и зону.on_zone_enterпроигрывает"jumpscare", крик гаста и создаёт дым от костра для внезапного пугающего эффекта.- Все игроки разделяют флаг этой привязки скрипта. Он сбрасывается через 1200 серверных тиков, то есть 60 секунд при 20 TPS. После этого следующий вход в зону может снова запустить эффект.
- Третий аргумент
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 seconds between spawns
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end
-- Prevent spawn spam
if context.state.on_cooldown then
player:send_message("&7The spawner is recharging...")
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Try to spawn the EliteMobs boss
local boss = context.prop:spawn_elitemobs_boss(BOSS_FILE, loc.x, loc.y + 1, loc.z)
if boss then
-- Success -- play spawn effects
context.world:spawn_particle("FLAME", loc.x, loc.y + 1, loc.z, 20, 0.5, 0.5, 0.5, 0.05)
context.world:play_sound("ENTITY_EVOKER_PREPARE_SUMMON", loc.x, loc.y, loc.z, 1.0, 1.0)
player:send_message("&cA goblin warrior emerges!")
else
-- EliteMobs is not installed or the boss file was not found
context.log:warn("Could not spawn boss '" .. BOSS_FILE .. "' -- is EliteMobs installed?")
player:send_message("&7The spawner fizzles... (EliteMobs not available)")
-- Fizzle particles as visual feedback
context.world:spawn_particle("SMOKE", loc.x, loc.y + 1, loc.z, 10, 0.3, 0.3, 0.3, 0.02)
context.world:play_sound("BLOCK_FIRE_EXTINGUISH", loc.x, loc.y, loc.z, 0.8, 1.2)
end
-- Set cooldown
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}
Разбор
context.prop:spawn_elitemobs_boss(filename, x, y, z)создаёт босса EliteMobs в указанных координатах. Имя должно соответствовать.ymlв папкеcustombossesEliteMobs.- Метод возвращает
nil, если интеграция не может вернуть живого босса: например, плагин недоступен, конфигурация отсутствует или спавн завершился неудачей. Сообщение примера предлагает проверить EliteMobs, но не определяет точную причину. - Запрошенная позиция находится на один блок выше пропса. Это не проверка ландшафта или коллизии. Выберите свободное место, подходящее боссу.
- Привязка скрипта разрешает одну попытку каждые 200 серверных тиков, то есть 10 секунд при 20 TPS, общую для всех игроков. Ограничения числа живых боссов нет, и исчезновение пропса не удаляет созданных боссов. Для повторяемого игрового события добавьте необходимые ограничения.
- При успехе используются пламя и звук призыва заклинателя; при неудаче дым и тушение огня. Игрок видит результат, не открывая консоль.
Имя вроде "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его не меняет.
Проверка качества
Перед использованием скрипта проверьте следующее:
- Файл возвращает ровно одну таблицу с
api_version = 1. - Имена хуков точно соответствуют списку хуков пропса. Хуки зачарований относятся к отдельной среде выполнения.
- Вне известного хука клика перед отменой проверены и
context.event, иcontext.event.cancel. - Поля
context.stateинициализируются вon_spawn. - Каждому
scheduler:run_repeating(...)соответствуетscheduler:cancel(...)вon_destroy. - Коллбэки используют свой параметр контекста, а не внешний
context. - Затратная работа в
on_game_tickограничена проверкой. - Все методы существуют в API пропсов, выдуманных псевдонимов нет.
- Частицы принимают
FLAMEиflame: API нормализует регистр перед поиском частицы Bukkit. Звуки принимают имена enum Bukkit без учёта регистра, допустимые ключи вродеminecraft:block.note_block.harpи ключи собственных ресурспаков. Звук должен быть в ресурспаке клиента. - В хуках и коллбэках нет блокирующих или длительных операций.
Советы по генерации через ИИ
Укажите в запросе:
- Точное имя хука, например
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: больше не активирует скрипт предмета.