class_name TestInteractionPrompt extends GdUnitTestSuite # Tests for ticket #405: Interaction prompt system. # Covers protocol v4 decode, GameState storage, SimBridge test mode, # InteractionPrompt UI, and input encoding. # -- Protocol: v4 nearby_interactions decoding -- func test_protocol_decode_v4_with_nearby_interactions() -> void: var raw := { "tick": 10, "version": Protocol.PROTOCOL_VERSION, "entities": [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": "Player", "visibility": "Forward", "relationship": "Unknown", "observation": "Visible"}, ], "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}, ], }], } var encoded = Messagepack.encode(raw) assert_that(encoded.status == null).is_true() var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot).is_not_null() assert_that(snapshot.nearby_interactions.size()).is_equal(1) var ni = snapshot.nearby_interactions[0] assert_that(ni.entity_id).is_equal(2) assert_that(ni.entity_type).is_equal("Npc") assert_that(ni.distance).is_equal(1) assert_that(ni.verbs.size()).is_equal(2) assert_that(ni.verbs[0].kind).is_equal("Talk") assert_that(ni.verbs[0].label).is_equal("Talk") assert_that(ni.verbs[0].priority).is_equal(1) assert_that(ni.verbs[0].available).is_true() assert_that(ni.verbs[1].kind).is_equal("ExamineNpc") func test_protocol_decode_v4_no_nearby_interactions() -> void: var raw := { "tick": 5, "version": Protocol.PROTOCOL_VERSION, "entities": [], } 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(0) func test_protocol_decode_empty_nearby_interactions() -> void: var raw := { "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], "nearby_interactions": [], } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot.nearby_interactions.size()).is_equal(0) func test_protocol_decode_interaction_missing_verbs() -> void: var raw := { "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], "nearby_interactions": [{"entity_id": 2}], } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot.nearby_interactions.size()).is_equal(0) func test_protocol_decode_interaction_empty_verbs() -> void: var raw := { "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], "nearby_interactions": [{"entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": []}], } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot.nearby_interactions.size()).is_equal(0) func test_protocol_decode_v4_entity_relationship() -> void: var raw := { "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": "Npc", "visibility": "Forward", "relationship": "Friendly", "observation": "Visible"}, ], } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot.entities[0].relationship).is_equal("Friendly") func test_protocol_rejects_version_mismatch() -> void: var raw := { "tick": 5, "version": 2, "entities": [], } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot).is_null() # -- GameState: nearby_interactions storage -- func test_game_state_stores_nearby_interactions() -> void: var ni := [{"entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}]}] GameState.apply_snapshot({"tick": 1, "entities": [], "nearby_interactions": ni}) assert_that(GameState.nearby_interactions.size()).is_equal(1) assert_that(GameState.nearby_interactions[0].entity_id).is_equal(2) GameState.nearby_interactions = [] func test_game_state_clears_nearby_interactions_when_absent() -> void: var ni := [{"entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}]}] GameState.apply_snapshot({"tick": 1, "entities": [], "nearby_interactions": ni}) assert_that(GameState.nearby_interactions.size()).is_equal(1) GameState.apply_snapshot({"tick": 2, "entities": []}) assert_that(GameState.nearby_interactions.size()).is_equal(0) # -- SimBridge test mode: nearby_interactions generation -- func test_sim_bridge_test_snapshot_interaction_when_near_npc() -> void: SimBridge.reset_test_state() SimBridge._test_player_pos = Vector2i(11, 9) var snap = SimBridge._test_snapshot() assert_that(snap.has("nearby_interactions")).is_true() assert_that(snap.nearby_interactions.size()).is_equal(1) assert_that(snap.nearby_interactions[0].entity_id).is_equal(2) assert_that(snap.nearby_interactions[0].verbs.size()).is_greater(0) func test_sim_bridge_test_snapshot_no_interaction_when_far() -> void: SimBridge.reset_test_state() SimBridge._test_player_pos = Vector2i(10, 10) var snap = SimBridge._test_snapshot() assert_that(snap.nearby_interactions.size()).is_equal(0) func test_sim_bridge_test_snapshot_interaction_at_range_2() -> void: SimBridge.reset_test_state() SimBridge._test_player_pos = Vector2i(10, 9) var snap = SimBridge._test_snapshot() assert_that(snap.nearby_interactions.size()).is_equal(1) func test_sim_bridge_test_snapshot_protocol_version() -> void: SimBridge.reset_test_state() var snap = SimBridge._test_snapshot() assert_that(snap.version).is_equal(Protocol.PROTOCOL_VERSION) # -- InteractionPrompt UI -- func test_prompt_get_selected_verb_returns_first_kind() -> void: 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}, ], }] var prompt = _make_prompt() assert_that(prompt.get_selected_verb()).is_equal("Talk") prompt.queue_free() GameState.nearby_interactions = [] func test_prompt_get_selected_verb_empty_when_no_interactions() -> void: GameState.nearby_interactions = [] var prompt = _make_prompt() assert_that(prompt.get_selected_verb()).is_equal("") prompt.queue_free() func test_prompt_get_target_negative_when_no_interactions() -> void: GameState.nearby_interactions = [] var prompt = _make_prompt() assert_that(prompt.get_interaction_target()).is_equal(-1) prompt.queue_free() func test_prompt_hidden_initially() -> void: GameState.nearby_interactions = [] var prompt = _make_prompt() assert_that(prompt._is_showing).is_false() prompt.queue_free() # -- Input encoding: Interact -- func test_interact_encodes_as_struct_variant() -> void: # Interact is always a struct variant: {"Interact": {"target_entity_id": null, "verb": null}} var inputs: Array = [{"tick": 100, "action_name": "Interact"}] var bytes := Protocol.encode_player_inputs(inputs) var raw = Messagepack.decode(bytes) assert_that(raw.status == null).is_true() assert_that(raw.value[0]["action"] is Dictionary).is_true() assert_that(raw.value[0]["action"].has("Interact")).is_true() var interact_data: Dictionary = raw.value[0]["action"]["Interact"] assert_that(interact_data.has("target_entity_id")).is_true() assert_that(interact_data.has("verb")).is_true() func test_interact_with_data_encodes_as_data_variant() -> void: var inputs: Array = [{ "tick": 100, "action_name": "Interact", "action_data": {"target_entity_id": 2, "verb": "Talk"}, }] var bytes := Protocol.encode_player_inputs(inputs) var raw = Messagepack.decode(bytes) assert_that(raw.status == null).is_true() assert_that(raw.value[0]["action"] is Dictionary).is_true() assert_that(raw.value[0]["action"].has("Interact")).is_true() # -- Helpers -- func _make_prompt() -> Control: var PromptScript = load("res://ui/interaction_prompt.gd") var ctrl = Control.new() ctrl.set_script(PromptScript) add_child(ctrl) # triggers _ready() which creates ImplantPanel + ImplantDataRow return ctrl