Files
tiny_tackle_heroes/Assets/_Project/Tests/PlayMode/Defense25DBodyColliderPlayModeTests.cs
T
2026-09-27 21:08:05 +09:00

169 lines
9.2 KiB
C#

using System.IO;
using BumpCombat.Defense;
using BumpCombat.Defense.Comparison;
using NUnit.Framework;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
using Object = UnityEngine.Object;
namespace BumpCombat.Tests
{
public sealed class Defense25DBodyColliderPlayModeTests
{
private const string ScenePath = "Assets/_Project/Scenes/Defense25DComparison.unity";
private const float Ppu = 32f;
private Scene ownedTestScene;
[UnityTest]
public System.Collections.IEnumerator BodyCoreCapsulesMatchFixedFamilyScreenBoundsAndCenters()
{
Time.timeScale = 1f;
yield return EditorSceneManager.LoadSceneAsyncInPlayMode(ScenePath,
new LoadSceneParameters(LoadSceneMode.Single));
ownedTestScene = SceneManager.GetActiveScene();
Defense25DComparison world = Object.FindAnyObjectByType<Defense25DComparison>();
Assert.That(world, Is.Not.Null);
yield return null;
world.AutoSpawn = false;
world.ReadInput = false;
world.ShowBoundaries = true;
world.SetPaused(true);
Vector2 origin = new(0f, -2f);
world.Player.ResetAt(origin + Vector2.left * 4f);
world.Enemies[0].Spawn(origin + Vector2.left * 2f);
Defense25DActor slime = Summon(world, "Body Slime", origin, 1, DefenseTowerBranch.First);
Defense25DActor giant = Summon(world, "Body Giant", origin + Vector2.right * 2f, 4, DefenseTowerBranch.First);
Defense25DActor lava = Summon(world, "Body Lava", origin + Vector2.right * 4f, 4, DefenseTowerBranch.Second);
lava.Facing = Vector2.left;
Physics.SyncTransforms();
yield return null;
yield return null;
Defense25DActor[] actors = { world.Player, world.Enemies[0].Actor, slime, giant, lava };
string output = System.Environment.GetEnvironmentVariable("DEFENSE25D_OUTPUT");
if (string.IsNullOrWhiteSpace(output)) output = Path.Combine(Path.GetTempPath(), "Defense25DValidation", "captures");
Directory.CreateDirectory(output);
string path = Path.Combine(output, "body-collider-overlay.png");
world.View.Camera.Render();
RenderTexture.active = world.View.Texture;
Texture2D image = new(world.View.Texture.width, world.View.Texture.height, TextureFormat.RGBA32, false);
image.ReadPixels(new Rect(0, 0, image.width, image.height), 0, 0);
image.Apply();
File.WriteAllBytes(path, image.EncodeToPNG());
Object.DestroyImmediate(image);
RenderTexture.active = null;
// Player/Skeleton retain their existing 2D-collider-derived baseline.
AssertShape(world.Player, .275f, .85f);
AssertShape(world.Enemies[0].Actor, .275f, .85f);
// Fixed inscribed body cores from the shadowless Idle audit, 32 PPU, actual variant render scale.
AssertShape(slime, .1890625f, .378125f); // 11px inner body core * 1.1 / 32
AssertShape(giant, .5328125f, 1.065625f); // 22px inner body core * 1.55 / 32
AssertShape(lava, .23359375f, .4671875f); // 13px inner body core * 1.15 / 32
AssertSkeletonCenter(world, world.Enemies[0].Actor);
AssertBodyCenter(world, slime, -1.5f, -1.5f);
AssertBodyCenter(world, giant, -3f, 17f);
AssertBodyCenter(world, lava, -2f, -.5f);
AssertCapsuleScreenCore(world, slime, 11f);
AssertCapsuleScreenCore(world, giant, 22f);
AssertCapsuleScreenCore(world, lava, 13f);
Defense25DActor probe = Summon(world, "Body Slime contact probe",
origin + Vector2.right * .37f, 1, DefenseTowerBranch.First);
Physics.SyncTransforms();
Assert.That(slime.GapTo(probe), Is.EqualTo(-.008125f).Within(.002f));
Assert.That(Physics.ComputePenetration(slime.BodyCollider, slime.transform.position,
slime.transform.rotation, probe.BodyCollider, probe.transform.position,
probe.transform.rotation, out _, out _), Is.True,
"The actual capsule bodies overlap at the tested body-core contact distance.");
probe.ResetAt(origin + Vector2.right * .4f);
Physics.SyncTransforms();
Assert.That(slime.GapTo(probe), Is.EqualTo(.021875f).Within(.002f));
Assert.That(Physics.ComputePenetration(slime.BodyCollider, slime.transform.position,
slime.transform.rotation, probe.BodyCollider, probe.transform.position,
probe.transform.rotation, out _, out _), Is.False,
"The actual capsule bodies miss beyond the tested body-core boundary.");
Assert.That(File.Exists(path), Is.True);
yield return null;
}
[UnityTearDown]
public System.Collections.IEnumerator UnloadOwnedTestScene()
{
Time.timeScale = 1f;
if (ownedTestScene.IsValid() && ownedTestScene.isLoaded)
{
Scene cleanup = SceneManager.CreateScene("BodyColliderTestCleanup");
SceneManager.SetActiveScene(cleanup);
AsyncOperation unload = SceneManager.UnloadSceneAsync(ownedTestScene);
if (unload != null) yield return unload;
}
ownedTestScene = default;
}
private static void AssertShape(Defense25DActor actor, float radius, float height)
{
Assert.That(actor.Radius, Is.EqualTo(radius).Within(.0001f), actor.name + " combat radius");
Assert.That(actor.BodyCollider.radius, Is.EqualTo(radius).Within(.0001f), actor.name + " capsule radius");
Assert.That(actor.BodyCollider.height, Is.EqualTo(height).Within(.0001f), actor.name + " capsule height");
}
private static void AssertBodyCenter(Defense25DComparison world, Defense25DActor actor,
float centerXFromPivotPixels, float centerUpFromPivotPixels)
{
float scale = actor.Visual.VisualScale;
Vector3 anchor = world.View.Camera.WorldToScreenPoint(actor.Visual.FootPosition);
Vector3 center = world.View.Camera.WorldToScreenPoint(
actor.BodyCollider.transform.TransformPoint(actor.BodyCollider.center));
float pixelsPerUnit = world.View.Camera.pixelHeight / (2f * world.View.Camera.orthographicSize);
float footCorrectionPixels = -actor.Visual.FootVisualOffset * Ppu / scale;
float facingSign = actor.Facing.x < 0f ? -1f : 1f;
float expectedX = centerXFromPivotPixels * facingSign * scale / Ppu * pixelsPerUnit;
float expectedY = (centerUpFromPivotPixels + footCorrectionPixels) * scale / Ppu * pixelsPerUnit;
Assert.That(center.x - anchor.x, Is.EqualTo(expectedX).Within(1.5f), actor.name + " projected body center X");
Assert.That(center.y - anchor.y, Is.EqualTo(expectedY).Within(1.5f), actor.name + " projected body center Y");
}
private static void AssertSkeletonCenter(Defense25DComparison world, Defense25DActor skeleton)
{
Camera camera = world.View.Camera;
Vector3 anchor = camera.WorldToScreenPoint(skeleton.Visual.FootPosition);
Vector3 center = camera.WorldToScreenPoint(
skeleton.BodyCollider.transform.TransformPoint(skeleton.BodyCollider.center));
float pixelsPerUnit = camera.pixelHeight / (2f * camera.orthographicSize);
float currentCenterHeight = Vector3.Dot(camera.transform.up,
Vector3.up * (skeleton.BodyCollider.height * .5f)) * pixelsPerUnit;
Assert.That(center.y - anchor.y, Is.EqualTo(currentCenterHeight + 3f).Within(1f),
"Skeleton keeps its 2D radius and raises the fixed 2.5D capsule center by the reviewed three internal pixels.");
}
private static void AssertCapsuleScreenCore(Defense25DComparison world,
Defense25DActor actor, float auditedCoreDiameterPixels)
{
float pixelsPerUnit = world.View.Camera.pixelHeight / (2f * world.View.Camera.orthographicSize);
float actualDiameterPixels = actor.BodyCollider.radius * 2f * pixelsPerUnit;
float expectedDiameterPixels = auditedCoreDiameterPixels * actor.Visual.VisualScale
/ Ppu * pixelsPerUnit;
Assert.That(actualDiameterPixels, Is.EqualTo(expectedDiameterPixels).Within(.5f),
actor.name + " projected capsule diameter must fit inside the literal audited body core height.");
}
private static Defense25DActor Summon(Defense25DComparison world, string name,
Vector2 position, int tier, DefenseTowerBranch branch)
{
GameObject root = new(name);
root.transform.SetParent(world.transform, false);
root.transform.position = Defense25DActor.OnGround(position, world.Settings.GroundY);
Defense25DActor actor = root.AddComponent<Defense25DActor>();
actor.Configure(world, ComparisonActorKind.Summon);
actor.Visual.ConfigureSummonArt(tier, branch);
actor.ResetAt(position);
return actor;
}
}
}