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

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

Здесь приведены полные примеры Lua-способностей EliteMobs с пояснениями. Сохраните каждый пример в отдельный файл .lua, назначьте его тестовому боссу, выполните /em reload и после правок создайте нового босса. Анимации slam, roar, frenzy и transform должны существовать в пользовательской модели босса: это не встроенные анимации. Босс без пользовательской модели пропускает эти вызовы. Время ниже рассчитано для 20 TPS.

Если вы новичок в Lua-способностях, начните с Начала работы. Для NPC см. Скрипты NPC. Подробности 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 задаёт вершину конуса на один блок выше босса. Первая разрешённая позиция Target2 определяет центр основания и длину конуса; radius = 5 задаёт радиус основания. NEARBY_PLAYERS ищет в прямоугольной области, простирающейся на 20 блоков по каждой оси, и не гарантирует ближайшего игрока. Чтобы нацелиться на атакующего в этом хуке урона, используйте DIRECT_TARGET. Если игрок не найден, конуса нет; концы должны быть разными точками одного мира.

  2. Генерация частиц -- cone:full_target(0.4) возвращает дескриптор цели, который при каждом вызове случайно выбирает точки зоны с вероятностью 40%. Это не гарантирует точное количество и не фильтрует получателей урона. Настройки частиц используют поля EliteScript particle, amount и speed. Утилита смещает выбранные позиции зоны на полблока по X и Z, поэтому частицы не обозначают точные границы попадания по сущностям.

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

Пример выполняется при каждом переданном событии удара: локальной и глобальной перезарядки нет, как и задержки предупреждения между частицами и уроном. При сочетании с другими атаками добавьте явные проверки перезарядки. Каждый запрос статической зоны заново разрешает цели; Lua-дескриптор зоны не применяет автоматически условия YAML-действий или кэширование целей.

Имена полей 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 секунд). Перед первым вызовом он не ждёт пять секунд. Если нужна такая задержка, используйте run_repeating(100, 100, callback). Колбэк получает свежий контекст без исходного события или игрока, поэтому loop_context.boss — свежий снимок. Сохраните возвращённый числовой ID задачи в состоянии. Пример только выбирает анимацию и сам по себе не наносит урон.
  4. Очистка при выходе -- on_exit_combat отменяет повторяющуюся задачу по сохранённому ID и сбрасывает состояние. Без этой очистки планировщик продолжает работать после окончания боя.
Всегда отменяйте задачи планировщика

Отменяйте задачи, нужные только в бою, в 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

if not context.cooldowns:global_ready() 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

-- Coordinate with other powers that check the shared cooldown
context.cooldowns:set_global(40)

-- 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!")
end
}

Пояснение​

  1. Защита от nil -- context.player разрешается из игрока-участника текущего события или его прямой цели. Проверяйте его перед использованием, особенно при переносе кода в другой хук. Колбэки планировщика не наследуют этого игрока.
  2. Локальная перезарядка -- После проверки global_ready() вызов context.cooldowns:check_local("fire_touch", 60) проверяет ключ и сразу задаёт перезарядку на 60 тиков, если он готов. Иначе возвращает false, и хук завершается. Способности на одном боссе разделяют этот ключ; разные боссы независимы. При объединении примеров добавляйте к ключам префикс имени способности.
  3. Время горения -- context.player:set_fire_ticks(60) задаёт Bukkit длительность горения 60 тиков (3 секунды), проверив, что игрок действителен, жив, онлайн, находится в мире источника и, внутри экземпляра подземелья, является текущим участником. Время заменяется, а не увеличивается на 60 тиков. Другие игровые механики могут погасить огонь; сообщение и частицы не доказывают, что эффект был принят.
  4. Частицы -- context.world:spawn_particle_at_location(location, spec) создаёт частицы в указанной позиции. Таблица настроек принимает particle (имя перечисления частиц Bukkit), amount и speed.
  5. Сообщение -- context.player:send_message(text) отправляет цветное сообщение в чат. Стандартные цветовые коды Minecraft, например &c (красный), обрабатываются автоматически.
  6. Глобальная перезарядка -- context.cooldowns:set_global(40) задаёт общую перезарядку босса на 40 тиков (2 секунды) до применения эффектов. Пример проверяет её до расходования локальной перезарядки. Другие Lua-хуки и колбэки тоже должны вызывать global_ready(), если им следует ждать: установка глобальной перезарядки не приостанавливает все Lua-колбэки.

