# #185 Outdoor-Stairs Phantom Fix — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: this plan is intended for **inline execution by the > lead engineer with full context** (superpowers:executing-plans), NOT subagent dispatch — it edits > frozen collision internals and pins the fix empirically (CLAUDE.md: "Do not integrate via subagent > unless the subagent has the full context"). Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** the local player walks up the full house-on-stilts outdoor staircase with no jam and no jump. **Architecture:** restore retail's invariant that a grounded mover on a continuous walkable surface keeps `contact_plane_valid` on the forward move (`transitional_insert` short-circuit, pc 273244), so it never falls into the step-down recovery that cannot reach the *coplanar (at-level)* stair continuation and fabricates a horizontal `(0,1,0)` sliding normal (`PrecipiceSlide`, the #137/TS-4 family). The exact retention-loss line is pinned by a dat-backed replay test, then fixed faithfully. **Tech stack:** C# .NET 10, xUnit, `DatCollection` dat reader, `AcDream.Core.Physics` (`PhysicsEngine.ResolveWithTransition`, `Transition`, `SpherePath`, `ShadowObjectRegistry`). **Design spec:** `docs/superpowers/specs/2026-07-08-185-outdoor-stairs-fix-design.md` (read first). **Fixtures already captured (this session, in the scratchpad):** `185-recapture.jsonl` (jam pinned at `(131.71, 77.914, 61.485)`, `slidingNormal (0,1,0)`, `collisionNormal (0,0.78,0.62)`); `gfxdump/0x01000AC5.gfxobj.json` (step-box tread = poly id 4, local plane `(-0.625,0,0.781,-0.312)`); `[resolve-bldg]` per-object world origins, e.g. `0xF6822103 → entOrigin_lb (132.0,77.2,60.4)`, tread world verts `(132.75,77.495,61.015)…`. **Copy the two gfxobj dumps into `tests/AcDream.Core.Tests/Physics/fixtures/` in Task 1** so the test is self-contained. **Reference tests to mirror (read before writing Task 1):** `tests/AcDream.Core.Tests/Physics/Issue137CorridorSeamReplayTests.cs` (replay + probe-capture shape), `DoorBugTrajectoryReplayTests.cs` / `CellarUpTrajectoryReplayTests.cs` (registering a **building/shadow object** and replaying a trajectory — the outdoor-static harness #185 needs), `GfxObjDumpRoundTripTests.cs` (loading a gfxobj + its physics polys), `ShadowObjectRegistryMultiPartTests.cs` (`RegisterMultiPart` API). --- ## Task 1: Red replay test — reproduce the wedge over the real stair objects **Files:** - Create: `tests/AcDream.Core.Tests/Physics/Issue185OutdoorStairsSeamReplayTests.cs` - Create (fixtures): copy `…/scratchpad/gfxdump/0x01000AC5.gfxobj.json` (+ `0x01000ACA`) → `tests/AcDream.Core.Tests/Physics/fixtures/` - Read (harness API): `src/AcDream.Core/Physics/ShadowObjectRegistry.cs:92,152` (`Register` / `RegisterMultiPart`), `PhysicsEngine.ResolveWithTransition`, and the two building-replay reference tests above. - [ ] **Step 1: Read the reference harness.** Read `DoorBugTrajectoryReplayTests.cs` and `CellarUpTrajectoryReplayTests.cs` end-to-end to learn exactly how they (a) build a `PhysicsEngine`, (b) register a building object's collision shape (`ShadowShapeBuilder`/`RegisterMultiPart` from a Setup/GfxObj + world transform), and (c) drive `ResolveWithTransition`. Read `GfxObjDumpRoundTripTests.cs` for how a gfxobj's `ResolvedPolygons` (the physics polys, incl. tread poly id 4) are obtained from the dat. Confirm whether registration takes a Setup id or a GfxObj id; the stairs are landblock statics whose collision is the GfxObj's own BSP/polys (no Setup cylsphere). - [ ] **Step 2: Write the harness + the FAILING wedge test.** Build a `PhysicsEngine`, register the stair step objects at their captured world transforms (each = `entOrigin_lb` translation × the +90°-about-Z rotation that maps local tread normal `(-0.625,0,0.781)` → world `(0,-0.625,0.781)`), covering at least the objects spanning the jam seam (`0xF68221` parts 1–3 at entOrigin Y 76.2/76.7/77.2 **and** the next object up whose tread covers Y∈[77.5,78.0]+ — derive its origin as the +0.5 world-Y / +0.4 world-Z step from `0xF68221` part 3). Seed a grounded body on tread N (contact plane + walkable poly = the tread quad, world verts `(132.75,77.495,61.015),(131.25,77.495,61.015),(131.25,76.995,60.615),(132.75,76.995,60.615)`). Replay the forward run from `(131.72,77.49,61.15)` toward `(131.71,78.18,61.15)` for ~6 held-forward frames (flat target, +Y ≈ 0.2/frame — the movement controller sends flat, physics climbs). Use the `ResolveWithTransition` args from the capture: `sphereRadius 0.48`, `sphereHeight 1.835`, `stepUpHeight 0.6`, `stepDownHeight 1.5`, `isOnGround true`, `moverFlags IsPlayer|EdgeSlide`. ```csharp // The wedge assertion (RED before the fix): the run must climb PAST the seam, not pin at Y≈77.9. Assert.True(pos.Y > 78.10f, $"Player must climb past the tread seam (reached Y={pos.Y:F3}); pinned at ~77.9 with " + $"slidingNormal=(0,1,0) = the #185 fabricated-precipice wedge."); Assert.False(body.TransientState.HasFlag(TransientStateFlags.Sliding), "A continuous walkable ramp seam must not persist a horizontal sliding normal (#137 family)."); ``` - [ ] **Step 3: Run it — verify it FAILS (reproduces the wedge).** Run: `dotnet test tests/AcDream.Core.Tests --filter "FullyQualifiedName~Issue185OutdoorStairsSeamReplay" -v n` Expected: **FAIL** — `pos.Y` pins at ~77.9 and/or `Sliding` flag set (the wedge reproduced offline). **If it does NOT reproduce** (climbs clean offline), the live trigger involves state beyond the replay's reach (as the #137 corridor replay initially did) — STOP and add the missing seed (the exact `bodyBefore` from `185-recapture.jsonl` tick 4277: contact plane, walkable verts, and the prior-frame `slidingNormal`) before proceeding; do not weaken the assertion. - [ ] **Step 4: Commit the red test.** ```bash git add tests/AcDream.Core.Tests/Physics/Issue185OutdoorStairsSeamReplayTests.cs \ tests/AcDream.Core.Tests/Physics/fixtures/0x01000AC5.gfxobj.json \ tests/AcDream.Core.Tests/Physics/fixtures/0x01000ACA.gfxobj.json git commit -m "test(#185): red — dat-backed outdoor-stairs seam replay reproduces the wedge Registers the real stair step-boxes and replays the captured forward run; pins the jam (no advance past Y~77.9, persisted (0,1,0) sliding normal). RED before the fix. Co-Authored-By: Claude Opus 4.8 " ``` --- ## Task 2: Pin the exact retention-loss site **Files:** - Modify (temporary probe only, reverted at end): the replay test — wrap the seam-crossing frame with `PhysicsDiagnostics.ProbeContactPlaneEnabled = true` (env `ACDREAM_PROBE_CONTACT_PLANE`) + `ProbeStepWalkEnabled` + `ProbeIndoorBspEnabled`, capturing `Console.Out` (exactly as `Issue137CorridorSeamReplayTests.SeamShake_WestBoundary_SnapshotExact_Advances` does, lines 207–236). - Read: `src/AcDream.Core/Physics/TransitionTypes.cs` (TransitionalInsert loop `~1030–1110` Slid-clears at 1047/1073/1102; the neg-poly/step-down dispatch `~1339`; `ValidateTransition` LKCP retention `~4645–4735`), `src/AcDream.Core/Physics/PhysicsEngine.cs` (seed `~1004`, writeback `~1131`). - [ ] **Step 1: Capture the seam-crossing frame's contact-plane transitions.** Run the red test with the probes on; in the emitted trace, find the frame that reaches the step-down dispatch with `ci.ContactPlaneValid == false`, and identify **which** upstream event set it false: (a) an env/building/object channel returned **Slid** (clears cp at 1047/1073/1102), or (b) the seed at `PhysicsEngine.cs:1004` never re-validated and the forward main-sweep set no fresh contact plane, or (c) `ValidateTransition`'s LKCP proximity guard (`~4698`, `radius+ε > |angle|`) DROPPED the plane because the floated sphere is >radius from tread N's plane. Record the concrete site (file:line + the branch) in a scratch note; this selects the Task-3 candidate. - [ ] **Step 2: Confirm the retail-faithful expectation for that site.** Grep the named-retail decomp for the corresponding retail function (the Slid channel → `BSPTREE::find_collisions` Contact branch pc 323838+, or `set_contact_plane`/`get_object_info` pc 272547 for the retain path) and confirm what retail does that keeps `contact_plane_valid` here. Do NOT invent behavior — cite the pc line. This is the gate: the fix must make acdream match that retail behavior, not add a guard. --- ## Task 3: Apply the retail-faithful grounding-retention fix **Files:** - Modify: the pinned site from Task 2 in `src/AcDream.Core/Physics/TransitionTypes.cs` (or `PhysicsEngine.cs`). Exactly ONE of the candidates below, chosen by Task 2's pin + Task 2's retail cite. - [ ] **Step 1: Apply the fix matching the pinned site.** Candidate edits (pick the one Task 2 selected): - **Candidate A (pin = spurious Slid on the coplanar-graze):** the grounded foot grazing the coplanar continuation tread is being routed to a Slid (clearing cp) where retail records a walkable contact/step (keeps cp). Fix the specific collision-response branch so a grounded mover grazing a **walkable** (normal.Z ≥ FloorZ) coplanar poly it is resting on retains/sets the contact plane instead of clearing it — mirroring the retail Contact-branch path Task 2 cited. Do not blanket-skip Slid; scope to the walkable-coplanar-continuation case. - **Candidate B (pin = forward move sets no fresh contact plane):** ensure the grounded forward move re-establishes `ci.ContactPlane` from the coplanar tread the sphere rests on (retail's forward landing keeps `contact_plane_valid`, pc 273244) — e.g. the walkable-landing/`find_walkable` on the forward insert must find the at-level continuation, not only a below-foot surface. - **Candidate C (pin = LKCP proximity guard drops the plane):** the floated sphere separates from tread N's plane by >radius so the guard (`~4698`) drops it — but the sphere is genuinely resting on the coplanar continuation. Fix so the proximity check is evaluated against the surface actually under the sphere (the coplanar continuation), matching retail's `get_object_info` retain logic (pc 272547), rather than dropping grounding at the seam. Each candidate is a **localized** change. If the pinned faithful fix is larger than a localized edit, STOP and return to the user (scope guard, per the spec §3). - [ ] **Step 2: Register bookkeeping in the same commit.** Amend `docs/architecture/retail-divergence-register.md` row **TS-4** (or add a new row) to record the grounding-retention change and its retail oracle (pc 273244 / the Task-2 cite); retire/narrow as the port dictates. --- ## Task 4: Green the replay + remove the temporary probes - [ ] **Step 1: Remove the temporary probe wrapping** added in Task 2 (leave the test asserting behavior, no probe side effects). - [ ] **Step 2: Run the red test — verify it now PASSES.** Run: `dotnet test tests/AcDream.Core.Tests --filter "FullyQualifiedName~Issue185OutdoorStairsSeamReplay" -v n` Expected: **PASS** — the run climbs past Y=78.10 with no persisted sliding normal. - [ ] **Step 3: Commit the fix + green test.** ```bash git add -A git commit -m "fix(#185): keep the mover grounded on a continuous walkable ramp seam . The grounded forward move now retains contact_plane_valid across the coplanar stair seam (retail transitional_insert pc 273244) instead of dropping into the step-down recovery that can't reach the at-level continuation and fabricates a horizontal (0,1,0) sliding normal (PrecipiceSlide, #137/TS-4 family). Register TS-4 amended. Co-Authored-By: Claude Opus 4.8 " ``` --- ## Task 5: Regression — the shared grounded-move path - [ ] **Step 1: Run the #137 suites (must stay green / un-skipped).** Run: `dotnet test tests/AcDream.Core.Tests --filter "FullyQualifiedName~Issue137" -v n` Expected: PASS (`Issue137CorridorSeamReplayTests`, `Issue137SlidingNormalLifecycleTests`, `Issue137CorridorSeamInspectionTests`). Also run `CellarUpTrajectoryReplayTests`, `BSPStepUpTests`, `SphereCollisionFamilyTests` — the step-up/step-down/slide neighbors. - [ ] **Step 2: Full build + test.** Run: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug` then `dotnet test` (Core / App / UI / Net). Expected: all green, 0 warnings. - [ ] **Step 3: If any regressed,** the fix over-reached (not scoped to the walkable-coplanar-continuation case) — narrow it and re-run; do NOT loosen the regression assertions. --- ## Task 6: Bookkeeping (docs + memory) - [ ] **Step 1:** `docs/ISSUES.md` — move **#185** to Recently closed with the fix SHA. - [ ] **Step 2:** `claude-memory/project_physics_collision_digest.md` — add the #185 banner (root cause + the fix + DO-NOT-RETRY: don't un-zero Z, don't touch step-up budget, fix the grounding provenance). - [ ] **Step 3:** roadmap/milestones — note the M1.5 #137-family stair fix if a phase line applies. - [ ] **Step 4: Commit the bookkeeping.** ```bash git add docs/ISSUES.md claude-memory/project_physics_collision_digest.md git commit -m "docs(#185): close — grounding-retention fix for the outdoor-stairs phantom Co-Authored-By: Claude Opus 4.8 " ``` --- ## Task 7: Live visual gate (USER — acceptance) - [ ] **Step 1: Build green, then launch** (canonical live command; no pre-close of running clients). - [ ] **Step 2: USER walks `+Acdream` up the FULL outdoor staircase** — must climb with no jam and no jump. Then regression spot-checks: indoor cellar/corridor stairs, another outdoor building, a ramp, flat ground — no new phantom blocks AND no sliding through walkable ramps/edges. - [ ] **Step 3:** on pass, #185 is closed (bookkeeping already landed in Task 6). On fail, capture with the apparatus (§6 of the spec) and re-pin — do not guess a follow-up.