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.
AdLike2048/assets/script/battle/view/PathFlowLinePresenter.ts

240 lines
8.2 KiB

3 weeks ago
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;
}
}