# Phase L.3.1 — InterpolationManager Core + MoveOrTeleport Routing — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
>
> **Heavy subagent use is the user's explicit request.** Tasks marked **`[PARALLEL-A]`**, **`[PARALLEL-B]`**, etc. can be dispatched simultaneously as concurrent Sonnet subagents and the parent reviews each return before integrating. The plan flags every parallelization opportunity.

**Goal:** Replace acdream's hard-snap-then-Euler-extrapolate remote-entity motion with retail's queued position-waypoint pipeline (`InterpolationManager` + `MoveOrTeleport` routing). Apply parsed-but-ignored `VectorUpdate.Omega`. Tear out the now-redundant `RemoteMotion` soft-snap residual code. Ship behind `ACDREAM_INTERP_MANAGER=1` env-var gate, then collapse the dual paths after visual verification.

**Architecture:** New pure-data `InterpolationManager` class (FIFO queue cap 20 + `AdjustOffset(dt, maxSpeed) → Vector3` per-frame catch-up) composed into the existing per-remote `RemoteMotion` container. Inbound `0xF748` UpdatePosition handler (`OnLivePositionUpdated`) replaced by retail-faithful router (stale-seq → ignore; teleport-seq newer → snap; within 96 m → enqueue; beyond 96 m → slide-snap). Per-frame remote tick adds `Interp.AdjustOffset(dt) → body.Position`. Single-keyword env-var rollback during dev; cleanup commit after sign-off.

**Tech Stack:** C# / .NET 10 / xUnit. Edits in `AcDream.App` + `AcDream.Core`. No new NuGet deps. Tests at `tests/AcDream.Core.Tests/Physics/*Tests.cs`.

**Spec:** [`docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md`](../specs/2026-05-02-l3-remote-entity-motion-design.md) (committed 08cb7f9).

**Research baseline:** [`docs/research/2026-05-02-remote-entity-motion/resolved-via-cdb.md`](../../research/2026-05-02-remote-entity-motion/resolved-via-cdb.md).

---

## File Structure

| File | Action | Responsibility |
|---|---|---|
| `src/AcDream.Core/Physics/InterpolationManager.cs` | **CREATE** | Pure-data FIFO position-queue + adjust_offset math. No game/window deps. Composed into RemoteMotion. |
| `tests/AcDream.Core.Tests/Physics/InterpolationManagerTests.cs` | **CREATE** | ~13 unit tests covering queue mechanics, AdjustOffset math, stall detection. |
| `src/AcDream.Core/Physics/MotionInterpreter.cs` | **MODIFY** | Add public `GetMaxSpeed()` returning motion-table-derived max for current InterpretedState. |
| `tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs` | **MODIFY** | Add ~3 tests covering GetMaxSpeed for Walk/Run/Idle. |
| `src/AcDream.App/Rendering/GameWindow.cs` | **MODIFY** | (a) RemoteMotion class gains `Interp` field. (b) `OnLivePositionUpdated` env-var gated routing. (c) Per-frame remote tick env-var gated `Interp.AdjustOffset` add. (d) `OnLiveVectorUpdated` applies `Omega` to body. |
| `docs/plans/2026-04-11-roadmap.md` | **MODIFY** | Insert Phase L.3 entry between L.2 and M. |
| (cleanup commit) `src/AcDream.App/Rendering/GameWindow.cs` | **MODIFY** | Delete env-var dual-path branches; delete old hard-snap path; delete RemoteMotion soft-snap residual fields. |

---

## Open Precision Item

The spec flags one ambiguity: **does retail's `InterpolationManager::UseTime` (acclient @ 0x00555F20) blip-to-HEAD or blip-to-TAIL on stall?** The two agent reports disagreed. Default for the initial port = HEAD. Task 0 below resolves this via a 30-second cdb static decomp dive (no live attach needed).

---

## Task Decomposition Overview

