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
+11
View File
@@ -70,6 +70,11 @@ var insert_active: bool = true
# Null in v0.1 (server does not yet send this field; protocol change required).
var rng_seed: Variant = null
# v15 fields (#554, D-085): save/load result from server.
# {success: bool, kind: "save"|"load", error: Variant} or null.
# One-shot: consumed by main.gd after display, then set back to null.
var save_result: Variant = null
# v7 fields (#431, D-059/D-060)
var pending_recognitions: Array = [] # [{entity_id, x, y, z, remaining_ticks, total_delay_ticks}]
@@ -279,6 +284,12 @@ func apply_snapshot(snapshot: Dictionary) -> void:
else:
current_examine_result = null
# v15: save_result (#554, D-085) — one-shot save/load confirmation from server.
if snapshot.has("save_result") and snapshot.save_result is Dictionary:
save_result = snapshot.save_result
else:
save_result = null
# v14: player_knowledge (#264, D-041) — partial KG dump for journal panel.
# Only update when field is present (null means no change, server sends when KG changes).
if snapshot.has("player_knowledge") and snapshot.player_knowledge is Dictionary:
+15 -2
View File
@@ -22,6 +22,8 @@ enum Action {
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)
SAVE_GAME, # #554: F5 quicksave — sends SaveGame to server with save path
LOAD_GAME, # #554: F6 quickload — sends LoadGame to server with save path
}
var input_queue: Array[Dictionary] = []
@@ -112,12 +114,23 @@ func _unhandled_input(event: InputEvent) -> void:
elif event.is_action_pressed("teleport_hub"):
if GameState.gauntlet_mode:
action = Action.TELEPORT_HUB
elif event.is_action_pressed("quicksave"):
action = Action.SAVE_GAME
elif event.is_action_pressed("quickload"):
action = Action.LOAD_GAME
if action != -1:
input_queue.append({
var entry := {
"action": action,
"timestamp_msec": Time.get_ticks_msec(),
})
}
# #554: Attach save path for SaveGame/LoadGame actions
if action == Action.SAVE_GAME or action == Action.LOAD_GAME:
var game_id := GameState.current_game_id
if game_id.is_empty():
return # No active session — ignore save/load
entry["action_data"] = {"path": "user://saves/" + game_id + "/quicksave.sav"}
input_queue.append(entry)
get_viewport().set_input_as_handled()
+8 -2
View File
@@ -84,8 +84,14 @@ func quit_to_menu() -> void:
func _do_quit_to_menu() -> void:
_cleanup_quit_dialog()
# #554: F5 quicksave will be triggered here before scene change when server
# supports SaveCommand. For now: navigate to menu without saving.
# #554: Trigger quicksave before navigating to menu.
if not GameState.current_game_id.is_empty():
var path := "user://saves/" + GameState.current_game_id + "/quicksave.sav"
SimBridge.send_input({
"action": InputMapper.Action.SAVE_GAME,
"timestamp_msec": Time.get_ticks_msec(),
"action_data": {"path": path},
})
GameState.current_game_id = ""
get_tree().change_scene_to_file(MENU_SCENE)
+7
View File
@@ -289,6 +289,9 @@ func receive_bytes(bytes: PackedByteArray) -> void:
if old_conv_ended.size() > 0:
var new_conv_ended: Array = snapshot.get("conversation_ended", [])
snapshot["conversation_ended"] = old_conv_ended + new_conv_ended
# #554: Carry forward save/load result (one-shot, consumed by main.gd)
if snapshot.get("save_result") == null and _last_snapshot.get("save_result") != null:
snapshot["save_result"] = _last_snapshot["save_result"]
_last_snapshot = snapshot
# Drain the outbound buffer. Returns raw input entries for batch encoding.
@@ -326,6 +329,10 @@ static func action_enum_to_wire(action: int) -> String:
return "SetFacing" # D-054: facing octant update (no movement)
InputMapper.Action.TELEPORT_HUB:
return "TeleportToHub" # #501: Gauntlet dev teleport (not production fast-travel)
InputMapper.Action.SAVE_GAME:
return "SaveGame" # #554: F5 quicksave (D-085)
InputMapper.Action.LOAD_GAME:
return "LoadGame" # #554: F6 quickload (D-085)
_:
push_warning("SimBridge: unknown action enum %s" % action)
return ""