1
This commit is contained in:
@@ -163,6 +163,8 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
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;
|
||||
@@ -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<BullitMove>();
|
||||
// ⭐ 从对象池取子弹(池空时内部会 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>();
|
||||
|
||||
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<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;
|
||||
|
||||
// ====== 锁定技能:走真实锁定系统(显示锁定图标) ======
|
||||
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<string> monsterIDs = null;
|
||||
if (isSanShe && isLock && target != null)
|
||||
{
|
||||
monsterIDs = new List<string> { 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
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
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>按档位索引设置炮台消耗(直接对齐 consumeGoldNum,避免触发音效/SyncCannonMultiple)</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();
|
||||
}
|
||||
|
||||
/// <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);
|
||||
|
||||
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)
|
||||
private MonsterContral GetOrFindBotTarget(SitPosData sit, CreateFishManager fishList, Vector3 turretPos, BotSkillRuntime skill,
|
||||
BotBehaviorMode mode, List<MonsterContral> 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;
|
||||
}
|
||||
|
||||
/// <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) continue;
|
||||
if (b.beilv > maxBeiLv)
|
||||
{
|
||||
maxBeiLv = b.beilv;
|
||||
best = b;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按优先级选目标:特殊Boss(5) > Boss(2) > 黄金鱼(3) > 特殊鱼(4) > 普通鱼(1)
|
||||
/// 同一优先级内选最近的,只选炮台前方的鱼
|
||||
|
||||
Reference in New Issue
Block a user