```
                                                           ┌──────────────────────────────┐
                                                           │  Task 0 [PARALLEL-A]         │
                                                           │  Resolve UseTime head/tail   │
                                                           │  (decomp read, ~5 min)       │
                                                           └──────────────────────────────┘
                                                           ┌──────────────────────────────┐
                                                           │  Task 1 [PARALLEL-B]         │
        ┌─ DISPATCH 3 SUBAGENTS IN PARALLEL ──────────────►│  InterpolationManager + tests│
        │                                                  └──────────────────────────────┘
        │                                                  ┌──────────────────────────────┐
        │                                                  │  Task 2 [PARALLEL-C]         │
        │                                                  │  MotionInterpreter.GetMaxSpeed│
        │                                                  └──────────────────────────────┘

  ┌─ AFTER 0+1+2 LAND ────────────────────────────────────────────────────────────────────┐
  │                                                                                       │
  │  Task 3 — RemoteMotion.Interp field (sequential, single edit)                         │
  │       ↓                                                                               │
  │  Task 4 — OnLivePositionUpdated env-var routing (sequential)                          │
  │       ↓                                                                               │
  │  Task 5 — Per-frame remote tick env-var Interp.AdjustOffset (sequential)              │
  │       ↓                                                                               │
  │  Task 6 — OnLiveVectorUpdated.Omega (sequential, 3 lines)                             │
  │                                                                                       │
  │  Task 7 — Visual verification (USER GATE)                                             │
  │       ↓ user signs off                                                                │
  │                                                                                       │
  │  ┌─ DISPATCH 2 SUBAGENTS IN PARALLEL ──────┐                                          │
  │  │  Task 8: Cleanup commit                  │                                          │
  │  │  Task 9: Roadmap update                  │                                          │
  │  └──────────────────────────────────────────┘                                          │
  └────────────────────────────────────────────────────────────────────────────────────────┘
```

---

## Task 0 — [PARALLEL-A] Resolve `UseTime` head-vs-tail via static decomp

**Owner:** Sonnet subagent (general-purpose). Read-only; no code changes.

**Files:**
- Read: `docs/research/named-retail/acclient_2013_pseudo_c.txt` (search for `InterpolationManager::UseTime` near line ~352261-353375)

**Subagent dispatch prompt** (use `general-purpose` agent type, Sonnet):

> Read the named retail decomp at `docs/research/named-retail/acclient_2013_pseudo_c.txt`. Find `InterpolationManager::UseTime` (search by exact string `InterpolationManager::UseTime`). It should appear around line 352261-353375. Read the body of that function (~100 lines).
>
> The function decides what to do when the per-5-frame stall counter shows the entity isn't catching up to its queued waypoints (`node_fail_counter > 3`). The two prior research agents disagreed on whether the resulting "blip" snaps the body to the HEAD of the queue (the next intended waypoint) or to the TAIL (the most recent server-sent position).
>
> Report under 200 words: which is it (HEAD or TAIL), with the line range from the decomp that proves it. If the decompile is ambiguous (e.g. comparison polarity artifact), flag that and recommend a default. No code edits.

**Steps:**

- [ ] **Step 0.1: Dispatch the subagent**

Use the Agent tool with `subagent_type=general-purpose`, model `sonnet`, prompt above.

- [ ] **Step 0.2: Read subagent report; record decision in implementation note**

Append a one-line note to the InterpolationManager source comment (created in Task 1) recording the resolution.

---

## Task 1 — [PARALLEL-B] InterpolationManager class + ~13 unit tests

**Owner:** Sonnet subagent (general-purpose). Independent of Tasks 0 + 2.

**Files:**
- Create: `src/AcDream.Core/Physics/InterpolationManager.cs`
- Create: `tests/AcDream.Core.Tests/Physics/InterpolationManagerTests.cs`

**Subagent dispatch prompt** (use `general-purpose` agent type, Sonnet):

> You are implementing Phase L.3.1 Task 1. Read the spec at `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` sections "L.3.1 architecture" → "New file" through the unit-test list. Read the research at `docs/research/2026-05-02-remote-entity-motion/resolved-via-cdb.md` for the constants table.
>
> Create the file `src/AcDream.Core/Physics/InterpolationManager.cs` matching the spec's API:
> ```csharp
> public sealed class InterpolationManager {
>     void Enqueue(Vector3 targetPosition, float ownerHeading, bool isMovingTo);
>     Vector3 AdjustOffset(double dt, Vector3 currentBodyPosition, float maxSpeedFromMinterp);
>     bool IsActive { get; }
>     void Clear();
>     // constants from spec
> }
> ```
> The spec uses retail's `Position` type in the signature, but acdream's PhysicsBody uses `Vector3 Position` separately from `uint CellId`. So:
> - `Enqueue(Vector3 targetPosition, float ownerHeading, bool isMovingTo)` — caller is responsible for resolving cell deltas
> - `AdjustOffset(double dt, Vector3 currentBodyPosition, float maxSpeedFromMinterp)` returns the world-space delta to add to body.Position this frame
>
> Implement the spec's `AdjustOffset` algorithm exactly (steps 1-9 as written). For the stall-blip branch, use HEAD as the default (Task 0 may override this; Task 0's report should be available — if it says TAIL, use TAIL). Use `LinkedList<InterpolationNode>` for the queue.
>
> Create `tests/AcDream.Core.Tests/Physics/InterpolationManagerTests.cs` with the 13 tests listed in the spec under "L.3.1 unit tests" → "Queue mechanics", "AdjustOffset math", "Stall detection". Use xUnit. Match the test-file pattern of existing files (e.g. `tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs`): top-level `using` block, `namespace AcDream.Core.Tests.Physics;`, then test methods. Use `file sealed class` for any test-only helpers.
>
> Build with `cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug --nologo` and `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~InterpolationManager"`. Both must be green.
>
> Commit with `feat(physics): InterpolationManager core (L.3.1 Task 1)` and Co-Authored-By Claude Opus 4.7. Direct-to-main per CLAUDE.md.
>
> Report under 300 words: what you built, test results, any deviations from the spec (if you had to deviate, justify).

