# Карты: изометрия, A*, формат карт, Tiled

## Мировые юниты (1 юнит = 1 тайл)

Мир живёт в **мировых юнитах** (1 юнит = 1 тайл, float в плоскости изометрии);
экран — проекция юнитов. Тайловые координаты `{x, y}` (целые) — частный случай:
границы тайлов в юнитах точные (`floor(wx), floor(wy)`). Экранные оси — мировые
диагонали: экранное «вниз» = мировой `(1, 1)`, «вправо» = `(1, −1)`.

```ts
import { worldToScreen, screenToWorld, unitsToPx, pxToUnits } from '@rpg/engine';

worldToScreen(wx, wy);   // проекция точки в px (float, БЕЗ округления — округляет вью)
screenToWorld(px, py);   // обратно: экранные px -> юниты (для кликов)
unitsToPx(units);        // скаляр (дистанция, радиус, скорость): px = units·tileW (32 px/юнит)
pxToUnits(px);           // обратная линейка
```

Для «точных» тайловых задач есть мосты `tileToWorld`/`worldToTile` и экранный
пикинг по ромбу (`isoToScreen`/`screenToIso`/`screenToIsoExact`) — для прямых
кликов по спрайтам без юнитов.

Клик по миру: из виртуальных px указателя вычтите позицию `worldRoot`
(камера), затем `screenToWorld` → юниты → `worldToTile`:

```ts
const w = screenToWorld(
    pointer.x - engine.renderer.worldRoot.position.x,
    pointer.y - engine.renderer.worldRoot.position.y
);
const tile = worldToTile(w.x, w.y, map.width, map.height);
```

## IsometricTileMap

Карта — числа (id тайлов) + таблица id → Texture. Блокирующие id не дают ходить;
`tall` — высокие объекты, рисуются поверх земли по строкам глубины:

```ts
import { IsometricTileMap, DEFAULT_ISO } from '@rpg/engine';

const data: TileMapData = {
    width: 28, height: 28,
    tiles: [...],          // length = width * height
    blocked: [TILE_WATER, TILE_TREE],
    // высота в мировых юнитах (·tileW = px на экране), ground — чем рисовать землю под ним
    tall: { [TILE_TREE]: { height: 1.5, ground: TILE_GRASS } }
};
const map = new IsometricTileMap(data, textures, DEFAULT_ISO);
worldRoot.addChild(map.view);

map.isWalkable(x, y);      // Grid для A*
map.worldBounds;           // { x: 0, y: 0, width, height } — границы камеры в юнитах
map.setTile(x, y, id);     // изменить тайл (сбор предметов, посадка) — данные + перерисовка ячейки
```

## A* (pathfinding)

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

const path = findPath(map, { x: 2, y: 3 }, { x: 10, y: 8 });
// путь без стартового тайла, включая конечный; null — пути нет
```

Диагонали включаются параметром `allowDiagonal = true`, но **без среза углов**:
диагональный шаг разрешён, только если оба ортогональных соседа проходимы.

Для взаимодействия с занятым тайлом (NPC, объект) есть `findPathToNeighbor`:
цель может быть непроходимой — путь проложится к ближайшей проходимой клетке
рядом с ней (8-соседство), а если цель проходима — обычный путь к ней:

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

const path = findPathToNeighbor(map, from, npcTile); // к краю тайла NPC
```

## Формат карт (JSON + RLE)

Карты можно хранить файлами: `encodeMap` упаковывает тайлы в RLE (пары `[id, длина]`),
`parseMap` валидирует и распаковывает:

```ts
import { encodeMap, parseMap } from '@rpg/engine';

const file = encodeMap(data);          // { format: 'rpg-map', version: 1, encoding: 'rle', tiles: [[0, 12], [1, 3], ...] }
fs.writeFileSync('meadows.json', JSON.stringify(file));

const data = parseMap(JSON.parse(raw)); // бросает понятную ошибку на битых данных
```

`encoding: 'raw'` — плоский массив, если RLE не нужен. Файл читается и правится
текстовым редактором.

## Импорт из Tiled

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

const data = fromTiledIso(tiledJson, {
    layers: ['ground', 'props'],   // имена слоёв снизу вверх (по умолчанию все tilelayer)
    blocked: [2, 5],               // id после смещения на firstgid
    tall: { 3: { height: 1.5 } }   // высоты — в мировых юнитах
});
```

Требования к карте в Tiled: ориентация **isometric**, размер тайла 32×16,
tilesets один с известным `firstgid`. GID из Tiled смещаются на `firstgid`,
чтобы id шли с 0; верхние слои заполняют пустые (0) клетки нижних.