squad-proto/docs/16-descent.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

213 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.

# 16 — Спуск: глубина как первичное понятие
## Что за игра
Бесконечный спуск в подземелье. Чем глубже, тем страшнее окружение, злее твари,
богаче находки. Забег кончается, когда отряд полёг; сколько этажей он прошёл —
и есть результат.
Из этого следует главное решение всей вехи: **глубина не параметр уровня, а
первичное понятие движка**. От неё зависят планировка, население, лут, отделка
и свет. Поэтому она не размазана по генератору, а собрана в одну функцию.
## Кривая — одно место
[engine/progression.h](../engine/progression.h). `DescribeFloor(depth)`
возвращает `FloorSpec`: тема отделки, бюджет опасности, плотность, сколько и
какого лута, насколько сжат обзор, множитель опыта.
Коэффициенты с вехи M14 живут не в коде, а в манифесте проекта
([18-engine.md](18-engine.md)): `projects/descent/project.txt`. В коде осталась
ФОРМА кривой — проект гнёт её, но не может подменить линейный рост
экспонентой.
Кривая сложности — это то, что правят чаще всего и правят на ощупь. Размазанная
по генератору, каталогу и рендеру, она через месяц перестаёт поддаваться правке:
«сделать десятый этаж чуть злее» превращается в раскопки по трём файлам.
Чего в кривой **нет намеренно**:
- **множителей урона и здоровья тварей.** «Тот же бредущий, но со втрое большим
HP» — это не страшнее, это дольше. Глубина меняет, КТО выходит и сколько их;
- **потолка.** Функция определена для любого N: этаж 200 обязан собраться, даже
если каталог кончился на 12-м.
| Что | Как растёт | Почему так |
|---|---|---|
| опасность | линейно + квадратичный хвост | чистая линейка читается как «то же, только больше»; чистая экспонента обрывает забег там, где игрок начал понимать правила |
| плотность точек | до ×1.5 и упор | выше упора все точки срабатывают всегда, то есть этажи становятся одинаковыми |
| тир лута | +1 каждые 3 этажа | медленнее опасности: спуск обязан опережать снаряжение |
| обзор | до ×0.6 и упор | ниже — это уже не «страшно», а «не видно, куда идти» |
| опыт | +15% за этаж | медленнее опасности: спускаться выгодно, но не настолько, чтобы прыгать через этажи |
Полосы отделки: `STEEL` 13, `RUST` 47, `FLESH` 812, `FROST` 13+. Последняя
открытая: менять тему дальше не на что, а «случайная тема каждый этаж»
превратила бы спуск в чересполосицу.
## Бюджет опасности
Каждая сущность каталога несёт цену `danger` — не урон и не HP, а «сколько места
в голове игрока она занимает». Этаж получает бюджет и тратит его:
1. срабатывают точки спавна комнат — из их таблиц отбирается то, что разрешено
на этой глубине и по карману;
2. остаток **доливается** тварями вне комнатных точек.
Второй шаг обязателен. Без него глубина упирается в число точек, нарисованных в
комнатах: на двадцатом этаже все точки уже срабатывают и уже выдают самое
дорогое, и дальше этаж не становится страшнее вообще. Долив превращает бюджет из
потолка в норму, которую этаж обязан выбрать.
**Когда мест мало, а бюджета много, этаж становится злее, а не многолюднее.**
Тел в мире не может быть больше `MAX_TARGETS`, и на глубине бюджет неизбежно
перерастает это число. Поэтому на каждое оставшееся место считается, сколько
опасности обязано на него прийтись, и всё дешевле отбрасывается: хвост этажа
набирается самыми дорогими тварями.
> Потолок `MAX_TARGETS` пришлось поднять с 32 до 64. Это не «запас на будущее»:
> `tile2d_editor --check` показал, что примерно с шестнадцатого этажа бюджет
> перестаёт влезать в число тел, и глубже этажи переставали отличаться. Маска
> `MeleeWeapon::hitMask` стала 64-битной ровно поэтому.
## Каталог: глубина и цена
[projects/descent/catalog/entities.txt](../projects/descent/catalog/entities.txt) перешёл на
`ключ=значение`. Первая версия была позиционной, и глубина её сломала: каждое
новое свойство ломало бы все уже написанные строки.
```
entity shambler class=monster depth=1 danger=1.0 radius=0.34 color=116,52,52 sprite=shambler_walk name=SHAMBLER
entity brute class=monster depth=7 danger=4.5 radius=0.46 color=142,60,78 sprite=brute_walk name=BRUTE
entity plate class=loot tier=3 depth=9 radius=0.30 color=140,146,156 sprite=item_icon_plate name=PLATE
```
- `depth=N`с этажа N и глубже; `depth=N..M` — по M включительно;
- `danger` — цена в бюджете. У обстановки и лута она нулевая по определению:
ящик не делает этаж страшнее, он делает его обжитее;
- `tier` — для лута: этаж выдаёт свой тир и всё, что ниже. Бинт с глубиной не
исчезает, он просто перестаёт быть удачей.
Третий класс — `loot`. Мост в игру тот же: `sim/spawn_catalog.cpp` переводит id
в `EnemyKind` **и** в `ItemId` двумя таблицами.
Комнаты тоже получили глубину: `depth <min> [max]` в файле. Планировка — часть
погружения: широкая арена первых этажей и теснота, где не развернуть строй, —
это разные игры.
## Находки
[src/sim/loot.h](../src/sim/loot.h). Предмет лежит на полу отдельной сущностью
и подбирается **пассивно**: боец, прошедший по нему, кладёт его в свободную
ячейку сумки.
Почему не «выдать предмет при убийстве»: в игре про спуск находка — это МЕСТО,
до которого надо дойти. Лут, падающий в инвентарь сам, не заставляет никуда идти
и превращает «глубже — лучше лут» в строку в логе. Лежащий предмет плохо виден в
темноте, лежит там, куда отряд ещё не заходил, и за ним надо разомкнуть строй —
это и есть цена находки.
Отдельной кнопки подбора нет намеренно: игрок и так не нажимает «огонь»
(спек 5), и заводить ради лута первое ручное действие в игре значило бы менять
её жанр. Полная сумка — предмет остаётся лежать: «некуда положить» обязано быть
видно, а не молча съедено.
## Спуск
Точка спуска — **самый дальний достижимый тайл** от старта, найденный той же
заливкой, что считает достижимость. Не «дальняя комната»: комната может
оказаться отрезанной, а этот выбор недостижимым быть не может по построению.
Рисуется пульсирующим кольцом и видна даже в полной темноте — единственная цель
этажа обязана читаться с порога.
`F` на спуске уводит отряд вниз. Именно нажатие, а не заход на клетку: спуск —
решение игрока. Этаж не зачищен, лут не собран, уходить рано — это нормальный
выбор, и отряд может стоять на спуске сколько угодно.
Вниз переезжает **всё, что нажито**: здоровье, оружие в руках, содержимое сумок.
Павшие не воскресают — воскрешение на лестнице обесценило бы единственную
настоящую потерю в забеге. Стартовый комплект снаряжения выдаётся только тем, кто
пришёл ни с чем: раздавать его заново на каждом этаже значило бы стирать весь
смысл находок.
Симуляция сама этаж не меняет: `Game::Step` только отмечает `atExit`. Смена мира
посреди шага, между движением и стрельбой, оставила бы половину систем со
ссылками на снесённые сущности.
| Действие | Что делает |
|---|---|
| `NEW RAID` в меню | выбор чекпоинта, потом новый забег ([17-extraction.md](17-extraction.md)) |
| `F` на спуске | следующий этаж, отряд как есть |
| `TRY AGAIN` после поражения | тот же этаж заново |
| `R` в бою | тот же этаж заново |
## Опыт
Копится за убитых: цена `danger` умножается на множитель этажа. Обстановка опыта
не даёт — платить за ящик значило бы сделать разбивание хлама самым выгодным
занятием в игре.
Тратится он на улучшения отряда ([17-extraction.md](17-extraction.md)): запас
патронов, переноска, обзор, живучесть. Заглушкой это было три вехи — ровно до
тех пор, пока не нашлось, во что вкладываться так, чтобы не сломать жанр.
## Свет как главный признак глубины
Окружение становится страшнее не новыми спрайтами, а тем, что видно всё меньше.
`FloorSpec::sightScale` сжимает радиус обзора всем бойцам одинаково, поверх их
личных множителей: фонарь на двадцатом этаже по-прежнему лучше савана, просто
оба светят хуже.
Значение живёт в `Lighting::sightScale`, а не в `g_tune`: tuning общий на весь
запуск, и запись туда означала бы, что двенадцатый этаж подкрутил ручку, с
которой потом работает и первый этаж, и песочница приёмки.
## Цифры на экране
HUD прототипа был принципиально бессловесным (`render/hud.h`). Для спуска этого
перестало хватать ровно на одном: **номер этажа — это число**, и показать его
формой нельзя. Двадцать семь зарубок на краю экрана никто не считает.
Поэтому в движке появился растровый шрифт 3×5
([engine/microfont.h](../engine/microfont.h)). Не raylib-текст: правило
«ни одного примитива raylib в игровой сцене» остаётся в силе, потому что этот
шрифт пишет в тот же софтверный фреймбуфер и попадает под ту же сетку пикселей.
## Проверки
```bash
tile2d_editor.exe --check покрытие глубин и сборка на 1/3/6/10/16/24
squad_proto.exe --descend 8 забег на 8 этажей без окна
squad_proto.exe --rooms --depth 12 --seed 42 сразу двенадцатый этаж
```
`--check` отвечает на главный вопрос игры про бесконечный спуск: не «хороша ли
комната», а **есть ли чем застроить сотый этаж**. Набор, у которого на глубине
не остаётся ни одной подходящей комнаты, ни одной твари или ни одного предмета,
обрывает забег — и обрывает молча, если это не проверять.
Он же различает две разные вещи:
- **FAIL** — этаж собран неправильно: точки не встали, что-то отрезано от
старта, некуда спускаться, нет находок;
- **WARN** — этаж собран правильно, но набор комнат не успевает за кривой, и
глубина перестаёт ощущаться. Это повод дорисовать комнат, а не поломка.
`--descend` проверяет то, что не проверить ни скриншотом, ни `--check`: цепочку
«спуск → пересборка мира → перенос отряда». Ошибка здесь не падает и не
рисуется — она выглядит как отряд, приехавший вниз с пустыми руками.
```
DEPTH THEME MONSTER DANGER LOOT EXIT SQUAD
1 STEEL 3 3 2 63 живых 5, в сумках 8
8 FLESH 16 23 4 135 живых 5, в сумках 30
```
## Чего ещё нет
- **траты опыта** — см. выше, это отдельная веха с отдельными критериями;
- **боссов и особых этажей** — кривая пока гладкая, а спуск без вех в памяти
сливается в один длинный коридор;
- **сохранения забега** — вылет означает потерю прогресса;
- **уникального лута** — все находки берутся из общего каталога предметов, и
отличаются только тиром.