feat(client): save/load client UI — F5/F6 quicksave/quickload (#554)

Wire SaveGame/LoadGame player actions through the full client stack:
protocol v15 decode, InputMapper F5/F6 bindings, SimBridge wire mapping
with one-shot carry-forward, GameState save_result field, and HUD
notification via monologue display. Quit-to-menu triggers quicksave
before scene change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 13:15:05 +01:00
co-authored by Claude Opus 4.6
parent e51650ec74
commit e1ea07e746
10 changed files with 135 additions and 5 deletions
+43
View File
@@ -35,6 +35,8 @@ const _LATTICE_COLORS: Dictionary = {
}
const _FALLBACK_STANDARD: Color = Color("#c8d0e0")
const _FALLBACK_URGENT: Color = Color("#e0e8f8")
const _NOTIFICATION_COLOR: Color = Color("#8890a0") # #554: neutral system notification
const _NOTIFICATION_DURATION: float = 2.5
@onready var _vbox: VBoxContainer = $VBoxContainer
@@ -71,6 +73,18 @@ func _process(delta: float) -> void:
# Empty text is silently ignored — no slot created, no queue entry.
# lattice_profile is read from GameState here and passed down — renderer stays
# decoupled from the autoload (D-020 renderer contract).
# #554: Show a brief system notification (save/load result, connection status).
# Uses neutral color, short duration, bypasses lattice_profile styling.
func show_notification(text: String) -> void:
if text.is_empty():
return
var now := float(Time.get_ticks_msec())
if _visible.size() < MAX_VISIBLE and now >= _next_fade_in_msec:
_show_notification_line(text)
else:
_enqueue(text, _NOTIFICATION_DURATION, 1, false, "")
func show_monologue(text: String, duration: float, priority: int = 2, is_urgent: bool = false) -> void:
if text.is_empty():
return
@@ -86,6 +100,35 @@ func show_monologue(text: String, duration: float, priority: int = 2, is_urgent:
# Internal
# ---------------------------------------------------------------------------
func _show_notification_line(text: String) -> void:
var container := MarginContainer.new()
container.add_theme_constant_override("margin_left", 4)
container.add_theme_constant_override("margin_right", 4)
container.add_theme_constant_override("margin_top", 2)
container.add_theme_constant_override("margin_bottom", 2)
var label := RichTextLabel.new()
label.bbcode_enabled = true
label.fit_content = true
label.scroll_active = false
label.add_theme_font_size_override("normal_font_size", 13)
var safe_text := text.replace("[", "[lb]")
label.text = "[color=#%s]%s[/color]" % [_NOTIFICATION_COLOR.to_html(false), safe_text]
container.add_child(label)
_vbox.add_child(container)
var slot := {
node = container,
expire_timer = _NOTIFICATION_DURATION,
priority = 1,
tween = null,
}
_visible.append(slot)
_next_fade_in_msec = float(Time.get_ticks_msec()) + STAGGER_SEC * 1000.0
container.modulate.a = 0.0
var tween := create_tween()
slot.tween = tween
tween.tween_property(container, "modulate:a", 0.85, FADE_IN_SEC)
func _show_line(text: String, duration: float, priority: int, is_urgent: bool, lattice_profile: String) -> void:
var line_node := _build_line_node(text, is_urgent, lattice_profile)
_vbox.add_child(line_node)