stick-the-quick/autoload/Storyboard/Storyboard.gd

634 lines
18 KiB
GDScript

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