diff --git a/SyncPopup.tscn b/SyncPopup.tscn index 528c3fc..885aeb5 100644 --- a/SyncPopup.tscn +++ b/SyncPopup.tscn @@ -145,13 +145,13 @@ text = "SoundtracksRemoved" unique_name_in_owner = true layout_mode = 2 -[node name="CatchedErrorsVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=63733711] +[node name="CaughtErrorsVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=63733711] unique_name_in_owner = true visible = false layout_mode = 2 size_flags_horizontal = 3 -[node name="CatchedErrorsLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/CatchedErrorsVBox" unique_id=1784389644] +[node name="CaughtErrorsLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/CaughtErrorsVBox" unique_id=1784389644] layout_mode = 2 text = "CatchedErrors" horizontal_alignment = 1 diff --git a/about_popup.gd b/about_popup.gd index 78a5f20..eb9f30a 100644 --- a/about_popup.gd +++ b/about_popup.gd @@ -1,18 +1,8 @@ class_name AboutPopup extends BasePopup +## Shows the game version and what is new in each version. -@onready -var about_panel: Panel = %AboutPanel - -@onready -var version_label: Label = %VersionLabel - -@onready -var whats_new_label: Label = %WhatsNewLabel - - - -# Changelog as Dictionary with version as key and description as value +## What is new in each version, newest first. const WHATS_NEW: Dictionary = { "1.9.0": "Made improvements to the Search window Made small changes to the inspiration list scrolling @@ -51,15 +41,19 @@ const WHATS_NEW: Dictionary = { "0.7-Beta": "Can now hop between songs" } +@onready var about_panel: Panel = %AboutPanel +@onready var version_label: Label = %VersionLabel +@onready var whats_new_label: Label = %WhatsNewLabel + func open() -> void: super() version_label.text = Settings.version - + # Build changelog text with version headers var changelog_text: String = "" for version: String in WHATS_NEW.keys(): changelog_text += "\n" + version + "\n" changelog_text += WHATS_NEW[version] + "\n" - + whats_new_label.text = changelog_text diff --git a/base_popup.gd b/base_popup.gd index f384fe4..813c32d 100644 --- a/base_popup.gd +++ b/base_popup.gd @@ -6,6 +6,7 @@ extends Control ## override [method open] or [method close] and call [code]super()[/code]. ## The visible part of the popup. Clicks outside it close the popup. + @export var panel: Control diff --git a/character_card.gd b/character_card.gd index 6ed1170..4c31b0b 100644 --- a/character_card.gd +++ b/character_card.gd @@ -1,21 +1,30 @@ class_name CharacterCard extends Control +## One character in the character select popup: its picture and name. Clicking it picks +## the character. -@onready -var panel: Panel = $Panel - -@onready -var character_image: TextureRect = %CharacterTextureRect - -@onready -var character_name_label: Label = %CharacterName +## Emitted with the character's picture when the card is clicked. signal new_character_selected(img_texture: ImageTexture) +@onready var panel: Panel = $Panel +@onready var character_image: TextureRect = %CharacterTextureRect +@onready var character_name_label: Label = %CharacterName + + +func _input(event: InputEvent) -> void: + if visible && Settings.character_select_open: + if event is InputEventMouseButton && event.is_pressed() \ + && event.get_button_index() == MOUSE_BUTTON_LEFT: + var local_event: InputEvent = make_input_local(event) + if Rect2(Vector2(0, 0), panel.size).has_point(local_event.position): + GameLog.debug(character_name_label.text) + new_character_selected.emit(character_image.texture) + + +## Shows [param character_name] and fetches its picture from the server. func set_character_name(character_name: String) -> void: panel.size = Vector2(240, 145) - #margin.size = Vector2(240, 145) - #margin set margin 15, 10, 15, 0 character_name_label.text = character_name.split(".")[0] character_name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER var image_fetched: Callable = func image_fetched(image: PackedByteArray) -> void: @@ -37,11 +46,3 @@ func set_character_name(character_name: String) -> void: character_image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED character_image.texture = new_image MusicServerClient.fetch_character_image(character_name, image_fetched) - -func _input(event: InputEvent) -> void: - if self.visible == true && Settings.character_select_open == true: - if event is InputEventMouseButton && event.is_pressed() && event.get_button_index() == MOUSE_BUTTON_LEFT: - var evLocal: InputEvent = make_input_local(event) - if Rect2(Vector2(0, 0), panel.size).has_point(evLocal.position): - GameLog.debug(character_name_label.text) - new_character_selected.emit(character_image.texture) diff --git a/character_select_popup.gd b/character_select_popup.gd index 6f70a0e..79d0162 100644 --- a/character_select_popup.gd +++ b/character_select_popup.gd @@ -1,35 +1,31 @@ class_name CharacterSelectPopup extends BasePopup -@onready -var character_panel: Panel = %CharacterPanel - -@onready -var title_label: Label = %TitleLabel - -@onready -var character_grid: GridContainer = %CharacterGridContainer - -@onready -var close_timer: Timer = %CloseTimer +## Emitted with the picture of the character that was picked. +signal character_selected(texture: ImageTexture) var can_select_character: bool = false - -signal new_new_character_selected(file_name: String) - var character: PackedScene = preload("res://CharacterCard.tscn") +@onready var character_panel: Panel = %CharacterPanel +@onready var title_label: Label = %TitleLabel +@onready var character_grid: GridContainer = %CharacterGridContainer +@onready var close_timer: Timer = %CloseTimer + + func _ready() -> void: close_timer.timeout.connect(_on_close_timeout) + func _on_close_timeout() -> void: GameLog.debug("_on_close_timeout!") Settings.character_select_open = false close_timer.stop() + func load_characters() -> void: GameLog.debug("load_characters") - var fetch_character_list: Callable = func fetch_character_list(list) -> void: + var fetch_character_list: Callable = func fetch_character_list(list: Variant) -> void: GameLog.debug("fetch_character_list") Settings.delete_children(character_grid) if typeof(list) == TYPE_ARRAY: @@ -40,9 +36,10 @@ func load_characters() -> void: character_box.custom_minimum_size = Vector2(235, 145) character_box.size = Vector2(235, 145) character_box.set_character_name(character_name) - character_box.connect("new_character_selected", _on_character_selected) + character_box.new_character_selected.connect(_on_character_selected) MusicServerClient.fetch_character_list(fetch_character_list) + func show_grid(player_name: String) -> void: GameLog.debug("show_grid") title_label.text = "Select character for " + player_name @@ -51,16 +48,18 @@ func show_grid(player_name: String) -> void: can_select_character = true Settings.character_select_open = true + func close() -> void: super() Settings.character_select_open = false + func _on_character_selected(texture: ImageTexture) -> void: GameLog.debug("_on_character_selected") # Not close(): character_select_open stays true until close_timer fires, so the click # that picked the character does not also select a song behind it - self.visible = false + visible = false if can_select_character: - new_new_character_selected.emit(texture) + character_selected.emit(texture) close_timer.start() can_select_character = false diff --git a/http_transport.gd b/http_transport.gd index d497b10..63cc4ad 100644 --- a/http_transport.gd +++ b/http_transport.gd @@ -6,8 +6,8 @@ extends RefCounted ## Seconds before a request is given up and reported as failed. This covers the whole ## download, so it is generous enough for long songs on a slow connection. -var timeout_seconds: float = 120.0 +var timeout_seconds: float = 120.0 var _owner: Node diff --git a/inspiration_list.gd b/inspiration_list.gd index 40a08ef..8b3bb92 100644 --- a/inspiration_list.gd +++ b/inspiration_list.gd @@ -4,35 +4,36 @@ extends ScrollContainer ## by itself, at the speed set in the settings. ## The soundtracks shown, in order. + var soundtracks: Array = [] # Scroll direction: 1 = down, -1 = up var scroll_direction: int = 1 -@onready -var _rows: VBoxContainer = $VBoxContainer +@onready var _rows: VBoxContainer = $VBoxContainer + -# Called when the node enters the scene tree for the first time. func _ready() -> void: scroll_direction = 1 -# Called every frame. 'delta' is the elapsed time since the previous frame. + func _process(delta: float) -> void: var speed_pixels_per_second: float = float(Settings.inspiration_list_speed) * 60.0 var scroll_amount: float = speed_pixels_per_second * delta * float(scroll_direction) - var scroll_bar = get_v_scroll_bar() + var scroll_bar: VScrollBar = get_v_scroll_bar() # Don't scroll if list is empty or too short if scroll_bar.max_value <= 0: return - var current_pos = scroll_bar.value + var current_pos: float = scroll_bar.value if scroll_direction == 1: # Scrolling down if current_pos + scroll_bar.page < scroll_bar.max_value: - scroll_bar.value = min(current_pos + scroll_amount, scroll_bar.max_value - scroll_bar.page) + scroll_bar.value = minf(current_pos + scroll_amount, + scroll_bar.max_value - scroll_bar.page) else: # Reached bottom, reverse direction scroll_direction = -1 @@ -40,7 +41,7 @@ func _process(delta: float) -> void: else: # Scrolling up if current_pos > 0: - scroll_bar.value = max(current_pos + scroll_amount, 0) + scroll_bar.value = maxf(current_pos + scroll_amount, 0) else: # Reached top, reverse direction scroll_direction = 1 @@ -50,7 +51,7 @@ func _process(delta: float) -> void: ## Fetches every soundtrack from the server and shows them. func load_from_server() -> void: GameLog.debug("load_from_server") - var populate_list: Callable = func populate_list(array) -> void: + var populate_list: Callable = func populate_list(array: Variant) -> void: if typeof(array) == TYPE_ARRAY: show_soundtracks(array) else: diff --git a/list_row.gd b/list_row.gd index a201fdd..9453b88 100644 --- a/list_row.gd +++ b/list_row.gd @@ -16,6 +16,8 @@ var label: Label ## The item's text. var text: String: + + get: return label.text diff --git a/log_popup.gd b/log_popup.gd index 3dbd90c..b2be63a 100644 --- a/log_popup.gd +++ b/log_popup.gd @@ -1,19 +1,20 @@ class_name LogPopup extends BasePopup - -@onready -var log_panel: Panel = %LogPanel - -@onready -var log_box: VBoxContainer = %LogVBoxContainer +## The game log: a row with the time for each thing that happens in the quiz, such as a +## player getting a point. Opened with Alt+L. var log_rows: Array +@onready var log_panel: Panel = %LogPanel +@onready var log_box: VBoxContainer = %LogVBoxContainer + + func add_log_row(text: String) -> void: var log_row: String = get_time() + text log_rows.append(log_row) log_box.add_child(make_log_label(log_row)) + func make_log_label(row: String) -> Label: var log_label: Label = Label.new() log_label.add_theme_font_size_override("font_size", 20) @@ -21,6 +22,7 @@ func make_log_label(row: String) -> Label: log_label.autowrap_mode = TextServer.AUTOWRAP_WORD return log_label + func get_time() -> String: var time: Dictionary = Time.get_time_dict_from_system() return "%02d:%02d:%02d " % [time.hour, time.minute, time.second] diff --git a/main_window.gd b/main_window.gd index ebef216..bb44da9 100644 --- a/main_window.gd +++ b/main_window.gd @@ -5,39 +5,20 @@ extends Control ## Everything that is not part of a quiz lives here, so other views can be added next ## to the quiz later. -@onready -var top_bar: TopBar = $TopBar - -@onready -var quiz: Quiz = $Quiz - -@onready -var search_popup: SearchPopup = $SearchPopup - -@onready -var sync_popup: SyncPopup = %SyncPopup - -@onready -var shortcuts_popup: ShortcutsPopup = %ShortcutsPopup - -@onready -var qr_popup: QRPopup = %QRPopup - -@onready -var about_popup: AboutPopup = %AboutPopup - -@onready -var log_popup: LogPopup = %LogPopup - -@onready -var statistic_popup: PopupPanel = $StatisticsPopupPanel - -@onready -var statistic_label: Label = %StatisticsLabel - ## Dims the window behind an open popup and keeps clicks from reaching the buttons under it. var popup_backdrop: PopupBackdrop +@onready var top_bar: TopBar = $TopBar +@onready var quiz: Quiz = $Quiz +@onready var search_popup: SearchPopup = $SearchPopup +@onready var sync_popup: SyncPopup = %SyncPopup +@onready var shortcuts_popup: ShortcutsPopup = %ShortcutsPopup +@onready var qr_popup: QRPopup = %QRPopup +@onready var about_popup: AboutPopup = %AboutPopup +@onready var log_popup: LogPopup = %LogPopup +@onready var statistic_popup: PopupPanel = $StatisticsPopupPanel +@onready var statistic_label: Label = %StatisticsLabel + func _ready() -> void: popup_backdrop = PopupBackdrop.add_to(self, _popups()) @@ -50,8 +31,8 @@ func _ready() -> void: top_bar.about_pressed.connect(show_about) top_bar.qr_pressed.connect(show_qr) - sync_popup.connect("sync_started", _on_sync_started) - sync_popup.connect("sync_finished", _on_sync_finished) + sync_popup.sync_started.connect(_on_sync_started) + sync_popup.sync_finished.connect(_on_sync_finished) quiz.server_changed.connect(search_popup.get_list_of_soundtracks) quiz.event_logged.connect(log_popup.add_log_row) @@ -63,9 +44,9 @@ func _input(event: InputEvent) -> void: if event.alt_pressed && event.keycode == KEY_ENTER: GameLog.debug("Alt + Enter pressed") Settings.fullscreen = !Settings.fullscreen - if Settings.fullscreen == true: + if Settings.fullscreen: DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN) - if Settings.fullscreen == false: + if !Settings.fullscreen: DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) if event.alt_pressed && event.keycode == KEY_K: shortcuts_popup.toggle() diff --git a/music_list_panel.gd b/music_list_panel.gd index bf01f6e..87d3d12 100644 --- a/music_list_panel.gd +++ b/music_list_panel.gd @@ -3,12 +3,12 @@ extends PanelContainer ## The list of songs in the playlist, one row per song. ## 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://SongRow.tscn") -@onready -var _rows: VBoxContainer = %MusicList +@onready var _rows: VBoxContainer = %MusicList ## Shows the songs in the playlist again, so it shows their latest state. diff --git a/music_player.gd b/music_player.gd index deb2cde..dffc0cc 100644 --- a/music_player.gd +++ b/music_player.gd @@ -3,76 +3,59 @@ extends Control ## The bar that plays the songs, with play, restart, progress and volume, and the ## auto repeat switch under it. -# LOC 20/1-24: 136 -# LOC 12/6-24: 170 - -@onready -var play_button: TextureButton = %PlayTextureButton - -@onready -var restart_button: TextureButton = %RestartTextureButton - -@onready -var audio_player: AudioStreamPlayer = $AudioStreamPlayer - -@onready -var sound_player: AudioStreamPlayer = $AudioSoundPlayer - -@onready -var sound_effect_player: AudioStreamPlayer = $AudioSoundEffectPlayer - -@onready -var progress_slider: HSlider = %MusicPlayerSlider - -@onready -var volume_slider: VolumeSlider = %VolumeSlider - -@onready -var music_time_label: Label = %MusicTimeLabel - -@onready -var auto_repeat_button: CheckButton = $RepeatSongCheckButton +signal update_song_list +signal play_next_song var play_icon: Texture = preload("res://icons/play_icon_light.svg") var pause_icon: Texture = preload("res://icons/pause_icon_light.svg") - var songs: Array = [] - var is_changing: bool = false var playback_position: float var stream: AudioStream var song_finished: bool = false -signal update_song_list -signal play_next_song +@onready var play_button: TextureButton = %PlayTextureButton +@onready var restart_button: TextureButton = %RestartTextureButton +@onready var audio_player: AudioStreamPlayer = $AudioStreamPlayer +@onready var sound_player: AudioStreamPlayer = $AudioSoundPlayer +@onready var sound_effect_player: AudioStreamPlayer = $AudioSoundEffectPlayer +@onready var progress_slider: HSlider = %MusicPlayerSlider +@onready var volume_slider: VolumeSlider = %VolumeSlider +@onready var music_time_label: Label = %MusicTimeLabel +@onready var auto_repeat_button: CheckButton = $RepeatSongCheckButton + -# Called when the node enters the scene tree for the first time. func _ready() -> void: play_button.pressed.connect(play_or_pause) restart_button.pressed.connect(restart) progress_slider.drag_started.connect(_on_drag_started) progress_slider.drag_ended.connect(_on_drag_ended) - + audio_player.finished.connect(_on_finished) auto_repeat_button.pressed.connect(_on_auto_repeat_pressed) + func _on_auto_repeat_pressed() -> void: Settings.auto_repeat_song = !Settings.auto_repeat_song + func _process(_delta: float) -> void: if audio_player.has_stream_playback() && !is_changing && !audio_player.stream_paused: - if (!Settings.hide_beginning || audio_player.get_playback_position() > Settings.hide_for_seconds): + if !Settings.hide_beginning \ + || audio_player.get_playback_position() > Settings.hide_for_seconds: progress_slider.value = audio_player.get_playback_position() if stream != null: music_time_label.text = format_text(progress_slider.value, stream.get_length()) else: music_time_label.text = format_text(progress_slider.value, 0.0) + func format_time(time: float) -> String: var total_seconds: int = roundi(time) @warning_ignore("integer_division") return "%02d:%02d" % [total_seconds / 60, total_seconds % 60] + func format_text(part: float, total: float) -> String: if (Settings.hide_beginning && part < Settings.hide_for_seconds) && Settings.hide_length: return "??:?? / ??:??" @@ -83,6 +66,7 @@ func format_text(part: float, total: float) -> String: else: return format_time(part) + " / " + format_time(total) + func play_or_pause() -> void: if song_finished: restart() @@ -100,11 +84,13 @@ func play_or_pause() -> void: else: pause() + func pause() -> void: audio_player.stream_paused = true playback_position = audio_player.get_playback_position() play_button.texture_normal = play_icon + func restart() -> void: audio_player.stop() audio_player.stream_paused = false @@ -114,7 +100,8 @@ func restart() -> void: audio_player.seek(playback_position) play_button.texture_normal = pause_icon audio_player.play() - + + func _on_finished() -> void: play_button.texture_normal = play_icon song_finished = true @@ -122,26 +109,31 @@ func _on_finished() -> void: play_next_song.emit() if Settings.auto_repeat_song: restart() - + + func _on_drag_started() -> void: is_changing = true + func _on_drag_ended(_changed: bool) -> void: audio_player.seek(progress_slider.value) playback_position = progress_slider.value is_changing = false + func seek(new_position: float) -> void: progress_slider.value += new_position is_changing = true audio_player.seek(progress_slider.value) playback_position = progress_slider.value is_changing = false - + + func change_volume(value: float) -> void: volume_slider.value += value volume_slider.change_volume(volume_slider.value) + func _on_point_triggered(point: String) -> void: if point == "first": var value: int = randi_range(0, 10) @@ -153,11 +145,13 @@ func _on_point_triggered(point: String) -> void: play_sound(preload("res://sounds/intro_short.mp3")) elif point == "match": play_sound(preload("res://sounds/sound0.mp3")) - + + func play_sound_effect(sound_effect_name: AudioStream) -> void: sound_effect_player.stream = sound_effect_name sound_effect_player.play() + func play_sound(sound_name: AudioStream) -> void: sound_player.stream = sound_name audio_player.stop() @@ -165,9 +159,8 @@ func play_sound(sound_name: AudioStream) -> void: song_finished = true play_button.texture_normal = play_icon progress_slider.value = 0 - #if stream != null: - # music_time_label.text = format_text(progress_slider.value, stream.get_length()) - + + func play_song(song: AudioStream) -> void: audio_player.stream = song audio_player.play() @@ -183,6 +176,7 @@ func play_song(song: AudioStream) -> void: progress_slider.tick_count = round(stream.get_length() / 60) music_time_label.text = format_text(progress_slider.value, stream.get_length()) + func play_song_object(song_object_no: int) -> void: GameLog.debug("play_song_object") if audio_player.is_playing(): @@ -210,6 +204,7 @@ func play_song_object(song_object_no: int) -> void: Playlist.song_is_playing(song_object_no) update_song_list.emit() + func get_sound_test_song() -> void: var test_sound: Callable = func test_sound(body: PackedByteArray) -> void: var sound: AudioStream = AudioStreamMP3.new() @@ -220,12 +215,14 @@ func get_sound_test_song() -> void: ##### LOCAL var local_path: String = '/Users/sebastian/ResilioSync/Sorterat_test/Metal Gear Solid 4 - Guns of the Patriots/2-16 Metal Gear Saga.mp3' + func load_mp3(_path: String) -> AudioStream: var file: FileAccess = FileAccess.open(_path, FileAccess.READ) var sound: AudioStream = AudioStreamMP3.new() sound.data = file.get_buffer(file.get_length()) return sound + func play_local_song() -> void: if songs: local_path = songs[0] @@ -233,5 +230,6 @@ func play_local_song() -> void: GameLog.debug(FileAccess.file_exists(local_path)) play_sound(load_mp3(local_path)) + func sound_test_local() -> void: play_sound(preload("res://sounds/01. Opening.mp3")) diff --git a/music_server_client.gd b/music_server_client.gd index 73fdc51..3f83292 100644 --- a/music_server_client.gd +++ b/music_server_client.gd @@ -7,6 +7,7 @@ extends Node ## without a running server. ## Sends the actual HTTP requests. Swap it out in tests. + var transport: HttpTransport diff --git a/player_name_field.gd b/player_name_field.gd index 206938a..c5f6f95 100644 --- a/player_name_field.gd +++ b/player_name_field.gd @@ -1,19 +1,28 @@ class_name PlayerNameField extends TextEdit +## The field for a new player's name. It keeps to one line of at most +## [constant MAX_NAME_LENGTH] characters, and reports Enter so the player can be added. + +## Emitted when Enter is pressed while the field has focus. signal enter_key_pressed + +## Emitted when Alt+A is pressed. signal close_pressed -@export -var LIMIT: int = 10 -var current_text = '' -var cursor_line = 0 -var cursor_column = 0 +## The longest name a player can have. +const MAX_NAME_LENGTH: int = 10 -func _ready(): +var current_text: String = "" +var cursor_line: int = 0 +var cursor_column: int = 0 + + +func _ready() -> void: text_changed.connect(_on_text_changed) -func _input(event): + +func _input(event: InputEvent) -> void: if event is InputEventKey and event.pressed: if event.alt_pressed && event.keycode == KEY_A: close_pressed.emit() @@ -21,15 +30,16 @@ func _input(event): if event.keycode == KEY_ENTER || event.keycode == KEY_KP_ENTER: enter_key_pressed.emit() -func _on_text_changed(): + +func _on_text_changed() -> void: if text.contains("\n"): text = text.replace("\n", "") - - var new_text : String = text - if new_text.length() > LIMIT: + + var new_text: String = text + if new_text.length() > MAX_NAME_LENGTH: text = current_text # when replacing the text, the cursor will get moved to the beginning of the - # text, so move it back to where it was + # text, so move it back to where it was set_caret_line(cursor_line) set_caret_column(cursor_column) diff --git a/player_object.gd b/player_object.gd index d885d40..b569bc6 100644 --- a/player_object.gd +++ b/player_object.gd @@ -1,40 +1,42 @@ -extends RefCounted class_name PlayerObject +extends RefCounted +## A player in the quiz: name, score and character. The players are kept in +## [code]Settings.player_array[/code]. -signal first_point_triggerd -signal match_point_triggerd -signal winner_triggerd +signal first_point_triggered +signal match_point_triggered +signal winner_triggered signal point_given_sound signal player_point_given signal player_point_taken -## Unique id that never changes, unlike the player's position in Settings.player_array. -var id: String = _generate_uuid() - -@export -var player_name: String - -@export -var player_score: int - -var is_first_point: bool = true - ## Picture shown until the player picks a character. const NO_CHARACTER_PATH: String = "res://noCharacter.png" +@export var player_name: String +@export var player_score: int + +## Unique id that never changes, unlike the player's position in Settings.player_array. +var id: String = _generate_uuid() + +var is_first_point: bool = true var character: Texture = load(NO_CHARACTER_PATH) + func _init(new_name: String) -> void: player_name = new_name player_score = 0 + ## Returns a random version 4 UUID, e.g. "3f2b8c1e-9a4d-4e7f-b2c6-5d8e1f0a7b93". static func _generate_uuid() -> String: var bytes: PackedByteArray = Crypto.new().generate_random_bytes(16) bytes[6] = (bytes[6] & 0x0f) | 0x40 bytes[8] = (bytes[8] & 0x3f) | 0x80 var hex: String = bytes.hex_encode() - return "%s-%s-%s-%s-%s" % [hex.substr(0, 8), hex.substr(8, 4), hex.substr(12, 4), hex.substr(16, 4), hex.substr(20, 12)] + return "%s-%s-%s-%s-%s" % [hex.substr(0, 8), hex.substr(8, 4), hex.substr(12, 4), + hex.substr(16, 4), hex.substr(20, 12)] + func add_point() -> void: if player_score < Settings.winning_score: @@ -46,14 +48,15 @@ func add_point() -> void: # With a low winning score the first point can also be match point or the win, # so the more important event is checked first if player_score == Settings.winning_score: - winner_triggerd.emit() + winner_triggered.emit() elif player_score == Settings.winning_score - 1: - match_point_triggerd.emit() + match_point_triggered.emit() elif first_point: - first_point_triggerd.emit() + first_point_triggered.emit() else: point_given_sound.emit() + func minus_point() -> void: if player_score > 0: player_score -= 1 @@ -61,6 +64,7 @@ func minus_point() -> void: if player_score == 0: is_first_point = true + func reset_points() -> void: player_score = 0 is_first_point = true diff --git a/player_row.gd b/player_row.gd index f8cf51c..3fbfe25 100644 --- a/player_row.gd +++ b/player_row.gd @@ -1,21 +1,6 @@ class_name PlayerRow extends Control -@onready -var player_name_field: Label = %Name - -@onready -var add_button: Button = %AddPoint - -@onready -var minus_button: Button = %RemovePoint - -@onready -var character: TextureButton = %Character - -@onready -var remove_player: TextureButton = %RemovePlayer - signal change_character_clicked signal player_removed @@ -26,7 +11,13 @@ var player_object: PlayerObject ## gives this player a point and Ctrl+Alt+ takes one away. var list_index: int -# Called when the node enters the scene tree for the first time. +@onready var player_name_field: Label = %Name +@onready var add_button: Button = %AddPoint +@onready var minus_button: Button = %RemovePoint +@onready var character: TextureButton = %Character +@onready var remove_player: TextureButton = %RemovePlayer + + func _ready() -> void: add_button.pressed.connect(add_point) add_button.set_shortcut(create_button_shortcut(list_index, false)) @@ -37,42 +28,51 @@ func _ready() -> void: update_score() set_player_character() + func _process(delta: float) -> void: remove_player.visible = Settings.edit_players + func create_button_shortcut(index: int, ctrl_pressed: bool) -> Shortcut: var button_shortcut: Shortcut = Shortcut.new() - var button_event : InputEventKey = InputEventKey.new() + var button_event: InputEventKey = InputEventKey.new() button_event.keycode = KEY_1 + index button_event.alt_pressed = true button_event.ctrl_pressed = ctrl_pressed button_shortcut.events.append(button_event) return button_shortcut + func add_point() -> void: player_object.add_point() update_score() + func minus_point() -> void: player_object.minus_point() update_score() - + + func change_character() -> void: change_character_clicked.emit() + func new_character_selected(character_texture: ImageTexture) -> void: GameLog.debug("Back in player list with new") player_object.character = character_texture set_player_character() + func set_player_character() -> void: character.custom_minimum_size = Vector2(80, 40) character.ignore_texture_size = true character.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT_CENTERED character.texture_normal = player_object.character + func update_score() -> void: if OS.has_feature("debug"): - player_name_field.text = str(list_index + 1) + " " + player_object.player_name + ": " + str(player_object.player_score) + player_name_field.text = "%d %s: %d" % [list_index + 1, player_object.player_name, + player_object.player_score] else: player_name_field.text = player_object.player_name + ": " + str(player_object.player_score) diff --git a/players_panel.gd b/players_panel.gd index 442b6bf..ee3fd01 100644 --- a/players_panel.gd +++ b/players_panel.gd @@ -7,6 +7,7 @@ extends PanelContainer ## Emitted when a player has been added to [code]Settings.player_array[/code], before ## the list shows it. + signal player_added(player_object: PlayerObject) ## Emitted when a player has been removed from [code]Settings.player_array[/code]. @@ -17,26 +18,17 @@ signal change_character_requested(row: PlayerRow, player_name: String) var _player_row: PackedScene = preload("res://PlayerRow.tscn") -@onready -var add_players_button: TextureButton = %AddPlayersButton - -@onready -var add_player_container: HBoxContainer = %AddPlayerContainer - -@onready -var add_player_button: Button = %AddPlayerButton - -@onready -var new_player_name_field: TextEdit = %PlayerNameField - -@onready -var players_list_container: VBoxContainer = %PlayersListVBoxContainer +@onready var add_players_button: TextureButton = %AddPlayersButton +@onready var add_player_container: HBoxContainer = %AddPlayerContainer +@onready var add_player_button: Button = %AddPlayerButton +@onready var new_player_name_field: PlayerNameField = %PlayerNameField +@onready var players_list_container: VBoxContainer = %PlayersListVBoxContainer func _ready() -> void: add_players_button.pressed.connect(add_players) add_player_button.pressed.connect(add_player) - new_player_name_field.connect("enter_key_pressed", add_player) + new_player_name_field.enter_key_pressed.connect(add_player) ## Shows or hides the row for adding players, and the buttons for removing them. @@ -51,7 +43,8 @@ func add_players() -> void: ## already as many players as there can be. func add_player() -> void: GameLog.debug("add_player") - if new_player_name_field.text != "" && Settings.player_array.size() < Settings.MAX_NUMBER_OF_PLAYERS: + if new_player_name_field.text != "" \ + && Settings.player_array.size() < Settings.MAX_NUMBER_OF_PLAYERS: var new_player_object: PlayerObject = PlayerObject.new(new_player_name_field.text) new_player_name_field.text = "" Settings.player_array.append(new_player_object) @@ -72,8 +65,9 @@ func reload() -> void: new_player.player_object = player_object new_player.list_index = counter - new_player.connect("change_character_clicked", _on_change_character_clicked.bind(new_player, player_object.player_name)) - new_player.connect("player_removed", _on_player_removed.bind(player_object)) + new_player.change_character_clicked.connect( + _on_change_character_clicked.bind(new_player, player_object.player_name)) + new_player.player_removed.connect(_on_player_removed.bind(player_object)) players_list_container.add_child(new_player) counter += 1 diff --git a/playlist.gd b/playlist.gd index 08dec57..033283f 100644 --- a/playlist.gd +++ b/playlist.gd @@ -1,26 +1,32 @@ extends Node +## The songs of the current quiz, in the order they were added, and which of them are +## played, answered and playing. +## +## Registered as the [code]Playlist[/code] autoload. var song_object_array: Array[SongObject] = [] var latest_played_song: int = 0 var currently_playing_song: int = -1 - var first_song_downloaded: bool = false var first_song_played: bool = false - var number_of_tracks_to_preload: int = 1 var is_song_downloading: bool = false + func get_next_song() -> SongObject: return song_object_array[latest_played_song] + func set_latest_played_song() -> void: latest_played_song += 1 if latest_played_song > song_object_array.size() - 1: latest_played_song = song_object_array.size() - 1 + func update_last_played_song() -> void: latest_played_song = song_object_array.size() + func get_preloaded_songs() -> int: var number_of_tracks_preloaded: int = 0 for song: SongObject in song_object_array: @@ -28,9 +34,11 @@ func get_preloaded_songs() -> int: number_of_tracks_preloaded += 1 return number_of_tracks_preloaded + func should_preload_more_songs() -> bool: return number_of_tracks_to_preload > get_preloaded_songs() + func song_is_answered(song_no: int = currently_playing_song) -> void: # Nothing is playing yet (-1); a negative index would answer the last, unplayed song if song_no < 0 || song_no >= song_object_array.size(): @@ -41,9 +49,11 @@ func song_is_answered(song_no: int = currently_playing_song) -> void: GameLog.debug("add to stats: ", song_no) MusicServerClient.mark_song_played(song_no) + func add_to_playlist(new_song_object: SongObject) -> void: song_object_array.append(new_song_object) + func get_playlist() -> Array[SongObject]: if Settings.is_debug: return song_object_array @@ -54,46 +64,59 @@ func get_playlist() -> Array[SongObject]: temp_list.append(s) return temp_list + func reset_playlist() -> void: currently_playing_song = -1 latest_played_song = 0 clear_song_list() - + + func clear_song_list() -> void: song_object_array.clear() + func unset_is_playing() -> void: for song_object: SongObject in song_object_array: song_object.is_playing = false + func set_currently_playing_song(value: int) -> void: currently_playing_song = value + func get_current_soundtrack_title() -> String: return song_object_array[currently_playing_song].get_soundtrack_title() + func get_current_song_title() -> String: return song_object_array[currently_playing_song].get_song_title() + func add_point(player_id: String) -> void: song_object_array[currently_playing_song].add_point(player_id) + func remove_point(player_id: String) -> void: song_object_array[currently_playing_song].remove_point(player_id) + ## Removes a player's points from every song. func remove_player_from_list(player_id: String) -> void: for song_object: SongObject in song_object_array: song_object.remove_point(player_id) + func get_song(song_no: int) -> AudioStream: return song_object_array[song_no].song + func add_song_to_song_object(song_no: int, sound: AudioStream) -> void: song_object_array[song_no].song = sound + func song_has_played(song_no: int = latest_played_song) -> void: song_object_array[song_no].has_played = true + func song_is_playing(song_no: int = latest_played_song) -> void: song_object_array[song_no].is_playing = true diff --git a/qr_popup.gd b/qr_popup.gd index 33df3b9..42b3f26 100644 --- a/qr_popup.gd +++ b/qr_popup.gd @@ -1,27 +1,25 @@ class_name QRPopup extends BasePopup +## Shows a QR code with the address of the search page on the MusicServer, so players +## can search for soundtracks on their phones. -@onready -var qr_panel: Panel = $QRPanel - -@onready -var qr_code_rect: QRCodeRect = %QRCodeRect - -@onready -var url_label: Label = %URLLabel +@onready var qr_panel: Panel = $QRPanel +@onready var qr_code_rect: QRCodeRect = %QRCodeRect +@onready var url_label: Label = %URLLabel func _ready() -> void: qr_code_rect.light_module_color = Color.WHITE qr_code_rect.dark_module_color = Color.BLACK qr_code_rect.quiet_zone_size = 4 - + # Generate QR code for the search URL var url: String = Settings.default_path + "/search" qr_code_rect.data = url url_label.text = qr_code_rect.data GameLog.debug("QR code generated for: ", url) + func open() -> void: super() # Regenerate QR code in case settings changed diff --git a/quiz.gd b/quiz.gd index aed4fa3..2ae7907 100644 --- a/quiz.gd +++ b/quiz.gd @@ -9,54 +9,13 @@ extends Control ## Emitted when another MusicServer is picked in the settings, after the quiz has ## reloaded from it. + signal server_changed ## Emitted with a line for the game log, e.g. when a player gets a point. signal event_logged(text: String) -@onready -var open_button: Button = $Open - -@onready -var file_dialog: FileDialog = $FileDialog - -@onready -var inspiration_list: InspirationList = $InspirationList - -@onready -var now_playing: NowPlaying = $NowPlaying - -@onready -var players_panel: PlayersPanel = $PlayersPanel - -@onready -var toolbar: QuizToolbar = $QuizToolbar - -@onready -var settings_popup: SettingsPopup = $SettingsPopup - -@onready -var winner_popup: WinnerPopup = %WinnerPopup - -@onready -var song_controls: SongControls = $SongControls - -@onready -var song_loader: SongLoader = $SongLoader - -@onready -var music_list_panel: MusicListPanel = $MusicListPanel - -@onready -var character_select_popup: CharacterSelectPopup = %CharacterSelectPopup - -@onready -var music_player: MusicPlayer = $MusicPlayer - -@onready -var debug_label: Label = $DebugLabel - -var songs: Array= [] +var songs: Array = [] var current_player_row: PlayerRow ## True while statistics are off because a player won, until points are reset. @@ -65,6 +24,21 @@ var stats_paused_by_win: bool = false ## Dims the window behind an open popup and keeps clicks from reaching the buttons under it. var popup_backdrop: PopupBackdrop +@onready var open_button: Button = $Open +@onready var file_dialog: FileDialog = $FileDialog +@onready var inspiration_list: InspirationList = $InspirationList +@onready var now_playing: NowPlaying = $NowPlaying +@onready var players_panel: PlayersPanel = $PlayersPanel +@onready var toolbar: QuizToolbar = $QuizToolbar +@onready var settings_popup: SettingsPopup = $SettingsPopup +@onready var winner_popup: WinnerPopup = %WinnerPopup +@onready var song_controls: SongControls = $SongControls +@onready var song_loader: SongLoader = $SongLoader +@onready var music_list_panel: MusicListPanel = $MusicListPanel +@onready var character_select_popup: CharacterSelectPopup = %CharacterSelectPopup +@onready var music_player: MusicPlayer = $MusicPlayer +@onready var debug_label: Label = $DebugLabel + func _ready() -> void: popup_backdrop = PopupBackdrop.add_to(self, _popups()) @@ -83,14 +57,14 @@ func _ready() -> void: song_loader.song_list_loaded.connect(update_song_list) song_loader.song_added.connect(_on_song_added) - character_select_popup.connect("new_new_character_selected", _on_new_character_selected) + character_select_popup.character_selected.connect(_on_new_character_selected) music_list_panel.song_clicked.connect(song_object_clicked) players_panel.player_added.connect(_on_player_added) players_panel.player_removed.connect(_on_player_removed) players_panel.change_character_requested.connect(_on_change_character_requested) - music_player.connect("play_next_song", play_next_song) - music_player.connect("update_song_list", update_song_list) - settings_popup.connect("server_changed", _on_server_changed) + music_player.play_next_song.connect(play_next_song) + music_player.update_song_list.connect(update_song_list) + settings_popup.server_changed.connect(_on_server_changed) settings_popup.visibility_changed.connect(_on_settings_popup_visibility_changed) open_button.pressed.connect(open) @@ -192,7 +166,7 @@ func update_song_list() -> void: func song_object_clicked(song_no: int) -> void: GameLog.debug("Clicked Song Object: ", Playlist.song_object_array[song_no].to_string()) - if Settings.character_select_open == false: + if !Settings.character_select_open: if Playlist.get_song(song_no) == null: GameLog.debug("has no song, need to download") var song_downloaded: Callable = func song_downloaded(body: PackedByteArray) -> void: @@ -233,12 +207,12 @@ func show_settings() -> void: # Each new player's points reach the music player, the log and the winner popup. func _on_player_added(new_player_object: PlayerObject) -> void: - new_player_object.connect("first_point_triggerd", music_player._on_point_triggered.bind("first")) - new_player_object.connect("match_point_triggerd", music_player._on_point_triggered.bind("match")) - new_player_object.connect("winner_triggerd", _on_player_won.bind(new_player_object)) - new_player_object.connect("point_given_sound", _on_make_point_given_sound) - new_player_object.connect("player_point_given", _on_point_given.bind(new_player_object)) - new_player_object.connect("player_point_taken", _on_point_taken.bind(new_player_object)) + new_player_object.first_point_triggered.connect(music_player._on_point_triggered.bind("first")) + new_player_object.match_point_triggered.connect(music_player._on_point_triggered.bind("match")) + new_player_object.winner_triggered.connect(_on_player_won.bind(new_player_object)) + new_player_object.point_given_sound.connect(_on_make_point_given_sound) + new_player_object.player_point_given.connect(_on_point_given.bind(new_player_object)) + new_player_object.player_point_taken.connect(_on_point_taken.bind(new_player_object)) func _on_point_given(player_object: PlayerObject) -> void: @@ -293,6 +267,7 @@ func _on_new_character_selected(character_texture: ImageTexture) -> void: var local_path: String = '/Users/sebastian/ResilioSync/Sorterat_test/Metal Gear Solid 4 - Guns of the Patriots/2-16 Metal Gear Saga.mp3' + func open() -> void: file_dialog.popup() diff --git a/scroll_to_end_container.gd b/scroll_to_end_container.gd index 4523b0b..38f5316 100644 --- a/scroll_to_end_container.gd +++ b/scroll_to_end_container.gd @@ -1,9 +1,12 @@ class_name ScrollToEndContainer extends ScrollContainer +## A scroll container that scrolls to the end whenever its content grows, so the newest +## item is always in view. -var max_value = 0; +var max_value: float = 0.0 -func _draw(): - if max_value != self.get_v_scroll_bar().max_value: - max_value = self.get_v_scroll_bar().max_value - self.scroll_vertical = max_value + +func _draw() -> void: + if max_value != get_v_scroll_bar().max_value: + max_value = get_v_scroll_bar().max_value + scroll_vertical = int(max_value) diff --git a/search_popup.gd b/search_popup.gd index 532267b..27bd6c1 100644 --- a/search_popup.gd +++ b/search_popup.gd @@ -1,65 +1,65 @@ class_name SearchPopup extends BasePopup - -@onready -var search_list: VBoxContainer = %SearchResults - -@onready -var search_panel: Panel = %SearchPanel - -@onready -var clear_button: Button = $ClearButton - -@onready -var search_bar: TextEdit = $Searchbar +## Searches every soundtrack on the server as you type. Matches ignore case, spaces and +## special characters, and also find the letters in order with others in between. var soundtracks: Array = [] -# Precomputed soundtrack data: array of dicts with original, lowercase, and cleaned fields +## Each soundtrack as a Dictionary with its original, lowercase and cleaned name. var precomputed_soundtracks: Array = [] -# Regex cache: maps cleaned search terms to compiled RegEx objects +## Compiled patterns for the letters-in-order search, by cleaned search term. var regex_cache: Dictionary = {} var label_pool: Array[Label] = [] -# Called when the node enters the scene tree for the first time. +@onready var search_list: VBoxContainer = %SearchResults +@onready var search_panel: Panel = %SearchPanel +@onready var clear_button: Button = $ClearButton +@onready var search_bar: TextEdit = $Searchbar + + func _ready() -> void: clear_button.pressed.connect(clear) search_bar.grab_focus() search_bar.text_changed.connect(search) visibility_changed.connect(focus) + func focus() -> void: - if self.visible == true: + if visible: search_bar.grab_focus() clear() + func close() -> void: clear() super() + func search() -> void: GameLog.debug(search_bar.text) var search_text: String = search_bar.text - + if search_text == "": show_all_soundtracks() return - + perform_search(search_text) + func show_all_soundtracks() -> void: # Hide all visible labels and add to pool for child: Label in search_list.get_children(): if child.visible: child.visible = false label_pool.append(child) - + # Show all soundtracks in order for soundtrack: String in soundtracks: add_soundtrack_reuse(soundtrack) + func perform_search(search_text: String) -> void: # Hide all visible labels and add to pool for child: Label in search_list.get_children(): @@ -70,6 +70,7 @@ func perform_search(search_text: String) -> void: for soundtrack: String in find_matches(search_text): add_soundtrack_reuse(soundtrack) + ## Returns the soundtracks matching [param search_text], best matches first: plain ## case-insensitive matches, then matches ignoring spaces and special characters, ## then matches with the letters in order but not next to each other. @@ -81,25 +82,25 @@ func find_matches(search_text: String) -> Array[String]: var matches: Array[String] = [] # Pass 1: Exact case-insensitive contains match on lowercase versions - for i in range(precomputed_soundtracks.size()): - var st_data = precomputed_soundtracks[i] + for i: int in range(precomputed_soundtracks.size()): + var st_data: Dictionary = precomputed_soundtracks[i] if st_data["lowercase"].contains(search_text_lower): if not st_data["original"] in seen: seen[st_data["original"]] = true matches.append(st_data["original"]) # Pass 2: Contains match on cleaned terms - for i in range(precomputed_soundtracks.size()): - var st_data = precomputed_soundtracks[i] + for i: int in range(precomputed_soundtracks.size()): + var st_data: Dictionary = precomputed_soundtracks[i] if st_data["cleaned"].contains(clean_search_text): if not st_data["original"] in seen: seen[st_data["original"]] = true matches.append(st_data["original"]) # Pass 3: Regex match on cleaned terms - var regex = compile_regex_cached(clean_search_text) - for i in range(precomputed_soundtracks.size()): - var st_data = precomputed_soundtracks[i] + var regex: RegEx = compile_regex_cached(clean_search_text) + for i: int in range(precomputed_soundtracks.size()): + var st_data: Dictionary = precomputed_soundtracks[i] if regex.search(st_data["cleaned"]): if not st_data["original"] in seen: seen[st_data["original"]] = true @@ -107,30 +108,35 @@ func find_matches(search_text: String) -> Array[String]: return matches -# Cleans a term by removing spaces and replacing special characters. -# Matches MusicServer's clean_term() function. -func clean_term(term: String) -> String: - return term.replace(" ", "").replace("é", "e").replace("+", "plus").replace("&", "and").replace("'n", "and").to_lower() -# Compiles a regex pattern with caching to avoid recompiling the same pattern multiple times. +## Cleans a term by removing spaces and replacing special characters. +## Matches MusicServer's clean_term() function. +func clean_term(term: String) -> String: + return term.replace(" ", "").replace("é", "e").replace("+", "plus").replace("&", "and") \ + .replace("'n", "and").to_lower() + + +## Compiles the letters-in-order pattern for [param search_term], once per term. func compile_regex_cached(search_term: String) -> RegEx: if not search_term in regex_cache: - var regex = RegEx.new() - var regText: String = ".*" + var regex: RegEx = RegEx.new() + var regex_text: String = ".*" for letter: String in search_term: # Escape regex metacharacters so searches like "Q*bert" or "(Remix)" work if "\\^$.|?*+()[]{}".contains(letter): - regText += "\\" - regText += letter + ".*" - regex.compile(regText) + regex_text += "\\" + regex_text += letter + ".*" + regex.compile(regex_text) regex_cache[search_term] = regex return regex_cache[search_term] + func add_soundtrack(soundtrack: String) -> void: # Backward compatibility - delegates to reuse version add_soundtrack_reuse(soundtrack) -# Optimized version that reuses label nodes from pool instead of creating new ones + +## Shows [param soundtrack], reusing a hidden label from the pool when there is one. func add_soundtrack_reuse(soundtrack: String) -> void: var label: Label if label_pool.size() > 0: @@ -146,6 +152,7 @@ func add_soundtrack_reuse(soundtrack: String) -> void: search_list.add_child(label) label.visible = true + func check_if_soundtrack_exists(soundtrack: String) -> bool: var soundtrack_exists: bool = false for child: Label in search_list.get_children(): @@ -153,9 +160,10 @@ func check_if_soundtrack_exists(soundtrack: String) -> bool: soundtrack_exists = true return soundtrack_exists + func get_list_of_soundtracks() -> void: GameLog.debug("get_list_of_soundtracks") - var handle_soundtracks: Callable = func handle_soundtracks(array) -> void: + var handle_soundtracks: Callable = func handle_soundtracks(array: Variant) -> void: if typeof(array) == TYPE_ARRAY: # Reuse existing labels: hide all and add to pool for child: Label in search_list.get_children(): @@ -168,6 +176,7 @@ func get_list_of_soundtracks() -> void: GameLog.warning("Unexpected data") MusicServerClient.fetch_all_soundtracks_ordered(handle_soundtracks) + ## Replaces the searchable soundtracks and precomputes their search forms. func set_soundtracks(list: Array) -> void: soundtracks = [] @@ -180,6 +189,7 @@ func set_soundtracks(list: Array) -> void: "cleaned": clean_term(soundtrack) }) + func clear() -> void: search_bar.text = "" show_all_soundtracks() diff --git a/settings.gd b/settings.gd index 2f39516..cc3266a 100644 --- a/settings.gd +++ b/settings.gd @@ -1,14 +1,14 @@ extends Node +## The game settings and the players, shared by the whole game. +## +## Registered as the [code]Settings[/code] autoload. const MAX_NUMBER_OF_PLAYERS: int = 7 var default_path: String = "https://music.sanplex.xyz" var selected_server: int = 0 - var is_local: bool = false - var is_debug: bool = false - var stop_after_current: bool = true var auto_repeat_song: bool = true var hide_next_track: bool = true @@ -22,7 +22,6 @@ var hide_for_seconds: float = 5.0 var hide_beginning: bool = false var hide_length: bool = false var hide_ticks: bool = false - var player_array: Array[PlayerObject] var edit_players: bool = false var currently_syncing: bool = false @@ -32,6 +31,7 @@ var character_select_open: bool = false ## Releases are made from it, see RELEASING.md. var version: String = ProjectSettings.get_setting("application/config/version", "") + func delete_children(node: Node) -> void: for n: Node in node.get_children(): node.remove_child(n) diff --git a/settings_popup.gd b/settings_popup.gd index d76e192..bb65ecc 100644 --- a/settings_popup.gd +++ b/settings_popup.gd @@ -1,82 +1,40 @@ class_name SettingsPopup extends BasePopup +## The settings: how songs are shown and played, the winning score, the song cache, +## which MusicServer to use and fullscreen. -@onready -var settings_panel: Panel = %SettingsPanel - -@onready -var stop_after_current_button: CheckButton = %StopAfterCurrentCheckButton - -@onready -var hide_next_track_button: CheckButton = %HideNextTrackCheckButton - -@onready -var add_to_database_button: CheckButton = %AddToDatabaseCheckButton - -@onready -var low_played_button: CheckButton = %LowPlayedCheckButton - -@onready -var hide_beginning_button: CheckButton = %HideBeginningCheckButton - -@onready -var hide_length_button: CheckButton = %HideLengthCheckButton - -@onready -var hide_ticks_button: CheckButton = %HideTicksCheckButton - -@onready -var score_label: Label = %ScoreLabel - -@onready -var lower_winning_score_button: Button = %LowerButton - -@onready -var increase_winning_score_button: Button = %IncreaseButton - -@onready -var cache_label: Label = %CacheLabel - -@onready -var lower_cache_button: Button = %LowerCacheButton - -@onready -var increase_cache_button: Button = %IncreaseCacheButton - -@onready -var inspiration_speed_label: Label = %InspirationListSpeedLabel - -@onready -var lower_inspiration_speed_button: Button = %LowerInspirationSpeedButton - -@onready -var increase_inspiration_speed_button: Button = %IncreaseInspirationSpeedButton - -@onready -var hide_for_seconds_label: Label = %HideForSecondsLabel - -@onready -var lower_hide_for_seconds_button: Button = %LowerHideForSecondsButton - -@onready -var increase_hide_for_seconds_button: Button = %IncreaseHideForSecondsButton - -@onready -var select_server_button: OptionButton = %SelectServerButton - -@onready -var fullscreen_button: CheckButton = %FullscreenButton - -@onready -var local_button: CheckButton = %LocalButton - +## Emitted when another MusicServer is picked. signal server_changed -# Called when the node enters the scene tree for the first time. +@onready var settings_panel: Panel = %SettingsPanel +@onready var stop_after_current_button: CheckButton = %StopAfterCurrentCheckButton +@onready var hide_next_track_button: CheckButton = %HideNextTrackCheckButton +@onready var add_to_database_button: CheckButton = %AddToDatabaseCheckButton +@onready var low_played_button: CheckButton = %LowPlayedCheckButton +@onready var hide_beginning_button: CheckButton = %HideBeginningCheckButton +@onready var hide_length_button: CheckButton = %HideLengthCheckButton +@onready var hide_ticks_button: CheckButton = %HideTicksCheckButton +@onready var score_label: Label = %ScoreLabel +@onready var lower_winning_score_button: Button = %LowerButton +@onready var increase_winning_score_button: Button = %IncreaseButton +@onready var cache_label: Label = %CacheLabel +@onready var lower_cache_button: Button = %LowerCacheButton +@onready var increase_cache_button: Button = %IncreaseCacheButton +@onready var inspiration_speed_label: Label = %InspirationListSpeedLabel +@onready var lower_inspiration_speed_button: Button = %LowerInspirationSpeedButton +@onready var increase_inspiration_speed_button: Button = %IncreaseInspirationSpeedButton +@onready var hide_for_seconds_label: Label = %HideForSecondsLabel +@onready var lower_hide_for_seconds_button: Button = %LowerHideForSecondsButton +@onready var increase_hide_for_seconds_button: Button = %IncreaseHideForSecondsButton +@onready var select_server_button: OptionButton = %SelectServerButton +@onready var fullscreen_button: CheckButton = %FullscreenButton +@onready var local_button: CheckButton = %LocalButton + + func _ready() -> void: fullscreen_button.button_pressed = Settings.fullscreen select_server_button.selected = Settings.selected_server - + stop_after_current_button.pressed.connect(stop_after_current) hide_next_track_button.pressed.connect(hide_next_track) add_to_database_button.pressed.connect(add_to_database) @@ -94,7 +52,7 @@ func _ready() -> void: increase_hide_for_seconds_button.pressed.connect(increase_hide_for_seconds) fullscreen_button.pressed.connect(fullscreen) local_button.pressed.connect(local_play) - + stop_after_current_button.button_pressed = Settings.stop_after_current hide_next_track_button.button_pressed = Settings.hide_next_track add_to_database_button.button_pressed = Settings.add_to_stats @@ -114,95 +72,117 @@ func _ready() -> void: # Alt+Enter switches fullscreen from the main window, which resizes the window get_window().size_changed.connect(_update_fullscreen_button) + # Settings can change outside this popup, e.g. winning turns statistics off func _on_visibility_changed() -> void: if visible: add_to_database_button.button_pressed = Settings.add_to_stats _update_fullscreen_button() + func _update_fullscreen_button() -> void: fullscreen_button.set_pressed_no_signal(Settings.fullscreen) + func fullscreen() -> void: Settings.fullscreen = !Settings.fullscreen - if Settings.fullscreen == true: + if Settings.fullscreen: DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN) - if Settings.fullscreen == false: + if !Settings.fullscreen: DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) + func local_play() -> void: Settings.play_local = !Settings.play_local - + + func stop_after_current() -> void: Settings.stop_after_current = !Settings.stop_after_current + func hide_next_track() -> void: Settings.hide_next_track = !Settings.hide_next_track - + + func add_to_database() -> void: Settings.add_to_stats = !Settings.add_to_stats - + + func low_played() -> void: Settings.use_low_played_mode = !Settings.use_low_played_mode + func hide_beginning() -> void: Settings.hide_beginning = !Settings.hide_beginning update_hide_for_seconds_enabled() + func update_hide_for_seconds_enabled() -> void: var enabled: bool = Settings.hide_beginning %HideForSecondsHBoxContainer.visible = enabled + func hide_length() -> void: Settings.hide_length = !Settings.hide_length + func hide_ticks() -> void: Settings.hide_ticks = !Settings.hide_ticks + func lower_winning_score() -> void: Settings.winning_score = max(Settings.winning_score - 1, 1) score_label.text = str(Settings.winning_score) + func increase_winning_score() -> void: Settings.winning_score += 1 score_label.text = str(Settings.winning_score) + func set_winning_score(new_score: int) -> void: Settings.winning_score = new_score score_label.text = str(Settings.winning_score) + func lower_cache() -> void: Playlist.number_of_tracks_to_preload -= 1 if Playlist.number_of_tracks_to_preload < 1: Playlist.number_of_tracks_to_preload = 1 cache_label.text = str(Playlist.number_of_tracks_to_preload) + func increase_cache() -> void: Playlist.number_of_tracks_to_preload += 1 cache_label.text = str(Playlist.number_of_tracks_to_preload) + func lower_inspiration_speed() -> void: Settings.inspiration_list_speed -= 1 if Settings.inspiration_list_speed < 1: Settings.inspiration_list_speed = 1 inspiration_speed_label.text = str(Settings.inspiration_list_speed) + func increase_inspiration_speed() -> void: Settings.inspiration_list_speed += 1 inspiration_speed_label.text = str(Settings.inspiration_list_speed ) + func lower_hide_for_seconds() -> void: Settings.hide_for_seconds -= 1.0 if Settings.hide_for_seconds < 0: Settings.hide_for_seconds = 0 hide_for_seconds_label.text = str(Settings.hide_for_seconds) + func increase_hide_for_seconds() -> void: Settings.hide_for_seconds += 1.0 if Settings.hide_for_seconds > 10.0: Settings.hide_for_seconds = 10.0 hide_for_seconds_label.text = str(Settings.hide_for_seconds) + func select_server(new_server: int) -> void: GameLog.debug("select_server") Settings.default_path = select_server_button.get_item_text(new_server) @@ -210,9 +190,11 @@ func select_server(new_server: int) -> void: GameLog.debug("Settings.default_path: " + Settings.default_path) GameLog.debug("Settings.selected_server: " + str(Settings.selected_server)) GameLog.debug("new_server: " + str(new_server)) - GameLog.debug("select_server_button.get_item_text(new_server): " + select_server_button.get_item_text(new_server)) + GameLog.debug("select_server_button.get_item_text(new_server): ", + select_server_button.get_item_text(new_server)) server_changed.emit() + func set_selected_server(new_server: int) -> void: GameLog.debug("set_selected_server") select_server_button.select(new_server) @@ -221,5 +203,6 @@ func set_selected_server(new_server: int) -> void: GameLog.debug("Settings.default_path: " + Settings.default_path) GameLog.debug("Settings.selected_server: " + str(Settings.selected_server)) GameLog.debug("new_server: " + str(new_server)) - GameLog.debug("select_server_button.get_item_text(new_server): " + select_server_button.get_item_text(new_server)) + GameLog.debug("select_server_button.get_item_text(new_server): ", + select_server_button.get_item_text(new_server)) server_changed.emit() diff --git a/shortcuts_popup.gd b/shortcuts_popup.gd index 2a9c961..15150ee 100644 --- a/shortcuts_popup.gd +++ b/shortcuts_popup.gd @@ -1,11 +1,6 @@ class_name ShortcutsPopup extends BasePopup - -@onready -var shortcuts_vbox: VBoxContainer = %shortcutsVBox - -@onready -var shortcuts_panel: Panel = %ShortcutsPanel +## Lists the keyboard shortcuts. Opened with Alt+K or the Shortcuts button. var shortcuts_dic: Dictionary = { "Alt + S": "Search", @@ -26,14 +21,17 @@ var shortcuts_dic: Dictionary = { "Alt + Ctrl + 1 - 9": "Take point from player", } +@onready var shortcuts_vbox: VBoxContainer = %shortcutsVBox +@onready var shortcuts_panel: Panel = %ShortcutsPanel + + #Color(0.18, 0.18, 0.18) #Color(0.302, 0.302, 0.302) #Color(0.259, 0.259, 0.259) - func _ready() -> void: shortcuts_dic.sort() Settings.delete_children(shortcuts_vbox) - + var title: Label = Label.new() title.add_theme_font_size_override("font_size", 20) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER @@ -45,28 +43,26 @@ func _ready() -> void: var panel: PanelContainer = PanelContainer.new() var hbox: HBoxContainer = HBoxContainer.new() hbox.alignment = BoxContainer.ALIGNMENT_CENTER - + var label: Label = Label.new() label.add_theme_font_size_override("font_size", 20) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.text = shortcut label.autowrap_mode = TextServer.AUTOWRAP_WORD label.size_flags_horizontal = Control.SIZE_EXPAND_FILL - + var label2: Label = Label.new() label2.add_theme_font_size_override("font_size", 20) label2.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label2.text = shortcuts_dic[shortcut] label2.autowrap_mode = TextServer.AUTOWRAP_WORD label2.size_flags_horizontal = Control.SIZE_EXPAND_FILL - + if count % 2 == 0: var new_stylebox_normal: Resource = panel.get_theme_stylebox("panel").duplicate() new_stylebox_normal.bg_color = Color(0.18, 0.18, 0.18) panel.add_theme_stylebox_override("panel", new_stylebox_normal) - #label.add_theme_stylebox_override("normal", new_stylebox_normal) - #label2.add_theme_stylebox_override("normal", new_stylebox_normal) - + hbox.add_child(label) hbox.add_child(label2) panel.add_child(hbox) diff --git a/song_loader.gd b/song_loader.gd index 6fba366..ce85697 100644 --- a/song_loader.gd +++ b/song_loader.gd @@ -8,6 +8,7 @@ extends Node ## Emitted when the songs already in the server playlist have been loaded into the ## Playlist. + signal song_list_loaded ## Emitted when a new song has been downloaded and added to the Playlist. @@ -21,7 +22,7 @@ var retry_seconds: float = 5.0 ## Preloading waits for the list, because loading the list clears the playlist. func load_song_list() -> void: GameLog.debug("load_song_list") - var song_list_fetched: Callable = func song_list_fetched(data) -> void: + var song_list_fetched: Callable = func song_list_fetched(data: Variant) -> void: Playlist.clear_song_list() if typeof(data) == TYPE_ARRAY: for d: Dictionary in data: diff --git a/song_object.gd b/song_object.gd index a390df3..7500c90 100644 --- a/song_object.gd +++ b/song_object.gd @@ -3,53 +3,57 @@ extends RefCounted class_name SongObject var song_number: int - var song_title: String - var soundtrack_title: String ## Ids of the players who got a point for this song. var players_given_point: Array[String] = [] var is_answered: bool = false - var has_played: bool = false - var is_playing: bool = false - var song: AudioStream + func get_soundtrack_title() -> String: if is_answered: return soundtrack_title else: return "????????" + func get_song_title() -> String: if is_answered: return song_title else: return "??????" + func get_song_info() -> String: if Settings.is_debug: var format_string: String = "%d. %s - %s | played: %s | answered: %s" - return format_string % [(song_number + 1), soundtrack_title, song_title, has_played, is_answered] + return format_string % [(song_number + 1), soundtrack_title, song_title, has_played, + is_answered] elif is_answered: var format_string: String = "%d. %s - %s" return format_string % [(song_number + 1), soundtrack_title, song_title] else: return "??? - ???" + func add_point(player_id: String) -> void: GameLog.debug("add_point") if !players_given_point.has(player_id): players_given_point.append(player_id) + func remove_point(player_id: String) -> void: GameLog.debug("remove_point") players_given_point.erase(player_id) + func _to_string() -> String: - var format_string: String = "song_number: %d | soundtrack_title: %s | song_title: %s | is_answered: %s | has_played: %s | is_playing: %s" - return format_string % [(song_number), soundtrack_title, song_title, is_answered, has_played, is_playing] + var format_string: String = "song_number: %d | soundtrack_title: %s | song_title: %s" \ + + " | is_answered: %s | has_played: %s | is_playing: %s" + return format_string % [song_number, soundtrack_title, song_title, is_answered, has_played, + is_playing] diff --git a/song_row.gd b/song_row.gd index ef24f51..8728180 100644 --- a/song_row.gd +++ b/song_row.gd @@ -1,33 +1,19 @@ class_name SongRow extends PanelContainer - -@onready -var song_list_label: Label = %SongListLabel - -@onready -var song_list_players: HBoxContainer = %SongListPlayers +## One song in the music list: its number, soundtrack and title, and a marker for +## each player who got a point for it. var song_object: SongObject -#Color(0.18, 0.18, 0.18) -#Color(0.302, 0.302, 0.302) -#Color(0.259, 0.259, 0.259) +@onready var song_list_label: Label = %SongListLabel +@onready var song_list_players: HBoxContainer = %SongListPlayers + -# Called when the node enters the scene tree for the first time. func _ready() -> void: - #if song_object != null && song_object.song_number % 2 == 0: - # var new_stylebox_normal = self.get_theme_stylebox("panel").duplicate() - # new_stylebox_normal.bg_color = Color(0.18, 0.18, 0.18) - # self.add_theme_stylebox_override("panel", new_stylebox_normal) - # song_list_label.add_theme_stylebox_override("normal", new_stylebox_normal) - #else: - # var new_stylebox_normal = self.get_theme_stylebox("panel").duplicate() - # new_stylebox_normal.bg_color = Color(0.18, 0.18, 0.18) - # self.add_theme_stylebox_override("panel", new_stylebox_normal) - # song_list_label.add_theme_stylebox_override("normal", new_stylebox_normal) set_label() update_players() + func set_label() -> void: if song_object != null: var size_to_change: int = song_object.players_given_point.size() * 50 @@ -36,6 +22,7 @@ func set_label() -> void: song_list_label.add_theme_color_override("font_color", Color(1, 0.5, 0)) song_list_label.text = song_object.get_song_info() + func update_players() -> void: Settings.delete_children(song_list_players) if song_object != null: diff --git a/sync_popup.gd b/sync_popup.gd index 5e1d6bc..d295b3d 100644 --- a/sync_popup.gd +++ b/sync_popup.gd @@ -1,79 +1,49 @@ class_name SyncPopup extends BasePopup +## Asks the server to sync its soundtracks with the music folder, shows the progress, +## and then lists what was added, changed or removed. -@onready -var sync_panel: Panel = %SyncPanel - -@onready -var sync_title_label: Label = %SyncTitleLabel - -@onready -var no_changes_label: Label = %NoChangesLabel - -@onready -var progress_hbox: HBoxContainer = %ProgressHBoxContainer - -@onready -var sync_hbox: HBoxContainer = %SyncHBoxContainer - -@onready -var sync_progress_bar: ProgressBar = %SyncProgressBar - -@onready -var soundtracks_added_vbox: VBoxContainer = %SoundtracksAddedVBox - -@onready -var soundtracks_added_separator: VSeparator = %SoundtracksAddedSeparator - -@onready -var soundtracks_readded_vbox: VBoxContainer = %SoundtracksReAddedVBox - -@onready -var soundtracks_readded_separator: VSeparator = %SoundtracksReAddedSeparator - -@onready -var soundtracks_changed_title_vbox: VBoxContainer = %SoundtracksChangedTitleVBox - -@onready -var soundtracks_changed_title_separator: VSeparator = %SoundtracksChangedTitleSeparator - -@onready -var soundtracks_changed_content_vbox: VBoxContainer = %SoundtracksChangedContentVBox - -@onready -var soundtracks_changed_content_separator: VSeparator = %SoundtracksChangedContentSeparator - -@onready -var soundtracks_removed_vbox: VBoxContainer = %SoundtracksRemovedVBox - -@onready -var soundtracks_removed_separator: VSeparator = %SoundtracksRemovedSeparator - -@onready -var catched_errors_vbox: VBoxContainer = %CatchedErrorsVBox - -@onready -var sync_time: Label = %SyncTime - -@onready -var sync_timer: Timer = %SyncTimer - -@onready -var restart_sync_button: Button = %RestartSyncButton - -var changes: bool = false - -var has_synced: bool = false +## Emitted when the server has started syncing. signal sync_started +## Emitted when the sync is done and its result is shown. signal sync_finished +var changes: bool = false +var has_synced: bool = false + +@onready var sync_panel: Panel = %SyncPanel +@onready var sync_title_label: Label = %SyncTitleLabel +@onready var no_changes_label: Label = %NoChangesLabel +@onready var progress_hbox: HBoxContainer = %ProgressHBoxContainer +@onready var sync_hbox: HBoxContainer = %SyncHBoxContainer +@onready var sync_progress_bar: ProgressBar = %SyncProgressBar +@onready var soundtracks_added_vbox: VBoxContainer = %SoundtracksAddedVBox +@onready var soundtracks_added_separator: VSeparator = %SoundtracksAddedSeparator +@onready var soundtracks_readded_vbox: VBoxContainer = %SoundtracksReAddedVBox +@onready var soundtracks_readded_separator: VSeparator = %SoundtracksReAddedSeparator +@onready var soundtracks_changed_title_vbox: VBoxContainer = %SoundtracksChangedTitleVBox +@onready var soundtracks_changed_title_separator: VSeparator = %SoundtracksChangedTitleSeparator +@onready var soundtracks_changed_content_vbox: VBoxContainer = %SoundtracksChangedContentVBox +@onready var soundtracks_changed_content_separator: VSeparator = %SoundtracksChangedContentSeparator +@onready var soundtracks_removed_vbox: VBoxContainer = %SoundtracksRemovedVBox +@onready var soundtracks_removed_separator: VSeparator = %SoundtracksRemovedSeparator +@onready var caught_errors_vbox: VBoxContainer = %CaughtErrorsVBox +@onready var sync_time: Label = %SyncTime +@onready var sync_timer: Timer = %SyncTimer +@onready var restart_sync_button: Button = %RestartSyncButton + + func _ready() -> void: sync_timer.timeout.connect(_on_sync_timeout) restart_sync_button.pressed.connect(start_sync) restart_sync_button.visible = false -func show_section_if_has_data(data: Variant, vbox: VBoxContainer, separator: VSeparator, header_text: String) -> bool: + +## Lists [param data] under [param header_text] in [param vbox] and returns true, or hides +## the column and its separator when there is nothing to list. +func show_section_if_has_data(data: Variant, vbox: VBoxContainer, separator: VSeparator, + header_text: String) -> bool: if data != null && data.size() > 0: vbox.visible = true make_list(data, vbox, header_text) @@ -83,9 +53,10 @@ func show_section_if_has_data(data: Variant, vbox: VBoxContainer, separator: VSe separator.visible = false return false + func start_sync() -> void: GameLog.debug("start_sync") - self.visible = true + visible = true if has_synced: # Already synced, just show the results show_sync_result() @@ -98,6 +69,7 @@ func start_sync() -> void: if !Settings.currently_syncing: MusicServerClient.start_sync(sync_request_sent) + func _on_sync_timeout() -> void: GameLog.debug("_on_sync_timeout!") var get_progress: Callable = func get_progress(data_received: Dictionary) -> void: @@ -111,38 +83,46 @@ func _on_sync_timeout() -> void: on_sync_finished(data_received) MusicServerClient.fetch_sync_progress(get_progress) + func on_sync_finished(data_received: Dictionary) -> void: has_synced = true reset_sync_result() - - if show_section_if_has_data(data_received.soundtracks_added, soundtracks_added_vbox, soundtracks_added_separator, "Added"): + + if show_section_if_has_data(data_received.soundtracks_added, soundtracks_added_vbox, + soundtracks_added_separator, "Added"): changes = true - - if show_section_if_has_data(data_received.soundtracks_re_added, soundtracks_readded_vbox, soundtracks_readded_separator, "Re-added"): + + if show_section_if_has_data(data_received.soundtracks_re_added, soundtracks_readded_vbox, + soundtracks_readded_separator, "Re-added"): changes = true - - if data_received.soundtracks_changed_title != null && data_received.soundtracks_changed_title.size() > 0: + + var changed_titles: Variant = data_received.soundtracks_changed_title + if changed_titles != null && changed_titles.size() > 0: changes = true Settings.delete_children(soundtracks_changed_title_vbox) make_header_label(soundtracks_changed_title_vbox, "Changed Title") var row: int = 0 - for old_soundtrack_title: String in data_received.soundtracks_changed_title: - var new_soundtrack_title: String = data_received.soundtracks_changed_title[old_soundtrack_title] + for old_soundtrack_title: String in changed_titles: + var new_soundtrack_title: String = changed_titles[old_soundtrack_title] soundtracks_changed_title_vbox.add_child(make_soundtrack_row( old_soundtrack_title + "\nchanged title to\n" + new_soundtrack_title, row)) row += 1 soundtracks_changed_title_vbox.visible = true - - if show_section_if_has_data(data_received.soundtracks_changed_content, soundtracks_changed_content_vbox, soundtracks_changed_content_separator, "Changed Content"): + + if show_section_if_has_data(data_received.soundtracks_changed_content, + soundtracks_changed_content_vbox, soundtracks_changed_content_separator, + "Changed Content"): changes = true - - if show_section_if_has_data(data_received.soundtracks_removed, soundtracks_removed_vbox, soundtracks_removed_separator, "Removed"): + + if show_section_if_has_data(data_received.soundtracks_removed, soundtracks_removed_vbox, + soundtracks_removed_separator, "Removed"): changes = true - + + # The server's name for the errors it caught if data_received.catched_errors != null && data_received.catched_errors.size() > 0: - catched_errors_vbox.visible = true - make_list(data_received.catched_errors, catched_errors_vbox, "Errors") - + caught_errors_vbox.visible = true + make_list(data_received.catched_errors, caught_errors_vbox, "Errors") + # Separators go only between visible columns, not after the last one var visible_sections: Array = [] if soundtracks_added_vbox.visible: @@ -155,11 +135,11 @@ func on_sync_finished(data_received: Dictionary) -> void: visible_sections.append("changed_content") if soundtracks_removed_vbox.visible: visible_sections.append("removed") - + # Show section separators only if there's a visible section after them - for i in range(visible_sections.size()): - var section = visible_sections[i] - var separator_map = { + for i: int in range(visible_sections.size()): + var section: String = visible_sections[i] + var separator_map: Dictionary = { "added": soundtracks_added_separator, "readded": soundtracks_readded_separator, "changed_title": soundtracks_changed_title_separator, @@ -171,10 +151,11 @@ func on_sync_finished(data_received: Dictionary) -> void: separator_map[section].visible = true else: separator_map[section].visible = false - + if !changes: no_changes_label.visible = true - sync_time.text = "Total time: " + data_received.total_time if data_received.has("total_time") else "" + sync_time.text = "Total time: " + data_received.total_time \ + if data_received.has("total_time") else "" Settings.currently_syncing = false sync_timer.stop() GameLog.debug("sync finished") @@ -182,9 +163,11 @@ func on_sync_finished(data_received: Dictionary) -> void: restart_sync_button.visible = true sync_finished.emit() -func list_visible(vbox: VBoxContainer, seperator: VSeparator) -> void: + +func list_visible(vbox: VBoxContainer, separator: VSeparator) -> void: vbox.visible = true - seperator.visible = true + separator.visible = true + func make_header_label(vbox: VBoxContainer, text: String) -> void: var label: Label = Label.new() @@ -194,6 +177,7 @@ func make_header_label(vbox: VBoxContainer, text: String) -> void: label.text = text vbox.add_child(label) + # Items alternate between white and orange text as well as background shade func make_soundtrack_row(text: String, row: int) -> ListRow: var list_row: ListRow = ListRow.create(text, row, 18, HORIZONTAL_ALIGNMENT_CENTER) @@ -201,6 +185,7 @@ func make_soundtrack_row(text: String, row: int) -> ListRow: list_row.label.add_theme_color_override("font_color", Color(1, 0.5, 0)) return list_row + func reset_sync_result() -> void: sync_progress_bar.value = 0 sync_time.text = "" @@ -216,7 +201,8 @@ func reset_sync_result() -> void: soundtracks_changed_content_separator.visible = false soundtracks_removed_vbox.visible = false soundtracks_removed_separator.visible = false - catched_errors_vbox.visible = false + caught_errors_vbox.visible = false + func make_list(list: Array, vbox: VBoxContainer, text: String) -> void: Settings.delete_children(vbox) @@ -224,16 +210,18 @@ func make_list(list: Array, vbox: VBoxContainer, text: String) -> void: for row: int in list.size(): vbox.add_child(make_soundtrack_row(list[row], row)) + func show_sync_result() -> void: sync_title_label.text = "Sync Result" progress_hbox.visible = false sync_hbox.visible = true restart_sync_button.visible = true - + + func show_sync_progress() -> void: sync_title_label.text = "Sync in Progress" progress_hbox.visible = true no_changes_label.visible = false sync_hbox.visible = false - catched_errors_vbox.visible = false + caught_errors_vbox.visible = false restart_sync_button.visible = false diff --git a/tests/fakes/ui_tour.gd b/tests/fakes/ui_tour.gd index ce8af82..45074f6 100644 --- a/tests/fakes/ui_tour.gd +++ b/tests/fakes/ui_tour.gd @@ -87,6 +87,7 @@ static func prepare_autoloads() -> void: func run(on_state: Callable) -> void: # The debug build lowers the winning score on startup; keep it out of reach Settings.winning_score = 20 + _stop_caret_blink(_main) await _settle() await on_state.call("startup", null) @@ -194,6 +195,14 @@ func _call(method: String, args: Array = []) -> Variant: 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 diff --git a/tests/integration/test_layout_unchanged.gd b/tests/integration/test_layout_unchanged.gd index 56819fa..06c1673 100644 --- a/tests/integration/test_layout_unchanged.gd +++ b/tests/integration/test_layout_unchanged.gd @@ -58,9 +58,14 @@ const RENAMED_SCENES: Dictionary = { "Player": "PlayerRow", } -## Nodes renamed inside a popup: old path from the main window, and new path. +const SYNC_COLUMNS: String = "SyncPopup/SyncPanel/ScrollContainer/SyncVBoxContainer" + +## Nodes renamed inside a popup: the path after the moves above, and the new path. const RENAMED_PATHS: Dictionary = { "SearchPopup/ScrollContainer/VBoxContainer": "SearchPopup/ScrollContainer/SearchResults", + SYNC_COLUMNS + "/CatchedErrorsVBox": SYNC_COLUMNS + "/CaughtErrorsVBox", + SYNC_COLUMNS + "/CaughtErrorsVBox/CatchedErrorsLabel": + SYNC_COLUMNS + "/CaughtErrorsVBox/CaughtErrorsLabel", } ## How far a node may be from its old place, in pixels. @@ -102,9 +107,10 @@ static func new_path(old_path: String) -> String: var first: String = parts[0] parts[0] = MOVED.get(first, first) var path: String = "/".join(parts) + # In order, so a node inside a renamed node is found after its parent's rename for renamed: String in RENAMED_PATHS: if path == renamed || path.begins_with(renamed + "/"): - return RENAMED_PATHS[renamed] + path.trim_prefix(renamed) + path = RENAMED_PATHS[renamed] + path.trim_prefix(renamed) return path diff --git a/tests/integration/test_players_panel.gd b/tests/integration/test_players_panel.gd index d54b894..c11c97d 100644 --- a/tests/integration/test_players_panel.gd +++ b/tests/integration/test_players_panel.gd @@ -105,7 +105,8 @@ func test_rows_follow_player_order_and_numbering() -> void: _add("Marle") _add("Lucca") - assert_array(_rows().map(func(row: PlayerRow) -> String: return row.player_object.player_name)) \ + assert_array(_rows().map( + func(row: PlayerRow) -> String: return row.player_object.player_name)) \ .contains_exactly(["Crono", "Marle", "Lucca"]) assert_array(_rows().map(func(row: PlayerRow) -> int: return row.list_index)) \ .contains_exactly([0, 1, 2]) diff --git a/tests/integration/test_scene_split.gd b/tests/integration/test_scene_split.gd index 4018525..5d8b100 100644 --- a/tests/integration/test_scene_split.gd +++ b/tests/integration/test_scene_split.gd @@ -9,6 +9,12 @@ const MAIN_WINDOW_CHILDREN: Array[String] = [ "ShortcutsPopup", "StatisticsPopupPanel", "AboutPopup", "LogPopup", ] +## The quiz buttons that are disabled while the server syncs. +const LOCKED_BY_SYNC: Array[String] = [ + "QuizToolbar/SoundTestButton", "QuizToolbar/ResetPlaylistButton", + "SongControls/ShowAnswerButton", "SongControls/NextButton", +] + const TOP_BAR_CHILDREN: Array[String] = [ "TopPanelContainer", "Title", "SyncButton", "SearchButton", "StatisticsButton", "ShortcutsButton", "AboutButton", "QRButton", @@ -130,7 +136,8 @@ func test_points_from_the_player_list_reach_the_song_and_log() -> void: (_player_row(crono).get_node("HBoxContainer/AddPoint") as BaseButton).pressed.emit() assert_array(Playlist.song_object_array[0].players_given_point).contains_exactly([crono.id]) - assert_int((_window.get_node("%LogPopup").get_node("%LogVBoxContainer") as Node).get_child_count()) \ + var log_rows: Node = _window.get_node("%LogPopup").get_node("%LogVBoxContainer") + assert_int(log_rows.get_child_count()) \ .is_equal(1) @@ -143,12 +150,13 @@ func test_character_button_in_the_player_list_picks_a_new_character() -> void: assert_bool(character_select.visible).is_true() var texture: ImageTexture = ImageTexture.create_from_image( Image.create(4, 4, false, Image.FORMAT_RGBA8)) - character_select.new_new_character_selected.emit(texture) + character_select.character_selected.emit(texture) assert_object(crono.character).is_same(texture) func test_sound_test_button_fetches_the_sound_test_song() -> void: - _server.respond_bytes("/music/soundTest", FileAccess.get_file_as_bytes("res://tests/data/short.mp3")) + _server.respond_bytes("/music/soundTest", + FileAccess.get_file_as_bytes("res://tests/data/short.mp3")) await _click_at(_center(_quiz.get_node("QuizToolbar/SoundTestButton"))) @@ -166,7 +174,8 @@ func test_loaded_song_list_shows_in_the_music_list() -> void: _quiz.song_loader.load_song_list() - var rows: Array[Node] = _quiz.get_node("MusicListPanel/ScrollContainer/MusicList").get_children() + var rows: Array[Node] = _quiz.get_node("MusicListPanel/ScrollContainer/MusicList") \ + .get_children() assert_array(rows.map(func(row: SongRow) -> int: return row.song_object.song_number)) \ .contains_exactly([0, 1]) @@ -203,8 +212,7 @@ func test_sync_disables_quiz_buttons_until_finished() -> void: _top_bar_button("SyncButton").pressed.emit() sync_popup.sync_timer.stop() - for button_name: String in ["QuizToolbar/SoundTestButton", "QuizToolbar/ResetPlaylistButton", "SongControls/ShowAnswerButton", - "SongControls/NextButton"]: + for button_name: String in LOCKED_BY_SYNC: assert_bool((_quiz.get_node(button_name) as Button).disabled) \ .override_failure_message(button_name + " not disabled").is_true() assert_bool(_top_bar_button("SyncButton").disabled).is_false() @@ -214,8 +222,7 @@ func test_sync_disables_quiz_buttons_until_finished() -> void: sync_popup._on_sync_timeout() await _runner.simulate_frames(2) - for button_name: String in ["QuizToolbar/SoundTestButton", "QuizToolbar/ResetPlaylistButton", "SongControls/ShowAnswerButton", - "SongControls/NextButton"]: + for button_name: String in LOCKED_BY_SYNC: assert_bool((_quiz.get_node(button_name) as Button).disabled) \ .override_failure_message(button_name + " still disabled").is_false() # The search list in the main window and the quiz playlist are both reloaded diff --git a/tests/integration/test_song_loader.gd b/tests/integration/test_song_loader.gd index a71fe47..9a4feb2 100644 --- a/tests/integration/test_song_loader.gd +++ b/tests/integration/test_song_loader.gd @@ -1,7 +1,9 @@ extends GdUnitTestSuite ## Downloading songs into the Playlist ahead of time, against a fake server. -const SONG_INFO: Dictionary = {"SongNo": 0, "Soundtrack": "Chrono Trigger", "Song": "Corridors of Time"} +const SONG_INFO: Dictionary = { + "SongNo": 0, "Soundtrack": "Chrono Trigger", "Song": "Corridors of Time", +} var _snapshot: AutoloadSnapshot var _server: FakeHttpTransport @@ -32,7 +34,8 @@ func test_load_song_list_adds_songs_already_played_on_the_server() -> void: _server.respond_json("/music/list", [ {"SongNo": 0, "Soundtrack": "Chrono Trigger", "Song": "Corridors of Time"}, ]) - _server.respond_json("/music/info", {"SongNo": 1, "Soundtrack": "Final Fantasy VI", "Song": "Dancing Mad"}) + _server.respond_json("/music/info", + {"SongNo": 1, "Soundtrack": "Final Fantasy VI", "Song": "Dancing Mad"}) var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_loader)) _loader.load_song_list() @@ -57,7 +60,8 @@ func test_fetch_song_downloads_queues_and_adds_the_song() -> void: _loader.fetch_song() await signals.is_emitted("song_added") - assert_array(_server.requested_paths).contains_exactly(["/music/rand", "/music/addQue", "/music/info"]) + assert_array(_server.requested_paths) \ + .contains_exactly(["/music/rand", "/music/addQue", "/music/info"]) var song: SongObject = Playlist.song_object_array[0] assert_str(song.song_title).is_equal("Corridors of Time") assert_object(song.song).is_not_null() @@ -75,7 +79,8 @@ func test_fetch_song_keeps_going_until_the_cache_is_full() -> void: func test_low_played_mode_uses_its_endpoint() -> void: Settings.use_low_played_mode = true - _server.respond_bytes("/music/rand/low", FileAccess.get_file_as_bytes("res://tests/data/short.mp3")) + _server.respond_bytes("/music/rand/low", + FileAccess.get_file_as_bytes("res://tests/data/short.mp3")) _loader.fetch_song() diff --git a/tests/integration/test_sync_popup.gd b/tests/integration/test_sync_popup.gd index 94161c1..cbadbcd 100644 --- a/tests/integration/test_sync_popup.gd +++ b/tests/integration/test_sync_popup.gd @@ -1,4 +1,5 @@ extends GdUnitTestSuite +## The sync popup on its own: starting a sync, its progress, and how the result is shown. var _server: FakeHttpTransport var _snapshot: AutoloadSnapshot @@ -132,7 +133,7 @@ func test_errors_are_listed() -> void: _finish_sync(result) - assert_bool(_node("CatchedErrorsVBox").visible).is_true() + assert_bool(_node("CaughtErrorsVBox").visible).is_true() func test_reopening_after_sync_shows_result_without_syncing_again() -> void: @@ -152,7 +153,7 @@ func test_errors_section_has_errors_header() -> void: _finish_sync(result) - assert_str((_node("CatchedErrorsVBox").get_child(0) as Label).text).is_equal("Errors") + assert_str((_node("CaughtErrorsVBox").get_child(0) as Label).text).is_equal("Errors") func test_long_names_wrap_instead_of_widening_the_window() -> void: @@ -173,7 +174,7 @@ func test_long_names_wrap_instead_of_widening_the_window() -> void: var panel_width: float = (_window.get_node("SyncPanel") as Control).size.x assert_float(_node("SyncHBoxContainer").get_combined_minimum_size().x) \ .is_less(panel_width) - assert_float(_node("CatchedErrorsVBox").get_combined_minimum_size().x) \ + assert_float(_node("CaughtErrorsVBox").get_combined_minimum_size().x) \ .is_less(panel_width) diff --git a/tests/unit/test_list_row.gd b/tests/unit/test_list_row.gd index ce5b428..a6acc00 100644 --- a/tests/unit/test_list_row.gd +++ b/tests/unit/test_list_row.gd @@ -1,4 +1,5 @@ extends GdUnitTestSuite +## ListRow: text, alternating backgrounds and wrapping. func _background(list_row: ListRow) -> Color: diff --git a/tests/unit/test_music_player_time.gd b/tests/unit/test_music_player_time.gd index a048882..cd8e938 100644 --- a/tests/unit/test_music_player_time.gd +++ b/tests/unit/test_music_player_time.gd @@ -1,4 +1,5 @@ extends GdUnitTestSuite +## How the music player shows a song's time, including the hidden parts. var _music_player: MusicPlayer var _hide_beginning: bool diff --git a/tests/unit/test_music_server_client.gd b/tests/unit/test_music_server_client.gd index 3fb5e7c..d4e8e92 100644 --- a/tests/unit/test_music_server_client.gd +++ b/tests/unit/test_music_server_client.gd @@ -1,4 +1,5 @@ extends GdUnitTestSuite +## MusicServerClient: which paths it asks for and how it handles the answers. var _server: FakeHttpTransport @@ -115,7 +116,8 @@ func test_mark_song_played_sends_put() -> void: func test_character_name_is_url_encoded() -> void: - MusicServerClient.fetch_character_image("Paper Mario.png", func(_body: PackedByteArray) -> void: pass) + MusicServerClient.fetch_character_image("Paper Mario.png", + func(_body: PackedByteArray) -> void: pass) assert_array(_server.requested_paths).contains_exactly(["/character?name=Paper%20Mario.png"]) diff --git a/tests/unit/test_player_object.gd b/tests/unit/test_player_object.gd index e9753aa..524e735 100644 --- a/tests/unit/test_player_object.gd +++ b/tests/unit/test_player_object.gd @@ -1,4 +1,5 @@ extends GdUnitTestSuite +## PlayerObject: points, and the first point, match point and winner signals. var _winning_score: int var _player: PlayerObject @@ -20,7 +21,7 @@ func test_first_point_triggers_first_point_signal() -> void: _player.add_point() assert_int(_player.player_score).is_equal(1) - await assert_signal(_player).is_emitted("first_point_triggerd") + await assert_signal(_player).is_emitted("first_point_triggered") await assert_signal(_player).is_emitted("player_point_given") @@ -30,7 +31,7 @@ func test_one_point_from_winning_triggers_match_point() -> void: _player.add_point() - await assert_signal(_player).is_emitted("match_point_triggerd") + await assert_signal(_player).is_emitted("match_point_triggered") func test_reaching_winning_score_triggers_winner() -> void: @@ -41,7 +42,7 @@ func test_reaching_winning_score_triggers_winner() -> void: _player.add_point() assert_int(_player.player_score).is_equal(3) - await assert_signal(_player).is_emitted("winner_triggerd") + await assert_signal(_player).is_emitted("winner_triggered") func test_score_does_not_go_past_winning_score() -> void: @@ -91,7 +92,7 @@ func test_dropping_back_to_zero_allows_first_point_again() -> void: _player.add_point() - await assert_signal(_player).is_emitted("first_point_triggerd") + await assert_signal(_player).is_emitted("first_point_triggered") func test_reset_points() -> void: @@ -125,7 +126,7 @@ func test_winning_score_of_one_is_won_with_first_point() -> void: _player.add_point() - await assert_signal(_player).is_emitted("winner_triggerd") + await assert_signal(_player).is_emitted("winner_triggered") func test_first_point_is_match_point_when_winning_score_is_two() -> void: @@ -134,5 +135,5 @@ func test_first_point_is_match_point_when_winning_score_is_two() -> void: _player.add_point() - await assert_signal(_player).is_emitted("match_point_triggerd") + await assert_signal(_player).is_emitted("match_point_triggered") assert_bool(_player.is_first_point).is_false() diff --git a/tests/unit/test_playlist.gd b/tests/unit/test_playlist.gd index 01e7225..60fbd70 100644 --- a/tests/unit/test_playlist.gd +++ b/tests/unit/test_playlist.gd @@ -1,4 +1,5 @@ extends GdUnitTestSuite +## Playlist: moving through the songs, answering them and keeping points. var _server: FakeHttpTransport var _add_to_stats: bool diff --git a/tests/unit/test_search.gd b/tests/unit/test_search.gd index 53e761a..8b4454b 100644 --- a/tests/unit/test_search.gd +++ b/tests/unit/test_search.gd @@ -1,4 +1,5 @@ extends GdUnitTestSuite +## How the search popup finds and orders matching soundtracks. const SearchWindowScript: GDScript = preload("res://search_popup.gd") @@ -36,7 +37,8 @@ func test_ignores_spaces() -> void: func test_and_matches_ampersand() -> void: - assert_array(_search.find_matches("mario and luigi")).contains(["Mario & Luigi: Superstar Saga"]) + assert_array(_search.find_matches("mario and luigi")) \ + .contains(["Mario & Luigi: Superstar Saga"]) func test_accented_letters_match_plain_letters() -> void: diff --git a/tests/unit/test_settings.gd b/tests/unit/test_settings.gd index 3753150..2bfa5ca 100644 --- a/tests/unit/test_settings.gd +++ b/tests/unit/test_settings.gd @@ -1,4 +1,5 @@ extends GdUnitTestSuite +## Settings: the version number. func test_version_comes_from_project_settings() -> void: diff --git a/tests/unit/test_song_object.gd b/tests/unit/test_song_object.gd index 73f8fdd..a72b276 100644 --- a/tests/unit/test_song_object.gd +++ b/tests/unit/test_song_object.gd @@ -1,4 +1,5 @@ extends GdUnitTestSuite +## SongObject: what it shows before and after the answer, and its points. var _is_debug: bool var _song: SongObject diff --git a/volume_slider.gd b/volume_slider.gd index ffa168d..05f2138 100644 --- a/volume_slider.gd +++ b/volume_slider.gd @@ -1,19 +1,24 @@ class_name VolumeSlider extends HSlider +## A slider that sets the volume of an audio bus. -@export -var audio_bus_name := "music" +## The audio bus whose volume the slider sets. -@onready -var _bus: int +@export var audio_bus_name: String = "music" -func _ready(): +var _bus: int + + +func _ready() -> void: _bus = AudioServer.get_bus_index(audio_bus_name) value_changed.connect(_on_value_changed) value = db_to_linear(AudioServer.get_bus_volume_db(_bus)) -func _on_value_changed(changed_value: float) -> void: - AudioServer.set_bus_volume_db(_bus, linear_to_db(changed_value)) +## Sets the bus volume to [param changed_value], from 0 (silent) to 1 (full). func change_volume(changed_value: float) -> void: AudioServer.set_bus_volume_db(_bus, linear_to_db(changed_value)) + + +func _on_value_changed(changed_value: float) -> void: + AudioServer.set_bus_volume_db(_bus, linear_to_db(changed_value)) diff --git a/winner_popup.gd b/winner_popup.gd index 271b6e7..4330f3f 100644 --- a/winner_popup.gd +++ b/winner_popup.gd @@ -1,17 +1,11 @@ class_name WinnerPopup extends BasePopup +## Shows who won, with their character, and where the other players placed. -@onready -var winner_panel: Panel = %WinnerPanel - -@onready -var winner_picture: TextureRect = %WinnerPicture - -@onready -var winner_label: Label = %WinnerLabel - -@onready -var players_list: VBoxContainer = %PlayersList +@onready var winner_panel: Panel = %WinnerPanel +@onready var winner_picture: TextureRect = %WinnerPicture +@onready var winner_label: Label = %WinnerLabel +@onready var players_list: VBoxContainer = %PlayersList func _ready() -> void: @@ -54,6 +48,3 @@ func show_winner(winner: PlayerObject) -> void: label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER players_list.add_child(label) placing += 1 - - -