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

Уровни и карты EternalTD

«Уровень» в EternalTD — это YAML-конфигурация в plugins/EternalTD/levels/ в паре с папкой шаблона мира в plugins/EternalTD/worlds/. Когда игрок присоединяется, EternalTD клонирует шаблонный мир в контейнер миров сервера и запускает сессию в этой клонированной копии.

Поля конфигурации уровня

ПолеТипПо умолчаниюПримечания
isEnabledbooltrueОтключённые уровни пропускаются при загрузке
levelNamestringnullОтображаемое имя, показываемое в сообщениях, таблицах и меню NPC
levelDescriptionсписок строк[]Строки, показываемые в меню NPC. Поддерживает плейсхолдеры $highscoreWave и $highscorePlayer
worldNamestringnullИмя папки шаблона в plugins/EternalTD/worlds/
startLocationсписок строкnullСписок сериализованных локаций, где спавнятся мобы
endLocationсписок строкnullСписок сериализованных локаций, к которым идут мобы («красные» плитки)
levelLocationsсписок строкnullКаждый проходимый квадрат сетки на уровне, генерируется автоматически при регистрации выбора этажа
wavesConfigFilestringобязательноИмя файла связанной конфигурации волны (waves/<name>.yml)
waveCountint-1Кэшированное количество волн, в настоящее время информационное
highscoreWaveint0Лучшая волна, достигнутая на этом уровне
highscorePlayerNamestring"no one"Отображаемое имя игрока, установившего рекорд
environmentenumNORMALОкружение мира, используемое при загрузке клонированного мира

Размер сетки, используемый повсюду, — 3 блока на логический квадрат (константа GRID_SIZE в коде).

Формат строки локации

Локации в EternalTD сериализуются как строки, разделённые запятыми, вида:

worldName,x,y,z,yaw,pitch

Обычно вы не пишете их вручную — /etd selectfloor плюс команды регистрации вычисляют и сохраняют их за вас.

Жизненный цикл мира

Когда игрок присоединяется к уровню:

  1. EternalTD проверяет, что у этого игрока ещё нет копирования в процессе. Второй /etd join, пока первый ещё копируется, отклоняется сообщением «Your level is already being prepared.».
  2. Он ищет папку шаблона по worldName в plugins/EternalTD/worlds/ и отклоняет вход, если папка мира отсутствует, связанный файл волн не загрузился или имя мира не является безопасным именем папки.
  3. Он резервирует следующий свободный числовой суффикс (<worldName>_0, <worldName>_1, ...). Резервирование синхронизировано и запоминает имена, которые зарезервированы, но ещё не появились на диске, поэтому два игрока, входящих одновременно, не могут получить одно и то же имя инстанса.
  4. Шаблон копируется в контейнер миров сервера вне основного потока, поэтому сервер не подвисает на большой карте.
  5. Клонированный мир загружается как временный пустой мир через TemporaryWorldManager из MagmaCore, поэтому миграция Paper 26.1+ изолируется, а отсутствующие чанки возвращаются как пустота, а не как заново сгенерированный рельеф.
  6. Игрок телепортируется в новый мир; внутренний 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) ничего не делает.

Правила защиты инстанса

Пока уровень активен, в клонированном мире применяются следующие правила:

  • Взрывы отключены
  • Течение жидкостей отключено
  • Элитры отключены
  • Переключение полёта запрещено
  • Дружественный огонь запрещён
  • Стандартный спавн мобов запрещён

Рабочий процесс создания карт

Текущий процесс создания карт использует игровой инструментарий:

  1. Поместите папку шаблона мира в plugins/EternalTD/worlds/<worldName>/.
  2. Создайте или загрузите соответствующий YAML уровня в plugins/EternalTD/levels/.
  3. Выполните /etd reload и присоединитесь к миру уровня вручную (или откройте его в одиночной игре для настройки).
  4. Используйте /etd selectfloor и кликните правой/левой кнопкой по двум углам, чтобы отметить игровую область, или используйте /etd selectfloorcoordinates <x1> <y1> <z1> <x2> <y2> <z2>, чтобы задать их напрямую.
  5. Выполните /etd showselection <level>, чтобы убедиться, что выбор выглядит правильно.
  6. Выполните /etd register <level>, чтобы очистить выбор. Обратите внимание, что в текущей сборке ни register, ни showselection фактически не сохраняют область этажа — помощник, который должен сохранять levelLocations (LevelsConfigFields#addLevelLocations), определён, но ни одна команда его не вызывает. На данный момент levelLocations приходится вручную записывать в YAML уровня, если они ещё не заполнены загруженным пакетом.
  7. Встаньте на стартовую плитку спавна и выполните /etd register <level> start. Повторите для каждой стартовой плитки (эта команда действительно сохраняется в startLocation).
  8. Встаньте на конечную плитку и выполните /etd register <level> end. Повторите для каждой конечной плитки (эта команда действительно сохраняется в endLocation).
  9. Перезагрузите снова и протестируйте уровень, присоединившись к нему через меню NPC или /etd join <level>.

Команды выбора генерируют квадраты сетки по формуле:

size = abs(corner1 - corner2 + 1) / 3

Квадраты, верхний блок которых — воздух (или чей блок пола проходим), пропускаются, поэтому блоки пола должны быть твёрдыми, чтобы квадрат регистрировался как играбельный.

Проверка пути

EternalTD кеширует для каждой стартовой плитки дешевейший путь A* до какой-либо конечной плитки — один наземный и один воздушный. Кеш перестраивается при старте сессии, при установке башни и при продаже башни.

Воздушные враги используют отдельный воздушный путь, который полностью игнорирует блокировку башнями и вместо этого следует воздушному смещению (4 блока над настроенным путём).

Кеш также решает, играбелен ли уровень вообще: если у какой-либо стартовой плитки отсутствует наземный или воздушный путь, сессия отклоняется на этапе входа, вместо того чтобы начинать забег, который враги не смогут закончить. Та же проверка выполняется при установке башни, поэтому башня, которая перекрыла бы стартовую плитку, отклоняется, и золото не тратится.

Стартовая или конечная локация, не находящаяся в центре своего квадрата сетки 3x3, отбрасывается с предупреждением в консоли — это обычная причина, по которой уровень сообщает о неполном наборе путей.

NPC и меню уровней

Конфигурации NPC в plugins/EternalTD/npcs/ связывают NPC-жителей с одним или несколькими уровнями. Клик правой кнопкой мыши по NPC открывает инвентарь из 9 слотов, в котором каждый уровень показан как панель из зелёного окрашенного стекла с названием и описанием уровня.

ПолеТипПо умолчаниюПримечания
isEnabledbooltrueОтключённые NPC пропускаются
levelIDsсписок строкобязательноИмена файлов уровней, предлагаемых этим NPC
locationstringnullЛокация спавна в стандартном формате worldName,x,y,z,yaw,pitch
namestring"Default Name"Отображаемое имя NPC
difficultystring"Difficulty: Not Set"Метка сложности, показываемая над NPC
disguisestringnullДескриптор LibsDisguises (например, custom:etd_tutorial_npc)
customDisguiseDatastringnullДополнительные данные команды 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 ещё инициализировался, переносятся в ту же точку появления после завершения инициализации, если только они уже не находятся в мире-хабе.