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.
206 lines
8.2 KiB
206 lines
8.2 KiB
import type { Cell } from './Cell';
|
|
import type { GridMap } from './GridMap';
|
|
import { buildPatrolLoopFromWalkable } from './PatrolLoopBuilder';
|
|
import { createMembershipSet, membershipAdd, membershipToArray, mapValuesToArray } from '../../../tools/MembershipSet';
|
|
|
|
/** 地图拓扑快照(中性数据,由项目层从 prefab 导入) */
|
|
export interface MapTopologySnapshot {
|
|
/** 每格是否可走,length = rows * cols,行优先 */
|
|
walkable: boolean[];
|
|
/** 每格是否可建造(可选,默认 true) */
|
|
buildable?: boolean[];
|
|
/** 每格阵营区域 ID(与 walkable 等长;来自 MapGrid.CampAreaID) */
|
|
campAreaByCell: number[];
|
|
/** 出怪口:port(1-based) → 格子 */
|
|
spawnPorts: Record<number, Cell>;
|
|
/** 出怪口 port → 落点格阵营区域 ID */
|
|
spawnPortCampArea: Record<number, number>;
|
|
/** 攻击目标格(可走格,供寻路) */
|
|
attackTargets: Cell[];
|
|
/** 攻击目标索引 → 站立格阵营区域 ID(与 attackTargets 等长) */
|
|
attackTargetCampArea: number[];
|
|
/**
|
|
* 攻击目标节点世界坐标(逻辑像素,与 GridMap.cellToWorld 同系)
|
|
* 与 attackTargets 等长;缺省时回退为对应格心
|
|
*/
|
|
attackTargetWorlds?: { x: number; y: number }[];
|
|
/**
|
|
* 是否启用外围环走(仅循环圈等模式由项目层置 true)
|
|
* 为 false 时即使无攻击目标也不建 patrolLoop,保持旧逻辑
|
|
*/
|
|
enablePatrolLoop?: boolean;
|
|
}
|
|
|
|
/** 地图拓扑元数据(出怪口、攻击点),与 GridMap 格状态配合使用 */
|
|
export class MapTopology {
|
|
/** 出怪口 port(1-based) → 格子 */
|
|
readonly spawnPorts = new Map<number, Cell>();
|
|
/** 出怪口 port → 阵营区域 ID */
|
|
readonly spawnPortCampArea = new Map<number, number>();
|
|
/** 攻击目标格列表 */
|
|
readonly attackTargets: Cell[] = [];
|
|
/** 攻击目标站立格阵营区域 ID(与 attackTargets 等长) */
|
|
readonly attackTargetCampArea: number[] = [];
|
|
/** 攻击目标节点世界坐标(与 attackTargets 等长) */
|
|
readonly attackTargetWorlds: { x: number; y: number }[] = [];
|
|
/** 每格阵营区域 ID(行优先,length = rows * cols) */
|
|
campAreaByCell: number[] = [];
|
|
/** 外围闭环(顺时针);仅 enablePatrolLoop 且无攻击目标时有值 */
|
|
readonly patrolLoop: Cell[] = [];
|
|
/** 是否启用外围环走(由快照写入,默认 false) */
|
|
enablePatrolLoop = false;
|
|
|
|
private _gridCols = 0;
|
|
|
|
/** 清空拓扑 */
|
|
clear(): void {
|
|
this.spawnPorts.clear();
|
|
this.spawnPortCampArea.clear();
|
|
this.attackTargets.length = 0;
|
|
this.attackTargetCampArea.length = 0;
|
|
this.attackTargetWorlds.length = 0;
|
|
this.campAreaByCell = [];
|
|
this.patrolLoop.length = 0;
|
|
this.enablePatrolLoop = false;
|
|
this._gridCols = 0;
|
|
}
|
|
|
|
/**
|
|
* 将快照写入 GridMap 并更新本对象
|
|
* @param grid 目标格子地图
|
|
* @param snapshot 拓扑快照
|
|
*/
|
|
apply(grid: GridMap, snapshot: MapTopologySnapshot): void {
|
|
const size = grid.rows * grid.cols;
|
|
this._gridCols = grid.cols;
|
|
for (let i = 0; i < size; i++) {
|
|
const cell = grid.cellFromIndex(i);
|
|
grid.setWalkable(cell, snapshot.walkable[i] ?? false);
|
|
grid.setBuildable(cell, snapshot.buildable?.[i] ?? false);
|
|
}
|
|
this.clear();
|
|
this._gridCols = grid.cols;
|
|
this.campAreaByCell = snapshot.campAreaByCell.slice();
|
|
this.enablePatrolLoop = !!snapshot.enablePatrolLoop;
|
|
for (const [port, cell] of Object.entries(snapshot.spawnPorts)) {
|
|
const portNum = Number(port);
|
|
this.spawnPorts.set(portNum, { ...cell });
|
|
const area = snapshot.spawnPortCampArea[portNum];
|
|
if (area !== undefined) {
|
|
this.spawnPortCampArea.set(portNum, area);
|
|
}
|
|
}
|
|
for (let i = 0; i < snapshot.attackTargets.length; i++) {
|
|
const cell = snapshot.attackTargets[i];
|
|
this.attackTargets.push({ ...cell });
|
|
this.attackTargetCampArea.push(snapshot.attackTargetCampArea[i]);
|
|
const w = snapshot.attackTargetWorlds?.[i] ?? grid.cellToWorld(cell);
|
|
this.attackTargetWorlds.push({ x: w.x, y: w.y });
|
|
}
|
|
this.rebuildPatrolLoop(grid);
|
|
}
|
|
|
|
/**
|
|
* 可走性变更后重建巡逻环(仅 enablePatrolLoop 且无攻击目标时)
|
|
* @returns 是否成功;未启用环走或有攻击目标时返回 true
|
|
*/
|
|
rebuildPatrolLoop(grid: GridMap): boolean {
|
|
this.patrolLoop.length = 0;
|
|
if (!this.enablePatrolLoop) return true;
|
|
if (this.attackTargets.length > 0) return true;
|
|
const loop = buildPatrolLoopFromWalkable(grid);
|
|
if (!loop?.length) {
|
|
console.error('[MapTopology] 循环圈模式无法从可走格构建外围闭环');
|
|
return false;
|
|
}
|
|
for (const c of loop) {
|
|
this.patrolLoop.push({ ...c });
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param port 出怪口编号(1-based)
|
|
* @returns 出生格,未配置时 undefined
|
|
*/
|
|
getSpawnCell(port: number): Cell | undefined {
|
|
const c = this.spawnPorts.get(port);
|
|
return c ? { ...c } : undefined;
|
|
}
|
|
|
|
/** @returns 出怪口 port 所属阵营区域;未配置时 undefined */
|
|
getSpawnPortCampArea(port: number): number | undefined {
|
|
return this.spawnPortCampArea.get(port);
|
|
}
|
|
|
|
/** @returns 所有出怪口格子 */
|
|
getAllSpawnCells(): Cell[] {
|
|
return mapValuesToArray(this.spawnPorts).map(c => ({ ...c }));
|
|
}
|
|
|
|
/**
|
|
* @param cell 格子坐标
|
|
* @returns 阵营区域 ID;越界或未导入 campAreaByCell 时 undefined
|
|
*/
|
|
getCampAreaAtCell(cell: Cell): number | undefined {
|
|
if (!this.campAreaByCell.length || this._gridCols <= 0) return undefined;
|
|
const idx = cell.row * this._gridCols + cell.col;
|
|
if (idx < 0 || idx >= this.campAreaByCell.length) return undefined;
|
|
return this.campAreaByCell[idx];
|
|
}
|
|
|
|
/** @returns 地图上出现过的不同阵营区域 ID(升序) */
|
|
getDistinctCampAreaIds(): number[] {
|
|
const set = createMembershipSet<number>();
|
|
for (const id of this.spawnPortCampArea.values()) {
|
|
if (id >= 1) membershipAdd(set, id);
|
|
}
|
|
for (const id of this.attackTargetCampArea) {
|
|
if (id >= 1) membershipAdd(set, id);
|
|
}
|
|
if (set.size === 0) {
|
|
for (const id of this.campAreaByCell) {
|
|
if (id >= 1) membershipAdd(set, id);
|
|
}
|
|
}
|
|
return membershipToArray(set).sort((a, b) => a - b);
|
|
}
|
|
|
|
/** @returns 是否存在多个阵营区域(出怪口/攻击点维度) */
|
|
hasMultipleCampAreas(): boolean {
|
|
return this.getDistinctCampAreaIds().length > 1;
|
|
}
|
|
|
|
/** @returns 指定阵营区域的攻击目标格 */
|
|
getAttackTargetsForCampArea(campAreaId: number): Cell[] {
|
|
const out: Cell[] = [];
|
|
for (let i = 0; i < this.attackTargets.length; i++) {
|
|
if (this.attackTargetCampArea[i] === campAreaId) {
|
|
out.push({ ...this.attackTargets[i] });
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** 攻击目标格 → 对应节点世界坐标(逻辑像素);多目标同格时按 campAreaId 区分 */
|
|
getAttackTargetWorld(cell: Cell, campAreaId?: number): { x: number; y: number } | null {
|
|
for (let i = 0; i < this.attackTargets.length; i++) {
|
|
const t = this.attackTargets[i];
|
|
if (t.row !== cell.row || t.col !== cell.col) continue;
|
|
if (campAreaId !== undefined && this.attackTargetCampArea[i] !== campAreaId) continue;
|
|
return { ...this.attackTargetWorlds[i] };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** 攻击目标索引(与 attackTargets / attackTargetWorlds 同序) */
|
|
findAttackTargetIndex(cell: Cell, campAreaId?: number): number {
|
|
for (let i = 0; i < this.attackTargets.length; i++) {
|
|
const t = this.attackTargets[i];
|
|
if (t.row !== cell.row || t.col !== cell.col) continue;
|
|
if (campAreaId !== undefined && this.attackTargetCampArea[i] !== campAreaId) continue;
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
}
|
|
|