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

15 KiB
Raw Blame History

NEO-25 — Implementation plan

Story reference

Field Value
Key NEO-25
Title E1.M3: InteractableDescriptor list / projection (multi-entity)
Linear NEO-25
Slug E1M3-03
Git branch NEO-25-e1m3-interactable-descriptor-list
Parent context Epic 1 — Core Player Runtime · E1M3 prototype backlog
Decomposition E1.M3 — InteractionAndTargetingLayer

Goal, scope, and out-of-scope

Goal: The server exposes a versioned JSON snapshot of InteractableDescriptor rows for ≥2 prototype interactables so the client can discover anchors, radii, and kind without treating GDScript as the long-term registry. The existing NEO-9 POST /game/players/{id}/interact path remains authoritative per id (horizontal X/Z reach, inclusive radius).

In scope

  • Registry: Extend PrototypeInteractableRegistry with a second row: stable id prototype_resource_node_alpha, world anchor (5, 0.5, 5), interactionRadius (same 3.0 m as the terminal unless testing needs otherwise — document in registry comments), kind string resource_node. Keep prototype_terminal at (0, 0.5, 0), kind terminal, radius 3.0 (unchanged NEO-9 defaults).
  • HTTP: GET /game/world/interactables returns InteractablesListResponse v1: top-level schemaVersion, interactables array of descriptors. Each element includes interactableId (lowercase canonical), kind, anchor (x, y, z), interactionRadius. Stable JSON order: ascending interactableId so tests and docs stay deterministic (prototype_resource_node_alpha before prototype_terminal).
  • Client (Decision — option A): On boot (after or in parallel with position authority wiring — see Technical approach), GET the list and treat it as the only source for anchor/radius/kind used by preview glow and world meshes. Remove prototype_interaction_constants.gd; no duplicated anchor/radius constants for gameplay preview.
  • Client visuals: For each descriptor, spawn one visible prop mesh at the anchor (distinct materials by kind for readability) plus two small glow marker meshes per interactable (same emission pattern as todays interaction_radius_indicators.gd, but N interactables × 2 markers). Preview still compares player global_position to each descriptors anchor on X/Z only with inclusive <= vs that rows interactionRadius — server POST …/interact remains authoritative.
  • Client interaction QA: Two input actions so each id is explicitly reachable without cycling ambiguity: interact (default E) → prototype_terminal; interact_secondary (default R) → prototype_resource_node_alpha. Document in client/project.godot and client/README.md.
  • Tests: C# integration tests for the new GET (schema, count ≥ 2, field presence, ordering). Extend or add interaction tests proving POST …/interact allows each id when the seeded player is moved in range and denies when out of range.
  • Bruno: New request(s) under bruno/neon-sprawl-server/ for the GET (per testing-expectations).
  • Manual QA: During implementation, add docs/manual-qa/NEO-25.md per planning-implementation-docs.

Out of scope (per Linear / backlog)

  • Full data-driven content pipeline (contracts.md content kind).
  • Gather loop / rewards (E3.M1).
  • Per-player or zone-filtered lists (registry is global; route is world-scoped intentionally — future visibility can add query params or a different resource without breaking this v1 list).
  • positionHint on interact (NEO-24-style) — not required for NEO-25; interact still reads IPositionStateStore only.

Acceptance criteria checklist

  • ≥2 interactables in PrototypeInteractableRegistry with distinct ids, anchors, radii, and kinds (terminal vs resource_node).
  • GET /game/world/interactables returns versioned JSON whose interactables entries each include interactableId, anchor (world), interactionRadius, kind.
  • POST /game/players/{id}/interact with InteractionRequest v1 succeeds for each id when the player is in horizontal range; denies with stable reasonCode when out of range (NEO-9 semantics preserved).
  • Godot: preview glow + scene props are driven from the fetched descriptor list (no prototype_interaction_constants.gd). interact / interact_secondary each hit the correct id for manual QA.
  • Bruno + server README document the new GET.