**Steps:**

- [ ] **Step 1.1: Dispatch the subagent in parallel with Tasks 0 and 2**

Use the Agent tool with `subagent_type=general-purpose`, `model=sonnet`. Send all 3 dispatch calls in a single message so they run concurrently.

- [ ] **Step 1.2: Verify subagent's commit**

```bash
git log -1 --stat src/AcDream.Core/Physics/InterpolationManager.cs
```

Expected: commit message starts with `feat(physics): InterpolationManager core (L.3.1 Task 1)`. Files changed include `InterpolationManager.cs` + `InterpolationManagerTests.cs`.

- [ ] **Step 1.3: Re-run tests in parent session to confirm green**

```bash
cd C:/Users/erikn/source/repos/acdream && dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~InterpolationManager"
```

Expected: all ~13 tests pass.

- [ ] **Step 1.4: Spot-check the implementation file**

Read the created file. Verify: API surface matches spec exactly; constants are public consts with the spec's values; `AdjustOffset` algorithm follows spec steps 1-9; stall-blip uses HEAD (or TAIL per Task 0 outcome).

If anything diverges materially from the spec without justification in the subagent's report, dispatch a fix subagent. If the deviation is minor and harmless, accept it.

---

## Task 2 — [PARALLEL-C] `MotionInterpreter.GetMaxSpeed()` + ~3 unit tests

**Owner:** Sonnet subagent (general-purpose). Independent of Tasks 0 + 1.

**Files:**
- Modify: `src/AcDream.Core/Physics/MotionInterpreter.cs` (add one public method, ~10-15 lines)
- Modify: `tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs` (add ~3 tests)

**Subagent dispatch prompt** (use `general-purpose` agent type, Sonnet):

> You are implementing Phase L.3.1 Task 2. Read the spec at `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` section "L.3.1 architecture" → "Modified — `MotionInterpreter`".
>
> Add a public method `GetMaxSpeed()` to `src/AcDream.Core/Physics/MotionInterpreter.cs`. It must port retail's `CMotionInterp::get_max_speed` semantics: return the motion-table-derived max speed for the current `InterpretedState.ForwardCommand`. Acdream's `MotionInterpreter` already knows the constants `RunAnimSpeed = 4.0f` and `WalkAnimSpeed = 3.12f` (search the file for these). Approximate retail logic:
> ```csharp
> public float GetMaxSpeed() {
>     return InterpretedState.ForwardCommand switch {
>         MotionCommand.RunForward  => RunAnimSpeed * (WeenieObj?.InqRunRate(out var r) == true ? r : MyRunRate),
>         MotionCommand.WalkForward => WalkAnimSpeed,
>         MotionCommand.WalkBackward => WalkAnimSpeed * 0.65f,  // BackwardsFactor
>         _ => 0f,  // idle / non-locomotion
>     };
> }
> ```
> If retail decomp suggests a different formula, prefer that — search the named decomp at `docs/research/named-retail/acclient_2013_pseudo_c.txt` for `CMotionInterp::get_max_speed` (around line 305235-305280). Report what you found.
>
> Add ~3 unit tests to `tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs`:
> - `GetMaxSpeed_RunForward_ReturnsRunAnimSpeedTimesRunRate`
> - `GetMaxSpeed_WalkForward_ReturnsWalkAnimSpeed`
> - `GetMaxSpeed_Idle_ReturnsZero`
>
> Use the existing `FakeWeenie` test helper from MotionInterpreterTests.cs.
>
> Build + test:
> ```bash
> cd C:/Users/erikn/source/repos/acdream
> dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug --nologo
> dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~MotionInterpreter"
> ```
> Both green.
>
> Commit with `feat(physics): MotionInterpreter.GetMaxSpeed for InterpolationManager (L.3.1 Task 2)` and Co-Authored-By Claude Opus 4.7. Direct-to-main.
>
> Report under 200 words: what the formula is, decomp reference if found, test results.

**Steps:**

- [ ] **Step 2.1: Dispatch in parallel with Tasks 0 and 1** (single message, three concurrent Agent tool calls)

