using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using Xunit;
namespace AcDream.Core.Tests.Physics.Motion;
///
/// R5 conformance — (retail 0x00556090-…).
/// The leash-band taper + the 90 % IsFullyConstrained jump gate (port-plan §2b/§2c).
///
public sealed class ConstraintManagerTests
{
private static (R5Host host, ConstraintManager cm) Setup()
{
var world = new Dictionary();
var host = new R5Host(10u, world);
return (host, new ConstraintManager(host));
}
private static Position Anchor(float x) => new(1u, new Vector3(x, 0f, 0f), Quaternion.Identity);
[Fact]
public void ConstrainTo_InitializesOffsetToCurrentDistanceFromAnchor()
{
var (host, cm) = Setup();
host.SetOrigin(Vector3.Zero);
cm.ConstrainTo(Anchor(5f), startDistance: 2f, maxDistance: 10f);
Assert.True(cm.IsConstrained);
Assert.Equal(5f, cm.ConstraintPosOffset, 3); // distance(anchor(5,0,0), self(0,0,0))
}
[Fact]
public void IsFullyConstrained_TrueOnlyBeyond90PercentOfMax()
{
var (host, cm) = Setup();
host.SetOrigin(Vector3.Zero);
cm.ConstrainTo(Anchor(8f), 2f, 10f); // offset 8, 90% of 10 = 9 → 8 < 9
Assert.False(cm.IsFullyConstrained());
cm.ConstrainTo(Anchor(9.5f), 2f, 10f); // offset 9.5 > 9
Assert.True(cm.IsFullyConstrained());
}
[Fact]
public void IsFullyConstrained_FalseWhenNotConstrainedYet()
{
var (_, cm) = Setup();
Assert.False(cm.IsFullyConstrained()); // max 0 → 0 < 0 is false
}
[Fact]
public void AdjustOffset_InBand_AppliesLinearTaper()
{
var (host, cm) = Setup();
host.SetOrigin(Vector3.Zero);
host.InContact = true;
cm.ConstrainTo(Anchor(5f), startDistance: 2f, maxDistance: 10f); // offset 5 in (2,10)
var frame = new MotionDeltaFrame { Origin = new Vector3(1f, 0f, 0f) };
cm.AdjustOffset(frame, 0.1);
// taper = (10-5)/(10-2) = 5/8 = 0.625.
Assert.Equal(0.625f, frame.Origin.X, 3);
Assert.Equal(0.625f, cm.ConstraintPosOffset, 3); // recomputed = |offset|
}
[Fact]
public void AdjustOffset_PastMax_HardClampsToZero()
{
var (host, cm) = Setup();
host.SetOrigin(Vector3.Zero);
host.InContact = true;
cm.ConstrainTo(Anchor(20f), 2f, 10f); // offset 20 >= max 10
var frame = new MotionDeltaFrame { Origin = new Vector3(1f, 0f, 0f) };
cm.AdjustOffset(frame, 0.1);
Assert.Equal(Vector3.Zero, frame.Origin);
}
[Fact]
public void AdjustOffset_BelowStart_PassesThrough()
{
var (host, cm) = Setup();
host.SetOrigin(Vector3.Zero);
host.InContact = true;
cm.ConstrainTo(Anchor(1f), startDistance: 2f, maxDistance: 10f); // offset 1 < start 2
var frame = new MotionDeltaFrame { Origin = new Vector3(1f, 0f, 0f) };
cm.AdjustOffset(frame, 0.1);
Assert.Equal(1f, frame.Origin.X, 3); // unscaled
Assert.Equal(1f, cm.ConstraintPosOffset, 3);
}
[Fact]
public void AdjustOffset_Airborne_SkipsClampButStillTracksLength()
{
var (host, cm) = Setup();
host.SetOrigin(Vector3.Zero);
host.InContact = false; // airborne
cm.ConstrainTo(Anchor(20f), 2f, 10f); // would clamp if grounded
var frame = new MotionDeltaFrame { Origin = new Vector3(3f, 4f, 0f) };
cm.AdjustOffset(frame, 0.1);
Assert.Equal(new Vector3(3f, 4f, 0f), frame.Origin); // untouched while airborne
Assert.Equal(5f, cm.ConstraintPosOffset, 3); // |(3,4,0)| = 5, still updated
}
[Fact]
public void AdjustOffset_NotConstrained_IsNoOp()
{
var (host, cm) = Setup();
var frame = new MotionDeltaFrame { Origin = new Vector3(7f, 0f, 0f) };
cm.AdjustOffset(frame, 0.1);
Assert.Equal(new Vector3(7f, 0f, 0f), frame.Origin);
}
[Fact]
public void UnConstrain_ClearsFlag()
{
var (host, cm) = Setup();
cm.ConstrainTo(Anchor(5f), 2f, 10f);
cm.UnConstrain();
Assert.False(cm.IsConstrained);
}
}