200 lines
6.7 KiB
GDScript
200 lines
6.7 KiB
GDScript
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 is_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_debug("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_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: 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_debug("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:
|
|
if current_room < 0 or current_room >= State.rooms.keys().size():
|
|
return false
|
|
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
|