# Phase B.2 — Player Movement Mode Design

**Status:** Spec, 2026-04-12, brainstormed with user.
**Scope:** A new player-controlled movement mode where WASD walks the character on collision-resolved terrain, the server sees valid movement, other clients see the character at the correct position, and the character plays walk/run/turn/idle animations locally. Toggled with Tab; coexists with the existing fly and orbit camera modes.
**Parent:** `docs/plans/2026-04-11-roadmap.md` — Phase B (Gameplay), sub-piece B.2.
**Depends on:** Phase B.3 (physics collision engine) — already shipped.

## Goals

1. Walk the player character on the server with WASD controls.
2. The server accepts the movement (no rejection / rubber-banding).
3. Other clients (original AC client or another acdream instance) see the character at the correct position.
4. The character plays walk/run/turn/idle animations locally (not sliding).
5. Third-person chase camera follows behind the character.
6. Mouse + A/D keyboard turning rotates the character; the turn is sent to the server.
7. Toggle between player mode and fly mode with Tab.

## Non-goals

- Jump (physics arc + landing — adds gravity state complexity, deferred).
- Combat stance switching (deferred to Phase B.4+).
- Indoor movement (B.3's PhysicsEngine supports it; we just don't test it in the acceptance criteria).
- NPC/entity collision (only terrain + EnvCell floor collision via PhysicsEngine).
- Smooth position interpolation for other players' movement (existing `UpdatePosition` snap behavior is acceptable for now).

## Input Mapping (Player Mode Active)

| Key | Action |
|---|---|
| **W / S** | Walk forward / backward (~4 u/s) |
| **Shift + W / Shift + S** | Run forward / backward (~7 u/s) |
| **A / D** | Turn left / right (keyboard turning) |
| **Z / X** | Strafe left / right |
| **Mouse X** | Turn (mouse turning, same yaw axis as A/D) |
| **Mouse Y** | Adjust chase camera pitch (look up/down) |
| **Tab** | Exit player mode, return to fly mode |

Speed values are approximate — the exact values should match what ACE's `MovementHandler` accepts. Check `references/ACE/Source/ACE.Server/` and `references/holtburger/` for the validated run/walk rates.

## Outbound Server Messages

### `MoveToState` (GameAction, opcode 0xF61C)

Sent when the player's motion state *changes*: starts walking, stops, starts turning, changes speed (walk↔run), starts strafing.

**Payload (per holtburger `MoveToStateActionData`):**
- `RawMotionState`:
  - `flags: RawMotionFlags` — bitmask of which fields are present
  - `current_hold_key: Option<u32>` — hold key (Run = shift held)
  - `current_style: Option<u32>` — combat stance (NonCombat for MVP)
  - `forward_command: Option<u32>` — WalkForward (0x0005) / RunForward (0x0007)
  - `forward_speed: Option<f32>` — movement speed multiplier
  - `sidestep_command: Option<u32>` — SideStepRight (0x000F) / SideStepLeft (0x0010)
  - `sidestep_speed: Option<f32>`
  - `turn_command: Option<u32>` — TurnRight (0x000D) / TurnLeft (0x000E)
  - `turn_speed: Option<f32>`
- `WorldPosition`:
  - `cell_id: u32` — full landblock+cell ID (from PhysicsEngine.Resolve)
  - `position: Vector3` — world XYZ
  - `rotation: Quaternion` — world orientation
- Sequence numbers: `instance_sequence`, `server_control_sequence`, `teleport_sequence`, `force_position_sequence` (all u16)
- `contact_long_jump: u8` — ground contact flag

### `AutonomousPosition` (GameAction, opcode 0xF753)

Periodic heartbeat sent every ~200ms while the player is moving. Carries the current `WorldPosition` + sequence numbers + ground contact flag. Does NOT carry motion commands — the server already knows those from `MoveToState`.

### Wrapping in GameAction

Both messages are sent as `GameAction` (game message opcode 0xF7B1):
```
u32 game_message_opcode = 0xF7B1
u32 action_sequence     = next game action sequence
u32 action_type_opcode  = 0xF61C (MoveToState) or 0xF753 (AutonomousPosition)
... payload ...
```

This matches how `GameActionLoginComplete` (0x00A1) is already sent in Phase 4.10. The difference is that MoveToState/AutonomousPosition carry a structured payload after the action type opcode.

