# 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** | оверлей F1–F4, метрики, журнал попаданий по своим, runtime-тюнинг `[ ] - =`, `--set` | скриншот боевого кадра: зелёные линии у всех пятерых, FRIENDLY_HITS 0, uptime 99 % | | **M7** | ближний бой: классы оружия, сектор удара, три фазы, свой больший круг, дуга; отзывчивый диск на поводке | `--accept`: M7.1–M7.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` сперва показал, что стихии меняют урон на 4–36%, то есть механика декоративна; после правки содержимого все пять видов в коридоре 1.9–3.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** | семь новых тварей на глубины 9–73: акты II–VI перестали быть пустыми | `--tool enemies` и `--tool ttk`: каждая тварь попадает в свою полосу глубины, время убийства держится в коридоре ([16-descent.md](16-descent.md)) | | **M35** | 19 новых вещей (тиры 1–6, глубины 3–31), исправлена кривая редкости и выбор тиров аффиксов | `--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).