Уровни и карты EternalTD
«Уровень» в EternalTD — это YAML-конфигурация в plugins/EternalTD/levels/ в паре с папкой шаблона мира в plugins/EternalTD/worlds/. Когда игрок присоединяется, EternalTD клонирует шаблонный мир в контейнер миров сервера и запускает сессию в этой клонированной копии.
Поля конфигурации уровня
| Поле | Тип | По умолчанию | Примечания |
|---|---|---|---|
isEnabled | bool | true | Отключённые уровни пропускаются при загрузке |
levelName | string | null | Отображаемое имя, показываемое в сообщениях, таблицах и меню NPC |
levelDescription | список строк | [] | Строки, показываемые в меню NPC. Поддерживает плейсхолдеры $highscoreWave и $highscorePlayer |
worldName | string | null | Имя папки шаблона в plugins/EternalTD/worlds/ |
startLocation | список строк | null | Список сериализованных локаций, где спавнятся мобы |
endLocation | список строк | null | Список сериализованных локаций, к которым идут мобы («красные» плитки) |
levelLocations | список строк | null | Каждый проходимый квадрат сетки на уровне, генерируется автоматически при регистрации выбора этажа |
wavesConfigFile | string | обязательно | Имя файла связанной конфигурации волны (waves/<name>.yml) |
waveCount | int | -1 | Кэшированное количество волн, в настоящее время информационное |
highscoreWave | int | 0 | Лучшая волна, достигнутая на этом уровне |
highscorePlayerName | string | "no one" | Отображаемое имя игрока, установившего рекорд |
environment | enum | NORMAL | Окружение мира, используемое при загрузке клонированного мира |
Размер сетки, используемый повсюду, — 3 блока на логический квадрат (константа GRID_SIZE в коде).
Формат строки локации
Локации в EternalTD сериализуются как строки, разделённые запятыми, вида:
worldName,x,y,z,yaw,pitch
Обычно вы не пишете их вручную — /etd selectfloor плюс команды регистрации вычисляют и сохраняют их за вас.
Жизненный цикл мира
Когда игрок присоединяется к уровню:
- EternalTD проверяет, что у этого игрока ещё нет копирования в процессе. Второй
/etd join, пока первый ещё копируется, отклоняется сообщением «Your level is already being prepared.». - Он ищет папку шаблона по
worldNameвplugins/EternalTD/worlds/и отклоняет вход, если папка мира отсутствует, связанный файл волн не загрузился или имя мира не является безопасным именем папки. - Он резервирует следующий свободный числовой суффикс (
<worldName>_0,<worldName>_1, ...). Резервирование синхронизировано и запоминает имена, которые зарезервированы, но ещё не появились на диске, поэтому два игрока, входящих одновременно, не могут получить одно и то же имя инстанса. - Шаблон копируется в контейнер миров сервера вне основного потока, поэтому сервер не подвисает на большой карте.
- Клонированный мир загружается как временный пустой мир через
TemporaryWorldManagerиз MagmaCore, поэтому миграция Paper 26.1+ изолируется, а отсутствующие чанки возвращаются как пустота, а не как заново сгенерированный рельеф. - Игрок телепортируется в новый мир; внутренний
InstanceProtectorприменяет правила защиты EternalTD.
Требования к шаблону
Копирование проверяется до того, как что-либо будет записано, и шаблон отклоняется, если:
- это не каталог или это символическая ссылка
- в его корне нет
level.dat - он содержит символическую ссылку где-либо внутри
- он содержит файл с именем
.eternaltd-instance(это имя зарезервировано, см. ниже)
Неудачное копирование откатывается, и игроку сообщается, что уровень не был запущен.
Маркеры владения инстансом
Каждому клону в корень записывается файл .eternaltd-instance с именем шаблона и именем инстанса. EternalTD удалит папку мира, только если её маркер соответствует инстансу, который он собирается удалить. Именно эта защита не даёт устаревшему совпадению имён, созданной вручную папке или несоответствующей сессии снести мир, который EternalTD не создавал — если маркер отсутствует или не совпадает, папка остаётся на месте, а в лог пишется предупреждение.
Устаревшие инстансы, оставшиеся после падения, вычищаются при загрузке уровней, и даже тогда только те папки, которые несут корректный маркер и не загружены в данный момент.
Проверка запуска сессии
Сессия становится играбельной, только если выполняется всё перечисленное. Любая неудача прерывает забег, восстанавливает игрока (инвентарь, режим игры, полёт, скорборд) и удаляет временный мир:
levelLocations,startLocationиendLocationприсутствуют и непусты — иначе «This level doesn't have a complete play area, start, and end configuration!»- сама игровая зона разбирается в корректную сетку — иначе «This level's play area is invalid!»
- у каждой стартовой плитки есть и наземный, и воздушный путь к какой-либо конечной плитке — иначе «This level does not have complete land and air paths from every start to an end!»
Когда сессия заканчивается:
- Любые оставшиеся игроки в клонированном мире телепортируются обратно в точку появления из
config.ymlили кикаются, если точка появления не настроена. - Правила защиты мира снимаются, и все оставшиеся башни разбираются (башня, выбросившая исключение при удалении, логируется и пропускается, а не блокирует остальные).
- Клонированный мир выгружается и удаляется с диска (
TemporaryWorldManager.permanentlyDeleteWorld) — как в устаревшей, так и в мигрированной раскладке Paper 26.1+. - Завершение идемпотентно — повторный
end()(например,/etd quitсразу за которым/etd hub) ничего не делает.
Правила защиты инстанса
Пока уровень активен, в клонированном мире применяются следующие правила:
- Взрывы отключены
- Течение жидкостей отключено
- Элитры отключены
- Переключение полёта запрещено
- Дружественный огонь запрещён
- Стандартный спавн мобов запрещён
Рабочий процесс создания карт
Текущий процесс создания карт использует игровой инструментарий:
- Поместите папку шаблона мира в
plugins/EternalTD/worlds/<worldName>/. - Создайте или загрузите соответствующий YAML уровня в
plugins/EternalTD/levels/. - Выполните
/etd reloadи присоединитесь к миру уровня вручную (или откройте его в одиночной игре для настройки). - Используйте
/etd selectfloorи кликните правой/левой кнопкой по двум углам, чтобы отметить игровую область, или используйте/etd selectfloorcoordinates <x1> <y1> <z1> <x2> <y2> <z2>, чтобы задать их напрямую. - Выполните
/etd showselection <level>, чтобы убедиться, что выбор выглядит правильно. - Выполните
/etd register <level>, чтобы очистить выбор. Обратите внимание, что в текущей сборке ниregister, ниshowselectionфактически не сохраняют область этажа — помощник, который должен сохранятьlevelLocations(LevelsConfigFields#addLevelLocations), определён, но ни одна команда его не вызывает. На данный моментlevelLocationsприходится вручную записывать в YAML уровня, если они ещё не заполнены загруженным пакетом. - Встаньте на стартовую плитку спавна и выполните
/etd register <level> start. Повторите для каждой стартовой плитки (эта команда действительно сохраняется вstartLocation). - Встаньте на конечную плитку и выполните
/etd register <level> end. Повторите для каждой конечной плитки (эта команда действительно сохраняется вendLocation). - Перезагрузите снова и протестируйте уровень, присоединившись к нему через меню NPC или
/etd join <level>.
Команды выбора генерируют квадраты сетки по формуле:
size = abs(corner1 - corner2 + 1) / 3
Квадраты, верхний блок которых — воздух (или чей блок пола проходим), пропускаются, поэтому блоки пола должны быть твёрдыми, чтобы квадрат регистрировался как играбельный.
Проверка пути
EternalTD кеширует для каждой стартовой плитки дешевейший путь A* до какой-либо конечной плитки — один наземный и один воздушный. Кеш перестраивается при старте сессии, при установке башни и при продаже башни.
Воздушные враги используют отдельный воздушный путь, который полностью игнорирует блокировку башнями и вместо этого следует воздушному смещению (4 блока над настроенным путём).
Кеш также решает, играбелен ли уровень вообще: если у какой-либо стартовой плитки отсутствует наземный или воздушный путь, сессия отклоняется на этапе входа, вместо того чтобы начинать забег, который враги не смогут закончить. Та же проверка выполняется при установке башни, поэтому башня, которая перекрыла бы стартовую плитку, отклоняется, и золото не тратится.
Стартовая или конечная локация, не находящаяся в центре своего квадрата сетки 3x3, отбрасывается с предупреждением в консоли — это обычная причина, по которой уровень сообщает о неполном наборе путей.
NPC и меню уровней
Конфигурации NPC в plugins/EternalTD/npcs/ связывают NPC-жителей с одним или несколькими уровнями. Клик правой кнопкой мыши по NPC открывает инвентарь из 9 слотов, в котором каждый уровень показан как панель из зелёного окрашенного стекла с названием и описанием уровня.
| Поле | Тип | По умолчанию | Примечания |
|---|---|---|---|
isEnabled | bool | true | Отключённые NPC пропускаются |
levelIDs | список строк | обязательно | Имена файлов уровней, предлагаемых этим NPC |
location | string | null | Локация спавна в стандартном формате worldName,x,y,z,yaw,pitch |
name | string | "Default Name" | Отображаемое имя NPC |
difficulty | string | "Difficulty: Not Set" | Метка сложности, показываемая над NPC |
disguise | string | null | Дескриптор LibsDisguises (например, custom:etd_tutorial_npc) |
customDisguiseData | string | null | Дополнительные данные команды LibsDisguises — обычно длинная строка скина игрока |
Житель появляется неуязвимым, с отключённым AI, постоянным и помеченным namespace-ключом NPC EternalTD. Если установлен LibsDisguises и заданы оба поля disguise и customDisguiseData, житель маскируется при спавне.
Плавающая стойка для брони с меткой difficulty спавнится в 2.3 блоках над NPC.
Поведение точки появления
DefaultConfig управляет тем, как игроки обрабатываются в мире-хабе:
setupDone— флаг, отслеживающий, было ли завершено руководство первоначальной настройки (по умолчаниюfalse).spawnLocations— по умолчаниюetd_spawn,0,65,0,0,0. Используется только когда существует мирetd_spawn.manageSpawn— по умолчаниюtrue. Когда включено, присоединяющиеся игроки телепортируются в точку появления через 1 тик после входа.playerGuide— текст игрового руководства.nightbreak.autoDownloadPluginUpdates— общая настройка MagmaCore (по умолчаниюfalse). Когда включено, обновления плагина и контента загружаются автоматически при запуске.
Когда manageSpawn равно true и мир точки появления загружен, каждый игрок, присоединяющийся к серверу, телепортируется в spawnLocations. Игроки, которые были подключены, пока EternalTD ещё инициализировался, переносятся в ту же точку появления после завершения инициализации, если только они уже не находятся в мире-хабе.