NEON-25: add implementation plan for isometric follow camera
parent
b07f6980c6
commit
00c2c4520e
|
|
@ -0,0 +1,84 @@
|
|||
# NEON-25 — Implementation plan
|
||||
|
||||
## Story reference
|
||||
|
||||
| Field | Value |
|
||||
|--------|--------|
|
||||
| **Key** | NEON-25 |
|
||||
| **Title** | E1.M2: Isometric follow camera (fixed yaw prototype; CameraState seam) |
|
||||
| **Jira** | [NEON-25](https://neon-sprawl.atlassian.net/browse/NEON-25) |
|
||||
| **Parent** | [NEON-1 — Epic 1 — Core Player Runtime](https://neon-sprawl.atlassian.net/browse/NEON-1) |
|
||||
| **Module** | [E1.M2 — IsometricCameraController](../decomposition/modules/E1_M2_IsometricCameraController.md); umbrella [NEON-10](https://neon-sprawl.atlassian.net/browse/NEON-10) |
|
||||
|
||||
## Goal, scope, and out-of-scope
|
||||
|
||||
**Goal:** Client-local isometric follow camera that tracks the E1.M1 player anchor with fixed framing for prototype play, while **modeling yaw in state** for future consumers and optional orbit.
|
||||
|
||||
**In scope**
|
||||
|
||||
- Godot: `Camera3D` (on a small rig) for isometric presentation; follow the player / follow target each frame (**smooth follow** via lerp/damped tracking unless profiling suggests snap — document the chosen behavior in code/README).
|
||||
- **Player-facing controls:** no camera rotate / orbit; **yaw stays at default** (`0` rad) for normal play.
|
||||
- **`CameraState`** (dedicated script type): follow target reference, **yaw** (always `0` in prototype UX), default distance or zoom index (**single band** / scalar distance until NEON-26).
|
||||
- **Seam:** `@export` or config fields (e.g. `allow_yaw`, `max_yaw_deg`) default off; short comment where orbit input would apply.
|
||||
- **Semantics:** movement and picking remain **world-anchored** (existing `ground_pick` + nav); camera does not redefine aim axes.
|
||||
- Authority: camera pose stays **client-only** (see `docs/decomposition/modules/client_server_authority.md`).
|
||||
|
||||
**Out of scope**
|
||||
|
||||
- Binding rotate input or shipping orbit UX.
|
||||
- Multiple zoom bands (NEON-26).
|
||||
- Occlusion policy (NEON-27).
|
||||
- Server use of camera pose.
|
||||
|
||||
## Acceptance criteria checklist
|
||||
|
||||
- [ ] While the player moves via E1.M1, the avatar stays **readably framed** without manual camera control.
|
||||
- [ ] **Pitch/roll** do not change from user input; **yaw** remains at configured default (`0`) for prototype UX — document how enforced.
|
||||
- [ ] `CameraState` exposes yaw (even when always zero) so dependents do not assume yaw is impossible.
|
||||
- [ ] **Seam** for optional yaw (config/flags) documented in code or module implementation snapshot.
|
||||
- [ ] Short note in client or `E1_M2_IsometricCameraController.md` implementation snapshot when merged.
|
||||
|
||||
## Technical approach
|
||||
|
||||
1. **Introduce `CameraState`** as a small **RefCounted** (or equivalent) script under `res://scripts/` with explicit fields: follow target (`Node3D` reference or `NodePath` resolved each frame), **yaw** (`float`, default `0`), **distance** or zoom index placeholder for NEON-26, and room for future flags. Avoid `class_name` so headless CI matches [godot-client-script-organization](../../.cursor/rules/godot-client-script-organization.md) guidance; tests can `preload` the script.
|
||||
2. **Isometric rig** — add a **`Node3D`** (e.g. `IsometricFollowCamera` or `CameraRig`) with script that:
|
||||
- Owns or references the active **`Camera3D`** (`current = true`).
|
||||
- Each `_process` (or `_physics_process` if we want lockstep with player; default `_process` for smooth view): read player **`global_position`**, compute desired eye position from **fixed pitch** + **effective yaw** (forced to `0` when `allow_yaw` is false), optionally **lerp** rig origin toward target+offset for stability.
|
||||
- Updates a **`CameraState`** instance each frame (or on change) for future consumers.
|
||||
3. **Scene wiring** — replace the static **`World/Camera3D`** transform in `main.tscn` with a rig under `World` (camera as child), **`@export`** follow target = `Player`, distances matching current prototype readability (~existing diagonal framing).
|
||||
4. **`main.gd`** — keep thin: obtain rig/camera reference for **`ground_pick.fallback_camera`** (unchanged contract); no camera math in `main.gd`.
|
||||
5. **Yaw seam** — when `allow_yaw` is false: ignore any future input hook and set state yaw to `0`; when true (dev-only): clamp by `max_yaw_deg` and apply to basis (implementation stub can leave input unbound).
|
||||
6. **Docs** — add a short **Implementation snapshot** subsection to `E1_M2_IsometricCameraController.md` after merge (or README pointer + module snapshot — satisfy AC).
|
||||
|
||||
## Files to add
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `client/scripts/camera_state.gd` | RefCounted (or similar) holding follow target ref/path, **yaw**, distance/zoom index placeholder, future flags; no `class_name`. |
|
||||
| `client/scripts/isometric_follow_camera.gd` | `Node3D` rig: positions/orients `Camera3D`, updates `CameraState`, exports distance/pitch/yaw seam. |
|
||||
|
||||
Godot may add companion `.uid` files when scripts are first touched in the editor; commit them if the toolchain creates them.
|
||||
|
||||
## Files to modify
|
||||
|
||||
| Path | Rationale |
|
||||
|------|-----------|
|
||||
| `client/scenes/main.tscn` | Replace fixed `Camera3D` transform with rig + follow target wiring; keep `current` camera and layers consistent with picking. |
|
||||
| `client/scripts/main.gd` | Point `_camera` (and `ground_pick.fallback_camera`) at the rig’s **`Camera3D`**; minimal diff. |
|
||||
| `client/README.md` | One short paragraph: follow camera behavior, fixed yaw prototype, where to tune distance/pitch. |
|
||||
| `docs/decomposition/modules/E1_M2_IsometricCameraController.md` | **Implementation snapshot** (date + NEON-25): rig script names, yaw seam, single-band distance until NEON-26. |
|
||||
|
||||
## Tests
|
||||
|
||||
| File | What to cover |
|
||||
|------|----------------|
|
||||
| `client/test/camera_state_test.gd` | **New** — default **yaw** is `0`; fields survive assign/read; optional: follow target path assignment (if exposed). |
|
||||
| `client/test/isometric_follow_camera_test.gd` | **New** — if pure helpers are extracted (e.g. static offset from pitch/distance/yaw), unit-test them; otherwise minimal suite: instantiate rig + dummy `Node3D` target, advance one frame, assert camera global position moves toward expected quadrant (tolerance-based). If full follow is too scene-heavy, document **manual** verification as primary and keep **camera_state** tests as the automated baseline. |
|
||||
|
||||
**Manual (required):** Run main scene with server per README: click-move across floor and **NEON-7** bumps; confirm avatar stays framed, no user-driven rotation, ground pick still hits walkable surfaces from the moving camera.
|
||||
|
||||
## Open questions / risks
|
||||
|
||||
- **Smooth vs snap:** Default to **damped follow**; if lag is noticeable at 120 Hz physics, reduce smoothing or align update phase — document final choice.
|
||||
- **`player.gd` `_snap_capsule_upright`:** Identity basis today; camera yaw `0` matches world-aligned capsule. Future facing yaw must stay consistent with E1.M2 policy (not this story’s implementation unless trivial).
|
||||
- **gdUnit rig tests:** May require small refactor (static math helpers) for stable headless asserts; acceptable follow-up within the same story if the first PR ships manual AC + `camera_state` tests.
|
||||
Loading…
Reference in New Issue