squad-proto/docs/18-engine.md

227 lines
16 KiB
Markdown
Raw Permalink 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
# 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)), и с видом сверху пока не дружит.