GameEnd.ts 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069
  1. import { _decorator, Component, Node, Label, Button, tween, Tween, Vec3, UIOpacity, find } from 'cc';
  2. import { SaveDataManager } from '../LevelSystem/SaveDataManager';
  3. import { InGameManager, GameState } from '../LevelSystem/IN_game';
  4. import EventBus, { GameEvents } from '../Core/EventBus';
  5. import { Audio } from '../AudioManager/AudioManager';
  6. import { AdManager } from '../Ads/AdManager';
  7. const { ccclass, property } = _decorator;
  8. /**
  9. * 游戏结算界面管理器
  10. * 负责处理游戏结束后的奖励显示和双倍奖励功能
  11. */
  12. @ccclass('GameEnd')
  13. export class GameEnd extends Component {
  14. // === UI节点引用 ===
  15. @property({
  16. type: Button,
  17. tooltip: '双倍奖励按钮 (Canvas/GameEnd/double)'
  18. })
  19. public doubleButton: Button = null;
  20. @property({
  21. type: Label,
  22. tooltip: '钞票数量显示 '
  23. })
  24. public moneyLabel: Label = null;
  25. @property({
  26. type: Label,
  27. tooltip: '钻石数量显示 '
  28. })
  29. public diamondLabel: Label = null;
  30. @property({
  31. type: Button,
  32. tooltip: '继续按钮 (Canvas/GameEnd/Continue)'
  33. })
  34. public continueButton: Button = null;
  35. @property({
  36. type: InGameManager,
  37. tooltip: '游戏管理器组件'
  38. })
  39. public inGameManager: InGameManager = null;
  40. @property({
  41. type: Label,
  42. tooltip: 'EndLabel文本显示 (Canvas/GameEnd/Sprite/EndLabel)'
  43. })
  44. public endLabel: Label = null;
  45. // === 动画相关属性 ===
  46. @property({ tooltip: '动画持续时间(秒)' })
  47. public animationDuration: number = 0.3;
  48. // 淡入淡出和缩放动画都直接作用于当前节点(Canvas/GameEnd)
  49. // 不需要额外的目标节点属性
  50. private _originalScale: Vec3 = new Vec3();
  51. // === 私有属性 ===
  52. private saveDataManager: SaveDataManager = null;
  53. public currentRewards: {money: number, diamonds: number} = {money: -1, diamonds: -1};
  54. private hasDoubledReward: boolean = false;
  55. private isGameSuccess: boolean = false;
  56. private hasProcessedGameEnd: boolean = false; // 添加处理状态标志
  57. private isAdRequestPending: boolean = false; // 防止重复点击广告期间的状态
  58. // === 弹窗冷却控制 ===
  59. private static lastPopupTimestamp: number = 0; // 最近一次弹出时间戳
  60. private static readonly popupCooldownMs: number = 1; // 弹窗冷却时长(毫秒)
  61. private isInCooldown(): boolean {
  62. const now = Date.now();
  63. return (now - GameEnd.lastPopupTimestamp) < GameEnd.popupCooldownMs;
  64. }
  65. onLoad() {
  66. console.log('[GameEnd] onLoad方法被调用');
  67. this.setupEventListeners();
  68. }
  69. start() {
  70. console.log('[GameEnd] start方法被调用');
  71. // 保存原始缩放值
  72. if (this.node) {
  73. this._originalScale.set(this.node.scale);
  74. }
  75. // 设置事件监听器
  76. this.setupEventListeners();
  77. // 初始化为隐藏状态(但保持节点激活以便接收事件)
  78. this.initializeHiddenStateKeepActive();
  79. console.log('[GameEnd] start方法完成,面板已初始化为隐藏状态');
  80. }
  81. onEnable() {
  82. console.log('[GameEnd] onEnable方法被调用,节点已激活');
  83. // 初始化管理器
  84. this.initializeManagers();
  85. // UI节点已通过装饰器挂载,无需自动查找
  86. // 绑定按钮事件
  87. this.bindButtonEvents();
  88. // 初始化UI状态
  89. this.initializeUI();
  90. // 检查当前游戏状态并处理奖励(解决时序问题)
  91. this.checkAndHandleGameState();
  92. }
  93. /**
  94. * 初始化管理器
  95. */
  96. private initializeManagers() {
  97. this.saveDataManager = SaveDataManager.getInstance();
  98. // InGameManager已通过装饰器挂载,无需查找
  99. // 如果UI组件未在编辑器中绑定,尝试自动查找
  100. this.autoFindUIComponents();
  101. }
  102. /**
  103. * 自动查找UI组件(如果编辑器中未绑定)
  104. */
  105. private autoFindUIComponents() {
  106. // 自动查找moneyLabel
  107. if (!this.moneyLabel) {
  108. // 尝试多个可能的路径
  109. const moneyLabelPaths = [
  110. 'ResourceNode/MoneyResourceNode/MoneyLabel',
  111. 'Sprite/ResourceNode/MoneyResourceNode/MoneyLabel',
  112. 'ResourceNode/MoneyLabel',
  113. 'MoneyResourceNode/MoneyLabel'
  114. ];
  115. for (const path of moneyLabelPaths) {
  116. const moneyLabelNode = this.node.getChildByPath(path);
  117. if (moneyLabelNode) {
  118. this.moneyLabel = moneyLabelNode.getComponent(Label);
  119. if (this.moneyLabel) {
  120. console.log(`[GameEnd] 自动找到moneyLabel,路径: ${path}`);
  121. break;
  122. }
  123. }
  124. }
  125. // 如果还是没找到,尝试递归查找
  126. if (!this.moneyLabel) {
  127. this.moneyLabel = this.findLabelByName(this.node, 'MoneyLabel');
  128. if (this.moneyLabel) {
  129. console.log('[GameEnd] 通过递归查找找到moneyLabel');
  130. }
  131. }
  132. }
  133. // 自动查找diamondLabel
  134. if (!this.diamondLabel) {
  135. // 尝试多个可能的路径
  136. const diamondLabelPaths = [
  137. 'ResourceNode/DiamondResourceNode/DiamondLabel',
  138. 'Sprite/ResourceNode/DiamondResourceNode/DiamondLabel',
  139. 'ResourceNode/DiamondLabel',
  140. 'DiamondResourceNode/DiamondLabel'
  141. ];
  142. for (const path of diamondLabelPaths) {
  143. const diamondLabelNode = this.node.getChildByPath(path);
  144. if (diamondLabelNode) {
  145. this.diamondLabel = diamondLabelNode.getComponent(Label);
  146. if (this.diamondLabel) {
  147. console.log(`[GameEnd] 自动找到diamondLabel,路径: ${path}`);
  148. break;
  149. }
  150. }
  151. }
  152. // 如果还是没找到,尝试递归查找
  153. if (!this.diamondLabel) {
  154. this.diamondLabel = this.findLabelByName(this.node, 'DiamondLabel');
  155. if (this.diamondLabel) {
  156. console.log('[GameEnd] 通过递归查找找到diamondLabel');
  157. }
  158. }
  159. }
  160. console.log('[GameEnd] UI组件自动查找完成 - moneyLabel:', !!this.moneyLabel, 'diamondLabel:', !!this.diamondLabel);
  161. // 如果仍然没有找到组件,打印节点结构用于调试
  162. if (!this.moneyLabel || !this.diamondLabel) {
  163. console.warn('[GameEnd] 部分UI组件未找到,打印节点结构用于调试:');
  164. this.printNodeStructure(this.node, 0, 3); // 最多打印3层
  165. }
  166. }
  167. /**
  168. * 递归查找指定名称的Label组件
  169. */
  170. private findLabelByName(node: Node, targetName: string): Label | null {
  171. // 检查当前节点
  172. if (node.name === targetName) {
  173. const label = node.getComponent(Label);
  174. if (label) return label;
  175. }
  176. // 递归检查子节点
  177. for (const child of node.children) {
  178. const result = this.findLabelByName(child, targetName);
  179. if (result) return result;
  180. }
  181. return null;
  182. }
  183. /**
  184. * 打印节点结构用于调试
  185. */
  186. private printNodeStructure(node: Node, depth: number, maxDepth: number) {
  187. if (depth > maxDepth) return;
  188. const indent = ' '.repeat(depth);
  189. const components = node.getComponents(Component).map(c => c.constructor.name).join(', ');
  190. console.log(`${indent}${node.name} [${components || 'No Components'}]`);
  191. for (const child of node.children) {
  192. this.printNodeStructure(child, depth + 1, maxDepth);
  193. }
  194. }
  195. /**
  196. * 绑定按钮事件
  197. */
  198. private bindButtonEvents() {
  199. if (this.doubleButton) {
  200. this.doubleButton.node.on(Button.EventType.CLICK, this.onDoubleButtonClick, this);
  201. }
  202. if (this.continueButton) {
  203. this.continueButton.node.on(Button.EventType.CLICK, this.onContinueButtonClick, this);
  204. }
  205. }
  206. /**
  207. * 设置事件监听器
  208. */
  209. private setupEventListeners() {
  210. console.log('[GameEnd] 开始设置事件监听器');
  211. const eventBus = EventBus.getInstance();
  212. // 监听游戏成功事件
  213. eventBus.on(GameEvents.GAME_SUCCESS, this.onGameSuccess, this);
  214. console.log('[GameEnd] 已注册GAME_SUCCESS事件监听器');
  215. // 监听游戏失败事件
  216. eventBus.on(GameEvents.GAME_DEFEAT, this.onGameDefeat, this);
  217. console.log('[GameEnd] 已注册GAME_DEFEAT事件监听器');
  218. // 监听游戏开始事件
  219. eventBus.on(GameEvents.GAME_START, this.onGameStart, this);
  220. console.log('[GameEnd] 已注册GAME_START事件监听器');
  221. // 监听UI重置事件
  222. eventBus.on(GameEvents.RESET_UI_STATES, this.onResetUI, this);
  223. console.log('[GameEnd] 事件监听器设置完成');
  224. }
  225. /**
  226. * 初始化UI状态
  227. */
  228. private initializeUI() {
  229. // 重置状态
  230. this.hasDoubledReward = false;
  231. this.currentRewards = {money: 0, diamonds: 0};
  232. console.log('[GameEnd] UI状态初始化完成');
  233. }
  234. /**
  235. * 检查当前游戏状态并处理奖励(解决时序问题)
  236. */
  237. private checkAndHandleGameState() {
  238. console.log('[GameEnd] 检查当前游戏状态');
  239. if (!this.inGameManager) {
  240. console.log('[GameEnd] InGameManager未初始化,无法检查游戏状态');
  241. return;
  242. }
  243. const currentState = this.inGameManager.getCurrentState();
  244. console.log('[GameEnd] 当前游戏状态:', currentState);
  245. // 如果游戏已经结束,主动处理奖励
  246. if (currentState === GameState.SUCCESS) {
  247. console.log('[GameEnd] 检测到游戏成功状态,主动处理奖励');
  248. this.isGameSuccess = true;
  249. this.calculateAndShowRewards();
  250. } else if (currentState === GameState.DEFEAT) {
  251. console.log('[GameEnd] 检测到游戏失败状态,主动处理奖励');
  252. this.isGameSuccess = false;
  253. this.calculateAndShowRewards();
  254. }
  255. }
  256. /**
  257. * 处理游戏成功事件
  258. * 统一处理游戏成功逻辑:状态切换 + UI显示 + 奖励计算
  259. */
  260. private onGameSuccess() {
  261. console.log('[GameEnd] 接收到GAME_SUCCESS事件');
  262. console.log('[GameEnd] 游戏成功事件处理,开始统一处理流程');
  263. // 冷却期内拦截重复的胜利判定,避免二次弹窗/奖励
  264. if (this.isInCooldown()) {
  265. console.log('[GameEnd] 冷却期内,忽略重复的GAME_SUCCESS事件');
  266. return;
  267. }
  268. // 1. 清理敌人(阶段一:UI弹出阶段的职责)
  269. this.clearAllEnemies();
  270. // 2. 设置游戏状态为成功(如果还未设置)
  271. if (this.inGameManager && this.inGameManager.getCurrentState() !== GameState.SUCCESS) {
  272. this.inGameManager.setCurrentState(GameState.SUCCESS);
  273. console.log('[GameEnd] 已将游戏状态切换为SUCCESS');
  274. }
  275. // 3. 播放游戏成功音效
  276. Audio.playUISound('data/弹球音效/win');
  277. console.log('[GameEnd] 已播放游戏成功音效');
  278. // 4. 设置EndLabel文本
  279. this.setEndLabelText('SUCCESS');
  280. // 5. 设置成功标志
  281. this.isGameSuccess = true;
  282. // 6. 计算和显示奖励(包含面板动画显示)
  283. this.calculateAndShowRewards();
  284. }
  285. /**
  286. * 处理游戏失败事件
  287. * 统一处理游戏失败逻辑:状态切换 + UI显示 + 奖励计算
  288. */
  289. private onGameDefeat() {
  290. console.log('[GameEnd] 接收到GAME_DEFEAT事件');
  291. console.log('[GameEnd] 游戏失败事件处理,开始统一处理流程');
  292. // 冷却期内拦截重复的失败判定,避免二次弹窗/奖励
  293. if (this.isInCooldown()) {
  294. console.log('[GameEnd] 冷却期内,忽略重复的GAME_DEFEAT事件');
  295. return;
  296. }
  297. // 1. 清理敌人(阶段一:UI弹出阶段的职责)
  298. this.clearAllEnemies();
  299. // 2. 设置游戏状态为失败(如果还未设置)
  300. if (this.inGameManager && this.inGameManager.getCurrentState() !== GameState.DEFEAT) {
  301. this.inGameManager.setCurrentState(GameState.DEFEAT);
  302. console.log('[GameEnd] 已将游戏状态切换为DEFEAT');
  303. }
  304. // 3. 播放游戏失败音效
  305. Audio.playUISound('data/弹球音效/lose');
  306. console.log('[GameEnd] 已播放游戏失败音效');
  307. // 4. 设置EndLabel文本
  308. this.setEndLabelText('DEFEAT');
  309. // 5. 设置失败标志
  310. this.isGameSuccess = false;
  311. // 6. 计算和显示奖励(包含面板动画显示)
  312. this.calculateAndShowRewards();
  313. }
  314. /**
  315. * 游戏开始处理
  316. */
  317. private onGameStart() {
  318. console.log('[GameEnd] 收到游戏开始事件,重置奖励显示');
  319. // 重置奖励显示,为新游戏做准备
  320. this.currentRewards = {money: 0, diamonds: 0};
  321. this.hasProcessedGameEnd = false;
  322. this.hasDoubledReward = false;
  323. this.isGameSuccess = false;
  324. }
  325. /**
  326. * 计算并显示奖励
  327. */
  328. private async calculateAndShowRewards() {
  329. console.log('[GameEnd] 开始计算并显示奖励');
  330. // 防止重复计算奖励(使用专用标志判断)
  331. if (this.hasProcessedGameEnd) {
  332. console.log('[GameEnd] 游戏结束已处理过,跳过重复计算,直接显示面板');
  333. this.updateRewardDisplay();
  334. // 确保面板显示
  335. this.showEndPanelWithAnimation();
  336. return;
  337. }
  338. // 标记为已处理,防止重复执行
  339. this.hasProcessedGameEnd = true;
  340. if (!this.saveDataManager) {
  341. console.error('[GameEnd] SaveDataManager未初始化');
  342. return;
  343. }
  344. const currentLevel = this.saveDataManager.getCurrentLevel();
  345. console.log(`[GameEnd] 当前关卡: ${currentLevel}, 游戏成功: ${this.isGameSuccess}`);
  346. try {
  347. if (this.isGameSuccess) {
  348. // 游戏成功,先完成关卡(更新maxUnlockedLevel),再给予奖励
  349. console.log('[GameEnd] 准备完成关卡并给予成功奖励');
  350. // 获取游戏时长
  351. const gameTime = this.inGameManager ? Math.floor(this.inGameManager.getGameDuration() / 1000) : 0;
  352. console.log(`[GameEnd] 游戏时长: ${gameTime}秒`);
  353. // 先调用completeLevel更新maxUnlockedLevel,这样武器解锁机制才能正常工作
  354. this.saveDataManager.completeLevel(currentLevel, 0, gameTime);
  355. console.log('[GameEnd] 已完成关卡,maxUnlockedLevel已更新');
  356. // 然后给予奖励
  357. await this.saveDataManager.giveCompletionRewards(currentLevel);
  358. console.log('[GameEnd] 已给予成功奖励');
  359. } else {
  360. // 游戏失败,给予按比例奖励
  361. const totalWaves = this.inGameManager?.levelWaves?.length || 1;
  362. const completedWaves = this.inGameManager ? Math.max(0, this.inGameManager.getCurrentWave() - 1) : 0;
  363. console.log(`[GameEnd] 准备给予失败奖励,完成波数: ${completedWaves}/${totalWaves}`);
  364. await this.saveDataManager.giveFailureRewards(currentLevel, completedWaves, totalWaves);
  365. console.log(`[GameEnd] 已给予失败奖励,完成波数: ${completedWaves}/${totalWaves}`);
  366. }
  367. // 获取奖励数据并显示
  368. this.currentRewards = this.saveDataManager.getLastRewards();
  369. console.log('[GameEnd] 获取到的奖励数据:', this.currentRewards);
  370. console.log('[GameEnd] 奖励计算完成');
  371. this.updateRewardDisplay();
  372. // 显示结算面板(通过动画)
  373. this.showEndPanelWithAnimation();
  374. } catch (error) {
  375. console.error('[GameEnd] 计算奖励时出错:', error);
  376. }
  377. }
  378. /**
  379. * 更新奖励显示
  380. */
  381. private updateRewardDisplay() {
  382. console.log(`[GameEnd] 开始更新奖励显示 - 钞票: ${this.currentRewards.money}, 钻石: ${this.currentRewards.diamonds}`);
  383. // 检查moneyLabel绑定状态
  384. if (this.moneyLabel) {
  385. this.moneyLabel.string = this.currentRewards.money.toString();
  386. console.log(`[GameEnd] 钞票标签已更新: ${this.currentRewards.money}`);
  387. } else {
  388. console.error('[GameEnd] moneyLabel未绑定!请在编辑器中将Canvas/GameEnd/ResourceNode/MoneyResourceNode/MoneyLabel拖拽到GameEnd组件的moneyLabel属性');
  389. }
  390. // 检查diamondLabel绑定状态
  391. if (this.diamondLabel) {
  392. this.diamondLabel.string = this.currentRewards.diamonds.toString();
  393. console.log(`[GameEnd] 钻石标签已更新: ${this.currentRewards.diamonds}`);
  394. } else {
  395. console.error('[GameEnd] diamondLabel未绑定!请在编辑器中将Canvas/GameEnd/ResourceNode/DiamondResourceNode/DiamondLabel拖拽到GameEnd组件的diamondLabel属性');
  396. }
  397. console.log(`[GameEnd] 奖励显示更新完成`);
  398. }
  399. /**
  400. * 显示结算面板(通过动画)
  401. */
  402. private showEndPanelWithAnimation() {
  403. console.log('[GameEnd] 开始显示结算面板动画');
  404. // 若处于冷却期,直接跳过显示,避免重复弹窗
  405. if (this.isInCooldown()) {
  406. console.log('[GameEnd] 弹窗处于冷却期,跳过本次显示');
  407. return;
  408. }
  409. // 确保节点处于激活状态
  410. if (this.node && !this.node.active) {
  411. this.node.active = true;
  412. console.log('[GameEnd] 激活GameEnd节点');
  413. }
  414. // 重置双倍奖励状态
  415. this.hasDoubledReward = false;
  416. if (this.doubleButton) {
  417. this.doubleButton.interactable = true;
  418. }
  419. // 确保有UIOpacity组件
  420. if (this.node) {
  421. let uiOpacity = this.node.getComponent(UIOpacity);
  422. if (!uiOpacity) {
  423. uiOpacity = this.node.addComponent(UIOpacity);
  424. console.log('[GameEnd] 在showEndPanelWithAnimation中添加UIOpacity组件');
  425. }
  426. }
  427. // 记录弹窗时间戳(无论是否播放动画,都计入冷却)
  428. GameEnd.lastPopupTimestamp = Date.now();
  429. // 播放显示动画(如果有的话)
  430. if (this.animationDuration > 0) {
  431. console.log(`[GameEnd] 动画持续时间: ${this.animationDuration}秒,开始播放GameEnd面板弹出动画`);
  432. this.playShowAnimation();
  433. } else {
  434. // 如果没有动画,直接显示
  435. if (this.node) {
  436. this.node.setScale(1, 1, 1);
  437. const uiOpacity = this.node.getComponent(UIOpacity);
  438. if (uiOpacity) {
  439. uiOpacity.opacity = 255;
  440. }
  441. }
  442. console.log('[GameEnd] 无动画配置,直接显示面板');
  443. }
  444. console.log('[GameEnd] GameEnd面板显示流程完成');
  445. }
  446. /**
  447. * 播放显示动画
  448. */
  449. private playShowAnimation() {
  450. if (!this.node) {
  451. console.error('[GameEnd] playShowAnimation: 节点不存在');
  452. return;
  453. }
  454. console.log('[GameEnd] 开始播放GameEnd面板弹出动画');
  455. // 停止所有正在进行的动画
  456. this.stopAllAnimations();
  457. // 设置节点位置到屏幕中心
  458. this.centerNodeOnScreen();
  459. // 确保有UIOpacity组件
  460. let uiOpacity = this.node.getComponent(UIOpacity);
  461. if (!uiOpacity) {
  462. uiOpacity = this.node.addComponent(UIOpacity);
  463. console.log('[GameEnd] 添加UIOpacity组件');
  464. }
  465. // 设置初始状态
  466. this.node.setScale(0, 0, 1);
  467. uiOpacity.opacity = 0;
  468. console.log('[GameEnd] 设置动画初始状态 - 缩放: 0, 透明度: 0');
  469. // 播放缩放和淡入动画
  470. tween(this.node)
  471. .to(this.animationDuration, {
  472. scale: new Vec3(1, 1, 1)
  473. }, {
  474. easing: 'backOut'
  475. })
  476. .call(() => {
  477. console.log('[GameEnd] GameEnd面板缩放动画完成');
  478. })
  479. .start();
  480. tween(uiOpacity)
  481. .to(this.animationDuration, {
  482. opacity: 255
  483. })
  484. .call(() => {
  485. console.log('[GameEnd] GameEnd面板淡入动画完成');
  486. })
  487. .start();
  488. console.log('[GameEnd] GameEnd面板弹出动画开始执行');
  489. }
  490. /**
  491. * 将节点居中到屏幕中央
  492. */
  493. private centerNodeOnScreen() {
  494. if (!this.node) return;
  495. // 获取Canvas节点
  496. const canvas = find('Canvas');
  497. if (!canvas) {
  498. console.warn('[GameEnd] 未找到Canvas节点');
  499. return;
  500. }
  501. // 设置位置为(0, 0),这在Canvas坐标系中是屏幕中心
  502. this.node.setPosition(0, 0, 0);
  503. console.log('[GameEnd] 已将面板居中到屏幕中央');
  504. }
  505. /**
  506. * 双倍按钮点击事件
  507. */
  508. private onDoubleButtonClick() {
  509. // 播放UI点击音效
  510. Audio.playUISound('data/弹球音效/ui play');
  511. if (this.hasDoubledReward) {
  512. console.log('[GameEnd] 已经获得过双倍奖励');
  513. return;
  514. }
  515. console.log('[GameEnd] 点击双倍奖励按钮');
  516. // 若广告请求正在进行,拦截重复点击(按钮保持不置灰)
  517. if (this.isAdRequestPending) {
  518. console.log('[GameEnd] 广告请求进行中,忽略重复点击');
  519. return;
  520. }
  521. // 标记广告请求进行中
  522. this.isAdRequestPending = true;
  523. // 显示激励视频广告
  524. AdManager.getInstance().showRewardedVideoAd(
  525. () => {
  526. // 广告观看完成,给予双倍奖励
  527. this.giveDoubleReward();
  528. // 广告流程完成,解除请求中状态
  529. this.isAdRequestPending = false;
  530. GameEnd.lastPopupTimestamp = Date.now();
  531. },
  532. (error) => {
  533. console.error('[GameEnd] 广告显示失败:', error);
  534. // 广告失败:不返回主界面,保留当前结算面板供用户选择继续
  535. // 解除请求中状态,按钮不置灰,允许用户再次尝试
  536. this.isAdRequestPending = false;
  537. GameEnd.lastPopupTimestamp = Date.now();
  538. }
  539. );
  540. }
  541. /**
  542. * 给予双倍奖励
  543. */
  544. private giveDoubleReward() {
  545. if (!this.saveDataManager || this.hasDoubledReward) {
  546. return;
  547. }
  548. // 计算双倍奖励
  549. const doubleMoney = this.currentRewards.money;
  550. const doubleDiamonds = this.currentRewards.diamonds;
  551. // 添加额外奖励到玩家账户
  552. if (doubleMoney > 0) {
  553. this.saveDataManager.addMoney(doubleMoney, 'double_reward');
  554. }
  555. if (doubleDiamonds > 0) {
  556. this.saveDataManager.addDiamonds(doubleDiamonds, 'double_reward');
  557. }
  558. // 更新当前奖励显示(显示双倍后的数值)
  559. this.currentRewards.money += doubleMoney;
  560. this.currentRewards.diamonds += doubleDiamonds;
  561. this.updateRewardDisplay();
  562. // 标记已获得双倍奖励
  563. this.hasDoubledReward = true;
  564. // 禁用双倍按钮
  565. if (this.doubleButton) {
  566. this.doubleButton.interactable = false;
  567. }
  568. console.log(`[GameEnd] 双倍奖励已给予 - 额外钞票: ${doubleMoney}, 额外钻石: ${doubleDiamonds}`);
  569. // 触发货币变化事件
  570. EventBus.getInstance().emit(GameEvents.CURRENCY_CHANGED);
  571. // 不自动返回主页面,不隐藏结算面板;由用户点击“继续”按钮决定返回
  572. }
  573. /**
  574. * 继续按钮点击事件
  575. */
  576. private onContinueButtonClick() {
  577. // 播放UI点击音效
  578. Audio.playUISound('data/弹球音效/ui play');
  579. console.log('[GameEnd] 点击继续按钮');
  580. // 触发返回主菜单事件(奖励动画由主界面统一播放)
  581. EventBus.getInstance().emit('CONTINUE_CLICK');
  582. // 隐藏结算面板(通过动画)
  583. this.hideEndPanelWithAnimation();
  584. }
  585. /**
  586. * 隐藏结算面板(通过动画)
  587. */
  588. private async hideEndPanelWithAnimation() {
  589. // 播放隐藏动画
  590. await this.fadeOutWithScale();
  591. console.log('[GameEnd] 结算面板已通过动画隐藏');
  592. }
  593. /**
  594. * 重置UI状态
  595. */
  596. private onResetUI() {
  597. console.log('[GameEnd] 重置UI状态');
  598. // 停止所有动画
  599. this.stopAllAnimations();
  600. // 直接重置到隐藏状态,不使用动画
  601. this.initializeHiddenState();
  602. // 重置所有状态,为下一局游戏做准备
  603. this.hasDoubledReward = false;
  604. // 注意:不重置currentRewards,保持奖励显示直到下次游戏开始
  605. // this.currentRewards = {money: -1, diamonds: -1}; // 移除这行,避免重置奖励显示
  606. this.isGameSuccess = false;
  607. this.hasProcessedGameEnd = false; // 重置处理状态标志
  608. // 重置按钮状态
  609. if (this.doubleButton) {
  610. this.doubleButton.interactable = true;
  611. }
  612. // 清理广告请求状态,避免跨局残留
  613. this.isAdRequestPending = false;
  614. console.log('[GameEnd] UI状态重置完成');
  615. }
  616. /**
  617. * 设置EndLabel文本
  618. * @param text 要显示的文本内容
  619. */
  620. private setEndLabelText(text: string) {
  621. if (this.endLabel) {
  622. this.endLabel.string = text;
  623. console.log(`[GameEnd] 已设置EndLabel文本为: ${text}`);
  624. } else {
  625. // 如果endLabel未绑定,尝试通过路径查找
  626. const endLabelNode = find('Canvas/GameEnd/Sprite/EndLabel');
  627. if (endLabelNode) {
  628. const labelComponent = endLabelNode.getComponent(Label);
  629. if (labelComponent) {
  630. labelComponent.string = text;
  631. console.log(`[GameEnd] 通过路径查找设置EndLabel文本为: ${text}`);
  632. } else {
  633. console.warn('[GameEnd] 找到EndLabel节点但无Label组件');
  634. }
  635. } else {
  636. console.warn('[GameEnd] 未找到EndLabel节点,请检查路径: Canvas/GameEnd/Sprite/EndLabel');
  637. }
  638. }
  639. }
  640. /**
  641. * 获取当前奖励信息(用于外部查询)
  642. */
  643. public getCurrentRewards(): {money: number, diamonds: number} {
  644. return {...this.currentRewards};
  645. }
  646. /**
  647. * 检查是否已获得双倍奖励
  648. */
  649. public hasGotDoubleReward(): boolean {
  650. return this.hasDoubledReward;
  651. }
  652. // === 动画方法 ===
  653. /**
  654. * 初始化为隐藏状态
  655. */
  656. private initializeHiddenState() {
  657. // 设置初始透明度为0
  658. if (this.node) {
  659. let uiOpacity = this.node.getComponent(UIOpacity);
  660. if (!uiOpacity) {
  661. uiOpacity = this.node.addComponent(UIOpacity);
  662. console.log('[GameEnd] 添加UIOpacity组件到节点');
  663. }
  664. uiOpacity.opacity = 0;
  665. }
  666. // 设置初始缩放为0(完全隐藏,恢复到历史版本实现)
  667. if (this.node) {
  668. this.node.setScale(0, 0, 1);
  669. }
  670. console.log('[GameEnd] 初始化为隐藏状态 - 缩放: 0, 透明度: 0');
  671. }
  672. /**
  673. * 初始化为隐藏状态但保持节点激活(用于游戏开始时)
  674. */
  675. private initializeHiddenStateKeepActive() {
  676. // 设置初始透明度为0
  677. if (this.node) {
  678. let uiOpacity = this.node.getComponent(UIOpacity);
  679. if (!uiOpacity) {
  680. uiOpacity = this.node.addComponent(UIOpacity);
  681. console.log('[GameEnd] 添加UIOpacity组件到节点');
  682. }
  683. uiOpacity.opacity = 0;
  684. }
  685. // 设置初始缩放为0(完全隐藏,恢复到历史版本实现)
  686. if (this.node) {
  687. this.node.setScale(0, 0, 1);
  688. }
  689. console.log('[GameEnd] 初始化为隐藏状态(保持激活) - 缩放: 0, 透明度: 0');
  690. }
  691. /**
  692. * 淡入动画
  693. */
  694. public fadeIn(target?: Node, duration?: number): Promise<void> {
  695. const animTarget = target || this.node;
  696. const animDuration = duration !== undefined ? duration : this.animationDuration;
  697. if (!animTarget) {
  698. console.warn('[GameEnd] fadeIn: 未指定目标节点');
  699. return Promise.resolve();
  700. }
  701. return new Promise<void>((resolve) => {
  702. let uiOpacity = animTarget.getComponent(UIOpacity);
  703. if (!uiOpacity) {
  704. uiOpacity = animTarget.addComponent(UIOpacity);
  705. }
  706. Tween.stopAllByTarget(uiOpacity);
  707. uiOpacity.opacity = 0;
  708. tween(uiOpacity)
  709. .to(animDuration, { opacity: 255 }, { easing: 'quadOut' })
  710. .call(() => {
  711. resolve();
  712. })
  713. .start();
  714. });
  715. }
  716. /**
  717. * 淡出动画
  718. */
  719. public fadeOut(target?: Node, duration?: number): Promise<void> {
  720. const animTarget = target || this.node;
  721. const animDuration = duration !== undefined ? duration : this.animationDuration;
  722. if (!animTarget) {
  723. console.warn('[GameEnd] fadeOut: 未指定目标节点');
  724. return Promise.resolve();
  725. }
  726. return new Promise<void>((resolve) => {
  727. let uiOpacity = animTarget.getComponent(UIOpacity);
  728. if (!uiOpacity) {
  729. uiOpacity = animTarget.addComponent(UIOpacity);
  730. }
  731. Tween.stopAllByTarget(uiOpacity);
  732. tween(uiOpacity)
  733. .to(animDuration, { opacity: 0 }, { easing: 'quadIn' })
  734. .call(() => {
  735. resolve();
  736. })
  737. .start();
  738. });
  739. }
  740. /**
  741. * 缩放弹出动画
  742. */
  743. public scalePopIn(target?: Node, duration?: number): Promise<void> {
  744. const animTarget = target || this.node;
  745. const animDuration = duration !== undefined ? duration : this.animationDuration;
  746. if (!animTarget) {
  747. console.warn('[GameEnd] scalePopIn: 未指定目标节点');
  748. return Promise.resolve();
  749. }
  750. return new Promise<void>((resolve) => {
  751. Tween.stopAllByTarget(animTarget);
  752. animTarget.setScale(0, 0, 1);
  753. tween(animTarget)
  754. .to(animDuration, { scale: this._originalScale }, { easing: 'backOut' })
  755. .call(() => {
  756. resolve();
  757. })
  758. .start();
  759. });
  760. }
  761. /**
  762. * 缩放收缩动画
  763. */
  764. public scalePopOut(target?: Node, duration?: number): Promise<void> {
  765. const animTarget = target || this.node;
  766. const animDuration = duration !== undefined ? duration : this.animationDuration;
  767. if (!animTarget) {
  768. console.warn('[GameEnd] scalePopOut: 未指定目标节点');
  769. return Promise.resolve();
  770. }
  771. return new Promise<void>((resolve) => {
  772. Tween.stopAllByTarget(animTarget);
  773. tween(animTarget)
  774. .to(animDuration, { scale: new Vec3(0, 0, 1) }, { easing: 'backIn' })
  775. .call(() => {
  776. resolve();
  777. })
  778. .start();
  779. });
  780. }
  781. /**
  782. * 组合动画:淡入 + 缩放弹出
  783. */
  784. public async fadeInWithScale(fadeTarget?: Node, scaleTarget?: Node, duration?: number): Promise<void> {
  785. const promises: Promise<void>[] = [];
  786. if (fadeTarget || this.node) {
  787. promises.push(this.fadeIn(fadeTarget, duration));
  788. }
  789. if (scaleTarget || this.node) {
  790. promises.push(this.scalePopIn(scaleTarget, duration));
  791. }
  792. await Promise.all(promises);
  793. }
  794. /**
  795. * 组合动画:淡出 + 缩放收缩
  796. */
  797. public async fadeOutWithScale(fadeTarget?: Node, scaleTarget?: Node, duration?: number): Promise<void> {
  798. const promises: Promise<void>[] = [];
  799. if (fadeTarget || this.node) {
  800. promises.push(this.fadeOut(fadeTarget, duration));
  801. }
  802. if (scaleTarget || this.node) {
  803. promises.push(this.scalePopOut(scaleTarget, duration));
  804. }
  805. await Promise.all(promises);
  806. }
  807. /**
  808. * 停止所有动画
  809. */
  810. public stopAllAnimations() {
  811. if (this.node) {
  812. // 停止所有Tween动画
  813. Tween.stopAllByTarget(this.node);
  814. // 停止UIOpacity组件的动画
  815. const uiOpacity = this.node.getComponent(UIOpacity);
  816. if (uiOpacity) {
  817. Tween.stopAllByTarget(uiOpacity);
  818. }
  819. console.log('[GameEnd] 已停止所有动画');
  820. } else {
  821. console.warn('[GameEnd] stopAllAnimations: 节点不存在');
  822. }
  823. }
  824. /**
  825. * 清理所有敌人
  826. * 在游戏结束时调用,符合阶段一UI弹出阶段的职责
  827. */
  828. /**
  829. * 通过事件系统清理所有敌人
  830. * 使用CLEAR_ALL_ENEMIES事件,避免直接引用EnemyController
  831. */
  832. private clearAllEnemies() {
  833. console.log('[GameEnd] 游戏结束,派发CLEAR_ALL_ENEMIES事件清理所有敌人');
  834. try {
  835. // 通过事件总线派发清理敌人事件
  836. EventBus.getInstance().emit(GameEvents.CLEAR_ALL_ENEMIES);
  837. console.log('[GameEnd] 已派发CLEAR_ALL_ENEMIES事件,统一清理敌人');
  838. } catch (error) {
  839. console.error('[GameEnd] 派发清理敌人事件时发生错误:', error);
  840. }
  841. }
  842. /**
  843. * 重置所有目标节点到初始状态
  844. */
  845. public resetToInitialState() {
  846. this.stopAllAnimations();
  847. if (this.node) {
  848. let uiOpacity = this.node.getComponent(UIOpacity);
  849. if (uiOpacity) {
  850. uiOpacity.opacity = 255;
  851. }
  852. this.node.setScale(this._originalScale);
  853. }
  854. }
  855. onDisable() {
  856. console.log('[GameEnd] onDisable方法被调用,节点已禁用');
  857. // 停止所有动画
  858. this.stopAllAnimations();
  859. // 清理事件监听
  860. const eventBus = EventBus.getInstance();
  861. eventBus.off(GameEvents.GAME_SUCCESS, this.onGameSuccess, this);
  862. eventBus.off(GameEvents.GAME_DEFEAT, this.onGameDefeat, this);
  863. eventBus.off(GameEvents.GAME_START, this.onGameStart, this);
  864. eventBus.off(GameEvents.RESET_UI_STATES, this.onResetUI, this);
  865. console.log('[GameEnd] 事件监听器已清理');
  866. }
  867. protected onDestroy() {
  868. // 停止所有动画
  869. this.stopAllAnimations();
  870. // 清理事件监听
  871. const eventBus = EventBus.getInstance();
  872. eventBus.off(GameEvents.GAME_SUCCESS, this.onGameSuccess, this);
  873. eventBus.off(GameEvents.GAME_DEFEAT, this.onGameDefeat, this);
  874. eventBus.off(GameEvents.GAME_START, this.onGameStart, this);
  875. eventBus.off(GameEvents.RESET_UI_STATES, this.onResetUI, this);
  876. // 清理按钮事件
  877. if (this.doubleButton) {
  878. this.doubleButton.node.off(Button.EventType.CLICK, this.onDoubleButtonClick, this);
  879. }
  880. if (this.continueButton) {
  881. this.continueButton.node.off(Button.EventType.CLICK, this.onContinueButtonClick, this);
  882. }
  883. }
  884. }