import { instantiate, Node, Tween, tween, UIOpacity, UITransform, v3, Vec3 } from 'cc'; /** 单次飞行时长(秒) */ const COIN_FLY_SEC = 0.45; /** 影子透明度 */ const SHADOW_OPACITY = 200; /** 影子起飞间隔(秒) */ const COIN_SHADOW_STAGGER = 0.04; /** 每个起点生成金币枚数 */ const COINS_PER_SOURCE = 5; /** 起点横向散开间距(像素) */ const SPAWN_SPREAD_X = 14; /** 起点纵向随机偏移(像素) */ const SPAWN_SPREAD_Y = 10; /** ui_money 下金币 icon 节点名 */ const MONEY_COIN_NODE = '金币'; /** 俸禄金币飞行起点(PopEmolument 领取前记录世界坐标) */ export interface EmolumentCoinFlySource { /** 起点世界坐标 */ worldPos: Vec3; } /** * 首页俸禄领取:多起点金币影子飞向 ui_money/金币。 */ export class PanelHomeEmolumentCoinFlyPresenter { private _flyLayer: Node | null = null; private _moneyRoot: Node | null = null; private readonly _shadowNodes: Node[] = []; /** * 绑定飞行层与 ui_money。 * @param flyLayer 影子父节点(建议 PanelHome 根节点) * @param moneyRoot ui_money */ bind(flyLayer: Node | null | undefined, moneyRoot: Node | null | undefined): void { this._flyLayer = flyLayer?.isValid ? flyLayer : null; this._moneyRoot = moneyRoot?.isValid ? moneyRoot : null; } /** 停止飞行并清理影子 */ unbind(): void { this._stopShadows(); this._flyLayer = null; this._moneyRoot = null; } /** * 从各起点生成 5 枚影子,飞向 ui_money/金币。 * @param sources 奖励 > 0 的起点列表 * @param onComplete 全部到达或跳过 */ play(sources: EmolumentCoinFlySource[], onComplete?: () => void): void { const done = onComplete ?? (() => { }); const flyLayer = this._flyLayer; const targetCoin = this._resolveMoneyCoin(); if (!flyLayer?.isValid || !targetCoin?.isValid) { console.log('[PanelHomeEmolumentCoinFly] 飞行参数无效,跳过'); done(); return; } if (!sources.length) { done(); return; } this._stopShadows(); gg.audio.playEffect({ name: '结算金币效果' }); const endPos = this._resolveWorldCenter(targetCoin); const shadowTemplate = targetCoin; const shadowWorldScale = targetCoin.worldScale.clone(); let spawnedCount = 0; let arrived = 0; let shadowIndex = 0; for (let s = 0; s < sources.length; s++) { const source = sources[s]; spawnedCount += COINS_PER_SOURCE; const srcPos = source.worldPos; for (let i = 0; i < COINS_PER_SOURCE; i++) { const shadow = instantiate(shadowTemplate); shadow.name = 'emolumentCoinShadow'; flyLayer.addChild(shadow); shadow.setSiblingIndex(flyLayer.children.length - 1); const spread = (i - (COINS_PER_SOURCE - 1) * 0.5) * SPAWN_SPREAD_X; shadow.setWorldPosition( srcPos.x + spread, srcPos.y + (Math.random() - 0.5) * SPAWN_SPREAD_Y, srcPos.z, ); shadow.setWorldScale(shadowWorldScale); shadow.getOrAddComponent(UIOpacity).opacity = SHADOW_OPACITY; this._shadowNodes.push(shadow); const delayIndex = shadowIndex; shadowIndex++; Tween.stopAllByTarget(shadow); tween(shadow) .delay(delayIndex * 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 >= spawnedCount) done(); }) .start(); } } if (spawnedCount <= 0) { done(); } } /** ui_money/金币 */ private _resolveMoneyCoin(): Node | null { const moneyRoot = this._moneyRoot; if (!moneyRoot?.isValid) { console.log('[PanelHomeEmolumentCoinFly] ui_money 缺失'); return null; } const coin = moneyRoot.getChildByName(MONEY_COIN_NODE); if (!coin?.isValid) { console.log('[PanelHomeEmolumentCoinFly] ui_money 缺少', MONEY_COIN_NODE); return null; } return coin; } /** 节点中心世界坐标 */ 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; } }