fix: keep friend numerals readable through camera rotation

This commit is contained in:
NINI
2026-10-05 03:00:14 +09:00
2 changed files with 43 additions and 1 deletions
@@ -4,6 +4,8 @@ using System.Linq;
using MagnetismHeroes.Core;
using MagnetismHeroes.UnityRuntime.Bootstrap;
using MagnetismHeroes.UnityRuntime.StageAssets;
using MagnetismHeroes.UnityRuntime.Presentation;
using TMPro;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
@@ -66,6 +68,46 @@ namespace MagnetismHeroes.Tests
Drag(start, camera.WorldToScreenPoint(destination + offset));
}
[UnityTest] public IEnumerator GroupNumberFrontViewStaysAboveAndVisible() => CheckGroupNumber(Quaternion.identity);
[UnityTest] public IEnumerator GroupNumberBelowViewStaysAboveAndVisible() => CheckGroupNumber(Quaternion.Euler(-90, 0, 0));
[UnityTest] public IEnumerator GroupNumberRolledViewStaysAboveAndVisible() => CheckGroupNumber(Quaternion.Euler(0, 0, 90));
[UnityTest] public IEnumerator GroupNumberPreviewFollowsTheFriendWithoutChangingCommittedState() => CheckGroupNumber(Quaternion.Euler(-90, 0, 0), true);
private IEnumerator CheckGroupNumber(Quaternion rotation, bool preview = false)
{
_app.StartStage(1); yield return null;
var state = _app.Session.Board.Balls.Single();
_app.CommitMove(_app.Session.Board.Groups.Single().id, new Position3(4 - state.position.x, 0, 0));
_app.CountBall(state.id); Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.FreePlay));
var saved = JsonUtility.ToJson(_store.Saved); var position = _app.Session.Board.Balls.Single().position;
var committedCamera = JsonUtility.ToJson(_app.Session.Camera);
_app.Orbit.Apply(new CameraState { target = new Position3(4, 0.6f, 0), distance = 8,
rotation = new Rotation4(rotation.x, rotation.y, rotation.z, rotation.w) });
if (preview) _app.Presenter.PreviewGroup(_app.Session.Board.Groups.Single().id, new Vector3(0.8f, 0, 0.3f));
yield return null; Physics.SyncTransforms();
var camera = _app.Presenter.Camera; var badge = _app.Presenter.GetComponentInChildren<GroupBadge>();
var number = badge.GetComponentInChildren<TextMeshPro>(); number.ForceMeshUpdate();
Assert.That(number.text, Is.EqualTo("1")); Assert.That(number.textInfo.characterInfo[0].isVisible, Is.True);
Assert.That(Vector3.Dot(number.transform.forward, camera.transform.forward), Is.EqualTo(1).Within(0.001));
Assert.That(Vector3.Dot(badge.transform.Find("RepresentativeFace").forward, camera.transform.forward), Is.EqualTo(1).Within(0.001));
var glyph = number.textInfo.characterInfo[0];
var glyphWorld = new[] { glyph.bottomLeft, glyph.topLeft, glyph.topRight, glyph.bottomRight }.Select(number.transform.TransformPoint).ToArray();
var glyphScreen = glyphWorld.Select(camera.WorldToScreenPoint).ToArray();
Assert.That(glyphScreen.All(point => point.z > camera.nearClipPlane && point.x > 0 && point.x < Screen.width && point.y > 0 && point.y < Screen.height), Is.True);
var ballBounds = _app.Presenter.Balls.Values.Single().GetComponent<SphereCollider>().bounds;
float ballTop = float.NegativeInfinity;
foreach (int x in new[] { -1, 1 }) foreach (int y in new[] { -1, 1 }) foreach (int z in new[] { -1, 1 })
ballTop = Mathf.Max(ballTop, camera.WorldToScreenPoint(ballBounds.center + Vector3.Scale(ballBounds.extents, new Vector3(x, y, z))).y);
Assert.That(glyphScreen.Min(point => point.y), Is.GreaterThan(ballTop), "The visible glyph must sit above the sphere in the current camera view.");
var glyphCenter = glyphWorld.Aggregate(Vector3.zero, (sum, vertex) => sum + vertex) / glyphWorld.Length;
var ray = camera.ScreenPointToRay(camera.WorldToScreenPoint(glyphCenter));
Assert.That(Physics.Raycast(ray, Vector3.Distance(ray.origin, glyphCenter)), Is.False, "Scene colliders must not hide the number before its glyph plane.");
Assert.That(_app.Session.Board.Balls.Single().position, Is.EqualTo(position));
Assert.That(JsonUtility.ToJson(_app.Session.Camera), Is.EqualTo(committedCamera)); Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved));
_app.Presenter.CancelPreview();
Assert.That(_app.Presenter.Balls.Values.Single().transform.position, Is.EqualTo(new Vector3(position.x, position.y, position.z)));
}
[UnityTest] public IEnumerator PinchReleaseOutsideRestoresCameraAndSuppressesRemainingTouch()
{
_touchscreen = InputSystem.AddDevice<Touchscreen>(); _app.StartStage(1); yield return null; Physics.SyncTransforms();
@@ -79,7 +79,7 @@ namespace MagnetismHeroes.UnityRuntime.Presentation
var group = groups[index];
var center = group.ballIds.Select(id => _balls[id].transform.position).Aggregate(Vector3.zero, (sum, value) => sum + value) / group.ballIds.Length;
var representative = _balls[group.ballIds[0]].transform.position;
_badges[index].Bind(group.ballIds.Length, center + Vector3.up * 0.95f,
_badges[index].Bind(group.ballIds.Length, center + _camera.transform.up * 0.95f,
representative - _camera.transform.forward * 0.49f);
_badges[index].FaceCamera(_camera);
}