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