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>
212 lines
12 KiB
Markdown
212 lines
12 KiB
Markdown
# 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 ради одной игровой сущности: если не
|
||
хватает генератора — это отдельная задача в его репозитории.
|