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.
204 lines
7.5 KiB
204 lines
7.5 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;
|
|
/** 起飞后清空原槽/格残留 */
|
|
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,
|
|
startWorld: primaryLeg.startWorld,
|
|
showTrail: DeviceProfile.enableBoardPlaceTrail,
|
|
});
|
|
hooks.purgeVacatedCell(primaryLeg.vacateFrom);
|
|
|
|
if (swapLeg) {
|
|
BagToMapPlaceAnimator.play(swapLeg.grid, swapLeg.targetWorld, dragLayer, onAnimDone, {
|
|
attachParent: swapLeg.attachParent,
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|