import { Vec3, type Node } from 'cc'; import { createMembershipSet, mapKeysToArray, membershipAdd, membershipFromArray, membershipDedupeArray, type MembershipSet, type ReadonlyMembershipSet } from '../../../mx/tools/MembershipSet'; import type { BattleSession, Cell } from 'db://assets/mx/module/battle'; import { PARATROOP_TANGSENG_EXCLUSION_RADIUS_CELLS } from '../mechanism/MechanismConstants'; import { resolveAttackStandGridForCamp, resolveWallTarget1StandGrid } from '../view/tangSengMap'; import type { MapGrid } from './legacy/MapGrid'; import type { CocosMapView } from './legacy/MapManager'; /** * Cocos 地图视图桥接(世界坐标 / MapGrid ↔ MX Cell) * 寻路算法统一在 `mx/module/battle/battlefield/Pathfinder` */ function resolveCampAreaForAttack( session: BattleSession, startGrid: MapGrid, spawnPort?: number, ): number | undefined { if (spawnPort !== undefined && spawnPort >= 1) { const fromPort = session.topology.getSpawnPortCampArea(spawnPort); if (fromPort !== undefined) return fromPort; console.error('[MapPathQueries] 出怪口缺少阵营区域映射', spawnPort); return undefined; } const fromCell: Cell = { row: startGrid.Row, col: startGrid.Col }; return session.topology.getCampAreaAtCell(fromCell); } /** 世界坐标 → 最近攻击目标格(按出怪口阵营区域寻路,与 MovementSystem 一致) */ export function resolveAttackTargetGrid( map: CocosMapView, worldPos: Vec3, session?: BattleSession, spawnPort?: number, ): MapGrid | null { const startGrid = map.getMapGridByWorldPos(worldPos); if (!startGrid?.isCanWalk) return null; if (session?.topology.attackTargets.length) { const from = { row: startGrid.Row, col: startGrid.Col }; const campAreaId = resolveCampAreaForAttack(session, startGrid, spawnPort); const target = session.pathfinder.shortestTargetCell(from, campAreaId); if (target) return map.getMapGrid(target.row, target.col); return null; } return null; } /** 世界坐标 → 攻击城墙节点(与拓扑攻击点索引对齐,支持多目标) */ export function resolveAttackTargetNode( map: CocosMapView, worldPos: Vec3, session?: BattleSession, spawnPort?: number, ): Node | null { const startGrid = map.getMapGridByWorldPos(worldPos); if (!startGrid) return null; if (session?.topology.attackTargets.length) { const targetGrid = resolveAttackTargetGrid(map, worldPos, session, spawnPort); if (!targetGrid) return null; const cell: Cell = { row: targetGrid.Row, col: targetGrid.Col }; const campAreaId = resolveCampAreaForAttack(session, startGrid, spawnPort); const idx = session.topology.findAttackTargetIndex(cell, campAreaId); if (idx >= 0) { const node = map.getAttackPointNode(idx); if (node?.isValid) return node; } } return map.allAttackPointGrids.get(startGrid) ?? null; } /** MapGrid → MX Cell */ export function gridToCell(grid: MapGrid): { row: number; col: number } { return { row: grid.Row, col: grid.Col }; } /** 出怪口格 → 本阵营区域内最近攻击目标格的寻路步数(格数);不可达返回 Infinity */ export function getSpawnToTargetGridDistance( session: BattleSession, spawn: Cell, spawnPort?: number, ): number { const pf = session.pathfinder; const campAreaId = spawnPort !== undefined ? session.topology.getSpawnPortCampArea(spawnPort) : session.topology.getCampAreaAtCell(spawn); const target = pf.shortestTargetCell(spawn, campAreaId); if (!target) return Number.POSITIVE_INFINITY; const path = pf.buildPathToTarget(spawn, target, campAreaId); if (path.length === 0) return Number.POSITIVE_INFINITY; return Math.max(0, path.length - 1); } /** 逻辑格是否可达本阵营区域内最近攻击目标(Pathfinder 距离场;仅诊断/工具用) */ export function isCellReachableToAttack(session: BattleSession, cell: Cell): boolean { if (!session.topology.attackTargets.length) return false; const campAreaId = session.topology.getCampAreaAtCell(cell); const target = session.pathfinder.shortestTargetCell(cell, campAreaId); if (!target) return false; const path = session.pathfinder.buildPathToTarget(cell, target, campAreaId); return path.length > 1; } const PARATROOP_BFS_DIRS: readonly { dr: number; dc: number }[] = [ { dr: -1, dc: 0 }, { dr: 1, dc: 0 }, { dr: 0, dc: -1 }, { dr: 0, dc: 1 }, { dr: -1, dc: -1 }, { dr: -1, dc: 1 }, { dr: 1, dc: -1 }, { dr: 1, dc: 1 }, ]; /** 八方向格距(切比雪夫) */ export function getChebyshevGridDistance(a: Cell, b: Cell): number { return Math.max(Math.abs(a.row - b.row), Math.abs(a.col - b.col)); } /** 唐僧攻击站立格(wallTarget1),与寻路 attackTargets 一致 */ export function resolveTangSengStandCell(map: CocosMapView): Cell | null { const grid = resolveWallTarget1StandGrid(map); if (!grid) return null; return { row: grid.Row, col: grid.Col }; } /** 飞禽禁落:唐僧站立格 radiusCells 切比雪夫半径内 */ export function isParatroopForbiddenNearTangSeng( map: CocosMapView, cell: Cell, radiusCells: number = PARATROOP_TANGSENG_EXCLUSION_RADIUS_CELLS, ): boolean { if (radiusCells <= 0) return false; const center = resolveTangSengStandCell(map); if (!center) return false; return getChebyshevGridDistance(center, cell) <= radiusCells; } /** 地图静态可走 + 当前战斗拓扑可走(塔/障碍占用后 session.grid 会关 walkable) */ function isCellCurrentlyWalkable( session: BattleSession, map: CocosMapView, cell: Cell, ): boolean { const grid = map.getMapGrid(cell.row, cell.col); if (!grid?.isCanWalk) return false; if (!session.grid.inBounds(cell)) return false; return session.grid.isWalkable(cell); } /** * 从 from 出发 BFS 找最近「当前仍可走」的 isCanWalk 格(与 MovementSystem 驱逐逻辑一致,不看 Pathfinder)。 * 须穿过放字格等不可走格,否则棋盘内侧的哪吒/空投找不到路边。 */ export function findNearestCurrentlyWalkableCell( map: CocosMapView, session: BattleSession, from: Cell, reject?: (cell: Cell) => boolean, ): MapGrid | null { const acceptable = (cell: Cell): boolean => isCellCurrentlyWalkable(session, map, cell) && !(reject?.(cell) ?? false); if (acceptable(from)) { return map.getMapGrid(from.row, from.col); } const visited = createMembershipSet(); const queue: Cell[] = []; const push = (cell: Cell) => { const key = cell.row * 1000 + cell.col; if (visited.has(key)) return; membershipAdd(visited, key); queue.push(cell); }; push(from); while (queue.length > 0) { const cur = queue.shift()!; for (const { dr, dc } of PARATROOP_BFS_DIRS) { const nb = { row: cur.row + dr, col: cur.col + dc }; if (!session.grid.inBounds(nb)) continue; const grid = map.getMapGrid(nb.row, nb.col); if (!grid) continue; if (acceptable(nb)) return grid; push(nb); } } return null; } /** @deprecated 空投/机制落点请用 findNearestCurrentlyWalkableCell + assignPath */ export function findNearestReachableWalkGrid( map: CocosMapView, session: BattleSession, from: Cell, ): MapGrid | null { return findNearestCurrentlyWalkableCell(map, session, from); } /** 随机选取 count 个可达攻击点的可行走格(MapGrid.isCanWalk,空投/机制落点) */ export function pickRandomReachableWalkGrids( map: CocosMapView, session: BattleSession, count: number, rowRange?: { startRow: number; endRow: number }, ): MapGrid[] { if (count <= 0) return []; let candidates = map.getAllCanWalkMapGrids(); if (rowRange) { candidates = candidates.filter(g => g.Row >= rowRange.startRow && g.Row <= rowRange.endRow); } for (let i = candidates.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [candidates[i], candidates[j]] = [candidates[j], candidates[i]]; } const picked: MapGrid[] = []; for (const grid of candidates) { const cell = { row: grid.Row, col: grid.Col }; if (!isCellCurrentlyWalkable(session, map, cell)) continue; if (!isCellReachableToAttack(session, cell)) continue; picked.push(grid); if (picked.length >= count) break; } return picked; } /** * 空投落点:与普通出怪一致,只要求 MapGrid.isCanWalk 且当前未被塔占(session.grid.isWalkable)。 * 禁落唐僧站立格 PARATROOP_TANGSENG_EXCLUSION_RADIUS_CELLS 切比雪夫半径内。 * 能否走到唐僧交给落地后 MovementSystem.assignPath,不在选格阶段预检 Pathfinder。 */ export function pickParatroopLandingGrids( map: CocosMapView, session: BattleSession, count: number, rowRange?: { startRow: number; endRow: number }, ): MapGrid[] { if (count <= 0) return []; let candidates = map.getAllCanWalkMapGrids(); if (rowRange) { candidates = candidates.filter(g => g.Row >= rowRange.startRow && g.Row <= rowRange.endRow); } for (let i = candidates.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [candidates[i], candidates[j]] = [candidates[j], candidates[i]]; } const picked: MapGrid[] = []; for (const grid of candidates) { const cell = { row: grid.Row, col: grid.Col }; if (!isCellCurrentlyWalkable(session, map, cell)) continue; if (isParatroopForbiddenNearTangSeng(map, cell)) continue; picked.push(grid); if (picked.length >= count) break; } return picked; } /** 是否存在任一出怪口到本阵营攻击目标的寻路距离小于 maxGrids 格 */ export function hasAnySpawnWithinGridDistance(session: BattleSession, maxGrids: number): boolean { if (maxGrids <= 0) return false; for (const [port, spawn] of session.topology.spawnPorts) { if (getSpawnToTargetGridDistance(session, spawn, port) < maxGrids) return true; } return false; } /** Cell 路径 → MapGrid 路径(Cocos 表现层用) */ export function cellsToMapGrids(map: CocosMapView, path: readonly { row: number; col: number }[]): MapGrid[] { const result: MapGrid[] = []; for (const c of path) { const g = map.getMapGrid(c.row, c.col); if (g) result.push(g); } return result; } const SPAWN_POINT_NAME_RE = /^spawnPoint_(\d+)$/; /** 本机视角:敌方怪物出怪口(port2 攻本机) */ export const LOCAL_ENEMY_SPAWN_PORT = 2; /** AI 视角:敌方怪物出怪口(port1 攻 AI) */ export const AI_ENEMY_SPAWN_PORT = 1; /** * 进攻某防守方的敌方出怪口。 * @param defenderPlayerId 0=本机,1=AI */ export function resolveEnemySpawnPortForDefender(defenderPlayerId: number): number { return Math.max(0, Math.floor(Number(defenderPlayerId) || 0)) === 0 ? LOCAL_ENEMY_SPAWN_PORT : AI_ENEMY_SPAWN_PORT; } /** * 进攻某防守方的敌方阵营区域 id。 * @param session 战斗会话 * @param defenderPlayerId 防守方 playerId */ export function resolveEnemyCampAreaIdForDefender(session: BattleSession, defenderPlayerId: number = 0): number | null { const port = resolveEnemySpawnPortForDefender(defenderPlayerId); const id = session.topology.getSpawnPortCampArea(port); if (id === undefined) { console.log('[MapPathQueries] 敌方出怪口缺少阵营区域', port, 'defender=', defenderPlayerId); return null; } return id; } /** 攻击本机的敌方阵营区域 id */ export function resolveLocalEnemyCampAreaId(session: BattleSession): number | null { return resolveEnemyCampAreaIdForDefender(session, 0); } /** * 构建进攻某防守方的怪物行军路线格子索引。 * @param map 地图 * @param session 会话 * @param defenderPlayerId 防守方;默认本机 */ export function buildEnemyMarchPathGridIndices( map: CocosMapView, session: BattleSession, defenderPlayerId: number = 0, ): ReadonlyMembershipSet { const set = createMembershipSet(); const enemyCamp = resolveEnemyCampAreaIdForDefender(session, defenderPlayerId); if (enemyCamp == null) return set; const spawnNodes = map.spawnPointParent?.children ?? []; for (let i = 0; i < spawnNodes.length; i++) { const spawn = spawnNodes[i]; if (!spawn?.isValid) continue; const match = spawn.name.match(SPAWN_POINT_NAME_RE); if (!match) continue; const port = parseInt(match[1], 10); if (session.topology.getSpawnPortCampArea(port) !== enemyCamp) continue; const startGrid = map.getMapGridByWorldPos(spawn.worldPosition); if (!startGrid?.isCanWalk) continue; const start: Cell = { row: startGrid.Row, col: startGrid.Col }; let target: Cell | null = null; try { target = session.pathfinder.shortestTargetCell(start, enemyCamp); } catch { target = null; } if (!target) continue; let path: Cell[] = []; try { path = session.pathfinder.findPath(start, target, true, enemyCamp); } catch { path = []; } for (let j = 0; j < path.length; j++) { const mg = map.getMapGrid(path[j].row, path[j].col); if (mg) membershipAdd(set, mg.Index); } } return set; } /** * 格是否位于进攻某防守方的怪物行军路线上。 * @param map 地图 * @param session 会话 * @param grid 格 * @param marchPathIndices 可选缓存 * @param defenderPlayerId 防守方;默认本机 */ export function isEnemyMonsterWalkGrid( map: CocosMapView, session: BattleSession, grid: MapGrid | null | undefined, marchPathIndices?: ReadonlyMembershipSet | null, defenderPlayerId: number = 0, ): boolean { if (!grid?.isCanWalk) return false; const enemyCamp = resolveEnemyCampAreaIdForDefender(session, defenderPlayerId); if (enemyCamp == null || grid.CampAreaID !== enemyCamp) return false; const indices = marchPathIndices ?? buildEnemyMarchPathGridIndices(map, session, defenderPlayerId); return indices.has(grid.Index); } /** 出怪口 port 对应的可走地图格 */ export function resolveSpawnGridByPort( map: CocosMapView, session: BattleSession, port: number, ): MapGrid | null { const cell = session.topology.getSpawnCell(port); if (cell) { const grid = map.getMapGrid(cell.row, cell.col); if (grid?.isCanWalk) return grid; } const spawnNode = map.spawnPointParent?.getChildByName(`spawnPoint_${port}`); if (!spawnNode) return null; const spawnGrid = map.getMapGridByWorldPos(spawnNode.worldPosition); if (!spawnGrid?.isCanWalk) return null; return spawnGrid; } /** 指定阵营区域的所有出怪口编号(升序) */ export function collectSpawnPortsForCampArea(session: BattleSession, campAreaId: number): number[] { const ports = mapKeysToArray(session.topology.spawnPorts); const out: number[] = []; for (let i = 0; i < ports.length; i++) { const port = ports[i]; if (port <= 0) continue; if (session.topology.getSpawnPortCampArea(port) === campAreaId) { out.push(port); } } out.sort((a, b) => a - b); return out; } /** * 出怪口沿进路向防守方前进 steps 格(1 = 出怪口下一格)。 * @param map 地图 * @param session 会话 * @param port 出怪口编号 * @param defenderPlayerId 被进攻防守方 playerId * @param steps 沿进路前进格数 */ export function resolveGridAheadOfSpawnPort( map: CocosMapView, session: BattleSession, port: number, defenderPlayerId: number, steps: number = 1, ): MapGrid | null { const enemyCamp = resolveEnemyCampAreaIdForDefender(session, defenderPlayerId); if (enemyCamp == null) return null; const spawnGrid = resolveSpawnGridByPort(map, session, port); if (!spawnGrid) return null; const stand = resolveAttackStandGridForCamp(map, session, enemyCamp); if (!stand) return null; const stepCount = Math.max(1, Math.floor(steps) || 1); session.pathfinder.rebuild(session.topology); const from: Cell = { row: spawnGrid.Row, col: spawnGrid.Col }; const to: Cell = { row: stand.Row, col: stand.Col }; let path = session.pathfinder.buildPathToTarget(from, to, enemyCamp); if (path.length < stepCount + 1) { path = session.pathfinder.findPath(from, to, true, enemyCamp); } if (path.length < stepCount + 1) return null; const cell = path[stepCount]; return map.getMapGrid(cell.row, cell.col); } /** * 出怪口沿进路向防守方前进,返回 spawn→目标的前缀路径(含出怪口格 path[0])。 * @param map 地图 * @param session 会话 * @param port 出怪口编号 * @param defenderPlayerId 被进攻防守方 playerId * @param maxSteps 沿进路前进格数(0 仅出怪口格) */ export function resolvePathCellsFromSpawnPort( map: CocosMapView, session: BattleSession, port: number, defenderPlayerId: number, maxSteps: number, ): Cell[] { const enemyCamp = resolveEnemyCampAreaIdForDefender(session, defenderPlayerId); if (enemyCamp == null) return []; const spawnGrid = resolveSpawnGridByPort(map, session, port); if (!spawnGrid) return []; const stand = resolveAttackStandGridForCamp(map, session, enemyCamp); if (!stand) return []; const stepCount = Math.max(0, Math.floor(maxSteps) || 0); session.pathfinder.rebuild(session.topology); const from: Cell = { row: spawnGrid.Row, col: spawnGrid.Col }; const to: Cell = { row: stand.Row, col: stand.Col }; let path = session.pathfinder.buildPathToTarget(from, to, enemyCamp); if (path.length < 2) { path = session.pathfinder.findPath(from, to, true, enemyCamp); } const take = Math.min(path.length, stepCount + 1); return path.slice(0, take); }