# 12. Ассеты: конвейер SpriteForge ## Что где лежит SpriteForge — внешний инструмент, отдельный репозиторий: `C:\Users\uuu\Documents\spriteforge`. Игра его исходники не трогает и ничего под конкретную игровую сущность в нём не правит. Игра хранит только: | Путь | Что это | В git | |---|---|---| | `assets/manifests/*.yaml` | описания ассетов — единственный источник истины | да | | `assets/sources/` | исходные PNG и риги для бэкендов `import` / `blender` | да | | `generated/spriteforge_assets.h` | числовые ID ассетов, вывод `sf codegen` | да | | `build/sprites/` | собранные `.sfa`, `palettes.sfp`, `index.json` | нет | | `.sfcache/` | контентный кеш сборки, одноразовый | нет | Собранная библиотека живёт внутри `build/`, поэтому полная очистка каталога сборки сносит и её. Восстановление — одна команда `sf build`; кеш `.sfcache/` лежит отдельно и переживает такую чистку, так что пересборка почти мгновенная. ## Установка ```powershell cd C:\Users\uuu\Documents\spriteforge python -m pip install -e . sf version ``` Установка editable: правки в SpriteForge видны игре без переустановки. Если `sf` не находится, каталог скриптов Python не в PATH — либо добавить `%APPDATA%\Python\Python313\Scripts`, либо звать `python -m spriteforge.cli`. ## Добавить ассет Порядок ровно такой, шаги не пропускать: 1. Поправить манифест в `assets/manifests/`. 2. План сборки, без записи файлов: ```powershell sf build assets\manifests\environment.yaml --asset stone_altar --dry-run ``` 3. Собрать только этот ассет: ```powershell sf build assets\manifests\environment.yaml --asset stone_altar ` --output build\sprites --cache-dir .sfcache ``` 4. Проверить текстом: ```powershell sf validate assets\manifests\environment.yaml --build-dir build\sprites sf ascii stone_altar --build-dir build\sprites ``` 5. Обновить числовые ID: ```powershell sf codegen --build-dir build\sprites --output generated\spriteforge_assets.h ``` Сборка всей библиотеки: ```powershell sf build assets\manifests\environment.yaml --output build\sprites ` --cache-dir .sfcache --jobs 8 sf validate assets\manifests\environment.yaml --build-dir build\sprites sf codegen --build-dir build\sprites --output generated\spriteforge_assets.h ``` `--rebuild-palette` пересчитывает общую палитру библиотеки и допустим только при полной сборке без фильтров: частичная пересборка палитры обесценила бы индексы в незатронутых ассетах. ## Контроль результата Агент проверяет сборку **только текстом**: `sf list`, `sf describe`, `sf ascii`, `sf validate`, `sf stats`. `sf contact-sheet` делает PNG для человека — агент его не открывает. Со стороны C++ есть своя проверка: `squad_proto.exe --sprites` печатает, что загрузчик реально видит в библиотеке. ``` build/sprites: 3 ассет(ов) ID HASH FRAMES SIZE PIVOT floor_slab 78D3F27E 1 30x16 15,8 floor_slab_dim 30CD3FBB 1 30x16 15,8 wall_block 892CB17B 1 30x32 15,31 ``` ## Загрузчик `engine/sfa_library.h` — единственное место игры, знающее про формат `.sfa`. Заголовок загрузчика `sfa.h` берётся из репозитория SpriteForge, копии в игре нет; путь задаётся кешируемой переменной CMake: ```powershell cmake -S . -B build -DSPRITEFORGE_DIR=<путь к репозиторию spriteforge> ``` По умолчанию `SPRITEFORGE_DIR` = `../spriteforge` рядом с игрой. Слой ровно один: кадр `.sfa` (индексы палитры + RLE по строкам) превращается в обычный игровой `Sprite` с альфа-тестом. Дальше по коду разницы между процедурным спрайтом и собранным нет: тот же `Blit`, тот же свет, та же сортировка. **Строковых ID ассетов в коде игры нет.** Обращение идёт по константам `SF_ASSET_*` из `generated/spriteforge_assets.h`. Строка живёт ровно в двух местах: в манифесте и в имени файла `.sfa`, по которому загрузчик и собирает свой индекс. ### Холст и точка привязки SpriteForge обрезает кадр по силуэту, поэтому размер кадра плывёт вместе с содержимым: ромб 32x16 приходит как 30x16 с pivot (15,8). Для декора это безразлично, для тайлов — нет: `ShadeQuad` из `Lighting::QuadFor` считается в координатах спрайта и ждёт ромб на своём месте (docs/10-atmosphere.md). Съехавший на пиксель холст увёл бы свет пола относительно света стен. Поэтому `SpriteLibrary::Read` принимает `FramePlacement`: холст нужного размера и точка, в которую на нём ложится pivot кадра. Тайлы просят канонический `TILE_W x TILE_H`, остальное берётся в натуральную величину. ## Тайлы из библиотеки и hot reload По умолчанию весь набор спрайтов процедурный и ни от одного файла не зависит — `--accept` и `--headless` считают то же, что считали раньше. Пол и стены можно подменить собранными: ```powershell build\bin\squad_proto.exe --sf-tiles ``` Осечка на любом шаге (нет библиотеки, нет ассета, битый файл) оставляет процедурный набор целиком: полусобранный хуже прежнего. Бойцы и клинки остаются процедурными сознательно: там цвет занят номером агента и раздаётся из кода, а не из манифеста. ## Анимированные твари ```powershell build\bin\squad_proto.exe --sf-enemies # только твари build\bin\squad_proto.exe --sf # тайлы и твари разом ``` Манифест — `assets/manifests/enemies.yaml`, по одному ассету `_walk` на вид. **Один ассет = один клип.** Бэкенд `import` имён клипов не читает и всегда кладёт единственную анимацию `default`, поэтому `idle` и `attack`, когда до них дойдёт дело, станут отдельными ассетами `_idle`, а не второй анимацией внутри существующего. Правку SpriteForge ради этого заводить не нужно. ### Откуда берутся листы `assets/sources/make_enemy_sheets.py` — детерминированный генератор: ```powershell python assets\sources\make_enemy_sheets.py ``` Он рисует **ту же геометрию**, что процедурный запасной вариант в `src/render/sprites.cpp` (объединение эллипсов, свет по верхне-левой кромке, обводка по силуэту), и лишь сдвигает конечности по фазе шага. Это не удобство, а требование: разъедься силуэты — и `--sf-enemies` менял бы не плавность движения, а саму тварь, которую игрок узнаёт по пятну. Цвета в скрипте — копия `EnemyDef` из `src/sim/target.cpp`, и разойтись им нельзя по той же причине. Ни одного случайного числа: перегенерация даёт побитово тот же PNG. ### Кто и что хранит Кадры лежат в `SpriteLibrary` в одном экземпляре на ассет. `SpriteSet` держит только ID (`enemyWalk[kind]`, 0 — «рисуй процедурный»), а фазу клипа — `View`: - `View::enemyAnim[]` — `Animator` на каждую цель; - `View::enemyPrevPos[]` — позиция прошлого кадра. Фаза шага живёт в `View`, а не в `Target`, ровно по правилу [02-architecture.md](02-architecture.md): попав в симуляцию, она поехала бы в цифрах приёмки. Шаг проигрывается **только когда тварь реально идёт** — скорости у `Target` нет (`vel` — это отдача от удара), поэтому движение определяется сравнением позиций. Иначе цикл крутился бы у стоящего плевка, который по замыслу держит дистанцию, и читался бы как ошибка. `LoadEnemies` — всё или ничего: одна анимированная тварь среди четырёх статичных выглядит хуже пяти статичных. Watch-режим в отдельном терминале: ```powershell sf watch assets\manifests\environment.yaml --output build\sprites ` --cache-dir .sfcache --jobs 8 ``` После каждой успешной пересборки SpriteForge атомарно переписывает `build/sprites/reload.json`. Игра опрашивает его раз в кадр и перечитывает библиотеку целиком. Новая библиотека собирается рядом и подменяет старую только если собралась целиком — недописанная пересборка не гасит картинку. Кровь при перезагрузке сбрасывается по той же причине, по которой она не переживает смену уровня: она запечена в спрайты пола, а пол уже другой (docs/10-atmosphere.md). ## Чего не делать - не открывать `build/sprites/*.sfa`, `.sfcache/`, сгенерированные PNG и contact sheet — для проверки есть текстовые команды; - не редактировать `generated/spriteforge_assets.h` руками: это вывод `sf codegen`; - не заводить строковые ID ассетов в C++; - не править репозиторий SpriteForge ради одной игровой сущности: если не хватает генератора — это отдельная задача в его репозитории.