diff --git a/tests/AcDream.Core.Tests/Physics/AnimationSequencerTests.cs b/tests/AcDream.Core.Tests/Physics/AnimationSequencerTests.cs index 717ec2e..8292bbd 100644 --- a/tests/AcDream.Core.Tests/Physics/AnimationSequencerTests.cs +++ b/tests/AcDream.Core.Tests/Physics/AnimationSequencerTests.cs @@ -1174,6 +1174,165 @@ public sealed class AnimationSequencerTests Assert.Equal(1.5f, seq.CurrentSpeedMod, 3); } + // ── PlayAction: Action / Modifier / ChatEmote routing ─────────────────── + + [Fact] + public void PlayAction_Action_ResolvesFromLinksDict() + { + // An Action-class command (mask 0x10) resolves via the Links dict + // keyed by (style, currentSubstate) → motion. Example: a ThrustMed + // attack while in SwordCombat stance. + const uint Style = 0x003Eu; // SwordCombat + const uint IdleMotion = 0x41000003u; // Ready + const uint ActionMotion = 0x10000058u; // ThrustMed (Action class) + const uint IdleAnimId = 0x03000501u; + const uint ActionAnimId= 0x03000502u; + + var idleAnim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity); + // Action anim: distinct non-zero origin so we can detect it played. + var actionAnim = Fixtures.MakeAnim(3, 1, new Vector3(99, 0, 0), Quaternion.Identity); + + var setup = Fixtures.MakeSetup(1); + var mt = new MotionTable(); + mt.DefaultStyle = (DRWMotionCommand)Style; + int cycleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu)); + mt.Cycles[cycleKey] = Fixtures.MakeMotionData(IdleAnimId, framerate: 10f); + + // Link: (SwordCombat, Ready) → ThrustMed + int linkOuter = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu)); + var cmdData = new MotionCommandData(); + cmdData.MotionData[(int)ActionMotion] = Fixtures.MakeMotionData(ActionAnimId, framerate: 10f); + mt.Links[linkOuter] = cmdData; + + var loader = new FakeLoader(); + loader.Register(IdleAnimId, idleAnim); + loader.Register(ActionAnimId, actionAnim); + + var seq = new AnimationSequencer(setup, mt, loader); + seq.SetCycle(Style, IdleMotion); + seq.Advance(0.01f); // burn the first idle frame + + // Fire the action. + seq.PlayAction(ActionMotion); + + // After a small advance, we should be reading the action anim (origin X=99). + var fr = seq.Advance(0.01f); + Assert.Single(fr); + Assert.Equal(99f, fr[0].Origin.X, 1); + } + + [Fact] + public void PlayAction_Modifier_ResolvesFromModifiersDict() + { + // A Modifier-class command (mask 0x20) — like Jump (0x2500003B) — + // resolves from the Modifiers dict, first with style-specific key + // then with unstyled fallback. Empirically: the modifier's anim + // plays on top of the current cycle. + const uint Style = 0x003Du; + const uint IdleMotion = 0x41000003u; + const uint JumpMotion = 0x2500003Bu; // Modifier class + const uint IdleAnimId = 0x03000510u; + const uint JumpAnimId = 0x03000511u; + + var idleAnim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity); + var jumpAnim = Fixtures.MakeAnim(3, 1, new Vector3(0, 0, 77), Quaternion.Identity); + + var setup = Fixtures.MakeSetup(1); + var mt = new MotionTable(); + mt.DefaultStyle = (DRWMotionCommand)Style; + int cycleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu)); + mt.Cycles[cycleKey] = Fixtures.MakeMotionData(IdleAnimId, framerate: 10f); + + // Modifier: (Style, Jump) + int modKey = (int)((Style << 16) | (JumpMotion & 0xFFFFFFu)); + mt.Modifiers[modKey] = Fixtures.MakeMotionData(JumpAnimId, framerate: 10f); + + var loader = new FakeLoader(); + loader.Register(IdleAnimId, idleAnim); + loader.Register(JumpAnimId, jumpAnim); + + var seq = new AnimationSequencer(setup, mt, loader); + seq.SetCycle(Style, IdleMotion); + + seq.PlayAction(JumpMotion); + + var fr = seq.Advance(0.01f); + Assert.Single(fr); + Assert.Equal(77f, fr[0].Origin.Z, 1); + } + + [Fact] + public void PlayAction_Emote_RoutesThroughActionBranch() + { + // ChatEmotes like Wave (0x13000087) have class byte 0x13 = + // Action(0x10) | ChatEmote(0x02) | Mappable(0x01). Because the + // Action bit is set, they route through the Links-dict lookup just + // like attacks. Verifies the class-bit math. + const uint Style = 0x003Du; + const uint IdleMotion = 0x41000003u; + const uint WaveMotion = 0x13000087u; + const uint IdleAnimId = 0x03000520u; + const uint WaveAnimId = 0x03000521u; + + var idleAnim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity); + var waveAnim = Fixtures.MakeAnim(5, 1, new Vector3(0, 55, 0), Quaternion.Identity); + + var setup = Fixtures.MakeSetup(1); + var mt = new MotionTable(); + mt.DefaultStyle = (DRWMotionCommand)Style; + int cycleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu)); + mt.Cycles[cycleKey] = Fixtures.MakeMotionData(IdleAnimId, framerate: 10f); + + // Register Links[(style, Ready)][Wave] = wave anim. + int linkOuter = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu)); + var cmdData = new MotionCommandData(); + cmdData.MotionData[(int)WaveMotion] = Fixtures.MakeMotionData(WaveAnimId, framerate: 10f); + mt.Links[linkOuter] = cmdData; + + var loader = new FakeLoader(); + loader.Register(IdleAnimId, idleAnim); + loader.Register(WaveAnimId, waveAnim); + + var seq = new AnimationSequencer(setup, mt, loader); + seq.SetCycle(Style, IdleMotion); + + seq.PlayAction(WaveMotion); + + var fr = seq.Advance(0.01f); + Assert.Single(fr); + Assert.Equal(55f, fr[0].Origin.Y, 1); + } + + [Fact] + public void PlayAction_NoEntryInTable_IsNoOp() + { + // If neither Links nor Modifiers has the motion, PlayAction should + // silently return without disturbing the current cycle. + const uint Style = 0x003Du; + const uint IdleMotion = 0x41000003u; + const uint IdleAnimId = 0x03000530u; + const uint UnknownAction = 0x10001234u; + + var idleAnim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity); + var setup = Fixtures.MakeSetup(1); + var mt = new MotionTable(); + mt.DefaultStyle = (DRWMotionCommand)Style; + int cycleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu)); + mt.Cycles[cycleKey] = Fixtures.MakeMotionData(IdleAnimId, framerate: 10f); + + var loader = new FakeLoader(); + loader.Register(IdleAnimId, idleAnim); + + var seq = new AnimationSequencer(setup, mt, loader); + seq.SetCycle(Style, IdleMotion); + seq.Advance(0.05f); + int queueBefore = seq.QueueCount; + + seq.PlayAction(UnknownAction); // unknown motion → no-op + + Assert.Equal(queueBefore, seq.QueueCount); + } + // ── Helpers ────────────────────────────────────────────────────────────── /// Expose _framePosition (double) via reflection (test-only).