Files
tiny_tackle_heroes/Assets/_Project/Tests/PlayMode/ArtifactBalanceBuffRegressionTests.cs

432 lines
16 KiB
C#

using System.Collections;
using BumpCombat.Enemies;
using BumpCombat.Player;
using BumpCombat.Progression;
using BumpCombat.Spawning;
using BumpCombat.Core;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
namespace BumpCombat.PlayModeTests
{
public sealed class ArtifactBalanceBuffRegressionTests
{
private GameObject isolationOwner;
[SetUp]
public void SetUp()
{
RunManager.ForceProductionModeForTests = false;
ArtifactRewardController.GrantCatalogForTests = true;
Time.timeScale = 1f;
}
[TearDown]
public void TearDown()
{
if (isolationOwner != null)
{
Object.Destroy(isolationOwner);
}
RunManager.ForceProductionModeForTests = false;
ArtifactRewardController.GrantCatalogForTests = false;
Time.timeScale = 1f;
}
[UnityTest]
public IEnumerator EnemyCharacterModel_ModifiersDriveRuntimeAndRemainInstanceLocal()
{
yield return LoadCombatPrototype();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
if (director != null)
{
director.enabled = false;
}
EnemyController enemy = FindOrdinaryEnemy();
if (enemy == null && director != null)
{
director.DebugSpawnImmediate(1);
yield return null;
enemy = FindOrdinaryEnemy();
}
Assert.That(enemy, Is.Not.Null);
Assert.That(enemy.IsDead, Is.False);
Assert.That(enemy.IsEventEnemy, Is.False);
Assert.That(enemy.IsGroggyTierGated, Is.False);
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
Assert.That(player, Is.Not.Null);
PlayerStats playerStats = player.GetComponent<PlayerStats>();
EnemyModel enemyModel = enemy.GetComponent<EnemyModel>();
Assert.That(enemyModel, Is.Not.Null);
float initialHealth = enemy.CurrentHealth;
float initialMaximumHealth = enemy.MaximumHealth;
float initialMoveSpeed = enemy.MoveSpeed;
float initialAttackDamage = enemy.AttackDamage;
Assert.That(initialMaximumHealth, Is.GreaterThan(0f));
Assert.That(initialHealth, Is.GreaterThan(2f));
Assert.That(initialMoveSpeed, Is.GreaterThan(0f));
Assert.That(initialAttackDamage, Is.GreaterThan(0f));
Assert.That(enemyModel.AddModifier(new StatModifier(
"test.enemy.max-health",
CharacterStat.MaxHealth,
ModifierOperation.Increased,
0.5f)), Is.True);
Assert.That(enemy.MaximumHealth, Is.EqualTo(initialMaximumHealth * 1.5f).Within(0.001f));
Assert.That(enemy.CurrentHealth, Is.EqualTo(initialHealth).Within(0.001f));
Assert.That(enemyModel.AddModifier(new StatModifier(
"test.enemy.move-speed",
CharacterStat.MoveSpeed,
ModifierOperation.Increased,
0.25f)), Is.True);
Assert.That(enemy.MoveSpeed, Is.EqualTo(initialMoveSpeed * 1.25f).Within(0.001f));
Assert.That(enemyModel.AddModifier(new StatModifier(
"test.enemy.attack-damage",
CharacterStat.AttackDamage,
ModifierOperation.Increased,
0.25f)), Is.True);
Assert.That(enemy.AttackDamage, Is.EqualTo(initialAttackDamage * 1.25f).Within(0.001f));
Assert.That(enemyModel.AddModifier(new StatModifier(
"test.enemy.incoming-damage",
CharacterStat.IncomingDamage,
ModifierOperation.Increased,
0.25f)), Is.True);
Assert.That(enemy.ApplyShock(3f, 0.2f), Is.True);
Assert.That(enemy.TryTakeDamage(1.4f, Vector2.zero, 0f, false), Is.True);
Assert.That(enemy.LastAppliedDamage, Is.EqualTo(2f).Within(0.001f));
Assert.That(enemy.CurrentHealth, Is.EqualTo(initialHealth - 2f).Within(0.001f));
isolationOwner = new GameObject("Second Enemy Model");
EnemyModel secondEnemyModel = isolationOwner.AddComponent<EnemyModel>();
secondEnemyModel.ConfigureDefinition(enemy.Definition);
Assert.That(
secondEnemyModel.GetStackCount(
"test.enemy.move-speed",
CharacterStat.MoveSpeed),
Is.Zero);
Assert.That(
playerStats.GetStackCount(
"test.enemy.move-speed",
CharacterStat.MoveSpeed),
Is.Zero);
typeof(EnemyController)
.GetProperty(nameof(enemy.CurrentHealth))
.GetSetMethod(true)
.Invoke(enemy, new object[] { enemy.MaximumHealth });
enemy.enabled = false;
Assert.That(
enemyModel.RemoveModifiersFromSource("test.enemy.max-health"),
Is.EqualTo(1));
Assert.That(enemy.MaximumHealth, Is.EqualTo(initialMaximumHealth).Within(0.001f));
Assert.That(enemy.CurrentHealth, Is.GreaterThan(enemy.MaximumHealth));
enemy.enabled = true;
Assert.That(enemy.CurrentHealth, Is.EqualTo(initialMaximumHealth).Within(0.001f));
}
[UnityTest]
public IEnumerator IgniteLethalTick_UsesSceneDeathLifecycleOnce()
{
yield return LoadCombatPrototype();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
Assert.That(director, Is.Not.Null);
director.enabled = false;
EnemyController enemy = Object.FindAnyObjectByType<EnemyController>();
if (enemy == null)
{
director.DebugSpawnImmediate(1);
yield return null;
enemy = Object.FindAnyObjectByType<EnemyController>();
}
Assert.That(enemy, Is.Not.Null);
int deathCount = 0;
bool effectsClearedAtDeath = false;
enemy.OnDied += deadEnemy =>
{
deathCount++;
effectsClearedAtDeath = deadEnemy.IsDead
&& !deadEnemy.IsIgnited
&& !deadEnemy.IsShocked
&& !deadEnemy.IsBumpVulnerable;
};
Assert.That(enemy.ApplyShock(3f, 0.2f), Is.True);
Assert.That(enemy.ApplyBumpVulnerability(3f, 0.25f), Is.True);
Assert.That(
enemy.ApplyIgnite(3f, 0.5f, enemy.CurrentHealth),
Is.True);
float timeout = 1.5f;
while (deathCount == 0 && timeout > 0f)
{
timeout -= Time.deltaTime;
yield return null;
}
Assert.That(deathCount, Is.EqualTo(1));
Assert.That(effectsClearedAtDeath, Is.True);
float despawnDeadline = Time.realtimeSinceStartup + 3f;
while (enemy != null
&& Time.realtimeSinceStartup < despawnDeadline)
{
yield return null;
}
Assert.That(enemy == null, Is.True);
Assert.That(deathCount, Is.EqualTo(1));
}
[UnityTest]
public IEnumerator PulseAndCycloneBuffs_SurviveSelectionChangesWithExactValues()
{
yield return LoadCombatPrototype();
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
ActiveArtifactController artifacts =
player.GetComponent<ActiveArtifactController>();
PlayerStats stats = player.GetComponent<PlayerStats>();
SelectArtifact(artifacts, ActiveArtifactEffect.Pulse);
Assert.That(artifacts.TryUseCurrent(false), Is.True);
Assert.That(
stats.Evaluate(CharacterStat.IncomingDamage, 20f),
Is.EqualTo(16f).Within(0.001f));
Assert.That(
stats.GetStackCount(
"pulse.damage-reduction",
CharacterStat.IncomingDamage),
Is.EqualTo(1));
Assert.That(artifacts.SelectNext(), Is.True);
Assert.That(
stats.Evaluate(CharacterStat.IncomingDamage, 20f),
Is.EqualTo(16f).Within(0.001f));
SelectArtifact(artifacts, ActiveArtifactEffect.Cyclone);
Assert.That(artifacts.TryUseCurrent(false), Is.True);
Assert.That(artifacts.HasCycloneMoveSpeedBuff, Is.True);
Assert.That(
stats.MoveSpeed,
Is.EqualTo(3.45f).Within(0.001f));
yield return new WaitForSecondsRealtime(0.45f);
Assert.That(artifacts.SelectNext(), Is.True);
Assert.That(artifacts.HasCycloneMoveSpeedBuff, Is.True);
Assert.That(
stats.MoveSpeed,
Is.EqualTo(3.45f).Within(0.001f));
}
[UnityTest]
public IEnumerator CycloneBuff_AddsToGrowthAndPreservesGrowthAfterExpiry()
{
yield return LoadCombatPrototype();
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
ActiveArtifactController artifacts =
player.GetComponent<ActiveArtifactController>();
PlayerStats stats = player.GetComponent<PlayerStats>();
Assert.That(
stats.AddModifier(new StatModifier(
"growth.move-speed",
CharacterStat.MoveSpeed,
ModifierOperation.Increased,
0.1f)),
Is.True);
SelectArtifact(artifacts, ActiveArtifactEffect.Cyclone);
Assert.That(artifacts.TryUseCurrent(false), Is.True);
Assert.That(stats.MoveSpeed, Is.EqualTo(3.75f).Within(0.001f));
yield return new WaitForSecondsRealtime(3.15f);
Assert.That(stats.MoveSpeed, Is.EqualTo(3.3f).Within(0.001f));
Assert.That(
stats.GetStackCount("growth.move-speed", CharacterStat.MoveSpeed),
Is.EqualTo(1));
Assert.That(
stats.GetStackCount("cyclone.move-speed", CharacterStat.MoveSpeed),
Is.Zero);
}
[UnityTest]
public IEnumerator CycloneBuff_StrongReapplicationWinsOverWeakReapplication()
{
yield return LoadCombatPrototype();
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
ActiveArtifactController artifacts =
player.GetComponent<ActiveArtifactController>();
PlayerStats stats = player.GetComponent<PlayerStats>();
SelectArtifact(artifacts, ActiveArtifactEffect.Cyclone);
Assert.That(artifacts.TryUseCurrent(true), Is.True);
yield return new WaitForSecondsRealtime(0.75f);
Assert.That(artifacts.IsExecutingArtifact, Is.False);
Assert.That(artifacts.TryUseCurrent(false), Is.True);
Assert.That(stats.MoveSpeed, Is.EqualTo(3.75f).Within(0.001f));
yield return new WaitForSecondsRealtime(3.15f);
Assert.That(
stats.MoveSpeed,
Is.EqualTo(3.75f).Within(0.001f),
"A weak reapplication must not shorten the strong buff.");
yield return new WaitForSecondsRealtime(1.05f);
Assert.That(stats.MoveSpeed, Is.EqualTo(3f).Within(0.001f));
}
[UnityTest]
public IEnumerator TemporaryBuffs_PauseTimersAndClearOnControllerDisable()
{
yield return LoadCombatPrototype();
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
ActiveArtifactController artifacts =
player.GetComponent<ActiveArtifactController>();
PlayerStats stats = player.GetComponent<PlayerStats>();
SelectArtifact(artifacts, ActiveArtifactEffect.Pulse);
Assert.That(artifacts.TryUseCurrent(false), Is.True);
Assert.That(
stats.GetStackCount(
"pulse.damage-reduction",
CharacterStat.IncomingDamage),
Is.EqualTo(1));
Time.timeScale = 0f;
yield return new WaitForSecondsRealtime(0.35f);
Assert.That(
stats.GetStackCount(
"pulse.damage-reduction",
CharacterStat.IncomingDamage),
Is.EqualTo(1));
Assert.That(
stats.Evaluate(CharacterStat.IncomingDamage, 20f),
Is.EqualTo(16f).Within(0.001f));
artifacts.enabled = false;
Assert.That(
stats.GetStackCount(
"pulse.damage-reduction",
CharacterStat.IncomingDamage),
Is.Zero);
Time.timeScale = 1f;
}
[UnityTest]
public IEnumerator CycloneBuff_CancelledByDamageStillExpiresAndPreservesGrowth()
{
yield return LoadCombatPrototype();
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
PlayerHealth health = player.GetComponent<PlayerHealth>();
ActiveArtifactController artifacts =
player.GetComponent<ActiveArtifactController>();
PlayerStats stats = player.GetComponent<PlayerStats>();
Assert.That(
stats.AddModifier(new StatModifier(
"growth.move-speed.cancel-test",
CharacterStat.MoveSpeed,
ModifierOperation.Increased,
0.1f)),
Is.True);
SelectArtifact(artifacts, ActiveArtifactEffect.Cyclone);
Assert.That(artifacts.TryUseCurrent(false), Is.True);
Assert.That(artifacts.IsExecutingArtifact, Is.True);
Assert.That(health.TryTakeDamage(1f, Vector2.zero, 0f), Is.True);
for (int frame = 0;
frame < 30 && artifacts.IsExecutingArtifact;
frame++)
{
yield return null;
}
Assert.That(artifacts.IsExecutingArtifact, Is.False);
Time.timeScale = 1f;
yield return new WaitForSecondsRealtime(3.2f);
Assert.That(
stats.MoveSpeed,
Is.EqualTo(3.3f).Within(0.001f),
"Cancelling CY must not orphan its three-second expiration.");
Assert.That(
stats.GetStackCount(
"growth.move-speed.cancel-test",
CharacterStat.MoveSpeed),
Is.EqualTo(1));
Assert.That(
stats.GetStackCount(
"cyclone.move-speed",
CharacterStat.MoveSpeed),
Is.Zero);
}
private static IEnumerator LoadCombatPrototype()
{
AsyncOperation load =
SceneManager.LoadSceneAsync("CombatPrototype");
while (!load.isDone)
{
yield return null;
}
yield return null;
yield return null;
yield return null;
}
private static EnemyController FindOrdinaryEnemy()
{
EnemyController[] enemies =
Object.FindObjectsByType<EnemyController>(FindObjectsSortMode.None);
foreach (EnemyController enemy in enemies)
{
if (enemy != null
&& !enemy.IsDead
&& !enemy.IsEventEnemy
&& !enemy.IsGroggyTierGated)
{
return enemy;
}
}
return null;
}
private static void SelectArtifact(
ActiveArtifactController artifacts,
ActiveArtifactEffect effect)
{
Assert.That(artifacts.OwnedArtifactCount, Is.GreaterThanOrEqualTo(2));
for (int i = 0; i < artifacts.OwnedArtifactCount; i++)
{
if (artifacts.CurrentArtifact != null
&& artifacts.CurrentArtifact.Effect == effect)
{
return;
}
Assert.That(artifacts.SelectNext(), Is.True);
}
Assert.Fail($"Artifact {effect} was not found in the debug catalog.");
}
}
}