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.
156 lines
5.7 KiB
156 lines
5.7 KiB
|
3 weeks ago
|
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;
|
||
|
|
}
|