[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"
|
QuestManager="*res://scripts/progression/QuestManager.gd"
|
||||||
AchievementManager="*res://scripts/progression/AchievementManager.gd"
|
AchievementManager="*res://scripts/progression/AchievementManager.gd"
|
||||||
ComboTracker="*res://scripts/progression/ComboTracker.gd"
|
ComboTracker="*res://scripts/progression/ComboTracker.gd"
|
||||||
|
SaveManager="*res://scripts/progression/SaveManager.gd"
|
||||||
|
|
||||||
[rendering]
|
[rendering]
|
||||||
environment/defaults/default_clear_color=Color(0.05, 0.15, 0.25, 1)
|
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")
|
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")
|
var ui_scene: PackedScene = load("res://scenes/InventoryUI.tscn")
|
||||||
if ui_scene != null:
|
if ui_scene != null:
|
||||||
_inventory_ui = ui_scene.instantiate() as CanvasLayer
|
_inventory_ui = ui_scene.instantiate() as CanvasLayer
|
||||||
|
|||||||
@@ -75,6 +75,16 @@ func has_items(requirements: Array) -> bool:
|
|||||||
return true
|
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:
|
func consume_items(requirements: Array) -> bool:
|
||||||
if not has_items(requirements):
|
if not has_items(requirements):
|
||||||
return false
|
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