- [ ] **Step 2.2: Verify subagent's commit**

```bash
git log -1 --stat src/AcDream.Core/Physics/MotionInterpreter.cs
```

Expected: commit message `feat(physics): MotionInterpreter.GetMaxSpeed (L.3.1 Task 2)`.

- [ ] **Step 2.3: Re-run tests**

```bash
cd C:/Users/erikn/source/repos/acdream && dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~MotionInterpreter"
```

Expected: existing tests + new ~3 tests all pass.

---

## Task 3 — Add `Interp` field to `RemoteMotion` class

**Owner:** Parent (you). Tiny mechanical edit; not worth a subagent.

**Files:**
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (line ~224 — `RemoteMotion` class)

**Steps:**

- [ ] **Step 3.1: Read the RemoteMotion class definition**

```bash
grep -n "private sealed class RemoteMotion" "C:/Users/erikn/source/repos/acdream/src/AcDream.App/Rendering/GameWindow.cs"
```

Then Read tool from the line returned, ~120 lines.

- [ ] **Step 3.2: Add `Interp` field**

In the `RemoteMotion` class body (after the existing field declarations, before the constructor `public RemoteMotion()`), add:

```csharp
        /// <summary>
        /// Per-remote position-waypoint queue + catch-up math (retail's
        /// InterpolationManager). Replaces the hard-snap-then-Euler-extrapolate
        /// path when ACDREAM_INTERP_MANAGER=1 — see L.3.1 spec.
        /// </summary>
        public AcDream.Core.Physics.InterpolationManager Interp { get; } =
            new AcDream.Core.Physics.InterpolationManager();
```

- [ ] **Step 3.3: Build and verify**

```bash
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
```

Expected: 0 warnings, 0 errors, "Build succeeded."

- [ ] **Step 3.4: Run all tests**

```bash
cd C:/Users/erikn/source/repos/acdream && dotnet test --no-build --nologo
```

Expected: existing tests still pass (no behavior change yet).

- [ ] **Step 3.5: Commit**

```bash
git add src/AcDream.App/Rendering/GameWindow.cs
git commit -m "$(cat <<'EOF'
feat(motion): RemoteMotion gains InterpolationManager field (L.3.1 Task 3)

Composes the new InterpolationManager (Task 1) into the per-remote
container. Field exists but is not consumed yet — Tasks 4 and 5 wire
it into the routing + per-frame tick.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
```

---

## Task 4 — Env-var gated routing in `OnLivePositionUpdated`

**Owner:** Parent. Manual edit because the surrounding handler is complex (~70 lines) and we need to wrap it without disrupting the legacy path.

**Files:**
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (line ~3151 — `OnLivePositionUpdated`)

**Steps:**

- [ ] **Step 4.1: Read the entire OnLivePositionUpdated method to understand current structure**

```bash
grep -n "OnLivePositionUpdated\b" "C:/Users/erikn/source/repos/acdream/src/AcDream.App/Rendering/GameWindow.cs"
```

Then Read tool from `OnLivePositionUpdated` start, ~100 lines (until the next method declaration).

Note: the method currently does (a) lazy-create RemoteMotion if not in dict, (b) hard-snap `body.Position` and `body.Orientation`, (c) update RemoteMotion.SnapResidualDecayRate / soft-snap residual fields, (d) clear airborne / set Z-fields if has-velocity changed.

- [ ] **Step 4.2: Locate the specific point where the hard-snap happens**

Look for `rm.Body.Position = ...` (or `body.Position = ...`) inside this handler. Mark its surrounding context.

- [ ] **Step 4.3: Wrap the snap block in env-var conditional**

Pseudocode of the change:

