aboutsummaryrefslogtreecommitdiff
path: root/game/src/Game/Menu/OptionMenu
diff options
context:
space:
mode:
author Spartan322 <Megacake1234@gmail.com>2023-06-03 20:37:10 +0200
committer Spartan322 <Megacake1234@gmail.com>2023-06-03 20:37:10 +0200
commitcef940108fe15752c3ef66f43f5169403fa2f71d (patch)
treefe4de5a05830e3bddeae78f74f729503b7cee1e9 /game/src/Game/Menu/OptionMenu
parent73e29d02e48739aba5ca5db1b9575c67e795400f (diff)
Reorganize the file structure of the files in `game/src`
Diffstat (limited to 'game/src/Game/Menu/OptionMenu')
-rw-r--r--game/src/Game/Menu/OptionMenu/AutosaveIntervalSelector.gd2
-rw-r--r--game/src/Game/Menu/OptionMenu/ControlsTab.tscn14
-rw-r--r--game/src/Game/Menu/OptionMenu/GeneralTab.gd9
-rw-r--r--game/src/Game/Menu/OptionMenu/GeneralTab.tscn81
-rw-r--r--game/src/Game/Menu/OptionMenu/GuiScaleSelector.gd64
-rw-r--r--game/src/Game/Menu/OptionMenu/MonitorDisplaySelector.gd18
-rw-r--r--game/src/Game/Menu/OptionMenu/OptionsMenu.gd68
-rw-r--r--game/src/Game/Menu/OptionMenu/OptionsMenu.tscn52
-rw-r--r--game/src/Game/Menu/OptionMenu/OtherTab.tscn18
-rw-r--r--game/src/Game/Menu/OptionMenu/QualityPresetSelector.gd4
-rw-r--r--game/src/Game/Menu/OptionMenu/RefreshRateSelector.gd5
-rw-r--r--game/src/Game/Menu/OptionMenu/ResolutionRevertDialog.gd35
-rw-r--r--game/src/Game/Menu/OptionMenu/ResolutionSelector.gd91
-rw-r--r--game/src/Game/Menu/OptionMenu/ScreenModeSelector.gd48
-rw-r--r--game/src/Game/Menu/OptionMenu/SettingNodes/SettingHSlider.gd41
-rw-r--r--game/src/Game/Menu/OptionMenu/SettingNodes/SettingOptionButton.gd77
-rw-r--r--game/src/Game/Menu/OptionMenu/SettingNodes/SettingRevertButton.gd27
-rw-r--r--game/src/Game/Menu/OptionMenu/SoundTab.gd4
-rw-r--r--game/src/Game/Menu/OptionMenu/SoundTab.tscn34
-rw-r--r--game/src/Game/Menu/OptionMenu/VideoTab.gd9
-rw-r--r--game/src/Game/Menu/OptionMenu/VideoTab.tscn181
-rw-r--r--game/src/Game/Menu/OptionMenu/VolumeGrid.gd70
-rw-r--r--game/src/Game/Menu/OptionMenu/VolumeGrid.tscn8
23 files changed, 960 insertions, 0 deletions
diff --git a/game/src/Game/Menu/OptionMenu/AutosaveIntervalSelector.gd b/game/src/Game/Menu/OptionMenu/AutosaveIntervalSelector.gd
new file mode 100644
index 0000000..2c55862
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/AutosaveIntervalSelector.gd
@@ -0,0 +1,2 @@
+extends SettingOptionButton
+
diff --git a/game/src/Game/Menu/OptionMenu/ControlsTab.tscn b/game/src/Game/Menu/OptionMenu/ControlsTab.tscn
new file mode 100644
index 0000000..b84dc85
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/ControlsTab.tscn
@@ -0,0 +1,14 @@
+[gd_scene load_steps=2 format=3 uid="uid://cdwymd51i4b2f"]
+
+[ext_resource type="PackedScene" uid="uid://by4gggse2nsdx" path="res://addons/keychain/ShortcutEdit.tscn" id="1_fv8sh"]
+
+[node name="Controls" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="ShortcutEdit" parent="." instance=ExtResource("1_fv8sh")]
+layout_mode = 1
diff --git a/game/src/Game/Menu/OptionMenu/GeneralTab.gd b/game/src/Game/Menu/OptionMenu/GeneralTab.gd
new file mode 100644
index 0000000..3d98678
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/GeneralTab.gd
@@ -0,0 +1,9 @@
+extends HBoxContainer
+
+@export var initial_focus: Control
+
+func _notification(what : int) -> void:
+ match(what):
+ NOTIFICATION_VISIBILITY_CHANGED:
+ if visible and is_inside_tree():
+ initial_focus.grab_focus()
diff --git a/game/src/Game/Menu/OptionMenu/GeneralTab.tscn b/game/src/Game/Menu/OptionMenu/GeneralTab.tscn
new file mode 100644
index 0000000..4e9ff6a
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/GeneralTab.tscn
@@ -0,0 +1,81 @@
+[gd_scene load_steps=5 format=3 uid="uid://duwjal7sd7p6w"]
+
+[ext_resource type="Script" path="res://src/OptionMenu/GeneralTab.gd" id="1_gbutn"]
+[ext_resource type="PackedScene" uid="uid://b7oncobnacxmt" path="res://src/LocaleButton.tscn" id="2_5cfd7"]
+[ext_resource type="Script" path="res://src/OptionMenu/SettingNodes/SettingOptionButton.gd" id="2_msx2u"]
+[ext_resource type="Script" path="res://src/OptionMenu/AutosaveIntervalSelector.gd" id="2_t06tb"]
+
+[node name="GeneralTab" type="HBoxContainer" node_paths=PackedStringArray("initial_focus")]
+editor_description = "UI-48, UIFUN-45"
+alignment = 1
+script = ExtResource("1_gbutn")
+initial_focus = NodePath("VBoxContainer/GridContainer/SavegameFormatSelector")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Control" type="Control" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+size_flags_stretch_ratio = 0.1
+
+[node name="GridContainer" type="GridContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+columns = 2
+
+[node name="SavegameFormatLabel" type="Label" parent="VBoxContainer/GridContainer"]
+layout_mode = 2
+text = "OPTIONS_GENERAL_SAVEFORMAT"
+
+[node name="SavegameFormatSelector" type="OptionButton" parent="VBoxContainer/GridContainer"]
+editor_description = "UI-50"
+layout_mode = 2
+focus_neighbor_bottom = NodePath("../AutosaveIntervalSelector")
+item_count = 2
+selected = 0
+popup/item_0/text = "OPTIONS_GENERAL_BINARY"
+popup/item_0/id = 0
+popup/item_1/text = "OPTIONS_GENERAL_TEXT"
+popup/item_1/id = 1
+script = ExtResource("2_msx2u")
+section_name = "general"
+setting_name = "savegame_format"
+default_selected = 0
+
+[node name="AutosaveIntervalLabel" type="Label" parent="VBoxContainer/GridContainer"]
+layout_mode = 2
+text = "OPTIONS_GENERAL_AUTOSAVE"
+
+[node name="AutosaveIntervalSelector" type="OptionButton" parent="VBoxContainer/GridContainer"]
+editor_description = "UI-15, UIFUN-19"
+layout_mode = 2
+focus_neighbor_top = NodePath("../SavegameFormatSelector")
+focus_neighbor_bottom = NodePath("../LocaleButton")
+item_count = 5
+selected = 0
+popup/item_0/text = "OPTIONS_GENERAL_AUTOSAVE_MONTHLY"
+popup/item_0/id = 0
+popup/item_1/text = "OPTIONS_GENERAL_AUTOSAVE_BIMONTHLY"
+popup/item_1/id = 1
+popup/item_2/text = "OPTIONS_GENERAL_AUTOSAVE_YEARLY"
+popup/item_2/id = 2
+popup/item_3/text = "OPTIONS_GENERAL_AUTOSAVE_BIYEARLY"
+popup/item_3/id = 3
+popup/item_4/text = "OPTIONS_GENERAL_AUTOSAVE_NEVER"
+popup/item_4/id = 4
+script = ExtResource("2_t06tb")
+section_name = "general"
+setting_name = "autosave_interval"
+default_selected = 0
+
+[node name="LocaleLabel" type="Label" parent="VBoxContainer/GridContainer"]
+layout_mode = 2
+text = "OPTIONS_GENERAL_LANGUAGE"
+
+[node name="LocaleButton" parent="VBoxContainer/GridContainer" instance=ExtResource("2_5cfd7")]
+editor_description = "UI-79"
+layout_mode = 2
+focus_neighbor_top = NodePath("../AutosaveIntervalSelector")
+alignment = 0
+text_overrun_behavior = 4
diff --git a/game/src/Game/Menu/OptionMenu/GuiScaleSelector.gd b/game/src/Game/Menu/OptionMenu/GuiScaleSelector.gd
new file mode 100644
index 0000000..4dd86e1
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/GuiScaleSelector.gd
@@ -0,0 +1,64 @@
+extends SettingOptionButton
+
+# REQUIREMENTS
+# * UIFUN-24
+# * UIFUN-31
+
+@export
+var default_value : float = GuiScale.error_guiscale
+
+func _find_guiscale_index_by_value(value : float) -> int:
+ for item_index in item_count:
+ if get_item_metadata(item_index) == value:
+ return item_index
+ return -1
+
+func _sync_guiscales(to_select : float = GuiScale.get_current_guiscale()) -> void:
+ clear()
+ default_selected = -1
+ selected = -1
+ for guiscale_value in GuiScale.get_guiscale_value_list():
+ add_item(GuiScale.get_guiscale_display_name(guiscale_value))
+ set_item_metadata(item_count - 1, guiscale_value)
+
+ if guiscale_value == default_value:
+ default_selected = item_count - 1
+
+ if guiscale_value == to_select:
+ selected = item_count - 1
+
+ if default_selected == -1:
+ default_selected = item_count - 1
+
+ if selected == -1:
+ selected = default_selected
+
+func _setup_button():
+ if default_value <= 0:
+ default_value = ProjectSettings.get_setting("display/window/stretch/scale")
+ GuiScale.add_guiscale(default_value, &"default")
+ _sync_guiscales()
+
+func _get_value_for_file(select_value : int):
+ if _valid_index(select_value):
+ return get_item_metadata(select_value)
+ else:
+ return null
+
+func _set_value_from_file(load_value):
+ if typeof(load_value) == TYPE_FLOAT:
+ var target_guiscale : float = load_value
+ selected = _find_guiscale_index_by_value(target_guiscale)
+ if selected != -1: return
+ if GuiScale.add_guiscale(target_guiscale):
+ _sync_guiscales(target_guiscale)
+ return
+ push_error("Setting value '%s' invalid for setting [%s] %s" % [load_value, section_name, setting_name])
+ selected = default_selected
+
+func _on_option_selected(index : int, by_user : bool):
+ if _valid_index(index):
+ GuiScale.set_guiscale(get_item_metadata(index))
+ else:
+ push_error("Invalid GuiScaleSelector index: %d" % index)
+ reset_setting(not by_user)
diff --git a/game/src/Game/Menu/OptionMenu/MonitorDisplaySelector.gd b/game/src/Game/Menu/OptionMenu/MonitorDisplaySelector.gd
new file mode 100644
index 0000000..7de033a
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/MonitorDisplaySelector.gd
@@ -0,0 +1,18 @@
+extends SettingOptionButton
+
+func _setup_button() -> void:
+ clear()
+ for screen_index in DisplayServer.get_screen_count():
+ add_item("Monitor %d" % (screen_index + 1))
+ default_selected = get_viewport().get_window().current_screen
+
+func _on_option_selected(index : int, by_user : bool) -> void:
+ if _valid_index(index):
+ var window := get_viewport().get_window()
+ var mode := window.mode
+ window.mode = Window.MODE_WINDOWED
+ get_viewport().get_window().set_current_screen(index)
+ window.mode = mode
+ else:
+ push_error("Invalid MonitorDisplaySelector index: %d" % index)
+ reset_setting(not by_user)
diff --git a/game/src/Game/Menu/OptionMenu/OptionsMenu.gd b/game/src/Game/Menu/OptionMenu/OptionsMenu.gd
new file mode 100644
index 0000000..5f6a088
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/OptionsMenu.gd
@@ -0,0 +1,68 @@
+extends Control
+
+# REQUIREMENTS
+# * SS-13
+
+signal back_button_pressed
+
+func _ready():
+ # Prepare options menu before loading user settings
+ var tab_bar : TabBar = $Margin/Tab.get_child(0, true)
+
+ # This ends up easier to manage then trying to manually recreate the TabContainer's behavior
+ # These buttons can be accessed regardless of the tab
+ var button_list := HBoxContainer.new()
+ button_list.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
+ button_list.alignment = BoxContainer.ALIGNMENT_END
+ tab_bar.add_child(button_list)
+
+ # REQUIREMENTS
+ # * UI-12
+ # * UIFUN-14
+ var reset_button := Button.new()
+ reset_button.text = "OPTIONS_RESET"
+ reset_button.pressed.connect(Events.Options.try_reset_settings)
+ button_list.add_child(reset_button)
+
+ # REQUIREMENTS
+ # * UI-11
+ # * UIFUN-17
+ var back_button := Button.new()
+ back_button.text = "OPTIONS_BACK"
+ back_button.pressed.connect(_on_back_button_pressed)
+ button_list.add_child(back_button)
+ get_viewport().get_window().close_requested.connect(_on_window_close_requested)
+ _save_overrides.call_deferred()
+ Events.Options.save_settings.connect(func(_f): self._save_overrides.call_deferred())
+
+func _notification(what):
+ match what:
+ NOTIFICATION_CRASH:
+ _on_window_close_requested()
+
+func _input(event):
+ if self.is_visible_in_tree():
+ if event.is_action_pressed("ui_cancel"):
+ _on_back_button_pressed()
+
+func _on_back_button_pressed():
+ Events.Options.save_settings_to_file()
+ back_button_pressed.emit()
+
+func _on_window_close_requested() -> void:
+ if visible:
+ Events.Options.save_settings_to_file()
+
+func _save_overrides() -> void:
+ var override_path : String = ProjectSettings.get_setting("application/config/project_settings_override", "")
+ if override_path.is_empty():
+ override_path = ProjectSettings.get_setting(Events.Options.settings_file_path_setting, Events.Options.settings_file_path_default)
+ var file := ConfigFile.new()
+ var err_ret := file.load(override_path)
+ if err_ret != OK: push_error("Failed to load overrides from %s" % override_path)
+ file.set_value("display", "window/size/mode", get_viewport().get_window().mode)
+ var resolution : Vector2i = Resolution.get_current_resolution()
+ file.set_value("display", "window/size/viewport_width", resolution.x)
+ file.set_value("display", "window/size/viewport_height", resolution.y)
+ err_ret = file.save(override_path)
+ if err_ret != OK: push_error("Failed to save overrides to %s" % override_path)
diff --git a/game/src/Game/Menu/OptionMenu/OptionsMenu.tscn b/game/src/Game/Menu/OptionMenu/OptionsMenu.tscn
new file mode 100644
index 0000000..3156e33
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/OptionsMenu.tscn
@@ -0,0 +1,52 @@
+[gd_scene load_steps=8 format=3 uid="uid://cnbfxjy1m6wja"]
+
+[ext_resource type="Theme" uid="uid://fbxssqcg1s0m" path="res://theme/options_menu.tres" id="1_0up1d"]
+[ext_resource type="Script" path="res://src/OptionMenu/OptionsMenu.gd" id="1_tlein"]
+[ext_resource type="PackedScene" uid="uid://bq3awxxjn1tuw" path="res://src/OptionMenu/VideoTab.tscn" id="2_ji8xr"]
+[ext_resource type="PackedScene" uid="uid://cbtgwpx2wxi33" path="res://src/OptionMenu/SoundTab.tscn" id="3_4w35t"]
+[ext_resource type="PackedScene" uid="uid://duwjal7sd7p6w" path="res://src/OptionMenu/GeneralTab.tscn" id="3_6gvf6"]
+[ext_resource type="PackedScene" uid="uid://bq7ibhm0txl5p" path="res://addons/keychain/ShortcutEdit.tscn" id="4_vdhjp"]
+[ext_resource type="PackedScene" uid="uid://dp2grvybtecqu" path="res://src/OptionMenu/OtherTab.tscn" id="5_ahefp"]
+
+[node name="OptionsMenu" type="PanelContainer"]
+editor_description = "UI-25"
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme = ExtResource("1_0up1d")
+theme_type_variation = &"BackgroundPanel"
+script = ExtResource("1_tlein")
+
+[node name="Margin" type="MarginContainer" parent="."]
+layout_mode = 2
+theme_type_variation = &"TabMargin"
+
+[node name="Tab" type="TabContainer" parent="Margin"]
+editor_description = "UI-45"
+layout_mode = 2
+tab_alignment = 1
+use_hidden_tabs_for_min_size = true
+
+[node name="General" parent="Margin/Tab" instance=ExtResource("3_6gvf6")]
+layout_mode = 2
+
+[node name="Video" parent="Margin/Tab" instance=ExtResource("2_ji8xr")]
+editor_description = "UI-46, UIFUN-43"
+visible = false
+layout_mode = 2
+
+[node name="Sound" parent="Margin/Tab" instance=ExtResource("3_4w35t")]
+editor_description = "UI-47, UIFUN-44"
+visible = false
+layout_mode = 2
+
+[node name="Controls" parent="Margin/Tab" instance=ExtResource("4_vdhjp")]
+editor_description = "SS-27, UI-49, UIFUN-46"
+visible = false
+layout_mode = 2
+alignment = 1
+
+[node name="Other" parent="Margin/Tab" instance=ExtResource("5_ahefp")]
+layout_mode = 2
diff --git a/game/src/Game/Menu/OptionMenu/OtherTab.tscn b/game/src/Game/Menu/OptionMenu/OtherTab.tscn
new file mode 100644
index 0000000..0ffc92d
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/OtherTab.tscn
@@ -0,0 +1,18 @@
+[gd_scene format=3 uid="uid://dp2grvybtecqu"]
+
+[node name="Other" type="Control"]
+visible = false
+layout_mode = 3
+anchors_preset = 0
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 0
+offset_right = 40.0
+offset_bottom = 40.0
+
+[node name="Label" type="Label" parent="HBoxContainer"]
+layout_mode = 2
+text = "Spinbox Example :)"
+
+[node name="SpinBox" type="SpinBox" parent="HBoxContainer"]
+layout_mode = 2
diff --git a/game/src/Game/Menu/OptionMenu/QualityPresetSelector.gd b/game/src/Game/Menu/OptionMenu/QualityPresetSelector.gd
new file mode 100644
index 0000000..57ba4ab
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/QualityPresetSelector.gd
@@ -0,0 +1,4 @@
+extends SettingOptionButton
+
+func _setup_button():
+ pass
diff --git a/game/src/Game/Menu/OptionMenu/RefreshRateSelector.gd b/game/src/Game/Menu/OptionMenu/RefreshRateSelector.gd
new file mode 100644
index 0000000..31b115b
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/RefreshRateSelector.gd
@@ -0,0 +1,5 @@
+extends SettingOptionButton
+
+
+func _setup_button():
+ pass
diff --git a/game/src/Game/Menu/OptionMenu/ResolutionRevertDialog.gd b/game/src/Game/Menu/OptionMenu/ResolutionRevertDialog.gd
new file mode 100644
index 0000000..4d2b8f2
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/ResolutionRevertDialog.gd
@@ -0,0 +1,35 @@
+extends ConfirmationDialog
+class_name ResolutionRevertDialog
+
+signal dialog_accepted(button : SettingRevertButton)
+signal dialog_reverted(button : SettingRevertButton)
+
+@export_group("Nodes")
+@export var timer : Timer
+
+var _revert_node : SettingRevertButton = null
+
+func show_dialog(button : SettingRevertButton, time : float = 0) -> void:
+ timer.start(time)
+ popup_centered(Vector2(1,1))
+ _revert_node = button
+
+func _notification(what):
+ if what == NOTIFICATION_VISIBILITY_CHANGED:
+ set_process(visible)
+ if not visible: _revert_node = null
+
+func _process(_delta) -> void:
+ dialog_text = tr("OPTIONS_VIDEO_RESOLUTION_DIALOG_TEXT").format({ "time": int(timer.time_left) })
+
+func _on_canceled_or_close_requested() -> void:
+ timer.stop()
+ dialog_reverted.emit(_revert_node)
+
+func _on_confirmed() -> void:
+ timer.stop()
+ dialog_accepted.emit(_revert_node)
+
+func _on_resolution_revert_timer_timeout() -> void:
+ dialog_reverted.emit(_revert_node)
+ hide()
diff --git a/game/src/Game/Menu/OptionMenu/ResolutionSelector.gd b/game/src/Game/Menu/OptionMenu/ResolutionSelector.gd
new file mode 100644
index 0000000..ebdf718
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/ResolutionSelector.gd
@@ -0,0 +1,91 @@
+extends SettingRevertButton
+
+# REQUIREMENTS
+# * UIFUN-21
+# * UIFUN-28
+# * UIFUN-301
+# * UIFUN-302
+
+@export var default_value : Vector2i = Resolution.error_resolution
+
+func _find_resolution_index_by_value(value : Vector2i) -> int:
+ for item_index in item_count:
+ if get_item_metadata(item_index) == value:
+ return item_index
+ return -1
+
+func _sync_resolutions(
+ value : Vector2i = Resolution.error_resolution,
+ _resolution_name = null,
+ _resolution_display_name = null
+) -> void:
+ clear()
+ default_selected = -1
+ selected = -1
+ var resolution_list := Resolution.get_resolution_value_list()
+ if value != Resolution.error_resolution:
+ resolution_list.append(value)
+ for resolution_value in resolution_list:
+ var display_name := "%sx%s" % [resolution_value.x, resolution_value.y]
+ var resolution_name := Resolution.get_resolution_name(resolution_value)
+ if resolution_name == &"Default":
+ display_name = "Default (%s)" % resolution_name
+ if not resolution_name.is_empty():
+ display_name = "%s (%s)" % [display_name, resolution_name + (", Default" if resolution_value == default_value else "")]
+ add_item(display_name)
+ set_item_metadata(item_count - 1, resolution_value)
+
+ if resolution_value == default_value:
+ default_selected = item_count - 1
+
+ if resolution_value == Resolution.get_current_resolution():
+ selected = item_count - 1
+
+ if default_selected == -1:
+ default_selected = item_count - 1
+
+ if selected == -1:
+ selected = default_selected
+
+func _setup_button() -> void:
+ Resolution.resolution_added.connect(_sync_resolutions)
+ if default_value.x <= 0:
+ default_value.x = ProjectSettings.get_setting("display/window/size/viewport_width")
+ if default_value.y <= 0:
+ default_value.y = ProjectSettings.get_setting("display/window/size/viewport_height")
+ if not Resolution.has_resolution(default_value):
+ Resolution.add_resolution(default_value, &"Default")
+ else:
+ _sync_resolutions()
+
+func _get_value_for_file(select_value : int) -> Variant:
+ if _valid_index(select_value):
+ return get_item_metadata(select_value)
+ else:
+ return null
+
+func _set_value_from_file(load_value) -> void:
+ var target_resolution := Resolution.error_resolution
+ match typeof(load_value):
+ TYPE_VECTOR2I: target_resolution = load_value
+ TYPE_STRING, TYPE_STRING_NAME: target_resolution = Resolution.get_resolution_value_from_string(load_value)
+ if target_resolution != Resolution.error_resolution:
+ selected = _find_resolution_index_by_value(target_resolution)
+ if selected != -1: return
+ if Resolution.add_resolution(target_resolution):
+ Resolution.set_resolution(target_resolution)
+ return
+ push_error("Setting value '%s' invalid for setting [%s] %s" % [load_value, section_name, setting_name])
+ selected = default_selected
+
+func _on_option_selected(index : int, by_user : bool) -> void:
+ if _valid_index(index):
+ if by_user:
+ print("Start Revert Countdown!")
+ revert_dialog.show_dialog.call_deferred(self)
+ previous_index = _find_resolution_index_by_value(Resolution.get_current_resolution())
+
+ Resolution.set_resolution(get_item_metadata(index))
+ else:
+ push_error("Invalid ResolutionSelector index: %d" % index)
+ reset_setting(not by_user)
diff --git a/game/src/Game/Menu/OptionMenu/ScreenModeSelector.gd b/game/src/Game/Menu/OptionMenu/ScreenModeSelector.gd
new file mode 100644
index 0000000..af95901
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/ScreenModeSelector.gd
@@ -0,0 +1,48 @@
+extends SettingRevertButton
+
+# REQUIREMENTS
+# * UIFUN-42
+
+enum ScreenMode { Unknown = -1, Fullscreen, Borderless, Windowed }
+
+func get_screen_mode_from_window_mode(window_mode : int) -> ScreenMode:
+ match window_mode:
+ Window.MODE_EXCLUSIVE_FULLSCREEN:
+ return ScreenMode.Fullscreen
+ Window.MODE_FULLSCREEN:
+ return ScreenMode.Borderless
+ Window.MODE_WINDOWED:
+ return ScreenMode.Windowed
+ _:
+ return ScreenMode.Unknown
+
+func get_window_mode_from_screen_mode(screen_mode : int) -> Window.Mode:
+ match screen_mode:
+ ScreenMode.Fullscreen:
+ return Window.MODE_EXCLUSIVE_FULLSCREEN
+ ScreenMode.Borderless:
+ return Window.MODE_FULLSCREEN
+ ScreenMode.Windowed:
+ return Window.MODE_WINDOWED
+ _:
+ return Window.MODE_EXCLUSIVE_FULLSCREEN
+
+func _setup_button():
+ default_selected = get_screen_mode_from_window_mode(get_viewport().get_window().mode)
+ selected = default_selected
+
+func _on_option_selected(index : int, by_user : bool) -> void:
+ if _valid_index(index):
+ if by_user:
+ print("Start Revert Countdown!")
+ revert_dialog.show_dialog.call_deferred(self)
+ previous_index = get_screen_mode_from_window_mode(get_viewport().get_window().mode)
+
+ var current_resolution := Resolution.get_current_resolution()
+ var window_mode := get_window_mode_from_screen_mode(index)
+ Resolution.window_mode_changed.emit(window_mode)
+ get_viewport().get_window().mode = window_mode
+ Resolution.set_resolution(current_resolution)
+ else:
+ push_error("Invalid ScreenModeSelector index: %d" % index)
+ reset_setting(not by_user)
diff --git a/game/src/Game/Menu/OptionMenu/SettingNodes/SettingHSlider.gd b/game/src/Game/Menu/OptionMenu/SettingNodes/SettingHSlider.gd
new file mode 100644
index 0000000..6fa30ed
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/SettingNodes/SettingHSlider.gd
@@ -0,0 +1,41 @@
+extends HSlider
+class_name SettingHSlider
+
+@export
+var section_name : String = "setting"
+
+@export
+var setting_name : String = "setting_hslider"
+
+@export
+var default_value : float = 0
+
+func _ready():
+ Events.Options.load_settings.connect(load_setting)
+ Events.Options.save_settings.connect(save_setting)
+ Events.Options.reset_settings.connect(reset_setting)
+
+func load_setting(file : ConfigFile):
+ if file == null: return
+ var load_value = file.get_value(section_name, setting_name, default_value)
+ match typeof(load_value):
+ TYPE_FLOAT, TYPE_INT:
+ if value == load_value: value_changed.emit(value)
+ value = load_value
+ return
+ TYPE_STRING, TYPE_STRING_NAME:
+ var load_string := load_value as String
+ if load_string.is_valid_float():
+ load_value = load_string.to_float()
+ if value == load_value: value_changed.emit(value)
+ value = load_value
+ return
+ push_error("Setting value '%s' invalid for setting [%s] \"%s\"" % [load_value, section_name, setting_name])
+ value = default_value
+
+func save_setting(file : ConfigFile):
+ if file == null: return
+ file.set_value(section_name, setting_name, value)
+
+func reset_setting():
+ value = default_value
diff --git a/game/src/Game/Menu/OptionMenu/SettingNodes/SettingOptionButton.gd b/game/src/Game/Menu/OptionMenu/SettingNodes/SettingOptionButton.gd
new file mode 100644
index 0000000..c5a805e
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/SettingNodes/SettingOptionButton.gd
@@ -0,0 +1,77 @@
+extends OptionButton
+class_name SettingOptionButton
+
+signal option_selected(index : int, by_user : bool)
+
+@export
+var section_name : String = "setting"
+
+@export
+var setting_name : String = "setting_optionbutton"
+
+@export
+var default_selected : int = -1:
+ get: return default_selected
+ set(v):
+ if v < 0 or item_count == 0:
+ default_selected = -1
+ return
+ default_selected = v % item_count
+
+func _valid_index(index : int) -> bool:
+ return 0 <= index and index < item_count
+
+func _get_value_for_file(select_value : int):
+ if _valid_index(select_value):
+ return select_value
+ else:
+ return null
+
+func _set_value_from_file(load_value) -> void:
+ match typeof(load_value):
+ TYPE_INT:
+ if _valid_index(load_value):
+ selected = load_value
+ return
+ TYPE_STRING, TYPE_STRING_NAME:
+ var load_string := load_value as String
+ if load_string.is_valid_int():
+ var load_int := load_string.to_int()
+ if _valid_index(load_int):
+ selected = load_int
+ return
+ for item_index in item_count:
+ if load_string == get_item_text(item_index):
+ selected = item_index
+ return
+ push_error("Setting value '%s' invalid for setting [%s] \"%s\"" % [load_value, section_name, setting_name])
+ selected = default_selected
+
+func _setup_button() -> void:
+ pass
+
+func _ready():
+ Events.Options.load_settings.connect(load_setting)
+ Events.Options.save_settings.connect(save_setting)
+ Events.Options.reset_settings.connect(reset_setting)
+ item_selected.connect(func(index : int): option_selected.emit(index, true))
+ _setup_button()
+ if not _valid_index(default_selected) or selected == -1:
+ var msg := "Failed to generate %s %s options." % [setting_name, section_name]
+ push_error(msg)
+ OS.alert(msg, "%s Options Error" % section_name)
+ get_tree().quit()
+
+func load_setting(file : ConfigFile) -> void:
+ if file == null: return
+ _set_value_from_file(file.get_value(section_name, setting_name, _get_value_for_file(default_selected)))
+ option_selected.emit(selected, false)
+
+func save_setting(file : ConfigFile) -> void:
+ if file == null: return
+ file.set_value(section_name, setting_name, _get_value_for_file(selected))
+
+func reset_setting(no_emit : bool = false) -> void:
+ selected = default_selected
+ if not no_emit:
+ option_selected.emit(selected, false)
diff --git a/game/src/Game/Menu/OptionMenu/SettingNodes/SettingRevertButton.gd b/game/src/Game/Menu/OptionMenu/SettingNodes/SettingRevertButton.gd
new file mode 100644
index 0000000..945d35b
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/SettingNodes/SettingRevertButton.gd
@@ -0,0 +1,27 @@
+extends SettingOptionButton
+class_name SettingRevertButton
+
+@export_group("Nodes")
+@export var revert_dialog : ResolutionRevertDialog
+
+var previous_index : int = -1
+
+func _ready():
+ super()
+ if revert_dialog != null:
+ revert_dialog.visibility_changed.connect(_on_revert_dialog_visibility_changed)
+ revert_dialog.dialog_accepted.connect(_on_accepted)
+ revert_dialog.dialog_reverted.connect(_on_reverted)
+
+func _on_revert_dialog_visibility_changed() -> void:
+ disabled = revert_dialog.visible
+ if not revert_dialog.visible:
+ previous_index = -1
+
+func _on_reverted(button : SettingRevertButton) -> void:
+ if button != self: return
+ selected = previous_index
+ option_selected.emit(selected, false)
+
+func _on_accepted(button : SettingRevertButton) -> void:
+ if button != self: return
diff --git a/game/src/Game/Menu/OptionMenu/SoundTab.gd b/game/src/Game/Menu/OptionMenu/SoundTab.gd
new file mode 100644
index 0000000..c707605
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/SoundTab.gd
@@ -0,0 +1,4 @@
+extends HBoxContainer
+
+func _on_ear_exploder_toggled(button_pressed):
+ print("KABOOM!!!" if button_pressed else "DEFUSED!!!")
diff --git a/game/src/Game/Menu/OptionMenu/SoundTab.tscn b/game/src/Game/Menu/OptionMenu/SoundTab.tscn
new file mode 100644
index 0000000..10d7f10
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/SoundTab.tscn
@@ -0,0 +1,34 @@
+[gd_scene load_steps=3 format=3 uid="uid://cbtgwpx2wxi33"]
+
+[ext_resource type="Script" path="res://src/OptionMenu/SoundTab.gd" id="1_a7k0s"]
+[ext_resource type="PackedScene" uid="uid://dy4si8comamnv" path="res://src/OptionMenu/VolumeGrid.tscn" id="1_okpft"]
+
+[node name="Sound" type="HBoxContainer"]
+alignment = 1
+script = ExtResource("1_a7k0s")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Control" type="Control" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+size_flags_stretch_ratio = 0.1
+
+[node name="VolumeGrid" parent="VBoxContainer" instance=ExtResource("1_okpft")]
+layout_mode = 2
+
+[node name="ButtonGrid" type="GridContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 2
+columns = 2
+
+[node name="Spacer" type="Control" parent="VBoxContainer/ButtonGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="EarExploder" type="CheckButton" parent="VBoxContainer/ButtonGrid"]
+layout_mode = 2
+text = "Explode Eardrums on Startup?"
+
+[connection signal="toggled" from="VBoxContainer/ButtonGrid/EarExploder" to="." method="_on_ear_exploder_toggled"]
diff --git a/game/src/Game/Menu/OptionMenu/VideoTab.gd b/game/src/Game/Menu/OptionMenu/VideoTab.gd
new file mode 100644
index 0000000..3d98678
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/VideoTab.gd
@@ -0,0 +1,9 @@
+extends HBoxContainer
+
+@export var initial_focus: Control
+
+func _notification(what : int) -> void:
+ match(what):
+ NOTIFICATION_VISIBILITY_CHANGED:
+ if visible and is_inside_tree():
+ initial_focus.grab_focus()
diff --git a/game/src/Game/Menu/OptionMenu/VideoTab.tscn b/game/src/Game/Menu/OptionMenu/VideoTab.tscn
new file mode 100644
index 0000000..244f481
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/VideoTab.tscn
@@ -0,0 +1,181 @@
+[gd_scene load_steps=9 format=3 uid="uid://bq3awxxjn1tuw"]
+
+[ext_resource type="Script" path="res://src/OptionMenu/ResolutionSelector.gd" id="1_i8nro"]
+[ext_resource type="Script" path="res://src/OptionMenu/VideoTab.gd" id="1_jvv62"]
+[ext_resource type="Script" path="res://src/OptionMenu/ScreenModeSelector.gd" id="2_wa7vw"]
+[ext_resource type="Script" path="res://src/OptionMenu/GuiScaleSelector.gd" id="3_pgc5d"]
+[ext_resource type="Script" path="res://src/OptionMenu/MonitorDisplaySelector.gd" id="3_y6lyb"]
+[ext_resource type="Script" path="res://src/OptionMenu/RefreshRateSelector.gd" id="4_381mg"]
+[ext_resource type="Script" path="res://src/OptionMenu/QualityPresetSelector.gd" id="5_srg4v"]
+[ext_resource type="Script" path="res://src/OptionMenu/ResolutionRevertDialog.gd" id="8_802cr"]
+
+[node name="Video" type="HBoxContainer" node_paths=PackedStringArray("initial_focus")]
+editor_description = "UI-46"
+alignment = 1
+script = ExtResource("1_jvv62")
+initial_focus = NodePath("VideoSettingList/VideoSettingGrid/ResolutionSelector")
+
+[node name="VideoSettingList" type="VBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Control" type="Control" parent="VideoSettingList"]
+layout_mode = 2
+size_flags_vertical = 3
+size_flags_stretch_ratio = 0.1
+
+[node name="VideoSettingGrid" type="GridContainer" parent="VideoSettingList"]
+layout_mode = 2
+size_flags_vertical = 3
+columns = 2
+
+[node name="ResolutionLabel" type="Label" parent="VideoSettingList/VideoSettingGrid"]
+layout_mode = 2
+text = "OPTIONS_VIDEO_RESOLUTION"
+
+[node name="ResolutionSelector" type="OptionButton" parent="VideoSettingList/VideoSettingGrid" node_paths=PackedStringArray("revert_dialog")]
+editor_description = "UI-19"
+layout_mode = 2
+focus_neighbor_bottom = NodePath("../ScreenModeSelector")
+item_count = 1
+selected = 0
+popup/item_0/text = "MISSING"
+popup/item_0/id = 0
+script = ExtResource("1_i8nro")
+revert_dialog = NodePath("../../../ResolutionRevertDialog")
+section_name = "video"
+setting_name = "resolution"
+
+[node name="GuiScaleLabel" type="Label" parent="VideoSettingList/VideoSettingGrid"]
+layout_mode = 2
+text = "OPTIONS_VIDEO_GUI_SCALE"
+
+[node name="GuiScaleSelector" type="OptionButton" parent="VideoSettingList/VideoSettingGrid"]
+editor_description = "UI-23"
+layout_mode = 2
+focus_neighbor_bottom = NodePath("../ScreenModeSelector")
+item_count = 1
+selected = 0
+popup/item_0/text = "MISSING"
+popup/item_0/id = 0
+script = ExtResource("3_pgc5d")
+section_name = "video"
+setting_name = "gui_scale"
+
+[node name="ScreenModeLabel" type="Label" parent="VideoSettingList/VideoSettingGrid"]
+editor_description = "UI-44"
+layout_mode = 2
+text = "OPTIONS_VIDEO_SCREEN_MODE"
+
+[node name="ScreenModeSelector" type="OptionButton" parent="VideoSettingList/VideoSettingGrid" node_paths=PackedStringArray("revert_dialog")]
+layout_mode = 2
+focus_neighbor_top = NodePath("../ResolutionSelector")
+focus_neighbor_bottom = NodePath("../MonitorDisplaySelector")
+item_count = 3
+selected = 0
+popup/item_0/text = "OPTIONS_VIDEO_FULLSCREEN"
+popup/item_0/id = 0
+popup/item_1/text = "OPTIONS_VIDEO_BORDERLESS"
+popup/item_1/id = 1
+popup/item_2/text = "OPTIONS_VIDEO_WINDOWED"
+popup/item_2/id = 2
+script = ExtResource("2_wa7vw")
+revert_dialog = NodePath("../../../ResolutionRevertDialog")
+section_name = "video"
+setting_name = "mode_selected"
+
+[node name="MonitorSelectionLabel" type="Label" parent="VideoSettingList/VideoSettingGrid"]
+layout_mode = 2
+text = "OPTIONS_VIDEO_MONITOR_SELECTION"
+
+[node name="MonitorDisplaySelector" type="OptionButton" parent="VideoSettingList/VideoSettingGrid"]
+layout_mode = 2
+focus_neighbor_top = NodePath("../ScreenModeSelector")
+focus_neighbor_bottom = NodePath("../RefreshRateSelector")
+item_count = 1
+selected = 0
+popup/item_0/text = "MISSING"
+popup/item_0/id = 0
+script = ExtResource("3_y6lyb")
+section_name = "video"
+setting_name = "current_screen"
+
+[node name="RefreshRateLabel" type="Label" parent="VideoSettingList/VideoSettingGrid"]
+layout_mode = 2
+text = "OPTIONS_VIDEO_REFRESH_RATE"
+
+[node name="RefreshRateSelector" type="OptionButton" parent="VideoSettingList/VideoSettingGrid"]
+editor_description = "UI-18, UIFUN-20"
+layout_mode = 2
+tooltip_text = "OPTIONS_VIDEO_REFRESH_RATE_TOOLTIP"
+focus_neighbor_top = NodePath("../MonitorDisplaySelector")
+focus_neighbor_bottom = NodePath("../QualityPresetSelector")
+item_count = 8
+selected = 0
+popup/item_0/text = "VSYNC"
+popup/item_0/id = 0
+popup/item_1/text = "30hz"
+popup/item_1/id = 1
+popup/item_2/text = "60hz"
+popup/item_2/id = 2
+popup/item_3/text = "90hz"
+popup/item_3/id = 3
+popup/item_4/text = "120hz"
+popup/item_4/id = 4
+popup/item_5/text = "144hz"
+popup/item_5/id = 5
+popup/item_6/text = "365hz"
+popup/item_6/id = 6
+popup/item_7/text = "Unlimited"
+popup/item_7/id = 7
+script = ExtResource("4_381mg")
+section_name = "video"
+setting_name = "refresh_rate"
+default_selected = 0
+
+[node name="QualityPresetLabel" type="Label" parent="VideoSettingList/VideoSettingGrid"]
+layout_mode = 2
+text = "OPTIONS_VIDEO_QUALITY"
+
+[node name="QualityPresetSelector" type="OptionButton" parent="VideoSettingList/VideoSettingGrid"]
+editor_description = "UI-21, UIFUN-22"
+layout_mode = 2
+focus_neighbor_top = NodePath("../RefreshRateSelector")
+item_count = 5
+selected = 1
+popup/item_0/text = "Low"
+popup/item_0/id = 0
+popup/item_1/text = "Medium"
+popup/item_1/id = 1
+popup/item_2/text = "High"
+popup/item_2/id = 2
+popup/item_3/text = "Ultra"
+popup/item_3/id = 3
+popup/item_4/text = "Custom"
+popup/item_4/id = 4
+script = ExtResource("5_srg4v")
+section_name = "video"
+setting_name = "quality_preset"
+default_selected = 1
+
+[node name="ResolutionRevertDialog" type="ConfirmationDialog" parent="." node_paths=PackedStringArray("timer")]
+editor_description = "UI-873"
+disable_3d = true
+title = "OPTIONS_VIDEO_RESOLUTION_DIALOG_TITLE"
+size = Vector2i(730, 100)
+ok_button_text = "DIALOG_OK"
+cancel_button_text = "DIALOG_CANCEL"
+script = ExtResource("8_802cr")
+timer = NodePath("ResolutionRevertTimer")
+
+[node name="ResolutionRevertTimer" type="Timer" parent="ResolutionRevertDialog"]
+wait_time = 5.0
+one_shot = true
+
+[connection signal="option_selected" from="VideoSettingList/VideoSettingGrid/ResolutionSelector" to="VideoSettingList/VideoSettingGrid/ResolutionSelector" method="_on_option_selected"]
+[connection signal="option_selected" from="VideoSettingList/VideoSettingGrid/GuiScaleSelector" to="VideoSettingList/VideoSettingGrid/GuiScaleSelector" method="_on_option_selected"]
+[connection signal="option_selected" from="VideoSettingList/VideoSettingGrid/ScreenModeSelector" to="VideoSettingList/VideoSettingGrid/ScreenModeSelector" method="_on_option_selected"]
+[connection signal="option_selected" from="VideoSettingList/VideoSettingGrid/MonitorDisplaySelector" to="VideoSettingList/VideoSettingGrid/MonitorDisplaySelector" method="_on_option_selected"]
+[connection signal="canceled" from="ResolutionRevertDialog" to="ResolutionRevertDialog" method="_on_canceled_or_close_requested"]
+[connection signal="close_requested" from="ResolutionRevertDialog" to="ResolutionRevertDialog" method="_on_canceled_or_close_requested"]
+[connection signal="confirmed" from="ResolutionRevertDialog" to="ResolutionRevertDialog" method="_on_confirmed"]
+[connection signal="timeout" from="ResolutionRevertDialog/ResolutionRevertTimer" to="ResolutionRevertDialog" method="_on_resolution_revert_timer_timeout"]
diff --git a/game/src/Game/Menu/OptionMenu/VolumeGrid.gd b/game/src/Game/Menu/OptionMenu/VolumeGrid.gd
new file mode 100644
index 0000000..46613b4
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/VolumeGrid.gd
@@ -0,0 +1,70 @@
+extends GridContainer
+
+const RATIO_FOR_LINEAR : float = 100
+
+var _slider_dictionary : Dictionary
+
+var initial_focus : Control
+
+func get_db_as_volume_value(db : float) -> float:
+ # db_to_linear produces a float between 0 and 1 from a db value
+ return db_to_linear(db) * RATIO_FOR_LINEAR
+
+func get_volume_value_as_db(value : float) -> float:
+ # linear_to_db consumes a float between 0 and 1 to produce the db value
+ return linear_to_db(value / RATIO_FOR_LINEAR)
+
+func add_volume_row(bus_name : String, bus_index : int) -> HSlider:
+ var volume_label := Label.new()
+ if bus_name == "Master":
+ volume_label.text = "MASTER_BUS"
+ else:
+ volume_label.text = bus_name
+ add_child(volume_label)
+
+ var volume_slider := SettingHSlider.new()
+ volume_slider.section_name = "audio"
+ volume_slider.setting_name = volume_label.text
+ volume_slider.custom_minimum_size = Vector2(290, 0)
+ volume_slider.size_flags_vertical = Control.SIZE_FILL
+ volume_slider.min_value = 0
+ volume_slider.default_value = 100
+ volume_slider.max_value = 120 # 120 so volume can be boosted somewhat
+ volume_slider.value_changed.connect(_on_slider_value_changed.bind(bus_index))
+ add_child(volume_slider)
+
+ _slider_dictionary[volume_label.text] = volume_slider
+ if not initial_focus: initial_focus = volume_slider
+ return volume_slider
+
+# REQUIREMENTS
+# * UI-22
+func _ready():
+ for bus_index in AudioServer.bus_count:
+ add_volume_row(AudioServer.get_bus_name(bus_index), bus_index)
+
+func _notification(what : int) -> void:
+ match(what):
+ NOTIFICATION_VISIBILITY_CHANGED:
+ if visible and is_inside_tree() and initial_focus: initial_focus.grab_focus()
+
+# REQUIREMENTS
+# * UIFUN-30
+func _on_slider_value_changed(value : float, bus_index : int) -> void:
+ AudioServer.set_bus_volume_db(bus_index, get_volume_value_as_db(value))
+
+
+func _on_options_menu_load_settings(load_file : ConfigFile):
+ for volume_slider in _slider_dictionary.values():
+ volume_slider.load_setting(load_file)
+
+# REQUIREMENTS
+# * UIFUN-23
+func _on_options_menu_save_settings(save_file : ConfigFile):
+ for volume_slider in _slider_dictionary.values():
+ volume_slider.save_setting(save_file)
+
+
+func _on_options_menu_reset_settings():
+ for volume_slider in _slider_dictionary.values():
+ volume_slider.reset_setting()
diff --git a/game/src/Game/Menu/OptionMenu/VolumeGrid.tscn b/game/src/Game/Menu/OptionMenu/VolumeGrid.tscn
new file mode 100644
index 0000000..6d4de3c
--- /dev/null
+++ b/game/src/Game/Menu/OptionMenu/VolumeGrid.tscn
@@ -0,0 +1,8 @@
+[gd_scene load_steps=2 format=3 uid="uid://dy4si8comamnv"]
+
+[ext_resource type="Script" path="res://src/OptionMenu/VolumeGrid.gd" id="1_wb64h"]
+
+[node name="VolumeGrid" type="GridContainer"]
+size_flags_vertical = 0
+columns = 2
+script = ExtResource("1_wb64h")