Compare commits

..

No commits in common. "e9700b760d2caa3391a24e1fe8f471a96cf23558" and "bc91204aa27e1bce2b9673a0a3da35757ca96311" have entirely different histories.

11 changed files with 199 additions and 236 deletions

View File

@ -49,9 +49,6 @@ func pull_save_state(save: SaveGame) -> void:
save.current_room = State.rooms.ADULTHOOD save.current_room = State.rooms.ADULTHOOD
save_game = save save_game = save
# Call parent to restore player position
super.pull_save_state(save)
func prepare_transition(): func prepare_transition():
for child in %Stations.get_children(): for child in %Stations.get_children():
if child is Node3D: if child is Node3D:

View File

@ -29,24 +29,14 @@ func _ready():
card_picker.cards_picked.connect(card_board.populate_board) card_picker.cards_picked.connect(card_board.populate_board)
func pull_save_state(save: SaveGame) -> void: func pull_save_state(save: SaveGame) -> void:
save.board_state = card_board.get_save_dict()
save.current_room = State.rooms.ADULTHOOD save.current_room = State.rooms.ADULTHOOD
#FIXME: fix the bloddy card board loading algorythm save.mementos_complete = Scenes.completed_sequences
#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): func _on_scene_finished(_id: int, _repeat:bool):
await get_tree().create_timer(3).timeout await get_tree().create_timer(3).timeout
save_room() 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(): func prepare_transition():
pass pass

View File

@ -62,26 +62,17 @@ func _ready():
func pull_save_state(save: SaveGame) -> void: func pull_save_state(save: SaveGame) -> void:
save_game = save save_game = save
save_game.current_room = id 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) card_board.initialise_from_save(save_game)
Scenes.started_sequences = save_game.mementos_complete Scenes.started_sequences = save_game.mementos_complete
Scenes.completed_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): func _on_scene_finished(_id: int, _repeat:bool):
await get_tree().create_timer(3).timeout await get_tree().create_timer(3).timeout
save_room() 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(): func prepare_transition():
save_room() save_room()

View File

@ -1871,7 +1871,7 @@ light_size = 20.0
omni_range = 16.8518 omni_range = 16.8518
[node name="MaskInteractable" parent="logic" instance=ExtResource("22_ks23q")] [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.028929986, 0.58693635, 2.552513) transform = Transform3D(-0.8827416, 0, 0.4698562, 0, 1, 0, -0.4698562, 0, -0.8827416, -0.025371574, 0.55708295, 2.5263817)
interaction = ExtResource("12_viwxf") interaction = ExtResource("12_viwxf")
[node name="MindBoardInteractable" parent="logic" instance=ExtResource("22_ks23q")] [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) 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")] [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.6334484, 1.1331886, -0.8914416) transform = Transform3D(0.7935111, 0, -0.60855323, 0, 1, 0, 0.60855323, 0, 0.7935111, 1.6713148, 1.089737, -0.92289597)
interaction = ExtResource("12_x3dlb") interaction = ExtResource("12_x3dlb")
[node name="Sprite3D" parent="logic/ClothesInteractable/View" index="0"] [node name="Sprite3D" parent="logic/ClothesInteractable/View" index="0"]

View File

@ -41,23 +41,7 @@ func start_room():
pass pass
func pull_save_state(_save: SaveGame) -> void: func pull_save_state(_save: SaveGame) -> void:
# Try to restore player position from save pass
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(): func save_room():
save_game.save_to_file(get_tree().root.get_texture()) save_game.save_to_file(get_tree().root.get_texture())

View File

