Split the main window into TopBar and Quiz scenes

The quiz (song, playlist, players, music player and the popups opened from
it) moves into Quiz.tscn and the top panel into TopBar.tscn. The main window
keeps only the top bar, the quiz and the popups opened from the top bar, so
other views can be added next to the quiz later. Every node keeps its place
on screen.

Screenshots of every UI state before and after are pixel-identical, and a
layout test checks each node against a baseline taken before the split.
This commit is contained in:
Sansan committed 2026-10-06 10:02:22 +02:00
1 parent 75c59eb223
commit 3c7b4e5c9d
27 files changed
+3427 -975

No files matched your search

+52 -510
View File
@@ -1,66 +1,21 @@
extends Control
# 601 LOC 11/9 - 2023
# 628 LOC 15/9 - 2023
# 631 LOC 16/9 - 2023
# 554 LOC 30/5 - 2024
# 551 LOC 3/6 - 2024
# 386 LOC 12/6 - 2024
##TODO
# 15. Fix for local play
## The main window: the top bar, the popups opened from it and the quiz below it.
##
## Everything that is not part of a quiz lives here, so other views can be added next
## to the quiz later.
@onready
var open_button: Button = $Open
var top_bar: TopBar = $TopBar
@onready
var fileDialog: FileDialog = $FileDialog
var quiz: Quiz = $Quiz
@onready
var inspiration_scroll: ScrollContainer = $ScrollContainer
@onready
var inspiration_list: VBoxContainer = $ScrollContainer/VBoxContainer
@onready
var soundtrack_label: Label = $VBoxContainer/SoundtrackLabel
@onready
var song_label: Label = $VBoxContainer/SongLabel
@onready
var add_player_container: HBoxContainer = $Players/VBoxContainer/AddPlayerContainer
@onready
var add_players_button: TextureButton = $Players/VBoxContainer/HBoxContainer/AddPlayersButton
@onready
var add_player_button: Button = $Players/VBoxContainer/AddPlayerContainer/AddPlayerButton
@onready
var new_player_name_field: TextEdit = $Players/VBoxContainer/AddPlayerContainer/PlayerNameField
@onready
var reset_playlist_button: Button = $ResetPlaylistButton
@onready
var reset_points_button: Button = $ResetPointsButton
@onready
var sound_test_button: Button = $SoundTestButton
@onready
var sync_button: Button = $SyncButton
var search_view: Control = $Search
@onready
var sync_window: Control = %SyncWindow
@onready
var settings_button: Button = $SettingsButton
@onready
var settings_window: Control = $SettingsWindow
@onready
var shortcuts_window: Control = %ShortcutsWindow
@@ -68,13 +23,7 @@ var shortcuts_window: Control = %ShortcutsWindow
var qr_window: Control = %QRWindow
@onready
var winner_window: Control = %WinnerWindow
@onready
var qr_button: Button = $QRButton
@onready
var statistics_button: Button = $StatisticsButton
var about_window: Control = %AboutWindow
@onready
var statistic_popup: PopupPanel = $StatisticsPopupPanel
@@ -82,491 +31,84 @@ var statistic_popup: PopupPanel = $StatisticsPopupPanel
@onready
var statistic_label: Label = $StatisticsPopupPanel/StatisticsLabel
@onready
var shortcuts_button: Button = $ShortcutsButton
@onready
var about_button: Button = $AboutButton
@onready
var about_window: Control = %AboutWindow
@onready
var show_answer_button: Button = $ShowAnswerButton
@onready
var next_button: Button = $NextButton
@onready
var music_list_scroll: ScrollContainer = $MusicListPanel/ScrollContainer
@onready
var music_list: VBoxContainer = $MusicListPanel/ScrollContainer/MusicList
@onready
var player_container: VBoxContainer = $Players/VBoxContainer
@onready
var players_list_container: VBoxContainer = $Players/VBoxContainer/ScrollContainer/PlayersListVBoxContainer
@onready
var new_character_select: Control = %NewCharacterSelect
@onready
var search_button: Button = $SearchButton
@onready
var search_view: Control= $Search
@onready
var auto_repeat_song_button: CheckButton = $RepeatSongCheckButton
@onready
var music_player_container: PanelContainer = $MusicPlayer
@onready
var log_window: Control = %Log
@onready
var debug_label: Label = $DebugLabel
var player: PackedScene = preload("res://Player.tscn")
var song_list_object: PackedScene = preload("res://song_list_object.tscn")
var songs: Array= []
var soundtracks: Array = []
var current_player: Node
## True while statistics are off because a player won, until points are reset.
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: ColorRect
var popup_backdrop: PopupBackdrop
## Seconds to wait before retrying a song download step that failed.
var song_fetch_retry_seconds: float = 5.0
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
_setup_popup_backdrop()
var debug: bool = OS.has_feature("debug")
var is_mac: bool = OS.has_feature("macos")
if debug:
print("is_debug")
debug_label.visible = true
Settings.is_debug = true
settings_window.set_winning_score(5)
settings_window.set_selected_server(4)
if is_mac:
popup_backdrop = PopupBackdrop.add_to(self, _popups())
if OS.has_feature("macos"):
print("is_mac")
next_button.pressed.connect(play_next_song)
show_answer_button.pressed.connect(show_answer_pressed)
search_button.pressed.connect(show_search)
sync_button.pressed.connect(sync_soundtracks)
sound_test_button.pressed.connect(music_player_container.get_sound_test_song)
statistics_button.pressed.connect(get_statistics)
shortcuts_button.pressed.connect(show_shortcuts)
about_button.pressed.connect(show_about)
settings_button.pressed.connect(show_settings)
qr_button.pressed.connect(show_qr)
reset_playlist_button.pressed.connect(reset_playlist)
reset_points_button.pressed.connect(reset_points)
top_bar.search_pressed.connect(show_search)
top_bar.sync_pressed.connect(sync_soundtracks)
top_bar.statistics_pressed.connect(get_statistics)
top_bar.shortcuts_pressed.connect(show_shortcuts)
top_bar.about_pressed.connect(show_about)
top_bar.qr_pressed.connect(show_qr)
new_character_select.connect("new_new_character_selected", _on_new_character_selected)
new_player_name_field.connect("enter_key_pressed", add_player)
music_player_container.connect("play_next_song", play_next_song)
music_player_container.connect("update_song_list", update_song_list)
settings_window.connect("server_changed", server_updated)
settings_window.visibility_changed.connect(_on_settings_window_visibility_changed)
sync_window.connect("sync_started", _on_sync_started)
sync_window.connect("sync_finished", _on_sync_finished)
quiz.server_changed.connect(search_view.get_list_of_soundtracks)
add_players_button.pressed.connect(add_players)
add_player_button.pressed.connect(add_player)
open_button.pressed.connect(open)
auto_repeat_song_button.pressed.connect(repeat_song)
get_suggestion_list()
fetch_song_list_at_start()
search_view.get_list_of_soundtracks()
new_character_select.load_characters()
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed:
if event.alt_pressed && event.keycode == KEY_UP:
print("Alt + UP pressed")
music_player_container.change_volume(0.05)
if event.alt_pressed && event.keycode == KEY_DOWN:
print("Alt + DOWN pressed")
music_player_container.change_volume(-0.05)
if event.alt_pressed && event.ctrl_pressed && event.keycode == KEY_LEFT:
print("Ctrl + Alt + LEFT pressed")
music_player_container.restart()
elif event.alt_pressed && event.keycode == KEY_LEFT:
print("Alt + LEFT pressed")
music_player_container.seek(-5)
if event.alt_pressed && event.keycode == KEY_RIGHT:
print("Alt + RIGHT pressed")
music_player_container.seek(5)
if event.alt_pressed && event.keycode == KEY_ENTER:
print("Alt + Enter pressed")
Settings.fullscreen = !Settings.fullscreen
settings_window.fullscreen_button.button_pressed = Settings.fullscreen
if Settings.fullscreen == true:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
if Settings.fullscreen == false:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
if event.alt_pressed && event.keycode == KEY_K:
shortcuts_window.toggle()
if event.alt_pressed && event.keycode == KEY_L:
log_window.toggle()
func server_updated() -> void:
print("server_updated")
update_from_server()
reset_points()
func update_from_server() -> void:
print("update_from_server")
search_view.get_list_of_soundtracks()
get_suggestion_list()
reset_playlist()
new_character_select.load_characters()
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:
Playlist.set_latest_played_song()
Playlist.first_song_played = true
music_player_container.play_song_object(Playlist.latest_played_song)
if !(Playlist.get_preloaded_songs() > 0):
next_button.disabled = true
if !Playlist.is_song_downloading:
fetch_song()
#not first "/music/addPlayed"
#"/music/rand"
#"/music/addQue"
#"/music/info"
func _popups() -> Array[PopupWindow]:
return [search_view, sync_window, settings_window, qr_window, new_character_select,
shortcuts_window, about_window, winner_window, log_window]
return [search_view, sync_window, qr_window, shortcuts_window, about_window]
# The backdrop sits above the main window and below every popup. Popups close
# themselves when clicked outside, and the backdrop keeps that click from reaching
# the buttons behind them.
func _setup_popup_backdrop() -> void:
popup_backdrop = ColorRect.new()
popup_backdrop.name = "PopupBackdrop"
popup_backdrop.color = Color(0, 0, 0, 0.55)
popup_backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
add_child(popup_backdrop)
popup_backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
for popup: PopupWindow in _popups():
move_child(popup, -1)
popup.visibility_changed.connect(_update_popup_backdrop)
_update_popup_backdrop()
func _update_popup_backdrop() -> void:
popup_backdrop.visible = _popups().any(func(popup: PopupWindow) -> bool: return popup.visible)
# 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()
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 update_song_list() -> void:
print("update_song_list")
if Playlist.currently_playing_song >= 0:
soundtrack_label.text = Playlist.get_current_soundtrack_title()
song_label.text = Playlist.get_current_song_title()
Settings.delete_children(music_list)
for s: SongObject in Playlist.get_playlist():
var new_song_list_object: SongListObject = song_list_object.instantiate()
new_song_list_object.song_object = s
new_song_list_object.gui_input.connect(song_object_clicked.bind(s.song_number))
music_list.add_child(new_song_list_object)
func song_object_clicked(event: InputEvent, song_no: int) -> void:
if (event is InputEventMouseButton && event.pressed && event.button_index == MOUSE_BUTTON_LEFT):
print("Clicked Song Object: ", Playlist.song_object_array[song_no].to_string())
if Settings.character_select_open == false:
if Playlist.get_song(song_no) == null:
print("has no song, need to download")
var song_downloaded: Callable = func song_downloaded(body: PackedByteArray) -> void:
var sound: AudioStream = AudioStreamMP3.new()
sound.data = body
Playlist.add_song_to_song_object(song_no, sound)
music_player_container.play_song_object(song_no)
MusicServerClient.fetch_song(song_no, song_downloaded)
else:
print("song already downloaded")
music_player_container.play_song_object(song_no)
func reset_playlist() -> void:
print("reset_playlist")
var playlist_reset_done: Callable = func playlist_reset_done() -> void:
Playlist.reset_playlist()
fetch_song_list_at_start()
MusicServerClient.reset_playlist(playlist_reset_done)
func reset_points() -> void:
print("reset_points")
for player_objects: PlayerObject in Settings.player_array:
player_objects.reset_points()
load_players()
# A new game starts, so turn statistics back on if winning turned them off
if stats_paused_by_win:
Settings.add_to_stats = true
stats_paused_by_win = false
func show_about() -> void:
print("show_about")
about_window.open()
func show_shortcuts() -> void:
print("show_shortcuts")
shortcuts_window.open()
func show_settings() -> void:
print("show_settings")
settings_window.open()
func show_qr() -> void:
print("show_qr")
qr_window.open()
func get_statistics() -> void:
print("get_statistics")
statistic_popup.visible = true
statistic_label.text = "Total amount of soundtracks in the playlist: " + str(soundtracks.size())
func sync_soundtracks() -> void:
print("sync_soundtracks")
sync_window.start_sync()
func _on_sync_started() -> void:
print("_on_sync_started")
sync_button.disabled = false
sound_test_button.disabled = true
reset_playlist_button.disabled = true
show_answer_button.disabled = true
next_button.disabled = true
func _on_sync_finished() -> void:
print("_on_sync_finished")
update_from_server()
sound_test_button.disabled = false
reset_playlist_button.disabled = false
show_answer_button.disabled = false
reset_points()
func get_suggestion_list() -> void:
print("get_suggestion_list")
var populate_list: Callable = func populate_list(array) -> void:
if typeof(array) == TYPE_ARRAY:
soundtracks = []
Settings.delete_children(inspiration_list)
soundtracks.append_array(array)
for i: int in soundtracks.size():
inspiration_list.add_child(ListRow.create(soundtracks[i], i, 20))
else:
print("Unexpected data")
MusicServerClient.fetch_all_soundtracks(populate_list)
func add_players() -> void:
print("add_players")
Settings.edit_players = !Settings.edit_players
add_player_container.visible = !add_player_container.visible
new_player_name_field.grab_focus()
func add_player() -> void:
print("add_player")
if new_player_name_field.text != "" && Settings.player_array.size() < Settings.MAX_NUMBER_OF_PLAYERS:
var new_player_object: PlayerObject = PlayerObject.new(new_player_name_field.text)
new_player_name_field.text = ""
Settings.player_array.append(new_player_object)
new_player_object.connect("first_point_triggerd", music_player_container._on_point_triggered.bind("first"))
new_player_object.connect("match_point_triggerd", music_player_container._on_point_triggered.bind("match"))
new_player_object.connect("winner_triggerd", _on_player_won.bind(new_player_object))
new_player_object.connect("point_given_sound", _on_make_point_given_sound)
new_player_object.connect("player_point_given", _on_point_given.bind(new_player_object))
new_player_object.connect("player_point_taken", _on_point_taken.bind(new_player_object))
load_players()
func load_players() -> void:
print("load_players")
Settings.delete_children(players_list_container)
var counter: int = 0
for player_object: PlayerObject in Settings.player_array:
print(player_object.player_name)
var new_player: Player = player.instantiate()
new_player.player_object = player_object
new_player.list_index = counter
new_player.connect("change_character_clicked", _on_player_change_character_clicked.bind(new_player, player_object.player_name))
new_player.connect("player_removed", _on_player_removed.bind(player_object))
players_list_container.add_child(new_player)
counter += 1
func _on_point_given(player_object: PlayerObject) -> void:
print("_on_point_given")
log_window.add_log_row(player_object.player_name + " got a point")
if Playlist.currently_playing_song >= 0:
Playlist.add_point(player_object.id)
update_song_list()
func _on_make_point_given_sound() -> void:
music_player_container.play_sound_effect(preload("res://sounds/itemopen.wav"))
func _on_point_taken(player_object: PlayerObject) -> void:
print("_on_point_taken")
log_window.add_log_row(player_object.player_name + " lost a point")
music_player_container.play_sound_effect(preload("res://sounds/itemequip.wav"))
if Playlist.currently_playing_song >= 0:
Playlist.remove_point(player_object.id)
update_song_list()
func _on_player_won(player_object: PlayerObject) -> void:
print("_on_player_won")
winner_window.show_winner(player_object)
music_player_container.play_song(preload("res://sounds/winning.mp3"))
# Songs played after the game is won don't count towards the statistics
if Settings.add_to_stats:
Settings.add_to_stats = false
stats_paused_by_win = true
func _on_player_removed(player_object: PlayerObject) -> void:
print("_on_player_removed ", player_object.player_name)
Settings.player_array.erase(player_object)
load_players()
Playlist.remove_player_from_list(player_object.id)
update_song_list()
func _on_player_change_character_clicked(new_player: Player, player_name: String) -> void:
print("_on_player_change_character_clicked")
current_player = new_player
print("Change character for: " + player_name)
new_character_select.show_grid(player_name)
func _on_new_character_selected(character_texture: ImageTexture) -> void:
print("_on_new_character_selected")
current_player.new_character_selected(character_texture)
func show_search() -> void:
print("show_search")
search_view.toggle()
###Local
func show_about() -> void:
print("show_about")
about_window.open()
var local_path: String = '/Users/sebastian/ResilioSync/Sorterat_test/Metal Gear Solid 4 - Guns of the Patriots/2-16 Metal Gear Saga.mp3'
func open() -> void:
fileDialog.popup()
func show_shortcuts() -> void:
print("show_shortcuts")
shortcuts_window.open()
func _on_file_dialog_dir_selected(path: String) -> void:
print(path)
dir_contents(path)
func dir_contents(path: String) -> void:
var dir: DirAccess = DirAccess.open(path)
if dir:
dir.list_dir_begin()
var file_name: String = dir.get_next()
songs.clear()
soundtracks = []
while file_name != "":
if dir.current_is_dir():
#print("Found directory: " + file_name)
soundtracks.append(file_name)
else:
#print("Found file: " + file_name)
if file_name.ends_with(".mp3"):
songs.append(path + "/" + file_name)
file_name = dir.get_next()
else:
print("An error occurred when trying to access the path.")
func show_qr() -> void:
print("show_qr")
qr_window.open()
for i: int in soundtracks.size():
inspiration_list.add_child(ListRow.create(soundtracks[i], i, 20))
func get_statistics() -> void:
print("get_statistics")
statistic_popup.visible = true
statistic_label.text = "Total amount of soundtracks in the playlist: " \
+ str(search_view.soundtracks.size())
func sync_soundtracks() -> void:
print("sync_soundtracks")
sync_window.start_sync()
func _on_sync_started() -> void:
print("_on_sync_started")
top_bar.sync_button.disabled = false
quiz.lock_for_sync()
func _on_sync_finished() -> void:
print("_on_sync_finished")
search_view.get_list_of_soundtracks()
quiz.on_sync_finished()
+4 -414
View File
@@ -1,73 +1,15 @@
[gd_scene format=3 uid="uid://xwq863o6uvsu"]
[ext_resource type="PackedScene" path="res://TopBar.tscn" id="top_bar_scene"]
[ext_resource type="PackedScene" path="res://Quiz.tscn" id="quiz_scene"]
[ext_resource type="Script" uid="uid://b8f4g15cas2j2" path="res://MainWindow.gd" id="1_eu0t5"]
[ext_resource type="Script" uid="uid://elgn7gkxgagq" path="res://InsperationScrollContainer.gd" id="2_gxtxm"]
[ext_resource type="Texture2D" uid="uid://bcfmpd7h512ef" path="res://icons/person_add_light.svg" id="5_31tjv"]
[ext_resource type="PackedScene" uid="uid://ds15cgsf8vpvc" path="res://MusicPlayer.tscn" id="5_emn36"]
[ext_resource type="PackedScene" uid="uid://cdy4kvemwaiom" path="res://new_character_select.tscn" id="6_dhrvg"]
[ext_resource type="Script" uid="uid://gf1bk6xbe3j" path="res://MusicListScrollContainer.gd" id="7_dj026"]
[ext_resource type="Script" uid="uid://cgakje8gb37tf" path="res://PlayerNameField.gd" id="7_qsdfy"]
[ext_resource type="Theme" uid="uid://rxexo3ur85as" path="res://LightGrayTheme.tres" id="7_wxbv6"]
[ext_resource type="PackedScene" uid="uid://bxydgil1yifps" path="res://SearchWindow.tscn" id="9_5ijvr"]
[ext_resource type="PackedScene" uid="uid://dpdem7pdxweb5" path="res://SyncWindow.tscn" id="10_yxw0b"]
[ext_resource type="PackedScene" uid="uid://dldpeo5y3l5hq" path="res://SettingsWindow.tscn" id="11_k62u5"]
[ext_resource type="PackedScene" uid="uid://bd1by80q1v27e" path="res://shortcutsWindow.tscn" id="13_8eebo"]
[ext_resource type="PackedScene" uid="uid://bijh5h5yrivm3" path="res://Log.tscn" id="14_26rwn"]
[ext_resource type="PackedScene" uid="uid://btupbowehiyyu" path="res://QRWindow.tscn" id="15_qr_window"]
[ext_resource type="PackedScene" uid="uid://cp54idaudb8dp" path="res://AboutWindow.tscn" id="16_about_window"]
[ext_resource type="PackedScene" uid="uid://cxlsmxnrp0agh" path="res://WinnerWindow.tscn" id="17_winner_window"]
[ext_resource type="Theme" path="res://popup_theme.tres" id="popup_theme"]
[sub_resource type="LabelSettings" id="LabelSettings_ychxr"]
font_size = 25
[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_03bm3"]
device = -1
alt_pressed = true
keycode = 65
unicode = 63743
[sub_resource type="Shortcut" id="Shortcut_jafqj"]
events = [SubResource("InputEventKey_03bm3")]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qk7sj"]
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_k1ygi"]
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
[sub_resource type="InputEventKey" id="InputEventKey_06rl4"]
device = -1
alt_pressed = true
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")]
[sub_resource type="InputEventKey" id="InputEventKey_ujjlu"]
device = -1
alt_pressed = true
keycode = 83
[sub_resource type="Shortcut" id="Shortcut_fbju4"]
events = [SubResource("InputEventKey_ujjlu")]
[node name="Control" type="Control" unique_id=407149696]
layout_mode = 3
anchors_preset = 15
@@ -77,301 +19,9 @@ grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_eu0t5")
[node name="TopPanelContainer" type="PanelContainer" parent="." unique_id=663404711]
layout_mode = 0
offset_right = 1920.0
offset_bottom = 56.0
[node name="TopBar" parent="." unique_id=1734261695 instance=ExtResource("top_bar_scene")]
[node name="Title" type="Label" parent="." unique_id=1187907861]
layout_mode = 0
offset_left = 64.0
offset_top = 12.0
offset_right = 369.0
offset_bottom = 50.0
size_flags_horizontal = 0
text = "Music Player Randomizer"
label_settings = SubResource("LabelSettings_ychxr")
[node name="Open" type="Button" parent="." unique_id=165052329]
visible = false
layout_mode = 0
offset_left = 1296.0
offset_top = 66.0
offset_right = 1346.0
offset_bottom = 97.0
text = "Open"
[node name="FileDialog" type="FileDialog" parent="." unique_id=485771559]
title = "Open a Directory"
initial_position = 2
size = Vector2i(616, 400)
ok_button_text = "Select Current Folder"
file_mode = 2
access = 2
show_hidden_files = true
[node name="MusicListPanel" type="PanelContainer" parent="." unique_id=2011292375]
layout_mode = 0
offset_left = 64.0
offset_top = 384.0
offset_right = 1152.0
offset_bottom = 896.0
theme = ExtResource("7_wxbv6")
[node name="ScrollContainer" type="ScrollContainer" parent="MusicListPanel" unique_id=707750459]
layout_mode = 2
follow_focus = true
script = ExtResource("7_dj026")
[node name="MusicList" type="VBoxContainer" parent="MusicListPanel/ScrollContainer" unique_id=373398568]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/separation = 10
[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=498283761]
layout_mode = 0
offset_left = 1216.0
offset_top = 128.0
offset_right = 1857.0
offset_bottom = 512.0
horizontal_scroll_mode = 0
vertical_scroll_mode = 3
script = ExtResource("2_gxtxm")
[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer" unique_id=1215162949]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=42468498]
layout_mode = 0
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_top = 992.0
offset_right = 1856.0
offset_bottom = 1024.0
grow_horizontal = 1
grow_vertical = 1
[node name="Players" type="PanelContainer" parent="." unique_id=1708375945]
layout_mode = 0
offset_left = 1216.0
offset_top = 576.0
offset_right = 1857.0
offset_bottom = 977.0
[node name="VBoxContainer" type="VBoxContainer" parent="Players" unique_id=270084211]
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="Players/VBoxContainer" unique_id=513047023]
custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2
[node name="Label" type="Label" parent="Players/VBoxContainer/HBoxContainer" unique_id=1331357386]
custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
text = "Players"
horizontal_alignment = 1
vertical_alignment = 1
[node name="AddPlayersButton" type="TextureButton" parent="Players/VBoxContainer/HBoxContainer" unique_id=634410148]
custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2
size_flags_horizontal = 8
focus_mode = 0
action_mode = 0
shortcut = SubResource("Shortcut_jafqj")
texture_normal = ExtResource("5_31tjv")
[node name="AddPlayerContainer" type="HBoxContainer" parent="Players/VBoxContainer" unique_id=1371948739]
visible = false
custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2
[node name="Panel" type="Panel" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=1569854056]
layout_mode = 2
size_flags_horizontal = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_qk7sj")
[node name="PlayerNameField" type="TextEdit" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=332535327]
custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Player name"
script = ExtResource("7_qsdfy")
[node name="AddPlayerButton" type="Button" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=245001313]
custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2
text = "Add"
[node name="Panel2" type="Panel" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=60215448]
custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2
size_flags_horizontal = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_k1ygi")
[node name="ScrollContainer" type="ScrollContainer" parent="Players/VBoxContainer" unique_id=1000000001]
layout_mode = 2
size_flags_vertical = 15
horizontal_scroll_mode = 0
[node name="PlayersListVBoxContainer" type="VBoxContainer" parent="Players/VBoxContainer/ScrollContainer" unique_id=1000000002]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 15
[node name="ResetPlaylistButton" type="Button" parent="." unique_id=1754155620]
layout_mode = 0
offset_left = 1360.0
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="SyncButton" type="Button" parent="." unique_id=1228313556]
layout_mode = 0
offset_left = 1442.0
offset_top = 8.0
offset_right = 1512.0
offset_bottom = 48.0
focus_mode = 0
action_mode = 0
text = "Sync"
[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]
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="SearchButton" type="Button" parent="." unique_id=1782453581]
layout_mode = 0
offset_left = 1354.0
offset_top = 8.0
offset_right = 1424.0
offset_bottom = 48.0
focus_mode = 0
action_mode = 0
shortcut = SubResource("Shortcut_fbju4")
text = "Search"
[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="StatisticsButton" type="Button" parent="." unique_id=2045021269]
layout_mode = 0
offset_left = 1530.0
offset_top = 8.0
offset_right = 1600.0
offset_bottom = 48.0
focus_mode = 0
action_mode = 0
text = "Statistics"
[node name="ShortcutsButton" type="Button" parent="." unique_id=2066498226]
layout_mode = 0
offset_left = 1626.0
offset_top = 8.0
offset_right = 1696.0
offset_bottom = 48.0
focus_mode = 0
action_mode = 0
text = "Shortcuts"
[node name="AboutButton" type="Button" parent="." unique_id=1057035253]
layout_mode = 0
offset_left = 1722.0
offset_top = 8.0
offset_right = 1792.0
offset_bottom = 48.0
focus_mode = 0
action_mode = 0
text = "About"
[node name="QRButton" type="Button" parent="." unique_id=2021908559]
layout_mode = 0
offset_left = 1802.0
offset_top = 8.0
offset_right = 1872.0
offset_bottom = 48.0
focus_mode = 0
action_mode = 0
text = "QR"
[node name="Quiz" parent="." unique_id=1357908642 instance=ExtResource("quiz_scene")]
[node name="Search" parent="." unique_id=1661250375 instance=ExtResource("9_5ijvr")]
visible = false
@@ -390,14 +40,6 @@ offset_top = 153.0
offset_right = 210.0
offset_bottom = 153.0
[node name="SettingsWindow" parent="." unique_id=1323246978 instance=ExtResource("11_k62u5")]
visible = false
layout_mode = 1
offset_left = 720.0
offset_top = 153.0
offset_right = 720.0
offset_bottom = 153.0
[node name="QRWindow" parent="." unique_id=589733821 instance=ExtResource("15_qr_window")]
unique_name_in_owner = true
visible = false
@@ -407,15 +49,6 @@ offset_top = 153.0
offset_right = 720.0
offset_bottom = 153.0
[node name="NewCharacterSelect" parent="." unique_id=1679856322 instance=ExtResource("6_dhrvg")]
unique_name_in_owner = true
visible = false
layout_mode = 1
offset_left = 405.0
offset_top = 165.0
offset_right = 1557.0
offset_bottom = 805.0
[node name="ShortcutsWindow" parent="." unique_id=1513416242 instance=ExtResource("13_8eebo")]
unique_name_in_owner = true
visible = false
@@ -457,46 +90,3 @@ offset_top = 153.0
offset_right = 720.0
offset_bottom = 153.0
[node name="WinnerWindow" parent="." unique_id=999999999 instance=ExtResource("17_winner_window")]
unique_name_in_owner = true
visible = false
layout_mode = 1
offset_left = 720.0
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
offset_left = 974.0
offset_top = 904.0
offset_right = 1014.0
offset_bottom = 927.0
scale = Vector2(3, 3)
text = "DEBUG"
horizontal_alignment = 1
vertical_alignment = 1
autowrap_mode = 2
[node name="Log" parent="." unique_id=1289881388 instance=ExtResource("14_26rwn")]
unique_name_in_owner = true
visible = false
layout_mode = 1
offset_left = 562.0
offset_top = 132.0
offset_right = 1262.0
offset_bottom = 832.0
[connection signal="dir_selected" from="FileDialog" to="." method="_on_file_dialog_dir_selected"]
+343
View File
@@ -0,0 +1,343 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://quiz.gd" id="quiz_script"]
[ext_resource type="Script" uid="uid://elgn7gkxgagq" path="res://InsperationScrollContainer.gd" id="2_gxtxm"]
[ext_resource type="Texture2D" uid="uid://bcfmpd7h512ef" path="res://icons/person_add_light.svg" id="5_31tjv"]
[ext_resource type="PackedScene" uid="uid://ds15cgsf8vpvc" path="res://MusicPlayer.tscn" id="5_emn36"]
[ext_resource type="PackedScene" uid="uid://cdy4kvemwaiom" path="res://new_character_select.tscn" id="6_dhrvg"]
[ext_resource type="Script" uid="uid://gf1bk6xbe3j" path="res://MusicListScrollContainer.gd" id="7_dj026"]
[ext_resource type="Script" uid="uid://cgakje8gb37tf" path="res://PlayerNameField.gd" id="7_qsdfy"]
[ext_resource type="Theme" uid="uid://rxexo3ur85as" path="res://LightGrayTheme.tres" id="7_wxbv6"]
[ext_resource type="PackedScene" uid="uid://dldpeo5y3l5hq" path="res://SettingsWindow.tscn" id="11_k62u5"]
[ext_resource type="PackedScene" uid="uid://bijh5h5yrivm3" path="res://Log.tscn" id="14_26rwn"]
[ext_resource type="PackedScene" uid="uid://cxlsmxnrp0agh" path="res://WinnerWindow.tscn" id="17_winner_window"]
[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_03bm3"]
device = -1
alt_pressed = true
keycode = 65
unicode = 63743
[sub_resource type="Shortcut" id="Shortcut_jafqj"]
events = [SubResource("InputEventKey_03bm3")]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qk7sj"]
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_k1ygi"]
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
[sub_resource type="InputEventKey" id="InputEventKey_06rl4"]
device = -1
alt_pressed = true
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]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
script = ExtResource("quiz_script")
[node name="Open" type="Button" parent="." unique_id=165052329]
visible = false
layout_mode = 0
offset_left = 1296.0
offset_top = 66.0
offset_right = 1346.0
offset_bottom = 97.0
text = "Open"
[node name="FileDialog" type="FileDialog" parent="." unique_id=485771559]
title = "Open a Directory"
initial_position = 2
size = Vector2i(616, 400)
ok_button_text = "Select Current Folder"
file_mode = 2
access = 2
show_hidden_files = true
[node name="MusicListPanel" type="PanelContainer" parent="." unique_id=2011292375]
layout_mode = 0
offset_left = 64.0
offset_top = 384.0
offset_right = 1152.0
offset_bottom = 896.0
theme = ExtResource("7_wxbv6")
[node name="ScrollContainer" type="ScrollContainer" parent="MusicListPanel" unique_id=707750459]
layout_mode = 2
follow_focus = true
script = ExtResource("7_dj026")
[node name="MusicList" type="VBoxContainer" parent="MusicListPanel/ScrollContainer" unique_id=373398568]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/separation = 10
[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=498283761]
layout_mode = 0
offset_left = 1216.0
offset_top = 128.0
offset_right = 1857.0
offset_bottom = 512.0
horizontal_scroll_mode = 0
vertical_scroll_mode = 3
script = ExtResource("2_gxtxm")
[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer" unique_id=1215162949]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=42468498]
layout_mode = 0
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_top = 992.0
offset_right = 1856.0
offset_bottom = 1024.0
grow_horizontal = 1
grow_vertical = 1
[node name="Players" type="PanelContainer" parent="." unique_id=1708375945]
layout_mode = 0
offset_left = 1216.0
offset_top = 576.0
offset_right = 1857.0
offset_bottom = 977.0
[node name="VBoxContainer" type="VBoxContainer" parent="Players" unique_id=270084211]
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="Players/VBoxContainer" unique_id=513047023]
custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2
[node name="Label" type="Label" parent="Players/VBoxContainer/HBoxContainer" unique_id=1331357386]
custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
text = "Players"
horizontal_alignment = 1
vertical_alignment = 1
[node name="AddPlayersButton" type="TextureButton" parent="Players/VBoxContainer/HBoxContainer" unique_id=634410148]
custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2
size_flags_horizontal = 8
focus_mode = 0
action_mode = 0
shortcut = SubResource("Shortcut_jafqj")
texture_normal = ExtResource("5_31tjv")
[node name="AddPlayerContainer" type="HBoxContainer" parent="Players/VBoxContainer" unique_id=1371948739]
visible = false
custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2
[node name="Panel" type="Panel" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=1569854056]
layout_mode = 2
size_flags_horizontal = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_qk7sj")
[node name="PlayerNameField" type="TextEdit" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=332535327]
custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Player name"
script = ExtResource("7_qsdfy")
[node name="AddPlayerButton" type="Button" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=245001313]
custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2
text = "Add"
[node name="Panel2" type="Panel" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=60215448]
custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2
size_flags_horizontal = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_k1ygi")
[node name="ScrollContainer" type="ScrollContainer" parent="Players/VBoxContainer" unique_id=1000000001]
layout_mode = 2
size_flags_vertical = 15
horizontal_scroll_mode = 0
[node name="PlayersListVBoxContainer" type="VBoxContainer" parent="Players/VBoxContainer/ScrollContainer" unique_id=1000000002]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 15
[node name="ResetPlaylistButton" type="Button" parent="." unique_id=1754155620]
layout_mode = 0
offset_left = 1360.0
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]
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
layout_mode = 1
offset_left = 720.0
offset_top = 153.0
offset_right = 720.0
offset_bottom = 153.0
[node name="NewCharacterSelect" parent="." unique_id=1679856322 instance=ExtResource("6_dhrvg")]
unique_name_in_owner = true
visible = false
layout_mode = 1
offset_left = 405.0
offset_top = 165.0
offset_right = 1557.0
offset_bottom = 805.0
[node name="WinnerWindow" parent="." unique_id=999999999 instance=ExtResource("17_winner_window")]
unique_name_in_owner = true
visible = false
layout_mode = 1
offset_left = 720.0
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
offset_left = 974.0
offset_top = 904.0
offset_right = 1014.0
offset_bottom = 927.0
scale = Vector2(3, 3)
text = "DEBUG"
horizontal_alignment = 1
vertical_alignment = 1
autowrap_mode = 2
[node name="Log" parent="." unique_id=1289881388 instance=ExtResource("14_26rwn")]
unique_name_in_owner = true
visible = false
layout_mode = 1
offset_left = 562.0
offset_top = 132.0
offset_right = 1262.0
offset_bottom = 832.0
[connection signal="dir_selected" from="FileDialog" to="." method="_on_file_dialog_dir_selected"]
+6
View File
@@ -110,11 +110,17 @@ func _ready() -> void:
select_server_button.select(Settings.selected_server)
select_server_button.item_selected.connect(select_server)
visibility_changed.connect(_on_visibility_changed)
# Alt+Enter switches fullscreen from the main window, which resizes the window
get_window().size_changed.connect(_update_fullscreen_button)
# Settings can change outside this window, e.g. winning turns statistics off
func _on_visibility_changed() -> void:
if visible:
add_to_database_button.button_pressed = Settings.add_to_stats
_update_fullscreen_button()
func _update_fullscreen_button() -> void:
fullscreen_button.set_pressed_no_signal(Settings.fullscreen)
func fullscreen() -> void:
Settings.fullscreen = !Settings.fullscreen
+100
View File
@@ -0,0 +1,100 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://top_bar.gd" id="top_bar_script"]
[sub_resource type="LabelSettings" id="LabelSettings_ychxr"]
font_size = 25
[sub_resource type="InputEventKey" id="InputEventKey_ujjlu"]
device = -1
alt_pressed = true
keycode = 83
[sub_resource type="Shortcut" id="Shortcut_fbju4"]
events = [SubResource("InputEventKey_ujjlu")]
[node name="TopBar" type="Control" unique_id=1489162753]
layout_mode = 3
anchors_preset = 10
anchor_right = 1.0
offset_bottom = 56.0
grow_horizontal = 2
mouse_filter = 2
script = ExtResource("top_bar_script")
[node name="TopPanelContainer" type="PanelContainer" parent="." unique_id=663404711]
layout_mode = 0
offset_right = 1920.0
offset_bottom = 56.0
[node name="Title" type="Label" parent="." unique_id=1187907861]
layout_mode = 0
offset_left = 64.0
offset_top = 12.0
offset_right = 369.0
offset_bottom = 50.0
size_flags_horizontal = 0
text = "Music Player Randomizer"
label_settings = SubResource("LabelSettings_ychxr")
[node name="SyncButton" type="Button" parent="." unique_id=1228313556]
layout_mode = 0
offset_left = 1442.0
offset_top = 8.0
offset_right = 1512.0
offset_bottom = 48.0
focus_mode = 0
action_mode = 0
text = "Sync"
[node name="SearchButton" type="Button" parent="." unique_id=1782453581]
layout_mode = 0
offset_left = 1354.0
offset_top = 8.0
offset_right = 1424.0
offset_bottom = 48.0
focus_mode = 0
action_mode = 0
shortcut = SubResource("Shortcut_fbju4")
text = "Search"
[node name="StatisticsButton" type="Button" parent="." unique_id=2045021269]
layout_mode = 0
offset_left = 1530.0
offset_top = 8.0
offset_right = 1600.0
offset_bottom = 48.0
focus_mode = 0
action_mode = 0
text = "Statistics"
[node name="ShortcutsButton" type="Button" parent="." unique_id=2066498226]
layout_mode = 0
offset_left = 1626.0
offset_top = 8.0
offset_right = 1696.0
offset_bottom = 48.0
focus_mode = 0
action_mode = 0
text = "Shortcuts"
[node name="AboutButton" type="Button" parent="." unique_id=1057035253]
layout_mode = 0
offset_left = 1722.0
offset_top = 8.0
offset_right = 1792.0
offset_bottom = 48.0
focus_mode = 0
action_mode = 0
text = "About"
[node name="QRButton" type="Button" parent="." unique_id=2021908559]
layout_mode = 0
offset_left = 1802.0
offset_top = 8.0
offset_right = 1872.0
offset_bottom = 48.0
focus_mode = 0
action_mode = 0
text = "QR"
+31
View File
@@ -0,0 +1,31 @@
class_name PopupBackdrop
extends ColorRect
## Dims everything behind an open popup and keeps clicks from reaching the buttons
## under it.
##
## The backdrop sits above the rest of its parent and below the popups it belongs to.
## Popups close themselves when clicked outside, and the backdrop keeps that click from
## reaching the buttons behind them.
var _popups: Array[PopupWindow] = []
## Adds a backdrop to [param parent] and moves [param popups] above it. The backdrop
## shows while any of them is open.
static func add_to(parent: Control, popups: Array[PopupWindow]) -> PopupBackdrop:
var backdrop: PopupBackdrop = PopupBackdrop.new()
backdrop.name = "PopupBackdrop"
backdrop.color = Color(0, 0, 0, 0.55)
backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
backdrop._popups = popups
parent.add_child(backdrop)
backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
for popup: PopupWindow in popups:
parent.move_child(popup, -1)
popup.visibility_changed.connect(backdrop._update)
backdrop._update()
return backdrop
func _update() -> void:
visible = _popups.any(func(popup: PopupWindow) -> bool: return popup.visible)
+1
View File
@@ -0,0 +1 @@
uid://swjhekc8beob
+496
View File
@@ -0,0 +1,496 @@
class_name Quiz
extends Control
## The quiz: the song that is playing, the playlist, the players and their points, and
## the popups opened from here (settings, character select, winner and log).
##
## The main window shows the quiz below its top bar. Pictures and buttons keep the
## positions they had in the main window, so the quiz fills the whole window and lets
## clicks through where it has nothing.
## Emitted when another MusicServer is picked in the settings, after the quiz has
## reloaded from it.
signal server_changed
@onready
var open_button: Button = $Open
@onready
var fileDialog: FileDialog = $FileDialog
@onready
var inspiration_scroll: ScrollContainer = $ScrollContainer
@onready
var inspiration_list: VBoxContainer = $ScrollContainer/VBoxContainer
@onready
var soundtrack_label: Label = $VBoxContainer/SoundtrackLabel
@onready
var song_label: Label = $VBoxContainer/SongLabel
@onready
var add_player_container: HBoxContainer = $Players/VBoxContainer/AddPlayerContainer
@onready
var add_players_button: TextureButton = $Players/VBoxContainer/HBoxContainer/AddPlayersButton
@onready
var add_player_button: Button = $Players/VBoxContainer/AddPlayerContainer/AddPlayerButton
@onready
var new_player_name_field: TextEdit = $Players/VBoxContainer/AddPlayerContainer/PlayerNameField
@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
@onready
var settings_window: Control = $SettingsWindow
@onready
var winner_window: Control = %WinnerWindow
@onready
var show_answer_button: Button = $ShowAnswerButton
@onready
var next_button: Button = $NextButton
@onready
var music_list_scroll: ScrollContainer = $MusicListPanel/ScrollContainer
@onready
var music_list: VBoxContainer = $MusicListPanel/ScrollContainer/MusicList
@onready
var player_container: VBoxContainer = $Players/VBoxContainer
@onready
var players_list_container: VBoxContainer = $Players/VBoxContainer/ScrollContainer/PlayersListVBoxContainer
@onready
var new_character_select: Control = %NewCharacterSelect
@onready
var auto_repeat_song_button: CheckButton = $RepeatSongCheckButton
@onready
var music_player_container: PanelContainer = $MusicPlayer
@onready
var log_window: Control = %Log
@onready
var debug_label: Label = $DebugLabel
var player: PackedScene = preload("res://Player.tscn")
var song_list_object: PackedScene = preload("res://song_list_object.tscn")
var songs: Array= []
var soundtracks: Array = []
var current_player: Node
## True while statistics are off because a player won, until points are reset.
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())
if OS.has_feature("debug"):
print("is_debug")
debug_label.visible = true
Settings.is_debug = true
settings_window.set_winning_score(5)
settings_window.set_selected_server(4)
next_button.pressed.connect(play_next_song)
show_answer_button.pressed.connect(show_answer_pressed)
sound_test_button.pressed.connect(music_player_container.get_sound_test_song)
settings_button.pressed.connect(show_settings)
reset_playlist_button.pressed.connect(reset_playlist)
reset_points_button.pressed.connect(reset_points)
new_character_select.connect("new_new_character_selected", _on_new_character_selected)
new_player_name_field.connect("enter_key_pressed", add_player)
music_player_container.connect("play_next_song", play_next_song)
music_player_container.connect("update_song_list", update_song_list)
settings_window.connect("server_changed", _on_server_changed)
settings_window.visibility_changed.connect(_on_settings_window_visibility_changed)
add_players_button.pressed.connect(add_players)
add_player_button.pressed.connect(add_player)
open_button.pressed.connect(open)
auto_repeat_song_button.pressed.connect(repeat_song)
get_suggestion_list()
fetch_song_list_at_start()
new_character_select.load_characters()
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed:
if event.alt_pressed && event.keycode == KEY_UP:
print("Alt + UP pressed")
music_player_container.change_volume(0.05)
if event.alt_pressed && event.keycode == KEY_DOWN:
print("Alt + DOWN pressed")
music_player_container.change_volume(-0.05)
if event.alt_pressed && event.ctrl_pressed && event.keycode == KEY_LEFT:
print("Ctrl + Alt + LEFT pressed")
music_player_container.restart()
elif event.alt_pressed && event.keycode == KEY_LEFT:
print("Alt + LEFT pressed")
music_player_container.seek(-5)
if event.alt_pressed && event.keycode == KEY_RIGHT:
print("Alt + RIGHT pressed")
music_player_container.seek(5)
if event.alt_pressed && event.keycode == KEY_L:
log_window.toggle()
## 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
## Reloads from the server after a sync and starts a new game.
func on_sync_finished() -> void:
refresh_from_server()
sound_test_button.disabled = false
reset_playlist_button.disabled = false
show_answer_button.disabled = false
reset_points()
## Reloads the soundtracks, playlist and characters from the server.
func refresh_from_server() -> void:
print("refresh_from_server")
get_suggestion_list()
reset_playlist()
new_character_select.load_characters()
func _on_server_changed() -> void:
print("_on_server_changed")
refresh_from_server()
reset_points()
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:
Playlist.set_latest_played_song()
Playlist.first_song_played = true
music_player_container.play_song_object(Playlist.latest_played_song)
if !(Playlist.get_preloaded_songs() > 0):
next_button.disabled = true
if !Playlist.is_song_downloading:
fetch_song()
func _popups() -> Array[PopupWindow]:
return [settings_window, new_character_select, winner_window, log_window]
# 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()
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 update_song_list() -> void:
print("update_song_list")
if Playlist.currently_playing_song >= 0:
soundtrack_label.text = Playlist.get_current_soundtrack_title()
song_label.text = Playlist.get_current_song_title()
Settings.delete_children(music_list)
for s: SongObject in Playlist.get_playlist():
var new_song_list_object: SongListObject = song_list_object.instantiate()
new_song_list_object.song_object = s
new_song_list_object.gui_input.connect(song_object_clicked.bind(s.song_number))
music_list.add_child(new_song_list_object)
func song_object_clicked(event: InputEvent, song_no: int) -> void:
if (event is InputEventMouseButton && event.pressed && event.button_index == MOUSE_BUTTON_LEFT):
print("Clicked Song Object: ", Playlist.song_object_array[song_no].to_string())
if Settings.character_select_open == false:
if Playlist.get_song(song_no) == null:
print("has no song, need to download")
var song_downloaded: Callable = func song_downloaded(body: PackedByteArray) -> void:
var sound: AudioStream = AudioStreamMP3.new()
sound.data = body
Playlist.add_song_to_song_object(song_no, sound)
music_player_container.play_song_object(song_no)
MusicServerClient.fetch_song(song_no, song_downloaded)
else:
print("song already downloaded")
music_player_container.play_song_object(song_no)
func reset_playlist() -> void:
print("reset_playlist")
var playlist_reset_done: Callable = func playlist_reset_done() -> void:
Playlist.reset_playlist()
fetch_song_list_at_start()
MusicServerClient.reset_playlist(playlist_reset_done)
func reset_points() -> void:
print("reset_points")
for player_objects: PlayerObject in Settings.player_array:
player_objects.reset_points()
load_players()
# A new game starts, so turn statistics back on if winning turned them off
if stats_paused_by_win:
Settings.add_to_stats = true
stats_paused_by_win = false
func show_settings() -> void:
print("show_settings")
settings_window.open()
func get_suggestion_list() -> void:
print("get_suggestion_list")
var populate_list: Callable = func populate_list(array) -> void:
if typeof(array) == TYPE_ARRAY:
soundtracks = []
Settings.delete_children(inspiration_list)
soundtracks.append_array(array)
for i: int in soundtracks.size():
inspiration_list.add_child(ListRow.create(soundtracks[i], i, 20))
else:
print("Unexpected data")
MusicServerClient.fetch_all_soundtracks(populate_list)
func add_players() -> void:
print("add_players")
Settings.edit_players = !Settings.edit_players
add_player_container.visible = !add_player_container.visible
new_player_name_field.grab_focus()
func add_player() -> void:
print("add_player")
if new_player_name_field.text != "" && Settings.player_array.size() < Settings.MAX_NUMBER_OF_PLAYERS:
var new_player_object: PlayerObject = PlayerObject.new(new_player_name_field.text)
new_player_name_field.text = ""
Settings.player_array.append(new_player_object)
new_player_object.connect("first_point_triggerd", music_player_container._on_point_triggered.bind("first"))
new_player_object.connect("match_point_triggerd", music_player_container._on_point_triggered.bind("match"))
new_player_object.connect("winner_triggerd", _on_player_won.bind(new_player_object))
new_player_object.connect("point_given_sound", _on_make_point_given_sound)
new_player_object.connect("player_point_given", _on_point_given.bind(new_player_object))
new_player_object.connect("player_point_taken", _on_point_taken.bind(new_player_object))
load_players()
func load_players() -> void:
print("load_players")
Settings.delete_children(players_list_container)
var counter: int = 0
for player_object: PlayerObject in Settings.player_array:
print(player_object.player_name)
var new_player: Player = player.instantiate()
new_player.player_object = player_object
new_player.list_index = counter
new_player.connect("change_character_clicked", _on_player_change_character_clicked.bind(new_player, player_object.player_name))
new_player.connect("player_removed", _on_player_removed.bind(player_object))
players_list_container.add_child(new_player)
counter += 1
func _on_point_given(player_object: PlayerObject) -> void:
print("_on_point_given")
log_window.add_log_row(player_object.player_name + " got a point")
if Playlist.currently_playing_song >= 0:
Playlist.add_point(player_object.id)
update_song_list()
func _on_make_point_given_sound() -> void:
music_player_container.play_sound_effect(preload("res://sounds/itemopen.wav"))
func _on_point_taken(player_object: PlayerObject) -> void:
print("_on_point_taken")
log_window.add_log_row(player_object.player_name + " lost a point")
music_player_container.play_sound_effect(preload("res://sounds/itemequip.wav"))
if Playlist.currently_playing_song >= 0:
Playlist.remove_point(player_object.id)
update_song_list()
func _on_player_won(player_object: PlayerObject) -> void:
print("_on_player_won")
winner_window.show_winner(player_object)
music_player_container.play_song(preload("res://sounds/winning.mp3"))
# Songs played after the game is won don't count towards the statistics
if Settings.add_to_stats:
Settings.add_to_stats = false
stats_paused_by_win = true
func _on_player_removed(player_object: PlayerObject) -> void:
print("_on_player_removed ", player_object.player_name)
Settings.player_array.erase(player_object)
load_players()
Playlist.remove_player_from_list(player_object.id)
update_song_list()
func _on_player_change_character_clicked(new_player: Player, player_name: String) -> void:
print("_on_player_change_character_clicked")
current_player = new_player
print("Change character for: " + player_name)
new_character_select.show_grid(player_name)
func _on_new_character_selected(character_texture: ImageTexture) -> void:
print("_on_new_character_selected")
current_player.new_character_selected(character_texture)
###Local
var local_path: String = '/Users/sebastian/ResilioSync/Sorterat_test/Metal Gear Solid 4 - Guns of the Patriots/2-16 Metal Gear Saga.mp3'
func open() -> void:
fileDialog.popup()
func _on_file_dialog_dir_selected(path: String) -> void:
print(path)
dir_contents(path)
func dir_contents(path: String) -> void:
var dir: DirAccess = DirAccess.open(path)
if dir:
dir.list_dir_begin()
var file_name: String = dir.get_next()
songs.clear()
soundtracks = []
while file_name != "":
if dir.current_is_dir():
#print("Found directory: " + file_name)
soundtracks.append(file_name)
else:
#print("Found file: " + file_name)
if file_name.ends_with(".mp3"):
songs.append(path + "/" + file_name)
file_name = dir.get_next()
else:
print("An error occurred when trying to access the path.")
for i: int in soundtracks.size():
inspiration_list.add_child(ListRow.create(soundtracks[i], i, 20))
+1
View File
@@ -0,0 +1 @@
uid://b0od47vseenx4
File diff suppressed because it is too large. Load diff
+16 -4
View File
@@ -11,6 +11,7 @@ var _server: FakeHttpTransport
var _snapshot: AutoloadSnapshot
var _runner: GdUnitSceneRunner
var _window: Control
var _quiz: Quiz
func before_test() -> void:
@@ -39,6 +40,7 @@ func before_test() -> void:
_before_window_starts()
_runner = scene_runner("res://MainWindow.tscn")
_window = _runner.scene()
_quiz = _window.get_node("Quiz")
# The debug build lowers the winning score on startup; keep it out of reach
Settings.winning_score = 20
@@ -61,8 +63,9 @@ func _next_song_is(info: Dictionary) -> void:
_server.respond_json("/music/info", info)
## Presses a button in the quiz.
func _press(button_path: String) -> void:
(_window.get_node(button_path) as Button).pressed.emit()
(_quiz.get_node(button_path) as Button).pressed.emit()
## Presses Next and waits until the first song is playing.
@@ -73,16 +76,16 @@ func _play_first_song() -> void:
## Adds a player through the name field, the same way the user does.
func _add_player(player_name: String) -> PlayerObject:
var name_field: TextEdit = _window.get_node(
var name_field: TextEdit = _quiz.get_node(
"Players/VBoxContainer/AddPlayerContainer/PlayerNameField")
name_field.text = player_name
_runner.invoke("add_player")
_quiz.add_player()
return Settings.player_array.back() if !Settings.player_array.is_empty() else null
## Returns the row in the player list that shows [param player_object].
func _player_row(player_object: PlayerObject) -> Player:
var rows: Node = _window.get_node(
var rows: Node = _quiz.get_node(
"Players/VBoxContainer/ScrollContainer/PlayersListVBoxContainer")
for row: Player in rows.get_children():
if row.player_object == player_object:
@@ -97,6 +100,15 @@ func _remove_player(player_object: PlayerObject) -> void:
await _runner.simulate_frames(1)
## Clicks at [param position], given in the window's 1920x1080 layout coordinates.
func _click_at(position: Vector2) -> void:
# The runner takes window pixels; the headless test window is much smaller than the
# layout, so convert like a real mouse position would be
_runner.set_mouse_position(_window.get_viewport().get_final_transform() * position)
await _runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT)
await _runner.await_input_processed()
## Presses [param keys] together, e.g. [code][KEY_ALT, KEY_1][/code], then releases them.
func _press_keys(keys: Array[Key]) -> void:
for key: Key in keys:
+246
View File
@@ -0,0 +1,246 @@
class_name UiTour
extends RefCounted
## Walks the main window through a fixed set of states against a fake MusicServer:
## startup, players added, a song playing, every popup open and a sync.
##
## Used for screenshots ([code]tools/take_screenshots.gd[/code]) and for the layout
## test, so both look at exactly the same states. Nodes are looked up by name in the
## main window and in its Quiz and TopBar scenes, so the tour works no matter which of
## them a node lives in.
const SOUNDTRACKS: Array[String] = [
"Chrono Trigger", "Final Fantasy VI", "Secret of Mana", "Terranigma",
"Earthbound", "Super Metroid", "The Legend of Zelda: A Link to the Past",
"Donkey Kong Country 2", "Lufia II", "Illusion of Gaia",
]
const SONG_INFO: Dictionary = {
"SongNo": 0, "Soundtrack": "Chrono Trigger", "Song": "Corridors of Time",
}
const SYNC_PROGRESS: Dictionary = {"progress": 40, "time_spent": "00:00:02"}
const SYNC_RESULT: Dictionary = {
"soundtracks_added": ["Terranigma", "Lufia II"],
"soundtracks_re_added": [],
"soundtracks_changed_title": {"FF6": "Final Fantasy VI"},
"soundtracks_changed_content": ["Chrono Trigger"],
"soundtracks_removed": ["Old Soundtrack"],
"catched_errors": [],
"total_time": "00:00:03",
}
## Popups in the order the tour opens them.
const POPUPS: Array[String] = [
"Search", "SyncWindow", "SettingsWindow", "QRWindow", "NewCharacterSelect",
"ShortcutsWindow", "AboutWindow", "WinnerWindow", "Log", "StatisticsPopupPanel",
]
var _main: Control
func _init(main: Control) -> void:
_main = main
## Returns a fake server that answers every request the main window makes on the tour.
## Call [method FakeHttpTransport.install] before the main window is created.
static func make_server() -> FakeHttpTransport:
var server: FakeHttpTransport = FakeHttpTransport.new()
var mp3: PackedByteArray = FileAccess.get_file_as_bytes("res://tests/data/short.mp3")
server.respond_json("/music/all", SOUNDTRACKS)
server.respond_json("/music/all/order", SOUNDTRACKS)
server.respond_json("/music/list", [])
server.respond_json("/characters", [])
server.respond_bytes("/music/rand", mp3)
server.respond_bytes("/music/addQue", PackedByteArray())
server.respond_json("/music/info", SONG_INFO)
server.respond_bytes("/music/reset", PackedByteArray())
server.respond_bytes("/music/played?song=0", PackedByteArray())
server.respond_bytes("/sync", PackedByteArray())
server.respond_json("/sync/progress", SYNC_PROGRESS)
return server
## Puts Settings and Playlist in the state the tour expects. Call it before the main
## window is created.
static func prepare_autoloads() -> void:
Playlist.reset_playlist()
Playlist.first_song_played = false
Playlist.is_song_downloading = false
Settings.player_array.clear()
Settings.edit_players = false
Settings.currently_syncing = false
Settings.character_select_open = false
Settings.add_to_stats = true
Settings.hide_next_track = true
Settings.use_low_played_mode = false
Settings.winning_score = 20
# The inspiration list scrolls by itself, which would make every picture different
Settings.inspiration_list_speed = 0
## Runs the tour. [param on_state] is called (and awaited) with the name of each state
## and the popup that is open in it, or [code]null[/code] when no popup is open.
func run(on_state: Callable) -> void:
# The debug build lowers the winning score on startup; keep it out of reach
Settings.winning_score = 20
await _settle()
await on_state.call("startup", null)
_call("add_players")
await _settle()
await on_state.call("add_player_row", null)
_add_player("Crono")
_add_player("Marle")
_call("add_players")
await _settle()
await on_state.call("players_added", null)
_call("play_next_song")
await _settle()
_freeze_music_player()
_player_row(0).add_point()
_player_row(0).add_point()
_player_row(1).add_point()
_call("show_answer_pressed")
await _settle()
_freeze_music_player()
_fix_log_times()
await _settle()
await on_state.call("playing", null)
for popup_name: String in POPUPS:
var popup: Node = await _open_popup(popup_name)
await _settle()
await on_state.call("popup_" + popup_name, popup)
_close_popup(popup)
await _settle()
var sync_window: PopupWindow = _node("SyncWindow")
Settings.currently_syncing = false
sync_window.has_synced = false
sync_window.start_sync()
sync_window.sync_timer.stop()
sync_window._on_sync_timeout()
await _settle()
await on_state.call("sync_in_progress", sync_window)
sync_window.close()
await _settle()
await on_state.call("sync_buttons_locked", null)
_server().respond_json("/sync/progress", SYNC_RESULT)
sync_window._on_sync_timeout()
sync_window.open()
await _settle()
await on_state.call("sync_result", sync_window)
sync_window.close()
await _settle()
## Returns where every Control and Window in [param subtree] is on screen, keyed by its
## path from [param main]: [code]{"rect": [x, y, width, height], "visible": bool}[/code].
## Nodes added at runtime without a name are keyed by their index, e.g. [code]#2[/code],
## so the keys are the same on every run.
static func dump_layout(main: Node, subtree: Node) -> Dictionary:
var layout: Dictionary = {}
var path: String = String(main.get_path_to(subtree))
_dump_node(subtree, path, layout)
return layout
static func _dump_node(node: Node, path: String, layout: Dictionary) -> void:
if node is Control:
var rect: Rect2 = (node as Control).get_global_rect()
layout[path] = {"rect": _round_rect(rect), "visible": node.is_visible_in_tree()}
elif node is Window:
var window: Window = node
layout[path] = {
"rect": _round_rect(Rect2(window.position, window.size)),
"visible": window.visible,
}
for child: Node in node.get_children():
var key: String = "#%d" % child.get_index() if "@" in child.name \
else String(child.name)
_dump_node(child, key if path == "." else path + "/" + key, layout)
static func _round_rect(rect: Rect2) -> Array[float]:
return [snappedf(rect.position.x, 0.01), snappedf(rect.position.y, 0.01),
snappedf(rect.size.x, 0.01), snappedf(rect.size.y, 0.01)]
## Finds a node of the main window by [param node_name], whether it is a direct child
## of the main window or of its Quiz or TopBar scene.
func _node(node_name: String) -> Node:
for parent: Node in [_main, _main.get_node_or_null("Quiz"),
_main.get_node_or_null("TopBar")]:
if parent != null && parent.has_node(node_name):
return parent.get_node(node_name)
push_error("UiTour: no node named " + node_name)
return null
## Calls [param method] on the main window, or on its Quiz scene if that has it.
func _call(method: String, args: Array = []) -> Variant:
var quiz: Node = _main.get_node_or_null("Quiz")
var target: Node = quiz if quiz != null && quiz.has_method(method) else _main
return target.callv(method, args)
func _server() -> FakeHttpTransport:
return MusicServerClient.transport
func _settle() -> void:
for i: int in 4:
await _main.get_tree().process_frame
func _add_player(player_name: String) -> void:
(_node("Players").get_node("VBoxContainer/AddPlayerContainer/PlayerNameField") \
as TextEdit).text = player_name
_call("add_player")
func _player_row(index: int) -> Player:
return _node("Players").get_node(
"VBoxContainer/ScrollContainer/PlayersListVBoxContainer").get_child(index)
## Stops the song so the time and progress bar look the same on every run.
func _freeze_music_player() -> void:
var music_player: MusicPlayer = _node("MusicPlayer")
music_player.audio_player.stop()
music_player.sound_player.stop()
music_player.progress_slider.value = 0
music_player.music_time_label.text = "00:00 / 00:01"
## The log starts each row with the time of day; use a fixed time instead.
func _fix_log_times() -> void:
var log_window: PopupWindow = _node("Log")
for row: Label in log_window.log_box.get_children():
row.text = "12:00:00 " + row.text.substr(9)
func _open_popup(popup_name: String) -> Node:
var popup: Node = _node(popup_name)
match popup_name:
"SyncWindow":
# Opening the window starts a sync; show the empty window instead
popup.open()
"NewCharacterSelect":
popup.show_grid("Crono")
"WinnerWindow":
popup.show_winner(Settings.player_array[0])
"StatisticsPopupPanel":
_call("get_statistics")
_:
popup.open()
return popup
func _close_popup(popup: Node) -> void:
if popup is PopupWindow:
popup.close()
else:
popup.visible = false
+1
View File
@@ -0,0 +1 @@
uid://v42qdc68bn17
@@ -0,0 +1,98 @@
extends GdUnitTestSuite
## Checks that every node of the main window is where it was before the window was
## split into scenes.
##
## [code]tests/data/layout_baseline.json[/code] was written by
## [code]tools/take_screenshots.gd[/code] before the split. It holds the position, size
## and visibility of every node in each UiTour state, keyed by the node's old path. This
## test runs the same tour and compares, after moving each old path to where the node
## lives now.
const BASELINE_PATH: String = "res://tests/data/layout_baseline.json"
## Nodes that moved from the main window into the Quiz scene, by their old name.
const QUIZ_NODES: Array[String] = [
"Open", "FileDialog", "MusicListPanel", "ScrollContainer", "VBoxContainer",
"MusicPlayer", "Players", "ResetPlaylistButton", "ResetPointsButton", "SettingsButton",
"SoundTestButton", "ShowAnswerButton", "NextButton", "RepeatSongCheckButton",
"DebugLabel", "SettingsWindow", "NewCharacterSelect", "WinnerWindow", "Log",
]
## Nodes that moved from the main window into the TopBar scene, by their old name.
const TOP_BAR_NODES: Array[String] = [
"TopPanelContainer", "Title", "SearchButton", "SyncButton", "StatisticsButton",
"ShortcutsButton", "AboutButton", "QRButton",
]
## How far a node may be from its old place, in pixels.
const TOLERANCE: float = 0.5
var _snapshot: AutoloadSnapshot
var _server: FakeHttpTransport
var _main: Control
var _layout: Dictionary = {}
func before() -> void:
_snapshot = AutoloadSnapshot.take([Settings, Playlist])
UiTour.prepare_autoloads()
_server = UiTour.make_server().install()
# Laid out at the size the baseline was taken at, like the screenshots
var viewport: SubViewport = auto_free(SubViewport.new())
viewport.size = Vector2i(1920, 1080)
viewport.gui_embed_subwindows = true
Engine.get_main_loop().root.add_child(viewport)
_main = load("res://MainWindow.tscn").instantiate()
viewport.add_child(_main)
await UiTour.new(_main).run(func(state: String, popup: Node) -> void:
_layout[state] = UiTour.dump_layout(_main, _main if popup == null else popup))
func after() -> void:
_main.free()
await Engine.get_main_loop().process_frame
_server.uninstall()
_snapshot.restore()
## Where the node that used to be at [param old_path] in the main window is now.
static func new_path(old_path: String) -> String:
var first: String = old_path.get_slice("/", 0)
if first in QUIZ_NODES:
return "Quiz/" + old_path
if first in TOP_BAR_NODES:
return "TopBar/" + old_path
return old_path
func _baseline() -> Dictionary:
return JSON.parse_string(FileAccess.get_file_as_string(BASELINE_PATH))
func test_tour_has_the_same_states_as_the_baseline() -> void:
assert_array(_layout.keys()).contains_exactly_in_any_order(_baseline().keys())
func test_every_node_is_where_it_was() -> void:
var problems: Array[String] = []
var baseline: Dictionary = _baseline()
for state: String in baseline:
var actual: Dictionary = _layout.get(state, {})
for old_path: String in baseline[state]:
var path: String = new_path(old_path)
var expected: Dictionary = baseline[state][old_path]
if !actual.has(path):
problems.append("%s: %s is missing (was %s)" % [state, path, old_path])
continue
var rect: Array = actual[path].rect
for i: int in 4:
if absf(rect[i] - expected.rect[i]) > TOLERANCE:
problems.append("%s: %s is at %s, was %s"
% [state, path, rect, expected.rect])
break
if actual[path].visible != expected.visible:
problems.append("%s: %s visible is %s, was %s"
% [state, path, actual[path].visible, expected.visible])
assert_array(problems).override_failure_message(
"%d nodes moved:\n%s" % [problems.size(), "\n".join(problems.slice(0, 30))]) \
.is_empty()
@@ -0,0 +1 @@
uid://b2077235y1g0f
+16 -16
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_array(_server.requested_paths).contains(["/music/rand", "/music/addQue", "/music/info"])
assert_int(Playlist.song_object_array.size()).is_equal(1)
assert_bool((_window.get_node("NextButton") as Button).disabled).is_false()
assert_bool((_quiz.get_node("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"))
_runner.invoke("fetch_song")
_quiz.fetch_song()
assert_int(_server.count("/music/rand/low")).is_equal(1)
@@ -33,7 +33,7 @@ func test_next_plays_preloaded_song_and_fetches_another() -> void:
func test_title_is_hidden_until_answer_is_shown() -> void:
var soundtrack_label: Label = _window.get_node("VBoxContainer/SoundtrackLabel")
var soundtrack_label: Label = _quiz.get_node("VBoxContainer/SoundtrackLabel")
_press("NextButton")
await _runner.simulate_frames(2)
@@ -81,13 +81,13 @@ func test_cannot_add_player_without_name() -> void:
func test_failed_song_download_is_retried_until_the_server_answers() -> void:
_window.song_fetch_retry_seconds = 0.05
_quiz.song_fetch_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")
_runner.invoke("fetch_song")
_quiz.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:
_window.song_fetch_retry_seconds = 0.05
_quiz.song_fetch_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")
_runner.invoke("fetch_song")
_quiz.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
_runner.invoke("fetch_song")
_quiz.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
_runner.invoke("fetch_song")
_quiz.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:
_window.song_fetch_retry_seconds = 0.05
_quiz.song_fetch_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)
_runner.invoke("fetch_song")
_quiz.fetch_song()
await await_millis(150)
assert_int(Playlist.get_preloaded_songs()).is_equal(2)
@@ -165,11 +165,11 @@ func _open_settings() -> void:
func _close_settings() -> void:
(_window.get_node("SettingsWindow") as Control).visible = false
(_quiz.get_node("SettingsWindow") as Control).visible = false
func _press_settings_button(unique_name: String) -> void:
(_window.get_node("SettingsWindow").get_node("%" + unique_name) as Button).pressed.emit()
(_quiz.get_node("SettingsWindow").get_node("%" + unique_name) as Button).pressed.emit()
func test_raising_cache_size_does_not_download_while_settings_are_open() -> void:
@@ -224,12 +224,12 @@ func test_show_answer_before_first_song_does_not_reveal_next_song() -> void:
await _play_first_song()
assert_str((_window.get_node("VBoxContainer/SoundtrackLabel") as Label).text) \
assert_str((_quiz.get_node("VBoxContainer/SoundtrackLabel") as Label).text) \
.is_equal("????????")
func test_log_adds_one_row_per_point_with_padded_time() -> void:
var log_box: VBoxContainer = _window.get_node("%Log").get_node("%LogVBoxContainer")
var log_box: VBoxContainer = _quiz.get_node("%Log").get_node("%LogVBoxContainer")
var crono: PlayerObject = _add_player("Crono")
await _play_first_song()
@@ -243,7 +243,7 @@ func test_log_adds_one_row_per_point_with_padded_time() -> void:
func test_inspiration_list_alternates_row_backgrounds() -> void:
var rows: Array[Node] = _window.get_node("ScrollContainer/VBoxContainer").get_children()
var rows: Array[Node] = _quiz.get_node("ScrollContainer/VBoxContainer").get_children()
assert_int(rows.size()).is_equal(2)
assert_str((rows[0] as ListRow).text).is_equal("Chrono Trigger")
@@ -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:
_window.song_fetch_retry_seconds = 0.05
_quiz.song_fetch_retry_seconds = 0.05
_server.respond_json("/music/list", [], 503)
_server.release("/music/list")
+1 -1
View File
@@ -143,7 +143,7 @@ func test_player_list_shows_rows_in_player_order() -> void:
await _remove_player(_crono)
var robo: PlayerObject = _add_player("Robo")
var rows: Array[Node] = _window.get_node(
var rows: Array[Node] = _quiz.get_node(
"Players/VBoxContainer/ScrollContainer/PlayersListVBoxContainer").get_children()
assert_array(rows.map(func(row: Player) -> PlayerObject: return row.player_object)) \
+32 -27
View File
@@ -1,28 +1,27 @@
extends MainWindowTestSuite
## All popups share PopupWindow: clicking outside the panel closes them, the backdrop
## dims the window behind them, and the closing click does not reach the main window.
## The main window and the quiz each have their own popups and their own backdrop.
const POPUP_NODES: Array[String] = [
"Search", "SyncWindow", "SettingsWindow", "QRWindow", "NewCharacterSelect",
"ShortcutsWindow", "AboutWindow", "WinnerWindow", "Log",
## Popups opened from the top bar, in the main window.
const MAIN_POPUPS: Array[String] = [
"Search", "SyncWindow", "QRWindow", "ShortcutsWindow", "AboutWindow",
]
## Popups opened from the quiz, in the Quiz scene.
const QUIZ_POPUPS: Array[String] = [
"SettingsWindow", "NewCharacterSelect", "WinnerWindow", "Log",
]
func _popup(node_name: String) -> PopupWindow:
return _window.get_node(node_name)
return _quiz.get_node(node_name) if node_name in QUIZ_POPUPS \
else _window.get_node(node_name)
func _backdrop() -> ColorRect:
return _window.get_node("PopupBackdrop")
## Clicks at [param position], given in the window's 1920x1080 layout coordinates.
func _click_at(position: Vector2) -> void:
# The runner takes window pixels; the headless test window is much smaller than the
# layout, so convert like a real mouse position would be
_runner.set_mouse_position(_window.get_viewport().get_final_transform() * position)
await _runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT)
await _runner.await_input_processed()
## The backdrop that shows behind [param popup].
func _backdrop(popup: PopupWindow = _popup("SettingsWindow")) -> ColorRect:
return popup.get_parent().get_node("PopupBackdrop")
## A point on screen outside the popup's panel.
@@ -32,34 +31,40 @@ func _outside(popup: PopupWindow) -> Vector2:
else Vector2(rect.position.x - 5, rect.position.y - 5)
func test_backdrop_is_hidden_without_popups() -> void:
assert_bool(_backdrop().visible).is_false()
func test_backdrops_are_hidden_without_popups() -> void:
assert_bool(_backdrop(_popup("SettingsWindow")).visible).is_false()
assert_bool(_backdrop(_popup("AboutWindow")).visible).is_false()
func test_backdrop_covers_the_whole_window() -> void:
_popup("SettingsWindow").open()
func test_backdrops_cover_the_whole_window() -> void:
for node_name: String in ["SettingsWindow", "AboutWindow"]:
var popup: PopupWindow = _popup(node_name)
popup.open()
assert_vector(_backdrop().get_global_rect().size).is_equal(_window.get_viewport_rect().size)
assert_object(_backdrop(popup).get_global_rect()).override_failure_message(
"%s: backdrop does not cover the window" % node_name) \
.is_equal(_window.get_viewport_rect())
popup.close()
func test_every_popup_is_a_popup_window() -> void:
for node_name: String in POPUP_NODES:
assert_object(_window.get_node(node_name)).is_instanceof(PopupWindow)
for node_name: String in MAIN_POPUPS + QUIZ_POPUPS:
assert_object(_popup(node_name)).is_instanceof(PopupWindow)
func test_every_popup_shows_backdrop_and_closes_on_outside_click() -> void:
for node_name: String in POPUP_NODES:
for node_name: String in MAIN_POPUPS + QUIZ_POPUPS:
var popup: PopupWindow = _popup(node_name)
popup.open()
await _runner.simulate_frames(1)
assert_bool(_backdrop().visible).override_failure_message(
assert_bool(_backdrop(popup).visible).override_failure_message(
"%s: backdrop not shown" % node_name).is_true()
await _click_at(_outside(popup))
assert_bool(popup.visible).override_failure_message(
"%s: not closed by outside click" % node_name).is_false()
assert_bool(_backdrop().visible).override_failure_message(
assert_bool(_backdrop(popup).visible).override_failure_message(
"%s: backdrop still shown" % node_name).is_false()
@@ -75,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 = _window.get_node("ResetPlaylistButton")
var reset_button: Button = _quiz.get_node("ResetPlaylistButton")
settings.open()
await _runner.simulate_frames(1)
assert_bool(settings.panel.get_global_rect().intersects(reset_button.get_global_rect())) \
@@ -88,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 = _window.get_node("ResetPlaylistButton")
var reset_button: Button = _quiz.get_node("ResetPlaylistButton")
await _click_at(reset_button.get_global_rect().get_center())
+195
View File
@@ -0,0 +1,195 @@
extends MainWindowTestSuite
## The main window holds the top bar, the quiz and the popups opened from the top bar.
## These tests check that the parts are where they belong and still work together:
## clicks reach the right scene, popups dim the whole window, and syncing, changing
## server and the keyboard shortcuts reach both the main window and the quiz.
const MAIN_WINDOW_CHILDREN: Array[String] = [
"TopBar", "Quiz", "PopupBackdrop", "Search", "SyncWindow", "QRWindow",
"ShortcutsWindow", "StatisticsPopupPanel", "AboutWindow",
]
const TOP_BAR_CHILDREN: Array[String] = [
"TopPanelContainer", "Title", "SyncButton", "SearchButton", "StatisticsButton",
"ShortcutsButton", "AboutButton", "QRButton",
]
## Each top bar button and the popup it opens in the main window.
const TOP_BAR_POPUPS: Dictionary = {
"SearchButton": "Search",
"SyncButton": "SyncWindow",
"StatisticsButton": "StatisticsPopupPanel",
"ShortcutsButton": "ShortcutsWindow",
"AboutButton": "AboutWindow",
"QRButton": "QRWindow",
}
func _top_bar_button(button_name: String) -> Button:
return _window.get_node("TopBar/" + button_name)
func _children(node: Node) -> Array[String]:
var names: Array[String] = []
for child: Node in node.get_children():
names.append(String(child.name))
return names
func _center(control: Control) -> Vector2:
return control.get_global_rect().get_center()
func test_main_window_holds_only_top_bar_quiz_and_its_popups() -> void:
assert_array(_children(_window)).contains_exactly_in_any_order(MAIN_WINDOW_CHILDREN)
func test_top_bar_is_drawn_below_the_quiz() -> void:
# The quiz popups' backdrop has to cover the top bar too
assert_int(_window.get_node("TopBar").get_index()) \
.is_less(_window.get_node("Quiz").get_index())
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)
func test_quiz_and_top_bar_let_clicks_through_where_they_are_empty() -> void:
assert_int(_quiz.mouse_filter).is_equal(Control.MOUSE_FILTER_IGNORE)
assert_int((_window.get_node("TopBar") as Control).mouse_filter) \
.is_equal(Control.MOUSE_FILTER_IGNORE)
func test_clicking_each_top_bar_button_opens_its_popup() -> void:
for button_name: String in TOP_BAR_POPUPS:
var popup: Node = _window.get_node(TOP_BAR_POPUPS[button_name])
await _click_at(_center(_top_bar_button(button_name)))
assert_bool(popup.visible).override_failure_message(
"%s did not open %s" % [button_name, popup.name]).is_true()
if popup is PopupWindow:
popup.close()
else:
popup.visible = false
await _runner.simulate_frames(1)
func test_clicking_quiz_buttons_plays_and_shows_answer() -> void:
await _click_at(_center(_quiz.get_node("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) \
.is_not_equal("Chrono Trigger")
await _click_at(_center(_quiz.get_node("ShowAnswerButton")))
assert_str((_quiz.get_node("VBoxContainer/SoundtrackLabel") as Label).text) \
.is_equal("Chrono Trigger")
func test_quiz_popup_backdrop_covers_the_top_bar() -> void:
var settings: PopupWindow = _quiz.get_node("SettingsWindow")
settings.open()
await _runner.simulate_frames(1)
var backdrop: Control = _quiz.get_node("PopupBackdrop")
assert_bool(backdrop.visible).is_true()
assert_bool(backdrop.get_global_rect().encloses(
(_window.get_node("TopBar/TopPanelContainer") as Control).get_global_rect())) \
.is_true()
func test_click_on_top_bar_while_quiz_popup_is_open_only_closes_it() -> void:
var settings: PopupWindow = _quiz.get_node("SettingsWindow")
var search: PopupWindow = _window.get_node("Search")
settings.open()
await _runner.simulate_frames(1)
await _click_at(_center(_top_bar_button("SearchButton")))
assert_bool(settings.visible).is_false()
assert_bool(search.visible).is_false()
func test_sync_disables_quiz_buttons_until_finished() -> void:
_server.respond_bytes("/sync", PackedByteArray())
_server.respond_json("/sync/progress", {"progress": 10, "time_spent": "00:00:01"})
var sync_window: PopupWindow = _window.get_node("SyncWindow")
_top_bar_button("SyncButton").pressed.emit()
sync_window.sync_timer.stop()
for button_name: String in ["SoundTestButton", "ResetPlaylistButton", "ShowAnswerButton",
"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()
_server.respond_json("/sync/progress", UiTour.SYNC_RESULT)
var reloads_before: int = _server.count("/music/all/order")
sync_window._on_sync_timeout()
await _runner.simulate_frames(2)
for button_name: String in ["SoundTestButton", "ResetPlaylistButton", "ShowAnswerButton",
"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
assert_int(_server.count("/music/all/order")).is_equal(reloads_before + 1)
assert_int(_server.count("/music/reset")).is_equal(1)
func test_changing_server_reloads_search_and_quiz() -> void:
var search_loads: int = _server.count("/music/all/order")
var inspiration_loads: int = _server.count("/music/all")
var player: PlayerObject = _add_player("Crono")
player.add_point()
_quiz.get_node("SettingsWindow").server_changed.emit()
assert_int(_server.count("/music/all/order")).is_equal(search_loads + 1)
assert_int(_server.count("/music/all")).is_equal(inspiration_loads + 1)
assert_int(_server.count("/music/reset")).is_equal(1)
assert_int(player.player_score).is_equal(0)
func test_alt_k_opens_shortcuts_and_alt_l_opens_log() -> void:
await _press_keys([KEY_ALT, KEY_K])
assert_bool((_window.get_node("ShortcutsWindow") as Control).visible).is_true()
await _press_keys([KEY_ALT, KEY_L])
assert_bool((_quiz.get_node("Log") as Control).visible).is_true()
func test_alt_down_lowers_the_volume() -> void:
var bus: int = AudioServer.get_bus_index("Master")
var volume_db: float = AudioServer.get_bus_volume_db(bus)
var slider: HSlider = _quiz.get_node("MusicPlayer").volume_slider
var volume: float = slider.value
await _press_keys([KEY_ALT, KEY_DOWN])
assert_float(slider.value).is_less(volume)
AudioServer.set_bus_volume_db(bus, volume_db)
func test_alt_enter_switches_fullscreen_and_settings_shows_it() -> void:
var settings: PopupWindow = _quiz.get_node("SettingsWindow")
var was_fullscreen: bool = Settings.fullscreen
await _press_keys([KEY_ALT, KEY_ENTER])
settings.open()
assert_bool(Settings.fullscreen).is_equal(!was_fullscreen)
assert_bool(settings.fullscreen_button.button_pressed).is_equal(!was_fullscreen)
await _press_keys([KEY_ALT, KEY_ENTER])
assert_bool(Settings.fullscreen).is_equal(was_fullscreen)
func test_statistics_counts_the_soundtracks() -> void:
_top_bar_button("StatisticsButton").pressed.emit()
assert_str((_window.get_node("StatisticsPopupPanel/StatisticsLabel") as Label).text) \
.is_equal("Total amount of soundtracks in the playlist: 2")
@@ -0,0 +1 @@
uid://c6lqhrfu02227
+2 -2
View File
@@ -12,7 +12,7 @@ func before_test() -> void:
func _stats_checkbox() -> CheckButton:
return _window.get_node("SettingsWindow").get_node("%AddToDatabaseCheckButton")
return _quiz.get_node("SettingsWindow").get_node("%AddToDatabaseCheckButton")
func test_winning_turns_statistics_off() -> void:
@@ -57,7 +57,7 @@ func test_songs_after_a_win_are_not_counted() -> void:
func test_winning_score_cannot_go_below_one() -> void:
_press("SettingsButton")
var lower_button: Button = _window.get_node("SettingsWindow").get_node("%LowerButton")
var lower_button: Button = _quiz.get_node("SettingsWindow").get_node("%LowerButton")
for i: int in 3:
lower_button.pressed.emit()
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Compares two folders of screenshots from tools/take_screenshots.gd pixel by pixel.
Usage: tools/compare_screenshots.py <before dir> <after dir> [<diff dir>]
Prints how many pixels differ in each screenshot. With a diff dir, writes an image
for each screenshot that differs, with the changed pixels in red over a faded copy of
the after picture. Exits with 1 if any screenshot differs or is missing.
"""
import sys
from pathlib import Path
from PIL import Image, ImageChops
def main() -> int:
if len(sys.argv) < 3:
print(__doc__)
return 2
before_dir, after_dir = Path(sys.argv[1]), Path(sys.argv[2])
diff_dir = Path(sys.argv[3]) if len(sys.argv) > 3 else None
if diff_dir:
diff_dir.mkdir(parents=True, exist_ok=True)
names = sorted({p.name for p in before_dir.glob("*.png")}
| {p.name for p in after_dir.glob("*.png")})
failed = False
for name in names:
before_path, after_path = before_dir / name, after_dir / name
if not before_path.exists() or not after_path.exists():
print(f"{name}: missing in {'before' if not before_path.exists() else 'after'}")
failed = True
continue
before = Image.open(before_path).convert("RGB")
after = Image.open(after_path).convert("RGB")
if before.size != after.size:
print(f"{name}: size {before.size} -> {after.size}")
failed = True
continue
mask = ImageChops.difference(before, after).convert("L").point(
lambda value: 255 if value else 0)
changed = mask.histogram()[255]
print(f"{name}: {changed} pixels differ")
if changed:
failed = True
if diff_dir:
faded = Image.blend(after, Image.new("RGB", after.size, "white"), 0.7)
faded.paste(Image.new("RGB", after.size, "red"), mask=mask)
faded.save(diff_dir / name)
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())
+53
View File
@@ -0,0 +1,53 @@
extends SceneTree
## Takes a screenshot of the main window in every UiTour state, against a fake server.
##
## Usage:
## godot --path . -s tools/take_screenshots.gd -- <output dir>
##
## Writes [code]<state>.png[/code] for each state and [code]layout.json[/code] with where
## every node is in each state. With [code]--headless[/code] only the layout is written.
## Compare two runs with [code]tools/compare_screenshots.py[/code].
const SIZE: Vector2i = Vector2i(1920, 1080)
func _initialize() -> void:
_take.call_deferred()
func _take() -> void:
var args: PackedStringArray = OS.get_cmdline_user_args()
var out_dir: String = args[0] if !args.is_empty() else "user://screenshots"
DirAccess.make_dir_recursive_absolute(out_dir)
var draws: bool = DisplayServer.get_name() != "headless"
# Loaded here, not named as a class: autoloads such as Playlist do not exist yet
# when this script is compiled
var ui_tour: GDScript = load("res://tests/fakes/ui_tour.gd")
ui_tour.prepare_autoloads()
var server: RefCounted = ui_tour.make_server().install()
# Drawn at the layout size, so the pictures do not depend on the screen size
var viewport: SubViewport = SubViewport.new()
viewport.size = SIZE
viewport.gui_embed_subwindows = true
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
root.add_child(viewport)
var main: Control = load("res://MainWindow.tscn").instantiate()
viewport.add_child(main)
var layout: Dictionary = {}
var save_state: Callable = func(state: String, popup: Node) -> void:
layout[state] = ui_tour.dump_layout(main, main if popup == null else popup)
if draws:
await RenderingServer.frame_post_draw
viewport.get_texture().get_image().save_png(out_dir.path_join(state + ".png"))
print("Saved state ", state)
await ui_tour.new(main).run(save_state)
var file: FileAccess = FileAccess.open(out_dir.path_join("layout.json"), FileAccess.WRITE)
file.store_string(JSON.stringify(layout, " ", true))
file.close()
server.uninstall()
main.queue_free()
quit()
+1
View File
@@ -0,0 +1 @@
uid://0837em1gsrrx
+22
View File
@@ -0,0 +1,22 @@
class_name TopBar
extends Control
## The bar along the top of the main window, with the title and the buttons for things
## that are not part of a quiz.
signal search_pressed
signal sync_pressed
signal statistics_pressed
signal shortcuts_pressed
signal about_pressed
signal qr_pressed
@onready var sync_button: Button = $SyncButton
func _ready() -> void:
$SearchButton.pressed.connect(search_pressed.emit)
sync_button.pressed.connect(sync_pressed.emit)
$StatisticsButton.pressed.connect(statistics_pressed.emit)
$ShortcutsButton.pressed.connect(shortcuts_pressed.emit)
$AboutButton.pressed.connect(about_pressed.emit)
$QRButton.pressed.connect(qr_pressed.emit)
+1
View File
@@ -0,0 +1 @@
uid://bb2ukabiec1vv