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>
16 KiB
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 — единственное место, где движок знает про изометрию. Проекция отвечает на четыре вопроса:
- куда мировая точка попадает на экран и обратно;
- чем сортируется список отрисовки;
- какой формы плита пола;
- насколько крыша стены поднята над своей плитой.
Всё остальное — тайлсет, свет, кровь, эффекты, курсор редактора — считается через эти четыре ответа и потому переживает смену вида.
Режима сразу два, и это не запас на будущее: абстракция с одной реализацией ничем не проверена и обычно оказывается неправильной. Вид сверху нашёл ровно такие места — кольцо эвакуации рисовало свой ромб мимо проекции и висело поверх квадратной плиты в другом ракурсе.
| Изометрия | Вид сверху | |
|---|---|---|
| плита | ромб 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), и с видом сверху пока не дружит.


