# 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 ассета — строго `_`: ровно это требует `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` про это предупреждает: снимать предупреждение — это отдельное решение на стороне рантайма, не конвейера.