Commit graph

173 commits

Author SHA1 Message Date
Erik
3e140cfe71 fix(phys): A6.P3 slice 3 v2 — point-in stickiness (was sphere-overlap)
Slice 3 v1 (8898166) used SphereIntersectsCellBsp for the stickiness
check. User verification showed: ping-pong WAS closed (3 cell-transit
events vs 20+ pre-fix) but user still couldn't walk up out of cellar
because the stickiness was OVER-CORRECTED — the sphere still partially
overlapped the cellar cell at the top of stairs, so stickiness held the
player in the cellar even when the center had transitioned to the
cottage main floor cell.

Fix: switch the stickiness check from SphereIntersectsCellBsp (sphere
overlap) to PointInsideCellBsp (center-in). Matches FindCellList's
own semantics for "which cell are you in." Player stays in fallback
only while center is still inside fallback's BSP volume.

Trade-off:
- More permissive transitions (good — cellar-up works)
- Less aggressive stickiness, so some boundary ping-ponging may return
  IF the sphere center oscillates across the boundary (rare; would
  require sub-mm Z drift across the boundary line)

If the trade-off bites (ping-pong returns somewhere), the fix is a
small geometric margin around the point-in check — but verify before
adding.

Test suite: 1148 pass + 8 pre-existing fail (baseline maintained).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:04:51 +02:00
Erik
88981669fe fix(phys): A6.P3 slice 3 — cell-resolver stickiness for ping-pong fix
Closes A6.P2 Finding 3 (cell-resolver instability) + issue #98 (cellar
ascent stuck at last step) + likely closes #97 (phantom collisions +
fall-through on 2nd floor; same instability family).

Adds a cell-stickiness check at the top of ResolveCellId's indoor
branch: before re-resolving via FindCellList, check if the fallback
(previous-tick) CellId's BSP still validly contains the sphere. If
yes, return fallbackCellId immediately — preserves cell membership
when the sphere is at a boundary where multiple cells overlap.

The bug: at cell boundaries (cellar last step, indoor doorways,
between two adjacent indoor cells), the sphere overlaps multiple
cells geometrically. FindCellList's candidate-iteration order
(HashSet, implementation-defined) determines which cell wins. That
order may shift tick-to-tick → CellId ping-pong → AdjustOffset
operates against a different cell's geometry each tick → player
can't accumulate forward motion → stuck.

Evidence: scen3_inn_2nd_floor_slice2v2 capture shows the ping-pong
chain at the cellar boundary:
  0xA9B4014B → 0xA9B4014A → 0xA9B4013F → 0xA9B4014A → 0xA9B4014B
  (Z stable ~96.4; CellId oscillates every tick; reason=resolver)

Retail oracle: cell-array hysteresis pattern from
CObjCell::find_cell_list Position-variant at
acclient_2013_pseudo_c.txt:308742-308783. Retail preserves cell
membership when sphere is close to (but slightly past) cell
boundaries.

Implementation: 9 lines added (sphere-overlap check against
fallbackCellId's CellBSP before falling through to FindCellList).
Existing #90 workaround at line 299-300 (post-FindCellList sphere-
overlap check) is now redundant in the common case but kept for
safety; deferred to A6.P4 removal after visual verification.

Test suite: 1148 pass + 8 pre-existing fail (baseline maintained).
Visual verification: pending — user happy-test will confirm cellar-
up walk succeeds + no ping-pong in cell-transit log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:01:28 +02:00
Erik
f8d669be88 fix(phys): A6.P3 slice 2 v2 — revert seed removal + add no-op guard
First slice 2 attempt at commit 892019b removed PhysicsEngine.cs L622
per-tick CP seed entirely. User happy-test surfaced a regression: BSP
step_up at the last step of stairs failed because sub-step 1's
AdjustOffset had no ContactPlane to compute the lift direction (the
seed was load-bearing for step_up correctness).

Revert + better fix:
  1. Re-add the L622 seed (PhysicsEngine.cs:620-626).
  2. Add no-op-if-unchanged guard inside CollisionInfo.SetContactPlane
     (TransitionTypes.cs:259-279). When called with values identical
     to current state, early-return without incrementing
     ContactPlaneWriteCount or rewriting fields.

When the player stands on the same plane tick after tick, the L622
seed re-calls SetContactPlane with identical args — these now no-op
instead of inflating the counter and re-writing the same values.
Only actual state changes (e.g. landing on a new step's plane, cell
crossing) increment the counter.

Verification (post-rebuild, pre-this-commit slice 2 first attempt):
- scen3 walk produced 2,690 cp-writes (down from 30,420 = 91%
  reduction from L622-seed presence)
- BUT user could not pass the last step of stairs — step_up regression
- Test suite: 1148 + 8 pre-existing fail baseline maintained but
  physical behavior broke

Post-this-commit expectations:
- Test suite: 1148 + 8 (unchanged, no behavioral change in fixtures
  because the seed value is what the fixtures already expect)
- Stair-walking: works (seed restored)
- CP-write count: significantly reduced (most seeds are no-ops because
  body.CP doesn't change tick-over-tick on stable footing)
- Issues #96 / #97: re-test in re-capture; #96 should be largely
  closed via the guard; #97 (fall-through + stuck-in-falling) was
  observed pre-slice-2 too, so unrelated to the seed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:32:23 +02:00
Erik
892019bc9a fix(phys): A6.P3 slice 2 — remove L622 per-tick CP seed (issue #96)
Closes issue #96 (per-tick PhysicsEngine.ResolveWithTransition CP seed
contributing 99.3% of post-slice-1 CP writes). Matches retail's
CTransition::init at acclient_2013_pseudo_c.txt:271954 which explicitly
clears contact_plane_valid = 0 at transition start.

Cross-tick CP retention now flows entirely via retail-faithful
mechanisms:
  - Mechanism A: BSPQuery.FindCollisions Path-6 land write
  - Mechanism B: Transition.ValidateTransition LKCP restore (slice 1)
  - Body persist at transition end (already existed)

Cost (deliberate): AdjustOffset on sub-step 1 of each tick takes the
'no contact plane' path. Slope-snap loss is imperceptible (sub-steps
are small, sub-steps 2+ pick up CP normally).

Likely closes issue #97 (phantom collisions + fall-through) as
side-effect — hypothesis was stale-CP slope-snap from body.ContactPlane
of a previous cell. To be verified post-commit via re-capture + user
happy-test.

Verification this commit:
- Test suite: 1148 pass + 8 pre-existing fail (baseline maintained)
- scen3 re-capture pending (separate commit)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:19:48 +02:00
Erik
5f7722a3a4 fix(phys): A6.P3 slice 1 step 2 — strip indoor walkable synthesis
Closes A6.P2 Finding 2 (ContactPlane resynthesis blowup, 250x to ∞x
more CP writes than retail). Indoor branch of Transition.FindEnvCollisions
now matches retail's CEnvCell::find_env_collisions tiny shape (decomp
line 309573): call BSPTREE::find_collisions, return OK. No synthesis,
no per-frame ValidateWalkable call, no per-frame ContactPlane write.