**Note on game action sequence:** holtburger increments a `game_action_sequence` counter per outbound game action. WorldSession already has `_fragmentSequence` for fragment-level sequencing; a new `_gameActionSequence` counter is needed for the action-level sequence field.

## Third-Person Chase Camera

New `ChaseCamera` class implementing `ICamera`:

- **Position:** behind + above the player at a configurable offset. In player-local space: `(-distance * cos(pitch), 0, distance * sin(pitch))` rotated by the player's yaw. Default distance ~8 units, default pitch ~20 degrees above horizontal.
- **LookAt:** the player's position + a small vertical offset (eye height, ~1.5 units).
- **Update per frame:** receives the player's current world position + yaw + mouse-Y-driven pitch delta. Recomputes the camera's world position from the offset math.
- **Projection:** same parameters as FlyCamera (FovY = PI/3, near = 1, far = 5000).

Camera does NOT collide with terrain/buildings — if the camera clips through a hill behind the player, that's acceptable for the MVP. Camera collision is Phase C polish.

## Walk Animation (Local)

When player mode is active:
- Detect which motion command the current input maps to:
  - W held → `WalkForward` (0x45000005), or `RunForward` (0x44000007) if Shift held
  - S held → `WalkForward` with negative speed (backward)
  - Z held → `SideStepLeft` (0x65000010)
  - X held → `SideStepRight` (0x6500000F)
  - A held (no mouse) → `TurnLeft` (0x6500000E)
  - D held (no mouse) → `TurnRight` (0x6500000D)
  - Nothing held → `Ready` (0x41000003, idle breathing)
- Use `MotionResolver.GetIdleCycle` with the command as `commandOverride` to resolve the animation cycle from the player's motion table.
- Register/update the player entity in `_animatedEntities` so `TickAnimations` plays the resolved cycle with slerp interpolation (existing infrastructure from Phase 6.4/6.5).
- Only update the cycle when the motion command *changes* (idle→walk, walk→run, etc.) — don't re-resolve every frame.

**Priority when multiple keys are held:** forward/backward takes priority over sidestep; sidestep takes priority over turn. Matches retail client behavior.

## Architecture

### New Files

| File | Responsibility |
|---|---|
| `src/AcDream.App/Input/PlayerMovementController.cs` | Per-frame input → physics → position update + outbound message decision logic |
| `src/AcDream.App/Rendering/ChaseCamera.cs` | Third-person camera implementing `ICamera` |
| `src/AcDream.Core.Net/Messages/MoveToState.cs` | Outbound `MoveToState` message builder |
| `src/AcDream.Core.Net/Messages/AutonomousPosition.cs` | Outbound `AutonomousPosition` heartbeat builder |

### Modified Files

| File | Changes |
|---|---|
| `src/AcDream.App/Rendering/GameWindow.cs` | Tab toggle, wire PlayerMovementController + ChaseCamera into OnUpdate/OnRender, local animation cycle switching, identify player entity by guid |
| `src/AcDream.App/Rendering/CameraController.cs` | Add chase camera as a third mode (orbit / fly / chase) |
| `src/AcDream.Core.Net/WorldSession.cs` | Add `_gameActionSequence` counter, add `SendGameAction(byte[])` method that wraps payload in the 0xF7B1 GameAction envelope with incrementing sequence |

### Data Flow (Per Frame, Player Mode)

```
OnUpdate:
  1. Read keyboard (WASD/ZX/Shift/AD) + mouse delta
  2. PlayerMovementController.Update(dt, inputState):
     a. Compute yaw delta from mouse X + A/D keyboard turn
     b. Update player yaw
     c. Compute movement delta from W/S + Z/X in the player's
        facing direction (yaw-rotated), scaled by walk/run speed * dt
     d. PhysicsEngine.Resolve(currentPos, currentCellId, delta, stepUpHeight)
        → resolvedPos, resolvedCellId, isOnGround
     e. Update playerEntity.Position = resolvedPos
     f. Update playerEntity.Rotation = Quaternion.CreateFromAxisAngle(UnitZ, yaw)
     g. Detect motion state change (compare current vs previous command)
     h. If changed: build MoveToState → WorldSession.SendGameAction
     i. Every ~200ms: build AutonomousPosition → WorldSession.SendGameAction
  3. Update walk animation on playerEntity:
     - If motion command changed: re-resolve cycle via MotionResolver
     - Update _animatedEntities entry
  4. ChaseCamera.Update(playerEntity.Position, yaw, cameraPitch)

OnRender:
  - _cameraController.Active returns ChaseCamera when in player mode
  - Frustum culling + terrain + static mesh draw as usual
```

