fix(movement): jump works locally (airborne velocity preserved)

Two fixes for jump physics:
- Skip ground-snap when velocity Z > 0 (prevents immediate re-landing
  at high framerates where per-frame Z delta < 0.05 snap threshold)
- Guard apply_current_movement velocity write behind OnWalkable check
  (prevents MotionInterpreter.DoMotion from zeroing jump velocity on
  every frame while airborne)
- Guard PlayerMovementController velocity replacement behind OnWalkable
  (preserves momentum during airborne flight)

Jump works locally but server packet not yet sent (BUG-002).
Facing direction mismatch logged as BUG-003.
RunRate not verified as BUG-004.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-14 00:12:11 +02:00
parent e08a06ac5b
commit 157ed9d974
4 changed files with 68 additions and 31 deletions

View file

@ -18,6 +18,37 @@ the "Fixed" section with the commit hash that resolved it.
or the CreaturePalette pipeline in `InstancedMeshRenderer`.
- **Phase:** Unassigned (investigate after rendering rebuild Step 5).
### BUG-002: Jump not visible from retail client
- **Observed:** 2026-04-14
- **Description:** Jump works locally (player rises and falls) but no
jump packet is sent to the server. Other clients don't see the jump.
- **Repro:** Jump in acdream, observe from retail client — no jump visible.
- **Likely area:** GameWindow.cs jump packet sending (currently a TODO
Console.WriteLine). Need to research jump packet format from holtburger.
- **Phase:** B.2 (movement completion)
### BUG-003: Facing direction mismatch with server
- **Observed:** 2026-04-14
- **Description:** Character facing direction in acdream doesn't match
what other clients see. Our quaternion convention for outbound
MoveToState/AutonomousPosition packets is wrong.
AC convention: heading 0 = West, 90 = North, 180 = East, 270 = South.
Our convention: Yaw 0 = +X (East in AC terms). The wireRot quaternion
needs to use holtburger's `from_heading` formula:
`theta = (450 - degrees).to_radians()`, `w=cos(theta/2)`, `z=sin(theta/2)`.
- **Repro:** Walk in acdream, observe from retail client — character faces
wrong direction.
- **Likely area:** GameWindow.cs wireRot computation (line ~1899).
- **Phase:** B.2 (movement completion)
### BUG-004: Run speed not verified working
- **Observed:** 2026-04-14
- **Description:** RunRate wiring exists (Task 1) but user reported no
speed change. Either the server's UpdateMotion ForwardSpeed isn't being
received, or it's not propagating to the velocity computation. Need to
add diagnostic logging to verify the UpdateMotion path fires.
- **Phase:** B.2 (movement completion)
---
## Fixed

View file

@ -1,9 +1,11 @@
# Rendering Rebuild from ACME
# Rendering Rebuild from ACME — COMPLETE
Port ACME's rendering pipeline to acdream. Each step produces a
visually testable result. The animation system stays unchanged (ACME
has none — ours is ported from the decompiled client).
**Status:** All 5 steps shipped 2026-04-13.
## Step 1: Port StaticObject shader + instanced rendering
The biggest performance win. Replace per-entity DrawElements with

View file

