181404010226 5 сар өмнө
parent
commit
7b2ed031e8

+ 8 - 2
assets/Scenes/GameLevel.scene

@@ -21179,7 +21179,7 @@
     "_lpos": {
       "__type__": "cc.Vec3",
       "x": 311.674,
-      "y": -1214.659,
+      "y": -1224.422,
       "z": 0
     },
     "_lrot": {
@@ -23912,7 +23912,7 @@
     "_target": null,
     "_left": 1.6739999999999782,
     "_right": 0,
-    "_top": 1030.159,
+    "_top": 1039.922,
     "_bottom": -1229.178,
     "_horizontalCenter": 0,
     "_verticalCenter": 0,
@@ -25656,6 +25656,12 @@
     "confirmButton": {
       "__id__": 617
     },
+    "blockSelectionUINode": {
+      "__id__": 6
+    },
+    "dibanNode": {
+      "__id__": 559
+    },
     "_id": "ealiXZigZIgrXLo2NL22Ns"
   },
   {

+ 1 - 1
assets/assets/Prefabs/Ball.prefab

@@ -56,7 +56,7 @@
       "z": 1
     },
     "_mobility": 0,
-    "_layer": 33554432,
+    "_layer": 1073741824,
     "_euler": {
       "__type__": "cc.Vec3",
       "x": 0,

+ 9 - 0
assets/excel.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "677c4d56-8358-4ddd-9ced-4e24296771bc",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 9 - 0
assets/resources/data/excel.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "96bfde5e-91cf-42de-8cc1-75643cc9c7d0",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 5 - 0
assets/resources/data/excel/blockSizes.csv

@@ -0,0 +1,5 @@
+blockSize
+1x1
+1x2
+2x1
+2x2 

+ 11 - 0
assets/resources/data/excel/blockSizes.csv.meta

@@ -0,0 +1,11 @@
+{
+  "ver": "1.0.1",
+  "importer": "text",
+  "imported": true,
+  "uuid": "fe944950-c253-45c6-ab13-b3d03a45fa4b",
+  "files": [
+    ".json"
+  ],
+  "subMetas": {},
+  "userData": {}
+}

+ 5 - 0
assets/resources/data/excel/bulletEffectTypes.csv

@@ -0,0 +1,5 @@
+type,values
+count,"single,spread,burst"
+trajectory,"straight,arc,homing_arc"
+hitEffects,"normal_damage,pierce_damage,explosion,ground_burn,ricochet_damage"
+lifecycle,"hit_destroy,range_limit,ricochet_counter,ground_impact,return_trip,ground_impact_with_effect,target_impact" 

+ 11 - 0
assets/resources/data/excel/bulletEffectTypes.csv.meta

@@ -0,0 +1,11 @@
+{
+  "ver": "1.0.1",
+  "importer": "text",
+  "imported": true,
+  "uuid": "280f1575-2a09-4a86-ace7-e0de29b64d6c",
+  "files": [
+    ".json"
+  ],
+  "subMetas": {},
+  "userData": {}
+}

+ 5 - 0
assets/resources/data/excel/rarityWeights.csv

@@ -0,0 +1,5 @@
+rarity,weight
+common,60
+uncommon,25
+rare,12
+epic,3 

+ 11 - 0
assets/resources/data/excel/rarityWeights.csv.meta

@@ -0,0 +1,11 @@
+{
+  "ver": "1.0.1",
+  "importer": "text",
+  "imported": true,
+  "uuid": "7bf92ddc-ea8b-4927-b228-6728e8fb5e0c",
+  "files": [
+    ".json"
+  ],
+  "subMetas": {},
+  "userData": {}
+}

+ 9 - 0
assets/resources/data/excel/weapons.csv

@@ -0,0 +1,9 @@
+id,name,type,rarity,weight,damage,fireRate,range,bulletSpeed,accuracy,bulletType,bulletAmount,spreadAngle,burstCount,burstDelay,trajectoryType,bulletSpeed,gravity,arcHeight,homingStrength,homingDelay,hitEffectType,hitEffectDamage,hitEffectRadius,hitEffectDuration,lifecycleType,maxLifetime,penetration,ricochetCount,returnToOrigin,bulletPrefab,hitEffect,trailEffect,muzzleFlash,weaponSprite1x1,weaponSprite1x2,weaponSprite2x1,weaponSprite2x2,fireSound
+pea_shooter,毛豆射手,single_shot,common,30,20,1.0,300,30,0.95,single,1,0,1,0,straight,200,0,0,0,0,normal_damage,20,0,0,hit_destroy,5.0,1,0,false,bullets/PeaBullet,Animation/WeaponTx/tx0002/tx0002,,Animation/WeaponTx/tx0002/tx0002,images/PlantsSprite/001-1,images/PlantsSprite/001-1,images/PlantsSprite/001-1,images/PlantsSprite/001-1,audio/pea_shot
+sharp_carrot,尖胡萝卜,piercing,common,25,15,0.8,400,30,0.9,single,1,0,1,0,straight,250,0,0,0,0,pierce_damage,15,0,0,range_limit,5.0,999,0,false,bullets/CarrotBullet,,Animation/WeaponTx/tx0001/tx0001,Animation/WeaponTx/tx0001/tx0001,images/PlantsSprite/002,images/PlantsSprite/002,images/PlantsSprite/002,images/PlantsSprite/002,audio/carrot_shot
+saw_grass,锯齿草,ricochet_piercing,uncommon,20,25,0.6,350,30,0.85,single,1,0,1,0,straight,180,0,0,0,0,ricochet_damage/pierce_damage,25,0,0,ricochet_counter,8.0,3,3,false,bullets/SawBullet,Animation/WeaponTx/tx0002/tx0002,,Animation/WeaponTx/tx0002/tx0002,images/PlantsSprite/003,images/PlantsSprite/003,images/PlantsSprite/003,images/PlantsSprite/003,audio/saw_shot
+watermelon_bomb,西瓜炸弹,explosive,rare,15,50,0.4,250,30,0.8,single,1,0,1,0,arc,20,0,0,0,0,explosion,80,100,0.1,ground_impact,5.0,1,0,false,bullets/WatermelonBomb,Animation/WeaponTx/tx0007/tx0007,,Animation/WeaponTx/tx0007/tx0007,images/PlantsSprite/007,images/PlantsSprite/007,images/PlantsSprite/007,images/PlantsSprite/007,audio/bomb_launch
+boomerang_plant,回旋镖盆栽,boomerang,uncommon,18,30,0.5,300,30,0.9,single,1,0,1,0,homing,15,0,0,0.5,0.3,pierce_damage,30,0,0,return_trip,10.0,999,0,true,bullets/BoomerangBullet,Animation/WeaponTx/tx0003/tx0003,,Animation/WeaponTx/tx0003/tx0003,images/PlantsSprite/004,images/PlantsSprite/004,images/PlantsSprite/004,images/PlantsSprite/004,audio/boomerang_throw
+hot_pepper,炙热辣椒,area_burn,rare,12,40,0.3,280,30,0.75,single,1,0,1,0,straight,100,0,0,0,0,explosion/ground_burn,40,120,5.0,ground_impact_with_effect,5.0,1,0,false,bullets/PepperBomb,Animation/WeaponTx/tx0005/tx0005,,Animation/WeaponTx/tx0005/tx0005,images/PlantsSprite/005,images/PlantsSprite/005,images/PlantsSprite/005,images/PlantsSprite/005,audio/pepper_launch
+cactus_shotgun,仙人散弹,shotgun,uncommon,22,12,0.7,200,30,0.7,spread,5,30,1,0,straight,180,0,0,0,0,normal_damage,12,0,0,range_limit,3.0,1,0,false,bullets/CactusBullet,Animation/WeaponTx/tx0005/tx0005,,Animation/WeaponTx/tx0005/tx0005,images/PlantsSprite/008,images/PlantsSprite/008,images/PlantsSprite/008,images/PlantsSprite/008,audio/cactus_shot
+okra_missile,秋葵导弹,homing_missile,epic,8,70,0.25,500,30,1.0,single,1,0,1,0,homing,20,0.2,100,0.8,0.3,explosion,70,150,0,target_impact,8.0,1,0,false,bullets/OkraMissile,Animation/WeaponTx/tx0006/tx0006,,Animation/WeaponTx/tx0006/tx0006,images/PlantsSprite/006,images/PlantsSprite/006,images/PlantsSprite/006,images/PlantsSprite/006,audio/missile_launch 

+ 11 - 0
assets/resources/data/excel/weapons.csv.meta

@@ -0,0 +1,11 @@
+{
+  "ver": "1.0.1",
+  "importer": "text",
+  "imported": true,
+  "uuid": "0cf29420-e225-488c-903b-64bc09a3e30b",
+  "files": [
+    ".json"
+  ],
+  "subMetas": {},
+  "userData": {}
+}

+ 104 - 3
assets/scripts/Animations/GameStartMove.ts

@@ -27,6 +27,7 @@ export class GameStartMove extends Component {
     public moveDuration: number = 0.4;
 
     private _originalPos: Vec3 = new Vec3();
+    private _originalDibanPos: Vec3 = new Vec3();
 
     onLoad () {
         // Use self node if cameraNode not assigned via the editor.
@@ -35,6 +36,11 @@ export class GameStartMove extends Component {
         }
         // Save initial position so we can always restore relative to it.
         this._originalPos.set(this.cameraNode.position);
+        
+        // Save diban original position if available
+        if (this.dibanNode) {
+            this._originalDibanPos.set(this.dibanNode.position);
+        }
     }
 
     /**
@@ -76,19 +82,114 @@ export class GameStartMove extends Component {
      * @param duration 动画时长,默认 0.3s
      */
     public slideDibanDownAndHide(distance: number = 300, duration: number = 0.3) {
-        if (!this.dibanNode || !this.blockSelectionUI) return;
+        console.log('GameStartMove.slideDibanDownAndHide 开始执行');
+        
+        if (!this.dibanNode || !this.blockSelectionUI) {
+            console.log('GameStartMove.slideDibanDownAndHide 条件检查失败:', {
+                dibanNode: !!this.dibanNode,
+                blockSelectionUI: !!this.blockSelectionUI
+            });
+            return;
+        }
+
+        // 使用存储的原始位置,如果没有存储则使用当前位置
+        let originalPos: Vec3;
+        if (this._originalDibanPos.equals(Vec3.ZERO)) {
+            // 如果没有存储原始位置,使用当前位置并存储它
+            originalPos = this.dibanNode.position.clone();
+            this._originalDibanPos = originalPos.clone();
+            console.log('GameStartMove.slideDibanDownAndHide 首次使用,存储原始位置:', this._originalDibanPos);
+        } else {
+            // 使用存储的原始位置
+            originalPos = this._originalDibanPos.clone();
+            console.log('GameStartMove.slideDibanDownAndHide 使用存储的原始位置:', originalPos);
+        }
 
-        const originalPos = this.dibanNode.position.clone();
+        // 获取当前位置作为动画起始位置
+        const currentPos = this.dibanNode.position.clone();
+        console.log('GameStartMove.slideDibanDownAndHide 当前位置:', currentPos);
 
         // 停止现有 tween
         Tween.stopAllByTarget(this.dibanNode);
 
+        const targetPos = new Vec3(currentPos.x, currentPos.y - distance, currentPos.z);
+        console.log('GameStartMove.slideDibanDownAndHide 目标位置:', targetPos);
+
         tween(this.dibanNode)
-            .to(duration, { position: new Vec3(originalPos.x, originalPos.y - distance, originalPos.z) }, { easing: 'quadIn' })
+            .to(duration, { position: targetPos }, { easing: 'quadIn' })
             .call(() => {
+                console.log('GameStartMove.slideDibanDownAndHide 动画完成回调');
+                // 先隐藏BlockSelectionUI,然后立即重置diban位置到存储的原始位置
+                // 这样用户就看不到位置重置的过程
                 this.blockSelectionUI.active = false;
                 this.dibanNode.setPosition(originalPos);
+                console.log('GameStartMove.slideDibanDownAndHide UI已隐藏,位置已重置到:', originalPos);
             })
             .start();
+            
+        console.log('GameStartMove.slideDibanDownAndHide 动画已启动');
+    }
+
+    /**
+     * 从底部上滑显示 diban
+     * @param distance 从下方移动的距离,默认 300
+     * @param duration 动画时长,默认 0.3s
+     */
+    public slideUpFromBottom(distance: number = 300, duration: number = 0.3) {
+        console.log('GameStartMove.slideUpFromBottom 开始执行');
+        
+        if (!this.dibanNode || !this.blockSelectionUI) {
+            console.log('GameStartMove.slideUpFromBottom 条件检查失败:', {
+                dibanNode: !!this.dibanNode,
+                blockSelectionUI: !!this.blockSelectionUI
+            });
+            return;
+        }
+
+        // 确保BlockSelectionUI可见
+        this.blockSelectionUI.active = true;
+
+        // 使用存储的原始位置作为目标位置
+        let targetPos: Vec3;
+        if (this._originalDibanPos.equals(Vec3.ZERO)) {
+            // 如果没有存储原始位置,使用当前位置并存储它
+            targetPos = this.dibanNode.position.clone();
+            this._originalDibanPos = targetPos.clone();
+            console.log('GameStartMove.slideUpFromBottom 首次使用,存储原始位置:', this._originalDibanPos);
+        } else {
+            // 使用存储的原始位置
+            targetPos = this._originalDibanPos.clone();
+            console.log('GameStartMove.slideUpFromBottom 使用存储的原始位置:', targetPos);
+        }
+        
+        // 设置初始位置(在目标位置下方)
+        const startPos = new Vec3(targetPos.x, targetPos.y - distance, targetPos.z);
+        this.dibanNode.setPosition(startPos);
+        console.log('GameStartMove.slideUpFromBottom 起始位置:', startPos);
+
+        // 停止现有 tween
+        Tween.stopAllByTarget(this.dibanNode);
+
+        // 播放上滑动画到目标位置
+        tween(this.dibanNode)
+            .to(duration, { position: targetPos }, { easing: 'quadOut' })
+            .call(() => {
+                console.log('GameStartMove.slideUpFromBottom 动画完成');
+            })
+            .start();
+            
+        console.log('GameStartMove.slideUpFromBottom 动画已启动');
+    }
+
+    /**
+     * 更新保存的diban原始位置(当diban节点在运行时改变时调用)
+     */
+    public updateDibanOriginalPosition() {
+        if (this.dibanNode) {
+            this._originalDibanPos.set(this.dibanNode.position);
+            console.log('GameStartMove.updateDibanOriginalPosition 存储原始位置:', this._originalDibanPos);
+        } else {
+            console.log('GameStartMove.updateDibanOriginalPosition dibanNode为空,无法存储位置');
+        }
     }
 }

+ 4 - 138
assets/scripts/CombatSystem/BallController.ts

@@ -87,11 +87,6 @@ export class BallController extends Component {
         
         // 只进行初始设置,不创建小球
         this.calculateGameBounds();
-        
-        // 延迟执行一些检查方法,但不创建小球
-        this.scheduleOnce(() => {
-            this.logCollisionMatrix();
-        }, 1.0);
     }
 
     // 计算游戏边界(使用GameArea节点)
@@ -128,10 +123,7 @@ export class BallController extends Component {
             return;
         }
 
-        // 如果已经有活动的球,先销毁它
-        if (this.activeBall && this.activeBall.isValid) {
-            this.activeBall.destroy();
-        }
+    
 
         // 实例化小球
         this.activeBall = instantiate(this.ballPrefab);
@@ -164,81 +156,8 @@ export class BallController extends Component {
     }
 
 
-    // 为指定节点随机定位
-    private positionBallRandomlyForNode(ballNode: Node) {
-        const transform = ballNode.getComponent(UITransform);
-        const ballRadius = transform ? transform.width / 2 : 25;
-        
-        // 计算可生成的范围
-        const minX = this.gameBounds.left + ballRadius + 20;
-        const maxX = this.gameBounds.right - ballRadius - 20;
-        const minY = this.gameBounds.bottom + ballRadius + 20;
-        const maxY = this.gameBounds.top - ballRadius - 20;
-
-        // 随机生成位置
-        const randomX = Math.random() * (maxX - minX) + minX;
-        const randomY = Math.random() * (maxY - minY) + minY;
-
-        // 将世界坐标转换为相对于GameArea的本地坐标
-        const gameArea = find('Canvas/GameLevelUI/GameArea');
-        if (gameArea) {
-            const localPos = gameArea.getComponent(UITransform).convertToNodeSpaceAR(new Vec3(randomX, randomY, 0));
-            ballNode.position = localPos;
-        }
-    }
-
-    // 为指定小球设置碰撞组件
-    private setupColliderForBall(ballNode: Node, ballRadius: number) {
-        // 确保小球有刚体组件
-        let rigidBody = ballNode.getComponent(RigidBody2D);
-        if (!rigidBody) {
-            rigidBody = ballNode.addComponent(RigidBody2D);
-            rigidBody.type = 2; // Dynamic
-            rigidBody.gravityScale = 0;
-            rigidBody.enabledContactListener = true;
-            rigidBody.fixedRotation = true;
-            rigidBody.allowSleep = false;
-            rigidBody.linearDamping = 0;
-            rigidBody.angularDamping = 0;
-        }
-
-        // 确保小球有碰撞组件
-        let collider = ballNode.getComponent(CircleCollider2D);
-        if (!collider) {
-            collider = ballNode.addComponent(CircleCollider2D);
-            collider.radius = ballRadius;
-            collider.tag = 1;
-            collider.group = 1;
-            collider.sensor = false;
-            collider.friction = 0;
-            collider.restitution = 1;
-        }
-
-        // 设置碰撞监听
-        const physics = PhysicsManager.getInstance()?.getSystem();
-        if (physics) {
-            physics.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
-            physics.on(Contact2DType.END_CONTACT, this.onEndContact, this);
-            physics.on(Contact2DType.PRE_SOLVE, this.onPreSolve, this);
-            physics.on(Contact2DType.POST_SOLVE, this.onPostSolve, this);
-        }
-    }
-
-    // 启动指定小球的运动
-    private startBallMovement(ballNode: Node) {
-        const rigidBody = ballNode.getComponent(RigidBody2D);
-        if (rigidBody) {
-            // 随机初始方向
-            const angle = Math.random() * Math.PI * 2;
-            const direction = new Vec2(Math.cos(angle), Math.sin(angle));
-            
-            rigidBody.linearVelocity = new Vec2(
-                direction.x * this.speed,
-                direction.y * this.speed
-            );
-        }
-    }
-
+ 
+  
     // 检查所有已放置方块的碰撞体组件
     private checkBlockColliders() {
         
@@ -749,14 +668,6 @@ export class BallController extends Component {
         return null;
     }
 
-    // 辅助方法:打印节点结构
-    private logNodeStructure(node: Node, depth: number) {
-        const indent = '  '.repeat(depth);
-        for (let i = 0; i < node.children.length; i++) {
-            this.logNodeStructure(node.children[i], depth + 1);
-        }
-    }
-
     // 获取节点的完整路径
     private getNodePath(node: Node): string {
         let path = node.name;
@@ -853,30 +764,7 @@ export class BallController extends Component {
             }
         }
     }
-    
-    // 检查碰撞矩阵
-    private checkCollisionMatrix(group1: number, group2: number): boolean {
-        // 根据项目配置检查碰撞矩阵
-        // "collisionMatrix": { "0": 3, "1": 39, "2": 6, "3": 16, "4": 40, "5": 18 }
-        const collisionMatrix: { [key: string]: number } = {
-            "0": 3,
-            "1": 39,
-            "2": 6,
-            "3": 16,
-            "4": 40,
-            "5": 18
-        };
-        
-        const mask1 = collisionMatrix[group1.toString()];
-        const mask2 = collisionMatrix[group2.toString()];
-        
-        // 检查group1是否能与group2碰撞
-        const canCollide1to2 = mask1 && (mask1 & (1 << group2)) !== 0;
-        // 检查group2是否能与group1碰撞
-        const canCollide2to1 = mask2 && (mask2 & (1 << group1)) !== 0;
-        
-        return canCollide1to2 && canCollide2to1;
-    }
+
 
     // 初始化方向
     initializeDirection() {
@@ -975,28 +863,6 @@ export class BallController extends Component {
             }
         }
     }
