171 lines
12 KiB
C#
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
|