using System.Collections; using BumpCombat.Constants; using BumpCombat.Defense; using BumpCombat.Defense.Comparison; using NUnit.Framework; using UnityEditor; using UnityEngine; using UnityEngine.TestTools; namespace BumpCombat.Tests { public sealed class Defense25DSummonPackPlayModeTests { private Defense25DComparison world; private Defense25DComparisonSettings settings; [UnitySetUp] public IEnumerator SetUp() { settings = Object.Instantiate(AssetDatabase.LoadAssetAtPath( "Assets/_Project/Constants/Defense25DComparison/ComparisonSettings.asset")); world = new GameObject("2.5D summon pack test").AddComponent(); world.Initialize(settings); world.AutoSpawn = false; world.ReadInput = false; yield return null; } [UnityTearDown] public IEnumerator TearDown() { if (world != null) Object.Destroy(world.gameObject); if (settings != null) Object.Destroy(settings); yield return null; } [Test] public void UpgradeReplacesEverySlotAndResetsThePackTimer() { Defense25DTower tower = BuildSummonTower(); Defense25DSummon oldUnit = world.Summons[0]; oldUnit.Actor.TakeDamage(1000f, Vector2.zero); Assert.That(tower.SummonRespawnRemaining, Is.EqualTo(GameplayConstants.Current.Defense.SummonUnitRespawnSeconds)); UpgradeToTierFour(tower, DefenseTowerBranch.Second); int expected = GameplayConstants.Current.Defense.SummonUnitCount + 4; Assert.That(tower.TargetSummonCount, Is.EqualTo(expected)); Assert.That(tower.SummonCount, Is.EqualTo(expected)); Assert.That(tower.SummonRespawnRemaining, Is.Zero); Assert.That(world.Summons.Count, Is.EqualTo(expected)); CollectionAssert.DoesNotContain(world.Summons, oldUnit); CollectionAssert.AreEquivalent(System.Linq.Enumerable.Range(0, expected), System.Linq.Enumerable.Select(world.Summons, summon => summon.SlotIndex)); } [UnityTest] public IEnumerator BasicSlimeDealsNoInstantDamageAndRequiresAttackFrameContact() { Defense25DTower tower = BuildSummonTower(); Defense25DSummon summon = world.Summons[0]; foreach (Defense25DSummon other in world.Summons) if (other != summon) other.gameObject.SetActive(false); world.Player.ResetAt(new Vector2(4f, 4f)); Defense25DEnemy target = SpawnSummonVulnerableEnemy(); target.Actor.ResetAt(summon.Actor.Position + Vector2.right * .9f); Physics.SyncTransforms(); tower.StepSummonPack(0f); float health = target.Actor.CurrentHealth; float elapsed = 0f; while (elapsed < .124f) { yield return new WaitForFixedUpdate(); elapsed += Time.fixedDeltaTime; } Assert.That(target.Actor.CurrentHealth, Is.EqualTo(health), "the body-slam hit is gated by the authored impact frame"); float deadline = Time.realtimeSinceStartup + 2f; while (target.Actor.CurrentHealth >= health && Time.realtimeSinceStartup < deadline) yield return new WaitForFixedUpdate(); Assert.That(target.Actor.CurrentHealth, Is.LessThan(health), "the later attack frame must make physical contact before applying damage"); Assert.That(summon.AttackEvents, Is.EqualTo(1), "the isolated summon must own the only successful damage event"); Assert.That(summon.LastDamageFrame, Is.GreaterThanOrEqualTo( GameplayConstants.Current.Defense.SummonBodySlamImpactFrame), "damage must occur on or after the authored impact frame"); Assert.That(summon.Actor.GapTo(target.Actor), Is.LessThanOrEqualTo( GameplayConstants.Current.Defense.SummonBodySlamContactMargin), "damage requires physical body contact"); yield return null; } [Test] public void SummonStartsAttackUsingPositionReachedByThisStep() { Defense25DTower tower = BuildSummonTower(); Defense25DSummon summon = world.Summons[0]; foreach (Defense25DSummon other in world.Summons) if (other != summon) other.gameObject.SetActive(false); world.Player.ResetAt(new Vector2(4f, 4f)); Defense25DEnemy target = SpawnSummonVulnerableEnemy(); target.Actor.ResetAt(summon.Actor.Position + Vector2.right * 1.25f); Physics.SyncTransforms(); tower.StepSummonPack(.2f); summon.Step(.2f); Assert.That(summon.IsAttacking, Is.True, $"The original starts an attack when the post-move root position enters attack range. " + $"target={tower.CommandTarget}, running={world.IsRunning}, operational={world.TowersOperational}, " + $"available={target.IsAvailable}, summon={summon.Actor.Position}, " + $"targetPosition={target.Actor.Position}, blocked={summon.Actor.MovementBlocked}, " + $"lastMove={summon.Actor.LastMove}"); } [UnityTest] public IEnumerator SummonWithNoValidTargetReturnsToItsAnchor() { Defense25DTower tower = BuildSummonTower(); Defense25DSummon summon = world.Summons[0]; foreach (Defense25DSummon other in world.Summons) if (other != summon) other.gameObject.SetActive(false); world.Player.ResetAt(new Vector2(4f, 4f)); Vector2 anchor = summon.Actor.Position; summon.Actor.ResetAt(anchor + Vector2.right * .5f); Physics.SyncTransforms(); Vector2 before = summon.Actor.Position; tower.StepSummonPack(.2f); summon.Step(.2f); Assert.That(tower.CommandTarget, Is.Null); Assert.That(Vector2.Dot(summon.Actor.LastMove, anchor - before), Is.GreaterThan(0f), "The original moves toward its anchor while no command target is valid."); yield return null; } [UnityTest] public IEnumerator OnlyBasicSlimeReturnsToItsAnchorAfterAttack() { Defense25DTower tower = BuildSummonTower(); Defense25DSummon summon = world.Summons[0]; foreach (Defense25DSummon other in world.Summons) if (other != summon) other.gameObject.SetActive(false); world.Player.ResetAt(new Vector2(4f, 4f)); Defense25DEnemy target = SpawnSummonVulnerableEnemy(); target.Actor.ResetAt(summon.Actor.Position + Vector2.right * .7f); Physics.SyncTransforms(); tower.StepSummonPack(.01f); summon.Step(.01f); Assert.That(summon.IsAttacking, Is.True, $"Basic summon should start when its target is in range; target={tower.CommandTarget}, " + $"running={world.IsRunning}, operational={world.TowersOperational}, " + $"available={target.IsAvailable}, summon={summon.Actor.Position}, targetPosition={target.Actor.Position}"); float deadline = Time.realtimeSinceStartup + 2f; while (summon.IsAttacking && Time.realtimeSinceStartup < deadline) yield return new WaitForFixedUpdate(); Assert.That(summon.IsAttacking, Is.False); Assert.That(summon.IsReturningToAnchor, Is.True, "Basic Slime returns after its attack sequence."); yield return null; } [UnityTest] public IEnumerator TierFourSentinelDoesNotReturnAfterAttack() { Defense25DTower tower = BuildSummonTower(); UpgradeToTierFour(tower, DefenseTowerBranch.First); Defense25DSummon summon = world.Summons[0]; foreach (Defense25DSummon other in world.Summons) if (other != summon) other.gameObject.SetActive(false); world.Player.ResetAt(new Vector2(4f, 4f)); Defense25DEnemy target = SpawnSummonVulnerableEnemy(); target.Actor.ResetAt(summon.Actor.Position + Vector2.right * .7f); Physics.SyncTransforms(); tower.StepSummonPack(.01f); summon.Step(.01f); Assert.That(summon.IsAttacking, Is.True, $"Sentinel should start its attack in range; target={tower.CommandTarget}, " + $"running={world.IsRunning}, operational={world.TowersOperational}, " + $"available={target.IsAvailable}, summon={summon.Actor.Position}, targetPosition={target.Actor.Position}"); float deadline = Time.realtimeSinceStartup + 2f; while (summon.IsAttacking && Time.realtimeSinceStartup < deadline) yield return new WaitForFixedUpdate(); Assert.That(summon.IsAttacking, Is.False); Assert.That(summon.IsReturningToAnchor, Is.False, "Original return-to-anchor behavior is limited to basic Slime attacks."); yield return null; } [UnityTest] public IEnumerator ScoutGroundSpikeWaitsForItsOriginalLaunchAndTravelTime() { Defense25DTower tower = BuildSummonTower(); UpgradeToTierFour(tower, DefenseTowerBranch.Second); Defense25DSummon scout = world.Summons[0]; Defense25DEnemy target = SpawnSummonVulnerableEnemy(); target.Actor.ResetAt(tower.Position + Vector2.right); Physics.SyncTransforms(); float health = target.Actor.CurrentHealth; Assert.That(tower.TryAttackSummon(scout, target), Is.True); tower.StepParityAttacks(.48f); Assert.That(target.Actor.CurrentHealth, Is.EqualTo(health), "launch frame is not impact"); tower.StepParityAttacks(.159f); Assert.That(target.Actor.CurrentHealth, Is.EqualTo(health), "the projectile remains in flight"); tower.StepParityAttacks(.001f); Assert.That(target.Actor.CurrentHealth, Is.LessThan(health)); yield return null; } [UnityTest] public IEnumerator SummonRespawnTimerPausesWhilePlayerIsDead() { Defense25DTower tower = BuildSummonTower(); Defense25DSummon summon = world.Summons[0]; summon.Actor.TakeDamage(1000f, Vector2.zero); float respawnSeconds = GameplayConstants.Current.Defense.SummonUnitRespawnSeconds; Assert.That(tower.SummonRespawnRemaining, Is.EqualTo(respawnSeconds)); Assert.That(world.Player.TakeDamage(world.Player.CurrentHealth + 1f, Vector2.left), Is.True); Assert.That(world.Player.IsAlive, Is.False, "the timer pause requires an actually dead player"); Assert.That(world.TowersOperational, Is.False); tower.StepSummonPack(1f); Assert.That(tower.SummonRespawnRemaining, Is.EqualTo(respawnSeconds)); Assert.That(tower.SummonCount, Is.EqualTo(GameplayConstants.Current.Defense.SummonUnitCount - 1)); world.Player.ResetAt(GameplayConstants.Current.Defense.GetBuildPadPosition(0)); tower.StepSummonPack(respawnSeconds - .01f); Assert.That(tower.SummonCount, Is.EqualTo(GameplayConstants.Current.Defense.SummonUnitCount - 1)); tower.StepSummonPack(.02f); Assert.That(tower.SummonCount, Is.EqualTo(GameplayConstants.Current.Defense.SummonUnitCount)); yield return null; } private Defense25DTower BuildSummonTower() { world.Player.ResetAt(GameplayConstants.Current.Defense.GetBuildPadPosition(0)); Assert.That(world.TowerManager.TryBuildTowerAtPad(0, DefenseTowerKind.Summon), Is.True); return world.TowerManager.GetTowerAtPad(0); } private void UpgradeToTierFour(Defense25DTower tower, DefenseTowerBranch branch) { world.TowerManager.AddCurrency(1000); Assert.That(world.TowerManager.TryUpgradeTowerAtPad(tower.PadIndex, DefenseTowerBranch.None), Is.True); Assert.That(world.TowerManager.TryUpgradeTowerAtPad(tower.PadIndex, DefenseTowerBranch.None), Is.True); Assert.That(world.TowerManager.TryUpgradeTowerAtPad(tower.PadIndex, branch), Is.True); } private Defense25DEnemy SpawnSummonVulnerableEnemy() { for (int i = 0; i < 2; i++) { Defense25DEnemy candidate = world.SpawnNextEnemy(); if (!world.IsTowerImmuneToEnemy(DefenseTowerKind.Summon, candidate)) return candidate; } Assert.Fail("Both portal lanes unexpectedly reject summon-tower damage."); return null; } } }