Files
MusicPlayer/popups/score_graph.gd
T
SansanandClaude Opus 5.5 94e8dfb602
Build / test (push) Successful in 1m31s
Build / build (push) Successful in 1m13s
Add a theme picker and an Original web theme (#42)
Settings has a Theme row listing Settings.THEMES: Classic (main_theme.tres,
still the default) and Original web (original_web_theme.tres), which looks like
MusicFrontend: orange-bordered buttons, a #333 header and popups, #424242
panels, #4e4e4e alternating rows, a yellow playing song and a light grey
player bar. Picking one sets the root window's theme at once and saves it as
theme_id in settings.cfg, and it is applied at start.

The popup backdrop and score graph follow a theme switch. The top bar has its
own TopBarBackground style. test_theme.gd checks that every theme has
everything main_theme.tres has. The screenshot tool takes a theme id.

Closes #42

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:50:12 +02:00

167 lines
6.7 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
func _notification(what: int) -> void:
# The colours come from the theme, which can be switched in Settings
if what == NOTIFICATION_THEME_CHANGED:
queue_redraw()
## Shows [param history]. The y axis goes up to [param winning_score], or higher if
## someone has more points. A winning score of 0 draws no winning line.
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)
if _winning_score <= 0:
return
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