Files
zeling_v2/Assets/Tests/EditMode/AI/PerceptionRecipeSoTests.cs
T
joywayer ecdb0040cb feat(editor): 敌人 AI 配方脚手架向导;AssignModules 改名并失效缓存
- AiRecipeSO 新增 protected InvalidateGraph(),子类装配模块后调用以丢弃缓存图,
  避免改动要等到下次进 Play 才生效的静默陈旧图问题。
- PerceptionRecipeSO.SetModulesForTests 改名为 AssignModules(向导也要用它装配模块,
  旧名字具有误导性),并在装配后调用 InvalidateGraph。
- 新增 EnemyAiRecipeWizard:按 AssetFolderSpec 定名定路径创建感知型 AI 配方资产
  (Assets/_Game/Data/Enemies/{EnemyID}/ENM_{EnemyID}_Ai.asset),已存在则选中不覆盖。
- PerceptionRecipeSoTests 同步改名调用点,并新增测试钉住"装配后缓存必须失效"契约。
2026-07-29 15:28:25 +08:00

99 lines
3.8 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 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.AssignModules(
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);
}
[Test]
public void AssignModules_InvalidatesCachedGraph()
{
// 不失效的话,装配后拿到的还是旧图——静默的陈旧配置。
var so = MakeE001Recipe();
var first = so.GetOrBuildGraph();
so.AssignModules(new SinglePost(LocomotionMode.Idle),
new RushEngagement("other", RushExit.OnLostTarget));
var second = so.GetOrBuildGraph();
Assert.AreNotSame(first, second);
Assert.AreEqual(SinglePost.Post, second.EntryState); // 新模块生效
Object.DestroyImmediate(so);
}
}
}