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/bag/MapPlaceValidator.ts

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);
}
}