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; /** 出怪口 port → 落点格阵营区域 ID */ spawnPortCampArea: Record; /** 攻击目标格(可走格,供寻路) */ 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(); /** 出怪口 port → 阵营区域 ID */ readonly spawnPortCampArea = new Map(); /** 攻击目标格列表 */ 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(); 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; } }