squad-proto/docs/15-engine-and-editor.md
z.kirill 4382af2322 Документация: границы движка, работа через модель, дизайн игры до лейтгейма
18-engine.md    границы Tile2D, проекции 2D, формат проекта, второй проект
  19-agent.md     инструменты --tool, скиллы, терминал в редакторе
  20-gdd.md       акты, петли, рост отряда, закон масштаба, бюджет 200 часов
  21-damage.md    семь стихий, статусы, десять реакций, формулы, триггеры
  22-loot.md      редкости, аффиксы, артефакты, зачарования, материалы
  23-endgame.md   Бездна, мутации, лидерборды, 262 ачивки

Плюс правки 01/02/07/15/16/17 под переехавшие пути и новые правила забега.

Дизайнерская часть держится на одном законе: угроза растёт квадратично, сила
отряда — линейно с затуханием, разрыв закрывается ЗНАНИЕМ, а не числами. Из
него выведены и экономика опыта (41 млн против 44 млн дохода), и то, почему
реакции считаются от глубины, а не от урона оружия, и почему в Бездне
множитель HP разрешён, а во всём авторском контенте запрещён.

.claude/commands: скиллы /tile2d, /room, /floor — правила этого движка,
которые нельзя вывести из кода. Главное записано первым: не отчитываться об
успехе без прохода --tool validate.

Числа в этих документах — вход для инструментов, а не украшение. Расхождение
между таблицей и выводом инструмента означает, что неправ документ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 21:07:45 +03:00

18 KiB
Raw Blame History

15 — Движок и редактор комнат

Зачем это отдельно

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

tile2d (STATIC)   engine/ — не знает про правила боя и ни про одну игру по имени
      ├── squad_proto.exe    игра Descent: src/ + projects/descent/
      └── tile2d_editor.exe  редактор комнат: editor/

tile2d_editor линкуется только с движком. Поэтому «редактор полез в правила игры» — это ошибка сборки, а не то, что надо заметить на ревью. Это единственная причина, по которой редактор сделан отдельным приложением, а не режимом игры: режим внутри игры видит EnemyKind, Agent и g_tune целиком, и удержать его от этого нечем.

Что лежит в движке

Всё, что лежит в engine/. Раньше состав задавался списком в CMakeLists.txt, потому что каталоги src/render и src/world делили обе цели; теперь у движка свой каталог, и решение «это движок» принимается тем, куда положен файл (18-engine.md).

Файл О чём
engine/config.h размеры кадра, тайла и карты: то, что нельзя менять без пересборки
engine/framebuffer софтверный буфер RGBA8, световая рампа, блит на экран
engine/sfa_library чтение библиотеки SpriteForge
engine/projection ЕДИНСТВЕННОЕ место, где движок знает про изометрию
engine/view2d.h камера, список отрисовки, сортировка по глубине
engine/project манифест проекта: пути, вид отображения, кривая глубины
engine/tilemap карта, DDA-рейкаст, circle-vs-AABB
engine/pixel ромб, обводка силуэта, ShadeColor
engine/tileset пол и стены по темам отделки
engine/theme сколько тем и как они называются
engine/catalog каталог размещаемых сущностей
engine/room формат комнаты-шаблона и набор комнат
engine/level_gen сборка этажа из комнат по сиду
engine/progression кривая глубины: что означает этаж N (16-descent.md)
engine/microfont растровый шрифт 3×5 для софтверного буфера
engine/text_parse.h разбор строчных текстовых форматов

Чего в движке НЕТ и быть не может: путей к содержимому. Каталог комнат и каталог сущностей называет проект, поэтому один и тот же tile2d_editor открывает и projects/descent, и projects/hollow.

Каталог сущностей — мост между движком и игрой

projects/descent/catalog/entities.txt:

entity shambler  class=monster  depth=1   danger=1.0  color=116,52,52  sprite=shambler_walk  name=SHAMBLER
entity crate     class=prop     depth=1   color=104,76,46  sprite=prop_crate  name=CRATE
entity plate     class=loot     tier=3    depth=9   color=140,146,156  sprite=item_icon_plate  name=PLATE

