85 lines
7.0 KiB
Markdown
85 lines
7.0 KiB
Markdown
# NEON-26 — Implementation plan
|
|
|
|
## Story reference
|
|
|
|
| Field | Value |
|
|
|--------|--------|
|
|
| **Key** | NEON-26 |
|
|
| **Title** | E1.M2: ZoomBandConfig + discrete zoom input (clamped) |
|
|
| **Jira** | [NEON-26](https://neon-sprawl.atlassian.net/browse/NEON-26) |
|
|
| **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:** 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` (no `class_name`; match [godot-client-script-organization](../../.cursor/rules/godot-client-script-organization.md)) 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 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).
|
|
- [ ] `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.gd`** — `extends 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: ZoomBandConfig`. Keep **`follow_distance`** as **fallback** when config is null or has no bands (preserves NEON-25 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 `var d := effective_follow_distance()` from config + index or fallback export. Replace uses of raw `follow_distance` in `desired_eye_world` with `d`. **`_sync_camera_state`:** assign `_state.zoom_band_index` and `_state.distance = d` (and 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 Jira examples.
|
|
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 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`. |
|
|
|
|
## 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 `.tres` after first playtest.
|