feat(ui): knowledge/journal display panel (#264)

Right-side insert panel toggled with J key. Facts grouped by entity
with confidence, source, and state metadata. Contradicted entries
in amber with strikethrough (THE FRIEND arc surface). Stale entries
dimmed. Mutual exclusion with dialogue box. All labels via UIStrings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 02:30:04 +01:00
co-authored by Claude Opus 4.6
parent 9b80ff69c3
commit 86ff83d927
4 changed files with 265 additions and 0 deletions
+5
View File
@@ -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)
+3
View File
@@ -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
+194
View File
@@ -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()
+63
View File
@@ -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