Browse Source

小球逻辑

181404010226 4 weeks ago
parent
commit
7ba7991ae2

File diff suppressed because it is too large
+ 391 - 180
assets/Scenes/GameLevel.scene


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

@@ -35,10 +35,13 @@
       },
       {
         "__id__": 14
+      },
+      {
+        "__id__": 16
       }
     ],
     "_prefab": {
-      "__id__": 16
+      "__id__": 18
     },
     "_lpos": {
       "__type__": "cc.Vec3",
@@ -334,6 +337,40 @@
     "__type__": "cc.CompPrefabInfo",
     "fileId": "79BEOOH4hJDbrlpnLzyFts"
   },
+  {
+    "__type__": "9bec1lTuw5F36b1pk+vYHKE",
+    "_name": "",
+    "_objFlags": 0,
+    "__editorExtras__": {},
+    "node": {
+      "__id__": 1
+    },
+    "_enabled": true,
+    "__prefab": {
+      "__id__": 17
+    },
+    "baseSpeed": 60,
+    "currentSpeed": 60,
+    "enableOscillationDetection": true,
+    "oscillationTimeWindow": 3,
+    "directionChangeThreshold": 4,
+    "positionHistorySize": 20,
+    "oscillationDistanceThreshold": 100,
+    "testMode": true,
+    "enableBallPositionCheck": true,
+    "positionCheckInterval": 2,
+    "edgeOffset": 16,
+    "safeDistance": 36,
+    "ballRadius": 12,
+    "maxAttempts": 24,
+    "positionCheckBoundaryExtension": 24,
+    "placedBlocksContainer": null,
+    "_id": ""
+  },
+  {
+    "__type__": "cc.CompPrefabInfo",
+    "fileId": "387LObbQtNf5wbQlRpN4mM"
+  },
   {
     "__type__": "cc.PrefabInfo",
     "root": {

+ 124 - 359
assets/scripts/CombatSystem/BallController.ts

@@ -9,6 +9,7 @@ import { WeaponInfo } from './BlockSelection/WeaponInfo';
 import { BlockInfo } from './BlockSelection/BlockInfo';
 import { BallAni } from '../Animations/BallAni';
 import { JsonConfigLoader } from '../Core/JsonConfigLoader';
+import { BallInstance } from './BallInstance';
 const { ccclass, property } = _decorator;
 
 @ccclass('BallController')
@@ -164,7 +165,7 @@ export class BallController extends Component {
     private draggingBlocks: Set<Node> = new Set();
 
     // 小球位置检查相关变量
-    private lastPositionCheckTime: number = 0; // 上次位置检查的时间
+    // 控制器不再维护全局位置检查计时,由 BallInstance 负责实例级调度
 
     async start() {
         // 如果没有指定placedBlocksContainer,尝试找到它
@@ -344,7 +345,8 @@ export class BallController extends Component {
      */
     private onGamePauseEvent() {
         console.log('[BallController] 接收到游戏暂停事件,暂停小球运动');
-        this.pauseBall();
+        // 统一为暂停所有小球,避免仅暂停主球导致状态不一致
+        this.pauseAllBalls();
     }
 
     /**
@@ -360,7 +362,7 @@ export class BallController extends Component {
      */
     private onGamePauseBlockSelectionEvent() {
         console.log('[BallController] 接收到底板选择暂停事件,小球继续运动');
-        // 底板选择时不暂停小球运动,只是标记游戏状态为暂停
+        // 底板选择时不暂停小球运动,只是标记游戏状态为暂停,不修改速度或休眠状态
         this.isPaused = true;
     }
 
@@ -385,7 +387,8 @@ export class BallController extends Component {
      */
     private onGameResumeBlockSelectionEvent() {
         console.log('[BallController] 接收到底板选择恢复事件,恢复小球运动');
-        this.resumeAllBalls();
+        // 底板选择恢复,不需要恢复速度(未暂停),仅恢复标记
+        this.isPaused = false;
     }
     
     /**
@@ -423,14 +426,26 @@ export class BallController extends Component {
     /**
      * 处理子弹发射检查事件
      */
-    private onBallFireBulletEvent(blockNode: Node, fireWorldPos: Vec3) {
-        // 如果游戏暂停,则不允许发射子弹
+    private onBallFireBulletEvent(...args: any[]) {
+        // 兼容两种负载形态:
+        // 1) 检查形态 { canFire: (value:boolean)=>void }
+        // 2) 发射形态 (blockNode: Node, fireWorldPos: Vec3)
+        if (args.length === 1 && args[0] && typeof args[0].canFire === 'function') {
+            // 这是“是否允许发射”的检查事件,BallController 不参与决策,直接忽略即可
+            return;
+        }
+
+        const [blockNode, fireWorldPos] = args;
+        if (!(blockNode instanceof Node) || !(fireWorldPos instanceof Vec3)) {
+            // 负载不匹配,忽略
+            return;
+        }
+
         if (this.isPaused) {
             console.log('[BallController] 游戏暂停中,阻止子弹发射');
             return;
         }
-        
-        // 如果游戏未暂停,则继续执行子弹发射逻辑
+
         this.fireBulletAt(blockNode, fireWorldPos);
     }
     
@@ -518,8 +533,7 @@ export class BallController extends Component {
             this.node.addChild(this.activeBall);
         }
 
-        // 随机位置小球
-        this.positionBallRandomly();
+        // 位置随机由 BallInstance 统一处理(在注入配置后调用)
 
         // 设置球的半径
         const transform = this.activeBall.getComponent(UITransform);
@@ -529,6 +543,31 @@ export class BallController extends Component {
             this.radius = 25; // 默认半径
         }
 
+        // 为该球挂载 per-ball 逻辑组件(振荡检测/防围困)
+        const ballComp = this.activeBall.getComponent(BallInstance) || this.activeBall.addComponent(BallInstance);
+        if (ballComp) {
+            ballComp.baseSpeed = this.baseSpeed;
+            ballComp.currentSpeed = this.currentSpeed;
+            ballComp.positionHistorySize = this.positionHistorySize;
+            ballComp.oscillationTimeWindow = this.oscillationTimeWindow;
+            ballComp.directionChangeThreshold = this.directionChangeThreshold;
+            ballComp.oscillationDistanceThreshold = this.oscillationDistanceThreshold;
+            ballComp.testMode = this.testMode;
+            ballComp.enableOscillationDetection = true;
+            // 定期位置检查(实例级)
+            ballComp.enableBallPositionCheck = this.enableBallPositionCheck;
+            ballComp.positionCheckInterval = this.positionCheckInterval;
+            // 注入安全区域与避让配置
+            ballComp.edgeOffset = this.edgeOffset;
+            ballComp.safeDistance = this.safeDistance;
+            ballComp.ballRadius = this.ballRadius;
+            ballComp.maxAttempts = this.maxAttempts;
+            ballComp.positionCheckBoundaryExtension = this.positionCheckBoundaryExtension;
+            ballComp.placedBlocksContainer = this.placedBlocksContainer;
+            // 统一使用 BallInstance 的安全随机定位(避让方块,边界安全)
+            ballComp.placeRandomlyInGameArea();
+        }
+
         // 确保有碰撞组件
         this.setupCollider();
 
@@ -546,7 +585,6 @@ export class BallController extends Component {
 
         // 实例化新的小球
         const newBall = instantiate(this.ballPrefab);
-        newBall.name = 'AdditionalBall';
         
         // 将小球添加到GameArea中
         const gameArea = find('Canvas/GameLevelUI/GameArea');
@@ -556,12 +594,36 @@ export class BallController extends Component {
             this.node.addChild(newBall);
         }
 
-        // 随机位置小球
-        this.positionAdditionalBall(newBall);
+        // 位置随机改为由 BallInstance 统一处理(在注入配置后执行)
 
         // 设置球的碰撞组件
         this.setupBallCollider(newBall);
         
+        // 为额外球挂载 per-ball 逻辑组件(振荡检测/防围困)
+        const addComp = newBall.getComponent(BallInstance) || newBall.addComponent(BallInstance);
+        if (addComp) {
+            addComp.baseSpeed = this.baseSpeed;
+            addComp.currentSpeed = this.currentSpeed;
+            addComp.positionHistorySize = this.positionHistorySize;
+            addComp.oscillationTimeWindow = this.oscillationTimeWindow;
+            addComp.directionChangeThreshold = this.directionChangeThreshold;
+            addComp.oscillationDistanceThreshold = this.oscillationDistanceThreshold;
+            addComp.testMode = this.testMode;
+            addComp.enableOscillationDetection = true;
+            // 定期位置检查(实例级)
+            addComp.enableBallPositionCheck = this.enableBallPositionCheck;
+            addComp.positionCheckInterval = this.positionCheckInterval;
+            // 注入安全区域与避让配置
+            addComp.edgeOffset = this.edgeOffset;
+            addComp.safeDistance = this.safeDistance;
+            addComp.ballRadius = this.ballRadius;
+            addComp.maxAttempts = this.maxAttempts;
+            addComp.positionCheckBoundaryExtension = this.positionCheckBoundaryExtension;
+            addComp.placedBlocksContainer = this.placedBlocksContainer;
+            // 统一使用 BallInstance 的安全随机定位(避让方块,边界安全)
+            addComp.placeRandomlyInGameArea();
+        }
+        
         // 设置初始方向和速度
         this.initializeBallDirection(newBall);
         
@@ -687,6 +749,10 @@ export class BallController extends Component {
                 if (blockCollider.sensor) {
                     blockCollider.sensor = false;
                 }
+
+                // 统一方块的摩擦与回弹,避免能量衰减
+                blockCollider.friction = 0;
+                blockCollider.restitution = 1;
             }
             
             // 检查B1子节点的碰撞体
@@ -704,13 +770,29 @@ export class BallController extends Component {
                     if (b1Collider.sensor) {
                         b1Collider.sensor = false;
                     }
+
+                    // 统一子节点的摩擦与回弹
+                    b1Collider.friction = 0;
+                    b1Collider.restitution = 1;
                 }
             }
             
             // 检查Weapon子节点
             const weaponNode = this.findWeaponNode(block);
             if (weaponNode) {
-                // 武器节点存在
+                // 武器节点存在,确保其碰撞体也不引入能量损失
+                const weaponCollider = weaponNode.getComponent(Collider2D);
+                if (weaponCollider) {
+                    if (weaponCollider.group !== 2) {
+                        weaponCollider.group = 2;
+                        fixedCount++;
+                    }
+                    if (weaponCollider.sensor) {
+                        weaponCollider.sensor = false;
+                    }
+                    weaponCollider.friction = 0;
+                    weaponCollider.restitution = 1;
+                }
             }
         }
     }
@@ -719,96 +801,11 @@ export class BallController extends Component {
     positionBallRandomly() {
         if (!this.activeBall) return;
 
-        const transform = this.activeBall.getComponent(UITransform);
-        const ballRadius = transform ? transform.width / 2 : this.ballRadius;
-        
-        // 计算可生成的范围(考虑小球半径,避免生成在边缘)
-        const minX = this.gameBounds.left + ballRadius + this.edgeOffset; // 额外偏移,避免生成在边缘
-        const maxX = this.gameBounds.right - ballRadius - this.edgeOffset;
-        const minY = this.gameBounds.bottom + ballRadius + this.edgeOffset;
-        const maxY = this.gameBounds.top - ballRadius - this.edgeOffset;
-
-        // 获取GameArea节点
-        const gameArea = find('Canvas/GameLevelUI/GameArea');
-        if (!gameArea) {
-            return;
-        }
-        
-        // 查找PlacedBlocks节点,它包含所有放置的方块
-        if (!this.placedBlocksContainer) {
-            this.setRandomPositionDefault(this.activeBall, minX, maxX, minY, maxY);
-            return;
+        // 优先委派到 BallInstance 的公开方法(统一避让与安全逻辑)
+        const inst = this.activeBall.getComponent(BallInstance);
+        if (inst) {
+            inst.placeRandomlyInGameArea();
         }
-    
-        
-        // 获取所有已放置的方块
-        const placedBlocks = [];
-        for (let i = 0; i < this.placedBlocksContainer.children.length; i++) {
-            const block = this.placedBlocksContainer.children[i];
-            // 检查是否是方块节点(通常以Block命名或有特定标识)
-            if (block.name.includes('Block') || block.getChildByName('B1')) {
-                placedBlocks.push(block);
-            }
-        }
-        
-        // 如果没有方块,使用默认随机位置
-        if (placedBlocks.length === 0) {
-            this.setRandomPositionDefault(this.activeBall, minX, maxX, minY, maxY);
-            return;
-        }
-        
-        // 尝试找到一个不与任何方块重叠的位置
-        let validPosition = false;
-        let attempts = 0;
-        const maxAttempts = this.maxAttempts; // 从配置中获取最大尝试次数
-        let randomX, randomY;
-        
-        while (!validPosition && attempts < maxAttempts) {
-            // 随机生成位置
-            randomX = Math.random() * (maxX - minX) + minX;
-            randomY = Math.random() * (maxY - minY) + minY;
-            
-            // 检查是否与任何方块重叠
-            let overlapping = false;
-            for (const block of placedBlocks) {
-                // 获取方块的世界坐标
-                const blockWorldPos = block.worldPosition;
-                
-                // 计算小球与方块的距离
-                const distance = Math.sqrt(
-                    Math.pow(randomX - blockWorldPos.x, 2) + 
-                    Math.pow(randomY - blockWorldPos.y, 2)
-                );
-                
-                // 获取方块的尺寸
-                const blockTransform = block.getComponent(UITransform);
-                const blockSize = blockTransform ? 
-                    Math.max(blockTransform.width, blockTransform.height) / 2 : this.safeDistance;
-                
-                // 如果距离小于小球半径+方块尺寸的一半+安全距离,认为重叠
-                if (distance < ballRadius + blockSize + this.safeDistance) {
-                    overlapping = true;
-                    break;
-                }
-            }
-            
-            // 如果没有重叠,找到了有效位置
-            if (!overlapping) {
-                validPosition = true;
-            }
-            
-            attempts++;
-        }
-        
-        // 如果找不到有效位置,使用默认位置(游戏区域底部中心)
-        if (!validPosition) {
-            randomX = (this.gameBounds.left + this.gameBounds.right) / 2;
-            randomY = this.gameBounds.bottom + ballRadius + this.safeDistance; // 底部上方安全距离单位
-        }
-        
-        // 将世界坐标转换为相对于GameArea的本地坐标
-        const localPos = gameArea.getComponent(UITransform).convertToNodeSpaceAR(new Vec3(randomX, randomY, 0));
-        this.activeBall.position = localPos;
     }
 
 
@@ -1487,18 +1484,15 @@ export class BallController extends Component {
         this.maintainAllBallsSpeed();
         
         // 更新小球运动历史数据(用于检测振荡模式)
-        this.updateBallMovementHistory();
+        // 如果未挂载 BallInstance,则保持旧逻辑作为回退;否则由 BallInstance 自行处理
+        const hasInstance = this.activeBall && this.activeBall.isValid && this.activeBall.getComponent(BallInstance);
+        if (!hasInstance) {
+            this.updateBallMovementHistory();
+        }
         
         // 清理过期的防围困状态
         this.cleanupExpiredAntiTrapStates();
-        
-        // 定期检查小球位置,防止小球被挤出游戏区域
-        this.lastPositionCheckTime += dt;
-        if (this.lastPositionCheckTime >= this.positionCheckInterval) {
-            this.checkAndRescueAllBalls();
-            this.lastPositionCheckTime = 0;
-        }
-        
+
         // 定期检查小球是否接近方块但没有触发碰撞(调试用)
         if (this.activeBall && this.activeBall.isValid) {
             this.debugCheckNearBlocks();
@@ -1799,43 +1793,20 @@ export class BallController extends Component {
     }
 
 
-    // 初始化方向
+    // 初始化方向(统一随机角度)
     initializeDirection() {
-        // 测试模式:使用完全垂直或水平方向来测试防围困机制
-        if (this.testMode) {
-            // 四个基本方向:上、下、左、右
-            const directions = [
-                { x: 0, y: 1 },   // 向上
-                { x: 0, y: -1 },  // 向下
-                { x: 1, y: 0 },   // 向右
-                { x: -1, y: 0 }   // 向左
-            ];
-            
-            // 随机选择一个基本方向
-            const randomIndex = Math.floor(Math.random() * directions.length);
-            const selectedDirection = directions[randomIndex];
-            
-            this.direction.x = selectedDirection.x;
-            this.direction.y = selectedDirection.y;
-            this.direction.normalize();
-            
-            console.log(`[BallController] 测试模式 - 小球初始方向: ${randomIndex === 0 ? '向上' : randomIndex === 1 ? '向下' : randomIndex === 2 ? '向右' : '向左'}`);
-        } else {
-            // 原始随机方向模式
-            const angle = Math.random() * Math.PI * 2; // 0-2π之间的随机角度
-            this.direction.x = Math.cos(angle);
-            this.direction.y = Math.sin(angle);
-            this.direction.normalize();
-        }
-        
-        // 设置初始速度
-        if (this.activeBall) {
-            const rigidBody = this.activeBall.getComponent(RigidBody2D);
-            if (rigidBody) {
-                rigidBody.linearVelocity = new Vec2(
-                    this.direction.x * this.currentSpeed,
-                    this.direction.y * this.currentSpeed
-                );
+        if (!this.activeBall) return;
+        // 使用统一的随机角度初始化速度
+        this.initializeBallDirection(this.activeBall);
+        // 同步 controller.direction 为当前速度方向
+        const rigidBody = this.activeBall.getComponent(RigidBody2D);
+        if (rigidBody) {
+            const v = rigidBody.linearVelocity;
+            const dir = new Vec2(v.x, v.y);
+            if (dir.length() > 0) {
+                dir.normalize();
+                this.direction.x = dir.x;
+                this.direction.y = dir.y;
             }
         }
     }
@@ -2184,219 +2155,13 @@ export class BallController extends Component {
         return balls;
     }
     
-    /**
-     * 检查单个小球是否在游戏区域内
-     * @param ball 要检查的小球节点
-     * @returns 如果小球在游戏区域内返回true,否则返回false
-     */
-    private checkBallPosition(ball: Node): boolean {
-        if (!ball || !ball.isValid) {
-            return false;
-        }
-        
-        // 获取小球的世界坐标
-        const worldPos = ball.getWorldPosition();
-        
-        // 计算扩展后的边界(比游戏区域稍大,避免误判)
-        const extension = this.positionCheckBoundaryExtension;
-        const extendedBounds = {
-            left: this.gameBounds.left - extension,
-            right: this.gameBounds.right + extension,
-            top: this.gameBounds.top + extension,
-            bottom: this.gameBounds.bottom - extension
-        };
-        
-        // 检查小球是否在扩展边界内
-        const isInBounds = worldPos.x >= extendedBounds.left && 
-                          worldPos.x <= extendedBounds.right && 
-                          worldPos.y >= extendedBounds.bottom && 
-                          worldPos.y <= extendedBounds.top;
-        
-        if (!isInBounds) {
-            console.warn(`[BallController] 小球 ${ball.name} 超出游戏区域:`, {
-                position: { x: worldPos.x, y: worldPos.y },
-                bounds: extendedBounds
-            });
-        }
-        
-        return isInBounds;
-    }
     
-    /**
-     * 将小球重置回游戏区域
-     * @param ball 要重置的小球节点
-     */
-    private rescueBallToGameArea(ball: Node): void {
-        if (!ball || !ball.isValid) {
-            return;
-        }
-        
-        console.log(`[BallController] 正在找回小球 ${ball.name}`);
-        
-        // 使用与小球生成相同的安全位置设置逻辑
-        this.positionBallSafely(ball);
-        
-        // 重新设置小球的运动方向和速度
-        const rigidBody = ball.getComponent(RigidBody2D);
-        if (rigidBody) {
-            // 随机新的运动方向
-            const angle = Math.random() * Math.PI * 2;
-            const direction = new Vec2(Math.cos(angle), Math.sin(angle)).normalize();
-            
-            // 设置新的速度
-            rigidBody.linearVelocity = new Vec2(
-                direction.x * this.currentSpeed,
-                direction.y * this.currentSpeed
-            );
-        }
-        
-        console.log(`[BallController] 小球 ${ball.name} 已重置到安全位置`);
-    }
     
-    /**
-     * 为小球设置安全位置(复用生成位置逻辑)
-     * @param ball 要设置位置的小球节点
-     */
-    private positionBallSafely(ball: Node): void {
-        if (!ball) return;
-
-        const transform = ball.getComponent(UITransform);
-        const ballRadius = transform ? transform.width / 2 : this.ballRadius;
-        
-        // 计算可生成的范围(考虑小球半径,避免生成在边缘)
-        const minX = this.gameBounds.left + ballRadius + this.edgeOffset;
-        const maxX = this.gameBounds.right - ballRadius - this.edgeOffset;
-        const minY = this.gameBounds.bottom + ballRadius + this.edgeOffset;
-        const maxY = this.gameBounds.top - ballRadius - this.edgeOffset;
-
-        // 获取GameArea节点
-        const gameArea = find('Canvas/GameLevelUI/GameArea');
-        if (!gameArea) {
-            return;
-        }
-        
-        // 查找PlacedBlocks节点,它包含所有放置的方块
-        if (!this.placedBlocksContainer) {
-            this.setRandomPositionDefault(ball, minX, maxX, minY, maxY);
-            return;
-        }
-        
-        if (!this.placedBlocksContainer.isValid) {
-            this.setRandomPositionDefault(ball, minX, maxX, minY, maxY);
-            return;
-        }
-        
-        // 获取所有已放置的方块
-        const placedBlocks = [];
-        for (let i = 0; i < this.placedBlocksContainer.children.length; i++) {
-            const block = this.placedBlocksContainer.children[i];
-            // 检查是否是方块节点(通常以Block命名或有特定标识)
-            if (block.name.includes('Block') || block.getChildByName('B1')) {
-                placedBlocks.push(block);
-            }
-        }
-        
-        // 如果没有方块,使用默认随机位置
-        if (placedBlocks.length === 0) {
-            this.setRandomPositionDefault(ball, minX, maxX, minY, maxY);
-            return;
-        }
-        
-        // 尝试找到一个不与任何方块重叠的位置
-        let validPosition = false;
-        let attempts = 0;
-        const maxAttempts = this.maxAttempts;
-        let randomX, randomY;
-        
-        while (!validPosition && attempts < maxAttempts) {
-            // 随机生成位置
-            randomX = Math.random() * (maxX - minX) + minX;
-            randomY = Math.random() * (maxY - minY) + minY;
-            
-            // 检查是否与任何方块重叠
-            let overlapping = false;
-            for (const block of placedBlocks) {
-                // 获取方块的世界坐标
-                const blockWorldPos = block.worldPosition;
-                
-                // 计算小球与方块的距离
-                const distance = Math.sqrt(
-                    Math.pow(randomX - blockWorldPos.x, 2) + 
-                    Math.pow(randomY - blockWorldPos.y, 2)
-                );
-                
-                // 获取方块的尺寸
-                const blockTransform = block.getComponent(UITransform);
-                const blockSize = blockTransform ? 
-                    Math.max(blockTransform.width, blockTransform.height) / 2 : this.safeDistance;
-                
-                // 如果距离小于小球半径+方块尺寸的一半+安全距离,认为重叠
-                if (distance < ballRadius + blockSize + this.safeDistance) {
-                    overlapping = true;
-                    break;
-                }
-            }
-            
-            // 如果没有重叠,找到了有效位置
-            if (!overlapping) {
-                validPosition = true;
-            }
-            
-            attempts++;
-        }
-        
-        // 如果找不到有效位置,使用默认位置(游戏区域底部中心)
-        if (!validPosition) {
-            randomX = (this.gameBounds.left + this.gameBounds.right) / 2;
-            randomY = this.gameBounds.bottom + ballRadius + this.safeDistance;
-        }
-        
-        // 将世界坐标转换为相对于GameArea的本地坐标
-        const localPos = gameArea.getComponent(UITransform).convertToNodeSpaceAR(new Vec3(randomX, randomY, 0));
-        ball.position = localPos;
-    }
     
-    /**
-     * 设置小球默认位置
-     * @param ball 小球节点
-     * @param minX 最小X坐标
-     * @param maxX 最大X坐标
-     * @param minY 最小Y坐标
-     * @param maxY 最大Y坐标
-     */
-    private setRandomPositionDefault(ball: Node, minX: number, maxX: number, minY: number, maxY: number): void {
-        const gameArea = find('Canvas/GameLevelUI/GameArea');
-        if (!gameArea) return;
-        
-        // 随机生成位置
-        const randomX = Math.random() * (maxX - minX) + minX;
-        const randomY = Math.random() * (maxY - minY) + minY;
-        
-        // 将世界坐标转换为相对于GameArea的本地坐标
-        const localPos = gameArea.getComponent(UITransform).convertToNodeSpaceAR(new Vec3(randomX, randomY, 0));
-        ball.position = localPos;
-    }
     
-    /**
-     * 检查并找回所有超出游戏区域的小球
-     */
-    private checkAndRescueAllBalls(): void {
-        if (!this.enableBallPositionCheck) {
-            return;
-        }
-        
-        const balls = this.getAllActiveBalls();
-        let rescuedCount = 0;
-        
-        balls.forEach(ball => {
-            if (!this.checkBallPosition(ball)) {
-                this.rescueBallToGameArea(ball);
-                rescuedCount++;
-            }
-        });
-        
-        if (rescuedCount > 0) {
-            console.log(`[BallController] 本次检查找回了 ${rescuedCount} 个小球`);
-        }
-    }
+    
+    
+    
+    
+    
 }

+ 307 - 0
assets/scripts/CombatSystem/BallInstance.ts

@@ -0,0 +1,307 @@
+import { _decorator, Component, Node, Vec2, Vec3, RigidBody2D, CircleCollider2D, find, UITransform } from 'cc';
+const { ccclass, property } = _decorator;
+
+@ccclass('BallInstance')
+export class BallInstance extends Component {
+    @property({ tooltip: '基础速度,用于防围困时的冲量大小参考' })
+    public baseSpeed: number = 60;
+
+    @property({ tooltip: '目标速度(不在此组件内强制维持,用于参考)' })
+    public currentSpeed: number = 60;
+
+    @property({ tooltip: '是否启用振荡检测与增强防围困' })
+    public enableOscillationDetection: boolean = true;
+
+    @property({ tooltip: '振荡检测时间窗口(秒)' })
+    public oscillationTimeWindow: number = 3.0;
+
+    @property({ tooltip: '方向改变阈值(次)' })
+    public directionChangeThreshold: number = 4;
+
+    @property({ tooltip: '位置历史记录大小' })
+    public positionHistorySize: number = 20;
+
+    @property({ tooltip: '振荡距离阈值(像素)' })
+    public oscillationDistanceThreshold: number = 100;
+
+    @property({ tooltip: '测试模式(启用日志)' })
+    public testMode: boolean = true;
+
+    // —— 位置检查(实例级调度) ——
+    @property({ tooltip: '启用位置检查与救援(实例级)' })
+    public enableBallPositionCheck: boolean = true;
+
+    @property({ tooltip: '位置检查间隔(秒)(实例级)' })
+    public positionCheckInterval: number = 2.0;
+
+    private _positionCheckElapsed: number = 0;
+
+    // —— 安全区域与安全放置配置(由控制器注入) ——
+    @property({ tooltip: '与边缘保持的最小距离' })
+    public edgeOffset: number = 16;
+
+    @property({ tooltip: '与方块保持的最小安全距离' })
+    public safeDistance: number = 36;
+
+    @property({ tooltip: '球半径(用于避让计算)' })
+    public ballRadius: number = 12;
+
+    @property({ tooltip: '放置尝试最大次数' })
+    public maxAttempts: number = 24;
+
+    @property({ tooltip: '位置检查边界扩展(像素)' })
+    public positionCheckBoundaryExtension: number = 24;
+
+    @property({ type: Node, tooltip: '已放置方块容器(Canvas/GameLevelUI/GameArea/PlacedBlocks)' })
+    public placedBlocksContainer: Node | null = null;
+
+    private posHistory: Vec2[] = [];
+    private dirHistory: Vec2[] = [];
+    private lastDirectionChange: number = 0;
+    private directionChangeCount: number = 0;
+    private oscillationAxis: 'horizontal' | 'vertical' | 'none' = 'none';
+    private oscillationStartTime: number = 0;
+
+    update(dt: number) {
+        if (!this.enableOscillationDetection) return;
+
+        const ball = this.node;
+        if (!ball || !ball.isValid) return;
+
+        const rigidBody = ball.getComponent(RigidBody2D);
+        if (!rigidBody) return;
+
+        const currentTime = Date.now() / 1000;
+        const pos = ball.getWorldPosition();
+        const vel = rigidBody.linearVelocity;
+        const dir = new Vec2(vel.x, vel.y).normalize();
+
+        // 初始化
+        if (this.posHistory.length === 0) {
+            this.lastDirectionChange = currentTime;
+            this.directionChangeCount = 0;
+            this.oscillationAxis = 'none';
+            this.oscillationStartTime = 0;
+        }
+
+        // 位置历史
+        this.posHistory.push(new Vec2(pos.x, pos.y));
+        if (this.posHistory.length > this.positionHistorySize) {
+            this.posHistory.shift();
+        }
+
+        // 方向历史
+        this.dirHistory.push(dir.clone());
+        if (this.dirHistory.length > this.positionHistorySize) {
+            this.dirHistory.shift();
+        }
+
+        // 检测方向改变(点积 < 0 认为显著改变)
+        if (this.dirHistory.length >= 2) {
+            const prev = this.dirHistory[this.dirHistory.length - 2];
+            const dot = Vec2.dot(prev, dir);
+            if (dot < 0) {
+                this.directionChangeCount++;
+                this.lastDirectionChange = currentTime;
+                if (this.testMode) {
+                    // console.log(`[BallInstance] 方向改变,累计: ${this.directionChangeCount}`);
+                }
+            }
+        }
+
+        // 时间窗口外重置
+        const timeSinceLast = currentTime - this.lastDirectionChange;
+        if (timeSinceLast > this.oscillationTimeWindow) {
+            this.directionChangeCount = 0;
+            this.oscillationAxis = 'none';
+            return;
+        }
+
+        // 达到方向改变阈值,分析振荡轴向并触发
+        if (this.directionChangeCount >= this.directionChangeThreshold) {
+            const axis = this.analyzeOscillationAxis(this.posHistory);
+            if (axis !== 'none' && this.oscillationAxis === 'none') {
+                this.oscillationAxis = axis;
+                this.oscillationStartTime = currentTime;
+                if (this.testMode) {
+                    console.log(`[BallInstance] 检测到${axis === 'horizontal' ? '水平' : '垂直'}振荡,触发增强防围困`);
+                }
+                this.triggerEnhancedAntiTrap(axis);
+            }
+        }
+
+        // 实例级位置检查调度
+        if (this.enableBallPositionCheck) {
+            this._positionCheckElapsed += dt;
+            if (this._positionCheckElapsed >= this.positionCheckInterval) {
+                this.checkAndRescueIfNeeded();
+                this._positionCheckElapsed = 0;
+            }
+        }
+    }
+
+    private analyzeOscillationAxis(history: Vec2[]): 'horizontal' | 'vertical' | 'none' {
+        if (!history || history.length < 10) return 'none';
+        const recent = history.slice(-10);
+        let minX = recent[0].x, maxX = recent[0].x;
+        let minY = recent[0].y, maxY = recent[0].y;
+        for (const p of recent) {
+            minX = Math.min(minX, p.x);
+            maxX = Math.max(maxX, p.x);
+            minY = Math.min(minY, p.y);
+            maxY = Math.max(maxY, p.y);
+        }
+        const xRange = maxX - minX;
+        const yRange = maxY - minY;
+        if (xRange > this.oscillationDistanceThreshold && yRange < this.oscillationDistanceThreshold / 2) {
+            return 'horizontal';
+        }
+        if (yRange > this.oscillationDistanceThreshold && xRange < this.oscillationDistanceThreshold / 2) {
+            return 'vertical';
+        }
+        return 'none';
+    }
+
+    private triggerEnhancedAntiTrap(axis: 'horizontal' | 'vertical') {
+        const rigidBody = this.node.getComponent(RigidBody2D);
+        if (!rigidBody) return;
+
+        if (axis === 'horizontal') {
+            const verticalImpulse = (Math.random() - 0.5) * this.baseSpeed * 0.8;
+            const newVel = new Vec2(rigidBody.linearVelocity.x * 0.3, verticalImpulse);
+            rigidBody.linearVelocity = newVel;
+            if (this.testMode) {
+                console.log(`[BallInstance] 水平振荡 -> 垂直冲量: ${verticalImpulse.toFixed(2)}`);
+            }
+        } else {
+            const horizontalImpulse = (Math.random() - 0.5) * this.baseSpeed * 0.8;
+            const newVel = new Vec2(horizontalImpulse, rigidBody.linearVelocity.y * 0.3);
+            rigidBody.linearVelocity = newVel;
+            if (this.testMode) {
+                console.log(`[BallInstance] 垂直振荡 -> 水平冲量: ${horizontalImpulse.toFixed(2)}`);
+            }
+        }
+    }
+
+    // ---------- Safe Area Logic (migrated) ----------
+    private getGameBounds(): { left: number; right: number; top: number; bottom: number; gameArea: Node; ui: UITransform } | null {
+        const gameArea = find('Canvas/GameLevelUI/GameArea');
+        if (!gameArea) return null;
+        const ui = gameArea.getComponent(UITransform);
+        if (!ui) return null;
+        const wpos = gameArea.worldPosition;
+        const halfW = ui.width / 2;
+        const halfH = ui.height / 2;
+        return {
+            left: wpos.x - halfW,
+            right: wpos.x + halfW,
+            bottom: wpos.y - halfH,
+            top: wpos.y + halfH,
+            gameArea,
+            ui,
+        };
+    }
+
+    public checkInBounds(): boolean {
+        const bounds = this.getGameBounds();
+        if (!bounds) return true;
+        const ext = this.positionCheckBoundaryExtension;
+        const pos = this.node.worldPosition;
+        const inX = pos.x >= (bounds.left - ext) && pos.x <= (bounds.right + ext);
+        const inY = pos.y >= (bounds.bottom - ext) && pos.y <= (bounds.top + ext);
+        return inX && inY;
+    }
+
+    public rescueToGameArea(): void {
+        const bounds = this.getGameBounds();
+        if (!bounds) return;
+        this.positionSafely(bounds);
+        const rigidBody = this.node.getComponent(RigidBody2D);
+        if (rigidBody) {
+            const angle = Math.random() * Math.PI * 2;
+            const dir = new Vec2(Math.cos(angle), Math.sin(angle)).normalize();
+            rigidBody.linearVelocity = new Vec2(dir.x * this.currentSpeed, dir.y * this.currentSpeed);
+        }
+    }
+
+    public checkAndRescueIfNeeded(): boolean {
+        if (this.checkInBounds()) return false;
+        this.rescueToGameArea();
+        return true;
+    }
+
+    /**
+     * 对外公开:在游戏区域内安全随机摆放(考虑方块避让)
+     */
+    public placeRandomlyInGameArea(): void {
+        const bounds = this.getGameBounds();
+        if (!bounds) return;
+        this.positionSafely(bounds);
+    }
+
+    private positionSafely(bounds: { left: number; right: number; top: number; bottom: number; gameArea: Node; ui: UITransform; }): void {
+        const minX = bounds.left + this.ballRadius + this.edgeOffset;
+        const maxX = bounds.right - this.ballRadius - this.edgeOffset;
+        const minY = bounds.bottom + this.ballRadius + this.edgeOffset;
+        const maxY = bounds.top - this.ballRadius - this.edgeOffset;
+
+        if (!this.placedBlocksContainer || !this.placedBlocksContainer.isValid) {
+            this.setRandomPositionDefault(minX, maxX, minY, maxY, bounds);
+            return;
+        }
+
+        const placedBlocks: Node[] = [];
+        for (const child of this.placedBlocksContainer.children) {
+            if (child.name.includes('Block') || child.getChildByName('B1')) {
+                placedBlocks.push(child);
+            }
+        }
+
+        if (placedBlocks.length === 0) {
+            this.setRandomPositionDefault(minX, maxX, minY, maxY, bounds);
+            return;
+        }
+
+        let attempts = 0;
+        const maxAttempts = this.maxAttempts;
+        while (attempts < maxAttempts) {
+            const rx = Math.random() * (maxX - minX) + minX;
+            const ry = Math.random() * (maxY - minY) + minY;
+            const worldPos = new Vec3(rx, ry, 0);
+
+            let overlapping = false;
+            for (const block of placedBlocks) {
+                const bpos = block.worldPosition;
+                const dx = rx - bpos.x;
+                const dy = ry - bpos.y;
+                const dist = Math.sqrt(dx * dx + dy * dy);
+                const bt = block.getComponent(UITransform);
+                const bsize = bt ? Math.max(bt.width, bt.height) / 2 : this.safeDistance;
+                if (dist < this.ballRadius + bsize + this.safeDistance) {
+                    overlapping = true;
+                    break;
+                }
+            }
+
+            if (!overlapping) {
+                const local = bounds.ui.convertToNodeSpaceAR(worldPos);
+                this.node.setPosition(local);
+                return;
+            }
+            attempts++;
+        }
+
+        // 兜底:底部中心
+        const cx = (bounds.left + bounds.right) / 2;
+        const cy = bounds.bottom + this.ballRadius + this.safeDistance;
+        const fallback = new Vec3(cx, cy, 0);
+        this.node.setPosition(bounds.ui.convertToNodeSpaceAR(fallback));
+    }
+
+    private setRandomPositionDefault(minX: number, maxX: number, minY: number, maxY: number, bounds: { ui: UITransform }): void {
+        const rx = Math.random() * (maxX - minX) + minX;
+        const ry = Math.random() * (maxY - minY) + minY;
+        const worldPos = new Vec3(rx, ry, 0);
+        this.node.setPosition(bounds.ui.convertToNodeSpaceAR(worldPos));
+    }
+}

+ 9 - 0
assets/scripts/CombatSystem/BallInstance.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "9bec1953-bb0e-45df-a6f5-a64faf607284",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

Some files were not shown because too many files changed in this diff