Files
MusicPlayer/popups/score_graph.gd
T
Sansan e1e05f3904
Build / test (push) Successful in 38s
Build / build (push) Successful in 1m12s
Show a graph of the game on the Winner popup
The Winner popup now shows every player's score after each song as a
step line, with the winning score marked, and three facts under it: the
longest streak, the longest standstill and the song most players scored
on. The data comes from the points each song already records, through
the new ScoreHistory. The popup grows to 1600x920 with the graph across
the full width, so a 60-song game with five players stays readable.

Reset points now starts a new game: it clears the points on the songs
and the graph starts after the song that was playing. Songs already on
the server at startup count as an earlier game.

The screenshot tour has a long five-player game, and only the Winner
popup differs from before.
2026-10-07 10:40:46 +02:00

159 lines
6.4 KiB
GDScript

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