aboutsummaryrefslogtreecommitdiff
path: root/client/gui/menus
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-09-04 23:47:24 +0200
committermetamuffin <metamuffin@disroot.org>2025-09-05 23:07:07 +0200
commit81deaf81c800900e30046cb927be1c9d91ae61b8 (patch)
tree20ce9898465e8d4c49eeff12a9ea55572517ea7b /client/gui/menus
parentfd80142282fcef628466a18e3ea62f0d1372d807 (diff)
downloadhurrycurry-81deaf81c800900e30046cb927be1c9d91ae61b8.tar
hurrycurry-81deaf81c800900e30046cb927be1c9d91ae61b8.tar.bz2
hurrycurry-81deaf81c800900e30046cb927be1c9d91ae61b8.tar.zst
reorganize client gui files
Diffstat (limited to 'client/gui/menus')
-rw-r--r--client/gui/menus/character.gd96
-rw-r--r--client/gui/menus/character.gd.uid1
-rw-r--r--client/gui/menus/character.tscn230
-rw-r--r--client/gui/menus/chat.gd48
-rw-r--r--client/gui/menus/chat.gd.uid1
-rw-r--r--client/gui/menus/chat.tscn56
-rw-r--r--client/gui/menus/document/document.gd152
-rw-r--r--client/gui/menus/document/document.gd.uid1
-rw-r--r--client/gui/menus/document/document.tscn29
-rw-r--r--client/gui/menus/entry.gd35
-rw-r--r--client/gui/menus/entry.gd.uid1
-rw-r--r--client/gui/menus/entry.tscn12
-rw-r--r--client/gui/menus/error.gd23
-rw-r--r--client/gui/menus/error.gd.uid1
-rw-r--r--client/gui/menus/error.tscn70
-rw-r--r--client/gui/menus/game.gd64
-rw-r--r--client/gui/menus/game.gd.uid1
-rw-r--r--client/gui/menus/game.tscn46
-rw-r--r--client/gui/menus/ingame.gd82
-rw-r--r--client/gui/menus/ingame.gd.uid1
-rw-r--r--client/gui/menus/ingame.tscn142
-rw-r--r--client/gui/menus/main/about.gd169
-rw-r--r--client/gui/menus/main/about.gd.uid1
-rw-r--r--client/gui/menus/main/about.tscn93
-rw-r--r--client/gui/menus/main/background.gd50
-rw-r--r--client/gui/menus/main/background.gd.uid1
-rw-r--r--client/gui/menus/main/background.tscn71
-rw-r--r--client/gui/menus/main/clouds.gdshader36
-rw-r--r--client/gui/menus/main/clouds.gdshader.uid1
-rw-r--r--client/gui/menus/main/main.gd44
-rw-r--r--client/gui/menus/main/main.gd.uid1
-rw-r--r--client/gui/menus/main/main.tscn94
-rw-r--r--client/gui/menus/main/play.gd208
-rw-r--r--client/gui/menus/main/play.gd.uid1
-rw-r--r--client/gui/menus/main/play.tscn149
-rw-r--r--client/gui/menus/main/server_list_item.gd38
-rw-r--r--client/gui/menus/main/server_list_item.gd.uid1
-rw-r--r--client/gui/menus/main/server_list_item.tscn39
-rw-r--r--client/gui/menus/menu.gd151
-rw-r--r--client/gui/menus/menu.gd.uid1
-rw-r--r--client/gui/menus/popup.gd32
-rw-r--r--client/gui/menus/popup.gd.uid1
-rw-r--r--client/gui/menus/popup.tscn52
-rw-r--r--client/gui/menus/popup_large.gd25
-rw-r--r--client/gui/menus/popup_large.gd.uid1
-rw-r--r--client/gui/menus/popup_large.tscn74
-rw-r--r--client/gui/menus/rating/desaturate.gdshader7
-rw-r--r--client/gui/menus/rating/desaturate.gdshader.uid1
-rw-r--r--client/gui/menus/rating/rating.gd65
-rw-r--r--client/gui/menus/rating/rating.gd.uid1
-rw-r--r--client/gui/menus/rating/rating.tscn168
-rw-r--r--client/gui/menus/settings/button_setting.gd30
-rw-r--r--client/gui/menus/settings/button_setting.gd.uid1
-rw-r--r--client/gui/menus/settings/dropdown_setting.gd36
-rw-r--r--client/gui/menus/settings/dropdown_setting.gd.uid1
-rw-r--r--client/gui/menus/settings/game_setting.gd46
-rw-r--r--client/gui/menus/settings/game_setting.gd.uid1
-rw-r--r--client/gui/menus/settings/input/input_manager.gd101
-rw-r--r--client/gui/menus/settings/input/input_manager.gd.uid1
-rw-r--r--client/gui/menus/settings/input/input_setting.gd39
-rw-r--r--client/gui/menus/settings/input/input_setting.gd.uid1
-rw-r--r--client/gui/menus/settings/input/input_value_node.gd74
-rw-r--r--client/gui/menus/settings/input/input_value_node.gd.uid1
-rw-r--r--client/gui/menus/settings/input/input_value_node.tscn24
-rw-r--r--client/gui/menus/settings/number_setting.gd41
-rw-r--r--client/gui/menus/settings/number_setting.gd.uid1
-rw-r--r--client/gui/menus/settings/path_setting.gd64
-rw-r--r--client/gui/menus/settings/path_setting.gd.uid1
-rw-r--r--client/gui/menus/settings/preset_row.gd46
-rw-r--r--client/gui/menus/settings/preset_row.gd.uid1
-rw-r--r--client/gui/menus/settings/range_setting.gd44
-rw-r--r--client/gui/menus/settings/range_setting.gd.uid1
-rw-r--r--client/gui/menus/settings/settings.gd40
-rw-r--r--client/gui/menus/settings/settings.gd.uid1
-rw-r--r--client/gui/menus/settings/settings.tscn61
-rw-r--r--client/gui/menus/settings/settings_category.gd49
-rw-r--r--client/gui/menus/settings/settings_category.gd.uid1
-rw-r--r--client/gui/menus/settings/settings_root.gd40
-rw-r--r--client/gui/menus/settings/settings_root.gd.uid1
-rw-r--r--client/gui/menus/settings/settings_row.gd37
-rw-r--r--client/gui/menus/settings/settings_row.gd.uid1
-rw-r--r--client/gui/menus/settings/settings_row.tscn40
-rw-r--r--client/gui/menus/settings/text_setting.gd38
-rw-r--r--client/gui/menus/settings/text_setting.gd.uid1
-rw-r--r--client/gui/menus/settings/toggle_setting.gd31
-rw-r--r--client/gui/menus/settings/toggle_setting.gd.uid1
-rw-r--r--client/gui/menus/setup/hairstyle_preview.gd27
-rw-r--r--client/gui/menus/setup/hairstyle_preview.gd.uid1
-rw-r--r--client/gui/menus/setup/hairstyle_preview.tscn55
-rw-r--r--client/gui/menus/setup/setup.gd110
-rw-r--r--client/gui/menus/setup/setup.gd.uid1
-rw-r--r--client/gui/menus/setup/setup.tscn398
-rw-r--r--client/gui/menus/transition/scene_transition.gd66
-rw-r--r--client/gui/menus/transition/scene_transition.gd.uid1
-rw-r--r--client/gui/menus/transition/scene_transition.tscn135
-rw-r--r--client/gui/menus/transition/text_loading_anim.gdshader13
-rw-r--r--client/gui/menus/transition/text_loading_anim.gdshader.uid1
97 files changed, 4373 insertions, 0 deletions
diff --git a/client/gui/menus/character.gd b/client/gui/menus/character.gd
new file mode 100644
index 00000000..3c1230ae
--- /dev/null
+++ b/client/gui/menus/character.gd
@@ -0,0 +1,96 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+extends Menu
+
+@onready var character: Character = $Node3D/Character
+@onready var back_button := $VBoxContainer/bottom_panel/back
+@onready var map: Map = $Node3D/Map
+@onready var username_edit = $VBoxContainer/top_panel/a/username
+
+func _ready():
+ super()
+ $VBoxContainer/top_panel/a/username.text = Global.get_profile("username")
+ character.set_style(Global.get_profile("character_style"), "chef")
+ init_map()
+
+func init_map():
+ var map_tile = func (t): match t:
+ ".": return "floor"
+ "=": return "counter"
+ "s": return "stove"
+ "c": return "chair"
+ "t": return "table"
+ "o": return "oven"
+ "#": return "wall"
+ _: push_error("unknown tile: ", t)
+ var tiles = [
+ "...............",
+ "###############",
+ "=oo==ss===.ctc#",
+ "..............#",
+ ".............=#",
+ ".............=#",
+ ".............=#"
+ ].map(func (l): return Array(l.split("")).map(map_tile))
+ var gt = func (e): return null if e[1] >= tiles.size() else null if e[0] >= tiles[e[1]].size() else tiles[e[1]][e[0]]
+ var co = Vector2i(floor(tiles[0].size() / 2), floor(tiles.size() - 2))
+ for y in tiles.size():
+ for x in tiles[y].size():
+ map.set_tile(Vector2i(x,y) - co, gt.call([x,y]), [[x,y-1],[x-1,y],[x,y+1],[x+1,y]].map(gt))
+ map.flush()
+
+func exit():
+ if username_edit.text == "":
+ var popup_data := MenuPopup.Data.new()
+ popup_data.text = tr("c.error.empty_username")
+ var accept_button := Button.new()
+ accept_button.text = tr("c.menu.accept")
+ popup_data.buttons = [accept_button]
+ await submenu("res://gui/menus/popup.tscn", popup_data)
+ return
+ Global.set_profile("username", username_edit.text)
+ Global.save_profile()
+ super()
+
+func _on_character_back_pressed():
+ modify_style(func m(current_style: Dictionary):
+ current_style.color = G.rem_euclid(current_style.color - 1, character.COLORS.size()))
+
+func _on_character_forward_pressed():
+ modify_style(func m(current_style: Dictionary):
+ current_style.color = G.rem_euclid(current_style.color + 1, character.COLORS.size()))
+
+func _on_headwear_back_pressed() -> void:
+ modify_style(func m(current_style: Dictionary):
+ current_style.headwear = G.rem_euclid(current_style.headwear - 1, character.headwears.size()))
+
+func _on_headwear_forward_pressed() -> void:
+ modify_style(func m(current_style: Dictionary):
+ current_style.headwear = G.rem_euclid(current_style.headwear + 1, character.headwears.size()))
+
+func _on_hairstyle_back_pressed() -> void:
+ modify_style(func m(current_style: Dictionary):
+ current_style.hairstyle = G.rem_euclid(current_style.hairstyle - 1, character.hairstyles.size()))
+
+func _on_hairstyle_forward_pressed() -> void:
+ modify_style(func m(current_style: Dictionary):
+ current_style.hairstyle = G.rem_euclid(current_style.hairstyle + 1, character.hairstyles.size()))
+
+func modify_style(modifier: Callable):
+ var current_style: Dictionary = Global.get_profile("character_style")
+ modifier.call(current_style)
+ Global.set_profile("character_style", current_style)
+ character.set_style(Global.get_profile("character_style"), "chef")
diff --git a/client/gui/menus/character.gd.uid b/client/gui/menus/character.gd.uid
new file mode 100644
index 00000000..d0df5488
--- /dev/null
+++ b/client/gui/menus/character.gd.uid
@@ -0,0 +1 @@
+uid://bglusga8l5c27
diff --git a/client/gui/menus/character.tscn b/client/gui/menus/character.tscn
new file mode 100644
index 00000000..7bdf03e6
--- /dev/null
+++ b/client/gui/menus/character.tscn
@@ -0,0 +1,230 @@
+[gd_scene load_steps=11 format=3 uid="uid://1f7xpirm5d28"]
+
+[ext_resource type="Theme" uid="uid://b0qmvo504e457" path="res://gui/resources/theme/theme.tres" id="1_ak2pw"]
+[ext_resource type="Script" uid="uid://bglusga8l5c27" path="res://gui/menus/character.gd" id="1_brhd1"]
+[ext_resource type="PackedScene" uid="uid://b4gone8fu53r7" path="res://map/map.tscn" id="3_6mc88"]
+[ext_resource type="PackedScene" uid="uid://b3hhir2fvnunu" path="res://player/character/character.tscn" id="3_odq7n"]
+[ext_resource type="PackedScene" uid="uid://bg2d78ycorcqk" path="res://gui/menus/transition/scene_transition.tscn" id="4_c0ocf"]
+[ext_resource type="Texture2D" uid="uid://35rd5gamtyqm" path="res://gui/resources/icons/arrow.svg" id="5_kvd7k"]
+[ext_resource type="Texture2D" uid="uid://j75dbytlbju" path="res://gui/resources/icons/arrow_pressed.svg" id="5_xpff8"]
+[ext_resource type="Texture2D" uid="uid://b33qmctbpf48g" path="res://gui/resources/icons/arrow_hover.svg" id="6_soj8g"]
+[ext_resource type="Texture2D" uid="uid://by3qsrpxnfq4w" path="res://gui/resources/icons/arrow_focus.svg" id="6_u31hl"]
+
+[sub_resource type="Environment" id="Environment_ex25y"]
+background_mode = 1
+background_color = Color(0.145548, 0.151043, 0.207031, 1)
+
+[node name="CharacterMenu" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme = ExtResource("1_ak2pw")
+script = ExtResource("1_brhd1")
+
+[node name="Node3D" type="Node3D" parent="."]
+
+[node name="WorldEnvironment" type="WorldEnvironment" parent="Node3D"]
+environment = SubResource("Environment_ex25y")
+
+[node name="Camera3D" type="Camera3D" parent="Node3D"]
+transform = Transform3D(1, 0, 0, 0, 0.977046, 0.21303, 0, -0.21303, 0.977046, 0, 1.137, 2.703)
+current = true
+fov = 41.8
+
+[node name="Map" parent="Node3D" instance=ExtResource("3_6mc88")]
+transform = Transform3D(0.866025, 0, 0.5, 0, 1, 0, -0.5, 0, 0.866025, 0, 0, 0)
+
+[node name="Character" parent="Node3D" instance=ExtResource("3_odq7n")]
+
+[node name="SpotLight3D" type="SpotLight3D" parent="Node3D"]
+transform = Transform3D(0.631535, -0.571246, 0.524254, 0.0428654, 0.700843, 0.712026, -0.774162, -0.427197, 0.467093, 1.79161, 3.07541, 1.58055)
+light_energy = 2.689
+spot_range = 20.159
+spot_angle = 17.9256
+
+[node name="SpotLight3D2" type="SpotLight3D" parent="Node3D"]
+transform = Transform3D(0.32457, 0.109091, -0.93955, 0.0604837, 0.9889, 0.135716, 0.943926, -0.100877, 0.314369, -5.22608, 2.10824, 2.35824)
+light_energy = 2.689
+spot_range = 20.159
+spot_angle = 17.9256
+
+[node name="SpotLight3D3" type="SpotLight3D" parent="Node3D"]
+transform = Transform3D(0.114088, -0.0173997, 0.993318, 0.0610452, 0.99808, 0.0104718, -0.991594, 0.0594426, 0.114931, 8.10732, 0.437069, 2.35824)
+light_energy = 2.689
+spot_range = 20.159
+spot_angle = 17.9256
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="top_panel" type="Panel" parent="VBoxContainer"]
+custom_minimum_size = Vector2(0, 100)
+layout_mode = 2
+
+[node name="a" type="VBoxContainer" parent="VBoxContainer/top_panel"]
+layout_mode = 1
+anchors_preset = 5
+anchor_left = 0.5
+anchor_right = 0.5
+offset_left = -213.0
+offset_top = 13.0
+offset_right = 216.0
+offset_bottom = 110.0
+grow_horizontal = 2
+
+[node name="Label" type="Label" parent="VBoxContainer/top_panel/a"]
+layout_mode = 2
+text = "c.settings.username"
+horizontal_alignment = 1
+
+[node name="username" type="LineEdit" parent="VBoxContainer/top_panel/a"]
+layout_mode = 2
+max_length = 32
+
+[node name="Spacer" type="MarginContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_constants/margin_left = 50
+theme_override_constants/margin_top = 50
+theme_override_constants/margin_right = 50
+theme_override_constants/margin_bottom = 50
+
+[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/Spacer"]
+layout_mode = 2
+alignment = 1
+
+[node name="Headware" type="HBoxContainer" parent="VBoxContainer/Spacer/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+alignment = 1
+
+[node name="Back" type="TextureButton" parent="VBoxContainer/Spacer/VBoxContainer/Headware"]
+layout_mode = 2
+size_flags_horizontal = 3
+focus_neighbor_right = NodePath("../Forward")
+texture_normal = ExtResource("5_kvd7k")
+texture_pressed = ExtResource("5_xpff8")
+texture_hover = ExtResource("6_soj8g")
+texture_focused = ExtResource("6_u31hl")
+ignore_texture_size = true
+stretch_mode = 5
+flip_h = true
+
+[node name="Spacer" type="Control" parent="VBoxContainer/Spacer/VBoxContainer/Headware"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Forward" type="TextureButton" parent="VBoxContainer/Spacer/VBoxContainer/Headware"]
+layout_mode = 2
+size_flags_horizontal = 3
+focus_neighbor_left = NodePath("../Back")
+texture_normal = ExtResource("5_kvd7k")
+texture_pressed = ExtResource("5_xpff8")
+texture_hover = ExtResource("6_soj8g")
+texture_focused = ExtResource("6_u31hl")
+ignore_texture_size = true
+stretch_mode = 5
+
+[node name="Hairstyle" type="HBoxContainer" parent="VBoxContainer/Spacer/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+alignment = 1
+
+[node name="Back" type="TextureButton" parent="VBoxContainer/Spacer/VBoxContainer/Hairstyle"]
+layout_mode = 2
+size_flags_horizontal = 3
+focus_neighbor_right = NodePath("../Forward")
+texture_normal = ExtResource("5_kvd7k")
+texture_pressed = ExtResource("5_xpff8")
+texture_hover = ExtResource("6_soj8g")
+texture_focused = ExtResource("6_u31hl")
+ignore_texture_size = true
+stretch_mode = 5
+flip_h = true
+
+[node name="Spacer" type="Control" parent="VBoxContainer/Spacer/VBoxContainer/Hairstyle"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Forward" type="TextureButton" parent="VBoxContainer/Spacer/VBoxContainer/Hairstyle"]
+layout_mode = 2
+size_flags_horizontal = 3
+focus_neighbor_left = NodePath("../Back")
+texture_normal = ExtResource("5_kvd7k")
+texture_pressed = ExtResource("5_xpff8")
+texture_hover = ExtResource("6_soj8g")
+texture_focused = ExtResource("6_u31hl")
+ignore_texture_size = true
+stretch_mode = 5
+
+[node name="Character" type="HBoxContainer" parent="VBoxContainer/Spacer/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+alignment = 1
+
+[node name="Back" type="TextureButton" parent="VBoxContainer/Spacer/VBoxContainer/Character"]
+layout_mode = 2
+size_flags_horizontal = 3
+focus_neighbor_right = NodePath("../Forward")
+texture_normal = ExtResource("5_kvd7k")
+texture_pressed = ExtResource("5_xpff8")
+texture_hover = ExtResource("6_soj8g")
+texture_focused = ExtResource("6_u31hl")
+ignore_texture_size = true
+stretch_mode = 5
+flip_h = true
+
+[node name="Spacer" type="Control" parent="VBoxContainer/Spacer/VBoxContainer/Character"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Forward" type="TextureButton" parent="VBoxContainer/Spacer/VBoxContainer/Character"]
+layout_mode = 2
+size_flags_horizontal = 3
+focus_neighbor_left = NodePath("../Back")
+texture_normal = ExtResource("5_kvd7k")
+texture_pressed = ExtResource("5_xpff8")
+texture_hover = ExtResource("6_soj8g")
+texture_focused = ExtResource("6_u31hl")
+ignore_texture_size = true
+stretch_mode = 5
+
+[node name="bottom_panel" type="Panel" parent="VBoxContainer"]
+custom_minimum_size = Vector2(0, 75)
+layout_mode = 2
+
+[node name="back" type="Button" parent="VBoxContainer/bottom_panel"]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -39.5
+offset_top = -22.0
+offset_right = 39.5
+offset_bottom = 22.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_vertical = 8
+text = "c.menu.back"
+
+[node name="SceneTransition" parent="." instance=ExtResource("4_c0ocf")]
+visible = false
+layout_mode = 1
+
+[connection signal="pressed" from="VBoxContainer/Spacer/VBoxContainer/Headware/Back" to="." method="_on_headwear_back_pressed"]
+[connection signal="pressed" from="VBoxContainer/Spacer/VBoxContainer/Headware/Forward" to="." method="_on_headwear_forward_pressed"]
+[connection signal="pressed" from="VBoxContainer/Spacer/VBoxContainer/Hairstyle/Back" to="." method="_on_hairstyle_back_pressed"]
+[connection signal="pressed" from="VBoxContainer/Spacer/VBoxContainer/Hairstyle/Forward" to="." method="_on_hairstyle_forward_pressed"]
+[connection signal="pressed" from="VBoxContainer/Spacer/VBoxContainer/Character/Back" to="." method="_on_character_back_pressed"]
+[connection signal="pressed" from="VBoxContainer/Spacer/VBoxContainer/Character/Forward" to="." method="_on_character_forward_pressed"]
+[connection signal="pressed" from="VBoxContainer/bottom_panel/back" to="." method="exit"]
diff --git a/client/gui/menus/chat.gd b/client/gui/menus/chat.gd
new file mode 100644
index 00000000..150b0e7e
--- /dev/null
+++ b/client/gui/menus/chat.gd
@@ -0,0 +1,48 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+extends Menu
+class_name ChatOpen
+
+const CHAT_MESSAGE_SCENE = preload("res://menu/communicate/chat/chat_message.tscn")
+
+@onready var messages_container: VBoxContainer = $PanelContainer/MarginContainer/VBoxContainer/ScrollContainerCustom/Messages
+@onready var scroll_container: ScrollContainerCustom = $PanelContainer/MarginContainer/VBoxContainer/ScrollContainerCustom
+@onready var line: LineEdit = $PanelContainer/MarginContainer/VBoxContainer/LineEdit
+@onready var game_menu: GameMenu = get_parent()
+@onready var game: Game = game_menu.game
+
+func _ready() -> void:
+ super()
+ for i in game.text_message_history:
+ add_message(i)
+
+ game.text_message.connect(
+ func message(m: Game.TextMessage):
+ add_message(m)
+ )
+
+func _input(event: InputEvent) -> void:
+ if Input.is_action_just_pressed("chat"):
+ if line.text != "":
+ game.mp.send_chat(game.my_player_id, line.text)
+ exit()
+ super(event)
+
+func add_message(message: Game.TextMessage):
+ var chat_message: ChatMessage = CHAT_MESSAGE_SCENE.instantiate()
+ messages_container.add_child(chat_message)
+ chat_message.set_message(message.username, message.text, message.color)
+ scroll_container.call_deferred("scroll_to_bottom")
diff --git a/client/gui/menus/chat.gd.uid b/client/gui/menus/chat.gd.uid
new file mode 100644
index 00000000..672792c0
--- /dev/null
+++ b/client/gui/menus/chat.gd.uid
@@ -0,0 +1 @@
+uid://cfweimyoq5vv0
diff --git a/client/gui/menus/chat.tscn b/client/gui/menus/chat.tscn
new file mode 100644
index 00000000..626038f4
--- /dev/null
+++ b/client/gui/menus/chat.tscn
@@ -0,0 +1,56 @@
+[gd_scene load_steps=7 format=3 uid="uid://dbd6k56l4p0ls"]
+
+[ext_resource type="Script" uid="uid://cfweimyoq5vv0" path="res://gui/menus/chat.gd" id="1_gntkb"]
+[ext_resource type="Material" uid="uid://beea1pc5nt67r" path="res://gui/resources/materials/dark_blur_material.tres" id="2_1au48"]
+[ext_resource type="Theme" uid="uid://b0qmvo504e457" path="res://gui/resources/theme/theme.tres" id="3_lrbjr"]
+[ext_resource type="StyleBox" uid="uid://bw4jamyna1top" path="res://gui/resources/style/panel_style_sidebar.tres" id="4_d4nta"]
+[ext_resource type="Script" uid="uid://cmncjc06kadpe" path="res://gui/components/blur_setup.gd" id="5_l1coj"]
+[ext_resource type="Script" uid="uid://bd7bylb2t2m0" path="res://gui/components/touch_scroll_container.gd" id="6_ff15x"]
+
+[node name="ChatOpen" type="Control"]
+layout_mode = 3
+anchors_preset = 9
+anchor_bottom = 1.0
+offset_right = 296.0
+grow_vertical = 2
+script = ExtResource("1_gntkb")
+support_anim = false
+auto_anim = null
+
+[node name="PanelContainer" type="PanelContainer" parent="."]
+material = ExtResource("2_1au48")
+layout_mode = 1
+anchors_preset = 9
+anchor_bottom = 1.0
+offset_right = 296.0
+grow_vertical = 2
+theme = ExtResource("3_lrbjr")
+theme_override_styles/panel = ExtResource("4_d4nta")
+script = ExtResource("5_l1coj")
+
+[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"]
+layout_mode = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/separation = 0
+
+[node name="ScrollContainerCustom" type="ScrollContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
+material = ExtResource("2_1au48")
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+script = ExtResource("6_ff15x")
+auto_scroll_to_bottom = true
+
+[node name="Messages" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/ScrollContainerCustom"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="LineEdit" type="LineEdit" parent="PanelContainer/MarginContainer/VBoxContainer" groups=["autoselect"]]
+layout_mode = 2
+placeholder_text = "c.chat.write_message"
+keep_editing_on_text_submit = true
diff --git a/client/gui/menus/document/document.gd b/client/gui/menus/document/document.gd
new file mode 100644
index 00000000..c7042852
--- /dev/null
+++ b/client/gui/menus/document/document.gd
@@ -0,0 +1,152 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+extends Menu
+
+const MARGIN: int = 75
+
+var labels := {}
+
+func _ready():
+ super()
+ $ScrollContainer/VBoxContainer.add_child(build_document(data))
+
+func build_document(element: Dictionary, bullet: bool = false) -> Control:
+ var node: Control
+ match element["t"]:
+ "document":
+ node = VBoxContainer.new()
+ node.name = "Document"
+ for e in element["es"]:
+ node.add_child(build_document(e))
+ "page":
+ node = PanelContainer.new()
+ node.name = "Page"
+ node.add_theme_stylebox_override("panel", preload("res://menu/theme/style/paper_panel_style.tres"))
+ node.set_custom_minimum_size(Vector2(800, 1131.371))
+ var margin := MarginContainer.new()
+ margin.add_theme_constant_override("margin_bottom", MARGIN)
+ margin.add_theme_constant_override("margin_top", MARGIN)
+ margin.add_theme_constant_override("margin_left", MARGIN)
+ margin.add_theme_constant_override("margin_right", MARGIN)
+ var vbox := VBoxContainer.new()
+ if element["background"]:
+ margin.add_child(background(element["background"]))
+ margin.add_child(vbox)
+ for e in element["es"]:
+ vbox.add_child(build_document(e, bullet))
+ node.add_child(margin)
+ "label":
+ var label_id = element["id"]
+ node = build_document(element["e"], bullet)
+ labels[label_id] = node
+ "list":
+ node = VBoxContainer.new()
+ node.name = "List"
+ for e in element["es"]:
+ node.add_child(build_document(e, true))
+ "table":
+ node = VBoxContainer.new()
+ node.name = "Rows"
+ node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ for r in range(element["es"].size()):
+ var row = HBoxContainer.new()
+ node.add_child(row)
+ row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ row.name = "Row%d" % r
+ for c in element["es"][r]:
+ var e = build_document(c, bullet)
+ e.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ row.add_child(e)
+ "par":
+ node = VBoxContainer.new()
+ node.name = "Paragraph"
+ for e in element["es"]:
+ node.add_child(build_document(e, bullet))
+ "ref":
+ # TODO: Support clicking
+ node = build_document(element["e"], bullet)
+ "conditional":
+ # Ignore all conditionals for now, since they are only revelant for typst version
+ node = Control.new()
+ "text":
+ node = text_node(element, bullet)
+ _:
+ node = Control.new()
+ push_error("Error building document: Unknown type \"%s\"" % element["t"])
+ return node
+
+func text_node(element: Dictionary, bullet: bool) -> Control:
+ var node: Control
+ var label := Label.new()
+ # we need a hbox container for rtl
+ if bullet:
+ node = HBoxContainer.new()
+ var bullet_label := Label.new()
+ bullet_label.text = "•"
+ if element.get("size"):
+ bullet_label.add_theme_font_size_override("font_size", element["size"])
+ # TODO: Ignore font color for now. Will be removed in the future.
+ # if element.get("color"):
+ # bullet_label.add_theme_color_override("font_color", Color(element["color"]))
+ bullet_label.add_theme_color_override("font_color", Color.BLACK)
+ label.add_theme_color_override("font_color", Color.BLACK)
+ node.add_child(bullet_label)
+ label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ node.add_child(label)
+ else:
+ node = label
+ label.name = "Text"
+ label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
+ label.text = Global.get_message_str(element["s"])
+ if element.get("font"):
+ match element["font"]:
+ "Great Vibes":
+ node.add_theme_font_override("font", preload("res://menu/theme/fonts/font-sansita-swashed.woff2"))
+ if element.get("size"):
+ node.add_theme_font_size_override("font_size", element["size"])
+ # TODO: Ignore font color for now. Will be removed in the future.
+ # if element.get("color"):
+ # label.add_theme_color_override("font_color", Color(element["color"]))
+ label.add_theme_color_override("font_color", Color.BLACK)
+ return node
+
+func background(background_name: String) -> SubViewportContainer:
+ var item_name: String
+ match background_name:
+ "cover": item_name = "plate:plate,plate,plate,dirt"
+ "toc": item_name = "tomato"
+ "tomato_soup": item_name = "plate:tomato-soup"
+ "burger": item_name = "plate:sliced-bun,sliced-tomato,sliced-lettuce"
+ "mochi": item_name = "plate:strawberry-mochi"
+ "curry": item_name = "plate:curry,cooked-rice"
+ "icecream": item_name = "plate:strawberry-icecream"
+ "drinks": item_name = "glass:strawberry-shake"
+ var n: item_name = n
+ var scene: ItemRender = preload("res://menu/communicate/item/item_render.tscn").instantiate()
+ scene.set_item(item_name, false)
+ var vc := SubViewportContainer.new()
+ var viewport := SubViewport.new()
+ viewport.add_child(scene)
+ viewport.own_world_3d = true
+ viewport.transparent_bg = true
+ vc.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
+ vc.size_flags_vertical = Control.SIZE_SHRINK_END
+ #vc.material = preload("res://menu/theme/materials/printed_material.tres")
+ vc.add_child(viewport)
+ return vc
+
+func _menu_open(): pass
+func _menu_exit(): pass
diff --git a/client/gui/menus/document/document.gd.uid b/client/gui/menus/document/document.gd.uid
new file mode 100644
index 00000000..c84b53b1
--- /dev/null
+++ b/client/gui/menus/document/document.gd.uid
@@ -0,0 +1 @@
+uid://c83p4k0nredmd
diff --git a/client/gui/menus/document/document.tscn b/client/gui/menus/document/document.tscn
new file mode 100644
index 00000000..537ac8b8
--- /dev/null
+++ b/client/gui/menus/document/document.tscn
@@ -0,0 +1,29 @@
+[gd_scene load_steps=3 format=3 uid="uid://bdggwo8un3mys"]
+
+[ext_resource type="Script" uid="uid://c83p4k0nredmd" path="res://gui/menus/document/document.gd" id="1_gyisx"]
+[ext_resource type="Script" uid="uid://bd7bylb2t2m0" path="res://gui/components/touch_scroll_container.gd" id="2_0d0p0"]
+
+[node name="Document" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_gyisx")
+support_anim = false
+auto_anim = null
+
+[node name="ScrollContainer" type="ScrollContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("2_0d0p0")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer"]
+layout_mode = 2
+size_flags_horizontal = 6
+size_flags_vertical = 4
diff --git a/client/gui/menus/entry.gd b/client/gui/menus/entry.gd
new file mode 100644
index 00000000..91bec795
--- /dev/null
+++ b/client/gui/menus/entry.gd
@@ -0,0 +1,35 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+class_name Entry
+extends Menu
+
+func _ready():
+ super()
+ get_window().title = "Hurry Curry!"
+
+ var args = OS.get_cmdline_user_args()
+ if args.size() == 1:
+ await submenu("res://gui/menus/game.tscn", args[0])
+ elif not Global.get_setting("gameplay.setup_completed"):
+ await submenu("res://gui/menus/setup/setup.tscn")
+ else:
+ await submenu("res://gui/menus/main/main.tscn")
+
+ print("Menu stack empty, quitting game.")
+ get_tree().quit()
+
+func quit():
+ pass
diff --git a/client/gui/menus/entry.gd.uid b/client/gui/menus/entry.gd.uid
new file mode 100644
index 00000000..4bd126a6
--- /dev/null
+++ b/client/gui/menus/entry.gd.uid
@@ -0,0 +1 @@
+uid://yxaynnimyxgr
diff --git a/client/gui/menus/entry.tscn b/client/gui/menus/entry.tscn
new file mode 100644
index 00000000..f4eced3e
--- /dev/null
+++ b/client/gui/menus/entry.tscn
@@ -0,0 +1,12 @@
+[gd_scene load_steps=2 format=3 uid="uid://cd52sr1cmo8oj"]
+
+[ext_resource type="Script" uid="uid://yxaynnimyxgr" path="res://gui/menus/entry.gd" id="1_kibw2"]
+
+[node name="Entry" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_kibw2")
diff --git a/client/gui/menus/error.gd b/client/gui/menus/error.gd
new file mode 100644
index 00000000..e11812d5
--- /dev/null
+++ b/client/gui/menus/error.gd
@@ -0,0 +1,23 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+extends Menu
+
+func _ready():
+ super()
+ $Panel/SmartMarginContainer/contents/mesage.text = Global.error_message
+
+func _on_return_pressed():
+ replace_menu("res://gui/menus/main/main.tscn")
diff --git a/client/gui/menus/error.gd.uid b/client/gui/menus/error.gd.uid
new file mode 100644
index 00000000..20a67804
--- /dev/null
+++ b/client/gui/menus/error.gd.uid
@@ -0,0 +1 @@
+uid://bl0n4atrdcogm
diff --git a/client/gui/menus/error.tscn b/client/gui/menus/error.tscn
new file mode 100644
index 00000000..dcebf322
--- /dev/null
+++ b/client/gui/menus/error.tscn
@@ -0,0 +1,70 @@
+[gd_scene load_steps=7 format=3 uid="uid://cimgn07lbcs4v"]
+
+[ext_resource type="Theme" uid="uid://b0qmvo504e457" path="res://gui/resources/theme/theme.tres" id="1_cabdu"]
+[ext_resource type="PackedScene" uid="uid://l4vm07dtda4j" path="res://gui/menus/main/background.tscn" id="2_5fxol"]
+[ext_resource type="Script" uid="uid://bl0n4atrdcogm" path="res://gui/menus/error.gd" id="2_dbe41"]
+[ext_resource type="PackedScene" uid="uid://bg2d78ycorcqk" path="res://gui/menus/transition/scene_transition.tscn" id="4_1nbt3"]
+[ext_resource type="Material" uid="uid://beea1pc5nt67r" path="res://gui/resources/materials/dark_blur_material.tres" id="4_hxkkd"]
+[ext_resource type="Script" uid="uid://byshs20og68tn" path="res://gui/components/smart_margin_container.gd" id="5_rfcg2"]
+
+[node name="ErrorMenu" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme = ExtResource("1_cabdu")
+script = ExtResource("2_dbe41")
+
+[node name="MenuBackground" parent="." instance=ExtResource("2_5fxol")]
+
+[node name="Panel" type="Panel" parent="."]
+material = ExtResource("4_hxkkd")
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="SmartMarginContainer" type="MarginContainer" parent="Panel"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("5_rfcg2")
+metadata/_custom_type_script = "uid://byshs20og68tn"
+
+[node name="contents" type="VBoxContainer" parent="Panel/SmartMarginContainer"]
+layout_mode = 2
+alignment = 1
+
+[node name="title" type="Label" parent="Panel/SmartMarginContainer/contents"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 61
+text = "Error"
+horizontal_alignment = 1
+
+[node name="mesage" type="Label" parent="Panel/SmartMarginContainer/contents"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 24
+text = "This should be the error message."
+horizontal_alignment = 1
+
+[node name="Control" type="Control" parent="Panel/SmartMarginContainer/contents"]
+custom_minimum_size = Vector2(0, 15.805)
+layout_mode = 2
+
+[node name="return" type="Button" parent="Panel/SmartMarginContainer/contents"]
+layout_mode = 2
+size_flags_horizontal = 4
+text = "Return to Main Menu"
+
+[node name="SceneTransition" parent="." instance=ExtResource("4_1nbt3")]
+visible = false
+layout_mode = 1
+
+[connection signal="pressed" from="Panel/SmartMarginContainer/contents/return" to="." method="_on_return_pressed"]
diff --git a/client/gui/menus/game.gd b/client/gui/menus/game.gd
new file mode 100644
index 00000000..a4916b92
--- /dev/null
+++ b/client/gui/menus/game.gd
@@ -0,0 +1,64 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+extends Menu
+class_name GameMenu
+
+@onready var game: Game = $Game
+@onready var debug_label: RichTextLabel = $Debug
+@onready var overlay: Overlay = $Overlay
+@onready var popup_message: PopupMessage = $PopupMessage
+@onready var chat_preview: ChatPreview = $ChatPreview
+@onready var pinned_items: PinnedItemMessages = $PinnedItemMessages
+
+func _ready():
+ get_tree().get_root().go_back_requested.connect(open_ingame_menu)
+ super()
+ transition.set_loading_text(tr("c.menu.game.connecting"))
+ Settings.hook_changed_init("ui.hide_overlays", false, apply_hide_overlays)
+
+func _input(_event):
+ if Input.is_action_just_pressed("ui_menu"):
+ open_ingame_menu()
+
+ if Input.is_action_just_pressed("chat"):
+ Sound.play_click()
+ chat_preview.visible = false
+ await submenu("res://gui/menus/chat.tscn")
+ chat_preview.visible = true
+
+ if Input.is_action_just_pressed("toggle_overlay"):
+ Global.set_setting("ui.hide_overlays", not Global.get_setting("ui.hide_overlays"))
+
+func _menu_cover(state):
+ game.follow_camera.disable_input_menu = state
+ game.follow_camera.update_disable_input()
+
+func _process(_delta):
+ if Global.get_setting("graphics.debug_info"):
+ debug_label.show()
+ debug_label.text = "%d FPS\nDriver: %s" % [Engine.get_frames_per_second(), ProjectSettings.get_setting("rendering/rendering_device/driver")]
+ else: debug_label.hide()
+
+func open_ingame_menu():
+ if popup != null: return
+ Sound.play_click()
+ submenu("res://gui/menus/ingame.tscn")
+
+func apply_hide_overlays(v: bool):
+ overlay.visible = v
+ pinned_items.visible = v
+ chat_preview.visible = v
+ popup_message.visible = false
diff --git a/client/gui/menus/game.gd.uid b/client/gui/menus/game.gd.uid
new file mode 100644
index 00000000..992bc7f3
--- /dev/null
+++ b/client/gui/menus/game.gd.uid
@@ -0,0 +1 @@
+uid://bmno0s2du3ie6
diff --git a/client/gui/menus/game.tscn b/client/gui/menus/game.tscn
new file mode 100644
index 00000000..56df81d6
--- /dev/null
+++ b/client/gui/menus/game.tscn
@@ -0,0 +1,46 @@
+[gd_scene load_steps=8 format=3 uid="uid://bbjwoxs71fnsk"]
+
+[ext_resource type="Script" uid="uid://bmno0s2du3ie6" path="res://gui/menus/game.gd" id="1_cdpsh"]
+[ext_resource type="PackedScene" uid="uid://c6krh36hoqfg8" path="res://game.tscn" id="2_uojcy"]
+[ext_resource type="PackedScene" uid="uid://bpikve6wlsjfl" path="res://gui/overlays/ingame/score.tscn" id="3_i0ytb"]
+[ext_resource type="PackedScene" uid="uid://bc50la65ntifb" path="res://gui/overlays/lobby/lobby.tscn" id="3_udxby"]
+[ext_resource type="PackedScene" uid="uid://b21nrnkygiyjt" path="res://gui/components/message/popup_message/popup_message.tscn" id="5_n1wy0"]
+[ext_resource type="PackedScene" uid="uid://xcxbmynn8mhi" path="res://gui/overlays/ingame/chat.tscn" id="6_dh5lr"]
+[ext_resource type="PackedScene" uid="uid://dcrr1rwdwbkq8" path="res://gui/components/message/popup_message/pinned_item_messages.tscn" id="7_lf2li"]
+
+[node name="GameMenu" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_cdpsh")
+auto_anim = false
+
+[node name="Game" parent="." instance=ExtResource("2_uojcy")]
+
+[node name="Overlay" parent="." instance=ExtResource("3_i0ytb")]
+layout_mode = 1
+
+[node name="Lobby" parent="." instance=ExtResource("3_udxby")]
+layout_mode = 1
+
+[node name="PinnedItemMessages" parent="." instance=ExtResource("7_lf2li")]
+layout_mode = 1
+
+[node name="ChatPreview" parent="." instance=ExtResource("6_dh5lr")]
+layout_mode = 1
+
+[node name="PopupMessage" parent="." instance=ExtResource("5_n1wy0")]
+layout_mode = 1
+
+[node name="Debug" type="RichTextLabel" parent="."]
+visible = false
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
diff --git a/client/gui/menus/ingame.gd b/client/gui/menus/ingame.gd
new file mode 100644
index 00000000..4809b2ee
--- /dev/null
+++ b/client/gui/menus/ingame.gd
@@ -0,0 +1,82 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+extends Menu
+
+@onready var anim = $AnimationPlayer
+@onready var options = $Side/Margin/Options
+@onready var game: Game = $"../Game"
+@onready var lobby_button: Button = $Side/Margin/Options/Lobby
+@onready var leave_button: Button = $Side/Margin/Options/Leave
+
+var opened
+func _ready():
+ opened = Time.get_ticks_msec()
+ game.join_state_updated.connect(_on_game_join_state_changed)
+ _on_game_join_state_changed(game.join_state)
+ update_lobby_button()
+ super()
+
+func update_lobby_button():
+ lobby_button.disabled = game.in_lobby or game.join_state == Game.JoinState.SPECTATING
+ if game.in_lobby:
+ lobby_button.tooltip_text = "Cannot cancel game since no game is running."
+ elif not game.join_state == Game.JoinState.JOINED:
+ lobby_button.tooltip_text = "You must join in order to be able to cancel the current game."
+ else:
+ lobby_button.tooltip_text = ""
+
+func anim_setup(): pass
+func _menu_open():
+ anim.play("activate")
+ await anim.animation_finished
+ game.mp.send_idle(true)
+func _menu_exit():
+ game.mp.send_idle(false)
+ anim.play_backwards("activate")
+ await anim.animation_finished
+
+func _on_resume_pressed():
+ exit()
+
+func _on_main_menu_pressed():
+ parent_menu.replace_menu("res://gui/menus/main/main.tscn")
+
+func _on_settings_pressed():
+ submenu("res://gui/menus/settings/settings.tscn")
+
+func _on_reconnect_pressed():
+ parent_menu.replace_menu("res://gui/menus/game.tscn", parent_menu.data)
+
+func _on_quit_pressed():
+ quit()
+
+func _on_lobby_pressed():
+ game.mp.send_chat(game.my_player_id, "/end")
+ exit()
+
+func _on_leave_pressed():
+ game.toggle_join()
+
+func _on_game_join_state_changed(state: Game.JoinState):
+ match state:
+ Game.JoinState.JOINED:
+ leave_button.disabled = false
+ leave_button.text = tr("c.menu.ingame.leave")
+ Game.JoinState.SPECTATING:
+ leave_button.disabled = false
+ leave_button.text = tr("c.menu.ingame.join")
+ Game.JoinState.WAITING:
+ leave_button.disabled = true
diff --git a/client/gui/menus/ingame.gd.uid b/client/gui/menus/ingame.gd.uid
new file mode 100644
index 00000000..496c313c
--- /dev/null
+++ b/client/gui/menus/ingame.gd.uid
@@ -0,0 +1 @@
+uid://dyi2xohgxeybb
diff --git a/client/gui/menus/ingame.tscn b/client/gui/menus/ingame.tscn
new file mode 100644
index 00000000..0a77af14
--- /dev/null
+++ b/client/gui/menus/ingame.tscn
@@ -0,0 +1,142 @@
+[gd_scene load_steps=12 format=3 uid="uid://lxlgtjm8hw7v"]
+
+[ext_resource type="Theme" uid="uid://b0qmvo504e457" path="res://gui/resources/theme/theme.tres" id="1_2vmyh"]
+[ext_resource type="Script" uid="uid://dyi2xohgxeybb" path="res://gui/menus/ingame.gd" id="2_0h3no"]
+[ext_resource type="Material" uid="uid://beea1pc5nt67r" path="res://gui/resources/materials/dark_blur_material.tres" id="3_vvvlt"]
+[ext_resource type="Script" uid="uid://cmncjc06kadpe" path="res://gui/components/blur_setup.gd" id="4_b6bm7"]
+[ext_resource type="FontFile" uid="uid://bo4vh5xkpvrh1" path="res://gui/resources/fonts/font-sansita-swashed.woff2" id="4_scupw"]
+[ext_resource type="StyleBox" uid="uid://bw4jamyna1top" path="res://gui/resources/style/panel_style_sidebar.tres" id="4_vr8y1"]
+[ext_resource type="Script" uid="uid://byshs20og68tn" path="res://gui/components/smart_margin_container.gd" id="6_poj4k"]
+
+[sub_resource type="Animation" id="Animation_8sedy"]
+length = 0.001
+tracks/0/type = "bezier"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("Side:position:x")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"handle_modes": PackedInt32Array(0),
+"points": PackedFloat32Array(0, -0.0005, 0, 0.0005, 0),
+"times": PackedFloat32Array(0)
+}
+
+[sub_resource type="Animation" id="Animation_660jl"]
+resource_name = "activate"
+tracks/0/type = "bezier"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("Side:position:x")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"handle_modes": PackedInt32Array(0, 0),
+"points": PackedFloat32Array(-400, -0.25, 0, 0.25, 0, 0, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0, 1)
+}
+
+[sub_resource type="AnimationLibrary" id="AnimationLibrary_u0kyp"]
+_data = {
+&"RESET": SubResource("Animation_8sedy"),
+&"activate": SubResource("Animation_660jl")
+}
+
+[sub_resource type="FontVariation" id="FontVariation_ud3l8"]
+base_font = ExtResource("4_scupw")
+variation_embolden = 0.5
+
+[node name="IngameMenu" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme = ExtResource("1_2vmyh")
+script = ExtResource("2_0h3no")
+
+[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
+libraries = {
+&"": SubResource("AnimationLibrary_u0kyp")
+}
+speed_scale = 8.0
+
+[node name="Side" type="PanelContainer" parent="."]
+material = ExtResource("3_vvvlt")
+layout_mode = 1
+anchors_preset = 9
+anchor_bottom = 1.0
+offset_right = 296.0
+grow_vertical = 2
+theme_override_styles/panel = ExtResource("4_vr8y1")
+script = ExtResource("4_b6bm7")
+
+[node name="Margin" type="MarginContainer" parent="Side"]
+layout_mode = 2
+theme_override_constants/margin_left = 20
+theme_override_constants/margin_top = 20
+theme_override_constants/margin_right = 20
+theme_override_constants/margin_bottom = 20
+script = ExtResource("6_poj4k")
+
+[node name="Options" type="VBoxContainer" parent="Side/Margin"]
+layout_mode = 2
+
+[node name="Title" type="Label" parent="Side/Margin/Options"]
+auto_translate_mode = 2
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0.566408, 0.208917, 0.266045, 1)
+theme_override_constants/outline_size = 10
+theme_override_fonts/font = SubResource("FontVariation_ud3l8")
+theme_override_font_sizes/font_size = 48
+text = "Hurry Curry!"
+
+[node name="Spacer" type="Control" parent="Side/Margin/Options"]
+custom_minimum_size = Vector2(0, 10)
+layout_mode = 2
+
+[node name="Resume" type="Button" parent="Side/Margin/Options"]
+layout_mode = 2
+text = "c.menu.ingame.resume"
+alignment = 0
+
+[node name="Leave" type="Button" parent="Side/Margin/Options"]
+layout_mode = 2
+text = "c.menu.ingame.join"
+alignment = 0
+
+[node name="Lobby" type="Button" parent="Side/Margin/Options"]
+layout_mode = 2
+text = "c.menu.ingame.cancel"
+alignment = 0
+
+[node name="Reconnect" type="Button" parent="Side/Margin/Options"]
+layout_mode = 2
+text = "c.menu.ingame.reconnect"
+alignment = 0
+
+[node name="Spacer2" type="Control" parent="Side/Margin/Options"]
+custom_minimum_size = Vector2(0, 10)
+layout_mode = 2
+
+[node name="Settings" type="Button" parent="Side/Margin/Options"]
+layout_mode = 2
+text = "c.menu.settings"
+alignment = 0
+
+[node name="Spacer3" type="Control" parent="Side/Margin/Options"]
+custom_minimum_size = Vector2(0, 10)
+layout_mode = 2
+
+[node name="MainMenu" type="Button" parent="Side/Margin/Options"]
+layout_mode = 2
+text = "c.menu.ingame.main_menu"
+alignment = 0
+
+[connection signal="pressed" from="Side/Margin/Options/Resume" to="." method="_on_resume_pressed"]
+[connection signal="pressed" from="Side/Margin/Options/Leave" to="." method="_on_leave_pressed"]
+[connection signal="pressed" from="Side/Margin/Options/Lobby" to="." method="_on_lobby_pressed"]
+[connection signal="pressed" from="Side/Margin/Options/Reconnect" to="." method="_on_reconnect_pressed"]
+[connection signal="pressed" from="Side/Margin/Options/Settings" to="." method="_on_settings_pressed"]
+[connection signal="pressed" from="Side/Margin/Options/MainMenu" to="." method="_on_main_menu_pressed"]
diff --git a/client/gui/menus/main/about.gd b/client/gui/menus/main/about.gd
new file mode 100644
index 00000000..b56d3941
--- /dev/null
+++ b/client/gui/menus/main/about.gd
@@ -0,0 +1,169 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+
+extends Menu
+
+var authors := ["metamuffin", "nokoe", "tpart"]
+var contributors := ["sofviic", "BigBrotherNii", "Miner34"]
+const cc_by_4 := "CC-BY 4.0"
+const cc_by_3 := "CC-BY 3.0"
+const cc0 := "CC0"
+
+const AGPL_NOTICE := """This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, version 3 of the License only.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <https://www.gnu.org/licenses/>."""
+
+const SOURCE_CODE := "https://codeberg.org/hurrycurry/hurrycurry"
+
+func _ready() -> void:
+ super()
+ $side/margin/options/first/source.tooltip_text = SOURCE_CODE
+
+var credits := [
+ [tr("c.credits.models"), [
+ ["kenney.nl", "Various Models", cc0],
+ ["Kay Lousberg", "Kitchen tiles", cc0],
+ ["Poly by Google", "Strawberry", cc_by_3],
+ ["Poly by Google", "Fish", cc_by_3],
+ ["jeremy", "Propeller hat", cc_by_3]
+ ]],
+ [tr("c.credits.sounds"), [
+ ["Dryoma", "Footstep sounds", cc_by_4],
+ ["Koops", "Page_Turn_24.wav", cc_by_4],
+ ["InspectorJ", "Pencil, Writing, Close, A.wav", cc_by_4],
+ ["Dillon Becker", "Super Dialogue Audio Pack", cc_by_4],
+ ["Ellr", "Universal UI/Menu Soundpack", cc_by_4],
+ ["toyoto", "Woosh Fleuret Escrime B.wav", cc_by_4],
+ ["deleted_user_877451", "Game_over.wav", cc_by_3],
+ ["Quaternius", "Someting, dont remember...", cc0],
+ ["Dillon Becker", "Super Dialogue Audio Pack V1", cc_by_4]
+ ]],
+ [tr("c.credits.translations"), {
+ tr("c.settings.ui.language.zh_Hans"): ["Outbreak2096"],
+ tr("c.settings.ui.language.zh_Hant"): ["hugoalh"],
+ tr("c.settings.ui.language.nl"): ["Vistaus"],
+ tr("c.settings.ui.language.it"): ["Miner34", "solemden"],
+ tr("c.settings.ui.language.eu"): ["josuigoa"],
+ tr("c.settings.ui.language.fr"): ["fnetX", "lejun"],
+ tr("c.settings.ui.language.pl"): ["tranzystorekk"],
+ tr("c.settings.ui.language.he"): ["RustyStriker"],
+ tr("c.settings.ui.language.el"): ["n0toose"],
+ tr("c.settings.ui.language.ja"): ["BigBrotherNii"],
+ tr("c.settings.ui.language.ar"): ["sofviic"],
+ tr("c.settings.ui.language.tr"): ["furkanunsalan", "tekrei"],
+ tr("c.settings.ui.language.ru"): ["0ko"],
+ }]
+]
+
+func _menu_cover(state):
+ $side.visible = not state
+
+func credits_text() -> String:
+ var text = "[center]"
+ authors.shuffle()
+ contributors.shuffle()
+
+ text += "\n\n\n[b]%s[/b]\n\n%s\n\n[b]%s[/b]\n\n%s\n\n[b]%s[/b]\n\n\n" % [
+ tr("c.credits.title"),
+ tr("c.credits.developed_by"),
+ "\n".join(authors),
+ tr("c.credits.contributors"),
+ ", ".join(contributors),
+ ]
+
+ for section in credits:
+ text += "[b]%s[/b]\n\n" % section[0]
+ if typeof(section[1]) == TYPE_DICTIONARY:
+ text += "[table=2]"
+ for key in section[1]:
+ text += "[cell][right]%s[/right][/cell]" % key
+ text += "[cell][left]%s[/left][/cell]" % ", ".join(section[1][key])
+ text += "[/table]"
+ else:
+ text += "[table=3]"
+ for entry in section[1]:
+ text += "[cell][right]%s[/right][/cell]" % entry[0]
+ text += "[cell][left]%s[/left][/cell]" % entry[1]
+ text += "[cell][left]%s[/left][/cell]" % entry[2]
+ text += "[/table]"
+ text += "\n\n\n"
+
+ text += "\n[b]%s[/b]\n\n\n[/center]" % tr("c.credits.thanks")
+ return text
+
+func legal_text() -> String:
+ var all: Array[String] = []
+ var translators: Array[String] = []
+ for c in credits[2][1].values():
+ translators.append_array(c)
+ translators.shuffle()
+ authors.shuffle()
+ contributors.shuffle()
+ all.append_array(authors)
+ all.append_array(contributors)
+ all.append_array(translators)
+
+ var text := "Hurry Curry! - a game about cooking\n"
+ text += "[code]Copyright 2024, 2025 %s\n\n" % ", ".join(dedup_array(all))
+ text += "%s[/code]\n\n" % AGPL_NOTICE
+ text += tr("c.legal.using_godot")
+ text += "\n\n[code]%s[/code]" % Engine.get_license_text()
+ return text
+
+func version_text() -> String:
+ var text := "[center][b]Hurry Curry![/b]\n\n"
+ OS.get_version()
+ text += "[table=2]"
+ var row = "[cell][right]%s[/right][/cell][cell][left]%s[/left][/cell]"
+ text += row % [tr("c.version.game"), Global.VERSION]
+ text += row % [tr("c.version.protocol"), "%s.%s" % [Multiplayer.VERSION_MAJOR, Multiplayer.VERSION_MINOR]]
+ text += row % [tr("c.version.godot"), Engine.get_version_info().string]
+ text += row % [tr("c.version.os"), OS.get_name()]
+ text += row % [tr("c.version.arch"), Engine.get_architecture_name()]
+ text += row % [tr("c.version.distribution"), Global.DISTRIBUTION]
+
+ text += "[/table]"
+ text += "[/center]"
+ return text
+
+func _on_credits_pressed() -> void:
+ submenu("res://gui/menus/popup_large.tscn", credits_text())
+
+func _on_legal_pressed() -> void:
+ submenu("res://gui/menus/popup_large.tscn", legal_text())
+
+func _on_version_pressed() -> void:
+ submenu("res://gui/menus/popup_large.tscn", version_text())
+
+func _on_back_pressed() -> void:
+ exit()
+
+
+func _on_source_pressed() -> void:
+ OS.shell_open(SOURCE_CODE)
+
+func dedup_array(a: Array) -> Array:
+ var b = []
+ for x in a: if not b.has(x): b.append(x)
+ return b
diff --git a/client/gui/menus/main/about.gd.uid b/client/gui/menus/main/about.gd.uid
new file mode 100644
index 00000000..3929f2c3
--- /dev/null
+++ b/client/gui/menus/main/about.gd.uid
@@ -0,0 +1 @@
+uid://pcu87stpkgd8
diff --git a/client/gui/menus/main/about.tscn b/client/gui/menus/main/about.tscn
new file mode 100644
index 00000000..006b61fe
--- /dev/null
+++ b/client/gui/menus/main/about.tscn
@@ -0,0 +1,93 @@
+[gd_scene load_steps=7 format=3 uid="uid://bpaenm8c6nmo8"]
+
+[ext_resource type="Script" uid="uid://pcu87stpkgd8" path="res://gui/menus/main/about.gd" id="1_0acu0"]
+[ext_resource type="Material" uid="uid://2j8a0c0a2ta5" path="res://gui/resources/materials/blur_material.tres" id="1_ai5pk"]
+[ext_resource type="StyleBox" uid="uid://bw4jamyna1top" path="res://gui/resources/style/panel_style_sidebar.tres" id="2_pya1x"]
+[ext_resource type="FontFile" uid="uid://bo4vh5xkpvrh1" path="res://gui/resources/fonts/font-sansita-swashed.woff2" id="4_kx3j7"]
+[ext_resource type="Script" uid="uid://byshs20og68tn" path="res://gui/components/smart_margin_container.gd" id="4_t51wf"]
+
+[sub_resource type="FontVariation" id="FontVariation_o2r3e"]
+base_font = ExtResource("4_kx3j7")
+variation_embolden = 0.5
+
+[node name="AboutMenu" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_0acu0")
+support_anim = false
+
+[node name="side" type="PanelContainer" parent="."]
+material = ExtResource("1_ai5pk")
+layout_mode = 1
+anchors_preset = 9
+anchor_bottom = 1.0
+offset_right = 294.0
+grow_vertical = 2
+theme_override_styles/panel = ExtResource("2_pya1x")
+
+[node name="margin" type="MarginContainer" parent="side"]
+layout_mode = 2
+theme_override_constants/margin_left = 20
+theme_override_constants/margin_top = 20
+theme_override_constants/margin_right = 20
+theme_override_constants/margin_bottom = 20
+script = ExtResource("4_t51wf")
+
+[node name="options" type="VBoxContainer" parent="side/margin"]
+layout_mode = 2
+
+[node name="title" type="Label" parent="side/margin/options"]
+auto_translate_mode = 2
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0.566408, 0.208917, 0.266045, 1)
+theme_override_constants/outline_size = 10
+theme_override_fonts/font = SubResource("FontVariation_o2r3e")
+theme_override_font_sizes/font_size = 48
+text = "Hurry Curry!"
+
+[node name="spacer" type="Control" parent="side/margin/options"]
+custom_minimum_size = Vector2(0, 10)
+layout_mode = 2
+
+[node name="first" type="VBoxContainer" parent="side/margin/options"]
+layout_mode = 2
+
+[node name="credits" type="Button" parent="side/margin/options/first"]
+layout_mode = 2
+text = "c.menu.about.credits"
+alignment = 0
+
+[node name="version" type="Button" parent="side/margin/options/first"]
+layout_mode = 2
+text = "c.menu.about.version"
+alignment = 0
+
+[node name="legal" type="Button" parent="side/margin/options/first"]
+layout_mode = 2
+text = "c.menu.about.legal"
+alignment = 0
+
+[node name="source" type="Button" parent="side/margin/options/first"]
+layout_mode = 2
+text = "c.menu.about.source"
+alignment = 0
+
+[node name="first2" type="VBoxContainer" parent="side/margin/options"]
+layout_mode = 2
+size_flags_vertical = 3
+alignment = 2
+
+[node name="back" type="Button" parent="side/margin/options/first2"]
+layout_mode = 2
+text = "c.menu.back"
+alignment = 0
+
+[connection signal="pressed" from="side/margin/options/first/credits" to="." method="_on_credits_pressed"]
+[connection signal="pressed" from="side/margin/options/first/version" to="." method="_on_version_pressed"]
+[connection signal="pressed" from="side/margin/options/first/legal" to="." method="_on_legal_pressed"]
+[connection signal="pressed" from="side/margin/options/first/source" to="." method="_on_source_pressed"]
+[connection signal="pressed" from="side/margin/options/first2/back" to="." method="_on_back_pressed"]
diff --git a/client/gui/menus/main/background.gd b/client/gui/menus/main/background.gd
new file mode 100644
index 00000000..4abb84b4
--- /dev/null
+++ b/client/gui/menus/main/background.gd
@@ -0,0 +1,50 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+extends Node3D
+
+const CRATES = ["tomato-crate", "steak-crate", "cheese-crate", "lettuce-crate", "flour-crate", "coconut-crate"]
+const TOOLS = ["stove", "stove", "stove", "sink", "cuttingboard", "sink", "cuttingboard", "oven", "freezer"]
+
+@onready var environment: WorldEnvironment = $Environment
+@onready var map: Map = $Map
+
+func _ready():
+ if !Global.on_vulkan():
+ environment.environment.tonemap_exposure = 0.25
+
+ var tiles = {}
+ for x in range(-10, 11):
+ for y in range(-10, 11):
+ var w = exp(-sqrt(x * x + y * y) * 0.15)
+ var k = randf() * w
+ var tn = null
+ if k > 0.25: tn = "floor"
+ if k > 0.4: tn = choose(CRATES) if randf() > 0.3 else "counter"
+ if k > 0.6: tn = choose(TOOLS)
+ if tn != null: tiles[str(Vector2i(x,y))] = [tn,[x,y]]
+
+ var gt = func (cs):
+ var t = tiles.get(str(Vector2i(cs[0],cs[1])))
+ return null if t == null else t[0]
+ for pk in tiles.keys():
+ var x = tiles[pk][1][0]
+ var y = tiles[pk][1][1]
+ var t = gt.call([x,y])
+ if t != null: map.set_tile(Vector2i(x,y), t, [[x,y-1],[x-1,y],[x,y+1],[x+1,y]].map(gt))
+
+ map.flush()
+
+func choose(a): return a[floor(a.size() * randf())]
diff --git a/client/gui/menus/main/background.gd.uid b/client/gui/menus/main/background.gd.uid
new file mode 100644
index 00000000..7d61d488
--- /dev/null
+++ b/client/gui/menus/main/background.gd.uid
@@ -0,0 +1 @@
+uid://b2tq5rcjjcxdg
diff --git a/client/gui/menus/main/background.tscn b/client/gui/menus/main/background.tscn
new file mode 100644
index 00000000..2bd4ee04
--- /dev/null
+++ b/client/gui/menus/main/background.tscn
@@ -0,0 +1,71 @@
+[gd_scene load_steps=12 format=3 uid="uid://l4vm07dtda4j"]
+
+[ext_resource type="Script" uid="uid://b2tq5rcjjcxdg" path="res://gui/menus/main/background.gd" id="1_pgu7b"]
+[ext_resource type="Script" uid="uid://cwg7wympevxs4" path="res://map/auto_setup/environment_setup.gd" id="2_7dwbj"]
+[ext_resource type="Shader" uid="uid://b1k6ipo0sagli" path="res://gui/menus/main/clouds.gdshader" id="3_lapmn"]
+[ext_resource type="PackedScene" uid="uid://b4gone8fu53r7" path="res://map/map.tscn" id="4_nslxb"]
+
+[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_uw50b"]
+sky_top_color = Color(0.55, 0.55, 0.55, 1)
+
+[sub_resource type="Sky" id="Sky_utrtx"]
+sky_material = SubResource("ProceduralSkyMaterial_uw50b")
+
+[sub_resource type="Environment" id="Environment_slkjl"]
+background_mode = 1
+background_color = Color(0.517035, 0.49506, 0.878906, 1)
+sky = SubResource("Sky_utrtx")
+tonemap_mode = 2
+tonemap_exposure = 0.6
+tonemap_white = 0.9
+ssao_enabled = true
+
+[sub_resource type="QuadMesh" id="QuadMesh_fvp2p"]
+size = Vector2(100, 100)
+
+[sub_resource type="FastNoiseLite" id="FastNoiseLite_e3eby"]
+noise_type = 2
+frequency = 0.008
+fractal_octaves = 7
+fractal_gain = 0.72
+
+[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_s4fnp"]
+width = 1024
+height = 1024
+generate_mipmaps = false
+seamless = true
+noise = SubResource("FastNoiseLite_e3eby")
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_gd87g"]
+render_priority = 0
+shader = ExtResource("3_lapmn")
+shader_parameter/noise = SubResource("NoiseTexture2D_s4fnp")
+shader_parameter/ccloud = Color(0.835938, 0.835938, 0.835938, 1)
+shader_parameter/csky = Color(0.329412, 0.333333, 0.8, 1)
+
+[node name="MenuBackground" type="Node3D"]
+script = ExtResource("1_pgu7b")
+
+[node name="Camera" type="Camera3D" parent="."]
+transform = Transform3D(0.614606, 0.499662, -0.610408, -0.00282255, 0.775198, 0.631712, 0.78883, -0.386531, 0.477852, -9.13611, 4.90356, 1.22532)
+projection = 1
+current = true
+size = 8.0
+far = 100.0
+
+[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
+transform = Transform3D(0.290334, 0.904946, -0.311092, 0.336606, 0.207739, 0.918445, 0.895769, -0.371371, -0.244296, 0, 7, 0)
+light_energy = 1.25
+shadow_enabled = true
+
+[node name="Environment" type="WorldEnvironment" parent="."]
+environment = SubResource("Environment_slkjl")
+script = ExtResource("2_7dwbj")
+allow_sdfgi = false
+
+[node name="the-sky-tm" type="MeshInstance3D" parent="."]
+transform = Transform3D(0.614606, 0.499662, -0.610408, -0.00282255, 0.775198, 0.631712, 0.78883, -0.386531, 0.477851, 6, -13, -11)
+mesh = SubResource("QuadMesh_fvp2p")
+surface_material_override/0 = SubResource("ShaderMaterial_gd87g")
+
+[node name="Map" parent="." instance=ExtResource("4_nslxb")]
diff --git a/client/gui/menus/main/clouds.gdshader b/client/gui/menus/main/clouds.gdshader
new file mode 100644
index 00000000..8103f691
--- /dev/null
+++ b/client/gui/menus/main/clouds.gdshader
@@ -0,0 +1,36 @@
+/*
+ Hurry Curry! - a game about cooking
+ Copyright (C) 2025 Hurry Curry! Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, version 3 of the License only.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+*/
+shader_type spatial;
+render_mode unshaded;
+
+uniform sampler2D noise : source_color;
+uniform vec3 ccloud : source_color;
+uniform vec3 csky : source_color;
+
+void fragment() {
+ vec2 uv = UV * 0.9;
+ uv += TIME * vec2(0.001,0.002);
+
+ float f = texture(noise, uv).x;
+ f = 1. - f;
+ f = pow(f, 1.5);
+ f = floor(f*5.)/5.;
+ f = pow(f, 2.);
+
+ ALBEDO = mix(csky, ccloud, f);
+}
diff --git a/client/gui/menus/main/clouds.gdshader.uid b/client/gui/menus/main/clouds.gdshader.uid
new file mode 100644
index 00000000..00c2d21a
--- /dev/null
+++ b/client/gui/menus/main/clouds.gdshader.uid
@@ -0,0 +1 @@
+uid://b1k6ipo0sagli
diff --git a/client/gui/menus/main/main.gd b/client/gui/menus/main/main.gd
new file mode 100644
index 00000000..423f756e
--- /dev/null
+++ b/client/gui/menus/main/main.gd
@@ -0,0 +1,44 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+extends Menu
+
+@onready var quit_button = $side/margin/options/first/quit
+@onready var margin_container: MarginContainer = $side/margin
+
+func _ready():
+ super()
+ if OS.has_feature("web"):
+ quit_button.hide()
+ Sound.play_music("MainMenu")
+ ServerList.one_shot()
+
+func _menu_cover(state):
+ $side.visible = not state
+
+func _on_quit_pressed():
+ quit()
+
+func _on_about_pressed():
+ submenu("res://gui/menus/main/about.tscn")
+
+func _on_change_character_pressed():
+ replace_menu("res://gui/menus/character.tscn", null, "res://gui/menus/main.tscn")
+
+func _on_settings_pressed():
+ submenu("res://gui/menus/settings/settings.tscn")
+
+func _on_play_pressed():
+ submenu("res://gui/menus/main/play.tscn")
diff --git a/client/gui/menus/main/main.gd.uid b/client/gui/menus/main/main.gd.uid
new file mode 100644
index 00000000..dc2cecd3
--- /dev/null
+++ b/client/gui/menus/main/main.gd.uid
@@ -0,0 +1 @@
+uid://bpiynadrmdd37
diff --git a/client/gui/menus/main/main.tscn b/client/gui/menus/main/main.tscn
new file mode 100644
index 00000000..81595966
--- /dev/null
+++ b/client/gui/menus/main/main.tscn
@@ -0,0 +1,94 @@
+[gd_scene load_steps=10 format=3 uid="uid://dbj8508whxgwv"]
+
+[ext_resource type="Theme" uid="uid://b0qmvo504e457" path="res://gui/resources/theme/theme.tres" id="1_3qfu3"]
+[ext_resource type="Script" uid="uid://bpiynadrmdd37" path="res://gui/menus/main/main.gd" id="2_xjnc3"]
+[ext_resource type="PackedScene" uid="uid://l4vm07dtda4j" path="res://gui/menus/main/background.tscn" id="3_4evao"]
+[ext_resource type="Material" uid="uid://2j8a0c0a2ta5" path="res://gui/resources/materials/blur_material.tres" id="4_nx4vf"]
+[ext_resource type="Script" uid="uid://cmncjc06kadpe" path="res://gui/components/blur_setup.gd" id="5_0mn56"]
+[ext_resource type="FontFile" uid="uid://bo4vh5xkpvrh1" path="res://gui/resources/fonts/font-sansita-swashed.woff2" id="5_k7bqq"]
+[ext_resource type="StyleBox" uid="uid://bw4jamyna1top" path="res://gui/resources/style/panel_style_sidebar.tres" id="5_qlyeo"]
+[ext_resource type="Script" uid="uid://byshs20og68tn" path="res://gui/components/smart_margin_container.gd" id="7_btdj1"]
+
+[sub_resource type="FontVariation" id="FontVariation_htgmg"]
+base_font = ExtResource("5_k7bqq")
+variation_embolden = 0.5
+
+[node name="MainMenu" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme = ExtResource("1_3qfu3")
+script = ExtResource("2_xjnc3")
+
+[node name="MenuBackground" parent="." instance=ExtResource("3_4evao")]
+
+[node name="side" type="PanelContainer" parent="."]
+material = ExtResource("4_nx4vf")
+layout_mode = 1
+anchors_preset = 9
+anchor_bottom = 1.0
+offset_right = 294.0
+grow_vertical = 2
+theme_override_styles/panel = ExtResource("5_qlyeo")
+script = ExtResource("5_0mn56")
+
+[node name="margin" type="MarginContainer" parent="side"]
+layout_mode = 2
+theme_override_constants/margin_left = 20
+theme_override_constants/margin_top = 20
+theme_override_constants/margin_right = 20
+theme_override_constants/margin_bottom = 20
+script = ExtResource("7_btdj1")
+
+[node name="options" type="VBoxContainer" parent="side/margin"]
+layout_mode = 2
+
+[node name="title" type="Label" parent="side/margin/options"]
+auto_translate_mode = 2
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0.566408, 0.208917, 0.266045, 1)
+theme_override_constants/outline_size = 10
+theme_override_fonts/font = SubResource("FontVariation_htgmg")
+theme_override_font_sizes/font_size = 48
+text = "Hurry Curry!"
+
+[node name="spacer" type="Control" parent="side/margin/options"]
+custom_minimum_size = Vector2(0, 10)
+layout_mode = 2
+
+[node name="first" type="VBoxContainer" parent="side/margin/options"]
+layout_mode = 2
+
+[node name="play" type="Button" parent="side/margin/options/first"]
+layout_mode = 2
+text = "c.menu.play"
+alignment = 0
+
+[node name="change_character" type="Button" parent="side/margin/options/first"]
+layout_mode = 2
+text = "c.menu.my_chef"
+alignment = 0
+
+[node name="settings" type="Button" parent="side/margin/options/first"]
+layout_mode = 2
+text = "c.menu.settings"
+alignment = 0
+
+[node name="about" type="Button" parent="side/margin/options/first"]
+layout_mode = 2
+text = "c.menu.about"
+alignment = 0
+
+[node name="quit" type="Button" parent="side/margin/options/first"]
+layout_mode = 2
+text = "c.menu.quit"
+alignment = 0
+
+[connection signal="pressed" from="side/margin/options/first/play" to="." method="_on_play_pressed"]
+[connection signal="pressed" from="side/margin/options/first/change_character" to="." method="_on_change_character_pressed"]
+[connection signal="pressed" from="side/margin/options/first/settings" to="." method="_on_settings_pressed"]
+[connection signal="pressed" from="side/margin/options/first/about" to="." method="_on_about_pressed"]
+[connection signal="pressed" from="side/margin/options/first/quit" to="." method="_on_quit_pressed"]
diff --git a/client/gui/menus/main/play.gd b/client/gui/menus/main/play.gd
new file mode 100644
index 00000000..c9cbee2f
--- /dev/null
+++ b/client/gui/menus/main/play.gd
@@ -0,0 +1,208 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+extends Menu
+
+var server_list_item: PackedScene = preload("res://gui/menus/main/server_list_item.tscn")
+var url_regex: RegEx = RegEx.new()
+
+@onready var server_list: VBoxContainer = $side/margin/options/second/ScrollContainerCustom/ServerList
+@onready var server_list_loading: Label = $side/margin/options/second/Loading
+@onready var server_list_empty: Label = $side/margin/options/second/NoServers
+@onready var connect_uri = $side/margin/options/second/connect/uri
+@onready var server = $side/margin/options/second/server
+@onready var server_control = $side/margin/options/second/server/control
+@onready var server_connect = $side/margin/options/second/server/connect
+@onready var editor_control = $side/margin/options/second/editor/control
+@onready var editor_connect = $side/margin/options/second/editor/connect
+@onready var editor_container = $side/margin/options/second/editor
+
+
+func _ready():
+ url_regex.compile("^(?:(ws|wss)://)?([^:]+)(?::([0-9]+))?$")
+ if OS.has_feature("web"):
+ server.hide()
+ connect_uri.text = Global.get_profile("last_server_url")
+ Sound.play_music("MainMenu")
+
+ ServerList.update_server_list.connect(update_server_list)
+ ServerList.update_loading.connect(update_server_list_loading)
+ update_server_list(ServerList.current_list)
+ update_server_list_loading(ServerList.loading)
+
+ super()
+ if not Global.get_profile("registry_asked"):
+ var popup_data := MenuPopup.Data.new()
+ popup_data.text = tr("c.menu.play.allow_query_registry").format([Global.get_setting("online.registry_url")])
+ var allow_button := Button.new()
+ allow_button.text = tr("c.menu.accept")
+ var deny_button := Button.new()
+ deny_button.text = tr("c.menu.deny")
+ allow_button.pressed.connect(func(): Global.set_setting("online.use_registry", true))
+ deny_button.pressed.connect(func(): Global.set_setting("online.use_registry", false))
+ popup_data.buttons = [allow_button, deny_button]
+ await submenu("res://gui/menus/popup.tscn", popup_data)
+ Global.set_profile("registry_asked", true)
+ Global.save_settings()
+ Global.save_profile()
+
+ ServerList.start()
+
+func update_server_list(lists: Array[Array]):
+ # Find out the index of the currently focused server in the list
+ var prev_selected_idx := -1
+ for i in range(server_list.get_children().size()):
+ if server_list.get_child(i).button.has_focus():
+ prev_selected_idx = i
+ break
+
+ for c in server_list.get_children():
+ c.queue_free()
+
+ var idx := 0
+ for l in lists:
+ for i in l:
+ var server_item: ServerListItem = server_list_item.instantiate()
+ server_list.add_child(server_item)
+ # TODO: Implement fallback address correctly
+ server_item.setup(i.name, roundi(i.players_online), i.version)
+ server_item.button.pressed.connect(connect_to.bind(i.address[0]))
+ # Focus the same server with the same index as the previously focused one
+ if idx == prev_selected_idx:
+ server_item.button.grab_focus()
+ idx += 1
+
+ if prev_selected_idx > idx:
+ # Same index cannot be focused, since number of servers decreased
+ if idx - 1 < 0:
+ connect_uri.grab_focus()
+ else:
+ server_list.get_child(idx - 1).button.grab_focus()
+
+ # Show message if no servers available
+ server_list_empty.visible = idx == 0
+
+func update_server_list_loading(status: bool):
+ server_list_loading.visible = status
+
+func _menu_cover(state):
+ $side.visible = not state
+
+func _on_connect_pressed():
+ var url = connect_uri.text
+ var result := url_regex.search(url)
+ if result != null:
+ if result.get_string(1) == "":
+ url = "ws://" + url
+ # only set default port for non-tls websocket connections
+ if result.get_string(3) == "" and result.get_string(1) != "wss":
+ url = url + ":27032"
+ connect_uri.text = url
+ Global.set_profile("last_server_url", url)
+ Global.save_profile()
+ connect_to(url)
+
+func _on_quick_connect_pressed():
+ if OS.has_feature("web"):
+ connect_to(JavaScriptBridge.eval("""
+ window.location.protocol.endsWith("s:")
+ ? `wss://${window.location.host}/`
+ : `ws://${window.location.hostname}:27032/`
+ """))
+ else:
+ connect_to("wss://hurrycurry.metamuffin.org/")
+
+func connect_to(url: String):
+ print("Connecting to %s" % url)
+ get_parent().replace_menu("res://gui/menus/game.tscn", url)
+
+func _on_server_control_pressed():
+ match Server.state:
+ Service.State.RUNNING: Server.stop()
+ Service.State.STOPPED: Server.start()
+ Service.State.FAILED: Server.start()
+
+func _on_editor_control_pressed():
+ match Editor.state:
+ Service.State.RUNNING: Editor.stop()
+ Service.State.STOPPED: Editor.start(); Server.start()
+ Service.State.FAILED: Editor.start()
+
+func _on_server_connect_pressed():
+ connect_to("ws://%s:%d" % [Server.connect_address(), Global.get_setting("server.bind_port")])
+
+func _on_editor_connect_pressed():
+ connect_to("ws://[::1]:27032/")
+
+func _process(_delta):
+ server_control.disabled = false
+ server_connect.visible = Server.state == Service.State.RUNNING
+ server_control.modulate = Color.WHITE
+ match Server.state:
+ Service.State.RUNNING:
+ server_control.text = tr("c.menu.play.server_stop")
+ server_control.modulate = Color.AQUAMARINE
+ Service.State.TESTING:
+ server_control.text = tr("c.menu.play.server_testing")
+ server_control.disabled = true
+ Service.State.STARTING:
+ server_control.text = tr("c.menu.play.server_starting")
+ server_control.disabled = true
+ Service.State.STOPPED:
+ server_control.text = tr("c.menu.play.server_start")
+ Service.State.FAILED:
+ server_control.text = tr("c.menu.play.server_failed")
+ server_control.modulate = Color(1, 0.4, 0.5)
+ server_control.tooltip_text = tr("c.menu.play.server_failed_tooltip")
+ Service.State.UNAVAILABLE:
+ server_control.text = tr("c.menu.play.server_unavailable")
+ server_control.disabled = true
+ server_control.tooltip_text = tr("c.menu.play.server_binary_not_found")
+
+ editor_control.disabled = false
+ editor_connect.visible = Editor.state == Service.State.RUNNING
+ editor_control.modulate = Color.WHITE
+ editor_container.visible = Editor.state != Service.State.UNAVAILABLE
+ match Editor.state:
+ Service.State.RUNNING:
+ editor_control.text = tr("c.menu.play.editor_stop")
+ editor_control.modulate = Color.AQUAMARINE
+ Service.State.TESTING:
+ editor_control.text = tr("c.menu.play.editor_testing")
+ editor_control.disabled = true
+ Service.State.STARTING:
+ editor_control.text = tr("c.menu.play.editor_starting")
+ editor_control.disabled = true
+ Service.State.STOPPED:
+ editor_control.text = tr("c.menu.play.editor_start")
+ Service.State.FAILED:
+ editor_control.text = tr("c.menu.play.editor_failed")
+ editor_control.modulate = Color(1, 0.4, 0.5)
+ editor_control.tooltip_text = tr("c.menu.play.server_failed_tooltip")
+ Service.State.UNAVAILABLE:
+ editor_control.text = tr("c.menu.play.editor_unavailable")
+ editor_control.disabled = true
+ editor_control.tooltip_text = tr("c.menu.play.server_binary_not_found")
+
+
+func _on_uri_text_changed(new_text):
+ connect_uri.modulate = Color.WHITE if url_regex.search(new_text) else Color.RED
+
+func _on_back_pressed():
+ exit()
+
+func _menu_exit():
+ ServerList.stop()
+ super()
diff --git a/client/gui/menus/main/play.gd.uid b/client/gui/menus/main/play.gd.uid
new file mode 100644
index 00000000..d8ca168f
--- /dev/null
+++ b/client/gui/menus/main/play.gd.uid
@@ -0,0 +1 @@
+uid://b126k2228nj4s
diff --git a/client/gui/menus/main/play.tscn b/client/gui/menus/main/play.tscn
new file mode 100644
index 00000000..441024a2
--- /dev/null
+++ b/client/gui/menus/main/play.tscn
@@ -0,0 +1,149 @@
+[gd_scene load_steps=9 format=3 uid="uid://c8url5fpttbem"]
+
+[ext_resource type="Theme" uid="uid://b0qmvo504e457" path="res://gui/resources/theme/theme.tres" id="1_cckds"]
+[ext_resource type="Script" uid="uid://b126k2228nj4s" path="res://gui/menus/main/play.gd" id="2_phxx0"]
+[ext_resource type="Material" uid="uid://2j8a0c0a2ta5" path="res://gui/resources/materials/blur_material.tres" id="3_fsbt7"]
+[ext_resource type="Script" uid="uid://byshs20og68tn" path="res://gui/components/smart_margin_container.gd" id="4_gst6r"]
+[ext_resource type="Script" uid="uid://bd7bylb2t2m0" path="res://gui/components/touch_scroll_container.gd" id="5_cm120"]
+[ext_resource type="FontFile" uid="uid://bo4vh5xkpvrh1" path="res://gui/resources/fonts/font-sansita-swashed.woff2" id="5_ojpbf"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ukani"]
+
+[sub_resource type="FontVariation" id="FontVariation_htgmg"]
+base_font = ExtResource("5_ojpbf")
+variation_embolden = 0.5
+
+[node name="PlayMenu" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme = ExtResource("1_cckds")
+script = ExtResource("2_phxx0")
+support_anim = false
+
+[node name="side" type="PanelContainer" parent="."]
+material = ExtResource("3_fsbt7")
+layout_mode = 1
+anchors_preset = 9
+anchor_bottom = 1.0
+offset_right = 294.0
+grow_vertical = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_ukani")
+
+[node name="margin" type="MarginContainer" parent="side"]
+layout_mode = 2
+theme_override_constants/margin_left = 20
+theme_override_constants/margin_top = 20
+theme_override_constants/margin_right = 20
+theme_override_constants/margin_bottom = 20
+script = ExtResource("4_gst6r")
+
+[node name="options" type="VBoxContainer" parent="side/margin"]
+layout_mode = 2
+
+[node name="title" type="Label" parent="side/margin/options"]
+auto_translate_mode = 2
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0.566408, 0.208917, 0.266045, 1)
+theme_override_constants/outline_size = 10
+theme_override_fonts/font = SubResource("FontVariation_htgmg")
+theme_override_font_sizes/font_size = 48
+text = "Hurry Curry!"
+
+[node name="spacer" type="Control" parent="side/margin/options"]
+custom_minimum_size = Vector2(0, 10)
+layout_mode = 2
+
+[node name="second" type="VBoxContainer" parent="side/margin/options"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="Loading" type="Label" parent="side/margin/options/second"]
+visible = false
+layout_mode = 2
+size_flags_horizontal = 3
+text = "c.menu.play.fetching_list"
+horizontal_alignment = 1
+
+[node name="NoServers" type="Label" parent="side/margin/options/second"]
+visible = false
+layout_mode = 2
+size_flags_horizontal = 3
+text = "c.menu.play.no_servers"
+horizontal_alignment = 1
+
+[node name="ScrollContainerCustom" type="ScrollContainer" parent="side/margin/options/second"]
+layout_mode = 2
+size_flags_vertical = 3
+horizontal_scroll_mode = 0
+script = ExtResource("5_cm120")
+
+[node name="ServerList" type="VBoxContainer" parent="side/margin/options/second/ScrollContainerCustom"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="spacer" type="Control" parent="side/margin/options/second"]
+custom_minimum_size = Vector2(0, 10)
+layout_mode = 2
+
+[node name="connect" type="HBoxContainer" parent="side/margin/options/second"]
+layout_mode = 2
+
+[node name="uri" type="LineEdit" parent="side/margin/options/second/connect"]
+auto_translate_mode = 2
+layout_mode = 2
+size_flags_horizontal = 3
+placeholder_text = "wss://example.org"
+
+[node name="connect" type="Button" parent="side/margin/options/second/connect"]
+layout_mode = 2
+text = "c.menu.play.connect"
+
+[node name="server" type="HBoxContainer" parent="side/margin/options/second"]
+layout_mode = 2
+
+[node name="control" type="Button" parent="side/margin/options/second/server"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "c.menu.play.server"
+alignment = 0
+
+[node name="connect" type="Button" parent="side/margin/options/second/server"]
+layout_mode = 2
+text = "c.menu.play.connect"
+
+[node name="editor" type="HBoxContainer" parent="side/margin/options/second"]
+layout_mode = 2
+
+[node name="control" type="Button" parent="side/margin/options/second/editor"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "c.menu.play.editor"
+alignment = 0
+
+[node name="connect" type="Button" parent="side/margin/options/second/editor"]
+layout_mode = 2
+text = "c.menu.play.connect"
+
+[node name="spacer2" type="Control" parent="side/margin/options/second"]
+custom_minimum_size = Vector2(0, 10)
+layout_mode = 2
+
+[node name="back" type="Button" parent="side/margin/options/second"]
+layout_mode = 2
+text = "c.menu.back"
+alignment = 0
+
+[node name="VBoxContainer" type="VBoxContainer" parent="side/margin/options/second"]
+layout_mode = 2
+
+[connection signal="text_changed" from="side/margin/options/second/connect/uri" to="." method="_on_uri_text_changed"]
+[connection signal="pressed" from="side/margin/options/second/connect/connect" to="." method="_on_connect_pressed"]
+[connection signal="pressed" from="side/margin/options/second/server/control" to="." method="_on_server_control_pressed"]
+[connection signal="pressed" from="side/margin/options/second/server/connect" to="." method="_on_server_connect_pressed"]
+[connection signal="pressed" from="side/margin/options/second/editor/control" to="." method="_on_editor_control_pressed"]
+[connection signal="pressed" from="side/margin/options/second/editor/connect" to="." method="_on_editor_connect_pressed"]
+[connection signal="pressed" from="side/margin/options/second/back" to="." method="_on_back_pressed"]
diff --git a/client/gui/menus/main/server_list_item.gd b/client/gui/menus/main/server_list_item.gd
new file mode 100644
index 00000000..0cffdd72
--- /dev/null
+++ b/client/gui/menus/main/server_list_item.gd
@@ -0,0 +1,38 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+class_name ServerListItem
+extends PanelContainer
+
+var error_style_focus: StyleBoxFlat = preload("res://gui/resources/style/error_focus_style.tres")
+@onready var title: Label = $MarginContainer/VBoxContainer/Title
+@onready var info: Label = $MarginContainer/VBoxContainer/Info
+@onready var button: Button = $Button
+
+func setup(name_: String, online_players: int, version: Array):
+ title.text = name_
+ if version[0] != Multiplayer.VERSION_MAJOR or version[1] > Multiplayer.VERSION_MINOR:
+ button.disabled = true
+ button.add_theme_stylebox_override("focus", error_style_focus)
+ info.text = tr("c.menu.play.server_version_mismatch")
+ info.add_theme_color_override("font_color", Color("ff2222"))
+ return
+ info.text = tr("c.menu.play.server_players").format([online_players])
+
+ # This node is in group not no_click_sound, so sounds won't be automatically connected
+ # by menu system. Reason: These nodes are deleted and re-created every few seconds
+ # in server list, and signals are only connected on ready.
+ button.pressed.connect(Sound.play_click)
+ button.mouse_entered.connect(Sound.play_hover)
diff --git a/client/gui/menus/main/server_list_item.gd.uid b/client/gui/menus/main/server_list_item.gd.uid
new file mode 100644
index 00000000..276bb06f
--- /dev/null
+++ b/client/gui/menus/main/server_list_item.gd.uid
@@ -0,0 +1 @@
+uid://xr5oigbgd0aw
diff --git a/client/gui/menus/main/server_list_item.tscn b/client/gui/menus/main/server_list_item.tscn
new file mode 100644
index 00000000..40e9eedb
--- /dev/null
+++ b/client/gui/menus/main/server_list_item.tscn
@@ -0,0 +1,39 @@
+[gd_scene load_steps=3 format=3 uid="uid://t2h60dhuvfsk"]
+
+[ext_resource type="Script" uid="uid://xr5oigbgd0aw" path="res://gui/menus/main/server_list_item.gd" id="1_1n1yg"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1n1yg"]
+
+[node name="ServerListItem" type="PanelContainer"]
+offset_right = 400.0
+offset_bottom = 40.0
+size_flags_horizontal = 3
+theme_override_styles/panel = SubResource("StyleBoxEmpty_1n1yg")
+script = ExtResource("1_1n1yg")
+
+[node name="Button" type="Button" parent="." groups=["no_click_sound"]]
+layout_mode = 2
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 2
+mouse_filter = 2
+theme_override_constants/margin_left = 10
+theme_override_constants/margin_top = 10
+theme_override_constants/margin_right = 10
+theme_override_constants/margin_bottom = 10
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="Title" type="Label" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.87451, 0.87451, 0.87451, 1)
+theme_override_font_sizes/font_size = 18
+text = "Example Server"
+
+[node name="Info" type="Label" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.749781, 0.74978, 0.74978, 1)
+theme_override_font_sizes/font_size = 14
+text = "5 players online"
diff --git a/client/gui/menus/menu.gd b/client/gui/menus/menu.gd
new file mode 100644
index 00000000..0f6e0624
--- /dev/null
+++ b/client/gui/menus/menu.gd
@@ -0,0 +1,151 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+class_name Menu
+extends Control
+
+
+#enum Anim { NONE, FADE }
+#@export var animation: Anim = Anim.NONE
+@export var support_anim := true
+@export var auto_anim := true
+
+var data
+
+signal submenu_close()
+
+const transition_scene = preload("res://gui/menus/transition/scene_transition.tscn")
+var transition: SceneTransition
+var parent_menu: Menu = null
+var previous_path = null # : String
+var open_since = 0
+
+func _ready():
+ open_since = Time.get_ticks_msec()
+ Global.focused_menu = self
+ focus_first(self)
+ connect_button_sounds(self)
+ disable_context_menus(self)
+ update_parent_menu(self.get_parent())
+ if support_anim: anim_setup()
+ if auto_anim: _menu_open()
+ get_tree().get_root().go_back_requested.connect(exit_maybe)
+
+func anim_setup():
+ transition = transition_scene.instantiate()
+ add_child(transition)
+func _menu_open():
+ if transition != null: await transition.fade_in()
+func _menu_exit():
+ if transition != null: await transition.fade_out()
+func _menu_cover(_state: bool):
+ pass
+
+var popup: Menu = null
+var covered := false
+func submenu(path: String, data_ = null):
+ var prev_focus = Global.focused_node
+ if popup != null: return
+ _disable_recursive(self, true)
+ covered = true
+ await _menu_cover(true)
+ popup = load(path).instantiate()
+ popup.data = data_
+ add_child(popup)
+ print("Submenu opened ", path)
+ await submenu_close
+ print("Submenu closed ", path)
+ await _menu_cover(false)
+ covered = false
+ Global.focused_menu = self
+ _disable_recursive(self, false)
+ if prev_focus != null: prev_focus.grab_focus()
+
+func _disable_recursive(node: Node, state: bool):
+ if node is BaseButton:
+ if state and node.disabled: node.add_to_group("was_disabled")
+ else: node.remove_from_group("was_disabled")
+ node.disabled = state or node.is_in_group("was_disabled")
+ for c in node.get_children(): _disable_recursive(c, state)
+
+func exit():
+ await self._menu_exit()
+ if previous_path != null:
+ replace_menu(previous_path)
+ else:
+ get_parent().submenu_close.emit()
+ queue_free()
+
+func quit():
+ await exit()
+ get_parent().quit()
+
+func replace_menu(path: String, data_ = null, prev_path = null): # prev_path: String?
+ print("Replace menu: ", path)
+ if popup != null: await popup.exit()
+ _disable_recursive(self, true)
+ await _menu_exit()
+ var new_popup: Menu = load(path).instantiate()
+ new_popup.data = data_
+ if prev_path != null: new_popup.previous_path = prev_path
+ get_parent().add_child(new_popup)
+ if parent_menu != null: parent_menu.popup = new_popup
+ queue_free()
+
+var focus_auto_changed := false
+func focus_first(node: Node) -> bool:
+ focus_auto_changed = true
+ if node.is_in_group("no_auto_focus"):
+ return false
+ if node is Button or node.is_in_group("autoselect"):
+ node.grab_focus()
+ print("Node %s (%s) was selected for focus" % [node.name, node])
+ return true
+ for c in node.get_children():
+ if focus_first(c):
+ return true
+ return false
+
+func connect_button_sounds(node: Node):
+ if node is Button or node is TextureButton:
+ if not node.is_in_group("no_click_sound"):
+ node.pressed.connect(Sound.play_click)
+ if (node is Button and not node.disabled) or (node is LineEdit and node.editable) or node is Slider:
+ if not node.is_in_group("no_click_sound"):
+ node.mouse_entered.connect(Sound.play_hover)
+ for c in node.get_children():
+ connect_button_sounds(c)
+
+func disable_context_menus(node: Node):
+ if node is LineEdit:
+ node.context_menu_enabled = false
+ for c in node.get_children():
+ disable_context_menus(c)
+
+func update_parent_menu(node: Node):
+ if node is Menu: parent_menu = node
+ elif node.get_parent() != null: update_parent_menu(node.get_parent())
+
+func _input(_event):
+ if Input.is_action_just_pressed("menu"):
+ exit_maybe()
+
+func exit_maybe() -> void:
+ # Exit menu if all conditions are met
+ if popup != null: return
+ var time := Time.get_ticks_msec()
+ if time - open_since < 100: return
+ Sound.play_click()
+ exit()
diff --git a/client/gui/menus/menu.gd.uid b/client/gui/menus/menu.gd.uid
new file mode 100644
index 00000000..5711a6e0
--- /dev/null
+++ b/client/gui/menus/menu.gd.uid
@@ -0,0 +1 @@
+uid://d2h2q16vykpl4
diff --git a/client/gui/menus/popup.gd b/client/gui/menus/popup.gd
new file mode 100644
index 00000000..d4849e92
--- /dev/null
+++ b/client/gui/menus/popup.gd
@@ -0,0 +1,32 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+extends Menu
+class_name MenuPopup
+
+@onready var message := $MarginContainer/CenterContainer/Panel/MarginContainer/VBoxContainer/Message
+@onready var button_container := $MarginContainer/CenterContainer/Panel/MarginContainer/VBoxContainer/HBoxContainer
+
+class Data:
+ var buttons: Array[Button]
+ var text: String
+
+func _ready():
+ var setup: Data = self.data
+ for i in setup.buttons:
+ button_container.add_child(i)
+ i.pressed.connect(exit)
+ message.text = setup.text
+ super()
diff --git a/client/gui/menus/popup.gd.uid b/client/gui/menus/popup.gd.uid
new file mode 100644
index 00000000..4800ca4d
--- /dev/null
+++ b/client/gui/menus/popup.gd.uid
@@ -0,0 +1 @@
+uid://bevyiytj5tawr
diff --git a/client/gui/menus/popup.tscn b/client/gui/menus/popup.tscn
new file mode 100644
index 00000000..13137ccb
--- /dev/null
+++ b/client/gui/menus/popup.tscn
@@ -0,0 +1,52 @@
+[gd_scene load_steps=6 format=3 uid="uid://lwtym0pbc17g"]
+
+[ext_resource type="Theme" uid="uid://b0qmvo504e457" path="res://gui/resources/theme/theme.tres" id="1_m0d0r"]
+[ext_resource type="Script" uid="uid://bevyiytj5tawr" path="res://gui/menus/popup.gd" id="2_1h10j"]
+[ext_resource type="Material" uid="uid://beea1pc5nt67r" path="res://gui/resources/materials/dark_blur_material.tres" id="3_iouvy"]
+[ext_resource type="Script" uid="uid://byshs20og68tn" path="res://gui/components/smart_margin_container.gd" id="3_j0ajn"]
+[ext_resource type="Script" uid="uid://cmncjc06kadpe" path="res://gui/components/blur_setup.gd" id="4_e4iqk"]
+
+[node name="Popup" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme = ExtResource("1_m0d0r")
+script = ExtResource("2_1h10j")
+support_anim = false
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("3_j0ajn")
+
+[node name="CenterContainer" type="CenterContainer" parent="MarginContainer"]
+layout_mode = 2
+
+[node name="Panel" type="PanelContainer" parent="MarginContainer/CenterContainer"]
+material = ExtResource("3_iouvy")
+layout_mode = 2
+script = ExtResource("4_e4iqk")
+
+[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/CenterContainer/Panel"]
+layout_mode = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/CenterContainer/Panel/MarginContainer"]
+layout_mode = 2
+theme_override_constants/separation = 16
+
+[node name="Message" type="Label" parent="MarginContainer/CenterContainer/Panel/MarginContainer/VBoxContainer"]
+custom_minimum_size = Vector2(400, 0)
+layout_mode = 2
+horizontal_alignment = 1
+autowrap_mode = 3
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/CenterContainer/Panel/MarginContainer/VBoxContainer"]
+layout_mode = 2
+alignment = 1
diff --git a/client/gui/menus/popup_large.gd b/client/gui/menus/popup_large.gd
new file mode 100644
index 00000000..909ee4c0
--- /dev/null
+++ b/client/gui/menus/popup_large.gd
@@ -0,0 +1,25 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+extends Menu
+
+@onready var label = $OuterMargin/Panel/InnerMargin/Vert/ScrollContainer/CreditsText
+
+func _ready():
+ super()
+ label.text = data
+
+func _on_back_pressed():
+ exit()
diff --git a/client/gui/menus/popup_large.gd.uid b/client/gui/menus/popup_large.gd.uid
new file mode 100644
index 00000000..aef2a852
--- /dev/null
+++ b/client/gui/menus/popup_large.gd.uid
@@ -0,0 +1 @@
+uid://c3eimx76ucpsp
diff --git a/client/gui/menus/popup_large.tscn b/client/gui/menus/popup_large.tscn
new file mode 100644
index 00000000..993dde9d
--- /dev/null
+++ b/client/gui/menus/popup_large.tscn
@@ -0,0 +1,74 @@
+[gd_scene load_steps=7 format=3 uid="uid://7mqbxa054bjv"]
+
+[ext_resource type="Theme" uid="uid://b0qmvo504e457" path="res://gui/resources/theme/theme.tres" id="1_kabr3"]
+[ext_resource type="Script" uid="uid://c3eimx76ucpsp" path="res://gui/menus/popup_large.gd" id="2_m0b5d"]
+[ext_resource type="Script" uid="uid://byshs20og68tn" path="res://gui/components/smart_margin_container.gd" id="3_36vhf"]
+[ext_resource type="Material" uid="uid://beea1pc5nt67r" path="res://gui/resources/materials/dark_blur_material.tres" id="4_8ybj3"]
+[ext_resource type="Script" uid="uid://cmncjc06kadpe" path="res://gui/components/blur_setup.gd" id="5_63jf0"]
+[ext_resource type="Script" uid="uid://bd7bylb2t2m0" path="res://gui/components/touch_scroll_container.gd" id="6_smk7v"]
+
+[node name="CreditsMenu" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme = ExtResource("1_kabr3")
+script = ExtResource("2_m0b5d")
+support_anim = false
+
+[node name="OuterMargin" type="MarginContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("3_36vhf")
+
+[node name="Panel" type="Panel" parent="OuterMargin"]
+material = ExtResource("4_8ybj3")
+layout_mode = 2
+script = ExtResource("5_63jf0")
+
+[node name="InnerMargin" type="MarginContainer" parent="OuterMargin/Panel"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 50
+theme_override_constants/margin_top = 50
+theme_override_constants/margin_right = 50
+theme_override_constants/margin_bottom = 50
+
+[node name="Vert" type="VBoxContainer" parent="OuterMargin/Panel/InnerMargin"]
+layout_mode = 2
+
+[node name="ScrollContainer" type="ScrollContainer" parent="OuterMargin/Panel/InnerMargin/Vert"]
+layout_mode = 2
+size_flags_vertical = 3
+script = ExtResource("6_smk7v")
+
+[node name="CreditsText" type="RichTextLabel" parent="OuterMargin/Panel/InnerMargin/Vert/ScrollContainer"]
+auto_translate_mode = 2
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/table_h_separation = 25
+theme_override_font_sizes/bold_italics_font_size = 22
+theme_override_font_sizes/italics_font_size = 22
+theme_override_font_sizes/mono_font_size = 22
+theme_override_font_sizes/normal_font_size = 22
+theme_override_font_sizes/bold_font_size = 22
+bbcode_enabled = true
+fit_content = true
+scroll_active = false
+
+[node name="back" type="Button" parent="OuterMargin/Panel/InnerMargin/Vert"]
+layout_mode = 2
+text = "c.menu.back"
+
+[connection signal="pressed" from="OuterMargin/Panel/InnerMargin/Vert/back" to="." method="_on_back_pressed"]
diff --git a/client/gui/menus/rating/desaturate.gdshader b/client/gui/menus/rating/desaturate.gdshader
new file mode 100644
index 00000000..e6861560
--- /dev/null
+++ b/client/gui/menus/rating/desaturate.gdshader
@@ -0,0 +1,7 @@
+shader_type canvas_item;
+
+uniform float t : hint_range(0.0, 1.0);
+
+void fragment() {
+ COLOR.rgb = mix(vec3(pow((COLOR.r+COLOR.g+COLOR.b)/3.,3.)),COLOR.rgb,t);
+}
diff --git a/client/gui/menus/rating/desaturate.gdshader.uid b/client/gui/menus/rating/desaturate.gdshader.uid
new file mode 100644
index 00000000..621837a6
--- /dev/null
+++ b/client/gui/menus/rating/desaturate.gdshader.uid
@@ -0,0 +1 @@
+uid://cekkkqsvd7rvw
diff --git a/client/gui/menus/rating/rating.gd b/client/gui/menus/rating/rating.gd
new file mode 100644
index 00000000..023c1333
--- /dev/null
+++ b/client/gui/menus/rating/rating.gd
@@ -0,0 +1,65 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+extends Menu
+
+const PARTICLE_AMOUNTS = [1, 6, 32, 128]
+
+@onready var game: Game = $"../Game"
+@onready var title: Label = $MarginContainer/PanelContainer/VBoxContainer/Text/Title
+@onready var subtitle: Label = $MarginContainer/PanelContainer/VBoxContainer/Text/Subtitle
+@onready var stars = $MarginContainer/PanelContainer/VBoxContainer/Stars.get_children()
+@onready var star_timer = $StarTimer
+@onready var particles = $Control/Particles
+@onready var close_button: Button = $MarginContainer/PanelContainer/VBoxContainer/HBoxContainer/Close
+
+func _ready():
+ super()
+ show_rating(data[0], data[1])
+ close_button.disabled = true # Disable for short time period to prevent accidental button press
+
+func _process(_delta):
+ particles.emission_rect_extents = get_viewport_rect().size * Vector2(0.5, 0.5)
+
+func show_rating(stars_: int, points: int):
+ match stars_:
+ 0: title.text = tr("c.score.poor")
+ 1: title.text = tr("c.score.acceptable")
+ 2: title.text = tr("c.score.good")
+ 3: title.text = tr("c.score.excellent")
+
+ subtitle.text = tr("c.score.points_par").format([points])
+
+ for i in range(0, stars_):
+ var star: TextureRect = stars[i]
+ star_timer.start()
+ await star_timer.timeout
+ star.material.set_shader_parameter("t", 1)
+ star.get_node("Sound").play()
+
+ particles.amount = PARTICLE_AMOUNTS[stars_]
+
+ if stars_ > 1:
+ particles.emitting = true
+
+func _on_close_pressed():
+ exit()
+
+func _on_button_timer_timeout() -> void:
+ close_button.disabled = false
+
+func _on_scoreboard_pressed() -> void:
+ exit()
+ game.mp.send_chat(game.my_player_id, "/scoreboard %s" % Global.last_map_name)
diff --git a/client/gui/menus/rating/rating.gd.uid b/client/gui/menus/rating/rating.gd.uid
new file mode 100644
index 00000000..fd729d8f
--- /dev/null
+++ b/client/gui/menus/rating/rating.gd.uid
@@ -0,0 +1 @@
+uid://5tmklxkaa6e0
diff --git a/client/gui/menus/rating/rating.tscn b/client/gui/menus/rating/rating.tscn
new file mode 100644
index 00000000..062dcca2
--- /dev/null
+++ b/client/gui/menus/rating/rating.tscn
@@ -0,0 +1,168 @@
+[gd_scene load_steps=12 format=3 uid="uid://buu3cdpigs8qq"]
+
+[ext_resource type="Texture2D" uid="uid://b10goh4dsa3b0" path="res://player/particles/satisfied/star.webp" id="1_7qv7r"]
+[ext_resource type="Shader" uid="uid://cekkkqsvd7rvw" path="res://gui/menus/rating/desaturate.gdshader" id="1_pddsm"]
+[ext_resource type="Theme" uid="uid://b0qmvo504e457" path="res://gui/resources/theme/theme.tres" id="1_uwajf"]
+[ext_resource type="Script" uid="uid://5tmklxkaa6e0" path="res://gui/menus/rating/rating.gd" id="2_cq0se"]
+[ext_resource type="Material" uid="uid://beea1pc5nt67r" path="res://gui/resources/materials/dark_blur_material.tres" id="4_hdurb"]
+[ext_resource type="AudioStream" uid="uid://camy77x26mmpv" path="res://gui/resources/sounds/success.ogg" id="5_tutpj"]
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_oi7xd"]
+shader = ExtResource("1_pddsm")
+shader_parameter/t = 0.0
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_ney6s"]
+shader = ExtResource("1_pddsm")
+shader_parameter/t = 0.0
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_27tx1"]
+shader = ExtResource("1_pddsm")
+shader_parameter/t = 0.0
+
+[sub_resource type="Curve" id="Curve_dqga7"]
+_data = [Vector2(0, 0), 0.0, 0.0, 0, 0, Vector2(0.0954774, 1), 0.262418, 0.0, 0, 0]
+point_count = 2
+
+[sub_resource type="Gradient" id="Gradient_majwe"]
+offsets = PackedFloat32Array(0, 0.0584795, 1)
+colors = PackedColorArray(1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0)
+
+[node name="Rating" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme = ExtResource("1_uwajf")
+script = ExtResource("2_cq0se")
+support_anim = false
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 128
+theme_override_constants/margin_top = 64
+theme_override_constants/margin_right = 128
+theme_override_constants/margin_bottom = 64
+
+[node name="PanelContainer" type="PanelContainer" parent="MarginContainer"]
+material = ExtResource("4_hdurb")
+layout_mode = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/PanelContainer"]
+layout_mode = 2
+theme_override_constants/separation = 64
+alignment = 1
+
+[node name="Text" type="VBoxContainer" parent="MarginContainer/PanelContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="Title" type="Label" parent="MarginContainer/PanelContainer/VBoxContainer/Text"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 48
+text = "Title here"
+horizontal_alignment = 1
+
+[node name="Subtitle" type="Label" parent="MarginContainer/PanelContainer/VBoxContainer/Text"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 24
+text = "Subtitle here"
+horizontal_alignment = 1
+
+[node name="Stars" type="HBoxContainer" parent="MarginContainer/PanelContainer/VBoxContainer"]
+layout_mode = 2
+alignment = 1
+
+[node name="Star1" type="TextureRect" parent="MarginContainer/PanelContainer/VBoxContainer/Stars"]
+material = SubResource("ShaderMaterial_oi7xd")
+custom_minimum_size = Vector2(128, 128)
+layout_mode = 2
+texture = ExtResource("1_7qv7r")
+expand_mode = 1
+stretch_mode = 5
+
+[node name="Sound" type="AudioStreamPlayer" parent="MarginContainer/PanelContainer/VBoxContainer/Stars/Star1"]
+stream = ExtResource("5_tutpj")
+pitch_scale = 1.5
+
+[node name="Star2" type="TextureRect" parent="MarginContainer/PanelContainer/VBoxContainer/Stars"]
+material = SubResource("ShaderMaterial_ney6s")
+custom_minimum_size = Vector2(128, 128)
+layout_mode = 2
+texture = ExtResource("1_7qv7r")
+expand_mode = 1
+stretch_mode = 5
+
+[node name="Sound" type="AudioStreamPlayer" parent="MarginContainer/PanelContainer/VBoxContainer/Stars/Star2"]
+stream = ExtResource("5_tutpj")
+pitch_scale = 1.65
+
+[node name="Star3" type="TextureRect" parent="MarginContainer/PanelContainer/VBoxContainer/Stars"]
+material = SubResource("ShaderMaterial_27tx1")
+custom_minimum_size = Vector2(128, 128)
+layout_mode = 2
+texture = ExtResource("1_7qv7r")
+expand_mode = 1
+stretch_mode = 5
+
+[node name="Sound" type="AudioStreamPlayer" parent="MarginContainer/PanelContainer/VBoxContainer/Stars/Star3"]
+stream = ExtResource("5_tutpj")
+pitch_scale = 1.9
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/PanelContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_constants/separation = 64
+alignment = 1
+
+[node name="Scoreboard" type="Button" parent="MarginContainer/PanelContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+text = "c.menu.scoreboard.button"
+
+[node name="Close" type="Button" parent="MarginContainer/PanelContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+text = "c.menu.accept"
+
+[node name="StarTimer" type="Timer" parent="."]
+wait_time = 0.5
+one_shot = true
+
+[node name="ButtonTimer" type="Timer" parent="."]
+one_shot = true
+autostart = true
+
+[node name="Control" type="Control" parent="."]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="Particles" type="CPUParticles2D" parent="Control"]
+emitting = false
+amount = 32
+texture = ExtResource("1_7qv7r")
+emission_shape = 3
+emission_rect_extents = Vector2(512, 256)
+direction = Vector2(0, -1)
+initial_velocity_min = 256.0
+initial_velocity_max = 256.0
+angular_velocity_min = -30.0
+angular_velocity_max = 30.0
+angle_min = -20.0
+angle_max = 20.0
+scale_amount_min = 0.1
+scale_amount_max = 0.2
+scale_amount_curve = SubResource("Curve_dqga7")
+color_ramp = SubResource("Gradient_majwe")
+
+[connection signal="pressed" from="MarginContainer/PanelContainer/VBoxContainer/HBoxContainer/Scoreboard" to="." method="_on_scoreboard_pressed"]
+[connection signal="pressed" from="MarginContainer/PanelContainer/VBoxContainer/HBoxContainer/Close" to="." method="_on_close_pressed"]
+[connection signal="timeout" from="ButtonTimer" to="." method="_on_button_timer_timeout"]
diff --git a/client/gui/menus/settings/button_setting.gd b/client/gui/menus/settings/button_setting.gd
new file mode 100644
index 00000000..fff8c184
--- /dev/null
+++ b/client/gui/menus/settings/button_setting.gd
@@ -0,0 +1,30 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+class_name ButtonSetting
+extends GameSetting
+
+var callback
+
+func _init(new_id: String, new_default, callback_):
+ callback = callback_
+ super (new_id, new_default)
+
+func create_row():
+ var row = super ()
+ row.value_node = Button.new()
+ row.value_node.text = tr(nskey + ".button_label")
+ row.value_node.pressed.connect(callback)
+ return row
diff --git a/client/gui/menus/settings/button_setting.gd.uid b/client/gui/menus/settings/button_setting.gd.uid
new file mode 100644
index 00000000..cf0a8d95
--- /dev/null
+++ b/client/gui/menus/settings/button_setting.gd.uid
@@ -0,0 +1 @@
+uid://dku75bw31ux1k
diff --git a/client/gui/menus/settings/dropdown_setting.gd b/client/gui/menus/settings/dropdown_setting.gd
new file mode 100644
index 00000000..514df666
--- /dev/null
+++ b/client/gui/menus/settings/dropdown_setting.gd
@@ -0,0 +1,36 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+class_name DropdownSetting
+extends GameSetting
+
+var options: Array
+
+func _init(new_id: String, new_default, new_options: Array):
+ super(new_id, new_default)
+ options = new_options
+
+func create_row():
+ var row = super()
+ row.value_node = OptionButton.new()
+ row.value_node.clip_text = true
+ for i in options: row.value_node.add_item(tr(nskey + "." + i))
+ Settings.hook_changed_init(key, true,
+ func(value):
+ if is_instance_valid(row):
+ row.value_node.select(options.find(value))
+ )
+ row.value_node.item_selected.connect(func(item): Global.set_setting(key, options[item]))
+ return row
diff --git a/client/gui/menus/settings/dropdown_setting.gd.uid b/client/gui/menus/settings/dropdown_setting.gd.uid
new file mode 100644
index 00000000..409bf3ab
--- /dev/null
+++ b/client/gui/menus/settings/dropdown_setting.gd.uid
@@ -0,0 +1 @@
+uid://cjqswo4mwbvon
diff --git a/client/gui/menus/settings/game_setting.gd b/client/gui/menus/settings/game_setting.gd
new file mode 100644
index 00000000..1c04ad3b
--- /dev/null
+++ b/client/gui/menus/settings/game_setting.gd
@@ -0,0 +1,46 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+class_name GameSetting
+extends Object
+
+var default
+var key: String
+var nskey: String
+
+func _init(new_id: String, new_default = null):
+ default = new_default
+ key = new_id
+
+func set_parent(parent: GameSetting):
+ if parent != null: key = parent.key + "." + key
+ nskey = "c.settings." + key
+
+func create_row():
+ var row = preload("res://gui/menus/settings/settings_row.tscn").instantiate()
+ row.description = tr(nskey)
+ row.reset.connect(func(): Global.set_setting(key, default))
+ return row
+
+func check():
+ if default != null:
+ if not key in Global.settings:
+ Global.set_setting_unchecked(key, default)
+ if typeof(default) != typeof(Global.settings[key]):
+ Global.set_setting_unchecked(key, default)
+
+func changed_keys():
+ if Global.get_setting(key) != default: return [key]
+ else: return []
diff --git a/client/gui/menus/settings/game_setting.gd.uid b/client/gui/menus/settings/game_setting.gd.uid
new file mode 100644
index 00000000..99d79bee
--- /dev/null
+++ b/client/gui/menus/settings/game_setting.gd.uid
@@ -0,0 +1 @@
+uid://cyy8l32i44l63
diff --git a/client/gui/menus/settings/input/input_manager.gd b/client/gui/menus/settings/input/input_manager.gd
new file mode 100644
index 00000000..e3158a03
--- /dev/null
+++ b/client/gui/menus/settings/input/input_manager.gd
@@ -0,0 +1,101 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+extends Node
+
+enum EventType {
+ KEYBOARD,
+ JOYPAD,
+ TOUCH,
+ OTHER
+}
+
+var default_input_map = {}
+var input_map: Dictionary
+
+func _init():
+ default_input_map = get_input_map()
+ input_map = default_input_map.duplicate(true)
+
+func get_input_map() -> Dictionary:
+ var actions = InputMap.get_actions().filter(func isBuiltIn(k: String): return !k.begins_with("ui_"))
+ var kb = {}
+ for a in actions:
+ var input_events: Array[InputEvent] = InputMap.action_get_events(a).duplicate(true)
+ kb[a] = input_events
+ return kb
+
+func get_events(action_name: String) -> Array:
+ if not input_map.has(action_name):
+ push_error("Tried to get action %s in input map which does not exist" % action_name)
+ return []
+ return input_map[action_name]
+
+func settings() -> Array:
+ var entries := []
+ for k in input_map.keys(): entries.append(InputSetting.new(k))
+ return entries
+
+func change_input_map_action(action_name: String, events: Array, save: bool = true):
+ if !InputMap.has_action(action_name):
+ push_error("Action %s does not exist" % action_name, false)
+ return
+ # Erase previous keybindings
+ InputMap.action_erase_events(action_name)
+ # Add new keybindings
+ for e in events:
+ InputMap.action_add_event(action_name, e)
+
+ if save:
+ # Update input map dictionary
+ input_map = get_input_map()
+ # Save settings
+ Global.set_setting("input_map", input_map.duplicate(true))
+
+func apply_input_map(new_input_map: Dictionary):
+ # Load into input map dictionary
+ for k in new_input_map.keys():
+ input_map[k] = []
+ for a in new_input_map[k]:
+ input_map[k].append(a)
+
+ # Apply keybindings
+ for k in input_map.keys():
+ change_input_map_action(k, input_map[k], false)
+
+func reset_input_map():
+ Global.set_setting("input_map", default_input_map.duplicate())
+ apply_input_map(Global.get_setting("input_map"))
+
+func get_event_type(input_event: InputEvent) -> EventType:
+ if input_event is InputEventKey or input_event is InputEventMouseButton:
+ return EventType.KEYBOARD
+ elif input_event is InputEventJoypadButton or input_event is InputEventJoypadMotion:
+ return EventType.JOYPAD
+ elif input_event is InputEventScreenTouch or input_event is InputEventScreenDrag:
+ return EventType.TOUCH
+ return EventType.OTHER
+
+func display_input_event(input_event: InputEvent) -> String:
+ if input_event is InputEventKey:
+ return tr("c.settings.input.keyboard").format([OS.get_keycode_string(input_event.physical_keycode)])
+ elif input_event is InputEventMouseButton:
+ return tr("c.settings.input.mouse_button").format([input_event.button_index])
+ elif input_event is InputEventJoypadButton:
+ return tr("c.settings.input.joypad").format([input_event.button_index])
+ elif input_event is InputEventJoypadMotion:
+ return tr("c.settings.input.joypad_axis").format([input_event.axis])
+ else:
+ return tr("c.settings.input.other_event")
diff --git a/client/gui/menus/settings/input/input_manager.gd.uid b/client/gui/menus/settings/input/input_manager.gd.uid
new file mode 100644
index 00000000..678c2192
--- /dev/null
+++ b/client/gui/menus/settings/input/input_manager.gd.uid
@@ -0,0 +1 @@
+uid://bfu78iwybbu2s
diff --git a/client/gui/menus/settings/input/input_setting.gd b/client/gui/menus/settings/input/input_setting.gd
new file mode 100644
index 00000000..fa903771
--- /dev/null
+++ b/client/gui/menus/settings/input/input_setting.gd
@@ -0,0 +1,39 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+class_name InputSetting
+extends GameSetting
+
+const INPUT_VALUE_NODE_SCENE = preload("res://gui/menus/settings/input/input_value_node.tscn")
+
+func _init(new_id: String):
+ super(new_id)
+ default = InputManager.default_input_map[new_id]
+
+func create_row():
+ var row = super()
+ row.value_node = INPUT_VALUE_NODE_SCENE.instantiate()
+ Settings.hook_changed_init(key, true,
+ func(value):
+ if is_instance_valid(row):
+ row.value_node.value = value
+ )
+ row.value_node.changed.connect(func(): Global.set_setting(key, row.value_node.value))
+ return row
+
+func changed_keys():
+ return [key]
+ # if Global.array_eq(Global.get_setting(key), default): return [key]
+ # else: return []
diff --git a/client/gui/menus/settings/input/input_setting.gd.uid b/client/gui/menus/settings/input/input_setting.gd.uid
new file mode 100644
index 00000000..7866fc2f
--- /dev/null
+++ b/client/gui/menus/settings/input/input_setting.gd.uid
@@ -0,0 +1 @@
+uid://d2xwn2u4ycpe8
diff --git a/client/gui/menus/settings/input/input_value_node.gd b/client/gui/menus/settings/input/input_value_node.gd
new file mode 100644
index 00000000..7c718e25
--- /dev/null
+++ b/client/gui/menus/settings/input/input_value_node.gd
@@ -0,0 +1,74 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+extends VBoxContainer
+class_name InputValueNode
+
+var value: Array[InputEvent] = []
+var listening := false
+
+signal changed()
+
+@onready var actions_container: VBoxContainer = $ActionsContainer
+@onready var add_button: Button = $Add
+@onready var add_text = add_button.text
+
+func _ready():
+ update()
+
+func update(fix_focus: bool = false):
+ for c in actions_container.get_children():
+ c.queue_free()
+
+ for e: InputEvent in value:
+ var description: String = InputManager.display_input_event(e)
+ var button := Button.new()
+
+ button.text = description
+ button.pressed.connect(erase_event.bind(e))
+ actions_container.add_child(button)
+
+ if fix_focus:
+ add_button.grab_focus()
+
+func erase_event(e: InputEvent):
+ value.erase(e)
+ update(true)
+ changed.emit()
+
+func _input(e: InputEvent):
+ if listening:
+ if e is InputEventKey or e is InputEventMouseButton or e is InputEventJoypadButton or e is InputEventJoypadMotion:
+ # Check if key was already added
+ for e2 in value:
+ if events_equal(e, e2): return
+
+ value.append(e)
+ _on_add_pressed()
+ update()
+ changed.emit()
+
+func events_equal(e1: InputEvent, e2: InputEvent) -> bool:
+ if e1 is InputEventKey and e2 is InputEventKey:
+ return e1.physical_keycode == e2.physical_keycode
+ if (e1 is InputEventMouseButton and e2 is InputEventMouseButton) or (e1 is InputEventJoypadButton and e2 is InputEventJoypadButton):
+ return e1.button_index == e2.button_index
+ if e1 is InputEventJoypadMotion and e2 is InputEventJoypadMotion:
+ return e1.axis == e2.axis
+ return false
+
+func _on_add_pressed() -> void:
+ listening = not listening
+ add_button.text = tr("c.settings.input.press_any_key") if listening else add_text
diff --git a/client/gui/menus/settings/input/input_value_node.gd.uid b/client/gui/menus/settings/input/input_value_node.gd.uid
new file mode 100644
index 00000000..3669b991
--- /dev/null
+++ b/client/gui/menus/settings/input/input_value_node.gd.uid
@@ -0,0 +1 @@
+uid://ckb78voiq05e3
diff --git a/client/gui/menus/settings/input/input_value_node.tscn b/client/gui/menus/settings/input/input_value_node.tscn
new file mode 100644
index 00000000..1b2e89c4
--- /dev/null
+++ b/client/gui/menus/settings/input/input_value_node.tscn
@@ -0,0 +1,24 @@
+[gd_scene load_steps=3 format=3 uid="uid://c6r0nv5daq7wc"]
+
+[ext_resource type="Script" uid="uid://ckb78voiq05e3" path="res://gui/menus/settings/input/input_value_node.gd" id="1_snxax"]
+[ext_resource type="Texture2D" uid="uid://cnfjbowd2i02r" path="res://gui/resources/icons/plus.svg" id="2_3vlvc"]
+
+[node name="InputValueNode" type="VBoxContainer"]
+offset_right = 128.0
+offset_bottom = 31.0
+theme_override_constants/separation = 0
+script = ExtResource("1_snxax")
+
+[node name="ActionsContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[node name="Add" type="Button" parent="."]
+custom_minimum_size = Vector2(128, 0)
+layout_mode = 2
+size_flags_vertical = 3
+text = "c.settings.input.add"
+icon = ExtResource("2_3vlvc")
+expand_icon = true
+
+[connection signal="pressed" from="Add" to="." method="_on_add_pressed"]
diff --git a/client/gui/menus/settings/number_setting.gd b/client/gui/menus/settings/number_setting.gd
new file mode 100644
index 00000000..5fa5a115
--- /dev/null
+++ b/client/gui/menus/settings/number_setting.gd
@@ -0,0 +1,41 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+class_name NumberSetting
+extends GameSetting
+
+var placeholder: String
+var min_value: int
+var max_value: int
+
+func _init(new_id: String, new_default: int, new_min_value: int, new_max_value: int):
+ super(new_id, new_default)
+ min_value = new_min_value
+ max_value = new_max_value
+
+func create_row():
+ var row = super()
+ var input := SpinBox.new()
+ input.min_value = min_value
+ input.max_value = max_value
+
+ input.value_changed.connect(func(value): Global.set_setting(key, value as int))
+ Settings.hook_changed_init(key, true,
+ func(v):
+ if is_instance_valid(input):
+ input.value = v
+ )
+ row.value_node = input
+ return row
diff --git a/client/gui/menus/settings/number_setting.gd.uid b/client/gui/menus/settings/number_setting.gd.uid
new file mode 100644
index 00000000..4301c642
--- /dev/null
+++ b/client/gui/menus/settings/number_setting.gd.uid
@@ -0,0 +1 @@
+uid://babmw2ohuhmuk
diff --git a/client/gui/menus/settings/path_setting.gd b/client/gui/menus/settings/path_setting.gd
new file mode 100644
index 00000000..37492ed7
--- /dev/null
+++ b/client/gui/menus/settings/path_setting.gd
@@ -0,0 +1,64 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+class_name PathSetting
+extends TextSetting
+
+var select_file_icon: Texture2D = preload("res://gui/resources/icons/select_file.svg")
+var select_dir_icon: Texture2D = preload("res://gui/resources/icons/select_directory.svg")
+
+var access: FileDialog.Access
+var file_mode: FileDialog.FileMode
+
+func _init(new_id: String,
+ new_default: String,
+ new_file_mode: FileDialog.FileMode,
+ new_placeholder: String = "",
+ new_access: FileDialog.Access = FileDialog.Access.ACCESS_FILESYSTEM
+):
+ super(new_id, new_default)
+ placeholder = new_placeholder
+ access = new_access
+ file_mode = new_file_mode
+
+func create_row():
+ var row = super ()
+ var input: LineEdit = row.value_node;
+ input.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ row.value_node = HBoxContainer.new()
+ row.value_node.add_child(input)
+ var button := Button.new()
+ button.icon = select_file_icon if file_mode == FileDialog.FileMode.FILE_MODE_OPEN_FILE else select_dir_icon
+ row.value_node.add_child(button)
+ button.pressed.connect(func():
+ var d := FileDialog.new()
+ Global.focused_menu.add_child(d)
+ d.move_to_center()
+ d.use_native_dialog = true
+ d.borderless = true
+ d.dir_selected.connect(_selected.bind(input))
+ d.file_selected.connect(_selected.bind(input))
+ d.file_mode = file_mode
+ d.access = access
+ d.show()
+ # this feels wrong
+ d.canceled.connect(d.queue_free)
+ d.confirmed.connect(d.queue_free)
+ )
+ return row
+
+func _selected(path: String, input: LineEdit):
+ input.text = path
+ input.text_changed.emit(path)
diff --git a/client/gui/menus/settings/path_setting.gd.uid b/client/gui/menus/settings/path_setting.gd.uid
new file mode 100644
index 00000000..a524b17c
--- /dev/null
+++ b/client/gui/menus/settings/path_setting.gd.uid
@@ -0,0 +1 @@
+uid://cj5jf7t771j5b
diff --git a/client/gui/menus/settings/preset_row.gd b/client/gui/menus/settings/preset_row.gd
new file mode 100644
index 00000000..f3c46a26
--- /dev/null
+++ b/client/gui/menus/settings/preset_row.gd
@@ -0,0 +1,46 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+class_name PresetRow
+extends GameSetting
+
+var options: Dictionary
+var arr: Array[Button]
+
+func _init(id: String, options_: Dictionary):
+ super(id)
+ options = options_
+
+var prefix = ""
+func set_parent(parent):
+ super(parent)
+ if parent != null: prefix = parent.key
+
+func apply(preset_name: String):
+ var preset = options[preset_name]
+ for i in preset.keys():
+ Global.set_setting(prefix + "." + i, preset[i])
+
+func create_row():
+ var row = super()
+ row.value_node = HBoxContainer.new()
+ for i in options.keys():
+ var button := Button.new()
+ button.pressed.connect(apply.bind(i))
+ button.text = tr(nskey + "." + i)
+ row.value_node.add_child(button)
+ return row
+
+func changed_keys(): return []
diff --git a/client/gui/menus/settings/preset_row.gd.uid b/client/gui/menus/settings/preset_row.gd.uid
new file mode 100644
index 00000000..51605058
--- /dev/null
+++ b/client/gui/menus/settings/preset_row.gd.uid
@@ -0,0 +1 @@
+uid://dawqyyllgis0b
diff --git a/client/gui/menus/settings/range_setting.gd b/client/gui/menus/settings/range_setting.gd
new file mode 100644
index 00000000..b8d392a4
--- /dev/null
+++ b/client/gui/menus/settings/range_setting.gd
@@ -0,0 +1,44 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+class_name RangeSetting
+extends GameSetting
+
+var min_value: float
+var max_value: float
+var tick_count
+var smooth: bool
+
+func _init(new_id: String, new_default: float, new_min_value: float, new_max_value: float, new_smooth: bool = true, new_tick_count = null):
+ super(new_id, new_default)
+ min_value = new_min_value
+ max_value = new_max_value
+ tick_count = new_tick_count
+ smooth = new_smooth
+
+func create_row():
+ var row = super()
+ row.value_node = HSlider.new()
+ row.value_node.min_value = min_value
+ row.value_node.max_value = max_value
+ row.value_node.tick_count = abs(max_value - min_value) if tick_count == null else tick_count
+ row.value_node.step = 0 if smooth else (1 if tick_count == null else abs(max_value - min_value) / (tick_count - 1))
+ Settings.hook_changed_init(key, true,
+ func(value):
+ if is_instance_valid(row):
+ row.value_node.value = value
+ )
+ row.value_node.value_changed.connect(func(value): Global.set_setting(key, value))
+ return row
diff --git a/client/gui/menus/settings/range_setting.gd.uid b/client/gui/menus/settings/range_setting.gd.uid
new file mode 100644
index 00000000..a4ca49a2
--- /dev/null
+++ b/client/gui/menus/settings/range_setting.gd.uid
@@ -0,0 +1 @@
+uid://civr7cckqfndj
diff --git a/client/gui/menus/settings/settings.gd b/client/gui/menus/settings/settings.gd
new file mode 100644
index 00000000..32da54cc
--- /dev/null
+++ b/client/gui/menus/settings/settings.gd
@@ -0,0 +1,40 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+extends Menu
+
+@onready var container = $OuterGap/Panel/InnerGap/VBoxContainer
+@onready var outer_gap = $OuterGap
+
+func _ready():
+ super()
+ var row = Global.settings_tree.create_row()
+ container.add_child(row)
+ container.move_child(row, 1)
+
+func _process(_dt):
+ var os := OS.get_name()
+ if os == "iOS" or os == "Android": return
+ # TODO probably bad performance, only update on change
+ var margin = max((self.size.x - 1200) / 2, 20)
+ outer_gap.add_theme_constant_override("margin_left", margin)
+ outer_gap.add_theme_constant_override("margin_right", margin)
+
+func _on_back_pressed():
+ exit()
+
+func exit():
+ Global.save_settings()
+ super()
diff --git a/client/gui/menus/settings/settings.gd.uid b/client/gui/menus/settings/settings.gd.uid
new file mode 100644
index 00000000..79b89b85
--- /dev/null
+++ b/client/gui/menus/settings/settings.gd.uid
@@ -0,0 +1 @@
+uid://bbqmsf8u5rhtn
diff --git a/client/gui/menus/settings/settings.tscn b/client/gui/menus/settings/settings.tscn
new file mode 100644
index 00000000..71549464
--- /dev/null
+++ b/client/gui/menus/settings/settings.tscn
@@ -0,0 +1,61 @@
+[gd_scene load_steps=6 format=3 uid="uid://8ic77jmadadj"]
+
+[ext_resource type="Theme" uid="uid://b0qmvo504e457" path="res://gui/resources/theme/theme.tres" id="1_1vjiw"]
+[ext_resource type="Script" uid="uid://bbqmsf8u5rhtn" path="res://gui/menus/settings/settings.gd" id="2_5xn7x"]
+[ext_resource type="Script" uid="uid://byshs20og68tn" path="res://gui/components/smart_margin_container.gd" id="3_h533i"]
+[ext_resource type="Material" uid="uid://beea1pc5nt67r" path="res://gui/resources/materials/dark_blur_material.tres" id="4_b0x33"]
+[ext_resource type="Script" uid="uid://cmncjc06kadpe" path="res://gui/components/blur_setup.gd" id="5_dvivs"]
+
+[node name="SettingsMenu" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme = ExtResource("1_1vjiw")
+script = ExtResource("2_5xn7x")
+support_anim = false
+
+[node name="OuterGap" type="MarginContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 80
+script = ExtResource("3_h533i")
+
+[node name="Panel" type="Panel" parent="OuterGap"]
+material = ExtResource("4_b0x33")
+layout_mode = 2
+script = ExtResource("5_dvivs")
+
+[node name="InnerGap" type="MarginContainer" parent="OuterGap/Panel"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 40
+theme_override_constants/margin_top = 40
+theme_override_constants/margin_right = 40
+theme_override_constants/margin_bottom = 40
+
+[node name="VBoxContainer" type="VBoxContainer" parent="OuterGap/Panel/InnerGap"]
+layout_mode = 2
+
+[node name="Title" type="Label" parent="OuterGap/Panel/InnerGap/VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 0
+theme_override_font_sizes/font_size = 36
+text = "c.menu.settings"
+
+[node name="Back" type="Button" parent="OuterGap/Panel/InnerGap/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 8
+text = "c.settings.apply"
+
+[connection signal="pressed" from="OuterGap/Panel/InnerGap/VBoxContainer/Back" to="." method="_on_back_pressed"]
diff --git a/client/gui/menus/settings/settings_category.gd b/client/gui/menus/settings/settings_category.gd
new file mode 100644
index 00000000..bf85abd9
--- /dev/null
+++ b/client/gui/menus/settings/settings_category.gd
@@ -0,0 +1,49 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+class_name SettingsCategory
+extends GameSetting
+
+var settings: Array # Dictionary[String, GameSetting]
+
+func _init(new_id: String, new_settings: Array):
+ super(new_id)
+ settings = new_settings
+
+func set_parent(parent: GameSetting):
+ super(parent)
+ for c in settings:
+ c.set_parent(self)
+
+func create_row():
+ var row = ScrollContainerCustom.new()
+ var options = VBoxContainer.new()
+ row.name = tr(nskey)
+ row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ options.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ row.add_child(options)
+
+ for r in settings: options.add_child(r.create_row())
+ return row
+
+func check():
+ for c in settings:
+ c.check()
+
+func changed_keys():
+ var changed = []
+ for c in settings:
+ changed.append_array(c.changed_keys())
+ return changed
diff --git a/client/gui/menus/settings/settings_category.gd.uid b/client/gui/menus/settings/settings_category.gd.uid
new file mode 100644
index 00000000..421ce213
--- /dev/null
+++ b/client/gui/menus/settings/settings_category.gd.uid
@@ -0,0 +1 @@
+uid://b8s3cqb01w3wh
diff --git a/client/gui/menus/settings/settings_root.gd b/client/gui/menus/settings/settings_root.gd
new file mode 100644
index 00000000..a9a024d8
--- /dev/null
+++ b/client/gui/menus/settings/settings_root.gd
@@ -0,0 +1,40 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+extends GameSetting
+class_name SettingsRoot
+
+var children: Array
+func _init(new_children: Array):
+ super("root")
+ children = new_children
+ for c in children:
+ c.set_parent(null)
+
+func create_row():
+ var row = TabContainer.new()
+ row.size_flags_vertical = Control.SIZE_EXPAND_FILL
+ for r in children: row.add_child(r.create_row())
+ return row
+
+func check():
+ for c in children:
+ c.check()
+
+func changed_keys():
+ var changed = []
+ for c in children:
+ changed.append_array(c.changed_keys())
+ return changed
diff --git a/client/gui/menus/settings/settings_root.gd.uid b/client/gui/menus/settings/settings_root.gd.uid
new file mode 100644
index 00000000..95a46d5e
--- /dev/null
+++ b/client/gui/menus/settings/settings_root.gd.uid
@@ -0,0 +1 @@
+uid://jonib2ixqsp7
diff --git a/client/gui/menus/settings/settings_row.gd b/client/gui/menus/settings/settings_row.gd
new file mode 100644
index 00000000..d88d49c1
--- /dev/null
+++ b/client/gui/menus/settings/settings_row.gd
@@ -0,0 +1,37 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+class_name SettingsRow
+extends PanelContainer
+
+signal reset()
+
+@onready var value_parent = $HBoxContainer/BoxContainer
+@onready var label = $HBoxContainer/Label
+@onready var reset_button = $HBoxContainer/Reset
+
+var value_node: Node
+var description = "No value was given to the row"
+
+func _ready():
+ if value_node != null:
+ var c: Control = value_node
+ c.size_flags_vertical = Control.SIZE_EXPAND_FILL
+ c.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ label.text = description
+ value_parent.add_child(c)
+
+func _on_reset_pressed():
+ reset.emit()
diff --git a/client/gui/menus/settings/settings_row.gd.uid b/client/gui/menus/settings/settings_row.gd.uid
new file mode 100644
index 00000000..a6dea492
--- /dev/null
+++ b/client/gui/menus/settings/settings_row.gd.uid
@@ -0,0 +1 @@
+uid://b3m1f76o5qo68
diff --git a/client/gui/menus/settings/settings_row.tscn b/client/gui/menus/settings/settings_row.tscn
new file mode 100644
index 00000000..09378ab6
--- /dev/null
+++ b/client/gui/menus/settings/settings_row.tscn
@@ -0,0 +1,40 @@
+[gd_scene load_steps=7 format=3 uid="uid://o5e5vpem8w0k"]
+
+[ext_resource type="Theme" uid="uid://b0qmvo504e457" path="res://gui/resources/theme/theme.tres" id="1_iij3k"]
+[ext_resource type="Script" uid="uid://b3m1f76o5qo68" path="res://gui/menus/settings/settings_row.gd" id="2_l8i7p"]
+[ext_resource type="FontFile" uid="uid://5ixo6b3bd3km" path="res://gui/resources/fonts/font-josefin-sans.woff2" id="3_7k5da"]
+[ext_resource type="Texture2D" uid="uid://cucnmy0j5n8l8" path="res://gui/resources/icons/reset.svg" id="4_bj3dr"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_pk3rs"]
+content_margin_left = 16.0
+
+[sub_resource type="FontVariation" id="FontVariation_o6i7s"]
+base_font = ExtResource("3_7k5da")
+
+[node name="SettingsRow" type="PanelContainer"]
+offset_right = 105.0
+offset_bottom = 23.0
+size_flags_horizontal = 3
+theme = ExtResource("1_iij3k")
+script = ExtResource("2_l8i7p")
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_styles/normal = SubResource("StyleBoxEmpty_pk3rs")
+
+[node name="BoxContainer" type="BoxContainer" parent="HBoxContainer"]
+custom_minimum_size = Vector2(300, 50)
+layout_mode = 2
+alignment = 2
+
+[node name="Reset" type="Button" parent="HBoxContainer"]
+layout_mode = 2
+theme_override_fonts/font = SubResource("FontVariation_o6i7s")
+theme_override_font_sizes/font_size = 24
+icon = ExtResource("4_bj3dr")
+
+[connection signal="pressed" from="HBoxContainer/Reset" to="." method="_on_reset_pressed"]
diff --git a/client/gui/menus/settings/text_setting.gd b/client/gui/menus/settings/text_setting.gd
new file mode 100644
index 00000000..8e2b6bec
--- /dev/null
+++ b/client/gui/menus/settings/text_setting.gd
@@ -0,0 +1,38 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+class_name TextSetting
+extends GameSetting
+
+var placeholder: String
+
+func _init(new_id: String, new_default: String, new_placeholder: String = ""):
+ super(new_id, new_default)
+ placeholder = new_placeholder
+
+func create_row():
+ var row = super()
+ var input := LineEdit.new()
+ input.placeholder_text = placeholder
+ input.text_changed.connect(func(text): Global.set_setting(key, text))
+ Settings.hook_changed_init(key, true,
+ func(text):
+ if is_instance_valid(input):
+ var pos = input.caret_column
+ input.text = text
+ input.caret_column = pos
+ )
+ row.value_node = input
+ return row
diff --git a/client/gui/menus/settings/text_setting.gd.uid b/client/gui/menus/settings/text_setting.gd.uid
new file mode 100644
index 00000000..58ac5abe
--- /dev/null
+++ b/client/gui/menus/settings/text_setting.gd.uid
@@ -0,0 +1 @@
+uid://3rgucgbbt135
diff --git a/client/gui/menus/settings/toggle_setting.gd b/client/gui/menus/settings/toggle_setting.gd
new file mode 100644
index 00000000..abcb7f4a
--- /dev/null
+++ b/client/gui/menus/settings/toggle_setting.gd
@@ -0,0 +1,31 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+class_name ToggleSetting
+extends GameSetting
+
+func _init(new_id: String, new_default: bool):
+ super(new_id, new_default)
+
+func create_row():
+ var row = super()
+ row.value_node = CheckButton.new()
+ row.value_node.pressed.connect(func(): Global.set_setting(key, row.value_node.button_pressed))
+ Settings.hook_changed_init(key, true,
+ func(value):
+ if is_instance_valid(row):
+ row.value_node.button_pressed = value
+ )
+ return row
diff --git a/client/gui/menus/settings/toggle_setting.gd.uid b/client/gui/menus/settings/toggle_setting.gd.uid
new file mode 100644
index 00000000..1d2ca55b
--- /dev/null
+++ b/client/gui/menus/settings/toggle_setting.gd.uid
@@ -0,0 +1 @@
+uid://cojnv8bmv6aw5
diff --git a/client/gui/menus/setup/hairstyle_preview.gd b/client/gui/menus/setup/hairstyle_preview.gd
new file mode 100644
index 00000000..78576491
--- /dev/null
+++ b/client/gui/menus/setup/hairstyle_preview.gd
@@ -0,0 +1,27 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+class_name HairstylePreview
+extends VBoxContainer
+
+signal selected(character_style: Dictionary)
+
+func setup(hairstyle: int, group: ButtonGroup):
+ var character_style = Global.default_profile.character_style
+ character_style.hairstyle = hairstyle
+ $HairViewport/Node3D/Character.set_style(character_style, "chef")
+ $Select.button_group = group
+ $Select.text = tr("c.setup.uniform.value").format([hairstyle + 1])
+ $Select.pressed.connect(func(): selected.emit(character_style))
diff --git a/client/gui/menus/setup/hairstyle_preview.gd.uid b/client/gui/menus/setup/hairstyle_preview.gd.uid
new file mode 100644
index 00000000..8f5b3cd4
--- /dev/null
+++ b/client/gui/menus/setup/hairstyle_preview.gd.uid
@@ -0,0 +1 @@
+uid://dvveoqur81l0s
diff --git a/client/gui/menus/setup/hairstyle_preview.tscn b/client/gui/menus/setup/hairstyle_preview.tscn
new file mode 100644
index 00000000..ee4a65e9
--- /dev/null
+++ b/client/gui/menus/setup/hairstyle_preview.tscn
@@ -0,0 +1,55 @@
+[gd_scene load_steps=7 format=3 uid="uid://dfon56nwd2tgn"]
+
+[ext_resource type="Script" uid="uid://dvveoqur81l0s" path="res://gui/menus/setup/hairstyle_preview.gd" id="1_0qdmv"]
+[ext_resource type="Shader" uid="uid://qjrh2imc53u1" path="res://gui/resources/shaders/grayscale.gdshader" id="1_sf0gc"]
+[ext_resource type="PackedScene" uid="uid://b3hhir2fvnunu" path="res://player/character/character.tscn" id="2_jtitc"]
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_entrs"]
+shader = ExtResource("1_sf0gc")
+
+[sub_resource type="ViewportTexture" id="ViewportTexture_giuq2"]
+viewport_path = NodePath("HairViewport")
+
+[sub_resource type="ButtonGroup" id="ButtonGroup_c5p7t"]
+
+[node name="HairstylePreview" type="VBoxContainer"]
+offset_right = 40.0
+offset_bottom = 40.0
+script = ExtResource("1_0qdmv")
+
+[node name="Preview" type="TextureRect" parent="."]
+material = SubResource("ShaderMaterial_entrs")
+layout_mode = 2
+texture = SubResource("ViewportTexture_giuq2")
+
+[node name="Select" type="CheckBox" parent="."]
+layout_mode = 2
+button_group = SubResource("ButtonGroup_c5p7t")
+text = "Hairstyle 1"
+
+[node name="HairViewport" type="SubViewport" parent="."]
+own_world_3d = true
+transparent_bg = true
+msaa_3d = 1
+size = Vector2i(128, 128)
+
+[node name="Node3D" type="Node3D" parent="HairViewport"]
+
+[node name="Camera3D" type="Camera3D" parent="HairViewport/Node3D"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.75, 1.5)
+fov = 25.5
+
+[node name="Character" parent="HairViewport/Node3D" instance=ExtResource("2_jtitc")]
+
+[node name="SpotLight3D" type="SpotLight3D" parent="HairViewport/Node3D"]
+transform = Transform3D(0.866025, 0, -0.5, 0, 1, 0, 0.5, 0, 0.866025, -2, 0, 2)
+
+[node name="SpotLight3D2" type="SpotLight3D" parent="HairViewport/Node3D"]
+transform = Transform3D(0.876399, 0, 0.481585, 0, 1, 0, -0.481585, 0, 0.876399, 2, 0.499189, 2)
+light_color = Color(0.857819, 0.80038, 0.775519, 1)
+light_energy = 4.11
+
+[node name="SpotLight3D3" type="SpotLight3D" parent="HairViewport/Node3D"]
+transform = Transform3D(-0.965926, 0, -0.258819, -0.129409, 0.866025, 0.482963, 0.224144, 0.5, -0.836516, -1, 2, -2)
+light_color = Color(0.540595, 0.865144, 1, 1)
+light_energy = 8.2
diff --git a/client/gui/menus/setup/setup.gd b/client/gui/menus/setup/setup.gd
new file mode 100644
index 00000000..6170786c
--- /dev/null
+++ b/client/gui/menus/setup/setup.gd
@@ -0,0 +1,110 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+extends Menu
+
+const SCROLL_SPEED := 500.
+
+var character_style = null # : Dictionary?
+
+@onready var anim: AnimationPlayer = $AnimationPlayer
+@onready var username: LineEdit = $ScrollContainer/Control/TextureRect/PaperMargin/Contents/NameEntry/LineEdit
+@onready var sign_sound: AudioStreamPlayer = $Sign
+@onready var sign_button: Button = $ScrollContainer/Control/TextureRect/PaperMargin/Contents/Signatures/EmployeeMargin/Sign/Signature
+@onready var scroll: ScrollContainer = $ScrollContainer
+@onready var select_uniform: HBoxContainer = $ScrollContainer/Control/TextureRect/PaperMargin/Contents/SelectUniform
+@onready var skip_tutorial: CheckBox = $ScrollContainer/Control/TextureRect/PaperMargin/Contents/SelectExperience/CheckBox
+
+func _ready():
+ anim.play("paper_slide")
+ var button_group := ButtonGroup.new()
+ for i in range(3):
+ var preview: HairstylePreview = preload("res://gui/menus/setup/hairstyle_preview.tscn").instantiate()
+ select_uniform.add_child(preview)
+ preview.setup(i, button_group)
+ preview.selected.connect(_character_selected)
+ if i < 2:
+ var spacer = Control.new()
+ spacer.size_flags_vertical = Control.SIZE_EXPAND
+ spacer.custom_minimum_size.x = 50
+ select_uniform.add_child(spacer)
+
+ # Enable msaa 2D for this scene
+ Global.get_viewport().msaa_2d = Viewport.MSAA_4X
+
+ if Global.profile.username != "": username.text = Global.profile.username
+
+ if TranslationServer.get_locale().begins_with("zh"):
+ $ScrollContainer/Control/TextureRect.rotation = 0
+ increase_font_size(self)
+
+ super()
+ $Back.visible = not is_instance_of(parent_menu, Entry)
+
+func increase_font_size(node: Node):
+ if node is RichTextLabel:
+ for oname in ["bold_italics_font_size", "italics_font_size", "normal_font_size", "mono_font_size", "bold_font_size"]:
+ node.add_theme_font_size_override(oname, node.get_theme_default_font_size() * 1.2)
+ for c in node.get_children(): increase_font_size(c)
+
+func _on_back_pressed() -> void:
+ exit()
+
+func _character_selected(style: Dictionary):
+ character_style = style
+
+func _process(delta):
+ var s = Input.get_axis("rotate_up", "rotate_down")
+ scroll.set_deferred("scroll_vertical", scroll.scroll_vertical + s * delta * SCROLL_SPEED)
+
+
+func check():
+ if username.text == "": return tr("c.error.empty_username")
+ if character_style == null: return tr("c.error.select_hairstyle")
+ return null
+
+func _on_sign_pressed():
+ if check() != null:
+ var popup_data := MenuPopup.Data.new()
+ popup_data.text = check()
+ var accept_button := Button.new()
+ accept_button.text = tr("c.menu.accept")
+ popup_data.buttons = [accept_button]
+ await submenu("res://gui/menus/popup.tscn", popup_data)
+ return
+
+ sign_button.disabled = true
+
+ sign_sound.play()
+ await sign_sound.finished
+ anim.play_backwards("paper_slide")
+ await anim.animation_finished
+
+ Global.set_profile("username", username.text)
+ Global.set_profile("character_style", character_style)
+ if skip_tutorial.button_pressed:
+ for k in Global.profile["hints"].keys():
+ Global.set_hint(k, true)
+ Global.save_profile()
+
+ Global.set_setting("gameplay.hints_started", skip_tutorial.button_pressed)
+ Global.set_setting("gameplay.tutorial_disabled", skip_tutorial.button_pressed)
+ Global.set_setting("gameplay.setup_completed", true)
+ Global.save_settings()
+
+ Global.get_viewport().msaa_2d = Viewport.MSAA_DISABLED
+
+ if not is_instance_of(parent_menu, Entry): exit()
+ else: replace_menu("res://gui/menu/main/main.tscn")
diff --git a/client/gui/menus/setup/setup.gd.uid b/client/gui/menus/setup/setup.gd.uid
new file mode 100644
index 00000000..c9c2be3c
--- /dev/null
+++ b/client/gui/menus/setup/setup.gd.uid
@@ -0,0 +1 @@
+uid://dxn6ow6hiwhbf
diff --git a/client/gui/menus/setup/setup.tscn b/client/gui/menus/setup/setup.tscn
new file mode 100644
index 00000000..d0cce350
--- /dev/null
+++ b/client/gui/menus/setup/setup.tscn
@@ -0,0 +1,398 @@
+[gd_scene load_steps=15 format=3 uid="uid://ddl3efikvqp66"]
+
+[ext_resource type="Script" uid="uid://dxn6ow6hiwhbf" path="res://gui/menus/setup/setup.gd" id="1_mo46n"]
+[ext_resource type="Theme" uid="uid://ci2qajdoa1an1" path="res://gui/resources/theme/paper.tres" id="1_yq0aa"]
+[ext_resource type="Script" uid="uid://bd7bylb2t2m0" path="res://gui/components/touch_scroll_container.gd" id="2_4caf2"]
+[ext_resource type="FontFile" uid="uid://bo4vh5xkpvrh1" path="res://gui/resources/fonts/font-sansita-swashed.woff2" id="3_2vg4d"]
+[ext_resource type="AudioStream" uid="uid://do7ii5hx71p0m" path="res://gui/resources/sounds/page.ogg" id="5_xac6d"]
+[ext_resource type="AudioStream" uid="uid://5b3noxjmasmu" path="res://gui/resources/sounds/sign.ogg" id="6_wf0gh"]
+
+[sub_resource type="Animation" id="Animation_m4a1a"]
+length = 0.001
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("ScrollContainer:position")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Vector2(0, 0)]
+}
+
+[sub_resource type="Animation" id="Animation_s1to2"]
+resource_name = "paper_slide"
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("ScrollContainer:position")
+tracks/0/interp = 2
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0, 1),
+"transitions": PackedFloat32Array(1, 1),
+"update": 0,
+"values": [Vector2(0, -1800), Vector2(0, 0)]
+}
+
+[sub_resource type="AnimationLibrary" id="AnimationLibrary_wjgak"]
+_data = {
+&"RESET": SubResource("Animation_m4a1a"),
+&"paper_slide": SubResource("Animation_s1to2")
+}
+
+[sub_resource type="Gradient" id="Gradient_nsc3h"]
+colors = PackedColorArray(0.941084, 0.949219, 0.918643, 1, 1, 1, 1, 1)
+
+[sub_resource type="FastNoiseLite" id="FastNoiseLite_amioi"]
+
+[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_bvvl7"]
+color_ramp = SubResource("Gradient_nsc3h")
+noise = SubResource("FastNoiseLite_amioi")
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e7xn5"]
+bg_color = Color(0.196078, 0.196078, 0.235294, 1)
+corner_radius_top_left = 10
+corner_radius_top_right = 10
+corner_radius_bottom_right = 10
+corner_radius_bottom_left = 10
+
+[sub_resource type="FontVariation" id="FontVariation_2cc7p"]
+base_font = ExtResource("3_2vg4d")
+
+[node name="SetupMenu" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_mo46n")
+support_anim = null
+auto_anim = null
+
+[node name="ColorRect" type="ColorRect" parent="."]
+layout_mode = 2
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+color = Color(0.196078, 0.196078, 0.235294, 1)
+
+[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
+libraries = {
+&"": SubResource("AnimationLibrary_wjgak")
+}
+speed_scale = 2.0
+
+[node name="ScrollContainer" type="ScrollContainer" parent="."]
+clip_contents = false
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+follow_focus = true
+horizontal_scroll_mode = 0
+script = ExtResource("2_4caf2")
+
+[node name="Control" type="Control" parent="ScrollContainer"]
+custom_minimum_size = Vector2(0, 1500)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="TextureRect" type="TextureRect" parent="ScrollContainer/Control"]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -400.0
+offset_top = -559.57
+offset_right = 400.0
+offset_bottom = 571.801
+grow_horizontal = 2
+grow_vertical = 2
+rotation = 0.0174533
+theme = ExtResource("1_yq0aa")
+texture = SubResource("NoiseTexture2D_bvvl7")
+
+[node name="Hole1" type="Panel" parent="ScrollContainer/Control/TextureRect"]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -365.0
+offset_top = -189.686
+offset_right = -345.0
+offset_bottom = -169.686
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_e7xn5")
+
+[node name="Hole2" type="Panel" parent="ScrollContainer/Control/TextureRect"]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -365.0
+offset_top = 130.314
+offset_right = -345.0
+offset_bottom = 150.314
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_e7xn5")
+
+[node name="PaperMargin" type="MarginContainer" parent="ScrollContainer/Control/TextureRect"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="Contents" type="VBoxContainer" parent="ScrollContainer/Control/TextureRect/PaperMargin"]
+layout_mode = 2
+
+[node name="Title" type="Label" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents"]
+layout_mode = 2
+size_flags_horizontal = 4
+theme_override_font_sizes/font_size = 30
+text = "c.setup.contract_title"
+
+[node name="Sep" type="HSeparator" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents"]
+layout_mode = 2
+
+[node name="Intro" type="RichTextLabel" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents"]
+layout_mode = 2
+bbcode_enabled = true
+text = "c.setup.contract_desc"
+fit_content = true
+scroll_active = false
+
+[node name="Name" type="RichTextLabel" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents"]
+layout_mode = 2
+bbcode_enabled = true
+text = "c.setup.name"
+fit_content = true
+scroll_active = false
+text_direction = 3
+
+[node name="NameEntry" type="HBoxContainer" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents"]
+layout_mode = 2
+tooltip_text = "c.setup.name.desc"
+
+[node name="LineEdit" type="LineEdit" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/NameEntry"]
+custom_minimum_size = Vector2(300, 30)
+layout_mode = 2
+max_length = 32
+
+[node name="Control" type="Control" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/NameEntry"]
+layout_mode = 2
+
+[node name="Position" type="RichTextLabel" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents"]
+layout_mode = 2
+bbcode_enabled = true
+text = "c.setup.position"
+fit_content = true
+scroll_active = false
+text_direction = 3
+
+[node name="PositionEntry" type="HBoxContainer" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents"]
+layout_mode = 2
+
+[node name="LineEdit" type="LineEdit" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/PositionEntry"]
+custom_minimum_size = Vector2(300, 30)
+layout_mode = 2
+theme_override_colors/font_uneditable_color = Color(0.458824, 0, 0, 1)
+theme_override_colors/font_color = Color(0.458824, 0, 0, 1)
+editable = false
+
+[node name="LineEdit2" type="Label" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/PositionEntry/LineEdit"]
+custom_minimum_size = Vector2(300, 30)
+layout_mode = 1
+offset_left = 9.97753
+offset_top = 3.2088
+offset_right = 309.978
+offset_bottom = 33.2088
+theme_override_colors/font_color = Color(0.458824, 0, 0, 1)
+theme_override_fonts/font = ExtResource("3_2vg4d")
+text = "c.setup.position.value"
+
+[node name="Uniform" type="RichTextLabel" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents"]
+layout_mode = 2
+bbcode_enabled = true
+text = "c.setup.uniform"
+fit_content = true
+scroll_active = false
+text_direction = 3
+
+[node name="SelectUniform" type="HBoxContainer" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents"]
+layout_mode = 2
+alignment = 1
+
+[node name="Experience" type="RichTextLabel" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents"]
+layout_mode = 2
+bbcode_enabled = true
+text = "c.setup.experience"
+fit_content = true
+scroll_active = false
+text_direction = 3
+
+[node name="SelectExperience" type="HBoxContainer" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents"]
+layout_mode = 2
+alignment = 1
+
+[node name="CheckBox" type="CheckBox" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/SelectExperience"]
+layout_mode = 2
+text = "c.setup.experience.skip"
+text_direction = 3
+
+[node name="Duties" type="RichTextLabel" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents"]
+layout_mode = 2
+bbcode_enabled = true
+text = "c.setup.duties"
+fit_content = true
+scroll_active = false
+text_direction = 3
+
+[node name="Terms" type="RichTextLabel" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents"]
+layout_mode = 2
+bbcode_enabled = true
+text = "c.setup.additional_terms"
+fit_content = true
+scroll_active = false
+text_direction = 3
+
+[node name="Compensation" type="RichTextLabel" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents"]
+layout_mode = 2
+bbcode_enabled = true
+text = "c.setup.compensation"
+fit_content = true
+scroll_active = false
+text_direction = 3
+
+[node name="CompensationEntry" type="HBoxContainer" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents"]
+layout_mode = 2
+
+[node name="Spacer" type="Control" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/CompensationEntry"]
+custom_minimum_size = Vector2(15.045, 0)
+layout_mode = 2
+
+[node name="Text1" type="RichTextLabel" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/CompensationEntry"]
+custom_minimum_size = Vector2(100.08, 0)
+layout_mode = 2
+bbcode_enabled = true
+text = "c.setup.compensation.salary.prefix"
+fit_content = true
+scroll_active = false
+autowrap_mode = 0
+
+[node name="LineEdit" type="LineEdit" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/CompensationEntry"]
+custom_minimum_size = Vector2(50, 30)
+layout_mode = 2
+theme_override_colors/font_uneditable_color = Color(0.478431, 0, 0, 1)
+editable = false
+
+[node name="LineEdit2" type="Label" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/CompensationEntry/LineEdit"]
+custom_minimum_size = Vector2(50, 30)
+layout_mode = 1
+offset_left = 9.55965
+offset_top = 4.09178
+offset_right = 79.5597
+offset_bottom = 34.0918
+theme_override_colors/font_color = Color(0.478431, 0, 0, 1)
+theme_override_fonts/font = ExtResource("3_2vg4d")
+text = "c.setup.compensation.salary"
+
+[node name="Text2" type="RichTextLabel" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/CompensationEntry"]
+custom_minimum_size = Vector2(100.08, 0)
+layout_mode = 2
+bbcode_enabled = true
+text = "c.setup.compensation.salary.suffix"
+scroll_active = false
+
+[node name="Spacer" type="Control" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents"]
+custom_minimum_size = Vector2(0, 200)
+layout_mode = 2
+
+[node name="Signatures" type="HBoxContainer" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents"]
+layout_mode = 2
+
+[node name="EmployerMargin" type="MarginContainer" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/Signatures"]
+layout_mode = 2
+
+[node name="Sign" type="VBoxContainer" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/Signatures/EmployerMargin"]
+layout_mode = 2
+
+[node name="Desc" type="RichTextLabel" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/Signatures/EmployerMargin/Sign"]
+layout_mode = 2
+theme_override_font_sizes/normal_font_size = 15
+bbcode_enabled = true
+text = "c.setup.frank_signature.desc"
+fit_content = true
+scroll_active = false
+text_direction = 3
+
+[node name="Signature" type="Label" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/Signatures/EmployerMargin/Sign"]
+custom_minimum_size = Vector2(200, 80)
+layout_mode = 2
+theme_override_colors/font_color = Color(0.415686, 0.0253044, 0.135441, 1)
+theme_override_fonts/font = SubResource("FontVariation_2cc7p")
+theme_override_font_sizes/font_size = 31
+text = "c.setup.frank_signature"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="Underline" type="HSeparator" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/Signatures/EmployerMargin/Sign"]
+layout_mode = 2
+
+[node name="EmployeeMargin" type="MarginContainer" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/Signatures"]
+layout_mode = 2
+
+[node name="Sign" type="VBoxContainer" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/Signatures/EmployeeMargin"]
+layout_mode = 2
+
+[node name="Desc" type="RichTextLabel" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/Signatures/EmployeeMargin/Sign"]
+layout_mode = 2
+theme_override_font_sizes/normal_font_size = 15
+bbcode_enabled = true
+text = "c.setup.user_signature.desc"
+fit_content = true
+scroll_active = false
+text_direction = 3
+
+[node name="Signature" type="Button" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/Signatures/EmployeeMargin/Sign" groups=["no_click_sound"]]
+custom_minimum_size = Vector2(200, 80)
+layout_mode = 2
+text = "c.setup.user_signature"
+
+[node name="Underline" type="HSeparator" parent="ScrollContainer/Control/TextureRect/PaperMargin/Contents/Signatures/EmployeeMargin/Sign"]
+layout_mode = 2
+
+[node name="Back" type="Button" parent="."]
+layout_mode = 1
+offset_right = 106.0
+offset_bottom = 31.0
+text = "c.menu.back"
+
+[node name="Page" type="AudioStreamPlayer" parent="."]
+stream = ExtResource("5_xac6d")
+volume_db = -16.0
+autoplay = true
+
+[node name="Sign" type="AudioStreamPlayer" parent="."]
+stream = ExtResource("6_wf0gh")
+volume_db = -16.0
+
+[connection signal="pressed" from="ScrollContainer/Control/TextureRect/PaperMargin/Contents/Signatures/EmployeeMargin/Sign/Signature" to="." method="_on_sign_pressed"]
+[connection signal="pressed" from="Back" to="." method="_on_back_pressed"]
diff --git a/client/gui/menus/transition/scene_transition.gd b/client/gui/menus/transition/scene_transition.gd
new file mode 100644
index 00000000..330d67d6
--- /dev/null
+++ b/client/gui/menus/transition/scene_transition.gd
@@ -0,0 +1,66 @@
+# Hurry Curry! - a game about cooking
+# Copyright (C) 2025 Hurry Curry! contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, version 3 of the License only.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+class_name SceneTransition
+extends Control
+
+@onready var black_anim: AnimationPlayer = $black_fader
+@onready var text_anim: AnimationPlayer = $text_fader
+@onready var text: Label = $text_margin/text
+
+var s_current = false
+var s_target = false
+var fading = false
+
+func _ready():
+ $black.visible = true
+ text.visible = true
+ text.text = ""
+
+func set_loading_text(s: String):
+ text.text = s
+ text_anim.play("fade")
+
+func next():
+ while fading: await black_anim.animation_finished
+ if s_target == s_current: return
+ fading = true
+ if s_target:
+ if text.text != "":
+ text_anim.play_backwards("fade")
+ await text_anim.animation_finished
+ black_anim.play_backwards("fade")
+ await black_anim.animation_finished
+ self.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ set_loading_text("")
+ s_current = true
+ else:
+ self.mouse_filter = Control.MOUSE_FILTER_STOP
+ black_anim.play("fade")
+ await black_anim.animation_finished
+ await get_tree().process_frame # animation finishes one frame early
+ s_current = false
+ fading = false
+ await next()
+
+func fade_in():
+ s_target = true
+ await next()
+func fade_out():
+ s_target = false
+ await next()
+
+func _exit_tree():
+ if fading: push_error("SceneTransition destroyed while fading")
diff --git a/client/gui/menus/transition/scene_transition.gd.uid b/client/gui/menus/transition/scene_transition.gd.uid
new file mode 100644
index 00000000..60f764ae
--- /dev/null
+++ b/client/gui/menus/transition/scene_transition.gd.uid
@@ -0,0 +1 @@
+uid://ciml1u2x4f1ci
diff --git a/client/gui/menus/transition/scene_transition.tscn b/client/gui/menus/transition/scene_transition.tscn
new file mode 100644
index 00000000..dab16084
--- /dev/null
+++ b/client/gui/menus/transition/scene_transition.tscn
@@ -0,0 +1,135 @@
+[gd_scene load_steps=11 format=3 uid="uid://bg2d78ycorcqk"]
+
+[ext_resource type="Script" uid="uid://ciml1u2x4f1ci" path="res://gui/menus/transition/scene_transition.gd" id="1_fpbwj"]
+[ext_resource type="Shader" uid="uid://bmxrbbw18xq7u" path="res://gui/menus/transition/text_loading_anim.gdshader" id="2_g21ck"]
+
+[sub_resource type="Animation" id="Animation_g21ck"]
+length = 0.001
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("black:color")
+tracks/0/interp = 1
+tracks/0/loop_wrap = false
+tracks/0/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Color(0, 0, 0, 1)]
+}
+
+[sub_resource type="Animation" id="Animation_e6dcd"]
+resource_name = "fade"
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("black:color")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0, 1),
+"transitions": PackedFloat32Array(1, 1),
+"update": 0,
+"values": [Color(0, 0, 0, 0), Color(0, 0, 0, 1)]
+}
+
+[sub_resource type="AnimationLibrary" id="AnimationLibrary_00tv0"]
+_data = {
+&"RESET": SubResource("Animation_g21ck"),
+&"fade": SubResource("Animation_e6dcd")
+}
+
+[sub_resource type="Animation" id="Animation_xgn2a"]
+length = 0.001
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("text_margin/text:modulate")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Color(1, 1, 1, 0)]
+}
+
+[sub_resource type="Animation" id="Animation_cq5i2"]
+resource_name = "fade"
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("text_margin/text:modulate")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0, 1),
+"transitions": PackedFloat32Array(1, 1),
+"update": 0,
+"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 1)]
+}
+
+[sub_resource type="AnimationLibrary" id="AnimationLibrary_pea72"]
+_data = {
+&"RESET": SubResource("Animation_xgn2a"),
+&"fade": SubResource("Animation_cq5i2")
+}
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_00tv0"]
+shader = ExtResource("2_g21ck")
+
+[sub_resource type="LabelSettings" id="LabelSettings_e6dcd"]
+font_size = 34
+
+[node name="SceneTransition" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_fpbwj")
+
+[node name="black_fader" type="AnimationPlayer" parent="."]
+libraries = {
+&"": SubResource("AnimationLibrary_00tv0")
+}
+speed_scale = 4.0
+
+[node name="black" type="ColorRect" parent="."]
+visible = false
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+color = Color(0, 0, 0, 1)
+
+[node name="text_fader" type="AnimationPlayer" parent="."]
+libraries = {
+&"": SubResource("AnimationLibrary_pea72")
+}
+speed_scale = 4.0
+
+[node name="text_margin" type="MarginContainer" parent="."]
+layout_mode = 1
+anchors_preset = 2
+anchor_top = 1.0
+anchor_bottom = 1.0
+offset_top = -107.0
+offset_right = 401.0
+grow_vertical = 0
+mouse_filter = 2
+theme_override_constants/margin_left = 50
+theme_override_constants/margin_top = 50
+theme_override_constants/margin_right = 50
+theme_override_constants/margin_bottom = 50
+
+[node name="text" type="Label" parent="text_margin"]
+modulate = Color(1, 1, 1, 0)
+material = SubResource("ShaderMaterial_00tv0")
+layout_mode = 2
+text = "Loading something..."
+label_settings = SubResource("LabelSettings_e6dcd")
diff --git a/client/gui/menus/transition/text_loading_anim.gdshader b/client/gui/menus/transition/text_loading_anim.gdshader
new file mode 100644
index 00000000..145dab78
--- /dev/null
+++ b/client/gui/menus/transition/text_loading_anim.gdshader
@@ -0,0 +1,13 @@
+shader_type canvas_item;
+
+varying vec4 vertex_color;
+void vertex() {
+ vertex_color = COLOR;
+}
+
+void fragment() {
+ vec4 tex = texture(TEXTURE, UV) * COLOR;
+ float wave = sin(VERTEX.x*0.01-TIME*10.) * 0.5 + 0.5;
+ wave = pow(wave, 3.);
+ COLOR = tex * (1. - wave * 0.2);
+}
diff --git a/client/gui/menus/transition/text_loading_anim.gdshader.uid b/client/gui/menus/transition/text_loading_anim.gdshader.uid
new file mode 100644
index 00000000..26730c73
--- /dev/null
+++ b/client/gui/menus/transition/text_loading_anim.gdshader.uid
@@ -0,0 +1 @@
+uid://bmxrbbw18xq7u