274 lines
12 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|