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

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)
-- Guard: the player may be nil in edge cases
if context.player == nil then
return
end

-- Check and set a 60-tick (3 second) local cooldown in one call
if not context.cooldowns:check_local("fire_touch", 60) then
return
end

-- Set the player on fire for 60 ticks (3 seconds)
context.player:set_fire_ticks(60)

-- Visual feedback: spawn flame particles at the player's location
context.world:spawn_particle_at_location(
context.player.current_location,
{ particle = "FLAME", amount = 20, speed = 0.1 }
)

-- Tell the player what happened
context.player:send_message("&cThe boss's touch burns!")

-- Set the global power cooldown so other powers on this boss
-- don't all fire at the same instant
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)
-- Prevent duplicate loops
if context.state.aoe_task_id ~= nil then
return
end

context.state.aoe_task_id = context.scheduler:run_every(60, function(tick_context)
-- Make sure the boss is still alive
if not tick_context.boss.exists then
return
end

-- Check a local cooldown so this doesn't stack with other effects
if not tick_context.cooldowns:check_local("pulse_aoe", 60) then
return
end

-- Build a sphere zone centered on the boss's current position
local zone_def = {
kind = "sphere",
radius = 8,
origin = tick_context.boss:get_location()
}

-- Find all players inside the sphere
local victims = tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" })

-- Damage and show particles on each player found
for i = 1, #victims do
local victim = victims[i]
victim:deal_custom_damage(4.0)

tick_context.world:spawn_particle_at_location(
victim.current_location,
{ particle = "DUST", amount = 15, speed = 0, red = 128, green = 0, blue = 255 }
)
end

-- Spawn visual ring particles at the boss
tick_context.world:spawn_particle_at_location(
tick_context.boss:get_location(),
{ particle = "WITCH", amount = 40, speed = 0.1 }
)

-- Set global cooldown
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)
-- Slow, heavy slam
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)
-- Fast, frantic multi-hit
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

local function start_attack_loop(context)
if context.state.attack_task_id ~= nil then return end
local phase = context.state.phase or 1
local interval = phase == 2 and 40 or 100
local attack = phase == 2 and phase_two_attack or phase_one_attack
context.state.attack_task_id = context.scheduler:run_every(interval, function(tick_context)
if not tick_context.boss.exists or not tick_context.boss.is_in_combat then return end
if tick_context.cooldowns:check_local("phase_attack", interval) then
attack(tick_context)
end
end)
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)
start_attack_loop(context)
end,

on_game_tick = function(context)
-- Only check phase transition every 20 ticks (1 second) to stay lightweight
if not context.boss.is_in_combat then return end
if not context.cooldowns:check_local("phase_check", 20) then
return
end

-- Skip if already in phase 2
if context.state.phase ~= 1 then
return
end

-- Check health ratio
local health_ratio = context.boss.health / context.boss.maximum_health

if health_ratio <= 0.5 then
-- Transition to phase 2
context.state.phase = 2
context.state.phase_switched = true

context.log:info("Boss entering phase 2 at " .. tostring(math.floor(health_ratio * 100)) .. "% health")

-- Cancel the old attack loop
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end

-- Play transition effects
context.boss:play_model_animation("transform")

-- Announce the phase change to nearby players
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

start_attack_loop(context)
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. Цикл атак -- on_enter_combat возобновляет текущую фазу через start_attack_loop: в фазе 1 атака происходит каждые 100 тиков, в фазе 2 — каждые 40. Выход из боя и повторный вход сохраняют фазу. Атака фазы 1 создаёт сферу радиусом 5 блоков, наносит урон игрокам внутри и воспроизводит анимацию удара с частицами взрыва.
  3. Проверка фазы в on_game_tick -- Вне боя on_game_tick сразу завершает работу, затем проверяет перезарядку в 20 тиков ("phase_check"). Если босс уже в фазе 2, обработчик также завершается досрочно.
  4. Порог здоровья -- При 50% или ниже начинается переход.
  5. Переход фазы -- Старый цикл отменяется, воспроизводится анимация трансформации, ближайшие игроки получают сообщение и заголовок, запускается новый более быстрый цикл (каждые 40 тиков).
  6. Атаки фазы 2 -- Большая сфера (8 блоков), меньший урон за удар но гораздо чаще, эффект замедления, красные частицы пыли и звук визера.
  7. Логирование -- context.log:info(...) пишет в серверную консоль.
  8. Очистка -- on_exit_combat отменяет любой активный цикл атак.
Держите on_game_tick лёгким

on_game_tick вызывается каждый серверный тик (20 раз в секунду). Ограничивайте тяжёлые операции проверкой перезарядки, например check_local("phase_check", 20). Превышение бюджета выполнения отключает способность.


Советы по генерации с помощью ИИ

Для надёжной генерации Lua-способностей ИИ включите в промпт: точное имя хука, нативные Lua-зоны или скриптовые утилиты (укажите какой), локальные и глобальные перезарядки с ключом и длительностью в тиках, имена анимаций модели, выбор цели, тип эффекта и только задокументированные имена методов.

Хороший пример промпта

Напишите Lua-способность, использующую on_enter_combat для запуска повторяющейся задачи каждые 80 тиков. На каждом тике создайте нативную Lua-сферическую зону (радиус 6) с центром на боссе, запросите игроков внутри и нанесите 2.0 пользовательского урона каждому. Используйте локальный ключ перезарядки "pulse" с длительностью 80. Отмените задачу в on_exit_combat. Сгенерируйте частицы DUST (red=0, green=255, blue=100) на каждой жертве.

Дополнительные ограничения, которые стоит включить

  • «Верните одну таблицу с api_version = 1
  • «Инициализируйте все поля состояния в on_spawn
  • «Защищайте обратные вызовы планировщика через if not tick_context.boss.exists then return end
  • «Отменяйте все повторяющиеся задачи в on_exit_combat
  • «Не выдумывайте имена методов — используйте только методы со страницы справочника API.»
  • «Используйте context.cooldowns:check_local(key, ticks) для совмещённой проверки и установки.»

Чеклист 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 лёгким. Ограничивайте тяжёлые операции перезарядкой и оставляйте большой запас до бюджета выполнения.

  • Отменяйте повторяющиеся задачи по завершении. Каждый 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 не загрузит способность.


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