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>
167 lines
6.7 KiB
GDScript
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
|