Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# 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.

## 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.
24 changes: 21 additions & 3 deletions src/presentation/inventory/inventory_selection_label.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
31 changes: 31 additions & 0 deletions tests/godot/world_inventory_context_labels_test.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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]:
Expand All @@ -136,6 +139,34 @@ 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()
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")
Expand Down
Loading