feat(ui): dialogue UI hardening and examine result overlay (#174)
Confrontation options use RichTextLabel with italic tags for first-person voice (D-063). Examine result overlay auto-dismisses after 5s with confidence-based color tinting. Dismisses when dialogue opens. D-062 invisible locked options confirmed correct. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,9 @@ extends Node2D
|
|||||||
@onready var gauntlet_hud = $UILayer/GauntletHUD # #496: room timer + personal bests
|
@onready var gauntlet_hud = $UILayer/GauntletHUD # #496: room timer + personal bests
|
||||||
@onready var checklist_overlay = $UILayer/ChecklistOverlay # #503: auto-checklist progress
|
@onready var checklist_overlay = $UILayer/ChecklistOverlay # #503: auto-checklist progress
|
||||||
@onready var time_display = $InsertOverlay/TimeDisplay # #263: diegetic time display (D-013, D-031)
|
@onready var time_display = $InsertOverlay/TimeDisplay # #263: diegetic time display (D-013, D-031)
|
||||||
|
@onready var minimap = $InsertOverlay/Minimap # #151: diegetic minimap overlay (D-013, D-049)
|
||||||
|
@onready var examine_display = $InsertOverlay/ExamineDisplay # #174: examine result overlay
|
||||||
|
@onready var journal_panel = $InsertOverlay/JournalPanel # #264: knowledge journal (D-041)
|
||||||
@onready var debug_overlay = $UILayer/DebugOverlay # #511: F3 debug overlay
|
@onready var debug_overlay = $UILayer/DebugOverlay # #511: F3 debug overlay
|
||||||
@onready var bug_report_dialog = $ModalLayer/BugReportDialog # #495: F12 WRONG button
|
@onready var bug_report_dialog = $ModalLayer/BugReportDialog # #495: F12 WRONG button
|
||||||
@onready var settings_dialog = $ModalLayer/SettingsDialog # #528: audio settings (ESC/OPEN_MENU)
|
@onready var settings_dialog = $ModalLayer/SettingsDialog # #528: audio settings (ESC/OPEN_MENU)
|
||||||
@@ -99,6 +102,8 @@ func _process(delta: float) -> void:
|
|||||||
interaction_list.set_insert_active(insert_state)
|
interaction_list.set_insert_active(insert_state)
|
||||||
if interaction_prompt and interaction_prompt.has_method("set_insert_active"):
|
if interaction_prompt and interaction_prompt.has_method("set_insert_active"):
|
||||||
interaction_prompt.set_insert_active(insert_state)
|
interaction_prompt.set_insert_active(insert_state)
|
||||||
|
if minimap and minimap.has_method("set_insert_active"):
|
||||||
|
minimap.set_insert_active(insert_state)
|
||||||
|
|
||||||
# D-057: Update interaction list from game state
|
# D-057: Update interaction list from game state
|
||||||
# Suppress during dialogue — player is in conversation, verb list is noise
|
# Suppress during dialogue — player is in conversation, verb list is noise
|
||||||
@@ -136,6 +141,10 @@ func _process(delta: float) -> void:
|
|||||||
if time_display and time_display.has_method("update_from_state"):
|
if time_display and time_display.has_method("update_from_state"):
|
||||||
time_display.update_from_state()
|
time_display.update_from_state()
|
||||||
|
|
||||||
|
# #264: Update journal panel — auto-close on dialogue, refresh if open
|
||||||
|
if journal_panel and journal_panel.has_method("update_from_state"):
|
||||||
|
journal_panel.update_from_state()
|
||||||
|
|
||||||
# #511: Update debug overlay (F3 toggle, dev tool)
|
# #511: Update debug overlay (F3 toggle, dev tool)
|
||||||
if debug_overlay and debug_overlay.has_method("update_from_state"):
|
if debug_overlay and debug_overlay.has_method("update_from_state"):
|
||||||
debug_overlay.update_from_state()
|
debug_overlay.update_from_state()
|
||||||
@@ -150,6 +159,9 @@ func _process(delta: float) -> void:
|
|||||||
# Only activates when no dialogue/confrontation dip is active (D-070).
|
# Only activates when no dialogue/confrontation dip is active (D-070).
|
||||||
_update_listening_focus()
|
_update_listening_focus()
|
||||||
|
|
||||||
|
# #174: Show examine result if server sent one this tick (#242)
|
||||||
|
_consume_examine_result()
|
||||||
|
|
||||||
# Show monologue if server sent one this tick (#414)
|
# Show monologue if server sent one this tick (#414)
|
||||||
_consume_monologue()
|
_consume_monologue()
|
||||||
|
|
||||||
@@ -184,6 +196,10 @@ func _process(delta: float) -> void:
|
|||||||
if bug_report_dialog and not bug_report_dialog.is_active():
|
if bug_report_dialog and not bug_report_dialog.is_active():
|
||||||
bug_report_dialog.start_capture()
|
bug_report_dialog.start_capture()
|
||||||
continue
|
continue
|
||||||
|
# #264: J — client-only, toggle knowledge journal panel
|
||||||
|
if input.action == InputMapper.Action.OPEN_JOURNAL:
|
||||||
|
_toggle_journal()
|
||||||
|
continue
|
||||||
# #528: ESC/OPEN_MENU — client-only, toggle audio settings dialog
|
# #528: ESC/OPEN_MENU — client-only, toggle audio settings dialog
|
||||||
if input.action == InputMapper.Action.OPEN_MENU:
|
if input.action == InputMapper.Action.OPEN_MENU:
|
||||||
if settings_dialog:
|
if settings_dialog:
|
||||||
@@ -318,6 +334,9 @@ func _consume_dialogue() -> void:
|
|||||||
GameState.current_dialogue = null
|
GameState.current_dialogue = null
|
||||||
return
|
return
|
||||||
_last_dialogue_tick = GameState.current_tick
|
_last_dialogue_tick = GameState.current_tick
|
||||||
|
# #264: Close journal when dialogue opens (cannot be open simultaneously)
|
||||||
|
if journal_panel and journal_panel.has_method("close"):
|
||||||
|
journal_panel.close()
|
||||||
var dlg: Dictionary = GameState.current_dialogue
|
var dlg: Dictionary = GameState.current_dialogue
|
||||||
_last_dialogue_npc_id = dlg.get("npc_entity_id", -1)
|
_last_dialogue_npc_id = dlg.get("npc_entity_id", -1)
|
||||||
_last_dialogue_npc_name = dlg.get("npc_name", "")
|
_last_dialogue_npc_name = dlg.get("npc_name", "")
|
||||||
@@ -456,6 +475,33 @@ func _teleport_transition() -> void:
|
|||||||
tween.tween_callback(_flash_rect.queue_free)
|
tween.tween_callback(_flash_rect.queue_free)
|
||||||
|
|
||||||
|
|
||||||
|
# #174: Consume examine result — show overlay when server sends character-filtered observation.
|
||||||
|
# Clears after display (single-consume). Dismiss examine when dialogue opens.
|
||||||
|
func _consume_examine_result() -> void:
|
||||||
|
if GameState.current_examine_result == null or not examine_display:
|
||||||
|
return
|
||||||
|
var result: Dictionary = GameState.current_examine_result
|
||||||
|
# Dismiss existing examine result if dialogue is active (focus priority)
|
||||||
|
if dialogue_box and dialogue_box.is_dialogue_active():
|
||||||
|
if examine_display.has_method("dismiss"):
|
||||||
|
examine_display.dismiss()
|
||||||
|
else:
|
||||||
|
if examine_display.has_method("show_result"):
|
||||||
|
examine_display.show_result(result)
|
||||||
|
GameState.current_examine_result = null
|
||||||
|
|
||||||
|
|
||||||
|
# #264: Toggle journal panel. Called from input handler when J key pressed.
|
||||||
|
func _toggle_journal() -> void:
|
||||||
|
if not journal_panel:
|
||||||
|
return
|
||||||
|
# Journal and dialogue cannot be open simultaneously (sprint briefing)
|
||||||
|
if dialogue_box and dialogue_box.is_dialogue_active():
|
||||||
|
return
|
||||||
|
if journal_panel.has_method("toggle"):
|
||||||
|
journal_panel.toggle()
|
||||||
|
|
||||||
|
|
||||||
# #502: Full-screen color flash — fades from color to transparent over duration.
|
# #502: Full-screen color flash — fades from color to transparent over duration.
|
||||||
# Used for room reset amber flash. Creates ephemeral ColorRect on UILayer.
|
# Used for room reset amber flash. Creates ephemeral ColorRect on UILayer.
|
||||||
func _screen_flash(color: Color, duration: float) -> void:
|
func _screen_flash(color: Color, duration: float) -> void:
|
||||||
|
|||||||
+44
-18
@@ -354,26 +354,45 @@ func _show_options(options: Array) -> void:
|
|||||||
var raw_text: String = opt.get("text", "")
|
var raw_text: String = opt.get("text", "")
|
||||||
var is_confrontation: bool = opt.get("confrontation", false)
|
var is_confrontation: bool = opt.get("confrontation", false)
|
||||||
|
|
||||||
var label := Label.new()
|
# D-063: Confrontation options render italic — first-person voice, weighted differently.
|
||||||
label.add_theme_font_size_override("font_size", 14)
|
# Use RichTextLabel with BBCode [i] tags for confrontation; plain Label for standard.
|
||||||
label.add_theme_color_override("font_color", Constants.INSERT_COLOR_TEXT)
|
var ctrl: Control
|
||||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
if is_confrontation:
|
||||||
label.mouse_filter = Control.MOUSE_FILTER_STOP
|
var rtl := RichTextLabel.new()
|
||||||
label.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
rtl.bbcode_enabled = true
|
||||||
|
rtl.fit_content = true
|
||||||
var numbered_text := "%d. %s" % [i + 1, raw_text]
|
rtl.scroll_active = false
|
||||||
label.text = numbered_text
|
rtl.add_theme_font_size_override("normal_font_size", 14)
|
||||||
|
rtl.add_theme_color_override("default_color", Color(
|
||||||
|
Constants.INSERT_COLOR_TEXT.r * 1.08,
|
||||||
|
Constants.INSERT_COLOR_TEXT.g * 0.96,
|
||||||
|
Constants.INSERT_COLOR_TEXT.b * 0.90,
|
||||||
|
1.0
|
||||||
|
)) # Slight warm tint for confrontation weight
|
||||||
|
rtl.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
rtl.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||||
|
rtl.text = "[i]%d. %s[/i]" % [i + 1, raw_text]
|
||||||
|
ctrl = rtl
|
||||||
|
else:
|
||||||
|
var label := Label.new()
|
||||||
|
label.add_theme_font_size_override("font_size", 14)
|
||||||
|
label.add_theme_color_override("font_color", Constants.INSERT_COLOR_TEXT)
|
||||||
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
label.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
label.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||||
|
label.text = "%d. %s" % [i + 1, raw_text]
|
||||||
|
ctrl = label
|
||||||
|
|
||||||
var idx := i
|
var idx := i
|
||||||
label.gui_input.connect(func(event: InputEvent):
|
ctrl.gui_input.connect(func(event: InputEvent):
|
||||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
_on_option_pressed(idx)
|
_on_option_pressed(idx)
|
||||||
)
|
)
|
||||||
label.mouse_entered.connect(_make_hover_on(label))
|
ctrl.mouse_entered.connect(_make_hover_on(ctrl))
|
||||||
label.mouse_exited.connect(_make_hover_off(label))
|
ctrl.mouse_exited.connect(_make_hover_off(ctrl))
|
||||||
|
|
||||||
options_container.add_child(label)
|
options_container.add_child(ctrl)
|
||||||
_option_controls.append(label)
|
_option_controls.append(ctrl)
|
||||||
_option_response_ids.append(opt.get("response_id", ""))
|
_option_response_ids.append(opt.get("response_id", ""))
|
||||||
_option_texts.append(raw_text)
|
_option_texts.append(raw_text)
|
||||||
_option_is_confrontation.append(is_confrontation)
|
_option_is_confrontation.append(is_confrontation)
|
||||||
@@ -623,11 +642,18 @@ func _clear_options() -> void:
|
|||||||
|
|
||||||
|
|
||||||
# Hover callbacks
|
# Hover callbacks
|
||||||
static func _make_hover_on(label: Control) -> Callable:
|
static func _make_hover_on(ctrl: Control) -> Callable:
|
||||||
return func():
|
return func():
|
||||||
label.add_theme_color_override("font_color", Constants.INSERT_COLOR_HOVER)
|
# RichTextLabel uses "default_color"; Label uses "font_color"
|
||||||
|
if ctrl is RichTextLabel:
|
||||||
|
ctrl.add_theme_color_override("default_color", Constants.INSERT_COLOR_HOVER)
|
||||||
|
else:
|
||||||
|
ctrl.add_theme_color_override("font_color", Constants.INSERT_COLOR_HOVER)
|
||||||
|
|
||||||
|
|
||||||
static func _make_hover_off(label: Control) -> Callable:
|
static func _make_hover_off(ctrl: Control) -> Callable:
|
||||||
return func():
|
return func():
|
||||||
label.add_theme_color_override("font_color", Constants.INSERT_COLOR_TEXT)
|
if ctrl is RichTextLabel:
|
||||||
|
ctrl.add_theme_color_override("default_color", Constants.INSERT_COLOR_TEXT)
|
||||||
|
else:
|
||||||
|
ctrl.add_theme_color_override("font_color", Constants.INSERT_COLOR_TEXT)
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
## Examine result display — #174, D-061 adjacent.
|
||||||
|
##
|
||||||
|
## Shows the character-filtered text returned by the Examine verb (#242).
|
||||||
|
## Non-interactive overlay. Auto-dismisses after DISMISS_DELAY seconds.
|
||||||
|
## Diegetic: reads as the neural insert processing what the character observed.
|
||||||
|
##
|
||||||
|
## Positioned in InsertOverlay (CanvasLayer 10, z-layer 6).
|
||||||
|
## Only one examine result is shown at a time — new result replaces old.
|
||||||
|
|
||||||
|
const DISMISS_DELAY: float = 5.0 # Auto-dismiss after 5 seconds
|
||||||
|
const FADE_IN: float = 0.18
|
||||||
|
const FADE_OUT: float = 0.35
|
||||||
|
|
||||||
|
# Confidence → alpha modifier: Direct is brightest, Suspects is dimmest
|
||||||
|
const CONFIDENCE_ALPHA: Dictionary = {
|
||||||
|
"Direct": 1.0,
|
||||||
|
"KnowsDetails": 0.9,
|
||||||
|
"KnowsOf": 0.75,
|
||||||
|
"Suspects": 0.6,
|
||||||
|
}
|
||||||
|
|
||||||
|
@onready var panel: PanelContainer = $PanelContainer
|
||||||
|
@onready var text_label: RichTextLabel = $PanelContainer/MarginContainer/TextLabel
|
||||||
|
|
||||||
|
var _dismiss_tween: Tween = null
|
||||||
|
var _active: bool = false
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
modulate.a = 0.0
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
|
||||||
|
## Show an examine result. Called from main.gd when GameState.current_examine_result is set.
|
||||||
|
## result: {entity_id, text, confidence}
|
||||||
|
func show_result(result: Dictionary) -> void:
|
||||||
|
var text: String = result.get("text", "")
|
||||||
|
var confidence: String = result.get("confidence", "KnowsOf")
|
||||||
|
|
||||||
|
if text.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Cancel any in-progress dismiss
|
||||||
|
if _dismiss_tween and _dismiss_tween.is_valid():
|
||||||
|
_dismiss_tween.kill()
|
||||||
|
|
||||||
|
# Apply confidence-based alpha to the insert color
|
||||||
|
var alpha: float = CONFIDENCE_ALPHA.get(confidence, 0.75)
|
||||||
|
var col := Color(Constants.INSERT_COLOR_TEXT.r, Constants.INSERT_COLOR_TEXT.g,
|
||||||
|
Constants.INSERT_COLOR_TEXT.b, alpha)
|
||||||
|
text_label.add_theme_color_override("default_color", col)
|
||||||
|
text_label.text = text
|
||||||
|
|
||||||
|
visible = true
|
||||||
|
_active = true
|
||||||
|
modulate.a = 0.0
|
||||||
|
|
||||||
|
# Fade in, then auto-dismiss
|
||||||
|
_dismiss_tween = create_tween()
|
||||||
|
_dismiss_tween.tween_property(self, "modulate:a", 1.0, FADE_IN)
|
||||||
|
_dismiss_tween.tween_interval(DISMISS_DELAY)
|
||||||
|
_dismiss_tween.tween_callback(_start_fade_out)
|
||||||
|
|
||||||
|
|
||||||
|
func _start_fade_out() -> void:
|
||||||
|
if not _active:
|
||||||
|
return
|
||||||
|
var t := create_tween()
|
||||||
|
t.tween_property(self, "modulate:a", 0.0, FADE_OUT)
|
||||||
|
t.tween_callback(func():
|
||||||
|
visible = false
|
||||||
|
_active = false
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Dismiss immediately (e.g. when dialogue opens).
|
||||||
|
func dismiss() -> void:
|
||||||
|
if not _active:
|
||||||
|
return
|
||||||
|
if _dismiss_tween and _dismiss_tween.is_valid():
|
||||||
|
_dismiss_tween.kill()
|
||||||
|
_start_fade_out()
|
||||||
|
|
||||||
|
|
||||||
|
func is_active() -> bool:
|
||||||
|
return _active
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://examine_display_sr"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://ui/examine_display.gd" id="1_examine"]
|
||||||
|
|
||||||
|
; ExamineDisplay — non-interactive observe result overlay. D-013, #174.
|
||||||
|
; Auto-dismisses after 5s. Positioned center-right, 40% from top.
|
||||||
|
; InsertOverlay (CanvasLayer 10). Diegetic: insert processing observed data.
|
||||||
|
|
||||||
|
[node name="ExamineDisplay" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 3
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_top = 0.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 0.0
|
||||||
|
offset_left = -440.0
|
||||||
|
offset_top = 120.0
|
||||||
|
offset_right = -16.0
|
||||||
|
offset_bottom = 240.0
|
||||||
|
grow_horizontal = 0
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
modulate = Color(1, 1, 1, 0)
|
||||||
|
script = ExtResource("1_examine")
|
||||||
|
|
||||||
|
[node name="PanelContainer" type="PanelContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/margin_left = 12
|
||||||
|
theme_override_constants/margin_top = 8
|
||||||
|
theme_override_constants/margin_right = 12
|
||||||
|
theme_override_constants/margin_bottom = 8
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="TextLabel" type="RichTextLabel" parent="PanelContainer/MarginContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
bbcode_enabled = true
|
||||||
|
fit_content = true
|
||||||
|
scroll_active = false
|
||||||
|
mouse_filter = 2
|
||||||
|
theme_override_font_sizes/normal_font_size = 13
|
||||||
|
theme_override_colors/default_color = Color(0.784, 0.816, 0.878, 0.75)
|
||||||
Reference in New Issue
Block a user