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

98 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# NEO-24 — Implementation plan
## Story reference
| Field | Value |
|--------|--------|
| **Key** | NEO-24 |
| **Title** | E1.M3: Client tab-target + lock UI synced to server |
| **Linear** | [NEO-24](https://linear.app/neon-sprawl/issue/NEO-24/e1m3-client-tab-target-lock-ui-synced-to-server) |
| **Slug** | E1M3-02 |
| **Git branch** | `NEO-24-e1m3-client-tab-target-lock-ui-synced-to-server` |
| **Parent context** | [Epic 1 — Core Player Runtime](https://linear.app/neon-sprawl/project/epic-1-core-player-runtime-client-controls-character-loop-66bd590cd016) · [E1M3 prototype backlog](E1M3-prototype-backlog.md) |
| **Depends on** | [NEO-23](NEO-23-implementation-plan.md) (targeting HTTP v1) |
| **Decomposition** | [E1.M3 — InteractionAndTargetingLayer](../decomposition/modules/E1_M3_InteractionAndTargetingLayer.md); authority notes: [client_server_authority — E1.M3](../decomposition/modules/client_server_authority.md#e1m3-interactionandtargetinglayer) |
## Goal, scope, and out-of-scope
**Goal:** **Tab** (or agreed input action) cycles **server-eligible** stub targets in a **documented deterministic order**; **HUD / debug overlay** reflects the last **server-acknowledged** `TargetState` (`lockedTargetId`, `validity`, `sequence`). Client sends **selection intent** via existing **`POST …/target/select`**; **optional optimistic** presentation **must** reconcile to the **authoritative** `targetState` in each **200** response (including denials), per E1.M3 authority doc.
**In scope**
- Godot: input → **`TargetSelectRequest`** v1 → parse **`TargetSelectResponse`** / **`PlayerTargetStateResponse`**; maintain **last acknowledged** target state for UI.
- **Tab cycle order:** match server stub contract — lexicographic ascending id over the **prototype registry ids** (`prototype_target_alpha`, then `prototype_target_beta`), wrapping from last → first; same ordering as [server README — Targeting](../../server/README.md#targeting-neo-23) / `PrototypeTargetRegistry` comments.
- **Clear lock:** **`POST`** with **`targetId`** omitted or JSON **`null`** (NEO-23 rule); binding documented in `client/project.godot` (proposed: **Esc** via `target_clear`).
- **UI:** readable label (or small overlay) showing **server** lock id (or **none** / empty), **`validity`**, and optionally **`sequence`** for debugging.
- **Soft lock / movement:** when the player moves, **`validity`** can become **`out_of_range`** without clearing **`lockedTargetId`**. Client must **refresh** authoritative state periodically or on hooks so the overlay stays truthful (see Technical approach).
- **Shared constants:** duplicate **stub target id list + order** in a small GDScript constants module aligned with `PrototypeTargetRegistry` (NEO-23 plan expectation for NEO-24 preview/cycle).
**Out of scope (per Linear / backlog)**
- Nearest-in-cone auto-target; sticky soft-target under latency.
- **`AbilityCastRequest`** / hotbar — [E1.M4](../decomposition/modules/E1_M4_AbilityInputScaffold.md).
- Server route or DTO changes (covered by NEO-23); **no** new ASP.NET endpoints unless a client bug reveals a contract gap (then narrow fix + plan update).
## Acceptance criteria checklist
- [ ] With **≥2** stub targets, **tab** advances selection intent in **ascending id** order and **wraps** at the end of the ordered list.
- [ ] After each successful **round-trip**, UI shows **`lockedTargetId`** (or equivalent **none**) and **`validity`** from the **response body** (`targetState` on POST, body on GET).
- [ ] On **`selectionApplied`: `false`**, UI shows **authoritative** `targetState` from the same response (no “stuck optimistic” lock id that the server denied).
- [ ] **Clear** action clears lock server-side and UI updates from the response.
- [ ] Input bindings for **tab cycle** and **clear** are documented in **`client/project.godot`** (and briefly in **`client/README.md`** if other prototype keys are documented there).
## Technical approach
1. **`prototype_target_constants.gd` (or rename if clearer):** export ordered `Array[String]` of stub ids matching `PrototypeTargetRegistry` (`alpha`, `beta`) for tab math only; single source for “eligible cycle list” on the client.
2. **`target_selection_client.gd`:** Node analogous to `interaction_request_client.gd` / `position_authority_client.gd`: owns injectable HTTP transport, `base_url`, `dev_player_id`, `_busy` guard. Responsibilities:
- **`request_sync_from_server()`** — `GET …/target` → parse `PlayerTargetStateResponse` v1 → cache + emit signal.
- **`request_select_target_id(target_id: String)`** — `POST …/target/select` with body `{"schemaVersion":1,"targetId":…}`.
- **`request_clear_target()`** — `POST` with no `targetId` / `null` per NEO-23.
- **`request_tab_next()`** — read cached `lockedTargetId` + `validity`; compute **next** id in the ordered list (if no lock, choose **first** id in order); POST select; on deny, signal carries **server** `targetState` only (no client-only lock).
- Signal e.g. **`target_state_changed(state: Dictionary)`** or typed fields for consumers (`lockedTargetId` Variant string-or-null, `validity` string, `sequence` int, `selection_applied` bool optional for last POST).
- **Polling:** while last known `lockedTargetId` is non-null, issue **`GET …/target`** on a **throttled** timer (e.g. every **0.250.5 s**) so **`out_of_range`** updates after locomotion without requiring new tab presses. Stop or slow polling when no lock if desired for noise (document choice in plan **Decisions** during implementation).
3. **Optimistic highlight (optional slice):** If implemented, keep **pending** selection separate from **acknowledged** state; on **200** denial, drop pending and paint from **`targetState`**. If timeboxed, **v1** may ship **ack-only** UI (still satisfies “optional optimistic” — none used).
4. **`main.tscn` / `main.gd`:** Add child node with `target_selection_client.gd`; connect signal to a **Label** (new `TargetLockLabel` under `UICanvas`). Call **`request_sync_from_server()`** once after boot position sync (or same frame as first authority ready) so HUD matches server on start.
5. **`project.godot`:** `target_tab`**Tab**; `target_clear`**Escape** (document; user may request alternatives before implementation).
6. **Manual QA:** Run server + client; tab between stubs; walk out of radius and confirm **`validity`** flips to **`out_of_range`** on overlay while id remains; clear with Esc.
## Files to add
| Path | Purpose |
|------|---------|
| `client/scripts/prototype_target_constants.gd` | Ordered stub **`targetId`** list + helpers for “next in cycle” matching server registry. |
| `client/scripts/target_selection_client.gd` | HTTP GET/POST targeting APIs; tab/clear entrypoints; throttled refresh; signals for UI. |
| `client/test/target_selection_test_double.gd` | Test subclass injecting mock HTTP (pattern from `position_authority_test_double.gd`). |
| `client/test/target_selection_client_test.gd` | GdUnit4: tab order wrap, POST success path, denial keeps server `lockedTargetId`, clear POST, GET parse. |
## Files to modify
| Path | Rationale |
|------|-----------|
| `client/scenes/main.tscn` | Add `TargetSelectionClient` node + **`TargetLockLabel`** (or reuse a debug panel) bound to scripts. |
| `client/scripts/main.gd` | Wire `@onready` target client + label; connect `target_state_changed`; trigger initial `request_sync_from_server()` after authority boot path. |
| `client/project.godot` | Register **`target_tab`** and **`target_clear`** input actions with defaults above. |
| `client/README.md` | One short subsection: tab-target prototype, bindings, and how to read the lock overlay (parity with interaction/move docs). |
## Tests
| Test file | What to cover |
|-----------|----------------|
| `client/test/target_selection_client_test.gd` | **Mock transport** enqueues JSON: GET returns a v1 envelope; POST **apply** returns `selectionApplied: true` + `targetState`; POST **deny** returns `selectionApplied: false` + `reasonCode` + authoritative `targetState`; **tab** from no lock selects first id in order; **tab** from `alpha` requests `beta`; wrap from `beta` to `alpha`; **clear** issues POST without `targetId`. Use `MockHttpTransport` pattern from `position_authority_client_test.gd`. |
No new **C#** tests (client-only story). **Bruno:** NEO-23 targeting requests already live under `bruno/neon-sprawl-server/targeting/`; **no** new `.bru` required unless implementation discovers a **server** contract change (then add per repo [testing expectations](../../.cursor/rules/testing-expectations.md) for HTTP contract changes).
## Open questions / risks
- **Input bindings:** Plan assumes **Tab** + **Esc**. Confirm or specify alternatives (e.g. mouse thumb buttons) before locking `project.godot`.
- **Polling cost:** Throttled GET while locked is simple but adds HTTP traffic; acceptable for prototype. If objection, switch to polling only when HUD visible or tie to `move-stream` cadence with plan update.
- **Scene visuals:** This story does **not** require 3D reticles on stub meshes; overlay text satisfies AC. World-space highlights can be a follow-up.
## Decisions
*(None locked yet — update this table during implementation chat / PR.)*