Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 96 additions & 49 deletions Forge.Tests/Attributes/AttributeSetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using FluentAssertions;
using Gamesmiths.Forge.Attributes;
using Gamesmiths.Forge.Tests.Helpers;

namespace Gamesmiths.Forge.Tests.Attributes;

Expand All @@ -19,6 +20,7 @@ public void Non_initialized_attribute_are_null()
}

[Fact]
[Trait("Initialization", null)]
public void Initialized_attribute_has_the_configured_default_values()
{
var set = new SimpleAttributeSet();
Expand All @@ -33,6 +35,7 @@ public void Initialized_attribute_has_the_configured_default_values()
}

[Fact]
[Trait("Initialization", null)]
public void Vital_attribute_set_attributes_initialize_with_configured_values()
{
var set = new VitalAttributeSet();
Expand All @@ -59,6 +62,99 @@ public void Vital_attribute_set_attributes_initialize_with_configured_values()
set.CurrentHealth.CurrentValue.Should().Be(100);
}

[Fact]
[Trait("SetValue", null)]
public void SetMaxValue_updates_the_max_value_of_the_attribute()
{
var set = new VitalAttributeSet();

set.CurrentHealth.Max.Should().Be(100);

set.UpdateMaxHealth(200);

set.CurrentHealth.Max.Should().Be(200);
}

[Fact]
[Trait("SetValue", null)]
public void SetMaxValue_clamps_base_value_when_it_exceeds_new_max()
{
var set = new VitalAttributeSet();

set.CurrentHealth.BaseValue.Should().Be(100);
set.CurrentHealth.Max.Should().Be(100);

set.UpdateMaxHealth(50);

set.CurrentHealth.Max.Should().Be(50);
set.CurrentHealth.BaseValue.Should().Be(50);
set.CurrentHealth.CurrentValue.Should().Be(50);
}

[Fact]
[Trait("SetValue", null)]
public void SetMaxValue_does_not_change_base_value_when_it_is_within_new_max()
{
var set = new VitalAttributeSet();

set.CurrentHealth.BaseValue.Should().Be(100);
set.CurrentHealth.Max.Should().Be(100);

set.UpdateMaxHealth(150);

set.CurrentHealth.Max.Should().Be(150);
set.CurrentHealth.BaseValue.Should().Be(100);
set.CurrentHealth.CurrentValue.Should().Be(100);
}

[Fact]
[Trait("SetValue", null)]
public void SetMinValue_updates_the_min_value_of_the_attribute()
{
var set = new VitalAttributeSet();

set.CurrentHealth.Min.Should().Be(0);

set.UpdateMinHealth(10);

set.CurrentHealth.Min.Should().Be(10);
}

[Fact]
[Trait("SetValue", null)]
public void SetMinValue_clamps_base_value_when_it_is_below_new_min()
{
var set = new VitalAttributeSet();

set.UpdateMaxHealth(200);
set.UpdateBaseHealth(50);

set.CurrentHealth.BaseValue.Should().Be(50);
set.CurrentHealth.Min.Should().Be(0);

set.UpdateMinHealth(75);

set.CurrentHealth.Min.Should().Be(75);
set.CurrentHealth.BaseValue.Should().Be(75);
set.CurrentHealth.CurrentValue.Should().Be(75);
}

[Fact]
[Trait("SetValue", null)]
public void SetMinValue_does_not_change_base_value_when_it_is_above_new_min()
{
var set = new VitalAttributeSet();

set.CurrentHealth.BaseValue.Should().Be(100);
set.CurrentHealth.Min.Should().Be(0);

set.UpdateMinHealth(10);

set.CurrentHealth.Min.Should().Be(10);
set.CurrentHealth.BaseValue.Should().Be(100);
set.CurrentHealth.CurrentValue.Should().Be(100);
}

private sealed class SimpleAttributeSet : AttributeSet
{
public EntityAttribute InitializedAttribute { get; }
Expand All @@ -74,53 +170,4 @@ public SimpleAttributeSet()
InitializedAttribute = InitializeAttribute(nameof(InitializedAttribute), 5, 0, 10);
}
}