Technical approach

  1. C# registry model: Extend PrototypeInteractableEntry (or equivalent) with Kind (string, stable machine values terminal, resource_node for v1). Add IReadOnlyList<InteractableDescriptorDto> or a static GetAllDescriptors() that projects registry rows in ascending id order for the HTTP handler.

  2. DTOs: New types under NeonSprawl.Server/Game/Interaction/ (e.g. InteractableDescriptorResponse, InteractablesListResponse) with XML/summary comments matching the README field table. Reuse PositionVector from targeting if it already serializes {x,y,z} cleanly; otherwise a small AnchorVector DTO co-located with interactables.

  3. Route registration: app.MapGet("/game/world/interactables", …)no player id; no store dependency (static registry only). If we later need player-scoped visibility, add a new route rather than overloading this one.

  4. Godot catalog client: New script (e.g. interactables_catalog_client.gd) with the same HTTP patterns as target_selection_client.gd / interaction_request_client.gd: HTTPRequest, _busy, base_url, request_catalog() on _ready or when main.gd triggers after first frame. Signal catalog_ready(descriptors: Array) where each element is a Dictionary mirroring JSON (or a small typed helper). On failure: push_error + leave InteractionMarkers empty (document: run server first for full scene).

  5. World build-out: Replace the static World/NavigationRegion3D/PrototypeTerminal + static World/InteractionMarkers/MarkerA|B pattern with either:

    • Preferred: an empty World/InteractablesRoot Node3D + child script interactable_scene_builder.gd (or merged into catalog client) that on catalog_ready spawns StaticBody3D (walkable group) + mesh per descriptor at anchor, and spawns two MeshInstance3D markers per descriptor under World/InteractionMarkers, then calls into the existing per-frame glow logic (refactor interaction_radius_indicators.gd into a Node3D that owns arrays of marker nodes + descriptor metrics, or one material per interactable row).
    • Scene cleanup: Remove hard-coded terminal + two markers from main.tscn once runtime spawn is wired; keep main.gd thin — connect catalog → builder → interaction_radius_indicators equivalent.
  6. main.gd sequencing: Call request_catalog() early enough that glow has data before the player walks (e.g. same _ready block after nav bake, before or after sync_from_server — if after, document one-frame dim state). Ensure InteractionRequestClient does not POST until catalog is ready or hard-bind the two actions to the stable ids above (catalog still drives visuals; keys use fixed id strings matching server contract).

  7. Docs: server/README.md — subsection Interactable descriptors (NEO-25) with curl example and field table. client/README.md — replace NEO-9 “constants file” language with “fetched from GET /game/world/interactables”. Optionally one line in E1_M3_InteractionAndTargetingLayer.md implementation snapshot when the story merges.

Decisions (kickoff — locked)

Topic Choice
List route GET /game/world/interactables (global registry projection).
Second row prototype_resource_node_alpha @ (5, 0.5, 5), kind: resource_node, radius 3.0 (align with terminal unless implementation discovers a reason to differ).
Client registry source Option A: fetch list on boot; no prototype_interaction_constants.gd.
Client visuals Full parity: one prop + two glow markers per descriptor.
Interaction keys interact → terminal; interact_secondary → resource node id.

Files to add

Path Purpose
server/NeonSprawl.Server/Game/Interaction/InteractablesListDtos.cs Versioned InteractablesListResponse + nested InteractableDescriptor JSON shape for GET.
server/NeonSprawl.Server/Game/Interaction/InteractablesWorldApi.cs MapInteractablesWorldApi — registers GET /game/world/interactables.
client/scripts/interactables_catalog_client.gd HTTP GET catalog; cache; catalog_ready signal.
client/scripts/interactable_world_builder.gd On catalog: spawn walkable meshes + marker children from descriptor data (keeps main.gd thin per godot-client-script-organization).
client/test/interactables_catalog_client_test.gd GdUnit: mock transport returns JSON; assert parse + ordering + field extraction.
client/test/interactables_catalog_test_double.gd Optional: mock HTTP helper mirroring other test doubles.
server/NeonSprawl.Server.Tests/Game/Interaction/InteractablesWorldApiTests.cs Integration: GET returns 200, schema 1, ≥2 items, ascending ids, required fields.
bruno/neon-sprawl-server/interaction/Get interactables list.bru (Or under bruno/neon-sprawl-server/world/ if we prefer grouping) — documents GET contract.
docs/manual-qa/NEO-25.md Story manual QA checklist (generated during implementation per project rule).

