Certify split-board restoration and preserve corrected camera poses
This commit is contained in:
@@ -99,6 +99,60 @@ namespace MagnetismHeroes.Tests
|
||||
Debug.Log($"[ProgressRestoreMeshDiagnostic] mesh={collider.sharedMesh.name}, point={point:R}, closestSurface={surface:R}, distance={closest:R}, triangle={closestTriangle}, position={collider.transform.position:R}, rotation={collider.transform.rotation:R}, scale={collider.transform.lossyScale:R}, vertices={vertices.Length}, triangles={triangles.Length/3}");
|
||||
}
|
||||
}
|
||||
[UnityTest] public IEnumerator ActualGardenSplitFiveResumeRepairsWithoutChangingThreeAndTwoMembershipOrOtherSlots()
|
||||
{
|
||||
_store.Save.puzzleStates=new[]{new BoardRules(_app.Session.Definitions.First(stage=>stage.Id==2)).ToPuzzleState(GameSession.DefaultCamera()),new BoardRules(_app.Session.Definitions.First(stage=>stage.Id==6)).ToPuzzleState(GameSession.DefaultCamera())};
|
||||
Reload();_app.StartStage(9);
|
||||
var camera = new CameraState {target=new Position3(0,.6f,0),distance=11,rotation=new Rotation4(-.022557568f,.96225017f,.08418597f,.2578342f)};
|
||||
var saved = new FreePlayState {sourceStageId=9,camera=camera,
|
||||
balls=new[] {
|
||||
new BallState {id="s09-b01",position=new Position3(-1.0351909f,.8f,.60371524f)},
|
||||
new BallState {id="s09-b02",position=new Position3(-.03519098f,.8f,.60371524f)},
|
||||
new BallState {id="s09-b03",position=new Position3(-3.9800465f,.8f,1.649168f)},
|
||||
new BallState {id="s09-b04",position=new Position3(.964809f,.8f,.60371524f)},
|
||||
new BallState {id="s09-b05",position=new Position3(-2.9800465f,.8f,1.649168f)}},
|
||||
groups=new[] {new GroupState {id="s09-g01",ballIds=new[] {"s09-b01","s09-b02","s09-b04"}},new GroupState {id="s09-g01-split",ballIds=new[] {"s09-b03","s09-b05"}}}};
|
||||
var rawRotation=new Quaternion(camera.rotation.x,camera.rotation.y,camera.rotation.z,camera.rotation.w);
|
||||
var rawEye=BoardPresenter.ToVector(camera.target)+Vector3.up*.65f-rawRotation*Vector3.forward*camera.distance;
|
||||
Assert.That(rawEye.y,Is.LessThan(_app.Environment.PlaygroundBounds.min.y),"The exact old Content camera is underground and must be corrected separately from the friends.");
|
||||
var source=saved.balls.ToDictionary(ball=>ball.id,ball=>BoardPresenter.ToVector(ball.position));
|
||||
var fixedPoints=saved.groups[0].ballIds.Select(id=>source[id]).ToArray();
|
||||
Assert.That(fixedPoints.All(point=>_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point))),Is.True);
|
||||
var movedPoints=saved.groups[1].ballIds.Select(id=>source[id]).ToArray();
|
||||
Assert.That(movedPoints.All(point=>_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point))),Is.False);
|
||||
Assert.That(movedPoints.All(point=>_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point+Vector3.forward))&&fixedPoints.All(other=>Vector3.Distance(point+Vector3.forward,other)>=.999f)),Is.True,"A valid one-unit escape proves this board is repairable.");
|
||||
float sampledUpperBound=1;
|
||||
for(int index=0;index<64;index++)
|
||||
{
|
||||
float y=1-2*(index+.5f)/64,angle=index*2.39996323f;
|
||||
var direction=new Vector3(Mathf.Sqrt(1-y*y)*Mathf.Cos(angle),y,Mathf.Sqrt(1-y*y)*Mathf.Sin(angle));
|
||||
for(float radius=.01f;radius<sampledUpperBound;radius+=.01f)
|
||||
if(movedPoints.All(point=>_app.Presenter.IsSafePosition(BoardPresenter.ToPosition(point+direction*radius))&&fixedPoints.All(other=>Vector3.Distance(point+direction*radius,other)>=.999f))) {sampledUpperBound=radius;break;}
|
||||
}
|
||||
_store.Save.latestFreePlay=saved; string puzzles=string.Join("|",_store.Save.puzzleStates.Select(state=>JsonUtility.ToJson(state))); var completed=(int[])_store.Save.completedStageIds.Clone(); var tutorials=(string[])_store.Save.seenTutorialIds.Clone();
|
||||
Reload();int before=_store.Writes;_app.ContinueFreePlay();yield return null;
|
||||
Assert.That(_app.Session.Board,Is.Not.Null,"The exact existing Content split-five record must enter after certified repair.");
|
||||
Assert.That(_app.Session.Screen,Is.EqualTo(SessionScreen.FreePlay));Assert.That(_app.Session.Board.StageId,Is.EqualTo(9));
|
||||
Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board),Is.True);Assert.That(_store.Writes,Is.EqualTo(before+1));
|
||||
foreach(var group in saved.groups)
|
||||
{
|
||||
var result=_app.Session.Board.Groups.Single(value=>value.id==group.id);Assert.That(result.ballIds,Is.EquivalentTo(group.ballIds));
|
||||
var target=_app.Session.Board.Balls.ToDictionary(ball=>ball.id,ball=>BoardPresenter.ToVector(ball.position));
|
||||
foreach(string id in group.ballIds) Assert.That(Vector3.Distance(target[id]-target[group.ballIds[0]],source[id]-source[group.ballIds[0]]),Is.LessThan(.0001f));
|
||||
}
|
||||
var restored=_app.Session.Board.Balls.ToDictionary(ball=>ball.id,ball=>BoardPresenter.ToVector(ball.position));
|
||||
Assert.That(restored.Keys,Is.EquivalentTo(source.Keys));Assert.That(_app.Session.Board.Groups.Length,Is.EqualTo(2));
|
||||
foreach(string id in saved.groups[0].ballIds) Assert.That(restored[id],Is.EqualTo(source[id]),"Already safe friends stay exactly placed.");
|
||||
float distance=Vector3.Distance(restored["s09-b03"],source["s09-b03"]);
|
||||
Assert.That(distance,Is.LessThanOrEqualTo(sampledUpperBound+.011f));
|
||||
foreach(string id in saved.groups[1].ballIds) foreach(string other in saved.groups[0].ballIds) Assert.That(Vector3.Distance(restored[id],restored[other]),Is.GreaterThanOrEqualTo(.999f));
|
||||
Assert.That(string.Join("|",_store.Save.puzzleStates.Select(state=>JsonUtility.ToJson(state))),Is.EqualTo(puzzles));Assert.That(_store.Save.completedStageIds,Is.EqualTo(completed));Assert.That(_store.Save.seenTutorialIds,Is.EqualTo(tutorials));
|
||||
Assert.That(_app.Orbit.Camera.transform.position.y,Is.GreaterThanOrEqualTo(_app.Environment.PlaygroundBounds.min.y+.35f-.001f));
|
||||
Assert.That(JsonUtility.ToJson(_store.Save.latestFreePlay.camera),Is.EqualTo(JsonUtility.ToJson(_app.Orbit.Snapshot())));
|
||||
Debug.Log($"[GardenRestoreActual] utc={System.DateTime.UtcNow:O}, distance={distance:R}, independentRadialUpperBound={sampledUpperBound:R}");
|
||||
_app.ShowHome();string corrected=JsonUtility.ToJson(_store.Save.latestFreePlay);_app.ContinueFreePlay();yield return null;
|
||||
Assert.That(JsonUtility.ToJson(_store.Save.latestFreePlay),Is.EqualTo(corrected));Assert.That(_app.Instruction,Is.Not.EqualTo("친구를 놀이터로 다시 보냈어요."));
|
||||
}
|
||||
[UnityTest] public IEnumerator CountingResumeRepairsTheWholeGroupWithoutCollectingOrCompletingAndWritesOnce()
|
||||
{
|
||||
var puzzle = new PuzzleState { stageId = 2, balls = new[] { Ball("old-a", 30), Ball("old-b", 31) },
|
||||
|
||||
@@ -135,7 +135,12 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap
|
||||
Add(new Vector3(x < 0 ? domain.min.x : x > 0 ? domain.max.x : closestBound.x,
|
||||
y < 0 ? domain.min.y : y > 0 ? domain.max.y : closestBound.y,
|
||||
z < 0 ? domain.min.z : z > 0 ? domain.max.z : closestBound.z));
|
||||
Add(domain.center);
|
||||
var best = Vector3.zero; bool found = false;
|
||||
// Establish a safe upper bound before short, unsafe surface candidates can consume the escape budget.
|
||||
// The domain center also avoids relying only on float-sensitive exact boundary faces.
|
||||
foreach (var candidate in candidates)
|
||||
if (Safe(candidate) && (!found || candidate.sqrMagnitude < best.sqrMagnitude)) { found = true; best = candidate; }
|
||||
// Escape candidates seed an upper bound. Their lengths never prune another direction or imply impossibility.
|
||||
for (int examined = 0; candidates.Count > 0 && examined < 128; examined++)
|
||||
{
|
||||
@@ -202,6 +207,10 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap
|
||||
for (int i = 1; i < cells.Count; i++) if (cells[i].lower < cells[index].lower) index = i;
|
||||
var cell = cells[index]; cells.RemoveAt(index);
|
||||
if (found && cell.lower >= best.magnitude - NearestTolerance) continue;
|
||||
// A cell projection may remain on an unsafe surface while its interior already contains safe placements.
|
||||
// Interior samples only improve the safe upper bound; all exclusion/lower-bound certificates remain unchanged.
|
||||
var center = cell.bounds.center;
|
||||
if ((!found || center.sqrMagnitude < best.sqrMagnitude) && Safe(center)) { found = true; best = center; }
|
||||
var closest = cell.bounds.ClosestPoint(Vector3.zero);
|
||||
if (Safe(closest))
|
||||
{
|
||||
|
||||
@@ -104,6 +104,8 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
|
||||
private Quaternion SafeRotation(Quaternion rotation, float distance)
|
||||
{
|
||||
float elevation = Mathf.Asin(Mathf.Clamp((rotation * Vector3.back).y, -1, 1)) * Mathf.Rad2Deg;
|
||||
// Reapplying an already valid stored pose must preserve its exact quaternion, including at the ground limit.
|
||||
if (elevation >= _minElevation && elevation <= _maxElevation && IsSafeCamera(Position(rotation, distance))) return rotation;
|
||||
float groundElevation = Mathf.Asin(Mathf.Clamp((Ground + _collisionMargin - Pivot.y) / distance, -1, 1)) * Mathf.Rad2Deg;
|
||||
float corrected = Mathf.Clamp(elevation, Mathf.Max(_minElevation, groundElevation), _maxElevation);
|
||||
var pitchAxis = Vector3.Cross(rotation * Vector3.back, Vector3.up).normalized;
|
||||
|
||||
Reference in New Issue
Block a user