import type { Node } from 'cc'; import { createMembershipSet, membershipAdd, membershipFromArray, membershipDedupeArray, type MembershipSet, type ReadonlyMembershipSet } from '../../../mx/tools/MembershipSet'; import type { MapGrid } from './legacy/MapGrid'; import type { CocosMapView } from './legacy/MapManager'; const SPAWN_POINT_NAME_RE = /^spawnPoint_(\d+)$/; /** 阵营区域 ID 合法下限(MapGrid 默认 1) */ export const MIN_CAMP_AREA_ID = 1; export function isValidCampAreaId(campAreaId: number): boolean { return Number.isFinite(campAreaId) && campAreaId >= MIN_CAMP_AREA_ID; } export interface MapCampPointBinding { port: number; nodeName: string; grid: MapGrid; campAreaId: number; } export interface MapAttackPointBinding { nodeName: string; standGrid: MapGrid; campAreaId: number; } /** * 从 MapManager 已扫描的出怪口/攻击点构建绑定并校验(无兜底)。 * @returns errors 非空表示地图配置非法,不得导入拓扑 */ export function collectAndValidateMapCampPoints( map: CocosMapView, options?: { requireAttackPerCampArea?: boolean }, ): { spawnBindings: MapCampPointBinding[]; attackBindings: MapAttackPointBinding[]; errors: string[]; } { const mapName = map.node?.name ?? 'MapManager'; const errors: string[] = []; const spawnBindings: MapCampPointBinding[] = []; const attackBindings: MapAttackPointBinding[] = []; const seenPorts = createMembershipSet(); const spawnCells = createMembershipSet(); for (const [grid, node] of map.allSpawnPointGrids) { const match = node.name.match(SPAWN_POINT_NAME_RE); if (!match) { errors.push(`${mapName}: 出怪口节点名须为 spawnPoint_N,实际为「${node.name}」`); continue; } const port = parseInt(match[1], 10); if (seenPorts.has(port)) { errors.push(`${mapName}: 出怪口 port ${port} 重复(节点 ${node.name})`); continue; } membershipAdd(seenPorts, port); if (!grid?.isCanWalk) { errors.push(`${mapName}: 出怪口 ${node.name} 不在可走格上`); continue; } if (spawnCells.has(grid.Index)) { errors.push(`${mapName}: 多个出怪口落在同一格 (${grid.Row},${grid.Col})`); continue; } membershipAdd(spawnCells, grid.Index); const campAreaId = grid.CampAreaID; if (!isValidCampAreaId(campAreaId)) { errors.push( `${mapName}: 出怪口 ${node.name} 落点格 CampAreaID 无效 (${campAreaId}),须 >= ${MIN_CAMP_AREA_ID}`, ); continue; } spawnBindings.push({ port, nodeName: node.name, grid, campAreaId }); } for (const [standGrid, node] of map.allAttackPointGrids) { if (!standGrid) { errors.push(`${mapName}: 攻击点 ${node.name} 无法映射到可走站立格`); continue; } const campAreaId = standGrid.CampAreaID; if (!isValidCampAreaId(campAreaId)) { errors.push( `${mapName}: 攻击点 ${node.name} 站立格 CampAreaID 无效 (${campAreaId}),须 >= ${MIN_CAMP_AREA_ID}`, ); continue; } attackBindings.push({ nodeName: node.name, standGrid, campAreaId }); } const campAreasWithSpawn = membershipFromArray(spawnBindings.map((b) => b.campAreaId)); const campAreasWithAttack = membershipFromArray(attackBindings.map((b) => b.campAreaId)); const requireAttackPerCampArea = options?.requireAttackPerCampArea !== false; if (requireAttackPerCampArea) { for (const areaId of campAreasWithSpawn.keys()) { if (!campAreasWithAttack.has(areaId)) { errors.push(`${mapName}: 阵营区域 ${areaId} 有出怪口但缺少攻击点`); } } } const boundSpawnNodes = createMembershipSet(); for (const [, node] of map.allSpawnPointGrids) { membershipAdd(boundSpawnNodes, node); } for (const n of map.spawnPointParent?.children ?? []) { if (!boundSpawnNodes.has(n)) { errors.push(`${mapName}: 出怪口 ${n.name} 未落在任何地图格上`); } } const boundAttackNodes = createMembershipSet(); for (const [, node] of map.allAttackPointGrids) { membershipAdd(boundAttackNodes, node); } for (const n of map.mapTargetParent?.children ?? []) { if (!boundAttackNodes.has(n)) { errors.push(`${mapName}: 攻击点 ${n.name} 无法映射到可走站立格`); } } if (!map.spawnPointParent?.children?.length) { errors.push(`${mapName}: 缺少「出怪口」子节点`); } else if (spawnBindings.length === 0) { errors.push(`${mapName}: 出怪口均未通过校验,无有效 spawnPoint_N`); } if (requireAttackPerCampArea) { if (!map.mapTargetParent?.children?.length || map.allAttackPointGrids.size === 0) { errors.push(`${mapName}: 缺少有效攻击点(非循环圈模式必须配置「目标点」)`); } } spawnBindings.sort((a, b) => a.port - b.port); return { spawnBindings, attackBindings, errors }; } /** 读取全图 campAreaByCell(与 walkable 同索引) */ export function readCampAreaByCell(map: CocosMapView, logicRows: number, logicCols: number): number[] { const size = logicRows * logicCols; const campAreaByCell = new Array(size); 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); campAreaByCell[idx] = mg?.CampAreaID ?? 0; } } return campAreaByCell; }