You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

194 lines
5.2 KiB

import type { Cell } from './Cell';
import { cellKey } from './Cell';
import type { EntityId } from '../ecs/EntityId';
/** 单格状态 */
export interface GridCellState {
/** 行 */
row: number;
/** 列 */
col: number;
/** 是否可行走 */
walkable: boolean;
/** 是否可建造 */
buildable: boolean;
/** 占用该格的实体 ID */
occupant: EntityId | null;
}
/** 战场格子地图(纯逻辑,不依赖引擎) */
export class GridMap {
/** 行数 */
readonly rows: number;
/** 列数 */
readonly cols: number;
/** 格子尺寸(逻辑像素) */
readonly cellSize: number;
private _cells: GridCellState[][];
/** 可走性版本:仅 walkable 真变化时递增;供 Pathfinder rebuild early-out */
private _walkRevision = 0;
/**
* @param rows 行数
* @param cols 列数
* @param cellSize 格子尺寸
*/
constructor(rows: number, cols: number, cellSize: number) {
this.rows = rows;
this.cols = cols;
this.cellSize = cellSize;
this._cells = [];
for (let r = 0; r < rows; r++) {
const row: GridCellState[] = [];
for (let c = 0; c < cols; c++) {
row.push({ row: r, col: c, walkable: true, buildable: true, occupant: null });
}
this._cells.push(row);
}
}
/**
* @param cell 格子坐标
* @returns 是否在地图范围内
*/
inBounds(cell: Cell): boolean {
return cell.row >= 0 && cell.row < this.rows && cell.col >= 0 && cell.col < this.cols;
}
/**
* @param cell 格子坐标
* @returns 格子状态,越界返回 undefined
*/
getCell(cell: Cell): GridCellState | undefined {
if (!this.inBounds(cell)) return undefined;
return this._cells[cell.row][cell.col];
}
/**
* @param cell 格子坐标
* @returns 是否可走
*/
isWalkable(cell: Cell): boolean {
return this.getCell(cell)?.walkable ?? false;
}
/** 设置格子是否可建造 */
setBuildable(cell: Cell, buildable: boolean): void {
const s = this.getCell(cell);
if (s) s.buildable = buildable;
}
/** 可走性版本号(walkable 真变化时 +1) */
get walkRevision(): number {
return this._walkRevision;
}
/** 设置格子是否可行走 */
setWalkable(cell: Cell, walkable: boolean): void {
const s = this.getCell(cell);
if (!s || s.walkable === walkable) return;
s.walkable = walkable;
this._walkRevision++;
}
/**
* @param cells 占用格子列表
* @param entity 占用实体 ID
*/
occupy(cells: Cell[], entity: EntityId): void {
for (const c of cells) {
const s = this.getCell(c);
if (s) s.occupant = entity;
}
}
/** 清除指定实体的所有占地 */
clearOccupant(entity: EntityId): void {
for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c++) {
if (this._cells[r][c].occupant === entity) {
this._cells[r][c].occupant = null;
}
}
}
}
/**
* @param cells 待检测格子列表
* @returns 所有格均可建造且未被占用
*/
isEmptyForDeploy(cells: Cell[]): boolean {
for (const c of cells) {
const s = this.getCell(c);
if (!s || !s.buildable || s.occupant != null) return false;
}
return true;
}
/**
* 写入格子中心世界坐标,避免热路径分配 {x,y}
* @param cell 格子坐标
* @param out 输出
* @returns out
*/
cellToWorldInto(cell: Cell, out: { x: number; y: number }): { x: number; y: number } {
out.x = (cell.col + 0.5) * this.cellSize;
out.y = (cell.row + 0.5) * this.cellSize;
return out;
}
/**
* @param cell 格子坐标
* @returns 格子中心世界坐标(逻辑坐标,Y 向下为正)
*/
cellToWorld(cell: Cell): { x: number; y: number } {
return this.cellToWorldInto(cell, { x: 0, y: 0 });
}
/**
* @param x 世界 X
* @param y 世界 Y
* @returns 对应格子坐标
*/
worldToCell(x: number, y: number): Cell {
return {
row: Math.floor(y / this.cellSize),
col: Math.floor(x / this.cellSize),
};
}
/**
* @param a 格子 A
* @param b 格子 B
* @returns 两格中心欧氏距离(格)
*/
distanceCells(a: Cell, b: Cell): number {
const dr = a.row - b.row;
const dc = a.col - b.col;
return Math.sqrt(dr * dr + dc * dc);
}
/**
* @param cell 格子坐标
* @returns 一维索引(row * cols + col)
*/
indexOf(cell: Cell): number {
return cell.row * this.cols + cell.col;
}
/**
* @param index 一维索引
* @returns 格子坐标
*/
cellFromIndex(index: number): Cell {
return { row: Math.floor(index / this.cols), col: index % this.cols };
}
/**
* @param cells 格子列表
* @returns 各格 cellKey 字符串数组
*/
keysOf(cells: Cell[]): string[] {
return cells.map(cellKey);
}
}