using System; using System.Collections; using System.Reflection; using BumpCombat.Constants; using BumpCombat.Core; using BumpCombat.Defense; using BumpCombat.Enemies; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; namespace BumpCombat.PlayModeTests { public sealed class DefenseSummonCrowdAttackPlayModeTests { private const float RepeatedHitTimeoutSeconds = 12f; private static readonly BindingFlags PrivateInstance = BindingFlags.Instance | BindingFlags.NonPublic; [UnityTest] public IEnumerator T1ToT3DefaultShowcaseSummonsRepeatedlyHitTheirOwnDummy() { MethodInfo startRun = typeof(DefensePrototypePlayModeTests).GetMethod( "StartDefenseRun", PrivateInstance); Assert.That(startRun, Is.Not.Null); IEnumerator setup = (IEnumerator)startRun.Invoke( new DefensePrototypePlayModeTests(), new object[] { true }); yield return setup; DefensePrototypeController controller = DefensePrototypeController.Instance; DefenseTower tower = FindSummonTower(); DefenseRouteAgent target = FindSummonDummy(); Assert.That(controller.IsTowerHitTestLayout, Is.True); Assert.That(tower, Is.Not.Null); Assert.That(target, Is.Not.Null); Assert.That(Vector2.Distance(tower.transform.position, controller.Tuning.GetBuildPadPosition(tower.PadIndex)), Is.LessThan(0.001f)); Assert.That(Vector2.Distance(target.transform.position, DefenseTowerHitTestLayout.GetDummyPosition(DefenseTowerKind.Summon)), Is.LessThan(0.001f)); foreach (DefenseTower other in UnityEngine.Object.FindObjectsByType( FindObjectsSortMode.None)) { if (other != tower) other.enabled = false; } SetEnemyCurrentHealth(target.Enemy, 10000f); tower.enabled = true; float readyDeadline = Time.realtimeSinceStartup + 8f; while (Time.realtimeSinceStartup < readyDeadline && (target.Enemy.State == EnemyState.Spawn || tower.CommandTarget != target)) { yield return new WaitForFixedUpdate(); } Assert.That(target.Enemy.State, Is.Not.EqualTo(EnemyState.Spawn)); Assert.That(tower.CommandTarget, Is.SameAs(target)); Assert.That(tower.Tier, Is.EqualTo(1)); Sprite[] attackFrames = DefenseTowerArt.Load("Summons/Slime/Attack01"); FieldInfo appliedField = typeof(DefenseSummonUnit).GetField( "attackDamageApplied", PrivateInstance); Assert.That(appliedField, Is.Not.Null); for (int tier = 1; tier <= 3; tier++) { if (tier > 1) { Assert.That(tower.Upgrade(DefenseTowerBranch.None), Is.True); yield return null; } Assert.That(tower.Tier, Is.EqualTo(tier)); Assert.That(tower.CommandTarget, Is.SameAs(target)); Assert.That(controller.Tuning.SummonBodySlamImpactFrame, Is.EqualTo(3)); Assert.That(controller.Tuning.SummonBodySlamSpeedMultiplier, Is.EqualTo(2f)); Assert.That(controller.Tuning.SummonBodySlamContactMargin, Is.EqualTo(0.06f)); DefenseSummonUnit[] units = tower.GetComponentsInChildren(); Assert.That(units.Length, Is.EqualTo(controller.Tuning.SummonUnitCount + tier - 1)); for (int i = 0; i < units.Length; i++) { for (int j = i + 1; j < units.Length; j++) { Assert.That(Physics2D.GetIgnoreCollision( units[i].BodyCollider, units[j].BodyCollider), Is.False, "Summon bodies must keep colliding while attack flow recovers."); } } float maximumHit = controller.Tuning.SummonDamage * (1f + controller.Tuning.TowerDamageBonusPerTier * (tier - 1)); int hitSteps = 0; float deadline = Time.realtimeSinceStartup + RepeatedHitTimeoutSeconds; while (hitSteps < 2 && Time.realtimeSinceStartup < deadline && !target.Enemy.IsDead) { float healthBefore = target.Enemy.CurrentHealth; bool[] appliedBefore = new bool[units.Length]; for (int i = 0; i < units.Length; i++) appliedBefore[i] = (bool)appliedField.GetValue(units[i]); yield return new WaitForFixedUpdate(); float damage = healthBefore - target.Enemy.CurrentHealth; if (damage <= 0f) continue; int resolvedAttacks = 0; bool impactFrameVisible = false; for (int i = 0; i < units.Length; i++) { if (!appliedBefore[i] && (bool)appliedField.GetValue(units[i])) resolvedAttacks++; int frame = Array.IndexOf( attackFrames, units[i].GetComponent().sprite); impactFrameVisible |= frame >= controller.Tuning.SummonBodySlamImpactFrame; } Assert.That(resolvedAttacks, Is.GreaterThan(0), "Every HP change must match a body-slam resolution in that physics step."); Assert.That(damage, Is.LessThanOrEqualTo( resolvedAttacks * maximumHit + 0.01f), "Each summon must damage at most once during one attack sequence."); Assert.That(impactFrameVisible, Is.True, "Damage must begin at Attack01 frame 3."); hitSteps++; Debug.Log($"SUMMON_CROWD T{tier} hitStep={hitSteps} damage={damage:F2}"); } Assert.That(hitSteps, Is.GreaterThanOrEqualTo(2), $"The real default T{tier} pack must land repeated hits without moving any unit or target. " + BuildBlockerDiagnostics(tower, target, units, attackFrames)); } } private static string BuildBlockerDiagnostics( DefenseTower tower, DefenseRouteAgent target, DefenseSummonUnit[] units, Sprite[] attackFrames) { Collider2D targetCollider = target.Enemy.GetComponent(); System.Text.StringBuilder result = new(); result.AppendLine($"tower={tower.transform.position} target={target.transform.position}"); foreach (DefenseSummonUnit unit in units) { Vector2 center = unit.BodyCollider.bounds.center; Vector2 delta = (Vector2)target.transform.position - center; float gap = Vector2.Distance(center, targetCollider.ClosestPoint(center)) - unit.BodyCollider.bounds.extents.x; int frame = Array.IndexOf( attackFrames, unit.GetComponent().sprite); result.Append($"{unit.name}[{unit.SlotIndex}] pos={center} gap={gap:F3} frame={frame} cast="); if (delta.sqrMagnitude > 0.0001f) { ContactFilter2D filter = ContactFilter2D.noFilter; filter.useTriggers = false; RaycastHit2D[] hits = new RaycastHit2D[16]; int count = unit.BodyCollider.Cast( delta.normalized, filter, hits, delta.magnitude); for (int i = 0; i < count; i++) { if (i > 0) result.Append(","); result.Append($"{hits[i].collider.name}@{hits[i].distance:F3}"); } } result.AppendLine(); } return result.ToString(); } private static DefenseTower FindSummonTower() { foreach (DefenseTower tower in UnityEngine.Object.FindObjectsByType( FindObjectsSortMode.None)) if (tower.Kind == DefenseTowerKind.Summon && tower.PadIndex == 1) return tower; return null; } private static DefenseRouteAgent FindSummonDummy() { foreach (DefenseRouteAgent agent in UnityEngine.Object.FindObjectsByType( FindObjectsSortMode.None)) if (agent.Enemy != null && agent.Enemy.name == "Tower Test Dummy Summon") return agent; return null; } private static void SetEnemyCurrentHealth(EnemyController enemy, float health) { FieldInfo field = typeof(EnemyController).GetField( "k__BackingField", PrivateInstance); Assert.That(field, Is.Not.Null); field.SetValue(enemy, health); } } }