Files
zeling_v2/Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs
T
joywayerandClaude Opus 5 3665504a58 feat(ai): 新增 IBossControl facet——Boss 阶段状态经 IAiContext 供图读取
非 Boss 敌人访问 IAiContext.Boss 显式抛 InvalidOperationException,
不返回空对象掩盖小怪图误挂 Boss 边的错误。BossResource 补 IsFull 供图条件边判定。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 12:15:54 +08:00

134 lines
5.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using UnityEngine;
using BaseGames.AI;
namespace BaseGames.Enemies
{
/// <summary>
/// 把 EnemyBase 的子系统包装成 BrainGraph 的能力接口(每敌人一实例)。
/// 状态逻辑只依赖这些接口,不直接引用具体子系统。
/// </summary>
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 为 nullBoss facet 访问时显式抛
}
// ---- IAiContext ----
public ISensor Sensor => this;
public IEnemyLocomotion Locomotion => _enemy.Locomotion;
public ICombatant Combat => this;
public IActorVitals Vitals => this;
public Blackboard Blackboard => _blackboard;
/// <summary>
/// Boss 专属决策面。非 Boss 敌人访问即抛——小怪图误挂了 Boss 边必须立刻暴露,
/// 不返回空对象让错误静默通过(CLAUDE.md 第 6 条)。
/// </summary>
public IBossControl Boss => _boss ?? throw new InvalidOperationException(
$"[EnemyBrainContext] '{_enemy.name}' 不是 Boss(未挂 BossBase 或其子类)," +
"但它的 AI 图访问了 IAiContext.Boss。请检查该敌人是否误用了 Boss 专属的图或图片段。");
/// <summary>每帧由 EnemyAiBrain 在 Tick 之前调用,维护最后已知位置与丢失计时(供需要的敌人用)。</summary>
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;
}
/// <summary>对象池复用时清空临时态。</summary>
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;
}
}