using System; using UnityEngine; using BaseGames.AI; namespace BaseGames.Enemies { /// /// 把 EnemyBase 的子系统包装成 BrainGraph 的能力接口(每敌人一实例)。 /// 状态逻辑只依赖这些接口,不直接引用具体子系统。 /// public sealed class EnemyBrainContext : IAiContext, ISensor, ICombatant, IActorVitals, IEnemyActor { readonly EnemyBase _enemy; readonly BossBase _boss; readonly Blackboard _blackboard = new Blackboard(); Vector2 _lastKnown; float _lostTimer; public EnemyBrainContext(EnemyBase enemy) { _enemy = enemy; _boss = enemy as BossBase; // 非 Boss 为 null,Boss facet 访问时显式抛 } // ---- IAiContext ---- public ISensor Sensor => this; public IEnemyLocomotion Locomotion => _enemy.Locomotion; public ICombatant Combat => this; public IActorVitals Vitals => this; public Blackboard Blackboard => _blackboard; /// /// Boss 专属决策面。非 Boss 敌人访问即抛——小怪图误挂了 Boss 边必须立刻暴露, /// 不返回空对象让错误静默通过(CLAUDE.md 第 6 条)。 /// public IBossControl Boss => _boss ?? throw new InvalidOperationException( $"[EnemyBrainContext] '{_enemy.name}' 不是 Boss(未挂 BossBase 或其子类)," + "但它的 AI 图访问了 IAiContext.Boss。请检查该敌人是否误用了 Boss 专属的图或图片段。"); /// 每帧由 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 ---- /// /// 执行指定 id 的能力。解析不到该 id 一律显式报错—— /// 建图期 AiStateFragments.Ability/AbilityOnce 已挡掉空 id, /// 所以运行期解析不到只可能是漏挂能力组件或 SO 上的 abilityId 写错,没有合法情形。 /// 静默返回 false 会让「能力态进入后什么都不做、IsAbilityRunning 恒假、 /// 完成条件立刻成立」表现为"演出被跳过"却零报错(CLAUDE.md 第 6 条)。 /// 用 LogError 而非抛异常:这是每帧路径,抛异常会打断整个 AiRuntime.Tick, /// 让一个漏配的敌人拖垮整场战斗。 /// public bool UseAbility(string abilityId) { var a = _enemy.Abilities?.Get(abilityId); if (a == null) { Debug.LogError($"[EnemyBrainContext] '{_enemy.name}' 的 AI 图请求能力 " + $"'{abilityId}',但该敌人身上没有这个能力组件。" + "请检查能力组件是否挂载、其 EnemyAbilitySO 的 abilityId 是否一致。", _enemy); return false; } return 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; // 折扣系数与 mode 同步每次刷新,使播放模式下调参立即生效(不必重进场景) if (_enemy.StatsSO != null) sel.AntiRepeatFactor = _enemy.StatsSO.attackAntiRepeatFactor; 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; } }