200 lines
9.1 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|