### Player Entity Identification

The player's own character is identified by matching the `EntitySpawn.Guid` from the `CreateObject` received at login against the character's GUID from `CharacterList`. GameWindow already stores `_entitiesByServerGuid` — the player entity is `_entitiesByServerGuid[chosenCharacterGuid]`. Store the player's server guid as a field on GameWindow when `EnterWorld` is called.

### Coordinate Space for Server Messages

AC positions on the wire use `(landblock cell ID, local XY, Z, rotation)`. The cell ID is the full 32-bit value: `(landblockX << 24) | (landblockY << 16) | cellIndex`. `PhysicsEngine.Resolve` currently returns a cell index (0x0001–0x0040 outdoor, 0x0100+ indoor) without the landblock prefix. The message builder needs to compose the full cell ID from the entity's world position (to get the landblock coordinates) + the engine's cell index.

acdream's internal world coordinates are center-landblock-relative. Converting back to AC wire coordinates requires:
- `landblockX = _liveCenterX + floor(worldPos.X / 192)`
- `landblockY = _liveCenterY + floor(worldPos.Y / 192)`
- `localX = worldPos.X - (landblockX - _liveCenterX) * 192`
- `localY = worldPos.Y - (landblockY - _liveCenterY) * 192`
- `localZ = worldPos.Z`
- `cellId = (landblockX << 24) | (landblockY << 16) | resolvedCellIndex`

This is the inverse of the `OnLiveEntitySpawned` coordinate translation.

## Testing Strategy

### Unit Tests

- **`PlayerMovementControllerTests`** — with a fake PhysicsEngine:
  - Forward input → position advances in facing direction
  - Shift+forward → position advances at run speed (faster)
  - Turn input → yaw changes
  - Motion state change detection: idle→walk→idle transitions
  - Heartbeat timing: AutonomousPosition sent every ~200ms during movement

- **`MoveToStateTests`** — message builder:
  - Build with a known RawMotionState + WorldPosition → assert byte layout
  - Verify GameAction envelope (0xF7B1 + sequence + 0xF61C)

- **`AutonomousPositionTests`** — message builder:
  - Build with a known WorldPosition → assert byte layout
  - Verify GameAction envelope (0xF7B1 + sequence + 0xF753)

- **`ChaseCameraTests`** — offset math:
  - Player at origin, yaw=0 → camera at (-8, 0, ~3)
  - Player at origin, yaw=PI/2 → camera at (0, -8, ~3)
  - Pitch adjustment changes camera height

### Integration / Visual Verification

- Log in at Holtburg.
- Press Tab to enter player mode.
- Walk with W → character walks forward on terrain, walk animation plays.
- Shift+W → run animation, faster movement.
- A/D → character turns, camera follows.
- Release all keys → character returns to idle breathing.
- Open original AC client with different account → see acdream's character at the correct position, visually moving.
- ACE console shows no movement rejections or errors.
- Close acdream → graceful logout (already working).

## Acceptance Criteria

Phase B.2 is done when:

1. Tab toggles between fly mode and player mode.
2. WASD/ZX walks/strafes the character on collision-resolved terrain.
3. A/D + mouse turns the character.
4. Shift toggles walk/run speed.
5. Walk/run/turn/idle animations play correctly on the player's character.
6. The server accepts the movement (no ACE-side rejections in the console log).
7. Other clients see the character at the correct position.
8. Third-person chase camera follows the character smoothly.
9. All new unit tests pass. Total test count increases by ~15-20.

## Open Questions (Resolved During Implementation)

- **Exact walk/run speed values:** check ACE's `MovementHandler` for the server-side validation thresholds. Use holtburger's default speeds as a starting point.
- **RawMotionState flag encoding:** the exact bitmask layout comes from holtburger's `RawMotionFlags`. Implementation will read `references/holtburger/crates/holtburger-protocol/src/messages/movement/types.rs` for the canonical encoding.
- **Sequence number management:** the four sequence counters (instance, server_control, teleport, force_position) need to be tracked per-session. Start at the values ACE sends in the initial handshake and increment as holtburger does. If exact behavior is unclear, start at 0 and adjust if ACE rejects.
- **WorldPosition rotation encoding:** AC wire quaternion order is (W, X, Y, Z). Our internal `System.Numerics.Quaternion` is (X, Y, Z, W). The message builder must swap the order — same transform already used in `OnLiveEntitySpawned`.