```csharp
private void OnLivePositionUpdated(AcDream.Core.Net.WorldSession.EntityPositionUpdate update)
{
    // ... existing lazy-create + parse + identification (unchanged) ...
    if (!_remoteDeadReckon.TryGetValue(update.Guid, out var rm)) {
        rm = new RemoteMotion();
        _remoteDeadReckon[update.Guid] = rm;
    }
    var targetPos = ...; // existing extraction (Vector3)
    var targetOri = ...; // existing extraction (Quaternion)

    // NEW: env-var gated retail-faithful routing (L.3.1)
    if (System.Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
    {
        // CPhysicsObj::MoveOrTeleport router (acclient @ 0x00516330):
        // - stale-seq: ignore (TODO: implement IsStaleSequence wrapping uint16 compare on the four sequence counters; for now allow all to land)
        // - teleport-seq newer or no-cell: SetPosition (hard-snap)
        // - has_contact false: no-op
        // - has_contact true && distance ≤ 96: Interp.Enqueue
        // - has_contact true && distance > 96: SetPositionSimple (slide-snap)

        // Distance source: retail uses entity->[+0x20] (entity-to-local-player).
        // Acdream computes equivalent via local player position.
        Vector3 localPlayerPos = _playerController?.Position ?? Vector3.Zero;
        float dist = Vector3.Distance(targetPos, localPlayerPos);

        bool teleportFlag = false; // TODO: source from update sequence comparison once IsStaleSequence is in
        bool hasContact = update.Position.HasContact; // verify field name in CreateObject.ServerPosition

        if (teleportFlag) {
            rm.Body.Position = targetPos;
            rm.Body.Orientation = targetOri;
            rm.Interp.Clear();
        }
        else if (!hasContact) {
            // no-op
        }
        else if (dist > 96f) {
            rm.Interp.Clear();
            rm.Body.Position = targetPos;
            rm.Body.Orientation = targetOri;
        }
        else {
            float headingFromQuat = ExtractYawFromQuaternion(targetOri); // see helper below
            rm.Interp.Enqueue(targetPos, headingFromQuat, isMovingTo: false);
        }
        return;
    }

    // EXISTING hard-snap path (unchanged) — kept until cleanup commit (Task 8)
    rm.Body.Position = targetPos;
    rm.Body.Orientation = targetOri;
    // ... rest of existing soft-snap + residual fields ...
}

// Helper (place near OnLivePositionUpdated):
private static float ExtractYawFromQuaternion(Quaternion q)
{
    // Inverse of YawToAcQuaternion: extract Z-axis rotation angle.
    // Acdream's player Yaw convention: Yaw=0 faces +X.
    return MathF.Atan2(2f * (q.W * q.Z + q.X * q.Y), 1f - 2f * (q.Y * q.Y + q.Z * q.Z));
}
```

If `update.Position.HasContact` doesn't exist, look for the equivalent on `CreateObject.ServerPosition` — likely `update.Position.IsGrounded` or similar based on parsed PositionPack flags. Use whatever's there; acceptable to file a TODO to plumb it through if it's not currently parsed.

- [ ] **Step 4.4: Build**

```bash
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
```

Expected: 0 errors, 0 warnings.

- [ ] **Step 4.5: Run tests**

```bash
cd C:/Users/erikn/source/repos/acdream && dotnet test --no-build --nologo
```

Expected: all existing tests pass (env-var off by default → existing behavior unchanged).

- [ ] **Step 4.6: Commit**

```bash
git add src/AcDream.App/Rendering/GameWindow.cs
git commit -m "$(cat <<'EOF'
feat(motion): MoveOrTeleport routing in OnLivePositionUpdated (L.3.1 Task 4)

Wraps the hard-snap path in ACDREAM_INTERP_MANAGER=1 env-var guard.
When set, runs retail-faithful routing (acclient!CPhysicsObj::
MoveOrTeleport @ 0x00516330): teleport-seq → SetPosition; within 96m
→ Interp.Enqueue; beyond 96m → SetPositionSimple slide-snap.

Existing hard-snap behavior preserved when flag is unset (default).
Old path will be removed in cleanup commit after visual verification.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
```

---

## Task 5 — Env-var gated per-frame `Interp.AdjustOffset` add

**Owner:** Parent. Touches the per-frame remote tick (~line 5680-5760).

**Files:**
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (per-frame remote tick block)

**Steps:**

- [ ] **Step 5.1: Locate the remote tick block**

```bash
grep -n "_remoteDeadReckon.TryGetValue.*serverGuid" "C:/Users/erikn/source/repos/acdream/src/AcDream.App/Rendering/GameWindow.cs"
```

Look for the line ~5689 entry; read 80 lines forward to see the whole tick block (where `apply_current_movement` and `body.UpdatePhysicsInternal` are called).

- [ ] **Step 5.2: Wrap the legacy tick body in an if/else against the env-var**

Pseudocode of the change:

```csharp
if (ae.Sequencer is not null
    && serverGuid != 0
    && serverGuid != _playerServerGuid
    && _remoteDeadReckon.TryGetValue(serverGuid, out var rm))
{
    if (System.Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
    {
        // NEW PATH: queued position-chase via InterpolationManager.
        // Walking remotes have m_velocityVector == 0 in retail; all visible
        // motion comes from adjust_offset walking the body toward queue head
        // at 2 × motion_max_speed × dt.
        if (rm.Interp.IsActive)
        {
            float maxSpeed = rm.Motion.GetMaxSpeed();   // Task 2 method
            Vector3 delta = rm.Interp.AdjustOffset((float)dt, rm.Body.Position, maxSpeed);
            rm.Body.Position += delta;
        }
        // For airborne remotes, OnLiveVectorUpdated has set body.Velocity;
        // body.UpdatePhysicsInternal below applies gravity. No queue
        // adjustment competes with the arc.
        rm.Body.UpdatePhysicsInternal((float)dt);
    }
    else
    {
        // EXISTING hard-snap + Euler path (unchanged) — kept until cleanup
        if (!rm.Airborne) {
            // ... existing apply_current_movement, force-OnWalkable, etc.
        }
        rm.Body.UpdatePhysicsInternal((float)dt);
        // ... existing post-physics processing ...
    }
}
```

