using System.Collections; 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.InputSystem; using UnityEngine.InputSystem.LowLevel; using UnityEngine.SceneManagement; using UnityEngine.TestTools; namespace BumpCombat.PlayModeTests { public sealed class ArtifactChargeVisualPlayModeTests { private Keyboard virtualKeyboard; private bool virtualKeyboardInputActive; private InputSettings.BackgroundBehavior previousBackgroundBehavior; private InputSettings.EditorInputBehaviorInPlayMode previousEditorInputBehavior; private bool previousRunInBackground; private bool inputSettingsCaptured; [SetUp] public void SetUp() { RunManager.ForceProductionModeForTests = false; ArtifactRewardController.GrantCatalogForTests = true; Time.timeScale = 1f; } [TearDown] public void TearDown() { if (virtualKeyboardInputActive && virtualKeyboard != null && virtualKeyboard.added) { InputSystem.QueueStateEvent(virtualKeyboard, new KeyboardState()); InputSystem.RemoveDevice(virtualKeyboard); } virtualKeyboard = null; virtualKeyboardInputActive = false; 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 Switching_ShowsDestinationBurstOnceAndCleansUp() { yield return LoadCombatPrototype(); ActiveArtifactController artifacts = Object.FindAnyObjectByType(); ArtifactChargeVisual visual = artifacts.GetComponent(); Object.FindAnyObjectByType().enabled = false; foreach (EnemyController enemy in Object.FindObjectsByType(FindObjectsSortMode.None)) { enemy.gameObject.SetActive(false); } Assert.That(visual.IsSwitchVisible, Is.False, "Initial acquisition is not a switch."); float gauge = artifacts.CurrentGauge; for (int i = 0; i < artifacts.OwnedArtifactCount; i++) { Assert.That(artifacts.SelectNext(), Is.True); Assert.That(visual.IsSwitchVisible, Is.True); SpriteRenderer burst = visual.SwitchObject.GetComponent(); Assert.That(burst.sprite.texture.name, Is.EqualTo(artifacts.CurrentArtifact.Effect + "-Release-v1")); Assert.That(burst.color, Is.EqualTo(Color.white)); Assert.That(visual.SwitchObject.transform.parent, Is.EqualTo(artifacts.transform)); yield return null; int bursts = 0; foreach (SpriteRenderer renderer in artifacts.GetComponentsInChildren()) { if (renderer.name == "Artifact Switch") bursts++; } Assert.That(bursts, Is.EqualTo(1), "Rapid switches replace the previous burst."); } Assert.That(artifacts.CurrentGauge, Is.EqualTo(gauge)); GameObject activeBurst = visual.SwitchObject; Sprite pausedFrame = activeBurst.GetComponent().sprite; Time.timeScale = 0f; yield return new WaitForSecondsRealtime(0.4f); Assert.That(visual.SwitchObject, Is.EqualTo(activeBurst)); Assert.That(activeBurst.GetComponent().sprite, Is.EqualTo(pausedFrame)); Time.timeScale = 1f; yield return new WaitForSeconds(ArtifactChargeVisual.SwitchVisualDuration + 0.05f); Assert.That(visual.IsSwitchVisible, Is.False); Assert.That(artifacts.SelectNext(), Is.True); visual.enabled = false; Assert.That(visual.IsSwitchVisible, Is.False); visual.enabled = true; Assert.That(visual.IsSwitchVisible, Is.False); } [UnityTest] public IEnumerator AInput_ChargeVisualsFollowReadyFlashAndReleaseOnce() { yield return LoadCombatPrototype(); SpawnDirector director = Object.FindAnyObjectByType(); if (director != null) { director.enabled = false; } foreach (EnemyController enemy in Object.FindObjectsByType( FindObjectsSortMode.None)) { enemy.gameObject.SetActive(false); } Keyboard keyboard = BeginVirtualKeyboardInput(); PlayerController player = Object.FindAnyObjectByType(); ActiveArtifactController artifacts = player.GetComponent(); Rigidbody2D body = player.GetComponent(); ArtifactChargeVisual visual = player.GetComponent(); Assert.That(visual, Is.Not.Null); SpriteRenderer playerRenderer = player.GetComponent(); Assert.That(playerRenderer, Is.Not.Null); ActiveArtifactDefinition definition = SelectArtifact( artifacts, ActiveArtifactEffect.Dash); EnsureGauge(artifacts, definition.ChargedGaugeCost); Vector2 start = player.transform.position; InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.A)); yield return null; Assert.That(keyboard.aKey.isPressed, Is.True); Assert.That(visual.IsAuraVisible, Is.True); SpriteRenderer auraRenderer = visual.AuraObject.GetComponent(); Assert.That(auraRenderer.sortingLayerID, Is.EqualTo(playerRenderer.sortingLayerID)); Assert.That(auraRenderer.sortingOrder, Is.EqualTo(playerRenderer.sortingOrder - 1)); Assert.That( visual.AuraObject.transform.position, Is.EqualTo((Vector3)(start + Vector2.up * ArtifactChargeVisual.ChargeVisualY))); body.position = start + Vector2.right; Physics2D.SyncTransforms(); yield return null; Vector2 movedPlayerPosition = player.transform.position; Assert.That( visual.AuraObject.transform.position, Is.EqualTo((Vector3)(movedPlayerPosition + Vector2.up * ArtifactChargeVisual.ChargeVisualY))); yield return new WaitForSeconds(definition.ChargeDuration + 0.05f); Assert.That(visual.IsReadyAuraVisible, Is.True); Assert.That(visual.IsAuraVisible, Is.False); Assert.That(visual.FlashPlayCount, Is.EqualTo(1)); yield return null; Assert.That(visual.FlashPlayCount, Is.EqualTo(1)); InputSystem.QueueStateEvent(keyboard, new KeyboardState()); yield return null; Assert.That(keyboard.aKey.isPressed, Is.False); Vector2 releaseOrigin = artifacts.LastArtifactUsePosition; Assert.That(visual.IsReadyAuraVisible, Is.False); Assert.That(visual.ReleasePlayCount, Is.EqualTo(1)); Assert.That(visual.IsReleaseVisible, Is.True); SpriteRenderer releaseRenderer = visual.ReleaseObject.GetComponent(); Assert.That(releaseRenderer.sortingLayerID, Is.EqualTo(playerRenderer.sortingLayerID)); Assert.That(releaseRenderer.sortingOrder, Is.EqualTo(playerRenderer.sortingOrder + 1)); Assert.That( visual.ReleaseObject.transform.position, Is.EqualTo((Vector3)(releaseOrigin + Vector2.up * ArtifactChargeVisual.ChargeVisualY))); body.position = releaseOrigin + Vector2.left; Physics2D.SyncTransforms(); yield return null; Assert.That( visual.ReleaseObject.transform.position, Is.EqualTo((Vector3)(releaseOrigin + Vector2.up * ArtifactChargeVisual.ChargeVisualY))); Sprite pausedReleaseFrame = releaseRenderer.sprite; Assert.That(RunManager.Instance.TryOpenPause(), Is.True); yield return new WaitForSecondsRealtime(0.18f); Assert.That(visual.IsReleaseVisible, Is.True); Assert.That(releaseRenderer.sprite, Is.SameAs(pausedReleaseFrame)); Assert.That(RunManager.Instance.ClosePause(), Is.True); yield return WaitForArtifactCompletion(artifacts, visual); } [UnityTest] public IEnumerator ChainLightning_NoTargetDoesNotEmitRelease() { yield return LoadCombatPrototype(); SpawnDirector director = Object.FindAnyObjectByType(); if (director != null) { director.enabled = false; } foreach (EnemyController enemy in Object.FindObjectsByType( FindObjectsSortMode.None)) { enemy.gameObject.SetActive(false); } Keyboard keyboard = BeginVirtualKeyboardInput(); PlayerController player = Object.FindAnyObjectByType(); ActiveArtifactController artifacts = player.GetComponent(); ArtifactChargeVisual visual = player.GetComponent(); ActiveArtifactDefinition definition = SelectArtifact( artifacts, ActiveArtifactEffect.ChainLightning); EnsureGauge(artifacts, definition.NormalGaugeCost); InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.A)); yield return null; InputSystem.QueueStateEvent(keyboard, new KeyboardState()); yield return null; Assert.That(visual.ReleasePlayCount, Is.Zero); Assert.That(visual.IsAuraVisible, Is.False); Assert.That(visual.IsReadyAuraVisible, Is.False); } [UnityTest] public IEnumerator ChargeVisual_CancelDamageSelectionAndDisableClearImmediately() { yield return LoadCombatPrototype(); DisableCombatActors(); Keyboard keyboard = BeginVirtualKeyboardInput(); PlayerController player = Object.FindAnyObjectByType(); ActiveArtifactController artifacts = player.GetComponent(); ArtifactChargeVisual visual = player.GetComponent(); PlayerHealth health = player.GetComponent(); ActiveArtifactDefinition definition = SelectArtifact( artifacts, ActiveArtifactEffect.Dash); EnsureGauge(artifacts, definition.NormalGaugeCost); yield return PressA(keyboard); Assert.That(visual.IsAuraVisible, Is.True); Assert.That(RunManager.Instance.TryOpenPause(), Is.True); yield return null; Assert.That(visual.IsAuraVisible, Is.False); Assert.That(RunManager.Instance.ClosePause(), Is.True); yield return ReleaseA(keyboard); yield return PressA(keyboard); Assert.That(visual.IsAuraVisible, Is.True); Assert.That( health.TryTakeDamage(1f, Vector2.left, 0f), Is.True); Assert.That(visual.IsAuraVisible, Is.False); yield return ReleaseA(keyboard); yield return new WaitForSeconds(0.6f); yield return PressA(keyboard); Assert.That(visual.IsAuraVisible, Is.True); visual.enabled = false; Assert.That(visual.IsAuraVisible, Is.False); visual.enabled = true; yield return null; Assert.That(visual.IsAuraVisible, Is.True); RunManager.Instance.SetSelectionOpen(true); Assert.That(visual.IsAuraVisible, Is.False); yield return null; RunManager.Instance.SetSelectionOpen(false); yield return null; Assert.That(visual.IsAuraVisible, Is.False); yield return ReleaseA(keyboard); yield return PressA(keyboard); Assert.That(visual.IsAuraVisible, Is.True); artifacts.enabled = false; yield return null; Assert.That(visual.IsAuraVisible, Is.False); artifacts.enabled = true; yield return ReleaseA(keyboard); SetGauge(artifacts, 0f); yield return PressA(keyboard); Assert.That(visual.IsAuraVisible, Is.False); yield return ReleaseA(keyboard); // Hurt ends before damage immunity. The death cleanup check needs // a new accepted hit after the earlier hit's protection expires. float immunityDeadline = Time.realtimeSinceStartup + health.InvulnerabilityDuration + 1f; while (health.IsInvulnerable && Time.realtimeSinceStartup < immunityDeadline) { yield return null; } Assert.That(health.IsInvulnerable, Is.False); EnsureGauge(artifacts, definition.NormalGaugeCost); yield return PressA(keyboard); Assert.That(visual.IsAuraVisible, Is.True); Assert.That( health.TryTakeDamage(health.CurrentHealth + 1f, Vector2.left, 0f), Is.True); Assert.That(visual.IsAuraVisible, Is.False); Assert.That(visual.IsReadyAuraVisible, Is.False); Assert.That(visual.IsFlashVisible, Is.False); Assert.That(visual.IsReleaseVisible, Is.False); } [UnityTest] public IEnumerator SixArtifacts_NormalAndChargedUseEmitOneReleaseEach() { yield return LoadCombatPrototype(); SpawnDirector director = Object.FindAnyObjectByType(); if (director != null) { director.enabled = false; } foreach (EnemyController enemy in Object.FindObjectsByType( FindObjectsSortMode.None)) { enemy.gameObject.SetActive(false); } PlayerController player = Object.FindAnyObjectByType(); Rigidbody2D playerBody = player.GetComponent(); ActiveArtifactController artifacts = player.GetComponent(); ArtifactChargeVisual visual = player.GetComponent(); BeginVirtualKeyboardInput(); ActiveArtifactEffect[] effects = { ActiveArtifactEffect.Dash, ActiveArtifactEffect.Pulse, ActiveArtifactEffect.Phoenix, ActiveArtifactEffect.Cyclone, ActiveArtifactEffect.ThunderCrash, ActiveArtifactEffect.ChainLightning, }; EnemyController chainTarget = null; for (int i = 0; i < effects.Length; i++) { ActiveArtifactDefinition definition = SelectArtifact( artifacts, effects[i]); EnsureGauge(artifacts, definition.ChargedGaugeCost); Invoke(artifacts, "BeginCharge"); yield return null; Assert.That(visual.IsAuraVisible, Is.True); Assert.That( visual.AuraObject.GetComponent().sprite.texture.name, Does.Contain(effects[i].ToString())); int flashCountBefore = visual.FlashPlayCount; SetChargeAsFullyCharged(artifacts, definition); yield return null; Assert.That(visual.IsReadyAuraVisible, Is.True); Assert.That( visual.FlashPlayCount, Is.EqualTo(flashCountBefore + 1)); yield return null; Assert.That( visual.FlashPlayCount, Is.EqualTo(flashCountBefore + 1)); Invoke(artifacts, "CancelCharge"); Assert.That(visual.IsReadyAuraVisible, Is.False); if (effects[i] == ActiveArtifactEffect.ChainLightning) { director.DebugSpawnImmediate(1); yield return null; chainTarget = Object.FindAnyObjectByType(); Assert.That(chainTarget, Is.Not.Null); chainTarget.enabled = false; typeof(EnemyController) .GetProperty("CurrentHealth") ?.SetValue(chainTarget, 100000f); Rigidbody2D targetBody = chainTarget.GetComponent(); targetBody.position = playerBody.position + Vector2.right; Physics2D.SyncTransforms(); } EnsureGauge(artifacts, definition.NormalGaugeCost); int releaseCountBefore = visual.ReleasePlayCount; Assert.That(artifacts.TryUseCurrent(false), Is.True); yield return WaitForArtifactCompletion(artifacts, visual); Assert.That( visual.ReleasePlayCount, Is.EqualTo(releaseCountBefore + 1)); EnsureGauge(artifacts, definition.ChargedGaugeCost); releaseCountBefore = visual.ReleasePlayCount; Assert.That(artifacts.TryUseCurrent(true), Is.True); yield return WaitForArtifactCompletion(artifacts, visual); Assert.That( visual.ReleasePlayCount, Is.EqualTo(releaseCountBefore + 1)); } if (chainTarget != null) { Object.Destroy(chainTarget.gameObject); } } 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(); virtualKeyboard.MakeCurrent(); virtualKeyboardInputActive = true; return virtualKeyboard; } private static IEnumerator PressA(Keyboard keyboard) { InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.A)); yield return null; Assert.That(keyboard.aKey.isPressed, Is.True); } private static IEnumerator ReleaseA(Keyboard keyboard) { InputSystem.QueueStateEvent(keyboard, new KeyboardState()); yield return null; Assert.That(keyboard.aKey.isPressed, Is.False); } private static void DisableCombatActors() { SpawnDirector director = Object.FindAnyObjectByType(); if (director != null) { director.enabled = false; } foreach (EnemyController enemy in Object.FindObjectsByType( FindObjectsSortMode.None)) { enemy.gameObject.SetActive(false); } } private static void SetChargeAsFullyCharged( ActiveArtifactController artifacts, ActiveArtifactDefinition definition) { typeof(ActiveArtifactController) .GetField("chargeStartedAt", BindingFlags.Instance | BindingFlags.NonPublic) ?.SetValue( artifacts, Time.time - definition.ChargeDuration - 0.01f); } private static void Invoke(object target, string method) { target.GetType() .GetMethod(method, BindingFlags.Instance | BindingFlags.NonPublic) ?.Invoke(target, null); } private static IEnumerator LoadCombatPrototype() { AsyncOperation load = SceneManager.LoadSceneAsync("CombatPrototype"); while (!load.isDone) { yield return null; } yield return null; yield return null; } private static IEnumerator WaitForArtifactCompletion( ActiveArtifactController artifacts, ArtifactChargeVisual visual) { DashController dash = artifacts.GetComponent(); float scaledDeadline = Time.time + 2.5f; float realtimeDeadline = Time.realtimeSinceStartup + 3f; while ((artifacts.IsExecutingArtifact || (dash != null && dash.IsDashing) || visual.IsReleaseVisible) && Time.time < scaledDeadline && Time.realtimeSinceStartup < realtimeDeadline) { yield return null; } Assert.That(artifacts.IsExecutingArtifact, Is.False); if (dash != null) { Assert.That(dash.IsDashing, Is.False); } Assert.That(visual.IsReleaseVisible, Is.False); } private static ActiveArtifactDefinition SelectArtifact( ActiveArtifactController artifacts, ActiveArtifactEffect effect) { for (int i = 0; i < artifacts.OwnedArtifactCount; i++) { if (artifacts.CurrentArtifact != null && artifacts.CurrentArtifact.Effect == effect) { return artifacts.CurrentArtifact; } Assert.That(artifacts.SelectNext(), Is.True); } Assert.Fail($"Artifact {effect} was not found in the debug catalog."); return null; } private static void EnsureGauge( ActiveArtifactController artifacts, float required) { while (artifacts.CurrentGauge < required) { artifacts.AddMovementCharge(1f); } } private static void SetGauge( ActiveArtifactController artifacts, float value) { typeof(ActiveArtifactController) .GetField( "k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic) ?.SetValue(artifacts, Mathf.Max(0f, value)); } } }