using System.Numerics; namespace AcDream.Core.Physics; /// /// Per-frame combiner for remote-entity motion: animation root motion /// + InterpolationManager catch-up correction. Pure function — no /// side effects, no hidden state. /// /// Mirrors retail CPhysicsObj::UpdateObjectInternal (acclient @ 0x00513730): /// rootOffset = CPartArray::Update(dt) // animation /// PositionManager::adjust_offset(rootOffset) // adds correction /// frame.origin += rootOffset /// /// In acdream the animation root motion is sourced from /// AnimationSequencer.CurrentVelocity (body-local velocity from the /// active locomotion cycle). We rotate that by the body's orientation /// to get a world-space delta, then add the InterpolationManager's /// world-space correction. /// public sealed class PositionManager { /// /// Compute the per-frame world-space delta to add to body.Position. /// /// Per-frame delta time, seconds. /// Body's current world-space position. /// /// Body-local velocity from the active animation cycle /// (from AnimationSequencer.CurrentVelocity); pass /// Vector3.Zero if the entity has no sequencer or is on a /// non-locomotion cycle. /// /// Body orientation; used to rotate seqVel from body-local to world. /// The remote's InterpolationManager (for AdjustOffset call). /// From MotionInterpreter.GetMaxSpeed() — passed to AdjustOffset for the catch-up clamp. /// /// Optional local terrain plane normal at the body's current XY. When /// supplied AND the queue-empty / head-reached fallback path runs, the /// world-space anim root motion is projected onto the plane so XY motion /// produces a corresponding Z change on slopes. Without this, the /// fallback advances XY at the locomotion cycle's pace but leaves Z at /// the last UP's reported Z — visible as a ~5 Hz staircase on slopes /// (the rate of server UpdatePositions). Mirrors retail's /// CTransition::adjust_offset contact-plane projection /// (named-retail acclient_2013_pseudo_c.txt:272296-272346) for grounded /// motion, applied here at the queue-empty boundary instead of inside /// the sweep. Pass null on flat ground / when no terrain sample /// is available — projection is a no-op when normal == +Z. /// public Vector3 ComputeOffset( double dt, Vector3 currentBodyPosition, Vector3 seqVel, Quaternion ori, InterpolationManager interp, float maxSpeed, Vector3? terrainNormal = null) { // Retail-faithful per-frame combiner. Mirrors // CPhysicsObj::UpdatePositionInternal (acclient @ 0x00512c30) + // InterpolationManager::adjust_offset (@ 0x00555d30): // // 1. CPartArray::Update writes rootOffset (animation root motion) // into the per-tick Frame. // 2. PositionManager::adjust_offset → InterpolationManager::adjust_offset // either: // a) RETURNS EARLY when distance(body, head) < 0.05m // (NodeCompleted; arg2 unmodified) — body uses root motion. // b) OVERWRITES arg2 with `direction × min(catchUpSpeed × dt, // distance)` when body is far from head — catch-up REPLACES // root motion for this frame. // // It is NOT additive. Our prior port added rootMotion + correction // every frame, which stacked the animation push (≈ RunAnimSpeed × // speedMod, ≈ 11.7 m/s) on top of the queue catch-up (capped at // ≈ 23.5 m/s) so the body advanced at up to ~3× the server's // broadcast pace and the head-behind-body case produced a backward // correction every UP — the visible 1-Hz blip the user reported. // // AdjustOffset returns Vector3.Zero in two cases mapped to retail's // early-return: empty queue OR distance < DesiredDistance (0.05m). // In both, body falls back to animation root motion. Vector3 correction = interp.AdjustOffset(dt, currentBodyPosition, maxSpeed); if (correction.LengthSquared() > 0f) return correction; Vector3 rootMotionLocal = seqVel * (float)dt; Vector3 rootMotionWorld = Vector3.Transform(rootMotionLocal, ori); // Slope projection (queue-empty fallback only). Locomotion cycles // bake Z=0 in body-local, so without projection the body's Z stays // at the last UP's reported value while XY advances at the running // pace — visible ~5 Hz staircase between UPs on hills. Projecting // the world-space anim motion onto the local terrain plane gives // it a Z component proportional to slope × forward speed, so the // body follows the terrain mesh smoothly. No-op on flat ground // (normal ≈ +Z, dot ≈ 0) so it can't regress the M2 flat-ground // verification. if (terrainNormal.HasValue && terrainNormal.Value.Z > 0.01f) { Vector3 N = terrainNormal.Value; float into = Vector3.Dot(rootMotionWorld, N); rootMotionWorld -= N * into; } return rootMotionWorld; } }