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

193 lines
6.2 KiB

import { instantiate, Node, Tween, tween, UIOpacity, UITransform, v3, Vec3 } from 'cc';
import { GBundle } from '../../game/ConfigRes';
/** 沿弧线飞移速度(px/s) */
const MOVE_SPEED_PX = 2000;
/** 最短飞行时长(秒) */
const MIN_TOTAL_SEC = 0.14;
/** 最长飞行时长(秒) */
const MAX_TOTAL_SEC = 0.34;
/** 弧顶抬高(dragLayer 本地像素) */
const LIFT_HEIGHT = 42;
/** 影子透明度(与 skill 拖拽一致) */
const ICON_GHOST_OPACITY = 200;
/** 落地后淡出时长(秒) */
const FADE_OUT_SEC = 0.12;
/** 被动 icon 弧线飞行选项 */
export interface PassiveIconArcFlightOptions {
/** 起点世界坐标;缺省为 iconNode 当前 worldPosition */
startWorld?: Readonly<Vec3> | null;
/** 飞行时显示 textGrid trail1 拖尾 */
showTrail?: boolean;
}
/**
* 被动 skill icon 影子沿二次贝塞尔弧线飞行(对齐 BagToMapPlaceAnimator 轨迹)。
*/
export class PassiveIconArcFlight {
/**
* 复制 icon 为影子并飞向目标世界坐标,落地淡出后回调。
* @param iconNode 被动槽 icon
* @param targetWorld 落点世界坐标
* @param flyLayer 飞行层(ui_fly)
* @param onComplete 动画结束
* @param opts 起点 / 拖尾
*/
static play(
iconNode: Node,
targetWorld: Readonly<Vec3>,
flyLayer: Node,
onComplete?: () => void,
opts?: PassiveIconArcFlightOptions,
): Node | null {
const finish = (): void => {
onComplete?.();
};
if (!iconNode?.isValid || !flyLayer?.isValid) {
finish();
return null;
}
if (!flyLayer.active) {
flyLayer.active = true;
}
const host = new Node('passiveIconArcFlightHost');
host.parent = flyLayer;
host.setSiblingIndex(flyLayer.children.length - 1);
const ghost = instantiate(iconNode);
ghost.name = 'passiveIconArcGhost';
ghost.parent = host;
ghost.setPosition(0, 0, 0);
ghost.setWorldScale(iconNode.worldScale);
const ghostOpacity = ghost.getComponent(UIOpacity) ?? ghost.addComponent(UIOpacity);
ghostOpacity.opacity = ICON_GHOST_OPACITY;
let trailNode: Node | null = null;
if (opts?.showTrail) {
trailNode = this._borrowTrail1Node();
if (trailNode?.isValid) {
trailNode.parent = host;
trailNode.setPosition(0, 0, 0);
trailNode.active = true;
}
}
const startWorld = opts?.startWorld
? new Vec3(opts.startWorld.x, opts.startWorld.y, opts.startWorld.z)
: iconNode.worldPosition.clone();
host.setWorldPosition(startWorld);
const ut = flyLayer.getComponent(UITransform);
if (!ut) {
this._destroyHost(host);
finish();
return null;
}
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,
);
host.parent = flyLayer;
host.setPosition(startLocal);
const arcLen = Vec3.distance(startLocal, control) + Vec3.distance(control, endLocal);
const totalSec = Math.min(
MAX_TOTAL_SEC,
Math.max(MIN_TOTAL_SEC, arcLen / MOVE_SPEED_PX),
);
const proxy = { t: 0 };
tween(proxy)
.to(totalSec, { t: 1 }, {
easing: 'sineInOut',
onUpdate: () => {
if (!host.isValid) return;
const t = proxy.t;
const u = 1 - t;
host.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 (!host.isValid) {
finish();
return;
}
host.setPosition(endLocal);
this._fadeOutHost(host, ghostOpacity, trailNode, finish);
})
.start();
return host;
}
/** 中断并销毁飞行 host */
static abort(host: Node | null): void {
if (!host?.isValid) return;
Tween.stopAllByTarget(host);
this._destroyHost(host);
}
/** 从对象池取 trail1 */
private static _borrowTrail1Node(): Node | null {
let node: Node | null = null;
try {
node = gg.res.getNode('prefab/', 'trail1', GBundle.BattleGame);
} catch (e) {
console.log('[PassiveIconArcFlight] trail1 加载失败', String(e));
return null;
}
if (!node?.isValid) {
console.log('[PassiveIconArcFlight] trail1 缺失');
return null;
}
return node;
}
/** 落地淡出后销毁 */
private static _fadeOutHost(
host: Node,
ghostOpacity: UIOpacity,
trailNode: Node | null,
onDone: () => void,
): void {
if (!host.isValid) {
onDone();
return;
}
Tween.stopAllByTarget(ghostOpacity);
tween(ghostOpacity)
.to(FADE_OUT_SEC, { opacity: 0 })
.call(() => {
if (trailNode?.isValid) trailNode.active = false;
this._destroyHost(host);
onDone();
})
.start();
}
/** 销毁 host 及子节点 */
private static _destroyHost(host: Node): void {
if (!host?.isValid) return;
Tween.stopAllByTarget(host);
for (let i = host.children.length - 1; i >= 0; i--) {
const child = host.children[i];
if (!child?.isValid) continue;
const opacity = child.getComponent(UIOpacity);
if (opacity?.isValid) Tween.stopAllByTarget(opacity);
if (child['resKey']) gg.res.putNode(child);
else child.destroy();
}
host.destroy();
}
}