# A6.P3 #98 — [step-walk-adjust] capture analysis (2026-05-23)

**Capture:** [docs/research/2026-05-23-a6-captures/stepwalkadjust/acdream.log](2026-05-23-a6-captures/stepwalkadjust/acdream.log) (1.3 MB, 6,467 lines)

**Plan ref:** [docs/superpowers/plans/2026-05-23-a6-p3-issue98-cellar-up-fix.md](../superpowers/plans/2026-05-23-a6-p3-issue98-cellar-up-fix.md)

**Probe commit:** `8a232a3` — added `[step-walk-adjust]` site inside `Transition.AdjustOffset` (branch token + zGain per call).

---

## TL;DR

The fix plan's four-branch decision tree (A / B / C / D) **does not match what the data shows**. The diagnostic conclusively proves:

1. **AdjustOffset is correct.** `branch=into-plane` for 145 of 146 calls; `zGain = +0.052 ± 0.001` per call when sphere offset points into the ramp normal `(0, 0.719, 0.695)`. Cumulative theoretical zGain across the climb portion: roughly **+5 m**, far more than the ~2 m the sphere actually climbed.

2. **Z gain accumulates correctly while mid-ramp.** Sphere world Z went 90.95 → 92.80 monotonically across the climb portion.

3. **The climb caps at world Z ≈ 92.80** with the sphere frozen at `cur=(141.5054, 7.1684, 92.7968)`. X drifts by ~0.006/tick from sliding; **Y and Z are nailed**. The cottage floor at world Z=94 is still 1.20 m above.

4. **At the freeze, the per-step rollback mechanism takes the +Z out.** The sequence:
   - `find-start` — winterp=1.0, walkPoly=True, CP=ramp ✓
   - `[step-walk-adjust]` — input=(0.006,-0.105,0), output=(0.006,-0.051,+0.052), branch=into-plane ✓
   - `after-adjust` — adj=(0.006,-0.051,+0.052), CP=ramp ✓
   - **CP cleared by the per-step reset at [TransitionTypes.cs:723-725](../../src/AcDream.Core/Physics/TransitionTypes.cs:723-725).**
   - `before-insert` — check advanced to (141.5117, 7.1179, **92.8491**), CP=n/a
   - Inside `TransitionalInsert(3)`: step-up branch fires (`stepUp=True`), step-down probes by 0.6m downward.
   - Step-down finds no walkable below the proposed position (cottage floor is ABOVE, not below).
   - **Two `stepdown-reject` fires** inside the insert.
   - `after-insert` — check rolled back to `(141.5117, **7.1684, 92.7968**)`. Only X advanced by 0.006. **walkPoly=False, winterp=-0.0000.**
   - `find-end` — same state, walkPoly=False.

5. **This is a NEW fix target — call it "Target E."** The plan's decision tree didn't anticipate this mode. AdjustOffset's slope projection works perfectly. The failure is in the step-up validation logic at the **top of the ramp**, where the next walkable surface (cottage floor) is ABOVE the proposed position, not below. The step-down probe inside step-up scans downward and finds nothing → rejects → rollback.

---

## Branch histogram (across the entire capture)

| Branch | Count | % |
|---|---:|---:|
| `into-plane` | 145 | 99.3% |
| `no-cp` | 1 | 0.7% |
| All others (away-plane, slide-crease, slide-degenerate, no-cp-slide, *+safety-push*) | 0 | 0% |

No safety-push annotations. No slide planes ever installed. No CP-cleared mid-climb (except by the deliberate per-step reset).

## zGain summary

