acdream/src/AcDream.Core/Physics/ServerControlledLocomotion.cs
Erik 9812965183 fix(anim): Phase L.1c match MoveTo run speed
Retail MovementManager::PerformMovement (0x00524440) reads MoveTo speed and runRate from the packet, MovementParameters::UnPackNet (0x0052AC50) defines the layout, and CMotionInterp::apply_run_to_command (0x00527BE0) multiplies RunForward by runRate. Parse those fields for UpdateMotion/CreateObject, seed server-controlled MoveTo locomotion with the retail speed multiplier, and avoid overriding active monster MoveTo with sparse UpdatePosition-derived velocity.
2026-04-28 20:58:22 +02:00

87 lines
2.9 KiB
C#

using System;
using System.Numerics;
namespace AcDream.Core.Physics;
/// <summary>
/// Chooses the visible locomotion cycle for server-controlled remotes whose
/// UpdateMotion packet is a MoveToObject/MoveToPosition union rather than an
/// InterpretedMotionState.
///
/// Retail references:
/// <list type="bullet">
/// <item><description>
/// <c>MovementManager::PerformMovement</c> (0x00524440) dispatches movement
/// types 6/7 into <c>MoveToManager::MoveToObject/MoveToPosition</c> instead
/// of unpacking an InterpretedMotionState.
/// </description></item>
/// <item><description>
/// <c>MovementParameters::UnPackNet</c> (0x0052AC50) shows MoveTo packets
/// carry movement params + run rate, not a ForwardCommand field.
/// </description></item>
/// <item><description>
/// ACE <c>MovementData.Write</c> uses the same movement type union; holtburger
/// documents the matching <c>MovementType::MoveToPosition = 7</c>.
/// </description></item>
/// </list>
/// </summary>
public static class ServerControlledLocomotion
{
public const float StopSpeed = 0.20f;
public const float RunThreshold = 1.25f;
public const float MinSpeedMod = 0.25f;
public const float MaxSpeedMod = 3.00f;
// Retail MoveToManager::BeginMoveForward -> MovementParameters::get_command
// (0x0052AA00) seeds forward motion before the next position update.
public static LocomotionCycle PlanMoveToStart(
float moveToSpeed = 1f,
float runRate = 1f,
bool canRun = true)
{
moveToSpeed = SanitizePositive(moveToSpeed);
runRate = SanitizePositive(runRate);
if (!canRun)
return new LocomotionCycle(MotionCommand.WalkForward, moveToSpeed, true);
return new LocomotionCycle(
MotionCommand.RunForward,
moveToSpeed * runRate,
true);
}
public static LocomotionCycle PlanFromVelocity(Vector3 worldVelocity)
{
float horizontalSpeed = MathF.Sqrt(
worldVelocity.X * worldVelocity.X +
worldVelocity.Y * worldVelocity.Y);
if (horizontalSpeed < StopSpeed)
return new LocomotionCycle(MotionCommand.Ready, 1f, false);
if (horizontalSpeed < RunThreshold)
{
float speedMod = Math.Clamp(
horizontalSpeed / MotionInterpreter.WalkAnimSpeed,
MinSpeedMod,
MaxSpeedMod);
return new LocomotionCycle(MotionCommand.WalkForward, speedMod, true);
}
return new LocomotionCycle(
MotionCommand.RunForward,
Math.Clamp(horizontalSpeed / MotionInterpreter.RunAnimSpeed, MinSpeedMod, MaxSpeedMod),
true);
}
public readonly record struct LocomotionCycle(
uint Motion,
float SpeedMod,
bool IsMoving);
private static float SanitizePositive(float value)
{
return float.IsFinite(value) && value > 0f ? value : 1f;
}
}