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
@@ -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;