9.8 KiB
NEON-28 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEON-28 |
| Title | E1.M2: Camera integration hardening + dependent contract notes (per decomposition backlog) |
| Jira | NEON-28 |
| Parent | NEON-1 — Epic 1 — Core Player Runtime |
| Module | E1.M2 — IsometricCameraController; umbrella 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-27, NEON-30): 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 theMeshInstance3Dis still valid. - Contract documentation (dependent notes):
- E1.M2 module doc: short “Consumer contract” subsection — fields on
CameraState, refresh cadence, meaning ofyawvs presentation yaw on the rig, client-only authority pointer to client_server_authority.md. - 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.
- E1.M2 module doc: short “Consumer contract” subsection — fields on
- Alignment hygiene: Update module_dependency_register.md E1.M2 note and 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
.tscnrequired).
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_overridesdoes not retain invalid object keys. - Consumer contract documented: E1.M2 documents
CameraStatesemantics 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-28’s 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
-
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 keynot is_instance_valid(body), purge the entry: for any stored{mesh, surface, original}rows, callset_surface_override_materialonly whenis_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_occlusionbefore computingnewly_blockingto 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).
- When iterating
-
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_processon 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
CameraStatefrom 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.
- E1_M2_IsometricCameraController.md: Add NEON-28 bullet under implementation snapshot + Consumer contract (table or short list:
-
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) -> voidextracted for testability, or test via internal method if extraction is too heavy. Minimal approach: extract a smallstatic functhat takes the override dict and validates keys (document in plan — if we keep logic inline only, use a test scene withStaticBody3D+ 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 usesis_instance_valid+ restore loop so tests do not needWorld3D. Keepsisometric_follow_camera.gdas 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() -> voidcalled from_update_occlusionentry; test by subclassing or by scene-free test usingEngine.get_main_loop()— hardest.Pragmatic test path: Add
isometric_follow_camera.gdstaticfunc occluder_override_key_is_valid(body: Variant) -> boolreturningbody is Node3D and is_instance_valid(body)— test the static; implementation uses it when filtering keys. Low ceremony, proves the guard exists. - 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.
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).
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 onVariant/ validity helpers plus one manual free-node check in editor. - None otherwise.