diff --git a/Game/scenes/lobby.tscn b/Game/scenes/lobby.tscn index 5cb26027..f43fc017 100644 --- a/Game/scenes/lobby.tscn +++ b/Game/scenes/lobby.tscn @@ -13,26 +13,40 @@ grow_vertical = 2 script = ExtResource("1_lobby") theme = ExtResource("2_theme") -[node name="CenterContainer" type="CenterContainer" parent="."] +[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 = 24 +theme_override_constants/margin_top = 24 +theme_override_constants/margin_right = 24 +theme_override_constants/margin_bottom = 24 -[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer"] +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer"] +layout_mode = 2 +follow_focus = true +horizontal_scroll_mode = 0 + +[node name="CenterContainer" type="CenterContainer" parent="MarginContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/ScrollContainer/CenterContainer"] custom_minimum_size = Vector2(520, 0) layout_mode = 2 theme_override_constants/separation = 10 -[node name="TitleLabel" type="Label" parent="CenterContainer/VBoxContainer"] +[node name="TitleLabel" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] layout_mode = 2 theme_override_font_sizes/font_size = 40 text = "Lobby" horizontal_alignment = 1 -[node name="StatusLabel" type="Label" parent="CenterContainer/VBoxContainer"] +[node name="StatusLabel" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] unique_name_in_owner = true modulate = Color(1, 1, 1, 0.65) layout_mode = 2 @@ -41,74 +55,74 @@ text = "Connecting..." horizontal_alignment = 1 autowrap_mode = 2 -[node name="TeamsSeparator" type="HSeparator" parent="CenterContainer/VBoxContainer"] +[node name="TeamsSeparator" type="HSeparator" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] layout_mode = 2 -[node name="TeamsRow" type="HBoxContainer" parent="CenterContainer/VBoxContainer"] +[node name="TeamsRow" type="HBoxContainer" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] layout_mode = 2 theme_override_constants/separation = 20 -[node name="Team0Panel" type="VBoxContainer" parent="CenterContainer/VBoxContainer/TeamsRow"] +[node name="Team0Panel" type="VBoxContainer" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/TeamsRow"] layout_mode = 2 size_flags_horizontal = 3 theme_override_constants/separation = 4 -[node name="Team0Header" type="Label" parent="CenterContainer/VBoxContainer/TeamsRow/Team0Panel"] +[node name="Team0Header" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/TeamsRow/Team0Panel"] layout_mode = 2 theme_override_font_sizes/font_size = 18 text = "Team 1" -[node name="Team0List" type="VBoxContainer" parent="CenterContainer/VBoxContainer/TeamsRow/Team0Panel"] +[node name="Team0List" type="VBoxContainer" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/TeamsRow/Team0Panel"] unique_name_in_owner = true layout_mode = 2 theme_override_constants/separation = 2 -[node name="TeamsVSeparator" type="VSeparator" parent="CenterContainer/VBoxContainer/TeamsRow"] +[node name="TeamsVSeparator" type="VSeparator" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/TeamsRow"] layout_mode = 2 -[node name="Team1Panel" type="VBoxContainer" parent="CenterContainer/VBoxContainer/TeamsRow"] +[node name="Team1Panel" type="VBoxContainer" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/TeamsRow"] layout_mode = 2 size_flags_horizontal = 3 theme_override_constants/separation = 4 -[node name="Team1Header" type="Label" parent="CenterContainer/VBoxContainer/TeamsRow/Team1Panel"] +[node name="Team1Header" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/TeamsRow/Team1Panel"] layout_mode = 2 theme_override_font_sizes/font_size = 18 text = "Team 2" -[node name="Team1List" type="VBoxContainer" parent="CenterContainer/VBoxContainer/TeamsRow/Team1Panel"] +[node name="Team1List" type="VBoxContainer" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/TeamsRow/Team1Panel"] unique_name_in_owner = true layout_mode = 2 theme_override_constants/separation = 2 -[node name="ControlsSeparator" type="HSeparator" parent="CenterContainer/VBoxContainer"] +[node name="ControlsSeparator" type="HSeparator" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] layout_mode = 2 -[node name="ControlsRow" type="HBoxContainer" parent="CenterContainer/VBoxContainer"] +[node name="ControlsRow" type="HBoxContainer" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 theme_override_constants/separation = 10 -[node name="SwitchTeamButton" type="Button" parent="CenterContainer/VBoxContainer/ControlsRow"] +[node name="SwitchTeamButton" type="Button" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/ControlsRow"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 48) layout_mode = 2 size_flags_horizontal = 3 text = "Switch Team" -[node name="ReadyButton" type="CheckButton" parent="CenterContainer/VBoxContainer/ControlsRow"] +[node name="ReadyButton" type="CheckButton" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/ControlsRow"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 48) layout_mode = 2 size_flags_horizontal = 3 text = "Ready" -[node name="LeaveButton" type="Button" parent="CenterContainer/VBoxContainer"] +[node name="LeaveButton" type="Button" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 48) layout_mode = 2 text = "Leave" -[connection signal="pressed" from="CenterContainer/VBoxContainer/ControlsRow/SwitchTeamButton" to="." method="_on_switch_team_pressed"] -[connection signal="toggled" from="CenterContainer/VBoxContainer/ControlsRow/ReadyButton" to="." method="_on_ready_toggled"] -[connection signal="pressed" from="CenterContainer/VBoxContainer/LeaveButton" to="." method="_on_leave_pressed"] +[connection signal="pressed" from="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/ControlsRow/SwitchTeamButton" to="." method="_on_switch_team_pressed"] +[connection signal="toggled" from="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/ControlsRow/ReadyButton" to="." method="_on_ready_toggled"] +[connection signal="pressed" from="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/LeaveButton" to="." method="_on_leave_pressed"] diff --git a/Game/scenes/main_menu.tscn b/Game/scenes/main_menu.tscn index b9ec927a..b728c4c4 100644 --- a/Game/scenes/main_menu.tscn +++ b/Game/scenes/main_menu.tscn @@ -13,124 +13,139 @@ grow_vertical = 2 script = ExtResource("1_menu") theme = ExtResource("2_theme") -[node name="CenterContainer" type="CenterContainer" parent="."] +[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 = 24 +theme_override_constants/margin_top = 24 +theme_override_constants/margin_right = 24 +theme_override_constants/margin_bottom = 24 -[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer"] +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer"] +layout_mode = 2 +follow_focus = true +horizontal_scroll_mode = 0 + +[node name="CenterContainer" type="CenterContainer" parent="MarginContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/ScrollContainer/CenterContainer"] custom_minimum_size = Vector2(420, 0) layout_mode = 2 theme_override_constants/separation = 10 -[node name="TitleLabel" type="Label" parent="CenterContainer/VBoxContainer"] +[node name="TitleLabel" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] layout_mode = 2 theme_override_font_sizes/font_size = 48 text = "Cosmic Clash" horizontal_alignment = 1 -[node name="SubtitleLabel" type="Label" parent="CenterContainer/VBoxContainer"] +[node name="SubtitleLabel" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] modulate = Color(1, 1, 1, 0.55) layout_mode = 2 theme_override_font_sizes/font_size = 16 text = "Physics-based soccer in space" horizontal_alignment = 1 -[node name="TitleSpacer" type="Control" parent="CenterContainer/VBoxContainer"] +[node name="TitleSpacer" type="Control" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] custom_minimum_size = Vector2(0, 14) layout_mode = 2 -[node name="FreePlayButton" type="Button" parent="CenterContainer/VBoxContainer"] +[node name="FreePlayButton" type="Button" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] +unique_name_in_owner = true custom_minimum_size = Vector2(0, 56) layout_mode = 2 text = "Free Play" -[node name="FreePlayHint" type="Label" parent="CenterContainer/VBoxContainer"] +[node name="FreePlayHint" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] modulate = Color(1, 1, 1, 0.55) layout_mode = 2 theme_override_font_sizes/font_size = 13 text = "Solo practice — no timer, R resets the ball" horizontal_alignment = 1 -[node name="ArenaRow" type="HBoxContainer" parent="CenterContainer/VBoxContainer"] +[node name="ArenaRow" type="HBoxContainer" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] layout_mode = 2 theme_override_constants/separation = 10 -[node name="ArenaLabel" type="Label" parent="CenterContainer/VBoxContainer/ArenaRow"] +[node name="ArenaLabel" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/ArenaRow"] layout_mode = 2 text = "Arena" -[node name="ArenaDropdown" type="OptionButton" parent="CenterContainer/VBoxContainer/ArenaRow"] +[node name="ArenaDropdown" type="OptionButton" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/ArenaRow"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 40) layout_mode = 2 size_flags_horizontal = 3 -[node name="MatchSeparator" type="HSeparator" parent="CenterContainer/VBoxContainer"] +[node name="MatchSeparator" type="HSeparator" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] layout_mode = 2 -[node name="MatchHeader" type="Label" parent="CenterContainer/VBoxContainer"] +[node name="MatchHeader" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] layout_mode = 2 theme_override_font_sizes/font_size = 22 text = "Match" -[node name="MatchHint" type="Label" parent="CenterContainer/VBoxContainer"] +[node name="MatchHint" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] modulate = Color(1, 1, 1, 0.55) layout_mode = 2 theme_override_font_sizes/font_size = 13 text = "A 2:30 match — you vs a trained bot" -[node name="MatchRow" type="HBoxContainer" parent="CenterContainer/VBoxContainer"] +[node name="MatchRow" type="HBoxContainer" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] layout_mode = 2 theme_override_constants/separation = 10 -[node name="DifficultyLabel" type="Label" parent="CenterContainer/VBoxContainer/MatchRow"] +[node name="DifficultyLabel" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/MatchRow"] layout_mode = 2 text = "Difficulty" -[node name="DifficultyDropdown" type="OptionButton" parent="CenterContainer/VBoxContainer/MatchRow"] +[node name="DifficultyDropdown" type="OptionButton" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/MatchRow"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 40) layout_mode = 2 size_flags_horizontal = 3 -[node name="MatchButton" type="Button" parent="CenterContainer/VBoxContainer"] +[node name="MatchButton" type="Button" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] custom_minimum_size = Vector2(0, 56) layout_mode = 2 text = "Play Match" -[node name="MultiplayerSeparator" type="HSeparator" parent="CenterContainer/VBoxContainer"] +[node name="MultiplayerSeparator" type="HSeparator" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] layout_mode = 2 -[node name="MultiplayerHeader" type="Label" parent="CenterContainer/VBoxContainer"] +[node name="MultiplayerHeader" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] layout_mode = 2 theme_override_font_sizes/font_size = 22 text = "Multiplayer" -[node name="MultiplayerHint" type="Label" parent="CenterContainer/VBoxContainer"] +[node name="MultiplayerHint" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] modulate = Color(1, 1, 1, 0.55) layout_mode = 2 theme_override_font_sizes/font_size = 13 text = "LAN / direct IP — host a match or join one" -[node name="FindMatchButton" type="Button" parent="CenterContainer/VBoxContainer"] +[node name="FindMatchButton" type="Button" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] custom_minimum_size = Vector2(0, 48) layout_mode = 2 text = "Find Match" -[node name="HostButton" type="Button" parent="CenterContainer/VBoxContainer"] +[node name="HostButton" type="Button" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] custom_minimum_size = Vector2(0, 48) layout_mode = 2 text = "Host" -[node name="JoinRow" type="HBoxContainer" parent="CenterContainer/VBoxContainer"] +[node name="JoinRow" type="HBoxContainer" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] layout_mode = 2 theme_override_constants/separation = 10 -[node name="JoinAddressEdit" type="LineEdit" parent="CenterContainer/VBoxContainer/JoinRow"] +[node name="JoinAddressEdit" type="LineEdit" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/JoinRow"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 40) layout_mode = 2 @@ -138,12 +153,12 @@ size_flags_horizontal = 3 text = "127.0.0.1" placeholder_text = "IP address" -[node name="JoinButton" type="Button" parent="CenterContainer/VBoxContainer/JoinRow"] +[node name="JoinButton" type="Button" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/JoinRow"] custom_minimum_size = Vector2(96, 40) layout_mode = 2 text = "Join" -[node name="MultiplayerErrorLabel" type="Label" parent="CenterContainer/VBoxContainer"] +[node name="MultiplayerErrorLabel" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] unique_name_in_owner = true modulate = Color(1, 0.5, 0.5, 1) layout_mode = 2 @@ -152,82 +167,82 @@ text = "" autowrap_mode = 2 visible = false -[node name="SettingsSeparator" type="HSeparator" parent="CenterContainer/VBoxContainer"] +[node name="SettingsSeparator" type="HSeparator" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] layout_mode = 2 -[node name="SettingsButton" type="Button" parent="CenterContainer/VBoxContainer"] +[node name="SettingsButton" type="Button" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] custom_minimum_size = Vector2(0, 56) layout_mode = 2 text = "Settings" -[node name="DevSection" type="VBoxContainer" parent="CenterContainer/VBoxContainer"] +[node name="DevSection" type="VBoxContainer" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 theme_override_constants/separation = 10 -[node name="DevSeparator" type="HSeparator" parent="CenterContainer/VBoxContainer/DevSection"] +[node name="DevSeparator" type="HSeparator" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/DevSection"] layout_mode = 2 -[node name="DevHeader" type="Label" parent="CenterContainer/VBoxContainer/DevSection"] +[node name="DevHeader" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/DevSection"] layout_mode = 2 theme_override_font_sizes/font_size = 22 text = "Developer" -[node name="DevHint" type="Label" parent="CenterContainer/VBoxContainer/DevSection"] +[node name="DevHint" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/DevSection"] modulate = Color(1, 1, 1, 0.55) layout_mode = 2 theme_override_font_sizes/font_size = 13 text = "Dev-only — hidden in release builds" -[node name="DevOpponentRow" type="HBoxContainer" parent="CenterContainer/VBoxContainer/DevSection"] +[node name="DevOpponentRow" type="HBoxContainer" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/DevSection"] layout_mode = 2 theme_override_constants/separation = 10 -[node name="DevOpponentLabel" type="Label" parent="CenterContainer/VBoxContainer/DevSection/DevOpponentRow"] +[node name="DevOpponentLabel" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/DevSection/DevOpponentRow"] layout_mode = 2 text = "Opponent override" -[node name="DevBotDropdown" type="OptionButton" parent="CenterContainer/VBoxContainer/DevSection/DevOpponentRow"] +[node name="DevBotDropdown" type="OptionButton" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/DevSection/DevOpponentRow"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 40) layout_mode = 2 size_flags_horizontal = 3 -[node name="SpectateSeparator" type="HSeparator" parent="CenterContainer/VBoxContainer/DevSection"] +[node name="SpectateSeparator" type="HSeparator" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/DevSection"] layout_mode = 2 -[node name="SpectateHeader" type="Label" parent="CenterContainer/VBoxContainer/DevSection"] +[node name="SpectateHeader" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/DevSection"] layout_mode = 2 theme_override_font_sizes/font_size = 22 text = "Spectate" -[node name="SpectateHint" type="Label" parent="CenterContainer/VBoxContainer/DevSection"] +[node name="SpectateHint" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/DevSection"] modulate = Color(1, 1, 1, 0.55) layout_mode = 2 theme_override_font_sizes/font_size = 13 text = "Watch two bots play each other" -[node name="SpectateRow" type="HBoxContainer" parent="CenterContainer/VBoxContainer/DevSection"] +[node name="SpectateRow" type="HBoxContainer" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/DevSection"] layout_mode = 2 theme_override_constants/separation = 10 -[node name="BotADropdown" type="OptionButton" parent="CenterContainer/VBoxContainer/DevSection/SpectateRow"] +[node name="BotADropdown" type="OptionButton" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/DevSection/SpectateRow"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 40) layout_mode = 2 size_flags_horizontal = 3 -[node name="VsLabel" type="Label" parent="CenterContainer/VBoxContainer/DevSection/SpectateRow"] +[node name="VsLabel" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/DevSection/SpectateRow"] layout_mode = 2 text = "vs" -[node name="BotBDropdown" type="OptionButton" parent="CenterContainer/VBoxContainer/DevSection/SpectateRow"] +[node name="BotBDropdown" type="OptionButton" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/DevSection/SpectateRow"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 40) layout_mode = 2 size_flags_horizontal = 3 -[node name="SpectateButton" type="Button" parent="CenterContainer/VBoxContainer/DevSection"] +[node name="SpectateButton" type="Button" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/DevSection"] custom_minimum_size = Vector2(0, 56) layout_mode = 2 text = "Watch Match" @@ -279,12 +294,12 @@ custom_minimum_size = Vector2(0, 48) layout_mode = 2 text = "Cancel" -[connection signal="pressed" from="CenterContainer/VBoxContainer/FreePlayButton" to="." method="_on_free_play_pressed"] -[connection signal="pressed" from="CenterContainer/VBoxContainer/MatchButton" to="." method="_on_match_pressed"] -[connection signal="pressed" from="CenterContainer/VBoxContainer/FindMatchButton" to="." method="_on_find_match_pressed"] -[connection signal="pressed" from="CenterContainer/VBoxContainer/HostButton" to="." method="_on_host_pressed"] -[connection signal="pressed" from="CenterContainer/VBoxContainer/JoinRow/JoinButton" to="." method="_on_join_pressed"] -[connection signal="text_submitted" from="CenterContainer/VBoxContainer/JoinRow/JoinAddressEdit" to="." method="_on_join_address_submitted"] -[connection signal="pressed" from="CenterContainer/VBoxContainer/SettingsButton" to="." method="_on_settings_pressed"] -[connection signal="pressed" from="CenterContainer/VBoxContainer/DevSection/SpectateButton" to="." method="_on_spectate_pressed"] +[connection signal="pressed" from="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/FreePlayButton" to="." method="_on_free_play_pressed"] +[connection signal="pressed" from="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/MatchButton" to="." method="_on_match_pressed"] +[connection signal="pressed" from="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/FindMatchButton" to="." method="_on_find_match_pressed"] +[connection signal="pressed" from="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/HostButton" to="." method="_on_host_pressed"] +[connection signal="pressed" from="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/JoinRow/JoinButton" to="." method="_on_join_pressed"] +[connection signal="text_submitted" from="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/JoinRow/JoinAddressEdit" to="." method="_on_join_address_submitted"] +[connection signal="pressed" from="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/SettingsButton" to="." method="_on_settings_pressed"] +[connection signal="pressed" from="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/DevSection/SpectateButton" to="." method="_on_spectate_pressed"] [connection signal="pressed" from="ConnectingOverlay/CenterContainer/VBoxContainer/ConnectingCancelButton" to="." method="_on_connecting_cancel_pressed"] diff --git a/Game/scenes/matchmaking.tscn b/Game/scenes/matchmaking.tscn index db039611..032edc15 100644 --- a/Game/scenes/matchmaking.tscn +++ b/Game/scenes/matchmaking.tscn @@ -13,45 +13,59 @@ grow_vertical = 2 script = ExtResource("1_matchmaking") theme = ExtResource("2_theme") -[node name="CenterContainer" type="CenterContainer" parent="."] +[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 = 24 +theme_override_constants/margin_top = 24 +theme_override_constants/margin_right = 24 +theme_override_constants/margin_bottom = 24 -[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer"] +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer"] +layout_mode = 2 +follow_focus = true +horizontal_scroll_mode = 0 + +[node name="CenterContainer" type="CenterContainer" parent="MarginContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/ScrollContainer/CenterContainer"] custom_minimum_size = Vector2(480, 0) layout_mode = 2 theme_override_constants/separation = 12 -[node name="TitleLabel" type="Label" parent="CenterContainer/VBoxContainer"] +[node name="TitleLabel" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] layout_mode = 2 theme_override_font_sizes/font_size = 40 text = "Find a Match" horizontal_alignment = 1 -[node name="PlaylistDropdown" type="OptionButton" parent="CenterContainer/VBoxContainer"] +[node name="PlaylistDropdown" type="OptionButton" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 48) layout_mode = 2 -[node name="StatusLabel" type="Label" parent="CenterContainer/VBoxContainer"] +[node name="StatusLabel" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 22 text = "Ready to search" horizontal_alignment = 1 -[node name="DetailLabel" type="Label" parent="CenterContainer/VBoxContainer"] +[node name="DetailLabel" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] unique_name_in_owner = true modulate = Color(1, 1, 1, 0.65) layout_mode = 2 autowrap_mode = 2 horizontal_alignment = 1 -[node name="RankedProfileLabel" type="Label" parent="CenterContainer/VBoxContainer"] +[node name="RankedProfileLabel" type="Label" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] unique_name_in_owner = true modulate = Color(1, 1, 1, 0.65) layout_mode = 2 @@ -59,24 +73,24 @@ text = "Ranked profile unavailable" horizontal_alignment = 1 visible = false -[node name="QueueButton" type="Button" parent="CenterContainer/VBoxContainer"] +[node name="QueueButton" type="Button" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 52) layout_mode = 2 text = "Search" -[node name="CancelButton" type="Button" parent="CenterContainer/VBoxContainer"] +[node name="CancelButton" type="Button" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 48) layout_mode = 2 text = "Cancel Search" visible = false -[node name="ProposalRow" type="HBoxContainer" parent="CenterContainer/VBoxContainer"] +[node name="ProposalRow" type="HBoxContainer" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] layout_mode = 2 theme_override_constants/separation = 10 -[node name="AcceptButton" type="Button" parent="CenterContainer/VBoxContainer/ProposalRow"] +[node name="AcceptButton" type="Button" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/ProposalRow"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 48) layout_mode = 2 @@ -84,7 +98,7 @@ size_flags_horizontal = 3 text = "Accept" visible = false -[node name="DeclineButton" type="Button" parent="CenterContainer/VBoxContainer/ProposalRow"] +[node name="DeclineButton" type="Button" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/ProposalRow"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 48) layout_mode = 2 @@ -92,14 +106,14 @@ size_flags_horizontal = 3 text = "Decline" visible = false -[node name="BackButton" type="Button" parent="CenterContainer/VBoxContainer"] +[node name="BackButton" type="Button" parent="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 48) layout_mode = 2 text = "Back" -[connection signal="pressed" from="CenterContainer/VBoxContainer/QueueButton" to="." method="_on_queue_pressed"] -[connection signal="pressed" from="CenterContainer/VBoxContainer/CancelButton" to="." method="_on_cancel_pressed"] -[connection signal="pressed" from="CenterContainer/VBoxContainer/ProposalRow/AcceptButton" to="." method="_on_accept_pressed"] -[connection signal="pressed" from="CenterContainer/VBoxContainer/ProposalRow/DeclineButton" to="." method="_on_decline_pressed"] -[connection signal="pressed" from="CenterContainer/VBoxContainer/BackButton" to="." method="_on_back_pressed"] +[connection signal="pressed" from="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/QueueButton" to="." method="_on_queue_pressed"] +[connection signal="pressed" from="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/CancelButton" to="." method="_on_cancel_pressed"] +[connection signal="pressed" from="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/ProposalRow/AcceptButton" to="." method="_on_accept_pressed"] +[connection signal="pressed" from="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/ProposalRow/DeclineButton" to="." method="_on_decline_pressed"] +[connection signal="pressed" from="MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/BackButton" to="." method="_on_back_pressed"] diff --git a/Game/scripts/main_menu.gd b/Game/scripts/main_menu.gd index d0f8dec2..9a5faa42 100644 --- a/Game/scripts/main_menu.gd +++ b/Game/scripts/main_menu.gd @@ -52,7 +52,7 @@ func _ready() -> void: _populate_dropdown(bot_b_dropdown, bots, GameSettings.spectate_bot_b_path) NetworkManager.connected_to_server.connect(_on_connected_to_server) NetworkManager.connection_failed.connect(_on_connection_failed) - $CenterContainer/VBoxContainer/FreePlayButton.grab_focus() + %FreePlayButton.grab_focus() # main_menu.gd's first async flow (task 1.7): Host is synchronous @@ -120,6 +120,11 @@ func _list_bots() -> Array[String]: # disk, else the newest (last) bot. func _populate_dropdown(dropdown: OptionButton, bots: Array[String], preferred_path: String, include_none: bool = false) -> void: dropdown.clear() + # These are filled from whatever checkpoints happen to be in res://bots, so + # a long filename would otherwise widen the OptionButton (size_flags_h = + # EXPAND_FILL) and drag the whole menu past its 420px minimum width. + dropdown.clip_text = true + dropdown.fit_to_longest_item = false if include_none: dropdown.add_item("(Use difficulty)") dropdown.set_item_metadata(0, "") @@ -173,7 +178,7 @@ func _on_match_pressed() -> void: func _on_settings_pressed() -> void: - get_tree().change_scene_to_file("res://scenes/settings.tscn") + get_tree().change_scene_to_file(ScenePaths.SETTINGS) func _on_spectate_pressed() -> void: diff --git a/Game/scripts/scene_paths.gd b/Game/scripts/scene_paths.gd index 9abc87b3..4f53baa5 100644 --- a/Game/scripts/scene_paths.gd +++ b/Game/scripts/scene_paths.gd @@ -1,6 +1,7 @@ class_name ScenePaths const MAIN_MENU := "res://scenes/main_menu.tscn" +const SETTINGS := "res://scenes/settings.tscn" # §6.2 step 10: after RESULTS both peers return HERE, not to the main menu — # a community server whose players are all dumped back to their own menus # every 2.5 minutes has no way to keep a lobby together. diff --git a/Game/tests/cases/test_menu_layout.gd b/Game/tests/cases/test_menu_layout.gd new file mode 100644 index 00000000..4340de08 --- /dev/null +++ b/Game/tests/cases/test_menu_layout.gd @@ -0,0 +1,136 @@ +extends "res://tests/test_case.gd" + +# Guards the menu screens against the overflow bug that made the main menu +# unusable: the layout runs in a hard-fixed 1920x1080 logical viewport +# (window/stretch/mode="viewport"), and a CenterContainer centres its child +# rather than clipping it, so once the content's minimum height passed 1080 the +# top and bottom spilled off-screen with no way to reach them. In a debug build +# the main menu's DevSection pushed it to roughly 1120px, cutting off both the +# title and the last button. +# +# What this file can and cannot do, stated plainly: it asserts the *structure* +# that makes overflow reachable — the bottom-most control of each screen sits +# inside a ScrollContainer, and that container follows focus so keyboard and +# controller navigation cannot strand the player on an off-screen row. It does +# not measure anything, so it cannot prove nothing visually clips; that check is +# manual, at several window sizes. It exists to stop the wrapper being removed +# or a new section being added outside it. +# +# Scenes are inspected through PackedScene.get_state() rather than instantiated. +# main_menu.gd and lobby.gd connect NetworkManager signals and scan res://bots +# in _ready(), so instantiating them in a unit test would be doing real work to +# answer a question about the scene file. + +# scene path -> the control furthest down that screen, i.e. the first thing to +# be lost to overflow. Naming a specific leaf rather than "some ScrollContainer +# exists" is what makes this assertion say something: a wrapper that does not +# actually contain the content would still pass the weaker version. +const DEEPEST_CONTROLS := { + "res://scenes/main_menu.tscn": "SpectateButton", + "res://scenes/settings.tscn": "ResetButton", + "res://scenes/lobby.tscn": "LeaveButton", + "res://scenes/matchmaking.tscn": "BackButton", +} + + +# Returns node index -> "Parent/Path/Name" for every node in the scene state, +# reconstructing full paths from SceneState's parent-relative storage. +func _node_paths(state: SceneState) -> Dictionary: + var paths := {} + for i in state.get_node_count(): + var parent := state.get_node_path(i, true) + var name := String(state.get_node_name(i)) + var parent_str := String(parent) + if parent_str == "." or parent_str == "": + paths[i] = name + else: + paths[i] = "%s/%s" % [parent_str, name] + return paths + + +func _property(state: SceneState, index: int, wanted: String, fallback): + for p in state.get_node_property_count(index): + if String(state.get_node_property_name(index, p)) == wanted: + return state.get_node_property_value(index, p) + return fallback + + +func test_every_menu_scene_loads() -> void: + for scene_path in DEEPEST_CONTROLS: + var scene := load(scene_path) + assert_true(scene is PackedScene, "%s loads as a PackedScene" % scene_path) + + +func test_the_bottom_of_each_menu_sits_inside_a_scroll_container() -> void: + for scene_path in DEEPEST_CONTROLS: + var scene: PackedScene = load(scene_path) + if scene == null: + assert_true(false, "%s failed to load" % scene_path) + continue + var state := scene.get_state() + var paths := _node_paths(state) + + # Collect the paths of every ScrollContainer in the scene... + var scroll_paths := [] + for i in state.get_node_count(): + if String(state.get_node_type(i)) == "ScrollContainer": + scroll_paths.append(paths[i]) + assert_true(not scroll_paths.is_empty(), "%s has a ScrollContainer" % scene_path) + + # ...then require the deepest control to live under one of them. + var wanted: String = DEEPEST_CONTROLS[scene_path] + var found_path := "" + for i in state.get_node_count(): + if String(state.get_node_name(i)) == wanted: + found_path = paths[i] + break + assert_true(found_path != "", "%s contains %s" % [scene_path, wanted]) + if found_path == "": + continue + + var scrolled := false + for scroll_path in scroll_paths: + if found_path.begins_with(String(scroll_path) + "/"): + scrolled = true + break + assert_true(scrolled, "%s's %s is inside a ScrollContainer (at %s)" % [scene_path, wanted, found_path]) + + +func test_menu_scroll_containers_follow_focus() -> void: + # Without follow_focus, grab_focus() on a control below the fold (main_menu + # focuses FreePlayButton on ready) leaves the view showing something else, + # and controller navigation walks focus off-screen silently. + for scene_path in DEEPEST_CONTROLS: + var scene: PackedScene = load(scene_path) + if scene == null: + continue + var state := scene.get_state() + var checked := 0 + for i in state.get_node_count(): + if String(state.get_node_type(i)) != "ScrollContainer": + continue + checked += 1 + assert_true( + _property(state, i, "follow_focus", false) == true, + "%s/%s has follow_focus" % [scene_path, state.get_node_name(i)] + ) + assert_true(checked > 0, "%s has at least one ScrollContainer to check" % scene_path) + + +func test_menu_scroll_containers_do_not_scroll_horizontally() -> void: + # Horizontal scrolling is disabled so content is clamped to the window + # width instead of growing a second scrollbar — the dev bot dropdowns are + # filled from filenames and would otherwise widen the whole menu. + for scene_path in DEEPEST_CONTROLS: + var scene: PackedScene = load(scene_path) + if scene == null: + continue + var state := scene.get_state() + for i in state.get_node_count(): + if String(state.get_node_type(i)) != "ScrollContainer": + continue + assert_eq( + _property(state, i, "horizontal_scroll_mode", ScrollContainer.SCROLL_MODE_AUTO), + ScrollContainer.SCROLL_MODE_DISABLED, + "%s/%s disables horizontal scrolling" % [scene_path, state.get_node_name(i)] + ) diff --git a/Game/tests/cases/test_menu_layout.gd.uid b/Game/tests/cases/test_menu_layout.gd.uid new file mode 100644 index 00000000..1ea82e66 --- /dev/null +++ b/Game/tests/cases/test_menu_layout.gd.uid @@ -0,0 +1 @@ +uid://bs8c31fs0tfhe diff --git a/Game/tests/main_menu_test_hooks.gd b/Game/tests/main_menu_test_hooks.gd index e0744207..948e2c62 100644 --- a/Game/tests/main_menu_test_hooks.gd +++ b/Game/tests/main_menu_test_hooks.gd @@ -35,7 +35,7 @@ func _ready() -> void: func _run_host() -> void: var menu := get_tree().current_scene - var host_btn: Button = menu.get_node("CenterContainer/VBoxContainer/HostButton") + var host_btn: Button = menu.get_node("MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/HostButton") host_btn.emit_signal("pressed") await get_tree().create_timer(1.0).timeout var scene := get_tree().current_scene @@ -58,7 +58,7 @@ func _run_join_ok() -> void: var menu := get_tree().current_scene var address_edit: LineEdit = menu.get_node("%JoinAddressEdit") address_edit.text = "127.0.0.1" - var join_btn: Button = menu.get_node("CenterContainer/VBoxContainer/JoinRow/JoinButton") + var join_btn: Button = menu.get_node("MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/JoinRow/JoinButton") join_btn.emit_signal("pressed") var overlay: Control = menu.get_node("%ConnectingOverlay") print("SMOKE INFO: overlay visible right after Join press = %s" % str(overlay.visible)) @@ -72,7 +72,7 @@ func _run_join_refused() -> void: var menu := get_tree().current_scene var address_edit: LineEdit = menu.get_node("%JoinAddressEdit") address_edit.text = "127.0.0.1" - var join_btn: Button = menu.get_node("CenterContainer/VBoxContainer/JoinRow/JoinButton") + var join_btn: Button = menu.get_node("MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/JoinRow/JoinButton") join_btn.emit_signal("pressed") var overlay: Control = menu.get_node("%ConnectingOverlay") print("SMOKE INFO: overlay visible right after Join press (no server) = %s" % str(overlay.visible)) @@ -92,7 +92,7 @@ func _run_join_cancel() -> void: var menu := get_tree().current_scene var address_edit: LineEdit = menu.get_node("%JoinAddressEdit") address_edit.text = "10.255.255.1" # non-routable; connect attempt just hangs until timeout/cancel - var join_btn: Button = menu.get_node("CenterContainer/VBoxContainer/JoinRow/JoinButton") + var join_btn: Button = menu.get_node("MarginContainer/ScrollContainer/CenterContainer/VBoxContainer/JoinRow/JoinButton") join_btn.emit_signal("pressed") var overlay: Control = menu.get_node("%ConnectingOverlay") await get_tree().create_timer(0.5).timeout