NEON-28: camera contracts, E6.M2 adjacency, occluder purge
parent
7e674c3710
commit
15d304fa2c
|
|
@ -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`).
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue