docs: UCG W2 (one membership) spec + plan
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
07e68e0aff
commit
83c452b87f
2 changed files with 333 additions and 0 deletions
196
docs/superpowers/plans/2026-06-02-unified-cell-graph-stage2.md
Normal file
196
docs/superpowers/plans/2026-06-02-unified-cell-graph-stage2.md
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
# Unified Cell Graph — Stage 2 (W2: One Membership) Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use `- [ ]`.
|
||||
|
||||
**Goal:** Make physics's `ResolveCellId` the single cell-membership answer (`CellGraph.CurrCell`), have render read it, and add retail `find_cell_list` stab-list doorway hysteresis — collapsing the dual render/physics resolvers.
|
||||
|
||||
**Architecture:** Spec `docs/superpowers/specs/2026-06-02-unified-cell-graph-stage2-design.md`. W1's `CellGraph`/`ObjCell`/`EnvCell`/`LandCell` are in place. Task 1 is additive (write-only, zero-behavior-change). Tasks 2–3 are behavior-changing → **visual verification at the Holtburg cottage is the acceptance gate** (do not mark W2 done without it).
|
||||
|
||||
**Tech Stack:** C# .NET 10, xUnit. Branch `claude/thirsty-goldberg-51bb9b` (unpushed — do NOT push). Every commit ends with `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
- Modify `src/AcDream.Core/Physics/PhysicsEngine.cs` — Task 1 (write `CurrCell`) + Task 3 (hysteresis).
|
||||
- Modify `src/AcDream.App/Rendering/CellVisibility.cs` + `src/AcDream.App/Rendering/GameWindow.cs` — Task 2 (render reads `CurrCell`).
|
||||
- Tests: `tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs` (T1, T3); `tests/AcDream.App.Tests/...` (T2, if an App test project pattern fits — else a Core-side test of the root-selection helper).
|
||||
|
||||
---
|
||||
|
||||
## Task 1: `ResolveCellId` writes `CellGraph.CurrCell` (additive, zero-behavior-change)
|
||||
|
||||
**Files:** Modify `src/AcDream.Core/Physics/PhysicsEngine.cs`; Test `tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs`.
|
||||
|
||||
- [ ] **Step 1: Confirm the return sites.** Read `PhysicsEngine.ResolveCellId` (`PhysicsEngine.cs:259-372`). Identify every `return <resolvedId>;` that returns a *resolved* cell (indoor result ~:310/:328; outdoor result ~:367-368) versus the no-match fallback `return fallbackCellId;` (~:371) which must leave `CurrCell` UNCHANGED. Confirm `DataCache` is the `PhysicsDataCache?` property (`:45`) exposing `.CellGraph` (W1).
|
||||
|
||||
- [ ] **Step 2: Write the failing test** at `tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs`:
|
||||
|
||||
```csharp
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World.Cells;
|
||||
using DatReaderWriter.Types;
|
||||
using Xunit;
|
||||
using DatEnvCell = DatReaderWriter.DBObjs.EnvCell;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
public class CellGraphMembershipTests
|
||||
{
|
||||
[Fact]
|
||||
public void ResolveCellId_IndoorResult_WritesCurrCellToTheEnvCell()
|
||||
{
|
||||
var engine = new PhysicsEngine();
|
||||
var cache = new PhysicsDataCache();
|
||||
engine.DataCache = cache;
|
||||
|
||||
// Register an EnvCell in the graph for the id ResolveCellId will return.
|
||||
// Use a minimal synthetic dat cell (no PhysicsBSP -> CacheCellStruct drops it from
|
||||
// _cellStruct but the graph add runs first, per W1 Task 7).
|
||||
var cs = new CellStruct {
|
||||
VertexArray = new VertexArray { Vertices = new Dictionary<ushort, SWVertex>() },
|
||||
Polygons = new Dictionary<ushort, Polygon>(),
|
||||
PhysicsBSP = null,
|
||||
};
|
||||
var dat = new DatEnvCell {
|
||||
Flags = (DatReaderWriter.Enums.EnvCellFlags)0,
|
||||
CellPortals = new List<DatReaderWriter.Types.CellPortal>(),
|
||||
VisibleCells = new List<ushort>(),
|
||||
};
|
||||
cache.CacheCellStruct(0xA9B40174u, dat, cs, Matrix4x4.Identity);
|
||||
|
||||
// With no physics cell data for collision, ResolveCellId on a bare indoor fallback
|
||||
// returns the fallback id (the indoor branch's early-return when no BSP confirms),
|
||||
// which is the indoor cell — assert CurrCell tracks it.
|
||||
engine.ResolveCellId(new Vector3(0,0,0), 0.5f, 0xA9B40174u);
|
||||
|
||||
Assert.NotNull(cache.CellGraph.CurrCell);
|
||||
Assert.Equal(0xA9B40174u, cache.CellGraph.CurrCell!.Id);
|
||||
}
|
||||
}
|
||||
```
|
||||
> Step 1 may show `ResolveCellId` returns the fallback id under these minimal conditions (no loaded landblock/cell-struct). If the exact return differs, adjust the *setup* so the test exercises a resolved-id return and asserts `CurrCell.Id == thatId` — keep the assertion shape (CurrCell tracks the resolved id). If `PhysicsEngine`/`DataCache` can't be constructed this simply, report NEEDS_CONTEXT.
|
||||
|
||||
- [ ] **Step 3: Run, verify FAIL:** `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Debug --filter "FullyQualifiedName~CellGraphMembershipTests"`
|
||||
|
||||
- [ ] **Step 4: Implement.** Add a private helper to `PhysicsEngine`:
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// UCG W2: record the resolved cell as the single membership answer. Additive —
|
||||
/// CurrCell is written here and read by the render root (W2 Task 2). Leaves CurrCell
|
||||
/// unchanged when the id can't be resolved in the graph (stale beats null).
|
||||
/// </summary>
|
||||
private uint SetCurrAndReturn(uint resolvedId)
|
||||
{
|
||||
if (DataCache?.CellGraph is { } cg && cg.GetVisible(resolvedId) is { } cell)
|
||||
cg.CurrCell = cell;
|
||||
return resolvedId;
|
||||
}
|
||||
```
|
||||
Wrap each *resolved-id* return in `ResolveCellId` as `return SetCurrAndReturn(<id>);` (the indoor-result returns + the outdoor-result return). Leave the no-match `return fallbackCellId;` (~:371) as-is (unchanged CurrCell). Do NOT alter any existing logic — only wrap the returns.
|
||||
|
||||
- [ ] **Step 5: Run, verify PASS.** Same filter. Then full Core build green.
|
||||
|
||||
- [ ] **Step 6: Confirm still zero-behavior-change.** `rg "CurrCell" src` shows only the W1 declaration + this write (no reader yet). Commit:
|
||||
```
|
||||
git add src/AcDream.Core/Physics/PhysicsEngine.cs tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs
|
||||
git commit -m "feat(core): UCG W2 Task 1 — ResolveCellId writes CellGraph.CurrCell (additive)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Render reads `CurrCell` (BEHAVIOR-CHANGING → visual gate)
|
||||
|
||||
**Files:** Modify `src/AcDream.App/Rendering/CellVisibility.cs`, `src/AcDream.App/Rendering/GameWindow.cs`.
|
||||
|
||||
- [ ] **Step 1: Confirm the render path.** Read `CellVisibility.ComputeVisibility`/`GetVisibleCells`/`FindCameraCell` (`CellVisibility.cs:327-413,488+`) and the call site `GameWindow.cs:7153-7157,7295`. Confirm: `TryGetCell(uint, out LoadedCell)` exists (`:286`); how `GameWindow` can reach the physics `CellGraph.CurrCell` (via `_physicsEngine.DataCache?.CellGraph` — confirm `_physicsEngine` field + `DataCache` accessibility). Report what you find before editing.
|
||||
|
||||
- [ ] **Step 2: Add `ComputeVisibilityFromRoot`.** In `CellVisibility`, add an overload that runs the existing portal BFS from a supplied root cell, falling back to the position path when the root is null:
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// UCG W2: compute visibility from a supplied root cell (the physics membership answer),
|
||||
/// instead of resolving the root from a position. Falls back to the position-based
|
||||
/// <see cref="ComputeVisibility(Vector3)"/> when <paramref name="root"/> is null (e.g. the
|
||||
/// cell isn't registered with this renderer yet) — so this never regresses below baseline.
|
||||
/// </summary>
|
||||
public VisibilityResult? ComputeVisibilityFromRoot(LoadedCell? root, Vector3 fallbackPos)
|
||||
{
|
||||
if (root is null) return ComputeVisibility(fallbackPos);
|
||||
_lastCameraCell = root; // seed the cache so the BFS roots here
|
||||
LastVisibilityResult = GetVisibleCells(root, fallbackPos); // BFS from root (see Step 3)
|
||||
return LastVisibilityResult;
|
||||
}
|
||||
```
|
||||
- [ ] **Step 3: Refactor `GetVisibleCells` to accept an explicit root.** Extract the BFS body into `GetVisibleCells(LoadedCell root, Vector3 viewerPos)` and have the existing `GetVisibleCells(Vector3)` call `FindCameraCell(pos)` then delegate to it (null root → null result, today's behavior). Keep the BFS/portal-clip logic byte-identical — only the root acquisition moves out.
|
||||
|
||||
- [ ] **Step 4: Wire `GameWindow` to prefer the physics root.** At `GameWindow.cs:7153-7156`, select the root from `CellGraph.CurrCell`:
|
||||
```csharp
|
||||
var visRootPos = (_playerMode && _playerController is not null) ? _playerController.Position : camPos;
|
||||
LoadedCell? physRoot = null;
|
||||
if (_physicsEngine.DataCache?.CellGraph.CurrCell is AcDream.Core.World.Cells.EnvCell e
|
||||
&& _cellVisibility.TryGetCell(e.Id, out var lc))
|
||||
physRoot = lc;
|
||||
var visibility = _cellVisibility.ComputeVisibilityFromRoot(physRoot, visRootPos);
|
||||
```
|
||||
(Confirm the exact `_physicsEngine` accessor in Step 1; adapt if the field name differs.)
|
||||
|
||||
- [ ] **Step 5: Test** the root-selection + fallback (in `AcDream.App.Tests` if that pattern exists; else a focused `CellVisibility` test): `ComputeVisibilityFromRoot(root, pos)` BFS-roots at `root`; `ComputeVisibilityFromRoot(null, pos)` equals `ComputeVisibility(pos)`. Build green; full Core+App build green.
|
||||
|
||||
- [ ] **Step 6: Commit** (behavior-changing — note the visual gate is pending):
|
||||
```
|
||||
git add src/AcDream.App/Rendering/CellVisibility.cs src/AcDream.App/Rendering/GameWindow.cs <test>
|
||||
git commit -m "feat(app): UCG W2 Task 2 — render root from physics CurrCell (FindCameraCell fallback)"
|
||||
```
|
||||
- [ ] **Step 7: STOP for visual verification (W2a).** Launch per CLAUDE.md; spawn in the cellar + walk the cottage; confirm the indoor path engages on spawn (no world-from-below from a null root). Do not proceed to Task 3 until the user confirms W2a.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Stab-list doorway hysteresis (BEHAVIOR-CHANGING → visual gate)
|
||||
|
||||
**Files:** Modify `src/AcDream.Core/Physics/PhysicsEngine.cs`; Test in `CellGraphMembershipTests.cs`.
|
||||
|
||||
- [ ] **Step 1: Confirm the fall-through point + read retail.** In `ResolveCellId`, find where the indoor branch falls through to outdoor (after the `:326-328` sphere-overlap check fails). Read retail `find_cell_list` `do_not_load_cells` prune (`docs/research/named-retail/acclient_2013_pseudo_c.txt:308829-308867`) to finalize the predicate. Confirm `CellGraph.CurrCell` is readable here (it's on `DataCache.CellGraph`) and `EnvCell.StabList` is the landblock-prefixed visible-cell list.
|
||||
|
||||
- [ ] **Step 2: Write the failing test** — doorway hold: a foot-sphere position that `ResolveCellId` would classify outdoor (`outdoorCandidate`), with the previous `CurrCell` = an indoor `EnvCell` whose `StabList` does NOT contain `outdoorCandidate`, AND the sphere still within the prev cell's proximity ⇒ `ResolveCellId` returns the indoor cell id (held). A second case where `StabList` DOES contain the candidate (or the sphere has cleared proximity) ⇒ returns outdoor. Use #98 cottage fixtures (`0xA9B4014x`). (Write concrete arrange/act/assert; finalize the proximity predicate from Step 1.)
|
||||
|
||||
- [ ] **Step 3: Run, verify FAIL.**
|
||||
|
||||
- [ ] **Step 4: Implement the prune.** Before the indoor→outdoor fall-through in `ResolveCellId`:
|
||||
```csharp
|
||||
// UCG W2b — retail find_cell_list do_not_load_cells prune (pseudo_c:308829-308867):
|
||||
// only accept a change to the outdoor candidate if it's reachable from the current
|
||||
// cell's stab list; otherwise hold the indoor cell one more tick. Anti-ping-pong the
|
||||
// #98 saga lacked.
|
||||
if (DataCache?.CellGraph.CurrCell is AcDream.Core.World.Cells.EnvCell prev
|
||||
&& !prev.StabList.Contains(outdoorCandidate)
|
||||
&& /* stillNearPrev: sphere within prev's expanded local AABB */ )
|
||||
{
|
||||
return SetCurrAndReturn(prev.Id);
|
||||
}
|
||||
```
|
||||
Finalize `stillNearPrev` (a containment/proximity test against `prev`'s bounds, expanded by the sphere radius) per Step 1. Keep it additive — it only adds a hold path before the existing fall-through.
|
||||
|
||||
- [ ] **Step 5: Run, verify PASS.** Full Core build + suite (no NEW deterministic failures vs baseline).
|
||||
|
||||
- [ ] **Step 6: Commit:**
|
||||
```
|
||||
git add src/AcDream.Core/Physics/PhysicsEngine.cs tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs
|
||||
git commit -m "feat(core): UCG W2 Task 3 — stab-list doorway hysteresis in ResolveCellId"
|
||||
```
|
||||
- [ ] **Step 7: STOP for visual verification (W2b).** Walk room ↔ cellar ↔ outside at the inn doorway; confirm `[cell-transit]` (ACDREAM_PROBE_CELL=1) stays stable (no ping-pong). User confirms before W2 is marked done.
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Verify + roadmap
|
||||
- [ ] App build 0 errors; full Core suite no NEW deterministic failures vs the static-leak baseline.
|
||||
- [ ] Both visual gates (Task 2 Step 7 + Task 3 Step 7) confirmed by the user.
|
||||
- [ ] Flip W2 to shipped in `docs/plans/2026-04-11-roadmap.md` (the Phase W table). Commit.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
- **Spec coverage:** §3 W2a (Tasks 1–2) + §3 W2b (Task 3) + §5 acceptance (Task 4 visual gates). ✓
|
||||
- **Placeholders:** Tasks 2–3 carry "confirm/finalize" steps (the render path + the hysteresis predicate need confirmation against live code + retail) — these are verify-then-use, with the concrete code supplied. Task 3's `stillNearPrev` predicate is the one genuinely-to-finalize item; flagged explicitly.
|
||||
- **Zero-behavior-change boundary:** Task 1 is additive/write-only (CurrCell read by nobody until Task 2) — provable like a W1 task. Tasks 2–3 are behavior-changing and gated on visual verification.
|
||||
- **Type consistency:** `CellGraph.CurrCell` (W1), `EnvCell.StabList`/`.Id` (W1), `CellVisibility.TryGetCell`/`ComputeVisibility` (existing), `PhysicsEngine.DataCache` (`:45`), `SetCurrAndReturn` (T1) used in T3.
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
# Unified Cell Graph (Phase W) — Stage 2 (W2): One Membership
|
||||
|
||||
**Status:** design approved 2026-06-02. Implementation pending plan.
|
||||
**Branch:** `claude/thirsty-goldberg-51bb9b` (unpushed).
|
||||
**Predecessor:** W1 (ObjCell scaffold, shipped `9cb1571`→`f2663b7`).
|
||||
**Grounding:** the Stage-2 research (in-session) + `docs/research/2026-06-02-render-cell-membership-evidence.md`.
|
||||
**Behavior-changing:** YES (first such stage). Requires visual verification at the Holtburg cottage.
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Make the player's cell membership a **single answer** that both physics and render obey,
|
||||
and give the indoor↔outdoor seam the **retail doorway hysteresis** it has always lacked.
|
||||
Today there are two independent resolvers that disagree (the proven root cause of the
|
||||
indoor "world from below"):
|
||||
- **Physics** — `PhysicsEngine.ResolveCellId` (BSP-based, `SphereIntersectsCellBsp`), feeds `PlayerMovementController.CellId`.
|
||||
- **Render** — `CellVisibility.FindCameraCell` (AABB-based, cache→neighbour→bruteforce→grace→None), feeds `clipRoot`.
|
||||
|
||||
They use the *same* position (player pos, post-flap-fix) but different containment tests
|
||||
and different registries, so they diverge — on spawn-in (physics knows the cell instantly,
|
||||
render fires zero probes) and on within-building flicker.
|
||||
|
||||
W2 makes **physics's `ResolveCellId` the single source**, writes it into
|
||||
`CellGraph.CurrCell`, and has render **read** that instead of re-resolving. Then it ports
|
||||
retail `find_cell_list`'s **stab-list prune** as the anti-ping-pong the #98 saga never had.
|
||||
|
||||
## 2. Current state (grounded, file:line)
|
||||
|
||||
- `PhysicsEngine.ResolveCellId(worldPos, sphereRadius, fallbackCellId)` (`PhysicsEngine.cs:259-372`):
|
||||
indoor branch (`fallbackLow >= 0x0100`) → `CellTransit.FindCellList` → if the resolved
|
||||
cell's `CellBSP.Root` non-null, the **#90 sphere-overlap post-check** (`:326-328`,
|
||||
`SphereIntersectsCellBsp`) keeps it indoor while the foot-sphere still overlaps, else
|
||||
falls through to outdoor; outdoor branch → `ComputeOutdoorCellId` + `CheckBuildingTransit`.
|
||||
Result → `ResolveResult.CellId` → `PlayerMovementController.UpdateCellId(.., "resolver")` (`:1296`).
|
||||
- **The #90 sphere-overlap check (`:326-328`) is the ONLY active stickiness.** The A6.P3
|
||||
slice-3 attempt was reverted (`:271-289` is now just a comment). **`CellTransit` is
|
||||
stateless per call** — no persistent `CellArray` like retail's `Transition.cell_array`,
|
||||
and **no stab-list prune** (the retail anti-ping-pong is entirely absent).
|
||||
- `CellVisibility.FindCameraCell` (`CellVisibility.cs:356-413`): AABB ladder + `_lastCameraCell`
|
||||
+ 3-frame grace. `ComputeVisibility(visRootPos)` (`:327`) wraps `GetVisibleCells` (BFS).
|
||||
Called from `GameWindow.cs:7153-7156` with `visRootPos = player pos`; result → `clipRoot` (`:7295`).
|
||||
- `CellGraph.CurrCell { get; internal set; }` exists (W1) and is inert.
|
||||
|
||||
## 3. Design
|
||||
|
||||
### W2a — unify the membership (Task 1 + Task 2)
|
||||
|
||||
**Task 1 — `ResolveCellId` writes `CurrCell` (additive, zero-behavior-change).**
|
||||
At each `ResolveCellId` return site, set `DataCache.CellGraph.CurrCell = CellGraph.GetVisible(resolvedId)`.
|
||||
Encapsulate in a private `PhysicsEngine` helper. The no-landblock-match fallback (`:371`,
|
||||
returns `fallbackCellId`) leaves `CurrCell` **unchanged** (stale beats null when we don't
|
||||
know where the player is). `CurrCell` is still **read by nobody** after Task 1 — so Task 1
|
||||
is provably zero-behavior-change and lands like a W1 task (unit-tested, no visual gate).
|
||||
|
||||
**Task 2 — render reads `CurrCell` (BEHAVIOR-CHANGING).**
|
||||
`GameWindow`'s visibility path selects the root from physics's answer, with a graceful
|
||||
fallback:
|
||||
```
|
||||
root = (CurrCell is EnvCell e && _cellVisibility.TryGetCell(e.Id, out var lc)) ? lc
|
||||
: FindCameraCell(visRootPos) // fallback: render's own resolution
|
||||
```
|
||||
Implement as a new `CellVisibility.ComputeVisibilityFromRoot(LoadedCell? root, Vector3 visRootPos)`
|
||||
that runs the existing portal BFS from the given root (or falls back to the position path
|
||||
when `root` is null). The BFS / portal traversal is **unchanged** — only the root source
|
||||
changes. The fallback preserves today's behavior whenever physics hasn't registered the
|
||||
cell with the render registry yet (the spawn-in registration-timing gap), so Task 2 can
|
||||
only *improve* on the divergence, never regress below today's baseline.
|
||||
|
||||
This kills the spawn-in + null-root-flicker divergence: whenever physics has the indoor
|
||||
cell AND it's registered with render, render uses the BSP-grounded answer and cannot
|
||||
diverge to a null/AABB-miss root.
|
||||
|
||||
### W2b — doorway hysteresis (Task 3, BEHAVIOR-CHANGING)
|
||||
|
||||
Port retail `find_cell_list`'s stab-list prune into `ResolveCellId`'s indoor→outdoor
|
||||
fall-through. Before falling through to the outdoor branch (after the `:326-328`
|
||||
sphere-overlap check fails), consult the previous `CurrCell`:
|
||||
```
|
||||
// Retail find_cell_list do_not_load_cells prune: a cell change to the outdoor candidate
|
||||
// is only accepted if that candidate is reachable from the current cell's stab list.
|
||||
if (prevCurr is EnvCell pe && !pe.StabList.Contains(outdoorCandidate) && stillNearPrev)
|
||||
return prevCurr.Id; // hold the indoor cell one more tick
|
||||
```
|
||||
`StabList` is already populated on `EnvCell` (W1, from `datCell.VisibleCells`). This is the
|
||||
retail-faithful anti-ping-pong; every prior #98 attempt guessed at cap mechanics and
|
||||
**never had the stab-list prune**. `stillNearPrev` is a small proximity guard (sphere
|
||||
within the prev cell's expanded AABB) so the hold releases once the player has genuinely
|
||||
left. Exact predicate to be finalized in the plan against the retail `:308829-308867` prune.
|
||||
|
||||
## 4. What W2 does NOT do
|
||||
- **Render draw gates / OutsideView clipping** (the full seamless seal) — **W3**.
|
||||
- **Collision** query mechanics (`FindEnvCollisions`, `ShadowObjects`) — **W4**.
|
||||
- **Streaming** / terrain-as-LandCell — **W5**.
|
||||
- W2 changes only *which cell is the membership answer* and *who reads it* + the seam hysteresis.
|
||||
|
||||
## 5. Acceptance (visual — the decisive gate)
|
||||
- **Spawn in the cellar** → indoor render path engages immediately; no "world from below"
|
||||
from a null/AABB-miss root.
|
||||
- **Walk room ↔ cellar ↔ outside** → `[cell-transit]` stays stable at the inn doorway (no
|
||||
ping-pong); render engaged throughout.
|
||||
- **No regression** to outdoor walking or existing collision behavior.
|
||||
|
||||
## 6. Tests
|
||||
- **Task 1:** after `ResolveCellId` resolves an indoor cell (registered in the graph),
|
||||
`CellGraph.CurrCell` is that `EnvCell`; an outdoor resolution sets a `LandCell` (or leaves
|
||||
it unchanged on no-match). Unit test via `PhysicsEngine` + a registered fixture cell.
|
||||
- **Task 2:** `ComputeVisibilityFromRoot(root, pos)` returns the BFS from `root` when given;
|
||||
falls back to the `FindCameraCell(pos)` path when `root == null`. Unit/integration test in
|
||||
`AcDream.App.Tests`.
|
||||
- **Task 3:** doorway-hysteresis unit test — foot-sphere just past the indoor BSP with an
|
||||
outdoor candidate NOT in the prev cell's `StabList` ⇒ `ResolveCellId` returns the indoor
|
||||
cell (held); once the sphere clears `stillNearPrev` ⇒ it releases to outdoor. Use the #98
|
||||
cottage fixtures.
|
||||
|
||||
## 7. Acceptance criteria
|
||||
- `dotnet build` green; new tests green; existing suite no NEW deterministic failures
|
||||
(vs the documented static-leak flaky baseline).
|
||||
- Task 1 verified zero-behavior-change (CurrCell write-only until Task 2).
|
||||
- Tasks 2–3 visual-verified at the cottage (the user's eyes) before W2 is marked done.
|
||||
|
||||
## 8. Risks
|
||||
- **This is the #98 ping-pong area** (~10 prior failed attempts). The stab-list prune is the
|
||||
retail-faithful approach none of them tried, so it's a genuine shot — but the visual gate
|
||||
is decisive. W2a (Tasks 1–2) stands on its own (kills spawn-in/flicker) even if W2b needs
|
||||
iteration.
|
||||
- **Registration-timing gap:** render can only read `CurrCell` for cells in its `_cellLookup`.
|
||||
The Task-2 fallback to `FindCameraCell` keeps behavior at today's baseline when the cell
|
||||
isn't registered yet; fully closing that gap (if it persists) is a W2 follow-up or W3.
|
||||
- **Thread:** `CurrCell` is written from the game thread (`ResolveWithTransition`) and read
|
||||
from the game thread (`OnRender`) — no threading issue while both stay on the main thread.
|
||||
|
||||
## 9. Task decomposition (→ plan)
|
||||
1. `ResolveCellId` writes `CurrCell` (additive, unit-tested, no visual gate).
|
||||
2. Render reads `CurrCell` with `FindCameraCell` fallback (behavior-changing → visual gate).
|
||||
3. Stab-list doorway hysteresis in `ResolveCellId` (behavior-changing → visual gate).
|
||||
4. Verify + visual-verify at the cottage.
|
||||
Loading…
Add table
Add a link
Reference in a new issue