diff --git a/data/playlist.gd b/data/playlist.gd index 033283f..c91bba9 100644 --- a/data/playlist.gd +++ b/data/playlist.gd @@ -11,6 +11,9 @@ var first_song_downloaded: bool = false var first_song_played: bool = false var number_of_tracks_to_preload: int = 1 var is_song_downloading: bool = false +## The first song of the current game. Songs before it belong to earlier games, which +## ended with Reset points. +var game_start_song: int = 0 func get_next_song() -> SongObject: @@ -68,6 +71,7 @@ func get_playlist() -> Array[SongObject]: func reset_playlist() -> void: currently_playing_song = -1 latest_played_song = 0 + game_start_song = 0 clear_song_list() @@ -100,6 +104,12 @@ func remove_point(player_id: String) -> void: song_object_array[currently_playing_song].remove_point(player_id) +## Removes every player's points from every song, for a new game. +func clear_points() -> void: + for song_object: SongObject in song_object_array: + song_object.players_given_point.clear() + + ## Removes a player's points from every song. func remove_player_from_list(player_id: String) -> void: for song_object: SongObject in song_object_array: diff --git a/data/score_history.gd b/data/score_history.gd new file mode 100644 index 0000000..6d998ac --- /dev/null +++ b/data/score_history.gd @@ -0,0 +1,121 @@ +class_name ScoreHistory +extends RefCounted +## Who got a point on which song of the current game, for the graph and the summary on +## the Winner popup. +## +## Songs are counted from 1 at the start of the game, which is the start of the playlist +## or the first song after the last Reset points. + +## The players, in the order of [code]Settings.player_array[/code]. +var players: Array[PlayerObject] = [] +## For each song of the game, in the order they were played, the ids of the players who +## got a point on it. +var songs: Array[Array] = [] + + +## The current game, from [code]Playlist.game_start_song[/code] up to the last song that +## has played. Songs downloaded ahead but not played yet are left out, and so are points +## of players who are no longer in [param game_players]. +static func from_playlist(game_players: Array[PlayerObject]) -> ScoreHistory: + var song_scorers: Array[Array] = [] + # Not just the song playing now: an older song may have been picked in the music list + var last_song: int = mini(Playlist.currently_playing_song, + Playlist.song_object_array.size() - 1) + for song_no: int in Playlist.song_object_array.size(): + if Playlist.song_object_array[song_no].has_played: + last_song = maxi(last_song, song_no) + for song_no: int in range(Playlist.game_start_song, last_song + 1): + var song: SongObject = Playlist.song_object_array[song_no] + if song.has_played || song_no == Playlist.currently_playing_song: + song_scorers.append(song.players_given_point) + return from_songs(game_players, song_scorers) + + +## A history of [param song_scorers], the ids of the players who scored on each song. +static func from_songs(game_players: Array[PlayerObject], song_scorers: Array) -> ScoreHistory: + var history: ScoreHistory = ScoreHistory.new() + history.players = game_players.duplicate() + var ids: Array[String] = [] + for player: PlayerObject in game_players: + ids.append(player.id) + for scorers: Array in song_scorers: + history.songs.append(scorers.filter(func(id: String) -> bool: return ids.has(id))) + return history + + +## [param player_id]'s score before the first song and after each song. +func scores_after_songs(player_id: String) -> Array[int]: + var scores: Array[int] = [0] + for scorers: Array in songs: + scores.append(scores.back() + (1 if scorers.has(player_id) else 0)) + return scores + + +## [param player_id]'s score after the last song. +func final_score(player_id: String) -> int: + return scores_after_songs(player_id).back() + + +## The player with the most songs in a row with a point, e.g. +## "Crono, 5 songs in a row (14–18)". The earliest streak wins a tie. +func longest_streak_text() -> String: + if songs.is_empty(): + return "No songs played" + var best_length: int = 0 + var best_start: int = 0 + var best_player: PlayerObject = null + for player: PlayerObject in players: + var length: int = 0 + for song: int in songs.size(): + length = length + 1 if songs[song].has(player.id) else 0 + var start: int = song - length + 1 + if length > best_length || (length == best_length && length > 0 \ + && start < best_start): + best_length = length + best_start = start + best_player = player + if best_length < 2: + return "Nobody scored twice in a row" + return "%s, %d songs in a row (%d–%d)" % [best_player.player_name, best_length, + best_start + 1, best_start + best_length] + + +## The most songs in a row where nobody scored, e.g. "Songs 6–10, nobody scored". +func longest_standstill_text() -> String: + if songs.is_empty(): + return "No songs played" + var best_length: int = 0 + var best_start: int = 0 + var length: int = 0 + for song: int in songs.size(): + length = length + 1 if songs[song].is_empty() else 0 + if length > best_length: + best_length = length + best_start = song - length + 1 + if best_length == 0: + return "Someone scored on every song" + if best_length == 1: + return "Song %d, nobody scored" % (best_start + 1) + return "Songs %d–%d, nobody scored" % [best_start + 1, best_start + best_length] + + +## The song most players scored on, e.g. "Song 11, Marle and Lucca", or +## "Song 11, 3 players" when more than two did. The earliest song wins a tie. +func most_shared_text() -> String: + if songs.is_empty(): + return "No songs played" + var best_song: int = -1 + for song: int in songs.size(): + if songs[song].size() >= 2 \ + && (best_song < 0 || songs[song].size() > songs[best_song].size()): + best_song = song + if best_song < 0: + return "No song had two scorers" + var scorers: Array = songs[best_song] + if scorers.size() > 2: + return "Song %d, %d players" % [best_song + 1, scorers.size()] + var names: PackedStringArray = [] + for player: PlayerObject in players: + if scorers.has(player.id): + names.append(player.player_name) + return "Song %d, %s" % [best_song + 1, " and ".join(names)] diff --git a/data/score_history.gd.uid b/data/score_history.gd.uid new file mode 100644 index 0000000..d4513cd --- /dev/null +++ b/data/score_history.gd.uid @@ -0,0 +1 @@ +uid://dcvwgwhlu80qh diff --git a/popups/WinnerPopup.tscn b/popups/WinnerPopup.tscn index 4c02b63..76ef549 100644 --- a/popups/WinnerPopup.tscn +++ b/popups/WinnerPopup.tscn @@ -2,6 +2,7 @@ [ext_resource type="Script" uid="uid://dxpb22xbwf3t1" path="res://popups/winner_popup.gd" id="1_winner_script"] [ext_resource type="Texture2D" uid="uid://r4as0nmtoa7p" path="res://noCharacter.png" id="2_no_character"] +[ext_resource type="Script" path="res://popups/score_graph.gd" id="3_score_graph"] [node name="WinnerPopup" type="Control" unique_id=1111111111 node_paths=PackedStringArray("panel")] layout_mode = 3 @@ -16,37 +17,120 @@ panel = NodePath("WinnerPanel") [node name="WinnerPanel" type="Panel" parent="." unique_id=89212869] unique_name_in_owner = true layout_mode = 0 -offset_right = 500.0 -offset_bottom = 600.0 +offset_right = 1600.0 +offset_bottom = 920.0 theme_type_variation = &"PopupBackground" -[node name="VBoxContainer" type="VBoxContainer" parent="WinnerPanel" unique_id=471159349] +[node name="GameColumn" type="VBoxContainer" parent="WinnerPanel" unique_id=471159351] layout_mode = 0 -offset_top = 25.0 -offset_right = 500.0 -offset_bottom = 282.0 -size_flags_horizontal = 3 -size_flags_vertical = 3 -alignment = 1 +offset_left = 24.0 +offset_top = 20.0 +offset_right = 1576.0 +offset_bottom = 900.0 +theme_override_constants/separation = 12 -[node name="WinnerPicture" type="TextureRect" parent="WinnerPanel/VBoxContainer" unique_id=1543774067] +[node name="Header" type="HBoxContainer" parent="WinnerPanel/GameColumn" unique_id=471159349] +layout_mode = 2 +alignment = 1 +theme_override_constants/separation = 32 + +[node name="WinnerPicture" type="TextureRect" parent="WinnerPanel/GameColumn/Header" unique_id=1543774067] unique_name_in_owner = true -custom_minimum_size = Vector2(480, 200) +custom_minimum_size = Vector2(360, 180) layout_mode = 2 texture = ExtResource("2_no_character") expand_mode = 3 stretch_mode = 5 -[node name="WinnerLabel" type="Label" parent="WinnerPanel/VBoxContainer" unique_id=1437597286] +[node name="WinnerInfo" type="VBoxContainer" parent="WinnerPanel/GameColumn/Header" unique_id=471159367] +custom_minimum_size = Vector2(520, 0) +layout_mode = 2 +alignment = 1 + +[node name="WinnerLabel" type="Label" parent="WinnerPanel/GameColumn/Header/WinnerInfo" unique_id=1437597286] unique_name_in_owner = true layout_mode = 2 theme_type_variation = &"TitleMedium" text = "Winner!" -horizontal_alignment = 1 -vertical_alignment = 2 +autowrap_mode = 3 -[node name="PlayersList" type="VBoxContainer" parent="WinnerPanel/VBoxContainer" unique_id=471159350] +[node name="PlayersList" type="VBoxContainer" parent="WinnerPanel/GameColumn/Header/WinnerInfo" unique_id=471159350] +unique_name_in_owner = true +layout_mode = 2 + +[node name="Legend" type="HBoxContainer" parent="WinnerPanel/GameColumn" unique_id=471159352] +unique_name_in_owner = true +layout_mode = 2 +alignment = 1 +theme_override_constants/separation = 20 + +[node name="ScoreGraph" type="Control" parent="WinnerPanel/GameColumn" unique_id=471159353] unique_name_in_owner = true layout_mode = 2 -size_flags_horizontal = 3 size_flags_vertical = 3 +mouse_filter = 2 +script = ExtResource("3_score_graph") + +[node name="Summary" type="HBoxContainer" parent="WinnerPanel/GameColumn" unique_id=471159354] +layout_mode = 2 +theme_override_constants/separation = 12 + +[node name="StreakBox" type="PanelContainer" parent="WinnerPanel/GameColumn/Summary" unique_id=471159355] +custom_minimum_size = Vector2(0, 84) +layout_mode = 2 +size_flags_horizontal = 3 +theme_type_variation = &"ListRowEven" + +[node name="VBox" type="VBoxContainer" parent="WinnerPanel/GameColumn/Summary/StreakBox" unique_id=471159356] +layout_mode = 2 + +[node name="Header" type="Label" parent="WinnerPanel/GameColumn/Summary/StreakBox/VBox" unique_id=471159357] +layout_mode = 2 +theme_type_variation = &"HighlightMedium" +text = "Longest streak" + +[node name="StreakLabel" type="Label" parent="WinnerPanel/GameColumn/Summary/StreakBox/VBox" unique_id=471159358] +unique_name_in_owner = true +custom_minimum_size = Vector2(1, 0) +layout_mode = 2 +autowrap_mode = 3 + +[node name="StandstillBox" type="PanelContainer" parent="WinnerPanel/GameColumn/Summary" unique_id=471159359] +custom_minimum_size = Vector2(0, 84) +layout_mode = 2 +size_flags_horizontal = 3 +theme_type_variation = &"ListRowEven" + +[node name="VBox" type="VBoxContainer" parent="WinnerPanel/GameColumn/Summary/StandstillBox" unique_id=471159360] +layout_mode = 2 + +[node name="Header" type="Label" parent="WinnerPanel/GameColumn/Summary/StandstillBox/VBox" unique_id=471159361] +layout_mode = 2 +theme_type_variation = &"HighlightMedium" +text = "Longest standstill" + +[node name="StandstillLabel" type="Label" parent="WinnerPanel/GameColumn/Summary/StandstillBox/VBox" unique_id=471159362] +unique_name_in_owner = true +custom_minimum_size = Vector2(1, 0) +layout_mode = 2 +autowrap_mode = 3 + +[node name="SharedBox" type="PanelContainer" parent="WinnerPanel/GameColumn/Summary" unique_id=471159363] +custom_minimum_size = Vector2(0, 84) +layout_mode = 2 +size_flags_horizontal = 3 +theme_type_variation = &"ListRowEven" + +[node name="VBox" type="VBoxContainer" parent="WinnerPanel/GameColumn/Summary/SharedBox" unique_id=471159364] +layout_mode = 2 + +[node name="Header" type="Label" parent="WinnerPanel/GameColumn/Summary/SharedBox/VBox" unique_id=471159365] +layout_mode = 2 +theme_type_variation = &"HighlightMedium" +text = "Most shared song" + +[node name="SharedLabel" type="Label" parent="WinnerPanel/GameColumn/Summary/SharedBox/VBox" unique_id=471159366] +unique_name_in_owner = true +custom_minimum_size = Vector2(1, 0) +layout_mode = 2 +autowrap_mode = 3 diff --git a/popups/score_graph.gd b/popups/score_graph.gd new file mode 100644 index 0000000..6e9a5cd --- /dev/null +++ b/popups/score_graph.gd @@ -0,0 +1,158 @@ +class_name ScoreGraph +extends Control +## Draws every player's score after each song of the game as a step line, so streaks and +## stretches where nobody scored stand out. Shown on the Winner popup. +## +## Colours and sizes come from the theme's [code]ScoreGraph[/code] type. Each player gets +## the colour of their position in [code]Settings.player_array[/code]. + +const THEME_TYPE: StringName = &"ScoreGraph" +## Room for the score labels on the left and right, and the song labels below. +const MARGIN_LEFT: float = 32.0 +const MARGIN_RIGHT: float = 32.0 +const MARGIN_TOP: float = 22.0 +const MARGIN_BOTTOM: float = 44.0 +## About how many song numbers fit along the bottom. +const MAX_SONG_LABELS: int = 20 + +var _history: ScoreHistory +var _winning_score: int = 0 + + +## Shows [param history]. The y axis goes up to [param winning_score], or higher if +## someone has more points. +func show_history(history: ScoreHistory, winning_score: int) -> void: + _history = history + _winning_score = winning_score + queue_redraw() + + +## The colour of the player at [param index] in the player list. +func player_color(index: int) -> Color: + return get_theme_color(StringName("player_%d" % (index % 7 + 1)), THEME_TYPE) + + +## Where the line of the player at [param index] is drawn after each song, starting +## before the first song. +func line_points(index: int) -> PackedVector2Array: + var points: PackedVector2Array = [] + var scores: Array[int] = _history.scores_after_songs(_history.players[index].id) + points.append(_point(0, 0, index)) + for song: int in range(1, scores.size()): + if scores[song] != scores[song - 1]: + points.append(_point(song, scores[song - 1], index)) + points.append(_point(song, scores[song], index)) + points.append(_point(scores.size() - 1, scores.back(), index)) + return points + + +func _draw() -> void: + if _history == null: + return + var font: Font = get_theme_default_font() + var font_size: int = get_theme_font_size(&"font_size", THEME_TYPE) + var text_color: Color = get_theme_color(&"axis_text", THEME_TYPE) + _draw_score_axis(font, font_size, text_color) + _draw_song_axis(font, font_size, text_color) + + var line_width: int = get_theme_constant(&"line_width", THEME_TYPE) + var dot_radius: int = get_theme_constant(&"dot_radius", THEME_TYPE) + for index: int in _history.players.size(): + var color: Color = player_color(index) + var scores: Array[int] = _history.scores_after_songs(_history.players[index].id) + draw_polyline(line_points(index), color, line_width, true) + for song: int in range(1, scores.size()): + if scores[song] > scores[song - 1]: + draw_circle(_point(song, scores[song], index), dot_radius, color) + var label_positions: Array[Vector2] = score_label_positions() + for index: int in _history.players.size(): + draw_string(font, label_positions[index], + str(_history.final_score(_history.players[index].id)), + HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, player_color(index)) + + +## Where each player's score is written at the end of their line, as the baseline of the +## text. Scores that would overlap are moved apart. +func score_label_positions() -> Array[Vector2]: + var font_size: int = get_theme_font_size(&"font_size", THEME_TYPE) + var positions: Array[Vector2] = [] + for index: int in _history.players.size(): + var score: int = _history.final_score(_history.players[index].id) + var end: Vector2 = _point(_history.songs.size(), score, index) + positions.append(end + Vector2(8, font_size * 0.35)) + # From the top down, push each label below the one above it + var order: Array = range(positions.size()) + order.sort_custom(func(a: int, b: int) -> bool: return positions[a].y < positions[b].y) + for i: int in range(1, order.size()): + var above: Vector2 = positions[order[i - 1]] + if positions[order[i]].y - above.y < font_size: + positions[order[i]].y = above.y + font_size + return positions + + +## The songs whose numbers are written along the bottom. +func labelled_songs() -> Array[int]: + var song_count: int = _history.songs.size() + var step: int = maxi(1, ceili(song_count / float(MAX_SONG_LABELS))) + var songs: Array[int] = [] + for song: int in range(1, song_count + 1): + # The last song is always labelled, unless that would crowd its neighbour + var is_last: bool = song == song_count && (song - 1) % step >= step / 2.0 + if (song - 1) % step == 0 || is_last: + songs.append(song) + return songs + + +## Where on the x axis [param song] is, counting the start of the game as song 0. +func song_x(song: int) -> float: + return _x(song) + + +func _draw_score_axis(font: Font, font_size: int, text_color: Color) -> void: + var grid_color: Color = get_theme_color(&"grid", THEME_TYPE) + var top: int = _top_score() + var step: int = maxi(1, ceili(top / 10.0)) + for score: int in range(0, top + 1, step): + var y: float = _y(score) + draw_line(Vector2(MARGIN_LEFT, y), Vector2(size.x - MARGIN_RIGHT, y), grid_color, 1) + draw_string(font, Vector2(0, y + font_size * 0.35), str(score), + HORIZONTAL_ALIGNMENT_RIGHT, MARGIN_LEFT - 8, font_size, text_color) + var winning_color: Color = get_theme_color(&"winning_line", THEME_TYPE) + var winning_y: float = _y(_winning_score) + draw_dashed_line(Vector2(MARGIN_LEFT, winning_y), Vector2(size.x - MARGIN_RIGHT, winning_y), + winning_color, 1, 6) + draw_string(font, Vector2(MARGIN_LEFT + 4, winning_y - 6), "winning score", + HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, winning_color) + + +func _draw_song_axis(font: Font, font_size: int, text_color: Color) -> void: + var label_y: float = size.y - MARGIN_BOTTOM + font_size + 6 + for song: int in labelled_songs(): + draw_string(font, Vector2(_x(song) - 20, label_y), str(song), + HORIZONTAL_ALIGNMENT_CENTER, 40, font_size, text_color) + draw_string(font, Vector2(0, size.y - 4), "Song", HORIZONTAL_ALIGNMENT_CENTER, size.x, + font_size, text_color) + + +func _point(song: int, score: int, index: int) -> Vector2: + # Lines of players with the same score sit side by side instead of on top of each other + var spread: float = get_theme_constant(&"line_width", THEME_TYPE) \ + * (index - (_history.players.size() - 1) / 2.0) + return Vector2(_x(song), _y(score) + spread) + + +func _x(song: int) -> float: + var width: float = size.x - MARGIN_LEFT - MARGIN_RIGHT + return MARGIN_LEFT + song * width / maxi(_history.songs.size(), 1) + + +func _y(score: int) -> float: + var height: float = size.y - MARGIN_TOP - MARGIN_BOTTOM + return size.y - MARGIN_BOTTOM - score * height / _top_score() + + +func _top_score() -> int: + var top: int = maxi(_winning_score, 1) + for player: PlayerObject in _history.players: + top = maxi(top, _history.final_score(player.id)) + return top diff --git a/popups/score_graph.gd.uid b/popups/score_graph.gd.uid new file mode 100644 index 0000000..0611218 --- /dev/null +++ b/popups/score_graph.gd.uid @@ -0,0 +1 @@ +uid://ccxvqduq1wmhp diff --git a/popups/winner_popup.gd b/popups/winner_popup.gd index 4330f3f..cbf56c0 100644 --- a/popups/winner_popup.gd +++ b/popups/winner_popup.gd @@ -1,11 +1,20 @@ class_name WinnerPopup extends BasePopup -## Shows who won, with their character, and where the other players placed. +## Shows who won, with their character, where the other players placed, and how the game +## went: a graph of everyone's score after each song and a few facts about it. + +## Size of the coloured square in front of each name in the legend. +const LEGEND_SQUARE_SIZE: Vector2 = Vector2(14, 14) @onready var winner_panel: Panel = %WinnerPanel @onready var winner_picture: TextureRect = %WinnerPicture @onready var winner_label: Label = %WinnerLabel @onready var players_list: VBoxContainer = %PlayersList +@onready var legend: HBoxContainer = %Legend +@onready var score_graph: ScoreGraph = %ScoreGraph +@onready var streak_label: Label = %StreakLabel +@onready var standstill_label: Label = %StandstillLabel +@onready var shared_label: Label = %SharedLabel func _ready() -> void: @@ -14,7 +23,6 @@ func _ready() -> void: func show_winner(winner: PlayerObject) -> void: visible = true - # Set winner info winner_label.text = winner.player_name + " won with " + str(winner.player_score) + \ " points!!" winner_picture.texture = winner.character @@ -23,9 +31,6 @@ func show_winner(winner: PlayerObject) -> void: winner_picture.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST else: winner_picture.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR - winner_picture.custom_minimum_size = Vector2(480, 200) - winner_picture.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL - winner_picture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED # Clear previous player list entries for child: Node in players_list.get_children(): @@ -45,6 +50,31 @@ func show_winner(winner: PlayerObject) -> void: var label: Label = Label.new() label.text = str(placing) + ". " + player.player_name + " - " + \ str(player.player_score) + " points" - label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER players_list.add_child(label) placing += 1 + + show_history(ScoreHistory.from_playlist(Settings.player_array)) + + +## Shows how the game went: the score graph, its legend and the summary boxes. +func show_history(history: ScoreHistory) -> void: + score_graph.show_history(history, Settings.winning_score) + streak_label.text = history.longest_streak_text() + standstill_label.text = history.longest_standstill_text() + shared_label.text = history.most_shared_text() + + for child: Node in legend.get_children(): + legend.remove_child(child) + child.queue_free() + for index: int in history.players.size(): + var player: PlayerObject = history.players[index] + var entry: HBoxContainer = HBoxContainer.new() + var square: ColorRect = ColorRect.new() + square.custom_minimum_size = LEGEND_SQUARE_SIZE + square.size_flags_vertical = Control.SIZE_SHRINK_CENTER + square.color = score_graph.player_color(index) + entry.add_child(square) + var name_label: Label = Label.new() + name_label.text = "%s %d" % [player.player_name, history.final_score(player.id)] + entry.add_child(name_label) + legend.add_child(entry) diff --git a/quiz/Quiz.tscn b/quiz/Quiz.tscn index 2e793c4..b850d2b 100644 --- a/quiz/Quiz.tscn +++ b/quiz/Quiz.tscn @@ -111,10 +111,10 @@ offset_bottom = 805.0 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 +offset_left = 160.0 +offset_top = 100.0 +offset_right = 160.0 +offset_bottom = 100.0 [node name="DebugLabel" type="Label" parent="." unique_id=1959221257] visible = false diff --git a/quiz/quiz.gd b/quiz/quiz.gd index 2ae7907..81f78fc 100644 --- a/quiz/quiz.gd +++ b/quiz/quiz.gd @@ -194,6 +194,10 @@ func reset_points() -> void: for player_objects: PlayerObject in Settings.player_array: player_objects.reset_points() players_panel.reload() + # The song playing now belongs to the game that just ended + Playlist.clear_points() + Playlist.game_start_song = Playlist.currently_playing_song + 1 + music_list_panel.refresh() # A new game starts, so turn statistics back on if winning turned them off if stats_paused_by_win: Settings.add_to_stats = true diff --git a/quiz/song_loader.gd b/quiz/song_loader.gd index cd829a4..f3ced59 100644 --- a/quiz/song_loader.gd +++ b/quiz/song_loader.gd @@ -33,6 +33,8 @@ func load_song_list() -> void: new_song_object.has_played = true Playlist.add_to_playlist(new_song_object) Playlist.update_last_played_song() + # Songs already on the server were played before, so the game starts after them + Playlist.game_start_song = Playlist.song_object_array.size() song_list_loaded.emit() fetch_song_if_idle() MusicServerClient.fetch_song_list(song_list_fetched, _retry_later.bind(load_song_list)) diff --git a/tests/data/layout_baseline.json b/tests/data/layout_baseline.json index 0ee3d75..b4ed50f 100644 --- a/tests/data/layout_baseline.json +++ b/tests/data/layout_baseline.json @@ -1874,8 +1874,8 @@ }, "Quiz/WinnerPopup": { "rect": [ - 720.0, - 153.0, + 160.0, + 100.0, 1920.0, 1080.0 ], @@ -1883,46 +1883,199 @@ }, "Quiz/WinnerPopup/WinnerPanel": { "rect": [ - 720.0, - 153.0, - 500.0, - 600.0 + 160.0, + 100.0, + 1600.0, + 920.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn": { "rect": [ - 720.0, - 178.0, - 500.0, - 257.0 + 184.0, + 120.0, + 1552.0, + 880.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/PlayersList": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header": { "rect": [ - 720.0, - 178.0, + 184.0, + 120.0, + 912.0, + 417.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo": { + "rect": [ + 184.0, + 120.0, + 520.0, + 417.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/PlayersList": { + "rect": [ + 184.0, + 120.0, 0.0, 0.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/WinnerLabel": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/WinnerLabel": { "rect": [ - 720.0, - 178.0, - 132.0, - 49.0 + 184.0, + 120.0, + 1.0, + 413.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/WinnerPicture": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerPicture": { "rect": [ - 720.0, - 178.0, - 480.0, - 200.0 + 184.0, + 120.0, + 360.0, + 180.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend": { + "rect": [ + 184.0, + 120.0, + 0.0, + 0.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/ScoreGraph": { + "rect": [ + 184.0, + 120.0, + 0.0, + 0.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary": { + "rect": [ + 184.0, + 120.0, + 508.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox": { + "rect": [ + 184.0, + 120.0, + 170.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox": { + "rect": [ + 184.0, + 120.0, + 154.0, + 53.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox/Header": { + "rect": [ + 184.0, + 120.0, + 154.0, + 26.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox/SharedLabel": { + "rect": [ + 184.0, + 120.0, + 1.0, + 23.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox": { + "rect": [ + 184.0, + 120.0, + 170.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox": { + "rect": [ + 184.0, + 120.0, + 154.0, + 53.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox/Header": { + "rect": [ + 184.0, + 120.0, + 154.0, + 26.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox/StandstillLabel": { + "rect": [ + 184.0, + 120.0, + 1.0, + 23.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox": { + "rect": [ + 184.0, + 120.0, + 144.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox": { + "rect": [ + 184.0, + 120.0, + 128.0, + 53.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox/Header": { + "rect": [ + 184.0, + 120.0, + 128.0, + 26.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox/StreakLabel": { + "rect": [ + 184.0, + 120.0, + 1.0, + 23.0 ], "visible": false }, @@ -5296,8 +5449,8 @@ }, "Quiz/WinnerPopup": { "rect": [ - 720.0, - 153.0, + 160.0, + 100.0, 1920.0, 1080.0 ], @@ -5305,46 +5458,199 @@ }, "Quiz/WinnerPopup/WinnerPanel": { "rect": [ - 720.0, - 153.0, - 500.0, - 600.0 + 160.0, + 100.0, + 1600.0, + 920.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn": { "rect": [ - 720.0, - 178.0, - 500.0, - 257.0 + 184.0, + 120.0, + 1552.0, + 880.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/PlayersList": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header": { "rect": [ - 720.0, - 178.0, + 184.0, + 120.0, + 912.0, + 417.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo": { + "rect": [ + 184.0, + 120.0, + 520.0, + 417.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/PlayersList": { + "rect": [ + 184.0, + 120.0, 0.0, 0.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/WinnerLabel": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/WinnerLabel": { "rect": [ - 720.0, - 178.0, - 132.0, - 49.0 + 184.0, + 120.0, + 1.0, + 413.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/WinnerPicture": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerPicture": { "rect": [ - 720.0, - 178.0, - 480.0, - 200.0 + 184.0, + 120.0, + 360.0, + 180.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend": { + "rect": [ + 184.0, + 120.0, + 0.0, + 0.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/ScoreGraph": { + "rect": [ + 184.0, + 120.0, + 0.0, + 0.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary": { + "rect": [ + 184.0, + 120.0, + 508.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox": { + "rect": [ + 184.0, + 120.0, + 170.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox": { + "rect": [ + 184.0, + 120.0, + 154.0, + 53.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox/Header": { + "rect": [ + 184.0, + 120.0, + 154.0, + 26.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox/SharedLabel": { + "rect": [ + 184.0, + 120.0, + 1.0, + 23.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox": { + "rect": [ + 184.0, + 120.0, + 170.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox": { + "rect": [ + 184.0, + 120.0, + 154.0, + 53.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox/Header": { + "rect": [ + 184.0, + 120.0, + 154.0, + 26.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox/StandstillLabel": { + "rect": [ + 184.0, + 120.0, + 1.0, + 23.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox": { + "rect": [ + 184.0, + 120.0, + 144.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox": { + "rect": [ + 184.0, + 120.0, + 128.0, + 53.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox/Header": { + "rect": [ + 184.0, + 120.0, + 128.0, + 26.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox/StreakLabel": { + "rect": [ + 184.0, + 120.0, + 1.0, + 23.0 ], "visible": false }, @@ -8808,8 +9114,8 @@ }, "Quiz/WinnerPopup": { "rect": [ - 720.0, - 153.0, + 160.0, + 100.0, 1920.0, 1080.0 ], @@ -8817,46 +9123,199 @@ }, "Quiz/WinnerPopup/WinnerPanel": { "rect": [ - 720.0, - 153.0, - 500.0, - 600.0 + 160.0, + 100.0, + 1600.0, + 920.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn": { "rect": [ - 720.0, - 178.0, - 500.0, - 257.0 + 184.0, + 120.0, + 1552.0, + 880.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/PlayersList": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header": { "rect": [ - 720.0, - 178.0, + 184.0, + 120.0, + 912.0, + 417.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo": { + "rect": [ + 184.0, + 120.0, + 520.0, + 417.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/PlayersList": { + "rect": [ + 184.0, + 120.0, 0.0, 0.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/WinnerLabel": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/WinnerLabel": { "rect": [ - 720.0, - 178.0, - 132.0, - 49.0 + 184.0, + 120.0, + 1.0, + 413.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/WinnerPicture": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerPicture": { "rect": [ - 720.0, - 178.0, - 480.0, - 200.0 + 184.0, + 120.0, + 360.0, + 180.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend": { + "rect": [ + 184.0, + 120.0, + 0.0, + 0.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/ScoreGraph": { + "rect": [ + 184.0, + 120.0, + 0.0, + 0.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary": { + "rect": [ + 184.0, + 120.0, + 508.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox": { + "rect": [ + 184.0, + 120.0, + 170.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox": { + "rect": [ + 184.0, + 120.0, + 154.0, + 53.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox/Header": { + "rect": [ + 184.0, + 120.0, + 154.0, + 26.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox/SharedLabel": { + "rect": [ + 184.0, + 120.0, + 1.0, + 23.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox": { + "rect": [ + 184.0, + 120.0, + 170.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox": { + "rect": [ + 184.0, + 120.0, + 154.0, + 53.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox/Header": { + "rect": [ + 184.0, + 120.0, + 154.0, + 26.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox/StandstillLabel": { + "rect": [ + 184.0, + 120.0, + 1.0, + 23.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox": { + "rect": [ + 184.0, + 120.0, + 144.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox": { + "rect": [ + 184.0, + 120.0, + 128.0, + 53.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox/Header": { + "rect": [ + 184.0, + 120.0, + 128.0, + 26.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox/StreakLabel": { + "rect": [ + 184.0, + 120.0, + 1.0, + 23.0 ], "visible": false }, @@ -12590,8 +13049,8 @@ "popup_WinnerWindow": { "Quiz/WinnerPopup": { "rect": [ - 720.0, - 153.0, + 160.0, + 100.0, 1920.0, 1080.0 ], @@ -12599,55 +13058,642 @@ }, "Quiz/WinnerPopup/WinnerPanel": { "rect": [ - 720.0, - 153.0, - 500.0, - 600.0 + 160.0, + 100.0, + 1600.0, + 920.0 ], "visible": true }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn": { "rect": [ - 720.0, - 178.0, - 500.0, - 280.0 + 184.0, + 120.0, + 1552.0, + 880.0 ], "visible": true }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/PlayersList": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header": { "rect": [ - 720.0, - 435.0, - 500.0, + 184.0, + 120.0, + 1552.0, + 180.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo": { + "rect": [ + 897.0, + 120.0, + 520.0, + 180.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/PlayersList": { + "rect": [ + 897.0, + 225.0, + 520.0, 23.0 ], "visible": true }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/PlayersList/#0": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/PlayersList/#0": { "rect": [ - 720.0, - 435.0, - 500.0, + 897.0, + 225.0, + 520.0, 23.0 ], "visible": true }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/WinnerLabel": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/WinnerLabel": { "rect": [ - 720.0, - 382.0, - 500.0, + 897.0, + 172.0, + 520.0, 49.0 ], "visible": true }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/WinnerPicture": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerPicture": { "rect": [ - 720.0, - 178.0, - 500.0, - 200.0 + 503.0, + 120.0, + 362.0, + 180.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend": { + "rect": [ + 184.0, + 312.0, + 1552.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#0": { + "rect": [ + 872.0, + 312.0, + 79.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#0/#0": { + "rect": [ + 872.0, + 316.0, + 14.0, + 14.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#0/#1": { + "rect": [ + 890.0, + 312.0, + 61.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#1": { + "rect": [ + 971.0, + 312.0, + 76.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#1/#0": { + "rect": [ + 971.0, + 316.0, + 14.0, + 14.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#1/#1": { + "rect": [ + 989.0, + 312.0, + 58.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/ScoreGraph": { + "rect": [ + 184.0, + 347.0, + 1552.0, + 557.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary": { + "rect": [ + 184.0, + 916.0, + 1552.0, + 84.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox": { + "rect": [ + 1226.0, + 916.0, + 510.0, + 84.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox": { + "rect": [ + 1234.0, + 918.0, + 494.0, + 80.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox/Header": { + "rect": [ + 1234.0, + 918.0, + 494.0, + 26.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox/SharedLabel": { + "rect": [ + 1234.0, + 948.0, + 494.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox": { + "rect": [ + 705.0, + 916.0, + 509.0, + 84.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox": { + "rect": [ + 713.0, + 918.0, + 493.0, + 80.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox/Header": { + "rect": [ + 713.0, + 918.0, + 493.0, + 26.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox/StandstillLabel": { + "rect": [ + 713.0, + 948.0, + 493.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox": { + "rect": [ + 184.0, + 916.0, + 509.0, + 84.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox": { + "rect": [ + 192.0, + 918.0, + 493.0, + 80.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox/Header": { + "rect": [ + 192.0, + 918.0, + 493.0, + 26.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox/StreakLabel": { + "rect": [ + 192.0, + 948.0, + 493.0, + 23.0 + ], + "visible": true + } + }, + "popup_WinnerWindow_long_game": { + "Quiz/WinnerPopup": { + "rect": [ + 160.0, + 100.0, + 1920.0, + 1080.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel": { + "rect": [ + 160.0, + 100.0, + 1600.0, + 920.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn": { + "rect": [ + 184.0, + 120.0, + 1552.0, + 880.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header": { + "rect": [ + 184.0, + 120.0, + 1552.0, + 180.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo": { + "rect": [ + 896.0, + 120.0, + 520.0, + 180.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/PlayersList": { + "rect": [ + 896.0, + 184.0, + 520.0, + 104.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/PlayersList/#0": { + "rect": [ + 896.0, + 184.0, + 520.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/PlayersList/#1": { + "rect": [ + 896.0, + 211.0, + 520.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/PlayersList/#2": { + "rect": [ + 896.0, + 238.0, + 520.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/PlayersList/#3": { + "rect": [ + 896.0, + 265.0, + 520.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/WinnerLabel": { + "rect": [ + 896.0, + 131.0, + 520.0, + 49.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerPicture": { + "rect": [ + 504.0, + 120.0, + 360.0, + 180.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend": { + "rect": [ + 184.0, + 312.0, + 1552.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#0": { + "rect": [ + 712.0, + 312.0, + 88.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#0/#0": { + "rect": [ + 712.0, + 316.0, + 14.0, + 14.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#0/#1": { + "rect": [ + 730.0, + 312.0, + 70.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#1": { + "rect": [ + 820.0, + 312.0, + 86.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#1/#0": { + "rect": [ + 820.0, + 316.0, + 14.0, + 14.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#1/#1": { + "rect": [ + 838.0, + 312.0, + 68.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#2": { + "rect": [ + 926.0, + 312.0, + 85.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#2/#0": { + "rect": [ + 926.0, + 316.0, + 14.0, + 14.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#2/#1": { + "rect": [ + 944.0, + 312.0, + 67.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#3": { + "rect": [ + 1031.0, + 312.0, + 75.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#3/#0": { + "rect": [ + 1031.0, + 316.0, + 14.0, + 14.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#3/#1": { + "rect": [ + 1049.0, + 312.0, + 57.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#4": { + "rect": [ + 1126.0, + 312.0, + 81.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#4/#0": { + "rect": [ + 1126.0, + 316.0, + 14.0, + 14.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#4/#1": { + "rect": [ + 1144.0, + 312.0, + 63.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/ScoreGraph": { + "rect": [ + 184.0, + 347.0, + 1552.0, + 557.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary": { + "rect": [ + 184.0, + 916.0, + 1552.0, + 84.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox": { + "rect": [ + 1226.0, + 916.0, + 510.0, + 84.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox": { + "rect": [ + 1234.0, + 918.0, + 494.0, + 80.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox/Header": { + "rect": [ + 1234.0, + 918.0, + 494.0, + 26.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox/SharedLabel": { + "rect": [ + 1234.0, + 948.0, + 494.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox": { + "rect": [ + 705.0, + 916.0, + 509.0, + 84.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox": { + "rect": [ + 713.0, + 918.0, + 493.0, + 80.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox/Header": { + "rect": [ + 713.0, + 918.0, + 493.0, + 26.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox/StandstillLabel": { + "rect": [ + 713.0, + 948.0, + 493.0, + 23.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox": { + "rect": [ + 184.0, + 916.0, + 509.0, + 84.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox": { + "rect": [ + 192.0, + 918.0, + 493.0, + 80.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox/Header": { + "rect": [ + 192.0, + 918.0, + 493.0, + 26.0 + ], + "visible": true + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox/StreakLabel": { + "rect": [ + 192.0, + 948.0, + 493.0, + 23.0 ], "visible": true } @@ -14527,8 +15573,8 @@ }, "Quiz/WinnerPopup": { "rect": [ - 720.0, - 153.0, + 160.0, + 100.0, 1920.0, 1080.0 ], @@ -14536,46 +15582,199 @@ }, "Quiz/WinnerPopup/WinnerPanel": { "rect": [ - 720.0, - 153.0, - 500.0, - 600.0 + 160.0, + 100.0, + 1600.0, + 920.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn": { "rect": [ - 720.0, - 178.0, - 500.0, - 257.0 + 184.0, + 120.0, + 1552.0, + 880.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/PlayersList": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header": { "rect": [ - 720.0, - 178.0, + 184.0, + 120.0, + 912.0, + 417.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo": { + "rect": [ + 184.0, + 120.0, + 520.0, + 417.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/PlayersList": { + "rect": [ + 184.0, + 120.0, 0.0, 0.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/WinnerLabel": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/WinnerLabel": { "rect": [ - 720.0, - 178.0, - 132.0, - 49.0 + 184.0, + 120.0, + 1.0, + 413.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/WinnerPicture": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerPicture": { "rect": [ - 720.0, - 178.0, - 480.0, - 200.0 + 184.0, + 120.0, + 360.0, + 180.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend": { + "rect": [ + 184.0, + 120.0, + 0.0, + 0.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/ScoreGraph": { + "rect": [ + 184.0, + 120.0, + 0.0, + 0.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary": { + "rect": [ + 184.0, + 120.0, + 508.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox": { + "rect": [ + 184.0, + 120.0, + 170.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox": { + "rect": [ + 184.0, + 120.0, + 154.0, + 53.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox/Header": { + "rect": [ + 184.0, + 120.0, + 154.0, + 26.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox/SharedLabel": { + "rect": [ + 184.0, + 120.0, + 1.0, + 23.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox": { + "rect": [ + 184.0, + 120.0, + 170.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox": { + "rect": [ + 184.0, + 120.0, + 154.0, + 53.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox/Header": { + "rect": [ + 184.0, + 120.0, + 154.0, + 26.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox/StandstillLabel": { + "rect": [ + 184.0, + 120.0, + 1.0, + 23.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox": { + "rect": [ + 184.0, + 120.0, + 144.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox": { + "rect": [ + 184.0, + 120.0, + 128.0, + 53.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox/Header": { + "rect": [ + 184.0, + 120.0, + 128.0, + 26.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox/StreakLabel": { + "rect": [ + 184.0, + 120.0, + 1.0, + 23.0 ], "visible": false }, @@ -22176,8 +23375,8 @@ }, "Quiz/WinnerPopup": { "rect": [ - 720.0, - 153.0, + 160.0, + 100.0, 1920.0, 1080.0 ], @@ -22185,55 +23384,262 @@ }, "Quiz/WinnerPopup/WinnerPanel": { "rect": [ - 720.0, - 153.0, - 500.0, - 600.0 + 160.0, + 100.0, + 1600.0, + 920.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn": { "rect": [ - 720.0, - 178.0, - 500.0, - 280.0 + 184.0, + 120.0, + 1552.0, + 880.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/PlayersList": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header": { "rect": [ - 720.0, - 435.0, - 500.0, + 184.0, + 120.0, + 1552.0, + 180.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo": { + "rect": [ + 897.0, + 120.0, + 520.0, + 180.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/PlayersList": { + "rect": [ + 897.0, + 225.0, + 520.0, 23.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/PlayersList/#0": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/PlayersList/#0": { "rect": [ - 720.0, - 435.0, - 500.0, + 897.0, + 225.0, + 520.0, 23.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/WinnerLabel": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerInfo/WinnerLabel": { "rect": [ - 720.0, - 382.0, - 500.0, + 897.0, + 172.0, + 520.0, 49.0 ], "visible": false }, - "Quiz/WinnerPopup/WinnerPanel/VBoxContainer/WinnerPicture": { + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Header/WinnerPicture": { "rect": [ - 720.0, - 178.0, - 500.0, - 200.0 + 503.0, + 120.0, + 362.0, + 180.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend": { + "rect": [ + 184.0, + 312.0, + 1552.0, + 23.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#0": { + "rect": [ + 872.0, + 312.0, + 79.0, + 23.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#0/#0": { + "rect": [ + 872.0, + 316.0, + 14.0, + 14.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#0/#1": { + "rect": [ + 890.0, + 312.0, + 61.0, + 23.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#1": { + "rect": [ + 971.0, + 312.0, + 76.0, + 23.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#1/#0": { + "rect": [ + 971.0, + 316.0, + 14.0, + 14.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Legend/#1/#1": { + "rect": [ + 989.0, + 312.0, + 58.0, + 23.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/ScoreGraph": { + "rect": [ + 184.0, + 347.0, + 1552.0, + 557.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary": { + "rect": [ + 184.0, + 916.0, + 1552.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox": { + "rect": [ + 1226.0, + 916.0, + 510.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox": { + "rect": [ + 1234.0, + 918.0, + 494.0, + 80.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox/Header": { + "rect": [ + 1234.0, + 918.0, + 494.0, + 26.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/SharedBox/VBox/SharedLabel": { + "rect": [ + 1234.0, + 948.0, + 494.0, + 23.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox": { + "rect": [ + 705.0, + 916.0, + 509.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox": { + "rect": [ + 713.0, + 918.0, + 493.0, + 80.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox/Header": { + "rect": [ + 713.0, + 918.0, + 493.0, + 26.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StandstillBox/VBox/StandstillLabel": { + "rect": [ + 713.0, + 948.0, + 493.0, + 23.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox": { + "rect": [ + 184.0, + 916.0, + 509.0, + 84.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox": { + "rect": [ + 192.0, + 918.0, + 493.0, + 80.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox/Header": { + "rect": [ + 192.0, + 918.0, + 493.0, + 26.0 + ], + "visible": false + }, + "Quiz/WinnerPopup/WinnerPanel/GameColumn/Summary/StreakBox/VBox/StreakLabel": { + "rect": [ + 192.0, + 948.0, + 493.0, + 23.0 ], "visible": false }, diff --git a/tests/fakes/ui_tour.gd b/tests/fakes/ui_tour.gd index 8b399f6..b7e61eb 100644 --- a/tests/fakes/ui_tour.gd +++ b/tests/fakes/ui_tour.gd @@ -47,6 +47,8 @@ const POPUPS: Array[Array] = [ ] var _main: Control +var _scores_before_win: Array[int] = [] +var _winning_score_before_win: int = 0 func _init(main: Control) -> void: @@ -104,6 +106,30 @@ static func soundtrack_statistics() -> Array[Dictionary]: return statistics +## Who scored on each song of a 60-song game to 20 points, the same on every run: the +## third of [param players] wins on the last song, nobody scores on songs 21–27 and the +## others stay below 20. Needs at least three players. +static func long_game(players: Array[PlayerObject]) -> Array: + var random: RandomNumberGenerator = RandomNumberGenerator.new() + random.seed = 7 + var winner: int = 2 + var scores: Array[int] = [] + scores.resize(players.size()) + var song_scorers: Array = [] + for song: int in 60: + var scorers: Array[String] = [] + for index: int in players.size(): + var limit: int = 19 if index == winner else 18 + var chance: float = 0.36 if index == winner else 0.2 + 0.02 * index + if song < 59 && (song < 20 || song >= 27) && scores[index] < limit \ + && random.randf() < chance: + scorers.append(players[index].id) + scores[index] += 1 + song_scorers.append(scorers) + song_scorers[59].append(players[winner].id) + return song_scorers + + ## The pictures in [constant CHARACTERS_DIR], sorted by name. static func character_names() -> Array[String]: var names: Array[String] = [] @@ -221,6 +247,24 @@ func run(on_state: Callable) -> void: _main.show_tab(&"quiz") await _settle() + # Last, as it adds players: a long game with five players on the Winner popup + for player_name: String in ["Lucca", "Frog", "Robo"]: + _add_player(player_name) + await _settle() + var players: Array[PlayerObject] = Settings.player_array + var song_scorers: Array = long_game(players) + var history: ScoreHistory = ScoreHistory.from_songs(players, song_scorers) + for player: PlayerObject in players: + player.player_score = history.final_score(player.id) + Settings.winning_score = 20 + var winner_popup: WinnerPopup = _node("WinnerPopup") + winner_popup.show_winner(players[2]) + winner_popup.show_history(history) + await _settle() + await on_state.call("popup_WinnerWindow_long_game", winner_popup) + winner_popup.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]. @@ -328,13 +372,41 @@ func _open_popup(popup_name: String) -> Node: "CharacterSelectPopup": popup.show_grid("Crono") "WinnerPopup": - popup.show_winner(Settings.player_array[0]) + _show_winner_after_a_game(popup) _: popup.open() return popup +## Shows the Winner popup after a whole game, so the graph has something to show. The +## scores are put back afterwards by [method _close_popup]. +func _show_winner_after_a_game(popup: WinnerPopup) -> void: + _scores_before_win = [] + for player: PlayerObject in Settings.player_array: + _scores_before_win.append(player.player_score) + var crono: PlayerObject = Settings.player_array[0] + var marle: PlayerObject = Settings.player_array[1] + var song_scorers: Array = [] + for song: int in range(1, 17): + var scorers: Array[String] = [] + if song in [2, 4, 5, 10, 11, 12, 13, 16]: + scorers.append(crono.id) + if song in [1, 3, 10, 14, 15]: + scorers.append(marle.id) + song_scorers.append(scorers) + crono.player_score = 8 + marle.player_score = 5 + _winning_score_before_win = Settings.winning_score + Settings.winning_score = 8 + popup.show_winner(crono) + popup.show_history(ScoreHistory.from_songs(Settings.player_array, song_scorers)) + + func _close_popup(popup: Node) -> void: + if popup is WinnerPopup: + Settings.winning_score = _winning_score_before_win + for i: int in _scores_before_win.size(): + Settings.player_array[i].player_score = _scores_before_win[i] if popup is BasePopup: popup.close() else: diff --git a/tests/integration/test_main_window_startup.gd b/tests/integration/test_main_window_startup.gd index cc03165..768ad9e 100644 --- a/tests/integration/test_main_window_startup.gd +++ b/tests/integration/test_main_window_startup.gd @@ -39,3 +39,12 @@ func test_failed_song_list_is_retried_before_preloading() -> void: assert_int(_server.count("/music/rand")).is_equal(1) assert_int(Playlist.get_preloaded_songs()).is_equal(1) + + +func test_game_starts_after_the_songs_already_on_the_server() -> void: + _server.release("/music/list") + + await _play_first_song() + + assert_int(Playlist.game_start_song).is_equal(1) + assert_int(ScoreHistory.from_playlist(Settings.player_array).songs.size()).is_equal(1) diff --git a/tests/integration/test_theme.gd b/tests/integration/test_theme.gd index 6a34dab..73dd506 100644 --- a/tests/integration/test_theme.gd +++ b/tests/integration/test_theme.gd @@ -36,6 +36,15 @@ const STYLES: Dictionary = { "TabButton": "Button", } +## Theme types that only scripts read from, so they are not styles a node can pick. +const CODE_ONLY_TYPES: Dictionary = { + "Palette": ["backdrop"], + "ScoreGraph": [ + "player_1", "player_2", "player_3", "player_4", "player_5", "player_6", "player_7", + "grid", "axis_text", "winning_line", + ], +} + var _theme: Theme = load(THEME_PATH) @@ -79,8 +88,11 @@ func test_theme_has_every_named_style() -> void: .is_equal(STYLES[style]) -func test_theme_has_the_palette() -> void: - assert_bool(_theme.has_color("backdrop", "Palette")).is_true() +func test_theme_has_the_colours_scripts_use() -> void: + for type: String in CODE_ONLY_TYPES: + for color: String in CODE_ONLY_TYPES[type]: + assert_bool(_theme.has_color(color, type)) \ + .override_failure_message("%s/%s is missing" % [type, color]).is_true() func test_every_style_named_in_scenes_and_scripts_exists() -> void: @@ -89,7 +101,7 @@ func test_every_style_named_in_scenes_and_scripts_exists() -> void: for path: String in _game_files(".tscn") + _game_files(".gd"): for found: RegExMatch in regex.search_all(FileAccess.get_file_as_string(path)): var style: String = found.get_string(1) - if !(style in STYLES) && style != "Palette": + if !(style in STYLES) && !(style in CODE_ONLY_TYPES): missing.append("%s: %s" % [path, style]) assert_array(missing).override_failure_message( "Styles not in the theme:\n" + "\n".join(missing)).is_empty() diff --git a/tests/integration/test_winner_popup.gd b/tests/integration/test_winner_popup.gd new file mode 100644 index 0000000..528afb2 --- /dev/null +++ b/tests/integration/test_winner_popup.gd @@ -0,0 +1,160 @@ +extends MainWindowTestSuite +## The Winner popup's graph of the game and the facts under it, built from the points +## given with the keybinds, and Reset points starting a new game. + +var _crono: PlayerObject +var _marle: PlayerObject +var _popup: WinnerPopup + + +func before_test() -> void: + super() + _crono = _add_player("Crono") + _marle = _add_player("Marle") + _popup = _quiz.get_node("WinnerPopup") + await _play_first_song() + Settings.winning_score = 3 + + +## Moves on to a new song, like Next does, without waiting for a download. Songs the +## game downloaded ahead are left unplayed. +func _next_song() -> void: + var song: SongObject = SongObject.new() + song.song_number = Playlist.song_object_array.size() + Playlist.add_to_playlist(song) + Playlist.set_currently_playing_song(song.song_number) + Playlist.song_has_played(song.song_number) + + +func _legend_texts() -> Array[String]: + var texts: Array[String] = [] + for entry: HBoxContainer in _popup.legend.get_children(): + texts.append((entry.get_child(1) as Label).text) + return texts + + +# Song 1: Crono. Song 2: nobody. Song 3: Crono and Marle. Song 4: Crono wins. +func _play_a_game() -> void: + await _give_point_with_keybind(1) + _next_song() + _next_song() + await _give_point_with_keybind(1) + await _give_point_with_keybind(2) + _next_song() + await _give_point_with_keybind(1) + + +func test_winning_shows_how_the_game_went() -> void: + await _play_a_game() + + assert_bool(_popup.visible).is_true() + var history: ScoreHistory = _popup.score_graph._history + assert_int(history.songs.size()).is_equal(4) + assert_array(history.scores_after_songs(_crono.id)).contains_exactly([0, 1, 1, 2, 3]) + assert_array(history.scores_after_songs(_marle.id)).contains_exactly([0, 0, 0, 1, 1]) + assert_array(_legend_texts()).contains_exactly(["Crono 3", "Marle 1"]) + assert_str(_popup.streak_label.text).is_equal("Crono, 2 songs in a row (3–4)") + assert_str(_popup.standstill_label.text).is_equal("Song 2, nobody scored") + assert_str(_popup.shared_label.text).is_equal("Song 3, Crono and Marle") + + +func test_graph_draws_a_line_per_player_up_to_their_score() -> void: + await _play_a_game() + var graph: ScoreGraph = _popup.score_graph + + for index: int in 2: + var points: PackedVector2Array = graph.line_points(index) + # Every line starts at song 0 with no points and ends at the last song + assert_float(points[0].x).is_less(points[points.size() - 1].x) + assert_float(points[0].y).is_greater(points[points.size() - 1].y) + # Crono's line ends higher than Marle's + var crono_end: Vector2 = graph.line_points(0)[graph.line_points(0).size() - 1] + var marle_end: Vector2 = graph.line_points(1)[graph.line_points(1).size() - 1] + assert_float(crono_end.y).is_less(marle_end.y) + assert_object(graph.player_color(0)).is_not_equal(graph.player_color(1)) + + +func test_point_taken_back_is_not_in_the_graph() -> void: + await _give_point_with_keybind(2) + # The -1 button; after Ctrl+Alt the test runner keeps Ctrl held for later keys + _player_row(_marle).minus_button.pressed.emit() + await _give_point_with_keybind(1) + await _give_point_with_keybind(1) + await _give_point_with_keybind(1) + + assert_array(_popup.score_graph._history.songs[0]).contains_exactly([_crono.id]) + + +func test_reset_points_starts_a_new_game() -> void: + await _play_a_game() + _popup.close() + + _press("QuizToolbar/ResetPointsButton") + for song: int in 3: + _next_song() + await _give_point_with_keybind(2) + + assert_bool(_popup.visible).is_true() + var history: ScoreHistory = _popup.score_graph._history + assert_int(history.songs.size()).is_equal(3) + assert_array(_legend_texts()).contains_exactly(["Crono 0", "Marle 3"]) + assert_str(_popup.streak_label.text).is_equal("Marle, 3 songs in a row (1–3)") + + +func test_reset_points_removes_the_points_from_the_songs() -> void: + await _play_a_game() + _popup.close() + + _press("QuizToolbar/ResetPointsButton") + await _runner.simulate_frames(1) + + for song: SongObject in Playlist.song_object_array: + assert_array(song.players_given_point).is_empty() + for row: SongRow in _quiz.music_list_panel._rows.get_children(): + assert_int(row.song_list_players.get_child_count()).is_equal(0) + assert_int(Playlist.game_start_song).is_equal(Playlist.currently_playing_song + 1) + + +func test_long_game_with_five_players_stays_readable() -> void: + for player_name: String in ["Lucca", "Frog", "Robo"]: + _add_player(player_name) + Settings.winning_score = 20 + var players: Array[PlayerObject] = Settings.player_array + var history: ScoreHistory = ScoreHistory.from_songs(players, UiTour.long_game(players)) + for player: PlayerObject in players: + player.player_score = history.final_score(player.id) + assert_int(players[2].player_score).is_equal(20) + + _popup.show_winner(players[2]) + _popup.show_history(history) + await _runner.simulate_frames(2) + + var graph: ScoreGraph = _popup.score_graph + assert_float(graph.size.x).override_failure_message("The graph is not full width") \ + .is_greater(1400.0) + # Song numbers along the bottom are at least a label's width apart + var songs: Array[int] = graph.labelled_songs() + assert_int(songs.front()).is_equal(1) + assert_int(songs.back()).is_equal(60) + for i: int in range(1, songs.size()): + assert_float(graph.song_x(songs[i]) - graph.song_x(songs[i - 1])).is_greater_equal(40.0) + # Scores at the end of the lines do not overlap and stay inside the graph + var font_size: int = graph.get_theme_font_size(&"font_size", ScoreGraph.THEME_TYPE) + var label_ys: Array[float] = [] + for position: Vector2 in graph.score_label_positions(): + assert_bool(Rect2(Vector2.ZERO, graph.size).has_point(position)).is_true() + label_ys.append(position.y) + label_ys.sort() + for i: int in range(1, label_ys.size()): + assert_float(label_ys[i] - label_ys[i - 1]).is_greater_equal(font_size) + # Every line stays inside the graph + for index: int in players.size(): + for point: Vector2 in graph.line_points(index): + assert_bool(Rect2(Vector2.ZERO, graph.size).has_point(point)).is_true() + # The facts fit on one line each + for label: Label in [_popup.streak_label, _popup.standstill_label, _popup.shared_label]: + assert_int(label.get_line_count()).override_failure_message(label.text + " wraps") \ + .is_equal(1) + # The whole popup fits in the window + assert_bool(Rect2(Vector2.ZERO, Vector2(1920, 1080)).encloses( + _popup.winner_panel.get_global_rect())).is_true() diff --git a/tests/integration/test_winner_popup.gd.uid b/tests/integration/test_winner_popup.gd.uid new file mode 100644 index 0000000..9e5c504 --- /dev/null +++ b/tests/integration/test_winner_popup.gd.uid @@ -0,0 +1 @@ +uid://fxnodtwhtyo6 diff --git a/tests/unit/test_playlist.gd b/tests/unit/test_playlist.gd index 60fbd70..fe6fb01 100644 --- a/tests/unit/test_playlist.gd +++ b/tests/unit/test_playlist.gd @@ -161,6 +161,26 @@ func test_remove_player_removes_only_that_players_points() -> void: assert_array(songs[1].players_given_point).contains_exactly(["crono-id"]) +func test_clear_points_empties_every_song() -> void: + var songs: Array[SongObject] = _add_songs(2) + songs[0].players_given_point.assign(["crono-id", "marle-id"]) + songs[1].players_given_point.assign(["crono-id"]) + + Playlist.clear_points() + + assert_array(songs[0].players_given_point).is_empty() + assert_array(songs[1].players_given_point).is_empty() + + +func test_reset_playlist_starts_a_new_game() -> void: + _add_songs(3) + Playlist.game_start_song = 2 + + Playlist.reset_playlist() + + assert_int(Playlist.game_start_song).is_equal(0) + + func test_reset_playlist_clears_songs_and_position() -> void: _add_songs(3) Playlist.latest_played_song = 2 diff --git a/tests/unit/test_score_history.gd b/tests/unit/test_score_history.gd new file mode 100644 index 0000000..e07f38d --- /dev/null +++ b/tests/unit/test_score_history.gd @@ -0,0 +1,164 @@ +extends GdUnitTestSuite +## ScoreHistory: the scores after each song of a game and the facts shown under the +## graph on the Winner popup. + +var _crono: PlayerObject +var _marle: PlayerObject +var _lucca: PlayerObject +var _players: Array[PlayerObject] = [] +var _snapshot: AutoloadSnapshot + + +func before_test() -> void: + _snapshot = AutoloadSnapshot.take([Playlist]) + Playlist.reset_playlist() + _crono = PlayerObject.new("Crono") + _marle = PlayerObject.new("Marle") + _lucca = PlayerObject.new("Lucca") + _players = [_crono, _marle, _lucca] + + +func after_test() -> void: + Playlist.reset_playlist() + _snapshot.restore() + + +## A history where song n (counting from 1) was scored on by the players in +## [param scorers][n - 1]. +func _history(scorers: Array) -> ScoreHistory: + var song_scorers: Array = [] + for players: Array in scorers: + song_scorers.append(players.map(func(player: PlayerObject) -> String: return player.id)) + return ScoreHistory.from_songs(_players, song_scorers) + + +func _add_songs(scorers: Array) -> void: + for players: Array in scorers: + var song: SongObject = SongObject.new() + for player: PlayerObject in players: + song.add_point(player.id) + Playlist.add_to_playlist(song) + + +func test_scores_after_each_song() -> void: + var history: ScoreHistory = _history([[_crono], [], [_crono, _marle], [_marle]]) + + assert_array(history.scores_after_songs(_crono.id)).contains_exactly([0, 1, 1, 2, 2]) + assert_array(history.scores_after_songs(_marle.id)).contains_exactly([0, 0, 0, 1, 2]) + assert_array(history.scores_after_songs(_lucca.id)).contains_exactly([0, 0, 0, 0, 0]) + assert_int(history.final_score(_crono.id)).is_equal(2) + + +func test_from_playlist_takes_the_songs_of_the_current_game() -> void: + _add_songs([[_lucca], [_crono], [_marle], [_crono], [_lucca]]) + for song_no: int in 4: + Playlist.song_object_array[song_no].has_played = true + Playlist.game_start_song = 1 + Playlist.currently_playing_song = 3 + + var history: ScoreHistory = ScoreHistory.from_playlist(_players) + + # Song 0 was the last game, song 4 is not played yet + assert_int(history.songs.size()).is_equal(3) + assert_array(history.scores_after_songs(_crono.id)).contains_exactly([0, 1, 1, 2]) + assert_int(history.final_score(_lucca.id)).is_equal(0) + + +func test_from_playlist_leaves_out_songs_not_played() -> void: + _add_songs([[_crono], [_marle], [_crono], [_marle]]) + Playlist.song_object_array[0].has_played = true + Playlist.song_object_array[2].has_played = true + Playlist.currently_playing_song = 3 + + var history: ScoreHistory = ScoreHistory.from_playlist(_players) + + assert_int(history.songs.size()).is_equal(3) + assert_array(history.scores_after_songs(_marle.id)).contains_exactly([0, 0, 0, 1]) + + +func test_from_playlist_goes_on_to_the_last_played_song() -> void: + _add_songs([[_crono], [_marle], [_crono]]) + for song: SongObject in Playlist.song_object_array: + song.has_played = true + # An older song picked in the music list + Playlist.currently_playing_song = 0 + + var history: ScoreHistory = ScoreHistory.from_playlist(_players) + + assert_int(history.songs.size()).is_equal(3) + + +func test_from_playlist_before_any_song() -> void: + _add_songs([[], []]) + + var history: ScoreHistory = ScoreHistory.from_playlist(_players) + + assert_array(history.songs).is_empty() + assert_str(history.longest_streak_text()).is_equal("No songs played") + assert_str(history.longest_standstill_text()).is_equal("No songs played") + assert_str(history.most_shared_text()).is_equal("No songs played") + + +func test_points_of_removed_players_are_left_out() -> void: + var history: ScoreHistory = ScoreHistory.from_songs([_crono], + [[_crono.id, "removed-player"], ["removed-player"]]) + + assert_array(history.songs[0]).contains_exactly([_crono.id]) + assert_array(history.songs[1]).is_empty() + + +func test_longest_streak() -> void: + var history: ScoreHistory = _history( + [[_marle], [_marle], [], [_crono], [_crono], [_crono], [_marle]]) + + assert_str(history.longest_streak_text()).is_equal("Crono, 3 songs in a row (4–6)") + + +func test_longest_streak_tie_goes_to_the_earliest() -> void: + var history: ScoreHistory = _history([[_crono], [_crono], [_marle], [_marle]]) + + assert_str(history.longest_streak_text()).is_equal("Crono, 2 songs in a row (1–2)") + history = _history([[_marle], [_marle], [_crono], [_crono]]) + assert_str(history.longest_streak_text()).is_equal("Marle, 2 songs in a row (1–2)") + + +func test_no_streak() -> void: + var history: ScoreHistory = _history([[_crono], [_marle], [_crono], []]) + + assert_str(history.longest_streak_text()).is_equal("Nobody scored twice in a row") + + +func test_longest_standstill() -> void: + var history: ScoreHistory = _history([[_crono], [], [], [_marle], [], [], [], [_crono]]) + + assert_str(history.longest_standstill_text()).is_equal("Songs 5–7, nobody scored") + + +func test_standstill_of_one_song() -> void: + var history: ScoreHistory = _history([[_crono], [], [_marle]]) + + assert_str(history.longest_standstill_text()).is_equal("Song 2, nobody scored") + + +func test_no_standstill() -> void: + var history: ScoreHistory = _history([[_crono], [_marle]]) + + assert_str(history.longest_standstill_text()).is_equal("Someone scored on every song") + + +func test_most_shared_song_names_two_players_in_player_order() -> void: + var history: ScoreHistory = _history([[_crono], [_lucca, _marle], [_crono, _lucca]]) + + assert_str(history.most_shared_text()).is_equal("Song 2, Marle and Lucca") + + +func test_most_shared_song_counts_more_than_two_players() -> void: + var history: ScoreHistory = _history([[_crono, _marle], [_crono, _marle, _lucca]]) + + assert_str(history.most_shared_text()).is_equal("Song 2, 3 players") + + +func test_no_shared_song() -> void: + var history: ScoreHistory = _history([[_crono], [_marle], []]) + + assert_str(history.most_shared_text()).is_equal("No song had two scorers") diff --git a/tests/unit/test_score_history.gd.uid b/tests/unit/test_score_history.gd.uid new file mode 100644 index 0000000..d20de2d --- /dev/null +++ b/tests/unit/test_score_history.gd.uid @@ -0,0 +1 @@ +uid://c6xby73bvflq6 diff --git a/themes/README.md b/themes/README.md index 52faaea..c9d8703 100644 --- a/themes/README.md +++ b/themes/README.md @@ -54,3 +54,13 @@ inspector or `theme_type_variation` in code: | Colour | Used for | |---|---| | `backdrop` | The dimming behind an open popup | + +**ScoreGraph**: the graph of the game on the Winner popup, drawn from code. + +| Item | Used for | +|---|---| +| `player_1` … `player_7` | Each player's line, dots, score and legend square, by their place in the player list | +| `grid`, `axis_text` | The gridlines and the numbers along the axes | +| `winning_line` | The dashed line at the winning score | +| `line_width`, `dot_radius` | How thick the lines are and how big the dots | +| `font_size` | The axis numbers and the scores at the end of the lines | diff --git a/themes/main_theme.tres b/themes/main_theme.tres index 0c454d4..d5da8db 100644 --- a/themes/main_theme.tres +++ b/themes/main_theme.tres @@ -897,6 +897,19 @@ ProgressBar/constants/outline_size = 0 ProgressBar/font_sizes/font_size = 16 ProgressBar/styles/background = SubResource("StyleBoxFlat_07kyi") ProgressBar/styles/fill = SubResource("StyleBoxFlat_ouuwv") +ScoreGraph/colors/axis_text = Color(0.75, 0.75, 0.75, 1) +ScoreGraph/colors/grid = Color(1, 1, 1, 0.1) +ScoreGraph/colors/player_1 = Color(0.25, 0.56, 0.95, 1) +ScoreGraph/colors/player_2 = Color(0.11, 0.69, 0.48, 1) +ScoreGraph/colors/player_3 = Color(0.93, 0.76, 0.2, 1) +ScoreGraph/colors/player_4 = Color(0.91, 0.48, 0.64, 1) +ScoreGraph/colors/player_5 = Color(0.62, 0.56, 0.95, 1) +ScoreGraph/colors/player_6 = Color(0.9, 0.4, 0.4, 1) +ScoreGraph/colors/player_7 = Color(0.55, 0.8, 0.3, 1) +ScoreGraph/colors/winning_line = Color(1, 0.5, 0, 1) +ScoreGraph/constants/dot_radius = 5 +ScoreGraph/constants/line_width = 3 +ScoreGraph/font_sizes/font_size = 14 ScrollContainer/colors/scroll_hint_horizontal_color = Color(0, 0, 0, 1) ScrollContainer/colors/scroll_hint_vertical_color = Color(0, 0, 0, 1) ScrollContainer/styles/focus = SubResource("StyleBoxFlat_35dwi")