import { Container, Graphics, Texture, Sprite } from 'pixi.js';
import { Grid } from './pathfinding';
import { IsoLayout, DEFAULT_ISO, isoToScreen } from '../math/iso';
/**
* Высокий объект на тайле: рисуется спрайтом (якорь в центре ромба) или,
* если текстуры нет, колонной высотой heightPx.
* ground — id тайла, каким рисовать землю под объектом (по умолчанию — сам объект).
*/
export interface TallSpec {
height: number;
ground?: number;
}
/**
* Изометрическая тайл-карта.
* Данные карты — числа (id тайлов); отрисовка — по таблице id -> Texture.
* Проходимость определяется набором блокирующих id; тайлы «высоких» объектов
* (стены, деревья) рисуются поверх земли по строкам глубины.
*/
export interface TileMapData {
width: number;
height: number;
/** Индексы тайлов, length = width * height. */
tiles: number[];
/** Тайлы с этими id непроходимы. */
blocked: number[];
/** Тайлы с этими id рисуются как вертикальные объекты. */
tall?: Record<number, number | TallSpec>;
}
export class IsometricTileMap implements Grid {
readonly data: TileMapData;
readonly iso: IsoLayout;
readonly view: Container;
private blockedSet: Set<number>;
private tallSpecs: Map<number, TallSpec>;
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.tallSpecs = new Map(
Object.entries(data.tall ?? {}).map(([id, spec]) => [
Number(id),
typeof spec === 'number' ? { height: spec } : spec
])
);
this.view = new Container();
const ground = new Container();
const objects = new Container();
this.view.addChild(ground, objects);
// Сортировка глубины по диагонали (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] });
}
}
cells.sort((a, b) => a.tx + a.ty - (b.tx + b.ty));
for (const cell of cells) {
const tall = this.tallSpecs.get(cell.id);
const p = isoToScreen(cell.tx, cell.ty, iso);
// Земля: под высоким объектом рисуем его ground-тайл (или плейсхолдер).
const groundId = tall?.ground ?? cell.id;
const groundTex = textures.get(groundId);
if (groundTex) {
const s = new Sprite(groundTex);
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);
}
// Высокий объект: спрайт с якорем в центре ромба, иначе колонна.
if (tall) {
const objTex = textures.get(cell.id);
if (objTex) {
const s = new Sprite(objTex);
s.anchor.set(0.5, 1);
s.position.set(p.x, p.y + iso.tileH / 2);
objects.addChild(s);
} else {
const block = new Graphics();
const h = tall.height;
const hw = iso.tileW / 2;
const hh = iso.tileH / 2;
block.poly([p.x, p.y - h, p.x + hw, p.y + hh - h, p.x, p.y + iso.tileH - h, p.x - hw, p.y + hh - h]);
block.fill(0x6b5a44);
block.poly([p.x - hw, p.y + hh - h, p.x, p.y + iso.tileH - h, p.x, p.y + iso.tileH, p.x - hw, p.y + hh]);
block.fill(0x4a3b2a);
block.poly([p.x + hw, p.y + hh - h, p.x, p.y + iso.tileH - h, p.x, p.y + iso.tileH, p.x + hw, p.y + hh]);
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 };
}
}