## Sprint 17 — UX: E-Talk overlay improvement (#537) — Phase 1 ## Tests for relationship color indicator in interaction_list.gd. ## ## Spec refs: ## D-033 (entity color = relationship to player) ## D-051 (diegetic insert display — insert-only overlay) ## D-057 (interaction list, z-layer 6) ## ## Phase 1 scope (2026-02-24): ## Color bar only — NPC name and dialogue tier deferred to Phase 2 pending ## server protocol change (no known_attributes in wire protocol v13). ## ## Implementation: client/ui/interaction_list.gd ## `var _relationship_color: Color = Constants.IMPLANT_TEXT_DIM` ## `func _cache_entity_relationship() -> void` ## 3px left-edge bar drawn in _draw() at 85% alpha, called from update_from_state() class_name TestETalkOverlaySprint17 extends GdUnitTestSuite func before_test() -> void: SimBridge.reset_test_state() GameState.nearby_interactions = [] GameState.visible_entities = [] GameState.player_stance = "" func after_test() -> void: GameState.nearby_interactions = [] GameState.visible_entities = [] GameState.player_stance = "" # ------------------------------------------------------------------------- # D-033: Constants.color_for_relationship() — palette baseline (pure logic) # ------------------------------------------------------------------------- func test_relationship_unknown_maps_to_teal() -> void: assert_that(Constants.color_for_relationship("Unknown")).is_equal(Constants.ENTITY_COLOR_UNKNOWN) func test_relationship_friendly_maps_to_green() -> void: assert_that(Constants.color_for_relationship("Friendly")).is_equal(Constants.ENTITY_COLOR_FRIENDLY) func test_relationship_poi_maps_to_amber() -> void: assert_that(Constants.color_for_relationship("PersonOfInterest")).is_equal(Constants.ENTITY_COLOR_POI) func test_relationship_hostile_maps_to_red() -> void: assert_that(Constants.color_for_relationship("Hostile")).is_equal(Constants.ENTITY_COLOR_HOSTILE) func test_relationship_known_falls_back_to_unknown_color() -> void: # "Known" not matched in color_for_relationship() — falls through _ → ENTITY_COLOR_UNKNOWN assert_that(Constants.color_for_relationship("Known")).is_equal(Constants.ENTITY_COLOR_UNKNOWN) func test_relationship_unknown_string_falls_back_to_unknown() -> void: assert_that(Constants.color_for_relationship("SomeNewState")).is_equal(Constants.ENTITY_COLOR_UNKNOWN) # ------------------------------------------------------------------------- # UIStrings: relationship state labels exist (Phase 2 pre-fixture) # ------------------------------------------------------------------------- func test_ui_strings_has_relationship_unknown_label() -> void: assert_that(UIStrings.has_key("relationship_states.unknown.label")).is_true() func test_ui_strings_has_relationship_known_label() -> void: assert_that(UIStrings.has_key("relationship_states.known.label")).is_true() func test_ui_strings_has_relationship_friendly_label() -> void: assert_that(UIStrings.has_key("relationship_states.friendly.label")).is_true() func test_ui_strings_has_relationship_poi_label() -> void: assert_that(UIStrings.has_key("relationship_states.person_of_interest.label")).is_true() func test_ui_strings_has_relationship_hostile_label() -> void: assert_that(UIStrings.has_key("relationship_states.hostile.label")).is_true() # ------------------------------------------------------------------------- # _relationship_color initial state # ------------------------------------------------------------------------- func test_relationship_color_default_is_implant_dim() -> void: # Before any update, _relationship_color starts at IMPLANT_TEXT_DIM var list = _make_list() assert_that(list._relationship_color).is_equal(Constants.IMPLANT_TEXT_DIM) list.queue_free() # ------------------------------------------------------------------------- # _cache_entity_relationship(): D-033 palette via update_from_state() (#537 core) # ------------------------------------------------------------------------- func test_cache_relationship_unknown_sets_unknown_color() -> void: _setup_npc_interaction(2, "Unknown") var list = _make_list() list.update_from_state() assert_that(list._relationship_color).is_equal(Constants.ENTITY_COLOR_UNKNOWN) list.queue_free() func test_cache_relationship_friendly_sets_green() -> void: _setup_npc_interaction(2, "Friendly") var list = _make_list() list.update_from_state() assert_that(list._relationship_color).is_equal(Constants.ENTITY_COLOR_FRIENDLY) list.queue_free() func test_cache_relationship_poi_sets_amber() -> void: _setup_npc_interaction(2, "PersonOfInterest") var list = _make_list() list.update_from_state() assert_that(list._relationship_color).is_equal(Constants.ENTITY_COLOR_POI) list.queue_free() func test_cache_relationship_hostile_sets_red() -> void: _setup_npc_interaction(2, "Hostile") var list = _make_list() list.update_from_state() assert_that(list._relationship_color).is_equal(Constants.ENTITY_COLOR_HOSTILE) list.queue_free() func test_cache_relationship_absent_field_uses_unknown_color() -> void: # Entity present but "relationship" key missing → get("relationship", "Unknown") → Unknown color GameState.visible_entities = [{ "entity_id": 2, "x": 12.0, "y": 9.0, "z": 0, "kind": {"variant": "Npc", "data": null}, "visibility": "Forward", }] GameState.nearby_interactions = [{ "entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}], }] var list = _make_list() list.update_from_state() assert_that(list._relationship_color).is_equal(Constants.ENTITY_COLOR_UNKNOWN) list.queue_free() func test_cache_relationship_entity_not_in_visible_uses_dim_fallback() -> void: # Entity in nearby_interactions but NOT in visible_entities → IMPLANT_TEXT_DIM fallback GameState.visible_entities = [] GameState.nearby_interactions = [{ "entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}], }] var list = _make_list() list.update_from_state() assert_that(list._relationship_color).is_equal(Constants.IMPLANT_TEXT_DIM) list.queue_free() func test_cache_relationship_updates_when_relationship_changes() -> void: # First call: Unknown _setup_npc_interaction(2, "Unknown") var list = _make_list() list.update_from_state() assert_that(list._relationship_color).is_equal(Constants.ENTITY_COLOR_UNKNOWN) # Relationship shifts → re-cache picks up new value _setup_npc_interaction(2, "Hostile") list.update_from_state() assert_that(list._relationship_color).is_equal(Constants.ENTITY_COLOR_HOSTILE) list.queue_free() func test_cache_relationship_targets_correct_entity_by_id() -> void: # Two entities visible; nearby_interactions[0] is the target (entity 2, Friendly) # Entity 3 (Hostile) must not pollute the color GameState.visible_entities = [ { "entity_id": 2, "x": 11.0, "y": 9.0, "z": 0, "kind": {"variant": "Npc", "data": null}, "visibility": "Forward", "relationship": "Friendly", "observation": "Visible", }, { "entity_id": 3, "x": 12.0, "y": 9.0, "z": 0, "kind": {"variant": "Npc", "data": null}, "visibility": "Forward", "relationship": "Hostile", "observation": "Visible", }, ] GameState.nearby_interactions = [ { "entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}], }, { "entity_id": 3, "entity_type": "Npc", "distance": 2, "verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}], }, ] var list = _make_list() list.update_from_state() # interaction[0] = entity 2 (Friendly) → green bar assert_that(list._relationship_color).is_equal(Constants.ENTITY_COLOR_FRIENDLY) list.queue_free() func test_z_layer_is_insert_canvas() -> void: # D-049 / D-057: interaction list lives on InsertOverlay (CanvasLayer 10) var list = _make_list() assert_that(list.get_z_layer()).is_equal(Constants.CANVAS_INSERT) list.queue_free() # ------------------------------------------------------------------------- # Regression: interaction_list public API unaffected by #537 changes # ------------------------------------------------------------------------- func test_list_hides_when_no_interactions() -> void: GameState.nearby_interactions = [] var list = _make_list() list.update_from_state() assert_that(list.is_showing()).is_false() list.queue_free() func test_list_shows_on_npc_interaction() -> void: _setup_npc_interaction(2, "Unknown") var list = _make_list() list.update_from_state() assert_that(list.is_showing()).is_true() list.queue_free() func test_list_get_selected_verb_returns_first_verb() -> void: GameState.nearby_interactions = [{ "entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [ {"kind": "Talk", "label": "Talk", "priority": 1, "available": true}, {"kind": "ExamineNpc", "label": "Look", "priority": 2, "available": true}, ], }] var list = _make_list() assert_that(list.get_selected_verb()).is_equal("Talk") list.queue_free() func test_list_get_interaction_target_returns_entity_id() -> void: GameState.visible_entities = [{ "entity_id": 5, "x": 12.0, "y": 9.0, "z": 0, "kind": {"variant": "Npc", "data": null}, "visibility": "Forward", "relationship": "Unknown", "observation": "Visible", }] GameState.nearby_interactions = [{ "entity_id": 5, "entity_type": "Npc", "distance": 1, "verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}], }] var list = _make_list() list.update_from_state() assert_that(list.get_interaction_target()).is_equal(5) list.queue_free() func test_list_suppressed_when_insert_inactive() -> void: _setup_npc_interaction(2, "Unknown") var list = _make_list() list.set_insert_active(false) list.update_from_state() assert_that(list.is_showing()).is_false() list.queue_free() func test_list_hides_on_empty_verb_list() -> void: GameState.nearby_interactions = [{ "entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [], }] var list = _make_list() list.update_from_state() assert_that(list.is_showing()).is_false() list.queue_free() # ------------------------------------------------------------------------- # Phase 2 placeholders — deferred pending server protocol change (no known_attributes in v13) # ------------------------------------------------------------------------- func skip_test_npc_name_displayed_when_known() -> void: pass func skip_test_dialogue_tier_context_hint_for_friendly() -> void: pass func skip_test_dialogue_tier_context_hint_for_hostile() -> void: pass # ------------------------------------------------------------------------- # Helpers # ------------------------------------------------------------------------- func _make_list() -> Control: var scene = load("res://ui/interaction_list.tscn") var list = scene.instantiate() add_child(list) # _ready() fires here — @onready var _vbox resolves return list ## Set up GameState with a single NPC entity + matching interaction for tests. func _setup_npc_interaction(entity_id: int, relationship: String) -> void: GameState.visible_entities = [{ "entity_id": entity_id, "x": 12.0, "y": 9.0, "z": 0, "kind": {"variant": "Npc", "data": null}, "visibility": "Forward", "relationship": relationship, "observation": "Visible", }] GameState.nearby_interactions = [{ "entity_id": entity_id, "entity_type": "Npc", "distance": 1, "verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}], }]