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

Lua-скриптинг: Скрипты NPC

webapp_banner.jpg

Lua-скрипты NPC в EliteMobs -- это автономные файлы .lua, которые прикрепляются к конфигам NPC. Они отделены от Lua-пауэров боссов: пауэры боссов лежат в plugins/EliteMobs/powers/, а скрипты NPC -- в plugins/EliteMobs/npc_scripts/.

Скрипты NPC используют общую среду выполнения MagmaCore вместе с пауэрами боссов и пропами FreeMinecraftModels. Они получают общие сервисы context.world, context.zones, context.scheduler, context.cooldowns и context.log, а также таблицу context.npc для NPC. Данные игрока и события зависят от хука. У пауэров боссов есть собственные варианты нескольких таблиц, поэтому в скриптах NPC используйте сигнатуры общего API.

Экспериментальная функция

Lua-скрипты NPC всё ещё экспериментальны. Специфичные для NPC хуки и помощники context.npc могут измениться. Общие таблицы (context.world, context.zones, context.scheduler, context.cooldowns, context.log, context.event, context.player) -- это те же самые, что задокументированы в разделах Движок скриптинга и Справочник Lua API.


Расположение файлов​

Создавайте файлы скриптов NPC в:

plugins/
EliteMobs/
npc_scripts/
wave.lua

Подпапки сканируются рекурсивно. Однако скрипты регистрируются только по имени файла, поэтому npc_scripts/wave.lua и npc_scripts/town/wave.lua конфликтуют -- держите базовые имена уникальными по всему дереву.

При обнаружении из каждого набора совпадающих имён без учёта регистра выбирается один файл в стабильном порядке абсолютных путей. Если копии различаются, выбранный и пропущенные пути записываются в лог. Ошибка в выбранном файле не приводит к загрузке пропущенного дубликата. При этом ссылки в конфиге должны точно совпадать с регистром выбранного имени.

Расширение .lua в конфиге NPC необязательно: и - wave, и - wave.lua разрешаются в wave.lua. Если конфиг NPC ссылается на несуществующий скрипт, EliteMobs выводит предупреждение, а NPC всё равно появляется.


Прикрепление скриптов к NPC​

Добавьте список scripts: в конфиг NPC:

scripts:
- wave.lua

К одному NPC можно прикрепить несколько скриптов:

scripts:
- wave.lua
- greeting_particles.lua

Скрипты выполняются в порядке приоритета: меньшие значения priority идут первыми, значение по умолчанию — 0. При равном приоритете сохраняется порядок списка scripts:. Это порядок обработки событий NPC, а не независимых задач планировщика.


Структура скрипта​

Каждый скрипт NPC должен возвращать одну таблицу:

return {
api_version = 1,
priority = 0,

on_spawn = function(context)
context.state.spawned = true
context.npc:play_model_animation("idle")
end
}

Принимаются только эти поля верхнего уровня:

ПолеТипПримечания
api_versionnumberОбязательное. Должно быть 1.
prioritynumberНеобязательное. Меньшие значения выполняются первыми.
on_spawnfunctionВыполняется после появления NPC.
on_removefunctionВыполняется при обычном удалении NPC, до закрытия скриптов и удаления сущности.
on_game_tickfunctionВыполняется каждый тик сервера после инициализации среды скрипта и запуска её таймера. Обработчик должен быть лёгким.
on_npc_interactfunctionВыполняется, когда обычный обработчик взаимодействия доходит до шага скриптов; ограничения описаны ниже.
on_npc_proximity_enterfunctionВыполняется один раз, когда игрок входит в радиус активации этого NPC.
on_npc_proximity_leavefunctionВыполняется один раз, когда игрок покидает радиус активации этого NPC.
on_zone_enterfunctionВыполняется, когда игрок входит в зону, за которой следит этот скрипт (см. context.zones).
on_zone_leavefunctionВыполняется, когда игрок покидает отслеживаемую зону.

Отслеживание зон учитывает только игроков -- мобы и другие сущности никогда не вызывают on_zone_enter / on_zone_leave.

Неизвестные ключи верхнего уровня отклоняются при загрузке скрипта. Вспомогательные функции следует объявлять как local-функции выше возвращаемой таблицы.

Загрузчик выполняет код на уровне файла для проверки, затем каждый экземпляр скрипта NPC выполняет его снова в собственной песочнице. Не используйте контекст хука при инициализации на уровне файла. Глобальные переменные и context.state отдельны у каждого скрипта, в том числе у скриптов одного NPC.

