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

404 lines
20 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BumpCombat.Defense;
using BumpCombat.Defense.Comparison;
using BumpCombat.Constants;
using BumpCombat.Player;
using NUnit.Framework;
using UnityEngine;
using UnityEditor;
using UnityEngine.TestTools;
namespace BumpCombat.Tests
{
public sealed class Defense25DTowerPlayModeTests
{
private Defense25DComparison world;
private Defense25DComparisonSettings settings;
[UnitySetUp]
public IEnumerator SetUp()
{
settings = UnityEngine.Object.Instantiate(AssetDatabase.LoadAssetAtPath<Defense25DComparisonSettings>(
"Assets/_Project/Constants/Defense25DComparison/ComparisonSettings.asset"));
world = new GameObject("2.5D tower test").AddComponent<Defense25DComparison>();
world.Initialize(settings);
world.AutoSpawn = false;
world.ReadInput = false;
yield return null;
}
[UnityTearDown]
public IEnumerator TearDown()
{
if (world != null) UnityEngine.Object.Destroy(world.gameObject);
if (settings != null) UnityEngine.Object.Destroy(settings);
yield return null;
}
[Test]
public void EachTowerFamilyCanBeBuiltOnAnEmptyPadWithItsConfiguredCost()
{
Type managerType = typeof(Defense25DComparison).Assembly.GetType(
"BumpCombat.Defense.Comparison.Defense25DTowerManager");
Assert.That(managerType, Is.Not.Null,
"The 2.5D run needs a tower manager that owns pads, construction currency, and tower instances.");
Component manager = world.GetComponent(managerType);
Assert.That(manager, Is.Not.Null);
MethodInfo build = managerType.GetMethod("TryBuildTowerAtPad",
new[] { typeof(int), typeof(DefenseTowerKind) });
MethodInfo getTower = managerType.GetMethod("GetTowerAtPad", new[] { typeof(int) });
Assert.That(build, Is.Not.Null);
Assert.That(getTower, Is.Not.Null);
foreach (DefenseTowerKind kind in new[]
{
DefenseTowerKind.Direct,
DefenseTowerKind.Summon,
DefenseTowerKind.Lightning,
DefenseTowerKind.Cold,
DefenseTowerKind.Seismic,
DefenseTowerKind.Empowering,
})
{
world.ResetRun();
world.Player.ResetAt(GameplayConstants.Current.Defense.GetBuildPadPosition(0));
Physics.SyncTransforms();
Assert.That(build.Invoke(manager, new object[] { 0, kind }), Is.EqualTo(true), kind.ToString());
object tower = getTower.Invoke(manager, new object[] { 0 });
Assert.That(tower, Is.Not.Null, kind.ToString());
Type towerType = tower.GetType();
Assert.That(towerType.GetProperty("Kind").GetValue(tower), Is.EqualTo(kind));
Assert.That(towerType.GetProperty("Tier").GetValue(tower), Is.EqualTo(1));
Assert.That(towerType.GetProperty("Branch").GetValue(tower),
Is.EqualTo(DefenseTowerBranch.None));
Assert.That(managerType.GetProperty("ConstructionCurrency").GetValue(manager),
Is.EqualTo(GameplayConstants.Current.Defense.InitialConstructionCurrency - BuildCost(kind)));
}
}
[Test]
public void TowerEfficiencyIsZeroUntilARealTowerIsBuilt()
{
world.Player.ResetAt(world.Settings.TowerPosition);
Assert.That(world.TowerEfficiency, Is.EqualTo(0f));
world.Player.ResetAt(GameplayConstants.Current.Defense.GetBuildPadPosition(0));
Assert.That(world.TowerManager.TryBuildTowerAtPad(0, DefenseTowerKind.Direct), Is.True);
Assert.That(world.TowerEfficiency, Is.EqualTo(1f));
}
[Test]
public void ImbuingIntervalUsesFirstConstructedSupportEvenWhenPadOrderIsReversed()
{
Defense25DTowerManager manager = world.TowerManager;
manager.AddCurrency(10000);
Defense25DTower firstBuilt = BuildTower(4, DefenseTowerKind.Empowering);
UpgradeToTierFourImbuing(manager, 4);
Defense25DTower secondBuilt = BuildTower(0, DefenseTowerKind.Empowering);
UpgradeToTierFourImbuing(manager, 0);
Defense25DTower receiver = BuildTower(2, DefenseTowerKind.Direct);
SetTowerPosition(firstBuilt, Vector2.zero);
SetTowerPosition(secondBuilt, new Vector2(6.5f, 0f));
SetTowerPosition(receiver, new Vector2(3.25f, 0f));
world.Player.ResetAt(Vector2.zero);
manager.Step(0f);
Assert.That(manager.TowersInConstructionOrder[0], Is.SameAs(firstBuilt));
Assert.That(manager.TowersInConstructionOrder[1], Is.SameAs(secondBuilt));
Assert.That(receiver.AttackIntervalMultiplier,
Is.EqualTo(GameplayConstants.Current.Defense.ImbuingAttackIntervalMultiplier).Within(.001f),
"The first built in-range Imbuing tower wins before the later, out-of-sight source.");
}
[Test]
public void NearestTowerBuildUsesTheClosestEmptyPadWithinInteractionRange()
{
Vector2 pad = GameplayConstants.Current.Defense.GetBuildPadPosition(0);
world.Player.ResetAt(pad);
MethodInfo buildNearest = typeof(Defense25DComparison).GetMethod("TryBuildNearestTower",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(buildNearest, Is.Not.Null);
Assert.That(buildNearest.Invoke(world, new object[] { DefenseTowerKind.Direct }), Is.EqualTo(true));
Assert.That(world.TowerManager.GetTowerAtPad(0), Is.Not.Null);
}
[Test]
public void EveryTowerFamilyCanReachBothTierFourBranches()
{
Type managerType = typeof(Defense25DComparison).Assembly.GetType(
"BumpCombat.Defense.Comparison.Defense25DTowerManager");
Component manager = world.GetComponent(managerType);
MethodInfo build = managerType.GetMethod("TryBuildTowerAtPad",
new[] { typeof(int), typeof(DefenseTowerKind) });
MethodInfo upgrade = managerType.GetMethod("TryUpgradeTowerAtPad",
new[] { typeof(int), typeof(DefenseTowerBranch) });
MethodInfo addCurrency = managerType.GetMethod("AddCurrency");
MethodInfo getTower = managerType.GetMethod("GetTowerAtPad");
foreach (DefenseTowerKind kind in System.Enum.GetValues(typeof(DefenseTowerKind)))
{
foreach (DefenseTowerBranch branch in new[] { DefenseTowerBranch.First, DefenseTowerBranch.Second })
{
world.ResetRun();
world.Player.ResetAt(GameplayConstants.Current.Defense.GetBuildPadPosition(0));
addCurrency.Invoke(manager, new object[] { 1000 });
Assert.That(build.Invoke(manager, new object[] { 0, kind }), Is.EqualTo(true), kind.ToString());
Assert.That(upgrade.Invoke(manager, new object[] { 0, DefenseTowerBranch.None }), Is.EqualTo(true));
Assert.That(upgrade.Invoke(manager, new object[] { 0, DefenseTowerBranch.None }), Is.EqualTo(true));
Assert.That(upgrade.Invoke(manager, new object[] { 0, branch }), Is.EqualTo(true));
object tower = getTower.Invoke(manager, new object[] { 0 });
Assert.That(tower.GetType().GetProperty("Tier").GetValue(tower), Is.EqualTo(4));
Assert.That(tower.GetType().GetProperty("Branch").GetValue(tower), Is.EqualTo(branch));
if (kind == DefenseTowerKind.Summon)
{
int expected = branch == DefenseTowerBranch.First ? 1
: GameplayConstants.Current.Defense.SummonUnitCount + 4;
Assert.That(world.Summons.Count, Is.EqualTo(expected), branch.ToString());
}
}
}
}
[Test]
public void DirectFireballDealsDamageOnlyAfterItsSourceFlightDuration()
{
world.Player.ResetAt(GameplayConstants.Current.Defense.GetBuildPadPosition(0));
Assert.That(world.TowerManager.TryBuildTowerAtPad(0, DefenseTowerKind.Direct), Is.True);
Defense25DTower tower = world.TowerManager.GetTowerAtPad(0);
Defense25DEnemy target = null;
for (int i = 0; i < 2; i++)
{
Defense25DEnemy candidate = world.SpawnNextEnemy();
if (!world.IsTowerImmuneToEnemy(DefenseTowerKind.Direct, candidate))
{
target = candidate;
break;
}
}
Assert.That(target, Is.Not.Null);
target.Actor.ResetAt(tower.Position + Vector2.right);
Physics.SyncTransforms();
float health = target.Actor.CurrentHealth;
tower.StepParityAttacks(0f);
Assert.That(target.Actor.CurrentHealth, Is.EqualTo(health), "launch must not apply damage");
tower.StepParityAttacks(.17f);
Assert.That(target.Actor.CurrentHealth, Is.EqualTo(health), "projectile is still in flight");
tower.StepParityAttacks(.01f);
Assert.That(target.Actor.CurrentHealth, Is.LessThan(health), "impact callback applies damage");
}
[Test]
public void ScoutAttackDamagesAfterOriginalLaunchAndFlightTiming()
{
Vector2 pad = GameplayConstants.Current.Defense.GetBuildPadPosition(0);
world.Player.ResetAt(pad);
Assert.That(world.TowerManager.TryBuildTowerAtPad(0, DefenseTowerKind.Summon), Is.True);
Defense25DTower tower = world.TowerManager.GetTowerAtPad(0);
world.TowerManager.AddCurrency(1000);
Assert.That(world.TowerManager.TryUpgradeTowerAtPad(0, DefenseTowerBranch.None), Is.True);
Assert.That(world.TowerManager.TryUpgradeTowerAtPad(0, DefenseTowerBranch.None), Is.True);
Assert.That(world.TowerManager.TryUpgradeTowerAtPad(0, DefenseTowerBranch.Second), Is.True);
Defense25DSummon scout = world.Summons[0];
Defense25DEnemy target = world.SpawnNextEnemy();
if (world.IsTowerImmuneToEnemy(DefenseTowerKind.Summon, target))
target = world.SpawnNextEnemy();
target.Actor.ResetAt(tower.Position + Vector2.right);
Physics.SyncTransforms();
float health = target.Actor.CurrentHealth;
Assert.That(tower.TryAttackSummon(scout, target), Is.True);
world.TowerManager.Step(.47f);
Assert.That(target.Actor.CurrentHealth, Is.EqualTo(health), "the scout's projectile has not launched");
world.TowerManager.Step(.01f);
Assert.That(target.Actor.CurrentHealth, Is.EqualTo(health), "the projectile has only just launched");
world.TowerManager.Step(.159f);
Assert.That(target.Actor.CurrentHealth, Is.EqualTo(health), "the projectile is still in flight");
world.TowerManager.Step(.001f);
Assert.That(target.Actor.CurrentHealth, Is.LessThan(health));
}
[UnityTest]
public IEnumerator OwnedSummonsKeepAttackingTheirTowerCommandTarget()
{
Vector2 pad = GameplayConstants.Current.Defense.GetBuildPadPosition(0);
world.Player.ResetAt(pad);
Assert.That(world.TowerManager.TryBuildTowerAtPad(0, DefenseTowerKind.Summon), Is.True);
Defense25DTower tower = world.TowerManager.GetTowerAtPad(0);
Defense25DSummon summon = world.Summons[0];
Defense25DEnemy target = world.SpawnNextEnemy();
if (world.IsTowerImmuneToEnemy(DefenseTowerKind.Summon, target))
target = world.SpawnNextEnemy();
target.Actor.ResetAt(summon.Actor.Position + Vector2.left * .9f);
target.ApplyStun(2f);
Physics.SyncTransforms();
float health = target.Actor.CurrentHealth;
tower.Step(0f);
summon.Step(0f);
Assert.That(summon.IsAttacking, Is.True);
for (int i = 0; i < 60 && target.Actor.CurrentHealth >= health; i++)
yield return new WaitForFixedUpdate();
Assert.That(target.Actor.CurrentHealth, Is.LessThan(health));
}
[UnityTest]
public IEnumerator BothPortalsSupplyTheirOwnRoutesAndRewardsGateVictory()
{
int addedArtifacts = 0;
foreach (ActiveArtifactDefinition artifact in world.Loadout.Catalog)
{
if (world.Loadout.TryAddArtifact(artifact)) addedArtifacts++;
if (addedArtifacts == 3) break;
}
Assert.That(addedArtifacts, Is.EqualTo(3), "The route/reward fixture needs three eligible owned artifacts.");
int expectedPerPortal = GameplayConstants.Current.Defense.PortalEnemyCount;
int totalExpected = expectedPerPortal * 2;
List<Defense25DEnemy> supplied = new();
for (int i = 0; i < totalExpected; i++)
{
Defense25DEnemy enemy = world.SpawnNextEnemy();
Assert.That(enemy, Is.Not.Null, $"supply {i + 1}/{totalExpected}");
supplied.Add(enemy);
}
Assert.That(world.SpawnNextEnemy(), Is.Null);
PropertyInfo laneProperty = typeof(Defense25DEnemy).GetProperty("Lane");
Assert.That(laneProperty, Is.Not.Null);
for (int i = 0; i < supplied.Count; i++)
{
int lane = (int)laneProperty.GetValue(supplied[i]);
Assert.That(lane, Is.EqualTo(i % 2), $"alternating portal supply #{i}");
Assert.That(Vector2.Distance(supplied[i].Actor.Position,
GameplayConstants.Current.Defense.GetPortalPosition(lane)), Is.LessThan(.001f));
}
foreach (Defense25DEnemy enemy in supplied)
enemy.Actor.TakeDamage(10000f, Vector2.zero);
yield return new WaitForSeconds(.1f);
Assert.That(world.IsRunning, Is.True, "both field rewards must be collected before victory");
MethodInfo collect = typeof(Defense25DComparison).GetMethod("TryCollectRewardAtLane");
Assert.That(collect, Is.Not.Null);
for (int lane = 0; lane < 2; lane++)
{
Vector2 pickup = GameplayConstants.Current.Defense.GetPortalPosition(lane) + Vector2.up * 1.2f;
world.Player.ResetAt(pickup);
Assert.That(collect.Invoke(world, new object[] { lane }), Is.EqualTo(true), $"reward lane {lane}");
Assert.That(world.IsRunning, Is.True, "The run must remain active while an earned relic choice is pending.");
Assert.That(world.HasPendingFieldRelicReward, Is.True, $"field relic choice lane {lane}");
Assert.That(world.Paused, Is.True, "The earned field choice must pause the run before the next pickup.");
Assert.That(world.ResolveFieldRelicChoice(world.PendingFieldRelicChoices[0]), Is.True,
$"resolve field relic choice lane {lane}");
}
Assert.That(world.IsRunning, Is.False);
Assert.That(typeof(Defense25DComparison).GetProperty("RunCleared").GetValue(world), Is.EqualTo(true));
}
[Test]
public void SummonVariantsUseAuditedCoreScaleForVisualColliderAndFoot()
{
world.TowerManager.AddCurrency(10000);
Defense25DTower slimeTower = BuildTower(1, DefenseTowerKind.Summon);
AssertSummonVariant(slimeTower, world.Settings.SummonVisualScale);
UpgradeToTierFourImbuing(world.TowerManager, 1);
AssertSummonVariant(slimeTower, 1.55f);
Defense25DTower scoutTower = BuildTower(2, DefenseTowerKind.Summon);
UpgradeToTierFourImbuing(world.TowerManager, 2, DefenseTowerBranch.Second);
AssertSummonVariant(scoutTower, 1.15f);
}
[UnityTest]
public IEnumerator TowerImmunityIsResolvedForEachPortalLane()
{
Defense25DEnemy upper = world.SpawnNextEnemy();
Defense25DEnemy lower = world.SpawnNextEnemy();
Assert.That(upper.Lane, Is.EqualTo(0));
Assert.That(lower.Lane, Is.EqualTo(1));
var tuning = GameplayConstants.Current.Defense;
Assert.That(world.IsTowerImmuneToEnemy(tuning.GetPortalImmuneTowerKind(0), upper), Is.True);
Assert.That(world.IsTowerImmuneToEnemy(tuning.GetPortalImmuneTowerKind(1), lower), Is.True);
Assert.That(world.IsTowerImmuneToEnemy(tuning.GetPortalImmuneTowerKind(0), lower), Is.False);
Assert.That(world.IsTowerImmuneToEnemy(tuning.GetPortalImmuneTowerKind(1), upper), Is.False);
yield return null;
}
private static int BuildCost(DefenseTowerKind kind)
{
var tuning = GameplayConstants.Current.Defense;
return kind switch
{
DefenseTowerKind.Direct => tuning.DirectTowerCost,
DefenseTowerKind.Summon => tuning.SummonTowerCost,
DefenseTowerKind.Lightning => tuning.LightningTowerCost,
DefenseTowerKind.Cold => tuning.ColdTowerCost,
DefenseTowerKind.Seismic => tuning.SeismicTowerCost,
DefenseTowerKind.Empowering => tuning.EmpoweringTowerCost,
_ => -1,
};
}
private Defense25DTower BuildTower(int pad, DefenseTowerKind kind)
{
world.Player.ResetAt(GameplayConstants.Current.Defense.GetBuildPadPosition(pad));
Physics.SyncTransforms();
Assert.That(world.TowerManager.TryBuildTowerAtPad(pad, kind), Is.True, $"pad={pad} kind={kind}");
return world.TowerManager.GetTowerAtPad(pad);
}
private static void UpgradeToTierFourImbuing(Defense25DTowerManager manager, int pad,
DefenseTowerBranch finalBranch = DefenseTowerBranch.First)
{
Assert.That(manager.TryUpgradeTowerAtPad(pad, DefenseTowerBranch.None), Is.True);
Assert.That(manager.TryUpgradeTowerAtPad(pad, DefenseTowerBranch.None), Is.True);
Assert.That(manager.TryUpgradeTowerAtPad(pad, finalBranch), Is.True);
}
private void SetTowerPosition(Defense25DTower tower, Vector2 position)
{
typeof(Defense25DTower).GetProperty(nameof(Defense25DTower.Position))
.SetValue(tower, position);
tower.transform.position = Defense25DActor.OnGround(position, world.Settings.GroundY);
}
private void AssertSummonVariant(Defense25DTower tower, float scale)
{
Defense25DSummon[] units = world.Summons.Where(unit => unit.Owner == tower).ToArray();
Assert.That(units.Length, Is.EqualTo(tower.TargetSummonCount));
float coreDiameter = scale > 1.4f
? Defense25DComparisonSettings.GiantSlimeBodyCoreDiameterPixels
: scale > world.Settings.SummonVisualScale
? Defense25DComparisonSettings.LavaSlimeBodyCoreDiameterPixels
: Defense25DComparisonSettings.SlimeBodyCoreDiameterPixels;
float radius = coreDiameter * scale / Defense25DComparisonSettings.SummonBodyPixelsPerUnit * .5f;
foreach (Defense25DSummon summon in units)
{
Defense25DSprite visual = summon.Actor.Visual;
Assert.That(visual.VisualScale, Is.EqualTo(scale).Within(.001f));
float expectedFootOffset = scale > 1.4f ? 0f
: world.Settings.SummonFootOffset * scale / world.Settings.SummonVisualScale;
Assert.That(visual.FootVisualOffset, Is.EqualTo(expectedFootOffset).Within(.001f));
Assert.That(summon.Actor.Radius, Is.EqualTo(radius).Within(.001f));
Assert.That(summon.Actor.BodyCollider.radius, Is.EqualTo(radius).Within(.001f));
Assert.That(summon.Actor.BodyCollider.height, Is.EqualTo(2f * radius).Within(.001f));
Assert.That(visual.transform.Find("Ground shadow"), Is.Null,
"The comparison actor keeps its existing authored sprite and has no added ground-shadow cylinder.");
}
}
}
}