Initial commit: Tiny Tackle Heroes Unity project
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user