Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
527 changes: 426 additions & 101 deletions Forge.Tests/Abilities/AbilitiesTests.cs

Large diffs are not rendered by default.

98 changes: 52 additions & 46 deletions Forge.Tests/Abilities/AbilityBehaviorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public void Behavior_OnStarted_and_OnEnded_are_invoked_per_instance()
AbilityHandle? handle = Grant(entity, data);
handle.Should().NotBeNull();

handle!.Activate(out AbilityActivationFailures failureFlags).Should().BeTrue();
handle!.TryActivate(out AbilityActivationFailures failureFlags).Should().BeTrue();
failureFlags.Should().Be(AbilityActivationFailures.None);
behavior.StartCount.Should().Be(1);
behavior.EndCount.Should().Be(0);
Expand Down Expand Up @@ -58,9 +58,9 @@ public void PerExecution_creates_distinct_behavior_instances()
AbilityHandle? handle = Grant(entity, data);
handle.Should().NotBeNull();

handle!.Activate(out _).Should().BeTrue();
handle.Activate(out _).Should().BeTrue();
handle.Activate(out _).Should().BeTrue();
handle!.TryActivate(out _).Should().BeTrue();
handle.TryActivate(out _).Should().BeTrue();
handle.TryActivate(out _).Should().BeTrue();
behaviors.Should().HaveCount(3);
behaviors.Sum(x => x.StartCount).Should().Be(3);
behaviors.Sum(x => x.EndCount).Should().Be(0);
Expand Down Expand Up @@ -96,23 +96,23 @@ public void Blocked_ability_tags_are_removed_only_after_last_instance_ends()
AbilityHandle? blockerHandle = Grant(entity, blocker);
AbilityHandle? blockedHandle = Grant(entity, blocked);

blockerHandle!.Activate(out _).Should().BeTrue();
blockerHandle!.Activate(out _).Should().BeTrue();
blockerHandle!.TryActivate(out _).Should().BeTrue();
blockerHandle!.TryActivate(out _).Should().BeTrue();

// While any blocker instance active, blocked ability cannot activate.
blockedHandle!.Activate(out AbilityActivationFailures failureFlags).Should().BeFalse();
blockedHandle!.TryActivate(out AbilityActivationFailures failureFlags).Should().BeFalse();
failureFlags.Should().Be(AbilityActivationFailures.BlockedByTags);
blockedHandle.IsActive.Should().BeFalse();

// End one blocker instance; still blocked.
behaviors[0].End();
blockedHandle.Activate(out failureFlags).Should().BeFalse();
blockedHandle.TryActivate(out failureFlags).Should().BeFalse();
failureFlags.Should().Be(AbilityActivationFailures.BlockedByTags);
blockedHandle.IsActive.Should().BeFalse();

// End last blocker instance; now unblocked.
behaviors[1].End();
blockedHandle.Activate(out failureFlags).Should().BeTrue();
blockedHandle.TryActivate(out failureFlags).Should().BeTrue();
failureFlags.Should().Be(AbilityActivationFailures.None);
blockedHandle.IsActive.Should().BeTrue();
}
Expand All @@ -139,9 +139,9 @@ public void Activation_owned_tags_are_applied_on_activation_and_removed_after_la

AbilityHandle? handle = Grant(entity, abilityWithOwned);

handle!.Activate(out _).Should().BeTrue();
handle!.Activate(out _).Should().BeTrue();
handle!.Activate(out _).Should().BeTrue();
handle!.TryActivate(out _).Should().BeTrue();
handle!.TryActivate(out _).Should().BeTrue();
handle!.TryActivate(out _).Should().BeTrue();

entity.Tags.AllTags.HasAll(ownedTags).Should().BeTrue();

Expand Down Expand Up @@ -182,10 +182,10 @@ public void PerEntity_retrigger_invokes_previous_OnEnded_before_new_OnStarted()
AbilityHandle? handle = Grant(entity, data);
handle.Should().NotBeNull();

handle!.Activate(out _).Should().BeTrue();
handle!.TryActivate(out _).Should().BeTrue();

// Second activation retriggers: ability should call previous.OnEnded() before new.OnStarted().
handle.Activate(out _).Should().BeTrue();
handle.TryActivate(out _).Should().BeTrue();

endedBeforeNew.Should().BeTrue("the previous instance should have ended before the new one started");
}
Expand All @@ -203,7 +203,7 @@ public void Context_provides_expected_values()
AbilityHandle? handle = Grant(target, data, sourceEntity: source);
handle.Should().NotBeNull();

handle!.Activate(out AbilityActivationFailures failureFlags, target).Should().BeTrue();
handle!.TryActivate(out AbilityActivationFailures failureFlags, target).Should().BeTrue();
failureFlags.Should().Be(AbilityActivationFailures.None);
captured.Should().NotBeNull();
captured!.Owner.Should().Be(target);
Expand All @@ -225,17 +225,17 @@ public void Behavior_can_end_instance_during_OnStarted()
AbilityHandle? handle = Grant(entity, data);
handle.Should().NotBeNull();

handle!.Activate(out AbilityActivationFailures failureFlags).Should().BeTrue();
handle!.TryActivate(out AbilityActivationFailures failureFlags).Should().BeTrue();
failureFlags.Should().Be(AbilityActivationFailures.None);
handle.IsActive.Should().BeFalse();
}

[Fact]
[Trait("CommitAbility", null)]
[Trait("TryCommitAbility", null)]
public void Behavior_commits_cooldown_and_cost_on_start()
{
var entity = new TestEntity(_tagsManager, _cuesManager);
var behavior = new CallbackBehavior(x => x.AbilityHandle.CommitAbility());
var behavior = new CallbackBehavior(x => x.AbilityHandle.TryCommitAbility());

AbilityData data = CreateAbilityData(
"CommitOnStart",
Expand All @@ -247,18 +247,18 @@ public void Behavior_commits_cooldown_and_cost_on_start()
AbilityHandle? handle = Grant(entity, data);
handle.Should().NotBeNull();

handle!.Activate(out AbilityActivationFailures failureFlags).Should().BeTrue();
handle!.TryActivate(out AbilityActivationFailures failureFlags).Should().BeTrue();
failureFlags.Should().Be(AbilityActivationFailures.None);

entity.Attributes["TestAttributeSet.Attribute90"].BaseValue.Should().Be(baseBefore - 5);

// Attempt re-activate during cooldown should fail.
handle.Activate(out failureFlags).Should().BeFalse();
handle.TryActivate(out failureFlags).Should().BeFalse();
failureFlags.Should().Be(AbilityActivationFailures.Cooldown);

// Advance time until cooldown expires.
entity.EffectsManager.UpdateEffects(2f);
handle.Activate(out failureFlags).Should().BeTrue();
handle.TryActivate(out failureFlags).Should().BeTrue();
failureFlags.Should().Be(AbilityActivationFailures.None);
}

Expand All @@ -274,7 +274,7 @@ public void Exception_in_OnStarted_cancels_instance_and_does_not_crash()
handle.Should().NotBeNull();

// Activation returns success (instance created then canceled).
handle!.Activate(out AbilityActivationFailures failureFlags).Should().BeTrue();
handle!.TryActivate(out AbilityActivationFailures failureFlags).Should().BeTrue();
handle.IsActive.Should().BeFalse();
behavior.StartAttempts.Should().Be(1);
}
Expand All @@ -290,7 +290,7 @@ public void Exception_in_OnEnded_does_not_prevent_deactivation()
AbilityHandle? handle = Grant(entity, data);
handle.Should().NotBeNull();

handle!.Activate(out _).Should().BeTrue();
handle!.TryActivate(out _).Should().BeTrue();
handle.IsActive.Should().BeTrue();

handle.Cancel();
Expand All @@ -313,7 +313,7 @@ public void Null_behavior_instance_is_ignored()
AbilityHandle? handle = Grant(entity, data);
handle.Should().NotBeNull();

