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

1277 lines
53 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using BumpCombat.Combat;
using BumpCombat.Core;
using BumpCombat.Enemies;
using BumpCombat.Player;
using BumpCombat.Progression;
using BumpCombat.Spawning;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
namespace BumpCombat.PlayModeTests
{
public sealed class ArtifactGroggyPlayModeRegressionTests
{
private static readonly BindingFlags NonPublicInstance =
BindingFlags.Instance | BindingFlags.NonPublic;
private bool previousGrantCatalogForTests;
private bool previousProductionModeForTests;
private float previousTimeScale;
[SetUp]
public void SetUp()
{
previousGrantCatalogForTests = ArtifactRewardController.GrantCatalogForTests;
previousProductionModeForTests = RunManager.ForceProductionModeForTests;
previousTimeScale = Time.timeScale;
ArtifactRewardController.GrantCatalogForTests = true;
RunManager.ForceProductionModeForTests = false;
Time.timeScale = 1f;
}
[TearDown]
public void TearDown()
{
ArtifactRewardController.GrantCatalogForTests = previousGrantCatalogForTests;
RunManager.ForceProductionModeForTests = previousProductionModeForTests;
Time.timeScale = previousTimeScale;
}
[UnityTest]
public IEnumerator BarePrefabsUseFiveSevenAndTenSecondGroggyWindows()
{
yield return LoadCombatScene();
SpawnDirector director = UnityEngine.Object.FindAnyObjectByType<SpawnDirector>();
PlayerHealth player = UnityEngine.Object.FindAnyObjectByType<PlayerHealth>();
Assert.That(director, Is.Not.Null);
Assert.That(player, Is.Not.Null);
director.enabled = false;
player.GrantInvulnerability(60f);
yield return AssertDefaultDuration(
director,
EnemyKind.ArmoredSkeleton,
RunTimedEvent.Elite,
5f,
"Elite");
yield return AssertDefaultDuration(
director,
EnemyKind.GreatswordSkeleton,
RunTimedEvent.MidBoss,
7f,
"MidBoss");
yield return AssertDefaultDuration(
director,
EnemyKind.Necromancer,
RunTimedEvent.FinalBoss,
10f,
"FinalBoss");
}
[UnityTest]
public IEnumerator InitialShieldUsesOwnedColorsAndDevelopmentKeepsRandomAndRgbCycle()
{
yield return LoadCombatScene();
SpawnDirector director = UnityEngine.Object.FindAnyObjectByType<SpawnDirector>();
PlayerHealth player = UnityEngine.Object.FindAnyObjectByType<PlayerHealth>();
ActiveArtifactController artifacts =
player?.GetComponent<ActiveArtifactController>();
Assert.That(director, Is.Not.Null);
Assert.That(player, Is.Not.Null);
Assert.That(artifacts, Is.Not.Null);
director.enabled = false;
player.GrantInvulnerability(60f);
EnemyController enemy = UnityEngine.Object.Instantiate(
FindCatalogPrefab(director, EnemyKind.ArmoredSkeleton),
new Vector2(20f, 20f),
Quaternion.identity);
yield return WaitForReady(enemy);
List<ActiveArtifactDefinition> ownedArtifacts =
GetOwnedArtifacts(artifacts);
ownedArtifacts.Clear();
ActiveArtifactDefinition redArtifact =
FindCatalogArtifact(artifacts, ArtifactColor.Red);
ActiveArtifactDefinition blueArtifact =
FindCatalogArtifact(artifacts, ArtifactColor.Blue);
Assert.That(artifacts.TryAddArtifact(redArtifact), Is.True);
Assert.That(artifacts.TryAddArtifact(blueArtifact), Is.True);
UnityEngine.Random.State previousRandomState = UnityEngine.Random.state;
try
{
UnityEngine.Random.InitState(8137);
int expectedIndex = UnityEngine.Random.Range(0, 2);
ArtifactColor expectedColor = expectedIndex == 0
? ArtifactColor.Red
: ArtifactColor.Blue;
UnityEngine.Random.InitState(8137);
SetPrivateField(enemy, "initialShieldColorSelected", false);
InvokePrivateNoArgs(enemy, "InitializeInitialShieldColor");
Assert.That(enemy.ShieldColor, Is.EqualTo(expectedColor));
}
finally
{
UnityEngine.Random.state = previousRandomState;
}
ownedArtifacts.Clear();
SetPrivateField(enemy, "initialShieldColorSelected", false);
InvokePrivateNoArgs(enemy, "InitializeInitialShieldColor");
Assert.That(enemy.ShieldColor, Is.EqualTo(ArtifactColor.Green));
SetPrivateField(enemy, "configuredGroggyDuration", 0.02f);
ArtifactColor[] expectedDevelopmentCycle =
{
ArtifactColor.Red,
ArtifactColor.Blue,
ArtifactColor.Green,
};
for (int i = 0; i < expectedDevelopmentCycle.Length; i++)
{
Assert.That(
enemy.RegisterArtifactContact(enemy.ShieldColor, 4100 + i),
Is.EqualTo(EnemyArtifactContactResult.Shielded));
yield return WaitUntilNotGroggy(enemy, 1f);
Assert.That(
enemy.ShieldColor,
Is.EqualTo(expectedDevelopmentCycle[i]));
}
UnityEngine.Object.Destroy(enemy.gameObject);
}
[UnityTest]
public IEnumerator ProductionShieldRotationSkipsUnownedColorsAndUsesNewRewards()
{
yield return LoadCombatScene();
SpawnDirector director = UnityEngine.Object.FindAnyObjectByType<SpawnDirector>();
PlayerHealth player = UnityEngine.Object.FindAnyObjectByType<PlayerHealth>();
ActiveArtifactController artifacts =
player?.GetComponent<ActiveArtifactController>();
RunManager runManager = RunManager.Instance;
Assert.That(director, Is.Not.Null);
Assert.That(player, Is.Not.Null);
Assert.That(artifacts, Is.Not.Null);
Assert.That(runManager, Is.Not.Null);
director.enabled = false;
player.GrantInvulnerability(60f);
EnemyController enemy = UnityEngine.Object.Instantiate(
FindCatalogPrefab(director, EnemyKind.ArmoredSkeleton),
new Vector2(20f, 20f),
Quaternion.identity);
yield return WaitForReady(enemy);
SetPrivateField(runManager, "selectedRunMode", RunMode.Production);
SetPrivateField(runManager, "hasSelectedRunMode", true);
Assert.That(runManager.IsProductionRun, Is.True);
List<ActiveArtifactDefinition> ownedArtifacts =
GetOwnedArtifacts(artifacts);
ownedArtifacts.Clear();
ActiveArtifactDefinition greenArtifact =
FindCatalogArtifact(artifacts, ArtifactColor.Green);
ActiveArtifactDefinition redArtifact =
FindCatalogArtifact(artifacts, ArtifactColor.Red);
ActiveArtifactDefinition blueArtifact =
FindCatalogArtifact(artifacts, ArtifactColor.Blue);
Assert.That(artifacts.TryAddArtifact(greenArtifact), Is.True);
SetPrivateField(enemy, "initialShieldColorSelected", false);
InvokePrivateNoArgs(enemy, "InitializeInitialShieldColor");
Assert.That(enemy.ShieldColor, Is.EqualTo(ArtifactColor.Green));
SetPrivateField(enemy, "configuredGroggyDuration", 0.02f);
Assert.That(
enemy.RegisterArtifactContact(enemy.ShieldColor, 4201),
Is.EqualTo(EnemyArtifactContactResult.Shielded));
yield return WaitUntilNotGroggy(enemy, 1f);
Assert.That(enemy.ShieldColor, Is.EqualTo(ArtifactColor.Green));
Assert.That(artifacts.TryAddArtifact(blueArtifact), Is.True);
Assert.That(
enemy.RegisterArtifactContact(enemy.ShieldColor, 4202),
Is.EqualTo(EnemyArtifactContactResult.Shielded));
yield return WaitUntilNotGroggy(enemy, 1f);
Assert.That(enemy.ShieldColor, Is.EqualTo(ArtifactColor.Blue));
Assert.That(
enemy.RegisterArtifactContact(enemy.ShieldColor, 4203),
Is.EqualTo(EnemyArtifactContactResult.Shielded));
yield return WaitUntilNotGroggy(enemy, 1f);
Assert.That(enemy.ShieldColor, Is.EqualTo(ArtifactColor.Green));
ownedArtifacts.Clear();
Assert.That(artifacts.TryAddArtifact(greenArtifact), Is.True);
SetPrivateField(enemy, "shieldColor", ArtifactColor.Green);
Assert.That(artifacts.TryAddArtifact(redArtifact), Is.True);
Assert.That(
enemy.RegisterArtifactContact(enemy.ShieldColor, 4204),
Is.EqualTo(EnemyArtifactContactResult.Shielded));
yield return WaitUntilNotGroggy(enemy, 1f);
Assert.That(enemy.ShieldColor, Is.EqualTo(ArtifactColor.Red));
Assert.That(artifacts.TryAddArtifact(blueArtifact), Is.True);
Assert.That(
enemy.RegisterArtifactContact(enemy.ShieldColor, 4205),
Is.EqualTo(EnemyArtifactContactResult.Shielded));
yield return WaitUntilNotGroggy(enemy, 1f);
Assert.That(enemy.ShieldColor, Is.EqualTo(ArtifactColor.Blue));
UnityEngine.Object.Destroy(enemy.gameObject);
}
[UnityTest]
public IEnumerator GroggyTimerPausesWithScaledTimeAndExpiresByActualState()
{
yield return LoadCombatScene();
SpawnDirector director = UnityEngine.Object.FindAnyObjectByType<SpawnDirector>();
PlayerHealth player = UnityEngine.Object.FindAnyObjectByType<PlayerHealth>();
Assert.That(director, Is.Not.Null);
Assert.That(player, Is.Not.Null);
director.enabled = false;
player.GrantInvulnerability(60f);
EnemyController enemy = UnityEngine.Object.Instantiate(
FindCatalogPrefab(director, EnemyKind.ArmoredSkeleton),
new Vector2(20f, 20f),
Quaternion.identity);
yield return WaitForReady(enemy);
SetPrivateField(enemy, "configuredGroggyDuration", 0.25f);
Assert.That(
enemy.RegisterArtifactContact(enemy.ShieldColor, 101),
Is.EqualTo(EnemyArtifactContactResult.Shielded));
Assert.That(enemy.IsGroggy, Is.True);
float beforePause = enemy.GroggyTimeRemaining;
Time.timeScale = 0f;
yield return new WaitForSecondsRealtime(0.2f);
Assert.That(enemy.IsGroggy, Is.True);
Assert.That(
enemy.GroggyTimeRemaining,
Is.EqualTo(beforePause).Within(0.025f));
Time.timeScale = 1f;
yield return WaitUntilNotGroggy(enemy, 2f);
Assert.That(enemy.IsGroggy, Is.False);
Assert.That(enemy.IsDamageInvulnerable, Is.True);
Assert.That(enemy.GroggyQualifyingContactCount, Is.Zero);
UnityEngine.Object.Destroy(enemy.gameObject);
}
[UnityTest]
public IEnumerator GroggyOpeningContactCancelsAnActiveEnemyAttack()
{
yield return LoadCombatScene();
SpawnDirector director = UnityEngine.Object.FindAnyObjectByType<SpawnDirector>();
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
PlayerHealth health = player?.GetComponent<PlayerHealth>();
Assert.That(director, Is.Not.Null);
Assert.That(player, Is.Not.Null);
Assert.That(health, Is.Not.Null);
director.enabled = false;
health.GrantInvulnerability(60f);
EnemyController enemy = UnityEngine.Object.Instantiate(
FindCatalogPrefab(director, EnemyKind.ArmoredSkeleton),
(Vector2)player.transform.position + Vector2.right * 0.75f,
Quaternion.identity);
yield return WaitForReady(enemy);
Assert.That(
enemy.TryReposition(
(Vector2)player.transform.position + Vector2.right * 0.75f),
Is.True);
Physics2D.SyncTransforms();
float deadline = Time.realtimeSinceStartup + 3f;
while (Time.realtimeSinceStartup < deadline
&& enemy.State != EnemyState.Warning
&& enemy.State != EnemyState.Active)
{
yield return new WaitForFixedUpdate();
}
Assert.That(
enemy.State,
Is.EqualTo(EnemyState.Warning).Or.EqualTo(EnemyState.Active));
SetPrivateField(enemy, "configuredGroggyDuration", 0.3f);
Assert.That(
enemy.RegisterArtifactContact(enemy.ShieldColor, 102),
Is.EqualTo(EnemyArtifactContactResult.Shielded));
Assert.That(enemy.IsGroggy, Is.True);
Assert.That(enemy.State, Is.EqualTo(EnemyState.Chase));
Assert.That(enemy.StateTimeRemaining, Is.EqualTo(0f));
UnityEngine.Object.Destroy(enemy.gameObject);
}
[UnityTest]
public IEnumerator PulseRingShieldContactCountsButDoesNotRepelOrStun()
{
yield return LoadCombatScene();
SpawnDirector director = UnityEngine.Object.FindAnyObjectByType<SpawnDirector>();
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
PlayerHealth health = player?.GetComponent<PlayerHealth>();
ActiveArtifactController artifacts =
player?.GetComponent<ActiveArtifactController>();
CombatFeedback feedback =
UnityEngine.Object.FindAnyObjectByType<CombatFeedback>();
Assert.That(director, Is.Not.Null);
Assert.That(player, Is.Not.Null);
Assert.That(health, Is.Not.Null);
Assert.That(artifacts, Is.Not.Null);
Assert.That(feedback, Is.Not.Null);
director.enabled = false;
health.GrantInvulnerability(60f);
DisableSceneEnemies();
artifacts.DebugGrantCatalogArtifacts();
SelectArtifact(artifacts, ActiveArtifactEffect.Pulse);
EnemyController enemy = UnityEngine.Object.Instantiate(
FindCatalogPrefab(director, EnemyKind.ArmoredSkeleton),
player.transform.position,
Quaternion.identity);
yield return WaitForReady(enemy);
enemy.enabled = false;
SetPrivateField(
enemy,
"shieldColor",
artifacts.CurrentArtifact.ArtifactColor);
Assert.That(
enemy.ShieldColor,
Is.EqualTo(artifacts.CurrentArtifact.ArtifactColor));
Rigidbody2D enemyBody = enemy.GetComponent<Rigidbody2D>();
Rigidbody2D playerBody = player.GetComponent<Rigidbody2D>();
Vector2 contactPosition = playerBody.position + Vector2.right * 0.8f;
enemyBody.position = contactPosition;
enemyBody.linearVelocity = Vector2.zero;
Physics2D.SyncTransforms();
float healthBefore = enemy.CurrentHealth;
Assert.That(artifacts.TryUseCurrent(false), Is.True);
yield return null;
AssertArtifactImpactVisual(
feedback,
ActiveArtifactEffect.Pulse,
false);
Assert.That(enemy.IsGroggy, Is.True);
Assert.That(enemy.GroggyQualifyingContactCount, Is.EqualTo(1));
Assert.That(enemy.CurrentHealth, Is.EqualTo(healthBefore));
Assert.That(enemy.IsStunned, Is.False);
Assert.That(
enemyBody.position,
Is.EqualTo(contactPosition),
"A shielded PR contact must not repel the target.");
UnityEngine.Object.Destroy(enemy.gameObject);
}
[UnityTest]
public IEnumerator MatchingShieldContactsRenderAllSixNormalAndChargedImpactsWithoutDamage()
{
yield return LoadCombatScene();
SpawnDirector director = UnityEngine.Object.FindAnyObjectByType<SpawnDirector>();
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
PlayerHealth health = player?.GetComponent<PlayerHealth>();
ActiveArtifactController artifacts =
player?.GetComponent<ActiveArtifactController>();
CombatFeedback feedback =
UnityEngine.Object.FindAnyObjectByType<CombatFeedback>();
Assert.That(director, Is.Not.Null);
Assert.That(player, Is.Not.Null);
Assert.That(health, Is.Not.Null);
Assert.That(artifacts, Is.Not.Null);
Assert.That(feedback, Is.Not.Null);
director.enabled = false;
health.GrantInvulnerability(60f);
DisableSceneEnemies();
artifacts.DebugGrantCatalogArtifacts();
ActiveArtifactEffect[] effects =
{
ActiveArtifactEffect.Dash,
ActiveArtifactEffect.Pulse,
ActiveArtifactEffect.Phoenix,
ActiveArtifactEffect.Cyclone,
ActiveArtifactEffect.ThunderCrash,
ActiveArtifactEffect.ChainLightning,
};
int validHitEvents = 0;
System.Action<CombatHitResult> observeHit = result =>
{
if (result.Attacker == player.gameObject)
{
validHitEvents++;
}
};
CombatEvents.OnValidHit += observeHit;
try
{
foreach (ActiveArtifactEffect effect in effects)
{
SelectArtifact(artifacts, effect);
ActiveArtifactDefinition definition = artifacts.CurrentArtifact;
Assert.That(definition, Is.Not.Null);
foreach (bool charged in new[] { false, true })
{
EnemyController enemy = SpawnImpactEnemy(
director,
EnemyKind.ArmoredSkeleton,
player);
yield return WaitForReady(enemy);
enemy.enabled = false;
SetPrivateField(enemy, "shieldColor", definition.ArtifactColor);
float healthBefore = enemy.CurrentHealth;
Assert.That(
InvokeApplyArtifactHit(
artifacts,
enemy,
definition,
charged,
1000 + (int)effect * 2 + (charged ? 1 : 0)),
Is.False,
"A shield contact has no HP damage to report.");
Assert.That(enemy.CurrentHealth, Is.EqualTo(healthBefore));
Assert.That(enemy.LastAppliedDamage, Is.EqualTo(0f));
Assert.That(enemy.GroggyQualifyingContactCount, Is.EqualTo(1));
Assert.That(enemy.IsGroggy, Is.True);
Assert.That(enemy.LastArtifactContactCounted, Is.True);
Assert.That(validHitEvents, Is.Zero);
AssertArtifactImpactVisual(feedback, effect, charged);
// Each impact frame advances on Update; allow batch-run
// frame scheduling while still bounding cleanup time.
float cleanupDeadline = Time.realtimeSinceStartup + 1f;
while (feedback.ActiveArtifactImpactVisualCount > 0
&& Time.realtimeSinceStartup < cleanupDeadline)
{
yield return null;
}
Assert.That(feedback.ActiveArtifactImpactVisualCount, Is.Zero);
UnityEngine.Object.Destroy(enemy.gameObject);
yield return null;
}
}
}
finally
{
CombatEvents.OnValidHit -= observeHit;
}
}
[UnityTest]
public IEnumerator ShieldImpactRequiresNewMatchingContactAndRejectsWrongDuplicateAndProtectedContacts()
{
yield return LoadCombatScene();
SpawnDirector director = UnityEngine.Object.FindAnyObjectByType<SpawnDirector>();
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
PlayerHealth health = player?.GetComponent<PlayerHealth>();
ActiveArtifactController artifacts =
player?.GetComponent<ActiveArtifactController>();
CombatFeedback feedback =
UnityEngine.Object.FindAnyObjectByType<CombatFeedback>();
Assert.That(director, Is.Not.Null);
Assert.That(player, Is.Not.Null);
Assert.That(health, Is.Not.Null);
Assert.That(artifacts, Is.Not.Null);
Assert.That(feedback, Is.Not.Null);
director.enabled = false;
health.GrantInvulnerability(60f);
DisableSceneEnemies();
artifacts.DebugGrantCatalogArtifacts();
SelectArtifact(artifacts, ActiveArtifactEffect.Pulse);
ActiveArtifactDefinition definition = artifacts.CurrentArtifact;
Assert.That(definition, Is.Not.Null);
EnemyController duplicateTarget = SpawnImpactEnemy(
director,
EnemyKind.GreatswordSkeleton,
player);
yield return WaitForReady(duplicateTarget);
duplicateTarget.enabled = false;
SetPrivateField(duplicateTarget, "shieldColor", definition.ArtifactColor);
Assert.That(
InvokeApplyArtifactHit(
artifacts,
duplicateTarget,
definition,
false,
2001),
Is.False);
Assert.That(duplicateTarget.GroggyQualifyingContactCount, Is.EqualTo(1));
Assert.That(duplicateTarget.LastArtifactContactCounted, Is.True);
AssertArtifactImpactVisual(feedback, definition.Effect, false);
yield return new WaitForSecondsRealtime(0.2f);
Assert.That(feedback.ActiveArtifactImpactVisualCount, Is.Zero);
Assert.That(
InvokeApplyArtifactHit(
artifacts,
duplicateTarget,
definition,
false,
2001),
Is.False,
"The same cast must remain suppressed after the first shield contact.");
Assert.That(duplicateTarget.GroggyQualifyingContactCount, Is.EqualTo(1));
Assert.That(duplicateTarget.LastArtifactContactCounted, Is.False);
Assert.That(feedback.ActiveArtifactImpactVisualCount, Is.Zero);
EnemyController wrongColorTarget = SpawnImpactEnemy(
director,
EnemyKind.GreatswordSkeleton,
player);
yield return WaitForReady(wrongColorTarget);
wrongColorTarget.enabled = false;
SetPrivateField(
wrongColorTarget,
"shieldColor",
definition.ArtifactColor == ArtifactColor.Green
? ArtifactColor.Red
: ArtifactColor.Green);
Assert.That(
InvokeApplyArtifactHit(
artifacts,
wrongColorTarget,
definition,
false,
2002),
Is.False);
Assert.That(wrongColorTarget.GroggyQualifyingContactCount, Is.Zero);
Assert.That(wrongColorTarget.LastArtifactContactCounted, Is.False);
Assert.That(feedback.ActiveArtifactImpactVisualCount, Is.Zero);
EnemyController protectedTarget = SpawnImpactEnemy(
director,
EnemyKind.Necromancer,
player);
yield return WaitForReady(protectedTarget);
protectedTarget.enabled = false;
SetPrivateField(protectedTarget, "shieldColor", definition.ArtifactColor);
NecromancerBossController protection =
protectedTarget.GetComponent<NecromancerBossController>();
Assert.That(protection, Is.Not.Null);
SetPrivateField(protection, "desperationPending", true);
Assert.That(protectedTarget.IsDamageInvulnerable, Is.True);
Assert.That(
InvokeApplyArtifactHit(
artifacts,
protectedTarget,
definition,
false,
2003),
Is.False);
Assert.That(protectedTarget.GroggyQualifyingContactCount, Is.Zero);
Assert.That(protectedTarget.LastArtifactContactCounted, Is.False);
Assert.That(feedback.ActiveArtifactImpactVisualCount, Is.Zero);
UnityEngine.Object.Destroy(duplicateTarget.gameObject);
UnityEngine.Object.Destroy(wrongColorTarget.gameObject);
UnityEngine.Object.Destroy(protectedTarget.gameObject);
yield return null;
}
[UnityTest]
public IEnumerator MatchingShieldDashContactsRenderNormalAndChargedImpactsWithoutBumpEvent()
{
yield return LoadCombatScene();
SpawnDirector director = UnityEngine.Object.FindAnyObjectByType<SpawnDirector>();
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
PlayerHealth health = player?.GetComponent<PlayerHealth>();
ActiveArtifactController artifacts =
player?.GetComponent<ActiveArtifactController>();
BumpCombatResolver resolver =
player?.GetComponent<BumpCombatResolver>();
CombatFeedback feedback =
UnityEngine.Object.FindAnyObjectByType<CombatFeedback>();
Assert.That(director, Is.Not.Null);
Assert.That(player, Is.Not.Null);
Assert.That(health, Is.Not.Null);
Assert.That(artifacts, Is.Not.Null);
Assert.That(resolver, Is.Not.Null);
Assert.That(feedback, Is.Not.Null);
director.enabled = false;
health.GrantInvulnerability(60f);
DisableSceneEnemies();
artifacts.DebugGrantCatalogArtifacts();
SelectArtifact(artifacts, ActiveArtifactEffect.Dash);
ActiveArtifactDefinition definition = artifacts.CurrentArtifact;
Assert.That(definition, Is.Not.Null);
EnemyController enemy = SpawnImpactEnemy(
director,
EnemyKind.GreatswordSkeleton,
player);
yield return WaitForReady(enemy);
enemy.enabled = false;
SetPrivateField(enemy, "shieldColor", definition.ArtifactColor);
float healthBefore = enemy.CurrentHealth;
int validHitEvents = 0;
System.Action<CombatHitResult> observeHit = result => validHitEvents++;
CombatEvents.OnValidHit += observeHit;
try
{
Assert.That(
InvokeResolveEnemy(
resolver,
enemy,
player,
definition,
false,
3001),
Is.False);
Assert.That(enemy.CurrentHealth, Is.EqualTo(healthBefore));
Assert.That(enemy.GroggyQualifyingContactCount, Is.EqualTo(1));
AssertArtifactImpactVisual(
feedback,
definition.Effect,
false);
Assert.That(validHitEvents, Is.Zero);
yield return new WaitForSecondsRealtime(0.2f);
Assert.That(feedback.ActiveArtifactImpactVisualCount, Is.Zero);
Assert.That(
InvokeResolveEnemy(
resolver,
enemy,
player,
definition,
true,
3002),
Is.False);
Assert.That(enemy.CurrentHealth, Is.EqualTo(healthBefore));
Assert.That(enemy.GroggyQualifyingContactCount, Is.EqualTo(2));
Assert.That(enemy.IsGroggy, Is.True);
AssertArtifactImpactVisual(
feedback,
definition.Effect,
true);
Assert.That(validHitEvents, Is.Zero);
}
finally
{
CombatEvents.OnValidHit -= observeHit;
UnityEngine.Object.Destroy(enemy.gameObject);
}
yield return null;
}
[UnityTest]
public IEnumerator ActiveArtifactController_AllSixEffectsRegisterOneCastContact()
{
yield return LoadCombatScene();
SpawnDirector director = UnityEngine.Object.FindAnyObjectByType<SpawnDirector>();
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
PlayerHealth health = player?.GetComponent<PlayerHealth>();
ActiveArtifactController artifacts =
player?.GetComponent<ActiveArtifactController>();
DashController dash = player?.GetComponent<DashController>();
Assert.That(director, Is.Not.Null);
Assert.That(player, Is.Not.Null);
Assert.That(health, Is.Not.Null);
Assert.That(artifacts, Is.Not.Null);
director.enabled = false;
health.GrantInvulnerability(60f);
DisableSceneEnemies();
SetPlayerDirection(player, Vector2.right);
EnemyKind targetKind = EnemyKind.GreatswordSkeleton;
ActiveArtifactEffect[] effects =
{
ActiveArtifactEffect.Dash,
ActiveArtifactEffect.Pulse,
ActiveArtifactEffect.Phoenix,
ActiveArtifactEffect.Cyclone,
ActiveArtifactEffect.ThunderCrash,
ActiveArtifactEffect.ChainLightning,
};
List<string> failures = new();
Rigidbody2D playerBody = player.GetComponent<Rigidbody2D>();
CircleCollider2D playerCollider = player.GetComponent<CircleCollider2D>();
foreach (ActiveArtifactEffect effect in effects)
{
artifacts.DebugGrantCatalogArtifacts();
SelectArtifact(artifacts, effect);
ActiveArtifactDefinition definition = artifacts.CurrentArtifact;
if (definition == null)
{
failures.Add($"{effect}: the catalog did not select a definition.");
continue;
}
EnemyController enemy = UnityEngine.Object.Instantiate(
FindCatalogPrefab(director, targetKind),
player.transform.position + Vector3.right,
Quaternion.identity);
yield return WaitForReady(enemy);
enemy.enabled = false;
SetPrivateField(enemy, "shieldColor", definition.ArtifactColor);
Assert.That(enemy.ShieldColor, Is.EqualTo(definition.ArtifactColor));
Vector2 playerStart = playerBody.position;
Rigidbody2D enemyBody = enemy.GetComponent<Rigidbody2D>();
Vector2 targetPosition = GetTargetPosition(
effect,
definition,
playerStart);
enemyBody.position = targetPosition;
enemyBody.linearVelocity = Vector2.zero;
Physics2D.SyncTransforms();
Vector2 targetStart = enemyBody.position;
Bounds playerBoundsBefore = playerCollider != null
? playerCollider.bounds
: default;
Bounds targetBoundsBefore = GetCombinedColliderBounds(enemy);
bool started = artifacts.TryUseCurrent(false);
bool finished = false;
bool contactObserved = false;
if (started)
{
yield return WaitForArtifactToFinish(
artifacts,
dash,
2f,
value => finished = value);
yield return WaitForArtifactContact(
enemy,
2f,
value => contactObserved = value);
}
if (!started
|| !finished
|| !contactObserved
|| enemy.GroggyQualifyingContactCount != 1
|| enemy.IsGroggy)
{
failures.Add(
DescribeArtifactContact(
effect,
definition,
started,
finished,
playerStart,
targetPosition,
targetStart,
playerBody,
playerCollider,
playerBoundsBefore,
enemy,
enemyBody,
targetBoundsBefore));
}
UnityEngine.Object.Destroy(enemy.gameObject);
yield return null;
}
Assert.That(
failures,
Is.Empty,
string.Join(Environment.NewLine, failures));
}
private static IEnumerator AssertDefaultDuration(
SpawnDirector director,
EnemyKind kind,
RunTimedEvent expectedTier,
float expectedDuration,
string label)
{
EnemyController enemy = UnityEngine.Object.Instantiate(
FindCatalogPrefab(director, kind),
new Vector2(20f, 20f),
Quaternion.identity);
yield return WaitForReady(enemy);
Assert.That(enemy.GroggyTier, Is.EqualTo(expectedTier), label);
switch (expectedTier)
{
case RunTimedEvent.Elite:
Assert.That(
enemy.RegisterArtifactContact(enemy.ShieldColor, 201),
Is.EqualTo(EnemyArtifactContactResult.Shielded));
break;
case RunTimedEvent.MidBoss:
Assert.That(
enemy.RegisterArtifactContact(enemy.ShieldColor, 202),
Is.EqualTo(EnemyArtifactContactResult.Shielded));
Assert.That(
enemy.RegisterArtifactContact(enemy.ShieldColor, 203),
Is.EqualTo(EnemyArtifactContactResult.Shielded));
break;
case RunTimedEvent.FinalBoss:
Assert.That(
enemy.RegisterArtifactContact(enemy.ShieldColor, 204),
Is.EqualTo(EnemyArtifactContactResult.Shielded));
Assert.That(
enemy.RegisterArtifactContact(enemy.ShieldColor, 205),
Is.EqualTo(EnemyArtifactContactResult.Shielded));
Assert.That(
enemy.RegisterArtifactContact(enemy.ShieldColor, 206),
Is.EqualTo(EnemyArtifactContactResult.Shielded));
Assert.That(
enemy.RegisterArtifactContact(enemy.ShieldColor, 207),
Is.EqualTo(EnemyArtifactContactResult.Shielded));
break;
}
Assert.That(enemy.IsGroggy, Is.True, label);
Assert.That(enemy.GroggyTimeRemaining, Is.EqualTo(expectedDuration).Within(0.05f), label);
UnityEngine.Object.Destroy(enemy.gameObject);
yield return null;
}
private static EnemyController SpawnImpactEnemy(
SpawnDirector director,
EnemyKind kind,
PlayerController player)
{
EnemyController enemy = UnityEngine.Object.Instantiate(
FindCatalogPrefab(director, kind),
player.transform.position + Vector3.right * 2f,
Quaternion.identity);
return enemy;
}
private static bool InvokeApplyArtifactHit(
ActiveArtifactController artifacts,
EnemyController enemy,
ActiveArtifactDefinition definition,
bool charged,
int castIdentity)
{
MethodInfo method = typeof(ActiveArtifactController).GetMethod(
"ApplyArtifactHit",
NonPublicInstance);
Assert.That(method, Is.Not.Null);
return (bool)method.Invoke(
artifacts,
new object[]
{
enemy,
definition,
0f,
Vector2.right,
0f,
true,
charged,
0f,
castIdentity,
});
}
private static bool InvokeResolveEnemy(
BumpCombatResolver resolver,
EnemyController enemy,
PlayerController player,
ActiveArtifactDefinition definition,
bool charged,
int castIdentity)
{
MethodInfo method = typeof(BumpCombatResolver).GetMethod(
"ResolveEnemy",
NonPublicInstance);
Assert.That(method, Is.Not.Null);
return (bool)method.Invoke(
resolver,
new object[]
{
enemy,
true,
enemy.GroundAnchorPosition,
(Vector2)player.transform.position,
Vector2.right,
(HitSide?)HitSide.Front,
charged,
(float?)0f,
(float?)0f,
definition.ArtifactId,
definition.DamageTag,
(ArtifactColor?)definition.ArtifactColor,
castIdentity,
(ActiveArtifactEffect?)definition.Effect,
});
}
private static void AssertArtifactImpactVisual(
CombatFeedback feedback,
ActiveArtifactEffect effect,
bool charged)
{
string effectName = effect switch
{
ActiveArtifactEffect.Phoenix => "SearingRay",
ActiveArtifactEffect.ChainLightning => "Arc",
_ => effect.ToString(),
};
string mode = charged ? "Charged" : "Normal";
GameObject visual = GameObject.Find(
$"Artifact Impact {effectName} {mode}");
Assert.That(visual, Is.Not.Null);
SpriteRenderer renderer = visual.GetComponent<SpriteRenderer>();
Assert.That(renderer, Is.Not.Null);
Sprite[] expectedFrames = Resources.LoadAll<Sprite>(
CombatFeedback.GetArtifactImpactResourcePath(effect, charged));
Array.Sort(
expectedFrames,
(left, right) => string.CompareOrdinal(left.name, right.name));
Assert.That(
expectedFrames.Length,
Is.EqualTo(CombatFeedback.ArtifactImpactFrameCount));
Assert.That(renderer.sprite, Is.SameAs(expectedFrames[0]));
Assert.That(renderer.color, Is.EqualTo(Color.white));
Assert.That(
visual.transform.localScale,
Is.EqualTo(Vector3.one * 2f));
Assert.That(feedback.ActiveArtifactImpactVisualCount, Is.EqualTo(1));
}
private static IEnumerator LoadCombatScene()
{
AsyncOperation load = SceneManager.LoadSceneAsync("CombatPrototype");
while (!load.isDone)
{
yield return null;
}
yield return null;
yield return null;
yield return null;
// These cases verify contacts and feedback, not mission rewards.
// Mission completion can otherwise pause an artifact mid-execution.
RunMissionTracker missions =
UnityEngine.Object.FindAnyObjectByType<RunMissionTracker>();
if (missions != null)
{
missions.enabled = false;
SetPrivateField(missions, "runHasStarted", false);
}
}
private static IEnumerator WaitForReady(EnemyController enemy)
{
float deadline = Time.realtimeSinceStartup + 3f;
while (Time.realtimeSinceStartup < deadline
&& enemy != null
&& (enemy.MaximumHealth <= 0f || enemy.State == EnemyState.Spawn))
{
yield return null;
}
Assert.That(enemy, Is.Not.Null);
Assert.That(enemy.MaximumHealth, Is.GreaterThan(0f));
Assert.That(enemy.State, Is.Not.EqualTo(EnemyState.Spawn));
}
private static IEnumerator WaitUntilNotGroggy(
EnemyController enemy,
float timeout)
{
float deadline = Time.realtimeSinceStartup + timeout;
while (Time.realtimeSinceStartup < deadline
&& enemy != null
&& enemy.IsGroggy)
{
yield return null;
}
Assert.That(enemy, Is.Not.Null);
Assert.That(enemy.IsGroggy, Is.False);
}
private static IEnumerator WaitForArtifactToFinish(
ActiveArtifactController artifacts,
DashController dash,
float timeout,
Action<bool> completed)
{
float deadline = Time.realtimeSinceStartup + timeout;
while (Time.realtimeSinceStartup < deadline
&& (artifacts.IsExecutingArtifact || (dash != null && dash.IsDashing)))
{
yield return null;
}
completed?.Invoke(
!artifacts.IsExecutingArtifact
&& (dash == null || !dash.IsDashing));
yield return new WaitForSecondsRealtime(0.05f);
}
private static IEnumerator WaitForArtifactContact(
EnemyController enemy,
float timeout,
Action<bool> completed)
{
float deadline = Time.realtimeSinceStartup + timeout;
while (Time.realtimeSinceStartup < deadline
&& enemy != null
&& enemy.GroggyQualifyingContactCount < 1)
{
yield return null;
}
completed?.Invoke(
enemy != null
&& enemy.GroggyQualifyingContactCount >= 1);
}
private static string DescribeArtifactContact(
ActiveArtifactEffect effect,
ActiveArtifactDefinition definition,
bool started,
bool finished,
Vector2 playerStart,
Vector2 requestedTargetPosition,
Vector2 targetStart,
Rigidbody2D playerBody,
CircleCollider2D playerCollider,
Bounds playerBoundsBefore,
EnemyController enemy,
Rigidbody2D enemyBody,
Bounds targetBoundsBefore)
{
Vector2 playerEnd = playerBody != null
? playerBody.position
: Vector2.zero;
Bounds playerBoundsAfter = playerCollider != null
? playerCollider.bounds
: default;
Bounds targetBoundsAfter = GetCombinedColliderBounds(enemy);
Vector2 targetEnd = enemyBody != null
? enemyBody.position
: Vector2.zero;
string targetBody = enemyBody == null
? "missing"
: $"type={enemyBody.bodyType},simulated={enemyBody.simulated},velocity={FormatVector(enemyBody.linearVelocity)}";
return $"{effect} contact regression: started={started},finished={finished},"
+ $"contacts={enemy.GroggyQualifyingContactCount},groggy={enemy.IsGroggy},"
+ $"normalRange={definition.NormalRange:0.###},"
+ $"playerStart={FormatVector(playerStart)},playerEnd={FormatVector(playerEnd)},"
+ $"requestedTarget={FormatVector(requestedTargetPosition)},"
+ $"targetStart={FormatVector(targetStart)},targetEnd={FormatVector(targetEnd)},"
+ $"playerCollider={DescribePlayerCollider(playerCollider)},"
+ $"playerBoundsBefore={FormatBounds(playerBoundsBefore)},"
+ $"playerBoundsAfter={FormatBounds(playerBoundsAfter)},"
+ $"targetBoundsBefore={FormatBounds(targetBoundsBefore)},"
+ $"targetBoundsAfter={FormatBounds(targetBoundsAfter)},"
+ $"targetBody=({targetBody})";
}
private static string DescribePlayerCollider(CircleCollider2D collider)
{
if (collider == null)
{
return "missing";
}
Vector3 scale = collider.transform.lossyScale;
return $"localRadius={collider.radius:0.###},offset={FormatVector(collider.offset)},"
+ $"lossyScale={FormatVector(scale)},"
+ $"worldRadiusX={collider.radius * Mathf.Abs(scale.x):0.###}";
}
private static Bounds GetCombinedColliderBounds(EnemyController enemy)
{
Collider2D[] colliders = enemy != null
? enemy.GetComponentsInChildren<Collider2D>(true)
: Array.Empty<Collider2D>();
if (colliders.Length == 0)
{
return default;
}
Bounds combined = colliders[0].bounds;
for (int i = 1; i < colliders.Length; i++)
{
combined.Encapsulate(colliders[i].bounds);
}
return combined;
}
private static string FormatBounds(Bounds bounds)
{
if (bounds.size == Vector3.zero)
{
return "none";
}
return $"center={FormatVector(bounds.center)},size={FormatVector(bounds.size)}";
}
private static string FormatVector(Vector2 value)
{
return $"({value.x:0.###},{value.y:0.###})";
}
private static string FormatVector(Vector3 value)
{
return $"({value.x:0.###},{value.y:0.###},{value.z:0.###})";
}
private static void DisableSceneEnemies()
{
EnemyController[] enemies = UnityEngine.Object.FindObjectsByType<EnemyController>(
FindObjectsInactive.Exclude,
FindObjectsSortMode.None);
foreach (EnemyController enemy in enemies)
{
enemy.gameObject.SetActive(false);
}
}
private static void SelectArtifact(
ActiveArtifactController artifacts,
ActiveArtifactEffect effect)
{
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.");
}
private static Vector2 GetTargetPosition(
ActiveArtifactEffect effect,
ActiveArtifactDefinition definition,
Vector2 playerPosition)
{
switch (effect)
{
case ActiveArtifactEffect.Dash:
return playerPosition + Vector2.right * 0.65f;
case ActiveArtifactEffect.Pulse:
case ActiveArtifactEffect.Cyclone:
return playerPosition + Vector2.right * 0.2f;
case ActiveArtifactEffect.Phoenix:
return playerPosition + Vector2.right * definition.NormalRange * 0.5f;
case ActiveArtifactEffect.ThunderCrash:
// Use a fixed interior contact point for this smoke test;
// the exact edge is covered by the Thunder Crash range tests.
return playerPosition + new Vector2(0.9f, 0.225f);
case ActiveArtifactEffect.ChainLightning:
return playerPosition + Vector2.right * 0.8f;
default:
return playerPosition + Vector2.right * 0.2f;
}
}
private static EnemyController FindCatalogPrefab(
SpawnDirector director,
EnemyKind kind)
{
string[] fieldNames =
{
"enemyPrefabs",
"elitePrefabs",
"midBossPrefabs",
"finalBossPrefabs",
};
foreach (string fieldName in fieldNames)
{
FieldInfo field = typeof(SpawnDirector).GetField(
fieldName,
NonPublicInstance);
EnemyController[] prefabs = field?.GetValue(director) as EnemyController[];
if (prefabs == null)
{
continue;
}
foreach (EnemyController prefab in prefabs)
{
if (prefab != null
&& prefab.Definition != null
&& prefab.Definition.Kind == kind)
{
return prefab;
}
}
}
Assert.Fail($"Could not find the {kind} catalog prefab.");
return null;
}
private static void SetPlayerDirection(
PlayerController player,
Vector2 direction)
{
SetPrivateField(player, "<FacingDirection>k__BackingField", direction);
SetPrivateField(player, "<MoveDirection>k__BackingField", direction);
}
private static void SetPrivateField(
object target,
string fieldName,
object value)
{
FieldInfo field = target.GetType().GetField(
fieldName,
NonPublicInstance);
Assert.That(field, Is.Not.Null, $"Missing private field {fieldName}.");
field.SetValue(target, value);
}
private static void InvokePrivateNoArgs(object target, string methodName)
{
MethodInfo method = target.GetType().GetMethod(
methodName,
NonPublicInstance);
Assert.That(method, Is.Not.Null, $"Missing private method {methodName}.");
method.Invoke(target, null);
}
private static List<ActiveArtifactDefinition> GetOwnedArtifacts(
ActiveArtifactController artifacts)
{
FieldInfo field = typeof(ActiveArtifactController).GetField(
"ownedArtifacts",
NonPublicInstance);
Assert.That(field, Is.Not.Null);
List<ActiveArtifactDefinition> ownedArtifacts =
field.GetValue(artifacts) as List<ActiveArtifactDefinition>;
Assert.That(ownedArtifacts, Is.Not.Null);
return ownedArtifacts;
}
private static ActiveArtifactDefinition FindCatalogArtifact(
ActiveArtifactController artifacts,
ArtifactColor color)
{
for (int i = 0; i < artifacts.CatalogCount; i++)
{
ActiveArtifactDefinition definition = artifacts.GetCatalogArtifactAt(i);
if (definition != null && definition.ArtifactColor == color)
{
return definition;
}
}
Assert.Fail($"Could not find a {color} catalog artifact.");
return null;
}
}
}