neon-sprawl/docs/plans/NEON-28-implementation-plan.md

100 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# NEON-28 — Implementation plan
## Story reference
| Field | Value |
|--------|--------|
| **Key** | NEON-28 |
| **Title** | E1.M2: Camera integration hardening + dependent contract notes (per [decomposition backlog](../decomposition/modules/E1_M2_IsometricCameraController.md#jira-backlog)) |
| **Jira** | [NEON-28](https://neon-sprawl.atlassian.net/browse/NEON-28) |
| **Parent** | [NEON-1 — Epic 1 — Core Player Runtime](https://neon-sprawl.atlassian.net/browse/NEON-1) |
| **Module** | [E1.M2 — IsometricCameraController](../decomposition/modules/E1_M2_IsometricCameraController.md); umbrella [NEON-10](https://neon-sprawl.atlassian.net/browse/NEON-10) |
**Jira sync:** This plan was drafted from repo decomposition and code because the Atlassian MCP plugin is unavailable in this environment. **Reconcile the acceptance checklist below with the issue description in Jira** before merge; add or strike items so the plan matches Jira AC exactly.
**Board:** If the issue is still **To Do**, transition it to **In Progress** when implementation starts (Jira UI or MCP when available).
## Goal, scope, and out-of-scope
**Goal:** Close the E1.M2 “integration hardening” gap after follow + zoom + occlusion + pick-through shipped ([NEON-25](NEON-25-implementation-plan.md)[NEON-27](NEON-27-implementation-plan.md), [NEON-30](NEON-30-implementation-plan.md)): make the camera stack **safe under edge cases** and write **explicit dependent contract notes** so downstream modules (notably **E6.M2 — ConsentAndRiskUxSignals**) know what they may rely on from the client camera layer.
**In scope**
- **Runtime hardening** on `IsometricFollowCamera`: occluder override bookkeeping must not **leak or fault** when occluder bodies are **freed**, removed from the tree, or otherwise invalid while still listed in `_occluder_overrides`; periodic reconciliation should **drop invalid keys** and **restore** mesh overrides where the `MeshInstance3D` is still valid.
- **Contract documentation (dependent notes):**
- **E1.M2** module doc: short **“Consumer contract”** subsection — fields on `CameraState`, refresh cadence, meaning of `yaw` vs presentation yaw on the rig, client-only authority pointer to [client_server_authority.md](../decomposition/modules/client_server_authority.md#e1m2-isometriccameracontroller).
- **E6.M2** module doc: **“E1.M2 camera adjacency”** subsection — what risk UX may read today (`CameraState` / rig path), explicit statement that **server must not** use camera pose for gameplay (link authority doc), and placeholder for future flags if product adds them.
- **Alignment hygiene:** Update [module_dependency_register.md](../decomposition/modules/module_dependency_register.md) **E1.M2 note** and [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) **E1.M2** row to reflect NEON-28 completion (integration hardened + contracts noted).
- **Tests** for hardening paths that stay **headless-friendly** (no full `.tscn` required).
**Out of scope**
- **Yaw orbit** input, new zoom/occlusion techniques, or server-side camera awareness.
- **E9.M1** throttled telemetry (`camera_zoom_changed`, occlusion stress telemetry) — remains TODO on the rig / policy scripts.
- **E6.M2 implementation** (HUD, `RiskPrompt`, etc.) — documentation and stability only here.
## Acceptance criteria checklist
Confirm against Jira; edit this section if Jira differs.
- [ ] **Occluder lifecycle:** Fading an occluder then **queue_free** (or equivalent removal) does not leave permanent material overrides on surviving meshes and does not spam errors; `_occluder_overrides` does not retain invalid object keys.
- [ ] **Consumer contract documented:** E1.M2 documents **`CameraState`** semantics and cadence; E6.M2 documents what it may assume from E1.M2 today and points to authority rules.
- [ ] **Dependency / alignment docs** updated so E1.M2 no longer reads “integration hardening remains” for NEON-28s scope (status/note row reflects shipped hardening + docs).
- [ ] **Automated tests** cover the new invalid-occluder / cleanup behavior (or document why impossible with current harness — prefer tests).
## Technical approach
1. **`isometric_follow_camera.gd`**
- When iterating `_occluder_overrides` (restore pass and/or a small **sanity pass** each frame or only when keys change): if an occluder key **`not is_instance_valid(body)`**, **purge** the entry: for any stored `{mesh, surface, original}` rows, call `set_surface_override_material` only when `is_instance_valid(mi)`; then **erase** the dictionary key. This mirrors “restore then forget” without requiring the body to still exist.
- Optionally run the same invalid-key purge at the **start** of `_update_occlusion` before computing `newly_blocking` to avoid freed keys participating in logic.
- Keep behavior **identical** on the happy path (no extra allocations in the common case if implemented with a single backward pass over keys).
2. **Documentation**
- **E1_M2_IsometricCameraController.md:** Add **NEON-28** bullet under implementation snapshot + **Consumer contract** (table or short list: `focus_world`, `distance`, `zoom_band_index`, `yaw` = orbit only, `follow_target_path`; refreshed every `_process` on the rig; presentation angle lives on rig exports, not duplicated in state unless we explicitly add a field later).
- **E6_M2_ConsentAndRiskUxSignals.md:** Add **E1.M2 camera adjacency** — read `CameraState` from the follow rig when implementing risk UI; do not send camera pose to server for eligibility; link E1.M2 + authority.
- **module_dependency_register.md** / **documentation_and_implementation_alignment.md:** Replace “hardening remains (NEON-28)” with **done** wording once this story merges.
3. **Tests**
- Prefer a **GdUnit** suite that instantiates the camera script in isolation (or minimal scene) if needed: register a fake occluder flow is hard without physics — **fallback:** unit-test a **new static helper** e.g. `prune_invalid_occluder_entries(overrides: Dictionary) -> void` extracted for testability, or test via internal method if extraction is too heavy. **Minimal approach:** extract a small `static func` that takes the override dict and validates keys (document in plan — if we keep logic inline only, use a test scene with `StaticBody3D` + mesh under test project — only if CI can run it headless).
**Decision during implementation:** Prefer **extracted static** `prune_freed_occluder_overrides(overrides: Dictionary) -> void` (or similar) that only uses `is_instance_valid` + restore loop so tests do not need `World3D`. Keeps `isometric_follow_camera.gd` as the single owner of restore semantics by having the static call the same restore logic via a callback — actually simpler: **instance method** `_purge_invalid_occluder_keys() -> void` called from `_update_occlusion` entry; test by subclassing or by scene-free test using `Engine.get_main_loop()` — hardest.
**Pragmatic test path:** Add `isometric_follow_camera.gd` **static** `func occluder_override_key_is_valid(body: Variant) -> bool` returning `body is Node3D and is_instance_valid(body)` — test the static; implementation uses it when filtering keys. Low ceremony, proves the guard exists.
## Decisions
| Topic | Choice | Rationale |
|-------|--------|-----------|
| **API surface for E6.M2** | **Docs only** in NEON-28 | No new `CameraState` fields until a risk UX story needs them; avoids speculative schema. |
| **Invalid occluder handling** | **Purge + restore surviving meshes** | Prevents stuck transparency and dictionary leaks when level chunks or props despawn. |
| **Jira AC** | **Reconcile after fetch** | Decomposition title is the source for this draft; Jira remains canonical. |
## Files to add
**None** — changes land in existing `isometric_follow_camera.gd`, decomposition docs, and `client/test/isometric_follow_camera_test.gd`. If occlusion tests grow large during implementation, add `client/test/isometric_follow_camera_occlusion_hardening_test.gd` (listed as optional under [Tests](#tests)).
## Files to modify
| Path | Rationale |
|------|-----------|
| `client/scripts/isometric_follow_camera.gd` | Invalid occluder key purge + optional small static/helper for testability; keep happy-path behavior unchanged. |
| `docs/decomposition/modules/E1_M2_IsometricCameraController.md` | NEON-28 snapshot + consumer contract for `CameraState` / rig. |
| `docs/decomposition/modules/E6_M2_ConsentAndRiskUxSignals.md` | Dependent notes: E1.M2 adjacency + authority link. |
| `docs/decomposition/modules/module_dependency_register.md` | E1.M2 note: NEON-28 complete vs “remains.” |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E1.M2 row: reflect hardening + contract notes shipped. |
## Tests
| File | Coverage |
|------|----------|
| `client/test/isometric_follow_camera_test.gd` | **Change.** Add tests for any **new static guard** (e.g. invalid/freed `Node3D` key handling) or for **occluder override key validity** helper if introduced. |
| `client/test/isometric_follow_camera_occlusion_hardening_test.gd` | **Optional add.** Only if tests are too large for the existing suite; split occluder-lifecycle cases here. |
**Manual verification:** Run main scene; stand behind `Obstacle` to trigger fade; use debugger or a one-off dev step to free the obstacle node and confirm no errors and materials on other objects look correct. Repeat zoom in/out and click-through (NEON-30) smoke check.
## Open questions / risks
- **Jira AC drift:** Issue text may list extra items (e.g. specific perf or UX gates). Re-read NEON-28 in Jira when the plugin works and **update this plan** before implementation lands.
- **Headless tests vs freed `Object`:** Freed instance behavior in GDScript is subtle; prefer **static** tests on `Variant` / validity helpers plus one manual free-node check in editor.
- None otherwise.