using System.Collections; using System.Reflection; using BumpCombat.Combat; using BumpCombat.Constants; using BumpCombat.Defense.Comparison; using BumpCombat.Enemies; using BumpCombat.Player; using BumpCombat.Progression; using NUnit.Framework; using UnityEditor; using UnityEngine; using UnityEngine.TestTools; using Object = UnityEngine.Object; namespace BumpCombat.Tests { public sealed class Defense25DRelicReplayPlayModeTests { private const string SettingsPath = "Assets/_Project/Constants/Defense25DComparison/ComparisonSettings.asset"; private GameplayConstants originalConstants; private GameplayConstants testConstants; private PassiveRelicConstants testRelicTuning; private Defense25DComparisonSettings settings; private Defense25DComparison world; private Defense25DEnemy enemy; private ActiveArtifactDefinition phoenix; private int ordinaryHits; private int replayHits; private int ordinaryHitFrame = -1; private int replayHitFrame = -1; private float previousTimeScale; [UnitySetUp] public IEnumerator SetUp() { previousTimeScale = Time.timeScale; Time.timeScale = 1f; ordinaryHits = replayHits = 0; ordinaryHitFrame = replayHitFrame = -1; originalConstants = GameplayConstants.Current; testConstants = Object.Instantiate(originalConstants); testRelicTuning = Object.Instantiate(originalConstants.PassiveRelics); testRelicTuning.ResonanceEchoChance = 1f; testRelicTuning.ResonantCrystalChance = 1f; testRelicTuning.ResonantCrystalDelaySeconds = .1f; typeof(GameplayConstants).GetField("passiveRelics", BindingFlags.Instance | BindingFlags.NonPublic) .SetValue(testConstants, testRelicTuning); GameplayConstants.SetCurrentForTests(testConstants); settings = Object.Instantiate(AssetDatabase.LoadAssetAtPath(SettingsPath)); Assert.That(settings, Is.Not.Null); world = new GameObject("Relic replay integration world").AddComponent(); world.Initialize(settings); world.AutoSpawn = false; world.ReadInput = false; world.Player.ResetAt(Vector2.zero); phoenix = world.Loadout.FindArtifact(ActiveArtifactEffect.Phoenix); Assert.That(world.Loadout.TryAddArtifact(phoenix), Is.True); testRelicTuning.ResonantCrystalDelaySeconds = .5f; enemy = world.Enemies[0]; enemy.Actor.SetEnemyMaximumHealth(1000f); enemy.Spawn(Vector2.right * .45f); Physics.SyncTransforms(); CombatEvents.OnValidHit += OnValidHit; yield return new WaitForFixedUpdate(); } [UnityTearDown] public IEnumerator TearDown() { CombatEvents.OnValidHit -= OnValidHit; if (world != null) Object.Destroy(world.gameObject); if (settings != null) Object.Destroy(settings); if (testRelicTuning != null) Object.Destroy(testRelicTuning); if (testConstants != null) Object.Destroy(testConstants); GameplayConstants.SetCurrentForTests(originalConstants); Time.timeScale = previousTimeScale; yield return null; } [UnityTest] public IEnumerator ResonanceCoreReplaysImmediatelyWithoutExtraGaugeOrRelicRecursion() { Assert.That(world.Loadout.TryAcquireRelic(PassiveRelicId.ResonanceCore), Is.True); world.Loadout.AddGauge(phoenix.NormalGaugeCost); Assert.That(world.Loadout.Gauge, Is.GreaterThanOrEqualTo(phoenix.NormalGaugeCost)); float initialHealth = enemy.Actor.CurrentHealth; float initialGauge = world.Loadout.Gauge; Assert.That(world.Loadout.TryUse(false), Is.True); Assert.That(ordinaryHits, Is.EqualTo(1)); Assert.That(replayHits, Is.EqualTo(1)); Assert.That(replayHitFrame, Is.EqualTo(ordinaryHitFrame), "Echo damage must be applied synchronously in the original hit's frame."); Assert.That(world.ArtifactExecutor.IsExecutingArtifact, Is.False); float expectedDamage = DamageCalculator.FinalizeDamage( world.Player.CalculateDamage(phoenix.NormalDamage, phoenix.DamageTag)) + DamageCalculator.FinalizeDamage(world.Player.CalculateDamage( phoenix.NormalDamage * testRelicTuning.ResonanceEchoDamageMultiplier, phoenix.DamageTag)); Assert.That(initialHealth - enemy.Actor.CurrentHealth, Is.EqualTo(expectedDamage).Within(.02f)); Assert.That(ordinaryHits, Is.EqualTo(1)); Assert.That(replayHits, Is.EqualTo(1)); Assert.That(world.Loadout.Gauge, Is.EqualTo(initialGauge - phoenix.NormalGaugeCost).Within(.001f)); yield return null; } [UnityTest] public IEnumerator ResonantCrystalReplaysImmediatelyWhenDelayExpiresWithoutExtraGaugeOrAnotherPendingRepeat() { Assert.That(world.Loadout.TryAcquireRelic(PassiveRelicId.ResonantCrystal), Is.True); world.Loadout.AddGauge(phoenix.NormalGaugeCost); Assert.That(world.Loadout.Gauge, Is.GreaterThanOrEqualTo(phoenix.NormalGaugeCost)); float initialHealth = enemy.Actor.CurrentHealth; float initialGauge = world.Loadout.Gauge; Assert.That(world.Loadout.TryUse(false), Is.True); Assert.That(world.RelicRuntime.IsRepeatPending, Is.True); Assert.That(ordinaryHits, Is.EqualTo(1)); Assert.That(replayHits, Is.Zero); Assert.That(world.Loadout.Gauge, Is.EqualTo(initialGauge - phoenix.NormalGaugeCost).Within(.001f)); float timeout = Time.realtimeSinceStartup + 2f; while (world.RelicRuntime.IsRepeatPending && Time.realtimeSinceStartup < timeout) { yield return new WaitForFixedUpdate(); Assert.That(world.Player.IsAlive, Is.True, "The player must remain alive throughout the repeat delay."); Assert.That(enemy.Actor.IsAlive, Is.True, "The target must survive the initial Phoenix hit and ignite ticks."); Assert.That(Vector2.Distance(world.Player.Position, enemy.Actor.Position), Is.LessThanOrEqualTo(phoenix.NormalRange), "The target must stay within the replay ray during its delay."); } float expectedDamage = DamageCalculator.FinalizeDamage( world.Player.CalculateDamage(phoenix.NormalDamage, phoenix.DamageTag)) + DamageCalculator.FinalizeDamage(world.Player.CalculateDamage( phoenix.NormalDamage * testRelicTuning.ResonantCrystalDamageFraction, phoenix.DamageTag)); Assert.That(world.RelicRuntime.IsRepeatPending, Is.False); Assert.That(replayHits, Is.EqualTo(1)); Assert.That(replayHitFrame, Is.GreaterThan(ordinaryHitFrame)); Assert.That(replayHitFrame, Is.EqualTo(Time.frameCount), "Repeat damage must land in the frame the relic delay expires, not in a later cast step."); Assert.That(world.ArtifactExecutor.IsExecutingArtifact, Is.False); Assert.That(initialHealth - enemy.Actor.CurrentHealth, Is.EqualTo(expectedDamage).Within(.02f)); Assert.That(ordinaryHits, Is.EqualTo(1)); Assert.That(replayHits, Is.EqualTo(1)); Assert.That(world.Loadout.Gauge, Is.EqualTo(initialGauge - phoenix.NormalGaugeCost).Within(.001f)); yield return null; } private void OnValidHit(CombatHitResult hit) { if (hit.Target != enemy?.gameObject || !hit.IsArtifactHit) return; if (hit.SourceId == Defense25DRelicRuntime.EchoSourceId) { replayHits++; replayHitFrame = Time.frameCount; } else if (hit.SourceId == phoenix?.ArtifactId) { ordinaryHits++; ordinaryHitFrame = Time.frameCount; } } } }