using System.Collections; using System.Reflection; using BumpCombat.Combat; using BumpCombat.Core; using BumpCombat.Player; using NUnit.Framework; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.LowLevel; using UnityEngine.TestTools; namespace BumpCombat.PlayModeTests { public sealed class ArtifactSwitchEventPlayModeTests { private GameObject testPlayer; private ActiveArtifactController artifacts; private ActiveArtifactDefinition firstArtifact; private ActiveArtifactDefinition secondArtifact; private Keyboard virtualKeyboard; private bool virtualKeyboardInputActive; private InputSettings.BackgroundBehavior previousBackgroundBehavior; private InputSettings.EditorInputBehaviorInPlayMode previousEditorInputBehavior; private bool inputSettingsCaptured; [SetUp] public void SetUp() { Time.timeScale = 1f; } [TearDown] public void TearDown() { Time.timeScale = 1f; if (virtualKeyboardInputActive && virtualKeyboard != null && virtualKeyboard.added) { InputSystem.QueueStateEvent(virtualKeyboard, new KeyboardState()); InputSystem.RemoveDevice(virtualKeyboard); } if (inputSettingsCaptured) { InputSystem.settings.backgroundBehavior = previousBackgroundBehavior; InputSystem.settings.editorInputBehaviorInPlayMode = previousEditorInputBehavior; } if (testPlayer != null) { Object.Destroy(testPlayer); } if (firstArtifact != null) { Object.Destroy(firstArtifact); } if (secondArtifact != null) { Object.Destroy(secondArtifact); } virtualKeyboard = null; virtualKeyboardInputActive = false; inputSettingsCaptured = false; } [UnityTest] public IEnumerator ArtifactSwitchedEvent_OnlyFiresOnceAfterSuccessfulSwitch() { RunManager previousRunManager = RunManager.Instance; if (previousRunManager == null) { previousRunManager = null; } bool previousRunManagerEnabled = previousRunManager != null && previousRunManager.enabled; if (previousRunManager != null) { previousRunManager.enabled = false; } SetRunManagerInstanceForTest(null); Time.timeScale = 1f; ActiveArtifactController[] existingControllers = Object.FindObjectsByType( FindObjectsSortMode.None); bool[] previousControllerStates = new bool[existingControllers.Length]; for (int i = 0; i < existingControllers.Length; i++) { previousControllerStates[i] = existingControllers[i].enabled; existingControllers[i].enabled = false; } Keyboard keyboard = null; try { testPlayer = new GameObject("Artifact Switch Event Test Player"); testPlayer.AddComponent(); artifacts = testPlayer.AddComponent(); firstArtifact = CreateArtifact( "switch-event-dash", ActiveArtifactEffect.Dash); secondArtifact = CreateArtifact( "switch-event-pulse", ActiveArtifactEffect.Pulse); int switchCount = 0; int chargeStartCount = 0; ActiveArtifactDefinition eventDefinition = null; ActiveArtifactDefinition selectedDuringEvent = null; artifacts.OnArtifactSwitched += definition => { switchCount++; eventDefinition = definition; selectedDuringEvent = artifacts.CurrentArtifact; }; artifacts.OnArtifactChargeStarted += _ => chargeStartCount++; int useCount = 0; artifacts.OnArtifactUseSucceeded += (_, _) => useCount++; Assert.That(artifacts.TryAddArtifact(firstArtifact), Is.True); Assert.That(artifacts.SelectNext(), Is.False); Assert.That(switchCount, Is.Zero, "Acquisition and a failed switch must not emit the switch event."); artifacts.AddMovementCharge(1f); artifacts.AddMovementCharge(49f); Assert.That(artifacts.CurrentGauge, Is.GreaterThan(0f)); Assert.That(artifacts.CurrentGauge, Is.EqualTo(100f)); Assert.That(switchCount, Is.Zero, "Gauge changes must not emit the switch event."); keyboard = BeginVirtualKeyboardInput(); InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.A)); yield return null; Assert.That(artifacts.IsCharging, Is.True); Assert.That(chargeStartCount, Is.EqualTo(1)); Assert.That(artifacts.SelectNext(), Is.False); Assert.That(switchCount, Is.Zero, "A one-artifact inventory cannot switch and keeps charging."); Assert.That(artifacts.IsCharging, Is.True, "A failed single-slot switch must preserve the charge."); Assert.That(artifacts.TryAddArtifact(secondArtifact), Is.True); Assert.That(switchCount, Is.Zero, "Adding another owned artifact must not be reported as switching."); yield return new WaitForSeconds(firstArtifact.ChargeDuration * 0.5f); Assert.That(artifacts.ChargeProgress, Is.GreaterThan(0f).And.LessThan(1f)); Assert.That(artifacts.SelectNext(), Is.True); Assert.That(artifacts.IsCharging, Is.False, "A successful switch must cancel a partial charge."); Assert.That(switchCount, Is.EqualTo(1)); Assert.That(eventDefinition, Is.SameAs(secondArtifact)); Assert.That(selectedDuringEvent, Is.SameAs(secondArtifact), "The event must observe the newly selected artifact."); Assert.That(artifacts.CurrentArtifact, Is.SameAs(secondArtifact)); Assert.That(artifacts.CurrentGauge, Is.EqualTo(100f), "Switching during a partial charge must not spend gauge."); Assert.That(useCount, Is.Zero, "Switching during a partial charge must not activate an artifact."); InputSystem.QueueStateEvent(keyboard, new KeyboardState()); yield return null; Assert.That(useCount, Is.Zero, "Releasing the held charge after switching must not activate either artifact."); } finally { if (keyboard != null && keyboard.added) { InputSystem.QueueStateEvent(keyboard, new KeyboardState()); } for (int i = 0; i < existingControllers.Length; i++) { if (existingControllers[i] != null) { existingControllers[i].enabled = previousControllerStates[i]; } } if (previousRunManager != null) { previousRunManager.enabled = previousRunManagerEnabled; } SetRunManagerInstanceForTest(previousRunManager); } } [UnityTest] public IEnumerator FullyChargedSwitch_CancelsWithoutActivationOrGaugeCost() { testPlayer = new GameObject("Fully Charged Artifact Switch Test Player"); testPlayer.AddComponent(); artifacts = testPlayer.AddComponent(); firstArtifact = CreateArtifact( "fully-charged-switch-pulse", ActiveArtifactEffect.Pulse); secondArtifact = CreateArtifact( "fully-charged-switch-cyclone", ActiveArtifactEffect.Cyclone); Assert.That(artifacts.TryAddArtifact(firstArtifact), Is.True); Assert.That(artifacts.TryAddArtifact(secondArtifact), Is.True); artifacts.AddMovementCharge(50f); float gaugeBeforeSwitch = artifacts.CurrentGauge; int useCount = 0; artifacts.OnArtifactUseSucceeded += (_, _) => useCount++; InvokePrivate(artifacts, "BeginCharge"); typeof(ActiveArtifactController) .GetField("chargeStartedAt", BindingFlags.Instance | BindingFlags.NonPublic) ?.SetValue( artifacts, Time.time - firstArtifact.ChargeDuration - 0.01f); Assert.That(artifacts.IsFullyCharged, Is.True); Assert.That(artifacts.SelectNext(), Is.True); Assert.That(artifacts.CurrentArtifact, Is.SameAs(secondArtifact)); Assert.That(artifacts.IsCharging, Is.False); Assert.That(artifacts.IsExecutingArtifact, Is.False); Assert.That(artifacts.CurrentGauge, Is.EqualTo(gaugeBeforeSwitch)); Assert.That(useCount, Is.Zero); yield return null; } [UnityTest] public IEnumerator SWhileHoldingA_CancelsChargeAndReleaseWaitsForFreshPress() { testPlayer = new GameObject("Held A Artifact Switch Test Player"); testPlayer.AddComponent(); artifacts = testPlayer.AddComponent(); firstArtifact = CreateArtifact( "held-a-switch-pulse", ActiveArtifactEffect.Pulse); secondArtifact = CreateArtifact( "held-a-switch-cyclone", ActiveArtifactEffect.Cyclone); Assert.That(artifacts.TryAddArtifact(firstArtifact), Is.True); Assert.That(artifacts.TryAddArtifact(secondArtifact), Is.True); artifacts.AddMovementCharge(50f); float gaugeBeforeSwitch = artifacts.CurrentGauge; int chargeStartCount = 0; int useCount = 0; artifacts.OnArtifactChargeStarted += _ => chargeStartCount++; artifacts.OnArtifactUseSucceeded += (_, _) => useCount++; Keyboard keyboard = BeginVirtualKeyboardInput(); InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.A)); yield return null; Assert.That(artifacts.IsCharging, Is.True); Assert.That(chargeStartCount, Is.EqualTo(1)); yield return new WaitForSeconds(firstArtifact.ChargeDuration + 0.05f); Assert.That(artifacts.IsFullyCharged, Is.True); InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.A, Key.S)); yield return null; Assert.That(keyboard.aKey.isPressed, Is.True); Assert.That(keyboard.sKey.isPressed, Is.True); Assert.That(artifacts.CurrentArtifact, Is.SameAs(secondArtifact)); Assert.That(artifacts.IsCharging, Is.False, "S must cancel a fully charged artifact while A remains held."); Assert.That(artifacts.CurrentGauge, Is.EqualTo(gaugeBeforeSwitch)); Assert.That(useCount, Is.Zero); InputSystem.QueueStateEvent(keyboard, new KeyboardState()); yield return null; Assert.That(artifacts.IsCharging, Is.False); Assert.That(artifacts.IsExecutingArtifact, Is.False); Assert.That(artifacts.CurrentGauge, Is.EqualTo(gaugeBeforeSwitch)); Assert.That(useCount, Is.Zero, "Releasing the A held through the switch must not activate either artifact."); InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.A)); yield return null; Assert.That(artifacts.IsCharging, Is.True, "A fresh press after switching may begin a new charge."); Assert.That(chargeStartCount, Is.EqualTo(2)); Assert.That(artifacts.CurrentArtifact, Is.SameAs(secondArtifact)); InvokePrivate(artifacts, "CancelCharge"); InputSystem.QueueStateEvent(keyboard, new KeyboardState()); yield return null; Assert.That(useCount, Is.Zero); Assert.That(artifacts.CurrentGauge, Is.EqualTo(gaugeBeforeSwitch)); } private static ActiveArtifactDefinition CreateArtifact( string id, ActiveArtifactEffect effect) { ActiveArtifactDefinition definition = ScriptableObject.CreateInstance(); definition.Configure( id, id, "", effect, DamageTag.Collision, Color.white, Color.white, 25f, 60f, 0.8f, 15f, 30f, 1.25f, 2f, 1.5f, 3f); return definition; } private Keyboard BeginVirtualKeyboardInput() { previousBackgroundBehavior = InputSystem.settings.backgroundBehavior; previousEditorInputBehavior = InputSystem.settings.editorInputBehaviorInPlayMode; inputSettingsCaptured = true; InputSystem.settings.backgroundBehavior = InputSettings.BackgroundBehavior.IgnoreFocus; InputSystem.settings.editorInputBehaviorInPlayMode = InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView; virtualKeyboard = InputSystem.AddDevice(); virtualKeyboard.MakeCurrent(); virtualKeyboardInputActive = true; return virtualKeyboard; } private static void InvokePrivate( ActiveArtifactController target, string methodName) { MethodInfo method = typeof(ActiveArtifactController).GetMethod( methodName, BindingFlags.Instance | BindingFlags.NonPublic); Assert.That(method, Is.Not.Null, $"Could not find {methodName}."); method.Invoke(target, null); } private static void SetRunManagerInstanceForTest(RunManager instance) { FieldInfo instanceField = typeof(RunManager).GetField( "k__BackingField", BindingFlags.Static | BindingFlags.NonPublic); Assert.That(instanceField, Is.Not.Null); instanceField.SetValue(null, instance); } } }