diff --git a/Assets/MagnetismHeroes/Tests/PlayMode/VisualUpgradePlayTests.cs b/Assets/MagnetismHeroes/Tests/PlayMode/VisualUpgradePlayTests.cs index a85d1ea..9fcdf8e 100644 --- a/Assets/MagnetismHeroes/Tests/PlayMode/VisualUpgradePlayTests.cs +++ b/Assets/MagnetismHeroes/Tests/PlayMode/VisualUpgradePlayTests.cs @@ -61,7 +61,8 @@ namespace MagnetismHeroes.Tests { public string name, ballId, kind = "boundary"; public FreePlayState source, expected; - public Vector3 requested; + public Vector3 requested, grabOffset, pointerDelta; + public string firstHitId; public Vector2 start, end; } private Store _store; @@ -139,6 +140,60 @@ namespace MagnetismHeroes.Tests } [UnityTearDown] public IEnumerator TearDown() { Object.Destroy(_app.gameObject); yield return null; } + private Vector2 NativePoint(Vector3 world) + { + var viewport = _app.Orbit.Camera.WorldToViewportPoint(world); + return new Vector2((float)Math.Round(viewport.x * 2400), (float)Math.Round((1 - viewport.y) * 1080)); + } + private Vector2 PlayerPoint(Vector2 native) => _app.Orbit.Camera.ViewportToScreenPoint(new Vector3(native.x / 2400, 1 - native.y / 1080)); + private NativeDrag ExerciseNativeDrag(string name, string kind, FreePlayState source, Vector3 requested) + { + var id = source.balls[0].id; var origin = BoardPresenter.ToVector(source.balls[0].position); + Assert.That(JsonUtility.ToJson(_app.Session.Camera), Is.EqualTo(JsonUtility.ToJson(source.camera)), "Exported camera must be the actual confirmed manual pose."); + Assert.That(JsonUtility.ToJson(_app.Orbit.Snapshot()), Is.EqualTo(JsonUtility.ToJson(source.camera))); + Assert.That(JsonUtility.ToJson(_app.Session.Board.ToFreePlayState(_app.Session.Camera)), Is.EqualTo(JsonUtility.ToJson(source)), "Entry must preserve every fixture ID, position and group."); + var start = NativePoint(origin); Physics.SyncTransforms(); + var ray = _app.Orbit.Camera.ScreenPointToRay(PlayerPoint(start)); + var nativeRay = _app.Orbit.Camera.ViewportPointToRay(new Vector3(start.x / 2400, 1 - start.y / 1080)); + Assert.That(Vector3.Distance(ray.origin, nativeRay.origin), Is.LessThan(.00001f)); + Assert.That(Vector3.Distance(ray.direction, nativeRay.direction), Is.LessThan(.00001f), "Editor pixelRect must preserve the same native viewport ray."); + Assert.That(Physics.Raycast(ray, out var hit, 100), Is.True); + Assert.That(hit.collider.TryGetComponent(out var ball) ? ball.BallId : hit.collider.name, Is.EqualTo(id), name + " actual first hit must select the requested sphere."); + var grabOffset = hit.point - origin; + var end = NativePoint(origin + requested + grabOffset); + foreach (var point in new[] { start, end }) + Assert.That(point.x >= 530 && point.x <= 1850 && point.y >= 280 && point.y <= 750, Is.True, name + " integer native pointer must stay in the phone world UI-safe region."); + var plane = new Plane(_app.Orbit.Camera.transform.forward, hit.point); + var endRay = _app.Orbit.Camera.ScreenPointToRay(PlayerPoint(end)); + Assert.That(plane.Raycast(ray, out var from), Is.True); Assert.That(plane.Raycast(endRay, out var to), Is.True); + var pointerDelta = endRay.GetPoint(to) - ray.GetPoint(from); + var saved = JsonUtility.ToJson(_store.Saved); + _app.Input.Begin(PlayerPoint(start), false); _app.Input.Move(PlayerPoint(end)); + Assert.That(_app.Orbit.IsAssisting, Is.True, "Actual BoardInput must acquire and move this friend."); + Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved), "Preview and assistance cannot save intermediate state."); + var actual = _app.Presenter.DropDelta; var merge = _app.Presenter.MergeCandidate; + if (kind == "boundary") Assert.That(actual.magnitude, Is.InRange(1.19f, 1.21f), "Actual pointer must stop at the .5 sphere margin."); + if (kind == "foliage-pass") + { + Assert.That(Vector3.Distance(actual, pointerDelta), Is.LessThan(.001f), "The entire actual integer-pointer movement through foliage must pass without clipping."); + Assert.That(actual.x, Is.GreaterThan(3.9f)); + } + if (kind == "thin-obstacle") { Assert.That(actual.x, Is.GreaterThan(.1f).And.LessThan(requested.x - 1)); Assert.That(merge, Is.Null); } + if (kind == "two-plus-three") Assert.That(merge, Is.EqualTo("native-contact-target")); + var preview = _app.Presenter.Balls.ToDictionary(pair => pair.Key, pair => pair.Value.transform.position); + var members = source.groups.Single(group => group.ballIds.Contains(id)).ballIds; + foreach (var value in source.balls) + Assert.That(Vector3.Distance(preview[value.id], BoardPresenter.ToVector(value.position) + (members.Contains(value.id) ? actual : Vector3.zero)), Is.LessThan(.001f), "Only the whole grabbed group may translate; all relative offsets and other friends are preserved."); + _app.Input.End(PlayerPoint(end)); + Assert.That(_app.Session.Board.Balls.Select(value => value.id), Is.EquivalentTo(source.balls.Select(value => value.id))); + foreach (var value in _app.Session.Board.Balls) Assert.That(Vector3.Distance(BoardPresenter.ToVector(value.position), preview[value.id]), Is.LessThan(.001f), "Drop must equal the actual input preview for every ID."); + Assert.That(JsonUtility.ToJson(_app.Session.Camera), Is.EqualTo(JsonUtility.ToJson(source.camera))); + if (kind != "two-plus-three") Assert.That(_app.Session.Board.Groups.Select(group => JsonUtility.ToJson(group)), Is.EqualTo(source.groups.Select(group => JsonUtility.ToJson(group)))); + else Assert.That(_app.Session.Board.Groups.Single().ballIds, Is.EquivalentTo(source.balls.Select(value => value.id))); + return new NativeDrag { name = name, kind = kind, ballId = id, source = source, requested = requested, start = start, end = end, + grabOffset = grabOffset, pointerDelta = pointerDelta, firstHitId = ball.BallId, expected = _app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot()) }; + } + [UnityTest] public IEnumerator ExportActualMainPlaygroundNativeBoundaryFixtures() { var stages = new System.Collections.Generic.List(); @@ -233,13 +288,8 @@ namespace MagnetismHeroes.Tests _store.Saved.latestFreePlay = source; _app.Initialize(_store); _app.ContinueFreePlay(); yield return null; _app.Orbit.Camera.aspect = 2400f / 1080; var requested = Vector3.zero; requested[axis] = sign * 3; - var start = _app.Orbit.Camera.WorldToViewportPoint(points[0]); var end = _app.Orbit.Camera.WorldToViewportPoint(points[0] + requested); - var actual = _app.Environment.ConstrainTranslation(points, requested, _app.Presenter.transform); - Assert.That(actual.magnitude, Is.InRange(1.19f, 1.21f), "Actual boundary clipping must stop at the .5 sphere margin."); - _app.CommitMove("native-drag-group", BoardPresenter.ToPosition(actual)); yield return null; - drags.Add(new NativeDrag { name = "boundary-" + count + "-" + axis + "-" + sign, ballId = id, source = source, - expected = _app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot()), requested = requested, - start = new Vector2(start.x * 2400, (1 - start.y) * 1080), end = new Vector2(end.x * 2400, (1 - end.y) * 1080) }); + drags.Add(ExerciseNativeDrag("boundary-" + count + "-" + axis + "-" + sign, "boundary", source, requested)); + yield return null; } _app.StartStage(1); yield return null; Physics.SyncTransforms(); var arch = _app.Environment.GetComponentsInChildren().First(value => value.enabled && value.GetComponentsInParent().Any(parent => parent.name.Contains("OpenStoneArch"))); @@ -279,15 +329,22 @@ namespace MagnetismHeroes.Tests var source = new FreePlayState { sourceStageId = 1, camera = camera, balls = balls.ToArray(), groups = groups.ToArray() }; _store.Saved.latestFreePlay = source; _app.Initialize(_store); _app.ContinueFreePlay(); yield return null; Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board), Is.True); _app.Orbit.Camera.aspect = 2400f / 1080; - var start = _app.Orbit.Camera.WorldToViewportPoint(sourcePoint); var end = _app.Orbit.Camera.WorldToViewportPoint(sourcePoint + requested); - _app.Presenter.PreviewGroup("native-contact-source", requested); - var actual = _app.Presenter.DropDelta; var merge = _app.Presenter.MergeCandidate; - if (kind == "foliage-pass") Assert.That(Vector3.Distance(actual, requested), Is.LessThan(.001f)); - if (kind == "thin-obstacle") { Assert.That(actual.x, Is.GreaterThan(.1f).And.LessThan(requested.x - 1)); Assert.That(merge, Is.Null); } - if (kind == "two-plus-three") Assert.That(merge, Is.EqualTo("native-contact-target")); - _app.CommitMove("native-contact-source", BoardPresenter.ToPosition(actual), merge); yield return null; - drags.Add(new NativeDrag { name = kind, kind = kind, ballId = source.balls[0].id, source = source, expected = _app.Session.Board.ToFreePlayState(_app.Orbit.Snapshot()), requested = requested, - start = new Vector2(start.x * 2400, (1 - start.y) * 1080), end = new Vector2(end.x * 2400, (1 - end.y) * 1080) }); + if (kind == "thin-obstacle") + { + bool visible = false; + foreach (float pitch in new[] { 0f, 15f, 30f, 45f, 60f }) + { + var rotation = Quaternion.Euler(pitch, 0, 0); + camera.rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w); _app.Orbit.Apply(camera); + Physics.SyncTransforms(); var ray = _app.Orbit.Camera.ScreenPointToRay(PlayerPoint(NativePoint(sourcePoint))); + if (!Physics.Raycast(ray, out var hit, 100) || !hit.collider.TryGetComponent(out var ball) || ball.BallId != source.balls[0].id) continue; + Assert.That(Quaternion.Angle(_app.Orbit.Camera.transform.rotation, rotation), Is.LessThan(.001f), "Chosen pitch must already be a valid manual pose."); + source.camera = _app.Orbit.Snapshot(); _app.Session.CommitCamera(source.camera); visible = true; break; + } + Assert.That(visible, Is.True, "The same thin-arch source must have an actually selectable supported pitch."); + } + Capture(kind + "-native-pointer-source", "Actual Editor-selected first hit and preserved source; integer native pointer oracle, not Android pass", 2400, 1080); + drags.Add(ExerciseNativeDrag(kind, kind, source, requested)); yield return null; } var directory = Environment.GetEnvironmentVariable("MAGNETISM_NATIVE_FIXTURES") ?? Path.GetFullPath("artifacts/verification/PlaygroundInteraction/NativeFixtures"); Directory.CreateDirectory(directory);