This commit is contained in:
2026-07-16 20:16:05 +08:00
parent d0902c6ba8
commit efce942e52
21 changed files with 1655 additions and 195 deletions
+13
View File
@@ -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 残留导致特效永生
/// <summary>重新播放所有粒子</summary>
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)
{
@@ -1059,6 +1059,8 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
public void CreateFish(SyncGameData syncGameData)
{
if (isFrozen) return; // 冰冻中不创建鱼
// ⭐ 场景卸载/CloseSpawnSystem 后 spawnDic 会变 null,此时收到的广播直接忽略
if (spawnDic == null) return;
var rotateData = FrameDataParser.Parse<FrameData_CreateFish>(syncGameData.frameData);
if (rotateData != null)
SpawnFishMainThread(rotateData, syncGameData.time);
@@ -1068,6 +1070,8 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
/// </summary>
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}");
+1 -1
View File
@@ -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";
@@ -320,6 +320,15 @@ namespace Assets.MyGame.Scripts.Net
}, callback);
}
/// <summary>
/// 使用VIP经验包(GET /api/Hall/UseVipExp)
/// 返回:string
/// </summary>
public static void UseVipExp(Action<string> callback = null)
{
APIManager.Response<string>(APIType.Hall_UseVipExp, callback);
}
#endregion
}
}
@@ -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,
}
+3
View File
@@ -342,6 +342,9 @@ namespace Assets.MyGame.Scripts.Net
/// <summary>获取房间专属 socket 地址(GET /api/Room/Assign?roomId=)</summary>
Room_Assign,
/// <summary>使用VIP经验包(GET /api/Hall/UseVipExp)</summary>
Hall_UseVipExp,
// ---------------- Customer ----------------
/// <summary>发起会话(POST)</summary>
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
/// <summary>
@@ -119,4 +119,7 @@ public class RoomPlayerInfo
/// 玩家因子
/// </summary>
public object playerFactor;
/// <summary>是否为机器人(后端下发)</summary>
public bool isBot;
}
@@ -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 正常,其他状态可扩展)
/// </summary>
public int state;
/// <summary>是否为机器人(后端下发)</summary>
public bool isBot;
}
}
@@ -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":
@@ -60,6 +60,15 @@ public class SitPosData : MonoBehaviour
public Transform tra_CurrTurret;
public float currentAngle;
/// <summary>
/// 炮台在世界坐标(Z=0平面)的位置缓存。
/// 因为炮台是 UGUI 元素,tra_CurrTurret.position 是 Canvas 坐标,不能直接和鱼的世界坐标比较距离。
/// 这里用和子弹起点(GetBulletStartWorldPos)相同的转换方式,把 UI 坐标转成世界坐标。
/// 由 LoadTurret 加载完炮台后计算并缓存,bot AI/锁定检测距离时用此字段。
/// </summary>
[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}");
}
/// <summary>
/// 把炮台 UI 坐标转换成世界坐标(Z=0平面)并缓存到 turretWorldPos。
/// 转换方式和 TurretData.GetBulletStartWorldPos 一致。
/// </summary>
public void UpdateTurretWorldPos()
{
if (tra_CurrTurret == null)
{
turretWorldPos = Vector3.zero;
return;
}
RectTransform rect = tra_CurrTurret.GetComponent<RectTransform>();
if (rect != null && UIManager.Instance != null && UIManager.Instance.uiRoot != null)
{
Canvas canvas = UIManager.Instance.uiRoot.GetComponent<Canvas>();
Camera cam = Camera.main;
Vector3 screenPos = RectTransformUtility.WorldToScreenPoint(
canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera,
rect.position
);
float distance = Mathf.Abs(cam.transform.position.z - 0f);
Vector3 worldPos = cam.ScreenToWorldPoint(new Vector3(screenPos.x, screenPos.y, distance));
worldPos.z = 0f;
turretWorldPos = worldPos;
}
else
{
// 非 UI(3D 物体)直接用 position
turretWorldPos = tra_CurrTurret.position;
}
}
/// <summary>根据炮台编号字符串加载。</summary>
public void LoadTurret(string turretNumberStr)
{
@@ -59,16 +59,6 @@
"attackAudioID": 0,
"bulletID": []
},
{
"turretNumber": 11007,
"turretName": "萌娃驾到",
"turretType": 1,
"prefabTurretResourcesPath": "paotai11007",
"animationType": 1,
"prefabType": 0,
"attackAudioID": 0,
"bulletID": []
},
{
"turretNumber": 11008,
"turretName": "创世之心",
@@ -229,16 +219,6 @@
"attackAudioID": 0,
"bulletID": []
},
{
"turretNumber": 11024,
"turretName": "醒狮纳吉1",
"turretType": 1,
"prefabTurretResourcesPath": "paotai11024",
"animationType": 1,
"prefabType": 0,
"attackAudioID": 0,
"bulletID": []
},
{
"turretNumber": 11025,
"turretName": "鸿兔先锋",
@@ -409,16 +389,6 @@
"attackAudioID": 0,
"bulletID": []
},
{
"turretNumber": 11042,
"turretName": "战魂之刃",
"turretType": 1,
"prefabTurretResourcesPath": "paotai11042",
"animationType": 1,
"prefabType": 0,
"attackAudioID": 0,
"bulletID": []
},
{
"turretNumber": 11043,
"turretName": "战神之怒",
@@ -439,16 +409,6 @@
"attackAudioID": 0,
"bulletID": []
},
{
"turretNumber": 11045,
"turretName": "裂魂之刃",
"turretType": 1,
"prefabTurretResourcesPath": "paotai11045",
"animationType": 1,
"prefabType": 0,
"attackAudioID": 0,
"bulletID": []
},
{
"turretNumber": 12001,
"turretName": "自由之翼",
@@ -111,7 +111,7 @@ PrefabInstance:
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
type: 3}
propertyPath: idleAnim
value: idle
value: idel
objectReference: {fileID: 0}
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
type: 3}
@@ -116,7 +116,7 @@ PrefabInstance:
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
type: 3}
propertyPath: attackAnim
value: attack_1
value: attack
objectReference: {fileID: 0}
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
type: 3}
@@ -116,7 +116,7 @@ PrefabInstance:
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
type: 3}
propertyPath: attackAnim
value: attack_1
value: attcak_1
objectReference: {fileID: 0}
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
type: 3}
@@ -116,7 +116,7 @@ PrefabInstance:
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
type: 3}
propertyPath: attackAnim
value: attack_1
value: attcak_1
objectReference: {fileID: 0}
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
type: 3}
@@ -116,7 +116,7 @@ PrefabInstance:
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
type: 3}
propertyPath: attackAnim
value: attack_1
value: attcak_1
objectReference: {fileID: 0}
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
type: 3}
@@ -217,10 +217,14 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
foreach (var item in GameManager.Instance.gameData.currRoomData.players)
{
JoinGame(item.position, item.gold, item.diamond, item.cannonMultiple, 0, item.userId, item.cannonId, item.wingsId);
// 后端 players 列表里 userId 以 "bot_" 开头的是机器人
// ⭐ 优先用后端 isBot 字段,兼容 userId.StartsWith("bot_")
var sit = list_Sits.Find(s => s.posID == item.position);
if (sit != null && !string.IsNullOrEmpty(item.userId) && item.userId.StartsWith("bot_"))
sit.isBot = true;
if (sit != null)
{
bool isBot = item.isBot ||
(!string.IsNullOrEmpty(item.userId) && item.userId.StartsWith("bot_"));
if (isBot) sit.isBot = true;
}
}
GameManager.Instance.gameData.roomJoinNotice = v.arguments[0];
isfirstShow = false;
@@ -249,12 +253,25 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
}
else
{
// 玩家加入 → 如果座位被bot占用则替换
var targetSit = list_Sits.Find(s => s.posID == roomJoinNotice.position);
if (targetSit != null && targetSit.isBot)
// ⭐ 优先用后端 isBot 字段判断,兼容 userId.StartsWith("bot_")
bool isBotJoin = roomJoinNotice.isBot ||
(!string.IsNullOrEmpty(roomJoinNotice.userId) && roomJoinNotice.userId.StartsWith("bot_"));
if (isBotJoin)
{
targetSit.isBot = false;
targetSit.ShowWaiting();
// bot 加入 → 标记 isBot=true (JoinGame 里会自动 SetActive(true) 座位)
if (targetSit != null)
targetSit.isBot = true;
}
else
{
// 真人玩家加入 → 如果座位被bot占用则替换
if (targetSit != null && targetSit.isBot)
{
targetSit.isBot = false;
targetSit.ShowWaiting();
}
}
JoinGame(roomJoinNotice.position, roomJoinNotice.gold, roomJoinNotice.diamond, roomJoinNotice.cannonMultiple, 0, roomJoinNotice.userId, roomJoinNotice.cannonId, roomJoinNotice.wingsId);
@@ -1171,6 +1188,81 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
#endregion
#region 锁定位置解析(统一工具)
/// <summary>
/// 统一锁定位置解析:colliderList 第一个活跃碰撞器 → lockPoint → monster.transform
/// 所有设置锁定位置的地方都应该调用此方法,避免不同地方用不同优先级导致锁定图标位置和子弹终点不一致。
/// 注意:Unity 序列化字段未赋值时是"假 null",?? 运算符无法识别,必须用 == null 判断。
/// </summary>
private Transform ResolveLockTransform(MonsterContral monster, out int colliderIndex)
{
colliderIndex = -1;
if (monster == null) return null;
// 1. 优先用 colliderList 中第一个活跃的碰撞器
if (monster.colliderList != null && monster.colliderList.Count > 0)
{
for (int i = 0; i < monster.colliderList.Count; i++)
{
var col = monster.colliderList[i];
if (col != null && col.gameObject.activeInHierarchy)
{
colliderIndex = i;
return col;
}
}
}
// 2. 其次用 lockPoint(锁定标志锚点,通常是屏幕中间/鱼身中心)
if (monster.lockPoint != null)
return monster.lockPoint;
// 3. 兜底用 monster.transform
return monster.transform;
}
/// <summary>简化重载,不关心 colliderIndex 时用</summary>
private Transform ResolveLockTransform(MonsterContral monster)
{
return ResolveLockTransform(monster, out _);
}
#endregion
#region 路径进度过滤(锁定范围控制)
/// <summary>bot/玩家锁定时,鱼路径进度下限:小于此值表示鱼刚出生,不锁定</summary>
private const float BotLockPathMin = 0.2f;
/// <summary>bot/玩家锁定时,鱼路径进度上限:大于此值表示鱼即将离场,不锁定</summary>
private const float BotLockPathMax = 0.8f;
/// <summary>
/// 计算鱼在路径上的进度 [0, 1]。
/// 进度 = moveTime / monsterData.moveLine.moveTime
/// 0 = 刚出生,1 = 即将离场
/// </summary>
private float GetFishPathProgress(MonsterContral m)
{
if (m == null || m.monsterData == null || m.monsterData.moveLine == null) return 1f;
float total = m.monsterData.moveLine.moveTime;
if (total <= 0f) return 1f;
return Mathf.Clamp01(m.moveTime / total);
}
/// <summary>
/// 判断鱼的路径进度是否在可锁定范围内 [BotLockPathMin, BotLockPathMax]
/// 刚出生(< 0.2)或即将离场(> 0.8)的鱼不能锁定,避免锁定后立即丢失目标
/// </summary>
private bool IsFishPathLockable(MonsterContral m)
{
if (m == null) return false;
float progress = GetFishPathProgress(m);
return progress >= BotLockPathMin && progress <= BotLockPathMax;
}
#endregion
#region 多目标查找(TripleLock)
@@ -1229,15 +1321,11 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
MonsterContral m = kv.Value;
if (m == null || !m.gameObject.activeInHierarchy) continue;
if (selected.Contains(m.guid)) continue; // 跳过已选中的
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的鱼不作为目标
if (!IsFishPathLockable(m)) continue;
Transform pos = m.transform;
if (m.colliderList != null && m.colliderList.Count > 0)
{
foreach (var col in m.colliderList)
{
if (col.gameObject.activeInHierarchy) { pos = col; break; }
}
}
// ⭐ 统一用 ResolveLockTransform:colliderList → lockPoint → transform
Transform pos = ResolveLockTransform(m);
result.Add((m, pos));
selected.Add(m.guid);
@@ -1356,17 +1444,11 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
// 处理选中的最佳命中
MonsterContral monster = bestHit.collider.GetComponentInParent<MonsterContral>();
// ⭐ 两个判断:lockPoint(屏幕中间/锁定锚点) 优先 → colliderList 中命中的碰撞器 → 兜底命中碰撞器
// 不用 fish.gameObject.position;与 bot 锁定逻辑保持一致
Transform lockTr;
if (monster.lockPoint != null)
lockTr = monster.lockPoint;
else if (monster.colliderList != null && monster.colliderList.Count > 0)
lockTr = bestHit.collider.transform; // 命中的碰撞器(在 colliderList 内)
else
lockTr = bestHit.collider.transform; // 兜底
// ⭐ 统一用 ResolveLockTransform:colliderList 第一个活跃 → lockPoint → transform
// 返回的 colliderIndex 用于网络同步,其他客户端按此索引取同一碰撞器
int colliderIndex;
Transform lockTr = ResolveLockTransform(monster, out colliderIndex);
currSitData.SetLockTarget(monster, lockTr);
int colliderIndex = (monster.colliderList != null) ? monster.colliderList.IndexOf(bestHit.collider.transform) : -1;
SyncLock(new List<string> { monster?.guid }, colliderIndex);
}
private void UpdateLockFX()
@@ -1397,35 +1479,15 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
if (!hitPos.gameObject.activeInHierarchy)
{
if (monster.colliderList != null && monster.colliderList.Count > 0)
{
bool found = false;
foreach (Transform t in monster.colliderList)
{
if (t.gameObject.activeInHierarchy)
{
pos = t.position;
found = true;
break;
}
}
if (!found)
{
if (icon.activeSelf) icon.SetActive(false);
return;
}
}
else
{
// 注意:Unity 序列化字段未赋值时是"假 null",?? 运算符无法识别,必须用 == null
var lp = (monster.lockPoint != null) ? monster.lockPoint : monster.transform;
pos = lp.position;
}
if (!hitPos.gameObject.activeInHierarchy)
// ⭐ 原始 hitPos 失效 → 用 ResolveLockTransform 重新找一个有效位置
// 统一逻辑:colliderList 第一个活跃 → lockPoint → transform
Transform fallback = ResolveLockTransform(monster);
if (fallback == null || !fallback.gameObject.activeInHierarchy)
{
if (icon.activeSelf) icon.SetActive(false);
return;
}
pos = fallback.position;
}
else
{
@@ -1472,30 +1534,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
MonsterContral target = FindAutoLockTarget();
if (target != null)
{
// ⭐ 锁定位置与 HandleLock 一致:lockPoint 优先 → colliderList 第一个活跃碰撞器 → target.transform
Transform lockTr;
int colliderIndex = -1;
if (target.lockPoint != null)
{
lockTr = target.lockPoint;
}
else if (target.colliderList != null && target.colliderList.Count > 0)
{
lockTr = target.transform;
for (int i = 0; i < target.colliderList.Count; i++)
{
if (target.colliderList[i].gameObject.activeInHierarchy)
{
lockTr = target.colliderList[i];
colliderIndex = i;
break;
}
}
}
else
{
lockTr = target.transform;
}
// ⭐ 统一用 ResolveLockTransform:colliderList 第一个活跃 → lockPoint → transform
int colliderIndex;
Transform lockTr = ResolveLockTransform(target, out colliderIndex);
currSitData.SetLockTarget(target, lockTr);
SyncLock(new List<string> { target?.guid }, colliderIndex);
}
@@ -1517,12 +1558,15 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
MonsterContral best = null;
float bestSqr = float.MaxValue;
Vector3 origin = currSitData.tra_CurrTurret.position;
// ⭐ 用世界坐标(Z=0平面),和 bot AI 一致,否则距离计算会异常(出现 3.4E+38 之类)
Vector3 origin = currSitData.turretWorldPos;
foreach (var kv in CreateFishManager.Instance.fishDatas_hashCodekey)
{
MonsterContral m = kv.Value;
if (m == null || !m.gameObject.activeInHierarchy) continue;
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的鱼不作为自动锁定目标
if (!IsFishPathLockable(m)) continue;
float sqr = (m.transform.position - origin).sqrMagnitude;
if (sqr < bestSqr)
@@ -1840,15 +1884,11 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
}
MonsterContral monster = CreateFishManager.Instance.fishDatas_hashCodekey[data.targetGuid[0]];
Transform hitPos;
// ⭐ 与本地 HandleLock 保持一致:lockPoint(屏幕中间) 优先 → colliderList[colliderIndex] → fish.transform
if (monster.lockPoint != null)
hitPos = monster.lockPoint;
else if (monster.colliderList != null && monster.colliderList.Count > 0 && data.colliderIndex != -1)
hitPos = monster.colliderList[data.colliderIndex];
else
hitPos = monster.transform;
// ⭐ 统一用 ResolveLockTransform:colliderList 第一个活跃 → lockPoint → transform
// 注:网络同步过来的 colliderIndex 在统一逻辑下不再使用,因为客户端之间
// colliderList 顺序可能不一致,统一用"第一个活跃"更稳定
Transform hitPos = ResolveLockTransform(monster);
list_Sits[sync.sitPos].SetLockTarget(monster, hitPos);
}
@@ -2004,6 +2044,10 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
{
if (sit.posID != sitNum) continue;
// ⭐ 确保座位 GameObject 本身是激活的(可能被 HandleBotExit 隐藏或初始为 inactive)
if (!sit.gameObject.activeSelf)
sit.gameObject.SetActive(true);
sit.user = user ?? GameManager.Instance.user.id;
sit.goldNum = goldNum;
sit.consumeGoldNum = consumeGold;
@@ -2074,7 +2118,7 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
// ====== 退出检查 ======
if (CheckBotExit(sit, state, cfg)) continue;
Vector3 turretPos = sit.tra_CurrTurret.position;
Vector3 turretPos = sit.turretWorldPos;
// ====== 技能刷新(底层随机技能) ======
if (cfg != null && cfg.skills != null)
@@ -2101,10 +2145,8 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
{
sit.isLock = true;
sit.monCotl_Lock = target;
// ⭐ 锁定位置优先用 lockPoint(锁定标志锚点),与玩家锁定保持一致
sit.monCotl_Lock_Position = (target.lockPoint != null) ? target.lockPoint
: (target.colliderList != null && target.colliderList.Count > 0
? target.colliderList[0].transform : target.transform);
// ⭐ 统一用 ResolveLockTransform:colliderList 第一个活跃 → lockPoint → transform
sit.monCotl_Lock_Position = ResolveLockTransform(target);
if (sit.lockFX != null) sit.lockFX.SetActive(true);
}
}
@@ -2195,6 +2237,8 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
{
var m = kv.Value;
if (m == null || !m.gameObject.activeInHierarchy) continue;
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的 Boss 不作为目标
if (!IsFishPathLockable(m)) continue;
int ft = (int)m.fishenum;
bool isBoss = false;
@@ -2312,7 +2356,7 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
return true;
}
/// <summary>处理 bot 退出:发离房消息、座位清理</summary>
/// <summary>处理 bot 退出:发离房消息、座位清理、转移房主、保证出鱼</summary>
private void HandleBotExit(SitPosData sit, BotRuntimeState state, BotConfig cfg)
{
state.hasExited = true;
@@ -2337,6 +2381,27 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
botLastAngle.Remove(sit.user);
botLastFireData.Remove(sit.user);
// ⭐ bot 退出后转移房主,否则房主是已退出的 bot 会导致出鱼停止
// 1) 清除 bot 的房主引用(如果它是房主)
// 2) TransferRoomOwner 会把房主交给第一个活跃的非 bot 玩家
if (SitPosData.roomOwnerSit == sit)
{
SitPosData.roomOwnerSit = null;
SitPosData.TransferRoomOwner();
}
// ⭐ 强制保证本地玩家能继续出鱼:
// bot 退出后如果本地玩家是新的房主(或房主为空但本地玩家在场),开启 fangzhu
// 这是对 ChangeControllerUser 网络消息的兜底,防止网络消息丢失或时序问题导致不出鱼
if (CreateFishManager.Instance != null)
{
bool localIsOwner = SitPosData.roomOwnerSit != null &&
SitPosData.roomOwnerSit.user == GameManager.Instance.user.id;
bool noOwner = SitPosData.roomOwnerSit == null;
if (localIsOwner || noOwner)
CreateFishManager.Instance.fangzhu = true;
}
if (cfg != null && cfg.exitCondition != null && cfg.exitCondition.showWaitingAfterExit)
sit.ShowWaiting();
else
@@ -2399,15 +2464,30 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
if (fishList.fishDatas_hashCodekey.TryGetValue(guid, out var m)
&& m != null && m.gameObject.activeInHierarchy)
{
// 锁定时不检查 inFront,始终跟踪目标
if (isLocked) return m;
Vector3 fp = m.transform.position;
bool inFront = sit.IsFlipped ? fp.y < turretPos.y : fp.y > turretPos.y;
if (inFront) return m;
// ⭐ 路径进度过滤:如果记忆目标已经超出可锁定范围(即将离场),清除记忆重新找
if (!IsFishPathLockable(m))
{
botTargetGuid.Remove(sit.posID);
}
else if (isLocked)
{
// 锁定时不检查 inFront,始终跟踪目标
return m;
}
else
{
Vector3 fp = m.transform.position;
bool inFront = sit.IsFlipped ? fp.y < turretPos.y : fp.y > turretPos.y;
if (inFront) return m;
// 不在前方 → 清除记忆
botTargetGuid.Remove(sit.posID);
}
}
else
{
// 目标无效 → 清除记忆
botTargetGuid.Remove(sit.posID);
}
// 目标无效 → 清除记忆
botTargetGuid.Remove(sit.posID);
}
// 2. 锁定Boss模式 → 优先最大倍率Boss
@@ -2468,6 +2548,8 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
{
var m = kv.Value;
if (m == null || !m.gameObject.activeInHierarchy) continue;
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的鱼不作为目标
if (!IsFishPathLockable(m)) continue;
Vector3 fishPos = m.transform.position;
if (sit.IsFlipped && fishPos.y >= turretPos.y) continue;
@@ -2505,13 +2587,16 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
var result = new List<MonsterContral>();
if (CreateFishManager.Instance == null) return result;
var fishList = CreateFishManager.Instance;
Vector3 turretPos = sit.tra_CurrTurret.position;
// ⭐ 用世界坐标(Z=0平面),和 bot AI / 玩家自动锁定一致
Vector3 turretPos = sit.turretWorldPos;
foreach (var kv in fishList.fishDatas_hashCodekey)
{
var m = kv.Value;
if (m == null || !m.gameObject.activeInHierarchy) continue;
if (m == exclude) continue;
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的鱼不作为目标
if (!IsFishPathLockable(m)) continue;
Vector3 fishPos = m.transform.position;
if (sit.IsFlipped && fishPos.y >= turretPos.y) continue;
@@ -23,6 +23,7 @@ public enum PropEnum
大喇叭 = 9,
技能 = 10,
召唤卡 = 11,
VIP经验 = 12,
}
/// <summary>
@@ -196105,9 +196105,9 @@ RectTransform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 1
m_Children:
- {fileID: 7752753625073944563}
- {fileID: 7718285068269790146}
- {fileID: 1106327632507529043}
- {fileID: 7752753625073944563}
m_Father: {fileID: 408568649757843747}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
@@ -201581,6 +201581,21 @@ PrefabInstance:
propertyPath: m_AnchoredPosition.y
value: -532.66003
objectReference: {fileID: 0}
- target: {fileID: 1465445835306222036, guid: 9351da778e1c18c4f9243b73d7569c66,
type: 3}
propertyPath: m_Spacing.x
value: 35
objectReference: {fileID: 0}
- target: {fileID: 1465445835306222036, guid: 9351da778e1c18c4f9243b73d7569c66,
type: 3}
propertyPath: m_Padding.m_Top
value: 20
objectReference: {fileID: 0}
- target: {fileID: 1465445835306222036, guid: 9351da778e1c18c4f9243b73d7569c66,
type: 3}
propertyPath: m_Padding.m_Left
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1987338642318344794, guid: 9351da778e1c18c4f9243b73d7569c66,
type: 3}
propertyPath: m_AnchorMax.y
File diff suppressed because it is too large Load Diff
@@ -217,6 +217,20 @@ public class UIHome_BY_BackPackPanel : SubUIBase
break;
case PropEnum.技能: break;
case PropEnum.召唤卡: break;
case PropEnum.VIP经验:
UIManager.Instance.ShowTiShi($"确定要使用VIP经验包吗?", () =>
{
APIRequestList.UseVipExp((result) =>
{
UIManager.Instance.ShowBoardError("使用成功");
// 刷新用户数据
APIRequestList.GetUser(GameManager.Instance.user.id, (user) =>
{
GameManager.Instance.UpdataUserData(user);
});
});
});
break;
default: break;
}
});