294 lines
14 KiB
C#
294 lines
14 KiB
C#
using System.Collections;
|
|
using System.Linq;
|
|
using BumpCombat.Constants;
|
|
using BumpCombat.Defense;
|
|
using BumpCombat.Defense.Comparison;
|
|
using BumpCombat.Enemies;
|
|
using NUnit.Framework;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using UnityEngine.TestTools;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace BumpCombat.PlayModeTests
|
|
{
|
|
public sealed class Defense25DTowerManagerPlayModeTests
|
|
{
|
|
private const string SettingsPath =
|
|
"Assets/_Project/Constants/Defense25DComparison/ComparisonSettings.asset";
|
|
private const string RunDefinitionPath =
|
|
"Assets/_Project/Constants/Defense25DComparison/LongDefenseRun.asset";
|
|
|
|
private Defense25DComparisonSettings settings;
|
|
private Defense25DComparison world;
|
|
private Defense25DFacilityVisuals facilityVisuals;
|
|
private EnemyDefinition sourceEnemy;
|
|
private float previousTimeScale;
|
|
|
|
[UnitySetUp]
|
|
public IEnumerator SetUp()
|
|
{
|
|
previousTimeScale = Time.timeScale;
|
|
Time.timeScale = 1f;
|
|
settings = Object.Instantiate(
|
|
AssetDatabase.LoadAssetAtPath<Defense25DComparisonSettings>(SettingsPath));
|
|
Assert.That(settings, Is.Not.Null);
|
|
sourceEnemy = AssetDatabase.LoadAssetAtPath<EnemyDefinition>(
|
|
"Assets/_Project/Constants/Enemies/Slime.asset");
|
|
settings.Enemy = Object.Instantiate(sourceEnemy);
|
|
settings.EnemyArt = ReadCharacter("Slime", "Slime", "Attack");
|
|
settings.EnemyRadius = .24f;
|
|
settings.EnemyColliderOffset = new Vector2(0f, -.28f);
|
|
settings.EnemyVisualScale = 1f;
|
|
settings.EnemyFootOffset = -.4f;
|
|
settings.EnemyBodyCenterCorrectionPixels = 0f;
|
|
|
|
Defense25DRunDefinition definition =
|
|
AssetDatabase.LoadAssetAtPath<Defense25DRunDefinition>(RunDefinitionPath);
|
|
Assert.That(definition, Is.Not.Null);
|
|
world = new GameObject("Long-run tower manager regression")
|
|
.AddComponent<Defense25DComparison>();
|
|
world.InitializeLongRun(settings, definition, 14);
|
|
world.AutoSpawn = false;
|
|
world.ReadInput = false;
|
|
yield return null;
|
|
}
|
|
|
|
[UnityTearDown]
|
|
public IEnumerator TearDown()
|
|
{
|
|
if (facilityVisuals != null)
|
|
{
|
|
facilityVisuals.Clear();
|
|
Object.Destroy(facilityVisuals.gameObject);
|
|
}
|
|
if (world != null)
|
|
Object.Destroy(world.gameObject);
|
|
if (settings != null)
|
|
{
|
|
if (settings.Enemy != null && settings.Enemy != sourceEnemy)
|
|
Object.Destroy(settings.Enemy);
|
|
Object.Destroy(settings);
|
|
}
|
|
Time.timeScale = previousTimeScale;
|
|
yield return null;
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator LongRunManagerBuildsAtTheEighteenthProfilePad()
|
|
{
|
|
Assert.That(world.RunLayout.PadCount, Is.EqualTo(18));
|
|
Assert.That(world.TowerManager.PadCount, Is.EqualTo(18));
|
|
Assert.That(world.TowerManager.GetPadPosition(17),
|
|
Is.EqualTo(world.RunLayout.PadPositions[17]));
|
|
|
|
Vector2 pad = world.TowerManager.GetPadPosition(17);
|
|
world.Player.ResetAt(pad);
|
|
Assert.That(world.TowerManager.TryBuildTowerAtPad(17, DefenseTowerKind.Direct),
|
|
Is.True);
|
|
Defense25DTower tower = world.TowerManager.GetTowerAtPad(17);
|
|
Assert.That(tower, Is.Not.Null);
|
|
Assert.That(tower.PadIndex, Is.EqualTo(17));
|
|
Assert.That(tower.Position, Is.EqualTo(pad));
|
|
yield return null;
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator SummonAtEighteenthPadReturnsTowardItsAnchorWithoutAView()
|
|
{
|
|
Defense25DTowerManager manager = world.TowerManager;
|
|
Assert.That(world.View, Is.Null,
|
|
"This test keeps presentation absent so summon movement cannot depend on camera visibility.");
|
|
Vector2 pad = manager.GetPadPosition(17);
|
|
world.Player.ResetAt(pad);
|
|
Assert.That(manager.TryBuildTowerAtPad(17, DefenseTowerKind.Summon), Is.True);
|
|
|
|
Defense25DTower tower = manager.GetTowerAtPad(17);
|
|
Assert.That(world.TowersOperational, Is.True);
|
|
Assert.That(tower.CommandTarget, Is.Null);
|
|
Defense25DSummon[] pack = world.Summons
|
|
.Where(candidate => candidate.Owner == tower)
|
|
.OrderBy(candidate => candidate.SlotIndex)
|
|
.ToArray();
|
|
Assert.That(pack, Has.Length.EqualTo(GameplayConstants.Current.Defense.SummonUnitCount));
|
|
Defense25DSummon summon = pack[0];
|
|
Vector2 anchor = summon.Actor.Position;
|
|
Vector2 packCenter = pack
|
|
.Select(candidate => candidate.Actor.Position)
|
|
.Aggregate(Vector2.zero, (sum, position) => sum + position) / pack.Length;
|
|
Vector2 awayFromPack = (anchor - packCenter).normalized;
|
|
Assert.That(awayFromPack.sqrMagnitude, Is.GreaterThan(.5f));
|
|
summon.Actor.ResetAt(anchor + awayFromPack * .6f);
|
|
Physics.SyncTransforms();
|
|
Vector2 displacedPosition = summon.Actor.Position;
|
|
float initialDistance = Vector2.Distance(displacedPosition, anchor);
|
|
|
|
summon.Step(.1f);
|
|
Assert.That(Vector2.Dot(summon.Actor.LastMove, anchor - displacedPosition),
|
|
Is.GreaterThan(0f),
|
|
$"The summon should move toward its anchor without intersecting its pack; "
|
|
+ $"blocked by {summon.Actor.LastMovementBlocker?.name ?? "none"}.");
|
|
yield return new WaitForFixedUpdate();
|
|
|
|
Assert.That(Vector2.Distance(summon.Actor.Position, anchor),
|
|
Is.LessThan(initialDistance),
|
|
"A summon on the outermost long-run pad must continue moving back to its anchor without a view.");
|
|
yield return null;
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator EveryLongRunPadCanBeSelectedBuiltUpgradedAndResetWithDefaultSummonPack()
|
|
{
|
|
Defense25DTowerManager manager = world.TowerManager;
|
|
DefenseConstants tuning = GameplayConstants.Current.Defense;
|
|
Assert.That(world.RunLayout.PadCount, Is.EqualTo(18));
|
|
Assert.That(manager.PadCount, Is.EqualTo(18));
|
|
Assert.That(manager.Towers.Count, Is.EqualTo(18));
|
|
Assert.That(world.SelectBuildKind(DefenseTowerKind.Summon), Is.True);
|
|
manager.AddCurrency(10_000);
|
|
|
|
for (int padId = 0; padId < world.RunLayout.PadCount; padId++)
|
|
{
|
|
Vector2 pad = world.RunLayout.PadPositions[padId];
|
|
Assert.That(manager.GetPadPosition(padId), Is.EqualTo(pad),
|
|
$"Pad {padId} must use this run's layout position.");
|
|
Assert.That(world.SelectBuildPad(padId), Is.True,
|
|
$"Pad {padId} must be selectable.");
|
|
Assert.That(world.SelectedBuildPad, Is.EqualTo(padId));
|
|
Assert.That(manager.GetTowerAtPad(padId), Is.Null);
|
|
|
|
world.Player.ResetAt(pad);
|
|
int beforeBuild = manager.ConstructionCurrency;
|
|
Assert.That(world.BuildSelectedTower(), Is.True,
|
|
$"Pad {padId} must be reachable and buildable.");
|
|
Assert.That(manager.ConstructionCurrency,
|
|
Is.EqualTo(beforeBuild - tuning.SummonTowerCost));
|
|
|
|
Defense25DTower tower = manager.GetTowerAtPad(padId);
|
|
Assert.That(manager.Towers[padId], Is.SameAs(tower));
|
|
Assert.That(manager.TowersInConstructionOrder[padId], Is.SameAs(tower));
|
|
Assert.That(tower.PadIndex, Is.EqualTo(padId));
|
|
Assert.That(tower.Position, Is.EqualTo(pad));
|
|
Assert.That(tower.Tier, Is.EqualTo(1));
|
|
Assert.That(tower.TargetSummonCount, Is.EqualTo(tuning.SummonUnitCount));
|
|
Assert.That(tower.SummonCount, Is.EqualTo(tuning.SummonUnitCount));
|
|
AssertDefaultSummonPack(tower, tuning.SummonUnitCount);
|
|
|
|
int beforeRejectedUpgrade = manager.ConstructionCurrency;
|
|
world.Player.ResetAt(pad + Vector2.right * (tuning.BuildInteractionRadius + .25f));
|
|
Assert.That(world.UpgradeSelectedTower(DefenseTowerBranch.None), Is.False,
|
|
$"Pad {padId} must reject upgrade interaction from outside its access radius.");
|
|
Assert.That(tower.Tier, Is.EqualTo(1));
|
|
Assert.That(manager.ConstructionCurrency, Is.EqualTo(beforeRejectedUpgrade));
|
|
|
|
world.Player.ResetAt(pad);
|
|
Assert.That(world.UpgradeSelectedTower(DefenseTowerBranch.None), Is.True,
|
|
$"Pad {padId} must allow a nearby upgrade through its selected-pad route.");
|
|
Assert.That(tower.Tier, Is.EqualTo(2));
|
|
Assert.That(tower.TargetSummonCount, Is.EqualTo(tuning.SummonUnitCount + 1));
|
|
Assert.That(tower.SummonCount, Is.EqualTo(tuning.SummonUnitCount + 1));
|
|
AssertDefaultSummonPack(tower, tuning.SummonUnitCount + 1);
|
|
yield return null;
|
|
}
|
|
|
|
Assert.That(world.SelectBuildPad(18), Is.False,
|
|
"A profile with 18 pads must reject the first out-of-range index.");
|
|
Assert.That(world.SelectedBuildPad, Is.EqualTo(17),
|
|
"A rejected index must preserve the last valid selection.");
|
|
|
|
int nextGeneration = world.RunGeneration + 1;
|
|
world.ResetRun();
|
|
yield return null;
|
|
|
|
Assert.That(world.RunGeneration, Is.EqualTo(nextGeneration));
|
|
Assert.That(manager.PadCount, Is.EqualTo(18));
|
|
Assert.That(manager.Towers.Count, Is.EqualTo(18));
|
|
Assert.That(manager.TowersInConstructionOrder, Is.Empty);
|
|
Assert.That(manager.ConstructionCurrency, Is.EqualTo(tuning.InitialConstructionCurrency));
|
|
Assert.That(world.Summons, Is.Empty,
|
|
"Restart must unregister every summon from every pad's pack.");
|
|
for (int padId = 0; padId < 18; padId++)
|
|
{
|
|
Assert.That(manager.GetTowerAtPad(padId), Is.Null,
|
|
$"Restart must clear tower state at pad {padId}.");
|
|
Assert.That(manager.GetPadPosition(padId), Is.EqualTo(world.RunLayout.PadPositions[padId]),
|
|
$"Restart must retain the selected profile's position for pad {padId}.");
|
|
}
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator SuccessfulUpgradeDeductsCostAndPlaysOneFacilityEffect()
|
|
{
|
|
Defense25DFacilityVisualConstants facilitySettings =
|
|
AssetDatabase.LoadAssetAtPath<Defense25DFacilityVisualConstants>(
|
|
"Assets/_Project/Constants/Defense25DComparison/FacilityVisualConstants.asset");
|
|
Assert.That(facilitySettings, Is.Not.Null);
|
|
facilityVisuals = new GameObject("Manager facility visual test")
|
|
.AddComponent<Defense25DFacilityVisuals>();
|
|
Assert.That(facilityVisuals.Bind(world, facilitySettings, 21, out string error),
|
|
Is.True, error);
|
|
world.TowerManager.SetFacilityVisuals(facilityVisuals);
|
|
|
|
Vector2 pad = world.TowerManager.GetPadPosition(17);
|
|
world.Player.ResetAt(pad);
|
|
Assert.That(world.TowerManager.TryBuildTowerAtPad(17, DefenseTowerKind.Direct), Is.True);
|
|
int beforeUpgrade = world.TowerManager.ConstructionCurrency;
|
|
int upgradeCost = GameplayConstants.Current.Defense.Tier2UpgradeCost;
|
|
|
|
Assert.That(world.TowerManager.TryUpgradeTowerAtPad(17, DefenseTowerBranch.None), Is.True);
|
|
Assert.That(world.TowerManager.ConstructionCurrency, Is.EqualTo(beforeUpgrade - upgradeCost));
|
|
Assert.That(world.TowerManager.GetTowerAtPad(17).Tier, Is.EqualTo(2));
|
|
Assert.That(facilityVisuals.transform.childCount, Is.EqualTo(1),
|
|
"One successful upgrade invokes the visual hook once.");
|
|
GameObject effect = GameObject.Find("Facility Tower Upgrade Pad 17 T2");
|
|
Assert.That(effect, Is.Not.Null);
|
|
|
|
int afterUpgrade = world.TowerManager.ConstructionCurrency;
|
|
Assert.That(world.TowerManager.TryUpgradeTowerAtPad(17, DefenseTowerBranch.First), Is.False,
|
|
"A rejected branch cannot invoke the facility hook or consume currency.");
|
|
Assert.That(world.TowerManager.ConstructionCurrency, Is.EqualTo(afterUpgrade));
|
|
Assert.That(facilityVisuals.transform.childCount, Is.EqualTo(1));
|
|
Assert.That(GameObject.Find("Facility Tower Upgrade Pad 17 T2"), Is.SameAs(effect));
|
|
yield return null;
|
|
}
|
|
|
|
private static ComparisonSpriteFrames ReadCharacter(
|
|
string folder,
|
|
string prefix,
|
|
string attack)
|
|
{
|
|
Sprite[] Read(string suffix) => AssetDatabase.LoadAllAssetsAtPath(
|
|
$"Assets/_Project/Art/Characters/{folder}/{prefix}_{suffix}.png")
|
|
.OfType<Sprite>()
|
|
.OrderBy(sprite => int.TryParse(
|
|
sprite.name[(sprite.name.LastIndexOf('_') + 1)..], out int frame)
|
|
? frame
|
|
: 0)
|
|
.ToArray();
|
|
return new ComparisonSpriteFrames
|
|
{
|
|
Idle = Read("Idle"),
|
|
Walk = Read("Walk"),
|
|
Attack = Read(attack),
|
|
Hurt = Read("Hurt"),
|
|
Death = Read("Death"),
|
|
};
|
|
}
|
|
|
|
private void AssertDefaultSummonPack(Defense25DTower tower, int expectedCount)
|
|
{
|
|
Defense25DSummon[] pack = world.Summons
|
|
.Where(summon => summon != null && summon.Owner == tower)
|
|
.OrderBy(summon => summon.SlotIndex)
|
|
.ToArray();
|
|
Assert.That(pack, Has.Length.EqualTo(expectedCount));
|
|
CollectionAssert.AreEqual(Enumerable.Range(0, expectedCount),
|
|
pack.Select(summon => summon.SlotIndex));
|
|
Assert.That(pack.All(summon => summon.Owner == tower
|
|
&& summon.Actor != null && summon.Actor.IsAlive), Is.True);
|
|
Assert.That(pack.All(summon => Vector2.Distance(summon.Actor.Position, tower.Position) < 2f),
|
|
Is.True, "The default summon pack must start anchored around its owning pad.");
|
|
}
|
|
}
|
|
}
|