diff --git a/Assets/Framework/Pool/EffectPool.cs b/Assets/Framework/Pool/EffectPool.cs
index 870e37111..3c8df4453 100644
--- a/Assets/Framework/Pool/EffectPool.cs
+++ b/Assets/Framework/Pool/EffectPool.cs
@@ -128,6 +128,7 @@ public class EffectAutoReturn : MonoBehaviour
private ParticleSystem[] particles;
private float checkTimer;
private const float MinPlayTime = 0.3f; // 至少播 0.3s 再检测,避免第一帧 IsAlive=false 误判
+ private const float MaxLifetime = 6f; // ⭐ 兜底:超过 6 秒强制回池,防止 looping 残留导致特效永生
/// 重新播放所有粒子
public void Play()
@@ -140,6 +141,11 @@ public class EffectAutoReturn : MonoBehaviour
{
if (p != null)
{
+ // ⭐ 强制关闭 looping:有些预制体子级 ParticleSystem 设了 looping=1,
+ // 会导致 IsAlive(true) 永远返回 true,特效一直累加直至内存爆满死机
+ var main = p.main;
+ main.loop = false;
+
p.Clear(true);
p.Play(true);
}
@@ -151,6 +157,13 @@ public class EffectAutoReturn : MonoBehaviour
checkTimer += Time.deltaTime;
if (checkTimer < MinPlayTime) return;
+ // ⭐ 6 秒兜底:无论是否还存活,超过 6 秒强制回池
+ if (checkTimer >= MaxLifetime)
+ {
+ EffectPool.Instance.Return(gameObject);
+ return;
+ }
+
// 所有粒子都播完 → 回池
if (particles == null || particles.Length == 0)
{
diff --git a/Assets/MyGame/Scripts/Manager/CreateFishManager.cs b/Assets/MyGame/Scripts/Manager/CreateFishManager.cs
index 2fe55d8db..49bb3c0db 100644
--- a/Assets/MyGame/Scripts/Manager/CreateFishManager.cs
+++ b/Assets/MyGame/Scripts/Manager/CreateFishManager.cs
@@ -1059,6 +1059,8 @@ public class CreateFishManager : MonoSingleton
public void CreateFish(SyncGameData syncGameData)
{
if (isFrozen) return; // 冰冻中不创建鱼
+ // ⭐ 场景卸载/CloseSpawnSystem 后 spawnDic 会变 null,此时收到的广播直接忽略
+ if (spawnDic == null) return;
var rotateData = FrameDataParser.Parse(syncGameData.frameData);
if (rotateData != null)
SpawnFishMainThread(rotateData, syncGameData.time);
@@ -1068,6 +1070,8 @@ public class CreateFishManager : MonoSingleton
///
private void SpawnFishMainThread(FrameData_CreateFish createFishData, long time)
{
+ // ⭐ spawnDic 可能在场景卸载时被置 null,这里再保护一次
+ if (spawnDic == null) return;
if (!spawnDic.TryGetValue(createFishData.fishNumber, out var cfg))
{
Debug.LogWarning($"[CreateFishManager] 找不到 spawnDic 配置: {createFishData.fishNumber}");
diff --git a/Assets/MyGame/Scripts/Net/APIManager.cs b/Assets/MyGame/Scripts/Net/APIManager.cs
index 57908dbfb..aa755d85b 100644
--- a/Assets/MyGame/Scripts/Net/APIManager.cs
+++ b/Assets/MyGame/Scripts/Net/APIManager.cs
@@ -1,6 +1,5 @@
using Assets.MyGame.Scripts.Net.API;
using Assets.MyGame.Scripts.Net.APIRequest;
-using Best.HTTP.SecureProtocol.Org.BouncyCastle.Ocsp;
using Newtonsoft.Json;
using System;
using System.Collections;
@@ -166,6 +165,7 @@ namespace Assets.MyGame.Scripts.Net
dic_API[APIType.Hall_GetChessRoomPartitionCount] = "/api/Hall/GetChessRoomPartitionCount";
dic_API[APIType.Hall_SendBroadcast] = "/api/Hall/SendBroadcast";
dic_API[APIType.Room_Assign] = "/api/Room/Assign";
+ dic_API[APIType.Hall_UseVipExp] = "/api/Hall/UseVipExp";
// ---------------- Customer ----------------
dic_API[APIType.Customer_Start] = "/customerapi/Customer/start";
diff --git a/Assets/MyGame/Scripts/Net/APIRequestList_Hall.cs b/Assets/MyGame/Scripts/Net/APIRequestList_Hall.cs
index 5fac6cd6a..846fed85c 100644
--- a/Assets/MyGame/Scripts/Net/APIRequestList_Hall.cs
+++ b/Assets/MyGame/Scripts/Net/APIRequestList_Hall.cs
@@ -320,6 +320,15 @@ namespace Assets.MyGame.Scripts.Net
}, callback);
}
+ ///
+ /// 使用VIP经验包(GET /api/Hall/UseVipExp)
+ /// 返回:string
+ ///
+ public static void UseVipExp(Action callback = null)
+ {
+ APIManager.Response(APIType.Hall_UseVipExp, callback);
+ }
+
#endregion
}
}
diff --git a/Assets/MyGame/Scripts/Net/APIResponses/Enum/ProductTypeEnum.cs b/Assets/MyGame/Scripts/Net/APIResponses/Enum/ProductTypeEnum.cs
index e6a1e914d..bde5ce47d 100644
--- a/Assets/MyGame/Scripts/Net/APIResponses/Enum/ProductTypeEnum.cs
+++ b/Assets/MyGame/Scripts/Net/APIResponses/Enum/ProductTypeEnum.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -22,6 +22,7 @@ namespace Assets.MyGame.Scripts.Net.APIResponses.Enum
大喇叭=9,
技能=10,
召唤卡=11,
+ VIP经验=12,
}
diff --git a/Assets/MyGame/Scripts/Net/APIType.cs b/Assets/MyGame/Scripts/Net/APIType.cs
index 7ddaa152c..4ac0d4cbe 100644
--- a/Assets/MyGame/Scripts/Net/APIType.cs
+++ b/Assets/MyGame/Scripts/Net/APIType.cs
@@ -342,6 +342,9 @@ namespace Assets.MyGame.Scripts.Net
/// 获取房间专属 socket 地址(GET /api/Room/Assign?roomId=)
Room_Assign,
+ /// 使用VIP经验包(GET /api/Hall/UseVipExp)
+ Hall_UseVipExp,
+
// ---------------- Customer ----------------
/// 发起会话(POST)
diff --git a/Assets/MyGame/Scripts/Net/WebResponses/RoomChangeResponse.cs b/Assets/MyGame/Scripts/Net/WebResponses/RoomChangeResponse.cs
index de962eba5..f19935b89 100644
--- a/Assets/MyGame/Scripts/Net/WebResponses/RoomChangeResponse.cs
+++ b/Assets/MyGame/Scripts/Net/WebResponses/RoomChangeResponse.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
///
@@ -119,4 +119,7 @@ public class RoomPlayerInfo
/// 玩家因子
///
public object playerFactor;
+
+ /// 是否为机器人(后端下发)
+ public bool isBot;
}
diff --git a/Assets/MyGame/Scripts/Net/WebResponses/RoomJoinNotice.cs b/Assets/MyGame/Scripts/Net/WebResponses/RoomJoinNotice.cs
index 1a06eb12a..c5f2b0720 100644
--- a/Assets/MyGame/Scripts/Net/WebResponses/RoomJoinNotice.cs
+++ b/Assets/MyGame/Scripts/Net/WebResponses/RoomJoinNotice.cs
@@ -1,4 +1,4 @@
-using Assets.MyGame.Scripts.Net.WebResponses;
+using Assets.MyGame.Scripts.Net.WebResponses;
using System;
using System.Collections.Generic;
namespace Assets.MyGame.Scripts.Net.WebResponses
@@ -93,5 +93,8 @@ namespace Assets.MyGame.Scripts.Net.WebResponses
/// 玩家状态(0 正常,其他状态可扩展)
///
public int state;
+
+ /// 是否为机器人(后端下发)
+ public bool isBot;
}
}
\ No newline at end of file
diff --git a/Assets/MyGame/Scripts/Net/WebSocketManager_Message.cs b/Assets/MyGame/Scripts/Net/WebSocketManager_Message.cs
index 5a124c5be..cf8c5aeb8 100644
--- a/Assets/MyGame/Scripts/Net/WebSocketManager_Message.cs
+++ b/Assets/MyGame/Scripts/Net/WebSocketManager_Message.cs
@@ -452,8 +452,10 @@ namespace Assets.MyGame.Scripts.Net
}
else
{
- // 房主是其他真人:本地停止出鱼,避免重复发包
- CreateFishManager.Instance.fangzhu = false;
+ // ⭐ 只在 userId 非空且确实是真人玩家时才停止本地出鱼
+ // userId 为空(异常情况)时保持原状,避免误关出鱼导致 bot 退出后不出鱼
+ if (!string.IsNullOrEmpty(userId))
+ CreateFishManager.Instance.fangzhu = false;
}
break;
case "Exception":
diff --git a/Assets/MyGame/Scripts/Turret/SitPosData.cs b/Assets/MyGame/Scripts/Turret/SitPosData.cs
index 7f1e76b24..b897b27f8 100644
--- a/Assets/MyGame/Scripts/Turret/SitPosData.cs
+++ b/Assets/MyGame/Scripts/Turret/SitPosData.cs
@@ -60,6 +60,15 @@ public class SitPosData : MonoBehaviour
public Transform tra_CurrTurret;
public float currentAngle;
+ ///
+ /// 炮台在世界坐标(Z=0平面)的位置缓存。
+ /// 因为炮台是 UGUI 元素,tra_CurrTurret.position 是 Canvas 坐标,不能直接和鱼的世界坐标比较距离。
+ /// 这里用和子弹起点(GetBulletStartWorldPos)相同的转换方式,把 UI 坐标转成世界坐标。
+ /// 由 LoadTurret 加载完炮台后计算并缓存,bot AI/锁定检测距离时用此字段。
+ ///
+ [System.NonSerialized]
+ public Vector3 turretWorldPos;
+
// -------------------------
// 锁定系统
// -------------------------
@@ -232,9 +241,47 @@ public class SitPosData : MonoBehaviour
// 绑定状态机入口
OnStateChange = currentTurret.ChangeState;
+ // ⭐ 缓存炮台在世界坐标(Z=0平面)的位置,bot AI/锁定检测距离时用
+ // 和子弹起点(GetBulletStartWorldPos)用相同的 UI→世界 转换方式
+ UpdateTurretWorldPos();
+
Debug.Log($"[SitPosData] 炮台已加载: #{turretNumber}");
}
+ ///
+ /// 把炮台 UI 坐标转换成世界坐标(Z=0平面)并缓存到 turretWorldPos。
+ /// 转换方式和 TurretData.GetBulletStartWorldPos 一致。
+ ///
+ public void UpdateTurretWorldPos()
+ {
+ if (tra_CurrTurret == null)
+ {
+ turretWorldPos = Vector3.zero;
+ return;
+ }
+
+ RectTransform rect = tra_CurrTurret.GetComponent();
+ if (rect != null && UIManager.Instance != null && UIManager.Instance.uiRoot != null)
+ {
+ Canvas canvas = UIManager.Instance.uiRoot.GetComponent