handle!.Activate(out _).Should().BeTrue();
handle!.TryActivate(out _).Should().BeTrue();
handle.IsActive.Should().BeTrue();

handle.Cancel();
Expand All @@ -333,7 +333,7 @@ public void OnAbilityEnded_fires_when_ability_instance_ends()
AbilityEndedData? capturedData = null;
entity.Abilities.OnAbilityEnded += x => { capturedData = x; };

handle!.Activate(out AbilityActivationFailures failureFlags).Should().BeTrue();
handle!.TryActivate(out AbilityActivationFailures failureFlags).Should().BeTrue();
failureFlags.Should().Be(AbilityActivationFailures.None);
behavior.StartCount.Should().Be(1);
behavior.EndCount.Should().Be(0);
Expand All @@ -356,14 +356,17 @@ public void Ability_is_granted_and_activated_once()
var behavior = new TrackingBehavior();

AbilityData data = CreateAbilityData("Tracked", behaviorFactory: () => behavior);
entity.Abilities.GrantAbilityAndActivateOnce(
entity.Abilities.TryGrantAbilityAndActivateOnce(
data,
1,
LevelComparison.None,
out AbilityActivationFailures failureFlags);
out AbilityActivationFailures failureFlags,
out AbilityHandle? grantedAbility).Should().BeTrue();

failureFlags.Should().Be(AbilityActivationFailures.None);

// The behavior keeps the instance running, so the transient grant is still in place.
grantedAbility.Should().NotBeNull();
entity.Abilities.GrantedAbilities.Should().ContainSingle();
behavior.StartCount.Should().Be(1);
behavior.EndCount.Should().Be(0);
Expand All @@ -385,19 +388,22 @@ public void Ability_is_granted_and_activated_once_with_typed_data()
"TypedProc",
behaviorFactory: () => new TypedPayloadBehavior<TestActivationData>((_, x) => capturedData = x));

entity.Abilities.GrantAbilityAndActivateOnce(
entity.Abilities.TryGrantAbilityAndActivateOnce(
data,
1,
LevelComparison.None,
new TestActivationData("Proc", 7),
out AbilityActivationFailures failureFlags);
out AbilityActivationFailures failureFlags,
out AbilityHandle? grantedAbility).Should().BeTrue();

failureFlags.Should().Be(AbilityActivationFailures.None);
capturedData.Should().NotBeNull();
capturedData!.StringValue.Should().Be("Proc");
capturedData.IntValue.Should().Be(7);

// Transient grant: the behavior ends the instance, so the ability is removed again.
// Transient grant: the behavior ends the instance, so the ability is removed again and the handle is freed
// even though the activation succeeded.
grantedAbility.Should().BeNull();
entity.Abilities.GrantedAbilities.Should().BeEmpty();
}

Expand Down Expand Up @@ -479,7 +485,7 @@ public void Generic_activate_creates_typed_context_with_payload()
handle.Should().NotBeNull();

var activationData = new TestActivationData("TestValue", 42);
handle!.Activate(activationData, out AbilityActivationFailures failureFlags).Should().BeTrue();
handle!.TryActivate(activationData, out AbilityActivationFailures failureFlags).Should().BeTrue();
failureFlags.Should().Be(AbilityActivationFailures.None);

capturedContext.Should().NotBeNull();
Expand All @@ -504,7 +510,7 @@ public void Non_generic_activate_creates_base_context()
AbilityHandle? handle = Grant(entity, data);
handle.Should().NotBeNull();

handle!.Activate(out AbilityActivationFailures failureFlags).Should().BeTrue();
handle!.TryActivate(out AbilityActivationFailures failureFlags).Should().BeTrue();
failureFlags.Should().Be(AbilityActivationFailures.None);

capturedContext.Should().NotBeNull();
Expand All @@ -526,7 +532,7 @@ public void Value_type_payload_is_preserved_in_context()
handle.Should().NotBeNull();

