squad-proto/docs/18-engine.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

227 lines
16 KiB
Markdown
Raw 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.

# 18 — Tile2D: движок и проекты
## Что изменилось
До этой вехи «движком» называлась библиотека `squad_engine`, которая лежала
внутри `src/`, включала `src/tuning.h` и сама искала `assets/rooms`. То есть она
не собиралась без игры и знала имя ровно одной игры. Такой движок работает с
одним проектом и притворяется общим.
Теперь их трое, и границы проведены не договорённостью, а сборкой:
| Что | Где | Что знает |
|---|---|---|
| **Tile2D** | `engine/` | окно и цикл, кадр, проекции 2D, тайлы, комнаты, генератор этажей, формат проекта |
| **редактор** | `editor/` | Tile2D и **любой** проект |
| **Descent** | `src/` + `projects/descent/` | правила боя, отряд, снаряжение — то есть игра |
| **пример** | `samples/walk/` | ничего: он и есть проверка, что движком можно пользоваться со стороны |
Игра стала **проектом внутри движка**. Проверяется это не обещанием, а вторым
проектом: [projects/hollow](../projects/hollow) собирается тем же движком, тем
же редактором и тем же исполняемым файлом, а отличается видом отображения,
кривой сложности, каталогом и комнатами.
```bash
squad_proto.exe --rooms # DESCENT, изометрия
squad_proto.exe --project hollow # HOLLOW, вид сверху
tile2d_editor.exe --check --project hollow # проверить чужой набор ЕГО кривой
```
## Что именно вынуто из игры
Три зависимости, которые делали движок несамостоятельным. Каждая нашлась не
рассуждением, а компилятором — после переезда в свой каталог движок просто
перестал собираться:
- **`src/tuning.h`.** Размеры кадра, тайла и карты уехали в
[engine/config.h](../engine/config.h); `tuning.h` теперь его включает и
оставляет прежние имена, поэтому код игры не изменился ни строкой.
- **`MAX_TARGETS`.** Генератор уровней кэпил население игровой константой —
шириной маски удара клинком. У движка теперь свой потолок
`MAX_LEVEL_SPAWNS`, а `src/game.h` проверяет `static_assert`-ом, что одно
влезает в другое. Раньше рассогласование дало бы молча обкусанный этаж.
- **`SQUAD_START_X/Y`.** Запасная точка старта этажа была позицией отряда из
игры. Стала центром карты: движок не знает, кто к нему приедет.
Ещё две — `FindRoomsDir()` и `FindCatalogPath()` — просто удалены. Путь к
содержимому называет проект.
## Проекция: шов под виды 2D
[engine/projection.h](../engine/projection.h) — единственное место, где движок
знает про изометрию. Проекция отвечает на четыре вопроса:
1. куда мировая точка попадает на экран и обратно;
2. чем сортируется список отрисовки;
3. какой формы плита пола;
4. насколько крыша стены поднята над своей плитой.
Всё остальное — тайлсет, свет, кровь, эффекты, курсор редактора — считается
**через** эти четыре ответа и потому переживает смену вида.
Режима сразу два, и это не запас на будущее: абстракция с одной реализацией
ничем не проверена и обычно оказывается неправильной. Вид сверху нашёл ровно
такие места — кольцо эвакуации рисовало свой ромб мимо проекции и висело поверх
квадратной плиты в другом ракурсе.
| | Изометрия | Вид сверху |
|---|---|---|
| плита | ромб 32×16, наклон 2:1 | квадрат 32×32 |
| стена | крыша + две вертикальные грани | только крыша |
| сортировка | `x + y` | `y` |
| свет на плите | точная билинейка по углам ромба | без косого члена (см. ниже) |
![вид сверху](view-topdown-hollow.png)
Свет вплавляется в блит спрайта патчем `a + b·px + c·py + e·px² + f·py²`
(`ShadeQuad`). В изометрии билинейка по четырём углам ромба раскладывается по
этим членам **точно**, поэтому на общем ребре соседних тайлов значений не
расходится и швов нет. У квадрата честным слагаемым был бы `u·v`, которого в
этой форме нет, — он отбрасывается. На тайле в 32 пикселя это доли ступени
рампы, и лишний член в горячем блите того не стоит.
Вид выбирается **один раз** при старте: тайлсет рисуется под форму плиты, и
смена вида на ходу означала бы перерисовку всех спрайтов посреди кадра.
Чего вид сверху пока не умеет: тайлы из SpriteForge. Библиотека нарисована в
изометрии, и положить её в другой ракурс нельзя — это не «неточно», это другой
ракурс. `LoadFromForge` в этом режиме честно отказывается, и набор остаётся
процедурным целиком.
## Проект
[engine/project.h](../engine/project.h). Построчный `ключ значение`, как у
комнат и каталога: файл лежит в git, правится руками и обязан читаться в diff.
```
name DESCENT
view iso # iso | topdown
rooms rooms
catalog catalog/entities.txt
startdepth 1
danger 3.0 2.2 0.10 # база, линейный рост, квадратичный хвост
density 0.05 1.5 # прирост за этаж, упор
loot 2 4 3 # база, этажей на +1 предмет, этажей на тир
sight 0.035 0.60 # сжатие за этаж, нижний упор
xp 0.15
themes 1 4 8 13 # с какого этажа начинается каждая тема
```
Неизвестный ключ **валит загрузку**, а не пропускается: опечатка в текстовом
формате — самая дорогая ошибка, строка просто не действует, и это выглядит как
«движок не слушается».
Кривая глубины ([16-descent.md](16-descent.md)) переехала из кода в манифест:
её правят на ощупь и часто, и «сделать десятый этаж чуть злее» не должно
означать пересборку движка. **Форма** кривой при этом осталась в коде —
`FloorCurve` гнёт её, но не может подменить линейный рост экспонентой. Проект,
которому нужна другая форма, — это повод завести ещё одно поле, а не язык
выражений в текстовом файле.
### Как проект находится
`--project` принимает **имя** (каталог в `projects/`) или путь. Без ключа берётся
единственный найденный проект. Поиск идёт от текущего каталога вверх — запуск из
корня репозитория и из `build/bin` одинаково рабочие.
Несколько проектов и ни одного названного — отказ со списком доступных. Молча
взятый «первый попавшийся» читался бы как «движок открыл не то».
## Второй проект
[projects/hollow](../projects/hollow) — вид сверху, четыре комнаты, свой
каталог, короткая и злая кривая: `danger 5.0 3.4 0.25` против `3.0 2.2 0.10`,
плоть уже на четвёртом этаже вместо восьмого.
![редактор с чужим проектом](editor-hollow-topdown.png)
Редактор показывает палитру HOLLOW (без мишени приёмки, которой в этом наборе
нет) и считает предпросмотр **его** кривой: `danger 4.3 / 5.0` на первом этаже,
где у DESCENT было бы `3.0`.
Общее у двух проектов ровно одно — **id сущностей**. Это граница ИГРЫ, а не
движка: `squad_proto` умеет превращать в тварей только те id, что перечислены в
[src/sim/spawn_catalog.cpp](../src/sim/spawn_catalog.cpp). Движку всё равно, как
они называются; проект с чужими id соберётся, но эта игра его не населит.
## Что осталось компилтаймовым
В [engine/config.h](../engine/config.h), и это честно записано там же:
- **размер кадра** 480×270 — по нему считаются раскладки HUD, меню и оверлея;
- **размер карты** 48×48 — тайлмап плоский массив;
- **размер тайла** — проекция берёт свой, но `TILE_MAX_*` задаёт верхнюю оценку
для отсечения по краю кадра.
Проект пока не может поменять ни одно из трёх. Сделать их данными — отдельная
веха: это означает динамические буферы там, где сейчас массивы, и трогает
раскладку всего интерфейса.
## Приложение: окно, кадр, ход времени
[engine/app.h](../engine/app.h). Раньше окно, безрамочный режим, фреймбуфер,
блиттер и fixed timestep жили двумя почти одинаковыми копиями — в игре и в
редакторе. Третьему приложению пришлось бы написать третью, и она бы уже
отличалась: где-то забытый `SetExitKey`, где-то другой потолок шагов.
Инверсии управления здесь нет: цикл остаётся у приложения, а `App` отвечает на
три вопроса — пора ли закрываться, сколько шагов симуляции прошло, куда лёг кадр
в окне. Колбэки спрятали бы порядок «шаг — рендер — интерфейс», а он у каждого
приложения свой, и в нём вся суть.
```cpp
while (app.NextFrame())
{
for (int i = 0, n = app.Advance(halted); i < n; ++i) game.Step(FIXED_DT);
RenderGame(game, view, app.fb, app.Alpha(), app.frameDt);
app.Present(); // кадр на экран
ui.Draw(game, app.fit); // поверх кадра
app.Finish();
}
```
## Третье приложение
[samples/walk](../samples/walk/main.cpp) — открыть проект, собрать этаж,
походить по нему камерой. Полторы сотни строк вместе с разбором аргументов.
![пример на движке](sample-walk.png)
Он существует не для демонстрации. Игра и редактор проверить самостоятельность
движка не могут: редактор мира не рисует вовсе, а игра — это ровно то место,
куда общий код и уезжает незаметно. Пример же ломается сразу, как только для
показа этажа понадобится что-нибудь из `src/`.
Первое, что он потребовал, — `DrawTilemap` ([engine/tilemap_draw.h](../engine/tilemap_draw.h)):
до него движок не умел нарисовать собственную карту. Игра свой проход по полу
оставила себе, потому что вплавляет в каждый блит свет; две реализации здесь не
дублирование, а разные задачи — «показать карту» и «нарисовать сцену со светом».
Общий у них порядок сортировки, и он один на всех, потому что живёт в проекции.
## Проверки
```bash
squad_proto.exe --accept приёмка игры (спек 11)
tile2d_editor.exe --check --project descent DESCENT: набор и покрытие глубин
tile2d_editor.exe --check --project hollow HOLLOW: то же, его кривой
squad_proto.exe --descend 6 забег DESCENT
squad_proto.exe --project hollow --descend 4 забег HOLLOW
tile2d_walk.exe --project hollow --depth 3 пример: движок без игры
```
`--check` на втором проекте сразу нашёл в нём то же, что когда-то в `storage`:
глухую камеру посередине комнаты `maw`. На сетке она выглядит нормально, но
точка спавна внутри недостижима — тварь не дойдёт до отряда и не будет убита, и
этаж тихо станет легче.
## Чего ещё нет
- **Пространства имён.** Типы движка (`Sprite`, `Tilemap`, `Room`) лежат в
глобальном пространстве. Пока потребитель один репозиторий — терпимо; как
библиотека для чужого кода — нет.
- **Своей палитры видов.** Вид сверху есть, гексов и косой проекции нет.
Добавлять их теперь дёшево: это одна запись в `MODES` и две ветки в
`Projection`.
- **Генерации спрайтов внутри движка.** Она снаружи, в SpriteForge
([12-assets.md](12-assets.md)), и с видом сверху пока не дружит.