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.
 
 
 
 

709 lines
28 KiB

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 { MapAreaUnlock } from './MapAreaUnlock';
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;
/** 按区域依次解锁 */
areaUnlock = new MapAreaUnlock();
/** 英雄表现层(格子节点的后一个兄弟,不挂在格子下) */
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.areaUnlock.sync(this.mapGrids);
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 && mapGrid.unlockAreaId <= 0) {
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 };