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.
283 lines
9.0 KiB
283 lines
9.0 KiB
import {
|
|
isValid,
|
|
Node,
|
|
sp,
|
|
UITransform,
|
|
Vec3,
|
|
} from 'cc';
|
|
import { SkillTransformDriver } from '../skill/SkillTransformDriver';
|
|
import { BattleSpinePlayback } from '../presentation/BattleSpinePlayback';
|
|
|
|
const FLY_SPEED = 576;
|
|
const OWNED_RESTORE_KEY = '__axeOwnedRestore';
|
|
|
|
type OwnedAxeRestore = {
|
|
parent: Node;
|
|
localPosition: Vec3;
|
|
localScale: Vec3;
|
|
angle: number;
|
|
active: boolean;
|
|
};
|
|
|
|
/** 斧投掷:英雄预制体 attack 节点飞出 → 落地播停斧 → 归位(不再使用 battleGameMonster/prefab/grid 池化预制体) */
|
|
export class AxeFlyPresenter {
|
|
|
|
static play(opts: {
|
|
flyLayer: Node;
|
|
anchorNode: Node;
|
|
flyNode: Node;
|
|
targetWorld: Readonly<Vec3>;
|
|
piercePx?: number;
|
|
flyAnim?: string;
|
|
stopAnim?: string;
|
|
onFlyNodeReady?: (flyNode: Node) => void;
|
|
onReachTarget?: () => void;
|
|
onComplete?: () => void;
|
|
}): void {
|
|
const {
|
|
flyLayer,
|
|
anchorNode,
|
|
flyNode,
|
|
targetWorld,
|
|
piercePx = 0,
|
|
flyAnim,
|
|
stopAnim,
|
|
onFlyNodeReady,
|
|
onReachTarget,
|
|
onComplete,
|
|
} = opts;
|
|
|
|
if (!flyLayer?.isValid || !anchorNode?.isValid || !flyNode?.isValid) {
|
|
onComplete?.();
|
|
return;
|
|
}
|
|
|
|
flyLayer.active = true;
|
|
const flyUt = flyLayer.getComponent(UITransform);
|
|
if (!flyUt) {
|
|
console.log('[AxeFly] flyLayer missing UITransform');
|
|
onComplete?.();
|
|
return;
|
|
}
|
|
|
|
const startWorld = anchorNode.worldPosition.clone();
|
|
const targetWorldPos = new Vec3(targetWorld.x, targetWorld.y, targetWorld.z);
|
|
const startLocal = flyUt.convertToNodeSpaceAR(startWorld);
|
|
const targetLocal = flyUt.convertToNodeSpaceAR(targetWorldPos);
|
|
|
|
const dx = targetLocal.x - startLocal.x;
|
|
const dy = targetLocal.y - startLocal.y;
|
|
const len = Math.hypot(dx, dy) || 1;
|
|
const nx = dx / len;
|
|
const ny = dy / len;
|
|
|
|
const pierceEndLocal = new Vec3(
|
|
targetLocal.x + nx * piercePx,
|
|
targetLocal.y + ny * piercePx,
|
|
targetLocal.z,
|
|
);
|
|
|
|
this._attachOwnedFlyNode(flyNode, anchorNode, flyLayer, flyUt, startWorld, startLocal);
|
|
|
|
const visualScaleMag = Math.max(Math.abs(flyNode.worldScale.x), Math.abs(flyNode.worldScale.y), 0.01);
|
|
if (flyAnim) {
|
|
this._restartFlySpine(flyNode, flyAnim, 1, visualScaleMag);
|
|
}
|
|
|
|
onFlyNodeReady?.(flyNode);
|
|
|
|
const distToTarget = Vec3.distance(startWorld, targetWorldPos);
|
|
const durToTarget = Math.max(0.06, distToTarget / FLY_SPEED);
|
|
const durPierce = piercePx > 0 ? Math.max(0.04, piercePx / FLY_SPEED) : 0;
|
|
|
|
SkillTransformDriver.cancelNode(flyNode);
|
|
|
|
const finishFly = (): void => {
|
|
if (!flyNode?.isValid) {
|
|
this._maybeHideLayer(flyLayer);
|
|
onComplete?.();
|
|
return;
|
|
}
|
|
|
|
if (stopAnim) {
|
|
this._finishOwnedFly(flyNode, stopAnim, flyLayer, onComplete);
|
|
return;
|
|
}
|
|
|
|
this._stopFlySpine(flyNode);
|
|
flyNode.setScale(1, 1, 1);
|
|
flyNode.angle = 0;
|
|
this._restoreOwnedFlyNode(flyNode, flyLayer);
|
|
onComplete?.();
|
|
};
|
|
|
|
SkillTransformDriver.animate({
|
|
node: flyNode,
|
|
duration: durToTarget,
|
|
fromPos: startLocal,
|
|
toPos: targetLocal,
|
|
easing: SkillTransformDriver.easeSineInOut,
|
|
onComplete: () => {
|
|
onReachTarget?.();
|
|
if (durPierce > 0) {
|
|
SkillTransformDriver.animate({
|
|
node: flyNode,
|
|
duration: durPierce,
|
|
fromPos: targetLocal,
|
|
toPos: pierceEndLocal,
|
|
easing: SkillTransformDriver.easeLinear,
|
|
onComplete: finishFly,
|
|
});
|
|
} else {
|
|
finishFly();
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
/** 取消单颗飞斧(不触发 play 的 onComplete) */
|
|
static recycleFlyNode(node: Node | null, flyLayer?: Node | null): void {
|
|
if (!node?.isValid) return;
|
|
SkillTransformDriver.cancelNode(node);
|
|
this._stopFlySpine(node);
|
|
this._restoreOwnedFlyNode(node, flyLayer);
|
|
}
|
|
|
|
static stopOnLayer(flyLayer: Node | null): void {
|
|
if (!flyLayer?.isValid) return;
|
|
for (const child of [...flyLayer.children]) {
|
|
if (!isValid(child)) continue;
|
|
this.recycleFlyNode(child, flyLayer);
|
|
}
|
|
flyLayer.active = false;
|
|
}
|
|
|
|
private static _attachOwnedFlyNode(
|
|
node: Node,
|
|
anchorNode: Node,
|
|
flyLayer: Node,
|
|
flyUt: UITransform,
|
|
startWorld: Vec3,
|
|
startLocal: Vec3,
|
|
): void {
|
|
const restore: OwnedAxeRestore = {
|
|
parent: node.parent!,
|
|
localPosition: node.position.clone(),
|
|
localScale: node.scale.clone(),
|
|
angle: node.angle,
|
|
active: node.active,
|
|
};
|
|
(node as Node & { [OWNED_RESTORE_KEY]?: OwnedAxeRestore })[OWNED_RESTORE_KEY] = restore;
|
|
|
|
const worldRot = anchorNode.worldRotation.clone();
|
|
const worldScale = anchorNode.worldScale.clone();
|
|
node.active = true;
|
|
node.parent = flyLayer;
|
|
node.setPosition(startLocal);
|
|
node.setWorldRotation(worldRot);
|
|
node.setWorldScale(worldScale);
|
|
if (!node.worldPosition.equals(startWorld)) {
|
|
node.setWorldPosition(startWorld);
|
|
}
|
|
}
|
|
|
|
private static _finishOwnedFly(
|
|
node: Node,
|
|
stopAnim: string,
|
|
flyLayer: Node,
|
|
onComplete?: () => void,
|
|
): void {
|
|
this._playStopSpine(node, stopAnim, () => {
|
|
this._restoreOwnedFlyNode(node, flyLayer);
|
|
onComplete?.();
|
|
});
|
|
}
|
|
|
|
private static _playStopSpine(node: Node, stopAnim: string, onDone: () => void): void {
|
|
const spine = node.getComponent(sp.Skeleton);
|
|
if (!spine?.isValid || !spine.skeletonData) {
|
|
onDone();
|
|
return;
|
|
}
|
|
const runtime = spine.skeletonData.getRuntimeData?.();
|
|
if (!runtime?.findAnimation?.(stopAnim)) {
|
|
console.log('[AxeFly] spine anim missing:', stopAnim, 'on', node.name);
|
|
onDone();
|
|
return;
|
|
}
|
|
spine.enabled = true;
|
|
spine.paused = false;
|
|
BattleSpinePlayback.resetTracks(spine);
|
|
BattleSpinePlayback.play(spine, stopAnim, {
|
|
loop: false,
|
|
onComplete: onDone,
|
|
});
|
|
}
|
|
|
|
private static _restoreOwnedFlyNode(node: Node, flyLayer?: Node | null): void {
|
|
const restore = this._readOwnedRestore(node);
|
|
if (!restore || !node.isValid) return;
|
|
|
|
SkillTransformDriver.cancelNode(node);
|
|
this._stopFlySpine(node);
|
|
|
|
if (restore.parent?.isValid) {
|
|
node.parent = restore.parent;
|
|
node.setPosition(restore.localPosition);
|
|
node.setScale(restore.localScale);
|
|
node.angle = restore.angle;
|
|
node.active = restore.active;
|
|
} else {
|
|
node.active = false;
|
|
}
|
|
|
|
delete (node as Node & { [OWNED_RESTORE_KEY]?: OwnedAxeRestore })[OWNED_RESTORE_KEY];
|
|
if (flyLayer?.isValid) this._maybeHideLayer(flyLayer);
|
|
}
|
|
|
|
private static _readOwnedRestore(node: Node): OwnedAxeRestore | null {
|
|
return (node as Node & { [OWNED_RESTORE_KEY]?: OwnedAxeRestore })[OWNED_RESTORE_KEY] ?? null;
|
|
}
|
|
|
|
private static _restartFlySpine(
|
|
node: Node,
|
|
flyAnim: string,
|
|
timeScale: number,
|
|
visualScaleMag = 1,
|
|
): void {
|
|
const spine = node.getComponent(sp.Skeleton);
|
|
if (!spine?.isValid || !spine.skeletonData) return;
|
|
|
|
spine.enabled = true;
|
|
spine.paused = false;
|
|
spine.clearTracks();
|
|
spine.setToSetupPose();
|
|
const entry = spine.setAnimation(0, flyAnim, true);
|
|
spine.timeScale = timeScale / Math.max(visualScaleMag, 0.01);
|
|
|
|
if (!entry) {
|
|
console.log('[AxeFly] spine anim missing:', flyAnim, 'on', node.name);
|
|
}
|
|
}
|
|
|
|
private static _stopFlySpine(node: Node): void {
|
|
const spine = node.getComponent(sp.Skeleton);
|
|
if (!spine?.isValid) return;
|
|
BattleSpinePlayback.clearPendingComplete(spine);
|
|
BattleSpinePlayback.resetTracks(spine);
|
|
}
|
|
|
|
private static _maybeHideLayer(flyLayer: Node): void {
|
|
if (flyLayer?.isValid && flyLayer.children.length === 0) {
|
|
flyLayer.active = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function parseAxePiercePx(params: Readonly<Record<string, unknown>>): number {
|
|
const fromParams = params.piercePx;
|
|
if (typeof fromParams === 'number' && fromParams >= 0) return fromParams;
|
|
const parts = String(params.atkType ?? '').split(',');
|
|
if (Number(parts[0]) !== 3) return 0;
|
|
return Math.max(0, Number(parts[1]) || 0);
|
|
}
|
|
|