fix: harden actor behavior authority logic
This commit is contained in:
@@ -41,6 +41,10 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
throw new InvalidOperationException("Behavior config type mismatch: expected " + type);
|
throw new InvalidOperationException("Behavior config type mismatch: expected " + type);
|
||||||
|
|
||||||
int version = skillRoot.OptionalInt("version", 1);
|
int version = skillRoot.OptionalInt("version", 1);
|
||||||
|
int flyVersion = flyRoot.OptionalInt("version", 1);
|
||||||
|
int buffVersion = buffRoot.OptionalInt("version", 1);
|
||||||
|
if (flyVersion != version || buffVersion != version)
|
||||||
|
throw new InvalidOperationException("Behavior config version mismatch: expected " + version);
|
||||||
var flyObjects = ParseFlyObjects(flyRoot.OptionalArray("flyObjects"));
|
var flyObjects = ParseFlyObjects(flyRoot.OptionalArray("flyObjects"));
|
||||||
var buffs = ParseBuffs(buffRoot.OptionalArray("buffs"));
|
var buffs = ParseBuffs(buffRoot.OptionalArray("buffs"));
|
||||||
var behaviors = ParseBehaviors(skillRoot.OptionalArray("behaviors"));
|
var behaviors = ParseBehaviors(skillRoot.OptionalArray("behaviors"));
|
||||||
@@ -229,14 +233,33 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
foreach (TimelineEntrySpec entry in timeline)
|
foreach (TimelineEntrySpec entry in timeline)
|
||||||
{
|
{
|
||||||
if (entry.Time < 0f) throw new InvalidOperationException("Negative timeline time in " + owner);
|
if (entry.Time < 0f) throw new InvalidOperationException("Negative timeline time in " + owner);
|
||||||
if (entry.Kind == TimelineEntryKind.LogicEffect && !localEffects.Contains(entry.EffectId))
|
if (entry.Kind == TimelineEntryKind.Animation && string.IsNullOrEmpty(entry.Animation))
|
||||||
|
throw new InvalidOperationException("Missing animation for timeline entry in " + owner);
|
||||||
|
if (entry.Kind == TimelineEntryKind.Effect && string.IsNullOrEmpty(entry.Prefab))
|
||||||
|
throw new InvalidOperationException("Missing prefab for timeline entry in " + owner);
|
||||||
|
if (entry.Kind == TimelineEntryKind.LogicEffect)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(entry.EffectId))
|
||||||
|
throw new InvalidOperationException("Missing effectId for timeline entry in " + owner);
|
||||||
|
if (!localEffects.Contains(entry.EffectId))
|
||||||
throw new InvalidOperationException("Missing logic effect reference " + entry.EffectId + " in " + owner);
|
throw new InvalidOperationException("Missing logic effect reference " + entry.EffectId + " in " + owner);
|
||||||
if (entry.Kind == TimelineEntryKind.Projectile && !_flyObjects.ContainsKey(entry.FlyObjectId))
|
}
|
||||||
|
if (entry.Kind == TimelineEntryKind.Projectile)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(entry.FlyObjectId))
|
||||||
|
throw new InvalidOperationException("Missing flyObjectId for timeline entry in " + owner);
|
||||||
|
if (!_flyObjects.ContainsKey(entry.FlyObjectId))
|
||||||
throw new InvalidOperationException("Missing flyObject reference " + entry.FlyObjectId + " in " + owner);
|
throw new InvalidOperationException("Missing flyObject reference " + entry.FlyObjectId + " in " + owner);
|
||||||
if (entry.Kind == TimelineEntryKind.Buff && !_buffs.ContainsKey(entry.BuffId))
|
}
|
||||||
|
if (entry.Kind == TimelineEntryKind.Buff)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(entry.BuffId))
|
||||||
|
throw new InvalidOperationException("Missing buffId for timeline entry in " + owner);
|
||||||
|
if (!_buffs.ContainsKey(entry.BuffId))
|
||||||
throw new InvalidOperationException("Missing buff reference " + entry.BuffId + " in " + owner);
|
throw new InvalidOperationException("Missing buff reference " + entry.BuffId + " in " + owner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ValidateLogicEffects(List<LogicEffectSpec> effects, string owner)
|
private void ValidateLogicEffects(List<LogicEffectSpec> effects, string owner)
|
||||||
{
|
{
|
||||||
@@ -254,11 +277,23 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
throw new InvalidOperationException("Invalid width for " + effect.Id + " in " + owner);
|
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)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(op.BuffId))
|
||||||
|
throw new InvalidOperationException("Missing buffId in " + owner);
|
||||||
|
if (!_buffs.ContainsKey(op.BuffId))
|
||||||
throw new InvalidOperationException("Missing buff reference " + op.BuffId + " in " + owner);
|
throw new InvalidOperationException("Missing buff reference " + op.BuffId + " in " + owner);
|
||||||
if (op.Type == LogicEffectOpKind.Projectile && !_flyObjects.ContainsKey(op.FlyObjectId))
|
}
|
||||||
|
if (op.Type == LogicEffectOpKind.Projectile)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(op.FlyObjectId))
|
||||||
|
throw new InvalidOperationException("Missing flyObjectId in " + owner);
|
||||||
|
if (!_flyObjects.ContainsKey(op.FlyObjectId))
|
||||||
throw new InvalidOperationException("Missing flyObject reference " + op.FlyObjectId + " in " + owner);
|
throw new InvalidOperationException("Missing flyObject reference " + op.FlyObjectId + " in " + owner);
|
||||||
}
|
}
|
||||||
|
if (op.Type == LogicEffectOpKind.Attribute && string.IsNullOrEmpty(op.Attribute))
|
||||||
|
throw new InvalidOperationException("Missing attribute in " + owner);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -251,13 +251,18 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
|
|
||||||
private void ApplyLogicEffect(int sourceActorId, BehaviorConfigCatalog catalog, LogicEffectSpec effect, BehaviorInput input, int hitActorId)
|
private void ApplyLogicEffect(int sourceActorId, BehaviorConfigCatalog catalog, LogicEffectSpec effect, BehaviorInput input, int hitActorId)
|
||||||
{
|
{
|
||||||
foreach (int targetActorId in SelectTargets(sourceActorId, effect, input, hitActorId))
|
ApplyLogicEffect(sourceActorId, sourceActorId, catalog, effect, input, hitActorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyLogicEffect(int eventSourceActorId, int originActorId, BehaviorConfigCatalog catalog, LogicEffectSpec effect, BehaviorInput input, int hitActorId)
|
||||||
|
{
|
||||||
|
foreach (int targetActorId in SelectTargets(originActorId, effect, input, hitActorId))
|
||||||
{
|
{
|
||||||
_events.Add(new BehaviorEvent
|
_events.Add(new BehaviorEvent
|
||||||
{
|
{
|
||||||
Kind = BehaviorEventKind.LogicEffectApplied,
|
Kind = BehaviorEventKind.LogicEffectApplied,
|
||||||
ActorId = sourceActorId,
|
ActorId = eventSourceActorId,
|
||||||
SourceActorId = sourceActorId,
|
SourceActorId = eventSourceActorId,
|
||||||
TargetActorId = targetActorId,
|
TargetActorId = targetActorId,
|
||||||
LogicEffectId = effect.Id
|
LogicEffectId = effect.Id
|
||||||
});
|
});
|
||||||
@@ -270,8 +275,8 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
_events.Add(new BehaviorEvent
|
_events.Add(new BehaviorEvent
|
||||||
{
|
{
|
||||||
Kind = BehaviorEventKind.AttributeEffectRequested,
|
Kind = BehaviorEventKind.AttributeEffectRequested,
|
||||||
ActorId = sourceActorId,
|
ActorId = eventSourceActorId,
|
||||||
SourceActorId = sourceActorId,
|
SourceActorId = eventSourceActorId,
|
||||||
TargetActorId = targetActorId,
|
TargetActorId = targetActorId,
|
||||||
LogicEffectId = effect.Id,
|
LogicEffectId = effect.Id,
|
||||||
Attribute = op.Attribute,
|
Attribute = op.Attribute,
|
||||||
@@ -280,11 +285,11 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
}
|
}
|
||||||
else if (op.Type == LogicEffectOpKind.Projectile)
|
else if (op.Type == LogicEffectOpKind.Projectile)
|
||||||
{
|
{
|
||||||
SpawnProjectile(sourceActorId, catalog, op.FlyObjectId, input);
|
SpawnProjectile(originActorId, catalog, op.FlyObjectId, input);
|
||||||
}
|
}
|
||||||
else if (op.Type == LogicEffectOpKind.Buff)
|
else if (op.Type == LogicEffectOpKind.Buff)
|
||||||
{
|
{
|
||||||
ApplyBuff(targetActorId, catalog.Type, op.BuffId, sourceActorId);
|
ApplyBuff(targetActorId, catalog.Type, op.BuffId, eventSourceActorId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,7 +323,7 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
if (_options.Mode == BehaviorRunMode.ServerLogic)
|
if (_options.Mode == BehaviorRunMode.ServerLogic)
|
||||||
{
|
{
|
||||||
for (int e = 0; e < buff.Spec.LogicEffects.Count; e++)
|
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);
|
ApplyLogicEffect(buff.SourceActorId, buff.TargetActorId, buff.Catalog, buff.Spec.LogicEffects[e], BehaviorInput.FromTargetActor(buff.TargetActorId), buff.TargetActorId);
|
||||||
}
|
}
|
||||||
buff.NextTickTime += buff.Spec.TickInterval;
|
buff.NextTickTime += buff.Spec.TickInterval;
|
||||||
}
|
}
|
||||||
@@ -448,10 +453,12 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
if (!actor.Alive) continue;
|
if (!actor.Alive) continue;
|
||||||
if (actor.ActorId == projectile.SourceActorId) continue;
|
if (actor.ActorId == projectile.SourceActorId) continue;
|
||||||
if (actor.TeamId == source.TeamId) continue;
|
if (actor.TeamId == source.TeamId) continue;
|
||||||
|
if (projectile.HitActorIds.Contains(actor.ActorId)) continue;
|
||||||
float distance = BehaviorVector3.DistanceXZ(projectile.Position, actor.Position);
|
float distance = BehaviorVector3.DistanceXZ(projectile.Position, actor.Position);
|
||||||
if (distance > projectile.Spec.Collision.Radius + actor.Radius) continue;
|
if (distance > projectile.Spec.Collision.Radius + actor.Radius) continue;
|
||||||
|
|
||||||
projectile.HitCount++;
|
projectile.HitCount++;
|
||||||
|
projectile.HitActorIds.Add(actor.ActorId);
|
||||||
_events.Add(new BehaviorEvent
|
_events.Add(new BehaviorEvent
|
||||||
{
|
{
|
||||||
Kind = BehaviorEventKind.ProjectileHit,
|
Kind = BehaviorEventKind.ProjectileHit,
|
||||||
@@ -549,7 +556,7 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
{
|
{
|
||||||
if (!actor.Alive) continue;
|
if (!actor.Alive) continue;
|
||||||
if (!PassesTargetFilter(source, actor, effect.Target)) continue;
|
if (!PassesTargetFilter(source, actor, effect.Target)) continue;
|
||||||
if (IsInsideShape(source, actor, effect))
|
if (IsInsideShape(source, actor, effect, ResolveEffectDirection(source, input)))
|
||||||
yield return actor.ActorId;
|
yield return actor.ActorId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -565,7 +572,20 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsInsideShape(BehaviorActorState source, BehaviorActorState target, LogicEffectSpec effect)
|
private BehaviorVector3 ResolveEffectDirection(BehaviorActorState source, BehaviorInput input)
|
||||||
|
{
|
||||||
|
if (input.Kind == BehaviorInputKind.Direction)
|
||||||
|
return input.Direction.NormalizedXZ();
|
||||||
|
if (input.Kind == BehaviorInputKind.TargetPosition)
|
||||||
|
return (input.TargetPosition - source.Position).NormalizedXZ();
|
||||||
|
if (input.Kind == BehaviorInputKind.TargetActor
|
||||||
|
&& input.TargetActorId != 0
|
||||||
|
&& _actors.TryGetValue(input.TargetActorId, out BehaviorActorState target))
|
||||||
|
return (target.Position - source.Position).NormalizedXZ();
|
||||||
|
return source.Forward.NormalizedXZ();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsInsideShape(BehaviorActorState source, BehaviorActorState target, LogicEffectSpec effect, BehaviorVector3 direction)
|
||||||
{
|
{
|
||||||
float distance = BehaviorVector3.DistanceXZ(source.Position, target.Position);
|
float distance = BehaviorVector3.DistanceXZ(source.Position, target.Position);
|
||||||
if (effect.Shape == LogicShapeKind.Sphere)
|
if (effect.Shape == LogicShapeKind.Sphere)
|
||||||
@@ -576,7 +596,7 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
{
|
{
|
||||||
if (distance > effect.Radius + target.Radius) return false;
|
if (distance > effect.Radius + target.Radius) return false;
|
||||||
BehaviorVector3 toTarget = (target.Position - source.Position).NormalizedXZ();
|
BehaviorVector3 toTarget = (target.Position - source.Position).NormalizedXZ();
|
||||||
BehaviorVector3 forward = source.Forward.NormalizedXZ();
|
BehaviorVector3 forward = direction.NormalizedXZ();
|
||||||
float dot = Clamp(BehaviorVector3.DotXZ(forward, toTarget), -1f, 1f);
|
float dot = Clamp(BehaviorVector3.DotXZ(forward, toTarget), -1f, 1f);
|
||||||
float angle = (float)(Math.Acos(dot) * 180.0 / Math.PI);
|
float angle = (float)(Math.Acos(dot) * 180.0 / Math.PI);
|
||||||
return angle <= effect.Angle * 0.5f;
|
return angle <= effect.Angle * 0.5f;
|
||||||
@@ -584,7 +604,7 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
if (effect.Shape == LogicShapeKind.Line)
|
if (effect.Shape == LogicShapeKind.Line)
|
||||||
{
|
{
|
||||||
float width = effect.Width > 0f ? effect.Width : target.Radius;
|
float width = effect.Width > 0f ? effect.Width : target.Radius;
|
||||||
float dist = DistancePointToSegmentXZ(target.Position, source.Position, source.Position + source.Forward.NormalizedXZ() * effect.Radius);
|
float dist = DistancePointToSegmentXZ(target.Position, source.Position, source.Position + direction.NormalizedXZ() * effect.Radius);
|
||||||
return dist <= width + target.Radius;
|
return dist <= width + target.Radius;
|
||||||
}
|
}
|
||||||
return target.ActorId == source.ActorId;
|
return target.ActorId == source.ActorId;
|
||||||
@@ -637,6 +657,7 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
public float VerticalVelocity;
|
public float VerticalVelocity;
|
||||||
public int HitCount;
|
public int HitCount;
|
||||||
public readonly HashSet<int> FiredTimeline = new HashSet<int>();
|
public readonly HashSet<int> FiredTimeline = new HashSet<int>();
|
||||||
|
public readonly HashSet<int> HitActorIds = new HashSet<int>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class ActiveBuff
|
private sealed class ActiveBuff
|
||||||
|
|||||||
@@ -34,6 +34,17 @@ namespace XWorld.Framework.Tests.ActorBehavior
|
|||||||
Assert.Contains("type", ex.Message, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("type", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadFromJson_RejectsMismatchedVersion()
|
||||||
|
{
|
||||||
|
string mismatchedFlyObject = TestBehaviorJson.FlyObject.Replace("\"version\": 1", "\"version\": 2");
|
||||||
|
|
||||||
|
InvalidOperationException ex = Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
BehaviorConfigCatalog.LoadFromJson("fighter", TestBehaviorJson.Skill, mismatchedFlyObject, TestBehaviorJson.Buff));
|
||||||
|
|
||||||
|
Assert.Contains("version", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LoadFromJson_RejectsMissingReferences()
|
public void LoadFromJson_RejectsMissingReferences()
|
||||||
{
|
{
|
||||||
@@ -75,6 +86,24 @@ namespace XWorld.Framework.Tests.ActorBehavior
|
|||||||
Assert.Contains("width", lineEx.Message, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("width", lineEx.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadFromJson_RejectsTimelineEntriesMissingRequiredFields()
|
||||||
|
{
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
BehaviorConfigCatalog.LoadFromJson("fighter", TestBehaviorJson.SkillMissingAnimationName, TestBehaviorJson.FlyObject, TestBehaviorJson.Buff));
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
BehaviorConfigCatalog.LoadFromJson("fighter", TestBehaviorJson.SkillMissingEffectPrefab, TestBehaviorJson.FlyObject, TestBehaviorJson.Buff));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadFromJson_RejectsLogicOpsMissingRequiredFields()
|
||||||
|
{
|
||||||
|
InvalidOperationException ex = Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
BehaviorConfigCatalog.LoadFromJson("fighter", TestBehaviorJson.SkillMissingAttributeName, TestBehaviorJson.FlyObject, TestBehaviorJson.Buff));
|
||||||
|
|
||||||
|
Assert.Contains("attribute", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ExampleConfigFiles_LoadFromRepository()
|
public void ExampleConfigFiles_LoadFromRepository()
|
||||||
{
|
{
|
||||||
@@ -230,6 +259,51 @@ namespace XWorld.Framework.Tests.ActorBehavior
|
|||||||
}]
|
}]
|
||||||
}";
|
}";
|
||||||
|
|
||||||
|
public const string SkillMissingAnimationName = @"{
|
||||||
|
""type"": ""fighter"",
|
||||||
|
""version"": 1,
|
||||||
|
""behaviors"": [{
|
||||||
|
""id"": ""bad_animation"",
|
||||||
|
""duration"": 0.2,
|
||||||
|
""timeline"": [
|
||||||
|
{ ""time"": 0.0, ""kind"": ""Animation"" }
|
||||||
|
],
|
||||||
|
""logicEffects"": []
|
||||||
|
}]
|
||||||
|
}";
|
||||||
|
|
||||||
|
public const string SkillMissingEffectPrefab = @"{
|
||||||
|
""type"": ""fighter"",
|
||||||
|
""version"": 1,
|
||||||
|
""behaviors"": [{
|
||||||
|
""id"": ""bad_effect"",
|
||||||
|
""duration"": 0.2,
|
||||||
|
""timeline"": [
|
||||||
|
{ ""time"": 0.0, ""kind"": ""Effect"" }
|
||||||
|
],
|
||||||
|
""logicEffects"": []
|
||||||
|
}]
|
||||||
|
}";
|
||||||
|
|
||||||
|
public const string SkillMissingAttributeName = @"{
|
||||||
|
""type"": ""fighter"",
|
||||||
|
""version"": 1,
|
||||||
|
""behaviors"": [{
|
||||||
|
""id"": ""bad_attribute"",
|
||||||
|
""duration"": 0.2,
|
||||||
|
""timeline"": [
|
||||||
|
{ ""time"": 0.0, ""kind"": ""LogicEffect"", ""effectId"": ""bad_hit"" }
|
||||||
|
],
|
||||||
|
""logicEffects"": [{
|
||||||
|
""id"": ""bad_hit"",
|
||||||
|
""shape"": ""Sphere"",
|
||||||
|
""radius"": 1.0,
|
||||||
|
""target"": ""Enemy"",
|
||||||
|
""effects"": [{ ""type"": ""Attribute"", ""value"": -1 }]
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}";
|
||||||
|
|
||||||
public const string SkillWithInvalidLine = @"{
|
public const string SkillWithInvalidLine = @"{
|
||||||
""type"": ""fighter"",
|
""type"": ""fighter"",
|
||||||
""version"": 1,
|
""version"": 1,
|
||||||
|
|||||||
@@ -32,6 +32,38 @@ namespace XWorld.Framework.Tests.ActorBehavior
|
|||||||
Assert.Contains(world.DrainEvents(), e =>
|
Assert.Contains(world.DrainEvents(), e =>
|
||||||
e.Kind == BehaviorEventKind.AttributeEffectRequested && e.TargetActorId == 3);
|
e.Kind == BehaviorEventKind.AttributeEffectRequested && e.TargetActorId == 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("sector_test")]
|
||||||
|
[InlineData("line_test")]
|
||||||
|
public void DirectionalShapes_UseInputDirectionInsteadOfActorForward(string behaviorId)
|
||||||
|
{
|
||||||
|
BehaviorWorld world = TestTargetWorld.Create();
|
||||||
|
|
||||||
|
world.TryStartBehavior(1, behaviorId, BehaviorInput.FromDirection(0, 1));
|
||||||
|
world.Tick(0.01f);
|
||||||
|
var events = world.DrainEvents();
|
||||||
|
|
||||||
|
Assert.Contains(events, e =>
|
||||||
|
e.Kind == BehaviorEventKind.AttributeEffectRequested && e.TargetActorId == 4);
|
||||||
|
Assert.DoesNotContain(events, e =>
|
||||||
|
e.Kind == BehaviorEventKind.AttributeEffectRequested && e.TargetActorId == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DirectionalShapes_UseTargetPositionDirectionInsteadOfActorForward()
|
||||||
|
{
|
||||||
|
BehaviorWorld world = TestTargetWorld.Create();
|
||||||
|
|
||||||
|
world.TryStartBehavior(1, "sector_test", BehaviorInput.FromTargetPosition(0, 0, 2));
|
||||||
|
world.Tick(0.01f);
|
||||||
|
var events = world.DrainEvents();
|
||||||
|
|
||||||
|
Assert.Contains(events, e =>
|
||||||
|
e.Kind == BehaviorEventKind.AttributeEffectRequested && e.TargetActorId == 4);
|
||||||
|
Assert.DoesNotContain(events, e =>
|
||||||
|
e.Kind == BehaviorEventKind.AttributeEffectRequested && e.TargetActorId == 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class TestTargetWorld
|
internal static class TestTargetWorld
|
||||||
|
|||||||
@@ -116,6 +116,23 @@ namespace XWorld.Framework.Tests.ActorBehavior
|
|||||||
Assert.Equal(0, world.ActiveBuffCount(2));
|
Assert.Equal(0, world.ActiveBuffCount(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuffTickLogic_AttributesEffectsToOriginalSource()
|
||||||
|
{
|
||||||
|
BehaviorWorld world = TestWorld.CreateServerWorld();
|
||||||
|
|
||||||
|
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.AttributeEffectRequested
|
||||||
|
&& e.SourceActorId == 1
|
||||||
|
&& e.TargetActorId == 2
|
||||||
|
&& e.Attribute == "moveSpeed");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ClientPresentation_BuffTickDoesNotEmitAuthoritativeAttributeRequests()
|
public void ClientPresentation_BuffTickDoesNotEmitAuthoritativeAttributeRequests()
|
||||||
{
|
{
|
||||||
@@ -159,6 +176,27 @@ namespace XWorld.Framework.Tests.ActorBehavior
|
|||||||
|
|
||||||
Assert.Contains(events, e => e.Kind == BehaviorEventKind.TimelineEffect && e.FlyObjectId == "blade_wave" && e.Prefab == "Assets/Game/Art/Effect/blade_wave.prefab");
|
Assert.Contains(events, e => e.Kind == BehaviorEventKind.TimelineEffect && e.FlyObjectId == "blade_wave" && e.Prefab == "Assets/Game/Art/Effect/blade_wave.prefab");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Projectile_DoesNotHitSameActorMoreThanOnce()
|
||||||
|
{
|
||||||
|
string fly = TestBehaviorJson.FlyObject.Replace("\"hitLimit\": 1", "\"hitLimit\": 2");
|
||||||
|
BehaviorConfigCatalog catalog = BehaviorConfigCatalog.LoadFromJson("fighter", TestBehaviorJson.Skill, fly, 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, "slash", BehaviorInput.FromDirection(1, 0));
|
||||||
|
world.Tick(0.33f);
|
||||||
|
world.DrainEvents();
|
||||||
|
world.Tick(0.1f);
|
||||||
|
world.Tick(0.1f);
|
||||||
|
var events = world.DrainEvents();
|
||||||
|
|
||||||
|
Assert.Equal(1, events.Count(e => e.Kind == BehaviorEventKind.ProjectileHit && e.TargetActorId == 2));
|
||||||
|
Assert.Equal(1, world.ActiveProjectileCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class TestWorld
|
internal static class TestWorld
|
||||||
|
|||||||
Reference in New Issue
Block a user