/*
======================== GameMainPanel_FishBase ========================
【主控制层职责】
负责:输入 → 旋转 → 攻击 → 子弹 → 锁定 → 网络同步 → 广播处理
不负责:动画(TurretData)、发射点(TurretData)、金币(SitPosData)
=======================================================================
① InputModule(输入系统)
├── HandleInput() 主入口
├── 按下(PressDown)
├── 长按(PressHold)
├── 松开(PressUp)
├── CheckTouchIsChangeTurret() 判断是否点到“换炮台按钮”
└── HandleChangeTurretHold() 长按换炮台按钮 → 打开换炮台面板
-----------------------------------------------------------------------
② RotateModule(旋转系统)
├── HandleRotate() 手动旋转入口
├── RotateToTouch() 朝触摸点旋转
├── RotateToLockTarget() 朝锁定目标旋转
├── HandleAutoRotate() 自动开火时的自动旋转
└── AutoSweepRotate() 自动扫射(左右摆动)
-----------------------------------------------------------------------
③ FireModule(攻击系统)
├── TryFire() 长按触发攻击(冷却计时)
├── FireBullet() 发射子弹(普通/三射/双倍/锁定)
├── StartLaser() 激光开始(LaserStart)
├── LoopLaser() 激光持续(LaserLoop)
└── StopLaser() 激光结束(LaserEnd)
-----------------------------------------------------------------------
④ BulletModule(子弹系统)
├── CreateBullet() 创建子弹实例
-----------------------------------------------------------------------
⑤ LockModule(锁定系统)
├── HandleLock() 点击锁定
├── 自动锁定(AutoLock) 可选扩展
├── CancelLock() 取消锁定
├── UpdateLockFX() 锁定特效跟随目标
└── SyncLock() 网络同步锁定状态
-----------------------------------------------------------------------
⑥ NetSyncModule(网络同步)
├── SyncRotate() 同步旋转
├── SyncFire() 同步发射
├── SyncLock() 同步锁定
├── SyncTurretState() 同步激光状态
└── SyncChangeTurret() 同步炮台切换(可选)
-----------------------------------------------------------------------
⑦ NetReceiveModule(服务器广播处理)
├── SyncGameData_Rotate() 广播旋转
├── SyncGameData_FireBullet() 广播发射
├── SyncGameData_ChangeTarget() 广播锁定
├── SyncGameData_ChangeTurret() 广播炮台切换(可选)
└── SyncGameData_LaserState() 广播激光状态(可选)
-----------------------------------------------------------------------
⑧ ResourceModule(资源加载)
├── LoadResources() 加载音频/鱼资源/路线
└── UnLoadResources() 卸载资源
-----------------------------------------------------------------------
⑨ PlayerJoinModule(玩家加入/退出)
├── JoinGame() 玩家加入房间
└── ExitGame() 玩家退出房间
=======================================================================
*/
using Assets.MyGame.Scripts.Net;
using Assets.MyGame.Scripts.Net.API;
using Assets.MyGame.Scripts.Net.API.UserData_SubClass;
using Assets.MyGame.Scripts.Net.APIResponses.Enum;
using Assets.MyGame.Scripts.Net.WebResponses;
using DG.Tweening;
using Newtonsoft.Json;
using Spine.Unity;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using TMPro;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.Playables;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using Random = UnityEngine.Random;
public class GameMainPanel_FishBase : GameMainPanel_Base
{
[Header("离开游戏倒计时")]
public GameObject obj_DJSPanel;
public TextMeshProUGUI tmp_DJS; //确认(10)
public Button btn_QueRen;
public Button btn_QuXiao;
/// 是否正在退出倒计时(期间屏蔽帧同步和爆奖特效)
private bool _isLeavingCountdown = false;
private Coroutine _leaveCountdownCoroutine;
[Header("游戏场景")]
public string sceneName;
public GameTypeEnum gameType;
[Header("设置")]
protected bool isOpenSetting;
public Button btn_SettingList;
public Transform SittingListParent;
public Button btn_TuJian;
public Button btn_ChangeTurret;
public Button btn_TuiChu;
public Button btn_Setting;
//[Header("我在这")]
public GameObject mySitAnim;
public Dictionary dic_Skill_Toggle = new Dictionary();//开关型技能
[Header("Skillbottom")]
public Toggle toggle_SkillAuto;
public Toggle toggle_Lock;
public Toggle toggle_Three;
public Toggle toggle_Shuang;
public Toggle toggle_BaoZou;
public TextMeshProUGUI tmp_BaoZouCount;
public Image img_BaoZou;
float frenzyDuration = 5f; // 疯狂状态持续时间
public float currFrenzyTime = 0f;
public Toggle toggle_BingFeng;
public TextMeshProUGUI tmp_BingFengCount;
public Image img_BingFeng;
public GameObject bingFengEffect; // 冰封特效预制体(首次Instantiate,之后激活/隐藏)
private GameObject _bingFengInstance; // 克隆出来的实例
float bingFengDuration = 15f; // 冰封状态持续时间(服务端 FishFrozen 延长 15 秒,客户端写死 15s 倒计时)
public float currBingFengTime = 0f;
private Dictionary _frozenAnimSpeeds = new Dictionary();
// ⭐ 暴走/冰封本地数量:因为暂无扣减 API,激活时本地 -1,避免被 OnUserDataChanged 覆盖回服务端旧值
private int _localBaoZouCount = -1; // -1 表示尚未初始化
private int _localBingFengCount = -1;
// 上次从服务端读到的数量:用于判断服务端是否变化(刮刮卡奖励/其他端使用),
// 服务端值变化时信任服务端;未变化时保留本地扣减值
private int _lastServerBaoZouCount = -1;
private int _lastServerBingFengCount = -1;
[Header("Skillleft")]
public Button btn_GuaGuaKa;
public TextMeshProUGUI tmp_GGKCount;
public List list_Sits = new List();
public SitPosData currSitData;
[Header("攻击参数")]
public float attackTime = 0.5f;
[Header("人机参数")]
public float botRotateSpeed = 80f; // 兜底旋转速度(BotConfig 为空时用)
float clickTime = 0;
private Dictionary botFireTimer = new Dictionary();
private Dictionary botLastAngle = new Dictionary();
// bot 旋转广播节流:记录上次广播的角度/时间
private Dictionary botLastSyncAngle = new Dictionary();
private Dictionary botLastSyncTime = new Dictionary();
// bot 技能运行时
private Dictionary botSkills = new Dictionary();
// bot 目标记忆
private Dictionary botTargetGuid = new Dictionary();
// bot 锁定同步节流:记录上次广播的锁定目标 guid(null 表示未锁定),只有目标变化才广播
private Dictionary botLastSyncLockGuid = new Dictionary();
// bot 炮台状态同步节流:记录上次广播的 TurretState,只有状态变化才广播
private Dictionary botLastSyncTurretState = new Dictionary();
// bot 状态计时:开火后持续 Attack 动画,空闲 0.3s 切回 Idle
private Dictionary botIdleTimer = new Dictionary();
// bot 最近的 FrameData_FireBullet(给其他客户端还原技能状态)
private Dictionary botLastFireData = new Dictionary();
// bot 运行时状态(进场金币/倍数档位/行为模式/退出)
private Dictionary botRuntimeStates = new Dictionary();
public GameObject bullet_Test;
public GameObject bulletParent;
float changeTurretTime = 0f;
bool isChangingTurret = false;
// XJMH 长按炮台扇形切换
[Header("XJMH 炮台长按切换")]
[Tooltip("长按炮台触发扇形切换的阈值(秒)")]
public float turretLongPressThreshold = 0.5f;
/// 是否正在等待长按炮台判定(按下炮台但尚未超过阈值)
protected bool _isPendingTurretLongPress = false;
/// 炮台扇形切换UI是否已显示
protected bool _isTurretSwitcherActive = false;
// ⭐ 起飞炮台(FlyTurretData):轻点空白处触发起飞
// 按下时先走正常开火流程,松开时判定为"轻点"(时长短+几乎没移动)才起飞,
// 保证按住扫射的普通开火完全不受影响
private bool _flyTapPending = false; // 本次按下待判定轻点
private float _flyTapTime; // 按下时刻
private Vector2 _flyTapPos; // 按下位置
private const float flyTapMaxDuration = 0.25f; // 轻点判定:按下到松开最大时长(秒)
private const float flyTapMaxMove = 40f; // 轻点判定:最大移动距离(像素)
int bulletCount = 0;
public GameObject boardCast_Boss;
[Header("创建视频播放器")]
[Tooltip("鱼创建时播放的入场视频控件,由 Boss 预制体的 createVideoClip 指定视频片段。Inspector 拖拽场景中的 VideoPlayer 控件")]
public UnityEngine.Video.VideoPlayer createVideoPlayer;
AudioClip bulletClip;
public List paoBeiList = new List();
public ProgressBar JinDuTiao;
#region UI
bool isfirstShow = true;
protected override void OnShow(object args = null)
{
base.OnShow(args);
LoadResources();
RefreshGGKCount();
//UIManager.Instance.ShowBoard("太好看了");
var v = JsonConvert.DeserializeObject>(args.ToString());
if (v.arguments.Count > 0)
{
RoomJoinNotice roomJoinNotice = v.arguments[0];
// ⭐ paoBeiList 每次进入房间都重新根据当前场次+房间等级获取,
// 避免再次进入同一场次的不同等级房间时沿用旧 paoBeiList(例如先高级场后低级场)
{
int t = GameManager.Instance.gameData.currRoomData.gameType;
int l = GameManager.Instance.gameData.currRoomData.roomLevel;
foreach (var item in GameManager.Instance.gameConfig.roomConfigs)
{
if (item.gameType == t && l == item.roomLevel)
{
paoBeiList = item.cannonMultipleList;
}
}
}
if (isfirstShow)
{
string roomId = GameManager.Instance.gameData.currRoomData.roomId;
//string roomId = GameManager.Instance.gameData.currRoomData;
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, item.warGodId);
// ⭐ 优先用后端 isBot 字段,兼容 userId.StartsWith("bot_")
var sit = list_Sits.Find(s => s.posID == item.position);
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;
foreach (var item in list_Sits)
{
if (item.posID == roomJoinNotice.position)
{
currSitData = item;
// ⭐ 长按本地座位的 img_OpenPT 开关炮台面板(与 btn_ChangeTurret 同逻辑)
HookOpenPTLongPress(currSitData);
}
else
{
item.btn_TurretAdd.gameObject.SetActive(false);
item.btn_TurretMinus.gameObject.SetActive(false);
}
}
//// currSitData.ChangeTurret(1); — 已由 SitPosData.OnEnable 自动加载
if (currSitData.currentTurret != null)
SyncChangeTurret(currSitData.currentTurret.turretNumber);
currSitData.lockFX.transform.GetChild(0).gameObject.SetActive(false);
currSitData.lockFX.transform.GetChild(1).gameObject.SetActive(true);
if (mySitAnim != null && currSitData.turretParent != null)
mySitAnim.transform.position = currSitData.turretParent.position;
}
else
{
var targetSit = list_Sits.Find(s => s.posID == roomJoinNotice.position);
// ⭐ 优先用后端 isBot 字段判断,兼容 userId.StartsWith("bot_")
bool isBotJoin = roomJoinNotice.isBot ||
(!string.IsNullOrEmpty(roomJoinNotice.userId) && roomJoinNotice.userId.StartsWith("bot_"));
if (isBotJoin)
{
// bot 加入 → 标记 isBot=true (JoinGame 里会自动 SetActive(true) 座位)
if (targetSit != null)
targetSit.isBot = true;
}
else
{
// 真人玩家加入 → 如果座位被bot占用则替换
if (targetSit != null && targetSit.isBot)
{
targetSit.isBot = false;
targetSit.ShowWaiting();
}
// ⭐ 新玩家同步走"主动请求"流程:
// 新玩家资源加载完毕后会主动发 requestSyncFish,
// 房主收到后才广播场上活鱼(避免新玩家还没加载完就收包丢失)
}
JoinGame(roomJoinNotice.position, roomJoinNotice.gold, roomJoinNotice.diamond, roomJoinNotice.cannonMultiple, 0, roomJoinNotice.userId, roomJoinNotice.cannonId, roomJoinNotice.wingsId, roomJoinNotice.warGodId);
}
}
else
{
UIManager.Instance.ShowBoardError("获取信息失败");
}
}
private void RefreshGGKCount()
{
var user = GameManager.Instance.user;
var props = user?.props;
int ggkCount = 0;
if (props != null)
{
foreach (var p in props)
{
if (p.number == "40005" && p.count > 0)
ggkCount = p.count;
}
}
if (tmp_GGKCount != null)
tmp_GGKCount.text = ggkCount.ToString();
// 同步刷新暴走/冰封本地计数(仅首次或服务端数据变化时同步)
RefreshSkillPropsCount(props);
// 同步更新游戏内金币显示(刮刮卡领奖、邮件领取等都会触发)
// ⭐ 但游戏内金币由房间数据(RoomJoinNotice.gold / SyncPlayerGold)管理,
// user.assets.gold 是账户总金币,进房时后端推送 AssetUpdated 会触发此处,
// 如果用账户总金币覆盖房间金币,会导致显示与后端房间数据不一致。
// 因此游戏内(isGame=true)不覆盖 goldNum,退房后由 SyncPlayerGold 或重新进房时设置。
if (currSitData != null && user?.assets != null && !WebSocketManager.Instance.isGame)
{
currSitData.goldNum = user.assets.gold;
if (currSitData.tMP_GoldNum != null)
currSitData.tMP_GoldNum.text = user.assets.gold.ToString();
}
}
///
/// 刷新暴走/冰封道具数量显示
/// 策略:记录上次服务端数量,只要服务端数量相比上次有变化(增加或减少)就信任服务端
/// (覆盖刮刮卡奖励、其他端使用等场景);服务端数量未变时保留本地扣减值,
/// 避免被服务端旧值覆盖本地扣减
///
private void RefreshSkillPropsCount(List props)
{
int serverBaoZou = 0;
int serverBingFeng = 0;
if (props != null)
{
foreach (var p in props)
{
if (p.number == "40004" && p.count > 0) serverBaoZou = p.count;
else if (p.number == "40003" && p.count > 0) serverBingFeng = p.count;
}
}
// 首次初始化,或服务端数量相比上次有变化(刮刮卡奖励增加/其他端使用减少)→ 以服务端为准
if (_localBaoZouCount < 0 || serverBaoZou != _lastServerBaoZouCount)
_localBaoZouCount = serverBaoZou;
if (_localBingFengCount < 0 || serverBingFeng != _lastServerBingFengCount)
_localBingFengCount = serverBingFeng;
_lastServerBaoZouCount = serverBaoZou;
_lastServerBingFengCount = serverBingFeng;
UpdateSkillPropsUI();
}
/// 仅更新 UI 文本
private void UpdateSkillPropsUI()
{
if (tmp_BaoZouCount != null)
tmp_BaoZouCount.text = Mathf.Max(0, _localBaoZouCount).ToString();
if (tmp_BingFengCount != null)
tmp_BingFengCount.text = Mathf.Max(0, _localBingFengCount).ToString();
}
protected override void OnHide()
{
base.OnHide();
// ⭐ 清理 XJMH 扇形切换UI(如果还在显示)
CleanupTurretSwitcher();
// ⭐ 解绑长按 img_OpenPT 打开炮台面板的订阅
UnhookOpenPTLongPress();
UnLoadResources();
// 场景可能未加载或已卸载,做保护避免重复卸载报错
var scene = SceneManager.GetSceneByName(sceneName);
if (scene.IsValid() && scene.isLoaded)
SceneManager.UnloadSceneAsync(sceneName);
// 清除场景加载标记,下次进入可重新加载
Assets.MyGame.Scripts.Net.WebSocketReponseList.ClearLoadedGameScene(sceneName);
WebSocketManager.Instance.Close(SocketType.game);
// ⭐ 重置本地道具计数,下次进入房间重新从服务端读取
_localBaoZouCount = -1;
_localBingFengCount = -1;
_lastServerBaoZouCount = -1;
_lastServerBingFengCount = -1;
}
#endregion
#region 长按 img_OpenPT 开关炮台面板
/// 已订阅长按事件的座位(换座位/重进房间时先解绑避免重复订阅)
private SitPosData _openPTHookedSit;
/// 缓存的长按处理器(解绑用)
private Action _openPTLongPressHandler;
///
/// 订阅本地座位的 img_OpenPT 长按事件:长按开关炮台面板(subUIManager,与 btn_ChangeTurret 同逻辑)。
/// XJMH 模式没有炮台面板(用扇形切换),不订阅。
///
private void HookOpenPTLongPress(SitPosData sit)
{
// 先解绑旧订阅
UnhookOpenPTLongPress();
if (sit == null || sit.turretControlMode == TurretControlMode.XJMH) return;
_openPTLongPressHandler = () =>
{
// 与 btn_ChangeTurret 完全相同的开关逻辑:开着就关,没开就打开
var panel = subUIManager.Get(UIPanel.TurretPanel);
if (panel != null && panel.gameObject.activeSelf)
subUIManager.Hide(UIPanel.TurretPanel);
else
{
subUIManager.ShowAsync(UIPanel.TurretPanel, (ItemPanelSelect.Turrect, true));
StartCoroutine(CloseSettingList());
}
};
sit.OnOpenPTLongPress += _openPTLongPressHandler;
_openPTHookedSit = sit;
}
/// 解绑长按 img_OpenPT 的订阅(面板隐藏/重新订阅前调用)
private void UnhookOpenPTLongPress()
{
if (_openPTHookedSit != null && _openPTLongPressHandler != null)
_openPTHookedSit.OnOpenPTLongPress -= _openPTLongPressHandler;
_openPTHookedSit = null;
_openPTLongPressHandler = null;
}
#endregion
#region 生命周期
private float baseAttackTime;
public override void Awake()
{
base.Awake();
GameManager.Instance.OnUserDataChanged += RefreshGGKCount;
baseAttackTime = attackTime;
btn_ChangeTurret.onClick.AddListener(() =>
{
var panel = subUIManager.Get(UIPanel.TurretPanel);
if (panel != null && panel.gameObject.activeSelf)
subUIManager.Hide(UIPanel.TurretPanel);
else
{
subUIManager.ShowAsync(UIPanel.TurretPanel, (ItemPanelSelect.Turrect, true));
StartCoroutine(CloseSettingList());
}
});
btn_Setting.onClick.AddListener(() => {subUIManager.ShowAsync(UIPanel.SettingPanel); });
btn_TuiChu.onClick.AddListener(() =>
{
StartLeaveCountdown();
});
// 服务器广播监听
EventCenter.Instance.AddEventListener(ConstEventString.SyncGameData_Rotate, SyncGameData_Rotate);
EventCenter.Instance.AddEventListener(ConstEventString.SyncGameData_FireBullet, SyncGameData_FireBullit);
EventCenter.Instance.AddEventListener(ConstEventString.SyncGameData_ChangeTargetMonster, SyncGameData_ChangeTargetMonster);
EventCenter.Instance.AddEventListener(ConstEventString.SyncGameData_RendromDieFish, SyncGameData_RendromDieFish);
EventCenter.Instance.AddEventListener(ConstEventString.SyncGameData_TurretState, SyncGameData_TurretState);
EventCenter.Instance.AddEventListener(ConstEventString.SyncGameData_ChangeTurret, SyncGameData_ChangeTurret);
EventCenter.Instance.AddEventListener(ConstEventString.SyncGameData_ChangeCannonMultiple, SyncGameData_ChangeCannonMultiple);
EventCenter.Instance.AddEventListener(ConstEventString.SyncGameData_ChangeWing, SyncGameData_ChangeWing);
// ⭐ 战神切换帧同步(changeZhanShen):其他玩家装备/卸下战神,对应座位加载/清除战神模型
EventCenter.Instance.AddEventListener(ConstEventString.SyncGameData_ChangeZhanShen, SyncGameData_ChangeZhanShen);
// SyncGameData_FreezeFish 已废弃:冰冻改由服务端 FishFrozen 事件统一触发
EventCenter.Instance.AddEventListener(ConstEventString.FishFrozen, OnFishFrozen);
EventCenter.Instance.AddEventListener(ConstEventString.SyncGameData_BotFireAngle, SyncGameData_BotFireAngle);
// ⭐ 爆奖结束帧同步(baoJiangEnd):其他玩家爆奖完成,显示最终 BJEndUI 并加分
EventCenter.Instance.AddEventListener(ConstEventString.BaoJiangEnd, OnBaoJiangEnd);
btn_TuJian.onClick.AddListener(() =>
{
subUIManager.ShowAsync(UIPanel.GameTuJian);
StartCoroutine(CloseSettingList());
});
btn_GuaGuaKa.onClick.AddListener(() =>
{
subUIManager.ShowAsync(UIPanel.GuaGuaKa);
});
}
/// 打开离开游戏确认面板:点确认直接退出房间(无倒计时)
public void StartLeaveCountdown()
{
if (_isLeavingCountdown) return;
_isLeavingCountdown = true;
if (obj_DJSPanel != null) obj_DJSPanel.SetActive(true);
// ⭐ 无倒计时:确认按钮直接可点
if (btn_QueRen != null) btn_QueRen.interactable = true;
if (tmp_DJS != null) tmp_DJS.text = "确认";
// 绑定按钮事件
if (btn_QueRen != null)
{
btn_QueRen.onClick.RemoveAllListeners();
btn_QueRen.onClick.AddListener(() =>
{
// 确认:关闭确认面板,发送离房请求 + 启动进度条卸载
if (obj_DJSPanel != null) obj_DJSPanel.SetActive(false);
RequestLeaveRoom();
});
}
if (btn_QuXiao != null)
{
btn_QuXiao.onClick.RemoveAllListeners();
btn_QuXiao.onClick.AddListener(CancelLeaveCountdown);
}
}
/// 是否正在离开倒计时
public bool IsLeavingCountdown() => _isLeavingCountdown;
/// 取消离开倒计时
public void CancelLeaveCountdown()
{
_isLeavingCountdown = false;
if (_leaveCountdownCoroutine != null)
{
StopCoroutine(_leaveCountdownCoroutine);
_leaveCountdownCoroutine = null;
}
if (obj_DJSPanel != null) obj_DJSPanel.SetActive(false);
}
private IEnumerator LeaveCountdownRoutine(int seconds)
{
int remaining = seconds;
while (remaining > 0)
{
if (tmp_DJS != null) tmp_DJS.text = $"确认({remaining})";
yield return new WaitForSeconds(1f);
remaining--;
}
// 倒计时结束:按钮启用,文本显示"确认"
if (tmp_DJS != null) tmp_DJS.text = "确认";
if (btn_QueRen != null) btn_QueRen.interactable = true;
}
public virtual void OnDestroy()
{
EventCenter.Instance.RemoveEventListener(ConstEventString.SyncGameData_Rotate, SyncGameData_Rotate);
EventCenter.Instance.RemoveEventListener(ConstEventString.SyncGameData_FireBullet, SyncGameData_FireBullit);
EventCenter.Instance.RemoveEventListener(ConstEventString.SyncGameData_ChangeTargetMonster, SyncGameData_ChangeTargetMonster);
EventCenter.Instance.RemoveEventListener(ConstEventString.SyncGameData_RendromDieFish, SyncGameData_RendromDieFish);
EventCenter.Instance.RemoveEventListener(ConstEventString.SyncGameData_TurretState, SyncGameData_TurretState);
EventCenter.Instance.RemoveEventListener(ConstEventString.SyncGameData_ChangeTurret, SyncGameData_ChangeTurret);
EventCenter.Instance.RemoveEventListener(ConstEventString.SyncGameData_ChangeCannonMultiple, SyncGameData_ChangeCannonMultiple);
EventCenter.Instance.RemoveEventListener(ConstEventString.SyncGameData_ChangeWing, SyncGameData_ChangeWing);
EventCenter.Instance.RemoveEventListener(ConstEventString.SyncGameData_ChangeZhanShen, SyncGameData_ChangeZhanShen);
// SyncGameData_FreezeFish 已废弃:冰冻改由服务端 FishFrozen 事件统一触发
EventCenter.Instance.RemoveEventListener(ConstEventString.FishFrozen, OnFishFrozen);
EventCenter.Instance.RemoveEventListener(ConstEventString.SyncGameData_BotFireAngle, SyncGameData_BotFireAngle);
EventCenter.Instance.RemoveEventListener(ConstEventString.BaoJiangEnd, OnBaoJiangEnd);
// ⭐ 离场清理:子弹池 + 特效池彻底清空,防止内存泄漏
BulletPool.Instance.ClearAll();
EffectPool.Instance.ClearAll();
// ⭐ 清理未完成的 not-self 爆奖 pending(避免悬空引用)
BaoJiangNotSelfHandler.ClearAll();
}
public virtual void Start()
{
btn_SettingList.onClick.AddListener(() =>
{
if (isOpenSetting) StartCoroutine(CloseSettingList());
else StartCoroutine(OpenSettingList());
});
if (toggle_Lock != null)
{
toggle_Lock.onValueChanged.AddListener(on =>
{
if (!on)
{
// ⭐ 取消锁定开关 → 重置自动锁定激活状态,下次需重新手动锁定激活
_autoLockActivated = false;
// 本地清理锁定
if (currSitData != null)
{
currSitData.ClearLock();
SyncTurretState(TurretState.Idle);
}
// 同步给服务器:取消锁定
SyncGameData sgd = new SyncGameData
{
sitPos = currSitData.posID,
frameType = FrameDataType.changeAutoTarget,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
frameData = new FrameData_ChangeAutoTarget { targetGuid = null }
};
WebSocketSendList.SyncGameData(sgd);
}
});
}
toggle_BaoZou.onValueChanged.AddListener(on =>
{
if (!on)
{
// ⭐ 打开后只能等倒计时结束自动关闭,不允许玩家手动关闭
// 直接 SetIsOnWithoutNotify(true) 避免递归触发 onValueChanged
toggle_BaoZou.SetIsOnWithoutNotify(true);
return;
}
if (_localBaoZouCount <= 0)
{
toggle_BaoZou.SetIsOnWithoutNotify(false);
return;
}
// 只发请求,效果等 UsePropNotice 回调触发
WebSocketSendList.UseProp(
GameManager.Instance.gameData.currRoomData.roomId,
GameManager.Instance.user.id,
"40004", 0);
});
toggle_SkillAuto.onValueChanged.AddListener(on =>
{
if (!on)
{
// 自动开火关闭 → 立即切回 Idle
currSitData.ChangeState(TurretState.Idle);
SyncTurretState(TurretState.Idle);
}
});
toggle_BingFeng.onValueChanged.AddListener(on => {
if (!on)
{
// ⭐ 打开后只能等倒计时结束自动关闭,不允许玩家手动关闭
// 直接 SetIsOnWithoutNotify(true) 避免递归触发 onValueChanged
toggle_BingFeng.SetIsOnWithoutNotify(true);
return;
}
if (_localBingFengCount <= 0)
{
toggle_BingFeng.SetIsOnWithoutNotify(false);
return;
}
// 只发请求,效果等 UsePropNotice 回调触发
WebSocketSendList.UseProp(
GameManager.Instance.gameData.currRoomData.roomId,
GameManager.Instance.user.id,
"40003", 0);
});
}
public bool isFrenzy;
public void SetFrenzy(bool on)
{
bool frenzy = CreateFishManager.Instance.currGameType.isFrenzy;
// 攻击速度翻倍
attackTime = frenzy ? baseAttackTime * 0.5f : baseAttackTime;
// 动画倍速
if (currSitData != null && currSitData.currentTurret != null)
currSitData.currentTurret.SetFrenzy(frenzy);
}
public void OnEnable()
{
}
int ina = 0;
protected virtual void Update()
{
HandleAutoRotate();
HandleInput();
CheckLockTargetValid();//锁定目标失效自动清除
HandleAutoFire(); // 自动开火真正发射
HandleLock();
HandleAutoLock(); // ⭐ 先切换锁定目标(切换后图标位置同帧更新)
UpdateLockFX(); // 锁定图标跟随目标
RefreshTripleLockIcons();
LoopLaser();
SettingClose();
DaoJiShi();
HandleBotAI();
WebSocketSendList.FlushSyncQueues(Time.deltaTime);
}
float sittingTime = 10;
float currSittingTime;
///
/// 等待10秒未操作关闭设置面板
///
public void SettingClose()
{
//if (isOpenSetting) StartCoroutine(CloseSettingList());
//else StartCoroutine(OpenSettingList());
if (isOpenSetting)
{
currSittingTime -= Time.deltaTime;
if (currSittingTime <= 0)
{
StartCoroutine(CloseSettingList());
}
}
else
{
currSittingTime = 10;
}
}
#endregion
#region 资源加载模块
public async void LoadResources()
{
CreateFishManager.Instance.isResources = false;
// ⭐ 重置卸载标记,下次进入可重新加载
_resourcesUnloaded = false;
await AudioResourcesData.LoadAudioGame();
JinDuTiao.SetProgress(0.2f);
await MonsterMoveLine.LoadBezier();
JinDuTiao.SetProgress(0.4f);
await FishResourcesData.LoadFishAssetsData();
JinDuTiao.SetProgress(0.6f);
await Addressables.LoadAssetsAsync("Audio", clip =>
{
if (clip.name == "PW_Shoot")
bulletClip = clip;
}).Task;
JinDuTiao.SetProgress(0.8f);
await Task.Delay(100);
CreateFishManager.Instance.EnterRoom(gameType);
CreateFishManager.Instance.isResources = true;
JinDuTiao.SetProgress(0.9f);
// ⭐ 鱼创建由服务端 OnFishSpawn 事件统一下发,客户端无需房主出鱼/请求同步。
// 玩家进入房间后服务端会主动推送 OnFishSpawn(type="init") 初始鱼,
// 重连/取消挂机时服务端会推送 OnFishSpawn(type="sync") 同步当前鱼。
// ⭐ RoomInfo.activeFishes 处理:非首个进房/断线重连/取消挂机时,
// RoomInfo 会携带 activeFishes(当前场上活鱼),用它初始化场景鱼。
// 首个进房时 activeFishes 为 null,由服务端 OnFishSpawn(type="init") 下发。
var activeFishes = GameManager.Instance.gameData.currRoomData?.activeFishes;
if (activeFishes != null && activeFishes.Count > 0)
{
var spawnData = new OnFishSpawnData
{
type = "sync",
timestamp = BeijingTimeManager.Instance.CurrentTimestampMs,
fish = activeFishes
};
CreateFishManager.Instance.OnFishSpawn(spawnData);
// 处理完清空,避免重复处理(如场景重载)
GameManager.Instance.gameData.currRoomData.activeFishes = null;
}
await Task.Delay(2000);
JinDuTiao.SetProgress(1);
await Task.Delay(300);
JinDuTiao.Hide();
}
public async void UnLoadResources()
{
if (_resourcesUnloaded) return;
CreateFishManager.Instance.ExitRoom();
CreateFishManager.Instance.isResources = false;
await FishResourcesData.UnLoadFishAssetsData();
await AudioResourcesData.UnLoadAudioGame();
await MonsterMoveLine.UnLoadBezier();
// ⭐ 清空子弹池 + 特效池(鱼池已在 UnLoadFishAssetsData 内清理)
BulletPool.Instance.ClearAll();
EffectPool.Instance.ClearAll();
_resourcesUnloaded = true;
}
/// 资源是否已卸载(避免 OnHide 重复卸载)
private bool _resourcesUnloaded = false;
/// 离房 socket 响应等待信号
private TaskCompletionSource _leaveRoomResponseTcs;
/// 正在离开的游戏面板类型(完成后关闭它切到 home)
private UIPanel _leavingGamePanelType;
///
/// 请求离开房间:发送离房 socket 请求 + 启动进度条卸载资源。
/// 进度条走到 0.9 后等待 socket 返回 LeaveRoomResult,
/// 收到返回后走完剩余进度并切回 home 界面。
///
public void RequestLeaveRoom()
{
if (_leaveRoomResponseTcs != null) return; // 防重复请求
// 先创建等待信号(在发送请求前创建,避免响应太快丢失)
_leaveRoomResponseTcs = new TaskCompletionSource();
// 发送离房请求
WebSocketSendList.LeaveRoom(
GameManager.Instance.gameData.currRoomData.roomId,
currSitData.user);
//CreateFishManager.Instance.fangzhu = false;
// 启动带进度条的卸载流程
BeginLeaveRoomUnload();
}
///
/// 离房卸载流程:进度条到 0.9 后等待 socket 响应,
/// 收到信号后走到 1.0 并切回 home 界面。
/// ⭐ 用 try-finally 保证无论中间是否异常,最终一定会关闭 GamePanel 并切回 Home
///
private async void BeginLeaveRoomUnload()
{
if (_resourcesUnloaded)
{
_leaveRoomResponseTcs = null;
return;
}
try
{
if (JinDuTiao != null) JinDuTiao.Show();
if (JinDuTiao != null) JinDuTiao.SetProgress(0.2f);
await FishResourcesData.UnLoadFishAssetsData();
if (JinDuTiao != null) JinDuTiao.SetProgress(0.4f);
await AudioResourcesData.UnLoadAudioGame();
if (JinDuTiao != null) JinDuTiao.SetProgress(0.6f);
await MonsterMoveLine.UnLoadBezier();
if (JinDuTiao != null) JinDuTiao.SetProgress(0.8f);
await Task.Delay(100);
CreateFishManager.Instance.ExitRoom();
CreateFishManager.Instance.isResources = false;
// ⭐ 清空子弹池 + 特效池(鱼池已在 UnLoadFishAssetsData 内清理)
BulletPool.Instance.ClearAll();
EffectPool.Instance.ClearAll();
if (JinDuTiao != null) JinDuTiao.SetProgress(0.9f);
// ⭐ 卡在 0.9 等待 socket 返回 LeaveRoomResult
// - socket 已早返回(比卸载快) → 在 0.9 再等 1 秒(美观)后变 1
// - socket 未返回 → 等待 socket,最多 10 秒,超时强制变 1 退出
if (_leaveRoomResponseTcs != null)
{
bool alreadyReceived = _leaveRoomResponseTcs.Task.IsCompleted;
if (alreadyReceived)
{
// socket 比卸载早返回 → 0.9 停留 1 秒
await Task.Delay(1000);
}
else
{
// socket 未返回 → 等待,最多 10 秒
var timeoutTask = Task.Delay(10000);
var done = await Task.WhenAny(_leaveRoomResponseTcs.Task, timeoutTask);
if (done == timeoutTask)
{
// 超时 10 秒 → 强制退出,必须执行和 ExitFishingGame 一致的清理
// 否则 game socket 不关闭、isGame 不复位,导致无法再次进入游戏
Debug.LogWarning("[LeaveRoom] 等待 socket 超时 10 秒,强制退出并清理游戏状态");
try
{
WebSocketManager.Instance.isGame = false;
GameManager.LogGoldFlow("ExitRoom", $"超时强制退出 房间={GameManager.Instance.gameData?.currRoomData?.roomId}");
WebSocketManager.Instance.Close(SocketType.game);
APIRequestList.GetUser((user) =>
{
GameManager.Instance.UpdataUserData(user);
});
}
catch (Exception cleanEx)
{
Debug.LogError($"[LeaveRoom] 超时清理异常: {cleanEx}");
}
}
}
}
// 标记已卸载,OnHide 跳过重复卸载
_resourcesUnloaded = true;
if (JinDuTiao != null) JinDuTiao.SetProgress(1);
await Task.Delay(300);
if (JinDuTiao != null) JinDuTiao.Hide();
}
catch (Exception ex)
{
Debug.LogError($"[LeaveRoom] 卸载流程异常: {ex}");
}
finally
{
// ⭐ 无论中间是否异常,都必须关闭 GamePanel 并切回 Home
// 否则 GamePanel 留在 panels 字典里,下次进入游戏时 Get 返回非 null 导致无法创建新面板
_leaveRoomResponseTcs = null;
if (JinDuTiao != null)
{
try { JinDuTiao.Hide(); } catch { }
}
// 进入 home 界面(关闭当前游戏面板)
switch (GameManager.Instance.gameData.currGameType)
{
case GameType.JQQD:
break;
case GameType.defaultPanel:
break;
case GameType.FLZY:
UIManager.Instance.ShowAsync(UIPanel.HomeMainPanel, UIPanel.MainGamePanelFLZY);
break;
case GameType.YYSC:
UIManager.Instance.ShowAsync(UIPanel.HomeMainPanel, UIPanel.MainGamePanelYYSC);
break;
case GameType.ZGXQ:
UIManager.Instance.ShowAsync(UIPanel.HomeMainPanel, UIPanel.MainGamePanelZGXQ);
break;
case GameType.XJMH:
UIManager.Instance.ShowAsync(UIPanel.HomeMainPanel, UIPanel.MainGamePanelXJMH);
break;
case GameType.HHWK:
UIManager.Instance.ShowAsync(UIPanel.HomeMainPanel, UIPanel.MainGamePanelHHWK);
break;
default:
break;
}
UIManager.Instance.ShowAsync(UIPanel.HomeMainPanel, GameManager.Instance.gameData.currGameType);
}
}
///
/// socket 收到 LeaveRoomResult 后调用,解除 0.9 的等待。
///
public void OnLeaveRoomResultReceived(UIPanel gamePanel)
{
_leavingGamePanelType = gamePanel;
_leaveRoomResponseTcs?.TrySetResult(true);
}
#endregion
#region 输入系统(InputModule)
private bool IsPointerOverUI(Vector2 pos)
{
PointerEventData eventData = new PointerEventData(EventSystem.current);
eventData.position = pos;
List results = new List();
EventSystem.current.RaycastAll(eventData, results);
return results.Count > 0;
}
public struct CrossPlatformInput
{
public bool isPressed;
public bool wasPressedThisFrame;
public bool wasReleasedThisFrame;
public Vector2 position;
}
private CrossPlatformInput GetInput()
{
CrossPlatformInput input = new CrossPlatformInput();
// ============================
// PC / Editor → 鼠标输入
// ============================
#if UNITY_STANDALONE || UNITY_EDITOR
if (Mouse.current != null)
{
input.isPressed = Mouse.current.leftButton.isPressed;
input.wasPressedThisFrame = Mouse.current.leftButton.wasPressedThisFrame;
input.wasReleasedThisFrame = Mouse.current.leftButton.wasReleasedThisFrame;
input.position = Mouse.current.position.ReadValue();
}
#else
// ============================
// 移动平台 → 触摸输入
// ============================
var touch = Touchscreen.current?.primaryTouch;
if (touch != null)
{
input.isPressed = touch.press.isPressed;
input.wasPressedThisFrame = touch.press.wasPressedThisFrame;
input.wasReleasedThisFrame = touch.press.wasReleasedThisFrame;
input.position = touch.position.ReadValue();
}
#endif
return input;
}
private void HandleInput()
{
// ⭐ 退出倒计时期间屏蔽本地玩家手动输入(旋转/开火/锁定),其他玩家和机器人不受影响
if (_isLeavingCountdown) return;
CrossPlatformInput input = GetInput();
if (!input.isPressed && !input.wasPressedThisFrame && !input.wasReleasedThisFrame)
return;
if (input.wasPressedThisFrame)
{
// ⭐ UI 点击过滤
if (IsPointerOverUI(input.position))
return;
}
// 按下
if (input.wasPressedThisFrame)
{
isChangingTurret = CheckTouchIsChangeTurret(input.position);
clickTime = attackTime;
if (mySitAnim != null && mySitAnim.gameObject.activeSelf)
mySitAnim.gameObject.SetActive(false);
// XJMH 模式:检测是否按在当前炮台上(长按触发扇形切换)
_isPendingTurretLongPress = !isChangingTurret
&& currSitData.turretControlMode == TurretControlMode.XJMH
&& IsTouchOnCurrentTurret(input.position);
if (!isChangingTurret)
{
// ⭐ 起飞炮台(FlyTurretData):
// 飞行/悬停中 → 点击空白处=改目标(点到哪飞到哪),立即响应;
// 未起飞 → 仅记录轻点判定,继续走下方正常开火流程(松开时判定轻点才起飞),
// 保证平时按住扫射/自动开火与普通炮台完全一致
if (currSitData.currentTurret is FlyTurretData flyTurret)
{
if (flyTurret.IsFlying)
{
if (!_isPendingTurretLongPress && flyTurret.TakeoffToScreenPos(input.position))
{
HookFlyTurretUpwardFire(flyTurret);
return;
}
}
else if (!_isPendingTurretLongPress && !currSitData.isDrillActive)
{
_flyTapPending = true;
_flyTapTime = Time.time;
_flyTapPos = input.position;
}
}
// ⭐ 本地金币校验:没钱不切换 Attack 状态,也不广播,避免无效请求
if (!HasEnoughGoldToFire(toggle_Three.isOn))
return;
// 按下即进入 Attack:短按→射击,长按超过阈值后切回 Idle 弹扇形
currSitData.ChangeState(TurretState.Attack);
SyncTurretState(TurretState.Attack);
}
}
// 长按
if (input.isPressed)
{
// XJMH 长按炮台判定:超过阈值后弹出扇形切换UI
if (_isPendingTurretLongPress && !_isTurretSwitcherActive)
{
changeTurretTime += Time.deltaTime;
if (changeTurretTime >= turretLongPressThreshold)
{
_isPendingTurretLongPress = false;
_isTurretSwitcherActive = true;
// 扇形切换弹出时切回 Idle,停止射击动画
currSitData.ChangeState(TurretState.Idle);
SyncTurretState(TurretState.Idle);
OnTurretSwitcherShow(input.position);
}
}
if (_isTurretSwitcherActive)
{
OnTurretSwitcherUpdate(input.position);
return;
}
if (isChangingTurret)
{
HandleChangeTurretHold();
return;
}
// XJMH 待判定长按:不旋转(手指在炮台上旋转无意义),但仍允许开火(短按=射击)
if (_isPendingTurretLongPress)
{
TryFire();
return;
}
// ⭐ 起飞炮台:飞行/悬停期间手指按住不旋转不开火(炮口固定朝上,子弹自行向上发射)
if (currSitData.currentTurret is FlyTurretData flyHold && flyHold.IsFlying)
return;
// 锁定模式:不手动旋转/开火,交给 HandleAutoRotate + HandleAutoFire
// ⭐ 特殊鱼爆奖期间例外:自动锁定已禁用,玩家点击仍可手动旋转炮台瞄准发射方向
if (toggle_Lock.isOn && !(currSitData != null && currSitData.isDrillActive)) return;
// UI 上不旋转不发射
if (IsPointerOverUI(input.position)) return;
HandleRotate(input.position);
TryFire();
}
// 松开
if (input.wasReleasedThisFrame)
{
// XJMH 扇形切换释放:根据滑动位置选择炮台
if (_isTurretSwitcherActive)
{
_isTurretSwitcherActive = false;
_isPendingTurretLongPress = false;
changeTurretTime = 0;
OnTurretSwitcherHide(input.position);
return;
}
// XJMH 短按炮台释放(未触发长按)→ 正常回到 Idle
if (_isPendingTurretLongPress)
{
_isPendingTurretLongPress = false;
changeTurretTime = 0;
}
if (isChangingTurret)
{
isChangingTurret = false;
changeTurretTime = 0;
CloseChangeTurretUI();
return;
}
// ⭐ 起飞炮台:轻点空白处=起飞(按下时已正常开火,松开时判定轻点→触发起飞)
// 长按/拖动扫射时此判定不通过,正常走下方 Idle,普通开火完全不受影响
if (_flyTapPending)
{
_flyTapPending = false;
if (currSitData.currentTurret is FlyTurretData flyTap
&& !flyTap.IsFlying
&& Time.time - _flyTapTime <= flyTapMaxDuration
&& Vector2.Distance(input.position, _flyTapPos) <= flyTapMaxMove
&& flyTap.TakeoffToScreenPos(input.position))
{
HookFlyTurretUpwardFire(flyTap);
return; // 起飞:跳过下方 Idle 切换,避免打断起飞动画
}
}
currSitData.ChangeState(TurretState.Idle);
SyncTurretState(TurretState.Idle);
}
}
private void HandleChangeTurretHold()
{
changeTurretTime += Time.deltaTime;
if (changeTurretTime > 1f)
{
// 长按换炮台:已改为按钮打开 TurretPanel
}
}
private bool CheckTouchIsChangeTurret(Vector2 pos)
{
PointerEventData pointerData = new PointerEventData(EventSystem.current);
pointerData.position = pos;
List results = new List();
EventSystem.current.RaycastAll(pointerData, results);
foreach (var result in results)
{
if (result.gameObject.CompareTag("ChangeTurret"))
{
SitPosData sit = result.gameObject.GetComponentInParent();
if (sit == currSitData)
return true;
}
}
return false;
}
private void CloseChangeTurretUI()
{
// currSitData.changeTurretList.SetActive(false); — 已废弃
changeTurretTime = 0;
isChangingTurret = false;
}
// ==================== XJMH 炮台扇形切换 ====================
///
/// 检测触摸点是否在当前炮台的 RectTransform 范围内(XJMH 长按切换用)。
///
protected bool IsTouchOnCurrentTurret(Vector2 screenPos)
{
if (currSitData == null || currSitData.currentTurret == null) return false;
RectTransform rt = currSitData.currentTurret.GetComponent();
if (rt == null) return false;
Canvas canvas = UIManager.Instance != null && UIManager.Instance.uiRoot != null
? UIManager.Instance.uiRoot.GetComponent