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.
136 lines
4.8 KiB
136 lines
4.8 KiB
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;
|
|
/** 落地世界坐标;地图字挂专用层时必须给,否则会落到层原点 */
|
|
restWorld?: Readonly<Vec3> | null;
|
|
/** 飞行时显示字块拖尾(与背包刷新入场同一套 trail1/2/3) */
|
|
showTrail?: boolean;
|
|
/** 飞行起点世界坐标;缺省为 node 当前 worldPosition(拉线拖动用 ghost/跟指位置) */
|
|
startWorld?: Readonly<Vec3> | 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<Vec3>,
|
|
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, opts?.restWorld);
|
|
finish();
|
|
})
|
|
.start();
|
|
}
|
|
|
|
static stop(grid: TextGrid | null): void {
|
|
if (!grid?.node?.isValid) return;
|
|
grid.hideEntranceTrail();
|
|
grid.resetNodeTransform();
|
|
}
|
|
|
|
/**
|
|
* 挂到目标父节点。
|
|
* 有 restWorld:对齐该世界坐标(地图字专用层)。
|
|
* 无 restWorld:本地归零(背包槽中心)。
|
|
*/
|
|
private static _attachToParent(node: Node, attachParent: Node | null | undefined, restWorld?: Readonly<Vec3> | null): void {
|
|
if (!attachParent?.isValid) return;
|
|
node.parent = attachParent;
|
|
if (restWorld) {
|
|
node.setWorldPosition(restWorld.x, restWorld.y, restWorld.z);
|
|
return;
|
|
}
|
|
node.setPosition(0, 0, 0);
|
|
}
|
|
}
|
|
|