Среда NPC инициализируется лениво. Сейчас хост NPC создаёт экземпляры и отправляет on_spawn, но не вызывает метод немедленной инициализации start(). Поэтому файл, содержащий только on_game_tick, сам не начинает тикать. Добавьте on_spawn = function(context) end, чтобы инициализировать его при появлении, либо дождитесь другого объявленного хука события.

Ошибка Lua закрывает затронутый экземпляр скрипта и отменяет принадлежащие ему задачи. Последующие события NPC отбрасывают закрытый экземпляр, а не создают его заново. Перезагрузите исправленное определение и создайте новый экземпляр скрипта NPC. Состояние не сохраняется после удаления или перезапуска; on_remove — хук удаления NPC, а не универсальный обработчик ошибок или завершения.


Хуки близости​

Хуки близости NPC используют значение конфига activationRadius этого NPC.

ХукКогда срабатывает
on_npc_proximity_enterИгрок перемещается из-за пределов радиуса активации NPC внутрь него.
on_npc_proximity_leaveИгрок перемещается изнутри радиуса активации NPC за его пределы.

Серверный сканер проверяет каждую пару NPC/игрок каждые 100 тиков, обычно раз в пять секунд при 20 TPS, по сферическому расстоянию от текущей сущности NPC. Если игрок уже внутри при первой проверке, это считается входом. Короткое посещение между проверками может остаться незамеченным. Близость к одному NPC не мешает событию входа другого, а непрерывное обнаружение внутри не вызывает повторных входов.

NPC, которые сейчас движутся по патрулю или редактируются, пропускаются. Поэтому ранее обнаруженная пара может дать событие выхода при начале движения и входа при возобновлении обнаружения. Удаление NPC и выход игрока с сервера не гарантируют хук выхода: для отправки нужны и отслеживаемый NPC, и игрок онлайн. Пустой набор NPC или игроков онлайн очищает отслеживание без событий выхода. activationRadius не выше нуля отключает активацию по близости; отдельный радиус маркера квестодателя в 30 блоков не расширяет активацию Lua.

Обычное поведение приветствий, диалогов и индикаторов заданий по-прежнему работает. Lua-хук добавляет поведение поверх него.


Общий интерфейс скриптов​

Поскольку скрипты NPC работают в единой среде выполнения, каждый хук NPC также получает общие таблицы контекста MagmaCore, используемые скриптами FreeMinecraftModels. Пауэры боссов EliteMobs работают в той же среде выполнения, но используют специфичные для боссов варианты нескольких таблиц. Полные списки методов находятся в Справочнике Lua API и Движке скриптинга:

ТаблицаЧто она делает
context.worldЭффекты и запросы мира: strike_lightning, spawn_particle, play_sound, set_block_at, place_temporary_block, spawn_entity, spawn_firework, get_nearby_entities, get_nearby_players, raycast и другие. Принимаются обе формы -- координатная (strike_lightning(x, y, z)) и с таблицей локации (strike_lightning_at_location(loc)).
context.zonesСоздаёт зоны через create_sphere(x, y, z, radius), create_cylinder(x, y, z, radius, height) или create_cuboid(x, y, z, xSize, ySize, zSize). Возвращает числовой дескриптор либо nil, если у владельца нет мира. watch(handle, on_enter, on_leave) включает соответствующие хуки верхнего уровня, если передан аргумент-функция; сама переданная функция не вызывается. unwatch(handle) удаляет зону и прекращает отслеживание.
context.schedulerrun_later(ticks, fn), run_repeating(delay, interval, fn), cancel(task_id).
context.cooldownsОбщие кулдауны MagmaCore: local_ready, local_remaining, check_local, set_local, global_ready, set_global.
context.loginfo(msg), warn(msg), error(msg) — пишет в консоль сервера.
context.eventДанные события или действующего лица, если хук их предоставляет. См. ниже.
context.playerВзаимодействующий/вызвавший срабатывание игрок, если он есть. См. ниже.
context.stateОбычная Lua-таблица, которая сохраняется для этого экземпляра скрипта NPC до удаления NPC.

