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

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

Эта страница содержит полные рабочие примеры Lua-способностей EliteMobs, а также практические паттерны, лучшие практики и советы. Каждый пример включает пояснение того, что он делает и почему.

Если вы новичок в Lua-способностях, начните с Начало работы. Подробности API см. в Справочнике API, Босс и сущности, Мир и окружение, Зоны и нацеливание и Перечисления.

webapp_banner.jpg


Пример: Зонное нацеливание со скриптовыми утилитами

Чему учит этот пример: Как использовать 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
}

Пояснение

  1. Создание зоны -- context.script:zone(...) создаёт форму конуса, используя те же имена полей, что и Зоны EliteScript. Target задаёт начало конуса (сам босс, смещённый на 1 блок вверх), Target2 задаёт назначение (ближайшие игроки в радиусе 20 блоков). radius управляет раскрытием конуса.

  2. Генерация частиц -- cone:full_target(0.4) возвращает дескриптор цели, разрешающийся во все позиции внутри конуса с 40% покрытием.

  3. Урон -- context.script:damage(cone:full_target(), 1.0, 1.5) поражает все живые сущности внутри конуса. Первое число (1.0) — базовый урон, второе (1.5) — множитель урона для игроков.

Имена полей EliteScript

Таблицы зон и целей, передаваемые в 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
}

Пояснение

  1. Инициализация состояния -- on_spawn устанавливает context.state.started в false и context.state.loop_task_id в nil. Таблица state сохраняется на всё время жизни экземпляра босса.
  2. Защита боя -- on_enter_combat проверяет context.state.started перед запуском цикла, предотвращая множественные перекрывающиеся циклы.
  3. Паттерн планировщика -- context.scheduler:run_every(100, callback) выполняет колбэк каждые 100 тиков (5 секунд). Колбэк получает свежий контекст.
  4. Очистка при выходе -- 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
}

Пояснение

  1. Защита от nil -- context.player может быть nil в редких крайних случаях. Всегда проверяйте перед использованием.
  2. Локальная перезарядка -- context.cooldowns:check_local("fire_touch", 60) атомарно проверяет и устанавливает перезарядку.
  3. Тики огня -- context.player:set_fire_ticks(60) поджигает игрока на 60 тиков (3 секунды).
  4. Частицы -- context.world:spawn_particle_at_location(location, spec) генерирует частицы в позиции.
  5. Сообщение -- context.player:send_message(text) отправляет сообщение с цветовыми кодами.
  6. Глобальная перезарядка -- 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
}

Пояснение

  1. Настройка состояния -- on_spawn инициализирует aoe_task_id как nil.
  2. Повторяющаяся атака -- on_enter_combat запускает повторяющуюся задачу каждые 60 тиков.
  3. Определение зоны -- Таблица zone_def использует синтаксис нативной Lua-зоны с kind, radius и origin.
  4. Запрос сущностей -- Возвращает Lua-массив всех игроков внутри сферы.
  5. Нанесение урона -- victim:deal_custom_damage(4.0) наносит 4 очка урона, приписанных боссу.
  6. Визуальная обратная связь -- Фиолетовые частицы пыли на каждой жертве, фоновые частицы на позиции босса.
  7. Очистка -- on_exit_combat отменяет задачу и очищает состояние.
Нативные зоны vs. зоны скриптовых утилит

Этот пример использует нативные 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
}

Пояснение

  1. Инициализация состояния -- on_spawn устанавливает phase в 1, attack_task_id в nil и phase_switched в false.
  2. Цикл атак фазы 1 -- on_enter_combat запускает повторяющуюся задачу, выполняющую phase_one_attack каждые 100 тиков.
  3. Проверка фазы в on_game_tick -- Проверяет перезарядку в 20 тиков, чтобы избежать тяжёлой логики каждый тик.
  4. Порог здоровья -- При 50% или ниже начинается переход.
  5. Переход фазы -- Старый цикл отменяется, воспроизводится анимация трансформации, ближайшие игроки получают сообщение и заголовок, запускается новый более быстрый цикл (каждые 40 тиков).
  6. Атаки фазы 2 -- Большая сфера (8 блоков), меньший урон за удар но гораздо чаще, эффект замедления, красные частицы пыли и звук визера.
  7. Логирование -- context.log:info(...) пишет в серверную консоль.
  8. Очистка -- on_exit_combat отменяет любой активный цикл атак.
Держите on_game_tick лёгким

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

  1. Файл возвращает ровно одну таблицу с api_version = 1.
  2. Имена хуков точно соответствуют списку хуков.
  3. context.player защищён проверкой == nil перед использованием.
  4. Поля context.state инициализированы в on_spawn.
  5. Каждый run_every имеет соответствующий cancel_task в on_exit_combat.
  6. Колбэки планировщика используют параметр контекста колбэка, а не внешний context.
  7. Ключи перезарядки — описательные строки, длительности — в тиках.
  8. Хуки on_game_tick защищают тяжёлую работу проверкой перезарядки.
  9. Все имена методов существуют в Справочнике API.
  10. Таблицы скриптовых утилит используют имена полей EliteScript.
  11. Нативные определения зон используют kind, radius, origin, destination и т.д.
  12. Способность не вызывает блокирующих операций внутри хуков или колбэков.
  13. Спецификации частиц используют допустимые имена перечислений частиц 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 не загрузит способность.


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