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 PressureStreakPlayModeTests { private const BindingFlags NonPublicInstance = BindingFlags.Instance | BindingFlags.NonPublic; private readonly List testTargets = new(); private bool previousGrantCatalogForTests; private bool previousProductionModeForTests; private float previousTimeScale; private PlayerStats playerStats; private RunManager runManager; private SpawnDirector spawnDirector; private EnemyController ordinarySceneTarget; [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() { foreach (GameObject target in testTargets) { if (target != null) { Object.Destroy(target); } } testTargets.Clear(); ArtifactRewardController.GrantCatalogForTests = previousGrantCatalogForTests; RunManager.ForceProductionModeForTests = previousProductionModeForTests; Time.timeScale = previousTimeScale; } [UnityTest] public IEnumerator TwoKillsDoNotApply_ThirdKillAppliesTwentyPercent() { yield return LoadCombatPrototype(); SetRunTime(100f); EnemyController first = CreateDeadEnemy(); yield return null; RaiseOrdinaryHit(first); Assert.That(playerStats.PressureStreakRemaining, Is.GreaterThan(0f)); Assert.That(playerStats.IsPressureStreakActive, Is.False); SetRunTime(101f); EnemyController second = CreateDeadEnemy(); RaiseOrdinaryHit(second); Assert.That(playerStats.PressureStreakKillCount, Is.EqualTo(2)); Assert.That(playerStats.PressureStreakRemaining, Is.GreaterThan(0f)); Assert.That(playerStats.IsPressureStreakActive, Is.False); EnemyController third = CreateDeadEnemy(); yield return null; RaiseOrdinaryHit(third); Assert.That(playerStats.PressureStreakKillCount, Is.EqualTo(3)); Assert.That(playerStats.IsPressureStreakActive, Is.True); Assert.That(playerStats.MoveSpeed, Is.EqualTo(3.6f).Within(0.001f)); } [UnityTest] public IEnumerator ConsecutiveWindow_UsesKillToKillGap_AndFourthRefreshesWithoutStacking() { yield return LoadCombatPrototype(); SetRunTime(200f); RaiseOrdinaryHit(CreateDeadEnemy()); SetRunTime(202f); RaiseOrdinaryHit(CreateDeadEnemy()); SetRunTime(204f); RaiseOrdinaryHit(CreateDeadEnemy()); Assert.That(playerStats.IsPressureStreakActive, Is.True); SetRunTime(205f); playerStats.RefreshPressureStreak(); float beforeRefresh = playerStats.PressureStreakRemaining; RaiseOrdinaryHit(CreateDeadEnemy()); Assert.That(playerStats.PressureStreakKillCount, Is.EqualTo(4)); Assert.That( playerStats.GetStackCount("player.pressure-streak", CharacterStat.MoveSpeed), Is.EqualTo(1)); Assert.That(playerStats.MoveSpeed, Is.EqualTo(3.6f).Within(0.001f)); Assert.That(playerStats.PressureStreakRemaining, Is.GreaterThan(beforeRefresh)); SetRunTime(208.01f); playerStats.RefreshPressureStreak(); Assert.That(playerStats.PressureStreakKillCount, Is.EqualTo(0)); Assert.That(playerStats.IsPressureStreakActive, Is.False); SetRunTime(220f); RaiseOrdinaryHit(CreateDeadEnemy()); SetRunTime(223f); RaiseOrdinaryHit(CreateDeadEnemy()); SetRunTime(226f); RaiseOrdinaryHit(CreateDeadEnemy()); Assert.That(playerStats.PressureStreakKillCount, Is.EqualTo(3)); Assert.That(playerStats.IsPressureStreakActive, Is.True); SetRunTime(230.01f); playerStats.RefreshPressureStreak(); SetRunTime(240f); RaiseOrdinaryHit(CreateDeadEnemy()); SetRunTime(243.01f); RaiseOrdinaryHit(CreateDeadEnemy()); SetRunTime(246.02f); RaiseOrdinaryHit(CreateDeadEnemy()); Assert.That(playerStats.PressureStreakKillCount, Is.EqualTo(1)); Assert.That(playerStats.IsPressureStreakActive, Is.False); } [UnityTest] public IEnumerator ArtifactDashNonlethalAndRetiredEvents_DoNotCount() { yield return LoadCombatPrototype(); EnemyController artifact = CreateDeadEnemy(); EnemyController dash = CreateDeadEnemy(); EnemyController nonlethal = CreateLivingEnemy(); EnemyController retired = CreateDeadEnemy(); yield return null; CombatEvents.RaiseValidHit(CreateHit( artifact.gameObject, isArtifactHit: true, sourceId: "artifact.pulse")); CombatEvents.RaiseValidHit(CreateHit( dash.gameObject, isDash: true)); RaiseOrdinaryHit(nonlethal); CombatEvents.RaiseEnemyRetired(retired.gameObject); Assert.That(playerStats.PressureStreakKillCount, Is.Zero); Assert.That(playerStats.IsPressureStreakActive, Is.False); } [UnityTest] public IEnumerator SameEnemyLifetime_IsCountedOnce_AndOtherModifiersRemainOwned() { yield return LoadCombatPrototype(); playerStats.AddModifier(new StatModifier( "level-up.move-speed", CharacterStat.MoveSpeed, ModifierOperation.Increased, 0.1f)); SetRunTime(300f); EnemyController target = CreateDeadEnemy(); yield return null; RaiseOrdinaryHit(target); RaiseOrdinaryHit(target); Assert.That(playerStats.PressureStreakKillCount, Is.EqualTo(1)); RaiseOrdinaryHit(CreateDeadEnemy()); RaiseOrdinaryHit(CreateDeadEnemy()); Assert.That(playerStats.MoveSpeed, Is.EqualTo(3.9f).Within(0.001f)); SetRunTime(304.01f); playerStats.RefreshPressureStreak(); Assert.That(playerStats.GetStackCount( "level-up.move-speed", CharacterStat.MoveSpeed), Is.EqualTo(1)); Assert.That(playerStats.GetStackCount( "player.pressure-streak", CharacterStat.MoveSpeed), Is.Zero); Assert.That(playerStats.MoveSpeed, Is.EqualTo(3.3f).Within(0.001f)); } [UnityTest] public IEnumerator PauseAndDisable_DoNotConsumeOrRetainPressureStreak() { yield return LoadCombatPrototype(); SetRunTime(400f); RaiseOrdinaryHit(CreateDeadEnemy()); RaiseOrdinaryHit(CreateDeadEnemy()); RaiseOrdinaryHit(CreateDeadEnemy()); float remainingBeforePause = playerStats.PressureStreakRemaining; runManager.SetSelectionOpen(true); yield return new WaitForSecondsRealtime(0.2f); playerStats.RefreshPressureStreak(); Assert.That( playerStats.PressureStreakRemaining, Is.EqualTo(remainingBeforePause).Within(0.001f)); runManager.SetSelectionOpen(false); playerStats.enabled = false; Assert.That(playerStats.GetStackCount( "player.pressure-streak", CharacterStat.MoveSpeed), Is.Zero); playerStats.enabled = true; Assert.That(playerStats.PressureStreakKillCount, Is.Zero); Assert.That(playerStats.IsPressureStreakActive, Is.False); } [UnityTest] public IEnumerator LethalResolverBump_RaisesOrdinaryDeathHitForTheStreak() { yield return LoadCombatPrototype(); if (ordinarySceneTarget == null) { Assert.That(spawnDirector, Is.Not.Null); spawnDirector.DebugSpawnImmediate(1); yield return null; ordinarySceneTarget = FindOrdinarySceneTarget(); } Assert.That(ordinarySceneTarget, Is.Not.Null); ordinarySceneTarget.gameObject.SetActive(true); yield return null; SetEnemyState(ordinarySceneTarget, EnemyState.Chase); FieldInfo healthBackingField = typeof(EnemyController).GetField( "k__BackingField", NonPublicInstance); Assert.That(healthBackingField, Is.Not.Null); healthBackingField.SetValue(ordinarySceneTarget, 1f); BumpCombatResolver resolver = playerStats.GetComponent(); Assert.That(resolver, Is.Not.Null); MethodInfo resolveEnemy = typeof(BumpCombatResolver).GetMethod( "ResolveEnemy", NonPublicInstance); Assert.That(resolveEnemy, Is.Not.Null); object result = resolveEnemy.Invoke( resolver, new object[] { ordinarySceneTarget, false, (Vector2)ordinarySceneTarget.transform.position, (Vector2)playerStats.transform.position, Vector2.right, HitSide.Front, false, 100f, null, null, DamageTag.Collision, null, 0, null, }); Assert.That(result, Is.EqualTo(true)); Assert.That(ordinarySceneTarget.IsDead, Is.True); Assert.That(playerStats.PressureStreakKillCount, Is.EqualTo(1)); } private IEnumerator LoadCombatPrototype() { AsyncOperation load = SceneManager.LoadSceneAsync("CombatPrototype"); while (!load.isDone) { yield return null; } yield return null; yield return null; yield return null; runManager = RunManager.Instance; playerStats = Object.FindAnyObjectByType(); Assert.That(runManager, Is.Not.Null); Assert.That(playerStats, Is.Not.Null); spawnDirector = Object.FindAnyObjectByType(); if (spawnDirector != null) { spawnDirector.enabled = false; } foreach (EnemyController enemy in Object.FindObjectsByType( FindObjectsSortMode.None)) { if (ordinarySceneTarget == null && !enemy.IsEventEnemy && !enemy.IsDead) { ordinarySceneTarget = enemy; } enemy.gameObject.SetActive(false); } SetRunTime(100f); playerStats.RefreshPressureStreak(); } private EnemyController CreateDeadEnemy() { GameObject target = new( "Pressure Streak Dead Target", typeof(BoxCollider2D)); testTargets.Add(target); EnemyController enemy = target.AddComponent(); SetEnemyState(enemy, EnemyState.Dead); return enemy; } private EnemyController CreateLivingEnemy() { GameObject target = new( "Pressure Streak Living Target", typeof(BoxCollider2D)); testTargets.Add(target); EnemyController enemy = target.AddComponent(); SetEnemyState(enemy, EnemyState.Chase); return enemy; } private void RaiseOrdinaryHit(EnemyController enemy) { CombatEvents.RaiseValidHit(CreateHit(enemy.gameObject)); } private CombatHitResult CreateHit( GameObject target, bool isDash = false, bool isArtifactHit = false, string sourceId = null) { return new CombatHitResult( playerStats.gameObject, target, isDash, HitSide.Front, 1f, Vector2.right, 0f, target.transform.position, sourceId: sourceId, isArtifactHit: isArtifactHit); } private void SetRunTime(float value) { FieldInfo elapsedTime = typeof(RunManager).GetField( "k__BackingField", NonPublicInstance); Assert.That(elapsedTime, Is.Not.Null); elapsedTime.SetValue(runManager, value); } private static void SetEnemyState(EnemyController enemy, EnemyState state) { FieldInfo stateBackingField = typeof(EnemyController).GetField( "k__BackingField", NonPublicInstance); Assert.That(stateBackingField, Is.Not.Null); stateBackingField.SetValue(enemy, state); } private static EnemyController FindOrdinarySceneTarget() { foreach (EnemyController enemy in Object.FindObjectsByType( FindObjectsSortMode.None)) { if (enemy != null && !enemy.IsDead && !enemy.IsEventEnemy && !enemy.IsSummoned) { return enemy; } } return null; } } }