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

137 lines
4.8 KiB

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