squad-proto/docs/07-milestones.md
z.kirill b29358ad12 Инвентарь: 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 18:42:28 +03:00

106 lines
31 KiB
Markdown
Raw Permalink 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.

# 07 — Журнал вех
Порядок из раздела 10 спека: после каждой вехи сборка и запуск, потом следующая.
| Веха | Что сделано | Чем проверено |
|---|---|---|
| **M0** | CMake + raylib через FetchContent, фреймбуфер 480×270, ×3 апскейл, fixed timestep, изометрическая сетка | сборка MSVC x64, окно открывается, текстура 480×270 RGBA8 создаётся |
| **M1** | тайлмап 48×48, генерация уровня, процедурные спрайты пола и стен, камера за якорем, сортировка по глубине | скриншот: ромбы пола шахматкой, кубы стен перекрывают друг друга правильно |
| **M2** | 5 агентов, движение якоря в экранных осях, три строя, separation, коллизии со стенами | скриншот: пятеро в клину, цвета различимы, стены не проходятся |
| **M3** | мишени с респавном, DDA-рейкаст, `LaneClear`, авто-огонь, пули-снаряды, дружественный урон, `FRIENDLY_HITS` | `--headless 60 --no-solver --no-laneavoid`: 1 попадание по своим, uptime 13.6 % — то есть проблема, которую должно решать ядро, воспроизводится |
| **M4** | решатель огневых позиций, гистерезис и коммит, реестр линий и расступание | то же 60 с с включённым ядром: 0 попаданий, uptime 88 % на стоянке у мишеней |
| **M5** | ручное прицеливание, конус ±20°, назначенная цель, `Tab` | сценарий `11.7` в `--accept`: 0 попаданий по своим при активной назначенной цели |
| **M6** | оверлей F1F4, метрики, журнал попаданий по своим, runtime-тюнинг `[ ] - =`, `--set` | скриншот боевого кадра: зелёные линии у всех пятерых, FRIENDLY_HITS 0, uptime 99 % |
| **M7** | ближний бой: классы оружия, сектор удара, три фазы, свой больший круг, дуга; отзывчивый диск на поводке | `--accept`: M7.1M7.3 PASS, 81 взмах / 81 попадание / 23 убийства за 30 с, FRIENDLY_HITS 0; все критерии спека 11 по-прежнему PASS |
| **M8** | конвейер ассетов SpriteForge: манифесты, загрузчик `.sfa` в игровой `Sprite`, hot reload по `sf watch`, dev-режимы `--sprites` и `--sf-tiles` | `sf validate` 0 ошибок, `--sprites` читает все три ассета, тайлы подменены на живой игре без перезапуска ([12-assets.md](12-assets.md)) |
| **M9** | игровой слой: 5 стволов и 3 клинка, 4 вида тварей с телеграфом удара, снаряжение с инвентарём, HUD, меню, экран отряда, безрамочный полный экран | `--accept` PASS без изменений (бой живёт под `combatMode`, приёмка — в песочнице), 36 с боя без потерь в отряде ([13-combat.md](13-combat.md)) |
| **M10** | разрушаемая обстановка (ящик, бочка, шкафчик) в пуле `Target` с последним приоритетом цели; силуэты тварей вместо капсул + `LURKER`; варианты плиты пола; анимированные твари из SpriteForge (`--sf-enemies` / `--sf`) | `--accept` PASS без изменений (обстановки в песочнице нет), метки целей в бою — только твари при живом противнике рядом; `sf validate` 0 ошибок на 8 ассетах, `--sprites` читает все шесть циклов шага ([13-combat.md](13-combat.md), [12-assets.md](12-assets.md)) |
| **M11** | движок отдельной целью сборки (`tile2d`) и редактор комнат отдельным приложением (`tile2d_editor.exe`); каталог размещаемых сущностей данными; формат комнаты-шаблона с таблицами весов спавна; сборка уровня из комнат по сиду; темы отделки слоем `Tilemap::theme[]` | `tile2d_editor --check`: 8 комнат OK, круговой прогон через запись сходится, 5 сидов без недостижимых и срезанных сущностей; `--accept` PASS без изменений (набор комнат приёмка не подключает) ([15-engine-and-editor.md](15-engine-and-editor.md)) |
| **M12** | спуск как жанр: глубина первичным понятием движка (`DescribeFloor`), бюджет опасности вместо голого шанса, лут на полу с пассивным подбором, точка спуска и перенос отряда между этажами, сжатие обзора с глубиной, растровый шрифт 3x5 для номера этажа | `tile2d_editor --check`: покрытие глубин 1..16 и сборка на 1/3/6/10/16/24 без ошибок, бюджет выбирается полностью; `--descend 8`: сумки доезжают вниз (8→30 предметов); `--accept` PASS без изменений ([16-descent.md](16-descent.md)) |
| **M13** | экстракшен: эвакуация там, где вошли, профиль со схроном и снаряжением между вылазками, чекпоинты каждые 5 этажей, экран итога с вынесенным и потерянным, сборы на экране отряда | `--descend 7`: сумки доезжают вниз, эвакуация уносит 40 предметов, профиль сходится после записи; `--accept` PASS без изменений ([17-extraction.md](17-extraction.md)) |
| **M14** | Tile2D как самостоятельный движок: `engine/` своим каталогом и целью, `engine/config.h` вместо зависимости от `tuning.h`, проекция отдельным швом и второй вид отображения (сверху), формат проекта с путями и кривой глубины, игра стала проектом `projects/descent` | компилятор нашёл три зависимости движка от игры (`tuning.h`, `MAX_TARGETS`, `SQUAD_START_*`) — все сняты; `tile2d_editor --check` и `--descend` проходят на ОБОИХ проектах, на втором сразу нашлась глухая камера в комнате `maw`; `--accept` PASS без изменений ([18-engine.md](18-engine.md)) |
| **M15** | движок и языковая модель: инструменты `--tool` (проект, комнаты, каталог, кривая, сборка этажа, ASCII-карта, проверка набора), скиллы `.claude/commands/{tile2d,room,floor}.md`, терминал в редакторе с режимами shell/claude/codex и внешним интерактивным CLI | `--tool validate` даёт код 0 на обоих проектах и код 1 на подложенной поломке; `claude -p` отвечает из терминала редактора (docs/editor-terminal.png); `--accept` PASS без изменений ([19-agent.md](19-agent.md)) |
| **M16** | цикл в движке: `App` (окно, безрамочный режим, кадр, fixed timestep с интерполяцией) вместо двух копий в игре и редакторе; `DrawTilemap` — движок научился показывать свою карту; третье приложение `samples/walk` | игра и редактор перешли на `App` без изменений в поведении: `--accept` PASS, забеги и `--tool validate` те же; пример собирает и рисует этаж, не линкуясь ни с чем из `src/` ([18-engine.md](18-engine.md)) |
| **M17** | вес: у каждого предмета своя тяжесть, два порога переноски (до нормы свободно, дальше медленнее, выше предела не поднимет), сброс вещи под ноги правой кнопкой, нагрузка в HUD и на экране отряда | проверка правила в `--descend`: налегке 6.9 → 100% скорости, с грузом 12.9 → 63%, отказ приходит по ВЕСУ, а не по ячейкам; две первые калибровки порогов провалились молча и были пойманы этой же проверкой; `--accept` PASS без изменений ([17-extraction.md](17-extraction.md)) |
| **M18** | ресурс вместо таймера: конечный боезапас на вылазку (перезарядка тянет из запаса, запас едет вниз), пачка патронов как находка, расходники применяются сами — бинт вне схватки, патроны когда стрелять нечем | проверка правил в `--descend`: запас 40 → 16 при магазине 24, бинт лечит 33 → 58, обе вещи уходят из сумки; проверка нашла, что боец с клинком не тратил найденные патроны при запасном стволе в сумке; `--accept` PASS без изменений — в песочнице боезапас бесконечен намеренно ([17-extraction.md](17-extraction.md)) |
| **M19** | сумка одна на отряд (16 ячеек, вес делится на живых), надетое остаётся личным; подсказка под курсором на каждой вещи и на каждой строке характеристик | `--descend 8`: из сумки в схрон доезжают все 16, эвакуация выносит 26; проверка переноса поймала, что опыт с расходниками разбирал сумку до замера, и что массив виджетов экрана переполнился — из схрона молча пропали ячейки; `--accept` PASS без изменений ([17-extraction.md](17-extraction.md)) |
| **M20** | опыт тратится: четыре ресурсных улучшения отряда (патроны, переноска, обзор, живучесть) в профиле, экран UPGRADES, цена растёт с уровнем | проверка в `--descend`: покупка списывает опыт, здоровье 110 → 122 и запас 120 → 140 на следующей вылазке; проверка дважды ломала сам прогон (вызывала NewRun посреди забега) и была отправлена в конец; `--accept` PASS — приёмка профиля не подключает ([17-extraction.md](17-extraction.md)) |
| **M21** | стихии: семь элементов, сопротивления видом, единственная формула урона; вторая поверхность инструментов `squad_proto --tool` (weapons, enemies, ttk, economy) | `--tool ttk` сперва показал, что стихии меняют урон на 436%, то есть механика декоративна; после правки содержимого все пять видов в коридоре 1.93.1x; `--accept` PASS без изменений — стихии есть только у находимого оружия ([21-damage.md](21-damage.md)) |
| **M22** | статусы стихий и четыре реакции (SHATTER, IGNITE, SUPERCOND, DECAY) с силой от ГЛУБИНЫ, а не от урона оружия; три ограничителя — потолок стаков, откат на цели, запрет рекурсии | `--tool reactions`: все четыре срабатывают, площадные задевают соседа вполовину, стаки гасятся, вторая пара внутри отката даёт 0.0, урон от реакции не кладёт статусов; шаг симуляции 2.9 мкс из 16666; `--accept` PASS ([21-damage.md](21-damage.md)) |
| **M23** | свет как расходник: заряд фонаря общий на отряд и тратится за ПРОЙДЕННЫЙ ПУТЬ, а не за время; погас — обзор 35% и твари замечают в 1.35 раза дальше; батарея встаёт сама, как бинт | проверка в `--descend`: стоянка стоит 0.2 из 420 (то есть таймера нет), ход 1.10 за тайл при 1.10 в каталоге, заряда на 4.0 прямых пути до спуска, обзор 1.00 → 0.35, батарея сработала; первая версия расхода была ПОСЕКУНДНОЙ — то есть таймером, которого в игре нет по требованию к жанру, и переписана на путь; измеритель тоже был неправ (делил на смещение, а не на путь) и показывал 1.55 против 1.10; `--accept` PASS без изменений — в песочнице свет бесконечен ([17-extraction.md](17-extraction.md)) |
| **M24** | отряд растёт с ОДНОГО оперативника до пяти: размер в профиле, найм за опыт по кривой 600/2400/9600/38400, стартовый комплект одиночки — нож, фонарь, куртка; пустой слот отличается от павшего (`present` против `alive`) | проверка в `--descend`: в профиле 1 → на этаже 1, найм доводит второго, шестой не нанимается ни за какие деньги; одиночка с ножом на первом этаже выживает и убивает двух тварей, теряя 59 из 100 здоровья; первая версия этой проверки была ПУСТОЙ — боец стоял минуту, к нему не подошёл никто, и «выжил» ничего не значило; попутно нашлось, что `Profile::Save` три вехи не писала ни версию, ни улучшения, а круговой прогон сверял три поля из десяти и потому этого не видел; `--accept` PASS без изменений — без профиля отряд по-прежнему впятером ([17-extraction.md](17-extraction.md)) |
| **M25** | редкости и аффиксы: экземпляр предмета вместо вида (`ItemInstance`), четыре редкости, десять тегов, бюджет и тир по глубине; выкатанное ложится в те же четыре числа бойца и в стихии оружия; инструменты `--tool drops` и `--tool affix` | замеры поймали ТРИ расхождения дизайн-документа с самим собой: формула редкости противоречила своей же таблице (2.7% против 22% на первом этаже), бюджет пускал восемь высших аффиксов вместо обещанных шести, а предельная вещь собиралась к семидесятому этажу вместо сотого — числа пересчитаны, документ исправлен; проверка «доезжает до бойца» показала урон 12→18, здоровье 100→140, броню 0.10→0.25, конверсию KIN 1.00→0.60 при сумме долей ровно 1.000; `--accept` PASS без изменений ([22-loot.md](22-loot.md)) |
| **M26** | `--ui-check`: пункты меню проверяются кодом возврата, как всё остальное | нашлось по жалобе «игра крашится при открытии улучшений»: пункт UPGRADES не крашил, а ВЫХОДИЛ из игры — в обработчике стоял `default: quit`, а экран улучшений добавили четвёртым пунктом, и обработчик о нём не узнал; проверка сперва воспроизведена на сломанной сборке (код 1, «пункт UPGRADES не делает ничего») и только потом объявлена рабочей ([01-build-and-run.md](01-build-and-run.md)) |
| **M27** | материалы и мастерская: пять материалов со своими полосами добычи, пять операций над вещью, экран WORKSHOP; материалы — валюта, зачисляются только при эвакуации | `--tool craft` поймал, что материалы бесплатны: с твари падало по её danger, и вылазка на 80 этаже приносила тысячу ядер при цене доводки в шестьсот (0.6 вылазки на предмет вместо восьми) — правило заменено на «одно ядро с мелочи, два с крупной», стало 6.2; сам инструмент тоже считал неверно, беря доход из бюджета опасности, хотя на этаж влезает не больше MAX_LEVEL_SPAWNS тварей; залом замерен на тысяче попыток: 11.6% потерь при заявленных 12%, платят всегда, слот теряется всегда; `--accept` PASS ([22-loot.md](22-loot.md)) |
| **M28** | триггеры: четыре условных эффекта (ON_HIT поджигает, ON_KILL даёт рывок стаками, ON_HURT обжигает ударившего, ON_DOWN даёт ярость отряду) отдельным тегом аффикса; три ограничителя — откат по видам, бюджет предмета, запрет рекурсии | дизайн предлагал вешать триггеры на журнал событий — так делать нельзя: журнал это канал ЭФФЕКТОВ, в headless его нет вовсе, и правила боя стали бы зависеть от наличия окна; сделано срабатыванием в месте урона; проверка в `--descend`: аффикс доезжает до бойца, ON_HIT кладёт стак, внутри отката 0, ON_KILL даёт 1.06 скорости, за шаг не больше одного стака; проверка сперва брала `targets[0]` и падала на hollow, где первым в пуле стоял ящик; шаг симуляции 3.7 мкс; `--accept` PASS ([21-damage.md](21-damage.md)) |
| **M29** | правки по игре: первый этаж вдвое тише и светлее (бюджет 2.2, бегуны с 4-го, своя яркость этажа), опыт платится за убийства сразу, вайп оставляет ~половину добычи с поправкой на редкость, аномалия возврата переехала к спуску, чекпоинты через 50 этажей, оперативники НАХОДЯТСЯ в капсулах вместо покупки за опыт | замер на сотне вайпов: доезжает 52% (обычное 69%, реликты 25%) — первая версия проверки считала одну сумку и получила «обычного доехало 112%», потому что спасение забирает ещё и надетое; опыт 558 -> 578 за убийство без эвакуации; капсула поднимает отряд до двух и даёт наёмнику свои числа; `--accept` PASS ([17-extraction.md](17-extraction.md)) |
| **M30** | личинка для первых этажей (бредущий уехал на третий), парирование клинком с ответным ударом и отбиванием плевков, инструмент `--wall` | `--wall` нашёл, что `PlaceSquad` воскрешал незанятые слоты: прежний замер «одиночка выжил на первом этаже» был боем впятером, а на самом деле одиночка с ножом проигрывал двум бредущим (210 здоровья против ножа); стена растёт с силой отряда — D7 у одиночки против D30 у пятерых; парирование проверено разменом целиком: нож 34% против тесака 8%, отбив снимает урон, ответ проходит, откат не сбрасывается, стоя столбом не отбивает; `--accept` PASS ([11-melee.md](11-melee.md)) |
| **M32** | дерево улучшений вместо четырёх покупок: пять ветвей по восемь узлов, пять уровней у каждого, цепочка открытия, цена растёт от вложенного; старые покупки возвращают опыт в банк | `--tool economy` поймал, что первая версия дерева стоит 9% одного прохода 1..100 — то есть выбор кончался к сороковому этажу; введено общее удорожание (каждая вложенная точка поднимает цену следующих), стало 34 млн против 41 млн заложенных; проводка проверена по всем пяти ветвям: урон 12.0 → 14.4, здоровье 100 → 150, скорость 1.06 → 1.22, парирование 0.34 → 0.49, свет 420 → 620, закрытый узел не покупается; `--accept` PASS ([17-extraction.md](17-extraction.md)) |
| **M33** | крафт стал выбором: закалка (двигает рулон выбранного аффикса) и отпечаток (переносит аффикс с другой вещи, сжигая её), отсечение и возвышение работают по ВЫБРАННОМУ аффиксу, направленные операции вдвое дороже слепых | прежние пять операций были слепыми — игрок жал кнопку и смотрел, что выпало; проверка меряет направленность, а не срабатывание: отсечение пробуется на СРЕДНЕМ аффиксе из трёх, иначе слепая версия прошла бы её тоже; `--accept` PASS ([22-loot.md](22-loot.md)) |
| **M34** | семь новых тварей на глубины 973: акты IIVI перестали быть пустыми | `--tool enemies` и `--tool ttk`: каждая тварь попадает в свою полосу глубины, время убийства держится в коридоре ([16-descent.md](16-descent.md)) |
| **M35** | 19 новых вещей (тиры 16, глубины 331), исправлена кривая редкости и выбор тиров аффиксов | `--tool drops` и `--tool affix`: добыча перестала быть мусором, ядро тега гарантируется ([22-loot.md](22-loot.md)) |
| **M36** | пистолет не пропадает при переодевании, эвакуация с ЛЮБОГО этажа, кнопки игровых меню снизу HUD, твари не воскресают в пределах забега | пропажа была потерей экземпляра: снаряжение бойца не сохранялось в профиль после перекладывания; `--ui-check` и `--descend 8` PASS ([13-combat.md](13-combat.md)) |
| **M37** | **поле путей**: отставший боец возвращается в строй по геометрии уровня, а не по прямой; обход включается только тому, кто упёрся и потерял строй | это был тот самый «отряд не проходит там, где одиночка проходит»: `--play 6 5` вставал на первом этаже на всех восьми сидах, стало 47 из 48 (единственная потеря — вайп в бою); по дороге вскрылись ДВЕ неверные мерки: критерий 11.6 награждал брошенных за стеной бойцов (растянутый отряд = «колонна»), а `--wall` читал «стены не нашлось» как «стена на нулевом этаже» и объявлял лучший результат провалом; обе переписаны и проверены на сломанной сборке; шаг симуляции 6.1 мкс; `--accept` PASS ([05-squad-ai.md](05-squad-ai.md), [08-decisions.md](08-decisions.md)) |
| **M38** | инвентарь перестал быть перетаскиванием: SHIFT+клик отправляет вещь туда, где ей место, кнопка SORT раскладывает схрон, подсказка показывает, что заменится | `--ui-check` меряет не «функция вызвалась», а СОХРАННОСТЬ: вещей было 5 — стало 5, ствол в руках, прежний в схроне, профиль знает; проверка воспроизведена на сломанной сборке (без записи в профиль печатает «профиль НЕ знает») ([13-combat.md](13-combat.md)) |
## Что находилось по ходу и чинилось
Прогоны без окна оказались полезнее, чем разглядывание кадра: три дефекта,
которые на глаз не видны, нашлись именно так.
1. **Отставшие агенты.** `--probe` показал агента на `y=12`, когда отряд стоял на
`y=26`. Причина — залипание в «тени» стены (локальный минимум seek против
отталкивания) плюс отсутствие правила «идти к отряду, когда стрелять не по чему».
Починено касательным обходом стен и возвратом в слот строя (A5, A6).
2. **Резерв линии, который исчезает, когда нужен.** Линии регистрировались только у
реально стреляющих. Стоит кому-то влезть в линию — стрелок теряет линию, отрезок
пропадает из реестра, и перекрывшего ничто не выталкивает (A3).
3. **Пуля, летящая за цель.** Последнее упорное попадание по своим
(`t=94.49`, воспроизводилось при любых настройках) — снаряд проходил место
умершей мишени и находил своего за ней (A1).
4. **Выход за границы в блиттере спрайтов.** `Framebuffer::Blit` вычислял адрес
строки спрайта до внутреннего цикла. Если спрайт срезан левым краем экрана
больше, чем на свою ширину (`sx0 >= s.w`), индекс уезжал за конец `px`:
цикл пустой, разыменования нет, но индекс уже невалиден. Release это молча
глотал, Debug валился с `vector subscript out of range`. Починено ранним
выходом при пустом диапазоне обрезки.
Вывод на будущее: **проверять в Debug-сборке**. В Release такие ошибки
не проявляются — там нет проверок итераторов MSVC.
5. **Критерий 11.6 мерил не то, что означал.** Тест требовал, чтобы в коридоре
огонь вёл передний, и считал ВСЕ выстрелы. Диагностика показала, что
«нарушения» — это хвостовой агент, стреляющий **назад**, в оставленную позади
комнату: к правилу «не стрелять сквозь своего» это отношения не имеет, впереди
по такой линии никого нет по построению. Теперь считаются только выстрелы по
ходу движения, и результат стал строже: 5/0 вместо 12/4.
6. **Двое с клинками на одной мишени.** Взмахи обрывались друг об друга, урона
почти не было. Починено ценой цели: 37 взмахов / 31 попадание → 74 / 75.
Подробности и замеры по каждому — [08-decisions.md](08-decisions.md).
## Текущий статус
Все измеримые критерии раздела 11 проходят: `squad_proto.exe --accept`
«все проверяемые критерии PASS». Цифры — [09-acceptance.md](09-acceptance.md).