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

212 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`. Строка живёт ровно в двух
местах: в манифесте и в имени файла `<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` считают то же, что считали раньше. Пол и стены можно
подменить собранными:
```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`, по одному ассету `<kind>_walk` на
вид. **Один ассет = один клип.** Бэкенд `import` имён клипов не читает и всегда
кладёт единственную анимацию `default`, поэтому `idle` и `attack`, когда до них
дойдёт дело, станут отдельными ассетами `<kind>_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 ради одной игровой сущности: если не
хватает генератора — это отдельная задача в его репозитории.