private sealed class VitalAttributeSet : AttributeSet
{
public EntityAttribute Vitality { get; }

public EntityAttribute MaxHealth { get; }

public EntityAttribute CurrentHealth { get; }

public VitalAttributeSet()
{
Vitality = InitializeAttribute(nameof(Vitality), 10, 0, 99);
MaxHealth = InitializeAttribute(nameof(MaxHealth), Vitality.CurrentValue * 10, 0, 1000);
CurrentHealth = InitializeAttribute(nameof(CurrentHealth), 100, 0, MaxHealth.CurrentValue);
}

protected override void AttributeOnValueChanged(EntityAttribute attribute, int change)
{
base.AttributeOnValueChanged(attribute, change);

if (attribute == Vitality)
{
// Do health to vit calculations here.
SetAttributeMaxValue(MaxHealth, Vitality.CurrentValue * 10);
}

if (attribute == MaxHealth)
{
SetAttributeMaxValue(CurrentHealth, MaxHealth.CurrentValue);
}

if (attribute == CurrentHealth)
{
if (change < 0)
{
Console.WriteLine($"Damage: {change}");

if (CurrentHealth.CurrentValue <= 0)
{
Console.WriteLine("Death");
}
}
else
{
Console.WriteLine($"Healing: {change}");
}
}
}
}
}
91 changes: 91 additions & 0 deletions Forge.Tests/Effects/EffectsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3774,6 +3774,97 @@ public void Set_by_caller_magnitude_does_not_update_periodic_application_value()
TestUtils.TestAttribute(target, "TestAttributeSet.Attribute1", [3, 3, 0, 0]);
}

[Fact]
[Trait("Instant", null)]
public void Instant_effect_on_vitality_cascades_max_value_changes_to_dependent_attributes()
{
var owner = new TestEntity(_tagsManager, _cuesManager);
var target = new VitalTestEntity(_tagsManager, _cuesManager);

var effectData = new EffectData(
"Vitality Drain",
new DurationData(DurationType.Instant),
[
new Modifier(
"VitalAttributeSet.Vitality",
ModifierOperation.FlatBonus,
new ModifierMagnitude(MagnitudeCalculationType.ScalableFloat, new ScalableFloat(-5)))
]);

var effect = new Effect(
effectData,
new EffectOwnership(
new TestEntity(_tagsManager, _cuesManager),
owner));

target.EffectsManager.ApplyEffect(effect);

target.VitalAttributeSet.Vitality.BaseValue.Should().Be(5);
target.VitalAttributeSet.Vitality.CurrentValue.Should().Be(5);

target.VitalAttributeSet.MaxHealth.Max.Should().Be(50);
target.VitalAttributeSet.MaxHealth.BaseValue.Should().Be(50);
target.VitalAttributeSet.MaxHealth.CurrentValue.Should().Be(50);

target.VitalAttributeSet.CurrentHealth.Max.Should().Be(50);
target.VitalAttributeSet.CurrentHealth.BaseValue.Should().Be(50);
target.VitalAttributeSet.CurrentHealth.CurrentValue.Should().Be(50);
}

[Fact]
[Trait("Duration", null)]
public void Duration_effect_on_vitality_cascades_max_value_changes_and_restores_max_on_removal()
{
var owner = new TestEntity(_tagsManager, _cuesManager);
var target = new VitalTestEntity(_tagsManager, _cuesManager);

var effectData = new EffectData(
"Vitality Curse",
new DurationData(DurationType.Infinite),
[
new Modifier(
"VitalAttributeSet.Vitality",
ModifierOperation.FlatBonus,
new ModifierMagnitude(MagnitudeCalculationType.ScalableFloat, new ScalableFloat(-8)))
]);

var effect = new Effect(
effectData,
new EffectOwnership(owner, new TestEntity(_tagsManager, _cuesManager)));

ActiveEffectHandle? handle = target.EffectsManager.ApplyEffect(effect);

// With -8 modifier, Vitality.CurrentValue drops from 10 to 2.
// This cascades: MaxHealth.Max = 2 * 10 = 20, clamping its BaseValue from 100 to 20.
// Then CurrentHealth.Max = 20, clamping its BaseValue from 100 to 20.
target.VitalAttributeSet.Vitality.CurrentValue.Should().Be(2);
target.VitalAttributeSet.Vitality.Modifier.Should().Be(-8);

target.VitalAttributeSet.MaxHealth.Max.Should().Be(20);
target.VitalAttributeSet.MaxHealth.BaseValue.Should().Be(20);
target.VitalAttributeSet.MaxHealth.CurrentValue.Should().Be(20);

target.VitalAttributeSet.CurrentHealth.Max.Should().Be(20);
target.VitalAttributeSet.CurrentHealth.BaseValue.Should().Be(20);
target.VitalAttributeSet.CurrentHealth.CurrentValue.Should().Be(20);

target.EffectsManager.RemoveEffect(handle!);

// After removal, Vitality modifier is gone so CurrentValue returns to 10.
// MaxHealth.Max is restored to 10 * 10 = 100, but BaseValue was permanently clamped to 20.
// CurrentHealth.Max remains 20 since MaxHealth.CurrentValue didn't change (still 20).
target.VitalAttributeSet.Vitality.CurrentValue.Should().Be(10);
target.VitalAttributeSet.Vitality.Modifier.Should().Be(0);

target.VitalAttributeSet.MaxHealth.Max.Should().Be(100);
target.VitalAttributeSet.MaxHealth.BaseValue.Should().Be(20);
target.VitalAttributeSet.MaxHealth.CurrentValue.Should().Be(20);

target.VitalAttributeSet.CurrentHealth.Max.Should().Be(20);
target.VitalAttributeSet.CurrentHealth.BaseValue.Should().Be(20);
target.VitalAttributeSet.CurrentHealth.CurrentValue.Should().Be(20);
}

