Files
zeling_v2/Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs
T
joywayerandClaude Opus 4.8 4ce798ea6e refactor(ai): E001 AI 重写为纯决策层,符合设计并委托能力模块
依据小怪设计文档重构 E001 状态机 Idle_Disguise→Skill_Start→Skill_Loop→Skill_End→Move_Patrol:
追击移动/接触伤害全部委托 e001_chase(ContactChaseAbility),AI 不再裸 MoveTo。
新增 ISensor.DetectsPlayer(视线OR近身,对齐设计)、IMover.UseChase/PatrolSpeed、
IEnemyActor.SetPhase(AI 声明阶段、EnemyBase 播动画)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 16:51:47 +08:00

118 lines
4.3 KiB
C#

using UnityEngine;
using BaseGames.AI;
namespace BaseGames.Enemies
{
/// <summary>
/// 把 EnemyBase 的子系统包装成 BrainGraph 的能力接口(每敌人一实例)。
/// 状态逻辑只依赖这些接口,不直接引用具体子系统。
/// </summary>
public sealed class EnemyBrainContext : IAiContext, ISensor, IMover, 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 IMover Mover => this;
public ICombatant Combat => this;
public IActorVitals Vitals => this;
public Blackboard Blackboard => _blackboard;
/// <summary>每帧由 EnemyAiBrain 在 Tick 之前调用,维护最后已知位置与丢失计时。</summary>
public void Refresh(float dt)
{
if (DetectsPlayer() && _enemy.PlayerTransform != null)
{
_lastKnown = _enemy.PlayerTransform.position;
_lostTimer = 0f;
}
else
{
_lostTimer += dt;
}
}
/// <summary>对象池复用时清空临时态。</summary>
public void ResetScratch()
{
_blackboard.Clear();
_lastKnown = _enemy.transform.position;
_lostTimer = 0f;
}
// ---- ISensor ----
public bool SeesPlayer() => _enemy.IsPlayerVisible();
/// <summary>视线 OR 近身 aggro 槽位,任一探到玩家即为真(对齐设计"视线 OR 碰撞触发圆")。</summary>
public bool DetectsPlayer()
{
if (_enemy.IsPlayerVisible()) return true;
var hub = _enemy.SensorHub;
return hub != null && hub.HasAnyDetection(Perception.SensorSlotNames.Aggro);
}
public bool InRange(float range) => _enemy.IsPlayerInRange(range);
public bool LostFor(float seconds) => !DetectsPlayer() && _lostTimer >= seconds;
public Vector2 LastKnown => _lastKnown;
// ---- IMover ----
public void MoveTo(Vector2 target) => _enemy.MoveTo(target);
public void FacePlayer() => _enemy.FacePlayer();
public void Stop() => _enemy.StopMovement();
public void WalkRandom()
{
var nav = _enemy.Nav;
if (nav != null && !nav.IsMoving) nav.WalkToRandom();
}
public void LookAround() => _enemy.BeginLookAround();
public void UseChaseSpeed() => _enemy.Nav?.SetSpeed(_enemy.Stats != null ? _enemy.Stats.RunSpeed : 0f);
public void UsePatrolSpeed() => _enemy.Nav?.SetSpeed(_enemy.Stats != null ? _enemy.Stats.WalkSpeed : 0f);
// ---- 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;
}
// ---- 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 void SetPhase(AiPhase phase) => _enemy.SetAiPhase(phase);
}
}