Пример: Зонная 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

if not tick_context.cooldowns:global_ready() then
return
end

-- Check and set this boss's named cooldown before applying effects
if not tick_context.cooldowns:check_local("pulse_aoe", 60) then
return
end
tick_context.cooldowns:set_global(60)

-- 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 a particle burst at the boss; this does not draw the sphere
tick_context.world:spawn_particle_at_location(
tick_context.boss:get_location(),
{ particle = "WITCH", amount = 40, speed = 0.1 }
)
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. Здесь будет храниться ID повторяющейся задачи планировщика.
  2. Повторяющаяся атака -- on_enter_combat запускает задачу без начальной задержки с интервалом 60 тиков (3 секунды). Проверка предотвращает дублирование циклов. Каждый колбэк проверяет глобальную перезарядку, затем проверяет и устанавливает pulse_aoe, после чего задаёт глобальную перезарядку на 60 тиков до эффектов. Это координирует только способности, проверяющие эти ключи, и не блокирует автоматически все скрипты.
  3. Определение зоны -- zone_def использует синтаксис нативных Lua-зон. kind задаёт форму ("sphere"), radius — размер, а origin получает позицию босса на момент выполнения колбэка. Поэтому зона следует за движущимся боссом.
  4. Запрос сущностей -- tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" }) возвращает Lua-массив с индексами от 1, используя пересечение формы с сущностью, а не точную проверку расстояния до ног. Поиск идёт в текущем мире босса и не фильтрует игроков по режиму игры или участию в бою. Фильтры принимают "players", "elites", "mobs" или "living" (по умолчанию); сам босс исключён.
  5. Нанесение урона -- victim:deal_custom_damage(4.0) передаёт 4 базового урона от босса через BossCustomAttackDamage. Неуязвимые и мёртвые цели, а также игроки вне режимов Survival и Adventure пропускаются. В пути элитного моба к игроку вызов обходит обычную формулу урона и снижение от блокирования, а ванильное снижение от брони обнуляется. Обработчики урона, отмена события и поглощение всё ещё могут изменить потерю здоровья. Утилита сбрасывает нативную перезарядку урона цели после отправки; это другой путь, чем context.script:damage.
  6. Частицы -- Фиолетовая пыль появляется в сохранённой позиции каждого найденного игрока, даже если урон отклонён. На боссе создаётся всплеск WITCH, а не кольцо или граница сферы. В этих компактных таблицах частиц разброс позиции по умолчанию нулевой, а размер пыли фиксирован на 1.
  7. Очистка -- on_exit_combat отменяет повторяющуюся задачу и очищает состояние, как в предыдущем примере.
Нативные зоны vs. зоны скриптовых утилит

Этот пример использует нативные Lua-зоны (context.zones:get_entities_in_zone()), принимающие простую таблицу с kind, radius, origin и другими полями. Скриптовые утилиты (context.script:zone(...)) используют имена полей EliteScript, например shape, Target и Target2. Для простых форм используйте нативные зоны, для расширенного разрешения целей — context.script.


Пример: Многофазная механика босса​

Чему учит этот пример: Использование состояния для отслеживания фаз босса и смены поведения при пороговых значениях здоровья.

Это фазы состояния Lua, а не YAML phases: EliteMobs, заменяющие определение босса. Здесь босс не заменяется и не исцеляется. Последующее лечение не возвращает его в фазу 1; выход из боя сохраняет фазу 2 до пересоздания рантайма способности.