private sealed class DurationFromSourceAttributeCalculator : CustomModifierMagnitudeCalculator
{
private readonly AttributeCaptureDefinition _sourceAttr;
Expand Down
73 changes: 73 additions & 0 deletions Forge.Tests/Helpers/VitalAttributeSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright © Gamesmiths Guild.

using Gamesmiths.Forge.Attributes;

namespace Gamesmiths.Forge.Tests.Helpers;

public sealed class VitalAttributeSet : AttributeSet
{
public EntityAttribute Vitality { get; }

public EntityAttribute MaxHealth { get; }

public EntityAttribute CurrentHealth { get; }

public VitalAttributeSet()
{
Vitality = InitializeAttribute(nameof(Vitality), 10, 0, 99);
MaxHealth = InitializeAttribute(nameof(MaxHealth), Vitality.CurrentValue * 10, 0, 1000);
CurrentHealth = InitializeAttribute(nameof(CurrentHealth), 100, 0, MaxHealth.CurrentValue);
}

public void UpdateMaxHealth(int newMax)
{
// These three Update methods exist exclusively for unit testing. In production, attribute changes should
// always flow through effects and modifiers to ensure changes can be replicated, and done/undone through
// effect application and removal.
SetAttributeMaxValue(CurrentHealth, newMax);
}

public void UpdateMinHealth(int newMin)
{
SetAttributeMinValue(CurrentHealth, newMin);
}

public void UpdateBaseHealth(int newBase)
{
SetAttributeBaseValue(CurrentHealth, newBase);
}

protected override void AttributeOnValueChanged(EntityAttribute attribute, int change)
{
base.AttributeOnValueChanged(attribute, change);

if (attribute == Vitality)
{
// Do health to vit calculations here. Those participate in the flow of attribute changes, so they
// should be replicated fine.
SetAttributeMaxValue(MaxHealth, Vitality.CurrentValue * 10);
}

if (attribute == MaxHealth)
{
SetAttributeMaxValue(CurrentHealth, MaxHealth.CurrentValue);
}

if (attribute == CurrentHealth)
{
if (change < 0)
{
Console.WriteLine($"Damage: {change}");

if (CurrentHealth.CurrentValue <= 0)
{
Console.WriteLine("Death");
}
}
else
{
Console.WriteLine($"Healing: {change}");
}
}
}
}
Empty file.
39 changes: 39 additions & 0 deletions Forge.Tests/Helpers/VitalTestEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright © Gamesmiths Guild.

using Gamesmiths.Forge.Core;
using Gamesmiths.Forge.Cues;
using Gamesmiths.Forge.Effects;
using Gamesmiths.Forge.Events;
using Gamesmiths.Forge.Statescript;
using Gamesmiths.Forge.Tags;

namespace Gamesmiths.Forge.Tests.Helpers;

public class VitalTestEntity : IForgeEntity
{
public VitalAttributeSet VitalAttributeSet { get; }

public EntityAttributes Attributes { get; }

public EntityTags Tags { get; }

public EffectsManager EffectsManager { get; }

public EntityAbilities Abilities { get; }

public EventManager Events { get; }

public Variables SharedVariables { get; } = new Variables();

public VitalTestEntity(TagsManager tagsManager, CuesManager cuesManager)
{
VitalAttributeSet = new VitalAttributeSet();
var originalTags = new TagContainer(tagsManager, []);

EffectsManager = new(this, cuesManager);
Attributes = new(VitalAttributeSet);
Tags = new(originalTags);
Abilities = new(this);
Events = new();
}
}
Loading
Loading