diff --git a/Assets/MagnetismHeroes/Tests/EditMode/VisualUpgradeAssetTests.cs b/Assets/MagnetismHeroes/Tests/EditMode/VisualUpgradeAssetTests.cs index 6801c10..ce83173 100644 --- a/Assets/MagnetismHeroes/Tests/EditMode/VisualUpgradeAssetTests.cs +++ b/Assets/MagnetismHeroes/Tests/EditMode/VisualUpgradeAssetTests.cs @@ -76,7 +76,7 @@ namespace MagnetismHeroes.Tests string name = decoration.prefab.name; bool solid = name == "MeadowClearing" || name == "GentleHill" || name.Contains("Tree") || name.Contains("Limestone") || name == "SlateBoulder" || name == "OpenStoneArch" || name == "MossStoneSteps"; var colliders = decoration.prefab.GetComponentsInChildren(true); - if (solid) Assert.That(colliders, Is.Not.Empty, name + " needs a shape-matched collider."); + if (solid) Assert.That(colliders, Is.Not.Empty, name + " needs a shape-matched collider; runtime disables inaccessible instances, while authoring uses terrain for surface probes."); else Assert.That(colliders, Is.Empty, name + " is small vegetation or unreachable scenery."); foreach (var collider in colliders) { diff --git a/Assets/MagnetismHeroes/Tests/PlayMode/PlaygroundSceneryPlayTests.cs b/Assets/MagnetismHeroes/Tests/PlayMode/PlaygroundSceneryPlayTests.cs index bc87f43..d50f60b 100644 --- a/Assets/MagnetismHeroes/Tests/PlayMode/PlaygroundSceneryPlayTests.cs +++ b/Assets/MagnetismHeroes/Tests/PlayMode/PlaygroundSceneryPlayTests.cs @@ -1,6 +1,7 @@ #if UNITY_EDITOR using System; using System.Collections; +using System.Collections.Generic; using System.IO; using System.Linq; using MagnetismHeroes.Core; @@ -112,6 +113,58 @@ namespace MagnetismHeroes.Tests Capture("world1-ten-single-groups-phone-default", "Validated synthetic Core FreePlay fixture of ten single groups; underlying stage 1 UI remains puzzle. Used for like-for-like Editor resource observation only."); Assert.That(_app.Presenter.Balls.Count, Is.EqualTo(10)); } + [UnityTest] public IEnumerator AllThirtyRuntimeEnvironmentsOnlyBlockWithReachableScenery() + { + var errors = new List(); int distant = 0, reachable = 0, selected = 0; + for (int id = 1; id <= 30; id++) + { + _app.StartStage(id); _app.Voice.Clear(); yield return null; yield return null; Physics.SyncTransforms(); + var playground = _app.Environment.PlaygroundBounds; + foreach (var collider in _app.Environment.GetComponentsInChildren()) + { + var surface = collider.GetComponent().bounds; + bool outside = collider.name != "MeadowClearing" && !playground.Intersects(surface); + if (outside) + { + distant++; + if (collider.enabled) errors.Add($"Stage {id} unreachable {collider.name} still blocks at {surface}."); + } + else + { + reachable++; + Assert.That(collider.enabled && !collider.isTrigger, Is.True, $"Stage {id} reachable {collider.name} must block."); + } + bool hit = false; + foreach (var direction in new[] { Vector3.up, Vector3.down, Vector3.left, Vector3.right, Vector3.forward, Vector3.back }) + { + float distance = surface.extents.magnitude + 1; + hit |= Physics.RaycastAll(surface.center + direction * distance, -direction, distance * 2) + .Any(item => item.collider == collider); + } + if (outside && hit) errors.Add($"Stage {id} background {collider.name} still receives a Physics ray."); + if (!outside) Assert.That(hit, Is.True, $"Stage {id} reachable {collider.name} no longer blocks a Physics ray."); + } + Assert.That(Physics.Raycast(new Vector3(0, .1f, 0), Vector3.down, out var ground, 2), Is.True); + Assert.That(ground.collider.GetComponentInParent(), Is.SameAs(_app.Environment), "Visible play ground must still block."); + Debug.Log($"Stage {id} ground ray y={ground.point.y:R}, aerial bounds min y={playground.min.y:R}, difference={ground.point.y - playground.min.y:R}"); + foreach (var ball in _app.Presenter.Balls.Values) + { + bool found = false; + foreach (float pitch in new[] { 20f, 45f, 70f }) + for (int yaw = 0; yaw < 360 && !found; yaw += 30) + { + View(yaw, pitch, 12); var point = _app.Presenter.Camera.WorldToViewportPoint(ball.transform.position); + if (point.z <= 0 || point.x < .05f || point.x > .95f || point.y < .05f || point.y > .95f) continue; + var ray = _app.Presenter.Camera.ViewportPointToRay(point); + found = Physics.Raycast(ray, out var first, 100) && first.collider == ball.GetComponent(); + } + Assert.That(found, Is.True, $"Stage {id} sphere {ball.BallId} no longer selectable by first hit."); selected++; + } + } + Debug.Log($"Runtime scenery roles: 30 stages, {distant} distant colliders, {reachable} reachable colliders, {selected} selectable spheres."); + Assert.That(distant, Is.GreaterThan(0)); Assert.That(reachable, Is.GreaterThan(0)); + Assert.That(errors, Is.Empty, string.Join("\n", errors)); + } } } #endif diff --git a/Assets/MagnetismHeroes/UnityRuntime/Presentation/BoardEnvironment.cs b/Assets/MagnetismHeroes/UnityRuntime/Presentation/BoardEnvironment.cs index e3f0b75..04cb78b 100644 --- a/Assets/MagnetismHeroes/UnityRuntime/Presentation/BoardEnvironment.cs +++ b/Assets/MagnetismHeroes/UnityRuntime/Presentation/BoardEnvironment.cs @@ -29,6 +29,14 @@ namespace MagnetismHeroes.UnityRuntime.Presentation (corner & 2) == 0 ? bounds.min.y : bounds.max.y, (corner & 4) == 0 ? bounds.min.z : bounds.max.z))); PlaygroundBounds = worldBounds; + Physics.SyncTransforms(); + foreach (Transform prop in transform) + { + // The clearing supports the aerial volume from just below its lower boundary. + if (!prop.gameObject.activeSelf || prop.name == "MeadowClearing(Clone)") continue; + foreach (var collider in prop.GetComponentsInChildren()) + if (!PlaygroundBounds.Intersects(collider.bounds)) collider.enabled = false; + } } } }