The exact shape depends on the existing code — preserve everything in the `else` branch verbatim. The `if` branch is the new one.

- [ ] **Step 5.3: Build**

```bash
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
```

Expected: 0 errors.

- [ ] **Step 5.4: Run tests**

```bash
cd C:/Users/erikn/source/repos/acdream && dotnet test --no-build --nologo
```

Expected: all pass (flag off → existing behavior).

- [ ] **Step 5.5: Commit**

```bash
git add src/AcDream.App/Rendering/GameWindow.cs
git commit -m "$(cat <<'EOF'
feat(motion): per-frame Interp.AdjustOffset in remote tick (L.3.1 Task 5)

When ACDREAM_INTERP_MANAGER=1, the per-frame remote tick uses
InterpolationManager.AdjustOffset to walk body.Position toward the
queue head at 2 × motion-max-speed × dt (retail's
acclient!InterpolationManager::adjust_offset @ 0x00555D30).

Legacy apply_current_movement + Euler dead-reckoning preserved when
flag is unset.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
```

---

## Task 6 — Apply `VectorUpdate.Omega` in `OnLiveVectorUpdated`

**Owner:** Parent. Tiny edit, no env-var gate (this is a strict bug-fix that improves both old and new paths).

**Files:**
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (line ~3064 — `OnLiveVectorUpdated`)

**Steps:**

- [ ] **Step 6.1: Read the existing handler**

```bash
grep -n "OnLiveVectorUpdated" "C:/Users/erikn/source/repos/acdream/src/AcDream.App/Rendering/GameWindow.cs"
```

Read from line returned, ~30 lines.

- [ ] **Step 6.2: Find the velocity-application line and add omega next to it**

Find:
```csharp
if (update.Velocity is { } v)
    rm.Body.Velocity = v;
```

Add immediately after:
```csharp
if (update.Omega is { } w)
    rm.Body.Omega = w;
```

Verify the field name on `VectorUpdate.Parsed` — it might be `Omega` or `AngularVelocity`. If it's not present at all, that's a parser gap — file as a follow-up issue and skip this task. Most likely it's already parsed because the spec confirmed "currently parsed-but-ignored".

- [ ] **Step 6.3: Build + test**

```bash
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo && dotnet test --no-build --nologo
```

Expected: green.

- [ ] **Step 6.4: Commit**

```bash
git add src/AcDream.App/Rendering/GameWindow.cs
git commit -m "$(cat <<'EOF'
fix(motion): apply VectorUpdate.Omega to remote body (L.3.1 Task 6)

VectorUpdate.Omega was parsed by WorldSession but never written to
the remote body's Omega field, leaving remote jumping/turning
arcs flat. Apply it alongside the existing Velocity assignment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
```

---

## Task 7 — Visual verification (USER GATE)

**Owner:** User. Cannot be automated.

**Steps:**

- [ ] **Step 7.1: Kill any running acdream**

```powershell
Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 6
```

- [ ] **Step 7.2: Launch acdream with ACDREAM_INTERP_MANAGER=1**

```powershell
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
$env:ACDREAM_LIVE = "1"
$env:ACDREAM_TEST_HOST = "127.0.0.1"
$env:ACDREAM_TEST_PORT = "9000"
$env:ACDREAM_TEST_USER = "testaccount"
$env:ACDREAM_TEST_PASS = "testpassword"
$env:ACDREAM_INTERP_MANAGER = "1"
dotnet run --project C:\Users\erikn\source\repos\acdream\src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath "C:\Users\erikn\source\repos\acdream\.claude\worktrees\jovial-blackburn-773942\launch.log"
```

- [ ] **Step 7.3: User performs the visual test matrix**

Have a parallel retail observer toon watching `+Acdream`. On the retail observer side:

1. Walk forward 5 sec
2. Walk backward 5 sec
3. Strafe left/right 5 sec each
4. Stop
5. Run forward 5 sec
6. Jump from standstill 2-3x
7. Jump while running 2-3x
8. Turn quickly while running

For each, verify (against the acceptance criteria in the spec):
- Walking remotes glide smoothly (no 1-Hz popping)
- Backward / strafe / turn behaviors from commit 17a9ff1 still work
- Jump arcs are curved (Omega applied)

