fix: complete actor behavior timelines
This commit is contained in:
@@ -204,7 +204,11 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
foreach (FlyObjectSpec fly in _flyObjects.Values)
|
foreach (FlyObjectSpec fly in _flyObjects.Values)
|
||||||
{
|
{
|
||||||
ValidateDuration(fly.Duration, "flyObject " + fly.Id);
|
ValidateDuration(fly.Duration, "flyObject " + fly.Id);
|
||||||
|
if (fly.Movement.Speed < 0f)
|
||||||
|
throw new InvalidOperationException("Invalid speed for flyObject " + fly.Id);
|
||||||
ValidateDuration(fly.Collision.Radius, "flyObject collision " + fly.Id);
|
ValidateDuration(fly.Collision.Radius, "flyObject collision " + fly.Id);
|
||||||
|
if (fly.Collision.HitLimit < 0)
|
||||||
|
throw new InvalidOperationException("Invalid hitLimit for flyObject " + fly.Id);
|
||||||
ValidateTimeline(fly.Timeline, fly.LogicEffects, "flyObject " + fly.Id);
|
ValidateTimeline(fly.Timeline, fly.LogicEffects, "flyObject " + fly.Id);
|
||||||
ValidateLogicEffects(fly.LogicEffects, "flyObject " + fly.Id);
|
ValidateLogicEffects(fly.LogicEffects, "flyObject " + fly.Id);
|
||||||
}
|
}
|
||||||
@@ -242,8 +246,12 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
AddUnique(ids, effect.Id, "logicEffect");
|
AddUnique(ids, effect.Id, "logicEffect");
|
||||||
if (effect.Shape != LogicShapeKind.Locked && effect.Radius < 0f)
|
if (effect.Shape != LogicShapeKind.Locked && effect.Radius < 0f)
|
||||||
throw new InvalidOperationException("Invalid radius for " + effect.Id + " in " + owner);
|
throw new InvalidOperationException("Invalid radius for " + effect.Id + " in " + owner);
|
||||||
|
if (effect.Shape == LogicShapeKind.Ring && (effect.MinRadius < 0f || effect.MinRadius > effect.Radius))
|
||||||
|
throw new InvalidOperationException("Invalid minRadius for " + effect.Id + " in " + owner);
|
||||||
if (effect.Shape == LogicShapeKind.Sector && (effect.Angle <= 0f || effect.Angle > 360f))
|
if (effect.Shape == LogicShapeKind.Sector && (effect.Angle <= 0f || effect.Angle > 360f))
|
||||||
throw new InvalidOperationException("Invalid sector angle for " + effect.Id + " in " + owner);
|
throw new InvalidOperationException("Invalid sector angle for " + effect.Id + " in " + owner);
|
||||||
|
if (effect.Shape == LogicShapeKind.Line && effect.Width < 0f)
|
||||||
|
throw new InvalidOperationException("Invalid width for " + effect.Id + " in " + owner);
|
||||||
foreach (LogicEffectOpSpec op in effect.Effects)
|
foreach (LogicEffectOpSpec op in effect.Effects)
|
||||||
{
|
{
|
||||||
if (op.Type == LogicEffectOpKind.Buff && !_buffs.ContainsKey(op.BuffId))
|
if (op.Type == LogicEffectOpKind.Buff && !_buffs.ContainsKey(op.BuffId))
|
||||||
|
|||||||
@@ -236,6 +236,13 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entry.Kind == TimelineEntryKind.Buff)
|
||||||
|
{
|
||||||
|
int targetActorId = active.Input.Kind == BehaviorInputKind.TargetActor ? active.Input.TargetActorId : active.ActorId;
|
||||||
|
ApplyBuff(targetActorId, active.Catalog.Type, entry.BuffId, active.ActorId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (entry.Kind == TimelineEntryKind.Projectile)
|
if (entry.Kind == TimelineEntryKind.Projectile)
|
||||||
{
|
{
|
||||||
SpawnProjectile(active.ActorId, active.Catalog, entry.FlyObjectId, active.Input);
|
SpawnProjectile(active.ActorId, active.Catalog, entry.FlyObjectId, active.Input);
|
||||||
@@ -308,8 +315,11 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
TargetActorId = buff.TargetActorId,
|
TargetActorId = buff.TargetActorId,
|
||||||
BuffId = buff.Spec.Id
|
BuffId = buff.Spec.Id
|
||||||
});
|
});
|
||||||
for (int e = 0; e < buff.Spec.LogicEffects.Count; e++)
|
if (_options.Mode == BehaviorRunMode.ServerLogic)
|
||||||
ApplyLogicEffect(buff.TargetActorId, buff.Catalog, buff.Spec.LogicEffects[e], BehaviorInput.FromTargetActor(buff.TargetActorId), buff.TargetActorId);
|
{
|
||||||
|
for (int e = 0; e < buff.Spec.LogicEffects.Count; e++)
|
||||||
|
ApplyLogicEffect(buff.TargetActorId, buff.Catalog, buff.Spec.LogicEffects[e], BehaviorInput.FromTargetActor(buff.TargetActorId), buff.TargetActorId);
|
||||||
|
}
|
||||||
buff.NextTickTime += buff.Spec.TickInterval;
|
buff.NextTickTime += buff.Spec.TickInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,6 +374,7 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
FlyObjectId = spec.Id,
|
FlyObjectId = spec.Id,
|
||||||
Position = projectile.Position
|
Position = projectile.Position
|
||||||
});
|
});
|
||||||
|
FireProjectileTimeline(projectile, -0.00001f, 0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private BehaviorVector3 ResolveProjectileDirection(BehaviorActorState source, BehaviorInput input, int targetActorId)
|
private BehaviorVector3 ResolveProjectileDirection(BehaviorActorState source, BehaviorInput input, int targetActorId)
|
||||||
@@ -382,6 +393,7 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
var expired = new HashSet<ActiveProjectile>();
|
var expired = new HashSet<ActiveProjectile>();
|
||||||
foreach (ActiveProjectile projectile in _projectiles)
|
foreach (ActiveProjectile projectile in _projectiles)
|
||||||
{
|
{
|
||||||
|
float previousAge = projectile.Age;
|
||||||
projectile.Age += deltaTime;
|
projectile.Age += deltaTime;
|
||||||
MoveProjectile(projectile, deltaTime);
|
MoveProjectile(projectile, deltaTime);
|
||||||
_events.Add(new BehaviorEvent
|
_events.Add(new BehaviorEvent
|
||||||
@@ -392,6 +404,7 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
FlyObjectId = projectile.Spec.Id,
|
FlyObjectId = projectile.Spec.Id,
|
||||||
Position = projectile.Position
|
Position = projectile.Position
|
||||||
});
|
});
|
||||||
|
FireProjectileTimeline(projectile, previousAge, projectile.Age);
|
||||||
|
|
||||||
if (_options.Mode == BehaviorRunMode.ServerLogic)
|
if (_options.Mode == BehaviorRunMode.ServerLogic)
|
||||||
CheckProjectileCollision(projectile, expired);
|
CheckProjectileCollision(projectile, expired);
|
||||||
@@ -458,6 +471,54 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void FireProjectileTimeline(ActiveProjectile projectile, float previousElapsed, float currentElapsed)
|
||||||
|
{
|
||||||
|
List<TimelineEntrySpec> timeline = projectile.Spec.Timeline;
|
||||||
|
for (int i = 0; i < timeline.Count; i++)
|
||||||
|
{
|
||||||
|
if (projectile.FiredTimeline.Contains(i)) continue;
|
||||||
|
TimelineEntrySpec entry = timeline[i];
|
||||||
|
if (entry.Trigger != TimelineTriggerKind.Time) continue;
|
||||||
|
if (entry.Time > currentElapsed || entry.Time <= previousElapsed) continue;
|
||||||
|
projectile.FiredTimeline.Add(i);
|
||||||
|
FireProjectileTimelineEntry(projectile, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FireProjectileTimelineEntry(ActiveProjectile projectile, TimelineEntrySpec entry)
|
||||||
|
{
|
||||||
|
if (entry.Kind == TimelineEntryKind.Effect)
|
||||||
|
{
|
||||||
|
if (_options.Mode == BehaviorRunMode.ClientPresentation || _options.Mode == BehaviorRunMode.ClientPredict)
|
||||||
|
{
|
||||||
|
_events.Add(new BehaviorEvent
|
||||||
|
{
|
||||||
|
Kind = BehaviorEventKind.TimelineEffect,
|
||||||
|
ActorId = projectile.SourceActorId,
|
||||||
|
SourceActorId = projectile.SourceActorId,
|
||||||
|
FlyObjectId = projectile.Spec.Id,
|
||||||
|
Prefab = entry.Prefab,
|
||||||
|
Position = projectile.Position
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.Kind == TimelineEntryKind.LogicEffect && _options.Mode == BehaviorRunMode.ServerLogic)
|
||||||
|
{
|
||||||
|
LogicEffectSpec effect = FindLogicEffect(projectile.Spec.LogicEffects, entry.EffectId);
|
||||||
|
if (effect != null)
|
||||||
|
ApplyLogicEffect(projectile.SourceActorId, projectile.Catalog, effect, BehaviorInput.FromTargetActor(projectile.TargetActorId), projectile.TargetActorId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.Kind == TimelineEntryKind.Buff)
|
||||||
|
{
|
||||||
|
int targetActorId = projectile.TargetActorId != 0 ? projectile.TargetActorId : projectile.SourceActorId;
|
||||||
|
ApplyBuff(targetActorId, projectile.Catalog.Type, entry.BuffId, projectile.SourceActorId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void FireProjectileCollisionLogic(ActiveProjectile projectile, int hitActorId)
|
private void FireProjectileCollisionLogic(ActiveProjectile projectile, int hitActorId)
|
||||||
{
|
{
|
||||||
List<TimelineEntrySpec> timeline = projectile.Spec.Timeline;
|
List<TimelineEntrySpec> timeline = projectile.Spec.Timeline;
|
||||||
@@ -575,6 +636,7 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
public float Age;
|
public float Age;
|
||||||
public float VerticalVelocity;
|
public float VerticalVelocity;
|
||||||
public int HitCount;
|
public int HitCount;
|
||||||
|
public readonly HashSet<int> FiredTimeline = new HashSet<int>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class ActiveBuff
|
private sealed class ActiveBuff
|
||||||
|
|||||||
@@ -45,6 +45,36 @@ namespace XWorld.Framework.Tests.ActorBehavior
|
|||||||
Assert.Contains("missing_wave", ex.Message);
|
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]
|
[Fact]
|
||||||
public void ExampleConfigFiles_LoadFromRepository()
|
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.Contains(events, e => e.Kind == BehaviorEventKind.BuffRemoved && e.TargetActorId == 2);
|
||||||
Assert.Equal(0, world.ActiveBuffCount(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
|
internal static class TestWorld
|
||||||
|
|||||||
Reference in New Issue
Block a user