## D-057: Entity interaction vertical list tests (#432). ## Tests multi-verb vertical list rendering, insert styling, z-layer 6, ## verb click dispatch, and D-055 sprint suppression integration. ## ## Also covers stance toggle input encoding (D-053, #439). ## Spec refs: D-057, D-056, D-055, D-053, D-049 class_name TestInteractionList extends GdUnitTestSuite # -- Helpers ------------------------------------------------------------------- func _interaction_list_exists() -> bool: for path in ["res://ui/interaction_list.tscn", "res://ui/entity_interaction_list.tscn", "res://scenes/interaction_list.tscn"]: if ResourceLoader.exists(path): return true return false func _make_interaction_list() -> Node: for path in ["res://ui/interaction_list.tscn", "res://ui/entity_interaction_list.tscn", "res://scenes/interaction_list.tscn"]: if ResourceLoader.exists(path): var scene = load(path) var node = scene.instantiate() add_child(node) return node return null func _make_list_or_skip() -> Node: var list = _make_interaction_list() if list == null: push_warning("TestInteractionList: interaction list scene not found — test skipped (awaiting #432)") return list # -- Multi-verb rendering (D-057: 2-4 options max) ---------------------------- func test_list_renders_two_verbs() -> void: var list = _make_list_or_skip() if list == null: return GameState.nearby_interactions = [{ "entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [ {"kind": "Talk", "label": "Talk", "priority": 1, "available": true}, {"kind": "ExamineNpc", "label": "Observe", "priority": 2, "available": true}, ], }] if list.has_method("update_from_state"): list.update_from_state() if list.has_method("get_visible_verb_count"): assert_that(list.get_visible_verb_count()).is_equal(2) list.queue_free() GameState.nearby_interactions = [] func test_list_renders_four_verbs_max() -> void: var list = _make_list_or_skip() if list == null: return # D-057: "2-4 options max" GameState.nearby_interactions = [{ "entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [ {"kind": "Talk", "label": "Talk", "priority": 1, "available": true}, {"kind": "ExamineNpc", "label": "Observe", "priority": 2, "available": true}, {"kind": "Confront", "label": "Confront", "priority": 3, "available": true}, {"kind": "Observe", "label": "Look at", "priority": 4, "available": true}, ], }] if list.has_method("update_from_state"): list.update_from_state() if list.has_method("get_visible_verb_count"): assert_that(list.get_visible_verb_count()).is_less_equal(4) list.queue_free() GameState.nearby_interactions = [] func test_list_empty_when_no_interactions() -> void: var list = _make_list_or_skip() if list == null: return GameState.nearby_interactions = [] if list.has_method("update_from_state"): list.update_from_state() if list.has_method("get_visible_verb_count"): assert_that(list.get_visible_verb_count()).is_equal(0) list.queue_free() func test_list_hidden_when_no_interactions() -> void: var list = _make_list_or_skip() if list == null: return GameState.nearby_interactions = [] if list.has_method("update_from_state"): list.update_from_state() if list.has_method("is_showing"): assert_that(list.is_showing()).is_false() list.queue_free() # -- Verb click dispatch (D-057) ----------------------------------------------- func test_get_selected_verb_returns_kind() -> void: var list = _make_list_or_skip() if list == null: return GameState.nearby_interactions = [{ "entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [ {"kind": "Talk", "label": "Talk", "priority": 1, "available": true}, ], }] if list.has_method("get_selected_verb"): # Default selection should be the first (highest priority) verb assert_that(list.get_selected_verb()).is_equal("Talk") list.queue_free() GameState.nearby_interactions = [] func test_get_interaction_target_returns_entity_id() -> void: var list = _make_list_or_skip() if list == null: return GameState.nearby_interactions = [{ "entity_id": 42, "entity_type": "Npc", "distance": 1, "verbs": [ {"kind": "Talk", "label": "Talk", "priority": 1, "available": true}, ], }] if list.has_method("get_interaction_target"): assert_that(list.get_interaction_target()).is_equal(42) list.queue_free() GameState.nearby_interactions = [] # -- Verb priority ordering (D-057: sorted by priority) ----------------------- func test_verbs_sorted_by_priority() -> void: var list = _make_list_or_skip() if list == null: return GameState.nearby_interactions = [{ "entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [ {"kind": "ExamineNpc", "label": "Observe", "priority": 3, "available": true}, {"kind": "Talk", "label": "Talk", "priority": 1, "available": true}, {"kind": "Confront", "label": "Confront", "priority": 2, "available": true}, ], }] if list.has_method("update_from_state") and list.has_method("get_verb_labels"): list.update_from_state() var labels = list.get_verb_labels() # Should be sorted: Talk (1), Confront (2), Observe (3) if labels.size() >= 3: assert_that(labels[0]).is_equal("Talk") assert_that(labels[1]).is_equal("Confront") assert_that(labels[2]).is_equal("Observe") list.queue_free() GameState.nearby_interactions = [] # -- D-055: Sprint suppression ------------------------------------------------ func test_sprint_suppresses_interaction_list() -> void: # D-055: Sprint stance clears interaction buffer — no verbs should show var list = _make_list_or_skip() if list == null: return # Set up interactions GameState.nearby_interactions = [{ "entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [ {"kind": "Talk", "label": "Talk", "priority": 1, "available": true}, ], }] # Set stance to Sprint GameState.player_stance = "Sprint" if list.has_method("update_from_state"): list.update_from_state() # The list should be hidden when sprinting if list.has_method("is_showing"): assert_that(list.is_showing()).is_false() elif list.has_method("get_visible_verb_count"): assert_that(list.get_visible_verb_count()).is_equal(0) list.queue_free() GameState.nearby_interactions = [] GameState.player_stance = "Walk" func test_walk_shows_interaction_list() -> void: var list = _make_list_or_skip() if list == null: return GameState.nearby_interactions = [{ "entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [ {"kind": "Talk", "label": "Talk", "priority": 1, "available": true}, ], }] GameState.player_stance = "Walk" if list.has_method("update_from_state"): list.update_from_state() if list.has_method("is_showing"): assert_that(list.is_showing()).is_true() list.queue_free() GameState.nearby_interactions = [] func test_careful_shows_interaction_list() -> void: var list = _make_list_or_skip() if list == null: return GameState.nearby_interactions = [{ "entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [ {"kind": "Talk", "label": "Talk", "priority": 1, "available": true}, ], }] GameState.player_stance = "Careful" if list.has_method("update_from_state"): list.update_from_state() if list.has_method("is_showing"): assert_that(list.is_showing()).is_true() list.queue_free() GameState.nearby_interactions = [] GameState.player_stance = "Walk" # -- Z-layer compliance (D-049: interaction list on layer 6) ------------------ func test_interaction_list_z_layer() -> void: var list = _make_list_or_skip() if list == null: return # D-057: Labels render on insert overlay (CanvasLayer 10) if list.has_method("get_z_layer"): assert_that(list.get_z_layer()).is_equal(Constants.CANVAS_INSERT) list.queue_free() # -- Diegetic test (D-057: insert off → labels disappear) -------------------- func test_insert_off_hides_interaction_list() -> void: var list = _make_list_or_skip() if list == null: return # D-057: "If insert is off, labels disappear" GameState.nearby_interactions = [{ "entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [ {"kind": "Talk", "label": "Talk", "priority": 1, "available": true}, ], }] if list.has_method("set_insert_active"): list.set_insert_active(false) if list.has_method("is_showing"): assert_that(list.is_showing()).is_false() list.queue_free() GameState.nearby_interactions = [] # -- Stance toggle wire mapping (D-053) ---------------------------------------- func test_stance_up_wire_mapping() -> void: # Verify InputMapper.Action.TOGGLE_STANCE_UP maps to "ToggleStanceUp" wire name var wire = SimBridge.action_enum_to_wire(InputMapper.Action.TOGGLE_STANCE_UP) assert_that(wire).is_equal("ToggleStanceUp") func test_stance_down_wire_mapping() -> void: var wire = SimBridge.action_enum_to_wire(InputMapper.Action.TOGGLE_STANCE_DOWN) assert_that(wire).is_equal("ToggleStanceDown") func test_all_movement_actions_have_wire_mapping() -> void: # Verify no action produces an empty wire name (except OPEN_MENU which is client-only) var actions_with_mapping := [ InputMapper.Action.MOVE_NORTH, InputMapper.Action.MOVE_NORTHEAST, InputMapper.Action.MOVE_EAST, InputMapper.Action.MOVE_SOUTHEAST, InputMapper.Action.MOVE_SOUTH, InputMapper.Action.MOVE_SOUTHWEST, InputMapper.Action.MOVE_WEST, InputMapper.Action.MOVE_NORTHWEST, InputMapper.Action.INTERACT, InputMapper.Action.USE_PERCEPTION_MODE, InputMapper.Action.PAUSE, InputMapper.Action.TOGGLE_STANCE_UP, InputMapper.Action.TOGGLE_STANCE_DOWN, ] for action in actions_with_mapping: var wire = SimBridge.action_enum_to_wire(action) assert_that(wire.length()).is_greater(0) # -- Contradicted entity indicator (D-057 + #422) ---------------------------- func test_contradicted_entity_verbs_decode() -> void: # #422: NearbyInteraction.contradicted=true should be decodeable var raw := { "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], "nearby_interactions": [{ "entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}], "contradicted": true, }], } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot).is_not_null() assert_that(snapshot.nearby_interactions.size()).is_equal(1) # Note: contradicted field may not be decoded yet in protocol.gd # This test documents the expected behavior for #422 integration # -- Object type verb sets (D-057 Phase 1) ------------------------------------ func test_object_type_verbs_decode() -> void: # #421: ObjectType appears in NearbyInteraction.object_type var raw := { "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], "nearby_interactions": [{ "entity_id": 3, "entity_type": "Object", "distance": 1, "verbs": [ {"kind": "Open", "label": "Open", "priority": 1, "available": true}, {"kind": "Search", "label": "Search", "priority": 2, "available": true}, ], "object_type": "Container", }], } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot).is_not_null() assert_that(snapshot.nearby_interactions.size()).is_equal(1) assert_that(snapshot.nearby_interactions[0].verbs.size()).is_equal(2) assert_that(snapshot.nearby_interactions[0].verbs[0].kind).is_equal("Open") assert_that(snapshot.nearby_interactions[0].verbs[1].kind).is_equal("Search")