diff --git a/client/scripts/main.gd b/client/scripts/main.gd index 9c68ba529..387ab937e 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -15,6 +15,9 @@ extends Node2D @onready var gauntlet_hud = $UILayer/GauntletHUD # #496: room timer + personal bests @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 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 bug_report_dialog = $ModalLayer/BugReportDialog # #495: F12 WRONG button @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) if interaction_prompt and interaction_prompt.has_method("set_insert_active"): 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 # 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"): 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) if debug_overlay and debug_overlay.has_method("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). _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) _consume_monologue() @@ -184,6 +196,10 @@ func _process(delta: float) -> void: if bug_report_dialog and not bug_report_dialog.is_active(): bug_report_dialog.start_capture() 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 if input.action == InputMapper.Action.OPEN_MENU: if settings_dialog: @@ -318,6 +334,9 @@ func _consume_dialogue() -> void: GameState.current_dialogue = null return _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 _last_dialogue_npc_id = dlg.get("npc_entity_id", -1) _last_dialogue_npc_name = dlg.get("npc_name", "") @@ -456,6 +475,33 @@ func _teleport_transition() -> void: 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. # Used for room reset amber flash. Creates ephemeral ColorRect on UILayer. func _screen_flash(color: Color, duration: float) -> void: diff --git a/client/ui/dialogue_box.gd b/client/ui/dialogue_box.gd index bc79227fe..90d7885cf 100644 --- a/client/ui/dialogue_box.gd +++ b/client/ui/dialogue_box.gd @@ -354,26 +354,45 @@ func _show_options(options: Array) -> void: var raw_text: String = opt.get("text", "") var is_confrontation: bool = opt.get("confrontation", false) - 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 - - var numbered_text := "%d. %s" % [i + 1, raw_text] - label.text = numbered_text + # D-063: Confrontation options render italic — first-person voice, weighted differently. + # Use RichTextLabel with BBCode [i] tags for confrontation; plain Label for standard. + var ctrl: Control + if is_confrontation: + var rtl := RichTextLabel.new() + rtl.bbcode_enabled = true + rtl.fit_content = true + rtl.scroll_active = false + 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 - 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: _on_option_pressed(idx) ) - label.mouse_entered.connect(_make_hover_on(label)) - label.mouse_exited.connect(_make_hover_off(label)) + ctrl.mouse_entered.connect(_make_hover_on(ctrl)) + ctrl.mouse_exited.connect(_make_hover_off(ctrl)) - options_container.add_child(label) - _option_controls.append(label) + options_container.add_child(ctrl) + _option_controls.append(ctrl) _option_response_ids.append(opt.get("response_id", "")) _option_texts.append(raw_text) _option_is_confrontation.append(is_confrontation) @@ -623,11 +642,18 @@ func _clear_options() -> void: # Hover callbacks -static func _make_hover_on(label: Control) -> Callable: +static func _make_hover_on(ctrl: Control) -> Callable: 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(): - 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) diff --git a/client/ui/examine_display.gd b/client/ui/examine_display.gd new file mode 100644 index 000000000..3606e20e7 --- /dev/null +++ b/client/ui/examine_display.gd @@ -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 diff --git a/client/ui/examine_display.tscn b/client/ui/examine_display.tscn new file mode 100644 index 000000000..6cc2b9f5d --- /dev/null +++ b/client/ui/examine_display.tscn @@ -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)