[agent:claude-cli] feat(save): save/load automatique (30s + mort) — user://save.json, persist XP/level/inventory/achievements/position, autoload SaveManager

This commit is contained in:
2026-04-21 08:40:22 +00:00
parent 2c49e0c9db
commit 6cb5925da8
4 changed files with 148 additions and 0 deletions

View File

@@ -102,6 +102,7 @@ PlayerProgress="*res://scripts/progression/PlayerProgress.gd"
QuestManager="*res://scripts/progression/QuestManager.gd"
AchievementManager="*res://scripts/progression/AchievementManager.gd"
ComboTracker="*res://scripts/progression/ComboTracker.gd"
SaveManager="*res://scripts/progression/SaveManager.gd"
[rendering]
environment/defaults/default_clear_color=Color(0.05, 0.15, 0.25, 1)

View File

@@ -86,6 +86,12 @@ func _spawn_local_dolphin() -> void:
dolphin.add_to_group("player")
# Save system
var save_mgr: Node = get_node_or_null("/root/SaveManager")
if save_mgr != null:
save_mgr.register_player(dolphin)
save_mgr.load_game()
var ui_scene: PackedScene = load("res://scenes/InventoryUI.tscn")
if ui_scene != null:
_inventory_ui = ui_scene.instantiate() as CanvasLayer

View File

@@ -75,6 +75,16 @@ func has_items(requirements: Array) -> bool:
return true
func get_all_slots() -> Array:
return slots.duplicate(true)
func load_slots(saved_slots: Array) -> void:
for i: int in range(min(saved_slots.size(), TOTAL_SLOTS)):
slots[i] = saved_slots[i]
inventory_changed.emit()
func consume_items(requirements: Array) -> bool:
if not has_items(requirements):
return false

View File

@@ -0,0 +1,131 @@
extends Node
## SaveManager — autoload singleton
## Sauvegarde automatique (toutes les 30s + à la mort).
## Persiste : XP, niveau, inventaire, achievements débloqués, position spawn.
const SAVE_PATH := "user://save.json"
const AUTOSAVE_INTERVAL: float = 30.0
var _autosave_timer: float = 0.0
var _player: Node = null
func _ready() -> void:
# Connect mort du joueur dès que le nœud player existe
# (on le cherche au premier process car le joueur est spawné après)
pass
func register_player(player: Node) -> void:
_player = player
if player.has_signal("player_died"):
if not player.player_died.is_connected(_on_player_died):
player.player_died.connect(_on_player_died)
func _process(delta: float) -> void:
_autosave_timer += delta
if _autosave_timer >= AUTOSAVE_INTERVAL:
_autosave_timer = 0.0
save_game()
func save_game() -> void:
var data: Dictionary = {}
# --- PlayerProgress ---
var pp: Node = get_node_or_null("/root/PlayerProgress")
if pp != null:
data["xp"] = pp.current_xp
data["level"] = pp.level
# --- Inventory ---
var main: Node = get_tree().get_first_node_in_group("main")
if main != null and main.get("inventory") != null:
var inv: Node = main.inventory
var slots: Array = []
if inv.has_method("get_all_slots"):
slots = inv.call("get_all_slots")
else:
# Fallback: read slots array directly if exposed
if "slots" in inv:
for s in inv.slots:
slots.append(s)
data["inventory"] = slots
# --- Achievements ---
var am: Node = get_node_or_null("/root/AchievementManager")
if am != null and am.get("_unlocked") != null:
data["achievements"] = am._unlocked
# --- Spawn position ---
if is_instance_valid(_player):
var pos: Vector3 = _player.global_position
data["spawn"] = {"x": pos.x, "y": pos.y, "z": pos.z}
# Write JSON
var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if file == null:
push_warning("SaveManager: impossible d'ouvrir " + SAVE_PATH)
return
file.store_string(JSON.stringify(data, "\t"))
file.close()
func load_game() -> bool:
if not FileAccess.file_exists(SAVE_PATH):
return false
var file := FileAccess.open(SAVE_PATH, FileAccess.READ)
if file == null:
return false
var raw: String = file.get_as_text()
file.close()
var parsed = JSON.parse_string(raw)
if parsed == null or not (parsed is Dictionary):
push_warning("SaveManager: save corrompu")
return false
var data: Dictionary = parsed
# --- PlayerProgress ---
var pp: Node = get_node_or_null("/root/PlayerProgress")
if pp != null:
if data.has("xp"):
pp.current_xp = int(data["xp"])
if data.has("level"):
pp.level = int(data["level"])
pp.emit_state()
# --- Achievements ---
var am: Node = get_node_or_null("/root/AchievementManager")
if am != null and data.has("achievements"):
am._unlocked = data["achievements"]
# --- Inventory ---
var main: Node = get_tree().get_first_node_in_group("main")
if main != null and main.get("inventory") != null and data.has("inventory"):
var inv: Node = main.inventory
var slots: Array = data["inventory"]
if inv.has_method("load_slots"):
inv.call("load_slots", slots)
elif "slots" in inv:
# Best-effort direct assignment
for i: int in range(min(slots.size(), inv.slots.size())):
inv.slots[i] = slots[i]
# --- Spawn position ---
if is_instance_valid(_player) and data.has("spawn"):
var sp: Dictionary = data["spawn"]
_player.global_position = Vector3(
float(sp.get("x", 0.0)),
float(sp.get("y", 55.0)),
float(sp.get("z", 0.0))
)
return true
func _on_player_died() -> void:
save_game()