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.
240 lines
8.2 KiB
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;
|
||
|
|
}
|
||
|
|
}
|