neon-sprawl/docs/plans/NEON-25-implementation-plan.md

7.6 KiB
Raw Blame History

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
Parent NEON-1 — Epic 1 — Core Player Runtime
Module E1.M2 — IsometricCameraController; umbrella 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 per locked defaults below (smooth follow + teleport snap).
  • 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.

Locked defaults (agreed before implementation)

Topic Choice
Follow motion Damped / lerped toward the desired rig position; teleport snap when error exceeds a threshold (e.g. after snap_to_server or huge separation) so the camera never eases across the map.
Framing Match the current static World/Camera3D framing in main.tscn as the starting distance / pitch / FOV; fine-tune in the inspector after follow is wired.
CameraState refresh Rig owns one instance and updates it every frame after computing pose (simplest contract for future readers).
Follow target wiring @export NodePath (resolve to Node3D each tick); tests may assign path or set target via code as needed.
Update phase _process (not _physics_process) for the rig.

Technical approach

  1. Introduce CameraState as a small RefCounted (or equivalent) script under res://scripts/ with explicit fields: follow target path (or resolved node identity in state for debug), 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 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: resolve NodePath follow target, read global_position, compute desired eye position from fixed pitch + effective yaw (forced to 0 when allow_yaw is false), lerp rig toward target+offset; snap rig/camera when separation exceeds a configured threshold.
    • Updates the owned CameraState instance every frame after pose is finalized.
  3. Scene wiring — replace the static World/Camera3D transform in main.tscn with a rig under World (camera as child), @export follow NodePathPlayer, initial exports chosen to match the previous static camera readability.
  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 rigs 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

  • Smoothing tuning: If follow feels floaty or laggy after the locked defaults, adjust lerp speed or snap threshold; document chosen constants in script or README.
  • 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 storys 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.