diff --git a/CLAUDE.md b/CLAUDE.md index 1f2ec4a..4836d9c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -776,35 +776,32 @@ acdream's plan lives in two files committed to the repo: acceptance criteria. Do not drift from the spec without explicit user approval. -**Indoor walking Phase 1 — BSP cluster (Cluster A) partially shipped -2026-05-19.** Seven commits across five phases: -- `18a2e28` — implementation plan -- `27d7de1` — Phase A: `[indoor-bsp]` probe + `ProbeIndoorBspEnabled` toggle -- `3764867` — Phase B: `CellBspRayOccluder` in `WorldPicker.Pick` (**closes #86**) -- `4e308d5` — Phase B follow-up: screen-rect cell-occlusion tests -- `c19d6fb` — Phase D: AABB containment for indoor CellId promotion + L.2e bare-low-byte fix (partial #84 fix) -- `fda6af7` — Phase E first commit: `[cell-cache]` probe -- `1f11ba9` — Phase E second commit: extended `[cell-cache]` with AABB + bsphere + poly counts +**Indoor walking Phase 2 — Portal-based cell tracking shipped +2026-05-19.** Six commits: +- `1969c55` — CellBSP + Portals wired into CellPhysics (`PortalInfo` struct, `VisibleCellIds`) +- `aad6976` — `CellTransit.FindCellList` + `FindTransitCellsSphere` + `AddAllOutsideCells`; `ResolveCellId` rename +- `069534a` — `BuildingPhysics` + `CheckBuildingTransit` for outdoor→indoor entry via `BldPortalInfo` +- `702b30a` — code-review polish (DRY cell-id derivation, `PortalFlags.ExactMatch` enum, docs) +- `3ffe1e4` — critical fix: pass foot-sphere center (`GlobalSphere[0].Origin`) not `CheckPos` to `ResolveCellId` +- `eb0f772` — `TryFindIndoorWalkablePlane` synthesizes indoor walkable plane from cell floor poly -**#86** (click selection penetrates walls) — **CLOSED.** `WorldPicker.Pick` -consults `CellBspRayOccluder.NearestWallT`; entities behind walls are filtered. -**#84** (blocked by air indoors) — **PARTIAL.** The "spawn-in-building stuck -above floor" variant is resolved (Phase D promotes CellId to the indoor cell -on spawn-in). The remaining "walls don't block from inside during normal -walking" symptom is the same root cause as #85 — AABB containment is too -tight for threshold/doorway cells (Z range ~0.2 m, player stands at ~0.46 m) -to keep CellId promoted. Both are tracked under new issue **#87**. -**#85** (pass through walls outside→in) — **OPEN.** Root cause confirmed as -same as #84 remaining symptom — CellId drifts back to outdoor cell, indoor -BSP never fires. See #87. -**#87** (indoor portal-based cell tracking) — **FILED.** Retail-faithful fix -via `CObjMaint::HandleObjectEnterCell` + `CEnvCell` portal connectivity. -Diagnostic infrastructure from Cluster A (`[indoor-bsp]` + `[cell-cache]` -probes, both runtime-toggleable) stays in place as scaffolding for the -follow-up phase. Handoff: [`docs/research/2026-05-19-cluster-a-shipped-handoff.md`](docs/research/2026-05-19-cluster-a-shipped-handoff.md). +**#86** (click selection penetrates walls) — **CLOSED** (Phase 1 Cluster A). +**#84** (blocked by air indoors) — **FULLY CLOSED.** Spawn-in-building variant +closed by Phase 1 (Phase D AABB containment). Wall-block-from-inside variant +closed by Phase 2 (portal-graph traversal). +**#85** (pass through walls outside→in) — **CLOSED** by Phase 2. +`CheckBuildingTransit` promotes CellId via the building-shell portal graph +on outdoor→indoor entry; indoor-BSP collision fires from both sides. +**#87** (indoor portal-based cell tracking) — **CLOSED** by Phase 2. +**#88** (indoor static objects vibrate) — **FILED** (pre-existing, Medium). +**#89** (port `BSPQuery.SphereIntersectsCellBsp`) — **FILED** (Low, documented +approximation in `CheckBuildingTransit`). +Diagnostic infrastructure: `[indoor-bsp]`, `[cell-cache]`, `[cell-transit]`, +`[check-bldg]` probes all stay in place. +Handoff: [`docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md`](docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md). +Phase 1 handoff: [`docs/research/2026-05-19-cluster-a-shipped-handoff.md`](docs/research/2026-05-19-cluster-a-shipped-handoff.md). **Next phase is Claude's choice** per work-order autonomy. Candidates: -indoor portal-based cell tracking (#87, completes the indoor walking story); M2 critical path (F.2 / F.3 / F.5a / L.1c / L.1b — kill-a-drudge demo); or the pre-existing "next phase candidates" list below. diff --git a/docs/ISSUES.md b/docs/ISSUES.md index d80b7bf..8e1db95 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -237,9 +237,10 @@ to the second floor without getting stuck. --- -## #84 — Blocked by air indoors +## #84 — [DONE 2026-05-19] Blocked by air indoors -**Status:** OPEN (partial fix 2026-05-19) +**Status:** DONE +**Closed:** 2026-05-19 **Severity:** HIGH (blocks indoor navigation) **Filed:** 2026-05-19 **Component:** physics, collision @@ -274,58 +275,68 @@ enables the `FindEnvCollisions` indoor-BSP branch. This resolved the "spawn in building and be stuck above the floor" variant of #84 — player's CellId now promotes to the interior cell on spawn-in, the floor is walkable, and the player can move freely. The "invisible air obstacle" -symptom for rooms the player walks INTO from outside is now superseded by -the root cause in #87 (AABB containment is too tight for threshold/ -doorway cells to keep CellId promoted during normal walking). That -remaining symptom will be resolved by the portal-based cell tracking -fix. +symptom for rooms the player walks INTO from outside was tracked under #87 +and required portal-based cell tracking. + +**Resolution (2026-05-19 full · `1969c55, aad6976, 069534a, 702b30a, 3ffe1e4, eb0f772`):** +Indoor walking Phase 2 replaced AABB containment with portal-graph cell traversal +(`CellTransit.FindCellList` + `CheckBuildingTransit`). CellId now promotes to indoor +cells via portals and remains promoted during normal walking through doorways. Indoor +cell-BSP collision fires consistently. Indoor walkable plane synthesized from floor +poly (`TryFindIndoorWalkablePlane`) so the resolver tracks walkability correctly when +the player is standing on an indoor floor. User visually verified at Holtburg cottage: +walls block from inside, multi-room navigation works, walking outdoors through a door +works. Issue fully closed. --- -## #85 — Pass through walls from outside→in +## #85 — [DONE 2026-05-19 · 1969c55, aad6976, 069534a, 702b30a, 3ffe1e4, eb0f772] Pass through walls from outside→in -**Status:** OPEN -**Severity:** HIGH (gameplay-breaking) +**Status:** DONE +**Closed:** 2026-05-19 +**Commits:** `1969c55, aad6976, 069534a, 702b30a, 3ffe1e4, eb0f772` **Filed:** 2026-05-19 **Component:** physics, collision -**Description:** Approaching a building from the outside, the player +**Resolution (2026-05-19 · Indoor walking Phase 2):** The root cause (CellId never promoted +to the indoor cell during outdoor→indoor walking) was resolved by portal-graph cell +traversal in `CellTransit.CheckBuildingTransit`. Once `CellId` promotes to the indoor +cell, the indoor-BSP collision branch in `FindEnvCollisions` fires for approaches from +both inside and outside. User visually verified walls block from outside (player must +use the door portal to enter). See #87 and handoff: +[`docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md`](2026-05-19-indoor-walking-phase2-shipped-handoff.md). + +**Original description:** Approaching a building from the outside, the player can walk THROUGH walls into the interior — one-directional wall collision. From the inside trying to exit, the wall does block. -**Root cause / status:** Cell BSP polygons likely have one-sided -normals (front-facing only). Approach from the inside hits the front; -approach from the outside hits the back which BSP traversal treats as -"behind the plane" → no collision. Retail handles this via two-sided -collision polys or per-poly back-face handling. - -**Files:** -- `src/AcDream.Core/Physics/BSPQuery.cs` -- `src/AcDream.Core/Physics/TransitionTypes.cs` (`FindObjCollisions` cell - branch). - -**Acceptance:** Walking into an inn wall from outside collides; player -must enter via the door portal. - -**Status update (2026-05-19):** The root cause is now pinned as the -same failure as #84's remaining symptom — `CellId` isn't promoted to -the indoor cell during normal outdoor→indoor walking because AABB -containment is too tight for threshold/doorway cells. Without CellId -in the indoor cell, the indoor-BSP collision branch in -`FindEnvCollisions` never fires regardless of approach direction. -See new issue #87 (portal-based indoor cell tracking) for the -retail-faithful fix. +The root cause was pinned (Cluster A 2026-05-19) as the same failure as +#84's remaining symptom — `CellId` wasn't promoted to the indoor cell +during normal outdoor→indoor walking because AABB containment was too +tight for threshold/doorway cells. Without CellId in the indoor cell, +the indoor-BSP collision branch in `FindEnvCollisions` never fired +regardless of approach direction. --- -## #87 — Indoor cell tracking uses AABB containment instead of portal traversal +## #87 — [DONE 2026-05-19 · 1969c55, aad6976, 069534a, 702b30a, 3ffe1e4, eb0f772] Indoor cell tracking uses AABB containment instead of portal traversal -**Status:** OPEN -**Severity:** HIGH +**Status:** DONE +**Closed:** 2026-05-19 +**Commits:** `1969c55, aad6976, 069534a, 702b30a, 3ffe1e4, eb0f772` **Filed:** 2026-05-19 **Component:** physics -**Description:** `PhysicsDataCache.TryFindContainingCell` promotes the +**Resolution (2026-05-19 · Indoor walking Phase 2):** Portal-graph cell traversal +(`CellTransit.FindCellList` + `CheckBuildingTransit`) replaced the AABB containment +shortcut. Player CellId now correctly promotes to indoor cells via portals; +indoor cell-BSP collision branch fires consistently; walls block from inside. +Outdoor→indoor entry via `BuildingPhysics` + `BldPortalInfo` (`CheckBuildingTransit`) +wires the building-shell portal graph. Indoor walkable plane synthesized from the +cell's floor poly so the resolver tracks walkability during indoor movement (`TryFindIndoorWalkablePlane`). +See handoff: [`docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md`](2026-05-19-indoor-walking-phase2-shipped-handoff.md). + +**Original description:** `PhysicsDataCache.TryFindContainingCell` promotes the player's `CellId` to an indoor EnvCell when their world position falls inside any cached cell's local AABB. This is too tight to keep `CellId` promoted to an indoor cell during normal walking. Threshold/doorway cells @@ -338,34 +349,44 @@ physics is unreliable. The retail fix is portal-based cell traversal — when the player crosses a cell portal boundary, the cell ownership propagates through portal connectivity data in `CEnvCell`. -**Evidence:** `launch-cluster-a-cache-diag3.log` (Cluster A Phase E -capture). Cell `0xA9B40143` (real room) has -`physicsPolyCount=14 bspTotalLeafPolys=14 bspUnmatchedIds=0 -aabbMin=(-11.60,-1.60,0.00) aabbMax=(-6.20,7.60,2.80)` — geometry is -complete and the AABB spans 2.8 m height, which works. Cell `0xA9B40146` -(threshold/doorway) has `physicsPolyCount=4 -aabbMin=(-11.60,2.80,-0.20) aabbMax=(-10.00,7.60,0.00)` — Z range is -only 0.2 m; a standing player is always outside it. Only 6 `[indoor-bsp]` -lines fired across an entire indoor walking session (all during mid-jump -frames when the player was briefly inside the room AABB at jump height). +--- + +## #88 — Indoor static objects vibrate (bookshelves, open furnaces) + +**Status:** OPEN +**Severity:** MEDIUM (visual jitter; doesn't block gameplay) +**Filed:** 2026-05-19 +**Component:** rendering, animation + +**Description:** Static objects inside cells (bookshelves, open furnaces, possibly other interior props) show per-frame transform jitter / vibration. Pre-existing (user noticed before Phase 2 shipped). Likely candidates: + +1. `EntityScriptActivator.OnCreate/OnRemove` firing repeatedly as the player's CellId promotes/demotes near cell boundaries (less likely after Phase 2's portal-based tracking — but worth investigating). +2. Per-part transforms for cell-static `WorldEntity` instances getting recomputed each frame with floating-point drift. +3. Particle-emitter offsets accumulating instead of resetting. + +**Files to investigate:** +- `src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs` — OnCreate/OnRemove call patterns +- `src/AcDream.App/Rendering/GpuWorldState.cs` — entity transform updates per frame +- `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` — per-batch transform composition + +**Acceptance:** Indoor static objects render stable (no per-frame jitter). + +--- + +## #89 — Port BSPQuery.SphereIntersectsCellBsp for retail-faithful CheckBuildingTransit + +**Status:** OPEN +**Severity:** LOW (Phase 2 ships with a documented approximation) +**Filed:** 2026-05-19 +**Component:** physics + +**Description:** Retail's `CEnvCell::check_building_transit` uses `CCellStruct::sphere_intersects_cell` — a radius-aware sphere-vs-BSP test that returns Inside/Crossing/Outside. Phase 2's `CellTransit.CheckBuildingTransit` uses `BSPQuery.PointInsideCellBsp` (radius-less, tests only the sphere CENTER). Practical effect: outdoor→indoor entry fires ~sphereRadius (~0.48m) deeper into the doorway than retail. The sphereRadius parameter is plumbed through but currently unused. **Files:** -- `src/AcDream.Core/Physics/PhysicsDataCache.cs` (`TryFindContainingCell`, - approximately line 261) -- `src/AcDream.Core/Physics/PhysicsEngine.cs` (`ResolveOutdoorCellId`, - approximately line 238) -- `src/AcDream.Core/Physics/TransitionTypes.cs` (`FindEnvCollisions` cell - branch, approximately line 1188) +- `src/AcDream.Core/Physics/CellTransit.cs::CheckBuildingTransit` (line ~162) +- `src/AcDream.Core/Physics/BSPQuery.cs::PointInsideCellBsp` (line ~940) — existing point test to model the new sphere variant after -**Retail reference:** PDB symbols `CObjMaint::HandleObjectEnterCell` and -`CEnvCell` portal data. See `docs/research/named-retail/acclient.h` lines -31715-31726 for `CCellStructure` shape; `acclient_2013_pseudo_c.txt` for -the implementations. - -**Acceptance:** Player walking from outside the Holtburg cottage into the -interior crosses portals and `CellId` updates accordingly; walls block -from both inside and outside; the `[indoor-bsp]` probe fires consistently -during indoor walking (not just during mid-jump frames). +**Acceptance:** `CellTransit.CheckBuildingTransit` calls a new `BSPQuery.SphereIntersectsCellBsp(node, sphereCenter, sphereRadius)` that returns `Inside`/`Crossing`/`Outside`. Entry timing matches retail visually at the Holtburg cottage door. --- diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index 81ab9aa..9d5541b 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -72,6 +72,7 @@ | Indoor lighting + rendering — Phase 2 (fix) | Three-component diagnostic-driven fix for missing-floor bug. Component 1: `WbMeshAdapter` captures the `Task` from `PrepareMeshDataAsync` and attaches a `ContinueWith` for EnvCell ids — surfaces faulted-task exceptions + clean-null returns. Component 2: replaced `NullLogger` with a Console-backed `ConsoleErrorLogger` so WB's intentional `_logger.LogError(ex, ...)` at the swallow site at `ObjectMeshManager.cs:589` writes `[wb-error]` lines. **Root cause definitively identified in one capture: `ArgumentOutOfRangeException` from `DatReaderWriter.Setup.Unpack` at WB's `PrepareEnvCellMeshData` line 1223 — `TryGet(stab.Id, ...)` was called blindly on every `envCell.StaticObjects` id without checking the Setup-prefix bit. GfxObj-typed stabs (0x01xxxxxx) caused mid-deserialization throws, bubbling up to PrepareMeshData's outer catch which silently returned null. Entire cell upload failed, room mesh never reached `_renderData`.** Component 3 fix: one-line type-check guard `(stab.Id & 0xFF000000u) == 0x02000000u && _dats.Portal.TryGet(stab.Id, out var stabSetup)`. Committed to WB submodule on branch `acdream-fix-floor-rendering` at SHA `34460c4` — needs submodule pointer advance at merge time. **Verification: 0 [wb-error] (was 385), 0 NULL_RESULT (was 55), Holtburg 123/123 cells complete (was 97/123). User visually confirmed floors render in Holtburg Inn.** Surfaced 9 pre-existing indoor bugs (see-through floor, indoor collision, stairs, walls, click-thru, indoor lighting artifacts, atmospheric-lighting-on-stabs, slope terrain lighting) — all filed in `docs/ISSUES.md` for follow-up phases. Cause report: [`docs/research/2026-05-19-indoor-cell-rendering-cause.md`](../research/2026-05-19-indoor-cell-rendering-cause.md). Verification: [`docs/research/2026-05-19-indoor-cell-rendering-verification.md`](../research/2026-05-19-indoor-cell-rendering-verification.md). Plan: [`docs/superpowers/plans/2026-05-19-phase2-indoor-cell-rendering-fix.md`](../superpowers/plans/2026-05-19-phase2-indoor-cell-rendering-fix.md). | Live ✓ | | C.1.5b | Per-part PES transforms + dat-hydrated entity DefaultScript dispatch. Closes issue #56. Shipped 2026-05-12 across 5 commits (`1e3c33b` docs+plan, `f3bc15e` SetupPartTransforms helper, `11521f4` ParticleHookSink applies `CreateParticleHook.PartIndex`, `5ca5827` activator refactor + GameWindow resolver lambda, `8735c39` GpuWorldState 4 new fire-sites). **Slice A** — new [`SetupPartTransforms.Compute(setup)`](../../src/AcDream.Core/Meshing/SetupPartTransforms.cs) walks `PlacementFrames[Resting]` → `[Default]` → first-available (mirrors `SetupMesh.Flatten` priority) and returns `Matrix4x4` per part; new `ParticleHookSink.SetEntityPartTransforms(entityId, partTransforms)` mirrors the existing `_rotationByEntity` pattern; `SpawnFromHook` now transforms hook offset through `partTransforms[partIndex]` before applying entity rotation. **Slice B** — activator's `ServerGuid==0` guard relaxed: keys by `entity.ServerGuid` when non-zero, else `entity.Id` (collision-free with server guids in the `0x40xxxxxx` interior / `0x80xxxxxx` scenery / `0xC0xxxxxx` ranges). Resolver delegate refactored to return `ScriptActivationInfo(ScriptId, PartTransforms)` so one dat lookup yields both pieces. `GpuWorldState` fires the activator from 4 new sites: `AddLandblock` + `AddEntitiesToExistingLandblock` (Far→Near promotion) for OnCreate, `RemoveLandblock` + `RemoveEntitiesFromLandblock` (Near→Far demotion) for OnRemove. ServerGuid==0 filter on AddLandblock avoids double-firing pending-bucket merges. **Reality discovery folded into spec §3**: EnvCell `StaticObjects` are already hydrated as `WorldEntity` instances by `GameWindow.BuildInteriorEntitiesForStreaming` (with stable `entity.Id` in `0x40xxxxxx`) — no synthetic-ID scheme or separate walker class needed (handoff §4 Q1/Q2 mooted). **Visual verification 2026-05-12**: Holtburg Town network portal swirl distributes across the arch (no ground-burial), Inn fireplace flames render over the firebox, cottage chimney smoke columns render, spell-cast animation-hook particles all match retail. 18 new + 4 updated tests, all Vfx/Meshing/Streaming/Activator green. Spec: [`docs/superpowers/specs/2026-05-13-phase-c1.5b-design.md`](../superpowers/specs/2026-05-13-phase-c1.5b-design.md). Plan: [`docs/superpowers/plans/2026-05-13-phase-c1.5b.md`](../superpowers/plans/2026-05-13-phase-c1.5b.md). | Live ✓ | | Indoor walking Phase 1 — BSP cluster (partial) | 2026-05-19. Probe + WorldPicker cell-BSP occlusion (#86 closed) + CellId promotion via AABB containment (partial #84 fix). Seven commits across 5 phases: `18a2e28` plan, `27d7de1` Phase A `[indoor-bsp]` probe + toggle, `3764867` Phase B CellBspRayOccluder in WorldPicker, `4e308d5` Phase B screen-rect tests, `c19d6fb` Phase D AABB containment + L.2e bare-low-byte fix, `fda6af7` Phase E `[cell-cache]` diagnostic, `1f11ba9` Phase E extended AABB/bsphere/poly-count fields. **#86 closed** (picker occlusion). **#84 partially closed** (spawn-in-building stuck-above-floor resolved; threshold/doorway walls remain open under #87). **#85 open** (wall pass-through root cause confirmed as same as #84 remaining symptom — CellId doesn't stay promoted during outdoor→indoor walking). **#87 filed** (portal-based indoor cell tracking — retail-faithful follow-up). `[indoor-bsp]` + `[cell-cache]` probes stay in place as scaffolding for the follow-up phase. Handoff: [`docs/research/2026-05-19-cluster-a-shipped-handoff.md`](../research/2026-05-19-cluster-a-shipped-handoff.md). Plan: [`docs/superpowers/plans/2026-05-19-indoor-walking-phase1-bsp-cluster.md`](../superpowers/plans/2026-05-19-indoor-walking-phase1-bsp-cluster.md). | Tests ✓ | +| Indoor walking Phase 2 — Portal-based cell tracking | 2026-05-19. Portal-graph traversal replaces Phase D's AABB containment. Six commits: `1969c55` CellBSP+Portals wired into CellPhysics; `aad6976` CellTransit.FindCellList + FindTransitCellsSphere + AddAllOutsideCells + ResolveCellId rename; `069534a` BuildingPhysics + CheckBuildingTransit for outdoor→indoor entry; `702b30a` code-review polish; `3ffe1e4` pass foot-sphere center to ResolveCellId (critical fix — was passing CheckPos instead of GlobalSphere[0].Origin, causing PointInsideCellBsp to return false at floor level); `eb0f772` TryFindIndoorWalkablePlane synthesizes walkable plane from cell floor poly so the resolver doesn't fall through to outdoor SampleTerrainWalkable. **Closes #87, #85, and the wall-pass-through portion of #84 (fully closes #84).** Files #88 (indoor static object vibration — pre-existing) and #89 (BSPQuery.SphereIntersectsCellBsp — approximation in CheckBuildingTransit). `[cell-transit]`, `[indoor-bsp]`, `[check-bldg]`, `[cell-cache]` probes stay in place. Handoff: [`docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md`](../research/2026-05-19-indoor-walking-phase2-shipped-handoff.md). | Live ✓ | Plus polish that doesn't get its own phase number: - FlyCamera default speed lowered + Shift-to-boost diff --git a/docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md b/docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md new file mode 100644 index 0000000..1365c70 --- /dev/null +++ b/docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md @@ -0,0 +1,284 @@ +# Indoor walking Phase 2 — Portal-based cell tracking — handoff (2026-05-19) + +**Date:** 2026-05-19. +**Branch:** `claude/competent-robinson-dec1f4` (commits land here; merge to main handled by controller). +**Predecessor:** Indoor walking Phase 1 — BSP cluster (Cluster A). Partially shipped 2026-05-19; closed #86 cleanly, filed #87 for the portal-traversal root cause. Diagnostic infrastructure (`[indoor-bsp]` + `[cell-cache]` probes) remained as scaffolding. Handoff: [`docs/research/2026-05-19-cluster-a-shipped-handoff.md`](2026-05-19-cluster-a-shipped-handoff.md). + +--- + +## TL;DR + +Phase 2 fully closes the indoor-walking story. Six commits replace Phase D's +AABB-containment shortcut with retail-faithful portal-graph cell traversal. +`CellId` now promotes to indoor cells via portals and remains promoted through +doorways, thresholds, and multi-room navigation. Indoor cell-BSP collision fires +consistently. A critical fix in commit 5 passes the foot-sphere center (not the +entity reference point) to `ResolveCellId`, which was the production failure that +made PointInsideCellBsp return false at floor level. Commit 6 adds +`TryFindIndoorWalkablePlane` so the walkability resolver doesn't fall through to +outdoor terrain when the player is inside. + +**Visual verification at Holtburg cottage (2026-05-19, user testing live ACE):** +- Walls block from inside — player cannot walk through cottage walls. +- Multi-room navigation via doorways works — `[cell-transit]` log shows `0xA9B40145 → 0x143 → 0x144 → 0x13F` chains. +- Walking back outdoors through a door works (post-walkable fix in commit 6). +- Cell tracking is robust through multiple indoor sessions. + +--- + +## Commits + +| # | SHA | Subject | +|---|---|---| +| 1 | `1969c55` | `feat(physics): Phase 2 — wire CellBSP + Portals into CellPhysics` | +| 2 | `aad6976` | `feat(physics): Phase 2 — port CellTransit + wire into ResolveCellId` | +| 3 | `069534a` | `feat(physics): Phase 2 — BuildingPhysics + CheckBuildingTransit` | +| 4 | `702b30a` | `refactor(physics): Phase 2 — code-review polish on BuildingPhysics commit` | +| 5 | `3ffe1e4` | `fix(physics): Phase 2 — pass foot-sphere center to ResolveCellId` | +| 6 | `eb0f772` | `fix(physics): Phase 2 — synthesize indoor walkable plane from cell floor` | + +**Build:** clean on all commits. +**Tests:** `dotnet test` shows the same 8 pre-existing failures in +`AcDream.Core.Tests` (MotionInterpreter / BSPStepUp / etc., unchanged). All +new Phase 2 tests and the walkable-plane tests green. + +--- + +## What shipped + +### Commit 1 — CellBSP + Portals wired into CellPhysics + +New `PortalInfo` struct holds `PortalId`, `PortalPolygonIndex`, `PortalFlags`, +and `OtherCellId`. `CellPhysics` extended with: +- `CellBSP` — a third BSP tree (alongside `PhysicsBSP` and the render BSP) used + for point-in-cell tests. Retail: `CCellStructure::cell_bsp`. +- `Portals` — `IReadOnlyList` built from `envCell.CellPortals`. +- `PortalPolygons` — the visible polygons that portals reference (`cellStruct.Polygons`, + not `PhysicsPolygons`; portals reference the visible-geometry polygon list). +- `VisibleCellIds` — cells visible from this cell (used by `AddAllOutsideCells`). + +Phase D's `LocalAabbMin/Max` + `TryFindContainingCell` are deleted — they are now +superseded by the portal traversal in `CellTransit`. + +### Commit 2 — CellTransit + ResolveCellId + +New `CellTransit` static class implements the retail portal-neighbour walk. +Three public entry points: + +- **`FindTransitCellsSphere(sphereCenter, sphereRadius, startCell, cache)`** — + walks portal connectivity from `startCell` outward. For each portal, tests + whether the sphere overlaps the portal polygon (using `PointInsideCellBsp` on + the sphere center as an approximation — see issue #89 for the retail-faithful + sphere variant). Recurses into neighbour cells up to a depth limit. + +- **`AddAllOutsideCells(sphereCenter, blockId, cache, results)`** — for the + outdoor path: populates a 24m grid of outdoor cell ids around the sphere center + using `TerrainSurface.ComputeOutdoorCellId`. Mirrors retail's `add_all_outside_cells`. + +- **`FindCellList(sp, startCell, cache)`** — top-level driver. Determines whether + `startCell` is an indoor (EnvCell) or outdoor cell and dispatches accordingly. + Returns a list of candidate cell ids. + +`PhysicsEngine.ResolveOutdoorCellId` renamed to `ResolveCellId` (accepts +`sphereRadius` parameter). Body splits on indoor vs outdoor: +- **Indoor:** delegates to `FindCellList` and picks the candidate cell where + `PointInsideCellBsp` returns true for the sphere center. +- **Outdoor:** existing terrain-grid loop (`AddAllOutsideCells`). + +`BSPQuery.PointInsideCellBsp` retyped from `PhysicsBSPNode?` to `CellBSPNode?` +(dead code retype — no behavior change). Phase D's test file deleted. + +### Commit 3 — BuildingPhysics + CheckBuildingTransit + +Outdoor→indoor entry path via building-shell portal graph. New `BuildingPhysics` +class caches per-building portal data (`BldPortalInfo` structs with `PortalId`, +`OtherCellId`, `CellBSP`). `PhysicsDataCache` gains `_buildings` cache keyed by +building entity id. `GameWindow` iterates `lbInfo.Buildings` at landblock load and +populates the cache. + +`CellTransit.CheckBuildingTransit(sphereCenter, sphereRadius, blockId, physicsCache)` +ports retail's outdoor→indoor portal-graph entry: +1. For each building in the landblock's physics cache, test whether the sphere + center is inside the building's shell cell BSP (`PointInsideCellBsp`). +2. If inside, walk the building's portal graph to find the indoor EnvCell that + contains the sphere center. +3. Returns the EnvCell id (or 0 if no match). + +`PhysicsEngine.ResolveCellId`'s outdoor branch hooks `CheckBuildingTransit` after +the terrain-grid loop, so outdoor→indoor transition is detected during normal walking. + +### Commit 4 — Code-review polish + +Five items addressed from reviewer: +1. DRY cell-id derivation via existing `TerrainSurface.ComputeOutdoorCellId` + (removed inline duplicate in `CheckBuildingTransit`). +2. Named `PortalFlags.ExactMatch` enum instead of raw `0x01` literal. +3. Comment clarity on `ExactMatch` reserved field. +4. Doc comment on `CheckBuildingTransit` calling out the sphere-vs-point + divergence from retail's `sphere_intersects_cell` (see issue #89). +5. Rename misleading test method name. + +### Commit 5 — Critical fix: foot-sphere center to ResolveCellId + +**This was the production bug that prevented Phase 2 from working until the last run.** + +`ResolveCellId` was being called with `sp.CheckPos` (the entity's reference point +at feet level, world Z = terrain Z after the +0.02f bump) instead of +`sp.GlobalSphere[0].Origin` (the foot sphere CENTER, approximately +0.48m above terrain). + +Combined with the +0.02f Z-bump applied to cell origins in `PhysicsDataCache`, the +test point landed at cell-local Z = -0.02 m — just below the cell's floor — and +`PointInsideCellBsp` returned false for every cell. CellId never promoted to indoor +cells during normal walking despite `FindCellList` correctly finding the right +candidate cells. + +Passing the foot-sphere center (which sits 0.48m above the floor, well inside any +room cell) made portal-based cell tracking actually work in production. + +Also adds the `[check-bldg]` diagnostic line (logged when `CheckBuildingTransit` +returns a non-zero indoor cell id). + +### Commit 6 — TryFindIndoorWalkablePlane + +**Root cause of the post-Phase-2 falling-stuck bug.** + +When indoor cell-BSP returned OK (no wall collision), the code fell through to +outdoor `SampleTerrainWalkable` + `ValidateWalkable`. Outdoor terrain Z is below +the indoor floor (due to the +0.02f Z-bump), so `ValidateWalkable` computed the +player as floating well above terrain → not walkable → player stuck in the falling +animation when blocked by an indoor wall. + +New `TryFindIndoorWalkablePlane(worldPos, cellPhysics)`: finds the floor polygon +directly under the player's world position by testing `worldPos` against each +physics polygon's plane normal (upward-facing = floor) and building a `ContactPlane` +from it. Called from the indoor branch of `ResolveWithTransition` before the outdoor +terrain fallback. Returns true when a floor poly is found; the resolver uses the +synthesized plane for walkability. + +--- + +## Issue status after Phase 2 + +| Issue | Status | Notes | +|---|---|---| +| #84 Blocked by air indoors | **FULLY CLOSED** | Spawn-in-building variant: Phase D (Cluster A). Wall-block-from-inside + falling-stuck variants: Phase 2 commits 2, 5, 6. | +| #85 Pass through walls outside→in | **CLOSED** | `CheckBuildingTransit` + portal traversal. CellId promotes to indoor on outdoor→indoor entry. | +| #86 Click selection penetrates walls | CLOSED (Phase 1) | `WorldPicker.Pick` + `CellBspRayOccluder`. | +| #87 Indoor portal-based cell tracking | **CLOSED** | `CellTransit.FindCellList` + `FindTransitCellsSphere` + `AddAllOutsideCells`. Portal-graph traversal replaces AABB containment. | +| #88 Indoor static objects vibrate | OPEN (new) | Pre-existing visual jitter on bookshelves/furnaces. Filed 2026-05-19. Medium severity. | +| #89 Port BSPQuery.SphereIntersectsCellBsp | OPEN (new) | `CheckBuildingTransit` uses `PointInsideCellBsp` (radius-less approximation) instead of retail's `sphere_intersects_cell`. Filed 2026-05-19. Low severity. | + +--- + +## Probe evidence — log file findings + +### `launch-phase2-verify3.log` + +First run that showed indoor cell-transits firing. `[cell-transit]` output +confirmed the portal traversal was finding indoor cells. `[indoor-bsp]` probe +fired consistently during indoor walking (not just during mid-jump frames as in +Cluster A). This log is the first evidence that `CellTransit.FindCellList` was +working correctly for room interiors, though outdoor→indoor entry was not yet +exercised. + +### `launch-phase2-verify4.log` + +Multi-room navigation run. `[cell-transit]` log shows +`0xA9B40145 → 0x143 → 0x144 → 0x13F` chains as the player walked between +rooms in the Holtburg cottage via doorways. Confirmed the `FindTransitCellsSphere` +recursive portal walk was promoting CellId correctly through threshold cells. +Walls blocked from inside in all rooms tested. + +### `launch-phase2-verify5.log` + +Walkable bug evidence run. After the outdoor→indoor transition was wired +(`CheckBuildingTransit`), the player could walk into the cottage from outside, +but colliding with an indoor wall produced a falling-stuck state (the `[indoor-bsp]` +probe fired for the wall collision, but `ValidateWalkable` returned false because +it was sampling outdoor terrain Z). This log captured the falling-stuck symptom +and the `SampleTerrainWalkable` fallthrough trace, motivating commit 6. + +### `launch-phase2-verify6.log` + +Post-walkable-fix verification run. After `TryFindIndoorWalkablePlane` was added: +- Outdoor→indoor entry works (player walks through doorway, CellId promotes). +- Indoor wall collision works (walls block, player doesn't pass through). +- Walking back outdoors through the door works (CellId demotes to outdoor cell). +- No falling-stuck state observed. User confirmed all three behaviors. + +--- + +## Diagnostic infrastructure remaining in place + +All four probes stay committed and wired. They serve as production diagnostics +and as debugging aids for follow-up issues: + +- **`ACDREAM_PROBE_INDOOR_BSP=1`** / DebugPanel "Indoor BSP probe": logs one + `[indoor-bsp]` line each time `FindEnvCollisions` takes the indoor-cell branch. + After Phase 2, this fires consistently whenever the player is indoors. Useful + for confirming the indoor-BSP path is active. + +- **`ACDREAM_PROBE_CELL_CACHE=1`** / DebugPanel "Cell cache probe": dumps all + cached EnvCell physics data (poly counts, BSP bounding sphere, AABB, unmatched + ID count, portal count). Useful for verifying cell struct loads and portal + connectivity. + +- **`ACDREAM_PROBE_CELL=1`** (existing L.2a slice 1): one `[cell-transit]` line + per `PlayerMovementController.CellId` change (old → new cell, world position, + reason tag). Essential for tracing indoor promotion/demotion sequences. + +- **`[check-bldg]`** (commit 5): logged by `ResolveCellId` when + `CheckBuildingTransit` returns a non-zero indoor cell id. Fires once per + outdoor→indoor transition detection. + +All gated behind `PhysicsDiagnostics` static class (existing pattern from L.2a). + +--- + +## Visual verification outcomes + +**2026-05-19, user testing live against local ACE at Holtburg.** + +| Scenario | Result | +|---|---| +| Walk into cottage wall from inside | Blocked ✓ | +| Walk between rooms via doorway | CellId transitions logged, multi-room navigation works ✓ | +| Walk from outside into cottage through door | Outdoor→indoor entry promoted CellId; indoor BSP collision active ✓ | +| Walk back outside through door | CellId demoted to outdoor cell; outdoor physics resumed ✓ | +| No falling-stuck after post-walkable fix | Confirmed ✓ | +| Robust across multiple indoor sessions | Confirmed ✓ | + +--- + +## Known follow-ups + +**#88 — Indoor static objects vibrate (bookshelves, open furnaces).** Pre-existing +visual jitter spotted before Phase 2 shipped. Medium severity. Candidates: repeated +`EntityScriptActivator.OnCreate/OnRemove` near cell boundaries, per-part transform +drift, or particle-emitter offset accumulation. Investigate in a follow-up session. + +**#89 — Port `BSPQuery.SphereIntersectsCellBsp`.** `CellTransit.CheckBuildingTransit` +currently uses `PointInsideCellBsp` (tests sphere CENTER only). Retail's +`CEnvCell::check_building_transit` uses `CCellStruct::sphere_intersects_cell` +(radius-aware, returns Inside/Crossing/Outside). Practical effect: entry fires +~0.48m deeper into the doorway than retail. Low severity — visually acceptable. +The `sphereRadius` parameter is already plumbed through for when this is ported. + +**#80 — Indoor darkness (camera on 2nd floor goes very dark).** Still open. +Not in Phase 2's scope. Lighting / ambient-occlusion issue that predates indoor +rendering Phase 2. + +--- + +## State at handoff + +- **Branch:** `claude/competent-robinson-dec1f4`, 6 commits of Phase 2 work + (plus 7 from Phase 1 / Cluster A on the same branch). +- **Build state:** `dotnet build -c Debug` clean. +- **Tests:** 8 pre-existing failures unchanged (MotionInterpreter / BSPStepUp + baseline). All targeted test projects green. +- **Issues:** #84, #85, #87 CLOSED. #86 CLOSED (Phase 1). #88, #89 OPEN (new). +- **Diagnostic probes:** `[indoor-bsp]`, `[cell-cache]`, `[cell-transit]`, + `[check-bldg]` all active and wired. +- **Next:** M2 critical path (F.2 / F.3 / F.5a / L.1c / L.1b — kill-a-drudge + demo) or other candidates per work-order autonomy in CLAUDE.md.