feat(ui): implement dialogue pipeline — selection, walk-away, confrontation

Protocol: decode current_dialogue with structured options {text,
response_id, priority, confrontation} and npc_entity_id (#435).
Dialogue box: priority sort, max 3 visible, RichTextLabel for BBCode
italic confrontation options (D-063), 1.5s monologue beat with audio
dip before confrontation send. Walk-away: WASD triggers WalkAway
input, 300ms fade, dialogue_active flag gates movement (D-064).
Implements #435, #437, #436.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 16:34:55 +01:00
co-authored by Claude Opus 4.6
parent 9e6b859cdf
commit bfc699c15d
6 changed files with 185 additions and 39 deletions
+20 -7
View File
@@ -17,6 +17,7 @@ extends Node2D
# If a new snapshot arrives with null dialogue while fade-in is still running,
# we don't re-trigger show_dialogue because _last_dialogue_id still matches.
var _last_dialogue_id: int = 0
var _last_dialogue_npc_id: int = -1 # D-064: NPC entity_id for WalkAway input
var _camera_anchored: bool = false
var _last_monologue_tick: int = -1 # Prevent re-consuming monologue when same tick polled twice
var _last_dialogue_tick: int = -1
@@ -50,6 +51,7 @@ func _ready() -> void:
if dialogue_box:
dialogue_box.option_selected.connect(_on_dialogue_option_selected)
dialogue_box.dialogue_dismissed.connect(_on_dialogue_dismissed)
dialogue_box.confrontation_monologue.connect(_on_confrontation_monologue)
func _process(_delta: float) -> void:
@@ -70,8 +72,13 @@ func _process(_delta: float) -> void:
world_renderer.update_from_state()
# D-057: Update interaction list from game state
# Suppress during dialogue — player is in conversation, verb list is noise
if interaction_list and interaction_list.has_method("update_from_state"):
interaction_list.update_from_state()
if dialogue_box and dialogue_box.is_dialogue_active():
if interaction_list.is_showing():
interaction_list._hide()
else:
interaction_list.update_from_state()
# D-065: Update inventory grid
if inventory_grid and inventory_grid.has_method("update_from_state"):
@@ -156,6 +163,7 @@ func _consume_dialogue() -> void:
return
_last_dialogue_tick = GameState.current_tick
var dlg: Dictionary = GameState.current_dialogue
_last_dialogue_npc_id = dlg.get("npc_entity_id", -1)
dialogue_box.show_dialogue(
dlg.get("npc_name", ""),
dlg.get("speech", ""),
@@ -166,26 +174,31 @@ func _consume_dialogue() -> void:
# D-061: Handle dialogue option selection → send to server
func _on_dialogue_option_selected(index: int, text: String) -> void:
func _on_dialogue_option_selected(response_id: String, text: String) -> void:
SimBridge.send_input({
"action": InputMapper.Action.INTERACT,
"timestamp_msec": Time.get_ticks_msec(),
"action_data": {
"target_entity_id": null,
"verb": "DialogueResponse",
"dialogue_option_index": index,
"dialogue_option_text": text,
"response_id": response_id,
},
})
# D-064: Handle walk-away → send DialogueEnd to server
# D-063: Handle confrontation beat monologue → show on monologue display (layer 7)
func _on_confrontation_monologue(text: String, duration: float) -> void:
if monologue_display:
monologue_display.show_monologue(text, duration)
# D-064: Handle walk-away → send WalkAway{npc_id} to server
func _on_dialogue_dismissed() -> void:
SimBridge.send_input({
"action": InputMapper.Action.INTERACT,
"timestamp_msec": Time.get_ticks_msec(),
"action_data": {
"target_entity_id": null,
"verb": "DialogueEnd",
"target_entity_id": _last_dialogue_npc_id if _last_dialogue_npc_id >= 0 else null,
"verb": "WalkAway",
},
})