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.
244 lines
9.0 KiB
244 lines
9.0 KiB
|
3 weeks ago
|
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.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.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.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);
|
||
|
|
}
|
||
|
|
}
|