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

16 KiB
Raw Blame History

18 — Tile2D: движок и проекты

Что изменилось

До этой вехи «движком» называлась библиотека squad_engine, которая лежала внутри src/, включала src/tuning.h и сама искала assets/rooms. То есть она не собиралась без игры и знала имя ровно одной игры. Такой движок работает с одним проектом и притворяется общим.

Теперь их трое, и границы проведены не договорённостью, а сборкой:

Что Где Что знает
Tile2D engine/ окно и цикл, кадр, проекции 2D, тайлы, комнаты, генератор этажей, формат проекта
редактор editor/ Tile2D и любой проект
Descent src/ + projects/descent/ правила боя, отряд, снаряжение — то есть игра
пример samples/walk/ ничего: он и есть проверка, что движком можно пользоваться со стороны

Игра стала проектом внутри движка. Проверяется это не обещанием, а вторым проектом: projects/hollow собирается тем же движком, тем же редактором и тем же исполняемым файлом, а отличается видом отображения, кривой сложности, каталогом и комнатами.

squad_proto.exe --rooms                     # DESCENT, изометрия
squad_proto.exe --project hollow            # HOLLOW, вид сверху
tile2d_editor.exe --check --project hollow  # проверить чужой набор ЕГО кривой

Что именно вынуто из игры

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

  • src/tuning.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 — единственное место, где движок знает про изометрию. Проекция отвечает на четыре вопроса:

  1. куда мировая точка попадает на экран и обратно;
  2. чем сортируется список отрисовки;
  3. какой формы плита пола;
  4. насколько крыша стены поднята над своей плитой.

Всё остальное — тайлсет, свет, кровь, эффекты, курсор редактора — считается через эти четыре ответа и потому переживает смену вида.

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

Изометрия Вид сверху
плита ромб 32×16, наклон 2:1 квадрат 32×32
стена крыша + две вертикальные грани только крыша
сортировка x + y y
свет на плите точная билинейка по углам ромба без косого члена (см. ниже)

вид сверху

Свет вплавляется в блит спрайта патчем a + b·px + c·py + e·px² + f·py² (ShadeQuad). В изометрии билинейка по четырём углам ромба раскладывается по этим членам точно, поэтому на общем ребре соседних тайлов значений не расходится и швов нет. У квадрата честным слагаемым был бы u·v, которого в этой форме нет, — он отбрасывается. На тайле в 32 пикселя это доли ступени рампы, и лишний член в горячем блите того не стоит.

Вид выбирается один раз при старте: тайлсет рисуется под форму плиты, и смена вида на ходу означала бы перерисовку всех спрайтов посреди кадра.

Чего вид сверху пока не умеет: тайлы из SpriteForge. Библиотека нарисована в изометрии, и положить её в другой ракурс нельзя — это не «неточно», это другой ракурс. LoadFromForge в этом режиме честно отказывается, и набор остаётся процедурным целиком.

Проект

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) переехала из кода в манифест: её правят на ощупь и часто, и «сделать десятый этаж чуть злее» не должно означать пересборку движка. Форма кривой при этом осталась в коде — FloorCurve гнёт её, но не может подменить линейный рост экспонентой. Проект, которому нужна другая форма, — это повод завести ещё одно поле, а не язык выражений в текстовом файле.

Как проект находится

--project принимает имя (каталог в projects/) или путь. Без ключа берётся единственный найденный проект. Поиск идёт от текущего каталога вверх — запуск из корня репозитория и из build/bin одинаково рабочие.

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

Второй проект

projects/hollow — вид сверху, четыре комнаты, свой каталог, короткая и злая кривая: danger 5.0 3.4 0.25 против 3.0 2.2 0.10, плоть уже на четвёртом этаже вместо восьмого.

редактор с чужим проектом

Редактор показывает палитру HOLLOW (без мишени приёмки, которой в этом наборе нет) и считает предпросмотр его кривой: danger 4.3 / 5.0 на первом этаже, где у DESCENT было бы 3.0.

Общее у двух проектов ровно одно — id сущностей. Это граница ИГРЫ, а не движка: squad_proto умеет превращать в тварей только те id, что перечислены в src/sim/spawn_catalog.cpp. Движку всё равно, как они называются; проект с чужими id соберётся, но эта игра его не населит.

Что осталось компилтаймовым

В engine/config.h, и это честно записано там же:

  • размер кадра 480×270 — по нему считаются раскладки HUD, меню и оверлея;
  • размер карты 48×48 — тайлмап плоский массив;
  • размер тайла — проекция берёт свой, но TILE_MAX_* задаёт верхнюю оценку для отсечения по краю кадра.

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

Приложение: окно, кадр, ход времени

engine/app.h. Раньше окно, безрамочный режим, фреймбуфер, блиттер и fixed timestep жили двумя почти одинаковыми копиями — в игре и в редакторе. Третьему приложению пришлось бы написать третью, и она бы уже отличалась: где-то забытый SetExitKey, где-то другой потолок шагов.

Инверсии управления здесь нет: цикл остаётся у приложения, а App отвечает на три вопроса — пора ли закрываться, сколько шагов симуляции прошло, куда лёг кадр в окне. Колбэки спрятали бы порядок «шаг — рендер — интерфейс», а он у каждого приложения свой, и в нём вся суть.

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 — открыть проект, собрать этаж, походить по нему камерой. Полторы сотни строк вместе с разбором аргументов.

пример на движке

Он существует не для демонстрации. Игра и редактор проверить самостоятельность движка не могут: редактор мира не рисует вовсе, а игра — это ровно то место, куда общий код и уезжает незаметно. Пример же ломается сразу, как только для показа этажа понадобится что-нибудь из src/.

Первое, что он потребовал, — DrawTilemap (engine/tilemap_draw.h): до него движок не умел нарисовать собственную карту. Игра свой проход по полу оставила себе, потому что вплавляет в каждый блит свет; две реализации здесь не дублирование, а разные задачи — «показать карту» и «нарисовать сцену со светом». Общий у них порядок сортировки, и он один на всех, потому что живёт в проекции.

Проверки

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), и с видом сверху пока не дружит.