7.4 KiB
7.4 KiB
NEON-26 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEON-26 |
| Title | E1.M2: ZoomBandConfig + discrete zoom input (clamped) |
| Jira | NEON-26 |
| Parent | NEON-1 — Epic 1 — Core Player Runtime |
| Module | E1.M2 — IsometricCameraController; umbrella NEON-10 |
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 NEON-25 orbit seam.
In scope
ZoomBandConfig—Resource(noclass_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—@exportreference toZoomBandConfig; internal active band index clamped to[0, band_count - 1]; each frame use distance for the active band indesired_eye_world(same pitch / presentation yaw as today).- Input: cycle bands in / out (e.g. mouse wheel up/down and/or
+/-keys) viaInputMapactions inproject.godot; handle in_unhandled_input(or equivalent) on the rig so UI can steal focus later if needed. CameraState: setzoom_band_indexto the active index anddistanceto 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/...tresor inline default) editable in the inspector; rig points at it. - Optional: stub signal / throttled
print/TODO(E9.M1)forcamera_zoom_changedtelemetry when a schema exists.
Out of scope (per Jira)
- Smooth analog zoom between bands (unless a one-line lerp is explicitly chosen later — default no).
- Occlusion policy (NEON-27).
- Yaw orbit input (NEON-25 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 NEON-25 baseline).
CameraStatereflects active band and effective distance, and still exposes yaw unchanged in meaning.- Config lives in one tunable place for designers (
ZoomBandConfigresource referenced by the rig).
Technical approach
zoom_band_config.gd—extends Resourcewith e.g.@export var band_distances: PackedFloat32Array(length ≥ 1) and@export var default_band_index: int. Provideband_count,clamp_index(i: int) -> int,distance_at(index: int) -> float(uses clamped index). Validate in_validate_propertyor getter: if empty at runtime, treat as single band using rig fallback (see below).isometric_follow_camera.gd— Add@export var zoom_band_config: Resource(noclass_nameon the config script); at runtime requireget_script() == preload("…/zoom_band_config.gd")so headless CI and typed exports stay aligned. Conceptually this isZoomBandConfig. Keepfollow_distanceas fallback when config is null, empty, or has non‑positive band distances (preserves NEON-25 scenes/tests). Track_zoom_band_indexinitialized from config default (clamped). On zoom-in/out actions: adjust index withclamp_index, no op at ends (or wrap — clamp per story)._process: compute effective distance from config + index or fallback export. Replace uses of rawfollow_distanceindesired_eye_worldwith that value._sync_camera_state: pass effective distance into state (zoom_band_index+distance, plus existing yaw / path / focus).project.godot— Addcamera_zoom_inandcamera_zoom_out(or one action with axis — prefer two actions for wheel + keys) with mouse wheel and key events as in Jira examples.- Scene / resource — Add a default
ZoomBandConfig.tresunderres://resources/(or co-located underclient/resources/) with ≥ 3 bands bracketing ~25.709 m so readability can be checked; assign onIsometricFollowCamerainmain.tscn. - Telemetry stub — Optional
signal zoom_band_changed(new_index: int, distance: float)orTODOcomment 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 NEON-25); 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 NEON-26: ZoomBandConfig path, input actions, discrete bands replace “single scalar until NEON-26”. |
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
.tresafter first playtest.