271 lines
13 KiB
C#
271 lines
13 KiB
C#
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<Defense25DComparisonSettings>(
|
|
"Assets/_Project/Constants/Defense25DComparison/ComparisonSettings.asset"));
|
|
world = new GameObject("2.5D summon pack test").AddComponent<Defense25DComparison>();
|
|
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;
|
|
}
|
|
}
|
|
}
|