diff --git a/client/project.godot b/client/project.godot index 8aa99cf2f..479146b4c 100644 --- a/client/project.godot +++ b/client/project.godot @@ -125,6 +125,11 @@ debug_overlay={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194334,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +open_journal={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":74,"key_label":0,"unicode":106,"location":0,"echo":false,"script":null) +] +} teleport_hub={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194317,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) diff --git a/client/scripts/autoloads/input_mapper.gd b/client/scripts/autoloads/input_mapper.gd index 9a51eee07..37957efd0 100644 --- a/client/scripts/autoloads/input_mapper.gd +++ b/client/scripts/autoloads/input_mapper.gd @@ -19,6 +19,7 @@ enum Action { INTERACT, USE_PERCEPTION_MODE, OPEN_MENU, PAUSE, UNPAUSE, TOGGLE_STANCE_UP, TOGGLE_STANCE_DOWN, BUG_REPORT, # #495: F12 WRONG button — client-only, not sent to server + OPEN_JOURNAL, # #264: J key — toggle knowledge journal panel, client-only SET_FACING, # D-054: facing octant update (no movement) TELEPORT_HUB, # #501: Home key — Gauntlet dev teleport (not production fast-travel) } @@ -106,6 +107,8 @@ func _unhandled_input(event: InputEvent) -> void: action = Action.TOGGLE_STANCE_DOWN elif event.is_action_pressed("bug_report"): action = Action.BUG_REPORT + elif event.is_action_pressed("open_journal"): + action = Action.OPEN_JOURNAL elif event.is_action_pressed("teleport_hub"): if GameState.gauntlet_mode: action = Action.TELEPORT_HUB diff --git a/client/ui/journal_panel.gd b/client/ui/journal_panel.gd new file mode 100644 index 000000000..2f39ee02e --- /dev/null +++ b/client/ui/journal_panel.gd @@ -0,0 +1,194 @@ +extends Control + +## Journal panel — knowledge graph review. #264, D-041. +## +## Toggle with J key. Displays accumulated facts from GameState.player_knowledge +## grouped by entity. Read-only — no player interaction beyond scrolling. +## +## KnowledgeState rendering: +## Active → normal color (INSERT_COLOR_TEXT) +## Stale → dimmed (IMPLANT_TEXT_DIM) +## Contradicted → amber tint + strikethrough (ENTITY_COLOR_POI) — THE FRIEND arc surface +## +## KnowledgeConfidence labels (D-041): +## Direct → KnowsDetails → KnowsOf → Suspects +## +## Cannot be open simultaneously with dialogue (sprint briefing constraint). +## Closes when dialogue opens. + +const FADE_IN: float = 0.18 +const FADE_OUT: float = 0.25 + +# D-041 confidence → display label +const CONFIDENCE_LABELS: Dictionary = { + "Direct": "Confirmed", + "KnowsDetails": "Detailed", + "KnowsOf": "Known", + "Suspects": "Unconfirmed", +} + +# D-041 source → display label +const SOURCE_LABELS: Dictionary = { + "DirectObservation": "Observed", + "ToldBy": "Told", + "Heard": "Overheard", + "Inferred": "Inferred", + "Background": "Prior", +} + +@onready var panel: PanelContainer = $PanelContainer +@onready var title_label: Label = $PanelContainer/MarginContainer/VBoxContainer/TitleLabel +@onready var scroll: ScrollContainer = $PanelContainer/MarginContainer/VBoxContainer/ScrollContainer +@onready var entries_container: VBoxContainer = $PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/EntriesContainer + +var _visible_state: bool = false +var _last_rendered_tick: int = -1 + +func _ready() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE + visible = false + modulate.a = 0.0 + title_label.text = UIStrings.get_text("knowledge_panel.tab_contacts") + + +## Toggle open/close. Called from main.gd on J key press. +func toggle() -> void: + if _visible_state: + _hide_panel() + else: + _show_panel() + + +## Force-close. Called when dialogue opens. +func close() -> void: + if _visible_state: + _hide_panel() + + +func is_open() -> bool: + return _visible_state + + +func _show_panel() -> void: + _rebuild_entries() + _visible_state = true + visible = true + var t := create_tween() + t.tween_property(self, "modulate:a", 1.0, FADE_IN) + + +func _hide_panel() -> void: + _visible_state = false + var t := create_tween() + t.tween_property(self, "modulate:a", 0.0, FADE_OUT) + t.tween_callback(func(): visible = false) + + +## Rebuild the entry list from GameState.player_knowledge. +func _rebuild_entries() -> void: + # Clear previous entries + for child in entries_container.get_children(): + child.queue_free() + + var knowledge: Variant = GameState.player_knowledge + if knowledge == null: + _add_empty_state() + return + + var entities: Array = knowledge.get("entities", []) + if entities.is_empty(): + _add_empty_state() + return + + for entity in entities: + if not entity is Dictionary: + continue + _add_entity_entry(entity) + + +func _add_empty_state() -> void: + var label := Label.new() + label.text = UIStrings.get_text("knowledge_panel.empty_state") + label.add_theme_color_override("font_color", Constants.IMPLANT_TEXT_DIM) + label.add_theme_font_size_override("font_size", 13) + entries_container.add_child(label) + + +func _add_entity_entry(entity: Dictionary) -> void: + var name_str: String = entity.get("name", "Unknown") + var confidence: String = entity.get("confidence", "Suspects") + var source: String = entity.get("source", "") + var state: String = entity.get("state", "Active") + var relationship: String = entity.get("relationship", "Unknown") + + # Entity header — name + relationship status + var header_rtl := RichTextLabel.new() + header_rtl.bbcode_enabled = true + header_rtl.fit_content = true + header_rtl.scroll_active = false + header_rtl.mouse_filter = Control.MOUSE_FILTER_IGNORE + header_rtl.add_theme_font_size_override("normal_font_size", 14) + + var rel_color: Color = Constants.color_for_relationship(relationship) + var rel_label: String = UIStrings.get_text("relationship_states.%s.label" % relationship.to_lower()) + if rel_label == "relationship_states.%s.label" % relationship.to_lower(): + rel_label = relationship # fallback if key missing + + var state_color: Color = _state_color(state) + var conf_label: String = CONFIDENCE_LABELS.get(confidence, confidence) + var src_label: String = SOURCE_LABELS.get(source, source) + + # Build BBCode: + # [color=#hex][b]Name[/b][/color] [color=#rel_hex]Status[/color] + var name_hex: String = state_color.to_html(false) + var rel_hex: String = rel_color.to_html(false) + + var header_bbcode: String + if state == "Contradicted": + header_bbcode = "[color=#%s][b][s]%s[/s][/b][/color] [color=#%s]%s[/color]" % [ + name_hex, name_str, rel_hex, rel_label + ] + else: + header_bbcode = "[color=#%s][b]%s[/b][/color] [color=#%s]%s[/color]" % [ + name_hex, name_str, rel_hex, rel_label + ] + header_rtl.text = header_bbcode + entries_container.add_child(header_rtl) + + # Detail line — confidence + source + var detail_rtl := RichTextLabel.new() + detail_rtl.bbcode_enabled = true + detail_rtl.fit_content = true + detail_rtl.scroll_active = false + detail_rtl.mouse_filter = Control.MOUSE_FILTER_IGNORE + detail_rtl.add_theme_font_size_override("normal_font_size", 12) + + var detail_hex: String = Constants.IMPLANT_TEXT_DIM.to_html(false) + var detail_text: String = "%s · %s" % [conf_label, src_label] if src_label else conf_label + detail_rtl.text = "[color=#%s]%s[/color]" % [detail_hex, detail_text] + entries_container.add_child(detail_rtl) + + # Spacer between entries + var spacer := Control.new() + spacer.custom_minimum_size = Vector2(0, 6) + entries_container.add_child(spacer) + + +func _state_color(state: String) -> Color: + match state: + "Contradicted": + return Constants.ENTITY_COLOR_POI # amber — THE FRIEND arc + "Stale": + return Constants.IMPLANT_TEXT_DIM # dimmed + _: + return Constants.INSERT_COLOR_TEXT # active — normal + + +## Called from main.gd on each snapshot to close the panel when dialogue opens. +func update_from_state() -> void: + if _visible_state and GameState.dialogue_active: + close() + # Rebuild if open and knowledge data is newer than last render + if _visible_state and GameState.current_tick != _last_rendered_tick: + _last_rendered_tick = GameState.current_tick + _rebuild_entries() diff --git a/client/ui/journal_panel.tscn b/client/ui/journal_panel.tscn new file mode 100644 index 000000000..9eedb4da9 --- /dev/null +++ b/client/ui/journal_panel.tscn @@ -0,0 +1,63 @@ +[gd_scene load_steps=2 format=3 uid="uid://journal_panel_sr"] + +[ext_resource type="Script" path="res://ui/journal_panel.gd" id="1_journal"] + +; JournalPanel — knowledge graph review. #264, D-041. +; Toggle J key. Right side, 380px wide, full height minus margins. +; InsertOverlay (CanvasLayer 10, z-layer 6). Read-only, closes with dialogue. + +[node name="JournalPanel" type="Control"] +layout_mode = 3 +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 0.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -400.0 +offset_top = 16.0 +offset_right = -16.0 +offset_bottom = -16.0 +grow_horizontal = 0 +grow_vertical = 2 +mouse_filter = 2 +modulate = Color(1, 1, 1, 0) +script = ExtResource("1_journal") + +[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 = 14 +theme_override_constants/margin_top = 12 +theme_override_constants/margin_right = 14 +theme_override_constants/margin_bottom = 12 + +[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer"] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="TitleLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 +text = "Contacts" +theme_override_font_sizes/font_size = 15 +theme_override_colors/font_color = Color(0.784, 0.816, 0.878, 0.9) + +[node name="Separator" type="HSeparator" parent="PanelContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 +theme_override_colors/separator_color = Color(0.784, 0.816, 0.878, 0.2) + +[node name="ScrollContainer" type="ScrollContainer" parent="PanelContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="EntriesContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 2