Редактор знает про сущность ровно это: строковый id, класс, цвет, радиус, имя ассета SpriteForge и на какой глубине она встречается. Что такое SHAMBLER и чем он бьёт — знание игры, и живёт оно в sim/target.cpp. Связывает одно с другим единственная таблица — src/sim/spawn_catalog.cpp.

Добавить тварь = строка в каталоге + запись в EnemyDefOf + строка в таблице перевода. Формат комнат и редактор при этом не меняются вообще.

Формат ключ=значение, а не позиционный: свойства сущности прирастают (глубина, цена, тир), и каждое новое ломало бы все уже написанные строки. Смысл ключей — в 16-descent.md.

Цвет и радиус в каталоге не для красоты: ими редактор рисует сущность, пока для неё не собран спрайт. Пустая палитра не должна мешать планировать уровень.

Комната — это шаблон, а не кусок карты

projects/descent/rooms/arena.room:

name arena
size 14 12
weight 3          вес при выборе генератором: «как часто встречается»
theme 0           набор тайлов отделки
start 0           годится ли как стартовая комната отряда
depth 1 0         с какого этажа и по какой (0 — до бесконечности)
tiles
####++####++##    '.' пол   '#' стена   '+' дверь
#............#
...
spawn 4 4 0.90 shambler:4 rusher:3 spitter:2
spawn 6 1 0.40 crate:2 barrel:1

Ключевое в формате — точка спавна не хранит, кто в ней стоит. Она хранит шанс срабатывания и таблицу весов по id каталога. Одна и та же комната, поставленная дважды, населена по-разному; уровень из восьми комнат читается как уровень, а не как одна кишка, повторённая девять раз.

Ограничения формата и почему они такие:

  • размер от 5×5 до 14×14 — генератор ставит комнаты в слоты по 16 тайлов, и комната размером в слот не оставила бы коридорам места;
  • двери только на рамке и не в углах — коридор тянется НАРУЖУ, а из угла непонятно, в какую сторону;
  • точка спавна только на полу; точка с пустой таблицей в файл не пишется.

Текстовый построчный формат выбран потому, что комнаты лежат в git и правка планировки обязана читаться в diff.

Сборка уровня

engine/level_gen.h. Карта 48×48 делится на сетку 3×3 слотов по 16 тайлов.

  1. в каждый слот выбирается комната по весу;
  2. если ни одной стартовой не выпало — одна ставится принудительно;
  3. вся карта заливается камнем, комнаты отпечатываются по центрам слотов;
  4. по слотам строится остовное дерево (обход в глубину со случайным порядком соседей) — оно гарантирует, что из стартовой достижима каждая;
  5. поверх дерева добавляются лишние связи с шансом 1/3: без них уровень — дерево-кишка, с шансом 1 — решётка без тупиков;
  6. связи режутся коридорами шириной 2 от двери к двери, тремя отрезками по осям (нет двери на нужной стороне — стена пробивается посередине);
  7. разыгрываются точки спавна;
  8. заливкой от точки старта считается достижимость, недостижимые точки снимаются и попадают в отчёт.

Шаг 8 — не перестраховка: он поймал замурованную внутреннюю камеру у storage в тот же день, когда её нарисовали. Тварь в отрезанном кармане не дойдёт до отряда, отряд не дойдёт до неё, а счётчик зачистки будет вечно показывать недобитого противника.

Про детерминизм

RNG крутится только здесь, один раз, при сборке уровня. К моменту первого Game::Step всё уже разложено, и правило «в симуляции ни одного вызова RNG» (02-architecture.md) не нарушено. Один и тот же набор комнат плюс один и тот же сид дают побайтово ту же карту и то же население.

Броски по каждой точке спавна делаются всегда, даже когда точка не сработает: иначе последовательность уводило бы вбок и сид перестал бы значить одно и то же.

--accept и --headless набор комнат не подключают никогда — критерии меряются на прежней захардкоженной карте, иначе цифры не с чем сравнивать.

Темы отделки

Слой Tilemap::theme[] — отдельный от tiles[] намеренно: проходимость и внешний вид это два разных вопроса, и смешав их, мы получили бы отдельный тип тайла на каждое сочетание. Симуляция этот слой не читает вообще.

