Add animations to popups, points, songs, the winner and the tabs (#48)
Popups fade in while growing and close the reverse way, with the backdrop fading along. The winner gets confetti, bouncing pictures and a score graph that draws itself from the left. Filtered statistics cards and search rows fade in, the tabs slide in from their side, and every button bounces on hover and click (ButtonBounce, now added to all buttons). In the player list the score pops on every point, the first point flashes the row, and a player on match point glows with a "Match point!" tag. A song starting fades in the title, flashes its row (kept across list rebuilds) and pops the play button. Common/anim.gd holds the helpers. A saved "Reduce animations" switch in Settings turns all movement off; the tests use it. The flash colours are theme colours (Palette first_point, match_point, flash) in every theme. Closes #48 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ae231975bd
commit
0fc3be79e1
31 files changed
+1510
-639
No files matched your search
+140
@@ -0,0 +1,140 @@
|
||||
class_name Anim
|
||||
extends RefCounted
|
||||
## Short animations made with tweens, used all over the game: popping, fading, flashing
|
||||
## and pulsing.
|
||||
##
|
||||
## Every animation is short so a quiz never waits for one. With
|
||||
## [member Settings.reduce_animations] on, nothing moves: each helper jumps straight to
|
||||
## where the animation would end.
|
||||
|
||||
## How long quick feedback, such as a button press, takes.
|
||||
const FAST: float = 0.12
|
||||
## How long most animations take.
|
||||
const NORMAL: float = 0.2
|
||||
## How long flashes and bigger moments take.
|
||||
const SLOW: float = 0.45
|
||||
|
||||
|
||||
## Whether animations are on.
|
||||
static func enabled() -> bool:
|
||||
return !Settings.reduce_animations
|
||||
|
||||
|
||||
## A new tween on [param node] for the animation called [param key], stopping an earlier
|
||||
## one with the same key so they don't fight. Returns null when animations are off or the
|
||||
## node is not in the tree.
|
||||
static func tween(node: Node, key: StringName = &"anim") -> Tween:
|
||||
var meta: StringName = StringName("_tween_" + key)
|
||||
var old: Variant = node.get_meta(meta) if node.has_meta(meta) else null
|
||||
if old is Tween && (old as Tween).is_valid():
|
||||
(old as Tween).kill()
|
||||
if !enabled() || !node.is_inside_tree():
|
||||
return null
|
||||
var new_tween: Tween = node.create_tween()
|
||||
node.set_meta(meta, new_tween)
|
||||
return new_tween
|
||||
|
||||
|
||||
## Makes [param control] grow from its middle.
|
||||
static func center_pivot(control: Control) -> void:
|
||||
control.pivot_offset = control.size / 2
|
||||
|
||||
|
||||
## Makes [param control] jump to [param peak] times its size and spring back.
|
||||
static func pop(control: Control, peak: float = 1.3, duration: float = NORMAL) -> void:
|
||||
var pop_tween: Tween = tween(control, &"pop")
|
||||
control.scale = Vector2.ONE
|
||||
if pop_tween == null:
|
||||
return
|
||||
center_pivot(control)
|
||||
control.scale = Vector2.ONE * peak
|
||||
pop_tween.tween_property(control, "scale", Vector2.ONE, duration) \
|
||||
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
|
||||
|
||||
## Makes [param control] grow from [param from] times its size with a bounce.
|
||||
static func bounce_in(control: Control, from: float = 0.4, duration: float = SLOW) -> void:
|
||||
var bounce_tween: Tween = tween(control, &"pop")
|
||||
control.scale = Vector2.ONE
|
||||
if bounce_tween == null:
|
||||
return
|
||||
center_pivot(control)
|
||||
control.scale = Vector2.ONE * from
|
||||
bounce_tween.tween_property(control, "scale", Vector2.ONE, duration * 1.6) \
|
||||
.set_trans(Tween.TRANS_ELASTIC).set_ease(Tween.EASE_OUT)
|
||||
|
||||
|
||||
## Fades [param item] in from invisible.
|
||||
static func fade_in(item: CanvasItem, duration: float = NORMAL) -> void:
|
||||
var fade_tween: Tween = tween(item, &"fade")
|
||||
item.modulate.a = 1.0
|
||||
if fade_tween == null:
|
||||
return
|
||||
item.modulate.a = 0.0
|
||||
fade_tween.tween_property(item, "modulate:a", 1.0, duration)
|
||||
|
||||
|
||||
## Slides [param control] in from [param offset] away from where it is, fading in.
|
||||
static func slide_in(control: Control, offset: Vector2, duration: float = NORMAL) -> void:
|
||||
var slide_tween: Tween = tween(control, &"slide")
|
||||
if control.has_meta(&"_slide_home"):
|
||||
control.position = control.get_meta(&"_slide_home")
|
||||
control.modulate.a = 1.0
|
||||
if slide_tween == null:
|
||||
return
|
||||
var home: Vector2 = control.position
|
||||
control.set_meta(&"_slide_home", home)
|
||||
control.position = home + offset
|
||||
control.modulate.a = 0.0
|
||||
slide_tween.set_parallel()
|
||||
slide_tween.tween_property(control, "position", home, duration) \
|
||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
slide_tween.tween_property(control, "modulate:a", 1.0, duration)
|
||||
slide_tween.chain().tween_callback(func() -> void: control.remove_meta(&"_slide_home"))
|
||||
|
||||
|
||||
## Scales [param control] to [param target] times its size, e.g. a little bigger under
|
||||
## the mouse.
|
||||
static func scale_to(control: Control, target: float, duration: float = FAST) -> void:
|
||||
var scale_tween: Tween = tween(control, &"scale")
|
||||
if scale_tween == null:
|
||||
control.scale = Vector2.ONE
|
||||
return
|
||||
center_pivot(control)
|
||||
scale_tween.tween_property(control, "scale", Vector2.ONE * target, duration) \
|
||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
|
||||
|
||||
## Lights up [param container] with [param colour] for a moment, e.g. a row that just
|
||||
## changed. A coloured layer is laid over the container and fades away.
|
||||
static func flash(container: Container, colour: Color, duration: float = SLOW) -> void:
|
||||
if !enabled() || !container.is_inside_tree():
|
||||
return
|
||||
var layer: ColorRect = _layer(container, colour)
|
||||
var flash_tween: Tween = layer.create_tween()
|
||||
flash_tween.tween_property(layer, "color:a", 0.0, duration) \
|
||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
||||
flash_tween.tween_callback(layer.queue_free)
|
||||
|
||||
|
||||
## Keeps [param container] glowing in [param colour], softly pulsing, until the returned
|
||||
## layer is freed. With animations off the glow stays still.
|
||||
static func pulse(container: Container, colour: Color) -> ColorRect:
|
||||
var layer: ColorRect = _layer(container, colour)
|
||||
layer.name = "Pulse"
|
||||
if enabled():
|
||||
var pulse_tween: Tween = layer.create_tween().set_loops()
|
||||
pulse_tween.tween_property(layer, "color:a", colour.a * 0.3, 0.6) \
|
||||
.set_trans(Tween.TRANS_SINE)
|
||||
pulse_tween.tween_property(layer, "color:a", colour.a, 0.6) \
|
||||
.set_trans(Tween.TRANS_SINE)
|
||||
return layer
|
||||
|
||||
|
||||
# A layer of [param colour] over the whole of [param container] that lets clicks through
|
||||
static func _layer(container: Container, colour: Color) -> ColorRect:
|
||||
var layer: ColorRect = ColorRect.new()
|
||||
layer.color = colour
|
||||
layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
container.add_child(layer)
|
||||
return layer
|
||||
@@ -0,0 +1 @@
|
||||
uid://67is3ejxq0j8
|
||||
+29
-1
@@ -4,6 +4,7 @@ extends RefCounted
|
||||
## shrinks while it is held down, and can pop when its icon changes.
|
||||
##
|
||||
## The button is scaled around its middle, so its place in a container does not change.
|
||||
## With [member Settings.reduce_animations] on, buttons keep their size.
|
||||
|
||||
## How big a button is under the mouse.
|
||||
const HOVER_SCALE: Vector2 = Vector2(1.08, 1.08)
|
||||
@@ -18,10 +19,32 @@ const POP_DURATION: float = 0.18
|
||||
|
||||
const _TWEEN_META: StringName = &"button_bounce_tween"
|
||||
const _PIVOT_META: StringName = &"button_bounce_pivot"
|
||||
const _ADDED_META: StringName = &"button_bounce_added"
|
||||
|
||||
|
||||
## Makes [param button] grow under the mouse and shrink while held down.
|
||||
## Gives every button in [param tree] the bounce, also buttons added later, e.g. rows
|
||||
## of the player list. Check buttons are left out: they span a whole settings row.
|
||||
static func add_to_every_button(tree: SceneTree) -> void:
|
||||
if !tree.node_added.is_connected(_on_node_added):
|
||||
tree.node_added.connect(_on_node_added)
|
||||
var nodes: Array[Node] = [tree.root]
|
||||
while !nodes.is_empty():
|
||||
var node: Node = nodes.pop_back()
|
||||
_on_node_added(node)
|
||||
nodes.append_array(node.get_children())
|
||||
|
||||
|
||||
static func _on_node_added(node: Node) -> void:
|
||||
if node is BaseButton && !(node is CheckButton) && !(node is CheckBox):
|
||||
add_to(node)
|
||||
|
||||
|
||||
## Makes [param button] grow under the mouse and shrink while held down. Adding it to
|
||||
## the same button twice does nothing.
|
||||
static func add_to(button: BaseButton) -> void:
|
||||
if button.has_meta(_ADDED_META):
|
||||
return
|
||||
button.set_meta(_ADDED_META, true)
|
||||
_keep_pivot_centred(button)
|
||||
button.mouse_entered.connect(func() -> void:
|
||||
if !button.disabled:
|
||||
@@ -38,6 +61,8 @@ static func resting_scale(button: BaseButton) -> Vector2:
|
||||
|
||||
## Shrinks [param button] and lets it spring back, e.g. when its icon changes.
|
||||
static func pop(button: BaseButton) -> void:
|
||||
if !Anim.enabled():
|
||||
return
|
||||
_keep_pivot_centred(button)
|
||||
button.scale = POP_SCALE
|
||||
var tween: Tween = _new_tween(button)
|
||||
@@ -46,6 +71,9 @@ static func pop(button: BaseButton) -> void:
|
||||
|
||||
|
||||
static func _scale_to(button: BaseButton, target: Vector2) -> void:
|
||||
if !Anim.enabled():
|
||||
button.scale = Vector2.ONE
|
||||
return
|
||||
var tween: Tween = _new_tween(button)
|
||||
tween.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
tween.tween_property(button, ^"scale", target, DURATION)
|
||||
|
||||
+3
-1
@@ -29,7 +29,7 @@ const SAVED_SETTINGS: Dictionary[String, Array] = {
|
||||
&"inspiration_list_speed",
|
||||
],
|
||||
"window": [&"fullscreen"],
|
||||
"app": [&"last_seen_version", &"theme_id"],
|
||||
"app": [&"last_seen_version", &"theme_id", &"reduce_animations"],
|
||||
}
|
||||
## The looks the game can have, picked in Settings: an id that is saved, the name shown
|
||||
## and the theme file. The first is the default. See themes/README.md.
|
||||
@@ -96,6 +96,8 @@ var queue_ids: Dictionary[String, String] = {}
|
||||
## The version the game last showed the About popup for. When it is not [member version],
|
||||
## this version is new and the main window opens the About popup to show what is new.
|
||||
var last_seen_version: String = ""
|
||||
## Turns every animation off, so nothing moves. See [Anim].
|
||||
var reduce_animations: bool = false
|
||||
## The id of the theme in use, one of [constant THEMES].
|
||||
var theme_id: String = THEMES[0].id
|
||||
## The folder songs are played from instead of the server, picked in the settings, or null
|
||||
|
||||
@@ -5,6 +5,9 @@ extends Control
|
||||
##
|
||||
## Everything that is not part of a quiz lives here.
|
||||
|
||||
## How far a tab slides in from the side when it is chosen.
|
||||
const TAB_SLIDE: float = 60.0
|
||||
|
||||
## Dims the window behind an open popup and keeps clicks from reaching the buttons under it.
|
||||
var popup_backdrop: PopupBackdrop
|
||||
|
||||
@@ -23,6 +26,8 @@ var popup_backdrop: PopupBackdrop
|
||||
|
||||
func _ready() -> void:
|
||||
popup_backdrop = PopupBackdrop.add_to(self, _popups())
|
||||
# Every button grows a little under the mouse and shrinks when clicked
|
||||
ButtonBounce.add_to_every_button(get_tree())
|
||||
if OS.has_feature("macos"):
|
||||
GameLog.debug("is_mac")
|
||||
quiz.search_pressed.connect(show_search)
|
||||
@@ -114,8 +119,14 @@ func show_qr() -> void:
|
||||
func show_tab(tab: StringName) -> void:
|
||||
GameLog.debug("show_tab ", tab)
|
||||
top_bar.select_tab(tab)
|
||||
var was_statistics: bool = statistics_view.visible
|
||||
statistics_view.visible = tab == &"statistics"
|
||||
quiz.visible = !statistics_view.visible
|
||||
# The tab that comes in slides from its side: statistics from the right
|
||||
if statistics_view.visible && !was_statistics:
|
||||
Anim.slide_in(statistics_view, Vector2(TAB_SLIDE, 0))
|
||||
elif quiz.visible && was_statistics:
|
||||
Anim.slide_in(quiz, Vector2(-TAB_SLIDE, 0))
|
||||
if statistics_view.visible:
|
||||
statistics_view.refresh()
|
||||
|
||||
|
||||
@@ -218,6 +218,8 @@ func play_song_object(song_object_no: int) -> void:
|
||||
audio_player.play()
|
||||
song_finished = false
|
||||
_show_playing(true)
|
||||
# A new song makes the play button pop, also when it was already playing
|
||||
ButtonBounce.pop(play_button)
|
||||
stream = audio_player.stream
|
||||
progress_slider.value = 0
|
||||
progress_slider.max_value = round(stream.get_length())
|
||||
|
||||
@@ -164,6 +164,13 @@ layout_mode = 2
|
||||
focus_mode = 0
|
||||
text = "Fullscreen"
|
||||
|
||||
[node name="ReduceAnimationsButton" type="CheckButton" parent="SettingsPanel/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "Reduce animations"
|
||||
|
||||
[node name="CacheRow" type="HBoxContainer" parent="SettingsPanel/VBoxContainer" unique_id=658210492]
|
||||
layout_mode = 2
|
||||
|
||||
|
||||
+51
-4
@@ -4,32 +4,79 @@ extends Control
|
||||
##
|
||||
## Clicking outside [member panel] closes it. Popups that need to prepare or clean up
|
||||
## override [method open] or [method close] and call [code]super()[/code].
|
||||
##
|
||||
## The panel fades in while growing from [constant START_SCALE] of its size, and closing
|
||||
## does the reverse; the popup is hidden once that has finished. See [Anim].
|
||||
|
||||
## Emitted when the popup opens, also when it is opened again while closing.
|
||||
signal opening
|
||||
## Emitted when the popup starts closing, before it is hidden, so the backdrop can fade
|
||||
## out with it.
|
||||
signal closing
|
||||
|
||||
## How big the panel starts when it opens, and ends when it closes.
|
||||
const START_SCALE: float = 0.95
|
||||
|
||||
## The visible part of the popup. Clicks outside it close the popup.
|
||||
|
||||
@export var panel: Control
|
||||
|
||||
## True while the popup is fading out after [method close].
|
||||
var is_closing: bool = false
|
||||
|
||||
|
||||
## Shows the popup.
|
||||
func open() -> void:
|
||||
is_closing = false
|
||||
visible = true
|
||||
opening.emit()
|
||||
var open_tween: Tween = Anim.tween(panel, &"popup")
|
||||
panel.modulate.a = 1.0
|
||||
panel.scale = Vector2.ONE
|
||||
if open_tween == null:
|
||||
return
|
||||
Anim.center_pivot(panel)
|
||||
panel.modulate.a = 0.0
|
||||
panel.scale = Vector2.ONE * START_SCALE
|
||||
open_tween.set_parallel()
|
||||
open_tween.tween_property(panel, "modulate:a", 1.0, Anim.NORMAL)
|
||||
open_tween.tween_property(panel, "scale", Vector2.ONE, Anim.NORMAL) \
|
||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
|
||||
|
||||
## Hides the popup.
|
||||
func close() -> void:
|
||||
visible = false
|
||||
if !visible || is_closing:
|
||||
return
|
||||
is_closing = true
|
||||
closing.emit()
|
||||
var close_tween: Tween = Anim.tween(panel, &"popup")
|
||||
if close_tween == null:
|
||||
_hide()
|
||||
return
|
||||
Anim.center_pivot(panel)
|
||||
close_tween.set_parallel()
|
||||
close_tween.tween_property(panel, "modulate:a", 0.0, Anim.FAST)
|
||||
close_tween.tween_property(panel, "scale", Vector2.ONE * START_SCALE, Anim.FAST)
|
||||
close_tween.chain().tween_callback(_hide)
|
||||
|
||||
|
||||
## Opens the popup if it is closed and closes it if it is open.
|
||||
func toggle() -> void:
|
||||
if visible:
|
||||
if visible && !is_closing:
|
||||
close()
|
||||
else:
|
||||
open()
|
||||
|
||||
|
||||
func _hide() -> void:
|
||||
is_closing = false
|
||||
visible = false
|
||||
panel.modulate.a = 1.0
|
||||
panel.scale = Vector2.ONE
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if !visible || !(event is InputEventMouseButton) || !event.pressed:
|
||||
if !visible || is_closing || !(event is InputEventMouseButton) || !event.pressed:
|
||||
return
|
||||
var click: InputEventMouseButton = panel.make_input_local(event)
|
||||
if !Rect2(Vector2.ZERO, panel.size).has_point(click.position):
|
||||
|
||||
@@ -23,6 +23,8 @@ static func add_to(parent: Control, popups: Array[BasePopup]) -> PopupBackdrop:
|
||||
for popup: BasePopup in popups:
|
||||
parent.move_child(popup, -1)
|
||||
popup.visibility_changed.connect(backdrop._update)
|
||||
popup.closing.connect(backdrop._on_popup_closing)
|
||||
popup.opening.connect(backdrop._on_popup_opening)
|
||||
backdrop._update()
|
||||
return backdrop
|
||||
|
||||
@@ -34,4 +36,25 @@ func _notification(what: int) -> void:
|
||||
|
||||
|
||||
func _update() -> void:
|
||||
var was_visible: bool = visible
|
||||
visible = _popups.any(func(popup: BasePopup) -> bool: return popup.visible)
|
||||
if visible && !was_visible:
|
||||
Anim.fade_in(self)
|
||||
|
||||
|
||||
# A popup opened again while closing stops the backdrop fading out
|
||||
func _on_popup_opening() -> void:
|
||||
var fade_tween: Tween = Anim.tween(self, &"fade")
|
||||
if fade_tween == null || modulate.a >= 1.0:
|
||||
modulate.a = 1.0
|
||||
return
|
||||
fade_tween.tween_property(self, "modulate:a", 1.0, Anim.FAST)
|
||||
|
||||
|
||||
# Fades out with the last open popup, which hides the backdrop when it has closed
|
||||
func _on_popup_closing() -> void:
|
||||
if _popups.any(func(popup: BasePopup) -> bool: return popup.visible && !popup.is_closing):
|
||||
return
|
||||
var fade_tween: Tween = Anim.tween(self, &"fade")
|
||||
if fade_tween != null:
|
||||
fade_tween.tween_property(self, "modulate:a", 0.0, Anim.FAST)
|
||||
+43
-3
@@ -15,6 +15,13 @@ const MARGIN_BOTTOM: float = 44.0
|
||||
## About how many song numbers fit along the bottom.
|
||||
const MAX_SONG_LABELS: int = 20
|
||||
|
||||
## How much of the lines is drawn, from 0 (none) to 1 (all), so they can be drawn from
|
||||
## left to right.
|
||||
var progress: float = 1.0:
|
||||
set(value):
|
||||
progress = value
|
||||
queue_redraw()
|
||||
|
||||
var _history: ScoreHistory
|
||||
var _winning_score: int = 0
|
||||
|
||||
@@ -33,6 +40,32 @@ func show_history(history: ScoreHistory, winning_score: int) -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
## Draws the lines from left to right over [param duration] seconds.
|
||||
func draw_lines_in(duration: float = 1.2) -> void:
|
||||
var draw_tween: Tween = Anim.tween(self, &"draw")
|
||||
progress = 1.0
|
||||
if draw_tween == null:
|
||||
return
|
||||
progress = 0.0
|
||||
draw_tween.tween_property(self, "progress", 1.0, duration) \
|
||||
.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
|
||||
|
||||
|
||||
## The part of [param points] left of [param limit_x], cut where the line crosses it.
|
||||
static func clip_line(points: PackedVector2Array, limit_x: float) -> PackedVector2Array:
|
||||
var clipped: PackedVector2Array = []
|
||||
for i: int in points.size():
|
||||
if points[i].x <= limit_x:
|
||||
clipped.append(points[i])
|
||||
continue
|
||||
if i > 0 && points[i - 1].x < limit_x:
|
||||
var before: Vector2 = points[i - 1]
|
||||
var weight: float = (limit_x - before.x) / (points[i].x - before.x)
|
||||
clipped.append(before.lerp(points[i], weight))
|
||||
break
|
||||
return clipped
|
||||
|
||||
|
||||
## 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)
|
||||
@@ -63,13 +96,20 @@ func _draw() -> void:
|
||||
|
||||
var line_width: int = get_theme_constant(&"line_width", THEME_TYPE)
|
||||
var dot_radius: int = get_theme_constant(&"dot_radius", THEME_TYPE)
|
||||
var limit_x: float = lerpf(MARGIN_LEFT, size.x - MARGIN_RIGHT, progress)
|
||||
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)
|
||||
var points: PackedVector2Array = clip_line(line_points(index), limit_x)
|
||||
if points.size() >= 2:
|
||||
draw_polyline(points, 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 dot: Vector2 = _point(song, scores[song], index)
|
||||
if scores[song] > scores[song - 1] && dot.x <= limit_x:
|
||||
draw_circle(dot, dot_radius, color)
|
||||
# The final scores are written once the lines have reached them
|
||||
if progress < 1.0:
|
||||
return
|
||||
var label_positions: Array[Vector2] = score_label_positions()
|
||||
for index: int in _history.players.size():
|
||||
draw_string(font, label_positions[index],
|
||||
|
||||
@@ -54,6 +54,9 @@ func search() -> void:
|
||||
var row: PanelContainer = _rows[shown[position]]
|
||||
if row.get_index() != position:
|
||||
search_results.move_child(row, position)
|
||||
# Rows that come back with the search fade in
|
||||
if !row.visible:
|
||||
Anim.fade_in(row, Anim.FAST)
|
||||
row.visible = true
|
||||
row.theme_type_variation = &"ListRowOdd" if position % 2 != 0 else &"ListRowEven"
|
||||
_show_name(row, soundtracks[shown[position]], search_text)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class_name SettingsPopup
|
||||
extends BasePopup
|
||||
## The application settings: which MusicServer or music folder to play from, the theme,
|
||||
## fullscreen, how many songs to download ahead, whether to prefer less played soundtracks
|
||||
## fullscreen, fewer animations, how many songs to download ahead, whether to prefer less played soundtracks
|
||||
## and whether plays count in the statistics, and a button that opens the folder the
|
||||
## settings are kept in. Opened from the Settings button in the top bar.
|
||||
##
|
||||
@@ -25,6 +25,7 @@ const PADDING: Vector2 = Vector2(20, 12)
|
||||
@onready var settings_panel: Panel = %SettingsPanel
|
||||
@onready var select_server_button: OptionButton = %SelectServerButton
|
||||
@onready var fullscreen_button: CheckButton = %FullscreenButton
|
||||
@onready var reduce_animations_button: CheckButton = %ReduceAnimationsButton
|
||||
@onready var theme_button: OptionButton = %ThemeButton
|
||||
@onready var cache_label: Label = %CacheLabel
|
||||
@onready var lower_cache_button: Button = %LowerCacheButton
|
||||
@@ -66,6 +67,8 @@ func _ready() -> void:
|
||||
cache_label.text = str(Playlist.number_of_tracks_to_preload)
|
||||
|
||||
fullscreen_button.pressed.connect(fullscreen)
|
||||
reduce_animations_button.button_pressed = Settings.reduce_animations
|
||||
reduce_animations_button.toggled.connect(set_reduce_animations)
|
||||
choose_folder_button.pressed.connect(folder_dialog.popup_centered)
|
||||
folder_dialog.dir_selected.connect(use_music_folder)
|
||||
use_server_button.pressed.connect(use_server)
|
||||
@@ -93,6 +96,12 @@ func select_theme(index: int) -> void:
|
||||
Settings.apply_theme(Settings.THEMES[index].id)
|
||||
|
||||
|
||||
## Turns every animation off or on, and saves it.
|
||||
func set_reduce_animations(on: bool) -> void:
|
||||
Settings.reduce_animations = on
|
||||
Settings.save_settings()
|
||||
|
||||
|
||||
func fullscreen() -> void:
|
||||
Settings.set_fullscreen(!Settings.fullscreen)
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ func show_section_if_has_data(data: Variant, vbox: VBoxContainer, separator: VSe
|
||||
|
||||
func start_sync() -> void:
|
||||
GameLog.debug("start_sync")
|
||||
visible = true
|
||||
open()
|
||||
if has_synced:
|
||||
# Already synced, just show the results
|
||||
show_sync_result()
|
||||
|
||||
+52
-1
@@ -2,7 +2,12 @@ class_name WinnerPopup
|
||||
extends BasePopup
|
||||
## Shows who won, with their character, where the other players placed, and how the game
|
||||
## went: a graph of everyone's score after each song and a few facts about it.
|
||||
##
|
||||
## The winners' pictures bounce in, confetti falls over the panel and the graph draws its
|
||||
## lines from left to right. See [Anim].
|
||||
|
||||
## How many pieces of confetti fly when someone wins.
|
||||
const CONFETTI_AMOUNT: int = 160
|
||||
## Size of the coloured square in front of each name in the legend.
|
||||
const LEGEND_SQUARE_SIZE: Vector2 = Vector2(14, 14)
|
||||
|
||||
@@ -29,7 +34,7 @@ func show_winner(winner: PlayerObject) -> void:
|
||||
## Shows [param winners], who share the top score, with everyone else's placing below.
|
||||
## Tied players get the same placing. With no players, it says the quiz is over.
|
||||
func show_winners(winners: Array[PlayerObject]) -> void:
|
||||
visible = true
|
||||
open()
|
||||
winner_label.text = winners_text(winners)
|
||||
_show_pictures(winners)
|
||||
|
||||
@@ -49,6 +54,7 @@ func show_winners(winners: Array[PlayerObject]) -> void:
|
||||
players_list.add_child(label)
|
||||
|
||||
show_history(ScoreHistory.from_playlist(Settings.player_array))
|
||||
_celebrate()
|
||||
|
||||
|
||||
## The headline for [param winners], e.g. "Crono won with 5 points!!" or "Crono and
|
||||
@@ -80,6 +86,51 @@ static func placing(player: PlayerObject) -> int:
|
||||
return place
|
||||
|
||||
|
||||
# The winners' pictures bounce in, confetti flies and the graph draws itself
|
||||
func _celebrate() -> void:
|
||||
for picture: Node in winner_picture.get_parent().get_children():
|
||||
if picture is TextureRect && (picture as TextureRect).visible:
|
||||
Anim.bounce_in(picture)
|
||||
score_graph.draw_lines_in()
|
||||
if Anim.enabled():
|
||||
_throw_confetti()
|
||||
|
||||
|
||||
# A burst of confetti from the top of the panel, in the players' graph colours
|
||||
func _throw_confetti() -> void:
|
||||
var old: Node = winner_panel.get_node_or_null("Confetti")
|
||||
if old != null:
|
||||
old.free()
|
||||
var confetti: CPUParticles2D = CPUParticles2D.new()
|
||||
confetti.name = "Confetti"
|
||||
confetti.position = Vector2(winner_panel.size.x / 2, 0)
|
||||
confetti.emission_shape = CPUParticles2D.EMISSION_SHAPE_RECTANGLE
|
||||
confetti.emission_rect_extents = Vector2(winner_panel.size.x / 2, 4)
|
||||
confetti.amount = CONFETTI_AMOUNT
|
||||
confetti.lifetime = 2.6
|
||||
confetti.one_shot = true
|
||||
confetti.explosiveness = 0.85
|
||||
confetti.direction = Vector2.DOWN
|
||||
confetti.spread = 35.0
|
||||
confetti.gravity = Vector2(0, 260)
|
||||
confetti.initial_velocity_min = 80.0
|
||||
confetti.initial_velocity_max = 260.0
|
||||
confetti.angular_velocity_min = -360.0
|
||||
confetti.angular_velocity_max = 360.0
|
||||
confetti.scale_amount_min = 5.0
|
||||
confetti.scale_amount_max = 9.0
|
||||
var colours: Gradient = Gradient.new()
|
||||
colours.interpolation_mode = Gradient.GRADIENT_INTERPOLATE_CONSTANT
|
||||
colours.offsets = PackedFloat32Array()
|
||||
colours.colors = PackedColorArray()
|
||||
for i: int in 7:
|
||||
colours.add_point(i / 7.0, score_graph.player_color(i))
|
||||
confetti.color_initial_ramp = colours
|
||||
confetti.finished.connect(confetti.queue_free)
|
||||
winner_panel.add_child(confetti)
|
||||
confetti.emitting = true
|
||||
|
||||
|
||||
# One picture per winner, next to each other
|
||||
func _show_pictures(winners: Array[PlayerObject]) -> void:
|
||||
var header: Node = winner_picture.get_parent()
|
||||
|
||||
@@ -41,6 +41,14 @@ vertical_alignment = 1
|
||||
clip_text = true
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="MatchPointTag" type="Label" parent="HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"HighlightMedium"
|
||||
text = "Match point!"
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="Score" type="Label" parent="HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(40, 0)
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
class_name MusicListPanel
|
||||
extends PanelContainer
|
||||
## The list of songs in the playlist, one row per song.
|
||||
## The list of songs in the playlist, one row per song. A song's row flashes when the
|
||||
## song starts playing and when it is answered.
|
||||
|
||||
## Emitted when the row of song number [param song_no] is clicked.
|
||||
|
||||
signal song_clicked(song_no: int)
|
||||
|
||||
var _song_list_object: PackedScene = preload("res://quiz/SongRow.tscn")
|
||||
# Whether each song was playing and answered when the list was last shown, so a row
|
||||
# can flash when its song starts or is answered
|
||||
var _last_state: Dictionary = {}
|
||||
# When each flash ends, in msec, so a flash goes on when the list is shown again
|
||||
var _flash_ends: Dictionary = {}
|
||||
|
||||
@onready var _rows: VBoxContainer = %MusicList
|
||||
|
||||
@@ -15,6 +21,7 @@ var _song_list_object: PackedScene = preload("res://quiz/SongRow.tscn")
|
||||
func refresh() -> void:
|
||||
Settings.delete_children(_rows)
|
||||
var songs: Array[SongObject] = Playlist.get_playlist()
|
||||
var state: Dictionary = {}
|
||||
for i: int in songs.size():
|
||||
var s: SongObject = songs[i]
|
||||
var new_song_list_object: SongRow = _song_list_object.instantiate()
|
||||
@@ -22,6 +29,17 @@ func refresh() -> void:
|
||||
new_song_list_object.index = i
|
||||
new_song_list_object.gui_input.connect(_on_row_input.bind(s.song_number))
|
||||
_rows.add_child(new_song_list_object)
|
||||
state[s] = [s.is_playing, s.is_answered]
|
||||
# Only songs that were listed before flash, not a whole list loaded at once
|
||||
if _last_state.has(s) && _last_state[s] != state[s] \
|
||||
&& (s.is_playing || s.is_answered):
|
||||
_flash_ends[s] = Time.get_ticks_msec() + int(Anim.SLOW * 1000)
|
||||
var left: float = (_flash_ends.get(s, 0) - Time.get_ticks_msec()) / 1000.0
|
||||
if left > 0:
|
||||
Anim.flash(new_song_list_object, get_theme_color(&"flash", &"Palette"), left)
|
||||
else:
|
||||
_flash_ends.erase(s)
|
||||
_last_state = state
|
||||
|
||||
|
||||
func _on_row_input(event: InputEvent, song_no: int) -> void:
|
||||
|
||||
+7
-1
@@ -1,10 +1,16 @@
|
||||
class_name NowPlaying
|
||||
extends VBoxContainer
|
||||
## The soundtrack and song that are playing, in large text at the top of the quiz.
|
||||
## The soundtrack and song that are playing, in large text at the top of the quiz. They
|
||||
## fade in when they change.
|
||||
|
||||
|
||||
## Shows [param soundtrack_title] and [param song_title]. The playlist hides them as
|
||||
## question marks until the answer is shown.
|
||||
func show_song(soundtrack_title: String, song_title: String) -> void:
|
||||
var changed: bool = $SoundtrackLabel.text != soundtrack_title \
|
||||
|| $SongLabel.text != song_title
|
||||
$SoundtrackLabel.text = soundtrack_title
|
||||
$SongLabel.text = song_title
|
||||
# A new song, or the answer being shown, fades in
|
||||
if changed:
|
||||
Anim.fade_in(self, Anim.SLOW)
|
||||
@@ -2,6 +2,9 @@ class_name PlayerRow
|
||||
extends PanelContainer
|
||||
## One player in the player list: their name, points and character, and buttons for
|
||||
## giving and taking points. Clicking the character picks another one.
|
||||
##
|
||||
## The score pops when it changes. The first point of a game makes the row flash, and a
|
||||
## player one point from winning glows and says "Match point!" until that changes.
|
||||
|
||||
signal change_character_clicked
|
||||
signal player_removed
|
||||
@@ -15,6 +18,7 @@ var list_index: int
|
||||
|
||||
@onready var player_name_field: Label = %Name
|
||||
@onready var score_label: Label = %Score
|
||||
@onready var match_point_tag: Label = %MatchPointTag
|
||||
@onready var character_frame: PanelContainer = %CharacterFrame
|
||||
@onready var add_button: Button = %AddPoint
|
||||
@onready var minus_button: Button = %RemovePoint
|
||||
@@ -29,6 +33,9 @@ func _ready() -> void:
|
||||
minus_button.set_shortcut(create_button_shortcut(list_index, true))
|
||||
character.pressed.connect(change_character)
|
||||
remove_player.pressed.connect(func() -> void: player_removed.emit())
|
||||
player_object.player_point_given.connect(_on_score_changed)
|
||||
player_object.player_point_taken.connect(_on_score_changed)
|
||||
player_object.first_point_triggered.connect(_on_first_point)
|
||||
character.mouse_entered.connect(_set_character_hovered.bind(true))
|
||||
character.mouse_exited.connect(_set_character_hovered.bind(false))
|
||||
# Rows take turns being lighter and darker, like the other lists
|
||||
@@ -81,6 +88,36 @@ func set_player_character() -> void:
|
||||
|
||||
func update_score() -> void:
|
||||
score_label.text = str(player_object.player_score)
|
||||
_show_match_point(is_match_point())
|
||||
|
||||
|
||||
## Whether the player is one point from winning. A prepared quiz has no winning score.
|
||||
func is_match_point() -> bool:
|
||||
return !Settings.is_prepared_quiz() && Settings.winning_score > 1 \
|
||||
&& player_object.player_score == Settings.winning_score - 1
|
||||
|
||||
|
||||
func _on_score_changed() -> void:
|
||||
Anim.pop(score_label)
|
||||
|
||||
|
||||
func _on_first_point() -> void:
|
||||
Anim.flash(self, get_theme_color(&"first_point", &"Palette"), Anim.SLOW * 2)
|
||||
Anim.pop(score_label, 1.8, Anim.SLOW)
|
||||
|
||||
|
||||
# The glow and the tag show while the player is on match point
|
||||
func _show_match_point(on: bool) -> void:
|
||||
var glow: Node = get_node_or_null("Pulse")
|
||||
if on == match_point_tag.visible && on == (glow != null):
|
||||
return
|
||||
match_point_tag.visible = on
|
||||
if glow != null:
|
||||
glow.queue_free()
|
||||
glow.name = "PulseEnding"
|
||||
if on:
|
||||
Anim.pulse(self, get_theme_color(&"match_point", &"Palette"))
|
||||
Anim.pop(match_point_tag, 1.4, Anim.SLOW)
|
||||
|
||||
|
||||
func _set_character_hovered(hovered: bool) -> void:
|
||||
|
||||
@@ -124,10 +124,14 @@ func apply_filter() -> void:
|
||||
var filter: ShowFilter = show_option.selected as ShowFilter
|
||||
var shown: int = 0
|
||||
for card: SoundtrackCard in cards:
|
||||
var was_visible: bool = card.visible
|
||||
card.visible = (search_text.is_empty() || matching.has(card.soundtrack.soundtrack_name)) \
|
||||
&& _passes_filter(card.soundtrack, filter)
|
||||
if card.visible:
|
||||
shown += 1
|
||||
# Cards that come back with the filter fade in
|
||||
if !was_visible:
|
||||
Anim.fade_in(card)
|
||||
if shown == cards.size():
|
||||
count_label.text = "%d soundtracks" % cards.size()
|
||||
else:
|
||||
|
||||
+822
-624
File diff suppressed because it is too large.
Load diff
@@ -30,6 +30,8 @@ func before_test() -> void:
|
||||
Settings.winning_score = 20
|
||||
# Otherwise the About popup opens at start, as for a new version
|
||||
Settings.last_seen_version = Settings.version
|
||||
# Popups open and close at once, so the tests don't wait for animations
|
||||
Settings.reduce_animations = true
|
||||
|
||||
_server = FakeHttpTransport.new().install()
|
||||
var mp3: PackedByteArray = FileAccess.get_file_as_bytes("res://tests/data/short.mp3")
|
||||
|
||||
@@ -175,6 +175,8 @@ static func prepare_autoloads() -> void:
|
||||
Settings.use_low_played_mode = false
|
||||
Settings.winning_score = 20
|
||||
Settings.last_seen_version = Settings.version
|
||||
# The same picture every time, without half-finished animations
|
||||
Settings.reduce_animations = true
|
||||
# The inspiration list scrolls by itself, which would make every picture different
|
||||
Settings.inspiration_list_speed = 0
|
||||
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
extends MainWindowTestSuite
|
||||
## The animations, with Reduce animations off: popups, points, songs starting, the
|
||||
## winner, filtering, the tabs and the buttons. The other tests run with animations off.
|
||||
|
||||
var _settings: SettingsPopup
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
await super()
|
||||
Settings.reduce_animations = false
|
||||
_settings = _window.get_node("SettingsPopup")
|
||||
|
||||
|
||||
func _wait(seconds: float) -> void:
|
||||
await await_millis(int(seconds * 1000) + 60)
|
||||
|
||||
|
||||
func _layers(container: Node) -> Array[Node]:
|
||||
return container.get_children().filter(func(node: Node) -> bool: return node is ColorRect)
|
||||
|
||||
|
||||
func test_popup_fades_in_while_growing() -> void:
|
||||
_settings.open()
|
||||
|
||||
assert_float(_settings.settings_panel.modulate.a).is_less(1.0)
|
||||
assert_float(_settings.settings_panel.scale.x).is_less(1.0)
|
||||
await _wait(Anim.NORMAL)
|
||||
assert_float(_settings.settings_panel.modulate.a).is_equal(1.0)
|
||||
assert_vector(_settings.settings_panel.scale).is_equal(Vector2.ONE)
|
||||
|
||||
|
||||
func test_closing_popup_hides_after_fading_out() -> void:
|
||||
_settings.open()
|
||||
await _wait(Anim.NORMAL)
|
||||
|
||||
_settings.close()
|
||||
|
||||
assert_bool(_settings.visible).is_true()
|
||||
assert_bool(_settings.is_closing).is_true()
|
||||
await _wait(Anim.FAST)
|
||||
assert_bool(_settings.visible).is_false()
|
||||
assert_bool((_window.get_node("PopupBackdrop") as Control).visible).is_false()
|
||||
# Ready to open again at full size
|
||||
assert_float(_settings.settings_panel.modulate.a).is_equal(1.0)
|
||||
|
||||
|
||||
func test_popup_opened_again_while_closing_stays_open() -> void:
|
||||
_settings.open()
|
||||
await _wait(Anim.NORMAL)
|
||||
_settings.close()
|
||||
|
||||
_settings.open()
|
||||
await _wait(Anim.NORMAL)
|
||||
|
||||
assert_bool(_settings.visible).is_true()
|
||||
var backdrop: Control = _window.get_node("PopupBackdrop")
|
||||
assert_bool(backdrop.visible).is_true()
|
||||
assert_float(backdrop.modulate.a).is_equal(1.0)
|
||||
|
||||
|
||||
func test_reduce_animations_opens_and_closes_at_once() -> void:
|
||||
Settings.reduce_animations = true
|
||||
|
||||
_settings.open()
|
||||
assert_float(_settings.settings_panel.modulate.a).is_equal(1.0)
|
||||
_settings.close()
|
||||
|
||||
assert_bool(_settings.visible).is_false()
|
||||
|
||||
|
||||
func test_reduce_animations_switch_is_saved() -> void:
|
||||
# The switch shows the setting the window started with, which the tests turn on
|
||||
assert_bool(_settings.reduce_animations_button.button_pressed).is_true()
|
||||
|
||||
_settings.reduce_animations_button.button_pressed = false
|
||||
assert_bool(Settings.reduce_animations).is_false()
|
||||
_settings.reduce_animations_button.button_pressed = true
|
||||
|
||||
assert_bool(Settings.reduce_animations).is_true()
|
||||
var saved: ConfigFile = ConfigFile.new()
|
||||
assert_int(saved.load(Settings.saved_settings_path())).is_equal(OK)
|
||||
assert_bool(saved.get_value("app", "reduce_animations")).is_true()
|
||||
|
||||
|
||||
func test_first_point_flashes_the_row_and_pops_the_score() -> void:
|
||||
var crono: PlayerObject = _add_player("Crono")
|
||||
await _play_first_song()
|
||||
var row: PlayerRow = _player_row(crono)
|
||||
|
||||
row.add_point()
|
||||
|
||||
assert_array(_layers(row)).has_size(1)
|
||||
assert_float(row.score_label.scale.x).is_greater(1.0)
|
||||
await _wait(Anim.SLOW * 2)
|
||||
assert_array(_layers(row)).is_empty()
|
||||
assert_vector(row.score_label.scale).is_equal(Vector2.ONE)
|
||||
|
||||
|
||||
func test_later_points_only_pop_the_score() -> void:
|
||||
var crono: PlayerObject = _add_player("Crono")
|
||||
await _play_first_song()
|
||||
var row: PlayerRow = _player_row(crono)
|
||||
row.add_point()
|
||||
await _wait(Anim.SLOW * 2)
|
||||
|
||||
row.add_point()
|
||||
|
||||
assert_array(_layers(row)).is_empty()
|
||||
assert_float(row.score_label.scale.x).is_greater(1.0)
|
||||
|
||||
|
||||
func test_match_point_glows_and_says_so_until_it_changes() -> void:
|
||||
Settings.winning_score = 3
|
||||
var crono: PlayerObject = _add_player("Crono")
|
||||
await _play_first_song()
|
||||
var row: PlayerRow = _player_row(crono)
|
||||
|
||||
row.add_point()
|
||||
row.add_point()
|
||||
|
||||
assert_bool(row.match_point_tag.visible).is_true()
|
||||
assert_object(row.get_node_or_null("Pulse")).is_not_null()
|
||||
|
||||
row.minus_point()
|
||||
|
||||
assert_bool(row.match_point_tag.visible).is_false()
|
||||
assert_object(row.get_node_or_null("Pulse")).is_null()
|
||||
|
||||
|
||||
func test_match_point_glows_still_with_reduced_animations() -> void:
|
||||
Settings.reduce_animations = true
|
||||
Settings.winning_score = 2
|
||||
var crono: PlayerObject = _add_player("Crono")
|
||||
await _play_first_song()
|
||||
|
||||
_player_row(crono).add_point()
|
||||
|
||||
assert_object(_player_row(crono).get_node_or_null("Pulse")).is_not_null()
|
||||
|
||||
|
||||
func test_song_start_fades_in_the_title_and_flashes_its_row() -> void:
|
||||
Settings.hide_next_track = false
|
||||
# The next song is downloaded while the first plays
|
||||
_next_song_is({"SongNo": 1, "Soundtrack": "Final Fantasy VI", "Song": "Dancing Mad"})
|
||||
await _play_first_song()
|
||||
await _wait(Anim.SLOW)
|
||||
|
||||
_press("SongControls/NextButton")
|
||||
await _wait(0.55)
|
||||
|
||||
var now_playing: Control = _quiz.get_node("NowPlaying")
|
||||
assert_float(now_playing.modulate.a).is_less(1.0)
|
||||
var rows: Array[Node] = _quiz.music_list_panel.get_node("%MusicList").get_children()
|
||||
var flashing: Array = rows.filter(func(row: Node) -> bool: return !_layers(row).is_empty())
|
||||
assert_array(flashing).is_not_empty()
|
||||
|
||||
|
||||
func test_winner_gets_confetti_and_a_graph_drawn_from_the_left() -> void:
|
||||
Settings.winning_score = 1
|
||||
var crono: PlayerObject = _add_player("Crono")
|
||||
await _play_first_song()
|
||||
|
||||
_player_row(crono).add_point()
|
||||
await _runner.simulate_frames(1)
|
||||
|
||||
var winner: WinnerPopup = _quiz.winner_popup
|
||||
assert_bool(winner.visible).is_true()
|
||||
assert_object(winner.winner_panel.get_node_or_null("Confetti")).is_not_null()
|
||||
assert_float(winner.score_graph.progress).is_less(1.0)
|
||||
await _wait(1.3)
|
||||
assert_float(winner.score_graph.progress).is_equal(1.0)
|
||||
|
||||
|
||||
func test_graph_lines_are_cut_where_the_drawing_has_reached() -> void:
|
||||
var points: PackedVector2Array = [Vector2(0, 0), Vector2(10, 0), Vector2(10, 10),
|
||||
Vector2(30, 10)]
|
||||
|
||||
assert_array(Array(ScoreGraph.clip_line(points, 20))) \
|
||||
.contains_exactly([Vector2(0, 0), Vector2(10, 0), Vector2(10, 10), Vector2(20, 10)])
|
||||
assert_array(Array(ScoreGraph.clip_line(points, 100))).has_size(4)
|
||||
assert_array(Array(ScoreGraph.clip_line(points, 5))) \
|
||||
.contains_exactly([Vector2(0, 0), Vector2(5, 0)])
|
||||
|
||||
|
||||
func test_statistics_tab_slides_in_and_ends_in_place() -> void:
|
||||
var statistics: Control = _window.get_node("StatisticsView")
|
||||
var home: Vector2 = statistics.position
|
||||
|
||||
_window.show_tab(&"statistics")
|
||||
|
||||
assert_float(statistics.position.x).is_greater(home.x)
|
||||
await _wait(Anim.NORMAL)
|
||||
assert_vector(statistics.position).is_equal(home)
|
||||
assert_float(statistics.modulate.a).is_equal(1.0)
|
||||
|
||||
|
||||
func test_every_button_bounces() -> void:
|
||||
var sync: Button = _window.get_node("TopBar/%SyncButton")
|
||||
var reset: Button = _quiz.get_node("QuizToolbar/ResetPointsButton")
|
||||
var crono: PlayerObject = _add_player("Crono")
|
||||
|
||||
for button: BaseButton in [sync, reset, _player_row(crono).add_button]:
|
||||
assert_bool(button.has_meta(&"button_bounce_added")).override_failure_message(
|
||||
button.name).is_true()
|
||||
|
||||
|
||||
func test_helpers_do_nothing_with_reduced_animations() -> void:
|
||||
Settings.reduce_animations = true
|
||||
var label: Label = _quiz.get_node("NowPlaying/SongLabel")
|
||||
|
||||
Anim.pop(label)
|
||||
Anim.fade_in(label)
|
||||
|
||||
assert_vector(label.scale).is_equal(Vector2.ONE)
|
||||
assert_float(label.modulate.a).is_equal(1.0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cuukc27kr8360
|
||||
@@ -64,7 +64,7 @@ const STYLES: Dictionary = {
|
||||
|
||||
## Theme types that only scripts read from, so they are not styles a node can pick.
|
||||
const CODE_ONLY_TYPES: Dictionary = {
|
||||
"Palette": ["backdrop"],
|
||||
"Palette": ["backdrop", "first_point", "match_point", "flash"],
|
||||
"ScoreGraph": [
|
||||
"player_1", "player_2", "player_3", "player_4", "player_5", "player_6", "player_7",
|
||||
"grid", "axis_text", "winning_line",
|
||||
|
||||
@@ -7,6 +7,7 @@ const SAVED_VALUES: Dictionary = {
|
||||
&"hide_beginning": true, &"hide_for_seconds": 7.0, &"hide_length": true,
|
||||
&"hide_ticks": true, &"winning_score": 13, &"inspiration_list_speed": 4,
|
||||
&"fullscreen": true, &"last_seen_version": "1.2.3", &"theme_id": "original_web",
|
||||
&"reduce_animations": true,
|
||||
}
|
||||
|
||||
var _snapshot: AutoloadSnapshot
|
||||
|
||||
@@ -88,6 +88,9 @@ inspector or `theme_type_variation` in code:
|
||||
| Colour | Used for |
|
||||
|---|---|
|
||||
| `backdrop` | The dimming behind an open popup |
|
||||
| `first_point` | The flash over a player's row on their first point (see `common/anim.gd`) |
|
||||
| `match_point` | The glow over a player's row while they are one point from winning |
|
||||
| `flash` | The flash over a song's row when it starts playing or is answered |
|
||||
|
||||
**ScoreGraph**: the graph of the game on the Winner popup, drawn from code.
|
||||
|
||||
|
||||
@@ -1356,6 +1356,9 @@ OptionButton/styles/normal = SubResource("StyleBoxFlat_a4gt2")
|
||||
OptionButton/styles/normal_mirrored = SubResource("StyleBoxFlat_uisqs")
|
||||
OptionButton/styles/pressed = SubResource("StyleBoxFlat_dinf2")
|
||||
OptionButton/styles/pressed_mirrored = SubResource("StyleBoxFlat_a0ksy")
|
||||
Palette/colors/first_point = Color(0.498, 0.82, 0.545, 0.4)
|
||||
Palette/colors/match_point = Color(0.949, 0.843, 0.478, 0.35)
|
||||
Palette/colors/flash = Color(0.992, 0.98, 0.953, 0.22)
|
||||
Palette/colors/backdrop = Color(0.1, 0.12, 0.16, 0.55)
|
||||
Panel/styles/panel = SubResource("StyleBoxFlat_hwwyi")
|
||||
PanelContainer/styles/panel = SubResource("StyleBoxFlat_wl3gy")
|
||||
|
||||
@@ -1164,6 +1164,9 @@ OptionButton/styles/normal = SubResource("StyleBoxFlat_a4gt2")
|
||||
OptionButton/styles/normal_mirrored = SubResource("StyleBoxFlat_uisqs")
|
||||
OptionButton/styles/pressed = SubResource("StyleBoxFlat_dinf2")
|
||||
OptionButton/styles/pressed_mirrored = SubResource("StyleBoxFlat_a0ksy")
|
||||
Palette/colors/first_point = Color(0.3, 0.85, 0.4, 0.35)
|
||||
Palette/colors/match_point = Color(1, 0.5, 0, 0.3)
|
||||
Palette/colors/flash = Color(1, 1, 1, 0.22)
|
||||
Palette/colors/backdrop = Color(0, 0, 0, 0.55)
|
||||
Panel/styles/panel = SubResource("StyleBoxFlat_hwwyi")
|
||||
PanelContainer/styles/panel = SubResource("StyleBoxFlat_wl3gy")
|
||||
|
||||
@@ -1339,6 +1339,9 @@ OptionButton/styles/normal = SubResource("StyleBoxFlat_a4gt2")
|
||||
OptionButton/styles/normal_mirrored = SubResource("StyleBoxFlat_uisqs")
|
||||
OptionButton/styles/pressed = SubResource("StyleBoxFlat_dinf2")
|
||||
OptionButton/styles/pressed_mirrored = SubResource("StyleBoxFlat_a0ksy")
|
||||
Palette/colors/first_point = Color(0.639, 0.745, 0.549, 0.4)
|
||||
Palette/colors/match_point = Color(0.922, 0.796, 0.545, 0.3)
|
||||
Palette/colors/flash = Color(0.925, 0.937, 0.957, 0.18)
|
||||
Palette/colors/backdrop = Color(0.1, 0.12, 0.16, 0.55)
|
||||
Panel/styles/panel = SubResource("StyleBoxFlat_hwwyi")
|
||||
PanelContainer/styles/panel = SubResource("StyleBoxFlat_wl3gy")
|
||||
|
||||
@@ -1265,6 +1265,9 @@ OptionButton/styles/normal = SubResource("StyleBoxFlat_a4gt2")
|
||||
OptionButton/styles/normal_mirrored = SubResource("StyleBoxFlat_uisqs")
|
||||
OptionButton/styles/pressed = SubResource("StyleBoxFlat_dinf2")
|
||||
OptionButton/styles/pressed_mirrored = SubResource("StyleBoxFlat_a0ksy")
|
||||
Palette/colors/first_point = Color(0.298, 0.686, 0.314, 0.4)
|
||||
Palette/colors/match_point = Color(1, 0.612, 0, 0.3)
|
||||
Palette/colors/flash = Color(1, 1, 0, 0.18)
|
||||
Palette/colors/backdrop = Color(0, 0, 0, 0.6)
|
||||
Panel/styles/panel = SubResource("StyleBoxFlat_hwwyi")
|
||||
PanelContainer/styles/panel = SubResource("StyleBoxFlat_wl3gy")
|
||||
|
||||
Reference in new issue
Block a user