From d2c8d91748f240350a5db75bb4d62c61b2a9ad2f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=A3=AE2003?= <2384017988@qq.com>
Date: Sat, 11 Jul 2026 17:07:32 +0800
Subject: [PATCH] 1
---
Assets/Framework/Pool/BulletPool.cs | 179 +++++++++
Assets/Framework/Pool/BulletPool.cs.meta | 11 +
Assets/Framework/Pool/Pool.cs | 7 +-
.../Scripts/Items/Bullits/BullitMove.cs | 23 +-
.../Scripts/Net/WebResponses/BotConfig.cs | 140 +++++++
.../ChineseChess/ChineseChessGameClassList.cs | 54 ++-
.../ChineseChess/UIChineseChessDistrict.cs | 19 +-
.../aa/UI/Game/GameMainPanel_FishBase.cs | 363 +++++++++++++++---
Assets/MyGame/aa/UI/Home/Sub/ZhanLing.prefab | 8 +-
9 files changed, 733 insertions(+), 71 deletions(-)
create mode 100644 Assets/Framework/Pool/BulletPool.cs
create mode 100644 Assets/Framework/Pool/BulletPool.cs.meta
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: