Newer
Older
rpg / packages / engine / src / map / IsometricTileMap.ts
import { Container, Graphics, Texture, Sprite } from 'pixi.js';
import { Grid } from './pathfinding';
import { IsoLayout, DEFAULT_ISO, isoToScreen } from '../math/iso';

/**
 * Изометрическая тайл-карта.
 * Данные карты — числа (id тайлов); отрисовка — по таблице id -> Texture.
 * Проходимость определяется набором блокирующих id; тайлы «высоких» объектов
 * (стены, деревья) рисуются поверх земли по строкам глубины.
 */
export interface TileMapData {
    width: number;
    height: number;
    /** Индексы тайлов, length = width * height. */
    tiles: number[];
    /** Тайлы с этими id непроходимы. */
    blocked: number[];
    /** Тайлы с этими id рисуются как вертикальные объекты (высота heightPx). */
    tall?: Record<number, number>;
}

export class IsometricTileMap implements Grid {
    readonly data: TileMapData;
    readonly iso: IsoLayout;
    readonly view: Container;

    private blockedSet: Set<number>;

    get width(): number {
        return this.data.width;
    }

    get height(): number {
        return this.data.height;
    }

    constructor(data: TileMapData, textures: Map<number, Texture>, iso: IsoLayout = DEFAULT_ISO) {
        this.data = data;
        this.iso = iso;
        this.blockedSet = new Set(data.blocked);
        this.view = new Container();

        const ground = new Container();
        const objects = new Container();
        this.view.addChild(ground, objects);

        // Рисуем по строкам (ty) внутри — для простоты; сортировка глубины по (tx + ty)
        // для объектов обеспечивается порядком добавления по диагоналям ниже.
        const cells: { tx: number; ty: number; id: number }[] = [];
        for (let ty = 0; ty < data.height; ty++) {
            for (let tx = 0; tx < data.width; tx++) {
                cells.push({ tx, ty, id: data.tiles[ty * data.width + tx] });
            }
        }
        // Глубина = tx + ty (классика изометрии).
        cells.sort((a, b) => a.tx + a.ty - (b.tx + b.ty));

        for (const cell of cells) {
            const tex = textures.get(cell.id);
            const p = isoToScreen(cell.tx, cell.ty, iso);
            if (tex) {
                const s = new Sprite(tex);
                s.anchor.set(0.5, 0);
                s.position.set(p.x, p.y);
                ground.addChild(s);
            } else {
                // Нет текстуры — плейсхолдер-ромб: зелёный (проходимо) или коричневый (блок).
                const g = new Graphics();
                const hw = iso.tileW / 2;
                const hh = iso.tileH / 2;
                g.poly([p.x, p.y, p.x + hw, p.y + hh, p.x, p.y + iso.tileH, p.x - hw, p.y + hh]);
                g.fill(this.blockedSet.has(cell.id) ? 0x4a3b2a : 0x2d5a27);
                ground.addChild(g);
            }

            const tallHeight = data.tall?.[cell.id];
            if (tallHeight !== undefined) {
                // Столб высоты: ромб сверху + тёмные грани, чтобы объект читался объёмным.
                const block = new Graphics();
                block.poly([
                    p.x, p.y - tallHeight,
                    p.x + iso.tileW / 2, p.y + iso.tileH / 2 - tallHeight,
                    p.x, p.y + iso.tileH - tallHeight,
                    p.x - iso.tileW / 2, p.y + iso.tileH / 2 - tallHeight
                ]);
                block.fill(0x6b5a44);
                // Грани вниз на высоту tallHeight.
                block.poly([
                    p.x - iso.tileW / 2, p.y + iso.tileH / 2 - tallHeight,
                    p.x, p.y + iso.tileH - tallHeight,
                    p.x, p.y + iso.tileH,
                    p.x - iso.tileW / 2, p.y + iso.tileH / 2
                ]);
                block.fill(0x4a3b2a);
                block.poly([
                    p.x + iso.tileW / 2, p.y + iso.tileH / 2 - tallHeight,
                    p.x, p.y + iso.tileH - tallHeight,
                    p.x, p.y + iso.tileH,
                    p.x + iso.tileW / 2, p.y + iso.tileH / 2
                ]);
                block.fill(0x352a1e);
                objects.addChild(block);
            }
        }
    }

    isWalkable(x: number, y: number): boolean {
        if (x < 0 || y < 0 || x >= this.data.width || y >= this.data.height) return false;
        const id = this.data.tiles[y * this.data.width + x];
        return !this.blockedSet.has(id);
    }

    /** Размер карты в экранных координатах (для ограничения камеры). */
    get screenSize(): { width: number; height: number } {
        const halfW = (this.iso.tileW / 2) * (this.data.width + this.data.height);
        const halfH = (this.iso.tileH / 2) * (this.data.width + this.data.height);
        return { width: halfW, height: halfH };
    }
}