diff --git a/Assets/Tests/EditMode/AI/Fakes/FakeAiContext.cs b/Assets/Tests/EditMode/AI/Fakes/FakeAiContext.cs index 34b2bf33..a0d037bf 100644 --- a/Assets/Tests/EditMode/AI/Fakes/FakeAiContext.cs +++ b/Assets/Tests/EditMode/AI/Fakes/FakeAiContext.cs @@ -25,6 +25,13 @@ namespace BaseGames.Tests.EditMode.AI public bool NoAbilityRunning => Running == null; public bool CanUseAbility(string id) => CanUse; public void InterruptAbilities() => Running = null; + public bool Eligible; // 测试控制:是否有合格攻击 + public bool HasEligibleAttack() => Eligible; + public bool UseBestAttack() + { + if (!Eligible) return false; + Used.Add("best"); Running = "best"; return true; + } } public sealed class FakeVitals : IActorVitals diff --git a/Assets/_Game/Scripts/AI/ICombatant.cs b/Assets/_Game/Scripts/AI/ICombatant.cs index fc4dda9b..62f3560f 100644 --- a/Assets/_Game/Scripts/AI/ICombatant.cs +++ b/Assets/_Game/Scripts/AI/ICombatant.cs @@ -8,5 +8,11 @@ namespace BaseGames.AI bool NoAbilityRunning { get; } bool CanUseAbility(string abilityId); void InterruptAbilities(); // 中断当前所有能力(如退出追击时停追击能力) + + /// 攻击选择器中是否有"够得着且未冷却"的招(供 Approach→Attack 转换)。 + bool HasEligibleAttack(); + + /// 按敌人配置的选招模式选一个攻击并触发;返回是否成功触发。 + bool UseBestAttack(); } } diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs b/Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs index 50ea5111..24a4e7f8 100644 --- a/Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs +++ b/Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs @@ -82,6 +82,25 @@ namespace BaseGames.Enemies public void InterruptAbilities() => _enemy.Abilities?.InterruptAll(Abilities.InterruptReason.ExternalRequest); + public bool HasEligibleAttack() + { + var sel = _enemy.AttackSelector; + if (sel == null) return false; + return sel.HasEligible(_enemy.IsPlayerVisible(), _enemy.Movement != null && _enemy.Movement.IsGrounded); + } + + public bool UseBestAttack() + { + var sel = _enemy.AttackSelector; + if (sel == null) return false; + var mode = _enemy.StatsSO != null + ? _enemy.StatsSO.attackSelectionMode + : Abilities.AttackSelectionMode.WeightedRandom; + bool grounded = _enemy.Movement != null && _enemy.Movement.IsGrounded; + var pick = sel.Select(_enemy.IsPlayerVisible(), grounded, mode); + return pick != null && pick.Execute(); + } + // ---- IActorVitals ---- public bool IsAlive => _enemy.IsAlive; public bool IsControllable => _enemy.CurrentState == EnemyStateType.Controlled; diff --git a/Assets/_Game/Scripts/Enemies/EnemyBase.cs b/Assets/_Game/Scripts/Enemies/EnemyBase.cs index 85f5b86f..993a1dd3 100644 --- a/Assets/_Game/Scripts/Enemies/EnemyBase.cs +++ b/Assets/_Game/Scripts/Enemies/EnemyBase.cs @@ -221,6 +221,9 @@ namespace BaseGames.Enemies /// 能力注册表(架构 §8.3)。Awake 时自动收集所有 EnemyAbilityBase 组件。 public EnemyAbilityRegistry Abilities => _abilities; private readonly EnemyAbilityRegistry _abilities = new EnemyAbilityRegistry(); + /// 攻击选择器(从已注册能力里筛出 category==Attack 的候选,Awake 时构建)。 + public Abilities.EnemyAttackSelector AttackSelector => _attackSelector; + private Abilities.EnemyAttackSelector _attackSelector; /// 由 _onPlayerSpawned 事件缓存的玩家 Transform,供 BD 任务读取。 public Transform PlayerTransform => _playerTransform; /// 感知 Hub;供 BD 任务及 QuotaManager 暂停/恢复感知使用。 @@ -537,6 +540,7 @@ namespace BaseGames.Enemies _pooledObject = GetComponent(); _brain = GetComponent(); _abilities.CollectFrom(gameObject); + BuildAttackSelector(); _colliders = GetComponentsInChildren(true); // 收集配置型行为模块(零代码扩展点) @@ -552,6 +556,30 @@ namespace BaseGames.Enemies // 订阅在 OnEnable 中处理 } + /// + /// 从已注册能力里收集 category==Attack 的候选,构建攻击选择器。 + /// 根因校验:攻击招 rangeRadius<=0(永远够不着)显式报错,不静默兜底。 + /// + private void BuildAttackSelector() + { + var candidates = new System.Collections.Generic.List(); + var all = _abilities?.All; + if (all != null) + { + for (int i = 0; i < all.Count; i++) + { + var ab = all[i]; + if (ab == null || ab.Config == null) continue; + if (ab.Config.category != Abilities.AbilityCategory.Attack) continue; + if (ab.Config.rangeRadius <= 0f) + Debug.LogError($"[EnemyBase] 攻击招 '{ab.Config.abilityId}' 的 rangeRadius<=0," + + "永远够不着玩家。请在其 EnemyAbilitySO 上配置攻击触发半径。", ab); + candidates.Add(ab); + } + } + _attackSelector = new Abilities.EnemyAttackSelector(candidates); + } + protected virtual void Update() { _stats?.TickAttackCooldown(Time.deltaTime);