10 KiB
NEON-28 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEON-28 |
| Title | E1.M2: Camera integration hardening + dependent contract notes |
| Jira | NEON-28 |
| Parent | NEON-1 — Epic 1 — Core Player Runtime |
| Module | E1.M2 — IsometricCameraController; umbrella NEON-10 |
Jira source: Description and acceptance criteria were pulled from Jira via Atlassian MCP (cloud neon-sprawl, issue NEON-28). This plan is the working implementation breakdown; if Jira edits after this date, re-sync the checklist.
Board: Transitioned To Do → In Progress on kickoff (Atlassian MCP transitionJiraIssue, transition id 2 / name prog).
Goal, scope, and out-of-scope
Goal (from Jira): Close the E1.M2 prototype slice with stable contracts, documentation, and hooks so E6.M2 (and other dependents) can rely on CameraState and presentation assumptions without spelunking implementation.
In scope (from Jira)
- Finalize
CameraState: follow target, yaw (prototype0; document semantics if orbit is enabled mid-project), zoom band / effective framing, policy flags as needed (add fields only when the documented consumer contract requires them; otherwise state explicitly what is not in state and where to read it on the rig). - Implementation snapshot in module doc: Mid-project rotation policy and post-release freeze from
E1_M2_IsometricCameraController.md(prototype fixed yaw for players; optionalallow_yaw/ limits seam; world-anchored gameplay preferred; telegraph readability). Cross-link NEON-10 / module backlog for traceability. - Client-local authority and fixed pitch/roll for isometric presentation — align client_server_authority.md references in E1.M2 / E6.M2 notes.
- Optional telemetry: document hooks for throttled
camera_zoom_changedand occlusion/perf counters when E9.M1 schema exists (no requirement to implement telemetry in this story unless already trivial TODO cleanup). - Dependency register + alignment: E1.M2 Ready (or agreed status) with pointers to scripts/scenes per acceptance criteria.
Additional in-repo hardening (engineering, supports “stable contracts”)
IsometricFollowCameraoccluder lifecycle:_occluder_overridesmust not leak or fault when occluder bodies are freed or invalid; erase stale dict keys without callingset_surface_override_materialon a freed subtree (that errors); live bodies still restore when they leave the ray.
Out of scope (from Jira)
- New gameplay features; E1.M3 / E1.M4 work.
- Committing to yaw orbit in UX without product sign-off (policy allows mid-project enablement only).
Acceptance criteria checklist
Jira acceptance criteria (verbatim intent):
- Module dependency register / module doc: E1.M2 Ready (or agreed status) with pointers to scripts/scenes.
CameraStatecontract summarized in one place and matches module doc (includes yaw).- E6.M2 note: what consumers may read vs undefined; how yaw affects camera-adjacent UX if non-zero later.
- Epic 1 Slice 2 acceptance satisfied: prototype keeps yaw fixed for players, zoom within bands, readable motion, occlusion policy active; aligns with updated module wording (not “camera has no yaw,” but yaw not exposed / remains default in UX).
Supporting engineering checks (not a substitute for Jira AC):
- Occluder invalid-key purge covered by tests or documented manual steps (see Tests).
- epic_01_core_player_runtime.md Slice 2 bullets use the same yaw / presentation language as the module (replace over-simplified “no rotation” if it contradicts the rotation policy).
Technical approach
-
Contract documentation (primary)
camera_state.gd: Expand file header (and/or class-level comments) so it is a concise, accurate summary of fields:yaw= orbit delta vs presentation compass;distance;zoom_band_index;focus_world;follow_target_path; refresh every_physics_processtick on the rig (process_physics_priorityafterPlayer— see Decisions).E1_M2_IsometricCameraController.md: Single Consumer contract subsection: table linking eachCameraStatefield + rig exports (allow_yaw,presentation_yaw_deg, resources). Include mid-project rotation + post-release freeze pointers; link NEON-10, NEON-25–NEON-27, NEON-30 as appropriate.E6_M2_ConsentAndRiskUxSignals.md: E1.M2 camera adjacency — allowed reads (CameraStatefields), undefined (what stays on rig only), yaw non-zero later (presentation / HUD implications in one short paragraph); link authority (server must not use camera pose for gameplay).module_dependency_register.md: Set E1.M2 Status to Ready (or agreed label) and replace “NEON-28 remains” narrative with shipped wording + script/scene pointers.documentation_and_implementation_alignment.md: E1.M2 row matches register + NEON-28 completion.epic_01_core_player_runtime.md: Adjust Slice 2 scope/acceptance lines so they reflect fixed yaw in UX and documented orbit seam, not flat “camera never rotates” if that misleads.
-
CameraState“finalize”- Reconcile code and docs. Add policy flags to
CameraStateonly if the E6.M2 / E1.M2 contract text commits to them; otherwise explicitly document “no policy flags in state yet; read rig exports.”
- Reconcile code and docs. Add policy flags to
-
isometric_follow_camera.gdhardening- At start of
_update_occlusion(and/or restore passes): erase dictionary keys whose body isnot is_instance_valid(do not restore materials on that path — freed occluder’s meshes are gone). Harden_restore_occluder_overrides_listwithis_instance_validon mesh and non-nulloriginalfor the live-body restore path.
- At start of
-
Optional telemetry
- Keep or add TODO(E9.M1) pointers in rig / policy scripts; no new telemetry pipeline unless out of scope exception is agreed.
-
Tests
- Static helper tests for occluder key validity / purge behavior where headless-safe; manual queue_free occluder check in editor for full confidence.
Decisions
| Topic | Choice | Rationale |
|---|---|---|
CameraState policy flags |
Doc-first | Add fields only if Jira/E6.M2 contract text requires a snapshot; avoid speculative API. |
| Invalid occluder keys | Erase only | Freed body ⇒ subtree is gone; restoring overrides hits freed MeshInstance3D / materials. Drop dict entry; live bodies restore when they leave the ray. |
| Epic Slice 2 wording | Align with rotation policy | Satisfies Jira AC on “yaw not exposed / default in UX” vs misleading “no rotation.” |
| Rig tick (jitter follow-up) | _physics_process + process_physics_priority = 1 |
Player default 0 runs before 1; camera reads post–move_and_slide. Avoids _process vs 120 Hz physics jitter (camera was above Player in scene order). |
Files to add
None unless occlusion tests are split out (optional client/test/isometric_follow_camera_occlusion_hardening_test.gd).
Files to modify
| Path | Rationale |
|---|---|
client/scripts/camera_state.gd |
Finalize field documentation; optional policy flags if contract requires. |
client/scripts/isometric_follow_camera.gd |
Occluder invalid-key purge; keep TODO(E9.M1) aligned with Jira optional telemetry note. |
docs/decomposition/modules/E1_M2_IsometricCameraController.md |
Consumer contract, NEON-28 snapshot, NEON-10 / rotation policy cross-links. |
docs/decomposition/modules/E6_M2_ConsentAndRiskUxSignals.md |
E1.M2 adjacency + yaw UX note + authority. |
docs/decomposition/modules/module_dependency_register.md |
E1.M2 Ready + pointers; remove “hardening remains.” |
docs/decomposition/modules/documentation_and_implementation_alignment.md |
E1.M2 row updated. |
docs/decomposition/epics/epic_01_core_player_runtime.md |
Slice 2 wording vs yaw/presentation policy. |
client/test/isometric_follow_camera_test.gd |
Tests for new static guards / occluder key validity if added. |
Tests
| File | Coverage |
|---|---|
client/test/isometric_follow_camera_test.gd |
Change. Guards for occluder override dictionary key validity / purge helper if extracted. |
client/test/camera_state_test.gd |
Change only if new CameraState fields ship — round-trip defaults. |
Manual verification: Main scene — zoom bands, occlusion fade, fixed yaw UX; spot-check Slice 2 behaviors (readable motion, occlusion active).
Freed occluder (end-to-end, editor): Position player so Obstacle is between camera and player (fade active), then queue_free the occluder. Expect no recurring script errors and no stuck transparent materials on other meshes. Obstacle lives under NavigationRegion3D — after removing it, rebake the nav mesh (see main.gd F9 dev path: bake_navigation_mesh(false) + refresh NavigationAgent3D target) or click-to-move can stop until the next full scene reload. Complements static occluder_override_key_is_valid tests, which do not execute the full purge path in CI.
Open questions / risks
CameraStatepolicy flags: Product may later require explicit booleans for risk UX; if E6.M2 text stays non-committal, keep flags out and document rig as source until a follow-on story.- None otherwise.
Shipped (2026-04-10)
camera_state.gdconsumer contract header;isometric_follow_camera.gdinvalid occluder purge +occluder_override_key_is_valid; tests inisometric_follow_camera_test.gd.- Docs:
E1_M2_IsometricCameraController.md(Ready, consumer contract, NEON-28 snapshot),E6_M2_ConsentAndRiskUxSignals.md(E1.M2 adjacency),module_dependency_register.md,documentation_and_implementation_alignment.md,epic_01_core_player_runtime.mdSlice 2. - Follow-up: rig
_physics_process+ priority (see Decisions).