MoneyAni.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. import { _decorator, Component, Node, Vec3, tween, instantiate, Prefab, find, math, Label } from 'cc';
  2. import { SaveDataManager } from '../LevelSystem/SaveDataManager';
  3. import { TopBarController } from '../FourUI/TopBarController';
  4. const { ccclass, property } = _decorator;
  5. /**
  6. * 奖励动画系统
  7. * 负责在关卡结束后播放金币和钻石的奖励动画
  8. */
  9. @ccclass('MoneyAni')
  10. export class MoneyAni extends Component {
  11. @property({
  12. type: Prefab,
  13. tooltip: '金币预制体'
  14. })
  15. public coinPrefab: Prefab = null;
  16. @property({
  17. type: Prefab,
  18. tooltip: '钻石预制体'
  19. })
  20. public diamondPrefab: Prefab = null;
  21. @property({
  22. type: Node,
  23. tooltip: '奖励生成起始位置节点(MainUI中的奖励显示区域)'
  24. })
  25. public rewardStartNode: Node = null;
  26. @property({
  27. type: Node,
  28. tooltip: 'Canvas节点,用于添加动画元素'
  29. })
  30. public canvasNode: Node = null;
  31. private saveDataManager: SaveDataManager = null;
  32. onLoad() {
  33. this.saveDataManager = SaveDataManager.getInstance();
  34. }
  35. /**
  36. * 播放奖励动画
  37. * @param coinAmount 金币数量
  38. * @param diamondAmount 钻石数量
  39. * @param onComplete 动画完成回调
  40. */
  41. public playRewardAnimation(coinAmount: number, diamondAmount: number, onComplete?: () => void) {
  42. console.log(`[MoneyAni] 开始播放奖励动画 - 金币: ${coinAmount}, 钻石: ${diamondAmount}`);
  43. if (!this.canvasNode) {
  44. this.canvasNode = find('Canvas');
  45. }
  46. if (!this.canvasNode) {
  47. console.error('[MoneyAni] 找不到Canvas节点');
  48. if (onComplete) onComplete();
  49. return;
  50. }
  51. let animationsCompleted = 0;
  52. const totalAnimations = (coinAmount > 0 ? 1 : 0) + (diamondAmount > 0 ? 1 : 0);
  53. const onAnimationComplete = () => {
  54. animationsCompleted++;
  55. if (animationsCompleted >= totalAnimations) {
  56. console.log('[MoneyAni] 所有奖励动画播放完成');
  57. if (onComplete) onComplete();
  58. }
  59. };
  60. // 播放金币动画
  61. if (coinAmount > 0) {
  62. this.playCoinAnimation(coinAmount, onAnimationComplete);
  63. }
  64. // 播放钻石动画(延迟0.2秒开始)
  65. if (diamondAmount > 0) {
  66. this.scheduleOnce(() => {
  67. this.playDiamondAnimation(diamondAmount, onAnimationComplete);
  68. }, 0.2);
  69. }
  70. // 如果没有奖励,直接调用完成回调
  71. if (totalAnimations === 0) {
  72. if (onComplete) onComplete();
  73. }
  74. }
  75. /**
  76. * 播放金币动画
  77. */
  78. private playCoinAnimation(amount: number, onComplete?: () => void) {
  79. if (!this.coinPrefab) {
  80. console.error('[MoneyAni] 金币预制体未设置');
  81. if (onComplete) onComplete();
  82. return;
  83. }
  84. // 获取目标位置(TopBar中的金币图标)
  85. const targetNode = find('Canvas/TopBar')?.getChildByName('moneyLabel') ||
  86. find('Canvas/TopBar/moneyLabel');
  87. if (!targetNode) {
  88. console.error('[MoneyAni] 找不到金币目标节点');
  89. if (onComplete) onComplete();
  90. return;
  91. }
  92. // 生成金币数量(最多10个,超过则按比例显示)
  93. const coinCount = Math.min(amount, 10);
  94. const startPos = this.getRewardStartPosition();
  95. console.log(`[MoneyAni] 生成${coinCount}个金币动画`);
  96. let coinsCompleted = 0;
  97. for (let i = 0; i < coinCount; i++) {
  98. // 延迟生成,创造连续效果
  99. this.scheduleOnce(() => {
  100. this.createAndAnimateCoin(startPos, targetNode, () => {
  101. coinsCompleted++;
  102. if (coinsCompleted >= coinCount) {
  103. // 所有金币动画完成,更新数据
  104. this.saveDataManager.addCoins(amount, 'level_reward');
  105. TopBarController.updateTopBarUI();
  106. if (onComplete) onComplete();
  107. }
  108. });
  109. }, i * 0.1);
  110. }
  111. }
  112. /**
  113. * 播放钻石动画
  114. */
  115. private playDiamondAnimation(amount: number, onComplete?: () => void) {
  116. if (!this.diamondPrefab) {
  117. console.error('[MoneyAni] 钻石预制体未设置');
  118. if (onComplete) onComplete();
  119. return;
  120. }
  121. // 获取目标位置(TopBar中的钻石图标)
  122. const targetNode = find('Canvas/TopBar')?.getChildByName('diamondLabel') ||
  123. find('Canvas/TopBar/diamondLabel');
  124. if (!targetNode) {
  125. console.error('[MoneyAni] 找不到钻石目标节点');
  126. if (onComplete) onComplete();
  127. return;
  128. }
  129. // 生成钻石数量(最多5个)
  130. const diamondCount = Math.min(amount, 5);
  131. const startPos = this.getRewardStartPosition();
  132. console.log(`[MoneyAni] 生成${diamondCount}个钻石动画`);
  133. let diamondsCompleted = 0;
  134. for (let i = 0; i < diamondCount; i++) {
  135. // 延迟生成
  136. this.scheduleOnce(() => {
  137. this.createAndAnimateDiamond(startPos, targetNode, () => {
  138. diamondsCompleted++;
  139. if (diamondsCompleted >= diamondCount) {
  140. // 所有钻石动画完成,更新数据
  141. this.saveDataManager.addDiamonds(amount, 'level_reward');
  142. TopBarController.updateTopBarUI();
  143. if (onComplete) onComplete();
  144. }
  145. });
  146. }, i * 0.15);
  147. }
  148. }
  149. /**
  150. * 创建并播放单个金币动画
  151. */
  152. private createAndAnimateCoin(startPos: Vec3, targetNode: Node, onComplete?: () => void) {
  153. const coinNode = instantiate(this.coinPrefab);
  154. this.canvasNode.addChild(coinNode);
  155. // 设置初始位置
  156. coinNode.setWorldPosition(startPos);
  157. // 生成随机散开位置
  158. const scatterPos = this.getScatterPosition(startPos);
  159. // 获取目标世界位置
  160. const targetWorldPos = new Vec3();
  161. targetNode.getWorldPosition(targetWorldPos);
  162. // 动画序列:散开 -> 飞向目标
  163. tween(coinNode)
  164. // 第一阶段:散开(带旋转和缩放)
  165. .parallel(
  166. tween().to(0.3, { worldPosition: scatterPos }, { easing: 'quadOut' }),
  167. tween().to(0.3, { scale: new Vec3(1.2, 1.2, 1.2) }, { easing: 'quadOut' }),
  168. tween().by(0.3, { eulerAngles: new Vec3(0, 0, 180) })
  169. )
  170. // 短暂停留
  171. .delay(0.1)
  172. // 第二阶段:飞向目标(带缩小和旋转)
  173. .parallel(
  174. tween().to(0.5, { worldPosition: targetWorldPos }, { easing: 'quadIn' }),
  175. tween().to(0.5, { scale: new Vec3(0.3, 0.3, 0.3) }, { easing: 'quadIn' }),
  176. tween().by(0.5, { eulerAngles: new Vec3(0, 0, 360) })
  177. )
  178. .call(() => {
  179. // 动画完成,销毁节点
  180. coinNode.destroy();
  181. if (onComplete) onComplete();
  182. })
  183. .start();
  184. }
  185. /**
  186. * 创建并播放单个钻石动画
  187. */
  188. private createAndAnimateDiamond(startPos: Vec3, targetNode: Node, onComplete?: () => void) {
  189. const diamondNode = instantiate(this.diamondPrefab);
  190. this.canvasNode.addChild(diamondNode);
  191. // 设置初始位置
  192. diamondNode.setWorldPosition(startPos);
  193. // 生成随机散开位置
  194. const scatterPos = this.getScatterPosition(startPos);
  195. // 获取目标世界位置
  196. const targetWorldPos = new Vec3();
  197. targetNode.getWorldPosition(targetWorldPos);
  198. // 动画序列:散开 -> 飞向目标
  199. tween(diamondNode)
  200. // 第一阶段:散开(带旋转和缩放)
  201. .parallel(
  202. tween().to(0.4, { worldPosition: scatterPos }, { easing: 'quadOut' }),
  203. tween().to(0.4, { scale: new Vec3(1.3, 1.3, 1.3) }, { easing: 'quadOut' }),
  204. tween().by(0.4, { eulerAngles: new Vec3(0, 0, -180) })
  205. )
  206. // 短暂停留
  207. .delay(0.15)
  208. // 第二阶段:飞向目标(带缩小和旋转)
  209. .parallel(
  210. tween().to(0.6, { worldPosition: targetWorldPos }, { easing: 'quadIn' }),
  211. tween().to(0.6, { scale: new Vec3(0.4, 0.4, 0.4) }, { easing: 'quadIn' }),
  212. tween().by(0.6, { eulerAngles: new Vec3(0, 0, -360) })
  213. )
  214. .call(() => {
  215. // 动画完成,销毁节点
  216. diamondNode.destroy();
  217. if (onComplete) onComplete();
  218. })
  219. .start();
  220. }
  221. /**
  222. * 获取奖励生成起始位置
  223. */
  224. private getRewardStartPosition(): Vec3 {
  225. if (this.rewardStartNode) {
  226. const worldPos = new Vec3();
  227. this.rewardStartNode.getWorldPosition(worldPos);
  228. return worldPos;
  229. }
  230. // 默认位置(屏幕中央偏下)
  231. return new Vec3(0, -200, 0);
  232. }
  233. /**
  234. * 获取散开位置
  235. */
  236. private getScatterPosition(startPos: Vec3): Vec3 {
  237. const scatterRadius = 150; // 散开半径
  238. const angle = math.randomRange(0, Math.PI * 2); // 随机角度
  239. const distance = math.randomRange(50, scatterRadius); // 随机距离
  240. return new Vec3(
  241. startPos.x + Math.cos(angle) * distance,
  242. startPos.y + Math.sin(angle) * distance,
  243. startPos.z
  244. );
  245. }
  246. /**
  247. * 静态方法:播放奖励动画
  248. * 供外部调用的便捷接口
  249. */
  250. public static playReward(coinAmount: number, diamondAmount: number, onComplete?: () => void) {
  251. // 查找MoneyAni组件
  252. const moneyAniNode = find('Canvas/MoneyAni') || find('MoneyAni');
  253. if (!moneyAniNode) {
  254. console.error('[MoneyAni] 找不到MoneyAni节点');
  255. if (onComplete) onComplete();
  256. return;
  257. }
  258. const moneyAni = moneyAniNode.getComponent(MoneyAni);
  259. if (!moneyAni) {
  260. console.error('[MoneyAni] 找不到MoneyAni组件');
  261. if (onComplete) onComplete();
  262. return;
  263. }
  264. moneyAni.playRewardAnimation(coinAmount, diamondAmount, onComplete);
  265. }
  266. /**
  267. * 根据关卡配置播放奖励动画
  268. */
  269. public async playLevelReward(level: number, onComplete?: () => void) {
  270. try {
  271. const rewards = await this.saveDataManager.getLevelRewardsFromConfig(level);
  272. if (rewards) {
  273. this.playRewardAnimation(rewards.coins, rewards.diamonds, onComplete);
  274. } else {
  275. console.warn(`[MoneyAni] 无法获取关卡${level}的奖励配置,使用默认奖励`);
  276. // 使用默认奖励
  277. this.playRewardAnimation(50, 5, onComplete);
  278. }
  279. } catch (error) {
  280. console.error('[MoneyAni] 获取关卡奖励配置时出错:', error);
  281. // 使用默认奖励
  282. this.playRewardAnimation(50, 5, onComplete);
  283. }
  284. }
  285. }