# Рендер: Renderer, pixel-perfect, камера, IsoDepthLayer, частицы

## Мир в юнитах, экран в пикселях

Внутренние координаты мира — **мировые юниты** (1 юнит = 1 тайл, float в плоскости
изометрии). Экранные пиксели — это **проекция** юнитов: округление до целого пикселя
происходит только на границе мир→экран (`worldToScreen` + округление во вью/`Camera.apply`),
поэтому пиксель-арт остаётся резким. UI живёт в виртуальных пикселях (480×270) и
никогда не переводится в юниты. Две «линейки»:

- **точка** — анизотропная проекция: 1 юнит = `tileW/2` px по X экрана и `tileH/2` px по Y;
- **скаляр** (дистанция, радиус, скорость, высота) — «линейка проекции»: `px = units·tileW` (32 px/юнит).

Правило отображения: *«внутри вью — всегда px; вью позиционируется в мире через `worldToScreen`»*.

## Renderer и pixel-perfect

Виртуальное разрешение (например 480×270) определяет **экранную** систему координат:
UI и вью-позиции — в виртуальных пикселях, мир — в юнитах. Отрисовка двухслойная:

- CSS-размер канваса — виртуальное разрешение, растянутое **целым** числом
  (`computeScale`), без зазоров и обрезки;
- бэкинг-стор канваса — в **пикселях устройства** (scale × devicePixelRatio):
  UI и текст рендерятся 1:1 с экраном и остаются резкими, в том числе на HiDPI;
- мир при этом остаётся пиксельным: текстуры, загруженные через `AssetLoader`,
  сэмплируются `nearest` (чёткие квадратные пиксели арта).

- `computeScale(vw, vh, winW, winH)` — максимальный целый масштаб, при котором
  виртуальный экран влезает в окно;
- `renderer.resize(newScale)` — смена масштаба; движок сам пересчитывает
  resolution устройства и растеризацию текста (см. `PixelText`);
  Engine делает это автоматически при ресайзе окна (`autoResize: true`);
- канвасу выставлены `image-rendering: pixelated`, `antialias: false`, `roundPixels: true`.

Корневые контейнеры:

```ts
engine.renderer.worldRoot  // мир — сюда применяется камера
engine.renderer.uiRoot     // UI поверх мира, камерой не двигается
```

Оверлей fade-переходов добавляется в `uiRoot` самим Engine — всегда верхний слой.

## Camera

Позиция камеры (`x`, `y`) и `bounds` — в **мировых юнитах** (центр взгляда в плоскости
изометрии). Клэмп через проекцию: экранный bbox границ не обязан лежать в положительных
координатах — у ромба карты западный угол отрицательный, движок считает это сам:

```ts
const camera = engine.camera;
camera.follow(targetX, targetY);         // следить за точкой (юниты)
camera.bounds = map.worldBounds;         // { x: 0, y: 0, width, height } в юнитах
```

Позиция округляется до целого пикселя (`apply`), поэтому арт не дрожит при движении.

«Мёртвая зона» — современное поведение камеры: пока цель внутри окна, камера
неподвижна; у границ окна цель «толкает» камеру (каждая ось независимо). Окно —
**экранное**, остаётся в виртуальных пикселях (мировые прямоугольники проецируются
в экранное окно строго 2:1, поэтому произвольное окно в юнитах невыразимо):

```ts
camera.deadZonePx = { width: 180, height: 120 }; // окно в виртуальных пикселях
camera.deadZonePx = null;                        // снова жёсткое центрирование
camera.snap(x, y);                               // мгновенно, минуя окно (спавн/респаун)
```

Тряска — для ударов, взрывов и урона. Движок сам тикает камеру (`camera.update(dt)`
внутри фиксированного шага), игре достаточно запустить толчок:

```ts
camera.addShake(3, 0.25); // амплитуда 3 px, 0.25 сек
camera.shaking;           // true, пока трясёт
camera.clearShake();      // сбросить
```

Смещение детерминировано (внутри `Shake` — движковый `Rng`) и округляется до целых
пикселей, так что пиксель-арт не размывается.

## IsoDepthLayer

Сортировка глубины для сущностей на изометрической карте: глубина = `tx + ty`
(чем юго-восточнее, тем «ближе»). Герой, NPC, деревья добавляются сюда, а не в слой карты:

```ts
import { IsoDepthLayer } from '@rpg/engine';

const actors = new IsoDepthLayer();       // sortableChildren = true внутри
engine.renderer.worldRoot.addChild(actors);

actors.add(heroView, tileX, tileY);       // добавить и выставить глубину
actors.setDepth(heroView, tileX, tileY);  // обновить при движении (по смене тайла)
```

Слой карты (`IsometricTileMap.view`) и слой сущностей — соседи: карта рисует
высокие объекты со своей внутренней сортировкой, сущности сортируются отдельно.
Для корректного перекрытия герой/дерево должны быть в одном слое с деревьями —
либо используйте `tall`-объекты карты и держите сущности поверх (см. recipes).

## Particles

Эмиттер частиц для атмосферы (пепел, мотыльки, пыль, искры). Частицы — подкрашенные
квадратики 1–4 виртуальных пикселя, детерминированные по seed:

```ts
import { ParticleEmitter } from '@rpg/engine';

const moths = new ParticleEmitter({
    color: 0xf0d878,
    rate: 3,                            // частиц в секунду
    lifetime: [2, 5],                   // сек
    velocity: { x: [-6, 6], y: [-9, -3] },
    acceleration: { y: 1.5 },           // «тяжёлый» полёт мотылька
    size: 2,
    spawnArea: { width: 480, height: 200 },
    seed: 42
});
moths.position.set(240, 135);
engine.renderer.worldRoot.addChild(moths);

// в update сцены:
moths.update(dt);
// при выходе со сцены:
moths.clear(); moths.destroy();
```

Для боевых эффектов (удар, смерть, попадание) есть разовые всплески:

```ts
// burst: мгновенно выпустить частицы из существующего эмиттера
hitFx.burst(12, { color: 0x999988, radialSpeed: [40, 90], lifetime: [0.2, 0.5] });

// oneShot: самостоятельный эмиттер-взрыв, уничтожается сам, когда все частицы умерли
const boom = ParticleEmitter.oneShot(16, {
    color: 0x777766, rate: 0, lifetime: [0.3, 0.6],
    velocity: { x: [0, 0], y: [0, 0] }, // не используется при radialSpeed
    radialSpeed: [50, 120], size: 2, seed: 7
});
boom.position.set(x, y);
worldRoot.addChild(boom);
// обновлять в update сцены: boom.update(dt)
```

Интеграция частицы (`stepParticle`, `sampleSpawn`, `sampleBurst`) вынесена в чистый
модуль `render/particleSim` — её можно тикать и без Pixi (например, в тестах боя).
Частицы — display-layer: все внутренние величины эмиттера — экранные px относительно
его origin; в мире эмиттер позиционируется через `worldToScreen`.

## Порядок отрисовки

Стек сцен рисуется снизу вверх; внутри сцены порядок задают дочерние контейнеры
(`worldRoot`: карта → сущности → эффекты; `uiRoot`: HUD → диалоги → fade-оверлей).
`SceneManager.render()` вызывает `render()` всех сцен в стеке (не только верхней).