using BumpCombat.Core; using BumpCombat.Combat; using BumpCombat.Player; using NUnit.Framework; using UnityEngine; namespace BumpCombat.Tests { public sealed class PlayerStatsTests { private GameObject owner; private PlayerStats stats; [SetUp] public void SetUp() { owner = new GameObject("PlayerStats Tests"); stats = owner.AddComponent(); } [TearDown] public void TearDown() { Object.DestroyImmediate(owner); } [Test] public void Evaluate_AppliesFlatThenSummedIncreasedThenEachMore() { stats.AddModifier(new StatModifier( "flat", CharacterStat.CollisionDamage, ModifierOperation.Flat, 10f)); stats.AddModifier(new StatModifier( "increased-a", CharacterStat.CollisionDamage, ModifierOperation.Increased, 0.1f)); stats.AddModifier(new StatModifier( "increased-b", CharacterStat.CollisionDamage, ModifierOperation.Increased, 0.2f)); stats.AddModifier(new StatModifier( "more-a", CharacterStat.CollisionDamage, ModifierOperation.More, 0.1f)); stats.AddModifier(new StatModifier( "more-b", CharacterStat.CollisionDamage, ModifierOperation.More, 0.2f)); Assert.That( stats.Evaluate(CharacterStat.CollisionDamage, 100f), Is.EqualTo(188.76f).Within(0.001f)); } [Test] public void CalculateDamage_CollisionModifierDoesNotAffectSpell() { stats.AddModifier(new StatModifier( "collision-increased", CharacterStat.CollisionDamage, ModifierOperation.Increased, 0.5f)); Assert.That( stats.CalculateDamage(10f, DamageTag.Collision), Is.EqualTo(15f).Within(0.001f)); Assert.That( stats.CalculateDamage(10f, DamageTag.Spell), Is.EqualTo(10f).Within(0.001f)); } [Test] public void CalculateDamage_AppliesSharedAttackDamageBeforeCollisionDamage() { stats.AddModifier(new StatModifier( "shared-attack-increased", CharacterStat.AttackDamage, ModifierOperation.Increased, 0.1f)); stats.AddModifier(new StatModifier( "collision-increased", CharacterStat.CollisionDamage, ModifierOperation.Increased, 0.5f)); Assert.That( stats.CalculateDamage(10f, DamageTag.Collision), Is.EqualTo(16.5f).Within(0.001f)); Assert.That( stats.CalculateDamage(10f, DamageTag.Spell), Is.EqualTo(11f).Within(0.001f)); } [Test] public void CalculateDamage_PreservesFractionsUntilTheFinalHit() { stats.AddModifier(new StatModifier( "damage-flat", CharacterStat.CollisionDamage, ModifierOperation.Flat, 0.8f)); stats.AddModifier(new StatModifier( "damage-increased", CharacterStat.CollisionDamage, ModifierOperation.Increased, 0.25f)); stats.AddModifier(new StatModifier( "damage-more", CharacterStat.CollisionDamage, ModifierOperation.More, 0.2f)); float calculatedDamage = stats.CalculateDamage(10f, DamageTag.Collision); Assert.That(calculatedDamage, Is.EqualTo(16.2f).Within(0.001f)); Assert.That( stats.Evaluate(CharacterStat.CollisionDamage, 10f), Is.EqualTo(calculatedDamage).Within(0.001f), "Damage stat evaluation and the public damage API must share the same calculation."); Assert.That(DamageCalculator.FinalizeDamage(calculatedDamage), Is.EqualTo(16f)); Assert.That( stats.CalculateDamage(10.9f, DamageTag.Spell), Is.EqualTo(10.9f).Within(0.001f), "Spell damage has no collision modifiers and keeps its intermediate fraction."); } [Test] public void CalculateIncomingDamage_PreservesFractionsUntilThePlayerHit() { stats.AddModifier(new StatModifier( "incoming-flat", CharacterStat.IncomingDamage, ModifierOperation.Flat, 0.8f)); stats.AddModifier(new StatModifier( "incoming-increased", CharacterStat.IncomingDamage, ModifierOperation.Increased, 0.25f)); stats.AddModifier(new StatModifier( "incoming-more", CharacterStat.IncomingDamage, ModifierOperation.More, 0.2f)); float incomingDamage = stats.CalculateIncomingDamage(10f); Assert.That(incomingDamage, Is.EqualTo(16.2f).Within(0.001f)); Assert.That( stats.Evaluate(CharacterStat.IncomingDamage, 10f), Is.EqualTo(incomingDamage).Within(0.001f)); Assert.That(DamageCalculator.FinalizeDamage(incomingDamage), Is.EqualTo(16f)); } [Test] public void CalculateDamage_PreservesFractionsAcrossMoreStacks() { StatModifier more = new( "damage-more-stacked", CharacterStat.CollisionDamage, ModifierOperation.More, 0.2f, 3); stats.AddModifier(more); stats.AddModifier(more); stats.AddModifier(more); float calculatedDamage = stats.CalculateDamage(10f, DamageTag.Collision); Assert.That(calculatedDamage, Is.EqualTo(17.28f).Within(0.001f)); Assert.That( DamageCalculator.FinalizeDamage(calculatedDamage), Is.EqualTo(17), "10 × 1.2 × 1.2 × 1.2 stays fractional until the final hit."); } [Test] public void Evaluate_IncomingDamageMoreModifierReducesReceivedDamage() { stats.AddModifier(new StatModifier( "artifact.pulse.damage-reduction", CharacterStat.IncomingDamage, ModifierOperation.More, -0.35f)); Assert.That( stats.Evaluate(CharacterStat.IncomingDamage, 20f), Is.EqualTo(13f).Within(0.001f)); } [Test] public void AddModifier_StopsAtMaxStacksAndReportsCurrentStacks() { StatModifier modifier = new( "level-up.move-speed", CharacterStat.MoveSpeed, ModifierOperation.Increased, 0.05f, 2); Assert.That(stats.AddModifier(modifier), Is.True); Assert.That(stats.AddModifier(modifier), Is.True); Assert.That(stats.AddModifier(modifier), Is.False); Assert.That( stats.GetStackCount("level-up.move-speed", CharacterStat.MoveSpeed), Is.EqualTo(2)); AppliedStatModifier applied = stats.GetAppliedModifiers()[0]; Assert.That(applied.CurrentStacks, Is.EqualTo(2)); Assert.That(applied.MaxStacks, Is.EqualTo(2)); } [Test] public void RemoveModifiersFromSource_RemovesEveryStackFromThatSource() { StatModifier modifier = new( "temporary-buff", CharacterStat.ArtifactGaugeGain, ModifierOperation.Increased, 0.1f, 3); stats.AddModifier(modifier); stats.AddModifier(modifier); Assert.That( stats.RemoveModifiersFromSource("temporary-buff"), Is.EqualTo(2)); Assert.That( stats.GetStackCount( "temporary-buff", CharacterStat.ArtifactGaugeGain), Is.Zero); Assert.That( stats.Evaluate(CharacterStat.ArtifactGaugeGain, 5f), Is.EqualTo(5f).Within(0.001f)); } [Test] public void BaseProperties_UseConfiguredDefaultsAndModifiers() { stats.AddModifier(new StatModifier( "level-up.max-health", CharacterStat.MaxHealth, ModifierOperation.Flat, 10f)); stats.AddModifier(new StatModifier( "level-up.move-speed", CharacterStat.MoveSpeed, ModifierOperation.Increased, 0.05f)); Assert.That(stats.MaxHealth, Is.EqualTo(110f).Within(0.001f)); Assert.That(stats.MoveSpeed, Is.EqualTo(3.15f).Within(0.001f)); } [Test] public void CharacterStat_KeepsExistingSerializedValuesAndAppendsAttackDamage() { Assert.That((int)CharacterStat.CollisionDamage, Is.EqualTo(0)); Assert.That((int)CharacterStat.IncomingDamage, Is.EqualTo(8)); Assert.That((int)CharacterStat.AttackDamage, Is.EqualTo(9)); } } }