MoneyAni.ts 13 KB

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