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.
202 lines
6.5 KiB
202 lines
6.5 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);
|
|
}
|
|
|
|
/** 从 textGrid 借 trail1 节点(instantiate 副本) */
|
|
private static _borrowTrail1Node(): Node | null {
|
|
let gridNode: Node | null = null;
|
|
try {
|
|
gridNode = gg.res.getNode('prefab/', 'textGrid', GBundle.BattleGame);
|
|
} catch (e) {
|
|
console.log('[PassiveIconArcFlight] textGrid 加载失败', String(e));
|
|
return null;
|
|
}
|
|
if (!gridNode?.isValid) {
|
|
console.log('[PassiveIconArcFlight] textGrid 缺失');
|
|
return null;
|
|
}
|
|
const trail = gridNode.getChildByName('trail1');
|
|
if (!trail?.isValid) {
|
|
gg.res.putNode(gridNode);
|
|
console.log('[PassiveIconArcFlight] trail1 缺失');
|
|
return null;
|
|
}
|
|
const trailClone = instantiate(trail);
|
|
gg.res.putNode(gridNode);
|
|
return trailClone;
|
|
}
|
|
|
|
/** 落地淡出后销毁 */
|
|
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);
|
|
child.destroy();
|
|
}
|
|
host.destroy();
|
|
}
|
|
}
|
|
|