Files to modify

Path Rationale
server/NeonSprawl.Server/Game/Interaction/PrototypeInteractableRegistry.cs Add second entry + Kind on entries; expose enumeration / projection for GET; update XML to drop “sync GDScript constants” wording in favor of NEO-25 GET.
server/NeonSprawl.Server/Game/Interaction/InteractionApi.cs No path change to POST; only if shared helpers are needed for descriptor projection (otherwise leave untouched).
server/NeonSprawl.Server/Program.cs Register MapInteractablesWorldApi() (or equivalent one-liner).
server/README.md Document NEO-25 GET + example JSON + field table next to Interaction (NEO-9).
server/NeonSprawl.Server.Tests/Game/Interaction/InteractionApiTests.cs Add/adjust cases: interact allowed for each id in range; unknown id unchanged; optional ordering smoke for registry if useful.
client/scripts/interaction_radius_indicators.gd Refactor to multi-descriptor input (arrays / setup from builder) or merge into builder with the same emission rules as today.
client/scripts/interaction_request_client.gd Remove preload of deleted constants; wire interact / interact_secondary to the two stable ids; optionally block POST until catalog loaded (minimal: document that keys use contract ids).
client/scripts/main.gd Instantiate/connect InteractablesCatalogClient, call request_catalog, connect catalog_ready → world builder + indicator setup; remove references to deleted constants script.
client/scenes/main.tscn Remove static PrototypeTerminal + static MarkerA/MarkerB once runtime spawn replaces them; add InteractablesCatalogClient node + empty roots as needed.
client/project.godot Register interact_secondary (default R).
client/README.md NEO-9/NEO-25 manual flow: fetch-driven preview, dual keys, server-first tuning.
docs/decomposition/modules/E1_M3_InteractionAndTargetingLayer.md Optional one-line implementation snapshot update when NEO-25 merges (descriptor list live).

Files to remove

Path Rationale
client/scripts/prototype_interaction_constants.gd Superseded by server-backed catalog (Option A); delete .gd and .uid companion if present.

Tests

Test file What to cover
server/NeonSprawl.Server.Tests/Game/Interaction/InteractablesWorldApiTests.cs GET returns 200; schemaVersion 1; interactables length ≥ 2; sorted by interactableId; each entry has kind, anchor, interactionRadius; spot-check values for prototype_terminal and prototype_resource_node_alpha.
server/NeonSprawl.Server.Tests/Game/Interaction/InteractionApiTests.cs For each registry id: seed player position in rangeallowed: true; move out of rangeallowed: false, reasonCode: out_of_range; preserve 400/unknown interactable behaviors from NEO-9.
client/test/interactables_catalog_client_test.gd Parse happy-path JSON; assert signal payload shape and stable ordering.
client/test/interaction_request_client_test.gd Add if missing: when adding/changed interaction_request_client.gd, per testing-expectations — mock HTTP; assert correct interactableId in POST bodies for E vs R actions. (If file does not exist yet, create minimal suite alongside the client change.)

Manual: docs/manual-qa/NEO-25.md — server + client; confirm GET in browser/curl; walk to each anchor; glow per row; E / R posts match allowed / reasonCode in Output.

Open questions / risks

  • Boot without server: Descriptor GET fails → empty interactables / error log. Acceptable for prototype; document in README (run server before F5 for full layout).
  • Collision / nav: New StaticBody3D props must remain walkable (or not block nav) — place on floor; use same collision pattern as the old PrototypeTerminal.
  • Id strings in interaction_request_client.gd: Stable interact / interact_secondary bindings reference canonical ids as literals — not geometry duplication, but if registry ids rename, update client + tests together.