Files
tiny_tackle_heroes/Assets/_Project/Tests/PlayMode/DefenseSummonCrowdAttackPlayModeTests.cs
T
2026-09-27 21:08:05 +09:00

200 lines
9.1 KiB
C#

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<DefenseTower>(
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<DefenseSummonUnit>();
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<SpriteRenderer>().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<Collider2D>();
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<SpriteRenderer>().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<DefenseTower>(
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<DefenseRouteAgent>(
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(
"<CurrentHealth>k__BackingField", PrivateInstance);
Assert.That(field, Is.Not.Null);
field.SetValue(enemy, health);
}
}
}