Lua-скриптинг: Примеры и паттерны
Эта страница содержит полные рабочие примеры скриптов пропсов и предметов FreeMinecraftModels, а также практические паттерны и лучшие практики. Каждый пример включает разбор, объясняющий что он делает и почему.
Если вы новичок в скриптинге, начните с Начало работы. Полные детали API см. в Prop & Item 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)
-- Make the door invulnerable
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
local loc = context.prop.current_location
if context.state.is_open then
-- Close the door
context.prop:stop_animation()
context.prop:play_animation(CLOSE_ANIMATION, true, false)
context.state.is_open = false
if loc then
context.world:play_sound(
"BLOCK_WOODEN_DOOR_CLOSE",
loc.x, loc.y, loc.z,
1.0, 1.0
)
end
else
-- Open the door
context.prop:stop_animation()
context.prop:play_animation(OPEN_ANIMATION, true, false)
context.state.is_open = true
if loc then
context.world:play_sound(
"BLOCK_WOODEN_DOOR_OPEN",
loc.x, loc.y, loc.z,
1.0, 1.0
)
end
end
end
}
Разбор
-
Константы на уровне файла —
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()проигрывает звук по имени из enum Bukkit Sound в позиции пропса. Имена звуков должны быть именами enum в ВЕРХНЕМ_РЕГИСТРЕ.
Имена анимаций вроде "open" и "close" должны совпадать с тем, что определено в файле модели. Если анимация не найдена, play_animation() возвращает false и ничего не происходит. Проверьте файл модели на точные имена анимаций.
Пример: Триггер близости с частицами
Чему учит: Создание зоны, отслеживание зоны на события входа/выхода, эффекты частиц и очистка.
Полный файл скрипта (нажмите, чтобы развернуть)
local ZONE_RADIUS = 8
local PARTICLE_INTERVAL = 10 -- ticks between particle bursts
return {
api_version = 1,
on_spawn = function(context)
context.state.zone_handle = nil
context.state.particle_task = nil
context.state.players_in_zone = 0
local loc = context.prop.current_location
if loc == nil then return end
-- Create a sphere zone around the prop
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, ZONE_RADIUS)
context.state.zone_handle = handle
-- Pass placeholder functions to enable on_zone_enter and on_zone_leave.
-- The actual logic lives in the hooks below.
context.zones:watch(handle, function() end, function() end)
-- Start a repeating particle effect at the zone boundary
context.state.particle_task = context.scheduler:run_repeating(0, PARTICLE_INTERVAL, function(tick_context)
local prop_loc = tick_context.prop.current_location
if prop_loc == nil then return end
-- Spawn particles in a ring at the zone boundary
for angle = 0, 350, 30 do
local rad = math.rad(angle)
local px = prop_loc.x + math.cos(rad) * ZONE_RADIUS
local pz = prop_loc.z + math.sin(rad) * ZONE_RADIUS
if (tick_context.state.players_in_zone or 0) > 0 then
-- Flame particles when players are inside
tick_context.world:spawn_particle("FLAME", px, prop_loc.y + 0.5, pz, 1, 0, 0, 0, 0)
else
-- Green particles when zone is empty
tick_context.world:spawn_particle("HAPPY_VILLAGER", px, prop_loc.y + 0.5, pz, 1, 0, 0, 0, 0)
end
end
end)
end,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_zone_enter = function(context)
context.state.players_in_zone = (context.state.players_in_zone or 0) + 1
end,
on_zone_leave = function(context)
context.state.players_in_zone = math.max(0, (context.state.players_in_zone or 0) - 1)
end,
on_destroy = function(context)
-- Clean up the repeating task
if context.state.particle_task then
context.scheduler:cancel(context.state.particle_task)
context.state.particle_task = nil
end
-- Clean up the zone watch
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}
Разбор
-
Константы —
ZONE_RADIUSиPARTICLE_INTERVALна уровне файла для удобной настройки. -
Инициализация состояния —
on_spawnзадаёт всем полям состоянияnil/0, прежде чем делать что-либо ещё. -
Создание зоны —
context.zones:create_sphere()создаёт сферическую зону с центром на пропсе. Возвращаемый handle — это числовой ID, используемый для ссылки на эту зону позже. -
Отслеживание зоны —
context.zones:watch(handle, function() end, function() end)включает хукиon_zone_enterиon_zone_leave. Эти хуки увеличивают и уменьшают счётчик, хранящийся вcontext.state; они также получают пересекающего границу игрока какcontext.player/context.event.player, если вам нужно поведение, зависящее от игрока. -
Цикл частиц — повторяющаяся задача спавнит частицы кольцом вокруг пропса каждые полсекунды. Тип частиц меняется в зависимости от того, есть ли игроки в зоне.
-
Очистка —
on_destroyотменяет повторяющуюся задачу и снимает отслеживание зоны. Хотя обе очищаются автоматически при удалении пропса, явная очистка — это лучшая практика.
Спавн множества частиц каждый тик может влиять на производительность. Используйте разумный интервал (10–20 тиков) и держите количество частиц низким. Пример выше использует PARTICLE_INTERVAL = 10 (дважды в секунду) всего с 12 частицами на кольцо.
Пример: Пропс, издающий звук
Чему учит: Воспроизведение звуков при взаимодействии, использование context.cooldowns и предотвращение слишком частых взаимодействий.
Полный файл скрипта (нажмите, чтобы развернуть)
local SOUND_NAME = "BLOCK_NOTE_BLOCK_HARP"
local COOLDOWN_TICKS = 40 -- 2 seconds between sounds
return {
api_version = 1,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
-- Prevent spam
if not context.cooldowns:check_local("sound", COOLDOWN_TICKS) then
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Play the sound
context.world:play_sound(SOUND_NAME, loc.x, loc.y, loc.z, 1.0, 1.0)
-- Show some particles
context.world:spawn_particle("NOTE", loc.x, loc.y + 1.5, loc.z, 5, 0.3, 0.3, 0.3, 0)
end
}
Разбор
-
Паттерн кулдауна —
context.cooldowns:check_local("sound", COOLDOWN_TICKS)проверяет, готов ли кулдаун"sound", и, если готов, тут же его запускает. Это сохраняет обработчик клика маленьким и избегает создания задач планировщика лишь ради сброса флага. -
Воспроизведение звука —
context.world:play_sound()принимает имя enum Bukkit Sound в ВЕРХНЕМ_РЕГИСТРЕ, координаты, громкость и высоту тона. -
Отклик частицами — частицы NOTE появляются над пропсом, когда проигрывается звук, давая визуальную подсказку.
-
Неуязвимость — хук
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".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
-- Check if the player is already sitting on this prop
local passengers = context.prop:get_passengers()
for i = 1, #passengers do
if passengers[i].uuid == player.uuid then
-- Player is already seated, do nothing on right-click
return
end
end
-- Mount the player on the chair
context.prop:mount(player)
local loc = context.prop.current_location
if loc then
context.world:play_sound("BLOCK_WOOD_PLACE", loc.x, loc.y, loc.z, 0.8, 1.2)
end
end,
on_left_click = function(context)
-- Cancel damage so the chair is invulnerable
if context.event then
context.event.cancel()
end
local player = context.event and context.event.player
if not player then return end
-- Check if the player is sitting and dismount them
local passengers = context.prop:get_passengers()
for i = 1, #passengers do
if passengers[i].uuid == player.uuid then
context.prop:dismount(player)
local loc = context.prop.current_location
if loc then
context.world:play_sound("BLOCK_WOOD_BREAK", loc.x, loc.y, loc.z, 0.8, 1.0)
end
return
end
end
end
}
Разбор
-
Правый клик, чтобы сесть —
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 seconds between uses
local BLESSINGS = {
{ effect = "speed", name = "Swiftness" },
{ effect = "strength", name = "Strength" },
{ effect = "regeneration", name = "Regeneration" },
{ effect = "resistance", name = "Resistance" },
{ effect = "jump_boost", name = "Leap" },
{ effect = "haste", name = "Haste" },
}
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end
-- Check cooldown
if context.state.on_cooldown then
player:send_message("&eThe shrine is recharging... Please wait.")
return
end
-- Check if the player is holding a gold ingot
local held = player:get_held_item()
if not held or held.type ~= "GOLD_INGOT" then
player:send_message("&eThe shrine demands a gold offering...")
return
end
-- Consume one gold ingot
player:consume_held_item(1)
-- Play blessing effects
local loc = context.prop.current_location
if loc then
context.world:spawn_particle("ENCHANT", loc.x, loc.y + 1.5, loc.z, 30, 0.5, 0.5, 0.5, 0.5)
context.world:spawn_particle("HAPPY_VILLAGER", loc.x, loc.y + 1, loc.z, 10, 0.3, 0.3, 0.3, 0)
context.world:play_sound("BLOCK_BEACON_ACTIVATE", loc.x, loc.y, loc.z, 1.0, 1.5)
end
-- Apply a random blessing
local chosen = BLESSINGS[math.random(#BLESSINGS)]
player:add_potion_effect(chosen.effect, 600, 1) -- 30 seconds, level II
player:send_message("&aThe shrine blesses you with " .. chosen.name .. "!")
-- Set cooldown
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}
Разбор
-
Проверка предмета —
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. -
Кулдаун — тот же паттерн «булев флаг плюс планировщик» из примера «Пропс, издающий звук». 30-секундный кулдаун предотвращает спам святилища.
-
Отклик — частицы зачарований и счастливого жителя плюс звук активации маяка создают ощущение «божественного благословения».
Пример: Проклятое святилище
Чему учит: Негативные эффекты зелий, удары молнии, спавн сущностей и ветвление логики на основе подношений игрока.
Полный файл скрипта (нажмите, чтобы развернуть)
local COOLDOWN_TICKS = 600 -- 30 seconds between uses
local BUFFS = {
{ effect = "speed", name = "Swiftness" },
{ effect = "strength", name = "Strength" },
{ effect = "regeneration", name = "Regeneration" },
{ effect = "resistance", name = "Resistance" },
}
local CURSES = {
{ effect = "slowness", name = "Slowness" },
{ effect = "weakness", name = "Weakness" },
{ effect = "poison", name = "Poison" },
{ effect = "mining_fatigue", name = "Mining Fatigue" },
}
local ZOMBIE_COUNT = 4
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end
-- Check cooldown
if context.state.on_cooldown then
player:send_message("&7The dark shrine pulses with residual energy...")
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Check if the player is holding a gold ingot
local held = player:get_held_item()
if not held or held.type ~= "GOLD_INGOT" then
-- No offering -- punish the player!
player:send_message("&4The shrine demands tribute! You dare approach empty-handed?!")
-- Strike lightning on the player
local player_loc = player.current_location
if player_loc then
context.world:strike_lightning(player_loc.x, player_loc.y, player_loc.z)
end
-- Spawn a horde of zombies around the shrine
for i = 1, ZOMBIE_COUNT do
local angle = math.rad((360 / ZOMBIE_COUNT) * i)
local spawn_x = loc.x + math.cos(angle) * 3
local spawn_z = loc.z + math.sin(angle) * 3
context.world:spawn_entity("zombie", spawn_x, loc.y, spawn_z)
end
-- Apply a random curse
local chosen_curse = CURSES[math.random(#CURSES)]
player:add_potion_effect(chosen_curse.effect, 400, 1) -- 20 seconds, level II
player:send_message("&cThe shrine curses you with " .. chosen_curse.name .. "!")
-- Ominous effects
context.world:spawn_particle("SMOKE", loc.x, loc.y + 1, loc.z, 30, 0.5, 0.5, 0.5, 0.05)
context.world:play_sound("ENTITY_WITHER_AMBIENT", loc.x, loc.y, loc.z, 1.0, 0.5)
else
-- Gold offered -- reward the player
player:consume_held_item(1)
-- Apply a random buff
local chosen_buff = BUFFS[math.random(#BUFFS)]
player:add_potion_effect(chosen_buff.effect, 600, 1) -- 30 seconds, level II
player:send_message("&aThe dark shrine accepts your offering. You are blessed with " .. chosen_buff.name .. "!")
-- Positive feedback
context.world:spawn_particle("ENCHANT", loc.x, loc.y + 1.5, loc.z, 30, 0.5, 0.5, 0.5, 0.5)
context.world:play_sound("BLOCK_BEACON_ACTIVATE", loc.x, loc.y, loc.z, 1.0, 0.8)
end
-- Set cooldown regardless of path
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}
Разбор
-
Ветвление по подношению — скрипт проверяет
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 секунд Poison II. Имена эффектов — это строки в нижнем регистре, совпадающие с именамиPotionEffectTypeBukkit. -
Путь награды — когда предлагается золото, святилище расходует один слиток и накладывает случайный положительный эффект, повторяя поведение Святилища благословения.
-
Кулдаун — 30-секундный кулдаун применяется независимо от выбранной ветки, предотвращая слишком частые наказания или награды.
Спавн нескольких сущностей за раз может влиять на производительность сервера. Держите количество низким (4–6) и подумайте о добавлении кулдауна на каждого игрока, если святилищем пользуются много игроков одновременно.
Пример: Вращающийся глобус
Чему учит: Воспроизведение анимации по таймеру при взаимодействии, планирование остановки анимации и механические звуковые эффекты.
Полный файл скрипта (нажмите, чтобы развернуть)
local SPIN_ANIMATION = "spin"
local SPIN_DURATION = 100 -- 5 seconds in ticks
return {
api_version = 1,
on_spawn = function(context)
context.state.is_spinning = false
end,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
-- Prevent starting a new spin while already spinning
if context.state.is_spinning then
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Start the spin animation (non-looping)
context.prop:play_animation(SPIN_ANIMATION, true, false)
context.state.is_spinning = true
-- Play a mechanical clicking sound
context.world:play_sound("BLOCK_CHAIN_PLACE", loc.x, loc.y, loc.z, 1.0, 1.5)
-- Schedule the animation to stop after 5 seconds
context.scheduler:run_later(SPIN_DURATION, function(later_context)
later_context.prop:stop_animation()
later_context.state.is_spinning = false
local stop_loc = later_context.prop.current_location
if stop_loc then
later_context.world:play_sound("BLOCK_CHAIN_FALL", stop_loc.x, stop_loc.y, stop_loc.z, 1.0, 0.8)
end
end)
end
}
Разбор
-
Защита состоянием —
context.state.is_spinningпредотвращает множественные накладывающиеся запросы на вращение. Флаг устанавливается при старте вращения и сбрасывается, когда срабатывает запланированная остановка. -
Анимация по таймеру —
play_animation(SPIN_ANIMATION, true, false)проигрывает анимацию один раз (без цикла). Вызовscheduler:run_later(100, ...)останавливает анимацию ровно через 5 секунд, на случай если сама анимация длиннее или зациклена. -
Механические звуки —
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)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_destroy = function(context)
-- Clean up the zone watch
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}
Разбор
-
Зона близости —
context.zones:create_sphere()создаёт зону радиусом 3 блока.context.zones:watch(handle, function() end, nil)включаетon_zone_enter, который срабатывает, когда любой игрок заходит внутрь. -
Пугающие эффекты — хук
on_zone_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 seconds between spawns
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end
-- Prevent spawn spam
if context.state.on_cooldown then
player:send_message("&7The spawner is recharging...")
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Try to spawn the EliteMobs boss
local boss = context.prop:spawn_elitemobs_boss(BOSS_FILE, loc.x, loc.y + 1, loc.z)
if boss then
-- Success -- play spawn effects
context.world:spawn_particle("FLAME", loc.x, loc.y + 1, loc.z, 20, 0.5, 0.5, 0.5, 0.05)
context.world:play_sound("ENTITY_EVOKER_PREPARE_SUMMON", loc.x, loc.y, loc.z, 1.0, 1.0)
player:send_message("&cA goblin warrior emerges!")
else
-- EliteMobs is not installed or the boss file was not found
context.log:warn("Could not spawn boss '" .. BOSS_FILE .. "' -- is EliteMobs installed?")
player:send_message("&7The spawner fizzles... (EliteMobs not available)")
-- Fizzle particles as visual feedback
context.world:spawn_particle("SMOKE", loc.x, loc.y + 1, loc.z, 10, 0.3, 0.3, 0.3, 0.02)
context.world:play_sound("BLOCK_FIRE_EXTINGUISH", loc.x, loc.y, loc.z, 0.8, 1.2)
end
-- Set cooldown
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}
Разбор
-
Спавн босса —
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_equip, on_shift_right_click для запуска способности, scheduler:run_repeating() для расширяющейся волны, get_nearby_entities() для поиска целей, методы боя сущностей, эффекты частиц/звука, модификацию ландшафта через get_highest_block_y(), show_action_bar() для отклика в интерфейсе и защиту от неживых сущностей.
Полный файл скрипта (нажмите, чтобы развернуть)
-- Frost Shockwave Sword
-- Shift + Right-click to release a horizontal frost wave
local WAVE_MAX_RADIUS = 12
local WAVE_SPEED = 2
local WAVE_TICK_INTERVAL = 2
local WAVE_DAMAGE = 4.0
local WAVE_KNOCKBACK = 1.2
local WAVE_CONE_ANGLE = 70
return {
api_version = 1,
on_equip = function(context)
context.state.wave_active = false
end,
on_shift_right_click = function(context)
if context.state.wave_active then return end
local loc = context.player.current_location
local yaw_rad = math.rad(loc.yaw)
local dir_x = -math.sin(yaw_rad)
local dir_z = math.cos(yaw_rad)
context.event:cancel()
context.state.wave_active = true
context.state.hit_entities = {}
context.state.destroyed_blocks = {}
local current_radius = 1
local origin_x, origin_y, origin_z = loc.x, loc.y, loc.z
local player_uuid = context.player.uuid
context.player:show_action_bar("&b&lFrost Shockwave!", 40)
context.world:play_sound("ENTITY_PLAYER_ATTACK_SWEEP", origin_x, origin_y, origin_z, 1.5, 0.5)
local task_id = context.scheduler:run_repeating(0, WAVE_TICK_INTERVAL, function(tick_context)
if current_radius > WAVE_MAX_RADIUS then
tick_context.state.wave_active = false
tick_context.scheduler:cancel(tick_context.state.wave_task)
return
end
local half_angle = math.rad(WAVE_CONE_ANGLE / 2)
local steps = math.floor(current_radius * 8)
for i = 0, steps do
local angle = (i / steps) * 2 * math.pi
local px = origin_x + math.cos(angle) * current_radius
local pz = origin_z + math.sin(angle) * current_radius
local to_x, to_z = px - origin_x, pz - origin_z
local to_len = math.sqrt(to_x * to_x + to_z * to_z)
if to_len > 0 then
local dot = (to_x * dir_x + to_z * dir_z) / to_len
if dot >= math.cos(half_angle) then
local ground_y = tick_context.world:get_highest_block_y(math.floor(px), math.floor(pz))
tick_context.world:spawn_particle("SNOWFLAKE", px, ground_y + 1.0, pz, 3, 0.3, 0.2, 0.3, 0.02)
local bx, bz = math.floor(px), math.floor(pz)
local block_key = bx .. "," .. bz
if not tick_context.state.destroyed_blocks[block_key] then
tick_context.state.destroyed_blocks[block_key] = true
local block_type = tick_context.world:get_block_at(bx, ground_y, bz)
if block_type ~= "bedrock" and block_type ~= "obsidian" and block_type ~= "barrier" then
tick_context.world:set_block_at(bx, ground_y, bz, "AIR")
end
end
end
end
end
local entities = tick_context.world:get_nearby_entities(origin_x, origin_y, origin_z, current_radius + 1)
for _, entity in ipairs(entities) do
if entity.uuid ~= player_uuid and not tick_context.state.hit_entities[entity.uuid] then
local eloc = entity.current_location
local dx, dz = eloc.x - origin_x, eloc.z - origin_z
local dist = math.sqrt(dx * dx + dz * dz)
if dist >= current_radius - 2 and dist <= current_radius + 1 and dist > 0 then
local dot = (dx * dir_x + dz * dir_z) / dist
if dot >= math.cos(half_angle) and entity.damage then
tick_context.state.hit_entities[entity.uuid] = true
entity:damage(WAVE_DAMAGE)
entity:push((dx/dist)*WAVE_KNOCKBACK, 0.3, (dz/dist)*WAVE_KNOCKBACK)
if entity.is_alive then entity:add_potion_effect("SLOWNESS", 60, 1) end
tick_context.world:spawn_particle("SNOWFLAKE", eloc.x, eloc.y+1, eloc.z, 10, 0.5, 0.5, 0.5, 0.1)
end
end
end
end
tick_context.world:play_sound("BLOCK_GLASS_BREAK",
origin_x + dir_x * current_radius, origin_y,
origin_z + dir_z * current_radius, 0.8, 1.5)
current_radius = current_radius + WAVE_SPEED
end)
context.state.wave_task = task_id
end
}
Разбор
-
Инициализация состояния —
on_equipзадаётwave_active = false, чтобы предотвратить несколько накладывающихся ударных волн. Этот хук срабатывает, когда игрок экипирует меч. -
Направленный конус — yaw игрока преобразуется в вектор направления (
dir_x,dir_z). Конус в 70 градусов перед игроком определяет разброс волны. -
Расширяющаяся волна —
scheduler:run_repeating()запускается каждые 2 тика. Каждый тик радиус волны растёт наWAVE_SPEED. Частицы спавнятся кольцом, но только в пределах угла конуса. -
Разрушение ландшафта —
get_highest_block_y()находит уровень земли в каждой точке. Поверхностные блоки разрушаются (ставятся в AIR), с исключением bedrock/obsidian/barrier. Таблицаdestroyed_blocksпредотвращает повторную обработку. -
Нацеливание на сущности —
get_nearby_entities()находит все сущности рядом с началом координат. Скрипт проверяет, что каждая сущность находится в текущей полосе волны, внутри конуса и ещё не была задета. Проверкаif entity.damage thenкритична —get_nearby_entities()возвращает ВСЕ сущности (стойки для брони, предметы и т. д.), а не только живые. -
Боевые эффекты — задетые сущности получают урон, отбрасываются в направлении волны и получают 3 секунды Slowness II.
-
Отклик игроку —
show_action_bar()отображает «Frost Shockwave!» на 40 тиков. Звуки разбития стекла продвигаются вместе с фронтом волны. -
Очистка — когда радиус превышает
WAVE_MAX_RADIUS, повторяющаяся задача отменяет сама себя и сбрасывает флагwave_active.
Всегда проверяйте if entity.damage then перед вызовом entity:damage(), entity:push() или entity:add_potion_effect(). Метод get_nearby_entities() возвращает все сущности в радиусе, включая неживые, такие как стойки для брони, выпавшие предметы и сферы опыта, у которых нет этих методов.
Пример: Жезл исцеления (скрипт предмета)
Чему учит: Более простой скрипт предмета, показывающий активацию по правому клику, управление кулдауном, метод item:consume() для ограниченного числа использований и исцеление через эффекты зелий.
Полный файл скрипта (нажмите, чтобы развернуть)
-- Healing Wand
-- Right-click to heal yourself. Consumes one charge per use.
local HEAL_AMOUNT = 6.0 -- 3 hearts
local COOLDOWN_TICKS = 60 -- 3 seconds
local REGEN_DURATION = 40 -- 2 seconds of regen
local REGEN_AMPLIFIER = 1 -- Regeneration II
return {
api_version = 1,
on_right_click = function(context)
-- Check remaining charges
local uses = context.item:get_uses()
if uses <= 0 then
context.player:show_action_bar("&c&lOut of charges!", 40)
context.world:play_sound("BLOCK_FIRE_EXTINGUISH",
context.player.current_location.x,
context.player.current_location.y,
context.player.current_location.z,
0.8, 1.5)
return
end
-- Check cooldown
if not context.cooldowns:check_local("heal", COOLDOWN_TICKS) then
context.player:show_action_bar("&cWand recharging...", 20)
return
end
-- Cancel the event so we don't interact with blocks
if context.event then
context.event:cancel()
end
-- Consume one charge
context.item:set_uses(uses - 1)
-- Heal the player
local loc = context.player.current_location
context.player:add_potion_effect("REGENERATION", REGEN_DURATION, REGEN_AMPLIFIER)
-- Visual and audio feedback
context.world:spawn_particle("HEART", loc.x, loc.y + 1.5, loc.z, 8, 0.4, 0.3, 0.4, 0)
context.world:spawn_particle("HAPPY_VILLAGER", loc.x, loc.y + 1, loc.z, 15, 0.5, 0.5, 0.5, 0)
context.world:play_sound("ENTITY_PLAYER_LEVELUP", loc.x, loc.y, loc.z, 0.7, 1.8)
-- UI feedback
context.player:show_action_bar("&a&lHealed! &7(" .. (uses - 1) .. " charges left)", 40)
end
}
Разбор
-
Паттерн кулдауна —
context.cooldowns:check_local("heal", COOLDOWN_TICKS)проверяет и запускает кулдаун перезарядки одним вызовом. Локальные кулдауны предмета сохраняются между циклами экипировки и снятия. -
Система зарядов —
context.item:get_uses()читает пользовательский счётчик использований из PDC предмета. Каждое использование уменьшает его черезset_uses(). Когда заряды достигают 0, жезл проигрывает «пшикающий» звук и отказывается активироваться. -
Исцеление — вместо прямой установки здоровья скрипт накладывает Regeneration II на 2 секунды. Это работает более естественно с системой здоровья Minecraft и правильно стакается с другими эффектами.
-
Отмена события —
context.event:cancel()предотвращает взаимодействие правого клика с соседними блоками (установку блоков, открытие дверей и т. д.). -
Отклик в интерфейсе —
show_action_bar()сообщает игроку результат действия и оставшиеся заряды. Разные сообщения для кулдауна, отсутствия зарядов и успешного исцеления делают предмет отзывчивым.
Используйте /fmm giveitem <id>, чтобы получить правильно тегированный пользовательский предмет. Предметам, созданным иными способами, может не хватать PDC-ключа fmm_item_id, из-за чего скрипты для них не активируются.
Лучшие практики
-
Начинайте с крошечного хука и проверяйте. Напишите один
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. -
Держите
on_game_tickлёгким. Если вы определяете этот хук, он запускается каждый серверный тик (20 раз в секунду). Ограждайте затратную работу черезcontext.cooldownsили интервал планировщика. -
Делайте пропсы неуязвимыми по умолчанию. Если только вы не хотите, чтобы пропс был ломаемым, включайте отмену урона
on_left_clickв каждый скрипт. -
Используйте ВЕРХНИЙ_РЕГИСТР для enum Bukkit. Имена звуков и частиц должны использовать формат констант enum Bukkit (например,
"FLAME", а не"flame").
Частые ошибки новичков
-
Использование внешнего
contextвнутри коллбэка планировщика. Внешний контекст захватывает снимок на момент срабатывания хука. Внутри коллбэков всегда используйте собственный параметр коллбэка. -
Забывают отменять повторяющиеся задачи. Если вы запускаете
run_repeatingвon_spawn, но никогда не отменяете её, задача работает, пока пропс не будет удалён. -
Не инициализируют состояние в
on_spawn. Чтениеcontext.state.xдо его установки возвращаетnil, что может молча сломать вашу логику. -
Неправильные имена анимаций. Если
play_animation("open")возвращаетfalse, имя анимации не совпадает с тем, что в файле модели. Проверьте модель на точные имена. -
Имена звуков/частиц в нижнем регистре.
"flame"не работает — используйте"FLAME". API внутренне преобразует частицы в ВЕРХНИЙ_РЕГИСТР, но имена enum 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 & Item API — никаких выдуманных алиасов.
- Имена звуков и частиц используют имена enum Bukkit в ВЕРХНЕМ_РЕГИСТРЕ.
- Скрипт не вызывает никаких блокирующих или долго выполняющихся операций внутри хука или коллбэка.
Советы по генерации через ИИ
Если вы хотите, чтобы ИИ надёжно генерировал скрипты пропсов, убедитесь, что промпт включает:
- Точное имя хука — например,
on_right_click, а не «когда игрок кликает по пропсу». - Имена анимаций из файла модели — ИИ не может их угадать; предоставьте их.
- Имена enum звуков — например,
"BLOCK_NOTE_BLOCK_HARP", а не «звук арфы». - Имена enum частиц — например,
"FLAME", а не «частицы огня». - Должен ли пропс быть неуязвимым — если да, включите
on_left_clickсcontext.event.cancel(). - Используйте только задокументированные имена методов — если его нет на странице Prop API, его не существует.
Пример хорошего промпта
Напиши скрипт пропса FMM, который проигрывает анимацию "activate" при правом клике, делает пропс неуязвимым, спавнит частицы FLAME в позиции пропса при клике, проигрывает звук BLOCK_LEVER_CLICK и имеет 2-секундный кулдаун между кликами через context.cooldowns:check_local("activate", 40).
Коллекция предметов гоблинов
Набор из 10 примеров скриптов предметов, тематически связанных с оружием гоблинов, доступен как загружаемый пример контента. Они не входят в базовый jar плагина — единственные скрипты, которые сам плагин записывает в scripts/ при первом запуске, это четыре готовых скрипта пропсов: invulnerable.lua, pickupable.lua, storage_single.lua и storage_double.lua. Каждый скрипт гоблина ниже демонстрирует разные боевые механики, эффекты частиц и паттерны использования API.
| Скрипт | Предмет | Эффект |
|---|---|---|
goblin_golden_sword.lua | Золотой меч | 25% шанс размашистого удара — наносит урон и отталкивает всех ближайших врагов |
goblin_iron_axe.lua | Железный топор | 20% шанс шипа — подбрасывает цель вверх с визуалом сталактитов |
goblin_bow.lua | Лук | Притягивает задетого моба к игроку |
goblin_crossbow.lua | Арбалет | 33% шанс залпа фейерверков |
goblin_trident.lua | Трезубец | Ледяной купол сковывает цель на 3 с |
goblin_iron_hoe.lua | Железная мотыга | 15% шанс ядовитых облаков |
goblin_mace.lua | Булава | Метеоритные удары с неба (правый клик) |
goblin_spear.lua | Копьё | Огненный луч вдоль прицела (shift+правый клик) |
goblin_shield.lua | Щит | Стеклянный купол при блоке (реактивно) |
goblin_iron_sword.lua | Железный меч | Заряжающееся кольцо тьмы (shift+правый клик) |
Эти скрипты работают с предметами, выглядящими ванильно. Каждому нужен лишь конфиг YML с полем material: — файл .bbmodel не требуется. Например, чтобы включить скрипт золотого меча, создайте конфиг YML вроде:
isEnabled: true
material: GOLDEN_SWORD
scripts:
- goblin_golden_sword.lua