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.
159 lines
6.4 KiB
GDScript
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
|