Skip to content

Commit b5076db

Browse files
authored
Merge pull request #35 from gamesmiths-guild/task/effect-node-refactor
Added ActiveEffect output and ContextData input support to Effect Nodes.
2 parents d10cf0d + 057868a commit b5076db

39 files changed

Lines changed: 1678 additions & 407 deletions

Forge.Tests/Helpers/StatescriptTestHelpers.cs

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -48,47 +48,21 @@ public static SetVariableNode CreateSetVariableNode(
4848

4949
public static ApplyEffectNode CreateApplyEffectNode(
5050
StringKey effectPropertyName,
51-
StringKey targetPropertyName,
52-
StringKey? levelPropertyName = null,
53-
StringKey? ownershipPropertyName = null)
51+
StringKey targetPropertyName)
5452
{
5553
var node = new ApplyEffectNode();
5654
node.BindInput(ApplyEffectNode.EffectInput, effectPropertyName);
5755
node.BindInput(ApplyEffectNode.TargetInput, targetPropertyName);
58-
59-
if (levelPropertyName.HasValue && levelPropertyName.Value != StringKey.Empty)
60-
{
61-
node.BindInput(ApplyEffectNode.LevelInput, levelPropertyName.Value);
62-
}
63-
64-
if (ownershipPropertyName.HasValue && ownershipPropertyName.Value != StringKey.Empty)
65-
{
66-
node.BindInput(ApplyEffectNode.OwnershipInput, ownershipPropertyName.Value);
67-
}
68-
6956
return node;
7057
}
7158

7259
public static EffectNode CreateEffectNode(
7360
StringKey effectPropertyName,
74-
StringKey targetPropertyName,
75-
StringKey? levelPropertyName = null,
76-
StringKey? ownershipPropertyName = null)
61+
StringKey targetPropertyName)
7762
{
7863
var node = new EffectNode();
7964
node.BindInput(EffectNode.EffectInput, effectPropertyName);
8065
node.BindInput(EffectNode.TargetInput, targetPropertyName);
81-
82-
if (levelPropertyName.HasValue && levelPropertyName.Value != StringKey.Empty)
83-
{
84-
node.BindInput(EffectNode.LevelInput, levelPropertyName.Value);
85-
}
86-
87-
if (ownershipPropertyName.HasValue && ownershipPropertyName.Value != StringKey.Empty)
88-
{
89-
node.BindInput(EffectNode.OwnershipInput, ownershipPropertyName.Value);
90-
}
91-
9266
return node;
9367
}
9468
}

Forge.Tests/Statescript/Nodes/Action/ApplyEffectNodeTests.cs

Lines changed: 199 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public void Apply_effect_node_defaults_level_and_ownership_from_ability_context(
8989

9090
graph.VariableDefinitions.DefineObjectProperty(
9191
"effect",
92-
new EffectDataResolver(CreateTrackingEffectData("Tracked", DurationType.Instant, capture)));
92+
new EffectFromDataResolver(CreateTrackingEffectData("Tracked", DurationType.Instant, capture)));
9393
graph.VariableDefinitions.DefineObjectVariable<IForgeEntity>("entity", target);
9494

