fix(combat): #159 — derive CombatAnimationMotionCommands from the DRW enum

CombatAnimationPlanner.CombatAnimationMotionCommands hand-transcribed the
combat MotionCommand constants from the Sept 2013 EoR decomp. The whole
late-combat block (Offhand* / Attack4-6 / Punch*) is numbered +3 lower there
than in ACE/DatReaderWriter — a contiguous low-word shift that begins around
SnowAngelState (0x115). So against a live ACE server every one of those 40
commands misclassified: the resolver returned the correct ACE value (e.g.
wire 0x0173 -> 0x10000173 OffhandSlashHigh) but the planner's constant held
the 2013 value (0x10000170), so no switch case matched. Reload was worse —
hardcoded 0x100000D4, a value absent from DRW entirely; the real ACE/DRW
Reload is the 0x40000016 SubState.

Instead of re-transcribing 40 hex constants (exactly how the drift crept in),
derive each constant directly from DatReaderWriter.Enums.MotionCommand by
name: `= (uint)Drw.Name`. The value IS the oracle by construction, can never
drift from the wire again, and ~80 magic numbers are gone. Ground truth was
taken by reflecting over the same DatReaderWriter 2.1.7 assembly the runtime
binds (409 enum values). Blocks 1-2 (stances + single melee, 0x3C-0x12A) were
already correct and are unchanged in value.

Tests: new PlanFromWireCommand_LateCombatBlock_UsesAceDrwNumbering pins the
full wire -> resolve -> classify pipeline for 14 late-block ACE wire values
(both full value and kind), plus a Reload SubState parity fact. Fixed two rows
in ClassifyMotionCommand_RecognisesRetailCombatCommands whose literal values
change meaning under DRW numbering (0x1000018E is AttackLow6/CreatureAttack,
not PunchFastLow; Reload moves to 0x40000016). Core suite 2499 green.

Note: CombatAnimationPlanner is not yet wired into the runtime dispatch (the
live path is AnimationCommandRouter -> MotionInterpreter/AnimationSequencer),
so this is a latent-correctness fix — it does not by itself change the #170
Mite Scamp symptom (whose attacks 0x62-0x64 live in the already-correct block).

Ref: docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-04 09:08:11 +02:00
parent e4155cd962
commit 2de5a011b4
2 changed files with 191 additions and 128 deletions

View file

