Advance blocked split previews to the last safe Core delta

This commit is contained in:
NINI
2026-10-10 03:21:54 +09:00
parent f31ebc6fd9
commit 6e353a1281
2 changed files with 35 additions and 11 deletions
@@ -182,11 +182,15 @@ namespace MagnetismHeroes.Tests
Assert.That(_app.Presenter.CanDropGroup("group-0"), Is.True);
var validDelta = _app.Presenter.DropDelta; var validPoint = _app.Presenter.Balls["safety-2"].transform.position;
_app.Input.Move(point + new Vector2(140, 0));
Assert.That(_app.Presenter.DropDelta, Is.EqualTo(validDelta));
Assert.That(_app.Presenter.Balls["safety-2"].transform.position, Is.EqualTo(validPoint));
var stopped = _app.Presenter.Balls["safety-2"].transform.position;
Assert.That(stopped.x, Is.GreaterThan(validPoint.x + .2f), "Continue the safe part of the split drag up to the obstacle.");
Assert.That(stopped.x, Is.EqualTo(.689f).Within(.003f), "The whole unit sphere must stop just before the thin wall.");
Assert.That(_app.Presenter.DropDelta.x, Is.GreaterThan(validDelta.x + .2f));
var corePreview = _app.Session.Board.PreviewSplit("group-0", new[] { "safety-2" }, BoardPresenter.ToPosition(_app.Presenter.DropDelta));
foreach (var ball in corePreview) Assert.That(_app.Presenter.Balls[ball.id].transform.position, Is.EqualTo(BoardPresenter.ToVector(ball.position)));
_app.Input.Move(point + new Vector2(128, 0));
var reverse = _app.Presenter.Balls["safety-2"].transform.position;
Assert.That(reverse.x, Is.LessThan(validPoint.x - .01f));
Assert.That(reverse.x, Is.LessThan(stopped.x - .01f), "Reverse must act immediately at the last safe split delta.");
var preview = _app.Presenter.Balls.ToDictionary(pair => pair.Key, pair => pair.Value.transform.position);
_app.Input.End(point + new Vector2(128, 0)); yield return null;
Assert.That(_app.Session.Board.Groups.Length, Is.EqualTo(2));
@@ -144,20 +144,40 @@ namespace MagnetismHeroes.UnityRuntime.Presentation
CancelPreview(); _previewGroup = groupId; _splitPreview = true; _previewValid = false;
return false;
}
if (!CanSweepSplit(positions, selected))
{
if (!hadValidPreview) { _previewValid = false; return false; }
// Search Core input deltas, so the last safe arrays and the committed delta stay identical.
var previousDelta = DropDelta;
positions = _board.PreviewSplit(groupId, selected, ToPosition(previousDelta));
float safe = 0, blocked = 1;
for (int iteration = 0; iteration < 20; iteration++)
{
float fraction = (safe + blocked) * .5f;
var candidateDelta = Vector3.Lerp(previousDelta, delta, fraction);
var candidate = _board.PreviewSplit(groupId, selected, ToPosition(candidateDelta));
if (candidate != null && candidate.All(ball => _environment.IsSafeSphere(ToVector(ball.position), transform))
&& CanSweepSplit(candidate, selected))
{ safe = fraction; positions = candidate; }
else blocked = fraction;
}
delta = Vector3.Lerp(previousDelta, delta, safe);
}
foreach (var ball in positions) _balls[ball.id].transform.position = ToVector(ball.position);
DropDelta = delta; _previewValid = true;
PositionBadges(); return true;
}
private bool CanSweepSplit(IReadOnlyList<BallState> positions, IReadOnlyList<string> selected)
{
foreach (var ball in positions.Where(ball => selected.Contains(ball.id)))
{
var start = _balls[ball.id].transform.position;
var movement = ToVector(ball.position) - start;
if ((_environment.ConstrainTranslation(new[] { start }, movement, transform) - movement).sqrMagnitude > .00000001f)
{
// Keep exactly the last Core-produced split and its input delta; never commit a clipped shape.
_previewValid = hadValidPreview;
return hadValidPreview;
}
return false;
}
foreach (var ball in positions) _balls[ball.id].transform.position = ToVector(ball.position);
DropDelta = delta; _previewValid = true;
PositionBadges(); return true;
return true;
}
public void ShowSelection(IReadOnlyCollection<string> selected)