Split now playing, the quiz buttons and song loading out of the quiz

NowPlaying, QuizToolbar and SongControls become their own scenes, the auto
repeat switch moves into the music player scene, and downloading songs moves
into a SongLoader node. The quiz only wires the parts together. 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 new part has its own tests.
This commit is contained in:
Sansan committed 2026-10-06 11:11:18 +02:00
1 parent bad4b3ad8b
commit e94c75e84d
32 files changed
+820 -298

No files matched your search

+15 -6
View File
@@ -1,14 +1,16 @@
class_name MusicPlayer 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 20/1-24: 136
# LOC 12/6-24: 170 # LOC 12/6-24: 170
@onready @onready
var play_button: TextureButton = $MusicPlayerContainer/PlayTextureButton var play_button: TextureButton = $PlayerBar/MusicPlayerContainer/PlayTextureButton
@onready @onready
var restart_button: TextureButton = $MusicPlayerContainer/RestartTextureButton var restart_button: TextureButton = $PlayerBar/MusicPlayerContainer/RestartTextureButton
@onready @onready
var audio_player: AudioStreamPlayer = $AudioStreamPlayer var audio_player: AudioStreamPlayer = $AudioStreamPlayer
@@ -20,13 +22,16 @@ var sound_player: AudioStreamPlayer = $AudioSoundPlayer
var sound_effect_player: AudioStreamPlayer = $AudioSoundEffectPlayer var sound_effect_player: AudioStreamPlayer = $AudioSoundEffectPlayer
@onready @onready
var progress_slider: HSlider = $MusicPlayerContainer/MusicPlayerSlider var progress_slider: HSlider = $PlayerBar/MusicPlayerContainer/MusicPlayerSlider
@onready @onready
var volume_slider: HSlider = $MusicPlayerContainer/VolumeSlider var volume_slider: HSlider = $PlayerBar/MusicPlayerContainer/VolumeSlider
@onready @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 play_icon: Texture = preload("res://icons/play_icon_light.svg")
var pause_icon: Texture = preload("res://icons/pause_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) progress_slider.drag_ended.connect(_on_drag_ended)
audio_player.finished.connect(_on_finished) 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: func _process(_delta: float) -> void:
if audio_player.has_stream_playback() && !is_changing && !audio_player.stream_paused: if audio_player.has_stream_playback() && !is_changing && !audio_player.stream_paused:
+27 -19
View File
@@ -26,19 +26,11 @@ unicode = 90
[sub_resource type="Shortcut" id="Shortcut_i7swq"] [sub_resource type="Shortcut" id="Shortcut_i7swq"]
events = [SubResource("InputEventKey_2nvce")] events = [SubResource("InputEventKey_2nvce")]
[node name="MusicPlayer" type="PanelContainer"] [node name="MusicPlayer" type="Control"]
anchors_preset = 8 layout_mode = 3
anchor_left = 0.5 offset_right = 1792.0
anchor_top = 0.5 offset_bottom = 71.0
anchor_right = 0.5 mouse_filter = 2
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
script = ExtResource("1_t24ra") script = ExtResource("1_t24ra")
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."] [node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."]
@@ -53,11 +45,17 @@ bus = &"music"
stream = ExtResource("2_xti80") stream = ExtResource("2_xti80")
bus = &"music" 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 layout_mode = 2
size_flags_vertical = 4 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) custom_minimum_size = Vector2(32, 32)
layout_mode = 2 layout_mode = 2
focus_mode = 0 focus_mode = 0
@@ -67,7 +65,7 @@ texture_normal = ExtResource("3_6g308")
ignore_texture_size = true ignore_texture_size = true
stretch_mode = 0 stretch_mode = 0
[node name="RestartTextureButton" type="TextureButton" parent="MusicPlayerContainer"] [node name="RestartTextureButton" type="TextureButton" parent="PlayerBar/MusicPlayerContainer"]
custom_minimum_size = Vector2(32, 32) custom_minimum_size = Vector2(32, 32)
layout_mode = 2 layout_mode = 2
focus_mode = 0 focus_mode = 0
@@ -78,17 +76,27 @@ ignore_texture_size = true
stretch_mode = 0 stretch_mode = 0
flip_h = true flip_h = true
[node name="MusicPlayerSlider" type="HSlider" parent="MusicPlayerContainer"] [node name="MusicPlayerSlider" type="HSlider" parent="PlayerBar/MusicPlayerContainer"]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 4 size_flags_vertical = 4
scrollable = false scrollable = false
[node name="MusicTimeLabel" type="Label" parent="MusicPlayerContainer"] [node name="MusicTimeLabel" type="Label" parent="PlayerBar/MusicPlayerContainer"]
layout_mode = 2 layout_mode = 2
text = "0:00 / 0:00" 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) custom_minimum_size = Vector2(100, 0)
layout_mode = 2 layout_mode = 2
size_flags_vertical = 4 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"
+29
View File
@@ -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
+12 -110
View File
@@ -9,30 +9,10 @@
[ext_resource type="PackedScene" path="res://MusicListPanel.tscn" id="scene_MusicListPanel"] [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://PlayersPanel.tscn" id="scene_PlayersPanel"]
[ext_resource type="PackedScene" path="res://InspirationList.tscn" id="scene_InspirationList"] [ext_resource type="PackedScene" path="res://InspirationList.tscn" id="scene_InspirationList"]
[ext_resource type="PackedScene" path="res://QuizToolbar.tscn" id="scene_QuizToolbar"]
[sub_resource type="LabelSettings" id="LabelSettings_qspbu"] [ext_resource type="PackedScene" path="res://SongControls.tscn" id="scene_SongControls"]
font_size = 50 [ext_resource type="PackedScene" path="res://NowPlaying.tscn" id="scene_NowPlaying"]
[ext_resource type="Script" path="res://song_loader.gd" id="song_loader_script"]
[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")]
[node name="Quiz" type="Control" unique_id=1146705218] [node name="Quiz" type="Control" unique_id=1146705218]
layout_mode = 3 layout_mode = 3
@@ -76,40 +56,19 @@ offset_top = 128.0
offset_right = 1857.0 offset_right = 1857.0
offset_bottom = 512.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 layout_mode = 0
offset_left = 64.0 offset_left = 64.0
offset_top = 64.0 offset_top = 64.0
offset_right = 1152.0 offset_right = 1152.0
offset_bottom = 384.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")] [node name="MusicPlayer" parent="." unique_id=1990459832 instance=ExtResource("5_emn36")]
layout_mode = 0 layout_mode = 0
anchors_preset = 0 offset_left = 64.0
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
offset_left = 65.0
offset_top = 992.0 offset_top = 992.0
offset_right = 1856.0 offset_right = 1856.0
offset_bottom = 1024.0 offset_bottom = 1063.0
grow_horizontal = 1
grow_vertical = 1
[node name="PlayersPanel" parent="." unique_id=1708375945 instance=ExtResource("scene_PlayersPanel")] [node name="PlayersPanel" parent="." unique_id=1708375945 instance=ExtResource("scene_PlayersPanel")]
layout_mode = 0 layout_mode = 0
@@ -118,68 +77,19 @@ offset_top = 576.0
offset_right = 1857.0 offset_right = 1857.0
offset_bottom = 977.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 layout_mode = 0
offset_left = 1360.0 offset_left = 1360.0
offset_top = 64.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_right = 1855.0
offset_bottom = 95.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 layout_mode = 0
offset_left = 65.0 offset_left = 65.0
offset_top = 944.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_right = 368.0
offset_bottom = 975.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")] [node name="SettingsWindow" parent="." unique_id=1323246978 instance=ExtResource("11_k62u5")]
visible = false visible = false
@@ -207,17 +117,6 @@ offset_top = 153.0
offset_right = 720.0 offset_right = 720.0
offset_bottom = 153.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] [node name="DebugLabel" type="Label" parent="." unique_id=1959221257]
visible = false visible = false
layout_mode = 0 layout_mode = 0
@@ -240,4 +139,7 @@ offset_top = 132.0
offset_right = 1262.0 offset_right = 1262.0
offset_bottom = 832.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"] [connection signal="dir_selected" from="FileDialog" to="." method="_on_file_dialog_dir_selected"]
+45
View File
@@ -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"
+48
View File
@@ -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"
+10
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://beoi75d4u568h
+27 -119
View File
@@ -21,25 +21,13 @@ var fileDialog: FileDialog = $FileDialog
var inspiration_list: InspirationList = $InspirationList var inspiration_list: InspirationList = $InspirationList
@onready @onready
var soundtrack_label: Label = $VBoxContainer/SoundtrackLabel var now_playing: NowPlaying = $NowPlaying
@onready
var song_label: Label = $VBoxContainer/SongLabel
@onready @onready
var players_panel: PlayersPanel = $PlayersPanel var players_panel: PlayersPanel = $PlayersPanel
@onready @onready
var reset_playlist_button: Button = $ResetPlaylistButton var toolbar: QuizToolbar = $QuizToolbar
@onready
var reset_points_button: Button = $ResetPointsButton
@onready
var sound_test_button: Button = $SoundTestButton
@onready
var settings_button: Button = $SettingsButton
@onready @onready
var settings_window: Control = $SettingsWindow var settings_window: Control = $SettingsWindow
@@ -48,10 +36,10 @@ var settings_window: Control = $SettingsWindow
var winner_window: Control = %WinnerWindow var winner_window: Control = %WinnerWindow
@onready @onready
var show_answer_button: Button = $ShowAnswerButton var song_controls: SongControls = $SongControls
@onready @onready
var next_button: Button = $NextButton var song_loader: SongLoader = $SongLoader
@onready @onready
var music_list_panel: MusicListPanel = $MusicListPanel var music_list_panel: MusicListPanel = $MusicListPanel
@@ -60,10 +48,7 @@ var music_list_panel: MusicListPanel = $MusicListPanel
var new_character_select: Control = %NewCharacterSelect var new_character_select: Control = %NewCharacterSelect
@onready @onready
var auto_repeat_song_button: CheckButton = $RepeatSongCheckButton var music_player_container: MusicPlayer = $MusicPlayer
@onready
var music_player_container: PanelContainer = $MusicPlayer
@onready @onready
var log_window: Control = %Log 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. ## Dims the window behind an open popup and keeps clicks from reaching the buttons under it.
var popup_backdrop: PopupBackdrop 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: func _ready() -> void:
popup_backdrop = PopupBackdrop.add_to(self, _popups()) popup_backdrop = PopupBackdrop.add_to(self, _popups())
@@ -92,12 +74,14 @@ func _ready() -> void:
Settings.is_debug = true Settings.is_debug = true
settings_window.set_winning_score(5) settings_window.set_winning_score(5)
settings_window.set_selected_server(4) settings_window.set_selected_server(4)
next_button.pressed.connect(play_next_song) song_controls.next_pressed.connect(play_next_song)
show_answer_button.pressed.connect(show_answer_pressed) song_controls.show_answer_pressed.connect(show_answer_pressed)
sound_test_button.pressed.connect(music_player_container.get_sound_test_song) toolbar.sound_test_pressed.connect(music_player_container.get_sound_test_song)
settings_button.pressed.connect(show_settings) toolbar.settings_pressed.connect(show_settings)
reset_playlist_button.pressed.connect(reset_playlist) toolbar.reset_playlist_pressed.connect(reset_playlist)
reset_points_button.pressed.connect(reset_points) 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) new_character_select.connect("new_new_character_selected", _on_new_character_selected)
music_list_panel.song_clicked.connect(song_object_clicked) 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) settings_window.visibility_changed.connect(_on_settings_window_visibility_changed)
open_button.pressed.connect(open) open_button.pressed.connect(open)
auto_repeat_song_button.pressed.connect(repeat_song)
inspiration_list.load_from_server() inspiration_list.load_from_server()
fetch_song_list_at_start() song_loader.load_song_list()
new_character_select.load_characters() 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. ## Disables the buttons that use the song list while the server syncs it.
func lock_for_sync() -> void: func lock_for_sync() -> void:
sound_test_button.disabled = true toolbar.set_locked(true)
reset_playlist_button.disabled = true song_controls.set_locked(true)
show_answer_button.disabled = true
next_button.disabled = true
## Reloads from the server after a sync and starts a new game. ## Reloads from the server after a sync and starts a new game.
func on_sync_finished() -> void: func on_sync_finished() -> void:
refresh_from_server() refresh_from_server()
sound_test_button.disabled = false toolbar.set_locked(false)
reset_playlist_button.disabled = false song_controls.set_locked(false)
show_answer_button.disabled = false
reset_points() reset_points()
@@ -170,41 +150,12 @@ func _on_server_changed() -> void:
server_changed.emit() server_changed.emit()
func repeat_song() -> void:
Settings.auto_repeat_song = !Settings.auto_repeat_song
func show_answer_pressed() -> void: func show_answer_pressed() -> void:
print("show_answer_pressed") print("show_answer_pressed")
Playlist.song_is_answered() Playlist.song_is_answered()
update_song_list() 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: func play_next_song() -> void:
print("play_next_song ", Playlist.latest_played_song) print("play_next_song ", Playlist.latest_played_song)
if Playlist.first_song_played: if Playlist.first_song_played:
@@ -212,9 +163,8 @@ func play_next_song() -> void:
Playlist.first_song_played = true Playlist.first_song_played = true
music_player_container.play_song_object(Playlist.latest_played_song) music_player_container.play_song_object(Playlist.latest_played_song)
if !(Playlist.get_preloaded_songs() > 0): if !(Playlist.get_preloaded_songs() > 0):
next_button.disabled = true song_controls.set_next_enabled(false)
if !Playlist.is_song_downloading: song_loader.fetch_song_if_idle()
fetch_song()
func _popups() -> Array[PopupWindow]: 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 # 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. # avoids starting downloads while the user is still clicking the number up and down.
func _on_settings_window_visibility_changed() -> void: func _on_settings_window_visibility_changed() -> void:
if !settings_window.visible && !Playlist.is_song_downloading && Playlist.should_preload_more_songs(): if !settings_window.visible:
fetch_song() song_loader.fill_cache()
func fetch_song() -> void: func _on_song_added() -> void:
print("fetch_song") song_controls.set_next_enabled(true)
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: if Settings.is_debug:
update_song_list() 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 update_song_list() -> void: func update_song_list() -> void:
print("update_song_list") print("update_song_list")
if Playlist.currently_playing_song >= 0: if Playlist.currently_playing_song >= 0:
soundtrack_label.text = Playlist.get_current_soundtrack_title() now_playing.show_song(Playlist.get_current_soundtrack_title(),
song_label.text = Playlist.get_current_song_title() Playlist.get_current_song_title())
music_list_panel.refresh() music_list_panel.refresh()
@@ -304,7 +212,7 @@ func reset_playlist() -> void:
print("reset_playlist") print("reset_playlist")
var playlist_reset_done: Callable = func playlist_reset_done() -> void: var playlist_reset_done: Callable = func playlist_reset_done() -> void:
Playlist.reset_playlist() Playlist.reset_playlist()
fetch_song_list_at_start() song_loader.load_song_list()
MusicServerClient.reset_playlist(playlist_reset_done) MusicServerClient.reset_playlist(playlist_reset_done)
+25
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://dmsy36s1ht7x5
+27
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://d6nevh66yaw1
+100
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://dwqirg7x7mmj6
+1 -1
View File
@@ -70,7 +70,7 @@ func _press(button_path: String) -> void:
## Presses Next and waits until the first song is playing. ## Presses Next and waits until the first song is playing.
func _play_first_song() -> void: func _play_first_song() -> void:
_press("NextButton") _press("SongControls/NextButton")
await _runner.simulate_frames(2) await _runner.simulate_frames(2)
+13 -5
View File
@@ -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. ## Nodes that moved from the main window into the Quiz scene, by their old name.
const QUIZ_NODES: Array[String] = [ const QUIZ_NODES: Array[String] = [
"Open", "FileDialog", "MusicListPanel", "VBoxContainer", "Open", "FileDialog", "MusicListPanel", "DebugLabel", "SettingsWindow",
"MusicPlayer", "ResetPlaylistButton", "ResetPointsButton", "SettingsButton", "NewCharacterSelect", "WinnerWindow", "Log",
"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. ## Nodes that moved into the Quiz scene under another path: old name to new path in
## the quiz.
const RENAMED_QUIZ_NODES: Dictionary = { const RENAMED_QUIZ_NODES: Dictionary = {
"Players": "PlayersPanel", "Players": "PlayersPanel",
"ScrollContainer": "InspirationList", "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. ## Nodes that moved from the main window into the TopBar scene, by their old name.
+20 -20
View File
@@ -7,7 +7,7 @@ func test_startup_loads_soundtracks_and_preloads_a_song() -> void:
assert_int(_server.count("/music/all")).is_equal(1) assert_int(_server.count("/music/all")).is_equal(1)
assert_array(_server.requested_paths).contains(["/music/rand", "/music/addQue", "/music/info"]) assert_array(_server.requested_paths).contains(["/music/rand", "/music/addQue", "/music/info"])
assert_int(Playlist.song_object_array.size()).is_equal(1) 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: 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( _server.respond_bytes(
"/music/rand/low", FileAccess.get_file_as_bytes("res://tests/data/short.mp3")) "/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) 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: func test_next_plays_preloaded_song_and_fetches_another() -> void:
_next_song_is(SONG_INFO[1]) _next_song_is(SONG_INFO[1])
_press("NextButton") _press("SongControls/NextButton")
await _runner.simulate_frames(2) await _runner.simulate_frames(2)
assert_int(Playlist.currently_playing_song).is_equal(0) 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: func test_title_is_hidden_until_answer_is_shown() -> void:
var soundtrack_label: Label = _quiz.get_node("VBoxContainer/SoundtrackLabel") var soundtrack_label: Label = _quiz.get_node("NowPlaying/SoundtrackLabel")
_press("NextButton") _press("SongControls/NextButton")
await _runner.simulate_frames(2) await _runner.simulate_frames(2)
assert_str(soundtrack_label.text).is_equal("????????") assert_str(soundtrack_label.text).is_equal("????????")
_press("ShowAnswerButton") _press("SongControls/ShowAnswerButton")
assert_str(soundtrack_label.text).is_equal("Chrono Trigger") assert_str(soundtrack_label.text).is_equal("Chrono Trigger")
assert_int(_server.count("/music/played?song=0")).is_equal(1) assert_int(_server.count("/music/played?song=0")).is_equal(1)
func test_reset_playlist_clears_server_and_preloads_again() -> void: func test_reset_playlist_clears_server_and_preloads_again() -> void:
_press("NextButton") _press("SongControls/NextButton")
await _runner.simulate_frames(2) await _runner.simulate_frames(2)
_press("ResetPlaylistButton") _press("QuizToolbar/ResetPlaylistButton")
assert_int(_server.count("/music/reset")).is_equal(1) assert_int(_server.count("/music/reset")).is_equal(1)
assert_int(Playlist.currently_playing_song).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("Crono").add_point()
_add_player("Marle").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[0].player_score).is_equal(0)
assert_int(Settings.player_array[1].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: 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 songs_before: int = Playlist.song_object_array.size()
var mp3: PackedByteArray = FileAccess.get_file_as_bytes("res://tests/data/short.mp3") var mp3: PackedByteArray = FileAccess.get_file_as_bytes("res://tests/data/short.mp3")
_server.respond_bytes("/music/rand", PackedByteArray(), 503) _server.respond_bytes("/music/rand", PackedByteArray(), 503)
var rand_requests_before: int = _server.count("/music/rand") var rand_requests_before: int = _server.count("/music/rand")
_quiz.fetch_song() _quiz.song_loader.fetch_song()
await await_millis(200) await await_millis(200)
assert_int(_server.count("/music/rand") - rand_requests_before).is_greater(1) 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: 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() var songs_before: int = Playlist.song_object_array.size()
_server.respond_bytes("/music/info", PackedByteArray(), 503) _server.respond_bytes("/music/info", PackedByteArray(), 503)
var rand_before: int = _server.count("/music/rand") var rand_before: int = _server.count("/music/rand")
var queue_before: int = _server.count("/music/addQue") var queue_before: int = _server.count("/music/addQue")
_quiz.fetch_song() _quiz.song_loader.fetch_song()
await await_millis(200) await await_millis(200)
_next_song_is(SONG_INFO[1]) _next_song_is(SONG_INFO[1])
await await_millis(200) 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: func test_preloads_as_many_songs_as_the_cache_setting() -> void:
Playlist.number_of_tracks_to_preload = 3 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_int(Playlist.get_preloaded_songs()).is_equal(3)
assert_bool(Playlist.is_song_downloading).is_false() 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: func test_playing_a_song_tops_the_cache_up_again() -> void:
Playlist.number_of_tracks_to_preload = 3 Playlist.number_of_tracks_to_preload = 3
_quiz.fetch_song() _quiz.song_loader.fetch_song()
await _play_first_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: 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 Playlist.number_of_tracks_to_preload = 3
var mp3: PackedByteArray = FileAccess.get_file_as_bytes("res://tests/data/short.mp3") var mp3: PackedByteArray = FileAccess.get_file_as_bytes("res://tests/data/short.mp3")
# One more download works, then the server stops answering # One more download works, then the server stops answering
_server.fail_after("/music/rand", 1) _server.fail_after("/music/rand", 1)
_quiz.fetch_song() _quiz.song_loader.fetch_song()
await await_millis(150) await await_millis(150)
assert_int(Playlist.get_preloaded_songs()).is_equal(2) 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: func _open_settings() -> void:
_press("SettingsButton") _press("QuizToolbar/SettingsButton")
func _close_settings() -> void: 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: 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_bool(Playlist.song_object_array[0].is_answered).is_false()
assert_int(_server.count("/music/played?song=-1")).is_equal(0) assert_int(_server.count("/music/played?song=-1")).is_equal(0)
await _play_first_song() 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("????????") .is_equal("????????")
@@ -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: 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.respond_json("/music/list", [], 503)
_server.release("/music/list") _server.release("/music/list")
@@ -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)
@@ -0,0 +1 @@
uid://ck3ti57dgce3w
+34
View File
@@ -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)
@@ -0,0 +1 @@
uid://cidytlxgrmlk0
+2 -2
View File
@@ -80,7 +80,7 @@ func test_click_inside_popup_keeps_it_open() -> void:
func test_closing_click_does_not_press_button_behind_popup() -> void: func test_closing_click_does_not_press_button_behind_popup() -> void:
var settings: PopupWindow = _popup("SettingsWindow") var settings: PopupWindow = _popup("SettingsWindow")
var reset_button: Button = _quiz.get_node("ResetPlaylistButton") var reset_button: Button = _quiz.get_node("QuizToolbar/ResetPlaylistButton")
settings.open() settings.open()
await _runner.simulate_frames(1) await _runner.simulate_frames(1)
assert_bool(settings.panel.get_global_rect().intersects(reset_button.get_global_rect())) \ 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: 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()) await _click_at(reset_button.get_global_rect().get_center())
+69
View File
@@ -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)
@@ -0,0 +1 @@
uid://ecsdudhpw8oi
+44 -10
View File
@@ -51,11 +51,21 @@ func test_top_bar_is_drawn_below_the_quiz() -> void:
func test_quiz_parts_are_their_own_scenes() -> 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) \ assert_str(_quiz.get_node(node_name).scene_file_path) \
.is_equal("res://%s.tscn" % node_name) .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: func test_top_bar_holds_only_title_and_buttons() -> void:
assert_array(_children(_window.get_node("TopBar"))) \ assert_array(_children(_window.get_node("TopBar"))) \
.contains_exactly_in_any_order(TOP_BAR_CHILDREN) .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: 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) await _runner.simulate_frames(2)
assert_int(Playlist.currently_playing_song).is_equal(0) 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") .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") .is_equal("Chrono Trigger")
func test_clicking_a_song_in_the_music_list_plays_it() -> void: func test_clicking_a_song_in_the_music_list_plays_it() -> void:
_next_song_is(SONG_INFO[1]) _next_song_is(SONG_INFO[1])
await _play_first_song() await _play_first_song()
_press("NextButton") _press("SongControls/NextButton")
# Changing song waits a moment for the old one to stop # Changing song waits a moment for the old one to stop
await get_tree().create_timer(0.7).timeout await get_tree().create_timer(0.7).timeout
assert_int(Playlist.currently_playing_song).is_equal(1) 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) 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: func test_quiz_popup_backdrop_covers_the_top_bar() -> void:
var settings: PopupWindow = _quiz.get_node("SettingsWindow") var settings: PopupWindow = _quiz.get_node("SettingsWindow")
settings.open() settings.open()
@@ -169,8 +203,8 @@ func test_sync_disables_quiz_buttons_until_finished() -> void:
_top_bar_button("SyncButton").pressed.emit() _top_bar_button("SyncButton").pressed.emit()
sync_window.sync_timer.stop() sync_window.sync_timer.stop()
for button_name: String in ["SoundTestButton", "ResetPlaylistButton", "ShowAnswerButton", for button_name: String in ["QuizToolbar/SoundTestButton", "QuizToolbar/ResetPlaylistButton", "SongControls/ShowAnswerButton",
"NextButton"]: "SongControls/NextButton"]:
assert_bool((_quiz.get_node(button_name) as Button).disabled) \ assert_bool((_quiz.get_node(button_name) as Button).disabled) \
.override_failure_message(button_name + " not disabled").is_true() .override_failure_message(button_name + " not disabled").is_true()
assert_bool(_top_bar_button("SyncButton").disabled).is_false() 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() sync_window._on_sync_timeout()
await _runner.simulate_frames(2) await _runner.simulate_frames(2)
for button_name: String in ["SoundTestButton", "ResetPlaylistButton", "ShowAnswerButton", for button_name: String in ["QuizToolbar/SoundTestButton", "QuizToolbar/ResetPlaylistButton", "SongControls/ShowAnswerButton",
"NextButton"]: "SongControls/NextButton"]:
assert_bool((_quiz.get_node(button_name) as Button).disabled) \ assert_bool((_quiz.get_node(button_name) as Button).disabled) \
.override_failure_message(button_name + " still disabled").is_false() .override_failure_message(button_name + " still disabled").is_false()
# The search list in the main window and the quiz playlist are both reloaded # The search list in the main window and the quiz playlist are both reloaded
+85
View File
@@ -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()
@@ -0,0 +1 @@
uid://c43ii6v73reks
+122
View File
@@ -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)
@@ -0,0 +1 @@
uid://caew6u3yi8m4n
+5 -5
View File
@@ -24,7 +24,7 @@ func test_winning_turns_statistics_off() -> void:
func test_resetting_points_turns_statistics_back_on() -> void: func test_resetting_points_turns_statistics_back_on() -> void:
await _give_point_with_keybind(1) await _give_point_with_keybind(1)
_press("ResetPointsButton") _press("QuizToolbar/ResetPointsButton")
assert_bool(Settings.add_to_stats).is_true() 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 Settings.add_to_stats = false
await _give_point_with_keybind(1) await _give_point_with_keybind(1)
_press("ResetPointsButton") _press("QuizToolbar/ResetPointsButton")
assert_bool(Settings.add_to_stats).is_false() 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: func test_settings_checkbox_shows_statistics_off_after_win() -> void:
await _give_point_with_keybind(1) await _give_point_with_keybind(1)
_press("SettingsButton") _press("QuizToolbar/SettingsButton")
assert_bool(_stats_checkbox().button_pressed).is_false() 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) await _give_point_with_keybind(1)
var played_before: int = _server.count("/music/played?song=0") 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) assert_int(_server.count("/music/played?song=0")).is_equal(played_before)
func test_winning_score_cannot_go_below_one() -> void: 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") var lower_button: Button = _quiz.get_node("SettingsWindow").get_node("%LowerButton")
for i: int in 3: for i: int in 3: