feat(enemy): ICombatant 增加 HasEligibleAttack/UseBestAttack + EnemyBase 构建选择器

This commit is contained in:
2026-07-24 12:20:54 +08:00
parent 04a267b69f
commit 702df35efd
4 changed files with 60 additions and 0 deletions
@@ -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
+6
View File
@@ -8,5 +8,11 @@ namespace BaseGames.AI
bool NoAbilityRunning { get; }
bool CanUseAbility(string abilityId);
void InterruptAbilities(); // 中断当前所有能力(如退出追击时停追击能力)
/// <summary>攻击选择器中是否有"够得着且未冷却"的招(供 Approach→Attack 转换)。</summary>
bool HasEligibleAttack();
/// <summary>按敌人配置的选招模式选一个攻击并触发;返回是否成功触发。</summary>
bool UseBestAttack();
}
}
@@ -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;
+28
View File
@@ -221,6 +221,9 @@ namespace BaseGames.Enemies
/// <summary>能力注册表(架构 §8.3)。Awake 时自动收集所有 EnemyAbilityBase 组件。</summary>
public EnemyAbilityRegistry Abilities => _abilities;
private readonly EnemyAbilityRegistry _abilities = new EnemyAbilityRegistry();
/// <summary>攻击选择器(从已注册能力里筛出 category==Attack 的候选,Awake 时构建)。</summary>
public Abilities.EnemyAttackSelector AttackSelector => _attackSelector;
private Abilities.EnemyAttackSelector _attackSelector;
/// <summary>由 _onPlayerSpawned 事件缓存的玩家 Transform,供 BD 任务读取。</summary>
public Transform PlayerTransform => _playerTransform;
/// <summary>感知 Hub;供 BD 任务及 QuotaManager 暂停/恢复感知使用。</summary>
@@ -537,6 +540,7 @@ namespace BaseGames.Enemies
_pooledObject = GetComponent<PooledObject>();
_brain = GetComponent<EnemyAiBrain>();
_abilities.CollectFrom(gameObject);
BuildAttackSelector();
_colliders = GetComponentsInChildren<Collider2D>(true);
// 收集配置型行为模块(零代码扩展点)
@@ -552,6 +556,30 @@ namespace BaseGames.Enemies
// 订阅在 OnEnable 中处理
}
/// <summary>
/// 从已注册能力里收集 category==Attack 的候选,构建攻击选择器。
/// 根因校验:攻击招 rangeRadius<=0(永远够不着)显式报错,不静默兜底。
/// </summary>
private void BuildAttackSelector()
{
var candidates = new System.Collections.Generic.List<Abilities.IAttackCandidate>();
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);