Полный файл способности (нажмите для раскрытия)
local function phase_one_attack(context)
-- One damage pulse per callback; the animation does not delay it
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)
-- One pulse per callback, repeated more frequently in phase 2
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
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.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. Состояние принадлежит этому рантайму способности, не разделяется с другими способностями и не сохраняется между перезагрузками.
  2. Цикл атак -- on_enter_combat возобновляет текущую фазу через start_attack_loop: интервал фазы 1 — 100 тиков, фазы 2 — 40. В обоих случаях первый колбэк планируется без задержки. Для каждой атаки всё равно должна завершиться перезарядка phase_attack; смена фазы не очищает старую перезарядку, поэтому колбэк может пропустить атаку. Пример не проверяет глобальную перезарядку. Фаза 1 передаёт 3 пользовательского урона игрокам в сфере радиусом 5 блоков и воспроизводит анимацию и частицы без ожидания длительности анимации.
  3. Проверка фазы в on_game_tick -- Вне боя on_game_tick сразу завершает работу, затем проверяет перезарядку в 20 тиков ("phase_check"). Если босс уже в фазе 2, обработчик также завершается досрочно.
  4. Порог здоровья -- context.boss.health / context.boss.maximum_health даёт долю текущего здоровья. При 50% или ниже начинается переход.
  5. Переход фазы -- Старый цикл отменяется, воспроизводится анимация трансформации, ближайшие игроки получают сообщение и заголовок, запускается новый более быстрый цикл (каждые 40 тиков).
  6. Атаки фазы 2 -- phase_two_attack использует сферу радиусом 8 блоков и передаёт один удар на 2 урона каждому найденному игроку за успешный колбэк. Он пытается применить замедление II на 40 тиков (amplifier = 1) с описанными выше проверками участия для эффектов, затем показывает красную пыль и воспроизводит звук визера. Несколько ударов внутри одного колбэка не планируются.
  7. Логирование -- context.log:info(...) пишет в серверную консоль.
  8. Очистка -- on_exit_combat отменяет любой активный цикл атак.
Держите on_game_tick лёгким

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


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

Для надёжной генерации Lua-способностей включите в промпт:

  • Точное имя хука — например, on_player_damaged_by_boss, а не «когда босс ударяет игрока».
  • Нативные Lua-зоны или скриптовые утилиты — укажите вариант. Нативные зоны используют context.zones:get_entities_in_zone(zone_def, opts) с kind, radius, origin. Скриптовые утилиты используют context.script:zone(...) с полями EliteScript, такими как shape, Target, Target2.
  • Локальные и глобальные перезарядки — укажите ключ с префиксом способности и длительность в тиках. context.cooldowns:check_local(key, ticks) проверяет и устанавливает именованную перезарядку босса. Для координации с глобальной перезарядкой явно проверяйте global_ready() до расходования локального ключа и задавайте глобальную длительность до эффектов.
  • Имена анимаций пользовательской модели — например, context.boss:play_model_animation("slam"). ИИ не может угадать имена анимаций; сообщите их.
  • Выбор цели — укажите context.player (игрок события), context.players.nearby_players(range) или запрос зоны.
  • Тип эффекта — урон (deal_custom_damage), зелье (apply_potion_effect), огонь (set_fire_ticks), скорость (set_velocity_vector, apply_push_vector) и т. д.
  • Методы правильной обёртки — предоставьте подходящий справочник API босса или общего API и не придумывайте псевдонимы.
  • Поля EliteScript для скриптовых утилит — targetType, shape, Target, Target2, range, offset, coverage, а не имена в стиле Lua.
  • Время и контекст планировщика — укажите, нужен ли первый колбэк сразу или с задержкой. У run_every начальная задержка нулевая; run_repeating принимает её отдельно. Используйте свежий контекст колбэка: в нём нет исходного игрока или события. Явно сохраните UUID игрока или позицию, если они понадобятся позже.

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

Напишите Lua-способность, которая в on_enter_combat запускает повторяющуюся задачу с начальной задержкой 80 тиков и интервалом 80 тиков. В каждом колбэке создайте нативную Lua-сферу радиусом 6 с центром на боссе, найдите игроков и передайте каждому 2.0 пользовательского урона. Проверьте глобальную перезарядку перед проверкой и установкой локального ключа "my_power:pulse" на 80 тиков, затем задайте глобальную перезарядку 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​

Проверьте Lua-способность по этому списку перед развёртыванием:

  1. Файл возвращает ровно одну таблицу с api_version = 1.
  2. Имена хуков точно соответствуют списку хуков, например on_player_damaged_by_boss, а не on_player_hit.
  3. В хуках, где игрок может отсутствовать, context.player проверяется через == nil перед использованием.
  4. Поля context.state инициализированы в on_spawn.
  5. Каждая повторяющаяся задача, нужная только в бою, созданная через run_every или run_repeating, отменяется в on_exit_combat; её начальная задержка выбрана намеренно.
  6. Колбэки планировщика используют собственный контекст и не предполагают наличие исходного игрока или события.
  7. Ключи перезарядки содержат префикс имени способности, длительности указаны в тиках. Глобальная координация включает явную проверку готовности.
  8. Хуки on_game_tick защищают тяжёлую работу проверкой перезарядки.
  9. Все имена методов существуют в Справочнике API; выдуманных псевдонимов нет.
  10. Таблицы скриптовых утилит используют поля EliteScript (targetType, shape, Target и т. д.), а не имена в стиле Lua.
  11. Нативные определения зон используют kind, radius, origin, destination и т.д.
  12. Способность не вызывает блокирующих или длительных операций внутри хуков и колбэков.
  13. Настройки частиц используют допустимые имена перечислений Bukkit в UPPER_CASE, например "FLAME", "DUST", "EXPLOSION".
  14. Перезагрузите изменённые файлы, создайте нового босса и проверьте видимое поведение и консоль на тестовом сервере. Успешный разбор файла сам по себе не доказывает работоспособность атаки.

