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/D-042: confidence and source labels loaded from UIStrings (data/ui-strings.yaml). # Keys: knowledge_panel.confidence_{lower} and knowledge_panel.source_{lower} # Fallback: raw value if key not found (UIStrings returns the key itself). @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 = UIStrings.get_text("knowledge_panel.confidence_%s" % confidence.to_lower()) var src_label: String = _resolve_source_label(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) ## Resolve a source string from the server into a human-readable label. ## Handles plain keys ("DirectObservation", "Heard") and ToldBy(N) format. ## ToldBy(N) looks up entity_id N in player_knowledge.entities for the name. func _resolve_source_label(source: String) -> String: if source.is_empty(): return "" # ToldBy(entity_id) format — e.g. "ToldBy(12)" if source.begins_with("ToldBy(") and source.ends_with(")"): var id_str: String = source.substr(7, source.length() - 8) var entity_id: int = id_str.to_int() var name := _entity_name_for_id(entity_id) var told_prefix: String = UIStrings.get_text("knowledge_panel.source_toldby") if told_prefix == "knowledge_panel.source_toldby": told_prefix = "Told" return "%s: %s" % [told_prefix, name] # Plain source key — look up UIStrings var key: String = "knowledge_panel.source_%s" % source.to_lower() return UIStrings.get_text(key) ## Look up an entity name by stable ID from GameState.player_knowledge.entities. func _entity_name_for_id(entity_id: int) -> String: var knowledge: Variant = GameState.player_knowledge if knowledge == null: return "#%d" % entity_id for entity in knowledge.get("entities", []): if entity is Dictionary and int(entity.get("entity_id", -1)) == entity_id: return entity.get("name", "#%d" % entity_id) return "#%d" % entity_id 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()