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/BoardPlaceFlightExecutor.ts

208 lines
7.7 KiB

import { Vec3, type Node } from 'cc';
import type { DragSource, DropTarget, PlaceRejectReason } from 'db://assets/mx/module/merge';
import { BagToMapPlaceAnimator } from './BagToMapPlaceAnimator';
import { ClientErrorReporter } from '../../../mx/tools/ClientErrorReporter';
import { DeviceProfile } from './DeviceProfile';
import type { GridDetachSource } from './BoardGridRegistry';
import type { TextGrid } from './TextGrid';
/** 单次飞行落点 */
export interface BoardPlaceFlightLeg {
/** 飞行的字块 */
grid: TextGrid;
/** 落点世界坐标 */
targetWorld: Readonly<Vec3>;
/** 动画结束后挂接父节点;合成时不挂(格上已有真身) */
attachParent?: Node | null;
/** 落地世界坐标(地图字挂 ui_textGrids 时用) */
restWorld?: Readonly<Vec3> | null;
/** 起飞后清空原槽/格残留 */
vacateFrom: GridDetachSource;
/** 起点世界坐标 */
startWorld?: Readonly<Vec3> | null;
}
/** 落子成功后的 rebuild 策略 */
export type BoardPlaceSuccessRebuild = 'flush' | 'requestSession' | 'none';
/** 落子飞行动画执行回调 */
export interface BoardPlaceFlightHooks {
/** drop 前:禁止 MergeEvent 触发全量 rebuild */
beginDropViewSync(): void;
/** drop 失败:复位同步阶段 */
cancelDropViewSync(): void;
/** 动画批次开始 */
onInflightStart(): void;
/** 世代守卫 */
finishPlaceFlightGuard(flightGen: number): boolean;
/** 逻辑落子 */
drop(source: DragSource, target: DropTarget): PlaceRejectReason | null;
/** 视图提交 */
commitViews(
grid: TextGrid,
target: DropTarget,
swap?: { grid: TextGrid; target: DropTarget } | null,
): void;
/** drop 前额外校验;返回非空则失败 */
validateBeforeDrop?(): PlaceRejectReason | 'rebuild' | null;
/** 落子被拒时的提示 */
onDropRejected?(reason: PlaceRejectReason): void;
/** 失败时全量重建 */
rebuildOnFailure(): void;
/** 冲刷暂缓 rebuild */
flushPendingRebuilds(): void;
/** 请求全量 session rebuild */
requestSessionRebuild?(): void;
/** 回收孤儿 */
disposeOrphan(grid: TextGrid): void;
/** 成功后的 rebuild 策略 */
onSuccessRebuild: BoardPlaceSuccessRebuild;
/** 落子成功且 rebuild 冲刷后(背包→地图等待绑定攻速符等) */
afterDropSuccess?(source: DragSource, target: DropTarget): void;
/** 外部完成回调 */
onComplete?(ok: boolean): void;
/** 动画前清理(慢动作/幽灵等) */
beforeAnim?(): void;
/** 每个 leg 动画前 */
onLegPrepare?(grid: TextGrid): void;
/** 清空原槽/格残留节点 */
purgeVacatedCell(from: GridDetachSource): void;
}
/** 落子飞行动画请求 */
export interface BoardPlaceFlightRequest {
dragLayer: Node;
flightGen: number;
dropSource: DragSource;
dropTarget: DropTarget;
primaryLeg: BoardPlaceFlightLeg;
swapLeg?: BoardPlaceFlightLeg | null;
swapCommit?: { grid: TextGrid; target: DropTarget } | null;
hooks: BoardPlaceFlightHooks;
}
/** 飞行动画最长等待(毫秒);tween 丢失回调时强制落地,避免 rebuild/操作永久锁死 */
const PLACE_FLIGHT_SAFETY_MS = 800;
/**
* 统一背包↔地图 / 交换 / 落子飞行动画管线:
* animate → drop → commit → rebuild。
*/
export class BoardPlaceFlightExecutor {
/** 播放飞行动画并在结束后落子 */
static execute(req: BoardPlaceFlightRequest): void {
const {
dragLayer,
primaryLeg,
swapLeg,
hooks,
} = req;
hooks.onInflightStart();
hooks.beforeAnim?.();
hooks.onLegPrepare?.(primaryLeg.grid);
if (swapLeg) hooks.onLegPrepare?.(swapLeg.grid);
let pending = swapLeg ? 2 : 1;
let settled = false;
const failState = { failed: false };
let safetyTimer = 0;
const onAnimDone = (): void => {
if (settled) return;
pending--;
if (pending > 0) return;
settled = true;
clearTimeout(safetyTimer);
BoardPlaceFlightExecutor._completeFlight(req, failState);
};
safetyTimer = setTimeout(() => {
if (settled) return;
console.warn('[BoardPlaceFlight] 飞行动画超时,强制落地');
pending = 0;
onAnimDone();
}, PLACE_FLIGHT_SAFETY_MS);
BagToMapPlaceAnimator.play(primaryLeg.grid, primaryLeg.targetWorld, dragLayer, onAnimDone, {
attachParent: primaryLeg.attachParent,
restWorld: primaryLeg.restWorld,
startWorld: primaryLeg.startWorld,
showTrail: DeviceProfile.enableBoardPlaceTrail,
});
hooks.purgeVacatedCell(primaryLeg.vacateFrom);
if (swapLeg) {
BagToMapPlaceAnimator.play(swapLeg.grid, swapLeg.targetWorld, dragLayer, onAnimDone, {
attachParent: swapLeg.attachParent,
restWorld: swapLeg.restWorld,
startWorld: swapLeg.startWorld,
showTrail: DeviceProfile.enableBoardPlaceTrail,
});
hooks.purgeVacatedCell(swapLeg.vacateFrom);
}
}
/**
* 动画结束:drop / commit / rebuild。
* @param req 飞行请求
* @param failState 落子失败标记
*/
private static _completeFlight(req: BoardPlaceFlightRequest, failState: { failed: boolean }): void {
const { flightGen, dropSource, dropTarget, primaryLeg, swapLeg, swapCommit, hooks } = req;
if (!hooks.finishPlaceFlightGuard(flightGen)) {
hooks.disposeOrphan(primaryLeg.grid);
if (swapLeg) hooks.disposeOrphan(swapLeg.grid);
primaryLeg.grid.finishPlaceFlightVisual();
swapLeg?.grid.finishPlaceFlightVisual();
hooks.onComplete?.(false);
return;
}
try {
const pre = hooks.validateBeforeDrop?.();
if (pre === 'rebuild') {
failState.failed = true;
primaryLeg.grid.restoreOrigin();
hooks.cancelDropViewSync();
} else if (pre) {
failState.failed = true;
primaryLeg.grid.restoreOrigin();
hooks.onDropRejected?.(pre);
hooks.cancelDropViewSync();
} else {
hooks.beginDropViewSync();
const reason = hooks.drop(dropSource, dropTarget);
if (reason) {
failState.failed = true;
primaryLeg.grid.restoreOrigin();
hooks.onDropRejected?.(reason);
hooks.cancelDropViewSync();
} else {
hooks.commitViews(primaryLeg.grid, dropTarget, swapCommit ?? undefined);
}
}
} catch (e) {
failState.failed = true;
ClientErrorReporter.report('BoardPlaceFlight_dropCommit', e);
hooks.cancelDropViewSync();
} finally {
if (failState.failed) {
hooks.rebuildOnFailure();
} else if (hooks.onSuccessRebuild === 'requestSession') {
hooks.requestSessionRebuild?.();
hooks.flushPendingRebuilds();
} else if (hooks.onSuccessRebuild === 'flush') {
hooks.flushPendingRebuilds();
}
if (!failState.failed) {
hooks.afterDropSuccess?.(dropSource, dropTarget);
}
primaryLeg.grid.finishPlaceFlightVisual();
swapLeg?.grid.finishPlaceFlightVisual();
hooks.disposeOrphan(primaryLeg.grid);
if (swapLeg) hooks.disposeOrphan(swapLeg.grid);
hooks.onComplete?.(!failState.failed);
}
}
}