extends Node signal map_loaded @export var characters: Array[CharacterProfile] @export var levels: Array[LevelDescriptor] @export var initial_save_data: SaveData @onready var _lock := NonReentrantCoroutineMutex.new() @onready var _transition_lock := NonReentrantCoroutineMutex.new() var _characters: Dictionary var _levels: Dictionary var _map_loaded := MapID.new() var save_data: SaveData func _time() -> int: return Time.get_unix_time_from_datetime_dict( Time.get_datetime_dict_from_system() ) func _ready() -> void: _build_character_dictionary() _build_level_dictionary() if not load_game(): new_game() func _build_character_dictionary() -> void: for profile in characters: _characters[profile.id] = profile func _build_level_dictionary() -> void: for level in levels: var ldict := { &'descriptor': level, &'missions': {}, &'connections': {} } _levels[level.id] = ldict var lmdict := ldict.missions as Dictionary var lcdict := ldict.connections as Dictionary for connection in level.connections: lcdict[connection.origin] = connection for mission in level.missions: var mdict := { &'descriptor': mission, &'connections': lcdict.duplicate() } lmdict[mission.id] = mdict var mcdict := mdict.connections as Dictionary for connection in mission.connections: mcdict[connection.origin] = connection func save_data_exists() -> bool: return ResourceLoader.exists("user://save.res", &'SaveData') func load_game() -> bool: if save_data_exists(): save_data = ResourceLoader.load("user://save.res", &'SaveData') return true else: return false func save_game() -> void: save_data.switch_character_story_state(&'') ResourceSaver.save( save_data, "user://save.res", ResourceSaver.FLAG_OMIT_EDITOR_PROPERTIES | ResourceSaver.FLAG_COMPRESS ) func _make_save_data() -> SaveData: var result := initial_save_data.duplicate(true) result.ctime = _time() return result func new_game() -> void: save_data = _make_save_data() func get_character(id: StringName) -> CharacterProfile: return _characters[id] func _get_level(id: StringName) -> Dictionary: if _levels.has(id): return _levels[id] else: push_error("no such level %s" % id) return {} func get_level(id: StringName) -> LevelDescriptor: var level := _get_level(id) return null if level.is_empty() else level.descriptor func _get_mission( level_id: StringName, mission_id: StringName ) -> Dictionary: var level := _get_level(level_id) if level.is_empty() or mission_id.is_empty(): return {} elif level.missions.has(mission_id): return level.missions[mission_id] else: push_error("level %s has no such mission %s" % [level_id, mission_id]) return {} func get_mission( level_id: StringName, mission_id: StringName, ) -> MissionDescriptor: var mission := _get_mission(level_id, mission_id) return null if mission.is_empty() else mission.descriptor func get_map_connection( level_id: StringName, mission_id: StringName, origin: StringName, fail_is_error: bool = true ) -> MapConnection: var map := ( _get_level(level_id) if mission_id.is_empty() else _get_mission(level_id, mission_id) ) if map.is_empty(): return null elif map.connections.has(origin): var connection := map.connections[origin] as MapConnection if fail_is_error and not connection: push_error(( "level %s has map connection %s, " + "but its mission %s does not" ) % [ level_id, origin, mission_id ]) return connection elif mission_id.is_empty(): if fail_is_error: push_error("level %s has no such map connection %s" % [ level_id, origin ]) return null else: if fail_is_error: push_error(( "mission %s of level %s " + "has no such map connection %s" ) % [ mission_id, level_id, origin ]) return null func get_play_time() -> int: return _time() - save_data.ctime func get_current_checkpoint() -> Checkpoint: assert(in_the_right_map()) return save_data.checkpoint func set_checkpoint(where: LoadingZone) -> void: assert( actually_in_a_map() and in_the_right_map() and where.is_inside_tree() ) save_data.checkpoint.location.loading_zone = where.name save_data.checkpoint.time = HUD.time func get_game_mode() -> LevelDescriptor.GameMode: return save_data.checkpoint.game_mode func set_game_mode(game_mode: LevelDescriptor.GameMode): save_data.checkpoint.game_mode = game_mode if game_mode == LevelDescriptor.GameMode.HUB: HUD.stop_timer() else: HUD.start_timer() func get_current_character() -> CharacterProfile: return _characters[save_data.checkpoint.character] func _transition_out() -> void: await _transition_lock.acquire() PlayerControl.pause() await FX.fade(Color.BLACK) func _transition_in( fx: ScreenEffectsConfiguration = null ) -> void: var do_fade := func(): if not fx or not fx.set_fade: await FX.clear_fade() var do_other := func(): if fx: await fx.apply(ScreenEffects.DEFAULT_TRANSITION) await Promise.new([ do_fade, do_other ]).join() PlayerControl.unpause() _transition_lock.relinquish() func _instantiate_runner() -> Runner: var runner := get_current_character().runner.instantiate() as Runner get_tree().current_scene.add_child(runner) var old_runner := PlayerControl.get_runner() if old_runner: runner.global_position = old_runner.global_position PlayerControl.switch_runner(runner) if old_runner: old_runner.queue_free() return runner func switch_character(whom: StringName) -> void: await _lock.acquire() save_data.switch_character_story_state(whom) if actually_in_a_map(): await _transition_out() _instantiate_runner() await _transition_in() _lock.relinquish() func in_a_map() -> bool: return not save_data.checkpoint.location.map.level.is_empty() func actually_in_a_map() -> bool: return not _map_loaded.level.is_empty() func in_the_right_map() -> bool: return ( save_data.checkpoint.location.map.level == _map_loaded.level and save_data.checkpoint.location.map.mission == _map_loaded.mission ) func get_actual_map() -> MapID: return _map_loaded.dupicate() func get_level_we_should_be_in() -> LevelDescriptor: return ( null if save_data.checkpoint.location.map.level.is_empty() else get_level(save_data.checkpoint.location.map.level) ) func get_current_level() -> LevelDescriptor: assert(in_the_right_map()) return get_level_we_should_be_in() func get_mission_we_should_be_in() -> MissionDescriptor: var id := save_data.checkpoint.location.map.mission if id.is_empty(): return null else: return get_mission( save_data.checkpoint.location.map.level, id ) func get_current_mission() -> MissionDescriptor: assert(in_the_right_map()) return get_mission_we_should_be_in() func get_last_loading_zone() -> LoadingZone: assert(actually_in_a_map() and in_the_right_map()) return get_tree().current_scene.get_node( save_data.checkpoint.location.loading_zone as String as NodePath ) func _wait_for_scene_change_to_take() -> void: get_tree().paused = false while ( not get_tree().current_scene or not get_tree().current_scene.is_node_ready() ): await get_tree().process_frame get_tree().paused = true func go_to_non_level_scene( scene: PackedScene, fx: ScreenEffectsConfiguration = null ) -> void: await _lock.acquire() await _transition_out() _map_loaded.level = &'' if save_data.checkpoint: save_data.checkpoint.location.map.level = &'' save_data.checkpoint.location.map.mission = &'' save_data.checkpoint.location.loading_zone = &'' get_tree().change_scene_to_packed(scene) await _wait_for_scene_change_to_take() await _transition_in(fx) _lock.relinquish() func _change_scene_to_node(scene: Node) -> void: var prev_scene := get_tree().current_scene get_tree().root.remove_child(prev_scene) get_tree().current_scene = null await Wait.tick() prev_scene.free() get_tree().root.add_child(scene) get_tree().current_scene = scene func _go_to_map_based_on_save_data(show_level_card: bool) -> void: await _transition_out() FX.get_camera().detach() var level_descriptor := get_level( save_data.checkpoint.location.map.level ) var mission_descriptor: MissionDescriptor = ( null if save_data.checkpoint.location.map.mission.is_empty() else get_mission( save_data.checkpoint.location.map.level, save_data.checkpoint.location.map.mission ) ) var level_card: LevelCardCutIn = null if not in_the_right_map(): _map_loaded.level = &'' if show_level_card: UI.Call(load("res://ui/LevelCardCutIn/LevelCardCutIn.tscn"), { &'level_descriptor': level_descriptor, &'mission_descriptor': mission_descriptor, &'game_mode': save_data.checkpoint.game_mode, &'save_data': save_data.get_map_completion_mark( save_data.checkpoint.character, level_descriptor.id, mission_descriptor.id if mission_descriptor else &'', false ), &'no_dismiss': true, &'ui_open_sound_override': null, &'ui_close_sound_override': null }) level_card = UI.context() await level_card.storyboard_should_start_loading_level map_loaded.connect( level_card.on_loading_finished, CONNECT_ONE_SHOT ) var scene := (load( mission_descriptor.scene_path_override if ( mission_descriptor and not mission_descriptor.scene_path_override.is_empty() ) else level_descriptor.scene_path ) as PackedScene).instantiate() MapPreinit.setup(scene) await _change_scene_to_node(scene) _map_loaded.level = level_descriptor.id _map_loaded.mission = ( mission_descriptor.id if mission_descriptor else &'' ) assert(in_the_right_map()) await _wait_for_scene_change_to_take() _instantiate_runner() var runner := PlayerControl.get_runner() as Runner var loading_zone := get_last_loading_zone() if loading_zone: loading_zone.dropoff(runner) else: push_error("Loading zone not found: %s" % save_data.checkpoint.location.loading_zone) runner.global_position = Vector3.UP runner.up_normal = Vector3.UP FX.hard_reorient_camera() var bgm: AudioStream = ( mission_descriptor.bgm_override if ( mission_descriptor and mission_descriptor.bgm_override ) else level_descriptor.bgm ) var fx: ScreenEffectsConfiguration = ( mission_descriptor.fx_override if ( mission_descriptor and mission_descriptor.fx_override ) else level_descriptor.fx ) if bgm: FX.play_bgm(bgm) map_loaded.emit() if level_card: await UI.returned await _transition_in(fx) if save_data.checkpoint.game_mode == LevelDescriptor.GameMode.HUB: HUD.stop_timer() save_data.saved_hub_world_location.map.level = ( save_data.checkpoint.location.map.level ) save_data.saved_hub_world_location.map.mission = ( save_data.checkpoint.location.map.mission ) save_data.saved_hub_world_location.loading_zone = ( save_data.checkpoint.location.loading_zone ) else: HUD.start_timer(save_data.checkpoint.time) func go_to_map(params: LevelEntranceParameters) -> void: await _lock.acquire() if params is GameStartParameters: save_data.switch_character_story_state(params.character) save_data.checkpoint.character = params.character if not params.level.is_empty(): save_data.saved_hub_world_location.map.level = params.level if not params.mission.is_empty(): save_data.saved_hub_world_location.map.mission = params.mission if not params.loading_zone.is_empty(): save_data.saved_hub_world_location.loading_zone = ( params.loading_zone ) else: var in_a_hub_now := ( in_a_map() and in_the_right_map() and get_game_mode() == LevelDescriptor.GameMode.HUB ) var will_be_in_a_hub := ( params.game_mode == LevelDescriptor.GameMode.HUB ) if in_a_hub_now and not will_be_in_a_hub: save_data.saved_hub_world_location.map.level = ( save_data.checkpoint.location.map.level ) save_data.saved_hub_world_location.map.mission = ( save_data.checkpoint.location.map.mission ) save_data.saved_hub_world_location.loading_zone = ( save_data.checkpoint.location.loading_zone if params.origin_loading_zone.is_empty() else params.origin_loading_zone ) _map_loaded.level = &'' var level_descriptor := get_level(params.level) var mission_descriptor: MissionDescriptor = ( null if params.mission.is_empty() else get_mission(params.level, params.mission) ) save_data.checkpoint.location.map.level = level_descriptor.id save_data.checkpoint.location.map.mission = ( mission_descriptor.id if mission_descriptor else &'' ) var loading_zone := params.loading_zone if loading_zone.is_empty(): loading_zone = ( mission_descriptor.fast_travel_entrance_override if ( mission_descriptor and not mission_descriptor.fast_travel_entrance_override.is_empty() ) else level_descriptor.fast_travel_entrance ) save_data.checkpoint.location.loading_zone = loading_zone save_data.checkpoint.time = 0.0 save_data.checkpoint.game_mode = params.game_mode await _go_to_map_based_on_save_data(bool(params.show_level_card)) _lock.relinquish() func return_to_hub_world() -> void: var params := LevelEntranceParameters.new() params.game_mode = LevelDescriptor.GameMode.HUB params.level = save_data.saved_hub_world_location.map.level params.mission = save_data.saved_hub_world_location.map.mission params.loading_zone = save_data.saved_hub_world_location.loading_zone params.origin_loading_zone = &'' params.show_level_card = false await go_to_map(params) func get_map_supported_game_modes( level: StringName, mission: StringName = &'' ) -> Array[LevelDescriptor.GameMode]: if not allowed_into_map(level, mission): return [] elif mission.is_empty(): var a := get_level(level).game_modes_supported.duplicate() var i: int = a.find(LevelDescriptor.GameMode.MISSION) if i >= 0: a.remove_at(i) return a else: return [LevelDescriptor.GameMode.MISSION] func map_freerun_available(level: StringName) -> bool: var game_modes := get_map_supported_game_modes(level) if not (allowed_into_map(level) and ( LevelDescriptor.GameMode.FREERUN in game_modes )): return false if LevelDescriptor.GameMode.MISSION in game_modes: for mission in get_level(level).missions: if map_completed(level, mission.id): return true return false else: return true func allowed_into_map( level: StringName, mission: StringName = &'' ) -> bool: var level_descriptor := get_level(level) var mission_descriptor: MissionDescriptor = ( null if mission.is_empty() else get_mission(level, mission) ) var character := get_current_character() return map_unlocked(level, mission) and ( (mission_descriptor and character.id in mission_descriptor.characters_allowed) or ((not mission_descriptor) and character.id in level_descriptor.characters_allowed) ) func character_unlocked(character: StringName) -> bool: return character in save_data.characters_unlocked func unlock_character(character: StringName) -> void: if not character in save_data.characters_unlocked: save_data.characters_unlocked.push_back(character) func character_story_completed(character: StringName) -> bool: return character in save_data.character_stories_completed func set_character_story_as_completed(character: StringName) -> void: if not character in save_data.character_stories_completed: save_data.character_stories_completed.push_back(character) func map_unlocked(level: StringName, mission: StringName = &'') -> bool: return not not save_data.get_map_unlock_mark( save_data.checkpoint.character, level, mission, false ) func map_completed(level: StringName, mission: StringName = &'') -> bool: return not not save_data.get_map_completion_mark( save_data.checkpoint.character, level, mission, false ) func get_best_time(level: StringName, mission: StringName = &'') -> float: var mark := save_data.get_map_completion_mark( save_data.checkpoint.character, level, mission, false ) if mark: return mark.best_time else: return NAN func unlock_map(level: StringName, mission: StringName = &'') -> void: save_data.get_map_unlock_mark( save_data.checkpoint.character, level, mission, true ) func set_map_as_completed_and_check_if_personal_best( level: StringName, mission: StringName = &'' ) -> bool: var mark := save_data.get_map_completion_mark( save_data.checkpoint.character, level, mission, true ) if is_nan(mark.best_time) or HUD.time < mark.best_time: mark.best_time = HUD.time return true else: return false func follow_map_connection(loading_zone: StringName) -> void: assert(in_a_map() and in_the_right_map()) var mc := get_map_connection( save_data.checkpoint.location.map.level, save_data.checkpoint.location.map.mission, loading_zone ) assert(mc) var params := LevelEntranceParameters.new() params.level = mc.destination.map.level params.mission = mc.destination.map.mission params.loading_zone = mc.destination.loading_zone var supported_game_modes := get_map_supported_game_modes( params.level, params.mission ) if params.mission.is_empty(): params.game_mode = get_game_mode() if params.game_mode == LevelDescriptor.GameMode.MISSION: params.game_mode = LevelDescriptor.GameMode.HUB if not (params.game_mode in supported_game_modes): if params.game_mode == LevelDescriptor.GameMode.HUB: params.game_mode = LevelDescriptor.GameMode.FREERUN else: params.game_mode = LevelDescriptor.GameMode.HUB else: params.game_mode = LevelDescriptor.GameMode.MISSION params.show_level_card = bool( params.game_mode != LevelDescriptor.GameMode.HUB and not ( params.game_mode == get_game_mode() and params.level == save_data.checkpoint.location.map.level and params.mission == save_data.checkpoint.location.map.mission ) ) params.origin_loading_zone = loading_zone await go_to_map(params) func try_follow_map_connection(loading_zone: StringName) -> bool: var mc := get_map_connection( save_data.checkpoint.location.map.level, save_data.checkpoint.location.map.mission, loading_zone, false ) if mc and allowed_into_map( mc.destination.map.level, mc.destination.map.mission ): await follow_map_connection(loading_zone) return true else: return false func try_go_to_map(params: LevelEntranceParameters) -> bool: if not map_unlocked(params.level, params.mission): return false var supported_game_modes = get_map_supported_game_modes( params.level, params.mission ) if not (params.game_mode in supported_game_modes): return false if params.game_mode == LevelDescriptor.GameMode.FREERUN and not ( map_freerun_available(params.level) ): return false await go_to_map(params) return true func count_cleared_missions( level: StringName = &'', icosahedra_only: bool = false ) -> int: var count: int = 0 for completion_mark in save_data.maps_completed: if ( level.is_empty() or completion_mark.map.level == level ) and ((not icosahedra_only) or get_mission( completion_mark.map.level, completion_mark.map.mission ).awards_icosahedron): count += 1 return count