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.
 
 
 
 
AdLike2048/assets/script/battle/map/campAreaMapValidation.ts

155 lines
5.7 KiB

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<number>();
const spawnCells = createMembershipSet<number>();
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<Node>();
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<Node>();
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<number>(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;
}