NEO-25: move resource_node anchor clear of Obstacle at (6,1,5)

pull/48/head
VinPropane 2026-04-24 20:42:12 -04:00
parent 90bd71d336
commit 0e72d98177
11 changed files with 17 additions and 13 deletions

View File

@ -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).

View File

@ -0,0 +1 @@
uid://jqrna6yahhgg

View File

@ -0,0 +1 @@
uid://dw7xslquwep7t

View File

@ -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")

View File

@ -0,0 +1 @@
uid://dmehy73a1mqnh

View File

@ -0,0 +1 @@
uid://5tpu7v4uineq

View File

@ -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

View File

@ -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 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.
@ -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. |

View File

@ -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);

View File

@ -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);

View File

@ -15,8 +15,8 @@ public static class PrototypeInteractableRegistry
/// <summary>Source-of-truth anchor and radius; client discovers via <c>GET /game/world/interactables</c> (NEO-25).</summary>
public static readonly PrototypeInteractableEntry PrototypeTerminal = new(0, 0.5, 0, 3.0, "terminal");
/// <summary>+XZ corner walk demo; same radius as terminal for predictable reach tuning.</summary>
public static readonly PrototypeInteractableEntry PrototypeResourceNodeAlpha = new(5, 0.5, 5, 3.0, "resource_node");
/// <summary>+X / Z open floor (avoids overlap with scene <c>Obstacle</c> at ~(6,1,5)); same radius as terminal.</summary>
public static readonly PrototypeInteractableEntry PrototypeResourceNodeAlpha = new(12, 0.5, -6, 3.0, "resource_node");
private static readonly Dictionary<string, PrototypeInteractableEntry> ById = new(StringComparer.Ordinal)
{