Initial commit: Tiny Tackle Heroes Unity project
This commit is contained in:
@@ -0,0 +1,685 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user