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.
166 lines
5.8 KiB
166 lines
5.8 KiB
|
3 weeks ago
|
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,
|
||
|
|
} from './campAreaMapValidation';
|
||
|
|
|
||
|
|
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);
|
||
|
|
|
||
|
|
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<number, Cell> = {};
|
||
|
|
const spawnPortCampArea: Record<number, number> = {};
|
||
|
|
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: [],
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 攻击目标节点 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;
|
||
|
|
}
|