diff --git a/MusicPlayer.gd b/MusicPlayer.gd index b65d224..74de4fd 100644 --- a/MusicPlayer.gd +++ b/MusicPlayer.gd @@ -1,14 +1,16 @@ class_name MusicPlayer -extends PanelContainer +extends Control +## The bar that plays the songs, with play, restart, progress and volume, and the +## auto repeat switch under it. # LOC 20/1-24: 136 # LOC 12/6-24: 170 @onready -var play_button: TextureButton = $MusicPlayerContainer/PlayTextureButton +var play_button: TextureButton = $PlayerBar/MusicPlayerContainer/PlayTextureButton @onready -var restart_button: TextureButton = $MusicPlayerContainer/RestartTextureButton +var restart_button: TextureButton = $PlayerBar/MusicPlayerContainer/RestartTextureButton @onready var audio_player: AudioStreamPlayer = $AudioStreamPlayer @@ -20,13 +22,16 @@ var sound_player: AudioStreamPlayer = $AudioSoundPlayer var sound_effect_player: AudioStreamPlayer = $AudioSoundEffectPlayer @onready -var progress_slider: HSlider = $MusicPlayerContainer/MusicPlayerSlider +var progress_slider: HSlider = $PlayerBar/MusicPlayerContainer/MusicPlayerSlider @onready -var volume_slider: HSlider = $MusicPlayerContainer/VolumeSlider +var volume_slider: HSlider = $PlayerBar/MusicPlayerContainer/VolumeSlider @onready -var music_time_label: Label = $MusicPlayerContainer/MusicTimeLabel +var music_time_label: Label = $PlayerBar/MusicPlayerContainer/MusicTimeLabel + +@onready +var auto_repeat_button: CheckButton = $RepeatSongCheckButton var play_icon: Texture = preload("res://icons/play_icon_light.svg") var pause_icon: Texture = preload("res://icons/pause_icon_light.svg") @@ -49,6 +54,10 @@ func _ready() -> void: progress_slider.drag_ended.connect(_on_drag_ended) audio_player.finished.connect(_on_finished) + auto_repeat_button.pressed.connect(_on_auto_repeat_pressed) + +func _on_auto_repeat_pressed() -> void: + Settings.auto_repeat_song = !Settings.auto_repeat_song func _process(_delta: float) -> void: if audio_player.has_stream_playback() && !is_changing && !audio_player.stream_paused: diff --git a/MusicPlayer.tscn b/MusicPlayer.tscn index 94955e7..c692604 100644 --- a/MusicPlayer.tscn +++ b/MusicPlayer.tscn @@ -26,19 +26,11 @@ unicode = 90 [sub_resource type="Shortcut" id="Shortcut_i7swq"] events = [SubResource("InputEventKey_2nvce")] -[node name="MusicPlayer" type="PanelContainer"] -anchors_preset = 8 -anchor_left = 0.5 -anchor_top = 0.5 -anchor_right = 0.5 -anchor_bottom = 0.5 -offset_left = -121.0 -offset_top = -15.5 -offset_right = 121.0 -offset_bottom = 16.5 -grow_horizontal = 2 -grow_vertical = 2 -size_flags_vertical = 4 +[node name="MusicPlayer" type="Control"] +layout_mode = 3 +offset_right = 1792.0 +offset_bottom = 71.0 +mouse_filter = 2 script = ExtResource("1_t24ra") [node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."] @@ -53,11 +45,17 @@ bus = &"music" stream = ExtResource("2_xti80") bus = &"music" -[node name="MusicPlayerContainer" type="HBoxContainer" parent="."] +[node name="PlayerBar" type="PanelContainer" parent="."] +layout_mode = 0 +offset_left = 1.0 +offset_right = 1792.0 +offset_bottom = 32.0 + +[node name="MusicPlayerContainer" type="HBoxContainer" parent="PlayerBar"] layout_mode = 2 size_flags_vertical = 4 -[node name="PlayTextureButton" type="TextureButton" parent="MusicPlayerContainer"] +[node name="PlayTextureButton" type="TextureButton" parent="PlayerBar/MusicPlayerContainer"] custom_minimum_size = Vector2(32, 32) layout_mode = 2 focus_mode = 0 @@ -67,7 +65,7 @@ texture_normal = ExtResource("3_6g308") ignore_texture_size = true stretch_mode = 0 -[node name="RestartTextureButton" type="TextureButton" parent="MusicPlayerContainer"] +[node name="RestartTextureButton" type="TextureButton" parent="PlayerBar/MusicPlayerContainer"] custom_minimum_size = Vector2(32, 32) layout_mode = 2 focus_mode = 0 @@ -78,17 +76,27 @@ ignore_texture_size = true stretch_mode = 0 flip_h = true -[node name="MusicPlayerSlider" type="HSlider" parent="MusicPlayerContainer"] +[node name="MusicPlayerSlider" type="HSlider" parent="PlayerBar/MusicPlayerContainer"] layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 4 scrollable = false -[node name="MusicTimeLabel" type="Label" parent="MusicPlayerContainer"] +[node name="MusicTimeLabel" type="Label" parent="PlayerBar/MusicPlayerContainer"] layout_mode = 2 text = "0:00 / 0:00" -[node name="VolumeSlider" parent="MusicPlayerContainer" instance=ExtResource("5_iifuj")] +[node name="VolumeSlider" parent="PlayerBar/MusicPlayerContainer" instance=ExtResource("5_iifuj")] custom_minimum_size = Vector2(100, 0) layout_mode = 2 size_flags_vertical = 4 + +[node name="RepeatSongCheckButton" type="CheckButton" parent="."] +layout_mode = 0 +offset_top = 40.0 +offset_right = 144.0 +offset_bottom = 71.0 +focus_mode = 0 +button_pressed = true +action_mode = 0 +text = "Auto repeat" diff --git a/NowPlaying.tscn b/NowPlaying.tscn new file mode 100644 index 0000000..3609c9c --- /dev/null +++ b/NowPlaying.tscn @@ -0,0 +1,29 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://now_playing.gd" id="part_script"] + +[sub_resource type="LabelSettings" id="LabelSettings_qspbu"] +font_size = 50 + +[sub_resource type="LabelSettings" id="LabelSettings_3m52w"] +font_size = 35 + +[node name="NowPlaying" type="VBoxContainer" unique_id=42468498] +layout_mode = 3 +offset_right = 1088.0 +offset_bottom = 320.0 +script = ExtResource("part_script") + +[node name="SoundtrackLabel" type="Label" parent="." unique_id=173147793] +custom_minimum_size = Vector2(1, 1) +layout_mode = 2 +label_settings = SubResource("LabelSettings_qspbu") +horizontal_alignment = 1 +autowrap_mode = 2 + +[node name="SongLabel" type="Label" parent="." unique_id=1687490027] +custom_minimum_size = Vector2(1, 1) +layout_mode = 2 +label_settings = SubResource("LabelSettings_3m52w") +horizontal_alignment = 1 +autowrap_mode = 2 diff --git a/Quiz.tscn b/Quiz.tscn index 4df21c6..b579cef 100644 --- a/Quiz.tscn +++ b/Quiz.tscn @@ -9,30 +9,10 @@ [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 - -[sub_resource type="LabelSettings" id="LabelSettings_3m52w"] -font_size = 35 - -[sub_resource type="InputEventKey" id="InputEventKey_06rl4"] -device = -1 -alt_pressed = true -keycode = 86 -unicode = 8249 - -[sub_resource type="Shortcut" id="Shortcut_a7fvb"] -events = [SubResource("InputEventKey_06rl4")] - -[sub_resource type="InputEventKey" id="InputEventKey_7dnqw"] -device = -1 -alt_pressed = true -keycode = 67 -unicode = 231 - -[sub_resource type="Shortcut" id="Shortcut_d6fml"] -events = [SubResource("InputEventKey_7dnqw")] +[ext_resource type="PackedScene" path="res://QuizToolbar.tscn" id="scene_QuizToolbar"] +[ext_resource type="PackedScene" path="res://SongControls.tscn" id="scene_SongControls"] +[ext_resource type="PackedScene" path="res://NowPlaying.tscn" id="scene_NowPlaying"] +[ext_resource type="Script" path="res://song_loader.gd" id="song_loader_script"] [node name="Quiz" type="Control" unique_id=1146705218] layout_mode = 3 @@ -76,40 +56,19 @@ offset_top = 128.0 offset_right = 1857.0 offset_bottom = 512.0 -[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=42468498] +[node name="NowPlaying" parent="." unique_id=42468498 instance=ExtResource("scene_NowPlaying")] layout_mode = 0 offset_left = 64.0 offset_top = 64.0 offset_right = 1152.0 offset_bottom = 384.0 -[node name="SoundtrackLabel" type="Label" parent="VBoxContainer" unique_id=173147793] -custom_minimum_size = Vector2(1, 1) -layout_mode = 2 -label_settings = SubResource("LabelSettings_qspbu") -horizontal_alignment = 1 -autowrap_mode = 2 - -[node name="SongLabel" type="Label" parent="VBoxContainer" unique_id=1687490027] -custom_minimum_size = Vector2(1, 1) -layout_mode = 2 -label_settings = SubResource("LabelSettings_3m52w") -horizontal_alignment = 1 -autowrap_mode = 2 - [node name="MusicPlayer" parent="." unique_id=1990459832 instance=ExtResource("5_emn36")] layout_mode = 0 -anchors_preset = 0 -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 -offset_left = 65.0 +offset_left = 64.0 offset_top = 992.0 offset_right = 1856.0 -offset_bottom = 1024.0 -grow_horizontal = 1 -grow_vertical = 1 +offset_bottom = 1063.0 [node name="PlayersPanel" parent="." unique_id=1708375945 instance=ExtResource("scene_PlayersPanel")] layout_mode = 0 @@ -118,68 +77,19 @@ offset_top = 576.0 offset_right = 1857.0 offset_bottom = 977.0 -[node name="ResetPlaylistButton" type="Button" parent="." unique_id=1754155620] +[node name="QuizToolbar" parent="." unique_id=1873301941 instance=ExtResource("scene_QuizToolbar")] layout_mode = 0 offset_left = 1360.0 offset_top = 64.0 -offset_right = 1471.0 -offset_bottom = 95.0 -focus_mode = 0 -action_mode = 0 -text = "Reset playlist" - -[node name="ResetPointsButton" type="Button" parent="." unique_id=942880776] -layout_mode = 0 -offset_left = 1488.0 -offset_top = 64.0 -offset_right = 1599.0 -offset_bottom = 95.0 -focus_mode = 0 -action_mode = 0 -text = "Reset points" - -[node name="SoundTestButton" type="Button" parent="." unique_id=1332971879] -layout_mode = 0 -offset_left = 1744.0 -offset_top = 64.0 offset_right = 1855.0 offset_bottom = 95.0 -focus_mode = 0 -action_mode = 0 -text = "Sound test" -[node name="ShowAnswerButton" type="Button" parent="." unique_id=415388039] +[node name="SongControls" parent="." unique_id=1640212773 instance=ExtResource("scene_SongControls")] layout_mode = 0 offset_left = 65.0 offset_top = 944.0 -offset_right = 176.0 -offset_bottom = 975.0 -focus_mode = 0 -action_mode = 0 -shortcut = SubResource("Shortcut_a7fvb") -text = "Show answer" - -[node name="NextButton" type="Button" parent="." unique_id=1744703488] -layout_mode = 0 -offset_left = 193.0 -offset_top = 944.0 offset_right = 368.0 offset_bottom = 975.0 -focus_mode = 0 -disabled = true -action_mode = 0 -shortcut = SubResource("Shortcut_d6fml") -text = "Randomize new track" - -[node name="SettingsButton" type="Button" parent="." unique_id=1193400323] -layout_mode = 0 -offset_left = 1616.0 -offset_top = 64.0 -offset_right = 1727.0 -offset_bottom = 95.0 -focus_mode = 0 -action_mode = 0 -text = "Settings" [node name="SettingsWindow" parent="." unique_id=1323246978 instance=ExtResource("11_k62u5")] visible = false @@ -207,17 +117,6 @@ offset_top = 153.0 offset_right = 720.0 offset_bottom = 153.0 -[node name="RepeatSongCheckButton" type="CheckButton" parent="." unique_id=776835053] -layout_mode = 0 -offset_left = 64.0 -offset_top = 1032.0 -offset_right = 208.0 -offset_bottom = 1063.0 -focus_mode = 0 -button_pressed = true -action_mode = 0 -text = "Auto repeat" - [node name="DebugLabel" type="Label" parent="." unique_id=1959221257] visible = false layout_mode = 0 @@ -240,4 +139,7 @@ offset_top = 132.0 offset_right = 1262.0 offset_bottom = 832.0 +[node name="SongLoader" type="Node" parent="." unique_id=1520867349] +script = ExtResource("song_loader_script") + [connection signal="dir_selected" from="FileDialog" to="." method="_on_file_dialog_dir_selected"] diff --git a/QuizToolbar.tscn b/QuizToolbar.tscn new file mode 100644 index 0000000..647ad75 --- /dev/null +++ b/QuizToolbar.tscn @@ -0,0 +1,45 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://quiz_toolbar.gd" id="part_script"] + +[node name="QuizToolbar" type="Control" unique_id=1873301941] +layout_mode = 3 +offset_right = 495.0 +offset_bottom = 31.0 +mouse_filter = 2 +script = ExtResource("part_script") + +[node name="ResetPlaylistButton" type="Button" parent="." unique_id=1754155620] +layout_mode = 0 +offset_right = 111.0 +offset_bottom = 31.0 +focus_mode = 0 +action_mode = 0 +text = "Reset playlist" + +[node name="ResetPointsButton" type="Button" parent="." unique_id=942880776] +layout_mode = 0 +offset_left = 128.0 +offset_right = 239.0 +offset_bottom = 31.0 +focus_mode = 0 +action_mode = 0 +text = "Reset points" + +[node name="SettingsButton" type="Button" parent="." unique_id=1193400323] +layout_mode = 0 +offset_left = 256.0 +offset_right = 367.0 +offset_bottom = 31.0 +focus_mode = 0 +action_mode = 0 +text = "Settings" + +[node name="SoundTestButton" type="Button" parent="." unique_id=1332971879] +layout_mode = 0 +offset_left = 384.0 +offset_right = 495.0 +offset_bottom = 31.0 +focus_mode = 0 +action_mode = 0 +text = "Sound test" diff --git a/SongControls.tscn b/SongControls.tscn new file mode 100644 index 0000000..86a51ac --- /dev/null +++ b/SongControls.tscn @@ -0,0 +1,48 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://song_controls.gd" id="part_script"] + +[sub_resource type="InputEventKey" id="InputEventKey_06rl4"] +device = -1 +alt_pressed = true +keycode = 86 +unicode = 8249 + +[sub_resource type="Shortcut" id="Shortcut_a7fvb"] +events = [SubResource("InputEventKey_06rl4")] + +[sub_resource type="InputEventKey" id="InputEventKey_7dnqw"] +device = -1 +alt_pressed = true +keycode = 67 +unicode = 231 + +[sub_resource type="Shortcut" id="Shortcut_d6fml"] +events = [SubResource("InputEventKey_7dnqw")] + +[node name="SongControls" type="Control" unique_id=1640212773] +layout_mode = 3 +offset_right = 303.0 +offset_bottom = 31.0 +mouse_filter = 2 +script = ExtResource("part_script") + +[node name="ShowAnswerButton" type="Button" parent="." unique_id=415388039] +layout_mode = 0 +offset_right = 111.0 +offset_bottom = 31.0 +focus_mode = 0 +action_mode = 0 +shortcut = SubResource("Shortcut_a7fvb") +text = "Show answer" + +[node name="NextButton" type="Button" parent="." unique_id=1744703488] +layout_mode = 0 +offset_left = 128.0 +offset_right = 303.0 +offset_bottom = 31.0 +focus_mode = 0 +disabled = true +action_mode = 0 +shortcut = SubResource("Shortcut_d6fml") +text = "Randomize new track" diff --git a/now_playing.gd b/now_playing.gd new file mode 100644 index 0000000..634ca87 --- /dev/null +++ b/now_playing.gd @@ -0,0 +1,10 @@ +class_name NowPlaying +extends VBoxContainer +## The soundtrack and song that are playing, in large text at the top of the quiz. + + +## Shows [param soundtrack_title] and [param song_title]. The playlist hides them as +## question marks until the answer is shown. +func show_song(soundtrack_title: String, song_title: String) -> void: + $SoundtrackLabel.text = soundtrack_title + $SongLabel.text = song_title diff --git a/now_playing.gd.uid b/now_playing.gd.uid new file mode 100644 index 0000000..c271808 --- /dev/null +++ b/now_playing.gd.uid @@ -0,0 +1 @@ +uid://beoi75d4u568h diff --git a/quiz.gd b/quiz.gd index f2c5b9d..e0588d9 100644 --- a/quiz.gd +++ b/quiz.gd @@ -21,25 +21,13 @@ var fileDialog: FileDialog = $FileDialog var inspiration_list: InspirationList = $InspirationList @onready -var soundtrack_label: Label = $VBoxContainer/SoundtrackLabel - -@onready -var song_label: Label = $VBoxContainer/SongLabel +var now_playing: NowPlaying = $NowPlaying @onready var players_panel: PlayersPanel = $PlayersPanel @onready -var reset_playlist_button: Button = $ResetPlaylistButton - -@onready -var reset_points_button: Button = $ResetPointsButton - -@onready -var sound_test_button: Button = $SoundTestButton - -@onready -var settings_button: Button = $SettingsButton +var toolbar: QuizToolbar = $QuizToolbar @onready var settings_window: Control = $SettingsWindow @@ -48,10 +36,10 @@ var settings_window: Control = $SettingsWindow var winner_window: Control = %WinnerWindow @onready -var show_answer_button: Button = $ShowAnswerButton +var song_controls: SongControls = $SongControls @onready -var next_button: Button = $NextButton +var song_loader: SongLoader = $SongLoader @onready var music_list_panel: MusicListPanel = $MusicListPanel @@ -60,10 +48,7 @@ var music_list_panel: MusicListPanel = $MusicListPanel var new_character_select: Control = %NewCharacterSelect @onready -var auto_repeat_song_button: CheckButton = $RepeatSongCheckButton - -@onready -var music_player_container: PanelContainer = $MusicPlayer +var music_player_container: MusicPlayer = $MusicPlayer @onready var log_window: Control = %Log @@ -80,9 +65,6 @@ var stats_paused_by_win: bool = false ## Dims the window behind an open popup and keeps clicks from reaching the buttons under it. var popup_backdrop: PopupBackdrop -## Seconds to wait before retrying a song download step that failed. -var song_fetch_retry_seconds: float = 5.0 - func _ready() -> void: popup_backdrop = PopupBackdrop.add_to(self, _popups()) @@ -92,12 +74,14 @@ func _ready() -> void: 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) + song_controls.next_pressed.connect(play_next_song) + song_controls.show_answer_pressed.connect(show_answer_pressed) + toolbar.sound_test_pressed.connect(music_player_container.get_sound_test_song) + toolbar.settings_pressed.connect(show_settings) + toolbar.reset_playlist_pressed.connect(reset_playlist) + toolbar.reset_points_pressed.connect(reset_points) + song_loader.song_list_loaded.connect(update_song_list) + song_loader.song_added.connect(_on_song_added) new_character_select.connect("new_new_character_selected", _on_new_character_selected) music_list_panel.song_clicked.connect(song_object_clicked) @@ -110,10 +94,9 @@ func _ready() -> void: settings_window.visibility_changed.connect(_on_settings_window_visibility_changed) open_button.pressed.connect(open) - auto_repeat_song_button.pressed.connect(repeat_song) inspiration_list.load_from_server() - fetch_song_list_at_start() + song_loader.load_song_list() new_character_select.load_characters() @@ -140,18 +123,15 @@ func _input(event: InputEvent) -> void: ## 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 + toolbar.set_locked(true) + song_controls.set_locked(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 + toolbar.set_locked(false) + song_controls.set_locked(false) reset_points() @@ -170,41 +150,12 @@ func _on_server_changed() -> void: server_changed.emit() -func repeat_song() -> void: - Settings.auto_repeat_song = !Settings.auto_repeat_song - - func show_answer_pressed() -> void: 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)) - - func play_next_song() -> void: print("play_next_song ", Playlist.latest_played_song) if Playlist.first_song_played: @@ -212,9 +163,8 @@ func play_next_song() -> void: 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() + song_controls.set_next_enabled(false) + song_loader.fetch_song_if_idle() func _popups() -> Array[PopupWindow]: @@ -224,63 +174,21 @@ func _popups() -> Array[PopupWindow]: # 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: + song_loader.fill_cache() -func fetch_song() -> void: - 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))) - - -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))) - - -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))) - - -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) +func _on_song_added() -> void: + song_controls.set_next_enabled(true) + if Settings.is_debug: + update_song_list() 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() + now_playing.show_song(Playlist.get_current_soundtrack_title(), + Playlist.get_current_song_title()) music_list_panel.refresh() @@ -304,7 +212,7 @@ 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() + song_loader.load_song_list() MusicServerClient.reset_playlist(playlist_reset_done) diff --git a/quiz_toolbar.gd b/quiz_toolbar.gd new file mode 100644 index 0000000..81818f3 --- /dev/null +++ b/quiz_toolbar.gd @@ -0,0 +1,25 @@ +class_name QuizToolbar +extends Control +## The row of quiz buttons under the top bar: reset playlist, reset points, settings +## and sound test. + +signal reset_playlist_pressed +signal reset_points_pressed +signal settings_pressed +signal sound_test_pressed + +@onready var reset_playlist_button: Button = $ResetPlaylistButton +@onready var sound_test_button: Button = $SoundTestButton + + +func _ready() -> void: + reset_playlist_button.pressed.connect(reset_playlist_pressed.emit) + $ResetPointsButton.pressed.connect(reset_points_pressed.emit) + $SettingsButton.pressed.connect(settings_pressed.emit) + sound_test_button.pressed.connect(sound_test_pressed.emit) + + +## Disables the buttons that use the song list, e.g. while the server syncs it. +func set_locked(locked: bool) -> void: + reset_playlist_button.disabled = locked + sound_test_button.disabled = locked diff --git a/quiz_toolbar.gd.uid b/quiz_toolbar.gd.uid new file mode 100644 index 0000000..8d5020b --- /dev/null +++ b/quiz_toolbar.gd.uid @@ -0,0 +1 @@ +uid://dmsy36s1ht7x5 diff --git a/song_controls.gd b/song_controls.gd new file mode 100644 index 0000000..26d9fe0 --- /dev/null +++ b/song_controls.gd @@ -0,0 +1,27 @@ +class_name SongControls +extends Control +## The buttons for the song that is playing: show the answer, and go to the next song. + +signal show_answer_pressed +signal next_pressed + +@onready var show_answer_button: Button = $ShowAnswerButton +@onready var next_button: Button = $NextButton + + +func _ready() -> void: + show_answer_button.pressed.connect(show_answer_pressed.emit) + next_button.pressed.connect(next_pressed.emit) + + +## Enables Next while there is a downloaded song to play next. +func set_next_enabled(enabled: bool) -> void: + next_button.disabled = !enabled + + +## Disables both buttons, e.g. while the server syncs the song list. Unlocking enables +## Show answer again; Next comes back when the next song has been downloaded. +func set_locked(locked: bool) -> void: + show_answer_button.disabled = locked + if locked: + next_button.disabled = true diff --git a/song_controls.gd.uid b/song_controls.gd.uid new file mode 100644 index 0000000..94131e8 --- /dev/null +++ b/song_controls.gd.uid @@ -0,0 +1 @@ +uid://d6nevh66yaw1 diff --git a/song_loader.gd b/song_loader.gd new file mode 100644 index 0000000..613347e --- /dev/null +++ b/song_loader.gd @@ -0,0 +1,100 @@ +class_name SongLoader +extends Node +## Downloads songs from the server into the Playlist ahead of time, so the next song +## can start at once. +## +## How many songs it keeps ready is the cache size in the settings +## ([code]Playlist.number_of_tracks_to_preload[/code]). + +## Emitted when the songs already in the server playlist have been loaded into the +## Playlist. +signal song_list_loaded + +## Emitted when a new song has been downloaded and added to the Playlist. +signal song_added + +## Seconds to wait before retrying a download step that failed. +var retry_seconds: float = 5.0 + + +## 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 load_song_list() -> void: + print("load_song_list") + var song_list_fetched: Callable = func song_list_fetched(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() + song_list_loaded.emit() + fetch_song_if_idle() + MusicServerClient.fetch_song_list(song_list_fetched, _retry_later.bind(load_song_list)) + + +## Starts downloading one more song, which keeps downloading until the cache is full. +func fetch_song() -> void: + print("fetch_song") + Playlist.is_song_downloading = true + _download_random_song(SongObject.new()) + + +## Starts downloading a song unless one is already downloading. +func fetch_song_if_idle() -> void: + if !Playlist.is_song_downloading: + fetch_song() + + +## Starts downloading if nothing is downloading and the cache is not full, e.g. after +## the cache size was raised. +func fill_cache() -> void: + if !Playlist.is_song_downloading && Playlist.should_preload_more_songs(): + fetch_song() + + +# 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))) + + +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))) + + +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) + if Playlist.should_preload_more_songs(): + fetch_song() + else: + Playlist.is_song_downloading = false + song_added.emit() + 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" % retry_seconds) + get_tree().create_timer(retry_seconds).timeout.connect(step) diff --git a/song_loader.gd.uid b/song_loader.gd.uid new file mode 100644 index 0000000..ced5424 --- /dev/null +++ b/song_loader.gd.uid @@ -0,0 +1 @@ +uid://dwqirg7x7mmj6 diff --git a/tests/fakes/main_window_test_suite.gd b/tests/fakes/main_window_test_suite.gd index f3c85c6..a5fe69f 100644 --- a/tests/fakes/main_window_test_suite.gd +++ b/tests/fakes/main_window_test_suite.gd @@ -70,7 +70,7 @@ func _press(button_path: String) -> void: ## Presses Next and waits until the first song is playing. func _play_first_song() -> void: - _press("NextButton") + _press("SongControls/NextButton") await _runner.simulate_frames(2) diff --git a/tests/integration/test_layout_unchanged.gd b/tests/integration/test_layout_unchanged.gd index fbfc6e2..1b8411c 100644 --- a/tests/integration/test_layout_unchanged.gd +++ b/tests/integration/test_layout_unchanged.gd @@ -12,16 +12,24 @@ 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", "VBoxContainer", - "MusicPlayer", "ResetPlaylistButton", "ResetPointsButton", "SettingsButton", - "SoundTestButton", "ShowAnswerButton", "NextButton", "RepeatSongCheckButton", - "DebugLabel", "SettingsWindow", "NewCharacterSelect", "WinnerWindow", "Log", + "Open", "FileDialog", "MusicListPanel", "DebugLabel", "SettingsWindow", + "NewCharacterSelect", "WinnerWindow", "Log", ] -## Nodes that moved into the Quiz scene and were renamed there: old name to new name. +## Nodes that moved into the Quiz scene under another path: old name to new path in +## the quiz. const RENAMED_QUIZ_NODES: Dictionary = { "Players": "PlayersPanel", "ScrollContainer": "InspirationList", + "VBoxContainer": "NowPlaying", + "MusicPlayer": "MusicPlayer/PlayerBar", + "RepeatSongCheckButton": "MusicPlayer/RepeatSongCheckButton", + "ResetPlaylistButton": "QuizToolbar/ResetPlaylistButton", + "ResetPointsButton": "QuizToolbar/ResetPointsButton", + "SettingsButton": "QuizToolbar/SettingsButton", + "SoundTestButton": "QuizToolbar/SoundTestButton", + "ShowAnswerButton": "SongControls/ShowAnswerButton", + "NextButton": "SongControls/NextButton", } ## Nodes that moved from the main window into the TopBar scene, by their old name. diff --git a/tests/integration/test_main_window.gd b/tests/integration/test_main_window.gd index 66fc24b..82d23ab 100644 --- a/tests/integration/test_main_window.gd +++ b/tests/integration/test_main_window.gd @@ -7,7 +7,7 @@ func test_startup_loads_soundtracks_and_preloads_a_song() -> void: assert_int(_server.count("/music/all")).is_equal(1) assert_array(_server.requested_paths).contains(["/music/rand", "/music/addQue", "/music/info"]) assert_int(Playlist.song_object_array.size()).is_equal(1) - assert_bool((_quiz.get_node("NextButton") as Button).disabled).is_false() + assert_bool((_quiz.get_node("SongControls/NextButton") as Button).disabled).is_false() func test_low_played_mode_fetches_from_low_played_endpoint() -> void: @@ -15,7 +15,7 @@ func test_low_played_mode_fetches_from_low_played_endpoint() -> void: _server.respond_bytes( "/music/rand/low", FileAccess.get_file_as_bytes("res://tests/data/short.mp3")) - _quiz.fetch_song() + _quiz.song_loader.fetch_song() assert_int(_server.count("/music/rand/low")).is_equal(1) @@ -23,7 +23,7 @@ func test_low_played_mode_fetches_from_low_played_endpoint() -> void: func test_next_plays_preloaded_song_and_fetches_another() -> void: _next_song_is(SONG_INFO[1]) - _press("NextButton") + _press("SongControls/NextButton") await _runner.simulate_frames(2) assert_int(Playlist.currently_playing_song).is_equal(0) @@ -33,23 +33,23 @@ func test_next_plays_preloaded_song_and_fetches_another() -> void: func test_title_is_hidden_until_answer_is_shown() -> void: - var soundtrack_label: Label = _quiz.get_node("VBoxContainer/SoundtrackLabel") - _press("NextButton") + var soundtrack_label: Label = _quiz.get_node("NowPlaying/SoundtrackLabel") + _press("SongControls/NextButton") await _runner.simulate_frames(2) assert_str(soundtrack_label.text).is_equal("????????") - _press("ShowAnswerButton") + _press("SongControls/ShowAnswerButton") assert_str(soundtrack_label.text).is_equal("Chrono Trigger") assert_int(_server.count("/music/played?song=0")).is_equal(1) func test_reset_playlist_clears_server_and_preloads_again() -> void: - _press("NextButton") + _press("SongControls/NextButton") await _runner.simulate_frames(2) - _press("ResetPlaylistButton") + _press("QuizToolbar/ResetPlaylistButton") assert_int(_server.count("/music/reset")).is_equal(1) assert_int(Playlist.currently_playing_song).is_equal(-1) @@ -61,7 +61,7 @@ func test_reset_points_sets_all_scores_to_zero() -> void: _add_player("Crono").add_point() _add_player("Marle").add_point() - _press("ResetPointsButton") + _press("QuizToolbar/ResetPointsButton") assert_int(Settings.player_array[0].player_score).is_equal(0) assert_int(Settings.player_array[1].player_score).is_equal(0) @@ -81,13 +81,13 @@ func test_cannot_add_player_without_name() -> void: func test_failed_song_download_is_retried_until_the_server_answers() -> void: - _quiz.song_fetch_retry_seconds = 0.05 + _quiz.song_loader.retry_seconds = 0.05 var songs_before: int = Playlist.song_object_array.size() var mp3: PackedByteArray = FileAccess.get_file_as_bytes("res://tests/data/short.mp3") _server.respond_bytes("/music/rand", PackedByteArray(), 503) var rand_requests_before: int = _server.count("/music/rand") - _quiz.fetch_song() + _quiz.song_loader.fetch_song() await await_millis(200) assert_int(_server.count("/music/rand") - rand_requests_before).is_greater(1) @@ -102,13 +102,13 @@ func test_failed_song_download_is_retried_until_the_server_answers() -> void: func test_failed_song_info_retries_only_that_step() -> void: - _quiz.song_fetch_retry_seconds = 0.05 + _quiz.song_loader.retry_seconds = 0.05 var songs_before: int = Playlist.song_object_array.size() _server.respond_bytes("/music/info", PackedByteArray(), 503) var rand_before: int = _server.count("/music/rand") var queue_before: int = _server.count("/music/addQue") - _quiz.fetch_song() + _quiz.song_loader.fetch_song() await await_millis(200) _next_song_is(SONG_INFO[1]) await await_millis(200) @@ -124,7 +124,7 @@ func test_failed_song_info_retries_only_that_step() -> void: func test_preloads_as_many_songs_as_the_cache_setting() -> void: Playlist.number_of_tracks_to_preload = 3 - _quiz.fetch_song() + _quiz.song_loader.fetch_song() assert_int(Playlist.get_preloaded_songs()).is_equal(3) assert_bool(Playlist.is_song_downloading).is_false() @@ -132,7 +132,7 @@ func test_preloads_as_many_songs_as_the_cache_setting() -> void: func test_playing_a_song_tops_the_cache_up_again() -> void: Playlist.number_of_tracks_to_preload = 3 - _quiz.fetch_song() + _quiz.song_loader.fetch_song() await _play_first_song() @@ -141,13 +141,13 @@ func test_playing_a_song_tops_the_cache_up_again() -> void: func test_cache_keeps_filling_after_a_failed_download() -> void: - _quiz.song_fetch_retry_seconds = 0.05 + _quiz.song_loader.retry_seconds = 0.05 Playlist.number_of_tracks_to_preload = 3 var mp3: PackedByteArray = FileAccess.get_file_as_bytes("res://tests/data/short.mp3") # One more download works, then the server stops answering _server.fail_after("/music/rand", 1) - _quiz.fetch_song() + _quiz.song_loader.fetch_song() await await_millis(150) assert_int(Playlist.get_preloaded_songs()).is_equal(2) @@ -161,7 +161,7 @@ func test_cache_keeps_filling_after_a_failed_download() -> void: func _open_settings() -> void: - _press("SettingsButton") + _press("QuizToolbar/SettingsButton") func _close_settings() -> void: @@ -217,14 +217,14 @@ func test_closing_settings_with_full_cache_downloads_nothing() -> void: func test_show_answer_before_first_song_does_not_reveal_next_song() -> void: - _press("ShowAnswerButton") + _press("SongControls/ShowAnswerButton") assert_bool(Playlist.song_object_array[0].is_answered).is_false() assert_int(_server.count("/music/played?song=-1")).is_equal(0) await _play_first_song() - assert_str((_quiz.get_node("VBoxContainer/SoundtrackLabel") as Label).text) \ + assert_str((_quiz.get_node("NowPlaying/SoundtrackLabel") as Label).text) \ .is_equal("????????") diff --git a/tests/integration/test_main_window_startup.gd b/tests/integration/test_main_window_startup.gd index 27e9fb4..cc03165 100644 --- a/tests/integration/test_main_window_startup.gd +++ b/tests/integration/test_main_window_startup.gd @@ -28,7 +28,7 @@ func test_next_plays_the_preloaded_song_after_a_slow_song_list() -> void: func test_failed_song_list_is_retried_before_preloading() -> void: - _quiz.song_fetch_retry_seconds = 0.05 + _quiz.song_loader.retry_seconds = 0.05 _server.respond_json("/music/list", [], 503) _server.release("/music/list") diff --git a/tests/integration/test_music_player_scene.gd b/tests/integration/test_music_player_scene.gd new file mode 100644 index 0000000..0297795 --- /dev/null +++ b/tests/integration/test_music_player_scene.gd @@ -0,0 +1,50 @@ +extends GdUnitTestSuite +## The music player scene: the player bar and the auto repeat switch under it. + +var _snapshot: AutoloadSnapshot +var _runner: GdUnitSceneRunner +var _player: MusicPlayer + + +func before_test() -> void: + _snapshot = AutoloadSnapshot.take([Settings]) + Settings.auto_repeat_song = true + _runner = scene_runner("res://MusicPlayer.tscn") + _player = _runner.scene() + + +func after_test() -> void: + _snapshot.restore() + + +func _click(control: Control) -> void: + _runner.set_mouse_position(_player.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_auto_repeat_starts_on() -> void: + assert_bool(_player.auto_repeat_button.button_pressed).is_true() + + +func test_clicking_auto_repeat_turns_it_off_and_on() -> void: + await _click(_player.auto_repeat_button) + assert_bool(Settings.auto_repeat_song).is_false() + assert_bool(_player.auto_repeat_button.button_pressed).is_false() + + await _click(_player.auto_repeat_button) + assert_bool(Settings.auto_repeat_song).is_true() + + +func test_auto_repeat_is_under_the_player_bar() -> void: + await _runner.simulate_frames(1) + var bar: Rect2 = (_player.get_node("PlayerBar") as Control).get_global_rect() + var repeat: Rect2 = _player.auto_repeat_button.get_global_rect() + + assert_float(repeat.position.y).is_greater_equal(bar.end.y) + assert_bool(_player.get_global_rect().encloses(repeat)).is_true() + + +func test_empty_space_lets_clicks_through() -> void: + assert_int(_player.mouse_filter).is_equal(Control.MOUSE_FILTER_IGNORE) diff --git a/tests/integration/test_music_player_scene.gd.uid b/tests/integration/test_music_player_scene.gd.uid new file mode 100644 index 0000000..7f7c4a0 --- /dev/null +++ b/tests/integration/test_music_player_scene.gd.uid @@ -0,0 +1 @@ +uid://ck3ti57dgce3w diff --git a/tests/integration/test_now_playing.gd b/tests/integration/test_now_playing.gd new file mode 100644 index 0000000..86f726c --- /dev/null +++ b/tests/integration/test_now_playing.gd @@ -0,0 +1,34 @@ +extends GdUnitTestSuite +## The soundtrack and song shown at the top of the quiz. + +var _runner: GdUnitSceneRunner +var _now_playing: NowPlaying + + +func before_test() -> void: + _runner = scene_runner("res://NowPlaying.tscn") + _now_playing = _runner.scene() + + +func _label(label_name: String) -> Label: + return _now_playing.get_node(label_name) + + +func test_starts_empty() -> void: + assert_str(_label("SoundtrackLabel").text).is_empty() + assert_str(_label("SongLabel").text).is_empty() + + +func test_show_song_shows_soundtrack_and_song() -> void: + _now_playing.show_song("Chrono Trigger", "Corridors of Time") + + assert_str(_label("SoundtrackLabel").text).is_equal("Chrono Trigger") + assert_str(_label("SongLabel").text).is_equal("Corridors of Time") + + +func test_soundtrack_is_above_song() -> void: + _now_playing.show_song("Chrono Trigger", "Corridors of Time") + await _runner.simulate_frames(1) + + assert_float(_label("SoundtrackLabel").get_global_rect().end.y) \ + .is_less_equal(_label("SongLabel").get_global_rect().position.y) diff --git a/tests/integration/test_now_playing.gd.uid b/tests/integration/test_now_playing.gd.uid new file mode 100644 index 0000000..fcd248d --- /dev/null +++ b/tests/integration/test_now_playing.gd.uid @@ -0,0 +1 @@ +uid://cidytlxgrmlk0 diff --git a/tests/integration/test_popups.gd b/tests/integration/test_popups.gd index cd45caa..ad0ae80 100644 --- a/tests/integration/test_popups.gd +++ b/tests/integration/test_popups.gd @@ -80,7 +80,7 @@ func test_click_inside_popup_keeps_it_open() -> void: func test_closing_click_does_not_press_button_behind_popup() -> void: var settings: PopupWindow = _popup("SettingsWindow") - var reset_button: Button = _quiz.get_node("ResetPlaylistButton") + var reset_button: Button = _quiz.get_node("QuizToolbar/ResetPlaylistButton") settings.open() await _runner.simulate_frames(1) assert_bool(settings.panel.get_global_rect().intersects(reset_button.get_global_rect())) \ @@ -93,7 +93,7 @@ func test_closing_click_does_not_press_button_behind_popup() -> void: func test_button_works_again_after_popup_closed() -> void: - var reset_button: Button = _quiz.get_node("ResetPlaylistButton") + var reset_button: Button = _quiz.get_node("QuizToolbar/ResetPlaylistButton") await _click_at(reset_button.get_global_rect().get_center()) diff --git a/tests/integration/test_quiz_toolbar.gd b/tests/integration/test_quiz_toolbar.gd new file mode 100644 index 0000000..8ac2ca8 --- /dev/null +++ b/tests/integration/test_quiz_toolbar.gd @@ -0,0 +1,69 @@ +extends GdUnitTestSuite +## The row of quiz buttons: each button reports its press, and locking disables the +## buttons that use the song list. + +const BUTTON_SIGNALS: Dictionary = { + "ResetPlaylistButton": "reset_playlist_pressed", + "ResetPointsButton": "reset_points_pressed", + "SettingsButton": "settings_pressed", + "SoundTestButton": "sound_test_pressed", +} + +var _runner: GdUnitSceneRunner +var _toolbar: QuizToolbar + + +func before_test() -> void: + _runner = scene_runner("res://QuizToolbar.tscn") + _toolbar = _runner.scene() + + +func _button(button_name: String) -> Button: + return _toolbar.get_node(button_name) + + +func _click(button_name: String) -> void: + _runner.set_mouse_position(_toolbar.get_viewport().get_final_transform() + * _button(button_name).get_global_rect().get_center()) + await _runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) + await _runner.await_input_processed() + + +func test_clicking_each_button_reports_it() -> void: + for button_name: String in BUTTON_SIGNALS: + var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_toolbar)) + + await _click(button_name) + + await signals.override_failure_message(button_name + " not reported") \ + .is_emitted(BUTTON_SIGNALS[button_name]) + + +func test_buttons_are_in_order_left_to_right() -> void: + var lefts: Array = ["ResetPlaylistButton", "ResetPointsButton", "SettingsButton", + "SoundTestButton"].map(func(n: String) -> float: return _button(n).position.x) + + for i: int in lefts.size() - 1: + assert_float(lefts[i]).is_less(lefts[i + 1]) + + +func test_locking_disables_only_buttons_that_use_the_song_list() -> void: + _toolbar.set_locked(true) + + assert_bool(_button("ResetPlaylistButton").disabled).is_true() + assert_bool(_button("SoundTestButton").disabled).is_true() + assert_bool(_button("ResetPointsButton").disabled).is_false() + assert_bool(_button("SettingsButton").disabled).is_false() + + +func test_unlocking_enables_them_again() -> void: + _toolbar.set_locked(true) + + _toolbar.set_locked(false) + + assert_bool(_button("ResetPlaylistButton").disabled).is_false() + assert_bool(_button("SoundTestButton").disabled).is_false() + + +func test_clicks_between_buttons_go_through() -> void: + assert_int(_toolbar.mouse_filter).is_equal(Control.MOUSE_FILTER_IGNORE) diff --git a/tests/integration/test_quiz_toolbar.gd.uid b/tests/integration/test_quiz_toolbar.gd.uid new file mode 100644 index 0000000..2e37dc3 --- /dev/null +++ b/tests/integration/test_quiz_toolbar.gd.uid @@ -0,0 +1 @@ +uid://ecsdudhpw8oi diff --git a/tests/integration/test_scene_split.gd b/tests/integration/test_scene_split.gd index b1455fc..6e61f5b 100644 --- a/tests/integration/test_scene_split.gd +++ b/tests/integration/test_scene_split.gd @@ -51,11 +51,21 @@ func test_top_bar_is_drawn_below_the_quiz() -> void: func test_quiz_parts_are_their_own_scenes() -> void: - for node_name: String in ["MusicListPanel", "PlayersPanel", "InspirationList"]: + for node_name: String in ["MusicListPanel", "PlayersPanel", "InspirationList", + "NowPlaying", "QuizToolbar", "SongControls", "MusicPlayer"]: assert_str(_quiz.get_node(node_name).scene_file_path) \ .is_equal("res://%s.tscn" % node_name) +func test_quiz_downloads_songs_with_a_song_loader() -> void: + assert_object(_quiz.song_loader).is_instanceof(SongLoader) + + +func test_auto_repeat_is_part_of_the_music_player() -> void: + assert_object(_quiz.get_node("MusicPlayer/RepeatSongCheckButton")).is_instanceof(CheckButton) + assert_object(_quiz.get_node_or_null("RepeatSongCheckButton")).is_null() + + 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) @@ -83,22 +93,22 @@ func test_clicking_each_top_bar_button_opens_its_popup() -> void: func test_clicking_quiz_buttons_plays_and_shows_answer() -> void: - await _click_at(_center(_quiz.get_node("NextButton"))) + await _click_at(_center(_quiz.get_node("SongControls/NextButton"))) await _runner.simulate_frames(2) assert_int(Playlist.currently_playing_song).is_equal(0) - assert_str((_quiz.get_node("VBoxContainer/SoundtrackLabel") as Label).text) \ + assert_str((_quiz.get_node("NowPlaying/SoundtrackLabel") as Label).text) \ .is_not_equal("Chrono Trigger") - await _click_at(_center(_quiz.get_node("ShowAnswerButton"))) + await _click_at(_center(_quiz.get_node("SongControls/ShowAnswerButton"))) - assert_str((_quiz.get_node("VBoxContainer/SoundtrackLabel") as Label).text) \ + assert_str((_quiz.get_node("NowPlaying/SoundtrackLabel") as Label).text) \ .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") + _press("SongControls/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) @@ -137,6 +147,30 @@ func test_character_button_in_the_player_list_picks_a_new_character() -> void: assert_object(crono.character).is_same(texture) +func test_sound_test_button_fetches_the_sound_test_song() -> void: + _server.respond_bytes("/music/soundTest", FileAccess.get_file_as_bytes("res://tests/data/short.mp3")) + + await _click_at(_center(_quiz.get_node("QuizToolbar/SoundTestButton"))) + + assert_int(_server.count("/music/soundTest")).is_equal(1) + + +func test_loaded_song_list_shows_in_the_music_list() -> void: + # No new song finishes downloading, so only loading the list can show the songs + _server.hold("/music/rand") + _server.respond_json("/music/list", [ + {"SongNo": 0, "Soundtrack": "Chrono Trigger", "Song": "Corridors of Time"}, + {"SongNo": 1, "Soundtrack": "Final Fantasy VI", "Song": "Dancing Mad"}, + ]) + Playlist.is_song_downloading = false + + _quiz.song_loader.load_song_list() + + var rows: Array[Node] = _quiz.get_node("MusicListPanel/ScrollContainer/MusicList").get_children() + assert_array(rows.map(func(row: SongListObject) -> int: return row.song_object.song_number)) \ + .contains_exactly([0, 1]) + + func test_quiz_popup_backdrop_covers_the_top_bar() -> void: var settings: PopupWindow = _quiz.get_node("SettingsWindow") settings.open() @@ -169,8 +203,8 @@ func test_sync_disables_quiz_buttons_until_finished() -> void: _top_bar_button("SyncButton").pressed.emit() sync_window.sync_timer.stop() - for button_name: String in ["SoundTestButton", "ResetPlaylistButton", "ShowAnswerButton", - "NextButton"]: + for button_name: String in ["QuizToolbar/SoundTestButton", "QuizToolbar/ResetPlaylistButton", "SongControls/ShowAnswerButton", + "SongControls/NextButton"]: assert_bool((_quiz.get_node(button_name) as Button).disabled) \ .override_failure_message(button_name + " not disabled").is_true() assert_bool(_top_bar_button("SyncButton").disabled).is_false() @@ -180,8 +214,8 @@ func test_sync_disables_quiz_buttons_until_finished() -> void: sync_window._on_sync_timeout() await _runner.simulate_frames(2) - for button_name: String in ["SoundTestButton", "ResetPlaylistButton", "ShowAnswerButton", - "NextButton"]: + for button_name: String in ["QuizToolbar/SoundTestButton", "QuizToolbar/ResetPlaylistButton", "SongControls/ShowAnswerButton", + "SongControls/NextButton"]: assert_bool((_quiz.get_node(button_name) as Button).disabled) \ .override_failure_message(button_name + " still disabled").is_false() # The search list in the main window and the quiz playlist are both reloaded diff --git a/tests/integration/test_song_controls.gd b/tests/integration/test_song_controls.gd new file mode 100644 index 0000000..7d34630 --- /dev/null +++ b/tests/integration/test_song_controls.gd @@ -0,0 +1,85 @@ +extends GdUnitTestSuite +## Show answer and Next: each reports its press, also from its keyboard shortcut, and +## Next is only enabled while a song is ready. + +var _runner: GdUnitSceneRunner +var _controls: SongControls + + +func before_test() -> void: + _runner = scene_runner("res://SongControls.tscn") + _controls = _runner.scene() + + +func _click(button: Button) -> void: + _runner.set_mouse_position(_controls.get_viewport().get_final_transform() + * button.get_global_rect().get_center()) + await _runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) + await _runner.await_input_processed() + + +func _press_keys(keys: Array[Key]) -> void: + for key: Key in keys: + _runner.simulate_key_press(key) + await _runner.await_input_processed() + keys.reverse() + for key: Key in keys: + _runner.simulate_key_release(key) + await _runner.await_input_processed() + + +func test_next_starts_disabled() -> void: + assert_bool(_controls.next_button.disabled).is_true() + + +func test_clicking_show_answer_reports_it() -> void: + var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_controls)) + + await _click(_controls.show_answer_button) + + await signals.is_emitted("show_answer_pressed") + + +func test_clicking_next_reports_it_when_enabled() -> void: + _controls.set_next_enabled(true) + var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_controls)) + + await _click(_controls.next_button) + + await signals.is_emitted("next_pressed") + + +func test_clicking_disabled_next_does_nothing() -> void: + var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_controls)) + + await _click(_controls.next_button) + + await signals.wait_until(200).is_not_emitted("next_pressed") + + +func test_alt_v_shows_answer_and_alt_c_plays_next() -> void: + _controls.set_next_enabled(true) + var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_controls)) + + await _press_keys([KEY_ALT, KEY_V]) + await signals.is_emitted("show_answer_pressed") + await _press_keys([KEY_ALT, KEY_C]) + await signals.is_emitted("next_pressed") + + +func test_locking_disables_both() -> void: + _controls.set_next_enabled(true) + + _controls.set_locked(true) + + assert_bool(_controls.show_answer_button.disabled).is_true() + assert_bool(_controls.next_button.disabled).is_true() + + +func test_unlocking_enables_show_answer_but_waits_for_a_song_for_next() -> void: + _controls.set_locked(true) + + _controls.set_locked(false) + + assert_bool(_controls.show_answer_button.disabled).is_false() + assert_bool(_controls.next_button.disabled).is_true() diff --git a/tests/integration/test_song_controls.gd.uid b/tests/integration/test_song_controls.gd.uid new file mode 100644 index 0000000..2c0c817 --- /dev/null +++ b/tests/integration/test_song_controls.gd.uid @@ -0,0 +1 @@ +uid://c43ii6v73reks diff --git a/tests/integration/test_song_loader.gd b/tests/integration/test_song_loader.gd new file mode 100644 index 0000000..a71fe47 --- /dev/null +++ b/tests/integration/test_song_loader.gd @@ -0,0 +1,122 @@ +extends GdUnitTestSuite +## Downloading songs into the Playlist ahead of time, against a fake server. + +const SONG_INFO: Dictionary = {"SongNo": 0, "Soundtrack": "Chrono Trigger", "Song": "Corridors of Time"} + +var _snapshot: AutoloadSnapshot +var _server: FakeHttpTransport +var _loader: SongLoader + + +func before_test() -> void: + _snapshot = AutoloadSnapshot.take([Settings, Playlist]) + Playlist.reset_playlist() + Playlist.is_song_downloading = false + Playlist.number_of_tracks_to_preload = 1 + Settings.use_low_played_mode = false + _server = FakeHttpTransport.new().install() + _server.respond_json("/music/list", []) + _server.respond_bytes("/music/rand", FileAccess.get_file_as_bytes("res://tests/data/short.mp3")) + _server.respond_bytes("/music/addQue", PackedByteArray()) + _server.respond_json("/music/info", SONG_INFO) + _loader = auto_free(SongLoader.new()) + add_child(_loader) + + +func after_test() -> void: + _server.uninstall() + _snapshot.restore() + + +func test_load_song_list_adds_songs_already_played_on_the_server() -> void: + _server.respond_json("/music/list", [ + {"SongNo": 0, "Soundtrack": "Chrono Trigger", "Song": "Corridors of Time"}, + ]) + _server.respond_json("/music/info", {"SongNo": 1, "Soundtrack": "Final Fantasy VI", "Song": "Dancing Mad"}) + var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_loader)) + + _loader.load_song_list() + + await signals.is_emitted("song_list_loaded") + var first: SongObject = Playlist.song_object_array[0] + assert_str(first.soundtrack_title).is_equal("Chrono Trigger") + assert_bool(first.has_played).is_true() + assert_bool(first.is_answered).is_true() + + +func test_load_song_list_then_preloads_a_new_song() -> void: + _loader.load_song_list() + + assert_int(_server.count("/music/rand")).is_equal(1) + assert_int(Playlist.song_object_array.size()).is_equal(1) + + +func test_fetch_song_downloads_queues_and_adds_the_song() -> void: + var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_loader)) + + _loader.fetch_song() + + await signals.is_emitted("song_added") + assert_array(_server.requested_paths).contains_exactly(["/music/rand", "/music/addQue", "/music/info"]) + var song: SongObject = Playlist.song_object_array[0] + assert_str(song.song_title).is_equal("Corridors of Time") + assert_object(song.song).is_not_null() + assert_bool(Playlist.is_song_downloading).is_false() + + +func test_fetch_song_keeps_going_until_the_cache_is_full() -> void: + Playlist.number_of_tracks_to_preload = 3 + + _loader.fetch_song() + + assert_int(Playlist.song_object_array.size()).is_equal(3) + assert_int(_server.count("/music/rand")).is_equal(3) + + +func test_low_played_mode_uses_its_endpoint() -> void: + Settings.use_low_played_mode = true + _server.respond_bytes("/music/rand/low", FileAccess.get_file_as_bytes("res://tests/data/short.mp3")) + + _loader.fetch_song() + + assert_int(_server.count("/music/rand/low")).is_equal(1) + assert_int(_server.count("/music/rand")).is_equal(0) + + +func test_fetch_song_if_idle_does_nothing_while_downloading() -> void: + Playlist.is_song_downloading = true + + _loader.fetch_song_if_idle() + + assert_int(_server.count("/music/rand")).is_equal(0) + + +func test_fill_cache_does_nothing_when_the_cache_is_full() -> void: + _loader.fetch_song() + + _loader.fill_cache() + + assert_int(_server.count("/music/rand")).is_equal(1) + + +func test_fill_cache_downloads_after_the_cache_size_grows() -> void: + _loader.fetch_song() + Playlist.number_of_tracks_to_preload = 2 + + _loader.fill_cache() + + assert_int(_server.count("/music/rand")).is_equal(2) + + +func test_failed_step_is_retried_alone() -> void: + _loader.retry_seconds = 0.05 + _server.fail_after("/music/info", 0) + + _loader.fetch_song() + assert_int(Playlist.song_object_array.size()).is_equal(0) + _server.respond_json("/music/info", SONG_INFO) + await get_tree().create_timer(0.2).timeout + + assert_int(_server.count("/music/rand")).is_equal(1) + assert_int(_server.count("/music/addQue")).is_equal(1) + assert_int(Playlist.song_object_array.size()).is_equal(1) diff --git a/tests/integration/test_song_loader.gd.uid b/tests/integration/test_song_loader.gd.uid new file mode 100644 index 0000000..1826e9d --- /dev/null +++ b/tests/integration/test_song_loader.gd.uid @@ -0,0 +1 @@ +uid://caew6u3yi8m4n diff --git a/tests/integration/test_stats_after_win.gd b/tests/integration/test_stats_after_win.gd index 0a5bc60..f315af8 100644 --- a/tests/integration/test_stats_after_win.gd +++ b/tests/integration/test_stats_after_win.gd @@ -24,7 +24,7 @@ func test_winning_turns_statistics_off() -> void: func test_resetting_points_turns_statistics_back_on() -> void: await _give_point_with_keybind(1) - _press("ResetPointsButton") + _press("QuizToolbar/ResetPointsButton") assert_bool(Settings.add_to_stats).is_true() @@ -33,7 +33,7 @@ func test_statistics_turned_off_by_hand_stay_off_after_reset() -> void: Settings.add_to_stats = false await _give_point_with_keybind(1) - _press("ResetPointsButton") + _press("QuizToolbar/ResetPointsButton") assert_bool(Settings.add_to_stats).is_false() @@ -41,7 +41,7 @@ func test_statistics_turned_off_by_hand_stay_off_after_reset() -> void: func test_settings_checkbox_shows_statistics_off_after_win() -> void: await _give_point_with_keybind(1) - _press("SettingsButton") + _press("QuizToolbar/SettingsButton") assert_bool(_stats_checkbox().button_pressed).is_false() @@ -50,13 +50,13 @@ func test_songs_after_a_win_are_not_counted() -> void: await _give_point_with_keybind(1) var played_before: int = _server.count("/music/played?song=0") - _press("ShowAnswerButton") + _press("SongControls/ShowAnswerButton") assert_int(_server.count("/music/played?song=0")).is_equal(played_before) func test_winning_score_cannot_go_below_one() -> void: - _press("SettingsButton") + _press("QuizToolbar/SettingsButton") var lower_button: Button = _quiz.get_node("SettingsWindow").get_node("%LowerButton") for i: int in 3: