From 1441e2ebf29b136feaa8c72cfe90ed19cf5e5134 Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Sun, 11 Oct 2026 20:41:14 -0400 Subject: [PATCH 1/3] Document contextual inventory interaction hint scope --- .../contextual-inventory-interaction-hints-milestone.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/development/contextual-inventory-interaction-hints-milestone.md diff --git a/docs/development/contextual-inventory-interaction-hints-milestone.md b/docs/development/contextual-inventory-interaction-hints-milestone.md new file mode 100644 index 0000000..e16fc2c --- /dev/null +++ b/docs/development/contextual-inventory-interaction-hints-milestone.md @@ -0,0 +1,9 @@ +# Contextual inventory interaction hints + +Presentation-only follow-up to PR122 (89ef6df247c7a4825a23595ed79c471ff42469b3). + +Scope: the existing hovered/selected world-item card in InventorySelectionLabel and ProceduralWorldInventoryPreview, focused real-shell regression coverage, and this milestone record. Controls help already documents selection/collection; no additional persistent instruction card is needed. + +Acceptance: opt-in captions remain quiet by default; hovered/selected context explains existing selection/pickup inputs, clears on reset/disconnect, remains bounded in a narrow shell, and preserves neutral unknown role, exact observed details, paid inventory records, geometry, stable request identity and pending/outcome uncertainty. + +Verification pending. No simulation, schemas, transports, shared input/bootstrap/camera, machinery captions or walker changes. No self-merge, video, uploads, release or deployment. From 01627068a97c974a7eaa818148a1f2f34ce3f80b Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Sun, 11 Oct 2026 20:43:48 -0400 Subject: [PATCH 2/3] Explain current world-item selection and pickup bindings --- ...l-inventory-interaction-hints-milestone.md | 8 +++++++ .../inventory/inventory_selection_label.gd | 24 ++++++++++++++++--- .../procedural_world_inventory_preview.gd | 2 +- .../world_inventory_context_labels_test.gd | 22 +++++++++++++++++ 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/docs/development/contextual-inventory-interaction-hints-milestone.md b/docs/development/contextual-inventory-interaction-hints-milestone.md index e16fc2c..ad3ee80 100644 --- a/docs/development/contextual-inventory-interaction-hints-milestone.md +++ b/docs/development/contextual-inventory-interaction-hints-milestone.md @@ -7,3 +7,11 @@ Scope: the existing hovered/selected world-item card in InventorySelectionLabel Acceptance: opt-in captions remain quiet by default; hovered/selected context explains existing selection/pickup inputs, clears on reset/disconnect, remains bounded in a narrow shell, and preserves neutral unknown role, exact observed details, paid inventory records, geometry, stable request identity and pending/outcome uncertainty. Verification pending. No simulation, schemas, transports, shared input/bootstrap/camera, machinery captions or walker changes. No self-merge, video, uploads, release or deployment. + +## Implementation and local evidence + +One fourth, wrapping row in the existing contextual card explains the configured world-selection binding. Mouse hover describes select and double-click request; selection describes double-click pickup request. Keyboard/controller bindings describe select-and-request; an unbound route points to Controls. The caption makes no eligibility or success claim and unknown definitions retain the neutral Item role. The existing observed details still expose exact identity and contents uncertainty. + +Godot 4.7.2, Windows: bounded runner import/startup and world_inventory_context_labels_test.gd, world_inventory_preview_test.gd, selected_target_controls_test.gd passed. The actual-shell fixture uses eight real items including four paid builds, opens no ports and saves nothing. It checks default-off captions, hover/selection, remaps/unbound input, reset/rebuild/disconnect, 640px and 1440px shells, unchanged meshes/colliders and canonical snapshot, exact pickup identity, and pending/disconnect uncertainty. Headless hover uses the native Area3D signal after a genuine camera ray assertion; mouse selection is injected through the shell. + +No internal stills were needed: source and layout/runtime assertions establish the scoped behavior. Human/controller hardware/VR and Linux graphical acceptance were not exercised; this is no claim of those acceptance paths. Required Windows/Linux CI and exact tested synthetic SHA will be recorded in the handoff after completion. diff --git a/src/presentation/inventory/inventory_selection_label.gd b/src/presentation/inventory/inventory_selection_label.gd index eb41c60..a432805 100644 --- a/src/presentation/inventory/inventory_selection_label.gd +++ b/src/presentation/inventory/inventory_selection_label.gd @@ -3,12 +3,13 @@ class_name InventorySelectionLabel ## One bounded screen card; the leader terminates at the actual display proxy. const MARGIN: float = 12.0 -const CARD_SIZE := Vector2(320, 104) +const CARD_SIZE := Vector2(320, 144) var target: Node3D var enabled: bool = true var _card: PanelContainer var _lines: Array[Label] = [] var _anchor := Vector2.ZERO +var _selected: bool = false func _ready() -> void: @@ -28,20 +29,26 @@ func _ready() -> void: var rows := VBoxContainer.new() rows.mouse_filter = Control.MOUSE_FILTER_IGNORE _card.add_child(rows) - for index in range(3): + for index in range(4): var line := Label.new() line.mouse_filter = Control.MOUSE_FILTER_IGNORE line.add_theme_font_size_override("font_size", 18) line.add_theme_color_override("font_color", Color("#fff3be") if index == 0 else Color.WHITE) line.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS line.clip_text = true + if index == 3: + line.add_theme_font_size_override("font_size", 16) + line.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + line.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING + line.clip_text = false rows.add_child(line) _lines.append(line) visible = false -func select_proxy(proxy: Node3D, item_id: String, container_id: String, distance_mm: int, definition_id: String = "") -> void: +func select_proxy(proxy: Node3D, item_id: String, container_id: String, distance_mm: int, definition_id: String = "", selected: bool = false) -> void: target = proxy + _selected = selected _lines[0].text = "%s: %s" % [observed_role(definition_id), item_id] _lines[1].text = "Root: " + container_id _lines[2].text = "Host observed: %.2f m" % (float(distance_mm) / 1000.0) @@ -60,6 +67,16 @@ static func observed_role(definition_id: String) -> String: return "Item" +## Describes the existing input route, never host eligibility or success. +func interaction_hint() -> String: + var events: Array[InputEvent] = InputMap.action_get_events("world_select") + if events.is_empty(): return "Selection unbound — open Controls" + var binding: String = PlayerInputSettings.instance().binding_label("world_select") + if events[0] is InputEventMouseButton: + return "Double-click %s: request pickup" % binding if _selected else "%s: select · double-click: request pickup" % binding + return "%s: select and request pickup" % binding + + func _process(_delta: float) -> void: var viewport: Viewport = get_viewport() var camera: Camera3D = viewport.get_camera_3d() @@ -74,6 +91,7 @@ func _process(_delta: float) -> void: if not Rect2(Vector2.ZERO, extent).has_point(_anchor): visible = false return + _lines[3].text = interaction_hint() size = extent _card.size = Vector2(minf(CARD_SIZE.x, extent.x - MARGIN * 2), CARD_SIZE.y) _card.position = Vector2(extent.x - _card.size.x - MARGIN, MARGIN) diff --git a/src/presentation/inventory/procedural_world_inventory_preview.gd b/src/presentation/inventory/procedural_world_inventory_preview.gd index 28455ec..44accde 100644 --- a/src/presentation/inventory/procedural_world_inventory_preview.gd +++ b/src/presentation/inventory/procedural_world_inventory_preview.gd @@ -179,7 +179,7 @@ func _highlight() -> void: _selection_label.enabled = labels_visible and _label_observation_available if _visible_items.has(context_key): var selected: Dictionary = _visible_items[context_key] - _selection_label.select_proxy(selected["mesh"], selected["item_id"], selected["container_id"], selected["distance"], selected["definition_id"]) + _selection_label.select_proxy(selected["mesh"], selected["item_id"], selected["container_id"], selected["distance"], selected["definition_id"], context_key == _selected_key) else: _selection_label.visible = false diff --git a/tests/godot/world_inventory_context_labels_test.gd b/tests/godot/world_inventory_context_labels_test.gd index 7778711..75a2b4c 100644 --- a/tests/godot/world_inventory_context_labels_test.gd +++ b/tests/godot/world_inventory_context_labels_test.gd @@ -63,6 +63,7 @@ func verify() -> void: camera.set_process(false) view = client._habitat.world_inventory var panel: PlayerInventoryPanel = client._inventory_panel + check(not view.labels_visible and not view._selection_label.visible, "context hints remain opt-in in the actual shell") # Exercise the actual opt-in captions setting; default remains quiet. for control in client.find_children("*", "CheckButton", true, false): if control.text == "Show world captions": control.button_pressed = true @@ -98,6 +99,7 @@ func verify() -> void: if DisplayServer.get_name() == "headless": area.mouse_entered.emit() check(view._hovered_key == JSON.stringify(["world.tray-1", paid_ids[0]]), "Area3D hover retains exact identity (native OS pointer in graphical mode)") check(view._selection_label.visible and view._selection_label._lines[0].text == "Canister: " + paid_ids[0], "observed role and exact short identity on hover") + check(view._selection_label._lines[3].text == PlayerInputSettings.instance().binding_label("world_select") + ": select · double-click: request pickup", "hover explains the existing mouse route without promising success") check(view._selected_key.is_empty() and selected.is_empty() and pickup.is_empty() and remote.sent.is_empty(), "hover selects nothing and queues no intent") check(not view._tray_labels["world.tray-1"].visible, "ordinary tray identity does not duplicate the item card or cover neighbors") await snap("hover") @@ -112,6 +114,7 @@ func verify() -> void: root.push_input(click, true) await settle() check(selected == "world.tray-1:" + paid_ids[0] and pickup.is_empty(), "ordinary click emits exact selection only") + check(view._selection_label.interaction_hint() == "Double-click %s: request pickup" % PlayerInputSettings.instance().binding_label("world_select"), "selected hint describes pickup request only") # World selection has its own identity card; overlay selection is independent. for index in range(panel._world_items.item_count): if panel._world_items.get_item_metadata(index)["item_id"] == paid_ids[0]: @@ -136,6 +139,25 @@ func verify() -> void: for line in view._selection_label._lines: check(view._selection_label._card.get_global_rect().encloses(line.get_global_rect()), "all identity rows bounded by opaque card") await snap("selected-%d" % extent.x) + # Local remaps update the caption without changing selection or sending work. + InputMap.action_erase_events("world_select") + var key_binding := InputEventKey.new() + key_binding.physical_keycode = KEY_K + InputMap.action_add_event("world_select", key_binding) + await settle() + check(view._selection_label._lines[3].text == "%s: select and request pickup" % PlayerInputSettings.instance().binding_label("world_select"), "keyboard remap uses the existing activate route") + InputMap.action_erase_events("world_select") + var pad_binding := InputEventJoypadButton.new() + pad_binding.button_index = JOY_BUTTON_A + InputMap.action_add_event("world_select", pad_binding) + await settle() + check(view._selection_label._lines[3].text == "%s: select and request pickup" % PlayerInputSettings.instance().binding_label("world_select"), "controller remap has no misleading double-click advice") + InputMap.action_erase_events("world_select") + await settle() + check(view._selection_label._lines[3].text == "Selection unbound — open Controls", "unbound input gives neutral settings guidance") + PlayerInputSettings.instance()._reset_map() + await settle() + check(view._selection_label.target == mesh and remote.sent.is_empty() and panel._pending_world == pending_before and panel._world_details.text == details_before, "binding hint changes preserve exact context and pending details without requests") view.labels_visible = false await settle() check(not view._selection_label.visible and not view._tray_labels["world.tray-1"].visible, "label setting hides every context without geometry changes") From c8124ef3af8ae767642b707c88fb80423679e0fb Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Sun, 11 Oct 2026 20:45:55 -0400 Subject: [PATCH 3/3] Verify wrapped hover hint in the narrow shell --- tests/godot/world_inventory_context_labels_test.gd | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/godot/world_inventory_context_labels_test.gd b/tests/godot/world_inventory_context_labels_test.gd index 75a2b4c..7276d94 100644 --- a/tests/godot/world_inventory_context_labels_test.gd +++ b/tests/godot/world_inventory_context_labels_test.gd @@ -139,6 +139,15 @@ func verify() -> void: for line in view._selection_label._lines: check(view._selection_label._card.get_global_rect().encloses(line.get_global_rect()), "all identity rows bounded by opaque card") await snap("selected-%d" % extent.x) + view.clear_selection() + area.mouse_entered.emit() + await settle() + check(view._selection_label.visible and view._selection_label._lines[3].text.contains(": select · double-click:"), "hover hint remains readable in resized real shell") + check(viewport.get_visible_rect().encloses(Rect2(view._selection_label._card.position, view._selection_label._card.size)), "longer hover hint card fits narrow viewport") + check(view._selection_label._card.get_global_rect().encloses(view._selection_label._lines[3].get_global_rect()), "wrapped hint fits its existing card") + click.pressed = true + area.input_event.emit(camera, click, mesh.global_position, Vector3.UP, 0) + await settle() # Local remaps update the caption without changing selection or sending work. InputMap.action_erase_events("world_select") var key_binding := InputEventKey.new()