feat(ui): bind dialogue speaker colors to entity identity (#573)
Maintains Dict[entity_id → Color] in dialogue_box for player conversations. On first encounter, assigns a round-robin palette color; reuses on subsequent lines. Eliminates position-based name-hash coloring for player dialogue. Changes: - Add _npc_entity_colors dict, _npc_entity_id, _next_npc_color fields - Add _assign_npc_color(entity_id) — registers palette color on first encounter - show_dialogue: accept npc_entity_id param, register entity color - append_line: optional speaker_entity_id/target_entity_id stored in log entries - append_player_line: pass _npc_entity_id as target_entity_id - append_dialogue_response: accept entity_id, register, pass to append_line - _format_entry else branch: look up _npc_entity_colors before name-hash fallback - main.gd: pass _last_dialogue_npc_id to show_dialogue, speaker_entity_id to append_dialogue_response Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
+53
-10
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user