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>
18 KiB
15 — Движок и редактор комнат
Зачем это отдельно
До этой вехи проект был одним исполняемым файлом. Теперь их три, и граница между ними проведена линковкой, а не договорённостью:
tile2d (STATIC) engine/ — не знает про правила боя и ни про одну игру по имени
├── squad_proto.exe игра Descent: src/ + projects/descent/
└── tile2d_editor.exe редактор комнат: editor/
tile2d_editor линкуется только с движком. Поэтому «редактор полез в правила
игры» — это ошибка сборки, а не то, что надо заметить на ревью. Это единственная
причина, по которой редактор сделан отдельным приложением, а не режимом игры:
режим внутри игры видит EnemyKind, Agent и g_tune целиком, и удержать его
от этого нечем.
Что лежит в движке
Всё, что лежит в engine/. Раньше состав задавался списком в
CMakeLists.txt, потому что каталоги src/render и
src/world делили обе цели; теперь у движка свой каталог, и решение «это
движок» принимается тем, куда положен файл (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) |
engine/microfont |
растровый шрифт 3×5 для софтверного буфера |
engine/text_parse.h |
разбор строчных текстовых форматов |
Чего в движке НЕТ и быть не может: путей к содержимому. Каталог комнат и
каталог сущностей называет проект, поэтому один и тот же tile2d_editor
открывает и projects/descent, и projects/hollow.
Каталог сущностей — мост между движком и игрой
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.
Добавить тварь = строка в каталоге + запись в EnemyDefOf + строка в
таблице перевода. Формат комнат и редактор при этом не меняются вообще.
Формат ключ=значение, а не позиционный: свойства сущности прирастают (глубина,
цена, тир), и каждое новое ломало бы все уже написанные строки. Смысл ключей —
в 16-descent.md.
Цвет и радиус в каталоге не для красоты: ими редактор рисует сущность, пока для неё не собран спрайт. Пустая палитра не должна мешать планировать уровень.
Комната — это шаблон, а не кусок карты
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. Карта 48×48 делится на сетку 3×3 слотов по 16 тайлов.
- в каждый слот выбирается комната по весу;
- если ни одной стартовой не выпало — одна ставится принудительно;
- вся карта заливается камнем, комнаты отпечатываются по центрам слотов;
- по слотам строится остовное дерево (обход в глубину со случайным порядком соседей) — оно гарантирует, что из стартовой достижима каждая;
- поверх дерева добавляются лишние связи с шансом 1/3: без них уровень — дерево-кишка, с шансом 1 — решётка без тупиков;
- связи режутся коридорами шириной 2 от двери к двери, тремя отрезками по осям (нет двери на нужной стороне — стена пробивается посередине);
- разыгрываются точки спавна;
- заливкой от точки старта считается достижимость, недостижимые точки снимаются и попадают в отчёт.
Шаг 8 — не перестраховка: он поймал замурованную внутреннюю камеру у storage
в тот же день, когда её нарисовали. Тварь в отрезанном кармане не дойдёт до
отряда, отряд не дойдёт до неё, а счётчик зачистки будет вечно показывать
недобитого противника.
Про детерминизм
RNG крутится только здесь, один раз, при сборке уровня. К моменту первого
Game::Step всё уже разложено, и правило «в симуляции ни одного вызова RNG»
(02-architecture.md) не нарушено. Один и тот же набор
комнат плюс один и тот же сид дают побайтово ту же карту и то же население.
Броски по каждой точке спавна делаются всегда, даже когда точка не сработает: иначе последовательность уводило бы вбок и сид перестал бы значить одно и то же.
--accept и --headless набор комнат не подключают никогда — критерии
меряются на прежней захардкоженной карте, иначе цифры не с чем сравнивать.
Темы отделки
Слой Tilemap::theme[] — отдельный от tiles[] намеренно: проходимость и
внешний вид это два разных вопроса, и смешав их, мы получили бы отдельный тип
тайла на каждое сочетание. Симуляция этот слой не читает вообще.
Тем четыре: STEEL, RUST, FLESH, FROST. Пока они получаются перекраской
базового набора по каналам — это честная заглушка, а не временное решение: она
даёт различимые материалы, не требуя ни одного собранного ассета. Когда для темы
соберут свой тайлсет, поменяется engine/tileset.cpp, а формат комнат и уже
нарисованные комнаты — нет.
Редактор
build/bin/tile2d_editor.exe
Раскладка кадра 480×270: верхняя панель — операции над комнатами, холст — изометрия комнаты тем же кодом, которым рисует игра, правая панель — кисти, свойства и палитра из каталога, нижняя полоса — таблица выбранной точки спавна.
| Действие | Мышь | Клавиши |
|---|---|---|
| рисовать / стирать | ЛКМ / ПКМ | — |
| кисть | панель справа | 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 отдельным процессом. Не
встроенным режимом: иначе редактору пришлось бы знать правила игры, и вся
граница между целями сборки была бы декоративной.
Предпросмотр сборки
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).
- Поворот комнат при сборке. Комната ставится как нарисована. Повороты удвоили бы разнообразие, но автор комнаты перестал бы понимать, что увидит.
- Свои тайлсеты на тему. Пока перекраска базового набора; место для сборки
из SpriteForge уже есть —
TileSet::LoadFromForge.


