diff --git a/client/README.md b/client/README.md index 78d89b5..98680a6 100644 --- a/client/README.md +++ b/client/README.md @@ -89,8 +89,8 @@ On boot, **`InteractablesCatalogClient`** **`GET`s** **`/game/world/interactable ### Manual check (NEO-9 + NEO-25) 1. Run the game server and client as in NEO-7. -2. **F5** in Godot: after catalog load, two interactable sites appear (center + **+XZ**). Default spawn **(-5, 0.9, -5)** is out of range of the **center** node (3 m); markers near **origin** stay **dim** until you **WASD** within horizontal **3** m of **(0, 0.5, 0)** on X/Z — then that pair **brightens**. -3. Press **E** (**`interact`**): Output **`allowed=true`** when the **terminal** pair glows; **`allowed=false`** / **`out_of_range`** when dim. Walk to **(5, 5)** area until the **second** pair glows; press **R** (**`interact_secondary`**) for **`allowed=true`** on the resource node. +2. **F5** in Godot: after catalog load, two interactable sites appear (center + **+X / −Z** floor, anchor **(12, 0.5, −6)** — clear of the gray **Obstacle** near **(6, 1, 5)**). Default spawn **(-5, 0.9, -5)** is out of range of the **center** node (3 m); markers near **origin** stay **dim** until you **WASD** within horizontal **3** m of **(0, 0.5, 0)** on X/Z — then that pair **brightens**. +3. Press **E** (**`interact`**): Output **`allowed=true`** when the **terminal** pair glows; **`allowed=false`** / **`out_of_range`** when dim. Walk toward **(12, −6)** until the **second** pair glows; press **R** (**`interact_secondary`**) for **`allowed=true`** on the resource node. 4. Interaction uses **`_input`** (and key fallbacks) so keys register in the embedded **Game** dock; click the game view if the editor had focus elsewhere. Full checklist: [`docs/manual-qa/NEO-25.md`](../manual-qa/NEO-25.md). diff --git a/client/scripts/interactable_world_builder.gd.uid b/client/scripts/interactable_world_builder.gd.uid new file mode 100644 index 0000000..15cf31d --- /dev/null +++ b/client/scripts/interactable_world_builder.gd.uid @@ -0,0 +1 @@ +uid://jqrna6yahhgg diff --git a/client/scripts/interactables_catalog_client.gd.uid b/client/scripts/interactables_catalog_client.gd.uid new file mode 100644 index 0000000..9d01604 --- /dev/null +++ b/client/scripts/interactables_catalog_client.gd.uid @@ -0,0 +1 @@ +uid://dw7xslquwep7t diff --git a/client/test/interactables_catalog_client_test.gd b/client/test/interactables_catalog_client_test.gd index 84872d8..b8cedc2 100644 --- a/client/test/interactables_catalog_client_test.gd +++ b/client/test/interactables_catalog_client_test.gd @@ -7,7 +7,7 @@ func test_parse_catalog_json_orders_and_fields() -> void: var json := ( '{"schemaVersion":1,"interactables":[' + '{"interactableId":"prototype_resource_node_alpha","kind":"resource_node",' - + '"anchor":{"x":5,"y":0.5,"z":5},"interactionRadius":3},' + + '"anchor":{"x":12,"y":0.5,"z":-6},"interactionRadius":3},' + '{"interactableId":"prototype_terminal","kind":"terminal",' + '"anchor":{"x":0,"y":0.5,"z":0},"interactionRadius":3}' + "]}" @@ -22,7 +22,7 @@ func test_parse_catalog_json_orders_and_fields() -> void: assert_that(r0.get("kind", "")).is_equal("resource_node") var a0: Variant = r0.get("anchor", null) assert_that(a0 is Dictionary).is_true() - assert_that(float((a0 as Dictionary).get("x", -1.0))).is_equal(5.0) + assert_that(float((a0 as Dictionary).get("x", -1.0))).is_equal(12.0) assert_that(r1.get("interactableId", "")).is_equal("prototype_terminal") diff --git a/client/test/interactables_catalog_client_test.gd.uid b/client/test/interactables_catalog_client_test.gd.uid new file mode 100644 index 0000000..8168f06 --- /dev/null +++ b/client/test/interactables_catalog_client_test.gd.uid @@ -0,0 +1 @@ +uid://dmehy73a1mqnh diff --git a/client/test/interaction_request_client_test.gd.uid b/client/test/interaction_request_client_test.gd.uid new file mode 100644 index 0000000..94ee647 --- /dev/null +++ b/client/test/interaction_request_client_test.gd.uid @@ -0,0 +1 @@ +uid://5tpu7v4uineq diff --git a/docs/manual-qa/NEO-25.md b/docs/manual-qa/NEO-25.md index 6b7c13a..3b291c7 100644 --- a/docs/manual-qa/NEO-25.md +++ b/docs/manual-qa/NEO-25.md @@ -10,9 +10,9 @@ ## Checks 1. **Descriptor GET:** `curl -s http://localhost:5253/game/world/interactables` returns JSON with `schemaVersion: 1`, `interactables` length ≥ 2, ascending `interactableId`, each row has `kind`, `anchor` `{x,y,z}`, `interactionRadius`. -2. **F5 with server up:** After load, **two** prototype props appear (center + **+XZ** corner) and **two pairs** of small blue glow markers (offset from each anchor). Default spawn **(-5, 0.9, -5)**: markers near **origin** glow when in 3 m radius; corner node stays dim until you walk toward **(5, 5)**. +2. **F5 with server up:** After load, **two** prototype props appear (center + **+X / −Z** open floor) and **two pairs** of small blue glow markers (offset from each anchor). Default spawn **(-5, 0.9, -5)**: markers near **origin** glow when in 3 m radius; second node stays dim until you walk near **(12, 0.5, −6)** (kept clear of the gray **Obstacle** at ~(6, 1, 5)). 3. **E** (`interact`): at origin in range → Output `allowed=true` for terminal; walk out past 3 m horizontal → `allowed=false`, `out_of_range`. -4. **R** (`interact_secondary`): stand within 3 m of **(5, 0.5, 5)** on X/Z → `allowed=true` for resource node; far from that anchor → `out_of_range`. +4. **R** (`interact_secondary`): stand within 3 m of **(12, 0.5, −6)** on X/Z → `allowed=true` for resource node; far from that anchor → `out_of_range`. 5. **Server down / wrong port:** catalog request errors in Output; scene still runs (no props / no glow groups — acceptable prototype degradation). ## Notes diff --git a/docs/plans/NEO-25-implementation-plan.md b/docs/plans/NEO-25-implementation-plan.md index e14c789..639f847 100644 --- a/docs/plans/NEO-25-implementation-plan.md +++ b/docs/plans/NEO-25-implementation-plan.md @@ -18,7 +18,7 @@ **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). +- **Registry:** Extend `PrototypeInteractableRegistry` with a **second** row: stable id **`prototype_resource_node_alpha`**, world anchor **`(12, 0.5, −6)`** (open floor; avoids **`Obstacle`** in `main.tscn` at ~(6, 1, 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 today’s `interaction_radius_indicators.gd`, but **N** interactables × 2 markers). Preview still compares player **`global_position`** to each descriptor’s anchor on **X/Z** only with **inclusive** `<=` vs that row’s **`interactionRadius`** — server **`POST …/interact`** remains authoritative. @@ -65,7 +65,7 @@ | 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). | +| **Second row** | **`prototype_resource_node_alpha`** @ **`(12, 0.5, −6)`**, **`kind`:** **`resource_node`**, radius **3.0** (moved from +XZ prototype to stay clear of scene **`Obstacle`**). | | **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. | diff --git a/server/NeonSprawl.Server.Tests/Game/Interaction/InteractablesWorldApiTests.cs b/server/NeonSprawl.Server.Tests/Game/Interaction/InteractablesWorldApiTests.cs index 0d631cf..3193f52 100644 --- a/server/NeonSprawl.Server.Tests/Game/Interaction/InteractablesWorldApiTests.cs +++ b/server/NeonSprawl.Server.Tests/Game/Interaction/InteractablesWorldApiTests.cs @@ -28,9 +28,9 @@ public class InteractablesWorldApiTests var alpha = body.Interactables.Single(x => x.InteractableId == PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId); Assert.Equal("resource_node", alpha.Kind); - Assert.Equal(5, alpha.Anchor.X); + Assert.Equal(12, alpha.Anchor.X); Assert.Equal(0.5, alpha.Anchor.Y); - Assert.Equal(5, alpha.Anchor.Z); + Assert.Equal(-6, alpha.Anchor.Z); Assert.Equal(3.0, alpha.InteractionRadius); var term = body.Interactables.Single(x => x.InteractableId == PrototypeInteractableRegistry.PrototypeTerminalId); diff --git a/server/NeonSprawl.Server.Tests/Game/Interaction/InteractionApiTests.cs b/server/NeonSprawl.Server.Tests/Game/Interaction/InteractionApiTests.cs index b08a1c1..70f4dd8 100644 --- a/server/NeonSprawl.Server.Tests/Game/Interaction/InteractionApiTests.cs +++ b/server/NeonSprawl.Server.Tests/Game/Interaction/InteractionApiTests.cs @@ -239,7 +239,7 @@ public class InteractionApiTests var move = new MoveCommandRequest { SchemaVersion = MoveCommandRequest.CurrentSchemaVersion, - Target = new PositionVector { X = 5, Y = 0.9, Z = 5 }, + Target = new PositionVector { X = 12, Y = 0.9, Z = -6 }, }; Assert.Equal(HttpStatusCode.OK, (await client.PostAsJsonAsync("/game/players/dev-local-1/move", move)).StatusCode); diff --git a/server/NeonSprawl.Server/Game/Interaction/PrototypeInteractableRegistry.cs b/server/NeonSprawl.Server/Game/Interaction/PrototypeInteractableRegistry.cs index 010b5cf..460f26b 100644 --- a/server/NeonSprawl.Server/Game/Interaction/PrototypeInteractableRegistry.cs +++ b/server/NeonSprawl.Server/Game/Interaction/PrototypeInteractableRegistry.cs @@ -15,8 +15,8 @@ public static class PrototypeInteractableRegistry /// Source-of-truth anchor and radius; client discovers via GET /game/world/interactables (NEO-25). public static readonly PrototypeInteractableEntry PrototypeTerminal = new(0, 0.5, 0, 3.0, "terminal"); - /// +XZ corner walk demo; same radius as terminal for predictable reach tuning. - public static readonly PrototypeInteractableEntry PrototypeResourceNodeAlpha = new(5, 0.5, 5, 3.0, "resource_node"); + /// +X / −Z open floor (avoids overlap with scene Obstacle at ~(6,1,5)); same radius as terminal. + public static readonly PrototypeInteractableEntry PrototypeResourceNodeAlpha = new(12, 0.5, -6, 3.0, "resource_node"); private static readonly Dictionary ById = new(StringComparer.Ordinal) {