fix(animation): restore retail transition sign gates
Match the v11.4186 CSequence transition assembly and ACE cross-reference, including direction-specific pose gates and the strict physics epsilon boundary. Add conformance tests and correct the stale research interpretation. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
c5ab99081c
commit
31a0889f08
4 changed files with 155 additions and 42 deletions
|
|
@ -17,8 +17,8 @@ namespace AcDream.Core.Tests.Physics.Motion;
|
|||
/// Skeleton per the ACE-verified structure (P0-pins.md): overshoot →
|
||||
/// clamp to the RAW high/low frame + leftover carry + animDone →
|
||||
/// per-crossed-frame pose/physics/hook loop → AnimDone gate
|
||||
/// (list HEAD != first_cyclic) → advance (pose-out both directions,
|
||||
/// forward wraps to first_cyclic, reverse wraps to the LIST TAIL) →
|
||||
/// (list HEAD != first_cyclic) → advance (sign-gated pose transition,
|
||||
/// forward wraps to first_cyclic, negative elapsed wraps to the LIST TAIL) →
|
||||
/// carry the leftover (P0 pin) → loop. NO safety cap, NO boundary
|
||||
/// epsilon.
|
||||
/// </summary>
|
||||
|
|
@ -68,6 +68,24 @@ public class CSequenceUpdateTests
|
|||
return new AnimData { AnimId = qid, LowFrame = low, HighFrame = high, Framerate = framerate };
|
||||
}
|
||||
|
||||
private static Animation MakePoseAnim(float poseX)
|
||||
{
|
||||
var anim = new Animation();
|
||||
var part = new AnimationFrame(1u);
|
||||
part.Frames.Add(new Frame
|
||||
{
|
||||
Origin = Vector3.Zero,
|
||||
Orientation = Quaternion.Identity,
|
||||
});
|
||||
anim.PartFrames.Add(part);
|
||||
anim.PosFrames.Add(new Frame
|
||||
{
|
||||
Origin = new Vector3(poseX, 0f, 0f),
|
||||
Orientation = Quaternion.Identity,
|
||||
});
|
||||
return anim;
|
||||
}
|
||||
|
||||
private static (CSequence seq, RecordingHookQueue hooks) Cyclic(int frames, float framerate = 30f)
|
||||
{
|
||||
var loader = new MapLoader();
|
||||
|
|
@ -195,6 +213,75 @@ public class CSequenceUpdateTests
|
|||
Assert.Equal(3, seq.CurrAnim!.HighFrame); // B (4 frames → high 3)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdvanceToNextAnimation_PositiveElapsed_SkipsPositiveOutgoingPose()
|
||||
{
|
||||
var loader = new MapLoader();
|
||||
loader.Add(1u, MakePoseAnim(1f));
|
||||
loader.Add(2u, MakePoseAnim(2f));
|
||||
var seq = new CSequence(loader);
|
||||
seq.AppendAnimation(Ad(1u, framerate: 30f, high: 0));
|
||||
seq.AppendAnimation(Ad(2u, framerate: 30f, high: 0));
|
||||
seq.FrameNumber = 0;
|
||||
var frame = new Frame
|
||||
{
|
||||
Origin = new Vector3(10f, 0f, 0f),
|
||||
Orientation = Quaternion.Identity,
|
||||
};
|
||||
|
||||
seq.AdvanceToNextAnimation(timeElapsed: 0.1, frame);
|
||||
|
||||
// Retail's positive-elapsed branch removes the outgoing pose only
|
||||
// when that node plays backward. The positive incoming pose applies.
|
||||
Assert.Equal(12f, frame.Origin.X, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdvanceToNextAnimation_NegativeElapsed_SkipsNegativeOutgoingPose()
|
||||
{
|
||||
var loader = new MapLoader();
|
||||
loader.Add(1u, MakePoseAnim(1f));
|
||||
loader.Add(2u, MakePoseAnim(2f));
|
||||
var seq = new CSequence(loader);
|
||||
seq.AppendAnimation(Ad(1u, framerate: -30f, high: 0));
|
||||
seq.AppendAnimation(Ad(2u, framerate: -30f, high: 0));
|
||||
seq.FrameNumber = 0;
|
||||
var frame = new Frame
|
||||
{
|
||||
Origin = new Vector3(10f, 0f, 0f),
|
||||
Orientation = Quaternion.Identity,
|
||||
};
|
||||
|
||||
seq.AdvanceToNextAnimation(timeElapsed: -0.1, frame);
|
||||
|
||||
// Retail's negative-elapsed branch removes only a non-negative
|
||||
// outgoing node, then applies the negative incoming node.
|
||||
Assert.Equal(12f, frame.Origin.X, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdvanceToNextAnimation_ExactEpsilon_AppliesPoseButNotPhysics()
|
||||
{
|
||||
var loader = new MapLoader();
|
||||
loader.Add(1u, MakePoseAnim(1f));
|
||||
loader.Add(2u, MakePoseAnim(2f));
|
||||
var seq = new CSequence(loader);
|
||||
seq.AppendAnimation(Ad(1u, framerate: 30f, high: 0));
|
||||
seq.AppendAnimation(Ad(2u, framerate: FrameOps.FEpsilon, high: 0));
|
||||
seq.SetVelocity(Vector3.UnitX);
|
||||
var frame = new Frame
|
||||
{
|
||||
Origin = new Vector3(10f, 0f, 0f),
|
||||
Orientation = Quaternion.Identity,
|
||||
};
|
||||
|
||||
seq.AdvanceToNextAnimation(timeElapsed: 0.1, frame);
|
||||
|
||||
// The sign gate owns pose application. Physics has the separate,
|
||||
// strict abs(framerate) > F_EPSILON boundary.
|
||||
Assert.Equal(12f, frame.Origin.X, 5);
|
||||
}
|
||||
|
||||
// ── stationary / degenerate (G8 + else-branch) ──────────────────────
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue