using System.Collections.Generic; using UnityEngine; namespace BaseGames.Enemies.Abilities { /// /// 多攻击选招器:从候选(category==Attack)里按"射程 + 冷却 + LOS + 着地"过滤, /// 再按 选一个。纯逻辑、无 MonoBehaviour 依赖,可单测。 /// public sealed class EnemyAttackSelector { /// 防重复折扣系数的默认值。EnemyStatsSO 字段初值与 EnemyBase 的兜底分支共用此常量。 public const float DefaultAntiRepeatFactor = 0.3f; private readonly List _candidates; // 权重缓冲区(与 _candidates 等长,每次选招复用):避免每次选招 new List 造成 GC private readonly List _weightBuf; // 防重复折扣系数:0 = 有替代时绝不连续重复,1 = 不惩罚 private float _antiRepeatFactor; private IAttackCandidate _lastPicked; public EnemyAttackSelector(IEnumerable candidates, float antiRepeatFactor = DefaultAntiRepeatFactor) { _candidates = new List(candidates); _weightBuf = new List(_candidates.Count); AntiRepeatFactor = antiRepeatFactor; // 经属性赋值,夹紧逻辑只有一处 } public int Count => _candidates.Count; /// 防重复折扣系数。与 attackSelectionMode 一样每次选招前由调用方刷新, /// 使策划在播放模式下调参能立即生效。 public float AntiRepeatFactor { get => _antiRepeatFactor; set => _antiRepeatFactor = Mathf.Clamp01(value); } /// 上一次实际选中的招(防重复折扣的作用对象;供测试与调试面板读取)。 public IAttackCandidate LastPicked => _lastPicked; /// 对象池复用 / 阶段切换时清除防重复记忆。 public void ResetRepeatMemory() => _lastPicked = null; private static bool Eligible(IAttackCandidate c, bool hasLOS, bool grounded) => c != null && c.CanUse && c.InAttackRange() && (!c.RequiresLineOfSight || hasLOS) && (!c.RequiresGrounded || grounded); public bool HasEligible(bool hasLOS, bool grounded) { for (int i = 0; i < _candidates.Count; i++) if (Eligible(_candidates[i], hasLOS, grounded)) return true; return false; } public IAttackCandidate Select(bool hasLOS, bool grounded, AttackSelectionMode mode) { switch (mode) { case AttackSelectionMode.Priority: return Remember(SelectByPriority(hasLOS, grounded)); case AttackSelectionMode.WeightedRandomAntiRepeat: return Remember(SelectByWeight(hasLOS, grounded, antiRepeat: true)); default: return Remember(SelectByWeight(hasLOS, grounded, antiRepeat: false)); } } // 只在真选中了东西时更新记忆:选空时保留上一招,避免"空一帧就清掉防重复"。 private IAttackCandidate Remember(IAttackCandidate pick) { if (pick != null) _lastPicked = pick; return pick; } private IAttackCandidate SelectByPriority(bool hasLOS, bool grounded) { IAttackCandidate best = null; for (int i = 0; i < _candidates.Count; i++) { var c = _candidates[i]; if (!Eligible(c, hasLOS, grounded)) continue; if (best == null || c.Priority > best.Priority) best = c; // 并列取首个 } return best; } private IAttackCandidate SelectByWeight(bool hasLOS, bool grounded, bool antiRepeat) { // 有效权重:不合格候选填 0(由共享原语保证零权重项绝不被选中)。缓冲区复用 → 选招零 GC。 _weightBuf.Clear(); for (int i = 0; i < _candidates.Count; i++) { var c = _candidates[i]; float w = Eligible(c, hasLOS, grounded) ? Mathf.Max(0f, c.Weight) : 0f; // 防重复:上一招权重打折。折扣可把权重压到 0——此时若它是唯一候选, // 下面的 Priority 退化路径会把它选出来,不会出现"敌人永远不出手"。 if (antiRepeat && w > 0f && ReferenceEquals(c, _lastPicked)) w *= _antiRepeatFactor; _weightBuf.Add(w); } int idx = BaseGames.Core.WeightedPick.Index(_weightBuf); if (idx >= 0) return _candidates[idx]; // 无正权重(合格候选权重全 0,或全被防重复折扣压平)→ 退化为按 Priority 选(并列取首个) return SelectByPriority(hasLOS, grounded); } } }