Files
MagnetismHeroes/Assets/MagnetismHeroes/Tests/PlayMode/PlaygroundSceneryPlayTests.cs
T

171 lines
12 KiB
C#

#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using MagnetismHeroes.Core;
using MagnetismHeroes.UnityRuntime.Bootstrap;
using MagnetismHeroes.UnityRuntime.Presentation;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.TestTools;
using Object = UnityEngine.Object;
namespace MagnetismHeroes.Tests
{
public sealed class PlaygroundSceneryPlayTests
{
private sealed class Store : IProgressStore
{
public bool TryLoad(out ProgressSave save, out string error)
{ save = new ProgressSave { completedStageIds = Enumerable.Range(1, 30).ToArray(), seenTutorialIds = new[] { "merge", "split" } }; error = null; return true; }
public bool TrySave(ProgressSave save, out string error) { error = null; return true; }
}
private GameApp _app;
[UnitySetUp] public IEnumerator SetUp()
{
foreach (var listener in Object.FindObjectsByType<AudioListener>()) listener.enabled = false;
foreach (var camera in Object.FindObjectsByType<Camera>()) camera.enabled = false;
_app = Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GameApp.prefab")).GetComponent<GameApp>();
_app.Initialize(new Store()); yield return null;
}
[UnityTearDown] public IEnumerator TearDown() { Object.Destroy(_app.gameObject); yield return null; }
[Serializable] private sealed class CaptureRecord
{
public string utc, sourceCommit, boardOrigin, method, theme;
public bool sourceDirty;
public int stage, balls, groups, meshInstances, triangles, sharedMaterials, width = 2400, height = 1080;
public long textureMemory, allocatedMemory;
public float editorCameraRenderMilliseconds, fieldOfView;
public Vector3 cameraPosition; public Quaternion cameraRotation; public Bounds playgroundBounds;
}
private void View(float yaw, float pitch, float distance)
{
var rotation = Quaternion.Euler(pitch, yaw, 0);
_app.Orbit.Apply(new CameraState { target = new Position3(0, .6f, 0), distance = distance,
rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w) });
}
private void Capture(string name, string origin)
{
string folder = Environment.GetEnvironmentVariable("MAGNETISM_PLAYGROUND_EVIDENCE") ?? Path.GetFullPath("artifacts/verification/PlaygroundInteraction/T1");
Directory.CreateDirectory(folder); var camera = _app.Presenter.Camera; var canvas = _app.GetComponentInChildren<Canvas>();
var previousTexture = camera.targetTexture; var previousActive = RenderTexture.active; var previousMode = canvas.renderMode;
var previousCamera = canvas.worldCamera; float previousPlane = canvas.planeDistance, previousAspect = camera.aspect;
var texture = RenderTexture.GetTemporary(2400, 1080, 24); var image = new Texture2D(2400, 1080, TextureFormat.RGB24, false);
try
{
camera.targetTexture = texture; camera.aspect = 2400f / 1080; canvas.renderMode = RenderMode.ScreenSpaceCamera; canvas.worldCamera = camera; canvas.planeDistance = .5f;
Canvas.ForceUpdateCanvases(); camera.Render(); camera.Render();
var timer = System.Diagnostics.Stopwatch.StartNew(); camera.Render(); timer.Stop(); RenderTexture.active = texture;
image.ReadPixels(new Rect(0, 0, 2400, 1080), 0, 0); image.Apply(); File.WriteAllBytes(Path.Combine(folder, name + ".png"), image.EncodeToPNG());
var meshes = _app.GetComponentsInChildren<MeshFilter>();
var record = new CaptureRecord { utc = DateTime.UtcNow.ToString("o"), sourceCommit = Environment.GetEnvironmentVariable("MAGNETISM_PROOF_COMMIT"),
boardOrigin = origin, method = "Actual GameApp prefab in PlayMode Camera.Render to 2400x1080 RT with temporary ScreenSpaceCamera canvas. Mesh index counts include active hidden legacy floor; not submitted GPU triangles. Texture memory is all Editor-loaded textures; Camera.Render is warmed Editor CPU wall time, not Android FPS.",
sourceDirty = true, stage = _app.Session.Board.StageId, theme = _app.Environment.Theme, balls = _app.Presenter.Balls.Count, groups = _app.GetComponentsInChildren<GroupBadge>().Length,
meshInstances = meshes.Length, triangles = meshes.Sum(mesh => Enumerable.Range(0, mesh.sharedMesh.subMeshCount).Sum(index => (int)mesh.sharedMesh.GetIndexCount(index)) / 3),
sharedMaterials = _app.GetComponentsInChildren<MeshRenderer>().SelectMany(renderer => renderer.sharedMaterials).Distinct().Count(),
textureMemory = Resources.FindObjectsOfTypeAll<Texture>().Sum(item => Profiler.GetRuntimeMemorySizeLong(item)), allocatedMemory = Profiler.GetTotalAllocatedMemoryLong(),
editorCameraRenderMilliseconds = (float)timer.Elapsed.TotalMilliseconds, fieldOfView = camera.fieldOfView, cameraPosition = camera.transform.position, cameraRotation = camera.transform.rotation,
playgroundBounds = _app.Environment.PlaygroundBounds };
File.WriteAllText(Path.Combine(folder, name + ".json"), JsonUtility.ToJson(record, true));
}
finally
{
camera.targetTexture = previousTexture; camera.aspect = previousAspect; RenderTexture.active = previousActive; canvas.renderMode = previousMode; canvas.worldCamera = previousCamera; canvas.planeDistance = previousPlane;
Object.Destroy(image); RenderTexture.ReleaseTemporary(texture); Canvas.ForceUpdateCanvases();
}
}
[UnityTest] public IEnumerator SixLandscapesRenderAndAllThirtyOriginalBoardsRemainSelectable()
{
for (int id = 1; id <= 30; id++)
{
_app.StartStage(id); _app.Voice.Clear(); yield return null; yield return null; Physics.SyncTransforms();
Assert.That(_app.Environment.PlaygroundBounds.size.sqrMagnitude, Is.GreaterThan(0));
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 hit, 100) && hit.collider == ball.GetComponent<SphereCollider>();
}
Assert.That(found, Is.True, $"Stage {id} original sphere {ball.BallId} cannot be reached by a supported above-ground view.");
}
if ((id - 1) % 5 == 0)
{
View(0, 0, 8); yield return null; Capture($"stage{id:D2}-default", "Actual original puzzle board; seeded completion IDs used only to unlock stage access.");
View(30, 25, 12); yield return null; Capture($"stage{id:D2}-overview", "Actual original puzzle board; seeded completion IDs used only to unlock stage access.");
View(150, 12, 15); yield return null; Capture($"stage{id:D2}-horizon", "Actual original puzzle board; seeded completion IDs used only to unlock stage access.");
View(270, 45, 6); yield return null; Capture($"stage{id:D2}-near-ground", "Actual original puzzle board; seeded completion IDs used only to unlock stage access.");
}
}
_app.StartStage(1); _app.Voice.Clear(); yield return null; yield return null;
var balls = Enumerable.Range(0, 10).Select(i => new BallState { id = "capture-" + i, position = new Position3(i % 5 - 2, .8f + i / 5 * 1.8f, 0) }).ToArray();
var board = BoardRules.CreateFreePlay(new FreePlayState { sourceStageId = 1, balls = balls, groups = balls.Select(ball => new GroupState { id = ball.id, ballIds = new[] { ball.id } }).ToArray() });
_app.Presenter.Render(board); View(0, 0, 8); yield return null; yield return null;
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<string>(); 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<MeshCollider>())
{
var surface = collider.GetComponent<MeshRenderer>().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<BoardEnvironment>(), 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<SphereCollider>();
}
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