[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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
131
scripts/progression/SaveManager.gd
Normal file
131
scripts/progression/SaveManager.gd
Normal 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()
|
||||
Reference in New Issue
Block a user