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