fix(anim): Phase L.1b route motion commands

This commit is contained in:
Erik 2026-04-28 10:46:22 +02:00
parent 1c69670392
commit 460f95cb42
6 changed files with 218 additions and 53 deletions

View file

@ -0,0 +1,66 @@
using AcDream.Core.Physics;
using DatReaderWriter.DBObjs;
using Xunit;
namespace AcDream.Core.Tests.Physics;
public sealed class AnimationCommandRouterTests
{
private const uint NonCombat = 0x8000003Du;
[Theory]
[InlineData(0x00000000u, AnimationCommandRouteKind.None)]
[InlineData(0x10000057u, AnimationCommandRouteKind.Action)] // Sanctuary
[InlineData(0x2500003Bu, AnimationCommandRouteKind.Modifier)] // Jump
[InlineData(0x13000087u, AnimationCommandRouteKind.ChatEmote)] // Wave
[InlineData(0x41000003u, AnimationCommandRouteKind.SubState)] // Ready
[InlineData(0x40000011u, AnimationCommandRouteKind.SubState)] // Dead
[InlineData(0x8000003Du, AnimationCommandRouteKind.Ignored)] // NonCombat style
public void Classify_ReturnsRetailRouteKind(uint command, AnimationCommandRouteKind expected)
{
Assert.Equal(expected, AnimationCommandRouter.Classify(command));
}
[Fact]
public void RouteWireCommand_SubState_UsesSetCycle()
{
var seq = MakeEmptySequencer();
var route = AnimationCommandRouter.RouteWireCommand(seq, NonCombat, 0x0011);
Assert.Equal(AnimationCommandRouteKind.SubState, route);
Assert.Equal(NonCombat, seq.CurrentStyle);
Assert.Equal(MotionCommand.Dead, seq.CurrentMotion);
}
[Fact]
public void RouteWireCommand_Sanctuary_IsActionNotDeadCycle()
{
var seq = MakeEmptySequencer();
var route = AnimationCommandRouter.RouteWireCommand(seq, NonCombat, 0x0057);
Assert.Equal(AnimationCommandRouteKind.Action, route);
Assert.Equal(0u, seq.CurrentMotion);
}
[Fact]
public void RouteWireCommand_Wave_IsChatEmote()
{
var seq = MakeEmptySequencer();
var route = AnimationCommandRouter.RouteWireCommand(seq, NonCombat, 0x0087);
Assert.Equal(AnimationCommandRouteKind.ChatEmote, route);
}
private static AnimationSequencer MakeEmptySequencer()
{
return new AnimationSequencer(new Setup(), new MotionTable(), new NullAnimationLoader());
}
private sealed class NullAnimationLoader : IAnimationLoader
{
public Animation? LoadAnimation(uint id) => null;
}
}

View file

@ -21,6 +21,10 @@ public class MotionCommandResolverTests
[InlineData(0x000E, 0x6500000Eu)] // TurnLeft
[InlineData(0x000F, 0x6500000Fu)] // SideStepRight
[InlineData(0x0015, 0x40000015u)] // Falling
[InlineData(0x0011, 0x40000011u)] // Dead
[InlineData(0x0012, 0x41000012u)] // Crouch
[InlineData(0x0013, 0x41000013u)] // Sitting
[InlineData(0x0014, 0x41000014u)] // Sleeping
// Action-class one-shots: melee attacks, death, portals
[InlineData(0x0057, 0x10000057u)] // Sanctuary (death)
[InlineData(0x0058, 0x10000058u)] // ThrustMed

View file

@ -685,6 +685,33 @@ public sealed class MotionInterpreterTests
Assert.False(allowed);
}
[Fact]
public void ContactAllowsMove_DeadState_RejectsMove()
{
var body = MakeGrounded();
var interp = MakeInterp(body);
interp.InterpretedState.ForwardCommand = MotionCommand.Dead;
bool allowed = interp.contact_allows_move(MotionCommand.WalkForward);
Assert.False(allowed);
}
[Theory]
[InlineData(MotionCommand.Crouch)]
[InlineData(MotionCommand.Sitting)]
[InlineData(MotionCommand.Sleeping)]
public void ContactAllowsMove_PostureState_RejectsMove(uint postureCommand)
{
var body = MakeGrounded();
var interp = MakeInterp(body);
interp.InterpretedState.ForwardCommand = postureCommand;
bool allowed = interp.contact_allows_move(MotionCommand.WalkForward);
Assert.False(allowed);
}
[Fact]
public void ContactAllowsMove_CrouchRange_RejectsMove()
{