var activationData = new ValueTypeActivationData(1.5f, 2.5f, 3.5f);
handle!.Activate(activationData, out AbilityActivationFailures failureFlags).Should().BeTrue();
handle!.TryActivate(activationData, out AbilityActivationFailures failureFlags).Should().BeTrue();
failureFlags.Should().Be(AbilityActivationFailures.None);

capturedContext.Should().NotBeNull();
Expand Down Expand Up @@ -556,8 +562,8 @@ public void Context_data_is_passed_through_for_each_instance_in_PerExecution()
var activationData1 = new TestActivationData("First", 1);
var activationData2 = new TestActivationData("Second", 2);

handle!.Activate(activationData1, out _).Should().BeTrue();
handle.Activate(activationData2, out _).Should().BeTrue();
handle!.TryActivate(activationData1, out _).Should().BeTrue();
handle.TryActivate(activationData2, out _).Should().BeTrue();

capturedContexts.Should().HaveCount(2);

Expand Down Expand Up @@ -591,8 +597,8 @@ public void PerEntity_retrigger_passes_new_context_data()
var activationData1 = new TestActivationData("First", 1);
var activationData2 = new TestActivationData("Second", 2);

handle!.Activate(activationData1, out _).Should().BeTrue();
handle.Activate(activationData2, out _).Should().BeTrue();
handle!.TryActivate(activationData1, out _).Should().BeTrue();
handle.TryActivate(activationData2, out _).Should().BeTrue();

