diag(phys): A6.P3 #98 — [step-walk-adjust] probe inside AdjustOffset
Adds one log line per AdjustOffset call (gated by ACDREAM_PROBE_STEP_WALK) naming the branch taken (no-cp / no-cp-slide / slide-degenerate / slide-crease / into-plane / away-plane, optionally +safety-push) plus zGain = output.Z - input.Z. No math or control-flow changes — pure observability so the next capture can disambiguate the three failure-mode hypotheses for the cellar-ramp climb cap. Re-reading the existing capture (a6-issue98-negpoly-...log) showed the sphere DOES climb 90.00 -> 92.79 (2.79 m gain), then caps, contradicting the divergence comparison's "no altitude gain" framing. The real question is what stops the climb at world Z ~= 92.79 with the cottage floor still 1.21 m higher. Existing [step-walk] probes wrap AdjustOffset; this new probe reveals which branch the projection takes. Fix plan with the four-branch decision tree at docs/superpowers/plans/2026-05-23-a6-p3-issue98-cellar-up-fix.md. Test baseline maintained: 1167 + 8. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
67005e21f1
commit
8a232a3e6e
3 changed files with 562 additions and 0 deletions
481
docs/superpowers/plans/2026-05-23-a6-p3-issue98-cellar-up-fix.md
Normal file
481
docs/superpowers/plans/2026-05-23-a6-p3-issue98-cellar-up-fix.md
Normal file
|
|
@ -0,0 +1,481 @@
|
|||
# A6.P3 issue #98 — cellar-up fix plan (diagnostic-first)
|
||||
|
||||
> **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:** Close issue #98 (sphere stuck on cottage cellar ramp) by identifying the EXACT failure point through a focused diagnostic probe, then writing a minimal evidence-driven fix. The user can walk up and out of a Holtburg cottage cellar in the live client.
|
||||
|
||||
**Architecture:** Three phases. Phase 0 re-reads the existing capture for everything the divergence comparison missed — no code. Phase 1 adds ONE focused probe at the only site where existing instrumentation is silent (inside `AdjustOffset`). Phase 2 runs the probe and decides which of three branches the fix must take. Phase 3 writes the fix against the replay harness as TDD oracle.
|
||||
|
||||
**Tech Stack:** C# .NET 10. The deterministic replay loop ([Issue98CellarUpReplayTests.cs](../../tests/AcDream.Core.Tests/Physics/Issue98CellarUpReplayTests.cs), <200ms) is the inner test loop. The cdb capture script ([tools/cdb/issue98-runner.ps1](../../tools/cdb/issue98-runner.ps1)) is the outer ground-truth loop.
|
||||
|
||||
**Risk:** Four prior sessions guessed-and-shipped (10+ variants). The pattern: convinced of diagnosis → ship fix → user reports "still can't pass." This plan refuses to ship code until the diagnostic data NAMES the failure site.
|
||||
|
||||
---
|
||||
|
||||
## What the existing log already proves (Phase 0 already partially done)
|
||||
|
||||
The slice 7 handoff and the divergence comparison both claimed *"our sphere oscillates at world Z ≈ 92.01 with no altitude gain."* That framing is **wrong**. Scanning the 2,589 `[step-walk]` lines in [a6-issue98-negpoly-20260523-135032.out.log](../../a6-issue98-negpoly-20260523-135032.out.log) shows:
|
||||
|
||||
| What the log shows | Implication |
|
||||
|---|---|
|
||||
| Sphere `cur` Z climbs **90.00 → 92.79** across the capture (2.79 m gain). | The climb works for most of the ramp. Z gain per `[step-walk] site=after-adjust` is +0.197 to +0.227 when offset points INTO the ramp normal. |
|
||||
| Climb **caps at world Z ≈ 92.79**, then descends back. | Something at the top of the ramp prevents the last ~1.21 m of climb to cottage floor (world Z=94). |
|
||||
| At the peak (line 17458–17485 in the log): `before-insert` has `check=(141.58, 7.18, 92.79)`, `walkPoly=True`. `after-insert` has same position but **`walkPoly=False`** + `winterp=-0.0000`. | `DoStepDown` at the peak burned `WalkInterp` from 1.0 → 0 in one tick and the sphere ended up off the walkable polygon. |
|
||||
| `cell=0xA9B40147->0xA9B40147` for every line. | Our sphere never transitions to a cottage cell (`0xA9B40143` / `0xA9B40146`) during the climb. Retail's BPA capture shows queries against cottage cells starting at BPA hit#430. |
|
||||
| ContactPlane normal is `(0, +0.719, +0.695)` in our log. The divergence doc says retail's ramp normal is `(0, -0.719, +0.695)` (sign-flipped) — but retail's BPE NEVER writes the ramp as ContactPlane at all (only flat cellar floor or flat cottage floor). | Retail does not use the ramp as a ContactPlane. Our code does. This is a SHAPE-of-the-fix question for later; do not act on it before the diagnostic confirms it matters. |
|
||||
|
||||
**Revised hypothesis (informed by the data, not the divergence doc):**
|
||||
|
||||
The climb works while the sphere is mid-ramp. It **fails at the top of the ramp** where the polygon ends. Three mutually-exclusive failure modes are plausible:
|
||||
|
||||
1. **Geometric cap.** The ramp polygon physically ends at world Z ≈ 92.79; there is no further walkable surface within the step-up reach. Retail's sphere reaches the cottage floor by a transition we never trigger.
|
||||
2. **Cell-set divergence.** At world Z ≈ 92.79 the sphere overlaps cottage cell volumes, but our `CheckOtherCells` either doesn't query the cottage cell, or queries it with the wrong sphere reference (foot vs lifted). Retail's BPA hit#430 / hit#434 show queries against TWO different leaves at SIGNIFICANTLY different sphere positions (cell A: local 0.48, cell B: local 0.63) — that's the retail two-step-up pattern at work, which we may not be doing.
|
||||
3. **WalkInterp depletion before forward motion applies.** The peak `[step-walk]` line at 17485 shows `winterp=-0.0000` after `DoStepDown`. If `DoStepDown` consumed all WalkInterp on the lift (the slice 7 handoff's reading), the next tick starts WalkInterp at 1.0 again — so this isn't the inter-tick blocker the handoff thought. But INTRA-tick, the next call's `AdjustOffset` runs with no remaining WalkInterp, which could legitimately gate further motion.
|
||||
|
||||
**The point of Phase 1 is to disambiguate between (1), (2), and (3).**
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — focused diagnostic (no code logic changes)
|
||||
|
||||
Add ONE probe site inside `AdjustOffset` and rerun. The existing `[step-walk]` probe instruments the call from the outside (req → adj across the call) but never reveals which **branch** AdjustOffset took. The new probe reveals the branch; that's the missing signal.
|
||||
|
||||
### Task 1.1: Add `[step-walk-adjust]` probe call inside AdjustOffset
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.Core/Physics/PhysicsDiagnostics.cs` (add one new helper)
|
||||
- Modify: `src/AcDream.Core/Physics/TransitionTypes.cs:2635-2739` (add 4 probe calls inside AdjustOffset's branches)
|
||||
|
||||
- [ ] **Step 1: Add the diagnostic emitter to PhysicsDiagnostics.cs**
|
||||
|
||||
Add this method right after `LogStepWalk` (currently ending around line 700). It is intentionally separate from `LogStepWalk` so the line format stays short and greppable.
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// A6.P3 issue #98 (2026-05-23) — focused probe INSIDE AdjustOffset
|
||||
/// revealing which branch was taken and the Z gain per call. Pair with
|
||||
/// <c>[step-walk] site=after-adjust</c> at the call site to triangulate
|
||||
/// where the projection ends up. Caller MUST guard with
|
||||
/// <c>if (!ProbeStepWalkEnabled) return;</c> before calling.
|
||||
/// </summary>
|
||||
public static void LogStepWalkAdjust(
|
||||
string branch,
|
||||
Vector3 input,
|
||||
Vector3 output,
|
||||
Plane? contactPlane,
|
||||
bool slidingValid,
|
||||
Vector3 slidingNormal,
|
||||
float collisionAngle,
|
||||
float walkInterp)
|
||||
{
|
||||
var culture = System.Globalization.CultureInfo.InvariantCulture;
|
||||
|
||||
string cpDesc = contactPlane is { } cp
|
||||
? string.Format(culture,
|
||||
"n=({0:F4},{1:F4},{2:F4}) d={3:F4}",
|
||||
cp.Normal.X, cp.Normal.Y, cp.Normal.Z, cp.D)
|
||||
: "n/a";
|
||||
|
||||
string slideDesc = slidingValid
|
||||
? string.Format(culture,
|
||||
"({0:F4},{1:F4},{2:F4})",
|
||||
slidingNormal.X, slidingNormal.Y, slidingNormal.Z)
|
||||
: "n/a";
|
||||
|
||||
Console.WriteLine(string.Format(culture,
|
||||
"[step-walk-adjust] branch={0} input=({1:F4},{2:F4},{3:F4}) " +
|
||||
"output=({4:F4},{5:F4},{6:F4}) zGain={7:F4} " +
|
||||
"cp={8} slide={9} colAngle={10:F4} winterp={11:F4}",
|
||||
branch,
|
||||
input.X, input.Y, input.Z,
|
||||
output.X, output.Y, output.Z,
|
||||
output.Z - input.Z,
|
||||
cpDesc, slideDesc, collisionAngle, walkInterp));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Wire the probe at the four AdjustOffset branches**
|
||||
|
||||
Open [TransitionTypes.cs](../../src/AcDream.Core/Physics/TransitionTypes.cs) at line 2635 (`private Vector3 AdjustOffset(Vector3 offset)`). The body currently has FOUR exit/branch points:
|
||||
|
||||
1. **no-cp** path (line 2654–2659): `if (!ci.ContactPlaneValid) return result;`
|
||||
2. **slide** path (lines 2665–2676): `if (checkSlide) { ... result = ... }`
|
||||
3. **into-plane** path (lines 2677–2681): `else if (collisionAngle <= 0f) { result -= ... }`
|
||||
4. **away-plane** path (lines 2682–2687): `else { result -= ... }` — same arithmetic as `into-plane`, kept distinct for the probe.
|
||||
|
||||
At the FINAL return (line 2738 `return result;`), emit ONE probe line that captures the branch taken and the final result.
|
||||
|
||||
Replace lines 2635–2739 with a body that tracks the branch token (initialize to `"unknown"`) and assigns it at each branch point. Concretely:
|
||||
|
||||
```csharp
|
||||
private Vector3 AdjustOffset(Vector3 offset)
|
||||
{
|
||||
var sp = SpherePath;
|
||||
var ci = CollisionInfo;
|
||||
|
||||
Vector3 result = offset;
|
||||
bool checkSlide = false;
|
||||
string branch = "init"; // ← new
|
||||
|
||||
// Check if we should apply sliding.
|
||||
float slidingAngle = Vector3.Dot(result, ci.SlidingNormal);
|
||||
if (ci.SlidingNormalValid)
|
||||
{
|
||||
if (slidingAngle < 0f)
|
||||
checkSlide = true;
|
||||
else
|
||||
ci.SlidingNormalValid = false;
|
||||
}
|
||||
|
||||
// No contact plane — simple slide projection.
|
||||
if (!ci.ContactPlaneValid)
|
||||
{
|
||||
if (checkSlide)
|
||||
{
|
||||
result -= ci.SlidingNormal * slidingAngle;
|
||||
branch = "no-cp-slide"; // ← new
|
||||
}
|
||||
else
|
||||
{
|
||||
branch = "no-cp"; // ← new
|
||||
}
|
||||
|
||||
if (PhysicsDiagnostics.ProbeStepWalkEnabled) // ← new
|
||||
PhysicsDiagnostics.LogStepWalkAdjust(
|
||||
branch, offset, result,
|
||||
contactPlane: null,
|
||||
slidingValid: ci.SlidingNormalValid,
|
||||
slidingNormal: ci.SlidingNormal,
|
||||
collisionAngle: 0f,
|
||||
walkInterp: sp.WalkInterp);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Have a contact plane — project movement onto the contact surface.
|
||||
float collisionAngle = Vector3.Dot(result, ci.ContactPlane.Normal);
|
||||
Vector3 slideOffset = Vector3.Cross(ci.ContactPlane.Normal, ci.SlidingNormal);
|
||||
|
||||
if (checkSlide)
|
||||
{
|
||||
// Project movement along the crease between contact and slide planes.
|
||||
float slideLen = slideOffset.Length();
|
||||
if (slideLen < PhysicsGlobals.EPSILON)
|
||||
{
|
||||
result = Vector3.Zero;
|
||||
branch = "slide-degenerate"; // ← new
|
||||
}
|
||||
else
|
||||
{
|
||||
slideOffset /= slideLen;
|
||||
result = Vector3.Dot(slideOffset, result) * slideOffset;
|
||||
branch = "slide-crease"; // ← new
|
||||
}
|
||||
}
|
||||
else if (collisionAngle <= 0f)
|
||||
{
|
||||
// Moving into the contact plane: remove component into the plane.
|
||||
result -= ci.ContactPlane.Normal * collisionAngle;
|
||||
branch = "into-plane"; // ← new
|
||||
}
|
||||
else
|
||||
{
|
||||
// Moving away from contact plane: snap to plane surface.
|
||||
result -= ci.ContactPlane.Normal * collisionAngle;
|
||||
branch = "away-plane"; // ← new
|
||||
}
|
||||
|
||||
// ── (Existing safety check unchanged — keep lines 2689–2736 verbatim) ──
|
||||
if (ci.ContactPlaneCellId != 0 && !ci.ContactPlaneIsWater)
|
||||
{
|
||||
Vector3 globCenter = sp.GlobalSphere[0].Origin;
|
||||
float radius = sp.GlobalSphere[0].Radius;
|
||||
|
||||
float dist = Vector3.Dot(globCenter, ci.ContactPlane.Normal)
|
||||
+ ci.ContactPlane.D;
|
||||
|
||||
float naturalRestingDist = radius * ci.ContactPlane.Normal.Z;
|
||||
|
||||
if (dist < naturalRestingDist - PhysicsGlobals.EPSILON)
|
||||
{
|
||||
float zDist = (naturalRestingDist - dist) / ci.ContactPlane.Normal.Z;
|
||||
if (radius > MathF.Abs(zDist))
|
||||
{
|
||||
sp.AddOffsetToCheckPos(new Vector3(0f, 0f, zDist));
|
||||
branch += "+safety-push"; // ← new — branch annotation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (PhysicsDiagnostics.ProbeStepWalkEnabled) // ← new
|
||||
PhysicsDiagnostics.LogStepWalkAdjust(
|
||||
branch, offset, result,
|
||||
contactPlane: ci.ContactPlane,
|
||||
slidingValid: ci.SlidingNormalValid,
|
||||
slidingNormal: ci.SlidingNormal,
|
||||
collisionAngle: collisionAngle,
|
||||
walkInterp: sp.WalkInterp);
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
**What this adds:** ONE log line per AdjustOffset call (probe-gated) naming the branch and showing Z gain. Nothing else changes — the math is identical.
|
||||
|
||||
- [ ] **Step 3: Run dotnet build to confirm the edit compiles**
|
||||
|
||||
```powershell
|
||||
dotnet build src\AcDream.Core\AcDream.Core.csproj -c Debug
|
||||
```
|
||||
|
||||
Expected: green build. The diff is additive (one new diagnostic helper + four single-line probe gates inside an existing method).
|
||||
|
||||
- [ ] **Step 4: Run dotnet test to confirm the apparatus tests stay green**
|
||||
|
||||
```powershell
|
||||
dotnet test tests\AcDream.Core.Tests\AcDream.Core.Tests.csproj -c Debug --filter "FullyQualifiedName~Issue98CellarUpReplayTests"
|
||||
```
|
||||
|
||||
Expected: all 7 Issue98CellarUpReplayTests pass (they document the bug; the failing-frame assertions still pin the current behavior). No new failures elsewhere.
|
||||
|
||||
- [ ] **Step 5: Commit the probe-only change**
|
||||
|
||||
```powershell
|
||||
git add src\AcDream.Core\Physics\PhysicsDiagnostics.cs src\AcDream.Core\Physics\TransitionTypes.cs
|
||||
git commit -m "diag(phys): A6.P3 #98 — [step-walk-adjust] probe inside AdjustOffset
|
||||
|
||||
Adds one log line per AdjustOffset call (gated by ACDREAM_PROBE_STEP_WALK)
|
||||
naming the branch taken (no-cp / no-cp-slide / slide-degenerate /
|
||||
slide-crease / into-plane / away-plane, optionally +safety-push) plus
|
||||
zGain = output.Z - input.Z.
|
||||
|
||||
No math changes — pure observability so the next capture can disambiguate
|
||||
the three failure-mode hypotheses for the cellar-ramp climb cap at
|
||||
world Z ≈ 92.79."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1.2: Capture with the new probe
|
||||
|
||||
- [ ] **Step 1: Confirm ACE is up and the test character is in the cottage cellar**
|
||||
|
||||
The character is `+Acdream` (server guid `0x5000000A`). Stand on the cellar ramp, facing the top.
|
||||
|
||||
- [ ] **Step 2: Launch the client with the step-walk probe enabled**
|
||||
|
||||
```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_PROBE_STEP_WALK = "1"
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
|
||||
Tee-Object -FilePath "a6-issue98-stepwalkadjust-$timestamp.out.log"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: User walks SLOWLY up the cellar ramp until stuck — no 180° turn-around**
|
||||
|
||||
The previous capture (negpoly log) polluted the trajectory because the user turned around and walked back. This capture must be a clean monotone climb.
|
||||
|
||||
- [ ] **Step 4: User closes the client (graceful — Alt-F4 / window close, not Ctrl-C)**
|
||||
|
||||
Per the logout-before-reconnect rules in CLAUDE.md — hard kill costs 3 minutes of ACE session recovery.
|
||||
|
||||
- [ ] **Step 5: Snapshot the capture to docs/research**
|
||||
|
||||
```powershell
|
||||
$captureDir = "docs\research\2026-05-23-a6-captures\stepwalkadjust"
|
||||
New-Item -ItemType Directory -Force $captureDir | Out-Null
|
||||
Copy-Item "a6-issue98-stepwalkadjust-$timestamp.out.log" "$captureDir\acdream.log"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1.3: Analyze the new probe data
|
||||
|
||||
- [ ] **Step 1: Extract the [step-walk-adjust] lines from the climb portion only**
|
||||
|
||||
```powershell
|
||||
Select-String "step-walk-adjust" "docs\research\2026-05-23-a6-captures\stepwalkadjust\acdream.log" |
|
||||
Select-Object -ExpandProperty Line |
|
||||
Set-Content "docs\research\2026-05-23-a6-captures\stepwalkadjust\adjust-only.log"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Cross-correlate [step-walk-adjust] with [step-walk] site=after-adjust by line position**
|
||||
|
||||
For each ramp-climb tick, you should see the pattern:
|
||||
```
|
||||
[step-walk] site=after-adjust cur=(...) req=(rX,rY,0) adj=(aX,aY,aZ) ...
|
||||
[step-walk-adjust] branch=... input=(rX,rY,0) output=(aX,aY,aZ) zGain=aZ cp=(...) ...
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Classify the climb by branch token**
|
||||
|
||||
Walk forward through the lines from the start of the climb to the peak (world Z ≈ 92.79). Build a histogram:
|
||||
|
||||
```
|
||||
branch=into-plane — how many calls? Avg zGain?
|
||||
branch=away-plane — how many? Avg zGain?
|
||||
branch=slide-crease — how many? Avg zGain?
|
||||
branch=no-cp — how many? (means ContactPlaneValid cleared mid-climb)
|
||||
+safety-push annotation — how often? At what zGain?
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Decide which Phase 2 branch the fix takes**
|
||||
|
||||
Decision tree (read the histogram + the climb-cap moment together):
|
||||
|
||||
| Observation | Implies fix target | Phase 2 branch |
|
||||
|---|---|---|
|
||||
| `into-plane` dominates the climb, +zGain ~0.2/call, then at peak the branch flips to `no-cp` or `away-plane` with zero zGain | **Target A: ContactPlane is being cleared / replaced at the ramp top.** Fix: investigate why the ramp's CP is dropping when the climb is incomplete. | **Branch A** |
|
||||
| `into-plane` continues at peak but zGain becomes near-zero (offset.Y trends to zero) | **Target B: forward motion is being consumed elsewhere.** Either by step-up burning WalkInterp before AdjustOffset gets the offset, or by the offset being slid horizontally by a wall hit. Look at `[step-walk] site=before-insert` for a Y-collapsing pattern. | **Branch B** |
|
||||
| `into-plane` at peak with correct zGain but CurPos doesn't advance | **Target C: the offset is computed correctly but never committed.** Look at `TransitionalInsert` / `Insert` for a path that returns before commit. | **Branch C** |
|
||||
| Any branch with frequent `slide-crease` mid-climb | **Side-finding: a SlidingNormal is being set against the ramp.** Should not happen on a clean slope — points at a wall poly being mis-classified. | **Branch D (side-investigation, not the fix)** |
|
||||
|
||||
- [ ] **Step 5: Write a 1-page findings note**
|
||||
|
||||
Save to `docs/research/2026-05-23-a6-stepwalkadjust-findings.md`. Format:
|
||||
|
||||
```markdown
|
||||
# A6.P3 #98 — [step-walk-adjust] capture analysis (YYYY-MM-DD)
|
||||
|
||||
## Climb branch histogram (90.00 → peak)
|
||||
- into-plane: X calls, avg zGain=Y
|
||||
- away-plane: ...
|
||||
- ...
|
||||
|
||||
## At the climb cap (world Z ≈ ZZZ):
|
||||
- Branch flipped from X to Y at log line NNN
|
||||
- ContactPlane normal at cap: (...)
|
||||
- WalkInterp at cap: ...
|
||||
|
||||
## Conclusion: Fix target is Branch A / B / C / D.
|
||||
|
||||
## Reason (one paragraph)
|
||||
...
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Commit the findings note**
|
||||
|
||||
```powershell
|
||||
git add docs\research\2026-05-23-a6-stepwalkadjust-findings.md docs\research\2026-05-23-a6-captures\stepwalkadjust\
|
||||
git commit -m "research(phys): A6.P3 #98 — [step-walk-adjust] capture + findings
|
||||
|
||||
Identifies fix target Branch [A/B/C/D] for the cellar-up climb cap."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — branch to fix target (data-driven)
|
||||
|
||||
**Do NOT execute Phase 2 until Phase 1's findings note exists and the user has reviewed it.** This is the explicit lesson from the 4-session failure pattern: shipping speculative fixes wastes a session each time.
|
||||
|
||||
Per CLAUDE.md (no-workarounds rule) the fix must address the root cause. Each branch below names the LIKELY fix shape. The actual code lands as a separate plan once Phase 1 confirms the branch.
|
||||
|
||||
### Branch A — ContactPlane clearing / replacement at ramp top
|
||||
|
||||
If the climb works while CP=ramp and FAILS when CP changes mid-climb:
|
||||
|
||||
**Investigate first (no code):**
|
||||
- Where in `Transition.FindEnvCollisions` / `Transition.SetContactPlane` is the ramp CP dropped? Likely candidates: `[TransitionTypes.cs:1814](../../src/AcDream.Core/Physics/TransitionTypes.cs:1814)` (FindEnvCollisions write), `[TransitionTypes.cs:1837](../../src/AcDream.Core/Physics/TransitionTypes.cs:1837)` (post-stepdown write).
|
||||
- Compare against retail's BPE pattern: retail's CP toggles cellar-floor → cottage-floor with **no intermediate** (per the divergence comparison table). It never sets CP to the ramp. Our code DOES. The fix MAY be making our ramp-CP behavior match retail (never set CP=ramp; set CP=cellar-floor while climbing, then transition CP=cottage-floor when the sphere is above cottage floor) — but this is a SHAPE-of-the-fix question; verify it's actually needed before changing the contract.
|
||||
|
||||
**Probable fix file:** [src/AcDream.Core/Physics/TransitionTypes.cs](../../src/AcDream.Core/Physics/TransitionTypes.cs) around `FindEnvCollisions` (line 1700–1900).
|
||||
|
||||
**Acceptance:** Phase 1's findings note's branch histogram shows the failure point. The fix must produce a log where `cur` Z monotonically increases from 90.00 to ≥ 94.00 across the climb.
|
||||
|
||||
### Branch B — forward motion consumed before AdjustOffset
|
||||
|
||||
If `into-plane` continues at peak but zGain → 0 because input offset Y → 0:
|
||||
|
||||
**Investigate first:**
|
||||
- Look at `[step-walk] site=before-insert` in the existing log around line 17458 at the peak. Compare the `req` value to the next tick's `req`. If `req.Y` decays to zero across consecutive ticks, the motion source itself is being cut. Trace back to where the per-tick offset is generated — `[PlayerMovementController](../../src/AcDream.Core/Physics/PlayerMovementController.cs)` and the input → velocity → offset chain.
|
||||
- Compare against retail's BPF (adjust_sphere_to_plane) cadence — 431 hits over 35K BPs suggests retail re-projects relatively frequently but not in a way that zeros out motion. Our code might be over-projecting.
|
||||
|
||||
**Probable fix file:** [src/AcDream.Core/Physics/PlayerMovementController.cs](../../src/AcDream.Core/Physics/PlayerMovementController.cs) or the velocity-source upstream of the physics tick.
|
||||
|
||||
**Acceptance:** Same as Branch A.
|
||||
|
||||
### Branch C — offset computed but never committed (CurPos doesn't advance)
|
||||
|
||||
If `[step-walk-adjust] zGain` is correct per call but `[step-walk] cur` doesn't accumulate across calls:
|
||||
|
||||
**Investigate first:**
|
||||
- `TransitionalInsert` in [TransitionTypes.cs](../../src/AcDream.Core/Physics/TransitionTypes.cs) — find the path that returns without committing `sp.CurPos += sp.GlobalOffset`. Likely guard: a collision state that retreats.
|
||||
- The `[step-walk] delta=(0,0,0)` pattern across consecutive lines is the smoking gun if it persists at the peak.
|
||||
|
||||
**Probable fix file:** `TransitionTypes.cs`, the per-step commit at the bottom of the step loop (around line 689–760).
|
||||
|
||||
**Acceptance:** Same as Branch A.
|
||||
|
||||
### Branch D — sliding-normal mis-classification (side-investigation only)
|
||||
|
||||
If `slide-crease` appears mid-climb, the ramp polygon is being treated as a wall by some upstream call. This is unlikely to be the fix for #98 — slide is the right answer for walls, the question is which polygon triggered it.
|
||||
|
||||
**Investigate:** which polygon's normal got installed as `SlidingNormal`? Add a one-line probe at `ci.SlidingNormal = ...` write sites and capture again.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — write the fix (TDD against replay tests)
|
||||
|
||||
Once Phase 1 names the branch and the findings note is committed, write a fresh plan for the fix:
|
||||
|
||||
`docs/superpowers/plans/2026-MM-DD-a6-p3-issue98-cellar-up-fix-impl.md`
|
||||
|
||||
Use [superpowers:test-driven-development](../../.claude/superpowers/skills/test-driven-development/SKILL.md) discipline:
|
||||
|
||||
1. **Flip the replay test assertions FIRST** so they document the FIXED behavior. Two assertions to invert:
|
||||
- `FailingFrame_CottageNeighborA_NearestWalkableIsOutsideSphereAndEdges` — at minimum one of `OverlapsSphere` / `InsideEdges` must become `true`.
|
||||
- `FailingFrame_NoCottageNeighbourYieldsAcceptedWalkable` — at least one neighbour cell must accept.
|
||||
2. Run `dotnet test`; the inverted assertions fail (red).
|
||||
3. Implement the minimal fix in the file Phase 2 identified.
|
||||
4. Run `dotnet test`; the inverted assertions pass (green).
|
||||
5. Run the live client; the user verifies they can walk up out of the cottage cellar.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance for Phase 1 (the only Phase landing in this plan)
|
||||
|
||||
- [ ] `dotnet build` green
|
||||
- [ ] `dotnet test --filter "FullyQualifiedName~Issue98CellarUpReplayTests"` — 7 passing (no regressions)
|
||||
- [ ] `dotnet test` overall — baseline 1167 + 8 maintained (no new failures)
|
||||
- [ ] Diagnostic commit landed (one commit, additive only — no math/control-flow changes)
|
||||
- [ ] Capture commit landed (acdream.log + findings note)
|
||||
- [ ] Findings note names ONE branch (A / B / C / D) as the Phase 2 fix target
|
||||
|
||||
**Acceptance for Phase 3 (the eventual fix, captured here for the next session):**
|
||||
|
||||
- [ ] `Issue98CellarUpReplayTests.FailingFrame_NoCottageNeighbourYieldsAcceptedWalkable` — assertion inverted to require acceptance; test passes.
|
||||
- [ ] `Issue98CellarUpReplayTests.FailingFrame_CottageNeighborA_NearestWalkableIsOutsideSphereAndEdges` — assertion inverted to require sphere-overlap; test passes.
|
||||
- [ ] `dotnet build` green.
|
||||
- [ ] `dotnet test` baseline 1167 + 8 (or better) maintained.
|
||||
- [ ] User walks up out of the Holtburg cottage cellar in the live client — **visual verification required**.
|
||||
|
||||
---
|
||||
|
||||
## What this plan does NOT do (per CLAUDE.md no-workarounds rule)
|
||||
|
||||
- Does not reattempt placement-insert bypasses in `BSPQuery.FindCollisions`, `Transition.FindEnvCollisions`, or `Transition.DoStepDown`. Six variants already tried; none worked.
|
||||
- Does not reattempt cell-resolver tiebreaker changes in `PhysicsEngine.ResolveCellId`. Slice 3 already shipped a stickiness fix; the bug persists.
|
||||
- Does not reattempt negative-side polygon handling. Reverted in `35b37df`; the neg-poly branch fired zero times in the failing log.
|
||||
- Does not reattempt the bldg-check / IsLandblockBuilding flag propagation. Reverted in `35b37df`.
|
||||
- Does not add suppression flags, grace periods, retry loops, or `if (problematicState) return` symptom guards.
|
||||
|
||||
If at any point during Phase 1 implementation you feel certain about the fix without having seen the new probe's branch histogram, **STOP**. The 4-session pattern was: convinced of diagnosis → ship fix → user reports "still can't pass." Verifying via the probe is what breaks the pattern.
|
||||
|
||||
---
|
||||
|
||||
## Self-review (writing-plans skill discipline)
|
||||
|
||||
**Spec coverage.** The user's prompt asks for: state altitudes (done in handoff), diagnostic-first verification (Task 1.1–1.3), branch-to-fix decision (Phase 2), TDD against replay tests (Phase 3), forbidden shortcut list (final section). ✓
|
||||
|
||||
**Placeholder scan.** No "TBD" / "add error handling" / "similar to Task N" left in the plan. Exact file paths and line numbers throughout. ✓
|
||||
|
||||
**Type consistency.** `LogStepWalkAdjust` matches `LogStepWalk`'s signature pattern. Branch tokens are stringly-typed but enumerated explicitly. `[step-walk-adjust]` prefix is distinct from `[step-walk]`. ✓
|
||||
|
||||
**Realism.** Phase 1 ships one diagnostic commit + one capture commit. ~30 minutes if everything goes smoothly. Phase 2 is research (no code). Phase 3 is the actual fix — separate plan. The whole arc fits in 1–2 working days, broken at the user-review checkpoint after Phase 1's findings note.
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue