import type { BattleSession } from 'db://assets/mx/module/battle'; import { isCampAreaScoped } from 'db://assets/mx/module/battle'; import type { IMapPathChecker } from 'db://assets/mx/module/merge'; import { MapCellKind } from 'db://assets/mx/module/merge'; import type { CocosMapView } from '../map/legacy/MapManager'; import type { MapGrid } from '../map/legacy/MapGrid'; import { mapGridToLogicCell } from '../map/BattleMapSpec'; import { Vec3 } from 'cc'; /** CocosMapView 格状态桥接 + MX Pathfinder 可达性(业务规则在项目层) */ export class MapPlaceValidator implements IMapPathChecker { /** 落子归属玩家(0 本机 → CampArea 1;1 敌方/AI → CampArea 2) */ private readonly _ownerPlayerId: number; /** * @param _map 地图 * @param _session 战斗会话(提供 campPolicy) * @param ownerPlayerId 落子归属玩家槽 */ constructor( private readonly _map: CocosMapView, private readonly _session?: BattleSession, ownerPlayerId = 0, ) { this._ownerPlayerId = Math.max(0, Math.floor(Number(ownerPlayerId) || 0)); } get session(): BattleSession | undefined { return this._session; } /** * 所属玩家是否可在该格落子(placeValidation 开启时须本阵营 CampAreaID)。 * 单阵营 / 未开隔离时恒 true。 */ isAllowedCampForPlace(gridIndex: number): boolean { const camp = this._ownerCampAreaId(); if (camp == null) return true; return this.isOwnerCampCell(gridIndex); } /** * 是否所属玩家阵营格(0→1、1→2)。不看 placeValidation。 * @param gridIndex 地图格 */ isOwnerCampCell(gridIndex: number): boolean { const grid = this._map.getMapGridByIndex(gridIndex); if (!grid) return false; return (grid.CampAreaID | 0) === this._ownerPlayerId + 1; } /** * 所属玩家放置区 CampAreaID;无需校验时返回 null。 * 与地图标注一致:playerId 0→1、1→2(不作出怪口反查)。 */ private _ownerCampAreaId(): number | null { const session = this._session; if (!session || !isCampAreaScoped(session.campPolicy, 'placeValidation')) { return null; } return this._ownerPlayerId + 1; } isCanPutGrid(gridIndex: number): boolean { return !!this._map.getMapGridByIndex(gridIndex)?.isCanPutGrid; } isSpiderWebBlocked(gridIndex: number): boolean { const grid = this._map.getMapGridByIndex(gridIndex); if (!grid) return false; if (grid.hasSpiderWeb) return true; const silk = grid.node?.getChildByName('spiderSilk'); return !!silk?.isValid && silk.active; } getLiveCellKind(gridIndex: number): MapCellKind | null { const grid = this._map.getMapGridByIndex(gridIndex); if (!grid) return null; if (grid.isCanWalk) return MapCellKind.Walkable; // 编辑误配 dig+putText 双标时:可放置优先(不当 Dig) if (grid.isCanPutText) return MapCellKind.Put; if (grid.isAreaUnlock) return MapCellKind.Unlock; if (grid.isCanBeDig) return MapCellKind.Dig; return MapCellKind.Put; } canPlaceType3Grid(gridIndex: number): boolean { const grid = this._map.getMapGridByIndex(gridIndex); if (!grid?.isCanPutGrid) return false; if (!this._session) { console.error('[MapPlaceValidator] canPlaceType3Grid: session missing'); return false; } const cell = this._gridToLogic(grid); if (!cell) return false; if (this._session.topology.enablePatrolLoop) { const loop = this._session.topology.patrolLoop; if (!loop.length) { console.error('[MapPlaceValidator] canPlaceType3Grid: patrolLoop empty'); return false; } for (let i = 0; i < loop.length; i++) { if (loop[i].row === cell.row && loop[i].col === cell.col) { return false; } } return true; } if (!this._session.topology.attackTargets.length) { console.error('[MapPlaceValidator] canPlaceType3Grid: attackTargets empty'); return false; } const spawns = this._session.topology.getAllSpawnCells(); if (!spawns.length) { console.error('[MapPlaceValidator] canPlaceType3Grid: spawnPorts empty'); return false; } try { return this._session.pathfinder.checkMultiSourceReachability( spawns, this._session.topology.attackTargets, cell, false, ); } catch (e) { console.error('[MapPlaceValidator] canPlaceType3Grid pathfinder error', String(e)); return false; } } /** * 将逻辑层格语义写回 MapGrid,并同步 MX GridMap。 * @returns MX 可走性是否相对同步前发生变化 */ applyCellKind(grid: MapGrid, kind: MapCellKind): boolean { switch (kind) { case MapCellKind.Walkable: grid.isCanWalk = true; grid.isCanPutText = false; grid.isCanBeDig = false; break; case MapCellKind.Dig: grid.isCanWalk = false; grid.isCanPutText = false; grid.isCanBeDig = true; break; case MapCellKind.Unlock: grid.isCanWalk = false; grid.isCanPutText = false; grid.isCanBeDig = true; break; case MapCellKind.Put: // Dig→Put / 可放置格:一律不可走(路上铲开的空 Put 也必须挡路) grid.isCanWalk = false; grid.isCanPutText = true; grid.isCanBeDig = false; break; } grid.updateGrid(); return this._syncCellToSession(grid); } /** * 同步单格到 MX:以 MapGrid 为准对齐 walkable(纠正 Dig 路障与 MX 默认 true 的分叉)。 * @returns MX 可走性是否变化 */ private _syncCellToSession(grid: MapGrid): boolean { if (!this._session) return false; const cell = this._gridToLogic(grid); if (!cell) return false; const walkable = !!grid.isCanWalk; const walkChanged = this._session.grid.isWalkable(cell) !== walkable; if (walkChanged) { this._session.grid.setWalkable(cell, walkable); } this._session.grid.setBuildable(cell, grid.isCanPutText || grid.isCanPutGrid); return walkChanged; } snapshotCell(grid: MapGrid): { kind: MapCellKind; builtinPut: boolean; canPutGrid: boolean } { let kind: MapCellKind; if (grid.isCanWalk) { kind = MapCellKind.Walkable; } else if (grid.isCanPutText) { // 可放置优先:dig+putText 双标按 Put 入逻辑层,避免 AI/玩家无法落子 kind = MapCellKind.Put; } else if (grid.isAreaUnlock) { kind = MapCellKind.Unlock; } else if (grid.isCanBeDig) { kind = MapCellKind.Dig; } else { kind = MapCellKind.Put; } const builtinPut = kind === MapCellKind.Put; return { kind, builtinPut, canPutGrid: grid.isCanPutGrid }; } /** * 将 MapGrid 可走性同步到 MX GridMap。 * 可走性只跟格语义(Walkable/Put/Dig)有关;放字块不挡路。 * @returns 是否有格子的可走性发生了变化 */ resyncWalkabilityFromMap(_isExtraBlocked?: (gridIndex: number) => boolean): boolean { if (!this._session) return false; let changed = false; const logicRows = this._session.grid.rows; const logicCols = this._session.grid.cols; for (let row = 0; row < logicRows; row++) { for (let col = 0; col < logicCols; col++) { const grid = this._map.getMapGrid(row, col); if (!grid) continue; const cell = { row, col }; const walkable = !!grid.isCanWalk; if (this._session.grid.isWalkable(cell) !== walkable) { changed = true; } this._session.grid.setWalkable(cell, walkable); this._session.grid.setBuildable(cell, grid.isCanPutText || grid.isCanPutGrid); } } return changed; } /** * 同步可走性并重建寻路、刷新怪物路径。 * @param forceRefresh 为 true 时即使可走性未变也刷新(例如雷劈改图) */ notifyWalkStateChanged(forceRefresh = true): void { this._map.notifyMapWalkStateChanged(); if (!this._session) return; if (forceRefresh) { this._session.notifyWalkabilityChanged(); } } getGridByIndex(index: number): MapGrid | null { return this._map.getMapGridByIndex(index); } getGridByWorldPos(pos: Vec3): MapGrid | null { return this._map.getMapGridByWorldPos(pos); } private _gridToLogic(grid: MapGrid): { row: number; col: number } | null { if (!this._session) return null; return mapGridToLogicCell(grid.Row, grid.Col, this._session.grid.rows, this._session.grid.cols); } }