feat: savegames save as .tres
feat: savegames restore player position fix: savegames now more robust
This commit is contained in:
parent
bc91204aa2
commit
eefc38fb42
|
|
@ -48,6 +48,9 @@ func pull_save_state(save: SaveGame) -> void:
|
|||
save.sequences_enabled = Scenes.enabled_sequences
|
||||
save.current_room = State.rooms.ADULTHOOD
|
||||
save_game = save
|
||||
|
||||
# Call parent to restore player position
|
||||
super.pull_save_state(save)
|
||||
|
||||
func prepare_transition():
|
||||
for child in %Stations.get_children():
|
||||
|
|
|
|||
|
|
@ -29,14 +29,24 @@ func _ready():
|
|||
card_picker.cards_picked.connect(card_board.populate_board)
|
||||
|
||||
func pull_save_state(save: SaveGame) -> void:
|
||||
save.board_state = card_board.get_save_dict()
|
||||
save.current_room = State.rooms.ADULTHOOD
|
||||
save.mementos_complete = Scenes.completed_sequences
|
||||
#FIXME: fix the bloddy card board loading algorythm
|
||||
#card_board.initialise_from_save(save)
|
||||
|
||||
# Call parent to restore player position
|
||||
super.pull_save_state(save)
|
||||
|
||||
func _on_scene_finished(_id: int, _repeat:bool):
|
||||
await get_tree().create_timer(3).timeout
|
||||
save_room()
|
||||
|
||||
func save_room():
|
||||
# Update board state before saving
|
||||
save_game.board_state = card_board.get_save_dict()
|
||||
save_game.mementos_complete = Scenes.completed_sequences
|
||||
save_game.sequences_enabled = Scenes.enabled_sequences
|
||||
super.save_room()
|
||||
|
||||
func prepare_transition():
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -62,17 +62,26 @@ func _ready():
|
|||
func pull_save_state(save: SaveGame) -> void:
|
||||
save_game = save
|
||||
save_game.current_room = id
|
||||
save_game.board_state = card_board.get_save_dict()
|
||||
|
||||
# Load board state from save first, before overwriting it
|
||||
card_board.initialise_from_save(save_game)
|
||||
|
||||
Scenes.started_sequences = save_game.mementos_complete
|
||||
Scenes.completed_sequences = save_game.mementos_complete
|
||||
|
||||
# Call parent to restore player position
|
||||
super.pull_save_state(save)
|
||||
|
||||
|
||||
func _on_scene_finished(_id: int, _repeat:bool):
|
||||
await get_tree().create_timer(3).timeout
|
||||
save_room()
|
||||
|
||||
func save_room():
|
||||
# Update board state before saving
|
||||
save_game.board_state = card_board.get_save_dict()
|
||||
save_game.mementos_complete = Scenes.completed_sequences
|
||||
super.save_room()
|
||||
|
||||
func prepare_transition():
|
||||
save_room()
|
||||
|
|
|
|||
|
|
@ -1871,7 +1871,7 @@ light_size = 20.0
|
|||
omni_range = 16.8518
|
||||
|
||||
[node name="MaskInteractable" parent="logic" instance=ExtResource("22_ks23q")]
|
||||
transform = Transform3D(-0.8827416, 0, 0.4698562, 0, 1, 0, -0.4698562, 0, -0.8827416, -0.025371574, 0.55708295, 2.5263817)
|
||||
transform = Transform3D(-0.8827416, 0, 0.4698562, 0, 1, 0, -0.4698562, 0, -0.8827416, 0.028929986, 0.58693635, 2.552513)
|
||||
interaction = ExtResource("12_viwxf")
|
||||
|
||||
[node name="MindBoardInteractable" parent="logic" instance=ExtResource("22_ks23q")]
|
||||
|
|
@ -1890,7 +1890,7 @@ interaction = ExtResource("13_v3447")
|
|||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.36574292, 0.099999994, 0.032779038)
|
||||
|
||||
[node name="ClothesInteractable" parent="logic" instance=ExtResource("22_ks23q")]
|
||||
transform = Transform3D(0.7935111, 0, -0.60855323, 0, 1, 0, 0.60855323, 0, 0.7935111, 1.6713148, 1.089737, -0.92289597)
|
||||
transform = Transform3D(0.7935111, 0, -0.60855323, 0, 1, 0, 0.60855323, 0, 0.7935111, 1.6334484, 1.1331886, -0.8914416)
|
||||
interaction = ExtResource("12_x3dlb")
|
||||
|
||||
[node name="Sprite3D" parent="logic/ClothesInteractable/View" index="0"]
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ func start_soundtrack():
|
|||
$VoiceTraining.play(70)
|
||||
|
||||
func _on_scene_starting(scene_id: Scenes.id, _repeat: bool) -> void:
|
||||
|
||||
|
||||
# Handle scene-specific setup (music, chest animation, etc.)
|
||||
match scene_id:
|
||||
Scenes.id.YOUTH_CHILDHOOD:
|
||||
|
|
@ -28,13 +28,13 @@ func _on_scene_starting(scene_id: Scenes.id, _repeat: bool) -> void:
|
|||
|
||||
func _on_scene_finished(scene_id: Scenes.id, _repeat: bool) -> void:
|
||||
print_debug("YouthRoomScenePlayer._on_scene_finished(%s)" % Scenes.id.keys()[scene_id])
|
||||
|
||||
|
||||
match scene_id:
|
||||
Scenes.id.YOUTH_CHILDHOOD:
|
||||
play_backwards("childhood_music")
|
||||
Scenes.id.YOUTH_VOICE_TRAINING:
|
||||
play_backwards("voice_music")
|
||||
|
||||
|
||||
queue("RESET")
|
||||
|
||||
func _play_chest_animation() -> void:
|
||||
|
|
|
|||
|
|
@ -41,7 +41,23 @@ func start_room():
|
|||
pass
|
||||
|
||||
func pull_save_state(_save: SaveGame) -> void:
|
||||
pass
|
||||
# Try to restore player position from save
|
||||
restore_player_from_save(_save)
|
||||
|
||||
## Attempts to find player controller and restore position/rotation from save
|
||||
func restore_player_from_save(save: SaveGame) -> void:
|
||||
var player: PlayerController = null
|
||||
|
||||
# Try to find player controller in common locations
|
||||
if has_node("%PlayerController"):
|
||||
player = get_node("%PlayerController")
|
||||
elif has_node("logic/PlayerController"):
|
||||
player = get_node("logic/PlayerController")
|
||||
|
||||
if player and player is PlayerController:
|
||||
player.restore_from_save(save)
|
||||
else:
|
||||
print_debug("RoomTemplate: Could not find PlayerController to restore position")
|
||||
|
||||
func save_room():
|
||||
save_game.save_to_file(get_tree().root.get_texture())
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
class_name SaveGame extends Resource
|
||||
|
||||
|
||||
var _is_initialised: bool = false
|
||||
|
||||
var current_room_path: String:
|
||||
|
|
@ -38,6 +39,18 @@ var current_room_path: String:
|
|||
childhood_mementos = value
|
||||
if _is_initialised: changed.emit()
|
||||
@export var is_childhood_board_complete: bool = false
|
||||
@export var player_position: Vector3 = Vector3.ZERO:
|
||||
set(value):
|
||||
player_position = value
|
||||
if _is_initialised: changed.emit()
|
||||
@export var player_yaw: float = 0.0:
|
||||
set(value):
|
||||
player_yaw = value
|
||||
if _is_initialised: changed.emit()
|
||||
@export var player_pitch: float = 0.0:
|
||||
set(value):
|
||||
player_pitch = value
|
||||
if _is_initialised: changed.emit()
|
||||
@export var thumbnail: Texture = preload("res://import/interface-elements/empty_save_slot.png"):
|
||||
set(value):
|
||||
thumbnail = value
|
||||
|
|
@ -52,7 +65,7 @@ var current_room_path: String:
|
|||
@export var is_empty: bool = true:
|
||||
get():
|
||||
return not FileAccess.file_exists(filepath) or (current_room == State.rooms.NULL)
|
||||
|
||||
|
||||
@export var save_manually: bool = false:
|
||||
set(val):
|
||||
if val: save_to_file(thumbnail)
|
||||
|
|
@ -67,107 +80,81 @@ func _validate_property(property: Dictionary):
|
|||
if property.name == "is_empty":
|
||||
property.usage |= PROPERTY_USAGE_READ_ONLY
|
||||
|
||||
func _init(initial_filepath = "") -> void:
|
||||
if initial_filepath == "":
|
||||
filepath = "%s/%s.json" % [State.user_saves_path, unique_save_name]
|
||||
elif initial_filepath == "DEBUG":
|
||||
filepath = initial_filepath
|
||||
else:
|
||||
filepath = initial_filepath
|
||||
unique_save_name = initial_filepath.get_file()
|
||||
|
||||
read_save_file()
|
||||
_is_initialised = true
|
||||
|
||||
func read_save_file() -> void:
|
||||
if not DirAccess.dir_exists_absolute(filepath.get_base_dir()):
|
||||
DirAccess.make_dir_absolute(filepath.get_base_dir())
|
||||
|
||||
if filepath == "DEBUG":
|
||||
## Creates a NEW save game (for starting a new game)
|
||||
func _init(filepath_or_debug: String = "") -> void:
|
||||
if filepath_or_debug == "":
|
||||
filepath = "%s/%s.tres" % [State.user_saves_path, unique_save_name]
|
||||
elif filepath_or_debug == "DEBUG":
|
||||
filepath = "DEBUG"
|
||||
if OS.has_feature("debug") or OS.has_feature("demo"):
|
||||
push_warning("Created DEBUG savegame. Progress will not be stored!")
|
||||
else:
|
||||
print_debug(get_stack())
|
||||
push_error("Created DEBUG savegame outside of demo or debug environment. This is unintentional and will lead to data loss. Please contact support and attatch the stack above.")
|
||||
#TODO maybe cause a crash here?
|
||||
return
|
||||
|
||||
if FileAccess.file_exists(filepath):
|
||||
print_debug("Savegame: Reading from: %s" % filepath)
|
||||
var file := FileAccess.open(filepath, FileAccess.READ)
|
||||
var raw_json := FileAccess.get_file_as_string(filepath)
|
||||
file.close()
|
||||
var parsed: Dictionary = JSON.parse_string(raw_json)
|
||||
|
||||
var tmp_img: Image
|
||||
|
||||
if FileAccess.file_exists("%s/thumbnails/%s.png" % [filepath.get_base_dir(), unique_save_name]):
|
||||
tmp_img = Image.load_from_file("%s/thumbnails/%s.png" % [filepath.get_base_dir(), unique_save_name])
|
||||
|
||||
var are_types_valid := (
|
||||
parsed["unique_save_name"] is String and
|
||||
parsed["current_room"] is float and
|
||||
parsed["mementos_complete"] is float and
|
||||
parsed["board_state"] is Dictionary and
|
||||
parsed["is_childhood_board_complete"] is bool and
|
||||
parsed["last_saved"] is float# and FIXME
|
||||
#parsed["demo"] is bool and last_saved != 0
|
||||
)
|
||||
|
||||
if are_types_valid:
|
||||
for key in parsed.keys():
|
||||
set(key, parsed[key])
|
||||
|
||||
for dict:Dictionary in [board_state["cards"], board_state["stickies"]]:
|
||||
for key in dict.keys():
|
||||
if dict[key] is String:
|
||||
if dict[key].begins_with("("):
|
||||
dict[key] = parse_vec_from_string(dict[key])
|
||||
|
||||
var cards: Dictionary[StringName, Variant]
|
||||
var stickies: Dictionary[StringName, Variant]
|
||||
var randoms: Array[StringName]
|
||||
|
||||
for cardname:String in board_state["cards"]:
|
||||
cards[StringName(cardname)] = board_state["cards"][cardname]
|
||||
for sticky_name:String in board_state["stickies"]:
|
||||
stickies[StringName(sticky_name)] = board_state["stickies"][sticky_name]
|
||||
for random_name:StringName in board_state["randoms"]:
|
||||
randoms.append( random_name )
|
||||
|
||||
board_state = {
|
||||
"cards": cards,
|
||||
"stickies": stickies,
|
||||
"randoms": randoms
|
||||
}
|
||||
|
||||
is_valid = are_types_valid \
|
||||
and current_room >= 0 \
|
||||
and current_room < State.rooms.keys().size() \
|
||||
and validate_board_state()
|
||||
|
||||
if not is_valid:
|
||||
push_error("Parsing of Save failed.")
|
||||
|
||||
if tmp_img != null:
|
||||
thumbnail = ImageTexture.create_from_image(tmp_img)
|
||||
is_empty = false
|
||||
else:
|
||||
print_debug("Savegame: Creating (in memory) for path: %s" % filepath)
|
||||
is_valid = true
|
||||
filepath = filepath_or_debug
|
||||
unique_save_name = filepath_or_debug.get_file().get_basename()
|
||||
|
||||
func _get_save_dict() -> Dictionary:
|
||||
return {
|
||||
"unique_save_name": unique_save_name,
|
||||
"current_room": current_room,
|
||||
"mementos_complete": mementos_complete,
|
||||
"sequences_enabled": sequences_enabled,
|
||||
"childhood_mementos": childhood_mementos,
|
||||
"board_state": board_state,
|
||||
"is_childhood_board_complete": is_childhood_board_complete,
|
||||
"last_saved": last_saved,
|
||||
"is_demo": is_demo
|
||||
}
|
||||
# Ensure save directory exists
|
||||
if filepath != "DEBUG" and not DirAccess.dir_exists_absolute(filepath.get_base_dir()):
|
||||
DirAccess.make_dir_absolute(filepath.get_base_dir())
|
||||
|
||||
print_debug("Savegame: Creating new save for path: %s" % filepath)
|
||||
is_valid = true
|
||||
_is_initialised = true
|
||||
|
||||
## Static factory method to load an EXISTING save from disk
|
||||
static func load_from_file(save_filepath: String) -> SaveGame:
|
||||
if not FileAccess.file_exists(save_filepath):
|
||||
push_error("SaveGame: File does not exist: %s" % save_filepath)
|
||||
return null
|
||||
|
||||
print_debug("Savegame: Loading from: %s" % save_filepath)
|
||||
|
||||
var loaded: SaveGame = ResourceLoader.load(save_filepath, "", ResourceLoader.CACHE_MODE_IGNORE)
|
||||
|
||||
if not loaded:
|
||||
push_error("Failed to load SaveGame resource from: %s" % save_filepath)
|
||||
return null
|
||||
|
||||
# Update filepath to actual location (in case file was moved)
|
||||
loaded.filepath = save_filepath
|
||||
loaded.unique_save_name = save_filepath.get_file().get_basename()
|
||||
|
||||
# Ensure randoms array exists (backwards compatibility)
|
||||
if "randoms" not in loaded.board_state:
|
||||
loaded.board_state["randoms"] = []
|
||||
|
||||
# Load thumbnail separately (not stored in .tres)
|
||||
var thumbnail_path := "%s/thumbnails/%s.png" % [save_filepath.get_base_dir(), loaded.unique_save_name]
|
||||
if FileAccess.file_exists(thumbnail_path):
|
||||
var tmp_img: Image = Image.load_from_file(thumbnail_path)
|
||||
if tmp_img != null:
|
||||
loaded.thumbnail = ImageTexture.create_from_image(tmp_img)
|
||||
|
||||
# Validate the loaded data
|
||||
loaded.is_valid = loaded.current_room >= 0 \
|
||||
and loaded.current_room < State.rooms.keys().size() \
|
||||
and loaded.validate_board_state()
|
||||
|
||||
if not loaded.is_valid:
|
||||
push_error("Validation of loaded save failed: %s" % save_filepath)
|
||||
return null
|
||||
|
||||
loaded._is_initialised = true
|
||||
|
||||
return loaded
|
||||
|
||||
## Captures current player position and camera rotation
|
||||
func capture_player_state() -> void:
|
||||
if State.player:
|
||||
player_position = State.player.global_position
|
||||
# Access yaw and pitch nodes
|
||||
var yaw: Node3D = State.player.get_node("Yaw")
|
||||
var pitch: Node3D = yaw.get_node("Pitch")
|
||||
player_yaw = yaw.rotation.y
|
||||
player_pitch = pitch.rotation.x
|
||||
print_debug("SaveGame: Captured player state - pos: %s, yaw: %.2f, pitch: %.2f" % [player_position, player_yaw, player_pitch])
|
||||
|
||||
func save_to_file(screen_shot: Texture) -> void:
|
||||
print_debug("Savegame: Saving to file: %s" % filepath)
|
||||
|
|
@ -180,7 +167,12 @@ func save_to_file(screen_shot: Texture) -> void:
|
|||
push_warning("Savegame: Not saving empty savegame.")
|
||||
return
|
||||
|
||||
# Capture player state before saving
|
||||
capture_player_state()
|
||||
|
||||
last_saved = int(Time.get_unix_time_from_system())
|
||||
|
||||
# Save thumbnail separately as PNG
|
||||
var thumbnail_image: Image = screen_shot.get_image()
|
||||
thumbnail_image.convert(Image.Format.FORMAT_RGB8)
|
||||
thumbnail_image.linear_to_srgb()
|
||||
|
|
@ -193,10 +185,13 @@ func save_to_file(screen_shot: Texture) -> void:
|
|||
|
||||
var thumbnail_path: String = "%s/thumbnails/%s.png" % [filepath.get_base_dir(), unique_save_name]
|
||||
thumbnail_image.save_png(thumbnail_path)
|
||||
print_debug(filepath.get_base_dir())
|
||||
var file := FileAccess.open(filepath, FileAccess.WRITE)
|
||||
file.store_string(JSON.stringify(_get_save_dict()))
|
||||
file.close()
|
||||
|
||||
# Save the resource using Godot's native serialization
|
||||
var save_result := ResourceSaver.save(self, filepath)
|
||||
if save_result != OK:
|
||||
push_error("Failed to save resource to: %s (Error code: %d)" % [filepath, save_result])
|
||||
else:
|
||||
print_debug("Successfully saved to: %s" % filepath)
|
||||
|
||||
|
||||
func calculate_completed_sequences() -> int:
|
||||
|
|
@ -226,7 +221,3 @@ func validate_board_state() -> bool:
|
|||
return false
|
||||
return true
|
||||
return false
|
||||
|
||||
func parse_vec_from_string(string: String) -> Vector2:
|
||||
var string_array := string.replace("(", "").replace(")", "").split(", ")
|
||||
return Vector2(float(string_array[0]), float(string_array[1]))
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ func _player_active(value: bool) -> void:
|
|||
|
||||
func expand() -> void:
|
||||
shown = true
|
||||
_process_billboard()
|
||||
|
||||
if tween and tween.is_valid():
|
||||
tween.kill()
|
||||
|
|
@ -72,7 +73,6 @@ func collapse() -> void:
|
|||
tween.parallel().tween_property(frame, "scale", Vector3.ONE * 2.0, 1.0).set_trans(Tween.TRANS_QUAD)
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_process_billboard()
|
||||
_process_hover()
|
||||
|
||||
func _process_billboard() -> void:
|
||||
|
|
|
|||
|
|
@ -128,6 +128,16 @@ func _ready():
|
|||
func _on_player_enable(enable: bool) -> void:
|
||||
enabled = enable
|
||||
|
||||
## Restores player position and camera rotation from save game
|
||||
func restore_from_save(save: SaveGame) -> void:
|
||||
if save.player_position != Vector3.ZERO:
|
||||
global_position = save.player_position
|
||||
yaw.rotation.y = save.player_yaw
|
||||
pitch.rotation.x = save.player_pitch
|
||||
print_debug("PlayerController: Restored position %s, yaw %.2f, pitch %.2f" % [save.player_position, save.player_yaw, save.player_pitch])
|
||||
else:
|
||||
print_debug("PlayerController: No saved position data, using default spawn")
|
||||
|
||||
func _process(_delta) -> void:
|
||||
if not enabled:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -43,10 +43,10 @@ func _load_games():
|
|||
var filepaths: PackedStringArray = save_game_dir.get_files()
|
||||
|
||||
for path in filepaths:
|
||||
if path is String and path.ends_with(".json"):
|
||||
var save := SaveGame.new("%s/%s" % [State.user_saves_path, path.get_basename()])
|
||||
# HACK: Skip empty saves (we decide later what to do with them)
|
||||
if not save.is_empty:
|
||||
if path is String and path.ends_with(".tres"):
|
||||
var save := SaveGame.load_from_file("%s/%s" % [State.user_saves_path, path])
|
||||
# Skip invalid/empty saves
|
||||
if save != null and not save.is_empty:
|
||||
saves.append(save)
|
||||
|
||||
_sort_saves()
|
||||
|
|
|
|||
Loading…
Reference in New Issue