MoneyAni.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. import { _decorator, Component, Node, Vec3, tween, instantiate, Prefab, math, Label, director } from 'cc';
  2. import { SaveDataManager } from '../LevelSystem/SaveDataManager';
  3. import { TopBarController } from '../FourUI/TopBarController';
  4. import EventBus, { GameEvents } from '../Core/EventBus';
  5. import { Audio } from '../AudioManager/AudioManager';
  6. const { ccclass, property } = _decorator;
  7. /**
  8. * 奖励动画系统
  9. * 负责在关卡结束后播放钞票和钻石的奖励动画
  10. */
  11. @ccclass('MoneyAni')
  12. export class MoneyAni extends Component {
  13. @property({
  14. type: Prefab,
  15. tooltip: '钞票预制体'
  16. })
  17. public coinPrefab: Prefab = null;
  18. @property({
  19. type: Prefab,
  20. tooltip: '钻石预制体'
  21. })
  22. public diamondPrefab: Prefab = null;
  23. @property({
  24. type: Node,
  25. tooltip: '奖励生成起始位置节点(MainUI中的奖励显示区域)'
  26. })
  27. public rewardStartNode: Node = null;
  28. @property({
  29. type: Node,
  30. tooltip: '钞票动画起始位置节点(Canvas/ShopUI/ScrollView/view/content/bill/Sprite/Sprite)'
  31. })
  32. public coinStartNode: Node = null;
  33. @property({
  34. type: Node,
  35. tooltip: '钻石动画起始位置节点(Canvas/ShopUI/ScrollView/view/content/diamond/Sprite/Sprite)'
  36. })
  37. public diamondStartNode: Node = null;
  38. @property({
  39. type: Node,
  40. tooltip: 'Canvas节点,用于添加动画元素'
  41. })
  42. public canvasNode: Node = null;
  43. @property({
  44. type: Node,
  45. tooltip: '钞票目标节点(TopBar中的钞票标签)'
  46. })
  47. public coinTargetNode: Node = null;
  48. @property({
  49. type: Node,
  50. tooltip: '钻石目标节点(TopBar中的钻石标签)'
  51. })
  52. public diamondTargetNode: Node = null;
  53. private saveDataManager: SaveDataManager = null;
  54. // 新增:奖励动画延迟的状态与挂起数据
  55. private gatingEnabled: boolean = false;
  56. private pendingReward: { money: number; diamonds: number } | null = null;
  57. private pendingOnComplete: (() => void) | null = null;
  58. // 新增:统一派发奖励动画完成事件
  59. private emitRewardCompleted(money: number, diamonds: number): void {
  60. EventBus.getInstance().emit(GameEvents.REWARD_ANIMATION_COMPLETED, { money, diamonds });
  61. }
  62. onLoad() {
  63. this.saveDataManager = SaveDataManager.getInstance();
  64. // 监听奖励动画事件
  65. EventBus.getInstance().on('PLAY_REWARD_ANIMATION', this.onPlayRewardAnimation, this);
  66. // 新增:监听显示GainUI与确认事件
  67. EventBus.getInstance().on(GameEvents.SHOW_GAIN_UI, this.onShowGainUI, this);
  68. EventBus.getInstance().on(GameEvents.GAIN_UI_CONFIRMED, this.onGainUIConfirmed, this);
  69. }
  70. /**
  71. * 处理奖励动画事件
  72. */
  73. private onPlayRewardAnimation(data: {money: number, diamonds: number}) {
  74. console.log('[MoneyAni] 接收到奖励动画事件:', data);
  75. this.playRewardAnimation(data.money, data.diamonds);
  76. }
  77. onDestroy() {
  78. // 移除事件监听器
  79. EventBus.getInstance().off('PLAY_REWARD_ANIMATION', this.onPlayRewardAnimation, this);
  80. EventBus.getInstance().off(GameEvents.SHOW_GAIN_UI, this.onShowGainUI, this);
  81. EventBus.getInstance().off(GameEvents.GAIN_UI_CONFIRMED, this.onGainUIConfirmed, this);
  82. }
  83. /**
  84. * 播放奖励动画
  85. * @param coinAmount 钞票数量
  86. * @param diamondAmount 钻石数量
  87. * @param onComplete 动画完成回调
  88. */
  89. public playRewardAnimation(coinAmount: number, diamondAmount: number, onComplete?: () => void) {
  90. if (this.gatingEnabled) {
  91. this.pendingReward = { money: coinAmount, diamonds: diamondAmount };
  92. this.pendingOnComplete = onComplete || null;
  93. console.log('[MoneyAni] 已延迟奖励动画,等待GainUI确认');
  94. return;
  95. }
  96. console.log(`[MoneyAni] 开始播放奖励动画 - 钞票: ${coinAmount}, 钻石: ${diamondAmount}`);
  97. // 如果奖励为0,直接返回不播放动画
  98. if (coinAmount <= 0 && diamondAmount <= 0) {
  99. console.log('[MoneyAni] 奖励为0,跳过动画播放');
  100. this.emitRewardCompleted(coinAmount, diamondAmount);
  101. if (onComplete) onComplete();
  102. return;
  103. }
  104. if (!this.canvasNode) {
  105. console.error('[MoneyAni] Canvas节点未设置,请在编辑器中拖拽Canvas节点到canvasNode属性');
  106. this.emitRewardCompleted(coinAmount, diamondAmount);
  107. if (onComplete) onComplete();
  108. return;
  109. }
  110. let animationsCompleted = 0;
  111. const totalAnimations = (coinAmount > 0 ? 1 : 0) + (diamondAmount > 0 ? 1 : 0);
  112. const onAnimationComplete = () => {
  113. animationsCompleted++;
  114. if (animationsCompleted >= totalAnimations) {
  115. console.log('[MoneyAni] 所有奖励动画播放完成');
  116. this.emitRewardCompleted(coinAmount, diamondAmount);
  117. if (onComplete) onComplete();
  118. }
  119. };
  120. // 播放钞票动画
  121. if (coinAmount > 0) {
  122. this.playCoinAnimation(coinAmount, onAnimationComplete);
  123. }
  124. // 播放钻石动画(延迟0.2秒开始)
  125. if (diamondAmount > 0) {
  126. this.scheduleOnce(() => {
  127. this.playDiamondAnimation(diamondAmount, onAnimationComplete);
  128. }, 0.2);
  129. }
  130. // 如果没有奖励,直接调用完成回调
  131. if (totalAnimations === 0) {
  132. this.emitRewardCompleted(coinAmount, diamondAmount);
  133. if (onComplete) onComplete();
  134. }
  135. }
  136. /**
  137. * 播放钞票动画
  138. */
  139. private playCoinAnimation(amount: number, onComplete?: () => void) {
  140. if (!this.coinPrefab) {
  141. console.error('[MoneyAni] 钞票预制体未设置');
  142. if (onComplete) onComplete();
  143. return;
  144. }
  145. // 如果钞票数量为0或负数,直接返回不播放动画和音效
  146. if (amount <= 0) {
  147. console.log('[MoneyAni] 钞票数量为0,跳过钞票动画');
  148. if (onComplete) onComplete();
  149. return;
  150. }
  151. // 使用装饰器引用的钞票目标节点
  152. if (!this.coinTargetNode) {
  153. console.error('[MoneyAni] 钞票目标节点未设置,请在编辑器中拖拽TopBar中的钞票标签节点');
  154. if (onComplete) onComplete();
  155. return;
  156. }
  157. const targetNode = this.coinTargetNode;
  158. // 生成钞票数量(最多10个,超过则按比例显示)
  159. const coinCount = Math.min(amount, 10);
  160. const startPos = this.getCoinStartPosition();
  161. console.log(`[MoneyAni] 生成${coinCount}个钞票动画`);
  162. let coinsCompleted = 0;
  163. for (let i = 0; i < coinCount; i++) {
  164. // 延迟生成,创造连续效果
  165. this.scheduleOnce(() => {
  166. this.createAndAnimateCoin(startPos, targetNode, () => {
  167. coinsCompleted++;
  168. if (coinsCompleted >= coinCount) {
  169. // 所有钞票动画完成
  170. if (onComplete) onComplete();
  171. }
  172. });
  173. }, i * 0.1);
  174. }
  175. }
  176. /**
  177. * 播放钻石动画
  178. */
  179. private playDiamondAnimation(amount: number, onComplete?: () => void) {
  180. if (!this.diamondPrefab) {
  181. console.error('[MoneyAni] 钻石预制体未设置');
  182. if (onComplete) onComplete();
  183. return;
  184. }
  185. // 如果钻石数量为0或负数,直接返回不播放动画和音效
  186. if (amount <= 0) {
  187. console.log('[MoneyAni] 钻石数量为0,跳过钻石动画');
  188. if (onComplete) onComplete();
  189. return;
  190. }
  191. // 使用装饰器引用的钻石目标节点
  192. if (!this.diamondTargetNode) {
  193. console.error('[MoneyAni] 钻石目标节点未设置,请在编辑器中拖拽TopBar中的钻石标签节点');
  194. if (onComplete) onComplete();
  195. return;
  196. }
  197. const targetNode = this.diamondTargetNode;
  198. // 生成钻石数量(最多5个)
  199. const diamondCount = Math.min(amount, 5);
  200. const startPos = this.getDiamondStartPosition();
  201. console.log(`[MoneyAni] 生成${diamondCount}个钻石动画`);
  202. let diamondsCompleted = 0;
  203. for (let i = 0; i < diamondCount; i++) {
  204. // 延迟生成
  205. this.scheduleOnce(() => {
  206. this.createAndAnimateDiamond(startPos, targetNode, () => {
  207. diamondsCompleted++;
  208. if (diamondsCompleted >= diamondCount) {
  209. // 所有钻石动画完成
  210. if (onComplete) onComplete();
  211. }
  212. });
  213. }, i * 0.15);
  214. }
  215. }
  216. /**
  217. * 创建并播放单个钞票动画
  218. */
  219. private createAndAnimateCoin(startPos: Vec3, targetNode: Node, onComplete?: () => void) {
  220. const coinNode = instantiate(this.coinPrefab);
  221. this.canvasNode.addChild(coinNode);
  222. // 设置初始位置
  223. coinNode.setWorldPosition(startPos);
  224. // 生成随机散开位置
  225. const scatterPos = this.getScatterPosition(startPos);
  226. // 获取目标世界位置
  227. const targetWorldPos = new Vec3();
  228. targetNode.getWorldPosition(targetWorldPos);
  229. // 动画序列:散开 -> 飞向目标
  230. tween(coinNode)
  231. // 第一阶段:散开(带旋转和缩放)
  232. .parallel(
  233. tween().to(0.3, { worldPosition: scatterPos }, { easing: 'quadOut' }),
  234. tween().to(0.3, { scale: new Vec3(1.2, 1.2, 1.2) }, { easing: 'quadOut' }),
  235. tween().by(0.3, { eulerAngles: new Vec3(0, 0, 180) })
  236. )
  237. // 短暂停留
  238. .delay(0.1)
  239. // 播放音效并开始第二阶段:飞向目标(带缩小和旋转)
  240. .call(() => {
  241. Audio.playUISound('data/弹球音效/get money');
  242. })
  243. .parallel(
  244. tween().to(0.5, { worldPosition: targetWorldPos }, { easing: 'quadIn' }),
  245. tween().to(0.5, { scale: new Vec3(0.3, 0.3, 0.3) }, { easing: 'quadIn' }),
  246. tween().by(0.5, { eulerAngles: new Vec3(0, 0, 360) })
  247. )
  248. .call(() => {
  249. // 动画完成,销毁节点
  250. coinNode.destroy();
  251. if (onComplete) onComplete();
  252. })
  253. .start();
  254. }
  255. /**
  256. * 创建并播放单个钻石动画
  257. */
  258. private createAndAnimateDiamond(startPos: Vec3, targetNode: Node, onComplete?: () => void) {
  259. const diamondNode = instantiate(this.diamondPrefab);
  260. this.canvasNode.addChild(diamondNode);
  261. // 设置初始位置
  262. diamondNode.setWorldPosition(startPos);
  263. // 生成随机散开位置
  264. const scatterPos = this.getScatterPosition(startPos);
  265. // 获取目标世界位置
  266. const targetWorldPos = new Vec3();
  267. targetNode.getWorldPosition(targetWorldPos);
  268. // 动画序列:散开 -> 飞向目标
  269. tween(diamondNode)
  270. // 第一阶段:散开(带旋转和缩放)
  271. .parallel(
  272. tween().to(0.4, { worldPosition: scatterPos }, { easing: 'quadOut' }),
  273. tween().to(0.4, { scale: new Vec3(1.3, 1.3, 1.3) }, { easing: 'quadOut' }),
  274. tween().by(0.4, { eulerAngles: new Vec3(0, 0, -180) })
  275. )
  276. // 短暂停留
  277. .delay(0.15)
  278. // 播放音效并开始第二阶段:飞向目标(带缩小和旋转)
  279. .call(() => {
  280. Audio.playUISound('data/弹球音效/get money');
  281. })
  282. .parallel(
  283. tween().to(0.6, { worldPosition: targetWorldPos }, { easing: 'quadIn' }),
  284. tween().to(0.6, { scale: new Vec3(0.4, 0.4, 0.4) }, { easing: 'quadIn' }),
  285. tween().by(0.6, { eulerAngles: new Vec3(0, 0, -360) })
  286. )
  287. .call(() => {
  288. // 动画完成,销毁节点
  289. diamondNode.destroy();
  290. if (onComplete) onComplete();
  291. })
  292. .start();
  293. }
  294. /**
  295. * 获取奖励生成起始位置
  296. */
  297. private getRewardStartPosition(): Vec3 {
  298. if (this.rewardStartNode) {
  299. const worldPos = new Vec3();
  300. this.rewardStartNode.getWorldPosition(worldPos);
  301. return worldPos;
  302. }
  303. // 默认位置(屏幕中央偏下)
  304. return new Vec3(0, -200, 0);
  305. }
  306. /**
  307. * 获取钞票动画起始位置
  308. */
  309. private getCoinStartPosition(): Vec3 {
  310. if (this.coinStartNode) {
  311. const worldPos = new Vec3();
  312. this.coinStartNode.getWorldPosition(worldPos);
  313. return worldPos;
  314. }
  315. // 如果没有设置专门的钞票起始节点,使用通用的奖励起始位置
  316. return this.getRewardStartPosition();
  317. }
  318. /**
  319. * 获取钻石动画起始位置
  320. */
  321. private getDiamondStartPosition(): Vec3 {
  322. if (this.diamondStartNode) {
  323. const worldPos = new Vec3();
  324. this.diamondStartNode.getWorldPosition(worldPos);
  325. return worldPos;
  326. }
  327. // 如果没有设置专门的钻石起始节点,使用通用的奖励起始位置
  328. return this.getRewardStartPosition();
  329. }
  330. /**
  331. * 获取散开位置
  332. */
  333. private getScatterPosition(startPos: Vec3): Vec3 {
  334. const scatterRadius = 150; // 散开半径
  335. const angle = math.randomRange(0, Math.PI * 2); // 随机角度
  336. const distance = math.randomRange(50, scatterRadius); // 随机距离
  337. return new Vec3(
  338. startPos.x + Math.cos(angle) * distance,
  339. startPos.y + Math.sin(angle) * distance,
  340. startPos.z
  341. );
  342. }
  343. /**
  344. * 静态方法:播放奖励动画(需要传入实例)
  345. * @param moneyAniInstance MoneyAni组件实例
  346. * @param coinAmount 钞票数量
  347. * @param diamondAmount 钻石数量
  348. * @param onComplete 完成回调
  349. */
  350. public static playRewardWithInstance(moneyAniInstance: MoneyAni, coinAmount: number, diamondAmount: number, onComplete?: () => void) {
  351. if (!moneyAniInstance) {
  352. console.error('[MoneyAni] MoneyAni实例为空,请传入有效的MoneyAni组件实例');
  353. if (onComplete) onComplete();
  354. return;
  355. }
  356. moneyAniInstance.playRewardAnimation(coinAmount, diamondAmount, onComplete);
  357. }
  358. /**
  359. * 静态方法:播放奖励动画(简化版本)
  360. * 自动查找场景中的MoneyAni组件
  361. * @param coinAmount 钞票数量
  362. * @param diamondAmount 钻石数量
  363. * @param onComplete 完成回调
  364. */
  365. public static playReward(coinAmount: number, diamondAmount: number, onComplete?: () => void) {
  366. // 尝试从director获取场景中的MoneyAni组件
  367. const scene = director.getScene();
  368. if (!scene) {
  369. console.error('[MoneyAni] 无法获取当前场景');
  370. if (onComplete) onComplete();
  371. return;
  372. }
  373. // 递归查找MoneyAni组件
  374. const findMoneyAni = (node: Node): MoneyAni | null => {
  375. const moneyAni = node.getComponent(MoneyAni);
  376. if (moneyAni) return moneyAni;
  377. for (const child of node.children) {
  378. const result = findMoneyAni(child);
  379. if (result) return result;
  380. }
  381. return null;
  382. };
  383. const moneyAni = findMoneyAni(scene);
  384. if (!moneyAni) {
  385. console.error('[MoneyAni] 场景中未找到MoneyAni组件,请确保场景中存在MoneyAni组件');
  386. if (onComplete) onComplete();
  387. return;
  388. }
  389. moneyAni.playRewardAnimation(coinAmount, diamondAmount, onComplete);
  390. }
  391. /**
  392. * 根据关卡配置播放奖励动画
  393. */
  394. public async playLevelReward(level: number, onComplete?: () => void) {
  395. try {
  396. const rewards = await this.saveDataManager.getLevelRewardsFromConfig(level);
  397. if (rewards) {
  398. if (this.gatingEnabled) {
  399. this.pendingReward = { money: rewards.money, diamonds: rewards.diamonds };
  400. this.pendingOnComplete = onComplete || null;
  401. console.log('[MoneyAni] 已延迟关卡奖励动画,等待GainUI确认');
  402. } else {
  403. this.playRewardAnimation(rewards.money, rewards.diamonds, onComplete);
  404. }
  405. } else {
  406. console.warn(`[MoneyAni] 无法获取关卡${level}的奖励配置,使用默认奖励`);
  407. this.playRewardAnimation(50, 5, onComplete);
  408. }
  409. } catch (error) {
  410. console.error('[MoneyAni] 获取关卡奖励配置时出错:', error);
  411. this.playRewardAnimation(50, 5, onComplete);
  412. }
  413. }
  414. private onShowGainUI = () => {
  415. this.gatingEnabled = true;
  416. console.log('[MoneyAni] 检测到SHOW_GAIN_UI,启用奖励动画延迟');
  417. }
  418. private onGainUIConfirmed = () => {
  419. console.log('[MoneyAni] 检测到GAIN_UI_CONFIRMED,准备播放延迟的奖励动画');
  420. const pending = this.pendingReward;
  421. const onComplete = this.pendingOnComplete || undefined;
  422. this.pendingReward = null;
  423. this.pendingOnComplete = null;
  424. this.gatingEnabled = false;
  425. if (pending) {
  426. this.playRewardAnimation(pending.money, pending.diamonds, onComplete);
  427. }
  428. }
  429. }