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