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>
12 KiB
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/
лежит отдельно и переживает такую чистку, так что пересборка почти мгновенная.
Установка
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.
Добавить ассет
Порядок ровно такой, шаги не пропускать:
- Поправить манифест в
assets/manifests/. - План сборки, без записи файлов:
sf build assets\manifests\environment.yaml --asset stone_altar --dry-run - Собрать только этот ассет:
sf build assets\manifests\environment.yaml --asset stone_altar ` --output build\sprites --cache-dir .sfcache - Проверить текстом:
sf validate assets\manifests\environment.yaml --build-dir build\sprites sf ascii stone_altar --build-dir build\sprites - Обновить числовые ID:
sf codegen --build-dir build\sprites --output generated\spriteforge_assets.h
Сборка всей библиотеки:
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:
cmake -S . -B build -DSPRITEFORGE_DIR=<путь к репозиторию spriteforge>
По умолчанию SPRITEFORGE_DIR = ../spriteforge рядом с игрой.
Слой ровно один: кадр .sfa (индексы палитры + RLE по строкам) превращается в
обычный игровой Sprite с альфа-тестом. Дальше по коду разницы между
процедурным спрайтом и собранным нет: тот же Blit, тот же свет, та же
сортировка.
Строковых ID ассетов в коде игры нет. Обращение идёт по константам
SF_ASSET_* из generated/spriteforge_assets.h. Строка живёт ровно в двух
местах: в манифесте и в имени файла <asset_id>.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 считают то же, что считали раньше. Пол и стены можно
подменить собранными:
build\bin\squad_proto.exe --sf-tiles
Осечка на любом шаге (нет библиотеки, нет ассета, битый файл) оставляет процедурный набор целиком: полусобранный хуже прежнего.
Бойцы и клинки остаются процедурными сознательно: там цвет занят номером агента и раздаётся из кода, а не из манифеста.
Анимированные твари
build\bin\squad_proto.exe --sf-enemies # только твари
build\bin\squad_proto.exe --sf # тайлы и твари разом
Манифест — assets/manifests/enemies.yaml, по одному ассету <kind>_walk на
вид. Один ассет = один клип. Бэкенд import имён клипов не читает и всегда
кладёт единственную анимацию default, поэтому idle и attack, когда до них
дойдёт дело, станут отдельными ассетами <kind>_idle, а не второй анимацией
внутри существующего. Правку SpriteForge ради этого заводить не нужно.
Откуда берутся листы
assets/sources/make_enemy_sheets.py — детерминированный генератор:
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: попав в симуляцию, она поехала бы в
цифрах приёмки. Шаг проигрывается только когда тварь реально идёт —
скорости у Target нет (vel — это отдача от удара), поэтому движение
определяется сравнением позиций. Иначе цикл крутился бы у стоящего плевка,
который по замыслу держит дистанцию, и читался бы как ошибка.
LoadEnemies — всё или ничего: одна анимированная тварь среди четырёх
статичных выглядит хуже пяти статичных.
Watch-режим в отдельном терминале:
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 ради одной игровой сущности: если не хватает генератора — это отдельная задача в его репозитории.