import { Node, sp, Vec3 } from 'cc'; import type { EntityId } from 'db://assets/mx/module/ecs'; import { BattleTeamId, MovementComp, TeamComp, TransformComp, UnitMetaComp, isBajiaoBlowRetreating, isYanSoulClaimed, type BattleSession, type Cell, } from 'db://assets/mx/module/battle'; import { createMembershipSet, membershipAdd, membershipHas, membershipToArray } from '../../../mx/tools/MembershipSet'; import { GBundle } from '../../game/ConfigRes'; import { BATTLE_HERO_BUFF_PATH } from '../../battle/res/BattleHeroResCollector'; import { getShieldPushCells } from '../../battle/bag/BagPrepConfig'; import { BattleSpinePlayback } from '../../battle/presentation/BattleSpinePlayback'; import { isMechanismUnit } from '../../battle/mechanism/MechanismServices'; import { MonsterType } from '../../manager/MonsterDataManager'; import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime'; import type { MapManager } from '../../battle/map/legacy/MapManager'; import type { MapGrid } from '../../battle/map/legacy/MapGrid'; import { resolveEnemyCampAreaIdForDefender } from '../../battle/map/MapPathQueries'; import { resolveAttackStandGridForCamp } from '../../battle/view/tangSengMap'; import { logicalWorldToCocos } from '../../battle/bridge/CocosBattleMovementBridge'; import type { MembershipSet } from '../../../mx/tools/MembershipSet'; /** 门神陷阱预制体(battleHero/prefab/buff/门神) */ const MENSHEN_PREFAB_NAME = '门神'; /** 怪物到达陷阱前一格时触发门神 */ const TRAP_TRIGGER_BEFORE_CELLS = 1; /** 门神扫掠移动速度:格/秒 */ const MENSHEN_SWEEP_CELLS_PER_SEC = 1; /** 门神与怪物世界距离 ≤ 该像素值时开始击退 */ const MENSHEN_CATCH_PX = 40; /** 与 Pathfinder 正交步长一致 */ const PATH_ORTH_STEP = 1000; /** 待机 Spine 动画名 */ const IDLE_ANIM = { left: '向左待机', right: '向右待机', up: '向上待机', down: '向下待机', } as const; /** 击退 Spine 动画名 */ const KNOCK_ANIM = { left: '向左击退', right: '向右击退', up: '向上击退', down: '向下击退', } as const; /** 四向动画键 */ type MenShenDir = keyof typeof IDLE_ANIM; /** 单条进路门神陷阱 */ type MenShenTrapEntry = { /** 出怪口 */ port: number; /** 陷阱格索引 */ gridIndex: number; /** 行 */ row: number; /** 列 */ col: number; /** 出怪口→攻击站立格 完整进路(含陷阱与终点) */ routePath: Cell[]; /** 陷阱在 routePath 中的下标 */ trapPathIndex: number; /** 表现节点 */ node: Node; /** Spine 组件 */ skeleton: sp.Skeleton | null; /** 待机朝向(上一格相对陷阱的位置) */ idleDir: MenShenDir; /** 是否已触发扫掠 */ triggered: boolean; /** 是否正在播击退 */ knockActive: boolean; /** 当前击退朝向 */ currentKnockDir: MenShenDir | null; /** 扫掠路径(陷阱格→进路前方,含起点) */ moveCells: Cell[]; /** 当前扫掠段起点下标(moveCells) */ moveSegIndex: number; /** 扫掠期间门神逻辑坐标 X */ sweepLogicalX: number; /** 扫掠期间门神逻辑坐标 Y */ sweepLogicalY: number; /** 陷阱格心世界坐标 */ homeWorldPos: Vec3; /** 是否正在沿进路扫掠 */ sweeping: boolean; /** 本局 Const 推怪格数 */ pushCellsConfig: number; /** 被门神顶住、帧同步随门神后移的怪 */ riderIds: MembershipSet | null; }; /** * 门神盾牌:广告后在进路终点格布置;怪到陷阱前一格触发扫掠,碰到怪后帧同步后移 Const24 格,门神消失后怪再寻路。 */ export class BattleGameMenShenTrapController { private _battle: ProjectBattleHandle | null = null; private _map: MapManager | null = null; private _defenderPlayerId = 0; private readonly _traps: MenShenTrapEntry[] = []; private _retreatPathfinderBuilt = false; private readonly _tmpWorldA = new Vec3(); /** * @param battle 战斗句柄 * @param map 地图 * @param defenderPlayerId 防守方 playerId */ bind(battle: ProjectBattleHandle | null, map: MapManager | null, defenderPlayerId: number = 0): void { this.unbind(); this._battle = battle; this._map = map; this._defenderPlayerId = Math.max(0, Math.floor(Number(defenderPlayerId) || 0)); } /** 释放陷阱 */ unbind(): void { for (let i = 0; i < this._traps.length; i++) { this._releaseTrapNode(this._traps[i].node); } this._traps.length = 0; this._battle = null; this._map = null; this._retreatPathfinderBuilt = false; } /** 是否已布置陷阱 */ hasTrap(): boolean { return this._traps.length > 0; } /** * 观看广告成功后:在进路终点格布置门神。 * @returns 是否至少成功放置一处 */ tryPlaceTraps(): boolean { const battle = this._battle; const map = this._map; if (!battle || !map?.gridsInitialized) return false; if (this._traps.length > 0) return true; const session = battle.session; const enemyCampAreaId = resolveEnemyCampAreaIdForDefender(session, this._defenderPlayerId); if (enemyCampAreaId == null) { console.log('[MenShenTrap] 进攻方阵营区域缺失', this._defenderPlayerId); return false; } const stand = resolveAttackStandGridForCamp(map, session, enemyCampAreaId); if (!stand) { console.log('[MenShenTrap] 敌方进路攻击站立格缺失', enemyCampAreaId); return false; } session.pathfinder.rebuild(session.topology); const ports = this._collectSpawnPorts(session, enemyCampAreaId); let placed = false; for (let i = 0; i < ports.length; i++) { const port = ports[i]; const spawnGrid = this._resolveSpawnGridByPort(map, session, port); if (!spawnGrid) continue; const trapGrid = stand; if (!trapGrid?.node?.isValid) continue; if (this._hasTrapAt(trapGrid.Index)) continue; const spawnCell = { row: spawnGrid.Row, col: spawnGrid.Col }; const standCell = { row: stand.Row, col: stand.Col }; const routePath = this._buildAdvancePath( session, spawnCell, standCell, enemyCampAreaId, ); if (routePath.length < 2) continue; let trapPathIndex = -1; for (let j = 0; j < routePath.length; j++) { const c = routePath[j]; if (c.row === trapGrid.Row && c.col === trapGrid.Col) { trapPathIndex = j; break; } } if (trapPathIndex < 1) { console.log('[MenShenTrap] 陷阱不在进路上', port, trapGrid.Index); continue; } const prev = routePath[trapPathIndex - 1]; const curr = routePath[trapPathIndex]; const spawned = this._spawnTrapNode(trapGrid, prev, curr); if (!spawned) continue; this._traps.push({ port, gridIndex: trapGrid.Index, row: trapGrid.Row, col: trapGrid.Col, routePath, trapPathIndex, node: spawned.node, skeleton: spawned.skeleton, idleDir: spawned.idleDir, triggered: false, knockActive: false, currentKnockDir: null, moveCells: [], moveSegIndex: 0, sweepLogicalX: 0, sweepLogicalY: 0, homeWorldPos: new Vec3(), sweeping: false, pushCellsConfig: 0, riderIds: null, }); placed = true; } if (!placed) { console.log('[MenShenTrap] 门神陷阱放置失败'); } return placed; } /** 帧更新:怪到陷阱前一格时启动门神扫掠 */ tick(_dt: number): void { if (this._traps.length === 0) return; const battle = this._battle; const session = battle?.session; if (!session || session.ended) return; for (let i = this._traps.length - 1; i >= 0; i--) { const trap = this._traps[i]; if (!trap.node?.isValid) continue; if (!trap.triggered) { const triggerId = this._findTriggerMonster(session, trap); if (triggerId == null) continue; if (!this._triggerMenShenSweep(trap)) continue; trap.triggered = true; } if (trap.triggered) { this._tickMenShenMove(trap, _dt); } } } /** * 扫描敌方是否首次进入盾牌广告触发距离(离终点 Const23 格内)。 * @param triggerCells 触发距离 * @returns 是否应展示广告 */ shouldOfferShieldAd(triggerCells: number): boolean { const battle = this._battle; if (!battle || !(triggerCells > 0)) return false; return this._findClosestThreatRemaining(battle.session, triggerCells) != null; } /** 找最靠近终点、且处于触发距离内的进攻怪 */ private _findClosestThreatRemaining(session: BattleSession, thresholdCells: number): EntityId | null { const battle = this._battle; if (!battle) return null; let bestId: EntityId | null = null; let bestRemaining = Number.POSITIVE_INFINITY; session.world.forEachAlive((entityId) => { if (!this._isPushCandidate(battle, entityId)) return; const remaining = this._getRemainingCellsToAttackTarget(session, entityId); if (!Number.isFinite(remaining) || remaining > thresholdCells) return; if (remaining < bestRemaining) { bestRemaining = remaining; bestId = entityId; } }); return bestId; } /** 找已到达陷阱前一格、最靠近陷阱的怪 */ private _findTriggerMonster(session: BattleSession, trap: MenShenTrapEntry): EntityId | null { const battle = this._battle; if (!battle) return null; let bestId: EntityId | null = null; let bestDist = Number.POSITIVE_INFINITY; session.world.forEachAlive((entityId) => { if (!this._isPushCandidate(battle, entityId)) return; const meta = session.world.getComponent(entityId, UnitMetaComp); if ((meta?.spawnPoint ?? 0) !== trap.port) return; const tf = session.world.getComponent(entityId, TransformComp); if (!tf) return; if (!this._isCellOnPath(trap.routePath, tf.row, tf.col)) return; const dist = this._distAlongPathToTrap(trap.routePath, trap.trapPathIndex, tf.row, tf.col); if (dist !== TRAP_TRIGGER_BEFORE_CELLS) return; if (dist < bestDist) { bestDist = dist; bestId = entityId; } }); return bestId; } /** 触发门神:自陷阱格沿进路前进,碰到怪才推退;失败时不标记 triggered */ private _triggerMenShenSweep(trap: MenShenTrapEntry): boolean { const session = this._battle?.session; if (!session) return false; if (!this._retreatPathfinderBuilt) { session.pathfinder.rebuild(session.topology); this._retreatPathfinderBuilt = true; } const pushCells = getShieldPushCells(); if (!(pushCells > 0)) { console.log('[MenShenTrap] Const 推怪格数无效'); return false; } trap.pushCellsConfig = pushCells; trap.riderIds = createMembershipSet(); return this._beginMenShenSweep(trap, pushCells); } /** 门神沿进路向出怪口扫掠(Spine 击退为原地步态,须同步位移节点) */ private _beginMenShenSweep(trap: MenShenTrapEntry, pushCells: number): boolean { const map = this._map; const session = this._battle?.session; const node = trap.node; if (!map || !session || !node?.isValid) return false; trap.moveCells = this._buildMenShenSweepPath(trap, pushCells); trap.moveSegIndex = 0; trap.sweeping = trap.moveCells.length >= 2; const homeLogical = session.grid.cellToWorld({ row: trap.row, col: trap.col }); trap.sweepLogicalX = homeLogical.x; trap.sweepLogicalY = homeLogical.y; if (node.parent !== map.node) { node.parent = map.node; } this._syncDoorNodeFromLogical(trap, session, map); node.getWorldPosition(trap.homeWorldPos); if (!trap.sweeping) { console.log('[MenShenTrap] 扫掠路径过短', trap.moveCells.length, trap.trapPathIndex); this._finishMenShenSweep(trap); return false; } const dir = this._resolveMoveDirBetween(trap.moveCells[0], trap.moveCells[1]); this._playMenShenKnockDir(trap, dir); this._tryAttachRidersOnContact(trap); return true; } /** 构建门神扫掠格序列:陷阱格沿进路向出怪口反推 pushCells 格 */ private _buildMenShenSweepPath(trap: MenShenTrapEntry, pushCells: number): Cell[] { const out: Cell[] = []; const start = trap.trapPathIndex; const end = Math.max(0, start - Math.max(1, Math.floor(Number(pushCells) || 0))); for (let i = start; i >= end; i--) { out.push({ row: trap.routePath[i].row, col: trap.routePath[i].col }); } return out; } /** 帧更新:逻辑坐标驱动门神与怪同速后移 → 扫掠 Const 格数后消失 */ private _tickMenShenMove(trap: MenShenTrapEntry, dt: number): void { const map = this._map; const session = this._battle?.session; if (!trap.node?.isValid || !map || !session) return; if (!trap.sweeping || trap.moveCells.length < 2) return; const logicalSpeed = Math.max(1, session.grid.cellSize) * MENSHEN_SWEEP_CELLS_PER_SEC * Math.max(0, dt); if (logicalSpeed <= 0) return; const prevSeg = trap.moveSegIndex; const logicalStep = this._advanceDoorAlongSweep(trap, session, logicalSpeed); this._syncDoorNodeFromLogical(trap, session, map); this._tryAttachRidersOnContact(trap); if (logicalStep > 0) this._advanceRidersWithShield(trap, logicalStep); if (trap.moveSegIndex >= trap.moveCells.length - 1) { this._finishMenShenSweep(trap); return; } if (trap.moveSegIndex > prevSeg) { const dir = this._resolveMoveDirBetween( trap.moveCells[trap.moveSegIndex], trap.moveCells[trap.moveSegIndex + 1], ); this._playMenShenKnockDir(trap, dir); } } /** 门神沿扫掠路径推进 stepPx(逻辑坐标),返回本帧实际位移 */ private _advanceDoorAlongSweep(trap: MenShenTrapEntry, session: BattleSession, stepPx: number): number { if (!(stepPx > 0)) return 0; let remaining = stepPx; let moved = 0; while (remaining > 1e-4 && trap.moveSegIndex + 1 < trap.moveCells.length) { const toCell = trap.moveCells[trap.moveSegIndex + 1]; const targetWorld = session.grid.cellToWorld(toCell); const dx = targetWorld.x - trap.sweepLogicalX; const dy = targetWorld.y - trap.sweepLogicalY; const distSq = dx * dx + dy * dy; if (distSq <= remaining * remaining || distSq < 1e-8) { const dist = Math.sqrt(distSq); trap.sweepLogicalX = targetWorld.x; trap.sweepLogicalY = targetWorld.y; trap.moveSegIndex++; moved += dist; remaining -= dist; continue; } const dist = Math.sqrt(distSq); trap.sweepLogicalX += (dx / dist) * remaining; trap.sweepLogicalY += (dy / dist) * remaining; moved += remaining; remaining = 0; } return moved; } /** 门神逻辑坐标 → Cocos 节点位置 */ private _syncDoorNodeFromLogical(trap: MenShenTrapEntry, session: BattleSession, map: MapManager): void { const node = trap.node; if (!node?.isValid) return; const anchor = trap.moveCells[trap.moveSegIndex]; if (!anchor) return; const cocos = logicalWorldToCocos( session, map, anchor.row, anchor.col, trap.sweepLogicalX, trap.sweepLogicalY, ); node.setWorldPosition(cocos); } /** 门神移动中碰到进路怪:停步并加入随门神后移列表 */ private _tryAttachRidersOnContact(trap: MenShenTrapEntry): void { const battle = this._battle; const session = battle?.session; const map = this._map; const node = trap.node; const riderIds = trap.riderIds; if (!battle || !session || !map || !node?.isValid || !riderIds) return; node.getWorldPosition(this._tmpWorldA); const catchSq = MENSHEN_CATCH_PX * MENSHEN_CATCH_PX; const minPathIdx = this._resolveSweepEndPathIndex(trap); const doorPathIdx = this._resolveDoorPathIndex(trap); session.world.forEachAlive((entityId) => { if (membershipHas(riderIds, entityId)) return; if (!this._isMenShenPathMonster(battle, session, entityId, trap)) return; const tf = session.world.getComponent(entityId, TransformComp); const move = session.world.getComponent(entityId, MovementComp); if (!tf || !move) return; const monsterIdx = this._resolveRoutePathIndex(trap.routePath, tf.row, tf.col); if (monsterIdx < minPathIdx || monsterIdx > trap.trapPathIndex) return; if (monsterIdx > doorPathIdx + 1 || monsterIdx < doorPathIdx - 1) return; const pathTouch = monsterIdx >= doorPathIdx - 1 && monsterIdx <= doorPathIdx; if (!pathTouch) { const monsterWorld = logicalWorldToCocos(session, map, tf.row, tf.col, move.worldX, move.worldY); const dx = monsterWorld.x - this._tmpWorldA.x; const dy = monsterWorld.y - this._tmpWorldA.y; if (dx * dx + dy * dy > catchSq) return; } if (this._attachMonsterToShield(session, entityId)) { membershipAdd(riderIds, entityId); } }); } /** 碰到门神:停寻路,保留当前坐标,沿当前格→前一格方向后移 */ private _attachMonsterToShield(session: BattleSession, entityId: EntityId): boolean { const world = session.world; if (!world.isAlive(entityId)) return false; const move = world.getComponent(entityId, MovementComp); if (!move) return false; if (move.spawnProtected) move.spawnProtected = false; move.presentLocked = false; move.forcePathSpeed = 0; move.forcePathHoldAtEnd = false; move.path = []; move.pathIndex = 0; move.reachedGoal = false; move.hasAttackGoal = false; move.bajiaoBlowRetreating = true; move.forcePresentSync = true; return true; } /** 被顶住的怪与门神同帧后移(沿进路格心,步长与门神本帧逻辑位移一致) */ private _advanceRidersWithShield(trap: MenShenTrapEntry, stepLogicalPx: number): void { if (!(stepLogicalPx > 0) || !trap.riderIds || trap.riderIds.size <= 0) return; const session = this._battle?.session; if (!session) return; const ids = membershipToArray(trap.riderIds); for (let i = 0; i < ids.length; i++) { this._advanceRiderAlongRoute(session, ids[i], trap, stepLogicalPx); } } /** * 沿进路后移 stepPx:在下一格侧则先回当前格格心,再走向前一格格心;不吸附、不平行漂移。 */ private _advanceRiderAlongRoute( session: BattleSession, entityId: EntityId, trap: MenShenTrapEntry, stepPx: number, ): void { const world = session.world; if (!world.isAlive(entityId)) return; const move = world.getComponent(entityId, MovementComp); const tf = world.getComponent(entityId, TransformComp); if (!move || !tf) return; const routePath = trap.routePath; const minPathIdx = this._resolveSweepEndPathIndex(trap); const maxPathIdx = trap.trapPathIndex; let pathIdx = this._resolveRoutePathIndex(routePath, tf.row, tf.col); if (pathIdx < 0 || pathIdx <= minPathIdx) return; let remaining = stepPx; while (remaining > 1e-4 && pathIdx > minPathIdx) { const target = this._resolveRiderRetreatTarget( session, routePath, pathIdx, maxPathIdx, move.worldX, move.worldY, ); const dx = target.x - move.worldX; const dy = target.y - move.worldY; const dist = Math.hypot(dx, dy); if (dist < 1e-4) { if (target.isPrevCell) { const prevCell = routePath[pathIdx - 1]; tf.row = prevCell.row; tf.col = prevCell.col; session.spatial.updateEntity(entityId, { row: tf.row, col: tf.col }); pathIdx--; } continue; } if (dist <= remaining) { move.worldX = target.x; move.worldY = target.y; remaining -= dist; if (target.isPrevCell) { const prevCell = routePath[pathIdx - 1]; tf.row = prevCell.row; tf.col = prevCell.col; session.spatial.updateEntity(entityId, { row: tf.row, col: tf.col }); pathIdx--; } continue; } move.worldX += (dx / dist) * remaining; move.worldY += (dy / dist) * remaining; remaining = 0; } move.presentLocked = false; move.forcePresentSync = true; } /** 本帧后移目标:在进路下一格侧则先回当前格格心,否则走向前一格格心 */ private _resolveRiderRetreatTarget( session: BattleSession, routePath: readonly Cell[], pathIdx: number, maxPathIdx: number, worldX: number, worldY: number, ): { x: number; y: number; isPrevCell: boolean } { const currCenter = session.grid.cellToWorld(routePath[pathIdx]); if (this._isRiderAheadOnRoute(session, routePath, pathIdx, maxPathIdx, worldX, worldY)) { return { x: currCenter.x, y: currCenter.y, isPrevCell: false }; } const prevCenter = session.grid.cellToWorld(routePath[pathIdx - 1]); return { x: prevCenter.x, y: prevCenter.y, isPrevCell: true }; } /** 逻辑坐标是否在当前格沿进路朝陷阱方向一侧(两格之间) */ private _isRiderAheadOnRoute( session: BattleSession, routePath: readonly Cell[], pathIdx: number, maxPathIdx: number, worldX: number, worldY: number, ): boolean { if (pathIdx >= maxPathIdx) return false; const currCenter = session.grid.cellToWorld(routePath[pathIdx]); const nextCenter = session.grid.cellToWorld(routePath[pathIdx + 1]); const fx = nextCenter.x - currCenter.x; const fy = nextCenter.y - currCenter.y; const lenSq = fx * fx + fy * fy; if (lenSq < 1e-8) return false; const px = worldX - currCenter.x; const py = worldY - currCenter.y; return (px * fx + py * fy) / lenSq > 1e-4; } /** 门神消失:被顶怪落格心并恢复寻路 */ private _releaseRiders(trap: MenShenTrapEntry): void { const session = this._battle?.session; const riderIds = trap.riderIds; if (!session || !riderIds || riderIds.size <= 0) return; const ids = membershipToArray(riderIds); for (let i = 0; i < ids.length; i++) { const entityId = ids[i]; if (!session.world.isAlive(entityId)) continue; const tf = session.world.getComponent(entityId, TransformComp); const move = session.world.getComponent(entityId, MovementComp); if (!tf || !move) continue; const center = session.grid.cellToWorld({ row: tf.row, col: tf.col }); move.worldX = center.x; move.worldY = center.y; move.bajiaoBlowRetreating = false; move.presentLocked = false; move.forcePathSpeed = 0; move.forcePresentSync = true; move.reachedGoal = false; session.movement.assignPath(entityId); } } /** 门神当前在进路中的下标(随 moveSegIndex 后移) */ private _resolveDoorPathIndex(trap: MenShenTrapEntry): number { return trap.trapPathIndex - trap.moveSegIndex; } /** 门神扫掠终点在进路中的下标 */ private _resolveSweepEndPathIndex(trap: MenShenTrapEntry): number { if (trap.moveCells.length <= 0) return trap.trapPathIndex; return trap.trapPathIndex - (trap.moveCells.length - 1); } /** 扫掠进路上的可推怪(同出怪口、在 routePath 上) */ private _isMenShenPathMonster( battle: ProjectBattleHandle, session: BattleSession, entityId: EntityId, trap: MenShenTrapEntry, ): boolean { if (!this._isPushCandidate(battle, entityId)) return false; const meta = session.world.getComponent(entityId, UnitMetaComp); if ((meta?.spawnPoint ?? 0) !== trap.port) return false; const tf = session.world.getComponent(entityId, TransformComp); if (!tf) return false; return this._isCellOnPath(trap.routePath, tf.row, tf.col); } /** 格在进路中的下标;-1 表示不在路径上 */ private _resolveRoutePathIndex(path: readonly Cell[], row: number, col: number): number { for (let i = 0; i < path.length; i++) { if (path[i].row === row && path[i].col === col) return i; } return -1; } /** 扫掠结束:回收门神节点并从陷阱列表移除 */ private _finishMenShenSweep(trap: MenShenTrapEntry): void { trap.sweeping = false; trap.knockActive = false; trap.currentKnockDir = null; this._releaseRiders(trap); trap.riderIds = null; this._releaseTrapNode(trap.node); trap.node = null; trap.skeleton = null; const idx = this._traps.indexOf(trap); if (idx >= 0) this._traps.splice(idx, 1); } /** 播放指定方向击退并记录朝向 */ private _playMenShenKnockDir(trap: MenShenTrapEntry, dir: MenShenDir): void { const skel = trap.skeleton; if (!skel?.isValid) return; if (trap.knockActive && trap.currentKnockDir === dir) return; trap.knockActive = true; trap.currentKnockDir = dir; BattleSpinePlayback.play(skel, KNOCK_ANIM[dir], { loop: true }); } /** 实体沿进路至攻击终点的剩余格数 */ private _getRemainingCellsToAttackTarget(session: BattleSession, entityId: EntityId): number { const world = session.world; const move = world.getComponent(entityId, MovementComp); const tf = world.getComponent(entityId, TransformComp); if (!move || !tf) return Number.POSITIVE_INFINITY; if (move.reachedGoal) return 0; let best = Number.POSITIVE_INFINITY; const pathSteps = move.path.length - move.pathIndex; if (pathSteps > 0) best = pathSteps; const campAreaId = this._resolveEntityCampAreaId(session, entityId); const cost = session.pathfinder.getPathCost({ row: tf.row, col: tf.col }, campAreaId); if (Number.isFinite(cost) && cost >= 0) { const costCells = Math.max(0, Math.ceil(cost / PATH_ORTH_STEP)); if (costCells < best) best = costCells; } if (Number.isFinite(best)) return best; const forward = this._buildPathToAttackTarget(session, entityId); return forward.length <= 0 ? 0 : forward.length; } /** 从实体当前格起沿进路至攻击目标的路径格(不含起点) */ private _buildPathToAttackTarget(session: BattleSession, entityId: EntityId): Cell[] { const world = session.world; const tf = world.getComponent(entityId, TransformComp); if (!tf) return []; const move = world.getComponent(entityId, MovementComp); const out: Cell[] = []; if (move && move.pathIndex < move.path.length) { for (let i = move.pathIndex; i < move.path.length; i++) { const c = move.path[i]; const last = out.length > 0 ? out[out.length - 1] : tf; if (last.row === c.row && last.col === c.col) continue; out.push({ row: c.row, col: c.col }); } } if (out.length > 0) return out; const campAreaId = this._resolveEntityCampAreaId(session, entityId); const from = { row: tf.row, col: tf.col }; const target = session.pathfinder.shortestTargetCell(from, campAreaId); if (!target) return []; const fullPath = session.pathfinder.buildPathToTarget(from, target, campAreaId); if (fullPath.length <= 1) return []; for (let i = 1; i < fullPath.length; i++) { const c = fullPath[i]; const last = out.length > 0 ? out[out.length - 1] : from; if (last.row === c.row && last.col === c.col) continue; out.push({ row: c.row, col: c.col }); } return out; } /** 加载陷阱并按上一格相对陷阱的位置播待机 */ private _spawnTrapNode(parent: MapGrid, prev: Cell, curr: Cell): { node: Node; skeleton: sp.Skeleton; idleDir: MenShenDir } | null { if (!parent?.node?.isValid) return null; let node: Node | null = null; try { node = gg.res.getNode(BATTLE_HERO_BUFF_PATH, MENSHEN_PREFAB_NAME, GBundle.BattleHero); } catch (e) { console.log('[MenShenTrap] 门神预制体加载失败', String(e)); return null; } if (!node?.isValid) { console.log('[MenShenTrap] 门神预制体缺失', MENSHEN_PREFAB_NAME); return null; } node.parent = parent.node; node.setPosition(0, 0, 0); node.active = true; const skel = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton); if (!skel?.isValid) { console.log('[MenShenTrap] 门神 Spine 缺失'); gg.res.putNode(node); return null; } const idleDir = this._resolveIdleDirFromPrev(prev, curr); skel.setAnimationCacheMode(sp.AnimationCacheMode.REALTIME); BattleSpinePlayback.play(skel, IDLE_ANIM[idleDir], { loop: true }); return { node, skeleton: skel, idleDir }; } private _releaseTrapNode(node: Node | null): void { if (!node?.isValid) return; const skel = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton); if (skel?.isValid) BattleSpinePlayback.clearPendingComplete(skel); gg.res.putNode(node); } private _hasTrapAt(gridIndex: number): boolean { const idx = Number(gridIndex) || 0; for (let i = 0; i < this._traps.length; i++) { if (this._traps[i].gridIndex === idx) return true; } return false; } /** 上一格相对陷阱格的位置 → 待机动画(陷阱朝向进路来向) */ private _resolveIdleDirFromPrev(prev: Cell, curr: Cell): MenShenDir { return this._resolveMoveDirBetween(curr, prev); } /** 从 from 格走向 to 格的四向(col 增=右,row 增=下) */ private _resolveMoveDirBetween(from: Cell, to: Cell): MenShenDir { const dr = to.row - from.row; const dc = to.col - from.col; if (Math.abs(dc) >= Math.abs(dr)) { return dc > 0 ? 'right' : 'left'; } return dr > 0 ? 'down' : 'up'; } private _buildAdvancePath( session: BattleSession, spawn: Cell, trap: Cell, campAreaId: number, ): Cell[] { let path = session.pathfinder.buildPathToTarget(spawn, trap, campAreaId); if (path.length < 2) { path = session.pathfinder.findPath(spawn, trap, true, campAreaId); } return path; } private _distAlongPathToTrap( path: readonly Cell[], trapPathIndex: number, row: number, col: number, ): number { let idx = -1; for (let i = 0; i < path.length; i++) { if (path[i].row === row && path[i].col === col) { idx = i; break; } } if (idx < 0) return Number.POSITIVE_INFINITY; return Math.abs(trapPathIndex - idx); } private _isCellOnPath(path: readonly Cell[], row: number, col: number): boolean { for (let i = 0; i < path.length; i++) { if (path[i].row === row && path[i].col === col) return true; } return false; } private _resolveEntityCampAreaId(session: BattleSession, entityId: EntityId): number | undefined { const meta = session.world.getComponent(entityId, UnitMetaComp); const port = meta?.spawnPoint ?? 0; if (port < 1) return undefined; return session.topology.getSpawnPortCampArea(port); } private _collectSpawnPorts(session: BattleSession, enemyCampAreaId: number): number[] { const ports = createMembershipSet(); for (const port of session.topology.spawnPorts.keys()) { if (port <= 0) continue; if (session.topology.getSpawnPortCampArea(port) !== enemyCampAreaId) continue; membershipAdd(ports, port); } if (ports.size === 0) membershipAdd(ports, 1); return membershipToArray(ports).sort((a, b) => a - b); } private _resolveSpawnGridByPort(map: MapManager, 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; } private _isPushCandidate(battle: ProjectBattleHandle, entityId: EntityId): boolean { const session = battle.session; const world = session.world; if (!world.isAlive(entityId)) return false; if (isMechanismUnit(session, entityId)) return false; const team = world.getComponent(entityId, TeamComp); if (!team || !BattleTeamId.isEnemyTeam(team.team)) return false; if (Math.max(0, Math.floor(Number(team.ownerId) || 0)) !== this._defenderPlayerId) return false; const row = battle.resolveMonsterRow(entityId); if (!row) return false; const type = Math.floor(Number(row.type)); if (type !== MonsterType.SmallMonster && type !== MonsterType.Boss && type !== MonsterType.SummonMonster) { return false; } const move = world.getComponent(entityId, MovementComp); if (move?.spawnProtected || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) { return false; } return true; } }