chore: refactored save game initialization logic
feat: save game factory method create_new()
This commit is contained in:
parent
eefc38fb42
commit
c9fef83b3a
|
|
@ -1,115 +1,95 @@
|
||||||
class_name SaveGame extends Resource
|
class_name SaveGame extends Resource
|
||||||
|
|
||||||
|
## Save game data container
|
||||||
|
## This is primarily a data class - file I/O helpers are thin wrappers around ResourceSaver/Loader
|
||||||
|
|
||||||
var _is_initialised: bool = false
|
# === Computed Properties ===
|
||||||
|
|
||||||
var current_room_path: String:
|
var current_room_path: String:
|
||||||
get():
|
get:
|
||||||
return Main.room_paths[current_room]
|
return Main.room_paths[current_room] if Main else ""
|
||||||
|
|
||||||
@export var filepath: String:
|
var is_empty: bool:
|
||||||
set(value):
|
get:
|
||||||
filepath = value
|
|
||||||
if _is_initialised:
|
|
||||||
changed.emit()
|
|
||||||
|
|
||||||
@export var unique_save_name: String = "frame_of_mind_%s_%s" % [Time.get_date_string_from_system(), Time.get_time_string_from_system().replace(":", "-")] + "-" + str(randi()):
|
|
||||||
set(value):
|
|
||||||
unique_save_name = value
|
|
||||||
if _is_initialised: changed.emit()
|
|
||||||
|
|
||||||
@export var current_room: State.rooms = State.rooms.NULL:
|
|
||||||
set(value):
|
|
||||||
current_room = value
|
|
||||||
if _is_initialised: changed.emit()
|
|
||||||
@export_flags("Intro", "Childhood", "Voice Training", "Jui Jutsu") var mementos_complete: int = 0:
|
|
||||||
set(value):
|
|
||||||
mementos_complete = value
|
|
||||||
if _is_initialised: changed.emit()
|
|
||||||
@export_flags_2d_physics var sequences_enabled: int = 63:
|
|
||||||
set(value):
|
|
||||||
sequences_enabled = value
|
|
||||||
if _is_initialised: changed.emit()
|
|
||||||
@export var board_state: Dictionary = {"cards": {}, "stickies": {}, "randoms": []}:
|
|
||||||
set(value):
|
|
||||||
board_state = value
|
|
||||||
if _is_initialised: changed.emit()
|
|
||||||
@export var childhood_mementos: Dictionary = {"cards": {}, "stickies": {}, "randoms": []}:
|
|
||||||
set(value):
|
|
||||||
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
|
|
||||||
if _is_initialised: changed.emit()
|
|
||||||
@export var last_saved: int = int(Time.get_unix_time_from_system()):
|
|
||||||
set(value):
|
|
||||||
last_saved = value
|
|
||||||
if _is_initialised: changed.emit()
|
|
||||||
|
|
||||||
@export var is_valid: bool = false
|
|
||||||
@export var is_demo: bool = OS.has_feature("Demo")
|
|
||||||
@export var is_empty: bool = true:
|
|
||||||
get():
|
|
||||||
return not FileAccess.file_exists(filepath) or (current_room == State.rooms.NULL)
|
return not FileAccess.file_exists(filepath) or (current_room == State.rooms.NULL)
|
||||||
|
|
||||||
|
# === Data Fields ===
|
||||||
|
|
||||||
|
@export var filepath: String = ""
|
||||||
|
@export var unique_save_name: String = ""
|
||||||
|
@export var current_room: State.rooms = State.rooms.NULL
|
||||||
|
@export_flags("Intro", "Childhood", "Voice Training", "Jui Jutsu") var mementos_complete: int = 0
|
||||||
|
@export_flags_2d_physics var sequences_enabled: int = 63
|
||||||
|
@export var board_state: Dictionary = {"cards": {}, "stickies": {}, "randoms": []}
|
||||||
|
@export var childhood_mementos: Dictionary = {"cards": {}, "stickies": {}, "randoms": []}
|
||||||
|
@export var is_childhood_board_complete: bool = false
|
||||||
|
@export var player_position: Vector3 = Vector3.ZERO
|
||||||
|
@export var player_yaw: float = 0.0
|
||||||
|
@export var player_pitch: float = 0.0
|
||||||
|
@export var thumbnail: Texture = preload("res://import/interface-elements/empty_save_slot.png")
|
||||||
|
@export var last_saved: int = 0
|
||||||
|
@export var is_valid: bool = false
|
||||||
|
@export var is_demo: bool = false
|
||||||
|
|
||||||
|
# === Editor Conveniences ===
|
||||||
|
|
||||||
@export var save_manually: bool = false:
|
@export var save_manually: bool = false:
|
||||||
set(val):
|
set(val):
|
||||||
if val: save_to_file(thumbnail)
|
if val:
|
||||||
|
save_to_file(thumbnail)
|
||||||
|
|
||||||
func _validate_property(property: Dictionary):
|
func _validate_property(property: Dictionary):
|
||||||
if property.name == filepath:
|
if property.name == "filepath":
|
||||||
property.usage |= PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED
|
property.usage |= PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED
|
||||||
if property.name == "thumbnail":
|
if property.name in ["thumbnail", "is_valid", "is_empty"]:
|
||||||
property.usage |= PROPERTY_USAGE_READ_ONLY
|
|
||||||
if property.name == "is_valid":
|
|
||||||
property.usage |= PROPERTY_USAGE_READ_ONLY
|
|
||||||
if property.name == "is_empty":
|
|
||||||
property.usage |= PROPERTY_USAGE_READ_ONLY
|
property.usage |= PROPERTY_USAGE_READ_ONLY
|
||||||
|
|
||||||
## Creates a NEW save game (for starting a new game)
|
# === Static Helpers ===
|
||||||
func _init(filepath_or_debug: String = "") -> void:
|
|
||||||
if filepath_or_debug == "":
|
## Generates a unique save filepath
|
||||||
filepath = "%s/%s.tres" % [State.user_saves_path, unique_save_name]
|
static func generate_save_path() -> String:
|
||||||
elif filepath_or_debug == "DEBUG":
|
var timestamp := "%s_%s" % [Time.get_date_string_from_system(), Time.get_time_string_from_system().replace(":", "-")]
|
||||||
filepath = "DEBUG"
|
var unique_name := "frame_of_mind_%s-%d" % [timestamp, randi()]
|
||||||
if OS.has_feature("debug") or OS.has_feature("demo"):
|
return "%s/%s.tres" % [State.user_saves_path, unique_name]
|
||||||
push_warning("Created DEBUG savegame. Progress will not be stored!")
|
|
||||||
else:
|
## Creates a new save game with generated filepath
|
||||||
print_debug(get_stack())
|
static func create_new() -> SaveGame:
|
||||||
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.")
|
var save := SaveGame.new()
|
||||||
else:
|
save.filepath = generate_save_path()
|
||||||
filepath = filepath_or_debug
|
save.unique_save_name = save.filepath.get_file().get_basename()
|
||||||
unique_save_name = filepath_or_debug.get_file().get_basename()
|
save.is_valid = true
|
||||||
|
save.is_demo = OS.has_feature("Demo")
|
||||||
|
save.last_saved = int(Time.get_unix_time_from_system())
|
||||||
|
|
||||||
# Ensure save directory exists
|
# Ensure save directory exists
|
||||||
if filepath != "DEBUG" and not DirAccess.dir_exists_absolute(filepath.get_base_dir()):
|
if not DirAccess.dir_exists_absolute(save.filepath.get_base_dir()):
|
||||||
DirAccess.make_dir_absolute(filepath.get_base_dir())
|
DirAccess.make_dir_absolute(save.filepath.get_base_dir())
|
||||||
|
|
||||||
print_debug("Savegame: Creating new save for path: %s" % filepath)
|
print_debug("SaveGame: Created new save: %s" % save.filepath)
|
||||||
is_valid = true
|
return save
|
||||||
_is_initialised = true
|
|
||||||
|
|
||||||
## Static factory method to load an EXISTING save from disk
|
## Creates a DEBUG save (not persisted to disk)
|
||||||
|
static func create_debug() -> SaveGame:
|
||||||
|
var save := SaveGame.new()
|
||||||
|
save.filepath = "DEBUG"
|
||||||
|
save.unique_save_name = "DEBUG"
|
||||||
|
save.is_valid = true
|
||||||
|
save.is_demo = OS.has_feature("Demo")
|
||||||
|
|
||||||
|
if OS.has_feature("debug") or OS.has_feature("demo"):
|
||||||
|
push_warning("Created DEBUG savegame. Progress will not be stored!")
|
||||||
|
else:
|
||||||
|
push_error("Created DEBUG savegame outside of demo/debug environment. This will lead to data loss!")
|
||||||
|
|
||||||
|
return save
|
||||||
|
|
||||||
|
## Loads an existing save from disk
|
||||||
static func load_from_file(save_filepath: String) -> SaveGame:
|
static func load_from_file(save_filepath: String) -> SaveGame:
|
||||||
if not FileAccess.file_exists(save_filepath):
|
if not FileAccess.file_exists(save_filepath):
|
||||||
push_error("SaveGame: File does not exist: %s" % save_filepath)
|
push_error("SaveGame: File does not exist: %s" % save_filepath)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
print_debug("Savegame: Loading from: %s" % save_filepath)
|
print_debug("SaveGame: Loading from: %s" % save_filepath)
|
||||||
|
|
||||||
var loaded: SaveGame = ResourceLoader.load(save_filepath, "", ResourceLoader.CACHE_MODE_IGNORE)
|
var loaded: SaveGame = ResourceLoader.load(save_filepath, "", ResourceLoader.CACHE_MODE_IGNORE)
|
||||||
|
|
||||||
|
|
@ -117,107 +97,124 @@ static func load_from_file(save_filepath: String) -> SaveGame:
|
||||||
push_error("Failed to load SaveGame resource from: %s" % save_filepath)
|
push_error("Failed to load SaveGame resource from: %s" % save_filepath)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
# Update filepath to actual location (in case file was moved)
|
# Update filepath metadata
|
||||||
loaded.filepath = save_filepath
|
loaded.filepath = save_filepath
|
||||||
loaded.unique_save_name = save_filepath.get_file().get_basename()
|
loaded.unique_save_name = save_filepath.get_file().get_basename()
|
||||||
|
|
||||||
# Ensure randoms array exists (backwards compatibility)
|
# Backwards compatibility
|
||||||
if "randoms" not in loaded.board_state:
|
if "randoms" not in loaded.board_state:
|
||||||
loaded.board_state["randoms"] = []
|
loaded.board_state["randoms"] = []
|
||||||
|
|
||||||
# Load thumbnail separately (not stored in .tres)
|
# Load thumbnail separately (not stored in .tres)
|
||||||
var thumbnail_path := "%s/thumbnails/%s.png" % [save_filepath.get_base_dir(), loaded.unique_save_name]
|
_load_thumbnail(loaded)
|
||||||
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
|
# Validate
|
||||||
loaded.is_valid = loaded.current_room >= 0 \
|
loaded.is_valid = loaded._validate()
|
||||||
and loaded.current_room < State.rooms.keys().size() \
|
|
||||||
and loaded.validate_board_state()
|
|
||||||
|
|
||||||
if not loaded.is_valid:
|
if not loaded.is_valid:
|
||||||
push_error("Validation of loaded save failed: %s" % save_filepath)
|
push_error("Validation of loaded save failed: %s" % save_filepath)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
loaded._is_initialised = true
|
|
||||||
|
|
||||||
return loaded
|
return loaded
|
||||||
|
|
||||||
## Captures current player position and camera rotation
|
## Helper to load thumbnail from separate PNG file
|
||||||
|
static func _load_thumbnail(save: SaveGame) -> void:
|
||||||
|
var thumbnail_path := "%s/thumbnails/%s.png" % [save.filepath.get_base_dir(), save.unique_save_name]
|
||||||
|
if FileAccess.file_exists(thumbnail_path):
|
||||||
|
var img := Image.load_from_file(thumbnail_path)
|
||||||
|
if img:
|
||||||
|
save.thumbnail = ImageTexture.create_from_image(img)
|
||||||
|
|
||||||
|
# === Instance Methods ===
|
||||||
|
|
||||||
|
## Captures current player position/rotation from State.player
|
||||||
func capture_player_state() -> void:
|
func capture_player_state() -> void:
|
||||||
if State.player:
|
if not State.player:
|
||||||
player_position = State.player.global_position
|
return
|
||||||
# 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])
|
|
||||||
|
|
||||||
|
player_position = State.player.global_position
|
||||||
|
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])
|
||||||
|
|
||||||
|
## Saves to disk with thumbnail
|
||||||
func save_to_file(screen_shot: Texture) -> void:
|
func save_to_file(screen_shot: Texture) -> void:
|
||||||
print_debug("Savegame: Saving to file: %s" % filepath)
|
|
||||||
|
|
||||||
if filepath == "DEBUG":
|
if filepath == "DEBUG":
|
||||||
push_warning("Savegame: Saving DEBUG save skipped. This is intentional.")
|
push_warning("SaveGame: DEBUG save skipped (intentional).")
|
||||||
return
|
return
|
||||||
|
|
||||||
if current_room == State.rooms.NULL:
|
if current_room == State.rooms.NULL:
|
||||||
push_warning("Savegame: Not saving empty savegame.")
|
push_warning("SaveGame: Not saving empty savegame.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Capture player state before saving
|
print_debug("SaveGame: Saving to file: %s" % filepath)
|
||||||
capture_player_state()
|
|
||||||
|
|
||||||
|
# Capture current state
|
||||||
|
capture_player_state()
|
||||||
last_saved = int(Time.get_unix_time_from_system())
|
last_saved = int(Time.get_unix_time_from_system())
|
||||||
|
|
||||||
# Save thumbnail separately as PNG
|
# Save thumbnail
|
||||||
var thumbnail_image: Image = screen_shot.get_image()
|
_save_thumbnail(screen_shot)
|
||||||
thumbnail_image.convert(Image.Format.FORMAT_RGB8)
|
|
||||||
thumbnail_image.linear_to_srgb()
|
|
||||||
thumbnail_image.resize(384, 216, Image.INTERPOLATE_LANCZOS) # nonexistent call in ViewportTexture
|
|
||||||
thumbnail_image.crop(384, 216)
|
|
||||||
|
|
||||||
|
# Save resource
|
||||||
|
var result := ResourceSaver.save(self, filepath)
|
||||||
|
if result != OK:
|
||||||
|
push_error("Failed to save resource to: %s (Error: %d)" % [filepath, result])
|
||||||
|
else:
|
||||||
|
print_debug("Successfully saved to: %s" % filepath)
|
||||||
|
|
||||||
|
## Processes and saves thumbnail as PNG
|
||||||
|
func _save_thumbnail(screen_shot: Texture) -> void:
|
||||||
|
var img: Image = screen_shot.get_image()
|
||||||
|
img.convert(Image.Format.FORMAT_RGB8)
|
||||||
|
img.linear_to_srgb()
|
||||||
|
img.resize(384, 216, Image.INTERPOLATE_LANCZOS)
|
||||||
|
img.crop(384, 216)
|
||||||
|
|
||||||
|
# Ensure thumbnails directory exists
|
||||||
var save_dir := DirAccess.open(filepath.get_base_dir())
|
var save_dir := DirAccess.open(filepath.get_base_dir())
|
||||||
if not save_dir.dir_exists("thumbnails"):
|
if not save_dir.dir_exists("thumbnails"):
|
||||||
save_dir.make_dir("thumbnails")
|
save_dir.make_dir("thumbnails")
|
||||||
|
|
||||||
var thumbnail_path: String = "%s/thumbnails/%s.png" % [filepath.get_base_dir(), unique_save_name]
|
var thumbnail_path := "%s/thumbnails/%s.png" % [filepath.get_base_dir(), unique_save_name]
|
||||||
thumbnail_image.save_png(thumbnail_path)
|
img.save_png(thumbnail_path)
|
||||||
|
|
||||||
# Save the resource using Godot's native serialization
|
## Validates save data integrity
|
||||||
var save_result := ResourceSaver.save(self, filepath)
|
func _validate() -> bool:
|
||||||
if save_result != OK:
|
if current_room < 0 or current_room >= State.rooms.keys().size():
|
||||||
push_error("Failed to save resource to: %s (Error code: %d)" % [filepath, save_result])
|
return false
|
||||||
else:
|
return validate_board_state()
|
||||||
print_debug("Successfully saved to: %s" % filepath)
|
|
||||||
|
|
||||||
|
# === Helper Methods ===
|
||||||
|
|
||||||
func calculate_completed_sequences() -> int:
|
func calculate_completed_sequences() -> int:
|
||||||
var i: int = mementos_complete - ((mementos_complete >> 1) & 0x55555555);
|
var i: int = mementos_complete - ((mementos_complete >> 1) & 0x55555555)
|
||||||
i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
|
i = (i & 0x33333333) + ((i >> 2) & 0x33333333)
|
||||||
i = (i + (i >> 4)) & 0x0F0F0F0F;
|
i = (i + (i >> 4)) & 0x0F0F0F0F
|
||||||
i *= 0x01010101;
|
i *= 0x01010101
|
||||||
return i >> 24;
|
return i >> 24
|
||||||
|
|
||||||
func calculate_total_connections() -> int:
|
func calculate_total_connections() -> int:
|
||||||
var connections:= 0
|
var connections := 0
|
||||||
|
|
||||||
for sticky_position in board_state.stickies.values():
|
for sticky_position in board_state.stickies.values():
|
||||||
connections += int(sticky_position is String)
|
connections += int(sticky_position is String)
|
||||||
|
|
||||||
return connections
|
return connections
|
||||||
|
|
||||||
func validate_board_state() -> bool:
|
func validate_board_state() -> bool:
|
||||||
if board_state.keys().has("cards") and board_state.keys().has("stickies"):
|
if not board_state.has("cards") or not board_state.has("stickies"):
|
||||||
for card in board_state.cards.values():
|
return false
|
||||||
if not card is Vector2:
|
|
||||||
push_error("Save %s could not be parsed: Corrupted Cards." % unique_save_name)
|
for card in board_state.cards.values():
|
||||||
return false
|
if not card is Vector2:
|
||||||
for sticky in board_state.stickies.values():
|
push_error("Save %s: Corrupted cards" % unique_save_name)
|
||||||
if not (sticky is int or sticky is Vector2 or sticky is float or board_state.cards.keys().has(sticky)):
|
return false
|
||||||
push_error("Save %s could not be parsed: Corrupted Sticky Notes.")
|
|
||||||
return false
|
for sticky in board_state.stickies.values():
|
||||||
return true
|
if not (sticky is int or sticky is Vector2 or sticky is float or board_state.cards.has(sticky)):
|
||||||
return false
|
push_error("Save %s: Corrupted sticky notes" % unique_save_name)
|
||||||
|
return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ func _ready() -> void:
|
||||||
|
|
||||||
func _new_game() -> void:
|
func _new_game() -> void:
|
||||||
print_debug("main_menu.gd: start_new_game()")
|
print_debug("main_menu.gd: start_new_game()")
|
||||||
State.save_game = SaveGame.new()
|
State.save_game = SaveGame.create_new()
|
||||||
_start_game()
|
_start_game()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue