diff --git a/Assets/MagnetismHeroes/Tests/PlayMode/FirstPuzzlePlayTests.cs b/Assets/MagnetismHeroes/Tests/PlayMode/FirstPuzzlePlayTests.cs index ea87ed6..81023f7 100644 --- a/Assets/MagnetismHeroes/Tests/PlayMode/FirstPuzzlePlayTests.cs +++ b/Assets/MagnetismHeroes/Tests/PlayMode/FirstPuzzlePlayTests.cs @@ -58,6 +58,59 @@ namespace MagnetismHeroes.Tests } private void Drag(Vector2 start, Vector2 end) { _app.Input.Begin(start, false); _app.Input.Move(end); _app.Input.End(end); } private void Tap(Vector2 point) { _app.Input.Begin(point, false); _app.Input.End(point); } + private void DragTo(Vector3 destination) + { + Physics.SyncTransforms(); var start = BallPoint(); + var camera = _app.Presenter.Camera; Physics.Raycast(camera.ScreenPointToRay(start), out var hit, 100); + var offset = hit.point - _app.Presenter.Balls.Values.Single().transform.position; + Drag(start, camera.WorldToScreenPoint(destination + offset)); + } + + [UnityTest] public IEnumerator PinchReleaseOutsideRestoresCameraAndSuppressesRemainingTouch() + { + _touchscreen = InputSystem.AddDevice(); _app.StartStage(1); yield return null; Physics.SyncTransforms(); + var point = BallPoint(); var second = point + new Vector2(90, 0); var initial = _app.Session.Camera.distance; + InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 71, phase = UnityEngine.InputSystem.TouchPhase.Began, position = point }); + InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 72, phase = UnityEngine.InputSystem.TouchPhase.Began, position = second }); yield return null; yield return null; + InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 72, phase = UnityEngine.InputSystem.TouchPhase.Moved, position = second + new Vector2(80, 0) }); yield return null; yield return null; + Assert.That(_app.Orbit.Snapshot().distance, Is.LessThan(initial)); var saved = JsonUtility.ToJson(_store.Saved); + InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 72, phase = UnityEngine.InputSystem.TouchPhase.Ended, position = new Vector2(-1, second.y) }); yield return null; yield return null; + Assert.That(_app.Orbit.Snapshot().distance, Is.EqualTo(initial)); Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved)); + InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 71, phase = UnityEngine.InputSystem.TouchPhase.Moved, position = point + new Vector2(70, 20) }); yield return null; yield return null; + Assert.That(_app.Orbit.Snapshot().rotation, Is.EqualTo(_app.Session.Camera.rotation)); + Assert.That(_app.Session.Board.CollectedBallIds, Is.Empty); + } + + [UnityTest] public IEnumerator CanceledDragCannotBeInheritedByANewFingerInTheSameFrame() + { + _touchscreen = InputSystem.AddDevice(); _app.StartStage(1); yield return null; Physics.SyncTransforms(); + var point = BallPoint(); var initial = _app.Session.Board.Balls.Single().position; + InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 81, phase = UnityEngine.InputSystem.TouchPhase.Began, position = point }); yield return null; yield return null; + InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 81, phase = UnityEngine.InputSystem.TouchPhase.Moved, position = point + new Vector2(70, 20) }); yield return null; yield return null; + var saved = JsonUtility.ToJson(_store.Saved); + InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 81, phase = UnityEngine.InputSystem.TouchPhase.Canceled, position = point + new Vector2(70, 20) }); + InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 82, phase = UnityEngine.InputSystem.TouchPhase.Began, position = point + new Vector2(140, 30) }); yield return null; yield return null; + Assert.That(_app.Presenter.Balls.Values.Single().transform.position, Is.EqualTo(new Vector3(initial.x, initial.y, initial.z))); + InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 82, phase = UnityEngine.InputSystem.TouchPhase.Ended, position = point + new Vector2(140, 30) }); yield return null; yield return null; + Assert.That(_app.Session.Board.Balls.Single().position, Is.EqualTo(initial)); Assert.That(_app.Session.Board.CollectedBallIds, Is.Empty); + Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved)); + } + + [UnityTest] public IEnumerator BackgroundInteriorDropIsRejectedAndSurfaceContactRemainsPlayable() + { + _app.StartStage(1); yield return null; DragTo(new Vector3(1.8f, 0.8f, 0)); yield return null; + Assert.That(_app.Session.Board.CollectedBallIds.Length, Is.EqualTo(1)); + _app.Orbit.Orbit(new Vector2(360, 0)); _app.Session.CommitCamera(_app.Orbit.Snapshot()); + DragTo(new Vector3(1.8f, 0.65f, 1.1f)); yield return null; Physics.SyncTransforms(); + var initial = _app.Session.Board.Balls.Single().position; + Assert.That(initial.y, Is.EqualTo(0.65f).Within(0.001)); Assert.That(initial.z, Is.EqualTo(1.1f).Within(0.001)); BallPoint(); + var saved = JsonUtility.ToJson(_store.Saved); var camera = _app.Orbit.Snapshot(); + DragTo(new Vector3(1.8f, -0.4f, 1.1f)); yield return null; + Assert.That(_app.Session.Board.Balls.Single().position, Is.EqualTo(initial), "The scene interior must not become a committed drop."); + Assert.That(_app.Presenter.Balls.Values.Single().transform.position, Is.EqualTo(new Vector3(initial.x, initial.y, initial.z))); + Assert.That(_app.Orbit.Snapshot().rotation, Is.EqualTo(camera.rotation)); Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved)); + Physics.SyncTransforms(); BallPoint(); + } [UnityTest] public IEnumerator InspectorButtonsNavigateAndHomeKeepsACameraAndListener() { diff --git a/Assets/MagnetismHeroes/UnityRuntime/Interaction/BoardInput.cs b/Assets/MagnetismHeroes/UnityRuntime/Interaction/BoardInput.cs index 57fade6..7db4790 100644 --- a/Assets/MagnetismHeroes/UnityRuntime/Interaction/BoardInput.cs +++ b/Assets/MagnetismHeroes/UnityRuntime/Interaction/BoardInput.cs @@ -59,11 +59,14 @@ namespace MagnetismHeroes.UnityRuntime.Interaction public void End(Vector2 point, bool canceled = false) { - bool inside = point.x >= 0 && point.x < Screen.width && point.y >= 0 && point.y < Screen.height; - if (canceled || !inside) { Cancel(); return; } + if (canceled || !IsInside(point)) { Cancel(); return; } if (_gesture == Gesture.Friend) { - if (_dragged) _app.CommitMove(_groupId, BoardPresenter.ToPosition(_previewDelta)); + if (_dragged) + { + if (!_presenter.CanDropGroup(_groupId)) { Cancel(); return; } + _app.CommitMove(_groupId, BoardPresenter.ToPosition(_previewDelta)); + } else _app.CountBall(_ballId); } else if (_gesture == Gesture.Orbit && _dragged) _app.Session.CommitCamera(_orbit.Snapshot()); @@ -97,23 +100,49 @@ namespace MagnetismHeroes.UnityRuntime.Interaction return hits.Count > 0; } + private static bool IsInside(Vector2 point) => point.x >= 0 && point.x < Screen.width && point.y >= 0 && point.y < Screen.height; + + private static UnityEngine.InputSystem.Controls.TouchControl FindTouch(Touchscreen screen, int id) + { + foreach (var touch in screen.touches) if (touch.touchId.ReadValue() == id) return touch; + return null; + } + + private void FinishSingleTouch(Touchscreen screen) + { + var touch = FindTouch(screen, _fingerId); + if (touch == null || touch.phase.ReadValue() != UnityEngine.InputSystem.TouchPhase.Ended) { Cancel(); return; } + var point = touch.position.ReadValue(); Move(point); End(point); + } + private void Update() { if (!AcceptInput) return; var touchscreen = Touchscreen.current; if (touchscreen != null) { - if (_gesture == Gesture.Pinch) - { - foreach (var touch in touchscreen.touches) - { - var id = touch.touchId.ReadValue(); - if ((id == _pinchFirstId || id == _pinchSecondId) && touch.phase.ReadValue() == UnityEngine.InputSystem.TouchPhase.Canceled) - { Cancel(); _waitForAllTouchesReleased = true; return; } - } - } var active = new List(); foreach (var touch in touchscreen.touches) if (touch.press.isPressed) active.Add(touch); + if (_fingerId != -1 && !active.Exists(touch => touch.touchId.ReadValue() == _fingerId)) + { + // Finish the original pointer before any new pointer can inherit its preview. + FinishSingleTouch(touchscreen); _waitForAllTouchesReleased = active.Count > 0; return; + } + if (_gesture == Gesture.Pinch) + { + Vector2 release = _last; bool ended = false, canceled = false; + foreach (int id in new[] { _pinchFirstId, _pinchSecondId }) + { + var touch = FindTouch(touchscreen, id); + if (touch == null) { canceled = true; continue; } + var phase = touch.phase.ReadValue(); + if (phase == UnityEngine.InputSystem.TouchPhase.Canceled) canceled = true; + if (phase == UnityEngine.InputSystem.TouchPhase.Ended) + { release = touch.position.ReadValue(); ended = true; canceled |= !IsInside(release); } + } + if (canceled || ended) + { End(release, canceled); _waitForAllTouchesReleased = active.Count > 0; return; } + } // A canceled/finished pinch cannot restart when a third finger joins a remaining finger. if (_waitForAllTouchesReleased && _gesture == Gesture.None && active.Count > 0) return; if (active.Count >= 2) @@ -130,7 +159,6 @@ namespace MagnetismHeroes.UnityRuntime.Interaction } if (_waitForAllTouchesReleased) { - if (_gesture == Gesture.Pinch) End(_last); if (active.Count == 0) { _waitForAllTouchesReleased = false; _gesture = Gesture.None; _fingerId = -1; } return; } @@ -144,16 +172,6 @@ namespace MagnetismHeroes.UnityRuntime.Interaction else Move(point); return; } - if (_fingerId != -1) - { - bool canceled = false; - Vector2 releasePoint = _last; - foreach (var touch in touchscreen.touches) - if (touch.touchId.ReadValue() == _fingerId) - { canceled = touch.phase.ReadValue() == UnityEngine.InputSystem.TouchPhase.Canceled; releasePoint = touch.position.ReadValue(); break; } - if (!canceled) Move(releasePoint); - End(releasePoint, canceled); return; - } } var mouse = Mouse.current; if (mouse == null) return; diff --git a/Assets/MagnetismHeroes/UnityRuntime/Presentation/BoardPresenter.cs b/Assets/MagnetismHeroes/UnityRuntime/Presentation/BoardPresenter.cs index 8224efd..cc56be7 100644 --- a/Assets/MagnetismHeroes/UnityRuntime/Presentation/BoardPresenter.cs +++ b/Assets/MagnetismHeroes/UnityRuntime/Presentation/BoardPresenter.cs @@ -50,6 +50,26 @@ namespace MagnetismHeroes.UnityRuntime.Presentation PositionBadges(); } + public bool CanDropGroup(string groupId) + { + Physics.SyncTransforms(); + foreach (var ball in _balls.Values.Where(ball => ball.GroupId == groupId)) + { + var sphere = ball.GetComponent(); + var scale = sphere.transform.lossyScale; + float radius = sphere.radius * Mathf.Max(Mathf.Abs(scale.x), Mathf.Abs(scale.y), Mathf.Abs(scale.z)); + foreach (var blocker in Physics.OverlapSphere(sphere.transform.TransformPoint(sphere.center), radius, ~0, QueryTriggerInteraction.Ignore)) + { + // Board friends and their face/count decorations are not background obstacles. + if (blocker.transform.IsChildOf(transform) || blocker.attachedRigidbody != null) continue; + if (Physics.ComputePenetration(sphere, sphere.transform.position, sphere.transform.rotation, + blocker, blocker.transform.position, blocker.transform.rotation, out _, out float depth) && depth > 0.001f) + return false; + } + } + return true; + } + private void PositionBadges() { if (_board == null) return;