2868 lines
106 KiB
C#
2868 lines
106 KiB
C#
/*
|
||
======================== GameMainPanel_FishBase ========================
|
||
|
||
【主控制层职责】
|
||
负责:输入 → 旋转 → 攻击 → 子弹 → 锁定 → 网络同步 → 广播处理
|
||
不负责:动画(TurretData)、发射点(TurretData)、金币(SitPosData)
|
||
|
||
=======================================================================
|
||
|
||
① InputModule(输入系统)
|
||
├── HandleInput() 主入口
|
||
├── 按下(PressDown)
|
||
├── 长按(PressHold)
|
||
├── 松开(PressUp)
|
||
├── CheckTouchIsChangeTurret() 判断是否点到“换炮台按钮”
|
||
└── HandleChangeTurretHold() 长按换炮台按钮 → 打开换炮台面板
|
||
|
||
-----------------------------------------------------------------------
|
||
|
||
② RotateModule(旋转系统)
|
||
├── HandleRotate() 手动旋转入口
|
||
├── RotateToTouch() 朝触摸点旋转
|
||
├── RotateToLockTarget() 朝锁定目标旋转
|
||
├── HandleAutoRotate() 自动开火时的自动旋转
|
||
└── AutoSweepRotate() 自动扫射(左右摆动)
|
||
|
||
-----------------------------------------------------------------------
|
||
|
||
③ FireModule(攻击系统)
|
||
├── TryFire() 长按触发攻击(冷却计时)
|
||
├── FireBullet() 发射子弹(普通/三射/双倍/锁定)
|
||
├── StartLaser() 激光开始(LaserStart)
|
||
├── LoopLaser() 激光持续(LaserLoop)
|
||
└── StopLaser() 激光结束(LaserEnd)
|
||
|
||
-----------------------------------------------------------------------
|
||
|
||
④ BulletModule(子弹系统)
|
||
├── CreateBullet() 创建子弹实例
|
||
|
||
|
||
-----------------------------------------------------------------------
|
||
|
||
⑤ LockModule(锁定系统)
|
||
├── HandleLock() 点击锁定
|
||
├── 自动锁定(AutoLock) 可选扩展
|
||
├── CancelLock() 取消锁定
|
||
├── UpdateLockFX() 锁定特效跟随目标
|
||
└── SyncLock() 网络同步锁定状态
|
||
|
||
-----------------------------------------------------------------------
|
||
|
||
⑥ NetSyncModule(网络同步)
|
||
├── SyncRotate() 同步旋转
|
||
├── SyncFire() 同步发射
|
||
├── SyncLock() 同步锁定
|
||
├── SyncTurretState() 同步激光状态
|
||
└── SyncChangeTurret() 同步炮台切换(可选)
|
||
|
||
-----------------------------------------------------------------------
|
||
|
||
⑦ NetReceiveModule(服务器广播处理)
|
||
├── SyncGameData_Rotate() 广播旋转
|
||
├── SyncGameData_FireBullet() 广播发射
|
||
├── SyncGameData_ChangeTarget() 广播锁定
|
||
├── SyncGameData_ChangeTurret() 广播炮台切换(可选)
|
||
└── SyncGameData_LaserState() 广播激光状态(可选)
|
||
|
||
-----------------------------------------------------------------------
|
||
|
||
⑧ ResourceModule(资源加载)
|
||
├── LoadResources() 加载音频/鱼资源/路线
|
||
└── UnLoadResources() 卸载资源
|
||
|
||
-----------------------------------------------------------------------
|
||
|
||
⑨ PlayerJoinModule(玩家加入/退出)
|
||
├── JoinGame() 玩家加入房间
|
||
└── ExitGame() 玩家退出房间
|
||
|
||
=======================================================================
|
||
*/
|
||
|
||
using Assets.MyGame.Scripts.Net;
|
||
using Assets.MyGame.Scripts.Net.API;
|
||
using Assets.MyGame.Scripts.Net.API.UserData_SubClass;
|
||
using Assets.MyGame.Scripts.Net.APIResponses.Enum;
|
||
using Assets.MyGame.Scripts.Net.WebResponses;
|
||
using DG.Tweening;
|
||
using Newtonsoft.Json;
|
||
using Spine.Unity;
|
||
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Security.Cryptography;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.AddressableAssets;
|
||
using UnityEngine.EventSystems;
|
||
using UnityEngine.InputSystem;
|
||
using UnityEngine.Playables;
|
||
using UnityEngine.SceneManagement;
|
||
using UnityEngine.UI;
|
||
using Random = UnityEngine.Random;
|
||
|
||
public class GameMainPanel_FishBase : GameMainPanel_Base
|
||
{
|
||
[Header("离开游戏倒计时")]
|
||
public GameObject obj_DJSPanel;
|
||
public TextMeshProUGUI tmp_DJS; //确认(10)
|
||
public Button btn_QueRen;
|
||
public Button btn_QuXiao;
|
||
/// <summary>是否正在退出倒计时(期间屏蔽帧同步和爆奖特效)</summary>
|
||
private bool _isLeavingCountdown = false;
|
||
private Coroutine _leaveCountdownCoroutine;
|
||
[Header("游戏场景")]
|
||
public string sceneName;
|
||
public GameTypeEnum gameType;
|
||
|
||
[Header("设置")]
|
||
protected bool isOpenSetting;
|
||
public Button btn_SettingList;
|
||
public Transform SittingListParent;
|
||
public Button btn_TuJian;
|
||
public Button btn_ChangeTurret;
|
||
|
||
public Button btn_TuiChu;
|
||
public Button btn_Setting;
|
||
|
||
//[Header("我在这")]
|
||
public GameObject mySitAnim;
|
||
public Dictionary<string, Toggle> dic_Skill_Toggle = new Dictionary<string, Toggle>();//开关型技能
|
||
[Header("Skillbottom")]
|
||
public Toggle toggle_SkillAuto;
|
||
public Toggle toggle_Lock;
|
||
public Toggle toggle_Three;
|
||
public Toggle toggle_Shuang;
|
||
public Toggle toggle_BaoZou;
|
||
public TextMeshProUGUI tmp_BaoZouCount;
|
||
public Image img_BaoZou;
|
||
public float frenzyDuration = 5f; // 疯狂状态持续时间
|
||
public float currFrenzyTime = 0f;
|
||
public Toggle toggle_BingFeng;
|
||
public TextMeshProUGUI tmp_BingFengCount;
|
||
public Image img_BingFeng;
|
||
public GameObject bingFengEffect; // 冰封特效预制体(首次Instantiate,之后激活/隐藏)
|
||
private GameObject _bingFengInstance; // 克隆出来的实例
|
||
public float bingFengDuration = 5f; // 冰封状态持续时间
|
||
public float currBingFengTime = 0f;
|
||
private Dictionary<Component, float> _frozenAnimSpeeds = new Dictionary<Component, float>();
|
||
|
||
// ⭐ 暴走/冰封本地数量:因为暂无扣减 API,激活时本地 -1,避免被 OnUserDataChanged 覆盖回服务端旧值
|
||
private int _localBaoZouCount = -1; // -1 表示尚未初始化
|
||
private int _localBingFengCount = -1;
|
||
|
||
[Header("Skillleft")]
|
||
public Button btn_GuaGuaKa;
|
||
public TextMeshProUGUI tmp_GGKCount;
|
||
|
||
public List<SitPosData> list_Sits = new List<SitPosData>();
|
||
public SitPosData currSitData;
|
||
|
||
[Header("攻击参数")]
|
||
public float attackTime = 0.5f;
|
||
[Header("人机参数")]
|
||
public float botRotateSpeed = 80f; // 兜底旋转速度(BotConfig 为空时用)
|
||
float clickTime = 0;
|
||
private Dictionary<int, float> botFireTimer = new Dictionary<int, float>();
|
||
private Dictionary<string, float> botLastAngle = new Dictionary<string, float>();
|
||
// bot 旋转广播节流:记录上次广播的角度/时间
|
||
private Dictionary<int, float> botLastSyncAngle = new Dictionary<int, float>();
|
||
private Dictionary<int, float> botLastSyncTime = new Dictionary<int, float>();
|
||
// bot 技能运行时
|
||
private Dictionary<int, BotSkillRuntime> botSkills = new Dictionary<int, BotSkillRuntime>();
|
||
// bot 目标记忆
|
||
private Dictionary<int, string> botTargetGuid = new Dictionary<int, string>();
|
||
// bot 状态计时:开火后持续 Attack 动画,空闲 0.3s 切回 Idle
|
||
private Dictionary<int, float> botIdleTimer = new Dictionary<int, float>();
|
||
// bot 最近的 FrameData_FireBullet(给其他客户端还原技能状态)
|
||
private Dictionary<string, FrameData_FireBullet> botLastFireData = new Dictionary<string, FrameData_FireBullet>();
|
||
// bot 运行时状态(进场金币/倍数档位/行为模式/退出)
|
||
private Dictionary<int, BotRuntimeState> botRuntimeStates = new Dictionary<int, BotRuntimeState>();
|
||
public GameObject bullet_Test;
|
||
public GameObject bulletParent;
|
||
float changeTurretTime = 0f;
|
||
bool isChangingTurret = false;
|
||
|
||
int bulletCount = 0;
|
||
public GameObject boardCast_Boss;
|
||
|
||
AudioClip bulletClip;
|
||
|
||
public List<int> paoBeiList = new List<int>();
|
||
|
||
public ProgressBar JinDuTiao;
|
||
#region UI
|
||
bool isfirstShow = true;
|
||
protected override void OnShow(object args = null)
|
||
{
|
||
base.OnShow(args);
|
||
LoadResources();
|
||
RefreshGGKCount();
|
||
//UIManager.Instance.ShowBoard("太好看了");
|
||
var v = JsonConvert.DeserializeObject<WebSocketResponseBase<RoomJoinNotice>>(args.ToString());
|
||
if (v.arguments.Count > 0)
|
||
{
|
||
|
||
RoomJoinNotice roomJoinNotice = v.arguments[0];
|
||
// ⭐ paoBeiList 每次进入房间都重新根据当前场次+房间等级获取,
|
||
// 避免再次进入同一场次的不同等级房间时沿用旧 paoBeiList(例如先高级场后低级场)
|
||
{
|
||
int t = GameManager.Instance.gameData.currRoomData.gameType;
|
||
int l = GameManager.Instance.gameData.currRoomData.roomLevel;
|
||
foreach (var item in GameManager.Instance.gameConfig.roomConfigs)
|
||
{
|
||
if (item.gameType == t && l == item.roomLevel)
|
||
{
|
||
paoBeiList = item.cannonMultipleList;
|
||
}
|
||
}
|
||
}
|
||
if (isfirstShow)
|
||
{
|
||
string roomId = GameManager.Instance.gameData.currRoomData.roomId;
|
||
//string roomId = GameManager.Instance.gameData.currRoomData;
|
||
|
||
|
||
foreach (var item in GameManager.Instance.gameData.currRoomData.players)
|
||
{
|
||
JoinGame(item.position, item.gold, item.diamond, item.cannonMultiple, 0, item.userId, item.cannonId, item.wingsId);
|
||
// ⭐ 优先用后端 isBot 字段,兼容 userId.StartsWith("bot_")
|
||
var sit = list_Sits.Find(s => s.posID == item.position);
|
||
if (sit != null)
|
||
{
|
||
bool isBot = item.isBot ||
|
||
(!string.IsNullOrEmpty(item.userId) && item.userId.StartsWith("bot_"));
|
||
if (isBot) sit.isBot = true;
|
||
}
|
||
}
|
||
GameManager.Instance.gameData.roomJoinNotice = v.arguments[0];
|
||
isfirstShow = false;
|
||
|
||
foreach (var item in list_Sits)
|
||
{
|
||
if (item.posID == roomJoinNotice.position)
|
||
{
|
||
currSitData = item;
|
||
|
||
}
|
||
else
|
||
{
|
||
item.btn_TurretAdd.gameObject.SetActive(false);
|
||
item.btn_TurretMinus.gameObject.SetActive(false);
|
||
}
|
||
|
||
}
|
||
//// currSitData.ChangeTurret(1); — 已由 SitPosData.OnEnable 自动加载
|
||
if (currSitData.currentTurret != null)
|
||
SyncChangeTurret(currSitData.currentTurret.turretNumber);
|
||
currSitData.lockFX.transform.GetChild(0).gameObject.SetActive(false);
|
||
currSitData.lockFX.transform.GetChild(1).gameObject.SetActive(true);
|
||
if (mySitAnim != null && currSitData.turretParent != null)
|
||
mySitAnim.transform.position = currSitData.turretParent.position;
|
||
}
|
||
else
|
||
{
|
||
var targetSit = list_Sits.Find(s => s.posID == roomJoinNotice.position);
|
||
// ⭐ 优先用后端 isBot 字段判断,兼容 userId.StartsWith("bot_")
|
||
bool isBotJoin = roomJoinNotice.isBot ||
|
||
(!string.IsNullOrEmpty(roomJoinNotice.userId) && roomJoinNotice.userId.StartsWith("bot_"));
|
||
|
||
if (isBotJoin)
|
||
{
|
||
// bot 加入 → 标记 isBot=true (JoinGame 里会自动 SetActive(true) 座位)
|
||
if (targetSit != null)
|
||
targetSit.isBot = true;
|
||
}
|
||
else
|
||
{
|
||
// 真人玩家加入 → 如果座位被bot占用则替换
|
||
if (targetSit != null && targetSit.isBot)
|
||
{
|
||
targetSit.isBot = false;
|
||
targetSit.ShowWaiting();
|
||
}
|
||
// ⭐ 新玩家同步走"主动请求"流程:
|
||
// 新玩家资源加载完毕后会主动发 requestSyncFish,
|
||
// 房主收到后才广播场上活鱼(避免新玩家还没加载完就收包丢失)
|
||
}
|
||
JoinGame(roomJoinNotice.position, roomJoinNotice.gold, roomJoinNotice.diamond, roomJoinNotice.cannonMultiple, 0, roomJoinNotice.userId, roomJoinNotice.cannonId, roomJoinNotice.wingsId);
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
}
|
||
else
|
||
{
|
||
UIManager.Instance.ShowBoardError("获取信息失败");
|
||
}
|
||
|
||
}
|
||
private void RefreshGGKCount()
|
||
{
|
||
var user = GameManager.Instance.user;
|
||
var props = user?.props;
|
||
int ggkCount = 0;
|
||
|
||
if (props != null)
|
||
{
|
||
foreach (var p in props)
|
||
{
|
||
if (p.number == "40005" && p.count > 0)
|
||
ggkCount = p.count;
|
||
}
|
||
}
|
||
|
||
if (tmp_GGKCount != null)
|
||
tmp_GGKCount.text = ggkCount.ToString();
|
||
|
||
// 同步刷新暴走/冰封本地计数(仅首次或服务端数据变化时同步)
|
||
RefreshSkillPropsCount(props);
|
||
|
||
// 同步更新游戏内金币显示(刮刮卡领奖、邮件领取等都会触发)
|
||
if (currSitData != null && user?.assets != null)
|
||
{
|
||
currSitData.goldNum = user.assets.gold;
|
||
if (currSitData.tMP_GoldNum != null)
|
||
currSitData.tMP_GoldNum.text = user.assets.gold.ToString();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新暴走/冰封道具数量显示
|
||
/// 策略:本地计数未初始化(_localXxxCount == -1)时从服务端 props 读取;
|
||
/// 之后由激活扣减本地维护,避免 OnUserDataChanged 把本地扣减值覆盖回服务端旧值
|
||
/// </summary>
|
||
private void RefreshSkillPropsCount(List<BaseProductCount> props)
|
||
{
|
||
int serverBaoZou = 0;
|
||
int serverBingFeng = 0;
|
||
if (props != null)
|
||
{
|
||
foreach (var p in props)
|
||
{
|
||
if (p.number == "40006" && p.count > 0) serverBaoZou = p.count;
|
||
else if (p.number == "40007" && p.count > 0) serverBingFeng = p.count;
|
||
}
|
||
}
|
||
|
||
// 首次初始化,或服务端数量比本地少(说明服务端已扣减,比如其他端使用)→ 以服务端为准
|
||
if (_localBaoZouCount < 0 || serverBaoZou < _localBaoZouCount)
|
||
_localBaoZouCount = serverBaoZou;
|
||
if (_localBingFengCount < 0 || serverBingFeng < _localBingFengCount)
|
||
_localBingFengCount = serverBingFeng;
|
||
|
||
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();
|
||
}
|
||
protected override void OnHide()
|
||
{
|
||
base.OnHide();
|
||
|
||
UnLoadResources();
|
||
// 场景可能未加载或已卸载,做保护避免重复卸载报错
|
||
var scene = SceneManager.GetSceneByName(sceneName);
|
||
if (scene.IsValid() && scene.isLoaded)
|
||
SceneManager.UnloadSceneAsync(sceneName);
|
||
// 清除场景加载标记,下次进入可重新加载
|
||
Assets.MyGame.Scripts.Net.WebSocketReponseList.ClearLoadedGameScene(sceneName);
|
||
WebSocketManager.Instance.Close(SocketType.game);
|
||
// ⭐ 重置本地道具计数,下次进入房间重新从服务端读取
|
||
_localBaoZouCount = -1;
|
||
_localBingFengCount = -1;
|
||
|
||
}
|
||
#endregion
|
||
|
||
#region 生命周期
|
||
private float baseAttackTime;
|
||
|
||
public override void Awake()
|
||
{
|
||
base.Awake();
|
||
GameManager.Instance.OnUserDataChanged += RefreshGGKCount;
|
||
baseAttackTime = attackTime;
|
||
btn_ChangeTurret.onClick.AddListener(() =>
|
||
{
|
||
var panel = subUIManager.Get<SubUIBase>(UIPanel.TurretPanel);
|
||
if (panel != null && panel.gameObject.activeSelf)
|
||
subUIManager.Hide(UIPanel.TurretPanel);
|
||
else
|
||
{
|
||
subUIManager.ShowAsync(UIPanel.TurretPanel, (ItemPanelSelect.Turrect, true));
|
||
StartCoroutine(CloseSettingList());
|
||
}
|
||
});
|
||
btn_Setting.onClick.AddListener(() => {subUIManager.ShowAsync(UIPanel.SettingPanel); });
|
||
btn_TuiChu.onClick.AddListener(() =>
|
||
{
|
||
StartLeaveCountdown();
|
||
});
|
||
|
||
// 服务器广播监听
|
||
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_Rotate, SyncGameData_Rotate);
|
||
EventCenter.Instance.AddEventListener<FireCannonSendData>(ConstEventString.SyncGameData_FireBullet, SyncGameData_FireBullit);
|
||
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);
|
||
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeCannonMultiple, SyncGameData_ChangeCannonMultiple);
|
||
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeWing, SyncGameData_ChangeWing);
|
||
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_FreezeFish, SyncGameData_FreezeFish);
|
||
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_BotFireAngle, SyncGameData_BotFireAngle);
|
||
btn_TuJian.onClick.AddListener(() =>
|
||
{
|
||
subUIManager.ShowAsync(UIPanel.GameTuJian);
|
||
StartCoroutine(CloseSettingList());
|
||
});
|
||
btn_GuaGuaKa.onClick.AddListener(() =>
|
||
{
|
||
subUIManager.ShowAsync(UIPanel.GuaGuaKa);
|
||
});
|
||
|
||
}
|
||
/// <summary>开始离开游戏倒计时:10秒倒计时期间屏蔽帧同步和爆奖特效,结束后点确认才退出</summary>
|
||
public void StartLeaveCountdown()
|
||
{
|
||
if (_isLeavingCountdown) return;
|
||
_isLeavingCountdown = true;
|
||
if (obj_DJSPanel != null) obj_DJSPanel.SetActive(true);
|
||
// 倒计时期间按钮禁用
|
||
if (btn_QueRen != null) btn_QueRen.interactable = false;
|
||
// 绑定按钮事件
|
||
if (btn_QueRen != null)
|
||
{
|
||
btn_QueRen.onClick.RemoveAllListeners();
|
||
btn_QueRen.onClick.AddListener(() =>
|
||
{
|
||
// 确认:直接退出房间
|
||
WebSocketSendList.LeaveRoom(GameManager.Instance.gameData.currRoomData.roomId, currSitData.user);
|
||
CreateFishManager.Instance.fangzhu = false;
|
||
CancelLeaveCountdown();
|
||
});
|
||
}
|
||
if (btn_QuXiao != null)
|
||
{
|
||
btn_QuXiao.onClick.RemoveAllListeners();
|
||
btn_QuXiao.onClick.AddListener(CancelLeaveCountdown);
|
||
}
|
||
if (_leaveCountdownCoroutine != null) StopCoroutine(_leaveCountdownCoroutine);
|
||
_leaveCountdownCoroutine = StartCoroutine(LeaveCountdownRoutine(10));
|
||
}
|
||
|
||
/// <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;
|
||
}
|
||
|
||
public virtual void OnDestroy()
|
||
{
|
||
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_Rotate, SyncGameData_Rotate);
|
||
EventCenter.Instance.RemoveEventListener<FireCannonSendData>(ConstEventString.SyncGameData_FireBullet, SyncGameData_FireBullit);
|
||
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);
|
||
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeCannonMultiple, SyncGameData_ChangeCannonMultiple);
|
||
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeWing, SyncGameData_ChangeWing);
|
||
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_FreezeFish, SyncGameData_FreezeFish);
|
||
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_BotFireAngle, SyncGameData_BotFireAngle);
|
||
|
||
// ⭐ 离场清理:子弹池 + 特效池彻底清空,防止内存泄漏
|
||
BulletPool.Instance.ClearAll();
|
||
EffectPool.Instance.ClearAll();
|
||
|
||
}
|
||
public virtual void Start()
|
||
{
|
||
btn_SettingList.onClick.AddListener(() =>
|
||
{
|
||
if (isOpenSetting) StartCoroutine(CloseSettingList());
|
||
else StartCoroutine(OpenSettingList());
|
||
});
|
||
|
||
if (toggle_Lock != null)
|
||
{
|
||
toggle_Lock.onValueChanged.AddListener(on =>
|
||
{
|
||
if (!on)
|
||
{
|
||
// 本地清理锁定
|
||
if (currSitData != null)
|
||
{
|
||
currSitData.ClearLock();
|
||
SyncTurretState(TurretState.Idle);
|
||
}
|
||
|
||
// 同步给服务器:取消锁定
|
||
SyncGameData sgd = new SyncGameData
|
||
{
|
||
sitPos = currSitData.posID,
|
||
frameType = FrameDataType.changeAutoTarget,
|
||
time = BeijingTimeManager.Instance.CurrentTimestampMs,
|
||
frameData = new FrameData_ChangeAutoTarget { targetGuid = null }
|
||
};
|
||
WebSocketSendList.SyncGameData(sgd);
|
||
}
|
||
});
|
||
}
|
||
toggle_BaoZou.onValueChanged.AddListener(on =>
|
||
{
|
||
if (!on) return;
|
||
if (_localBaoZouCount <= 0)
|
||
{
|
||
toggle_BaoZou.SetIsOnWithoutNotify(false);
|
||
return;
|
||
}
|
||
// 只发请求,效果等 UsePropNotice 回调触发
|
||
WebSocketSendList.UseProp(
|
||
GameManager.Instance.gameData.currRoomData.roomId,
|
||
GameManager.Instance.user.BaseInfo.number,
|
||
"40006", 0);
|
||
});
|
||
toggle_SkillAuto.onValueChanged.AddListener(on =>
|
||
{
|
||
if (!on)
|
||
{
|
||
// 自动开火关闭 → 立即切回 Idle
|
||
currSitData.ChangeState(TurretState.Idle);
|
||
SyncTurretState(TurretState.Idle);
|
||
}
|
||
});
|
||
toggle_BingFeng.onValueChanged.AddListener(on => {
|
||
if (!on) return;
|
||
if (_localBingFengCount <= 0)
|
||
{
|
||
toggle_BingFeng.SetIsOnWithoutNotify(false);
|
||
return;
|
||
}
|
||
// 只发请求,效果等 UsePropNotice 回调触发
|
||
WebSocketSendList.UseProp(
|
||
GameManager.Instance.gameData.currRoomData.roomId,
|
||
GameManager.Instance.user.BaseInfo.number,
|
||
"40007", 0);
|
||
});
|
||
|
||
|
||
|
||
}
|
||
public bool isFrenzy;
|
||
public void SetFrenzy(bool on)
|
||
{
|
||
bool frenzy = CreateFishManager.Instance.currGameType.isFrenzy;
|
||
|
||
// 攻击速度翻倍
|
||
attackTime = frenzy ? baseAttackTime * 0.5f : baseAttackTime;
|
||
|
||
// 动画倍速
|
||
if (currSitData != null && currSitData.currentTurret != null)
|
||
currSitData.currentTurret.SetFrenzy(frenzy);
|
||
}
|
||
|
||
|
||
|
||
public void OnEnable()
|
||
{
|
||
|
||
}
|
||
int ina = 0;
|
||
|
||
protected virtual void Update()
|
||
{
|
||
|
||
|
||
|
||
HandleAutoRotate();
|
||
HandleInput();
|
||
CheckLockTargetValid();//锁定目标失效自动清除
|
||
|
||
|
||
HandleAutoFire(); // 自动开火真正发射
|
||
HandleLock();
|
||
UpdateLockFX();
|
||
RefreshTripleLockIcons();
|
||
|
||
HandleAutoLock();
|
||
LoopLaser();
|
||
SettingClose();
|
||
DaoJiShi();
|
||
HandleBotAI();
|
||
WebSocketSendList.FlushSyncQueues(Time.deltaTime);
|
||
|
||
}
|
||
float sittingTime = 10;
|
||
float currSittingTime;
|
||
|
||
/// <summary>
|
||
/// 等待10秒未操作关闭设置面板
|
||
/// </summary>
|
||
public void SettingClose()
|
||
{
|
||
//if (isOpenSetting) StartCoroutine(CloseSettingList());
|
||
//else StartCoroutine(OpenSettingList());
|
||
if (isOpenSetting)
|
||
{
|
||
currSittingTime -= Time.deltaTime;
|
||
if (currSittingTime <= 0)
|
||
{
|
||
StartCoroutine(CloseSettingList());
|
||
}
|
||
}
|
||
else
|
||
{
|
||
currSittingTime = 10;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
#endregion
|
||
|
||
#region 资源加载模块
|
||
public async void LoadResources()
|
||
{
|
||
CreateFishManager.Instance.isResources = false;
|
||
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);
|
||
await Task.Delay(100);
|
||
|
||
|
||
CreateFishManager.Instance.EnterRoom(gameType);
|
||
CreateFishManager.Instance.isResources = true;
|
||
JinDuTiao.SetProgress(0.9f);
|
||
|
||
await Task.Delay(2000);
|
||
|
||
// ⭐ 进度条加载完毕:房主创建一批初始鱼(进度 0.2~0.8),屏幕开局就有鱼
|
||
// 只在房主端调用,通过 SyncGameData 广播给所有客户端
|
||
// 非房主(后加入的真人玩家):发"请求同步鱼"让房主广播场上活鱼
|
||
// 这里只发请求,实际接收广播在 CreateFishManager.CreateFish(已做 guid 去重)
|
||
if (CreateFishManager.Instance.fangzhu)
|
||
{
|
||
CreateFishManager.Instance.CreateInitialFishes(Random.Range(10, 21));
|
||
}
|
||
else
|
||
{
|
||
var req = new SyncGameData
|
||
{
|
||
sitPos = GameManager.Instance.gameData.roomJoinNotice.position,
|
||
frameType = FrameDataType.requestSyncFish,
|
||
time = BeijingTimeManager.Instance.CurrentTimestampMs,
|
||
frameData = null
|
||
};
|
||
WebSocketSendList.SyncGameData(req);
|
||
}
|
||
|
||
JinDuTiao.SetProgress(1);
|
||
await Task.Delay(300);
|
||
|
||
JinDuTiao.Hide();
|
||
}
|
||
|
||
public async void UnLoadResources()
|
||
{
|
||
CreateFishManager.Instance.ExitRoom();
|
||
CreateFishManager.Instance.isResources = false;
|
||
await FishResourcesData.UnLoadFishAssetsData();
|
||
await AudioResourcesData.UnLoadAudioGame();
|
||
await MonsterMoveLine.UnLoadBezier();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 输入系统(InputModule)
|
||
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;
|
||
}
|
||
|
||
|
||
private void HandleInput()
|
||
{
|
||
// ⭐ 退出倒计时期间屏蔽本地玩家手动输入(旋转/开火/锁定),其他玩家和机器人不受影响
|
||
if (_isLeavingCountdown) return;
|
||
|
||
CrossPlatformInput input = GetInput();
|
||
|
||
if (!input.isPressed && !input.wasPressedThisFrame && !input.wasReleasedThisFrame)
|
||
return;
|
||
if (input.wasPressedThisFrame)
|
||
{
|
||
// ⭐ UI 点击过滤
|
||
if (IsPointerOverUI(input.position))
|
||
return;
|
||
}
|
||
|
||
|
||
// 按下
|
||
if (input.wasPressedThisFrame)
|
||
{
|
||
isChangingTurret = CheckTouchIsChangeTurret(input.position);
|
||
clickTime = attackTime;
|
||
|
||
if (mySitAnim != null && mySitAnim.gameObject.activeSelf)
|
||
mySitAnim.gameObject.SetActive(false);
|
||
|
||
if (!isChangingTurret)
|
||
{
|
||
currSitData.ChangeState(TurretState.Attack);
|
||
SyncTurretState(TurretState.Attack);
|
||
}
|
||
}
|
||
|
||
// 长按
|
||
if (input.isPressed)
|
||
{
|
||
if (isChangingTurret)
|
||
{
|
||
HandleChangeTurretHold();
|
||
return;
|
||
}
|
||
|
||
// 锁定模式:不手动旋转/开火,交给 HandleAutoRotate + HandleAutoFire
|
||
if (toggle_Lock.isOn) return;
|
||
|
||
// UI 上不旋转不发射
|
||
if (IsPointerOverUI(input.position)) return;
|
||
|
||
HandleRotate(input.position);
|
||
TryFire();
|
||
}
|
||
|
||
// 松开
|
||
if (input.wasReleasedThisFrame)
|
||
{
|
||
if (isChangingTurret)
|
||
{
|
||
isChangingTurret = false;
|
||
changeTurretTime = 0;
|
||
CloseChangeTurretUI();
|
||
return;
|
||
}
|
||
|
||
currSitData.ChangeState(TurretState.Idle);
|
||
SyncTurretState(TurretState.Idle);
|
||
}
|
||
}
|
||
|
||
|
||
private void HandleChangeTurretHold()
|
||
{
|
||
changeTurretTime += Time.deltaTime;
|
||
|
||
if (changeTurretTime > 1f)
|
||
{
|
||
// 长按换炮台:已改为按钮打开 TurretPanel
|
||
}
|
||
}
|
||
private bool CheckTouchIsChangeTurret(Vector2 pos)
|
||
{
|
||
PointerEventData pointerData = new PointerEventData(EventSystem.current);
|
||
pointerData.position = pos;
|
||
|
||
List<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;
|
||
}
|
||
|
||
|
||
|
||
private void CloseChangeTurretUI()
|
||
{
|
||
// currSitData.changeTurretList.SetActive(false); — 已废弃
|
||
changeTurretTime = 0;
|
||
isChangingTurret = false;
|
||
}
|
||
|
||
|
||
|
||
|
||
#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)
|
||
|
||
|
||
//private void HandleRotate(Vector2 pos)
|
||
//{
|
||
// if (toggle_Lock.isOn && currSitData.monCotl_Lock != null)
|
||
// {
|
||
// RotateToLockTarget(currSitData);
|
||
// }
|
||
// else
|
||
// {
|
||
// RotateToTouch(currSitData, pos);
|
||
// }
|
||
//}
|
||
private void HandleRotate(Vector2 pos)
|
||
{
|
||
// ⭐ 只有真正锁定目标时才自动旋转
|
||
if (toggle_Lock.isOn && currSitData.isLock && currSitData.monCotl_Lock != null)
|
||
{
|
||
RotateToLockTarget(currSitData);
|
||
}
|
||
else
|
||
{
|
||
// 锁定开关开着但没有锁定目标 → 允许手动旋转
|
||
RotateToTouch(currSitData, pos);
|
||
}
|
||
}
|
||
|
||
|
||
private void RotateToTouch(SitPosData sit, Vector2 pos)
|
||
{
|
||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||
sit.tra_CurrTurret.transform.parent as RectTransform,
|
||
pos, null, out Vector2 localPos);
|
||
|
||
Vector2 dir = localPos - sit.tra_CurrTurret.GetComponent<RectTransform>().anchoredPosition;
|
||
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg - 90f;
|
||
|
||
// 上方座位(2,3)炮台预制体默认朝上,加180°让它朝下打鱼
|
||
if (sit.IsFlipped) angle = (angle + 180f) % 360f;
|
||
|
||
sit.SetRotation(angle);
|
||
SyncRotate(angle);
|
||
}
|
||
|
||
|
||
private void RotateToLockTarget(SitPosData sit)
|
||
{
|
||
Vector2 screenPos = Camera.main.WorldToScreenPoint(sit.monCotl_Lock_Position.position);
|
||
|
||
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 会不稳定导致原地转圈 → 跳过
|
||
const float minDistance = 5f;
|
||
if (dir.sqrMagnitude < minDistance * minDistance) return;
|
||
|
||
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg - 90f;
|
||
|
||
if (sit.IsFlipped) angle = (angle + 180f) % 360f;
|
||
|
||
sit.SetRotation(angle);
|
||
SyncRotate(angle);
|
||
}
|
||
|
||
private void HandleAutoRotate()
|
||
{
|
||
// ⭐ 退出倒计时期间屏蔽本地玩家自动旋转,其他玩家和机器人不受影响
|
||
if (_isLeavingCountdown) return;
|
||
|
||
|
||
// 锁定模式优先
|
||
if (toggle_Lock.isOn && currSitData.isLock)
|
||
{
|
||
RotateToLockTarget(currSitData);
|
||
return;
|
||
}
|
||
// 自动开火关闭 → 不自动旋转
|
||
if (!toggle_SkillAuto.isOn) return;
|
||
// 自动扫射(左右摆动)
|
||
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;
|
||
}
|
||
|
||
// 上方座位加180°,炮台朝下打鱼
|
||
float angle = currSitData.IsFlipped ? (autoRotateAngle + 180f) % 360f : autoRotateAngle;
|
||
currSitData.SetRotation(angle);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 远程玩家/bot 旋转:直接把目标角度赋给炮台,不做任何过渡。
|
||
/// 旋转和开炮都改成实时同步,收到广播立即应用。
|
||
/// </summary>
|
||
private void SmoothRotate(SitPosData sit, float angle)
|
||
{
|
||
if (sit == null || sit.tra_CurrTurret == null) return;
|
||
sit.SetRotation(angle);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Skill系统(SkillModule)
|
||
public void DaoJiShi()
|
||
{
|
||
// 暴走倒计时
|
||
if (toggle_BaoZou != null && toggle_BaoZou.isOn && frenzyDuration > 0)
|
||
{
|
||
if (img_BaoZou != null)
|
||
img_BaoZou.fillAmount = Mathf.Clamp01(currFrenzyTime / frenzyDuration);
|
||
|
||
currFrenzyTime -= Time.deltaTime;
|
||
|
||
if (currFrenzyTime <= 0)
|
||
{
|
||
toggle_BaoZou.isOn = false;
|
||
currFrenzyTime = 0f;
|
||
if (img_BaoZou != null) img_BaoZou.fillAmount = 1f;
|
||
}
|
||
}
|
||
|
||
// 冰封倒计时
|
||
if (toggle_BingFeng != null && toggle_BingFeng.isOn && bingFengDuration > 0)
|
||
{
|
||
if (img_BingFeng != null)
|
||
img_BingFeng.fillAmount = Mathf.Clamp01(currBingFengTime / bingFengDuration);
|
||
|
||
currBingFengTime -= Time.deltaTime;
|
||
|
||
if (currBingFengTime <= 0)
|
||
{
|
||
toggle_BingFeng.isOn = false;
|
||
currBingFengTime = 0f;
|
||
if (img_BingFeng != null) img_BingFeng.fillAmount = 1f;
|
||
// 解冻由 toggle.onValueChanged(false) 触发,不在此重复
|
||
}
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region ③ 攻击系统(FireModule)
|
||
private void TryFire()
|
||
{
|
||
clickTime += Time.deltaTime;
|
||
if (clickTime < attackTime) return;
|
||
|
||
// ⭐ 子弹上限校验(先于扣费和状态切换)
|
||
if (IsBulletLimitReached(currSitData.user))
|
||
{
|
||
clickTime = 0; // 重置计时,下次再试
|
||
return;
|
||
}
|
||
|
||
clickTime = 0;
|
||
|
||
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);
|
||
}
|
||
private void FireBullet(SitPosData sit, float angle, bool isSanShe, bool isDouble, bool isLock, List<string> bullitIDs,
|
||
List<string> fireTargetMonsterIDs = null)
|
||
{
|
||
Vector2 startPos = sit.GetBulletStartPos();
|
||
|
||
// ⭐ 三发 + 锁定 + 有多目标:每发子弹瞄准不同的目标
|
||
if (isSanShe && isLock && fireTargetMonsterIDs != null && fireTargetMonsterIDs.Count > 0)
|
||
{
|
||
for (int i = 0; i < 3 && i < fireTargetMonsterIDs.Count; i++)
|
||
{
|
||
// 按 GUID 查找目标
|
||
MonsterContral targetMonster = null;
|
||
Transform targetHitPos = null;
|
||
if (CreateFishManager.Instance.fishDatas_hashCodekey.TryGetValue(fireTargetMonsterIDs[i], out var m)
|
||
&& m != null && m.gameObject.activeInHierarchy)
|
||
{
|
||
targetMonster = m;
|
||
targetHitPos = m.transform;
|
||
// 优先用活跃 collider
|
||
if (m.colliderList != null && m.colliderList.Count > 0)
|
||
{
|
||
foreach (var col in m.colliderList)
|
||
{
|
||
if (col.gameObject.activeInHierarchy) { targetHitPos = col; break; }
|
||
}
|
||
}
|
||
}
|
||
|
||
if (targetMonster != null)
|
||
{
|
||
// 计算朝目标的角度
|
||
Vector3 targetWorldPos = targetHitPos.position;
|
||
Vector2 dir = ((Vector2)targetWorldPos - startPos).normalized;
|
||
float targetAngle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg - 90f;
|
||
|
||
string bulletId = i < bullitIDs.Count ? bullitIDs[i] : bullitIDs[0];
|
||
CreateBullet(sit, startPos, targetAngle, isDouble, true, bulletId, targetMonster, targetHitPos);
|
||
}
|
||
}
|
||
}
|
||
else if (isSanShe)
|
||
{
|
||
CreateBullet(sit, startPos, angle - 20, isDouble, isLock, bullitIDs[0]);
|
||
CreateBullet(sit, startPos, angle, isDouble, isLock, bullitIDs[1]);
|
||
CreateBullet(sit, startPos, angle + 20, isDouble, isLock, bullitIDs[2]);
|
||
}
|
||
else
|
||
{
|
||
CreateBullet(sit, startPos, angle, isDouble, isLock, bullitIDs[0]);
|
||
}
|
||
|
||
AudioManager.Instance.PlayFX(bulletClip);
|
||
}
|
||
|
||
private bool isLaser = false;
|
||
|
||
private void StartLaser()
|
||
{
|
||
isLaser = true;
|
||
currSitData.ChangeState(TurretState.LaserStart);
|
||
|
||
SyncTurretState(TurretState.LaserStart);
|
||
}
|
||
private void LoopLaser()
|
||
{
|
||
if (!isLaser) return;
|
||
|
||
// TODO: 激光持续伤害 / 特效
|
||
}
|
||
|
||
|
||
private void StopLaser()
|
||
{
|
||
isLaser = false;
|
||
currSitData.ChangeState(TurretState.LaserEnd);
|
||
|
||
SyncTurretState(TurretState.LaserEnd);
|
||
}
|
||
private float autoFireTimer = 0f;
|
||
|
||
//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);
|
||
//}
|
||
private void HandleAutoFire()
|
||
{
|
||
if (currSitData == null) return;
|
||
// ⭐ 退出倒计时期间屏蔽本地玩家自动开火,其他玩家和机器人不受影响
|
||
if (_isLeavingCountdown) return;
|
||
|
||
bool needAutoFire = toggle_SkillAuto.isOn || (toggle_Lock.isOn && currSitData.isLock && currSitData.lockFX.gameObject.activeSelf);
|
||
if (!needAutoFire)
|
||
{
|
||
autoFireTimer = 0f;
|
||
return;
|
||
}
|
||
|
||
autoFireTimer += Time.deltaTime;
|
||
if (autoFireTimer < attackTime) return;
|
||
|
||
// ⭐ 子弹上限校验(先于状态切换和扣费)
|
||
if (IsBulletLimitReached(currSitData.user))
|
||
{
|
||
autoFireTimer = 0f; // 重置计时,下次再试
|
||
return;
|
||
}
|
||
|
||
autoFireTimer = 0f;
|
||
|
||
float angle = currSitData.currentAngle;
|
||
bool isSanShe = toggle_Three.isOn;
|
||
bool isDouble = toggle_Shuang.isOn;
|
||
bool isLock = toggle_Lock.isOn;
|
||
|
||
// ⭐ 自动攻击也要播放攻击动画
|
||
currSitData.ChangeState(TurretState.Attack);
|
||
SyncTurretState(TurretState.Attack);
|
||
|
||
//FireBullet(currSitData, angle, isSanShe, isDouble, isLock);
|
||
SyncFire(angle, isSanShe, isDouble, isLock);
|
||
}
|
||
|
||
|
||
|
||
|
||
#endregion
|
||
|
||
#region 锁定位置解析(统一工具)
|
||
|
||
/// <summary>
|
||
/// 统一锁定位置解析:colliderList 第一个活跃碰撞器 → lockPoint → monster.transform
|
||
/// 所有设置锁定位置的地方都应该调用此方法,避免不同地方用不同优先级导致锁定图标位置和子弹终点不一致。
|
||
/// 注意:Unity 序列化字段未赋值时是"假 null",?? 运算符无法识别,必须用 == null 判断。
|
||
/// </summary>
|
||
private Transform ResolveLockTransform(MonsterContral monster, out int colliderIndex)
|
||
{
|
||
colliderIndex = -1;
|
||
if (monster == null) return null;
|
||
|
||
// 1. 优先用 colliderList 中第一个活跃的碰撞器
|
||
if (monster.colliderList != null && monster.colliderList.Count > 0)
|
||
{
|
||
for (int i = 0; i < monster.colliderList.Count; i++)
|
||
{
|
||
var col = monster.colliderList[i];
|
||
if (col != null && col.gameObject.activeInHierarchy)
|
||
{
|
||
colliderIndex = i;
|
||
return col;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. 其次用 lockPoint(锁定标志锚点,通常是屏幕中间/鱼身中心)
|
||
if (monster.lockPoint != null)
|
||
return monster.lockPoint;
|
||
|
||
// 3. 兜底用 monster.transform
|
||
return monster.transform;
|
||
}
|
||
|
||
/// <summary>简化重载,不关心 colliderIndex 时用</summary>
|
||
private Transform ResolveLockTransform(MonsterContral monster)
|
||
{
|
||
return ResolveLockTransform(monster, out _);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 路径进度过滤(锁定范围控制)
|
||
|
||
/// <summary>bot/玩家锁定时,鱼路径进度下限:小于此值表示鱼刚出生,不锁定</summary>
|
||
private const float BotLockPathMin = 0.2f;
|
||
/// <summary>bot/玩家锁定时,鱼路径进度上限:大于此值表示鱼即将离场,不锁定</summary>
|
||
private const float BotLockPathMax = 0.8f;
|
||
|
||
/// <summary>
|
||
/// 计算鱼在路径上的进度 [0, 1]。
|
||
/// 进度 = moveTime / monsterData.moveLine.moveTime
|
||
/// 0 = 刚出生,1 = 即将离场
|
||
/// </summary>
|
||
private float GetFishPathProgress(MonsterContral m)
|
||
{
|
||
if (m == null || m.monsterData == null || m.monsterData.moveLine == null) return 1f;
|
||
float total = m.monsterData.moveLine.moveTime;
|
||
if (total <= 0f) return 1f;
|
||
return Mathf.Clamp01(m.moveTime / total);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断鱼的路径进度是否在可锁定范围内 [BotLockPathMin, BotLockPathMax]
|
||
/// 刚出生(< 0.2)或即将离场(> 0.8)的鱼不能锁定,避免锁定后立即丢失目标
|
||
/// </summary>
|
||
private bool IsFishPathLockable(MonsterContral m)
|
||
{
|
||
if (m == null) return false;
|
||
float progress = GetFishPathProgress(m);
|
||
return progress >= BotLockPathMin && progress <= BotLockPathMax;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 多目标查找(TripleLock)
|
||
/// <summary>鱼锁定优先级:特殊Boss(5) > Boss鱼(4) > 特殊鱼(3) > 黄金鱼(2) > 普通鱼(1)</summary>
|
||
private int GetFishLockPriority(MonsterContral monster)
|
||
{
|
||
switch (monster.fishenum)
|
||
{
|
||
case FishTypeEnum.特殊Boss: return 5;
|
||
case FishTypeEnum.Boss鱼: return 4;
|
||
case FishTypeEnum.特殊鱼: return 3;
|
||
case FishTypeEnum.黄金鱼: return 2;
|
||
case FishTypeEnum.普通鱼: return 1;
|
||
default: return 0;
|
||
}
|
||
}
|
||
|
||
/// <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;
|
||
|
||
// ① 主锁目标排第一(打哪锁哪)
|
||
if (currSitData.isLock && currSitData.monCotl_Lock != null &&
|
||
currSitData.monCotl_Lock.gameObject.activeInHierarchy)
|
||
{
|
||
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;
|
||
}
|
||
|
||
// ② 按优先级从分类字典补满剩余位置
|
||
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,
|
||
};
|
||
|
||
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; // 跳过已选中的
|
||
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的鱼不作为目标
|
||
if (!IsFishPathLockable(m)) continue;
|
||
|
||
// ⭐ 统一用 ResolveLockTransform:colliderList → lockPoint → transform
|
||
Transform pos = ResolveLockTransform(m);
|
||
|
||
result.Add((m, pos));
|
||
selected.Add(m.guid);
|
||
|
||
if (result.Count >= maxCount) return result;
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
#endregion
|
||
|
||
#region 锁定系统(LockModule)
|
||
/// <summary>
|
||
/// 刷新三发锁定图标:三发+锁定开启时显示3个锁定圈
|
||
/// </summary>
|
||
private void RefreshTripleLockIcons()
|
||
{
|
||
if (currSitData == null) return;
|
||
|
||
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);
|
||
}
|
||
|
||
//private void HandleLock()
|
||
//{
|
||
// CrossPlatformInput input = GetInput();
|
||
|
||
// if (!toggle_Lock.isOn) return;
|
||
|
||
// // 只有按下或抬起的那一帧触发锁定(保留你原来的逻辑)
|
||
// if (!input.wasReleasedThisFrame && !input.wasPressedThisFrame) return;
|
||
|
||
// // UI 点击过滤
|
||
// if (IsPointerOverUI(input.position))
|
||
// return;
|
||
|
||
// // 🔧 修正:屏幕坐标转世界坐标需要指定 Z 轴(2D 正交相机专用)
|
||
// Vector3 mousePos = input.position;
|
||
// mousePos.z = -Camera.main.transform.position.z; // 关键修正
|
||
// Vector2 worldPos = Camera.main.ScreenToWorldPoint(mousePos);
|
||
|
||
// // 🔧 优化:将 LayerMask 缓存为成员变量,避免每次调用 GetMask(非必要,但更好)
|
||
// // 你可以在 Start 中初始化:fishLayerMask = LayerMask.GetMask("Fish");
|
||
// // 这里直接写原方式也可以,但建议如下:
|
||
// int fishLayer = LayerMask.GetMask("Fish");
|
||
// RaycastHit2D hit = Physics2D.Raycast(worldPos, Vector2.zero, 1, fishLayer);
|
||
|
||
// if (hit.collider != null)
|
||
// {
|
||
// MonsterContral monster = hit.collider.GetComponentInParent<MonsterContral>();
|
||
// currSitData.SetLockTarget(monster, hit.collider.transform);
|
||
|
||
// // 保留你原来的调用方式:new List<string> { monster?.guid }
|
||
// SyncLock(new List<string> { monster?.guid }, monster.colliderList.IndexOf(hit.collider.transform));
|
||
// }
|
||
// else
|
||
// {
|
||
// currSitData.ClearLock();
|
||
// SyncTurretState(TurretState.Idle);
|
||
// SyncLock(null); // 保留你原来的重载(可能是 SyncLock(object) 或 SyncLock(string))
|
||
// }
|
||
//}
|
||
private void HandleLock()
|
||
{
|
||
// ⭐ 退出倒计时期间屏蔽本地玩家手动锁定,其他玩家和机器人不受影响
|
||
if (_isLeavingCountdown) return;
|
||
|
||
CrossPlatformInput input = GetInput();
|
||
if (!toggle_Lock.isOn) return;
|
||
if (!input.wasPressedThisFrame && !input.wasReleasedThisFrame) return;
|
||
if (IsPointerOverUI(input.position)) return;
|
||
|
||
// 射线从相机出发,指向鼠标点击点
|
||
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");
|
||
|
||
// 获取所有命中(可能有多个)
|
||
RaycastHit2D[] hits = Physics2D.RaycastAll(mouseWorld, dir, distance, fishLayer);
|
||
if (hits.Length == 0)
|
||
{
|
||
// 未命中:清除锁定
|
||
currSitData.ClearLock();
|
||
SyncTurretState(TurretState.Idle);
|
||
SyncLock(null);
|
||
return;
|
||
}
|
||
|
||
// 按 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;
|
||
if (depth > bestDepth) // 更大 Z → 更近
|
||
{
|
||
bestDepth = depth;
|
||
bestHit = hit;
|
||
}
|
||
}
|
||
|
||
// 处理选中的最佳命中
|
||
MonsterContral monster = bestHit.collider.GetComponentInParent<MonsterContral>();
|
||
// ⭐ 统一用 ResolveLockTransform:colliderList 第一个活跃 → lockPoint → transform
|
||
// 返回的 colliderIndex 用于网络同步,其他客户端按此索引取同一碰撞器
|
||
int colliderIndex;
|
||
Transform lockTr = ResolveLockTransform(monster, out colliderIndex);
|
||
currSitData.SetLockTarget(monster, lockTr);
|
||
SyncLock(new List<string> { monster?.guid }, colliderIndex);
|
||
}
|
||
private void UpdateLockFX()
|
||
{
|
||
|
||
foreach (var sit in list_Sits)
|
||
{
|
||
if (!sit.isLock || sit.monCotl_Lock == null) continue;
|
||
|
||
// 更新主锁定图标
|
||
UpdateSingleLockFX(sit, sit.monCotl_Lock, sit.monCotl_Lock_Position, sit.lockFX);
|
||
|
||
// 更新三发锁定额外图标
|
||
for (int i = 0; i < sit.lockFX_Triple.Count && i < sit.tripleLockTargets.Count; i++)
|
||
{
|
||
var icon = sit.lockFX_Triple[i];
|
||
var monster = sit.tripleLockTargets[i];
|
||
var hitPos = sit.tripleLockPositions[i];
|
||
if (icon == null || monster == null || hitPos == null) continue;
|
||
UpdateSingleLockFX(sit, monster, hitPos, icon);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void UpdateSingleLockFX(SitPosData sit, MonsterContral monster, Transform hitPos, GameObject icon)
|
||
{
|
||
Vector3 pos = Vector3.zero;
|
||
|
||
if (!hitPos.gameObject.activeInHierarchy)
|
||
{
|
||
// ⭐ 原始 hitPos 失效 → 用 ResolveLockTransform 重新找一个有效位置
|
||
// 统一逻辑:colliderList 第一个活跃 → lockPoint → transform
|
||
Transform fallback = ResolveLockTransform(monster);
|
||
if (fallback == null || !fallback.gameObject.activeInHierarchy)
|
||
{
|
||
if (icon.activeSelf) icon.SetActive(false);
|
||
return;
|
||
}
|
||
pos = fallback.position;
|
||
}
|
||
else
|
||
{
|
||
pos = hitPos.position;
|
||
if (!icon.activeSelf) icon.SetActive(true);
|
||
}
|
||
|
||
pos.z = Camera.main.transform.position.z + 1;
|
||
icon.transform.position = pos;
|
||
}
|
||
private void CheckLockTargetValid()
|
||
{
|
||
if (!currSitData.isLock) return;
|
||
|
||
var monster = currSitData.monCotl_Lock;
|
||
|
||
// 目标无效:被对象池回收 / 不激活 / 被销毁
|
||
if (monster == null || !monster.gameObject.activeInHierarchy)
|
||
{
|
||
currSitData.ClearLock(); // 清除锁定
|
||
SyncTurretState(TurretState.Idle);
|
||
SyncLock(null); // 同步服务器
|
||
|
||
// ⭐⭐⭐ 关键:怪死了 → 动画回到 Idle ⭐⭐⭐
|
||
currSitData.ChangeState(TurretState.Idle);
|
||
SyncTurretState(TurretState.Idle);
|
||
}
|
||
}
|
||
|
||
private void HandleAutoLock()
|
||
{
|
||
// ⭐ 退出倒计时期间屏蔽本地玩家自动锁定,其他玩家和机器人不受影响
|
||
if (_isLeavingCountdown) return;
|
||
|
||
// 自动锁定必须满足:自动开火 + 锁定开关开启
|
||
if (!toggle_SkillAuto.isOn) return;
|
||
if (!toggle_Lock.isOn) return; // ✔ 自动锁定必须在锁定开关开启时才工作
|
||
|
||
// 已经有锁定目标并且还活着,就不重复找
|
||
if (currSitData.isLock &&
|
||
currSitData.monCotl_Lock != null &&
|
||
currSitData.monCotl_Lock.gameObject.activeInHierarchy)
|
||
{
|
||
return;
|
||
}
|
||
|
||
MonsterContral target = FindAutoLockTarget();
|
||
if (target != null)
|
||
{
|
||
// ⭐ 统一用 ResolveLockTransform:colliderList 第一个活跃 → lockPoint → transform
|
||
int colliderIndex;
|
||
Transform lockTr = ResolveLockTransform(target, out colliderIndex);
|
||
currSitData.SetLockTarget(target, lockTr);
|
||
SyncLock(new List<string> { target?.guid }, colliderIndex);
|
||
}
|
||
else
|
||
{
|
||
currSitData.ClearLock();
|
||
SyncTurretState(TurretState.Idle);
|
||
SyncLock(null);
|
||
}
|
||
}
|
||
|
||
private MonsterContral FindAutoLockTarget()
|
||
{
|
||
if (CreateFishManager.Instance == null ||
|
||
CreateFishManager.Instance.fishDatas_hashCodekey == null ||
|
||
CreateFishManager.Instance.fishDatas_hashCodekey.Count == 0)
|
||
return null;
|
||
|
||
MonsterContral best = null;
|
||
float bestSqr = float.MaxValue;
|
||
|
||
// ⭐ 用世界坐标(Z=0平面),和 bot AI 一致,否则距离计算会异常(出现 3.4E+38 之类)
|
||
Vector3 origin = currSitData.turretWorldPos;
|
||
|
||
foreach (var kv in CreateFishManager.Instance.fishDatas_hashCodekey)
|
||
{
|
||
MonsterContral m = kv.Value;
|
||
if (m == null || !m.gameObject.activeInHierarchy) continue;
|
||
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的鱼不作为自动锁定目标
|
||
if (!IsFishPathLockable(m)) continue;
|
||
|
||
float sqr = (m.transform.position - origin).sqrMagnitude;
|
||
if (sqr < bestSqr)
|
||
{
|
||
bestSqr = sqr;
|
||
best = m;
|
||
}
|
||
}
|
||
|
||
return best;
|
||
}
|
||
|
||
//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(子弹系统)
|
||
public int sitBulletMaxCount = 100;
|
||
/// <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;
|
||
}
|
||
|
||
private BullitMove CreateBullet(SitPosData sit, Vector2 pos, float angle, bool isDouble, bool isLock, string id,
|
||
MonsterContral lockTarget = null, Transform lockHitPos = null)
|
||
{
|
||
// ⭐ 远程座位炮台未加载时补加载
|
||
if (sit.currentTurret == null)
|
||
{
|
||
sit.LoadTurret(sit.currentTurretNumber);
|
||
if (sit.currentTurret == null) return null;
|
||
}
|
||
|
||
GameObject prefab = isDouble ? sit.currentTurret.bulletPrefab_Two : sit.currentTurret.bulletPrefab;
|
||
|
||
// ⭐ 每座位最多100发子弹(先校验,不扣钱)
|
||
int activeBullets = 0;
|
||
foreach (Transform child in bulletParent.transform)
|
||
{
|
||
if (child.TryGetComponent<BullitMove>(out var bm) && bm.userid == sit.user)
|
||
activeBullets++;
|
||
}
|
||
if (activeBullets >= sitBulletMaxCount)
|
||
{
|
||
UIManager.Instance.ShowBoardError($"[子弹上限] 座位{sit.posID}已达{sitBulletMaxCount}发上限");
|
||
return null;
|
||
}
|
||
|
||
if (!sit.ConsumeGold(isDouble ? 2 : 1))
|
||
return null;
|
||
|
||
// ⭐ 从对象池取子弹(池空时内部会 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>();
|
||
|
||
// ⭐ 池化复用时必须重新记录起点(Init 会把 vector3_startPosition 对齐到当前位置,z=-400)
|
||
// 否则 Update 里 transform.position = vector3_startPosition + ... 会用回收时的旧起点
|
||
bullet.Init();
|
||
|
||
bullet.userid = sit.user;
|
||
bullet.playerID = sit.user;
|
||
bullet.bulletID = id;
|
||
|
||
if (isLock)
|
||
{
|
||
if (lockTarget != null)
|
||
bullet.Lock(lockTarget, lockHitPos);
|
||
else if (sit.monCotl_Lock != null && sit.monCotl_Lock_Position != null)
|
||
bullet.Lock(sit.monCotl_Lock, sit.monCotl_Lock_Position);
|
||
// 锁定目标或位置为 null → 不锁定,按普通子弹发射
|
||
}
|
||
|
||
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)
|
||
{
|
||
var fireData = new FrameData_FireBullet
|
||
{
|
||
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();
|
||
}
|
||
|
||
SyncGameData sgd = new SyncGameData
|
||
{
|
||
sitPos = currSitData.posID,
|
||
frameType = FrameDataType.fireBullet,
|
||
time = BeijingTimeManager.Instance.CurrentTimestampMs,
|
||
frameData = fireData
|
||
};
|
||
//EnqueueSync(sgd);
|
||
var roomdata = GameManager.Instance.gameData.currRoomData;
|
||
var user = GameManager.Instance.user;
|
||
|
||
// 三射 = 3 发,否则 = 1 发
|
||
int count = isSanShe ? 3 : 1;
|
||
|
||
// ⭐ bulletIds 由服务端生成并通过 RoomFireNotice 广播回来,客户端不需要传
|
||
long timespan = BeijingTimeManager.Instance.CurrentTimestampMs; // 毫秒
|
||
long timestamp = timespan / 1000; // 秒级
|
||
string sign = WebSocketSendList.MakeFireCannonSign(
|
||
roomdata.roomId, user.id, currSitData.consumeGoldNum, count, timestamp, "63215f0afa3f5b25");
|
||
|
||
// FireCannon:roomId, userId, gold, count, timestamp, sign, extra
|
||
WebSocketSendList.FireCannon(
|
||
roomdata.roomId,
|
||
user.id,
|
||
currSitData.consumeGoldNum,
|
||
count,
|
||
timestamp,
|
||
sign,
|
||
sgd
|
||
);
|
||
}
|
||
|
||
|
||
|
||
private void SyncLock(List<string> monsterguid, int index = -1)
|
||
{
|
||
SyncGameData sgd = new SyncGameData
|
||
{
|
||
sitPos = currSitData.posID,
|
||
frameType = FrameDataType.changeAutoTarget,
|
||
time = BeijingTimeManager.Instance.CurrentTimestampMs,
|
||
frameData = new FrameData_ChangeAutoTarget
|
||
{
|
||
targetGuid = monsterguid,
|
||
colliderIndex = index,
|
||
}
|
||
};
|
||
WebSocketSendList.SyncGameData(sgd);
|
||
}
|
||
private void SyncTurretState(TurretState state)
|
||
{
|
||
SyncGameData sgd = new SyncGameData
|
||
{
|
||
sitPos = currSitData.posID,
|
||
frameType = FrameDataType.changeTurretState,
|
||
time = BeijingTimeManager.Instance.CurrentTimestampMs,
|
||
frameData = new FrameData_TurretState { state = state }
|
||
};
|
||
|
||
WebSocketSendList.SyncGameData(sgd);
|
||
}
|
||
public void SyncChangeTurret(int turretNumber)
|
||
{
|
||
SyncGameData sgd = new SyncGameData
|
||
{
|
||
sitPos = currSitData.posID,
|
||
frameType = FrameDataType.changeTurret,
|
||
time = BeijingTimeManager.Instance.CurrentTimestampMs,
|
||
frameData = new FrameData_ChangeTurret { turretIndex = turretNumber }
|
||
};
|
||
|
||
WebSocketSendList.SyncGameData(sgd);
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
|
||
#endregion
|
||
|
||
#region ⑦ 服务器广播处理(NetReceiveModule)
|
||
public void SyncGameData_Rotate(SyncGameData sync)
|
||
{
|
||
if (sync.sitPos == currSitData.posID) return;
|
||
|
||
var sit = list_Sits[sync.sitPos];
|
||
// ⭐ 房主不处理机器人旋转广播:房主在 RotateBotToTarget 里已直接旋转本地炮台,
|
||
// 再走 SmoothRotate 会和本地直接旋转冲突
|
||
if (sit != null && sit.isBot && currSitData == SitPosData.roomOwnerSit) return;
|
||
|
||
var data = FrameDataParser.Parse<FrameData_Rotate>(sync.frameData);
|
||
SmoothRotate(sit, data.angle);
|
||
}
|
||
|
||
public void SyncGameData_FireBullit(FireCannonSendData sync)
|
||
{
|
||
|
||
// ⭐ 跳过本地玩家(本地玩家开火由 SyncFire → 服务器广播回来后创建子弹,
|
||
// 但如果服务器不回广播给开火者,本地玩家需要在 SyncFire 里直接 FireBullet)
|
||
// 实际设计:本地玩家也通过广播回来创建子弹,所以不跳过
|
||
|
||
// extra 为空:从 botLastFireData 或默认参数还原(bot 或真人玩家都可能走到这里)
|
||
if (sync.extra == null || sync.extra.frameData == null)
|
||
{
|
||
SitPosData sit = list_Sits.Find(s => s.user == sync.userId);
|
||
if (sit == null) return;
|
||
|
||
float angle = botLastAngle.TryGetValue(sync.userId, out float a) ? a : sit.currentAngle;
|
||
if (sync.bulletIds == null || sync.bulletIds.Count == 0) return;
|
||
|
||
// ⭐ 优先用服务器广播的 sync.isBot 标记判断是否机器人
|
||
// bot 从 botLastFireData 还原完整技能参数;真人玩家用默认参数
|
||
bool isBot = sync.isBot || sit.isBot;
|
||
if (isBot && botLastFireData.TryGetValue(sync.userId, out var fireData))
|
||
{
|
||
FireBullet(sit, angle, fireData.isSanShe, fireData.twoRTruuect,
|
||
fireData.isSuoDing, sync.bulletIds, fireData.monsterIDs);
|
||
}
|
||
else
|
||
{
|
||
FireBullet(sit, angle, false, false, false, sync.bulletIds, null);
|
||
}
|
||
return;
|
||
}
|
||
|
||
var data = FrameDataParser.Parse<FrameData_FireBullet>(sync.extra.frameData);
|
||
FireBullet(list_Sits[sync.extra.sitPos], data.angle, data.isSanShe, data.twoRTruuect, data.isSuoDing, sync.bulletIds, data.monsterIDs);
|
||
}
|
||
|
||
/// <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;
|
||
botLastFireData[sit.user] = fireData; // 存储完整 fireData 供 SyncGameData_FireBullit 还原技能状态
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
MonsterContral monster = CreateFishManager.Instance.fishDatas_hashCodekey[data.targetGuid[0]];
|
||
|
||
// ⭐ 统一用 ResolveLockTransform:colliderList 第一个活跃 → lockPoint → transform
|
||
// 注:网络同步过来的 colliderIndex 在统一逻辑下不再使用,因为客户端之间
|
||
// colliderList 顺序可能不一致,统一用"第一个活跃"更稳定
|
||
Transform hitPos = ResolveLockTransform(monster);
|
||
|
||
list_Sits[sync.sitPos].SetLockTarget(monster, hitPos);
|
||
}
|
||
|
||
public void SyncGameData_RendromDieFish(SyncGameData sync)
|
||
{
|
||
var data = JsonConvert.DeserializeObject<FrameData_RandomDieFish>(sync.frameData.ToString());
|
||
|
||
// 用 sync.sitPos 判断是否本地玩家发的(服务器不会丢这个字段)
|
||
bool isLocalSender = sync.sitPos == currSitData.posID;
|
||
|
||
CreateFishManager.Instance.Id_DieListFish_FX(
|
||
data.guids,
|
||
data.coinNum,
|
||
new Vector3(data.x, data.y, data.z),
|
||
data.count,
|
||
isLocalSender
|
||
);
|
||
}
|
||
|
||
|
||
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);
|
||
list_Sits[sync.sitPos].LoadTurret(data.turretIndex);
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// UsePropNotice 回调:服务器确认道具使用后,激活对应效果
|
||
/// </summary>
|
||
public void OnUsePropNotice(string userId, string propNumber)
|
||
{
|
||
switch (propNumber)
|
||
{
|
||
case "40006": // 暴走
|
||
_localBaoZouCount--;
|
||
UpdateSkillPropsUI();
|
||
CreateFishManager.Instance.currGameType.isFrenzy = true;
|
||
SetFrenzy(true);
|
||
currFrenzyTime = frenzyDuration;
|
||
toggle_BaoZou.SetIsOnWithoutNotify(true);
|
||
break;
|
||
|
||
case "40007": // 冰封
|
||
_localBingFengCount--;
|
||
UpdateSkillPropsUI();
|
||
currBingFengTime = bingFengDuration;
|
||
toggle_BingFeng.SetIsOnWithoutNotify(true);
|
||
if (bingFengEffect != null)
|
||
{
|
||
if (_bingFengInstance == null)
|
||
_bingFengInstance = Instantiate(bingFengEffect);
|
||
_bingFengInstance.SetActive(true);
|
||
}
|
||
CreateFishManager.Instance.FreezeAllFish();
|
||
PauseAllFishAnimations();
|
||
break;
|
||
}
|
||
}
|
||
|
||
public void SyncGameData_FreezeFish(SyncGameData sync)
|
||
{
|
||
if (sync.sitPos == currSitData.posID) return; // 自己发的不管
|
||
|
||
var data = FrameDataParser.Parse<FrameData_FreezeFish>(sync.frameData);
|
||
if (data.isFreeze)
|
||
{
|
||
CreateFishManager.Instance.FreezeAllFish();
|
||
PauseAllFishAnimations();
|
||
}
|
||
else
|
||
{
|
||
CreateFishManager.Instance.UnfreezeAllFish();
|
||
ResumeAllFishAnimations();
|
||
}
|
||
}
|
||
|
||
|
||
#endregion
|
||
#region 冰冻动画暂停/恢复
|
||
|
||
private void PauseAllFishAnimations()
|
||
{
|
||
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;
|
||
foreach (AnimationState state in an) state.speed = 0f;
|
||
_frozenAnimSpeeds[an] = 1f;
|
||
}
|
||
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)
|
||
{
|
||
case Spine.Unity.SkeletonAnimation s: s.timeScale = kv.Value; break;
|
||
case Animator a: a.speed = kv.Value; break;
|
||
case Animation an:
|
||
foreach (AnimationState state in an) state.speed = 1f;
|
||
break;
|
||
case UnityEngine.Playables.PlayableDirector pd: pd.Resume(); break;
|
||
}
|
||
}
|
||
_frozenAnimSpeeds.Clear();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 玩家加入/退出(PlayerJoinModule)
|
||
|
||
public void JoinGame(int sitNum, long goldNum, long diamondNum, long consumeGold, int currentTurretIndex, string user = null, string cannonId = null, string wingId = null)
|
||
{
|
||
foreach (var sit in list_Sits)
|
||
{
|
||
if (sit.posID != sitNum) continue;
|
||
|
||
// ⭐ 确保座位 GameObject 本身是激活的(可能被 HandleBotExit 隐藏或初始为 inactive)
|
||
if (!sit.gameObject.activeSelf)
|
||
sit.gameObject.SetActive(true);
|
||
|
||
sit.user = user ?? GameManager.Instance.user.id;
|
||
sit.goldNum = goldNum;
|
||
sit.consumeGoldNum = consumeGold;
|
||
// ⭐ 同步 indexConsume 到后端下发的 cannonMultiple 对应档位,避免加减炮倍时从 0 档跳变
|
||
sit.SyncIndexByConsume(consumeGold);
|
||
|
||
// ⭐ 在 ShowPlayer 之前标记 isBot,否则 ShowPlayer 里的 AssignRoomOwner 会把 bot 设为房主
|
||
if (!string.IsNullOrEmpty(user) && user.StartsWith("bot_"))
|
||
sit.isBot = true;
|
||
|
||
// bot 进场:记录运行时状态(进场金币 / 初始档位)
|
||
if (sit.isBot)
|
||
InitBotRuntimeState(sit, goldNum);
|
||
|
||
sit.tMP_GoldNum.text = goldNum.ToString();
|
||
sit.tMP_DiamondNum.text = diamondNum.ToString();
|
||
sit.tMP_TurretConsumeNum.text = consumeGold.ToString();
|
||
|
||
sit.ShowPlayer();
|
||
|
||
if (!string.IsNullOrEmpty(cannonId) && int.TryParse(cannonId, out int turretNum))
|
||
sit.LoadTurret(turretNum);
|
||
else if (sit.isBot)
|
||
{
|
||
// bot 没有炮台ID时给一个默认炮台
|
||
var turretList = TurretResourcesData.GetTurretDataByType(1);
|
||
if (turretList != null && turretList.Count > 0)
|
||
sit.LoadTurret(turretList[0].turretNumber);
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(wingId) && int.TryParse(wingId, out int wingNum))
|
||
sit.LoadWing(wingNum);
|
||
}
|
||
}
|
||
|
||
public void TuiChuHHWK()
|
||
{
|
||
WebSocketSendList.LeaveRoom(GameManager.Instance.gameData.currRoomData.roomId, currSitData.user);
|
||
CreateFishManager.Instance.fangzhu = false;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 人机AI
|
||
private void HandleBotAI()
|
||
{
|
||
if (SitPosData.roomOwnerSit == null || currSitData != SitPosData.roomOwnerSit) return;
|
||
|
||
// ⭐ enableBot 是总开关(Inspector),优先级最高;cfg.enabled 是子开关
|
||
if (!GameManager.Instance.enableBot) return;
|
||
|
||
var cfg = GameManager.Instance.botConfig;
|
||
if (cfg == null || !cfg.enabled) return;
|
||
|
||
var fishList = CreateFishManager.Instance;
|
||
if (fishList == null || fishList.fishDatas_hashCodekey == null) return;
|
||
|
||
BotBehaviorConfig behavior = GetBotBehavior(cfg);
|
||
float rotateSpeed = behavior != null ? behavior.rotateSpeed : botRotateSpeed;
|
||
|
||
foreach (var sit in list_Sits)
|
||
{
|
||
if (!sit.isBot || sit.gameObject.activeSelf == false) continue;
|
||
if (sit.tra_CurrTurret == null) continue;
|
||
|
||
// ====== 运行时状态 ======
|
||
BotRuntimeState state = GetBotRuntimeState(sit);
|
||
if (state == null || state.hasExited) continue;
|
||
|
||
// ====== 退出检查 ======
|
||
if (CheckBotExit(sit, state, cfg)) continue;
|
||
|
||
Vector3 turretPos = sit.turretWorldPos;
|
||
|
||
// ====== 技能刷新(底层随机技能) ======
|
||
if (cfg != null && cfg.skills != null)
|
||
RefreshBotSkills(sit, cfg.skills);
|
||
var skill = botSkills.ContainsKey(sit.posID) ? botSkills[sit.posID] : null;
|
||
|
||
// ====== Boss 检测 → 决策行为模式 ======
|
||
List<MonsterContral> bosses = FindBossesInField(sit, fishList, turretPos, cfg);
|
||
BotBehaviorMode mode = DecideBotMode(sit, state, bosses, cfg, skill);
|
||
state.currentMode = mode;
|
||
|
||
// ====== 倍数调整(第一次:基于进场金币比率) ======
|
||
AdjustBotMultiplier(sit, state, cfg);
|
||
|
||
// ====== 目标选择 ======
|
||
MonsterContral target = GetOrFindBotTarget(sit, fishList, turretPos, skill, mode, bosses, cfg);
|
||
if (target == null) continue;
|
||
|
||
// ====== 锁定技能:走真实锁定系统 ======
|
||
bool wantLock = (skill != null && skill.lockActive) || mode == BotBehaviorMode.LockBoss;
|
||
if (wantLock)
|
||
{
|
||
if (sit.monCotl_Lock != target)
|
||
{
|
||
sit.isLock = true;
|
||
sit.monCotl_Lock = target;
|
||
// ⭐ 统一用 ResolveLockTransform:colliderList 第一个活跃 → lockPoint → transform
|
||
sit.monCotl_Lock_Position = ResolveLockTransform(target);
|
||
if (sit.lockFX != null) sit.lockFX.SetActive(true);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (sit.isLock)
|
||
{
|
||
sit.isLock = false;
|
||
sit.monCotl_Lock = null;
|
||
if (sit.lockFX != null) sit.lockFX.SetActive(false);
|
||
}
|
||
}
|
||
|
||
// ====== 旋转 ======
|
||
RotateBotToTarget(sit, target.transform.position, rotateSpeed);
|
||
|
||
// ====== 倍数再调整(第二次:策略执行后再校验) ======
|
||
AdjustBotMultiplier(sit, state, cfg);
|
||
|
||
// ====== 射击 ======
|
||
if (!botFireTimer.ContainsKey(sit.posID))
|
||
botFireTimer[sit.posID] = 0f;
|
||
botFireTimer[sit.posID] += Time.deltaTime;
|
||
|
||
float fireInterval = behavior != null
|
||
? Random.Range(behavior.fireIntervalMin, behavior.fireIntervalMax)
|
||
: attackTime;
|
||
if (skill != null && skill.frenzyActive)
|
||
fireInterval *= 0.5f;
|
||
|
||
if (botFireTimer[sit.posID] >= fireInterval)
|
||
{
|
||
botFireTimer[sit.posID] = 0f;
|
||
BotFire(sit, target, skill, mode, cfg, behavior);
|
||
}
|
||
|
||
// 空闲超时切回 Idle 动画
|
||
if (!botIdleTimer.ContainsKey(sit.posID))
|
||
botIdleTimer[sit.posID] = 0f;
|
||
botIdleTimer[sit.posID] += Time.deltaTime;
|
||
if (botIdleTimer[sit.posID] > 0.3f)
|
||
{
|
||
sit.ChangeState(TurretState.Idle);
|
||
botIdleTimer[sit.posID] = 0f;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>bot 进场时初始化运行时状态</summary>
|
||
private void InitBotRuntimeState(SitPosData sit, long entryGold)
|
||
{
|
||
var cfg = GameManager.Instance.botConfig;
|
||
int initIdx = cfg != null && cfg.multiplier != null ? cfg.multiplier.initialLevelIndex : 0;
|
||
|
||
var s = new BotRuntimeState
|
||
{
|
||
entryGold = entryGold,
|
||
entryTime = Time.time,
|
||
currentLevelIndex = initIdx,
|
||
nextMultiplierCheckTime = Time.time + 2f,
|
||
nextExitCheckTime = Time.time + 5f,
|
||
hasExited = false
|
||
};
|
||
botRuntimeStates[sit.posID] = s;
|
||
|
||
// 把初始档位应用到炮台
|
||
ApplyBotLevelIndex(sit, initIdx);
|
||
}
|
||
|
||
/// <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;
|
||
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的 Boss 不作为目标
|
||
if (!IsFishPathLockable(m)) continue;
|
||
|
||
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;
|
||
state.nextMultiplierCheckTime = Time.time + mc.checkInterval;
|
||
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按档位索引设置 bot 炮台消耗。
|
||
/// 同时更新底层数据 consumeGoldNum、文本显示,并广播给其他客户端,
|
||
/// 保证所有客户端的 bot 炮倍一致(发射子弹扣金币、打中鱼计算奖励都用最新值)。
|
||
/// 不走 SyncCannonMultiple(那个用的是本地玩家 currSitData.posID,不能给 bot 用)。
|
||
/// </summary>
|
||
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();
|
||
// ⭐ 同步 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);
|
||
}
|
||
|
||
/// <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;
|
||
}
|
||
|
||
/// <summary>处理 bot 退出:发离房消息、座位清理、转移房主、保证出鱼</summary>
|
||
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);
|
||
}
|
||
|
||
// 清理锁定
|
||
sit.isLock = false;
|
||
sit.monCotl_Lock = null;
|
||
if (sit.lockFX != null) sit.lockFX.SetActive(false);
|
||
|
||
// 清理记忆
|
||
botTargetGuid.Remove(sit.posID);
|
||
botFireTimer.Remove(sit.posID);
|
||
botIdleTimer.Remove(sit.posID);
|
||
botSkills.Remove(sit.posID);
|
||
botLastAngle.Remove(sit.user);
|
||
botLastFireData.Remove(sit.user);
|
||
|
||
// ⭐ bot 退出后转移房主,否则房主是已退出的 bot 会导致出鱼停止
|
||
// 1) 清除 bot 的房主引用(如果它是房主)
|
||
// 2) TransferRoomOwner 会把房主交给第一个活跃的非 bot 玩家
|
||
if (SitPosData.roomOwnerSit == sit)
|
||
{
|
||
SitPosData.roomOwnerSit = null;
|
||
SitPosData.TransferRoomOwner();
|
||
}
|
||
|
||
// ⭐ 强制保证本地玩家能继续出鱼:
|
||
// bot 退出后如果本地玩家是新的房主(或房主为空但本地玩家在场),开启 fangzhu
|
||
// 这是对 ChangeControllerUser 网络消息的兜底,防止网络消息丢失或时序问题导致不出鱼
|
||
if (CreateFishManager.Instance != null)
|
||
{
|
||
bool localIsOwner = SitPosData.roomOwnerSit != null &&
|
||
SitPosData.roomOwnerSit.user == GameManager.Instance.user.id;
|
||
bool noOwner = SitPosData.roomOwnerSit == null;
|
||
if (localIsOwner || noOwner)
|
||
CreateFishManager.Instance.fangzhu = true;
|
||
}
|
||
|
||
if (cfg != null && cfg.exitCondition != null && cfg.exitCondition.showWaitingAfterExit)
|
||
sit.ShowWaiting();
|
||
else
|
||
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);
|
||
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;
|
||
bool isLock = (skill != null && skill.lockActive) || mode == BotBehaviorMode.LockBoss;
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取或寻找目标:优先用记忆的目标(还在前方就沿用),死了/不在前方了再重新找
|
||
/// </summary>
|
||
private MonsterContral GetOrFindBotTarget(SitPosData sit, CreateFishManager fishList, Vector3 turretPos, BotSkillRuntime skill,
|
||
BotBehaviorMode mode, List<MonsterContral> bosses, BotConfig cfg)
|
||
{
|
||
bool isLocked = (skill != null && skill.lockActive) || mode == BotBehaviorMode.LockBoss;
|
||
|
||
// 1. 检查记忆目标是否还有效
|
||
if (botTargetGuid.TryGetValue(sit.posID, out string guid))
|
||
{
|
||
if (fishList.fishDatas_hashCodekey.TryGetValue(guid, out var m)
|
||
&& m != null && m.gameObject.activeInHierarchy)
|
||
{
|
||
// ⭐ 路径进度过滤:如果记忆目标已经超出可锁定范围(即将离场),清除记忆重新找
|
||
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);
|
||
}
|
||
}
|
||
|
||
// 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 > 黄金鱼 > 特殊鱼 > 普通鱼,同类型打最近)
|
||
MonsterContral target = SelectBestTarget(sit, fishList, turretPos);
|
||
if (target != null)
|
||
botTargetGuid[sit.posID] = target.guid;
|
||
return target;
|
||
}
|
||
|
||
/// <summary>从 Boss 列表中选倍率(beilv)最大的(跳过已死亡的)</summary>
|
||
private MonsterContral SelectMaxMultiplierBoss(List<MonsterContral> bosses)
|
||
{
|
||
MonsterContral best = null;
|
||
int maxBeiLv = int.MinValue;
|
||
foreach (var b in bosses)
|
||
{
|
||
if (b == null || !b.gameObject.activeInHierarchy) continue;
|
||
if (b.beilv > maxBeiLv)
|
||
{
|
||
maxBeiLv = b.beilv;
|
||
best = b;
|
||
}
|
||
}
|
||
return best;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按优先级选目标:特殊Boss(5) > Boss(2) > 黄金鱼(3) > 特殊鱼(4) > 普通鱼(1)
|
||
/// 同一优先级内选最近的,只选炮台前方的鱼
|
||
/// </summary>
|
||
private MonsterContral SelectBestTarget(SitPosData sit, CreateFishManager fishList, Vector3 turretPos)
|
||
{
|
||
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;
|
||
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的鱼不作为目标
|
||
if (!IsFishPathLockable(m)) continue;
|
||
|
||
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; // 普通鱼
|
||
}
|
||
}
|
||
|
||
/// <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;
|
||
// ⭐ 用世界坐标(Z=0平面),和 bot AI / 玩家自动锁定一致
|
||
Vector3 turretPos = sit.turretWorldPos;
|
||
|
||
foreach (var kv in fishList.fishDatas_hashCodekey)
|
||
{
|
||
var m = kv.Value;
|
||
if (m == null || !m.gameObject.activeInHierarchy) continue;
|
||
if (m == exclude) continue;
|
||
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的鱼不作为目标
|
||
if (!IsFishPathLockable(m)) continue;
|
||
|
||
Vector3 fishPos = m.transform.position;
|
||
if (sit.IsFlipped && fishPos.y >= turretPos.y) continue;
|
||
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;
|
||
}
|
||
|
||
/// <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);
|
||
}
|
||
|
||
/// <summary>人机炮台旋转(房主端执行)</summary>
|
||
private void RotateBotToTarget(SitPosData sit, Vector3 fishWorldPos, float rotateSpeed = -1f)
|
||
{
|
||
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;
|
||
|
||
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg - 90f;
|
||
|
||
if (sit.IsFlipped) angle = (angle + 180f) % 360f;
|
||
|
||
// Lerp 旋转:自然减速,速度由 rotateSpeed 控制
|
||
float current = sit.currentAngle;
|
||
float speed = rotateSpeed > 0f ? rotateSpeed : botRotateSpeed;
|
||
// t 越大旋转越快,clamp 防过冲
|
||
float t = Mathf.Min(speed * 0.15f * Time.deltaTime, 1f);
|
||
float smoothed = Mathf.LerpAngle(current, angle, t);
|
||
sit.SetRotation(smoothed);
|
||
|
||
// ⭐ 机器人旋转帧同步:和开炮一样实时广播
|
||
// 只保留极小节流(角度>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);
|
||
}
|
||
|
||
/// <summary>人机开火同步(房主代发)</summary>
|
||
private void SyncFire_ForBot(SitPosData botSit, float angle, long gold,
|
||
bool isSanShe, bool isDouble, bool isLock, List<string> monsterIDs)
|
||
{
|
||
var fireData = new FrameData_FireBullet
|
||
{
|
||
angle = angle,
|
||
gold = gold,
|
||
isSanShe = isSanShe,
|
||
twoRTruuect = isDouble,
|
||
isSuoDing = isLock,
|
||
};
|
||
|
||
if (monsterIDs != null && monsterIDs.Count > 0)
|
||
fireData.monsterIDs = monsterIDs;
|
||
|
||
SyncGameData sgd = new SyncGameData
|
||
{
|
||
sitPos = botSit.posID,
|
||
frameType = FrameDataType.fireBullet,
|
||
time = BeijingTimeManager.Instance.CurrentTimestampMs,
|
||
frameData = fireData
|
||
};
|
||
WebSocketSendList.SyncGameData(sgd);
|
||
}
|
||
#endregion
|
||
|
||
}
|