squad-proto/docs/12-assets.md
z.kirill 4382af2322 Документация: границы движка, работа через модель, дизайн игры до лейтгейма
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>
2026-08-13 21:07:45 +03:00

12 KiB
Raw Blame History

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.

Добавить ассет

Порядок ровно такой, шаги не пропускать:

  1. Поправить манифест в assets/manifests/.
  2. План сборки, без записи файлов:
    sf build assets\manifests\environment.yaml --asset stone_altar --dry-run
    
  3. Собрать только этот ассет:
    sf build assets\manifests\environment.yaml --asset stone_altar `
      --output build\sprites --cache-dir .sfcache
    
  4. Проверить текстом:
    sf validate assets\manifests\environment.yaml --build-dir build\sprites
    sf ascii stone_altar --build-dir build\sprites
    
  5. Обновить числовые 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 ради одной игровой сущности: если не хватает генератора — это отдельная задача в его репозитории.