aboutsummaryrefslogtreecommitdiff
path: root/client/gui/overlays
diff options
context:
space:
mode:
Diffstat (limited to 'client/gui/overlays')
-rw-r--r--client/gui/overlays/ingame/announce_title.gd42
-rw-r--r--client/gui/overlays/ingame/announce_title.gd.uid1
-rw-r--r--client/gui/overlays/ingame/chat.gd34
-rw-r--r--client/gui/overlays/ingame/chat.gd.uid1
-rw-r--r--client/gui/overlays/ingame/chat.tscn39
-rw-r--r--client/gui/overlays/ingame/score.gd68
-rw-r--r--client/gui/overlays/ingame/score.gd.uid1
-rw-r--r--client/gui/overlays/ingame/score.tscn555
-rw-r--r--client/gui/overlays/lobby/lobby.gd204
-rw-r--r--client/gui/overlays/lobby/lobby.gd.uid1
-rw-r--r--client/gui/overlays/lobby/lobby.tscn191
-rw-r--r--client/gui/overlays/lobby/player.gd22
-rw-r--r--client/gui/overlays/lobby/player.gd.uid1
-rw-r--r--client/gui/overlays/lobby/player.tscn43
14 files changed, 1203 insertions, 0 deletions
diff --git a/client/gui/overlays/ingame/announce_title.gd b/client/gui/overlays/ingame/announce_title.gd
new file mode 100644
index 00000000..227e184f
--- /dev/null
+++ b/client/gui/overlays/ingame/announce_title.gd
@@ -0,0 +1,42 @@
+# 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 Control
+
+@onready var init_delay: Timer = $InitialDelay
+@onready var prep_timer: Timer = $PreperationTimer
+@onready var anim_player: AnimationPlayer = $AnimationPlayer
+@onready var label: Label = $Center/Node2D/Label
+
+func announce_start() -> void:
+ init_delay.start()
+ await init_delay.timeout
+
+ Sound.play_game_start()
+ show()
+ label.text = tr("c.announce.ready")
+ anim_player.play("fade_in")
+ await anim_player.animation_finished
+ prep_timer.start()
+ await prep_timer.timeout
+ anim_player.play("fade_out")
+ await anim_player.animation_finished
+
+ label.text = tr("c.announce.go")
+ anim_player.play("fade_in")
+ await anim_player.animation_finished
+ anim_player.play("fade_out")
+ await anim_player.animation_finished
+ hide()
diff --git a/client/gui/overlays/ingame/announce_title.gd.uid b/client/gui/overlays/ingame/announce_title.gd.uid
new file mode 100644
index 00000000..e57b9722
--- /dev/null
+++ b/client/gui/overlays/ingame/announce_title.gd.uid
@@ -0,0 +1 @@
+uid://ci08whhm2ej1m
diff --git a/client/gui/overlays/ingame/chat.gd b/client/gui/overlays/ingame/chat.gd
new file mode 100644
index 00000000..673dc979
--- /dev/null
+++ b/client/gui/overlays/ingame/chat.gd
@@ -0,0 +1,34 @@
+# 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 Control
+class_name ChatPreview
+
+const CHAT_MESSAGE_SCENE = preload("res://gui/components/message/chat_message.tscn")
+
+@onready var game: Game = $"../Game"
+@onready var messages_container: VBoxContainer = $MarginContainer/ScrollContainer/PanelContainer/Messages
+
+func _ready():
+ game.text_message.connect(func(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,
+ true,
+ message.timeout_remaining)
+ )
diff --git a/client/gui/overlays/ingame/chat.gd.uid b/client/gui/overlays/ingame/chat.gd.uid
new file mode 100644
index 00000000..7a372630
--- /dev/null
+++ b/client/gui/overlays/ingame/chat.gd.uid
@@ -0,0 +1 @@
+uid://bgt04y4ncl1fv
diff --git a/client/gui/overlays/ingame/chat.tscn b/client/gui/overlays/ingame/chat.tscn
new file mode 100644
index 00000000..f5b1060c
--- /dev/null
+++ b/client/gui/overlays/ingame/chat.tscn
@@ -0,0 +1,39 @@
+[gd_scene load_steps=5 format=3 uid="uid://xcxbmynn8mhi"]
+
+[ext_resource type="Theme" uid="uid://b0qmvo504e457" path="res://gui/resources/theme/theme.tres" id="1_lmy51"]
+[ext_resource type="Script" uid="uid://bgt04y4ncl1fv" path="res://gui/overlays/ingame/chat.gd" id="2_3543w"]
+[ext_resource type="Material" uid="uid://beea1pc5nt67r" path="res://gui/resources/materials/dark_blur_material.tres" id="3_15i2y"]
+[ext_resource type="Script" uid="uid://cmncjc06kadpe" path="res://gui/components/blur_setup.gd" id="4_3rmhr"]
+
+[node name="ChatPreview" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+theme = ExtResource("1_lmy51")
+script = ExtResource("2_3543w")
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 2
+anchor_bottom = 1.0
+offset_right = 296.0
+grow_vertical = 2
+
+[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer"]
+layout_mode = 2
+horizontal_scroll_mode = 0
+
+[node name="PanelContainer" type="PanelContainer" parent="MarginContainer/ScrollContainer"]
+material = ExtResource("3_15i2y")
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 0
+mouse_filter = 2
+script = ExtResource("4_3rmhr")
+
+[node name="Messages" type="VBoxContainer" parent="MarginContainer/ScrollContainer/PanelContainer"]
+layout_mode = 2
+mouse_filter = 2
diff --git a/client/gui/overlays/ingame/score.gd b/client/gui/overlays/ingame/score.gd
new file mode 100644
index 00000000..d4e2e3d3
--- /dev/null
+++ b/client/gui/overlays/ingame/score.gd
@@ -0,0 +1,68 @@
+# 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 Overlay
+extends PanelContainer
+
+const LABEL_SCALE: Vector2 = Vector2(1.5, 1.5)
+
+var failed: int = 0
+var completed: int = 0
+var points: int = 0
+
+@onready var failed_label: Label = $Score/Paper/Margin/Lines/Line2/Failed
+@onready var completed_label: Label = $Score/Paper/Margin/Lines/Line1/Completed
+@onready var points_label: Label = $Score/Paper/Margin/Lines/Line3/Points
+@onready var timer: Timer = $Timer
+@onready var seconds_label: Label = $Time/Paper/Line/Seconds
+@onready var decimals_label: Label = $Time/Paper/Line/Decimals
+@onready var announcement = $Announcement
+
+func update(failed_: int, completed_: int, points_: int, time: float):
+ if failed_ - failed:
+ failed_label.text = str(failed_)
+ failed_label.scale = LABEL_SCALE
+ failed = failed_
+ if completed_ - completed:
+ completed_label.text = str(completed_)
+ completed_label.scale = LABEL_SCALE
+ completed = completed_
+ if points_ - points:
+ points_label.text = str(points_)
+ points_label.scale = LABEL_SCALE
+ points = points_
+
+ timer.stop()
+ if time > 0.:
+ timer.wait_time = time
+ timer.start()
+
+func set_ingame(_state: bool, lobby: bool):
+ if lobby:
+ hide()
+ timer.stop()
+ else:
+ show()
+
+func announce_start():
+ announcement.announce_start()
+
+func _process(delta):
+ failed_label.scale = G.interpolate(failed_label.scale, Vector2(1, 1), delta * 4.)
+ completed_label.scale = G.interpolate(completed_label.scale, Vector2(1, 1), delta * 4.)
+ points_label.scale = G.interpolate(points_label.scale, Vector2(1, 1), delta * 4.)
+ var seconds: float = floor(timer.time_left)
+ seconds_label.text = str(int(seconds))
+ decimals_label.text = "%01d" % int((timer.time_left - seconds) * 10)
diff --git a/client/gui/overlays/ingame/score.gd.uid b/client/gui/overlays/ingame/score.gd.uid
new file mode 100644
index 00000000..ff13fb53
--- /dev/null
+++ b/client/gui/overlays/ingame/score.gd.uid
@@ -0,0 +1 @@
+uid://mcgg3q0l03dx
diff --git a/client/gui/overlays/ingame/score.tscn b/client/gui/overlays/ingame/score.tscn
new file mode 100644
index 00000000..497275f5
--- /dev/null
+++ b/client/gui/overlays/ingame/score.tscn
@@ -0,0 +1,555 @@
+[gd_scene load_steps=14 format=3 uid="uid://bpikve6wlsjfl"]
+
+[ext_resource type="Theme" uid="uid://b0qmvo504e457" path="res://gui/resources/theme/theme.tres" id="1_4kujw"]
+[ext_resource type="Script" uid="uid://mcgg3q0l03dx" path="res://gui/overlays/ingame/score.gd" id="2_kbjds"]
+[ext_resource type="Texture2D" uid="uid://chxkwohi56cxx" path="res://gui/resources/shaders/paper.tres" id="3_oum5g"]
+[ext_resource type="FontFile" uid="uid://bo4vh5xkpvrh1" path="res://gui/resources/fonts/font-sansita-swashed.woff2" id="3_u54fv"]
+[ext_resource type="Script" uid="uid://ci08whhm2ej1m" path="res://gui/overlays/ingame/announce_title.gd" id="5_874f2"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_04ujj"]
+bg_color = Color(0, 0, 0, 0)
+
+[sub_resource type="FontVariation" id="FontVariation_8f216"]
+base_font = ExtResource("3_u54fv")
+variation_embolden = 0.5
+
+[sub_resource type="Animation" id="Animation_owy7i"]
+length = 0.001
+tracks/0/type = "bezier"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("Center/Node2D:scale:x")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"handle_modes": PackedInt32Array(0),
+"points": PackedFloat32Array(1e-05, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0)
+}
+tracks/1/type = "bezier"
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/path = NodePath("Center/Node2D:scale:y")
+tracks/1/interp = 1
+tracks/1/loop_wrap = true
+tracks/1/keys = {
+"handle_modes": PackedInt32Array(0),
+"points": PackedFloat32Array(1e-05, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0)
+}
+tracks/2/type = "bezier"
+tracks/2/imported = false
+tracks/2/enabled = true
+tracks/2/path = NodePath("Center/Node2D:modulate:r")
+tracks/2/interp = 1
+tracks/2/loop_wrap = true
+tracks/2/keys = {
+"handle_modes": PackedInt32Array(0),
+"points": PackedFloat32Array(1, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0)
+}
+tracks/3/type = "bezier"
+tracks/3/imported = false
+tracks/3/enabled = true
+tracks/3/path = NodePath("Center/Node2D:modulate:g")
+tracks/3/interp = 1
+tracks/3/loop_wrap = true
+tracks/3/keys = {
+"handle_modes": PackedInt32Array(0),
+"points": PackedFloat32Array(1, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0)
+}
+tracks/4/type = "bezier"
+tracks/4/imported = false
+tracks/4/enabled = true
+tracks/4/path = NodePath("Center/Node2D:modulate:b")
+tracks/4/interp = 1
+tracks/4/loop_wrap = true
+tracks/4/keys = {
+"handle_modes": PackedInt32Array(0),
+"points": PackedFloat32Array(1, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0)
+}
+tracks/5/type = "bezier"
+tracks/5/imported = false
+tracks/5/enabled = true
+tracks/5/path = NodePath("Center/Node2D:modulate:a")
+tracks/5/interp = 1
+tracks/5/loop_wrap = true
+tracks/5/keys = {
+"handle_modes": PackedInt32Array(0),
+"points": PackedFloat32Array(0, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0)
+}
+
+[sub_resource type="Animation" id="Animation_kmsgw"]
+resource_name = "fade_in"
+length = 0.5
+tracks/0/type = "bezier"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("Center/Node2D:scale:x")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"handle_modes": PackedInt32Array(0, 0),
+"points": PackedFloat32Array(1e-05, -0.25, 0, 0.25, 0, 1, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0, 0.5)
+}
+tracks/1/type = "bezier"
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/path = NodePath("Center/Node2D:scale:y")
+tracks/1/interp = 1
+tracks/1/loop_wrap = true
+tracks/1/keys = {
+"handle_modes": PackedInt32Array(0, 0),
+"points": PackedFloat32Array(1e-05, -0.25, 0, 0.25, 0, 1, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0, 0.5)
+}
+tracks/2/type = "bezier"
+tracks/2/imported = false
+tracks/2/enabled = true
+tracks/2/path = NodePath("Center/Node2D:modulate:r")
+tracks/2/interp = 1
+tracks/2/loop_wrap = true
+tracks/2/keys = {
+"handle_modes": PackedInt32Array(0, 0),
+"points": PackedFloat32Array(1, -0.25, 0, 0.25, 0, 1, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0, 0.5)
+}
+tracks/3/type = "bezier"
+tracks/3/imported = false
+tracks/3/enabled = true
+tracks/3/path = NodePath("Center/Node2D:modulate:g")
+tracks/3/interp = 1
+tracks/3/loop_wrap = true
+tracks/3/keys = {
+"handle_modes": PackedInt32Array(0, 0),
+"points": PackedFloat32Array(1, -0.25, 0, 0.25, 0, 1, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0, 0.5)
+}
+tracks/4/type = "bezier"
+tracks/4/imported = false
+tracks/4/enabled = true
+tracks/4/path = NodePath("Center/Node2D:modulate:b")
+tracks/4/interp = 1
+tracks/4/loop_wrap = true
+tracks/4/keys = {
+"handle_modes": PackedInt32Array(0, 0),
+"points": PackedFloat32Array(1, -0.25, 0, 0.25, 0, 1, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0, 0.5)
+}
+tracks/5/type = "bezier"
+tracks/5/imported = false
+tracks/5/enabled = true
+tracks/5/path = NodePath("Center/Node2D:modulate:a")
+tracks/5/interp = 1
+tracks/5/loop_wrap = true
+tracks/5/keys = {
+"handle_modes": PackedInt32Array(0, 0),
+"points": PackedFloat32Array(0, -0.25, 0, 0.25, 0, 1, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0, 0.5)
+}
+
+[sub_resource type="Animation" id="Animation_874f2"]
+resource_name = "fade_in_and_out"
+length = 0.8
+tracks/0/type = "bezier"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("Center/Node2D:modulate:r")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"handle_modes": PackedInt32Array(0, 0, 0),
+"points": PackedFloat32Array(1, -0.25, 0, 0.25, 0, 1, -0.25, 0, 0.25, 0, 1, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0, 0.3, 0.8)
+}
+tracks/1/type = "bezier"
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/path = NodePath("Center/Node2D:modulate:g")
+tracks/1/interp = 1
+tracks/1/loop_wrap = true
+tracks/1/keys = {
+"handle_modes": PackedInt32Array(0, 0, 0),
+"points": PackedFloat32Array(1, -0.25, 0, 0.25, 0, 1, -0.25, 0, 0.25, 0, 1, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0, 0.3, 0.8)
+}
+tracks/2/type = "bezier"
+tracks/2/imported = false
+tracks/2/enabled = true
+tracks/2/path = NodePath("Center/Node2D:modulate:b")
+tracks/2/interp = 1
+tracks/2/loop_wrap = true
+tracks/2/keys = {
+"handle_modes": PackedInt32Array(0, 0, 0),
+"points": PackedFloat32Array(1, -0.25, 0, 0.25, 0, 1, -0.25, 0, 0.25, 0, 1, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0, 0.3, 0.8)
+}
+tracks/3/type = "bezier"
+tracks/3/imported = false
+tracks/3/enabled = true
+tracks/3/path = NodePath("Center/Node2D:modulate:a")
+tracks/3/interp = 1
+tracks/3/loop_wrap = true
+tracks/3/keys = {
+"handle_modes": PackedInt32Array(0, 0, 0),
+"points": PackedFloat32Array(0, -0.25, 0, 0.25, 0, 1, -0.25, 0, 0.25, 0, 0, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0, 0.3, 0.8)
+}
+tracks/4/type = "bezier"
+tracks/4/imported = false
+tracks/4/enabled = true
+tracks/4/path = NodePath("Center/Node2D:scale:x")
+tracks/4/interp = 1
+tracks/4/loop_wrap = true
+tracks/4/keys = {
+"handle_modes": PackedInt32Array(0, 0),
+"points": PackedFloat32Array(1e-05, -0.25, 0, 0.25, 0, 2, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0, 0.8)
+}
+tracks/5/type = "bezier"
+tracks/5/imported = false
+tracks/5/enabled = true
+tracks/5/path = NodePath("Center/Node2D:scale:y")
+tracks/5/interp = 1
+tracks/5/loop_wrap = true
+tracks/5/keys = {
+"handle_modes": PackedInt32Array(0, 0),
+"points": PackedFloat32Array(1e-05, -0.25, 0, 0.25, 0, 2, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0, 0.8)
+}
+
+[sub_resource type="Animation" id="Animation_x6gse"]
+resource_name = "fade_out"
+tracks/0/type = "bezier"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("Center/Node2D:modulate:r")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"handle_modes": PackedInt32Array(0, 0),
+"points": PackedFloat32Array(1, -0.25, 0, 0.25, 0, 1, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0, 1)
+}
+tracks/1/type = "bezier"
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/path = NodePath("Center/Node2D:modulate:g")
+tracks/1/interp = 1
+tracks/1/loop_wrap = true
+tracks/1/keys = {
+"handle_modes": PackedInt32Array(0, 0),
+"points": PackedFloat32Array(1, -0.25, 0, 0.25, 0, 1, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0, 1)
+}
+tracks/2/type = "bezier"
+tracks/2/imported = false
+tracks/2/enabled = true
+tracks/2/path = NodePath("Center/Node2D:modulate:b")
+tracks/2/interp = 1
+tracks/2/loop_wrap = true
+tracks/2/keys = {
+"handle_modes": PackedInt32Array(0, 0),
+"points": PackedFloat32Array(1, -0.25, 0, 0.25, 0, 1, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0, 1)
+}
+tracks/3/type = "bezier"
+tracks/3/imported = false
+tracks/3/enabled = true
+tracks/3/path = NodePath("Center/Node2D:modulate:a")
+tracks/3/interp = 1
+tracks/3/loop_wrap = true
+tracks/3/keys = {
+"handle_modes": PackedInt32Array(0, 0),
+"points": PackedFloat32Array(1, -0.25, 0, 0.25, 0, 0, -0.25, 0, 0.25, 0),
+"times": PackedFloat32Array(0, 1)
+}
+
+[sub_resource type="Animation" id="Animation_deqe4"]
+resource_name = "fade_out_quick"
+length = 0.25
+tracks/0/type = "bezier"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("Center/Node2D:modulate:r")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"handle_modes": PackedInt32Array(0, 0),
+"points": PackedFloat32Array(1, -0.125, 0, 0.125, 0, 1, -0.125, 0, 0.125, 0),
+"times": PackedFloat32Array(0, 0.25)
+}
+tracks/1/type = "bezier"
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/path = NodePath("Center/Node2D:modulate:g")
+tracks/1/interp = 1
+tracks/1/loop_wrap = true
+tracks/1/keys = {
+"handle_modes": PackedInt32Array(0, 0),
+"points": PackedFloat32Array(1, -0.125, 0, 0.125, 0, 1, -0.125, 0, 0.125, 0),
+"times": PackedFloat32Array(0, 0.25)
+}
+tracks/2/type = "bezier"
+tracks/2/imported = false
+tracks/2/enabled = true
+tracks/2/path = NodePath("Center/Node2D:modulate:b")
+tracks/2/interp = 1
+tracks/2/loop_wrap = true
+tracks/2/keys = {
+"handle_modes": PackedInt32Array(0, 0),
+"points": PackedFloat32Array(1, -0.125, 0, 0.125, 0, 1, -0.125, 0, 0.125, 0),
+"times": PackedFloat32Array(0, 0.25)
+}
+tracks/3/type = "bezier"
+tracks/3/imported = false
+tracks/3/enabled = true
+tracks/3/path = NodePath("Center/Node2D:modulate:a")
+tracks/3/interp = 1
+tracks/3/loop_wrap = true
+tracks/3/keys = {
+"handle_modes": PackedInt32Array(0, 0),
+"points": PackedFloat32Array(1, -0.125, 0, 0.125, 0, 0, -0.125, 0, 0.125, 0),
+"times": PackedFloat32Array(0, 0.25)
+}
+
+[sub_resource type="AnimationLibrary" id="AnimationLibrary_deqe4"]
+_data = {
+&"RESET": SubResource("Animation_owy7i"),
+&"fade_in": SubResource("Animation_kmsgw"),
+&"fade_in_and_out": SubResource("Animation_874f2"),
+&"fade_out": SubResource("Animation_x6gse"),
+&"fade_out_quick": SubResource("Animation_deqe4")
+}
+
+[node name="Overlay" type="PanelContainer"]
+layout_direction = 2
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+theme = ExtResource("1_4kujw")
+theme_override_styles/panel = SubResource("StyleBoxFlat_04ujj")
+script = ExtResource("2_kbjds")
+
+[node name="Timer" type="Timer" parent="."]
+wait_time = 30.0
+
+[node name="Score" type="Control" parent="."]
+layout_mode = 2
+size_flags_horizontal = 8
+size_flags_vertical = 0
+
+[node name="Paper" type="TextureRect" parent="Score"]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -242.0
+offset_top = -327.0
+offset_right = 270.0
+offset_bottom = 185.0
+grow_horizontal = 2
+grow_vertical = 2
+rotation = 0.0610865
+texture = ExtResource("3_oum5g")
+
+[node name="Margin" type="MarginContainer" parent="Score/Paper"]
+layout_mode = 1
+anchors_preset = 2
+anchor_top = 1.0
+anchor_bottom = 1.0
+offset_top = -142.0
+offset_right = 258.0
+grow_vertical = 0
+theme_override_constants/margin_left = 10
+theme_override_constants/margin_bottom = 10
+
+[node name="Lines" type="VBoxContainer" parent="Score/Paper/Margin"]
+layout_direction = 1
+layout_mode = 2
+size_flags_horizontal = 0
+size_flags_vertical = 8
+
+[node name="Line1" type="HBoxContainer" parent="Score/Paper/Margin/Lines"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="Score/Paper/Margin/Lines/Line1"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+theme_override_fonts/font = ExtResource("3_u54fv")
+theme_override_font_sizes/font_size = 25
+text = "c.score.completed"
+
+[node name="Spacer" type="Control" parent="Score/Paper/Margin/Lines/Line1"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Completed" type="Label" parent="Score/Paper/Margin/Lines/Line1"]
+auto_translate_mode = 2
+custom_minimum_size = Vector2(100, 0)
+layout_mode = 2
+theme_override_colors/font_color = Color(0, 0.278431, 0, 1)
+theme_override_fonts/font = ExtResource("3_u54fv")
+theme_override_font_sizes/font_size = 35
+text = "0"
+horizontal_alignment = 1
+
+[node name="Line2" type="HBoxContainer" parent="Score/Paper/Margin/Lines"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="Score/Paper/Margin/Lines/Line2"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+theme_override_fonts/font = ExtResource("3_u54fv")
+theme_override_font_sizes/font_size = 25
+text = "c.score.failed"
+
+[node name="Spacer" type="Control" parent="Score/Paper/Margin/Lines/Line2"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Failed" type="Label" parent="Score/Paper/Margin/Lines/Line2"]
+auto_translate_mode = 2
+custom_minimum_size = Vector2(100, 0)
+layout_mode = 2
+theme_override_colors/font_color = Color(0.505882, 0, 0, 1)
+theme_override_fonts/font = ExtResource("3_u54fv")
+theme_override_font_sizes/font_size = 35
+text = "0"
+horizontal_alignment = 1
+
+[node name="Line3" type="HBoxContainer" parent="Score/Paper/Margin/Lines"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="Score/Paper/Margin/Lines/Line3"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+theme_override_fonts/font = ExtResource("3_u54fv")
+theme_override_font_sizes/font_size = 35
+text = "c.score.points"
+
+[node name="Spacer" type="Control" parent="Score/Paper/Margin/Lines/Line3"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Points" type="Label" parent="Score/Paper/Margin/Lines/Line3"]
+auto_translate_mode = 2
+custom_minimum_size = Vector2(100, 0)
+layout_mode = 2
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+theme_override_fonts/font = ExtResource("3_u54fv")
+theme_override_font_sizes/font_size = 45
+text = "0"
+horizontal_alignment = 1
+
+[node name="Time" type="Control" parent="."]
+layout_mode = 2
+size_flags_horizontal = 8
+size_flags_vertical = 8
+
+[node name="Paper" type="TextureRect" parent="Time"]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -206.0
+offset_top = -119.0
+offset_right = 306.0
+offset_bottom = 393.0
+grow_horizontal = 2
+grow_vertical = 2
+rotation = 0.0610865
+texture = ExtResource("3_oum5g")
+
+[node name="Line" type="HBoxContainer" parent="Time/Paper"]
+layout_mode = 0
+
+[node name="Seconds" type="Label" parent="Time/Paper/Line"]
+auto_translate_mode = 2
+custom_minimum_size = Vector2(100, 0)
+layout_mode = 2
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+theme_override_fonts/font = ExtResource("3_u54fv")
+theme_override_font_sizes/font_size = 45
+text = "300"
+horizontal_alignment = 2
+
+[node name="Point" type="Label" parent="Time/Paper/Line"]
+auto_translate_mode = 2
+layout_mode = 2
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+theme_override_fonts/font = ExtResource("3_u54fv")
+theme_override_font_sizes/font_size = 45
+text = "."
+horizontal_alignment = 1
+
+[node name="Decimals" type="Label" parent="Time/Paper/Line"]
+auto_translate_mode = 2
+layout_mode = 2
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+theme_override_fonts/font = ExtResource("3_u54fv")
+theme_override_font_sizes/font_size = 45
+text = "00"
+
+[node name="Announcement" type="Control" parent="."]
+visible = false
+layout_mode = 2
+script = ExtResource("5_874f2")
+
+[node name="Center" type="Control" parent="Announcement"]
+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="Node2D" type="Node2D" parent="Announcement/Center"]
+modulate = Color(1, 1, 1, 0)
+scale = Vector2(1e-05, 1e-05)
+
+[node name="Label" type="Label" parent="Announcement/Center/Node2D"]
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -375.0
+offset_top = -50.0
+offset_right = 375.0
+offset_bottom = 50.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_vertical = 1
+theme_override_colors/font_outline_color = Color(0.564706, 0.207843, 0.266667, 1)
+theme_override_constants/outline_size = 20
+theme_override_fonts/font = SubResource("FontVariation_8f216")
+theme_override_font_sizes/font_size = 64
+text = "GO!"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="AnimationPlayer" type="AnimationPlayer" parent="Announcement"]
+libraries = {
+&"": SubResource("AnimationLibrary_deqe4")
+}
+
+[node name="PreperationTimer" type="Timer" parent="Announcement"]
+one_shot = true
+
+[node name="InitialDelay" type="Timer" parent="Announcement"]
+wait_time = 0.5
+one_shot = true
diff --git a/client/gui/overlays/lobby/lobby.gd b/client/gui/overlays/lobby/lobby.gd
new file mode 100644
index 00000000..3971dbb3
--- /dev/null
+++ b/client/gui/overlays/lobby/lobby.gd
@@ -0,0 +1,204 @@
+# 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 Control
+class_name Lobby
+
+const MAX_BOT_COUNT_PER_TYPE: int = 3
+const PLAYER = preload("res://gui/overlays/lobby/player.tscn")
+
+var map_count
+var selected_map := 0
+var selected_map_name: String
+
+var bots_enabled := false
+var bot_counts := {}
+var bot_reset_buttons := {}
+var bot_inc_buttons := {}
+var bot_dec_buttons := {}
+
+@onready var game: Game = $"../Game"
+@onready var player_container = $PlayerList/VBoxContainer/Players
+
+@onready var map_name_label = $Sidebar/Bottom/MarginContainer/VBoxContainer/HBoxContainer/Map/Name
+@onready var map_player_label = $Sidebar/Bottom/MarginContainer/VBoxContainer/HBoxContainer/Map/Players
+@onready var map_difficulty_label = $Sidebar/Bottom/MarginContainer/VBoxContainer/HBoxContainer/Map/Difficulty
+@onready var map_list_container = $Sidebar/Bottom/MarginContainer/VBoxContainer/MapList/VBoxContainer
+@onready var map_list = $Sidebar/Bottom/MarginContainer/VBoxContainer/MapList
+
+@onready var map_selector = $Sidebar/Bottom/MarginContainer/VBoxContainer/HBoxContainer
+@onready var prev_map = $Sidebar/Bottom/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/Left
+@onready var next_map = $Sidebar/Bottom/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2/Right
+
+@onready var bots_container = $Sidebar/Bottom/MarginContainer/VBoxContainer/Bots
+@onready var bot_settings = $Sidebar/Bottom/MarginContainer/VBoxContainer/Bots/ScrollContainerCustom/BotSettings
+@onready var bot_settings_conainer = $Sidebar/Bottom/MarginContainer/VBoxContainer/Bots/ScrollContainerCustom
+
+@onready var join_spectate = $Sidebar/Bottom/MarginContainer/VBoxContainer/VBoxContainer/JoinSpectate
+@onready var start_button = $Sidebar/Bottom/MarginContainer/VBoxContainer/VBoxContainer/Start
+
+func _ready():
+ game.update_players.connect(update_players)
+ initialize()
+ game.data_updated.connect(initialize)
+ game.join_state_updated.connect(_on_game_join_state_updated)
+ _on_game_join_state_updated(game.join_state)
+ check_for_music()
+
+func initialize():
+ map_count = game.maps.size()
+
+ for c in map_list_container.get_children():
+ c.queue_free()
+ for c in bot_settings.get_children():
+ c.queue_free()
+
+ var i := 0
+ for m in game.maps:
+ var b := Button.new()
+ b.name = m[0]
+ b.text = "%s (%d)" % [m[1]["name"], m[1]["players"]]
+ b.pressed.connect(select_map.bind(i))
+ b.focus_entered.connect(select_map.bind(i))
+ map_list_container.add_child(b)
+ i += 1
+ select_map(0)
+
+ for algo_id: String in game.bot_algos:
+ if algo_id == "customer" or algo_id == "test":
+ continue
+
+ bot_counts[algo_id] = 0
+
+ var h := HBoxContainer.new()
+ h.name = algo_id
+ var add := Button.new()
+ add.text = "+"
+ var reset := Button.new()
+ reset.size_flags_horizontal = SIZE_EXPAND_FILL
+ var remove := Button.new()
+ remove.text = "-"
+ bot_reset_buttons[algo_id] = reset
+ bot_inc_buttons[algo_id] = add
+ bot_dec_buttons[algo_id] = remove
+ update_bot_reset_text(algo_id)
+ add.pressed.connect(increase_bot_count.bind(algo_id))
+ reset.pressed.connect(reset_bot_count.bind(algo_id))
+ remove.pressed.connect(decrease_bot_count.bind(algo_id))
+ h.add_child(remove)
+ h.add_child(reset)
+ h.add_child(add)
+ bot_settings.add_child(h)
+
+func select_map(i: int):
+ if i >= map_count:
+ return
+ selected_map = i
+ var map_data: Dictionary = game.maps[i][1]
+ map_name_label.text = map_data["name"]
+ map_player_label.text = tr("c.map.players_recommended").format([roundi(map_data["players"])])
+ map_difficulty_label.text = tr("c.map.difficulty.%d" % (map_data["difficulty"] - 1))
+ selected_map_name = game.maps[i][0]
+ if not game.menu.covered:
+ map_list_container.get_child(i).grab_focus()
+
+func increase_bot_count(algo_id: String):
+ bot_counts[algo_id] += 1
+ update_bot_reset_text(algo_id)
+
+func decrease_bot_count(algo_id: String):
+ bot_counts[algo_id] -= 1
+ update_bot_reset_text(algo_id)
+
+func reset_bot_count(algo_id: String):
+ bot_counts[algo_id] = 0
+ update_bot_reset_text(algo_id)
+
+func update_bot_reset_text(algo_id: String):
+ var display_name: String = tr("s.bot.%s" % algo_id)
+ bot_reset_buttons[algo_id].text = "%s (%d)" % [display_name, bot_counts[algo_id]]
+ bot_inc_buttons[algo_id].disabled = not bot_counts[algo_id] < MAX_BOT_COUNT_PER_TYPE
+ bot_dec_buttons[algo_id].disabled = not bot_counts[algo_id] > 0
+
+func update_players(player_list: Dictionary):
+ for i in player_container.get_children():
+ i.queue_free()
+
+ for i in player_list.keys():
+ var p: PlayerTag = PLAYER.instantiate()
+ player_container.add_child(p)
+ p.setup(player_list[i].username)
+
+func _input(_event):
+ if not visible:
+ return
+
+ if Input.is_action_just_pressed("previous") and not prev_map.disabled:
+ prev_map.emit_signal("pressed")
+ elif Input.is_action_just_pressed("next") and not next_map.disabled:
+ next_map.emit_signal("pressed")
+
+func _on_left_pressed():
+ selected_map = (selected_map - 1) % map_count
+ select_map(selected_map)
+
+func _on_right_pressed():
+ selected_map = (selected_map + 1) % map_count
+ select_map(selected_map)
+
+func _on_start_pressed():
+ if selected_map_name != null:
+ var start_msg := "/start %s" % selected_map_name
+
+ if bots_enabled:
+ for k in bot_counts.keys():
+ for i in range(bot_counts[k]):
+ start_msg += "\ncreate-bot %s" % k
+
+ game.mp.send_chat(game.my_player_id, start_msg)
+ Sound.play_music("stop") # TODO: Game music enter
+
+func _on_game_join_state_updated(state: Game.JoinState):
+ match state:
+ Game.JoinState.JOINED:
+ map_selector.show()
+ map_list.show()
+ bots_container.show()
+ start_button.disabled = false
+ join_spectate.disabled = false
+ join_spectate.text = tr("c.menu.ingame.spectate")
+ Game.JoinState.SPECTATING:
+ map_selector.hide()
+ map_list.hide()
+ bots_container.hide()
+ start_button.disabled = true
+ join_spectate.disabled = false
+ join_spectate.text = tr("c.menu.ingame.join")
+ Game.JoinState.WAITING:
+ join_spectate.disabled = true
+
+func _on_join_spectate_pressed():
+ game.toggle_join()
+
+func check_for_music():
+ if visible:
+ Sound.play_music("Lobby")
+ else:
+ Sound.play_music("stop") # TODO: Game music enter
+
+func _on_enable_bots_toggled(toggled_on):
+ bots_enabled = toggled_on
+ bot_settings_conainer.visible = toggled_on
+ bots_container.size_flags_vertical = SIZE_EXPAND_FILL if toggled_on else SIZE_FILL
diff --git a/client/gui/overlays/lobby/lobby.gd.uid b/client/gui/overlays/lobby/lobby.gd.uid
new file mode 100644
index 00000000..b92e8681
--- /dev/null
+++ b/client/gui/overlays/lobby/lobby.gd.uid
@@ -0,0 +1 @@
+uid://bssjvsu44l0fn
diff --git a/client/gui/overlays/lobby/lobby.tscn b/client/gui/overlays/lobby/lobby.tscn
new file mode 100644
index 00000000..58ae2add
--- /dev/null
+++ b/client/gui/overlays/lobby/lobby.tscn
@@ -0,0 +1,191 @@
+[gd_scene load_steps=17 format=3 uid="uid://bc50la65ntifb"]
+
+[ext_resource type="Theme" uid="uid://b0qmvo504e457" path="res://gui/resources/theme/theme.tres" id="1_u18ke"]
+[ext_resource type="Script" uid="uid://bssjvsu44l0fn" path="res://gui/overlays/lobby/lobby.gd" id="2_7657i"]
+[ext_resource type="StyleBox" uid="uid://de80aw86emnql" path="res://gui/resources/style/lobby_panel_override.tres" id="3_6iqoe"]
+[ext_resource type="Material" uid="uid://beea1pc5nt67r" path="res://gui/resources/materials/dark_blur_material.tres" id="3_esmbx"]
+[ext_resource type="Texture2D" uid="uid://35rd5gamtyqm" path="res://gui/resources/icons/arrow.svg" id="3_jxleg"]
+[ext_resource type="Texture2D" uid="uid://j75dbytlbju" path="res://gui/resources/icons/arrow_pressed.svg" id="4_eapmn"]
+[ext_resource type="Script" uid="uid://cmncjc06kadpe" path="res://gui/components/blur_setup.gd" id="5_am8pt"]
+[ext_resource type="Texture2D" uid="uid://b33qmctbpf48g" path="res://gui/resources/icons/arrow_hover.svg" id="5_odwav"]
+[ext_resource type="Script" uid="uid://byshs20og68tn" path="res://gui/components/smart_margin_container.gd" id="6_7mu2u"]
+[ext_resource type="Texture2D" uid="uid://by3qsrpxnfq4w" path="res://gui/resources/icons/arrow_focus.svg" id="6_tulu3"]
+[ext_resource type="FontFile" uid="uid://5ixo6b3bd3km" path="res://gui/resources/fonts/font-josefin-sans.woff2" id="8_cwbpa"]
+[ext_resource type="Texture2D" uid="uid://bsx6fo7mv2u6a" path="res://gui/resources/icons/controller_x.svg" id="9_q14bw"]
+[ext_resource type="Script" uid="uid://bd7bylb2t2m0" path="res://gui/components/touch_scroll_container.gd" id="10_bgene"]
+[ext_resource type="Texture2D" uid="uid://cr2a6ide6vnnv" path="res://gui/resources/icons/controller_y.svg" id="11_5uugf"]
+[ext_resource type="Script" uid="uid://b1eomxildrq30" path="res://gui/components/controller_button.gd" id="12_7mu2u"]
+
+[sub_resource type="FontVariation" id="FontVariation_5xxr2"]
+base_font = ExtResource("8_cwbpa")
+variation_embolden = 1.5
+
+[node name="Lobby" type="Control" groups=["no_auto_focus"]]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme = ExtResource("1_u18ke")
+script = ExtResource("2_7657i")
+
+[node name="PlayerList" type="MarginContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+theme_override_constants/margin_left = 342
+theme_override_constants/margin_top = 32
+theme_override_constants/margin_right = 342
+theme_override_constants/margin_bottom = 32
+
+[node name="VBoxContainer" type="VBoxContainer" parent="PlayerList"]
+layout_mode = 2
+
+[node name="Players" type="HBoxContainer" parent="PlayerList/VBoxContainer"]
+layout_mode = 2
+alignment = 1
+
+[node name="Sidebar" type="HBoxContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+alignment = 2
+
+[node name="Bottom" type="PanelContainer" parent="Sidebar"]
+material = ExtResource("3_esmbx")
+layout_mode = 2
+theme_override_styles/panel = ExtResource("3_6iqoe")
+script = ExtResource("5_am8pt")
+
+[node name="MarginContainer" type="MarginContainer" parent="Sidebar/Bottom"]
+layout_mode = 2
+script = ExtResource("6_7mu2u")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Sidebar/Bottom/MarginContainer"]
+layout_mode = 2
+theme_override_constants/separation = 24
+
+[node name="HBoxContainer" type="HBoxContainer" parent="Sidebar/Bottom/MarginContainer/VBoxContainer"]
+layout_direction = 2
+layout_mode = 2
+alignment = 1
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Sidebar/Bottom/MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+alignment = 1
+
+[node name="Left" type="TextureButton" parent="Sidebar/Bottom/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer"]
+custom_minimum_size = Vector2(19, 28)
+layout_mode = 2
+focus_mode = 0
+texture_normal = ExtResource("3_jxleg")
+texture_pressed = ExtResource("4_eapmn")
+texture_hover = ExtResource("5_odwav")
+texture_focused = ExtResource("6_tulu3")
+ignore_texture_size = true
+stretch_mode = 4
+flip_h = true
+
+[node name="Map" type="VBoxContainer" parent="Sidebar/Bottom/MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+
+[node name="Name" type="Label" parent="Sidebar/Bottom/MarginContainer/VBoxContainer/HBoxContainer/Map"]
+custom_minimum_size = Vector2(200, 0)
+layout_mode = 2
+theme_override_fonts/font = SubResource("FontVariation_5xxr2")
+theme_override_font_sizes/font_size = 24
+text = "Map name"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="Players" type="Label" parent="Sidebar/Bottom/MarginContainer/VBoxContainer/HBoxContainer/Map"]
+layout_mode = 2
+text = "Players"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="Difficulty" type="Label" parent="Sidebar/Bottom/MarginContainer/VBoxContainer/HBoxContainer/Map"]
+layout_mode = 2
+text = "Difficulty"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="VBoxContainer2" type="VBoxContainer" parent="Sidebar/Bottom/MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+alignment = 1
+
+[node name="Right" type="TextureButton" parent="Sidebar/Bottom/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2"]
+custom_minimum_size = Vector2(19, 28)
+layout_mode = 2
+focus_mode = 0
+texture_normal = ExtResource("3_jxleg")
+texture_pressed = ExtResource("4_eapmn")
+texture_hover = ExtResource("5_odwav")
+texture_focused = ExtResource("6_tulu3")
+ignore_texture_size = true
+stretch_mode = 4
+
+[node name="MapList" type="ScrollContainer" parent="Sidebar/Bottom/MarginContainer/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+script = ExtResource("10_bgene")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Sidebar/Bottom/MarginContainer/VBoxContainer/MapList"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Bots" type="VBoxContainer" parent="Sidebar/Bottom/MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="EnableBots" type="CheckButton" parent="Sidebar/Bottom/MarginContainer/VBoxContainer/Bots"]
+layout_mode = 2
+text = "c.menu.lobby.enable_bots"
+
+[node name="ScrollContainerCustom" type="ScrollContainer" parent="Sidebar/Bottom/MarginContainer/VBoxContainer/Bots"]
+visible = false
+layout_mode = 2
+size_flags_vertical = 3
+script = ExtResource("10_bgene")
+
+[node name="BotSettings" type="VBoxContainer" parent="Sidebar/Bottom/MarginContainer/VBoxContainer/Bots/ScrollContainerCustom"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Sidebar/Bottom/MarginContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_constants/separation = 15
+alignment = 1
+
+[node name="JoinSpectate" type="Button" parent="Sidebar/Bottom/MarginContainer/VBoxContainer/VBoxContainer"]
+layout_mode = 2
+text = "c.menu.ingame.spectate"
+expand_icon = true
+script = ExtResource("12_7mu2u")
+controller_texture = ExtResource("11_5uugf")
+press_action = "join_spectate"
+metadata/_custom_type_script = "uid://b1eomxildrq30"
+
+[node name="Start" type="Button" parent="Sidebar/Bottom/MarginContainer/VBoxContainer/VBoxContainer"]
+layout_mode = 2
+text = "c.menu.lobby.start"
+expand_icon = true
+script = ExtResource("12_7mu2u")
+controller_texture = ExtResource("9_q14bw")
+press_action = "start_game"
+metadata/_custom_type_script = "uid://b1eomxildrq30"
+
+[connection signal="visibility_changed" from="." to="." method="check_for_music"]
+[connection signal="pressed" from="Sidebar/Bottom/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/Left" to="." method="_on_left_pressed"]
+[connection signal="pressed" from="Sidebar/Bottom/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2/Right" to="." method="_on_right_pressed"]
+[connection signal="toggled" from="Sidebar/Bottom/MarginContainer/VBoxContainer/Bots/EnableBots" to="." method="_on_enable_bots_toggled"]
+[connection signal="pressed" from="Sidebar/Bottom/MarginContainer/VBoxContainer/VBoxContainer/JoinSpectate" to="." method="_on_join_spectate_pressed"]
+[connection signal="pressed" from="Sidebar/Bottom/MarginContainer/VBoxContainer/VBoxContainer/Start" to="." method="_on_start_pressed"]
diff --git a/client/gui/overlays/lobby/player.gd b/client/gui/overlays/lobby/player.gd
new file mode 100644
index 00000000..175d6341
--- /dev/null
+++ b/client/gui/overlays/lobby/player.gd
@@ -0,0 +1,22 @@
+# 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 PanelContainer
+class_name PlayerTag
+
+@onready var name_label = $MarginContainer/HBoxContainer/Label
+
+func setup(player_name: String):
+ name_label.text = player_name
diff --git a/client/gui/overlays/lobby/player.gd.uid b/client/gui/overlays/lobby/player.gd.uid
new file mode 100644
index 00000000..b0971cdb
--- /dev/null
+++ b/client/gui/overlays/lobby/player.gd.uid
@@ -0,0 +1 @@
+uid://buxb488rr2ncs
diff --git a/client/gui/overlays/lobby/player.tscn b/client/gui/overlays/lobby/player.tscn
new file mode 100644
index 00000000..aa8997a1
--- /dev/null
+++ b/client/gui/overlays/lobby/player.tscn
@@ -0,0 +1,43 @@
+[gd_scene load_steps=6 format=3 uid="uid://gmldnel4xbxy"]
+
+[ext_resource type="Theme" uid="uid://b0qmvo504e457" path="res://gui/resources/theme/theme.tres" id="1_flfqn"]
+[ext_resource type="Texture2D" uid="uid://222w1wha75od" path="res://gui/resources/icons/user.webp" id="2_mnaqt"]
+[ext_resource type="Script" uid="uid://buxb488rr2ncs" path="res://gui/overlays/lobby/player.gd" id="2_w3lyk"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1227j"]
+bg_color = Color(0, 0, 0, 1)
+corner_radius_top_left = 16
+corner_radius_top_right = 16
+corner_radius_bottom_right = 16
+corner_radius_bottom_left = 16
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_3yp6e"]
+content_margin_right = 8.0
+
+[node name="Player" type="PanelContainer"]
+offset_right = 40.0
+offset_bottom = 40.0
+theme = ExtResource("1_flfqn")
+theme_override_styles/panel = SubResource("StyleBoxFlat_1227j")
+script = ExtResource("2_w3lyk")
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 2
+theme_override_constants/margin_left = 4
+theme_override_constants/margin_top = 4
+theme_override_constants/margin_right = 4
+theme_override_constants/margin_bottom = 4
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+
+[node name="Icon" type="TextureRect" parent="MarginContainer/HBoxContainer"]
+layout_mode = 2
+texture = ExtResource("2_mnaqt")
+expand_mode = 2
+
+[node name="Label" type="Label" parent="MarginContainer/HBoxContainer"]
+layout_mode = 2
+theme_override_styles/normal = SubResource("StyleBoxEmpty_3yp6e")
+text = "Player"
+vertical_alignment = 1