diff --git a/client/scripts/main.gd b/client/scripts/main.gd index 934423248..716ee6e34 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -351,7 +351,8 @@ func _consume_dialogue() -> void: dialogue_box.show_dialogue( dlg.get("npc_name", ""), dlg.get("speech", ""), - dlg.get("options", []) + dlg.get("options", []), + _last_dialogue_npc_id ) GameState.current_dialogue = null @@ -385,7 +386,7 @@ func _consume_dialogue_response() -> void: var speaker_color_index: int = dr.get("speaker_color_index", -1) var speaker_name: String = dr.get("speaker_name", _last_dialogue_npc_name) dialogue_box.update_entity_display(speaker_entity_id, speaker_name, speaker_color_index) - dialogue_box.append_dialogue_response(speaker_name, dr.get("text", "")) + dialogue_box.append_dialogue_response(speaker_name, dr.get("text", ""), speaker_entity_id) GameState.dialogue_response = null diff --git a/client/ui/dialogue_box.gd b/client/ui/dialogue_box.gd index c1b4f2ec1..da30b0698 100644 --- a/client/ui/dialogue_box.gd +++ b/client/ui/dialogue_box.gd @@ -45,6 +45,13 @@ var _option_texts: Array[String] = [] var _option_is_confrontation: Array[bool] = [] var _npc_name: String = "" +# -- Entity color registry (#573) -- +# Maps entity_id → Color for dialogue participants. +# Assigned from _npc_colors palette on first encounter; player uses _player_color. +var _npc_entity_colors: Dictionary = {} # entity_id -> Color +var _npc_entity_id: int = -1 # Entity ID of the current player conversation NPC +var _next_npc_color: int = 0 # Round-robin palette index for client-side assignment + # -- UI state -- var _active_tween: Tween = null var _beat_tween: Tween = null # D-063: confrontation beat delay @@ -186,16 +193,22 @@ func _update_layout() -> void: ## speaker/target: display names. text: the spoken line. ## is_passive: true for overheard NPC-NPC (renders with ┃ prefix + desaturated). ## Active conversation entries are pinned (no timeout) while _in_player_conversation. -func append_line(speaker: String, target: String, text: String, is_passive: bool = false) -> void: +## speaker_entity_id/target_entity_id: optional entity IDs for stable color lookup (#573). +func append_line(speaker: String, target: String, text: String, is_passive: bool = false, speaker_entity_id: int = -1, target_entity_id: int = -1) -> void: var pinned := not is_passive and _in_player_conversation - _log_entries.append({ + var entry: Dictionary = { "speaker": speaker, "target": target, "text": text, "is_passive": is_passive, "pinned": pinned, "timestamp_msec": Time.get_ticks_msec(), - }) + } + if speaker_entity_id >= 0: + entry["speaker_entity_id"] = speaker_entity_id + if target_entity_id >= 0: + entry["target_entity_id"] = target_entity_id + _log_entries.append(entry) _log_dirty = true _ensure_visible() @@ -263,26 +276,32 @@ func on_conversation_ended(_event: Dictionary) -> void: ## Append the player's chosen response to the log. func append_player_line(target_npc: String, text: String) -> void: - append_line(PLAYER_NAME, target_npc, text, false) + append_line(PLAYER_NAME, target_npc, text, false, -1, _npc_entity_id) ## Append an NPC follow-up line (from dialogue_response). -func append_dialogue_response(npc_name: String, text: String) -> void: - append_line(npc_name, PLAYER_NAME, text, false) +func append_dialogue_response(npc_name: String, text: String, entity_id: int = -1) -> void: + if entity_id >= 0: + _assign_npc_color(entity_id) + append_line(npc_name, PLAYER_NAME, text, false, entity_id, -1) # -- Active player conversation -- ## Show dialogue with NPC speech and response options. ## npc_name: who is speaking. speech: the NPC's line. options: player choices. -func show_dialogue(npc_name: String, speech: String, options: Array = []) -> void: +## npc_entity_id: entity ID of the NPC for stable color assignment (#573). +func show_dialogue(npc_name: String, speech: String, options: Array = [], npc_entity_id: int = -1) -> void: _npc_name = npc_name + _npc_entity_id = npc_entity_id _cancel_beat() _in_player_conversation = true + if npc_entity_id >= 0: + _assign_npc_color(npc_entity_id) # Append NPC's line to the log if not speech.is_empty(): - append_line(npc_name, PLAYER_NAME, speech, false) + append_line(npc_name, PLAYER_NAME, speech, false, npc_entity_id, -1) # Clear old options and show new ones _clear_options() @@ -509,8 +528,17 @@ func _format_entry(entry: Dictionary, alpha: float) -> String: # Legacy string-keyed entry (player dialogue, backward compat) speaker = _escape_bbcode(entry.get("speaker", "?")) target = _escape_bbcode(entry.get("target", "?")) - speaker_color = _color_for_name(entry.get("speaker", "?")) - target_color = _color_for_name(entry.get("target", "?")) + # #573: use entity-ID-bound color if available; fall back to name-hash + var sp_eid: int = entry.get("speaker_entity_id", -1) + var tg_eid: int = entry.get("target_entity_id", -1) + if sp_eid >= 0 and _npc_entity_colors.has(sp_eid): + speaker_color = _npc_entity_colors[sp_eid] + else: + speaker_color = _color_for_name(entry.get("speaker", "?")) + if tg_eid >= 0 and _npc_entity_colors.has(tg_eid): + target_color = _npc_entity_colors[tg_eid] + else: + target_color = _color_for_name(entry.get("target", "?")) involves_player = (entry.get("speaker", "") == PLAYER_NAME) or (entry.get("target", "") == PLAYER_NAME) var text: String = _escape_bbcode(entry.text) @@ -546,6 +574,21 @@ static func _escape_bbcode(text: String) -> String: return text.replace("[", "[lb]").replace("]", "[rb]") +## Assign a palette color to an NPC entity ID on first encounter (#573). +## Returns the same color on subsequent calls for the same entity ID. +func _assign_npc_color(entity_id: int) -> Color: + if entity_id < 0: + return _speech_color + if _npc_entity_colors.has(entity_id): + return _npc_entity_colors[entity_id] + if _npc_colors.is_empty(): + return _speech_color + var color := _enforce_contrast(_npc_colors[_next_npc_color % _npc_colors.size()]) + _next_npc_color += 1 + _npc_entity_colors[entity_id] = color + return color + + ## Get a stable color for a character name, with contrast floor enforcement. func _color_for_name(char_name: String) -> Color: if char_name == PLAYER_NAME: