# 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.