Files
tiny_tackle_heroes/Assets/_Project/Tests/EditMode/FiniteArenaGeometryTests.cs
T

274 lines
12 KiB
C#

using System.Reflection;
using BumpCombat.Core;
using BumpCombat.Enemies;
using BumpCombat.Spawning;
using NUnit.Framework;
using UnityEngine;
namespace BumpCombat.Tests
{
public sealed class FiniteArenaGeometryTests
{
[TearDown]
public void Cleanup()
{
ArenaBounds[] bounds = Object.FindObjectsByType<ArenaBounds>(
FindObjectsSortMode.None);
for (int i = 0; i < bounds.Length; i++)
{
Object.DestroyImmediate(bounds[i].gameObject);
}
SpawnDirector[] directors = Object.FindObjectsByType<SpawnDirector>(
FindObjectsSortMode.None);
for (int i = 0; i < directors.Length; i++)
{
if (directors[i] != null)
{
Object.DestroyImmediate(directors[i].gameObject);
}
}
ArenaBounds.Instance?.gameObject.SetActive(false);
}
[Test]
public void DefaultGeometry_UsesFullFloorWithSmallSharedActorClearance()
{
GameObject root = CreateBounds();
ArenaBounds bounds = root.GetComponent<ArenaBounds>();
Assert.That(bounds.BackgroundHalfExtents.x, Is.EqualTo(15f).Within(0.0001f));
Assert.That(bounds.BackgroundHalfExtents.y, Is.EqualTo(8.4375f).Within(0.0001f));
Assert.That(bounds.PlayableHalfExtents, Is.EqualTo(bounds.BackgroundHalfExtents));
Assert.That(bounds.PlayerPadding, Is.EqualTo(new Vector2(0.5f, 0.625f)));
Assert.That(bounds.PlayerClampHalfExtents.x, Is.EqualTo(14.5f).Within(0.0001f));
Assert.That(bounds.PlayerClampHalfExtents.y, Is.EqualTo(7.8125f).Within(0.0001f));
Assert.That(bounds.SharedActorClampHalfExtents, Is.EqualTo(bounds.PlayerClampHalfExtents));
Assert.That(bounds.CameraCenterHalfExtents.x, Is.EqualTo(5f).Within(0.0001f));
Assert.That(bounds.CameraCenterHalfExtents.y, Is.EqualTo(2.8125f).Within(0.0001f));
}
[Test]
public void ClampsPlayerEnemyAndCameraInsideFiniteMap()
{
GameObject root = CreateBounds();
ArenaBounds bounds = root.GetComponent<ArenaBounds>();
Vector2 playerPosition = bounds.ClampPlayerPosition(new Vector2(99f, -99f));
Assert.That(playerPosition.x, Is.EqualTo(14.5f).Within(0.0001f));
Assert.That(playerPosition.y, Is.EqualTo(-7.8125f).Within(0.0001f));
Vector2 enemyPosition = bounds.ClampEnemyPosition(new Vector2(-99f, 99f));
Assert.That(enemyPosition.x, Is.EqualTo(-14.5f).Within(0.0001f));
Assert.That(enemyPosition.y, Is.EqualTo(7.8125f).Within(0.0001f));
Assert.That(
bounds.ClampEnemyPosition(new Vector2(99f, -99f)),
Is.EqualTo(bounds.ClampPlayerPosition(new Vector2(99f, -99f))));
Vector2 cameraPosition = bounds.ClampCameraCenter(new Vector2(99f, -99f));
Assert.That(cameraPosition.x, Is.EqualTo(5f).Within(0.0001f));
Assert.That(cameraPosition.y, Is.EqualTo(-2.8125f).Within(0.0001f));
GameObject cameraObject = new("Arena Bounds Camera Test");
Camera camera = cameraObject.AddComponent<Camera>();
camera.orthographic = true;
camera.orthographicSize = 5.625f;
camera.aspect = 16f / 9f;
Vector2 cameraCenter = bounds.ClampCameraCenter(
new Vector2(99f, 99f),
bounds.GetCameraVisibleHalfExtents(camera));
camera.transform.position = cameraCenter;
Bounds visibleBounds = bounds.GetCameraVisibleBounds(camera);
Assert.That(visibleBounds.min.x, Is.GreaterThanOrEqualTo(-15f));
Assert.That(visibleBounds.max.x, Is.EqualTo(15f).Within(0.0001f));
Assert.That(visibleBounds.min.y, Is.GreaterThanOrEqualTo(-8.4375f));
Assert.That(visibleBounds.max.y, Is.EqualTo(8.4375f).Within(0.0001f));
cameraCenter = bounds.ClampCameraCenter(
new Vector2(-99f, -99f),
bounds.GetCameraVisibleHalfExtents(camera));
camera.transform.position = cameraCenter;
visibleBounds = bounds.GetCameraVisibleBounds(camera);
Assert.That(visibleBounds.min.x, Is.EqualTo(-15f).Within(0.0001f));
Assert.That(visibleBounds.max.x, Is.LessThanOrEqualTo(15f));
Assert.That(visibleBounds.min.y, Is.EqualTo(-8.4375f).Within(0.0001f));
Assert.That(visibleBounds.max.y, Is.LessThanOrEqualTo(8.4375f));
Object.DestroyImmediate(cameraObject);
}
[Test]
public void DeadZone_HoldsUntilTargetCrossesEachAxis()
{
Assert.That(
ArenaCameraFollow.GetDeadZoneTargetCenter(
Vector2.zero,
new Vector2(0.5f, 0.3f),
new Vector2(0.5f, 0.3f)),
Is.EqualTo(Vector2.zero));
Vector2 movedCenter = ArenaCameraFollow.GetDeadZoneTargetCenter(
Vector2.zero,
new Vector2(0.7f, -0.5f),
new Vector2(0.5f, 0.3f));
Assert.That(movedCenter.x, Is.EqualTo(0.2f).Within(0.0001f));
Assert.That(movedCenter.y, Is.EqualTo(-0.2f).Within(0.0001f));
}
[Test]
public void SpawnPositionsStayReachableAndClearOfPlayer()
{
GameObject boundsObject = CreateBounds();
ArenaBounds bounds = boundsObject.GetComponent<ArenaBounds>();
GameObject directorObject = new("Spawn Director Test");
SpawnDirector director = directorObject.AddComponent<SpawnDirector>();
for (int i = 0; i < 64; i++)
{
Vector2 position = director.GetSpawnPositionForTests();
Assert.That(bounds.IsInsideReachableArena(position, 0.25f), Is.True);
Assert.That(position.magnitude, Is.GreaterThanOrEqualTo(4f));
}
}
[Test]
public void EnemyGroundAnchors_StayOnSharedPlayerBoundaryAfterEdgeAndCornerKnockback()
{
GameObject boundsObject = CreateBounds();
ArenaBounds bounds = boundsObject.GetComponent<ArenaBounds>();
EnemyController normalEnemy = CreateEnemy(
new Vector2(0.72f, 0.72f),
new Vector2(0.1f, -0.28f),
1f);
EnemyController eventEnemy = CreateEnemy(
new Vector2(0.72f, 0.72f),
new Vector2(-0.18f, -0.28f),
1.25f);
try
{
MethodInfo clampMethod = typeof(EnemyController).GetMethod(
"ClampEnemyPosition",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(clampMethod, Is.Not.Null);
EnemyController[] enemies = { normalEnemy, eventEnemy };
for (int enemyIndex = 0; enemyIndex < enemies.Length; enemyIndex++)
{
EnemyController enemy = enemies[enemyIndex];
Collider2D collider = enemy.GetComponent<Collider2D>();
Rigidbody2D body = enemy.GetComponent<Rigidbody2D>();
Vector2[] knockbackDestinations =
{
new(100f, 100f),
new(-100f, 100f),
new(100f, -100f),
new(-100f, -100f),
};
for (int i = 0; i < knockbackDestinations.Length; i++)
{
Vector2 clamped = (Vector2)clampMethod.Invoke(
enemy,
new object[] { knockbackDestinations[i] });
body.position = clamped;
Physics2D.SyncTransforms();
Vector2 safeHalfExtents = bounds.SharedActorClampHalfExtents;
Assert.That(body.position.x, Is.GreaterThanOrEqualTo(-safeHalfExtents.x - 0.0001f));
Assert.That(body.position.x, Is.LessThanOrEqualTo(safeHalfExtents.x + 0.0001f));
Assert.That(body.position.y, Is.GreaterThanOrEqualTo(-safeHalfExtents.y - 0.0001f));
Assert.That(body.position.y, Is.LessThanOrEqualTo(safeHalfExtents.y + 0.0001f));
Bounds colliderBounds = collider.bounds;
// The collider can overhang the ground-anchor edge by
// its art footprint; only the anchor defines movement.
Assert.That(colliderBounds.size.x, Is.GreaterThan(0f));
Assert.That(colliderBounds.size.y, Is.GreaterThan(0f));
}
}
}
finally
{
Object.DestroyImmediate(normalEnemy.gameObject);
Object.DestroyImmediate(eventEnemy.gameObject);
}
}
[Test]
public void EnemyGroundAnchor_PostPhysicsCorrectionRestoresSharedBoundary()
{
CreateBounds();
EnemyController enemy = CreateEnemy(
new Vector2(0.72f, 0.72f),
new Vector2(-0.18f, -0.28f),
1.25f);
try
{
Rigidbody2D body = enemy.GetComponent<Rigidbody2D>();
Collider2D collider = enemy.GetComponent<Collider2D>();
body.position = new Vector2(100f, -100f);
Physics2D.SyncTransforms();
MethodInfo correctionMethod = typeof(EnemyController).GetMethod(
"KeepBodyInsideArena",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(correctionMethod, Is.Not.Null);
correctionMethod.Invoke(enemy, null);
Physics2D.SyncTransforms();
Bounds colliderBounds = collider.bounds;
ArenaBounds bounds = ArenaBounds.Resolve();
Vector2 safeHalfExtents = bounds.SharedActorClampHalfExtents;
Assert.That(body.position.x, Is.GreaterThanOrEqualTo(-safeHalfExtents.x - 0.0001f));
Assert.That(body.position.x, Is.LessThanOrEqualTo(safeHalfExtents.x + 0.0001f));
Assert.That(body.position.y, Is.GreaterThanOrEqualTo(-safeHalfExtents.y - 0.0001f));
Assert.That(body.position.y, Is.LessThanOrEqualTo(safeHalfExtents.y + 0.0001f));
Assert.That(colliderBounds.size.x, Is.GreaterThan(0f));
}
finally
{
Object.DestroyImmediate(enemy.gameObject);
}
}
private static EnemyController CreateEnemy(
Vector2 colliderSize,
Vector2 colliderOffset,
float rootScale)
{
GameObject enemyObject = new("Boundary Test Enemy");
Rigidbody2D body = enemyObject.AddComponent<Rigidbody2D>();
body.bodyType = RigidbodyType2D.Kinematic;
body.gravityScale = 0f;
BoxCollider2D collider = enemyObject.AddComponent<BoxCollider2D>();
collider.size = colliderSize;
collider.offset = colliderOffset;
enemyObject.AddComponent<SpriteRenderer>();
enemyObject.transform.localScale = Vector3.one * rootScale;
EnemyController enemy = enemyObject.AddComponent<EnemyController>();
// Unity does not invoke runtime Awake in EditMode. Populate the
// same private references that Awake would cache so this test
// exercises the controller's real clamp path.
SetPrivateField(enemy, "body", body);
SetPrivateField(enemy, "bodyCollider", collider);
Physics2D.SyncTransforms();
return enemy;
}
private static void SetPrivateField<T>(
object target,
string fieldName,
T value)
{
typeof(EnemyController)
.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic)
?.SetValue(target, value);
}
private static GameObject CreateBounds()
{
GameObject root = new("Arena Bounds Test");
root.AddComponent<ArenaBounds>();
return root;
}
}
}