@ -189,12 +189,6 @@ public sealed class PlayerMovementController
_body.Orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, Yaw - MathF.PI / 2f);
// ── 2. Set velocity via MotionInterpreter state machine ───────────────
// Snapshot the current vertical velocity BEFORE calling DoMotion.
// DoMotion routes through apply_current_movement → set_local_velocity,
// which overwrites _body.Velocity with the horizontal state speed and
// zeros Z. We must snapshot Z first so we can restore it afterward.
float savedWorldVz = _body.Velocity.Z;
// Determine the dominant forward/backward command and speed.
uint forwardCmd;
float forwardCmdSpeed;
@ -205,7 +199,6 @@ public sealed class PlayerMovementController
}
else if (input.Backward)
{
// WalkBackward is tracked in interpreted state; we negate Y velocity below.
forwardCmd = MotionCommand.WalkBackward;
forwardCmdSpeed = 1.0f;
}
@ -215,7 +208,7 @@ public sealed class PlayerMovementController
forwardCmdSpeed = 1.0f;
}
// Update interpreted motion state.
// Update interpreted motion state (needed for animation + server messages).
_motion.DoMotion(forwardCmd, forwardCmdSpeed);
// Sidestep.
@ -229,28 +222,30 @@ public sealed class PlayerMovementController
_motion.StopInterpretedMotion(MotionCommand.SideStepLeft, modifyInterpretedState: true);
}
// get_state_velocity gives us the body-local speed magnitude from retail constants.
var stateVel = _motion.get_state_velocity();
// Only replace velocity with motion interpreter output when grounded.
// While airborne, the physics body's integrated velocity (from LeaveGround)
// persists — gravity pulls Z down, horizontal momentum is preserved.
// Retail AC works this way: you maintain momentum in the air.
if (_body.OnWalkable)
{
float savedWorldVz = _body.Velocity.Z;
var stateVel = _motion.get_state_velocity();
// Build the body-local velocity from the retail-derived speed values.
// get_state_velocity only fills +X for SideStepRight and +Y for forward;
// we must handle WalkBackward (negate Y) and SideStepLeft (negate X) manually.
float localY = 0f;
float localX = 0f;
float localY = 0f;
float localX = 0f;
if (input.Forward)
localY = stateVel.Y; // WalkAnimSpeed or RunAnimSpeed
else if (input.Backward)
localY = -(MotionInterpreter.WalkAnimSpeed * 0.65f); // retail backward is ~65% walk
if (input.Forward)
localY = stateVel.Y;
else if (input.Backward)
localY = -(MotionInterpreter.WalkAnimSpeed * 0.65f);
if (input.StrafeRight)
localX = MotionInterpreter.SidestepAnimSpeed * 0.5f;
else if (input.StrafeLeft)
localX = -MotionInterpreter.SidestepAnimSpeed * 0.5f;
if (input.StrafeRight)
localX = MotionInterpreter.SidestepAnimSpeed * 0.5f;
else if (input.StrafeLeft)
localX = -MotionInterpreter.SidestepAnimSpeed * 0.5f;
// Restore the vertical velocity snapshotted before DoMotion clobbered it.
// Rotation about Z does not affect the Z component, so world Vz == local Vz.
_body.set_local_velocity(new Vector3(localX, localY, savedWorldVz));
_body.set_local_velocity(new Vector3(localX, localY, savedWorldVz));
}
// ── 3. Jump (charged) ─────────────────────────────────────────────────
// Hold spacebar to charge (0→1 over JumpChargeRate seconds).
@ -301,9 +296,9 @@ public sealed class PlayerMovementController
float groundZ = resolveResult.Position.Z;
float bodyZ = _body.Position.Z;
if (bodyZ <= groundZ + 0.05f)
if (bodyZ <= groundZ + 0.05f && _body.Velocity.Z <= 0f)
{
// Player is at or below the ground — snap to surface and land.
// Player is at or below the ground AND not jumping upward — snap to surface.
_body.Position = new Vector3(_body.Position.X, _body.Position.Y, groundZ);
bool wasAirborne = !_body.OnWalkable;

View file

@ -539,8 +539,17 @@ public sealed class MotionInterpreter
if (InterpretedState.ForwardCommand == MotionCommand.RunForward)
MyRunRate = InterpretedState.ForwardSpeed;
var localVelocity = get_state_velocity();
PhysicsObj.set_local_velocity(localVelocity);
// Only replace velocity when grounded. While airborne, the physics
// body's integrated velocity (from LeaveGround) should persist —
// gravity pulls Z down, horizontal momentum is preserved.
// The retail client's apply_current_movement also gates on Contact+OnWalkable
// before writing velocity (the full decompiled flow routes through
// update_object which checks transient state).
if (PhysicsObj.OnWalkable)
{
var localVelocity = get_state_velocity();
PhysicsObj.set_local_velocity(localVelocity);
}
}
// ── FUN_00529390 — jump ───────────────────────────────────────────────────