neon-sprawl/docs/plans/NEO-16-implementation-plan.md

7.5 KiB
Raw Blame History

NEO-16 — Implementation plan

Story reference

Field Value
Key NEO-16
Title E1.M2: ZoomBandConfig + discrete zoom input (clamped)
Linear NEO-16
Parent Epic 1 — Core Player Runtime
Module E1.M2 — IsometricCameraController; umbrella E1.M2

Goal, scope, and out-of-scope

Goal: Add data-driven discrete zoom bands so the player can adjust framing within limits; CameraState reports the active band and effective distance while preserving yaw and the NEO-15 orbit seam.

In scope

  • ZoomBandConfigResource (no class_name; match godot-client-script-organization) holding an ordered list of follow distances (meters per band), plus a default band index (clamped to valid range when applied).
  • isometric_follow_camera.gd@export reference to ZoomBandConfig; internal active band index clamped to [0, band_count - 1]; each frame use distance for the active band in desired_eye_world (same pitch / presentation yaw as today).
  • Input: cycle bands in / out (e.g. mouse wheel up/down and/or +/- keys) via InputMap actions in project.godot; handle in _unhandled_input (or equivalent) on the rig so UI can steal focus later if needed.
  • CameraState: set zoom_band_index to the active index and distance to the effective distance used for framing (not the legacy single export alone when bands are active).
  • Designer tuning: one Resource asset (e.g. res://resources/...tres or inline default) editable in the inspector; rig points at it.
  • Optional: stub signal / throttled print / TODO(E9.M1) for camera_zoom_changed telemetry when a schema exists.

Out of scope (per Linear)

  • Smooth analog zoom between bands (unless a one-line lerp is explicitly chosen later — default no).
  • Occlusion policy (NEO-17).
  • Yaw orbit input (NEO-15 seam stays; do not bind rotate).

Acceptance criteria checklist

  • Zoom input only moves between defined bands (clamped; no out-of-range index).
  • At each band, player remains readably framed during motion (manual playtest + distances chosen around current NEO-15 baseline).
  • CameraState reflects active band and effective distance, and still exposes yaw unchanged in meaning.
  • Config lives in one tunable place for designers (ZoomBandConfig resource referenced by the rig).

Technical approach

  1. zoom_band_config.gdextends Resource with e.g. @export var band_distances: PackedFloat32Array (length ≥ 1) and @export var default_band_index: int. Provide band_count, clamp_index(i: int) -> int, distance_at(index: int) -> float (uses clamped index). Validate in _validate_property or getter: if empty at runtime, treat as single band using rig fallback (see below).
  2. isometric_follow_camera.gd — Add @export var zoom_band_config: Resource (no class_name on the config script); at runtime require get_script() == preload("…/zoom_band_config.gd") so headless CI and typed exports stay aligned. Conceptually this is ZoomBandConfig. Keep follow_distance as fallback when config is null, empty, or has nonpositive band distances (preserves NEO-15 scenes/tests). Track _zoom_band_index initialized from config default (clamped). On zoom-in/out actions: adjust index with clamp_index, no op at ends (or wrap — clamp per story). _process: compute effective distance from config + index or fallback export. Replace uses of raw follow_distance in desired_eye_world with that value. _sync_camera_state: pass effective distance into state (zoom_band_index + distance, plus existing yaw / path / focus).
  3. project.godot — Add camera_zoom_in and camera_zoom_out (or one action with axis — prefer two actions for wheel + keys) with mouse wheel and key events as in the Linear issue / product notes.
  4. Scene / resource — Add a default ZoomBandConfig .tres under res://resources/ (or co-located under client/resources/) with ≥ 3 bands bracketing ~25.709 m so readability can be checked; assign on IsometricFollowCamera in main.tscn.
  5. Telemetry stub — Optional signal zoom_band_changed(new_index: int, distance: float) or TODO comment referencing E9.M1; no external schema required for this story.

Decisions (pre-implementation)

Topic Choice
Band parameter Distance-only per step (same pitch / FOV as NEO-15); document in module snapshot. FOV-per-band deferred unless trivial.
Bounds at ends Clamp (no wrap): input at min/max does nothing extra.
Config null / empty Use follow_distance export only; zoom_band_index in state = 0.
Invalid band values Any ≤ 0 distance in band_distances treats the config as unusable (same fallback as null/empty).

Files to add

Path Purpose
client/scripts/zoom_band_config.gd Resource type for ordered band distances + default index; clamp helpers (no class_name).
client/resources/isometric_zoom_bands.tres Default ZoomBandConfig instance for designers (≥ 3 bands around current baseline distance).

Files to modify

Path Rationale
client/scripts/isometric_follow_camera.gd Wire config, band index, input handling, effective distance for eye position, correct CameraState sync (replace hard-coded zoom_band_index = 0).
client/scripts/camera_state.gd Tighten comments only if needed so distance = effective follow distance and zoom_band_index = active band are unambiguous (fields already exist).
client/project.godot Register camera_zoom_in / camera_zoom_out input actions.
client/scenes/main.tscn Assign zoom_band_config on the follow rig to the new .tres (or leave null to exercise fallback — prefer assigned for acceptance “designer tunable”).
docs/decomposition/modules/E1_M2_IsometricCameraController.md Update implementation snapshot for NEO-16: ZoomBandConfig path, input actions, discrete bands replace “single scalar until NEO-16”.

Tests

File Coverage
client/test/zoom_band_config_test.gd New: clamp_index for empty / single / multi band; distance_at matches configured values; default index clamped when out of range.
client/test/isometric_follow_camera_test.gd Change: add a test for a small static helper on the rig script (e.g. resolve effective follow distance from optional config + band index + fallback export) so headless CI covers band selection without simulating InputEvent.

Manual verification: in running client, wheel and keys cycle bands; confirm framing stays readable at each step and CameraState matches (debugger or brief log).

Open questions / risks

  • Band count = 1: Still valid; zoom input should no-op (clamped range size 1).
  • Readability AC is partly subjective; pick band spread conservatively and adjust .tres after first playtest.