432 lines
16 KiB
C#
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.");
|
|
}
|
|
}
|
|
}
|