Files
tiny_tackle_heroes/Assets/_Project/Tests/EditMode/ArtifactBalanceStatusRegressionTests.cs

356 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using BumpCombat.Core;
using System.Reflection;
using BumpCombat.Combat;
using BumpCombat.Enemies;
using BumpCombat.Player;
using NUnit.Framework;
using UnityEngine;
namespace BumpCombat.Tests
{
public sealed class ArtifactBalanceStatusRegressionTests
{
private static readonly BindingFlags NonPublicInstance =
BindingFlags.Instance | BindingFlags.NonPublic;
private GameObject owner;
[SetUp]
public void SetUp()
{
owner = new GameObject("Artifact Balance Status Tests");
}
[TearDown]
public void TearDown()
{
Object.DestroyImmediate(owner);
}
[Test]
public void Ignite_UsesHalfSecondFirstTickAndIncludesThreeSecondBoundary()
{
EnemyController enemy = CreateEnemy("Ignite Boundary Target");
SetAutoProperty(enemy, "CurrentHealth", 100f);
Assert.That(enemy.ApplyIgnite(3f, 0.5f, 3f), Is.True);
MethodInfo updateTimedEffects = GetUpdateTimedEffects();
for (int tick = 1; tick <= 6; tick++)
{
updateTimedEffects.Invoke(enemy, new object[] { 0.5f });
Assert.That(
enemy.CurrentHealth,
Is.EqualTo(100f - tick * 3f).Within(0.001f),
$"Expected ignite tick {tick} at the {tick * 0.5f:0.0}s boundary.");
}
Assert.That(enemy.IsIgnited, Is.False);
Assert.That(enemy.IgniteRemaining, Is.Zero);
}
[Test]
public void Ignite_ReapplicationKeepsStrongValueAndDoesNotDelayNextTick()
{
EnemyController enemy = CreateEnemy("Ignite Reapply Target");
SetAutoProperty(enemy, "CurrentHealth", 100f);
MethodInfo updateTimedEffects = GetUpdateTimedEffects();
Assert.That(enemy.ApplyIgnite(3f, 0.5f, 5f), Is.True);
updateTimedEffects.Invoke(enemy, new object[] { 0.5f });
Assert.That(enemy.CurrentHealth, Is.EqualTo(95f).Within(0.001f));
Assert.That(enemy.ApplyIgnite(1f, 0.5f, 2f), Is.True);
Assert.That(enemy.IgniteRemaining, Is.EqualTo(2.5f).Within(0.001f));
updateTimedEffects.Invoke(enemy, new object[] { 0.5f });
Assert.That(
enemy.CurrentHealth,
Is.EqualTo(90f).Within(0.001f),
"Reapplying ignite must not move the next scheduled tick.");
}
[Test]
public void Ignite_ShockAmplificationFloorsTheFinalTickDamage()
{
EnemyController enemy = CreateEnemy("Shocked Ignite Target");
SetAutoProperty(enemy, "CurrentHealth", 100f);
Assert.That(enemy.ApplyShock(1f, 0.2f), Is.True);
Assert.That(enemy.ApplyIgnite(0.5f, 0.5f, 11f), Is.True);
GetUpdateTimedEffects().Invoke(enemy, new object[] { 0.5f });
Assert.That(
enemy.CurrentHealth,
Is.EqualTo(87f),
"An 11-damage tick amplified by shock is floored from 13.2 to 13 at application.");
}
[Test]
public void DamageTakenEffects_StackByMultiplicationAndOnlyNormalBumpsUseVulnerability()
{
EnemyController enemy = CreateEnemy("Damage Taken Effects Target");
Assert.That(enemy.ApplyShock(4f, 0.2f), Is.True);
Assert.That(enemy.ApplyBumpVulnerability(3f, 0.25f), Is.True);
SetAutoProperty(enemy, "CurrentHealth", 100f);
Assert.That(
enemy.TryTakeDamage(10f, Vector2.zero, 0f, false, false),
Is.True);
Assert.That(enemy.CurrentHealth, Is.EqualTo(88f).Within(0.001f));
SetAutoProperty(enemy, "CurrentHealth", 100f);
Assert.That(
enemy.TryTakeDamage(10f, Vector2.zero, 0f, false, true),
Is.True);
Assert.That(enemy.CurrentHealth, Is.EqualTo(85f).Within(0.001f));
}
[Test]
public void DamageTakenEffects_FloorAfterShockAndVulnerability()
{
EnemyController enemy = CreateEnemy("Truncated Status Target");
Assert.That(enemy.ApplyShock(4f, 0.2f), Is.True);
Assert.That(enemy.ApplyBumpVulnerability(3f, 0.25f), Is.True);
SetAutoProperty(enemy, "CurrentHealth", 100f);
Assert.That(
enemy.TryTakeDamage(13f, Vector2.zero, 0f, false, true),
Is.True);
Assert.That(
enemy.LastReportedDamage,
Is.EqualTo(19f),
"13 × 1.2 × 1.25 = 19.5, with a single final floor to 19.");
Assert.That(enemy.CurrentHealth, Is.EqualTo(81f));
}
[Test]
public void ChainLightningDirectHit_UsesZeroAndOneShockProbabilityPaths()
{
GameObject playerObject = new("Chain Lightning Direct Hit Source");
playerObject.transform.SetParent(owner.transform);
playerObject.AddComponent<PlayerStats>();
playerObject.AddComponent<Rigidbody2D>();
ActiveArtifactController artifacts =
playerObject.AddComponent<ActiveArtifactController>();
typeof(ActiveArtifactController)
.GetMethod("Awake", NonPublicInstance)
?.Invoke(artifacts, null);
MethodInfo applyArtifactHit = typeof(ActiveArtifactController).GetMethod(
"ApplyArtifactHit",
NonPublicInstance);
Assert.That(applyArtifactHit, Is.Not.Null);
ActiveArtifactDefinition noShock = CreateChainDefinition(
"chain-no-shock",
0f);
EnemyController noShockTarget = CreateEnemy("No Shock Target");
SetAutoProperty(noShockTarget, "CurrentHealth", 100f);
Assert.That(
applyArtifactHit.Invoke(
artifacts,
new object[]
{
noShockTarget,
noShock,
1f,
Vector2.right,
0f,
false,
false,
0f,
1,
}),
Is.True);
Assert.That(noShockTarget.IsShocked, Is.False);
ActiveArtifactDefinition guaranteedShock = CreateChainDefinition(
"chain-guaranteed-shock",
1f);
EnemyController guaranteedTarget = CreateEnemy("Guaranteed Shock Target");
SetAutoProperty(guaranteedTarget, "CurrentHealth", 100f);
Assert.That(
applyArtifactHit.Invoke(
artifacts,
new object[]
{
guaranteedTarget,
guaranteedShock,
11f,
Vector2.right,
0f,
false,
false,
0f,
2,
}),
Is.True);
Assert.That(guaranteedTarget.IsShocked, Is.True);
Assert.That(guaranteedTarget.CurrentHealth, Is.EqualTo(89f).Within(0.001f));
Assert.That(
applyArtifactHit.Invoke(
artifacts,
new object[]
{
guaranteedTarget,
guaranteedShock,
11f,
Vector2.right,
0f,
false,
false,
0f,
3,
}),
Is.True);
Assert.That(
guaranteedTarget.CurrentHealth,
Is.EqualTo(76f).Within(0.001f),
"A shock applied by a direct hit amplifies the next hit from 11 to 13 damage.");
Object.DestroyImmediate(noShock);
Object.DestroyImmediate(guaranteedShock);
}
[Test]
public void TimedStatusReapplication_KeepsStrongestValuesAndCoexistsWithoutDuplicates()
{
EnemyController enemy = CreateEnemy("Status Reapplication Target");
Assert.That(enemy.ApplyShock(4f, 0.3f), Is.True);
Assert.That(enemy.ApplyShock(1f, 0.1f), Is.True);
Assert.That(enemy.ShockRemaining, Is.EqualTo(4f).Within(0.001f));
Assert.That(enemy.ShockIncrease, Is.EqualTo(0.3f).Within(0.001f));
Assert.That(enemy.ApplyBumpVulnerability(3f, 0.4f), Is.True);
Assert.That(enemy.ApplyBumpVulnerability(1f, 0.25f), Is.True);
Assert.That(
enemy.BumpVulnerabilityRemaining,
Is.EqualTo(3f).Within(0.001f));
Assert.That(
enemy.BumpVulnerabilityIncrease,
Is.EqualTo(0.4f).Within(0.001f));
Assert.That(enemy.IsShocked, Is.True);
Assert.That(enemy.IsBumpVulnerable, Is.True);
SetAutoProperty(enemy, "CurrentHealth", 100f);
Assert.That(
enemy.TryTakeDamage(10f, Vector2.zero, 0f, false, true),
Is.True);
Assert.That(enemy.LastReportedDamage, Is.EqualTo(18f));
Assert.That(enemy.CurrentHealth, Is.EqualTo(82f));
}
[Test]
public void Ignite_UsesFractionalDamageSnapshotAndFloorsAfterShockAtTick()
{
EnemyController enemy = CreateEnemy("Fractional Ignite Snapshot Target");
SetAutoProperty(enemy, "CurrentHealth", 100f);
GameObject playerObject = new("Fractional Ignite Snapshot Source");
playerObject.transform.SetParent(owner.transform);
PlayerStats stats = playerObject.AddComponent<PlayerStats>();
stats.AddModifier(new StatModifier(
"collision-increased",
CharacterStat.CollisionDamage,
ModifierOperation.Increased,
0.5f));
float snapshotDamage = stats.CalculateDamage(3f, DamageTag.Collision);
Assert.That(snapshotDamage, Is.EqualTo(4.5f).Within(0.001f));
Assert.That(enemy.ApplyShock(2f, 0.2f), Is.True);
Assert.That(enemy.ApplyIgnite(0.5f, 0.5f, snapshotDamage), Is.True);
GetUpdateTimedEffects().Invoke(enemy, new object[] { 0.5f });
Assert.That(enemy.LastReportedDamage, Is.EqualTo(5f));
Assert.That(enemy.CurrentHealth, Is.EqualTo(95f));
}
[Test]
public void Ignite_ConsumesZeroDamageTickWithoutReplayingItAfterShock()
{
EnemyController enemy = CreateEnemy("Zero Ignite Tick Target");
SetAutoProperty(enemy, "CurrentHealth", 100f);
MethodInfo updateTimedEffects = GetUpdateTimedEffects();
Assert.That(enemy.ApplyIgnite(2f, 1f, 0.9f), Is.True);
updateTimedEffects.Invoke(enemy, new object[] { 1f });
Assert.That(enemy.CurrentHealth, Is.EqualTo(100f));
Assert.That(enemy.ApplyShock(2f, 1f), Is.True);
updateTimedEffects.Invoke(enemy, new object[] { 0.5f });
Assert.That(enemy.CurrentHealth, Is.EqualTo(100f), "The first zero-damage tick must not be replayed.");
updateTimedEffects.Invoke(enemy, new object[] { 0.5f });
Assert.That(enemy.CurrentHealth, Is.EqualTo(99f), "Only the next scheduled tick is amplified and applied.");
}
private EnemyController CreateEnemy(string name)
{
GameObject enemyObject = new(name);
enemyObject.transform.SetParent(owner.transform);
enemyObject.AddComponent<Rigidbody2D>();
enemyObject.AddComponent<CircleCollider2D>();
enemyObject.AddComponent<SpriteRenderer>();
EnemyController enemy = enemyObject.AddComponent<EnemyController>();
EnemyAttack enemyAttack = enemyObject.GetComponent<EnemyAttack>();
typeof(EnemyAttack)
.GetMethod("Awake", NonPublicInstance)
?.Invoke(enemyAttack, null);
typeof(EnemyController)
.GetMethod("Awake", NonPublicInstance)
?.Invoke(enemy, null);
return enemy;
}
private static MethodInfo GetUpdateTimedEffects()
{
return typeof(EnemyController).GetMethod(
"UpdateTimedEffects",
NonPublicInstance);
}
private static ActiveArtifactDefinition CreateChainDefinition(
string id,
float shockChance)
{
ActiveArtifactDefinition definition =
ScriptableObject.CreateInstance<ActiveArtifactDefinition>();
definition.Configure(
id,
id,
"AR",
ActiveArtifactEffect.ChainLightning,
DamageTag.Collision,
Color.white,
Color.white,
30f,
30f,
0.8f,
1f,
1f,
2f,
2f,
0f,
0f,
normalElectrocuteChance: shockChance,
normalElectrocuteIncrease: 0.2f,
normalElectrocuteDuration: 3f);
return definition;
}
private static void SetAutoProperty(
object target,
string propertyName,
object value)
{
target.GetType()
.GetField(
$"<{propertyName}>k__BackingField",
NonPublicInstance)
?.SetValue(target, value);
}
}
}