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; 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>): 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); }