404 lines
20 KiB
C#
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.");
|
|
}
|
|
}
|
|
}
|
|
}
|