feat(enemy): PerceptionRecipeSO 配方资产 + SubclassSelector 模块下拉
未发现层/交战层各用 [SerializeReference]+[SubclassSelector] 下拉挑选模块, 新增模块类自动出现在下拉里,无需改枚举或已有文件。默认交战模块仍为 RushEngagement——未配能力时建图必须显式报错,而非静默产出打不出手的图。
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using BaseGames.AI;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Tests.EditMode.AI
|
||||
{
|
||||
public class PerceptionRecipeSoTests
|
||||
{
|
||||
sealed class Ctx : IAiContext, IEnemyActor
|
||||
{
|
||||
public FakeSensor S = new FakeSensor();
|
||||
public FakeLocomotion L = new FakeLocomotion();
|
||||
public FakeCombat C = new FakeCombat();
|
||||
public FakeVitals V = new FakeVitals();
|
||||
public Blackboard BB = new Blackboard();
|
||||
public ISensor Sensor => S;
|
||||
public IEnemyLocomotion Locomotion => L;
|
||||
public ICombatant Combat => C;
|
||||
public IActorVitals Vitals => V;
|
||||
public Blackboard Blackboard => BB;
|
||||
public bool HasAlertState => true;
|
||||
}
|
||||
|
||||
static PerceptionRecipeSO MakeE001Recipe()
|
||||
{
|
||||
var so = ScriptableObject.CreateInstance<PerceptionRecipeSO>();
|
||||
so.SetModulesForTests(
|
||||
new DisguiseThenPatrol(),
|
||||
new RushEngagement("e001_chase", RushExit.Committed));
|
||||
return so;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Graph_IsCachedAcrossCalls_Flyweight()
|
||||
{
|
||||
var so = MakeE001Recipe();
|
||||
Assert.AreSame(so.GetOrBuildGraph(), so.GetOrBuildGraph());
|
||||
Object.DestroyImmediate(so);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Graph_MatchesEquivalentHandWrittenAssembly()
|
||||
{
|
||||
var so = MakeE001Recipe();
|
||||
var fromRecipe = so.GetOrBuildGraph();
|
||||
|
||||
var b = new BrainBuilder();
|
||||
PerceptionSkeleton.Add(b,
|
||||
new DisguiseThenPatrol(),
|
||||
new RushEngagement("e001_chase", RushExit.Committed));
|
||||
var handWritten = b.Build();
|
||||
|
||||
Assert.AreEqual(handWritten.EntryState, fromRecipe.EntryState);
|
||||
Object.DestroyImmediate(so);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Graph_BehavesLikeE001()
|
||||
{
|
||||
var so = MakeE001Recipe();
|
||||
var ctx = new Ctx();
|
||||
var rt = new AiRuntime(so.GetOrBuildGraph(), ctx);
|
||||
Assert.AreEqual(DisguiseThenPatrol.Disguise, rt.CurrentStateName);
|
||||
ctx.S.Chase = true;
|
||||
rt.Tick(0.1f);
|
||||
Assert.AreEqual(RushEngagement.Rush, rt.CurrentStateName);
|
||||
Object.DestroyImmediate(so);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UnconfiguredRecipe_ThrowsClearError_NotSilentlyInert()
|
||||
{
|
||||
// 新建的空配方默认用 RushEngagement 但尚未配能力。建图时必须**显式报错**,
|
||||
// 而不是静默产出一张"永远不会出手"的图——第 6 条:暴露问题,不掩盖。
|
||||
// 注:策划建完资产到配好模块之间没有任何东西会调 GetOrBuildGraph(),
|
||||
// 它只在 EnemyAiBrain.Start() 运行时被调,所以这里报错不影响编辑期体验。
|
||||
var so = ScriptableObject.CreateInstance<PerceptionRecipeSO>();
|
||||
var ex = Assert.Throws<System.InvalidOperationException>(() => so.GetOrBuildGraph());
|
||||
StringAssert.Contains("RushEngagement", ex.Message);
|
||||
Object.DestroyImmediate(so);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user