Implements IWeenieObject with GetRunRate and GetJumpHeight from
decompiled client, cross-referenced against ACE MovementSystem.
Default skills (Run=200, Jump=100) used until skill parsing ships.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parse ForwardSpeed from UpdateMotion (0xF74C) InterpretedMotionState.
Feed server-echoed RunRate into the player's MotionInterpreter so
get_state_velocity produces the correct speed. Previously hardcoded
at 1.0 (4.0 m/s), now matches character's Run skill.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the ad-hoc movement simulation with the ported retail physics:
- PlayerMovementController now owns a PhysicsBody (gravity, friction, Euler
integration with sub-stepping) and a MotionInterpreter (motion state machine,
speed constants from retail dat).
- Orientation quaternion is synced from Yaw each frame (Yaw=0 → +X, matching
the cos/sin convention the camera and outbound messages expect).
- Horizontal velocity is composed from MotionInterpreter.get_state_velocity()
speeds (WalkAnimSpeed=3.12, RunAnimSpeed=4.0, SidestepAnimSpeed=1.25 from
decompiled globals) then pushed via PhysicsBody.set_local_velocity so the
orientation quaternion rotates them into world space correctly.
- Vertical velocity (gravity / jump / fall) is snapshot before DoMotion calls
so apply_current_movement's set_local_velocity(0,0,0) can't clobber it.
- Jump delegates to MotionInterpreter.jump() + LeaveGround() which calls
get_leave_ground_velocity() → DefaultJumpVz=10.0 (retail value).
- PhysicsEngine.Resolve is still called each frame with zero delta to sample
terrain/cell Z under the body and set Contact+OnWalkable accordingly.
- Drive UpdatePhysicsInternal(dt) directly instead of update_object(wallClock)
to avoid the MinQuantum (~33ms) guard that would silently drop 60fps frames.
Test update: jump loop extended from 30→50 frames to cover the longer flight
time from retail DefaultJumpVz=10 (≈2.04s) vs old JumpImpulse=5 (≈1.02s).
303 tests green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ROOT CAUSE FIX for persistent slope Z clipping.
The SWtoNE/SEtoNW triangle boundary tests were swapped. AC's naming is
counter-intuitive: "SWtoNE cut" means BL and TR are the ISOLATED vertices
— the shared hypotenuse runs TL(0,1)→BR(1,0), so the dividing test is
tx+ty=1, NOT ty=tx. We had them backwards, causing every cell to sample
from the wrong triangle — up to 7.5 unit Z errors on steep terrain.
Fixed by cross-referencing WorldBuilder-ACME-Edition which has:
- ClientReference.cs: faithful C# port of decompiled AC client code
- TerrainConformanceTests.cs: verified against 25,600 cells
- TerrainGeometryGenerator.GetHeight(): matches the mesh index buffer
Also removes the slope gradient hack from PlayerMovementController —
no longer needed since SampleZ now returns exact triangle-surface Z.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The triangle-aware Z sampling from the previous commit produces exact
terrain-surface Z values, but a point-sampled Z on a tilted surface
places the character's center on the surface while their feet (which
extend horizontally) clip into the rising terrain ahead/behind.
Fix: in PlayerMovementController, sample Z 1 unit ahead in the walk
direction and add 40% of the gradient as an upward bias. This
compensates for the character's collision cylinder radius on slopes
while producing zero bias on flat ground. The bias is applied in the
movement controller (gameplay concern) not in TerrainSurface.SampleZ
(which stays exact for physics/tests).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fundamental terrain fixes based on the AC2D + holtburger deep dive:
1. Terrain split formula: replaced WorldBuilder's physics-path formula
(214614067/1813693831) with AC2D's render-path formula (0x0CCAC033,
0x421BE3BD, 0x6C1AC587, 0x519B8F25). The two produce different splits
for some cells. Since the render mesh uses this formula, the physics
Z sampler must match it to avoid misalignment on slopes.
2. Triangle-aware Z: replaced bilinear interpolation in TerrainSurface
with per-triangle barycentric interpolation. Each cell is split into
two triangles (using the same AC2D formula). SampleZ determines which
triangle the query point falls in, then interpolates within that
triangle. This produces Z values that exactly match the visual terrain
mesh — no more slope clipping.
Removes the multi-point Z sampling hack from PlayerMovementController
(no longer needed with exact triangle Z).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Slope clipping: replaced single foot-forward Z sample with 4-point
sampling (forward, back, left, right at 0.7 units). Takes the max Z
across all samples so both uphill and downhill slopes keep feet above
the terrain mesh surface. Removed the +0.1 Z bias entirely.
2. Player culling: replaced per-entity scan (alwaysVisibleEntityId) with
per-landblock skip (neverCullLandblockId). The player's current
landblock is computed from _playerController.Position and passed to
the renderer. Simpler, faster, and more reliable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three fixes for user-reported movement bugs:
1. Character disappears far from spawn: streaming observer now computed
from _playerController.Position when player mode is active, instead
of _lastLivePlayerLandblockId which only updates from server echoes
(never for autonomous moves). The 5x5 streaming window now follows
the player as they walk.
2. Jump physics from ACE: JumpImpulse=5.0 and GravityAccel=9.8
matching AC's formula: velocity_z = sqrt(height * 19.6) where
height = BurdenMod * (JumpSkill / (JumpSkill + 1300) * 22.2 + 0.05)
For a new char (skill=100, burden=50%): height≈1.31, vz≈5.07.
3. Gravity reduced from 20 to 9.8 (AC's F_GRAVITY constant).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three user-reported movement fixes:
1. Player disappears when facing away: StaticMeshRenderer now accepts
an alwaysVisibleEntityId. When a culled landblock contains the
player entity, it is still drawn. Prevents the frustum culler from
hiding the player character when they walk far from their spawn
landblock.
2. Jump too high: JumpImpulse reduced from 10.0 to 3.5 (placeholder;
retail scales by Jump skill value from the server).
3. Slope Z alignment: replaced the frame-delta slope bias with a
foot-forward sampling approach — sample terrain Z at 1 unit ahead
in the walk direction and use max(center, foot) as the ground Z.
Handles multi-grade slopes where the terrain rises faster than a
single-point sample tracks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two targeted fixes for user-reported movement bugs:
1. Wall bounce: PortalPlane.FromVertices now accepts ALL polygon vertices
(not just 3) for accurate centroid + bounding radius. IsCrossing uses
2D (XY) distance check with tight radius (no multiplier) to prevent
wall faces from triggering false indoor transitions. Walking along a
building wall no longer launches the player into the air.
2. Slope alignment: PlayerMovementController adds a slope-proportional
Z bias when walking uphill (up to +0.8 on steep slopes, grounded
only). Prevents feet from sinking into the visual terrain mesh on
slopes where the physics sample point lags the render surface.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Four targeted fixes for user-reported movement/visual bugs:
1. Player entity disappearing: GpuWorldState now supports persistent
entities (MarkPersistent/DrainRescued). The player character survives
landblock unloads and gets re-injected into the streaming window at
the current center landblock.
2. Feet sinking into terrain: +0.15 Z bias in PlayerMovementController
keeps the character model above terrain z-fighting edge cases.
3. Camera after portal teleport: ChaseCamera.Update now called
immediately after teleport snap so the camera recenters on the new
position instead of lingering at the pre-teleport location.
4. Scenery on roads: SceneryGenerator now checks road status at the
final displaced position (not just the origin vertex), catching
objects that drift from non-road vertices onto road cells.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PlayerTeleport (0xF751) is a standalone GameMessage (u16 sequence,
align-4). When received, WorldSession fires TeleportStarted(uint sequence).
GameWindow subscribes: OnTeleportStarted sets PlayerMovementController.State
= PortalSpace, freezing all WASD/physics input. OnLivePositionUpdated
detects arrival (different landblock or >100 unit jump on our character guid),
recenters the streaming origin, resolves physics for ground Z, snaps the
player entity + controller, returns State to InWorld, and sends
GameActionLoginComplete directly (matching holtburger's PlayerTeleport
handler: send_login_complete on every portal transition).
PlayerMovementController gains PlayerState enum + early-return guard: if
State == PortalSpace, Update() returns a zero-movement result immediately
so no MoveToState / AutonomousPosition messages are emitted during transit.
WorldSession gains ResetLoginComplete() for callers that need to re-arm
the latch (documented; not called by the teleport path since we send
LoginComplete directly rather than through the PlayerCreate latch).
Opcode source: holtburger/crates/holtburger-protocol/src/opcodes.rs:84
Wire layout: holtburger/crates/.../movement/messages/teleport.rs
Build: 0 errors. Tests: 283 passed, 0 failed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes for the "position never changes when walking" bug:
1. StepUpHeight was 1.0 units — too tight. The player started at
Z=92.2 (ACE relocation from previous session) but terrain Z was
~94, so every movement attempt had a Z delta of 1.8 which
exceeded the limit. Increased to 5.0 (forgiving for MVP; AC
default for humans is ~2 from Setup.StepUpHeight).
2. Initial position now resolves through PhysicsEngine with a huge
step height (100) to snap to the correct terrain Z regardless
of where the server-sent Z currently is. With indoor transitions
disabled, this always produces the outdoor terrain height.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three more fixes from the diagnostic dump:
1. Initial position: PhysicsEngine.Resolve was mapping the player
into an indoor EnvCell (foundry at Z=66) when they're standing
on outdoor terrain at Z=93+. The cell-containment check was too
aggressive for initial placement. Now uses the server-sent
position directly — the server already gave us a valid position.
2. Yaw unbounded: mouse delta accumulated without wrapping, growing
to 24+ radians. Now wraps to [-PI, PI] after every turn.
3. Turn command spam: MouseDeltaX > 0.5 threshold was too low for
raw pixel deltas. Any mouse jitter triggered turnCmd flips every
frame → stateChanged=True → MoveToState flood to the server.
Mouse turning now only affects yaw directly; turn COMMANDS only
come from A/D keyboard (matching retail client behavior where
mouse-look doesn't generate a TurnRight/TurnLeft command).
265 tests still green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per-frame controller that reads MovementInput (WASD/ZX/Shift/mouse),
drives PhysicsEngine.Resolve for collision, and tracks motion state
changes for outbound server messages + animation switching. Walk
(~4 u/s) and run (~7 u/s) speeds match AC retail. Heartbeat timer
triggers AutonomousPosition every ~200ms while moving.
5 new tests covering idle, forward, run, turn, and state-change
detection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>