Тем четыре: STEEL, RUST, FLESH, FROST. Пока они получаются перекраской базового набора по каналам — это честная заглушка, а не временное решение: она даёт различимые материалы, не требуя ни одного собранного ассета. Когда для темы соберут свой тайлсет, поменяется engine/tileset.cpp, а формат комнат и уже нарисованные комнаты — нет.

Редактор

build/bin/tile2d_editor.exe

редактор комнат

Раскладка кадра 480×270: верхняя панель — операции над комнатами, холст — изометрия комнаты тем же кодом, которым рисует игра, правая панель — кисти, свойства и палитра из каталога, нижняя полоса — таблица выбранной точки спавна.

таблица весов точки спавна

Действие Мышь Клавиши
рисовать / стирать ЛКМ / ПКМ
кисть панель справа 15
сущность палитры вкладки MOB/PROP/LOOT, колесо Q / E
тема отделки кнопка темы T
диапазон глубин комнаты счётчики D
соседняя комната набора < > [ ]
новая комната NEW N
сохранить / перечитать SAVE / RELOAD Ctrl+S / Ctrl+R
сборка этажа / новый сид LEVEL / SEED F5 / F6
этаж предпросмотра D- / D+ PgUp / PgDn
проверить в бою PLAY
сдвиг холста стрелки, Home

Точка спавна ставится одним нажатием, а не мазком: кисть, тянущаяся по сетке, засеяла бы полкомнаты.

PLAY запускает squad_proto --rooms --seed N отдельным процессом. Не встроенным режимом: иначе редактору пришлось бы знать правила игры, и вся граница между целями сборки была бы декоративной.

Предпросмотр сборки

сборка уровня

F5 показывает этаж целиком видом сверху: 48×48 тайлов в изометрии — это полторы тысячи пикселей по ширине, и на 480×270 они не помещаются даже близко. Предпросмотр отвечает на вопрос «какая вышла планировка», а не «как это выглядит», и для этого вид сверху честнее.

В строке над картой — сид, число комнат, коридоров и сущностей. Всё, из-за чего уровень собрался не так, как задумано (срезано потолком, недостижимо от старта), печатается отдельной строкой и только когда оно не ноль: постоянная строка «0 ошибок» перестаёт читаться через минуту работы.

Самопроверка набора

tile2d_editor.exe --check

Аналог squad_proto --accept, только для содержимого: набор комнат — такой же исходник, как код, и ломаться он обязан на проверке.

ROOM             SIZE DOORS SPAWNS  THEME  STATE
arena           14x12    12      6  STEEL  OK
...
[PASS] сид 42     комнат 9, коридоров 9, сущностей 31, достижимо 859, ...

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

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

Запуск игры на собранном уровне

squad_proto.exe --rooms [каталог]   собрать уровень из комнат
squad_proto.exe --seed N            сид сборки; без него 1

Без --rooms игра работает ровно как раньше, на захардкоженной карте. Набор не нашёлся или не собрался — откат на неё же: прототип обязан запускаться на голом репозитории.

Собранный уровень населяет себя сам: захардкоженная расстановка мишеней и тварей относится к другой карте, и мешать их — верный способ получить тварь в стене на пустом месте.

Кто и когда меняет сид:

Действие Сид Почему
NEW RAID в меню следующий новая вылазка — новый уровень
TRY AGAIN после поражения тот же это переигрывание, а не новый заход
R в бою тот же на карте, которая меняется под руками, нечего отлаживать

«Следующий» считается тем же линейным конгруэнтным шагом, что и кнопка SEED в редакторе: сид, подобранный в редакторе, обязан повторяться в игре.

Что сюда ещё не входит

  • Предметы на полу появились отдельной вехой и раскладываются генератором по тиру этажа, а не ставятся в комнате руками (16-descent.md).
  • Поворот комнат при сборке. Комната ставится как нарисована. Повороты удвоили бы разнообразие, но автор комнаты перестал бы понимать, что увидит.
  • Свои тайлсеты на тему. Пока перекраска базового набора; место для сборки из SpriteForge уже есть — TileSet::LoadFromForge.