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(SettingsPath)); Assert.That(settings, Is.Not.Null); sourceEnemy = AssetDatabase.LoadAssetAtPath( "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(RunDefinitionPath); Assert.That(definition, Is.Not.Null); world = new GameObject("Long-run tower manager regression") .AddComponent(); 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( "Assets/_Project/Constants/Defense25DComparison/FacilityVisualConstants.asset"); Assert.That(facilitySettings, Is.Not.Null); facilityVisuals = new GameObject("Manager facility visual test") .AddComponent(); 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() .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."); } } }