The bar in the Sync popup is now an AnimatedProgressBar: taller, with a darker track and the theme's accent fill (orange, frost blue in Nord, green with a gold shine for Christmas). It shows Godot's busy animation until the server reports progress, glides to each new value, has a soft highlight sweeping along the filled part inside its rounded corners, and fills and pops before the result shows. With Reduce animations it only jumps to its values. The styles are SyncProgressBar, SyncProgressBarFill and SyncProgressBarText in every theme. Anim.stop stops a keyed animation without starting an empty one. The server now sends sync times as "MM:SS"; the spec is updated from MusicServer develop (also the linux-package route), and the test data and screenshots use the new format. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
568 lines
21 KiB
GDScript
568 lines
21 KiB
GDScript
class_name UiTour
|
||
extends RefCounted
|
||
## Walks the main window through a fixed set of states against a fake MusicServer:
|
||
## startup, players added, a song playing, every popup open, a sync and the statistics
|
||
## tab.
|
||
##
|
||
## Used for screenshots ([code]tools/take_screenshots.gd[/code]) and for the layout
|
||
## test, so both look at exactly the same states. Nodes are looked up by name in the
|
||
## main window and in its Quiz and TopBar scenes, so the tour works no matter which of
|
||
## them a node lives in.
|
||
|
||
const SOUNDTRACKS: Array[String] = [
|
||
"Chrono Trigger", "Final Fantasy VI", "Secret of Mana", "Terranigma",
|
||
"Earthbound", "Super Metroid", "The Legend of Zelda: A Link to the Past",
|
||
"Donkey Kong Country 2", "Lufia II", "Illusion of Gaia",
|
||
]
|
||
const SONG_INFO: Dictionary = {
|
||
"SongNo": 0, "Soundtrack": "Chrono Trigger", "SoundtrackUuid": "soundtrack-0",
|
||
"Song": "Corridors of Time",
|
||
}
|
||
const SYNC_PROGRESS: Dictionary = {"progress": 40, "time_spent": "00:02"}
|
||
const SYNC_RESULT: Dictionary = {
|
||
"soundtracks_added": ["Terranigma", "Lufia II"],
|
||
"soundtracks_re_added": [],
|
||
"soundtracks_changed_title": {"FF6": "Final Fantasy VI"},
|
||
"soundtracks_changed_content": ["Chrono Trigger"],
|
||
"soundtracks_removed": ["Old Soundtrack"],
|
||
"catched_errors": [],
|
||
"total_time": "00:03",
|
||
}
|
||
|
||
## The server's version history, newest first.
|
||
const SERVER_HISTORY: Array[Dictionary] = [
|
||
{"version": "5.0.0", "changelog": [
|
||
"#9 - Major routes refactor and expansion", "#15 - Added API for statistics",
|
||
"Token authentication for /api/v1",
|
||
]},
|
||
{"version": "4.5.0", "changelog": [
|
||
"#1 - Created request to check newest version of the app",
|
||
"#2 - Added request to download the newest version of the app",
|
||
]},
|
||
{"version": "4.0.0", "changelog": ["Changed framework from gin to Echo", "Reorganized the code"]},
|
||
]
|
||
|
||
## Song names of the first soundtrack in the statistics, to show Japanese text.
|
||
const JAPANESE_SONGS: Array[String] = [
|
||
"時の回廊", "カエルのテーマ", "風の憧憬", "魔王決戦", "遥かなる時の彼方へ",
|
||
]
|
||
|
||
## The character pictures the fake server hands out.
|
||
const CHARACTERS_DIR: String = "res://characters"
|
||
## Small copies of covers from the MusicServer, 2:3 like all its covers.
|
||
const COVERS_DIR: String = "res://tests/data/covers"
|
||
|
||
## Popups in the order the tour opens them: the state name, then the popup's node. The
|
||
## state names are what the popups were first called, so screenshots taken before they
|
||
## were renamed can still be compared with new ones.
|
||
const POPUPS: Array[Array] = [
|
||
["Search", "SearchPopup"], ["SyncWindow", "SyncPopup"],
|
||
["SettingsWindow", "SettingsPopup"], ["QRWindow", "QRPopup"],
|
||
["NewCharacterSelect", "CharacterSelectPopup"], ["ShortcutsWindow", "ShortcutsPopup"],
|
||
["AboutWindow", "AboutPopup"], ["WinnerWindow", "WinnerPopup"], ["Log", "LogPopup"],
|
||
["QuizSettings", "QuizSettingsCard"],
|
||
]
|
||
|
||
var _main: Control
|
||
var _scores_before_win: Array[int] = []
|
||
var _winning_score_before_win: int = 0
|
||
|
||
|
||
func _init(main: Control) -> void:
|
||
_main = main
|
||
|
||
|
||
## Returns a fake server that answers every request the main window makes on the tour.
|
||
## Call [method FakeHttpTransport.install] before the main window is created.
|
||
static func make_server() -> FakeHttpTransport:
|
||
var server: FakeHttpTransport = FakeHttpTransport.new()
|
||
var mp3: PackedByteArray = FileAccess.get_file_as_bytes("res://tests/data/short.mp3")
|
||
server.respond_json("/music/all", SOUNDTRACKS)
|
||
server.respond_json("/music/all/order", SOUNDTRACKS)
|
||
server.respond_json("/music/list", [])
|
||
server.respond_json("/download", "1.0.0")
|
||
var characters: Array[String] = character_names()
|
||
server.respond_json("/character/list", characters)
|
||
for character_name: String in characters:
|
||
server.respond_bytes("/character?name=" + character_name.uri_encode(),
|
||
FileAccess.get_file_as_bytes(CHARACTERS_DIR.path_join(character_name)))
|
||
server.respond_bytes("/music/rand", mp3)
|
||
server.respond_bytes("/music/addQue", PackedByteArray())
|
||
server.respond_json("/music/info", SONG_INFO)
|
||
server.respond_bytes("/music/reset", PackedByteArray())
|
||
server.respond_bytes("/music/played?song=0", PackedByteArray())
|
||
server.respond_bytes("/sync", PackedByteArray())
|
||
server.respond_json("/sync/progress", SYNC_PROGRESS)
|
||
server.respond_json("/statistics/soundtracks", soundtrack_statistics())
|
||
# Real covers from the server, made smaller, for every other soundtrack
|
||
for i: int in range(0, SOUNDTRACKS.size(), 2):
|
||
server.respond_bytes("/soundtracks/soundtrack-%d/cover" % i,
|
||
FileAccess.get_file_as_bytes(COVERS_DIR.path_join("cover_%d.jpg" % (i >> 1))))
|
||
# The soundtrack list tells the quiz which soundtracks have covers
|
||
var soundtracks: Array[Dictionary] = []
|
||
for soundtrack: Dictionary in soundtrack_statistics():
|
||
soundtracks.append({"uuid": soundtrack.soundtrack_uuid,
|
||
"name": soundtrack.soundtrack_name,
|
||
"cover_modified": soundtrack.get("cover_modified", 0),
|
||
"alternative_names": soundtrack.get("alternative_names", [])})
|
||
server.respond_json("/soundtracks", soundtracks)
|
||
server.respond_json("/version/history", SERVER_HISTORY)
|
||
return server
|
||
|
||
|
||
## Play statistics for every soundtrack in [constant SOUNDTRACKS], the same on every
|
||
## run. Some have never been played.
|
||
static func soundtrack_statistics() -> Array[Dictionary]:
|
||
var statistics: Array[Dictionary] = []
|
||
for i: int in SOUNDTRACKS.size():
|
||
var played: int = (i * 7) % 11
|
||
var songs: Array[Dictionary] = []
|
||
for song: int in 5:
|
||
songs.append({
|
||
"song_uuid": "song-%d-%d" % [i, song],
|
||
"soundtrack_uuid": "soundtrack-%d" % i,
|
||
"soundtrack_name": SOUNDTRACKS[i],
|
||
"song_name": JAPANESE_SONGS[song] if i == 0 else "Track %d" % (song + 1),
|
||
"times_played": maxi(0, played - song * 2),
|
||
})
|
||
var soundtrack: Dictionary = {
|
||
"soundtrack_uuid": "soundtrack-%d" % i,
|
||
"soundtrack_name": SOUNDTRACKS[i],
|
||
"soundtrack_played": played,
|
||
"songs": songs,
|
||
}
|
||
if played > 0:
|
||
soundtrack.soundtrack_last_played = "2026-09-%02dT20:00:00Z" % (i + 1)
|
||
# Every other soundtrack has a cover, the rest show their initials
|
||
if i % 2 == 0:
|
||
soundtrack.cover_modified = 1700000000
|
||
if i == 0:
|
||
soundtrack.alternative_names = ["CT", "クロノ・トリガー"]
|
||
statistics.append(soundtrack)
|
||
return statistics
|
||
|
||
|
||
## Who scored on each song of a 60-song game to 20 points, the same on every run: the
|
||
## third of [param players] wins on the last song, nobody scores on songs 21–27 and the
|
||
## others stay below 20. Needs at least three players.
|
||
static func long_game(players: Array[PlayerObject]) -> Array:
|
||
var random: RandomNumberGenerator = RandomNumberGenerator.new()
|
||
random.seed = 7
|
||
var winner: int = 2
|
||
var scores: Array[int] = []
|
||
scores.resize(players.size())
|
||
var song_scorers: Array = []
|
||
for song: int in 60:
|
||
var scorers: Array[String] = []
|
||
for index: int in players.size():
|
||
var limit: int = 19 if index == winner else 18
|
||
var chance: float = 0.36 if index == winner else 0.2 + 0.02 * index
|
||
if song < 59 && (song < 20 || song >= 27) && scores[index] < limit \
|
||
&& random.randf() < chance:
|
||
scorers.append(players[index].id)
|
||
scores[index] += 1
|
||
song_scorers.append(scorers)
|
||
song_scorers[59].append(players[winner].id)
|
||
return song_scorers
|
||
|
||
|
||
## The pictures in [constant CHARACTERS_DIR], sorted by name.
|
||
static func character_names() -> Array[String]:
|
||
var names: Array[String] = []
|
||
for file_name: String in DirAccess.get_files_at(CHARACTERS_DIR):
|
||
if file_name.ends_with(".png"):
|
||
names.append(file_name)
|
||
names.sort()
|
||
return names
|
||
|
||
|
||
## Puts Settings and Playlist in the state the tour expects. Call it before the main
|
||
## window is created.
|
||
static func prepare_autoloads() -> void:
|
||
Playlist.reset_playlist()
|
||
Playlist.first_song_played = false
|
||
Playlist.is_song_downloading = false
|
||
Settings.player_array.clear()
|
||
Settings.music_folder = null
|
||
Settings.character_folder = ""
|
||
# A quiz saved by an earlier run would be continued
|
||
QuizSave.delete()
|
||
Settings.edit_players = false
|
||
Settings.currently_syncing = false
|
||
Settings.character_select_open = false
|
||
Settings.add_to_stats = true
|
||
Settings.hide_next_track = true
|
||
Settings.random_mode = Settings.RandomMode.NORMAL
|
||
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
|
||
|
||
|
||
## Runs the tour. [param on_state] is called (and awaited) with the name of each state
|
||
## and the popup that is open in it, or [code]null[/code] when no popup is open.
|
||
func run(on_state: Callable) -> void:
|
||
_stop_caret_blink(_main)
|
||
await _settle()
|
||
await on_state.call("startup", null)
|
||
|
||
_call("add_players")
|
||
await _settle()
|
||
await on_state.call("add_player_row", null)
|
||
|
||
_add_player("Crono")
|
||
_add_player("Marle")
|
||
_call("add_players")
|
||
await _settle()
|
||
await on_state.call("players_added", null)
|
||
|
||
_call("play_next_song")
|
||
await _settle()
|
||
_freeze_music_player()
|
||
var picture: Image = Image.new()
|
||
picture.load_png_from_buffer(
|
||
FileAccess.get_file_as_bytes(CHARACTERS_DIR.path_join(character_names()[0])))
|
||
_player_row(0).new_character_selected(ImageTexture.create_from_image(picture),
|
||
character_names()[0])
|
||
_player_row(0).add_point()
|
||
_player_row(0).add_point()
|
||
_player_row(1).add_point()
|
||
_call("show_answer_pressed")
|
||
await _settle()
|
||
_freeze_music_player()
|
||
_fix_log_times()
|
||
await _settle()
|
||
await on_state.call("playing", null)
|
||
|
||
for state_and_node: Array in POPUPS:
|
||
var popup: Node = await _open_popup(state_and_node[1])
|
||
await _settle()
|
||
await on_state.call("popup_" + state_and_node[0], popup)
|
||
_close_popup(popup)
|
||
await _settle()
|
||
|
||
var search_popup: SearchPopup = _node("SearchPopup")
|
||
search_popup.open()
|
||
search_popup.search_field.text = "lttp"
|
||
search_popup.search()
|
||
await _settle()
|
||
await on_state.call("popup_Search_typed", search_popup)
|
||
search_popup.close()
|
||
await _settle()
|
||
|
||
var sync_popup: BasePopup = _node("SyncPopup")
|
||
Settings.currently_syncing = false
|
||
sync_popup.has_synced = false
|
||
sync_popup.start_sync()
|
||
sync_popup.sync_timer.stop()
|
||
sync_popup._on_sync_timeout()
|
||
await _settle()
|
||
await on_state.call("sync_in_progress", sync_popup)
|
||
sync_popup.close()
|
||
await _settle()
|
||
await on_state.call("sync_buttons_locked", null)
|
||
|
||
_server().respond_json("/sync/progress", SYNC_RESULT)
|
||
sync_popup._on_sync_timeout()
|
||
sync_popup.open()
|
||
await _settle()
|
||
await on_state.call("sync_result", sync_popup)
|
||
sync_popup.close()
|
||
await _settle()
|
||
|
||
var top_bar: TopBar = _node("TopBar")
|
||
top_bar.show_latest_version("99.10.10")
|
||
await _settle()
|
||
await on_state.call("update_available", null)
|
||
var update_popup: UpdatePopup = _node("UpdatePopup")
|
||
var updater: Updater = update_popup.updater
|
||
updater.in_editor = false
|
||
updater.os_name = "Windows"
|
||
DirAccess.make_dir_recursive_absolute(Settings.config_dir())
|
||
updater.executable_path = Settings.config_dir().path_join("MusicPlayer.exe")
|
||
update_popup.show_update("99.10.10")
|
||
await _settle()
|
||
await on_state.call("popup_UpdateWindow", update_popup)
|
||
update_popup.download_progress.visible = true
|
||
update_popup._set_busy(true)
|
||
update_popup._show_status("")
|
||
update_popup._on_progress(31 << 20, 80 << 20)
|
||
await _settle()
|
||
await on_state.call("popup_UpdateWindow_downloading", update_popup)
|
||
updater.in_editor = true
|
||
update_popup.show_update("99.10.10")
|
||
await _settle()
|
||
await on_state.call("popup_UpdateWindow_browser_only", update_popup)
|
||
update_popup.close()
|
||
await _settle()
|
||
top_bar.show_latest_version("1.0.0")
|
||
await _settle()
|
||
|
||
var statistics_view: StatisticsView = _node("StatisticsView")
|
||
_main.show_tab(&"statistics")
|
||
await _settle()
|
||
await on_state.call("statistics", statistics_view)
|
||
statistics_view.show_option.select(StatisticsView.ShowFilter.NEVER_PLAYED)
|
||
statistics_view.apply_filter()
|
||
await _settle()
|
||
await on_state.call("statistics_never_played", statistics_view)
|
||
statistics_view.show_option.select(StatisticsView.ShowFilter.ALL)
|
||
statistics_view.sort_option.select(StatisticsView.SortOrder.MOST_PLAYED)
|
||
statistics_view.apply_sort()
|
||
statistics_view.search_field.text = "chrono"
|
||
statistics_view.apply_filter()
|
||
await _settle()
|
||
await on_state.call("statistics_search", statistics_view)
|
||
# Chrono Trigger has never been played, so the song list searches for another
|
||
statistics_view.search_field.text = "zelda"
|
||
statistics_view.set_mode(StatisticsView.Mode.SONGS)
|
||
await _settle()
|
||
await on_state.call("statistics_songs_search", statistics_view)
|
||
statistics_view.clear_search()
|
||
await _settle()
|
||
await on_state.call("statistics_songs", statistics_view)
|
||
statistics_view.search_field.text = "chrono"
|
||
statistics_view.set_mode(StatisticsView.Mode.SOUNDTRACKS)
|
||
await _settle()
|
||
var stats_popup: SoundtrackStatsPopup = _node("SoundtrackStatsPopup")
|
||
for card: SoundtrackCard in statistics_view.cards:
|
||
if card.visible:
|
||
stats_popup.show_soundtrack(card.soundtrack)
|
||
break
|
||
await _settle()
|
||
await on_state.call("popup_SoundtrackStats", stats_popup)
|
||
stats_popup.edit_names()
|
||
stats_popup.new_name_field.text = "Chrono"
|
||
await _settle()
|
||
await on_state.call("popup_SoundtrackStats_names", stats_popup)
|
||
stats_popup.change_cover()
|
||
stats_popup.cover_id_field.text = "5248061"
|
||
await _settle()
|
||
await on_state.call("popup_SoundtrackStats_cover", stats_popup)
|
||
stats_popup.stop_editing()
|
||
stats_popup.close()
|
||
_main.show_tab(&"quiz")
|
||
await _settle()
|
||
|
||
# Last, as it adds players: a long game with five players on the Winner popup
|
||
for player_name: String in ["Lucca", "Frog", "Robo"]:
|
||
_add_player(player_name)
|
||
await _settle()
|
||
var players: Array[PlayerObject] = Settings.player_array
|
||
var song_scorers: Array = long_game(players)
|
||
var history: ScoreHistory = ScoreHistory.from_songs(players, song_scorers)
|
||
for player: PlayerObject in players:
|
||
player.player_score = history.final_score(player.id)
|
||
Settings.winning_score = 20
|
||
var winner_popup: WinnerPopup = _node("WinnerPopup")
|
||
winner_popup.show_winner(players[2])
|
||
winner_popup.show_history(history)
|
||
await _settle()
|
||
await on_state.call("popup_WinnerWindow_long_game", winner_popup)
|
||
winner_popup.close()
|
||
await _settle()
|
||
|
||
# Playing from a music folder: a soundtrack folder, then a prepared quiz in Settings
|
||
var music: String = _make_music_folders()
|
||
var settings_popup: SettingsPopup = _node("SettingsPopup")
|
||
# Songs are picked at random; the same picks every time keep the pictures the same
|
||
seed(1)
|
||
settings_popup.use_music_folder(music.path_join("Soundtracks"))
|
||
await _settle()
|
||
_call("play_next_song")
|
||
# A playing song is stopped first, which takes half a second
|
||
await _main.get_tree().create_timer(0.7).timeout
|
||
await _settle()
|
||
_call("show_answer_pressed")
|
||
await _settle()
|
||
await on_state.call("offline", null)
|
||
settings_popup.use_music_folder(music.path_join("Musiktävling"))
|
||
settings_popup.open()
|
||
await _settle()
|
||
await on_state.call("popup_SettingsWindow_offline", settings_popup)
|
||
settings_popup.close()
|
||
# The end of a prepared quiz, where two players share the most points
|
||
var scores: Array[int] = [7, 12, 12, 3, 9]
|
||
for index: int in Settings.player_array.size():
|
||
Settings.player_array[index].player_score = scores[index % scores.size()]
|
||
_call("show_final_winners")
|
||
var final_popup: WinnerPopup = _node("WinnerPopup")
|
||
await _settle()
|
||
await on_state.call("popup_WinnerWindow_prepared_quiz", final_popup)
|
||
final_popup.close()
|
||
settings_popup.use_server()
|
||
await _settle()
|
||
|
||
|
||
# Writes a soundtrack folder and a prepared quiz with copies of the test song, and
|
||
# returns the folder they are in
|
||
static func _make_music_folders() -> String:
|
||
var root: String = OS.get_user_data_dir().path_join("ui_tour_music")
|
||
var song: PackedByteArray = FileAccess.get_file_as_bytes("res://tests/data/short.mp3")
|
||
var files: Array[String] = [
|
||
"Soundtracks/Chrono Trigger/Corridors of Time.mp3",
|
||
"Soundtracks/Final Fantasy VI/Dancing Mad.mp3",
|
||
"Soundtracks/Secret of Mana/Into the Thick of It.mp3",
|
||
"Musiktävling/01. Opening.mp3",
|
||
"Musiktävling/02. Ace Combat 7 - Daredevil.mp3",
|
||
]
|
||
for file: String in files:
|
||
var path: String = root.path_join(file)
|
||
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
||
FileAccess.open(path, FileAccess.WRITE).store_buffer(song)
|
||
return root
|
||
|
||
|
||
## Returns where every Control and Window in [param subtree] is on screen, keyed by its
|
||
## path from [param main]: [code]{"rect": [x, y, width, height], "visible": bool}[/code].
|
||
## Nodes added at runtime without a name are keyed by their index, e.g. [code]#2[/code],
|
||
## so the keys are the same on every run.
|
||
static func dump_layout(main: Node, subtree: Node) -> Dictionary:
|
||
var layout: Dictionary = {}
|
||
var path: String = String(main.get_path_to(subtree))
|
||
_dump_node(subtree, path, layout)
|
||
return layout
|
||
|
||
|
||
static func _dump_node(node: Node, path: String, layout: Dictionary) -> void:
|
||
if node is Control:
|
||
var rect: Rect2 = (node as Control).get_global_rect()
|
||
layout[path] = {"rect": _round_rect(rect), "visible": node.is_visible_in_tree()}
|
||
elif node is Window:
|
||
var window: Window = node
|
||
layout[path] = {
|
||
"rect": _round_rect(Rect2(window.position, window.size)),
|
||
"visible": window.visible,
|
||
}
|
||
for child: Node in node.get_children():
|
||
var key: String = "#%d" % child.get_index() if "@" in child.name \
|
||
else String(child.name)
|
||
_dump_node(child, key if path == "." else path + "/" + key, layout)
|
||
|
||
|
||
static func _round_rect(rect: Rect2) -> Array[float]:
|
||
return [snappedf(rect.position.x, 0.01), snappedf(rect.position.y, 0.01),
|
||
snappedf(rect.size.x, 0.01), snappedf(rect.size.y, 0.01)]
|
||
|
||
|
||
## Finds a node of the main window by [param node_name], whether it is a direct child
|
||
## of the main window or of its Quiz or TopBar scene.
|
||
func _node(node_name: String) -> Node:
|
||
for parent: Node in [_main, _main.get_node_or_null("Quiz"),
|
||
_main.get_node_or_null("TopBar")]:
|
||
if parent != null && parent.has_node(node_name):
|
||
return parent.get_node(node_name)
|
||
push_error("UiTour: no node named " + node_name)
|
||
return null
|
||
|
||
|
||
## Calls [param method] on the players panel, the quiz or the main window, whichever
|
||
## has it first.
|
||
func _call(method: String, args: Array = []) -> Variant:
|
||
for path: String in ["Quiz/PlayersPanel", "Quiz"]:
|
||
var target: Node = _main.get_node_or_null(path)
|
||
if target != null && target.has_method(method):
|
||
return target.callv(method, args)
|
||
return _main.callv(method, args)
|
||
|
||
|
||
# A blinking text cursor would be shown in some pictures and not in others
|
||
func _stop_caret_blink(node: Node) -> void:
|
||
if node is TextEdit || node is LineEdit:
|
||
node.caret_blink = false
|
||
for child: Node in node.get_children():
|
||
_stop_caret_blink(child)
|
||
|
||
|
||
func _server() -> FakeHttpTransport:
|
||
return MusicServerClient.transport
|
||
|
||
|
||
func _settle() -> void:
|
||
for i: int in 4:
|
||
await _main.get_tree().process_frame
|
||
|
||
|
||
func _add_player(player_name: String) -> void:
|
||
(_node("PlayersPanel").get_node("VBoxContainer/AddPlayerContainer/PlayerNameField") \
|
||
as LineEdit).text = player_name
|
||
_call("add_player")
|
||
|
||
|
||
func _player_row(index: int) -> PlayerRow:
|
||
return _node("PlayersPanel").get_node(
|
||
"VBoxContainer/ScrollContainer/PlayersListVBoxContainer").get_child(index)
|
||
|
||
|
||
## Stops the song so the time and progress bar look the same on every run.
|
||
func _freeze_music_player() -> void:
|
||
var music_player: MusicPlayer = _node("MusicPlayer")
|
||
music_player.audio_player.stop()
|
||
music_player.sound_player.stop()
|
||
music_player.progress_slider.value = 0
|
||
music_player.music_time_label.text = "00:00 / 00:01"
|
||
# The buttons' animations would otherwise be caught halfway, see ButtonBounce
|
||
for button: Button in [music_player.play_button, music_player.restart_button]:
|
||
if button.has_meta(&"button_bounce_tween"):
|
||
(button.get_meta(&"button_bounce_tween") as Tween).kill()
|
||
button.scale = Vector2.ONE
|
||
|
||
|
||
## The log starts each row with the time of day; use a fixed time instead.
|
||
func _fix_log_times() -> void:
|
||
var log_popup: BasePopup = _node("LogPopup")
|
||
for row: ListRow in log_popup.log_box.get_children():
|
||
row.label.text = "12:00:00 " + row.label.text.substr(9)
|
||
|
||
|
||
func _open_popup(popup_name: String) -> Node:
|
||
var popup: Node = _node(popup_name)
|
||
match popup_name:
|
||
"SyncPopup":
|
||
# Opening the popup starts a sync; show the empty popup instead
|
||
popup.open()
|
||
"CharacterSelectPopup":
|
||
popup.show_grid("Crono")
|
||
"WinnerPopup":
|
||
_show_winner_after_a_game(popup)
|
||
_:
|
||
popup.open()
|
||
return popup
|
||
|
||
|
||
## Shows the Winner popup after a whole game, so the graph has something to show. The
|
||
## scores are put back afterwards by [method _close_popup].
|
||
func _show_winner_after_a_game(popup: WinnerPopup) -> void:
|
||
_scores_before_win = []
|
||
for player: PlayerObject in Settings.player_array:
|
||
_scores_before_win.append(player.player_score)
|
||
var crono: PlayerObject = Settings.player_array[0]
|
||
var marle: PlayerObject = Settings.player_array[1]
|
||
var song_scorers: Array = []
|
||
for song: int in range(1, 17):
|
||
var scorers: Array[String] = []
|
||
if song in [2, 4, 5, 10, 11, 12, 13, 16]:
|
||
scorers.append(crono.id)
|
||
if song in [1, 3, 10, 14, 15]:
|
||
scorers.append(marle.id)
|
||
song_scorers.append(scorers)
|
||
crono.player_score = 8
|
||
marle.player_score = 5
|
||
_winning_score_before_win = Settings.winning_score
|
||
Settings.winning_score = 8
|
||
popup.show_winner(crono)
|
||
popup.show_history(ScoreHistory.from_songs(Settings.player_array, song_scorers))
|
||
|
||
|
||
func _close_popup(popup: Node) -> void:
|
||
if popup is WinnerPopup:
|
||
Settings.winning_score = _winning_score_before_win
|
||
for i: int in _scores_before_win.size():
|
||
Settings.player_array[i].player_score = _scores_before_win[i]
|
||
if popup is BasePopup:
|
||
popup.close()
|
||
else:
|
||
popup.visible = false
|