# Арт-пайплайн: генерация пиксель-арта из кода

Арт игры генерируется кодом: сценарий `apps/game/tools/pixelart/gen.mjs` рисует спрайты
на канвасе из движка и кодирует PNG без сторонних зависимостей (`node:zlib` +
свой CRC32). Позже генератор можно заменить на рисованный арт — движку всё равно,
откуда текстуры.

## Файлы

| Файл | Что делает |
|---|---|
| `packages/engine/tools/png.mjs` | PNG-кодек без зависимостей: `encodePng`/`decodePng` (RGBA 8 бит) |
| `packages/engine/tools/canvas.mjs` | Canvas (таблица цветов — параметр): set/get пикселя, `fillDiamond` (изо-ромб), `fromAscii`, seeded rng |
| `packages/engine/tools/imaging.mjs` | Кроп по альфе, nearest-масштаб, квантизация в палитру, обзорный лист; `findFigures`/`resizeRect` — фигуры на однотонном фоне |
| `packages/engine/tools/atlas.mjs` | `buildAtlas` — кадры в строку + Pixi Spritesheet JSON (с `animations`) |
| `packages/engine/tools/frames.mjs` | Производные кадры из опорных: shift/bob/squash/swing/mirror, `deriveCycle` |
| `apps/game/tools/pixelart/palette.mjs` | Палитра игры из 32 цветов (hex + RGBA) — сценарный слой |
| `apps/game/tools/pixelart/gen.mjs` | Генерация всех спрайтов игры → `apps/game/assets/` (канвас с палитрой игры) |

## Запуск

```bash
npm run art     # node apps/game/tools/pixelart/gen.mjs — перегенерировать все PNG
```

Выходные файлы кладутся в `apps/game/assets/` и грузятся в игре по ключам
(`tiles/grass`, `chars/hero_down_1`, ...). Список ключей — в `apps/game/src/Game.ts`.

## Палитра и правила

- 32 цвета, тёмная холодная база; **тёплые цвета — только живое** (герой, NPC,
  колокольчики, огонь). Это правило сеттинга «Пепельных лугов» — см. `docs/art-style.md`.
- Размеры: тайл 32×16 (изометрия 2:1), персонаж ~16×24, высокое дерево 32×48.
  Номиналы — в реестре `apps/game/tools/pixelart/sizes.mjs` (его импортируют
  `gen.mjs` и линтер `aiart/lint-art.mjs` — новые размеры добавляются в одном месте);
  рантайм размер кадра читает из атласного JSON (`AssetLoader.frameSize`).
- Пиксель перо = 1 виртуальный пиксель; никаких сглаженных краёв — контуры ступенчатые.

## ASCII-спрайты

Персонажи задаются ASCII-картами (символ → цвет палитры) — читается и правится
прямо в коде:

```js
const heroDown = fromAscii(`
    ....0000....
    ...011111...
    ...
`, { '0': 'P4', '1': 'F2' });
```

## Процедурные тайлы

Тайлы земли рисуются шумом (seeded rng) + ромбы `fillDiamond` (свет — верхние склоны,
тень — нижние). Дерево: «облачная» крона из перекрывающихся окружностей, ствол,
свисающий кабель — детали сеттинга.

Варианты текстур (микс в игре по хэшу координат) — та же функция с другим seed:
`grassTile(83)`, `ashTile(41)` и т.д. Палитра одна и та же, различие — рисунок шума,
а не цвета. Все варианты кладутся в `apps/game/assets/tiles/` и перечисляются в
`Game.ASSET_KEYS` + `LocationScene.tileTextures()`.

## Атласы

Если спрайтов много (анимации), генератор может собрать атлас: кадры в один PNG +
Spritesheet JSON. Формат Pixi:

```js
const atlas = {
    frames: {
        hero_walk_1: { frame: { x: 0, y: 0, w: 16, h: 24 } },
        hero_walk_2: { frame: { x: 16, y: 0, w: 16, h: 24 } }
    },
    meta: { image: 'hero_sheet.png', size: { w: 32, h: 24 }, scale: 1 }
};
```

Загрузка в игре — `assets.loadAtlas('chars/hero_sheet')`, кадры — `assets.frames(key, 'hero_walk')`.
Генераторы могут заполнить `animations` (имя → список имён кадров, формат Pixi) —
такие анимации игра берёт целиком: `assets.animation('chars/hero_sheet', 'hero_walk_down')`.

Сборка атласа из готовых кадров — `buildAtlas(frames, names, { frameW, frameH, image, animations? })`
из `@rpg/engine/tools/atlas.mjs` → `{ png, json }` (формат выше); `animations` —
`Record<имя, number[]>` (индексы в `names`).

## Производные кадры: frames.mjs

Дешёвые циклы из 1–2 опорных кадров — `@rpg/engine/tools/frames.mjs`
(операции над RGBA `Uint8Array`, всё nearest):

- `shiftFrame(src, w, h, dx, dy)` — сдвиг, пустота — прозрачность;
- `bobFrame(src, w, h, dy = 1)` — «парение» вверх (idle-кадр сгустков/духов);
- `squashFrame(src, w, h)` — «приседание» вниз на 1 px (2-й кадр ходьбы из
  стоячей позы — так AI-листы с одинаковыми позами становятся циклом);
- `swingFrame(src, w, h, angle, pivot?)` — nearest-поворот вокруг опоры
  (низ-центр по умолчанию) — кадры покачивания;
- `mirrorFrame(src, w, h)` — зеркалирование (кадр «вправо» → «влево»);
- `deriveCycle(src, w, h, ops)` — цикл по списку дескрипторов
  `{ dx?, dy?, bob?, squash?, swing?, mirror? }`.

Пример: в `gen.mjs` NPC idle-кадры — `bobFrame` от опорного; вода — два
кадра с бликами, сдвинутыми на 1 px (кадр 2 рисуется со сдвигом штрихов).

## Фигуры на однотонном фоне

Для пост-обработки AI-листов (несколько фигур на белом фоне) — `imaging.mjs`:

- `findFigures(img, { bg = 'corner', dist = 60, maxCount = 4, minArea = 400 })` —
  bbox'ы связных компонент пикселей, отстоящих от фона (`'corner'` — угловой
  пиксель) дальше `dist`; сортировка сверху-вниз/слева-направо.
- `resizeRect(img, rect, w, h, { bg, dist })` — nearest-даунскейл прямоугольника
  до w×h; пиксели ближе `dist` к фону становятся прозрачными (квантизация —
  отдельный шаг `quantize` с палитрой-параметром).

Сценарии применения (манифест персонажей, gen/build/promote, примерка
`tryon.mjs` — подмена целевого атласа сборкой и скриншот в игре до
апрува, ремапер `recolor.mjs`, линтер `lint-art.mjs`) — в игре:
`apps/game/tools/aiart/`
(см. `docs/plan.md`, работа B).

## Замена генератора на рисованный арт

Движку не важно происхождение текстур. Чтобы перейти на рисованный арт:

1. Рисуете PNG того же размера (или атлас + JSON) в `apps/game/assets/`.
2. Ключи ассетов не меняются — код игры не трогается.
3. Палитру соблюдаете, чтобы арт не выбивался из сеттинга (`docs/art-style.md`).

Aseprite/Piskel: экспорт в PNG с теми же именами файлов, что даёт генератор.