diff --git a/client/scripts/camera_state.gd b/client/scripts/camera_state.gd index 63e1a38..ff85765 100644 --- a/client/scripts/camera_state.gd +++ b/client/scripts/camera_state.gd @@ -3,8 +3,9 @@ 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. -## Orbit yaw (rad) around the follow target **vertical axis**, relative to [member presentation_yaw_deg] -## on the rig. Prototype UX keeps this at **0** (`allow_yaw` false). Mid-project orbit would drive this. +## 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). 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 231aa41..0f61d87 100644 --- a/client/scripts/isometric_follow_camera.gd +++ b/client/scripts/isometric_follow_camera.gd @@ -32,6 +32,7 @@ var camera_state: var _state = null var _smoothed_eye: Vector3 = Vector3.ZERO var _orbit_yaw_rad: float = 0.0 +var _warned_missing_follow_target: bool = false func _ready() -> void: @@ -57,6 +58,7 @@ func _ready() -> void: _sync_camera_state(focus0) else: _smoothed_eye = global_position + _warn_missing_follow_target_once() func _process(delta: float) -> void: @@ -64,7 +66,10 @@ func _process(delta: float) -> void: return var target: Node3D = _resolve_target() if target == null: + # No eye/state update until the path resolves — avoids chasing a freed or miswired node. + _warn_missing_follow_target_once() return + _warned_missing_follow_target = false if not allow_yaw: _orbit_yaw_rad = 0.0 @@ -91,6 +96,7 @@ func _process(delta: float) -> void: func _sync_camera_state(focus: Vector3) -> void: + # `CameraState.yaw` stores **orbit delta** only; world-fixed diagonal framing is `presentation_yaw_deg`. _state.follow_target_path = follow_target_path _state.distance = follow_distance _state.zoom_band_index = 0 @@ -107,6 +113,15 @@ func _resolve_target() -> Node3D: return null +func _warn_missing_follow_target_once() -> void: + if _warned_missing_follow_target: + return + _warned_missing_follow_target = true + push_warning( + "IsometricFollowCamera: follow_target_path is empty or does not resolve to a Node3D; camera not updating." + ) + + static func desired_eye_world( focus: Vector3, distance: float, pitch_elevation_rad: float, yaw_rad: float ) -> Vector3: diff --git a/docs/decomposition/modules/E1_M2_IsometricCameraController.md b/docs/decomposition/modules/E1_M2_IsometricCameraController.md index 4898cbd..718f4bf 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** | Planned (see [dependency register](module_dependency_register.md)) | +| **Status** | In progress — follow + `CameraState` baseline shipped ([NEON-25](https://neon-sprawl.atlassian.net/browse/NEON-25)); zoom, occlusion, hardening open ([NEON-26](https://neon-sprawl.atlassian.net/browse/NEON-26)–[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 diff --git a/docs/decomposition/modules/module_dependency_register.md b/docs/decomposition/modules/module_dependency_register.md index ca1e6ae..a4335b3 100644 --- a/docs/decomposition/modules/module_dependency_register.md +++ b/docs/decomposition/modules/module_dependency_register.md @@ -13,10 +13,12 @@ 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 | Planned | +| E1.M2 | IsometricCameraController | E1.M1 | CameraState, ZoomBandConfig, OcclusionPolicy | Prototype | In progress | | 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` baseline is in repo ([NEON-25](https://neon-sprawl.atlassian.net/browse/NEON-25)). Discrete zoom bands, occlusion policy, and integration hardening remain ([NEON-26](https://neon-sprawl.atlassian.net/browse/NEON-26)–[NEON-28](https://neon-sprawl.atlassian.net/browse/NEON-28)). + ### Epic 2 — Skills and Progression Framework | Module ID | Module Name | Depends On | Contract Needed | Phase Required | Status |