diff --git a/Forge.Tests/Statescript/Resolvers/AllResolverTests.cs b/Forge.Tests/Statescript/Resolvers/AllResolverTests.cs new file mode 100644 index 0000000..9768032 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/AllResolverTests.cs @@ -0,0 +1,62 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class AllResolverTests +{ + [Fact] + [Trait("Resolver", "All")] + public void All_resolver_returns_true_when_every_element_matches_the_predicate() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(2)]); + + var resolver = new AllResolver( + new ArrayVariableResolver("numbers", typeof(int)), + new ComparisonResolver( + new ElementValueResolver(typeof(int)), + ComparisonOperation.GreaterThan, + new VariantResolver(new Variant128(1), typeof(int)))); + + resolver.Resolve(context).AsBool().Should().BeTrue(); + } + + [Fact] + [Trait("Resolver", "All")] + public void All_resolver_returns_false_when_any_element_fails_the_predicate() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1)]); + + var resolver = new AllResolver( + new ArrayVariableResolver("numbers", typeof(int)), + new ComparisonResolver( + new ElementValueResolver(typeof(int)), + ComparisonOperation.GreaterThan, + new VariantResolver(new Variant128(1), typeof(int)))); + + resolver.Resolve(context).AsBool().Should().BeFalse(); + } + + [Fact] + [Trait("Resolver", "All")] + public void All_resolver_returns_true_for_empty_arrays() + { + var resolver = new AllResolver( + new ArrayVariableResolver("missing", typeof(int)), + new ComparisonResolver( + new ElementValueResolver(typeof(int)), + ComparisonOperation.GreaterThan, + new VariantResolver(new Variant128(1), typeof(int)))); + + resolver.Resolve(new GraphContext()).AsBool().Should().BeTrue(); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/AnyResolverTests.cs b/Forge.Tests/Statescript/Resolvers/AnyResolverTests.cs new file mode 100644 index 0000000..8eec6e6 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/AnyResolverTests.cs @@ -0,0 +1,77 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class AnyResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "Any")] + public void Any_resolver_returns_true_for_non_empty_arrays_without_a_predicate() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable("numbers", [new Variant128(3)]); + + var resolver = new AnyResolver(new ArrayVariableResolver("numbers", typeof(int))); + + resolver.Resolve(context).AsBool().Should().BeTrue(); + } + + [Fact] + [Trait("Resolver", "Any")] + public void Any_resolver_returns_false_for_empty_arrays() + { + var resolver = new AnyResolver(new ArrayVariableResolver("missing", typeof(int))); + + resolver.Resolve(new GraphContext()).AsBool().Should().BeFalse(); + } + + [Fact] + [Trait("Resolver", "Any")] + public void Any_resolver_tests_entities_against_the_predicate() + { + var entity1 = new VitalTestEntity(_tagsManager, _cuesManager); + var entity2 = new VitalTestEntity(_tagsManager, _cuesManager); + entity1.VitalAttributeSet.UpdateBaseHealth(10); + entity2.VitalAttributeSet.UpdateBaseHealth(70); + + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2]); + + var resolver = new AnyResolver( + new EntityArrayVariableResolver("targets"), + new ComparisonResolver( + new AttributeResolver("VitalAttributeSet.CurrentHealth", new ElementEntityResolver()), + ComparisonOperation.GreaterThan, + new VariantResolver(new Variant128(50), typeof(int)))); + + resolver.Resolve(context).AsBool().Should().BeTrue(); + } + + [Fact] + [Trait("Resolver", "Any")] + public void Any_resolver_returns_false_when_no_element_matches_the_predicate() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable("numbers", [new Variant128(1), new Variant128(2)]); + + var resolver = new AnyResolver( + new ArrayVariableResolver("numbers", typeof(int)), + new ComparisonResolver( + new ElementValueResolver(typeof(int)), + ComparisonOperation.GreaterThan, + new VariantResolver(new Variant128(5), typeof(int)))); + + resolver.Resolve(context).AsBool().Should().BeFalse(); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/AppendResolverTests.cs b/Forge.Tests/Statescript/Resolvers/AppendResolverTests.cs new file mode 100644 index 0000000..bd04b86 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/AppendResolverTests.cs @@ -0,0 +1,53 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class AppendResolverTests +{ + [Fact] + [Trait("Resolver", "Append")] + public void Append_resolver_adds_elements_to_the_end() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable("numbers", [new Variant128(3)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new AppendResolver( + source, + new VariantResolver(new Variant128(1), typeof(int)), + new VariantResolver(new Variant128(2), typeof(int))); + + Variant128[] result = resolver.ResolveArray(context); + + result.Should().HaveCount(3); + result[0].AsInt().Should().Be(3); + result[1].AsInt().Should().Be(1); + result[2].AsInt().Should().Be(2); + } + + [Fact] + [Trait("Resolver", "Append")] + public void Append_resolver_rejects_element_resolvers_of_a_different_type() + { + var source = new ArrayVariableResolver("numbers", typeof(int)); + + Action act = () => _ = new AppendResolver(source, new VariantResolver(new Variant128(1f), typeof(float))); + + act.Should().Throw(); + } + + [Fact] + [Trait("Resolver", "Append")] + public void Append_resolver_rejects_null_element_resolvers() + { + var source = new ArrayVariableResolver("numbers", typeof(int)); + + Action act = () => _ = new AppendResolver(source, (IPropertyResolver)null!); + + act.Should().Throw(); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ArrayPipelineIntegrationTests.cs b/Forge.Tests/Statescript/Resolvers/ArrayPipelineIntegrationTests.cs new file mode 100644 index 0000000..1dee207 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ArrayPipelineIntegrationTests.cs @@ -0,0 +1,96 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +/// +/// End-to-end coverage for the motivating array pipeline: collect entities, sort them by a per-element key, and keep +/// the closest three. The per-element key is authored purely from composable resolvers +/// ( feeding an ). +/// +/// The fixture providing tags and cues managers. +public class ArrayPipelineIntegrationTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "ArrayPipeline")] + public void Order_by_take_pipeline_selects_the_three_closest_entities() + { + // "Distance to the owner" is modeled as an attribute so the whole pipeline runs on core resolvers. + VitalTestEntity[] entities = + [ + CreateEntityWithDistance(50), + CreateEntityWithDistance(10), + CreateEntityWithDistance(40), + CreateEntityWithDistance(20), + CreateEntityWithDistance(30), + ]; + + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable( + "nearbyEntities", + [entities[0], entities[1], entities[2], entities[3], entities[4]]); + + var threeClosest = new ObjectTakeResolver( + new ObjectOrderByResolver( + new EntityArrayVariableResolver("nearbyEntities"), + new AttributeResolver("VitalAttributeSet.CurrentHealth", new ElementEntityResolver())), + new VariantResolver(new Variant128(3), typeof(int))); + + IForgeEntity[] result = threeClosest.ResolveArray(context); + + result.Should().Equal(entities[1], entities[3], entities[4]); + } + + [Fact] + [Trait("Resolver", "ArrayPipeline")] + public void Where_order_by_take_pipeline_composes_filtering_and_sorting() + { + VitalTestEntity[] entities = + [ + CreateEntityWithDistance(50), + CreateEntityWithDistance(10), + CreateEntityWithDistance(40), + CreateEntityWithDistance(20), + ]; + + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable( + "nearbyEntities", + [entities[0], entities[1], entities[2], entities[3]]); + + var distanceKey = new AttributeResolver("VitalAttributeSet.CurrentHealth", new ElementEntityResolver()); + + // Keep entities farther than 15, then take the two closest of those. + var pipeline = new ObjectTakeResolver( + new ObjectOrderByResolver( + new ObjectWhereResolver( + new EntityArrayVariableResolver("nearbyEntities"), + new ComparisonResolver( + distanceKey, + ComparisonOperation.GreaterThan, + new VariantResolver(new Variant128(15), typeof(int)))), + distanceKey), + new VariantResolver(new Variant128(2), typeof(int))); + + IForgeEntity[] result = pipeline.ResolveArray(context); + + result.Should().Equal(entities[3], entities[2]); + } + + private VitalTestEntity CreateEntityWithDistance(int distance) + { + var entity = new VitalTestEntity(_tagsManager, _cuesManager); + entity.VitalAttributeSet.UpdateBaseHealth(distance); + return entity; + } +} diff --git a/Forge.Tests/Statescript/Resolvers/AverageResolverTests.cs b/Forge.Tests/Statescript/Resolvers/AverageResolverTests.cs new file mode 100644 index 0000000..a2d7dcd --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/AverageResolverTests.cs @@ -0,0 +1,49 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class AverageResolverTests +{ + [Fact] + [Trait("Resolver", "Average")] + public void Average_resolver_averages_int_elements_as_double() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + + var resolver = new AverageResolver(new ArrayVariableResolver("numbers", typeof(int))); + + resolver.ValueType.Should().Be(typeof(double)); + resolver.Resolve(context).AsDouble().Should().Be(2d); + } + + [Fact] + [Trait("Resolver", "Average")] + public void Average_resolver_averages_float_elements_as_float() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(1f), new Variant128(2f)]); + + var resolver = new AverageResolver(new ArrayVariableResolver("numbers", typeof(float))); + + resolver.ValueType.Should().Be(typeof(float)); + resolver.Resolve(context).AsFloat().Should().Be(1.5f); + } + + [Fact] + [Trait("Resolver", "Average")] + public void Average_resolver_returns_zero_for_empty_arrays() + { + var resolver = new AverageResolver(new ArrayVariableResolver("missing", typeof(int))); + + resolver.Resolve(new GraphContext()).AsDouble().Should().Be(0d); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ConcatResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ConcatResolverTests.cs new file mode 100644 index 0000000..30c798b --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ConcatResolverTests.cs @@ -0,0 +1,41 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ConcatResolverTests +{ + [Fact] + [Trait("Resolver", "Concat")] + public void Concat_resolver_appends_the_second_array_after_the_first() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable("first", [new Variant128(3), new Variant128(1)]); + context.GraphVariables.DefineArrayVariable("second", [new Variant128(2)]); + + var resolver = new ConcatResolver( + new ArrayVariableResolver("first", typeof(int)), + new ArrayVariableResolver("second", typeof(int))); + + Variant128[] result = resolver.ResolveArray(context); + + result.Should().HaveCount(3); + result[0].AsInt().Should().Be(3); + result[1].AsInt().Should().Be(1); + result[2].AsInt().Should().Be(2); + } + + [Fact] + [Trait("Resolver", "Concat")] + public void Concat_resolver_rejects_mismatched_element_types() + { + Action act = () => _ = new ConcatResolver( + new ArrayVariableResolver("first", typeof(int)), + new ArrayVariableResolver("second", typeof(float))); + + act.Should().Throw(); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ContainsResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ContainsResolverTests.cs new file mode 100644 index 0000000..c90540f --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ContainsResolverTests.cs @@ -0,0 +1,51 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ContainsResolverTests +{ + [Fact] + [Trait("Resolver", "Contains")] + public void Contains_resolver_returns_true_when_the_value_is_present() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + + var resolver = new ContainsResolver( + new ArrayVariableResolver("numbers", typeof(int)), + new VariantResolver(new Variant128(1), typeof(int))); + + resolver.Resolve(context).AsBool().Should().BeTrue(); + } + + [Fact] + [Trait("Resolver", "Contains")] + public void Contains_resolver_returns_false_when_the_value_is_absent() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable("numbers", [new Variant128(3)]); + + var resolver = new ContainsResolver( + new ArrayVariableResolver("numbers", typeof(int)), + new VariantResolver(new Variant128(9), typeof(int))); + + resolver.Resolve(context).AsBool().Should().BeFalse(); + } + + [Fact] + [Trait("Resolver", "Contains")] + public void Contains_resolver_rejects_value_resolvers_of_a_different_type() + { + var source = new ArrayVariableResolver("numbers", typeof(int)); + + Action act = () => _ = new ContainsResolver(source, new VariantResolver(new Variant128(1f), typeof(float))); + + act.Should().Throw(); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/CountResolverTests.cs b/Forge.Tests/Statescript/Resolvers/CountResolverTests.cs new file mode 100644 index 0000000..284b188 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/CountResolverTests.cs @@ -0,0 +1,64 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class CountResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "Count")] + public void Count_resolver_counts_all_elements_without_a_predicate() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + + var resolver = new CountResolver(new ArrayVariableResolver("numbers", typeof(int))); + + resolver.Resolve(context).AsInt().Should().Be(3); + } + + [Fact] + [Trait("Resolver", "Count")] + public void Count_resolver_counts_only_elements_matching_the_predicate() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + + var resolver = new CountResolver( + new ArrayVariableResolver("numbers", typeof(int)), + new ComparisonResolver( + new ElementValueResolver(typeof(int)), + ComparisonOperation.GreaterThan, + new VariantResolver(new Variant128(1), typeof(int)))); + + resolver.Resolve(context).AsInt().Should().Be(2); + } + + [Fact] + [Trait("Resolver", "Count")] + public void Count_resolver_counts_object_array_elements() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var entity2 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2]); + + var resolver = new CountResolver(new EntityArrayVariableResolver("targets")); + + resolver.Resolve(context).AsInt().Should().Be(2); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/DistinctResolverTests.cs b/Forge.Tests/Statescript/Resolvers/DistinctResolverTests.cs new file mode 100644 index 0000000..70897e1 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/DistinctResolverTests.cs @@ -0,0 +1,39 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class DistinctResolverTests +{ + [Fact] + [Trait("Resolver", "Distinct")] + public void Distinct_resolver_keeps_the_first_occurrence_of_each_value() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(3), new Variant128(2), new Variant128(1)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new DistinctResolver(source); + + Variant128[] result = resolver.ResolveArray(context); + + result.Should().HaveCount(3); + result[0].AsInt().Should().Be(3); + result[1].AsInt().Should().Be(1); + result[2].AsInt().Should().Be(2); + } + + [Fact] + [Trait("Resolver", "Distinct")] + public void Distinct_resolver_returns_empty_array_for_missing_variable() + { + var resolver = new DistinctResolver(new ArrayVariableResolver("missing", typeof(int))); + + resolver.ResolveArray(new GraphContext()).Should().BeEmpty(); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ElementAtResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ElementAtResolverTests.cs new file mode 100644 index 0000000..88602f8 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ElementAtResolverTests.cs @@ -0,0 +1,50 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ElementAtResolverTests +{ + [Fact] + [Trait("Resolver", "ElementAt")] + public void Element_at_resolver_reads_the_element_at_the_resolved_index() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new ElementAtResolver(source, new VariantResolver(new Variant128(1), typeof(int))); + + resolver.ValueType.Should().Be(typeof(int)); + resolver.Resolve(context).AsInt().Should().Be(1); + } + + [Fact] + [Trait("Resolver", "ElementAt")] + public void Element_at_resolver_returns_default_for_out_of_range_index() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable("numbers", [new Variant128(3)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new ElementAtResolver(source, new VariantResolver(new Variant128(5), typeof(int))); + + resolver.Resolve(context).AsInt().Should().Be(0); + } + + [Fact] + [Trait("Resolver", "ElementAt")] + public void Element_at_resolver_rejects_non_numeric_index_resolvers() + { + var source = new ArrayVariableResolver("numbers", typeof(int)); + + Action act = () => _ = new ElementAtResolver(source, new VariantResolver(new Variant128(true), typeof(bool))); + + act.Should().Throw(); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ElementEntityResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ElementEntityResolverTests.cs new file mode 100644 index 0000000..cb90a62 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ElementEntityResolverTests.cs @@ -0,0 +1,49 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ElementEntityResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "ElementEntity")] + public void Element_entity_resolver_returns_null_outside_array_iteration() + { + var resolver = new ElementEntityResolver(); + + resolver.Resolve(new GraphContext()).Should().BeNull(); + } + + [Fact] + [Trait("Resolver", "ElementEntity")] + public void Element_entity_resolver_composes_with_attribute_resolver_per_element() + { + var entity1 = new VitalTestEntity(_tagsManager, _cuesManager); + var entity2 = new VitalTestEntity(_tagsManager, _cuesManager); + entity1.VitalAttributeSet.UpdateBaseHealth(40); + entity2.VitalAttributeSet.UpdateBaseHealth(70); + + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2]); + var source = new ObjectArrayVariableResolver("targets"); + + var resolver = new SelectResolver( + source, + new AttributeResolver("VitalAttributeSet.CurrentHealth", new ElementEntityResolver())); + + Variant128[] result = resolver.ResolveArray(context); + + result.Should().HaveCount(2); + result[0].AsInt().Should().Be(40); + result[1].AsInt().Should().Be(70); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ElementIndexResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ElementIndexResolverTests.cs new file mode 100644 index 0000000..f33de09 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ElementIndexResolverTests.cs @@ -0,0 +1,39 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ElementIndexResolverTests +{ + [Fact] + [Trait("Resolver", "ElementIndex")] + public void Element_index_resolver_returns_default_outside_array_iteration() + { + var resolver = new ElementIndexResolver(); + + resolver.Resolve(new GraphContext()).AsInt().Should().Be(0); + } + + [Fact] + [Trait("Resolver", "ElementIndex")] + public void Element_index_resolver_reads_each_iterated_element_index() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(9), new Variant128(9), new Variant128(9)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new SelectResolver(source, new ElementIndexResolver()); + + Variant128[] result = resolver.ResolveArray(context); + + result.Should().HaveCount(3); + result[0].AsInt().Should().Be(0); + result[1].AsInt().Should().Be(1); + result[2].AsInt().Should().Be(2); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ElementResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ElementResolverTests.cs new file mode 100644 index 0000000..88859fa --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ElementResolverTests.cs @@ -0,0 +1,45 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ElementResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "Element")] + public void Element_resolver_returns_null_outside_array_iteration() + { + var resolver = new ElementResolver(); + + resolver.Resolve(new GraphContext()).Should().BeNull(); + } + + [Fact] + [Trait("Resolver", "Element")] + public void Element_resolver_reads_each_iterated_element() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var entity2 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, null!, entity2]); + var source = new EntityArrayVariableResolver("targets"); + + var resolver = new ObjectWhereResolver( + source, + new IsValidResolver(new ElementResolver())); + + IForgeEntity[] result = resolver.ResolveArray(context); + + result.Should().Equal(entity1, entity2); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ElementValueResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ElementValueResolverTests.cs new file mode 100644 index 0000000..1d74ce6 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ElementValueResolverTests.cs @@ -0,0 +1,46 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ElementValueResolverTests +{ + [Fact] + [Trait("Resolver", "ElementValue")] + public void Element_value_resolver_returns_default_outside_array_iteration() + { + var resolver = new ElementValueResolver(typeof(int)); + + resolver.Resolve(new GraphContext()).AsInt().Should().Be(0); + } + + [Fact] + [Trait("Resolver", "ElementValue")] + public void Element_value_resolver_reports_configured_value_type() + { + new ElementValueResolver(typeof(float)).ValueType.Should().Be(typeof(float)); + } + + [Fact] + [Trait("Resolver", "ElementValue")] + public void Element_value_resolver_reads_each_iterated_element() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new SelectResolver(source, new ElementValueResolver(typeof(int))); + + Variant128[] result = resolver.ResolveArray(context); + + result.Should().HaveCount(3); + result[0].AsInt().Should().Be(3); + result[1].AsInt().Should().Be(1); + result[2].AsInt().Should().Be(2); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/EntityElementAtResolverTests.cs b/Forge.Tests/Statescript/Resolvers/EntityElementAtResolverTests.cs new file mode 100644 index 0000000..aac238b --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/EntityElementAtResolverTests.cs @@ -0,0 +1,38 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class EntityElementAtResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "EntityElementAt")] + public void Entity_element_at_resolver_composes_with_attribute_resolver() + { + var entity1 = new VitalTestEntity(_tagsManager, _cuesManager); + var entity2 = new VitalTestEntity(_tagsManager, _cuesManager); + entity1.VitalAttributeSet.UpdateBaseHealth(40); + entity2.VitalAttributeSet.UpdateBaseHealth(70); + + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2]); + + var resolver = new AttributeResolver( + "VitalAttributeSet.CurrentHealth", + new EntityElementAtResolver( + new EntityArrayVariableResolver("targets"), + new VariantResolver(new Variant128(1), typeof(int)))); + + resolver.Resolve(context).AsInt().Should().Be(70); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/EntityFirstResolverTests.cs b/Forge.Tests/Statescript/Resolvers/EntityFirstResolverTests.cs new file mode 100644 index 0000000..2f2457d --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/EntityFirstResolverTests.cs @@ -0,0 +1,36 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class EntityFirstResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "EntityFirst")] + public void Entity_first_resolver_composes_with_attribute_resolver() + { + var entity1 = new VitalTestEntity(_tagsManager, _cuesManager); + var entity2 = new VitalTestEntity(_tagsManager, _cuesManager); + entity1.VitalAttributeSet.UpdateBaseHealth(40); + entity2.VitalAttributeSet.UpdateBaseHealth(70); + + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2]); + + var resolver = new AttributeResolver( + "VitalAttributeSet.CurrentHealth", + new EntityFirstResolver(new EntityArrayVariableResolver("targets"))); + + resolver.Resolve(context).AsInt().Should().Be(40); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/EntityLastResolverTests.cs b/Forge.Tests/Statescript/Resolvers/EntityLastResolverTests.cs new file mode 100644 index 0000000..1971b16 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/EntityLastResolverTests.cs @@ -0,0 +1,36 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class EntityLastResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "EntityLast")] + public void Entity_last_resolver_composes_with_attribute_resolver() + { + var entity1 = new VitalTestEntity(_tagsManager, _cuesManager); + var entity2 = new VitalTestEntity(_tagsManager, _cuesManager); + entity1.VitalAttributeSet.UpdateBaseHealth(40); + entity2.VitalAttributeSet.UpdateBaseHealth(70); + + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2]); + + var resolver = new AttributeResolver( + "VitalAttributeSet.CurrentHealth", + new EntityLastResolver(new EntityArrayVariableResolver("targets"))); + + resolver.Resolve(context).AsInt().Should().Be(70); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ExceptResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ExceptResolverTests.cs new file mode 100644 index 0000000..343219f --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ExceptResolverTests.cs @@ -0,0 +1,45 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ExceptResolverTests +{ + [Fact] + [Trait("Resolver", "Except")] + public void Except_resolver_removes_elements_found_in_the_other_array() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2), new Variant128(1)]); + context.GraphVariables.DefineArrayVariable("excluded", [new Variant128(1)]); + + var resolver = new ExceptResolver( + new ArrayVariableResolver("numbers", typeof(int)), + new ArrayVariableResolver("excluded", typeof(int))); + + Variant128[] result = resolver.ResolveArray(context); + + result.Should().HaveCount(2); + result[0].AsInt().Should().Be(3); + result[1].AsInt().Should().Be(2); + } + + [Fact] + [Trait("Resolver", "Except")] + public void Except_resolver_keeps_the_array_unchanged_when_the_other_is_empty() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable("numbers", [new Variant128(3), new Variant128(1)]); + + var resolver = new ExceptResolver( + new ArrayVariableResolver("numbers", typeof(int)), + new ArrayVariableResolver("missing", typeof(int))); + + resolver.ResolveArray(context).Should().HaveCount(2); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/FirstResolverTests.cs b/Forge.Tests/Statescript/Resolvers/FirstResolverTests.cs new file mode 100644 index 0000000..4580d9d --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/FirstResolverTests.cs @@ -0,0 +1,35 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class FirstResolverTests +{ + [Fact] + [Trait("Resolver", "First")] + public void First_resolver_reads_the_first_element() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new FirstResolver(source); + + resolver.ValueType.Should().Be(typeof(int)); + resolver.Resolve(context).AsInt().Should().Be(3); + } + + [Fact] + [Trait("Resolver", "First")] + public void First_resolver_returns_default_for_empty_array() + { + var resolver = new FirstResolver(new ArrayVariableResolver("missing", typeof(int))); + + resolver.Resolve(new GraphContext()).AsInt().Should().Be(0); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/IndexOfResolverTests.cs b/Forge.Tests/Statescript/Resolvers/IndexOfResolverTests.cs new file mode 100644 index 0000000..bf046ef --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/IndexOfResolverTests.cs @@ -0,0 +1,40 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class IndexOfResolverTests +{ + [Fact] + [Trait("Resolver", "IndexOf")] + public void Index_of_resolver_returns_the_index_of_the_first_occurrence() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(1)]); + + var resolver = new IndexOfResolver( + new ArrayVariableResolver("numbers", typeof(int)), + new VariantResolver(new Variant128(1), typeof(int))); + + resolver.Resolve(context).AsInt().Should().Be(1); + } + + [Fact] + [Trait("Resolver", "IndexOf")] + public void Index_of_resolver_returns_minus_one_when_the_value_is_absent() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable("numbers", [new Variant128(3)]); + + var resolver = new IndexOfResolver( + new ArrayVariableResolver("numbers", typeof(int)), + new VariantResolver(new Variant128(9), typeof(int))); + + resolver.Resolve(context).AsInt().Should().Be(-1); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/LastResolverTests.cs b/Forge.Tests/Statescript/Resolvers/LastResolverTests.cs new file mode 100644 index 0000000..502ab1c --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/LastResolverTests.cs @@ -0,0 +1,35 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class LastResolverTests +{ + [Fact] + [Trait("Resolver", "Last")] + public void Last_resolver_reads_the_last_element() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new LastResolver(source); + + resolver.ValueType.Should().Be(typeof(int)); + resolver.Resolve(context).AsInt().Should().Be(2); + } + + [Fact] + [Trait("Resolver", "Last")] + public void Last_resolver_returns_default_for_empty_array() + { + var resolver = new LastResolver(new ArrayVariableResolver("missing", typeof(int))); + + resolver.Resolve(new GraphContext()).AsInt().Should().Be(0); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/MaxElementResolverTests.cs b/Forge.Tests/Statescript/Resolvers/MaxElementResolverTests.cs new file mode 100644 index 0000000..fd30c03 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/MaxElementResolverTests.cs @@ -0,0 +1,34 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class MaxElementResolverTests +{ + [Fact] + [Trait("Resolver", "MaxElement")] + public void Max_element_resolver_returns_the_largest_element() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + + var resolver = new MaxElementResolver(new ArrayVariableResolver("numbers", typeof(int))); + + resolver.ValueType.Should().Be(typeof(int)); + resolver.Resolve(context).AsInt().Should().Be(3); + } + + [Fact] + [Trait("Resolver", "MaxElement")] + public void Max_element_resolver_returns_default_for_empty_arrays() + { + var resolver = new MaxElementResolver(new ArrayVariableResolver("missing", typeof(int))); + + resolver.Resolve(new GraphContext()).AsInt().Should().Be(0); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/MinElementResolverTests.cs b/Forge.Tests/Statescript/Resolvers/MinElementResolverTests.cs new file mode 100644 index 0000000..07ce924 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/MinElementResolverTests.cs @@ -0,0 +1,43 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class MinElementResolverTests +{ + [Fact] + [Trait("Resolver", "MinElement")] + public void Min_element_resolver_returns_the_smallest_element() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + + var resolver = new MinElementResolver(new ArrayVariableResolver("numbers", typeof(int))); + + resolver.ValueType.Should().Be(typeof(int)); + resolver.Resolve(context).AsInt().Should().Be(1); + } + + [Fact] + [Trait("Resolver", "MinElement")] + public void Min_element_resolver_returns_default_for_empty_arrays() + { + var resolver = new MinElementResolver(new ArrayVariableResolver("missing", typeof(int))); + + resolver.Resolve(new GraphContext()).AsInt().Should().Be(0); + } + + [Fact] + [Trait("Resolver", "MinElement")] + public void Min_element_resolver_rejects_non_numeric_element_types() + { + Action act = () => _ = new MinElementResolver(new ArrayVariableResolver("flags", typeof(bool))); + + act.Should().Throw(); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ObjectAppendResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ObjectAppendResolverTests.cs new file mode 100644 index 0000000..2b501b4 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ObjectAppendResolverTests.cs @@ -0,0 +1,49 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ObjectAppendResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "ObjectAppend")] + public void Object_append_resolver_adds_elements_to_the_end() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var entity2 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1]); + context.GraphVariables.DefineObjectVariable("extra", entity2); + + var resolver = new ObjectAppendResolver( + new EntityArrayVariableResolver("targets"), + new EntityVariableResolver("extra")); + + resolver.ResolveArray(context).Should().Equal(entity1, entity2); + } + + [Fact] + [Trait("Resolver", "ObjectAppend")] + public void Object_append_resolver_appends_to_an_empty_source() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectVariable("extra", entity1); + + var resolver = new ObjectAppendResolver( + new EntityArrayVariableResolver("missing"), + new EntityVariableResolver("extra")); + + resolver.ResolveArray(context).Should().Equal(entity1); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ObjectConcatResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ObjectConcatResolverTests.cs new file mode 100644 index 0000000..29684dc --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ObjectConcatResolverTests.cs @@ -0,0 +1,50 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ObjectConcatResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "ObjectConcat")] + public void Object_concat_resolver_appends_the_second_array_after_the_first() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var entity2 = new TestEntity(_tagsManager, _cuesManager); + var entity3 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("first", [entity1, entity2]); + context.GraphVariables.DefineObjectArrayVariable("second", [entity3]); + + var resolver = new ObjectConcatResolver( + new EntityArrayVariableResolver("first"), + new EntityArrayVariableResolver("second")); + + resolver.ResolveArray(context).Should().Equal(entity1, entity2, entity3); + } + + [Fact] + [Trait("Resolver", "ObjectConcat")] + public void Object_concat_resolver_returns_the_other_side_when_one_is_empty() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("first", [entity1]); + + var resolver = new ObjectConcatResolver( + new EntityArrayVariableResolver("first"), + new EntityArrayVariableResolver("missing")); + + resolver.ResolveArray(context).Should().Equal(entity1); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ObjectContainsResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ObjectContainsResolverTests.cs new file mode 100644 index 0000000..348c0a3 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ObjectContainsResolverTests.cs @@ -0,0 +1,52 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ObjectContainsResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "ObjectContains")] + public void Object_contains_resolver_returns_true_when_the_reference_is_present() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var entity2 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2]); + context.GraphVariables.DefineObjectVariable("candidate", entity2); + + var resolver = new ObjectContainsResolver( + new EntityArrayVariableResolver("targets"), + new EntityVariableResolver("candidate")); + + resolver.Resolve(context).AsBool().Should().BeTrue(); + } + + [Fact] + [Trait("Resolver", "ObjectContains")] + public void Object_contains_resolver_returns_false_when_the_reference_is_absent() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1]); + context.GraphVariables.DefineObjectVariable( + "candidate", + new TestEntity(_tagsManager, _cuesManager)); + + var resolver = new ObjectContainsResolver( + new EntityArrayVariableResolver("targets"), + new EntityVariableResolver("candidate")); + + resolver.Resolve(context).AsBool().Should().BeFalse(); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ObjectDistinctResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ObjectDistinctResolverTests.cs new file mode 100644 index 0000000..1ae8ec9 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ObjectDistinctResolverTests.cs @@ -0,0 +1,65 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Effects; +using Gamesmiths.Forge.Effects.Duration; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ObjectDistinctResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "ObjectDistinct")] + public void Object_distinct_resolver_keeps_the_first_occurrence_of_each_reference() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var entity2 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable( + "targets", + [entity1, entity2, entity1, entity2]); + + var resolver = new ObjectDistinctResolver(new EntityArrayVariableResolver("targets")); + + resolver.ResolveArray(context).Should().Equal(entity1, entity2); + } + + [Fact] + [Trait("Resolver", "ObjectDistinct")] + public void Object_distinct_resolver_dedupes_effects_by_reference_identity() + { + Effect burn = CreateInstantEffect("Burn"); + Effect chill = CreateInstantEffect("Chill"); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("effects", [burn, chill, burn, chill]); + + var resolver = new ObjectDistinctResolver(new ObjectArrayVariableResolver("effects")); + + resolver.ResolveArray(context).Should().Equal(burn, chill); + } + + [Fact] + [Trait("Resolver", "ObjectDistinct")] + public void Object_distinct_resolver_returns_empty_array_for_missing_variable() + { + var resolver = new ObjectDistinctResolver(new EntityArrayVariableResolver("missing")); + + resolver.ResolveArray(new GraphContext()).Should().BeEmpty(); + } + + private static Effect CreateInstantEffect(string name) + { + return new Effect( + new EffectData(name, new DurationData(DurationType.Instant)), + new EffectOwnership(null, null)); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ObjectElementAtResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ObjectElementAtResolverTests.cs new file mode 100644 index 0000000..b22ee67 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ObjectElementAtResolverTests.cs @@ -0,0 +1,76 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Effects; +using Gamesmiths.Forge.Effects.Duration; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ObjectElementAtResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "ObjectElementAt")] + public void Object_element_at_resolver_reads_the_element_at_the_resolved_index() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var entity2 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2]); + var source = new EntityArrayVariableResolver("targets"); + + var resolver = new ObjectElementAtResolver( + source, + new VariantResolver(new Variant128(1), typeof(int))); + + resolver.Resolve(context).Should().BeSameAs(entity2); + } + + [Fact] + [Trait("Resolver", "ObjectElementAt")] + public void Object_element_at_resolver_reads_the_effect_at_the_resolved_index() + { + Effect burn = CreateInstantEffect("Burn"); + Effect chill = CreateInstantEffect("Chill"); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("effects", [burn, chill]); + + var resolver = new ObjectElementAtResolver( + new ObjectArrayVariableResolver("effects"), + new VariantResolver(new Variant128(1), typeof(int))); + + resolver.Resolve(context).Should().BeSameAs(chill); + } + + [Fact] + [Trait("Resolver", "ObjectElementAt")] + public void Object_element_at_resolver_returns_null_for_out_of_range_index() + { + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable( + "targets", + [new TestEntity(_tagsManager, _cuesManager)]); + var source = new EntityArrayVariableResolver("targets"); + + var resolver = new ObjectElementAtResolver( + source, + new VariantResolver(new Variant128(-1), typeof(int))); + + resolver.Resolve(context).Should().BeNull(); + } + + private static Effect CreateInstantEffect(string name) + { + return new Effect( + new EffectData(name, new DurationData(DurationType.Instant)), + new EffectOwnership(null, null)); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ObjectExceptResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ObjectExceptResolverTests.cs new file mode 100644 index 0000000..a8fc745 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ObjectExceptResolverTests.cs @@ -0,0 +1,50 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ObjectExceptResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "ObjectExcept")] + public void Object_except_resolver_removes_references_found_in_the_other_array() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var entity2 = new TestEntity(_tagsManager, _cuesManager); + var entity3 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2, entity3]); + context.GraphVariables.DefineObjectArrayVariable("excluded", [entity2]); + + var resolver = new ObjectExceptResolver( + new EntityArrayVariableResolver("targets"), + new EntityArrayVariableResolver("excluded")); + + resolver.ResolveArray(context).Should().Equal(entity1, entity3); + } + + [Fact] + [Trait("Resolver", "ObjectExcept")] + public void Object_except_resolver_keeps_the_array_unchanged_when_the_other_is_empty() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1]); + + var resolver = new ObjectExceptResolver( + new EntityArrayVariableResolver("targets"), + new EntityArrayVariableResolver("missing")); + + resolver.ResolveArray(context).Should().Equal(entity1); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ObjectFirstResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ObjectFirstResolverTests.cs new file mode 100644 index 0000000..0be0833 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ObjectFirstResolverTests.cs @@ -0,0 +1,100 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Effects; +using Gamesmiths.Forge.Effects.Duration; +using Gamesmiths.Forge.Effects.Magnitudes; +using Gamesmiths.Forge.Effects.Modifiers; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ObjectFirstResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "ObjectFirst")] + public void Object_first_resolver_reads_the_first_element() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var entity2 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2]); + var source = new EntityArrayVariableResolver("targets"); + + var resolver = new ObjectFirstResolver(source); + + resolver.Resolve(context).Should().BeSameAs(entity1); + } + + [Fact] + [Trait("Resolver", "ObjectFirst")] + public void Object_first_resolver_reads_the_first_effect() + { + Effect burn = CreateInstantEffect("Burn"); + Effect chill = CreateInstantEffect("Chill"); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("effects", [burn, chill]); + + var resolver = new ObjectFirstResolver(new ObjectArrayVariableResolver("effects")); + + resolver.Resolve(context).Should().BeSameAs(burn); + } + + [Fact] + [Trait("Resolver", "ObjectFirst")] + public void Object_first_resolver_reads_the_first_active_effect_handle() + { + var entity = new VitalTestEntity(_tagsManager, _cuesManager); + ActiveEffectHandle first = ApplyInfiniteEffect(entity, "Regen"); + ActiveEffectHandle second = ApplyInfiniteEffect(entity, "Haste"); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("handles", [first, second]); + + var resolver = new ObjectFirstResolver( + new ObjectArrayVariableResolver("handles")); + + resolver.Resolve(context).Should().BeSameAs(first); + } + + [Fact] + [Trait("Resolver", "ObjectFirst")] + public void Object_first_resolver_returns_null_for_empty_array() + { + var resolver = new ObjectFirstResolver(new EntityArrayVariableResolver("missing")); + + resolver.Resolve(new GraphContext()).Should().BeNull(); + } + + private static Effect CreateInstantEffect(string name) + { + return new Effect( + new EffectData(name, new DurationData(DurationType.Instant)), + new EffectOwnership(null, null)); + } + + private static ActiveEffectHandle ApplyInfiniteEffect(VitalTestEntity entity, string name) + { + var effectData = new EffectData( + name, + new DurationData(DurationType.Infinite), + [ + new Modifier( + "VitalAttributeSet.CurrentHealth", + ModifierOperation.FlatBonus, + new ModifierMagnitude(MagnitudeCalculationType.ScalableFloat, new ScalableFloat(1))), + ]); + + ActiveEffectHandle? handle = entity.EffectsManager.ApplyEffect( + new Effect(effectData, new EffectOwnership(entity, entity))); + handle.Should().NotBeNull(); + return handle!; + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ObjectIndexOfResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ObjectIndexOfResolverTests.cs new file mode 100644 index 0000000..bd313b2 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ObjectIndexOfResolverTests.cs @@ -0,0 +1,52 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ObjectIndexOfResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "ObjectIndexOf")] + public void Object_index_of_resolver_returns_the_index_of_the_reference() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var entity2 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2]); + context.GraphVariables.DefineObjectVariable("candidate", entity2); + + var resolver = new ObjectIndexOfResolver( + new EntityArrayVariableResolver("targets"), + new EntityVariableResolver("candidate")); + + resolver.Resolve(context).AsInt().Should().Be(1); + } + + [Fact] + [Trait("Resolver", "ObjectIndexOf")] + public void Object_index_of_resolver_returns_minus_one_when_the_reference_is_absent() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1]); + context.GraphVariables.DefineObjectVariable( + "candidate", + new TestEntity(_tagsManager, _cuesManager)); + + var resolver = new ObjectIndexOfResolver( + new EntityArrayVariableResolver("targets"), + new EntityVariableResolver("candidate")); + + resolver.Resolve(context).AsInt().Should().Be(-1); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ObjectLastResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ObjectLastResolverTests.cs new file mode 100644 index 0000000..cb1d0c1 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ObjectLastResolverTests.cs @@ -0,0 +1,100 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Effects; +using Gamesmiths.Forge.Effects.Duration; +using Gamesmiths.Forge.Effects.Magnitudes; +using Gamesmiths.Forge.Effects.Modifiers; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ObjectLastResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "ObjectLast")] + public void Object_last_resolver_reads_the_last_element() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var entity2 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2]); + var source = new EntityArrayVariableResolver("targets"); + + var resolver = new ObjectLastResolver(source); + + resolver.Resolve(context).Should().BeSameAs(entity2); + } + + [Fact] + [Trait("Resolver", "ObjectLast")] + public void Object_last_resolver_reads_the_last_effect() + { + Effect burn = CreateInstantEffect("Burn"); + Effect chill = CreateInstantEffect("Chill"); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("effects", [burn, chill]); + + var resolver = new ObjectLastResolver(new ObjectArrayVariableResolver("effects")); + + resolver.Resolve(context).Should().BeSameAs(chill); + } + + [Fact] + [Trait("Resolver", "ObjectLast")] + public void Object_last_resolver_reads_the_last_active_effect_handle() + { + var entity = new VitalTestEntity(_tagsManager, _cuesManager); + ActiveEffectHandle first = ApplyInfiniteEffect(entity, "Regen"); + ActiveEffectHandle second = ApplyInfiniteEffect(entity, "Haste"); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("handles", [first, second]); + + var resolver = new ObjectLastResolver( + new ObjectArrayVariableResolver("handles")); + + resolver.Resolve(context).Should().BeSameAs(second); + } + + [Fact] + [Trait("Resolver", "ObjectLast")] + public void Object_last_resolver_returns_null_for_empty_array() + { + var resolver = new ObjectLastResolver(new EntityArrayVariableResolver("missing")); + + resolver.Resolve(new GraphContext()).Should().BeNull(); + } + + private static Effect CreateInstantEffect(string name) + { + return new Effect( + new EffectData(name, new DurationData(DurationType.Instant)), + new EffectOwnership(null, null)); + } + + private static ActiveEffectHandle ApplyInfiniteEffect(VitalTestEntity entity, string name) + { + var effectData = new EffectData( + name, + new DurationData(DurationType.Infinite), + [ + new Modifier( + "VitalAttributeSet.CurrentHealth", + ModifierOperation.FlatBonus, + new ModifierMagnitude(MagnitudeCalculationType.ScalableFloat, new ScalableFloat(1))), + ]); + + ActiveEffectHandle? handle = entity.EffectsManager.ApplyEffect( + new Effect(effectData, new EffectOwnership(entity, entity))); + handle.Should().NotBeNull(); + return handle!; + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ObjectOrderByResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ObjectOrderByResolverTests.cs new file mode 100644 index 0000000..82e5cf2 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ObjectOrderByResolverTests.cs @@ -0,0 +1,85 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ObjectOrderByResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "ObjectOrderBy")] + public void Object_order_by_resolver_sorts_entities_by_attribute_key() + { + var entity1 = new VitalTestEntity(_tagsManager, _cuesManager); + var entity2 = new VitalTestEntity(_tagsManager, _cuesManager); + var entity3 = new VitalTestEntity(_tagsManager, _cuesManager); + entity1.VitalAttributeSet.UpdateBaseHealth(40); + entity2.VitalAttributeSet.UpdateBaseHealth(10); + entity3.VitalAttributeSet.UpdateBaseHealth(30); + + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2, entity3]); + + var resolver = new ObjectOrderByResolver( + new EntityArrayVariableResolver("targets"), + new AttributeResolver("VitalAttributeSet.CurrentHealth", new ElementEntityResolver())); + + IForgeEntity[] result = resolver.ResolveArray(context); + + result.Should().Equal(entity2, entity3, entity1); + } + + [Fact] + [Trait("Resolver", "ObjectOrderBy")] + public void Object_order_by_resolver_sorts_entities_descending_when_configured() + { + var entity1 = new VitalTestEntity(_tagsManager, _cuesManager); + var entity2 = new VitalTestEntity(_tagsManager, _cuesManager); + entity1.VitalAttributeSet.UpdateBaseHealth(10); + entity2.VitalAttributeSet.UpdateBaseHealth(40); + + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2]); + + var resolver = new ObjectOrderByResolver( + new EntityArrayVariableResolver("targets"), + new AttributeResolver("VitalAttributeSet.CurrentHealth", new ElementEntityResolver()), + SortDirection.Descending); + + IForgeEntity[] result = resolver.ResolveArray(context); + + result.Should().Equal(entity2, entity1); + } + + [Fact] + [Trait("Resolver", "ObjectOrderBy")] + public void Object_order_by_resolver_keeps_original_order_for_equal_keys() + { + var entity1 = new VitalTestEntity(_tagsManager, _cuesManager); + var entity2 = new VitalTestEntity(_tagsManager, _cuesManager); + var entity3 = new VitalTestEntity(_tagsManager, _cuesManager); + entity1.VitalAttributeSet.UpdateBaseHealth(50); + entity2.VitalAttributeSet.UpdateBaseHealth(50); + entity3.VitalAttributeSet.UpdateBaseHealth(10); + + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2, entity3]); + + var resolver = new ObjectOrderByResolver( + new EntityArrayVariableResolver("targets"), + new AttributeResolver("VitalAttributeSet.CurrentHealth", new ElementEntityResolver())); + + IForgeEntity[] result = resolver.ResolveArray(context); + + result.Should().Equal(entity3, entity1, entity2); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ObjectRemoveAtResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ObjectRemoveAtResolverTests.cs new file mode 100644 index 0000000..ef08830 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ObjectRemoveAtResolverTests.cs @@ -0,0 +1,49 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ObjectRemoveAtResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "ObjectRemoveAt")] + public void Object_remove_at_resolver_removes_the_element_at_the_resolved_index() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var entity2 = new TestEntity(_tagsManager, _cuesManager); + var entity3 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2, entity3]); + + var resolver = new ObjectRemoveAtResolver( + new EntityArrayVariableResolver("targets"), + new VariantResolver(new Variant128(0), typeof(int))); + + resolver.ResolveArray(context).Should().Equal(entity2, entity3); + } + + [Fact] + [Trait("Resolver", "ObjectRemoveAt")] + public void Object_remove_at_resolver_keeps_the_array_unchanged_for_out_of_range_index() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1]); + + var resolver = new ObjectRemoveAtResolver( + new EntityArrayVariableResolver("targets"), + new VariantResolver(new Variant128(3), typeof(int))); + + resolver.ResolveArray(context).Should().Equal(entity1); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ObjectReverseResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ObjectReverseResolverTests.cs new file mode 100644 index 0000000..7117db0 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ObjectReverseResolverTests.cs @@ -0,0 +1,41 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ObjectReverseResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "ObjectReverse")] + public void Object_reverse_resolver_reverses_the_element_order() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var entity2 = new TestEntity(_tagsManager, _cuesManager); + var entity3 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2, entity3]); + + var resolver = new ObjectReverseResolver(new EntityArrayVariableResolver("targets")); + + resolver.ResolveArray(context).Should().Equal(entity3, entity2, entity1); + } + + [Fact] + [Trait("Resolver", "ObjectReverse")] + public void Object_reverse_resolver_returns_empty_array_for_missing_variable() + { + var resolver = new ObjectReverseResolver(new EntityArrayVariableResolver("missing")); + + resolver.ResolveArray(new GraphContext()).Should().BeEmpty(); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ObjectSkipResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ObjectSkipResolverTests.cs new file mode 100644 index 0000000..5caa0c2 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ObjectSkipResolverTests.cs @@ -0,0 +1,49 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ObjectSkipResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "ObjectSkip")] + public void Object_skip_resolver_drops_the_first_elements() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var entity2 = new TestEntity(_tagsManager, _cuesManager); + var entity3 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2, entity3]); + + var resolver = new ObjectSkipResolver( + new EntityArrayVariableResolver("targets"), + new VariantResolver(new Variant128(2), typeof(int))); + + resolver.ResolveArray(context).Should().Equal(entity3); + } + + [Fact] + [Trait("Resolver", "ObjectSkip")] + public void Object_skip_resolver_returns_empty_array_when_skipping_more_than_the_length() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1]); + + var resolver = new ObjectSkipResolver( + new EntityArrayVariableResolver("targets"), + new VariantResolver(new Variant128(5), typeof(int))); + + resolver.ResolveArray(context).Should().BeEmpty(); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ObjectTakeResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ObjectTakeResolverTests.cs new file mode 100644 index 0000000..fc22578 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ObjectTakeResolverTests.cs @@ -0,0 +1,49 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ObjectTakeResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "ObjectTake")] + public void Object_take_resolver_keeps_the_first_elements() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var entity2 = new TestEntity(_tagsManager, _cuesManager); + var entity3 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2, entity3]); + + var resolver = new ObjectTakeResolver( + new EntityArrayVariableResolver("targets"), + new VariantResolver(new Variant128(2), typeof(int))); + + resolver.ResolveArray(context).Should().Equal(entity1, entity2); + } + + [Fact] + [Trait("Resolver", "ObjectTake")] + public void Object_take_resolver_clamps_counts_larger_than_the_array() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1]); + + var resolver = new ObjectTakeResolver( + new EntityArrayVariableResolver("targets"), + new VariantResolver(new Variant128(5), typeof(int))); + + resolver.ResolveArray(context).Should().Equal(entity1); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ObjectWhereResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ObjectWhereResolverTests.cs new file mode 100644 index 0000000..f012b2e --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ObjectWhereResolverTests.cs @@ -0,0 +1,67 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ObjectWhereResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "ObjectWhere")] + public void Object_where_resolver_keeps_only_entities_matching_the_predicate() + { + var entity1 = new VitalTestEntity(_tagsManager, _cuesManager); + var entity2 = new VitalTestEntity(_tagsManager, _cuesManager); + var entity3 = new VitalTestEntity(_tagsManager, _cuesManager); + entity1.VitalAttributeSet.UpdateBaseHealth(40); + entity2.VitalAttributeSet.UpdateBaseHealth(70); + entity3.VitalAttributeSet.UpdateBaseHealth(10); + + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2, entity3]); + + var resolver = new ObjectWhereResolver( + new EntityArrayVariableResolver("targets"), + new ComparisonResolver( + new AttributeResolver("VitalAttributeSet.CurrentHealth", new ElementEntityResolver()), + ComparisonOperation.GreaterThan, + new VariantResolver(new Variant128(30), typeof(int)))); + + IForgeEntity[] result = resolver.ResolveArray(context); + + result.Should().Equal(entity1, entity2); + } + + [Fact] + [Trait("Resolver", "ObjectWhere")] + public void Object_where_resolver_returns_empty_array_for_missing_variable() + { + var resolver = new ObjectWhereResolver( + new EntityArrayVariableResolver("missing"), + new IsValidResolver(new ElementResolver())); + + resolver.ResolveArray(new GraphContext()).Should().BeEmpty(); + } + + [Fact] + [Trait("Resolver", "ObjectWhere")] + public void Object_where_resolver_rejects_non_boolean_predicates() + { + var source = new EntityArrayVariableResolver("targets"); + + Action act = () => _ = new ObjectWhereResolver( + source, + new VariantResolver(new Variant128(1), typeof(int))); + + act.Should().Throw(); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/OrderByResolverTests.cs b/Forge.Tests/Statescript/Resolvers/OrderByResolverTests.cs new file mode 100644 index 0000000..252cf32 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/OrderByResolverTests.cs @@ -0,0 +1,77 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class OrderByResolverTests +{ + [Fact] + [Trait("Resolver", "OrderBy")] + public void Order_by_resolver_sorts_elements_ascending_by_default() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new OrderByResolver(source, new ElementValueResolver(typeof(int))); + + Variant128[] result = resolver.ResolveArray(context); + + result.Should().HaveCount(3); + result[0].AsInt().Should().Be(1); + result[1].AsInt().Should().Be(2); + result[2].AsInt().Should().Be(3); + } + + [Fact] + [Trait("Resolver", "OrderBy")] + public void Order_by_resolver_sorts_elements_descending_when_configured() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new OrderByResolver( + source, + new ElementValueResolver(typeof(int)), + SortDirection.Descending); + + Variant128[] result = resolver.ResolveArray(context); + + result.Should().HaveCount(3); + result[0].AsInt().Should().Be(3); + result[1].AsInt().Should().Be(2); + result[2].AsInt().Should().Be(1); + } + + [Fact] + [Trait("Resolver", "OrderBy")] + public void Order_by_resolver_returns_empty_array_for_missing_variable() + { + var resolver = new OrderByResolver( + new ArrayVariableResolver("missing", typeof(int)), + new ElementValueResolver(typeof(int))); + + resolver.ResolveArray(new GraphContext()).Should().BeEmpty(); + } + + [Fact] + [Trait("Resolver", "OrderBy")] + public void Order_by_resolver_rejects_non_numeric_key_selectors() + { + var source = new ArrayVariableResolver("numbers", typeof(int)); + + Action act = () => _ = new OrderByResolver( + source, + new VariantResolver(new Variant128(true), typeof(bool))); + + act.Should().Throw(); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/RemoveAtResolverTests.cs b/Forge.Tests/Statescript/Resolvers/RemoveAtResolverTests.cs new file mode 100644 index 0000000..320a724 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/RemoveAtResolverTests.cs @@ -0,0 +1,42 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class RemoveAtResolverTests +{ + [Fact] + [Trait("Resolver", "RemoveAt")] + public void Remove_at_resolver_removes_the_element_at_the_resolved_index() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new RemoveAtResolver(source, new VariantResolver(new Variant128(1), typeof(int))); + + Variant128[] result = resolver.ResolveArray(context); + + result.Should().HaveCount(2); + result[0].AsInt().Should().Be(3); + result[1].AsInt().Should().Be(2); + } + + [Fact] + [Trait("Resolver", "RemoveAt")] + public void Remove_at_resolver_keeps_the_array_unchanged_for_out_of_range_index() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable("numbers", [new Variant128(3), new Variant128(1)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new RemoveAtResolver(source, new VariantResolver(new Variant128(5), typeof(int))); + + resolver.ResolveArray(context).Should().HaveCount(2); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/ReverseResolverTests.cs b/Forge.Tests/Statescript/Resolvers/ReverseResolverTests.cs new file mode 100644 index 0000000..08ab63c --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/ReverseResolverTests.cs @@ -0,0 +1,39 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class ReverseResolverTests +{ + [Fact] + [Trait("Resolver", "Reverse")] + public void Reverse_resolver_reverses_the_element_order() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new ReverseResolver(source); + + Variant128[] result = resolver.ResolveArray(context); + + result.Should().HaveCount(3); + result[0].AsInt().Should().Be(2); + result[1].AsInt().Should().Be(1); + result[2].AsInt().Should().Be(3); + } + + [Fact] + [Trait("Resolver", "Reverse")] + public void Reverse_resolver_returns_empty_array_for_missing_variable() + { + var resolver = new ReverseResolver(new ArrayVariableResolver("missing", typeof(int))); + + resolver.ResolveArray(new GraphContext()).Should().BeEmpty(); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/SelectObjectResolverTests.cs b/Forge.Tests/Statescript/Resolvers/SelectObjectResolverTests.cs new file mode 100644 index 0000000..6583416 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/SelectObjectResolverTests.cs @@ -0,0 +1,45 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class SelectObjectResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "SelectObject")] + public void Select_object_resolver_projects_each_object_element() + { + var entity1 = new TestEntity(_tagsManager, _cuesManager); + var entity2 = new TestEntity(_tagsManager, _cuesManager); + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2]); + + var resolver = new SelectObjectResolver( + new EntityArrayVariableResolver("targets"), + new ElementEntityResolver()); + + resolver.ElementType.Should().Be(typeof(IForgeEntity)); + resolver.ResolveArray(context).Should().Equal(entity1, entity2); + } + + [Fact] + [Trait("Resolver", "SelectObject")] + public void Select_object_resolver_returns_empty_array_for_missing_variable() + { + var resolver = new SelectObjectResolver( + new EntityArrayVariableResolver("missing"), + new ElementEntityResolver()); + + resolver.ResolveArray(new GraphContext()).Should().BeEmpty(); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/SelectResolverTests.cs b/Forge.Tests/Statescript/Resolvers/SelectResolverTests.cs new file mode 100644 index 0000000..fe0993e --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/SelectResolverTests.cs @@ -0,0 +1,78 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Core; +using Gamesmiths.Forge.Cues; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; +using Gamesmiths.Forge.Tags; +using Gamesmiths.Forge.Tests.Helpers; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class SelectResolverTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture +{ + private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager; + private readonly CuesManager _cuesManager = tagsAndCuesFixture.CuesManager; + + [Fact] + [Trait("Resolver", "Select")] + public void Select_resolver_projects_each_value_element() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new SelectResolver( + source, + new MultiplyResolver( + new ElementValueResolver(typeof(int)), + new VariantResolver(new Variant128(2), typeof(int)))); + + Variant128[] result = resolver.ResolveArray(context); + + result.Should().HaveCount(3); + result[0].AsInt().Should().Be(6); + result[1].AsInt().Should().Be(2); + result[2].AsInt().Should().Be(4); + } + + [Fact] + [Trait("Resolver", "Select")] + public void Select_resolver_projects_object_elements_into_values() + { + var entity1 = new VitalTestEntity(_tagsManager, _cuesManager); + var entity2 = new VitalTestEntity(_tagsManager, _cuesManager); + entity1.VitalAttributeSet.UpdateBaseHealth(40); + entity2.VitalAttributeSet.UpdateBaseHealth(70); + + var context = new GraphContext(); + context.GraphVariables.DefineObjectArrayVariable("targets", [entity1, entity2]); + + var resolver = new SelectResolver( + new EntityArrayVariableResolver("targets"), + new AttributeResolver("VitalAttributeSet.CurrentHealth", new ElementEntityResolver())); + + Variant128[] result = resolver.ResolveArray(context); + + result.Should().HaveCount(2); + result[0].AsInt().Should().Be(40); + result[1].AsInt().Should().Be(70); + } + + [Fact] + [Trait("Resolver", "Select")] + public void Select_resolver_reports_the_projection_value_type() + { + var resolver = new SelectResolver( + new ArrayVariableResolver("numbers", typeof(int)), + new ComparisonResolver( + new ElementValueResolver(typeof(int)), + ComparisonOperation.GreaterThan, + new VariantResolver(new Variant128(1), typeof(int)))); + + resolver.ElementType.Should().Be(typeof(bool)); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/SkipResolverTests.cs b/Forge.Tests/Statescript/Resolvers/SkipResolverTests.cs new file mode 100644 index 0000000..b5c5551 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/SkipResolverTests.cs @@ -0,0 +1,55 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class SkipResolverTests +{ + [Fact] + [Trait("Resolver", "Skip")] + public void Skip_resolver_drops_the_first_elements() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new SkipResolver(source, new VariantResolver(new Variant128(1), typeof(int))); + + Variant128[] result = resolver.ResolveArray(context); + + result.Should().HaveCount(2); + result[0].AsInt().Should().Be(1); + result[1].AsInt().Should().Be(2); + } + + [Fact] + [Trait("Resolver", "Skip")] + public void Skip_resolver_returns_empty_array_when_skipping_more_than_the_length() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable("numbers", [new Variant128(3), new Variant128(1)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new SkipResolver(source, new VariantResolver(new Variant128(5), typeof(int))); + + resolver.ResolveArray(context).Should().BeEmpty(); + } + + [Fact] + [Trait("Resolver", "Skip")] + public void Skip_resolver_keeps_all_elements_for_negative_counts() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable("numbers", [new Variant128(3), new Variant128(1)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new SkipResolver(source, new VariantResolver(new Variant128(-1), typeof(int))); + + resolver.ResolveArray(context).Should().HaveCount(2); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/SumResolverTests.cs b/Forge.Tests/Statescript/Resolvers/SumResolverTests.cs new file mode 100644 index 0000000..fbc9cda --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/SumResolverTests.cs @@ -0,0 +1,58 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class SumResolverTests +{ + [Fact] + [Trait("Resolver", "Sum")] + public void Sum_resolver_adds_all_int_elements() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + + var resolver = new SumResolver(new ArrayVariableResolver("numbers", typeof(int))); + + resolver.ValueType.Should().Be(typeof(int)); + resolver.Resolve(context).AsInt().Should().Be(6); + } + + [Fact] + [Trait("Resolver", "Sum")] + public void Sum_resolver_adds_all_float_elements() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(1.5f), new Variant128(2.25f)]); + + var resolver = new SumResolver(new ArrayVariableResolver("numbers", typeof(float))); + + resolver.ValueType.Should().Be(typeof(float)); + resolver.Resolve(context).AsFloat().Should().Be(3.75f); + } + + [Fact] + [Trait("Resolver", "Sum")] + public void Sum_resolver_returns_zero_for_empty_arrays() + { + var resolver = new SumResolver(new ArrayVariableResolver("missing", typeof(int))); + + resolver.Resolve(new GraphContext()).AsInt().Should().Be(0); + } + + [Fact] + [Trait("Resolver", "Sum")] + public void Sum_resolver_rejects_non_numeric_element_types() + { + Action act = () => _ = new SumResolver(new ArrayVariableResolver("flags", typeof(bool))); + + act.Should().Throw(); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/TakeResolverTests.cs b/Forge.Tests/Statescript/Resolvers/TakeResolverTests.cs new file mode 100644 index 0000000..6713cc7 --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/TakeResolverTests.cs @@ -0,0 +1,55 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class TakeResolverTests +{ + [Fact] + [Trait("Resolver", "Take")] + public void Take_resolver_keeps_the_first_elements() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new TakeResolver(source, new VariantResolver(new Variant128(2), typeof(int))); + + Variant128[] result = resolver.ResolveArray(context); + + result.Should().HaveCount(2); + result[0].AsInt().Should().Be(3); + result[1].AsInt().Should().Be(1); + } + + [Fact] + [Trait("Resolver", "Take")] + public void Take_resolver_clamps_counts_larger_than_the_array() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable("numbers", [new Variant128(3), new Variant128(1)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new TakeResolver(source, new VariantResolver(new Variant128(5), typeof(int))); + + resolver.ResolveArray(context).Should().HaveCount(2); + } + + [Fact] + [Trait("Resolver", "Take")] + public void Take_resolver_returns_empty_array_for_negative_counts() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable("numbers", [new Variant128(3), new Variant128(1)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new TakeResolver(source, new VariantResolver(new Variant128(-1), typeof(int))); + + resolver.ResolveArray(context).Should().BeEmpty(); + } +} diff --git a/Forge.Tests/Statescript/Resolvers/WhereResolverTests.cs b/Forge.Tests/Statescript/Resolvers/WhereResolverTests.cs new file mode 100644 index 0000000..84f3e0f --- /dev/null +++ b/Forge.Tests/Statescript/Resolvers/WhereResolverTests.cs @@ -0,0 +1,83 @@ +// Copyright © Gamesmiths Guild. + +using FluentAssertions; +using Gamesmiths.Forge.Statescript; +using Gamesmiths.Forge.Statescript.Properties; + +namespace Gamesmiths.Forge.Tests.Statescript.Resolvers; + +public class WhereResolverTests +{ + [Fact] + [Trait("Resolver", "Where")] + public void Where_resolver_keeps_only_elements_matching_the_predicate() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new WhereResolver( + source, + new ComparisonResolver( + new ElementValueResolver(typeof(int)), + ComparisonOperation.GreaterThan, + new VariantResolver(new Variant128(1), typeof(int)))); + + Variant128[] result = resolver.ResolveArray(context); + + result.Should().HaveCount(2); + result[0].AsInt().Should().Be(3); + result[1].AsInt().Should().Be(2); + } + + [Fact] + [Trait("Resolver", "Where")] + public void Where_resolver_removes_matching_elements_when_combined_with_not() + { + var context = new GraphContext(); + context.GraphVariables.DefineArrayVariable( + "numbers", + [new Variant128(3), new Variant128(1), new Variant128(2)]); + var source = new ArrayVariableResolver("numbers", typeof(int)); + + var resolver = new WhereResolver( + source, + new NotResolver( + new ComparisonResolver( + new ElementValueResolver(typeof(int)), + ComparisonOperation.GreaterThan, + new VariantResolver(new Variant128(1), typeof(int))))); + + Variant128[] result = resolver.ResolveArray(context); + + result.Should().ContainSingle(); + result[0].AsInt().Should().Be(1); + } + + [Fact] + [Trait("Resolver", "Where")] + public void Where_resolver_returns_empty_array_for_missing_variable() + { + var resolver = new WhereResolver( + new ArrayVariableResolver("missing", typeof(int)), + new ComparisonResolver( + new ElementValueResolver(typeof(int)), + ComparisonOperation.GreaterThan, + new VariantResolver(new Variant128(1), typeof(int)))); + + resolver.ResolveArray(new GraphContext()).Should().BeEmpty(); + } + + [Fact] + [Trait("Resolver", "Where")] + public void Where_resolver_rejects_non_boolean_predicates() + { + var source = new ArrayVariableResolver("numbers", typeof(int)); + + Action act = () => _ = new WhereResolver(source, new VariantResolver(new Variant128(1), typeof(int))); + + act.Should().Throw(); + } +} diff --git a/Forge/Statescript/ElementFrame.cs b/Forge/Statescript/ElementFrame.cs new file mode 100644 index 0000000..6f872cb --- /dev/null +++ b/Forge/Statescript/ElementFrame.cs @@ -0,0 +1,62 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript; + +/// +/// Represents the array element currently being iterated by an array resolver. Iterating resolvers (e.g. filter, sort, +/// and projection resolvers) publish one frame per element on the element stack while +/// evaluating their nested "lambda" resolvers, and element resolvers read the value back through +/// . +/// +public readonly struct ElementFrame +{ + /// + /// Gets the element value when iterating a value-typed array. Holds a default value when the frame represents an + /// object-backed element. + /// + public Variant128 Value { get; } + + /// + /// Gets the element value when iterating an object-backed array. Holds when the frame + /// represents a value-typed element. + /// + public object? ObjectValue { get; } + + /// + /// Gets the type of the iterated array's elements. + /// + public Type ElementType { get; } + + /// + /// Gets the zero-based index of the element within the iterated array. + /// + public int Index { get; } + + /// + /// Initializes a new instance of the struct for a value-typed array element. + /// + /// The element value. + /// The type of the element. + /// The zero-based index of the element within the iterated array. + public ElementFrame(Variant128 value, Type elementType, int index) + { + Value = value; + ObjectValue = null; + ElementType = elementType; + Index = index; + } + + /// + /// Initializes a new instance of the struct for an object-backed array element. + /// + /// The element value. + /// The type of the element. + /// The zero-based index of the element within the iterated array. + public ElementFrame(object? objectValue, Type elementType, int index) + { + Value = default; + ObjectValue = objectValue; + ElementType = elementType; + Index = index; + } +} diff --git a/Forge/Statescript/GraphContext.cs b/Forge/Statescript/GraphContext.cs index 158b3a3..b64c86d 100644 --- a/Forge/Statescript/GraphContext.cs +++ b/Forge/Statescript/GraphContext.cs @@ -14,6 +14,8 @@ public sealed class GraphContext { private readonly Dictionary _nodeContexts = []; + private readonly List _elementFrames = []; + /// /// Gets a value indicating whether the graph is currently active. A graph is considered active if it has at least /// one active state node. @@ -78,6 +80,28 @@ public bool TryGetActivationContext([NotNullWhen(true)] out T? data) return false; } + /// + /// Attempts to retrieve the array element currently being iterated by an enclosing array resolver. Iterating + /// resolvers (filter, sort, projection, etc.) push one per element while evaluating + /// their nested "lambda" resolvers; element resolvers use this method to read the current element back. Frames + /// form a stack, so nested array operations always observe the innermost element. + /// + /// When this method returns , contains the innermost element frame. + /// + /// if an array element is currently being iterated; otherwise, + /// . + public bool TryGetCurrentElement(out ElementFrame frame) + { + if (_elementFrames.Count == 0) + { + frame = default; + return false; + } + + frame = _elementFrames[^1]; + return true; + } + /// /// Resolves a named value by first checking graph variables and then falling back to read-only property definitions /// on the graph. This is the primary way for nodes to read a named value at runtime without caring whether it is a @@ -389,6 +413,16 @@ internal T GetOrCreateNodeContext(Guid nodeID) return newContext; } + internal void PushElement(in ElementFrame frame) + { + _elementFrames.Add(frame); + } + + internal void PopElement() + { + _elementFrames.RemoveAt(_elementFrames.Count - 1); + } + internal bool HasNodeContext(Guid nodeID) { return _nodeContexts.ContainsKey(nodeID); diff --git a/Forge/Statescript/Node.cs b/Forge/Statescript/Node.cs index a28e119..d2ed0b5 100644 --- a/Forge/Statescript/Node.cs +++ b/Forge/Statescript/Node.cs @@ -244,8 +244,12 @@ protected static T CreatePort(byte index, string label = "") /// and . /// /// - /// The default implementation declares no parameters, which is correct for nodes like - /// and that have no data dependencies. + /// The default implementation declares no parameters, which is correct for nodes like + /// and that have no data dependencies. + /// This method is invoked from the base constructor, before any derived constructor body + /// runs. If the parameter schema depends on constructor arguments, capture them through field or + /// primary-constructor initializers (which execute before the base constructor). Assignments inside a constructor + /// body happen too late and this method would observe default field values. /// /// The list to add input property declarations to. /// The list to add output variable declarations to. diff --git a/Forge/Statescript/Properties/AllResolver.cs b/Forge/Statescript/Properties/AllResolver.cs new file mode 100644 index 0000000..d0415a1 --- /dev/null +++ b/Forge/Statescript/Properties/AllResolver.cs @@ -0,0 +1,71 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves a indicating whether every element of a nested array resolver satisfies a nested boolean +/// predicate resolver (e.g. "are all targets dead?"). The predicate is evaluated once per element with the current +/// element published on the element stack, so it can read the element through the element resolvers. +/// +/// +/// The source may come from either lane: a value-typed array () or an object-backed +/// array (). An empty array resolves to , and evaluation stops +/// at the first non-matching element. +/// +public class AllResolver : IPropertyResolver +{ + private readonly IArrayPropertyResolver? _valueSource; + + private readonly IObjectArrayResolver? _objectSource; + + private readonly IPropertyResolver _predicate; + + /// + public Type ValueType => typeof(bool); + + /// + /// Initializes a new instance of the class over a value-typed source array. + /// + /// The resolver providing the source array. + /// The resolver evaluated per element. Must resolve to . + public AllResolver(IArrayPropertyResolver source, IPropertyResolver predicate) + : this(source, null, predicate) + { + } + + /// + /// Initializes a new instance of the class over an object-backed source array. + /// + /// The resolver providing the source array. + /// The resolver evaluated per element. Must resolve to . + public AllResolver(IObjectArrayResolver source, IPropertyResolver predicate) + : this(null, source, predicate) + { + } + + private AllResolver( + IArrayPropertyResolver? valueSource, + IObjectArrayResolver? objectSource, + IPropertyResolver predicate) + { + _valueSource = valueSource; + _objectSource = objectSource; + _predicate = BooleanTypeUtils.ValidateBoolOperand(nameof(AllResolver), nameof(predicate), predicate); + } + + /// + public Variant128 Resolve(GraphContext graphContext) + { + var sequence = ElementSequence.Resolve(graphContext, _valueSource, _objectSource); + + for (int i = 0; i < sequence.Length; i++) + { + if (!ElementLambda.EvaluatePredicate(graphContext, _predicate, sequence.GetFrame(i))) + { + return new Variant128(false); + } + } + + return new Variant128(true); + } +} diff --git a/Forge/Statescript/Properties/AnyResolver.cs b/Forge/Statescript/Properties/AnyResolver.cs new file mode 100644 index 0000000..108b54d --- /dev/null +++ b/Forge/Statescript/Properties/AnyResolver.cs @@ -0,0 +1,80 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves a indicating whether a nested array resolver produces any elements, optionally testing +/// them against a nested boolean predicate resolver (e.g. "is any enemy in range?"). The predicate is evaluated once +/// per element with the current element published on the element stack, so it can read the element through the element +/// resolvers. +/// +/// +/// The source may come from either lane: a value-typed array () or an +/// object-backed array (). Evaluation stops at the first matching element. +/// +public class AnyResolver : IPropertyResolver +{ + private readonly IArrayPropertyResolver? _valueSource; + + private readonly IObjectArrayResolver? _objectSource; + + private readonly IPropertyResolver? _predicate; + + /// + public Type ValueType => typeof(bool); + + /// + /// Initializes a new instance of the class over a value-typed source array. + /// + /// The resolver providing the source array. + /// The optional resolver evaluated per element. Must resolve to . + /// When omitted, the resolver only checks that the array is not empty. + public AnyResolver(IArrayPropertyResolver source, IPropertyResolver? predicate = null) + : this(source, null, predicate) + { + } + + /// + /// Initializes a new instance of the class over an object-backed source array. + /// + /// The resolver providing the source array. + /// The optional resolver evaluated per element. Must resolve to . + /// When omitted, the resolver only checks that the array is not empty. + public AnyResolver(IObjectArrayResolver source, IPropertyResolver? predicate = null) + : this(null, source, predicate) + { + } + + private AnyResolver( + IArrayPropertyResolver? valueSource, + IObjectArrayResolver? objectSource, + IPropertyResolver? predicate) + { + _valueSource = valueSource; + _objectSource = objectSource; + _predicate = predicate is null + ? null + : BooleanTypeUtils.ValidateBoolOperand(nameof(AnyResolver), nameof(predicate), predicate); + } + + /// + public Variant128 Resolve(GraphContext graphContext) + { + var sequence = ElementSequence.Resolve(graphContext, _valueSource, _objectSource); + + if (_predicate is null) + { + return new Variant128(sequence.Length > 0); + } + + for (int i = 0; i < sequence.Length; i++) + { + if (ElementLambda.EvaluatePredicate(graphContext, _predicate, sequence.GetFrame(i))) + { + return new Variant128(true); + } + } + + return new Variant128(false); + } +} diff --git a/Forge/Statescript/Properties/AppendResolver.cs b/Forge/Statescript/Properties/AppendResolver.cs new file mode 100644 index 0000000..aacf826 --- /dev/null +++ b/Forge/Statescript/Properties/AppendResolver.cs @@ -0,0 +1,78 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves a nested array resolver with additional elements appended to the end. Each appended element is produced by +/// its own nested resolver, allowing constants, variables, or computed values to be added. +/// +public class AppendResolver : IArrayPropertyResolver +{ + private readonly IArrayPropertyResolver _source; + + private readonly IPropertyResolver[] _elements; + + /// + public Type ElementType { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The resolver providing the source array. + /// The nested resolvers producing the elements to append. Each must resolve to the source + /// element type. + public AppendResolver(IArrayPropertyResolver source, params IPropertyResolver[] elements) + { + ValidateElements(source.ElementType, elements); + _source = source; + _elements = elements; + ElementType = source.ElementType; + } + + /// + public Variant128[] ResolveArray(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + var result = new Variant128[values.Length + _elements.Length]; + Array.Copy(values, result, values.Length); + + for (int i = 0; i < _elements.Length; i++) + { + result[values.Length + i] = _elements[i].Resolve(graphContext); + } + + return result; + } + + private static void ValidateElements(Type elementType, IPropertyResolver[] elements) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(elements); +#else + if (elements is null) + { + throw new ArgumentNullException(nameof(elements)); + } +#endif + + for (int i = 0; i < elements.Length; i++) + { + IPropertyResolver? element = elements[i]; + + if (element is null) + { + throw new ArgumentException( + "AppendResolver does not allow null element resolvers.", + nameof(elements)); + } + + if (element.ValueType != elementType) + { + throw new ArgumentException( + $"AppendResolver element resolver at index {i} produces '{element.ValueType}', which does not " + + $"match the source element type '{elementType}'.", + nameof(elements)); + } + } + } +} diff --git a/Forge/Statescript/Properties/ArrayResolverUtils.cs b/Forge/Statescript/Properties/ArrayResolverUtils.cs new file mode 100644 index 0000000..a2a189d --- /dev/null +++ b/Forge/Statescript/Properties/ArrayResolverUtils.cs @@ -0,0 +1,125 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Shared utility methods for array resolvers: operand validation and numeric conversion of index/count operands. +/// +internal static class ArrayResolverUtils +{ + internal static IPropertyResolver ValidateNumericOperand( + string resolverName, + string parameterName, + IPropertyResolver operand) + { + if (!MathTypeUtils.IsNumericType(operand.ValueType)) + { + throw new ArgumentException( + $"{resolverName} requires {parameterName} to resolve to a numeric type. Got '{operand.ValueType}'.", + parameterName); + } + + return operand; + } + + internal static IPropertyResolver ValidateElementOperand( + string resolverName, + string parameterName, + Type elementType, + IPropertyResolver operand) + { + if (operand.ValueType != elementType) + { + throw new ArgumentException( + $"{resolverName} requires {parameterName} to resolve to the source element type '{elementType}'. " + + $"Got '{operand.ValueType}'.", + parameterName); + } + + return operand; + } + + internal static Type ValidateMatchingElementTypes( + string resolverName, + Type firstElementType, + Type secondElementType) + { + if (firstElementType != secondElementType) + { + throw new ArgumentException( + $"{resolverName} requires matching element types. Got '{firstElementType}' and " + + $"'{secondElementType}'."); + } + + return firstElementType; + } + + internal static Type ValidateNumericElementType(string resolverName, Type elementType) + { + if (!MathTypeUtils.IsNumericType(elementType)) + { + throw new ArgumentException( + $"{resolverName} requires a numeric element type. Got '{elementType}'."); + } + + return elementType; + } + + /// + /// Resolves a numeric operand (index or count) as an , truncating any fractional part. + /// + /// The graph context providing the runtime state. + /// The numeric resolver to evaluate. + /// The resolved value truncated to an . + internal static int ResolveInt(GraphContext graphContext, IPropertyResolver resolver) + { + return (int)ResolveAsDouble(resolver.ValueType, resolver.Resolve(graphContext)); + } + + /// + /// Converts a numeric variant to , extending + /// with support. + /// + /// The numeric type of the stored value. + /// The variant holding the value. + /// The value converted to . + internal static double ResolveAsDouble(Type type, Variant128 value) + { + if (type == typeof(decimal)) + { + return (double)value.AsDecimal(); + } + + return MathTypeUtils.ResolveAsDouble(type, value); + } + + /// + /// Produces the element indexes of a source array sorted by their pre-computed keys. The sort is stable: elements + /// with equal keys keep their original relative order. + /// + /// The sort key of each source element, by element index. + /// The ordering to apply. + /// The source indexes in sorted order. + internal static int[] SortIndexesByKey(double[] keys, SortDirection direction) + { + int[] indexes = new int[keys.Length]; + for (int i = 0; i < indexes.Length; i++) + { + indexes[i] = i; + } + + Array.Sort(indexes, (left, right) => + { + int comparison = keys[left].CompareTo(keys[right]); + + if (direction == SortDirection.Descending) + { + comparison = -comparison; + } + + return comparison != 0 ? comparison : left.CompareTo(right); + }); + + return indexes; + } +} diff --git a/Forge/Statescript/Properties/AverageResolver.cs b/Forge/Statescript/Properties/AverageResolver.cs new file mode 100644 index 0000000..d3c11f8 --- /dev/null +++ b/Forge/Statescript/Properties/AverageResolver.cs @@ -0,0 +1,77 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the arithmetic mean of all elements of a nested numeric array resolver. elements +/// average to , elements to , and all other +/// numeric element types to . +/// +/// +/// An empty array averages to zero. +/// +public class AverageResolver : IPropertyResolver +{ + private readonly IArrayPropertyResolver _source; + + private readonly Type _elementType; + + /// + public Type ValueType { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The resolver providing the source array. Must have a numeric element type. + public AverageResolver(IArrayPropertyResolver source) + { + _elementType = ArrayResolverUtils.ValidateNumericElementType(nameof(AverageResolver), source.ElementType); + _source = source; + + if (_elementType == typeof(float)) + { + ValueType = typeof(float); + } + else if (_elementType == typeof(decimal)) + { + ValueType = typeof(decimal); + } + else + { + ValueType = typeof(double); + } + } + + /// + public Variant128 Resolve(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + + if (values.Length == 0) + { + return default; + } + + if (ValueType == typeof(decimal)) + { + decimal decimalSum = 0m; + + for (int i = 0; i < values.Length; i++) + { + decimalSum += MathTypeUtils.ResolveAsDecimal(_elementType, values[i]); + } + + return new Variant128(decimalSum / values.Length); + } + + double sum = 0d; + + for (int i = 0; i < values.Length; i++) + { + sum += ArrayResolverUtils.ResolveAsDouble(_elementType, values[i]); + } + + double average = sum / values.Length; + return ValueType == typeof(float) ? new Variant128((float)average) : new Variant128(average); + } +} diff --git a/Forge/Statescript/Properties/ConcatResolver.cs b/Forge/Statescript/Properties/ConcatResolver.cs new file mode 100644 index 0000000..5cdcbd9 --- /dev/null +++ b/Forge/Statescript/Properties/ConcatResolver.cs @@ -0,0 +1,45 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the concatenation of two nested array resolvers, producing the elements of the first followed by the +/// elements of the second. +/// +/// The resolver providing the leading elements. +/// The resolver providing the trailing elements. Must share the first resolver's element type. +/// +public class ConcatResolver(IArrayPropertyResolver first, IArrayPropertyResolver second) : IArrayPropertyResolver +{ + private readonly IArrayPropertyResolver _first = first; + + private readonly IArrayPropertyResolver _second = second; + + /// + public Type ElementType { get; } = ArrayResolverUtils.ValidateMatchingElementTypes( + nameof(ConcatResolver), + first.ElementType, + second.ElementType); + + /// + public Variant128[] ResolveArray(GraphContext graphContext) + { + Variant128[] firstValues = _first.ResolveArray(graphContext); + Variant128[] secondValues = _second.ResolveArray(graphContext); + + if (firstValues.Length == 0) + { + return secondValues; + } + + if (secondValues.Length == 0) + { + return firstValues; + } + + var result = new Variant128[firstValues.Length + secondValues.Length]; + Array.Copy(firstValues, result, firstValues.Length); + Array.Copy(secondValues, 0, result, firstValues.Length, secondValues.Length); + return result; + } +} diff --git a/Forge/Statescript/Properties/ContainsResolver.cs b/Forge/Statescript/Properties/ContainsResolver.cs new file mode 100644 index 0000000..9f463eb --- /dev/null +++ b/Forge/Statescript/Properties/ContainsResolver.cs @@ -0,0 +1,43 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves a indicating whether a nested array resolver contains a given value. The value is itself +/// a nested resolver, allowing both constants and computed values. +/// +/// +/// Floating-point elements are compared exactly. +/// +/// The resolver providing the source array. +/// The resolver providing the value to search for. Must resolve to the source element type. +/// +public class ContainsResolver(IArrayPropertyResolver source, IPropertyResolver value) : IPropertyResolver +{ + private readonly IArrayPropertyResolver _source = source; + + private readonly IPropertyResolver _value = + ArrayResolverUtils.ValidateElementOperand(nameof(ContainsResolver), nameof(value), source.ElementType, value); + + private readonly Type _elementType = source.ElementType; + + /// + public Type ValueType => typeof(bool); + + /// + public Variant128 Resolve(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + Variant128 target = _value.Resolve(graphContext); + + for (int i = 0; i < values.Length; i++) + { + if (VariantEquality.AreEqual(values[i], target, _elementType)) + { + return new Variant128(true); + } + } + + return new Variant128(false); + } +} diff --git a/Forge/Statescript/Properties/CountResolver.cs b/Forge/Statescript/Properties/CountResolver.cs new file mode 100644 index 0000000..0a03cd7 --- /dev/null +++ b/Forge/Statescript/Properties/CountResolver.cs @@ -0,0 +1,81 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the number of elements of a nested array resolver, optionally counting only the elements that satisfy a +/// nested boolean predicate resolver. The predicate is evaluated once per element with the current element published +/// on the element stack, so it can read the element through the element resolvers. +/// +/// +/// The source may come from either lane: a value-typed array () or an object-backed +/// array (). +/// +public class CountResolver : IPropertyResolver +{ + private readonly IArrayPropertyResolver? _valueSource; + + private readonly IObjectArrayResolver? _objectSource; + + private readonly IPropertyResolver? _predicate; + + /// + public Type ValueType => typeof(int); + + /// + /// Initializes a new instance of the class over a value-typed source array. + /// + /// The resolver providing the source array. + /// The optional resolver evaluated per element. Must resolve to . + /// When omitted, all elements are counted. + public CountResolver(IArrayPropertyResolver source, IPropertyResolver? predicate = null) + : this(source, null, predicate) + { + } + + /// + /// Initializes a new instance of the class over an object-backed source array. + /// + /// The resolver providing the source array. + /// The optional resolver evaluated per element. Must resolve to . + /// When omitted, all elements are counted. + public CountResolver(IObjectArrayResolver source, IPropertyResolver? predicate = null) + : this(null, source, predicate) + { + } + + private CountResolver( + IArrayPropertyResolver? valueSource, + IObjectArrayResolver? objectSource, + IPropertyResolver? predicate) + { + _valueSource = valueSource; + _objectSource = objectSource; + _predicate = predicate is null + ? null + : BooleanTypeUtils.ValidateBoolOperand(nameof(CountResolver), nameof(predicate), predicate); + } + + /// + public Variant128 Resolve(GraphContext graphContext) + { + var sequence = ElementSequence.Resolve(graphContext, _valueSource, _objectSource); + + if (_predicate is null) + { + return new Variant128(sequence.Length); + } + + int count = 0; + + for (int i = 0; i < sequence.Length; i++) + { + if (ElementLambda.EvaluatePredicate(graphContext, _predicate, sequence.GetFrame(i))) + { + count++; + } + } + + return new Variant128(count); + } +} diff --git a/Forge/Statescript/Properties/DistinctResolver.cs b/Forge/Statescript/Properties/DistinctResolver.cs new file mode 100644 index 0000000..b85c537 --- /dev/null +++ b/Forge/Statescript/Properties/DistinctResolver.cs @@ -0,0 +1,55 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the distinct elements of a nested array resolver, keeping the first occurrence of each value and preserving +/// the original order. +/// +/// +/// Floating-point elements are compared exactly. +/// +/// The resolver providing the source array. +public class DistinctResolver(IArrayPropertyResolver source) : IArrayPropertyResolver +{ + private readonly IArrayPropertyResolver _source = source; + + /// + public Type ElementType { get; } = source.ElementType; + + /// + public Variant128[] ResolveArray(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + + if (values.Length <= 1) + { + return values; + } + + var result = new List(values.Length); + + for (int i = 0; i < values.Length; i++) + { + if (!ContainsValue(result, values[i])) + { + result.Add(values[i]); + } + } + + return [.. result]; + } + + private bool ContainsValue(List values, Variant128 value) + { + for (int i = 0; i < values.Count; i++) + { + if (VariantEquality.AreEqual(values[i], value, ElementType)) + { + return true; + } + } + + return false; + } +} diff --git a/Forge/Statescript/Properties/ElementAtResolver.cs b/Forge/Statescript/Properties/ElementAtResolver.cs new file mode 100644 index 0000000..0578e45 --- /dev/null +++ b/Forge/Statescript/Properties/ElementAtResolver.cs @@ -0,0 +1,32 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the element at a given index of a nested array resolver. The index is itself a nested numeric resolver, +/// allowing both constant indices and computed ones (e.g. a variable or an ). +/// +/// +/// If the index is out of range, a default (zero) is returned. Fractional index values are +/// truncated. +/// +/// The resolver providing the source array. +/// The resolver providing the zero-based element index. Must resolve to a numeric type. +public class ElementAtResolver(IArrayPropertyResolver source, IPropertyResolver index) : IPropertyResolver +{ + private readonly IArrayPropertyResolver _source = source; + + private readonly IPropertyResolver _index = + ArrayResolverUtils.ValidateNumericOperand(nameof(ElementAtResolver), nameof(index), index); + + /// + public Type ValueType { get; } = source.ElementType; + + /// + public Variant128 Resolve(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + int resolvedIndex = ArrayResolverUtils.ResolveInt(graphContext, _index); + return resolvedIndex >= 0 && resolvedIndex < values.Length ? values[resolvedIndex] : default; + } +} diff --git a/Forge/Statescript/Properties/ElementEntityResolver.cs b/Forge/Statescript/Properties/ElementEntityResolver.cs new file mode 100644 index 0000000..9354397 --- /dev/null +++ b/Forge/Statescript/Properties/ElementEntityResolver.cs @@ -0,0 +1,12 @@ +// Copyright © Gamesmiths Guild. + +using Gamesmiths.Forge.Core; + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the array element currently being iterated by an enclosing array resolver. As an +/// it composes with entity-aware resolvers such as and +/// , enabling per-element predicates and sort keys (e.g. "the current entity's health"). +/// +public class ElementEntityResolver : ElementResolver, IEntityResolver; diff --git a/Forge/Statescript/Properties/ElementIndexResolver.cs b/Forge/Statescript/Properties/ElementIndexResolver.cs new file mode 100644 index 0000000..bcc7991 --- /dev/null +++ b/Forge/Statescript/Properties/ElementIndexResolver.cs @@ -0,0 +1,22 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the zero-based index of the array element currently being iterated by an enclosing array resolver. Use +/// this inside nested "lambda" resolvers for index-aware predicates and projections. +/// +/// +/// If no array element is currently being iterated, a default (zero) is returned. +/// +public class ElementIndexResolver : IPropertyResolver +{ + /// + public Type ValueType => typeof(int); + + /// + public Variant128 Resolve(GraphContext graphContext) + { + return graphContext.TryGetCurrentElement(out ElementFrame frame) ? new Variant128(frame.Index) : default; + } +} diff --git a/Forge/Statescript/Properties/ElementLambda.cs b/Forge/Statescript/Properties/ElementLambda.cs new file mode 100644 index 0000000..6feb63c --- /dev/null +++ b/Forge/Statescript/Properties/ElementLambda.cs @@ -0,0 +1,52 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Shared helpers for evaluating nested "lambda" resolvers against a single array element. The element is published on +/// the element stack for the duration of the evaluation so that element resolvers +/// (, , ) can read +/// it back. +/// +internal static class ElementLambda +{ + internal static Variant128 Evaluate(GraphContext graphContext, IPropertyResolver lambda, in ElementFrame frame) + { + graphContext.PushElement(in frame); + + try + { + return lambda.Resolve(graphContext); + } + finally + { + graphContext.PopElement(); + } + } + + internal static object? Evaluate(GraphContext graphContext, IObjectResolver lambda, in ElementFrame frame) + { + graphContext.PushElement(in frame); + + try + { + return lambda.Resolve(graphContext); + } + finally + { + graphContext.PopElement(); + } + } + + internal static bool EvaluatePredicate(GraphContext graphContext, IPropertyResolver predicate, in ElementFrame frame) + { + return Evaluate(graphContext, predicate, in frame).AsBool(); + } + + internal static double EvaluateKey(GraphContext graphContext, IPropertyResolver keySelector, in ElementFrame frame) + { + return ArrayResolverUtils.ResolveAsDouble( + keySelector.ValueType, + Evaluate(graphContext, keySelector, in frame)); + } +} diff --git a/Forge/Statescript/Properties/ElementResolver.cs b/Forge/Statescript/Properties/ElementResolver.cs new file mode 100644 index 0000000..102dc21 --- /dev/null +++ b/Forge/Statescript/Properties/ElementResolver.cs @@ -0,0 +1,30 @@ +// Copyright © Gamesmiths Guild. + +using System.Diagnostics.CodeAnalysis; + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the object-backed array element currently being iterated by an enclosing array resolver (e.g. +/// , ). Use this inside nested "lambda" +/// resolvers as the stand-in for the lambda parameter. +/// +/// +/// If no array element is currently being iterated, or the current element is not compatible with +/// , is returned. +/// +/// The element type this resolver produces. +public class ElementResolver : ObjectResolver +{ + /// + [return: MaybeNull] + public override T Resolve(GraphContext graphContext) + { + if (graphContext.TryGetCurrentElement(out ElementFrame frame) && frame.ObjectValue is T typedValue) + { + return typedValue; + } + + return default; + } +} diff --git a/Forge/Statescript/Properties/ElementSequence.cs b/Forge/Statescript/Properties/ElementSequence.cs new file mode 100644 index 0000000..c109efa --- /dev/null +++ b/Forge/Statescript/Properties/ElementSequence.cs @@ -0,0 +1,51 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Internal view over a resolved source array from either the value lane () or the +/// object lane (). Used by resolvers whose source may come from either lane (e.g. +/// reductions and projections) to iterate elements uniformly as instances. +/// +internal readonly struct ElementSequence +{ + private readonly Variant128[]? _values; + + private readonly object?[]? _objects; + + internal Type ElementType { get; } + + internal int Length => _values?.Length ?? _objects!.Length; + + private ElementSequence(Variant128[]? values, object?[]? objects, Type elementType) + { + _values = values; + _objects = objects; + ElementType = elementType; + } + + internal static ElementSequence Resolve( + GraphContext graphContext, + IArrayPropertyResolver? valueSource, + IObjectArrayResolver? objectSource) + { + if (valueSource is not null) + { + return new ElementSequence(valueSource.ResolveArray(graphContext), null, valueSource.ElementType); + } + + return new ElementSequence(null, objectSource!.ResolveArray(graphContext), objectSource.ElementType); + } + + internal static Type GetElementType(IArrayPropertyResolver? valueSource, IObjectArrayResolver? objectSource) + { + return valueSource?.ElementType ?? objectSource!.ElementType; + } + + internal ElementFrame GetFrame(int index) + { + return _values is not null + ? new ElementFrame(_values[index], ElementType, index) + : new ElementFrame(_objects![index], ElementType, index); + } +} diff --git a/Forge/Statescript/Properties/ElementValueResolver.cs b/Forge/Statescript/Properties/ElementValueResolver.cs new file mode 100644 index 0000000..aea22b6 --- /dev/null +++ b/Forge/Statescript/Properties/ElementValueResolver.cs @@ -0,0 +1,26 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the value-typed array element currently being iterated by an enclosing array resolver (e.g. +/// , , ). Use this inside nested +/// "lambda" resolvers as the stand-in for the lambda parameter. +/// +/// +/// If no array element is currently being iterated (the resolver is evaluated outside an array operation), a default +/// (zero) is returned. +/// +/// The element type this resolver produces. Must match the iterated array's element type. +/// +public class ElementValueResolver(Type valueType) : IPropertyResolver +{ + /// + public Type ValueType { get; } = valueType; + + /// + public Variant128 Resolve(GraphContext graphContext) + { + return graphContext.TryGetCurrentElement(out ElementFrame frame) ? frame.Value : default; + } +} diff --git a/Forge/Statescript/Properties/EntityElementAtResolver.cs b/Forge/Statescript/Properties/EntityElementAtResolver.cs new file mode 100644 index 0000000..f36eb89 --- /dev/null +++ b/Forge/Statescript/Properties/EntityElementAtResolver.cs @@ -0,0 +1,16 @@ +// Copyright © Gamesmiths Guild. + +using Gamesmiths.Forge.Core; + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the at a given index of a nested entity array resolver. As an +/// it composes with entity-aware resolvers such as . +/// +/// The resolver providing the source entity array. +/// The resolver providing the zero-based element index. Must resolve to a numeric type. +public class EntityElementAtResolver(IObjectArrayResolver source, IPropertyResolver index) + : ObjectElementAtResolver(source, index), IEntityResolver +{ +} diff --git a/Forge/Statescript/Properties/EntityFirstResolver.cs b/Forge/Statescript/Properties/EntityFirstResolver.cs new file mode 100644 index 0000000..838f1df --- /dev/null +++ b/Forge/Statescript/Properties/EntityFirstResolver.cs @@ -0,0 +1,15 @@ +// Copyright © Gamesmiths Guild. + +using Gamesmiths.Forge.Core; + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the first of a nested entity array resolver. As an +/// it composes with entity-aware resolvers such as . +/// +/// The resolver providing the source entity array. +public class EntityFirstResolver(IObjectArrayResolver source) + : ObjectFirstResolver(source), IEntityResolver +{ +} diff --git a/Forge/Statescript/Properties/EntityLastResolver.cs b/Forge/Statescript/Properties/EntityLastResolver.cs new file mode 100644 index 0000000..bf1ea4e --- /dev/null +++ b/Forge/Statescript/Properties/EntityLastResolver.cs @@ -0,0 +1,15 @@ +// Copyright © Gamesmiths Guild. + +using Gamesmiths.Forge.Core; + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the last of a nested entity array resolver. As an +/// it composes with entity-aware resolvers such as . +/// +/// The resolver providing the source entity array. +public class EntityLastResolver(IObjectArrayResolver source) + : ObjectLastResolver(source), IEntityResolver +{ +} diff --git a/Forge/Statescript/Properties/ExceptResolver.cs b/Forge/Statescript/Properties/ExceptResolver.cs new file mode 100644 index 0000000..af8eedb --- /dev/null +++ b/Forge/Statescript/Properties/ExceptResolver.cs @@ -0,0 +1,63 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the elements of a nested array resolver that do not appear in a second nested array resolver, preserving +/// their original order. +/// +/// +/// Unlike LINQ's set-based Except, duplicates in the source are preserved unless they appear in +/// . Floating-point elements are compared exactly. +/// +/// The resolver providing the source array. +/// The resolver providing the elements to remove. Must share the source's element type. +public class ExceptResolver(IArrayPropertyResolver source, IArrayPropertyResolver other) : IArrayPropertyResolver +{ + private readonly IArrayPropertyResolver _source = source; + + private readonly IArrayPropertyResolver _other = other; + + /// + public Type ElementType { get; } = ArrayResolverUtils.ValidateMatchingElementTypes( + nameof(ExceptResolver), + source.ElementType, + other.ElementType); + + /// + public Variant128[] ResolveArray(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + Variant128[] excludedValues = _other.ResolveArray(graphContext); + + if (values.Length == 0 || excludedValues.Length == 0) + { + return values; + } + + var result = new List(values.Length); + + for (int i = 0; i < values.Length; i++) + { + if (!ContainsValue(excludedValues, values[i])) + { + result.Add(values[i]); + } + } + + return [.. result]; + } + + private bool ContainsValue(Variant128[] values, Variant128 value) + { + for (int i = 0; i < values.Length; i++) + { + if (VariantEquality.AreEqual(values[i], value, ElementType)) + { + return true; + } + } + + return false; + } +} diff --git a/Forge/Statescript/Properties/FirstResolver.cs b/Forge/Statescript/Properties/FirstResolver.cs new file mode 100644 index 0000000..c936f4a --- /dev/null +++ b/Forge/Statescript/Properties/FirstResolver.cs @@ -0,0 +1,25 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the first element of a nested array resolver. +/// +/// +/// If the source array is empty, a default (zero) is returned. +/// +/// The resolver providing the source array. +public class FirstResolver(IArrayPropertyResolver source) : IPropertyResolver +{ + private readonly IArrayPropertyResolver _source = source; + + /// + public Type ValueType { get; } = source.ElementType; + + /// + public Variant128 Resolve(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + return values.Length > 0 ? values[0] : default; + } +} diff --git a/Forge/Statescript/Properties/IndexOfResolver.cs b/Forge/Statescript/Properties/IndexOfResolver.cs new file mode 100644 index 0000000..d51fe70 --- /dev/null +++ b/Forge/Statescript/Properties/IndexOfResolver.cs @@ -0,0 +1,43 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the zero-based index of the first occurrence of a given value in a nested array resolver, or -1 when the +/// value is not present. The value is itself a nested resolver, allowing both constants and computed values. +/// +/// +/// Floating-point elements are compared exactly. +/// +/// The resolver providing the source array. +/// The resolver providing the value to search for. Must resolve to the source element type. +/// +public class IndexOfResolver(IArrayPropertyResolver source, IPropertyResolver value) : IPropertyResolver +{ + private readonly IArrayPropertyResolver _source = source; + + private readonly IPropertyResolver _value = + ArrayResolverUtils.ValidateElementOperand(nameof(IndexOfResolver), nameof(value), source.ElementType, value); + + private readonly Type _elementType = source.ElementType; + + /// + public Type ValueType => typeof(int); + + /// + public Variant128 Resolve(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + Variant128 target = _value.Resolve(graphContext); + + for (int i = 0; i < values.Length; i++) + { + if (VariantEquality.AreEqual(values[i], target, _elementType)) + { + return new Variant128(i); + } + } + + return new Variant128(-1); + } +} diff --git a/Forge/Statescript/Properties/LastResolver.cs b/Forge/Statescript/Properties/LastResolver.cs new file mode 100644 index 0000000..2839643 --- /dev/null +++ b/Forge/Statescript/Properties/LastResolver.cs @@ -0,0 +1,25 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the last element of a nested array resolver. +/// +/// +/// If the source array is empty, a default (zero) is returned. +/// +/// The resolver providing the source array. +public class LastResolver(IArrayPropertyResolver source) : IPropertyResolver +{ + private readonly IArrayPropertyResolver _source = source; + + /// + public Type ValueType { get; } = source.ElementType; + + /// + public Variant128 Resolve(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + return values.Length > 0 ? values[^1] : default; + } +} diff --git a/Forge/Statescript/Properties/MaxElementResolver.cs b/Forge/Statescript/Properties/MaxElementResolver.cs new file mode 100644 index 0000000..32d12da --- /dev/null +++ b/Forge/Statescript/Properties/MaxElementResolver.cs @@ -0,0 +1,48 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the largest element of a nested numeric array resolver. Unlike the binary , which +/// compares two operands, this resolver aggregates over an array. +/// +/// +/// The original element value is returned unchanged, so the result type matches the source element type. If the source +/// array is empty, a default (zero) is returned. Ties resolve to the first occurrence. +/// +/// The resolver providing the source array. Must have a numeric element type. +public class MaxElementResolver(IArrayPropertyResolver source) : IPropertyResolver +{ + private readonly IArrayPropertyResolver _source = source; + + /// + public Type ValueType { get; } = + ArrayResolverUtils.ValidateNumericElementType(nameof(MaxElementResolver), source.ElementType); + + /// + public Variant128 Resolve(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + + if (values.Length == 0) + { + return default; + } + + int bestIndex = 0; + double bestKey = ArrayResolverUtils.ResolveAsDouble(ValueType, values[0]); + + for (int i = 1; i < values.Length; i++) + { + double key = ArrayResolverUtils.ResolveAsDouble(ValueType, values[i]); + + if (key.CompareTo(bestKey) > 0) + { + bestIndex = i; + bestKey = key; + } + } + + return values[bestIndex]; + } +} diff --git a/Forge/Statescript/Properties/MinElementResolver.cs b/Forge/Statescript/Properties/MinElementResolver.cs new file mode 100644 index 0000000..e7501fe --- /dev/null +++ b/Forge/Statescript/Properties/MinElementResolver.cs @@ -0,0 +1,48 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the smallest element of a nested numeric array resolver. Unlike the binary , which +/// compares two operands, this resolver aggregates over an array. +/// +/// +/// The original element value is returned unchanged, so the result type matches the source element type. If the source +/// array is empty, a default (zero) is returned. Ties resolve to the first occurrence. +/// +/// The resolver providing the source array. Must have a numeric element type. +public class MinElementResolver(IArrayPropertyResolver source) : IPropertyResolver +{ + private readonly IArrayPropertyResolver _source = source; + + /// + public Type ValueType { get; } = + ArrayResolverUtils.ValidateNumericElementType(nameof(MinElementResolver), source.ElementType); + + /// + public Variant128 Resolve(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + + if (values.Length == 0) + { + return default; + } + + int bestIndex = 0; + double bestKey = ArrayResolverUtils.ResolveAsDouble(ValueType, values[0]); + + for (int i = 1; i < values.Length; i++) + { + double key = ArrayResolverUtils.ResolveAsDouble(ValueType, values[i]); + + if (key.CompareTo(bestKey) < 0) + { + bestIndex = i; + bestKey = key; + } + } + + return values[bestIndex]; + } +} diff --git a/Forge/Statescript/Properties/ObjectAppendResolver.cs b/Forge/Statescript/Properties/ObjectAppendResolver.cs new file mode 100644 index 0000000..faf9adc --- /dev/null +++ b/Forge/Statescript/Properties/ObjectAppendResolver.cs @@ -0,0 +1,64 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves a nested object-backed array resolver with additional elements appended to the end. Each appended element +/// is produced by its own nested object resolver, allowing stored references or computed ones to be added. +/// +/// The element type to read. +public class ObjectAppendResolver : ObjectArrayResolver +{ + private readonly IObjectArrayResolver _source; + + private readonly IObjectResolver[] _elements; + + /// + /// Initializes a new instance of the class. + /// + /// The resolver providing the source array. + /// The nested resolvers producing the elements to append. + public ObjectAppendResolver(IObjectArrayResolver source, params IObjectResolver[] elements) + { + ValidateElements(elements); + _source = source; + _elements = elements; + } + + /// + public override T[] ResolveArray(GraphContext graphContext) + { + T[] values = _source.ResolveArray(graphContext); + var result = new T[values.Length + _elements.Length]; + Array.Copy(values, result, values.Length); + + for (int i = 0; i < _elements.Length; i++) + { + result[values.Length + i] = _elements[i].Resolve(graphContext)!; + } + + return result; + } + + private static void ValidateElements(IObjectResolver[] elements) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(elements); +#else + if (elements is null) + { + throw new ArgumentNullException(nameof(elements)); + } +#endif + + for (int i = 0; i < elements.Length; i++) + { + if (elements[i] is null) + { + throw new ArgumentException( + "ObjectAppendResolver does not allow null element resolvers.", + nameof(elements)); + } + } + } +} diff --git a/Forge/Statescript/Properties/ObjectConcatResolver.cs b/Forge/Statescript/Properties/ObjectConcatResolver.cs new file mode 100644 index 0000000..668f55f --- /dev/null +++ b/Forge/Statescript/Properties/ObjectConcatResolver.cs @@ -0,0 +1,40 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the concatenation of two nested object-backed array resolvers, producing the elements of the first followed +/// by the elements of the second. +/// +/// The element type to read. +/// The resolver providing the leading elements. +/// The resolver providing the trailing elements. +public class ObjectConcatResolver(IObjectArrayResolver first, IObjectArrayResolver second) + : ObjectArrayResolver +{ + private readonly IObjectArrayResolver _first = first; + + private readonly IObjectArrayResolver _second = second; + + /// + public override T[] ResolveArray(GraphContext graphContext) + { + T[] firstValues = _first.ResolveArray(graphContext); + T[] secondValues = _second.ResolveArray(graphContext); + + if (firstValues.Length == 0) + { + return secondValues; + } + + if (secondValues.Length == 0) + { + return firstValues; + } + + var result = new T[firstValues.Length + secondValues.Length]; + Array.Copy(firstValues, result, firstValues.Length); + Array.Copy(secondValues, 0, result, firstValues.Length, secondValues.Length); + return result; + } +} diff --git a/Forge/Statescript/Properties/ObjectContainsResolver.cs b/Forge/Statescript/Properties/ObjectContainsResolver.cs new file mode 100644 index 0000000..b38f132 --- /dev/null +++ b/Forge/Statescript/Properties/ObjectContainsResolver.cs @@ -0,0 +1,40 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves a indicating whether a nested object-backed array resolver contains a given reference, +/// using reference identity (e.g. "is this entity already in the target list?"). +/// +/// +/// A search value matches stored elements. Combine with +/// when missing values must not count as a match. +/// +/// The resolver providing the source array. +/// The resolver providing the reference to search for. +public class ObjectContainsResolver(IObjectArrayResolver source, IObjectResolver value) : IPropertyResolver +{ + private readonly IObjectArrayResolver _source = source; + + private readonly IObjectResolver _value = value; + + /// + public Type ValueType => typeof(bool); + + /// + public Variant128 Resolve(GraphContext graphContext) + { + object?[] values = _source.ResolveArray(graphContext); + object? target = _value.Resolve(graphContext); + + for (int i = 0; i < values.Length; i++) + { + if (ReferenceEquals(values[i], target)) + { + return new Variant128(true); + } + } + + return new Variant128(false); + } +} diff --git a/Forge/Statescript/Properties/ObjectDistinctResolver.cs b/Forge/Statescript/Properties/ObjectDistinctResolver.cs new file mode 100644 index 0000000..48300ad --- /dev/null +++ b/Forge/Statescript/Properties/ObjectDistinctResolver.cs @@ -0,0 +1,51 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the distinct elements of a nested object-backed array resolver, keeping the first occurrence of each +/// reference and preserving the original order. Elements are matched by reference identity, making this useful to avoid +/// processing the same target twice. +/// +/// The element type to read. +/// The resolver providing the source array. +public class ObjectDistinctResolver(IObjectArrayResolver source) : ObjectArrayResolver +{ + private readonly IObjectArrayResolver _source = source; + + /// + public override T[] ResolveArray(GraphContext graphContext) + { + T[] values = _source.ResolveArray(graphContext); + + if (values.Length <= 1) + { + return values; + } + + var result = new List(values.Length); + + for (int i = 0; i < values.Length; i++) + { + if (!ContainsReference(result, values[i])) + { + result.Add(values[i]); + } + } + + return [.. result]; + } + + private static bool ContainsReference(List values, T value) + { + for (int i = 0; i < values.Count; i++) + { + if (ReferenceEquals(values[i], value)) + { + return true; + } + } + + return false; + } +} diff --git a/Forge/Statescript/Properties/ObjectElementAtResolver.cs b/Forge/Statescript/Properties/ObjectElementAtResolver.cs new file mode 100644 index 0000000..97b7012 --- /dev/null +++ b/Forge/Statescript/Properties/ObjectElementAtResolver.cs @@ -0,0 +1,32 @@ +// Copyright © Gamesmiths Guild. + +using System.Diagnostics.CodeAnalysis; + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the element at a given index of a nested object-backed array resolver. The index is itself a nested +/// numeric resolver, allowing both constant indices and computed ones. +/// +/// +/// If the index is out of range, is returned. Fractional index values are truncated. +/// +/// The element type to read. +/// The resolver providing the source array. +/// The resolver providing the zero-based element index. Must resolve to a numeric type. +public class ObjectElementAtResolver(IObjectArrayResolver source, IPropertyResolver index) : ObjectResolver +{ + private readonly IObjectArrayResolver _source = source; + + private readonly IPropertyResolver _index = + ArrayResolverUtils.ValidateNumericOperand(nameof(ObjectElementAtResolver), nameof(index), index); + + /// + [return: MaybeNull] + public override T Resolve(GraphContext graphContext) + { + T[] values = _source.ResolveArray(graphContext); + int resolvedIndex = ArrayResolverUtils.ResolveInt(graphContext, _index); + return resolvedIndex >= 0 && resolvedIndex < values.Length ? values[resolvedIndex] : default; + } +} diff --git a/Forge/Statescript/Properties/ObjectExceptResolver.cs b/Forge/Statescript/Properties/ObjectExceptResolver.cs new file mode 100644 index 0000000..9601d28 --- /dev/null +++ b/Forge/Statescript/Properties/ObjectExceptResolver.cs @@ -0,0 +1,59 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the elements of a nested object-backed array resolver that do not appear in a second nested object-backed +/// array resolver, preserving their original order. Elements are matched by reference identity. +/// +/// +/// Unlike LINQ's set-based Except, duplicates in the source are preserved unless they appear in +/// . +/// +/// The element type to read. +/// The resolver providing the source array. +/// The resolver providing the elements to remove. +public class ObjectExceptResolver(IObjectArrayResolver source, IObjectArrayResolver other) + : ObjectArrayResolver +{ + private readonly IObjectArrayResolver _source = source; + + private readonly IObjectArrayResolver _other = other; + + /// + public override T[] ResolveArray(GraphContext graphContext) + { + T[] values = _source.ResolveArray(graphContext); + T[] excludedValues = _other.ResolveArray(graphContext); + + if (values.Length == 0 || excludedValues.Length == 0) + { + return values; + } + + var result = new List(values.Length); + + for (int i = 0; i < values.Length; i++) + { + if (!ContainsReference(excludedValues, values[i])) + { + result.Add(values[i]); + } + } + + return [.. result]; + } + + private static bool ContainsReference(T[] values, T value) + { + for (int i = 0; i < values.Length; i++) + { + if (ReferenceEquals(values[i], value)) + { + return true; + } + } + + return false; + } +} diff --git a/Forge/Statescript/Properties/ObjectFirstResolver.cs b/Forge/Statescript/Properties/ObjectFirstResolver.cs new file mode 100644 index 0000000..88e7a05 --- /dev/null +++ b/Forge/Statescript/Properties/ObjectFirstResolver.cs @@ -0,0 +1,26 @@ +// Copyright © Gamesmiths Guild. + +using System.Diagnostics.CodeAnalysis; + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the first element of a nested object-backed array resolver. +/// +/// +/// If the source array is empty, is returned. +/// +/// The element type to read. +/// The resolver providing the source array. +public class ObjectFirstResolver(IObjectArrayResolver source) : ObjectResolver +{ + private readonly IObjectArrayResolver _source = source; + + /// + [return: MaybeNull] + public override T Resolve(GraphContext graphContext) + { + T[] values = _source.ResolveArray(graphContext); + return values.Length > 0 ? values[0] : default; + } +} diff --git a/Forge/Statescript/Properties/ObjectIndexOfResolver.cs b/Forge/Statescript/Properties/ObjectIndexOfResolver.cs new file mode 100644 index 0000000..6989b9c --- /dev/null +++ b/Forge/Statescript/Properties/ObjectIndexOfResolver.cs @@ -0,0 +1,39 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the zero-based index of the first occurrence of a given reference in a nested object-backed array resolver, +/// or -1 when the reference is not present. Elements are matched by reference identity. +/// +/// +/// A search value matches stored elements. +/// +/// The resolver providing the source array. +/// The resolver providing the reference to search for. +public class ObjectIndexOfResolver(IObjectArrayResolver source, IObjectResolver value) : IPropertyResolver +{ + private readonly IObjectArrayResolver _source = source; + + private readonly IObjectResolver _value = value; + + /// + public Type ValueType => typeof(int); + + /// + public Variant128 Resolve(GraphContext graphContext) + { + object?[] values = _source.ResolveArray(graphContext); + object? target = _value.Resolve(graphContext); + + for (int i = 0; i < values.Length; i++) + { + if (ReferenceEquals(values[i], target)) + { + return new Variant128(i); + } + } + + return new Variant128(-1); + } +} diff --git a/Forge/Statescript/Properties/ObjectLastResolver.cs b/Forge/Statescript/Properties/ObjectLastResolver.cs new file mode 100644 index 0000000..a1a6b31 --- /dev/null +++ b/Forge/Statescript/Properties/ObjectLastResolver.cs @@ -0,0 +1,26 @@ +// Copyright © Gamesmiths Guild. + +using System.Diagnostics.CodeAnalysis; + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the last element of a nested object-backed array resolver. +/// +/// +/// If the source array is empty, is returned. +/// +/// The element type to read. +/// The resolver providing the source array. +public class ObjectLastResolver(IObjectArrayResolver source) : ObjectResolver +{ + private readonly IObjectArrayResolver _source = source; + + /// + [return: MaybeNull] + public override T Resolve(GraphContext graphContext) + { + T[] values = _source.ResolveArray(graphContext); + return values.Length > 0 ? values[^1] : default; + } +} diff --git a/Forge/Statescript/Properties/ObjectOrderByResolver.cs b/Forge/Statescript/Properties/ObjectOrderByResolver.cs new file mode 100644 index 0000000..15d85a1 --- /dev/null +++ b/Forge/Statescript/Properties/ObjectOrderByResolver.cs @@ -0,0 +1,57 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the elements of a nested object-backed array resolver sorted by a nested numeric key selector resolver. +/// The key selector is evaluated once per element with the current element published on the element stack, so it can +/// read the element through (or for entity +/// arrays, composing with e.g. or for the key). The +/// sort is stable: elements with equal keys keep their original relative order. +/// +/// The element type to read. +/// The resolver providing the source array. +/// The resolver evaluated per element to produce its sort key. Must resolve to a numeric +/// type. +/// The ordering to apply. Defaults to . +public class ObjectOrderByResolver( + IObjectArrayResolver source, + IPropertyResolver keySelector, + SortDirection direction = SortDirection.Ascending) : ObjectArrayResolver +{ + private readonly IObjectArrayResolver _source = source; + + private readonly IPropertyResolver _keySelector = + ArrayResolverUtils.ValidateNumericOperand(nameof(ObjectOrderByResolver), nameof(keySelector), keySelector); + + private readonly SortDirection _direction = direction; + + /// + public override T[] ResolveArray(GraphContext graphContext) + { + T[] values = _source.ResolveArray(graphContext); + + if (values.Length <= 1) + { + return values; + } + + double[] keys = new double[values.Length]; + + for (int i = 0; i < values.Length; i++) + { + var frame = new ElementFrame(values[i], ElementType, i); + keys[i] = ElementLambda.EvaluateKey(graphContext, _keySelector, in frame); + } + + int[] sortedIndexes = ArrayResolverUtils.SortIndexesByKey(keys, _direction); + var result = new T[values.Length]; + + for (int i = 0; i < values.Length; i++) + { + result[i] = values[sortedIndexes[i]]; + } + + return result; + } +} diff --git a/Forge/Statescript/Properties/ObjectRemoveAtResolver.cs b/Forge/Statescript/Properties/ObjectRemoveAtResolver.cs new file mode 100644 index 0000000..92b4545 --- /dev/null +++ b/Forge/Statescript/Properties/ObjectRemoveAtResolver.cs @@ -0,0 +1,39 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves a nested object-backed array resolver with the element at a given index removed. The index is itself a +/// nested numeric resolver, allowing both constant indices and computed ones. +/// +/// +/// If the index is out of range, the source array is returned unchanged. Fractional index values are truncated. +/// +/// The element type to read. +/// The resolver providing the source array. +/// The resolver providing the zero-based index to remove. Must resolve to a numeric type. +public class ObjectRemoveAtResolver(IObjectArrayResolver source, IPropertyResolver index) + : ObjectArrayResolver +{ + private readonly IObjectArrayResolver _source = source; + + private readonly IPropertyResolver _index = + ArrayResolverUtils.ValidateNumericOperand(nameof(ObjectRemoveAtResolver), nameof(index), index); + + /// + public override T[] ResolveArray(GraphContext graphContext) + { + T[] values = _source.ResolveArray(graphContext); + int resolvedIndex = ArrayResolverUtils.ResolveInt(graphContext, _index); + + if (resolvedIndex < 0 || resolvedIndex >= values.Length) + { + return values; + } + + var result = new T[values.Length - 1]; + Array.Copy(values, result, resolvedIndex); + Array.Copy(values, resolvedIndex + 1, result, resolvedIndex, values.Length - resolvedIndex - 1); + return result; + } +} diff --git a/Forge/Statescript/Properties/ObjectReverseResolver.cs b/Forge/Statescript/Properties/ObjectReverseResolver.cs new file mode 100644 index 0000000..aaa594c --- /dev/null +++ b/Forge/Statescript/Properties/ObjectReverseResolver.cs @@ -0,0 +1,29 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the elements of a nested object-backed array resolver in reverse order. +/// +/// The element type to read. +/// The resolver providing the source array. +public class ObjectReverseResolver(IObjectArrayResolver source) : ObjectArrayResolver +{ + private readonly IObjectArrayResolver _source = source; + + /// + public override T[] ResolveArray(GraphContext graphContext) + { + T[] values = _source.ResolveArray(graphContext); + + if (values.Length <= 1) + { + return values; + } + + var result = new T[values.Length]; + Array.Copy(values, result, values.Length); + Array.Reverse(result); + return result; + } +} diff --git a/Forge/Statescript/Properties/ObjectSkipResolver.cs b/Forge/Statescript/Properties/ObjectSkipResolver.cs new file mode 100644 index 0000000..40c01c1 --- /dev/null +++ b/Forge/Statescript/Properties/ObjectSkipResolver.cs @@ -0,0 +1,37 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the elements of a nested object-backed array resolver after skipping the first N. The count is itself a +/// nested numeric resolver, allowing both constant and computed counts. +/// +/// +/// Counts are clamped to the source length; negative counts skip nothing. Fractional count values are truncated. +/// +/// The element type to read. +/// The resolver providing the source array. +/// The resolver providing the number of elements to skip. Must resolve to a numeric type. +public class ObjectSkipResolver(IObjectArrayResolver source, IPropertyResolver count) : ObjectArrayResolver +{ + private readonly IObjectArrayResolver _source = source; + + private readonly IPropertyResolver _count = + ArrayResolverUtils.ValidateNumericOperand(nameof(ObjectSkipResolver), nameof(count), count); + + /// + public override T[] ResolveArray(GraphContext graphContext) + { + T[] values = _source.ResolveArray(graphContext); + int resolvedCount = Math.Clamp(ArrayResolverUtils.ResolveInt(graphContext, _count), 0, values.Length); + + if (resolvedCount == 0) + { + return values; + } + + var result = new T[values.Length - resolvedCount]; + Array.Copy(values, resolvedCount, result, 0, result.Length); + return result; + } +} diff --git a/Forge/Statescript/Properties/ObjectTakeResolver.cs b/Forge/Statescript/Properties/ObjectTakeResolver.cs new file mode 100644 index 0000000..8c5b11f --- /dev/null +++ b/Forge/Statescript/Properties/ObjectTakeResolver.cs @@ -0,0 +1,38 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the first N elements of a nested object-backed array resolver. The count is itself a nested numeric +/// resolver, allowing both constant and computed counts. +/// +/// +/// Counts are clamped to the source length; negative counts produce an empty array. Fractional count values are +/// truncated. +/// +/// The element type to read. +/// The resolver providing the source array. +/// The resolver providing the number of elements to keep. Must resolve to a numeric type. +public class ObjectTakeResolver(IObjectArrayResolver source, IPropertyResolver count) : ObjectArrayResolver +{ + private readonly IObjectArrayResolver _source = source; + + private readonly IPropertyResolver _count = + ArrayResolverUtils.ValidateNumericOperand(nameof(ObjectTakeResolver), nameof(count), count); + + /// + public override T[] ResolveArray(GraphContext graphContext) + { + T[] values = _source.ResolveArray(graphContext); + int resolvedCount = Math.Clamp(ArrayResolverUtils.ResolveInt(graphContext, _count), 0, values.Length); + + if (resolvedCount == values.Length) + { + return values; + } + + var result = new T[resolvedCount]; + Array.Copy(values, result, resolvedCount); + return result; + } +} diff --git a/Forge/Statescript/Properties/ObjectWhereResolver.cs b/Forge/Statescript/Properties/ObjectWhereResolver.cs new file mode 100644 index 0000000..27bac96 --- /dev/null +++ b/Forge/Statescript/Properties/ObjectWhereResolver.cs @@ -0,0 +1,43 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the elements of a nested object-backed array resolver that satisfy a nested boolean predicate resolver, +/// preserving their original order. The predicate is evaluated once per element with the current element published on +/// the element stack, so it can read the element through (or +/// for entity arrays). +/// +/// +/// To remove matching elements instead, wrap the predicate in a . +/// +/// The element type to read. +/// The resolver providing the source array. +/// The resolver evaluated per element. Must resolve to . +public class ObjectWhereResolver(IObjectArrayResolver source, IPropertyResolver predicate) + : ObjectArrayResolver +{ + private readonly IObjectArrayResolver _source = source; + + private readonly IPropertyResolver _predicate = + BooleanTypeUtils.ValidateBoolOperand(nameof(ObjectWhereResolver), nameof(predicate), predicate); + + /// + public override T[] ResolveArray(GraphContext graphContext) + { + T[] values = _source.ResolveArray(graphContext); + var result = new List(values.Length); + + for (int i = 0; i < values.Length; i++) + { + var frame = new ElementFrame(values[i], ElementType, i); + + if (ElementLambda.EvaluatePredicate(graphContext, _predicate, in frame)) + { + result.Add(values[i]); + } + } + + return [.. result]; + } +} diff --git a/Forge/Statescript/Properties/OrderByResolver.cs b/Forge/Statescript/Properties/OrderByResolver.cs new file mode 100644 index 0000000..94a5526 --- /dev/null +++ b/Forge/Statescript/Properties/OrderByResolver.cs @@ -0,0 +1,58 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the elements of a nested array resolver sorted by a nested numeric key selector resolver. The key selector +/// is evaluated once per element with the current element published on the element stack, so it can read the element +/// through . The sort is stable: elements with equal keys keep their original +/// relative order. +/// +/// The resolver providing the source array. +/// The resolver evaluated per element to produce its sort key. Must resolve to a numeric +/// type. +/// The ordering to apply. Defaults to . +public class OrderByResolver( + IArrayPropertyResolver source, + IPropertyResolver keySelector, + SortDirection direction = SortDirection.Ascending) : IArrayPropertyResolver +{ + private readonly IArrayPropertyResolver _source = source; + + private readonly IPropertyResolver _keySelector = + ArrayResolverUtils.ValidateNumericOperand(nameof(OrderByResolver), nameof(keySelector), keySelector); + + private readonly SortDirection _direction = direction; + + /// + public Type ElementType { get; } = source.ElementType; + + /// + public Variant128[] ResolveArray(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + + if (values.Length <= 1) + { + return values; + } + + double[] keys = new double[values.Length]; + + for (int i = 0; i < values.Length; i++) + { + var frame = new ElementFrame(values[i], ElementType, i); + keys[i] = ElementLambda.EvaluateKey(graphContext, _keySelector, in frame); + } + + int[] sortedIndexes = ArrayResolverUtils.SortIndexesByKey(keys, _direction); + var result = new Variant128[values.Length]; + + for (int i = 0; i < values.Length; i++) + { + result[i] = values[sortedIndexes[i]]; + } + + return result; + } +} diff --git a/Forge/Statescript/Properties/RemoveAtResolver.cs b/Forge/Statescript/Properties/RemoveAtResolver.cs new file mode 100644 index 0000000..dbe7b7f --- /dev/null +++ b/Forge/Statescript/Properties/RemoveAtResolver.cs @@ -0,0 +1,40 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves a nested array resolver with the element at a given index removed. The index is itself a nested numeric +/// resolver, allowing both constant indices and computed ones. +/// +/// +/// If the index is out of range, the source array is returned unchanged. Fractional index values are truncated. +/// +/// The resolver providing the source array. +/// The resolver providing the zero-based index to remove. Must resolve to a numeric type. +public class RemoveAtResolver(IArrayPropertyResolver source, IPropertyResolver index) : IArrayPropertyResolver +{ + private readonly IArrayPropertyResolver _source = source; + + private readonly IPropertyResolver _index = + ArrayResolverUtils.ValidateNumericOperand(nameof(RemoveAtResolver), nameof(index), index); + + /// + public Type ElementType { get; } = source.ElementType; + + /// + public Variant128[] ResolveArray(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + int resolvedIndex = ArrayResolverUtils.ResolveInt(graphContext, _index); + + if (resolvedIndex < 0 || resolvedIndex >= values.Length) + { + return values; + } + + var result = new Variant128[values.Length - 1]; + Array.Copy(values, result, resolvedIndex); + Array.Copy(values, resolvedIndex + 1, result, resolvedIndex, values.Length - resolvedIndex - 1); + return result; + } +} diff --git a/Forge/Statescript/Properties/ReverseResolver.cs b/Forge/Statescript/Properties/ReverseResolver.cs new file mode 100644 index 0000000..fdf9bf2 --- /dev/null +++ b/Forge/Statescript/Properties/ReverseResolver.cs @@ -0,0 +1,31 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the elements of a nested array resolver in reverse order. +/// +/// The resolver providing the source array. +public class ReverseResolver(IArrayPropertyResolver source) : IArrayPropertyResolver +{ + private readonly IArrayPropertyResolver _source = source; + + /// + public Type ElementType { get; } = source.ElementType; + + /// + public Variant128[] ResolveArray(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + + if (values.Length <= 1) + { + return values; + } + + var result = new Variant128[values.Length]; + Array.Copy(values, result, values.Length); + Array.Reverse(result); + return result; + } +} diff --git a/Forge/Statescript/Properties/SelectObjectResolver.cs b/Forge/Statescript/Properties/SelectObjectResolver.cs new file mode 100644 index 0000000..38074f5 --- /dev/null +++ b/Forge/Statescript/Properties/SelectObjectResolver.cs @@ -0,0 +1,70 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves an object-backed array by evaluating a nested object projection resolver for each element of a nested +/// source array, like a LINQ Select producing references. The projection is evaluated once per element with +/// the current element published on the element stack, so it can read the element through +/// (value-typed sources) or +/// / (object-backed sources). +/// +/// +/// The source may come from either lane: a value-typed array () or an object-backed +/// array (). +/// +/// The element type produced by the projection. +public class SelectObjectResolver : ObjectArrayResolver +{ + private readonly IArrayPropertyResolver? _valueSource; + + private readonly IObjectArrayResolver? _objectSource; + + private readonly IObjectResolver _projection; + + /// + /// Initializes a new instance of the class over a value-typed source + /// array. + /// + /// The resolver providing the source array. + /// The resolver evaluated per element to produce the projected value. + public SelectObjectResolver(IArrayPropertyResolver source, IObjectResolver projection) + : this(source, null, projection) + { + } + + /// + /// Initializes a new instance of the class over an object-backed + /// source array. + /// + /// The resolver providing the source array. + /// The resolver evaluated per element to produce the projected value. + public SelectObjectResolver(IObjectArrayResolver source, IObjectResolver projection) + : this(null, source, projection) + { + } + + private SelectObjectResolver( + IArrayPropertyResolver? valueSource, + IObjectArrayResolver? objectSource, + IObjectResolver projection) + { + _valueSource = valueSource; + _objectSource = objectSource; + _projection = projection; + } + + /// + public override TResult[] ResolveArray(GraphContext graphContext) + { + var sequence = ElementSequence.Resolve(graphContext, _valueSource, _objectSource); + var values = new TResult[sequence.Length]; + + for (int i = 0; i < sequence.Length; i++) + { + values[i] = (TResult)ElementLambda.Evaluate(graphContext, _projection, sequence.GetFrame(i))!; + } + + return values; + } +} diff --git a/Forge/Statescript/Properties/SelectResolver.cs b/Forge/Statescript/Properties/SelectResolver.cs new file mode 100644 index 0000000..4351a51 --- /dev/null +++ b/Forge/Statescript/Properties/SelectResolver.cs @@ -0,0 +1,71 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves an array by evaluating a nested projection resolver for each element of a nested source array, like a LINQ +/// Select. The projection is evaluated once per element with the current element published on the element stack, +/// so it can read the element through (value-typed sources) or +/// / (object-backed sources). +/// +/// +/// The source may come from either lane: a value-typed array () or an +/// object-backed array (), enabling projections such as "the health of each entity +/// in the array". The resulting element type is the projection's value type. +/// +public class SelectResolver : IArrayPropertyResolver +{ + private readonly IArrayPropertyResolver? _valueSource; + + private readonly IObjectArrayResolver? _objectSource; + + private readonly IPropertyResolver _projection; + + /// + public Type ElementType { get; } + + /// + /// Initializes a new instance of the class over a value-typed source array. + /// + /// The resolver providing the source array. + /// The resolver evaluated per element to produce the projected value. + public SelectResolver(IArrayPropertyResolver source, IPropertyResolver projection) + : this(source, null, projection) + { + } + + /// + /// Initializes a new instance of the class over an object-backed source array. + /// + /// The resolver providing the source array. + /// The resolver evaluated per element to produce the projected value. + public SelectResolver(IObjectArrayResolver source, IPropertyResolver projection) + : this(null, source, projection) + { + } + + private SelectResolver( + IArrayPropertyResolver? valueSource, + IObjectArrayResolver? objectSource, + IPropertyResolver projection) + { + _valueSource = valueSource; + _objectSource = objectSource; + _projection = projection; + ElementType = projection.ValueType; + } + + /// + public Variant128[] ResolveArray(GraphContext graphContext) + { + var sequence = ElementSequence.Resolve(graphContext, _valueSource, _objectSource); + var values = new Variant128[sequence.Length]; + + for (int i = 0; i < sequence.Length; i++) + { + values[i] = ElementLambda.Evaluate(graphContext, _projection, sequence.GetFrame(i)); + } + + return values; + } +} diff --git a/Forge/Statescript/Properties/SkipResolver.cs b/Forge/Statescript/Properties/SkipResolver.cs new file mode 100644 index 0000000..03ed42c --- /dev/null +++ b/Forge/Statescript/Properties/SkipResolver.cs @@ -0,0 +1,39 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the elements of a nested array resolver after skipping the first N. The count is itself a nested numeric +/// resolver, allowing both constant and computed counts. +/// +/// +/// Counts are clamped to the source length; negative counts skip nothing. Fractional count values are truncated. +/// +/// The resolver providing the source array. +/// The resolver providing the number of elements to skip. Must resolve to a numeric type. +public class SkipResolver(IArrayPropertyResolver source, IPropertyResolver count) : IArrayPropertyResolver +{ + private readonly IArrayPropertyResolver _source = source; + + private readonly IPropertyResolver _count = + ArrayResolverUtils.ValidateNumericOperand(nameof(SkipResolver), nameof(count), count); + + /// + public Type ElementType { get; } = source.ElementType; + + /// + public Variant128[] ResolveArray(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + int resolvedCount = Math.Clamp(ArrayResolverUtils.ResolveInt(graphContext, _count), 0, values.Length); + + if (resolvedCount == 0) + { + return values; + } + + var result = new Variant128[values.Length - resolvedCount]; + Array.Copy(values, resolvedCount, result, 0, result.Length); + return result; + } +} diff --git a/Forge/Statescript/Properties/SortDirection.cs b/Forge/Statescript/Properties/SortDirection.cs new file mode 100644 index 0000000..d1faeca --- /dev/null +++ b/Forge/Statescript/Properties/SortDirection.cs @@ -0,0 +1,20 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Specifies the ordering applied by sorting resolvers such as and +/// . +/// +public enum SortDirection +{ + /// + /// Elements are ordered from the smallest key to the largest key. + /// + Ascending = 0, + + /// + /// Elements are ordered from the largest key to the smallest key. + /// + Descending = 1, +} diff --git a/Forge/Statescript/Properties/SumResolver.cs b/Forge/Statescript/Properties/SumResolver.cs new file mode 100644 index 0000000..a6988c9 --- /dev/null +++ b/Forge/Statescript/Properties/SumResolver.cs @@ -0,0 +1,74 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the sum of all elements of a nested numeric array resolver. The result type follows the standard numeric +/// promotion rules (e.g. elements sum to , elements +/// to ). +/// +/// +/// An empty array sums to zero. +/// +public class SumResolver : IPropertyResolver +{ + private readonly IArrayPropertyResolver _source; + + private readonly Type _elementType; + + /// + public Type ValueType { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The resolver providing the source array. Must have a numeric element type. + public SumResolver(IArrayPropertyResolver source) + { + _elementType = ArrayResolverUtils.ValidateNumericElementType(nameof(SumResolver), source.ElementType); + _source = source; + ValueType = MathTypeUtils.PromoteNumericTypes(_elementType, _elementType); + } + + /// + public Variant128 Resolve(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + + if (ValueType == typeof(decimal)) + { + decimal decimalSum = 0m; + + for (int i = 0; i < values.Length; i++) + { + decimalSum += MathTypeUtils.ResolveAsDecimal(_elementType, values[i]); + } + + return new Variant128(decimalSum); + } + + double sum = 0d; + + for (int i = 0; i < values.Length; i++) + { + sum += ArrayResolverUtils.ResolveAsDouble(_elementType, values[i]); + } + + if (ValueType == typeof(int)) + { + return new Variant128((int)sum); + } + + if (ValueType == typeof(long)) + { + return new Variant128((long)sum); + } + + if (ValueType == typeof(float)) + { + return new Variant128((float)sum); + } + + return new Variant128(sum); + } +} diff --git a/Forge/Statescript/Properties/TakeResolver.cs b/Forge/Statescript/Properties/TakeResolver.cs new file mode 100644 index 0000000..8247d38 --- /dev/null +++ b/Forge/Statescript/Properties/TakeResolver.cs @@ -0,0 +1,40 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the first N elements of a nested array resolver. The count is itself a nested numeric resolver, allowing +/// both constant and computed counts. +/// +/// +/// Counts are clamped to the source length; negative counts produce an empty array. Fractional count values are +/// truncated. +/// +/// The resolver providing the source array. +/// The resolver providing the number of elements to keep. Must resolve to a numeric type. +public class TakeResolver(IArrayPropertyResolver source, IPropertyResolver count) : IArrayPropertyResolver +{ + private readonly IArrayPropertyResolver _source = source; + + private readonly IPropertyResolver _count = + ArrayResolverUtils.ValidateNumericOperand(nameof(TakeResolver), nameof(count), count); + + /// + public Type ElementType { get; } = source.ElementType; + + /// + public Variant128[] ResolveArray(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + int resolvedCount = Math.Clamp(ArrayResolverUtils.ResolveInt(graphContext, _count), 0, values.Length); + + if (resolvedCount == values.Length) + { + return values; + } + + var result = new Variant128[resolvedCount]; + Array.Copy(values, result, resolvedCount); + return result; + } +} diff --git a/Forge/Statescript/Properties/VariantEquality.cs b/Forge/Statescript/Properties/VariantEquality.cs new file mode 100644 index 0000000..d4ea6e6 --- /dev/null +++ b/Forge/Statescript/Properties/VariantEquality.cs @@ -0,0 +1,109 @@ +// Copyright © Gamesmiths Guild. + +using System.Numerics; + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Shared helper for comparing two values of a known element type. Used by array resolvers +/// that need value equality (contains, index-of, distinct, except). Floating-point values are compared exactly. +/// +internal static class VariantEquality +{ + internal static bool AreEqual(Variant128 left, Variant128 right, Type type) + { + if (type == typeof(bool)) + { + return left.AsBool() == right.AsBool(); + } + + if (type == typeof(byte)) + { + return left.AsByte() == right.AsByte(); + } + + if (type == typeof(sbyte)) + { + return left.AsSByte() == right.AsSByte(); + } + + if (type == typeof(char)) + { + return left.AsChar() == right.AsChar(); + } + + if (type == typeof(decimal)) + { + return left.AsDecimal() == right.AsDecimal(); + } + +#pragma warning disable S1244 // Floating point numbers should not be tested for equality + if (type == typeof(double)) + { + return left.AsDouble() == right.AsDouble(); + } + + if (type == typeof(float)) + { + return left.AsFloat() == right.AsFloat(); + } +#pragma warning restore S1244 // Floating point numbers should not be tested for equality + + if (type == typeof(int)) + { + return left.AsInt() == right.AsInt(); + } + + if (type == typeof(uint)) + { + return left.AsUInt() == right.AsUInt(); + } + + if (type == typeof(long)) + { + return left.AsLong() == right.AsLong(); + } + + if (type == typeof(ulong)) + { + return left.AsULong() == right.AsULong(); + } + + if (type == typeof(short)) + { + return left.AsShort() == right.AsShort(); + } + + if (type == typeof(ushort)) + { + return left.AsUShort() == right.AsUShort(); + } + + if (type == typeof(Vector2)) + { + return left.AsVector2() == right.AsVector2(); + } + + if (type == typeof(Vector3)) + { + return left.AsVector3() == right.AsVector3(); + } + + if (type == typeof(Vector4)) + { + return left.AsVector4() == right.AsVector4(); + } + + if (type == typeof(Plane)) + { + return left.AsPlane() == right.AsPlane(); + } + + if (type == typeof(Quaternion)) + { + return left.AsQuaternion() == right.AsQuaternion(); + } + + throw new ArgumentException($"VariantEquality does not support element type '{type}'."); + } +} diff --git a/Forge/Statescript/Properties/WhereResolver.cs b/Forge/Statescript/Properties/WhereResolver.cs new file mode 100644 index 0000000..2c425fc --- /dev/null +++ b/Forge/Statescript/Properties/WhereResolver.cs @@ -0,0 +1,44 @@ +// Copyright © Gamesmiths Guild. + +namespace Gamesmiths.Forge.Statescript.Properties; + +/// +/// Resolves the elements of a nested array resolver that satisfy a nested boolean predicate resolver, preserving their +/// original order. The predicate is evaluated once per element with the current element published on the element stack, +/// so it can read the element through (and its index through +/// ). +/// +/// +/// To remove matching elements instead, wrap the predicate in a . +/// +/// The resolver providing the source array. +/// The resolver evaluated per element. Must resolve to . +public class WhereResolver(IArrayPropertyResolver source, IPropertyResolver predicate) : IArrayPropertyResolver +{ + private readonly IArrayPropertyResolver _source = source; + + private readonly IPropertyResolver _predicate = + BooleanTypeUtils.ValidateBoolOperand(nameof(WhereResolver), nameof(predicate), predicate); + + /// + public Type ElementType { get; } = source.ElementType; + + /// + public Variant128[] ResolveArray(GraphContext graphContext) + { + Variant128[] values = _source.ResolveArray(graphContext); + var result = new List(values.Length); + + for (int i = 0; i < values.Length; i++) + { + var frame = new ElementFrame(values[i], ElementType, i); + + if (ElementLambda.EvaluatePredicate(graphContext, _predicate, in frame)) + { + result.Add(values[i]); + } + } + + return [.. result]; + } +} diff --git a/docs/statescript/resolvers/README.md b/docs/statescript/resolvers/README.md index a4a19d1..cfb8891 100644 --- a/docs/statescript/resolvers/README.md +++ b/docs/statescript/resolvers/README.md @@ -83,6 +83,63 @@ regular node-bindable properties. | [IsValidResolver](is-valid-resolver.md) | `bool` | Checks whether an object-backed resolver produces a valid (non-null) value. | | [ObjectEqualsResolver](object-equals-resolver.md) | `bool` | Checks whether two object-backed resolvers produce the same instance (reference identity). | +--- + +## Array Operations + +LINQ-inspired resolvers for building array pipelines (filter → sort → take, projections, reductions). Most operations ship in two variants that share a doc page: a value-lane resolver for `Variant128` arrays and an object-lane `Object*Resolver` for reference arrays. Entity-flavored helpers (`Entit*Resolver`) implement `IEntityResolver` so their result plugs into `AttributeResolver`, `TagQueryResolver`, and friends. + +### Element (Lambda) Resolvers + +Operations that take a nested predicate, key selector, or projection evaluate it once per element with the current element published on the graph context. These resolvers are the "lambda parameter", they read the current element back inside that nested resolver. + +| Resolver | Output Type | Description | +|----------|-------------|-------------| +| [ElementEntityResolver](element-entity-resolver.md) | `IForgeEntity?` | Reads the iterated entity; composes with entity-aware resolvers for per-element keys. | +| [ElementIndexResolver](element-index-resolver.md) | `int` | Reads the zero-based index of the element currently being iterated. | +| [ElementResolver<T>](element-resolver.md) | `T?` | Reads the object-backed element currently being iterated. | +| [ElementValueResolver](element-value-resolver.md) | *(configured)* | Reads the value-typed element currently being iterated. | + +### Element Access + +| Resolver | Output Type | Description | +|----------|-------------|-------------| +| [ElementAtResolver](element-at-resolver.md) | *(element type)* | Reads the element at a resolved index. Object/entity variants: `ObjectElementAtResolver`, `EntityElementAtResolver`. | +| [FirstResolver](first-resolver.md) | *(element type)* | Reads the first element. Object/entity variants: `ObjectFirstResolver`, `EntityFirstResolver`. | +| [LastResolver](last-resolver.md) | *(element type)* | Reads the last element. Object/entity variants: `ObjectLastResolver`, `EntityLastResolver`. | + +### Transformation + +| Resolver | Output Type | Description | +|----------|-------------|-------------| +| [AppendResolver](append-resolver.md) | *(element array)* | Appends nested-resolver elements to the end. Object variant: `ObjectAppendResolver`. | +| [ConcatResolver](concat-resolver.md) | *(element array)* | Concatenates two arrays. Object variant: `ObjectConcatResolver`. | +| [DistinctResolver](distinct-resolver.md) | *(element array)* | De-duplicates, keeping first occurrences. Object variant: `ObjectDistinctResolver`. | +| [ExceptResolver](except-resolver.md) | *(element array)* | Removes the elements found in another array. Object variant: `ObjectExceptResolver`. | +| [OrderByResolver](order-by-resolver.md) | *(element array)* | Stable-sorts elements by a nested numeric key selector. Object variant: `ObjectOrderByResolver`. | +| [RemoveAtResolver](remove-at-resolver.md) | *(element array)* | Removes the element at a resolved index. Object variant: `ObjectRemoveAtResolver`. | +| [ReverseResolver](reverse-resolver.md) | *(element array)* | Reverses the element order. Object variant: `ObjectReverseResolver`. | +| [SelectResolver](select-resolver.md) | *(projected array)* | Projects each element through a nested resolver (either source lane). Object-producing variant: `SelectObjectResolver`. | +| [SkipResolver](skip-resolver.md) | *(element array)* | Drops the first N elements. Object variant: `ObjectSkipResolver`. | +| [TakeResolver](take-resolver.md) | *(element array)* | Keeps the first N elements. Object variant: `ObjectTakeResolver`. | +| [WhereResolver](where-resolver.md) | *(element array)* | Keeps the elements matching a nested boolean predicate. Object variant: `ObjectWhereResolver`. | + +### Reductions and Aggregation + +| Resolver | Output Type | Description | +|----------|-------------|-------------| +| [AllResolver](all-resolver.md) | `bool` | Checks whether every element matches a nested predicate (either source lane). | +| [AnyResolver](any-resolver.md) | `bool` | Checks whether any element exists or matches a nested predicate (either source lane). | +| [AverageResolver](average-resolver.md) | `double`/`float`/`decimal` | Computes the arithmetic mean of a numeric array. | +| [ContainsResolver](contains-resolver.md) | `bool` | Checks whether the array contains a resolved value. Object variant: `ObjectContainsResolver`. | +| [CountResolver](count-resolver.md) | `int` | Counts elements, optionally only those matching a nested predicate (either source lane). | +| [IndexOfResolver](index-of-resolver.md) | `int` | Finds the index of the first occurrence of a resolved value, or -1. Object variant: `ObjectIndexOfResolver`. | +| [MaxElementResolver](max-element-resolver.md) | *(element type)* | Returns the largest element of a numeric array. | +| [MinElementResolver](min-element-resolver.md) | *(element type)* | Returns the smallest element of a numeric array. | +| [SumResolver](sum-resolver.md) | *(promoted numeric)* | Adds up all elements of a numeric array. | + +--- + ## Boolean Expressions | Resolver | Output Type | Description | diff --git a/docs/statescript/resolvers/all-resolver.md b/docs/statescript/resolvers/all-resolver.md new file mode 100644 index 0000000..4379536 --- /dev/null +++ b/docs/statescript/resolvers/all-resolver.md @@ -0,0 +1,52 @@ +# AllResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.AllResolver` +> **Output Type:** `bool` + +Checks whether every element of a nested array resolver satisfies a nested boolean predicate resolver, a LINQ `All`, answering questions like "are all targets dead?". The source may come from either lane. + +## Constructor + +```csharp +new AllResolver(source, predicate) +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` or `IObjectArrayResolver` | The resolver providing the source array (either lane). | +| predicate | `IPropertyResolver` | Evaluated per element with the element published on the element stack. Must resolve to `bool`. | + +## Behavior + +- Returns `false` at the first element for which the predicate resolves to `false` (evaluation stops there). +- Empty or missing sources resolve to `true` (vacuous truth, matching LINQ). +- Throws `ArgumentException` at construction when the predicate does not resolve to `bool`. + +## Usage + +```csharp +new AllResolver( + new ArrayVariableResolver("charges", typeof(int)), + new ComparisonResolver( + new ElementValueResolver(typeof(int)), + ComparisonOperation.GreaterThan, + new VariantResolver(new Variant128(0), typeof(int)))) +``` + +## Composition + +```csharp +// "Have all marked targets been reduced to zero health?" +new AllResolver( + new EntityArrayVariableResolver("markedTargets"), + new ComparisonResolver( + new AttributeResolver("CombatAttributeSet.Health", new ElementEntityResolver()), + ComparisonOperation.LessThanOrEqual, + new VariantResolver(new Variant128(0), typeof(int)))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [AnyResolver](any-resolver.md) +- [CountResolver](count-resolver.md) diff --git a/docs/statescript/resolvers/any-resolver.md b/docs/statescript/resolvers/any-resolver.md new file mode 100644 index 0000000..64d3343 --- /dev/null +++ b/docs/statescript/resolvers/any-resolver.md @@ -0,0 +1,48 @@ +# AnyResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.AnyResolver` +> **Output Type:** `bool` + +Checks whether a nested array resolver produces any elements, optionally testing them against a nested boolean predicate resolver, a LINQ `Any`, answering questions like "is any enemy in range?". The source may come from either lane. + +## Constructors + +```csharp +new AnyResolver(source) // any element at all? +new AnyResolver(source, predicate) // any element matching? +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` or `IObjectArrayResolver` | The resolver providing the source array (either lane). | +| predicate | `IPropertyResolver` | Optional. Evaluated per element with the element published on the element stack. Must resolve to `bool`. | + +## Behavior + +- Without a predicate, returns `true` when the array is not empty. +- With a predicate, returns `true` at the first element for which it resolves to `true` (evaluation stops there). +- Empty or missing sources resolve to `false`. + +## Usage + +```csharp +new AnyResolver(new EntityArrayVariableResolver("nearbyEntities")) +``` + +## Composition + +```csharp +// "Is any target below 25% health?" +new AnyResolver( + new EntityArrayVariableResolver("targets"), + new ComparisonResolver( + new AttributeResolver("CombatAttributeSet.Health", new ElementEntityResolver()), + ComparisonOperation.LessThan, + new VariantResolver(new Variant128(25), typeof(int)))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [AllResolver](all-resolver.md) +- [CountResolver](count-resolver.md) diff --git a/docs/statescript/resolvers/append-resolver.md b/docs/statescript/resolvers/append-resolver.md new file mode 100644 index 0000000..6c818f3 --- /dev/null +++ b/docs/statescript/resolvers/append-resolver.md @@ -0,0 +1,46 @@ +# AppendResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.AppendResolver` (value arrays), `ObjectAppendResolver` (reference arrays) +> **Output Type:** *(array of the source's element type)* + +Appends additional elements to the end of a nested array resolver. Each appended element is produced by its own nested resolver, allowing constants, variables, or computed values to be added. + +## Constructors + +```csharp +new AppendResolver(source, params elements) // Variant128 arrays +new ObjectAppendResolver(source, params elements) // reference arrays +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` / `IObjectArrayResolver` | The resolver providing the source array. | +| elements | `IPropertyResolver[]` / `IObjectResolver[]` | The nested resolvers producing the elements to append. Value-lane elements must resolve to the source element type. | + +## Behavior + +- Returns the source elements followed by one element per appended resolver, evaluated in order. +- Throws `ArgumentException` at construction for null element resolvers, or (value lane) for element resolvers whose value type does not match the source element type. + +## Usage + +```csharp +new AppendResolver( + new ArrayVariableResolver("damageRolls", typeof(int)), + new VariantResolver(new Variant128(10), typeof(int))) +``` + +## Composition + +```csharp +// Add the current ability target to a stored target list +new ObjectAppendResolver( + new EntityArrayVariableResolver("targets"), + new AbilityTargetResolver()); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [ConcatResolver](concat-resolver.md) +- [RemoveAtResolver](remove-at-resolver.md) diff --git a/docs/statescript/resolvers/average-resolver.md b/docs/statescript/resolvers/average-resolver.md new file mode 100644 index 0000000..9ce63a4 --- /dev/null +++ b/docs/statescript/resolvers/average-resolver.md @@ -0,0 +1,47 @@ +# AverageResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.AverageResolver` +> **Output Type:** `double` (`float` for float elements, `decimal` for decimal elements) + +Computes the arithmetic mean of all elements of a nested numeric array resolver, a LINQ `Average`. + +## Constructor + +```csharp +new AverageResolver(source) +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` | The resolver providing the source array. Must have a numeric element type. | + +## Behavior + +- Returns the mean of all elements; an empty array averages to zero (unlike LINQ, it never throws). +- `float` elements average to `float`, `decimal` elements to `decimal`, all other numeric element types to `double`. +- Throws `ArgumentException` at construction for non-numeric element types. + +## Usage + +```csharp +new AverageResolver(new ArrayVariableResolver("recentDamage", typeof(int))) +``` + +## Composition + +```csharp +// "Is the group's average health below half?" +new ComparisonResolver( + new AverageResolver( + new SelectResolver( + new EntityArrayVariableResolver("party"), + new AttributeResolver("CombatAttributeSet.Health", new ElementEntityResolver()))), + ComparisonOperation.LessThan, + new VariantResolver(new Variant128(50d), typeof(double))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [SumResolver](sum-resolver.md) +- [MinElementResolver](min-element-resolver.md) diff --git a/docs/statescript/resolvers/concat-resolver.md b/docs/statescript/resolvers/concat-resolver.md new file mode 100644 index 0000000..9356a20 --- /dev/null +++ b/docs/statescript/resolvers/concat-resolver.md @@ -0,0 +1,48 @@ +# ConcatResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.ConcatResolver` (value arrays), `ObjectConcatResolver` (reference arrays) +> **Output Type:** *(array of the shared element type)* + +Concatenates two nested array resolvers, producing the elements of the first followed by the elements of the second. + +## Constructors + +```csharp +new ConcatResolver(first, second) // Variant128 arrays +new ObjectConcatResolver(first, second) // reference arrays +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| first | `IArrayPropertyResolver` / `IObjectArrayResolver` | The resolver providing the leading elements. | +| second | `IArrayPropertyResolver` / `IObjectArrayResolver` | The resolver providing the trailing elements. The value-lane variant requires both element types to match. | + +## Behavior + +- Returns one array holding the first source's elements followed by the second source's elements. +- When either side is empty, the other side is returned as-is. +- The value-lane variant throws `ArgumentException` at construction for mismatched element types. + +## Usage + +```csharp +new ConcatResolver( + new ArrayVariableResolver("baseDamage", typeof(int)), + new ArrayVariableResolver("bonusDamage", typeof(int))) +``` + +## Composition + +```csharp +// Merge two target lists, then de-duplicate shared members +new ObjectDistinctResolver( + new ObjectConcatResolver( + new EntityArrayVariableResolver("meleeTargets"), + new EntityArrayVariableResolver("rangedTargets"))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [AppendResolver](append-resolver.md) +- [DistinctResolver](distinct-resolver.md) diff --git a/docs/statescript/resolvers/contains-resolver.md b/docs/statescript/resolvers/contains-resolver.md new file mode 100644 index 0000000..6e988c4 --- /dev/null +++ b/docs/statescript/resolvers/contains-resolver.md @@ -0,0 +1,52 @@ +# ContainsResolver + +Checks whether a nested array resolver contains a given value or reference. Both variants resolve the search value through a nested resolver, allowing constants, variables, or computed values. + +## Value arrays: ContainsResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.ContainsResolver` +> **Output Type:** `bool` + +```csharp +new ContainsResolver(source, value) +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` | The resolver providing the source array. | +| value | `IPropertyResolver` | The resolver providing the value to search for. Must resolve to the source element type. | + +- Elements are compared by value; floating-point values are compared exactly. +- Throws `ArgumentException` at construction when the value type does not match the source element type. + +## Reference arrays: ObjectContainsResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.ObjectContainsResolver` +> **Output Type:** `bool` + +```csharp +new ObjectContainsResolver(source, value) +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IObjectArrayResolver` | The resolver providing the source array. | +| value | `IObjectResolver` | The resolver providing the reference to search for. | + +- Elements are matched by reference identity. +- A `null` search value matches stored `null` elements; combine with [IsValidResolver](is-valid-resolver.md) when missing values must not count as a match. + +## Composition + +```csharp +// "Has this entity already been hit?" — skip it if so +new ObjectContainsResolver( + new EntityArrayVariableResolver("alreadyHit"), + new AbilityTargetResolver()); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [IndexOfResolver](index-of-resolver.md) +- [AnyResolver](any-resolver.md) diff --git a/docs/statescript/resolvers/count-resolver.md b/docs/statescript/resolvers/count-resolver.md new file mode 100644 index 0000000..cbf5711 --- /dev/null +++ b/docs/statescript/resolvers/count-resolver.md @@ -0,0 +1,52 @@ +# CountResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.CountResolver` +> **Output Type:** `int` + +Counts the elements of a nested array resolver, optionally counting only the elements that satisfy a nested boolean predicate resolver, a LINQ `Count`. The source may come from either lane: a value array (`IArrayPropertyResolver`) or a reference array (`IObjectArrayResolver`). + +## Constructors + +```csharp +new CountResolver(source) // count everything +new CountResolver(source, predicate) // count matches only +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` or `IObjectArrayResolver` | The resolver providing the source array (either lane). | +| predicate | `IPropertyResolver` | Optional. Evaluated per element with the element published on the element stack. Must resolve to `bool`. | + +## Behavior + +- Without a predicate, returns the array length. +- With a predicate, returns the number of elements for which it resolves to `true`. +- Empty or missing sources count as zero. +- Throws `ArgumentException` at construction when the predicate does not resolve to `bool`. + +## Usage + +```csharp +new CountResolver(new EntityArrayVariableResolver("nearbyEntities")) +``` + +## Composition + +```csharp +// "Are at least two enemies wounded?" +new ComparisonResolver( + new CountResolver( + new EntityArrayVariableResolver("nearbyEntities"), + new ComparisonResolver( + new AttributeResolver("CombatAttributeSet.Health", new ElementEntityResolver()), + ComparisonOperation.LessThan, + new VariantResolver(new Variant128(50), typeof(int)))), + ComparisonOperation.GreaterThanOrEqual, + new VariantResolver(new Variant128(2), typeof(int))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [AnyResolver](any-resolver.md) +- [AllResolver](all-resolver.md) diff --git a/docs/statescript/resolvers/distinct-resolver.md b/docs/statescript/resolvers/distinct-resolver.md new file mode 100644 index 0000000..2e340f7 --- /dev/null +++ b/docs/statescript/resolvers/distinct-resolver.md @@ -0,0 +1,44 @@ +# DistinctResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.DistinctResolver` (value arrays), `ObjectDistinctResolver` (reference arrays) +> **Output Type:** *(array of the source's element type)* + +De-duplicates a nested array resolver, keeping the first occurrence of each element and preserving the original order. The object-lane variant matches elements by reference identity, useful to avoid processing the same target twice. + +## Constructors + +```csharp +new DistinctResolver(source) // Variant128 arrays +new ObjectDistinctResolver(source) // reference arrays +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` / `IObjectArrayResolver` | The resolver providing the source array. | + +## Behavior + +- Returns the source elements with later duplicates removed; the first occurrence wins. +- Value-lane elements are compared by value (floating-point values exactly); object-lane elements by reference identity. + +## Usage + +```csharp +new DistinctResolver(new ArrayVariableResolver("rolledValues", typeof(int))) +``` + +## Composition + +```csharp +// Merge two target lists without hitting anyone twice +new ObjectDistinctResolver( + new ObjectConcatResolver( + new EntityArrayVariableResolver("primaryTargets"), + new EntityArrayVariableResolver("splashTargets"))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [ExceptResolver](except-resolver.md) +- [ConcatResolver](concat-resolver.md) diff --git a/docs/statescript/resolvers/element-at-resolver.md b/docs/statescript/resolvers/element-at-resolver.md new file mode 100644 index 0000000..e2eabac --- /dev/null +++ b/docs/statescript/resolvers/element-at-resolver.md @@ -0,0 +1,50 @@ +# ElementAtResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.ElementAtResolver` (value arrays), `ObjectElementAtResolver` (reference arrays), `EntityElementAtResolver` (entity arrays) +> **Output Type:** *(the source's element type)* + +Reads the element at a given index of a nested array resolver. The index is itself a nested numeric resolver, allowing both constant indices and computed ones (a variable, an [ElementIndexResolver](element-index-resolver.md), math, etc.). + +## Constructors + +```csharp +new ElementAtResolver(source, index) // Variant128 arrays +new ObjectElementAtResolver(source, index) // reference arrays +new EntityElementAtResolver(source, index) // entity arrays, usable as IEntityResolver +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` / `IObjectArrayResolver` | The resolver providing the source array. | +| index | `IPropertyResolver` | The resolver providing the zero-based element index. Must resolve to a numeric type; fractional values are truncated. | + +## Behavior + +- Resolves the source array, then the index, and returns the element at that position. +- Out-of-range indices return a default `Variant128` (value lane) or `null` (object lane) — they never throw. +- `EntityElementAtResolver` implements `IEntityResolver`, so it plugs into `AttributeResolver`, `TagQueryResolver`, and other entity-aware resolvers. + +## Usage + +```csharp +new ElementAtResolver( + new ArrayVariableResolver("damageTable", typeof(int)), + new VariableResolver("comboStep", typeof(int))) +``` + +## Composition + +```csharp +// Read the health of the second entity in a stored array +new AttributeResolver( + "CombatAttributeSet.Health", + new EntityElementAtResolver( + new EntityArrayVariableResolver("targets"), + new VariantResolver(new Variant128(1), typeof(int)))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [FirstResolver](first-resolver.md) +- [LastResolver](last-resolver.md) diff --git a/docs/statescript/resolvers/element-entity-resolver.md b/docs/statescript/resolvers/element-entity-resolver.md new file mode 100644 index 0000000..6b9fd12 --- /dev/null +++ b/docs/statescript/resolvers/element-entity-resolver.md @@ -0,0 +1,44 @@ +# ElementEntityResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.ElementEntityResolver` +> **Output Type:** `IForgeEntity?` + +Resolves the `IForgeEntity` array element currently being iterated by an enclosing array resolver. Because it implements `IEntityResolver`, it composes with every entity-aware resolver (`AttributeResolver`, `TagQueryResolver`, etc.), which is what makes per-element predicates and sort keys like "the current entity's health" expressible. + +## Constructor + +```csharp +new ElementEntityResolver() +``` + +*(no parameters)* + +## Behavior + +- Reads the innermost element frame published by an enclosing array resolver on the graph context. +- Returns `null` when evaluated outside an array iteration or when the current element is not an `IForgeEntity`. + +## Usage + +```csharp +// A per-element sort key: the iterated entity's health +new AttributeResolver("CombatAttributeSet.Health", new ElementEntityResolver()) +``` + +## Composition + +```csharp +// Sort entities by health, then keep the three lowest. +new ObjectTakeResolver( + new ObjectOrderByResolver( + new EntityArrayVariableResolver("nearbyEntities"), + new AttributeResolver("CombatAttributeSet.Health", new ElementEntityResolver())), + new VariantResolver(new Variant128(3), typeof(int))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [OrderByResolver](order-by-resolver.md) +- [AttributeResolver](attribute-resolver.md) +- [ElementResolver<T>](element-resolver.md) diff --git a/docs/statescript/resolvers/element-index-resolver.md b/docs/statescript/resolvers/element-index-resolver.md new file mode 100644 index 0000000..f30acee --- /dev/null +++ b/docs/statescript/resolvers/element-index-resolver.md @@ -0,0 +1,44 @@ +# ElementIndexResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.ElementIndexResolver` +> **Output Type:** `int` + +Resolves the zero-based index of the array element currently being iterated by an enclosing array resolver. Use it inside nested "lambda" resolvers for index-aware predicates and projections. + +## Constructor + +```csharp +new ElementIndexResolver() +``` + +*(no parameters)* + +## Behavior + +- Reads the innermost element frame published by an enclosing array resolver on the graph context. +- Returns a default `Variant128` (zero) when evaluated outside an array iteration. + +## Usage + +```csharp +// The current element's position in the iterated array +new ElementIndexResolver() +``` + +## Composition + +```csharp +// Keep only the elements at even positions: numbers.Where((x, i) => i % 2 == 0) +new WhereResolver( + new ArrayVariableResolver("numbers", typeof(int)), + new ComparisonResolver( + new ModuloResolver(new ElementIndexResolver(), new VariantResolver(new Variant128(2), typeof(int))), + ComparisonOperation.Equal, + new VariantResolver(new Variant128(0), typeof(int)))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [ElementValueResolver](element-value-resolver.md) +- [WhereResolver](where-resolver.md) diff --git a/docs/statescript/resolvers/element-resolver.md b/docs/statescript/resolvers/element-resolver.md new file mode 100644 index 0000000..b766258 --- /dev/null +++ b/docs/statescript/resolvers/element-resolver.md @@ -0,0 +1,42 @@ +# ElementResolver<T> + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.ElementResolver` +> **Output Type:** `T?` + +Resolves the object-backed array element currently being iterated by an enclosing array resolver (`ObjectWhereResolver`, `ObjectOrderByResolver`, etc.). Use it inside nested "lambda" resolvers as the stand-in for the lambda parameter when iterating reference arrays. + +## Constructor + +```csharp +new ElementResolver() +``` + +*(no parameters)* + +## Behavior + +- Reads the innermost element frame published by an enclosing array resolver on the graph context. +- Returns `null` when evaluated outside an array iteration or when the current element is not compatible with `T`. + +## Usage + +```csharp +// The lambda parameter of an object array operation +new ElementResolver() +``` + +## Composition + +```csharp +// Drop null entries from a reference array: targets.Where(x => x is not null) +new ObjectWhereResolver( + new EntityArrayVariableResolver("targets"), + new IsValidResolver(new ElementResolver())); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [ElementEntityResolver](element-entity-resolver.md) +- [IsValidResolver](is-valid-resolver.md) +- [WhereResolver](where-resolver.md) diff --git a/docs/statescript/resolvers/element-value-resolver.md b/docs/statescript/resolvers/element-value-resolver.md new file mode 100644 index 0000000..c653d00 --- /dev/null +++ b/docs/statescript/resolvers/element-value-resolver.md @@ -0,0 +1,48 @@ +# ElementValueResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.ElementValueResolver` +> **Output Type:** *(configured at construction time)* + +Resolves the value-typed array element currently being iterated by an enclosing array resolver (`WhereResolver`, `OrderByResolver`, `SelectResolver`, etc.). Use it inside nested "lambda" resolvers as the stand-in for the lambda parameter, the `x` in `x => x > 2`. + +## Constructor + +```csharp +new ElementValueResolver(valueType) +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| valueType | `Type` | The element type this resolver produces. Must match the iterated array's element type. | + +## Behavior + +- Reads the innermost element frame published by an enclosing array resolver on the graph context. +- Returns a default `Variant128` (zero) when evaluated outside an array iteration. +- Frames form a stack, so nested array operations always observe the innermost element. + +## Usage + +```csharp +// The lambda parameter of a filter over an int array +new ElementValueResolver(typeof(int)) +``` + +## Composition + +```csharp +// Keep elements greater than 2: numbers.Where(x => x > 2) +new WhereResolver( + new ArrayVariableResolver("numbers", typeof(int)), + new ComparisonResolver( + new ElementValueResolver(typeof(int)), + ComparisonOperation.GreaterThan, + new VariantResolver(new Variant128(2), typeof(int)))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [WhereResolver](where-resolver.md) +- [SelectResolver](select-resolver.md) +- [ElementIndexResolver](element-index-resolver.md) diff --git a/docs/statescript/resolvers/except-resolver.md b/docs/statescript/resolvers/except-resolver.md new file mode 100644 index 0000000..c07fd64 --- /dev/null +++ b/docs/statescript/resolvers/except-resolver.md @@ -0,0 +1,48 @@ +# ExceptResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.ExceptResolver` (value arrays), `ObjectExceptResolver` (reference arrays) +> **Output Type:** *(array of the source's element type)* + +Removes from a nested array resolver every element that appears in a second nested array resolver, preserving the source order. The object-lane variant matches elements by reference identity. + +## Constructors + +```csharp +new ExceptResolver(source, other) // Variant128 arrays +new ObjectExceptResolver(source, other) // reference arrays +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` / `IObjectArrayResolver` | The resolver providing the source array. | +| other | `IArrayPropertyResolver` / `IObjectArrayResolver` | The resolver providing the elements to remove. The value-lane variant requires both element types to match. | + +## Behavior + +- Keeps the source elements that are not present in `other`, in their original order. +- Unlike LINQ's set-based `Except`, duplicates in the source are preserved unless they appear in `other`. +- Value-lane elements are compared by value (floating-point values exactly); object-lane elements by reference identity. +- The value-lane variant throws `ArgumentException` at construction for mismatched element types. + +## Usage + +```csharp +new ExceptResolver( + new ArrayVariableResolver("allLanes", typeof(int)), + new ArrayVariableResolver("blockedLanes", typeof(int))) +``` + +## Composition + +```csharp +// All nearby entities except the ones already hit +new ObjectExceptResolver( + new EntityArrayVariableResolver("nearbyEntities"), + new EntityArrayVariableResolver("alreadyHit")); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [DistinctResolver](distinct-resolver.md) +- [WhereResolver](where-resolver.md) diff --git a/docs/statescript/resolvers/first-resolver.md b/docs/statescript/resolvers/first-resolver.md new file mode 100644 index 0000000..9ad9cf4 --- /dev/null +++ b/docs/statescript/resolvers/first-resolver.md @@ -0,0 +1,47 @@ +# FirstResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.FirstResolver` (value arrays), `ObjectFirstResolver` (reference arrays), `EntityFirstResolver` (entity arrays) +> **Output Type:** *(the source's element type)* + +Reads the first element of a nested array resolver. Combined with [OrderByResolver](order-by-resolver.md), this expresses "the best element by some key", e.g. the closest enemy. + +## Constructors + +```csharp +new FirstResolver(source) // Variant128 arrays +new ObjectFirstResolver(source) // reference arrays +new EntityFirstResolver(source) // entity arrays, usable as IEntityResolver +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` / `IObjectArrayResolver` | The resolver providing the source array. | + +## Behavior + +- Resolves the source array and returns its first element. +- Empty arrays return a default `Variant128` (value lane) or `null` (object lane). +- `EntityFirstResolver` implements `IEntityResolver`, so it plugs into entity-aware resolvers. + +## Usage + +```csharp +new FirstResolver(new ArrayVariableResolver("damageRolls", typeof(int))) +``` + +## Composition + +```csharp +// The single closest entity: nearby.OrderBy(distance).First() +new EntityFirstResolver( + new ObjectOrderByResolver( + new EntityArrayVariableResolver("nearbyEntities"), + new AttributeResolver("MovementAttributeSet.DistanceToOwner", new ElementEntityResolver()))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [LastResolver](last-resolver.md) +- [ElementAtResolver](element-at-resolver.md) +- [OrderByResolver](order-by-resolver.md) diff --git a/docs/statescript/resolvers/index-of-resolver.md b/docs/statescript/resolvers/index-of-resolver.md new file mode 100644 index 0000000..d80d53b --- /dev/null +++ b/docs/statescript/resolvers/index-of-resolver.md @@ -0,0 +1,53 @@ +# IndexOfResolver + +Resolves the zero-based index of the first occurrence of a given value or reference in a nested array resolver, or `-1` when it is not present. + +## Value arrays: IndexOfResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.IndexOfResolver` +> **Output Type:** `int` + +```csharp +new IndexOfResolver(source, value) +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` | The resolver providing the source array. | +| value | `IPropertyResolver` | The resolver providing the value to search for. Must resolve to the source element type. | + +- Elements are compared by value; floating-point values are compared exactly. +- Throws `ArgumentException` at construction when the value type does not match the source element type. + +## Reference arrays: ObjectIndexOfResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.ObjectIndexOfResolver` +> **Output Type:** `int` + +```csharp +new ObjectIndexOfResolver(source, value) +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IObjectArrayResolver` | The resolver providing the source array. | +| value | `IObjectResolver` | The resolver providing the reference to search for. | + +- Elements are matched by reference identity. A `null` search value matches stored `null` elements. + +## Composition + +```csharp +// Remove a specific entity from a list by identity +new ObjectRemoveAtResolver( + new EntityArrayVariableResolver("targets"), + new ObjectIndexOfResolver( + new EntityArrayVariableResolver("targets"), + new EntityVariableResolver("candidate"))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [ContainsResolver](contains-resolver.md) +- [RemoveAtResolver](remove-at-resolver.md) diff --git a/docs/statescript/resolvers/last-resolver.md b/docs/statescript/resolvers/last-resolver.md new file mode 100644 index 0000000..d5840f0 --- /dev/null +++ b/docs/statescript/resolvers/last-resolver.md @@ -0,0 +1,46 @@ +# LastResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.LastResolver` (value arrays), `ObjectLastResolver` (reference arrays), `EntityLastResolver` (entity arrays) +> **Output Type:** *(the source's element type)* + +Reads the last element of a nested array resolver. + +## Constructors + +```csharp +new LastResolver(source) // Variant128 arrays +new ObjectLastResolver(source) // reference arrays +new EntityLastResolver(source) // entity arrays, usable as IEntityResolver +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` / `IObjectArrayResolver` | The resolver providing the source array. | + +## Behavior + +- Resolves the source array and returns its last element. +- Empty arrays return a default `Variant128` (value lane) or `null` (object lane). +- `EntityLastResolver` implements `IEntityResolver`, so it plugs into entity-aware resolvers. + +## Usage + +```csharp +new LastResolver(new ArrayVariableResolver("comboDamage", typeof(int))) +``` + +## Composition + +```csharp +// The farthest entity: nearby.OrderBy(distance).Last() +new EntityLastResolver( + new ObjectOrderByResolver( + new EntityArrayVariableResolver("nearbyEntities"), + new AttributeResolver("MovementAttributeSet.DistanceToOwner", new ElementEntityResolver()))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [FirstResolver](first-resolver.md) +- [ElementAtResolver](element-at-resolver.md) diff --git a/docs/statescript/resolvers/max-element-resolver.md b/docs/statescript/resolvers/max-element-resolver.md new file mode 100644 index 0000000..ef65a58 --- /dev/null +++ b/docs/statescript/resolvers/max-element-resolver.md @@ -0,0 +1,41 @@ +# MaxElementResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.MaxElementResolver` +> **Output Type:** *(the source's element type)* + +Resolves the largest element of a nested numeric array resolver. Unlike the binary [MaxResolver](max-resolver.md), which compares two operands, this resolver aggregates over an array. + +## Constructor + +```csharp +new MaxElementResolver(source) +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` | The resolver providing the source array. Must have a numeric element type. | + +## Behavior + +- Returns the largest element unchanged, so the result type matches the source element type. +- Empty arrays return a default `Variant128` (zero); ties resolve to the first occurrence. +- Throws `ArgumentException` at construction for non-numeric element types. + +## Usage + +```csharp +new MaxElementResolver(new ArrayVariableResolver("damageRolls", typeof(int))) +``` + +## Composition + +```csharp +// The hardest hit taken this fight +new MaxElementResolver(new ArrayVariableResolver("damageTaken", typeof(int))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [MinElementResolver](min-element-resolver.md) +- [MaxResolver](max-resolver.md) diff --git a/docs/statescript/resolvers/min-element-resolver.md b/docs/statescript/resolvers/min-element-resolver.md new file mode 100644 index 0000000..fa51814 --- /dev/null +++ b/docs/statescript/resolvers/min-element-resolver.md @@ -0,0 +1,45 @@ +# MinElementResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.MinElementResolver` +> **Output Type:** *(the source's element type)* + +Resolves the smallest element of a nested numeric array resolver. Unlike the binary [MinResolver](min-resolver.md), which compares two operands, this resolver aggregates over an array. + +## Constructor + +```csharp +new MinElementResolver(source) +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` | The resolver providing the source array. Must have a numeric element type. | + +## Behavior + +- Returns the smallest element unchanged, so the result type matches the source element type. +- Empty arrays return a default `Variant128` (zero); ties resolve to the first occurrence. +- Throws `ArgumentException` at construction for non-numeric element types. + +## Usage + +```csharp +new MinElementResolver(new ArrayVariableResolver("cooldowns", typeof(float))) +``` + +## Composition + +```csharp +// The lowest health among all targets +new MinElementResolver( + new SelectResolver( + new EntityArrayVariableResolver("targets"), + new AttributeResolver("CombatAttributeSet.Health", new ElementEntityResolver()))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [MaxElementResolver](max-element-resolver.md) +- [MinResolver](min-resolver.md) +- [OrderByResolver](order-by-resolver.md) diff --git a/docs/statescript/resolvers/order-by-resolver.md b/docs/statescript/resolvers/order-by-resolver.md new file mode 100644 index 0000000..d9a9d50 --- /dev/null +++ b/docs/statescript/resolvers/order-by-resolver.md @@ -0,0 +1,54 @@ +# OrderByResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.OrderByResolver` (value arrays), `ObjectOrderByResolver` (reference arrays) +> **Output Type:** *(array of the source's element type)* + +Sorts a nested array resolver by a nested numeric key selector resolver, a LINQ `OrderBy` for statescript graphs. The key selector is evaluated once per element with the current element published on the element stack, so it reads the element through [ElementValueResolver](element-value-resolver.md) (value arrays) or [ElementEntityResolver](element-entity-resolver.md) (entity arrays, composing with `AttributeResolver`, `DistanceResolver`, etc. for the key). + +## Constructors + +```csharp +new OrderByResolver(source, keySelector, direction) // Variant128 arrays +new ObjectOrderByResolver(source, keySelector, direction) // reference arrays +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` / `IObjectArrayResolver` | The resolver providing the source array. | +| keySelector | `IPropertyResolver` | The resolver evaluated per element to produce its sort key. Must resolve to a numeric type. | +| direction | `SortDirection` | Optional. `Ascending` (default) or `Descending`. | + +## Behavior + +- Computes one numeric key per element, then returns the elements sorted by key. +- The sort is **stable**: elements with equal keys keep their original relative order. +- Empty or missing sources produce an empty array. +- Throws `ArgumentException` at construction when the key selector does not resolve to a numeric type. + +## Usage + +```csharp +// numbers.OrderByDescending(x => x) +new OrderByResolver( + new ArrayVariableResolver("numbers", typeof(int)), + new ElementValueResolver(typeof(int)), + SortDirection.Descending); +``` + +## Composition + +```csharp +// The motivating skill example: keep the three closest entities. +new ObjectTakeResolver( + new ObjectOrderByResolver( + new EntityArrayVariableResolver("nearbyEntities"), + new AttributeResolver("MovementAttributeSet.DistanceToOwner", new ElementEntityResolver())), + new VariantResolver(new Variant128(3), typeof(int))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [TakeResolver](take-resolver.md) +- [WhereResolver](where-resolver.md) +- [MinElementResolver](min-element-resolver.md) diff --git a/docs/statescript/resolvers/remove-at-resolver.md b/docs/statescript/resolvers/remove-at-resolver.md new file mode 100644 index 0000000..8288912 --- /dev/null +++ b/docs/statescript/resolvers/remove-at-resolver.md @@ -0,0 +1,48 @@ +# RemoveAtResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.RemoveAtResolver` (value arrays), `ObjectRemoveAtResolver` (reference arrays) +> **Output Type:** *(array of the source's element type)* + +Removes the element at a given index from a nested array resolver. The index is itself a nested numeric resolver. + +## Constructors + +```csharp +new RemoveAtResolver(source, index) // Variant128 arrays +new ObjectRemoveAtResolver(source, index) // reference arrays +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` / `IObjectArrayResolver` | The resolver providing the source array. | +| index | `IPropertyResolver` | The resolver providing the zero-based index to remove. Must resolve to a numeric type; fractional values are truncated. | + +## Behavior + +- Returns the source array without the element at the resolved index. +- Out-of-range indices return the source array unchanged, they never throw. + +## Usage + +```csharp +new RemoveAtResolver( + new ArrayVariableResolver("queue", typeof(int)), + new VariantResolver(new Variant128(0), typeof(int))) +``` + +## Composition + +```csharp +// Remove a specific entity found by identity: targets.RemoveAt(targets.IndexOf(candidate)) +new ObjectRemoveAtResolver( + new EntityArrayVariableResolver("targets"), + new ObjectIndexOfResolver( + new EntityArrayVariableResolver("targets"), + new EntityVariableResolver("candidate"))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [ExceptResolver](except-resolver.md) +- [IndexOfResolver](index-of-resolver.md) diff --git a/docs/statescript/resolvers/reverse-resolver.md b/docs/statescript/resolvers/reverse-resolver.md new file mode 100644 index 0000000..617b49d --- /dev/null +++ b/docs/statescript/resolvers/reverse-resolver.md @@ -0,0 +1,42 @@ +# ReverseResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.ReverseResolver` (value arrays), `ObjectReverseResolver` (reference arrays) +> **Output Type:** *(array of the source's element type)* + +Reverses the element order of a nested array resolver. + +## Constructors + +```csharp +new ReverseResolver(source) // Variant128 arrays +new ObjectReverseResolver(source) // reference arrays +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` / `IObjectArrayResolver` | The resolver providing the source array. | + +## Behavior + +- Returns the source elements in reverse order. + +## Usage + +```csharp +new ReverseResolver(new ArrayVariableResolver("queue", typeof(int))) +``` + +## Composition + +```csharp +// Farthest-first ordering without a descending sort +new ObjectReverseResolver( + new ObjectOrderByResolver( + new EntityArrayVariableResolver("nearbyEntities"), + new AttributeResolver("MovementAttributeSet.DistanceToOwner", new ElementEntityResolver()))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [OrderByResolver](order-by-resolver.md) diff --git a/docs/statescript/resolvers/select-resolver.md b/docs/statescript/resolvers/select-resolver.md new file mode 100644 index 0000000..f05f318 --- /dev/null +++ b/docs/statescript/resolvers/select-resolver.md @@ -0,0 +1,57 @@ +# SelectResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.SelectResolver` (projects to values), `SelectObjectResolver` (projects to references) +> **Output Type:** *(array of the projection's value type)* + +Projects each element of a nested source array through a nested projection resolver, a LINQ `Select`. The projection is evaluated once per element with the current element published on the element stack, so it reads the element through the element resolvers. The **source may come from either lane**, a value array (`IArrayPropertyResolver`) or a reference array (`IObjectArrayResolver`), enabling projections such as "the health of each entity in the array". + +## Constructors + +```csharp +new SelectResolver(source, projection) // → Variant128 array +new SelectObjectResolver(source, projection) // → TResult array +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` or `IObjectArrayResolver` | The resolver providing the source array (either lane). | +| projection | `IPropertyResolver` (`SelectResolver`) / `IObjectResolver` (`SelectObjectResolver`) | The resolver evaluated per element to produce the projected value. | + +## Behavior + +- Evaluates the projection for each source element and returns the projected array, same length and order. +- The resulting element type is the projection's value type. +- Empty or missing sources produce an empty array. + +## Usage + +```csharp +// numbers.Select(x => x * 2) +new SelectResolver( + new ArrayVariableResolver("numbers", typeof(int)), + new MultiplyResolver( + new ElementValueResolver(typeof(int)), + new VariantResolver(new Variant128(2), typeof(int)))); +``` + +## Composition + +```csharp +// entities.Select(e => e.Health) — object lane in, value lane out +new SelectResolver( + new EntityArrayVariableResolver("targets"), + new AttributeResolver("CombatAttributeSet.Health", new ElementEntityResolver())); + +// Sum the health of all targets +new SumResolver( + new SelectResolver( + new EntityArrayVariableResolver("targets"), + new AttributeResolver("CombatAttributeSet.Health", new ElementEntityResolver()))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [WhereResolver](where-resolver.md) +- [SumResolver](sum-resolver.md) +- [ElementValueResolver](element-value-resolver.md) diff --git a/docs/statescript/resolvers/skip-resolver.md b/docs/statescript/resolvers/skip-resolver.md new file mode 100644 index 0000000..6c8e37f --- /dev/null +++ b/docs/statescript/resolvers/skip-resolver.md @@ -0,0 +1,48 @@ +# SkipResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.SkipResolver` (value arrays), `ObjectSkipResolver` (reference arrays) +> **Output Type:** *(array of the source's element type)* + +Drops the first N elements of a nested array resolver, a LINQ `Skip`. The count is itself a nested numeric resolver, allowing both constant and computed counts. + +## Constructors + +```csharp +new SkipResolver(source, count) // Variant128 arrays +new ObjectSkipResolver(source, count) // reference arrays +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` / `IObjectArrayResolver` | The resolver providing the source array. | +| count | `IPropertyResolver` | The resolver providing the number of elements to skip. Must resolve to a numeric type; fractional values are truncated. | + +## Behavior + +- Returns the source elements after the first `count`. +- Counts are clamped to the source length; negative counts skip nothing. + +## Usage + +```csharp +new SkipResolver( + new ArrayVariableResolver("queue", typeof(int)), + new VariantResolver(new Variant128(1), typeof(int))) +``` + +## Composition + +```csharp +// Everything except the closest entity (e.g. chain lightning bounces) +new ObjectSkipResolver( + new ObjectOrderByResolver( + new EntityArrayVariableResolver("nearbyEntities"), + new AttributeResolver("MovementAttributeSet.DistanceToOwner", new ElementEntityResolver())), + new VariantResolver(new Variant128(1), typeof(int))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [TakeResolver](take-resolver.md) +- [OrderByResolver](order-by-resolver.md) diff --git a/docs/statescript/resolvers/sum-resolver.md b/docs/statescript/resolvers/sum-resolver.md new file mode 100644 index 0000000..5e3836d --- /dev/null +++ b/docs/statescript/resolvers/sum-resolver.md @@ -0,0 +1,43 @@ +# SumResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.SumResolver` +> **Output Type:** *(the source element type, promoted)* + +Adds up all elements of a nested numeric array resolver, a LINQ `Sum`. The result type follows the standard numeric promotion rules (`int` elements sum to `int`, `float` to `float`, `uint` to `long`, etc.). + +## Constructor + +```csharp +new SumResolver(source) +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` | The resolver providing the source array. Must have a numeric element type. | + +## Behavior + +- Returns the sum of all elements; an empty array sums to zero. +- Throws `ArgumentException` at construction for non-numeric element types. + +## Usage + +```csharp +new SumResolver(new ArrayVariableResolver("damageRolls", typeof(int))) +``` + +## Composition + +```csharp +// Total health across all targets: targets.Select(e => e.Health).Sum() +new SumResolver( + new SelectResolver( + new EntityArrayVariableResolver("targets"), + new AttributeResolver("CombatAttributeSet.Health", new ElementEntityResolver()))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [AverageResolver](average-resolver.md) +- [SelectResolver](select-resolver.md) diff --git a/docs/statescript/resolvers/take-resolver.md b/docs/statescript/resolvers/take-resolver.md new file mode 100644 index 0000000..0382a94 --- /dev/null +++ b/docs/statescript/resolvers/take-resolver.md @@ -0,0 +1,48 @@ +# TakeResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.TakeResolver` (value arrays), `ObjectTakeResolver` (reference arrays) +> **Output Type:** *(array of the source's element type)* + +Keeps the first N elements of a nested array resolver, a LINQ `Take`. The count is itself a nested numeric resolver, allowing both constant and computed counts. + +## Constructors + +```csharp +new TakeResolver(source, count) // Variant128 arrays +new ObjectTakeResolver(source, count) // reference arrays +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` / `IObjectArrayResolver` | The resolver providing the source array. | +| count | `IPropertyResolver` | The resolver providing the number of elements to keep. Must resolve to a numeric type; fractional values are truncated. | + +## Behavior + +- Returns the first `count` elements of the source array. +- Counts are clamped to the source length; negative counts produce an empty array. + +## Usage + +```csharp +new TakeResolver( + new ArrayVariableResolver("damageRolls", typeof(int)), + new VariantResolver(new Variant128(3), typeof(int))) +``` + +## Composition + +```csharp +// Sort by distance, keep the three closest +new ObjectTakeResolver( + new ObjectOrderByResolver( + new EntityArrayVariableResolver("nearbyEntities"), + new AttributeResolver("MovementAttributeSet.DistanceToOwner", new ElementEntityResolver())), + new VariantResolver(new Variant128(3), typeof(int))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [SkipResolver](skip-resolver.md) +- [OrderByResolver](order-by-resolver.md) diff --git a/docs/statescript/resolvers/where-resolver.md b/docs/statescript/resolvers/where-resolver.md new file mode 100644 index 0000000..184f460 --- /dev/null +++ b/docs/statescript/resolvers/where-resolver.md @@ -0,0 +1,56 @@ +# WhereResolver + +> **Type:** `Gamesmiths.Forge.Statescript.Properties.WhereResolver` (value arrays), `ObjectWhereResolver` (reference arrays) +> **Output Type:** *(array of the source's element type)* + +Filters a nested array resolver by a nested boolean predicate resolver, preserving element order, a LINQ `Where` for statescript graphs. The predicate is evaluated once per element with the current element published on the element stack, so it reads the element through [ElementValueResolver](element-value-resolver.md) (value arrays) or [ElementResolver<T>](element-resolver.md)/[ElementEntityResolver](element-entity-resolver.md) (reference arrays). + +## Constructors + +```csharp +new WhereResolver(source, predicate) // Variant128 arrays +new ObjectWhereResolver(source, predicate) // reference arrays +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| source | `IArrayPropertyResolver` / `IObjectArrayResolver` | The resolver providing the source array. | +| predicate | `IPropertyResolver` | The resolver evaluated per element. Must resolve to `bool`. | + +## Behavior + +- Evaluates the predicate for each element and keeps the elements that resolve to `true`, in their original order. +- To remove matching elements instead, wrap the predicate in a `NotResolver`. +- Empty or missing sources produce an empty array. +- Throws `ArgumentException` at construction when the predicate does not resolve to `bool`. + +## Usage + +```csharp +// numbers.Where(x => x > 2) +new WhereResolver( + new ArrayVariableResolver("numbers", typeof(int)), + new ComparisonResolver( + new ElementValueResolver(typeof(int)), + ComparisonOperation.GreaterThan, + new VariantResolver(new Variant128(2), typeof(int)))); +``` + +## Composition + +```csharp +// Keep entities with health above 30, using a per-element attribute read +new ObjectWhereResolver( + new EntityArrayVariableResolver("targets"), + new ComparisonResolver( + new AttributeResolver("CombatAttributeSet.Health", new ElementEntityResolver()), + ComparisonOperation.GreaterThan, + new VariantResolver(new Variant128(30), typeof(int)))); +``` + +## See Also + +- [Resolvers Overview](README.md) +- [OrderByResolver](order-by-resolver.md) +- [CountResolver](count-resolver.md) +- [NotResolver](not-resolver.md)