squad-proto/docs/13-combat.md

287 lines
21 KiB
Markdown
Raw 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
# 13. Бой: оружие, твари, снаряжение, экраны
Прототип из вех M0M6 проверял ОТРЯД: строй, линии огня, правило «не бить
своих». Противника там не было — были мишени, которые стоят и не отвечают.
Эта веха достраивает вторую половину: твари, которые идут и бьют, каталог
оружия, снаряжение и экраны вне боя.
![вылазка](raid.png)
## Два режима, и это важно
| Режим | Что в мире | Кто им пользуется |
|---|---|---|
| боевой (по умолчанию) | твари 5 видов, на бойцах комплекты снаряжения | игра |
| песочница (`--sandbox`) | прежние мишени, отряд без снаряжения | ручные проверки |
| приёмка (`--accept`, `--headless`) | всегда песочница | замеры критериев |
Разделение не косметическое. Броня несёт множитель скорости, а на скорости
движения меряются критерии 11.4 и 11.5; твари бьют, а от урона меняется состав
живых. Поэтому `Game::combatMode` выключен во всех прогонах приёмки, мишень
`EnemyKind::DUMMY` ведёт себя ровно как раньше, и `--accept` считает те же
цифры, что считал до этой вехи ([09-acceptance.md](09-acceptance.md)).
## Оружие
Каталог — [src/sim/weapon_defs.h](../src/sim/weapon_defs.h). Карабин и сабля
по-прежнему читают числа из `tuning.h`: это оружие по умолчанию, и все прежние
ручки оверлея крутят именно их ([08-decisions.md](08-decisions.md)).
| Ствол | Дальность | Интервал | Урон | Магазин | Чем отличается |
|---|---|---|---|---|---|
| CARBINE | 7.2 | 0.35 | 12 | 24 | штатный: ровный во всём |
| PISTOL | 4.6 | 0.22 | 8 | 7 | ближе и слабее, зато быстрая перезарядка |
| SMG | 5.4 | 0.09 | 6 | 40 | шквал; увод копится быстрее, чем оседает |
| SHOTGUN | 3.4 | 0.85 | 26 x6 | 5 | залп дробин, в упор рвёт |
| MAGNUM | 8.6 | 0.95 | 78 | 6 | дальше всех и валит с ног, но раз в секунду |
| Клинок | Досягаемость | Дуга | Урон | Чем отличается |
|---|---|---|---|---|
| SABER | 1.40 | 100° | 130 | штатный |
| KNIFE | 1.00 | 55° | 62 | почти без замаха; узкий сектор чаще чист |
| CLEAVER | 1.65 | 135° | 240 | тяжёлый; занести его в тесноте негде |
**Картечь и правило 6.3.** Дробины уходят в ТОТ ЖЕ конус `SpreadRad`, который
проверен на своих перед спуском. Правило не ослаблено ни на градус — просто у
дробовика конус широкий, и выстрел отсекается заметно чаще. Это его цена, а не
исключение из правила.
## Твари
Каталог — [src/sim/target.cpp](../src/sim/target.cpp), поведение —
[src/sim/enemy.cpp](../src/sim/enemy.cpp).
| Вид | HP | Скорость | Урон | Замах | Атака | Роль |
|---|---|---|---|---|---|---|
| SHAMBLER | 210 | 0.95 | 13 | 0.42 | SWIPE | основа толпы; страшен числом |
| RUSHER | 110 | 3.3 | 14 | 0.28 | LUNGE | добегает до перезарядки |
| SPITTER | 110 | 1.15 | 16 | 0.55 | SPIT | единственный, кто достаёт издалека |
| BRUTE | 520 | 1.05 | 46 | 0.95 | SLAM | разбирать вдвоём |
| LURKER | 150 | 1.90 | 9/тик | 0.50 | GRAB | вырывает бойца из строя |
Поведение намеренно простое: заметил ближайшего живого бойца — идёт — дошёл —
**замахивается** — бьёт. Замах (`windupTimer`) виден всегда: тело подсвечивается
и глаза вспыхивают ровным светом даже в темноте. Между замахом и ударом у
игрока есть время увести бойца. Без телеграфа удар твари неотличим от случайной
потери здоровья, и бой перестаёт быть боем.
Ось удара на замахе НЕ доводится: тварь бьёт туда, куда замахнулась. Ровно это
и делает уклонение возможным.
### Пять атак — пять разных задач игроку
Один удар на всех делал любую тварь одинаковой: подошла — сняла здоровье.
Разная ГЕОМЕТРИЯ (`AttackKind`, [src/sim/target.h](../src/sim/target.h)) — то
единственное, ради чего отряд приходится расставлять по-разному.
| Атака | Форма | Что требует от игрока |
|---|---|---|
| SWIPE | сектор `swipeArcDeg` (100° у бредущего) в пределах `attackRange` | уходить **вбок**: назад от короткого удара не успеть |
| LUNGE | рывок на `lungeDist` (3.4) за `lungeTime` (0.20 с); урон всем, кого задело по пути | освобождать **ось**, а не точку: на прямой броска безопасных мест нет |
| SPIT | снаряд `SpawnHostileBullet`, летит `spitRange` (8.4) — дальше, чем плевок «достаёт» | **единственная** атака, которую перекрывает стена |
| SLAM | круг `slamRadius` (2.15) вокруг твари | не толпиться возле громилы: второй боец платит наравне с целью |
| GRAB | захват на `grabTime` (2.6 с), урон тикает раз в `grabTick` (0.45 с) | вытаскивать своего **огнём** |
Рывок — это движение по времени, а не телепорт: позиция бегуна меняется каждый
шаг и упирается в стены как обычно (`map.ResolveCircle`). Каждого бойца один
рывок задевает не больше одного раза (`lungeMask`). Откат считается от КОНЦА
броска, а не от его начала.
Захват держит **ноги и только их**. `Squad::UpdateMovement` не двигает бойца с
`grabbedTimer > 0` — ни строй, ни решатель, ни расступание из чужой линии.
Стрельбу и клинок захват не читает вообще: правило 6.3 и сектор удара работают
как обычно, иначе единственным выходом из хватки был бы чужой выстрел. Сам
таймер гаснет, а держащая тварь продлевает его каждый шаг на `enemyGrabHold`:
убили хвата — продлевать некому, и боец свободен через одну шестую секунды.
Поэтому про захват не знают ни `bullet.cpp`, ни `melee.cpp`.
Общих ручек всего две, обе в [src/tuning.h](../src/tuning.h):
`enemyWindupScale` (длина всех телеграфов разом) и `enemyGrabHold`. Формы —
`swipeArcDeg`, `lungeDist`, `lungeTime`, `spitRange`, `slamRadius`, `grabTime`,
`grabTick`
свойства ВИДА и живут в `EnemyDef` рядом с остальными статами.
Эффекты получают отдельные события (`sim/events.h`): `ENEMY_WINDUP`,
`ENEMY_LUNGE`, `ENEMY_SLAM``owner` = индекс ТВАРИ; `ENEMY_HIT`, `ENEMY_GRAB`,
`ENEMY_RELEASE``owner` = индекс БОЙЦА. Индексы из разных массивов, путать их
нельзя.
**DUMMY по-прежнему инертна.** У мишени приёмки вид атаки выставлен формально, с
нулевыми уроном и дальностью, а `UpdateEnemies` до неё вообще не доходит:
`--accept` меряет ровно то же, что мерял.
Упавшего бойца твари не добивают — иначе отряд, потерявший одного, теряет всех
подряд, пока игрок ничего не может сделать.
Расстановка: `NUM_TARGETS` кучек по прежним точкам приёмки плюс
`NUM_EXTRA_ENEMIES` тварей по всей карте — без вторых зачистка первой кучки
заканчивала вылазку. Хваты стоят только в глубине: захват — ловушка для отряда,
который уже разошёлся по позициям, а не встречающий у порога.
## Разрушаемая обстановка
Ящики, бочки и шкафчики (`CRATE`, `BARREL`, `LOCKER`) живут **в том же пуле
`Target`**, что и твари. Заводить под них вторую сущность было незачем: пуля,
клинок, стена и правило 6.3 работают с ними одинаково — как с телом, у которого
есть позиция, радиус и HP. Ни одну из этих систем трогать не пришлось.
Отличий ровно четыре, и каждое проверяется через `Target::IsProp()`:
| Где | Что иначе |
|---|---|
| `UpdateEnemies` | не шевелится: ни скорости, ни агро, ни удара |
| `UpdateTargets` | не воскресает — разбитый хлам остаётся разбитым |
| выбор цели в `ai/` | **последний приоритет** (ниже) |
| события | `HIT_PROP` / `PROP_BREAK` вместо `HIT_TARGET` / `HIT_KILL` |
Отдельные виды событий нужны не для красоты: слой крови
([src/render/blood.cpp](../src/render/blood.cpp)) знает только позицию события,
и отличить попадание в бочку от попадания в тварь можно ТОЛЬКО видом события.
Иначе ящик кровоточит. Заодно разбитый хлам не идёт ни в `KILLS`, ни в
напряжение, ни в счётчик ударов клинком.
### Приоритет: обстановка — всегда последняя
Правило одно на оба класса оружия и живёт в одной функции —
`EnemyNearby` ([src/ai/firing_solver.h](../src/ai/firing_solver.h)):
> пока в радиусе `propYieldRadius` есть хоть одна живая тварь, ящик целью
> не становится **вообще**.
И стрелок (`EvaluateAgentLane`), и клинок (`EvaluateMeleeTarget`) делают два
прохода по целям: сначала твари, и только при полностью пустом первом проходе —
обстановка. Даже если линию до твари перекрыл свой, а до ящика она чиста, боец
переступает (решатель 6.5), а не разряжает магазин в мебель.
Радиус **свой**, а не дальность ствола. С дальностью правило читалось бы как
«не достаю — стреляю по ящику», и подошедшую тварь боец встречал бы на
перезарядке. Стены его не отменяют намеренно: тварь за углом в пяти шагах — всё
ещё повод не заниматься мебелью.
Линия к хламу никогда не `priorityLane`: приоритет запрещает решателю искать
позицию получше, а ради ящика боец переступать обязан.
Назначенная игроком цель (6.8) правило обходит — это осознанно: явный приказ
сильнее автоматики.
## Снаряжение
[src/sim/items.h](../src/sim/items.h). Предмет даёт бойцу ровно четыре числа:
броня, скорость, запас здоровья, дальность обзора. **Ни один предмет не может
ослабить правило «не бить своих»** — у него просто нет такой ручки, и это не
самоограничение, а устройство: `FireWeapons` и `UpdateMelee` про инвентарь не
знают вообще, они читают уже готовые статы агента.
Броня складывается, но упирается в `ARMOR_CAP = 0.72`: неуязвимых нет.
| Броня | Поглощение | Скорость | Прочее |
|---|---|---|---|
| JACKET | 10% | 100% | — |
| VEST | 28% | 95% | +10 HP |
| PLATE | 48% | 82% | +25 HP |
| SHROUD | 4% | 112% | обзор 92% |
В карман: LANTERN (обзор 135%), CHARM (+30 HP), SPURS (скорость 114%).
Свет как расходник: заряд тратится за путь, а не за время (M23) Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он меняется с тварями местами. Батарея встаёт сама, как бинт. Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия считала по секундам, то есть была таймером, которого в игре нет по прямому требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как секундомер, только без циферблата, и думать над этажом стало бы дорого. За расстояние получается тот же размен, что и с весом, но с другой стороны: вес делает дорогим «унести всё», свет — «обшарить всё». Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды. Проверка в --descend меряет правило целиком, и первая её строка не про баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже был неправ — делил потраченное на смещение отряда, а не на пройденный путь, и показывал 1.55 за тайл при 1.10 в каталоге. свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска обзор 1.00 -> 0.35 в темноте, батарея встала сама Хватает ли заряда на этаж считается в этажах и по средней длине забега: путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы удачу, а не баланс. --accept PASS без изменений: в песочнице света бесконечно, как и патронов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
Расходники: BANDAGE, STIM, AMMO, BATTERY.
Фонарь — единственная вещь с ЗАРЯДОМ: он садится за пройденный путь, и погасший
не даёт ничего. Заряд общий на отряд, батарея встаёт сама
([17-extraction.md](17-extraction.md)).
Документация: границы движка, работа через модель, дизайн игры до лейтгейма 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
Отряд растёт с одного оперативника до пяти (M24) Игра начинается ОДНИМ человеком: тактический нож, слабый фонарь, куртка. Остальные четверо нанимаются за опыт по кривой 600/2400/9600/38400 — самая дорогая покупка в игре и намеренно. Одиночка с ножом это первое впечатление, и продать второго бойца за одну вылазку значило бы выбросить весь хоррор начала: вдвоём уже не страшно. NUM_AGENTS осталась величиной времени сборки и означает теперь ВМЕСТИМОСТЬ, а не размер отряда: делать её динамической это сотня мест ради числа, которое всё равно упирается в пять. Занятость слота это Agent::present, и он отличается от alive. Правила боя читают только alive — для них пустой слот и павший одно, и это верно. Разница нужна там, где показывают человека: у павшего есть тело на полу и перечёркнутая карточка, у пустого слота нет ни того, ни другого. Проверка в --descend меряет цепочку «в профиле -> на этаже» и потолок, а главное — переживает ли одиночка первую схватку: отряд: в профиле 1 -> на этаже 1, после найма 2, цена 600, сверх потолка отказ одиночка с ножом на первом этаже: выжил, 100 -> 41, подошло 2, убил 2 Первая версия этой проверки не измеряла ничего: боец стоял минуту, к нему не подошёл никто, и «выжил» было пустым словом. Теперь отряд ставится рядом с тварью. Попутно найдено и исправлено: Profile::Save три вехи не писала ни версию формата, ни улучшения. Купленный запас патронов действовал до конца запуска и молча пропадал при чтении, а профиль без версии читался как формат 1. Круговой прогон этого не видел, потому что сверял три поля из десяти и делал это на пустом профиле, где всё сходится само. Теперь он сверяет поля списком и на набитом профиле. Формат профиля 3: добавлена строка squad. Старый профиль играл впятером и получает пятерых при миграции — отобрать их значило бы съесть отряд, которого игрок не терял. --accept PASS без изменений: без профиля отряд по-прежнему впятером. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:29:19 +07:00
В забеге с профилем всё иначе: отряд выходит в том, в чём вернулся, и начинается
он **одним оперативником** с ножом, фонарём и курткой — остальные четверо
нанимаются за опыт ([17-extraction.md](17-extraction.md)). Комплекты ниже — это
dev-режим без профиля и песочница приёмки.
Документация: границы движка, работа через модель, дизайн игры до лейтгейма 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
Комплекты раздаются по СЛОТУ СТРОЯ, а не «персонажу» (`Game::EquipKits`):
остриё клина берёт плиту и тесак, хвост — лёгкую броню и дальний ствол. Состав
отряда читается прямо по строю.
## Экраны
![меню](menu.png)
Меню, пауза, экран отряда и поражение живут в [src/ui/menu.cpp](../src/ui/menu.cpp).
Это второе и последнее место в игре, где разрешены примитивы raylib (первое —
`debug/overlay`): здесь нужны слова, а собственный шрифт на 480x270 стоил бы
дороже всего остального UI вместе взятого. Мир по-прежнему рисуется только в
софтверный фреймбуфер, меню ложится поверх готового кадра.
![отряд](squad.png)
Экран отряда (`I` в бою) показывает те же числа, которые читает симуляция:
HP, броню, скорость, обзор. `ENTER` меняет местами оружие в руках и выбранную
ячейку сумки — отряд не носит воздух, у него всегда что-то в руках.
Инвентарь: SHIFT+клик, сортировка схрона, сравнение с надетым (M38) Перетаскивание мышью в кадре 480x270 — это попадание в ячейку 74x18 с зажатой кнопкой, и делать так тридцать раз после каждой вылазки нельзя. * SHIFT + клик отправляет вещь ТУДА, ГДЕ ЕЙ МЕСТО: ствол в руки, плита в броню, брелок в карман, расходник в сумку; из слота — обратно в схрон. Обмен идёт через тот же ApplyDrop, что и перетаскивание: своя копия правил означала бы ещё одно место, где вещь может потеряться. * Кнопка SORT раскладывает схрон: класс -> редкость -> аффиксы -> имя. * Подсказка вещи из схрона дописывает блок NOW: что стоит в этом слоте сейчас и чем новое отличается. --ui-check меряет сохранность, а не факт вызова: вещей было 5 — стало 5, ствол в руках, прежний в схроне, профиль о переодевании знает. Проверка сперва воспроизведена на сломанной сборке: без SaveLoadout печатает «профиль НЕ знает». Замеры: --ui-check поломок 0, --accept PASS, --descend 8 ошибок 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 22:42:28 +07:00
### Как перекладываются вещи
Три способа, и все три ведут в один и тот же обмен (`ApplyDrop`): своя копия
правил в каждом означала бы три места, где вещь может потеряться.
| Способ | Что делает |
|---|---|
| **SHIFT + клик** | отправляет вещь ТУДА, ГДЕ ЕЙ МЕСТО: ствол в руки, плита в броню, брелок в карман, расходник в сумку; из слота — обратно в схрон |
| перетаскивание | точный перенос в конкретную ячейку (порядок схрона — единственное, чем игрок им управляет) |
| ПКМ по вещи | выбросить под ноги; из рук нельзя — безоружный боец не боец |
SHIFT + клик появился потому, что кадр 480x270: ячейка схрона 74x18 пикселей, и
попадать в неё зажатой кнопкой тридцать раз после каждой вылазки невозможно.
Кнопка **SORT** раскладывает схрон: класс → редкость → число аффиксов → имя.
Именно в таком порядке, а не по имени: игрок ищет «во что переодеться», то есть
сначала вид вещи, а внутри вида — лучшее.
Подсказка вещи из схрона или сумки дописывает блок **NOW**: что стоит в том же
слоте сейчас и чем новое от него отличается. Без него выбор снаряжения — это
запоминание чисел: навёл на плиту, увидел «ARMOR +18%», ушёл смотреть на бойца
и уже не помнишь первое.
Всё это проверяется кодом возврата, а не глазом: `--ui-check` меряет
СОХРАННОСТЬ (сколько вещей было в схроне и слотах, столько и осталось), доехало
ли переодевание до профиля и не теряет ли вещи сортировка. Проверка сперва
воспроизведена на сломанной сборке — без записи в профиль она печатает
«профиль НЕ знает» и падает.
Документация: границы движка, работа через модель, дизайн игры до лейтгейма 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
## HUD
[src/render/hud.cpp](../src/render/hud.cpp) рисуется в тот же софтверный
фреймбуфер, что и мир, — значит попадает под ту же сетку пикселей. Текста в нём
нет ни одной буквы:
- цвет полосы — номер бойца, тот же, что у его тела на карте;
- длина полосы — здоровье, тонкая полоска под ней — броня;
- квадратики — патроны, гаснут по одному; сплошная полоса — перезарядка;
- у клинка вместо патронов фаза удара: замах, удар, восстановление;
- уголок над карточкой — боец при цели;
- карточка перечёркнута — боец упал.
Свет как расходник: заряд тратится за путь, а не за время (M23) Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он меняется с тварями местами. Батарея встаёт сама, как бинт. Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия считала по секундам, то есть была таймером, которого в игре нет по прямому требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как секундомер, только без циферблата, и думать над этажом стало бы дорого. За расстояние получается тот же размен, что и с весом, но с другой стороны: вес делает дорогим «унести всё», свет — «обшарить всё». Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды. Проверка в --descend меряет правило целиком, и первая её строка не про баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже был неправ — делил потраченное на смещение отряда, а не на пройденный путь, и показывал 1.55 за тайл при 1.10 в каталоге. свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска обзор 1.00 -> 0.35 в темноте, батарея встала сама Хватает ли заряда на этаж считается в этажах и по средней длине забега: путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы удачу, а не баланс. --accept PASS без изменений: в песочнице света бесконечно, как и патронов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
Исключение одно и на весь HUD — строка забега сверху: этаж, опыт, добыча и
заряд фонаря. Номер этажа формой не покажешь. Погасший фонарь там показан
СЛОВОМ `DARK`, а не полосой нулевой длины: темнота меняет правила, а
«полоски не видно» читалось бы как «полоски тут и не было».
Документация: границы движка, работа через модель, дизайн игры до лейтгейма 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
## Смерть бойца
Мёртвый агент остаётся в массиве: `NUM_AGENTS` — величина времени сборки, по
ней ходят все циклы, слоты строя, метрики и оверлей. Убирать его из пула значило
бы переиндексировать полмира на каждой смерти. Поэтому смерть — это флаг
`Agent::alive`, и его проверяют все системы: движение, расталкивание, реестр
линий, решатель, проверка «свой на линии», сектор клинка, пули.
Павший не перекрывает линию огня и не попадает под лезвие. Тело остаётся на
полу до конца вылазки.
## Сложность
Всё крутится на ходу через оверлей или `--set`:
```bash
squad_proto.exe --set enemyDamage=1.6 # во сколько раз больнее бьют
squad_proto.exe --set agentHP=60 # запас бойца до брони
squad_proto.exe --set enemyRespawn=8 # как быстро твари возвращаются
```
Числа видов правятся в `EnemyDefOf`, числа предметов — в `ItemDefOf`.
Баланс на 24 тварей и 5 бойцов подобран грубо и ждёт живых прогонов: отряд,
стоящий на месте, держится, но постепенно теряет здоровье.