- [ ] **Step 7.4: User signs off OR files a regression**

If anything regresses, file the specifics and either fix forward (parent dispatches a focused-fix subagent) or revert the env-var to legacy mode while debugging.

If everything looks right, proceed to Tasks 8 + 9 (parallel cleanup + roadmap).

---

## Task 8 — [PARALLEL-D] Cleanup commit

**Owner:** Sonnet subagent (general-purpose). Independent of Task 9.

**Files:**
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (delete env-var dual paths in 4 + 5; delete RemoteMotion soft-snap residual fields)

**Subagent dispatch prompt** (use `general-purpose` agent type, Sonnet):

> You are implementing Phase L.3.1 Task 8: cleanup. The user has visually verified that ACDREAM_INTERP_MANAGER=1 works correctly. Now collapse the dual-path scaffolding into a single retail-faithful path.
>
> In `src/AcDream.App/Rendering/GameWindow.cs`:
>
> 1. **In `OnLivePositionUpdated`** (added in Task 4): delete the `if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")` wrapper. Keep ONLY the routing block inside it (the new path). Delete the legacy hard-snap fall-through.
>
> 2. **In the per-frame remote tick block** (modified in Task 5): same — delete the `if/else` env-var gate. Keep ONLY the new path (Interp.AdjustOffset). Delete the legacy `apply_current_movement` + force-OnWalkable + Euler-extrapolate code in the `else` branch.
>
> 3. **In the `RemoteMotion` class** (~line 224): delete `SnapResidualDecayRate` and any soft-snap residual fields (search for `_snapResidual`, `SnapResidualDecayRate`, `SoftSnap`, etc.). Also delete any related code in `OnLivePositionUpdated` and the per-frame tick that touched those fields (it should already be gone if Task 4/5 wrapped them in the env-var gate, but double-check).
>
> 4. **Search for any remaining `ACDREAM_INTERP_MANAGER` references** in the codebase and confirm zero remain.
>
> Build: `cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo`. 0 warnings, 0 errors.
> Test: `dotnet test --no-build --nologo`. All green.
>
> Commit:
> ```
> chore(motion): remove ACDREAM_INTERP_MANAGER flag + dead soft-snap path (L.3.1 Task 8)
>
> User has visually verified the new InterpolationManager-based remote
> motion (commits f2 + f5 + f6 from L.3.1). Collapses the env-var
> dual-path: deletes legacy hard-snap + Euler-extrapolate code from
> OnLivePositionUpdated and the per-frame remote tick, deletes the
> SnapResidualDecayRate + soft-snap residual fields from RemoteMotion.
>
> Net diff: ~50 lines deletion. Single retail-faithful path remains.
>
> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
> ```
>
> Report under 200 words: line counts deleted, files touched, test results.

**Steps:**

- [ ] **Step 8.1: Dispatch the subagent in parallel with Task 9**

Use Agent tool, `general-purpose`, `sonnet`. Send simultaneously with Task 9's dispatch.

- [ ] **Step 8.2: Verify the commit landed and the diff is sensible**

```bash
git log -1 --stat
git show HEAD -- src/AcDream.App/Rendering/GameWindow.cs | head -100
```

Expected: ~50 lines deleted, no `ACDREAM_INTERP_MANAGER` in the diff (only its removal).

- [ ] **Step 8.3: Re-run all tests in parent session**

```bash
cd C:/Users/erikn/source/repos/acdream && dotnet test --no-build --nologo
```

Expected: green.

- [ ] **Step 8.4: Confirm zero env-var references remain**

```bash
grep -rn "ACDREAM_INTERP_MANAGER" "C:/Users/erikn/source/repos/acdream/src/" 2>&1
```

Expected: no output.

---

## Task 9 — [PARALLEL-D] Roadmap update

**Owner:** Sonnet subagent (general-purpose). Independent of Task 8.

**Files:**
- Modify: `docs/plans/2026-04-11-roadmap.md`

**Subagent dispatch prompt** (use `general-purpose` agent type, Sonnet):