@ -1,4 +1,5 @@
using AcDream.Core.Physics; using AcDream.Core.Physics;
using Drw = DatReaderWriter.Enums.MotionCommand;
namespace AcDream.Core.Combat; namespace AcDream.Core.Combat;
@ -200,109 +201,146 @@ public enum CombatAnimationKind
Death, Death,
} }
/// <summary>
/// The full 32-bit retail MotionCommand values the planner classifies, each
/// derived DIRECTLY from <see cref="Drw"/> (Chorizite.DatReaderWriter — the
/// same enum ACE broadcasts and that the local DAT MotionTables key on) so
/// the values can never drift from the wire again.
///
/// <para>
/// History (ISSUES #159): these were previously hand-transcribed from the
/// Sept 2013 EoR decomp's <c>command_ids</c> table, which numbers the whole
/// late-combat block (<c>Offhand*</c> / <c>Attack4-6</c> / <c>Punch*</c>)
/// THREE lower than ACE/DRW — a contiguous low-word "+3" shift that begins
/// around <c>SnowAngelState</c> (0x115). Against a live ACE server those
/// commands silently misclassified: the resolver returned the correct ACE
/// value (e.g. wire 0x0173 -&gt; <c>0x10000173</c> <c>OffhandSlashHigh</c>),
/// but the planner's constant held the 2013 value (<c>0x10000170</c>), so no
/// match. <c>Reload</c> was worse — hardcoded <c>0x100000D4</c>, a value
/// absent from DRW entirely; the real ACE/DRW <c>Reload</c> is the
/// <c>0x40000016</c> SubState. Deriving from the enum by name fixes the whole
/// block at once and drops ~80 magic numbers.
/// See <c>docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md</c>.
/// </para>
/// </summary>
internal static class CombatAnimationMotionCommands internal static class CombatAnimationMotionCommands
{ {
public const uint HandCombat = 0x8000003Cu; // Combat stances (Style class 0x80). Already correct before #159.
public const uint SwordCombat = 0x8000003Eu; public const uint HandCombat = (uint)Drw.HandCombat;
public const uint BowCombat = 0x8000003Fu; public const uint SwordCombat = (uint)Drw.SwordCombat;
public const uint SwordShieldCombat = 0x80000040u; public const uint BowCombat = (uint)Drw.BowCombat;
public const uint CrossbowCombat = 0x80000041u; public const uint SwordShieldCombat = (uint)Drw.SwordShieldCombat;
public const uint SlingCombat = 0x80000043u; public const uint CrossbowCombat = (uint)Drw.CrossbowCombat;
public const uint TwoHandedSwordCombat = 0x80000044u; public const uint SlingCombat = (uint)Drw.SlingCombat;
public const uint TwoHandedStaffCombat = 0x80000045u; public const uint TwoHandedSwordCombat = (uint)Drw.TwoHandedSwordCombat;
public const uint DualWieldCombat = 0x80000046u; public const uint TwoHandedStaffCombat = (uint)Drw.TwoHandedStaffCombat;
public const uint ThrownWeaponCombat = 0x80000047u; public const uint DualWieldCombat = (uint)Drw.DualWieldCombat;
public const uint Magic = 0x80000049u; public const uint ThrownWeaponCombat = (uint)Drw.ThrownWeaponCombat;
public const uint AtlatlCombat = 0x8000013Bu; public const uint Magic = (uint)Drw.Magic;
public const uint ThrownShieldCombat = 0x8000013Cu; public const uint AtlatlCombat = (uint)Drw.AtlatlCombat;
public const uint ThrownShieldCombat = (uint)Drw.ThrownShieldCombat;
public const uint FallDown = 0x10000050u; // Hit reactions / staggers (Action class 0x10, unshifted).
public const uint Twitch1 = 0x10000051u; public const uint FallDown = (uint)Drw.FallDown;
public const uint Twitch2 = 0x10000052u; public const uint Twitch1 = (uint)Drw.Twitch1;
public const uint Twitch3 = 0x10000053u; public const uint Twitch2 = (uint)Drw.Twitch2;
public const uint Twitch4 = 0x10000054u; public const uint Twitch3 = (uint)Drw.Twitch3;
public const uint StaggerBackward = 0x10000055u; public const uint Twitch4 = (uint)Drw.Twitch4;
public const uint StaggerForward = 0x10000056u; public const uint StaggerBackward = (uint)Drw.StaggerBackward;
public const uint Sanctuary = 0x10000057u; public const uint StaggerForward = (uint)Drw.StaggerForward;
public const uint ThrustMed = 0x10000058u; public const uint Sanctuary = (uint)Drw.Sanctuary;
public const uint ThrustLow = 0x10000059u;
public const uint ThrustHigh = 0x1000005Au;
public const uint SlashHigh = 0x1000005Bu;
public const uint SlashMed = 0x1000005Cu;
public const uint SlashLow = 0x1000005Du;
public const uint BackhandHigh = 0x1000005Eu;
public const uint BackhandMed = 0x1000005Fu;
public const uint BackhandLow = 0x10000060u;
public const uint Shoot = 0x10000061u;
public const uint AttackHigh1 = 0x10000062u;
public const uint AttackMed1 = 0x10000063u;
public const uint AttackLow1 = 0x10000064u;
public const uint AttackHigh2 = 0x10000065u;
public const uint AttackMed2 = 0x10000066u;
public const uint AttackLow2 = 0x10000067u;
public const uint AttackHigh3 = 0x10000068u;
public const uint AttackMed3 = 0x10000069u;
public const uint AttackLow3 = 0x1000006Au;
public const uint MissileAttack1 = 0x100000D0u; // Single melee attacks (Action class 0x10, unshifted).
public const uint MissileAttack2 = 0x100000D1u; public const uint ThrustMed = (uint)Drw.ThrustMed;
public const uint MissileAttack3 = 0x100000D2u; public const uint ThrustLow = (uint)Drw.ThrustLow;
public const uint CastSpell = 0x400000D3u; public const uint ThrustHigh = (uint)Drw.ThrustHigh;
public const uint Reload = 0x100000D4u; public const uint SlashHigh = (uint)Drw.SlashHigh;
public const uint UseMagicStaff = 0x400000E0u; public const uint SlashMed = (uint)Drw.SlashMed;
public const uint UseMagicWand = 0x400000E1u; public const uint SlashLow = (uint)Drw.SlashLow;
public const uint BackhandHigh = (uint)Drw.BackhandHigh;
public const uint BackhandMed = (uint)Drw.BackhandMed;
public const uint BackhandLow = (uint)Drw.BackhandLow;
public const uint DoubleSlashLow = 0x1000011Fu; // Missile attacks (Action 0x10) + reload (SubState 0x40, NOT the dead
public const uint DoubleSlashMed = 0x10000120u; // 2013 0x100000D4 — the real DRW Reload is 0x40000016).
public const uint DoubleSlashHigh = 0x10000121u; public const uint Shoot = (uint)Drw.Shoot;
public const uint TripleSlashLow = 0x10000122u; public const uint MissileAttack1 = (uint)Drw.MissileAttack1;
public const uint TripleSlashMed = 0x10000123u; public const uint MissileAttack2 = (uint)Drw.MissileAttack2;
public const uint TripleSlashHigh = 0x10000124u; public const uint MissileAttack3 = (uint)Drw.MissileAttack3;
public const uint DoubleThrustLow = 0x10000125u; public const uint Reload = (uint)Drw.Reload;
public const uint DoubleThrustMed = 0x10000126u;
public const uint DoubleThrustHigh = 0x10000127u;
public const uint TripleThrustLow = 0x10000128u;
public const uint TripleThrustMed = 0x10000129u;
public const uint TripleThrustHigh = 0x1000012Au;
public const uint OffhandSlashHigh = 0x10000170u; // Creature attacks 1-6 (Action class 0x10). 1-3 are the unshifted low
public const uint OffhandSlashMed = 0x10000171u; // block (0x62-0x6A); 4-6 live in the shifted late block (0x186+).
public const uint OffhandSlashLow = 0x10000172u; public const uint AttackHigh1 = (uint)Drw.AttackHigh1;
public const uint OffhandThrustHigh = 0x10000173u; public const uint AttackMed1 = (uint)Drw.AttackMed1;
public const uint OffhandThrustMed = 0x10000174u; public const uint AttackLow1 = (uint)Drw.AttackLow1;
public const uint OffhandThrustLow = 0x10000175u; public const uint AttackHigh2 = (uint)Drw.AttackHigh2;
public const uint OffhandDoubleSlashLow = 0x10000176u; public const uint AttackMed2 = (uint)Drw.AttackMed2;
public const uint OffhandDoubleSlashMed = 0x10000177u; public const uint AttackLow2 = (uint)Drw.AttackLow2;
public const uint OffhandDoubleSlashHigh = 0x10000178u; public const uint AttackHigh3 = (uint)Drw.AttackHigh3;
public const uint OffhandTripleSlashLow = 0x10000179u; public const uint AttackMed3 = (uint)Drw.AttackMed3;
public const uint OffhandTripleSlashMed = 0x1000017Au; public const uint AttackLow3 = (uint)Drw.AttackLow3;
public const uint OffhandTripleSlashHigh = 0x1000017Bu; public const uint AttackHigh4 = (uint)Drw.AttackHigh4;
public const uint OffhandDoubleThrustLow = 0x1000017Cu; public const uint AttackMed4 = (uint)Drw.AttackMed4;
public const uint OffhandDoubleThrustMed = 0x1000017Du; public const uint AttackLow4 = (uint)Drw.AttackLow4;
public const uint OffhandDoubleThrustHigh = 0x1000017Eu; public const uint AttackHigh5 = (uint)Drw.AttackHigh5;
public const uint OffhandTripleThrustLow = 0x1000017Fu; public const uint AttackMed5 = (uint)Drw.AttackMed5;
public const uint OffhandTripleThrustMed = 0x10000180u; public const uint AttackLow5 = (uint)Drw.AttackLow5;
public const uint OffhandTripleThrustHigh = 0x10000181u; public const uint AttackHigh6 = (uint)Drw.AttackHigh6;
public const uint OffhandKick = 0x10000182u; public const uint AttackMed6 = (uint)Drw.AttackMed6;
public const uint AttackHigh4 = 0x10000183u; public const uint AttackLow6 = (uint)Drw.AttackLow6;
public const uint AttackMed4 = 0x10000184u;
public const uint AttackLow4 = 0x10000185u; // Spell casts (SubState class 0x40).
public const uint AttackHigh5 = 0x10000186u; public const uint CastSpell = (uint)Drw.CastSpell;
public const uint AttackMed5 = 0x10000187u; public const uint UseMagicStaff = (uint)Drw.UseMagicStaff;
public const uint AttackLow5 = 0x10000188u; public const uint UseMagicWand = (uint)Drw.UseMagicWand;
public const uint AttackHigh6 = 0x10000189u;
public const uint AttackMed6 = 0x1000018Au; // Multi-strike melee (Action class 0x10, unshifted 0x11F-0x12A).
public const uint AttackLow6 = 0x1000018Bu; public const uint DoubleSlashLow = (uint)Drw.DoubleSlashLow;
public const uint PunchFastHigh = 0x1000018Cu; public const uint DoubleSlashMed = (uint)Drw.DoubleSlashMed;
public const uint PunchFastMed = 0x1000018Du; public const uint DoubleSlashHigh = (uint)Drw.DoubleSlashHigh;
public const uint PunchFastLow = 0x1000018Eu; public const uint TripleSlashLow = (uint)Drw.TripleSlashLow;
public const uint PunchSlowHigh = 0x1000018Fu; public const uint TripleSlashMed = (uint)Drw.TripleSlashMed;
public const uint PunchSlowMed = 0x10000190u; public const uint TripleSlashHigh = (uint)Drw.TripleSlashHigh;
public const uint PunchSlowLow = 0x10000191u; public const uint DoubleThrustLow = (uint)Drw.DoubleThrustLow;
public const uint OffhandPunchFastHigh = 0x10000192u; public const uint DoubleThrustMed = (uint)Drw.DoubleThrustMed;
public const uint OffhandPunchFastMed = 0x10000193u; public const uint DoubleThrustHigh = (uint)Drw.DoubleThrustHigh;
public const uint OffhandPunchFastLow = 0x10000194u; public const uint TripleThrustLow = (uint)Drw.TripleThrustLow;
public const uint OffhandPunchSlowHigh = 0x10000195u; public const uint TripleThrustMed = (uint)Drw.TripleThrustMed;
public const uint OffhandPunchSlowMed = 0x10000196u; public const uint TripleThrustHigh = (uint)Drw.TripleThrustHigh;
public const uint OffhandPunchSlowLow = 0x10000197u;
// Offhand strikes (Action class 0x10, shifted late block 0x173+).
public const uint OffhandSlashHigh = (uint)Drw.OffhandSlashHigh;
public const uint OffhandSlashMed = (uint)Drw.OffhandSlashMed;
public const uint OffhandSlashLow = (uint)Drw.OffhandSlashLow;
public const uint OffhandThrustHigh = (uint)Drw.OffhandThrustHigh;
public const uint OffhandThrustMed = (uint)Drw.OffhandThrustMed;
public const uint OffhandThrustLow = (uint)Drw.OffhandThrustLow;
public const uint OffhandDoubleSlashLow = (uint)Drw.OffhandDoubleSlashLow;
public const uint OffhandDoubleSlashMed = (uint)Drw.OffhandDoubleSlashMed;
public const uint OffhandDoubleSlashHigh = (uint)Drw.OffhandDoubleSlashHigh;
public const uint OffhandTripleSlashLow = (uint)Drw.OffhandTripleSlashLow;
public const uint OffhandTripleSlashMed = (uint)Drw.OffhandTripleSlashMed;
public const uint OffhandTripleSlashHigh = (uint)Drw.OffhandTripleSlashHigh;
public const uint OffhandDoubleThrustLow = (uint)Drw.OffhandDoubleThrustLow;
public const uint OffhandDoubleThrustMed = (uint)Drw.OffhandDoubleThrustMed;
public const uint OffhandDoubleThrustHigh = (uint)Drw.OffhandDoubleThrustHigh;
public const uint OffhandTripleThrustLow = (uint)Drw.OffhandTripleThrustLow;
public const uint OffhandTripleThrustMed = (uint)Drw.OffhandTripleThrustMed;
public const uint OffhandTripleThrustHigh = (uint)Drw.OffhandTripleThrustHigh;
public const uint OffhandKick = (uint)Drw.OffhandKick;
// Punches (Action class 0x10, shifted late block).
public const uint PunchFastHigh = (uint)Drw.PunchFastHigh;
public const uint PunchFastMed = (uint)Drw.PunchFastMed;
public const uint PunchFastLow = (uint)Drw.PunchFastLow;
public const uint PunchSlowHigh = (uint)Drw.PunchSlowHigh;
public const uint PunchSlowMed = (uint)Drw.PunchSlowMed;
public const uint PunchSlowLow = (uint)Drw.PunchSlowLow;
public const uint OffhandPunchFastHigh = (uint)Drw.OffhandPunchFastHigh;
public const uint OffhandPunchFastMed = (uint)Drw.OffhandPunchFastMed;
public const uint OffhandPunchFastLow = (uint)Drw.OffhandPunchFastLow;
public const uint OffhandPunchSlowHigh = (uint)Drw.OffhandPunchSlowHigh;
public const uint OffhandPunchSlowMed = (uint)Drw.OffhandPunchSlowMed;
public const uint OffhandPunchSlowLow = (uint)Drw.OffhandPunchSlowLow;
} }