Общие зоны используют абсолютные координаты в мире NPC на момент создания и не следуют за его перемещением. Создание сразу регистрирует ежетиковую проверку игроков в текущем мире владельца, даже до установки обработки входа/выхода через watch. Передайте функцию для каждого нужного направления и реализуйте соответствующий on_zone_enter или on_zone_leave в возвращаемой таблице скрипта. Эти хуки получают context.player и таблицу события с действующим лицом, но без идентификатора зоны. unwatch полностью удаляет дескриптор; для повторного отслеживания создайте другую зону. Игроки внутри при первой проверке считаются вошедшими, а игрок офлайн не может получить коллбэк выхода.

Пример: удар молнией при взаимодействии​

return {
api_version = 1,

on_npc_interact = function(context)
-- NPC scripts can now reach the full world API.
context.world:strike_lightning_at_location(context.npc:get_location())
end
}

context.npc​

context.npc доступен в каждом хуке NPC.

Поля​

ПолеТипПримечания
namestringОтображаемое имя NPC из конфига.
filenamestringИмя файла конфига NPC.
uuidstringUUID NPC во время выполнения.
activation_radiusnumberНастроенный радиус активации.
current_locationтаблица локацииСнимок локации, когда стоящая за NPC сущность существует.
entity_typestringТип сущности Bukkit, когда стоящая за NPC сущность существует.

Методы​

МетодАргументыВозвращаетПримечания
is_valid()-booleanЕсть ли у NPC всё ещё валидная стоящая за ним сущность.
get_location()-таблица локацииТекущая локация NPC или локация спавна, если сущность недоступна.
get_eye_location()-таблица локацииТекущая локация глаз или запасной вариант -- локация спавна.
get_activation_radius()-numberТекущий настроенный радиус активации.
get_nearby_players(radius)numbertableОбёртки игроков в сферическом радиусе в мире NPC. Режим игры не фильтруется; используйте неотрицательный радиус.
face_direction_or_location(target)вектор, локация или таблица сущностиnilПоворачивает в направлении или к позиции в том же мире. Таблица с world или current_location считается позицией, а одни координаты — направлением. Ничего не делает во время активного движения патруля.
say_greeting(player?)player, UUID, имя или nilnilОтправляет настроенное приветствие. По умолчанию -- вызвавшему срабатывание игроку, если он доступен.
say_dialog(player?)player, UUID, имя или nilnilОтправляет настроенный диалог. По умолчанию -- вызвавшему срабатывание игроку, если он доступен.
say_farewell(player?)player, UUID, имя или nilnilОтправляет настроенный текст прощания. По умолчанию -- вызвавшему срабатывание игроку, если он доступен.
play_model_animation(name)stringnilВоспроизводит анимацию кастомной модели, если она существует. Иначе безопасно ничего не делает.
patrol_pause()-booleanПриостанавливает настроенный патруль.
patrol_resume()-booleanЗавершает ожидание или временное движение и возобновляет патруль.
walk_to(x, y, z)три числаbooleanИдёт к смещению от заданной автором точки появления, затем возобновляет патруль. Длинные маршруты рассчитываются автоматически.
hold(x, y, z)три числаbooleanИдёт к смещению от заданной исходной точки и остаётся там.
teleport(x, y, z)три числаbooleanТелепортируется к смещению от заданной исходной точки, если там обрабатываются тики сущностей.
remove()-nilУдаляет текущую сущность NPC обычным путём с причиной OTHER, не удаляя заданное размещение. Не вызывайте из on_remove: это повторно запустит удаление.

Методы движения возвращают false, если у NPC нет настроенного контроллера патруля или запрос нельзя принять. Настройка маршрутов и дальние перемещения описаны в Патрулях NPC и боссов.


context.player​

context.player доступен в on_npc_interact, on_npc_proximity_enter, on_npc_proximity_leave и хуках входа/выхода отслеживаемых зон. Он равен nil в хуках жизненного цикла, не связанных с игроком.

Это общая обёртка игрока MagmaCore, которую также используют скрипты FMM. Она предоставляет здоровье, эффекты зелий, send_message, show_title, show_action_bar, get_held_item, рейкастинг и другое. У пауэров боссов собственная обёртка игрока, поэтому её методы не взаимозаменяемы с этими. Полный общий список см. в Справочнике Lua API. Часто используемые поля и методы:

Поле / МетодПримечания
nameИмя игрока.
uuidUUID игрока.
current_locationПозиция игрока читается заново при каждом обращении к полю. Сохранённая ранее таблица позиции остаётся снимком прежнего состояния.
get_eye_location()Текущая локация глаз игрока.
send_message(text)Отправляет сообщение в чат. Поддерживает цветовые коды.