Лучшие практики​

  • Начните с маленького хука и проверьте. Напишите один on_spawn, отправляющий сообщение в лог. Подтвердите, что он срабатывает. Затем стройте дальше.

  • Держите вспомогательные функции локальными. Объявляйте функции вроде local function pick_action(context) над возвращаемой таблицей, чтобы зависимости оставались явными. Каждый экземпляр способности уже имеет собственный Lua-рантайм.

  • Размещайте геометрию в скриптовых утилитах. Для конусов, вращающихся и перемещающихся лучей или анимированных зон используйте context.script:zone(...) с полями EliteScript. Эти утилиты используют движок зон EliteScript.

  • Используйте context.state для состояния выполнения. Оно принадлежит одному рантайму способности на одном боссе и сохраняется между хуками этого рантайма. С другими способностями оно не разделяется и после перезагрузки не сохраняется.

  • Используйте именованные локальные ключи перезарядки. Вместо простых чисел выбирайте понятные ключи вроде "fire_touch" или "aoe_pulse". Явные ключи общие для всех способностей одного босса. Добавляйте префикс способности, например "my_power:fire_touch", чтобы избежать конфликтов.

  • Держите on_game_tick лёгким. Ограничивайте тяжёлые операции перезарядкой и оставляйте большой запас до бюджета выполнения.

  • Отменяйте повторяющиеся задачи по завершении. Боевые циклы отменяйте в on_exit_combat. Задачи рантайма автоматически отменяются при его закрытии, но после окончания боя продолжаются без явной отмены.

  • Используйте свежие контексты колбэков планировщика. run_every, run_repeating и run_after передают свежий контекст без события или игрока, запланировавшего вызов. Используйте этот аргумент и явно сохраняйте необходимые UUID или позиции: внешний контекст может содержать устаревшие снимки.

  • Записывайте переходы состояния через context.log:info(). Во время разработки записывайте смену фаз, начало перезарядок и запуск или остановку планировщика. Перед развёртыванием удалите частые сообщения: context.log:debug() босса тоже пишет на уровне info, только с другим префиксом.

  • Используйте подходящие справочники EliteScript. Зоны, Цели и Относительные векторы описывают общие парсеры. YAML-условия, ожидания и кэширование на уровне действий не выполняются автоматически отдельными Lua-утилитами; выразите проверки в Lua. Наследуемые цели и позиции приземления требуют контекста YAML-действия, которого у этих дескрипторов нет.


Распространённые ошибки новичков​

  • Использование внешнего 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. Уточняйте, какие хуки предоставляют игрока. В запланированных колбэках его по умолчанию нет; отсутствие проверки может закрыть рантайм способности.

  • Используют имена полей в стиле Lua в скриптовых утилитах. Используйте документированные поля EliteScript: targetType, Target, Target2, shape. Варианты с подчёркиваниями, такие как target_type и zone_shape, не поддерживаются. Парсер зон принимает строчные target и target2, но это не делает допустимыми произвольные псевдонимы в стиле Lua.

  • Выполняют тяжёлую логику в on_game_tick без перезарядки. Поиск сущностей во всём мире и плотная геометрия частиц на множестве боссов дороги. Ограничивайте частоту этой работы и соблюдайте бюджет каждого колбэка.

  • Смешивают API обёрток. Специальные обёртки боссов EliteMobs используют entity:teleport_to_location(loc) и player:set_velocity_vector(vec). У общих обёрток MagmaCore есть дополнительные псевдонимы. Прежде чем брать метод из примера другого плагина, проверьте справочник собственного контекста.

  • Используют context.boss.health для установки здоровья. context.boss.health — это снимок только для чтения. Чтобы исцелить босса, используйте context.boss:restore_health(amount).

  • Забывают api_version = 1. Возвращаемая таблица должна включать это поле, иначе EliteMobs не загрузит способность.


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