fix: complete actor behavior timelines

This commit is contained in:
ud18010
2026-07-30 13:29:35 +08:00
parent 1380d5107a
commit bc52b74ff8
4 changed files with 221 additions and 2 deletions
@@ -45,6 +45,36 @@ namespace XWorld.Framework.Tests.ActorBehavior
Assert.Contains("missing_wave", ex.Message);
}
[Fact]
public void LoadFromJson_RejectsInvalidFlyObjectMovementAndCollision()
{
string badSpeedFlyObject = TestBehaviorJson.FlyObject.Replace("\"speed\": 6.0", "\"speed\": -6.0");
string badHitLimitFlyObject = TestBehaviorJson.FlyObject.Replace("\"hitLimit\": 1", "\"hitLimit\": -1");
InvalidOperationException speedEx = Assert.Throws<InvalidOperationException>(() =>
BehaviorConfigCatalog.LoadFromJson("fighter", TestBehaviorJson.Skill, badSpeedFlyObject, TestBehaviorJson.Buff));
InvalidOperationException hitLimitEx = Assert.Throws<InvalidOperationException>(() =>
BehaviorConfigCatalog.LoadFromJson("fighter", TestBehaviorJson.Skill, badHitLimitFlyObject, TestBehaviorJson.Buff));
Assert.Contains("speed", speedEx.Message, StringComparison.OrdinalIgnoreCase);
Assert.Contains("hitLimit", hitLimitEx.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void LoadFromJson_RejectsInvalidRingAndLineShapeParameters()
{
InvalidOperationException ringEx = Assert.Throws<InvalidOperationException>(() =>
BehaviorConfigCatalog.LoadFromJson("fighter", TestBehaviorJson.SkillWithInvalidRing, TestBehaviorJson.FlyObject, TestBehaviorJson.Buff));
InvalidOperationException negativeRingEx = Assert.Throws<InvalidOperationException>(() =>
BehaviorConfigCatalog.LoadFromJson("fighter", TestBehaviorJson.SkillWithNegativeRingMinRadius, TestBehaviorJson.FlyObject, TestBehaviorJson.Buff));
InvalidOperationException lineEx = Assert.Throws<InvalidOperationException>(() =>
BehaviorConfigCatalog.LoadFromJson("fighter", TestBehaviorJson.SkillWithInvalidLine, TestBehaviorJson.FlyObject, TestBehaviorJson.Buff));
Assert.Contains("minRadius", ringEx.Message, StringComparison.OrdinalIgnoreCase);
Assert.Contains("minRadius", negativeRingEx.Message, StringComparison.OrdinalIgnoreCase);
Assert.Contains("width", lineEx.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void ExampleConfigFiles_LoadFromRepository()
{
@@ -144,5 +174,80 @@ namespace XWorld.Framework.Tests.ActorBehavior
}]
}]
}";
public const string SkillWithBuffTimeline = @"{
""type"": ""fighter"",
""version"": 1,
""behaviors"": [{
""id"": ""slow_cast"",
""duration"": 0.2,
""canBeInterrupted"": true,
""input"": ""TargetActor"",
""timeline"": [
{ ""time"": 0.0, ""kind"": ""Buff"", ""buffId"": ""slow_01"" }
],
""logicEffects"": []
}]
}";
public const string SkillWithInvalidRing = @"{
""type"": ""fighter"",
""version"": 1,
""behaviors"": [{
""id"": ""bad_ring"",
""duration"": 0.2,
""timeline"": [
{ ""time"": 0.0, ""kind"": ""LogicEffect"", ""effectId"": ""ring_hit"" }
],
""logicEffects"": [{
""id"": ""ring_hit"",
""shape"": ""Ring"",
""radius"": 1.0,
""minRadius"": 2.0,
""target"": ""Enemy"",
""effects"": [{ ""type"": ""Attribute"", ""attribute"": ""hp"", ""value"": -1 }]
}]
}]
}";
public const string SkillWithNegativeRingMinRadius = @"{
""type"": ""fighter"",
""version"": 1,
""behaviors"": [{
""id"": ""bad_negative_ring"",
""duration"": 0.2,
""timeline"": [
{ ""time"": 0.0, ""kind"": ""LogicEffect"", ""effectId"": ""ring_hit"" }
],
""logicEffects"": [{
""id"": ""ring_hit"",
""shape"": ""Ring"",
""radius"": 1.0,
""minRadius"": -0.1,
""target"": ""Enemy"",
""effects"": [{ ""type"": ""Attribute"", ""attribute"": ""hp"", ""value"": -1 }]
}]
}]
}";
public const string SkillWithInvalidLine = @"{
""type"": ""fighter"",
""version"": 1,
""behaviors"": [{
""id"": ""bad_line"",
""duration"": 0.2,
""timeline"": [
{ ""time"": 0.0, ""kind"": ""LogicEffect"", ""effectId"": ""line_hit"" }
],
""logicEffects"": [{
""id"": ""line_hit"",
""shape"": ""Line"",
""radius"": 1.0,
""width"": -0.2,
""target"": ""Enemy"",
""effects"": [{ ""type"": ""Attribute"", ""attribute"": ""hp"", ""value"": -1 }]
}]
}]
}";
}
}
@@ -115,6 +115,50 @@ namespace XWorld.Framework.Tests.ActorBehavior
Assert.Contains(events, e => e.Kind == BehaviorEventKind.BuffRemoved && e.TargetActorId == 2);
Assert.Equal(0, world.ActiveBuffCount(2));
}
[Fact]
public void ClientPresentation_BuffTickDoesNotEmitAuthoritativeAttributeRequests()
{
BehaviorWorld world = TestWorld.CreatePresentationWorld();
Assert.True(world.ApplyBuff(2, "fighter", "slow_01", 1));
world.DrainEvents();
world.Tick(1.0f);
var events = world.DrainEvents();
Assert.Contains(events, e => e.Kind == BehaviorEventKind.BuffTicked && e.TargetActorId == 2);
Assert.DoesNotContain(events, e => e.Kind == BehaviorEventKind.AttributeEffectRequested);
Assert.DoesNotContain(events, e => e.Kind == BehaviorEventKind.LogicEffectApplied);
}
[Fact]
public void BehaviorTimelineBuff_AppliesConfiguredBuff()
{
BehaviorConfigCatalog catalog = BehaviorConfigCatalog.LoadFromJson("fighter", TestBehaviorJson.SkillWithBuffTimeline, TestBehaviorJson.FlyObject, TestBehaviorJson.Buff);
var world = new BehaviorWorld(new BehaviorWorldOptions { Mode = BehaviorRunMode.ServerLogic });
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 });
world.TryStartBehavior(1, "slow_cast", BehaviorInput.FromTargetActor(2));
world.Tick(0.01f);
var events = world.DrainEvents();
Assert.Contains(events, e => e.Kind == BehaviorEventKind.BuffAdded && e.TargetActorId == 2 && e.BuffId == "slow_01");
Assert.Equal(1, world.ActiveBuffCount(2));
}
[Fact]
public void ProjectileTimelineEffect_FiresInClientPresentation()
{
BehaviorWorld world = TestWorld.CreatePresentationWorld();
world.TryStartBehavior(1, "slash", BehaviorInput.FromDirection(1, 0));
world.Tick(0.33f);
var events = world.DrainEvents();
Assert.Contains(events, e => e.Kind == BehaviorEventKind.TimelineEffect && e.FlyObjectId == "blade_wave" && e.Prefab == "Assets/Game/Art/Effect/blade_wave.prefab");
}
}
internal static class TestWorld