diff --git a/Assets/MagnetismHeroes/Tests/PlayMode/DragCameraPlayTests.cs b/Assets/MagnetismHeroes/Tests/PlayMode/DragCameraPlayTests.cs index 8c73096..56af5cb 100644 --- a/Assets/MagnetismHeroes/Tests/PlayMode/DragCameraPlayTests.cs +++ b/Assets/MagnetismHeroes/Tests/PlayMode/DragCameraPlayTests.cs @@ -330,11 +330,20 @@ namespace MagnetismHeroes.Tests foreach (var collider in _disabledScenery) if (collider != null) collider.enabled = true; Physics.SyncTransforms(); var discoveries = new System.Collections.Generic.List(); + var dragProof = new System.Collections.Generic.List(); + int totalDragged = 0; for (int stage = 1; stage <= 30; stage++) { _app.StartStage(stage); _app.Voice.Clear(); yield return null; var before = _app.Session.Board.Balls.Select(ball => (ball.id, ball.position)).ToArray(); var visible = new System.Collections.Generic.HashSet(); var original = _app.Orbit.Snapshot(); + var dragged = new System.Collections.Generic.HashSet(); + var positions = _app.Session.Board.Balls.ToDictionary(ball => ball.id, ball => BoardPresenter.ToVector(ball.position)); + var boardSnapshot = JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Session.Camera)); + var saved = JsonUtility.ToJson(_store.Save); int writes = _store.Writes; + WriteRecords($"initial-stage-{stage}-board-before.json", new[] { boardSnapshot }); + WriteRecords($"initial-stage-{stage}-save-before.json", new[] { saved }); + var grabField = _app.Input.GetType().GetField("_ballId", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); foreach (float pitch in new[] { 15f, 35f, 60f, 80f }) for (float yaw = 0; yaw < 360; yaw += 15) { @@ -366,12 +375,53 @@ namespace MagnetismHeroes.Tests readable &= !Physics.Raycast(glyphRay, Vector3.Distance(camera.transform.position, glyphCenter) - .01f); if (!readable) continue; if (visible.Add(ball.id)) discoveries.Add($"stage={stage};id={ball.id};position={world};camera={camera.transform.position};pitch={pitch};yaw={yaw};visibleGlyph={number.text};glyph={number.transform.position}"); + if (dragged.Contains(ball.id) || view.x < .221f || view.x > .771f || view.y < .306f || view.y > .741f) continue; + foreach (var direction in new[] { Vector2.up, Vector2.down, Vector2.left, Vector2.right }) + { + var manual = _app.Orbit.Snapshot(); var manualJson = JsonUtility.ToJson(manual); + var point = (Vector2)camera.WorldToScreenPoint(world); var end = point + direction * 40; + var endView = new Vector2(end.x / Screen.width, end.y / Screen.height); + if (endView.x < .221f || endView.x > .771f || endView.y < .306f || endView.y > .741f) continue; + _app.Input.Begin(point, false); Assert.That(grabField.GetValue(_app.Input), Is.EqualTo(ball.id), "Begin must grab this exact initial sphere ID."); + _app.Input.Move(end); + var preview = _app.Presenter.Balls[ball.id].transform.position; + float pixels = Vector2.Distance(camera.WorldToScreenPoint(preview), point); var delta = _app.Presenter.DropDelta; + bool moved = pixels >= 25 && Vector3.Distance(preview, world) >= .05f && _app.Orbit.IsAssisting; + Assert.That(_app.Presenter.CanDropGroup(group.id), Is.True, "Every whole-sphere preview remains safe against the actual ground and scenery."); + Assert.That(_app.Presenter.Balls.Keys, Is.EquivalentTo(positions.Keys)); + foreach (var member in _app.Session.Board.Balls) + Assert.That(_app.Presenter.Balls[member.id].transform.position, Is.EqualTo(positions[member.id] + (group.ballIds.Contains(member.id) ? delta : Vector3.zero)), "Only the grabbed whole group translates, preserving its shape."); + Assert.That(JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Session.Camera)), Is.EqualTo(boardSnapshot)); + Assert.That(JsonUtility.ToJson(_app.Orbit.Snapshot()), Is.EqualTo(manualJson)); + Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(saved)); Assert.That(_store.Writes, Is.EqualTo(writes)); + var attempt = $"stage={stage};id={ball.id};grabbed={grabField.GetValue(_app.Input)};group={group.id};members={string.Join(",", group.ballIds)};initial={world};preview={preview};delta={delta};pointerStart={point};pointerEnd={end};pointerStartViewport={view};pointerEndViewport={endView};previewWorldDistance={Vector3.Distance(preview, world):R};previewPixels={pixels:R};passed={moved};manual={manualJson};coreUnchanged=True;saveWrites={writes};sceneryEnabled={_app.Environment.GetComponentsInChildren().Count(item => item.enabled && item.gameObject.activeInHierarchy && !item.isTrigger)}"; + _app.Input.Cancel(); + foreach (var member in _app.Session.Board.Balls) Assert.That(_app.Presenter.Balls[member.id].transform.position, Is.EqualTo(positions[member.id]), "Cancel restores every initial sphere exactly."); + Assert.That(_app.Presenter.DropDelta, Is.EqualTo(Vector3.zero)); Assert.That(_app.Presenter.MergeCandidate, Is.Null); + Assert.That(JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Session.Camera)), Is.EqualTo(boardSnapshot)); + Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(saved)); Assert.That(_store.Writes, Is.EqualTo(writes)); + Assert.That(JsonUtility.ToJson(_app.Orbit.Snapshot()), Is.EqualTo(manualJson)); + // The camera has not advanced a frame between Move and Cancel; verify the live return settles without a fixed sleep. + float deadline = Time.realtimeSinceStartup + 2; + while (_app.Orbit.IsReturning && Time.realtimeSinceStartup < deadline) yield return null; + Assert.That(_app.Orbit.IsReturning, Is.False); + Assert.That(Vector3.Distance(camera.transform.position, BoardPresenter.ToVector(manual.target) + Vector3.up * .65f + - new Quaternion(manual.rotation.x, manual.rotation.y, manual.rotation.z, manual.rotation.w) * Vector3.forward * manual.distance), Is.LessThan(.0011f)); + Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(saved)); Assert.That(_store.Writes, Is.EqualTo(writes)); + dragProof.Add(attempt + ";cancelRestored=True;manualRestored=True;idsShapeMembershipPreserved=True;saveUnchanged=True"); + WriteRecords("initial-173-drag-attempts.txt", dragProof); + if (moved) { dragged.Add(ball.id); totalDragged++; break; } + } } } Assert.That(visible, Is.EquivalentTo(_app.Session.Board.Balls.Select(ball => ball.id)), "Actual sphere first-hit discovery stage " + stage); + WriteRecords($"initial-stage-{stage}-board-after.json", new[] { JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Session.Camera)) }); + Assert.That(dragged, Is.EquivalentTo(_app.Session.Board.Balls.Select(ball => ball.id)), "Every original sphere ID must actually move at least 25 screen pixels through Input, then cancel without committing stage " + stage); Assert.That(_app.Session.Board.Balls.Select(ball => (ball.id, ball.position)), Is.EqualTo(before)); } + Assert.That(totalDragged, Is.EqualTo(173)); WriteRecords("thirty-stage-first-hit-discovery.txt", discoveries); + WriteRecords("initial-173-drag-attempts.txt", dragProof); } [UnityTest] public IEnumerator ObstructedReturnRetracesTheActualSafeAssistPathWithoutCameraSnap() {