This commit is contained in:
2026-10-11 16:51:07 +08:00
parent 65ccfce49e
commit 32f669ec6a
40 changed files with 820 additions and 88 deletions
@@ -258,6 +258,12 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
[Header("攻击参数")]
public float attackTime = 0.5f;
[Tooltip("远端座位攻击状态看门狗(秒):最后收到某座位的攻击状态/开火消息后,超过这个时间还没有新消息," +
"就判定这个座位已经不在攻击(典型:攻击中直接退出/掉线,永远不会再发待机包)→ 本地收回待机。\n" +
"实际等待时间 = max(本值, attackTime×3),避免开火间隔比看门狗还长的配置被误判")]
public float remoteAttackIdleTimeout = 1.5f;
/// <summary>远端座位最近一次"还在攻击"的心跳时间:收到该座位的攻击状态广播或开火消息时刷新</summary>
private readonly Dictionary<int, float> remoteAttackHeartbeat = new Dictionary<int, float>();
[Header("人机参数")]
public float botRotateSpeed = 80f; // 兜底旋转速度(BotConfig 为空时用)
float clickTime = 0;
@@ -1197,6 +1203,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
UpdateLockFX(); // 锁定图标跟随目标
RefreshTripleLockIcons();
// ⭐ 远端攻击状态看门狗:对方攻击中退出/掉线时不会再发待机包,本地按"还有没有攻击心跳"收回待机
UpdateRemoteTurretAttackTimeout();
LoopLaser();
SettingClose();
DaoJiShi();
@@ -3029,6 +3038,10 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
CrossPlatformInput input = GetInput();
// ⭐ 记录"开火键是否按住":手动开火是"按下进 Attack、松手回 Idle",
// 自动开火回收状态时要靠它区分"自动开火没开火"和"玩家正按住手动开火"(见 EnsureTurretIdle)
_firePointerDown = input.isPressed;
if (!input.isPressed && !input.wasPressedThisFrame && !input.wasReleasedThisFrame)
return;
if (input.wasPressedThisFrame)
@@ -3518,6 +3531,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
if (sit == null || bullitIDs == null || bullitIDs.Count == 0)
{
Debug.LogWarning("[FireBullet] sit 或 bullitIDs 为空,跳过本次开火");
// ⭐ 服务端这一发没回子弹(拒绝/异常)→ 本地不能再停在 Attack 空转:
// 攻击状态是开火请求发出前就切好的,不回滚就会一直循环攻击动画但一颗子弹都没有
if (sit == currSitData) EnsureTurretIdle();
return;
}
@@ -3785,6 +3801,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
bool needAutoFire = currSitData.IsAutoAttackOn || (toggle_Lock.isOn && currSitData.isLock && currSitData.lockFX.gameObject.activeSelf);
if (!needAutoFire)
{
// ⭐ 自动开火条件不再满足(关自动攻击/松开锁定目标):把还卡在 Attack 的炮台收回 Idle,
// 否则攻击动画一直循环但一颗子弹都不发射
EnsureTurretIdle();
autoFireTimer = 0f;
return;
}
@@ -3795,11 +3814,15 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
// ⭐ 本地金币校验:没钱直接返回,不切换攻击状态、不发送开炮请求
// autoFireTimer 保持满,金币恢复后立即开火
if (!HasEnoughGoldToFire(toggle_Three.isOn, toggle_Shuang != null && toggle_Shuang.isOn))
{
EnsureTurretIdle(); // 打不出去就别停在攻击状态
return;
}
// ⭐ 子弹上限校验(先于状态切换和扣费)
if (IsBulletLimitReached(currSitData.user))
{
EnsureTurretIdle(); // 子弹已满打不出去,回收攻击状态(金币恢复/上限解除后下一轮会重新进 Attack)
autoFireTimer = 0f; // 重置计时,下次再试
return;
}
@@ -3819,6 +3842,85 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
SyncFire(angle, isSanShe, isDouble, isLock);
}
/// <summary>
/// ⭐ 打不出去时把炮台从 Attack 收回 Idle(只处理 Attack,不碰 LaserStart/LaserLoop)。
/// 背景:攻击状态是在"按下/自动开火"那一刻就切好的(也要先把状态广播出去给其他客户端),
/// 但真正出弹要等服务端 RoomFireNotice 回来。中间任何一环被跳过——自动开火条件不再满足、
/// 金币不足、子弹上限、服务端没回子弹——以前都只 return 不收回状态,炮台就停在 Attack:
/// 攻击动画一直循环、一颗子弹都不发射(远端视角同样跟着卡住,因为状态是同步过去的)。
/// 🔁 只在"当前不是 Idle"时切一次:ChangeState 同状态早退 + 这里的判断,保证不会每帧刷广播。
/// </summary>
private void EnsureTurretIdle()
{
if (currSitData == null) return;
// ⚠ 玩家正按住手动开火时不能抢:手动开火的状态是"按下进 Attack、松手才回 Idle",
// 此时自动开火这一帧不开火(needAutoFire=false)不代表炮台不该是攻击状态。
// 少了这道判断会在按下同一帧把手动 Attack 顶回 Idle → 表现为"根本切不到攻击状态"。
if (IsManualFireActive) return;
// 状态由 OnStateChange 指向的那个 TurretData 接收:战神装备中指向战神(见 SitPosData.LoadZhanShen)
var turret = currSitData.currentZhanShenData != null ? currSitData.currentZhanShenData : currSitData.currentTurret;
if (turret == null || turret.CurrState != TurretState.Attack) return;
currSitData.ChangeState(TurretState.Idle);
SyncTurretState(TurretState.Idle);
}
/// <summary>开火键是否按住(HandleInput 每帧刷新)</summary>
private bool _firePointerDown;
/// <summary>
/// ⭐ 远端攻击状态看门狗:别的座位停在 Attack 上时,靠"这个座位还在不在发攻击消息"判断它是不是真的在攻击。
/// 为什么会卡:攻击状态是同步过来的,而"收回待机"也要等对方发待机包(changeTurretState=Idle)。
/// 对方攻击中直接退出/掉线/被踢 → 既不会再攻击也不会再发待机包 → 本端炮台永远停在攻击动画。
/// 这里按身份判定:开着战神时状态由战神接收(同 EnsureTurretIdle)。
/// 🔁 只改本地表现,不再广播回去(否则多端会互相刷 Idle 包)。
/// </summary>
private void UpdateRemoteTurretAttackTimeout()
{
if (list_Sits == null || list_Sits.Count == 0) return;
float now = Time.time;
// 等待时间至少是开火间隔的 3 倍:配置把攻击间隔调大时看门狗不能比它短,否则会误收回正在攻击的座位
float timeout = Mathf.Max(remoteAttackIdleTimeout, attackTime * 3f);
foreach (var sit in list_Sits)
{
if (sit == null || sit == currSitData) continue; // 本地座位由本地开火/回收逻辑管
var turret = sit.currentZhanShenData != null ? sit.currentZhanShenData : sit.currentTurret;
if (turret == null || turret.CurrState != TurretState.Attack) continue;
float last;
if (!remoteAttackHeartbeat.TryGetValue(sit.posID, out last))
{
// 没收到过心跳(进房间前的残留状态等)→ 给一个完整等待窗口,不立刻收回
remoteAttackHeartbeat[sit.posID] = now;
continue;
}
if (now - last <= timeout) continue;
// 超过等待时间没有任何"还在攻击"的消息 → 这个座位已经不在攻击了,收回待机
sit.ChangeState(TurretState.Idle);
}
}
/// <summary>
/// 手动开火是否正在生效:手动开火只在"自动攻击关 + 锁定关"时才工作
/// (TryFire 开头被这两个开关提前 return 拦掉,锁定模式下手动按住只旋转不开火),
/// 此时 Attack 状态完全由按下/按住驱动,回收逻辑必须让位。
/// </summary>
private bool IsManualFireActive
{
get
{
if (!_firePointerDown) return false;
if (currSitData != null && currSitData.IsAutoAttackOn) return false;
return toggle_Lock == null || !toggle_Lock.isOn;
}
}
@@ -5095,6 +5197,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
SitPosData sit = list_Sits.Find(s => s.user == sync.userId);
if (sit == null) return;
// ⭐ 攻击心跳:这个座位刚发了开火消息(bot 的状态广播是节流的,只有开火消息每发都来)
remoteAttackHeartbeat[sit.posID] = Time.time;
// ⭐ 座位镜像:botLastAngle/botLastFireData 缓存的是"源角度"(下方座位系);
// 真人兜底用 sit.currentAngle(本端世界角度,无需转换)
bool angleFromBotMemory = botLastAngle.TryGetValue(sync.userId, out float a);
@@ -5135,6 +5240,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
}
var targetSit = list_Sits[dispPos];
// ⭐ 攻击心跳:这个座位刚发了开火消息(真人每发都走这里,等于"还在攻击"的心跳)
remoteAttackHeartbeat[targetSit.posID] = Time.time;
// ⭐⭐ 战神散射(三发+锁定):发送端每发都带 isSanShe/isSuoDing 和3个激光目标GUID(monsterIDs)
// → 其他玩家的座位跟着补齐3个锁位 + 刷新散射标记:3条激光 + 2个散射起点球
// (JGFXLeftParent/JGFXRightParent) 一起显示。
@@ -5276,6 +5384,11 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
var data = FrameDataParser.Parse<FrameData_TurretState>(sync.frameData);
var sit = list_Sits[dispPos];
// ⭐ 攻击心跳:真人每次开火都会广播一次 Attack 状态(SyncTurretState(Attack) 在开火循环里),
// 等于"这个座位还在攻击"的心跳。刷新它,看门狗就不会误收回正在攻击的座位。
if (data.state == TurretState.Attack)
remoteAttackHeartbeat[sit.posID] = Time.time;
sit.ChangeState(data.state);
}
@@ -6115,6 +6228,7 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
botSkills.Remove(sit.posID);
botLastAngle.Remove(sit.user);
botLastFireData.Remove(sit.user);
remoteAttackHeartbeat.Remove(sit.posID); // 攻击心跳(看门狗用)
// ⭐ bot 退出后转移房主,否则房主是已退出的 bot 会导致房主显示异常
// 1) 清除 bot 的房主引用(如果它是房主)