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

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