@ -1,221 +1,232 @@
class_name SaveGame extends Resource class_name SaveGame extends Resource
## Save game data container var _is_initialised: bool = false
## This is primarily a data class - file I/O helpers are thin wrappers around ResourceSaver/Loader
# === Computed Properties ===
var current_room_path: String: var current_room_path: String:
get: get():
return Main.room_paths[current_room] if Main else "" return Main.room_paths[current_room]
var is_empty: bool: @export var filepath: String:
get: set(value):
return not FileAccess.file_exists(filepath) or (current_room == State.rooms.NULL) filepath = value
if _is_initialised:
changed.emit()
# === Data Fields === @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 filepath: String = "" @export var current_room: State.rooms = State.rooms.NULL:
@export var unique_save_name: String = "" set(value):
@export var current_room: State.rooms = State.rooms.NULL current_room = value
@export_flags("Intro", "Childhood", "Voice Training", "Jui Jutsu") var mementos_complete: int = 0 if _is_initialised: changed.emit()
@export_flags_2d_physics var sequences_enabled: int = 63 @export_flags("Intro", "Childhood", "Voice Training", "Jui Jutsu") var mementos_complete: int = 0:
@export var board_state: Dictionary = {"cards": {}, "stickies": {}, "randoms": []} set(value):
@export var childhood_mementos: Dictionary = {"cards": {}, "stickies": {}, "randoms": []} 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 is_childhood_board_complete: bool = false
@export var player_position: Vector3 = Vector3.ZERO @export var thumbnail: Texture = preload("res://import/interface-elements/empty_save_slot.png"):
@export var player_yaw: float = 0.0 set(value):
@export var player_pitch: float = 0.0 thumbnail = value
@export var last_saved: int = 0 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_valid: bool = false
@export var is_demo: bool = false @export var is_demo: bool = OS.has_feature("Demo")
@export var is_empty: bool = true:
var thumbnail: Texture = preload("res://import/interface-elements/empty_save_slot.png") get():
return not FileAccess.file_exists(filepath) or (current_room == State.rooms.NULL)
# === Editor Conveniences ===
@export var save_manually: bool = false: @export var save_manually: bool = false:
set(val): set(val):
if val: if val: save_to_file(thumbnail)
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 in ["thumbnail", "is_valid", "is_empty"]: if property.name == "thumbnail":
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
# === Static Helpers === 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()
## Generates a unique save filepath read_save_file()
static func generate_save_path() -> String: _is_initialised = true
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 func read_save_file() -> void:
static func create_new() -> SaveGame: if not DirAccess.dir_exists_absolute(filepath.get_base_dir()):
var save := SaveGame.new() DirAccess.make_dir_absolute(filepath.get_base_dir())
save.filepath = generate_save_path()
save.unique_save_name = save.filepath.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
if not DirAccess.dir_exists_absolute(save.filepath.get_base_dir()):
DirAccess.make_dir_absolute(save.filepath.get_base_dir())
print_debug("SaveGame: Created new save: %s" % save.filepath)
return save
## 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 filepath == "DEBUG":
if OS.has_feature("debug") or OS.has_feature("demo"): if OS.has_feature("debug") or OS.has_feature("demo"):
push_warning("Created DEBUG savegame. Progress will not be stored!") push_warning("Created DEBUG savegame. Progress will not be stored!")
else: else:
push_error("Created DEBUG savegame outside of demo/debug environment. This will lead to data loss!") 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.")
return save #TODO maybe cause a crash here?
## 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
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 metadata
loaded.filepath = save_filepath
loaded.unique_save_name = save_filepath.get_file().get_basename()
# Backwards compatibility
if "randoms" not in loaded.board_state:
loaded.board_state["randoms"] = []
# Load thumbnail separately (not stored in .tres)
_load_thumbnail(loaded)
# Validate
loaded.is_valid = loaded._validate()
if not loaded.is_valid:
push_error("Validation of loaded save 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.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:
if not State.player:
return return
player_position = State.player.global_position if FileAccess.file_exists(filepath):
var yaw: Node3D = State.player.get_node("Yaw") print_debug("Savegame: Reading from: %s" % filepath)
var pitch: Node3D = yaw.get_node("Pitch") var file := FileAccess.open(filepath, FileAccess.READ)
player_yaw = yaw.rotation.y var raw_json := FileAccess.get_file_as_string(filepath)
player_pitch = pitch.rotation.x file.close()
var parsed: Dictionary = JSON.parse_string(raw_json)
print_debug("SaveGame: Captured player state - pos: %s, yaw: %.2f, pitch: %.2f" % [player_position, player_yaw, player_pitch]) 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
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
}
## 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: DEBUG save skipped (intentional).") push_warning("Savegame: Saving DEBUG save skipped. This is 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
print_debug("SaveGame: Saving to file: %s" % filepath)
# Capture current state
capture_player_state()
last_saved = int(Time.get_unix_time_from_system()) last_saved = int(Time.get_unix_time_from_system())
var thumbnail_image: Image = screen_shot.get_image()
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 thumbnail
_save_thumbnail(screen_shot)
# 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 := "%s/thumbnails/%s.png" % [filepath.get_base_dir(), unique_save_name] var thumbnail_path: String = "%s/thumbnails/%s.png" % [filepath.get_base_dir(), unique_save_name]
img.save_png(thumbnail_path) 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()
## Validates save data integrity
func _validate() -> bool:
if current_room < 0 or current_room >= State.rooms.keys().size():
return false
return validate_board_state()
# === 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 not board_state.has("cards") or not board_state.has("stickies"): if board_state.keys().has("cards") and board_state.keys().has("stickies"):
return false
for card in board_state.cards.values(): for card in board_state.cards.values():
if not card is Vector2: if not card is Vector2:
push_error("Save %s: Corrupted cards" % unique_save_name) push_error("Save %s could not be parsed: Corrupted Cards." % unique_save_name)
return false return false
for sticky in board_state.stickies.values(): for sticky in board_state.stickies.values():
if not (sticky is int or sticky is Vector2 or sticky is float or board_state.cards.has(sticky)): if not (sticky is int or sticky is Vector2 or sticky is float or board_state.cards.keys().has(sticky)):
push_error("Save %s: Corrupted sticky notes" % unique_save_name) push_error("Save %s could not be parsed: Corrupted Sticky Notes.")
return false
return true
return false return false
return true 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]))

View File

@ -47,7 +47,6 @@ func _player_active(value: bool) -> void:
func expand() -> void: func expand() -> void:
shown = true shown = true
_process_billboard()
if tween and tween.is_valid(): if tween and tween.is_valid():
tween.kill() tween.kill()
@ -73,6 +72,7 @@ func collapse() -> void:
tween.parallel().tween_property(frame, "scale", Vector3.ONE * 2.0, 1.0).set_trans(Tween.TRANS_QUAD) tween.parallel().tween_property(frame, "scale", Vector3.ONE * 2.0, 1.0).set_trans(Tween.TRANS_QUAD)
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
_process_billboard()
_process_hover() _process_hover()
func _process_billboard() -> void: func _process_billboard() -> void:

View File

@ -128,16 +128,6 @@ func _ready():
func _on_player_enable(enable: bool) -> void: func _on_player_enable(enable: bool) -> void:
enabled = enable 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: func _process(_delta) -> void:
if not enabled: if not enabled:
return return

View File

@ -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.create_new() State.save_game = SaveGame.new()
_start_game() _start_game()

View File

@ -43,10 +43,10 @@ func _load_games():
var filepaths: PackedStringArray = save_game_dir.get_files() var filepaths: PackedStringArray = save_game_dir.get_files()
for path in filepaths: for path in filepaths:
if path is String and path.ends_with(".tres"): if path is String and path.ends_with(".json"):
var save := SaveGame.load_from_file("%s/%s" % [State.user_saves_path, path]) var save := SaveGame.new("%s/%s" % [State.user_saves_path, path.get_basename()])
# Skip invalid/empty saves # HACK: Skip empty saves (we decide later what to do with them)
if save != null and not save.is_empty: if not save.is_empty:
saves.append(save) saves.append(save)
_sort_saves() _sort_saves()