Files
tiny_tackle_heroes/Assets/_Project/Tests/PlayMode/Defense25DRelicReplayPlayModeTests.cs
T
2026-09-27 21:08:05 +09:00

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;
}
}
}
}