squad-proto/docs/14-monsters.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

110 lines
7.9 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.

# 14. Твари: конвейер ассетов
Blender-first конвейер для пяти видов: `shambler`, `rusher`, `spitter`,
`brute`, `lurker`. Отдельный от агентского и намеренно.
## Зачем отдельный конвейер
У агента слои бумажной куклы обязаны совпасть **пиксель в пиксель**: тело,
броня и оружие рисуются на одном скелете, и любое расхождение видно сразу.
Отсюда в `tools/agent_assets.py` калибровка, растровый `ingest` и проверка
«экипировка не вылезла за силуэт тела».
У твари слоёв нет вовсе, склеивать нечего. Зато пять видов обязаны
**различаться силуэтом** с одного взгляда: игрок принимает решение по пятну,
а не по деталям. Поэтому здесь пять независимых скелетов, никакой общей
калибровки — и проверка ровно обратного смысла: `validate` ругается, если два
вида дали похожее пятно.
Общее у двух конвейеров ровно одно и намеренно: **неподвижная
ортографическая изометрическая камера** (`elevation_deg = atan(0.5)`, тот же
угол, под которым в игре лежит пол) и вырезка кадра постоянным
прямоугольником. Начало координат рига проецируется в центр кадра, поэтому
упаковщик режет все кадры одинаково — тварь не «дышит» при повороте.
## Файлы
| Файл | Что делает |
|---|---|
| `assets/monsters/catalog.yaml` | геометрия ячейки, камера, список видов и клипов — единственный источник чисел |
| `tools/blender/monster_rig.py` | анатомия и анимации; запускается ВНУТРИ Blender |
| `tools/monster_assets.py` | CLI: рендер, упаковка листов, манифест, проверки |
| `tools/build_monster_assets.ps1` | полный прогон: init → render-all → validate → sf validate |
| `assets/sources/monsters/*.png` | собранные листы (вход SpriteForge) |
| `assets/manifests/monsters.generated.yaml` | манифест, пишется `sync` |
| `generated/monster_sprites.h` | константы `MONSTER_ASSET_*` и `MonsterAsset(EnemyKind, AnimKind)` |
## Геометрия
Ячейка `48x56`, pivot `(24,47)`, восемь направлений в том же порядке, что у
агентов: юг, юго-запад, запад, ... Лист **direction-major**: сначала все кадры
юга, затем юго-запада.
`pivot_y = 47`, а не `55`: изометрический пол уходит от точки опоры **вниз** по
экрану, и всё, что у твари позади (хвост бегуна, задние лапы затаившегося),
проецируется ниже точки опоры. Девять строк под pivot — запас на глубину
подставки, а не воздух.
Клипы: `walk` 8, `attack` 6, `hit` 4, `death` 8 кадров на направление.
`fps_scale` в каталоге задаёт повадку: бегун частит, громила волочится при
одинаковом числе кадров.
## Морфологии
Не капсулы. Каждый вид — своя анатомия, а не перекрашенный гуманоид:
* **shambler** — вскрытая наружу грудная клетка, рёбра веером, правая рука
доросла до колена, в череп вбита пластина; хромает;
* **rusher** — четвероногий, горизонтальный хребет, пальцеходящие задние ноги,
косы вместо предплечий, вместо головы раскрытый цветок челюстей;
* **spitter** — волочащийся железистый мешок больше грудной клетки, вживлённый
в горло клапан, сопло вместо лица, атрофированные руки;
* **brute** — к плечу приварено второе, наполовину поглощённое тело; грудь
забрана болтами; правая рука — молот из сросшейся кости;
* **lurker** — плоский зашитый скобами панцирь подвешен под четырьмя длинными
выгнутыми лапами, безглазый клин с крюками.
Анимации описаны в **нормированном** времени (список опорных значений на 0..1)
и сэмплируются под число кадров из каталога: `frames` в каталоге можно менять,
не трогая `monster_rig.py`.
`death` — это **осадка в кучу**, а не падение во весь рост. Лежащая во весь
рост туша не помещается в клетку `48x56` при повороте «от камеры», и в игре это
выглядело бы как обрезанный труп.
## Порядок работы
```
python tools/monster_assets.py monster-init # собрать риг .blend
python tools/monster_assets.py monster-render shambler walk # один вид, один клип
python tools/monster_assets.py monster-render-all # всё
python tools/monster_assets.py validate --strict
python tools/monster_assets.py sync
python -m unittest discover -s tools/tests
```
`monster-render` и `monster-render-all` сами вызывают `sync`.
## Что проверяет validate
* холст листа равен `48 x (8 направлений x кадры)`, пустых ячеек нет;
* силуэт **не упирается в кромку** клетки — иначе в игре это отрубленная лапа;
* тварь стоит на общей линии пола (`death` освобождён: труп имеет право осесть);
* «роза» из восьми направлений центрирована на `pivot_x`;
* зеркальные пары (ЮЗ,ЮВ), (З,В), (СЗ,СВ) совпадают при отражении — ловит
перестановку направлений;
* **пять видов различаются пятном** (IoU силуэтов шага ниже 0.90);
* манифест не разошёлся с каталогом по pivot и размеру кадра, и ни один ID не
объявлен вторым манифестом.
## Запреты
* ID ассета — строго `<kind>_<animation>`: ровно это требует
`tools/asset_coverage.py` от группы `enemy`. Переименуешь — упадёт покрытие.
* Строковых ID в C++ нет: только `MONSTER_ASSET_*` из `generated/monster_sprites.h`.
* PNG и contact sheet не открывать: проверка только текстом — `validate`,
`sf list`, `sf describe`, `sf ascii`.
* `assets/manifests/enemies.yaml` — старые односторонние листы-заглушки. Пока
живы оба файла, ID пересекаются, и `validate` про это предупреждает: снимать
предупреждение — это отдельное решение на стороне рантайма, не конвейера.