From 15d304fa2c233e1d232263574d82bbe4a81cd990 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Fri, 10 Apr 2026 22:11:40 -0400 Subject: [PATCH] NEON-28: camera contracts, E6.M2 adjacency, occluder purge --- client/scripts/camera_state.gd | 19 +++++++-- client/scripts/isometric_follow_camera.gd | 39 ++++++++++++++++--- client/test/isometric_follow_camera_test.gd | 22 ++++++++++- .../epics/epic_01_core_player_runtime.md | 4 +- .../E1_M2_IsometricCameraController.md | 35 +++++++++++++++-- .../modules/E6_M2_ConsentAndRiskUxSignals.md | 9 +++++ ...umentation_and_implementation_alignment.md | 2 +- .../modules/module_dependency_register.md | 4 +- docs/plans/NEON-28-implementation-plan.md | 17 +++++--- 9 files changed, 126 insertions(+), 25 deletions(-) diff --git a/client/scripts/camera_state.gd b/client/scripts/camera_state.gd index b71f310..2f2980e 100644 --- a/client/scripts/camera_state.gd +++ b/client/scripts/camera_state.gd @@ -1,12 +1,23 @@ extends RefCounted -## Client-local camera snapshot for consumers (risk UX, UI, debug). Refreshed each frame by -## `isometric_follow_camera.gd`. No `class_name` — see repo Godot headless / CI notes. +## Client-local **per-frame** snapshot from the follow rig (`isometric_follow_camera.gd`). +## The rig creates **one** instance and assigns fields every `_process` after pose is final. +## No `class_name` — see repo Godot headless / CI notes. +## +## **Consumer contract (NEON-28):** Risk UX / HUD may read this object from the rig’s +## `camera_state` getter. **Policy-style data** (occlusion enabled, +## zoom resource identity, pitch, presentation compass) is **not** duplicated here — read the +## rig’s exports (`allow_yaw`, `presentation_yaw_deg`, resources) when needed. Future E6.M2 work +## may promote selected flags into this type; until then, keep a single source on the rig. +## +## **Authority:** Camera pose is client-only; the server must not use it for gameplay checks +## (`docs/decomposition/modules/client_server_authority.md` — E1.M2). ## Orbit yaw (rad) around the follow target **vertical axis**, **relative to** the rig’s fixed ## presentation compass (`presentation_yaw_deg` on `isometric_follow_camera.gd`). -## This is **not** total camera compass—only the optional orbit delta. -## Prototype UX keeps this at **0** (`allow_yaw` false). +## **Not** total camera compass — only the optional orbit delta. Prototype UX keeps this at **0** +## (`allow_yaw` false). If orbit is enabled mid-project, HUD and world-anchored gameplay should +## treat framing as presentation + this delta (see E1.M2 rotation policy). var yaw: float = 0.0 ## Scene path to the follow anchor (typically `CharacterBody3D` / `Node3D`). diff --git a/client/scripts/isometric_follow_camera.gd b/client/scripts/isometric_follow_camera.gd index e548fbd..3760af8 100644 --- a/client/scripts/isometric_follow_camera.gd +++ b/client/scripts/isometric_follow_camera.gd @@ -2,12 +2,14 @@ extends Node3D ## NEON-25: client-local isometric follow; updates [CameraState] each `_process`. ## NEON-26: discrete zoom bands via [member zoom_band_config]; wheel / `camera_zoom_*` actions. +## NEON-28: purge freed occluder keys from [member _occluder_overrides] before ray pass (see +## [method occluder_override_key_is_valid]). ## Pitch / roll are fixed by presentation exports; **orbit yaw** in state stays **0** while ## [member allow_yaw] is false (no rotate input bound). Future orbit: read relative input here, ## add to `_orbit_yaw_rad`, clamp with [member max_yaw_deg]. -## TODO(E9.M1): map throttled product telemetry (for example `camera_zoom_changed`) -## to this signal when schema exists. +## TODO(E9.M1): map throttled product telemetry (for example `camera_zoom_changed`) to +## [signal zoom_band_changed] when schema exists; optional occlusion/perf counters per module doc. signal zoom_band_changed(new_index: int, distance: float) const CameraStateScript := preload("res://scripts/camera_state.gd") @@ -223,6 +225,31 @@ static func occlusion_policy_is_valid(policy: Resource) -> bool: return policy != null and policy.get_script() == OcclusionPolicyScript and policy.is_valid() +## True if [param body] is a live [Node3D] suitable as an [member _occluder_overrides] key. +## Used before occlusion ray work and from tests (NEON-28). +static func occluder_override_key_is_valid(body: Variant) -> bool: + return body is Node3D and is_instance_valid(body) + + +func _restore_occluder_overrides_list(overrides: Array) -> void: + for entry in overrides: + var mi: MeshInstance3D = entry["mesh"] + if is_instance_valid(mi): + mi.set_surface_override_material(entry["surface"], entry["original"]) + + +## Drop invalid keys from [member _occluder_overrides] and restore any surviving meshes. +func _purge_invalid_occluder_override_keys() -> void: + var keys: Array = _occluder_overrides.keys() + for body in keys: + if occluder_override_key_is_valid(body): + continue + var overrides: Variant = _occluder_overrides.get(body) + if overrides != null: + _restore_occluder_overrides_list(overrides) + _occluder_overrides.erase(body) + + ## Cast iterative rays from the smoothed eye to the player focus. ## Bodies in the [member occluder_group] that intersect the ray are faded; ## bodies that leave the path have their materials restored. @@ -231,6 +258,8 @@ func _update_occlusion(focus: Vector3) -> void: _restore_all_occluders() return + _purge_invalid_occluder_override_keys() + var policy = occlusion_policy var space_state := get_world_3d().direct_space_state var newly_blocking: Array[Node3D] = [] @@ -309,14 +338,12 @@ func _apply_occluder_fade(body: Node3D) -> void: func _restore_occluder(body: Node3D) -> void: if not _occluder_overrides.has(body): return - for entry in _occluder_overrides[body]: - var mi: MeshInstance3D = entry["mesh"] - if is_instance_valid(mi): - mi.set_surface_override_material(entry["surface"], entry["original"]) + _restore_occluder_overrides_list(_occluder_overrides[body]) _occluder_overrides.erase(body) ## Restore all currently faded occluders (called on policy disable or node exit). func _restore_all_occluders() -> void: + _purge_invalid_occluder_override_keys() for body in _occluder_overrides.keys(): _restore_occluder(body) diff --git a/client/test/isometric_follow_camera_test.gd b/client/test/isometric_follow_camera_test.gd index 542d935..c630d03 100644 --- a/client/test/isometric_follow_camera_test.gd +++ b/client/test/isometric_follow_camera_test.gd @@ -1,4 +1,4 @@ -# Static helpers for res://scripts/isometric_follow_camera.gd (NEON-25 framing, NEON-27 guard). +# Static helpers for res://scripts/isometric_follow_camera.gd (NEON-25 framing, NEON-27/28 guards). extends GdUnitTestSuite const IsoScript := preload("res://scripts/isometric_follow_camera.gd") @@ -62,3 +62,23 @@ func test_occlusion_policy_is_valid_false_when_disabled() -> void: var p = OcclusionPolicyScript.new() p.enabled = false assert_that(IsoScript.occlusion_policy_is_valid(p)).is_false() + + +func test_occluder_override_key_is_valid_null() -> void: + assert_that(IsoScript.occluder_override_key_is_valid(null)).is_false() + + +func test_occluder_override_key_is_valid_non_node() -> void: + assert_that(IsoScript.occluder_override_key_is_valid(42)).is_false() + + +func test_occluder_override_key_is_valid_live_node3d() -> void: + var n := Node3D.new() + assert_that(IsoScript.occluder_override_key_is_valid(n)).is_true() + n.free() + + +func test_occluder_override_key_is_valid_false_after_free() -> void: + var n := Node3D.new() + n.free() + assert_that(IsoScript.occluder_override_key_is_valid(n)).is_false() diff --git a/docs/decomposition/epics/epic_01_core_player_runtime.md b/docs/decomposition/epics/epic_01_core_player_runtime.md index 5582480..a26939e 100644 --- a/docs/decomposition/epics/epic_01_core_player_runtime.md +++ b/docs/decomposition/epics/epic_01_core_player_runtime.md @@ -58,10 +58,10 @@ Deliver the foundational client runtime: movement, locked isometric camera, worl ### Slice 2 - Locked isometric camera -- Scope: E1.M2 end-to-end: follow-center, zoom bands, occlusion policy, no rotation. +- Scope: E1.M2 end-to-end: follow-center, zoom bands, occlusion policy, **yaw fixed in prototype UX** with **`CameraState.yaw`** and rig seam (`allow_yaw` / `presentation_yaw_deg`) documented for optional mid-project orbit ([E1_M2 — rotation policy](../modules/E1_M2_IsometricCameraController.md#mid-project-rotation-policy-practical-compromise)). - Dependencies: E1.M1 - Acceptance criteria: - - Camera never rotates; zoom stays within configured bands; player remains readable during motion. + - **Orbit not exposed in UX** for players in prototype (`allow_yaw` default off); zoom stays within configured bands; player remains readable during motion; occlusion policy active; `CameraState` contract documented for dependents. - Telemetry hooks: `camera_zoom_changed` (optional throttled), occluder stress markers if needed for perf. ### Slice 3 - Interaction, targeting, and ability input wiring diff --git a/docs/decomposition/modules/E1_M2_IsometricCameraController.md b/docs/decomposition/modules/E1_M2_IsometricCameraController.md index d886726..3b43981 100644 --- a/docs/decomposition/modules/E1_M2_IsometricCameraController.md +++ b/docs/decomposition/modules/E1_M2_IsometricCameraController.md @@ -7,7 +7,7 @@ | **Module ID** | E1.M2 | | **Epic** | [Epic 1 — Core Player Runtime](../epics/epic_01_core_player_runtime.md) | | **Stage target** | Prototype | -| **Status** | In progress — follow + `CameraState` + **discrete zoom bands** + **occlusion policy** shipped ([NEON-25](https://neon-sprawl.atlassian.net/browse/NEON-25)–[NEON-27](https://neon-sprawl.atlassian.net/browse/NEON-27)); hardening open ([NEON-28](https://neon-sprawl.atlassian.net/browse/NEON-28)); see [dependency register](module_dependency_register.md) | +| **Status** | **Ready** (prototype slice) — follow + `CameraState` + **discrete zoom bands** + **occlusion** + pick-through ([NEON-25](https://neon-sprawl.atlassian.net/browse/NEON-25)–[NEON-27](https://neon-sprawl.atlassian.net/browse/NEON-27), [NEON-30](https://neon-sprawl.atlassian.net/browse/NEON-30)); contracts + hardening ([NEON-28](https://neon-sprawl.atlassian.net/browse/NEON-28)); see [dependency register](module_dependency_register.md) | | **Jira** | Feature [NEON-10](https://neon-sprawl.atlassian.net/browse/NEON-10); prototype backlog stories under parent [NEON-1](https://neon-sprawl.atlassian.net/browse/NEON-1) — [Jira backlog](#jira-backlog) | ## Purpose @@ -39,10 +39,37 @@ Delivers an isometric follow camera that keeps the player readable during motion | Contract | Role | |----------|------| -| `CameraState` | Current follow target, zoom level, **yaw** (default `0`; optional future orbit), and policy flags for consumers (e.g. risk UX). | +| `CameraState` | Per-frame snapshot: follow path, effective distance, zoom band index, focus, **yaw** (orbit delta; prototype `0`). **Policy / presentation exports are not duplicated** in this type for the prototype — read the rig when needed ([Consumer contract](#consumer-contract-neon-28)). | | `ZoomBandConfig` | Data-driven min/max or discrete zoom steps. | | `OcclusionPolicy` | Rules for fading, dithering, or offset when geometry blocks the view. | +## Consumer contract (NEON-28) + +Single place for dependents (e.g. **E6.M2**): what is on `CameraState` vs what stays on the rig. **Authority:** [E1.M2 — client vs server](client_server_authority.md#e1m2-isometriccameracontroller). + +### `CameraState` fields (`client/scripts/camera_state.gd`) + +Refreshed **every `_process`** on `IsometricFollowCamera` after pose is final. Same object each frame (rig-owned). + +| Field | Meaning | +|--------|---------| +| `yaw` | Orbit delta (rad) around the follow target **relative to** `presentation_yaw_deg` — **not** full compass. **0** while `allow_yaw` is false (prototype UX). | +| `follow_target_path` | `NodePath` to the follow anchor (e.g. player). | +| `distance` | Effective follow distance this tick (active band or fallback). | +| `zoom_band_index` | Active discrete band index, or **0** when zoom config is invalid / unused. | +| `focus_world` | World look-at focus last tick. | + +### Rig-only reads (`client/scripts/isometric_follow_camera.gd`, `World/IsometricFollowCamera` in `main.tscn`) + +| Export / node | Meaning | +|----------------|---------| +| `allow_yaw`, `max_yaw_deg` | Orbit seam (no rotate input bound in prototype). | +| `presentation_yaw_deg`, `pitch_elevation_deg`, `follow_distance`, `focus_vertical_offset` | Fixed isometric presentation (pitch/roll not exposed as free axes). | +| `zoom_band_config`, `occlusion_policy` | Resources driving bands and occlusion. | +| Child `Camera3D` | Active render camera (`ground_pick` uses viewport / `fallback_camera` from `main.gd`). | + +**Telemetry:** Throttled `camera_zoom_changed` and occlusion/perf hooks are **TODO(E9.M1)** until an event schema exists (`zoom_band_changed` signal on the rig today). + ## Module dependencies - **E1.M1** — InputAndMovementRuntime: camera follows the player anchor derived from movement/position. @@ -55,7 +82,7 @@ Delivers an isometric follow camera that keeps the player readable during motion See Epic 1 **Slice 2 — Locked isometric camera**: follow-center, zoom bands, occlusion policy, **yaw fixed for players in prototype** (implementation still models yaw per [mid-project rotation policy](#mid-project-rotation-policy-practical-compromise)); optional telemetry such as throttled `camera_zoom_changed` and perf stress markers for occluders. -## Implementation snapshot (NEON-25 + NEON-26 + NEON-27 + NEON-30, 2026-04-08 / 2026-04-09) +## Implementation snapshot (NEON-25 + NEON-26 + NEON-27 + NEON-30 + NEON-28, 2026-04-08 / 2026-04-10) - **Godot:** `client/scripts/isometric_follow_camera.gd` on **`World/IsometricFollowCamera`**; child **`Camera3D`** (current). **`scripts/camera_state.gd`** — `RefCounted` tick snapshot (`yaw` = orbit component, **0** while **`allow_yaw`** is false; **`distance`** = effective follow distance; **`zoom_band_index`**; **`focus_world`**, **`follow_target_path`**). - **Seam:** **`allow_yaw`** / **`max_yaw_deg`** on the rig; orbit would adjust **`_orbit_yaw_rad`** (no input bound yet). **Presentation** compass uses **`presentation_yaw_deg`** separately from orbit **`yaw`** in state. @@ -66,6 +93,8 @@ See Epic 1 **Slice 2 — Locked isometric camera**: follow-center, zoom bands, o - **Click-through input (NEON-30):** `scripts/ground_pick.gd` unconditionally skips bodies in the `"occluder"` group during click-to-move ground-pick raycasts. When a cast hits an occluder the ray origin advances `OCCLUDER_PICK_THROUGH` meters past the hit point and continues, independent of whether `OcclusionPolicy` is currently fading that body. This keeps the `"occluder"` group as the sole tagging convention shared between the camera occlusion system and ground-pick input — no dedicated collision layer is added for this case. +- **Integration hardening (NEON-28):** [Consumer contract](#consumer-contract-neon-28) + `camera_state.gd` header; **E6.M2** adjacency notes ([E6_M2_ConsentAndRiskUxSignals.md](E6_M2_ConsentAndRiskUxSignals.md)); invalid / freed occluder bodies are **purged** from `_occluder_overrides` before each occlusion ray pass and before full restore (`occluder_override_key_is_valid`, tests in `isometric_follow_camera_test.gd`). + ## Jira backlog Parent epic: [NEON-1 — Epic 1 — Core Player Runtime](https://neon-sprawl.atlassian.net/browse/NEON-1). diff --git a/docs/decomposition/modules/E6_M2_ConsentAndRiskUxSignals.md b/docs/decomposition/modules/E6_M2_ConsentAndRiskUxSignals.md index db18d13..c048905 100644 --- a/docs/decomposition/modules/E6_M2_ConsentAndRiskUxSignals.md +++ b/docs/decomposition/modules/E6_M2_ConsentAndRiskUxSignals.md @@ -13,6 +13,15 @@ Entry warnings, in-zone risk UI, and clear PvP state communication using eligibility from [E6.M1](E6_M1_PvPEligibilityAndFlagState.md) and camera/readability context from [E1.M2](E1_M2_IsometricCameraController.md). +## E1.M2 camera adjacency (NEON-28) + +When implementing zone-risk HUD and related **client** presentation, treat [E1.M2](E1_M2_IsometricCameraController.md) as the framing source: + +- **May read** the follow rig’s **`camera_state`** snapshot (`client/scripts/camera_state.gd`) — field meanings and refresh cadence: [Consumer contract](E1_M2_IsometricCameraController.md#consumer-contract-neon-28). +- **Not duplicated in `CameraState` today:** occlusion on/off, bound zoom/occlusion resources, `presentation_yaw_deg`, `pitch_elevation_deg` — read **`IsometricFollowCamera`** exports (`client/scripts/isometric_follow_camera.gd`) when needed. +- **If `CameraState.yaw` becomes non-zero** (orbit enabled mid-project per E1.M2 rotation policy), screen-space layouts that assumed a fixed diagonal may need a compass / telegraph pass; keep **gameplay-adjacent** semantics **world-anchored** where possible. +- **Authority:** [Client vs server — E1.M2](client_server_authority.md#e1m2-isometriccameracontroller) — the server must **not** use client-reported camera pose for gameplay checks (targeting, LOS, zone rules). + ## Responsibilities - Show `RiskPrompt` on low-sec entry; drive `ZoneRiskState` / `PvPIndicatorState` in HUD. diff --git a/docs/decomposition/modules/documentation_and_implementation_alignment.md b/docs/decomposition/modules/documentation_and_implementation_alignment.md index dbb0ceb..4c27936 100644 --- a/docs/decomposition/modules/documentation_and_implementation_alignment.md +++ b/docs/decomposition/modules/documentation_and_implementation_alignment.md @@ -47,7 +47,7 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not | Module | Register status | Snapshot | Plans / pointers | |--------|-----------------|----------|-------------------| | E1.M1 | Ready | Prototype milestone **Done** ([NEON-9](https://neon-sprawl.atlassian.net/browse/NEON-9)). Authoritative `PositionState` + **MoveCommand** over HTTP (JSON v1 snap); **in-memory** store by default, **Postgres** when configured ([NEON-5](../../plans/NEON-5-implementation-plan.md)); shared **NpgsqlDataSource** disposed on host shutdown ([NEON-15](../../plans/NEON-15-implementation-plan.md)); Godot sync + path-follow ([NEON-4](../../plans/NEON-4-implementation-plan.md), [NEON-8](../../plans/NEON-8-implementation-plan.md)); **InteractionRequest** + horizontal range ([NEON-6](../../plans/NEON-6-implementation-plan.md)). Follow-on: prediction/reconciliation, Slice 1 telemetry, Protobuf wire per [contracts.md](contracts.md). | [NEON-3](../../plans/NEON-3-implementation-plan.md), [NEON-4](../../plans/NEON-4-implementation-plan.md), [NEON-5](../../plans/NEON-5-implementation-plan.md), [NEON-6](../../plans/NEON-6-implementation-plan.md), [NEON-7](../../plans/NEON-7-implementation-plan.md), [NEON-8](../../plans/NEON-8-implementation-plan.md), [NEON-15](../../plans/NEON-15-implementation-plan.md); `server/NeonSprawl.Server/Game/PositionState/`, `Game/Interaction/`; [server README](../../../server/README.md) | -| E1.M2 | In progress | **Slice 2 prototype stories shipped:** fixed-yaw isometric follow + `CameraState` seam ([NEON-25](https://neon-sprawl.atlassian.net/browse/NEON-25)); discrete zoom bands via `ZoomBandConfig` resource ([NEON-26](https://neon-sprawl.atlassian.net/browse/NEON-26)); `OcclusionPolicy` RayCast-based material fade with `"occluder"` group tagging ([NEON-27](https://neon-sprawl.atlassian.net/browse/NEON-27)). Open: integration hardening + dependent contract notes ([NEON-28](https://neon-sprawl.atlassian.net/browse/NEON-28)). Client-local; no server use of camera pose. | [NEON-25](../../plans/NEON-25-implementation-plan.md), [NEON-26](../../plans/NEON-26-implementation-plan.md), [NEON-27](../../plans/NEON-27-implementation-plan.md); `client/scripts/isometric_follow_camera.gd`, `camera_state.gd`, `zoom_band_config.gd`, `occlusion_policy.gd`; [E1_M2_IsometricCameraController.md](E1_M2_IsometricCameraController.md) | +| E1.M2 | Ready | **Slice 2 prototype slice closed:** follow + `CameraState` ([NEON-25](https://neon-sprawl.atlassian.net/browse/NEON-25)); zoom bands ([NEON-26](https://neon-sprawl.atlassian.net/browse/NEON-26)); occlusion ([NEON-27](https://neon-sprawl.atlassian.net/browse/NEON-27)); occluder pick-through ([NEON-30](https://neon-sprawl.atlassian.net/browse/NEON-30)); contracts + hardening ([NEON-28](https://neon-sprawl.atlassian.net/browse/NEON-28)). Client-local; no server use of camera pose. | [NEON-25](../../plans/NEON-25-implementation-plan.md), [NEON-26](../../plans/NEON-26-implementation-plan.md), [NEON-27](../../plans/NEON-27-implementation-plan.md), [NEON-28](../../plans/NEON-28-implementation-plan.md), [NEON-30](../../plans/NEON-30-implementation-plan.md); `client/scripts/isometric_follow_camera.gd`, `camera_state.gd`, `zoom_band_config.gd`, `occlusion_policy.gd`, `ground_pick.gd`; [E1_M2_IsometricCameraController.md](E1_M2_IsometricCameraController.md) | --- diff --git a/docs/decomposition/modules/module_dependency_register.md b/docs/decomposition/modules/module_dependency_register.md index c52f230..dac8013 100644 --- a/docs/decomposition/modules/module_dependency_register.md +++ b/docs/decomposition/modules/module_dependency_register.md @@ -13,11 +13,11 @@ Fleshed-out scope, contracts, and integration notes live in **per-module documen | Module ID | Module Name | Depends On | Contract Needed | Phase Required | Status | |---|---|---|---|---|---| | E1.M1 | InputAndMovementRuntime | None | MoveCommand, PositionState, InteractionRequest | Prototype | Ready | -| E1.M2 | IsometricCameraController | E1.M1 | CameraState, ZoomBandConfig, OcclusionPolicy | Prototype | In progress | +| E1.M2 | IsometricCameraController | E1.M1 | CameraState, ZoomBandConfig, OcclusionPolicy | Prototype | Ready | | E1.M3 | InteractionAndTargetingLayer | E1.M1 | TargetState, InteractableDescriptor, SelectionEvent | Prototype | Planned | | E1.M4 | AbilityInputScaffold | E1.M3, E5.M1 | AbilityCastRequest, HotbarLoadout, CooldownSnapshot | Prototype | Planned | -**E1.M2 note:** Client follow rig + per-tick `CameraState` + **discrete zoom bands** (`ZoomBandConfig`, [NEON-26](https://neon-sprawl.atlassian.net/browse/NEON-26)) + **`OcclusionPolicy`** RayCast fade ([NEON-27](https://neon-sprawl.atlassian.net/browse/NEON-27)) are in repo ([NEON-25](https://neon-sprawl.atlassian.net/browse/NEON-25)–[NEON-27](https://neon-sprawl.atlassian.net/browse/NEON-27)). Integration hardening remains ([NEON-28](https://neon-sprawl.atlassian.net/browse/NEON-28)). +**E1.M2 note:** Prototype slice **Ready**: `client/scripts/isometric_follow_camera.gd` + child `Camera3D` on **`World/IsometricFollowCamera`** in `client/scenes/main.tscn`; per-tick **`CameraState`** (`client/scripts/camera_state.gd`); **`ZoomBandConfig`** ([NEON-26](https://neon-sprawl.atlassian.net/browse/NEON-26)); **`OcclusionPolicy`** ([NEON-27](https://neon-sprawl.atlassian.net/browse/NEON-27)); pick-through **`"occluder"`** convention ([NEON-30](https://neon-sprawl.atlassian.net/browse/NEON-30)); consumer contract + occluder lifecycle hardening ([NEON-28](https://neon-sprawl.atlassian.net/browse/NEON-28)). See [E1_M2_IsometricCameraController.md](E1_M2_IsometricCameraController.md). ### Epic 2 — Skills and Progression Framework diff --git a/docs/plans/NEON-28-implementation-plan.md b/docs/plans/NEON-28-implementation-plan.md index 4fd3482..73bc2b0 100644 --- a/docs/plans/NEON-28-implementation-plan.md +++ b/docs/plans/NEON-28-implementation-plan.md @@ -39,15 +39,15 @@ Jira acceptance criteria (verbatim intent): -- [ ] **Module dependency register / module doc:** E1.M2 **Ready** (or agreed status) with pointers to scripts/scenes. -- [ ] **`CameraState` contract** 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**). +- [x] **Module dependency register / module doc:** E1.M2 **Ready** (or agreed status) with pointers to scripts/scenes. +- [x] **`CameraState` contract** summarized in one place and matches module doc (**includes yaw**). +- [x] **E6.M2 note:** what consumers may read vs undefined; how **yaw** affects camera-adjacent UX if non-zero later. +- [x] **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](#tests)). -- [ ] [epic_01_core_player_runtime.md](../decomposition/epics/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). +- [x] Occluder invalid-key purge covered by tests or documented manual steps (see [Tests](#tests)). +- [x] [epic_01_core_player_runtime.md](../decomposition/epics/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 @@ -109,3 +109,8 @@ Jira acceptance criteria (verbatim intent): - **`CameraState` policy 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.gd` consumer contract header; `isometric_follow_camera.gd` invalid occluder purge + `occluder_override_key_is_valid`; tests in `isometric_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.md` Slice 2.