acdream/docs/superpowers/plans/2026-05-02-l3-positionmanager-jump.md
Erik d063ac884d 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>
2026-05-03 10:10:16 +02:00

35 KiB
Raw Blame History

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 (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:

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:

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:

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
    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
    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

    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:

    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:

    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:

    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:

    public readonly record struct EntityPositionUpdate(
        uint Guid,
        CreateObject.ServerPosition Position,
        System.Numerics.Vector3? Velocity);
    

    To:

    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:

    PositionUpdated?.Invoke(new EntityPositionUpdate(
        posUpdate.Value.Guid,
        posUpdate.Value.Position,
        posUpdate.Value.Velocity));
    

    To:

    PositionUpdated?.Invoke(new EntityPositionUpdate(
        posUpdate.Value.Guid,
        posUpdate.Value.Position,
        posUpdate.Value.Velocity,
        posUpdate.Value.IsGrounded));
    
  • Step 2.6: Build + test

    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

    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:

        /// <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:

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:

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:

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:

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:

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:

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:

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
    cd C:/Users/erikn/source/repos/acdream && git log -1 --stat src/AcDream.App/Rendering/GameWindow.cs
    
  • Step 3.3: Build + test in parent
    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

    Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | Stop-Process -Force
    Start-Sleep -Seconds 8
    
  • Step 4.2: Launch acdream with ACDREAM_INTERP_MANAGER=1

    $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:

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

    Expected: no output.

Build + test:

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:

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
    cd C:/Users/erikn/source/repos/acdream && git log -1 --stat
    
  • Step 5.3: Confirm zero env-var references remain
    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
    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

    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:

    **Status:** L.3.1+L.3.2 SHIPPED 2026-05-02. L.3.3 PLANNED.
    
  • Step 6.3: Commit (combined doc update)

    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:

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 (f43f168e08accf) 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.