diff --git a/Assets/Framework/Pool/BulletPool.cs b/Assets/Framework/Pool/BulletPool.cs new file mode 100644 index 000000000..aaddfb3f2 --- /dev/null +++ b/Assets/Framework/Pool/BulletPool.cs @@ -0,0 +1,179 @@ +using System.Collections.Generic; +using UnityEngine; + +/// +/// 子弹专用对象池 — 按 prefab 实例ID 分桶,与鱼的 Pool 完全隔离 +/// 用法: +/// var go = BulletPool.Instance.Get(prefab, pos, rot, parent); +/// BulletPool.Instance.Return(go); // 代替 Destroy +/// +public class BulletPool : Singleton +{ + // prefab.GetInstanceID() → 该 prefab 的池 + private Dictionary dic = new Dictionary(); + + // 每个池子的最大缓存数(超出直接 Destroy,防止内存膨胀) + private const int MaxCachePerPrefab = 200; + + // 池子根节点(隐藏在场景里) + private GameObject poolRoot; + + /// 从池中取一个子弹(池空则 Instantiate) + public GameObject Get(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent) + { + if (prefab == null) return null; + + int id = prefab.GetInstanceID(); + GameObject obj; + + if (dic.TryGetValue(id, out var data) && data.poolList.Count > 0) + { + obj = data.GetObj(); + obj.transform.position = position; + obj.transform.rotation = rotation; + obj.transform.SetParent(parent, false); + obj.SetActive(true); + } + else + { + obj = GameObject.Instantiate(prefab, position, rotation, parent); + obj.SetActive(true); + } + + // 确保挂上 tracker 记录 prefabID(回收时凭此找到对应池) + var tracker = obj.GetComponent(); + if (tracker == null) + tracker = obj.AddComponent(); + tracker.prefabID = id; + + return obj; + } + + /// 无 parent 重载 + public GameObject Get(GameObject prefab, Vector3 position, Quaternion rotation) + { + return Get(prefab, position, rotation, null); + } + + /// 回收子弹到池(超容量则 Destroy) + public void Return(GameObject obj) + { + if (obj == null) return; + + var tracker = obj.GetComponent(); + int id = tracker != null ? tracker.prefabID : 0; + + // 取不到 id → 兜底 Destroy(非池内对象) + if (id == 0) + { + GameObject.Destroy(obj); + return; + } + + if (!dic.TryGetValue(id, out var data)) + dic[id] = data = new BulletPoolData(EnsurePoolRoot().transform, id); + + if (data.poolList.Count >= MaxCachePerPrefab) + { + GameObject.Destroy(obj); + return; + } + + // 重置子弹状态(若有 BullitMove 组件) + var bm = obj.GetComponent(); + if (bm != null) bm.ResetState(); + + data.PushObj(obj); + } + + /// 预加载 N 个某 prefab 到池 + public void Preload(GameObject prefab, int count) + { + if (prefab == null || count <= 0) return; + int id = prefab.GetInstanceID(); + + if (!dic.TryGetValue(id, out var data)) + dic[id] = data = new BulletPoolData(EnsurePoolRoot().transform, id); + + for (int i = 0; i < count; i++) + { + var obj = GameObject.Instantiate(prefab, EnsurePoolRoot().transform); + var tracker = obj.GetComponent(); + if (tracker == null) + tracker = obj.AddComponent(); + tracker.prefabID = id; + data.PushObj(obj); + } + } + + /// 清理所有子弹池(切场景时调用) + public void Clear() + { + foreach (var kv in dic.Values) + kv.Clear(); + dic.Clear(); + + if (poolRoot != null) + { + GameObject.Destroy(poolRoot); + poolRoot = null; + } + } + + private GameObject EnsurePoolRoot() + { + if (poolRoot == null) + { + poolRoot = new GameObject("[BulletPool]"); + poolRoot.SetActive(false); // 池根节点隐藏,池内对象跟随隐藏 + } + return poolRoot; + } +} + +/// 挂在池内对象上,记录所属 prefabID(回收时凭此找到对应池) +public class BulletPoolTracker : MonoBehaviour +{ + public int prefabID; +} + +/// 单个 prefab 的子弹池数据 +public class BulletPoolData +{ + public Transform fatherObj; + public List poolList; + + public BulletPoolData(Transform root, int id) + { + fatherObj = new GameObject("bulletPool_" + id).transform; + fatherObj.SetParent(root, false); + poolList = new List(); + } + + public void PushObj(GameObject obj) + { + if (obj == null) return; + poolList.Add(obj); + obj.transform.SetParent(fatherObj, false); + obj.SetActive(false); + } + + public GameObject GetObj() + { + GameObject obj = poolList[poolList.Count - 1]; + poolList.RemoveAt(poolList.Count - 1); + return obj; + } + + public void Clear() + { + foreach (var obj in poolList) + { + if (obj != null) GameObject.Destroy(obj); + } + poolList.Clear(); + + if (fatherObj != null) + GameObject.Destroy(fatherObj.gameObject); + } +} diff --git a/Assets/Framework/Pool/BulletPool.cs.meta b/Assets/Framework/Pool/BulletPool.cs.meta new file mode 100644 index 000000000..c99b6054e --- /dev/null +++ b/Assets/Framework/Pool/BulletPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b450d462c4ecd3349b396f1ff2014f40 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Framework/Pool/Pool.cs b/Assets/Framework/Pool/Pool.cs index 10c1b1249..a199af11b 100644 --- a/Assets/Framework/Pool/Pool.cs +++ b/Assets/Framework/Pool/Pool.cs @@ -1,4 +1,4 @@ -using System.Collections; +using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; @@ -17,11 +17,12 @@ public class PoolData public void PushObj(GameObject obj,int id) { - obj.SetActive(false); + // 先实例化,再隐藏(原代码对 prefab 调 SetActive(false) 无效且语义混乱) obj = GameObject.Instantiate(obj); obj.name = id.ToString(); - poolList.Add(obj); obj.transform.parent = fatherObj.transform; + obj.SetActive(false); + poolList.Add(obj); } public void PushObj(GameObject obj) { diff --git a/Assets/MyGame/Scripts/Items/Bullits/BullitMove.cs b/Assets/MyGame/Scripts/Items/Bullits/BullitMove.cs index a6472d879..b7409c068 100644 --- a/Assets/MyGame/Scripts/Items/Bullits/BullitMove.cs +++ b/Assets/MyGame/Scripts/Items/Bullits/BullitMove.cs @@ -62,13 +62,30 @@ public class BullitMove : MonoBehaviour transform.position = vector3_startPosition; } + /// 对象池回收前调用:重置所有运行时状态,避免复用时残留旧数据 + public void ResetState() + { + userid = null; + playerID = null; + bulletID = null; + currShowTime = 0f; + isTriggerFirst = true; + target = null; + targetTra = null; + // 解除锁定 + 恢复碰撞器 + NotLock(); + // 重置起始位置(z=-400 是原 Awake→Init 的默认值) + vector3_startPosition = transform.position; + vector3_startPosition.z = -400; + } + void Update() { // 子弹生命周期:超时自动销毁(默认10秒) currShowTime += Time.deltaTime; if (currShowTime >= desTime) { - Destroy(gameObject); + BulletPool.Instance.Return(gameObject); return; } @@ -136,7 +153,7 @@ public class BullitMove : MonoBehaviour monster.monsterData.charactersHealth.OnHurt(10, CharacterHurtType.none); Destroy(monster.gameObject); } - Destroy(gameObject); + BulletPool.Instance.Return(gameObject); } } } @@ -228,6 +245,6 @@ public class BullitMove : MonoBehaviour } monster.OnHurt(); - Destroy(gameObject); + BulletPool.Instance.Return(gameObject); } } \ No newline at end of file diff --git a/Assets/MyGame/Scripts/Net/WebResponses/BotConfig.cs b/Assets/MyGame/Scripts/Net/WebResponses/BotConfig.cs index 28031e9ad..73758913f 100644 --- a/Assets/MyGame/Scripts/Net/WebResponses/BotConfig.cs +++ b/Assets/MyGame/Scripts/Net/WebResponses/BotConfig.cs @@ -30,6 +30,146 @@ public class BotConfig [Header("aggressive 状态参数")] public BotBehaviorConfig aggressiveBehavior = new BotBehaviorConfig(); + + [Header("倍数策略")] + public BotMultiplierConfig multiplier = new BotMultiplierConfig(); + + [Header("Boss策略")] + public BotBossStrategyConfig bossStrategy = new BotBossStrategyConfig(); + + [Header("退出条件")] + public BotExitConfig exitCondition = new BotExitConfig(); +} + +/// 机器人倍数(炮台档位)策略配置 +[Serializable] +public class BotMultiplierConfig +{ + [Tooltip("进场初始炮台档位索引(对应 paoBeiList 的下标, 0=最低档)")] + public int initialLevelIndex = 0; + + [Tooltip("低于此倍数(当前金币/进场金币) → 降档")] + public float decreaseThreshold = 0.5f; + + [Tooltip("高于此倍数(当前金币/进场金币) → 升档")] + public float increaseThreshold = 1.5f; + + [Tooltip("每次调整的档位数")] + public int step = 1; + + [Tooltip("倍数检查间隔(秒)")] + public float checkInterval = 3f; + + [Tooltip("最低保持档位索引(不会降到低于此值)")] + public int minLevelIndex = 0; + + [Tooltip("最高保持档位索引(不会升到高于此值), -1=不限制")] + public int maxLevelIndex = -1; +} + +/// 机器人Boss策略配置 +[Serializable] +public class BotBossStrategyConfig +{ + [Tooltip("视为Boss的鱼类型 (按 FishTypeEnum 的int值)")] + public int[] bossFishTypes = { 2, 5 }; // Boss鱼=2, 特殊Boss=5 + + [Tooltip("无Boss时启用散射乱射模式")] + public bool scatterWhenNoBoss = true; + + [Tooltip("无Boss时定点三发概率(0~1), 否则单射")] + [Range(0f, 1f)] public float tripleShotProb = 0.5f; + + [Tooltip("有Boss时优先锁定最大倍率(赔率)的Boss")] + public bool preferMaxMultiplierBoss = true; + + [Tooltip("Boss数量超过此值 → 开启散射模式")] + public int scatterModeThreshold = 3; + + [Tooltip("Boss数量<=此值时随机狂暴技能概率(0~1)")] + [Range(0f, 1f)] public float frenzyProb = 0.6f; + + [Tooltip("狂暴持续时间下限(秒)")] + public float frenzyMinTime = 3f; + + [Tooltip("狂暴持续时间上限(秒)")] + public float frenzyMaxTime = 8f; +} + +/// 机器人退出条件配置 +[Serializable] +public class BotExitConfig +{ + [Tooltip("金币超过此值时退出(盈利离场), 0=不检测")] + public long exitWhenGoldAbove = 0; + + [Tooltip("金币低于此值时退出(输完离场)")] + public long exitWhenGoldBelow = 100; + + [Tooltip("退出检查间隔(秒)")] + public float checkInterval = 5f; + + [Tooltip("退出时是否发送离房网络消息")] + public bool sendLeaveRoom = true; + + [Tooltip("退出后座位是否显示等待状态(等待下一个机器人)")] + public bool showWaitingAfterExit = true; +} + +/// 机器人当前行为模式 +public enum BotBehaviorMode +{ + Idle, // 待机(未开始) + ScatterRandom, // 散射乱射(无Boss): 随机定点三发或单射 + LockBoss, // 锁定Boss + ScatterMode, // 散射模式(Boss数量>3) + RandomFrenzy, // 随机狂暴(Boss数量<=3) + Exited // 已退出 +} + +/// 每个 bot 座位的运行时状态(进场金币/倍数/模式/退出) +public class BotRuntimeState +{ + // ===== 进场 ===== + [Tooltip("进场时的金币(基准)")] + public long entryGold; + + [Tooltip("进场时间")] + public float entryTime; + + // ===== 倍数 ===== + [Tooltip("当前炮台档位索引")] + public int currentLevelIndex; + + [Tooltip("下次倍数检查时间")] + public float nextMultiplierCheckTime; + + // ===== 行为模式 ===== + [Tooltip("当前行为模式")] + public BotBehaviorMode currentMode = BotBehaviorMode.Idle; + + [Tooltip("上次Boss检测时间")] + public float lastBossCheckTime; + + // ===== 狂暴(Boss<=3时随机) ===== + [Tooltip("狂暴是否激活")] + public bool frenzyActive; + + [Tooltip("狂暴结束时间")] + public float frenzyEndTime; + + // ===== 退出 ===== + [Tooltip("下次退出检查时间")] + public float nextExitCheckTime; + + [Tooltip("是否已退出")] + public bool hasExited; + + /// 当前金币相对进场金币的倍数 + public float GetGoldRatio(long currentGold) + { + return entryGold > 0 ? (float)currentGold / entryGold : 0f; + } } /// diff --git a/Assets/MyGame/Scripts/Net/WebResponses/ChineseChess/ChineseChessGameClassList.cs b/Assets/MyGame/Scripts/Net/WebResponses/ChineseChess/ChineseChessGameClassList.cs index 93959f753..cf1778784 100644 --- a/Assets/MyGame/Scripts/Net/WebResponses/ChineseChess/ChineseChessGameClassList.cs +++ b/Assets/MyGame/Scripts/Net/WebResponses/ChineseChess/ChineseChessGameClassList.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using UnityEngine; @@ -76,13 +76,63 @@ namespace Assets.MyGame.Scripts.Net.WebResponses.ChineseChess public string createUserId { get; set; } /// - /// 状态 1 未开始 2 进行中 3 已结束 + /// 状态 0 空房间 1 未开始 2 进行中 3 已结束 /// public int state { get; set; } = 1; /// /// 金币数量 /// public long gold { get; set; } + + /// + /// 机器人数量(房间内 AI 玩家数,计入总人数) + /// + public int robotCount { get; set; } + + /// + /// 结束游戏请求记录(前端不用,仅占位反序列化) + /// + public object endGameRequests { get; set; } + + /// + /// 最后活跃时间(Unix 秒) + /// + public long lastActiveTime { get; set; } + + /// + /// 超时秒数 + /// + public int timeoutSeconds { get; set; } = 60; + + /// + /// 超时输方用户ID + /// + public string timeoutLoserUserId { get; set; } + + /// + /// 待定赢方用户ID(可能为 null) + /// + public string pendingWinUserId { get; set; } + + /// + /// 游戏是否已结束 + /// + public bool isGameEnded { get; set; } + + /// + /// 房间唯一 GUID(与 roomId 不同,roomId 是业务编号) + /// + public string id { get; set; } + + /// + /// 是否已删除 + /// + public bool isDelete { get; set; } + + /// + /// 创建信息(前端不用,仅占位反序列化) + /// + public object createInfo { get; set; } } [Serializable] /// diff --git a/Assets/MyGame/aa/UI/Game/ChineseChess/UIChineseChessDistrict.cs b/Assets/MyGame/aa/UI/Game/ChineseChess/UIChineseChessDistrict.cs index f062e4f17..d0cfa7141 100644 --- a/Assets/MyGame/aa/UI/Game/ChineseChess/UIChineseChessDistrict.cs +++ b/Assets/MyGame/aa/UI/Game/ChineseChess/UIChineseChessDistrict.cs @@ -73,13 +73,14 @@ public class UIChineseChessDistrict : SubUIBase { switch (ChineseChessMainPanel.dic_RoomData[item.roomId].state) { - case 1: + case 0: // 空房间 + case 1: // 未开始 int temp = ChineseChessMainPanel.dic_RoomData[item.roomId].users.Count; - if (temp == 2) + if (temp >= 2) { UIManager.Instance.ShowBoardError("玩家满员"); } - else if (temp == 1) + else { uIManager.ShowAsync(UIPanel.ZGXQJoinGame, item); } @@ -88,7 +89,6 @@ public class UIChineseChessDistrict : SubUIBase UIManager.Instance.ShowBoardError("房间已经开始"); break; case 3: - case 0: uIManager.ShowAsync(UIPanel.ZGXQCreateRoom, item); break; default: @@ -100,7 +100,8 @@ public class UIChineseChessDistrict : SubUIBase ChineseChessMainPanel.dic_RoomData[item.roomId] = item; var slider = tran.transform.GetChild(0).GetComponent(); - int allPlayerCount = item.users.Count; + // 人数 = 真实玩家 + 机器人(users.Count 只含真实玩家,robotCount 是 AI) + int allPlayerCount = item.users.Count + item.robotCount; slider.value = (float)allPlayerCount / 2; if (allPlayerCount == 2) slider.fillRect.GetChild(0).gameObject.SetActive(true); @@ -193,13 +194,15 @@ public class UIChineseChessDistrict : SubUIBase ChineseChessMainPanel.dic_RoomData[roomID] = item; var tran = itemListDic[roomID]; - var slider = tran.transform.GetChild(1).GetComponent(); - int allPlayerCount = item.users.Count; + // 子节点索引与 Show 保持一致:0=Slider, 1=人数文本, 2=房间号 + var slider = tran.transform.GetChild(0).GetComponent(); + // 人数 = 真实玩家 + 机器人 + int allPlayerCount = item.users.Count + item.robotCount; slider.value = (float)allPlayerCount / 2; if (allPlayerCount == 2) slider.fillRect.GetChild(0).gameObject.SetActive(true); else slider.fillRect.GetChild(0).gameObject.SetActive(false); - tran.transform.GetChild(2).GetComponent().text = allPlayerCount + "/" + 2; + tran.transform.GetChild(1).GetComponent().text = allPlayerCount + "/" + 2; } } diff --git a/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs b/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs index 15348d662..d63541b07 100644 --- a/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs +++ b/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs @@ -163,6 +163,8 @@ public class GameMainPanel_FishBase : GameMainPanel_Base private Dictionary botIdleTimer = new Dictionary(); // bot 最近的 FrameData_FireBullet(给其他客户端还原技能状态) private Dictionary botLastFireData = new Dictionary(); + // bot 运行时状态(进场金币/倍数档位/行为模式/退出) + private Dictionary botRuntimeStates = new Dictionary(); public GameObject bullet_Test; public GameObject bulletParent; float changeTurretTime = 0f; @@ -1510,8 +1512,10 @@ public class GameMainPanel_FishBase : GameMainPanel_Base if (!sit.ConsumeGold(isDouble ? 2 : 1)) return null; - var bullet = Instantiate(prefab, pos, Quaternion.Euler(0, 0, angle), bulletParent.transform) - .GetComponent(); + // ⭐ 从对象池取子弹(池空时内部会 Instantiate) + var bulletGo = BulletPool.Instance.Get(prefab, pos, Quaternion.Euler(0, 0, angle), bulletParent.transform); + if (bulletGo == null) return null; + var bullet = bulletGo.GetComponent(); bullet.userid = sit.user; bullet.playerID = sit.user; @@ -1896,6 +1900,10 @@ public class GameMainPanel_FishBase : GameMainPanel_Base 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(); @@ -1945,20 +1953,35 @@ public class GameMainPanel_FishBase : GameMainPanel_Base 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.tra_CurrTurret.position; - // ====== 技能刷新 ====== + // ====== 技能刷新(底层随机技能) ====== if (cfg != null && cfg.skills != null) RefreshBotSkills(sit, cfg.skills); var skill = botSkills.ContainsKey(sit.posID) ? botSkills[sit.posID] : null; - // ====== 目标选择(记住当前目标,死了/飞走了才换) ====== - MonsterContral target = GetOrFindBotTarget(sit, fishList, turretPos, skill); + // ====== Boss 检测 → 决策行为模式 ====== + List 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; - // ====== 锁定技能:走真实锁定系统(显示锁定图标) ====== - if (skill != null && skill.lockActive) + // ====== 锁定技能:走真实锁定系统 ====== + bool wantLock = (skill != null && skill.lockActive) || mode == BotBehaviorMode.LockBoss; + if (wantLock) { if (sit.monCotl_Lock != target) { @@ -1979,10 +2002,13 @@ public class GameMainPanel_FishBase : GameMainPanel_Base } } - // ====== 旋转(始终运行,Lerp自然减速) ====== + // ====== 旋转 ====== RotateBotToTarget(sit, target.transform.position, rotateSpeed); - // ====== 持续射击 ====== + // ====== 倍数再调整(第二次:策略执行后再校验) ====== + AdjustBotMultiplier(sit, state, cfg); + + // ====== 射击 ====== if (!botFireTimer.ContainsKey(sit.posID)) botFireTimer[sit.posID] = 0f; botFireTimer[sit.posID] += Time.deltaTime; @@ -1996,48 +2022,7 @@ public class GameMainPanel_FishBase : GameMainPanel_Base if (botFireTimer[sit.posID] >= fireInterval) { botFireTimer[sit.posID] = 0f; - - // 攻击动画 - 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; - - // 技能参数(模仿真人 SyncFire 的行为) - bool isSanShe = skill != null && skill.tripleActive; - bool isDouble = skill != null && skill.doubleActive; - bool isLock = skill != null && skill.lockActive; - int count = isSanShe ? 3 : 1; - long gold = isDouble ? sit.consumeGoldNum * 2 : sit.consumeGoldNum; - - // 三发+锁定 → 找 N 个目标鱼的 GUID - List monsterIDs = null; - if (isSanShe && isLock && target != null) - { - monsterIDs = new List { target.guid }; - // 额外找 2 个目标(按优先级) - var extraTargets = FindBotLockTargets(sit, target, 2); - foreach (var m in extraTargets) monsterIDs.Add(m.guid); - } - - // 存储完整 fireData 供其他客户端还原 - 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); + BotFire(sit, target, skill, mode, cfg, behavior); } // 空闲超时切回 Idle 动画 @@ -2052,12 +2037,245 @@ public class GameMainPanel_FishBase : GameMainPanel_Base } } + /// bot 进场时初始化运行时状态 + 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); + } + + /// 获取 bot 运行时状态 + 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; + } + + /// 查找场内炮台前方的 Boss 列表 + private List FindBossesInField(SitPosData sit, CreateFishManager fishList, Vector3 turretPos, BotConfig cfg) + { + var result = new List(); + 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; + + 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; + } + + /// 根据 Boss 存在情况决策行为模式,并覆盖技能开关 + private BotBehaviorMode DecideBotMode(SitPosData sit, BotRuntimeState state, List 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; + } + + /// 倍数升降:当前金币/进场金币 < 阈值降档, > 阈值升档 + 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); + } + } + + /// 当前炮倍档位总数(paoBeiList 长度) + private int GetPaoBeiCount() + { + if (paoBeiList != null && paoBeiList.Count > 0) return paoBeiList.Count; + return 1; + } + + /// 按档位索引设置炮台消耗(直接对齐 consumeGoldNum,避免触发音效/SyncCannonMultiple) + 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(); + } + + /// 退出条件检查:金币>设定 或 金币<设定 + 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; + } + + /// 处理 bot 退出:发离房消息、座位清理 + 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); + + if (cfg != null && cfg.exitCondition != null && cfg.exitCondition.showWaitingAfterExit) + sit.ShowWaiting(); + else + sit.gameObject.SetActive(false); + } + + /// bot 开火执行(从 HandleBotAI 抽出) + 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 monsterIDs = null; + if (isSanShe && isLock && target != null) + { + monsterIDs = new List { 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); + } + /// /// 获取或寻找目标:优先用记忆的目标(还在前方就沿用),死了/不在前方了再重新找 /// - private MonsterContral GetOrFindBotTarget(SitPosData sit, CreateFishManager fishList, Vector3 turretPos, BotSkillRuntime skill) + private MonsterContral GetOrFindBotTarget(SitPosData sit, CreateFishManager fishList, Vector3 turretPos, BotSkillRuntime skill, + BotBehaviorMode mode, List bosses, BotConfig cfg) { - bool isLocked = skill != null && skill.lockActive; + bool isLocked = (skill != null && skill.lockActive) || mode == BotBehaviorMode.LockBoss; // 1. 检查记忆目标是否还有效 if (botTargetGuid.TryGetValue(sit.posID, out string guid)) @@ -2076,13 +2294,50 @@ public class GameMainPanel_FishBase : GameMainPanel_Base botTargetGuid.Remove(sit.posID); } - // 2. 重新找目标(Boss > 黄金鱼 > 特殊鱼 > 普通鱼,同类型打最近) + // 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; } + /// 从 Boss 列表中选倍率(beilv)最大的 + private MonsterContral SelectMaxMultiplierBoss(List bosses) + { + MonsterContral best = null; + int maxBeiLv = int.MinValue; + foreach (var b in bosses) + { + if (b == null) continue; + if (b.beilv > maxBeiLv) + { + maxBeiLv = b.beilv; + best = b; + } + } + return best; + } + /// /// 按优先级选目标:特殊Boss(5) > Boss(2) > 黄金鱼(3) > 特殊鱼(4) > 普通鱼(1) /// 同一优先级内选最近的,只选炮台前方的鱼 diff --git a/Assets/MyGame/aa/UI/Home/Sub/ZhanLing.prefab b/Assets/MyGame/aa/UI/Home/Sub/ZhanLing.prefab index ed072b09a..392f9ca2a 100644 --- a/Assets/MyGame/aa/UI/Home/Sub/ZhanLing.prefab +++ b/Assets/MyGame/aa/UI/Home/Sub/ZhanLing.prefab @@ -14640,13 +14640,16 @@ MonoBehaviour: m_EditorClassIdentifier: img_MianFei: {fileID: 4217081698748454709} img_MianFeiDuiGou: {fileID: 4330799887900817023} + img_MianFeiBackForword: {fileID: 0} tmp_MianFeiPropCount: {fileID: 2852674146106605074} img_JingYing: {fileID: 7888319788910343562} img_JingYingDuiGou: {fileID: 8171562548857813552} + img_JingYingBackForword: {fileID: 0} img_JingYingLocal: {fileID: 2255445385472211906} tmp_JingYingPropCount: {fileID: 9179038560444378523} img_ZhiZun: {fileID: 5489928632928310148} img_ZhiZunDuiGou: {fileID: 7680212169677354408} + img_ZhiZunBackForword: {fileID: 0} img_ZhiZunLocal: {fileID: 4908228247286929228} tmp_ZhiZunPropCount: {fileID: 5372547274870681584} tmp_currLevel: {fileID: 1548048096998792596} @@ -21780,13 +21783,16 @@ MonoBehaviour: m_EditorClassIdentifier: img_MianFei: {fileID: 4680046685106973707} img_MianFeiDuiGou: {fileID: 7485202241167141595} + img_MianFeiBackForword: {fileID: 0} tmp_MianFeiPropCount: {fileID: 2402272884510985401} img_JingYing: {fileID: 1845906190580021354} img_JingYingDuiGou: {fileID: 5578691452543731326} + img_JingYingBackForword: {fileID: 0} img_JingYingLocal: {fileID: 1603058651523562701} tmp_JingYingPropCount: {fileID: 962158706182398437} img_ZhiZun: {fileID: 6578186223567257880} img_ZhiZunDuiGou: {fileID: 1456507539317513943} + img_ZhiZunBackForword: {fileID: 0} img_ZhiZunLocal: {fileID: 3480563155218242293} tmp_ZhiZunPropCount: {fileID: 4221848766627442506} tmp_currLevel: {fileID: 2337044026528177313} @@ -25849,7 +25855,7 @@ RectTransform: m_AnchorMin: {x: 0, y: 1} m_AnchorMax: {x: 0, y: 1} m_AnchoredPosition: {x: 117.685, y: -25} - m_SizeDelta: {x: 235.37, y: 50} + m_SizeDelta: {x: 0, y: 50} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &4614401808319948171 CanvasRenderer: