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 # === Data Fields === @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 # Board state - properly typed fields @export var board_positions: Dictionary[StringName, Vector2] = {} # Position of all cards and stickies @export var board_attachments: Dictionary[StringName, StringName] = {} # Sticky name → Card name (if attached) ## Scenes / Items / IDs that were seen @export var seen : Array[StringName] = [] @export var childhood_board_complete: bool = false @export var subway_burnout : 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 last_saved: int = 0 # === Computed Properties === var is_valid: bool: get: return _validate() var current_room_path: String: get: return Main.room_paths[current_room] if Main else "" var is_empty: bool: get: return not FileAccess.file_exists(file_name) or (current_room == State.rooms.NULL) var completed_sequences: int: get: # Hamming weight (population count) algorithm for counting set bits var i: int = mementos_complete - ((mementos_complete >> 1) & 0x55555555) i = (i & 0x33333333) + ((i >> 2) & 0x33333333) i = (i + (i >> 4)) & 0x0F0F0F0F i *= 0x01010101 return i >> 24 var total_connections: int: get: return board_attachments.size() # === State Variables / External Data === ## Where to save the savegame to / where it was loaded from var file_name: String = "" ## Screenshot or placeholder image var thumbnail: Texture = preload("res://import/interface-elements/empty_save_slot.png") # === Static Helpers === ## Generates a unique save filepath static func generate_save_path() -> String: var timestamp := "%s_%s" % [Time.get_date_string_from_system(), Time.get_time_string_from_system().replace(":", "-")] var unique_name := "frame_of_mind_%s-%d" % [timestamp, randi()] return "%s/%s.tres" % [State.user_saves_path, unique_name] ## Creates a new save game with generated filepath static func create_new() -> SaveGame: var save := SaveGame.new() save.file_name = generate_save_path() save.unique_save_name = save.file_name.get_file().get_basename() save.last_saved = int(Time.get_unix_time_from_system()) # Ensure save directory exists if not DirAccess.dir_exists_absolute(save.file_name.get_base_dir()): DirAccess.make_dir_absolute(save.file_name.get_base_dir()) print("SaveGame: Created new save: %s" % save.file_name) return save ## Loads 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 var loaded: SaveGame = ResourceLoader.load(save_filepath, "", ResourceLoader.CACHE_MODE_IGNORE) if not loaded: push_error("SaveGame: Failed to load resource from: %s" % save_filepath) return null # Update filepath metadata loaded.file_name = save_filepath loaded.unique_save_name = save_filepath.get_file().get_basename() # Load thumbnail (stored separately as PNG) _load_thumbnail(loaded) # Validate and return loaded.is_valid = loaded._validate() if not loaded.is_valid: push_error("SaveGame: Validation failed: %s" % save_filepath) return null return loaded ## Helper to load thumbnail from separate PNG file static func _load_thumbnail(save: SaveGame) -> void: var thumbnail_path := "%s/thumbnails/%s.png" % [save.file_name.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: if not State.player: return 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("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: Texture2D) -> void: if unique_save_name == "DEBUG": push_warning("SaveGame: DEBUG save skipped (intentional).") return assert(State.room, "Trying to save while not in a room.") assert(State.room.id != State.rooms.NULL, "Trying to save in a room that's not correctly initialized.") # Save game can track the room it is in by itself; # and we should never save before having successfully entered a room. current_room = State.room.id prints("-----------", "SAVE POINT", State.room, "-----------") print("SaveGame: Saving to file: %s" % file_name) # Capture current state capture_player_state() last_saved = int(Time.get_unix_time_from_system()) # Save thumbnail _save_thumbnail(screen_shot) # Save resource var result := ResourceSaver.save(self, file_name) if result != OK: push_error("Failed to save resource to: %s (Error: %d)" % [file_name, result]) else: print("Successfully saved to: %s" % file_name) ## Processes and saves thumbnail as PNG func _save_thumbnail(screen_shot: Texture2D) -> void: var img: Image = screen_shot.get_image() img.convert(Image.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(file_name.get_base_dir()) if not save_dir.dir_exists("thumbnails"): save_dir.make_dir("thumbnails") var thumbnail_path := "%s/thumbnails/%s.png" % [file_name.get_base_dir(), unique_save_name] img.save_png(thumbnail_path) # === Legacy Validation (may want to be removed) === func _validate() -> bool: return _validate_board_state() func _validate_board_state() -> bool: # Validate positions for position in board_positions.values(): if not position is Vector2: push_error("Save %s: Corrupted board positions" % unique_save_name) return false # Validate attachments (sticky must exist, card must exist) for sticky_name in board_attachments.keys(): var card_name := board_attachments[sticky_name] if not board_positions.has(card_name): push_error("Save %s: Sticky '%s' attached to non-existent card '%s'" % [unique_save_name, sticky_name, card_name]) return false return true