using UnityEngine; using BaseGames.AI; namespace BaseGames.Enemies { /// /// 把 EnemyBase 的子系统包装成 BrainGraph 的能力接口(每敌人一实例)。 /// 状态逻辑只依赖这些接口,不直接引用具体子系统。 /// public sealed class EnemyBrainContext : IAiContext, ISensor, ICombatant, IActorVitals, IEnemyActor { readonly EnemyBase _enemy; readonly Blackboard _blackboard = new Blackboard(); Vector2 _lastKnown; float _lostTimer; public EnemyBrainContext(EnemyBase enemy) { _enemy = enemy; } // ---- IAiContext ---- public ISensor Sensor => this; public IEnemyLocomotion Locomotion => _enemy.Locomotion; public ICombatant Combat => this; public IActorVitals Vitals => this; public Blackboard Blackboard => _blackboard; /// 每帧由 EnemyAiBrain 在 Tick 之前调用,维护最后已知位置与丢失计时(供需要的敌人用)。 public void Refresh(float dt) { bool vision = InVisionZone(); // 追逐区(aggro)或视野内都刷新最后已知位置:Approach 逼近态仅由 aggro 门控进入, // 若只在视野时刷新,aggro 先于视野命中时 Pursue 会拿到 zero/过期点而扑向错误位置。 if ((vision || InChaseZone()) && _enemy.PlayerTransform != null) _lastKnown = _enemy.PlayerTransform.position; // 丢失计时仍以视野为准(LostFor 语义=脱离视野的时长,不受 aggro 影响)。 if (vision) _lostTimer = 0f; else _lostTimer += dt; } /// 对象池复用时清空临时态。 public void ResetScratch() { _blackboard.Clear(); _lastKnown = _enemy.transform.position; _lostTimer = 0f; } // ---- ISensor ----(纯感知查询,转换由 AI 状态机决定) public bool InChaseZone() => _enemy.InChaseZone(); public bool InVisionZone() => _enemy.InVisionZone(); public bool SeesPlayer() => _enemy.IsPlayerVisible(); public bool InRange(float range) => _enemy.IsPlayerInRange(range); public bool LostFor(float seconds) => !InVisionZone() && _lostTimer >= seconds; public Vector2 LastKnown => _lastKnown; // ---- ICombatant ---- public bool UseAbility(string abilityId) { var a = _enemy.Abilities?.Get(abilityId); return a != null && a.Execute(); } public bool IsAbilityRunning(string abilityId = null) { var reg = _enemy.Abilities; if (reg == null) return false; if (abilityId != null) { var a = reg.Get(abilityId); return a != null && a.IsRunning; } var all = reg.All; for (int i = 0; i < all.Count; i++) if (all[i].IsRunning) return true; return false; } public bool NoAbilityRunning => !IsAbilityRunning(); public bool CanUseAbility(string abilityId) { var a = _enemy.Abilities?.Get(abilityId); return a != null && a.CanUse; } 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; public float HpPercent { get { var stats = _enemy.Stats; return stats != null && stats.MaxHP > 0 ? (float)stats.CurrentHP / stats.MaxHP : 0f; } } public bool HpBelow(float ratio) => HpPercent < ratio; // ---- IEnemyActor ---- public bool HasAlertState => _enemy.StatsSO != null && _enemy.StatsSO.HasAlertState; } }