import type { BattleSession, Cell, MapTopologySnapshot } from 'db://assets/mx/module/battle'; import type { CampAreaPreset } from 'db://assets/mx/module/battle'; import type { MapManager } from './legacy/MapManager'; import type { MapGrid } from './legacy/MapGrid'; import type { Node } from 'cc'; import { collectAndValidateMapCampPoints, readCampAreaByCell, type MapAttackPointBinding, type MapCampPointBinding, } from './campAreaMapValidation'; import { resolveSpawnGoalWallName } from './MapPathQueries'; export interface TopologyBuildResult { ok: boolean; snapshot?: MapTopologySnapshot; errors: string[]; } /** * 从 Cocos 地图预制体导入中性拓扑快照(含阵营区域;校验失败不生成 snapshot) * 前提:MapManager.mapRows/mapCols/mapGridSize 与 session.grid 一致(由预制体探测写入 LevelDef) */ export function buildTopologySnapshotFromMapManager( map: MapManager, logicRows: number, logicCols: number, enablePatrolLoop = false, ): TopologyBuildResult { const errors: string[] = []; const mapName = map.node?.name ?? 'MapManager'; if (!map.gridsInitialized) { errors.push(`${mapName}: 地图格子未初始化,无法导入拓扑`); return { ok: false, errors }; } if (map.mapRows !== logicRows || map.mapCols !== logicCols) { console.error(`[buildTopologySnapshotFromMapManager] map/session grid mismatch map=${map.mapRows}x${map.mapCols} session=${logicRows}x${logicCols}`); } const isPerimeter = enablePatrolLoop; const { spawnBindings, attackBindings, errors: pointErrors } = collectAndValidateMapCampPoints(map, { requireAttackPerCampArea: !isPerimeter, }); errors.push(...pointErrors); if (errors.length > 0) { return { ok: false, errors }; } const size = logicRows * logicCols; const walkable: boolean[] = new Array(size).fill(false); const buildable: boolean[] = new Array(size).fill(false); const campAreaByCell = readCampAreaByCell(map, logicRows, logicCols); bindSpawnGoalWalls(spawnBindings, attackBindings, campAreaByCell, logicCols); for (let row = 0; row < logicRows; row++) { for (let col = 0; col < logicCols; col++) { const idx = row * logicCols + col; const mg = map.getMapGrid(row, col); if (!mg) continue; walkable[idx] = mg.isCanWalk ?? false; buildable[idx] = mg.isCanPutText ?? mg.isCanPutGrid ?? false; } } const spawnPorts: Record = {}; const spawnPortCampArea: Record = {}; for (const b of spawnBindings) { spawnPorts[b.port] = { row: b.grid.Row, col: b.grid.Col }; spawnPortCampArea[b.port] = b.campAreaId; } const attackTargets: Cell[] = []; const attackTargetCampArea: number[] = []; const attackTargetWorlds: { x: number; y: number }[] = []; for (const b of attackBindings) { const node = map.allAttackPointGrids.get(b.standGrid); if (!node) { errors.push(`${mapName}: 攻击点 ${b.nodeName} 内部映射丢失`); continue; } attackTargets.push({ row: b.standGrid.Row, col: b.standGrid.Col }); attackTargetCampArea.push(b.campAreaId); attackTargetWorlds.push(attackNodeToLogicalWorld(b.standGrid, node, map.mapGridSize)); } if (errors.length > 0) { return { ok: false, errors }; } return { ok: true, snapshot: { walkable, buildable, campAreaByCell, spawnPorts, spawnPortCampArea, attackTargets, attackTargetCampArea, attackTargetWorlds, enablePatrolLoop, }, errors: [], }; } /** * 出怪口终点按节点名绑定:spawnPoint_1→wallTarget2,spawnPoint_2→wallTarget1。 * 终点站立格并入该出怪口的阵营,寻路才走得到。 * @param spawnBindings 出怪口 * @param attackBindings 攻击点 * @param campAreaByCell 每格阵营 * @param logicCols 列数 */ function bindSpawnGoalWalls( spawnBindings: MapCampPointBinding[], attackBindings: MapAttackPointBinding[], campAreaByCell: number[], logicCols: number, ): void { for (let i = 0; i < spawnBindings.length; i++) { const spawn = spawnBindings[i]; const wallName = resolveSpawnGoalWallName(spawn.port); if (!wallName) continue; let wall: MapAttackPointBinding | null = null; for (let j = 0; j < attackBindings.length; j++) { if (attackBindings[j].nodeName === wallName) wall = attackBindings[j]; } if (!wall) { console.warn('[Topology] 出怪口缺少终点', spawn.port, wallName); continue; } wall.campAreaId = spawn.campAreaId; const idx = wall.standGrid.Row * logicCols + wall.standGrid.Col; campAreaByCell[idx] = spawn.campAreaId; } } /** * 攻击目标节点 Cocos 世界坐标 → 逻辑世界坐标(与 GridMap.cellToWorld 同系) * 以寻路站立格心为锚,X 同向、Y 翻转(Cocos↑ / 逻辑↓) */ function attackNodeToLogicalWorld(standGrid: MapGrid, attackNode: Node, cellSize: number): { x: number; y: number } { const s = cellSize; const logicalCenterX = (standGrid.Col + 0.5) * s; const logicalCenterY = (standGrid.Row + 0.5) * s; const cellWp = standGrid.node.worldPosition; const atkWp = attackNode.worldPosition; return { x: logicalCenterX + (atkWp.x - cellWp.x), y: logicalCenterY - (atkWp.y - cellWp.y), }; } /** * 将拓扑导入战斗会话;校验失败时打印全部错误且不 apply。 * @returns 是否成功导入 */ export function applyMapTopologyToSession( session: BattleSession, map: MapManager, campPreset?: CampAreaPreset, ): boolean { if (!map.gridsInitialized) { console.error('[applyMapTopologyToSession] map not initialized'); return false; } const logicRows = session.grid.rows; const logicCols = session.grid.cols; if (map.mapRows !== logicRows || map.mapCols !== logicCols) { console.error(`[applyMapTopologyToSession] map vs session grid mismatch — deploy/寻敌会异常 map=${map.mapRows}x${map.mapCols} session=${logicRows}x${logicCols}`); return false; } if (map.mapGridSize !== session.grid.cellSize) { console.error(`[applyMapTopologyToSession] cell size mismatch map=${map.mapGridSize} session=${session.grid.cellSize}`); return false; } const result = buildTopologySnapshotFromMapManager( map, logicRows, logicCols, isPerimeterBattle(), ); if (!result.ok || !result.snapshot) { for (const err of result.errors) { console.error(String(err)); } console.error('[applyMapTopologyToSession] 拓扑导入失败,共', result.errors.length, '项错误'); return false; } session.applyTopology(result.snapshot, campPreset); return true; } /** 当前是否为循环圈地图(巡逻环走,无攻击目标) */ function isPerimeterBattle(): boolean { return !!gg.game.CurentBattle?.session?.topology?.enablePatrolLoop; }