Всегда проверяйте context.player на nil перед использованием в общих вспомогательных функциях.

Здесь entity_type в нижнем регистре

В общих таблицах сущностей MagmaCore entity_type — это имя Bukkit в нижнем регистре ("player", "zombie"). Только context.npc.entity_type и таблицы сущностей в пауэрах боссов EliteMobs используют форму в верхнем регистре. Если скрипту нужно работать с обоими вариантами, сравнивайте без учёта регистра.

Поля EliteMobs, добавляемые в каждую общую таблицу сущности​

Пока EliteMobs запущен, он добавляет поля в общие таблицы сущностей MagmaCore, включая context.player, обёртки из context.npc:get_nearby_players(...) и обёртки скриптов пропов FreeMinecraftModels:

ПолеТипПримечания
is_elitebooleantrue, если EliteMobs отслеживает сущность как элиту
is_custom_bossbooleantrue, если это Кастомный босс (всегда false, когда is_elite равно false)
is_significant_bossbooleantrue для кастомного босса с множителем здоровья выше 1. Не проверяет, был ли он призван как подкрепление.
elitetable или nilПрисутствует только у элит. См. ниже
elite_playertable или nilВ общих обёртках игрока, созданных EliteMobs, предоставляет tier, если элитный инвентарь игрока загружен. Каждое обращение создаёт новую таблицу; сохранённая таблица сохраняет прежний tier.
can_receive_hostile_effect(actor_uuid)booleanПроверяет игрока-инициатора по общим правилам враждебных целей и разрешениям магического оружия EliteMobs. Передайте строку с корректным UUID.

Подтаблица elite:

Поле / МетодТипПримечания
elite.levelnumberУровень элиты
elite.namestring или nilОтображаемое имя элиты
elite.healthnumberТекущее здоровье элиты (читается в реальном времени)
elite.max_healthnumberМаксимальное здоровье элиты (читается в реальном времени)
elite.is_custom_bossbooleanТо же значение, что и у поля верхнего уровня
elite.health_multipliernumberНастроенный множитель здоровья
elite.damage_multipliernumberНастроенный множитель урона
elite:remove()—Убирает элиту из мира
-- Warn the approaching player if a real boss is loose near this NPC
on_npc_proximity_enter = function(context)
if context.player == nil then return end
local here = context.npc:get_location()
local nearby = context.world:get_nearby_entities(here.x, here.y, here.z, 40)
for i = 1, #nearby do
if nearby[i].is_significant_boss then
context.player:send_message("&cA boss is nearby: " .. tostring(nearby[i].elite.name))
return
end
end
end

Обёртки пауэров боссов EliteMobs создаются другим построителем и имеют иной набор полей. Некоторые имена совпадают, например is_elite, но общая вложенная таблица elite не взаимозаменяема с методами пауэров боссов. Их набор см. в Боссы и сущности. FMM предоставляет собственный адаптер EliteMobs; не считайте, что каждое дополнительное поле общей обёртки, созданной EliteMobs, существует и в FMM.


context.event​

context.event равен nil, только если у хука нет ни события, ни действующего лица. Это общая таблица событий MagmaCore:

Поле / МетодПримечания
is_cancelledСнимок состояния отмены при создании таблицы; false для неотменяемых событий. Не обновляется после cancel() или uncancel().
cancel()Есть только для отменяемого базового события. Отменяет его.
uncancel()Есть только для отменяемого базового события. Снимает отмену.
playerДействующее лицо события (например, взаимодействующий игрок) в виде обёртки игрока, если оно есть.

Для взаимодействующего игрока или игрока, вошедшего в радиус, предпочтительнее context.player (он установлен для этих хуков).

Сейчас on_npc_interact передаёт игрока без события Bukkit. Поэтому таблица события содержит player и is_cancelled: false, но не методы cancel или uncancel. Скрипт взаимодействия с NPC не может отменить обычное меню NPC через эту таблицу.

Обработка взаимодействий игнорирует уже отменённые события Bukkit, блокирует использование бирок и взаимодействие с редактируемыми NPC, а также подавляет повторные взаимодействия одного игрока в течение тика. Если цель задания с диалогом обрабатывает взаимодействие, выполнение возвращается до Lua-хука. Иначе хук запускается до обычного меню/действия NPC. События близости также неотменяемые; хуки зон передают действующее лицо без события Bukkit. Эти пути тоже не предоставляют cancel() или uncancel().


