diff --git a/project.godot b/project.godot index 5e45ed0..c35b1e9 100644 --- a/project.godot +++ b/project.godot @@ -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) diff --git a/scripts/Main.gd b/scripts/Main.gd index aee2ee2..2028aeb 100644 --- a/scripts/Main.gd +++ b/scripts/Main.gd @@ -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 diff --git a/scripts/inventory/Inventory.gd b/scripts/inventory/Inventory.gd index 5e615da..0798ff8 100644 --- a/scripts/inventory/Inventory.gd +++ b/scripts/inventory/Inventory.gd @@ -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 diff --git a/scripts/progression/SaveManager.gd b/scripts/progression/SaveManager.gd new file mode 100644 index 0000000..db7e466 --- /dev/null +++ b/scripts/progression/SaveManager.gd @@ -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()