173 lines
8.2 KiB
C#
173 lines
8.2 KiB
C#
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<Defense25DComparisonSettings>(SettingsPath));
|
|
Assert.That(settings, Is.Not.Null);
|
|
world = new GameObject("Relic replay integration world").AddComponent<Defense25DComparison>();
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|