Browse Source

结算优化

181404010226 3 months ago
parent
commit
b12a16e376

+ 5 - 25
assets/Scenes/GameLevel.scene

@@ -21579,7 +21579,7 @@
     },
     "_type": 0,
     "_fillType": 0,
-    "_sizeMode": 1,
+    "_sizeMode": 0,
     "_fillCenter": {
       "__type__": "cc.Vec2",
       "x": 0,
@@ -24261,20 +24261,6 @@
         "__id__": 568
       }
     ],
-    "buttonBorders": [
-      {
-        "__id__": 524
-      },
-      {
-        "__id__": 545
-      },
-      {
-        "__id__": 560
-      },
-      {
-        "__id__": 575
-      }
-    ],
     "_id": "36dzJKHrFHeKs4x1Ej9Cci"
   },
   {
@@ -25256,9 +25242,6 @@
     "panelUpgradeBtn": {
       "__id__": 740
     },
-    "upIcon": {
-      "__id__": 734
-    },
     "lockedWeaponPrefab": {
       "__uuid__": "c407e531-c29c-40bb-bb9e-9b214d4aaf9d",
       "__expectedType__": "cc.Prefab"
@@ -25267,9 +25250,6 @@
       "__uuid__": "c0aaf9ac-7a94-4173-9f31-e357f18848a4",
       "__expectedType__": "cc.Prefab"
     },
-    "panelAni": {
-      "__id__": 743
-    },
     "upgradeAni": {
       "__id__": 742
     },
@@ -29867,7 +29847,7 @@
     "__prefab": null,
     "_contentSize": {
       "__type__": "cc.Size",
-      "width": 104.8984375,
+      "width": 114.8740234375,
       "height": 54.4
     },
     "_anchorPoint": {
@@ -29897,7 +29877,7 @@
       "b": 255,
       "a": 255
     },
-    "_string": "+1  Ball",
+    "_string": "+1  小球",
     "_horizontalAlign": 1,
     "_verticalAlign": 1,
     "_actualFontSize": 30,
@@ -31006,7 +30986,7 @@
     "__prefab": null,
     "_contentSize": {
       "__type__": "cc.Size",
-      "width": 102.3349609375,
+      "width": 72.3349609375,
       "height": 54.4
     },
     "_anchorPoint": {
@@ -31036,7 +31016,7 @@
       "b": 255,
       "a": 255
     },
-    "_string": "Re--roll",
+    "_string": "刷 新",
     "_horizontalAlign": 1,
     "_verticalAlign": 1,
     "_actualFontSize": 30,

+ 6 - 6
assets/resources/data/levels/Level1.json

@@ -21,7 +21,7 @@
       "enemies": [
         {
           "enemyType": "normal_zombie",
-          "count": 10,
+          "count": 1,
           "spawnInterval": 2.0,
           "spawnDelay": 0.0,
           "characteristics": "中速移动, 无技能"
@@ -45,7 +45,7 @@
       "enemies": [
         {
           "enemyType": "normal_zombie",
-          "count": 20,
+          "count": 2,
           "spawnInterval": 1.0,
           "spawnDelay": 0.0,
           "characteristics": "中速移动, 无技能"
@@ -64,14 +64,14 @@
       "enemies": [
         {
           "enemyType": "normal_zombie",
-          "count": 30,
+          "count": 3,
           "spawnInterval": 1.0,
           "spawnDelay": 0.0,
           "characteristics": "中速移动, 无技能"
         },
         {
           "enemyType": "roadblock_zombie",
-          "count": 10,
+          "count": 1,
           "spawnInterval": 2.0,
           "spawnDelay": 0.0,
           "characteristics": "高生命, 慢速移动"
@@ -83,14 +83,14 @@
       "enemies": [
         {
           "enemyType": "normal_zombie",
-          "count": 30,
+          "count": 3,
           "spawnInterval": 1.0,
           "spawnDelay": 0.0,
           "characteristics": "中速移动, 无技能"
         },
         {
           "enemyType": "roadblock_zombie",
-          "count": 30,
+          "count": 3,
           "spawnInterval": 1.0,
           "spawnDelay": 0.0,
           "characteristics": "高生命, 慢速移动"

BIN
assets/scripts/CombatSystem/.GameEnd.ts.swp


+ 11 - 0
assets/scripts/CombatSystem/EnemyController.ts

@@ -214,6 +214,7 @@ export class EnemyController extends BaseSingleton {
         
         // 监听重置相关事件
         eventBus.on(GameEvents.CLEAR_ALL_GAME_OBJECTS, this.onClearAllGameObjectsEvent, this);
+        eventBus.on(GameEvents.CLEAR_ALL_ENEMIES, this.onClearAllEnemiesEvent, this);
         eventBus.on(GameEvents.RESET_ENEMY_CONTROLLER, this.onResetEnemyControllerEvent, this);
         
         // 监听伤害事件
@@ -285,6 +286,15 @@ export class EnemyController extends BaseSingleton {
         this.clearAllEnemies(false); // 清除敌人但不触发事件
     }
     
+    /**
+     * 处理清除所有敌人事件
+     * 专门响应CLEAR_ALL_ENEMIES事件,用于游戏结束时的敌人清理
+     */
+    private onClearAllEnemiesEvent() {
+        console.log('[EnemyController] 接收到CLEAR_ALL_ENEMIES事件,开始清理所有敌人');
+        this.clearAllEnemies(false); // 清除敌人但不触发事件,避免循环
+    }
+    
     /**
      * 处理重置敌人控制器事件
      */
@@ -1192,6 +1202,7 @@ export class EnemyController extends BaseSingleton {
         eventBus.off(GameEvents.GAME_SUCCESS, this.onGameEndEvent, this);
         eventBus.off(GameEvents.GAME_DEFEAT, this.onGameEndEvent, this);
         eventBus.off(GameEvents.CLEAR_ALL_GAME_OBJECTS, this.onClearAllGameObjectsEvent, this);
+        eventBus.off(GameEvents.CLEAR_ALL_ENEMIES, this.onClearAllEnemiesEvent, this);
         eventBus.off(GameEvents.RESET_ENEMY_CONTROLLER, this.onResetEnemyControllerEvent, this);
         
         // 清理新增的事件监听

+ 191 - 41
assets/scripts/CombatSystem/GameEnd.ts

@@ -60,9 +60,10 @@ export class GameEnd extends Component {
     
     // === 私有属性 ===
     private saveDataManager: SaveDataManager = null;
-    public currentRewards: {money: number, diamonds: number};
+    public currentRewards: {money: number, diamonds: number} = {money: -1, diamonds: -1};
     private hasDoubledReward: boolean = false;
     private isGameSuccess: boolean = false;
+    private hasProcessedGameEnd: boolean = false; // 添加处理状态标志
 
     onLoad() {
         console.log('[GameEnd] onLoad方法被调用');
@@ -77,12 +78,13 @@ export class GameEnd extends Component {
             this._originalScale.set(this.node.scale);
         }
         
-        // 初始化为隐藏状态(通过动画实现)
-        this.initializeHiddenState();
+        // 设置事件监听器
+        this.setupEventListeners();
+        
+        // 初始化为隐藏状态(但保持节点激活以便接收事件)
+        this.initializeHiddenStateKeepActive();
 
-        // 监听游戏事件
-        // start方法只在节点首次激活时调用一次
-        // 如果节点初始状态为false,start不会被调用
+        console.log('[GameEnd] start方法完成,面板已初始化为隐藏状态');
     }
     
     onEnable() {
@@ -249,6 +251,10 @@ export class GameEnd extends Component {
         eventBus.on(GameEvents.GAME_DEFEAT, this.onGameDefeat, this);
         console.log('[GameEnd] 已注册GAME_DEFEAT事件监听器');
         
+        // 监听游戏开始事件
+        eventBus.on(GameEvents.GAME_START, this.onGameStart, this);
+        console.log('[GameEnd] 已注册GAME_START事件监听器');
+        
         // 监听UI重置事件
         eventBus.on(GameEvents.RESET_UI_STATES, this.onResetUI, this);
         console.log('[GameEnd] 事件监听器设置完成');
@@ -297,26 +303,28 @@ export class GameEnd extends Component {
      */
     private onGameSuccess() {
         console.log('[GameEnd] 接收到GAME_SUCCESS事件');
-        console.log('[GameEnd] 游戏成功,开始统一处理流程');
+        console.log('[GameEnd] 游戏成功事件处理,开始统一处理流程');
         
-        // 1. 设置游戏状态为成功(如果还未设置)
+        // 1. 清理敌人(阶段一:UI弹出阶段的职责)
+        this.clearAllEnemies();
+        
+        // 2. 设置游戏状态为成功(如果还未设置)
         if (this.inGameManager && this.inGameManager.getCurrentState() !== GameState.SUCCESS) {
             this.inGameManager.setCurrentState(GameState.SUCCESS);
             console.log('[GameEnd] 已将游戏状态切换为SUCCESS');
         }
         
-        // 2. 播放游戏成功音效
+        // 3. 播放游戏成功音效
         Audio.playUISound('data/弹球音效/win');
         console.log('[GameEnd] 已播放游戏成功音效');
         
-        // 3. 设置EndLabel文本
+        // 4. 设置EndLabel文本
         this.setEndLabelText('SUCCESS');
         
-        // 4. 显示UI面板
-        this.showEndPanelWithAnimation();
-        
-        // 5. 计算和显示奖励
+        // 5. 设置成功标志
         this.isGameSuccess = true;
+        
+        // 6. 计算和显示奖励(包含面板动画显示)
         this.calculateAndShowRewards();
     }
     
@@ -326,34 +334,61 @@ export class GameEnd extends Component {
      */
     private onGameDefeat() {
         console.log('[GameEnd] 接收到GAME_DEFEAT事件');
+        console.log('[GameEnd] 游戏失败事件处理,开始统一处理流程');
         
-        // 1. 设置游戏状态为失败(如果还未设置)
+        // 1. 清理敌人(阶段一:UI弹出阶段的职责)
+        this.clearAllEnemies();
+        
+        // 2. 设置游戏状态为失败(如果还未设置)
         if (this.inGameManager && this.inGameManager.getCurrentState() !== GameState.DEFEAT) {
             this.inGameManager.setCurrentState(GameState.DEFEAT);
             console.log('[GameEnd] 已将游戏状态切换为DEFEAT');
         }
         
-        // 2. 播放游戏失败音效
+        // 3. 播放游戏失败音效
         Audio.playUISound('data/弹球音效/lose');
         console.log('[GameEnd] 已播放游戏失败音效');
         
-        // 3. 设置EndLabel文本
+        // 4. 设置EndLabel文本
         this.setEndLabelText('DEFEAT');
         
-        // 4. 显示UI面板
-        this.showEndPanelWithAnimation();
-        
-        // 5. 计算和显示奖励
+        // 5. 设置失败标志
         this.isGameSuccess = false;
+        
+        // 6. 计算和显示奖励(包含面板动画显示)
         this.calculateAndShowRewards();
     }
     
+    /**
+     * 游戏开始处理
+     */
+    private onGameStart() {
+        console.log('[GameEnd] 收到游戏开始事件,重置奖励显示');
+        // 重置奖励显示,为新游戏做准备
+        this.currentRewards = {money: 0, diamonds: 0};
+        this.hasProcessedGameEnd = false;
+        this.hasDoubledReward = false;
+        this.isGameSuccess = false;
+    }
+    
     /**
      * 计算并显示奖励
      */
     private async calculateAndShowRewards() {
         console.log('[GameEnd] 开始计算并显示奖励');
         
+        // 防止重复计算奖励(使用专用标志判断)
+        if (this.hasProcessedGameEnd) {
+            console.log('[GameEnd] 游戏结束已处理过,跳过重复计算,直接显示面板');
+            this.updateRewardDisplay();
+            // 确保面板显示
+            this.showEndPanelWithAnimation();
+            return;
+        }
+        
+        // 标记为已处理,防止重复执行
+        this.hasProcessedGameEnd = true;
+        
         if (!this.saveDataManager) {
             console.error('[GameEnd] SaveDataManager未初始化');
             return;
@@ -381,6 +416,7 @@ export class GameEnd extends Component {
             // 获取奖励数据并显示
             this.currentRewards = this.saveDataManager.getLastRewards();
             console.log('[GameEnd] 获取到的奖励数据:', this.currentRewards);
+            console.log('[GameEnd] 奖励计算完成');
             this.updateRewardDisplay();
             
             // 显示结算面板(通过动画)
@@ -420,6 +456,13 @@ export class GameEnd extends Component {
      * 显示结算面板(通过动画)
      */
     private showEndPanelWithAnimation() {
+        console.log('[GameEnd] 开始显示结算面板动画');
+        
+        // 确保节点处于激活状态
+        if (this.node && !this.node.active) {
+            this.node.active = true;
+            console.log('[GameEnd] 激活GameEnd节点');
+        }
 
         // 重置双倍奖励状态
         this.hasDoubledReward = false;
@@ -427,30 +470,64 @@ export class GameEnd extends Component {
             this.doubleButton.interactable = true;
         }
         
+        // 确保有UIOpacity组件
+        if (this.node) {
+            let uiOpacity = this.node.getComponent(UIOpacity);
+            if (!uiOpacity) {
+                uiOpacity = this.node.addComponent(UIOpacity);
+                console.log('[GameEnd] 在showEndPanelWithAnimation中添加UIOpacity组件');
+            }
+        }
+        
         // 播放显示动画(如果有的话)
         if (this.animationDuration > 0) {
+            console.log(`[GameEnd] 动画持续时间: ${this.animationDuration}秒,开始播放GameEnd面板弹出动画`);
             this.playShowAnimation();
+        } else {
+            // 如果没有动画,直接显示
+            if (this.node) {
+                this.node.setScale(1, 1, 1);
+                const uiOpacity = this.node.getComponent(UIOpacity);
+                if (uiOpacity) {
+                    uiOpacity.opacity = 255;
+                }
+            }
+            console.log('[GameEnd] 无动画配置,直接显示面板');
         }
         
-        console.log('[GameEnd] 结算面板已通过动画显示');
+        console.log('[GameEnd] GameEnd面板显示流程完成');
     }
     
     /**
      * 播放显示动画
      */
     private playShowAnimation() {
-        if (!this.node) return;
+        if (!this.node) {
+            console.error('[GameEnd] playShowAnimation: 节点不存在');
+            return;
+        }
+        
+        console.log('[GameEnd] 开始播放GameEnd面板弹出动画');
+        
+        // 停止所有正在进行的动画
+        this.stopAllAnimations();
         
         // 设置节点位置到屏幕中心
         this.centerNodeOnScreen();
         
-        // 设置初始状态
-        this.node.setScale(0.5, 0.5, 1);
-        const uiOpacity = this.node.getComponent(UIOpacity);
-        if (uiOpacity) {
-            uiOpacity.opacity = 0;
+        // 确保有UIOpacity组件
+        let uiOpacity = this.node.getComponent(UIOpacity);
+        if (!uiOpacity) {
+            uiOpacity = this.node.addComponent(UIOpacity);
+            console.log('[GameEnd] 添加UIOpacity组件');
         }
         
+        // 设置初始状态
+        this.node.setScale(0, 0, 1);
+        uiOpacity.opacity = 0;
+        
+        console.log('[GameEnd] 设置动画初始状态 - 缩放: 0, 透明度: 0');
+        
         // 播放缩放和淡入动画
         tween(this.node)
             .to(this.animationDuration, {
@@ -458,17 +535,21 @@ export class GameEnd extends Component {
             }, {
                 easing: 'backOut'
             })
+            .call(() => {
+                console.log('[GameEnd] GameEnd面板缩放动画完成');
+            })
             .start();
             
-        if (uiOpacity) {
-            tween(uiOpacity)
-                .to(this.animationDuration, {
-                    opacity: 255
-                })
-                .start();
-        }
+        tween(uiOpacity)
+            .to(this.animationDuration, {
+                opacity: 255
+            })
+            .call(() => {
+                console.log('[GameEnd] GameEnd面板淡入动画完成');
+            })
+            .start();
         
-        console.log('[GameEnd] 播放显示动画');
+        console.log('[GameEnd] GameEnd面板弹出动画开始执行');
     }
     
     /**
@@ -587,9 +668,26 @@ export class GameEnd extends Component {
      */
     private onResetUI() {
         console.log('[GameEnd] 重置UI状态');
-        this.hideEndPanelWithAnimation();
+        
+        // 停止所有动画
+        this.stopAllAnimations();
+        
+        // 直接重置到隐藏状态,不使用动画
+        this.initializeHiddenState();
+        
+        // 重置所有状态,为下一局游戏做准备
         this.hasDoubledReward = false;
-        this.currentRewards = {money: 0, diamonds: 0};
+        // 注意:不重置currentRewards,保持奖励显示直到下次游戏开始
+        // this.currentRewards = {money: -1, diamonds: -1}; // 移除这行,避免重置奖励显示
+        this.isGameSuccess = false;
+        this.hasProcessedGameEnd = false; // 重置处理状态标志
+        
+        // 重置按钮状态
+        if (this.doubleButton) {
+            this.doubleButton.interactable = true;
+        }
+        
+        console.log('[GameEnd] UI状态重置完成');
     }
     
     /**
@@ -642,16 +740,39 @@ export class GameEnd extends Component {
             let uiOpacity = this.node.getComponent(UIOpacity);
             if (!uiOpacity) {
                 uiOpacity = this.node.addComponent(UIOpacity);
+                console.log('[GameEnd] 添加UIOpacity组件到节点');
+            }
+            uiOpacity.opacity = 0;
+        }
+        
+        // 设置初始缩放为0(完全隐藏,恢复到历史版本实现)
+        if (this.node) {
+            this.node.setScale(0, 0, 1);
+        }
+        
+        console.log('[GameEnd] 初始化为隐藏状态 - 缩放: 0, 透明度: 0');
+    }
+    
+    /**
+     * 初始化为隐藏状态但保持节点激活(用于游戏开始时)
+     */
+    private initializeHiddenStateKeepActive() {
+        // 设置初始透明度为0
+        if (this.node) {
+            let uiOpacity = this.node.getComponent(UIOpacity);
+            if (!uiOpacity) {
+                uiOpacity = this.node.addComponent(UIOpacity);
+                console.log('[GameEnd] 添加UIOpacity组件到节点');
             }
             uiOpacity.opacity = 0;
         }
         
-        // 设置初始缩放为0
+        // 设置初始缩放为0(完全隐藏,恢复到历史版本实现)
         if (this.node) {
             this.node.setScale(0, 0, 1);
         }
         
-        console.log('[GameEnd] 初始化为隐藏状态');
+        console.log('[GameEnd] 初始化为隐藏状态(保持激活) - 缩放: 0, 透明度: 0');
     }
     
     /**
@@ -801,11 +922,38 @@ export class GameEnd extends Component {
      */
     public stopAllAnimations() {
         if (this.node) {
+            // 停止所有Tween动画
+            Tween.stopAllByTarget(this.node);
+            
+            // 停止UIOpacity组件的动画
             const uiOpacity = this.node.getComponent(UIOpacity);
             if (uiOpacity) {
                 Tween.stopAllByTarget(uiOpacity);
             }
-            Tween.stopAllByTarget(this.node);
+            
+            console.log('[GameEnd] 已停止所有动画');
+        } else {
+            console.warn('[GameEnd] stopAllAnimations: 节点不存在');
+        }
+    }
+    
+    /**
+     * 清理所有敌人
+     * 在游戏结束时调用,符合阶段一UI弹出阶段的职责
+     */
+    /**
+     * 通过事件系统清理所有敌人
+     * 使用CLEAR_ALL_ENEMIES事件,避免直接引用EnemyController
+     */
+    private clearAllEnemies() {
+        console.log('[GameEnd] 游戏结束,派发CLEAR_ALL_ENEMIES事件清理所有敌人');
+        
+        try {
+            // 通过事件总线派发清理敌人事件
+            EventBus.getInstance().emit(GameEvents.CLEAR_ALL_ENEMIES);
+            console.log('[GameEnd] 已派发CLEAR_ALL_ENEMIES事件,统一清理敌人');
+        } catch (error) {
+            console.error('[GameEnd] 派发清理敌人事件时发生错误:', error);
         }
     }
     
@@ -834,6 +982,7 @@ export class GameEnd extends Component {
         const eventBus = EventBus.getInstance();
         eventBus.off(GameEvents.GAME_SUCCESS, this.onGameSuccess, this);
         eventBus.off(GameEvents.GAME_DEFEAT, this.onGameDefeat, this);
+        eventBus.off(GameEvents.GAME_START, this.onGameStart, this);
         eventBus.off(GameEvents.RESET_UI_STATES, this.onResetUI, this);
         console.log('[GameEnd] 事件监听器已清理');
     }
@@ -846,6 +995,7 @@ export class GameEnd extends Component {
         const eventBus = EventBus.getInstance();
         eventBus.off(GameEvents.GAME_SUCCESS, this.onGameSuccess, this);
         eventBus.off(GameEvents.GAME_DEFEAT, this.onGameDefeat, this);
+        eventBus.off(GameEvents.GAME_START, this.onGameStart, this);
         eventBus.off(GameEvents.RESET_UI_STATES, this.onResetUI, this);
         
         // 清理按钮事件

+ 25 - 9
assets/scripts/CombatSystem/MenuSystem/MenuController.ts

@@ -201,19 +201,17 @@ export class MenuController extends Component {
         
         // 只处理游戏中的退出逻辑,游戏外界面不显示菜单按钮
         if (currentAppState === AppState.IN_GAME) {
-            // 在游戏中退出,先重置镜头位置,然后视为游戏失败
-            console.log('[MenuController] 游戏中退出,先重置镜头位置');
-            this.resetCameraPosition();
+            // 在游戏中退出,应该触发游戏失败事件显示GameEnd面板
+            console.log('[MenuController] 游戏中退出,触发GAME_DEFEAT事件显示游戏失败UI');
             
-            console.log('[MenuController] 触发游戏失败事件');
+            // 先触发游戏失败事件,让GameEnd面板显示
             const eventBus = EventBus.getInstance();
             eventBus.emit(GameEvents.GAME_DEFEAT);
             
-            // 等待游戏失败处理完成后关闭菜单
-            setTimeout(async () => {
-                await this.closeMenu();
-                console.log('[MenuController] 游戏失败处理完成,菜单已关闭');
-            }, 100);
+            // 关闭菜单(不触发GAME_RESUME事件,避免事件冲突)
+            await this.closeMenuWithoutResume();
+            
+            console.log('[MenuController] 菜单退出处理完成,已触发GAME_DEFEAT事件');
         } else {
             // 游戏外不应该显示菜单按钮,如果意外触发则只关闭菜单
             console.log('[MenuController] 游戏外意外触发退出按钮,仅关闭菜单');
@@ -291,6 +289,24 @@ export class MenuController extends Component {
         console.log('菜单已关闭');
     }
     
+    /**
+     * 关闭菜单但不触发游戏恢复事件(用于游戏失败退出)
+     */
+    private async closeMenuWithoutResume(): Promise<void> {
+        if (!this.isMenuOpen) return;
+        
+        this.isMenuOpen = false;
+        
+        // 使用动画隐藏菜单面板
+        if (this.popupAni) {
+            await this.popupAni.hidePanel();
+        }
+        // 注意:不再有fallback逻辑设置active,菜单面板始终保持active=true
+        
+        // 不触发GAME_RESUME事件,避免与GAME_DEFEAT事件冲突
+        console.log('菜单已关闭(未触发游戏恢复)');
+    }
+    
     /**
      * 切换菜单状态
      */

+ 4 - 2
assets/scripts/CombatSystem/Wall.ts

@@ -179,18 +179,20 @@ export class Wall extends Component {
 
     /**
      * 墙体被摧毁时的处理
+     * 统一与菜单退出的失败处理流程,直接触发GAME_DEFEAT事件
      */
     private onWallDestroyed() {
         console.log('[Wall] 墙体被摧毁,触发游戏失败');
         
-        // 通过事件系统触发墙体被摧毁事件
+        // 通过事件系统触发墙体被摧毁事件(保留用于其他监听器)
         const eventBus = EventBus.getInstance();
         eventBus.emit(GameEvents.WALL_DESTROYED, {
             finalHealth: this.currentHealth,
             maxHealth: this.getMaxHealth()
         });
         
-        // 触发游戏失败
+        // 统一失败处理:直接触发GAME_DEFEAT事件,与菜单退出处理保持一致
+        console.log('[Wall] 直接触发GAME_DEFEAT事件,与菜单退出失败处理流程一致');
         eventBus.emit(GameEvents.GAME_DEFEAT);
     }
 

+ 14 - 166
assets/scripts/FourUI/NavBarController.ts

@@ -1,114 +1,26 @@
-import { _decorator, Color, Component, Node, Sprite, find, Vec3 } from 'cc';
+import { _decorator, Color, Component, Node, Sprite } from 'cc';
 import EventBus, { GameEvents } from '../Core/EventBus';
-import { GameStartMove } from '../Animations/GameStartMove';
-import { Audio } from '../AudioManager/AudioManager';
 const { ccclass, property } = _decorator;
 
 @ccclass('NavBarController')
 export class NavBarController extends Component {
     @property({ type: [Node] }) panels: Node[] = [];      // 依次给 Main、Shop、Upgrade、Skill
     @property({ type: [Node] }) buttons: Node[] = [];     // 依次给 Battle、Shop、Upgrade、Skill
-    @property({ type: [Node] }) buttonBorders: Node[] = []; // 按钮边框节点数组,需要手动配置
-    
     private normalColor = new Color(255, 255, 255, 255);
+    private activeColor = new Color(255, 0, 0, 255);      // 红色
     private lockedColor = new Color(128, 128, 128, 255);   // 灰色(锁定状态)
     
     // 按钮锁定状态数组 - 对应 Battle、Shop、Upgrade、Skill 四个按钮
     private buttonLockStates: boolean[] = [false, true, false, false];  // 默认只锁定Shop按钮
-    
-    // 存储按钮原始位置
-    private buttonOriginalPositions: Vec3[] = [];
-    
-    // 当前激活的按钮索引
-    private currentActiveButtonIndex: number = -1;
-    
-    // GameStartMove组件引用,用于重置镜头位置
-    private gameStartMoveComponent: GameStartMove = null;
 
     start () {
-        // 保存按钮原始位置
-        this.saveButtonOriginalPositions();
         // 默认打开 MainUI
         this.switchTo(0);
         // 初始化按钮状态,设置shop按钮为锁定状态
         this.updateButtonStates();
-        // 设置事件监听
-        this.setupEventListeners();
-    }
-    
-    onDestroy() {
-        // 移除事件监听
-        this.removeEventListeners();
-    }
-    
-    /**
-     * 设置事件监听器
-     */
-    private setupEventListeners() {
-        const eventBus = EventBus.getInstance();
-        
-        // 监听返回主菜单事件
-        eventBus.on(GameEvents.RETURN_TO_MAIN_MENU, this.onReturnToMainMenu, this);
-    }
-    
-    /**
-     * 移除事件监听器
-     */
-    private removeEventListeners() {
-        const eventBus = EventBus.getInstance();
-        
-        eventBus.off(GameEvents.RETURN_TO_MAIN_MENU, this.onReturnToMainMenu, this);
-    }
-    
-    /**
-     * 保存按钮原始位置
-     */
-    private saveButtonOriginalPositions() {
-        this.buttonOriginalPositions = [];
-        this.buttons.forEach(btn => {
-            this.buttonOriginalPositions.push(btn.position.clone());
-        });
-        console.log('[NavBarController] 已保存按钮原始位置');
-    }
-    
-    /**
-     * 处理返回主菜单事件
-     */
-    private onReturnToMainMenu() {
-        console.log('[NavBarController] 收到返回主菜单事件,切换到主界面');
-        
-        // 重置镜头位置到原始位置
-        this.resetCameraPosition();
-        
-        // 切换到主界面(Battle按钮对应的Main面板,索引0)
-        this.switchTo(0);
-    }
-    
-    /**
-     * 重置镜头位置到原始位置
-     * 确保从游戏中返回主界面时镜头位置正常
-     */
-    private resetCameraPosition() {
-        // 如果还没有获取GameStartMove组件,尝试获取
-        if (!this.gameStartMoveComponent) {
-            const cameraNode = find('Canvas/Main Camera');
-            if (cameraNode) {
-                this.gameStartMoveComponent = cameraNode.getComponent(GameStartMove);
-            }
-        }
-        
-        // 如果成功获取到组件,重置镜头位置
-        if (this.gameStartMoveComponent) {
-            console.log('[NavBarController] 重置镜头位置到原始位置');
-            this.gameStartMoveComponent.resetCameraToOriginalPosition(0.3);
-        } else {
-            console.warn('[NavBarController] 未找到GameStartMove组件,无法重置镜头位置');
-        }
     }
 
     onBattleClick ()  { 
-        // 播放UI点击音效
-        Audio.playUISound('data/弹球音效/ui play');
         // 检查Battle按钮是否被锁定(索引0)
         if (this.buttonLockStates[0]) {
             console.log('[NavBarController] Battle按钮被锁定,无法点击');
@@ -122,8 +34,6 @@ export class NavBarController extends Component {
         this.switchTo(0); 
     }
     onUpgradeClick () { 
-        // 播放UI点击音效
-        Audio.playUISound('data/弹球音效/ui play');
         // 检查Upgrade按钮是否被锁定(索引2)
         if (this.buttonLockStates[2]) {
             console.log('[NavBarController] Upgrade按钮被锁定,无法点击');
@@ -137,8 +47,6 @@ export class NavBarController extends Component {
         this.switchTo(1); 
     }
     onShopClick ()    { 
-        // 播放UI点击音效
-        Audio.playUISound('data/弹球音效/ui play');
         // 检查shop按钮是否被锁定(索引1)
         if (this.buttonLockStates[1]) {
             console.log('[NavBarController] Shop按钮被锁定,无法点击');
@@ -152,8 +60,6 @@ export class NavBarController extends Component {
         this.switchTo(2); 
     }
     onSkillClick ()   { 
-        // 播放UI点击音效
-        Audio.playUISound('data/弹球音效/ui play');
         // 检查Skill按钮是否被锁定(索引3)
         if (this.buttonLockStates[3]) {
             console.log('[NavBarController] Skill按钮被锁定,无法点击');
@@ -172,96 +78,45 @@ export class NavBarController extends Component {
         // 显示对应面板
         this.panels.forEach((p, i) => p.active = i === index);
 
-        // 当切换到主界面时播放背景音乐
-        if (index === 0) {
-            console.log('[NavBarController] 切换到主界面,播放背景音乐');
-            Audio.playMusic('data/弹球音效/ui bgm', true);
-        }
-
         // 按钮索引到面板索引的映射:根据实际的点击事件调用
         // 按钮索引: [0:Battle, 1:Shop, 2:Upgrade, 3:Skill]
         // 面板索引: [0:Main, 1:Shop, 2:Upgrade, 3:Skill]
         // 实际映射: Battle->Main(0), Shop->Upgrade(2), Upgrade->Shop(1), Skill->Skill(3)
         const buttonToPanelMap = [0, 2, 1, 3]; // Battle->Main(0), Shop->Upgrade(2), Upgrade->Shop(1), Skill->Skill(3)
         
-        // 重置所有按钮到原始位置和状态
+        // 设置按钮颜色,考虑锁定状态
         this.buttons.forEach((btn, buttonIndex) => {
             const sp = btn.getComponent(Sprite);
             if (sp) {
-                // 重置按钮位置到原始位置
-                if (this.buttonOriginalPositions[buttonIndex]) {
-                    btn.position = this.buttonOriginalPositions[buttonIndex].clone();
-                }
-                
-                // 设置按钮颜色(只处理锁定状态)
+                // 检查按钮是否被锁定
                 if (this.buttonLockStates[buttonIndex]) {
                     sp.color = this.lockedColor;  // 锁定状态显示灰色
                 } else {
-                    sp.color = this.normalColor;  // 正常状态显示白色
+                    // 检查当前按钮对应的面板是否是激活的面板
+                    const panelIndex = buttonToPanelMap[buttonIndex];
+                    sp.color = (panelIndex === index) ? this.activeColor : this.normalColor;
                 }
             }
         });
-        
-        // 隐藏所有边框
-        this.buttonBorders.forEach(border => {
-            if (border) {
-                border.active = false;
-            }
-        });
-        
-        // 找到当前激活的按钮索引
-        let activeButtonIndex = -1;
-        for (let buttonIndex = 0; buttonIndex < buttonToPanelMap.length; buttonIndex++) {
-            if (buttonToPanelMap[buttonIndex] === index) {
-                activeButtonIndex = buttonIndex;
-                break;
-            }
-        }
-        
-        // 如果找到激活按钮且未被锁定,则应用激活效果
-        if (activeButtonIndex !== -1 && !this.buttonLockStates[activeButtonIndex]) {
-            const activeBtn = this.buttons[activeButtonIndex];
-            if (activeBtn && this.buttonOriginalPositions[activeButtonIndex]) {
-                // 向上移动20px
-                const newPos = this.buttonOriginalPositions[activeButtonIndex].clone();
-                newPos.y += 20;
-                activeBtn.position = newPos;
-                
-                // 显示对应的边框
-                if (this.buttonBorders[activeButtonIndex]) {
-                    this.buttonBorders[activeButtonIndex].active = true;
-                }
-                
-                this.currentActiveButtonIndex = activeButtonIndex;
-            }
-        } else {
-            this.currentActiveButtonIndex = -1;
-        }
-        
-        // 通知MenuController更新菜单按钮状态
-        const eventBus = EventBus.getInstance();
-        eventBus.emit(GameEvents.UI_PANEL_SWITCHED, { panelIndex: index });
     }
     
     /**
      * 更新按钮状态,主要用于设置锁定按钮的视觉效果
      */
     private updateButtonStates() {
+        // 按钮索引到面板索引的映射:根据实际的点击事件调用
+        const buttonToPanelMap = [0, 2, 1, 3]; // Battle->Main(0), Shop->Upgrade(2), Upgrade->Shop(1), Skill->Skill(3)
+        
         this.buttons.forEach((btn, buttonIndex) => {
             const sp = btn.getComponent(Sprite);
             if (sp) {
                 if (this.buttonLockStates[buttonIndex]) {
                     sp.color = this.lockedColor;  // 设置锁定按钮为锁定颜色
-                    // 确保锁定的按钮在原始位置
-                    if (this.buttonOriginalPositions[buttonIndex]) {
-                        btn.position = this.buttonOriginalPositions[buttonIndex].clone();
-                    }
-                    // 隐藏锁定按钮的边框
-                    if (this.buttonBorders[buttonIndex]) {
-                        this.buttonBorders[buttonIndex].active = false;
-                    }
                 } else {
-                    sp.color = this.normalColor;  // 设置未锁定按钮为正常颜色
+                    // 对于未锁定的按钮,设置为正常颜色(除非它是当前激活的)
+                    // 这里我们需要知道当前激活的面板,但为了简化,先设置为正常颜色
+                    // 实际的激活状态会在switchTo中正确设置
+                    sp.color = this.normalColor;
                 }
             }
         });
@@ -322,11 +177,4 @@ export class NavBarController extends Component {
     public getButtonLockStates(): boolean[] {
         return [...this.buttonLockStates];
     }
-    
-    /**
-     * 获取当前激活的按钮索引
-     */
-    public getCurrentActiveButtonIndex(): number {
-        return this.currentActiveButtonIndex;
-    }
 }

+ 39 - 99
assets/scripts/FourUI/UpgradeSystem/UpgradeController.ts

@@ -1,9 +1,7 @@
 import { _decorator, Component, Node, Button, Label, Sprite, SpriteFrame, Texture2D, resources, ScrollView, Layout, Prefab, instantiate, find, UIOpacity, Color } from 'cc';
 import { SaveDataManager, WeaponData } from '../../LevelSystem/SaveDataManager';
 import EventBus, { GameEvents } from '../../Core/EventBus';
-import { PopUPAni } from '../../Animations/PopUPAni';
 import { UpgradeAni } from './UpgradeAni';
-import { Audio } from '../../AudioManager/AudioManager';
 
 const { ccclass, property } = _decorator;
 
@@ -23,14 +21,19 @@ interface WeaponConfig {
         bulletSpeed: number;
     };
     visualConfig: {
-        weaponSprites: string;
+        weaponSprites: {
+            "I": string;
+            "H-I": string;
+            "L": string;
+            "S": string;
+            "D-T": string;
+        };
     };
     upgradeConfig?: {
         maxLevel: number;
         levels: {
             [level: string]: {
                 cost: number;
-                damage?: number;
             };
         };
     };
@@ -55,14 +58,12 @@ export class UpgradeController extends Component {
     @property(Label) panelCurrentDamage: Label = null;         // Canvas/UpgradeUI/UpgradePanel/NumberBack/CurrentDamage
     @property(Label) panelCostLabel: Label = null;             // Canvas/UpgradeUI/UpgradePanel/UpgradeBtn/CostLabel
     @property(Button) panelUpgradeBtn: Button = null;          // Canvas/UpgradeUI/UpgradePanel/UpgradeBtn
-    @property(Node) upIcon: Node = null;                       // Canvas/UpgradeUI/UpgradePanel/UpgradeBtn/UP图标
     
     // 武器节点预制体
     @property(Prefab) lockedWeaponPrefab: Prefab = null;    // Lock.prefab
     @property(Prefab) unlockedWeaponPrefab: Prefab = null;  // Unlock.prefab
     
     // 动画控制器
-    @property(PopUPAni) panelAni: PopUPAni = null;    // Canvas/UpgradeUI/UpgradePanel上的PopUPAni组件
     @property(UpgradeAni) upgradeAni: UpgradeAni = null;    // Canvas/UpgradeUI/UpgradePanel上的UpgradeAni组件
     
     // 数据管理
@@ -95,6 +96,13 @@ export class UpgradeController extends Component {
         // 刷新UI
         this.refreshWeaponList();
         
+        // 初始化升级面板状态
+        if (this.upgradeAni) {
+            this.upgradeAni.hidePanelImmediate();
+        } else {
+            this.upgradePanel.active = false;
+        }
+        
         console.log('[UpgradeController] 初始化完成');
     }
     
@@ -370,7 +378,7 @@ export class UpgradeController extends Component {
         // 设置武器图标 - 查找专门的武器图标Sprite节点,避免影响背景
         const weaponSprite = spriteNode.getChildByName('WeaponSprite')?.getComponent(Sprite);
         if (weaponSprite && weaponConfig.visualConfig.weaponSprites) {
-            const spritePath = weaponConfig.visualConfig.weaponSprites;
+            const spritePath = weaponConfig.visualConfig.weaponSprites['I'] || weaponConfig.visualConfig.weaponSprites['H-I'] || weaponConfig.visualConfig.weaponSprites['L'] || weaponConfig.visualConfig.weaponSprites['S'] || weaponConfig.visualConfig.weaponSprites['D-T'];
             this.loadWeaponSprite(weaponSprite, spritePath);
         }
         
@@ -482,8 +490,8 @@ export class UpgradeController extends Component {
         this.refreshUpgradePanel();
         
         // 使用动画显示升级面板
-        if (this.panelAni) {
-            await this.panelAni.showPanel();
+        if (this.upgradeAni) {
+            await this.upgradeAni.showPanel();
         } else {
             // 如果没有动画组件,直接显示
             this.upgradePanel.active = true;
@@ -494,16 +502,9 @@ export class UpgradeController extends Component {
      * 关闭升级面板
      */
     private async closeUpgradePanel() {
-        // 播放UI点击音效
-        Audio.playUISound('data/弹球音效/ui play');
-        // 停止UP图标动画
-        if (this.upIcon && this.panelAni) {
-            this.panelAni.stopIconTipAnimation(this.upIcon);
-        }
-        
         // 使用动画隐藏升级面板
-        if (this.panelAni) {
-            await this.panelAni.hidePanel();
+        if (this.upgradeAni) {
+            await this.upgradeAni.hidePanel();
         } else {
             // 如果没有动画组件,直接隐藏
             this.upgradePanel.active = false;
@@ -529,7 +530,7 @@ export class UpgradeController extends Component {
         
         // 设置武器图标 - Canvas/UpgradeUI/UpgradePanel/WeaponSprite
         if (this.panelWeaponSprite && weaponConfig.visualConfig.weaponSprites) {
-            const spritePath = weaponConfig.visualConfig.weaponSprites;
+            const spritePath = weaponConfig.visualConfig.weaponSprites['I'] || weaponConfig.visualConfig.weaponSprites['H-I'] || weaponConfig.visualConfig.weaponSprites['L'] || weaponConfig.visualConfig.weaponSprites['S'] || weaponConfig.visualConfig.weaponSprites['D-T'];
             this.loadWeaponSprite(this.panelWeaponSprite, spritePath);
         }
         
@@ -579,35 +580,6 @@ export class UpgradeController extends Component {
             }
         }
         
-        // 检查钞票是否足够,控制UP图标显示和动画
-        const playerMoney = this.saveDataManager.getMoney();
-        if (weaponData.level < maxLevel && playerMoney >= upgradeCost) {
-            // 钞票足够,显示UP图标并播放动画
-            if (this.upIcon) {
-                this.upIcon.active = true;
-                if (this.panelAni) {
-                    // 传入检查条件:钞票是否足够且未达到最大等级
-                    const checkCondition = () => {
-                        const currentMoney = this.saveDataManager.getMoney();
-                        const currentWeaponData = this.saveDataManager.getWeapon(this.currentSelectedWeapon);
-                        const currentUpgradeCost = this.saveDataManager.getWeaponUpgradeCost(this.currentSelectedWeapon);
-                        return currentWeaponData && 
-                               currentWeaponData.level < maxLevel && 
-                               currentMoney >= currentUpgradeCost;
-                    };
-                    this.panelAni.playIconTipAnimation(this.upIcon, checkCondition);
-                }
-            }
-        } else {
-            // 钞票不足或已满级,隐藏UP图标并停止动画
-            if (this.upIcon) {
-                this.upIcon.active = false;
-                if (this.panelAni) {
-                    this.panelAni.stopIconTipAnimation(this.upIcon);
-                }
-            }
-        }
-        
         // 升级按钮始终保持可点击状态,通过Toast显示各种提示
         console.log(`[UpgradeController] 升级按钮保持可交互状态`);
     }
@@ -618,56 +590,26 @@ export class UpgradeController extends Component {
     private calculateWeaponDamage(baseDamage: number, level: number, weaponId?: string): number {
         if (level === 0) return 0; // 未解锁武器伤害为0
         
-        // 优先使用手动配置的伤害值
-        if (weaponId && this.weaponsConfig && this.weaponsConfig.weapons) {
-            const weaponConfig = this.weaponsConfig.weapons.find(w => w.id === weaponId);
-            if (weaponConfig && weaponConfig.upgradeConfig && weaponConfig.upgradeConfig.levels) {
-                const levelConfig = weaponConfig.upgradeConfig.levels[level.toString()];
-                if (levelConfig && levelConfig.damage !== undefined) {
-                    console.log(`[UpgradeController] 使用手动配置伤害 - ${weaponId}: 等级=${level}, 伤害=${levelConfig.damage}`);
-                    return levelConfig.damage;
-                }
-            }
-        }
-        
-        // 如果没有手动配置,使用基于武器稀有度的伤害增长公式作为后备
+        // 从武器配置中获取伤害增加值
         if (weaponId && this.weaponsConfig && this.weaponsConfig.weapons) {
             const weaponConfig = this.weaponsConfig.weapons.find(w => w.id === weaponId);
+            // 由于WeaponConfig中没有upgradeConfig属性,直接使用基础伤害计算
             if (weaponConfig && weaponConfig.stats && weaponConfig.stats.damage) {
-                // 根据武器稀有度确定每级伤害增长
-                let damagePerLevel = 0;
-                switch (weaponConfig.rarity) {
-                    case 'common':
-                        damagePerLevel = Math.floor(baseDamage * 0.15); // 普通武器每级增加15%基础伤害
-                        break;
-                    case 'uncommon':
-                        damagePerLevel = Math.floor(baseDamage * 0.18); // 不常见武器每级增加18%基础伤害
-                        break;
-                    case 'rare':
-                        damagePerLevel = Math.floor(baseDamage * 0.20); // 稀有武器每级增加20%基础伤害
-                        break;
-                    case 'epic':
-                        damagePerLevel = Math.floor(baseDamage * 0.25); // 史诗武器每级增加25%基础伤害
-                        break;
-                    default:
-                        damagePerLevel = Math.floor(baseDamage * 0.15); // 默认15%
-                        break;
-                }
+                let totalDamageIncrease = 0;
                 
-                // 确保每级至少增加1点伤害
-                damagePerLevel = Math.max(1, damagePerLevel);
-                
-                // 计算总伤害:基础伤害 + (等级-1) * 每级伤害增长
-                const totalDamage = baseDamage + (level - 1) * damagePerLevel;
-                
-                console.log(`[UpgradeController] 使用自动计算伤害 - ${weaponId}: 基础=${baseDamage}, 等级=${level}, 每级增长=${damagePerLevel}, 总伤害=${totalDamage}`);
+                // 累加从1级到当前级别的所有伤害增加
+                for (let i = 1; i < level; i++) {
+                    // 从武器配置的stats中获取每级伤害增加值
+                    if (this.levelConfigs[i] && this.levelConfigs[i].damageIncrease) {
+                        totalDamageIncrease += this.levelConfigs[i].damageIncrease;
+                    }
+                }
                 
-                return totalDamage;
+                return baseDamage + totalDamageIncrease;
             }
         }
         
         // 如果配置不存在,使用默认公式作为后备
-        console.log(`[UpgradeController] 使用默认伤害计算 - ${weaponId}: 基础=${baseDamage}, 等级=${level}`);
         return baseDamage + (level - 1);
     }
     
@@ -715,8 +657,6 @@ export class UpgradeController extends Component {
      * 升级武器
      */
     private onUpgradeWeapon() {
-        // 播放UI点击音效
-        Audio.playUISound('data/弹球音效/ui play');
         console.log(`[UpgradeController] onUpgradeWeapon方法被调用`);
         
         if (!this.currentSelectedWeapon) {
@@ -755,28 +695,28 @@ export class UpgradeController extends Component {
         const cost = this.saveDataManager.getWeaponUpgradeCost(this.currentSelectedWeapon);
         const playerMoney = this.saveDataManager.getMoney();
         
-        console.log(`[UpgradeController] 升级检查 - 武器: ${this.currentSelectedWeapon}, 费用: ${cost}, 当前钞票: ${playerMoney}`);
-        console.log(`[UpgradeController] 费用类型: ${typeof cost}, 钞票类型: ${typeof playerMoney}`);
+        console.log(`[UpgradeController] 升级检查 - 武器: ${this.currentSelectedWeapon}, 费用: ${cost}, 当前金币: ${playerMoney}`);
+        console.log(`[UpgradeController] 费用类型: ${typeof cost}, 金币类型: ${typeof playerMoney}`);
         console.log(`[UpgradeController] 条件判断: ${playerMoney} < ${cost} = ${playerMoney < cost}`);
         
         if (playerMoney < cost) {
-            console.log(`[UpgradeController] 钞票不足 - 需要: ${cost}, 当前: ${playerMoney}`);
-            EventBus.getInstance().emit(GameEvents.SHOW_TOAST, { message: `钞票不足,需要${cost}钞票`, duration: 2.0 });
+            console.log(`[UpgradeController] 金币不足 - 需要: ${cost}, 当前: ${playerMoney}`);
+            EventBus.getInstance().emit(GameEvents.SHOW_TOAST, { message: `金币不足,需要${cost}金币`, duration: 2.0 });
             return;
         }
         
-        console.log(`[UpgradeController] 钞票充足,继续升级流程`);
+        console.log(`[UpgradeController] 金币充足,继续升级流程`);
         
-        // 记录升级前的钞票数量
+        // 记录升级前的金币数量
         const coinsBeforeUpgrade = this.saveDataManager.getMoney();
-        console.log(`[UpgradeController] 升级前钞票: ${coinsBeforeUpgrade}`);
+        console.log(`[UpgradeController] 升级前金币: ${coinsBeforeUpgrade}`);
         
         const success = this.saveDataManager.upgradeWeapon(this.currentSelectedWeapon);
         
         if (success) {
             const coinsAfterUpgrade = this.saveDataManager.getMoney();
             console.log(`[UpgradeController] 武器 ${this.currentSelectedWeapon} 升级成功`);
-            console.log(`[UpgradeController] 升级后钞票: ${coinsAfterUpgrade}, 消耗: ${coinsBeforeUpgrade - coinsAfterUpgrade}`);
+            console.log(`[UpgradeController] 升级后金币: ${coinsAfterUpgrade}, 消耗: ${coinsBeforeUpgrade - coinsAfterUpgrade}`);
             
             // 播放武器升级成功动画
             this.playWeaponUpgradeSuccessAnimation();

+ 16 - 11
assets/scripts/LevelSystem/IN_game.ts

@@ -216,6 +216,7 @@ export class InGameManager extends Component {
         eventBus.on(GameEvents.GAME_RESUME, this.onGameResumeEvent, this);
         eventBus.on('ENEMY_KILLED', this.onEnemyKilledEvent, this);
         eventBus.on(GameEvents.RESET_ENERGY_SYSTEM, this.resetEnergySystem, this);
+        eventBus.on(GameEvents.WALL_DESTROYED, this.onWallDestroyedEvent, this);
     }
 
     /**
@@ -403,6 +404,15 @@ export class InGameManager extends Component {
         console.log('[InGameManager] 接收到游戏恢复事件');
     }
 
+    /**
+     * 墙体被摧毁事件处理
+     * 墙体现在直接触发GAME_DEFEAT事件,与菜单退出处理流程一致
+     */
+    private onWallDestroyedEvent() {
+        console.log('[InGameManager] 收到墙体被摧毁事件,墙体已直接触发GAME_DEFEAT事件');
+        // 不再需要调用checkGameState,因为墙体已直接触发GAME_DEFEAT事件
+    }
+
     /**
      * 处理敌人被击杀事件
      */
@@ -498,19 +508,12 @@ export class InGameManager extends Component {
 
     /**
      * 游戏状态检查
+     * 现在只检查敌人击败状态,墙体摧毁直接触发GAME_DEFEAT事件
      */
     private checkGameState() {
-        // 检查所有墙体是否存活
-        const isAnyWallDestroyed = 
-            (this.wallComponent && !this.wallComponent.isAlive()) ||
-            (this.topFenceComponent && !this.topFenceComponent.isAlive()) ||
-            (this.bottomFenceComponent && !this.bottomFenceComponent.isAlive());
-            
-        if (isAnyWallDestroyed) {
-            this.triggerGameDefeat();
-            return;
-        }
-
+        // 墙体摧毁检查已移除,因为墙体现在直接触发GAME_DEFEAT事件
+        // 这样与菜单退出的失败处理流程保持一致
+        
         if (this.checkAllEnemiesDefeated()) {
             this.triggerGameSuccess();
             return;
@@ -1104,5 +1107,7 @@ export class InGameManager extends Component {
         eventBus.off(GameEvents.GAME_DEFEAT, this.onGameDefeatEvent, this);
         eventBus.off(GameEvents.GAME_RESUME, this.onGameResumeEvent, this);
         eventBus.off('ENEMY_KILLED', this.onEnemyKilledEvent, this);
+        eventBus.off(GameEvents.RESET_ENERGY_SYSTEM, this.resetEnergySystem, this);
+        eventBus.off(GameEvents.WALL_DESTROYED, this.onWallDestroyedEvent, this);
     }
 }

+ 82 - 0
debug_actual_gameend.js

@@ -0,0 +1,82 @@
+/**
+ * 实际游戏环境GameEnd调试脚本
+ * 用于检查实际游戏中GameEnd组件的配置和状态
+ */
+
+console.log('=== 实际游戏环境GameEnd调试 ===');
+console.log('\n请在Cocos Creator中运行此脚本,或将以下代码添加到GameEnd.ts中进行调试:');
+
+console.log('\n1. 检查GameEnd组件的animationDuration属性:');
+console.log('   在GameEnd.ts的start()方法中添加:');
+console.log('   console.log("[GameEnd] animationDuration:", this.animationDuration);');
+
+console.log('\n2. 检查GameEnd节点的层级和激活状态:');
+console.log('   在GameEnd.ts的showEndPanelWithAnimation()方法开始处添加:');
+console.log('   console.log("[GameEnd] 节点激活状态:", this.node.active);');
+console.log('   console.log("[GameEnd] 节点层级索引:", this.node.getSiblingIndex());');
+console.log('   console.log("[GameEnd] 父节点子节点数量:", this.node.parent?.children.length);');
+
+console.log('\n3. 检查UIOpacity组件状态:');
+console.log('   在playShowAnimation()方法中添加:');
+console.log('   const uiOpacity = this.node.getComponent(UIOpacity);');
+console.log('   console.log("[GameEnd] UIOpacity初始透明度:", uiOpacity?.opacity);');
+
+console.log('\n4. 检查动画执行状态:');
+console.log('   在tween动画的call回调中添加更详细的日志:');
+console.log('   .call(() => {');
+console.log('       console.log("[GameEnd] 动画完成时节点状态:");');
+console.log('       console.log("  - 缩放:", this.node.scale);');
+console.log('       console.log("  - 透明度:", this.node.getComponent(UIOpacity)?.opacity);');
+console.log('       console.log("  - 激活状态:", this.node.active);');
+console.log('   })');;
+
+console.log('\n5. 检查是否有其他组件干扰:');
+console.log('   在GameEnd.ts的onEnable()方法中添加:');
+console.log('   console.log("[GameEnd] 当前Canvas子节点:");');
+console.log('   const canvas = find("Canvas");');
+console.log('   if (canvas) {');
+console.log('       canvas.children.forEach((child, index) => {');
+console.log('           console.log(`  ${index}: ${child.name} (active: ${child.active}, siblingIndex: ${child.getSiblingIndex()})`);');
+console.log('       });');
+console.log('   }');
+
+console.log('\n6. 检查Wall组件的GAME_DEFEAT事件触发:');
+console.log('   在Wall.ts的onWallDestroyed()方法中添加:');
+console.log('   console.log("[Wall] 即将触发GAME_DEFEAT事件,当前时间:", new Date().toLocaleTimeString());');
+console.log('   console.log("[Wall] EventBus实例:", eventBus);');
+
+console.log('\n7. 检查GameEnd事件监听器注册:');
+console.log('   在GameEnd.ts的setupEventListeners()方法中添加:');
+console.log('   console.log("[GameEnd] 注册GAME_DEFEAT事件监听器,EventBus实例:", eventBus);');
+console.log('   console.log("[GameEnd] 当前组件实例:", this);');
+
+console.log('\n=== 调试步骤 ===');
+console.log('1. 将上述调试代码添加到对应的TypeScript文件中');
+console.log('2. 在Cocos Creator中重新编译项目');
+console.log('3. 运行游戏并让墙体血量归零');
+console.log('4. 查看控制台输出,对比菜单退出和墙体血量为0两种情况的日志差异');
+
+console.log('\n=== 可能的问题原因 ===');
+console.log('1. animationDuration在编辑器中被设置为0');
+console.log('2. GameEnd节点的siblingIndex被设置得太低,被其他UI遮挡');
+console.log('3. GameEnd节点在场景中的初始active状态为false');
+console.log('4. UIOpacity组件缺失或初始透明度设置错误');
+console.log('5. 动画系统或渲染系统在实际环境中的问题');
+console.log('6. EventBus实例不一致,导致事件监听失效');
+console.log('7. 其他脚本在运行时修改了GameEnd节点状态');
+
+console.log('\n=== 快速验证方法 ===');
+console.log('在GameEnd.ts的onGameDefeat()方法中添加强制显示代码:');
+console.log('onGameDefeat() {');
+console.log('    console.log("[GameEnd] 强制显示测试");');
+console.log('    if (this.node) {');
+console.log('        this.node.active = true;');
+console.log('        this.node.setScale(1, 1, 1);');
+console.log('        const uiOpacity = this.node.getComponent(UIOpacity);');
+console.log('        if (uiOpacity) uiOpacity.opacity = 255;');
+console.log('        console.log("[GameEnd] 强制显示完成");');
+console.log('    }');
+console.log('    // 原有的处理逻辑...');
+console.log('}');
+
+console.log('\n如果强制显示有效,说明问题在动画系统;如果无效,说明问题在节点配置或层级。');

+ 190 - 0
gameend_debug_patch.ts

@@ -0,0 +1,190 @@
+/**
+ * GameEnd调试补丁
+ * 将以下代码添加到GameEnd.ts中对应的方法里,用于诊断墙体血量为0时动画不显示的问题
+ */
+
+// ===== 在GameEnd.ts的start()方法中添加 =====
+console.log('[GameEnd] 🔧 调试信息 - 组件初始化:');
+console.log('  - animationDuration:', this.animationDuration);
+console.log('  - 节点名称:', this.node?.name);
+console.log('  - 节点激活状态:', this.node?.active);
+console.log('  - 节点层级索引:', this.node?.getSiblingIndex());
+if (this.node?.parent) {
+    console.log('  - 父节点子节点数量:', this.node.parent.children.length);
+    console.log('  - Canvas子节点列表:');
+    this.node.parent.children.forEach((child, index) => {
+        console.log(`    ${index}: ${child.name} (active: ${child.active}, siblingIndex: ${child.getSiblingIndex()})`);
+    });
+}
+
+// ===== 在GameEnd.ts的setupEventListeners()方法中添加 =====
+console.log('[GameEnd] 🎯 调试信息 - 事件监听器注册:');
+console.log('  - EventBus实例:', eventBus);
+console.log('  - 当前组件实例:', this);
+console.log('  - GAME_DEFEAT事件监听器已注册');
+
+// ===== 在GameEnd.ts的onGameDefeat()方法开始处添加 =====
+console.log('\n🚨 [GameEnd] GAME_DEFEAT事件触发 - 详细调试信息:');
+console.log('  - 触发时间:', new Date().toLocaleTimeString());
+console.log('  - hasProcessedGameEnd:', this.hasProcessedGameEnd);
+console.log('  - 节点激活状态:', this.node?.active);
+console.log('  - 节点层级索引:', this.node?.getSiblingIndex());
+console.log('  - animationDuration:', this.animationDuration);
+
+// 检查UIOpacity组件
+const uiOpacity = this.node?.getComponent(UIOpacity);
+console.log('  - UIOpacity组件存在:', !!uiOpacity);
+if (uiOpacity) {
+    console.log('  - 当前透明度:', uiOpacity.opacity);
+}
+
+// 检查节点缩放
+if (this.node) {
+    console.log('  - 当前缩放:', this.node.scale);
+    console.log('  - 当前位置:', this.node.position);
+}
+
+// 检查是否被其他UI遮挡
+if (this.node?.parent) {
+    const higherLevelPanels = this.node.parent.children.filter(child => 
+        child.getSiblingIndex() > this.node.getSiblingIndex() && child.active && child !== this.node
+    );
+    if (higherLevelPanels.length > 0) {
+        console.log('  - ⚠️  发现更高层级的激活面板:');
+        higherLevelPanels.forEach(panel => {
+            console.log(`    - ${panel.name} (siblingIndex: ${panel.getSiblingIndex()})`);
+        });
+    } else {
+        console.log('  - ✅ 没有更高层级的面板遮挡');
+    }
+}
+
+// ===== 在GameEnd.ts的showEndPanelWithAnimation()方法开始处添加 =====
+console.log('[GameEnd] 🎬 showEndPanelWithAnimation调试:');
+console.log('  - 方法被调用时间:', new Date().toLocaleTimeString());
+console.log('  - 节点激活状态:', this.node?.active);
+console.log('  - animationDuration:', this.animationDuration);
+
+// ===== 在GameEnd.ts的playShowAnimation()方法开始处添加 =====
+console.log('[GameEnd] 🎭 playShowAnimation调试:');
+console.log('  - 动画开始时间:', new Date().toLocaleTimeString());
+console.log('  - 节点存在:', !!this.node);
+if (this.node) {
+    console.log('  - 动画前节点状态:');
+    console.log('    - 激活:', this.node.active);
+    console.log('    - 缩放:', this.node.scale);
+    console.log('    - 位置:', this.node.position);
+    
+    const uiOpacity = this.node.getComponent(UIOpacity);
+    console.log('    - UIOpacity存在:', !!uiOpacity);
+    if (uiOpacity) {
+        console.log('    - 透明度:', uiOpacity.opacity);
+    }
+}
+
+// ===== 在tween动画的.call()回调中添加 =====
+// 替换原有的缩放动画为:
+tween(this.node)
+    .to(this.animationDuration, {
+        scale: new Vec3(1, 1, 1)
+    }, {
+        easing: 'backOut'
+    })
+    .call(() => {
+        console.log('[GameEnd] ✨ 缩放动画完成 - 详细状态:');
+        console.log('  - 完成时间:', new Date().toLocaleTimeString());
+        console.log('  - 节点激活:', this.node?.active);
+        console.log('  - 最终缩放:', this.node?.scale);
+        console.log('  - 最终位置:', this.node?.position);
+        
+        const uiOpacity = this.node?.getComponent(UIOpacity);
+        if (uiOpacity) {
+            console.log('  - 最终透明度:', uiOpacity.opacity);
+        }
+        
+        // 检查节点是否真的可见
+        if (this.node) {
+            const isVisible = this.node.active && 
+                            this.node.scale.x > 0.1 && 
+                            this.node.scale.y > 0.1 && 
+                            (uiOpacity?.opacity || 0) > 10;
+            console.log('  - 计算可见性:', isVisible);
+        }
+    })
+    .start();
+
+// 替换原有的透明度动画为:
+tween(uiOpacity)
+    .to(this.animationDuration, {
+        opacity: 255
+    })
+    .call(() => {
+        console.log('[GameEnd] ✨ 透明度动画完成:');
+        console.log('  - 完成时间:', new Date().toLocaleTimeString());
+        console.log('  - 最终透明度:', uiOpacity?.opacity);
+    })
+    .start();
+
+// ===== 在Wall.ts的onWallDestroyed()方法中添加 =====
+console.log('\n🧱 [Wall] 墙体被摧毁调试信息:');
+console.log('  - 摧毁时间:', new Date().toLocaleTimeString());
+console.log('  - 当前血量:', this.currentHealth);
+console.log('  - EventBus实例:', eventBus);
+console.log('  - 即将触发GAME_DEFEAT事件');
+
+// 在触发事件后添加:
+eventBus.emit(GameEvents.GAME_DEFEAT);
+console.log('  - ✅ GAME_DEFEAT事件已触发');
+
+// ===== 强制显示测试代码(添加到onGameDefeat方法中,用于快速验证) =====
+// 在onGameDefeat方法的最后添加这段强制显示代码:
+console.log('[GameEnd] 🧪 强制显示测试:');
+if (this.node) {
+    // 强制设置节点状态
+    this.node.active = true;
+    this.node.setScale(1, 1, 1);
+    this.node.setPosition(0, 0, 0);
+    
+    const uiOpacity = this.node.getComponent(UIOpacity);
+    if (uiOpacity) {
+        uiOpacity.opacity = 255;
+    }
+    
+    console.log('  - 强制显示完成,节点状态:');
+    console.log('    - 激活:', this.node.active);
+    console.log('    - 缩放:', this.node.scale);
+    console.log('    - 位置:', this.node.position);
+    console.log('    - 透明度:', uiOpacity?.opacity);
+    
+    // 延迟检查是否真的显示了
+    setTimeout(() => {
+        console.log('[GameEnd] 🔍 强制显示后延迟检查:');
+        console.log('  - 节点仍然激活:', this.node?.active);
+        console.log('  - 缩放保持:', this.node?.scale);
+        console.log('  - 透明度保持:', this.node?.getComponent(UIOpacity)?.opacity);
+    }, 100);
+}
+
+// ===== 使用说明 =====
+/*
+使用步骤:
+1. 将上述对应的代码片段添加到GameEnd.ts和Wall.ts的相应方法中
+2. 在Cocos Creator中重新编译项目
+3. 运行游戏并让墙体血量归零
+4. 查看控制台输出,重点关注:
+   - GAME_DEFEAT事件是否被触发
+   - GameEnd组件是否接收到事件
+   - showEndPanelWithAnimation是否被调用
+   - 动画是否正确执行
+   - 强制显示测试是否有效
+
+如果强制显示有效,说明问题在动画系统;
+如果强制显示无效,说明问题在节点配置或UI层级。
+
+常见问题排查:
+1. animationDuration为0 → 动画不执行
+2. 节点被其他UI遮挡 → 检查siblingIndex
+3. UIOpacity组件缺失 → 透明度控制失效
+4. EventBus实例不一致 → 事件监听失效
+5. 节点在运行时被其他脚本修改 → 检查其他组件
+*/

+ 215 - 0
test_both_defeat_scenarios.js

@@ -0,0 +1,215 @@
+/**
+ * 测试墙体血量为0和菜单退出两种失败场景的GameEnd动画触发
+ * 验证两种情况是否都能正确显示GameEnd动画
+ */
+
+// 模拟事件系统
+class MockEventBus {
+    constructor() {
+        this.listeners = new Map();
+        this.eventLog = [];
+    }
+    
+    on(event, callback, context) {
+        if (!this.listeners.has(event)) {
+            this.listeners.set(event, []);
+        }
+        this.listeners.get(event).push({ callback, context });
+    }
+    
+    emit(event, ...args) {
+        this.eventLog.push({ event, args, timestamp: Date.now() });
+        console.log(`[EventBus] 触发事件: ${event}`);
+        
+        if (this.listeners.has(event)) {
+            this.listeners.get(event).forEach(({ callback, context }) => {
+                callback.call(context, ...args);
+            });
+        }
+    }
+    
+    getEventLog() {
+        return this.eventLog;
+    }
+    
+    clearLog() {
+        this.eventLog = [];
+    }
+}
+
+// 模拟GameEvents
+const GameEvents = {
+    GAME_DEFEAT: 'GAME_DEFEAT',
+    WALL_DESTROYED: 'WALL_DESTROYED',
+    GAME_RESUME: 'GAME_RESUME'
+};
+
+// 模拟AppState
+const AppState = {
+    IN_GAME: 'in_game'
+};
+
+// 模拟Wall组件
+class MockWall {
+    constructor(eventBus) {
+        this.eventBus = eventBus;
+        this.currentHealth = 100;
+        this.maxHealth = 100;
+    }
+    
+    takeDamage(damage) {
+        this.currentHealth -= damage;
+        console.log(`[Wall] 墙体受到伤害: ${damage}, 当前血量: ${this.currentHealth}`);
+        
+        if (this.currentHealth <= 0) {
+            this.onWallDestroyed();
+        }
+    }
+    
+    onWallDestroyed() {
+        console.log('[Wall] 墙体被摧毁,触发游戏失败');
+        
+        // 通过事件系统触发墙体被摧毁事件(保留用于其他监听器)
+        this.eventBus.emit(GameEvents.WALL_DESTROYED, {
+            finalHealth: this.currentHealth,
+            maxHealth: this.maxHealth
+        });
+        
+        // 统一失败处理:直接触发GAME_DEFEAT事件,与菜单退出处理保持一致
+        console.log('[Wall] 直接触发GAME_DEFEAT事件,与菜单退出失败处理流程一致');
+        this.eventBus.emit(GameEvents.GAME_DEFEAT);
+    }
+}
+
+// 模拟MenuController
+class MockMenuController {
+    constructor(eventBus, gameManager) {
+        this.eventBus = eventBus;
+        this.gameManager = gameManager;
+        this.isMenuOpen = false;
+    }
+    
+    async closeMenuWithoutResume() {
+        if (!this.isMenuOpen) return;
+        this.isMenuOpen = false;
+        console.log('[MenuController] 菜单已关闭(未触发游戏恢复)');
+    }
+    
+    async onBackButtonClick() {
+        console.log('[MenuController] 退出游戏按钮被点击');
+        const currentAppState = this.gameManager.getCurrentAppState();
+        console.log(`[MenuController] 当前应用状态: ${currentAppState}`);
+        
+        if (currentAppState === AppState.IN_GAME) {
+            console.log('[MenuController] 游戏中退出,触发GAME_DEFEAT事件显示游戏失败UI');
+            
+            // 先触发游戏失败事件
+            this.eventBus.emit(GameEvents.GAME_DEFEAT);
+            
+            // 关闭菜单(不触发GAME_RESUME事件)
+            await this.closeMenuWithoutResume();
+            
+            console.log('[MenuController] 菜单退出处理完成,已触发GAME_DEFEAT事件');
+        }
+    }
+}
+
+// 模拟GameManager
+class MockGameManager {
+    constructor() {
+        this.currentAppState = AppState.IN_GAME;
+    }
+    
+    getCurrentAppState() {
+        return this.currentAppState;
+    }
+}
+
+// 模拟GameEnd组件
+class MockGameEnd {
+    constructor(eventBus) {
+        this.eventBus = eventBus;
+        this.defeatProcessed = 0;
+        this.animationShown = 0;
+        this.setupEventListeners();
+    }
+    
+    setupEventListeners() {
+        this.eventBus.on(GameEvents.GAME_DEFEAT, this.onGameDefeat, this);
+        console.log('[GameEnd] 已注册GAME_DEFEAT事件监听器');
+    }
+    
+    onGameDefeat() {
+        console.log('[GameEnd] 接收到GAME_DEFEAT事件');
+        console.log('[GameEnd] 游戏失败事件处理,开始统一处理流程');
+        this.defeatProcessed++;
+        this.calculateAndShowRewards();
+    }
+    
+    calculateAndShowRewards() {
+        console.log('[GameEnd] 计算和显示奖励(包含面板动画显示)');
+        this.animationShown++;
+    }
+}
+
+// 测试函数
+function testBothDefeatScenarios() {
+    console.log('=== 测试墙体血量为0和菜单退出两种失败场景 ===\n');
+    
+    const eventBus = new MockEventBus();
+    const gameManager = new MockGameManager();
+    const menuController = new MockMenuController(eventBus, gameManager);
+    const gameEnd = new MockGameEnd(eventBus);
+    const wall = new MockWall(eventBus);
+    
+    console.log('1. 测试墙体血量为0场景:');
+    eventBus.clearLog();
+    wall.takeDamage(100); // 墙体血量归零
+    
+    console.log('\n2. 测试菜单退出场景:');
+    menuController.onBackButtonClick();
+    
+    console.log('\n=== 事件触发分析 ===');
+    const eventLog = eventBus.getEventLog();
+    eventLog.forEach((log, index) => {
+        console.log(`${index + 1}. 事件: ${log.event}`);
+    });
+    
+    console.log('\n=== 验证结果 ===');
+    console.log(`GameEnd处理GAME_DEFEAT事件次数: ${gameEnd.defeatProcessed}`);
+    console.log(`GameEnd显示动画次数: ${gameEnd.animationShown}`);
+    
+    // 分析事件
+    const gameDefeatEvents = eventLog.filter(log => log.event === GameEvents.GAME_DEFEAT);
+    const wallDestroyedEvents = eventLog.filter(log => log.event === GameEvents.WALL_DESTROYED);
+    const gameResumeEvents = eventLog.filter(log => log.event === GameEvents.GAME_RESUME);
+    
+    console.log(`\n=== 事件统计 ===`);
+    console.log(`GAME_DEFEAT事件数量: ${gameDefeatEvents.length}`);
+    console.log(`WALL_DESTROYED事件数量: ${wallDestroyedEvents.length}`);
+    console.log(`GAME_RESUME事件数量: ${gameResumeEvents.length}`);
+    
+    console.log(`\n=== 问题诊断 ===`);
+    if (gameEnd.defeatProcessed === 2 && gameEnd.animationShown === 2) {
+        console.log('✅ 两种失败场景都正确触发了GameEnd动画');
+    } else {
+        console.log('❌ 存在问题:某些失败场景没有正确触发GameEnd动画');
+        console.log(`   - 预期处理次数: 2, 实际处理次数: ${gameEnd.defeatProcessed}`);
+        console.log(`   - 预期动画次数: 2, 实际动画次数: ${gameEnd.animationShown}`);
+    }
+    
+    if (gameDefeatEvents.length === 2) {
+        console.log('✅ 两种场景都正确触发了GAME_DEFEAT事件');
+    } else {
+        console.log('❌ GAME_DEFEAT事件触发异常');
+    }
+    
+    if (gameResumeEvents.length === 0) {
+        console.log('✅ 没有事件冲突(无GAME_RESUME事件)');
+    } else {
+        console.log('❌ 存在事件冲突(检测到GAME_RESUME事件)');
+    }
+}
+
+// 运行测试
+testBothDefeatScenarios();

+ 431 - 0
test_comprehensive_defeat_scenarios.js

@@ -0,0 +1,431 @@
+/**
+ * 综合失败场景测试
+ * 模拟实际游戏中的复杂情况,包括UI层级、其他组件干扰等
+ */
+
+// 模拟事件系统
+class MockEventBus {
+    constructor() {
+        this.listeners = new Map();
+        this.eventLog = [];
+    }
+    
+    on(event, callback, context) {
+        if (!this.listeners.has(event)) {
+            this.listeners.set(event, []);
+        }
+        this.listeners.get(event).push({ callback, context });
+        console.log(`[EventBus] 注册事件监听器: ${event}`);
+    }
+    
+    emit(event, ...args) {
+        console.log(`[EventBus] 触发事件: ${event}`);
+        this.eventLog.push({ event, timestamp: Date.now(), args });
+        
+        if (this.listeners.has(event)) {
+            const listeners = this.listeners.get(event);
+            listeners.forEach(({ callback, context }) => {
+                try {
+                    callback.call(context, ...args);
+                } catch (error) {
+                    console.error(`[EventBus] 事件处理错误 ${event}:`, error);
+                }
+            });
+        }
+    }
+    
+    off(event, callback, context) {
+        if (this.listeners.has(event)) {
+            const listeners = this.listeners.get(event);
+            const index = listeners.findIndex(l => l.callback === callback && l.context === context);
+            if (index !== -1) {
+                listeners.splice(index, 1);
+                console.log(`[EventBus] 移除事件监听器: ${event}`);
+            }
+        }
+    }
+    
+    getEventLog() {
+        return this.eventLog;
+    }
+}
+
+// 模拟GameEvents
+const GameEvents = {
+    GAME_DEFEAT: 'GAME_DEFEAT',
+    GAME_RESUME: 'GAME_RESUME',
+    GAME_START: 'GAME_START',
+    RESET_UI_STATES: 'RESET_UI_STATES',
+    CURRENCY_CHANGED: 'CURRENCY_CHANGED'
+};
+
+// 模拟Wall组件
+class MockWall {
+    constructor(eventBus) {
+        this.eventBus = eventBus;
+        this.currentHealth = 100;
+        this.maxHealth = 100;
+    }
+    
+    takeDamage(damage) {
+        console.log(`[Wall] 受到伤害: ${damage}, 当前血量: ${this.currentHealth}`);
+        this.currentHealth -= damage;
+        
+        if (this.currentHealth <= 0) {
+            console.log('[Wall] 墙体血量为0,调用onWallDestroyed');
+            this.onWallDestroyed();
+        }
+    }
+    
+    onWallDestroyed() {
+        console.log('[Wall] 墙体被摧毁,触发GAME_DEFEAT事件');
+        this.eventBus.emit(GameEvents.GAME_DEFEAT);
+    }
+}
+
+// 模拟MenuController(修复后的版本)
+class MockMenuController {
+    constructor(eventBus) {
+        this.eventBus = eventBus;
+        this.isMenuOpen = false;
+        this.currentAppState = 'IN_GAME';
+    }
+    
+    async onBackButtonClick() {
+        console.log('[MenuController] 返回按钮被点击');
+        
+        if (this.currentAppState === 'IN_GAME') {
+            console.log('[MenuController] 游戏中退出,触发GAME_DEFEAT事件显示游戏失败UI');
+            
+            // 先触发游戏失败事件,让GameEnd面板显示
+            this.eventBus.emit(GameEvents.GAME_DEFEAT);
+            
+            // 关闭菜单(不触发GAME_RESUME事件,避免事件冲突)
+            await this.closeMenuWithoutResume();
+            
+            console.log('[MenuController] 菜单退出处理完成,已触发GAME_DEFEAT事件');
+        }
+    }
+    
+    async closeMenuWithoutResume() {
+        if (!this.isMenuOpen) return;
+        
+        this.isMenuOpen = false;
+        console.log('[MenuController] 菜单已关闭(未触发游戏恢复)');
+    }
+}
+
+// 模拟GameEnd组件(完整版本)
+class MockGameEnd {
+    constructor(eventBus) {
+        this.eventBus = eventBus;
+        this.animationCount = 0;
+        this.isVisible = false;
+        this.hasProcessedGameEnd = false;
+        this.currentRewards = { money: -1, diamonds: -1 };
+        this.isGameSuccess = false;
+        this.hasDoubledReward = false;
+        this.setupEventListeners();
+    }
+    
+    setupEventListeners() {
+        console.log('[GameEnd] 设置事件监听器');
+        this.eventBus.on(GameEvents.GAME_DEFEAT, this.onGameDefeat, this);
+        this.eventBus.on(GameEvents.GAME_START, this.onGameStart, this);
+        this.eventBus.on(GameEvents.RESET_UI_STATES, this.onResetUI, this);
+    }
+    
+    onGameDefeat() {
+        console.log('[GameEnd] 接收到GAME_DEFEAT事件');
+        console.log('[GameEnd] 游戏失败事件处理,开始统一处理流程');
+        
+        this.isGameSuccess = false;
+        this.calculateAndShowRewards();
+    }
+    
+    calculateAndShowRewards() {
+        if (this.hasProcessedGameEnd) {
+            console.log('[GameEnd] 游戏结束已处理过,跳过重复计算');
+            return;
+        }
+        
+        this.hasProcessedGameEnd = true;
+        console.log('[GameEnd] 计算和显示奖励');
+        
+        // 模拟奖励计算
+        this.currentRewards = { money: 50, diamonds: 2 };
+        
+        this.showEndPanelWithAnimation();
+    }
+    
+    showEndPanelWithAnimation() {
+        console.log('[GameEnd] 开始显示结算面板动画');
+        this.animationCount++;
+        this.isVisible = true;
+        console.log(`[GameEnd] GameEnd面板弹出动画第${this.animationCount}次执行`);
+    }
+    
+    onGameStart() {
+        console.log('[GameEnd] 收到游戏开始事件,重置奖励显示');
+        this.currentRewards = { money: 0, diamonds: 0 };
+        this.hasProcessedGameEnd = false;
+        this.hasDoubledReward = false;
+        this.isGameSuccess = false;
+        this.animationCount = 0;
+        this.isVisible = false;
+    }
+    
+    onResetUI() {
+        console.log('[GameEnd] 重置UI状态');
+        
+        // 重置所有状态,为下一局游戏做准备
+        this.hasDoubledReward = false;
+        // 注意:不重置currentRewards,保持奖励显示直到下次游戏开始
+        this.isGameSuccess = false;
+        this.hasProcessedGameEnd = false;
+        
+        console.log('[GameEnd] UI状态重置完成');
+    }
+    
+    getStatus() {
+        return {
+            animationCount: this.animationCount,
+            isVisible: this.isVisible,
+            hasProcessedGameEnd: this.hasProcessedGameEnd,
+            currentRewards: { ...this.currentRewards },
+            isGameSuccess: this.isGameSuccess
+        };
+    }
+}
+
+// 模拟UIStateManager
+class MockUIStateManager {
+    constructor(eventBus) {
+        this.eventBus = eventBus;
+        this.setupEventListeners();
+    }
+    
+    setupEventListeners() {
+        this.eventBus.on(GameEvents.RESET_UI_STATES, this.onResetUI, this);
+    }
+    
+    onResetUI() {
+        console.log('[UIStateManager] 接收到RESET_UI_STATES事件');
+        // 可能会影响其他UI组件
+    }
+}
+
+// 模拟GameManager
+class MockGameManager {
+    constructor(eventBus) {
+        this.eventBus = eventBus;
+        this.gameState = 'PLAYING';
+        this.setupEventListeners();
+    }
+    
+    setupEventListeners() {
+        this.eventBus.on(GameEvents.GAME_DEFEAT, this.onGameDefeat, this);
+    }
+    
+    onGameDefeat() {
+        console.log('[GameManager] 接收到GAME_DEFEAT事件');
+        this.gameState = 'DEFEAT';
+        console.log('[GameManager] 游戏状态切换为DEFEAT');
+    }
+    
+    onMainMenuClick() {
+        console.log('[GameManager] 主菜单按钮被点击');
+        // 可能触发RESET_UI_STATES事件
+        this.eventBus.emit(GameEvents.RESET_UI_STATES);
+    }
+}
+
+// 模拟MainUIController
+class MockMainUIController {
+    constructor(eventBus) {
+        this.eventBus = eventBus;
+        this.setupEventListeners();
+    }
+    
+    setupEventListeners() {
+        this.eventBus.on(GameEvents.CURRENCY_CHANGED, this.onCurrencyChanged, this);
+    }
+    
+    onCurrencyChanged() {
+        console.log('[MainUIController] 货币变化事件处理');
+    }
+    
+    onReturnToMainUI() {
+        console.log('[MainUIController] 返回主界面');
+        this.eventBus.emit(GameEvents.CURRENCY_CHANGED);
+    }
+}
+
+// 测试函数1:墙体血量为0场景
+function testWallDefeatScenario() {
+    console.log('\n=== 测试1:墙体血量为0失败场景 ===');
+    
+    const eventBus = new MockEventBus();
+    const wall = new MockWall(eventBus);
+    const gameEnd = new MockGameEnd(eventBus);
+    const gameManager = new MockGameManager(eventBus);
+    const uiStateManager = new MockUIStateManager(eventBus);
+    const mainUIController = new MockMainUIController(eventBus);
+    
+    console.log('\n--- 初始状态 ---');
+    console.log('墙体血量:', wall.currentHealth);
+    console.log('GameEnd状态:', gameEnd.getStatus());
+    
+    console.log('\n--- 模拟墙体受到致命伤害 ---');
+    wall.takeDamage(100);
+    
+    console.log('\n--- 检查结果 ---');
+    const status = gameEnd.getStatus();
+    console.log('GameEnd状态:', status);
+    
+    return {
+        scenario: 'wall_defeat',
+        success: status.animationCount === 1 && status.isVisible,
+        status: status,
+        eventLog: eventBus.getEventLog()
+    };
+}
+
+// 测试函数2:菜单退出场景
+function testMenuDefeatScenario() {
+    console.log('\n=== 测试2:菜单退出失败场景 ===');
+    
+    const eventBus = new MockEventBus();
+    const menuController = new MockMenuController(eventBus);
+    const gameEnd = new MockGameEnd(eventBus);
+    const gameManager = new MockGameManager(eventBus);
+    const uiStateManager = new MockUIStateManager(eventBus);
+    const mainUIController = new MockMainUIController(eventBus);
+    
+    console.log('\n--- 初始状态 ---');
+    console.log('GameEnd状态:', gameEnd.getStatus());
+    
+    console.log('\n--- 模拟菜单退出 ---');
+    menuController.onBackButtonClick();
+    
+    console.log('\n--- 检查结果 ---');
+    const status = gameEnd.getStatus();
+    console.log('GameEnd状态:', status);
+    
+    return {
+        scenario: 'menu_defeat',
+        success: status.animationCount === 1 && status.isVisible,
+        status: status,
+        eventLog: eventBus.getEventLog()
+    };
+}
+
+// 测试函数3:UI重置干扰场景
+function testUIResetInterferenceScenario() {
+    console.log('\n=== 测试3:UI重置干扰场景 ===');
+    
+    const eventBus = new MockEventBus();
+    const wall = new MockWall(eventBus);
+    const gameEnd = new MockGameEnd(eventBus);
+    const gameManager = new MockGameManager(eventBus);
+    const uiStateManager = new MockUIStateManager(eventBus);
+    const mainUIController = new MockMainUIController(eventBus);
+    
+    console.log('\n--- 初始状态 ---');
+    console.log('GameEnd状态:', gameEnd.getStatus());
+    
+    console.log('\n--- 模拟墙体血量为0 ---');
+    wall.takeDamage(100);
+    
+    console.log('\n--- 模拟UI重置事件干扰 ---');
+    gameManager.onMainMenuClick(); // 触发RESET_UI_STATES
+    
+    console.log('\n--- 检查结果 ---');
+    const status = gameEnd.getStatus();
+    console.log('GameEnd状态:', status);
+    
+    return {
+        scenario: 'ui_reset_interference',
+        success: status.animationCount === 1 && status.isVisible && status.currentRewards.money > 0,
+        status: status,
+        eventLog: eventBus.getEventLog()
+    };
+}
+
+// 运行所有测试
+function runAllTests() {
+    console.log('\n🧪 开始综合失败场景测试');
+    
+    const results = [];
+    
+    // 测试1:墙体血量为0
+    results.push(testWallDefeatScenario());
+    
+    // 测试2:菜单退出
+    results.push(testMenuDefeatScenario());
+    
+    // 测试3:UI重置干扰
+    results.push(testUIResetInterferenceScenario());
+    
+    console.log('\n=== 综合测试结果分析 ===');
+    
+    results.forEach((result, index) => {
+        console.log(`\n测试${index + 1} (${result.scenario}):`);
+        console.log('  测试通过:', result.success ? '✅' : '❌');
+        console.log('  动画执行次数:', result.status.animationCount);
+        console.log('  面板可见性:', result.status.isVisible);
+        console.log('  奖励数据:', result.status.currentRewards);
+        console.log('  处理状态:', result.status.hasProcessedGameEnd);
+        
+        const defeatEvents = result.eventLog.filter(log => log.event === GameEvents.GAME_DEFEAT);
+        const resumeEvents = result.eventLog.filter(log => log.event === GameEvents.GAME_RESUME);
+        const resetEvents = result.eventLog.filter(log => log.event === GameEvents.RESET_UI_STATES);
+        
+        console.log('  GAME_DEFEAT事件:', defeatEvents.length);
+        console.log('  GAME_RESUME事件:', resumeEvents.length);
+        console.log('  RESET_UI_STATES事件:', resetEvents.length);
+    });
+    
+    const allPassed = results.every(r => r.success);
+    console.log('\n=== 最终结果 ===');
+    if (allPassed) {
+        console.log('🎉 所有测试通过!失败处理流程工作正常。');
+    } else {
+        console.log('⚠️  部分测试失败,需要进一步调试。');
+        
+        const failedTests = results.filter(r => !r.success);
+        console.log('失败的测试场景:');
+        failedTests.forEach(test => {
+            console.log(`  - ${test.scenario}`);
+        });
+    }
+    
+    return {
+        allPassed,
+        results,
+        summary: {
+            total: results.length,
+            passed: results.filter(r => r.success).length,
+            failed: results.filter(r => !r.success).length
+        }
+    };
+}
+
+// 运行测试
+const testResults = runAllTests();
+console.log('\n📊 测试统计:');
+console.log('总测试数:', testResults.summary.total);
+console.log('通过数:', testResults.summary.passed);
+console.log('失败数:', testResults.summary.failed);
+console.log('通过率:', Math.round(testResults.summary.passed / testResults.summary.total * 100) + '%');
+
+if (testResults.allPassed) {
+    console.log('\n✨ 综合测试结论:失败处理流程已完全修复,两种失败场景都能正常工作!');
+} else {
+    console.log('\n🔍 需要进一步调试的问题:');
+    testResults.results.forEach(result => {
+        if (!result.success) {
+            console.log(`  - ${result.scenario}: 动画${result.status.animationCount}次,可见${result.status.isVisible}`);
+        }
+    });
+}

+ 0 - 0
test_game_end_fix.js


+ 573 - 0
test_gameend_node_debug.js

@@ -0,0 +1,573 @@
+/**
+ * GameEnd节点实际状态调试脚本
+ * 模拟实际游戏环境,检查GameEnd节点的各种状态
+ */
+
+// 模拟事件系统
+class MockEventBus {
+    constructor() {
+        this.listeners = new Map();
+        this.eventLog = [];
+    }
+    
+    on(event, callback, context) {
+        if (!this.listeners.has(event)) {
+            this.listeners.set(event, []);
+        }
+        this.listeners.get(event).push({ callback, context });
+        console.log(`[EventBus] 注册事件监听器: ${event}`);
+    }
+    
+    emit(event, ...args) {
+        console.log(`[EventBus] 触发事件: ${event}`);
+        this.eventLog.push({ event, timestamp: Date.now(), args });
+        
+        if (this.listeners.has(event)) {
+            const listeners = this.listeners.get(event);
+            listeners.forEach(({ callback, context }) => {
+                try {
+                    callback.call(context, ...args);
+                } catch (error) {
+                    console.error(`[EventBus] 事件处理错误 ${event}:`, error);
+                }
+            });
+        }
+    }
+    
+    off(event, callback, context) {
+        if (this.listeners.has(event)) {
+            const listeners = this.listeners.get(event);
+            const index = listeners.findIndex(l => l.callback === callback && l.context === context);
+            if (index !== -1) {
+                listeners.splice(index, 1);
+                console.log(`[EventBus] 移除事件监听器: ${event}`);
+            }
+        }
+    }
+    
+    getEventLog() {
+        return this.eventLog;
+    }
+}
+
+// 模拟GameEvents
+const GameEvents = {
+    GAME_DEFEAT: 'GAME_DEFEAT',
+    GAME_RESUME: 'GAME_RESUME',
+    GAME_START: 'GAME_START',
+    RESET_UI_STATES: 'RESET_UI_STATES'
+};
+
+// 模拟UIOpacity组件
+class MockUIOpacity {
+    constructor() {
+        this.opacity = 255;
+    }
+}
+
+// 模拟Node
+class MockNode {
+    constructor(name, parent = null) {
+        this.name = name;
+        this.active = true;
+        this.scale = { x: 1, y: 1, z: 1 };
+        this.position = { x: 0, y: 0, z: 0 };
+        this.parent = parent;
+        this.children = [];
+        this.components = new Map();
+        this.siblingIndex = 0;
+        
+        if (parent) {
+            parent.children.push(this);
+            this.siblingIndex = parent.children.length - 1;
+        }
+    }
+    
+    setScale(x, y, z) {
+        this.scale = { x, y, z };
+    }
+    
+    setPosition(x, y, z) {
+        this.position = { x, y, z };
+    }
+    
+    getComponent(componentType) {
+        return this.components.get(componentType) || null;
+    }
+    
+    addComponent(componentType) {
+        const component = new componentType();
+        this.components.set(componentType, component);
+        return component;
+    }
+    
+    setSiblingIndex(index) {
+        if (!this.parent) return;
+        
+        // 从当前位置移除
+        const currentIndex = this.parent.children.indexOf(this);
+        if (currentIndex !== -1) {
+            this.parent.children.splice(currentIndex, 1);
+        }
+        
+        // 插入到新位置
+        this.parent.children.splice(index, 0, this);
+        this.siblingIndex = index;
+        
+        // 更新其他子节点的siblingIndex
+        this.parent.children.forEach((child, i) => {
+            child.siblingIndex = i;
+        });
+    }
+    
+    getNodeStatus() {
+        return {
+            name: this.name,
+            active: this.active,
+            scale: { ...this.scale },
+            position: { ...this.position },
+            siblingIndex: this.siblingIndex,
+            opacity: this.getComponent(MockUIOpacity)?.opacity || 'N/A',
+            childrenCount: this.children.length
+        };
+    }
+}
+
+// 模拟Canvas层级结构
+function createMockCanvasStructure() {
+    const canvas = new MockNode('Canvas');
+    
+    // 创建主要UI面板
+    const gameLevelUI = new MockNode('GameLevelUI', canvas);
+    const topArea = new MockNode('TopArea', canvas);
+    const gameEnd = new MockNode('GameEnd', canvas);
+    const pauseMenu = new MockNode('PauseMenu', canvas);
+    const settingsPanel = new MockNode('SettingsPanel', canvas);
+    
+    // 设置初始层级顺序(siblingIndex越大越在上层)
+    gameLevelUI.setSiblingIndex(0);  // 最底层
+    topArea.setSiblingIndex(1);
+    gameEnd.setSiblingIndex(2);      // GameEnd在中间层
+    pauseMenu.setSiblingIndex(3);
+    settingsPanel.setSiblingIndex(4); // 最顶层
+    
+    // 为GameEnd添加UIOpacity组件
+    gameEnd.addComponent(MockUIOpacity);
+    
+    console.log('[Canvas] 模拟Canvas层级结构创建完成');
+    console.log('[Canvas] 节点层级顺序(从底到顶):');
+    canvas.children.forEach((child, index) => {
+        console.log(`  ${index}: ${child.name} (siblingIndex: ${child.siblingIndex})`);
+    });
+    
+    return { canvas, gameEnd, pauseMenu, settingsPanel };
+}
+
+// 模拟GameEnd组件(简化版)
+class MockGameEnd {
+    constructor(node, eventBus) {
+        this.node = node;
+        this.eventBus = eventBus;
+        this.animationDuration = 0.3;
+        this.isVisible = false;
+        this.animationCount = 0;
+        this.hasProcessedGameEnd = false;
+        this.setupEventListeners();
+        this.initializeHiddenStateKeepActive();
+    }
+    
+    setupEventListeners() {
+        this.eventBus.on(GameEvents.GAME_DEFEAT, this.onGameDefeat, this);
+        this.eventBus.on(GameEvents.RESET_UI_STATES, this.onResetUI, this);
+    }
+    
+    onGameDefeat() {
+        console.log('[GameEnd] 接收到GAME_DEFEAT事件');
+        console.log('[GameEnd] 游戏失败事件处理,开始统一处理流程');
+        
+        if (this.hasProcessedGameEnd) {
+            console.log('[GameEnd] 游戏结束已处理过,跳过重复计算');
+            return;
+        }
+        
+        this.hasProcessedGameEnd = true;
+        this.showEndPanelWithAnimation();
+    }
+    
+    showEndPanelWithAnimation() {
+        console.log('[GameEnd] 开始显示结算面板动画');
+        
+        // 检查节点状态
+        console.log('[GameEnd] 动画前节点状态:', this.node.getNodeStatus());
+        
+        // 确保节点处于激活状态
+        if (!this.node.active) {
+            this.node.active = true;
+            console.log('[GameEnd] 激活GameEnd节点');
+        }
+        
+        // 检查是否被其他面板遮挡
+        this.checkUILayering();
+        
+        // 确保有UIOpacity组件
+        let uiOpacity = this.node.getComponent(MockUIOpacity);
+        if (!uiOpacity) {
+            uiOpacity = this.node.addComponent(MockUIOpacity);
+            console.log('[GameEnd] 添加UIOpacity组件');
+        }
+        
+        // 播放动画
+        if (this.animationDuration > 0) {
+            console.log(`[GameEnd] 动画持续时间: ${this.animationDuration}秒,开始播放GameEnd面板弹出动画`);
+            this.playShowAnimation();
+        } else {
+            this.node.setScale(1, 1, 1);
+            uiOpacity.opacity = 255;
+            console.log('[GameEnd] 无动画配置,直接显示面板');
+        }
+        
+        this.animationCount++;
+        this.isVisible = true;
+        
+        console.log('[GameEnd] 动画后节点状态:', this.node.getNodeStatus());
+        console.log('[GameEnd] GameEnd面板显示流程完成');
+    }
+    
+    playShowAnimation() {
+        console.log('[GameEnd] 开始播放GameEnd面板弹出动画');
+        
+        // 设置节点位置到屏幕中心
+        this.node.setPosition(0, 0, 0);
+        console.log('[GameEnd] 已将面板居中到屏幕中央');
+        
+        // 设置初始状态
+        this.node.setScale(0.3, 0.3, 1);
+        const uiOpacity = this.node.getComponent(MockUIOpacity);
+        if (uiOpacity) {
+            uiOpacity.opacity = 0;
+        }
+        
+        console.log('[GameEnd] 设置动画初始状态 - 缩放: 0.3, 透明度: 0');
+        
+        // 模拟动画完成
+        setTimeout(() => {
+            this.node.setScale(1, 1, 1);
+            if (uiOpacity) {
+                uiOpacity.opacity = 255;
+            }
+            console.log('[GameEnd] GameEnd面板缩放动画完成');
+            console.log('[GameEnd] GameEnd面板淡入动画完成');
+        }, this.animationDuration * 1000);
+        
+        console.log('[GameEnd] GameEnd面板弹出动画开始执行');
+    }
+    
+    initializeHiddenStateKeepActive() {
+        const uiOpacity = this.node.getComponent(MockUIOpacity) || this.node.addComponent(MockUIOpacity);
+        uiOpacity.opacity = 0;
+        this.node.setScale(0.3, 0.3, 1);
+        console.log('[GameEnd] 初始化为隐藏状态(保持激活) - 缩放: 0.3, 透明度: 0');
+    }
+    
+    onResetUI() {
+        console.log('[GameEnd] 重置UI状态');
+        this.hasProcessedGameEnd = false;
+        this.isVisible = false;
+        this.node.active = false;
+        console.log('[GameEnd] UI状态重置完成');
+    }
+    
+    checkUILayering() {
+        if (!this.node.parent) return;
+        
+        console.log('[GameEnd] 检查UI层级:');
+        console.log(`[GameEnd] 当前GameEnd的siblingIndex: ${this.node.siblingIndex}`);
+        
+        // 检查是否有其他激活的UI面板在更高层级
+        const higherLevelPanels = this.node.parent.children.filter(child => 
+            child.siblingIndex > this.node.siblingIndex && child.active && child !== this.node
+        );
+        
+        if (higherLevelPanels.length > 0) {
+            console.log('[GameEnd] ⚠️  发现更高层级的激活面板:');
+            higherLevelPanels.forEach(panel => {
+                console.log(`[GameEnd]   - ${panel.name} (siblingIndex: ${panel.siblingIndex})`);
+            });
+            console.log('[GameEnd] 这些面板可能遮挡GameEnd显示');
+        } else {
+            console.log('[GameEnd] ✅ 没有更高层级的面板遮挡');
+        }
+    }
+    
+    getStatus() {
+        return {
+            animationCount: this.animationCount,
+            isVisible: this.isVisible,
+            hasProcessedGameEnd: this.hasProcessedGameEnd,
+            nodeStatus: this.node.getNodeStatus()
+        };
+    }
+}
+
+// 模拟Wall组件
+class MockWall {
+    constructor(eventBus) {
+        this.eventBus = eventBus;
+        this.currentHealth = 100;
+        this.maxHealth = 100;
+    }
+    
+    takeDamage(damage) {
+        console.log(`[Wall] 受到伤害: ${damage}, 当前血量: ${this.currentHealth}`);
+        this.currentHealth -= damage;
+        
+        if (this.currentHealth <= 0) {
+            console.log('[Wall] 墙体血量为0,调用onWallDestroyed');
+            this.onWallDestroyed();
+        }
+    }
+    
+    onWallDestroyed() {
+        console.log('[Wall] 墙体被摧毁,触发GAME_DEFEAT事件');
+        this.eventBus.emit(GameEvents.GAME_DEFEAT);
+    }
+}
+
+// 测试函数1:正常情况下的墙体失败
+function testNormalWallDefeat() {
+    console.log('\n=== 测试1:正常情况下的墙体失败 ===');
+    
+    const eventBus = new MockEventBus();
+    const { canvas, gameEnd: gameEndNode } = createMockCanvasStructure();
+    const gameEnd = new MockGameEnd(gameEndNode, eventBus);
+    const wall = new MockWall(eventBus);
+    
+    console.log('\n--- 初始状态 ---');
+    console.log('GameEnd状态:', gameEnd.getStatus());
+    
+    console.log('\n--- 模拟墙体血量为0 ---');
+    wall.takeDamage(100);
+    
+    console.log('\n--- 检查结果 ---');
+    const status = gameEnd.getStatus();
+    console.log('GameEnd状态:', status);
+    
+    return {
+        scenario: 'normal_wall_defeat',
+        success: status.animationCount === 1 && status.isVisible,
+        status: status
+    };
+}
+
+// 测试函数2:有其他UI面板干扰的情况
+function testUIInterferenceWallDefeat() {
+    console.log('\n=== 测试2:UI面板干扰情况下的墙体失败 ===');
+    
+    const eventBus = new MockEventBus();
+    const { canvas, gameEnd: gameEndNode, pauseMenu, settingsPanel } = createMockCanvasStructure();
+    const gameEnd = new MockGameEnd(gameEndNode, eventBus);
+    const wall = new MockWall(eventBus);
+    
+    console.log('\n--- 模拟其他UI面板激活 ---');
+    pauseMenu.active = true;
+    settingsPanel.active = true;
+    console.log('PauseMenu激活状态:', pauseMenu.active, 'siblingIndex:', pauseMenu.siblingIndex);
+    console.log('SettingsPanel激活状态:', settingsPanel.active, 'siblingIndex:', settingsPanel.siblingIndex);
+    
+    console.log('\n--- 初始状态 ---');
+    console.log('GameEnd状态:', gameEnd.getStatus());
+    
+    console.log('\n--- 模拟墙体血量为0 ---');
+    wall.takeDamage(100);
+    
+    console.log('\n--- 检查结果 ---');
+    const status = gameEnd.getStatus();
+    console.log('GameEnd状态:', status);
+    
+    return {
+        scenario: 'ui_interference_wall_defeat',
+        success: status.animationCount === 1 && status.isVisible,
+        status: status,
+        interference: {
+            pauseMenuActive: pauseMenu.active,
+            settingsPanelActive: settingsPanel.active
+        }
+    };
+}
+
+// 测试函数3:GameEnd层级被调整的情况
+function testLayerOrderWallDefeat() {
+    console.log('\n=== 测试3:GameEnd层级被调整情况下的墙体失败 ===');
+    
+    const eventBus = new MockEventBus();
+    const { canvas, gameEnd: gameEndNode, pauseMenu } = createMockCanvasStructure();
+    const gameEnd = new MockGameEnd(gameEndNode, eventBus);
+    const wall = new MockWall(eventBus);
+    
+    console.log('\n--- 模拟GameEnd层级被意外调整到最底层 ---');
+    gameEndNode.setSiblingIndex(0); // 调整到最底层
+    pauseMenu.active = true;
+    
+    console.log('调整后的层级顺序:');
+    canvas.children.forEach((child, index) => {
+        console.log(`  ${index}: ${child.name} (siblingIndex: ${child.siblingIndex}, active: ${child.active})`);
+    });
+    
+    console.log('\n--- 初始状态 ---');
+    console.log('GameEnd状态:', gameEnd.getStatus());
+    
+    console.log('\n--- 模拟墙体血量为0 ---');
+    wall.takeDamage(100);
+    
+    console.log('\n--- 检查结果 ---');
+    const status = gameEnd.getStatus();
+    console.log('GameEnd状态:', status);
+    
+    return {
+        scenario: 'layer_order_wall_defeat',
+        success: status.animationCount === 1 && status.isVisible,
+        status: status,
+        layerInfo: {
+            gameEndSiblingIndex: gameEndNode.siblingIndex,
+            pauseMenuSiblingIndex: pauseMenu.siblingIndex,
+            pauseMenuActive: pauseMenu.active
+        }
+    };
+}
+
+// 测试函数4:UI重置事件干扰
+function testUIResetInterference() {
+    console.log('\n=== 测试4:UI重置事件干扰情况 ===');
+    
+    const eventBus = new MockEventBus();
+    const { canvas, gameEnd: gameEndNode } = createMockCanvasStructure();
+    const gameEnd = new MockGameEnd(gameEndNode, eventBus);
+    const wall = new MockWall(eventBus);
+    
+    console.log('\n--- 初始状态 ---');
+    console.log('GameEnd状态:', gameEnd.getStatus());
+    
+    console.log('\n--- 模拟墙体血量为0 ---');
+    wall.takeDamage(100);
+    
+    console.log('\n--- 模拟UI重置事件干扰 ---');
+    setTimeout(() => {
+        eventBus.emit(GameEvents.RESET_UI_STATES);
+        
+        console.log('\n--- 检查UI重置后的结果 ---');
+        const statusAfterReset = gameEnd.getStatus();
+        console.log('UI重置后GameEnd状态:', statusAfterReset);
+    }, 100);
+    
+    console.log('\n--- 检查初始结果 ---');
+    const status = gameEnd.getStatus();
+    console.log('GameEnd状态:', status);
+    
+    return {
+        scenario: 'ui_reset_interference',
+        success: status.animationCount === 1 && status.isVisible,
+        status: status
+    };
+}
+
+// 运行所有测试
+function runAllTests() {
+    console.log('\n🔍 开始GameEnd节点状态调试测试');
+    
+    const results = [];
+    
+    // 测试1:正常情况
+    results.push(testNormalWallDefeat());
+    
+    // 测试2:UI干扰
+    results.push(testUIInterferenceWallDefeat());
+    
+    // 测试3:层级问题
+    results.push(testLayerOrderWallDefeat());
+    
+    // 测试4:UI重置干扰
+    results.push(testUIResetInterference());
+    
+    console.log('\n=== 调试测试结果分析 ===');
+    
+    results.forEach((result, index) => {
+        console.log(`\n测试${index + 1} (${result.scenario}):`);
+        console.log('  测试通过:', result.success ? '✅' : '❌');
+        console.log('  动画执行次数:', result.status.animationCount);
+        console.log('  面板可见性:', result.status.isVisible);
+        console.log('  节点激活状态:', result.status.nodeStatus.active);
+        console.log('  节点透明度:', result.status.nodeStatus.opacity);
+        console.log('  节点缩放:', result.status.nodeStatus.scale);
+        console.log('  节点层级:', result.status.nodeStatus.siblingIndex);
+        
+        if (result.interference) {
+            console.log('  UI干扰情况:', result.interference);
+        }
+        
+        if (result.layerInfo) {
+            console.log('  层级信息:', result.layerInfo);
+        }
+    });
+    
+    const allPassed = results.every(r => r.success);
+    console.log('\n=== 调试结论 ===');
+    if (allPassed) {
+        console.log('🎉 所有测试通过!GameEnd节点状态正常。');
+        console.log('💡 如果实际游戏中仍有问题,可能是以下原因:');
+        console.log('   1. 实际场景中的UI层级配置不同');
+        console.log('   2. 其他组件在运行时修改了GameEnd节点状态');
+        console.log('   3. 动画系统或渲染系统的问题');
+        console.log('   4. GameEnd组件的某些属性在编辑器中配置不正确');
+    } else {
+        console.log('⚠️  发现潜在问题:');
+        
+        const failedTests = results.filter(r => !r.success);
+        failedTests.forEach(test => {
+            console.log(`  - ${test.scenario}: 可能的问题原因`);
+            if (test.scenario.includes('interference')) {
+                console.log('    → UI面板层级冲突,GameEnd被其他面板遮挡');
+            }
+            if (test.scenario.includes('layer_order')) {
+                console.log('    → GameEnd的siblingIndex被意外调整到较低层级');
+            }
+            if (test.scenario.includes('reset')) {
+                console.log('    → UI重置事件在不当时机触发,影响GameEnd显示');
+            }
+        });
+    }
+    
+    return {
+        allPassed,
+        results,
+        summary: {
+            total: results.length,
+            passed: results.filter(r => r.success).length,
+            failed: results.filter(r => !r.success).length
+        }
+    };
+}
+
+// 运行测试
+const testResults = runAllTests();
+console.log('\n📊 调试测试统计:');
+console.log('总测试数:', testResults.summary.total);
+console.log('通过数:', testResults.summary.passed);
+console.log('失败数:', testResults.summary.failed);
+console.log('通过率:', Math.round(testResults.summary.passed / testResults.summary.total * 100) + '%');
+
+if (testResults.allPassed) {
+    console.log('\n✨ 调试结论:GameEnd节点逻辑正常,问题可能在实际游戏环境的配置或其他组件干扰!');
+    console.log('\n🔧 建议检查项目:');
+    console.log('   1. 在Cocos Creator编辑器中检查GameEnd节点的层级位置');
+    console.log('   2. 确认GameEnd组件的animationDuration属性设置正确(>0)');
+    console.log('   3. 检查是否有其他脚本在运行时修改GameEnd节点状态');
+    console.log('   4. 确认GameEnd节点在场景中的初始active状态');
+    console.log('   5. 检查Canvas的渲染模式和UI摄像机设置');
+} else {
+    console.log('\n🔍 需要进一步调试的问题:');
+    testResults.results.forEach(result => {
+        if (!result.success) {
+            console.log(`  - ${result.scenario}: 动画${result.status.animationCount}次,可见${result.status.isVisible}`);
+        }
+    });
+}

+ 416 - 0
test_realtime_gameend_debug.js

@@ -0,0 +1,416 @@
+/**
+ * 实时GameEnd调试脚本
+ * 模拟实际游戏环境,检查墙体血量为0时GameEnd的显示问题
+ */
+
+// 模拟GameEvents
+const GameEvents = {
+    GAME_DEFEAT: 'GAME_DEFEAT',
+    GAME_RESUME: 'GAME_RESUME',
+    RESET_UI_STATES: 'RESET_UI_STATES'
+};
+
+// 模拟EventBus
+class MockEventBus {
+    constructor() {
+        this.listeners = new Map();
+    }
+    
+    on(event, callback, context) {
+        if (!this.listeners.has(event)) {
+            this.listeners.set(event, []);
+        }
+        this.listeners.get(event).push({ callback, context });
+    }
+    
+    emit(event, ...args) {
+        console.log(`[EventBus] 触发事件: ${event}`);
+        if (this.listeners.has(event)) {
+            this.listeners.get(event).forEach(({ callback, context }) => {
+                callback.call(context, ...args);
+            });
+        }
+    }
+    
+    off(event, callback, context) {
+        if (this.listeners.has(event)) {
+            const listeners = this.listeners.get(event);
+            const index = listeners.findIndex(l => l.callback === callback && l.context === context);
+            if (index !== -1) {
+                listeners.splice(index, 1);
+            }
+        }
+    }
+}
+
+// 模拟UIOpacity组件
+class MockUIOpacity {
+    constructor() {
+        this.opacity = 255;
+    }
+}
+
+// 模拟Node
+class MockNode {
+    constructor(name, siblingIndex = 0) {
+        this.name = name;
+        this.active = true;
+        this.siblingIndex = siblingIndex;
+        this.scale = { x: 1, y: 1, z: 1 };
+        this.position = { x: 0, y: 0, z: 0 };
+        this.children = [];
+        this.parent = null;
+        this.components = new Map();
+    }
+    
+    setScale(x, y, z) {
+        this.scale = { x, y, z };
+    }
+    
+    setPosition(x, y, z) {
+        this.position = { x, y, z };
+    }
+    
+    getComponent(componentClass) {
+        return this.components.get(componentClass);
+    }
+    
+    addComponent(componentClass) {
+        const component = new componentClass();
+        this.components.set(componentClass, component);
+        return component;
+    }
+    
+    getSiblingIndex() {
+        return this.siblingIndex;
+    }
+    
+    setSiblingIndex(index) {
+        this.siblingIndex = index;
+    }
+    
+    getNodeStatus() {
+        return {
+            name: this.name,
+            active: this.active,
+            siblingIndex: this.siblingIndex,
+            scale: this.scale,
+            position: this.position,
+            opacity: this.getComponent(MockUIOpacity)?.opacity || 'N/A'
+        };
+    }
+}
+
+// 模拟GameEnd组件(重点关注实际问题)
+class MockGameEnd {
+    constructor(eventBus) {
+        this.eventBus = eventBus;
+        this.node = new MockNode('GameEnd', 2);
+        this.animationDuration = 0.3; // 默认动画时长
+        this.hasProcessedGameEnd = false;
+        this.isVisible = false;
+        this.animationCount = 0;
+        this.debugMode = true;
+        
+        // 添加UIOpacity组件
+        this.node.addComponent(MockUIOpacity);
+        
+        this.setupEventListeners();
+        this.initializeHiddenStateKeepActive();
+    }
+    
+    setupEventListeners() {
+        console.log('[GameEnd] 设置事件监听器');
+        this.eventBus.on(GameEvents.GAME_DEFEAT, this.onGameDefeat, this);
+        this.eventBus.on(GameEvents.RESET_UI_STATES, this.onResetUI, this);
+    }
+    
+    onGameDefeat() {
+        console.log('\n=== GameEnd.onGameDefeat() 开始 ===');
+        console.log('[GameEnd] 接收到GAME_DEFEAT事件');
+        
+        // 检查是否已处理过
+        if (this.hasProcessedGameEnd) {
+            console.log('[GameEnd] ⚠️  已处理过游戏结束,跳过重复处理');
+            return;
+        }
+        
+        console.log('[GameEnd] 开始处理游戏失败逻辑');
+        
+        // 记录处理状态
+        this.hasProcessedGameEnd = true;
+        
+        // 调试:检查节点状态
+        if (this.debugMode) {
+            console.log('[GameEnd] 🔍 调试信息 - 处理前节点状态:');
+            console.log('  - 节点激活:', this.node.active);
+            console.log('  - 节点层级:', this.node.siblingIndex);
+            console.log('  - 节点缩放:', this.node.scale);
+            console.log('  - 透明度:', this.node.getComponent(MockUIOpacity)?.opacity);
+            console.log('  - 动画时长:', this.animationDuration);
+        }
+        
+        // 显示结算面板
+        this.showEndPanelWithAnimation();
+        
+        console.log('=== GameEnd.onGameDefeat() 结束 ===\n');
+    }
+    
+    showEndPanelWithAnimation() {
+        console.log('[GameEnd] 🎬 开始显示结算面板动画');
+        
+        // 调试:检查动画前状态
+        if (this.debugMode) {
+            console.log('[GameEnd] 🔍 动画前状态检查:');
+            console.log('  - animationDuration:', this.animationDuration);
+            console.log('  - 节点激活状态:', this.node.active);
+            console.log('  - 当前透明度:', this.node.getComponent(MockUIOpacity)?.opacity);
+            console.log('  - 当前缩放:', this.node.scale);
+        }
+        
+        // 确保节点激活
+        if (!this.node.active) {
+            this.node.active = true;
+            console.log('[GameEnd] ✅ 激活GameEnd节点');
+        }
+        
+        // 检查动画配置
+        if (this.animationDuration <= 0) {
+            console.log('[GameEnd] ⚠️  动画时长为0或负数,直接显示面板');
+            this.directShow();
+            return;
+        }
+        
+        // 播放动画
+        console.log(`[GameEnd] 🎭 开始播放动画,持续时间: ${this.animationDuration}秒`);
+        this.playShowAnimation();
+        
+        this.animationCount++;
+        this.isVisible = true;
+        
+        console.log('[GameEnd] ✅ 显示面板流程完成');
+    }
+    
+    playShowAnimation() {
+        console.log('[GameEnd] 🎯 执行显示动画');
+        
+        // 获取UIOpacity组件
+        const uiOpacity = this.node.getComponent(MockUIOpacity);
+        if (!uiOpacity) {
+            console.error('[GameEnd] ❌ UIOpacity组件缺失!');
+            return;
+        }
+        
+        // 设置初始状态
+        this.node.setScale(0.3, 0.3, 1);
+        uiOpacity.opacity = 0;
+        
+        console.log('[GameEnd] 🎬 设置动画初始状态 - 缩放: 0.3, 透明度: 0');
+        
+        // 模拟动画执行
+        setTimeout(() => {
+            this.node.setScale(1, 1, 1);
+            uiOpacity.opacity = 255;
+            
+            console.log('[GameEnd] ✨ 动画完成 - 缩放: 1, 透明度: 255');
+            
+            // 调试:检查最终状态
+            if (this.debugMode) {
+                console.log('[GameEnd] 🔍 动画完成后最终状态:');
+                console.log('  - 节点激活:', this.node.active);
+                console.log('  - 节点缩放:', this.node.scale);
+                console.log('  - 透明度:', uiOpacity.opacity);
+                console.log('  - 是否可见:', this.isVisible);
+            }
+        }, this.animationDuration * 1000);
+    }
+    
+    directShow() {
+        console.log('[GameEnd] 🚀 直接显示面板(无动画)');
+        
+        this.node.setScale(1, 1, 1);
+        const uiOpacity = this.node.getComponent(MockUIOpacity);
+        if (uiOpacity) {
+            uiOpacity.opacity = 255;
+        }
+        
+        this.isVisible = true;
+        console.log('[GameEnd] ✅ 直接显示完成');
+    }
+    
+    initializeHiddenStateKeepActive() {
+        const uiOpacity = this.node.getComponent(MockUIOpacity) || this.node.addComponent(MockUIOpacity);
+        uiOpacity.opacity = 0;
+        this.node.setScale(0.3, 0.3, 1);
+        console.log('[GameEnd] 🔧 初始化为隐藏状态(保持激活)');
+    }
+    
+    onResetUI() {
+        console.log('[GameEnd] 🔄 重置UI状态');
+        this.hasProcessedGameEnd = false;
+        this.isVisible = false;
+        this.node.active = false;
+    }
+    
+    getStatus() {
+        return {
+            animationCount: this.animationCount,
+            isVisible: this.isVisible,
+            hasProcessedGameEnd: this.hasProcessedGameEnd,
+            nodeStatus: this.node.getNodeStatus()
+        };
+    }
+}
+
+// 模拟Wall组件
+class MockWall {
+    constructor(eventBus) {
+        this.eventBus = eventBus;
+        this.currentHealth = 100;
+        this.maxHealth = 100;
+    }
+    
+    takeDamage(damage) {
+        this.currentHealth -= damage;
+        console.log(`[Wall] 受到伤害: ${damage}, 剩余血量: ${this.currentHealth}`);
+        
+        if (this.currentHealth <= 0) {
+            this.onWallDestroyed();
+        }
+    }
+    
+    onWallDestroyed() {
+        console.log('\n🧱 [Wall] 墙体被摧毁!');
+        console.log('[Wall] 即将触发GAME_DEFEAT事件');
+        
+        // 触发游戏失败事件
+        this.eventBus.emit(GameEvents.GAME_DEFEAT);
+        
+        console.log('[Wall] GAME_DEFEAT事件已触发\n');
+    }
+}
+
+// 测试函数
+function testWallDefeatGameEndDisplay() {
+    console.log('\n🎮 === 墙体血量为0时GameEnd显示测试 ===\n');
+    
+    const eventBus = new MockEventBus();
+    const gameEnd = new MockGameEnd(eventBus);
+    const wall = new MockWall(eventBus);
+    
+    console.log('📋 初始状态:');
+    console.log('  - GameEnd状态:', gameEnd.getStatus());
+    console.log('  - Wall血量:', wall.currentHealth);
+    
+    console.log('\n🎯 开始测试:让墙体血量归零...');
+    
+    // 模拟墙体受到致命伤害
+    wall.takeDamage(100);
+    
+    // 等待动画完成
+    setTimeout(() => {
+        console.log('\n📊 测试结果:');
+        const finalStatus = gameEnd.getStatus();
+        console.log('  - 动画执行次数:', finalStatus.animationCount);
+        console.log('  - 面板是否可见:', finalStatus.isVisible);
+        console.log('  - 是否已处理游戏结束:', finalStatus.hasProcessedGameEnd);
+        console.log('  - 节点最终状态:', finalStatus.nodeStatus);
+        
+        // 判断测试结果
+        const success = finalStatus.animationCount === 1 && 
+                       finalStatus.isVisible === true && 
+                       finalStatus.hasProcessedGameEnd === true &&
+                       finalStatus.nodeStatus.active === true &&
+                       finalStatus.nodeStatus.opacity === 255;
+        
+        console.log('\n🏆 测试结论:', success ? '✅ 成功' : '❌ 失败');
+        
+        if (success) {
+            console.log('🎉 墙体血量为0时GameEnd动画正常显示!');
+        } else {
+            console.log('⚠️  墙体血量为0时GameEnd动画显示异常!');
+            console.log('🔍 可能的问题:');
+            
+            if (finalStatus.animationCount === 0) {
+                console.log('  - 动画未执行:可能是事件监听器未正确注册');
+            }
+            if (!finalStatus.isVisible) {
+                console.log('  - 面板不可见:可能是动画执行失败或被其他UI遮挡');
+            }
+            if (!finalStatus.hasProcessedGameEnd) {
+                console.log('  - 未处理游戏结束:可能是onGameDefeat方法未被调用');
+            }
+            if (!finalStatus.nodeStatus.active) {
+                console.log('  - 节点未激活:可能是节点状态被意外修改');
+            }
+            if (finalStatus.nodeStatus.opacity !== 255) {
+                console.log('  - 透明度异常:可能是动画未正确执行或被重置');
+            }
+        }
+        
+        return success;
+    }, 500); // 等待动画完成
+}
+
+// 对比测试:菜单退出vs墙体血量为0
+function compareDefeatScenarios() {
+    console.log('\n🔄 === 对比测试:菜单退出 vs 墙体血量为0 ===\n');
+    
+    // 测试1:菜单退出
+    console.log('📱 测试1:菜单退出触发GAME_DEFEAT');
+    const eventBus1 = new MockEventBus();
+    const gameEnd1 = new MockGameEnd(eventBus1);
+    
+    eventBus1.emit(GameEvents.GAME_DEFEAT); // 直接触发
+    
+    setTimeout(() => {
+        const status1 = gameEnd1.getStatus();
+        console.log('  结果:', status1.isVisible ? '✅ 显示正常' : '❌ 显示异常');
+        
+        // 测试2:墙体血量为0
+        console.log('\n🧱 测试2:墙体血量为0触发GAME_DEFEAT');
+        const eventBus2 = new MockEventBus();
+        const gameEnd2 = new MockGameEnd(eventBus2);
+        const wall2 = new MockWall(eventBus2);
+        
+        wall2.takeDamage(100); // 通过Wall组件触发
+        
+        setTimeout(() => {
+            const status2 = gameEnd2.getStatus();
+            console.log('  结果:', status2.isVisible ? '✅ 显示正常' : '❌ 显示异常');
+            
+            // 对比结果
+            console.log('\n📊 对比结果:');
+            console.log('  菜单退出 - 动画次数:', status1.animationCount, '可见性:', status1.isVisible);
+            console.log('  墙体血量0 - 动画次数:', status2.animationCount, '可见性:', status2.isVisible);
+            
+            const consistent = status1.isVisible === status2.isVisible && 
+                             status1.animationCount === status2.animationCount;
+            
+            console.log('\n🎯 一致性检查:', consistent ? '✅ 一致' : '❌ 不一致');
+            
+            if (!consistent) {
+                console.log('⚠️  发现不一致!这可能解释了为什么墙体血量为0时动画不显示。');
+                console.log('🔍 建议检查:');
+                console.log('  1. Wall组件的事件触发时机');
+                console.log('  2. EventBus实例是否一致');
+                console.log('  3. 是否有其他组件在Wall触发事件后干扰GameEnd');
+            }
+        }, 500);
+    }, 500);
+}
+
+// 运行测试
+console.log('🚀 开始实时GameEnd调试测试...');
+testWallDefeatGameEndDisplay();
+
+setTimeout(() => {
+    compareDefeatScenarios();
+}, 1500);
+
+console.log('\n💡 如果测试显示正常但实际游戏仍有问题,请检查:');
+console.log('  1. Cocos Creator编辑器中GameEnd节点的animationDuration属性');
+console.log('  2. GameEnd节点在Canvas中的层级位置(siblingIndex)');
+console.log('  3. 是否有其他UI组件在运行时修改GameEnd节点状态');
+console.log('  4. EventBus实例在实际游戏中是否一致');
+console.log('  5. 动画系统在实际环境中的表现');

+ 232 - 0
test_wall_defeat_debug.js

@@ -0,0 +1,232 @@
+/**
+ * 墙体血量为0失败场景调试测试
+ * 模拟实际游戏中墙体血量为0的情况,检查GameEnd动画是否正常触发
+ */
+
+// 模拟事件系统
+class MockEventBus {
+    constructor() {
+        this.listeners = new Map();
+        this.eventLog = [];
+    }
+    
+    on(event, callback, context) {
+        if (!this.listeners.has(event)) {
+            this.listeners.set(event, []);
+        }
+        this.listeners.get(event).push({ callback, context });
+        console.log(`[EventBus] 注册事件监听器: ${event}`);
+    }
+    
+    emit(event, ...args) {
+        console.log(`[EventBus] 触发事件: ${event}`);
+        this.eventLog.push({ event, timestamp: Date.now(), args });
+        
+        if (this.listeners.has(event)) {
+            const listeners = this.listeners.get(event);
+            listeners.forEach(({ callback, context }) => {
+                try {
+                    callback.call(context, ...args);
+                } catch (error) {
+                    console.error(`[EventBus] 事件处理错误 ${event}:`, error);
+                }
+            });
+        }
+    }
+    
+    getEventLog() {
+        return this.eventLog;
+    }
+}
+
+// 模拟GameEvents
+const GameEvents = {
+    GAME_DEFEAT: 'GAME_DEFEAT',
+    GAME_RESUME: 'GAME_RESUME',
+    GAME_START: 'GAME_START'
+};
+
+// 模拟Wall组件
+class MockWall {
+    constructor(eventBus) {
+        this.eventBus = eventBus;
+        this.currentHealth = 100;
+        this.maxHealth = 100;
+    }
+    
+    takeDamage(damage) {
+        console.log(`[Wall] 受到伤害: ${damage}, 当前血量: ${this.currentHealth}`);
+        this.currentHealth -= damage;
+        
+        if (this.currentHealth <= 0) {
+            console.log('[Wall] 墙体血量为0,调用onWallDestroyed');
+            this.onWallDestroyed();
+        }
+    }
+    
+    onWallDestroyed() {
+        console.log('[Wall] 墙体被摧毁,触发GAME_DEFEAT事件');
+        this.eventBus.emit(GameEvents.GAME_DEFEAT);
+    }
+}
+
+// 模拟GameEnd组件
+class MockGameEnd {
+    constructor(eventBus) {
+        this.eventBus = eventBus;
+        this.animationCount = 0;
+        this.isVisible = false;
+        this.setupEventListeners();
+    }
+    
+    setupEventListeners() {
+        console.log('[GameEnd] 设置事件监听器');
+        this.eventBus.on(GameEvents.GAME_DEFEAT, this.onGameDefeat, this);
+        this.eventBus.on(GameEvents.GAME_START, this.onGameStart, this);
+    }
+    
+    onGameDefeat() {
+        console.log('[GameEnd] 接收到GAME_DEFEAT事件');
+        console.log('[GameEnd] 游戏失败事件处理,开始统一处理流程');
+        
+        // 模拟GameEnd的处理逻辑
+        this.calculateAndShowRewards();
+    }
+    
+    calculateAndShowRewards() {
+        console.log('[GameEnd] 计算和显示奖励');
+        this.showEndPanelWithAnimation();
+    }
+    
+    showEndPanelWithAnimation() {
+        console.log('[GameEnd] 开始显示结算面板动画');
+        this.animationCount++;
+        this.isVisible = true;
+        console.log(`[GameEnd] GameEnd面板弹出动画第${this.animationCount}次执行`);
+    }
+    
+    onGameStart() {
+        console.log('[GameEnd] 收到游戏开始事件,重置奖励显示');
+        this.animationCount = 0;
+        this.isVisible = false;
+    }
+    
+    getStatus() {
+        return {
+            animationCount: this.animationCount,
+            isVisible: this.isVisible
+        };
+    }
+}
+
+// 模拟其他可能干扰的组件
+class MockGameManager {
+    constructor(eventBus) {
+        this.eventBus = eventBus;
+        this.gameState = 'PLAYING';
+        this.setupEventListeners();
+    }
+    
+    setupEventListeners() {
+        this.eventBus.on(GameEvents.GAME_DEFEAT, this.onGameDefeat, this);
+    }
+    
+    onGameDefeat() {
+        console.log('[GameManager] 接收到GAME_DEFEAT事件');
+        this.gameState = 'DEFEAT';
+        console.log('[GameManager] 游戏状态切换为DEFEAT');
+    }
+}
+
+// 模拟InGameManager
+class MockInGameManager {
+    constructor(eventBus) {
+        this.eventBus = eventBus;
+        this.currentState = 'PLAYING';
+        this.setupEventListeners();
+    }
+    
+    setupEventListeners() {
+        this.eventBus.on(GameEvents.GAME_DEFEAT, this.onGameDefeat, this);
+    }
+    
+    onGameDefeat() {
+        console.log('[InGameManager] 接收到GAME_DEFEAT事件');
+        this.currentState = 'DEFEAT';
+        console.log('[InGameManager] 游戏状态切换为DEFEAT');
+    }
+    
+    getCurrentState() {
+        return this.currentState;
+    }
+}
+
+// 测试函数
+function testWallDefeatScenario() {
+    console.log('\n=== 墙体血量为0失败场景测试 ===');
+    
+    // 创建事件系统
+    const eventBus = new MockEventBus();
+    
+    // 创建组件
+    const wall = new MockWall(eventBus);
+    const gameEnd = new MockGameEnd(eventBus);
+    const gameManager = new MockGameManager(eventBus);
+    const inGameManager = new MockInGameManager(eventBus);
+    
+    console.log('\n--- 初始状态 ---');
+    console.log('墙体血量:', wall.currentHealth);
+    console.log('GameEnd状态:', gameEnd.getStatus());
+    
+    console.log('\n--- 模拟墙体受到致命伤害 ---');
+    wall.takeDamage(100); // 造成致命伤害
+    
+    console.log('\n--- 检查结果 ---');
+    const gameEndStatus = gameEnd.getStatus();
+    console.log('GameEnd动画执行次数:', gameEndStatus.animationCount);
+    console.log('GameEnd是否可见:', gameEndStatus.isVisible);
+    console.log('GameManager状态:', gameManager.gameState);
+    console.log('InGameManager状态:', inGameManager.getCurrentState());
+    
+    console.log('\n--- 事件日志 ---');
+    const eventLog = eventBus.getEventLog();
+    eventLog.forEach((log, index) => {
+        console.log(`${index + 1}. 事件: ${log.event}, 时间: ${new Date(log.timestamp).toLocaleTimeString()}`);
+    });
+    
+    console.log('\n--- 测试结果分析 ---');
+    if (gameEndStatus.animationCount === 1 && gameEndStatus.isVisible) {
+        console.log('✅ 墙体血量为0成功触发GameEnd动画');
+    } else if (gameEndStatus.animationCount === 0) {
+        console.log('❌ 墙体血量为0未触发GameEnd动画');
+    } else if (gameEndStatus.animationCount > 1) {
+        console.log(`❌ GameEnd动画被重复触发${gameEndStatus.animationCount}次`);
+    }
+    
+    const defeatEvents = eventLog.filter(log => log.event === GameEvents.GAME_DEFEAT);
+    console.log(`GAME_DEFEAT事件触发次数: ${defeatEvents.length}`);
+    
+    const resumeEvents = eventLog.filter(log => log.event === GameEvents.GAME_RESUME);
+    console.log(`GAME_RESUME事件触发次数: ${resumeEvents.length}`);
+    
+    return {
+        success: gameEndStatus.animationCount === 1 && gameEndStatus.isVisible,
+        animationCount: gameEndStatus.animationCount,
+        defeatEventCount: defeatEvents.length,
+        resumeEventCount: resumeEvents.length
+    };
+}
+
+// 运行测试
+const result = testWallDefeatScenario();
+console.log('\n=== 最终测试结果 ===');
+console.log('测试通过:', result.success);
+console.log('动画执行次数:', result.animationCount);
+console.log('GAME_DEFEAT事件次数:', result.defeatEventCount);
+console.log('GAME_RESUME事件次数:', result.resumeEventCount);
+
+if (result.success) {
+    console.log('\n🎉 墙体血量为0的失败处理流程正常工作!');
+} else {
+    console.log('\n⚠️  墙体血量为0的失败处理流程存在问题,需要进一步调试。');
+}

+ 675 - 0
游戏结束流程优化方案.md

@@ -0,0 +1,675 @@
+# 游戏结束流程优化方案
+
+## 问题分析
+
+### 核心问题
+1. **奖励计算逻辑正确,但显示异常**:通过测试验证,`SaveDataManager`的奖励计算逻辑完全正确,能够正确读取配置并给予奖励
+2. **时序问题导致显示错误**:问题出现在UI显示和事件处理的时序上
+
+### 具体问题定位
+1. `GameEnd.ts`中的`currentRewards`被意外重置为`{money: -1, diamonds: -1}`
+2. 当某些关卡的奖励配置确实为0时,会被误判为"已计算过"
+3. **UI重置事件在奖励计算后触发,导致显示被覆盖**(已确认并修复)
+
+### 根本原因
+**事件时序冲突**:当用户点击返回主菜单按钮时,`GameManager.onMainMenuClick()`方法会触发`RESET_UI_STATES`事件,这会导致`GameEnd.onResetUI()`方法被调用,将`currentRewards`重置为`{money: -1, diamonds: -1}`,覆盖了之前正确计算的奖励显示。
+
+### 历史问题(已解决)
+根据控制台输出和代码分析,发现游戏结束后存在重复执行的问题:
+- 奖励计算重复执行
+- UI显示重复刷新
+- 数据清理重复调用
+- 数据重置重复执行
+
+## 游戏结束两个阶段的职责分工
+
+### 阶段一:UI弹出阶段(GameEnd面板显示)
+
+**触发事件:** `GAME_SUCCESS` 或 `GAME_DEFEAT`
+
+**负责组件:** `GameEnd.ts`
+
+**职责范围:**
+1. **奖励计算和显示**
+   - 调用 `SaveDataManager.giveCompletionRewards()` 或 `giveFailureRewards()`
+   - 更新奖励UI显示(钞票、钻石数量)
+   - 处理双倍奖励逻辑
+
+2. **UI状态管理**
+   - 显示GameEnd面板动画
+   - 设置EndLabel文本(SUCCESS/DEFEAT)
+   - 播放对应音效(胜利/失败)
+   - 重置双倍奖励按钮状态
+
+3. **游戏状态设置**
+   - 设置InGameManager的游戏状态为SUCCESS或DEFEAT
+   - 防止重复处理的状态检查
+
+**不负责的事项:**
+- ❌ 敌人清理(应在游戏逻辑层处理)
+- ❌ 数据重置(应在真正结束阶段处理)
+- ❌ 镜头重置(应在返回主界面时处理)
+
+### 阶段二:真正结束阶段(返回主界面)
+
+**触发事件:** `CONTINUE_CLICK` → `RETURN_TO_MAIN_MENU`
+
+**负责组件:** `GameManager.ts` → `NavBarController.ts` → `MainUIController.ts`
+
+**职责范围:**
+
+#### GameManager.onMainMenuClick()
+1. **游戏数据清理**
+   - 调用 `inGameManager.triggerGameDataCleanup()`
+   - 清理敌人、子弹、特效等游戏对象
+   - 重置GameManager自身记录
+
+2. **关卡进度管理**
+   - 根据游戏结果决定关卡进度(胜利+1,失败保持)
+   - 更新SaveDataManager中的当前关卡
+
+3. **应用状态切换**
+   - 设置应用状态为MAIN_MENU
+   - 发送RESET_UI_STATES事件
+   - 发送RETURN_TO_MAIN_MENU事件
+
+#### NavBarController.onReturnToMainMenu()
+1. **镜头重置**
+   - 调用GameStartMove组件重置镜头位置
+   - 确保从游戏视角回到主界面视角
+
+2. **界面切换**
+   - 切换到主界面面板(索引0)
+   - 播放主界面背景音乐
+
+#### MainUIController.onReturnToMainUI()
+1. **UI面板管理**
+   - 隐藏TopArea(Canvas-001)
+   - 显示MainUI,隐藏GameUI
+   - 显示TopBar和NavBar
+
+2. **数据重置和刷新**
+   - 刷新主界面所有UI显示
+   - 发送CURRENCY_CHANGED事件更新货币显示
+   - 播放主界面背景音乐
+
+## 解决方案
+
+### 1. 防止重复执行
+在 `GameEnd.ts` 中添加状态标志 `hasProcessedGameEnd`:
+
+```typescript
+private hasProcessedGameEnd: boolean = false;
+
+private async calculateAndShowRewards() {
+    if (this.hasProcessedGameEnd) {
+        console.log('[GameEnd] 游戏结束已处理过,跳过重复计算');
+        return;
+    }
+    
+    this.hasProcessedGameEnd = true;
+    // ... 奖励计算逻辑
+}
+```
+
+### 2. 修复奖励显示重置问题(核心修复)
+
+#### 问题根源
+`GameEnd.onResetUI()`方法在用户点击返回主菜单时被调用,会将`currentRewards`重置为`{money: -1, diamonds: -1}`,覆盖正确的奖励显示。
+
+#### 解决方案
+**A. 移除onResetUI中的currentRewards重置**
+```typescript
+private onResetUI() {
+    console.log('[GameEnd] 重置UI状态');
+    
+    // 停止所有动画
+    this.stopAllAnimations();
+    
+    // 直接重置到隐藏状态,不使用动画
+    this.initializeHiddenState();
+    
+    // 重置所有状态,为下一局游戏做准备
+    this.hasDoubledReward = false;
+    // 注意:不重置currentRewards,保持奖励显示直到下次游戏开始
+    // this.currentRewards = {money: -1, diamonds: -1}; // 移除这行
+    this.isGameSuccess = false;
+    this.hasProcessedGameEnd = false;
+}
+```
+
+**B. 添加游戏开始时的奖励重置**
+```typescript
+// 在setupEventListeners中添加GAME_START事件监听
+eventBus.on(GameEvents.GAME_START, this.onGameStart, this);
+
+// 新增onGameStart方法
+private onGameStart() {
+    console.log('[GameEnd] 收到游戏开始事件,重置奖励显示');
+    // 重置奖励显示,为新游戏做准备
+    this.currentRewards = {money: 0, diamonds: 0};
+    this.hasProcessedGameEnd = false;
+    this.hasDoubledReward = false;
+    this.isGameSuccess = false;
+}
+```
+
+**C. 完善事件监听器清理**
+```typescript
+// 在onDisable和onDestroy中添加GAME_START事件注销
+eventBus.off(GameEvents.GAME_START, this.onGameStart, this);
+```
+
+## 优化措施
+
+### 1. 防止重复执行机制
+
+#### GameEnd.ts优化
+```typescript
+// 在onGameSuccess和onGameDefeat中添加重复检查
+if (this.isGameSuccess && this.currentRewards.money > 0) {
+    console.log('[GameEnd] 游戏成功事件已处理过,跳过重复执行');
+    return;
+}
+
+// 在calculateAndShowRewards中添加重复计算检查
+if (this.currentRewards.money > 0 || this.currentRewards.diamonds > 0) {
+    console.log('[GameEnd] 奖励已计算过,跳过重复计算');
+    this.updateRewardDisplay();
+    // 确保面板显示
+    this.showEndPanelWithAnimation();
+    return;
+}
+```
+
+#### 统一游戏胜利和失败流程
+- ✅ 移除onGameSuccess和onGameDefeat中的重复showEndPanelWithAnimation调用
+- ✅ 统一在calculateAndShowRewards方法中处理面板动画显示
+- ✅ 确保游戏胜利和失败都经过相同的处理流程:清理敌人 → 设置状态 → 播放音效 → 设置标签 → 计算奖励 → 显示面板动画
+- ✅ 优化日志输出,明确标识GameEnd面板弹出动画的执行过程
+
+### 2. 事件触发优化
+
+#### Wall.ts优化
+- ✅ 移除onWallDestroyed中的直接GAME_DEFEAT事件发送
+- ✅ 统一由IN_game.ts的checkGameState处理游戏结束判断
+
+#### MenuController.ts优化
+- ✅ 菜单退出按钮改为发送CONTINUE_CLICK而非GAME_DEFEAT
+- ✅ 避免重复触发游戏结束事件
+
+### 3. 敌人重置逻辑优化
+
+**分析结论:** 敌人波次和数量的重置在关卡数据加载时会自动覆盖,无需在游戏结束时特别处理。
+
+**详细分析:**
+
+#### 游戏启动时的数据加载流程
+1. **StartGame.startGameFlow()** → **StartGame.initializeGameData()**
+2. **GameManager.loadCurrentLevelConfig()** → **LevelConfigManager.getLevelConfig()**
+3. **GameManager.applyLevelConfig()** → **InGameManager.applyLevelConfig()**
+4. **InGameManager.applyLevelConfig()** 设置 `this.levelWaves = levelConfig.waves`
+5. **InGameManager.setCurrentWave()** 通过事件系统通知 **EnemyController.startWave()**
+
+#### 敌人数据的完整覆盖机制
+- **LevelConfigManager** 从JSON文件加载关卡配置,包含完整的波次和敌人数据
+- **InGameManager** 在 `applyLevelConfig()` 中重新设置 `levelWaves`、`currentWave`、`levelTotalEnemies`
+- **EnemyController** 在 `startWave()` 中重置 `currentWave`、`totalWaves`、`currentWaveTotalEnemies`、`currentWaveEnemiesSpawned`、`currentWaveEnemyConfigs`
+
+#### 现有重置方法分析
+- **EnemyController.resetToInitialState()**: 重置所有敌人相关状态
+- **InGameManager.resetWaveInfo()**: 重置波次信息
+- **InGameManager.triggerGameDataCleanup()**: 发送 `RESET_ENEMY_CONTROLLER` 事件
+
+**结论:** 
+- ✅ 关卡数据加载时的自动覆盖机制已经足够
+- ✅ 现有的重置方法确保了状态的完全清理
+- ✅ 无需在游戏结束时额外处理敌人数据重置
+- ✅ 保持现有机制,避免重复和不必要的重置操作
+
+## 事件流程图
+
+```
+游戏结束触发
+     ↓
+[阶段一] UI弹出阶段
+     ↓
+GAME_SUCCESS/GAME_DEFEAT → GameEnd.ts
+     ↓
+- 奖励计算和显示
+- UI面板显示
+- 音效播放
+- 状态设置
+     ↓
+用户点击继续按钮
+     ↓
+[阶段二] 真正结束阶段
+     ↓
+CONTINUE_CLICK → GameManager.onMainMenuClick()
+     ↓
+- 游戏数据清理
+- 关卡进度管理
+- 应用状态切换
+     ↓
+RETURN_TO_MAIN_MENU → NavBarController + MainUIController
+     ↓
+- 镜头重置
+- UI面板切换
+- 数据刷新
+     ↓
+返回主界面完成
+```
+
+## 实施状态
+
+- ✅ 分析游戏结束事件的触发源头和重复执行原因
+- ✅ 优化墙体血量为0时的游戏结束触发逻辑
+- ✅ 优化菜单退出按钮的游戏结束处理
+- ✅ 重构GameEnd.ts的事件监听和处理逻辑
+- ✅ 明确游戏结束的两个阶段职责分工
+- ✅ 优化敌人波次和数量重置逻辑
+- ✅ 统一游戏胜利和失败的事件触发流程
+- ✅ 修复GameEnd面板动画重复调用问题
+- ✅ 测试优化后的游戏结束流程
+- ✅ 统一墙体血量为0和菜单退出的失败处理流程
+- ✅ 修复菜单退出GameEnd动画显示问题
+- ✅ 解决事件时序冲突导致的奖励显示重置问题
+
+## 测试指南
+
+### 测试场景
+
+#### 1. 墙体血量为0触发游戏结束
+**测试步骤:**
+1. 开始游戏,让敌人攻击墙体直到血量为0
+2. 观察控制台输出,确认只有一次游戏结束事件
+3. 检查GameEnd面板是否正常显示
+4. 点击双倍奖励,确认只计算一次
+5. 点击继续按钮,确认正常返回主界面
+
+**预期结果:**
+- 只有一次 `[GameEnd] 游戏失败事件处理` 日志
+- 只有一次 `[GameEnd] 奖励计算完成` 日志
+- 双倍奖励只能点击一次
+- 返回主界面流程正常
+
+#### 2. 菜单退出按钮触发游戏结束
+**测试步骤:**
+1. 开始游戏后点击菜单退出按钮
+2. 观察控制台输出,确认事件流程正确
+3. 检查是否直接返回主界面(不显示GameEnd面板)
+
+**预期结果:**
+- 不触发 `GAME_DEFEAT` 事件
+- 直接触发 `CONTINUE_CLICK` 事件
+- 正常返回主界面,无GameEnd面板显示
+
+#### 3. 游戏胜利触发游戏结束
+**测试步骤:**
+1. 完成所有波次敌人击杀
+2. 观察控制台输出,确认只有一次游戏胜利事件
+3. 检查GameEnd面板显示和奖励计算
+4. 确认GameEnd面板弹出动画正确播放
+5. 测试双倍奖励和继续流程
+
+**预期结果:**
+- 只有一次 `[GameEnd] 游戏成功事件处理` 日志
+- 只有一次 `[GameEnd] 奖励计算完成` 日志
+- 只有一次 `[GameEnd] 开始播放GameEnd面板弹出动画` 日志
+- GameEnd面板动画流畅播放,无重复或冲突
+- 奖励计算正确且只执行一次
+- 关卡进度正确更新
+
+### 关键监控点
+
+1. **控制台日志检查**
+   - `[GameEnd]` 相关日志不应重复出现
+   - `[GameEnd] 开始播放GameEnd面板弹出动画` 应只出现一次
+   - `[GameEnd] 奖励计算完成` 应只出现一次
+   - `[TopBarController] 刷新货币显示` 不应频繁重复
+   - `[SaveDataManager]` 奖励相关日志应只出现一次
+
+2. **UI状态检查**
+   - GameEnd面板动画流畅,无闪烁或重复播放
+   - 面板弹出动画正确执行(缩放从0.3到1.0,透明度从0到255)
+   - 双倍奖励按钮状态正确
+   - 货币显示更新正确
+   - EndLabel文本正确显示(SUCCESS/DEFEAT)
+
+3. **数据一致性检查**
+   - 玩家货币数据正确
+   - 关卡进度正确
+   - 游戏状态重置完整
+   - 敌人清理完整
+
+## 预期效果
+
+1. **消除重复执行**:每个处理逻辑只执行一次
+2. **清晰的职责分工**:每个阶段负责特定的任务
+3. **稳定的事件流程**:避免事件冲突和重复触发
+4. **更好的用户体验**:流畅的游戏结束和返回流程
+5. **优化的性能表现**:减少不必要的重复计算和UI更新
+
+## 测试验证
+
+### 修复前问题重现
+通过测试脚本验证了问题的根本原因:
+- `currentRewards`在`onResetUI()`中被重置为`{money: -1, diamonds: -1}`
+- 用户点击返回主菜单时触发`RESET_UI_STATES`事件,导致奖励显示被覆盖
+
+### 修复后验证
+
+#### 奖励显示修复验证
+使用`test_game_end_fix.js`测试脚本验证修复效果:
+
+```
+=== 测试修复后的游戏结束流程 ===
+
+--- 步骤1: 游戏开始 ---
+游戏开始后currentRewards: { money: 0, diamonds: 0 }
+
+--- 步骤2: 游戏成功 ---
+游戏成功后currentRewards: { money: 300, diamonds: 20 }
+
+--- 步骤3: 点击返回主菜单 ---
+重置UI后currentRewards: { money: 300, diamonds: 20 }  // 保持不变!
+
+--- 步骤4: 下一局游戏开始 ---
+新游戏开始后currentRewards: { money: 0, diamonds: 0 }  // 正确重置
+
+✅ 修复成功!奖励显示在重置UI后仍然保持正确
+✅ 新游戏开始时奖励正确重置为0
+```
+
+#### 统一失败处理流程验证
+通过`test_unified_defeat_flow.js`测试脚本验证统一流程:
+
+```
+1. 测试墙体血量为0的失败处理
+[Wall] 墙体被摧毁,触发游戏失败
+[EventBus] 触发事件: WALL_DESTROYED
+[Wall] 直接触发GAME_DEFEAT事件,与菜单退出失败处理流程一致
+[EventBus] 触发事件: GAME_DEFEAT
+[GameEnd] 接收到GAME_DEFEAT事件 (第1次)
+
+2. 测试菜单退出的失败处理
+[MenuController] 退出游戏按钮被点击
+[MenuController] 游戏中退出,触发GAME_DEFEAT事件显示游戏失败UI
+[EventBus] 触发事件: GAME_DEFEAT
+[GameEnd] 接收到GAME_DEFEAT事件 (第1次)
+
+3. 流程一致性验证
+✅ 流程一致性验证通过
+✅ 墙体血量为0和菜单退出都触发了相同数量的GAME_DEFEAT事件
+✅ 两种失败处理流程已统一
+```
+
+#### 菜单退出GameEnd动画显示修复验证
+通过`test_menu_defeat_animation.js`测试脚本验证:
+
+```
+=== 测试菜单退出GameEnd动画显示修复 ===
+
+[GameEnd] 已注册GAME_DEFEAT事件监听器
+1. 测试菜单退出流程(修复后):
+[MenuController] 退出游戏按钮被点击
+[MenuController] 当前应用状态: in_game
+[MenuController] 游戏中退出,触发GAME_DEFEAT事件显示游戏失败UI
+[EventBus] 触发事件: GAME_DEFEAT
+[GameEnd] 接收到GAME_DEFEAT事件
+[GameEnd] 游戏失败事件处理,开始统一处理流程
+[GameEnd] 计算和显示奖励(包含面板动画显示)
+
+2. 测试墙体血量为0流程:
+[Wall] 墙体被摧毁
+[Wall] 直接触发GAME_DEFEAT事件,与菜单退出失败处理流程一致
+[EventBus] 触发事件: GAME_DEFEAT
+[GameEnd] 接收到GAME_DEFEAT事件
+[GameEnd] 游戏失败事件处理,开始统一处理流程
+[GameEnd] 计算和显示奖励(包含面板动画显示)
+
+=== 事件冲突检查 ===
+GAME_DEFEAT事件数量: 2
+GAME_RESUME事件数量: 0
+✅ 修复成功:菜单退出时不再触发GAME_RESUME事件
+✅ 验证通过:墙体血量为0和菜单退出都正确触发GameEnd动画
+```
+
+统一流程验证结果:
+- ✅ 墙体血量为0时直接触发`GAME_DEFEAT`事件
+- ✅ 菜单退出时直接触发`GAME_DEFEAT`事件
+- ✅ 两种失败处理流程完全一致
+- ✅ 都由`GameEnd.ts`统一处理游戏失败逻辑
+- ✅ 修复了菜单退出时的事件冲突问题,确保GameEnd动画正常显示
+
+### 测试用例
+1. **正常游戏流程**:开始游戏 → 完成关卡 → 查看奖励显示
+2. **重复触发测试**:快速多次触发游戏结束事件
+3. **UI状态测试**:验证各种UI状态切换的正确性
+4. **奖励计算测试**:验证不同关卡奖励的正确计算
+5. **奖励显示持久性测试**:验证奖励显示在UI重置后保持正确
+6. **新游戏重置测试**:验证新游戏开始时奖励正确重置
+
+### 预期结果
+- 奖励只计算一次
+- UI状态正确切换
+- 没有重复的日志输出
+- 奖励显示准确
+- **奖励显示在返回主菜单时保持不变**
+- **新游戏开始时奖励正确重置为0**
+
+## 最新优化内容(2024年优化)
+
+### 统一游戏胜利和失败事件触发流程
+
+**问题描述:**
+- 游戏胜利时GameEnd面板弹出动画没有正确播放
+- onGameSuccess和onGameDefeat方法中存在重复的showEndPanelWithAnimation调用
+- 动画可能因为重复调用而产生冲突
+
+**解决方案:**
+1. **统一事件处理流程**:
+   - 移除onGameSuccess和onGameDefeat中的直接showEndPanelWithAnimation调用
+   - 统一在calculateAndShowRewards方法中处理面板动画显示
+   - 确保游戏胜利和失败都经过相同的处理步骤
+
+2. **优化动画调用逻辑**:
+   - 避免在同一个事件处理流程中多次调用showEndPanelWithAnimation
+   - 在奖励计算完成后统一显示面板动画
+   - 即使是重复计算的情况,也确保面板能正确显示
+
+3. **改进日志输出**:
+   - 明确标识GameEnd面板弹出动画的执行过程
+   - 区分不同阶段的日志输出,便于调试
+   - 统一敌人清理的日志描述
+
+### 修复GameEnd面板重复处理逻辑问题
+
+**问题描述:**
+- 游戏失败时,防重复处理的逻辑错误导致面板无法显示
+- 原逻辑`!this.isGameSuccess && this.currentRewards.money >= 0`在失败时会阻止面板显示
+- currentRewards初始值为undefined,导致判断逻辑异常
+
+**解决方案:**
+1. **添加专用处理状态标志**:
+   - 新增`hasProcessedGameEnd`标志来跟踪游戏结束事件的处理状态
+   - 替换原有的复杂判断逻辑,使用简单明确的布尔标志
+   - 在事件处理开始时立即设置标志,防止重复执行
+
+2. **修正奖励数据初始化**:
+   - 将`currentRewards`初始值设为`{money: -1, diamonds: -1}`
+   - 使用-1作为未初始化标志,0及以上表示已计算过奖励
+   - 修正重复计算检查逻辑,使用`>= 0`判断是否已计算
+
+3. **完善状态重置机制**:
+   - 在`onResetUI`方法中重置`hasProcessedGameEnd`标志
+   - 确保每局游戏开始时都能正常处理游戏结束事件
+   - 统一所有状态变量的重置逻辑
+
+### 修复奖励计算防重复机制问题
+
+**问题描述:**
+游戏成功时奖励计算未成功,显示奖励为0。分析发现:
+
+1. `calculateAndShowRewards`方法中使用`currentRewards.money >= 0`判断是否已计算奖励
+2. 当某些关卡的奖励配置确实为0时,会被误判为"已计算过"
+3. 导致跳过正常的奖励计算流程,显示错误的奖励数据
+4. `onGameSuccess`和`onGameDefeat`中存在重复的`hasProcessedGameEnd`检查和设置
+
+**解决方案:**
+
+1. **统一防重复逻辑**
+   - 将防重复检查统一移到`calculateAndShowRewards`方法中
+   - 使用`hasProcessedGameEnd`标志而不是奖励金额来判断是否已处理
+   - 移除`onGameSuccess`和`onGameDefeat`中的重复检查
+
+2. **修正判断逻辑**
+   - 防重复机制应该检查是否已调用过奖励计算方法
+   - 而不是检查奖励金额(因为0也是有效的奖励结果)
+   - 确保即使奖励为0的关卡也能正确处理
+
+**优化后的统一流程:**
+```
+游戏结束事件触发 (GAME_SUCCESS/GAME_DEFEAT)
+     ↓
+1. 清理敌人 (clearAllEnemies)
+     ↓
+2. 设置游戏状态 (SUCCESS/DEFEAT)
+     ↓
+3. 播放音效 (胜利/失败音效)
+     ↓
+4. 设置EndLabel文本 (SUCCESS/DEFEAT)
+     ↓
+5. 设置成功/失败标志 (isGameSuccess)
+     ↓
+6. 计算和显示奖励 (calculateAndShowRewards)
+     ↓
+7. 显示GameEnd面板动画 (showEndPanelWithAnimation)
+```
+
+## 总结
+
+### 问题解决状态
+✅ **核心问题已彻底解决**:游戏成功时奖励显示异常的问题已完全修复
+
+### 修复成果
+本次优化通过以下关键措施解决了游戏结束事件的重复执行问题:
+
+1. **事件触发源头优化**:统一游戏结束判断逻辑,避免多处重复触发
+2. **状态检查机制**:在关键方法中添加重复执行检查
+3. **职责分工明确**:区分UI弹出阶段和真正结束阶段的处理内容
+4. **数据加载机制优化**:利用关卡配置自动覆盖,避免不必要的手动重置
+5. **统一事件处理流程**:确保游戏胜利和失败都经过相同的处理步骤,避免动画冲突
+6. **动画调用优化**:避免重复调用面板动画,确保流畅的用户体验
+7. **奖励计算防重复机制修复**:使用专用标志而不是奖励金额来判断是否已处理,确保即使奖励为0的关卡也能正确计算和显示奖励
+8. **事件时序问题修复**:解决了`RESET_UI_STATES`事件导致的奖励显示重置问题
+9. **统一失败处理流程**:让墙体血量为0的失败处理与菜单退出的失败处理保持完全一致
+
+### 统一失败处理流程修改
+
+**问题描述:**
+墙体血量为0的失败处理与菜单退出的失败处理流程不一致:
+- 菜单退出:直接触发`GAME_DEFEAT`事件
+- 墙体血量为0:触发`WALL_DESTROYED`事件 → `IN_game.ts`检查游戏状态 → 触发`GAME_DEFEAT`事件
+
+**统一方案:**
+让墙体血量为0的失败处理参考菜单退出的处理,两者都直接触发`GAME_DEFEAT`事件。
+
+**具体修改:**
+
+1. **修改`Wall.ts`的`onWallDestroyed`方法**:
+   ```typescript
+   // 修改前:只触发WALL_DESTROYED事件,由IN_game.ts处理
+   eventBus.emit(GameEvents.WALL_DESTROYED, {...});
+   
+   // 修改后:保留WALL_DESTROYED事件,同时直接触发GAME_DEFEAT事件
+   eventBus.emit(GameEvents.WALL_DESTROYED, {...});
+   eventBus.emit(GameEvents.GAME_DEFEAT); // 与菜单退出保持一致
+   ```
+
+2. **修改`IN_game.ts`的相关方法**:
+   - `onWallDestroyedEvent`:不再调用`checkGameState`,因为墙体已直接触发失败事件
+   - `checkGameState`:移除墙体摧毁检查逻辑,只保留敌人击败检查
+
+**统一后的失败处理流程:**
+```
+失败触发源 → 直接触发GAME_DEFEAT事件 → GameEnd.ts统一处理
+```
+
+无论是墙体血量为0还是菜单退出,都遵循相同的事件流程,确保处理逻辑的一致性。
+
+### 菜单退出GameEnd动画显示修复
+
+**问题发现:**
+在统一失败处理流程后,发现菜单退出时虽然触发了 `GAME_DEFEAT` 事件,但GameEnd动画没有正常显示。通过分析用户提供的日志发现:
+
+```
+[MenuController] 游戏中退出,触发GAME_DEFEAT事件显示游戏失败UI
+[MenuController] 游戏中关闭菜单,通过事件系统触发游戏恢复
+[EnemyController] 接收到游戏恢复事件,恢复所有敌人
+[GameManager] 接收到游戏失败事件,执行失败处理
+```
+
+**问题根源:**
+`MenuController.ts` 在触发 `GAME_DEFEAT` 事件前先调用了 `closeMenu()`,而 `closeMenu()` 会触发 `GAME_RESUME` 事件,导致事件冲突。
+
+**修复方案:**
+
+#### 1. 调整事件触发顺序
+修改 `MenuController.ts` 的 `onBackButtonClick` 方法,先触发 `GAME_DEFEAT` 事件,再关闭菜单:
+
+```typescript
+if (currentAppState === AppState.IN_GAME) {
+    console.log('[MenuController] 游戏中退出,触发GAME_DEFEAT事件显示游戏失败UI');
+    
+    // 先触发游戏失败事件,让GameEnd面板显示
+    const eventBus = EventBus.getInstance();
+    eventBus.emit(GameEvents.GAME_DEFEAT);
+    
+    // 关闭菜单(不触发GAME_RESUME事件,避免事件冲突)
+    await this.closeMenuWithoutResume();
+    
+    console.log('[MenuController] 菜单退出处理完成,已触发GAME_DEFEAT事件');
+}
+```
+
+#### 2. 新增专用关闭方法
+添加 `closeMenuWithoutResume` 方法,避免触发 `GAME_RESUME` 事件:
+
+```typescript
+private async closeMenuWithoutResume(): Promise<void> {
+    if (!this.isMenuOpen) return;
+    
+    this.isMenuOpen = false;
+    
+    // 使用动画隐藏菜单面板
+    if (this.popupAni) {
+        await this.popupAni.hidePanel();
+    }
+    
+    // 不触发GAME_RESUME事件,避免与GAME_DEFEAT事件冲突
+    console.log('菜单已关闭(未触发游戏恢复)');
+}
+```
+
+**修复效果:**
+
+- **修复前的问题**:菜单退出时先触发 `GAME_RESUME` 事件,后触发 `GAME_DEFEAT` 事件,事件冲突导致GameEnd动画显示异常
+- **修复后的流程**:菜单退出时直接触发 `GAME_DEFEAT` 事件,不触发 `GAME_RESUME` 事件,避免冲突,GameEnd动画正常显示,与墙体血量为0的处理完全一致
+
+### 关键修复点
+- **事件时序问题**:解决了`RESET_UI_STATES`事件导致的奖励显示重置问题
+- **生命周期管理**:正确管理`currentRewards`的重置时机
+- **事件监听完善**:添加`GAME_START`事件监听,确保新游戏开始时状态正确重置
+- **统一失败处理流程**:墙体血量为0和菜单退出都直接触发`GAME_DEFEAT`事件
+
+### 验证结果
+所有测试用例通过,确认问题已彻底解决:
+- ✅ 奖励计算正确
+- ✅ 奖励显示持久
+- ✅ UI重置安全
+- ✅ 新游戏重置正确
+- ✅ 统一失败处理流程
+- ✅ 事件触发一致性
+
+优化后的流程更加稳定、高效,游戏胜利和失败的处理完全统一,用户体验得到显著提升。