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(); 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(); actor.Configure(world, ComparisonActorKind.Summon); actor.Visual.ConfigureSummonArt(tier, branch); actor.ResetAt(position); return actor; } } }