// Both activations should have succeeded with their own context data
capturedContexts.Should().HaveCount(2);
Expand Down Expand Up @@ -712,7 +718,7 @@ public void Event_triggered_ability_with_value_type_payload_receives_payload()
public void Event_triggered_ability_respects_cooldown()
{
var entity = new TestEntity(_tagsManager, _cuesManager);
var behavior = new CallbackBehavior(x => x.AbilityHandle.CommitAbility());
var behavior = new CallbackBehavior(x => x.AbilityHandle.TryCommitAbility());
var eventTag = Tag.RequestTag(_tagsManager, "color.dark.red");

AbilityData data = CreateAbilityData(
Expand Down Expand Up @@ -778,7 +784,7 @@ public void Context_contains_magnitude_when_activated_with_magnitude()
AbilityHandle? handle = Grant(entity, data);
handle.Should().NotBeNull();

handle!.Activate(out AbilityActivationFailures failureFlags, magnitude: 75.5f).Should().BeTrue();
handle!.TryActivate(out AbilityActivationFailures failureFlags, magnitude: 75.5f).Should().BeTrue();
failureFlags.Should().Be(AbilityActivationFailures.None);

capturedContext.Should().NotBeNull();
Expand All @@ -799,7 +805,7 @@ public void Context_magnitude_defaults_to_zero_when_not_specified()
AbilityHandle? handle = Grant(entity, data);
handle.Should().NotBeNull();

handle!.Activate(out AbilityActivationFailures failureFlags).Should().BeTrue();
handle!.TryActivate(out AbilityActivationFailures failureFlags).Should().BeTrue();
failureFlags.Should().Be(AbilityActivationFailures.None);

capturedContext.Should().NotBeNull();
Expand Down Expand Up @@ -884,7 +890,7 @@ public void Generic_activate_passes_both_data_and_magnitude()
handle.Should().NotBeNull();

var activationData = new TestActivationData("TestValue", 42);
handle!.Activate(activationData, out AbilityActivationFailures failureFlags, magnitude: 100f).Should().BeTrue();
handle!.TryActivate(activationData, out AbilityActivationFailures failureFlags, magnitude: 100f).Should().BeTrue();
failureFlags.Should().Be(AbilityActivationFailures.None);

capturedContext.Should().NotBeNull();
Expand All @@ -911,9 +917,9 @@ public void Magnitude_is_preserved_across_instances_in_PerExecution()
AbilityHandle? handle = Grant(entity, data);
handle.Should().NotBeNull();

handle!.Activate(out _, magnitude: 10f).Should().BeTrue();
handle.Activate(out _, magnitude: 20f).Should().BeTrue();
handle.Activate(out _, magnitude: 30f).Should().BeTrue();
handle!.TryActivate(out _, magnitude: 10f).Should().BeTrue();
handle.TryActivate(out _, magnitude: 20f).Should().BeTrue();
handle.TryActivate(out _, magnitude: 30f).Should().BeTrue();

capturedMagnitudes.Should().HaveCount(3);
capturedMagnitudes.Should().HaveElementAt(0, 10f);
Expand All @@ -936,8 +942,8 @@ public void PerEntity_retrigger_uses_new_magnitude()
AbilityHandle? handle = Grant(entity, data);
handle.Should().NotBeNull();

handle!.Activate(out _, magnitude: 50f).Should().BeTrue();
handle.Activate(out _, magnitude: 75f).Should().BeTrue();
handle!.TryActivate(out _, magnitude: 50f).Should().BeTrue();
handle.TryActivate(out _, magnitude: 75f).Should().BeTrue();

capturedMagnitudes.Should().HaveCount(2);
capturedMagnitudes.Should().HaveElementAt(0, 50f);
Expand Down
12 changes: 6 additions & 6 deletions Forge.Tests/Abilities/EntityAbilitiesEventsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ public void Activating_an_ability_reports_activated_then_ended()

var log = new EventLog(entity.Abilities);

handle.Activate(out AbilityActivationFailures failureFlags).Should().BeTrue();
handle.TryActivate(out AbilityActivationFailures failureFlags).Should().BeTrue();
failureFlags.Should().Be(AbilityActivationFailures.None);

log.Entries.Should().Equal("Activated");
Expand All @@ -192,7 +192,7 @@ public void A_behavior_that_finishes_synchronously_still_reports_activated_befor

var log = new EventLog(entity.Abilities);

handle.Activate(out _).Should().BeTrue();
handle.TryActivate(out _).Should().BeTrue();

log.Entries.Should().Equal("Activated", "Ended");
handle.IsActive.Should().BeFalse();
Expand All @@ -211,8 +211,8 @@ public void A_second_concurrent_instance_reports_nothing_until_the_last_one_ends

var log = new EventLog(entity.Abilities);

handle.Activate(out _).Should().BeTrue();
handle.Activate(out _).Should().BeTrue();
handle.TryActivate(out _).Should().BeTrue();
handle.TryActivate(out _).Should().BeTrue();

// Both events track the ability, not its instances, so the second concurrent instance is silent.
log.Entries.Should().Equal("Activated");
Expand All @@ -235,7 +235,7 @@ public void A_refused_activation_reports_the_failure_flags()

var log = new EventLog(entity.Abilities);

handle.Activate(out AbilityActivationFailures failureFlags).Should().BeFalse();
handle.TryActivate(out AbilityActivationFailures failureFlags).Should().BeFalse();

log.Entries.Should().Equal("ActivationFailed");
log.Failures.Should().ContainSingle();
Expand All @@ -253,7 +253,7 @@ public void A_successful_activation_reports_no_failure()
CreateAbilityData("Fireball"), 1, LevelComparison.Higher, null);

var log = new EventLog(entity.Abilities);
handle.Activate(out _).Should().BeTrue();
handle.TryActivate(out _).Should().BeTrue();

log.Failures.Should().BeEmpty();
}
Expand Down
2 changes: 1 addition & 1 deletion Forge.Tests/Effects/BlockAbilityTagsComponentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public void Blocked_ability_fails_to_actually_activate()

entity.EffectsManager.ApplyEffect(CreateBlockingEffect(entity, ["item.equipment.weapon.axe"], 10f));

ability.Activate(out AbilityActivationFailures failureFlags).Should().BeFalse();
ability.TryActivate(out AbilityActivationFailures failureFlags).Should().BeFalse();
failureFlags.Should().HaveFlag(AbilityActivationFailures.BlockedByTags);
ability.IsActive.Should().BeFalse();
}
Expand Down
Loading
Loading