Состояние, планировщик и кулдауны​

context.state принадлежит отдельному экземпляру скрипта NPC и существует до его завершения. Она отделена от состояния других скриптов того же NPC.

context.scheduler -- это общий планировщик MagmaCore. Работают как имена MagmaCore, так и имена EliteMobs run_after / run_every -- это псевдонимы для одного и того же поведения:

МетодАргументыПримечания
run_later(ticks, callback) / run_after(ticks, callback)number, functionВыполняется один раз после задержки. Возвращает ID задачи.
run_repeating(delay, interval, callback)number, number, functionВыполняется повторно после начальной задержки. Возвращает ID задачи.
run_every(interval, callback)number, functionВыполняется каждые interval тиков (начальная задержка 0). Возвращает ID задачи.
cancel(task_id) / cancel_task(task_id)numberОтменяет принадлежащую скрипту задачу.

Коллбэки планировщика получают свежий контекст. Они не получают исходные context.player или context.event. Все принадлежащие скрипту задачи автоматически отменяются при удалении NPC.

context.cooldowns -- это общая таблица кулдаунов MagmaCore:

МетодАргументыВозвращаетПримечания
local_ready(key?)stringbooleanTrue, когда локальный кулдаун истёк.
local_remaining(key?)stringnumberОставшиеся тики или 0, когда готово.
check_local(key?, duration)string, numberbooleanЕсли готово, запускает кулдаун и возвращает true.
set_local(duration, key?)number, stringnilУстанавливает или сбрасывает кулдаун.
global_ready()-booleanTrue, когда общий глобальный кулдаун готов.
set_global(duration)numbernilЗапускает глобальный кулдаун.

У check_local длительность всегда второй аргумент: для ключа по умолчанию используйте check_local(nil, 60). У set_local, наоборот, длительность идёт первой. Ключ по умолчанию включает имя файла скрипта. Явно заданные локальные ключи и хранилище глобального кулдауна общие для скриптов одного адаптера NPC; используйте разные ключи, если не нужна совместная работа. Длительность измеряется монотонным прошедшим временем из расчёта 50 мс на номинальный тик, а не числом серверных тиков. Неположительная длительность снимает кулдаун. Установка глобального кулдауна не блокирует автоматически хуки или check_local; проверяйте global_ready() самостоятельно.

Единый API кулдаунов

Скрипты NPC теперь используют общий порядок аргументов кулдаунов MagmaCore (check_local(key?, duration)), такой же, как у пауэров боссов и скриптов FreeMinecraftModels. Более ранние экспериментальные сборки NPC использовали check_local(duration, key?) — обновите старые скрипты под общий порядок.


Пример: приветственный жест при приближении​

Этот скрипт поворачивает NPC к игроку, чей вход обнаружен, и воспроизводит анимацию кастомной модели wave. Вход определяется проверками раз в пять секунд, а не каждым пакетом движения. Кулдаун с ключом ограничивает повторы анимации при повторных уведомлениях о входе, но не повышает частоту сканирования.

return {
api_version = 1,
priority = 0,

on_npc_proximity_enter = function(context)
if context.player == nil then return end

if context.cooldowns:check_local("wave:" .. context.player.uuid, 60) then
context.npc:face_direction_or_location(context.player.current_location)
context.npc:play_model_animation("wave")
end
end
}

play_model_animation(name) безопасно ничего не делает, когда у NPC нет кастомной модели или в модели нет такой анимации.


Рекомендации по производительности​

  • Держите хуки on_game_tick небольшими. После инициализации их таймер работает каждый тик сервера, обычно 20 раз в секунду. Скрипт без этого хука всё равно может выполнять ежетиковые проверки зон или явно запланированные задачи.
  • Для поведения, зависящего от близости, предпочитайте on_npc_proximity_enter и on_npc_proximity_leave вместо опроса ближайших игроков каждый тик.
  • Используйте context.cooldowns:check_local(...), чтобы ограничивать анимации, звуки и всплески частиц.
  • Используйте context.scheduler:run_repeating(...) с разумным интервалом, когда поведению не нужно выполняться каждый тик.
  • Избегайте больших поисков в Lua. context.npc:get_nearby_players(radius) подходит для небольших локальных проверок, но широкое сканирование должно оставаться в среде выполнения плагина.