Cross-frame CP retention now flows via:
  - Mechanism A: BSPQuery.FindCollisions Path-3 step-down write on
    grounded movers (retail-faithful: BSPTREE::step_sphere_down at
    acclient_2013_pseudo_c.txt:323711 always writes contact_plane when
    it finds a walkable surface — only fires if sphere penetrates floor).
  - Mechanism B: per-transition LKCP restore in ValidateTransition
    (added in 5aba071) for the Collided/Adjusted/Slid result cases.
  - PhysicsEngine.RunTransitionResolve body persist (unchanged).

TryFindIndoorWalkablePlane definition retained for now; deleted in
A6.P4 alongside the #90 sphere-overlap workaround.

Test fix: IndoorContactPlaneRetentionTests sphere position corrected
from 5 cm below the floor (pre-fix arrangement to trigger synthesis)
to exactly on the floor (worldPosZ = floorZ). A grounded sphere at
its natural position does not penetrate the floor polygon, so BSP
Path 5 finds no intersection and returns OK immediately — zero
additional CP writes in 60 frames. Previously the below-floor position
was causing Path 5 → StepSphereUp → DoStepDown → SetContactPlane
every frame (60 writes), not the synthesis path.

Verification:
- IndoorContactPlaneRetentionTests: PASS (was the 9th expected fail;
  back to 1148 pass + 8 pre-existing fail).
- Full suite: 1148+420 pass, 8 fail (baseline maintained +1 pass).
- Re-capture verification (scen1/3/5) deferred to Task 6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:12:45 +02:00
Erik
5aba071aec feat(phys): A6.P3 slice 1 step 1 — add Mechanism B (LKCP restore)
Restores CollisionInfo.ContactPlane from LastKnownContactPlane when:
  - LKCP is valid
  - the sphere's current center is geometrically close to the LKCP
    plane (|dot(global_curr_center, N) + d| <= radius + EPSILON)

Matches retail's validate_transition LKCP-restore at
acclient_2013_pseudo_c.txt:272577 (CTransition::validate_transition,
address 0050aa70, lines 272565-272582). Slice 1 step 1 of the
A6.P3 indoor CP retention fix. Step 2 (Task 5) strips the
TryFindIndoorWalkablePlane synthesis from FindEnvCollisions.

Also fixes the proximity-check sphere: was using
sp.GlobalSphere[0].Origin (start sphere); now uses
sp.GlobalCurrCenter[0].Origin (current center) per retail
(acclient_2013_pseudo_c.txt:272568).

