Files
settled-reach/client/tests/test_interaction_prompt.gd
T
jpmschweitzerandClaude Opus 4.6 6bebbd9933 feat(client): add interaction prompt system (#405)
Server-driven interaction prompt that displays "E - Talk" when near
an interactable NPC. Decodes v4 nearby_interactions from snapshot,
stores in GameState, renders via InteractionPrompt UI with fade
animation. Extensible interface (get_interaction_target/get_selected_verb)
for future radial verb menu (v0.2).

- Protocol: decode nearby_interactions array with nested VerbOption structs,
  entity relationship/observation fields, tick_rate in GameTime
- GameState: store/clear nearby_interactions per snapshot
- SimBridge: test mode generates v4 format with structured verbs
- InteractionPrompt: PanelContainer with fade in/out, polls GameState
- Tests: 19 new test cases covering protocol, state, sim bridge, UI, encoding
- Fixture assertions updated for v4 protocol version

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:43:37 +01:00

220 lines
7.6 KiB
GDScript

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": 4,
"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": 0, "available": true},
{"kind": "ExamineNpc", "label": "Examine", "priority": 1, "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(0)
assert_that(ni.verbs[0].available).is_true()
assert_that(ni.verbs[1].kind).is_equal("ExamineNpc")
func test_protocol_decode_v2_no_nearby_interactions() -> void:
var raw := {
"tick": 5,
"version": 2,
"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,
"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,
"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,
"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": 4,
"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")
# -- 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": 0, "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": 0, "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_v4_version() -> void:
SimBridge.reset_test_state()
var snap = SimBridge._test_snapshot()
assert_that(snap.version).is_equal(4)
# -- 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": 0, "available": true},
{"kind": "ExamineNpc", "label": "Examine", "priority": 1, "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_unit_variant() -> void:
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_equal("Interact")
func test_interact_with_data_encodes_as_data_variant() -> void:
# Future: once server accepts Interact(InteractData)
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() -> PanelContainer:
var PromptScript = load("res://ui/interaction_prompt.gd")
var panel = PanelContainer.new()
panel.set_script(PromptScript)
var margin = MarginContainer.new()
margin.name = "MarginContainer"
panel.add_child(margin)
var label = Label.new()
label.name = "PromptLabel"
margin.add_child(label)
add_child(panel)
return panel