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.
195 lines
5.2 KiB
195 lines
5.2 KiB
|
3 weeks ago
|
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);
|
||
|
|
}
|
||
|
|
}
|