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 atc4446e7lists L.3.1's already-shipped 6 commits; this plan picks up from the revert at1641d6e. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
35 KiB
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+927636eTask 1 — InterpolationManager9c5634a+5b26d28Task 2 — MotionInterpreter.GetMaxSpeed517a3ceTask 3 — RemoteMotion.Interp field062e19fTask 4 — OnLivePositionUpdated env-var routing v1ae79e34Task 5 — Per-frame Interp.AdjustOffset v1e08accfTask 6 — VectorUpdate.Omega1641d6erevert of band-aidsc4446e7spec 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.mdsection "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.cswith EXACTLY these 6 test names (these are the contract):
ComputeOffset_StationaryRemote_BothSourcesZero_NoMotion
- seqVel = Vector3.Zero, no enqueued nodes in interp
- Assert: returned offset == Vector3.Zero
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)
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)
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
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
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 viafile sealed classif needed. Readtests/AcDream.Core.Tests/Physics/MotionInterpreterTests.csfor 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:
PositionManageris public sealed classComputeOffsetis 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
Expected: commit message starts withcd C:/Users/erikn/source/repos/acdream && git log -1 --stat src/AcDream.Core/Physics/PositionManager.csfeat(physics): PositionManager combiner class. - Step 1.3: Re-run tests in parent
Expected: 6 tests pass.cd C:/Users/erikn/source/repos/acdream && dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~PositionManager" - Step 1.4: Dispatch spec compliance reviewer (use
general-purpose, Sonnet). Verify the 6 tests have the EXACT names listed and verifyComputeOffsetalgorithm 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(addIsGroundedtoParsedrecord) - Modify:
src/AcDream.Core.Net/Messages/UpdatePosition.cs:166(populateIsGroundedin the constructor call) - Modify:
src/AcDream.Core.Net/WorldSession.cs:110-113(addIsGroundedtoEntityPositionUpdaterecord) - Modify:
src/AcDream.Core.Net/WorldSession.cs:711-714(passposUpdate.Value.IsGroundedthrough)
Steps:
-
Step 2.1: Read existing
UpdatePosition.Parsedrecord + TryParse returngrep -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
IsGroundedfield toUpdatePosition.ParsedEdit
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
IsGroundedin theParsedconstructor call (~line 166)Find the line
return new Parsed(guid, serverPos, velocity, placementId,(~line 166) and change to pass(flags & PositionFlags.IsGrounded) != 0as 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
IsGroundedfield toWorldSession.EntityPositionUpdateEdit
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
IsGroundedthrough 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 -6Expected: 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.csto 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:
RemoteMotionclass gainsPositionfieldFind the existing
Interpfield (added in commit517a3ce). 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
OnLivePositionUpdatedenv-var-on branchFind the existing env-var-on block in
OnLivePositionUpdated(was added at commit062e19f). 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
elsebranch (env-var unset) STAYS UNCHANGED.If
ExtractYawFromQuaterniondoesn't exist anymore (it might have been removed in the revert), re-add it near the original location (search for it in commit062e19f'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
TickAnimationsenv-var-on branchFind 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
elsebranch (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 -6Expected: 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.Positionfield added (alongside existingInterp)OnLivePositionUpdatedenv-var branch has 3 sub-branches: airborne return, landing transition, grounded routing (snap or enqueue)OnLivePositionUpdatedlegacyelsebranch UNCHANGEDTickAnimationsenv-var branch usesPositionManager.ComputeOffsetexclusively (no directAdjustOffsetcall)TickAnimationslegacyelsebranch UNCHANGEDExtractYawFromQuaternionhelper present (re-add if missing)OnLiveVectorUpdatedUNTOUCHED (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
elsepath 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
Expected: 0 build errors. Same 4 pre-existing failures.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 - Step 3.4: Spec compliance review (general-purpose subagent). Verify the rewrite matches the spec's pseudocode exactly. Verify legacy
elsepaths 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=1works 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:
In
OnLivePositionUpdated: delete theif (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1") { ... return; }wrapper. Keep ONLY the new logic inside it. Delete the legacy hard-snap path that came after.In
TickAnimations(per-frame remote tick): delete theif/elseenv-var gate. Keep ONLY the new path (PositionManager.ComputeOffset+UpdatePhysicsInternal). Delete the legacyapply_current_movement+force-OnWalkable+ Euler-extrapolate code in theelsebranch.In the
RemoteMotionclass (~line 224): deleteSnapResidualDecayRateand any soft-snap residual fields. Search for_snapResidual,SnapResidualDecayRate,SoftSnap. Also delete any related code in the call sites.Search for any remaining
ACDREAM_INTERP_MANAGERreferences in the codebase and confirm zero remain:grep -rn "ACDREAM_INTERP_MANAGER" "C:/Users/erikn/source/repos/acdream/src/" 2>&1Expected: 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 -6Expected: 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
Expected: no output.grep -rn "ACDREAM_INTERP_MANAGER" "C:/Users/erikn/source/repos/acdream/src/" 2>&1 - Step 5.4: Re-run all tests in parent
Expected: same baseline.cd C:/Users/erikn/source/repos/acdream && dotnet test --no-build --nologo 2>&1 | tail -6
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 (
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.