docs(plan): Phase L.3.1+L.3.2 PositionManager + retail-faithful jump plan

6-task plan with subagent dispatch on Tasks 1, 3, 5:
- Task 1: PositionManager class + 6 unit tests (subagent)
- Task 2: Plumb IsGrounded through EntityPositionUpdate (parent, ~5 lines)
- Task 3: Retail-faithful per-frame remote tick (subagent — biggest:
  RemoteMotion.Position field + OnLivePositionUpdated rewrite [airborne
  no-op + landing transition + grounded routing] + TickAnimations rewrite
  [PositionManager.ComputeOffset + UpdatePhysicsInternal])
- Task 4: USER GATE (visual verification with retail observer)
- Task 5: Cleanup commit (subagent, parallel with 6)
- Task 6: Roadmap + spec status update (parent, parallel with 5)

Each task has TDD-style steps with exact file paths, code blocks, and
commit messages. Spec at c4446e7 lists L.3.1's already-shipped 6 commits;
this plan picks up from the revert at 1641d6e.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-03 10:10:16 +02:00
parent c4446e76fb
commit d063ac884d

View file

@ -0,0 +1,785 @@
# Phase L.3.1+L.3.2 Combined — PositionManager + Retail-Faithful Remote Tick
> **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.
**Goal:** Add the PositionManager combiner (animation root motion + InterpolationManager corrections) that was originally deferred to L.3.2, plumb `IsGrounded` through `EntityPositionUpdate`, and rewrite the per-frame remote tick + `OnLivePositionUpdated` env-var-on branches to match retail's `MoveOrTeleport` semantics. This eliminates the 1-Hz chop and endless-jump bugs surfaced during Task 7 visual verification.
**Architecture:** Pure-data `PositionManager.ComputeOffset(dt, body.Position, seqVel, ori, interp, maxSpeed) → Vector3` returns the per-frame world-space delta to add to body.Position. Combines (a) animation root motion = `seqVel * dt` rotated by body orientation with (b) `InterpolationManager.AdjustOffset` correction. Per-frame tick always runs all steps (matches retail `UpdateObjectInternal`). `OnLivePositionUpdated` routes per `MoveOrTeleport`: airborne → no-op; landing transition → snap + clear flags; grounded → enqueue or slide-snap. Server is authoritative for airborne arcs (no local prediction fights gravity).
**Tech Stack:** C# / .NET 10 / xUnit. 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 `c4446e7`).
**Already shipped (do NOT rebuild):**
- `f43f168` + `927636e` Task 1 — InterpolationManager
- `9c5634a` + `5b26d28` Task 2 — MotionInterpreter.GetMaxSpeed
- `517a3ce` Task 3 — RemoteMotion.Interp field
- `062e19f` Task 4 — OnLivePositionUpdated env-var routing v1
- `ae79e34` Task 5 — Per-frame Interp.AdjustOffset v1
- `e08accf` Task 6 — VectorUpdate.Omega
- `1641d6e` revert of band-aids
- `c4446e7` spec revision
---
## File Structure
| File | Action | Responsibility |
|---|---|---|
| `src/AcDream.Core/Physics/PositionManager.cs` | **CREATE** | Pure-function combiner: animation root motion + Interp correction. ~50 lines including XML docs. |
| `tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs` | **CREATE** | 6 unit tests against pure `ComputeOffset`. |
| `src/AcDream.Core.Net/Messages/UpdatePosition.cs` | **MODIFY** | Add `IsGrounded` to `Parsed` record, populate from `flags & PositionFlags.IsGrounded`. ~3 lines. |
| `src/AcDream.Core.Net/WorldSession.cs` | **MODIFY** | Add `IsGrounded` to `EntityPositionUpdate` record, pass through in PositionUpdated invoke. ~2 lines. |
| `src/AcDream.App/Rendering/GameWindow.cs` | **MODIFY** | (a) `RemoteMotion` gains `Position` field; (b) rewrite `OnLivePositionUpdated` env-var-on branch (airborne no-op + landing transition + grounded routing); (c) rewrite `TickAnimations` env-var-on branch (`PositionManager.ComputeOffset` + `UpdatePhysicsInternal`). |
| (cleanup commit) `src/AcDream.App/Rendering/GameWindow.cs` | **MODIFY** | Delete env-var dual paths; delete `RemoteMotion` soft-snap residual fields. |
| `docs/plans/2026-04-11-roadmap.md` | **MODIFY** (cleanup phase) | Update Phase L.3 entry to reflect L.3.1+L.3.2 combined. |
| `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` | **MODIFY** (cleanup phase) | Mark L.3.1+L.3.2 as SHIPPED. |
---
## Task Decomposition Overview
```
Task 1 — PositionManager class + 6 tests (subagent)
Task 2 — Plumb IsGrounded through EntityPositionUpdate (parent, 2 files, ~5 lines)
Task 3 — Retail-faithful per-frame remote tick (subagent — biggest change)
Task 4 — USER GATE: visual verification with retail observer
↓ (after sign-off)
┌─ DISPATCH IN PARALLEL ──────────────────┐
│ Task 5: Cleanup commit (subagent) │
│ Task 6: Roadmap + spec status (parent) │
└──────────────────────────────────────────┘
```
---
## Task 1 — PositionManager class + 6 unit tests
**Owner:** Sonnet subagent (general-purpose).
**Files:**
- Create: `src/AcDream.Core/Physics/PositionManager.cs`
- Create: `tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs`
**Subagent dispatch prompt** (use `general-purpose` agent type, Sonnet):
> You are implementing Task 1 of Phase L.3.1+L.3.2 in the acdream codebase. Read the spec at `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` section "L.3.2 architecture" → "New file — `src/AcDream.Core/Physics/PositionManager.cs`".
>
> **What to build:**
>
> Create `src/AcDream.Core/Physics/PositionManager.cs`:
>
> ```csharp
> using System.Numerics;
>
> namespace AcDream.Core.Physics;
>
> /// <summary>
> /// 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.
> /// </summary>
> public sealed class PositionManager
> {
> /// <summary>
> /// Compute the per-frame world-space delta to add to body.Position.
> /// </summary>
> /// <param name="dt">Per-frame delta time, seconds.</param>
> /// <param name="currentBodyPosition">Body's current world-space position.</param>
> /// <param name="seqVel">
> /// Body-local velocity from the active animation cycle
> /// (from <c>AnimationSequencer.CurrentVelocity</c>); pass
> /// <c>Vector3.Zero</c> if the entity has no sequencer or is on a
> /// non-locomotion cycle.
> /// </param>
> /// <param name="ori">Body orientation; used to rotate seqVel from body-local to world.</param>
> /// <param name="interp">The remote's InterpolationManager (for AdjustOffset call).</param>
> /// <param name="maxSpeed">From <c>MotionInterpreter.GetMaxSpeed()</c> — passed to AdjustOffset for the catch-up clamp.</param>
> public Vector3 ComputeOffset(
> double dt,
> Vector3 currentBodyPosition,
> Vector3 seqVel,
> Quaternion ori,
> InterpolationManager interp,
> float maxSpeed)
> {
> // Step 1: animation root motion (body-local → world).
> Vector3 rootMotionLocal = seqVel * (float)dt;
> Vector3 rootMotionWorld = Vector3.Transform(rootMotionLocal, ori);
>
> // Step 2: interpolation correction (world-space already).
> Vector3 correction = interp.AdjustOffset(dt, currentBodyPosition, maxSpeed);
>
> // Step 3: combined delta.
> return rootMotionWorld + correction;
> }
> }
> ```
>
> Create `tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs` with EXACTLY these 6 test names (these are the contract):
>
> 1. `ComputeOffset_StationaryRemote_BothSourcesZero_NoMotion`
> - seqVel = Vector3.Zero, no enqueued nodes in interp
> - Assert: returned offset == Vector3.Zero
>
> 2. `ComputeOffset_AnimationOnly_Forward_BodyAdvances`
> - seqVel = (0, 4, 0) (4 m/s forward), ori = Quaternion.Identity, dt = 0.1
> - Assert: returned offset == (0, 0.4, 0) (forward 0.4m)
>
> 3. `ComputeOffset_AnimationOnly_OrientedSouth_BodyMovesSouth`
> - seqVel = (0, 4, 0), ori = quaternion rotating +Y → -Y (180° around Z), dt = 0.1
> - Assert: returned offset.Y ≈ -0.4 (south)
>
> 4. `ComputeOffset_InterpOnly_NoAnimation_BodyChasesQueue`
> - seqVel = Vector3.Zero, interp has 1 enqueued node 1m ahead, dt = 0.1, maxSpeed = 4f
> - Expected: AdjustOffset returns the catch-up step (≤ 1m, clamped); ComputeOffset returns same
>
> 5. `ComputeOffset_BothActive_Combined`
> - seqVel = (0, 4, 0) — root motion (0, 0.4, 0)
> - interp has node 1m ahead — AdjustOffset returns ~Vector3.UnitY * step
> - Assert: returned offset == rootMotion + correction
>
> 6. `ComputeOffset_LocalToWorldRotation_Yaw90`
> - seqVel = (0, 1, 0) (forward 1 m/s in body frame)
> - ori = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2f) (yaw +90°)
> - dt = 1
> - Verify the rotation is applied correctly. With yaw +90° around Z, body-local +Y rotates to world... compute the expected and assert with precision: 4.
>
> Use xUnit, `namespace AcDream.Core.Tests.Physics;`, file-private fakes via `file sealed class` if needed. Read `tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs` for the existing pattern.
>
> Note: Tests #4 and #5 need a real `InterpolationManager` (not a fake) because PositionManager calls AdjustOffset directly. Construct one inline in each test, Enqueue what you need, and call ComputeOffset.
>
> **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~PositionManager"
> ```
>
> Both green. 6 tests pass.
>
> **Commit:**
>
> ```bash
> git add src/AcDream.Core/Physics/PositionManager.cs tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs
> git commit -m "$(cat <<'EOF'
> feat(physics): PositionManager combiner class + 6 unit tests (L.3.2)
>
> Pure-function ComputeOffset(dt, pos, seqVel, ori, interp, maxSpeed) →
> Vector3. Combines animation root motion (seqVel × dt rotated by body
> orientation) with InterpolationManager.AdjustOffset world-space
> correction. Mirrors retail CPhysicsObj::UpdateObjectInternal
> (acclient @ 0x00513730).
>
> Composed into RemoteMotion in subsequent task (L.3.1+L.3.2 Task 3);
> not yet consumed.
>
> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
> EOF
> )"
> ```
>
> **Self-review checklist:**
> - [ ] `PositionManager` is public sealed class
> - [ ] `ComputeOffset` is the only public method (no other API)
> - [ ] All 6 tests have the exact names listed
> - [ ] Tests #4 and #5 use a real `InterpolationManager`
> - [ ] No game/window/sequencer dependencies — only `System.Numerics` + `AcDream.Core.Physics.InterpolationManager`
> - [ ] Build clean, all 6 tests pass
> - [ ] Commit references "L.3.2"
>
> **Report:**
> - Status: DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
> - What you built (1-2 sentences)
> - Test results (count, any deviations)
> - Files changed
> - Concerns (if any)
**Steps for the parent (controller):**
- [ ] **Step 1.1: Dispatch the implementer subagent** using the prompt above.
- [ ] **Step 1.2: Verify the commit landed**
```bash
cd C:/Users/erikn/source/repos/acdream && git log -1 --stat src/AcDream.Core/Physics/PositionManager.cs
```
Expected: commit message starts with `feat(physics): PositionManager combiner class`.
- [ ] **Step 1.3: Re-run tests in parent**
```bash
cd C:/Users/erikn/source/repos/acdream && dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~PositionManager"
```
Expected: 6 tests pass.
- [ ] **Step 1.4: Dispatch spec compliance reviewer** (use `general-purpose`, Sonnet). Verify the 6 tests have the EXACT names listed and verify `ComputeOffset` algorithm matches the spec's pseudocode.
- [ ] **Step 1.5: Dispatch code quality reviewer** (use `superpowers:code-reviewer`). Check for: API surface (only ComputeOffset public), test quality, no superfluous deps.
- [ ] **Step 1.6: Address review issues if any.** If issues found, dispatch fix subagent. Re-review.
---
## Task 2 — Plumb `IsGrounded` through `EntityPositionUpdate`
**Owner:** Parent. Mechanical edit, ~5 lines across 2 files.
**Files:**
- Modify: `src/AcDream.Core.Net/Messages/UpdatePosition.cs:62-69` (add `IsGrounded` to `Parsed` record)
- Modify: `src/AcDream.Core.Net/Messages/UpdatePosition.cs:166` (populate `IsGrounded` in the constructor call)
- Modify: `src/AcDream.Core.Net/WorldSession.cs:110-113` (add `IsGrounded` to `EntityPositionUpdate` record)
- Modify: `src/AcDream.Core.Net/WorldSession.cs:711-714` (pass `posUpdate.Value.IsGrounded` through)
**Steps:**
- [ ] **Step 2.1: Read existing `UpdatePosition.Parsed` record + TryParse return**
```bash
grep -n "public readonly record struct Parsed\|return new Parsed" "C:/Users/erikn/source/repos/acdream/src/AcDream.Core.Net/Messages/UpdatePosition.cs"
```
- [ ] **Step 2.2: Add `IsGrounded` field to `UpdatePosition.Parsed`**
Edit `src/AcDream.Core.Net/Messages/UpdatePosition.cs` (~line 62):
Change:
```csharp
public readonly record struct Parsed(
uint Guid,
CreateObject.ServerPosition Position,
System.Numerics.Vector3? Velocity,
uint? PlacementId,
ushort InstanceSequence = 0,
ushort TeleportSequence = 0,
ushort ForcePositionSequence = 0);
```
To:
```csharp
public readonly record struct Parsed(
uint Guid,
CreateObject.ServerPosition Position,
System.Numerics.Vector3? Velocity,
uint? PlacementId,
bool IsGrounded,
ushort InstanceSequence = 0,
ushort TeleportSequence = 0,
ushort ForcePositionSequence = 0);
```
- [ ] **Step 2.3: Populate `IsGrounded` in the `Parsed` constructor call (~line 166)**
Find the line `return new Parsed(guid, serverPos, velocity, placementId,` (~line 166) and change to pass `(flags & PositionFlags.IsGrounded) != 0` as the new IsGrounded argument. Looks roughly like:
```csharp
return new Parsed(guid, serverPos, velocity, placementId,
(flags & PositionFlags.IsGrounded) != 0,
instSeq, teleSeq, forceSeq);
```
(Verify the trailing-arg layout against what's actually there; preserve any existing trailing arguments.)
- [ ] **Step 2.4: Add `IsGrounded` field to `WorldSession.EntityPositionUpdate`**
Edit `src/AcDream.Core.Net/WorldSession.cs:110`:
Change:
```csharp
public readonly record struct EntityPositionUpdate(
uint Guid,
CreateObject.ServerPosition Position,
System.Numerics.Vector3? Velocity);
```
To:
```csharp
public readonly record struct EntityPositionUpdate(
uint Guid,
CreateObject.ServerPosition Position,
System.Numerics.Vector3? Velocity,
bool IsGrounded);
```
- [ ] **Step 2.5: Pass `IsGrounded` through in PositionUpdated invoke (~line 711)**
Change:
```csharp
PositionUpdated?.Invoke(new EntityPositionUpdate(
posUpdate.Value.Guid,
posUpdate.Value.Position,
posUpdate.Value.Velocity));
```
To:
```csharp
PositionUpdated?.Invoke(new EntityPositionUpdate(
posUpdate.Value.Guid,
posUpdate.Value.Position,
posUpdate.Value.Velocity,
posUpdate.Value.IsGrounded));
```
- [ ] **Step 2.6: 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 2>&1 | tail -6
```
Expected: 0 build errors. Same 4 pre-existing test failures, no new failures.
- [ ] **Step 2.7: Commit**
```bash
git add src/AcDream.Core.Net/Messages/UpdatePosition.cs src/AcDream.Core.Net/WorldSession.cs
git commit -m "$(cat <<'EOF'
feat(net): plumb IsGrounded through EntityPositionUpdate (L.3.2)
PositionFlags.IsGrounded (0x04) was already parsed by UpdatePosition
but not exposed through Parsed record or EntityPositionUpdate.
Adds the bool field to both records so OnLivePositionUpdated can
consume it for retail-faithful MoveOrTeleport routing
(acclient @ 0x00516330: has_contact=false → no-op during airborne arc).
Consumed in subsequent task (L.3.1+L.3.2 Task 3).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
```
---
## Task 3 — Retail-faithful per-frame remote tick
**Owner:** Sonnet subagent (general-purpose). Largest task — touches 3 distinct sites in `GameWindow.cs`.
**Files:**
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (RemoteMotion class line ~224 + OnLivePositionUpdated env-var branch + TickAnimations env-var branch)
**Subagent dispatch prompt:**
> You are implementing Task 3 of Phase L.3.1+L.3.2 in the acdream codebase. This task rewrites two env-var-gated branches in `src/AcDream.App/Rendering/GameWindow.cs` to consume the new PositionManager (Task 1) and IsGrounded plumbing (Task 2).
>
> **Repo:** `C:/Users/erikn/source/repos/acdream` — main branch — direct-to-main per CLAUDE.md.
>
> **Spec:** `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` "L.3.2 architecture" sections.
>
> **Three changes in `GameWindow.cs`:**
>
> ### Change 1: `RemoteMotion` class gains `Position` field
>
> Find the existing `Interp` field (added in commit `517a3ce`). Right after it, add:
>
> ```csharp
> /// <summary>
> /// Per-frame combiner for animation root motion + InterpolationManager
> /// correction (Phase L.3.2). Consumed in TickAnimations to compute the
> /// per-frame body.Position delta.
> /// </summary>
> public AcDream.Core.Physics.PositionManager Position { get; } =
> new AcDream.Core.Physics.PositionManager();
> ```
>
> ### Change 2: Rewrite `OnLivePositionUpdated` env-var-on branch
>
> Find the existing env-var-on block in `OnLivePositionUpdated` (was added at commit `062e19f`). It currently looks roughly like:
> ```csharp
> if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
> {
> rmState.Body.Orientation = rot;
> // teleport check, dist check, etc.
> return;
> }
> ```
>
> Replace the env-var-on body with this new logic:
>
> ```csharp
> if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
> {
> // Orientation always snaps on receipt — the InterpolationManager
> // walks position only; heading would otherwise lag the queue.
> rmState.Body.Orientation = rot;
>
> // ── AIRBORNE NO-OP ────────────────────────────────────────────
> // Mirrors retail CPhysicsObj::MoveOrTeleport (acclient @ 0x00516330):
> // when has_contact==0, return false (don't touch body, don't queue).
> // body.Velocity (set once by OnLiveVectorUpdated at jump start) keeps
> // integrating gravity via per-frame UpdatePhysicsInternal. Server is
> // authoritative for the arc; we don't predict it locally.
> if (!update.IsGrounded)
> return;
>
> // ── LANDING TRANSITION ─────────────────────────────────────────
> // First IsGrounded=true UP after rmState.Airborne signals landed.
> // Clear airborne flags, hard-snap to authoritative landing position,
> // clear interpolation queue (any pre-jump waypoints are stale).
> if (rmState.Airborne)
> {
> rmState.Airborne = false;
> rmState.Body.Velocity = System.Numerics.Vector3.Zero;
> rmState.Body.State &= ~AcDream.Core.Physics.PhysicsStateFlags.Gravity;
> rmState.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
> | AcDream.Core.Physics.TransientStateFlags.OnWalkable;
> rmState.Interp.Clear();
> rmState.Body.Position = worldPos;
> return;
> }
>
> // ── GROUNDED ROUTING (CPhysicsObj::MoveOrTeleport) ────────────
> const float MaxPhysicsDistance = 96f;
> var localPlayerPos = _playerController?.Position ?? System.Numerics.Vector3.Zero;
> float dist = System.Numerics.Vector3.Distance(worldPos, localPlayerPos);
>
> if (dist > MaxPhysicsDistance)
> {
> // Beyond view bubble: SetPositionSimple slide-snap. Clear queue.
> rmState.Interp.Clear();
> rmState.Body.Position = worldPos;
> }
> else
> {
> // Within view bubble: enqueue waypoint for adjust_offset to walk to.
> // PositionManager (called per-frame in TickAnimations) handles the
> // actual body advancement — mix of animation root motion + queue
> // correction.
> float headingFromQuat = ExtractYawFromQuaternion(rot);
> rmState.Interp.Enqueue(worldPos, headingFromQuat, isMovingTo: false);
> }
> return;
> }
> ```
>
> The legacy `else` branch (env-var unset) STAYS UNCHANGED.
>
> If `ExtractYawFromQuaternion` doesn't exist anymore (it might have been removed in the revert), re-add it near the original location (search for it in commit `062e19f`'s diff). The body is:
> ```csharp
> private static float ExtractYawFromQuaternion(System.Numerics.Quaternion q)
> {
> // Standard z-up yaw extraction: atan2(2(wz + xy), 1 - 2(y² + z²))
> return MathF.Atan2(2f * (q.W * q.Z + q.X * q.Y),
> 1f - 2f * (q.Y * q.Y + q.Z * q.Z));
> }
> ```
>
> ### Change 3: Rewrite `TickAnimations` env-var-on branch
>
> Find the existing env-var-on block in the per-frame remote tick (added at commit `ae79e34`). It currently looks roughly like:
> ```csharp
> if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
> {
> if (rm.Interp.IsActive) {
> float maxSpeed = rm.Motion.GetMaxSpeed();
> Vector3 delta = rm.Interp.AdjustOffset((double)dt, rm.Body.Position, maxSpeed);
> rm.Body.Position += delta;
> }
> rm.Body.UpdatePhysicsInternal(dt);
> // entity write-back
> }
> ```
>
> Replace with PositionManager call:
>
> ```csharp
> if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
> {
> // Always-run-all-steps per retail CPhysicsObj::UpdateObjectInternal
> // (acclient @ 0x00513730):
> // 1+2. animation root motion + interpolation correction (combined)
> // 3. physics integration (gravity for airborne; no-op for grounded)
> System.Numerics.Vector3 seqVel = ae.Sequencer?.CurrentVelocity
> ?? System.Numerics.Vector3.Zero;
> float maxSpeed = rm.Motion.GetMaxSpeed();
> System.Numerics.Vector3 offset = rm.Position.ComputeOffset(
> dt: (double)dt,
> currentBodyPosition: rm.Body.Position,
> seqVel: seqVel,
> ori: rm.Body.Orientation,
> interp: rm.Interp,
> maxSpeed: maxSpeed);
> rm.Body.Position += offset;
> rm.Body.UpdatePhysicsInternal(dt);
> // KEEP whatever entity write-back lines were here (ae.Entity.Position = ..., etc.)
> }
> else
> {
> // EXISTING legacy path UNCHANGED
> }
> ```
>
> The `else` branch (legacy path) stays UNCHANGED.
>
> **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 2>&1 | tail -6
> ```
>
> Expected: 0 build errors. Same 4 pre-existing failures (`DispatcherToMovementIntegrationTests` + `BSPStepUpTests` — these are not related to L.3 work). No NEW failures.
>
> **Commit:**
>
> ```bash
> git add src/AcDream.App/Rendering/GameWindow.cs
> git commit -m "$(cat <<'EOF'
> feat(motion): retail-faithful per-frame remote tick (L.3.1+L.3.2)
>
> Combines PositionManager (Task 1) + IsGrounded plumbing (Task 2) into
> the per-frame remote motion path. Three changes in GameWindow.cs,
> all gated behind ACDREAM_INTERP_MANAGER=1:
>
> 1. RemoteMotion gains Position field (PositionManager instance).
>
> 2. OnLivePositionUpdated env-var branch rewritten to mirror retail
> CPhysicsObj::MoveOrTeleport (acclient @ 0x00516330):
> - orientation snap-on-receipt (PositionManager handles position only)
> - airborne (!IsGrounded) → no-op (server is authoritative for arc;
> body.Velocity from VectorUpdate integrates gravity locally)
> - landing transition (first IsGrounded=true after Airborne) →
> clear airborne flags, hard-snap to landing pos, clear queue
> - grounded routing: dist > 96m → slide-snap; dist ≤ 96m → enqueue
>
> 3. TickAnimations env-var branch rewritten to use PositionManager:
> body.Position += PositionManager.ComputeOffset(dt, pos, seqVel,
> ori, interp, maxSpeed); body.UpdatePhysicsInternal(dt) for gravity.
>
> Replaces the L.3.1-only AdjustOffset-only path. Legacy (env-var off)
> path unchanged.
>
> Cleanup commit (next sub-task) deletes the env-var dual paths after
> visual verification.
>
> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
> EOF
> )"
> ```
>
> **Self-review checklist:**
> - [ ] `RemoteMotion.Position` field added (alongside existing `Interp`)
> - [ ] `OnLivePositionUpdated` env-var branch has 3 sub-branches: airborne return, landing transition, grounded routing (snap or enqueue)
> - [ ] `OnLivePositionUpdated` legacy `else` branch UNCHANGED
> - [ ] `TickAnimations` env-var branch uses `PositionManager.ComputeOffset` exclusively (no direct `AdjustOffset` call)
> - [ ] `TickAnimations` legacy `else` branch UNCHANGED
> - [ ] `ExtractYawFromQuaternion` helper present (re-add if missing)
> - [ ] `OnLiveVectorUpdated` UNTOUCHED (it already does the right thing)
> - [ ] Build clean, same 4 pre-existing failures
>
> **Report:**
> - Status: DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
> - Lines changed (with file:line refs)
> - Test count
> - Concerns (if any)
>
> If the existing legacy `else` path is so tangled that you can't safely rewrite the env-var branch without disturbing it, REPORT BLOCKED with specifics.
**Steps for the parent:**
- [ ] **Step 3.1: Dispatch the implementer subagent** using the prompt above.
- [ ] **Step 3.2: Verify the commit landed**
```bash
cd C:/Users/erikn/source/repos/acdream && git log -1 --stat src/AcDream.App/Rendering/GameWindow.cs
```
- [ ] **Step 3.3: Build + test in parent**
```bash
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo && dotnet test --no-build --nologo 2>&1 | tail -6
```
Expected: 0 build errors. Same 4 pre-existing failures.
- [ ] **Step 3.4: Spec compliance review** (general-purpose subagent). Verify the rewrite matches the spec's pseudocode exactly. Verify legacy `else` paths are byte-for-byte unchanged.
- [ ] **Step 3.5: Code quality review** (`superpowers:code-reviewer`). Specifically check: orientation snap is in ALL routing paths; airborne no-op is the FIRST gate; landing transition resets all the right flags; ExtractYawFromQuaternion is correct.
- [ ] **Step 3.6: Address review issues if any.** Fix subagent + re-review.
---
## Task 4 — USER GATE: visual verification
**Owner:** User. Cannot be automated.
**Steps:**
- [ ] **Step 4.1: Kill any running acdream**
```powershell
Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 8
```
- [ ] **Step 4.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 4.3: Visual test matrix** with parallel retail observer of `+Acdream`. On the retail side, walk + run + jump + turn the toon and verify:
| Scenario | Expected |
|---|---|
| Walk forward 5 sec | acdream observer sees smooth glide, NO 1-Hz popping |
| Walk backward 5 sec | smooth glide backward (regression check vs commit `17a9ff1`) |
| Strafe left/right 5 sec each | smooth glide sideways |
| Stop, then run forward 5 sec | smooth glide at run speed |
| Jump from standstill 2-3× | curved arc, lands cleanly, NO endless rise |
| Jump while running 2-3× | arc preserves forward motion, lands cleanly |
| Turn quickly while running | heading tracks smoothly (not stuck at login direction) |
- [ ] **Step 4.4: User signs off OR files a regression**
- If smooth + jumps land + turning works → proceed to Tasks 5+6.
- If anything regresses → describe the symptom; parent dispatches a fix subagent or unsets the env-var for instant rollback.
---
## Task 5 — Cleanup commit (parallel with Task 6)
**Owner:** Sonnet subagent (general-purpose). Independent of Task 6.
**Files:**
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (delete env-var dual paths + soft-snap fields)
**Subagent dispatch prompt:**
> You are implementing Task 5 of Phase L.3.1+L.3.2: cleanup. The user has visually verified that `ACDREAM_INTERP_MANAGER=1` works correctly. Now collapse the dual-path scaffolding.
>
> **Repo:** `C:/Users/erikn/source/repos/acdream` — main — direct-to-main per CLAUDE.md.
>
> **What to do in `src/AcDream.App/Rendering/GameWindow.cs`:**
>
> 1. **In `OnLivePositionUpdated`**: delete the `if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1") { ... return; }` wrapper. Keep ONLY the new logic inside it. Delete the legacy hard-snap path that came after.
>
> 2. **In `TickAnimations` (per-frame remote tick)**: delete the `if/else` env-var gate. Keep ONLY the new path (`PositionManager.ComputeOffset` + `UpdatePhysicsInternal`). 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`. Also delete any related code in the call sites.
>
> 4. **Search for any remaining `ACDREAM_INTERP_MANAGER` references** in the codebase and confirm zero remain:
> ```bash
> grep -rn "ACDREAM_INTERP_MANAGER" "C:/Users/erikn/source/repos/acdream/src/" 2>&1
> ```
> Expected: no output.
>
> **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 2>&1 | tail -6
> ```
> Expected: 0 build errors. Same 4 pre-existing failures, no new ones.
>
> **Commit:**
> ```bash
> git add src/AcDream.App/Rendering/GameWindow.cs
> git commit -m "$(cat <<'EOF'
> chore(motion): remove ACDREAM_INTERP_MANAGER flag + dead legacy paths (L.3.1+L.3.2 cleanup)
>
> User has visually verified the new PositionManager + IsGrounded
> routing path works correctly. Collapses the env-var dual-path:
> deletes legacy hard-snap + apply_current_movement + Euler-extrapolate
> code from OnLivePositionUpdated and the per-frame remote tick.
> Deletes SnapResidualDecayRate + soft-snap residual fields from
> RemoteMotion.
>
> Single retail-faithful path remains. ~80 lines net deletion.
>
> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
> EOF
> )"
> ```
>
> **Report:**
> - Status, line counts deleted, files touched, test results.
**Steps for the parent:**
- [ ] **Step 5.1: Dispatch the cleanup subagent in parallel with Task 6** (one message, two Agent tool calls).
- [ ] **Step 5.2: Verify the commit landed**
```bash
cd C:/Users/erikn/source/repos/acdream && git log -1 --stat
```
- [ ] **Step 5.3: Confirm zero env-var references remain**
```bash
grep -rn "ACDREAM_INTERP_MANAGER" "C:/Users/erikn/source/repos/acdream/src/" 2>&1
```
Expected: no output.
- [ ] **Step 5.4: Re-run all tests in parent**
```bash
cd C:/Users/erikn/source/repos/acdream && dotnet test --no-build --nologo 2>&1 | tail -6
```
Expected: same baseline.
---
## Task 6 — Roadmap + spec status update (parallel with Task 5)
**Owner:** Parent. Mechanical doc updates.
**Files:**
- Modify: `docs/plans/2026-04-11-roadmap.md` (Phase L.3 entry — mark L.3.1+L.3.2 SHIPPED)
- Modify: `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` (add SHIPPED status banner)
**Steps:**
- [ ] **Step 6.1: Find the Phase L.3 entry in the roadmap**
```bash
grep -n "Phase L.3\|L.3.1\|L.3.2\|L.3.3" "C:/Users/erikn/source/repos/acdream/docs/plans/2026-04-11-roadmap.md"
```
If the roadmap doesn't yet have a Phase L.3 entry, add one between L.2 and M with the L.3.1+L.3.2 combined status = SHIPPED, L.3.3 status = PLANNED.
- [ ] **Step 6.2: Update the spec doc's status**
In `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md`, near the top (after the title / methodology), add or update a status line:
```markdown
**Status:** L.3.1+L.3.2 SHIPPED 2026-05-02. L.3.3 PLANNED.
```
- [ ] **Step 6.3: Commit (combined doc update)**
```bash
git add docs/plans/2026-04-11-roadmap.md docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md
git commit -m "$(cat <<'EOF'
docs(roadmap+spec): Phase L.3.1+L.3.2 shipped (L.3.3 pending)
Roadmap Phase L.3 entry updated. Spec status banner reflects the
combined L.3.1+L.3.2 deliverable as shipped after visual verification.
L.3.3 (MoveToManager) remains a separate sub-lane to be specced and
scheduled.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
```
---
## Verification Plan
End-to-end smoke test after Task 6:
```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 # 4 pre-existing failures only
git log --oneline -10 # see commits in order
grep -rn "ACDREAM_INTERP_MANAGER" src/ # zero hits (cleanup confirmed)
grep -rn "SnapResidualDecayRate" src/ # zero hits (deleted)
```
User can re-run the visual test matrix WITHOUT setting `ACDREAM_INTERP_MANAGER` (default behavior is now the new path) and confirm parity.
If everything's green → Phase L.3.1+L.3.2 done; brainstorm L.3.3 (MoveToManager) as the next sub-lane.
---
## Self-Review Notes
- **Spec coverage:** every section of the spec maps to a task here. PositionManager → Task 1; IsGrounded plumbing → Task 2; per-frame tick rewrite + RemoteMotion field + OnLivePositionUpdated rewrite → Task 3; cleanup → Task 5; doc updates → Task 6.
- **Already-shipped commits NOT rebuilt.** L.3.1's first 6 commits (f43f168 → e08accf) already provide InterpolationManager + GetMaxSpeed + Interp field + v1 routing + v1 tick + Omega.
- **Reverted commits** (5154a3e + f199a6a) were band-aids; their replacements live in Task 3.
- **Subagent failure handling:** if a subagent reports BLOCKED on Task 3 (the largest), break it into smaller pieces (3a: RemoteMotion field; 3b: OnLivePositionUpdated rewrite; 3c: TickAnimations rewrite) and dispatch sequentially. Don't let a confused subagent leave broken code in main.
- **Task 4's visual verification is the gate.** Tasks 5+6 only fire after user sign-off. If visual fails, dispatch a fix subagent before Tasks 5+6.