Tests: 1147 pass, 9 fail (8 pre-existing + 1 IndoorContactPlaneRetention
from T3 — expected; T5 lands the actual synthesis-strip fix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 08:58:03 +02:00
Erik
869edd93b0 test(phys): A6.P3 slice 1 — add CollisionInfo.ContactPlaneWriteCount
Internal test-only counter incremented by SetContactPlane. Required
by IndoorContactPlaneRetentionTests to assert CP retention works
post-Finding-2 fix (A6.P2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 08:23:56 +02:00
Erik
642734dcd0 feat(physics): A6.P1 — instrument CheckOtherCells with [push-back-cell]
Wires LogPushBackCellTransit into the multi-cell BSP iteration loop
just before ApplyOtherCellResult halts. Captures primary/other
cell ids + BSP result for direct comparison to retail's
CTransition::check_other_cells loop (already ported as A4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:38:03 +02:00
Erik
66ee757926 feat(physics): A6.P1 — add LogPushBackCellTransit helper
One-line per-iteration emission helper for the CheckOtherCells
multi-cell BSP loop. Captures primary/other cell ids, BSP result,
and halted flag for direct comparison to retail's
CTransition::check_other_cells loop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:36:42 +02:00
Erik
35631d1ec0 feat(physics): A6.P1 — instrument FindCollisions with [push-back-disp]
Wires LogPushBackDispatch into the modern FindCollisions overload
at the entry block (after path/collisions/obj locals + movement
computed). Legacy overload at line ~1895 delegates to modern, so
single instrumentation site covers all dispatches.

returnState=-1 sentinel marks "entry log" — A6.P2 analysis pairs
each entry with subsequent [push-back] adjust-sphere lines and
the eventual return state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:34:55 +02:00
Erik
2d1f27d647 feat(physics): A6.P1 — add LogPushBackDispatch helper
One-line per-call emission helper for the FindCollisions dispatcher
instrumentation site. Captures path-selection state (collide flag,
insertType, objState) + walk-interp + return state for direct
comparison to retail's BSPTREE::find_collisions breakpoint.
Output uses the [push-back-disp] tag to disambiguate from
[push-back] adjust-sphere events.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:32:37 +02:00
Erik
eb8a3186e7 feat(physics): A6.P1 — instrument AdjustSphereToPlane with [push-back]
Wires the LogPushBackAdjust helper into all three return paths
of AdjustSphereToPlane (early-return on no-movement, early-return
on interp out-of-window, and the applied path). Probe is gated by
ProbePushBackEnabled so it's zero-cost when off.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:30:46 +02:00
Erik
3a173b9616 feat(physics): A6.P1 — add LogPushBackAdjust helper
One-line per-call emission helper for the AdjustSphereToPlane
instrumentation site. Direct field-for-field paired comparison to
retail's CPolygon::adjust_sphere_to_plane breakpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:28:20 +02:00
Erik
ad6c89de33 fix(physics): A6.P1 — drop unresolvable <see cref> to private method
BSPQuery.AdjustSphereToPlane is private; <see cref> from outside the
class can't resolve and emits CS1574. Switched to <c>...</c> code
span. Other two cross-refs (FindCollisions public, CheckOtherCells
internal-same-assembly) keep their <see cref> form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:27:00 +02:00
Erik
ace9e62213 feat(physics): A6.P1 — add ProbePushBackEnabled toggle
New PhysicsDiagnostics flag gates the [push-back] probe shipping
in subsequent tasks. Env-var ACDREAM_PROBE_PUSH_BACK=1 + DebugVM
mirror, matching the existing probe-toggle pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:24:22 +02:00
Erik
7ac8f544a7 fix(physics): #89 — sphere-overlap in CheckBuildingTransit closes login-inside-inn classification race
Outdoor→indoor entry path used PointInsideCellBsp (point-only) for the
building-portal containment test. When the player logs in INSIDE a
building and the foot-sphere center is just past the destination cell's
CellBSP boundary, the point-only check failed → CellId stuck as
outdoor → indoor BSP queries never ran → walls passable. User-reported
symptom: "logged in in the inn, at start ran through exterior walls,
ran back in and they block now."

Fix: swap PointInsideCellBsp for SphereIntersectsCellBsp (the radius-
aware port from #90). Promotes CellId to the interior cell the moment
ANY part of the foot-sphere crosses the destination cell boundary —
matches retail's CCellStruct::sphere_intersects_cell timing at
acclient_2013_pseudo_c.txt:317666 exactly.

The sphereRadius parameter was already plumbed through CheckBuildingTransit
per #89's documented "future upgrade" note from 2026-05-19 (which is
exactly today's symptom). Closes #89.

1147 + 8 baseline maintained.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 09:12:30 +02:00
Erik
c0d84057cb fix(physics): #91 — query indoor cell shadows in FindObjCollisions
Interior items (fireplaces, tables, chests) registered via A1.5's
ShadowObjectRegistry.Register `cellScope` parameter (commit 4d3bf6f)
are stored under their ParentCellId key (e.g. 0xA9B40121). But
GetNearbyObjects's broad-phase only iterates outdoor 24m landcell
keys (0xA9B40029 etc) and never looks up indoor cell keys, so
interior shadows were registered but unreachable. User-visible
symptom: tables/boxes/fireplaces don't block movement, while walls
DO block (the indoor BSP path is separate).

Fix: GetNearbyObjects accepts an optional indoorCellIds parameter
and additionally queries _cells[indoorCellId] for each entry with
low-byte >= 0x0100u. FindObjCollisions computes the set via
CellTransit.FindCellSet (same set A4 uses for multi-cell BSP
iteration) and passes it through. Outdoor seeds typically produce
sets containing only outdoor land-cells which the new branch filters
out, so the outdoor-only behavior is preserved.

1147 + 8 baseline maintained. Closes the user-reported regression
"walls block now correct but interior items such as tables and boxes
or fireplaces do not block."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 09:03:51 +02:00
Erik
4ca35966f8 fix(physics): #90 — sphere-overlap cell stickiness at doorway threshold
ResolveCellId's indoor-seed fall-through was point-only: when the indoor
BSP push-back moved the foot-sphere CENTER a few cm outside the indoor
CellBSP volume, the resolver flipped CellId back to outdoor. Next tick
re-promoted via CheckBuildingTransit. The ping-pong caused most ticks
to be classified outdoor, bypassing indoor BSP wall checks entirely
and producing the user-reported "walls walk through everywhere in the
inn" symptom.

Fix: port retail's BSPTREE::sphere_intersects_cell_bsp
(acclient_2013_pseudo_c.txt:323267 → BSPNODE variant at :325546) as
BSPQuery.SphereIntersectsCellBsp(node, center, radius). Replace the
point-only check at PhysicsEngine.ResolveCellId:285 with the radius-
aware overlap test. Player stays classified indoor as long as ANY
part of the foot-sphere still overlaps the indoor cell volume; only
flips to outdoor when the sphere is FULLY outside.

Retail uses a 0.01 m epsilon on the radius (acclient :325551); ported
verbatim. 8 new unit tests cover null/leaf/inside/on-plane/straddling/
fully-outside/tangent-boundary cases plus a regression-anchor test
that proves the old PointInsideCellBsp would have returned false for
the same straddling input.

1147 + 8 baseline maintained (was 1139 + 8 before #90 fix). Closes #90.
A4 multi-cell iteration (shipped earlier today) should now actually
exercise in production since the player can stably remain in indoor
cells.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 20:30:36 +02:00
Erik
691493e579 Reapply "feat(physics): A4 — wire CheckOtherCells into FindEnvCollisions"
This reverts commit 3add110449.
2026-05-20 20:06:14 +02:00
Erik
3add110449 Revert "feat(physics): A4 — wire CheckOtherCells into FindEnvCollisions"
This reverts commit 967d065141.
2026-05-20 18:50:26 +02:00
Erik
967d065141 feat(physics): A4 — wire CheckOtherCells into FindEnvCollisions
After the primary cell's BSP returns OK, query every other cell the
foot-sphere overlaps via CellTransit.FindCellSet + Transition.CheckOtherCells.
Closes the Holtburg inn vestibule wall walk-through: the vestibule
(cell 0xA9B40164) has only 4 BSP polys; walls live in the adjacent
interior cell (0xA9B40157). Without A4 the adjacent cell's BSP was
never queried.

End-to-end test reduces the real Holtburg bug to a minimal synthetic
two-cell fixture: empty vestibule BSP + interior cell with the
existing BSPStepUpFixtures.TallWall (the same fixture B2 uses to
prove a grounded mover can't scale a 5m wall). Pre-A4: returns OK
(walks through). Post-A4: returns Slid (the wall halts the
transition).

FindEnvCollisions visibility tightened from private → internal so
the integration test can call it directly without going through
FindTransitionalPosition's sub-step iteration.

Retail oracle: acclient_2013_pseudo_c.txt:272717-272798
(CTransition::check_other_cells).

Spec: docs/superpowers/specs/2026-05-20-phase-a4-multi-cell-bsp-design.md
Plan: docs/superpowers/plans/2026-05-20-phase-a4-multi-cell-bsp.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:23:00 +02:00
Erik
493c5e5ff6 feat(physics): A4 — Transition.CheckOtherCells + ApplyOtherCellResult
Port of retail's CTransition::check_other_cells at
acclient_2013_pseudo_c.txt:272717-272798. Iterates every non-primary
cell in a candidate set, runs BSPQuery.FindCollisions per cell with
that cell's WorldTransform-derived rotation + origin, halts on first
Collided/Adjusted/Slid.

ApplyOtherCellResult is the combine-semantics helper extracted for
unit testability — it pins the retail switch:
  - Collided/Adjusted → CollidedWithEnvironment = true (gated on
    !Contact), halt.
  - Slid              → ContactPlaneValid + ContactPlaneIsWater = false,
                        halt.
  - OK                → continue.

Not yet wired into FindEnvCollisions — see next commit. Probe gated
on PhysicsDiagnostics.ProbeIndoorBspEnabled (ACDREAM_PROBE_INDOOR_BSP).

Six new unit tests: five against the pure combine helper for each halt
case + one direct CheckOtherCells call exercising the null-BSP guard.

Spec: docs/superpowers/specs/2026-05-20-phase-a4-multi-cell-bsp-design.md
Plan: docs/superpowers/plans/2026-05-20-phase-a4-multi-cell-bsp.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:14:05 +02:00
Erik
e6369e266f feat(physics): A4 — CellTransit.FindCellSet overload exposes candidate set
Refactors FindCellList to delegate to a private helper
(BuildCellSetAndPickContaining) that returns BOTH the containing cell
id AND the full candidate HashSet. Public surface gains a new
FindCellSet overload; existing FindCellList behavior is unchanged.

Used by the upcoming Transition.CheckOtherCells (Phase A4) to iterate
every cell the sphere overlaps for per-cell BSP collision. Mirrors
retail's CObjCell::find_cell_list filling both cell_array AND var_4c
at acclient_2013_pseudo_c.txt:272725.

Three new unit tests cover sphere-fully-inside-primary,
sphere-straddling-portal, and outdoor-seed-neighbour-landcells cases.

Spec: docs/superpowers/specs/2026-05-20-phase-a4-multi-cell-bsp-design.md
Plan: docs/superpowers/plans/2026-05-20-phase-a4-multi-cell-bsp.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:11:31 +02:00
Erik
4679134d66 fix(physics): fall through to outdoor cell when indoor BSP doesn't contain player (A1.7)
ISSUES #83 Phase A1.7. CellTransit.FindCellList returns currentCellId
when no candidate cell's CellBSP contains the sphere center — but
this also fires when the player has walked OUTSIDE the entire
portal-connected indoor graph (e.g., breached a missing wall poly,
walked through a doorway gap). The player's CellId stays stuck on
the old indoor cell whose BSP is now far away, NodeIntersects fails
at the BSP root for every collision query, and no walls block in
their actual location.

Probe evidence (launch-stairs.utf8.log, A1.6 verify):
- Cell 0xA9B40164: 646 indoor-bsp queries, 644 returned OK (99.7%).
  No walls firing.
- Player local positions in cell 0xA9B40164 ranged X[-0.66..33.18],
  Y[10.68..63.53] — a 34x53m envelope. The player was geometrically
  ~62m from the cell's world origin while CellId never updated.

Compare to adjacent cells where collision works:
- Cell 0xA9B4015A: 230 queries, 32% hit rate
- Cell 0xA9B40157: 131 queries, 38% hit rate

Fix: after CellTransit.FindCellList returns, verify the resolved
cell's CellBSP actually contains the sphere center via
BSPQuery.PointInsideCellBsp. If not, fall through to the existing
outdoor cell resolution branch (terrain grid + CheckBuildingTransit
for re-entry into a different building).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 14:25:20 +02:00
Erik
4d3bf6fe37 fix(physics): scope interior cell shadows to ParentCellId (Phase A1.5)
ISSUES #83 Phase A1.5. ShadowObjectRegistry.Register() assigned each
entity to the outdoor landcell grid (8x8 cells, 24m square) based on
its XY position. For interior EnvCell statics (fireplace, furniture,
sign) hydrated by BuildInteriorEntitiesForStreaming with
ParentCellId = envCellId (a high-cellId interior cell like
0xA9B40121), this meant the shadow got stamped into the OUTDOOR
landcell whose XY they overlapped (e.g., 0xA9B40029).

When the player was OUTSIDE the building in 0xA9B40029, the indoor
chair/fireplace shadow fired collisions in "thin air" outdoors. The
user reported this on Holtburg cottage exteriors after the Phase A1
landblock-stab fallback fix.

Fix: add optional cellScope parameter to Register(). When non-zero
(passed as entity.ParentCellId ?? 0u from the 5 entity-loop call
sites in GameWindow), skip the XY-based landcell loop and register
the shadow ONLY in that cell. Live server-spawn registration at
GameWindow.cs:3137 keeps the XY-based behavior (live entities move
between cells).

Probe evidence (launch-a1-verify.utf8.log, post-A1 capture):
- 71 hits on 0x40B50054 (interior static) in OUTDOOR cell 0xA9B40029.
- 47 hits on 0xA9B47C00 (other Holtburg cottage BSP — legitimate).
- 31 hits on 0x40B50048 / 15 on 0x40B50018 (interior statics).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:54:29 +02:00
Erik
a2e7a87c25 feat(physics): [walk-miss] + [floor-polys] diagnostic emissions
Wires the WalkMissDiagnostic aggregator + flag into the two emission
sites per docs/superpowers/specs/2026-05-21-indoor-walk-miss-probe-design.md.

- [walk-miss] (per-frame, MISS branch of TryFindIndoorWalkablePlane):
  foot world+local position, nearest walkable poly with XY-containment
  flag and vertical gap, and LandCell terrain probe at the same XY.
- [floor-polys] (one-shot per cell at cache time): walkable poly id,
  normal Z, local-XY bbox, plane Z at bbox center.

Both gated on ACDREAM_PROBE_WALK_MISS=1. No physics behavior changes.
The live capture at the Holtburg cottage doorway + inn 2nd floor +
cellar descent disambiguates H1 (multi-cell iteration), H2 (probe
distance), H3 (poly absent / walkable_hits_sphere rejection) for
ISSUES #83.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:38:53 +02:00
Erik
31da57c94c feat(physics): WalkMissDiagnostic aggregator for ISSUES #83 probe spike
Pure-function aggregator that, given a CellPhysics.Resolved dict and
a foot local position, picks the nearest walkable-eligible polygon
(normal Z >= FloorZ) and reports XY-containment + signed vertical gap.
Also enumerates walkable polys with local-XY bboxes for the one-shot
[floor-polys] cell-load dump.

Pure-function, no behavior change. Wiring to emission sites lands in
the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:31:39 +02:00
Erik
27c728484d feat(physics): ProbeWalkMissEnabled flag for ISSUES #83 H-disambiguation
Adds a new diagnostic flag for the indoor-walking walk-miss probe
spike per docs/superpowers/specs/2026-05-21-indoor-walk-miss-probe-design.md.
Env var ACDREAM_PROBE_WALK_MISS=1, runtime-toggleable via property.
No DebugPanel mirror — spike-only. Following commits wire the
[walk-miss] and [floor-polys] emissions to this flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:23:00 +02:00
Erik
0a7ce8fd58 Revert "fix(physics): remove per-frame indoor walkable-plane synthesis"
This reverts commit 9f874f4650.
2026-05-20 09:17:24 +02:00
Erik
9f874f4650 fix(physics): remove per-frame indoor walkable-plane synthesis
The indoor branch of FindEnvCollisions called Transition.TryFindIndoorWalkablePlane
every frame to re-synthesize the ContactPlane after BSP returned OK.
The synthesis routed through BSPQuery.FindWalkableSphere ->
walkable_hits_sphere, which correctly rejects tangent contact via
|dist| > radius - epsilon. For a grounded player standing on or
brushing a floor, the foot sphere is tangent: 99.87% MISS rate per
the 2026-05-20 [cp-write] probe (3150 MISS / 3154 calls). Each MISS
fell through to outdoor terrain backstop, writing a ContactPlane
that's below the indoor floor by ~0.02m (the render Z-bump),
marking the player airborne and triggering the falling-animation
stuck symptom user-reported on 2nd-floor walks.

Fix: delete the synthesis + outdoor-fallthrough from the indoor OK
path. ContactPlane is retained from the prior tick's seed
(PhysicsEngine.ResolveWithTransition:583, init_contact_plane
equivalent) or refreshed by BSP Path 3 (step_sphere_down) / Path 4
(land-on-surface) during the same tick. Matches retail's
BSPTREE::find_collisions OK path (acclient_2013_pseudo_c.txt:323938).

Also deletes:
- Transition.TryFindIndoorWalkablePlane (~104 lines incl. doc-comment)
- INDOOR_WALKABLE_PROBE_DISTANCE constant
- [indoor-walkable] probe log line
- IndoorWalkablePlaneTests.cs (8 tests, the helper's coverage)
- TransitionTypesTests.cs (1 test, also tested the helper)

Net: -491 lines. BSPQuery.FindWalkableSphere + its 5 unit tests
retained as the underlying retail-faithful walkable-finder API
(reachable for spawn-placement / teleport-verification / future
debug needs; its doc-comment is updated to reflect the change).

Closes Bug A in the indoor ContactPlane retention phase.
Spec: docs/superpowers/specs/2026-05-20-indoor-walkable-synthesis-removal-design.md.
Plan: docs/superpowers/plans/2026-05-20-indoor-walkable-synthesis-removal.md.
Predecessor: de8ffde (Bug B, BSP world-origin fix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:11:04 +02:00
Erik
de8ffde4ca fix(physics): pass cell world-transform to indoor BSP collision
Indoor cell BSP queries at TransitionTypes.cs:1442 were calling
BSPQuery.FindCollisions with Quaternion.Identity + defaulted
Vector3.Zero worldOrigin. Inside the BSP, Path 3 (step_sphere_down)
and Path 4 (land-on-surface) use those params to build the
world-space ContactPlane. Result: planes written with D ~ 0 instead
of the cell's world floor Z (e.g. -94.02 for Holtburg cottages).
320 corrupt CP writes per Holtburg session per the [cp-write] probe.

Fix: decompose cellPhysics.WorldTransform once at the call site,
pass the rotation as localToWorld and the translation as
worldOrigin. Mirrors the existing correct pattern at :1808
(FindObjCollisions, passes obj.Rotation + obj.Position).

Retail oracle: BSPTREE::find_collisions (acclient_2013_pseudo_c.txt:323924)
calls Plane::localtoglobal at :323921 before set_contact_plane.
Our TransformNormal + TransformVertices + BuildWorldPlane chain is
the equivalent — it just needs the right rotation + origin.

Spec: docs/superpowers/specs/2026-05-20-indoor-bsp-worldorigin-fix-design.md.
Plan: docs/superpowers/plans/2026-05-20-indoor-bsp-worldorigin-fix.md.
Evidence: launch-cp-probe.log capture 2026-05-20, [cp-write] probe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:00:16 +02:00
Erik
66de00d09a feat(physics): [cp-write] probe for ContactPlane retention spike
Spike for the next phase of indoor-walking work: confirm/refute the
hypothesis that FindEnvCollisions's indoor branch rewrites the player's
ContactPlane every frame instead of retaining it across frames (retail's
actual behavior). The previous session shipped 6 commits on a wrong
diagnosis; this probe captures the data BEFORE designing the fix.

Two pieces:

1. Add PhysicsDiagnostics.ProbeContactPlaneEnabled flag, gated on
   ACDREAM_PROBE_CONTACT_PLANE=1 (also runtime-toggleable). Helper
   methods LogCpBoolWrite / LogCpPlaneWrite / LogCpCellIdWrite emit one
   [cp-write] line per CP/LKCP field mutation with caller (walked from
   the stack with file+line info) when the value actually changes.

2. Convert the 8 ContactPlane group + LastKnownContactPlane group
   fields on CollisionInfo from public fields to public properties
   with backing fields. Setters call the diagnostic helpers when the
   probe is on; getters/setters are inlined when the flag is off.
   Storage layout unchanged. No call site changes — grep confirmed no
   ref/out passing or sub-field writes.

Build green; tests green at the existing 8-failure baseline (2 BSPStepUp,
6 MotionInterpreter — all unrelated, pre-existing).

Capture command:
  ACDREAM_PROBE_CONTACT_PLANE=1 ACDREAM_PROBE_INDOOR_BSP=1 ACDREAM_DEVTOOLS=1

Spike-only — remove when the retention fix lands and the diagnostic
value is captured in the next phase's spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 07:22:55 +02:00
Erik
f845b2241a feat(physics): add [indoor-walkable] probe line
Extends the existing [indoor-bsp] probe surface in FindEnvCollisions
with a per-call [indoor-walkable] line gated on
PhysicsDiagnostics.ProbeIndoorBspEnabled (no new flag). Logs the
synthesized contact plane, the polyId hit, and the signed Z gap (dz)
between foot and plane.

Lets the visual-verification step distinguish "FindWalkableSphere
picked the right polygon" from "FindWalkableSphere returned a miss
and we fell through to outdoor-terrain backstop", which is critical
for triaging any remaining indoor collision oddities after the BSP
port lands.

Runtime-toggleable via the existing DebugPanel "Indoor BSP probe"
checkbox; zero cost when disabled.

Spec: docs/superpowers/specs/2026-05-19-indoor-walkable-plane-bsp-port-design.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 22:02:06 +02:00
Erik
7c516edd7b fix(physics): document adjustedCenter discard + restore wall-poly test
Code review feedback on Task 3 commit 91b29d1:

- TryFindIndoorWalkablePlane: comment explaining why FindWalkableSphere's
  adjustedCenter out param is intentionally discarded (ValidateWalkable
  recomputes contact geometry from plane + foot position, consistent
  with the outdoor terrain path).
- IndoorWalkablePlaneTests: new TryFindIndoorWalkablePlane_WallPolyInBsp_ReturnsFalse
  restores integration-level coverage that the renamed NoBsp_ReturnsFalse
  lost. Verifies WalkableAllowance gate rejects a wall polygon in the
  cell BSP. Steep-poly rejection is also covered at the BSPQuery layer
  by FindWalkableSphere_SteepPoly_RejectedByWalkableAllowance.

No behavior change. Build clean; all related tests pass; same 8
pre-existing failures.

Spec: docs/superpowers/specs/2026-05-19-indoor-walkable-plane-bsp-port-design.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:58:53 +02:00
Erik
91b29d1a89 fix(physics): route indoor walkable-plane synthesis through retail BSP walker
TryFindIndoorWalkablePlane (Phase 2 commit eb0f772) used a linear
first-match XY scan of cellPhysics.Resolved with no Z-proximity test.
For any cell with two walkable polys overlapping in XY at different Z
(cellars, 2nd floors, balconies, stairs spanning floors), it returned
whichever polygon came first in dictionary order — typically the upper
floor when descending, causing the player to be reported below the
synthesized plane → ValidateWalkable fails → falling-stuck. Symptoms
reported by user 2026-05-19: cannot descend into cellar; cannot walk
on 2nd floor; "invisible obstacles at certain spots" (suspected
cascade from wrong-Z ContactPlane misrouting the resolver state).

Fix: route through BSPQuery.FindWalkableSphere (added previous commit),
which wraps the existing retail-faithful FindWalkableInternal
(BSPNODE::find_walkable + BSPLEAF::find_walkable port). Adds a
sphereRadius parameter to TryFindIndoorWalkablePlane so the foot
sphere is built with the actual entity radius rather than a guess.
WalkableAllowance is save/restored via try/finally so the slope
threshold used by walkable_hits_sphere doesn't leak back to the
resolver. Method becomes an instance method (was static) to access
this.SpherePath.

Deletes the now-dead PointInPolygonXY helper.

Updates IndoorWalkablePlaneTests.cs: all TryFindIndoorWalkablePlane
test fixtures now include a PhysicsBSPTree leaf node (required by
the new routing path), calls pass sphereRadius, and the PointInPolygonXY
tests are removed (method deleted). Adds TransitionTypesTests.cs with
an integration test covering two-overlapping-floors selection AND
WalkableAllowance preservation.

Closes (pending visual verification): ISSUES #83.
Spec: docs/superpowers/specs/2026-05-19-indoor-walkable-plane-bsp-port-design.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:47:49 +02:00
Erik
86ecdf9ee1 fix(physics): tighten FindWalkableSphere test assertions + header
Code review feedback on Task 2 commit 7f55e14:

- Tests 1 and 2 now assert on adjustedCenter.Z (was the wrapper's
  primary behavioral contract — sphere placed on polygon plane —
  but it was unverified). Math derived from AdjustSphereToPlane:
  iDist = (dpPos - radius) / dpMove; new center = center - movement * iDist.
- Test 2 also gains the hitPoly.Plane.Normal.Z assertion that
  Test 1 already had.
- Test 4 comment slope-angle clarification.
- BSPQuery.cs FindWalkableSphere section header now notes this is
  not a direct retail port (it wraps BSPNODE::find_walkable +
  BSPLEAF::find_walkable via the existing FindWalkableInternal).

No behavior change. Build clean; 4/4 tests pass; same 8 pre-existing
failures.

Spec: docs/superpowers/specs/2026-05-19-indoor-walkable-plane-bsp-port-design.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:41:13 +02:00
Erik
7f55e14cd7 feat(physics): add BSPQuery.FindWalkableSphere wrapper
Thin public wrapper over the existing retail-faithful
FindWalkableInternal (BSPNODE::find_walkable + BSPLEAF::find_walkable
port). Probes downward by probeDistance along up, returns the closest
walkable polygon the sphere would rest on plus the adjusted center.

Will replace Transition.TryFindIndoorWalkablePlane's linear first-match
scan (next commit). The wrapper is callable from any "stand here, find
my floor" use case; current intent is indoor walkable-plane synthesis.

4 unit tests covering: two-floors-foot-between (sphere overlapping lower
floor), only-upper-floor-foot-above (sphere overlapping upper floor),
no-walkable-in-probe-range (sphere out of overlap distance for all
polygons), steep-poly-rejected-by-WalkableAllowance. Note: find_walkable
requires sphere to overlap the polygon plane (|dist| <= radius);
the tests use geometry that exercises this correctly, unlike the spec's
illustrative values which assumed a "nearest below" scan.

Spec: docs/superpowers/specs/2026-05-19-indoor-walkable-plane-bsp-port-design.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:33:27 +02:00
Erik
ff548b962c refactor(physics): expose hitPolyId from FindWalkableInternal
Adds a ref ushort hitPolyId parameter to FindWalkableInternal so callers
can identify which polygon was hit. The leaf branch already iterates
foreach (ushort polyId in node.Polygons); this surfaces it.

No behavior change. Existing callers (StepSphereDown, Path 4 Collide)
pass a discard local. The new BSPQuery.FindWalkableSphere wrapper
(next commit) will consume it.

Prep for indoor walkable-plane BSP port — see spec
docs/superpowers/specs/2026-05-19-indoor-walkable-plane-bsp-port-design.md
2026-05-19 21:22:40 +02:00
Erik
eb0f772f0f fix(physics): Phase 2 — synthesize indoor walkable plane from cell floor
When the indoor cell-BSP query returns OK (no wall collision), the player
is standing on a floor poly inside the cell. Previously the code fell
through to outdoor terrain (SampleTerrainWalkable + ValidateWalkable),
which used the OUTDOOR terrain plane — below the indoor floor due to the
+0.02f Z-bump applied for render z-fight prevention. ValidateWalkable
saw the player 0.5m above the outdoor plane → marked them as airborne
→ walkable=False → falling animation, never recovers.

Adds TryFindIndoorWalkablePlane (internal static for testability): scans
the cell's resolved physics polys for a walkable floor poly (normal.Z >=
0.6664, walkable-slope threshold matching retail) under the player's XY,
transforms its plane + vertices to world space via WorldTransform, and
calls ValidateWalkable with the indoor plane. Adds PointInPolygonXY
(ray-casting even-odd rule, ignores Z). Both are wired just after the
BSP OK branch in FindEnvCollisions; outdoor terrain remains a defensive
backstop if no floor poly is found under the player indoors (rare).

Matches retail's CEnvCell::find_env_collisions behavior: no fall-through
to terrain when the cell BSP successfully completes a query.

Evidence: launch-phase2-verify5.log captured 12,141 walkable=False
events during an indoor session where the player never managed to walk
back outdoor through a door — they got stuck against the indoor wall
and the resolver never re-established a walkable contact plane.

Adds 13 unit tests in IndoorWalkablePlaneTests.cs covering:
- player over floor poly (returns true, plane normal up, plane at correct Z)
- player outside poly XY (returns false)
- no walkable polys (returns false)
- empty Resolved dict (returns false)
- cell with world translation (plane + vertices in world space)
- PointInPolygonXY cases (centre, near corner, on boundary, outside, Z ignored)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 19:13:13 +02:00
Erik
3ffe1e44f6 fix(physics): Phase 2 — pass foot-sphere center to ResolveCellId
Visual test of Phase 2 portal traversal showed walls still didn't
block from inside buildings. Diagnosis: ResolveCellId was being
called with sp.CheckPos (entity reference, at the feet — world
Z=terrain) instead of sp.GlobalSphere[0].Origin (foot sphere center,
~0.5m above terrain). Combined with the +0.02f Z-bump on cached
cell origins (for render z-fight prevention), the test position
landed at cell-local Z=-0.02 — just below the cell floor — and
PointInsideCellBsp correctly reported "outside" for every cell.
CheckBuildingTransit never added candidates; player CellId stayed
outdoor; indoor cell-BSP collision branch never fired; walls didn't
block.

Retail's check_building_transit uses sphere.Center (the sphere CENTER,
not the entity reference) per the pseudocode at
docs/research/acclient_indoor_transitions_pseudocode.md:222-238.

Three call sites updated (PhysicsEngine x2 inside ResolveWithTransition;
TransitionTypes inside Transition.FindEnvCollisions).

Also adds a [check-bldg] diagnostic line to CheckBuildingTransit (gated
on the existing ACDREAM_PROBE_INDOOR_BSP flag) so future verification
captures show per-portal inside/outside results without needing
another diagnostic flag.

Spec: docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 18:54:10 +02:00
Erik
702b30a63e refactor(physics): Phase 2 — code-review polish on BuildingPhysics commit
Five reviewer-flagged items addressed:

- Fix #1: GameWindow building-loop now reuses TerrainSurface.ComputeOutdoorCellId
  instead of re-deriving the row-major cell-index formula. DRY win; no risk
  of the two formulas drifting.
- Fix #2: BuildingPhysics.ExactMatch decoder now references
  DatReaderWriter.Enums.PortalFlags.ExactMatch instead of magic 0x0001.
- Fix #3: ExactMatch XML doc clarified as "reserved per retail's
  CBldPortal::exact_match; not currently consumed by CheckBuildingTransit".
- Fix #4: CheckBuildingTransit docstring now explicitly documents the
  retail divergence — retail's sphere_intersects_cell (radius-aware) vs.
  our PointInsideCellBsp (radius-less). The sphereRadius parameter is
  reserved for the future sphere_intersects_cell port. Practical effect
  noted: entry fires ~sphereRadius (~0.48m) deeper than retail.
- Fix #5: Test method `SphereInsideBuildingPortalDestination_AddsInteriorCell`
  renamed to `BuildingPortalWithUnloadedCellBSP_NoCandidateAdded` — the
  test asserts Empty(candidates), not that the cell is added. Comment
  updated.

Spec: docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md
Plan: docs/superpowers/plans/2026-05-19-indoor-portal-cell-tracking.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 18:01:44 +02:00
Erik
069534a372 feat(physics): Phase 2 — BuildingPhysics + CheckBuildingTransit
Closes the outdoor→indoor entry path. New BuildingPhysics type holds
the per-SortCell BldPortal list + building world transform; PhysicsDataCache
caches it (CacheBuilding + GetBuilding); CellTransit.CheckBuildingTransit
tests each portal's destination cell via PointInsideCellBsp.

PhysicsEngine.ResolveCellId's outdoor branch now hooks CheckBuildingTransit
after the terrain-grid lookup: if the matched landcell has a cached
building stab, check whether the sphere has crossed into one of its
interior EnvCells before returning.

GameWindow at landblock-load time iterates LandBlockInfo.Buildings and
caches each via PhysicsDataCache.CacheBuilding. The landcell-id derivation
uses retail's row-major cell-index formula (gridX * 8 + gridY + 1).

Polish items from Subagent B/C reviews folded in:
- visited HashSet in FindCellList's BFS (avoids O(N^2) re-enqueue)
- ResolveCellId_NoDataCache_ReturnsFallback test (closes coverage gap)
- DataCache-asymmetry comment in PhysicsEngine.ResolveCellId
- Replaced misleading FindCellList outdoor-branch TODO with explicit
  note that ResolveCellId bypasses this branch — wired in ResolveCellId
  directly.
- Removed unused 'using DatReaderWriter.Types;' from CellTransit.cs
- 2 new CellTransitFindCellListTests integration tests
- 1 new CellTransitCheckBuildingTransitTests test (null-CellBSP guard
  case; happy path deferred to visual verification).

Spec: docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md
Plan: docs/superpowers/plans/2026-05-19-indoor-portal-cell-tracking.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 17:34:38 +02:00
Erik
aad697602e feat(physics): Phase 2 — port CellTransit + wire into ResolveCellId
New CellTransit static class ports retail's portal-graph cell traversal:
- FindTransitCellsSphere — indoor portal-neighbour walk
- AddAllOutsideCells     — outdoor 24m grid expansion
- FindCellList           — top-level driver (BFS through portals;
                           PointInsideCellBsp for final containment)

PhysicsEngine.ResolveOutdoorCellId renamed to ResolveCellId. Body
rewritten: indoor seeds delegate to CellTransit.FindCellList (portal-
graph BFS + BSP containment test); outdoor seeds keep the landblock
terrain grid lookup from the original implementation (preserving the
L.2e prefix-preservation fix). Signature extended with sphereRadius
parameter (needed by the sphere-vs-portal-plane test). Three call
sites updated (PhysicsEngine x2, TransitionTypes x1).

BSPQuery.PointInsideCellBsp retyped from PhysicsBSPNode? to CellBSPNode?
— the function operates on the cell-BSP tree (CellPhysics.CellBSP.Root
is a CellBSPNode). The previous PhysicsBSPNode typing was dead code, so
retype is safe.

Deletes the Phase D ResolveOutdoorCellIdTests.cs file. New ResolveCellIdTests
covers the equivalent contracts (fallback zero, outdoor seed with no
landblock).

Outdoor->indoor entry (check_building_transit) is stubbed pending the
BuildingPhysics infrastructure landing in the next commit.

Spec: docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md
Plan: docs/superpowers/plans/2026-05-19-indoor-portal-cell-tracking.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 17:14:04 +02:00
Erik
1969c55823 feat(physics): Phase 2 — wire CellBSP + Portals into CellPhysics
Adds PortalInfo struct and extends CellPhysics with CellBSP (third BSP
for point-in-cell tests, typed CellBSPTree from DatReaderWriter),
Portals (from envCell.CellPortals), PortalPolygons (resolved
cellStruct.Polygons — portals reference visible polys, not
PhysicsPolygons), and VisibleCellIds (populated for future use;
envCell.VisibleCells is List<UInt16>, not Dictionary).

Deletes CellPhysics.LocalAabbMin/Max and PhysicsDataCache.TryFindContainingCell
— Phase D's AABB shortcut is gone. CacheCellStruct's AABB compute
removed; the [cell-cache] diagnostic updated with portal/visible counts
instead.

CacheCellStruct signature gains an EnvCell parameter (one call site in
GameWindow.cs:5384 updated). ResolveOutdoorCellId drops the
TryFindContainingCell call; portal-graph CellTransit replaces it next.

ResolveOutdoorCellIdTests object initializers had the deleted AABB
properties stripped temporarily so the build stays green; the file gets
replaced wholesale in the next commit (CellTransit integration). Those
2 AABB-containment tests continue to fail (they were pre-broken on this
branch); no new failures introduced.

Spec: docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md
Plan: docs/superpowers/plans/2026-05-19-indoor-portal-cell-tracking.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:52:20 +02:00
Erik
1f11ba9b38 feat(diag): Cluster A — extend [cell-cache] with AABB + bsphere + recursive poly count
The original Phase E [cell-cache] probe (fda6af7) only showed the BSP root
node's direct poly count, which was always 0 for non-trivial trees (internal
node root). Extending the probe to:

- Recursively walk the BSP tree and count total leaf polys
- Detect unmatched poly IDs (BSP leaves referencing IDs not in our resolved dict)
- Dump the BSP root bounding sphere (center + radius)
- Dump the cell's local AABB (min/max from poly vertices)
- Dump the cell's world origin (cellTransform * (0,0,0))

The extended data made the route-δ diagnosis definitive: Holtburg cells DO
have full physics polygons in their BSPs (e.g. 0xA9B40143 has 14 polys all
resolved, full Z range 0-2.8 m). The bug is upstream — AABB-based cell
containment is too tight to capture a standing player at most thresholds
between rooms, so the indoor cell-BSP branch fires only intermittently.

Retail uses portal traversal (CObjMaint::HandleObjectEnterCell + cell-side
portal data) which propagates CellId at door crossings. Our AABB-containment
shortcut is partial. This diagnostic stays in place as infrastructure for
the follow-up "Indoor portal-based cell tracking" phase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:04:45 +02:00
Erik
fda6af7ad0 diag: add ACDREAM_PROBE_CELL_CACHE to explain indoor BSP poly=n/a
Every [indoor-bsp] probe line reports result=OK poly=n/a, meaning
BSPQuery.FindCollisions never records a hit polygon. Four hypotheses:
(a) PhysicsPolygons.Count == 0 for all cached EnvCells (empty data),
(b) BSP leaf Polygons IDs don't match PhysicsPolygons dict keys,
(c) ResolvePolygons filters out all polygons (vertex lookups fail or
    degenerate normals), or (d) sphere is too far from BSP leaf bounds.

Format analysis rules out (b): retail BSPLEAF::PackLeaf writes
poly_id (not array index) into the BSP leaf ushort list; CPolygon::Pack
writes poly_id as first field; DatReaderWriter reads it as dictionary
key. ACE DatLoader does the same. Keys are consistent end-to-end.

Add ProbeCellCacheEnabled (ACDREAM_PROBE_CELL_CACHE=1) to
PhysicsDiagnostics and a [cell-cache] log line at the end of
CacheCellStruct. One line per cached EnvCell:

  [cell-cache] envCellId=0x... physicsPolyCount=N resolvedCount=M
               bspRootPolyCount=K bspRootHasChildren=true|false

physicsPolyCount=0 -> hypothesis (a).
resolvedCount < physicsPolyCount -> hypothesis (c).
Non-zero counts + bspRootPolyCount=0 + bspRootHasChildren=true ->
  expected (internal node, leaves hold poly refs); then investigate (d).
Non-zero counts + bspRootPolyCount=0 + bspRootHasChildren=false ->
  leaf with empty Polygons list, deeper investigation needed.

Cross-referencing cell-cache lines with indoor-bsp lines (same
envCellId) will pin the root cause in the next launch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:47:59 +02:00
Erik
c19d6fb321 fix(physics): Cluster A #84 + #85 — indoor cell tracking
ResolveOutdoorCellId only resolved outdoor terrain landcells. A player
geometrically inside an EnvCell stayed in outdoor-landcell range, so
FindEnvCollisions' indoor cell-BSP branch (gated on cellLow >= 0x0100)
never fired. Both #84 (blocked by air indoors) and #85 (pass through
walls outside→in) are downstream of this — without indoor cell-BSP
collision the player gets stuck against outdoor-stab back-faces of the
building shell, and walls only block from one side.

Adds an indoor-cell-containment check via PhysicsDataCache: at
CacheCellStruct time, compute each cell's local AABB from its resolved
polygon vertices; at ResolveOutdoorCellId time, transform the world
position into each cached cell's local space and return the matched
cell's full id when contained. Falls through to the existing outdoor
terrain logic when no EnvCell contains the position.

Also fixes a pre-existing prefix-preservation bug in the outdoor branch:
the function now always applies the matched landblock's high-16 prefix
even when the input fallbackCellId arrived bare-low-byte (the L.2e
finding from CLAUDE.md). Updated two existing PhysicsEngineTests that
encoded the old bare-low-byte output.

Evidence: launch-cluster-a-capture.log @ 2026-05-19 — player at
worldPos (155.376, 14.010, 94.000) geometrically inside cottage cell
0xA9B40172, but sp.CheckCellId stuck at 0x00000031 (outdoor landcell)
across 454 [resolve] lines; zero [indoor-bsp] lines because the gate
never opened.

Closes #84.
Closes #85.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:20:36 +02:00
Erik
3764867566 fix(picker): Cluster A #86 — cell-BSP ray occlusion in WorldPicker
WorldPicker.Pick previously had no occlusion test — any entity along
the click ray within maxDistance was a candidate, including ones
behind walls. Adds the CellBspRayOccluder static helper that
Möller-Trumbore-tests the click ray against every polygon in every
currently-cached EnvCell BSP, returning the nearest wall-hit `t`.
Both Pick overloads gate candidate selection by that wall-t (legacy
ray-sphere via world-space `t`, screen-rect via camera-space clip.W
depth — matching ScreenProjection.TryProjectSphereToScreenRect's
convention).

PhysicsDataCache exposes a new CellStructIds snapshot accessor so the
caller can iterate without needing the private cache dictionary.
CellPhysics.BSP/PhysicsPolygons/Vertices relaxed from required to
nullable so test fixtures can construct a CellPhysics from Resolved
alone without a real DAT BSP object. GameWindow snapshots the loaded
cell physics on each Pick call and passes the occluder callback.

Closes #86.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:41:56 +02:00
Erik
27d7de11d8 feat(physics): Cluster A — indoor BSP collision probe
Adds the [indoor-bsp] probe + ProbeIndoorBspEnabled toggle for the
Indoor walking Phase 1 BSP-cluster investigation. Mirrors the existing
[resolve] / [cell-transit] / [indoor-*] pattern: one log line per
BSPQuery.FindCollisions call from FindEnvCollisions' cell branch,
capturing cell id, sphere local-pos, result TransitionState, and the
hit poly's normal + side-type via the LastBspHitPoly side-channel
(already wired for ProbeBuildingEnabled, now also fires for the indoor
flag).

Toggle via ACDREAM_PROBE_INDOOR_BSP=1 env var or DebugPanel checkbox.
Zero-cost when off.

Predecessor for the three fix commits that will close ISSUES.md
#84/#85/#86 after the capture session.

Spec: docs/superpowers/specs/2026-05-19-indoor-walking-phase1-bsp-cluster-design.md
Plan: docs/superpowers/plans/2026-05-19-indoor-walking-phase1-bsp-cluster.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:24:07 +02:00
Erik
9f069e14c9 fix(animation): close #61 + smooth stop from backward/sidestep-left/turn-left
Two related AnimationSequencer fixes for visible animation glitches at
motion-cycle boundaries.

1. Link-tail blend hold (closes #61). BuildBlendedFrame was wrapping
   nextIdx unconditionally to rangeLo at the high-frame boundary —
   correct for looping cyclic nodes (idle/run/walk loops), wrong for
   one-shot links and action overlays. During the ~30 ms fractional
   tail before the sequencer transitions to the next queue node, the
   blend mixed frame[end] with frame[0], producing a one-frame flash
   through the anim's starting pose. Symptoms: door swing-open flap
   (frame 0 = closed pose) and player run-stop twitch (frame 0 =
   mid-stride). Fix: gate the wrap on curr.IsLooping; non-looping
   nodes hold the boundary frame until AdvanceToNextAnimation fires.

2. Stop-anim direction fallback. Stopping from WalkBackward /
   SideStepLeft / TurnLeft hit a null linkData from GetLink (the dat
   authors a single forward/right stop link and reuses it for both
   directions). SetCycle then enqueued only the Ready cycle, snapping
   straight to idle with no leg-settle blend. Fix: when the primary
   GetLink lookup is null, retry with the substate's low byte remapped
   to its forward/right peer (0x06→0x05, 0x10→0x0F, 0x0E→0x0D).

Both fixes are pinned by new regression tests in
AnimationSequencerTests that fail against the prior code (Y=5.02 for
the link tail wrap → frame 0 blend; Y=0 for the backward stop snapping
to Ready cycle).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:16:20 +02:00