2.5 update
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user