> You are implementing Phase L.3.1 Task 9: add the Phase L.3 entry to the roadmap.
>
> Read `docs/plans/2026-04-11-roadmap.md` to understand the existing format. Find the spot between `### Phase L.2 — Movement & Collision Conformance` and `### Phase M — Network Stack Conformance` (search for those exact headings).
>
> Insert a new Phase L.3 entry with this content:
> ```markdown
> ### Phase L.3 — Remote Entity Motion Conformance
>
> **Status:** L.3.1 IN PROGRESS / SHIPPED (depending on whether cleanup commit has landed when you read this).
>
> **Goal:** Replace acdream's hard-snap-then-Euler-extrapolate remote-entity motion with retail's queued position-waypoint pipeline (`InterpolationManager` + `MoveOrTeleport` routing). Apply parsed-but-ignored `VectorUpdate.Omega`. Drop the parallel soft-snap residual scaffolding `RemoteMotion` was carrying.
>
> **Why now:** Live cdb traces (2026-05-02) confirmed retail uses a per-physobj FIFO position queue with `adjust_offset(dt)` walking the body at 2× motion-table-max-speed toward the head, NOT velocity-based dead-reckoning. acdream's chop comes from the wrong algorithm category, not just bad parameters.
>
> **Spec:** [`docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md`](../superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md).
>
> **Plan (L.3.1):** [`docs/superpowers/plans/2026-05-02-l3-1-interpolation-manager.md`](../superpowers/plans/2026-05-02-l3-1-interpolation-manager.md).
>
> **Sub-lanes:**
>
> - **L.3.1 — InterpolationManager core + routing.** New `InterpolationManager` class, `MoveOrTeleport` routing replacing the hard-snap, `VectorUpdate.Omega` application, deletion of `RemoteMotion` soft-snap residual.
> - **L.3.2 — PositionManager.** Combines per-frame animation root-motion offset with the InterpolationManager's catch-up offset before writing the body's frame. Mirrors retail `CPhysicsObj::UpdateObjectInternal`. Spec to be drafted after L.3.1 ships.
> - **L.3.3 — MoveToManager.** Replaces `RemoteMoveToDriver` MVP with full retail-faithful port: retracking, sticky-to-target, fail-distance progress checks, sphere-cylinder distance variants. Spec to be drafted after L.3.2 ships.
> ```
>
> Also update the file's top-line `**Status:** Living document. Updated YYYY-MM-DD for ...` line — change the date to today (2026-05-02) and the trailing reason to `for Phase L.3 remote-entity motion planning`.
>
> Build (no code changed but verify nothing broke):
> ```bash
> cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
> ```
>
> Commit:
> ```
> docs(roadmap): Phase L.3 — Remote Entity Motion Conformance (L.3.1 Task 9)
>
> Adds the Phase L.3 entry between L.2 (collision) and M (network).
> Lists the three sub-lanes (L.3.1 in progress, L.3.2 + L.3.3 sketched).
> Cross-references the design spec and the L.3.1 implementation plan.
>
> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
> ```
>
> Report under 100 words: that the entry is inserted, location, build green.

**Steps:**

- [ ] **Step 9.1: Dispatch in parallel with Task 8** (single message)

- [ ] **Step 9.2: Verify the commit**

```bash
git log -1 --stat docs/plans/2026-04-11-roadmap.md
```

Expected: commit message `docs(roadmap): Phase L.3 — Remote Entity Motion Conformance`. File diff shows the new section in the right location.

- [ ] **Step 9.3: Optional final push**

```bash
cd C:/Users/erikn/source/repos/acdream && git push origin main
```

(Per CLAUDE.md, ask user before pushing.)

---

## Verification Plan

End-to-end smoke test after L.3.1 fully lands (post-Task 9):

```bash
cd C:/Users/erikn/source/repos/acdream
dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo  # green
dotnet test --no-build --nologo                                     # all green (~110 tests)
git log --oneline -10                                               # see L.3.1 commits in order
grep -rn "ACDREAM_INTERP_MANAGER" src/                              # zero hits (cleanup confirmed)
grep -rn "SnapResidualDecayRate" src/                               # zero hits (deleted)
```

Then user re-runs the visual test matrix from Task 7.3 with no env-var set (default behavior is now the new path).

If everything's green: L.3.1 done. Brainstorm L.3.2 next (PositionManager).

---

## Self-Review Notes (for the executor)

- **Task 4's `IsStaleSequence`** is intentionally deferred — the legacy code doesn't check sequences either. Filing a follow-up TODO is acceptable; not a blocker for L.3.1.
- **Task 4's `update.Position.HasContact`** field name is a guess — verify against `CreateObject.ServerPosition` definition. If absent, file a parser-gap follow-up; for L.3.1 default to `hasContact = true` (allow all to enqueue).
- **Task 5's `dt` source** — the existing per-frame block already has `dt` from the render loop or computes it from `nowSec - lastTime`. Reuse whatever's there.
- **Task 6's `update.Omega`** field — verify against `VectorUpdate.Parsed`. If named `AngularVelocity` use that.
- **Subagent failure handling:** if a subagent reports a deviation that breaks the spec contract, dispatch a fix subagent or take it over manually. Don't let a confused subagent leave broken code in main.
