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

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