402 lines
30 KiB
C#
402 lines
30 KiB
C#
#if UNITY_EDITOR
|
|
using System;
|
|
using System.Collections;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using MagnetismHeroes.Core;
|
|
using MagnetismHeroes.UnityRuntime.Bootstrap;
|
|
using MagnetismHeroes.UnityRuntime.Presentation;
|
|
using NUnit.Framework;
|
|
using UnityEditor;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEngine;
|
|
using UnityEngine.Profiling;
|
|
using UnityEngine.SceneManagement;
|
|
using UnityEngine.TestTools;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace MagnetismHeroes.Tests
|
|
{
|
|
public sealed class VisualUpgradePlayTests
|
|
{
|
|
private sealed class Store : IProgressStore
|
|
{
|
|
public ProgressSave Saved = new ProgressSave { completedStageIds = Enumerable.Range(1, 30).ToArray(), seenTutorialIds = new[] { "merge", "split" } };
|
|
public bool TryLoad(out ProgressSave save, out string error) { save = Saved; error = null; return true; }
|
|
public bool TrySave(ProgressSave save, out string error) { Saved = save; error = null; return true; }
|
|
}
|
|
[Serializable] private sealed class CaptureRecord
|
|
{
|
|
public string name, sourceCommit, sourceDirty, boardOrigin, theme, utc, method;
|
|
public int width, height, stage, balls, groups, equipmentAssemblies, meshInstances, triangles, sharedMaterials;
|
|
public long textureMemory, allocatedMemory;
|
|
public float editorCameraRenderMilliseconds, fieldOfView;
|
|
public Vector3 cameraPosition;
|
|
public Quaternion cameraRotation;
|
|
}
|
|
private GameApp _app;
|
|
private Store _store;
|
|
private string _evidence;
|
|
[UnitySetUp] public IEnumerator SetUp()
|
|
{
|
|
EditorSceneManager.LoadSceneInPlayMode("Assets/MagnetismHeroes/Scenes/Main.unity", new LoadSceneParameters(LoadSceneMode.Single));
|
|
yield return null;
|
|
_app = Object.FindFirstObjectByType<GameApp>();
|
|
Assert.That(_app, Is.Not.Null, "Actual Main scene must initialize its app.");
|
|
_store = new Store(); _app.Initialize(_store);
|
|
_evidence = Environment.GetEnvironmentVariable("MAGNETISM_VISUAL_EVIDENCE") ?? Path.GetFullPath("artifacts/verification/VisualUpgrade/editor-play");
|
|
Directory.CreateDirectory(_evidence);
|
|
yield return null;
|
|
}
|
|
[UnityTearDown] public IEnumerator TearDown() { Object.Destroy(_app.gameObject); yield return null; }
|
|
|
|
private void AssertEquipmentFollowsRepresentatives(BoardRules board)
|
|
{
|
|
_app.Presenter.SendMessage("LateUpdate");
|
|
var badges = _app.Presenter.GetComponentsInChildren<GroupBadge>();
|
|
Assert.That(badges.Length, Is.EqualTo(board.Groups.Length));
|
|
Assert.That(_app.Presenter.Balls.Keys, Is.EquivalentTo(board.Balls.Select(ball => ball.id)));
|
|
Assert.That(board.Groups.SelectMany(group => group.ballIds), Is.EquivalentTo(board.Balls.Select(ball => ball.id)));
|
|
Assert.That(_app.Presenter.GetComponentsInChildren<SpriteRenderer>().Length, Is.EqualTo(board.Groups.Length));
|
|
Assert.That(_app.Presenter.GetComponentsInChildren<TMPro.TextMeshPro>().Select(label => label.text),
|
|
Is.EqualTo(board.Groups.Select(group => group.ballIds.Length.ToString())));
|
|
for (int index = 0; index < badges.Length; index++)
|
|
{
|
|
var gear = badges[index].transform.Find("KnightEquipment");
|
|
Assert.That(gear, Is.Not.Null);
|
|
var representative = board.Groups[index].ballIds.Select(id => _app.Presenter.Balls[id].transform.position)
|
|
.OrderBy(point => Vector3.Dot(point - _app.Presenter.Camera.transform.position, _app.Presenter.Camera.transform.forward)).First();
|
|
Assert.That(Vector3.Distance(gear.position, representative), Is.LessThan(.0001f));
|
|
Assert.That(Quaternion.Angle(gear.rotation, _app.Presenter.Camera.transform.rotation), Is.LessThan(.001f));
|
|
Assert.That(gear.GetComponentsInChildren<Collider>(), Is.Empty);
|
|
Assert.That(badges[index].transform.Find("RibbonCape"), Is.Not.Null);
|
|
foreach (string id in board.Groups[index].ballIds)
|
|
Assert.That(_app.Presenter.Balls[id].GroupId, Is.EqualTo(board.Groups[index].id));
|
|
}
|
|
}
|
|
|
|
private void CaptureViews(string name, string origin)
|
|
{
|
|
var saved = _app.Orbit.Snapshot();
|
|
Capture(name + "-phone-default", origin, 2400, 1080);
|
|
Capture(name + "-tablet-default", origin, 1600, 1000);
|
|
var rotation = Quaternion.Euler(25, 95, 0);
|
|
_app.Orbit.Apply(new CameraState { target = saved.target, distance = 9,
|
|
rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w) });
|
|
Capture(name + "-phone-orbitzoom", origin, 2400, 1080);
|
|
Capture(name + "-tablet-orbitzoom", origin, 1600, 1000);
|
|
_app.Orbit.Apply(saved);
|
|
}
|
|
|
|
[UnityTest] public IEnumerator DistantTerrainHasContinuousApronAcrossItsVisibleFootInsteadOfSkyGaps()
|
|
{
|
|
var report = new System.Collections.Generic.List<string>();
|
|
foreach (int stage in new[] { 1, 11, 21 })
|
|
{
|
|
_app.StartStage(stage); _app.Voice.Clear(); yield return null;
|
|
CaptureViews("horizon-world" + stage, "Actual Main board, unchanged default/orbitzoom cameras; terrain continuity regression");
|
|
var horizon = _app.Environment.GetComponentsInChildren<Transform>().Where(item => item.name == "FarHorizon(Clone)").ToArray();
|
|
var originalPositions = horizon.Select(item => item.position).ToArray();
|
|
var savedCamera = _app.Orbit.Snapshot(); var closeRotation = Quaternion.Euler(25, 95, 0);
|
|
_app.Orbit.Apply(new CameraState { target = savedCamera.target, distance = 9,
|
|
rotation = new Rotation4(closeRotation.x, closeRotation.y, closeRotation.z, closeRotation.w) });
|
|
var camera = _app.Presenter.Camera; float fieldOfView = camera.fieldOfView;
|
|
try
|
|
{
|
|
// Real camera renders of the reproduced (+X, z12) foot gap; no screenshot cropping/editing.
|
|
camera.fieldOfView = 14; camera.transform.LookAt(new Vector3(28, -.45f, 12));
|
|
Capture("horizon-world" + stage + "-foot-current", "Actual Main terrain foot; temporary narrow camera FOV only", 2400, 1080);
|
|
foreach (var far in horizon)
|
|
{
|
|
var direction = new Vector3(far.position.x, 0, far.position.z).normalized;
|
|
far.position = new Vector3(direction.x * 35, far.position.y, direction.z * 35);
|
|
}
|
|
Capture("horizon-world" + stage + "-foot-legacy-radius35", "Same actual Main board, materials and close camera; temporary previous radius35 placement control", 2400, 1080);
|
|
}
|
|
finally
|
|
{
|
|
for (int index = 0; index < horizon.Length; index++) horizon[index].position = originalPositions[index];
|
|
camera.fieldOfView = fieldOfView; _app.Orbit.Apply(savedCamera);
|
|
}
|
|
string board = JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Orbit.Snapshot()));
|
|
var collisionBefore = _app.GetComponentsInChildren<Collider>();
|
|
var probeScene = SceneManager.CreateScene("TerrainGeometryProbe", new CreateSceneParameters(LocalPhysicsMode.Physics3D));
|
|
var probes = new System.Collections.Generic.List<MeshCollider>();
|
|
try
|
|
{
|
|
var terrain = _app.Environment.GetComponentsInChildren<Transform>().Where(item =>
|
|
item.name == "MeadowClearing(Clone)" || item.name == "GentleHill(Clone)" ||
|
|
item.name == "DistantRidge(Clone)" || item.name == "FarHorizon(Clone)").ToArray();
|
|
foreach (var surface in terrain.SelectMany(item => item.GetComponentsInChildren<MeshFilter>()))
|
|
{
|
|
var probe = new GameObject("GeometryOnly-" + surface.name);
|
|
SceneManager.MoveGameObjectToScene(probe, probeScene);
|
|
probe.transform.SetPositionAndRotation(surface.transform.position, surface.transform.rotation);
|
|
probe.transform.localScale = surface.transform.lossyScale;
|
|
probes.Add(probe.AddComponent<MeshCollider>()); probes.Last().sharedMesh = surface.sharedMesh;
|
|
report.Add($"stage={stage}; terrain={surface.name}; actualRendererBounds={surface.GetComponent<Renderer>().bounds}; meshBounds={surface.sharedMesh.bounds}; rootPosition={surface.transform.position}");
|
|
}
|
|
probeScene.GetPhysicsScene().Simulate(.001f);
|
|
int missing = 0;
|
|
foreach (var far in terrain.Where(item => item.name == "FarHorizon(Clone)"))
|
|
{
|
|
var direction = new Vector3(far.position.x, 0, far.position.z).normalized;
|
|
var right = Vector3.Cross(Vector3.up, direction);
|
|
float distance = new Vector2(far.position.x, far.position.z).magnitude;
|
|
// Traverse the apron to the far mound crown across its central 24m foot.
|
|
// A positive gap is real missing geometry, independent of lighting/material colors.
|
|
for (float lateral = -12; lateral <= 12; lateral += 2)
|
|
for (float radial = 0; radial <= distance; radial += .25f)
|
|
{
|
|
var point = direction * radial + right * lateral;
|
|
var ray = new Ray(point + Vector3.up * 20, Vector3.down);
|
|
if (probes.Any(probe => probe.Raycast(ray, out _, 40))) continue;
|
|
missing++;
|
|
if (missing <= 24) report.Add($"stage={stage}; unsupportedApron={point}; farCrown={far.position}");
|
|
}
|
|
}
|
|
report.Add($"stage={stage}; missingApronSamples={missing}; probe=separate local PhysicsScene using actual shared mesh geometry, no product collider edits");
|
|
File.WriteAllLines(Path.Combine(_evidence, "horizon-geometry.txt"), report);
|
|
Assert.That(missing, Is.Zero, "Far terrain foot must join the actual clearing/hills/ridge without open geometry gaps.");
|
|
}
|
|
finally { SceneManager.UnloadSceneAsync(probeScene); }
|
|
yield return null;
|
|
Assert.That(_app.GetComponentsInChildren<Collider>(), Is.EqualTo(collisionBefore));
|
|
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Orbit.Snapshot())), Is.EqualTo(board));
|
|
}
|
|
}
|
|
|
|
[UnityTest] public IEnumerator FiveBallMergeDisconnectedSplitAndZeroResupplyKeepActualIdsAcrossAllWorlds()
|
|
{
|
|
foreach (int stage in new[] { 9, 11, 21 })
|
|
{
|
|
// Boundary fixture: a completed-stage store with a validated five-ball free board,
|
|
// not evidence of normal puzzle progression. All operations below use the real app/session.
|
|
var balls = Enumerable.Range(0, 5).Select(index => new BallState { id = "five-" + index,
|
|
position = new Position3(index - 2, .8f, 0) }).ToArray();
|
|
_store.Saved.latestFreePlay = new FreePlayState { sourceStageId = stage,
|
|
camera = _app.Orbit.Snapshot(), balls = balls,
|
|
groups = new[] { new GroupState { id = "two", ballIds = balls.Take(2).Select(ball => ball.id).ToArray() },
|
|
new GroupState { id = "three", ballIds = balls.Skip(2).Select(ball => ball.id).ToArray() } } };
|
|
_app.Initialize(_store); _app.ContinueFreePlay(); _app.Voice.Clear(); yield return null;
|
|
var ids = _app.Session.Board.Balls.Select(ball => ball.id).ToArray();
|
|
AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
|
CaptureViews("world" + stage + "-two-plus-three", "Validated five-ball 2+3 free-play store fixture; actual Main app/session");
|
|
var before = JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot()));
|
|
_app.Presenter.PreviewGroup("two", new Vector3(.3f, .2f, 0));
|
|
AssertEquipmentFollowsRepresentatives(_app.Session.Board); _app.Presenter.CancelPreview();
|
|
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot())), Is.EqualTo(before));
|
|
_app.CommitMove("two", new Position3(.1f, 0, 0), "three"); yield return null;
|
|
Assert.That(_app.Session.Board.Groups.Single().ballIds.Length, Is.EqualTo(5));
|
|
Assert.That(_app.Session.Board.Balls.Select(ball => ball.id), Is.EquivalentTo(ids));
|
|
AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
|
CaptureViews("world" + stage + "-merged-five", "Actual app/session 2+3 merge from labeled boundary fixture");
|
|
var selected = new[] { ids[0], ids[4] };
|
|
var points = selected.Select(id => BoardPresenter.ToVector(_app.Session.Board.Balls.Single(ball => ball.id == id).position)).ToArray();
|
|
Assert.That(Vector3.Distance(points[0], points[1]), Is.GreaterThan(2f), "Selected endpoints are disconnected.");
|
|
var merged = _app.Session.Board.Groups.Single();
|
|
before = JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot()));
|
|
Assert.That(_app.Presenter.PreviewSplit(merged.id, selected, new Vector3(0, 1.5f, 0)), Is.True);
|
|
AssertEquipmentFollowsRepresentatives(_app.Session.Board); _app.Presenter.CancelPreview();
|
|
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot())), Is.EqualTo(before));
|
|
Assert.That(_app.CommitSplit(merged.id, selected, new Position3(0, 1.5f, 0)), Is.True); yield return null;
|
|
Assert.That(_app.Session.Board.Groups.Select(group => group.ballIds.Length), Is.EquivalentTo(new[] { 2, 3 }));
|
|
Assert.That(_app.Session.Board.Groups.Single(group => group.ballIds.Length == 2).ballIds, Is.EquivalentTo(selected));
|
|
Assert.That(_app.Session.Board.Balls.Select(ball => ball.id), Is.EquivalentTo(ids));
|
|
AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
|
CaptureViews("world" + stage + "-disconnected-split-two-three", "Actual app/session split of disconnected five-group endpoints");
|
|
before = JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot()));
|
|
_app.ShowHome(); yield return null;
|
|
Assert.That(_app.Presenter.GetComponentsInChildren<GroupBadge>(), Is.Empty);
|
|
_app.ContinueFreePlay(); yield return null;
|
|
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot())), Is.EqualTo(before));
|
|
AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
|
foreach (var group in _app.Session.Board.Groups) Assert.That(_app.Session.ReturnGroup(group.id).Accepted, Is.True);
|
|
_app.ContinueFreePlay(); yield return null;
|
|
Assert.That(_app.Session.Board.BallCount, Is.Zero); AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
|
CaptureViews("world" + stage + "-zero", "Actual app/session return of both groups; empty free play");
|
|
_app.SpawnFriend(); yield return null;
|
|
Assert.That(_app.Session.Board.BallCount, Is.EqualTo(1)); AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
|
CaptureViews("world" + stage + "-single-resupplied", "Actual app/session resupply after empty board");
|
|
}
|
|
}
|
|
|
|
[UnityTest] public IEnumerator BelowGroundComparisonKeepsOriginalPhysicalOcclusionAndActualBoardUnchanged()
|
|
{
|
|
_app.StartStage(21); _app.Voice.Clear(); yield return null;
|
|
var rotation = Quaternion.Euler(-45, 210, 0);
|
|
_app.Orbit.Apply(new CameraState { target = _app.Orbit.Snapshot().target, distance = 15,
|
|
rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w) });
|
|
Physics.SyncTransforms();
|
|
var camera = _app.Presenter.Camera;
|
|
var floor = _app.transform.Find("Playground/PlaygroundIsland");
|
|
var originalRenderer = floor.GetComponent<MeshRenderer>(); var originalCollider = floor.GetComponent<Collider>();
|
|
var clearing = _app.Environment.GetComponentsInChildren<Transform>().Single(item => item.name == "MeadowClearing(Clone)");
|
|
var groundRenderers = clearing.GetComponentsInChildren<MeshRenderer>();
|
|
var board = JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Orbit.Snapshot()));
|
|
var proof = new System.Collections.Generic.List<string>();
|
|
var rays = _app.Session.Board.Balls.Select(ball => new Ray(camera.transform.position,
|
|
(BoardPresenter.ToVector(ball.position) - camera.transform.position).normalized)).ToArray();
|
|
var priorHits = rays.Select(ray => { Assert.That(Physics.Raycast(ray, out var hit, 100), Is.True); return hit; }).ToArray();
|
|
Capture("belowground-approved-clearing", "Actual stage21 board/camera; approved ground rendered, unchanged original collider");
|
|
try
|
|
{
|
|
foreach (var renderer in groundRenderers) renderer.enabled = false;
|
|
originalRenderer.enabled = true;
|
|
Capture("belowground-original-island", "Same actual board/camera/colliders; temporary original floor renderer instead of clearing");
|
|
for (int index = 0; index < rays.Length; index++)
|
|
{
|
|
Assert.That(Physics.Raycast(rays[index], out var hit, 100), Is.True);
|
|
Assert.That(hit.collider, Is.SameAs(priorHits[index].collider));
|
|
Assert.That(hit.distance, Is.EqualTo(priorHits[index].distance).Within(.0001f));
|
|
Assert.That(originalCollider.Raycast(rays[index], out var floorHit, 100), Is.True,
|
|
"Original floor physically occludes this below-ground board ray.");
|
|
var sphere = _app.Presenter.Balls[_app.Session.Board.Balls[index].id].GetComponent<SphereCollider>();
|
|
Assert.That(sphere.Raycast(rays[index], out var sphereHit, 100), Is.True);
|
|
Assert.That(floorHit.distance, Is.LessThan(sphereHit.distance));
|
|
proof.Add($"id={_app.Session.Board.Balls[index].id}; firstHit={hit.collider.name}; floorDepth={floorHit.distance}; sphereDepth={sphereHit.distance}; originalBounds={originalCollider.bounds}; clearingBounds={groundRenderers[0].bounds}");
|
|
}
|
|
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Orbit.Snapshot())), Is.EqualTo(board));
|
|
File.WriteAllLines(Path.Combine(_evidence, "belowground-raycast-comparison.txt"), proof);
|
|
}
|
|
finally { foreach (var renderer in groundRenderers) renderer.enabled = true; originalRenderer.enabled = false; }
|
|
}
|
|
|
|
[UnityTest] public IEnumerator AllWorldsProduceActualSoftShadowPixelsWithUnchangedBoardAndMaterials()
|
|
{
|
|
var sun = _app.transform.Find("Sun").GetComponent<Light>();
|
|
_app.ShowHome(); yield return null;
|
|
Capture("home-cyan-readiness", "Actual Main home UI for Android sky readiness predicate", 2400, 1080);
|
|
foreach (int stage in new[] { 1, 11, 21 })
|
|
{
|
|
_app.StartStage(stage); _app.Voice.Clear(); yield return null;
|
|
string before = JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Orbit.Snapshot()));
|
|
Capture("world" + stage + "-soft-shadow", "Actual Main daylight with soft shadows");
|
|
try { sun.shadows = LightShadows.None; Capture("world" + stage + "-shadow-off", "Same board/camera/materials; light shadow toggle comparison only"); }
|
|
finally { sun.shadows = LightShadows.Soft; }
|
|
var on = new Texture2D(2, 2); var off = new Texture2D(2, 2);
|
|
try
|
|
{
|
|
on.LoadImage(File.ReadAllBytes(Path.Combine(_evidence, "world" + stage + "-soft-shadow.png")));
|
|
off.LoadImage(File.ReadAllBytes(Path.Combine(_evidence, "world" + stage + "-shadow-off.png")));
|
|
var a = on.GetPixels32(); var b = off.GetPixels32(); int darker = 0;
|
|
// The central lower ground excludes the board, badge labels, scenery crowns and HUD.
|
|
for (int y = 140; y < 300; y++) for (int x = 350; x < 1000; x++)
|
|
{
|
|
int index = y * on.width + x;
|
|
if (a[index].r + a[index].g + a[index].b + 12 < b[index].r + b[index].g + b[index].b) darker++;
|
|
}
|
|
File.WriteAllText(Path.Combine(_evidence, "world" + stage + "-shadow-pixels.txt"), $"same-board/camera/materials; shadow-on darker ground ROI pixels={darker}; groundROI=350,140,650,160; viewport=1280x720; Editor RT only");
|
|
Assert.That(darker, Is.GreaterThan(100), "Actual rendered soft shadows must darken a meaningful ground area.");
|
|
}
|
|
finally { Object.Destroy(on); Object.Destroy(off); }
|
|
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Orbit.Snapshot())), Is.EqualTo(before));
|
|
}
|
|
}
|
|
[UnityTest] public IEnumerator ActualMainShowsThreeThemesAndEquipmentFollowsPreviewCancelMergeSplitAndReentry()
|
|
{
|
|
foreach (int stage in new[] { 1, 11, 21 })
|
|
{
|
|
_app.StartStage(stage); _app.Voice.Clear(); yield return null;
|
|
AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
|
Capture("stage" + stage.ToString("D2"), "Actual Main GameApp.Session.Board; unlocked-stage fixture store");
|
|
}
|
|
var normalView = _app.Orbit.Snapshot();
|
|
foreach (var view in new[] { (name: "orbit95-pitch45-distance6", yaw: 95f, pitch: 45f, distance: 6f),
|
|
(name: "orbit210-pitch-45-distance15", yaw: 210f, pitch: -45f, distance: 15f) })
|
|
{
|
|
var rotation = Quaternion.Euler(view.pitch, view.yaw, 0);
|
|
_app.Orbit.Apply(new CameraState { target = normalView.target, distance = view.distance,
|
|
rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w) });
|
|
yield return null; AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
|
Capture(view.name, "Actual Main session; camera-only orbit/pitch/distance exploration fixture");
|
|
}
|
|
_app.Orbit.Apply(normalView);
|
|
_app.StartStage(6); _app.Voice.Clear(); yield return null;
|
|
var original = _app.Session.Board.Balls.Select(ball => ball.position).ToArray();
|
|
var groups = _app.Session.Board.Groups;
|
|
_app.Presenter.PreviewGroup(groups[0].id, new Vector3(.3f, .2f, 0));
|
|
AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
|
_app.Presenter.CancelPreview();
|
|
AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
|
for (int index = 0; index < original.Length; index++)
|
|
Assert.That(_app.Presenter.Balls[_app.Session.Board.Balls[index].id].transform.position, Is.EqualTo(BoardPresenter.ToVector(original[index])));
|
|
_app.Presenter.ShowSelection(new[] { groups[0].ballIds[0] }); yield return null;
|
|
Capture("selected-two-groups", "Actual Main session, visual selection only");
|
|
_app.CommitMove(groups[0].id, new Position3(.1f, 0, 0), groups[1].id); yield return null;
|
|
Assert.That(_app.Session.Board.Groups.Length, Is.EqualTo(1));
|
|
AssertEquipmentFollowsRepresentatives(_app.Session.Board); Capture("merged-single-group", "Actual Main session merge -> free play");
|
|
var merged = _app.Session.Board.Groups[0];
|
|
Assert.That(_app.CommitSplit(merged.id, new[] { merged.ballIds[0] }, new Position3(1.5f, 0, 0)), Is.True); yield return null;
|
|
Assert.That(_app.Session.Board.Groups.Length, Is.EqualTo(2)); AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
|
_app.ShowHome(); _app.ContinueFreePlay(); yield return null;
|
|
Assert.That(_app.Session.Board.BallCount, Is.EqualTo(2));
|
|
Assert.That(_app.Environment.Theme, Is.EqualTo("Garden")); AssertEquipmentFollowsRepresentatives(_app.Session.Board);
|
|
Capture("split-reentered-two-groups", "Actual Main session free play split and home/reentry");
|
|
}
|
|
[UnityTest] public IEnumerator TenSingleGroupsAndOneTenBallGroupRenderActualImportedResourceCounts()
|
|
{
|
|
var balls = Enumerable.Range(0, 10).Select(index => new BallState { id = "render-" + index,
|
|
position = new Position3((index % 5 - 2) * 1.6f, .8f + index / 5 * 1.8f, 0) }).ToArray();
|
|
foreach (int stage in new[] { 1, 11, 21 })
|
|
{
|
|
_app.StartStage(stage); _app.Voice.Clear(); yield return null;
|
|
foreach (bool singles in new[] { true, false })
|
|
{
|
|
var groups = singles ? balls.Select(ball => new GroupState { id = ball.id, ballIds = new[] { ball.id } }).ToArray()
|
|
: new[] { new GroupState { id = "ten", ballIds = balls.Select(ball => ball.id).ToArray() } };
|
|
var board = BoardRules.CreateFreePlay(new FreePlayState { sourceStageId = stage, balls = balls, groups = groups });
|
|
_app.Presenter.Render(board); yield return null;
|
|
AssertEquipmentFollowsRepresentatives(board);
|
|
CaptureViews("world" + stage + (singles ? "-ten-single-groups" : "-ten-one-group"), "Validated synthetic Core FreePlay render fixture; session UI remains underlying stage " + stage);
|
|
}
|
|
}
|
|
}
|
|
private void Capture(string name, string origin, int width = 1280, int height = 720)
|
|
{
|
|
var camera = _app.Presenter.Camera; var canvas = _app.GetComponentInChildren<Canvas>();
|
|
var oldTarget = camera.targetTexture; var oldActive = RenderTexture.active; var oldMode = canvas.renderMode;
|
|
var oldCamera = canvas.worldCamera; float oldPlane = canvas.planeDistance, oldAspect = camera.aspect;
|
|
var target = new RenderTexture(width, height, 24); var image = new Texture2D(width, height, TextureFormat.RGB24, false);
|
|
try
|
|
{
|
|
camera.targetTexture = target; camera.aspect = (float)width / height;
|
|
canvas.renderMode = RenderMode.ScreenSpaceCamera; canvas.worldCamera = camera; canvas.planeDistance = .5f;
|
|
foreach (var ball in _app.Presenter.Balls.Values) ball.SendMessage("Update");
|
|
if (_app.Presenter.gameObject.activeInHierarchy) _app.Presenter.SendMessage("LateUpdate");
|
|
Canvas.ForceUpdateCanvases();
|
|
var timer = System.Diagnostics.Stopwatch.StartNew(); camera.Render(); timer.Stop();
|
|
RenderTexture.active = target; image.ReadPixels(new Rect(0, 0, width, height), 0, 0); image.Apply();
|
|
File.WriteAllBytes(Path.Combine(_evidence, name + ".png"), image.EncodeToPNG());
|
|
var meshes = _app.GetComponentsInChildren<MeshFilter>();
|
|
var badges = _app.Presenter.GetComponentsInChildren<GroupBadge>();
|
|
var record = new CaptureRecord { name = name, sourceCommit = Environment.GetEnvironmentVariable("MAGNETISM_PROOF_COMMIT"),
|
|
sourceDirty = Environment.GetEnvironmentVariable("MAGNETISM_PROOF_DIRTY"), utc = DateTime.UtcNow.ToString("o"),
|
|
boardOrigin = origin, theme = _app.Environment.Theme, stage = _app.Session.Board?.StageId ?? 0,
|
|
width = width, height = height, balls = _app.Presenter.Balls.Count, groups = badges.Length,
|
|
equipmentAssemblies = badges.Count(item => item.transform.Find("KnightEquipment") != null), meshInstances = meshes.Length,
|
|
triangles = meshes.Sum(item => Enumerable.Range(0, item.sharedMesh.subMeshCount).Sum(submesh => (int)item.sharedMesh.GetIndexCount(submesh) / 3)),
|
|
sharedMaterials = _app.GetComponentsInChildren<Renderer>().SelectMany(item => item.sharedMaterials).Where(item => item != null).Distinct().Count(),
|
|
editorCameraRenderMilliseconds = (float)timer.Elapsed.TotalMilliseconds,
|
|
fieldOfView = camera.fieldOfView,
|
|
textureMemory = Resources.FindObjectsOfTypeAll<Texture>().Sum(item => Profiler.GetRuntimeMemorySizeLong(item)),
|
|
allocatedMemory = Profiler.GetTotalAllocatedMemoryLong(), cameraPosition = camera.transform.position, cameraRotation = camera.transform.rotation,
|
|
method = "Actual Main scene PlayMode Camera.Render to RT, ScreenSpaceCamera canvas; simulated viewport, not Android capture. Triangles are resident index counts for active mesh instances, including the retained floor with its Renderer disabled; they are not submitted GPU triangles. Camera.Render elapsed is Editor CPU wall time, not GPU time or device FPS. Texture memory includes all Editor-loaded textures." };
|
|
File.WriteAllText(Path.Combine(_evidence, name + ".json"), JsonUtility.ToJson(record, true));
|
|
if (name == "world21-ten-single-groups-phone-default")
|
|
File.WriteAllLines(Path.Combine(_evidence, "editor-loaded-textures.txt"), Resources.FindObjectsOfTypeAll<Texture>()
|
|
.Select(item => $"{AssetDatabase.GetAssetPath(item)};name={item.name};dimensions={item.width}x{item.height};format={(item is Texture2D texture ? texture.format.ToString() : item.GetType().Name)};runtimeBytes={Profiler.GetRuntimeMemorySizeLong(item)}"));
|
|
}
|
|
finally
|
|
{
|
|
camera.targetTexture = oldTarget; camera.aspect = oldAspect; RenderTexture.active = oldActive;
|
|
canvas.renderMode = oldMode; canvas.worldCamera = oldCamera; canvas.planeDistance = oldPlane;
|
|
Object.DestroyImmediate(image); target.Release(); Object.DestroyImmediate(target);
|
|
if (_app.Presenter.gameObject.activeInHierarchy) _app.Presenter.SendMessage("LateUpdate");
|
|
Canvas.ForceUpdateCanvases();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|