9595
ApplyEffectNode node = CreateApplyEffectNode("effect", "entity");
@@ -107,7 +107,7 @@ public void Apply_effect_node_defaults_level_and_ownership_from_ability_context(
107107

108108
[Fact]
109109
[Trait("Graph", "ApplyEffect")]
110-
public void Apply_effect_node_uses_bound_level_and_ownership_when_present()
110+
public void Apply_effect_node_uses_level_and_ownership_configured_on_the_effect_resolver()
111111
{
112112
TestEntity abilityOwner = CreateTestEntity();
113113
TestEntity abilitySource = CreateTestEntity();
@@ -117,20 +117,20 @@ public void Apply_effect_node_uses_bound_level_and_ownership_when_present()
117117
var capture = new AppliedEffectCaptureComponent();
118118
var graph = new Graph();
119119

120-
graph.VariableDefinitions.DefineObjectProperty(
121-
"effect",
122-
new EffectDataResolver(CreateTrackingEffectData("Tracked", DurationType.Instant, capture)));
123-
graph.VariableDefinitions.DefineObjectVariable<IForgeEntity>("entity", target);
124120
graph.VariableDefinitions.DefineVariable("level", 7);
125121
graph.VariableDefinitions.DefineObjectVariable<IForgeEntity>("ownershipOwner", explicitOwner);
126122
graph.VariableDefinitions.DefineObjectVariable<IForgeEntity>("ownershipSource", explicitSource);
127123
graph.VariableDefinitions.DefineObjectProperty(
128-
"ownership",
129-
new OwnershipResolver(
130-
new EntityVariableResolver("ownershipOwner"),
131-
new EntityVariableResolver("ownershipSource")));
124+
"effect",
125+
new EffectFromDataResolver(
126+
CreateTrackingEffectData("Tracked", DurationType.Instant, capture),
127+
new VariableResolver("level", typeof(int)),
128+
new OwnershipResolver(
129+
new EntityVariableResolver("ownershipOwner"),
130+
new EntityVariableResolver("ownershipSource"))));
131+
graph.VariableDefinitions.DefineObjectVariable<IForgeEntity>("entity", target);
132132

133-
ApplyEffectNode node = CreateApplyEffectNode("effect", "entity", "level", "ownership");
133+
ApplyEffectNode node = CreateApplyEffectNode("effect", "entity");
134134
graph.AddNode(node);
135135
graph.AddConnection(new Connection(
136136
graph.EntryNode.OutputPorts[EntryNode.OutputPort],
@@ -143,6 +143,159 @@ public void Apply_effect_node_uses_bound_level_and_ownership_when_present()
143143
capture.LastSource.Should().BeSameAs(explicitSource);
144144
}
145145

146+
[Fact]
147+
[Trait("Graph", "ApplyEffect")]
148+
public void Apply_effect_node_writes_single_active_effect_output_for_non_instant_effect()
149+
{
150+
TestEntity target = CreateTestEntity();
151+
EffectData effectData = CreateFlatEffectData(
152+
"Buff",
153+
"TestAttributeSet.Attribute1",
154+
10,
155+
DurationType.Infinite);
156+
var graph = new Graph();
157+
158+
graph.VariableDefinitions.DefineObjectProperty("effect", new EffectFromDataResolver(effectData));
159+
graph.VariableDefinitions.DefineObjectVariable<IForgeEntity>("entity", target);
160+
graph.VariableDefinitions.DefineObjectVariable<ActiveEffectHandle>("activeEffect");
161+
162+
ApplyEffectNode node = CreateApplyEffectNode("effect", "entity");
163+
node.BindOutput(ApplyEffectNode.ActiveEffectOutput, "activeEffect");
164+
graph.AddNode(node);
165+
graph.AddConnection(new Connection(
166+
graph.EntryNode.OutputPorts[EntryNode.OutputPort],
167+
node.InputPorts[ActionNode.InputPort]));
168+
169+
var processor = new GraphProcessor(graph);
170+
processor.StartGraph();
171+
172+
processor.GraphContext.GraphVariables.TryGetObject("activeEffect", out ActiveEffectHandle? handle)
173+
.Should().BeTrue();
174+
handle.Should().NotBeNull();
175+
handle!.IsValid.Should().BeTrue();
176+
}
177+
178+
[Fact]
179+
[Trait("Graph", "ApplyEffect")]
180+
public void Apply_effect_node_writes_active_effect_array_output_following_input_shape()
181+
{
182+
TestEntity firstTarget = CreateTestEntity();
183+
TestEntity secondTarget = CreateTestEntity();
184+
EffectData effectData = CreateFlatEffectData(
185+
"Buff",
186+
"TestAttributeSet.Attribute1",
187+
10,
188+
DurationType.Infinite);
189+
var graph = new Graph();
190+
191+
graph.VariableDefinitions.DefineObjectProperty("effect", new EffectFromDataResolver(effectData));
192+
graph.VariableDefinitions.DefineObjectArrayVariable<IForgeEntity>("entities", firstTarget, secondTarget);
193+
graph.VariableDefinitions.DefineObjectArrayVariable<ActiveEffectHandle>("activeEffects");
194+
195+
ApplyEffectNode node = CreateApplyEffectNode("effect", "entities");
196+
node.BindOutput(ApplyEffectNode.ActiveEffectOutput, "activeEffects");
197+
graph.AddNode(node);
198+
graph.AddConnection(new Connection(
199+
graph.EntryNode.OutputPorts[EntryNode.OutputPort],
200+
node.InputPorts[ActionNode.InputPort]));
201+
202+
var processor = new GraphProcessor(graph);
203+
processor.StartGraph();
204+
205+
processor.GraphContext.GraphVariables.TryGetObjectArray("activeEffects", out ActiveEffectHandle[]? handles)
206+
.Should().BeTrue();
207+
handles.Should().HaveCount(2);
208+
handles.Should().OnlyContain(handle => handle.IsValid);
209+
}
210+
211+
[Fact]
212+
[Trait("Graph", "ApplyEffect")]
213+
public void Apply_effect_node_writes_null_active_effect_output_for_instant_effect()
214+
{
215+
TestEntity target = CreateTestEntity();
216+
EffectData effectData = CreateFlatEffectData(
217+
"Zap",
218+
"TestAttributeSet.Attribute1",
219+
10,
220+
DurationType.Instant);
221+
var graph = new Graph();
222+
223+
graph.VariableDefinitions.DefineObjectProperty("effect", new EffectFromDataResolver(effectData));
224+
graph.VariableDefinitions.DefineObjectVariable<IForgeEntity>("entity", target);
225+
graph.VariableDefinitions.DefineObjectVariable<ActiveEffectHandle>("activeEffect");
226+
227+
ApplyEffectNode node = CreateApplyEffectNode("effect", "entity");
228+
node.BindOutput(ApplyEffectNode.ActiveEffectOutput, "activeEffect");
229+
graph.AddNode(node);
230+
graph.AddConnection(new Connection(
231+
graph.EntryNode.OutputPorts[EntryNode.OutputPort],
232+
node.InputPorts[ActionNode.InputPort]));
233+
234+
var processor = new GraphProcessor(graph);
235+
processor.StartGraph();
236+
237+
processor.GraphContext.GraphVariables.TryGetObject("activeEffect", out ActiveEffectHandle? handle);
238+
handle.Should().BeNull();
239+
}
240+
241+
[Fact]
242+
[Trait("Graph", "ApplyEffect")]
243+
public void Apply_effect_node_passes_provider_built_context_data_through_the_pipeline()
244+
{
245+
TestEntity target = CreateTestEntity();
246+
var capture = new ContextCaptureComponent();
247+
var graph = new Graph();
248+
249+
graph.VariableDefinitions.DefineVariable("damage", 42);
250+
graph.VariableDefinitions.DefineObjectProperty(
251+
"effect",
252+
new EffectFromDataResolver(CreateTrackingEffectData("Tracked", DurationType.Instant, capture)));
253+
graph.VariableDefinitions.DefineObjectVariable<IForgeEntity>("entity", target);
254+
graph.VariableDefinitions.DefineObjectProperty(
255+
"context",
256+
new EffectContextDataResolver(new DamageContextProvider()));
257+
258+
ApplyEffectNode node = CreateApplyEffectNode("effect", "entity");
259+
node.BindInput(ApplyEffectNode.ContextDataInput, "context");
260+
graph.AddNode(node);
261+
graph.AddConnection(new Connection(
262+
graph.EntryNode.OutputPorts[EntryNode.OutputPort],
263+
node.InputPorts[ActionNode.InputPort]));
264+
265+
var processor = new GraphProcessor(graph);
266+
processor.StartGraph();
267+
268+
capture.WasApplied.Should().BeTrue();
269+
capture.ReceivedContext.Should().BeTrue();
270+
capture.ReceivedDamage.Should().Be(42);
271+
}
272+
273+
[Fact]
274+
[Trait("Graph", "ApplyEffect")]
275+
public void Apply_effect_node_applies_without_context_data_when_input_is_unbound()
276+
{
277+
TestEntity target = CreateTestEntity();
278+
var capture = new ContextCaptureComponent();
279+
var graph = new Graph();
280+
281+
graph.VariableDefinitions.DefineObjectProperty(
282+
"effect",
283+
new EffectFromDataResolver(CreateTrackingEffectData("Tracked", DurationType.Instant, capture)));
284+
graph.VariableDefinitions.DefineObjectVariable<IForgeEntity>("entity", target);
285+
286+
ApplyEffectNode node = CreateApplyEffectNode("effect", "entity");
287+
graph.AddNode(node);
288+
graph.AddConnection(new Connection(
289+
graph.EntryNode.OutputPorts[EntryNode.OutputPort],
290+
node.InputPorts[ActionNode.InputPort]));
291+
292+
var processor = new GraphProcessor(graph);
293+
processor.StartGraph();
294+
295+
capture.WasApplied.Should().BeTrue();
296+
capture.ReceivedContext.Should().BeFalse();
297+
}
298+
146299
private static void ConfigureEffectInput(
147300
Graph graph,
148301
bool useEffectArray,
@@ -153,11 +306,13 @@ private static void ConfigureEffectInput(
153306
{
154307
graph.VariableDefinitions.DefineObjectArrayProperty(
155308
"effect",
156-
new EffectDataArrayResolver(firstEffect, secondEffect));
309+
new EffectArrayFromDataResolver([firstEffect, secondEffect]));
157310
return;
158311
}
159312

160-
graph.VariableDefinitions.DefineObjectProperty("effect", new EffectDataResolver(firstEffect));
313+
graph.VariableDefinitions.DefineObjectProperty(
314+
"effect",
315+
new EffectFromDataResolver(firstEffect));
161316
}
162317

163318
private static void ConfigureEntityInput(
@@ -245,4 +400,35 @@ public void OnEffectApplied(IForgeEntity target, in EffectEvaluatedData effectEv
245400
LastSource = effectEvaluatedData.Effect.Ownership.Source;
246401
}
247402
}
403+
404+
private sealed record DamageContext(int Damage);
405+
406+
private sealed class DamageContextProvider : EffectContextDataProvider<DamageContext>
407+
{
408+
public override DamageContext CreateData(GraphContext graphContext, EffectContextDataInputs inputs)
409+
{
410+
graphContext.TryResolve("damage", out int damage);
411+
return new DamageContext(damage);
412+
}
413+
}
414+
415+
private sealed class ContextCaptureComponent : IEffectComponent
416+
{
417+
public bool WasApplied { get; private set; }
418+
419+
public bool ReceivedContext { get; private set; }
420+
421+
public int ReceivedDamage { get; private set; }
422+
423+
public void OnEffectApplied(IForgeEntity target, in EffectEvaluatedData effectEvaluatedData)
424+
{
425+
WasApplied = true;
426+
427+
if (effectEvaluatedData.TryGetContextData(out DamageContext? context))
428+
{
429+
ReceivedContext = true;
430+
ReceivedDamage = context.Damage;
431+
}
432+
}
433+
}
248434
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright © Gamesmiths Guild.
2+
3+
using FluentAssertions;
4+
using Gamesmiths.Forge.Core;
5+
using Gamesmiths.Forge.Cues;
6+
using Gamesmiths.Forge.Effects;
7+
using Gamesmiths.Forge.Effects.Duration;
8+
using Gamesmiths.Forge.Effects.Magnitudes;
9+
using Gamesmiths.Forge.Effects.Modifiers;
10+
using Gamesmiths.Forge.Statescript;
11+
using Gamesmiths.Forge.Statescript.Nodes;
12+
using Gamesmiths.Forge.Statescript.Nodes.Action;
13+
using Gamesmiths.Forge.Statescript.Properties;
14+
using Gamesmiths.Forge.Tags;
15+
using Gamesmiths.Forge.Tests.Core;
16+
using Gamesmiths.Forge.Tests.Helpers;
17+
18+
using static Gamesmiths.Forge.Tests.Helpers.NodeBindings;
19+
20+
namespace Gamesmiths.Forge.Tests.Statescript.Nodes;
21+
22+
public class EffectInstanceReuseTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture<TagsAndCuesFixture>
23+
{
24+
private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager;
25+
private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager;
26+
27+
[Fact]
28+
[Trait("Graph", "EffectInstanceReuse")]
29+
public void Stored_effect_instance_is_reused_and_updates_target_live_on_level_up()
30+
{
31+
var target = new TestEntity(_tagsManager, _cuesManager);
32+
33+
var effectData = new EffectData(
34+
"Level Scaling",
35+
new DurationData(DurationType.Infinite),
36+
[
37+
new Modifier(
38+
"TestAttributeSet.Attribute1",
39+
ModifierOperation.FlatBonus,
40+
new ModifierMagnitude(
41+
MagnitudeCalculationType.ScalableFloat,
42+
new ScalableFloat(10, new Curve(
43+
[
44+
new CurveKey(1, 1),
45+
new CurveKey(2, 2),
46+
]))))
47+
],
48+
snapshotLevel: false);
49+
50+
var graph = new Graph();
51+
52+
// The EffectFromDataResolver builds a single Effect instance; SetVariableNode stores it in the "effect"
53+
// variable so it can be re-read and reused, and the ApplyEffectNode reads that same instance back through the
54+
// variable.
55+
graph.VariableDefinitions.DefineObjectProperty(
56+
"effectSource",
57+
new EffectFromDataResolver(effectData));
58+
graph.VariableDefinitions.DefineObjectVariable<Effect>("effect");
59+
graph.VariableDefinitions.DefineObjectProperty("effectVar", new EffectVariableResolver("effect"));
60+
graph.VariableDefinitions.DefineObjectVariable<IForgeEntity>("target", target);
61+
62+
SetVariableNode storeEffect = CreateSetVariableNode("effectSource", "effect");
63+
ApplyEffectNode applyEffect = CreateApplyEffectNode("effectVar", "target");
64+
65+
graph.AddNode(storeEffect);
66+
graph.AddNode(applyEffect);
67+
graph.AddConnection(new Connection(
68+
graph.EntryNode.OutputPorts[EntryNode.OutputPort],
69+
storeEffect.InputPorts[ActionNode.InputPort]));
70+
graph.AddConnection(new Connection(
71+
storeEffect.OutputPorts[ActionNode.OutputPort],
72+
applyEffect.InputPorts[ActionNode.InputPort]));
73+
74+
var processor = new GraphProcessor(graph);
75+
processor.StartGraph();
76+
77+
// Applied at level 1 -> flat +10.
78+
TestUtils.TestAttribute(target, "TestAttributeSet.Attribute1", [11, 1, 10, 0]);
79+
80+
processor.GraphContext.GraphVariables.TryGetObject("effect", out Effect? storedEffect).Should().BeTrue();
81+
storedEffect.Should().NotBeNull();
82+
83+
// Mutating the stored instance updates the already-applied, non-snapshot effect live.
84+
storedEffect!.LevelUp();
85+
86+
TestUtils.TestAttribute(target, "TestAttributeSet.Attribute1", [21, 1, 20, 0]);
87+
}
88+
}

0 commit comments

Comments
 (0)