View file

@ -9,12 +9,12 @@ public sealed class CombatAnimationPlannerTests
[Theory] [Theory]
[InlineData(0x10000058u, CombatAnimationKind.MeleeSwing)] // ThrustMed [InlineData(0x10000058u, CombatAnimationKind.MeleeSwing)] // ThrustMed
[InlineData(0x1000005Bu, CombatAnimationKind.MeleeSwing)] // SlashHigh [InlineData(0x1000005Bu, CombatAnimationKind.MeleeSwing)] // SlashHigh
[InlineData(0x1000017Du, CombatAnimationKind.MeleeSwing)] // OffhandDoubleThrustMed [InlineData(0x1000017Du, CombatAnimationKind.MeleeSwing)] // OffhandTripleSlashMed (DRW)
[InlineData(0x1000018Eu, CombatAnimationKind.MeleeSwing)] // PunchFastLow [InlineData(0x1000018Eu, CombatAnimationKind.CreatureAttack)] // AttackLow6 (DRW) — was mislabelled PunchFastLow under 2013 numbering
[InlineData(0x10000061u, CombatAnimationKind.MissileAttack)] // Shoot [InlineData(0x10000061u, CombatAnimationKind.MissileAttack)] // Shoot
[InlineData(0x100000D4u, CombatAnimationKind.MissileAttack)] // Reload [InlineData(0x40000016u, CombatAnimationKind.MissileAttack)] // Reload (DRW SubState) — was the dead 2013 value 0x100000D4
[InlineData(0x10000062u, CombatAnimationKind.CreatureAttack)] // AttackHigh1 [InlineData(0x10000062u, CombatAnimationKind.CreatureAttack)] // AttackHigh1
[InlineData(0x1000018Bu, CombatAnimationKind.CreatureAttack)] // AttackLow6 [InlineData(0x1000018Bu, CombatAnimationKind.CreatureAttack)] // AttackLow5 (DRW)
[InlineData(0x400000D3u, CombatAnimationKind.SpellCast)] // CastSpell [InlineData(0x400000D3u, CombatAnimationKind.SpellCast)] // CastSpell
[InlineData(0x400000E0u, CombatAnimationKind.SpellCast)] // UseMagicStaff [InlineData(0x400000E0u, CombatAnimationKind.SpellCast)] // UseMagicStaff
[InlineData(0x10000051u, CombatAnimationKind.HitReaction)] // Twitch1 [InlineData(0x10000051u, CombatAnimationKind.HitReaction)] // Twitch1
@ -30,32 +30,13 @@ public sealed class CombatAnimationPlannerTests
Assert.Equal(expected, CombatAnimationPlanner.ClassifyMotionCommand(command)); Assert.Equal(expected, CombatAnimationPlanner.ClassifyMotionCommand(command));
} }
// L.1b correction (2026-06-30): these four expected values were pinned // These pin the RESOLVER (wire u16 -> full 32-bit) against the DRW enum,
// against MotionCommandResolver's old blind per-range override // independent of the planner. wire 0x0170 is IssueSlashCommand = 0x09000170
// (0x016E-0x0197 force-mapped to class 0x10000000), not against named // (class 0x09, UI command) — there is no Action-class (0x10) entry at that
// retail truth. The override is gone — MotionCommandResolver now // wire; the real OffhandSlashHigh is 0x10000173 (the 2013 decomp numbers it
// delegates to AceModernCommandCatalog, built cleanly from // 0x10000170, +3 low). See docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md.
// DatReaderWriter.Enums.MotionCommand (Chorizite.DatReaderWriter
// 2.1.7). Per that enum, wire 0x0170 is IssueSlashCommand = 0x09000170
// (class 0x09, UI command) — there is no Action-class (0x10) entry at
// that wire at all, so the old "OffhandSlashHigh" expectation here was
// simply wrong (real OffhandSlashHigh = 0x10000173, see
// docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md). Updated
// to the wire's real DRW-enum identity.
//
// NOTE: the surviving rows (0x017D/0x018B/0x018E) still pass, but only
// because they land on SOME Action-class value at that low word — their
// inline comments ("OffhandDoubleThrustMed"/"AttackLow6"/"PunchFastLow")
// were also wrong names (the real DRW names are
// OffhandTripleSlashMed/AttackLow5/AttackLow6 respectively). This
// reveals that CombatAnimationPlanner.CombatAnimationMotionCommands
// (src/AcDream.Core/Combat/CombatAnimationPlanner.cs:268-307) is itself
// built from 2013-decomp numbering, not ACE/DRW numbering — a
// pre-existing, separate bug outside this slice's scope (catalog only;
// CombatAnimationPlanner is explicitly out of bounds for L.1b). Flagged
// for follow-up rather than silently patched.
[Theory] [Theory]
[InlineData(0x0170, 0x09000170u)] // IssueSlashCommand (UI class) — see note above [InlineData(0x0170, 0x09000170u)] // IssueSlashCommand (UI class), NOT OffhandSlashHigh
[InlineData(0x017D, 0x1000017Du)] // OffhandTripleSlashMed [InlineData(0x017D, 0x1000017Du)] // OffhandTripleSlashMed
[InlineData(0x018B, 0x1000018Bu)] // AttackLow5 [InlineData(0x018B, 0x1000018Bu)] // AttackLow5
[InlineData(0x018E, 0x1000018Eu)] // AttackLow6 [InlineData(0x018E, 0x1000018Eu)] // AttackLow6
@ -66,6 +47,50 @@ public sealed class CombatAnimationPlannerTests
Assert.Equal(expectedFullCommand, MotionCommandResolver.ReconstructFullCommand(wireCommand)); Assert.Equal(expectedFullCommand, MotionCommandResolver.ReconstructFullCommand(wireCommand));
} }
// #159 parity: the full wire -> resolve -> classify pipeline for the
// late-combat block, which ACE/DRW numbers +3 above the 2013 decomp.
// CombatAnimationMotionCommands is now derived directly from
// DatReaderWriter.Enums.MotionCommand, so these ACE wire values must both
// reconstruct to their DRW full value AND classify into the right kind.
// (Expected full/kind pairs cross-checked against the DRW MotionCommand
// enum, Chorizite.DatReaderWriter 2.1.7.)
[Theory]
[InlineData((ushort)0x0173, 0x10000173u, CombatAnimationKind.MeleeSwing)] // OffhandSlashHigh
[InlineData((ushort)0x0175, 0x10000175u, CombatAnimationKind.MeleeSwing)] // OffhandSlashLow
[InlineData((ushort)0x0176, 0x10000176u, CombatAnimationKind.MeleeSwing)] // OffhandThrustHigh
[InlineData((ushort)0x017E, 0x1000017Eu, CombatAnimationKind.MeleeSwing)] // OffhandTripleSlashHigh
[InlineData((ushort)0x0182, 0x10000182u, CombatAnimationKind.MeleeSwing)] // OffhandTripleThrustLow
[InlineData((ushort)0x0185, 0x10000185u, CombatAnimationKind.MeleeSwing)] // OffhandKick
[InlineData((ushort)0x0186, 0x10000186u, CombatAnimationKind.CreatureAttack)] // AttackHigh4
[InlineData((ushort)0x0188, 0x10000188u, CombatAnimationKind.CreatureAttack)] // AttackLow4
[InlineData((ushort)0x018C, 0x1000018Cu, CombatAnimationKind.CreatureAttack)] // AttackHigh6
[InlineData((ushort)0x018E, 0x1000018Eu, CombatAnimationKind.CreatureAttack)] // AttackLow6
[InlineData((ushort)0x018F, 0x1000018Fu, CombatAnimationKind.MeleeSwing)] // PunchFastHigh
[InlineData((ushort)0x0191, 0x10000191u, CombatAnimationKind.MeleeSwing)] // PunchFastLow
[InlineData((ushort)0x0197, 0x10000197u, CombatAnimationKind.MeleeSwing)] // OffhandPunchFastLow
[InlineData((ushort)0x019A, 0x1000019Au, CombatAnimationKind.MeleeSwing)] // OffhandPunchSlowLow
public void PlanFromWireCommand_LateCombatBlock_UsesAceDrwNumbering(
ushort wireCommand,
uint expectedFullCommand,
CombatAnimationKind expectedKind)
{
var plan = CombatAnimationPlanner.PlanFromWireCommand(wireCommand);
Assert.Equal(expectedFullCommand, plan.MotionCommand);
Assert.Equal(expectedKind, plan.Kind);
}
// #159: Reload is the DRW SubState 0x40000016 (wire 0x0016), not the dead
// 2013 value 0x100000D4 the planner used to hold (absent from DRW entirely).
[Fact]
public void PlanFromWireCommand_Reload_UsesDrwSubStateValue()
{
var plan = CombatAnimationPlanner.PlanFromWireCommand(0x0016);
Assert.Equal(0x40000016u, plan.MotionCommand);
Assert.Equal(CombatAnimationKind.MissileAttack, plan.Kind);
Assert.Equal(AnimationCommandRouteKind.SubState, plan.RouteKind);
}
[Fact] [Fact]
public void PlanFromWireCommand_Swing_IsActionOverlay() public void PlanFromWireCommand_Swing_IsActionOverlay()
{ {