test(anim): PlayAction conformance — Action, Modifier, Emote
Four new tests covering the PlayAction routing paths that the new UpdateMotion Commands[] handler relies on: - PlayAction_Action_ResolvesFromLinksDict — a ThrustMed attack in SwordCombat stance resolves via Links[(SwordCombat, Ready)][ThrustMed] and its anim frames become visible after PlayAction is called. - PlayAction_Modifier_ResolvesFromModifiersDict — Jump (0x2500003B, Modifier class) resolves via Modifiers[(Style, Jump)] and its anim plays on top of the current cycle. - PlayAction_Emote_RoutesThroughActionBranch — Wave (0x13000087, class byte 0x13 = Action | ChatEmote | Mappable) goes through the Action branch because the Action bit is set, resolving from Links just like attacks. Validates the class-bit math. - PlayAction_NoEntryInTable_IsNoOp — silent no-op when the table has no entry for the motion, with the queue length unchanged. Together these lock in that the same PlayAction path correctly routes the three major one-shot classes the Commands[] handler fans out to NPCs and remote players. 658 tests green (was 654). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
11649da1cf
commit
6e589d3b89
1 changed files with 159 additions and 0 deletions
|
|
@ -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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Expose _framePosition (double) via reflection (test-only).</summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue