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

284 lines
9.0 KiB

3 weeks ago
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);
}