import { Node, Tween, tween, UITransform, v3, Vec3 } from 'cc'; import type { TextGrid } from './TextGrid'; /** 沿弧线飞移速度(px/s) */ const MOVE_SPEED_PX = 2000; const MIN_TOTAL_SEC = 0.14; const MAX_TOTAL_SEC = 0.34; /** 弧顶抬高(世界像素量级,换算到 dragLayer 本地) */ const LIFT_HEIGHT = 42; export interface BagToMapPlaceOptions { /** 动画结束后挂接的槽位/地图格节点(避免 rebuild 前停在 dragLayer 再瞬移) */ attachParent?: Node | null; /** 飞行时显示字块拖尾(与背包刷新入场同一套 trail1/2/3) */ showTrail?: boolean; /** 飞行起点世界坐标;缺省为 node 当前 worldPosition(拉线拖动用 ghost/跟指位置) */ startWorld?: Readonly | null; /** 沿弧线飞移速度(px/s);缺省 MOVE_SPEED_PX */ moveSpeedPx?: number; /** 最短飞行时长(秒);缺省 MIN_TOTAL_SEC */ minTotalSec?: number; /** 最长飞行时长(秒);缺省 MAX_TOTAL_SEC */ maxTotalSec?: number; } /** 字块松手后飞入目标格(背包↔地图 / 交换 / 落空格同一套) */ export class BagToMapPlaceAnimator { static play( grid: TextGrid, targetWorld: Readonly, dragLayer: Node, onComplete?: () => void, opts?: BagToMapPlaceOptions, ): void { const node = grid.node; const finish = (): void => { grid.hideEntranceTrail(); onComplete?.(); }; if (!node?.isValid || !dragLayer?.isValid) { finish(); return; } // prefab 中 ui_fly 默认可关;挂到未激活层会导致飞行全程不可见、落地才“瞬现” if (!dragLayer.active) { dragLayer.active = true; } Tween.stopAllByTarget(node); grid.hideEntranceTrail(); const startWorld = opts?.startWorld ? new Vec3(opts.startWorld.x, opts.startWorld.y, opts.startWorld.z) : node.worldPosition.clone(); node.parent = dragLayer; node.setWorldPosition(startWorld); const ut = dragLayer.getComponent(UITransform); if (!ut) { finish(); return; } const startLocal = ut.convertToNodeSpaceAR(startWorld); const endLocal = ut.convertToNodeSpaceAR(new Vec3(targetWorld.x, targetWorld.y, targetWorld.z)); const control = v3( (startLocal.x + endLocal.x) * 0.5, Math.max(startLocal.y, endLocal.y) + LIFT_HEIGHT, startLocal.z, ); const moveSpeedPx = opts?.moveSpeedPx ?? MOVE_SPEED_PX; const minTotalSec = opts?.minTotalSec ?? MIN_TOTAL_SEC; const maxTotalSec = opts?.maxTotalSec ?? MAX_TOTAL_SEC; const arcLen = Vec3.distance(startLocal, control) + Vec3.distance(control, endLocal); const totalSec = Math.min( maxTotalSec, Math.max(minTotalSec, arcLen / moveSpeedPx), ); node.setPosition(startLocal); node.setScale(1, 1, 1); if (opts?.showTrail) { grid.showEntranceTrail(); } const proxy = { t: 0 }; tween(proxy) .to(totalSec, { t: 1 }, { easing: 'sineInOut', onUpdate: () => { if (!node.isValid) return; const t = proxy.t; const u = 1 - t; node.setPosition( u * u * startLocal.x + 2 * u * t * control.x + t * t * endLocal.x, u * u * startLocal.y + 2 * u * t * control.y + t * t * endLocal.y, startLocal.z, ); }, }) .call(() => { if (!node.isValid) { finish(); return; } node.setPosition(endLocal); this._attachToParent(node, opts?.attachParent); finish(); }) .start(); } static stop(grid: TextGrid | null): void { if (!grid?.node?.isValid) return; grid.hideEntranceTrail(); grid.resetNodeTransform(); } /** 保持世界坐标挂到目标父节点,并归零本地坐标(与槽位/格心对齐) */ private static _attachToParent(node: Node, attachParent: Node | null | undefined): void { if (!attachParent?.isValid) return; const wp = node.worldPosition.clone(); node.parent = attachParent; node.setWorldPosition(wp); node.setPosition(0, 0, 0); } }