Files
tiny_tackle_heroes/Assets/_Project/Tests/PlayMode/Defense25DTowerManagerPlayModeTests.cs
T
2026-09-28 11:07:17 +09:00

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.");
}
}
}