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
+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
|
||||
|
||||
Reference in new issue
Block a user