squad-proto/docs/15-engine-and-editor.md

267 lines
18 KiB
Markdown
Raw Normal View History

Документация: границы движка, работа через модель, дизайн игры до лейтгейма 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-14 01:07:45 +07:00
# 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`.