-
-    // 输出碰撞矩阵调试信息
-    private logCollisionMatrix() {
-        // 根据项目配置检查碰撞矩阵
-        // "collisionMatrix": { "0": 3, "1": 39, "2": 6, "3": 16, "4": 40, "5": 18 }
-        const collisionMatrix: { [key: string]: number } = {
-            "0": 3,
-            "1": 39,
-            "2": 6,
-            "3": 16,
-            "4": 40,
-            "5": 18
-        };
-        
-        // 测试Ball组(1)和Block组(2)是否能碰撞
-        const ballMask = collisionMatrix["1"]; // 39
-        const blockMask = collisionMatrix["2"]; // 6
-        
-        const ballCanCollideWithBlock = (ballMask & (1 << 2)) !== 0; // 检查Ball能否与组2碰撞
-        const blockCanCollideWithBall = (blockMask & (1 << 1)) !== 0; // 检查Block能否与组1碰撞
-    }
-
     /**
      * 从给定世界坐标发射子弹
      */

+ 11 - 2
assets/scripts/CombatSystem/BlockManager.ts

@@ -1037,8 +1037,13 @@ export class BlockManager extends Component {
     
     // 设置方块稀有度颜色
     private setBlockRarityColor(block: Node, rarity: string) {
+        // Add null safety check for block
+        if (!block || !block.isValid) {
+            return;
+        }
+        
         const sprite = block.getComponent(Sprite);
-        if (!sprite) {
+        if (!sprite || !sprite.isValid) {
             return;
         }
         
@@ -1112,7 +1117,11 @@ export class BlockManager extends Component {
                 return;
             }
             
-            if (weaponSprite && spriteFrame) {
+            // Add comprehensive null safety checks before setting spriteFrame
+            if (weaponSprite && weaponSprite.isValid && 
+                weaponNode && weaponNode.isValid && 
+                block && block.isValid && 
+                spriteFrame && spriteFrame.isValid) {
                 weaponSprite.spriteFrame = spriteFrame;
             }
         });

+ 150 - 24
assets/scripts/CombatSystem/BlockSelection/GameBlockSelection.ts

@@ -1,8 +1,10 @@
-import { _decorator, Component, Node, Button, Label, find, UITransform, Sprite, Color } from 'cc';
+import { _decorator, Component, Node, Button, Label, find, UITransform, Sprite, Color, tween, Tween } from 'cc';
 import { LevelSessionManager } from '../../Core/LevelSessionManager';
 import { BallController } from '../BallController';
 import { BlockManager } from '../BlockManager';
 import { GameStartMove } from '../../Animations/GameStartMove';
+import { GameManager } from '../../LevelSystem/GameManager';
+import { GamePause } from '../GamePause';
 const { ccclass, property } = _decorator;
 
 @ccclass('GameBlockSelection')
@@ -68,6 +70,18 @@ export class GameBlockSelection extends Component {
     })
     public confirmButton: Node = null;
 
+    @property({
+        type: Node,
+        tooltip: '拖拽BlockSelectionUI根节点到这里'
+    })
+    public blockSelectionUINode: Node = null;
+
+    @property({
+        type: Node,
+        tooltip: '拖拽BlockSelectionUI/diban节点到这里'
+    })
+    public dibanNode: Node = null;
+
     // 按钮费用配置
     private readonly ADD_BALL_COST = 80;
     private readonly ADD_COIN_AMOUNT = 80;
@@ -77,11 +91,14 @@ export class GameBlockSelection extends Component {
     private ballController: BallController = null;
     private blockManager: BlockManager = null;
     private gameStartMove: GameStartMove = null;
+    private gameManager: GameManager = null;
 
     // 回调函数,用于通知GameManager
     public onConfirmCallback: () => void = null;
 
     start() {
+        console.log('GameBlockSelection.start() 开始初始化');
+        
         // 获取管理器实例
         this.session = LevelSessionManager.inst;
         
@@ -102,6 +119,29 @@ export class GameBlockSelection extends Component {
         // 获取GameStartMove组件
         if (this.cameraNode) {
             this.gameStartMove = this.cameraNode.getComponent(GameStartMove);
+            console.log('GameStartMove组件获取结果:', !!this.gameStartMove);
+            
+            // 如果GameStartMove存在,设置BlockSelectionUI和diban引用,并更新原始位置
+            if (this.gameStartMove && this.blockSelectionUINode && this.dibanNode) {
+                this.gameStartMove.blockSelectionUI = this.blockSelectionUINode;
+                this.gameStartMove.dibanNode = this.dibanNode;
+                this.gameStartMove.updateDibanOriginalPosition();
+                
+                console.log('GameStartMove引用设置完成:', {
+                    blockSelectionUISet: !!this.gameStartMove.blockSelectionUI,
+                    dibanNodeSet: !!this.gameStartMove.dibanNode,
+                    blockSelectionUINodeName: this.blockSelectionUINode.name,
+                    dibanNodeName: this.dibanNode.name
+                });
+            } else {
+                console.warn('GameStartMove引用设置失败:', {
+                    gameStartMove: !!this.gameStartMove,
+                    blockSelectionUINode: !!this.blockSelectionUINode,
+                    dibanNode: !!this.dibanNode
+                });
+            }
+        } else {
+            console.warn('Camera节点未绑定,请在Inspector中拖拽Canvas/Camera节点');
         }
 
         // 如果没有指定coinLabelNode,尝试找到它
@@ -109,8 +149,16 @@ export class GameBlockSelection extends Component {
             this.coinLabelNode = find('Canvas-001/TopArea/CoinNode/CoinLabel');
         }
 
+        // 获取GameManager实例
+        const gameManagerNode = find('Canvas/GameManager');
+        if (gameManagerNode) {
+            this.gameManager = gameManagerNode.getComponent(GameManager);
+        }
+
         // 绑定按钮事件
         this.bindButtonEvents();
+        
+        console.log('GameBlockSelection.start() 初始化完成');
     }
 
     // 绑定按钮事件
@@ -207,23 +255,8 @@ export class GameBlockSelection extends Component {
 
     // 确认按钮点击(从GameManager迁移的onConfirmButtonClicked)
     public onConfirmButtonClicked() {
-        // 执行相机动画和UI关闭
-        if (this.gameStartMove) {
-            this.gameStartMove.slideDibanDownAndHide();
-        } else {
-            // Fallback: 若未挂载 GameStartMove,直接关闭
-            this.node.active = false;
-        }
-
-        // Camera move back up with smooth animation
-        if (this.gameStartMove) {
-            this.gameStartMove.moveUpSmooth();
-        }
-
-        // 显示GridContainer
-        if (this.gridContainer) {
-            this.gridContainer.active = true;
-        }
+        // 使用统一的隐藏方法,包含动画
+        this.hideBlockSelection();
         
         // 保存已放置的方块
         this.preservePlacedBlocks();
@@ -232,6 +265,12 @@ export class GameBlockSelection extends Component {
         if (this.onConfirmCallback) {
             this.onConfirmCallback();
         }
+        
+        // 恢复游戏
+        const gamePause = GamePause.getInstance();
+        if (gamePause) {
+            gamePause.resumeGame();
+        }
     }
 
     // 保存已放置的方块(从GameManager迁移)
@@ -280,24 +319,111 @@ export class GameBlockSelection extends Component {
 
     // 显示方块选择UI(用于游戏开始或下一波)
     public showBlockSelection(isNextWave: boolean = false) {
+        // 检查游戏是否已结束(胜利或失败),如果是则不显示方块选择UI
+        if (this.gameManager && this.gameManager.isGameOver()) {
+            console.warn('[GameBlockSelection] 游戏已经结束(胜利或失败),不显示方块选择UI');
+            return;
+        }
+        
+        // 首先显示UI节点
         this.node.active = true;
         
         if (this.gridContainer) {
             this.gridContainer.active = true;
         }
 
-        console.log(isNextWave ? '显示下一波方块选择UI' : '显示游戏开始方块选择UI');
+        // 如果有BlockSelectionUI节点,确保它可见
+        if (this.blockSelectionUINode) {
+            this.blockSelectionUINode.active = true;
+        }
+
+        // 每次显示方块选择UI时,生成新的随机方块
+        if (this.blockManager) {
+            this.blockManager.refreshBlocks();
+            console.log('[GameBlockSelection] 生成新的随机方块');
+        } else {
+            console.warn('[GameBlockSelection] BlockManager未找到,无法生成随机方块');
+        }
+
+        // 播放BlockSelectionUI出现动画
+        this.playShowAnimation();
+
+        console.log(`[GameBlockSelection] ${isNextWave ? '显示下一波方块选择UI' : '显示游戏开始方块选择UI'}`);
     }
 
     // 隐藏方块选择UI
     public hideBlockSelection() {
-        this.node.active = false;
-        
-        if (this.gridContainer) {
-            this.gridContainer.active = false;
+        // 播放隐藏动画,动画完成后隐藏UI
+        this.playHideAnimation(() => {
+            this.node.active = false;
+            
+            // 移除隐藏GridContainer的代码,因为GridContainer应该在游戏过程中保持可见
+            // if (this.gridContainer) {
+            //     this.gridContainer.active = false;
+            // }
+
+            console.log('隐藏方块选择UI');
+        });
+    }
+
+    // 播放显示动画
+    private playShowAnimation() {
+        console.log('播放显示动画playShowAnimation');
+        if (this.gameStartMove && this.blockSelectionUINode && this.dibanNode) {
+            // 设置GameStartMove的blockSelectionUI和dibanNode引用
+            this.gameStartMove.blockSelectionUI = this.blockSelectionUINode;
+            this.gameStartMove.dibanNode = this.dibanNode;
+            
+            // 每次显示BlockSelectionUI时,摄像头下移182px
+            this.gameStartMove.moveDownInstant();
+            console.log('摄像头下移182px');
+            
+            // 使用GameStartMove的上滑入场动画
+            this.gameStartMove.slideUpFromBottom(300, 0.3);
         }
+    }
 
-        console.log('隐藏方块选择UI');
+    // 播放隐藏动画
+    private playHideAnimation(onComplete?: () => void) {
+        console.log('播放隐藏动画playHideAnimation');
+        console.log('GameBlockSelection 引用检查:', {
+            gameStartMove: !!this.gameStartMove,
+            blockSelectionUINode: !!this.blockSelectionUINode,
+            dibanNode: !!this.dibanNode,
+            blockSelectionUINodeName: this.blockSelectionUINode?.name,
+            dibanNodeName: this.dibanNode?.name
+        });
+        
+        if (this.gameStartMove && this.blockSelectionUINode && this.dibanNode) {
+            // 设置GameStartMove的blockSelectionUI和dibanNode引用
+            this.gameStartMove.blockSelectionUI = this.blockSelectionUINode;
+            this.gameStartMove.dibanNode = this.dibanNode;
+            
+            console.log('GameStartMove 引用设置后检查:', {
+                gameStartMoveBlockSelectionUI: !!this.gameStartMove.blockSelectionUI,
+                gameStartMoveDibanNode: !!this.gameStartMove.dibanNode
+            });
+            
+            // 每次隐藏BlockSelectionUI时,摄像头上移182px
+            this.gameStartMove.moveUpSmooth();
+            console.log('摄像头上移182px');
+            
+            // 播放下滑隐藏动画,动画完成后执行回调
+            this.gameStartMove.slideDibanDownAndHide(300, 0.3);
+            
+            // 由于slideDibanDownAndHide会自动隐藏blockSelectionUI和重置位置,我们需要在动画完成后执行回调
+            if (onComplete) {
+                this.scheduleOnce(() => {
+                    onComplete();
+                }, 0.3); // 与动画时长一致
+            }
+        } else {
+            console.log('GameBlockSelection 条件检查失败,无法播放动画');
+            if (onComplete) {
+                // 如果没有动画组件,直接执行回调
+                onComplete();
+            }
+        }
     }
 
     // 设置确认回调

+ 66 - 15
assets/scripts/CombatSystem/EnemyController.ts

@@ -5,6 +5,7 @@ import { EnemyComponent } from '../CombatSystem/EnemyComponent';
 import { EnemyInstance } from './EnemyInstance';
 import { BaseSingleton } from '../Core/BaseSingleton';
 import { SaveDataManager } from '../LevelSystem/SaveDataManager';
+import { GameManager } from '../LevelSystem/GameManager';
 const { ccclass, property } = _decorator;
 
 // 前向声明EnemyInstance类型,避免循环引用
@@ -261,14 +262,18 @@ export class EnemyController extends BaseSingleton {
     }
 
     // 游戏结束
-    stopGame() {
+    stopGame(clearEnemies: boolean = true) {
         this.gameStarted = false;
         
         // 停止生成敌人
         this.unschedule(this.spawnEnemy);
         
-        // 清除所有敌人
-        this.clearAllEnemies();
+        // 只有在指定时才清除所有敌人
+        if (clearEnemies) {
+            // 当游戏结束时清除敌人,不触发敌人死亡事件
+            const isGameOver = this.gameManager && typeof this.gameManager.isGameOver === 'function' && this.gameManager.isGameOver();
+            this.clearAllEnemies(!isGameOver); // 只有在游戏没有结束时才触发事件
+        }
         
         console.log('停止生成敌人');
     }
@@ -361,12 +366,28 @@ export class EnemyController extends BaseSingleton {
     }
 
     // 清除所有敌人
-    clearAllEnemies() {
+    clearAllEnemies(triggerEvents: boolean = true) {
+        // 如果不触发事件,先暂时禁用 notifyEnemyDead 方法
+        const originalNotifyMethod = this.notifyEnemyDead;
+        
+        if (!triggerEvents) {
+            // 临时替换为空函数
+            this.notifyEnemyDead = () => {};
+            console.log('[EnemyController] 临时禁用敌人死亡通知');
+        }
+        
         for (const enemy of this.activeEnemies) {
             if (enemy && enemy.isValid) {
                 enemy.destroy();
             }
         }
+        
+        // 恢复原来的方法
+        if (!triggerEvents) {
+            this.notifyEnemyDead = originalNotifyMethod;
+            console.log('[EnemyController] 恢复敌人死亡通知');
+        }
+        
         this.activeEnemies = [];
         // 更新敌人数量显示
         this.updateEnemyCountLabel();
@@ -432,6 +453,12 @@ export class EnemyController extends BaseSingleton {
     damageEnemy(enemy: Node, damage: number) {
         if (!enemy || !enemy.isValid) return;
         
+        // 检查游戏是否已经结束
+        if (this.gameManager && typeof this.gameManager.isGameOver === 'function' && this.gameManager.isGameOver()) {
+            console.warn('[EnemyController] 游戏已经结束,不再处理敌人伤害');
+            return;
+        }
+        
         // 获取敌人组件
         const enemyComp = enemy.getComponent(EnemyInstance);
         if (!enemyComp) return;
@@ -470,15 +497,20 @@ export class EnemyController extends BaseSingleton {
 
     // 游戏结束
     gameOver() {
-        // 停止游戏
-        this.stopGame();
+        // 停止游戏,但不清除敌人
+        this.stopGame(false);
         
         // 通知GameManager游戏结束
-        const gameManagerNode = find('Canvas/GameLevelUI/GameManager');
-        if (gameManagerNode) {
-            const gameManager = gameManagerNode.getComponent('GameManager') as any;
-            if (gameManager) {
-                gameManager.gameOver();
+        if (this.gameManager) {
+            this.gameManager.gameOver();
+        } else {
+            // 如果没有引用,尝试查找GameManager
+            const gameManagerNode = find('Canvas/GameManager');
+            if (gameManagerNode) {
+                const gameManager = gameManagerNode.getComponent(GameManager);
+                if (gameManager) {
+                    gameManager.gameOver();
+                }
             }
         }
     }
@@ -534,9 +566,19 @@ export class EnemyController extends BaseSingleton {
 
     // === 查找GameManager ===
     private findGameManager() {
-        const gameManagerNode = find('Canvas/GameLevelUI/GameManager');
+        // 尝试在GameLevelUI下查找GameManager
+        let gameManagerNode = find('Canvas/GameLevelUI/GameManager');
+        if (!gameManagerNode) {
+            // 如果没找到,尝试在Canvas下直接查找
+            gameManagerNode = find('Canvas/GameManager');
+        }
+        
         if (gameManagerNode) {
-            this.gameManager = gameManagerNode.getComponent('GameManager');
+            // 使用正确的类型获取组件
+            this.gameManager = gameManagerNode.getComponent(GameManager);
+            console.log('[EnemyController] GameManager引用已更新:', !!this.gameManager);
+        } else {
+            console.warn('[EnemyController] 找不到GameManager节点');
         }
     }
 
@@ -548,8 +590,17 @@ export class EnemyController extends BaseSingleton {
             this.currentWaveEnemiesKilled++;
             this.updateEnemyCountLabel();
         }
-        if (this.gameManager && this.gameManager.onEnemyKilled) {
-            this.gameManager.onEnemyKilled();
+        
+        if (this.gameManager) {
+            // 检查游戏是否已经结束
+            if (typeof this.gameManager.isGameOver === 'function' && this.gameManager.isGameOver()) {
+                console.warn('[EnemyController] 游戏已经结束,不再通知GameManager敌人被击杀');
+                return;
+            }
+            
+            if (this.gameManager.onEnemyKilled) {
+                this.gameManager.onEnemyKilled();
+            }
         }
     }
 

+ 2 - 2
assets/scripts/CombatSystem/SkillSelection/SkillSelectionController.ts

@@ -1,4 +1,4 @@
-import { _decorator, Component, Node, Button, find, Label, Sprite, Vec3 } from 'cc';
+import { _decorator, Component, Node, Button, find, Label, Sprite, Vec3, CCFloat } from 'cc';
 import { GameManager } from '../../LevelSystem/GameManager';
 import { SkillButtonAnimator } from './SkillButtonAnimator';
 import { GamePause } from '../GamePause';
@@ -16,7 +16,7 @@ export class SkillSelectionController extends Component {
     @property({ type: [Node], tooltip: '技能按钮节点列表,留空则自动从 SkillsContainer 获取' })
     public skillButtons: Node[] = [];
 
-    @property({ type: Number, tooltip: '收缩动画时长' })
+    @property({ type: CCFloat, tooltip: '收缩动画时长' })
     public shrinkDuration: number = 0.3;
 
     // 防止重复点击标记

+ 4 - 1
assets/scripts/CombatSystem/WeaponBullet.ts

@@ -514,7 +514,10 @@ export class WeaponBullet extends Component {
                 return;
             }
 
-            if (sprite && spriteFrame) {
+            // Add comprehensive null safety checks before setting spriteFrame
+            if (sprite && sprite.isValid && 
+                this.node && this.node.isValid && 
+                spriteFrame && spriteFrame.isValid) {
                 sprite.spriteFrame = spriteFrame;
 
                 // === 缩小至原始尺寸的 0.5 倍 ===

+ 18 - 2
assets/scripts/LevelSystem/GameManager.ts

@@ -869,9 +869,9 @@ export class GameManager extends Component {
             blockManager.onGameReset?.();
         }
  
-        // 清空关卡剩余敌人
+        // 清空关卡剩余敌人,不触发敌人死亡事件
         if (this.enemyController && this.enemyController.clearAllEnemies) {
-            this.enemyController.clearAllEnemies();
+            this.enemyController.clearAllEnemies(false);
         }
  
         // 重置墙体血量显示
@@ -1125,6 +1125,12 @@ export class GameManager extends Component {
 
     // 显示方块选择UI用于下一波
     private showBlockSelectionForNextWave() {
+        // 如果游戏已经结束,不显示方块选择UI
+        if (this.isGameOver()) {
+            console.warn('[GameManager] 游戏已经结束(胜利或失败),不显示下一波方块选择UI!当前状态:', this.currentState);
+            return;
+        }
+        
         if (this.blockSelectionComponent) {
             this.blockSelectionComponent.showBlockSelection(true);
             this.preparingNextWave = true;
@@ -1139,6 +1145,12 @@ export class GameManager extends Component {
 
     /** 敌人被消灭时由 EnemyController 调用 */
     public onEnemyKilled() {
+        // 如果游戏已经结束,不执行后续逻辑
+        if (this.isGameOver()) {
+            console.warn('[GameManager] 游戏已结束状态下onEnemyKilled被调用!当前状态:', this.currentState);
+            return;
+        }
+        
         this.enemiesKilled++;
         // 当前波击杀 +1
         this.currentWaveEnemyCount++;
@@ -1147,13 +1159,17 @@ export class GameManager extends Component {
         this.incrementEnergy();
         
         const remaining = this.currentWaveTotalEnemies - this.currentWaveEnemyCount;
+        console.log(`[GameManager] 敌人被消灭,当前波剩余敌人: ${remaining}/${this.currentWaveTotalEnemies}, 总击杀: ${this.enemiesKilled}`);
+        
         if (remaining <= 0) {
             // 当前波结束
             if (this.currentWave < (this.levelWaves?.length || 1)) {
                 // 还有下一波,显示提示
+                console.log(`[GameManager] 当前波结束,准备显示下一波(${this.currentWave + 1}/${this.levelWaves?.length || 1})选择UI`);
                 this.showNextWavePrompt();
             } else {
                 // 最后一波也结束
+                console.log('[GameManager] 最后一波结束,触发游戏胜利');
                 this.triggerGameSuccess();
             }
         }

+ 10 - 3
assets/scripts/test/WeaponRandomSpawner.ts

@@ -51,7 +51,8 @@ export class WeaponRandomSpawner extends Component {
 
             // Pick one sprite frame randomly and apply to weapon
             this._selectedFrame = assets[Math.floor(Math.random() * assets.length)];
-            if (this._weaponSprite) {
+            // Add null safety check before setting spriteFrame
+            if (this._weaponSprite && this._weaponSprite.isValid && this._weaponSprite.node && this._weaponSprite.node.isValid) {
                 this._weaponSprite.spriteFrame = this._selectedFrame;
             }
         });
@@ -62,6 +63,12 @@ export class WeaponRandomSpawner extends Component {
         this.schedule(this._fire.bind(this), this.fireInterval);
     }
 
+    onDestroy() {
+        // Clear references to prevent operations on destroyed components
+        this._weaponSprite = null;
+        this._selectedFrame = null;
+    }
+
     /**
      * Instantiate a bullet prefab, copy the weapon sprite frame to it (scaled to half size)
      * and give it an upward velocity.
@@ -76,9 +83,9 @@ export class WeaponRandomSpawner extends Component {
         this.node.scene?.addChild(bullet);
         bullet.setWorldPosition(this.node.worldPosition);
 
-        // Set bullet sprite frame
+        // Set bullet sprite frame with null safety checks
         const bulletSprite = bullet.getComponent(Sprite);
-        if (bulletSprite) {
+        if (bulletSprite && bulletSprite.isValid && bullet && bullet.isValid) {
             bulletSprite.spriteFrame = this._selectedFrame;
         }
 

+ 9 - 0
视觉音效资源配置表.csv

@@ -0,0 +1,9 @@
+武器ID,武器名称,1x1贴图,1x2贴图,2x1贴图,2x2贴图,子弹预制体,击中特效,拖尾特效,燃烧特效,枪口特效,发射音效
+pea_shooter,毛豆射手,images/PlantsSprite/001-1,images/PlantsSprite/001-1,images/PlantsSprite/001-1,images/PlantsSprite/001-1,bullets/PeaBullet,Animation/WeaponTx/tx0002/tx0002,,Animation/WeaponTx/tx0002/tx0002,audio/pea_shot
+sharp_carrot,尖胡萝卜,images/PlantsSprite/002,images/PlantsSprite/002,images/PlantsSprite/002,images/PlantsSprite/002,bullets/CarrotBullet,,Animation/WeaponTx/tx0001/tx0001,,Animation/WeaponTx/tx0001/tx0001,audio/carrot_shot
+saw_grass,锯齿草,images/PlantsSprite/003,images/PlantsSprite/003,images/PlantsSprite/003,images/PlantsSprite/003,bullets/SawBullet,Animation/WeaponTx/tx0002/tx0002,,,Animation/WeaponTx/tx0002/tx0002,audio/saw_shot
+watermelon_bomb,西瓜炸弹,images/PlantsSprite/007,images/PlantsSprite/007,images/PlantsSprite/007,images/PlantsSprite/007,bullets/WatermelonBomb,Animation/WeaponTx/tx0007/tx0007,,,Animation/WeaponTx/tx0007/tx0007,audio/bomb_launch
+boomerang_plant,回旋镖盆栽,images/PlantsSprite/004,images/PlantsSprite/004,images/PlantsSprite/004,images/PlantsSprite/004,bullets/BoomerangBullet,Animation/WeaponTx/tx0003/tx0003,,,Animation/WeaponTx/tx0003/tx0003,audio/boomerang_throw
+hot_pepper,炙热辣椒,images/PlantsSprite/005,images/PlantsSprite/005,images/PlantsSprite/005,images/PlantsSprite/005,bullets/PepperBomb,Animation/WeaponTx/tx0005/tx0005,,Animation/WeaponTx/tx0005/tx0005,Animation/WeaponTx/tx0005/tx0005,audio/pepper_launch
+cactus_shotgun,仙人散弹,images/PlantsSprite/008,images/PlantsSprite/008,images/PlantsSprite/008,images/PlantsSprite/008,bullets/CactusBullet,Animation/WeaponTx/tx0005/tx0005,,,Animation/WeaponTx/tx0005/tx0005,audio/cactus_shot
+okra_missile,秋葵导弹,images/PlantsSprite/006,images/PlantsSprite/006,images/PlantsSprite/006,images/PlantsSprite/006,bullets/OkraMissile,Animation/WeaponTx/tx0006/tx0006,,,Animation/WeaponTx/tx0006/tx0006,audio/missile_launch