From 06d9104157ed31a5cbebdb4c097386ae459b9cce Mon Sep 17 00:00:00 2001 From: VinPropane Date: Fri, 24 Apr 2026 20:44:43 -0400 Subject: [PATCH] NEO-25: route E/R interact via main _unhandled_key_input for reliable input --- client/README.md | 2 +- client/scripts/interaction_request_client.gd | 21 ++++++++--------- client/scripts/main.gd | 24 ++++++++++++++++++++ 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/client/README.md b/client/README.md index 98680a6..3f8325a 100644 --- a/client/README.md +++ b/client/README.md @@ -91,7 +91,7 @@ On boot, **`InteractablesCatalogClient`** **`GET`s** **`/game/world/interactable 1. Run the game server and client as in NEO-7. 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. +4. **E** / **R** are handled in **`main.gd`** **`_unhandled_key_input`** and forwarded to **`InteractionRequestClient`** (same pattern as dev keys) so they still 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/interaction_request_client.gd b/client/scripts/interaction_request_client.gd index 30f973f..60f9f8d 100644 --- a/client/scripts/interaction_request_client.gd +++ b/client/scripts/interaction_request_client.gd @@ -2,7 +2,9 @@ extends Node ## NS-18 / NEO-25: POST InteractionRequest; prints server allow/deny to Output (prototype). ## [kbd]E[/kbd] → [code]prototype_terminal[/code]; [kbd]R[/kbd] → [code]prototype_resource_node_alpha[/code] -## (stable contract ids — must match server registry). +## (stable contract ids — must match server registry). Key handling is delegated from +## [code]main.gd[/code] [method Node._unhandled_key_input] (see [method post_interact_terminal] / +## [method post_interact_resource]) so the embedded Game dock still receives E/R reliably. const ID_TERMINAL := "prototype_terminal" const ID_RESOURCE_NODE := "prototype_resource_node_alpha" @@ -28,17 +30,12 @@ func _ready() -> void: _http.request_completed.connect(_on_request_completed) -func _input(event: InputEvent) -> void: - if event.is_action_pressed("interact"): - _post_interact(ID_TERMINAL) - elif event.is_action_pressed("interact_secondary"): - _post_interact(ID_RESOURCE_NODE) - elif event is InputEventKey: - var k := event as InputEventKey - if k.pressed and not k.echo and (k.physical_keycode == KEY_E or k.keycode == KEY_E): - _post_interact(ID_TERMINAL) - elif k.pressed and not k.echo and (k.physical_keycode == KEY_R or k.keycode == KEY_R): - _post_interact(ID_RESOURCE_NODE) +func post_interact_terminal() -> void: + _post_interact(ID_TERMINAL) + + +func post_interact_resource() -> void: + _post_interact(ID_RESOURCE_NODE) func _base_root() -> String: diff --git a/client/scripts/main.gd b/client/scripts/main.gd index 978982a..82fb063 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -77,6 +77,7 @@ var _dev_obstacle_smoke: Node3D @onready var _player_pos_label: Label = $UICanvas/PlayerPositionLabel @onready var _target_lock_label: Label = $UICanvas/TargetLockLabel @onready var _target_client: Node = $TargetSelectionClient +@onready var _interaction_client: Node = $InteractionRequestClient @onready var _floor: StaticBody3D = $World/NavigationRegion3D/Floor @@ -279,11 +280,34 @@ func _on_move_rejected(reason_code: String) -> void: ## **F9** / **F10** are debugger shortcuts in the embedded game; use the Input Map action. ## For a true **freed-node** occluder test, reload the scene or remove the body in the editor. func _unhandled_key_input(event: InputEvent) -> void: + # NEO-25: route interact keys here — `InteractionRequestClient._input` is unreliable in the + # embedded Game dock / focus order; `_unhandled_key_input` on the scene root still fires. + if event.is_action_pressed("interact"): + _forward_interact_post("post_interact_terminal") + return + if event.is_action_pressed("interact_secondary"): + _forward_interact_post("post_interact_resource") + return + if event is InputEventKey: + var k := event as InputEventKey + if k.pressed and not k.echo and (k.physical_keycode == KEY_E or k.keycode == KEY_E): + _forward_interact_post("post_interact_terminal") + return + if k.pressed and not k.echo and (k.physical_keycode == KEY_R or k.keycode == KEY_R): + _forward_interact_post("post_interact_resource") + return if not event.is_action_pressed("dev_toggle_occluder_obstacle"): return call_deferred("_dev_toggle_obstacle_smoke_deferred") +func _forward_interact_post(method: String) -> void: + if not is_instance_valid(_interaction_client): + return + if _interaction_client.has_method(method): + _interaction_client.call(method) + + func _dev_toggle_obstacle_smoke_deferred() -> void: var obstacle := _dev_obstacle_smoke if obstacle == null or not is_instance_valid(obstacle):