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 };
}
}