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()