Files
5.24BuYu/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs
T

3856 lines
154 KiB
C#
Raw Normal View History

2026-06-08 21:23:40 +08:00
/*
2026-05-04 17:21:52 +08:00
======================== 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() 同步锁定
2026-06-13 01:25:41 +08:00
├── SyncTurretState() 同步激光状态
2026-05-04 17:21:52 +08:00
└── SyncChangeTurret() 同步炮台切换(可选)
-----------------------------------------------------------------------
⑦ NetReceiveModule(服务器广播处理)
├── SyncGameData_Rotate() 广播旋转
├── SyncGameData_FireBullet() 广播发射
├── SyncGameData_ChangeTarget() 广播锁定
├── SyncGameData_ChangeTurret() 广播炮台切换(可选)
└── SyncGameData_LaserState() 广播激光状态(可选)
-----------------------------------------------------------------------
⑧ ResourceModule(资源加载)
├── LoadResources() 加载音频/鱼资源/路线
└── UnLoadResources() 卸载资源
-----------------------------------------------------------------------
⑨ PlayerJoinModule(玩家加入/退出)
├── JoinGame() 玩家加入房间
└── ExitGame() 玩家退出房间
=======================================================================
*/
using Assets.MyGame.Scripts.Net;
2026-03-18 17:26:14 +08:00
using Assets.MyGame.Scripts.Net.API;
2026-07-19 17:39:31 +08:00
using Assets.MyGame.Scripts.Net.API.UserData_SubClass;
2026-03-18 17:26:14 +08:00
using Assets.MyGame.Scripts.Net.APIResponses.Enum;
using Assets.MyGame.Scripts.Net.WebResponses;
2026-04-29 01:18:39 +08:00
using DG.Tweening;
2026-03-18 17:26:14 +08:00
using Newtonsoft.Json;
2026-03-27 15:18:35 +08:00
using Spine.Unity;
2026-03-18 17:26:14 +08:00
using System;
using System.Collections;
2026-02-13 19:23:01 +08:00
using System.Collections.Generic;
2026-05-28 18:16:45 +08:00
using System.Linq;
2026-05-27 19:47:06 +08:00
using System.Security.Cryptography;
using System.Text;
2026-03-27 15:18:35 +08:00
using System.Threading.Tasks;
using TMPro;
2026-02-13 19:23:01 +08:00
using UnityEngine;
2026-03-27 15:18:35 +08:00
using UnityEngine.AddressableAssets;
2026-03-18 17:26:14 +08:00
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
2026-06-21 00:07:26 +08:00
using UnityEngine.Playables;
2026-03-18 17:26:14 +08:00
using UnityEngine.SceneManagement;
using UnityEngine.UI;
2026-07-05 16:07:22 +08:00
using Random = UnityEngine.Random;
2026-02-13 19:23:01 +08:00
public class GameMainPanel_FishBase : GameMainPanel_Base
{
2026-07-12 00:26:17 +08:00
[Header("离开游戏倒计时")]
public GameObject obj_DJSPanel;
public TextMeshProUGUI tmp_DJS; //确认(10)
public Button btn_QueRen;
public Button btn_QuXiao;
/// <summary>是否正在退出倒计时(期间屏蔽帧同步和爆奖特效)</summary>
private bool _isLeavingCountdown = false;
private Coroutine _leaveCountdownCoroutine;
2026-03-18 17:26:14 +08:00
[Header("游戏场景")]
public string sceneName;
public GameTypeEnum gameType;
[Header("设置")]
protected bool isOpenSetting;
public Button btn_SettingList;
2026-04-29 01:18:39 +08:00
public Transform SittingListParent;
2026-06-08 21:23:40 +08:00
public Button btn_TuJian;
public Button btn_ChangeTurret;
2026-05-01 06:26:50 +08:00
public Button btn_TuiChu;
2026-06-21 00:07:26 +08:00
public Button btn_Setting;
2026-05-01 06:26:50 +08:00
2026-06-25 01:48:07 +08:00
//[Header("我在这")]
2026-03-27 15:18:35 +08:00
public GameObject mySitAnim;
2026-03-18 17:26:14 +08:00
public Dictionary<string, Toggle> dic_Skill_Toggle = new Dictionary<string, Toggle>();//开关型技能
2026-06-25 01:48:07 +08:00
[Header("Skillbottom")]
2026-03-18 17:26:14 +08:00
public Toggle toggle_SkillAuto;
public Toggle toggle_Lock;
public Toggle toggle_Three;
public Toggle toggle_Shuang;
2026-05-18 18:39:11 +08:00
public Toggle toggle_BaoZou;
2026-07-18 14:39:25 +08:00
public TextMeshProUGUI tmp_BaoZouCount;
2026-06-14 20:26:21 +08:00
public Image img_BaoZou;
2026-07-22 17:46:15 +08:00
float frenzyDuration = 5f; // 疯狂状态持续时间
2026-05-27 19:47:06 +08:00
public float currFrenzyTime = 0f;
public Toggle toggle_BingFeng;
2026-07-18 14:39:25 +08:00
public TextMeshProUGUI tmp_BingFengCount;
2026-05-27 19:47:06 +08:00
public Image img_BingFeng;
2026-06-16 02:28:58 +08:00
public GameObject bingFengEffect; // 冰封特效预制体(首次Instantiate,之后激活/隐藏)
private GameObject _bingFengInstance; // 克隆出来的实例
2026-07-25 23:05:24 +08:00
float bingFengDuration = 15f; // 冰封状态持续时间(服务端 FishFrozen 延长 15 秒,客户端写死 15s 倒计时)
2026-05-27 19:47:06 +08:00
public float currBingFengTime = 0f;
2026-07-24 23:47:50 +08:00
private Dictionary<Component, object> _frozenAnimSpeeds = new Dictionary<Component, object>();
2026-06-25 01:48:07 +08:00
2026-07-19 17:39:31 +08:00
// ⭐ 暴走/冰封本地数量:因为暂无扣减 API,激活时本地 -1,避免被 OnUserDataChanged 覆盖回服务端旧值
private int _localBaoZouCount = -1; // -1 表示尚未初始化
private int _localBingFengCount = -1;
2026-07-23 01:51:07 +08:00
// 上次从服务端读到的数量:用于判断服务端是否变化(刮刮卡奖励/其他端使用),
// 服务端值变化时信任服务端;未变化时保留本地扣减值
private int _lastServerBaoZouCount = -1;
private int _lastServerBingFengCount = -1;
2026-07-19 17:39:31 +08:00
2026-06-25 01:48:07 +08:00
[Header("Skillleft")]
public Button btn_GuaGuaKa;
public TextMeshProUGUI tmp_GGKCount;
2026-03-18 17:26:14 +08:00
public List<SitPosData> list_Sits = new List<SitPosData>();
public SitPosData currSitData;
[Header("攻击参数")]
public float attackTime = 0.5f;
2026-07-03 18:55:56 +08:00
[Header("人机参数")]
2026-07-05 17:39:30 +08:00
public float botRotateSpeed = 80f; // 兜底旋转速度(BotConfig 为空时用)
2026-03-18 17:26:14 +08:00
float clickTime = 0;
2026-07-02 18:28:50 +08:00
private Dictionary<int, float> botFireTimer = new Dictionary<int, float>();
2026-07-05 16:07:22 +08:00
private Dictionary<string, float> botLastAngle = new Dictionary<string, float>();
2026-07-20 01:44:28 +08:00
// bot 旋转广播节流:记录上次广播的角度/时间
private Dictionary<int, float> botLastSyncAngle = new Dictionary<int, float>();
private Dictionary<int, float> botLastSyncTime = new Dictionary<int, float>();
2026-07-05 16:07:22 +08:00
// bot 技能运行时
private Dictionary<int, BotSkillRuntime> botSkills = new Dictionary<int, BotSkillRuntime>();
2026-07-05 17:39:30 +08:00
// bot 目标记忆
private Dictionary<int, string> botTargetGuid = new Dictionary<int, string>();
2026-07-22 03:41:06 +08:00
// bot 锁定同步节流:记录上次广播的锁定目标 guid(null 表示未锁定),只有目标变化才广播
private Dictionary<int, string> botLastSyncLockGuid = new Dictionary<int, string>();
2026-07-25 21:04:58 +08:00
// bot 炮台状态同步节流:记录上次广播的 TurretState,只有状态变化才广播
private Dictionary<int, TurretState> botLastSyncTurretState = new Dictionary<int, TurretState>();
2026-07-05 17:39:30 +08:00
// bot 状态计时:开火后持续 Attack 动画,空闲 0.3s 切回 Idle
private Dictionary<int, float> botIdleTimer = new Dictionary<int, float>();
2026-07-05 18:52:09 +08:00
// bot 最近的 FrameData_FireBullet(给其他客户端还原技能状态)
private Dictionary<string, FrameData_FireBullet> botLastFireData = new Dictionary<string, FrameData_FireBullet>();
2026-07-11 17:07:32 +08:00
// bot 运行时状态(进场金币/倍数档位/行为模式/退出)
private Dictionary<int, BotRuntimeState> botRuntimeStates = new Dictionary<int, BotRuntimeState>();
2026-03-18 17:26:14 +08:00
public GameObject bullet_Test;
public GameObject bulletParent;
float changeTurretTime = 0f;
bool isChangingTurret = false;
2026-07-28 18:29:49 +08:00
// XJMH 长按炮台扇形切换
[Header("XJMH 炮台长按切换")]
[Tooltip("长按炮台触发扇形切换的阈值(秒)")]
public float turretLongPressThreshold = 0.5f;
/// <summary>是否正在等待长按炮台判定(按下炮台但尚未超过阈值)</summary>
protected bool _isPendingTurretLongPress = false;
/// <summary>炮台扇形切换UI是否已显示</summary>
protected bool _isTurretSwitcherActive = false;
2026-03-18 17:26:14 +08:00
int bulletCount = 0;
public GameObject boardCast_Boss;
2026-07-27 23:30:25 +08:00
[Header("创建视频播放器")]
[Tooltip("鱼创建时播放的入场视频控件,由 Boss 预制体的 createVideoClip 指定视频片段。Inspector 拖拽场景中的 VideoPlayer 控件")]
public UnityEngine.Video.VideoPlayer createVideoPlayer;
2026-03-27 15:18:35 +08:00
AudioClip bulletClip;
2026-06-14 20:26:21 +08:00
public List<int> paoBeiList = new List<int>();
2026-04-24 22:51:34 +08:00
2026-05-01 06:26:50 +08:00
public ProgressBar JinDuTiao;
2026-05-04 17:21:52 +08:00
#region UI
2026-03-18 17:26:14 +08:00
bool isfirstShow = true;
protected override void OnShow(object args = null)
{
base.OnShow(args);
2026-05-01 06:26:50 +08:00
LoadResources();
2026-06-28 16:48:18 +08:00
RefreshGGKCount();
2026-03-18 17:26:14 +08:00
//UIManager.Instance.ShowBoard("太好看了");
var v = JsonConvert.DeserializeObject<WebSocketResponseBase<RoomJoinNotice>>(args.ToString());
if (v.arguments.Count > 0)
{
RoomJoinNotice roomJoinNotice = v.arguments[0];
2026-07-19 18:24:32 +08:00
// ⭐ paoBeiList 每次进入房间都重新根据当前场次+房间等级获取,
// 避免再次进入同一场次的不同等级房间时沿用旧 paoBeiList(例如先高级场后低级场)
2026-03-18 17:26:14 +08:00
{
2026-05-27 19:47:06 +08:00
int t = GameManager.Instance.gameData.currRoomData.gameType;
int l = GameManager.Instance.gameData.currRoomData.roomLevel;
foreach (var item in GameManager.Instance.gameConfig.roomConfigs)
{
2026-07-20 01:44:28 +08:00
if (item.gameType == t && l == item.roomLevel)
2026-05-27 19:47:06 +08:00
{
2026-06-14 20:26:21 +08:00
paoBeiList = item.cannonMultipleList;
2026-05-27 19:47:06 +08:00
}
}
2026-07-19 18:24:32 +08:00
}
if (isfirstShow)
{
2026-05-27 19:47:06 +08:00
string roomId = GameManager.Instance.gameData.currRoomData.roomId;
//string roomId = GameManager.Instance.gameData.currRoomData;
2026-03-18 17:26:14 +08:00
foreach (var item in GameManager.Instance.gameData.currRoomData.players)
{
2026-08-28 22:33:35 +08:00
JoinGame(item.position, item.gold, item.diamond, item.cannonMultiple, 0, item.userId, item.cannonId, item.wingsId, item.warGodId);
2026-07-16 20:16:05 +08:00
// ⭐ 优先用后端 isBot 字段,兼容 userId.StartsWith("bot_")
2026-07-02 19:09:00 +08:00
var sit = list_Sits.Find(s => s.posID == item.position);
2026-07-16 20:16:05 +08:00
if (sit != null)
{
bool isBot = item.isBot ||
(!string.IsNullOrEmpty(item.userId) && item.userId.StartsWith("bot_"));
if (isBot) sit.isBot = true;
}
2026-06-16 19:12:37 +08:00
}
2026-03-18 17:26:14 +08:00
GameManager.Instance.gameData.roomJoinNotice = v.arguments[0];
isfirstShow = false;
2026-04-29 01:18:39 +08:00
2026-06-13 01:25:41 +08:00
foreach (var item in list_Sits)
{
2026-06-14 20:26:21 +08:00
if (item.posID == roomJoinNotice.position)
2026-06-13 01:25:41 +08:00
{
currSitData = item;
}
else
{
item.btn_TurretAdd.gameObject.SetActive(false);
item.btn_TurretMinus.gameObject.SetActive(false);
}
}
2026-06-08 21:23:40 +08:00
//// currSitData.ChangeTurret(1); — 已由 SitPosData.OnEnable 自动加载
2026-06-13 22:34:43 +08:00
if (currSitData.currentTurret != null)
SyncChangeTurret(currSitData.currentTurret.turretNumber);
2026-03-18 17:26:14 +08:00
currSitData.lockFX.transform.GetChild(0).gameObject.SetActive(false);
currSitData.lockFX.transform.GetChild(1).gameObject.SetActive(true);
2026-06-15 00:03:33 +08:00
if (mySitAnim != null && currSitData.turretParent != null)
mySitAnim.transform.position = currSitData.turretParent.position;
2026-03-18 17:26:14 +08:00
}
else
{
2026-06-16 19:12:37 +08:00
var targetSit = list_Sits.Find(s => s.posID == roomJoinNotice.position);
2026-07-16 20:16:05 +08:00
// ⭐ 优先用后端 isBot 字段判断,兼容 userId.StartsWith("bot_")
bool isBotJoin = roomJoinNotice.isBot ||
(!string.IsNullOrEmpty(roomJoinNotice.userId) && roomJoinNotice.userId.StartsWith("bot_"));
if (isBotJoin)
2026-06-16 19:12:37 +08:00
{
2026-07-16 20:16:05 +08:00
// bot 加入 → 标记 isBot=true (JoinGame 里会自动 SetActive(true) 座位)
if (targetSit != null)
targetSit.isBot = true;
}
else
{
// 真人玩家加入 → 如果座位被bot占用则替换
if (targetSit != null && targetSit.isBot)
{
targetSit.isBot = false;
targetSit.ShowWaiting();
}
2026-07-20 22:32:52 +08:00
// ⭐ 新玩家同步走"主动请求"流程:
// 新玩家资源加载完毕后会主动发 requestSyncFish,
// 房主收到后才广播场上活鱼(避免新玩家还没加载完就收包丢失)
2026-06-16 19:12:37 +08:00
}
2026-08-28 22:33:35 +08:00
JoinGame(roomJoinNotice.position, roomJoinNotice.gold, roomJoinNotice.diamond, roomJoinNotice.cannonMultiple, 0, roomJoinNotice.userId, roomJoinNotice.cannonId, roomJoinNotice.wingsId, roomJoinNotice.warGodId);
2026-03-27 15:18:35 +08:00
2026-03-18 17:26:14 +08:00
}
}
else
{
UIManager.Instance.ShowBoardError("获取信息失败");
}
}
2026-06-28 16:48:18 +08:00
private void RefreshGGKCount()
{
2026-07-11 14:19:06 +08:00
var user = GameManager.Instance.user;
var props = user?.props;
2026-07-18 14:39:25 +08:00
int ggkCount = 0;
2026-06-28 16:48:18 +08:00
if (props != null)
{
foreach (var p in props)
{
if (p.number == "40005" && p.count > 0)
2026-07-18 14:39:25 +08:00
ggkCount = p.count;
2026-06-28 16:48:18 +08:00
}
}
2026-07-18 14:39:25 +08:00
2026-06-28 16:48:18 +08:00
if (tmp_GGKCount != null)
2026-07-18 14:39:25 +08:00
tmp_GGKCount.text = ggkCount.ToString();
2026-07-19 17:39:31 +08:00
// 同步刷新暴走/冰封本地计数(仅首次或服务端数据变化时同步)
RefreshSkillPropsCount(props);
2026-07-11 14:19:06 +08:00
// 同步更新游戏内金币显示(刮刮卡领奖、邮件领取等都会触发)
2026-07-29 00:31:59 +08:00
// ⭐ 但游戏内金币由房间数据(RoomJoinNotice.gold / SyncPlayerGold)管理,
// user.assets.gold 是账户总金币,进房时后端推送 AssetUpdated 会触发此处,
// 如果用账户总金币覆盖房间金币,会导致显示与后端房间数据不一致。
// 因此游戏内(isGame=true)不覆盖 goldNum,退房后由 SyncPlayerGold 或重新进房时设置。
if (currSitData != null && user?.assets != null && !WebSocketManager.Instance.isGame)
2026-07-11 14:19:06 +08:00
{
currSitData.goldNum = user.assets.gold;
if (currSitData.tMP_GoldNum != null)
currSitData.tMP_GoldNum.text = user.assets.gold.ToString();
}
2026-06-28 16:48:18 +08:00
}
2026-07-19 17:39:31 +08:00
/// <summary>
/// 刷新暴走/冰封道具数量显示
2026-07-23 01:51:07 +08:00
/// 策略:记录上次服务端数量,只要服务端数量相比上次有变化(增加或减少)就信任服务端
/// (覆盖刮刮卡奖励、其他端使用等场景);服务端数量未变时保留本地扣减值,
/// 避免被服务端旧值覆盖本地扣减
2026-07-19 17:39:31 +08:00
/// </summary>
private void RefreshSkillPropsCount(List<BaseProductCount> props)
{
int serverBaoZou = 0;
int serverBingFeng = 0;
if (props != null)
{
foreach (var p in props)
{
2026-07-23 01:51:07 +08:00
if (p.number == "40004" && p.count > 0) serverBaoZou = p.count;
else if (p.number == "40003" && p.count > 0) serverBingFeng = p.count;
2026-07-19 17:39:31 +08:00
}
}
2026-07-23 01:51:07 +08:00
// 首次初始化,或服务端数量相比上次有变化(刮刮卡奖励增加/其他端使用减少)→ 以服务端为准
if (_localBaoZouCount < 0 || serverBaoZou != _lastServerBaoZouCount)
2026-07-19 17:39:31 +08:00
_localBaoZouCount = serverBaoZou;
2026-07-23 01:51:07 +08:00
if (_localBingFengCount < 0 || serverBingFeng != _lastServerBingFengCount)
2026-07-19 17:39:31 +08:00
_localBingFengCount = serverBingFeng;
2026-07-23 01:51:07 +08:00
_lastServerBaoZouCount = serverBaoZou;
_lastServerBingFengCount = serverBingFeng;
2026-07-19 17:39:31 +08:00
UpdateSkillPropsUI();
}
/// <summary>仅更新 UI 文本</summary>
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();
}
2026-03-18 17:26:14 +08:00
protected override void OnHide()
{
base.OnHide();
2026-07-28 18:29:49 +08:00
// ⭐ 清理 XJMH 扇形切换UI(如果还在显示)
CleanupTurretSwitcher();
2026-05-01 06:26:50 +08:00
UnLoadResources();
2026-07-13 00:12:44 +08:00
// 场景可能未加载或已卸载,做保护避免重复卸载报错
var scene = SceneManager.GetSceneByName(sceneName);
if (scene.IsValid() && scene.isLoaded)
SceneManager.UnloadSceneAsync(sceneName);
2026-07-13 02:24:32 +08:00
// 清除场景加载标记,下次进入可重新加载
Assets.MyGame.Scripts.Net.WebSocketReponseList.ClearLoadedGameScene(sceneName);
2026-03-18 17:26:14 +08:00
WebSocketManager.Instance.Close(SocketType.game);
2026-07-19 17:39:31 +08:00
// ⭐ 重置本地道具计数,下次进入房间重新从服务端读取
_localBaoZouCount = -1;
_localBingFengCount = -1;
2026-07-23 01:51:07 +08:00
_lastServerBaoZouCount = -1;
_lastServerBingFengCount = -1;
2026-03-18 17:26:14 +08:00
2026-02-13 19:23:01 +08:00
}
2026-05-04 17:21:52 +08:00
#endregion
#region 生命周期
2026-05-18 18:39:11 +08:00
private float baseAttackTime;
2026-05-04 17:21:52 +08:00
public override void Awake()
{
base.Awake();
2026-06-28 16:48:18 +08:00
GameManager.Instance.OnUserDataChanged += RefreshGGKCount;
2026-05-18 18:39:11 +08:00
baseAttackTime = attackTime;
2026-06-08 21:23:40 +08:00
btn_ChangeTurret.onClick.AddListener(() =>
{
var panel = subUIManager.Get<SubUIBase>(UIPanel.TurretPanel);
if (panel != null && panel.gameObject.activeSelf)
subUIManager.Hide(UIPanel.TurretPanel);
else
{
subUIManager.ShowAsync(UIPanel.TurretPanel, (ItemPanelSelect.Turrect, true));
StartCoroutine(CloseSettingList());
}
});
2026-06-21 00:07:26 +08:00
btn_Setting.onClick.AddListener(() => {subUIManager.ShowAsync(UIPanel.SettingPanel); });
2026-05-04 17:21:52 +08:00
btn_TuiChu.onClick.AddListener(() =>
{
2026-07-12 00:26:17 +08:00
StartLeaveCountdown();
2026-05-04 17:21:52 +08:00
});
// 服务器广播监听
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_Rotate, SyncGameData_Rotate);
2026-05-27 19:47:06 +08:00
EventCenter.Instance.AddEventListener<FireCannonSendData>(ConstEventString.SyncGameData_FireBullet, SyncGameData_FireBullit);
2026-05-04 17:21:52 +08:00
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeTargetMonster, SyncGameData_ChangeTargetMonster);
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_RendromDieFish, SyncGameData_RendromDieFish);
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_TurretState, SyncGameData_TurretState);
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeTurret, SyncGameData_ChangeTurret);
2026-06-19 18:18:04 +08:00
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeCannonMultiple, SyncGameData_ChangeCannonMultiple);
2026-06-30 01:31:53 +08:00
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeWing, SyncGameData_ChangeWing);
2026-08-28 22:33:35 +08:00
// ⭐ 战神切换帧同步(changeZhanShen):其他玩家装备/卸下战神,对应座位加载/清除战神模型
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeZhanShen, SyncGameData_ChangeZhanShen);
2026-07-25 23:05:24 +08:00
// SyncGameData_FreezeFish 已废弃:冰冻改由服务端 FishFrozen 事件统一触发
EventCenter.Instance.AddEventListener(ConstEventString.FishFrozen, OnFishFrozen);
2026-07-02 18:28:50 +08:00
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_BotFireAngle, SyncGameData_BotFireAngle);
2026-07-30 22:09:55 +08:00
// ⭐ 爆奖结束帧同步(baoJiangEnd):其他玩家爆奖完成,显示最终 BJEndUI 并加分
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.BaoJiangEnd, OnBaoJiangEnd);
2026-06-08 21:23:40 +08:00
btn_TuJian.onClick.AddListener(() =>
{
subUIManager.ShowAsync(UIPanel.GameTuJian);
StartCoroutine(CloseSettingList());
});
2026-06-25 01:48:07 +08:00
btn_GuaGuaKa.onClick.AddListener(() =>
{
subUIManager.ShowAsync(UIPanel.GuaGuaKa);
});
2026-05-04 17:21:52 +08:00
}
2026-07-25 02:17:19 +08:00
/// <summary>打开离开游戏确认面板:点确认直接退出房间(无倒计时)</summary>
2026-07-12 00:26:17 +08:00
public void StartLeaveCountdown()
{
if (_isLeavingCountdown) return;
_isLeavingCountdown = true;
if (obj_DJSPanel != null) obj_DJSPanel.SetActive(true);
2026-07-25 02:17:19 +08:00
// ⭐ 无倒计时:确认按钮直接可点
if (btn_QueRen != null) btn_QueRen.interactable = true;
if (tmp_DJS != null) tmp_DJS.text = "确认";
2026-07-12 00:26:17 +08:00
// 绑定按钮事件
if (btn_QueRen != null)
{
btn_QueRen.onClick.RemoveAllListeners();
btn_QueRen.onClick.AddListener(() =>
{
2026-07-30 01:06:55 +08:00
// 确认:关闭确认面板,发送离房请求 + 启动进度条卸载
if (obj_DJSPanel != null) obj_DJSPanel.SetActive(false);
RequestLeaveRoom();
2026-07-12 00:26:17 +08:00
});
}
if (btn_QuXiao != null)
{
btn_QuXiao.onClick.RemoveAllListeners();
btn_QuXiao.onClick.AddListener(CancelLeaveCountdown);
}
}
/// <summary>是否正在离开倒计时</summary>
public bool IsLeavingCountdown() => _isLeavingCountdown;
/// <summary>取消离开倒计时</summary>
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;
}
2026-05-04 17:21:52 +08:00
public virtual void OnDestroy()
{
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_Rotate, SyncGameData_Rotate);
2026-05-27 19:47:06 +08:00
EventCenter.Instance.RemoveEventListener<FireCannonSendData>(ConstEventString.SyncGameData_FireBullet, SyncGameData_FireBullit);
2026-05-04 17:21:52 +08:00
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeTargetMonster, SyncGameData_ChangeTargetMonster);
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_RendromDieFish, SyncGameData_RendromDieFish);
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_TurretState, SyncGameData_TurretState);
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeTurret, SyncGameData_ChangeTurret);
2026-06-19 18:18:04 +08:00
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeCannonMultiple, SyncGameData_ChangeCannonMultiple);
2026-06-30 01:31:53 +08:00
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeWing, SyncGameData_ChangeWing);
2026-08-28 22:33:35 +08:00
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeZhanShen, SyncGameData_ChangeZhanShen);
2026-07-25 23:05:24 +08:00
// SyncGameData_FreezeFish 已废弃:冰冻改由服务端 FishFrozen 事件统一触发
EventCenter.Instance.RemoveEventListener(ConstEventString.FishFrozen, OnFishFrozen);
2026-07-02 18:28:50 +08:00
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_BotFireAngle, SyncGameData_BotFireAngle);
2026-07-30 22:09:55 +08:00
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.BaoJiangEnd, OnBaoJiangEnd);
2026-05-04 17:21:52 +08:00
2026-07-13 04:48:18 +08:00
// ⭐ 离场清理:子弹池 + 特效池彻底清空,防止内存泄漏
BulletPool.Instance.ClearAll();
EffectPool.Instance.ClearAll();
2026-07-30 22:09:55 +08:00
// ⭐ 清理未完成的 not-self 爆奖 pending(避免悬空引用)
BaoJiangNotSelfHandler.ClearAll();
2026-05-04 17:21:52 +08:00
}
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)
{
2026-07-24 23:47:50 +08:00
// ⭐ 取消锁定开关 → 重置自动锁定激活状态,下次需重新手动锁定激活
_autoLockActivated = false;
2026-05-18 18:39:11 +08:00
// 本地清理锁定
if (currSitData != null)
2026-06-13 18:23:25 +08:00
{
2026-05-18 18:39:11 +08:00
currSitData.ClearLock();
2026-06-13 18:23:25 +08:00
SyncTurretState(TurretState.Idle);
}
2026-05-18 18:39:11 +08:00
// 同步给服务器:取消锁定
2026-05-04 17:21:52 +08:00
SyncGameData sgd = new SyncGameData
{
sitPos = currSitData.posID,
frameType = FrameDataType.changeAutoTarget,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
frameData = new FrameData_ChangeAutoTarget { targetGuid = null }
};
WebSocketSendList.SyncGameData(sgd);
}
});
}
2026-05-18 18:39:11 +08:00
toggle_BaoZou.onValueChanged.AddListener(on =>
{
2026-07-24 23:47:50 +08:00
if (!on)
{
// ⭐ 打开后只能等倒计时结束自动关闭,不允许玩家手动关闭
// 直接 SetIsOnWithoutNotify(true) 避免递归触发 onValueChanged
toggle_BaoZou.SetIsOnWithoutNotify(true);
return;
}
2026-07-20 22:32:52 +08:00
if (_localBaoZouCount <= 0)
2026-07-19 17:39:31 +08:00
{
2026-07-20 22:32:52 +08:00
toggle_BaoZou.SetIsOnWithoutNotify(false);
return;
2026-07-19 17:39:31 +08:00
}
2026-07-20 22:32:52 +08:00
// 只发请求,效果等 UsePropNotice 回调触发
WebSocketSendList.UseProp(
GameManager.Instance.gameData.currRoomData.roomId,
2026-07-23 01:51:07 +08:00
GameManager.Instance.user.id,
"40004", 0);
2026-05-18 18:39:11 +08:00
});
toggle_SkillAuto.onValueChanged.AddListener(on =>
{
if (!on)
{
// 自动开火关闭 → 立即切回 Idle
currSitData.ChangeState(TurretState.Idle);
2026-06-13 01:25:41 +08:00
SyncTurretState(TurretState.Idle);
2026-05-18 18:39:11 +08:00
}
});
2026-05-28 18:16:45 +08:00
toggle_BingFeng.onValueChanged.AddListener(on => {
2026-07-24 23:47:50 +08:00
if (!on)
{
// ⭐ 打开后只能等倒计时结束自动关闭,不允许玩家手动关闭
// 直接 SetIsOnWithoutNotify(true) 避免递归触发 onValueChanged
toggle_BingFeng.SetIsOnWithoutNotify(true);
return;
}
2026-07-20 22:32:52 +08:00
if (_localBingFengCount <= 0)
2026-05-28 18:16:45 +08:00
{
2026-07-20 22:32:52 +08:00
toggle_BingFeng.SetIsOnWithoutNotify(false);
return;
2026-05-28 18:16:45 +08:00
}
2026-07-20 22:32:52 +08:00
// 只发请求,效果等 UsePropNotice 回调触发
WebSocketSendList.UseProp(
GameManager.Instance.gameData.currRoomData.roomId,
2026-07-23 01:51:07 +08:00
GameManager.Instance.user.id,
"40003", 0);
2026-05-27 19:47:06 +08:00
});
2026-05-04 17:21:52 +08:00
}
2026-05-18 18:39:11 +08:00
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);
}
2026-05-04 17:21:52 +08:00
public void OnEnable()
{
2026-05-27 19:47:06 +08:00
2026-05-04 17:21:52 +08:00
}
2026-05-27 19:47:06 +08:00
int ina = 0;
2026-07-20 01:44:28 +08:00
2026-05-29 17:03:08 +08:00
protected virtual void Update()
2026-05-04 17:21:52 +08:00
{
2026-05-27 19:47:06 +08:00
2026-05-04 17:21:52 +08:00
HandleAutoRotate();
2026-06-14 20:26:21 +08:00
HandleInput();
2026-05-18 18:39:11 +08:00
CheckLockTargetValid();//锁定目标失效自动清除
2026-06-14 20:26:21 +08:00
2026-05-04 17:21:52 +08:00
HandleAutoFire(); // 自动开火真正发射
2026-05-27 19:47:06 +08:00
HandleLock();
2026-07-25 21:04:58 +08:00
HandleAutoLock(); // ⭐ 先切换锁定目标(切换后图标位置同帧更新)
UpdateLockFX(); // 锁定图标跟随目标
2026-06-21 00:28:59 +08:00
RefreshTripleLockIcons();
2026-05-27 19:47:06 +08:00
2026-05-04 17:21:52 +08:00
LoopLaser();
2026-05-18 18:39:11 +08:00
SettingClose();
2026-05-28 18:16:45 +08:00
DaoJiShi();
2026-06-16 19:12:37 +08:00
HandleBotAI();
2026-06-17 21:26:06 +08:00
WebSocketSendList.FlushSyncQueues(Time.deltaTime);
2026-05-04 17:21:52 +08:00
}
2026-05-18 18:39:11 +08:00
float sittingTime = 10;
2026-05-27 19:47:06 +08:00
float currSittingTime;
2026-05-04 17:21:52 +08:00
2026-05-18 18:39:11 +08:00
/// <summary>
/// 等待10秒未操作关闭设置面板
/// </summary>
public void SettingClose()
{
//if (isOpenSetting) StartCoroutine(CloseSettingList());
//else StartCoroutine(OpenSettingList());
2026-05-27 19:47:06 +08:00
if (isOpenSetting)
2026-05-18 18:39:11 +08:00
{
currSittingTime -= Time.deltaTime;
2026-05-27 19:47:06 +08:00
if (currSittingTime <= 0)
2026-05-18 18:39:11 +08:00
{
StartCoroutine(CloseSettingList());
}
}
else
{
currSittingTime = 10;
}
}
2026-05-04 17:21:52 +08:00
#endregion
#region 资源加载模块
public async void LoadResources()
{
CreateFishManager.Instance.isResources = false;
2026-07-25 02:17:19 +08:00
// ⭐ 重置卸载标记,下次进入可重新加载
_resourcesUnloaded = false;
2026-05-04 17:21:52 +08:00
await AudioResourcesData.LoadAudioGame();
JinDuTiao.SetProgress(0.2f);
await MonsterMoveLine.LoadBezier();
JinDuTiao.SetProgress(0.4f);
await FishResourcesData.LoadFishAssetsData();
JinDuTiao.SetProgress(0.6f);
await Addressables.LoadAssetsAsync<AudioClip>("Audio", clip =>
{
if (clip.name == "PW_Shoot")
bulletClip = clip;
}).Task;
JinDuTiao.SetProgress(0.8f);
2026-05-07 03:46:03 +08:00
await Task.Delay(100);
2026-05-04 17:21:52 +08:00
CreateFishManager.Instance.EnterRoom(gameType);
CreateFishManager.Instance.isResources = true;
2026-05-07 03:46:03 +08:00
JinDuTiao.SetProgress(0.9f);
2026-05-04 17:21:52 +08:00
2026-07-25 23:05:24 +08:00
// ⭐ 鱼创建由服务端 OnFishSpawn 事件统一下发,客户端无需房主出鱼/请求同步。
// 玩家进入房间后服务端会主动推送 OnFishSpawn(type="init") 初始鱼,
// 重连/取消挂机时服务端会推送 OnFishSpawn(type="sync") 同步当前鱼。
2026-07-19 15:04:15 +08:00
2026-07-26 22:00:38 +08:00
// ⭐ 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;
}
2026-07-26 22:44:45 +08:00
await Task.Delay(2000);
2026-07-26 22:00:38 +08:00
2026-05-04 17:21:52 +08:00
JinDuTiao.SetProgress(1);
2026-05-07 03:46:03 +08:00
await Task.Delay(300);
2026-05-04 17:21:52 +08:00
JinDuTiao.Hide();
}
public async void UnLoadResources()
{
2026-07-25 02:17:19 +08:00
if (_resourcesUnloaded) return;
2026-05-08 20:44:54 +08:00
CreateFishManager.Instance.ExitRoom();
2026-05-04 17:21:52 +08:00
CreateFishManager.Instance.isResources = false;
await FishResourcesData.UnLoadFishAssetsData();
await AudioResourcesData.UnLoadAudioGame();
await MonsterMoveLine.UnLoadBezier();
2026-07-27 16:59:22 +08:00
// ⭐ 清空子弹池 + 特效池(鱼池已在 UnLoadFishAssetsData 内清理)
BulletPool.Instance.ClearAll();
EffectPool.Instance.ClearAll();
2026-07-25 02:17:19 +08:00
_resourcesUnloaded = true;
}
/// <summary>资源是否已卸载(避免 OnHide 重复卸载)</summary>
private bool _resourcesUnloaded = false;
2026-07-30 01:06:55 +08:00
/// <summary>离房 socket 响应等待信号</summary>
private TaskCompletionSource<bool> _leaveRoomResponseTcs;
/// <summary>正在离开的游戏面板类型(完成后关闭它切到 home)</summary>
private UIPanel _leavingGamePanelType;
2026-07-25 02:17:19 +08:00
/// <summary>
2026-07-30 01:06:55 +08:00
/// 请求离开房间:发送离房 socket 请求 + 启动进度条卸载资源。
/// 进度条走到 0.9 后等待 socket 返回 LeaveRoomResult,
/// 收到返回后走完剩余进度并切回 home 界面。
2026-07-25 02:17:19 +08:00
/// </summary>
2026-07-30 01:06:55 +08:00
public void RequestLeaveRoom()
{
if (_leaveRoomResponseTcs != null) return; // 防重复请求
// 先创建等待信号(在发送请求前创建,避免响应太快丢失)
_leaveRoomResponseTcs = new TaskCompletionSource<bool>();
// 发送离房请求
WebSocketSendList.LeaveRoom(
GameManager.Instance.gameData.currRoomData.roomId,
currSitData.user);
//CreateFishManager.Instance.fangzhu = false;
// 启动带进度条的卸载流程
BeginLeaveRoomUnload();
}
/// <summary>
/// 离房卸载流程:进度条到 0.9 后等待 socket 响应,
/// 收到信号后走到 1.0 并切回 home 界面。
2026-08-04 18:17:09 +08:00
/// ⭐ 用 try-finally 保证无论中间是否异常,最终一定会关闭 GamePanel 并切回 Home
2026-07-30 01:06:55 +08:00
/// </summary>
private async void BeginLeaveRoomUnload()
2026-07-25 02:17:19 +08:00
{
if (_resourcesUnloaded)
{
2026-07-30 01:06:55 +08:00
_leaveRoomResponseTcs = null;
2026-07-25 02:17:19 +08:00
return;
}
2026-08-04 18:17:09 +08:00
try
2026-07-30 01:06:55 +08:00
{
2026-08-04 18:17:09 +08:00
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)
2026-07-30 01:06:55 +08:00
{
2026-08-04 18:17:09 +08:00
bool alreadyReceived = _leaveRoomResponseTcs.Task.IsCompleted;
if (alreadyReceived)
2026-07-30 01:06:55 +08:00
{
2026-08-04 18:17:09 +08:00
// 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}");
}
}
2026-07-30 01:06:55 +08:00
}
}
2026-08-04 18:17:09 +08:00
// 标记已卸载,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 界面(关闭当前游戏面板)
2026-08-05 14:18:41 +08:00
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);
2026-07-30 01:06:55 +08:00
}
}
/// <summary>
/// socket 收到 LeaveRoomResult 后调用,解除 0.9 的等待。
/// </summary>
public void OnLeaveRoomResultReceived(UIPanel gamePanel)
{
_leavingGamePanelType = gamePanel;
_leaveRoomResponseTcs?.TrySetResult(true);
2026-05-04 17:21:52 +08:00
}
#endregion
#region 输入系统(InputModule)
2026-05-18 18:39:11 +08:00
private bool IsPointerOverUI(Vector2 pos)
{
PointerEventData eventData = new PointerEventData(EventSystem.current);
eventData.position = pos;
List<RaycastResult> results = new List<RaycastResult>();
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;
}
2026-05-04 17:21:52 +08:00
private void HandleInput()
{
2026-07-19 14:27:00 +08:00
// ⭐ 退出倒计时期间屏蔽本地玩家手动输入(旋转/开火/锁定),其他玩家和机器人不受影响
if (_isLeavingCountdown) return;
2026-05-18 18:39:11 +08:00
CrossPlatformInput input = GetInput();
if (!input.isPressed && !input.wasPressedThisFrame && !input.wasReleasedThisFrame)
return;
2026-06-14 02:18:37 +08:00
if (input.wasPressedThisFrame)
{
// ⭐ UI 点击过滤
if (IsPointerOverUI(input.position))
return;
}
2026-05-18 18:39:11 +08:00
2026-05-04 17:21:52 +08:00
// 按下
2026-05-18 18:39:11 +08:00
if (input.wasPressedThisFrame)
2026-05-04 17:21:52 +08:00
{
2026-05-18 18:39:11 +08:00
isChangingTurret = CheckTouchIsChangeTurret(input.position);
2026-05-04 17:21:52 +08:00
clickTime = attackTime;
if (mySitAnim != null && mySitAnim.gameObject.activeSelf)
mySitAnim.gameObject.SetActive(false);
2026-07-28 18:29:49 +08:00
// XJMH 模式:检测是否按在当前炮台上(长按触发扇形切换)
_isPendingTurretLongPress = !isChangingTurret
&& currSitData.turretControlMode == TurretControlMode.XJMH
&& IsTouchOnCurrentTurret(input.position);
2026-05-04 17:21:52 +08:00
if (!isChangingTurret)
2026-06-13 01:25:41 +08:00
{
2026-08-01 13:42:35 +08:00
// ⭐ 本地金币校验:没钱不切换 Attack 状态,也不广播,避免无效请求
if (!HasEnoughGoldToFire(toggle_Three.isOn))
return;
2026-07-28 18:29:49 +08:00
// 按下即进入 Attack:短按→射击,长按超过阈值后切回 Idle 弹扇形
2026-05-04 17:21:52 +08:00
currSitData.ChangeState(TurretState.Attack);
2026-06-13 01:25:41 +08:00
SyncTurretState(TurretState.Attack);
}
2026-05-04 17:21:52 +08:00
}
// 长按
2026-05-18 18:39:11 +08:00
if (input.isPressed)
2026-05-04 17:21:52 +08:00
{
2026-07-28 18:29:49 +08:00
// 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;
}
2026-05-04 17:21:52 +08:00
if (isChangingTurret)
{
HandleChangeTurretHold();
return;
}
2026-07-28 18:29:49 +08:00
// XJMH 待判定长按:不旋转(手指在炮台上旋转无意义),但仍允许开火(短按=射击)
if (_isPendingTurretLongPress)
{
TryFire();
return;
}
2026-06-15 00:34:19 +08:00
// 锁定模式:不手动旋转/开火,交给 HandleAutoRotate + HandleAutoFire
2026-08-29 17:28:00 +08:00
// ⭐ 特殊鱼爆奖期间例外:自动锁定已禁用,玩家点击仍可手动旋转炮台瞄准发射方向
if (toggle_Lock.isOn && !(currSitData != null && currSitData.isDrillActive)) return;
2026-06-15 00:34:19 +08:00
2026-06-15 21:51:42 +08:00
// UI 上不旋转不发射
if (IsPointerOverUI(input.position)) return;
2026-06-15 00:34:19 +08:00
2026-05-18 18:39:11 +08:00
HandleRotate(input.position);
2026-05-04 17:21:52 +08:00
TryFire();
}
// 松开
2026-05-18 18:39:11 +08:00
if (input.wasReleasedThisFrame)
2026-05-04 17:21:52 +08:00
{
2026-07-28 18:29:49 +08:00
// XJMH 扇形切换释放:根据滑动位置选择炮台
if (_isTurretSwitcherActive)
{
_isTurretSwitcherActive = false;
_isPendingTurretLongPress = false;
changeTurretTime = 0;
OnTurretSwitcherHide(input.position);
return;
}
// XJMH 短按炮台释放(未触发长按)→ 正常回到 Idle
if (_isPendingTurretLongPress)
{
_isPendingTurretLongPress = false;
changeTurretTime = 0;
}
2026-05-04 17:21:52 +08:00
if (isChangingTurret)
{
isChangingTurret = false;
changeTurretTime = 0;
CloseChangeTurretUI();
return;
}
currSitData.ChangeState(TurretState.Idle);
2026-06-13 01:25:41 +08:00
SyncTurretState(TurretState.Idle);
2026-05-04 17:21:52 +08:00
}
}
2026-05-18 18:39:11 +08:00
2026-05-04 17:21:52 +08:00
private void HandleChangeTurretHold()
{
changeTurretTime += Time.deltaTime;
if (changeTurretTime > 1f)
{
2026-06-08 21:23:40 +08:00
// 长按换炮台:已改为按钮打开 TurretPanel
2026-05-04 17:21:52 +08:00
}
}
2026-05-18 18:39:11 +08:00
private bool CheckTouchIsChangeTurret(Vector2 pos)
2026-05-04 17:21:52 +08:00
{
PointerEventData pointerData = new PointerEventData(EventSystem.current);
2026-05-18 18:39:11 +08:00
pointerData.position = pos;
2026-05-04 17:21:52 +08:00
List<RaycastResult> results = new List<RaycastResult>();
EventSystem.current.RaycastAll(pointerData, results);
foreach (var result in results)
{
if (result.gameObject.CompareTag("ChangeTurret"))
{
SitPosData sit = result.gameObject.GetComponentInParent<SitPosData>();
if (sit == currSitData)
return true;
}
}
return false;
}
2026-05-18 18:39:11 +08:00
2026-05-04 17:21:52 +08:00
private void CloseChangeTurretUI()
{
2026-06-08 21:23:40 +08:00
// currSitData.changeTurretList.SetActive(false); — 已废弃
2026-05-04 17:21:52 +08:00
changeTurretTime = 0;
isChangingTurret = false;
}
2026-07-28 18:29:49 +08:00
// ==================== XJMH 炮台扇形切换 ====================
/// <summary>
/// 检测触摸点是否在当前炮台的 RectTransform 范围内(XJMH 长按切换用)。
/// </summary>
protected bool IsTouchOnCurrentTurret(Vector2 screenPos)
{
if (currSitData == null || currSitData.currentTurret == null) return false;
RectTransform rt = currSitData.currentTurret.GetComponent<RectTransform>();
if (rt == null) return false;
Canvas canvas = UIManager.Instance != null && UIManager.Instance.uiRoot != null
? UIManager.Instance.uiRoot.GetComponent<Canvas>()
: null;
Camera cam = canvas != null && canvas.renderMode != RenderMode.ScreenSpaceOverlay ? canvas.worldCamera : null;
return RectTransformUtility.RectangleContainsScreenPoint(rt, screenPos, cam);
}
/// <summary>长按炮台达到阈值后弹出扇形切换UI(子类覆写实现具体UI)。</summary>
protected virtual void OnTurretSwitcherShow(Vector2 pressPos) { }
/// <summary>扇形切换UI显示期间,手指滑动时更新高亮(子类覆写)。</summary>
protected virtual void OnTurretSwitcherUpdate(Vector2 currentPos) { }
/// <summary>手指松开,关闭扇形切换UI并根据滑动位置切换炮台(子类覆写)。</summary>
protected virtual void OnTurretSwitcherHide(Vector2 releasePos) { }
/// <summary>清理扇形切换UI状态(面板隐藏时调用)</summary>
protected virtual void CleanupTurretSwitcher()
{
_isTurretSwitcherActive = false;
_isPendingTurretLongPress = false;
changeTurretTime = 0;
}
2026-05-04 17:21:52 +08:00
#endregion
#region 打开关闭右上角设置选项
public virtual IEnumerator OpenSettingList()
{
RectTransform tran = SittingListParent.GetComponent<RectTransform>();
tran.gameObject.SetActive(true);
tran.DOSizeDelta(new Vector2(466, 143), 0.2f);
isOpenSetting = true;
Vector3 v3 = btn_SettingList.transform.localScale;
v3.x = Mathf.Abs(v3.x) * -1;
btn_SettingList.transform.localScale = v3;
yield break;
}
public virtual IEnumerator CloseSettingList()
{
RectTransform tran = SittingListParent.GetComponent<RectTransform>();
Vector3 v3 = btn_SettingList.transform.localScale;
v3.x = Mathf.Abs(v3.x);
btn_SettingList.transform.localScale = v3;
tran.DOSizeDelta(new Vector2(0, 143), 0.2f).OnComplete(() =>
{
tran.gameObject.SetActive(false);
});
isOpenSetting = false;
yield break;
}
#endregion
#region ② 炮台旋转系统(RotateModule)
2026-05-18 18:39:11 +08:00
2026-05-19 17:59:23 +08:00
//private void HandleRotate(Vector2 pos)
//{
// if (toggle_Lock.isOn && currSitData.monCotl_Lock != null)
// {
// RotateToLockTarget(currSitData);
// }
// else
// {
// RotateToTouch(currSitData, pos);
// }
//}
2026-05-18 18:39:11 +08:00
private void HandleRotate(Vector2 pos)
2026-05-04 17:21:52 +08:00
{
2026-05-19 17:59:23 +08:00
// ⭐ 只有真正锁定目标时才自动旋转
if (toggle_Lock.isOn && currSitData.isLock && currSitData.monCotl_Lock != null)
2026-05-04 17:21:52 +08:00
{
RotateToLockTarget(currSitData);
}
else
{
2026-05-19 17:59:23 +08:00
// 锁定开关开着但没有锁定目标 → 允许手动旋转
2026-05-18 18:39:11 +08:00
RotateToTouch(currSitData, pos);
2026-05-04 17:21:52 +08:00
}
}
2026-05-18 18:39:11 +08:00
private void RotateToTouch(SitPosData sit, Vector2 pos)
2026-05-04 17:21:52 +08:00
{
2026-08-04 15:47:55 +08:00
// ⭐ 防御:炮台旋转节点可能尚未初始化(刚进房/切换炮台中)
if (sit == null || sit.tra_CurrTurret == null) return;
2026-05-04 17:21:52 +08:00
RectTransformUtility.ScreenPointToLocalPointInRectangle(
sit.tra_CurrTurret.transform.parent as RectTransform,
2026-05-18 18:39:11 +08:00
pos, null, out Vector2 localPos);
2026-05-04 17:21:52 +08:00
Vector2 dir = localPos - sit.tra_CurrTurret.GetComponent<RectTransform>().anchoredPosition;
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg - 90f;
2026-06-13 01:25:41 +08:00
// 上方座位(2,3)炮台预制体默认朝上,加180°让它朝下打鱼
if (sit.IsFlipped) angle = (angle + 180f) % 360f;
2026-05-04 17:21:52 +08:00
sit.SetRotation(angle);
SyncRotate(angle);
}
2026-05-18 18:39:11 +08:00
2026-05-04 17:21:52 +08:00
private void RotateToLockTarget(SitPosData sit)
{
2026-08-29 01:02:09 +08:00
// ⭐ 防御:锁定点为空或已被销毁(鱼死亡清锁/小白龙切模型销毁锚点)时直接跳过,
// 否则对空引用取 .position 会抛 NRE 中断整个 Update(锁定/开火/清锁全部卡死)
if (sit == null || sit.monCotl_Lock_Position == null) return;
2026-05-04 17:21:52 +08:00
Vector2 screenPos = Camera.main.WorldToScreenPoint(sit.monCotl_Lock_Position.position);
2026-05-18 18:39:11 +08:00
2026-05-04 17:21:52 +08:00
RectTransformUtility.ScreenPointToLocalPointInRectangle(
sit.tra_CurrTurret.transform.parent as RectTransform,
screenPos, null, out Vector2 localPos);
Vector2 dir = localPos - sit.tra_CurrTurret.GetComponent<RectTransform>().anchoredPosition;
2026-06-20 15:00:15 +08:00
// ⭐ 鱼太近时 dir 接近零向量,Atan2 会不稳定导致原地转圈 → 跳过
const float minDistance = 5f;
if (dir.sqrMagnitude < minDistance * minDistance) return;
2026-05-04 17:21:52 +08:00
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg - 90f;
2026-06-13 01:25:41 +08:00
if (sit.IsFlipped) angle = (angle + 180f) % 360f;
2026-05-04 17:21:52 +08:00
sit.SetRotation(angle);
2026-06-20 15:00:15 +08:00
SyncRotate(angle);
2026-05-04 17:21:52 +08:00
}
2026-05-18 18:39:11 +08:00
2026-05-04 17:21:52 +08:00
private void HandleAutoRotate()
{
2026-07-19 14:27:00 +08:00
// ⭐ 退出倒计时期间屏蔽本地玩家自动旋转,其他玩家和机器人不受影响
if (_isLeavingCountdown) return;
2026-05-19 17:59:23 +08:00
2026-08-29 17:28:00 +08:00
// ⭐ 特殊鱼爆奖(钻头/激光)期间禁用自动旋转:锁定跟随/自动扫射都让位,炮台方向只由玩家点击控制
if (currSitData != null && currSitData.isDrillActive) return;
2026-05-04 17:21:52 +08:00
// 锁定模式优先
if (toggle_Lock.isOn && currSitData.isLock)
{
RotateToLockTarget(currSitData);
return;
}
2026-05-19 17:59:23 +08:00
// 自动开火关闭 → 不自动旋转
if (!toggle_SkillAuto.isOn) return;
2026-05-04 17:21:52 +08:00
// 自动扫射(左右摆动)
AutoSweepRotate();
}
private float autoRotateAngle = 0;
private float autoRotateSpeed = 40f;
private float autoRotateMin = -60f;
private float autoRotateMax = 60f;
private int autoRotateDir = 1;
private void AutoSweepRotate()
{
autoRotateAngle += autoRotateSpeed * autoRotateDir * Time.deltaTime;
if (autoRotateAngle > autoRotateMax)
{
autoRotateAngle = autoRotateMax;
autoRotateDir = -1;
}
else if (autoRotateAngle < autoRotateMin)
{
autoRotateAngle = autoRotateMin;
autoRotateDir = 1;
}
2026-06-13 01:25:41 +08:00
// 上方座位加180°,炮台朝下打鱼
float angle = currSitData.IsFlipped ? (autoRotateAngle + 180f) % 360f : autoRotateAngle;
currSitData.SetRotation(angle);
2026-05-04 17:21:52 +08:00
}
2026-07-20 01:44:28 +08:00
/// <summary>
/// 远程玩家/bot 旋转:直接把目标角度赋给炮台,不做任何过渡。
/// 旋转和开炮都改成实时同步,收到广播立即应用。
/// </summary>
2026-05-04 17:21:52 +08:00
private void SmoothRotate(SitPosData sit, float angle)
{
2026-07-20 01:44:28 +08:00
if (sit == null || sit.tra_CurrTurret == null) return;
sit.SetRotation(angle);
2026-05-04 17:21:52 +08:00
}
#endregion
2026-05-27 19:47:06 +08:00
#region Skill系统(SkillModule)
public void DaoJiShi()
{
2026-05-29 17:03:08 +08:00
// 暴走倒计时
if (toggle_BaoZou != null && toggle_BaoZou.isOn && frenzyDuration > 0)
2026-05-27 19:47:06 +08:00
{
2026-05-29 17:03:08 +08:00
if (img_BaoZou != null)
img_BaoZou.fillAmount = Mathf.Clamp01(currFrenzyTime / frenzyDuration);
2026-05-27 19:47:06 +08:00
currFrenzyTime -= Time.deltaTime;
2026-05-29 17:03:08 +08:00
2026-05-27 19:47:06 +08:00
if (currFrenzyTime <= 0)
{
2026-07-24 23:47:50 +08:00
// ⭐ 倒计时结束自动关闭:用 SetIsOnWithoutNotify 避免触发 onValueChanged
// (onValueChanged 里禁止手动关闭会强制设回 true)
// 并清理狂暴状态(攻速/动画/isFrenzy 标志)
toggle_BaoZou.SetIsOnWithoutNotify(false);
2026-05-29 17:03:08 +08:00
currFrenzyTime = 0f;
if (img_BaoZou != null) img_BaoZou.fillAmount = 1f;
2026-07-24 23:47:50 +08:00
CreateFishManager.Instance.currGameType.isFrenzy = false;
SetFrenzy(false);
2026-05-27 19:47:06 +08:00
}
}
2026-05-29 17:03:08 +08:00
// 冰封倒计时
if (toggle_BingFeng != null && toggle_BingFeng.isOn && bingFengDuration > 0)
2026-05-27 19:47:06 +08:00
{
2026-05-29 17:03:08 +08:00
if (img_BingFeng != null)
img_BingFeng.fillAmount = Mathf.Clamp01(currBingFengTime / bingFengDuration);
2026-05-27 19:47:06 +08:00
currBingFengTime -= Time.deltaTime;
if (currBingFengTime <= 0)
{
2026-07-24 02:08:49 +08:00
toggle_BingFeng.SetIsOnWithoutNotify(false);
2026-05-29 17:03:08 +08:00
currBingFengTime = 0f;
if (img_BingFeng != null) img_BingFeng.fillAmount = 1f;
2026-07-25 23:05:24 +08:00
// ⭐ 冰冻结束:本地解冻(服务端不再下发解冻事件,15s 后客户端自动解冻)
// 之前 SyncFreeze(false) 客户端广播已废弃
2026-07-24 02:08:49 +08:00
CreateFishManager.Instance.UnfreezeAllFish();
ResumeAllFishAnimations();
if (_bingFengInstance != null) _bingFengInstance.SetActive(false);
2026-05-27 19:47:06 +08:00
}
}
}
#endregion
2026-05-04 17:21:52 +08:00
#region ③ 攻击系统(FireModule)
private void TryFire()
{
2026-08-28 18:57:36 +08:00
// ⭐ 特殊鱼炮台(钻头)活动期间禁用普通开火,点击只发射钻头
if (currSitData != null && currSitData.isDrillActive) return;
2026-05-04 17:21:52 +08:00
clickTime += Time.deltaTime;
if (clickTime < attackTime) return;
2026-08-01 13:42:35 +08:00
// ⭐ 本地金币校验:没钱直接返回,不发送开炮请求
// clickTime 保持满,金币恢复后立即开火
if (!HasEnoughGoldToFire(toggle_Three.isOn))
return;
2026-06-20 15:00:15 +08:00
// ⭐ 子弹上限校验(先于扣费和状态切换)
if (IsBulletLimitReached(currSitData.user))
{
clickTime = 0; // 重置计时,下次再试
return;
}
2026-05-04 17:21:52 +08:00
clickTime = 0;
float angle = currSitData.currentAngle;
bool isSanShe = toggle_Three.isOn;
bool isDouble = toggle_Shuang.isOn;
bool isLock = toggle_Lock.isOn;
2026-05-27 19:47:06 +08:00
//FireBullet(currSitData, angle, isSanShe, isDouble, isLock);
2026-05-04 17:21:52 +08:00
SyncFire(angle, isSanShe, isDouble, isLock);
}
2026-08-29 01:02:09 +08:00
/// <summary>战神激光命中延迟(秒):不开实体子弹,开火后延迟此时间直接发 KillFish 给后端(模拟弹道飞行)</summary>
[Tooltip("战神激光命中延迟(秒):不开实体子弹,开火后延迟此时间直接发 KillFish 给后端")]
public float laserHitDelay = 0.2f;
2026-05-28 18:16:45 +08:00
private void FireBullet(SitPosData sit, float angle, bool isSanShe, bool isDouble, bool isLock, List<string> bullitIDs,
2026-07-30 23:54:35 +08:00
List<string> fireTargetMonsterIDs = null, List<long> bulletGolds = null)
2026-05-04 17:21:52 +08:00
{
2026-07-31 17:57:24 +08:00
// ⭐ 防御:sit 或 bullitIDs 为空时直接返回,避免空引用
if (sit == null || bullitIDs == null || bullitIDs.Count == 0)
{
Debug.LogWarning("[FireBullet] sit 或 bullitIDs 为空,跳过本次开火");
return;
}
2026-08-29 01:02:09 +08:00
// ⭐ 战神激光模式(锁定中):不创建实体子弹,延迟后直接发 KillFish 给后端
if (sit.currentZhanShenData != null && isLock)
{
FireLaser(sit, isDouble, isSanShe, bullitIDs, fireTargetMonsterIDs, bulletGolds);
return;
}
2026-05-04 17:21:52 +08:00
Vector2 startPos = sit.GetBulletStartPos();
2026-05-28 18:16:45 +08:00
2026-07-30 23:54:35 +08:00
// ⭐ 按索引取该发子弹的服务端权威扣费金额(bullets[i].gold),没有则回退到0(ConsumeGold内部用consumeGoldNum)
long GetBulletGold(int i)
{
if (bulletGolds != null && i < bulletGolds.Count)
return bulletGolds[i];
return 0;
}
2026-07-30 12:54:22 +08:00
// ⭐ 三发 + 锁定:每发子弹优先瞄准对应目标,目标不足或失效则按默认角度发射
// 必须保证三发子弹都创建,否则金币消耗和后端对不上
if (isSanShe && isLock)
2026-05-28 18:16:45 +08:00
{
2026-07-30 12:54:22 +08:00
// 三发默认角度(无目标时用):左 -20,中 0,右 +20
float[] defaultAngles = { angle - 20, angle, angle + 20 };
for (int i = 0; i < 3; i++)
2026-05-28 18:16:45 +08:00
{
2026-07-30 12:54:22 +08:00
string bulletId = i < bullitIDs.Count ? bullitIDs[i] : bullitIDs[0];
2026-05-28 18:16:45 +08:00
MonsterContral targetMonster = null;
Transform targetHitPos = null;
2026-07-30 12:54:22 +08:00
// 尝试按 GUID 查找目标
if (fireTargetMonsterIDs != null && i < fireTargetMonsterIDs.Count)
2026-05-28 18:16:45 +08:00
{
2026-07-30 12:54:22 +08:00
if (CreateFishManager.Instance.fishDatas_hashCodekey.TryGetValue(fireTargetMonsterIDs[i], out var m)
&& m != null && m.gameObject.activeInHierarchy
2026-07-31 17:57:24 +08:00
// ⭐ 手动锁定的主目标(i==0 且是本地玩家主锁)不受路径进度限制
&& ((i == 0 && sit == currSitData && m == currSitData.monCotl_Lock && currSitData.isManualLock)
|| IsFishPathLockable(m)))
2026-07-30 12:54:22 +08:00
{
targetMonster = m;
targetHitPos = ResolveLockTransform(m);
}
2026-05-28 18:16:45 +08:00
}
2026-07-31 17:31:51 +08:00
if (targetMonster != null && targetHitPos != null)
2026-05-28 18:16:45 +08:00
{
2026-07-30 12:54:22 +08:00
// 朝目标发射
2026-05-28 18:16:45 +08:00
Vector3 targetWorldPos = targetHitPos.position;
Vector2 dir = ((Vector2)targetWorldPos - startPos).normalized;
float targetAngle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg - 90f;
2026-07-30 23:54:35 +08:00
CreateBullet(sit, startPos, targetAngle, isDouble, true, bulletId, targetMonster, targetHitPos, GetBulletGold(i));
2026-05-28 18:16:45 +08:00
}
2026-07-30 12:54:22 +08:00
else
{
// 目标不足或失效 → 按默认角度发射(不锁定)
2026-07-30 23:54:35 +08:00
CreateBullet(sit, startPos, defaultAngles[i], isDouble, isLock, bulletId, null, null, GetBulletGold(i));
2026-07-30 12:54:22 +08:00
}
2026-05-28 18:16:45 +08:00
}
}
else if (isSanShe)
2026-05-04 17:21:52 +08:00
{
2026-07-31 17:57:24 +08:00
// ⭐ 安全取值:bullitIDs 不足3个时复用最后一个,避免越界
string SafeBid(int i) => i < bullitIDs.Count ? bullitIDs[i] : bullitIDs[bullitIDs.Count - 1];
CreateBullet(sit, startPos, angle - 20, isDouble, isLock, SafeBid(0), null, null, GetBulletGold(0));
CreateBullet(sit, startPos, angle, isDouble, isLock, SafeBid(1), null, null, GetBulletGold(1));
CreateBullet(sit, startPos, angle + 20, isDouble, isLock, SafeBid(2), null, null, GetBulletGold(2));
2026-05-04 17:21:52 +08:00
}
else
{
2026-07-30 23:54:35 +08:00
CreateBullet(sit, startPos, angle, isDouble, isLock, bullitIDs[0], null, null, GetBulletGold(0));
2026-05-04 17:21:52 +08:00
}
2026-07-22 03:41:06 +08:00
if (sit.posID==currSitData.posID)
{
AudioManager.Instance.PlayFX(bulletClip);
2026-05-04 17:21:52 +08:00
2026-07-22 03:41:06 +08:00
}
2026-05-04 17:21:52 +08:00
}
2026-08-29 01:02:09 +08:00
/// <summary>
/// 战神激光开火(锁定中):不创建实体子弹(视觉就是 LineRenderer 激光线),
/// 每发按正常频率走 FireCannon/广播(频率不变),本地扣费后延迟 laserHitDelay 秒
/// 直接把 KillFish 发给后端结算(模拟弹道飞行时间)。
/// 目标:三发=锁定目标列表(与三条激光线一致),单发=主锁定目标。
/// </summary>
private void FireLaser(SitPosData sit, bool isDouble, bool isSanShe, List<string> bullitIDs,
List<string> fireTargetMonsterIDs, List<long> bulletGolds)
{
// ---- 收集激光目标(与激光线一一对应)----
var targets = new List<MonsterContral>();
if (isSanShe)
{
// 三发:优先用开火时随帧同步的目标 GUID
if (fireTargetMonsterIDs != null)
{
foreach (var guid in fireTargetMonsterIDs)
{
if (CreateFishManager.Instance.fishDatas_hashCodekey.TryGetValue(guid, out var m)
&& m != null && !m.isDead && m.gameObject.activeInHierarchy)
targets.Add(m);
}
}
// 兜底:座位上的三发锁定列表
if (targets.Count == 0)
{
foreach (var m in sit.tripleLockTargets)
if (m != null && !m.isDead && m.gameObject.activeInHierarchy)
targets.Add(m);
}
}
// 单发 / 三发兜底:主锁定目标
if (targets.Count == 0 && sit.monCotl_Lock != null
&& !sit.monCotl_Lock.isDead && sit.monCotl_Lock.gameObject.activeInHierarchy)
targets.Add(sit.monCotl_Lock);
// ---- 每发:扣费 + 延迟发 KillFish(无目标也扣费,与子弹打空一致)----
for (int i = 0; i < bullitIDs.Count; i++)
{
long gold = bulletGolds != null && i < bulletGolds.Count ? bulletGolds[i] : 0;
if (!sit.ConsumeGold(isDouble ? 2 : 1, gold))
return; // 没钱,后续不发
MonsterContral target = targets.Count > 0 ? targets[Mathf.Min(i, targets.Count - 1)] : null;
StartCoroutine(LaserHitRoutine(sit, target, bullitIDs[i]));
}
if (sit.posID == currSitData.posID)
AudioManager.Instance.PlayFX(bulletClip);
}
/// <summary>
/// 激光命中协程:延迟 laserHitDelay 后结算(不开实体子弹)。
/// 结算逻辑与 BullitMove.AttackFish 一致:本人发 KillFish、bot 由房主发 BotKillFish、本地播 OnHurt。
/// </summary>
private System.Collections.IEnumerator LaserHitRoutine(SitPosData sit, MonsterContral target, string bulletId)
{
if (laserHitDelay > 0f)
yield return new WaitForSeconds(laserHitDelay);
else
yield return null;
// 延迟期间目标死了/失效 → 本发打空(钱已扣,与子弹未命中一致)
if (target == null || target.isDead || !target.gameObject.activeInHierarchy)
yield break;
if (sit.user == GameManager.Instance.user.id)
{
// 真人玩家击杀:直接发后端
var v2 = GameManager.Instance.gameData.currRoomData;
WebSocketSendList.KillFish(v2.roomId, sit.user,
target.monsterData.fishTypeConfig.fishNumber, target.guid,
(int)sit.consumeGoldNum, bulletId, "63215f0afa3f5b25");
}
else if (!string.IsNullOrEmpty(sit.user) && sit.user.StartsWith("bot_"))
{
// 机器人击杀:只有房主客户端发送 BotKillFish
if (SitPosData.roomOwnerSit != null && SitPosData.roomOwnerSit.user == GameManager.Instance.user.id)
{
var v2 = GameManager.Instance.gameData.currRoomData;
WebSocketSendList.BotKillFish(
v2.roomId,
sit.user,
target.monsterData.fishTypeConfig.fishNumber,
target.guid,
sit.consumeGoldNum,
bulletId
);
}
}
target.OnHurt();
}
2026-05-04 17:21:52 +08:00
private bool isLaser = false;
private void StartLaser()
{
isLaser = true;
currSitData.ChangeState(TurretState.LaserStart);
2026-06-13 01:25:41 +08:00
SyncTurretState(TurretState.LaserStart);
2026-05-04 17:21:52 +08:00
}
private void LoopLaser()
{
if (!isLaser) return;
// TODO: 激光持续伤害 / 特效
}
private void StopLaser()
{
isLaser = false;
currSitData.ChangeState(TurretState.LaserEnd);
2026-06-13 01:25:41 +08:00
SyncTurretState(TurretState.LaserEnd);
2026-05-04 17:21:52 +08:00
}
private float autoFireTimer = 0f;
2026-05-18 18:39:11 +08:00
//private void HandleAutoFire()
//{
// if (!toggle_SkillAuto.isOn) return;
// if (currSitData == null) return;
// autoFireTimer += Time.deltaTime;
// if (autoFireTimer < attackTime) return;
// autoFireTimer = 0f;
// float angle = currSitData.currentAngle;
// bool isSanShe = toggle_Three.isOn;
// bool isDouble = toggle_Shuang.isOn;
// bool isLock = toggle_Lock.isOn;
// FireBullet(currSitData, angle, isSanShe, isDouble, isLock);
// SyncFire(angle, isSanShe, isDouble, isLock);
//}
2026-05-04 17:21:52 +08:00
private void HandleAutoFire()
{
if (currSitData == null) return;
2026-07-19 14:27:00 +08:00
// ⭐ 退出倒计时期间屏蔽本地玩家自动开火,其他玩家和机器人不受影响
if (_isLeavingCountdown) return;
2026-08-28 18:57:36 +08:00
// ⭐ 特殊鱼炮台(钻头)活动期间禁用普通自动开火
if (currSitData.isDrillActive) return;
2026-05-04 17:21:52 +08:00
2026-05-27 19:47:06 +08:00
bool needAutoFire = toggle_SkillAuto.isOn || (toggle_Lock.isOn && currSitData.isLock && currSitData.lockFX.gameObject.activeSelf);
2026-05-18 18:39:11 +08:00
if (!needAutoFire)
{
autoFireTimer = 0f;
return;
}
2026-05-04 17:21:52 +08:00
autoFireTimer += Time.deltaTime;
if (autoFireTimer < attackTime) return;
2026-08-01 13:42:35 +08:00
// ⭐ 本地金币校验:没钱直接返回,不切换攻击状态、不发送开炮请求
// autoFireTimer 保持满,金币恢复后立即开火
if (!HasEnoughGoldToFire(toggle_Three.isOn))
return;
2026-06-20 15:00:15 +08:00
// ⭐ 子弹上限校验(先于状态切换和扣费)
if (IsBulletLimitReached(currSitData.user))
{
autoFireTimer = 0f; // 重置计时,下次再试
return;
}
2026-05-04 17:21:52 +08:00
autoFireTimer = 0f;
float angle = currSitData.currentAngle;
bool isSanShe = toggle_Three.isOn;
bool isDouble = toggle_Shuang.isOn;
bool isLock = toggle_Lock.isOn;
2026-05-18 18:39:11 +08:00
// ⭐ 自动攻击也要播放攻击动画
currSitData.ChangeState(TurretState.Attack);
2026-06-13 01:25:41 +08:00
SyncTurretState(TurretState.Attack);
2026-05-18 18:39:11 +08:00
2026-05-27 19:47:06 +08:00
//FireBullet(currSitData, angle, isSanShe, isDouble, isLock);
2026-05-04 17:21:52 +08:00
SyncFire(angle, isSanShe, isDouble, isLock);
}
2026-05-18 18:39:11 +08:00
2026-07-16 20:16:05 +08:00
#endregion
#region 锁定位置解析(统一工具)
/// <summary>
/// 统一锁定位置解析:colliderList 第一个活跃碰撞器 → lockPoint → monster.transform
/// 所有设置锁定位置的地方都应该调用此方法,避免不同地方用不同优先级导致锁定图标位置和子弹终点不一致。
/// 注意:Unity 序列化字段未赋值时是"假 null",?? 运算符无法识别,必须用 == null 判断。
/// </summary>
2026-07-31 15:58:50 +08:00
/// <summary>
/// 解析锁定锚点 Transform。
/// ⭐ useHitCollider=true(手动锁定):点击碰撞器 → colliderList 激活项 → lockPoint
/// ⭐ useHitCollider=false(自动锁定/三发):直接用 lockPoint → colliderList 激活项 → transform
/// (自动锁定没有点击的碰撞器,优先用固定锁定点更稳定)
/// </summary>
private Transform ResolveLockTransform(MonsterContral monster, out int colliderIndex, bool useHitCollider = false)
2026-07-16 20:16:05 +08:00
{
colliderIndex = -1;
if (monster == null) return null;
2026-07-31 15:58:50 +08:00
// ① 手动锁定:优先用点击的碰撞器(hitCollider)
if (useHitCollider && monster.hitCollider != null && monster.hitCollider.gameObject.activeInHierarchy)
{
// 在 colliderList 里找到对应索引,用于网络同步
if (monster.colliderList != null)
{
for (int i = 0; i < monster.colliderList.Count; i++)
{
if (monster.colliderList[i] == monster.hitCollider)
{
colliderIndex = i;
break;
}
}
}
return monster.hitCollider;
}
// ② 自动锁定:直接用 lockPoint(锁定锚点)
if (!useHitCollider && monster.lockPoint != null && monster.lockPoint.gameObject.activeInHierarchy)
2026-07-23 01:51:07 +08:00
return monster.lockPoint;
2026-07-31 15:58:50 +08:00
// ③ 兜底:colliderList 第一个活跃碰撞器(手动/自动都走)
2026-07-16 20:16:05 +08:00
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;
}
}
}
2026-07-31 15:58:50 +08:00
// ④ 手动锁定兜底:lockPoint
if (useHitCollider && monster.lockPoint != null && monster.lockPoint.gameObject.activeInHierarchy)
return monster.lockPoint;
2026-07-31 16:24:35 +08:00
// ⑤ ⭐ lockPoint 和 colliderList 全部未激活 → 返回 null,调用方应换一条鱼
return null;
2026-07-16 20:16:05 +08:00
}
/// <summary>简化重载,不关心 colliderIndex 时用</summary>
private Transform ResolveLockTransform(MonsterContral monster)
{
2026-07-31 15:58:50 +08:00
return ResolveLockTransform(monster, out _, false);
2026-07-16 20:16:05 +08:00
}
2026-07-31 16:24:35 +08:00
/// <summary>
2026-07-31 17:31:51 +08:00
/// ⭐ 判断鱼是否可锁定。
/// - needLockEvent=false 的鱼:一直可锁定(只要 activeInHierarchy)
/// - needLockEvent=true 的鱼:出场/状态切换期间 isLockable=false,等 NotifyLockable() 后才可锁定
2026-08-27 15:44:36 +08:00
/// - 已死亡的鱼(isDead=true,死亡特效播放窗口期)不可锁定
2026-07-31 17:31:51 +08:00
/// 同时要求有激活的锁定锚点(lockPoint 或 colliderList 任一激活)
2026-07-31 16:24:35 +08:00
/// </summary>
2026-07-31 17:31:51 +08:00
private bool IsMonsterLockable(MonsterContral monster)
2026-07-31 16:24:35 +08:00
{
2026-07-31 17:31:51 +08:00
if (monster == null || !monster.gameObject.activeInHierarchy) return false;
2026-08-27 15:44:36 +08:00
// ⭐ 死亡的鱼不可锁定
if (monster.isDead) return false;
2026-07-31 17:31:51 +08:00
// ⭐ 可锁定事件控制:needLockEvent=true 的鱼未触发 NotifyLockable 前不可锁定
if (!monster.isLockable) return false;
// ⭐ 锚点激活检查:lockPoint 或 colliderList 任一激活
2026-07-31 16:24:35 +08:00
if (monster.lockPoint != null && monster.lockPoint.gameObject.activeInHierarchy) return true;
if (monster.colliderList != null)
{
for (int i = 0; i < monster.colliderList.Count; i++)
{
var col = monster.colliderList[i];
if (col != null && col.gameObject.activeInHierarchy) return true;
}
}
return false;
}
2026-07-16 20:16:05 +08:00
#endregion
#region 路径进度过滤(锁定范围控制)
/// <summary>bot/玩家锁定时,鱼路径进度下限:小于此值表示鱼刚出生,不锁定</summary>
2026-07-31 17:57:24 +08:00
private const float BotLockPathMin = 0.1f;
2026-07-16 20:16:05 +08:00
/// <summary>bot/玩家锁定时,鱼路径进度上限:大于此值表示鱼即将离场,不锁定</summary>
2026-07-31 17:57:24 +08:00
private const float BotLockPathMax = 0.9f;
2026-07-16 20:16:05 +08:00
/// <summary>
/// 计算鱼在路径上的进度 [0, 1]。
/// 进度 = moveTime / monsterData.moveLine.moveTime
/// 0 = 刚出生,1 = 即将离场
2026-07-31 15:58:50 +08:00
/// ⭐ isMove=false 的鱼(爆奖Boss等)不走路径,固定在场景中 → 返回中间值 0.5 表示可锁定
2026-07-16 20:16:05 +08:00
/// </summary>
private float GetFishPathProgress(MonsterContral m)
{
2026-07-31 15:58:50 +08:00
if (m == null) return 1f;
// ⭐ isMove=false 的鱼不走路径(如金龙/雷龙等爆奖Boss),一直可用
if (!m.isMove) return 0.5f;
if (m.monsterData == null || m.monsterData.moveLine == null) return 1f;
2026-07-16 20:16:05 +08:00
float total = m.monsterData.moveLine.moveTime;
if (total <= 0f) return 1f;
return Mathf.Clamp01(m.moveTime / total);
}
/// <summary>
/// 判断鱼的路径进度是否在可锁定范围内 [BotLockPathMin, BotLockPathMax]
2026-07-31 17:57:24 +08:00
/// 刚出生(< 0.1)或即将离场(> 0.9)的鱼不能锁定,避免锁定后立即丢失目标
/// ⭐ 注意:本函数仅用于自动锁定/bot/三发查找;玩家手动锁定不调用此函数
2026-07-16 20:16:05 +08:00
/// </summary>
private bool IsFishPathLockable(MonsterContral m)
{
if (m == null) return false;
float progress = GetFishPathProgress(m);
return progress >= BotLockPathMin && progress <= BotLockPathMax;
}
2026-05-28 18:16:45 +08:00
#endregion
#region 多目标查找(TripleLock)
/// <summary>鱼锁定优先级:特殊Boss(5) > Boss鱼(4) > 特殊鱼(3) > 黄金鱼(2) > 普通鱼(1)</summary>
private int GetFishLockPriority(MonsterContral monster)
{
switch (monster.fishenum)
{
case FishTypeEnum.特殊Boss: return 5;
2026-06-14 20:26:21 +08:00
case FishTypeEnum.Boss鱼: return 4;
case FishTypeEnum.特殊鱼: return 3;
case FishTypeEnum.黄金鱼: return 2;
case FishTypeEnum.普通鱼: return 1;
default: return 0;
2026-05-28 18:16:45 +08:00
}
}
/// <summary>
/// 查找 fire 时的多目标(三发锁定用)。
/// 第一个目标 = 主锁 monCotl_Lock(打哪锁哪),剩余从 Boss/特殊Boss/特殊鱼/黄金鱼 按优先级补满
/// </summary>
private List<(MonsterContral monster, Transform hitPos)> FindFireTargets(int maxCount)
{
var result = new List<(MonsterContral monster, Transform hitPos)>();
var selected = new HashSet<string>();
if (CreateFishManager.Instance == null) return result;
2026-07-31 17:57:24 +08:00
// ① 主锁目标排第一(打哪锁哪)
// ⭐ 手动锁定:只看鱼是否还活着;自动锁定:还要满足路径可锁定
2026-05-28 18:16:45 +08:00
if (currSitData.isLock && currSitData.monCotl_Lock != null &&
2026-07-30 12:54:22 +08:00
currSitData.monCotl_Lock.gameObject.activeInHierarchy &&
2026-07-31 17:57:24 +08:00
(currSitData.isManualLock || IsFishPathLockable(currSitData.monCotl_Lock)))
2026-05-28 18:16:45 +08:00
{
Transform pos = currSitData.monCotl_Lock_Position != null && currSitData.monCotl_Lock_Position.gameObject.activeInHierarchy
? currSitData.monCotl_Lock_Position
: currSitData.monCotl_Lock.transform;
result.Add((currSitData.monCotl_Lock, pos));
selected.Add(currSitData.monCotl_Lock.guid);
if (result.Count >= maxCount) return result;
}
// ② 按优先级从分类字典补满剩余位置
2026-07-30 12:54:22 +08:00
// ⭐ 修复:之前漏了 fishDatas_hashCodekey_Small(普通鱼),
// 导致场上只有普通鱼时三发只找到1个目标
2026-05-28 18:16:45 +08:00
var dicts = new Dictionary<string, MonsterContral>[]
{
CreateFishManager.Instance.fishDatas_hashCodekey_TS_Boss,
CreateFishManager.Instance.fishDatas_hashCodekey_Boss,
CreateFishManager.Instance.fishDatas_hashCodekey_TS,
CreateFishManager.Instance.fishDatas_hashCodekey_HJ,
2026-07-30 12:54:22 +08:00
CreateFishManager.Instance.fishDatas_hashCodekey_Small,
2026-05-28 18:16:45 +08:00
};
foreach (var dict in dicts)
{
if (dict == null) continue;
foreach (var kv in dict)
{
MonsterContral m = kv.Value;
if (m == null || !m.gameObject.activeInHierarchy) continue;
if (selected.Contains(m.guid)) continue; // 跳过已选中的
2026-07-16 20:16:05 +08:00
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的鱼不作为目标
if (!IsFishPathLockable(m)) continue;
2026-07-31 17:31:51 +08:00
// ⭐ 可锁定过滤:出场效果期间/needLockEvent 鱼未就绪 → 跳过换下一条
if (!IsMonsterLockable(m)) continue;
2026-05-28 18:16:45 +08:00
2026-07-31 16:24:35 +08:00
// ⭐ 统一用 ResolveLockTransform:lockPoint → colliderList → (null则不选)
2026-07-16 20:16:05 +08:00
Transform pos = ResolveLockTransform(m);
2026-07-31 16:24:35 +08:00
if (pos == null) continue;
2026-05-28 18:16:45 +08:00
result.Add((m, pos));
selected.Add(m.guid);
if (result.Count >= maxCount) return result;
}
}
return result;
}
2026-05-04 17:21:52 +08:00
#endregion
#region 锁定系统(LockModule)
2026-06-21 00:28:59 +08:00
/// <summary>
/// 刷新三发锁定图标:三发+锁定开启时显示3个锁定圈
/// </summary>
private void RefreshTripleLockIcons()
{
if (currSitData == null) return;
2026-08-31 16:40:04 +08:00
// ⭐ 散射激光模式标记(座位用它激活左右起点球父对象):三发+锁定开着+有锁定目标就算散射中,
// 不依赖实际凑满几个目标(目标不足时散射模式开着也要显示起点球)
currSitData.isScatterLaserOn = toggle_Three != null && toggle_Three.isOn
&& toggle_Lock != null && toggle_Lock.isOn
&& currSitData.isLock;
2026-06-21 00:28:59 +08:00
bool isTripleLock = toggle_Three.isOn && toggle_Lock.isOn && currSitData.isLock;
if (!isTripleLock)
{
currSitData.HideTripleLockIcons();
return;
}
// 已经有三发目标 → 跳过
if (currSitData.tripleLockTargets.Count > 0) return;
var targets = FindFireTargets(3);
if (targets.Count > 0)
currSitData.SetTripleLockTargets(targets);
}
2026-07-24 23:47:50 +08:00
/// <summary>自动锁定是否已激活:玩家首次手动锁定一条鱼后激活,该鱼死亡后开始自动转换目标</summary>
private bool _autoLockActivated = false;
2026-06-14 19:07:11 +08:00
2026-05-04 17:21:52 +08:00
private void HandleLock()
{
2026-07-19 14:27:00 +08:00
// ⭐ 退出倒计时期间屏蔽本地玩家手动锁定,其他玩家和机器人不受影响
if (_isLeavingCountdown) return;
2026-08-29 17:28:00 +08:00
// ⭐ 特殊鱼爆奖(钻头/激光)期间禁用手动锁定:点击只用于旋转瞄准/发射方向
if (currSitData != null && currSitData.isDrillActive) return;
2026-05-18 18:39:11 +08:00
CrossPlatformInput input = GetInput();
2026-05-04 17:21:52 +08:00
if (!toggle_Lock.isOn) return;
2026-07-25 16:13:21 +08:00
//if (!input.wasPressedThisFrame && !input.wasReleasedThisFrame) return;
if (!input.wasPressedThisFrame) return;
2026-06-14 19:07:11 +08:00
if (IsPointerOverUI(input.position)) return;
2026-05-18 18:39:11 +08:00
2026-06-14 19:07:11 +08:00
// 射线从相机出发,指向鼠标点击点
Vector3 mousePos = input.position;
mousePos.z = -Camera.main.transform.position.z;
Vector2 mouseWorld = Camera.main.ScreenToWorldPoint(mousePos);
//Vector2 origin = Camera.main.transform.position;
//Vector2 dir = (mouseWorld - origin).normalized;
Vector2 dir = Vector2.zero;
float distance = 10000f;
int fishLayer = LayerMask.GetMask("Fish");
2026-05-18 18:39:11 +08:00
2026-06-14 19:07:11 +08:00
// 获取所有命中(可能有多个)
RaycastHit2D[] hits = Physics2D.RaycastAll(mouseWorld, dir, distance, fishLayer);
if (hits.Length == 0)
2026-05-18 18:39:11 +08:00
{
2026-06-14 19:07:11 +08:00
// 未命中:清除锁定
2026-05-18 18:39:11 +08:00
currSitData.ClearLock();
2026-06-13 18:23:25 +08:00
SyncTurretState(TurretState.Idle);
2026-05-18 18:39:11 +08:00
SyncLock(null);
2026-06-14 19:07:11 +08:00
return;
2026-05-04 17:21:52 +08:00
}
2026-06-14 19:07:11 +08:00
// 按 Z 轴深度选择最前面的(Z 值最大的,因为相机在负 Z,更大 Z 更近)
// 如果您的相机在 Z = -10,物体 Z 范围 0 ~ 10,则 Z 越大越靠近相机
RaycastHit2D bestHit = hits[0];
float bestDepth = bestHit.collider.transform.position.z;
foreach (var hit in hits)
{
float depth = hit.collider.transform.position.z;
2026-07-31 15:58:50 +08:00
if (depth < bestDepth) // 更大 Z → 更远
2026-06-14 19:07:11 +08:00
{
bestDepth = depth;
bestHit = hit;
}
}
// 处理选中的最佳命中
MonsterContral monster = bestHit.collider.GetComponentInParent<MonsterContral>();
2026-07-31 15:58:50 +08:00
// ⭐ 记录被点击的碰撞器,用于 ResolveLockTransform 优先级判断
if (monster != null)
monster.hitCollider = bestHit.collider.transform;
// ⭐ 手动锁定:useHitCollider=true → 点击碰撞器 → colliderList 激活项 → lockPoint
2026-07-16 20:16:05 +08:00
int colliderIndex;
2026-07-31 15:58:50 +08:00
Transform lockTr = ResolveLockTransform(monster, out colliderIndex, true);
2026-07-31 16:24:35 +08:00
// ⭐ lockPoint 和 colliderList 全部未激活 → 不锁定这条鱼
if (lockTr == null) return;
2026-07-31 17:57:24 +08:00
// ⭐ 标记为手动锁定:不受路径进度(0.1~0.9)限制,鱼离场前一直锁定
currSitData.isManualLock = true;
2026-06-14 23:54:59 +08:00
currSitData.SetLockTarget(monster, lockTr);
2026-07-14 02:48:54 +08:00
SyncLock(new List<string> { monster?.guid }, colliderIndex);
2026-07-24 23:47:50 +08:00
// ⭐ 手动锁定成功 → 激活自动锁定(该鱼死亡后自动转换目标)
_autoLockActivated = true;
2026-05-04 17:21:52 +08:00
}
2026-05-27 19:47:06 +08:00
private void UpdateLockFX()
{
2026-05-18 18:39:11 +08:00
2026-05-27 19:47:06 +08:00
foreach (var sit in list_Sits)
{
if (!sit.isLock || sit.monCotl_Lock == null) continue;
2026-07-31 17:31:51 +08:00
// ⭐ 切换锁定点(小白龙等切换模型后锁定点失效):
// 当前 monCotl_Lock_Position 失效 → 调 SwitchLockPoint 重新取激活的锚点
if (sit.monCotl_Lock_Position == null || !sit.monCotl_Lock_Position.gameObject.activeInHierarchy)
{
Transform newLockPos = sit.monCotl_Lock.SwitchLockPoint();
if (newLockPos != null)
sit.monCotl_Lock_Position = newLockPos;
}
2026-06-21 00:28:59 +08:00
// 更新主锁定图标
UpdateSingleLockFX(sit, sit.monCotl_Lock, sit.monCotl_Lock_Position, sit.lockFX);
// 更新三发锁定额外图标
2026-07-30 12:54:22 +08:00
// ⭐ tripleLockTargets[0] 是主目标(已在主 lockFX 显示),
// lockFX_Triple[0] 应对应 tripleLockTargets[1],以此类推
for (int i = 0; i < sit.lockFX_Triple.Count; i++)
2026-05-27 19:47:06 +08:00
{
2026-07-30 12:54:22 +08:00
int targetIdx = i + 1; // 跳过索引0(主目标)
if (targetIdx >= sit.tripleLockTargets.Count) break;
2026-06-21 00:28:59 +08:00
var icon = sit.lockFX_Triple[i];
2026-07-30 12:54:22 +08:00
var monster = sit.tripleLockTargets[targetIdx];
var hitPos = sit.tripleLockPositions[targetIdx];
if (icon == null || monster == null) continue;
2026-07-31 17:31:51 +08:00
// ⭐ 同样为三发目标切换锁定点
if (hitPos == null || !hitPos.gameObject.activeInHierarchy)
{
Transform newLockPos = monster.SwitchLockPoint();
if (newLockPos != null)
{
sit.tripleLockPositions[targetIdx] = newLockPos;
hitPos = newLockPos;
}
}
2026-06-21 00:28:59 +08:00
UpdateSingleLockFX(sit, monster, hitPos, icon);
}
}
}
private void UpdateSingleLockFX(SitPosData sit, MonsterContral monster, Transform hitPos, GameObject icon)
{
2026-07-28 10:59:08 +08:00
// ⭐ 核心原则:鱼还活着就显示图标,鱼死了才隐藏
if (monster == null || !monster.gameObject.activeInHierarchy)
2026-06-21 00:28:59 +08:00
{
2026-07-28 10:59:08 +08:00
if (icon.activeSelf) icon.SetActive(false);
return;
}
// 鱼还活着 → 确定位置(优先 hitPos,失效则用 monster.transform 兜底)
Vector3 pos;
if (hitPos != null && hitPos.gameObject.activeInHierarchy)
{
pos = hitPos.position;
2026-05-27 19:47:06 +08:00
}
2026-06-21 00:28:59 +08:00
else
{
2026-07-28 10:59:08 +08:00
// hitPos 失效 → 直接用鱼自身位置,不再尝试 ResolveLockTransform(避免二次失败)
pos = monster.transform.position;
2026-06-21 00:28:59 +08:00
}
2026-07-28 10:59:08 +08:00
if (!icon.activeSelf) icon.SetActive(true);
2026-06-21 00:28:59 +08:00
pos.z = Camera.main.transform.position.z + 1;
icon.transform.position = pos;
2026-05-27 19:47:06 +08:00
}
2026-05-18 18:39:11 +08:00
private void CheckLockTargetValid()
{
if (!currSitData.isLock) return;
var monster = currSitData.monCotl_Lock;
2026-07-31 17:57:24 +08:00
// ⭐ 目标无效判定:被回收/销毁
// 路径进度检查仅对自动锁定生效;手动锁定不检查(玩家自己锁的鱼离场前一直跟)
2026-07-30 12:54:22 +08:00
bool mainInvalid = monster == null
|| !monster.gameObject.activeInHierarchy
2026-07-31 17:57:24 +08:00
|| (!currSitData.isManualLock && !IsFishPathLockable(monster));
2026-07-30 12:54:22 +08:00
if (mainInvalid)
2026-05-18 18:39:11 +08:00
{
currSitData.ClearLock(); // 清除锁定
2026-06-13 18:23:25 +08:00
SyncTurretState(TurretState.Idle);
2026-05-18 18:39:11 +08:00
SyncLock(null); // 同步服务器
// ⭐⭐⭐ 关键:怪死了 → 动画回到 Idle ⭐⭐⭐
currSitData.ChangeState(TurretState.Idle);
2026-06-13 01:25:41 +08:00
SyncTurretState(TurretState.Idle);
2026-07-30 12:54:22 +08:00
return;
}
// ⭐ 检查三发锁定目标:移除已死亡或已离场的额外目标
if (currSitData.tripleLockTargets.Count > 0)
{
bool changed = false;
for (int i = currSitData.tripleLockTargets.Count - 1; i >= 1; i--)
{
var t = currSitData.tripleLockTargets[i];
// 失效判定:被回收/销毁 OR 路径离场
if (t == null || !t.gameObject.activeInHierarchy || !IsFishPathLockable(t))
{
currSitData.tripleLockTargets.RemoveAt(i);
currSitData.tripleLockPositions.RemoveAt(i);
changed = true;
}
}
// 有目标失效 → 清空三发列表,让 RefreshTripleLockIcons 重新找满3个
if (changed)
{
currSitData.HideTripleLockIcons();
}
2026-05-18 18:39:11 +08:00
}
}
2026-05-04 17:21:52 +08:00
private void HandleAutoLock()
{
2026-07-19 14:27:00 +08:00
// ⭐ 退出倒计时期间屏蔽本地玩家自动锁定,其他玩家和机器人不受影响
if (_isLeavingCountdown) return;
2026-08-29 17:28:00 +08:00
// ⭐ 特殊鱼爆奖(钻头/激光)期间禁用自动锁定:锁定目标让位给炮台瞄准方向
if (currSitData != null && currSitData.isDrillActive) return;
2026-07-24 23:47:50 +08:00
// 自动锁定必须在锁定开关开启时才工作
if (!toggle_Lock.isOn) return;
// ⭐ 前提条件:玩家必须先手动锁定一条鱼来激活自动锁定
// 激活后,当锁定的鱼死亡,自动转换到下一个目标
if (!_autoLockActivated) return;
2026-05-04 17:21:52 +08:00
2026-07-31 17:57:24 +08:00
// 已经有锁定目标并且还活着,就不重复找
// ⭐ 手动锁定:只看鱼是否还活着,不检查路径进度(玩家自己锁的鱼离场前一直跟)
// 自动锁定:还要检查路径进度(超出 0.1~0.9 范围视为失效,换下一条)
2026-05-18 18:39:11 +08:00
if (currSitData.isLock &&
currSitData.monCotl_Lock != null &&
2026-07-30 12:54:22 +08:00
currSitData.monCotl_Lock.gameObject.activeInHierarchy &&
2026-07-31 17:57:24 +08:00
(currSitData.isManualLock || IsFishPathLockable(currSitData.monCotl_Lock)))
2026-05-18 18:39:11 +08:00
{
return;
}
2026-07-24 23:47:50 +08:00
// 当前锁定目标已死亡/失效 → 自动找下一个目标
2026-05-18 18:39:11 +08:00
MonsterContral target = FindAutoLockTarget();
if (target != null)
{
2026-07-31 16:24:35 +08:00
// ⭐ 自动锁定:useHitCollider=false → lockPoint → colliderList → null
2026-07-16 20:16:05 +08:00
int colliderIndex;
Transform lockTr = ResolveLockTransform(target, out colliderIndex);
2026-07-31 16:24:35 +08:00
// ⭐ lockPoint 和 colliderList 全部未激活(出场效果中) → 不锁定,等下一帧再找
if (lockTr == null) return;
2026-07-31 17:57:24 +08:00
// ⭐ 标记为自动锁定:受路径进度(0.1~0.9)限制
currSitData.isManualLock = false;
2026-07-14 02:48:54 +08:00
currSitData.SetLockTarget(target, lockTr);
SyncLock(new List<string> { target?.guid }, colliderIndex);
2026-07-27 16:59:22 +08:00
2026-07-28 10:59:08 +08:00
// ⭐ 切换目标瞬间立即更新主锁定图标(鱼还活着就一定显示)
if (currSitData.lockFX != null && target.gameObject.activeInHierarchy)
2026-07-27 16:59:22 +08:00
{
if (!currSitData.lockFX.activeSelf) currSitData.lockFX.SetActive(true);
2026-07-31 16:24:35 +08:00
Vector3 p = lockTr.position;
2026-07-27 16:59:22 +08:00
p.z = Camera.main.transform.position.z + 1;
currSitData.lockFX.transform.position = p;
}
2026-05-18 18:39:11 +08:00
}
else
{
2026-07-24 23:47:50 +08:00
// 场上没有可锁定目标 → 清除锁定(仅在当前有锁时清除,避免每帧广播)
if (currSitData.isLock)
{
currSitData.ClearLock();
SyncTurretState(TurretState.Idle);
SyncLock(null);
}
2026-05-18 18:39:11 +08:00
}
}
2026-05-19 17:59:23 +08:00
2026-05-18 18:39:11 +08:00
private MonsterContral FindAutoLockTarget()
{
if (CreateFishManager.Instance == null ||
CreateFishManager.Instance.fishDatas_hashCodekey == null ||
CreateFishManager.Instance.fishDatas_hashCodekey.Count == 0)
return null;
2026-07-16 20:16:05 +08:00
// ⭐ 用世界坐标(Z=0平面),和 bot AI 一致,否则距离计算会异常(出现 3.4E+38 之类)
Vector3 origin = currSitData.turretWorldPos;
2026-05-18 18:39:11 +08:00
2026-07-24 23:47:50 +08:00
// ⭐ 规则:特殊Boss / Boss鱼 / 特殊鱼 为高价值组(按优先级 + 距离最近),
// 黄金鱼 / 普通鱼 为普通组(随机选一只)
// 高价值组有目标时只从高价值组选;高价值组无目标时从普通组随机
MonsterContral best = null;
int bestPri = -1;
float bestSqr = float.MaxValue;
var normalList = new List<MonsterContral>();
2026-05-18 18:39:11 +08:00
foreach (var kv in CreateFishManager.Instance.fishDatas_hashCodekey)
{
MonsterContral m = kv.Value;
if (m == null || !m.gameObject.activeInHierarchy) continue;
2026-07-16 20:16:05 +08:00
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的鱼不作为自动锁定目标
if (!IsFishPathLockable(m)) continue;
2026-07-31 17:31:51 +08:00
// ⭐ 可锁定过滤:出场效果期间/needLockEvent 鱼未就绪 → 跳过换下一条
if (!IsMonsterLockable(m)) continue;
2026-05-18 18:39:11 +08:00
2026-07-24 23:47:50 +08:00
int pri = GetFishLockPriority(m);
// 高价值组:特殊Boss(5) / Boss鱼(4) / 特殊鱼(3) → 优先级 + 距离最近
if (pri >= 3)
2026-05-18 18:39:11 +08:00
{
2026-07-24 23:47:50 +08:00
float sqr = (m.transform.position - origin).sqrMagnitude;
if (pri > bestPri || (pri == bestPri && sqr < bestSqr))
{
bestPri = pri;
bestSqr = sqr;
best = m;
}
}
// 普通组:黄金鱼(2) / 普通鱼(1) → 先收集,后面随机
else
{
normalList.Add(m);
2026-05-18 18:39:11 +08:00
}
}
2026-07-24 23:47:50 +08:00
// 高价值组有目标 → 直接返回最高价值(同价值最近)的目标
if (best != null) return best;
// 高价值组无目标 → 从普通组随机选一只
if (normalList.Count > 0)
return normalList[Random.Range(0, normalList.Count)];
return null;
2026-05-04 17:21:52 +08:00
}
//private void HandleAutoLock()
//{
// if (!toggle_SkillAuto.isOn) return;
// if (toggle_Lock.isOn) return;
// if (currSitData.isLock) return;
// MonsterContral nearest = CreateFishManager.Instance;
// if (nearest != null)
// {
// currSitData.SetLockTarget(nearest, nearest.transform);
// SyncLock(nearest);
// }
//}
#endregion
#region BulletModule(子弹系统)
2026-06-21 00:07:26 +08:00
public int sitBulletMaxCount = 100;
2026-06-20 15:00:15 +08:00
/// <summary>
/// 检查指定用户是否已达50发子弹上限
/// </summary>
private bool IsBulletLimitReached(string userId)
{
if (bulletParent == null) return false;
int count = 0;
foreach (Transform child in bulletParent.transform)
{
if (child.TryGetComponent<BullitMove>(out var bm) && bm.userid == userId)
count++;
}
if (count >= sitBulletMaxCount)
{
UIManager.Instance.ShowBoardError("子弹已达上限");
return true;
}
return false;
}
2026-08-01 13:42:35 +08:00
/// <summary>
2026-08-03 13:56:30 +08:00
/// 金币不足退出房间弹窗是否可弹出。
/// 弹出后置为 false,仅在金币恢复到能发射最小炮倍时才重置为 true。
/// 不在弹窗关闭时重置——避免点确认后 RequestLeaveRoom 异步等待期间反复弹窗。
2026-08-01 13:42:35 +08:00
/// </summary>
2026-08-03 13:56:30 +08:00
private bool _canShowGoldInsufficientPanel = true;
2026-08-01 13:42:35 +08:00
/// <summary>
/// 获取当前房间最小炮倍(paoBeiList 中的最小值)
/// </summary>
private long GetMinConsumeGold()
{
if (paoBeiList == null || paoBeiList.Count == 0) return 0;
return paoBeiList.Min();
}
/// <summary>
/// 开炮前本地金币校验:三射按 3 发,否则按 1 发计算所需金币。
/// 金币不足则不切换攻击状态、不发送开炮请求,避免无效请求与服务端拒绝。
/// ⭐ 当金币不足以发射最小炮倍时,弹窗询问是否退出房间:
/// 确认 → 走退出房间逻辑;X号 → 关闭面板。本地判断,不发送开炮请求。
2026-08-03 13:56:30 +08:00
/// 通过 _canShowGoldInsufficientPanel bool 保证唯一性:
/// 弹出后设 false,仅在金币恢复到够最小炮倍时才重置 true,
/// 期间无论弹窗是否关闭、RequestLeaveRoom 是否异步等待,都不会再弹。
2026-08-01 13:42:35 +08:00
/// </summary>
private bool HasEnoughGoldToFire(bool isSanShe)
{
if (currSitData == null) return false;
int count = isSanShe ? 3 : 1;
long need = currSitData.consumeGoldNum * count;
if (currSitData.goldNum >= need)
{
2026-08-03 13:56:30 +08:00
// 金币够当前炮倍:可正常开火,重置弹窗标志
_canShowGoldInsufficientPanel = true;
2026-08-01 13:42:35 +08:00
return true;
}
2026-08-03 13:56:30 +08:00
// ⭐ 金币不够发射(散射按3发/单发按1发)→ 取消锁定,不允许继续锁定攻击
// toggle_Lock.isOn = false 会触发 onValueChanged → ClearLock + 同步服务器,
// 之后 HandleAutoFire 的 needAutoFire 因 toggle_Lock 关闭而变 false,停止自动锁定开火
if (toggle_Lock != null && toggle_Lock.isOn)
{
toggle_Lock.isOn = false;
}
2026-08-01 13:42:35 +08:00
// 金币不足当前炮倍:进一步判断是否连最小炮倍都发射不起
long minConsume = GetMinConsumeGold();
2026-08-01 15:48:03 +08:00
if (minConsume <= 0 || currSitData.goldNum >= minConsume)
2026-08-01 13:42:35 +08:00
{
2026-08-03 13:56:30 +08:00
// 金币够最小炮倍:玩家可降炮倍继续游戏,重置弹窗标志
_canShowGoldInsufficientPanel = true;
2026-08-01 15:48:03 +08:00
return false;
}
// ⭐ 金币不足以发射最小炮倍 → 弹窗询问是否退出房间
2026-08-03 13:56:30 +08:00
// 关键:只在金币恢复后才重置 bool,不在弹窗关闭时重置,
// 避免 RequestLeaveRoom 异步等待期间反复弹窗
if (_canShowGoldInsufficientPanel)
2026-08-01 15:48:03 +08:00
{
2026-08-03 13:56:30 +08:00
_canShowGoldInsufficientPanel = false;
UIManager.Instance.ShowTiShi(
2026-08-01 15:48:03 +08:00
"金币不足,是否退出房间?",
() => RequestLeaveRoom(),
true);
2026-08-01 13:42:35 +08:00
}
return false;
}
2026-05-28 18:16:45 +08:00
private BullitMove CreateBullet(SitPosData sit, Vector2 pos, float angle, bool isDouble, bool isLock, string id,
2026-07-30 23:54:35 +08:00
MonsterContral lockTarget = null, Transform lockHitPos = null, long fireConsumeGold = 0)
2026-05-04 17:21:52 +08:00
{
2026-08-28 22:33:35 +08:00
// ⭐ 战神装备中:子弹用战神的子弹预制体,不用炮台的
GameObject prefab = null;
if (sit.currentZhanShenData != null)
prefab = isDouble ? sit.currentZhanShenData.bulletPrefab_Two : sit.currentZhanShenData.bulletPrefab;
2026-06-13 01:25:41 +08:00
2026-08-28 22:33:35 +08:00
// ⭐ 战神没配子弹/没装备战神 -> 回退炮台子弹(远程座位炮台未加载时补加载)
if (prefab == null)
{
if (sit.currentTurret == null)
{
sit.LoadTurret(sit.currentTurretNumber);
if (sit.currentTurret == null) return null;
}
prefab = isDouble ? sit.currentTurret.bulletPrefab_Two : sit.currentTurret.bulletPrefab;
}
2026-05-04 17:21:52 +08:00
2026-06-21 00:07:26 +08:00
// ⭐ 每座位最多100发子弹(先校验,不扣钱)
2026-06-20 15:00:15 +08:00
int activeBullets = 0;
foreach (Transform child in bulletParent.transform)
{
if (child.TryGetComponent<BullitMove>(out var bm) && bm.userid == sit.user)
activeBullets++;
}
2026-06-21 00:07:26 +08:00
if (activeBullets >= sitBulletMaxCount)
2026-06-20 15:00:15 +08:00
{
2026-06-21 00:07:26 +08:00
UIManager.Instance.ShowBoardError($"[子弹上限] 座位{sit.posID}已达{sitBulletMaxCount}发上限");
2026-06-20 15:00:15 +08:00
return null;
}
2026-07-30 23:54:35 +08:00
if (!sit.ConsumeGold(isDouble ? 2 : 1, fireConsumeGold))
2026-05-04 17:21:52 +08:00
return null;
2026-07-11 17:07:32 +08:00
// ⭐ 从对象池取子弹(池空时内部会 Instantiate)
var bulletGo = BulletPool.Instance.Get(prefab, pos, Quaternion.Euler(0, 0, angle), bulletParent.transform);
if (bulletGo == null) return null;
var bullet = bulletGo.GetComponent<BullitMove>();
2026-05-04 17:21:52 +08:00
2026-07-11 19:05:27 +08:00
// ⭐ 池化复用时必须重新记录起点(Init 会把 vector3_startPosition 对齐到当前位置,z=-400)
// 否则 Update 里 transform.position = vector3_startPosition + ... 会用回收时的旧起点
bullet.Init();
2026-05-04 17:21:52 +08:00
bullet.userid = sit.user;
bullet.playerID = sit.user;
2026-05-27 19:47:06 +08:00
bullet.bulletID = id;
2026-05-04 17:21:52 +08:00
if (isLock)
2026-05-28 18:16:45 +08:00
{
if (lockTarget != null)
bullet.Lock(lockTarget, lockHitPos);
2026-07-11 19:05:27 +08:00
else if (sit.monCotl_Lock != null && sit.monCotl_Lock_Position != null)
bullet.Lock(sit.monCotl_Lock, sit.monCotl_Lock_Position);
// 锁定目标或位置为 null → 不锁定,按普通子弹发射
2026-05-28 18:16:45 +08:00
}
2026-05-04 17:21:52 +08:00
return bullet;
}
#endregion
#region ⑥ 网络同步(NetSyncModule)
private void SyncRotate(float angle)
{
SyncGameData sgd = new SyncGameData
{
sitPos = currSitData.posID,
frameType = FrameDataType.rotateTurrect,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
frameData = new FrameData_Rotate { angle = angle }
};
WebSocketSendList.SyncGameData(sgd);
}
private void SyncFire(float angle, bool isSanShe, bool isDouble, bool isLock)
{
2026-06-16 19:12:37 +08:00
var fireData = new FrameData_FireBullet
2026-05-28 18:16:45 +08:00
{
angle = angle,
gold = currSitData.goldNum,
isSanShe = isSanShe,
twoRTruuect = isDouble,
isSuoDing = isLock,
auto = toggle_SkillAuto.isOn
};
// ⭐ 三发 + 锁定:查找 3 个优先级目标,写入 monsterIDs
if (isSanShe && isLock)
{
var targets = FindFireTargets(3);
if (targets.Count > 0)
fireData.monsterIDs = targets.Select(t => t.monster.guid).ToList();
}
2026-05-04 17:21:52 +08:00
SyncGameData sgd = new SyncGameData
{
sitPos = currSitData.posID,
frameType = FrameDataType.fireBullet,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
2026-05-28 18:16:45 +08:00
frameData = fireData
2026-05-04 17:21:52 +08:00
};
2026-06-17 21:26:06 +08:00
//EnqueueSync(sgd);
2026-05-27 19:47:06 +08:00
var roomdata = GameManager.Instance.gameData.currRoomData;
var user = GameManager.Instance.user;
// 三射 = 3 发,否则 = 1 发
int count = isSanShe ? 3 : 1;
2026-07-18 14:39:25 +08:00
// ⭐ bulletIds 由服务端生成并通过 RoomFireNotice 广播回来,客户端不需要传
long timespan = BeijingTimeManager.Instance.CurrentTimestampMs; // 毫秒
long timestamp = timespan / 1000; // 秒级
string sign = WebSocketSendList.MakeFireCannonSign(
roomdata.roomId, user.id, currSitData.consumeGoldNum, count, timestamp, "63215f0afa3f5b25");
2026-07-17 17:50:31 +08:00
2026-07-18 14:39:25 +08:00
// FireCannon:roomId, userId, gold, count, timestamp, sign, extra
2026-05-27 19:47:06 +08:00
WebSocketSendList.FireCannon(
roomdata.roomId,
user.id,
currSitData.consumeGoldNum,
count,
2026-07-18 14:39:25 +08:00
timestamp,
sign,
sgd
2026-05-27 19:47:06 +08:00
);
2026-05-04 17:21:52 +08:00
}
2026-05-27 19:47:06 +08:00
2026-06-14 20:26:21 +08:00
private void SyncLock(List<string> monsterguid, int index = -1)
2026-05-04 17:21:52 +08:00
{
SyncGameData sgd = new SyncGameData
{
sitPos = currSitData.posID,
frameType = FrameDataType.changeAutoTarget,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
frameData = new FrameData_ChangeAutoTarget
{
2026-05-27 19:47:06 +08:00
targetGuid = monsterguid,
2026-06-14 20:26:21 +08:00
colliderIndex = index,
2026-05-04 17:21:52 +08:00
}
};
WebSocketSendList.SyncGameData(sgd);
}
2026-06-13 01:25:41 +08:00
private void SyncTurretState(TurretState state)
2026-05-04 17:21:52 +08:00
{
SyncGameData sgd = new SyncGameData
{
sitPos = currSitData.posID,
frameType = FrameDataType.changeTurretState,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
frameData = new FrameData_TurretState { state = state }
};
WebSocketSendList.SyncGameData(sgd);
}
2026-06-13 22:34:43 +08:00
public void SyncChangeTurret(int turretNumber)
2026-05-04 17:21:52 +08:00
{
SyncGameData sgd = new SyncGameData
{
sitPos = currSitData.posID,
frameType = FrameDataType.changeTurret,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
2026-06-08 21:23:40 +08:00
frameData = new FrameData_ChangeTurret { turretIndex = turretNumber }
2026-05-04 17:21:52 +08:00
};
WebSocketSendList.SyncGameData(sgd);
}
2026-06-30 01:31:53 +08:00
public void SyncChangeWing(int wingNumber)
{
SyncGameData sgd = new SyncGameData
{
sitPos = currSitData.posID,
frameType = FrameDataType.changeWing,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
frameData = new FrameData_ChangeTurret { turretIndex = wingNumber }
};
WebSocketSendList.SyncGameData(sgd);
}
2026-08-28 22:33:35 +08:00
/// <summary>广播战神切换(changeZhanShen):其他客户端在对应座位加载/清除战神模型</summary>
public void SyncChangeZhanShen(int zhanShenNumber)
{
SyncGameData sgd = new SyncGameData
{
sitPos = currSitData.posID,
frameType = FrameDataType.changeZhanShen,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
frameData = new FrameData_ChangeTurret { turretIndex = zhanShenNumber }
};
WebSocketSendList.SyncGameData(sgd);
}
2026-05-28 18:16:45 +08:00
private void SyncFreeze(bool isFreeze)
{
SyncGameData sgd = new SyncGameData
{
sitPos = currSitData.posID,
frameType = FrameDataType.freezeFish,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
frameData = new FrameData_FreezeFish { isFreeze = isFreeze }
};
WebSocketSendList.SyncGameData(sgd);
}
2026-05-04 17:21:52 +08:00
#endregion
#region ⑦ 服务器广播处理(NetReceiveModule)
public void SyncGameData_Rotate(SyncGameData sync)
{
if (sync.sitPos == currSitData.posID) return;
2026-07-20 01:44:28 +08:00
var sit = list_Sits[sync.sitPos];
// ⭐ 房主不处理机器人旋转广播:房主在 RotateBotToTarget 里已直接旋转本地炮台,
// 再走 SmoothRotate 会和本地直接旋转冲突
if (sit != null && sit.isBot && currSitData == SitPosData.roomOwnerSit) return;
2026-05-04 17:21:52 +08:00
var data = FrameDataParser.Parse<FrameData_Rotate>(sync.frameData);
2026-07-20 01:44:28 +08:00
SmoothRotate(sit, data.angle);
2026-05-04 17:21:52 +08:00
}
2026-05-27 19:47:06 +08:00
public void SyncGameData_FireBullit(FireCannonSendData sync)
2026-05-04 17:21:52 +08:00
{
2026-07-14 02:48:54 +08:00
// ⭐ 跳过本地玩家(本地玩家开火由 SyncFire → 服务器广播回来后创建子弹,
// 但如果服务器不回广播给开火者,本地玩家需要在 SyncFire 里直接 FireBullet)
// 实际设计:本地玩家也通过广播回来创建子弹,所以不跳过
2026-07-30 23:54:35 +08:00
// ⭐ 从服务端权威数据提取每发子弹扣费金额
// 1. 优先用 bullets 字段(type:3 调用返回值,包含每发子弹的扣费金额)
// 2. fallback 用 sync.gold(RoomFireNotice 广播里的炮倍,服务端权威)
List<long> bulletGolds = null;
if (sync.bullets != null && sync.bullets.Count > 0)
{
bulletGolds = new List<long>(sync.bullets.Count);
foreach (var bg in sync.bullets)
bulletGolds.Add(bg.gold);
}
else if (sync.gold > 0 && sync.bulletIds != null && sync.bulletIds.Count > 0)
{
// RoomFireNotice 没有 bullets 字段,用 sync.gold(服务端权威炮倍)作为每发扣费金额
bulletGolds = new List<long>(sync.bulletIds.Count);
for (int i = 0; i < sync.bulletIds.Count; i++)
bulletGolds.Add(sync.gold);
}
2026-07-14 02:48:54 +08:00
// extra 为空:从 botLastFireData 或默认参数还原(bot 或真人玩家都可能走到这里)
2026-07-02 18:28:50 +08:00
if (sync.extra == null || sync.extra.frameData == null)
{
2026-07-14 02:48:54 +08:00
SitPosData sit = list_Sits.Find(s => s.user == sync.userId);
if (sit == null) return;
2026-07-02 18:28:50 +08:00
2026-07-14 02:48:54 +08:00
float angle = botLastAngle.TryGetValue(sync.userId, out float a) ? a : sit.currentAngle;
2026-07-02 18:28:50 +08:00
if (sync.bulletIds == null || sync.bulletIds.Count == 0) return;
2026-07-05 18:52:09 +08:00
2026-07-17 17:50:31 +08:00
// ⭐ 优先用服务器广播的 sync.isBot 标记判断是否机器人
// bot 从 botLastFireData 还原完整技能参数;真人玩家用默认参数
bool isBot = sync.isBot || sit.isBot;
if (isBot && botLastFireData.TryGetValue(sync.userId, out var fireData))
2026-07-05 18:52:09 +08:00
{
2026-07-14 02:48:54 +08:00
FireBullet(sit, angle, fireData.isSanShe, fireData.twoRTruuect,
2026-07-30 23:54:35 +08:00
fireData.isSuoDing, sync.bulletIds, fireData.monsterIDs, bulletGolds);
2026-07-05 18:52:09 +08:00
}
else
{
2026-07-30 23:54:35 +08:00
FireBullet(sit, angle, false, false, false, sync.bulletIds, null, bulletGolds);
2026-07-05 18:52:09 +08:00
}
2026-07-02 18:28:50 +08:00
return;
}
2026-05-04 17:21:52 +08:00
2026-06-16 19:12:37 +08:00
var data = FrameDataParser.Parse<FrameData_FireBullet>(sync.extra.frameData);
2026-07-31 17:57:24 +08:00
// ⭐ 边界检查:防止 sitPos 越界导致 IndexOutOfRangeException
if (sync.extra.sitPos < 0 || sync.extra.sitPos >= list_Sits.Count)
{
Debug.LogWarning($"[SyncGameData_FireBullit] sitPos 越界: {sync.extra.sitPos}");
return;
}
2026-07-30 23:54:35 +08:00
var targetSit = list_Sits[sync.extra.sitPos];
FireBullet(targetSit, data.angle, data.isSanShe, data.twoRTruuect, data.isSuoDing, sync.bulletIds, data.monsterIDs, bulletGolds);
2026-05-04 17:21:52 +08:00
}
2026-07-02 18:28:50 +08:00
/// <summary>
/// 机器人角度同步:接收 SyncFire_ForBot 发出的 SyncGameData(fireBullet),
/// 将角度存入 botLastAngle,供 RoomFireNotice 到达时 SyncGameData_FireBullit 使用
/// </summary>
public void SyncGameData_BotFireAngle(SyncGameData sync)
{
if (sync == null || sync.frameData == null) return;
if (sync.sitPos < 0 || sync.sitPos >= list_Sits.Count) return;
var sit = list_Sits[sync.sitPos];
if (sit == null || !sit.isBot) return;
var fireData = FrameDataParser.Parse<FrameData_FireBullet>(sync.frameData);
if (fireData == null) return;
botLastAngle[sit.user] = fireData.angle;
2026-07-05 18:52:09 +08:00
botLastFireData[sit.user] = fireData; // 存储完整 fireData 供 SyncGameData_FireBullit 还原技能状态
2026-07-02 18:28:50 +08:00
}
2026-05-04 17:21:52 +08:00
public void SyncGameData_ChangeTargetMonster(SyncGameData sync)
{
if (sync.sitPos == currSitData.posID) return;
var data = FrameDataParser.Parse<FrameData_ChangeAutoTarget>(sync.frameData);
if (data.targetGuid == null)
{
list_Sits[sync.sitPos].ClearLock();
return;
}
2026-05-27 19:47:06 +08:00
MonsterContral monster = CreateFishManager.Instance.fishDatas_hashCodekey[data.targetGuid[0]];
2026-06-14 23:54:59 +08:00
2026-07-16 20:16:05 +08:00
// ⭐ 统一用 ResolveLockTransform:colliderList 第一个活跃 → lockPoint → transform
// 注:网络同步过来的 colliderIndex 在统一逻辑下不再使用,因为客户端之间
// colliderList 顺序可能不一致,统一用"第一个活跃"更稳定
Transform hitPos = ResolveLockTransform(monster);
2026-05-04 17:21:52 +08:00
list_Sits[sync.sitPos].SetLockTarget(monster, hitPos);
}
public void SyncGameData_RendromDieFish(SyncGameData sync)
{
var data = JsonConvert.DeserializeObject<FrameData_RandomDieFish>(sync.frameData.ToString());
2026-07-20 01:44:28 +08:00
// 用 sync.sitPos 判断是否本地玩家发的(服务器不会丢这个字段)
bool isLocalSender = sync.sitPos == currSitData.posID;
2026-05-04 17:21:52 +08:00
CreateFishManager.Instance.Id_DieListFish_FX(
data.guids,
data.coinNum,
new Vector3(data.x, data.y, data.z),
2026-07-20 01:44:28 +08:00
data.count,
isLocalSender
2026-05-04 17:21:52 +08:00
);
}
public void SyncGameData_TurretState(SyncGameData sync)
{
if (sync.sitPos == currSitData.posID) return;
var data = FrameDataParser.Parse<FrameData_TurretState>(sync.frameData);
var sit = list_Sits[sync.sitPos];
sit.ChangeState(data.state);
}
public void SyncGameData_ChangeTurret(SyncGameData sync)
{
if (sync.sitPos == currSitData.posID) return;
var data = FrameDataParser.Parse<FrameData_ChangeTurret>(sync.frameData);
2026-06-08 21:23:40 +08:00
list_Sits[sync.sitPos].LoadTurret(data.turretIndex);
2026-05-04 17:21:52 +08:00
}
2026-06-30 01:31:53 +08:00
public void SyncGameData_ChangeWing(SyncGameData sync)
{
if (sync.sitPos == currSitData.posID) return;
var data = FrameDataParser.Parse<FrameData_ChangeTurret>(sync.frameData);
list_Sits[sync.sitPos].LoadWing(data.turretIndex);
}
2026-08-28 22:33:35 +08:00
public void SyncGameData_ChangeZhanShen(SyncGameData sync)
{
if (sync.sitPos == currSitData.posID) return;
var data = FrameDataParser.Parse<FrameData_ChangeTurret>(sync.frameData);
// turretIndex<=0 = 卸下战神;>0 = 加载对应编号战神(内部会隐藏该座位的普通炮台)
if (data.turretIndex > 0)
list_Sits[sync.sitPos].LoadZhanShen(data.turretIndex);
else
list_Sits[sync.sitPos].ClearZhanShen();
}
2026-06-19 18:18:04 +08:00
public void SyncGameData_ChangeCannonMultiple(SyncGameData sync)
{
if (sync.sitPos == currSitData.posID) return;
var data = FrameDataParser.Parse<FrameData_ChangeCannonMultiple>(sync.frameData);
var sit = list_Sits[sync.sitPos];
sit.consumeGoldNum = data.cannonMultiple;
sit.tMP_TurretConsumeNum.text = data.cannonMultiple.ToString();
}
2026-07-30 23:54:35 +08:00
/// <summary>
/// 应用服务端确认的炮倍变更(ChangeGunMultipleNotice)。
/// 本地玩家切换炮倍后,等服务端广播此通知才更新 consumeGoldNum 和 UI,
/// 确保开火扣费时本地炮倍与服务端一致。
/// </summary>
public void ApplyCannonMultiple(string userId, int multiple)
{
var sit = list_Sits.Find(s => s.user == userId);
if (sit == null) return;
sit.consumeGoldNum = multiple;
sit.SyncIndexByConsume(multiple);
if (sit.tMP_TurretConsumeNum != null)
sit.tMP_TurretConsumeNum.text = multiple.ToString();
}
2026-07-30 22:09:55 +08:00
/// <summary>
/// 爆奖结束帧同步(baoJiangEnd):
/// 其他玩家爆奖流程完成,从 pending 取出对应 not-self 爆奖,
/// 显示最终 BJEndUI(开 tMP_beishu / 关 img_BaoJiangZhong)并加分。
/// 自己触发的爆奖不在这里处理(self 流程在 BJEventBase 内部已自行结束)。
/// </summary>
public void OnBaoJiangEnd(SyncGameData sync)
{
var data = FrameDataParser.Parse<FrameData_BaoJiangEnd>(sync.frameData);
if (data == null)
{
Debug.LogWarning("[GameMainPanel_FishBase] OnBaoJiangEnd: frameData 解析失败");
return;
}
// 自己触发的爆奖不在这里处理(self 流程已自行走完)
if (data.userId == GameManager.Instance.user.id) return;
BaoJiangNotSelfHandler.OnBaoJiangEnd(data.userId, data.fishId, data.beishu, data.score);
}
2026-06-19 18:18:04 +08:00
public void SyncCannonMultiple(int consumeGold)
{
if (currSitData == null) return;
SyncGameData sgd = new SyncGameData
{
sitPos = currSitData.posID,
frameType = FrameDataType.changeCannonMultiple,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
frameData = new FrameData_ChangeCannonMultiple { cannonMultiple = consumeGold }
};
WebSocketSendList.SyncGameData(sgd);
}
2026-07-20 22:32:52 +08:00
/// <summary>
/// UsePropNotice 回调:服务器确认道具使用后,激活对应效果
/// </summary>
public void OnUsePropNotice(string userId, string propNumber)
{
switch (propNumber)
{
2026-07-25 21:04:58 +08:00
case "40004": // 暴走 — 个人效果,只有使用者自己生效
// ⭐ 狂暴是个人增益(攻速翻倍+炮台动画),不是全房间效果。
// 服务器会把 UsePropNotice 广播给房间内所有人,必须判断 userId
// 否则别人用狂暴,本地也开了狂暴
if (userId != GameManager.Instance.user.id) break;
2026-07-20 22:32:52 +08:00
_localBaoZouCount--;
UpdateSkillPropsUI();
CreateFishManager.Instance.currGameType.isFrenzy = true;
SetFrenzy(true);
currFrenzyTime = frenzyDuration;
toggle_BaoZou.SetIsOnWithoutNotify(true);
break;
2026-07-23 01:51:07 +08:00
case "40003": // 冰封
2026-07-20 22:32:52 +08:00
{
2026-07-25 23:05:24 +08:00
// ⭐ 新协议:冰冻房间效果由服务端 FishFrozen 事件统一触发,
// UsePropNotice(40003) 只处理个人效果(数量/toggle)
// 房间效果(冻结鱼/暂停动画/冰冻特效)在 OnFishFrozen 中执行
2026-07-25 21:04:58 +08:00
bool isLocalSender = userId == GameManager.Instance.user.id;
if (isLocalSender)
{
_localBingFengCount--;
UpdateSkillPropsUI();
2026-07-25 23:05:24 +08:00
// 倒计时和 toggle 由 OnFishFrozen 统一启动,这里不重复设置
2026-07-25 21:04:58 +08:00
}
break;
2026-07-20 22:32:52 +08:00
}
}
}
2026-07-25 23:05:24 +08:00
/// <summary>
/// 服务端 FishFrozen 事件回调:冰冻道具(40003)生效。
/// 鱼存活时间延长 15 秒,客户端写死 15s 倒计时。
/// 所有客户端都执行(房间效果)。
/// </summary>
public void OnFishFrozen()
2026-05-28 18:16:45 +08:00
{
2026-07-25 23:05:24 +08:00
// 启动 15s 倒计时
currBingFengTime = bingFengDuration;
toggle_BingFeng.SetIsOnWithoutNotify(true);
2026-05-28 18:16:45 +08:00
2026-07-25 23:05:24 +08:00
// 冰冻特效
if (bingFengEffect != null)
2026-06-21 00:07:26 +08:00
{
2026-07-25 23:05:24 +08:00
if (_bingFengInstance == null)
_bingFengInstance = Instantiate(bingFengEffect);
_bingFengInstance.SetActive(true);
2026-06-21 00:07:26 +08:00
}
2026-07-25 23:05:24 +08:00
// 冻结所有鱼 + 暂停动画
CreateFishManager.Instance.FreezeAllFish();
PauseAllFishAnimations();
2026-05-28 18:16:45 +08:00
}
2026-07-25 23:05:24 +08:00
// SyncGameData_FreezeFish 已废弃:冰冻改由服务端 FishFrozen 事件统一触发
// SyncFreeze 已废弃:不再需要客户端广播冰冻状态
2026-05-04 17:21:52 +08:00
2026-06-21 00:07:26 +08:00
#endregion
#region 冰冻动画暂停/恢复
private void PauseAllFishAnimations()
{
2026-07-25 21:04:58 +08:00
// ⭐ 守卫:已暂停时直接返回,避免重复调用覆盖已记录的速度
// (其他客户端会先后被 OnUsePropNotice 和 SyncGameData_FreezeFish 触发两次,
// 第二次会记录 speed=0,解冻时恢复成 0 → 动画永远暂停)
if (_frozenAnimSpeeds.Count > 0) return;
2026-06-21 00:07:26 +08:00
var root = CreateFishManager.Instance.fishRoot;
if (root == null) return;
_frozenAnimSpeeds.Clear();
foreach (var s in root.GetComponentsInChildren<Spine.Unity.SkeletonAnimation>(true))
{
if (!s.gameObject.activeInHierarchy) continue;
_frozenAnimSpeeds[s] = s.timeScale;
s.timeScale = 0f;
}
foreach (var a in root.GetComponentsInChildren<Animator>(true))
{
if (!a.gameObject.activeInHierarchy) continue;
_frozenAnimSpeeds[a] = a.speed;
a.speed = 0f;
}
foreach (var an in root.GetComponentsInChildren<Animation>(true))
{
if (!an.gameObject.activeInHierarchy || !an.isPlaying) continue;
2026-07-24 23:47:50 +08:00
// ⭐ 记录每个 AnimationState 的原 speed,解冻时恢复原值而非硬编码 1f
var stateSpeeds = new List<float>();
foreach (AnimationState state in an)
{
stateSpeeds.Add(state.speed);
state.speed = 0f;
}
_frozenAnimSpeeds[an] = stateSpeeds;
2026-06-21 00:07:26 +08:00
}
foreach (var pd in root.GetComponentsInChildren<UnityEngine.Playables.PlayableDirector>(true))
{
if (!pd.gameObject.activeInHierarchy || pd.state != PlayState.Playing) continue;
_frozenAnimSpeeds[pd] = 1f;
pd.Pause();
}
}
private void ResumeAllFishAnimations()
{
if (_frozenAnimSpeeds.Count == 0) return;
foreach (var kv in _frozenAnimSpeeds)
{
if (kv.Key == null) continue;
switch (kv.Key)
{
2026-07-24 23:47:50 +08:00
case Spine.Unity.SkeletonAnimation s: s.timeScale = (float)kv.Value; break;
case Animator a: a.speed = (float)kv.Value; break;
2026-06-21 00:07:26 +08:00
case Animation an:
2026-07-24 23:47:50 +08:00
// ⭐ 恢复每个 AnimationState 的原 speed
if (kv.Value is List<float> stateSpeeds)
{
int i = 0;
foreach (AnimationState state in an)
{
if (i < stateSpeeds.Count)
state.speed = stateSpeeds[i];
i++;
}
}
2026-06-21 00:07:26 +08:00
break;
case UnityEngine.Playables.PlayableDirector pd: pd.Resume(); break;
}
}
_frozenAnimSpeeds.Clear();
}
2026-05-04 17:21:52 +08:00
#endregion
#region 玩家加入/退出(PlayerJoinModule)
2026-08-28 22:33:35 +08:00
public void JoinGame(int sitNum, long goldNum, long diamondNum, long consumeGold, int currentTurretIndex, string user = null, string cannonId = null, string wingId = null, string warGodId = null)
2026-05-04 17:21:52 +08:00
{
2026-05-27 19:47:06 +08:00
foreach (var sit in list_Sits)
{
if (sit.posID != sitNum) continue;
2026-05-04 17:21:52 +08:00
2026-07-16 20:16:05 +08:00
// ⭐ 确保座位 GameObject 本身是激活的(可能被 HandleBotExit 隐藏或初始为 inactive)
if (!sit.gameObject.activeSelf)
sit.gameObject.SetActive(true);
2026-05-27 19:47:06 +08:00
sit.user = user ?? GameManager.Instance.user.id;
sit.goldNum = goldNum;
sit.consumeGoldNum = consumeGold;
2026-07-29 17:17:14 +08:00
2026-07-29 22:59:26 +08:00
// ⭐ 记录金币流水:已加钱(进房设置初始金币,本地已执行)
GameManager.LogGoldFlowAppliedGain("JoinGame", $"座位={sitNum} 初始金币={goldNum} 钻石={diamondNum} 炮台消耗={consumeGold} 房间={GameManager.Instance.gameData?.currRoomData?.roomId}");
2026-07-19 18:24:32 +08:00
// ⭐ 同步 indexConsume 到后端下发的 cannonMultiple 对应档位,避免加减炮倍时从 0 档跳变
sit.SyncIndexByConsume(consumeGold);
2026-05-04 17:21:52 +08:00
2026-07-03 18:55:56 +08:00
// ⭐ 在 ShowPlayer 之前标记 isBot,否则 ShowPlayer 里的 AssignRoomOwner 会把 bot 设为房主
if (!string.IsNullOrEmpty(user) && user.StartsWith("bot_"))
sit.isBot = true;
2026-07-11 17:07:32 +08:00
// bot 进场:记录运行时状态(进场金币 / 初始档位)
if (sit.isBot)
InitBotRuntimeState(sit, goldNum);
2026-05-27 19:47:06 +08:00
sit.tMP_GoldNum.text = goldNum.ToString();
2026-07-28 18:29:49 +08:00
// XJMH 模式不显示钻石,tMP_DiamondNum 已隐藏
if (sit.turretControlMode != TurretControlMode.XJMH && sit.tMP_DiamondNum != null)
sit.tMP_DiamondNum.text = diamondNum.ToString();
2026-05-27 19:47:06 +08:00
sit.tMP_TurretConsumeNum.text = consumeGold.ToString();
2026-05-04 17:21:52 +08:00
2026-07-02 18:28:50 +08:00
sit.ShowPlayer();
2026-06-08 21:23:40 +08:00
2026-07-28 18:29:49 +08:00
// XJMH 模式:炮台是座位自带子对象,Awake 已初始化引用,不需要加载
if (sit.turretControlMode != TurretControlMode.XJMH)
2026-07-03 18:55:56 +08:00
{
2026-07-28 18:29:49 +08:00
if (!string.IsNullOrEmpty(cannonId) && int.TryParse(cannonId, out int turretNum))
sit.LoadTurret(turretNum);
else if (sit.isBot)
{
var availableTurrets = sit.GetAvailableTurretNumbers();
if (availableTurrets != null && availableTurrets.Count > 0)
sit.LoadTurret(availableTurrets[0]);
}
2026-06-30 01:31:53 +08:00
2026-07-28 18:29:49 +08:00
if (!string.IsNullOrEmpty(wingId) && int.TryParse(wingId, out int wingNum))
sit.LoadWing(wingNum);
2026-08-28 22:33:35 +08:00
// 战神:有装备就加载(内部会隐藏该座位的普通炮台,炮台/战神二选一)
if (!string.IsNullOrEmpty(warGodId) && int.TryParse(warGodId, out int zsNum) && zsNum > 0)
sit.LoadZhanShen(zsNum);
2026-07-28 18:29:49 +08:00
}
2026-05-27 19:47:06 +08:00
}
2026-05-04 17:21:52 +08:00
}
2026-05-27 19:47:06 +08:00
public void TuiChuHHWK()
{
2026-07-30 01:06:55 +08:00
RequestLeaveRoom();
2026-05-27 19:47:06 +08:00
}
2026-05-04 17:21:52 +08:00
2026-05-27 19:47:06 +08:00
#endregion
2026-05-04 17:21:52 +08:00
2026-06-16 19:12:37 +08:00
#region 人机AI
private void HandleBotAI()
{
if (SitPosData.roomOwnerSit == null || currSitData != SitPosData.roomOwnerSit) return;
2026-07-05 16:07:22 +08:00
2026-07-14 02:48:54 +08:00
// ⭐ enableBot 是总开关(Inspector),优先级最高;cfg.enabled 是子开关
if (!GameManager.Instance.enableBot) return;
2026-07-05 16:07:22 +08:00
var cfg = GameManager.Instance.botConfig;
2026-07-14 02:48:54 +08:00
if (cfg == null || !cfg.enabled) return;
2026-06-16 19:12:37 +08:00
var fishList = CreateFishManager.Instance;
if (fishList == null || fishList.fishDatas_hashCodekey == null) return;
2026-07-05 16:07:22 +08:00
BotBehaviorConfig behavior = GetBotBehavior(cfg);
float rotateSpeed = behavior != null ? behavior.rotateSpeed : botRotateSpeed;
2026-06-16 19:12:37 +08:00
foreach (var sit in list_Sits)
{
if (!sit.isBot || sit.gameObject.activeSelf == false) continue;
if (sit.tra_CurrTurret == null) continue;
2026-07-11 17:07:32 +08:00
// ====== 运行时状态 ======
BotRuntimeState state = GetBotRuntimeState(sit);
if (state == null || state.hasExited) continue;
// ====== 退出检查 ======
if (CheckBotExit(sit, state, cfg)) continue;
2026-07-16 20:16:05 +08:00
Vector3 turretPos = sit.turretWorldPos;
2026-07-03 18:55:56 +08:00
2026-07-11 17:07:32 +08:00
// ====== 技能刷新(底层随机技能) ======
2026-07-05 16:07:22 +08:00
if (cfg != null && cfg.skills != null)
RefreshBotSkills(sit, cfg.skills);
var skill = botSkills.ContainsKey(sit.posID) ? botSkills[sit.posID] : null;
2026-07-03 18:55:56 +08:00
2026-07-11 17:07:32 +08:00
// ====== Boss 检测 → 决策行为模式 ======
List<MonsterContral> bosses = FindBossesInField(sit, fishList, turretPos, cfg);
BotBehaviorMode mode = DecideBotMode(sit, state, bosses, cfg, skill);
state.currentMode = mode;
2026-07-05 16:07:22 +08:00
2026-07-11 17:07:32 +08:00
// ====== 倍数调整(第一次:基于进场金币比率) ======
AdjustBotMultiplier(sit, state, cfg);
// ====== 目标选择 ======
MonsterContral target = GetOrFindBotTarget(sit, fishList, turretPos, skill, mode, bosses, cfg);
2026-07-05 17:39:30 +08:00
if (target == null) continue;
2026-07-11 17:07:32 +08:00
// ====== 锁定技能:走真实锁定系统 ======
bool wantLock = (skill != null && skill.lockActive) || mode == BotBehaviorMode.LockBoss;
if (wantLock)
2026-06-16 19:12:37 +08:00
{
2026-07-05 17:39:30 +08:00
if (sit.monCotl_Lock != target)
{
2026-07-22 03:41:06 +08:00
// ⭐ 统一用 SetLockTarget:内部会设置 isLock / monCotl_Lock / monCotl_Lock_Position / lockFX.SetActive
sit.SetLockTarget(target, ResolveLockTransform(target));
// ⭐ 广播锁定状态给其他客户端(节流:目标 guid 变化才广播)
SyncBotLock(sit, target.guid);
2026-07-05 17:39:30 +08:00
}
2026-06-16 19:12:37 +08:00
}
2026-07-05 17:39:30 +08:00
else
{
if (sit.isLock)
{
2026-07-22 03:41:06 +08:00
// ⭐ 统一用 ClearLock:内部会清空 isLock / monCotl_Lock / lockFX.SetActive(false)
sit.ClearLock();
// ⭐ 广播取消锁定给其他客户端
SyncBotLock(sit, null);
2026-07-05 17:39:30 +08:00
}
}
2026-07-11 17:07:32 +08:00
// ====== 旋转 ======
2026-07-05 17:39:30 +08:00
RotateBotToTarget(sit, target.transform.position, rotateSpeed);
2026-07-03 18:55:56 +08:00
2026-07-11 17:07:32 +08:00
// ====== 倍数再调整(第二次:策略执行后再校验) ======
AdjustBotMultiplier(sit, state, cfg);
// ====== 射击 ======
2026-06-16 19:12:37 +08:00
if (!botFireTimer.ContainsKey(sit.posID))
botFireTimer[sit.posID] = 0f;
botFireTimer[sit.posID] += Time.deltaTime;
2026-07-05 16:07:22 +08:00
float fireInterval = behavior != null
? Random.Range(behavior.fireIntervalMin, behavior.fireIntervalMax)
: attackTime;
if (skill != null && skill.frenzyActive)
fireInterval *= 0.5f;
if (botFireTimer[sit.posID] >= fireInterval)
2026-06-16 19:12:37 +08:00
{
botFireTimer[sit.posID] = 0f;
2026-07-11 17:07:32 +08:00
BotFire(sit, target, skill, mode, cfg, behavior);
2026-06-16 19:12:37 +08:00
}
2026-07-05 17:39:30 +08:00
// 空闲超时切回 Idle 动画
if (!botIdleTimer.ContainsKey(sit.posID))
botIdleTimer[sit.posID] = 0f;
botIdleTimer[sit.posID] += Time.deltaTime;
if (botIdleTimer[sit.posID] > 0.3f)
2026-07-25 21:04:58 +08:00
{
sit.ChangeState(TurretState.Idle);
// ⭐ 广播炮台闲置状态给其他客户端(节流:状态变化才广播)
SyncBotTurretState(sit, TurretState.Idle);
botIdleTimer[sit.posID] = 0f;
}
2026-06-16 19:12:37 +08:00
}
}
2026-07-22 16:47:11 +08:00
/// <summary>
/// bot 进场时初始化运行时状态。
/// 按流程图:进场金币 / 入场限制金币 → 判断倍数等级 → 在对应档位区间随机选一个初始档位。
/// 这样多个 bot 即使进场金币相同(例如都是 1000),初始炮倍也会不一样。
/// </summary>
2026-07-11 17:07:32 +08:00
private void InitBotRuntimeState(SitPosData sit, long entryGold)
{
var cfg = GameManager.Instance.botConfig;
2026-07-22 16:47:11 +08:00
int cfgInitIdx = cfg != null && cfg.multiplier != null ? cfg.multiplier.initialLevelIndex : 0;
// ⭐ 按流程图:识别进场金币是入场限制金币的多少倍 → 判断等级 → 区间内随机选初始档位
int count = GetPaoBeiCount();
int initIdx = CalcBotInitialLevelIndex(entryGold, count, cfg, cfgInitIdx);
2026-07-11 17:07:32 +08:00
var s = new BotRuntimeState
{
entryGold = entryGold,
entryTime = Time.time,
currentLevelIndex = initIdx,
2026-07-27 16:59:22 +08:00
// ⭐ 初始检查时间加随机偏移(0~3s),避免多个 bot 同时换炮倍
nextMultiplierCheckTime = Time.time + 2f + Random.Range(0f, 3f),
2026-07-11 17:07:32 +08:00
nextExitCheckTime = Time.time + 5f,
hasExited = false
};
botRuntimeStates[sit.posID] = s;
// 把初始档位应用到炮台
ApplyBotLevelIndex(sit, initIdx);
}
2026-07-22 16:47:11 +08:00
/// <summary>
/// 按进场金币/入场限制金币的倍数,在对应档位区间内随机选一个初始档位。
/// 倍数越高,初始档位区间越高;区间内随机,保证多个 bot 即使金币相同炮倍也不同。
/// 读不到 roomGoldLimit 或档位不足时,降级用 cfgInitIdx + 随机偏移。
/// </summary>
private int CalcBotInitialLevelIndex(long entryGold, int count, BotConfig cfg, int cfgInitIdx)
{
if (count <= 1) return Mathf.Max(0, cfgInitIdx);
// 配置允许的档位范围 [minIdx, maxIdx]
int minIdx = Mathf.Max(0, cfg != null && cfg.multiplier != null ? cfg.multiplier.minLevelIndex : 0);
int maxIdx = cfg != null && cfg.multiplier != null && cfg.multiplier.maxLevelIndex >= 0
? Mathf.Min(cfg.multiplier.maxLevelIndex, count - 1)
: count - 1;
if (maxIdx < minIdx) maxIdx = minIdx;
if (maxIdx <= minIdx) return minIdx;
// 读取当前房间的入场金币限制
2026-08-09 17:57:46 +08:00
long roomGoldLimit = GetCurrentRoomGoldLimit();
2026-07-22 16:47:11 +08:00
if (roomGoldLimit <= 0 || entryGold <= 0)
{
// 读不到限制 → 在 [minIdx, maxIdx] 全区间随机
return Random.Range(minIdx, maxIdx + 1);
}
// ratio = 进场金币 / 入场限制金币
float ratio = (float)entryGold / roomGoldLimit;
int span = maxIdx - minIdx;
// 根据 ratio 划分三档区间:低(<1) / 中(1~2) / 高(>2)
int lo, hi;
if (ratio < 1f)
{
lo = minIdx;
hi = minIdx + Mathf.Max(0, span / 3);
}
else if (ratio < 2f)
{
lo = minIdx + Mathf.Max(0, span / 3);
hi = minIdx + Mathf.Max(0, span * 2 / 3);
}
else
{
lo = minIdx + Mathf.Max(0, span * 2 / 3);
hi = maxIdx;
}
if (lo > hi) lo = hi;
// 在 [lo, hi] 区间内随机选一个档位
return Random.Range(lo, hi + 1);
}
/// <summary>获取当前房间的入场金币限制(用于 bot 进场按金币倍数判断初始档位)</summary>
2026-08-09 17:57:46 +08:00
private long GetCurrentRoomGoldLimit()
2026-07-22 16:47:11 +08:00
{
var roomData = GameManager.Instance?.gameData?.currRoomData;
if (roomData == null) return 0;
int t = roomData.gameType;
int l = roomData.roomLevel;
var configs = GameManager.Instance?.gameConfig?.roomConfigs;
if (configs != null)
{
foreach (var item in configs)
{
if (item.gameType == t && l == item.roomLevel)
return item.roomGoldLimit;
}
}
return 0;
}
2026-07-11 17:07:32 +08:00
/// <summary>获取 bot 运行时状态</summary>
private BotRuntimeState GetBotRuntimeState(SitPosData sit)
{
if (botRuntimeStates.TryGetValue(sit.posID, out var s)) return s;
// 兜底:未初始化时按当前金币建一份
InitBotRuntimeState(sit, sit.goldNum);
return botRuntimeStates.TryGetValue(sit.posID, out s) ? s : null;
}
/// <summary>查找场内炮台前方的 Boss 列表</summary>
private List<MonsterContral> FindBossesInField(SitPosData sit, CreateFishManager fishList, Vector3 turretPos, BotConfig cfg)
{
var result = new List<MonsterContral>();
if (cfg == null || cfg.bossStrategy == null) return result;
var bossTypes = cfg.bossStrategy.bossFishTypes;
if (bossTypes == null || bossTypes.Length == 0) return result;
foreach (var kv in fishList.fishDatas_hashCodekey)
{
var m = kv.Value;
if (m == null || !m.gameObject.activeInHierarchy) continue;
2026-07-16 20:16:05 +08:00
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的 Boss 不作为目标
if (!IsFishPathLockable(m)) continue;
2026-07-11 17:07:32 +08:00
int ft = (int)m.fishenum;
bool isBoss = false;
foreach (var bt in bossTypes) { if (bt == ft) { isBoss = true; break; } }
if (!isBoss) continue;
Vector3 fishPos = m.transform.position;
if (sit.IsFlipped && fishPos.y >= turretPos.y) continue;
if (!sit.IsFlipped && fishPos.y <= turretPos.y) continue;
result.Add(m);
}
return result;
}
/// <summary>根据 Boss 存在情况决策行为模式,并覆盖技能开关</summary>
private BotBehaviorMode DecideBotMode(SitPosData sit, BotRuntimeState state, List<MonsterContral> bosses, BotConfig cfg, BotSkillRuntime skill)
{
if (cfg == null || cfg.bossStrategy == null) return BotBehaviorMode.ScatterRandom;
var bs = cfg.bossStrategy;
// 狂暴到期清理
if (state.frenzyActive && Time.time >= state.frenzyEndTime)
state.frenzyActive = false;
if (bosses == null || bosses.Count == 0)
{
// ===== 无Boss → 散射乱射 =====
// 随机选择定点三发或单射:覆盖 skill.tripleActive
if (skill != null)
skill.tripleActive = Random.value < bs.tripleShotProb;
// 无Boss时关闭锁定Boss模式
if (skill != null) skill.lockActive = false;
return bs.scatterWhenNoBoss ? BotBehaviorMode.ScatterRandom : BotBehaviorMode.Idle;
}
// ===== 有Boss =====
if (bosses.Count > bs.scatterModeThreshold)
{
// Boss > 阈值 → 散射模式
if (skill != null) skill.tripleActive = true;
return BotBehaviorMode.ScatterMode;
}
// Boss <= 阈值 → 随机狂暴
if (!state.frenzyActive && Random.value < bs.frenzyProb)
{
state.frenzyActive = true;
state.frenzyEndTime = Time.time + Random.Range(bs.frenzyMinTime, bs.frenzyMaxTime);
}
if (state.frenzyActive && skill != null)
skill.frenzyActive = true;
// 锁定最大倍数Boss
return BotBehaviorMode.LockBoss;
}
/// <summary>倍数升降:当前金币/进场金币 < 阈值降档, > 阈值升档</summary>
private void AdjustBotMultiplier(SitPosData sit, BotRuntimeState state, BotConfig cfg)
{
if (cfg == null || cfg.multiplier == null) return;
var mc = cfg.multiplier;
if (Time.time < state.nextMultiplierCheckTime) return;
2026-07-27 16:59:22 +08:00
// ⭐ 检查间隔加随机抖动(0.5~1.5 倍),避免多个 bot 同时换炮倍
state.nextMultiplierCheckTime = Time.time + mc.checkInterval * Random.Range(0.5f, 1.5f);
2026-07-11 17:07:32 +08:00
float ratio = state.GetGoldRatio(sit.goldNum);
int maxIdx = mc.maxLevelIndex < 0 ? GetPaoBeiCount() - 1 : mc.maxLevelIndex;
int minIdx = Mathf.Max(0, mc.minLevelIndex);
if (ratio < mc.decreaseThreshold && state.currentLevelIndex > minIdx)
{
state.currentLevelIndex = Mathf.Max(minIdx, state.currentLevelIndex - mc.step);
ApplyBotLevelIndex(sit, state.currentLevelIndex);
}
else if (ratio > mc.increaseThreshold && state.currentLevelIndex < maxIdx)
{
state.currentLevelIndex = Mathf.Min(maxIdx, state.currentLevelIndex + mc.step);
ApplyBotLevelIndex(sit, state.currentLevelIndex);
}
}
/// <summary>当前炮倍档位总数(paoBeiList 长度)</summary>
private int GetPaoBeiCount()
{
if (paoBeiList != null && paoBeiList.Count > 0) return paoBeiList.Count;
return 1;
}
2026-07-21 01:06:09 +08:00
/// <summary>
/// 按档位索引设置 bot 炮台消耗。
/// 同时更新底层数据 consumeGoldNum、文本显示,并广播给其他客户端,
/// 保证所有客户端的 bot 炮倍一致(发射子弹扣金币、打中鱼计算奖励都用最新值)。
/// 不走 SyncCannonMultiple(那个用的是本地玩家 currSitData.posID,不能给 bot 用)。
/// </summary>
2026-07-11 17:07:32 +08:00
private void ApplyBotLevelIndex(SitPosData sit, int targetIndex)
{
int count = GetPaoBeiCount();
if (count <= 0 || paoBeiList == null || paoBeiList.Count == 0) return;
targetIndex = Mathf.Clamp(targetIndex, 0, count - 1);
sit.consumeGoldNum = paoBeiList[targetIndex];
if (sit.tMP_TurretConsumeNum != null)
sit.tMP_TurretConsumeNum.text = sit.consumeGoldNum.ToString();
2026-07-21 01:06:09 +08:00
// ⭐ 同步 indexConsume,保持档位索引与 consumeGoldNum 一致
sit.SyncIndexByConsume(sit.consumeGoldNum);
// ⭐ 广播 bot 炮倍变化给其他客户端(用 bot 自己的 sitPos),
// 接收端 SyncGameData_ChangeCannonMultiple 会根据 sitPos 更新对应座位
var sgd = new SyncGameData
{
sitPos = sit.posID,
frameType = FrameDataType.changeCannonMultiple,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
frameData = new FrameData_ChangeCannonMultiple { cannonMultiple = (int)sit.consumeGoldNum }
};
WebSocketSendList.SyncGameData(sgd);
2026-07-11 17:07:32 +08:00
}
/// <summary>退出条件检查:金币>设定 或 金币<设定</summary>
private bool CheckBotExit(SitPosData sit, BotRuntimeState state, BotConfig cfg)
{
if (cfg == null || cfg.exitCondition == null) return false;
var ec = cfg.exitCondition;
if (Time.time < state.nextExitCheckTime) return false;
state.nextExitCheckTime = Time.time + ec.checkInterval;
bool shouldExit = false;
if (ec.exitWhenGoldAbove > 0 && sit.goldNum >= ec.exitWhenGoldAbove) shouldExit = true;
else if (sit.goldNum <= ec.exitWhenGoldBelow) shouldExit = true;
if (!shouldExit) return false;
HandleBotExit(sit, state, cfg);
return true;
}
2026-07-16 20:16:05 +08:00
/// <summary>处理 bot 退出:发离房消息、座位清理、转移房主、保证出鱼</summary>
2026-07-11 17:07:32 +08:00
private void HandleBotExit(SitPosData sit, BotRuntimeState state, BotConfig cfg)
{
state.hasExited = true;
state.currentMode = BotBehaviorMode.Exited;
if (cfg != null && cfg.exitCondition != null && cfg.exitCondition.sendLeaveRoom)
{
string roomId = GameManager.Instance.gameData.currRoomData.roomId;
WebSocketSendList.LeaveRoom(roomId, sit.user);
}
2026-07-22 03:41:06 +08:00
// 清理锁定(统一用 ClearLock,并广播取消锁定给其他客户端)
if (sit.isLock)
{
sit.ClearLock();
SyncBotLock(sit, null);
}
2026-07-11 17:07:32 +08:00
// 清理记忆
botTargetGuid.Remove(sit.posID);
2026-07-22 03:41:06 +08:00
botLastSyncLockGuid.Remove(sit.posID);
2026-07-25 21:04:58 +08:00
botLastSyncTurretState.Remove(sit.posID);
2026-07-11 17:07:32 +08:00
botFireTimer.Remove(sit.posID);
botIdleTimer.Remove(sit.posID);
botSkills.Remove(sit.posID);
botLastAngle.Remove(sit.user);
botLastFireData.Remove(sit.user);
2026-07-25 23:05:24 +08:00
// ⭐ bot 退出后转移房主,否则房主是已退出的 bot 会导致房主显示异常
2026-07-16 20:16:05 +08:00
// 1) 清除 bot 的房主引用(如果它是房主)
// 2) TransferRoomOwner 会把房主交给第一个活跃的非 bot 玩家
2026-07-25 23:05:24 +08:00
// 注:鱼创建已由服务端 OnFishSpawn 统一下发,不再需要 fangzhu 兜底
2026-07-16 20:16:05 +08:00
if (SitPosData.roomOwnerSit == sit)
{
SitPosData.roomOwnerSit = null;
SitPosData.TransferRoomOwner();
}
2026-07-11 17:07:32 +08:00
if (cfg != null && cfg.exitCondition != null && cfg.exitCondition.showWaitingAfterExit)
sit.ShowWaiting();
else
sit.gameObject.SetActive(false);
}
/// <summary>bot 开火执行(从 HandleBotAI 抽出)</summary>
private void BotFire(SitPosData sit, MonsterContral target, BotSkillRuntime skill, BotBehaviorMode mode, BotConfig cfg, BotBehaviorConfig behavior)
{
sit.ChangeState(TurretState.Attack);
2026-07-25 21:04:58 +08:00
// ⭐ 广播炮台攻击状态给其他客户端(节流:状态变化才广播)
SyncBotTurretState(sit, TurretState.Attack);
2026-07-11 17:07:32 +08:00
if (!botIdleTimer.ContainsKey(sit.posID))
botIdleTimer[sit.posID] = 0f;
botIdleTimer[sit.posID] = 0f;
float angle = sit.currentAngle;
botLastAngle[sit.user] = angle;
bool isSanShe = (skill != null && skill.tripleActive) || mode == BotBehaviorMode.ScatterMode || mode == BotBehaviorMode.ScatterRandom;
bool isDouble = skill != null && skill.doubleActive;
2026-07-23 01:51:07 +08:00
// ⭐ isLock 包含 RandomSweep 锁定跟随子模式,保证子弹追踪锁定目标
bool isLockFollow = mode == BotBehaviorMode.RandomSweep
&& botRuntimeStates.TryGetValue(sit.posID, out var rst) && rst.sweepIsLockFollow;
bool isLock = (skill != null && skill.lockActive) || mode == BotBehaviorMode.LockBoss || isLockFollow;
2026-07-11 17:07:32 +08:00
int count = isSanShe ? 3 : 1;
long gold = isDouble ? sit.consumeGoldNum * 2 : sit.consumeGoldNum;
// 三发+锁定 → 找 N 个目标鱼的 GUID
List<string> monsterIDs = null;
if (isSanShe && isLock && target != null)
{
monsterIDs = new List<string> { target.guid };
var extraTargets = FindBotLockTargets(sit, target, 2);
foreach (var m in extraTargets) monsterIDs.Add(m.guid);
}
botLastFireData[sit.user] = new FrameData_FireBullet
{
angle = angle,
gold = gold,
isSanShe = isSanShe,
twoRTruuect = isDouble,
isSuoDing = isLock,
monsterIDs = monsterIDs
};
WebSocketSendList.BotFireCannon(
GameManager.Instance.gameData.currRoomData.roomId,
sit.user, gold, count);
SyncFire_ForBot(sit, angle, gold, isSanShe, isDouble, isLock, monsterIDs);
}
2026-07-05 17:39:30 +08:00
/// <summary>
/// 获取或寻找目标:优先用记忆的目标(还在前方就沿用),死了/不在前方了再重新找
/// </summary>
2026-07-11 17:07:32 +08:00
private MonsterContral GetOrFindBotTarget(SitPosData sit, CreateFishManager fishList, Vector3 turretPos, BotSkillRuntime skill,
BotBehaviorMode mode, List<MonsterContral> bosses, BotConfig cfg)
2026-07-05 17:39:30 +08:00
{
2026-07-11 17:07:32 +08:00
bool isLocked = (skill != null && skill.lockActive) || mode == BotBehaviorMode.LockBoss;
2026-07-05 18:52:09 +08:00
2026-07-05 17:39:30 +08:00
// 1. 检查记忆目标是否还有效
if (botTargetGuid.TryGetValue(sit.posID, out string guid))
{
if (fishList.fishDatas_hashCodekey.TryGetValue(guid, out var m)
&& m != null && m.gameObject.activeInHierarchy)
{
2026-07-16 20:16:05 +08:00
// ⭐ 路径进度过滤:如果记忆目标已经超出可锁定范围(即将离场),清除记忆重新找
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);
2026-07-05 17:39:30 +08:00
}
}
2026-07-11 17:07:32 +08:00
// 2. 锁定Boss模式 → 优先最大倍率Boss
if (mode == BotBehaviorMode.LockBoss && bosses != null && bosses.Count > 0
&& cfg != null && cfg.bossStrategy != null && cfg.bossStrategy.preferMaxMultiplierBoss)
{
MonsterContral bossTarget = SelectMaxMultiplierBoss(bosses);
if (bossTarget != null)
{
botTargetGuid[sit.posID] = bossTarget.guid;
return bossTarget;
}
}
// 3. 散射模式 → 在 Boss 中随机选一个(覆盖面广)
if (mode == BotBehaviorMode.ScatterMode && bosses != null && bosses.Count > 0)
{
MonsterContral bossTarget = bosses[Random.Range(0, bosses.Count)];
botTargetGuid[sit.posID] = bossTarget.guid;
return bossTarget;
}
// 4. 重新找目标(Boss > 黄金鱼 > 特殊鱼 > 普通鱼,同类型打最近)
2026-07-05 17:39:30 +08:00
MonsterContral target = SelectBestTarget(sit, fishList, turretPos);
if (target != null)
botTargetGuid[sit.posID] = target.guid;
return target;
}
2026-07-11 17:07:32 +08:00
2026-07-11 19:05:27 +08:00
/// <summary>从 Boss 列表中选倍率(beilv)最大的(跳过已死亡的)</summary>
2026-07-11 17:07:32 +08:00
private MonsterContral SelectMaxMultiplierBoss(List<MonsterContral> bosses)
{
MonsterContral best = null;
int maxBeiLv = int.MinValue;
foreach (var b in bosses)
{
2026-07-11 19:05:27 +08:00
if (b == null || !b.gameObject.activeInHierarchy) continue;
2026-07-11 17:07:32 +08:00
if (b.beilv > maxBeiLv)
{
maxBeiLv = b.beilv;
best = b;
}
}
return best;
}
2026-07-05 17:39:30 +08:00
2026-07-05 16:07:22 +08:00
/// <summary>
/// 按优先级选目标:特殊Boss(5) > Boss(2) > 黄金鱼(3) > 特殊鱼(4) > 普通鱼(1)
/// 同一优先级内选最近的,只选炮台前方的鱼
/// </summary>
2026-07-05 17:39:30 +08:00
private MonsterContral SelectBestTarget(SitPosData sit, CreateFishManager fishList, Vector3 turretPos)
2026-07-05 16:07:22 +08:00
{
MonsterContral best = null;
float bestDist = float.MaxValue;
int bestPri = int.MaxValue;
foreach (var kv in fishList.fishDatas_hashCodekey)
{
var m = kv.Value;
if (m == null || !m.gameObject.activeInHierarchy) continue;
2026-07-16 20:16:05 +08:00
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的鱼不作为目标
if (!IsFishPathLockable(m)) continue;
2026-07-05 16:07:22 +08:00
Vector3 fishPos = m.transform.position;
if (sit.IsFlipped && fishPos.y >= turretPos.y) continue;
if (!sit.IsFlipped && fishPos.y <= turretPos.y) continue;
float d = (fishPos - turretPos).sqrMagnitude;
int pri = GetFishPriority((int)m.fishenum);
if (pri < bestPri || (pri == bestPri && d < bestDist))
{
bestPri = pri;
bestDist = d;
best = m;
}
}
return best;
}
private int GetFishPriority(int fishType)
{
switch (fishType)
{
case 5: return 0; // 特殊Boss — 最高优先
case 2: return 1; // Boss鱼
case 3: return 2; // 黄金鱼
case 4: return 3; // 特殊鱼
default: return 4; // 普通鱼
}
}
2026-07-05 18:52:09 +08:00
/// <summary>找 bot 三发锁定额外目标(排除已选中的)</summary>
private List<MonsterContral> FindBotLockTargets(SitPosData sit, MonsterContral exclude, int count)
{
var result = new List<MonsterContral>();
if (CreateFishManager.Instance == null) return result;
var fishList = CreateFishManager.Instance;
2026-07-16 20:16:05 +08:00
// ⭐ 用世界坐标(Z=0平面),和 bot AI / 玩家自动锁定一致
Vector3 turretPos = sit.turretWorldPos;
2026-07-05 18:52:09 +08:00
foreach (var kv in fishList.fishDatas_hashCodekey)
{
var m = kv.Value;
if (m == null || !m.gameObject.activeInHierarchy) continue;
if (m == exclude) continue;
2026-07-16 20:16:05 +08:00
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的鱼不作为目标
if (!IsFishPathLockable(m)) continue;
2026-07-05 18:52:09 +08:00
Vector3 fishPos = m.transform.position;
if (sit.IsFlipped && fishPos.y >= turretPos.y) continue;
if (!sit.IsFlipped && fishPos.y <= turretPos.y) continue;
result.Add(m);
if (result.Count >= count) break;
}
// 按优先级排序取前 count 个
result.Sort((a, b) =>
{
int pa = GetFishPriority((int)a.fishenum);
int pb = GetFishPriority((int)b.fishenum);
if (pa != pb) return pa.CompareTo(pb);
float da = (a.transform.position - turretPos).sqrMagnitude;
float db = (b.transform.position - turretPos).sqrMagnitude;
return da.CompareTo(db);
});
if (result.Count > count)
result.RemoveRange(count, result.Count - count);
return result;
}
2026-07-05 16:07:22 +08:00
/// <summary>根据 aiState 取对应行为参数,cfg 为 null 时返回 null</summary>
private BotBehaviorConfig GetBotBehavior(BotConfig cfg)
{
if (cfg == null) return null;
switch (cfg.aiState)
{
case "idle": return cfg.idleBehavior ?? cfg.activeBehavior;
case "aggressive": return cfg.aggressiveBehavior ?? cfg.activeBehavior;
default: return cfg.activeBehavior;
}
}
/// <summary>技能掷骰子:每隔 skillRefreshInterval 秒独立决定各技能开关</summary>
private void RefreshBotSkills(SitPosData sit, BotSkillConfig skillCfg)
{
if (skillCfg == null) return;
if (!botSkills.TryGetValue(sit.posID, out var s))
{
s = new BotSkillRuntime();
botSkills[sit.posID] = s;
}
if (Time.time < s.nextRefreshTime) return;
s.nextRefreshTime = Time.time + skillCfg.skillRefreshInterval;
// 独立掷骰子:还在持续中就沿用,否则重新掷
s.lockActive = (s.lockActive && Time.time < s.lockEndTime)
|| Random.value < skillCfg.lockProb;
if (s.lockActive && Time.time >= s.lockEndTime)
s.lockEndTime = Time.time + Random.Range(skillCfg.lockMinTime, skillCfg.lockMaxTime);
s.frenzyActive = (s.frenzyActive && Time.time < s.frenzyEndTime)
|| Random.value < skillCfg.frenzyProb;
if (s.frenzyActive && Time.time >= s.frenzyEndTime)
s.frenzyEndTime = Time.time + Random.Range(skillCfg.frenzyMinTime, skillCfg.frenzyMaxTime);
s.tripleActive = (s.tripleActive && Time.time < s.tripleEndTime)
|| Random.value < skillCfg.tripleProb;
if (s.tripleActive && Time.time >= s.tripleEndTime)
s.tripleEndTime = Time.time + Random.Range(skillCfg.tripleMinTime, skillCfg.tripleMaxTime);
s.doubleActive = (s.doubleActive && Time.time < s.doubleEndTime)
|| Random.value < skillCfg.doubleProb;
if (s.doubleActive && Time.time >= s.doubleEndTime)
s.doubleEndTime = Time.time + Random.Range(skillCfg.doubleMinTime, skillCfg.doubleMaxTime);
}
2026-07-20 01:44:28 +08:00
/// <summary>人机炮台旋转(房主端执行)</summary>
2026-07-05 16:07:22 +08:00
private void RotateBotToTarget(SitPosData sit, Vector3 fishWorldPos, float rotateSpeed = -1f)
2026-07-02 19:09:00 +08:00
{
2026-07-03 18:55:56 +08:00
Vector2 screenPos = Camera.main.WorldToScreenPoint(fishWorldPos);
RectTransformUtility.ScreenPointToLocalPointInRectangle(
sit.tra_CurrTurret.transform.parent as RectTransform,
screenPos, null, out Vector2 localPos);
Vector2 dir = localPos - sit.tra_CurrTurret.GetComponent<RectTransform>().anchoredPosition;
// 鱼太近时 dir 接近零向量,Atan2 不稳定 → 跳过
if (dir.sqrMagnitude < 25f) return;
2026-07-02 19:09:00 +08:00
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg - 90f;
if (sit.IsFlipped) angle = (angle + 180f) % 360f;
2026-07-05 17:39:30 +08:00
// Lerp 旋转:自然减速,速度由 rotateSpeed 控制
2026-07-04 19:56:30 +08:00
float current = sit.currentAngle;
2026-07-05 16:07:22 +08:00
float speed = rotateSpeed > 0f ? rotateSpeed : botRotateSpeed;
2026-07-05 17:39:30 +08:00
// t 越大旋转越快,clamp 防过冲
float t = Mathf.Min(speed * 0.15f * Time.deltaTime, 1f);
float smoothed = Mathf.LerpAngle(current, angle, t);
2026-07-04 19:56:30 +08:00
sit.SetRotation(smoothed);
2026-07-20 01:44:28 +08:00
// ⭐ 机器人旋转帧同步:和开炮一样实时广播
// 只保留极小节流(角度>2° 或 0.05s)避免每帧都广播造成网络洪流
float lastAngle = botLastSyncAngle.TryGetValue(sit.posID, out float la) ? la : -999f;
float lastTime = botLastSyncTime.TryGetValue(sit.posID, out float lt) ? lt : 0f;
if (Mathf.Abs(Mathf.DeltaAngle(lastAngle, smoothed)) > 2f || (Time.time - lastTime) > 0.05f)
{
SyncBotRotate(sit, smoothed);
botLastSyncAngle[sit.posID] = smoothed;
botLastSyncTime[sit.posID] = Time.time;
}
}
/// <summary>机器人旋转广播(房主代发,复用 rotateTurrect 帧类型)</summary>
private void SyncBotRotate(SitPosData botSit, float angle)
{
SyncGameData sgd = new SyncGameData
{
sitPos = botSit.posID,
frameType = FrameDataType.rotateTurrect,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
frameData = new FrameData_Rotate { angle = angle }
};
WebSocketSendList.SyncGameData(sgd);
2026-07-02 19:09:00 +08:00
}
2026-07-22 03:41:06 +08:00
/// <summary>
/// 机器人锁定状态广播(房主代发,复用 changeAutoTarget 帧类型)。
/// 用 bot 自己的 sitPos,接收端 SyncGameData_ChangeTargetMonster 会根据 sitPos
/// 调用 SetLockTarget/ClearLock 更新对应 bot 座位,从而激活 lockFX 特效和锁定字段。
/// 内置节流:只有目标 guid 变化(包括锁定→取消锁定)才真正广播。
/// </summary>
/// <param name="botSit">bot 座位</param>
/// <param name="targetGuid">锁定目标 guid,null 表示取消锁定</param>
private void SyncBotLock(SitPosData botSit, string targetGuid)
{
// ⭐ 节流:目标 guid 未变化不广播(避免每帧都发)
botLastSyncLockGuid.TryGetValue(botSit.posID, out string lastSynced);
if (lastSynced == targetGuid) return;
botLastSyncLockGuid[botSit.posID] = targetGuid;
List<string> guidList = null;
if (!string.IsNullOrEmpty(targetGuid))
guidList = new List<string> { targetGuid };
SyncGameData sgd = new SyncGameData
{
sitPos = botSit.posID,
frameType = FrameDataType.changeAutoTarget,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
frameData = new FrameData_ChangeAutoTarget
{
targetGuid = guidList,
colliderIndex = -1
}
};
WebSocketSendList.SyncGameData(sgd);
}
2026-07-25 21:04:58 +08:00
/// <summary>
/// 机器人炮台状态广播(房主代发,复用 changeTurretState 帧类型)。
/// 用 bot 自己的 sitPos,接收端 SyncGameData_TurretState 会根据 sitPos
/// 调用 sit.ChangeState(state) 切换炮台动画。
/// 内置节流:只有状态变化才广播(避免空闲时每帧广播 Idle)。
/// 走 LV2 队列(0.5s flush 一次),对 bot 攻击动画可接受。
/// </summary>
private void SyncBotTurretState(SitPosData botSit, TurretState state)
{
if (botSit == null) return;
// ⭐ 节流:状态未变化不广播
if (botLastSyncTurretState.TryGetValue(botSit.posID, out var lastSynced) && lastSynced == state)
return;
botLastSyncTurretState[botSit.posID] = state;
SyncGameData sgd = new SyncGameData
{
sitPos = botSit.posID,
frameType = FrameDataType.changeTurretState,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
frameData = new FrameData_TurretState { state = state }
};
WebSocketSendList.SyncGameData(sgd);
}
2026-06-16 19:12:37 +08:00
/// <summary>人机开火同步(房主代发)</summary>
2026-07-05 18:52:09 +08:00
private void SyncFire_ForBot(SitPosData botSit, float angle, long gold,
bool isSanShe, bool isDouble, bool isLock, List<string> monsterIDs)
2026-06-16 19:12:37 +08:00
{
2026-07-05 18:52:09 +08:00
var fireData = new FrameData_FireBullet
{
angle = angle,
gold = gold,
isSanShe = isSanShe,
twoRTruuect = isDouble,
isSuoDing = isLock,
};
if (monsterIDs != null && monsterIDs.Count > 0)
fireData.monsterIDs = monsterIDs;
2026-06-16 19:12:37 +08:00
SyncGameData sgd = new SyncGameData
{
sitPos = botSit.posID,
frameType = FrameDataType.fireBullet,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
2026-07-05 18:52:09 +08:00
frameData = fireData
2026-06-16 19:12:37 +08:00
};
WebSocketSendList.SyncGameData(sgd);
}
#endregion
2026-02-13 19:23:01 +08:00
}