|
|
|
|
import { _decorator, Component, Node, Sorting2D, Tween, UITransform, Vec3, sp, tween } from 'cc';
|
|
|
|
|
import { createMembershipSet, membershipAdd, membershipFromArray, membershipDedupeArray, type MembershipSet, type ReadonlyMembershipSet } from '../../../../mx/tools/MembershipSet';
|
|
|
|
|
import { MapGrid } from './MapGrid';
|
|
|
|
|
import { MapObstacle, ObstacleType } from './MapObstacle';
|
|
|
|
|
import { GBundle, GPath } from '../../../game/ConfigRes';
|
|
|
|
|
import { GEvent } from 'db://assets/mx/module/event/GEvent';
|
|
|
|
|
import {
|
|
|
|
|
LOCAL_ENEMY_SPAWN_PORT,
|
|
|
|
|
LOCAL_OWN_SPAWN_PORT,
|
|
|
|
|
pickParatroopLandingGrids,
|
|
|
|
|
pickRandomReachableWalkGrids,
|
|
|
|
|
resolveAttackTargetGrid,
|
|
|
|
|
resolveAttackTargetNode,
|
|
|
|
|
} from '../MapPathQueries';
|
|
|
|
|
import type { Cell } from 'db://assets/mx/module/battle/battlefield/Cell';
|
|
|
|
|
import type { BattleSession } from 'db://assets/mx/module/battle';
|
|
|
|
|
import { mapGridToLogicCell } from '../BattleMapSpec';
|
|
|
|
|
import { isRowInRange, MapRowRange } from '../../mechanism/MechanismSpawnArea';
|
|
|
|
|
import { collectAndValidateMapCampPoints } from '../campAreaMapValidation';
|
|
|
|
|
const { ccclass, property } = _decorator;
|
|
|
|
|
|
|
|
|
|
const SPAWN_PORTAL_NODE = '出怪口';
|
|
|
|
|
/** 我方出怪口(spawnPoint_2) */
|
|
|
|
|
const SPAWN_PORTAL_ANIM_ALLY = '蓝方';
|
|
|
|
|
/** 敌方出怪口(spawnPoint_1) */
|
|
|
|
|
const SPAWN_PORTAL_ANIM_ENEMY = '红方';
|
|
|
|
|
/** 出怪口出现/隐藏透明度渐变时长 */
|
|
|
|
|
const SPAWN_PORTAL_FADE_SEC = 0.12;
|
|
|
|
|
|
|
|
|
|
/** Cocos 地图视图:格子/出怪口/攻击点/推荐点(寻路由 MX Pathfinder 负责) */
|
|
|
|
|
@ccclass('MapManager')
|
|
|
|
|
export class CocosMapView extends Component {
|
|
|
|
|
|
|
|
|
|
@property({ displayName: '地图行数' })
|
|
|
|
|
mapRows: number = 10;
|
|
|
|
|
|
|
|
|
|
@property({ displayName: '地图列数' })
|
|
|
|
|
mapCols: number = 8;
|
|
|
|
|
|
|
|
|
|
@property({ displayName: '格子大小' })
|
|
|
|
|
mapGridSize: number = 93;
|
|
|
|
|
|
|
|
|
|
/**地图格子父节点 */
|
|
|
|
|
mapGridParent: Node = null;
|
|
|
|
|
|
|
|
|
|
/** 英雄表现层(格子节点的后一个兄弟,不挂在格子下) */
|
|
|
|
|
heroParent: Node = null;
|
|
|
|
|
/**地图目标父节点 */
|
|
|
|
|
mapTargetParent: Node = null;
|
|
|
|
|
/**出怪口父节点 */
|
|
|
|
|
spawnPointParent: Node = null;
|
|
|
|
|
/**推荐点父节点 */
|
|
|
|
|
guidePosParent: Node = null;
|
|
|
|
|
/**地图格子数组 */
|
|
|
|
|
mapGrids: MapGrid[] = [];
|
|
|
|
|
/**根据行列号缓存地图格子 */
|
|
|
|
|
mapGridCache: Map<string, MapGrid> = new Map();
|
|
|
|
|
//所有出怪口的格子
|
|
|
|
|
allSpawnPointGrids: Map<MapGrid, Node> = new Map();
|
|
|
|
|
//所有怪物攻击点的格子
|
|
|
|
|
allAttackPointGrids: Map<MapGrid, Node> = new Map();
|
|
|
|
|
/** 攻击点节点列表(与拓扑 attackTargets 导入顺序一致) */
|
|
|
|
|
attackPointNodes: Node[] = [];
|
|
|
|
|
/** 出怪口 port(1-based) → 落点格阵营区域(initMapGrids 校验通过后写入) */
|
|
|
|
|
spawnPortCampArea = new Map<number, number>();
|
|
|
|
|
/**所有推荐最佳障碍物摆放点 */
|
|
|
|
|
allBestObstaclePlacementGrids: MapGrid[] = [];
|
|
|
|
|
/**所有免费推荐点 */
|
|
|
|
|
allFreeRecommendGrids: MapGrid[] = [];
|
|
|
|
|
|
|
|
|
|
/**所有静态障碍物格子 */
|
|
|
|
|
allStaticObstacleGrids: MapGrid[] = [];
|
|
|
|
|
|
|
|
|
|
/** initMapGrids 是否已完成(出怪口/攻击点可用) */
|
|
|
|
|
private _gridsInitialized = false;
|
|
|
|
|
get gridsInitialized(): boolean {
|
|
|
|
|
return this._gridsInitialized;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 通知:地图可走状态发生变化(由 MapPlaceValidator 同步至 MX) */
|
|
|
|
|
public notifyMapWalkStateChanged() {
|
|
|
|
|
/* 寻路/可达性由 BattleSession.pathfinder 维护 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected onLoad(): void {
|
|
|
|
|
this.mapGridParent = this.node.getChildByName('格子');
|
|
|
|
|
this.mapTargetParent = this.node.getChildByName('目标点');
|
|
|
|
|
this.spawnPointParent = this.node.getChildByName('出怪口');
|
|
|
|
|
this.guidePosParent = new Node("guidePosParent");
|
|
|
|
|
this.guidePosParent.parent = this.node;
|
|
|
|
|
this.guidePosParent.setPosition(0, 0, 0);
|
|
|
|
|
this.guidePosParent.active = false;
|
|
|
|
|
this.mapGrids = this.mapGridParent?.children?.map((node) => node.getOrAddComponent(MapGrid)) ?? [];
|
|
|
|
|
this.ensureHeroParent();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 格子后的英雄挂点,供 heroTemplate 使用(须始终压在格子层之上) */
|
|
|
|
|
ensureHeroParent(): Node {
|
|
|
|
|
if (this.heroParent?.isValid) {
|
|
|
|
|
this._ensureHeroParentAboveGrids(this.heroParent);
|
|
|
|
|
return this.heroParent;
|
|
|
|
|
}
|
|
|
|
|
let n = this.node.getChildByName('英雄');
|
|
|
|
|
if (!n?.isValid) {
|
|
|
|
|
n = new Node('英雄');
|
|
|
|
|
n.layer = this.mapGridParent?.layer ?? this.node.layer;
|
|
|
|
|
const ut = n.addComponent(UITransform);
|
|
|
|
|
const src = this.mapGridParent?.getComponent(UITransform);
|
|
|
|
|
if (src) {
|
|
|
|
|
ut.setContentSize(src.contentSize);
|
|
|
|
|
ut.setAnchorPoint(src.anchorPoint);
|
|
|
|
|
}
|
|
|
|
|
n.setPosition(0, 0, 0);
|
|
|
|
|
this.node.addChild(n);
|
|
|
|
|
}
|
|
|
|
|
this._ensureHeroParentAboveGrids(n);
|
|
|
|
|
this.heroParent = n;
|
|
|
|
|
return n;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 预制体里「英雄」若排在「格子」前,字块会盖住 HeroActor,邻格放字时像「单位消失」 */
|
|
|
|
|
private _ensureHeroParentAboveGrids(heroParent: Node): void {
|
|
|
|
|
if (!heroParent?.isValid || !this.mapGridParent?.isValid) return;
|
|
|
|
|
if (heroParent.parent !== this.node) {
|
|
|
|
|
heroParent.parent = this.node;
|
|
|
|
|
}
|
|
|
|
|
const after = this.mapGridParent.getSiblingIndex() + 1;
|
|
|
|
|
if (heroParent.getSiblingIndex() !== after) {
|
|
|
|
|
heroParent.setSiblingIndex(after);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**初始化地图格子 */
|
|
|
|
|
initMapGrids() {
|
|
|
|
|
this._gridsInitialized = false;
|
|
|
|
|
this.allSpawnPointGrids.clear();
|
|
|
|
|
this.allAttackPointGrids.clear();
|
|
|
|
|
this.attackPointNodes = [];
|
|
|
|
|
this.spawnPortCampArea.clear();
|
|
|
|
|
this.mapGridCache.clear();
|
|
|
|
|
this.allBestObstaclePlacementGrids = [];
|
|
|
|
|
this.allStaticObstacleGrids = [];
|
|
|
|
|
this.allFreeRecommendGrids = [];
|
|
|
|
|
|
|
|
|
|
for (let row = 0; row < this.mapRows; row++) {
|
|
|
|
|
for (let col = 0; col < this.mapCols; col++) {
|
|
|
|
|
let index = this.getMapGridIndex(row, col);
|
|
|
|
|
let mg = this.getMapGridByIndex(index);
|
|
|
|
|
if (!mg) continue;
|
|
|
|
|
mg.Row = row;
|
|
|
|
|
mg.Col = col;
|
|
|
|
|
mg.Index = index;
|
|
|
|
|
this.mapGridCache.set(`${row}-${col}`, mg);
|
|
|
|
|
|
|
|
|
|
if (mg.isBestPlace) {
|
|
|
|
|
this.allBestObstaclePlacementGrids.push(mg);
|
|
|
|
|
}
|
|
|
|
|
if (mg.isFreePlace) {
|
|
|
|
|
this.allFreeRecommendGrids.push(mg);
|
|
|
|
|
}
|
|
|
|
|
if (mg.getComponent(MapObstacle)?.type == ObstacleType.静态障碍物) {
|
|
|
|
|
this.allStaticObstacleGrids.push(mg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//获取所有出怪口和怪物攻击点的格子
|
|
|
|
|
this.spawnPointParent?.children?.forEach((n) => {
|
|
|
|
|
let g = this.getMapGridByWorldPos(n.worldPosition);
|
|
|
|
|
if (g) this.allSpawnPointGrids.set(g, n);
|
|
|
|
|
});
|
|
|
|
|
this.mapTargetParent?.children?.forEach((n) => {
|
|
|
|
|
n.children.map(c => { let s = c.getOrAddComponent(Sorting2D); s.sortingLayer = 3; s.sortingOrder = 2; });
|
|
|
|
|
const g = this.findNearestWalkableGridForTarget(n.worldPosition);
|
|
|
|
|
if (g) {
|
|
|
|
|
this.allAttackPointGrids.set(g, n);
|
|
|
|
|
this.attackPointNodes.push(n);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
//需要修改位置的出怪口
|
|
|
|
|
let needMoveSpawnPointGrids: MapGrid[] = [];
|
|
|
|
|
//需要修改位置的怪物攻击点
|
|
|
|
|
let needMoveAttackPointGrids: MapGrid[] = [];
|
|
|
|
|
|
|
|
|
|
for (let row = 0; row < this.mapRows; row++) {
|
|
|
|
|
for (let col = 0; col < this.mapCols; col++) {
|
|
|
|
|
let index = this.getMapGridIndex(row, col);
|
|
|
|
|
let mg = this.getMapGridByIndex(index);
|
|
|
|
|
//宝箱副本最后两列预留宝箱怪区域,禁止通行、放置、铲除
|
|
|
|
|
// 旧宝箱模式列锁已移除(塔防重构)
|
|
|
|
|
if (false) {
|
|
|
|
|
mg.isCanWalk = false;
|
|
|
|
|
mg.isCanPutText = false;
|
|
|
|
|
mg.isCanRemove = false;
|
|
|
|
|
|
|
|
|
|
if (this.allSpawnPointGrids.has(mg)) {
|
|
|
|
|
needMoveSpawnPointGrids.push(mg);
|
|
|
|
|
}
|
|
|
|
|
if (this.allAttackPointGrids.has(mg)) {
|
|
|
|
|
needMoveAttackPointGrids.push(mg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//修改怪物出生位置
|
|
|
|
|
for (let i = 0; i < this.mapGrids.length; i++) {
|
|
|
|
|
if (i < 6) continue;
|
|
|
|
|
if (needMoveSpawnPointGrids.length == 0) break;
|
|
|
|
|
let mapGrid = this.mapGrids[i];
|
|
|
|
|
if (mapGrid.isCanWalk) {
|
|
|
|
|
let g = needMoveSpawnPointGrids.shift();
|
|
|
|
|
if (g) {
|
|
|
|
|
let node = this.allSpawnPointGrids.get(g)!;
|
|
|
|
|
node.worldPosition = mapGrid.node.worldPosition.clone();
|
|
|
|
|
this.allSpawnPointGrids.set(mapGrid, node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//修改怪物攻击位置
|
|
|
|
|
if (this.allAttackPointGrids.size == needMoveAttackPointGrids.length) {
|
|
|
|
|
for (let i = 0; i < needMoveAttackPointGrids.length; i++) {
|
|
|
|
|
let mapGrid = needMoveAttackPointGrids[i];
|
|
|
|
|
let n = this.allAttackPointGrids.get(mapGrid);
|
|
|
|
|
let mPg = this.mapGrids[188];
|
|
|
|
|
n.worldPosition = mPg.node.worldPosition.clone();
|
|
|
|
|
this.allAttackPointGrids.set(mPg, n);
|
|
|
|
|
n.destroyAllChildren();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for (let i = 0; i < needMoveAttackPointGrids.length; i++) {
|
|
|
|
|
let mapGrid = needMoveAttackPointGrids[i];
|
|
|
|
|
let n = this.allAttackPointGrids.get(mapGrid);
|
|
|
|
|
n.destroy();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 地图可走性/障碍状态初始化完成,通知寻路/可达性预计算失效
|
|
|
|
|
this.notifyMapWalkStateChanged();
|
|
|
|
|
const isPerimeter = !!gg.game.CurentBattle?.session?.topology?.enablePatrolLoop;
|
|
|
|
|
const campValidation = collectAndValidateMapCampPoints(this, {
|
|
|
|
|
requireAttackPerCampArea: !isPerimeter,
|
|
|
|
|
});
|
|
|
|
|
if (campValidation.errors.length > 0) {
|
|
|
|
|
const mapName = this.node?.name ?? 'MapManager';
|
|
|
|
|
for (const err of campValidation.errors) {
|
|
|
|
|
console.error(String(err));
|
|
|
|
|
}
|
|
|
|
|
console.error(`[MapManager] ${mapName}: 地图阵营/出怪口/攻击点校验失败,共 ${campValidation.errors.length} 项`);
|
|
|
|
|
this._gridsInitialized = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.spawnPortCampArea.clear();
|
|
|
|
|
for (const b of campValidation.spawnBindings) {
|
|
|
|
|
this.spawnPortCampArea.set(b.port, b.campAreaId);
|
|
|
|
|
}
|
|
|
|
|
this.validateMapPoints();
|
|
|
|
|
this._gridsInitialized = true;
|
|
|
|
|
this.hideRecommendPoints();
|
|
|
|
|
GEvent.Ins.emit(GEvent.MapGridsReady, this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 校验地图节点配置(制作规范,非运行时容错)
|
|
|
|
|
* - 出怪口须落在可走格上
|
|
|
|
|
* - 普通模式须有「目标点」
|
|
|
|
|
* - 循环圈地图允许无目标点
|
|
|
|
|
*/
|
|
|
|
|
validateMapPoints(): void {
|
|
|
|
|
const mapName = this.node?.name ?? 'MapManager';
|
|
|
|
|
if (!this.spawnPointParent?.children?.length) {
|
|
|
|
|
console.error(`[MapManager] ${mapName}: 缺少「出怪口」子节点`);
|
|
|
|
|
}
|
|
|
|
|
this.spawnPointParent?.children.forEach((n) => {
|
|
|
|
|
const grid = this.getMapGridByWorldPos(n.worldPosition);
|
|
|
|
|
if (!grid?.isCanWalk) {
|
|
|
|
|
console.error(
|
|
|
|
|
`[MapManager] ${mapName}: 出怪口 ${n.name} 不在可走格上`,
|
|
|
|
|
n.worldPosition,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const isPerimeter = !!gg.game.CurentBattle?.session?.topology?.enablePatrolLoop;
|
|
|
|
|
if (!this.mapTargetParent?.children?.length) {
|
|
|
|
|
if (isPerimeter) {
|
|
|
|
|
// console.log(`[MapManager] ${mapName}: 循环圈模式无「目标点」`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
console.error(`[MapManager] ${mapName}: 缺少「目标点」子节点`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.mapTargetParent.children.forEach((n) => {
|
|
|
|
|
const grid = this.findNearestWalkableGridForTarget(n.worldPosition);
|
|
|
|
|
if (!grid) {
|
|
|
|
|
console.error(
|
|
|
|
|
`[MapManager] ${mapName}: 攻击点 ${n.name} 无法映射到可走格`,
|
|
|
|
|
n.worldPosition,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (this.allAttackPointGrids.size === 0) {
|
|
|
|
|
console.error(`[MapManager] ${mapName}: 没有有效的攻击点格子`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @deprecated 推荐点引导已移除 */
|
|
|
|
|
createRecommendPoints(_grid: MapGrid): void {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @deprecated 推荐点引导已移除 */
|
|
|
|
|
updateRecommendPoints(_count: number = 1): void {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @deprecated 推荐点引导已移除 */
|
|
|
|
|
showRecommendPoints(): void {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @deprecated 推荐点引导已移除 */
|
|
|
|
|
hideRecommendPoints(): void {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**根据行列号获取地图索引 */
|
|
|
|
|
getMapGridIndex(row: number, col: number): number {
|
|
|
|
|
return row * this.mapCols + col;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**根据行列号获取地图格子 */
|
|
|
|
|
getMapGrid(row: number, col: number): MapGrid {
|
|
|
|
|
let grid = this.mapGridCache.get(`${row}-${col}`);
|
|
|
|
|
return grid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**根据地图索引获取地图格子 */
|
|
|
|
|
getMapGridByIndex(index: number): MapGrid {
|
|
|
|
|
if (!this.mapGrids?.length) return null;
|
|
|
|
|
if (index < 0 || index >= this.mapRows * this.mapCols) return null;
|
|
|
|
|
return this.mapGrids[index];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**根据世界坐标获取mapGrid */
|
|
|
|
|
getMapGridByWorldPos(worldPos: Vec3): MapGrid {
|
|
|
|
|
if (!worldPos) return null;
|
|
|
|
|
const grids = this.mapGrids;
|
|
|
|
|
if (!Array.isArray(grids) || grids.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
const p1 = worldPos;
|
|
|
|
|
const offset = this.mapGridSize * 0.5;
|
|
|
|
|
for (let i = 0, len = grids.length; i < len; i++) {
|
|
|
|
|
const mapGrid = grids[i];
|
|
|
|
|
if (!mapGrid || !mapGrid.node || !mapGrid.node.isValid) continue;
|
|
|
|
|
const p2 = mapGrid.node.worldPosition;
|
|
|
|
|
let minX = p2.x - offset;
|
|
|
|
|
let maxX = p2.x + offset;
|
|
|
|
|
let minY = p2.y - offset;
|
|
|
|
|
let maxY = p2.y + offset;
|
|
|
|
|
if (p1.x >= minX && p1.x <= maxX && p1.y >= minY && p1.y <= maxY) {
|
|
|
|
|
return mapGrid;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 攻击点/城墙节点常在不可走格上,寻路终点应取该格八邻域内最近的可走格(无全图回退)。
|
|
|
|
|
*/
|
|
|
|
|
public findNearestWalkableGridForTarget(worldPos: Vec3): MapGrid | null {
|
|
|
|
|
const direct = this.getMapGridByWorldPos(worldPos);
|
|
|
|
|
if (!direct) return null;
|
|
|
|
|
if (direct.isCanWalk) return direct;
|
|
|
|
|
|
|
|
|
|
const queue: MapGrid[] = [direct];
|
|
|
|
|
const visited = membershipFromArray<number>([direct.Index]);
|
|
|
|
|
const dirs: [number, number][] = [
|
|
|
|
|
[-1, 0], [1, 0], [0, -1], [0, 1],
|
|
|
|
|
[-1, -1], [-1, 1], [1, -1], [1, 1],
|
|
|
|
|
];
|
|
|
|
|
let head = 0;
|
|
|
|
|
while (head < queue.length) {
|
|
|
|
|
const cur = queue[head++];
|
|
|
|
|
if (cur.isCanWalk) return cur;
|
|
|
|
|
for (let i = 0; i < dirs.length; i++) {
|
|
|
|
|
const [dr, dc] = dirs[i];
|
|
|
|
|
const nb = this.getMapGrid(cur.Row + dr, cur.Col + dc);
|
|
|
|
|
if (!nb || visited.has(nb.Index)) continue;
|
|
|
|
|
membershipAdd(visited, nb.Index);
|
|
|
|
|
queue.push(nb);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**获取所有可通行的地图格子 */
|
|
|
|
|
getAllCanWalkMapGrids(): MapGrid[] {
|
|
|
|
|
let canWalkGrids: MapGrid[] = [];
|
|
|
|
|
for (let i = 0; i < this.mapGrids.length; i++) {
|
|
|
|
|
let mapGrid = this.mapGrids[i];
|
|
|
|
|
if (mapGrid.isCanWalk) {
|
|
|
|
|
canWalkGrids.push(mapGrid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return canWalkGrids;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 获取所有可被开挖的地图格子(运行时 isCanBeDig) */
|
|
|
|
|
getAllCanBeDigMapGrids(): MapGrid[] {
|
|
|
|
|
const grids: MapGrid[] = [];
|
|
|
|
|
for (let i = 0; i < this.mapGrids.length; i++) {
|
|
|
|
|
const mapGrid = this.mapGrids[i];
|
|
|
|
|
if (mapGrid.isCanBeDig) {
|
|
|
|
|
grids.push(mapGrid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return grids;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 随机获取 count 个不同的可被开挖地图格子 */
|
|
|
|
|
getRandomCanBeDigMapGrids(count: number): MapGrid[] {
|
|
|
|
|
const candidates = this.getAllCanBeDigMapGrids();
|
|
|
|
|
const shuffled = candidates.slice();
|
|
|
|
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
|
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
|
|
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
|
|
|
}
|
|
|
|
|
return shuffled.slice(0, count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**获取所有可放置障碍物的地图格子 */
|
|
|
|
|
getAllCanPutObstacleMapGrids(): MapGrid[] {
|
|
|
|
|
let canPutObstacleGrids: MapGrid[] = [];
|
|
|
|
|
for (let i = 0; i < this.mapGrids.length; i++) {
|
|
|
|
|
let mapGrid = this.mapGrids[i];
|
|
|
|
|
if (mapGrid.isCanPutText && mapGrid.node.active) {
|
|
|
|
|
canPutObstacleGrids.push(mapGrid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return canPutObstacleGrids;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**获取所有可铲除的地图格子 */
|
|
|
|
|
getAllCanRemoveMapGrids(): MapGrid[] {
|
|
|
|
|
let canRemoveGrids: MapGrid[] = [];
|
|
|
|
|
for (let i = 0; i < this.mapGrids.length; i++) {
|
|
|
|
|
let mapGrid = this.mapGrids[i];
|
|
|
|
|
if (mapGrid.isCanRemove) {
|
|
|
|
|
canRemoveGrids.push(mapGrid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return canRemoveGrids;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 随机获取 count 个可达攻击点的可行走格(MapGrid.isCanWalk,空投等) */
|
|
|
|
|
getRandomReachableWalkMapGrids(count: number, session: BattleSession, rowRange?: MapRowRange): MapGrid[] {
|
|
|
|
|
return pickRandomReachableWalkGrids(this, session, count, rowRange);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 空投落点:可达优先,不足时用当前可行走格补足 */
|
|
|
|
|
getParatroopLandingGrids(count: number, session: BattleSession, rowRange?: MapRowRange): MapGrid[] {
|
|
|
|
|
return pickParatroopLandingGrids(this, session, count, rowRange);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**随机获取 count 个不同的可行走地图格子 */
|
|
|
|
|
getRandomCanWalkMapGrids(count: number, rowRange?: MapRowRange): MapGrid[] {
|
|
|
|
|
let canWalkGrids = this.getAllCanWalkMapGrids();
|
|
|
|
|
if (rowRange) {
|
|
|
|
|
canWalkGrids = canWalkGrids.filter(g => isRowInRange(g.Row, rowRange));
|
|
|
|
|
}
|
|
|
|
|
const shuffled = canWalkGrids.slice(0);
|
|
|
|
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
|
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
|
|
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
|
|
|
}
|
|
|
|
|
return shuffled.slice(0, count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**随机获取count个不同的可放置的地图格子 */
|
|
|
|
|
getRandomCanPutObstacleMapGrids(count: number, rowRange?: MapRowRange): MapGrid[] {
|
|
|
|
|
let canPutObstacleGrids = this.getAllCanPutObstacleMapGrids();
|
|
|
|
|
if (rowRange) {
|
|
|
|
|
canPutObstacleGrids = canPutObstacleGrids.filter(g => isRowInRange(g.Row, rowRange));
|
|
|
|
|
}
|
|
|
|
|
let shuffled = canPutObstacleGrids.slice(0);
|
|
|
|
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
|
|
|
let j = Math.floor(Math.random() * (i + 1));
|
|
|
|
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
|
|
|
}
|
|
|
|
|
return shuffled.slice(0, count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 根据世界坐标获取路径最短的终点格子(委托 MX Pathfinder) */
|
|
|
|
|
getPathShortestTargetGrid(startPos: Vec3, spawnPort?: number): MapGrid | null {
|
|
|
|
|
const session = gg.game.CurentBattle?.session;
|
|
|
|
|
return resolveAttackTargetGrid(this, startPos, session, spawnPort);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 根据世界坐标获取路径最短的目标城墙节点 */
|
|
|
|
|
getPathShortestTarget(startPos: Vec3, spawnPort?: number): Node | null {
|
|
|
|
|
const session = gg.game.CurentBattle?.session;
|
|
|
|
|
return resolveAttackTargetNode(this, startPos, session, spawnPort);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 拓扑攻击点索引 → 城墙节点(与 importTopology attackTargets 同序) */
|
|
|
|
|
getAttackPointNode(index: number): Node | null {
|
|
|
|
|
const node = this.attackPointNodes[index];
|
|
|
|
|
return node?.isValid ? node : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**随机雷击一个障碍物 */
|
|
|
|
|
randomThunderObstacle() {
|
|
|
|
|
let grids = this.mapGrids.filter(grid => grid.isCanBeThunder);
|
|
|
|
|
if (grids.length == 0) return null;
|
|
|
|
|
let grid = grids[Math.floor(Math.random() * grids.length)];
|
|
|
|
|
grid.getComponent(MapObstacle).thunderAtk();
|
|
|
|
|
return grid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**找出 count 个最佳摆放点(type3:模拟放置后怪物绕路最远) */
|
|
|
|
|
getBestObstaclePlacementGrids(count: number): MapGrid[] {
|
|
|
|
|
if (count <= 0 || this.allBestObstaclePlacementGrids.length === 0) return [];
|
|
|
|
|
const canPlaceGrids = this.allBestObstaclePlacementGrids.filter(
|
|
|
|
|
(grid) => grid.isCanWalk && grid.isCanPutGrid && !grid.ShowGuidePos && this.canPlaceType3At(grid),
|
|
|
|
|
);
|
|
|
|
|
if (canPlaceGrids.length === 0) return [];
|
|
|
|
|
|
|
|
|
|
const session = gg.game.CurentBattle?.session;
|
|
|
|
|
if (!session) {
|
|
|
|
|
return canPlaceGrids.slice(0, count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const scored = canPlaceGrids.map((grid) => ({
|
|
|
|
|
grid,
|
|
|
|
|
detour: this._measureTotalPathDistance(session, grid),
|
|
|
|
|
}));
|
|
|
|
|
scored.sort((a, b) => b.detour - a.detour);
|
|
|
|
|
return scored.slice(0, count).map((item) => item.grid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 所有真实出怪口 × 攻击点的寻路距离之和。
|
|
|
|
|
* @param blocked 模拟在该格放置 type3 后统计
|
|
|
|
|
*/
|
|
|
|
|
private _measureTotalPathDistance(session: BattleSession, blocked?: MapGrid): number {
|
|
|
|
|
const blockCell = blocked
|
|
|
|
|
? mapGridToLogicCell(blocked.Row, blocked.Col, session.grid.rows, session.grid.cols)
|
|
|
|
|
: null;
|
|
|
|
|
return this._withSimulatedWalkBlock(session, blockCell, () =>
|
|
|
|
|
this._sumSpawnToTargetPathDistances(session),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 临时阻塞一格并重算寻路场,统计后立即还原(不触发 MovementSystem) */
|
|
|
|
|
private _withSimulatedWalkBlock(
|
|
|
|
|
session: BattleSession,
|
|
|
|
|
blockCell: Cell | null,
|
|
|
|
|
measure: () => number,
|
|
|
|
|
): number {
|
|
|
|
|
let restored = false;
|
|
|
|
|
if (blockCell && session.grid.inBounds(blockCell) && session.grid.isWalkable(blockCell)) {
|
|
|
|
|
session.grid.setWalkable(blockCell, false);
|
|
|
|
|
session.pathfinder.invalidate();
|
|
|
|
|
session.pathfinder.rebuild(session.topology);
|
|
|
|
|
restored = true;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
return measure();
|
|
|
|
|
} finally {
|
|
|
|
|
if (restored && blockCell) {
|
|
|
|
|
session.grid.setWalkable(blockCell, true);
|
|
|
|
|
session.pathfinder.invalidate();
|
|
|
|
|
session.pathfinder.rebuild(session.topology);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 全部出怪口到全部攻击点的 8 向路径世界距离累加 */
|
|
|
|
|
private _sumSpawnToTargetPathDistances(session: BattleSession): number {
|
|
|
|
|
const spawns = session.topology.getAllSpawnCells();
|
|
|
|
|
const targets = session.topology.attackTargets;
|
|
|
|
|
if (!spawns.length || !targets.length) return 0;
|
|
|
|
|
|
|
|
|
|
let total = 0;
|
|
|
|
|
const pf = session.pathfinder;
|
|
|
|
|
for (const spawn of spawns) {
|
|
|
|
|
for (const target of targets) {
|
|
|
|
|
const path = pf.findPath(spawn, target, true);
|
|
|
|
|
total += this._pathWorldLength(path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return total;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _pathWorldLength(path: readonly Cell[]): number {
|
|
|
|
|
if (path.length < 2) return 0;
|
|
|
|
|
let total = 0;
|
|
|
|
|
for (let k = 0; k < path.length - 1; k++) {
|
|
|
|
|
const g0 = this.getMapGrid(path[k].row, path[k].col);
|
|
|
|
|
const g1 = this.getMapGrid(path[k + 1].row, path[k + 1].col);
|
|
|
|
|
if (!g0?.node?.isValid || !g1?.node?.isValid) continue;
|
|
|
|
|
total += Vec3.distance(g0.node.worldPosition, g1.node.worldPosition);
|
|
|
|
|
}
|
|
|
|
|
return total;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 地图是否仍有可用的预设最佳摆放点 */
|
|
|
|
|
hasBestPlacementTargets(): boolean {
|
|
|
|
|
return this.allBestObstaclePlacementGrids.some(
|
|
|
|
|
(grid) => grid.isCanWalk && grid.isCanPutGrid,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 假设在该格放置 type3 扩展后路径是否仍通 */
|
|
|
|
|
canPlaceType3At(grid: MapGrid): boolean {
|
|
|
|
|
if (!grid?.isCanWalk || !grid.isCanPutGrid) return false;
|
|
|
|
|
const session = gg.game.CurentBattle?.session;
|
|
|
|
|
if (!session) return false;
|
|
|
|
|
const cell = { row: grid.Row, col: grid.Col };
|
|
|
|
|
const isPerimeter = !!gg.game.CurentBattle?.session?.topology?.enablePatrolLoop;
|
|
|
|
|
if (isPerimeter || session.topology.enablePatrolLoop) {
|
|
|
|
|
const loop = session.topology.patrolLoop;
|
|
|
|
|
if (!loop.length) 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 (!session.topology.attackTargets.length) return false;
|
|
|
|
|
try {
|
|
|
|
|
return session.pathfinder.checkMultiSourceReachability(
|
|
|
|
|
session.topology.getAllSpawnCells(),
|
|
|
|
|
session.topology.attackTargets,
|
|
|
|
|
cell,
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 出怪口:我方播蓝方、敌方播红方,再渐显 → 保持 → 渐隐 */
|
|
|
|
|
playSpawnPointEffect(spawnPort: number, durationSec: number): void {
|
|
|
|
|
const spawnNode = this.spawnPointParent?.getChildByName(`spawnPoint_${spawnPort}`);
|
|
|
|
|
if (!spawnNode?.isValid) {
|
|
|
|
|
console.error('[MapManager] spawn point missing for portal effect', spawnPort);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let portal = spawnNode.getChildByName(SPAWN_PORTAL_NODE);
|
|
|
|
|
if (!portal?.isValid) {
|
|
|
|
|
portal = gg.res.getNode(GPath.ObstaclePrefab(''), SPAWN_PORTAL_NODE, GBundle.BattleMap);
|
|
|
|
|
if (!portal?.isValid) {
|
|
|
|
|
console.error('[MapManager] 出怪口 prefab not loaded');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
portal.name = SPAWN_PORTAL_NODE;
|
|
|
|
|
portal.setParent(spawnNode);
|
|
|
|
|
portal.setPosition(0, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const skeleton = portal.getComponent(sp.Skeleton);
|
|
|
|
|
if (!skeleton) {
|
|
|
|
|
console.error('[MapManager] 出怪口 missing sp.Skeleton');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const anim = spawnPort === LOCAL_OWN_SPAWN_PORT
|
|
|
|
|
? SPAWN_PORTAL_ANIM_ALLY
|
|
|
|
|
: spawnPort === LOCAL_ENEMY_SPAWN_PORT
|
|
|
|
|
? SPAWN_PORTAL_ANIM_ENEMY
|
|
|
|
|
: '';
|
|
|
|
|
if (!anim || !skeleton.findAnimation(anim)) {
|
|
|
|
|
console.warn('[MapManager] 出怪口缺少阵营动画', spawnPort, anim);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dur = Math.max(durationSec, SPAWN_PORTAL_FADE_SEC * 2);
|
|
|
|
|
const fade = Math.min(SPAWN_PORTAL_FADE_SEC, dur * 0.25);
|
|
|
|
|
const hold = Math.max(0, dur - fade * 2);
|
|
|
|
|
|
|
|
|
|
Tween.stopAllByTarget(portal);
|
|
|
|
|
skeleton.setCompleteListener(null);
|
|
|
|
|
portal.active = true;
|
|
|
|
|
portal.opacity = 0;
|
|
|
|
|
skeleton.setAnimation(0, anim, true);
|
|
|
|
|
|
|
|
|
|
tween(portal)
|
|
|
|
|
.to(fade, { opacity: 255 })
|
|
|
|
|
.delay(hold)
|
|
|
|
|
.to(fade, { opacity: 0 })
|
|
|
|
|
.call(() => {
|
|
|
|
|
if (!portal.isValid) return;
|
|
|
|
|
skeleton.clearTracks();
|
|
|
|
|
portal.active = false;
|
|
|
|
|
})
|
|
|
|
|
.start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**获取所有可移除的障碍物 */
|
|
|
|
|
getAllCanRemoveObstacleGrids(): MapGrid[] {
|
|
|
|
|
return this.mapGrids.filter(grid => grid.isCanRemove);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @deprecated 请使用 CocosMapView */
|
|
|
|
|
export { CocosMapView as MapManager };
|