Files
tiny_tackle_heroes/Assets/_Project/Tests/PlayMode/RangeAuditPlayModeTests.cs
T

456 lines
17 KiB
C#

using System.Collections;
using System.Reflection;
using BumpCombat.Combat;
using BumpCombat.Core;
using BumpCombat.Enemies;
using BumpCombat.Player;
using BumpCombat.Progression;
using BumpCombat.Spawning;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
namespace BumpCombat.PlayModeTests
{
public sealed class RangeAuditPlayModeTests
{
private static readonly BindingFlags NonPublicInstance =
BindingFlags.Instance | BindingFlags.NonPublic;
private static readonly EnemyKind[] HorizontalRootKinds =
{
EnemyKind.Skeleton,
EnemyKind.ArmoredSkeleton,
EnemyKind.GreatswordSkeleton,
EnemyKind.Werewolf,
EnemyKind.Werebear,
EnemyKind.NecroGolem,
EnemyKind.Necromancer,
};
private bool previousGrantCatalogForTests;
private bool previousProductionModeForTests;
private float previousTimeScale;
[SetUp]
public void SetUp()
{
previousGrantCatalogForTests =
ArtifactRewardController.GrantCatalogForTests;
previousProductionModeForTests =
RunManager.ForceProductionModeForTests;
previousTimeScale = Time.timeScale;
ArtifactRewardController.GrantCatalogForTests = true;
RunManager.ForceProductionModeForTests = false;
Time.timeScale = 1f;
}
[TearDown]
public void TearDown()
{
ArtifactRewardController.GrantCatalogForTests =
previousGrantCatalogForTests;
RunManager.ForceProductionModeForTests =
previousProductionModeForTests;
Time.timeScale = previousTimeScale;
}
[UnityTest]
public IEnumerator NecroGolemAttack03_UsesActualPlayerBodyAndActiveEnd()
{
yield return LoadCombatPrototype();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
PlayerHealth health = player?.GetComponent<PlayerHealth>();
Rigidbody2D playerBody = player?.GetComponent<Rigidbody2D>();
Assert.That(director, Is.Not.Null);
Assert.That(player, Is.Not.Null);
Assert.That(health, Is.Not.Null);
Assert.That(playerBody, Is.Not.Null);
director.enabled = false;
DisableSceneEnemies();
player.enabled = false;
player.GetComponent<BumpCombatResolver>().enabled = false;
EnemyController golem = SpawnGolem(director, Vector2.zero);
yield return null;
ConfigureGolemAttack03(golem, director);
golem.enabled = false;
EnemyAttack attack = golem.GetComponent<EnemyAttack>();
Rigidbody2D golemBody = golem.GetComponent<Rigidbody2D>();
AssertGolemAttack03Contract(golem);
Vector2 root = golemBody.position;
Vector2 rightOutside = root + new Vector2(1.8f, 1.9f);
Vector2 rightInside = root + new Vector2(1.9f, 0.4f);
float activeDuration = golem.ActiveDuration;
PlacePlayer(playerBody, rightOutside);
float healthBefore = health.CurrentHealth;
BeginActiveAttack(
golem,
attack,
Vector2.right,
playerBody.position);
Assert.That(health.CurrentHealth, Is.EqualTo(healthBefore));
attack.TickActive(activeDuration * 0.5f);
Assert.That(health.CurrentHealth, Is.EqualTo(healthBefore));
PlacePlayer(playerBody, rightInside);
attack.TickActive(activeDuration);
Assert.That(
health.CurrentHealth,
Is.EqualTo(healthBefore),
"A contact first observed at the exact active end must not hit.");
attack.TickActive(activeDuration + 0.01f);
Assert.That(health.CurrentHealth, Is.EqualTo(healthBefore));
attack.EndAttack();
SetState(golem, EnemyState.Recovery, golem.RecoveryDuration);
attack.TickActive(activeDuration + 0.1f);
Assert.That(health.CurrentHealth, Is.EqualTo(healthBefore));
PlacePlayer(playerBody, rightInside);
BeginActiveAttack(
golem,
attack,
Vector2.right,
playerBody.position);
float healthAfterRightHit = health.CurrentHealth;
Assert.That(
healthAfterRightHit,
Is.LessThan(healthBefore),
"Attack03 must damage the actual player body inside the box.");
attack.TickActive(activeDuration * 0.8f);
attack.TickActive(activeDuration);
attack.TickActive(activeDuration + 0.01f);
attack.EndAttack();
SetState(golem, EnemyState.Recovery, golem.RecoveryDuration);
attack.TickActive(activeDuration + 0.1f);
Assert.That(
health.CurrentHealth,
Is.EqualTo(healthAfterRightHit).Within(0.0001f),
"Attack03 must damage once through active end and recovery.");
yield return new WaitForSecondsRealtime(
health.InvulnerabilityDuration + 0.05f);
health.Heal(health.MaxHealth);
Vector2 leftOutside = root + new Vector2(-1.8f, 1.9f);
Vector2 leftInside = root + new Vector2(-1.9f, 0.4f);
PlacePlayer(playerBody, leftOutside);
float leftBefore = health.CurrentHealth;
BeginActiveAttack(
golem,
attack,
Vector2.left,
playerBody.position);
Assert.That(health.CurrentHealth, Is.EqualTo(leftBefore));
PlacePlayer(playerBody, leftInside);
attack.TickActive(activeDuration * 0.8f);
Assert.That(
health.CurrentHealth,
Is.LessThan(leftBefore),
"The mirrored left Attack03 box must hit the actual body.");
float leftAfterHit = health.CurrentHealth;
attack.TickActive(activeDuration);
attack.TickActive(activeDuration + 0.01f);
attack.EndAttack();
SetState(golem, EnemyState.Recovery, golem.RecoveryDuration);
attack.TickActive(activeDuration + 0.1f);
Assert.That(
health.CurrentHealth,
Is.EqualTo(leftAfterHit).Within(0.0001f));
Object.Destroy(golem.gameObject);
}
[UnityTest]
public IEnumerator HorizontalRootKinds_LockDiagonalRequestedDirection()
{
yield return LoadCombatPrototype();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
Assert.That(director, Is.Not.Null);
director.enabled = false;
DisableSceneEnemies();
for (int i = 0; i < HorizontalRootKinds.Length; i++)
{
EnemyKind kind = HorizontalRootKinds[i];
EnemyController enemy = Object.Instantiate(
FindCatalogPrefab(director, kind),
new Vector2(20f + i * 2f, 20f),
Quaternion.identity);
yield return null;
enemy.enabled = false;
EnemyAttack attack = enemy.GetComponent<EnemyAttack>();
Assert.That(attack, Is.Not.Null, kind.ToString());
Assert.That(
enemy.CurrentAttackPattern.DirectionMode,
Is.EqualTo(EnemyAttackDirectionMode.HorizontalRoot),
kind.ToString());
attack.BeginWarning(
new Vector2(1f, 1f).normalized,
enemy.transform.position + Vector3.one,
enemy.WarningDuration);
Assert.That(
attack.LockedDirection,
Is.EqualTo(Vector2.right),
kind + " must resolve positive diagonal input horizontally.");
attack.EndAttack(true);
attack.BeginWarning(
new Vector2(-1f, 1f).normalized,
enemy.transform.position + Vector3.one,
enemy.WarningDuration);
Assert.That(
attack.LockedDirection,
Is.EqualTo(Vector2.left),
kind + " must preserve the mirrored horizontal side.");
attack.EndAttack(true);
Object.Destroy(enemy.gameObject);
yield return null;
}
}
[UnityTest]
public IEnumerator EnemyGroundAnchors_StayOnSharedPlayerBoundaryAfterSpawnWarningAndKnockback()
{
yield return LoadCombatPrototype();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
Assert.That(director, Is.Not.Null);
Assert.That(player, Is.Not.Null);
director.enabled = false;
DisableSceneEnemies();
ArenaBounds bounds = Object.FindAnyObjectByType<ArenaBounds>();
EnemyController normalPrefab = FindCatalogPrefab(
director,
EnemyKind.Skeleton);
EnemyController eventPrefab = FindCatalogPrefab(
director,
EnemyKind.GreatswordSkeleton);
Assert.That(bounds, Is.Not.Null);
Assert.That(normalPrefab, Is.Not.Null);
Assert.That(eventPrefab, Is.Not.Null);
Vector2 halfExtents = bounds.SharedActorClampHalfExtents;
Vector2[] corners =
{
new(halfExtents.x, halfExtents.y),
new(-halfExtents.x, -halfExtents.y),
};
for (int i = 0; i < corners.Length; i++)
{
EnemyController enemy = Object.Instantiate(
normalPrefab,
corners[i],
Quaternion.identity);
yield return null;
AssertEnemyBodyInside(bounds, enemy, "normal spawn");
Rigidbody2D body = enemy.GetComponent<Rigidbody2D>();
body.position = corners[i];
Physics2D.SyncTransforms();
SetState(enemy, EnemyState.Warning, 5f);
yield return null;
AssertEnemyBodyInside(bounds, enemy, "warning correction");
body.position = corners[i];
Physics2D.SyncTransforms();
bool damaged = enemy.TryTakeDamage(
1f,
corners[i].normalized,
2f);
Assert.That(damaged, Is.True, "a valid nonlethal knockback hit must be accepted");
yield return new WaitForFixedUpdate();
yield return null;
AssertEnemyBodyInside(bounds, enemy, "knockback correction");
Object.Destroy(enemy.gameObject);
yield return null;
}
EnemyController eventEnemy = Object.Instantiate(
eventPrefab,
corners[0],
Quaternion.identity);
eventEnemy.ConfigureRunEventEnemy(
RunTimedEvent.MidBoss,
director.GetEventEnemyTuning(RunTimedEvent.MidBoss));
yield return null;
AssertEnemyBodyInside(bounds, eventEnemy, "scaled event spawn");
Object.Destroy(eventEnemy.gameObject);
}
private static void AssertEnemyBodyInside(
ArenaBounds bounds,
EnemyController enemy,
string checkpoint)
{
Collider2D bodyCollider = enemy.GetComponent<Collider2D>();
Assert.That(bodyCollider, Is.Not.Null, checkpoint);
Vector2 safeHalfExtents = bounds.SharedActorClampHalfExtents;
Vector2 groundAnchor = enemy.GroundAnchorPosition;
Assert.That(
groundAnchor.x,
Is.GreaterThanOrEqualTo(-safeHalfExtents.x - 0.001f),
checkpoint);
Assert.That(
groundAnchor.x,
Is.LessThanOrEqualTo(safeHalfExtents.x + 0.001f),
checkpoint);
Assert.That(
groundAnchor.y,
Is.GreaterThanOrEqualTo(-safeHalfExtents.y - 0.001f),
checkpoint);
Assert.That(
groundAnchor.y,
Is.LessThanOrEqualTo(safeHalfExtents.y + 0.001f),
checkpoint);
}
private static void AssertGolemAttack03Contract(EnemyController golem)
{
EnemyAttackPattern pattern = golem.CurrentAttackPattern;
Assert.That(pattern.AttackShape, Is.EqualTo(EnemyAttackShape.Box));
Assert.That(pattern.AttackLength, Is.EqualTo(1.05f).Within(0.0001f));
Assert.That(pattern.AttackWidth, Is.EqualTo(0.55f).Within(0.0001f));
Assert.That(
pattern.AttackOriginOffset,
Is.EqualTo(new Vector2(0.05f, -0.05f)));
Assert.That(
pattern.AnimationLeadTime,
Is.EqualTo(0.625f).Within(0.0001f));
Assert.That(golem.ActiveDuration, Is.EqualTo(0.25f).Within(0.0001f));
Assert.That(
EnemyAttack.GetRootScale(golem.transform),
Is.EqualTo(2f).Within(0.001f));
Assert.That(golem.AttackLength, Is.EqualTo(2.1f).Within(0.001f));
Assert.That(golem.AttackWidth, Is.EqualTo(1.1f).Within(0.001f));
}
private static void BeginActiveAttack(
EnemyController enemy,
EnemyAttack attack,
Vector2 direction,
Vector2 target)
{
attack.BeginWarning(direction, target, enemy.WarningDuration);
SetState(enemy, EnemyState.Warning, enemy.WarningDuration);
attack.Activate();
SetState(enemy, EnemyState.Active, enemy.ActiveDuration);
}
private static EnemyController SpawnGolem(
SpawnDirector director,
Vector2 position)
{
EnemyController prefab = FindCatalogPrefab(
director,
EnemyKind.NecroGolem);
Assert.That(prefab, Is.Not.Null);
return Object.Instantiate(prefab, position, Quaternion.identity);
}
private static void ConfigureGolemAttack03(
EnemyController golem,
SpawnDirector director)
{
golem.ConfigureRunEventEnemy(
RunTimedEvent.MidBoss,
director.GetEventEnemyTuning(RunTimedEvent.MidBoss));
SetPrivateField(golem, "attackPatternIndex", 2);
}
private static void PlacePlayer(
Rigidbody2D playerBody,
Vector2 position)
{
playerBody.linearVelocity = Vector2.zero;
playerBody.position = position;
Physics2D.SyncTransforms();
}
private static void SetState(
EnemyController enemy,
EnemyState state,
float duration)
{
typeof(EnemyController)
.GetMethod("EnterState", NonPublicInstance)
?.Invoke(enemy, new object[] { state, duration });
}
private static void SetPrivateField<T>(
object target,
string fieldName,
T value)
{
target.GetType()
.GetField(fieldName, NonPublicInstance)
?.SetValue(target, value);
}
private static void DisableSceneEnemies()
{
EnemyController[] enemies = Object.FindObjectsByType<EnemyController>(
FindObjectsSortMode.None);
foreach (EnemyController enemy in enemies)
{
enemy.gameObject.SetActive(false);
}
}
private static EnemyController FindCatalogPrefab(
SpawnDirector director,
EnemyKind kind)
{
string[] fieldNames =
{
"enemyPrefabs",
"elitePrefabs",
"midBossPrefabs",
"finalBossPrefabs",
};
foreach (string fieldName in fieldNames)
{
FieldInfo field = typeof(SpawnDirector).GetField(
fieldName,
BindingFlags.Instance | BindingFlags.NonPublic);
EnemyController[] prefabs =
field?.GetValue(director) as EnemyController[];
if (prefabs == null)
{
continue;
}
foreach (EnemyController prefab in prefabs)
{
if (prefab != null
&& prefab.Definition != null
&& prefab.Definition.Kind == kind)
{
return prefab;
}
}
}
return null;
}
private static IEnumerator LoadCombatPrototype()
{
AsyncOperation load = SceneManager.LoadSceneAsync("CombatPrototype");
while (!load.isDone)
{
yield return null;
}
yield return null;
yield return null;
Assert.That(Time.timeScale, Is.EqualTo(1f));
}
}
}