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

267 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 15 — Движок и редактор комнат
## Зачем это отдельно
До этой вехи проект был одним исполняемым файлом. Теперь их три, и граница
между ними проведена **линковкой**, а не договорённостью:
```
tile2d (STATIC) engine/ — не знает про правила боя и ни про одну игру по имени
├── squad_proto.exe игра Descent: src/ + projects/descent/
└── tile2d_editor.exe редактор комнат: editor/
```
`tile2d_editor` линкуется **только** с движком. Поэтому «редактор полез в правила
игры» — это ошибка сборки, а не то, что надо заметить на ревью. Это единственная
причина, по которой редактор сделан отдельным приложением, а не режимом игры:
режим внутри игры видит `EnemyKind`, `Agent` и `g_tune` целиком, и удержать его
от этого нечем.
### Что лежит в движке
Всё, что лежит в `engine/`. Раньше состав задавался списком в
[CMakeLists.txt](../CMakeLists.txt), потому что каталоги `src/render` и
`src/world` делили обе цели; теперь у движка свой каталог, и решение «это
движок» принимается тем, куда положен файл ([18-engine.md](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](16-descent.md)) |
| `engine/microfont` | растровый шрифт 3×5 для софтверного буфера |
| `engine/text_parse.h` | разбор строчных текстовых форматов |
Чего в движке НЕТ и быть не может: путей к содержимому. Каталог комнат и
каталог сущностей называет проект, поэтому один и тот же `tile2d_editor`
открывает и `projects/descent`, и `projects/hollow`.
## Каталог сущностей — мост между движком и игрой
[projects/descent/catalog/entities.txt](../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](../src/sim/spawn_catalog.cpp).
**Добавить тварь** = строка в каталоге + запись в `EnemyDefOf` + строка в
таблице перевода. Формат комнат и редактор при этом не меняются вообще.
Формат `ключ=значение`, а не позиционный: свойства сущности прирастают (глубина,
цена, тир), и каждое новое ломало бы все уже написанные строки. Смысл ключей —
в [16-descent.md](16-descent.md).
Цвет и радиус в каталоге не для красоты: ими редактор рисует сущность, пока для
неё не собран спрайт. Пустая палитра не должна мешать планировать уровень.
## Комната — это шаблон, а не кусок карты
[projects/descent/rooms/arena.room](../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](../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](02-architecture.md)) не нарушено. Один и тот же набор
комнат плюс один и тот же сид дают побайтово ту же карту и то же население.
Броски по каждой точке спавна делаются **всегда**, даже когда точка не
сработает: иначе последовательность уводило бы вбок и сид перестал бы значить
одно и то же.
`--accept` и `--headless` набор комнат не подключают никогда — критерии
меряются на прежней захардкоженной карте, иначе цифры не с чем сравнивать.
## Темы отделки
Слой `Tilemap::theme[]` — отдельный от `tiles[]` намеренно: проходимость и
внешний вид это два разных вопроса, и смешав их, мы получили бы отдельный тип
тайла на каждое сочетание. Симуляция этот слой не читает вообще.
Тем четыре: `STEEL`, `RUST`, `FLESH`, `FROST`. Пока они получаются перекраской
базового набора по каналам — это честная заглушка, а не временное решение: она
даёт различимые материалы, не требуя ни одного собранного ассета. Когда для темы
соберут свой тайлсет, поменяется `engine/tileset.cpp`, а формат комнат и уже
нарисованные комнаты — нет.
## Редактор
```
build/bin/tile2d_editor.exe
```
![редактор комнат](editor-room.png)
Раскладка кадра 480×270: верхняя панель — операции над комнатами, холст —
изометрия комнаты **тем же кодом, которым рисует игра**, правая панель — кисти,
свойства и палитра из каталога, нижняя полоса — таблица выбранной точки спавна.
![таблица весов точки спавна](editor-spawn.png)
| Действие | Мышь | Клавиши |
|---|---|---|
| рисовать / стирать | ЛКМ / ПКМ | — |
| кисть | панель справа | `1``5` |
| сущность палитры | вкладки `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` **отдельным процессом**. Не
встроенным режимом: иначе редактору пришлось бы знать правила игры, и вся
граница между целями сборки была бы декоративной.
### Предпросмотр сборки
![сборка уровня](editor-level.png)
`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](16-descent.md)).
- **Поворот комнат при сборке.** Комната ставится как нарисована. Повороты
удвоили бы разнообразие, но автор комнаты перестал бы понимать, что увидит.
- **Свои тайлсеты на тему.** Пока перекраска базового набора; место для сборки
из SpriteForge уже есть — `TileSet::LoadFromForge`.