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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user