From 8c1987807c8ffe109bf223de19d80eacc3f65dc3 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 6 Oct 2026 10:27:35 +0200 Subject: [PATCH] Make the music list, player list and inspiration list their own scenes MusicListPanel, PlayersPanel (was Players) and InspirationList (was ScrollContainer) each get their own scene and script. They report what happens through signals, and the quiz wires them to the playlist, the music player, the log and the popups. Every node keeps its place on screen. Screenshots of every UI state are pixel-identical to before, the layout test still matches the original baseline, and each part is tested on its own as well as through the quiz. --- InsperationScrollContainer.gd | 38 -- InspirationList.tscn | 16 + MusicListPanel.tscn | 23 + PlayersPanel.tscn | 89 +++ Quiz.tscn | 112 +--- inspiration_list.gd | 67 +++ ...ontainer.gd.uid => inspiration_list.gd.uid | 0 music_list_panel.gd | 26 + music_list_panel.gd.uid | 1 + players_panel.gd | 90 +++ players_panel.gd.uid | 1 + quiz.gd | 523 ++++++++---------- tests/fakes/main_window_test_suite.gd | 6 +- tests/fakes/ui_tour.gd | 15 +- tests/integration/test_inspiration_list.gd | 92 +++ .../integration/test_inspiration_list.gd.uid | 1 + tests/integration/test_layout_unchanged.gd | 12 +- tests/integration/test_main_window.gd | 9 - tests/integration/test_music_list_panel.gd | 79 +++ .../integration/test_music_list_panel.gd.uid | 1 + tests/integration/test_player_keybinds.gd | 2 +- tests/integration/test_players_panel.gd | 146 +++++ tests/integration/test_players_panel.gd.uid | 1 + tests/integration/test_scene_split.gd | 48 ++ 24 files changed, 932 insertions(+), 466 deletions(-) delete mode 100644 InsperationScrollContainer.gd create mode 100644 InspirationList.tscn create mode 100644 MusicListPanel.tscn create mode 100644 PlayersPanel.tscn create mode 100644 inspiration_list.gd rename InsperationScrollContainer.gd.uid => inspiration_list.gd.uid (100%) create mode 100644 music_list_panel.gd create mode 100644 music_list_panel.gd.uid create mode 100644 players_panel.gd create mode 100644 players_panel.gd.uid create mode 100644 tests/integration/test_inspiration_list.gd create mode 100644 tests/integration/test_inspiration_list.gd.uid create mode 100644 tests/integration/test_music_list_panel.gd create mode 100644 tests/integration/test_music_list_panel.gd.uid create mode 100644 tests/integration/test_players_panel.gd create mode 100644 tests/integration/test_players_panel.gd.uid diff --git a/InsperationScrollContainer.gd b/InsperationScrollContainer.gd deleted file mode 100644 index aa83b8b..0000000 --- a/InsperationScrollContainer.gd +++ /dev/null @@ -1,38 +0,0 @@ -extends ScrollContainer - -# Scroll direction: 1 = down, -1 = up -var scroll_direction: int = 1 - -# Called when the node enters the scene tree for the first time. -func _ready() -> void: - scroll_direction = 1 - -# Called every frame. 'delta' is the elapsed time since the previous frame. -func _process(delta: float) -> void: - var speed_pixels_per_second: float = float(Settings.inspiration_list_speed) * 60.0 - var scroll_amount: float = speed_pixels_per_second * delta * float(scroll_direction) - - var scroll_bar = get_v_scroll_bar() - - # Don't scroll if list is empty or too short - if scroll_bar.max_value <= 0: - return - - var current_pos = scroll_bar.value - - if scroll_direction == 1: - # Scrolling down - if current_pos + scroll_bar.page < scroll_bar.max_value: - scroll_bar.value = min(current_pos + scroll_amount, scroll_bar.max_value - scroll_bar.page) - else: - # Reached bottom, reverse direction - scroll_direction = -1 - scroll_bar.value = scroll_bar.max_value - scroll_bar.page - else: - # Scrolling up - if current_pos > 0: - scroll_bar.value = max(current_pos + scroll_amount, 0) - else: - # Reached top, reverse direction - scroll_direction = 1 - scroll_bar.value = 0 diff --git a/InspirationList.tscn b/InspirationList.tscn new file mode 100644 index 0000000..4ec42ea --- /dev/null +++ b/InspirationList.tscn @@ -0,0 +1,16 @@ +[gd_scene format=3] + +[ext_resource type="Script" uid="uid://elgn7gkxgagq" path="res://inspiration_list.gd" id="2_gxtxm"] + +[node name="InspirationList" type="ScrollContainer" unique_id=498283761] +layout_mode = 3 +horizontal_scroll_mode = 0 +vertical_scroll_mode = 3 +offset_right = 641.0 +offset_bottom = 384.0 +script = ExtResource("2_gxtxm") + +[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1215162949] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 diff --git a/MusicListPanel.tscn b/MusicListPanel.tscn new file mode 100644 index 0000000..e5c89e6 --- /dev/null +++ b/MusicListPanel.tscn @@ -0,0 +1,23 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://music_list_panel.gd" id="part_script"] +[ext_resource type="Script" uid="uid://gf1bk6xbe3j" path="res://MusicListScrollContainer.gd" id="7_dj026"] +[ext_resource type="Theme" uid="uid://rxexo3ur85as" path="res://LightGrayTheme.tres" id="7_wxbv6"] + +[node name="MusicListPanel" type="PanelContainer" unique_id=2011292375] +layout_mode = 3 +theme = ExtResource("7_wxbv6") +offset_right = 1088.0 +offset_bottom = 512.0 +script = ExtResource("part_script") + +[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=707750459] +layout_mode = 2 +follow_focus = true +script = ExtResource("7_dj026") + +[node name="MusicList" type="VBoxContainer" parent="ScrollContainer" unique_id=373398568] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/separation = 10 diff --git a/PlayersPanel.tscn b/PlayersPanel.tscn new file mode 100644 index 0000000..db43c0b --- /dev/null +++ b/PlayersPanel.tscn @@ -0,0 +1,89 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://players_panel.gd" id="part_script"] +[ext_resource type="Texture2D" uid="uid://bcfmpd7h512ef" path="res://icons/person_add_light.svg" id="5_31tjv"] +[ext_resource type="Script" uid="uid://cgakje8gb37tf" path="res://PlayerNameField.gd" id="7_qsdfy"] + +[sub_resource type="InputEventKey" id="InputEventKey_03bm3"] +device = -1 +alt_pressed = true +keycode = 65 +unicode = 63743 + +[sub_resource type="Shortcut" id="Shortcut_jafqj"] +events = [SubResource("InputEventKey_03bm3")] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qk7sj"] +bg_color = Color(0.180392, 0.180392, 0.180392, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_k1ygi"] +bg_color = Color(0.180392, 0.180392, 0.180392, 1) + +[node name="PlayersPanel" type="PanelContainer" unique_id=1708375945] +layout_mode = 3 +offset_right = 641.0 +offset_bottom = 401.0 +script = ExtResource("part_script") + +[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=270084211] +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer" unique_id=513047023] +custom_minimum_size = Vector2(2.08165e-12, 40) +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer" unique_id=1331357386] +custom_minimum_size = Vector2(2.08165e-12, 40) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 1 +text = "Players" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="AddPlayersButton" type="TextureButton" parent="VBoxContainer/HBoxContainer" unique_id=634410148] +custom_minimum_size = Vector2(2.08165e-12, 40) +layout_mode = 2 +size_flags_horizontal = 8 +focus_mode = 0 +action_mode = 0 +shortcut = SubResource("Shortcut_jafqj") +texture_normal = ExtResource("5_31tjv") + +[node name="AddPlayerContainer" type="HBoxContainer" parent="VBoxContainer" unique_id=1371948739] +visible = false +custom_minimum_size = Vector2(2.08165e-12, 40) +layout_mode = 2 + +[node name="Panel" type="Panel" parent="VBoxContainer/AddPlayerContainer" unique_id=1569854056] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_styles/panel = SubResource("StyleBoxFlat_qk7sj") + +[node name="PlayerNameField" type="TextEdit" parent="VBoxContainer/AddPlayerContainer" unique_id=332535327] +custom_minimum_size = Vector2(2.08165e-12, 40) +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "Player name" +script = ExtResource("7_qsdfy") + +[node name="AddPlayerButton" type="Button" parent="VBoxContainer/AddPlayerContainer" unique_id=245001313] +custom_minimum_size = Vector2(2.08165e-12, 40) +layout_mode = 2 +text = "Add" + +[node name="Panel2" type="Panel" parent="VBoxContainer/AddPlayerContainer" unique_id=60215448] +custom_minimum_size = Vector2(2.08165e-12, 40) +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_styles/panel = SubResource("StyleBoxFlat_k1ygi") + +[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer" unique_id=1000000001] +layout_mode = 2 +size_flags_vertical = 15 +horizontal_scroll_mode = 0 + +[node name="PlayersListVBoxContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer" unique_id=1000000002] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 15 diff --git a/Quiz.tscn b/Quiz.tscn index 88bd1d8..4df21c6 100644 --- a/Quiz.tscn +++ b/Quiz.tscn @@ -1,16 +1,14 @@ [gd_scene format=3] [ext_resource type="Script" path="res://quiz.gd" id="quiz_script"] -[ext_resource type="Script" uid="uid://elgn7gkxgagq" path="res://InsperationScrollContainer.gd" id="2_gxtxm"] -[ext_resource type="Texture2D" uid="uid://bcfmpd7h512ef" path="res://icons/person_add_light.svg" id="5_31tjv"] [ext_resource type="PackedScene" uid="uid://ds15cgsf8vpvc" path="res://MusicPlayer.tscn" id="5_emn36"] [ext_resource type="PackedScene" uid="uid://cdy4kvemwaiom" path="res://new_character_select.tscn" id="6_dhrvg"] -[ext_resource type="Script" uid="uid://gf1bk6xbe3j" path="res://MusicListScrollContainer.gd" id="7_dj026"] -[ext_resource type="Script" uid="uid://cgakje8gb37tf" path="res://PlayerNameField.gd" id="7_qsdfy"] -[ext_resource type="Theme" uid="uid://rxexo3ur85as" path="res://LightGrayTheme.tres" id="7_wxbv6"] [ext_resource type="PackedScene" uid="uid://dldpeo5y3l5hq" path="res://SettingsWindow.tscn" id="11_k62u5"] [ext_resource type="PackedScene" uid="uid://bijh5h5yrivm3" path="res://Log.tscn" id="14_26rwn"] [ext_resource type="PackedScene" uid="uid://cxlsmxnrp0agh" path="res://WinnerWindow.tscn" id="17_winner_window"] +[ext_resource type="PackedScene" path="res://MusicListPanel.tscn" id="scene_MusicListPanel"] +[ext_resource type="PackedScene" path="res://PlayersPanel.tscn" id="scene_PlayersPanel"] +[ext_resource type="PackedScene" path="res://InspirationList.tscn" id="scene_InspirationList"] [sub_resource type="LabelSettings" id="LabelSettings_qspbu"] font_size = 50 @@ -18,21 +16,6 @@ font_size = 50 [sub_resource type="LabelSettings" id="LabelSettings_3m52w"] font_size = 35 -[sub_resource type="InputEventKey" id="InputEventKey_03bm3"] -device = -1 -alt_pressed = true -keycode = 65 -unicode = 63743 - -[sub_resource type="Shortcut" id="Shortcut_jafqj"] -events = [SubResource("InputEventKey_03bm3")] - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qk7sj"] -bg_color = Color(0.180392, 0.180392, 0.180392, 1) - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_k1ygi"] -bg_color = Color(0.180392, 0.180392, 0.180392, 1) - [sub_resource type="InputEventKey" id="InputEventKey_06rl4"] device = -1 alt_pressed = true @@ -79,39 +62,19 @@ file_mode = 2 access = 2 show_hidden_files = true -[node name="MusicListPanel" type="PanelContainer" parent="." unique_id=2011292375] +[node name="MusicListPanel" parent="." unique_id=2011292375 instance=ExtResource("scene_MusicListPanel")] layout_mode = 0 offset_left = 64.0 offset_top = 384.0 offset_right = 1152.0 offset_bottom = 896.0 -theme = ExtResource("7_wxbv6") -[node name="ScrollContainer" type="ScrollContainer" parent="MusicListPanel" unique_id=707750459] -layout_mode = 2 -follow_focus = true -script = ExtResource("7_dj026") - -[node name="MusicList" type="VBoxContainer" parent="MusicListPanel/ScrollContainer" unique_id=373398568] -layout_mode = 2 -size_flags_horizontal = 3 -size_flags_vertical = 3 -theme_override_constants/separation = 10 - -[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=498283761] +[node name="InspirationList" parent="." unique_id=498283761 instance=ExtResource("scene_InspirationList")] layout_mode = 0 offset_left = 1216.0 offset_top = 128.0 offset_right = 1857.0 offset_bottom = 512.0 -horizontal_scroll_mode = 0 -vertical_scroll_mode = 3 -script = ExtResource("2_gxtxm") - -[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer" unique_id=1215162949] -layout_mode = 2 -size_flags_horizontal = 3 -size_flags_vertical = 3 [node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=42468498] layout_mode = 0 @@ -148,76 +111,13 @@ offset_bottom = 1024.0 grow_horizontal = 1 grow_vertical = 1 -[node name="Players" type="PanelContainer" parent="." unique_id=1708375945] +[node name="PlayersPanel" parent="." unique_id=1708375945 instance=ExtResource("scene_PlayersPanel")] layout_mode = 0 offset_left = 1216.0 offset_top = 576.0 offset_right = 1857.0 offset_bottom = 977.0 -[node name="VBoxContainer" type="VBoxContainer" parent="Players" unique_id=270084211] -layout_mode = 2 - -[node name="HBoxContainer" type="HBoxContainer" parent="Players/VBoxContainer" unique_id=513047023] -custom_minimum_size = Vector2(2.08165e-12, 40) -layout_mode = 2 - -[node name="Label" type="Label" parent="Players/VBoxContainer/HBoxContainer" unique_id=1331357386] -custom_minimum_size = Vector2(2.08165e-12, 40) -layout_mode = 2 -size_flags_horizontal = 3 -size_flags_vertical = 1 -text = "Players" -horizontal_alignment = 1 -vertical_alignment = 1 - -[node name="AddPlayersButton" type="TextureButton" parent="Players/VBoxContainer/HBoxContainer" unique_id=634410148] -custom_minimum_size = Vector2(2.08165e-12, 40) -layout_mode = 2 -size_flags_horizontal = 8 -focus_mode = 0 -action_mode = 0 -shortcut = SubResource("Shortcut_jafqj") -texture_normal = ExtResource("5_31tjv") - -[node name="AddPlayerContainer" type="HBoxContainer" parent="Players/VBoxContainer" unique_id=1371948739] -visible = false -custom_minimum_size = Vector2(2.08165e-12, 40) -layout_mode = 2 - -[node name="Panel" type="Panel" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=1569854056] -layout_mode = 2 -size_flags_horizontal = 3 -theme_override_styles/panel = SubResource("StyleBoxFlat_qk7sj") - -[node name="PlayerNameField" type="TextEdit" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=332535327] -custom_minimum_size = Vector2(2.08165e-12, 40) -layout_mode = 2 -size_flags_horizontal = 3 -placeholder_text = "Player name" -script = ExtResource("7_qsdfy") - -[node name="AddPlayerButton" type="Button" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=245001313] -custom_minimum_size = Vector2(2.08165e-12, 40) -layout_mode = 2 -text = "Add" - -[node name="Panel2" type="Panel" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=60215448] -custom_minimum_size = Vector2(2.08165e-12, 40) -layout_mode = 2 -size_flags_horizontal = 3 -theme_override_styles/panel = SubResource("StyleBoxFlat_k1ygi") - -[node name="ScrollContainer" type="ScrollContainer" parent="Players/VBoxContainer" unique_id=1000000001] -layout_mode = 2 -size_flags_vertical = 15 -horizontal_scroll_mode = 0 - -[node name="PlayersListVBoxContainer" type="VBoxContainer" parent="Players/VBoxContainer/ScrollContainer" unique_id=1000000002] -layout_mode = 2 -size_flags_horizontal = 3 -size_flags_vertical = 15 - [node name="ResetPlaylistButton" type="Button" parent="." unique_id=1754155620] layout_mode = 0 offset_left = 1360.0 diff --git a/inspiration_list.gd b/inspiration_list.gd new file mode 100644 index 0000000..3ad9a4a --- /dev/null +++ b/inspiration_list.gd @@ -0,0 +1,67 @@ +class_name InspirationList +extends ScrollContainer +## The list of every soundtrack, shown for inspiration. It scrolls slowly up and down +## by itself, at the speed set in the settings. + +## The soundtracks shown, in order. +var soundtracks: Array = [] + +# Scroll direction: 1 = down, -1 = up +var scroll_direction: int = 1 + +@onready +var _rows: VBoxContainer = $VBoxContainer + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + scroll_direction = 1 + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _process(delta: float) -> void: + var speed_pixels_per_second: float = float(Settings.inspiration_list_speed) * 60.0 + var scroll_amount: float = speed_pixels_per_second * delta * float(scroll_direction) + + var scroll_bar = get_v_scroll_bar() + + # Don't scroll if list is empty or too short + if scroll_bar.max_value <= 0: + return + + var current_pos = scroll_bar.value + + if scroll_direction == 1: + # Scrolling down + if current_pos + scroll_bar.page < scroll_bar.max_value: + scroll_bar.value = min(current_pos + scroll_amount, scroll_bar.max_value - scroll_bar.page) + else: + # Reached bottom, reverse direction + scroll_direction = -1 + scroll_bar.value = scroll_bar.max_value - scroll_bar.page + else: + # Scrolling up + if current_pos > 0: + scroll_bar.value = max(current_pos + scroll_amount, 0) + else: + # Reached top, reverse direction + scroll_direction = 1 + scroll_bar.value = 0 + + +## Fetches every soundtrack from the server and shows them. +func load_from_server() -> void: + print("load_from_server") + var populate_list: Callable = func populate_list(array) -> void: + if typeof(array) == TYPE_ARRAY: + show_soundtracks(array) + else: + print("Unexpected data") + MusicServerClient.fetch_all_soundtracks(populate_list) + + +## Shows [param list] instead of the soundtracks shown now. +func show_soundtracks(list: Array) -> void: + soundtracks = [] + soundtracks.append_array(list) + Settings.delete_children(_rows) + for i: int in soundtracks.size(): + _rows.add_child(ListRow.create(soundtracks[i], i, 20)) diff --git a/InsperationScrollContainer.gd.uid b/inspiration_list.gd.uid similarity index 100% rename from InsperationScrollContainer.gd.uid rename to inspiration_list.gd.uid diff --git a/music_list_panel.gd b/music_list_panel.gd new file mode 100644 index 0000000..fdb0687 --- /dev/null +++ b/music_list_panel.gd @@ -0,0 +1,26 @@ +class_name MusicListPanel +extends PanelContainer +## The list of songs in the playlist, one row per song. + +## Emitted when the row of song number [param song_no] is clicked. +signal song_clicked(song_no: int) + +var _song_list_object: PackedScene = preload("res://song_list_object.tscn") + +@onready +var _rows: VBoxContainer = $ScrollContainer/MusicList + + +## Shows the songs in the playlist again, so it shows their latest state. +func refresh() -> void: + Settings.delete_children(_rows) + for s: SongObject in Playlist.get_playlist(): + var new_song_list_object: SongListObject = _song_list_object.instantiate() + new_song_list_object.song_object = s + new_song_list_object.gui_input.connect(_on_row_input.bind(s.song_number)) + _rows.add_child(new_song_list_object) + + +func _on_row_input(event: InputEvent, song_no: int) -> void: + if event is InputEventMouseButton && event.pressed && event.button_index == MOUSE_BUTTON_LEFT: + song_clicked.emit(song_no) diff --git a/music_list_panel.gd.uid b/music_list_panel.gd.uid new file mode 100644 index 0000000..2b22754 --- /dev/null +++ b/music_list_panel.gd.uid @@ -0,0 +1 @@ +uid://b8e27uui01tt diff --git a/players_panel.gd b/players_panel.gd new file mode 100644 index 0000000..96a51e1 --- /dev/null +++ b/players_panel.gd @@ -0,0 +1,90 @@ +class_name PlayersPanel +extends PanelContainer +## The players and their points, with a row for adding new players. +## +## The players themselves live in [code]Settings.player_array[/code]; this panel shows +## them and adds and removes them there. + +## Emitted when a player has been added to [code]Settings.player_array[/code], before +## the list shows it. +signal player_added(player_object: PlayerObject) + +## Emitted when a player has been removed from [code]Settings.player_array[/code]. +signal player_removed(player_object: PlayerObject) + +## Emitted when the character button in [param row] is pressed. +signal change_character_requested(row: Player, player_name: String) + +var _player_row: PackedScene = preload("res://Player.tscn") + +@onready +var add_players_button: TextureButton = $VBoxContainer/HBoxContainer/AddPlayersButton + +@onready +var add_player_container: HBoxContainer = $VBoxContainer/AddPlayerContainer + +@onready +var add_player_button: Button = $VBoxContainer/AddPlayerContainer/AddPlayerButton + +@onready +var new_player_name_field: TextEdit = $VBoxContainer/AddPlayerContainer/PlayerNameField + +@onready +var players_list_container: VBoxContainer = $VBoxContainer/ScrollContainer/PlayersListVBoxContainer + + +func _ready() -> void: + add_players_button.pressed.connect(add_players) + add_player_button.pressed.connect(add_player) + new_player_name_field.connect("enter_key_pressed", add_player) + + +## Shows or hides the row for adding players, and the buttons for removing them. +func add_players() -> void: + print("add_players") + Settings.edit_players = !Settings.edit_players + add_player_container.visible = !add_player_container.visible + new_player_name_field.grab_focus() + + +## Adds a player with the name in the name field, unless it is empty or there are +## already as many players as there can be. +func add_player() -> void: + print("add_player") + if new_player_name_field.text != "" && Settings.player_array.size() < Settings.MAX_NUMBER_OF_PLAYERS: + var new_player_object: PlayerObject = PlayerObject.new(new_player_name_field.text) + new_player_name_field.text = "" + Settings.player_array.append(new_player_object) + player_added.emit(new_player_object) + reload() + + +## Shows the players in [code]Settings.player_array[/code] again, e.g. after their +## points were reset. +func reload() -> void: + print("load_players") + Settings.delete_children(players_list_container) + var counter: int = 0 + for player_object: PlayerObject in Settings.player_array: + print(player_object.player_name) + + var new_player: Player = _player_row.instantiate() + new_player.player_object = player_object + new_player.list_index = counter + + new_player.connect("change_character_clicked", _on_change_character_clicked.bind(new_player, player_object.player_name)) + new_player.connect("player_removed", _on_player_removed.bind(player_object)) + + players_list_container.add_child(new_player) + counter += 1 + + +func _on_change_character_clicked(row: Player, player_name: String) -> void: + change_character_requested.emit(row, player_name) + + +func _on_player_removed(player_object: PlayerObject) -> void: + print("_on_player_removed ", player_object.player_name) + Settings.player_array.erase(player_object) + reload() + player_removed.emit(player_object) diff --git a/players_panel.gd.uid b/players_panel.gd.uid new file mode 100644 index 0000000..509afde --- /dev/null +++ b/players_panel.gd.uid @@ -0,0 +1 @@ +uid://ctayn0soyatfk diff --git a/quiz.gd b/quiz.gd index 071f4ce..f2c5b9d 100644 --- a/quiz.gd +++ b/quiz.gd @@ -18,10 +18,7 @@ var open_button: Button = $Open var fileDialog: FileDialog = $FileDialog @onready -var inspiration_scroll: ScrollContainer = $ScrollContainer - -@onready -var inspiration_list: VBoxContainer = $ScrollContainer/VBoxContainer +var inspiration_list: InspirationList = $InspirationList @onready var soundtrack_label: Label = $VBoxContainer/SoundtrackLabel @@ -30,16 +27,7 @@ var soundtrack_label: Label = $VBoxContainer/SoundtrackLabel var song_label: Label = $VBoxContainer/SongLabel @onready -var add_player_container: HBoxContainer = $Players/VBoxContainer/AddPlayerContainer - -@onready -var add_players_button: TextureButton = $Players/VBoxContainer/HBoxContainer/AddPlayersButton - -@onready -var add_player_button: Button = $Players/VBoxContainer/AddPlayerContainer/AddPlayerButton - -@onready -var new_player_name_field: TextEdit = $Players/VBoxContainer/AddPlayerContainer/PlayerNameField +var players_panel: PlayersPanel = $PlayersPanel @onready var reset_playlist_button: Button = $ResetPlaylistButton @@ -66,16 +54,7 @@ var show_answer_button: Button = $ShowAnswerButton var next_button: Button = $NextButton @onready -var music_list_scroll: ScrollContainer = $MusicListPanel/ScrollContainer - -@onready -var music_list: VBoxContainer = $MusicListPanel/ScrollContainer/MusicList - -@onready -var player_container: VBoxContainer = $Players/VBoxContainer - -@onready -var players_list_container: VBoxContainer = $Players/VBoxContainer/ScrollContainer/PlayersListVBoxContainer +var music_list_panel: MusicListPanel = $MusicListPanel @onready var new_character_select: Control = %NewCharacterSelect @@ -92,11 +71,7 @@ var log_window: Control = %Log @onready var debug_label: Label = $DebugLabel -var player: PackedScene = preload("res://Player.tscn") -var song_list_object: PackedScene = preload("res://song_list_object.tscn") - var songs: Array= [] -var soundtracks: Array = [] var current_player: Node ## True while statistics are off because a player won, until points are reset. @@ -110,354 +85,302 @@ var song_fetch_retry_seconds: float = 5.0 func _ready() -> void: - popup_backdrop = PopupBackdrop.add_to(self, _popups()) - if OS.has_feature("debug"): - print("is_debug") - debug_label.visible = true - Settings.is_debug = true - settings_window.set_winning_score(5) - settings_window.set_selected_server(4) - next_button.pressed.connect(play_next_song) - show_answer_button.pressed.connect(show_answer_pressed) - sound_test_button.pressed.connect(music_player_container.get_sound_test_song) - settings_button.pressed.connect(show_settings) - reset_playlist_button.pressed.connect(reset_playlist) - reset_points_button.pressed.connect(reset_points) + popup_backdrop = PopupBackdrop.add_to(self, _popups()) + if OS.has_feature("debug"): + print("is_debug") + debug_label.visible = true + Settings.is_debug = true + settings_window.set_winning_score(5) + settings_window.set_selected_server(4) + next_button.pressed.connect(play_next_song) + show_answer_button.pressed.connect(show_answer_pressed) + sound_test_button.pressed.connect(music_player_container.get_sound_test_song) + settings_button.pressed.connect(show_settings) + reset_playlist_button.pressed.connect(reset_playlist) + reset_points_button.pressed.connect(reset_points) - new_character_select.connect("new_new_character_selected", _on_new_character_selected) - new_player_name_field.connect("enter_key_pressed", add_player) - music_player_container.connect("play_next_song", play_next_song) - music_player_container.connect("update_song_list", update_song_list) - settings_window.connect("server_changed", _on_server_changed) - settings_window.visibility_changed.connect(_on_settings_window_visibility_changed) + new_character_select.connect("new_new_character_selected", _on_new_character_selected) + music_list_panel.song_clicked.connect(song_object_clicked) + players_panel.player_added.connect(_on_player_added) + players_panel.player_removed.connect(_on_player_removed) + players_panel.change_character_requested.connect(_on_player_change_character_clicked) + music_player_container.connect("play_next_song", play_next_song) + music_player_container.connect("update_song_list", update_song_list) + settings_window.connect("server_changed", _on_server_changed) + settings_window.visibility_changed.connect(_on_settings_window_visibility_changed) - add_players_button.pressed.connect(add_players) - add_player_button.pressed.connect(add_player) - open_button.pressed.connect(open) - auto_repeat_song_button.pressed.connect(repeat_song) + open_button.pressed.connect(open) + auto_repeat_song_button.pressed.connect(repeat_song) - get_suggestion_list() - fetch_song_list_at_start() - new_character_select.load_characters() + inspiration_list.load_from_server() + fetch_song_list_at_start() + new_character_select.load_characters() func _input(event: InputEvent) -> void: - if event is InputEventKey and event.pressed: - if event.alt_pressed && event.keycode == KEY_UP: - print("Alt + UP pressed") - music_player_container.change_volume(0.05) - if event.alt_pressed && event.keycode == KEY_DOWN: - print("Alt + DOWN pressed") - music_player_container.change_volume(-0.05) - if event.alt_pressed && event.ctrl_pressed && event.keycode == KEY_LEFT: - print("Ctrl + Alt + LEFT pressed") - music_player_container.restart() - elif event.alt_pressed && event.keycode == KEY_LEFT: - print("Alt + LEFT pressed") - music_player_container.seek(-5) - if event.alt_pressed && event.keycode == KEY_RIGHT: - print("Alt + RIGHT pressed") - music_player_container.seek(5) - if event.alt_pressed && event.keycode == KEY_L: - log_window.toggle() + if event is InputEventKey and event.pressed: + if event.alt_pressed && event.keycode == KEY_UP: + print("Alt + UP pressed") + music_player_container.change_volume(0.05) + if event.alt_pressed && event.keycode == KEY_DOWN: + print("Alt + DOWN pressed") + music_player_container.change_volume(-0.05) + if event.alt_pressed && event.ctrl_pressed && event.keycode == KEY_LEFT: + print("Ctrl + Alt + LEFT pressed") + music_player_container.restart() + elif event.alt_pressed && event.keycode == KEY_LEFT: + print("Alt + LEFT pressed") + music_player_container.seek(-5) + if event.alt_pressed && event.keycode == KEY_RIGHT: + print("Alt + RIGHT pressed") + music_player_container.seek(5) + if event.alt_pressed && event.keycode == KEY_L: + log_window.toggle() ## Disables the buttons that use the song list while the server syncs it. func lock_for_sync() -> void: - sound_test_button.disabled = true - reset_playlist_button.disabled = true - show_answer_button.disabled = true - next_button.disabled = true + sound_test_button.disabled = true + reset_playlist_button.disabled = true + show_answer_button.disabled = true + next_button.disabled = true ## Reloads from the server after a sync and starts a new game. func on_sync_finished() -> void: - refresh_from_server() - sound_test_button.disabled = false - reset_playlist_button.disabled = false - show_answer_button.disabled = false - reset_points() + refresh_from_server() + sound_test_button.disabled = false + reset_playlist_button.disabled = false + show_answer_button.disabled = false + reset_points() ## Reloads the soundtracks, playlist and characters from the server. func refresh_from_server() -> void: - print("refresh_from_server") - get_suggestion_list() - reset_playlist() - new_character_select.load_characters() + print("refresh_from_server") + inspiration_list.load_from_server() + reset_playlist() + new_character_select.load_characters() func _on_server_changed() -> void: - print("_on_server_changed") - refresh_from_server() - reset_points() - server_changed.emit() + print("_on_server_changed") + refresh_from_server() + reset_points() + server_changed.emit() func repeat_song() -> void: - Settings.auto_repeat_song = !Settings.auto_repeat_song + Settings.auto_repeat_song = !Settings.auto_repeat_song func show_answer_pressed() -> void: - print("show_answer_pressed") - Playlist.song_is_answered() - update_song_list() + print("show_answer_pressed") + Playlist.song_is_answered() + update_song_list() # Loads the songs already in the server playlist, then starts preloading new ones. # Preloading waits for the list, because loading the list clears the playlist. func fetch_song_list_at_start() -> void: - print("fetch_song_list_at_start") - var fetch_song_list_at_start_done: Callable = func fetch_songs_at_start_done(data) -> void: - Playlist.clear_song_list() - if typeof(data) == TYPE_ARRAY: - for d: Dictionary in data: - var new_song_object: SongObject = SongObject.new() - new_song_object.song_number = d.SongNo - new_song_object.soundtrack_title = d.Soundtrack - new_song_object.song_title = d.Song - new_song_object.is_answered = true - new_song_object.has_played = true - Playlist.add_to_playlist(new_song_object) - elif data != null: - print("Unexpected data") - Playlist.update_last_played_song() - update_song_list() - if !Playlist.is_song_downloading: - fetch_song() - MusicServerClient.fetch_song_list(fetch_song_list_at_start_done, - _retry_later.bind(fetch_song_list_at_start)) + print("fetch_song_list_at_start") + var fetch_song_list_at_start_done: Callable = func fetch_songs_at_start_done(data) -> void: + Playlist.clear_song_list() + if typeof(data) == TYPE_ARRAY: + for d: Dictionary in data: + var new_song_object: SongObject = SongObject.new() + new_song_object.song_number = d.SongNo + new_song_object.soundtrack_title = d.Soundtrack + new_song_object.song_title = d.Song + new_song_object.is_answered = true + new_song_object.has_played = true + Playlist.add_to_playlist(new_song_object) + elif data != null: + print("Unexpected data") + Playlist.update_last_played_song() + update_song_list() + if !Playlist.is_song_downloading: + fetch_song() + MusicServerClient.fetch_song_list(fetch_song_list_at_start_done, + _retry_later.bind(fetch_song_list_at_start)) func play_next_song() -> void: - print("play_next_song ", Playlist.latest_played_song) - if Playlist.first_song_played: - Playlist.set_latest_played_song() - Playlist.first_song_played = true - music_player_container.play_song_object(Playlist.latest_played_song) - if !(Playlist.get_preloaded_songs() > 0): - next_button.disabled = true - if !Playlist.is_song_downloading: - fetch_song() + print("play_next_song ", Playlist.latest_played_song) + if Playlist.first_song_played: + Playlist.set_latest_played_song() + Playlist.first_song_played = true + music_player_container.play_song_object(Playlist.latest_played_song) + if !(Playlist.get_preloaded_songs() > 0): + next_button.disabled = true + if !Playlist.is_song_downloading: + fetch_song() func _popups() -> Array[PopupWindow]: - return [settings_window, new_character_select, winner_window, log_window] + return [settings_window, new_character_select, winner_window, log_window] # The cache size can be changed in the settings. Waiting until the settings are closed # avoids starting downloads while the user is still clicking the number up and down. func _on_settings_window_visibility_changed() -> void: - if !settings_window.visible && !Playlist.is_song_downloading && Playlist.should_preload_more_songs(): - fetch_song() + if !settings_window.visible && !Playlist.is_song_downloading && Playlist.should_preload_more_songs(): + fetch_song() func fetch_song() -> void: - print("fetch_song") - Playlist.is_song_downloading = true - _download_random_song(SongObject.new()) + print("fetch_song") + Playlist.is_song_downloading = true + _download_random_song(SongObject.new()) # Fetching a song takes three requests. If one fails, only that step is retried, so # the server and the game keep the same songs in their playlists. func _download_random_song(new_song_object: SongObject) -> void: - var song_downloaded: Callable = func song_downloaded(body: PackedByteArray) -> void: - print("song_downloaded") - var sound: AudioStream = AudioStreamMP3.new() - sound.data = body - new_song_object.song = sound - _add_song_to_queue(new_song_object) - MusicServerClient.fetch_random_song(Settings.use_low_played_mode, song_downloaded, - _retry_later.bind(_download_random_song.bind(new_song_object))) + var song_downloaded: Callable = func song_downloaded(body: PackedByteArray) -> void: + print("song_downloaded") + var sound: AudioStream = AudioStreamMP3.new() + sound.data = body + new_song_object.song = sound + _add_song_to_queue(new_song_object) + MusicServerClient.fetch_random_song(Settings.use_low_played_mode, song_downloaded, + _retry_later.bind(_download_random_song.bind(new_song_object))) func _add_song_to_queue(new_song_object: SongObject) -> void: - MusicServerClient.add_to_queue(_fetch_song_info.bind(new_song_object), - _retry_later.bind(_add_song_to_queue.bind(new_song_object))) + MusicServerClient.add_to_queue(_fetch_song_info.bind(new_song_object), + _retry_later.bind(_add_song_to_queue.bind(new_song_object))) func _fetch_song_info(new_song_object: SongObject) -> void: - var song_info_fetched: Callable = func song_info_fetched(data_received: Dictionary) -> void: - print("song_info_fetched") - new_song_object.song_number = data_received.SongNo - new_song_object.soundtrack_title = data_received.Soundtrack - new_song_object.song_title = data_received.Song - print("new_song_object: ", new_song_object.to_string()) - Playlist.add_to_playlist(new_song_object) - next_button.disabled = false - if Playlist.should_preload_more_songs(): - fetch_song() - else: - Playlist.is_song_downloading = false - if Settings.is_debug: - update_song_list() - MusicServerClient.fetch_song_info(song_info_fetched, - _retry_later.bind(_fetch_song_info.bind(new_song_object))) + var song_info_fetched: Callable = func song_info_fetched(data_received: Dictionary) -> void: + print("song_info_fetched") + new_song_object.song_number = data_received.SongNo + new_song_object.soundtrack_title = data_received.Soundtrack + new_song_object.song_title = data_received.Song + print("new_song_object: ", new_song_object.to_string()) + Playlist.add_to_playlist(new_song_object) + next_button.disabled = false + if Playlist.should_preload_more_songs(): + fetch_song() + else: + Playlist.is_song_downloading = false + if Settings.is_debug: + update_song_list() + MusicServerClient.fetch_song_info(song_info_fetched, + _retry_later.bind(_fetch_song_info.bind(new_song_object))) func _retry_later(step: Callable) -> void: - print("Song download failed, retrying in %s seconds" % song_fetch_retry_seconds) - get_tree().create_timer(song_fetch_retry_seconds).timeout.connect(step) + print("Song download failed, retrying in %s seconds" % song_fetch_retry_seconds) + get_tree().create_timer(song_fetch_retry_seconds).timeout.connect(step) func update_song_list() -> void: - print("update_song_list") - if Playlist.currently_playing_song >= 0: - soundtrack_label.text = Playlist.get_current_soundtrack_title() - song_label.text = Playlist.get_current_song_title() - Settings.delete_children(music_list) - for s: SongObject in Playlist.get_playlist(): - var new_song_list_object: SongListObject = song_list_object.instantiate() - new_song_list_object.song_object = s - new_song_list_object.gui_input.connect(song_object_clicked.bind(s.song_number)) - music_list.add_child(new_song_list_object) + print("update_song_list") + if Playlist.currently_playing_song >= 0: + soundtrack_label.text = Playlist.get_current_soundtrack_title() + song_label.text = Playlist.get_current_song_title() + music_list_panel.refresh() -func song_object_clicked(event: InputEvent, song_no: int) -> void: - if (event is InputEventMouseButton && event.pressed && event.button_index == MOUSE_BUTTON_LEFT): - print("Clicked Song Object: ", Playlist.song_object_array[song_no].to_string()) - if Settings.character_select_open == false: - if Playlist.get_song(song_no) == null: - print("has no song, need to download") - var song_downloaded: Callable = func song_downloaded(body: PackedByteArray) -> void: - var sound: AudioStream = AudioStreamMP3.new() - sound.data = body - Playlist.add_song_to_song_object(song_no, sound) - music_player_container.play_song_object(song_no) - MusicServerClient.fetch_song(song_no, song_downloaded) - else: - print("song already downloaded") - music_player_container.play_song_object(song_no) +func song_object_clicked(song_no: int) -> void: + print("Clicked Song Object: ", Playlist.song_object_array[song_no].to_string()) + if Settings.character_select_open == false: + if Playlist.get_song(song_no) == null: + print("has no song, need to download") + var song_downloaded: Callable = func song_downloaded(body: PackedByteArray) -> void: + var sound: AudioStream = AudioStreamMP3.new() + sound.data = body + Playlist.add_song_to_song_object(song_no, sound) + music_player_container.play_song_object(song_no) + MusicServerClient.fetch_song(song_no, song_downloaded) + else: + print("song already downloaded") + music_player_container.play_song_object(song_no) func reset_playlist() -> void: - print("reset_playlist") - var playlist_reset_done: Callable = func playlist_reset_done() -> void: - Playlist.reset_playlist() - fetch_song_list_at_start() + print("reset_playlist") + var playlist_reset_done: Callable = func playlist_reset_done() -> void: + Playlist.reset_playlist() + fetch_song_list_at_start() - MusicServerClient.reset_playlist(playlist_reset_done) + MusicServerClient.reset_playlist(playlist_reset_done) func reset_points() -> void: - print("reset_points") - for player_objects: PlayerObject in Settings.player_array: - player_objects.reset_points() - load_players() - # A new game starts, so turn statistics back on if winning turned them off - if stats_paused_by_win: - Settings.add_to_stats = true - stats_paused_by_win = false + print("reset_points") + for player_objects: PlayerObject in Settings.player_array: + player_objects.reset_points() + players_panel.reload() + # A new game starts, so turn statistics back on if winning turned them off + if stats_paused_by_win: + Settings.add_to_stats = true + stats_paused_by_win = false func show_settings() -> void: - print("show_settings") - settings_window.open() + print("show_settings") + settings_window.open() -func get_suggestion_list() -> void: - print("get_suggestion_list") - var populate_list: Callable = func populate_list(array) -> void: - if typeof(array) == TYPE_ARRAY: - soundtracks = [] - Settings.delete_children(inspiration_list) - soundtracks.append_array(array) - for i: int in soundtracks.size(): - inspiration_list.add_child(ListRow.create(soundtracks[i], i, 20)) - else: - print("Unexpected data") - MusicServerClient.fetch_all_soundtracks(populate_list) - - -func add_players() -> void: - print("add_players") - Settings.edit_players = !Settings.edit_players - add_player_container.visible = !add_player_container.visible - new_player_name_field.grab_focus() - - -func add_player() -> void: - print("add_player") - if new_player_name_field.text != "" && Settings.player_array.size() < Settings.MAX_NUMBER_OF_PLAYERS: - var new_player_object: PlayerObject = PlayerObject.new(new_player_name_field.text) - new_player_name_field.text = "" - Settings.player_array.append(new_player_object) - new_player_object.connect("first_point_triggerd", music_player_container._on_point_triggered.bind("first")) - new_player_object.connect("match_point_triggerd", music_player_container._on_point_triggered.bind("match")) - new_player_object.connect("winner_triggerd", _on_player_won.bind(new_player_object)) - new_player_object.connect("point_given_sound", _on_make_point_given_sound) - new_player_object.connect("player_point_given", _on_point_given.bind(new_player_object)) - new_player_object.connect("player_point_taken", _on_point_taken.bind(new_player_object)) - load_players() - - -func load_players() -> void: - print("load_players") - Settings.delete_children(players_list_container) - var counter: int = 0 - for player_object: PlayerObject in Settings.player_array: - print(player_object.player_name) - - var new_player: Player = player.instantiate() - new_player.player_object = player_object - new_player.list_index = counter - - new_player.connect("change_character_clicked", _on_player_change_character_clicked.bind(new_player, player_object.player_name)) - new_player.connect("player_removed", _on_player_removed.bind(player_object)) - - players_list_container.add_child(new_player) - counter += 1 +# Each new player's points reach the music player, the log and the winner window. +func _on_player_added(new_player_object: PlayerObject) -> void: + new_player_object.connect("first_point_triggerd", music_player_container._on_point_triggered.bind("first")) + new_player_object.connect("match_point_triggerd", music_player_container._on_point_triggered.bind("match")) + new_player_object.connect("winner_triggerd", _on_player_won.bind(new_player_object)) + new_player_object.connect("point_given_sound", _on_make_point_given_sound) + new_player_object.connect("player_point_given", _on_point_given.bind(new_player_object)) + new_player_object.connect("player_point_taken", _on_point_taken.bind(new_player_object)) func _on_point_given(player_object: PlayerObject) -> void: - print("_on_point_given") - log_window.add_log_row(player_object.player_name + " got a point") - if Playlist.currently_playing_song >= 0: - Playlist.add_point(player_object.id) - update_song_list() + print("_on_point_given") + log_window.add_log_row(player_object.player_name + " got a point") + if Playlist.currently_playing_song >= 0: + Playlist.add_point(player_object.id) + update_song_list() func _on_make_point_given_sound() -> void: - music_player_container.play_sound_effect(preload("res://sounds/itemopen.wav")) + music_player_container.play_sound_effect(preload("res://sounds/itemopen.wav")) func _on_point_taken(player_object: PlayerObject) -> void: - print("_on_point_taken") - log_window.add_log_row(player_object.player_name + " lost a point") - music_player_container.play_sound_effect(preload("res://sounds/itemequip.wav")) - if Playlist.currently_playing_song >= 0: - Playlist.remove_point(player_object.id) - update_song_list() + print("_on_point_taken") + log_window.add_log_row(player_object.player_name + " lost a point") + music_player_container.play_sound_effect(preload("res://sounds/itemequip.wav")) + if Playlist.currently_playing_song >= 0: + Playlist.remove_point(player_object.id) + update_song_list() func _on_player_won(player_object: PlayerObject) -> void: - print("_on_player_won") - winner_window.show_winner(player_object) - music_player_container.play_song(preload("res://sounds/winning.mp3")) - # Songs played after the game is won don't count towards the statistics - if Settings.add_to_stats: - Settings.add_to_stats = false - stats_paused_by_win = true + print("_on_player_won") + winner_window.show_winner(player_object) + music_player_container.play_song(preload("res://sounds/winning.mp3")) + # Songs played after the game is won don't count towards the statistics + if Settings.add_to_stats: + Settings.add_to_stats = false + stats_paused_by_win = true func _on_player_removed(player_object: PlayerObject) -> void: - print("_on_player_removed ", player_object.player_name) - Settings.player_array.erase(player_object) - load_players() - Playlist.remove_player_from_list(player_object.id) - update_song_list() + Playlist.remove_player_from_list(player_object.id) + update_song_list() func _on_player_change_character_clicked(new_player: Player, player_name: String) -> void: - print("_on_player_change_character_clicked") - current_player = new_player - print("Change character for: " + player_name) - new_character_select.show_grid(player_name) + print("_on_player_change_character_clicked") + current_player = new_player + print("Change character for: " + player_name) + new_character_select.show_grid(player_name) func _on_new_character_selected(character_texture: ImageTexture) -> void: - print("_on_new_character_selected") - current_player.new_character_selected(character_texture) + print("_on_new_character_selected") + current_player.new_character_selected(character_texture) ###Local @@ -465,32 +388,30 @@ func _on_new_character_selected(character_texture: ImageTexture) -> void: var local_path: String = '/Users/sebastian/ResilioSync/Sorterat_test/Metal Gear Solid 4 - Guns of the Patriots/2-16 Metal Gear Saga.mp3' func open() -> void: - fileDialog.popup() + fileDialog.popup() func _on_file_dialog_dir_selected(path: String) -> void: - print(path) - dir_contents(path) + print(path) + dir_contents(path) func dir_contents(path: String) -> void: - var dir: DirAccess = DirAccess.open(path) - if dir: - dir.list_dir_begin() - var file_name: String = dir.get_next() - songs.clear() - soundtracks = [] - while file_name != "": - if dir.current_is_dir(): - #print("Found directory: " + file_name) - soundtracks.append(file_name) - else: - #print("Found file: " + file_name) - if file_name.ends_with(".mp3"): - songs.append(path + "/" + file_name) - file_name = dir.get_next() - else: - print("An error occurred when trying to access the path.") - - for i: int in soundtracks.size(): - inspiration_list.add_child(ListRow.create(soundtracks[i], i, 20)) + var dir: DirAccess = DirAccess.open(path) + if dir: + dir.list_dir_begin() + var file_name: String = dir.get_next() + songs.clear() + var soundtracks: Array = [] + while file_name != "": + if dir.current_is_dir(): + #print("Found directory: " + file_name) + soundtracks.append(file_name) + else: + #print("Found file: " + file_name) + if file_name.ends_with(".mp3"): + songs.append(path + "/" + file_name) + file_name = dir.get_next() + inspiration_list.show_soundtracks(soundtracks) + else: + print("An error occurred when trying to access the path.") diff --git a/tests/fakes/main_window_test_suite.gd b/tests/fakes/main_window_test_suite.gd index 5a90a84..2c4b792 100644 --- a/tests/fakes/main_window_test_suite.gd +++ b/tests/fakes/main_window_test_suite.gd @@ -77,16 +77,16 @@ func _play_first_song() -> void: ## Adds a player through the name field, the same way the user does. func _add_player(player_name: String) -> PlayerObject: var name_field: TextEdit = _quiz.get_node( - "Players/VBoxContainer/AddPlayerContainer/PlayerNameField") + "PlayersPanel/VBoxContainer/AddPlayerContainer/PlayerNameField") name_field.text = player_name - _quiz.add_player() + _quiz.players_panel.add_player() return Settings.player_array.back() if !Settings.player_array.is_empty() else null ## Returns the row in the player list that shows [param player_object]. func _player_row(player_object: PlayerObject) -> Player: var rows: Node = _quiz.get_node( - "Players/VBoxContainer/ScrollContainer/PlayersListVBoxContainer") + "PlayersPanel/VBoxContainer/ScrollContainer/PlayersListVBoxContainer") for row: Player in rows.get_children(): if row.player_object == player_object: return row diff --git a/tests/fakes/ui_tour.gd b/tests/fakes/ui_tour.gd index 03e1fb2..71e4b39 100644 --- a/tests/fakes/ui_tour.gd +++ b/tests/fakes/ui_tour.gd @@ -179,11 +179,14 @@ func _node(node_name: String) -> Node: return null -## Calls [param method] on the main window, or on its Quiz scene if that has it. +## Calls [param method] on the players panel, the quiz or the main window, whichever +## has it first. func _call(method: String, args: Array = []) -> Variant: - var quiz: Node = _main.get_node_or_null("Quiz") - var target: Node = quiz if quiz != null && quiz.has_method(method) else _main - return target.callv(method, args) + for path: String in ["Quiz/PlayersPanel", "Quiz"]: + var target: Node = _main.get_node_or_null(path) + if target != null && target.has_method(method): + return target.callv(method, args) + return _main.callv(method, args) func _server() -> FakeHttpTransport: @@ -196,13 +199,13 @@ func _settle() -> void: func _add_player(player_name: String) -> void: - (_node("Players").get_node("VBoxContainer/AddPlayerContainer/PlayerNameField") \ + (_node("PlayersPanel").get_node("VBoxContainer/AddPlayerContainer/PlayerNameField") \ as TextEdit).text = player_name _call("add_player") func _player_row(index: int) -> Player: - return _node("Players").get_node( + return _node("PlayersPanel").get_node( "VBoxContainer/ScrollContainer/PlayersListVBoxContainer").get_child(index) diff --git a/tests/integration/test_inspiration_list.gd b/tests/integration/test_inspiration_list.gd new file mode 100644 index 0000000..89f085d --- /dev/null +++ b/tests/integration/test_inspiration_list.gd @@ -0,0 +1,92 @@ +extends GdUnitTestSuite +## The inspiration list on its own: it shows every soundtrack from the server and +## scrolls by itself. + +const SOUNDTRACKS: Array[String] = ["Chrono Trigger", "Final Fantasy VI", "Secret of Mana"] + +var _snapshot: AutoloadSnapshot +var _server: FakeHttpTransport +var _runner: GdUnitSceneRunner +var _list: InspirationList + + +func before_test() -> void: + _snapshot = AutoloadSnapshot.take([Settings]) + Settings.inspiration_list_speed = 0 + _server = FakeHttpTransport.new().install() + _server.respond_json("/music/all", SOUNDTRACKS) + _runner = scene_runner("res://InspirationList.tscn") + _list = _runner.scene() + + +func after_test() -> void: + await await_idle_frame() + _server.uninstall() + _snapshot.restore() + + +func _rows() -> Array[Node]: + return _list.get_node("VBoxContainer").get_children() + + +func _texts() -> Array: + return _rows().map(func(row: ListRow) -> String: return row.text) + + +func test_load_from_server_shows_every_soundtrack_in_order() -> void: + _list.load_from_server() + + assert_array(_texts()).contains_exactly(SOUNDTRACKS) + assert_array(_list.soundtracks).contains_exactly(SOUNDTRACKS) + + +func test_rows_alternate_backgrounds() -> void: + _list.load_from_server() + + var rows: Array[Node] = _rows() + assert_that((rows[0] as ListRow).get_theme_stylebox("panel")) \ + .is_not_equal((rows[1] as ListRow).get_theme_stylebox("panel")) + assert_that((rows[0] as ListRow).get_theme_stylebox("panel")) \ + .is_equal((rows[2] as ListRow).get_theme_stylebox("panel")) + + +func test_loading_again_replaces_the_rows() -> void: + _list.load_from_server() + _server.respond_json("/music/all", ["Terranigma"]) + + _list.load_from_server() + + assert_array(_texts()).contains_exactly(["Terranigma"]) + + +func test_failed_load_keeps_the_rows() -> void: + _list.load_from_server() + _server.respond_bytes("/music/all", PackedByteArray(), 500) + + _list.load_from_server() + + assert_array(_texts()).contains_exactly(SOUNDTRACKS) + + +func _long_list() -> Array[String]: + var names: Array[String] = [] + for i: int in 40: + names.append("Soundtrack %d" % i) + return names + + +func test_scrolls_down_by_itself() -> void: + _list.show_soundtracks(_long_list()) + Settings.inspiration_list_speed = 5 + + await _runner.simulate_frames(30, 16) + + assert_int(_list.scroll_vertical).is_greater(0) + + +func test_does_not_scroll_at_speed_zero() -> void: + _list.show_soundtracks(_long_list()) + + await _runner.simulate_frames(30, 16) + + assert_int(_list.scroll_vertical).is_equal(0) diff --git a/tests/integration/test_inspiration_list.gd.uid b/tests/integration/test_inspiration_list.gd.uid new file mode 100644 index 0000000..cb05a55 --- /dev/null +++ b/tests/integration/test_inspiration_list.gd.uid @@ -0,0 +1 @@ +uid://2hfs3x1k46wu diff --git a/tests/integration/test_layout_unchanged.gd b/tests/integration/test_layout_unchanged.gd index d5df4ed..10aede3 100644 --- a/tests/integration/test_layout_unchanged.gd +++ b/tests/integration/test_layout_unchanged.gd @@ -12,12 +12,18 @@ const BASELINE_PATH: String = "res://tests/data/layout_baseline.json" ## Nodes that moved from the main window into the Quiz scene, by their old name. const QUIZ_NODES: Array[String] = [ - "Open", "FileDialog", "MusicListPanel", "ScrollContainer", "VBoxContainer", - "MusicPlayer", "Players", "ResetPlaylistButton", "ResetPointsButton", "SettingsButton", + "Open", "FileDialog", "MusicListPanel", "VBoxContainer", + "MusicPlayer", "ResetPlaylistButton", "ResetPointsButton", "SettingsButton", "SoundTestButton", "ShowAnswerButton", "NextButton", "RepeatSongCheckButton", "DebugLabel", "SettingsWindow", "NewCharacterSelect", "WinnerWindow", "Log", ] +## Nodes that moved into the Quiz scene and were renamed there: old name to new name. +const RENAMED_QUIZ_NODES: Dictionary = { + "Players": "PlayersPanel", + "ScrollContainer": "InspirationList", +} + ## Nodes that moved from the main window into the TopBar scene, by their old name. const TOP_BAR_NODES: Array[String] = [ "TopPanelContainer", "Title", "SearchButton", "SyncButton", "StatisticsButton", @@ -60,6 +66,8 @@ static func new_path(old_path: String) -> String: var first: String = old_path.get_slice("/", 0) if first in QUIZ_NODES: return "Quiz/" + old_path + if first in RENAMED_QUIZ_NODES: + return "Quiz/" + RENAMED_QUIZ_NODES[first] + old_path.trim_prefix(first) if first in TOP_BAR_NODES: return "TopBar/" + old_path return old_path diff --git a/tests/integration/test_main_window.gd b/tests/integration/test_main_window.gd index 91322e2..5f9dc11 100644 --- a/tests/integration/test_main_window.gd +++ b/tests/integration/test_main_window.gd @@ -240,12 +240,3 @@ func test_log_adds_one_row_per_point_with_padded_time() -> void: var time_pattern: RegEx = RegEx.create_from_string("^\\d\\d:\\d\\d:\\d\\d Crono got a point$") assert_object(time_pattern.search((log_box.get_child(0) as Label).text)).is_not_null() assert_str((log_box.get_child(1) as Label).text).ends_with("Crono lost a point") - - -func test_inspiration_list_alternates_row_backgrounds() -> void: - var rows: Array[Node] = _quiz.get_node("ScrollContainer/VBoxContainer").get_children() - - assert_int(rows.size()).is_equal(2) - assert_str((rows[0] as ListRow).text).is_equal("Chrono Trigger") - assert_that((rows[0] as ListRow).get_theme_stylebox("panel")) \ - .is_not_equal((rows[1] as ListRow).get_theme_stylebox("panel")) diff --git a/tests/integration/test_music_list_panel.gd b/tests/integration/test_music_list_panel.gd new file mode 100644 index 0000000..f6fa444 --- /dev/null +++ b/tests/integration/test_music_list_panel.gd @@ -0,0 +1,79 @@ +extends GdUnitTestSuite +## The music list on its own: one row per song in the playlist, and clicking a row +## tells which song was clicked. + +var _snapshot: AutoloadSnapshot +var _runner: GdUnitSceneRunner +var _panel: MusicListPanel + + +func before_test() -> void: + _snapshot = AutoloadSnapshot.take([Settings, Playlist]) + Playlist.reset_playlist() + for i: int in 3: + var song: SongObject = SongObject.new() + song.song_number = i + song.soundtrack_title = "Soundtrack %d" % i + song.song_title = "Song %d" % i + song.has_played = true + Playlist.add_to_playlist(song) + _runner = scene_runner("res://MusicListPanel.tscn") + _panel = _runner.scene() + + +func after_test() -> void: + await await_idle_frame() + _snapshot.restore() + + +func _rows() -> Array[Node]: + return _panel.get_node("ScrollContainer/MusicList").get_children() + + +func test_starts_empty() -> void: + assert_array(_rows()).is_empty() + + +func test_refresh_shows_one_row_per_song_in_order() -> void: + _panel.refresh() + + assert_array(_rows().map(func(row: SongListObject) -> int: return row.song_object.song_number)) \ + .contains_exactly([0, 1, 2]) + + +func test_refresh_again_replaces_the_rows() -> void: + _panel.refresh() + Playlist.song_object_array.pop_back() + + _panel.refresh() + + assert_int(_rows().size()).is_equal(2) + + +func test_clicking_a_row_tells_which_song() -> void: + _panel.refresh() + await _runner.simulate_frames(2) + var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_panel)) + var row: Control = _rows()[1] + # The left end of the row is the song text, not one of the player icons + var target: Vector2 = row.get_global_rect().position + Vector2(20, row.size.y / 2) + + _runner.set_mouse_position(_panel.get_viewport().get_final_transform() * target) + await _runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) + await _runner.await_input_processed() + + await signals.is_emitted("song_clicked", [1]) + + +func test_right_click_does_nothing() -> void: + _panel.refresh() + await _runner.simulate_frames(2) + var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_panel)) + var row: Control = _rows()[0] + + _runner.set_mouse_position(_panel.get_viewport().get_final_transform() + * (row.get_global_rect().position + Vector2(20, row.size.y / 2))) + await _runner.simulate_mouse_button_pressed(MOUSE_BUTTON_RIGHT) + await _runner.await_input_processed() + + await signals.wait_until(200).is_not_emitted("song_clicked") diff --git a/tests/integration/test_music_list_panel.gd.uid b/tests/integration/test_music_list_panel.gd.uid new file mode 100644 index 0000000..962ce7a --- /dev/null +++ b/tests/integration/test_music_list_panel.gd.uid @@ -0,0 +1 @@ +uid://c1dj75bnbdjle diff --git a/tests/integration/test_player_keybinds.gd b/tests/integration/test_player_keybinds.gd index 3c175f2..8dd3778 100644 --- a/tests/integration/test_player_keybinds.gd +++ b/tests/integration/test_player_keybinds.gd @@ -144,7 +144,7 @@ func test_player_list_shows_rows_in_player_order() -> void: var robo: PlayerObject = _add_player("Robo") var rows: Array[Node] = _quiz.get_node( - "Players/VBoxContainer/ScrollContainer/PlayersListVBoxContainer").get_children() + "PlayersPanel/VBoxContainer/ScrollContainer/PlayersListVBoxContainer").get_children() assert_array(rows.map(func(row: Player) -> PlayerObject: return row.player_object)) \ .contains_exactly([_marle, _lucca, robo]) diff --git a/tests/integration/test_players_panel.gd b/tests/integration/test_players_panel.gd new file mode 100644 index 0000000..07642a8 --- /dev/null +++ b/tests/integration/test_players_panel.gd @@ -0,0 +1,146 @@ +extends GdUnitTestSuite +## The player list on its own: adding players through the name field, removing them +## and asking for a new character. + +var _snapshot: AutoloadSnapshot +var _runner: GdUnitSceneRunner +var _panel: PlayersPanel + + +func before_test() -> void: + _snapshot = AutoloadSnapshot.take([Settings]) + Settings.player_array.clear() + Settings.edit_players = false + _runner = scene_runner("res://PlayersPanel.tscn") + _panel = _runner.scene() + + +func after_test() -> void: + await await_idle_frame() + _snapshot.restore() + + +func _rows() -> Array[Node]: + return _panel.players_list_container.get_children() + + +func _add(player_name: String) -> void: + _panel.new_player_name_field.text = player_name + _panel.add_player() + + +func _click(control: Control) -> void: + _runner.set_mouse_position(_panel.get_viewport().get_final_transform() + * control.get_global_rect().get_center()) + await _runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) + await _runner.await_input_processed() + + +func test_add_button_shows_and_hides_the_add_row() -> void: + assert_bool(_panel.add_player_container.visible).is_false() + + await _click(_panel.add_players_button) + assert_bool(_panel.add_player_container.visible).is_true() + assert_bool(Settings.edit_players).is_true() + + await _click(_panel.add_players_button) + assert_bool(_panel.add_player_container.visible).is_false() + assert_bool(Settings.edit_players).is_false() + + +func test_adding_a_name_adds_a_player_and_a_row() -> void: + var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_panel)) + + _add("Crono") + + assert_int(Settings.player_array.size()).is_equal(1) + var crono: PlayerObject = Settings.player_array[0] + assert_str(crono.player_name).is_equal("Crono") + await signals.is_emitted("player_added", [crono]) + assert_str(_panel.new_player_name_field.text).is_empty() + assert_object((_rows()[0] as Player).player_object).is_same(crono) + + +func test_add_button_in_the_row_adds_the_player() -> void: + await _click(_panel.add_players_button) + _panel.new_player_name_field.text = "Marle" + + await _click(_panel.add_player_button) + + assert_int(Settings.player_array.size()).is_equal(1) + + +func test_enter_in_the_name_field_adds_the_player() -> void: + await _click(_panel.add_players_button) + _panel.new_player_name_field.text = "Lucca" + _panel.new_player_name_field.grab_focus() + + _runner.simulate_key_pressed(KEY_ENTER) + await _runner.await_input_processed() + + assert_array(Settings.player_array.map( + func(p: PlayerObject) -> String: return p.player_name)).contains_exactly(["Lucca"]) + + +func test_empty_name_adds_nobody() -> void: + var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_panel)) + + _add("") + + assert_array(Settings.player_array).is_empty() + assert_array(_rows()).is_empty() + await signals.wait_until(200).is_not_emitted("player_added") + + +func test_cannot_add_more_than_max_players() -> void: + for i: int in Settings.MAX_NUMBER_OF_PLAYERS + 1: + _add("Player %d" % i) + + assert_int(Settings.player_array.size()).is_equal(Settings.MAX_NUMBER_OF_PLAYERS) + assert_int(_rows().size()).is_equal(Settings.MAX_NUMBER_OF_PLAYERS) + + +func test_rows_follow_player_order_and_numbering() -> void: + _add("Crono") + _add("Marle") + _add("Lucca") + + assert_array(_rows().map(func(row: Player) -> String: return row.player_object.player_name)) \ + .contains_exactly(["Crono", "Marle", "Lucca"]) + assert_array(_rows().map(func(row: Player) -> int: return row.list_index)) \ + .contains_exactly([0, 1, 2]) + + +func test_remove_button_removes_the_player_and_row() -> void: + _add("Crono") + _add("Marle") + var crono: PlayerObject = Settings.player_array[0] + var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_panel)) + + (_rows()[0].get_node("HBoxContainer/RemovePlayer") as BaseButton).pressed.emit() + + await signals.is_emitted("player_removed", [crono]) + assert_array(Settings.player_array.map( + func(p: PlayerObject) -> String: return p.player_name)).contains_exactly(["Marle"]) + assert_int(_rows().size()).is_equal(1) + assert_int((_rows()[0] as Player).list_index).is_equal(0) + + +func test_character_button_asks_for_a_new_character() -> void: + _add("Crono") + var row: Player = _rows()[0] + var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_panel)) + + (row.get_node("HBoxContainer/Character") as BaseButton).pressed.emit() + + await signals.is_emitted("change_character_requested", [row, "Crono"]) + + +func test_reload_shows_new_scores() -> void: + _add("Crono") + Settings.player_array[0].player_score = 3 + + _panel.reload() + + assert_str(((_rows()[0] as Player).get_node("HBoxContainer/Name") as Label).text) \ + .ends_with("Crono: 3") diff --git a/tests/integration/test_players_panel.gd.uid b/tests/integration/test_players_panel.gd.uid new file mode 100644 index 0000000..0e32cbb --- /dev/null +++ b/tests/integration/test_players_panel.gd.uid @@ -0,0 +1 @@ +uid://chtn6hbf4vqb6 diff --git a/tests/integration/test_scene_split.gd b/tests/integration/test_scene_split.gd index bc5fbdf..4291818 100644 --- a/tests/integration/test_scene_split.gd +++ b/tests/integration/test_scene_split.gd @@ -50,6 +50,12 @@ func test_top_bar_is_drawn_below_the_quiz() -> void: .is_less(_window.get_node("Quiz").get_index()) +func test_quiz_parts_are_their_own_scenes() -> void: + for node_name: String in ["MusicListPanel", "PlayersPanel", "InspirationList"]: + assert_str(_quiz.get_node(node_name).scene_file_path) \ + .is_equal("res://%s.tscn" % node_name) + + func test_top_bar_holds_only_title_and_buttons() -> void: assert_array(_children(_window.get_node("TopBar"))) \ .contains_exactly_in_any_order(TOP_BAR_CHILDREN) @@ -89,6 +95,48 @@ func test_clicking_quiz_buttons_plays_and_shows_answer() -> void: .is_equal("Chrono Trigger") +func test_clicking_a_song_in_the_music_list_plays_it() -> void: + _next_song_is(SONG_INFO[1]) + await _play_first_song() + _press("NextButton") + # Changing song waits a moment for the old one to stop + await get_tree().create_timer(0.7).timeout + assert_int(Playlist.currently_playing_song).is_equal(1) + var rows: Array[Node] = _quiz.get_node("MusicListPanel/ScrollContainer/MusicList") \ + .get_children() + var first_row: Control = rows.filter( + func(row: SongListObject) -> bool: return row.song_object.song_number == 0)[0] + + await _click_at(first_row.get_global_rect().position + Vector2(20, first_row.size.y / 2)) + await get_tree().create_timer(0.7).timeout + + assert_int(Playlist.currently_playing_song).is_equal(0) + + +func test_points_from_the_player_list_reach_the_song_and_log() -> void: + await _play_first_song() + var crono: PlayerObject = _add_player("Crono") + + (_player_row(crono).get_node("HBoxContainer/AddPoint") as BaseButton).pressed.emit() + + assert_array(Playlist.song_object_array[0].players_given_point).contains_exactly([crono.id]) + assert_int((_quiz.get_node("%Log").get_node("%LogVBoxContainer") as Node).get_child_count()) \ + .is_equal(1) + + +func test_character_button_in_the_player_list_picks_a_new_character() -> void: + var crono: PlayerObject = _add_player("Crono") + var character_select: PopupWindow = _quiz.get_node("NewCharacterSelect") + + (_player_row(crono).get_node("HBoxContainer/Character") as BaseButton).pressed.emit() + + assert_bool(character_select.visible).is_true() + var texture: ImageTexture = ImageTexture.create_from_image( + Image.create(4, 4, false, Image.FORMAT_RGBA8)) + character_select.new_new_character_selected.emit(texture) + assert_object(crono.character).is_same(texture) + + func test_quiz_popup_backdrop_covers_the_top_bar() -> void: var settings: PopupWindow = _quiz.get_node("SettingsWindow") settings.open()