- 146 calls total.
- Total zGain: **+6.63 m**.
- Mean per call: **+0.045 m**.
- Cellar-floor calls (CP normal `(0,0,1)`, d=-90.95): zGain=0 (expected — flat floor doesn't tilt motion).
- Ramp calls (CP normal `(0, 0.719, 0.695)`, d=-69.50): zGain ≈ +0.052 to +0.055 per call (very tight distribution).
- Math verified: collisionAngle = dot(input, normal) ≈ -0.076 → result -= normal × collisionAngle → +Z component matches log exactly.

## cur Z trajectory (from `[step-walk] site=after-adjust`)

| Phase | World Z | Notes |
|---|---|---|
| start | 90.9500 | Walking flat across cellar floor (cell 0xA9B40147 floor) |
| climb begins | 90.9500 → 91.013 → 91.068 → ... | Sphere reaches ramp foot |
| climb proceeds | rises by ~0.05/tick | Y decreasing as Z increasing — climbing -Y direction |
| **cap** | **92.7968** | Sphere locks here; X drifts only |
| end-of-capture | 92.7968 | Sphere never escapes |

Max Z reached: 92.7968. Cottage floor: 94.00. **Gap: 1.20 m.** Sphere top (center+radius): 93.28 — still 0.72 m below cottage floor.

## stepdown probe-site counts (across whole capture)

| Site | Count |
|---|---:|
| `stepdown-enter` | 236 |
| `stepdown-after-insert` | 236 |
| `stepdown-after-offset` | 134 |
| `stepdown-reject` | **101** |
| `stepdown-after-placement` | 1 |

101 rejections vs 1 acceptance + 134 offset-only outcomes. **Step-down is failing far more often than succeeding.** This is the failure-frequency signature.

---

## At the freeze: which validation rejects?

Reading [TransitionTypes.cs:2848-2850](../../src/AcDream.Core/Physics/TransitionTypes.cs:2848-2850):

```csharp
if (transitState == TransitionState.OK
    && CollisionInfo.ContactPlaneValid
    && CollisionInfo.ContactPlane.Normal.Z >= walkableZ)
```

The accept condition needs ALL three. At the freeze moment:
- `transitState == OK` — TRUE (per log).
- `CollisionInfo.ContactPlaneValid` — **FALSE** (per log: `cp=n/a` at stepdown-after-insert, stepdown-reject).
- `ContactPlane.Normal.Z >= walkableZ` — moot since CP is invalid.

So **`ContactPlaneValid` is the false condition**.

Why is `ContactPlaneValid` false after `TransitionalInsert(5)` (called by DoStepDown at line 2825)?

The CP was set to `(0, 0.719, 0.695)` at `find-start`. Then per-step reset at line 724 cleared it before `TransitionalInsert(3)` ran. Inside that insert, step-up logic fired. Step-up internally calls `DoStepDown(stepDownHeight=0.6, walkableZ=0.6642, runPlacement=true)`. **That nested DoStepDown runs `TransitionalInsert(5)` again**, and inside THAT, the sphere checks for walkable polys. None found below the proposed step-up position → CP stays unset → accept condition fails → `stepdown-reject`.

The retail behavior (from the cdb capture, [retail.decoded.log](2026-05-23-a6-captures/cellar_up_capture_1/retail.decoded.log)):
- **Retail's BPE writes ContactPlane to (0,0,1) d=-93.9998 (cottage floor at world Z=94) DIRECTLY from (0,0,1) d=-90.9500 (cellar floor) with no intermediate.**
- Retail's BPE writes never set CP to the cellar ramp normal.
- Retail's sphere DOES climb across the ramp, but the CP stays on the flat-floor planes the whole time.

So retail's mechanism: the sphere climbs the ramp by step-up SUCCEEDING and landing on cottage floor as the next walkable surface. The ramp itself isn't used as a ContactPlane in retail.

In acdream: the ramp is treated as a walkable surface. When the sphere reaches the top of the ramp, the next required walkable surface (cottage floor) is too far above the proposed position to be acceptable to the step-down probe.

---

## Conclusion: Fix target is "Target E" (new)

The previous decision tree (A / B / C / D) was based on the divergence comparison doc's framing of "no altitude gain." The data shows the climb DOES gain altitude (correctly). The bug is at the **top of the ramp**, in the **step-up + step-down validation**, NOT in `AdjustOffset`.

### Target E definition

**Name:** Step-up validation rejects ramp-climb advances when the next walkable surface (cottage floor) is too high above the proposed step-up position to be acceptable to the downward step-down probe.

**Failure mechanic:** At the top of the cellar ramp:
1. Sphere proposes to advance up the ramp by ~0.10 m horizontal + 0.05 m vertical.
2. The advance puts the sphere bottom AT world Z ≈ 92.37 (still 1.63 m below cottage floor at world Z=94).
3. Step-up logic fires (because there's a +Z component in the offset).
4. Step-up calls DoStepDown with stepDownHeight=0.6 m to find a walkable surface within reach.
5. Step-down probes the sphere downward by 0.6 m to world Z ≈ 91.77, but no walkable polygon exists at that altitude in any of the overlapping cells (0x0147, 0x0143, 0x0146).
6. step-down rejects → step-up rejects → rollback restores sphere Y and Z, advances X by sliding amount.
7. Sphere is now in IDENTICAL state next tick → infinite loop.

### Two candidate fix shapes (TO RESEARCH — DO NOT CODE YET)

**Shape 1 — keep ramp as ContactPlane during the climb.** Match retail's behavior of NOT clearing ContactPlane between AdjustOffset calls when the player is mid-ramp. Retail's BPE shows CP is "sticky" on the cellar floor, then suddenly transitions to cottage floor. Our per-step reset at TransitionTypes.cs:721-725 clears CP every step; this is the documented "ACE order" but may not match retail.

**Shape 2 — fix step-up to look UPWARD for cottage floor.** When step-up fails to find a walkable directly below the proposed position, probe UPWARD by `stepUpHeight` looking for a walkable that the sphere can land on after a vertical lift. This is the natural "climb up a ledge" behavior. The current step-up only probes downward (via DoStepDown).

**Shape 3 — preserve walkPoly across rollback.** When step-up rejects, the rollback should preserve `walkPoly=True` if the PREVIOUS frame had it (the sphere was on a valid walkable). Currently `walkPoly=False` after rollback, which then poisons the next tick's `OnWalkable` check.

These three shapes are NOT mutually exclusive. The fix may need shape 1 + 3, or shape 2 alone, or some combination.

---

## What this rules out

| Hypothesis | Status |
|---|---|
| AdjustOffset projection broken (decision-tree Branch A / B / C / D) | **RULED OUT** — projection works correctly, +zGain per call is consistent and matches the math. |
| WalkInterp depletion gating forward motion | **RULED OUT** — winterp=1.0 at find-start of every freeze tick. Only DEPLETED winterp=-0.0000 appears AFTER stepdown-reject, which is a consequence not a cause. |
| Cell-resolver ping-pong between cellar and cottage | **RULED OUT** — every tick has cell=0xA9B40147→0xA9B40147 (no transition); slice-3 stickiness fix held. |
| Step-down rejected because no walkable found above sphere | **NOT TESTABLE BY THIS PROBE** — this probe is inside AdjustOffset, not inside DoStepDown's accept-condition check. A follow-up probe inside the accept-condition check would prove which of the three accept clauses fails. We CAN see it indirectly: `cp=n/a` at stepdown-after-insert tells us ContactPlaneValid is false at the moment of the check. |

---

## Pickup prompt for the fix plan

```
A6.P3 issue #98 — [step-walk-adjust] capture analysis complete.

Read FIRST:
  docs/research/2026-05-23-a6-stepwalkadjust-findings.md
  docs/research/2026-05-23-a6-captures/stepwalkadjust/acdream.log
    (search for "stepdown-reject" and the freeze tick at line ~3891)

Conclusion: Fix target is "Target E" (new) — step-up validation
rejects ramp-climb advances at the top of the cellar ramp because
the cottage floor is too far ABOVE the proposed step-up position to
be found by the downward step-down probe.

Three candidate fix shapes:
  1. Keep ramp ContactPlane sticky across per-step resets (match retail).
  2. Make step-up probe UPWARD for the next walkable (climb-up behavior).
  3. Preserve walkPoly across rollback to avoid OnWalkable being poisoned.

Next: research which shape matches retail's named decomp at
acclient_2013_pseudo_c.txt (search step_sphere_up, step_sphere_down,
find_walkable). Retail's BPE writes ONLY ever set CP to flat floors
(cellar Z=90.95 then cottage Z=94) — never to the ramp.

The replay harness (Issue98CellarUpReplayTests, <200ms) is the inner
test loop. The cdb capture in cellar_up_capture_1/ is the ground-truth
oracle. The fix MUST flip the failing-frame assertions in the replay
tests — that's the contract.

Test baseline: 1167 + 8. CLAUDE.md rules apply. No workarounds.
```
