686 lines
28 KiB
C#
686 lines
28 KiB
C#
using System.Collections;
|
|
using System.Reflection;
|
|
using BumpCombat.Core;
|
|
using BumpCombat.Enemies;
|
|
using BumpCombat.Player;
|
|
using BumpCombat.Progression;
|
|
using BumpCombat.Spawning;
|
|
using BumpCombat.UI;
|
|
using NUnit.Framework;
|
|
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
using UnityEngine.InputSystem.LowLevel;
|
|
using UnityEngine.SceneManagement;
|
|
using UnityEngine.TestTools;
|
|
|
|
namespace BumpCombat.PlayModeTests
|
|
{
|
|
public sealed class GuardSystemPlayModeTests
|
|
{
|
|
private Keyboard virtualKeyboard;
|
|
private InputSettings.BackgroundBehavior previousBackgroundBehavior;
|
|
private InputSettings.EditorInputBehaviorInPlayMode previousEditorInputBehavior;
|
|
private bool previousRunInBackground;
|
|
private bool inputSettingsCaptured;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
ArtifactRewardController.GrantCatalogForTests = true;
|
|
RunManager.ForceProductionModeForTests = false;
|
|
Time.timeScale = 1f;
|
|
}
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
if (virtualKeyboard != null && virtualKeyboard.added)
|
|
{
|
|
InputSystem.QueueStateEvent(virtualKeyboard, new KeyboardState());
|
|
InputSystem.RemoveDevice(virtualKeyboard);
|
|
}
|
|
|
|
virtualKeyboard = null;
|
|
if (inputSettingsCaptured)
|
|
{
|
|
InputSystem.settings.backgroundBehavior = previousBackgroundBehavior;
|
|
InputSystem.settings.editorInputBehaviorInPlayMode =
|
|
previousEditorInputBehavior;
|
|
Application.runInBackground = previousRunInBackground;
|
|
inputSettingsCaptured = false;
|
|
}
|
|
ArtifactRewardController.GrantCatalogForTests = false;
|
|
RunManager.ForceProductionModeForTests = false;
|
|
Time.timeScale = 1f;
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator DInput_ActivatesGuardAndBlocksDamageWithoutHurtOrKnockback()
|
|
{
|
|
yield return LoadCombatPrototype();
|
|
DisableCombatActors();
|
|
EnsureRunStarted();
|
|
Keyboard keyboard = BeginVirtualKeyboardInput();
|
|
PlayerHealth health = FindHealth();
|
|
float startingHealth = health.CurrentHealth;
|
|
bool damageEventRaised = false;
|
|
health.OnDamageTaken += _ => damageEventRaised = true;
|
|
|
|
InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.D));
|
|
yield return null;
|
|
|
|
Assert.That(health.IsGuarding, Is.True);
|
|
Assert.That(health.GuardCooldownRemaining, Is.EqualTo(10f).Within(0.05f));
|
|
Assert.That(health.GuardCooldownNormalized, Is.EqualTo(1f).Within(0.01f));
|
|
Assert.That(health.GuardReadinessNormalized, Is.EqualTo(0f).Within(0.01f));
|
|
Assert.That(
|
|
health.TryTakeDamage(25f, Vector2.right, 1.1f),
|
|
Is.False);
|
|
Assert.That(health.CurrentHealth, Is.EqualTo(startingHealth));
|
|
Assert.That(health.IsHurtMovementLocked, Is.False);
|
|
Assert.That(health.GetComponent<PlayerController>().IsDamageKnockbackActive,
|
|
Is.False);
|
|
Assert.That(damageEventRaised, Is.False);
|
|
|
|
InputSystem.QueueStateEvent(keyboard, new KeyboardState());
|
|
yield return new WaitForSeconds(health.GuardDuration + 0.05f);
|
|
Assert.That(health.IsGuarding, Is.False);
|
|
Assert.That(health.TryTakeDamage(1f, Vector2.zero, 0f), Is.True);
|
|
Assert.That(health.CurrentHealth, Is.LessThan(startingHealth));
|
|
Assert.That(health.IsHurtMovementLocked, Is.True);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Guard_DefaultDurationRemainsActiveAtPointSixSecondsThenExpires()
|
|
{
|
|
yield return LoadCombatPrototype();
|
|
DisableCombatActors();
|
|
EnsureRunStarted();
|
|
PlayerHealth health = FindHealth();
|
|
|
|
Assert.That(health.GuardDuration,
|
|
Is.EqualTo(PlayerHealth.DefaultGuardDuration));
|
|
Assert.That(health.GuardDuration, Is.EqualTo(1f));
|
|
Assert.That(health.TryActivateGuard(), Is.True);
|
|
|
|
yield return new WaitForSeconds(0.6f);
|
|
Assert.That(health.IsGuarding, Is.True,
|
|
"The default one-second guard must still be active after 0.6 seconds.");
|
|
|
|
yield return new WaitForSeconds(health.GuardDuration - 0.6f + 0.05f);
|
|
Assert.That(health.IsGuarding, Is.False,
|
|
"The default guard must expire after its one-second duration.");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator DInput_ActivatesBeforeDamageInTheSameInputUpdate()
|
|
{
|
|
yield return LoadCombatPrototype();
|
|
DisableCombatActors();
|
|
EnsureRunStarted();
|
|
Keyboard keyboard = BeginVirtualKeyboardInput();
|
|
PlayerHealth health = FindHealth();
|
|
float startingHealth = health.CurrentHealth;
|
|
|
|
InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.D));
|
|
InputSystem.Update();
|
|
|
|
Assert.That(
|
|
health.TryTakeDamage(25f, Vector2.right, 1.1f),
|
|
Is.False,
|
|
"A same-frame attack must see D's processed input edge before health.Update runs.");
|
|
Assert.That(health.CurrentHealth, Is.EqualTo(startingHealth));
|
|
Assert.That(health.IsGuarding, Is.True);
|
|
Assert.That(health.IsHurtMovementLocked, Is.False);
|
|
Assert.That(health.GetComponent<PlayerController>().IsDamageKnockbackActive,
|
|
Is.False);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator GuardImpactSignal_OnlyFiresForPositiveGuardBlockedDamage()
|
|
{
|
|
yield return LoadCombatPrototype();
|
|
DisableCombatActors();
|
|
EnsureRunStarted();
|
|
PlayerHealth health = FindHealth();
|
|
int guardBlockedCount = 0;
|
|
int guardImpactCount = 0;
|
|
Vector2 impactDirection = Vector2.zero;
|
|
float artifactGaugeBefore =
|
|
health.GetComponent<ActiveArtifactController>().CurrentGauge;
|
|
health.OnGuardAttackBlocked += _ => guardBlockedCount++;
|
|
health.OnGuardImpact += direction =>
|
|
{
|
|
guardImpactCount++;
|
|
impactDirection = direction;
|
|
};
|
|
|
|
Assert.That(health.TryActivateGuard(), Is.True);
|
|
Assert.That(
|
|
health.GuardSuccessCooldownRechargePercent,
|
|
Is.EqualTo(PlayerHealth.DefaultGuardSuccessCooldownRechargePercent));
|
|
Assert.That(health.TryTakeDamage(0f, Vector2.left, 1.1f), Is.False);
|
|
Assert.That(guardBlockedCount, Is.Zero);
|
|
Assert.That(guardImpactCount, Is.Zero);
|
|
Assert.That(health.GuardCooldownRemaining,
|
|
Is.EqualTo(health.GuardCooldownDuration).Within(0.05f));
|
|
|
|
Assert.That(
|
|
health.TryTakeDamage(25f, new Vector2(3f, 4f), 1.1f),
|
|
Is.False);
|
|
Assert.That(guardBlockedCount, Is.EqualTo(1));
|
|
Assert.That(guardImpactCount, Is.EqualTo(1));
|
|
Assert.That(impactDirection, Is.EqualTo(new Vector2(0.6f, 0.8f)));
|
|
float cooldownAfterFirstBlock = health.GuardCooldownRemaining;
|
|
Assert.That(cooldownAfterFirstBlock,
|
|
Is.EqualTo(health.GuardCooldownDuration * 0.3f).Within(0.05f));
|
|
Assert.That(health.GuardReadinessNormalized,
|
|
Is.EqualTo(0.7f).Within(0.01f));
|
|
|
|
Assert.That(health.TryTakeDamage(0f, Vector2.left, 1.1f), Is.False);
|
|
Assert.That(guardBlockedCount, Is.EqualTo(1));
|
|
Assert.That(guardImpactCount, Is.EqualTo(1));
|
|
|
|
Assert.That(health.TryTakeDamage(25f, Vector2.left, 1.1f), Is.False);
|
|
Assert.That(guardBlockedCount, Is.EqualTo(2));
|
|
Assert.That(guardImpactCount, Is.EqualTo(2));
|
|
Assert.That(health.GuardCooldownRemaining,
|
|
Is.EqualTo(cooldownAfterFirstBlock).Within(0.05f),
|
|
"Only the first positive-damage block in an activation recharges the guard.");
|
|
Assert.That(
|
|
health.GetComponent<ActiveArtifactController>().CurrentGauge,
|
|
Is.EqualTo(artifactGaugeBefore),
|
|
"Guard readiness is independent from the shared artifact gauge.");
|
|
|
|
health.GrantInvulnerability(health.GuardDuration + 0.5f);
|
|
yield return new WaitForSeconds(health.GuardDuration + 0.05f);
|
|
Assert.That(health.IsGuarding, Is.False);
|
|
Assert.That(health.IsInvulnerable, Is.True);
|
|
Assert.That(health.TryTakeDamage(25f, Vector2.left, 1.1f), Is.False);
|
|
Assert.That(guardBlockedCount, Is.EqualTo(2));
|
|
Assert.That(guardImpactCount, Is.EqualTo(2));
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator GuardRecharge_UsesConfiguredPercentOncePerActivationAndClampsToReady()
|
|
{
|
|
yield return LoadCombatPrototype();
|
|
DisableCombatActors();
|
|
EnsureRunStarted();
|
|
PlayerHealth health = FindHealth();
|
|
SetPrivateField(health, "guardCooldownDuration", 0.4f);
|
|
SetPrivateField(health, "guardSuccessCooldownRechargePercent", 25f);
|
|
|
|
Assert.That(health.TryActivateGuard(), Is.True);
|
|
Assert.That(
|
|
health.TryTakeDamage(25f, Vector2.right, 1.1f),
|
|
Is.False);
|
|
Assert.That(health.GuardCooldownRemaining,
|
|
Is.EqualTo(0.3f).Within(0.05f));
|
|
Assert.That(health.TryTakeDamage(25f, Vector2.left, 1.1f), Is.False);
|
|
Assert.That(health.GuardCooldownRemaining,
|
|
Is.EqualTo(0.3f).Within(0.05f),
|
|
"Repeated blocks in one activation must not recharge again.");
|
|
|
|
yield return new WaitForSeconds(health.GuardDuration + 0.05f);
|
|
Assert.That(health.IsGuarding, Is.False);
|
|
Assert.That(health.TryActivateGuard(), Is.True,
|
|
"A new activation is available after its configured cooldown has elapsed.");
|
|
SetPrivateField(health, "guardSuccessCooldownRechargePercent", 0f);
|
|
Assert.That(
|
|
health.TryTakeDamage(25f, Vector2.right, 1.1f),
|
|
Is.False);
|
|
Assert.That(health.GuardCooldownRemaining,
|
|
Is.EqualTo(health.GuardCooldownDuration).Within(0.05f),
|
|
"A zero-percent setting must leave the cooldown unchanged.");
|
|
|
|
yield return new WaitForSeconds(health.GuardDuration + 0.05f);
|
|
Assert.That(health.TryActivateGuard(), Is.True);
|
|
SetPrivateField(health, "guardSuccessCooldownRechargePercent", 100f);
|
|
Assert.That(
|
|
health.TryTakeDamage(25f, Vector2.right, 1.1f),
|
|
Is.False);
|
|
Assert.That(health.GuardCooldownRemaining, Is.EqualTo(0f).Within(0.01f));
|
|
Assert.That(health.GuardReadinessNormalized, Is.EqualTo(1f).Within(0.01f));
|
|
Assert.That(health.IsGuarding, Is.True,
|
|
"Cooldown recharge must not end the current guard window.");
|
|
Assert.That(health.TryActivateGuard(), Is.False,
|
|
"A ready cooldown must not allow a second guard during the active window.");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Guard_BlocksRealMeleeAndProjectileHitsDuringActiveWindow()
|
|
{
|
|
yield return LoadCombatPrototype();
|
|
EnsureRunStarted();
|
|
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
|
|
PlayerHealth health = FindHealth();
|
|
Assert.That(director, Is.Not.Null);
|
|
director.enabled = false;
|
|
DisableCombatActors();
|
|
|
|
Rigidbody2D playerBody = health.GetComponent<Rigidbody2D>();
|
|
Assert.That(playerBody, Is.Not.Null);
|
|
playerBody.position = new Vector2(3f, 0f);
|
|
Physics2D.SyncTransforms();
|
|
|
|
EnemyController prefab = FindCatalogPrefab(director, EnemyKind.Skeleton);
|
|
Assert.That(prefab, Is.Not.Null);
|
|
GameObject instance = Object.Instantiate(
|
|
prefab.gameObject,
|
|
new Vector2(-2f, 0f),
|
|
Quaternion.identity);
|
|
EnemyController enemy = instance.GetComponent<EnemyController>();
|
|
EnemyAttack attack = instance.GetComponent<EnemyAttack>();
|
|
Assert.That(enemy, Is.Not.Null);
|
|
Assert.That(attack, Is.Not.Null);
|
|
yield return null;
|
|
enemy.enabled = false;
|
|
|
|
Assert.That(health.TryActivateGuard(), Is.True);
|
|
float startingHealth = health.CurrentHealth;
|
|
attack.BeginWarning(Vector2.right, health.transform.position, 0.7f);
|
|
attack.Activate();
|
|
playerBody.position = new Vector2(-1.5f, 0f);
|
|
Physics2D.SyncTransforms();
|
|
attack.TickActive(0.05f);
|
|
Assert.That(health.IsGuarding, Is.True);
|
|
Assert.That(health.CurrentHealth, Is.EqualTo(startingHealth));
|
|
Assert.That(health.IsHurtMovementLocked, Is.False);
|
|
Assert.That(health.GetComponent<PlayerController>().IsDamageKnockbackActive,
|
|
Is.False);
|
|
attack.EndAttack(cancelled: true);
|
|
|
|
enemy.enabled = true;
|
|
Collider2D playerCollider = health.GetComponent<Collider2D>();
|
|
Assert.That(playerCollider, Is.Not.Null);
|
|
Vector2 playerHitPoint = playerCollider.bounds.center;
|
|
EnemyProjectile projectile = EnemyProjectile.Launch(
|
|
enemy,
|
|
playerHitPoint - Vector2.right * 2f,
|
|
Vector2.right,
|
|
25f,
|
|
500f,
|
|
10f,
|
|
0.06f,
|
|
false);
|
|
int projectileFrames = 0;
|
|
while (projectile != null && projectileFrames++ < 10)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
Assert.That(
|
|
projectile == null,
|
|
Is.True,
|
|
"The projectile should hit and resolve.");
|
|
Assert.That(health.IsGuarding, Is.True);
|
|
Assert.That(health.CurrentHealth, Is.EqualTo(startingHealth));
|
|
Assert.That(health.IsHurtMovementLocked, Is.False);
|
|
Assert.That(health.GetComponent<PlayerController>().IsDamageKnockbackActive,
|
|
Is.False);
|
|
|
|
enemy.enabled = false;
|
|
yield return new WaitForSeconds(health.GuardDuration + 0.05f);
|
|
Assert.That(health.IsGuarding, Is.False);
|
|
float healthBeforeUnguardedMelee = health.CurrentHealth;
|
|
attack.BeginWarning(Vector2.right, health.transform.position, 0.7f);
|
|
attack.Activate();
|
|
attack.TickActive(0.05f);
|
|
Assert.That(
|
|
health.CurrentHealth,
|
|
Is.LessThan(healthBeforeUnguardedMelee),
|
|
"The same melee setup must deal damage after guard expires.");
|
|
attack.EndAttack(cancelled: true);
|
|
Object.Destroy(instance);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator DamageInvulnerability_ProtectsHurtRecoveryAndExpiresAfterOneSecond()
|
|
{
|
|
yield return LoadCombatPrototype();
|
|
DisableCombatActors();
|
|
EnsureRunStarted();
|
|
PlayerHealth health = FindHealth();
|
|
float startingHealth = health.CurrentHealth;
|
|
|
|
Assert.That(health.TryTakeDamage(1f, Vector2.zero, 0f), Is.True);
|
|
float damageTime = Time.time;
|
|
while (Time.time < damageTime + 0.51f)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
Assert.That(health.IsHurtMovementLocked, Is.False);
|
|
Assert.That(health.IsInvulnerable, Is.True);
|
|
Assert.That(health.TryTakeDamage(1f, Vector2.zero, 0f), Is.False);
|
|
Assert.That(health.CurrentHealth, Is.EqualTo(startingHealth - 1f));
|
|
|
|
while (Time.time < damageTime + health.InvulnerabilityDuration + 0.05f)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
Assert.That(health.IsInvulnerable, Is.False);
|
|
Assert.That(health.TryTakeDamage(1f, Vector2.zero, 0f), Is.True);
|
|
Assert.That(health.CurrentHealth, Is.EqualTo(startingHealth - 2f));
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator DamageInvulnerability_AllowsMovementAndArtifactAfterHurtUnlock()
|
|
{
|
|
yield return LoadCombatPrototype();
|
|
DisableCombatActors();
|
|
EnsureRunStarted();
|
|
Keyboard keyboard = BeginVirtualKeyboardInput();
|
|
PlayerController player = FindHealth().GetComponent<PlayerController>();
|
|
PlayerHealth health = player.GetComponent<PlayerHealth>();
|
|
ActiveArtifactController artifacts =
|
|
player.GetComponent<ActiveArtifactController>();
|
|
Rigidbody2D body = player.GetComponent<Rigidbody2D>();
|
|
|
|
Assert.That(health.TryTakeDamage(1f, Vector2.zero, 0f), Is.True);
|
|
while (health.IsHurtMovementLocked)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
Vector2 positionBeforeMovement = body.position;
|
|
InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.RightArrow));
|
|
yield return new WaitForFixedUpdate();
|
|
yield return new WaitForFixedUpdate();
|
|
InputSystem.QueueStateEvent(keyboard, new KeyboardState());
|
|
Assert.That(body.position.x, Is.GreaterThan(positionBeforeMovement.x));
|
|
Assert.That(health.IsInvulnerable, Is.True);
|
|
Assert.That(artifacts.TryUseCurrent(false), Is.True);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Guard_CooldownStartsOnActivationAndRejectsReactivation()
|
|
{
|
|
yield return LoadCombatPrototype();
|
|
DisableCombatActors();
|
|
EnsureRunStarted();
|
|
PlayerHealth health = FindHealth();
|
|
|
|
Assert.That(health.GuardCooldownRemaining, Is.EqualTo(0f));
|
|
Assert.That(health.TryActivateGuard(), Is.True);
|
|
Assert.That(health.TryActivateGuard(), Is.False);
|
|
Assert.That(health.GuardCooldownRemaining, Is.GreaterThan(9.9f));
|
|
|
|
yield return null;
|
|
Assert.That(health.GuardCooldownRemaining, Is.LessThan(10f));
|
|
Assert.That(health.GuardCooldownRemaining, Is.GreaterThan(9.8f));
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator ProductionGuard_UnlocksWithRunManagerAndBecomesReadyImmediately()
|
|
{
|
|
ArtifactRewardController.GrantCatalogForTests = false;
|
|
yield return LoadCombatPrototype();
|
|
DisableCombatActors();
|
|
|
|
RunManager runManager = RunManager.Instance;
|
|
PlayerHealth health = FindHealth();
|
|
RunTutorialController tutorials =
|
|
Object.FindAnyObjectByType<RunTutorialController>();
|
|
Assert.That(runManager, Is.Not.Null);
|
|
Assert.That(tutorials, Is.Not.Null);
|
|
Assert.That(runManager.IsTitleScreen, Is.True);
|
|
Assert.That(runManager.BeginRun(RunMode.Production), Is.True);
|
|
|
|
Assert.That(runManager.IsProductionRun, Is.True);
|
|
Assert.That(runManager.IsGuardUnlocked, Is.False);
|
|
Assert.That(health.IsGuardAvailable, Is.False);
|
|
Assert.That(health.IsGuarding, Is.False);
|
|
Assert.That(health.GuardReadinessNormalized, Is.EqualTo(0f).Within(0.01f));
|
|
Assert.That(health.TryActivateGuard(), Is.False);
|
|
|
|
runManager.UnlockGuard();
|
|
|
|
Assert.That(runManager.IsGuardUnlocked, Is.True);
|
|
Assert.That(health.IsGuardAvailable, Is.True);
|
|
Assert.That(health.GuardReadinessNormalized, Is.EqualTo(1f));
|
|
Assert.That(tutorials.DebugIsVisible, Is.True);
|
|
Assert.That(tutorials.DebugCurrentTutorial,
|
|
Is.EqualTo(RunTutorialController.TutorialKind.Guard));
|
|
Assert.That(health.TryActivateGuard(), Is.False,
|
|
"The guard tutorial modal must keep gameplay input locked until continued.");
|
|
Assert.That(tutorials.DebugContinue(), Is.True);
|
|
Assert.That(health.TryActivateGuard(), Is.True);
|
|
Assert.That(health.GuardReadinessNormalized, Is.EqualTo(0f).Within(0.0001f));
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Guard_HeldInputDoesNotRepeatAfterCooldownAndRepressActivates()
|
|
{
|
|
yield return LoadCombatPrototype();
|
|
DisableCombatActors();
|
|
EnsureRunStarted();
|
|
Keyboard keyboard = BeginVirtualKeyboardInput();
|
|
PlayerHealth health = FindHealth();
|
|
|
|
InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.D));
|
|
yield return null;
|
|
Assert.That(health.IsGuarding, Is.True);
|
|
|
|
Time.timeScale = 10f;
|
|
float deadline = Time.realtimeSinceStartup + 2f;
|
|
while (health.GuardCooldownRemaining > 0.1f
|
|
&& Time.realtimeSinceStartup < deadline)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
Assert.That(health.GuardCooldownRemaining, Is.LessThanOrEqualTo(0.1f));
|
|
Assert.That(health.IsGuarding, Is.False);
|
|
Time.timeScale = 1f;
|
|
while (health.GuardCooldownRemaining > 0f)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
Assert.That(health.IsGuarding, Is.False);
|
|
|
|
InputSystem.QueueStateEvent(keyboard, new KeyboardState());
|
|
yield return null;
|
|
InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.D));
|
|
yield return null;
|
|
Assert.That(health.IsGuarding, Is.True);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Guard_OverlapsIndependentGrantInvulnerabilityWithoutHurtLock()
|
|
{
|
|
yield return LoadCombatPrototype();
|
|
DisableCombatActors();
|
|
EnsureRunStarted();
|
|
PlayerHealth health = FindHealth();
|
|
|
|
Assert.That(health.TryActivateGuard(), Is.True);
|
|
health.GrantInvulnerability(health.GuardDuration + 0.5f);
|
|
Assert.That(health.IsHurtMovementLocked, Is.False);
|
|
yield return new WaitForSeconds(health.GuardDuration + 0.05f);
|
|
Assert.That(health.IsGuarding, Is.False);
|
|
Assert.That(health.IsInvulnerable, Is.True);
|
|
yield return new WaitForSeconds(0.5f);
|
|
Assert.That(health.IsInvulnerable, Is.False);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Guard_IsUnavailableDuringHurtPauseAndAfterDeath()
|
|
{
|
|
yield return LoadCombatPrototype();
|
|
DisableCombatActors();
|
|
EnsureRunStarted();
|
|
Keyboard keyboard = BeginVirtualKeyboardInput();
|
|
PlayerHealth health = FindHealth();
|
|
|
|
Assert.That(health.TryTakeDamage(1f, Vector2.zero, 0f), Is.True);
|
|
Assert.That(health.IsHurtMovementLocked, Is.True);
|
|
Assert.That(health.TryActivateGuard(), Is.False);
|
|
|
|
yield return new WaitForSeconds(health.InvulnerabilityDuration + 0.05f);
|
|
Assert.That(health.IsHurtMovementLocked, Is.False);
|
|
Assert.That(RunManager.Instance.TryOpenPause(), Is.True);
|
|
InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.D));
|
|
yield return null;
|
|
Assert.That(health.IsGuarding, Is.False);
|
|
Assert.That(RunManager.Instance.ClosePause(), Is.True);
|
|
|
|
InputSystem.QueueStateEvent(keyboard, new KeyboardState());
|
|
yield return null;
|
|
Assert.That(
|
|
health.TryTakeDamage(health.CurrentHealth + 1f, Vector2.zero, 0f),
|
|
Is.True);
|
|
Assert.That(health.CurrentHealth, Is.Zero);
|
|
Assert.That(health.TryActivateGuard(), Is.False);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Guard_TimersPauseWithScaledGameTime()
|
|
{
|
|
yield return LoadCombatPrototype();
|
|
DisableCombatActors();
|
|
EnsureRunStarted();
|
|
PlayerHealth health = FindHealth();
|
|
|
|
Assert.That(health.TryActivateGuard(), Is.True);
|
|
Assert.That(RunManager.Instance.TryOpenPause(), Is.True);
|
|
yield return new WaitForSecondsRealtime(0.7f);
|
|
Assert.That(health.IsGuarding, Is.True);
|
|
Assert.That(health.GuardCooldownRemaining, Is.GreaterThan(9.8f));
|
|
Assert.That(RunManager.Instance.ClosePause(), Is.True);
|
|
|
|
yield return new WaitForSeconds(health.GuardDuration + 0.05f);
|
|
Assert.That(health.IsGuarding, Is.False);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Guard_DisablingClearsActiveGuardWithoutResettingCooldown()
|
|
{
|
|
yield return LoadCombatPrototype();
|
|
DisableCombatActors();
|
|
EnsureRunStarted();
|
|
PlayerHealth health = FindHealth();
|
|
|
|
Assert.That(health.TryActivateGuard(), Is.True);
|
|
health.enabled = false;
|
|
Assert.That(health.IsGuarding, Is.False);
|
|
float remainingCooldown = health.GuardCooldownRemaining;
|
|
Assert.That(remainingCooldown, Is.GreaterThan(9.8f));
|
|
|
|
health.enabled = true;
|
|
Assert.That(health.IsGuarding, Is.False);
|
|
Assert.That(health.TryActivateGuard(), Is.False);
|
|
yield return null;
|
|
}
|
|
|
|
private static PlayerHealth FindHealth()
|
|
{
|
|
PlayerHealth health = Object.FindAnyObjectByType<PlayerHealth>();
|
|
Assert.That(health, Is.Not.Null);
|
|
return health;
|
|
}
|
|
|
|
private static void SetPrivateField(object target, string fieldName, object value)
|
|
{
|
|
FieldInfo field = target.GetType().GetField(
|
|
fieldName,
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
Assert.That(field, Is.Not.Null, $"Expected private field '{fieldName}'.");
|
|
field.SetValue(target, value);
|
|
}
|
|
|
|
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,
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static void EnsureRunStarted()
|
|
{
|
|
RunManager runManager = RunManager.Instance;
|
|
Assert.That(runManager, Is.Not.Null);
|
|
if (runManager.IsTitleScreen)
|
|
{
|
|
Assert.That(runManager.BeginRun(), Is.True);
|
|
}
|
|
}
|
|
|
|
private Keyboard BeginVirtualKeyboardInput()
|
|
{
|
|
inputSettingsCaptured = true;
|
|
previousBackgroundBehavior = InputSystem.settings.backgroundBehavior;
|
|
previousEditorInputBehavior =
|
|
InputSystem.settings.editorInputBehaviorInPlayMode;
|
|
previousRunInBackground = Application.runInBackground;
|
|
InputSystem.settings.backgroundBehavior =
|
|
InputSettings.BackgroundBehavior.IgnoreFocus;
|
|
InputSystem.settings.editorInputBehaviorInPlayMode =
|
|
InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView;
|
|
Application.runInBackground = true;
|
|
virtualKeyboard = InputSystem.AddDevice<Keyboard>();
|
|
virtualKeyboard.MakeCurrent();
|
|
return virtualKeyboard;
|
|
}
|
|
|
|
private static void DisableCombatActors()
|
|
{
|
|
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
|
|
if (director != null)
|
|
{
|
|
director.enabled = false;
|
|
}
|
|
|
|
foreach (EnemyController enemy in Object.FindObjectsByType<EnemyController>(
|
|
FindObjectsSortMode.None))
|
|
{
|
|
enemy.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
|
|
private static IEnumerator LoadCombatPrototype()
|
|
{
|
|
AsyncOperation load = SceneManager.LoadSceneAsync("CombatPrototype");
|
|
while (!load.isDone)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
yield return null;
|
|
yield return null;
|
|
}
|
|
}
|
|
}
|