import { Node, UITransform, Vec2 } from 'cc'; import type { BattleSession, Cell } from 'db://assets/mx/module/battle'; import { ArrowFlowLine } from 'db://assets/mx/components/ArrowFlowLine'; import type { MapManager } from '../map/legacy/MapManager'; const LAYER_NAME = 'ui_pathFlowLine'; /** * 出怪口→受击点流动箭头路径。 * - 开局限时展示 * - 任意字块/格子拖动期间展示当前怪物路线 * - 仅 type3 拖动 additionally 支持悬停格预览落子后路线 */ export class PathFlowLinePresenter { private _layer: Node | null = null; private _flow: ArrowFlowLine | null = null; private _map: MapManager | null = null; private _session: BattleSession | null = null; /** 开局展示截止时间戳(ms);0 表示无开局展示 */ private _introUntilMs = 0; /** 拖动期间展示当前路线 */ private _dragHold = false; /** type3 拖动:悬停格可预览落子后路线 */ private _type3Preview = false; private _introToken = 0; /** 悬停预览:假设在该格落下 type3(不可走) */ private _previewBlockCell: Cell | null = null; private _lastPreviewKey = ''; bind(parent: Node | null): void { this.unbind(); if (!parent?.isValid) return; let layer = parent.getChildByName(LAYER_NAME); if (!layer) { layer = new Node(LAYER_NAME); layer.layer = parent.layer; layer.parent = parent; layer.setPosition(0, 0, 0); const ut = layer.addComponent(UITransform); const parentUt = parent.getComponent(UITransform); if (parentUt) { ut.setContentSize(parentUt.contentSize); } else { ut.setContentSize(2000, 2000); } ut.setAnchorPoint(0.5, 0.5); } this._layer = layer; this._flow = layer.getOrAddComponent(ArrowFlowLine); this._flow.enableFlow = true; this._flow.arrowWidth = 14; this._flow.arrowLength = 22; layer.active = false; } unbind(): void { this._introToken++; this._introUntilMs = 0; this._dragHold = false; this._type3Preview = false; this._previewBlockCell = null; this._lastPreviewKey = ''; this._map = null; this._session = null; if (this._flow?.isValid) { this._flow.unscheduleAllCallbacks(); this._flow.setPaths([]); } if (this._layer?.isValid) { this._layer.active = false; } this._layer = null; this._flow = null; } showIntro(map: MapManager, session: BattleSession, seconds: number): void { this._map = map; this._session = session; const sec = Math.max(0, seconds); this._introUntilMs = Date.now() + sec * 1000; const token = ++this._introToken; this._paint(); if (!this._flow?.isValid || sec <= 0) return; this._flow.unscheduleAllCallbacks(); this._flow.scheduleOnce(() => { if (token !== this._introToken) return; this._introUntilMs = 0; this._applyVisibility(); }, sec); } cancelIntro(): void { this._introToken++; this._introUntilMs = 0; if (this._flow?.isValid) this._flow.unscheduleAllCallbacks(); this._applyVisibility(); } /** 开始拖动:展示当前怪物路线;type3 时额外开启悬停预览 */ beginDrag(map: MapManager, session: BattleSession, type3Preview = false): void { this._map = map; this._session = session; this._dragHold = true; this._type3Preview = type3Preview; this._previewBlockCell = null; this._lastPreviewKey = ''; this._paint(); } /** * 仅 type3:悬停地图格变化时预览「若落在此格」后的路线。 * 仅格子变化时重算寻路,不在每帧 DRAG_MOVE 重复 rebuild。 */ previewType3AtCell(cell: Cell | null): void { if (!this._dragHold || !this._type3Preview) return; const key = cell ? `${cell.row},${cell.col}` : ''; if (key === this._lastPreviewKey) return; this._lastPreviewKey = key; this._previewBlockCell = cell; this._paint(); } endDrag(): void { this._dragHold = false; this._type3Preview = false; this._previewBlockCell = null; this._lastPreviewKey = ''; this._applyVisibility(); } refreshIfVisible(): void { if (!this._shouldShow()) return; this._paint(); } private _shouldShow(): boolean { return this._dragHold || Date.now() < this._introUntilMs; } private _applyVisibility(): void { if (this._shouldShow()) { this._paint(); return; } if (this._flow?.isValid) this._flow.setPaths([]); if (this._layer?.isValid) this._layer.active = false; } private _paint(): void { const map = this._map; const session = this._session; const layer = this._layer; const flow = this._flow; if (!map?.gridsInitialized || !session || !layer?.isValid || !flow?.isValid) return; this._withPreviewBlock(session, () => { const paths = this._buildLocalPaths(map, session, layer); flow.setPaths(paths); layer.active = paths.length > 0; }); } /** 预览期临时阻塞一格并重算寻路场,绘制后立即还原(不触发 MovementSystem) */ private _withPreviewBlock(session: BattleSession, draw: () => void): void { const cell = this._previewBlockCell; let savedWalkable: boolean | undefined; let applied = false; if (cell && session.grid.inBounds(cell) && session.grid.isWalkable(cell)) { savedWalkable = true; session.grid.setWalkable(cell, false); session.pathfinder.invalidate(); session.pathfinder.rebuild(session.topology); applied = true; } try { draw(); } finally { if (applied && cell && savedWalkable !== undefined) { session.grid.setWalkable(cell, true); session.pathfinder.invalidate(); session.pathfinder.rebuild(session.topology); } } } private _buildLocalPaths(map: MapManager, session: BattleSession, layer: Node): Vec2[][] { const layerUt = layer.getComponent(UITransform); if (!layerUt) return []; const spawnNodes = map.spawnPointParent?.children ?? []; const out: Vec2[][] = []; for (const spawn of spawnNodes) { if (!spawn?.isValid) continue; const startGrid = map.getMapGridByWorldPos(spawn.worldPosition); if (!startGrid?.isCanWalk) continue; const start: Cell = { row: startGrid.Row, col: startGrid.Col }; let end: Cell | null = null; try { end = session.pathfinder.shortestTargetCell(start); } catch { end = null; } if (!end) { const fallback = this._nearestAttackCell(map, start); if (!fallback) continue; end = fallback; } const cells = session.pathfinder.findPath(start, end, true); if (cells.length < 2) continue; const localPts: Vec2[] = []; for (const c of cells) { const g = map.getMapGrid(c.row, c.col); if (!g?.node?.isValid) continue; const local = layerUt.convertToNodeSpaceAR(g.node.worldPosition); localPts.push(new Vec2(local.x, local.y)); } if (localPts.length >= 2) out.push(localPts); } return out; } private _nearestAttackCell(map: MapManager, from: Cell): Cell | null { let best: Cell | null = null; let bestD = Number.MAX_VALUE; for (const [grid] of map.allAttackPointGrids) { if (!grid?.isCanWalk) continue; const d = Math.abs(grid.Row - from.row) + Math.abs(grid.Col - from.col); if (d < bestD) { bestD = d; best = { row: grid.Row, col: grid.Col }; } } return best; } }