import { instantiate, Node, Tween, tween, UIOpacity, UITransform, v3, Vec3 } from 'cc'; import { GAudioPath } from '../../game/ConfigRes'; /** 单次飞行时长(秒) */ const COIN_FLY_SEC = 0.45; /** 影子透明度 */ const SHADOW_OPACITY = 200; /** 影子起飞间隔(秒) */ const COIN_SHADOW_STAGGER = 0.04; /** 影子最少枚数 */ const MIN_COIN_SHADOW = 3; /** 影子最多枚数 */ const MAX_COIN_SHADOW = 12; /** * 胜利结算:ui_coin 金币 icon 影子飞向 ui_money。 */ export class BattleSuccessCoinFlyPresenter { private _flyLayer: Node | null = null; private _shadowNodes: Node[] = []; /** 绑定飞行层(ui_center) */ bind(flyLayer: Node | null | undefined): void { this._flyLayer = flyLayer?.isValid ? flyLayer : null; } /** 停止并清理影子 */ unbind(): void { this._stopShadows(); this._flyLayer = null; } /** * 在 sourceIcon 附近生成多枚影子,飞向 targetCoin。 * @param sourceIcon ui_coin/icon * @param targetCoin ui_money/金币 * @param coinAmount 本次获得金币(用于计算影子数量) * @param onComplete 全部到达 */ play(sourceIcon: Node | null, targetCoin: Node | null, coinAmount: number, onComplete: () => void): void { const flyLayer = this._flyLayer; if (!sourceIcon?.isValid || !targetCoin?.isValid || !flyLayer?.isValid) { console.log('[BattleSuccessCoinFly] 飞行参数无效,跳过'); onComplete(); return; } const count = this._shadowCount(coinAmount); if (count <= 0) { onComplete(); return; } this._stopShadows(); gg.audio.playEffect({ name: '结算金币效果', path: GAudioPath.SfxUi }); const endPos = this._resolveWorldCenter(targetCoin); const srcPos = sourceIcon.worldPosition.clone(); const srcScale = sourceIcon.worldScale; const shadowScale = new Vec3(srcScale.x, srcScale.y, srcScale.z); let arrived = 0; for (let i = 0; i < count; i++) { const shadow = instantiate(sourceIcon); shadow.name = 'coinShadow'; flyLayer.addChild(shadow); shadow.setSiblingIndex(flyLayer.children.length - 1); const spread = (i - (count - 1) * 0.5) * 14; shadow.setWorldPosition(srcPos.x + spread, srcPos.y + (Math.random() - 0.5) * 10, srcPos.z); shadow.setWorldScale(shadowScale); const fade = shadow.getOrAddComponent(UIOpacity); if (fade) fade.opacity = SHADOW_OPACITY; this._shadowNodes.push(shadow); Tween.stopAllByTarget(shadow); tween(shadow) .delay(i * COIN_SHADOW_STAGGER) .to(COIN_FLY_SEC, { worldPosition: endPos.clone() }, { easing: 'sineIn' }) .call(() => { arrived++; if (shadow.isValid) { Tween.stopAllByTarget(shadow); shadow.destroy(); } const idx = this._shadowNodes.indexOf(shadow); if (idx >= 0) this._shadowNodes.splice(idx, 1); if (arrived >= count) onComplete(); }) .start(); } } /** 按金币数量估算影子枚数 */ private _shadowCount(amount: number): number { const n = Math.floor(Number(amount) || 0); if (n <= 0) return 0; const scaled = Math.ceil(Math.log10(n + 1) * 3); return Math.min(MAX_COIN_SHADOW, Math.max(MIN_COIN_SHADOW, scaled)); } /** 节点中心世界坐标 */ private _resolveWorldCenter(node: Node): Vec3 { const ut = node.getComponent(UITransform); if (ut) return ut.convertToWorldSpaceAR(v3(0, 0, 0)); return node.worldPosition.clone(); } /** 销毁所有影子 */ private _stopShadows(): void { for (let i = 0; i < this._shadowNodes.length; i++) { const shadow = this._shadowNodes[i]; if (!shadow?.isValid) continue; Tween.stopAllByTarget(shadow); shadow.destroy(); } this._shadowNodes.length = 0; } }