1277 lines
53 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|