aboutsummaryrefslogtreecommitdiff
path: root/game/src
diff options
context:
space:
mode:
Diffstat (limited to 'game/src')
-rw-r--r--game/src/Autoload/SaveManager.gd55
-rw-r--r--game/src/GameMenu.tscn2
-rw-r--r--game/src/GameSession/GameSession.tscn14
-rw-r--r--game/src/GameSession/GameSessionMenu.gd10
-rw-r--r--game/src/GameSession/GameSessionMenu.tscn4
-rw-r--r--game/src/LobbyMenu/LobbyMenu.gd145
-rw-r--r--game/src/LobbyMenu/LobbyMenu.tscn65
-rw-r--r--game/src/LobbyMenu/LobbyPanelButton.gd102
-rw-r--r--game/src/LobbyMenu/LobbyPanelButton.tscn30
-rw-r--r--game/src/SaveLoadMenu/SaveLoadMenu.gd119
-rw-r--r--game/src/SaveLoadMenu/SaveLoadMenu.tscn109
-rw-r--r--game/src/SaveLoadMenu/SavePanelButton.gd41
-rw-r--r--game/src/SaveLoadMenu/SavePanelButton.tscn58
-rw-r--r--game/src/SaveLoadMenu/SaveResource.gd59
14 files changed, 792 insertions, 21 deletions
diff --git a/game/src/Autoload/SaveManager.gd b/game/src/Autoload/SaveManager.gd
new file mode 100644
index 0000000..c653b2c
--- /dev/null
+++ b/game/src/Autoload/SaveManager.gd
@@ -0,0 +1,55 @@
+extends Node
+
+# Requirements
+# * FS-28
+const save_directory_setting := &"openvic2/data/saves_directory"
+
+var current_save : SaveResource
+var current_session_tag : StringName
+
+var _save_dictionary : Dictionary = {}
+var _dirty_save : SaveResource
+
+func _ready():
+ var saves_dir_path : String = ProjectSettings.get_setting_with_override(save_directory_setting)
+ assert(saves_dir_path != null, "'%s' setting could not be found." % save_directory_setting)
+
+ DirAccess.make_dir_recursive_absolute(saves_dir_path)
+ var saves_dir := DirAccess.open(saves_dir_path)
+ for file in saves_dir.get_files():
+ var save := SaveResource.new()
+ save.load_save(saves_dir_path.path_join(file))
+ add_or_replace_save(save, true)
+
+func get_save_file_name(save_name : StringName, session_tag : StringName = current_session_tag) -> StringName:
+ return ("%s - %s" % [save_name, session_tag]).validate_filename()
+
+func make_new_save(save_name : String, session_tag : StringName = current_session_tag) -> SaveResource:
+ var file_name := get_save_file_name(save_name, session_tag) + ".tres"
+ var new_save := SaveResource.new()
+ new_save.set_file_path(save_name, ProjectSettings.get_setting_with_override(save_directory_setting).path_join(file_name))
+ print(new_save.file_path)
+ new_save.session_tag = session_tag
+ return new_save
+
+func has_save(save_name : StringName, session_tag : StringName = current_session_tag) -> bool:
+ return _save_dictionary.has(get_save_file_name(save_name, session_tag))
+
+func add_or_replace_save(save : SaveResource, ignore_dirty : bool = false) -> void:
+ var binded_func := _on_save_deleted_or_moved.bind(save)
+ save.deleted.connect(binded_func)
+ save.trash_moved.connect(binded_func)
+ _save_dictionary[get_save_file_name(save.save_name, save.session_tag)] = save
+ if not ignore_dirty:
+ _dirty_save = save
+
+func delete_save(save : SaveResource) -> void:
+ save.delete()
+
+func flush_save() -> void:
+ if _dirty_save == null: return
+ _dirty_save.flush_save()
+ _dirty_save = null
+
+func _on_save_deleted_or_moved(save : SaveResource) -> void:
+ _save_dictionary.erase(get_save_file_name(save.save_name, save.session_tag))
diff --git a/game/src/GameMenu.tscn b/game/src/GameMenu.tscn
index c642351..224ae2e 100644
--- a/game/src/GameMenu.tscn
+++ b/game/src/GameMenu.tscn
@@ -4,7 +4,7 @@
[ext_resource type="PackedScene" uid="uid://bp5n3mlu45ygw" path="res://src/MainMenu/MainMenu.tscn" id="2_2jbkh"]
[ext_resource type="PackedScene" uid="uid://cnbfxjy1m6wja" path="res://src/OptionMenu/OptionsMenu.tscn" id="3_111lv"]
[ext_resource type="PackedScene" uid="uid://c8knthxkwj1uj" path="res://src/CreditsMenu/CreditsMenu.tscn" id="4_n0hoo"]
-[ext_resource type="PackedScene" uid="uid://crhkgngfnxf4y" path="res://src/LobbyMenu/LobbyMenu.tscn" id="4_nofk1"]
+[ext_resource type="PackedScene" uid="uid://do60kx0d3nrh4" path="res://src/LobbyMenu/LobbyMenu.tscn" id="4_nofk1"]
[ext_resource type="PackedScene" uid="uid://cvl76duuym1wq" path="res://src/MusicConductor/MusicPlayer.tscn" id="6_lts1m"]
[node name="GameMenu" type="Control" node_paths=PackedStringArray("_main_menu", "_options_menu", "_lobby_menu", "_credits_menu")]
diff --git a/game/src/GameSession/GameSession.tscn b/game/src/GameSession/GameSession.tscn
index ce3471f..996891a 100644
--- a/game/src/GameSession/GameSession.tscn
+++ b/game/src/GameSession/GameSession.tscn
@@ -1,4 +1,4 @@
-[gd_scene load_steps=9 format=3 uid="uid://bgnupcshe1m7r"]
+[gd_scene load_steps=10 format=3 uid="uid://bgnupcshe1m7r"]
[ext_resource type="Script" path="res://src/GameSession/GameSession.gd" id="1_eklvp"]
[ext_resource type="PackedScene" uid="uid://cvl76duuym1wq" path="res://src/MusicConductor/MusicPlayer.tscn" id="2_kt6aa"]
@@ -8,6 +8,7 @@
[ext_resource type="PackedScene" uid="uid://byq323jbel48u" path="res://src/GameSession/ProvinceOverviewPanel/ProvinceOverviewPanel.tscn" id="5_osjnn"]
[ext_resource type="PackedScene" uid="uid://cnbfxjy1m6wja" path="res://src/OptionMenu/OptionsMenu.tscn" id="6_p5mnx"]
[ext_resource type="PackedScene" uid="uid://dd8k3p7r3huwc" path="res://src/GameSession/GameSpeedPanel.tscn" id="7_myy4q"]
+[ext_resource type="PackedScene" uid="uid://dayy28gn8kq34" path="res://src/SaveLoadMenu/SaveLoadMenu.tscn" id="8_4g7ko"]
[node name="GameSession" type="Control" node_paths=PackedStringArray("_game_session_menu")]
editor_description = "SS-102, UI-546"
@@ -56,6 +57,15 @@ offset_bottom = 31.0
visible = false
layout_mode = 1
+[node name="SaveLoadMenu" parent="." instance=ExtResource("8_4g7ko")]
+visible = false
+layout_mode = 1
+anchors_preset = -1
+anchor_left = 0.5
+anchor_right = 0.5
+offset_left = -640.0
+offset_right = 640.0
+
[node name="MusicPlayer" parent="." instance=ExtResource("2_kt6aa")]
layout_mode = 1
anchors_preset = 1
@@ -66,7 +76,9 @@ offset_right = 0.0
grow_horizontal = 0
[connection signal="map_view_camera_changed" from="MapView" to="MapControlPanel" method="_on_map_view_camera_changed"]
+[connection signal="load_button_pressed" from="GameSessionMenu" to="SaveLoadMenu" method="show_for_load"]
[connection signal="options_button_pressed" from="GameSessionMenu" to="OptionsMenu" method="show"]
+[connection signal="save_button_pressed" from="GameSessionMenu" to="SaveLoadMenu" method="show_for_save"]
[connection signal="game_session_menu_button_pressed" from="MapControlPanel" to="." method="_on_game_session_menu_button_pressed"]
[connection signal="minimap_clicked" from="MapControlPanel" to="MapView" method="_on_minimap_clicked"]
[connection signal="mouse_entered" from="MapControlPanel" to="MapView" method="_on_mouse_exited_viewport"]
diff --git a/game/src/GameSession/GameSessionMenu.gd b/game/src/GameSession/GameSessionMenu.gd
index 6f373d7..23ef2ef 100644
--- a/game/src/GameSession/GameSessionMenu.gd
+++ b/game/src/GameSession/GameSessionMenu.gd
@@ -10,6 +10,8 @@ var _main_menu_save_separator : Control
var _quit_save_button : Button
var _quit_save_separator : Control
+signal save_button_pressed
+signal load_button_pressed
signal options_button_pressed
func _ready() -> void:
@@ -45,6 +47,8 @@ func show_save_dialog_button() -> void:
# * SS-47
# * UIFUN-69
func _on_main_menu_confirmed() -> void:
+ SaveManager.current_session_tag = ""
+ SaveManager.current_save = null
get_tree().change_scene_to_packed(_main_menu_scene)
# REQUIREMENTS:
@@ -68,3 +72,9 @@ func _on_quit_dialog_custom_action(action : StringName) -> void:
match action:
&"save_and_quit":
_on_quit_confirmed()
+
+func _on_save_button_pressed():
+ save_button_pressed.emit()
+
+func _on_load_button_pressed():
+ load_button_pressed.emit()
diff --git a/game/src/GameSession/GameSessionMenu.tscn b/game/src/GameSession/GameSessionMenu.tscn
index 99f38df..025ef3b 100644
--- a/game/src/GameSession/GameSessionMenu.tscn
+++ b/game/src/GameSession/GameSessionMenu.tscn
@@ -1,6 +1,6 @@
[gd_scene load_steps=4 format=3 uid="uid://dvdynl6eir40o"]
-[ext_resource type="Theme" uid="uid://dndova5cw036e" path="res://theme/game_session_menu.tres" id="1_2onog"]
+[ext_resource type="Theme" uid="uid://cqrfmjt5yeti7" path="res://theme/game_session_menu.tres" id="1_2onog"]
[ext_resource type="Script" path="res://src/GameSession/GameSessionMenu.gd" id="1_usq6o"]
[ext_resource type="PackedScene" uid="uid://o4u142w4qkln" path="res://src/GameMenu.tscn" id="2_xi6a4"]
@@ -79,6 +79,8 @@ ok_button_text = "DIALOG_OK"
dialog_text = "GAMESESSIONMENU_QUIT_DIALOG_TEXT"
cancel_button_text = "DIALOG_CANCEL"
+[connection signal="pressed" from="ButtonListMargin/ButtonList/SaveButton" to="." method="_on_save_button_pressed"]
+[connection signal="pressed" from="ButtonListMargin/ButtonList/LoadButton" to="." method="_on_load_button_pressed"]
[connection signal="pressed" from="ButtonListMargin/ButtonList/OptionsButton" to="." method="_on_options_button_pressed"]
[connection signal="pressed" from="ButtonListMargin/ButtonList/MainMenuButton" to="MainMenuDialog" method="popup_centered"]
[connection signal="pressed" from="ButtonListMargin/ButtonList/QuitButton" to="QuitDialog" method="popup_centered"]
diff --git a/game/src/LobbyMenu/LobbyMenu.gd b/game/src/LobbyMenu/LobbyMenu.gd
index 4fc06c9..3cd3b40 100644
--- a/game/src/LobbyMenu/LobbyMenu.gd
+++ b/game/src/LobbyMenu/LobbyMenu.gd
@@ -5,25 +5,97 @@ extends HBoxContainer
# * SS-12
signal back_button_pressed
-signal save_game_selected
+signal save_game_selected(save : SaveResource)
+signal start_date_selected(index : int)
-@export
-var start_button : BaseButton
+@export var lobby_panel_button : PackedScene
+@export var save_scene : PackedScene
+
+@export_group("Nodes")
+@export var game_select_start_date : BoxContainer
+@export var game_select_save_tab : TabBar
+@export var game_select_save_list : BoxContainer
+@export var start_button : BaseButton
+@export var session_tag_line_edit : LineEdit
+@export var session_tag_dialog : ConfirmationDialog
+@export var delete_dialog : ConfirmationDialog
+
+func filter_for_tag(tag : StringName) -> void:
+ for child in game_select_save_list.get_children():
+ if tag == &"":
+ child.show()
+ else:
+ if tag == child.resource.session_tag:
+ child.show()
+ else:
+ child.hide()
+
+func _build_date_list() -> void:
+ var start_date := lobby_panel_button.instantiate()
+ start_date.set_text(&"1836")
+ start_date.pressed.connect(_on_start_date_panel_button_pressed.bind(start_date))
+ game_select_start_date.add_child(start_date)
+ start_date = lobby_panel_button.instantiate()
+ start_date.set_text(&"1863")
+ start_date.pressed.connect(_on_start_date_panel_button_pressed.bind(start_date))
+ game_select_start_date.add_child(start_date)
+
+var _id_to_tag : Array[StringName] = []
+func _build_save_list() -> void:
+ game_select_save_tab.add_tab("GAMELOBBY_SELECT_ALL")
+ for save_name in SaveManager._save_dictionary:
+ var save : SaveResource = SaveManager._save_dictionary[save_name]
+ var save_node := _create_save_node(save)
+ game_select_save_list.add_child(save_node)
+ if not _id_to_tag.has(save.session_tag):
+ _id_to_tag.append(save.session_tag)
+ game_select_save_tab.add_tab(save.session_tag)
+
+func _create_save_node(resource : SaveResource) -> Control:
+ var save_node = save_scene.instantiate()
+ save_node.resource = resource
+ save_node.pressed.connect(_on_save_node_pressed.bind(save_node))
+ save_node.request_to_delete.connect(_on_save_node_delete_requested.bind(save_node))
+ return save_node
+
+func _queue_clear_lists() -> void:
+ var full_list = game_select_start_date.get_children()
+ full_list.append_array(game_select_save_list.get_children())
+ for child in full_list:
+ child.queue_free()
+ game_select_save_tab.clear_tabs()
+ _id_to_tag.clear()
# REQUIREMENTS:
# * SS-16
# * UIFUN-40
func _on_back_button_button_down():
print("Returning to Main Menu.")
+ SaveManager.current_session_tag = ""
+ SaveManager.current_save = null
back_button_pressed.emit()
-
# REQUIREMENTS:
# * SS-21
-func _on_start_button_button_down():
+func _on_start_button_pressed():
print("Starting new game.")
- get_tree().change_scene_to_file("res://src/GameSession/GameSession.tscn")
-
+ if SaveManager.current_session_tag == "":
+ # TODO: Get country tag as well
+ var datetime := Time.get_datetime_dict_from_system()
+ SaveManager.current_session_tag = "%s/%s/%s-%s:%s:%s" % [
+ datetime["day"],
+ datetime["month"],
+ datetime["year"],
+ datetime["hour"],
+ datetime["minute"],
+ datetime["second"]
+ ]
+ if SaveManager.current_save == null and SaveManager.current_session_tag in _id_to_tag:
+ session_tag_dialog.dialog_text = tr("GAMELOBBY_SESSIONTAG_DIALOG_TEXT").format({ "session_tag": SaveManager.current_session_tag })
+ session_tag_dialog.title = tr("GAMELOBBY_SESSIONTAG_DIALOG_TITLE").format({ "session_tag": SaveManager.current_session_tag })
+ session_tag_dialog.popup_centered()
+ else:
+ _on_session_tag_dialog_confirmed()
# REQUIREMENTS:
# * SS-19
@@ -31,11 +103,60 @@ func _on_game_select_list_item_selected(index):
print("Selected save game: ", index)
save_game_selected.emit(index)
-
-func _on_save_game_selected(_index):
- start_button.disabled = false
-
# If the date is double-clicked, start the game!
func _on_game_select_list_item_activated(index):
_on_game_select_list_item_selected(index)
- _on_start_button_button_down()
+ _on_start_button_pressed()
+
+func _on_session_tag_edit_text_submitted(new_text):
+ SaveManager.current_session_tag = new_text
+ _on_start_button_pressed()
+
+func _on_session_tag_dialog_confirmed():
+ get_tree().change_scene_to_file("res://src/GameSession/GameSession.tscn")
+
+var _requested_node_to_delete : Control
+func _on_save_node_delete_requested(node : Control) -> void:
+ _requested_node_to_delete = node
+ delete_dialog.dialog_text = tr("GAMELOBBY_DELETE_DIALOG_TEXT").format({ "file_name": _requested_node_to_delete.resource.save_name })
+ delete_dialog.title = tr("GAMELOBBY_DELETE_DIALOG_TITLE").format({ "file_name": _requested_node_to_delete.resource.save_name })
+ delete_dialog.popup_centered()
+
+var _start_date_index := -1
+func _on_start_date_panel_button_pressed(node : Control) -> void:
+ if node is LobbyPanelButton and node.get_index(true) == _start_date_index:
+ _on_start_button_pressed()
+ return
+ _start_date_index = node.get_index(true)
+ start_button.disabled = false
+ start_date_selected.emit(_start_date_index)
+
+func _on_save_node_pressed(node : Control) -> void:
+ if SaveManager.current_save != null and SaveManager.current_save == node.resource:
+ SaveManager.current_session_tag = SaveManager.current_save.session_tag
+ _on_start_button_pressed()
+ return
+ SaveManager.current_save = node.resource
+ if SaveManager.current_save != null:
+ session_tag_line_edit.text = SaveManager.current_save.session_tag
+ else:
+ session_tag_line_edit.text = ""
+ start_button.disabled = false
+ save_game_selected.emit(SaveManager.current_save)
+
+func _on_game_select_save_tab_tab_changed(tab) -> void:
+ if tab == 0:
+ filter_for_tag(&"")
+ else:
+ filter_for_tag(_id_to_tag[tab - 1])
+
+func _on_delete_dialog_confirmed():
+ _requested_node_to_delete.resource.delete()
+ _requested_node_to_delete.queue_free()
+
+func _on_visibility_changed():
+ if visible:
+ _build_date_list()
+ _build_save_list()
+ else:
+ _queue_clear_lists()
diff --git a/game/src/LobbyMenu/LobbyMenu.tscn b/game/src/LobbyMenu/LobbyMenu.tscn
index 174fb72..63a66d8 100644
--- a/game/src/LobbyMenu/LobbyMenu.tscn
+++ b/game/src/LobbyMenu/LobbyMenu.tscn
@@ -1,8 +1,10 @@
-[gd_scene load_steps=2 format=3 uid="uid://crhkgngfnxf4y"]
+[gd_scene load_steps=4 format=3 uid="uid://do60kx0d3nrh4"]
[ext_resource type="Script" path="res://src/LobbyMenu/LobbyMenu.gd" id="1_cvwum"]
+[ext_resource type="PackedScene" uid="uid://k71f5gibwmtc" path="res://src/LobbyMenu/LobbyPanelButton.tscn" id="2_exh17"]
+[ext_resource type="PackedScene" uid="uid://d2s7roinx2or7" path="res://src/SaveLoadMenu/SavePanelButton.tscn" id="3_4otj7"]
-[node name="LobbyMenu" type="HBoxContainer" node_paths=PackedStringArray("start_button")]
+[node name="LobbyMenu" type="HBoxContainer" node_paths=PackedStringArray("game_select_start_date", "game_select_save_tab", "game_select_save_list", "start_button", "session_tag_line_edit", "session_tag_dialog", "delete_dialog")]
editor_description = "UI-36"
anchors_preset = 15
anchor_right = 1.0
@@ -10,7 +12,15 @@ anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_cvwum")
+lobby_panel_button = ExtResource("2_exh17")
+save_scene = ExtResource("3_4otj7")
+game_select_start_date = NodePath("GameSelectPanel/VBoxContainer/GameSelectScroll/GameSelectList/GameSelectStartDate")
+game_select_save_tab = NodePath("GameSelectPanel/VBoxContainer/GameSelectScroll/GameSelectList/GameSelectSaveTab")
+game_select_save_list = NodePath("GameSelectPanel/VBoxContainer/GameSelectScroll/GameSelectList/GameSelectSaveList")
start_button = NodePath("GameStartPanel/VBoxContainer/StartButton")
+session_tag_line_edit = NodePath("GameStartPanel/VBoxContainer/SessionTagEdit")
+session_tag_dialog = NodePath("SessionTagDialog")
+delete_dialog = NodePath("DeleteDialog")
[node name="GameSelectPanel" type="PanelContainer" parent="."]
layout_mode = 2
@@ -19,7 +29,30 @@ size_flags_horizontal = 3
[node name="VBoxContainer" type="VBoxContainer" parent="GameSelectPanel"]
layout_mode = 2
+[node name="GameSelectScroll" type="ScrollContainer" parent="GameSelectPanel/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+horizontal_scroll_mode = 0
+
+[node name="GameSelectList" type="VBoxContainer" parent="GameSelectPanel/VBoxContainer/GameSelectScroll"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="GameSelectStartDate" type="VBoxContainer" parent="GameSelectPanel/VBoxContainer/GameSelectScroll/GameSelectList"]
+layout_mode = 2
+
+[node name="GameSelectSaveTab" type="TabBar" parent="GameSelectPanel/VBoxContainer/GameSelectScroll/GameSelectList"]
+layout_mode = 2
+tab_count = 1
+tab_0/title = "GAMELOBBY_SELECT_ALL"
+
+[node name="GameSelectSaveList" type="VBoxContainer" parent="GameSelectPanel/VBoxContainer/GameSelectScroll/GameSelectList"]
+layout_mode = 2
+size_flags_vertical = 3
+
[node name="GameSelectList" type="ItemList" parent="GameSelectPanel/VBoxContainer"]
+visible = false
layout_mode = 2
size_flags_vertical = 3
item_count = 2
@@ -67,6 +100,10 @@ custom_minimum_size = Vector2(0, 150)
layout_mode = 2
size_flags_vertical = 3
+[node name="SessionTagEdit" type="LineEdit" parent="GameStartPanel/VBoxContainer"]
+layout_mode = 2
+placeholder_text = "GAMELOBBY_SESSION_TAG"
+
[node name="StartButton" type="Button" parent="GameStartPanel/VBoxContainer"]
editor_description = "UI-43"
layout_mode = 2
@@ -77,8 +114,24 @@ text = "GAMELOBBY_START"
custom_minimum_size = Vector2(0, 33)
layout_mode = 2
-[connection signal="save_game_selected" from="." to="." method="_on_save_game_selected"]
-[connection signal="item_activated" from="GameSelectPanel/VBoxContainer/GameSelectList" to="." method="_on_game_select_list_item_activated"]
-[connection signal="item_selected" from="GameSelectPanel/VBoxContainer/GameSelectList" to="." method="_on_game_select_list_item_selected"]
+[node name="SessionTagDialog" type="ConfirmationDialog" parent="."]
+disable_3d = true
+title = "GAMELOBBY_SESSIONTAG_DIALOG_TITLE"
+ok_button_text = "DIALOG_OK"
+dialog_text = "GAMELOBBY_SESSIONTAG_DIALOG_TEXT"
+cancel_button_text = "DIALOG_CANCEL"
+
+[node name="DeleteDialog" type="ConfirmationDialog" parent="."]
+disable_3d = true
+title = "GAMELOBBY_DELETE_DIALOG_TITLE"
+ok_button_text = "DIALOG_OK"
+dialog_text = "GAMELOBBY_DELETE_DIALOG_TEXT"
+cancel_button_text = "DIALOG_CANCEL"
+
+[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"]
+[connection signal="tab_changed" from="GameSelectPanel/VBoxContainer/GameSelectScroll/GameSelectList/GameSelectSaveTab" to="." method="_on_game_select_save_tab_tab_changed"]
[connection signal="button_down" from="GameSelectPanel/VBoxContainer/BackButton" to="." method="_on_back_button_button_down"]
-[connection signal="button_down" from="GameStartPanel/VBoxContainer/StartButton" to="." method="_on_start_button_button_down"]
+[connection signal="text_submitted" from="GameStartPanel/VBoxContainer/SessionTagEdit" to="." method="_on_session_tag_edit_text_submitted"]
+[connection signal="pressed" from="GameStartPanel/VBoxContainer/StartButton" to="." method="_on_start_button_pressed"]
+[connection signal="confirmed" from="SessionTagDialog" to="." method="_on_session_tag_dialog_confirmed"]
+[connection signal="confirmed" from="DeleteDialog" to="." method="_on_delete_dialog_confirmed"]
diff --git a/game/src/LobbyMenu/LobbyPanelButton.gd b/game/src/LobbyMenu/LobbyPanelButton.gd
new file mode 100644
index 0000000..5f3ea46
--- /dev/null
+++ b/game/src/LobbyMenu/LobbyPanelButton.gd
@@ -0,0 +1,102 @@
+@tool
+class_name LobbyPanelButton
+extends Container
+
+signal button_down
+signal button_up
+signal pressed
+signal toggled(button_pressed : bool)
+
+var is_start_date : bool:
+ get = _is_start_date
+
+func _is_start_date() -> bool:
+ return true
+
+@export_group("Nodes")
+@export var background_button : BaseButton
+@export var name_label : Label
+
+var text : StringName:
+ get = get_text,
+ set = set_text
+
+func get_text() -> StringName:
+ return name_label.text
+
+func set_text(value : StringName) -> void:
+ name_label.text = value
+
+func _get_minimum_size() -> Vector2:
+ var result := Vector2()
+ for child in get_children():
+ child = child as Control
+ if child == null or not child.visible:
+ continue
+ if child.top_level:
+ continue
+
+ var minsize : Vector2 = child.get_combined_minimum_size()
+ result.x = max(result.x, minsize.x)
+ result.y = max(result.y, minsize.y)
+
+ var draw_style := _get_draw_mode_style()
+ if draw_style != null:
+ result += draw_style.get_minimum_size()
+
+ return result
+
+func _get_draw_mode_name(support_rtl : bool = true) -> StringName:
+ var rtl := support_rtl and background_button != null and background_button.is_layout_rtl()
+ match background_button.get_draw_mode() if background_button != null else BaseButton.DrawMode.DRAW_NORMAL:
+ BaseButton.DrawMode.DRAW_NORMAL:
+ if rtl: return &"normal_mirrored"
+ return &"normal"
+ BaseButton.DrawMode.DRAW_PRESSED:
+ if rtl: return &"pressed_mirrored"
+ return &"pressed"
+ BaseButton.DrawMode.DRAW_HOVER:
+ if rtl: return &"hover_mirrored"
+ return &"hover"
+ BaseButton.DrawMode.DRAW_DISABLED:
+ if rtl: return &"disabled_mirrored"
+ return &"disabled"
+ BaseButton.DrawMode.DRAW_HOVER_PRESSED:
+ if rtl: return &"hover_pressed_mirrored"
+ return &"hover_pressed"
+ return &""
+
+func _get_draw_mode_style() -> StyleBox:
+ if background_button == null: return null
+ var result := background_button.get_theme_stylebox(_get_draw_mode_name())
+ if result == null:
+ return background_button.get_theme_stylebox(_get_draw_mode_name(false))
+ return result
+
+func _notification(what) -> void:
+ if what == NOTIFICATION_SORT_CHILDREN:
+ var _size := size
+ var offset := Vector2()
+ var style := _get_draw_mode_style()
+ if style != null:
+ _size -= style.get_minimum_size()
+ offset += style.get_offset()
+
+ for child in get_children():
+ child = child as Control
+ if child == null or not child.is_visible_in_tree() or child.top_level:
+ continue
+
+ fit_child_in_rect(child, Rect2(offset, _size))
+
+func _on_background_button_button_down():
+ button_down.emit()
+
+func _on_background_button_button_up():
+ button_up.emit()
+
+func _on_background_button_pressed():
+ pressed.emit()
+
+func _on_background_button_toggled(button_pressed : bool):
+ toggled.emit(button_pressed)
diff --git a/game/src/LobbyMenu/LobbyPanelButton.tscn b/game/src/LobbyMenu/LobbyPanelButton.tscn
new file mode 100644
index 0000000..2ba6c99
--- /dev/null
+++ b/game/src/LobbyMenu/LobbyPanelButton.tscn
@@ -0,0 +1,30 @@
+[gd_scene load_steps=2 format=3 uid="uid://k71f5gibwmtc"]
+
+[ext_resource type="Script" path="res://src/LobbyMenu/LobbyPanelButton.gd" id="1_327u2"]
+
+[node name="LobbyPanelButton" type="Container" node_paths=PackedStringArray("background_button", "name_label")]
+offset_right = 113.0
+offset_bottom = 48.0
+script = ExtResource("1_327u2")
+background_button = NodePath("BackgroundButton")
+name_label = NodePath("SaveList/NameLabel")
+
+[node name="BackgroundButton" type="Button" parent="."]
+layout_mode = 2
+theme_type_variation = &"ButtonContainer"
+
+[node name="SaveList" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 0
+mouse_filter = 2
+
+[node name="NameLabel" type="Label" parent="SaveList"]
+layout_mode = 2
+size_flags_vertical = 1
+text = "PLACEHOLDER"
+vertical_alignment = 1
+
+[connection signal="button_down" from="BackgroundButton" to="." method="_on_background_button_button_down"]
+[connection signal="button_up" from="BackgroundButton" to="." method="_on_background_button_button_up"]
+[connection signal="pressed" from="BackgroundButton" to="." method="_on_background_button_pressed"]
+[connection signal="toggled" from="BackgroundButton" to="." method="_on_background_button_toggled"]
diff --git a/game/src/SaveLoadMenu/SaveLoadMenu.gd b/game/src/SaveLoadMenu/SaveLoadMenu.gd
new file mode 100644
index 0000000..abf1f8c
--- /dev/null
+++ b/game/src/SaveLoadMenu/SaveLoadMenu.gd
@@ -0,0 +1,119 @@
+extends Control
+
+@export var _save_scene : PackedScene
+
+@export_group("Nodes")
+@export var _label : Label
+@export var _scroll_list : BoxContainer
+@export var _save_line_edit : LineEdit
+@export var _save_load_button : Button
+@export var _tag_selection_tab : TabBar
+@export var _delete_dialog : ConfirmationDialog
+@export var _overwrite_dialog : ConfirmationDialog
+
+var is_save_menu : bool = true
+var _id_to_tag : Array[StringName] = []
+
+func filter_for_tag(tag : StringName) -> void:
+ for child in _scroll_list.get_children():
+ if tag == &"":
+ child.show()
+ else:
+ if tag == child.resource.session_tag:
+ child.show()
+ else:
+ child.hide()
+
+func show_for_load() -> void:
+ _label.text = "SAVELOADMENU_LOAD_TITLE"
+ _save_load_button.text = "SAVELOADMENU_LOAD_BUTTON"
+ _save_line_edit.editable = false
+ is_save_menu = false
+ show()
+
+func show_for_save() -> void:
+ _label.text = "SAVELOADMENU_SAVE_TITLE"
+ _save_load_button.text = "SAVELOADMENU_SAVE_BUTTON"
+ _save_line_edit.editable = true
+ is_save_menu = true
+ show()
+
+func _build_save_list() -> void:
+ _tag_selection_tab.add_tab("SAVELOADMENU_TABSELECTIONTABBAR_ALL")
+ for save_name in SaveManager._save_dictionary:
+ var save : SaveResource = SaveManager._save_dictionary[save_name]
+ var save_node := _create_save_node(save)
+ _scroll_list.add_child(save_node)
+ if not _id_to_tag.has(save.session_tag):
+ _id_to_tag.append(save.session_tag)
+ _tag_selection_tab.add_tab(save.session_tag)
+
+func _create_save_node(resource : SaveResource) -> Control:
+ var save_node = _save_scene.instantiate()
+ save_node.resource = resource
+ save_node.pressed.connect(_on_save_node_pressed.bind(save_node))
+ save_node.request_to_delete.connect(_on_save_node_delete_requested.bind(save_node))
+ return save_node
+
+func _queue_clear_scroll_list() -> void:
+ for child in _scroll_list.get_children():
+ child.queue_free()
+ _tag_selection_tab.clear_tabs()
+ _id_to_tag.clear()
+
+# REQUIREMENTS:
+# * UIFUN-84
+# * UIFUN-89
+func _on_close_button_pressed() -> void:
+ hide()
+
+func _on_delete_dialog_confirmed() -> void:
+ _requested_node_to_delete.resource.delete()
+ _requested_node_to_delete.queue_free()
+
+# REQUIREMENTS:
+# * UIFUNC-83
+func _on_overwrite_dialog_confirmed() -> void:
+ SaveManager.add_or_replace_save(SaveManager.make_new_save(_submitted_text))
+ _on_close_button_pressed()
+
+var _submitted_text : String = ""
+func _on_save_line_edit_text_submitted(new_text) -> void:
+ _submitted_text = new_text
+ if SaveManager.has_save(new_text):
+ _overwrite_dialog.dialog_text = tr("SAVELOADMENU_OVERWRITE_DIALOG_TEXT").format({ "file_name": _submitted_text })
+ _overwrite_dialog.title = tr("SAVELOADMENU_OVERWRITE_DIALOG_TITLE").format({ "file_name": _submitted_text })
+ _overwrite_dialog.popup_centered()
+ return
+ _on_overwrite_dialog_confirmed()
+
+func _on_save_load_button_pressed() -> void:
+ if is_save_menu:
+ _save_line_edit.text_submitted.emit(_save_line_edit.text)
+
+var _requested_node_to_delete : Control
+func _on_save_node_delete_requested(node : Control) -> void:
+ _requested_node_to_delete = node
+ _delete_dialog.dialog_text = tr("SAVELOADMENU_DELETE_DIALOG_TEXT").format({ "file_name": _requested_node_to_delete.resource.save_name })
+ _delete_dialog.title = tr("SAVELOADMENU_DELETE_DIALOG_TITLE").format({ "file_name": _requested_node_to_delete.resource.save_name })
+ _delete_dialog.popup_centered()
+
+# REQUIREMENTS:
+# * UIFUN-81
+# * UIFUN-86
+func _on_save_node_pressed(node : Control) -> void:
+ if is_save_menu:
+ _save_line_edit.text = node.resource.save_name
+
+func _on_tag_selection_tab_bar_tab_changed(tab) -> void:
+ if tab == 0:
+ filter_for_tag(&"")
+ else:
+ filter_for_tag(_id_to_tag[tab - 1])
+
+func _on_visibility_changed() -> void:
+ if visible:
+ _build_save_list()
+ else:
+ _queue_clear_scroll_list()
+ SaveManager.flush_save()
diff --git a/game/src/SaveLoadMenu/SaveLoadMenu.tscn b/game/src/SaveLoadMenu/SaveLoadMenu.tscn
new file mode 100644
index 0000000..e9f068e
--- /dev/null
+++ b/game/src/SaveLoadMenu/SaveLoadMenu.tscn
@@ -0,0 +1,109 @@
+[gd_scene load_steps=5 format=3 uid="uid://d3g6wbvwflmyk"]
+
+[ext_resource type="Script" path="res://src/SaveLoadMenu/SaveLoadMenu.gd" id="1_3jkds"]
+[ext_resource type="PackedScene" uid="uid://d2s7roinx2or7" path="res://src/SaveLoadMenu/SavePanelButton.tscn" id="2_fc6r3"]
+
+[sub_resource type="InputEventAction" id="InputEventAction_8vo2t"]
+action = &"ui_accept"
+pressed = true
+
+[sub_resource type="Shortcut" id="Shortcut_o1f2l"]
+events = [SubResource("InputEventAction_8vo2t")]
+
+[node name="SaveLoadMenu" type="MarginContainer" node_paths=PackedStringArray("_label", "_scroll_list", "_save_line_edit", "_save_load_button", "_tag_selection_tab", "_delete_dialog", "_overwrite_dialog")]
+editor_description = "UI-82, UI-89"
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 250
+theme_override_constants/margin_top = 100
+theme_override_constants/margin_right = 250
+theme_override_constants/margin_bottom = 100
+script = ExtResource("1_3jkds")
+_save_scene = ExtResource("2_fc6r3")
+_label = NodePath("SaveLoadPanel/SaveLoadList/TitleBarList/SaveLoadLabel")
+_scroll_list = NodePath("SaveLoadPanel/SaveLoadList/SaveLoadScroll/SaveLoadScrollList")
+_save_line_edit = NodePath("SaveLoadPanel/SaveLoadList/SaveLineEdit")
+_save_load_button = NodePath("SaveLoadPanel/SaveLoadList/SaveLoadButton")
+_tag_selection_tab = NodePath("SaveLoadPanel/SaveLoadList/TagSelectionList/TagSelectionTabBar")
+_delete_dialog = NodePath("DeleteDialog")
+_overwrite_dialog = NodePath("OverwriteDialog")
+
+[node name="SaveLoadPanel" type="PanelContainer" parent="."]
+layout_mode = 2
+
+[node name="SaveLoadList" type="VBoxContainer" parent="SaveLoadPanel"]
+layout_mode = 2
+
+[node name="TitleBarList" type="HBoxContainer" parent="SaveLoadPanel/SaveLoadList"]
+layout_mode = 2
+alignment = 2
+
+[node name="SaveLoadLabel" type="Label" parent="SaveLoadPanel/SaveLoadList/TitleBarList"]
+layout_mode = 2
+size_flags_horizontal = 6
+text = "SAVELOADMENU_SAVE_TITLE"
+
+[node name="CloseButton" type="Button" parent="SaveLoadPanel/SaveLoadList/TitleBarList"]
+editor_description = "UI-87, UI-94"
+layout_mode = 2
+text = "X"
+
+[node name="TagSelectionList" type="HBoxContainer" parent="SaveLoadPanel/SaveLoadList"]
+layout_mode = 2
+
+[node name="TagSelectionLabel" type="Label" parent="SaveLoadPanel/SaveLoadList/TagSelectionList"]
+layout_mode = 2
+text = "SAVELOADMENU_SESSION"
+
+[node name="TagSelectionTabBar" type="TabBar" parent="SaveLoadPanel/SaveLoadList/TagSelectionList"]
+layout_mode = 2
+size_flags_horizontal = 3
+tab_count = 1
+tab_0/title = "SAVELOADMENU_TABSELECTIONTABBAR_ALL"
+
+[node name="SaveLoadScroll" type="ScrollContainer" parent="SaveLoadPanel/SaveLoadList"]
+editor_description = "UI-83, UI-90"
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="SaveLoadScrollList" type="VBoxContainer" parent="SaveLoadPanel/SaveLoadList/SaveLoadScroll"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="SaveLineEdit" type="LineEdit" parent="SaveLoadPanel/SaveLoadList"]
+editor_description = "UI-85, UI-92"
+layout_mode = 2
+
+[node name="SaveLoadButton" type="Button" parent="SaveLoadPanel/SaveLoadList"]
+editor_description = "UIFUN-82, UIFUN-87"
+layout_mode = 2
+size_flags_horizontal = 4
+shortcut = SubResource("Shortcut_o1f2l")
+shortcut_feedback = false
+text = "SAVELOADMENU_SAVE_BUTTON"
+
+[node name="DeleteDialog" type="ConfirmationDialog" parent="."]
+disable_3d = true
+title = "SAVELOADMENU_DELETE_DIALOG_TITLE"
+ok_button_text = "DIALOG_OK"
+dialog_text = "SAVELOADMENU_DELETE_DIALOG_TEXT"
+cancel_button_text = "DIALOG_CANCEL"
+
+[node name="OverwriteDialog" type="ConfirmationDialog" parent="."]
+disable_3d = true
+title = "SAVELOADMENU_OVERWRITE_DIALOG_TITLE"
+ok_button_text = "DIALOG_OK"
+dialog_text = "SAVELOADMENU_OVERWRITE_DIALOG_TEXT"
+cancel_button_text = "DIALOG_CANCEL"
+
+[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"]
+[connection signal="pressed" from="SaveLoadPanel/SaveLoadList/TitleBarList/CloseButton" to="." method="_on_close_button_pressed"]
+[connection signal="tab_changed" from="SaveLoadPanel/SaveLoadList/TagSelectionList/TagSelectionTabBar" to="." method="_on_tag_selection_tab_bar_tab_changed"]
+[connection signal="text_submitted" from="SaveLoadPanel/SaveLoadList/SaveLineEdit" to="." method="_on_save_line_edit_text_submitted"]
+[connection signal="pressed" from="SaveLoadPanel/SaveLoadList/SaveLoadButton" to="." method="_on_save_load_button_pressed"]
+[connection signal="confirmed" from="DeleteDialog" to="." method="_on_delete_dialog_confirmed"]
+[connection signal="confirmed" from="OverwriteDialog" to="." method="_on_overwrite_dialog_confirmed"]
diff --git a/game/src/SaveLoadMenu/SavePanelButton.gd b/game/src/SaveLoadMenu/SavePanelButton.gd
new file mode 100644
index 0000000..5fe4917
--- /dev/null
+++ b/game/src/SaveLoadMenu/SavePanelButton.gd
@@ -0,0 +1,41 @@
+@tool
+extends LobbyPanelButton
+
+signal request_to_delete
+
+@export_group("Nodes")
+@export var country_flag : TextureRect
+@export var date_label : Label
+@export var delete_button : BaseButton
+
+var resource : SaveResource:
+ get:
+ return resource
+ set(value):
+ if resource != null:
+ resource.changed.disconnect(_resource_changed)
+ resource = value
+ if resource != null:
+ resource.changed.connect(_resource_changed)
+ _resource_changed()
+
+func get_text() -> StringName:
+ return resource.save_name
+
+func set_text(value : StringName) -> void:
+ if resource != null:
+ resource.save_name = value
+
+func _ready():
+ _resource_changed()
+
+func _is_start_date() -> bool:
+ return false
+
+func _resource_changed() -> void:
+ if resource == null: return
+ name_label.text = resource.save_name
+ date_label.text = Time.get_datetime_string_from_unix_time(resource.get_save_file_time(), true)
+
+func _on_delete_button_pressed() -> void:
+ request_to_delete.emit()
diff --git a/game/src/SaveLoadMenu/SavePanelButton.tscn b/game/src/SaveLoadMenu/SavePanelButton.tscn
new file mode 100644
index 0000000..3a71a57
--- /dev/null
+++ b/game/src/SaveLoadMenu/SavePanelButton.tscn
@@ -0,0 +1,58 @@
+[gd_scene load_steps=2 format=3 uid="uid://d2s7roinx2or7"]
+
+[ext_resource type="Script" path="res://src/SaveLoadMenu/SavePanelButton.gd" id="1_rtuo6"]
+
+[node name="SavePanelButton" type="Container" node_paths=PackedStringArray("country_flag", "date_label", "delete_button", "background_button", "name_label")]
+editor_description = "UI-84, UI-91"
+offset_right = 276.0
+offset_bottom = 48.0
+script = ExtResource("1_rtuo6")
+country_flag = NodePath("SaveList/CountryFlag")
+date_label = NodePath("SaveList/DateLabel")
+delete_button = NodePath("SaveList/DeleteButton")
+background_button = NodePath("BackgroundButton")
+name_label = NodePath("SaveList/NameLabel")
+
+[node name="BackgroundButton" type="Button" parent="."]
+layout_mode = 2
+theme_type_variation = &"ButtonContainer"
+
+[node name="SaveList" type="HBoxContainer" parent="."]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="CountryFlag" type="TextureRect" parent="SaveList"]
+layout_mode = 2
+size_flags_horizontal = 0
+size_flags_vertical = 4
+mouse_filter = 2
+
+[node name="NameLabel" type="Label" parent="SaveList"]
+layout_mode = 2
+size_flags_horizontal = 0
+size_flags_vertical = 1
+text = "PLACEHOLDER"
+vertical_alignment = 1
+
+[node name="DateLabel" type="Label" parent="SaveList"]
+layout_mode = 2
+size_flags_horizontal = 0
+size_flags_vertical = 1
+text = "00.00.0000"
+vertical_alignment = 1
+
+[node name="Separator" type="Control" parent="SaveList"]
+layout_mode = 2
+size_flags_horizontal = 3
+mouse_filter = 2
+
+[node name="DeleteButton" type="Button" parent="SaveList"]
+layout_mode = 2
+size_flags_horizontal = 8
+text = "x"
+
+[connection signal="button_down" from="BackgroundButton" to="." method="_on_background_button_button_down"]
+[connection signal="button_up" from="BackgroundButton" to="." method="_on_background_button_button_up"]
+[connection signal="pressed" from="BackgroundButton" to="." method="_on_background_button_pressed"]
+[connection signal="toggled" from="BackgroundButton" to="." method="_on_background_button_toggled"]
+[connection signal="pressed" from="SaveList/DeleteButton" to="." method="_on_delete_button_pressed"]
diff --git a/game/src/SaveLoadMenu/SaveResource.gd b/game/src/SaveLoadMenu/SaveResource.gd
new file mode 100644
index 0000000..5e7faa6
--- /dev/null
+++ b/game/src/SaveLoadMenu/SaveResource.gd
@@ -0,0 +1,59 @@
+extends Resource
+class_name SaveResource
+
+signal file_flushed(path : String)
+signal file_loaded
+signal file_moved_to_trash
+signal file_deleted
+signal trash_moved
+signal deleted
+
+var save_name : StringName:
+ get: return save_name
+ set(v):
+ save_name = v
+ file.set_value("Save", "name", save_name)
+ emit_changed()
+var session_tag : StringName:
+ get: return session_tag
+ set(v):
+ session_tag = v
+ file.set_value("Save", "session_tag", v)
+ emit_changed()
+var file_path : String:
+ get: return file_path
+ set(v):
+ file_path = v
+ emit_changed()
+var file : ConfigFile = ConfigFile.new()
+
+func set_file_path(name : StringName, path : String):
+ file_path = path
+ save_name = name
+
+func flush_save() -> Error:
+ file_flushed.emit(file_path)
+ var result := file.save(file_path)
+ file.clear()
+ return result
+
+func load_save(path : String = file_path) -> Error:
+ file_loaded.emit()
+ var result := file.load(path)
+ session_tag = file.get_value("Save", "session_tag", session_tag)
+ if path != file_path:
+ set_file_path(file.get_value("Save", "name", save_name), path)
+ return result
+
+func get_save_file_time() -> int:
+ return FileAccess.get_modified_time(file_path)
+
+func move_to_trash() -> Error:
+ trash_moved.emit()
+ file_moved_to_trash.emit()
+ return OS.move_to_trash(file_path)
+
+func delete() -> Error:
+ deleted.emit()
+ file_deleted.emit()
+ return DirAccess.remove_absolute(file_path)