MoneyAni.ts 14 KB

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