squad-proto/docs/12-assets.md

212 lines
12 KiB
Markdown
Raw Permalink Normal View History

Документация: границы движка, работа через модель, дизайн игры до лейтгейма 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-14 01:07:45 +07:00
# 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 ради одной игровой сущности: если не
хватает генератора — это отдельная задача в его репозитории.