feat: add actor behavior runtime

This commit is contained in:
ud18010
2026-07-30 13:12:27 +08:00
parent 7ef8427e01
commit 1517d46fc0
2 changed files with 387 additions and 0 deletions
@@ -0,0 +1,86 @@
using Xunit;
using XWorld.Framework.ActorBehavior;
namespace XWorld.Framework.Tests.ActorBehavior
{
public class BehaviorWorldTests
{
[Fact]
public void TryStartBehavior_AllowsOnlyOneActiveBehavior()
{
BehaviorWorld world = TestWorld.CreateServerWorld();
Assert.True(world.TryStartBehavior(1, "slash", BehaviorInput.FromDirection(1, 0)).Success);
BehaviorStartResult second = world.TryStartBehavior(1, "slash", BehaviorInput.FromDirection(1, 0));
Assert.False(second.Success);
Assert.True(world.HasActiveBehavior(1));
}
[Fact]
public void StopBehavior_AllowsInterruptibleBehaviorToSwitch()
{
BehaviorWorld world = TestWorld.CreateServerWorld();
world.TryStartBehavior(1, "slash", BehaviorInput.FromDirection(1, 0));
Assert.True(world.StopBehavior(1, BehaviorStopReason.Stunned));
Assert.True(world.TryStartBehavior(1, "slash", BehaviorInput.FromDirection(1, 0)).Success);
Assert.Contains(world.DrainEvents(), e => e.Kind == BehaviorEventKind.BehaviorStopped);
}
[Fact]
public void Tick_TriggersTimelineEventsAtConfiguredTimes()
{
BehaviorWorld world = TestWorld.CreatePresentationWorld();
world.TryStartBehavior(1, "slash", BehaviorInput.FromDirection(1, 0));
world.Tick(0.26f);
var events = world.DrainEvents();
Assert.Contains(events, e => e.Kind == BehaviorEventKind.TimelineAnimation && e.Animation == "slash_01");
Assert.Contains(events, e => e.Kind == BehaviorEventKind.TimelineEffect && e.Prefab == "Assets/Game/Art/Effect/slash.prefab");
}
[Fact]
public void ServerLogic_SkipsPresentationEventsAndAppliesLogic()
{
BehaviorWorld world = TestWorld.CreateServerWorld();
world.TryStartBehavior(1, "slash", BehaviorInput.FromDirection(1, 0));
world.Tick(0.26f);
var events = world.DrainEvents();
Assert.DoesNotContain(events, e => e.Kind == BehaviorEventKind.TimelineAnimation);
Assert.DoesNotContain(events, e => e.Kind == BehaviorEventKind.TimelineEffect);
Assert.Contains(events, e => e.Kind == BehaviorEventKind.AttributeEffectRequested && e.TargetActorId == 2 && e.Attribute == "hp");
}
}
internal static class TestWorld
{
public static BehaviorWorld CreateServerWorld()
{
return Create(BehaviorRunMode.ServerLogic);
}
public static BehaviorWorld CreatePresentationWorld()
{
return Create(BehaviorRunMode.ClientPresentation);
}
private static BehaviorWorld Create(BehaviorRunMode mode)
{
BehaviorConfigCatalog catalog = BehaviorConfigCatalog.LoadFromJson(
"fighter",
TestBehaviorJson.Skill,
TestBehaviorJson.FlyObject,
TestBehaviorJson.Buff);
var world = new BehaviorWorld(new BehaviorWorldOptions { Mode = mode });
world.RegisterCatalog(catalog);
world.AddActor(new BehaviorActorState { ActorId = 1, CatalogType = "fighter", TeamId = 1, Position = new BehaviorVector3(0, 0, 0), Forward = new BehaviorVector3(1, 0, 0), Radius = 0.3f, Alive = true });
world.AddActor(new BehaviorActorState { ActorId = 2, CatalogType = "fighter", TeamId = 2, Position = new BehaviorVector3(1, 0, 0), Forward = new BehaviorVector3(-1, 0, 0), Radius = 0.3f, Alive = true });
return world;
}
}
}