Предмет перестал быть строкой каталога. ItemInstance несёт редкость и до
шести аффиксов, и «карабин» с первого этажа больше не равен «карабину» с
сорокового. Четыре редкости, десять тегов, тир и бюджет от глубины.
Аффиксы не заводят новых каналов влияния: каждый тег ложится в то, что уже
есть — урон оружия, смесь стихий, пробитие, четыре числа бойца, ресурсы
вылазки. У предмета по-прежнему нет ручки к правилу «не бить своих», и
появиться ей неоткуда.
ItemInstance неявно приводится к ItemId, поэтому полторы сотни мест,
которым нужен ВИД вещи, остались как были. Цена решения — присваивание в
ItemId молча теряет рулоны; таких мест три (подбор, схрон, запись
профиля), и все они работают с экземпляром целиком.
Замеры поймали три расхождения дизайн-документа с самим собой:
* формула редкости противоречила таблице рядом с ней: 2.7% находок с
аффиксами на первом этаже против 22%. Выбрана формула, наклон поднят
до схождения с таблицей на дне (12% обычных на сотом);
* бюджет пускал восемь высших аффиксов вместо обещанных шести;
* предельная вещь собиралась к семидесятому этажу вместо сотого.
Числа пересчитаны, документ исправлен: 6·cost(9) = 294 <= B(100) = 295 <
7·cost(9) = 343. Предельная вещь существует ровно на дне и ровно одна.
Проверка в --descend меряет не бросок, а ПРОВОДКУ до бойца — там, где
дважды умирали вес и стихии:
аффиксы: урон 12.0 -> 18.0, здоровье 100 -> 140, броня 0.10 -> 0.25,
пробитие 0.20, KIN 1.00 -> 0.60, CRY 0.40, сумма долей 1.000
Последнее число не украшение: конверсия обязана переводить урон, а не
добавлять седьмую долю сверх единицы.
Попутно, на глубине 30 и только там:
* «зачистка поля» в проверках не держалась — у твари есть откат
воскрешения, и с нулевым таймером она оживала тем же шагом. Лечение
вне схватки и расход света на стоянке молча мерили бой. На восьмом
этаже тварей мало, и проверка проходила по удаче;
* порог «стоянка бесплатна» стоял в единицах заряда, а тот же шаг стоит
дороже с глубиной. Переведён в тайлы — и сразу показал, что отряд в
строю проходит 0.8 тайла за две секунды «стоянки». Свет теперь
списывается только в ADVANCING: топтание и драка бесплатны, тратит
РАЗВЕДКА. Без этого таймер возвращался через чёрный ход.
Формат профиля 4: токен предмета вида rifle@relic+dmg7,0:0.63. Миграции
не требует — у старых токенов нет хвоста после '@'.
--accept PASS без изменений: в песочнице находок нет вовсе.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
20 KiB
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) |
| M9 | игровой слой: 5 стволов и 3 клинка, 4 вида тварей с телеграфом удара, снаряжение с инвентарём, HUD, меню, экран отряда, безрамочный полный экран | --accept PASS без изменений (бой живёт под combatMode, приёмка — в песочнице), 36 с боя без потерь в отряде (13-combat.md) |
| M10 | разрушаемая обстановка (ящик, бочка, шкафчик) в пуле Target с последним приоритетом цели; силуэты тварей вместо капсул + LURKER; варианты плиты пола; анимированные твари из SpriteForge (--sf-enemies / --sf) |
--accept PASS без изменений (обстановки в песочнице нет), метки целей в бою — только твари при живом противнике рядом; sf validate 0 ошибок на 8 ассетах, --sprites читает все шесть циклов шага (13-combat.md, 12-assets.md) |
| M11 | движок отдельной целью сборки (tile2d) и редактор комнат отдельным приложением (tile2d_editor.exe); каталог размещаемых сущностей данными; формат комнаты-шаблона с таблицами весов спавна; сборка уровня из комнат по сиду; темы отделки слоем Tilemap::theme[] |
tile2d_editor --check: 8 комнат OK, круговой прогон через запись сходится, 5 сидов без недостижимых и срезанных сущностей; --accept PASS без изменений (набор комнат приёмка не подключает) (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) |
| M13 | экстракшен: эвакуация там, где вошли, профиль со схроном и снаряжением между вылазками, чекпоинты каждые 5 этажей, экран итога с вынесенным и потерянным, сборы на экране отряда | --descend 7: сумки доезжают вниз, эвакуация уносит 40 предметов, профиль сходится после записи; --accept PASS без изменений (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) |
| 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) |
| M16 | цикл в движке: App (окно, безрамочный режим, кадр, fixed timestep с интерполяцией) вместо двух копий в игре и редакторе; DrawTilemap — движок научился показывать свою карту; третье приложение samples/walk |
игра и редактор перешли на App без изменений в поведении: --accept PASS, забеги и --tool validate те же; пример собирает и рисует этаж, не линкуясь ни с чем из src/ (18-engine.md) |
| M17 | вес: у каждого предмета своя тяжесть, два порога переноски (до нормы свободно, дальше медленнее, выше предела не поднимет), сброс вещи под ноги правой кнопкой, нагрузка в HUD и на экране отряда | проверка правила в --descend: налегке 6.9 → 100% скорости, с грузом 12.9 → 63%, отказ приходит по ВЕСУ, а не по ячейкам; две первые калибровки порогов провалились молча и были пойманы этой же проверкой; --accept PASS без изменений (17-extraction.md) |
| M18 | ресурс вместо таймера: конечный боезапас на вылазку (перезарядка тянет из запаса, запас едет вниз), пачка патронов как находка, расходники применяются сами — бинт вне схватки, патроны когда стрелять нечем | проверка правил в --descend: запас 40 → 16 при магазине 24, бинт лечит 33 → 58, обе вещи уходят из сумки; проверка нашла, что боец с клинком не тратил найденные патроны при запасном стволе в сумке; --accept PASS без изменений — в песочнице боезапас бесконечен намеренно (17-extraction.md) |
| M19 | сумка одна на отряд (16 ячеек, вес делится на живых), надетое остаётся личным; подсказка под курсором на каждой вещи и на каждой строке характеристик | --descend 8: из сумки в схрон доезжают все 16, эвакуация выносит 26; проверка переноса поймала, что опыт с расходниками разбирал сумку до замера, и что массив виджетов экрана переполнился — из схрона молча пропали ячейки; --accept PASS без изменений (17-extraction.md) |
| M20 | опыт тратится: четыре ресурсных улучшения отряда (патроны, переноска, обзор, живучесть) в профиле, экран UPGRADES, цена растёт с уровнем | проверка в --descend: покупка списывает опыт, здоровье 110 → 122 и запас 120 → 140 на следующей вылазке; проверка дважды ломала сам прогон (вызывала NewRun посреди забега) и была отправлена в конец; --accept PASS — приёмка профиля не подключает (17-extraction.md) |
| M21 | стихии: семь элементов, сопротивления видом, единственная формула урона; вторая поверхность инструментов squad_proto --tool (weapons, enemies, ttk, economy) | --tool ttk сперва показал, что стихии меняют урон на 4–36%, то есть механика декоративна; после правки содержимого все пять видов в коридоре 1.9–3.1x; --accept PASS без изменений — стихии есть только у находимого оружия (21-damage.md) |
| M22 | статусы стихий и четыре реакции (SHATTER, IGNITE, SUPERCOND, DECAY) с силой от ГЛУБИНЫ, а не от урона оружия; три ограничителя — потолок стаков, откат на цели, запрет рекурсии | --tool reactions: все четыре срабатывают, площадные задевают соседа вполовину, стаки гасятся, вторая пара внутри отката даёт 0.0, урон от реакции не кладёт статусов; шаг симуляции 2.9 мкс из 16666; --accept PASS (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) |
| M24 | отряд растёт с ОДНОГО оперативника до пяти: размер в профиле, найм за опыт по кривой 600/2400/9600/38400, стартовый комплект одиночки — нож, фонарь, куртка; пустой слот отличается от павшего (present против alive) | проверка в --descend: в профиле 1 → на этаже 1, найм доводит второго, шестой не нанимается ни за какие деньги; одиночка с ножом на первом этаже выживает и убивает двух тварей, теряя 59 из 100 здоровья; первая версия этой проверки была ПУСТОЙ — боец стоял минуту, к нему не подошёл никто, и «выжил» ничего не значило; попутно нашлось, что Profile::Save три вехи не писала ни версию, ни улучшения, а круговой прогон сверял три поля из десяти и потому этого не видел; --accept PASS без изменений — без профиля отряд по-прежнему впятером (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) |
Что находилось по ходу и чинилось
Прогоны без окна оказались полезнее, чем разглядывание кадра: три дефекта, которые на глаз не видны, нашлись именно так.
-
Отставшие агенты.
--probeпоказал агента наy=12, когда отряд стоял наy=26. Причина — залипание в «тени» стены (локальный минимум seek против отталкивания) плюс отсутствие правила «идти к отряду, когда стрелять не по чему». Починено касательным обходом стен и возвратом в слот строя (A5, A6). -
Резерв линии, который исчезает, когда нужен. Линии регистрировались только у реально стреляющих. Стоит кому-то влезть в линию — стрелок теряет линию, отрезок пропадает из реестра, и перекрывшего ничто не выталкивает (A3).
-
Пуля, летящая за цель. Последнее упорное попадание по своим (
t=94.49, воспроизводилось при любых настройках) — снаряд проходил место умершей мишени и находил своего за ней (A1). -
Выход за границы в блиттере спрайтов.
Framebuffer::Blitвычислял адрес строки спрайта до внутреннего цикла. Если спрайт срезан левым краем экрана больше, чем на свою ширину (sx0 >= s.w), индекс уезжал за конецpx: цикл пустой, разыменования нет, но индекс уже невалиден. Release это молча глотал, Debug валился сvector subscript out of range. Починено ранним выходом при пустом диапазоне обрезки.Вывод на будущее: проверять в Debug-сборке. В Release такие ошибки не проявляются — там нет проверок итераторов MSVC.
-
Критерий 11.6 мерил не то, что означал. Тест требовал, чтобы в коридоре огонь вёл передний, и считал ВСЕ выстрелы. Диагностика показала, что «нарушения» — это хвостовой агент, стреляющий назад, в оставленную позади комнату: к правилу «не стрелять сквозь своего» это отношения не имеет, впереди по такой линии никого нет по построению. Теперь считаются только выстрелы по ходу движения, и результат стал строже: 5/0 вместо 12/4.
-
Двое с клинками на одной мишени. Взмахи обрывались друг об друга, урона почти не было. Починено ценой цели: 37 взмахов / 31 попадание → 74 / 75.
Подробности и замеры по каждому — 08-decisions.md.
Текущий статус
Все измеримые критерии раздела 11 проходят: squad_proto.exe --accept →
«все проверяемые критерии PASS». Цифры — 09-acceptance.md.