Lua-скриптинг: Примеры и паттерны
Эта страница содержит полные рабочие примеры Lua-способностей EliteMobs, а также практические паттерны, лучшие практики и советы. Каждый пример включает пояснение того, что он делает и почему.
Если вы новичок в Lua-способностях, начните с Начало работы. Подробности API см. в Справочнике API, Босс и сущности, Мир и окружение, Зоны и нацеливание и Перечисления.
Пример: Зонное нацеливание со скриптовыми утилитами
Чему учит этот пример: Как использовать context.script для создания геометрии зон в стиле EliteScript из Lua, генерации частиц и нанесения урона сущностям в зоне.
Полный файл способности (нажмите для раскрытия)
return {
api_version = 1,
on_boss_damaged_by_player = function(context)
local cone = context.script:zone({
shape = "CONE",
Target = { targetType = "SELF", offset = "0,1,0" },
Target2 = { targetType = "NEARBY_PLAYERS", range = 20 },
radius = 5
})
context.script:spawn_particles(cone:full_target(0.4), { particle = "FLAME", amount = 1, speed = 0.05 })
context.script:damage(cone:full_target(), 1.0, 1.5)
end
}
Пояснение
-
Создание зоны --
context.script:zone(...)создаёт форму конуса, используя те же имена полей, что и Зоны EliteScript.Targetзадаёт начало конуса (сам босс, смещённый на 1 блок вверх),Target2задаёт назначение (ближайшие игроки в радиусе 20 блоков).radiusуправляет раскрытием конуса. -
Генерация частиц --
cone:full_target(0.4)возвращает дескриптор цели, разрешающийся во все позиции внутри конуса с 40% покрытием. -
Урон --
context.script:damage(cone:full_target(), 1.0, 1.5)поражает все живые сущности внутри конуса. Первое число (1.0) — базовый урон, второе (1.5) — множитель урона для игроков.
Таблицы зон и целей, передаваемые в context.script, используют имена полей EliteScript (targetType, shape, Target, Target2, range, offset, coverage). Полный список см. в Зонах EliteScript и Целях EliteScript.
Пример: Цикл атак с состоянием и планировщиком
Чему учит этот пример: Использование context.state для отслеживания состояния выполнения, context.scheduler для повторяющихся задач и правильный жизненный цикл входа/выхода из боя.
Полный файл способности (нажмите для раскрытия)
local function pick_action(context)
local roll = math.random(1, 2)
if roll == 1 then context.boss:play_model_animation("slam")
else context.boss:play_model_animation("roar") end
end
return {
api_version = 1,
on_spawn = function(context)
context.state.started = false
context.state.loop_task_id = nil
end,
on_enter_combat = function(context)
if context.state.started then return end
context.state.started = true
context.state.loop_task_id = context.scheduler:run_every(100, function(loop_context)
if loop_context.boss.exists then pick_action(loop_context) end
end)
end,
on_exit_combat = function(context)
if context.state.loop_task_id ~= nil then
context.scheduler:cancel_task(context.state.loop_task_id)
context.state.loop_task_id = nil
end
context.state.started = false
end
}
Пояснение
- Инициализация состояния --
on_spawnустанавливаетcontext.state.startedвfalseиcontext.state.loop_task_idвnil. Таблицаstateсохраняется на всё время жизни экземпляра босса. - Защита боя --
on_enter_combatпроверяетcontext.state.startedперед запуском цикла, предотвращая множественные перекрывающиеся циклы. - Паттерн планировщика --
context.scheduler:run_every(100, callback)выполняет колбэк каждые 100 тиков (5 секунд). Колбэк получает свежий контекст. - Очистка при выходе --
on_exit_combatотменяет повторяющуюся задачу и сбрасывает состояние.
Если вы запускаете повторяющуюся задачу в on_enter_combat, всегда отменяйте её в on_exit_combat. Забытая отмена оставляет фоновую задачу, работающую до исчезновения босса.
Пример: Эффект огня при ударе
Чему учит этот пример: Простая способность «при ударе применить эффект» — самый распространённый паттерн для боевых способностей.
Полный файл способности (нажмите для раскрытия)
return {
api_version = 1,
on_player_damaged_by_boss = function(context)
if context.player == nil then return end
if not context.cooldowns:check_local("fire_touch", 60) then return end
context.player:set_fire_ticks(60)
context.world:spawn_particle_at_location(context.player.current_location, { particle = "FLAME", amount = 20, speed = 0.1 })
context.player:send_message("&cThe boss's touch burns!")
context.cooldowns:set_global(40)
end
}
Пояснение
- Защита от nil --
context.playerможет бытьnilв редких крайних случаях. Всегда проверяйте перед использованием. - Локальная перезарядка --
context.cooldowns:check_local("fire_touch", 60)атомарно проверяет и устанавливает перезарядку. - Тики огня --
context.player:set_fire_ticks(60)поджигает игрока на 60 тиков (3 секунды). - Частицы --
context.world:spawn_particle_at_location(location, spec)генерирует частицы в позиции. - Сообщение --
context.player:send_message(text)отправляет сообщение с цветовыми кодами. - Глобальная перезарядка --
context.cooldowns:set_global(40)ставит все способности босса на перезарядку в 40 тиков.
Пример: Зонная AoE-способность с нативными Lua-зонами
Чему учит этот пример: Создание и запрос нативных Lua-зон для нанесения урона игрокам в области.
Полный файл способности (нажмите для раскрытия)
return {
api_version = 1,
on_spawn = function(context) context.state.aoe_task_id = nil end,
on_enter_combat = function(context)
if context.state.aoe_task_id ~= nil then return end
context.state.aoe_task_id = context.scheduler:run_every(60, function(tick_context)
if not tick_context.boss.exists then return end
if not tick_context.cooldowns:check_local("pulse_aoe", 60) then return end
local zone_def = { kind = "sphere", radius = 8, origin = tick_context.boss:get_location() }
local victims = tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #victims do
victims[i]:deal_custom_damage(4.0)
tick_context.world:spawn_particle_at_location(victims[i].current_location, { particle = "DUST", amount = 15, speed = 0, red = 128, green = 0, blue = 255 })
end
tick_context.world:spawn_particle_at_location(tick_context.boss:get_location(), { particle = "SPELL_MOB", amount = 40, speed = 0.1 })
tick_context.cooldowns:set_global(60)
end)
end,
on_exit_combat = function(context)
if context.state.aoe_task_id ~= nil then
context.scheduler:cancel_task(context.state.aoe_task_id)
context.state.aoe_task_id = nil
end
end
}
Пояснение
- Настройка состояния --
on_spawnинициализируетaoe_task_idкакnil. - Повторяющаяся атака --
on_enter_combatзапускает повторяющуюся задачу каждые 60 тиков. - Определение зоны -- Таблица
zone_defиспользует синтаксис нативной Lua-зоны сkind,radiusиorigin. - Запрос сущностей -- Возвращает Lua-массив всех игроков внутри сферы.
- Нанесение урона --
victim:deal_custom_damage(4.0)наносит 4 очка урона, приписанных боссу. - Визуальная обратная связь -- Фиолетовые частицы пыли на каждой жертве, фоновые частицы на позиции босса.
- Очистка --
on_exit_combatотменяет задачу и очищает состояние.
Этот пример использует нативные Lua-зоны (context.zones:get_entities_in_zone()). Скриптовые утилиты (context.script:zone(...)) используют имена полей EliteScript. Оба варианта работают — используйте нативные зоны для простых форм и context.script для продвинутого разрешения целей.
Пример: Многофазная механика босса
Чему учит этот пример: Использование состояния для отслеживания фаз босса и смены поведения при пороговых значениях здоровья.
Полный файл способности (нажмите для раскрытия)
local function phase_one_attack(context)
context.boss:play_model_animation("slam")
local zone_def = { kind = "sphere", radius = 5, origin = context.boss:get_location() }
local targets = context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #targets do targets[i]:deal_custom_damage(3.0) end
context.world:spawn_particle_at_location(context.boss:get_location(), { particle = "EXPLOSION", amount = 3, speed = 0 })
end
local function phase_two_attack(context)
context.boss:play_model_animation("frenzy")
local zone_def = { kind = "sphere", radius = 8, origin = context.boss:get_location() }
local targets = context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #targets do
targets[i]:deal_custom_damage(2.0)
targets[i]:apply_potion_effect("SLOWNESS", 40, 1)
end
context.world:spawn_particle_at_location(context.boss:get_location(), { particle = "DUST", amount = 30, speed = 0.2, red = 255, green = 0, blue = 0 })
context.world:play_sound_at_location(context.boss:get_location(), "entity.wither.ambient", 1.0, 1.5)
end
return {
api_version = 1,
on_spawn = function(context)
context.state.phase = 1
context.state.attack_task_id = nil
context.state.phase_switched = false
end,
on_enter_combat = function(context)
if context.state.attack_task_id ~= nil then return end
context.state.attack_task_id = context.scheduler:run_every(100, function(tick_context)
if not tick_context.boss.exists then return end
if not tick_context.cooldowns:check_local("phase_attack", 100) then return end
phase_one_attack(tick_context)
end)
end,
on_game_tick = function(context)
if not context.cooldowns:check_local("phase_check", 20) then return end
if context.state.phase ~= 1 then return end
local health_ratio = context.boss.health / context.boss.maximum_health
if health_ratio <= 0.5 then
context.state.phase = 2
context.state.phase_switched = true
context.log:info("Boss entering phase 2 at " .. tostring(math.floor(health_ratio * 100)) .. "% health")
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end
context.boss:play_model_animation("transform")
local nearby = context.players.nearby_players(40)
for i = 1, #nearby do
nearby[i]:send_message("&4&lThe boss enters a frenzy!")
nearby[i]:show_title("&4Phase 2", "&cThe boss is enraged!", 10, 40, 10)
end
context.state.attack_task_id = context.scheduler:run_every(40, function(tick_context)
if not tick_context.boss.exists then return end
if not tick_context.cooldowns:check_local("phase_attack", 40) then return end
phase_two_attack(tick_context)
end)
end
end,
on_exit_combat = function(context)
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end
end
}
Пояснение
- Инициализация состояния --
on_spawnустанавливаетphaseв1,attack_task_idвnilиphase_switchedвfalse. - Цикл атак фазы 1 --
on_enter_combatзапускает повторяющуюся задачу, выполняющуюphase_one_attackкаждые 100 тиков. - Проверка фазы в on_game_tick -- Проверяет перезарядку в 20 тиков, чтобы избежать тяжёлой логики каждый тик.
- Порог здоровья -- При 50% или ниже начинается переход.
- Переход фазы -- Старый цикл отменяется, воспроизводится анимация трансформации, ближайшие игроки получают сообщение и заголовок, запускается новый более быстрый цикл (каждые 40 тиков).
- Атаки фазы 2 -- Большая сфера (8 блоков), меньший урон за удар но гораздо чаще, эффект замедления, красные частицы пыли и звук визера.
- Логирование --
context.log:info(...)пишет в серверную консоль. - Очистка --
on_exit_combatотменяет любой активный цикл атак.
on_game_tick выполняется каждый серверный тик (20 раз в секунду). Всегда защищайте тяжёлую работу проверкой перезарядки. Если хук превышает 50мс, EliteMobs автоматически отключит способность.
Советы по генерации с помощью ИИ
Для надёжной генерации Lua-способностей ИИ включите в промпт: точное имя хука, нативные Lua-зоны или скриптовые утилиты (укажите какой), локальные и глобальные перезарядки с ключом и длительностью в тиках, имена анимаций модели, выбор цели, тип эффекта и только задокументированные имена методов.
Хороший пример промпта
Напишите Lua-способность, использующую
on_enter_combatдля запуска повторяющейся задачи каждые 80 тиков. На каждом тике создайте нативную Lua-сферическую зону (радиус 6) с центром на боссе, запросите игроков внутри и нанесите 2.0 пользовательского урона каждому. Используйте локальный ключ перезарядки"pulse"с длительностью 80. Отмените задачу вon_exit_combat. Сгенерируйте частицы DUST (red=0, green=255, blue=100) на каждой жертве.
Чеклист QC
- Файл возвращает ровно одну таблицу с
api_version = 1. - Имена хуков точно соответствуют списку хуков.
context.playerзащищён проверкой== nilперед использованием.- Поля
context.stateинициализированы вon_spawn. - Каждый
run_everyимеет соответствующийcancel_taskвon_exit_combat. - Колбэки планировщика используют параметр контекста колбэка, а не внешний
context. - Ключи перезарядки — описательные строки, длительности — в тиках.
- Хуки
on_game_tickзащищают тяжёлую работу проверкой перезарядки. - Все имена методов существуют в Справочнике API.
- Таблицы скриптовых утилит используют имена полей EliteScript.
- Нативные определения зон используют
kind,radius,origin,destinationи т.д. - Способность не вызывает блокирующих операций внутри хуков или колбэков.
- Спецификации частиц используют допустимые имена перечислений частиц Bukkit в UPPER_CASE.
Лучшие практики
-
Начните с маленького хука и проверьте. Напишите один
on_spawn, отправляющий сообщение в лог. Подтвердите, что он срабатывает. Затем стройте дальше. -
Держите вспомогательные функции локальными. Объявляйте хелперы как
local function pick_action(context)над таблицей возврата. Это удерживает их вне глобальной области видимости и предотвращает конфликты с другими Lua-способностями, загруженными в том же рантайме. -
Размещайте геометрию в скриптовых утилитах. Если вам нужны конусы, вращающиеся лучи, перемещающиеся лучи или анимированные зоны, используйте
context.script:zone(...)с именами полей EliteScript. Скриптовые утилиты повторно используют проверенный движок зон EliteScript. -
Используйте
context.stateдля состояния выполнения. Не используйте глобальные переменные Lua.context.stateограничен одним экземпляром босса и сохраняется между хуками на протяжении всего времени жизни этого босса. -
Используйте именованные локальные ключи перезарядки. Вместо простых чисел используйте описательные ключи вроде
"fire_touch"или"aoe_pulse". Это облегчает отладку и предотвращает случайные конфликты между разными перезарядками в одной способности. -
Держите
on_game_tickлегким. Всегда защищайте проверкой перезарядки. Если ваша логика выполняется каждый тик, она должна завершаться значительно менее чем за 50мс, иначе способность будет отключена. -
Отменяйте повторяющиеся задачи по завершении. Каждый
run_everyдолжен иметь соответствующийcancel_taskвon_exit_combat(и, возможно,on_death). Утечённые задачи расходуют CPU и могут вызывать ошибки нулевых ссылок после деспавна босса. -
Используйте свежие контексты в колбэках планировщика. Колбэки планировщика (
run_every,run_after) получают свежий контекст как параметр. Всегда используйте этот параметр — не внешнийcontext— так как внешний контекст может содержать устаревшие снимки. -
Логируйте переходы состояний через
context.log:info(). Во время разработки добавляйте логирование для переключений фаз, начала перезарядок и запуска/остановки планировщика. Удалите или измените наcontext.log:debug()перед развёртыванием. -
Повторно используйте существующую документацию EliteScript. Страницы Зоны, Цели, Относительные векторы и Условия документируют те же имена полей, которые принимают скриптовые утилиты. Не дублируйте эту информацию в вашей Lua-способности — просто ссылайтесь на неё.
Распространённые ошибки новичков
-
Использование внешнего
contextвнутри колбэка планировщика. Внешний контекст захватывает снимок на момент выполнения хука. Внутри колбэкаrun_everyилиrun_afterвсегда используйте собственный параметр колбэка (например,tick_context), который даёт вам свежий снимок. -
Забывают отменить повторяющиеся задачи. Если вы запускаете
run_everyвon_enter_combat, но никогда не отменяете, задача выполняется до тех пор, пока босс не будет удалён с сервера, даже после окончания боя. -
Не инициализируют состояние в
on_spawn. Если вы читаетеcontext.state.phaseвon_game_tick, но никогда не устанавливаете его вon_spawn, оно будетnilи ваши сравнения будут вести себя неожиданно. -
Проверяют
context.playerбез защиты от nil. В хуках вродеon_player_damaged_by_bossигрок почти всегда доступен — но «почти всегда» — это не «всегда». Одна отсутствующая проверка nil может вызвать сбой способности. -
Используют Lua-стиль имён полей в вызовах скриптовых утилит. Скриптовые утилиты ожидают
targetType,Target,Target2,shape— неtarget_type,target,target2,zone_shape. Несовпадающие имена молча не производят результатов. -
Выполняют тяжёлую логику в
on_game_tickбез защиты перезарядкой. Этот хук срабатывает каждый серверный тик. Даже простая арифметика, повторяемая 20 раз в секунду на многих боссах, накапливается. -
Придумывают имена методов. Если метод не указан в Справочнике API, он не существует. Распространённые ошибки включают написание
entity:teleport(loc)вместоentity:teleport_to_location(loc), илиplayer:set_velocity(vec)вместоplayer:set_velocity_vector(vec). -
Используют
context.boss.healthдля установки здоровья.context.boss.health— это снимок только для чтения. Чтобы исцелить босса, используйтеcontext.boss:restore_health(amount). -
Забывают
api_version = 1. Возвращаемая таблица должна включать это поле, иначе EliteMobs не загрузит способность.
Следующие шаги
- Начало работы -- структура файлов, хуки, первая способность
- Хуки и жизненный цикл -- полный справочник хуков и контекста
- Босс и сущности -- методы босса, игрока и сущностей
- Мир и окружение -- частицы, звуки, молнии, блоки
- Зоны и нацеливание -- нативные зоны и зоны скриптовых утилит
- Перечисления -- допустимые значения для Particle, Sound, Material и других строковых констант
- Устранение неполадок -- распространённые проблемы, советы по отладке и миграции
