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/map/MapPathQueries.ts

504 lines
19 KiB

import { Vec3, type Node } from 'cc';
import { createMembershipSet, mapKeysToArray, membershipAdd, membershipFromArray, membershipDedupeArray, type MembershipSet, type ReadonlyMembershipSet } from '../../../mx/tools/MembershipSet';
import type { BattleSession, Cell } from 'db://assets/mx/module/battle';
import { PARATROOP_TANGSENG_EXCLUSION_RADIUS_CELLS } from '../mechanism/MechanismConstants';
import { resolveAttackStandGridForCamp, resolveWallTarget1StandGrid } from '../view/tangSengMap';
import type { MapGrid } from './legacy/MapGrid';
import type { CocosMapView } from './legacy/MapManager';
/**
* Cocos 地图视图桥接(世界坐标 / MapGrid ↔ MX Cell)
* 寻路算法统一在 `mx/module/battle/battlefield/Pathfinder`
*/
function resolveCampAreaForAttack(
session: BattleSession,
startGrid: MapGrid,
spawnPort?: number,
): number | undefined {
if (spawnPort !== undefined && spawnPort >= 1) {
const fromPort = session.topology.getSpawnPortCampArea(spawnPort);
if (fromPort !== undefined) return fromPort;
console.error('[MapPathQueries] 出怪口缺少阵营区域映射', spawnPort);
return undefined;
}
const fromCell: Cell = { row: startGrid.Row, col: startGrid.Col };
return session.topology.getCampAreaAtCell(fromCell);
}
/** 世界坐标 → 最近攻击目标格(按出怪口阵营区域寻路,与 MovementSystem 一致) */
export function resolveAttackTargetGrid(
map: CocosMapView,
worldPos: Vec3,
session?: BattleSession,
spawnPort?: number,
): MapGrid | null {
const startGrid = map.getMapGridByWorldPos(worldPos);
if (!startGrid?.isCanWalk) return null;
if (session?.topology.attackTargets.length) {
const from = { row: startGrid.Row, col: startGrid.Col };
const campAreaId = resolveCampAreaForAttack(session, startGrid, spawnPort);
const target = session.pathfinder.shortestTargetCell(from, campAreaId);
if (target) return map.getMapGrid(target.row, target.col);
return null;
}
return null;
}
/** 世界坐标 → 攻击城墙节点(与拓扑攻击点索引对齐,支持多目标) */
export function resolveAttackTargetNode(
map: CocosMapView,
worldPos: Vec3,
session?: BattleSession,
spawnPort?: number,
): Node | null {
const startGrid = map.getMapGridByWorldPos(worldPos);
if (!startGrid) return null;
if (session?.topology.attackTargets.length) {
const targetGrid = resolveAttackTargetGrid(map, worldPos, session, spawnPort);
if (!targetGrid) return null;
const cell: Cell = { row: targetGrid.Row, col: targetGrid.Col };
const campAreaId = resolveCampAreaForAttack(session, startGrid, spawnPort);
const idx = session.topology.findAttackTargetIndex(cell, campAreaId);
if (idx >= 0) {
const node = map.getAttackPointNode(idx);
if (node?.isValid) return node;
}
}
return map.allAttackPointGrids.get(startGrid) ?? null;
}
/** MapGrid → MX Cell */
export function gridToCell(grid: MapGrid): { row: number; col: number } {
return { row: grid.Row, col: grid.Col };
}
/** 出怪口格 → 本阵营区域内最近攻击目标格的寻路步数(格数);不可达返回 Infinity */
export function getSpawnToTargetGridDistance(
session: BattleSession,
spawn: Cell,
spawnPort?: number,
): number {
const pf = session.pathfinder;
const campAreaId = spawnPort !== undefined
? session.topology.getSpawnPortCampArea(spawnPort)
: session.topology.getCampAreaAtCell(spawn);
const target = pf.shortestTargetCell(spawn, campAreaId);
if (!target) return Number.POSITIVE_INFINITY;
const path = pf.buildPathToTarget(spawn, target, campAreaId);
if (path.length === 0) return Number.POSITIVE_INFINITY;
return Math.max(0, path.length - 1);
}
/** 逻辑格是否可达本阵营区域内最近攻击目标(Pathfinder 距离场;仅诊断/工具用) */
export function isCellReachableToAttack(session: BattleSession, cell: Cell): boolean {
if (!session.topology.attackTargets.length) return false;
const campAreaId = session.topology.getCampAreaAtCell(cell);
const target = session.pathfinder.shortestTargetCell(cell, campAreaId);
if (!target) return false;
const path = session.pathfinder.buildPathToTarget(cell, target, campAreaId);
return path.length > 1;
}
const PARATROOP_BFS_DIRS: readonly { dr: number; dc: number }[] = [
{ dr: -1, dc: 0 }, { dr: 1, dc: 0 }, { dr: 0, dc: -1 }, { dr: 0, dc: 1 },
{ dr: -1, dc: -1 }, { dr: -1, dc: 1 }, { dr: 1, dc: -1 }, { dr: 1, dc: 1 },
];
/** 八方向格距(切比雪夫) */
export function getChebyshevGridDistance(a: Cell, b: Cell): number {
return Math.max(Math.abs(a.row - b.row), Math.abs(a.col - b.col));
}
/** 唐僧攻击站立格(wallTarget1),与寻路 attackTargets 一致 */
export function resolveTangSengStandCell(map: CocosMapView): Cell | null {
const grid = resolveWallTarget1StandGrid(map);
if (!grid) return null;
return { row: grid.Row, col: grid.Col };
}
/** 飞禽禁落:唐僧站立格 radiusCells 切比雪夫半径内 */
export function isParatroopForbiddenNearTangSeng(
map: CocosMapView,
cell: Cell,
radiusCells: number = PARATROOP_TANGSENG_EXCLUSION_RADIUS_CELLS,
): boolean {
if (radiusCells <= 0) return false;
const center = resolveTangSengStandCell(map);
if (!center) return false;
return getChebyshevGridDistance(center, cell) <= radiusCells;
}
/** 地图静态可走 + 当前战斗拓扑可走(塔/障碍占用后 session.grid 会关 walkable) */
function isCellCurrentlyWalkable(
session: BattleSession,
map: CocosMapView,
cell: Cell,
): boolean {
const grid = map.getMapGrid(cell.row, cell.col);
if (!grid?.isCanWalk) return false;
if (!session.grid.inBounds(cell)) return false;
return session.grid.isWalkable(cell);
}
/**
* 从 from 出发 BFS 找最近「当前仍可走」的 isCanWalk 格(与 MovementSystem 驱逐逻辑一致,不看 Pathfinder)。
* 须穿过放字格等不可走格,否则棋盘内侧的哪吒/空投找不到路边。
*/
export function findNearestCurrentlyWalkableCell(
map: CocosMapView,
session: BattleSession,
from: Cell,
reject?: (cell: Cell) => boolean,
): MapGrid | null {
const acceptable = (cell: Cell): boolean =>
isCellCurrentlyWalkable(session, map, cell) && !(reject?.(cell) ?? false);
if (acceptable(from)) {
return map.getMapGrid(from.row, from.col);
}
const visited = createMembershipSet<number>();
const queue: Cell[] = [];
const push = (cell: Cell) => {
const key = cell.row * 1000 + cell.col;
if (visited.has(key)) return;
membershipAdd(visited, key);
queue.push(cell);
};
push(from);
while (queue.length > 0) {
const cur = queue.shift()!;
for (const { dr, dc } of PARATROOP_BFS_DIRS) {
const nb = { row: cur.row + dr, col: cur.col + dc };
if (!session.grid.inBounds(nb)) continue;
const grid = map.getMapGrid(nb.row, nb.col);
if (!grid) continue;
if (acceptable(nb)) return grid;
push(nb);
}
}
return null;
}
/** @deprecated 空投/机制落点请用 findNearestCurrentlyWalkableCell + assignPath */
export function findNearestReachableWalkGrid(
map: CocosMapView,
session: BattleSession,
from: Cell,
): MapGrid | null {
return findNearestCurrentlyWalkableCell(map, session, from);
}
/** 随机选取 count 个可达攻击点的可行走格(MapGrid.isCanWalk,空投/机制落点) */
export function pickRandomReachableWalkGrids(
map: CocosMapView,
session: BattleSession,
count: number,
rowRange?: { startRow: number; endRow: number },
): MapGrid[] {
if (count <= 0) return [];
let candidates = map.getAllCanWalkMapGrids();
if (rowRange) {
candidates = candidates.filter(g => g.Row >= rowRange.startRow && g.Row <= rowRange.endRow);
}
for (let i = candidates.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[candidates[i], candidates[j]] = [candidates[j], candidates[i]];
}
const picked: MapGrid[] = [];
for (const grid of candidates) {
const cell = { row: grid.Row, col: grid.Col };
if (!isCellCurrentlyWalkable(session, map, cell)) continue;
if (!isCellReachableToAttack(session, cell)) continue;
picked.push(grid);
if (picked.length >= count) break;
}
return picked;
}
/**
* 空投落点:与普通出怪一致,只要求 MapGrid.isCanWalk 且当前未被塔占(session.grid.isWalkable)。
* 禁落唐僧站立格 PARATROOP_TANGSENG_EXCLUSION_RADIUS_CELLS 切比雪夫半径内。
* 能否走到唐僧交给落地后 MovementSystem.assignPath,不在选格阶段预检 Pathfinder。
*/
export function pickParatroopLandingGrids(
map: CocosMapView,
session: BattleSession,
count: number,
rowRange?: { startRow: number; endRow: number },
): MapGrid[] {
if (count <= 0) return [];
let candidates = map.getAllCanWalkMapGrids();
if (rowRange) {
candidates = candidates.filter(g => g.Row >= rowRange.startRow && g.Row <= rowRange.endRow);
}
for (let i = candidates.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[candidates[i], candidates[j]] = [candidates[j], candidates[i]];
}
const picked: MapGrid[] = [];
for (const grid of candidates) {
const cell = { row: grid.Row, col: grid.Col };
if (!isCellCurrentlyWalkable(session, map, cell)) continue;
if (isParatroopForbiddenNearTangSeng(map, cell)) continue;
picked.push(grid);
if (picked.length >= count) break;
}
return picked;
}
/** 是否存在任一出怪口到本阵营攻击目标的寻路距离小于 maxGrids 格 */
export function hasAnySpawnWithinGridDistance(session: BattleSession, maxGrids: number): boolean {
if (maxGrids <= 0) return false;
for (const [port, spawn] of session.topology.spawnPorts) {
if (getSpawnToTargetGridDistance(session, spawn, port) < maxGrids) return true;
}
return false;
}
/** Cell 路径 → MapGrid 路径(Cocos 表现层用) */
export function cellsToMapGrids(map: CocosMapView, path: readonly { row: number; col: number }[]): MapGrid[] {
const result: MapGrid[] = [];
for (const c of path) {
const g = map.getMapGrid(c.row, c.col);
if (g) result.push(g);
}
return result;
}
const SPAWN_POINT_NAME_RE = /^spawnPoint_(\d+)$/;
/** 本机怪物出怪口(spawnPoint_2,终点 wallTarget1) */
export const LOCAL_OWN_SPAWN_PORT = 2;
/** 本机视角:敌方怪物出怪口(spawnPoint_1,终点 wallTarget2) */
export const LOCAL_ENEMY_SPAWN_PORT = 1;
/** 进攻 AI 的出怪口(本机怪物从 spawnPoint_2 出发) */
export const AI_ENEMY_SPAWN_PORT = 2;
/**
* 某玩家放出怪物的出怪口。本机走 spawnPoint_2,AI 走 spawnPoint_1。
* @param ownerPlayerId 放怪的玩家
*/
export function resolveOwnSpawnPort(ownerPlayerId: number): number {
return Math.floor(Number(ownerPlayerId) || 0) === 0 ? LOCAL_OWN_SPAWN_PORT : LOCAL_ENEMY_SPAWN_PORT;
}
/**
* 出怪口对应的终点节点。1→wallTarget2,2→wallTarget1。
* @param port 出怪口编号
*/
export function resolveSpawnGoalWallName(port: number): string {
if (port === LOCAL_ENEMY_SPAWN_PORT) return 'wallTarget2';
if (port === LOCAL_OWN_SPAWN_PORT) return 'wallTarget1';
return '';
}
/**
* 进攻某防守方的敌方出怪口。
* @param defenderPlayerId 0=本机,1=AI
*/
export function resolveEnemySpawnPortForDefender(defenderPlayerId: number): number {
return Math.max(0, Math.floor(Number(defenderPlayerId) || 0)) === 0
? LOCAL_ENEMY_SPAWN_PORT
: AI_ENEMY_SPAWN_PORT;
}
/**
* 进攻某防守方的敌方阵营区域 id。
* @param session 战斗会话
* @param defenderPlayerId 防守方 playerId
*/
export function resolveEnemyCampAreaIdForDefender(session: BattleSession, defenderPlayerId: number = 0): number | null {
const port = resolveEnemySpawnPortForDefender(defenderPlayerId);
const id = session.topology.getSpawnPortCampArea(port);
if (id === undefined) {
console.log('[MapPathQueries] 敌方出怪口缺少阵营区域', port, 'defender=', defenderPlayerId);
return null;
}
return id;
}
/** 攻击本机的敌方阵营区域 id */
export function resolveLocalEnemyCampAreaId(session: BattleSession): number | null {
return resolveEnemyCampAreaIdForDefender(session, 0);
}
/**
* 构建进攻某防守方的怪物行军路线格子索引。
* @param map 地图
* @param session 会话
* @param defenderPlayerId 防守方;默认本机
*/
export function buildEnemyMarchPathGridIndices(
map: CocosMapView,
session: BattleSession,
defenderPlayerId: number = 0,
): ReadonlyMembershipSet<number> {
const set = createMembershipSet<number>();
const enemyCamp = resolveEnemyCampAreaIdForDefender(session, defenderPlayerId);
if (enemyCamp == null) return set;
const spawnNodes = map.spawnPointParent?.children ?? [];
for (let i = 0; i < spawnNodes.length; i++) {
const spawn = spawnNodes[i];
if (!spawn?.isValid) continue;
const match = spawn.name.match(SPAWN_POINT_NAME_RE);
if (!match) continue;
const port = parseInt(match[1], 10);
if (session.topology.getSpawnPortCampArea(port) !== enemyCamp) continue;
const startGrid = map.getMapGridByWorldPos(spawn.worldPosition);
if (!startGrid?.isCanWalk) continue;
const start: Cell = { row: startGrid.Row, col: startGrid.Col };
let target: Cell | null = null;
try {
target = session.pathfinder.shortestTargetCell(start, enemyCamp);
} catch {
target = null;
}
if (!target) continue;
let path: Cell[] = [];
try {
path = session.pathfinder.findPath(start, target, true, enemyCamp);
} catch {
path = [];
}
for (let j = 0; j < path.length; j++) {
const mg = map.getMapGrid(path[j].row, path[j].col);
if (mg) membershipAdd(set, mg.Index);
}
}
return set;
}
/**
* 格是否位于进攻某防守方的怪物行军路线上。
* @param map 地图
* @param session 会话
* @param grid 格
* @param marchPathIndices 可选缓存
* @param defenderPlayerId 防守方;默认本机
*/
export function isEnemyMonsterWalkGrid(
map: CocosMapView,
session: BattleSession,
grid: MapGrid | null | undefined,
marchPathIndices?: ReadonlyMembershipSet<number> | null,
defenderPlayerId: number = 0,
): boolean {
if (!grid?.isCanWalk) return false;
const enemyCamp = resolveEnemyCampAreaIdForDefender(session, defenderPlayerId);
if (enemyCamp == null || grid.CampAreaID !== enemyCamp) return false;
const indices = marchPathIndices ?? buildEnemyMarchPathGridIndices(map, session, defenderPlayerId);
return indices.has(grid.Index);
}
/** 出怪口 port 对应的可走地图格 */
export function resolveSpawnGridByPort(
map: CocosMapView,
session: BattleSession,
port: number,
): MapGrid | null {
const cell = session.topology.getSpawnCell(port);
if (cell) {
const grid = map.getMapGrid(cell.row, cell.col);
if (grid?.isCanWalk) return grid;
}
const spawnNode = map.spawnPointParent?.getChildByName(`spawnPoint_${port}`);
if (!spawnNode) return null;
const spawnGrid = map.getMapGridByWorldPos(spawnNode.worldPosition);
if (!spawnGrid?.isCanWalk) return null;
return spawnGrid;
}
/** 指定阵营区域的所有出怪口编号(升序) */
export function collectSpawnPortsForCampArea(session: BattleSession, campAreaId: number): number[] {
const ports = mapKeysToArray(session.topology.spawnPorts);
const out: number[] = [];
for (let i = 0; i < ports.length; i++) {
const port = ports[i];
if (port <= 0) continue;
if (session.topology.getSpawnPortCampArea(port) === campAreaId) {
out.push(port);
}
}
out.sort((a, b) => a - b);
return out;
}
/**
* 出怪口沿进路向防守方前进 steps 格(1 = 出怪口下一格)。
* @param map 地图
* @param session 会话
* @param port 出怪口编号
* @param defenderPlayerId 被进攻防守方 playerId
* @param steps 沿进路前进格数
*/
export function resolveGridAheadOfSpawnPort(
map: CocosMapView,
session: BattleSession,
port: number,
defenderPlayerId: number,
steps: number = 1,
): MapGrid | null {
const enemyCamp = resolveEnemyCampAreaIdForDefender(session, defenderPlayerId);
if (enemyCamp == null) return null;
const spawnGrid = resolveSpawnGridByPort(map, session, port);
if (!spawnGrid) return null;
const stand = resolveAttackStandGridForCamp(map, session, enemyCamp);
if (!stand) return null;
const stepCount = Math.max(1, Math.floor(steps) || 1);
session.pathfinder.rebuild(session.topology);
const from: Cell = { row: spawnGrid.Row, col: spawnGrid.Col };
const to: Cell = { row: stand.Row, col: stand.Col };
let path = session.pathfinder.buildPathToTarget(from, to, enemyCamp);
if (path.length < stepCount + 1) {
path = session.pathfinder.findPath(from, to, true, enemyCamp);
}
if (path.length < stepCount + 1) return null;
const cell = path[stepCount];
return map.getMapGrid(cell.row, cell.col);
}
/**
* 出怪口沿进路向防守方前进,返回 spawn→目标的前缀路径(含出怪口格 path[0])。
* @param map 地图
* @param session 会话
* @param port 出怪口编号
* @param defenderPlayerId 被进攻防守方 playerId
* @param maxSteps 沿进路前进格数(0 仅出怪口格)
*/
export function resolvePathCellsFromSpawnPort(
map: CocosMapView,
session: BattleSession,
port: number,
defenderPlayerId: number,
maxSteps: number,
): Cell[] {
const enemyCamp = resolveEnemyCampAreaIdForDefender(session, defenderPlayerId);
if (enemyCamp == null) return [];
const spawnGrid = resolveSpawnGridByPort(map, session, port);
if (!spawnGrid) return [];
const stand = resolveAttackStandGridForCamp(map, session, enemyCamp);
if (!stand) return [];
const stepCount = Math.max(0, Math.floor(maxSteps) || 0);
session.pathfinder.rebuild(session.topology);
const from: Cell = { row: spawnGrid.Row, col: spawnGrid.Col };
const to: Cell = { row: stand.Row, col: stand.Col };
let path = session.pathfinder.buildPathToTarget(from, to, enemyCamp);
if (path.length < 2) {
path = session.pathfinder.findPath(from, to, true, enemyCamp);
}
const take = Math.min(path.length, stepCount + 1);
return path.slice(0, take);
}