Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
223 lines
7.3 KiB
GDScript
223 lines
7.3 KiB
GDScript
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).
|
|
|
|
var _visible_state: bool = false
|
|
var _last_rendered_tick: int = -1
|
|
|
|
@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
|
|
|
|
|
|
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()
|