Newer
Older
rpg / packages / engine / src / map / tiled.ts
import type { TileMapData } from './IsometricTileMap';

/**
 * Импортёр карт из Tiled (https://www.mapeditor.org).
 * Поддерживается isometric-карта с tilelayer-слоями (CSV или сжатые —
 * Tiled отдаёт их уже распакованными в JSON). GID 0 — пустой тайл;
 * gid смещаются на firstgid первого тайлсета, чтобы id шли с 0.
 */

/** Минимум того, что читаем из JSON Tiled. */
export interface TiledMap {
    width: number;
    height: number;
    tilewidth: number;
    tileheight: number;
    orientation: string;
    layers: {
        type: string;
        name?: string;
        data?: number[];
        visible?: boolean;
    }[];
    tilesets?: { firstgid: number }[];
}

export interface FromTiledOptions {
    /** Имена слоёв в порядке снизу вверх (по умолчанию — все tilelayer в порядке следования). */
    layers?: string[];
    /** id тайлов, которые считаются блокирующими (после смещения). */
    blocked?: number[];
    /** Высокие объекты. */
    tall?: TileMapData['tall'];
}

/**
 * Преобразовать isometric-карту Tiled в TileMapData движка.
 * Верхние слои перезаписывают нули (пустые клетки) нижних.
 */
export function fromTiledIso(tiled: TiledMap, options: FromTiledOptions = {}): TileMapData {
    if (tiled.orientation !== 'isometric') {
        throw new Error(`Tiled: ожидается orientation=isometric, получен ${tiled.orientation}`);
    }
    const { width, height } = tiled;

    const firstGid = tiled.tilesets?.[0]?.firstgid ?? 1;
    const layers = tiled.layers.filter(
        (l) =>
            l.type === 'tilelayer' &&
            l.visible !== false &&
            (options.layers ? options.layers.includes(l.name ?? '') : true)
    );
    if (layers.length === 0) {
        throw new Error('Tiled: в карте нет tilelayer-слоёв');
    }

    const tiles = new Array<number>(width * height).fill(0);
    for (const layer of layers) {
        if (!layer.data || layer.data.length !== width * height) {
            throw new Error(`Tiled: слой ${layer.name ?? '?'} неверного размера`);
        }
        for (let i = 0; i < layer.data.length; i++) {
            const gid = layer.data[i]!;
            if (gid !== 0 && tiles[i] === 0) {
                tiles[i] = gid - firstGid;
            }
        }
    }

    return {
        width,
        height,
        tiles,
        blocked: options.blocked ?? [],
        tall: options.tall
    };
}