Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05655cf81c | ||
|
|
6fdd7a2b76 | ||
|
|
d13210e32b | ||
|
|
74caacc9b3 | ||
|
|
249462f4a1 | ||
|
|
40ed4da908 | ||
|
|
91adf52cf4 | ||
|
|
87ff3d899b | ||
|
|
4a275e6a88 |
No files matched your search
@@ -83,11 +83,6 @@ jobs:
|
|||||||
godot --headless --path . --import
|
godot --headless --path . --import
|
||||||
godot --headless --path . --export-release "Windows Desktop" "build/${PROJECT_NAME}_${GAME_VERSION}.exe"
|
godot --headless --path . --export-release "Windows Desktop" "build/${PROJECT_NAME}_${GAME_VERSION}.exe"
|
||||||
godot --headless --path . --export-release "Linux" "build/${PROJECT_NAME}_${GAME_VERSION}.x86_64"
|
godot --headless --path . --export-release "Linux" "build/${PROJECT_NAME}_${GAME_VERSION}.x86_64"
|
||||||
|
|
||||||
# Linux binaries can't hold an icon; the package adds it with a menu entry and installer
|
|
||||||
- name: Package Linux
|
|
||||||
run: |
|
|
||||||
linux/package.sh "build/${PROJECT_NAME}_${GAME_VERSION}.x86_64" "$GAME_VERSION" build
|
|
||||||
ls -la build
|
ls -la build
|
||||||
|
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
@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
|
||||||
|
const WHATS_NEW: Dictionary = {
|
||||||
|
"1.9.0": "Made improvements to the Search window
|
||||||
|
Made small changes to the inspiration list scrolling
|
||||||
|
Made small changes to the player list
|
||||||
|
Changed the term game to soundtrack
|
||||||
|
#28: Made changes to the sync window
|
||||||
|
#34: Fixed bug in Character Select when clicking or scrolling",
|
||||||
|
"1.8.5": "#9: New winner screen with positions of all players
|
||||||
|
#32: New setting for how long to hide the beginning
|
||||||
|
#33: Fixed bug with hiding beginning when song restarts",
|
||||||
|
"1.8.0": "#14: Updated About page
|
||||||
|
#15: Create new keymap page
|
||||||
|
#20: The winner song can now be stopped
|
||||||
|
#24: The speed of the inspiration list can now be changed
|
||||||
|
#29: New settings to hide time left and things like that
|
||||||
|
#31: Added a QR page",
|
||||||
|
"1.7.0": "#22: Now block add player if player name is empty
|
||||||
|
#23: Add a log for when points are given to players
|
||||||
|
#25: Fixed some graphical stuff
|
||||||
|
#26: Changed so the same character can be on a song multiple times
|
||||||
|
#27: Turning off statistics after win",
|
||||||
|
"1.6.1": "#17: Removed Debug text in release
|
||||||
|
#18: Fixed bug with new character select screen",
|
||||||
|
"1.6.0": "#1: Fixed bug with inspiration list not reloading after sync
|
||||||
|
#2: New dialog for sync
|
||||||
|
#4: Now downloads characters from the server
|
||||||
|
#5: New character select screen
|
||||||
|
#6: Now show progress during sync
|
||||||
|
#7: Blocking all requests to the server during sync
|
||||||
|
#8: Added functionality to cache more than one song",
|
||||||
|
"1.5.0": "Made big changes to players and the song list and how the local song list works",
|
||||||
|
"0.9.0-Beta": "Fixed settings and updated the player view",
|
||||||
|
"0.8.0-Beta": "Fixed reset buttons and some other small things",
|
||||||
|
"0.7.8-Beta": "Added shortcuts. Added dialog for winner. Started cleaning code.",
|
||||||
|
"0.7.5-Beta": "Added settings menu, most things don't do anything yet",
|
||||||
|
"0.7-Beta": "Can now hop between songs"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if self.visible == true:
|
||||||
|
if event is InputEventMouseButton && event.is_pressed():
|
||||||
|
var evLocal: InputEvent = make_input_local(event)
|
||||||
|
if !Rect2(Vector2(0, 0), about_panel.size).has_point(evLocal.position):
|
||||||
|
self.visible = false
|
||||||
|
|
||||||
|
|
||||||
|
func show_window() -> void:
|
||||||
|
visible = true
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
func hide_window() -> void:
|
||||||
|
visible = false
|
||||||
File renamed without changes.
@@ -0,0 +1,63 @@
|
|||||||
|
[gd_scene format=3 uid="uid://cp54idaudb8dp"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://b7bh0of7olbcb" path="res://AboutWindow.gd" id="1_about_script"]
|
||||||
|
|
||||||
|
[node name="AboutControl" type="Control" unique_id=1626010355]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_about_script")
|
||||||
|
|
||||||
|
[node name="AboutPanel" type="Panel" parent="." unique_id=1907299275]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 0
|
||||||
|
offset_right = 600.0
|
||||||
|
offset_bottom = 640.0
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="AboutPanel" unique_id=200295721]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 0
|
||||||
|
offset_right = 600.0
|
||||||
|
offset_bottom = 50.0
|
||||||
|
text = "About"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="ScrollContainer" type="ScrollContainer" parent="AboutPanel" unique_id=417677913]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 10.0
|
||||||
|
offset_top = 50.0
|
||||||
|
offset_right = 590.0
|
||||||
|
offset_bottom = 630.0
|
||||||
|
|
||||||
|
[node name="ContentVBox" type="VBoxContainer" parent="AboutPanel/ScrollContainer" unique_id=1746337912]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="VersionLabel" type="Label" parent="AboutPanel/ScrollContainer/ContentVBox" unique_id=352884126]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="DescriptionLabel" type="Label" parent="AboutPanel/ScrollContainer/ContentVBox" unique_id=543094263]
|
||||||
|
texture_filter = 1
|
||||||
|
custom_minimum_size = Vector2(560, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
text = "Try your video game music knowledge with this VGM randomizer, invite your friends and see who is the best."
|
||||||
|
horizontal_alignment = 1
|
||||||
|
autowrap_mode = 2
|
||||||
|
|
||||||
|
[node name="WhatsNewLabel" type="Label" parent="AboutPanel/ScrollContainer/ContentVBox" unique_id=1363040291]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
texture_filter = 1
|
||||||
|
custom_minimum_size = Vector2(560, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
autowrap_mode = 2
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var character_grid_container := $ScrollContainer2
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var character_grid := $ScrollContainer2/GridContainer
|
||||||
|
|
||||||
|
signal character_selected(file_name)
|
||||||
|
|
||||||
|
# Called when the node enters the scene tree for the first time.
|
||||||
|
func _ready():
|
||||||
|
var characters := DirAccess.open("res://characters/")
|
||||||
|
if characters:
|
||||||
|
characters.list_dir_begin()
|
||||||
|
var file_name = characters.get_next()
|
||||||
|
while file_name != "":
|
||||||
|
file_name = file_name.replace('.import', '') # <--- remove the .import
|
||||||
|
if file_name.ends_with(".png"):
|
||||||
|
|
||||||
|
var texture = load("res://characters/" + file_name)
|
||||||
|
|
||||||
|
var texture_btn := TextureButton.new()
|
||||||
|
|
||||||
|
character_grid.add_child(texture_btn)
|
||||||
|
texture_btn.custom_minimum_size = Vector2(80, 40)
|
||||||
|
texture_btn.ignore_texture_size = true
|
||||||
|
texture_btn.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT
|
||||||
|
texture_btn.texture_normal = texture
|
||||||
|
|
||||||
|
texture_btn.pressed.connect(select_character.bind(file_name))
|
||||||
|
|
||||||
|
file_name = characters.get_next()
|
||||||
|
|
||||||
|
func show_grid():
|
||||||
|
character_grid_container.visible = true
|
||||||
|
|
||||||
|
func select_character(file_name: String):
|
||||||
|
print("select_character")
|
||||||
|
character_selected.emit(file_name)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cmxl2w45vvmx6
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://b16on0oscg1bv"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://cmxl2w45vvmx6" path="res://CharacterSelect.gd" id="1_4bba6"]
|
||||||
|
|
||||||
|
[node name="CharacterSelect" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 0
|
||||||
|
offset_right = 40.0
|
||||||
|
offset_bottom = 40.0
|
||||||
|
script = ExtResource("1_4bba6")
|
||||||
|
|
||||||
|
[node name="ScrollContainer2" type="ScrollContainer" parent="."]
|
||||||
|
clip_contents = false
|
||||||
|
custom_minimum_size = Vector2(100, 100)
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 194.0
|
||||||
|
offset_top = 152.0
|
||||||
|
offset_right = 670.0
|
||||||
|
offset_bottom = 375.0
|
||||||
|
|
||||||
|
[node name="GridContainer" type="GridContainer" parent="ScrollContainer2"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
columns = 3
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
extends ScrollContainer
|
||||||
|
|
||||||
|
# Scroll direction: 1 = down, -1 = up
|
||||||
|
var scroll_direction: int = 1
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
# Don't scroll if list is empty or too short
|
||||||
|
if scroll_bar.max_value <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
var current_pos = 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)
|
||||||
|
else:
|
||||||
|
# Reached bottom, reverse direction
|
||||||
|
scroll_direction = -1
|
||||||
|
scroll_bar.value = scroll_bar.max_value - scroll_bar.page
|
||||||
|
else:
|
||||||
|
# Scrolling up
|
||||||
|
if current_pos > 0:
|
||||||
|
scroll_bar.value = max(current_pos + scroll_amount, 0)
|
||||||
|
else:
|
||||||
|
# Reached top, reverse direction
|
||||||
|
scroll_direction = 1
|
||||||
|
scroll_bar.value = 0
|
||||||
File renamed without changes.
@@ -0,0 +1,7 @@
|
|||||||
|
[gd_resource type="Theme" load_steps=2 format=3 uid="uid://rxexo3ur85as"]
|
||||||
|
|
||||||
|
[ext_resource type="StyleBox" uid="uid://blf3g88mugovr" path="res://neutral_gray.tres" id="1_alhp2"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
Label/styles/normal = ExtResource("1_alhp2")
|
||||||
|
PanelContainer/styles/panel = ExtResource("1_alhp2")
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://bijh5h5yrivm3"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://cw41y87l64qo7" path="res://log.gd" id="1_q72ua"]
|
||||||
|
|
||||||
|
[node name="Control" type="Control"]
|
||||||
|
custom_minimum_size = Vector2(700, 700)
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_q72ua")
|
||||||
|
|
||||||
|
[node name="LogPanel" type="Panel" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visibility_layer = 513
|
||||||
|
custom_minimum_size = Vector2(700, 700)
|
||||||
|
layout_mode = 0
|
||||||
|
offset_right = 40.0
|
||||||
|
offset_bottom = 40.0
|
||||||
|
|
||||||
|
[node name="LogScrollContainer" type="ScrollContainer" parent="LogPanel"]
|
||||||
|
custom_minimum_size = Vector2(700, 700)
|
||||||
|
layout_mode = 0
|
||||||
|
offset_right = 40.0
|
||||||
|
offset_bottom = 40.0
|
||||||
|
|
||||||
|
[node name="LogVBoxContainer" type="VBoxContainer" parent="LogPanel/LogScrollContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(700, 700)
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 1
|
||||||
+556
@@ -0,0 +1,556 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
# 601 LOC 11/9 - 2023
|
||||||
|
# 628 LOC 15/9 - 2023
|
||||||
|
# 631 LOC 16/9 - 2023
|
||||||
|
# 554 LOC 30/5 - 2024
|
||||||
|
# 551 LOC 3/6 - 2024
|
||||||
|
# 386 LOC 12/6 - 2024
|
||||||
|
|
||||||
|
##TODO
|
||||||
|
# 15. Fix for local play
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var open_button: Button = $Open
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var fileDialog: FileDialog = $FileDialog
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var inspiration_scroll: ScrollContainer = $ScrollContainer
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var inspiration_list: VBoxContainer = $ScrollContainer/VBoxContainer
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var soundtrack_label: Label = $VBoxContainer/SoundtrackLabel
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var song_label: Label = $VBoxContainer/SongLabel
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var add_player_container: HBoxContainer = $Players/VBoxContainer/AddPlayerContainer
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var add_players_button: TextureButton = $Players/VBoxContainer/HBoxContainer/AddPlayersButton
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var add_player_button: Button = $Players/VBoxContainer/AddPlayerContainer/AddPlayerButton
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var new_player_name_field: TextEdit = $Players/VBoxContainer/AddPlayerContainer/PlayerNameField
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var reset_playlist_button: Button = $ResetPlaylistButton
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var reset_points_button: Button = $ResetPointsButton
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var sound_test_button: Button = $SoundTestButton
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var sync_button: Button = $SyncButton
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var sync_window: Control = %SyncWindow
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var settings_button: Button = $SettingsButton
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var settings_window: Control = $SettingsWindow
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var shortcuts_window: Control = %ShortcutsWindow
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var qr_window: Control = %QRWindow
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var winner_window: Control = %WinnerWindow
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var qr_button: Button = $QRButton
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var statistics_button: Button = $StatisticsButton
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var statistic_popup: PopupPanel = $StatisticsPopupPanel
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var statistic_label: Label = $StatisticsPopupPanel/StatisticsLabel
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var shortcuts_button: Button = $ShortcutsButton
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var about_button: Button = $AboutButton
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var about_window: Control = %AboutWindow
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var show_answer_button: Button = $ShowAnswerButton
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var next_button: Button = $NextButton
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var music_list_scroll: ScrollContainer = $MusicListPanel/ScrollContainer
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var music_list: VBoxContainer = $MusicListPanel/ScrollContainer/MusicList
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var player_container: VBoxContainer = $Players/VBoxContainer
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var players_list_container: VBoxContainer = $Players/VBoxContainer/ScrollContainer/PlayersListVBoxContainer
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var character_select: Control = $CharacterSelect
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var new_character_select: Control = %NewCharacterSelect
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var search_button: Button = $SearchButton
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var search_view: Control= $Search
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var auto_repeat_song_button: CheckButton = $RepeatSongCheckButton
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var music_player_container: PanelContainer = $MusicPlayer
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var log_window: Control = %Log
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var debug_label: Label = $DebugLabel
|
||||||
|
|
||||||
|
var player: PackedScene = preload("res://Player.tscn")
|
||||||
|
var song_list_object: PackedScene = preload("res://song_list_object.tscn")
|
||||||
|
|
||||||
|
var songs: Array= []
|
||||||
|
var soundtracks: Array = []
|
||||||
|
var current_player: Node
|
||||||
|
|
||||||
|
## Seconds to wait before retrying a song download step that failed.
|
||||||
|
var song_fetch_retry_seconds: float = 5.0
|
||||||
|
|
||||||
|
# Called when the node enters the scene tree for the first time.
|
||||||
|
func _ready() -> void:
|
||||||
|
var debug: bool = OS.has_feature("debug")
|
||||||
|
var is_mac: bool = OS.has_feature("macos")
|
||||||
|
if debug:
|
||||||
|
print("is_debug")
|
||||||
|
debug_label.visible = true
|
||||||
|
Settings.is_debug = true
|
||||||
|
settings_window.set_winning_score(5)
|
||||||
|
settings_window.set_selected_server(4)
|
||||||
|
if is_mac:
|
||||||
|
print("is_mac")
|
||||||
|
next_button.pressed.connect(play_next_song)
|
||||||
|
show_answer_button.pressed.connect(show_answer_pressed)
|
||||||
|
search_button.pressed.connect(show_search)
|
||||||
|
sync_button.pressed.connect(sync_soundtracks)
|
||||||
|
sound_test_button.pressed.connect(music_player_container.get_sound_test_song)
|
||||||
|
statistics_button.pressed.connect(get_statistics)
|
||||||
|
shortcuts_button.pressed.connect(show_shortcuts)
|
||||||
|
about_button.pressed.connect(show_about)
|
||||||
|
settings_button.pressed.connect(show_settings)
|
||||||
|
qr_button.pressed.connect(show_qr)
|
||||||
|
reset_playlist_button.pressed.connect(reset_playlist)
|
||||||
|
reset_points_button.pressed.connect(reset_points)
|
||||||
|
|
||||||
|
character_select.connect("character_selected", _on_character_selected)
|
||||||
|
new_character_select.connect("new_new_character_selected", _on_new_character_selected)
|
||||||
|
new_player_name_field.connect("enter_key_pressed", add_player)
|
||||||
|
music_player_container.connect("play_next_song", play_next_song)
|
||||||
|
music_player_container.connect("update_song_list", update_song_list)
|
||||||
|
settings_window.connect("server_changed", server_updated)
|
||||||
|
settings_window.visibility_changed.connect(_on_settings_window_visibility_changed)
|
||||||
|
sync_window.connect("sync_started", _on_sync_started)
|
||||||
|
sync_window.connect("sync_finished", _on_sync_finished)
|
||||||
|
|
||||||
|
|
||||||
|
add_players_button.pressed.connect(add_players)
|
||||||
|
add_player_button.pressed.connect(add_player)
|
||||||
|
open_button.pressed.connect(open)
|
||||||
|
auto_repeat_song_button.pressed.connect(repeat_song)
|
||||||
|
|
||||||
|
get_suggestion_list()
|
||||||
|
fetch_song_list_at_start()
|
||||||
|
fetch_song()
|
||||||
|
search_view.get_list_of_soundtracks()
|
||||||
|
new_character_select.load_characters()
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventKey and event.pressed:
|
||||||
|
if event.alt_pressed && event.keycode == KEY_UP:
|
||||||
|
print("Alt + UP pressed")
|
||||||
|
music_player_container.change_volume(0.05)
|
||||||
|
if event.alt_pressed && event.keycode == KEY_DOWN:
|
||||||
|
print("Alt + DOWN pressed")
|
||||||
|
music_player_container.change_volume(-0.05)
|
||||||
|
if event.alt_pressed && event.ctrl_pressed && event.keycode == KEY_LEFT:
|
||||||
|
print("Ctrl + Alt + LEFT pressed")
|
||||||
|
music_player_container.restart()
|
||||||
|
elif event.alt_pressed && event.keycode == KEY_LEFT:
|
||||||
|
print("Alt + LEFT pressed")
|
||||||
|
music_player_container.seek(-5)
|
||||||
|
if event.alt_pressed && event.keycode == KEY_RIGHT:
|
||||||
|
print("Alt + RIGHT pressed")
|
||||||
|
music_player_container.seek(5)
|
||||||
|
if event.alt_pressed && event.keycode == KEY_ENTER:
|
||||||
|
print("Alt + Enter pressed")
|
||||||
|
Settings.fullscreen = !Settings.fullscreen
|
||||||
|
settings_window.fullscreen_button.button_pressed = Settings.fullscreen
|
||||||
|
if Settings.fullscreen == true:
|
||||||
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
|
||||||
|
if Settings.fullscreen == false:
|
||||||
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
||||||
|
if event.alt_pressed && event.keycode == KEY_K:
|
||||||
|
shortcuts_window.visible = !shortcuts_window.visible
|
||||||
|
if event.alt_pressed && event.keycode == KEY_L:
|
||||||
|
log_window.visible = !log_window.visible
|
||||||
|
|
||||||
|
func server_updated() -> void:
|
||||||
|
print("server_updated")
|
||||||
|
update_from_server()
|
||||||
|
reset_points()
|
||||||
|
|
||||||
|
func update_from_server() -> void:
|
||||||
|
print("update_from_server")
|
||||||
|
search_view.get_list_of_soundtracks()
|
||||||
|
get_suggestion_list()
|
||||||
|
reset_playlist()
|
||||||
|
new_character_select.load_characters()
|
||||||
|
|
||||||
|
func repeat_song() -> void:
|
||||||
|
Settings.auto_repeat_song = !Settings.auto_repeat_song
|
||||||
|
|
||||||
|
func show_answer_pressed() -> void:
|
||||||
|
print("show_answer_pressed")
|
||||||
|
Playlist.song_is_answered()
|
||||||
|
update_song_list()
|
||||||
|
|
||||||
|
func fetch_song_list_at_start() -> void:
|
||||||
|
print("fetch_song_list_at_start")
|
||||||
|
var fetch_song_list_at_start_done: Callable = func fetch_songs_at_start_done(data) -> void:
|
||||||
|
Playlist.clear_song_list()
|
||||||
|
if data == null:
|
||||||
|
Playlist.update_last_played_song()
|
||||||
|
update_song_list()
|
||||||
|
return
|
||||||
|
if typeof(data) == TYPE_ARRAY:
|
||||||
|
for d: Dictionary in data:
|
||||||
|
var new_song_object: SongObject = SongObject.new()
|
||||||
|
new_song_object.song_number = d.SongNo
|
||||||
|
new_song_object.soundtrack_title = d.Soundtrack
|
||||||
|
new_song_object.song_title = d.Song
|
||||||
|
new_song_object.is_answered = true
|
||||||
|
new_song_object.has_played = true
|
||||||
|
Playlist.add_to_playlist(new_song_object)
|
||||||
|
else:
|
||||||
|
print("Unexpected data")
|
||||||
|
Playlist.update_last_played_song()
|
||||||
|
update_song_list()
|
||||||
|
MusicServerClient.fetch_song_list(fetch_song_list_at_start_done)
|
||||||
|
|
||||||
|
func play_next_song() -> void:
|
||||||
|
print("play_next_song ", Playlist.latest_played_song)
|
||||||
|
if Playlist.first_song_played:
|
||||||
|
Playlist.set_latest_played_song()
|
||||||
|
Playlist.first_song_played = true
|
||||||
|
music_player_container.play_song_object(Playlist.latest_played_song)
|
||||||
|
if !(Playlist.get_preloaded_songs() > 0):
|
||||||
|
next_button.disabled = true
|
||||||
|
if !Playlist.is_song_downloading:
|
||||||
|
fetch_song()
|
||||||
|
|
||||||
|
#not first "/music/addPlayed"
|
||||||
|
#"/music/rand"
|
||||||
|
#"/music/addQue"
|
||||||
|
#"/music/info"
|
||||||
|
# The cache size can be changed in the settings. Waiting until the settings are closed
|
||||||
|
# avoids starting downloads while the user is still clicking the number up and down.
|
||||||
|
func _on_settings_window_visibility_changed() -> void:
|
||||||
|
if !settings_window.visible && !Playlist.is_song_downloading && Playlist.should_preload_more_songs():
|
||||||
|
fetch_song()
|
||||||
|
|
||||||
|
func fetch_song() -> void:
|
||||||
|
print("fetch_song")
|
||||||
|
Playlist.is_song_downloading = true
|
||||||
|
_download_random_song(SongObject.new())
|
||||||
|
|
||||||
|
# Fetching a song takes three requests. If one fails, only that step is retried, so
|
||||||
|
# the server and the game keep the same songs in their playlists.
|
||||||
|
func _download_random_song(new_song_object: SongObject) -> void:
|
||||||
|
var song_downloaded: Callable = func song_downloaded(body: PackedByteArray) -> void:
|
||||||
|
print("song_downloaded")
|
||||||
|
var sound: AudioStream = AudioStreamMP3.new()
|
||||||
|
sound.data = body
|
||||||
|
new_song_object.song = sound
|
||||||
|
_add_song_to_queue(new_song_object)
|
||||||
|
MusicServerClient.fetch_random_song(Settings.use_low_played_mode, song_downloaded,
|
||||||
|
_retry_later.bind(_download_random_song.bind(new_song_object)))
|
||||||
|
|
||||||
|
func _add_song_to_queue(new_song_object: SongObject) -> void:
|
||||||
|
MusicServerClient.add_to_queue(_fetch_song_info.bind(new_song_object),
|
||||||
|
_retry_later.bind(_add_song_to_queue.bind(new_song_object)))
|
||||||
|
|
||||||
|
func _fetch_song_info(new_song_object: SongObject) -> void:
|
||||||
|
var song_info_fetched: Callable = func song_info_fetched(data_received: Dictionary) -> void:
|
||||||
|
print("song_info_fetched")
|
||||||
|
new_song_object.song_number = data_received.SongNo
|
||||||
|
new_song_object.soundtrack_title = data_received.Soundtrack
|
||||||
|
new_song_object.song_title = data_received.Song
|
||||||
|
print("new_song_object: ", new_song_object.to_string())
|
||||||
|
Playlist.add_to_playlist(new_song_object)
|
||||||
|
next_button.disabled = false
|
||||||
|
if Playlist.should_preload_more_songs():
|
||||||
|
fetch_song()
|
||||||
|
else:
|
||||||
|
Playlist.is_song_downloading = false
|
||||||
|
if Settings.is_debug:
|
||||||
|
update_song_list()
|
||||||
|
MusicServerClient.fetch_song_info(song_info_fetched,
|
||||||
|
_retry_later.bind(_fetch_song_info.bind(new_song_object)))
|
||||||
|
|
||||||
|
func _retry_later(step: Callable) -> void:
|
||||||
|
print("Song download failed, retrying in %s seconds" % song_fetch_retry_seconds)
|
||||||
|
get_tree().create_timer(song_fetch_retry_seconds).timeout.connect(step)
|
||||||
|
|
||||||
|
func update_song_list() -> void:
|
||||||
|
print("update_song_list")
|
||||||
|
if Playlist.currently_playing_song >= 0:
|
||||||
|
soundtrack_label.text = Playlist.get_current_soundtrack_title()
|
||||||
|
song_label.text = Playlist.get_current_song_title()
|
||||||
|
Settings.delete_children(music_list)
|
||||||
|
for s: SongObject in Playlist.get_playlist():
|
||||||
|
var new_song_list_object: SongListObject = song_list_object.instantiate()
|
||||||
|
new_song_list_object.song_object = s
|
||||||
|
new_song_list_object.gui_input.connect(song_object_clicked.bind(s.song_number))
|
||||||
|
music_list.add_child(new_song_list_object)
|
||||||
|
|
||||||
|
func song_object_clicked(event: InputEvent, song_no: int) -> void:
|
||||||
|
if (event is InputEventMouseButton && event.pressed && event.button_index == MOUSE_BUTTON_LEFT):
|
||||||
|
print("Clicked Song Object: ", Playlist.song_object_array[song_no].to_string())
|
||||||
|
if Settings.character_select_open == false:
|
||||||
|
if Playlist.get_song(song_no) == null:
|
||||||
|
print("has no song, need to download")
|
||||||
|
var song_downloaded: Callable = func song_downloaded(body: PackedByteArray) -> void:
|
||||||
|
var sound: AudioStream = AudioStreamMP3.new()
|
||||||
|
sound.data = body
|
||||||
|
Playlist.add_song_to_song_object(song_no, sound)
|
||||||
|
music_player_container.play_song_object(song_no)
|
||||||
|
MusicServerClient.fetch_song(song_no, song_downloaded)
|
||||||
|
else:
|
||||||
|
print("song already downloaded")
|
||||||
|
music_player_container.play_song_object(song_no)
|
||||||
|
|
||||||
|
func reset_playlist() -> void:
|
||||||
|
print("reset_playlist")
|
||||||
|
var playlist_reset_done: Callable = func playlist_reset_done() -> void:
|
||||||
|
Playlist.reset_playlist()
|
||||||
|
fetch_song_list_at_start()
|
||||||
|
fetch_song()
|
||||||
|
|
||||||
|
MusicServerClient.reset_playlist(playlist_reset_done)
|
||||||
|
|
||||||
|
func reset_points() -> void:
|
||||||
|
print("reset_points")
|
||||||
|
for player_objects: PlayerObject in Settings.player_array:
|
||||||
|
player_objects.reset_points()
|
||||||
|
load_players()
|
||||||
|
|
||||||
|
func show_about() -> void:
|
||||||
|
print("show_about")
|
||||||
|
about_window.show_window()
|
||||||
|
|
||||||
|
func show_shortcuts() -> void:
|
||||||
|
print("show_shortcuts")
|
||||||
|
shortcuts_window.visible = true
|
||||||
|
|
||||||
|
func show_settings() -> void:
|
||||||
|
print("show_settings")
|
||||||
|
settings_window.visible = true
|
||||||
|
|
||||||
|
func show_qr() -> void:
|
||||||
|
print("show_qr")
|
||||||
|
qr_window.show_window()
|
||||||
|
|
||||||
|
func get_statistics() -> void:
|
||||||
|
print("get_statistics")
|
||||||
|
statistic_popup.visible = true
|
||||||
|
statistic_label.text = "Total amount of soundtracks in the playlist: " + str(soundtracks.size())
|
||||||
|
|
||||||
|
func sync_soundtracks() -> void:
|
||||||
|
print("sync_soundtracks")
|
||||||
|
sync_window.start_sync()
|
||||||
|
|
||||||
|
func _on_sync_started() -> void:
|
||||||
|
print("_on_sync_started")
|
||||||
|
sync_button.disabled = false
|
||||||
|
sound_test_button.disabled = true
|
||||||
|
reset_playlist_button.disabled = true
|
||||||
|
show_answer_button.disabled = true
|
||||||
|
next_button.disabled = true
|
||||||
|
|
||||||
|
func _on_sync_finished() -> void:
|
||||||
|
print("_on_sync_finished")
|
||||||
|
update_from_server()
|
||||||
|
sound_test_button.disabled = false
|
||||||
|
reset_playlist_button.disabled = false
|
||||||
|
show_answer_button.disabled = false
|
||||||
|
reset_points()
|
||||||
|
|
||||||
|
func get_suggestion_list() -> void:
|
||||||
|
print("get_suggestion_list")
|
||||||
|
var populate_list: Callable = func populate_list(array) -> void:
|
||||||
|
if typeof(array) == TYPE_ARRAY:
|
||||||
|
soundtracks = []
|
||||||
|
Settings.delete_children(inspiration_list)
|
||||||
|
soundtracks.append_array(array)
|
||||||
|
for soundtrack: String in soundtracks:
|
||||||
|
var inspiration_label: Label = Label.new()
|
||||||
|
inspiration_label.set_texture_filter(TextureFilter.TEXTURE_FILTER_NEAREST)
|
||||||
|
inspiration_label.add_theme_font_size_override("font_size", 20)
|
||||||
|
inspiration_label.text = soundtrack
|
||||||
|
inspiration_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||||
|
inspiration_list.add_child(inspiration_label)
|
||||||
|
else:
|
||||||
|
print("Unexpected data")
|
||||||
|
MusicServerClient.fetch_all_soundtracks(populate_list)
|
||||||
|
|
||||||
|
func add_players() -> void:
|
||||||
|
print("add_players")
|
||||||
|
Settings.edit_players = !Settings.edit_players
|
||||||
|
add_player_container.visible = !add_player_container.visible
|
||||||
|
new_player_name_field.grab_focus()
|
||||||
|
|
||||||
|
func add_player() -> void:
|
||||||
|
print("add_player")
|
||||||
|
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)
|
||||||
|
new_player_object.connect("first_point_triggerd", music_player_container._on_point_triggered.bind("first"))
|
||||||
|
new_player_object.connect("match_point_triggerd", music_player_container._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))
|
||||||
|
load_players()
|
||||||
|
|
||||||
|
func load_players() -> void:
|
||||||
|
print("load_players")
|
||||||
|
Settings.delete_player_children(players_list_container)
|
||||||
|
var counter: int = 0
|
||||||
|
for player_object: PlayerObject in Settings.player_array:
|
||||||
|
print(player_object.player_name)
|
||||||
|
|
||||||
|
var new_player: Player = player.instantiate()
|
||||||
|
new_player.player_object = player_object
|
||||||
|
new_player.list_index = counter
|
||||||
|
|
||||||
|
new_player.connect("change_character_clicked", _on_player_change_character_clicked.bind(new_player, player_object.player_name))
|
||||||
|
new_player.connect("player_removed", _on_player_removed.bind(player_object))
|
||||||
|
|
||||||
|
players_list_container.add_child(new_player)
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
func _on_point_given(player_object: PlayerObject) -> void:
|
||||||
|
print("_on_point_given")
|
||||||
|
log_window.add_log_row(player_object.player_name + " got a point")
|
||||||
|
if Playlist.currently_playing_song >= 0:
|
||||||
|
Playlist.add_point(player_object.id)
|
||||||
|
update_song_list()
|
||||||
|
|
||||||
|
func _on_make_point_given_sound() -> void:
|
||||||
|
music_player_container.play_sound_effect(preload("res://sounds/itemopen.wav"))
|
||||||
|
|
||||||
|
func _on_point_taken(player_object: PlayerObject) -> void:
|
||||||
|
print("_on_point_taken")
|
||||||
|
log_window.add_log_row(player_object.player_name + " lost a point")
|
||||||
|
music_player_container.play_sound_effect(preload("res://sounds/itemequip.wav"))
|
||||||
|
if Playlist.currently_playing_song >= 0:
|
||||||
|
Playlist.remove_point(player_object.id)
|
||||||
|
update_song_list()
|
||||||
|
|
||||||
|
func _on_player_won(player_object: PlayerObject) -> void:
|
||||||
|
print("_on_player_won")
|
||||||
|
winner_window.show_winner(player_object)
|
||||||
|
music_player_container.play_song(preload("res://sounds/winning.mp3"))
|
||||||
|
Settings.add_to_stats = false
|
||||||
|
|
||||||
|
func _on_player_removed(player_object: PlayerObject) -> void:
|
||||||
|
print("_on_player_removed ", player_object.player_name)
|
||||||
|
Settings.player_array.erase(player_object)
|
||||||
|
load_players()
|
||||||
|
Playlist.remove_player_from_list(player_object.id)
|
||||||
|
update_song_list()
|
||||||
|
|
||||||
|
func _on_player_change_character_clicked(new_player: Player, player_name: String) -> void:
|
||||||
|
print("_on_player_change_character_clicked")
|
||||||
|
current_player = new_player
|
||||||
|
print("Change character for: " + player_name)
|
||||||
|
new_character_select.show_grid(player_name)
|
||||||
|
|
||||||
|
func _on_character_selected(file_name: String) -> void:
|
||||||
|
print("_on_character_selected")
|
||||||
|
character_select.visible = false
|
||||||
|
current_player.character_selected(file_name)
|
||||||
|
|
||||||
|
func _on_new_character_selected(character_texture: ImageTexture) -> void:
|
||||||
|
print("_on_new_character_selected")
|
||||||
|
current_player.new_character_selected(character_texture)
|
||||||
|
|
||||||
|
func show_search() -> void:
|
||||||
|
print("show_search")
|
||||||
|
if search_view.visible == false:
|
||||||
|
search_view.visible = true
|
||||||
|
else:
|
||||||
|
search_view.visible = false
|
||||||
|
|
||||||
|
|
||||||
|
###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 open() -> void:
|
||||||
|
fileDialog.popup()
|
||||||
|
|
||||||
|
func _on_file_dialog_dir_selected(path: String) -> void:
|
||||||
|
print(path)
|
||||||
|
dir_contents(path)
|
||||||
|
|
||||||
|
func dir_contents(path: String) -> void:
|
||||||
|
var dir: DirAccess = DirAccess.open(path)
|
||||||
|
if dir:
|
||||||
|
dir.list_dir_begin()
|
||||||
|
var file_name: String = dir.get_next()
|
||||||
|
songs.clear()
|
||||||
|
soundtracks = []
|
||||||
|
while file_name != "":
|
||||||
|
if dir.current_is_dir():
|
||||||
|
#print("Found directory: " + file_name)
|
||||||
|
soundtracks.append(file_name)
|
||||||
|
else:
|
||||||
|
#print("Found file: " + file_name)
|
||||||
|
if file_name.ends_with(".mp3"):
|
||||||
|
songs.append(path + "/" + file_name)
|
||||||
|
file_name = dir.get_next()
|
||||||
|
else:
|
||||||
|
print("An error occurred when trying to access the path.")
|
||||||
|
|
||||||
|
for soundtrack: String in soundtracks:
|
||||||
|
var local_inspiration_label: Label = Label.new()
|
||||||
|
local_inspiration_label.text = soundtrack
|
||||||
|
inspiration_list.add_child(local_inspiration_label)
|
||||||
File renamed without changes.
+527
@@ -0,0 +1,527 @@
|
|||||||
|
[gd_scene format=3 uid="uid://xwq863o6uvsu"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://b8f4g15cas2j2" path="res://MainWindow.gd" id="1_eu0t5"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://b16on0oscg1bv" path="res://CharacterSelect.tscn" id="2_76kf4"]
|
||||||
|
[ext_resource type="Script" uid="uid://elgn7gkxgagq" path="res://InsperationScrollContainer.gd" id="2_gxtxm"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://bcfmpd7h512ef" path="res://icons/person_add_light.svg" id="5_31tjv"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://ds15cgsf8vpvc" path="res://MusicPlayer.tscn" id="5_emn36"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://cdy4kvemwaiom" path="res://new_character_select.tscn" id="6_dhrvg"]
|
||||||
|
[ext_resource type="Script" uid="uid://gf1bk6xbe3j" path="res://MusicListScrollContainer.gd" id="7_dj026"]
|
||||||
|
[ext_resource type="Script" uid="uid://cgakje8gb37tf" path="res://PlayerNameField.gd" id="7_qsdfy"]
|
||||||
|
[ext_resource type="Theme" uid="uid://rxexo3ur85as" path="res://LightGrayTheme.tres" id="7_wxbv6"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://bxydgil1yifps" path="res://SearchWindow.tscn" id="9_5ijvr"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dpdem7pdxweb5" path="res://SyncWindow.tscn" id="10_yxw0b"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dldpeo5y3l5hq" path="res://SettingsWindow.tscn" id="11_k62u5"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://bd1by80q1v27e" path="res://shortcutsWindow.tscn" id="13_8eebo"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://bijh5h5yrivm3" path="res://Log.tscn" id="14_26rwn"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://btupbowehiyyu" path="res://QRWindow.tscn" id="15_qr_window"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://cp54idaudb8dp" path="res://AboutWindow.tscn" id="16_about_window"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://cxlsmxnrp0agh" path="res://WinnerWindow.tscn" id="17_winner_window"]
|
||||||
|
|
||||||
|
[sub_resource type="LabelSettings" id="LabelSettings_ychxr"]
|
||||||
|
font_size = 25
|
||||||
|
|
||||||
|
[sub_resource type="LabelSettings" id="LabelSettings_qspbu"]
|
||||||
|
font_size = 50
|
||||||
|
|
||||||
|
[sub_resource type="LabelSettings" id="LabelSettings_3m52w"]
|
||||||
|
font_size = 35
|
||||||
|
|
||||||
|
[sub_resource type="InputEventKey" id="InputEventKey_03bm3"]
|
||||||
|
device = -1
|
||||||
|
alt_pressed = true
|
||||||
|
keycode = 65
|
||||||
|
unicode = 63743
|
||||||
|
|
||||||
|
[sub_resource type="Shortcut" id="Shortcut_jafqj"]
|
||||||
|
events = [SubResource("InputEventKey_03bm3")]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qk7sj"]
|
||||||
|
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_k1ygi"]
|
||||||
|
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
|
||||||
|
|
||||||
|
[sub_resource type="InputEventKey" id="InputEventKey_06rl4"]
|
||||||
|
device = -1
|
||||||
|
alt_pressed = true
|
||||||
|
keycode = 86
|
||||||
|
unicode = 8249
|
||||||
|
|
||||||
|
[sub_resource type="Shortcut" id="Shortcut_a7fvb"]
|
||||||
|
events = [SubResource("InputEventKey_06rl4")]
|
||||||
|
|
||||||
|
[sub_resource type="InputEventKey" id="InputEventKey_7dnqw"]
|
||||||
|
device = -1
|
||||||
|
alt_pressed = true
|
||||||
|
keycode = 67
|
||||||
|
unicode = 231
|
||||||
|
|
||||||
|
[sub_resource type="Shortcut" id="Shortcut_d6fml"]
|
||||||
|
events = [SubResource("InputEventKey_7dnqw")]
|
||||||
|
|
||||||
|
[sub_resource type="InputEventKey" id="InputEventKey_ujjlu"]
|
||||||
|
device = -1
|
||||||
|
alt_pressed = true
|
||||||
|
keycode = 83
|
||||||
|
|
||||||
|
[sub_resource type="Shortcut" id="Shortcut_fbju4"]
|
||||||
|
events = [SubResource("InputEventKey_ujjlu")]
|
||||||
|
|
||||||
|
[node name="Control" type="Control" unique_id=407149696]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 0
|
||||||
|
script = ExtResource("1_eu0t5")
|
||||||
|
|
||||||
|
[node name="TopPanelContainer" type="PanelContainer" parent="." unique_id=663404711]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_right = 1920.0
|
||||||
|
offset_bottom = 56.0
|
||||||
|
|
||||||
|
[node name="Title" type="Label" parent="." unique_id=1187907861]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 64.0
|
||||||
|
offset_top = 12.0
|
||||||
|
offset_right = 369.0
|
||||||
|
offset_bottom = 50.0
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
text = "Music Player Randomizer"
|
||||||
|
label_settings = SubResource("LabelSettings_ychxr")
|
||||||
|
|
||||||
|
[node name="Open" type="Button" parent="." unique_id=165052329]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 1296.0
|
||||||
|
offset_top = 66.0
|
||||||
|
offset_right = 1346.0
|
||||||
|
offset_bottom = 97.0
|
||||||
|
text = "Open"
|
||||||
|
|
||||||
|
[node name="FileDialog" type="FileDialog" parent="." unique_id=485771559]
|
||||||
|
title = "Open a Directory"
|
||||||
|
initial_position = 2
|
||||||
|
size = Vector2i(616, 400)
|
||||||
|
ok_button_text = "Select Current Folder"
|
||||||
|
file_mode = 2
|
||||||
|
access = 2
|
||||||
|
show_hidden_files = true
|
||||||
|
|
||||||
|
[node name="MusicListPanel" type="PanelContainer" parent="." unique_id=2011292375]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 64.0
|
||||||
|
offset_top = 384.0
|
||||||
|
offset_right = 1152.0
|
||||||
|
offset_bottom = 896.0
|
||||||
|
theme = ExtResource("7_wxbv6")
|
||||||
|
|
||||||
|
[node name="ScrollContainer" type="ScrollContainer" parent="MusicListPanel" unique_id=707750459]
|
||||||
|
layout_mode = 2
|
||||||
|
follow_focus = true
|
||||||
|
script = ExtResource("7_dj026")
|
||||||
|
|
||||||
|
[node name="MusicList" type="VBoxContainer" parent="MusicListPanel/ScrollContainer" unique_id=373398568]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_constants/separation = 10
|
||||||
|
|
||||||
|
[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=498283761]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 1216.0
|
||||||
|
offset_top = 128.0
|
||||||
|
offset_right = 1857.0
|
||||||
|
offset_bottom = 512.0
|
||||||
|
horizontal_scroll_mode = 0
|
||||||
|
vertical_scroll_mode = 3
|
||||||
|
script = ExtResource("2_gxtxm")
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer" unique_id=1215162949]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=42468498]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 64.0
|
||||||
|
offset_top = 64.0
|
||||||
|
offset_right = 1152.0
|
||||||
|
offset_bottom = 384.0
|
||||||
|
|
||||||
|
[node name="SoundtrackLabel" type="Label" parent="VBoxContainer" unique_id=173147793]
|
||||||
|
texture_filter = 1
|
||||||
|
custom_minimum_size = Vector2(1, 1)
|
||||||
|
layout_mode = 2
|
||||||
|
label_settings = SubResource("LabelSettings_qspbu")
|
||||||
|
horizontal_alignment = 1
|
||||||
|
autowrap_mode = 2
|
||||||
|
|
||||||
|
[node name="SongLabel" type="Label" parent="VBoxContainer" unique_id=1687490027]
|
||||||
|
texture_filter = 1
|
||||||
|
custom_minimum_size = Vector2(1, 1)
|
||||||
|
layout_mode = 2
|
||||||
|
label_settings = SubResource("LabelSettings_3m52w")
|
||||||
|
horizontal_alignment = 1
|
||||||
|
autowrap_mode = 2
|
||||||
|
|
||||||
|
[node name="CharacterSelect" parent="." unique_id=2079506802 instance=ExtResource("2_76kf4")]
|
||||||
|
visible = false
|
||||||
|
top_level = true
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 616.0
|
||||||
|
offset_top = 264.0
|
||||||
|
offset_right = 776.0
|
||||||
|
offset_bottom = 424.0
|
||||||
|
|
||||||
|
[node name="MusicPlayer" parent="." unique_id=1990459832 instance=ExtResource("5_emn36")]
|
||||||
|
layout_mode = 0
|
||||||
|
anchors_preset = 0
|
||||||
|
anchor_left = 0.0
|
||||||
|
anchor_top = 0.0
|
||||||
|
anchor_right = 0.0
|
||||||
|
anchor_bottom = 0.0
|
||||||
|
offset_left = 65.0
|
||||||
|
offset_top = 992.0
|
||||||
|
offset_right = 1856.0
|
||||||
|
offset_bottom = 1024.0
|
||||||
|
grow_horizontal = 1
|
||||||
|
grow_vertical = 1
|
||||||
|
|
||||||
|
[node name="Players" type="PanelContainer" parent="." unique_id=1708375945]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 1216.0
|
||||||
|
offset_top = 576.0
|
||||||
|
offset_right = 1857.0
|
||||||
|
offset_bottom = 977.0
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="Players" unique_id=270084211]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="Players/VBoxContainer" unique_id=513047023]
|
||||||
|
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="Players/VBoxContainer/HBoxContainer" unique_id=1331357386]
|
||||||
|
texture_filter = 1
|
||||||
|
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 1
|
||||||
|
text = "Players"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="AddPlayersButton" type="TextureButton" parent="Players/VBoxContainer/HBoxContainer" unique_id=634410148]
|
||||||
|
texture_filter = 1
|
||||||
|
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 8
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
shortcut = SubResource("Shortcut_jafqj")
|
||||||
|
texture_normal = ExtResource("5_31tjv")
|
||||||
|
|
||||||
|
[node name="AddPlayerContainer" type="HBoxContainer" parent="Players/VBoxContainer" unique_id=1371948739]
|
||||||
|
visible = false
|
||||||
|
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Panel" type="Panel" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=1569854056]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_qk7sj")
|
||||||
|
|
||||||
|
[node name="PlayerNameField" type="TextEdit" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=332535327]
|
||||||
|
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
placeholder_text = "Player name"
|
||||||
|
script = ExtResource("7_qsdfy")
|
||||||
|
|
||||||
|
[node name="AddPlayerButton" type="Button" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=245001313]
|
||||||
|
texture_filter = 1
|
||||||
|
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Add"
|
||||||
|
|
||||||
|
[node name="Panel2" type="Panel" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=60215448]
|
||||||
|
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_k1ygi")
|
||||||
|
|
||||||
|
[node name="ScrollContainer" type="ScrollContainer" parent="Players/VBoxContainer" unique_id=1000000001]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 15
|
||||||
|
|
||||||
|
[node name="PlayersListVBoxContainer" type="VBoxContainer" parent="Players/VBoxContainer/ScrollContainer" unique_id=1000000002]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 15
|
||||||
|
|
||||||
|
[node name="ResetPlaylistButton" type="Button" parent="." unique_id=1754155620]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 1360.0
|
||||||
|
offset_top = 64.0
|
||||||
|
offset_right = 1471.0
|
||||||
|
offset_bottom = 95.0
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "Reset playlist"
|
||||||
|
|
||||||
|
[node name="ResetPointsButton" type="Button" parent="." unique_id=942880776]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 1488.0
|
||||||
|
offset_top = 64.0
|
||||||
|
offset_right = 1599.0
|
||||||
|
offset_bottom = 95.0
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "Reset points"
|
||||||
|
|
||||||
|
[node name="SyncButton" type="Button" parent="." unique_id=1228313556]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 1616.0
|
||||||
|
offset_top = 64.0
|
||||||
|
offset_right = 1759.0
|
||||||
|
offset_bottom = 95.0
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "Sync soundtracks"
|
||||||
|
|
||||||
|
[node name="SoundTestButton" type="Button" parent="." unique_id=1332971879]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 1776.0
|
||||||
|
offset_top = 64.0
|
||||||
|
offset_right = 1887.0
|
||||||
|
offset_bottom = 95.0
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "Sound test"
|
||||||
|
|
||||||
|
[node name="ShowAnswerButton" type="Button" parent="." unique_id=415388039]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 65.0
|
||||||
|
offset_top = 944.0
|
||||||
|
offset_right = 176.0
|
||||||
|
offset_bottom = 975.0
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
shortcut = SubResource("Shortcut_a7fvb")
|
||||||
|
text = "Show answer"
|
||||||
|
|
||||||
|
[node name="NextButton" type="Button" parent="." unique_id=1744703488]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 193.0
|
||||||
|
offset_top = 944.0
|
||||||
|
offset_right = 368.0
|
||||||
|
offset_bottom = 975.0
|
||||||
|
focus_mode = 0
|
||||||
|
disabled = true
|
||||||
|
action_mode = 0
|
||||||
|
shortcut = SubResource("Shortcut_d6fml")
|
||||||
|
text = "Randomize new track"
|
||||||
|
|
||||||
|
[node name="SearchButton" type="Button" parent="." unique_id=1782453581]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 1354.0
|
||||||
|
offset_top = 8.0
|
||||||
|
offset_right = 1424.0
|
||||||
|
offset_bottom = 48.0
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
shortcut = SubResource("Shortcut_fbju4")
|
||||||
|
text = "Search"
|
||||||
|
|
||||||
|
[node name="SettingsButton" type="Button" parent="." unique_id=1193400323]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 1442.0
|
||||||
|
offset_top = 8.0
|
||||||
|
offset_right = 1512.0
|
||||||
|
offset_bottom = 48.0
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "Settings"
|
||||||
|
|
||||||
|
[node name="StatisticsButton" type="Button" parent="." unique_id=2045021269]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 1530.0
|
||||||
|
offset_top = 8.0
|
||||||
|
offset_right = 1600.0
|
||||||
|
offset_bottom = 48.0
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "Statistics"
|
||||||
|
|
||||||
|
[node name="ShortcutsButton" type="Button" parent="." unique_id=2066498226]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 1626.0
|
||||||
|
offset_top = 8.0
|
||||||
|
offset_right = 1696.0
|
||||||
|
offset_bottom = 48.0
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "Shortcuts"
|
||||||
|
|
||||||
|
[node name="AboutButton" type="Button" parent="." unique_id=1057035253]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 1722.0
|
||||||
|
offset_top = 8.0
|
||||||
|
offset_right = 1792.0
|
||||||
|
offset_bottom = 48.0
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "About"
|
||||||
|
|
||||||
|
[node name="QRButton" type="Button" parent="." unique_id=2021908559]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 1802.0
|
||||||
|
offset_top = 8.0
|
||||||
|
offset_right = 1872.0
|
||||||
|
offset_bottom = 48.0
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "QR"
|
||||||
|
|
||||||
|
[node name="Search" parent="." unique_id=1661250375 instance=ExtResource("9_5ijvr")]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
offset_left = 56.0
|
||||||
|
offset_top = 96.0
|
||||||
|
offset_right = 56.0
|
||||||
|
offset_bottom = 96.0
|
||||||
|
|
||||||
|
[node name="SyncWindow" parent="." unique_id=182499699 instance=ExtResource("10_yxw0b")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
offset_left = 401.0
|
||||||
|
offset_top = 153.0
|
||||||
|
offset_right = 401.0
|
||||||
|
offset_bottom = 153.0
|
||||||
|
|
||||||
|
[node name="SettingsWindow" parent="." unique_id=1323246978 instance=ExtResource("11_k62u5")]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
offset_left = 720.0
|
||||||
|
offset_top = 153.0
|
||||||
|
offset_right = 720.0
|
||||||
|
offset_bottom = 153.0
|
||||||
|
|
||||||
|
[node name="QRWindow" parent="." unique_id=589733821 instance=ExtResource("15_qr_window")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
offset_left = 720.0
|
||||||
|
offset_top = 153.0
|
||||||
|
offset_right = 720.0
|
||||||
|
offset_bottom = 153.0
|
||||||
|
|
||||||
|
[node name="NewCharacterSelect" parent="." unique_id=1679856322 instance=ExtResource("6_dhrvg")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
offset_left = 420.0
|
||||||
|
offset_top = 165.0
|
||||||
|
offset_right = 1572.0
|
||||||
|
offset_bottom = 805.0
|
||||||
|
|
||||||
|
[node name="ShortcutsWindow" parent="." unique_id=1513416242 instance=ExtResource("13_8eebo")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
offset_left = 562.0
|
||||||
|
offset_top = 160.0
|
||||||
|
offset_right = 1262.0
|
||||||
|
offset_bottom = 860.0
|
||||||
|
|
||||||
|
[node name="StatisticsPopupPanel" type="PopupPanel" parent="." unique_id=298311407]
|
||||||
|
oversampling_override = 1.0
|
||||||
|
initial_position = 2
|
||||||
|
size = Vector2i(450, 100)
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="StatisticsPopupPanel" unique_id=548316493]
|
||||||
|
texture_filter = 1
|
||||||
|
offset_left = 4.0
|
||||||
|
offset_top = 4.0
|
||||||
|
offset_right = 446.0
|
||||||
|
offset_bottom = 96.0
|
||||||
|
text = "Statistics"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="StatisticsLabel" type="Label" parent="StatisticsPopupPanel" unique_id=58226477]
|
||||||
|
texture_filter = 1
|
||||||
|
offset_left = 4.0
|
||||||
|
offset_top = 4.0
|
||||||
|
offset_right = 446.0
|
||||||
|
offset_bottom = 96.0
|
||||||
|
text = "Total amount of soundtracks in the playlist: 9000"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="AboutWindow" parent="." unique_id=141261008 instance=ExtResource("16_about_window")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
offset_left = 720.0
|
||||||
|
offset_top = 153.0
|
||||||
|
offset_right = 720.0
|
||||||
|
offset_bottom = 153.0
|
||||||
|
|
||||||
|
[node name="WinnerWindow" parent="." unique_id=999999999 instance=ExtResource("17_winner_window")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
offset_left = 720.0
|
||||||
|
offset_top = 153.0
|
||||||
|
offset_right = 720.0
|
||||||
|
offset_bottom = 153.0
|
||||||
|
|
||||||
|
[node name="RepeatSongCheckButton" type="CheckButton" parent="." unique_id=776835053]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 64.0
|
||||||
|
offset_top = 1032.0
|
||||||
|
offset_right = 208.0
|
||||||
|
offset_bottom = 1063.0
|
||||||
|
focus_mode = 0
|
||||||
|
button_pressed = true
|
||||||
|
action_mode = 0
|
||||||
|
text = "Auto repeat"
|
||||||
|
|
||||||
|
[node name="DebugLabel" type="Label" parent="." unique_id=1959221257]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 974.0
|
||||||
|
offset_top = 904.0
|
||||||
|
offset_right = 1014.0
|
||||||
|
offset_bottom = 927.0
|
||||||
|
scale = Vector2(3, 3)
|
||||||
|
text = "DEBUG"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
vertical_alignment = 1
|
||||||
|
autowrap_mode = 2
|
||||||
|
|
||||||
|
[node name="Log" parent="." unique_id=1289881388 instance=ExtResource("14_26rwn")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
offset_left = 562.0
|
||||||
|
offset_top = 132.0
|
||||||
|
offset_right = 1262.0
|
||||||
|
offset_bottom = 832.0
|
||||||
|
|
||||||
|
[connection signal="dir_selected" from="FileDialog" to="." method="_on_file_dialog_dir_selected"]
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
extends ScrollContainer
|
||||||
|
|
||||||
|
var max_value = 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
|
||||||
File renamed without changes.
+231
@@ -0,0 +1,231 @@
|
|||||||
|
class_name MusicPlayer
|
||||||
|
extends PanelContainer
|
||||||
|
|
||||||
|
# LOC 20/1-24: 136
|
||||||
|
# LOC 12/6-24: 170
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var play_button: TextureButton = $MusicPlayerContainer/PlayTextureButton
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var restart_button: TextureButton = $MusicPlayerContainer/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 = $MusicPlayerContainer/MusicPlayerSlider
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var volume_slider: HSlider = $MusicPlayerContainer/VolumeSlider
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var music_time_label: Label = $MusicPlayerContainer/MusicTimeLabel
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var path: String = '/Users/sebastian/ResilioSync/Sorterat_test/Metal Gear Solid 4 - Guns of the Patriots/2-16 Metal Gear Saga.mp3'
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
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):
|
||||||
|
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 "??:?? / ??:??"
|
||||||
|
elif Settings.hide_length:
|
||||||
|
return format_time(part) + " / ??:??"
|
||||||
|
elif Settings.hide_beginning && part < Settings.hide_for_seconds:
|
||||||
|
return "??:?? / " + format_time(total)
|
||||||
|
else:
|
||||||
|
return format_time(part) + " / " + format_time(total)
|
||||||
|
|
||||||
|
func play_or_pause() -> void:
|
||||||
|
if song_finished:
|
||||||
|
restart()
|
||||||
|
elif audio_player.stream_paused:
|
||||||
|
play_button.texture_normal = pause_icon
|
||||||
|
audio_player.stream_paused = false
|
||||||
|
song_finished = false
|
||||||
|
audio_player.seek(playback_position)
|
||||||
|
print("continue")
|
||||||
|
progress_slider.max_value = round(stream.get_length())
|
||||||
|
if Settings.hide_ticks:
|
||||||
|
progress_slider.tick_count = 0
|
||||||
|
else:
|
||||||
|
progress_slider.tick_count = round(stream.get_length() / 60)
|
||||||
|
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
|
||||||
|
song_finished = false
|
||||||
|
progress_slider.value = 0
|
||||||
|
playback_position = audio_player.get_playback_position()
|
||||||
|
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
|
||||||
|
if !Settings.stop_after_current:
|
||||||
|
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)
|
||||||
|
if value == 0:
|
||||||
|
play_sound(preload("res://sounds/sound1.mp3"))
|
||||||
|
elif value < 5:
|
||||||
|
play_sound(preload("res://sounds/intro_long.mp3"))
|
||||||
|
else:
|
||||||
|
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()
|
||||||
|
sound_player.play()
|
||||||
|
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()
|
||||||
|
sound_player.stop()
|
||||||
|
song_finished = false
|
||||||
|
play_button.texture_normal = pause_icon
|
||||||
|
stream = audio_player.stream
|
||||||
|
progress_slider.value = 0
|
||||||
|
progress_slider.max_value = round(stream.get_length())
|
||||||
|
if Settings.hide_ticks:
|
||||||
|
progress_slider.tick_count = 0
|
||||||
|
else:
|
||||||
|
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:
|
||||||
|
print("play_song_object")
|
||||||
|
if audio_player.is_playing():
|
||||||
|
audio_player.stop()
|
||||||
|
await get_tree().create_timer(0.5).timeout
|
||||||
|
audio_player.stream = Playlist.get_song(song_object_no)
|
||||||
|
sound_player.stop()
|
||||||
|
audio_player.play()
|
||||||
|
song_finished = false
|
||||||
|
play_button.texture_normal = pause_icon
|
||||||
|
stream = audio_player.stream
|
||||||
|
progress_slider.value = 0
|
||||||
|
progress_slider.max_value = round(stream.get_length())
|
||||||
|
if Settings.hide_ticks:
|
||||||
|
progress_slider.tick_count = 0
|
||||||
|
else:
|
||||||
|
progress_slider.tick_count = round(stream.get_length() / 60)
|
||||||
|
music_time_label.text = format_text(progress_slider.value, stream.get_length())
|
||||||
|
Playlist.set_currently_playing_song(song_object_no)
|
||||||
|
Playlist.unset_is_playing()
|
||||||
|
if !Settings.hide_next_track:
|
||||||
|
print("Show answer now!!")
|
||||||
|
Playlist.song_is_answered(song_object_no)
|
||||||
|
Playlist.song_has_played(song_object_no)
|
||||||
|
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()
|
||||||
|
sound.data = body
|
||||||
|
play_song(sound)
|
||||||
|
MusicServerClient.fetch_sound_test_song(test_sound)
|
||||||
|
|
||||||
|
##### 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]
|
||||||
|
print(local_path)
|
||||||
|
print(FileAccess.file_exists(local_path))
|
||||||
|
play_sound(load_mp3(local_path))
|
||||||
|
|
||||||
|
func sound_test_local() -> void:
|
||||||
|
play_sound(preload("res://sounds/01. Opening.mp3"))
|
||||||
File renamed without changes.
@@ -0,0 +1,94 @@
|
|||||||
|
[gd_scene load_steps=10 format=3 uid="uid://ds15cgsf8vpvc"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://d1v0rquma6nt8" path="res://MusicPlayer.gd" id="1_t24ra"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://n2g8jddr85h2" path="res://sounds/01. Opening.mp3" id="2_xti80"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://comxqfiykp54f" path="res://icons/play_icon_light.svg" id="3_6g308"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://ccb6rvbldlgdg" path="res://icons/reload_light_icon.svg" id="4_jleuo"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://w400rnew7453" path="res://volume_slider.tscn" id="5_iifuj"]
|
||||||
|
|
||||||
|
[sub_resource type="InputEventKey" id="InputEventKey_r4qo3"]
|
||||||
|
device = -1
|
||||||
|
alt_pressed = true
|
||||||
|
keycode = 88
|
||||||
|
physical_keycode = 88
|
||||||
|
key_label = 88
|
||||||
|
unicode = 120
|
||||||
|
|
||||||
|
[sub_resource type="Shortcut" id="Shortcut_ipcfh"]
|
||||||
|
events = [SubResource("InputEventKey_r4qo3")]
|
||||||
|
|
||||||
|
[sub_resource type="InputEventKey" id="InputEventKey_2nvce"]
|
||||||
|
device = -1
|
||||||
|
alt_pressed = true
|
||||||
|
keycode = 90
|
||||||
|
unicode = 90
|
||||||
|
|
||||||
|
[sub_resource type="Shortcut" id="Shortcut_i7swq"]
|
||||||
|
events = [SubResource("InputEventKey_2nvce")]
|
||||||
|
|
||||||
|
[node name="MusicPlayer" type="PanelContainer"]
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -121.0
|
||||||
|
offset_top = -15.5
|
||||||
|
offset_right = 121.0
|
||||||
|
offset_bottom = 16.5
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
size_flags_vertical = 4
|
||||||
|
script = ExtResource("1_t24ra")
|
||||||
|
|
||||||
|
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."]
|
||||||
|
stream = ExtResource("2_xti80")
|
||||||
|
bus = &"music"
|
||||||
|
|
||||||
|
[node name="AudioSoundPlayer" type="AudioStreamPlayer" parent="."]
|
||||||
|
stream = ExtResource("2_xti80")
|
||||||
|
bus = &"music"
|
||||||
|
|
||||||
|
[node name="AudioSoundEffectPlayer" type="AudioStreamPlayer" parent="."]
|
||||||
|
stream = ExtResource("2_xti80")
|
||||||
|
bus = &"music"
|
||||||
|
|
||||||
|
[node name="MusicPlayerContainer" type="HBoxContainer" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 4
|
||||||
|
|
||||||
|
[node name="PlayTextureButton" type="TextureButton" parent="MusicPlayerContainer"]
|
||||||
|
custom_minimum_size = Vector2(32, 32)
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
shortcut = SubResource("Shortcut_ipcfh")
|
||||||
|
texture_normal = ExtResource("3_6g308")
|
||||||
|
ignore_texture_size = true
|
||||||
|
stretch_mode = 0
|
||||||
|
|
||||||
|
[node name="RestartTextureButton" type="TextureButton" parent="MusicPlayerContainer"]
|
||||||
|
custom_minimum_size = Vector2(32, 32)
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
shortcut = SubResource("Shortcut_i7swq")
|
||||||
|
texture_normal = ExtResource("4_jleuo")
|
||||||
|
ignore_texture_size = true
|
||||||
|
stretch_mode = 0
|
||||||
|
flip_h = true
|
||||||
|
|
||||||
|
[node name="MusicPlayerSlider" type="HSlider" parent="MusicPlayerContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 4
|
||||||
|
scrollable = false
|
||||||
|
|
||||||
|
[node name="MusicTimeLabel" type="Label" parent="MusicPlayerContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "0:00 / 0:00"
|
||||||
|
|
||||||
|
[node name="VolumeSlider" parent="MusicPlayerContainer" instance=ExtResource("5_iifuj")]
|
||||||
|
custom_minimum_size = Vector2(100, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 4
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
class_name Player
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var player_name_field := $HBoxContainer/Name
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var add_button := $HBoxContainer/AddPoint
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var minus_button := $HBoxContainer/RemovePoint
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var character := $HBoxContainer/Character
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var remove_player := $HBoxContainer/RemovePlayer
|
||||||
|
|
||||||
|
signal change_character_clicked
|
||||||
|
signal player_removed
|
||||||
|
|
||||||
|
## The player shown in this row.
|
||||||
|
var player_object: PlayerObject
|
||||||
|
|
||||||
|
## Position in Settings.player_array. Only used for the keybinds: Alt+<list_index + 1>
|
||||||
|
## gives this player a point and Ctrl+Alt+<list_index + 1> takes one away.
|
||||||
|
var list_index: int
|
||||||
|
|
||||||
|
# Called when the node enters the scene tree for the first time.
|
||||||
|
func _ready() -> void:
|
||||||
|
add_button.pressed.connect(add_point)
|
||||||
|
add_button.set_shortcut(create_button_shortcut(list_index, false))
|
||||||
|
minus_button.pressed.connect(minus_point)
|
||||||
|
minus_button.set_shortcut(create_button_shortcut(list_index, true))
|
||||||
|
character.pressed.connect(change_character)
|
||||||
|
remove_player.pressed.connect(func() -> void: player_removed.emit())
|
||||||
|
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()
|
||||||
|
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 character_selected(file_name: String) -> void:
|
||||||
|
print("Back in player list with: " + file_name)
|
||||||
|
player_object.character = load("res://characters/" + file_name)
|
||||||
|
set_player_character()
|
||||||
|
|
||||||
|
func new_character_selected(character_texture: ImageTexture) -> void:
|
||||||
|
print("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)
|
||||||
|
else:
|
||||||
|
player_name_field.text = player_object.player_name + ": " + str(player_object.player_score)
|
||||||
File renamed without changes.
+98
@@ -0,0 +1,98 @@
|
|||||||
|
[gd_scene load_steps=9 format=3 uid="uid://cslogy2csnd7a"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://dl6no2wr2c86y" path="res://Player.gd" id="1_if4kc"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://r4as0nmtoa7p" path="res://noCharacter.png" id="2_hpj3s"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://b2kj6m8qpsgb1" path="res://icons/person_remove_light.svg" id="3_j3uxe"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_821k2"]
|
||||||
|
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_w10e8"]
|
||||||
|
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_78a8g"]
|
||||||
|
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i56hb"]
|
||||||
|
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_b2crt"]
|
||||||
|
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
|
||||||
|
|
||||||
|
[node name="Player" type="Control"]
|
||||||
|
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 0
|
||||||
|
offset_bottom = 40.0
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
script = ExtResource("1_if4kc")
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="."]
|
||||||
|
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||||
|
layout_mode = 0
|
||||||
|
offset_right = 40.0
|
||||||
|
offset_bottom = 40.0
|
||||||
|
|
||||||
|
[node name="Panel4" type="Panel" parent="HBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(5, 40)
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_821k2")
|
||||||
|
|
||||||
|
[node name="Name" type="Label" parent="HBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(130, 40)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 1
|
||||||
|
text = ": 0"
|
||||||
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="Panel3" type="Panel" parent="HBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(80, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_w10e8")
|
||||||
|
|
||||||
|
[node name="Character" type="TextureButton" parent="HBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(80, 40)
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
texture_normal = ExtResource("2_hpj3s")
|
||||||
|
ignore_texture_size = true
|
||||||
|
stretch_mode = 5
|
||||||
|
|
||||||
|
[node name="Panel2" type="Panel" parent="HBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(30, 40)
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_78a8g")
|
||||||
|
|
||||||
|
[node name="AddPoint" type="Button" parent="HBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(40, 40)
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "+1"
|
||||||
|
|
||||||
|
[node name="Panel5" type="Panel" parent="HBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(3, 40)
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_i56hb")
|
||||||
|
|
||||||
|
[node name="RemovePoint" type="Button" parent="HBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(40, 40)
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "-1"
|
||||||
|
|
||||||
|
[node name="Panel" type="Panel" parent="HBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(30, 40)
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_b2crt")
|
||||||
|
|
||||||
|
[node name="RemovePlayer" type="TextureButton" parent="HBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(40, 40)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
texture_normal = ExtResource("3_j3uxe")
|
||||||
|
stretch_mode = 0
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
extends TextEdit
|
||||||
|
|
||||||
|
signal enter_key_pressed
|
||||||
|
signal close_pressed
|
||||||
|
|
||||||
|
@export
|
||||||
|
var LIMIT: int = 10
|
||||||
|
var current_text = ''
|
||||||
|
var cursor_line = 0
|
||||||
|
var cursor_column = 0
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
text_changed.connect(_on_text_changed)
|
||||||
|
|
||||||
|
func _input(event):
|
||||||
|
if event is InputEventKey and event.pressed:
|
||||||
|
if event.alt_pressed && event.keycode == KEY_A:
|
||||||
|
close_pressed.emit()
|
||||||
|
if has_focus():
|
||||||
|
if event.keycode == KEY_ENTER || event.keycode == KEY_KP_ENTER:
|
||||||
|
enter_key_pressed.emit()
|
||||||
|
|
||||||
|
func _on_text_changed():
|
||||||
|
if text.contains("\n"):
|
||||||
|
text = text.replace("\n", "")
|
||||||
|
|
||||||
|
var new_text : String = text
|
||||||
|
if new_text.length() > LIMIT:
|
||||||
|
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
|
||||||
|
set_caret_line(cursor_line)
|
||||||
|
set_caret_column(cursor_column)
|
||||||
|
|
||||||
|
current_text = text
|
||||||
|
# save current position of cursor for when we have reached the limit
|
||||||
|
cursor_line = get_caret_line()
|
||||||
|
cursor_column = get_caret_column()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cgakje8gb37tf
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
extends RefCounted
|
||||||
|
class_name PlayerObject
|
||||||
|
|
||||||
|
signal first_point_triggerd
|
||||||
|
signal match_point_triggerd
|
||||||
|
signal winner_triggerd
|
||||||
|
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
|
||||||
|
|
||||||
|
var character: Texture = load("res://noCharacter.png")
|
||||||
|
|
||||||
|
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)]
|
||||||
|
|
||||||
|
func add_point() -> void:
|
||||||
|
if player_score < Settings.winning_score:
|
||||||
|
player_score += 1
|
||||||
|
player_point_given.emit()
|
||||||
|
if player_score == 1 && is_first_point:
|
||||||
|
is_first_point = false
|
||||||
|
first_point_triggerd.emit()
|
||||||
|
elif player_score == Settings.winning_score - 1:
|
||||||
|
match_point_triggerd.emit()
|
||||||
|
elif player_score == Settings.winning_score:
|
||||||
|
winner_triggerd.emit()
|
||||||
|
else:
|
||||||
|
point_given_sound.emit()
|
||||||
|
|
||||||
|
func minus_point() -> void:
|
||||||
|
if player_score > 0:
|
||||||
|
player_score -= 1
|
||||||
|
player_point_taken.emit()
|
||||||
|
if player_score == 0:
|
||||||
|
is_first_point = true
|
||||||
|
|
||||||
|
func reset_points() -> void:
|
||||||
|
player_score = 0
|
||||||
|
is_first_point = true
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var qr_panel: Panel = $QRPanel
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var qr_code_rect: QRCodeRect = $QRPanel/QRCodeRect
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var url_label: Label = $QRPanel/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
|
||||||
|
print("QR code generated for: ", url)
|
||||||
|
|
||||||
|
func show_window() -> void:
|
||||||
|
visible = true
|
||||||
|
# Regenerate QR code in case settings changed
|
||||||
|
var url: String = Settings.default_path + "/search"
|
||||||
|
qr_code_rect.data = url
|
||||||
|
url_label.text = qr_code_rect.data
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if self.visible == true:
|
||||||
|
if event is InputEventMouseButton && event.is_pressed():
|
||||||
|
var evLocal: InputEvent = make_input_local(event)
|
||||||
|
if !Rect2(Vector2(0, 0), qr_panel.size).has_point(evLocal.position):
|
||||||
|
self.visible = false
|
||||||
|
|
||||||
|
|
||||||
|
func hide_window() -> void:
|
||||||
|
visible = false
|
||||||
File renamed without changes.
@@ -1,9 +1,9 @@
|
|||||||
[gd_scene format=3 uid="uid://btupbowehiyyu"]
|
[gd_scene format=3 uid="uid://btupbowehiyyu"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://bsa6xg5xos6h7" path="res://popups/qr_popup.gd" id="1_qr_script"]
|
[ext_resource type="Script" uid="uid://bsa6xg5xos6h7" path="res://QRWindow.gd" id="1_qr_script"]
|
||||||
[ext_resource type="Script" uid="uid://dynulhnyq5hpq" path="res://addons/kenyoni/qr_code/qr_code_rect.gd" id="2_jpsxt"]
|
[ext_resource type="Script" uid="uid://dynulhnyq5hpq" path="res://addons/kenyoni/qr_code/qr_code_rect.gd" id="2_jpsxt"]
|
||||||
|
|
||||||
[node name="QRPopup" type="Control" unique_id=1996957585 node_paths=PackedStringArray("panel")]
|
[node name="QRControl" type="Control" unique_id=1996957585]
|
||||||
layout_mode = 3
|
layout_mode = 3
|
||||||
anchors_preset = 15
|
anchors_preset = 15
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
@@ -11,32 +11,19 @@ anchor_bottom = 1.0
|
|||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
script = ExtResource("1_qr_script")
|
script = ExtResource("1_qr_script")
|
||||||
panel = NodePath("QRPanel")
|
|
||||||
|
|
||||||
[node name="QRPanel" type="Panel" parent="." unique_id=1021365843]
|
[node name="QRPanel" type="Panel" parent="." unique_id=1021365843]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
offset_right = 450.0
|
offset_right = 450.0
|
||||||
offset_bottom = 540.0
|
offset_bottom = 520.0
|
||||||
theme_type_variation = &"PopupBackground"
|
|
||||||
|
|
||||||
[node name="TitleLabel" type="Label" parent="QRPanel" unique_id=1021365844]
|
|
||||||
layout_mode = 0
|
|
||||||
offset_top = 8.0
|
|
||||||
offset_right = 450.0
|
|
||||||
offset_bottom = 56.0
|
|
||||||
theme_type_variation = &"Title"
|
|
||||||
text = "Scan to search on your phone"
|
|
||||||
horizontal_alignment = 1
|
|
||||||
vertical_alignment = 1
|
|
||||||
|
|
||||||
[node name="QRCodeRect" type="TextureRect" parent="QRPanel" unique_id=1054378438]
|
[node name="QRCodeRect" type="TextureRect" parent="QRPanel" unique_id=1054378438]
|
||||||
unique_name_in_owner = true
|
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
offset_left = 20.0
|
offset_left = 20.0
|
||||||
offset_top = 60.0
|
offset_top = 20.0
|
||||||
offset_right = 430.0
|
offset_right = 430.0
|
||||||
offset_bottom = 470.0
|
offset_bottom = 430.0
|
||||||
script = ExtResource("2_jpsxt")
|
script = ExtResource("2_jpsxt")
|
||||||
auto_update = true
|
auto_update = true
|
||||||
mode = 4
|
mode = 4
|
||||||
@@ -57,9 +44,9 @@ metadata/_custom_type_script = "uid://dynulhnyq5hpq"
|
|||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
offset_left = 20.0
|
offset_left = 20.0
|
||||||
offset_top = 482.0
|
offset_top = 450.0
|
||||||
offset_right = 430.0
|
offset_right = 430.0
|
||||||
offset_bottom = 522.0
|
offset_bottom = 490.0
|
||||||
text = "URL"
|
text = "URL"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
vertical_alignment = 1
|
vertical_alignment = 1
|
||||||
+2
-23
@@ -5,37 +5,16 @@ Releases are made from the version in `project.godot`
|
|||||||
same version in the About window.
|
same version in the About window.
|
||||||
|
|
||||||
1. Bump the version, e.g. from `1.9.0` to `1.10.0`, and add a `WHATS_NEW` entry in
|
1. Bump the version, e.g. from `1.9.0` to `1.10.0`, and add a `WHATS_NEW` entry in
|
||||||
`popups/about_popup.gd`.
|
`AboutWindow.gd`.
|
||||||
2. Merge to `main` and push.
|
2. Merge to `main` and push.
|
||||||
|
|
||||||
The `Build` workflow (`.gitea/workflows/gitea-build.yaml`) then:
|
The `Build` workflow (`.gitea/workflows/gitea-build.yaml`) then:
|
||||||
|
|
||||||
1. runs the tests,
|
1. runs the tests,
|
||||||
2. exports the Windows and Linux builds, and packages Linux as
|
2. exports the Windows and Linux builds,
|
||||||
`MusicPlayer_<version>_linux.tar.gz` with the icon, an app menu entry and `install.sh`
|
|
||||||
(Linux programs can't carry their own icon the way a Windows .exe does),
|
|
||||||
3. if no tag exists for the version yet: uploads the builds to the package registry
|
3. if no tag exists for the version yet: uploads the builds to the package registry
|
||||||
under `MusicPlayer/<version>/`, and creates the `<version>` tag and Gitea release
|
under `MusicPlayer/<version>/`, and creates the `<version>` tag and Gitea release
|
||||||
with the builds attached.
|
with the builds attached.
|
||||||
|
|
||||||
Pushes that don't change the version, and pushes to `develop`, only test and export.
|
Pushes that don't change the version, and pushes to `develop`, only test and export.
|
||||||
Don't create tags or `1.x.y` branches by hand; the workflow makes the tag.
|
Don't create tags or `1.x.y` branches by hand; the workflow makes the tag.
|
||||||
|
|
||||||
## Installing on Linux
|
|
||||||
|
|
||||||
Unpack `MusicPlayer_<version>_linux.tar.gz` and run `./install.sh` inside the
|
|
||||||
`MusicPlayer` folder. It installs the game to `~/.local/bin` and adds it, with its icon,
|
|
||||||
to the app menu. The bare `MusicPlayer_<version>.x86_64` is still published for anyone
|
|
||||||
who just wants to run the file.
|
|
||||||
|
|
||||||
## Updating
|
|
||||||
|
|
||||||
When the server has a newer release, the top bar says "Version X is available". Clicking
|
|
||||||
it offers **Update and restart**: the game downloads its new file from the release
|
|
||||||
(`MusicPlayer_<version>.exe` on Windows, `MusicPlayer_<version>.x86_64` on Linux, through
|
|
||||||
the server's `/download/<platform>` routes), puts it in place of the running file and
|
|
||||||
starts it, and the quiz continues where it was. On Windows the old file is kept as
|
|
||||||
`<game>.old` until the next start. So keep the release file names as they are: the server
|
|
||||||
finds them by their ending. Where the game can't replace itself (run from the editor, a
|
|
||||||
folder it can't write to, or another platform), it opens the download in the browser
|
|
||||||
instead.
|
|
||||||
+192
@@ -0,0 +1,192 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var search_list: VBoxContainer = $ScrollContainer/VBoxContainer
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var search_panel: Panel = %SearchPanel
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var clear_button: Button = $ClearButton
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var search_bar: TextEdit = $Searchbar
|
||||||
|
|
||||||
|
var soundtracks: Array = []
|
||||||
|
|
||||||
|
# Precomputed soundtrack data: array of dicts with original, lowercase, and cleaned fields
|
||||||
|
var precomputed_soundtracks: Array = []
|
||||||
|
|
||||||
|
# Regex cache: maps cleaned search terms to compiled RegEx objects
|
||||||
|
var regex_cache: Dictionary = {}
|
||||||
|
|
||||||
|
var label_pool: Array[Label] = []
|
||||||
|
|
||||||
|
# Called when the node enters the scene tree for the first time.
|
||||||
|
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:
|
||||||
|
search_bar.grab_focus()
|
||||||
|
clear()
|
||||||
|
|
||||||
|
func close() -> void:
|
||||||
|
clear()
|
||||||
|
self.visible = false
|
||||||
|
|
||||||
|
func search() -> void:
|
||||||
|
print(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():
|
||||||
|
if child.visible:
|
||||||
|
child.visible = false
|
||||||
|
label_pool.append(child)
|
||||||
|
|
||||||
|
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.
|
||||||
|
func find_matches(search_text: String) -> Array[String]:
|
||||||
|
var seen: Dictionary = {}
|
||||||
|
var search_text_lower: String = search_text.to_lower()
|
||||||
|
var clean_search_text: String = clean_term(search_text)
|
||||||
|
|
||||||
|
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]
|
||||||
|
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]
|
||||||
|
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]
|
||||||
|
if regex.search(st_data["cleaned"]):
|
||||||
|
if not st_data["original"] in seen:
|
||||||
|
seen[st_data["original"]] = true
|
||||||
|
matches.append(st_data["original"])
|
||||||
|
|
||||||
|
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.
|
||||||
|
func compile_regex_cached(search_term: String) -> RegEx:
|
||||||
|
if not search_term in regex_cache:
|
||||||
|
var regex = RegEx.new()
|
||||||
|
var regText: 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_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
|
||||||
|
func add_soundtrack_reuse(soundtrack: String) -> void:
|
||||||
|
var label: Label
|
||||||
|
if label_pool.size() > 0:
|
||||||
|
label = label_pool.pop_front()
|
||||||
|
label.text = soundtrack
|
||||||
|
label.visible = true
|
||||||
|
# Move to end to maintain display order
|
||||||
|
search_list.move_child(label, search_list.get_child_count() - 1)
|
||||||
|
else:
|
||||||
|
label = Label.new()
|
||||||
|
label.text = soundtrack
|
||||||
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||||
|
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():
|
||||||
|
if child.text == soundtrack and child.visible:
|
||||||
|
soundtrack_exists = true
|
||||||
|
return soundtrack_exists
|
||||||
|
|
||||||
|
func get_list_of_soundtracks() -> void:
|
||||||
|
print("get_list_of_soundtracks")
|
||||||
|
var handle_soundtracks: Callable = func handle_soundtracks(array) -> void:
|
||||||
|
if typeof(array) == TYPE_ARRAY:
|
||||||
|
# Reuse existing labels: hide all and add to pool
|
||||||
|
for child: Label in search_list.get_children():
|
||||||
|
child.visible = false
|
||||||
|
label_pool.append(child)
|
||||||
|
set_soundtracks(array)
|
||||||
|
for soundtrack: String in soundtracks:
|
||||||
|
add_soundtrack_reuse(soundtrack)
|
||||||
|
else:
|
||||||
|
print("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 = []
|
||||||
|
precomputed_soundtracks = []
|
||||||
|
soundtracks.append_array(list)
|
||||||
|
for soundtrack: String in soundtracks:
|
||||||
|
precomputed_soundtracks.append({
|
||||||
|
"original": soundtrack,
|
||||||
|
"lowercase": soundtrack.to_lower(),
|
||||||
|
"cleaned": clean_term(soundtrack)
|
||||||
|
})
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if self.visible == true:
|
||||||
|
if event is InputEventMouseButton && event.is_pressed():
|
||||||
|
var evLocal: InputEvent = make_input_local(event)
|
||||||
|
if !Rect2(Vector2(0, 0), search_panel.size).has_point(evLocal.position):
|
||||||
|
self.visible = false
|
||||||
|
|
||||||
|
func clear() -> void:
|
||||||
|
search_bar.text = ""
|
||||||
|
show_all_soundtracks()
|
||||||
|
search_bar.grab_focus()
|
||||||
File renamed without changes.
@@ -0,0 +1,51 @@
|
|||||||
|
[gd_scene format=3 uid="uid://bxydgil1yifps"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://bg5yo08638600" path="res://SearchWindow.gd" id="1_m7bij"]
|
||||||
|
|
||||||
|
[node name="Control" type="Control" unique_id=616238617]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_m7bij")
|
||||||
|
|
||||||
|
[node name="SearchPanel" type="Panel" parent="." unique_id=1262945994]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 0
|
||||||
|
offset_right = 1152.0
|
||||||
|
offset_bottom = 640.0
|
||||||
|
|
||||||
|
[node name="ClearButton" type="Button" parent="." unique_id=1549439398]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 936.0
|
||||||
|
offset_top = 16.0
|
||||||
|
offset_right = 1027.0
|
||||||
|
offset_bottom = 56.0
|
||||||
|
scale = Vector2(2, 2)
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "Clear"
|
||||||
|
|
||||||
|
[node name="Searchbar" type="TextEdit" parent="." unique_id=2112669372]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 32.0
|
||||||
|
offset_top = 16.0
|
||||||
|
offset_right = 468.0
|
||||||
|
offset_bottom = 56.0
|
||||||
|
scale = Vector2(2, 2)
|
||||||
|
placeholder_text = "Search"
|
||||||
|
|
||||||
|
[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=213733486]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_top = 128.0
|
||||||
|
offset_right = 576.0
|
||||||
|
offset_bottom = 384.0
|
||||||
|
scale = Vector2(2, 2)
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer" unique_id=1567403144]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
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
|
||||||
|
var add_to_stats: bool = true
|
||||||
|
var use_low_played_mode: bool = false
|
||||||
|
var winning_score: int = 20
|
||||||
|
var fullscreen: bool = false
|
||||||
|
var play_local: bool = false
|
||||||
|
var inspiration_list_speed: int = 1
|
||||||
|
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
|
||||||
|
var character_select_open: bool = false
|
||||||
|
|
||||||
|
## The game version, set in Project Settings > Application > Config > Version.
|
||||||
|
## 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)
|
||||||
|
n.queue_free()
|
||||||
|
|
||||||
|
func delete_player_children(node: Node) -> void:
|
||||||
|
for n: Node in node.get_children():
|
||||||
|
node.remove_child(n)
|
||||||
|
n.queue_free()
|
||||||
File renamed without changes.
@@ -0,0 +1,219 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
signal server_changed
|
||||||
|
|
||||||
|
# Called when the node enters the scene tree for the first time.
|
||||||
|
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)
|
||||||
|
low_played_button.pressed.connect(low_played)
|
||||||
|
hide_beginning_button.pressed.connect(hide_beginning)
|
||||||
|
hide_length_button.pressed.connect(hide_length)
|
||||||
|
hide_ticks_button.pressed.connect(hide_ticks)
|
||||||
|
lower_winning_score_button.pressed.connect(lower_winning_score)
|
||||||
|
increase_winning_score_button.pressed.connect(increase_winning_score)
|
||||||
|
lower_cache_button.pressed.connect(lower_cache)
|
||||||
|
increase_cache_button.pressed.connect(increase_cache)
|
||||||
|
lower_inspiration_speed_button.pressed.connect(lower_inspiration_speed)
|
||||||
|
increase_inspiration_speed_button.pressed.connect(increase_inspiration_speed)
|
||||||
|
lower_hide_for_seconds_button.pressed.connect(lower_hide_for_seconds)
|
||||||
|
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
|
||||||
|
low_played_button.button_pressed = Settings.use_low_played_mode
|
||||||
|
hide_beginning_button.button_pressed = Settings.hide_beginning
|
||||||
|
hide_length_button.button_pressed = Settings.hide_length
|
||||||
|
hide_ticks_button.button_pressed = Settings.hide_ticks
|
||||||
|
score_label.text = str(Settings.winning_score)
|
||||||
|
cache_label.text = str(Playlist.number_of_tracks_to_preload)
|
||||||
|
inspiration_speed_label.text = str(Settings.inspiration_list_speed)
|
||||||
|
hide_for_seconds_label.text = str(Settings.hide_for_seconds)
|
||||||
|
fullscreen_button.button_pressed = Settings.fullscreen
|
||||||
|
update_hide_for_seconds_enabled()
|
||||||
|
select_server_button.select(Settings.selected_server)
|
||||||
|
select_server_button.item_selected.connect(select_server)
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if self.visible == true:
|
||||||
|
if event is InputEventMouseButton && event.is_pressed():
|
||||||
|
var evLocal: InputEvent = make_input_local(event)
|
||||||
|
if !Rect2(Vector2(0, 0), settings_panel.size).has_point(evLocal.position):
|
||||||
|
self.visible = false
|
||||||
|
|
||||||
|
func fullscreen() -> void:
|
||||||
|
Settings.fullscreen = !Settings.fullscreen
|
||||||
|
if Settings.fullscreen == true:
|
||||||
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
|
||||||
|
if Settings.fullscreen == false:
|
||||||
|
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 -= 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:
|
||||||
|
print("select_server")
|
||||||
|
Settings.default_path = select_server_button.get_item_text(new_server)
|
||||||
|
Settings.selected_server = new_server
|
||||||
|
print("Settings.default_path: " + Settings.default_path)
|
||||||
|
print("Settings.selected_server: " + str(Settings.selected_server))
|
||||||
|
print("new_server: " + str(new_server))
|
||||||
|
print("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:
|
||||||
|
print("set_selected_server")
|
||||||
|
select_server_button.select(new_server)
|
||||||
|
Settings.default_path = select_server_button.get_item_text(new_server)
|
||||||
|
Settings.selected_server = new_server
|
||||||
|
print("Settings.default_path: " + Settings.default_path)
|
||||||
|
print("Settings.selected_server: " + str(Settings.selected_server))
|
||||||
|
print("new_server: " + str(new_server))
|
||||||
|
print("select_server_button.get_item_text(new_server): " + select_server_button.get_item_text(new_server))
|
||||||
|
server_changed.emit()
|
||||||
File renamed without changes.
@@ -0,0 +1,241 @@
|
|||||||
|
[gd_scene format=3 uid="uid://dldpeo5y3l5hq"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://dukqyjoduf1af" path="res://SettingsWindow.gd" id="1_bt55j"]
|
||||||
|
|
||||||
|
[sub_resource type="LabelSettings" id="LabelSettings_3xrlm"]
|
||||||
|
font_size = 25
|
||||||
|
|
||||||
|
[node name="SettingsControl" type="Control" unique_id=1298840328]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_bt55j")
|
||||||
|
|
||||||
|
[node name="SettingsPanel" type="Panel" parent="." unique_id=45537159]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 0
|
||||||
|
offset_right = 500.0
|
||||||
|
offset_bottom = 640.0
|
||||||
|
|
||||||
|
[node name="ScrollContainer" type="ScrollContainer" parent="SettingsPanel" unique_id=1325795358]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_right = 500.0
|
||||||
|
offset_bottom = 640.0
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="SettingsPanel/ScrollContainer" unique_id=1796635163]
|
||||||
|
clip_contents = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1111531402]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 0
|
||||||
|
text = "Settings"
|
||||||
|
label_settings = SubResource("LabelSettings_3xrlm")
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="StopAfterCurrentCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=508305575]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
button_pressed = true
|
||||||
|
action_mode = 0
|
||||||
|
text = "Stop after current"
|
||||||
|
|
||||||
|
[node name="HideNextTrackCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1568244599]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
button_pressed = true
|
||||||
|
action_mode = 0
|
||||||
|
text = "Hide next track"
|
||||||
|
|
||||||
|
[node name="AddToDatabaseCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1673750542]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "Turn on played to database"
|
||||||
|
|
||||||
|
[node name="LowPlayedCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1672657048]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "Use low played mode"
|
||||||
|
|
||||||
|
[node name="HideBeginningCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1370617867]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "Hide beginning of songs"
|
||||||
|
|
||||||
|
[node name="HideForSecondsHBoxContainer" type="HBoxContainer" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1368485120]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="HideForSecondsTitle" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/HideForSecondsHBoxContainer" unique_id=1822369383]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Hide Start Seconds: "
|
||||||
|
|
||||||
|
[node name="HideForSecondsLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/HideForSecondsHBoxContainer" unique_id=1580108945]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "5.0"
|
||||||
|
|
||||||
|
[node name="LowerHideForSecondsButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/HideForSecondsHBoxContainer" unique_id=180643442]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(30, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "-1"
|
||||||
|
|
||||||
|
[node name="IncreaseHideForSecondsButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/HideForSecondsHBoxContainer" unique_id=340681770]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(30, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "+1"
|
||||||
|
|
||||||
|
[node name="HideLengthCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=67806372]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "Hide length of songs"
|
||||||
|
|
||||||
|
[node name="HideTicksCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=706322752]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "Hide ticks in playbar"
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=2140610992]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label2" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/HBoxContainer" unique_id=1892907487]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Winning Score: "
|
||||||
|
|
||||||
|
[node name="ScoreLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/HBoxContainer" unique_id=444542662]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "20"
|
||||||
|
|
||||||
|
[node name="LowerButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/HBoxContainer" unique_id=614806355]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(30, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "-1"
|
||||||
|
|
||||||
|
[node name="IncreaseButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/HBoxContainer" unique_id=1837169457]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(30, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "+1"
|
||||||
|
|
||||||
|
[node name="CacheHBoxContainer" type="HBoxContainer" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=658210492]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="CacheTitleLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/CacheHBoxContainer" unique_id=365012193]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Predownload songs: "
|
||||||
|
|
||||||
|
[node name="CacheLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/CacheHBoxContainer" unique_id=1558370172]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "1"
|
||||||
|
|
||||||
|
[node name="LowerCacheButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/CacheHBoxContainer" unique_id=1990385038]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(30, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "-1"
|
||||||
|
|
||||||
|
[node name="IncreaseCacheButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/CacheHBoxContainer" unique_id=1806295533]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(30, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "+1"
|
||||||
|
|
||||||
|
[node name="InspirationHBoxContainer" type="HBoxContainer" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1686756540]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="InspirationListSpeedTitle" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/InspirationHBoxContainer" unique_id=1397861506]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Inspiration List Speed: "
|
||||||
|
|
||||||
|
[node name="InspirationListSpeedLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/InspirationHBoxContainer" unique_id=867387668]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "1"
|
||||||
|
|
||||||
|
[node name="LowerInspirationSpeedButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/InspirationHBoxContainer" unique_id=845209534]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(30, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "-1"
|
||||||
|
|
||||||
|
[node name="IncreaseInspirationSpeedButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/InspirationHBoxContainer" unique_id=1122701153]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(30, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
action_mode = 0
|
||||||
|
text = "+1"
|
||||||
|
|
||||||
|
[node name="SelectServerButton" type="OptionButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=434350694]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
selected = 0
|
||||||
|
item_count = 5
|
||||||
|
popup/item_0/text = "https://music.sanplex.xyz"
|
||||||
|
popup/item_0/id = 1
|
||||||
|
popup/item_1/text = "https://tmusic.sanplex.xyz"
|
||||||
|
popup/item_1/id = 2
|
||||||
|
popup/item_2/text = "https://rmusic.sanplex.xyz"
|
||||||
|
popup/item_2/id = 3
|
||||||
|
popup/item_3/text = "https://old-music.sanplex.xyz"
|
||||||
|
popup/item_3/id = 4
|
||||||
|
popup/item_4/text = "http://localhost:8080"
|
||||||
|
popup/item_4/id = 5
|
||||||
|
|
||||||
|
[node name="FullscreenButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=400304684]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
text = "Fullscreen"
|
||||||
|
|
||||||
|
[node name="QuickSyncButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=49497448]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
button_pressed = true
|
||||||
|
action_mode = 0
|
||||||
|
text = "Quick sync"
|
||||||
|
|
||||||
|
[node name="LocalButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=905844805]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
disabled = true
|
||||||
|
text = "Local"
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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]
|
||||||
|
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:
|
||||||
|
print("add_point")
|
||||||
|
if !players_given_point.has(player_id):
|
||||||
|
players_given_point.append(player_id)
|
||||||
|
|
||||||
|
func remove_point(player_id: String) -> void:
|
||||||
|
print("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]
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
[gd_scene format=3 uid="uid://dpdem7pdxweb5"]
|
[gd_scene format=3 uid="uid://dpdem7pdxweb5"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://c3ejukwth512a" path="res://popups/sync_popup.gd" id="1_8ba47"]
|
[ext_resource type="Script" uid="uid://c3ejukwth512a" path="res://sync_window.gd" id="1_8ba47"]
|
||||||
[ext_resource type="Script" path="res://common/animated_progress_bar.gd" id="progress_bar_script"]
|
|
||||||
|
|
||||||
[node name="SyncPopup" type="Control" unique_id=1302914356 node_paths=PackedStringArray("panel")]
|
[sub_resource type="LabelSettings" id="LabelSettings_anbfi"]
|
||||||
|
font_size = 25
|
||||||
|
|
||||||
|
[node name="Control2" type="Control" unique_id=1302914356]
|
||||||
layout_mode = 3
|
layout_mode = 3
|
||||||
anchors_preset = 15
|
anchors_preset = 15
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
@@ -11,34 +13,28 @@ anchor_bottom = 1.0
|
|||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
script = ExtResource("1_8ba47")
|
script = ExtResource("1_8ba47")
|
||||||
panel = NodePath("SyncPanel")
|
|
||||||
|
|
||||||
[node name="SyncPanel" type="Panel" parent="." unique_id=1714634204]
|
[node name="SyncPanel" type="Panel" parent="." unique_id=1714634204]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
offset_right = 1500.0
|
offset_right = 1152.0
|
||||||
offset_bottom = 640.0
|
offset_bottom = 640.0
|
||||||
theme_type_variation = &"PopupBackground"
|
|
||||||
|
|
||||||
[node name="ScrollContainer" type="ScrollContainer" parent="SyncPanel" unique_id=1289116349]
|
[node name="ScrollContainer" type="ScrollContainer" parent="SyncPanel" unique_id=1289116349]
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
offset_left = 20.0
|
offset_right = 1152.0
|
||||||
offset_top = 8.0
|
offset_bottom = 640.0
|
||||||
offset_right = 1480.0
|
|
||||||
offset_bottom = 632.0
|
|
||||||
horizontal_scroll_mode = 0
|
|
||||||
|
|
||||||
[node name="SyncVBoxContainer" type="VBoxContainer" parent="SyncPanel/ScrollContainer" unique_id=1594735567]
|
[node name="SyncVBoxContainer" type="VBoxContainer" parent="SyncPanel/ScrollContainer" unique_id=1594735567]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 6
|
||||||
theme_override_constants/separation = 12
|
|
||||||
|
|
||||||
[node name="SyncTitleLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=2125198054]
|
[node name="SyncTitleLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=2125198054]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_vertical = 0
|
size_flags_vertical = 0
|
||||||
theme_type_variation = &"Title"
|
|
||||||
text = "Sync"
|
text = "Sync"
|
||||||
|
label_settings = SubResource("LabelSettings_anbfi")
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
||||||
[node name="SyncTime" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=1344333200]
|
[node name="SyncTime" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=1344333200]
|
||||||
@@ -52,8 +48,8 @@ unique_name_in_owner = true
|
|||||||
visible = false
|
visible = false
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_vertical = 0
|
size_flags_vertical = 0
|
||||||
theme_type_variation = &"Title"
|
|
||||||
text = "No changes since last sync"
|
text = "No changes since last sync"
|
||||||
|
label_settings = SubResource("LabelSettings_anbfi")
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
||||||
[node name="ProgressHBoxContainer" type="HBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=196414805]
|
[node name="ProgressHBoxContainer" type="HBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=196414805]
|
||||||
@@ -63,23 +59,27 @@ layout_mode = 2
|
|||||||
|
|
||||||
[node name="SyncProgressBar" type="ProgressBar" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/ProgressHBoxContainer" unique_id=719135231]
|
[node name="SyncProgressBar" type="ProgressBar" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/ProgressHBoxContainer" unique_id=719135231]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(1300, 32)
|
custom_minimum_size = Vector2(1000, 0)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 6
|
size_flags_horizontal = 6
|
||||||
theme_type_variation = &"SyncProgressBar"
|
step = 1.0
|
||||||
script = ExtResource("progress_bar_script")
|
rounded = true
|
||||||
|
|
||||||
[node name="SyncHBoxContainer" type="HBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=1156646398]
|
[node name="SyncHBoxContainer" type="HBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=1156646398]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 6
|
||||||
theme_override_constants/separation = 16
|
theme_override_constants/separation = 30
|
||||||
|
|
||||||
|
[node name="VSeparator6" type="VSeparator" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=1432793432]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
[node name="SoundtracksAddedVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=897546696]
|
[node name="SoundtracksAddedVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=897546696]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 6
|
||||||
|
|
||||||
[node name="SoundtracksAddedLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/SoundtracksAddedVBox" unique_id=535797768]
|
[node name="SoundtracksAddedLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/SoundtracksAddedVBox" unique_id=535797768]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
@@ -93,7 +93,7 @@ layout_mode = 2
|
|||||||
[node name="SoundtracksReAddedVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=995736806]
|
[node name="SoundtracksReAddedVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=995736806]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 6
|
||||||
|
|
||||||
[node name="SoundtracksReAddedLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/SoundtracksReAddedVBox" unique_id=1142636525]
|
[node name="SoundtracksReAddedLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/SoundtracksReAddedVBox" unique_id=1142636525]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
@@ -106,7 +106,7 @@ layout_mode = 2
|
|||||||
[node name="SoundtracksChangedTitleVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=294212033]
|
[node name="SoundtracksChangedTitleVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=294212033]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 6
|
||||||
|
|
||||||
[node name="SoundtracksChangedTitleLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/SoundtracksChangedTitleVBox" unique_id=696960827]
|
[node name="SoundtracksChangedTitleLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/SoundtracksChangedTitleVBox" unique_id=696960827]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
@@ -119,7 +119,7 @@ layout_mode = 2
|
|||||||
[node name="SoundtracksChangedContentVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=1470922281]
|
[node name="SoundtracksChangedContentVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=1470922281]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 6
|
||||||
|
|
||||||
[node name="SoundtracksChangedContentLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/SoundtracksChangedContentVBox" unique_id=1209639099]
|
[node name="SoundtracksChangedContentLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/SoundtracksChangedContentVBox" unique_id=1209639099]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
@@ -132,7 +132,7 @@ layout_mode = 2
|
|||||||
[node name="SoundtracksRemovedVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=1174594338]
|
[node name="SoundtracksRemovedVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=1174594338]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 6
|
||||||
|
|
||||||
[node name="SoundtracksRemovedLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/SoundtracksRemovedVBox" unique_id=877713862]
|
[node name="SoundtracksRemovedLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/SoundtracksRemovedVBox" unique_id=877713862]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
@@ -142,13 +142,12 @@ text = "SoundtracksRemoved"
|
|||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
||||||
[node name="CaughtErrorsVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=63733711]
|
[node name="CatchedErrorsVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=63733711]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
|
||||||
|
|
||||||
[node name="CaughtErrorsLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/CaughtErrorsVBox" unique_id=1784389644]
|
[node name="CatchedErrorsLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/CatchedErrorsVBox" unique_id=1784389644]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "CatchedErrors"
|
text = "CatchedErrors"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
+3
-23
@@ -26,8 +26,8 @@ pass.
|
|||||||
|
|
||||||
| Folder | Contents |
|
| Folder | Contents |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `tests/unit/` | Logic without scenes: playlist, songs, player scoring, time formatting, search, the server client and the API classes |
|
| `tests/unit/` | Logic without scenes: playlist, songs, player scoring, time formatting, search, the server client |
|
||||||
| `tests/integration/` | Real scenes (`MainWindow.tscn`, `SyncPopup.tscn`) run against a fake server, including the point keybinds and the Statistics tab |
|
| `tests/integration/` | Real scenes (`MainWindow.tscn`, `SyncWindow.tscn`) run against a fake server, including the point keybinds |
|
||||||
| `tests/fakes/` | `FakeHttpTransport`, `AutoloadSnapshot` and the `MainWindowTestSuite` base class |
|
| `tests/fakes/` | `FakeHttpTransport`, `AutoloadSnapshot` and the `MainWindowTestSuite` base class |
|
||||||
| `tests/data/` | Files the fake server sends, e.g. a short MP3 |
|
| `tests/data/` | Files the fake server sends, e.g. a short MP3 |
|
||||||
|
|
||||||
@@ -48,27 +48,7 @@ assert_int(server.count("/music/rand")).is_equal(1)
|
|||||||
server.uninstall()
|
server.uninstall()
|
||||||
```
|
```
|
||||||
|
|
||||||
Paths are relative to the API (`/music/list` is `<server>/api/v1/music/list`). Paths
|
Paths without a response get a 404, so the code under test sees a failed request.
|
||||||
without a response get a 404, so the code under test sees a failed request.
|
|
||||||
|
|
||||||
Like the real server, the fake hands out tokens on `POST /token` and answers 401 to
|
|
||||||
requests without a valid one, so the client's token handling runs in every test. Token
|
|
||||||
requests are counted in `server.token_requests`, not in `requested_paths`. Use
|
|
||||||
`server.expire_tokens()` to make the client renew its token, `server.hold("/token")` to
|
|
||||||
keep it waiting for one, and `server.respond_json("/token", {...}, 401)` for a refused
|
|
||||||
password.
|
|
||||||
|
|
||||||
## API classes
|
|
||||||
|
|
||||||
The classes in `server/generated/` (`SongInfo`, `StatisticsSummary`, ...) are generated
|
|
||||||
from the server's OpenAPI spec, committed as `server/openapi.json`. To update them, copy
|
|
||||||
a new spec there (`just openapi` in MusicServer, or `<server>/openapi.json`) and run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
godot --headless --path . -s tools/generate_api_types.gd
|
|
||||||
```
|
|
||||||
|
|
||||||
`tests/unit/test_api_types.gd` parses a sample response with each class.
|
|
||||||
|
|
||||||
## Writing tests
|
## Writing tests
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
[gd_scene format=3 uid="uid://cxlsmxnrp0agh"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://dxpb22xbwf3t1" path="res://winner_window.gd" id="1_winner_script"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://r4as0nmtoa7p" path="res://noCharacter.png" id="2_no_character"]
|
||||||
|
|
||||||
|
[sub_resource type="LabelSettings" id="LabelSettings_hr75l"]
|
||||||
|
font_size = 35
|
||||||
|
|
||||||
|
[node name="WinnerControl" type="Control" unique_id=1111111111]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_winner_script")
|
||||||
|
|
||||||
|
[node name="WinnerPanel" type="Panel" parent="." unique_id=89212869]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 0
|
||||||
|
offset_right = 500.0
|
||||||
|
offset_bottom = 600.0
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="WinnerPanel" unique_id=471159349]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_top = 25.0
|
||||||
|
offset_right = 500.0
|
||||||
|
offset_bottom = 282.0
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="WinnerPicture" type="TextureRect" parent="WinnerPanel/VBoxContainer" unique_id=1543774067]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(480, 200)
|
||||||
|
layout_mode = 2
|
||||||
|
texture = ExtResource("2_no_character")
|
||||||
|
expand_mode = 3
|
||||||
|
stretch_mode = 5
|
||||||
|
|
||||||
|
[node name="WinnerLabel" type="Label" parent="WinnerPanel/VBoxContainer" unique_id=1437597286]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Winner!"
|
||||||
|
label_settings = SubResource("LabelSettings_hr75l")
|
||||||
|
horizontal_alignment = 1
|
||||||
|
vertical_alignment = 2
|
||||||
|
|
||||||
|
[node name="PlayersList" type="VBoxContainer" parent="WinnerPanel/VBoxContainer" unique_id=471159350]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
class_name Character
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var panel: Panel = $Panel
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var character_image: TextureRect = %CharacterTextureRect
|
||||||
|
|
||||||
|
@onready
|
||||||
|
var character_name_label: Label = %CharacterName
|
||||||
|
|
||||||
|
signal new_character_selected(img_texture: ImageTexture)
|
||||||
|
|
||||||
|
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:
|
||||||
|
var img: Image = Image.new()
|
||||||
|
var err: Error = img.load_png_from_buffer(image)
|
||||||
|
if err != OK:
|
||||||
|
print(err)
|
||||||
|
var err2: Error = img.load_jpg_from_buffer(image)
|
||||||
|
if err2 != OK:
|
||||||
|
print(err2)
|
||||||
|
var new_image: ImageTexture = ImageTexture.new()
|
||||||
|
new_image.set_image(img)
|
||||||
|
character_image.custom_minimum_size = Vector2(200, 100)
|
||||||
|
character_image.size = Vector2(200, 100)
|
||||||
|
character_image.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
|
character_image.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
|
character_image.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||||||
|
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):
|
||||||
|
print(character_name_label.text)
|
||||||
|
new_character_selected.emit(character_image.texture)
|
||||||
File renamed without changes.
@@ -0,0 +1,45 @@
|
|||||||
|
[gd_scene load_steps=4 format=3 uid="uid://c7ecjj2af6usf"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://i35b3scbpwpu" path="res://character.gd" id="1_pjt0s"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://cv3hdd6bsruau" path="res://icon.svg" id="2_pjt0s"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pjt0s"]
|
||||||
|
bg_color = Color(0.302, 0.302, 0.302, 1)
|
||||||
|
|
||||||
|
[node name="Control" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 0
|
||||||
|
script = ExtResource("1_pjt0s")
|
||||||
|
|
||||||
|
[node name="Panel" type="Panel" parent="."]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_right = 240.0
|
||||||
|
offset_bottom = 145.0
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_pjt0s")
|
||||||
|
|
||||||
|
[node name="MarginContainer" type="MarginContainer" parent="Panel"]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_right = 240.0
|
||||||
|
offset_bottom = 145.0
|
||||||
|
theme_override_constants/margin_left = 15
|
||||||
|
theme_override_constants/margin_top = 10
|
||||||
|
theme_override_constants/margin_right = 15
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="Panel/MarginContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="CharacterTextureRect" type="TextureRect" parent="Panel/MarginContainer/VBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(200, 100)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
size_flags_vertical = 4
|
||||||
|
texture = ExtResource("2_pjt0s")
|
||||||
|
expand_mode = 3
|
||||||
|
stretch_mode = 5
|
||||||
|
|
||||||
|
[node name="CharacterName" type="Label" parent="Panel/MarginContainer/VBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Aloy"
|
||||||
|
horizontal_alignment = 1
|
||||||
-145
@@ -1,145 +0,0 @@
|
|||||||
class_name Anim
|
|
||||||
extends RefCounted
|
|
||||||
## Short animations made with tweens, used all over the game: popping, fading, flashing
|
|
||||||
## and pulsing.
|
|
||||||
##
|
|
||||||
## Every animation is short so a quiz never waits for one. With
|
|
||||||
## [member Settings.reduce_animations] on, nothing moves: each helper jumps straight to
|
|
||||||
## where the animation would end.
|
|
||||||
|
|
||||||
## How long quick feedback, such as a button press, takes.
|
|
||||||
const FAST: float = 0.12
|
|
||||||
## How long most animations take.
|
|
||||||
const NORMAL: float = 0.2
|
|
||||||
## How long flashes and bigger moments take.
|
|
||||||
const SLOW: float = 0.45
|
|
||||||
|
|
||||||
|
|
||||||
## Whether animations are on.
|
|
||||||
static func enabled() -> bool:
|
|
||||||
return !Settings.reduce_animations
|
|
||||||
|
|
||||||
|
|
||||||
## A new tween on [param node] for the animation called [param key], stopping an earlier
|
|
||||||
## one with the same key so they don't fight. Returns null when animations are off or the
|
|
||||||
## node is not in the tree.
|
|
||||||
static func tween(node: Node, key: StringName = &"anim") -> Tween:
|
|
||||||
stop(node, key)
|
|
||||||
if !enabled() || !node.is_inside_tree():
|
|
||||||
return null
|
|
||||||
var new_tween: Tween = node.create_tween()
|
|
||||||
node.set_meta(StringName("_tween_" + key), new_tween)
|
|
||||||
return new_tween
|
|
||||||
|
|
||||||
|
|
||||||
## Stops the animation called [param key] on [param node], if one is running.
|
|
||||||
static func stop(node: Node, key: StringName = &"anim") -> void:
|
|
||||||
var meta: StringName = StringName("_tween_" + key)
|
|
||||||
var old: Variant = node.get_meta(meta) if node.has_meta(meta) else null
|
|
||||||
if old is Tween && (old as Tween).is_valid():
|
|
||||||
(old as Tween).kill()
|
|
||||||
|
|
||||||
|
|
||||||
## Makes [param control] grow from its middle.
|
|
||||||
static func center_pivot(control: Control) -> void:
|
|
||||||
control.pivot_offset = control.size / 2
|
|
||||||
|
|
||||||
|
|
||||||
## Makes [param control] jump to [param peak] times its size and spring back.
|
|
||||||
static func pop(control: Control, peak: float = 1.3, duration: float = NORMAL) -> void:
|
|
||||||
var pop_tween: Tween = tween(control, &"pop")
|
|
||||||
control.scale = Vector2.ONE
|
|
||||||
if pop_tween == null:
|
|
||||||
return
|
|
||||||
center_pivot(control)
|
|
||||||
control.scale = Vector2.ONE * peak
|
|
||||||
pop_tween.tween_property(control, "scale", Vector2.ONE, duration) \
|
|
||||||
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
|
||||||
|
|
||||||
|
|
||||||
## Makes [param control] grow from [param from] times its size with a bounce.
|
|
||||||
static func bounce_in(control: Control, from: float = 0.4, duration: float = SLOW) -> void:
|
|
||||||
var bounce_tween: Tween = tween(control, &"pop")
|
|
||||||
control.scale = Vector2.ONE
|
|
||||||
if bounce_tween == null:
|
|
||||||
return
|
|
||||||
center_pivot(control)
|
|
||||||
control.scale = Vector2.ONE * from
|
|
||||||
bounce_tween.tween_property(control, "scale", Vector2.ONE, duration * 1.6) \
|
|
||||||
.set_trans(Tween.TRANS_ELASTIC).set_ease(Tween.EASE_OUT)
|
|
||||||
|
|
||||||
|
|
||||||
## Fades [param item] in from invisible.
|
|
||||||
static func fade_in(item: CanvasItem, duration: float = NORMAL) -> void:
|
|
||||||
var fade_tween: Tween = tween(item, &"fade")
|
|
||||||
item.modulate.a = 1.0
|
|
||||||
if fade_tween == null:
|
|
||||||
return
|
|
||||||
item.modulate.a = 0.0
|
|
||||||
fade_tween.tween_property(item, "modulate:a", 1.0, duration)
|
|
||||||
|
|
||||||
|
|
||||||
## Slides [param control] in from [param offset] away from where it is, fading in.
|
|
||||||
static func slide_in(control: Control, offset: Vector2, duration: float = NORMAL) -> void:
|
|
||||||
var slide_tween: Tween = tween(control, &"slide")
|
|
||||||
if control.has_meta(&"_slide_home"):
|
|
||||||
control.position = control.get_meta(&"_slide_home")
|
|
||||||
control.modulate.a = 1.0
|
|
||||||
if slide_tween == null:
|
|
||||||
return
|
|
||||||
var home: Vector2 = control.position
|
|
||||||
control.set_meta(&"_slide_home", home)
|
|
||||||
control.position = home + offset
|
|
||||||
control.modulate.a = 0.0
|
|
||||||
slide_tween.set_parallel()
|
|
||||||
slide_tween.tween_property(control, "position", home, duration) \
|
|
||||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
|
||||||
slide_tween.tween_property(control, "modulate:a", 1.0, duration)
|
|
||||||
slide_tween.chain().tween_callback(func() -> void: control.remove_meta(&"_slide_home"))
|
|
||||||
|
|
||||||
|
|
||||||
## Scales [param control] to [param target] times its size, e.g. a little bigger under
|
|
||||||
## the mouse.
|
|
||||||
static func scale_to(control: Control, target: float, duration: float = FAST) -> void:
|
|
||||||
var scale_tween: Tween = tween(control, &"scale")
|
|
||||||
if scale_tween == null:
|
|
||||||
control.scale = Vector2.ONE
|
|
||||||
return
|
|
||||||
center_pivot(control)
|
|
||||||
scale_tween.tween_property(control, "scale", Vector2.ONE * target, duration) \
|
|
||||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
|
||||||
|
|
||||||
|
|
||||||
## Lights up [param container] with [param colour] for a moment, e.g. a row that just
|
|
||||||
## changed. A coloured layer is laid over the container and fades away.
|
|
||||||
static func flash(container: Container, colour: Color, duration: float = SLOW) -> void:
|
|
||||||
if !enabled() || !container.is_inside_tree():
|
|
||||||
return
|
|
||||||
var layer: ColorRect = _layer(container, colour)
|
|
||||||
var flash_tween: Tween = layer.create_tween()
|
|
||||||
flash_tween.tween_property(layer, "color:a", 0.0, duration) \
|
|
||||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
|
||||||
flash_tween.tween_callback(layer.queue_free)
|
|
||||||
|
|
||||||
|
|
||||||
## Keeps [param container] glowing in [param colour], softly pulsing, until the returned
|
|
||||||
## layer is freed. With animations off the glow stays still.
|
|
||||||
static func pulse(container: Container, colour: Color) -> ColorRect:
|
|
||||||
var layer: ColorRect = _layer(container, colour)
|
|
||||||
layer.name = "Pulse"
|
|
||||||
if enabled():
|
|
||||||
var pulse_tween: Tween = layer.create_tween().set_loops()
|
|
||||||
pulse_tween.tween_property(layer, "color:a", colour.a * 0.3, 0.6) \
|
|
||||||
.set_trans(Tween.TRANS_SINE)
|
|
||||||
pulse_tween.tween_property(layer, "color:a", colour.a, 0.6) \
|
|
||||||
.set_trans(Tween.TRANS_SINE)
|
|
||||||
return layer
|
|
||||||
|
|
||||||
|
|
||||||
# A layer of [param colour] over the whole of [param container] that lets clicks through
|
|
||||||
static func _layer(container: Container, colour: Color) -> ColorRect:
|
|
||||||
var layer: ColorRect = ColorRect.new()
|
|
||||||
layer.color = colour
|
|
||||||
layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
||||||
container.add_child(layer)
|
|
||||||
return layer
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://67is3ejxq0j8
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
class_name AnimatedProgressBar
|
|
||||||
extends ProgressBar
|
|
||||||
## A progress bar with some life in it:
|
|
||||||
## - [method wait] shows Godot's busy animation until there is any progress;
|
|
||||||
## - [method move_to] glides to each new value instead of jumping;
|
|
||||||
## - a soft highlight sweeps along the filled part, kept inside its rounded corners.
|
|
||||||
##
|
|
||||||
## The bar's style, e.g. [code]SyncProgressBar[/code], has a [code]shine[/code] colour for
|
|
||||||
## the highlight, and two more styles go with it: [code]<style>Fill[/code], a Panel shaped
|
|
||||||
## like the filled part that the highlight is kept inside, and [code]<style>Text[/code],
|
|
||||||
## the Label with the percentage. With Reduce animations the bar only jumps to its values,
|
|
||||||
## with no busy animation or highlight.
|
|
||||||
|
|
||||||
## How wide the highlight is, in pixels.
|
|
||||||
const SHINE_WIDTH: float = 260.0
|
|
||||||
## How fast the highlight moves, in pixels per second.
|
|
||||||
const SHINE_SPEED: float = 520.0
|
|
||||||
## How far past the end the highlight goes before starting over, so sweeps have a pause.
|
|
||||||
const SHINE_GAP: float = 600.0
|
|
||||||
|
|
||||||
# The filled part's shape, which only its children are drawn inside
|
|
||||||
var _mask: Panel
|
|
||||||
var _shine: TextureRect
|
|
||||||
# The percentage, drawn above the highlight rather than under it
|
|
||||||
var _label: Label
|
|
||||||
var _shine_x: float = -SHINE_WIDTH
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
_mask = Panel.new()
|
|
||||||
_mask.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
||||||
_mask.clip_children = CanvasItem.CLIP_CHILDREN_ONLY
|
|
||||||
add_child(_mask)
|
|
||||||
_shine = TextureRect.new()
|
|
||||||
_shine.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
||||||
_shine.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
|
||||||
_shine.stretch_mode = TextureRect.STRETCH_SCALE
|
|
||||||
_mask.add_child(_shine)
|
|
||||||
_label = Label.new()
|
|
||||||
_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
||||||
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
||||||
_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
||||||
_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
||||||
_label.visible = show_percentage
|
|
||||||
add_child(_label)
|
|
||||||
show_percentage = false
|
|
||||||
_mask.theme_type_variation = StringName(String(theme_type_variation) + "Fill")
|
|
||||||
_label.theme_type_variation = StringName(String(theme_type_variation) + "Text")
|
|
||||||
theme_changed.connect(_update_style)
|
|
||||||
value_changed.connect(func(_value: float) -> void: _update_label())
|
|
||||||
_update_style()
|
|
||||||
_update_label()
|
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
|
||||||
var fill_width: float = fill_width()
|
|
||||||
_mask.size = Vector2(fill_width, size.y)
|
|
||||||
_mask.visible = fill_width > 0.0 && !indeterminate && Anim.enabled()
|
|
||||||
if !_mask.visible:
|
|
||||||
return
|
|
||||||
_shine_x += SHINE_SPEED * delta
|
|
||||||
if _shine_x > fill_width + SHINE_GAP:
|
|
||||||
_shine_x = -SHINE_WIDTH
|
|
||||||
_shine.position = Vector2(_shine_x, 0.0)
|
|
||||||
_shine.size = Vector2(SHINE_WIDTH, size.y)
|
|
||||||
|
|
||||||
|
|
||||||
## Shows the busy animation, with no value, until [method move_to] has some progress.
|
|
||||||
func wait() -> void:
|
|
||||||
Anim.stop(self, &"progress")
|
|
||||||
value = min_value
|
|
||||||
indeterminate = Anim.enabled()
|
|
||||||
_update_label()
|
|
||||||
|
|
||||||
|
|
||||||
## Glides to [param target]. Any progress ends the busy animation.
|
|
||||||
func move_to(target: float) -> void:
|
|
||||||
if target > min_value:
|
|
||||||
indeterminate = false
|
|
||||||
var glide: Tween = Anim.tween(self, &"progress")
|
|
||||||
if glide == null:
|
|
||||||
value = target
|
|
||||||
return
|
|
||||||
glide.tween_property(self, "value", target, Anim.SLOW) \
|
|
||||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
|
||||||
|
|
||||||
|
|
||||||
## How wide the filled part is, the way [ProgressBar] draws it.
|
|
||||||
func fill_width() -> float:
|
|
||||||
var margins: float = get_theme_stylebox(&"fill").get_minimum_size().x
|
|
||||||
var filled: float = roundf(ratio_done() * (size.x - margins))
|
|
||||||
return filled + margins if filled > 0.0 else 0.0
|
|
||||||
|
|
||||||
|
|
||||||
func _update_style() -> void:
|
|
||||||
var shine: Color = get_theme_color(&"shine")
|
|
||||||
var clear: Color = shine
|
|
||||||
clear.a = 0.0
|
|
||||||
var gradient: Gradient = Gradient.new()
|
|
||||||
gradient.offsets = PackedFloat32Array([0.0, 0.5, 1.0])
|
|
||||||
gradient.colors = PackedColorArray([clear, shine, clear])
|
|
||||||
var texture: GradientTexture2D = GradientTexture2D.new()
|
|
||||||
texture.gradient = gradient
|
|
||||||
texture.width = 256
|
|
||||||
texture.height = 1
|
|
||||||
_shine.texture = texture
|
|
||||||
|
|
||||||
|
|
||||||
func _update_label() -> void:
|
|
||||||
if _label == null:
|
|
||||||
return
|
|
||||||
_label.text = "" if indeterminate else "%d%%" % roundi(ratio_done() * 100.0)
|
|
||||||
|
|
||||||
|
|
||||||
## How much is done, from 0 to 1.
|
|
||||||
func ratio_done() -> float:
|
|
||||||
var span: float = max_value - min_value
|
|
||||||
return clampf((value - min_value) / span, 0.0, 1.0) if span > 0.0 else 0.0
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://bxjx3kacfqg6j
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
class_name AudioFile
|
|
||||||
extends RefCounted
|
|
||||||
## Reads songs from files: the songs downloaded from the server, and the songs in a music
|
|
||||||
## folder.
|
|
||||||
|
|
||||||
## The file types Godot can play from a file while the game runs.
|
|
||||||
const PLAYABLE_EXTENSIONS: Array[String] = ["mp3", "ogg", "wav"]
|
|
||||||
|
|
||||||
|
|
||||||
## Whether [param path] is a file type the game can play.
|
|
||||||
static func is_playable(path: String) -> bool:
|
|
||||||
return path.get_extension().to_lower() in PLAYABLE_EXTENSIONS
|
|
||||||
|
|
||||||
|
|
||||||
## Reads the song in [param path]. Returns null if the file is missing, is not a playable
|
|
||||||
## type, or is broken.
|
|
||||||
static func load(path: String) -> AudioStream:
|
|
||||||
if !FileAccess.file_exists(path):
|
|
||||||
return null
|
|
||||||
var stream: AudioStream = null
|
|
||||||
match path.get_extension().to_lower():
|
|
||||||
"mp3":
|
|
||||||
stream = AudioStreamMP3.load_from_file(path)
|
|
||||||
"ogg":
|
|
||||||
stream = AudioStreamOggVorbis.load_from_file(path)
|
|
||||||
"wav":
|
|
||||||
stream = AudioStreamWAV.load_from_file(path)
|
|
||||||
if stream == null || stream.get_length() <= 0.0:
|
|
||||||
return null
|
|
||||||
return stream
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://drle78nqw2ve0
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
class_name ButtonBounce
|
|
||||||
extends RefCounted
|
|
||||||
## Small animations that make buttons feel alive: a button grows a little under the mouse,
|
|
||||||
## shrinks while it is held down, and can pop when its icon changes.
|
|
||||||
##
|
|
||||||
## The button is scaled around its middle, so its place in a container does not change.
|
|
||||||
## With [member Settings.reduce_animations] on, buttons keep their size.
|
|
||||||
|
|
||||||
## How big a button is under the mouse.
|
|
||||||
const HOVER_SCALE: Vector2 = Vector2(1.08, 1.08)
|
|
||||||
## How big a button is while it is held down.
|
|
||||||
const PRESSED_SCALE: Vector2 = Vector2(0.92, 0.92)
|
|
||||||
## How small a button starts when it pops.
|
|
||||||
const POP_SCALE: Vector2 = Vector2(0.8, 0.8)
|
|
||||||
## How long growing or shrinking takes, in seconds.
|
|
||||||
const DURATION: float = 0.08
|
|
||||||
## How long a pop takes, in seconds.
|
|
||||||
const POP_DURATION: float = 0.18
|
|
||||||
|
|
||||||
const _TWEEN_META: StringName = &"button_bounce_tween"
|
|
||||||
const _PIVOT_META: StringName = &"button_bounce_pivot"
|
|
||||||
const _ADDED_META: StringName = &"button_bounce_added"
|
|
||||||
|
|
||||||
|
|
||||||
## Gives every button in [param tree] the bounce, also buttons added later, e.g. rows
|
|
||||||
## of the player list. Check buttons are left out: they span a whole settings row.
|
|
||||||
static func add_to_every_button(tree: SceneTree) -> void:
|
|
||||||
if !tree.node_added.is_connected(_on_node_added):
|
|
||||||
tree.node_added.connect(_on_node_added)
|
|
||||||
var nodes: Array[Node] = [tree.root]
|
|
||||||
while !nodes.is_empty():
|
|
||||||
var node: Node = nodes.pop_back()
|
|
||||||
_on_node_added(node)
|
|
||||||
nodes.append_array(node.get_children())
|
|
||||||
|
|
||||||
|
|
||||||
static func _on_node_added(node: Node) -> void:
|
|
||||||
if node is BaseButton && !(node is CheckButton) && !(node is CheckBox):
|
|
||||||
add_to(node)
|
|
||||||
|
|
||||||
|
|
||||||
## Makes [param button] grow under the mouse and shrink while held down. Adding it to
|
|
||||||
## the same button twice does nothing.
|
|
||||||
static func add_to(button: BaseButton) -> void:
|
|
||||||
if button.has_meta(_ADDED_META):
|
|
||||||
return
|
|
||||||
button.set_meta(_ADDED_META, true)
|
|
||||||
_keep_pivot_centred(button)
|
|
||||||
button.mouse_entered.connect(func() -> void:
|
|
||||||
if !button.disabled:
|
|
||||||
_scale_to(button, HOVER_SCALE))
|
|
||||||
button.mouse_exited.connect(func() -> void: _scale_to(button, Vector2.ONE))
|
|
||||||
button.button_down.connect(func() -> void: _scale_to(button, PRESSED_SCALE))
|
|
||||||
button.button_up.connect(func() -> void: _scale_to(button, resting_scale(button)))
|
|
||||||
|
|
||||||
|
|
||||||
## The size [param button] goes back to when it is not held down: bigger under the mouse.
|
|
||||||
static func resting_scale(button: BaseButton) -> Vector2:
|
|
||||||
return HOVER_SCALE if button.is_hovered() && !button.disabled else Vector2.ONE
|
|
||||||
|
|
||||||
|
|
||||||
## Shrinks [param button] and lets it spring back, e.g. when its icon changes.
|
|
||||||
static func pop(button: BaseButton) -> void:
|
|
||||||
if !Anim.enabled():
|
|
||||||
return
|
|
||||||
_keep_pivot_centred(button)
|
|
||||||
button.scale = POP_SCALE
|
|
||||||
var tween: Tween = _new_tween(button)
|
|
||||||
tween.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
|
||||||
tween.tween_property(button, ^"scale", resting_scale(button), POP_DURATION)
|
|
||||||
|
|
||||||
|
|
||||||
static func _scale_to(button: BaseButton, target: Vector2) -> void:
|
|
||||||
if !Anim.enabled():
|
|
||||||
button.scale = Vector2.ONE
|
|
||||||
return
|
|
||||||
var tween: Tween = _new_tween(button)
|
|
||||||
tween.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
|
||||||
tween.tween_property(button, ^"scale", target, DURATION)
|
|
||||||
|
|
||||||
|
|
||||||
# A new tween for [param button], stopping the one before so they never fight
|
|
||||||
static func _new_tween(button: BaseButton) -> Tween:
|
|
||||||
if button.has_meta(_TWEEN_META):
|
|
||||||
var running: Tween = button.get_meta(_TWEEN_META)
|
|
||||||
if running.is_valid():
|
|
||||||
running.kill()
|
|
||||||
var tween: Tween = button.create_tween()
|
|
||||||
button.set_meta(_TWEEN_META, tween)
|
|
||||||
return tween
|
|
||||||
|
|
||||||
|
|
||||||
static func _keep_pivot_centred(button: BaseButton) -> void:
|
|
||||||
button.pivot_offset = button.size / 2
|
|
||||||
if !button.has_meta(_PIVOT_META):
|
|
||||||
button.set_meta(_PIVOT_META, true)
|
|
||||||
button.resized.connect(func() -> void: button.pivot_offset = button.size / 2)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://c4vekt3m8q1p7
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
class_name CharacterCache
|
|
||||||
extends RefCounted
|
|
||||||
## Character pictures kept on disk in the [code]characters[/code] folder next to the
|
|
||||||
## settings, so each one is downloaded only when it changed on the server.
|
|
||||||
##
|
|
||||||
## [code]index.json[/code] in the folder records, for each picture, when the server last
|
|
||||||
## changed it and when it was downloaded:
|
|
||||||
## [codeblock]
|
|
||||||
## {"Crono.png": {"modified": 1730000000, "downloaded": 1730100000}}
|
|
||||||
## [/codeblock]
|
|
||||||
## A cached picture is used while the server's change time matches the index. Servers that
|
|
||||||
## can't tell when pictures changed answer 404 for [code]/character/details[/code]; then a
|
|
||||||
## cached picture is used until it is [constant MAX_AGE_SECONDS] old. When the server
|
|
||||||
## can't be reached at all, the cached pictures are used as they are.
|
|
||||||
|
|
||||||
const FOLDER_NAME: String = "characters"
|
|
||||||
const INDEX_FILE_NAME: String = FileCache.INDEX_FILE_NAME
|
|
||||||
## How long a picture is used before downloading it again, when the server can't tell
|
|
||||||
## whether it changed.
|
|
||||||
const MAX_AGE_SECONDS: int = 7 * 24 * 60 * 60
|
|
||||||
## The change time of a picture when the server can't tell it.
|
|
||||||
const UNKNOWN_TIME: int = FileCache.UNKNOWN_TIME
|
|
||||||
|
|
||||||
# The pictures on disk and their index
|
|
||||||
var _files: FileCache = FileCache.new(FOLDER_NAME)
|
|
||||||
# When the server last changed each picture on the list, or UNKNOWN_TIME
|
|
||||||
var _modified: Dictionary[String, int] = {}
|
|
||||||
# True when the server could not be reached and only cached pictures are shown
|
|
||||||
var _cache_only: bool = false
|
|
||||||
|
|
||||||
|
|
||||||
## The folder the pictures are kept in.
|
|
||||||
static func folder() -> String:
|
|
||||||
return Settings.config_dir().path_join(FOLDER_NAME)
|
|
||||||
|
|
||||||
|
|
||||||
## Deletes every cached picture. They are downloaded again when next asked for.
|
|
||||||
func clear() -> void:
|
|
||||||
_files.clear()
|
|
||||||
|
|
||||||
|
|
||||||
## Fetches the names of the characters and calls [param on_done] with them as an
|
|
||||||
## [code]Array[String][/code]. Cached pictures of characters no longer on the server are
|
|
||||||
## deleted.
|
|
||||||
func load_list(on_done: Callable) -> void:
|
|
||||||
_files.load_index()
|
|
||||||
_cache_only = false
|
|
||||||
var details_fetched: Callable = func details_fetched(details: Array) -> void:
|
|
||||||
_modified.clear()
|
|
||||||
for character: CharacterDetails in details:
|
|
||||||
_modified[character.name] = character.modified
|
|
||||||
_list_loaded(on_done)
|
|
||||||
MusicServerClient.fetch_character_details(details_fetched, _load_names_only.bind(on_done))
|
|
||||||
|
|
||||||
|
|
||||||
## Calls [param on_done] with the picture of [param character_name] as an [Image], from
|
|
||||||
## disk when the cached one is still fresh, otherwise from the server.
|
|
||||||
func fetch_image(character_name: String, on_done: Callable) -> void:
|
|
||||||
if (_cache_only || _is_fresh(character_name)) && _files.has_file(character_name):
|
|
||||||
var cached: Image = CharacterCard.image_from_bytes(_files.read(character_name))
|
|
||||||
if cached != null:
|
|
||||||
on_done.call(cached)
|
|
||||||
return
|
|
||||||
GameLog.warning("The cached picture of ", character_name, " is broken")
|
|
||||||
var image_fetched: Callable = func image_fetched(bytes: PackedByteArray) -> void:
|
|
||||||
var picture: Image = CharacterCard.image_from_bytes(bytes)
|
|
||||||
if picture == null:
|
|
||||||
GameLog.warning("Could not load the picture of ", character_name)
|
|
||||||
return
|
|
||||||
_files.store(character_name, bytes, _modified.get(character_name, UNKNOWN_TIME))
|
|
||||||
on_done.call(picture)
|
|
||||||
MusicServerClient.fetch_character_image(character_name, image_fetched)
|
|
||||||
|
|
||||||
|
|
||||||
# Servers older than /character/details only list the names
|
|
||||||
func _load_names_only(on_done: Callable) -> void:
|
|
||||||
var list_fetched: Callable = func list_fetched(list: Variant) -> void:
|
|
||||||
_modified.clear()
|
|
||||||
if list is Array:
|
|
||||||
for character_name: Variant in list:
|
|
||||||
_modified[str(character_name)] = UNKNOWN_TIME
|
|
||||||
_list_loaded(on_done)
|
|
||||||
MusicServerClient.fetch_character_list(list_fetched, _load_cached_only.bind(on_done))
|
|
||||||
|
|
||||||
|
|
||||||
# The server can't be reached, so the pictures already cached are shown and kept
|
|
||||||
func _load_cached_only(on_done: Callable) -> void:
|
|
||||||
GameLog.warning("Could not get the characters from the server, showing the cached ones")
|
|
||||||
_cache_only = true
|
|
||||||
var names: Array[String] = []
|
|
||||||
for character_name: String in _files.keys():
|
|
||||||
if _files.has_file(character_name):
|
|
||||||
names.append(character_name)
|
|
||||||
names.sort_custom(func(a: String, b: String) -> bool: return a.naturalnocasecmp_to(b) < 0)
|
|
||||||
on_done.call(names)
|
|
||||||
|
|
||||||
|
|
||||||
func _list_loaded(on_done: Callable) -> void:
|
|
||||||
_files.keep_only(_modified)
|
|
||||||
var names: Array[String] = []
|
|
||||||
names.assign(_modified.keys())
|
|
||||||
on_done.call(names)
|
|
||||||
|
|
||||||
|
|
||||||
func _is_fresh(character_name: String) -> bool:
|
|
||||||
if !_files.keys().has(character_name):
|
|
||||||
return false
|
|
||||||
var modified: int = _modified.get(character_name, UNKNOWN_TIME)
|
|
||||||
if modified != UNKNOWN_TIME:
|
|
||||||
return _files.modified(character_name) == modified
|
|
||||||
return _files.age(character_name) < MAX_AGE_SECONDS
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://34xwe7vs2md7
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
class_name CoverCache
|
|
||||||
extends RefCounted
|
|
||||||
## Soundtrack covers kept on disk in the [code]covers[/code] folder next to the settings,
|
|
||||||
## by soundtrack UUID, so each one is downloaded only when it changed on the server (its
|
|
||||||
## [code]cover_modified[/code] time). See [FileCache].
|
|
||||||
##
|
|
||||||
## Covers are downloaded a few at a time, in the order they are asked for, so opening the
|
|
||||||
## statistics doesn't send a request for every soundtrack at once.
|
|
||||||
##
|
|
||||||
## It also remembers each soundtrack's cover time from the soundtrack lists it is given,
|
|
||||||
## so [method fetch_cover_of] finds a cover by the soundtrack alone, e.g. in the quiz.
|
|
||||||
|
|
||||||
## Emitted when the cover time of a soundtrack is new or changed, e.g. once the soundtrack
|
|
||||||
## list has loaded or a cover was changed.
|
|
||||||
signal cover_times_changed
|
|
||||||
|
|
||||||
const FOLDER_NAME: String = "covers"
|
|
||||||
## How many covers are downloaded at the same time.
|
|
||||||
const MAX_DOWNLOADS: int = 3
|
|
||||||
|
|
||||||
# The covers on disk and their index
|
|
||||||
var _files: FileCache = FileCache.new(FOLDER_NAME)
|
|
||||||
# Covers waiting to be downloaded: [uuid, modified]
|
|
||||||
var _waiting: Array[Array] = []
|
|
||||||
# Who is waiting for each cover that is asked for or downloading: uuid -> Array[Callable]
|
|
||||||
var _callbacks: Dictionary = {}
|
|
||||||
var _downloading: int = 0
|
|
||||||
# The server's cover time of each soundtrack seen: uuid -> int, 0 for none
|
|
||||||
var _times: Dictionary = {}
|
|
||||||
|
|
||||||
|
|
||||||
func _init() -> void:
|
|
||||||
_files.load_index()
|
|
||||||
|
|
||||||
|
|
||||||
## The folder the covers are kept in.
|
|
||||||
static func folder() -> String:
|
|
||||||
return Settings.config_dir().path_join(FOLDER_NAME)
|
|
||||||
|
|
||||||
|
|
||||||
## Deletes every cached cover. They are downloaded again when next asked for.
|
|
||||||
func clear() -> void:
|
|
||||||
_files.clear()
|
|
||||||
|
|
||||||
|
|
||||||
## Deletes the covers of soundtracks that are not in [param covers] (UUID -> cover
|
|
||||||
## time), and those whose soundtrack no longer has a cover.
|
|
||||||
func keep_only(covers: Dictionary) -> void:
|
|
||||||
set_cover_times(covers)
|
|
||||||
var kept: Dictionary = {}
|
|
||||||
for uuid: String in covers:
|
|
||||||
if int(covers[uuid]) > 0:
|
|
||||||
kept[uuid] = true
|
|
||||||
_files.keep_only(kept)
|
|
||||||
|
|
||||||
|
|
||||||
## Remembers the cover times in [param covers] (UUID -> cover time, 0 for no cover) and
|
|
||||||
## emits [signal cover_times_changed] when any is new or changed.
|
|
||||||
func set_cover_times(covers: Dictionary) -> void:
|
|
||||||
var changed: bool = false
|
|
||||||
for uuid: String in covers:
|
|
||||||
var modified: int = int(covers[uuid])
|
|
||||||
if _times.get(uuid, -1) != modified:
|
|
||||||
_times[uuid] = modified
|
|
||||||
changed = true
|
|
||||||
if changed:
|
|
||||||
cover_times_changed.emit()
|
|
||||||
|
|
||||||
|
|
||||||
## The cover time of the soundtrack [param uuid], 0 when it has no cover or none is known.
|
|
||||||
func cover_time(uuid: String) -> int:
|
|
||||||
return _times.get(uuid, 0)
|
|
||||||
|
|
||||||
|
|
||||||
## Like [method fetch_cover], with the cover time remembered for the soundtrack.
|
|
||||||
func fetch_cover_of(uuid: String, on_done: Callable) -> void:
|
|
||||||
fetch_cover(uuid, cover_time(uuid), on_done)
|
|
||||||
|
|
||||||
|
|
||||||
## Asks the server for the cover time of the soundtrack [param uuid] again, e.g. after
|
|
||||||
## its cover was changed, then calls [param on_done] with the new time, 0 for no cover.
|
|
||||||
## A removed cover is deleted from disk too. When the server doesn't answer, it is called
|
|
||||||
## with the time it had.
|
|
||||||
func refresh(uuid: String, on_done: Callable) -> void:
|
|
||||||
var with_soundtrack: Callable = func with_soundtrack(
|
|
||||||
soundtrack: SoundtrackWithSongList) -> void:
|
|
||||||
if soundtrack.cover_modified <= 0:
|
|
||||||
_files.remove(uuid)
|
|
||||||
set_cover_times({uuid: soundtrack.cover_modified})
|
|
||||||
on_done.call(soundtrack.cover_modified)
|
|
||||||
# Without an answer the cover is left as it was
|
|
||||||
MusicServerClient.fetch_soundtrack(uuid, with_soundtrack,
|
|
||||||
func() -> void: on_done.call(cover_time(uuid)))
|
|
||||||
|
|
||||||
|
|
||||||
## Calls [param on_done] with the cover of the soundtrack [param uuid] as an [Image]:
|
|
||||||
## from disk when the cached one has the server's time [param modified], otherwise once
|
|
||||||
## it has been downloaded. Nothing is called when the soundtrack has no cover
|
|
||||||
## ([param modified] 0) or the download fails.
|
|
||||||
func fetch_cover(uuid: String, modified: int, on_done: Callable) -> void:
|
|
||||||
if modified <= 0 || uuid.is_empty():
|
|
||||||
return
|
|
||||||
if _files.has_file(uuid) && _files.modified(uuid) == modified:
|
|
||||||
var cached: Image = CharacterCard.image_from_bytes(_files.read(uuid))
|
|
||||||
if cached != null:
|
|
||||||
on_done.call(cached)
|
|
||||||
return
|
|
||||||
if _callbacks.has(uuid):
|
|
||||||
_callbacks[uuid].append(on_done)
|
|
||||||
return
|
|
||||||
_callbacks[uuid] = [on_done]
|
|
||||||
_waiting.append([uuid, modified])
|
|
||||||
_download_next()
|
|
||||||
|
|
||||||
|
|
||||||
## How many covers are waiting for their turn or downloading.
|
|
||||||
func busy() -> int:
|
|
||||||
return _callbacks.size()
|
|
||||||
|
|
||||||
|
|
||||||
func _download_next() -> void:
|
|
||||||
while _downloading < MAX_DOWNLOADS && !_waiting.is_empty():
|
|
||||||
var next: Array = _waiting.pop_front()
|
|
||||||
var uuid: String = next[0]
|
|
||||||
_downloading += 1
|
|
||||||
MusicServerClient.fetch_soundtrack_cover(uuid,
|
|
||||||
_on_downloaded.bind(uuid, next[1]), _on_failed.bind(uuid))
|
|
||||||
|
|
||||||
|
|
||||||
func _on_downloaded(bytes: PackedByteArray, uuid: String, modified: int) -> void:
|
|
||||||
_downloading -= 1
|
|
||||||
var cover: Image = CharacterCard.image_from_bytes(bytes)
|
|
||||||
if cover == null:
|
|
||||||
GameLog.warning("The cover of soundtrack ", uuid, " could not be read")
|
|
||||||
else:
|
|
||||||
_files.store(uuid, bytes, modified)
|
|
||||||
for on_done: Callable in _callbacks.get(uuid, []):
|
|
||||||
# A card may be gone by the time its cover arrives
|
|
||||||
if cover != null && on_done.is_valid():
|
|
||||||
on_done.call(cover)
|
|
||||||
_callbacks.erase(uuid)
|
|
||||||
_download_next()
|
|
||||||
|
|
||||||
|
|
||||||
# A 404 means the server has no cover for it yet
|
|
||||||
func _on_failed(uuid: String) -> void:
|
|
||||||
_downloading -= 1
|
|
||||||
_callbacks.erase(uuid)
|
|
||||||
_download_next()
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://djxm2kkq8hol1
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
class_name FileCache
|
|
||||||
extends RefCounted
|
|
||||||
## Downloaded files kept in a folder next to the settings, with an [code]index.json[/code]
|
|
||||||
## that records for each file when the server last changed it and when it was
|
|
||||||
## downloaded:
|
|
||||||
## [codeblock]
|
|
||||||
## {"Crono.png": {"modified": 1730000000, "downloaded": 1730100000}}
|
|
||||||
## [/codeblock]
|
|
||||||
## Used by [CharacterCache] and [CoverCache], which decide when a file is fresh.
|
|
||||||
|
|
||||||
const INDEX_FILE_NAME: String = "index.json"
|
|
||||||
## The change time of a file when the server can't tell it.
|
|
||||||
const UNKNOWN_TIME: int = -1
|
|
||||||
|
|
||||||
## The folder's name in [method Settings.config_dir].
|
|
||||||
var folder_name: String
|
|
||||||
|
|
||||||
# What index.json holds: key -> {"modified": int, "downloaded": int}
|
|
||||||
var _index: Dictionary = {}
|
|
||||||
|
|
||||||
|
|
||||||
func _init(new_folder_name: String) -> void:
|
|
||||||
folder_name = new_folder_name
|
|
||||||
|
|
||||||
|
|
||||||
## The folder the files are kept in.
|
|
||||||
func folder() -> String:
|
|
||||||
return Settings.config_dir().path_join(folder_name)
|
|
||||||
|
|
||||||
|
|
||||||
## Reads [code]index.json[/code]. A missing or broken index starts empty.
|
|
||||||
func load_index() -> void:
|
|
||||||
_index = {}
|
|
||||||
var path: String = folder().path_join(INDEX_FILE_NAME)
|
|
||||||
if FileAccess.file_exists(path):
|
|
||||||
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(path))
|
|
||||||
if parsed is Dictionary:
|
|
||||||
_index = parsed
|
|
||||||
|
|
||||||
|
|
||||||
## The keys of the files in the index.
|
|
||||||
func keys() -> Array:
|
|
||||||
return _index.keys()
|
|
||||||
|
|
||||||
|
|
||||||
## Whether the file for [param key] is in the index and on disk.
|
|
||||||
func has_file(key: String) -> bool:
|
|
||||||
return _index.has(key) && FileAccess.file_exists(file_path(key))
|
|
||||||
|
|
||||||
|
|
||||||
## When the server last changed the file for [param key], as recorded when it was
|
|
||||||
## downloaded, or [constant UNKNOWN_TIME].
|
|
||||||
func modified(key: String) -> int:
|
|
||||||
return int(_index.get(key, {}).get("modified", UNKNOWN_TIME))
|
|
||||||
|
|
||||||
|
|
||||||
## How many seconds ago the file for [param key] was downloaded.
|
|
||||||
func age(key: String) -> float:
|
|
||||||
return Time.get_unix_time_from_system() - float(_index.get(key, {}).get("downloaded", 0))
|
|
||||||
|
|
||||||
|
|
||||||
## The file for [param key].
|
|
||||||
func read(key: String) -> PackedByteArray:
|
|
||||||
return FileAccess.get_file_as_bytes(file_path(key))
|
|
||||||
|
|
||||||
|
|
||||||
## Writes [param bytes] as the file for [param key], last changed on the server at
|
|
||||||
## [param modified_time], and saves the index. Returns false if it could not be written.
|
|
||||||
func store(key: String, bytes: PackedByteArray, modified_time: int) -> bool:
|
|
||||||
DirAccess.make_dir_recursive_absolute(folder())
|
|
||||||
var file: FileAccess = FileAccess.open(file_path(key), FileAccess.WRITE)
|
|
||||||
if file == null:
|
|
||||||
GameLog.warning("Could not cache ", key, " in ", folder_name, ": ",
|
|
||||||
error_string(FileAccess.get_open_error()))
|
|
||||||
return false
|
|
||||||
file.store_buffer(bytes)
|
|
||||||
file.close()
|
|
||||||
_index[key] = {
|
|
||||||
"modified": modified_time,
|
|
||||||
"downloaded": int(Time.get_unix_time_from_system()),
|
|
||||||
}
|
|
||||||
save_index()
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
## Deletes the file for [param key] and its entry, and saves the index.
|
|
||||||
func remove(key: String) -> void:
|
|
||||||
DirAccess.remove_absolute(file_path(key))
|
|
||||||
if _index.erase(key):
|
|
||||||
save_index()
|
|
||||||
|
|
||||||
|
|
||||||
## Deletes every file whose key is not in [param kept], e.g. files of things the server
|
|
||||||
## no longer has.
|
|
||||||
func keep_only(kept: Dictionary) -> void:
|
|
||||||
var removed: bool = false
|
|
||||||
for key: String in _index.keys():
|
|
||||||
if !kept.has(key):
|
|
||||||
DirAccess.remove_absolute(file_path(key))
|
|
||||||
_index.erase(key)
|
|
||||||
removed = true
|
|
||||||
if removed:
|
|
||||||
save_index()
|
|
||||||
|
|
||||||
|
|
||||||
## Deletes every file in the folder, the index too, e.g. to free the space they take.
|
|
||||||
func clear() -> void:
|
|
||||||
_index = {}
|
|
||||||
if !DirAccess.dir_exists_absolute(folder()):
|
|
||||||
return
|
|
||||||
for file_name: String in DirAccess.get_files_at(folder()):
|
|
||||||
DirAccess.remove_absolute(folder().path_join(file_name))
|
|
||||||
|
|
||||||
|
|
||||||
## How many bytes the files in [param dir] take, leaving out the paths in [param skip]
|
|
||||||
## and any subfolders. 0 when the folder doesn't exist.
|
|
||||||
static func folder_size(dir: String, skip: PackedStringArray = []) -> int:
|
|
||||||
if !DirAccess.dir_exists_absolute(dir):
|
|
||||||
return 0
|
|
||||||
var total: int = 0
|
|
||||||
for file_name: String in DirAccess.get_files_at(dir):
|
|
||||||
var path: String = dir.path_join(file_name)
|
|
||||||
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
|
|
||||||
if file != null && !skip.has(path):
|
|
||||||
total += file.get_length()
|
|
||||||
return total
|
|
||||||
|
|
||||||
|
|
||||||
func save_index() -> void:
|
|
||||||
DirAccess.make_dir_recursive_absolute(folder())
|
|
||||||
var file: FileAccess = FileAccess.open(folder().path_join(INDEX_FILE_NAME),
|
|
||||||
FileAccess.WRITE)
|
|
||||||
if file != null:
|
|
||||||
file.store_string(JSON.stringify(_index, "\t", true))
|
|
||||||
|
|
||||||
|
|
||||||
## Where the file for [param key] is. Keys come from the server, so they are made safe
|
|
||||||
## as file names.
|
|
||||||
func file_path(key: String) -> String:
|
|
||||||
return folder().path_join(key.validate_filename())
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://0jfq23w54ihw
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
class_name FolderCharacters
|
|
||||||
extends RefCounted
|
|
||||||
## Characters read from a folder on this computer instead of the server: every PNG, JPG
|
|
||||||
## or WebP picture in it is a character, named by its file name.
|
|
||||||
##
|
|
||||||
## Has the same two calls as [CharacterCache], so the character select popup uses either.
|
|
||||||
|
|
||||||
## The file endings of the pictures that are characters.
|
|
||||||
const PICTURE_EXTENSIONS: Array[String] = ["png", "jpg", "jpeg", "webp"]
|
|
||||||
|
|
||||||
## The folder the pictures are in.
|
|
||||||
var path: String = ""
|
|
||||||
|
|
||||||
|
|
||||||
func _init(folder_path: String) -> void:
|
|
||||||
path = folder_path
|
|
||||||
|
|
||||||
|
|
||||||
## The file names of the pictures in [param folder], in natural order. Hidden files and
|
|
||||||
## other files are left out.
|
|
||||||
static func picture_names(folder: String) -> Array[String]:
|
|
||||||
var names: Array[String] = []
|
|
||||||
for file_name: String in DirAccess.get_files_at(folder):
|
|
||||||
if !file_name.begins_with(".") \
|
|
||||||
&& PICTURE_EXTENSIONS.has(file_name.get_extension().to_lower()):
|
|
||||||
names.append(file_name)
|
|
||||||
names.sort_custom(func(a: String, b: String) -> bool: return a.naturalnocasecmp_to(b) < 0)
|
|
||||||
return names
|
|
||||||
|
|
||||||
|
|
||||||
## Calls [param on_done] with the names of the characters as an [code]Array[String][/code].
|
|
||||||
func load_list(on_done: Callable) -> void:
|
|
||||||
on_done.call(picture_names(path))
|
|
||||||
|
|
||||||
|
|
||||||
## Calls [param on_done] with the picture of [param character_name] as an [Image]. Nothing
|
|
||||||
## is called when the picture is gone or can't be read.
|
|
||||||
func fetch_image(character_name: String, on_done: Callable) -> void:
|
|
||||||
var file: String = path.path_join(character_name.validate_filename())
|
|
||||||
if !FileAccess.file_exists(file):
|
|
||||||
GameLog.warning("The picture of ", character_name, " is not in ", path)
|
|
||||||
return
|
|
||||||
var picture: Image = CharacterCard.image_from_bytes(FileAccess.get_file_as_bytes(file))
|
|
||||||
if picture == null:
|
|
||||||
GameLog.warning("Could not read the picture ", file)
|
|
||||||
return
|
|
||||||
on_done.call(picture)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://bc2nkmd162ibl
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
extends Node
|
|
||||||
## Writes messages about what the game is doing, for finding problems.
|
|
||||||
##
|
|
||||||
## Registered as the [code]GameLog[/code] autoload. Use [method debug] instead of
|
|
||||||
## [code]print()[/code]: the messages show in debug builds and the editor, and are left
|
|
||||||
## out of released games. Problems the player may notice go through [method warning].
|
|
||||||
|
|
||||||
|
|
||||||
## Prints [param parts] joined together, like [code]print()[/code], in debug builds only.
|
|
||||||
func debug(...parts: Array) -> void:
|
|
||||||
if OS.is_debug_build():
|
|
||||||
print(_join(parts))
|
|
||||||
|
|
||||||
|
|
||||||
## Reports a problem in the editor's debugger and the console, in every build.
|
|
||||||
func warning(...parts: Array) -> void:
|
|
||||||
push_warning(_join(parts))
|
|
||||||
|
|
||||||
|
|
||||||
func _join(parts: Array) -> String:
|
|
||||||
return "".join(parts.map(func(part: Variant) -> String: return str(part)))
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://c37mlo6svk03q
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
class_name IniFile
|
|
||||||
extends RefCounted
|
|
||||||
## A small INI file for settings people edit by hand: [code][section][/code] headers and
|
|
||||||
## [code]key = value[/code] lines.
|
|
||||||
##
|
|
||||||
## Lines starting with [code];[/code] or [code]#[/code] are comments. Comments take whole
|
|
||||||
## lines, so a value such as a password can hold any character. Quotes around a value are
|
|
||||||
## optional and removed. Section and key names are not case-sensitive.
|
|
||||||
|
|
||||||
const TRUE_WORDS: Array[String] = ["true", "yes", "on", "1"]
|
|
||||||
const FALSE_WORDS: Array[String] = ["false", "no", "off", "0"]
|
|
||||||
|
|
||||||
var _sections: Dictionary[String, Dictionary] = {}
|
|
||||||
|
|
||||||
|
|
||||||
## Reads INI [param text]. Lines that are neither a section, a setting nor a comment are
|
|
||||||
## skipped with a warning.
|
|
||||||
static func parse(text: String) -> IniFile:
|
|
||||||
var ini: IniFile = IniFile.new()
|
|
||||||
var section: String = ""
|
|
||||||
var line_number: int = 0
|
|
||||||
for raw_line: String in text.split("\n"):
|
|
||||||
line_number += 1
|
|
||||||
var line: String = raw_line.strip_edges()
|
|
||||||
if line.is_empty() or line.begins_with(";") or line.begins_with("#"):
|
|
||||||
continue
|
|
||||||
if line.begins_with("[") and line.ends_with("]"):
|
|
||||||
section = line.substr(1, line.length() - 2).strip_edges().to_lower()
|
|
||||||
continue
|
|
||||||
var equals: int = line.find("=")
|
|
||||||
if equals <= 0:
|
|
||||||
push_warning("Line %d is not a setting: %s" % [line_number, line])
|
|
||||||
continue
|
|
||||||
var key: String = line.substr(0, equals).strip_edges().to_lower()
|
|
||||||
var value: String = line.substr(equals + 1).strip_edges()
|
|
||||||
if value.length() >= 2 and value[0] in ["\"", "'"] and value[-1] == value[0]:
|
|
||||||
value = value.substr(1, value.length() - 2)
|
|
||||||
if !ini._sections.has(section):
|
|
||||||
ini._sections[section] = {}
|
|
||||||
ini._sections[section][key] = value
|
|
||||||
return ini
|
|
||||||
|
|
||||||
|
|
||||||
## Reads the INI file at [param path]. A missing file gives an empty [IniFile].
|
|
||||||
static func load_file(path: String) -> IniFile:
|
|
||||||
return parse(FileAccess.get_file_as_string(path))
|
|
||||||
|
|
||||||
|
|
||||||
func has_value(section: String, key: String) -> bool:
|
|
||||||
return _sections.get(section.to_lower(), {}).has(key.to_lower())
|
|
||||||
|
|
||||||
|
|
||||||
## The sections that have at least one setting.
|
|
||||||
func get_sections() -> Array[String]:
|
|
||||||
return _sections.keys()
|
|
||||||
|
|
||||||
|
|
||||||
func get_string(section: String, key: String, default: String = "") -> String:
|
|
||||||
return _sections.get(section.to_lower(), {}).get(key.to_lower(), default)
|
|
||||||
|
|
||||||
|
|
||||||
## Reads true/false, yes/no, on/off or 1/0. Anything else gives [param default] and a
|
|
||||||
## warning.
|
|
||||||
func get_bool(section: String, key: String, default: bool) -> bool:
|
|
||||||
if !has_value(section, key):
|
|
||||||
return default
|
|
||||||
var value: String = get_string(section, key).to_lower()
|
|
||||||
if value in TRUE_WORDS:
|
|
||||||
return true
|
|
||||||
if value in FALSE_WORDS:
|
|
||||||
return false
|
|
||||||
push_warning("%s in [%s] should be true or false, not %s" % [key, section, value])
|
|
||||||
return default
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://b262vckl8hbn4
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
class_name ListRow
|
|
||||||
extends PanelContainer
|
|
||||||
## One item in a list of names, such as the inspiration list or a sync result column.
|
|
||||||
##
|
|
||||||
## Every row has its own background, alternating between the theme's [code]ListRowEven[/code]
|
|
||||||
## and [code]ListRowOdd[/code] styles, so it is clear where an item starts even when its
|
|
||||||
## text wraps over several lines.
|
|
||||||
|
|
||||||
## The label showing the item's text.
|
|
||||||
var label: Label
|
|
||||||
|
|
||||||
## The item's text.
|
|
||||||
var text: String:
|
|
||||||
get:
|
|
||||||
return label.text
|
|
||||||
|
|
||||||
|
|
||||||
## Creates the row for item number [param row] (counting from 0) in a list. The text uses
|
|
||||||
## the theme's [param label_style], e.g. [code]TextLarge[/code].
|
|
||||||
static func create(
|
|
||||||
item_text: String,
|
|
||||||
row: int,
|
|
||||||
label_style: StringName,
|
|
||||||
alignment: HorizontalAlignment = HORIZONTAL_ALIGNMENT_LEFT,
|
|
||||||
) -> ListRow:
|
|
||||||
var list_row: ListRow = ListRow.new()
|
|
||||||
list_row.theme_type_variation = &"ListRowOdd" if row % 2 != 0 else &"ListRowEven"
|
|
||||||
list_row.label = Label.new()
|
|
||||||
list_row.label.theme_type_variation = label_style
|
|
||||||
list_row.label.text = item_text
|
|
||||||
list_row.label.horizontal_alignment = alignment
|
|
||||||
list_row.label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
||||||
list_row.add_child(list_row.label)
|
|
||||||
return list_row
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://c2o2qunso5n8l
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
class_name SoundtrackSearch
|
|
||||||
extends RefCounted
|
|
||||||
## Finds soundtrack names matching a search text. Matches ignore case, spaces and
|
|
||||||
## special characters. Searches of at least [code]Settings.LETTERS_FOR_LOOSE_SEARCH[/code]
|
|
||||||
## letters also find the letters in order with others in between.
|
|
||||||
##
|
|
||||||
## Used by the Search popup and the statistics view, so both find the same soundtracks.
|
|
||||||
##
|
|
||||||
## A soundtrack is also found by its alternative names, e.g. "FF7" for "Final Fantasy
|
|
||||||
## VII". The results are always the real names; [method matched_alternative] tells which
|
|
||||||
## alternative name matched.
|
|
||||||
|
|
||||||
## Each searchable name as a Dictionary: the soundtrack's real name ("original"), the name
|
|
||||||
## searched in ("text"), and its lowercase and cleaned forms.
|
|
||||||
var _names: Array[Dictionary] = []
|
|
||||||
# The alternative name that matched in the last search, by real name
|
|
||||||
var _matched_alternatives: Dictionary = {}
|
|
||||||
|
|
||||||
## Compiled patterns for the letters-in-order search, by cleaned search term.
|
|
||||||
var _regex_cache: Dictionary = {}
|
|
||||||
|
|
||||||
|
|
||||||
## Replaces the names to search in. [param alternatives] has the other names of a
|
|
||||||
## soundtrack, by its real name.
|
|
||||||
func set_names(names: Array, alternatives: Dictionary = {}) -> void:
|
|
||||||
_names.clear()
|
|
||||||
_matched_alternatives.clear()
|
|
||||||
for soundtrack: String in names:
|
|
||||||
_add_name(soundtrack, soundtrack)
|
|
||||||
for alternative: String in alternatives.get(soundtrack, []):
|
|
||||||
_add_name(soundtrack, alternative)
|
|
||||||
|
|
||||||
|
|
||||||
## The alternative name of [param soundtrack_name] that matched in the last
|
|
||||||
## [method find_matches], or an empty String when its real name matched.
|
|
||||||
func matched_alternative(soundtrack_name: String) -> String:
|
|
||||||
return _matched_alternatives.get(soundtrack_name, "")
|
|
||||||
|
|
||||||
|
|
||||||
## Returns the names 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.
|
|
||||||
func find_matches(search_text: String) -> Array[String]:
|
|
||||||
var seen: Dictionary = {}
|
|
||||||
var search_text_lower: String = search_text.to_lower()
|
|
||||||
var clean_search_text: String = clean_term(search_text)
|
|
||||||
var passes: Array[Callable] = [
|
|
||||||
func(entry: Dictionary) -> bool: return entry.lowercase.contains(search_text_lower),
|
|
||||||
func(entry: Dictionary) -> bool: return entry.cleaned.contains(clean_search_text),
|
|
||||||
]
|
|
||||||
if clean_search_text.length() >= Settings.LETTERS_FOR_LOOSE_SEARCH:
|
|
||||||
var regex: RegEx = _compile_regex_cached(clean_search_text)
|
|
||||||
passes.append(
|
|
||||||
func(entry: Dictionary) -> bool: return regex.search(entry.cleaned) != null)
|
|
||||||
|
|
||||||
var matches: Array[String] = []
|
|
||||||
_matched_alternatives.clear()
|
|
||||||
for matches_pass: Callable in passes:
|
|
||||||
for entry: Dictionary in _names:
|
|
||||||
if !seen.has(entry.original) && matches_pass.call(entry):
|
|
||||||
seen[entry.original] = true
|
|
||||||
matches.append(entry.original)
|
|
||||||
if entry.text != entry.original:
|
|
||||||
_matched_alternatives[entry.original] = entry.text
|
|
||||||
return matches
|
|
||||||
|
|
||||||
|
|
||||||
## The positions of the letters in [param soundtrack_name] that [param search_text]
|
|
||||||
## matched, for highlighting them: the text itself if the name contains it, otherwise the
|
|
||||||
## letters found in order the way [method find_matches] finds them. Empty if it does not
|
|
||||||
## match.
|
|
||||||
static func matched_letters(soundtrack_name: String, search_text: String) -> Array[int]:
|
|
||||||
var positions: Array[int] = []
|
|
||||||
if search_text.is_empty():
|
|
||||||
return positions
|
|
||||||
var start: int = soundtrack_name.to_lower().find(search_text.to_lower())
|
|
||||||
if start >= 0:
|
|
||||||
for i: int in search_text.length():
|
|
||||||
positions.append(start + i)
|
|
||||||
return positions
|
|
||||||
|
|
||||||
# Clean the name piece by piece, remembering which letter each cleaned letter came from
|
|
||||||
var cleaned: String = ""
|
|
||||||
var source: Array[int] = []
|
|
||||||
var i: int = 0
|
|
||||||
while i < soundtrack_name.length():
|
|
||||||
var piece: String
|
|
||||||
var length: int = 1
|
|
||||||
if soundtrack_name.substr(i, 2).to_lower() == "'n":
|
|
||||||
piece = "and"
|
|
||||||
length = 2
|
|
||||||
else:
|
|
||||||
piece = clean_term(soundtrack_name[i])
|
|
||||||
for letter: String in piece:
|
|
||||||
cleaned += letter
|
|
||||||
source.append(i)
|
|
||||||
i += length
|
|
||||||
|
|
||||||
var clean_search_text: String = clean_term(search_text)
|
|
||||||
var found: int = cleaned.find(clean_search_text)
|
|
||||||
var cleaned_positions: Array[int] = []
|
|
||||||
if found >= 0:
|
|
||||||
for j: int in clean_search_text.length():
|
|
||||||
cleaned_positions.append(found + j)
|
|
||||||
elif clean_search_text.length() >= Settings.LETTERS_FOR_LOOSE_SEARCH:
|
|
||||||
var at: int = 0
|
|
||||||
for letter: String in clean_search_text:
|
|
||||||
at = cleaned.find(letter, at)
|
|
||||||
if at < 0:
|
|
||||||
return positions
|
|
||||||
cleaned_positions.append(at)
|
|
||||||
at += 1
|
|
||||||
for cleaned_position: int in cleaned_positions:
|
|
||||||
if !positions.has(source[cleaned_position]):
|
|
||||||
positions.append(source[cleaned_position])
|
|
||||||
return positions
|
|
||||||
|
|
||||||
|
|
||||||
## Cleans a term by removing spaces and replacing special characters.
|
|
||||||
## Matches MusicServer's clean_term() function.
|
|
||||||
static func clean_term(term: String) -> String:
|
|
||||||
return term.replace(" ", "").replace("é", "e").replace("+", "plus").replace("&", "and") \
|
|
||||||
.replace("'n", "and").to_lower()
|
|
||||||
|
|
||||||
|
|
||||||
func _add_name(soundtrack: String, text: String) -> void:
|
|
||||||
_names.append({
|
|
||||||
"original": soundtrack,
|
|
||||||
"text": text,
|
|
||||||
"lowercase": text.to_lower(),
|
|
||||||
"cleaned": clean_term(text),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
# 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 = 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):
|
|
||||||
regex_text += "\\"
|
|
||||||
regex_text += letter + ".*"
|
|
||||||
regex.compile(regex_text)
|
|
||||||
_regex_cache[search_term] = regex
|
|
||||||
return _regex_cache[search_term]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://df8ox6xddobu2
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
class_name Updater
|
|
||||||
extends Node
|
|
||||||
## Updates the game in place to the newest release: downloads the new game next to the
|
|
||||||
## running one, checks it, swaps the files and starts the new version.
|
|
||||||
##
|
|
||||||
## The game is one file on Windows and Linux (the game data is embedded). A running .exe
|
|
||||||
## can't be overwritten on Windows, but it can be renamed, so it is moved aside to
|
|
||||||
## [code]<exe>.old[/code], which [method remove_old_files] deletes at the next start. On
|
|
||||||
## Linux the new file simply replaces the running one.
|
|
||||||
##
|
|
||||||
## [member executable_path], [member os_name], [member in_editor], [member start_process]
|
|
||||||
## and [member quit] can be replaced, so tests choose the platform and nothing restarts.
|
|
||||||
|
|
||||||
## Where the newest release can be downloaded by hand.
|
|
||||||
const RELEASES_PAGE: String = "https://gitea.sanplex.xyz/Sansan/MusicPlayer/releases/latest"
|
|
||||||
## Added to the game's path for the downloaded new version.
|
|
||||||
const UPDATE_SUFFIX: String = ".update"
|
|
||||||
## Added to the game's path for the old version, on Windows.
|
|
||||||
const OLD_SUFFIX: String = ".old"
|
|
||||||
## rwxr-xr-x, for the new game on Linux.
|
|
||||||
const EXECUTABLE_PERMISSIONS: int = 0b111_101_101
|
|
||||||
## Seconds the whole download may take.
|
|
||||||
const DOWNLOAD_TIMEOUT: float = 900.0
|
|
||||||
|
|
||||||
## The running game's file.
|
|
||||||
var executable_path: String = OS.get_executable_path()
|
|
||||||
## The platform, as [method OS.get_name] names it.
|
|
||||||
var os_name: String = OS.get_name()
|
|
||||||
## Whether the game runs from the Godot editor, where there is no game file to replace.
|
|
||||||
var in_editor: bool = OS.has_feature("editor")
|
|
||||||
## Starts a program: [code](path: String, arguments: PackedStringArray) -> int[/code].
|
|
||||||
var start_process: Callable = OS.create_process
|
|
||||||
## Quits the game.
|
|
||||||
var quit: Callable
|
|
||||||
|
|
||||||
var _request: HTTPRequest
|
|
||||||
var _on_progress: Callable
|
|
||||||
|
|
||||||
|
|
||||||
func _init() -> void:
|
|
||||||
quit = func() -> void: get_tree().quit()
|
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
|
||||||
if _request != null && _on_progress.is_valid():
|
|
||||||
_on_progress.call(_request.get_downloaded_bytes(), _request.get_body_size())
|
|
||||||
|
|
||||||
|
|
||||||
## The release file to download for [param os_name]: the game itself to update in place,
|
|
||||||
## or, when not [param in_place], what a person downloads, which on Linux is the package
|
|
||||||
## with an installer. Empty when there is no release for the platform.
|
|
||||||
static func asset_for(for_os: String, in_place: bool) -> String:
|
|
||||||
match for_os:
|
|
||||||
"Windows":
|
|
||||||
return "windows"
|
|
||||||
"Linux":
|
|
||||||
return "linux" if in_place else "linux-package"
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
## Why the game can't update itself here, or empty when it can.
|
|
||||||
func can_update_in_place() -> String:
|
|
||||||
if in_editor:
|
|
||||||
return "The game runs from the Godot editor."
|
|
||||||
if asset_for(os_name, true).is_empty():
|
|
||||||
return "The game updates itself on Windows and Linux."
|
|
||||||
var probe: String = executable_path + ".write-test"
|
|
||||||
var file: FileAccess = FileAccess.open(probe, FileAccess.WRITE)
|
|
||||||
if file == null:
|
|
||||||
return "The game's folder %s can't be written to." % executable_path.get_base_dir()
|
|
||||||
file.close()
|
|
||||||
DirAccess.remove_absolute(probe)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
## Where the new version is downloaded to, next to the game so it can be renamed into
|
|
||||||
## place.
|
|
||||||
func update_path() -> String:
|
|
||||||
return executable_path + UPDATE_SUFFIX
|
|
||||||
|
|
||||||
|
|
||||||
## Downloads the new game from [param url]. Calls [param on_progress] with the bytes
|
|
||||||
## downloaded and the total (-1 while unknown) every frame, then [param on_done] with the
|
|
||||||
## file's path once it is checked, or [param on_failed] with the reason.
|
|
||||||
func download(url: String, on_progress: Callable, on_done: Callable,
|
|
||||||
on_failed: Callable) -> void:
|
|
||||||
cancel()
|
|
||||||
_on_progress = on_progress
|
|
||||||
_request = HTTPRequest.new()
|
|
||||||
_request.download_file = update_path()
|
|
||||||
_request.timeout = DOWNLOAD_TIMEOUT
|
|
||||||
add_child(_request)
|
|
||||||
_request.request_completed.connect(_on_downloaded.bind(on_done, on_failed))
|
|
||||||
var error: Error = _request.request(url)
|
|
||||||
if error != OK:
|
|
||||||
cancel()
|
|
||||||
on_failed.call("The download could not start: " + error_string(error))
|
|
||||||
|
|
||||||
|
|
||||||
## Stops a download and deletes what was downloaded.
|
|
||||||
func cancel() -> void:
|
|
||||||
if _request != null:
|
|
||||||
_request.cancel_request()
|
|
||||||
_request.queue_free()
|
|
||||||
_request = null
|
|
||||||
DirAccess.remove_absolute(update_path())
|
|
||||||
|
|
||||||
|
|
||||||
## Whether [param path] looks like the game for [param for_os]: a Windows program starts
|
|
||||||
## with "MZ", a Linux one with the ELF header. An error page saved by mistake does not.
|
|
||||||
static func check_file(path: String, for_os: String) -> bool:
|
|
||||||
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
|
|
||||||
if file == null || file.get_length() < 4:
|
|
||||||
return false
|
|
||||||
var start: PackedByteArray = file.get_buffer(4)
|
|
||||||
match for_os:
|
|
||||||
"Windows":
|
|
||||||
return start.slice(0, 2) == "MZ".to_ascii_buffer()
|
|
||||||
"Linux":
|
|
||||||
return start == PackedByteArray([0x7F, 0x45, 0x4C, 0x46])
|
|
||||||
return false
|
|
||||||
|
|
||||||
|
|
||||||
## Puts the new game at [param update] in place of the game at [param exe]. On Windows
|
|
||||||
## the running game is moved aside first, and put back if the new one can't be moved in.
|
|
||||||
static func install(update: String, exe: String, for_os: String) -> Error:
|
|
||||||
if for_os == "Windows":
|
|
||||||
var old: String = exe + OLD_SUFFIX
|
|
||||||
if FileAccess.file_exists(old):
|
|
||||||
DirAccess.remove_absolute(old)
|
|
||||||
var error: Error = DirAccess.rename_absolute(exe, old)
|
|
||||||
if error != OK:
|
|
||||||
return error
|
|
||||||
error = DirAccess.rename_absolute(update, exe)
|
|
||||||
if error != OK:
|
|
||||||
DirAccess.rename_absolute(old, exe)
|
|
||||||
return error
|
|
||||||
FileAccess.set_unix_permissions(update, EXECUTABLE_PERMISSIONS)
|
|
||||||
return DirAccess.rename_absolute(update, exe)
|
|
||||||
|
|
||||||
|
|
||||||
## Deletes what an update left next to the game at [param exe]: the old version, and a
|
|
||||||
## download that was not finished.
|
|
||||||
static func remove_old_files(exe: String) -> void:
|
|
||||||
for suffix: String in [OLD_SUFFIX, UPDATE_SUFFIX]:
|
|
||||||
if FileAccess.file_exists(exe + suffix):
|
|
||||||
DirAccess.remove_absolute(exe + suffix)
|
|
||||||
|
|
||||||
|
|
||||||
## Starts the game again, now the new version, and quits this one.
|
|
||||||
func restart() -> void:
|
|
||||||
start_process.call(executable_path, OS.get_cmdline_args())
|
|
||||||
quit.call()
|
|
||||||
|
|
||||||
|
|
||||||
func _on_downloaded(result: int, response_code: int, _headers: PackedStringArray,
|
|
||||||
_body: PackedByteArray, on_done: Callable, on_failed: Callable) -> void:
|
|
||||||
var expected: int = _request.get_body_size()
|
|
||||||
_request.queue_free()
|
|
||||||
_request = null
|
|
||||||
var reason: String = ""
|
|
||||||
if result != HTTPRequest.RESULT_SUCCESS:
|
|
||||||
reason = "The download failed. Check the connection and try again."
|
|
||||||
elif response_code != 200:
|
|
||||||
reason = "The download failed: the server answered %d." % response_code
|
|
||||||
elif expected >= 0 && FileAccess.get_size(update_path()) != expected:
|
|
||||||
reason = "The download stopped before the end. Try again."
|
|
||||||
elif !check_file(update_path(), os_name):
|
|
||||||
reason = "The downloaded file is not the game for %s." % os_name
|
|
||||||
if !reason.is_empty():
|
|
||||||
DirAccess.remove_absolute(update_path())
|
|
||||||
on_failed.call(reason)
|
|
||||||
return
|
|
||||||
on_done.call(update_path())
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://bu3bi7qhxr7it
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
class_name MusicFolder
|
|
||||||
extends RefCounted
|
|
||||||
## A folder of songs to play instead of the MusicServer's, picked in the settings.
|
|
||||||
##
|
|
||||||
## A folder with subfolders is a soundtrack folder: each subfolder is a soundtrack, and
|
|
||||||
## songs are picked at random from a random soundtrack. Songs at the top are left out.
|
|
||||||
## A folder with songs and no subfolders is a prepared quiz: every song plays once, in
|
|
||||||
## order or at random, and only the song's name is the answer.
|
|
||||||
##
|
|
||||||
## Hidden files and folders, and files the game can't play (see [AudioFile]), are left
|
|
||||||
## out. A subfolder called [code]characters[/code] holds the quiz's own characters
|
|
||||||
## instead of songs, see [member characters_path].
|
|
||||||
|
|
||||||
enum Kind { EMPTY, SOUNDTRACKS, PREPARED_QUIZ }
|
|
||||||
enum PlayOrder { IN_ORDER, RANDOM }
|
|
||||||
|
|
||||||
## How many files are tried before giving up when files can't be read.
|
|
||||||
const MAX_TRIES: int = 20
|
|
||||||
## The subfolder with the quiz's own characters. Any case, e.g. "Characters", works.
|
|
||||||
const CHARACTERS_FOLDER: String = "characters"
|
|
||||||
|
|
||||||
## The folder's path.
|
|
||||||
var path: String = ""
|
|
||||||
var kind: Kind = Kind.EMPTY
|
|
||||||
## How a prepared quiz is played. Change it with [method set_play_order].
|
|
||||||
var play_order: PlayOrder = PlayOrder.IN_ORDER
|
|
||||||
## The folder's [constant CHARACTERS_FOLDER] subfolder when it has pictures, so a themed
|
|
||||||
## quiz can bring its own characters. Empty when there is none.
|
|
||||||
var characters_path: String = ""
|
|
||||||
|
|
||||||
# Soundtrack folder: soundtrack name -> paths of its songs
|
|
||||||
var _soundtracks: Dictionary[String, PackedStringArray] = {}
|
|
||||||
# Prepared quiz: the songs in order, the order they are played in and how many are played
|
|
||||||
var _quiz_songs: PackedStringArray = []
|
|
||||||
var _quiz_queue: PackedStringArray = []
|
|
||||||
var _quiz_next: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
## Reads the folder at [param folder_path]. Its [member kind] is [constant Kind.EMPTY]
|
|
||||||
## when there is nothing to play in it.
|
|
||||||
static func scan(folder_path: String) -> MusicFolder:
|
|
||||||
var folder: MusicFolder = MusicFolder.new()
|
|
||||||
folder.path = folder_path.trim_suffix("/")
|
|
||||||
var subfolders: PackedStringArray = []
|
|
||||||
for subfolder: String in _visible(DirAccess.get_directories_at(folder.path)):
|
|
||||||
if subfolder.to_lower() == CHARACTERS_FOLDER:
|
|
||||||
var characters: String = folder.path.path_join(subfolder)
|
|
||||||
if !FolderCharacters.picture_names(characters).is_empty():
|
|
||||||
folder.characters_path = characters
|
|
||||||
else:
|
|
||||||
subfolders.append(subfolder)
|
|
||||||
if !subfolders.is_empty():
|
|
||||||
for subfolder: String in subfolders:
|
|
||||||
var songs: PackedStringArray = _songs_in(folder.path.path_join(subfolder), true)
|
|
||||||
if !songs.is_empty():
|
|
||||||
folder._soundtracks[subfolder] = songs
|
|
||||||
if !folder._soundtracks.is_empty():
|
|
||||||
folder.kind = Kind.SOUNDTRACKS
|
|
||||||
else:
|
|
||||||
folder._quiz_songs = _songs_in(folder.path, false)
|
|
||||||
if !folder._quiz_songs.is_empty():
|
|
||||||
folder.kind = Kind.PREPARED_QUIZ
|
|
||||||
folder.restart()
|
|
||||||
return folder
|
|
||||||
|
|
||||||
|
|
||||||
## The folder's name, to show to the user.
|
|
||||||
func display_name() -> String:
|
|
||||||
return path.get_file()
|
|
||||||
|
|
||||||
|
|
||||||
## The names of the soundtracks, sorted. Empty for a prepared quiz.
|
|
||||||
func soundtrack_names() -> Array[String]:
|
|
||||||
var names: Array[String] = []
|
|
||||||
names.assign(_soundtracks.keys())
|
|
||||||
names.sort_custom(func(a: String, b: String) -> bool: return a.naturalnocasecmp_to(b) < 0)
|
|
||||||
return names
|
|
||||||
|
|
||||||
|
|
||||||
## How many songs a prepared quiz has.
|
|
||||||
func quiz_size() -> int:
|
|
||||||
return _quiz_songs.size()
|
|
||||||
|
|
||||||
|
|
||||||
## Plays a prepared quiz in [param order] from its first song.
|
|
||||||
func set_play_order(order: PlayOrder) -> void:
|
|
||||||
play_order = order
|
|
||||||
restart()
|
|
||||||
|
|
||||||
|
|
||||||
## Starts a prepared quiz over from its first song, shuffled again if it is played at
|
|
||||||
## random.
|
|
||||||
func restart() -> void:
|
|
||||||
_quiz_queue = _quiz_songs.duplicate()
|
|
||||||
if play_order == PlayOrder.RANDOM:
|
|
||||||
var shuffled: Array = Array(_quiz_queue)
|
|
||||||
shuffled.shuffle()
|
|
||||||
_quiz_queue = PackedStringArray(shuffled)
|
|
||||||
_quiz_next = 0
|
|
||||||
|
|
||||||
|
|
||||||
## How far a prepared quiz has come, to continue it after a restart with
|
|
||||||
## [method restore_progress].
|
|
||||||
func progress() -> Dictionary:
|
|
||||||
return {"play_order": play_order, "queue": Array(_quiz_queue), "next": _quiz_next}
|
|
||||||
|
|
||||||
|
|
||||||
## Continues a prepared quiz from [param saved], as given by [method progress]. Songs
|
|
||||||
## that are no longer in the folder are left out. Returns false, changing nothing, when
|
|
||||||
## [param saved] does not fit this folder.
|
|
||||||
func restore_progress(saved: Dictionary) -> bool:
|
|
||||||
if kind != Kind.PREPARED_QUIZ || !(saved.get("queue") is Array) \
|
|
||||||
|| !(saved.get("next") is float || saved.get("next") is int):
|
|
||||||
return false
|
|
||||||
var queue: PackedStringArray = []
|
|
||||||
var next: int = int(saved.next)
|
|
||||||
var known: Dictionary = {}
|
|
||||||
for song_path: String in _quiz_songs:
|
|
||||||
known[song_path] = true
|
|
||||||
for i: int in saved.queue.size():
|
|
||||||
if known.has(str(saved.queue[i])):
|
|
||||||
queue.append(str(saved.queue[i]))
|
|
||||||
elif i < int(saved.next):
|
|
||||||
next -= 1
|
|
||||||
play_order = int(saved.get("play_order", play_order)) as PlayOrder
|
|
||||||
_quiz_queue = queue
|
|
||||||
_quiz_next = clampi(next, 0, queue.size())
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
## The next song to play, or null when a prepared quiz has played every song or no file
|
|
||||||
## can be read.
|
|
||||||
func next_song() -> SongObject:
|
|
||||||
for i: int in MAX_TRIES:
|
|
||||||
var song_path: String = _pick()
|
|
||||||
if song_path.is_empty():
|
|
||||||
return null
|
|
||||||
if AudioFile.load(song_path) != null:
|
|
||||||
return _song_object(song_path)
|
|
||||||
GameLog.warning("Skipping a song that can't be played: ", song_path)
|
|
||||||
return null
|
|
||||||
|
|
||||||
|
|
||||||
func _pick() -> String:
|
|
||||||
if kind == Kind.SOUNDTRACKS:
|
|
||||||
var songs: PackedStringArray = _soundtracks[_soundtracks.keys().pick_random()]
|
|
||||||
return songs[randi() % songs.size()]
|
|
||||||
if _quiz_next >= _quiz_queue.size():
|
|
||||||
return ""
|
|
||||||
_quiz_next += 1
|
|
||||||
return _quiz_queue[_quiz_next - 1]
|
|
||||||
|
|
||||||
|
|
||||||
func _song_object(song_path: String) -> SongObject:
|
|
||||||
var song_object: SongObject = SongObject.new()
|
|
||||||
song_object.song_path = song_path
|
|
||||||
song_object.song_title = song_path.get_file().get_basename()
|
|
||||||
if kind == Kind.SOUNDTRACKS:
|
|
||||||
# The soundtrack is the subfolder right under the music folder
|
|
||||||
song_object.soundtrack_title = song_path.trim_prefix(path + "/").get_slice("/", 0)
|
|
||||||
return song_object
|
|
||||||
|
|
||||||
|
|
||||||
# The playable songs in [param dir], in natural order, also in subfolders if [param deep]
|
|
||||||
static func _songs_in(dir: String, deep: bool) -> PackedStringArray:
|
|
||||||
var songs: Array[String] = []
|
|
||||||
for file_name: String in _visible(DirAccess.get_files_at(dir)):
|
|
||||||
if AudioFile.is_playable(file_name):
|
|
||||||
songs.append(dir.path_join(file_name))
|
|
||||||
songs.sort_custom(func(a: String, b: String) -> bool:
|
|
||||||
return a.get_file().naturalnocasecmp_to(b.get_file()) < 0)
|
|
||||||
if deep:
|
|
||||||
for subfolder: String in _visible(DirAccess.get_directories_at(dir)):
|
|
||||||
songs.append_array(_songs_in(dir.path_join(subfolder), true))
|
|
||||||
return PackedStringArray(songs)
|
|
||||||
|
|
||||||
|
|
||||||
static func _visible(names: PackedStringArray) -> PackedStringArray:
|
|
||||||
var shown: PackedStringArray = []
|
|
||||||
for entry: String in names:
|
|
||||||
if !entry.begins_with("."):
|
|
||||||
shown.append(entry)
|
|
||||||
return shown
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://bcrem1inl47ve
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
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_triggered
|
|
||||||
signal match_point_triggered
|
|
||||||
signal winner_triggered
|
|
||||||
signal point_given_sound
|
|
||||||
signal player_point_given
|
|
||||||
signal player_point_taken
|
|
||||||
|
|
||||||
## 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)
|
|
||||||
## The name of the picked character, its picture's file name, or empty when none is
|
|
||||||
## picked. Saved with the quiz, so the picture can be found again after a restart.
|
|
||||||
var character_name: String = ""
|
|
||||||
|
|
||||||
|
|
||||||
func _init(new_name: String) -> void:
|
|
||||||
player_name = new_name
|
|
||||||
player_score = 0
|
|
||||||
|
|
||||||
|
|
||||||
## Shows [param texture], the character called [param new_name], for the player.
|
|
||||||
func set_character(texture: Texture, new_name: String) -> void:
|
|
||||||
character = texture
|
|
||||||
character_name = new_name
|
|
||||||
|
|
||||||
|
|
||||||
## Goes back to no character, e.g. when the characters come from somewhere else.
|
|
||||||
func clear_character() -> void:
|
|
||||||
character = load(NO_CHARACTER_PATH)
|
|
||||||
character_name = ""
|
|
||||||
|
|
||||||
|
|
||||||
## The player as a Dictionary that can be written as JSON, for [QuizSave].
|
|
||||||
func to_dict() -> Dictionary:
|
|
||||||
return {
|
|
||||||
"id": id, "name": player_name, "score": player_score,
|
|
||||||
"is_first_point": is_first_point, "character": character_name,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
## A player made from [param data], as given by [method to_dict], or null when
|
|
||||||
## [param data] is not a player. The character's picture is not loaded here.
|
|
||||||
static func from_dict(data: Variant) -> PlayerObject:
|
|
||||||
if !(data is Dictionary) || !(data.get("name") is String) || !(data.get("id") is String):
|
|
||||||
return null
|
|
||||||
var player: PlayerObject = PlayerObject.new(data.name)
|
|
||||||
player.id = data.id
|
|
||||||
player.player_score = maxi(int(data.get("score", 0)), 0)
|
|
||||||
player.is_first_point = bool(data.get("is_first_point", player.player_score == 0))
|
|
||||||
player.character_name = str(data.get("character", ""))
|
|
||||||
return player
|
|
||||||
|
|
||||||
|
|
||||||
## 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)]
|
|
||||||
|
|
||||||
|
|
||||||
## Gives the player a point. With a winning score, the point that reaches it wins; a
|
|
||||||
## prepared quiz has none, so points just add up until every song has played.
|
|
||||||
func add_point() -> void:
|
|
||||||
var has_winning_score: bool = !Settings.is_prepared_quiz()
|
|
||||||
if has_winning_score && player_score >= Settings.winning_score:
|
|
||||||
return
|
|
||||||
player_score += 1
|
|
||||||
player_point_given.emit()
|
|
||||||
var first_point: bool = player_score == 1 && is_first_point
|
|
||||||
if player_score == 1:
|
|
||||||
is_first_point = false
|
|
||||||
# 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 has_winning_score && player_score == Settings.winning_score:
|
|
||||||
winner_triggered.emit()
|
|
||||||
elif has_winning_score && player_score == Settings.winning_score - 1:
|
|
||||||
match_point_triggered.emit()
|
|
||||||
elif first_point:
|
|
||||||
first_point_triggered.emit()
|
|
||||||
else:
|
|
||||||
point_given_sound.emit()
|
|
||||||
|
|
||||||
|
|
||||||
func minus_point() -> void:
|
|
||||||
if player_score > 0:
|
|
||||||
player_score -= 1
|
|
||||||
player_point_taken.emit()
|
|
||||||
if player_score == 0:
|
|
||||||
is_first_point = true
|
|
||||||
|
|
||||||
|
|
||||||
func reset_points() -> void:
|
|
||||||
player_score = 0
|
|
||||||
is_first_point = true
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
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.
|
|
||||||
##
|
|
||||||
## Downloaded songs are kept on disk in the [code]songs[/code] folder next to the
|
|
||||||
## settings, not in memory. The folder only holds the current playlist: when the game
|
|
||||||
## starts, songs the saved quiz does not use are deleted (see [QuizSave]), and the folder
|
|
||||||
## is emptied when the playlist is reset.
|
|
||||||
|
|
||||||
const SONGS_FOLDER_NAME: String = "songs"
|
|
||||||
|
|
||||||
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
|
|
||||||
## The first song of the current game. Songs before it belong to earlier games, which
|
|
||||||
## ended with Reset points.
|
|
||||||
var game_start_song: int = 0
|
|
||||||
|
|
||||||
# How many song files have been written, so each gets a name of its own
|
|
||||||
var _song_files_written: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
## The folder downloaded songs are kept in.
|
|
||||||
static func songs_folder() -> String:
|
|
||||||
return Settings.config_dir().path_join(SONGS_FOLDER_NAME)
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
|
||||||
if !song.has_played:
|
|
||||||
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():
|
|
||||||
return
|
|
||||||
GameLog.debug("show_answer_pressed song_object: ", song_object_array[song_no]._to_string())
|
|
||||||
song_object_array[song_no].is_answered = true
|
|
||||||
if Settings.add_to_stats && !Settings.is_offline():
|
|
||||||
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
|
|
||||||
else:
|
|
||||||
var temp_list: Array[SongObject] = []
|
|
||||||
for s: SongObject in song_object_array:
|
|
||||||
if s.has_played:
|
|
||||||
temp_list.append(s)
|
|
||||||
return temp_list
|
|
||||||
|
|
||||||
|
|
||||||
func reset_playlist() -> void:
|
|
||||||
currently_playing_song = -1
|
|
||||||
latest_played_song = 0
|
|
||||||
game_start_song = 0
|
|
||||||
clear_song_list()
|
|
||||||
clear_song_files()
|
|
||||||
|
|
||||||
|
|
||||||
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_soundtrack_uuid() -> String:
|
|
||||||
return song_object_array[currently_playing_song].get_soundtrack_uuid()
|
|
||||||
|
|
||||||
|
|
||||||
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 every player's points from every song, for a new game.
|
|
||||||
func clear_points() -> void:
|
|
||||||
for song_object: SongObject in song_object_array:
|
|
||||||
song_object.players_given_point.clear()
|
|
||||||
|
|
||||||
|
|
||||||
## 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)
|
|
||||||
|
|
||||||
|
|
||||||
## The song of [param song_no], read from its file. Returns null if the file is gone, so
|
|
||||||
## the song can be downloaded again.
|
|
||||||
func get_song(song_no: int) -> AudioStream:
|
|
||||||
var song_object: SongObject = song_object_array[song_no]
|
|
||||||
if song_object.song_path.is_empty():
|
|
||||||
return song_object.song
|
|
||||||
return AudioFile.load(song_object.song_path)
|
|
||||||
|
|
||||||
|
|
||||||
func add_song_to_song_object(song_no: int, mp3: PackedByteArray) -> void:
|
|
||||||
store_song(song_object_array[song_no], mp3)
|
|
||||||
|
|
||||||
|
|
||||||
## Keeps a downloaded song in a file in [method songs_folder]. If the file can't be
|
|
||||||
## written, the song is kept in memory instead.
|
|
||||||
func store_song(song_object: SongObject, mp3: PackedByteArray) -> void:
|
|
||||||
DirAccess.make_dir_recursive_absolute(songs_folder())
|
|
||||||
# Songs kept from a saved quiz already have files, which must not be written over
|
|
||||||
var path: String = songs_folder().path_join("song_%d.mp3" % _song_files_written)
|
|
||||||
while FileAccess.file_exists(path):
|
|
||||||
_song_files_written += 1
|
|
||||||
path = songs_folder().path_join("song_%d.mp3" % _song_files_written)
|
|
||||||
_song_files_written += 1
|
|
||||||
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
|
|
||||||
if file == null:
|
|
||||||
GameLog.warning("Could not save a song to ", path, ", keeping it in memory: ",
|
|
||||||
error_string(FileAccess.get_open_error()))
|
|
||||||
var sound: AudioStreamMP3 = AudioStreamMP3.new()
|
|
||||||
sound.data = mp3
|
|
||||||
song_object.song = sound
|
|
||||||
song_object.song_path = ""
|
|
||||||
return
|
|
||||||
file.store_buffer(mp3)
|
|
||||||
file.close()
|
|
||||||
song_object.song = null
|
|
||||||
song_object.song_path = path
|
|
||||||
|
|
||||||
|
|
||||||
## Deletes the files of the songs downloaded so far, except those in [param keep].
|
|
||||||
## The downloaded song files the playlist uses, which must be kept while it does.
|
|
||||||
func song_files_in_use() -> PackedStringArray:
|
|
||||||
var files: PackedStringArray = []
|
|
||||||
for song_object: SongObject in song_object_array:
|
|
||||||
if !song_object.song_path.is_empty():
|
|
||||||
files.append(song_object.song_path)
|
|
||||||
return files
|
|
||||||
|
|
||||||
|
|
||||||
func clear_song_files(keep: PackedStringArray = []) -> void:
|
|
||||||
if !DirAccess.dir_exists_absolute(songs_folder()):
|
|
||||||
return
|
|
||||||
for file_name: String in DirAccess.get_files_at(songs_folder()):
|
|
||||||
var path: String = songs_folder().path_join(file_name)
|
|
||||||
if !keep.has(path):
|
|
||||||
DirAccess.remove_absolute(path)
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
class_name QuizSave
|
|
||||||
extends RefCounted
|
|
||||||
## The quiz going on, saved to a file after every change, so the game goes on where it
|
|
||||||
## was when it is started again: the players with their points and characters, and every
|
|
||||||
## song with who got a point for it and whether it was played, answered or is playing.
|
|
||||||
##
|
|
||||||
## Songs downloaded ahead keep their files, so they are not downloaded again. A new game
|
|
||||||
## (Reset points, a sync or another server) is saved like any other change, so the file
|
|
||||||
## always holds the quiz as it is now.
|
|
||||||
##
|
|
||||||
## Kept in [code]current_quiz.json[/code] next to the settings, or
|
|
||||||
## [code]current_quiz_debug.json[/code] in debug mode, like the saved settings.
|
|
||||||
|
|
||||||
const FILE_NAME: String = "current_quiz.json"
|
|
||||||
const DEBUG_FILE_NAME: String = "current_quiz_debug.json"
|
|
||||||
## Files with another version are from a game that saved differently, and are ignored.
|
|
||||||
const VERSION: int = 1
|
|
||||||
|
|
||||||
var players: Array[PlayerObject] = []
|
|
||||||
var songs: Array[SongObject] = []
|
|
||||||
var currently_playing_song: int = -1
|
|
||||||
var latest_played_song: int = 0
|
|
||||||
var game_start_song: int = 0
|
|
||||||
var first_song_played: bool = false
|
|
||||||
## Whether statistics were turned off because a player won, see [code]Quiz[/code].
|
|
||||||
var stats_paused_by_win: bool = false
|
|
||||||
## The server the songs came from. Empty when they came from a music folder.
|
|
||||||
var server: String = ""
|
|
||||||
## The music folder the songs came from, or empty when they came from the server.
|
|
||||||
var music_folder_path: String = ""
|
|
||||||
## How far a prepared quiz in [member music_folder_path] has come, see
|
|
||||||
## [method MusicFolder.progress].
|
|
||||||
var music_folder_progress: Dictionary = {}
|
|
||||||
## Where the characters came from, see [method Settings.characters_dir].
|
|
||||||
var characters_dir: String = ""
|
|
||||||
|
|
||||||
|
|
||||||
## Where the quiz is saved.
|
|
||||||
static func path() -> String:
|
|
||||||
return Settings.config_dir().path_join(DEBUG_FILE_NAME if Settings.is_debug else FILE_NAME)
|
|
||||||
|
|
||||||
|
|
||||||
## The quiz as it is now, in [code]Settings[/code] and [code]Playlist[/code].
|
|
||||||
static func capture(paused_by_win: bool) -> QuizSave:
|
|
||||||
var save: QuizSave = QuizSave.new()
|
|
||||||
save.players = Settings.player_array.duplicate()
|
|
||||||
save.songs = Playlist.song_object_array.duplicate()
|
|
||||||
save.currently_playing_song = Playlist.currently_playing_song
|
|
||||||
save.latest_played_song = Playlist.latest_played_song
|
|
||||||
save.game_start_song = Playlist.game_start_song
|
|
||||||
save.first_song_played = Playlist.first_song_played
|
|
||||||
save.stats_paused_by_win = paused_by_win
|
|
||||||
if Settings.is_offline():
|
|
||||||
save.music_folder_path = Settings.music_folder.path
|
|
||||||
save.music_folder_progress = Settings.music_folder.progress()
|
|
||||||
else:
|
|
||||||
save.server = Settings.default_path
|
|
||||||
save.characters_dir = Settings.characters_dir()
|
|
||||||
return save
|
|
||||||
|
|
||||||
|
|
||||||
## Reads the saved quiz, or returns null when there is none or it can't be read.
|
|
||||||
static func read() -> QuizSave:
|
|
||||||
if !FileAccess.file_exists(path()):
|
|
||||||
return null
|
|
||||||
var save: QuizSave = from_dict(JSON.parse_string(FileAccess.get_file_as_string(path())))
|
|
||||||
if save == null:
|
|
||||||
GameLog.warning("Could not read the saved quiz in ", path(), ", starting a new one")
|
|
||||||
return save
|
|
||||||
|
|
||||||
|
|
||||||
## Deletes the saved quiz.
|
|
||||||
static func delete() -> void:
|
|
||||||
if FileAccess.file_exists(path()):
|
|
||||||
DirAccess.remove_absolute(path())
|
|
||||||
|
|
||||||
|
|
||||||
## Saves the quiz. It is written to another file first and then moved, so a crash while
|
|
||||||
## saving never leaves half a file. Returns false when it could not be saved.
|
|
||||||
func write() -> bool:
|
|
||||||
var target: String = path()
|
|
||||||
var temporary: String = target + ".tmp"
|
|
||||||
DirAccess.make_dir_recursive_absolute(target.get_base_dir())
|
|
||||||
var file: FileAccess = FileAccess.open(temporary, FileAccess.WRITE)
|
|
||||||
if file == null:
|
|
||||||
GameLog.warning("Could not save the quiz to ", temporary, ": ",
|
|
||||||
error_string(FileAccess.get_open_error()))
|
|
||||||
return false
|
|
||||||
file.store_string(JSON.stringify(to_dict(), "\t"))
|
|
||||||
file.close()
|
|
||||||
var error: Error = DirAccess.rename_absolute(temporary, target)
|
|
||||||
if error != OK:
|
|
||||||
GameLog.warning("Could not save the quiz to ", target, ": ", error_string(error))
|
|
||||||
return false
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
## The song files the saved songs use, which must not be deleted.
|
|
||||||
func song_files() -> PackedStringArray:
|
|
||||||
var files: PackedStringArray = []
|
|
||||||
for song_object: SongObject in songs:
|
|
||||||
if !song_object.song_path.is_empty():
|
|
||||||
files.append(song_object.song_path)
|
|
||||||
return files
|
|
||||||
|
|
||||||
|
|
||||||
func to_dict() -> Dictionary:
|
|
||||||
var player_list: Array[Dictionary] = []
|
|
||||||
for player: PlayerObject in players:
|
|
||||||
player_list.append(player.to_dict())
|
|
||||||
var song_list: Array[Dictionary] = []
|
|
||||||
for song_object: SongObject in songs:
|
|
||||||
song_list.append(song_object.to_dict())
|
|
||||||
return {
|
|
||||||
"version": VERSION,
|
|
||||||
"players": player_list,
|
|
||||||
"songs": song_list,
|
|
||||||
"currently_playing_song": currently_playing_song,
|
|
||||||
"latest_played_song": latest_played_song,
|
|
||||||
"game_start_song": game_start_song,
|
|
||||||
"first_song_played": first_song_played,
|
|
||||||
"stats_paused_by_win": stats_paused_by_win,
|
|
||||||
"server": server,
|
|
||||||
"music_folder": {"path": music_folder_path, "progress": music_folder_progress},
|
|
||||||
"characters_dir": characters_dir,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
## A save made from [param data], as given by [method to_dict], or null when
|
|
||||||
## [param data] is not a saved quiz of this [constant VERSION]. Players and songs that
|
|
||||||
## can't be read are left out.
|
|
||||||
static func from_dict(data: Variant) -> QuizSave:
|
|
||||||
if !(data is Dictionary) || int(data.get("version", 0)) != VERSION \
|
|
||||||
|| !(data.get("players") is Array) || !(data.get("songs") is Array):
|
|
||||||
return null
|
|
||||||
var save: QuizSave = QuizSave.new()
|
|
||||||
for player_data: Variant in data.players:
|
|
||||||
var player: PlayerObject = PlayerObject.from_dict(player_data)
|
|
||||||
if player != null && save.players.size() < Settings.MAX_NUMBER_OF_PLAYERS:
|
|
||||||
save.players.append(player)
|
|
||||||
for song_data: Variant in data.songs:
|
|
||||||
var song_object: SongObject = SongObject.from_dict(song_data)
|
|
||||||
if song_object == null:
|
|
||||||
# The songs' places in the list are their numbers, so none can be left out
|
|
||||||
save.songs.clear()
|
|
||||||
break
|
|
||||||
save.songs.append(song_object)
|
|
||||||
save.currently_playing_song = clampi(int(data.get("currently_playing_song", -1)), -1,
|
|
||||||
save.songs.size() - 1)
|
|
||||||
save.latest_played_song = clampi(int(data.get("latest_played_song", 0)), 0,
|
|
||||||
maxi(save.songs.size() - 1, 0))
|
|
||||||
save.game_start_song = clampi(int(data.get("game_start_song", 0)), 0, save.songs.size())
|
|
||||||
save.first_song_played = bool(data.get("first_song_played", false))
|
|
||||||
save.stats_paused_by_win = bool(data.get("stats_paused_by_win", false))
|
|
||||||
save.server = str(data.get("server", ""))
|
|
||||||
var folder: Variant = data.get("music_folder", {})
|
|
||||||
if folder is Dictionary:
|
|
||||||
save.music_folder_path = str(folder.get("path", ""))
|
|
||||||
if folder.get("progress") is Dictionary:
|
|
||||||
save.music_folder_progress = folder.progress
|
|
||||||
save.characters_dir = str(data.get("characters_dir", ""))
|
|
||||||
return save
|
|
||||||
|
|
||||||
|
|
||||||
## Forgets the songs, e.g. when the server's playlist no longer has them. The players are
|
|
||||||
## kept, without points.
|
|
||||||
func drop_songs() -> void:
|
|
||||||
songs.clear()
|
|
||||||
currently_playing_song = -1
|
|
||||||
latest_played_song = 0
|
|
||||||
game_start_song = 0
|
|
||||||
first_song_played = false
|
|
||||||
for player: PlayerObject in players:
|
|
||||||
player.reset_points()
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://cfabuhf854ukp
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
class_name ScoreHistory
|
|
||||||
extends RefCounted
|
|
||||||
## Who got a point on which song of the current game, for the graph and the summary on
|
|
||||||
## the Winner popup.
|
|
||||||
##
|
|
||||||
## Songs are counted from 1 at the start of the game, which is the start of the playlist
|
|
||||||
## or the first song after the last Reset points.
|
|
||||||
|
|
||||||
## The players, in the order of [code]Settings.player_array[/code].
|
|
||||||
var players: Array[PlayerObject] = []
|
|
||||||
## For each song of the game, in the order they were played, the ids of the players who
|
|
||||||
## got a point on it.
|
|
||||||
var songs: Array[Array] = []
|
|
||||||
|
|
||||||
|
|
||||||
## The current game, from [code]Playlist.game_start_song[/code] up to the last song that
|
|
||||||
## has played. Songs downloaded ahead but not played yet are left out, and so are points
|
|
||||||
## of players who are no longer in [param game_players].
|
|
||||||
static func from_playlist(game_players: Array[PlayerObject]) -> ScoreHistory:
|
|
||||||
var song_scorers: Array[Array] = []
|
|
||||||
# Not just the song playing now: an older song may have been picked in the music list
|
|
||||||
var last_song: int = mini(Playlist.currently_playing_song,
|
|
||||||
Playlist.song_object_array.size() - 1)
|
|
||||||
for song_no: int in Playlist.song_object_array.size():
|
|
||||||
if Playlist.song_object_array[song_no].has_played:
|
|
||||||
last_song = maxi(last_song, song_no)
|
|
||||||
for song_no: int in range(Playlist.game_start_song, last_song + 1):
|
|
||||||
var song: SongObject = Playlist.song_object_array[song_no]
|
|
||||||
if song.has_played || song_no == Playlist.currently_playing_song:
|
|
||||||
song_scorers.append(song.players_given_point)
|
|
||||||
return from_songs(game_players, song_scorers)
|
|
||||||
|
|
||||||
|
|
||||||
## A history of [param song_scorers], the ids of the players who scored on each song.
|
|
||||||
static func from_songs(game_players: Array[PlayerObject], song_scorers: Array) -> ScoreHistory:
|
|
||||||
var history: ScoreHistory = ScoreHistory.new()
|
|
||||||
history.players = game_players.duplicate()
|
|
||||||
var ids: Array[String] = []
|
|
||||||
for player: PlayerObject in game_players:
|
|
||||||
ids.append(player.id)
|
|
||||||
for scorers: Array in song_scorers:
|
|
||||||
history.songs.append(scorers.filter(func(id: String) -> bool: return ids.has(id)))
|
|
||||||
return history
|
|
||||||
|
|
||||||
|
|
||||||
## [param player_id]'s score before the first song and after each song.
|
|
||||||
func scores_after_songs(player_id: String) -> Array[int]:
|
|
||||||
var scores: Array[int] = [0]
|
|
||||||
for scorers: Array in songs:
|
|
||||||
scores.append(scores.back() + (1 if scorers.has(player_id) else 0))
|
|
||||||
return scores
|
|
||||||
|
|
||||||
|
|
||||||
## [param player_id]'s score after the last song.
|
|
||||||
func final_score(player_id: String) -> int:
|
|
||||||
return scores_after_songs(player_id).back()
|
|
||||||
|
|
||||||
|
|
||||||
## The player with the most songs in a row with a point, e.g.
|
|
||||||
## "Crono, 5 songs in a row (14–18)". The earliest streak wins a tie.
|
|
||||||
func longest_streak_text() -> String:
|
|
||||||
if songs.is_empty():
|
|
||||||
return "No songs played"
|
|
||||||
var best_length: int = 0
|
|
||||||
var best_start: int = 0
|
|
||||||
var best_player: PlayerObject = null
|
|
||||||
for player: PlayerObject in players:
|
|
||||||
var length: int = 0
|
|
||||||
for song: int in songs.size():
|
|
||||||
length = length + 1 if songs[song].has(player.id) else 0
|
|
||||||
var start: int = song - length + 1
|
|
||||||
if length > best_length || (length == best_length && length > 0 \
|
|
||||||
&& start < best_start):
|
|
||||||
best_length = length
|
|
||||||
best_start = start
|
|
||||||
best_player = player
|
|
||||||
if best_length < 2:
|
|
||||||
return "Nobody scored twice in a row"
|
|
||||||
return "%s, %d songs in a row (%d–%d)" % [best_player.player_name, best_length,
|
|
||||||
best_start + 1, best_start + best_length]
|
|
||||||
|
|
||||||
|
|
||||||
## The most songs in a row where nobody scored, e.g. "Songs 6–10, nobody scored".
|
|
||||||
func longest_standstill_text() -> String:
|
|
||||||
if songs.is_empty():
|
|
||||||
return "No songs played"
|
|
||||||
var best_length: int = 0
|
|
||||||
var best_start: int = 0
|
|
||||||
var length: int = 0
|
|
||||||
for song: int in songs.size():
|
|
||||||
length = length + 1 if songs[song].is_empty() else 0
|
|
||||||
if length > best_length:
|
|
||||||
best_length = length
|
|
||||||
best_start = song - length + 1
|
|
||||||
if best_length == 0:
|
|
||||||
return "Someone scored on every song"
|
|
||||||
if best_length == 1:
|
|
||||||
return "Song %d, nobody scored" % (best_start + 1)
|
|
||||||
return "Songs %d–%d, nobody scored" % [best_start + 1, best_start + best_length]
|
|
||||||
|
|
||||||
|
|
||||||
## The song most players scored on, e.g. "Song 11, Marle and Lucca", or
|
|
||||||
## "Song 11, 3 players" when more than two did. The earliest song wins a tie.
|
|
||||||
func most_shared_text() -> String:
|
|
||||||
if songs.is_empty():
|
|
||||||
return "No songs played"
|
|
||||||
var best_song: int = -1
|
|
||||||
for song: int in songs.size():
|
|
||||||
if songs[song].size() >= 2 \
|
|
||||||
&& (best_song < 0 || songs[song].size() > songs[best_song].size()):
|
|
||||||
best_song = song
|
|
||||||
if best_song < 0:
|
|
||||||
return "No song had two scorers"
|
|
||||||
var scorers: Array = songs[best_song]
|
|
||||||
if scorers.size() > 2:
|
|
||||||
return "Song %d, %d players" % [best_song + 1, scorers.size()]
|
|
||||||
var names: PackedStringArray = []
|
|
||||||
for player: PlayerObject in players:
|
|
||||||
if scorers.has(player.id):
|
|
||||||
names.append(player.player_name)
|
|
||||||
return "Song %d, %s" % [best_song + 1, " and ".join(names)]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://dcvwgwhlu80qh
|
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
extends Node
|
|
||||||
## The game settings and the players, shared by the whole game.
|
|
||||||
##
|
|
||||||
## Registered as the [code]Settings[/code] autoload.
|
|
||||||
|
|
||||||
## Emitted when another theme is used, for code that reads theme colours itself.
|
|
||||||
signal theme_changed
|
|
||||||
|
|
||||||
## How the server picks a random song: any soundtrack then any of its songs, preferring
|
|
||||||
## soundtracks played less than average, or every song with the same chance.
|
|
||||||
enum RandomMode { NORMAL, LOW_PLAYED, CLASSIC }
|
|
||||||
|
|
||||||
const MAX_NUMBER_OF_PLAYERS: int = 7
|
|
||||||
## How many letters a search needs before it also finds names with the letters in order
|
|
||||||
## but apart, e.g. "lttp" for "A Link to the Past". Shorter searches only find names
|
|
||||||
## that contain the text, since a letter or two in order matches most of the library.
|
|
||||||
const LETTERS_FOR_LOOSE_SEARCH: int = 3
|
|
||||||
## The MusicServers that can always be picked in the settings.
|
|
||||||
const SERVERS: Array[String] = [
|
|
||||||
"https://music.sanplex.xyz", "https://tmusic.sanplex.xyz", "https://rmusic.sanplex.xyz",
|
|
||||||
"https://old-music.sanplex.xyz", "http://localhost:8080",
|
|
||||||
]
|
|
||||||
## The server debug mode uses.
|
|
||||||
const DEBUG_SERVER: String = "http://localhost:8080"
|
|
||||||
## The winning score debug mode starts with, so a game is quick to win while testing.
|
|
||||||
const DEBUG_WINNING_SCORE: int = 5
|
|
||||||
## The settings the game saves by itself, by section, in [method saved_settings_path].
|
|
||||||
## Count plays in statistics and Prefer less played soundtracks are left out on purpose.
|
|
||||||
const SAVED_SETTINGS: Dictionary[String, Array] = {
|
|
||||||
"quiz": [
|
|
||||||
&"auto_repeat_song", &"stop_after_current", &"hide_next_track", &"hide_beginning",
|
|
||||||
&"hide_for_seconds", &"hide_length", &"hide_ticks", &"winning_score",
|
|
||||||
&"inspiration_list_speed", &"short_songs", &"short_song_seconds",
|
|
||||||
],
|
|
||||||
"window": [&"fullscreen"],
|
|
||||||
"app": [&"last_seen_version", &"theme_id", &"reduce_animations"],
|
|
||||||
}
|
|
||||||
## The looks the game can have, picked in Settings: an id that is saved, the name shown
|
|
||||||
## and the theme file. The first is the default. See themes/README.md.
|
|
||||||
const THEMES: Array[Dictionary] = [
|
|
||||||
{"id": "classic", "name": "Classic", "path": "res://themes/main_theme.tres"},
|
|
||||||
{"id": "original_web", "name": "Original web", "path": "res://themes/original_web_theme.tres"},
|
|
||||||
{"id": "nord", "name": "Nord", "path": "res://themes/nord_theme.tres"},
|
|
||||||
{"id": "christmas", "name": "Christmas", "path": "res://themes/christmas_theme.tres"},
|
|
||||||
]
|
|
||||||
## When this environment variable is set, the settings files are kept in that folder
|
|
||||||
## instead of the user's config folder. The tests set it so they never touch the real files.
|
|
||||||
const CONFIG_DIR_ENV: String = "MUSICPLAYER_CONFIG_DIR"
|
|
||||||
## The file the user edits by hand. The game writes it only when it is missing, with every
|
|
||||||
## setting commented out.
|
|
||||||
const CONFIG_FILE_NAME: String = "config.ini"
|
|
||||||
const CONFIG_TEMPLATE: String = """# MusicPlayer config. The game never changes this file, so edit it as you like.
|
|
||||||
# Lines starting with # or ; are comments. Remove the # in front of a setting to use it,
|
|
||||||
# and put it back to go back to the default.
|
|
||||||
|
|
||||||
[server]
|
|
||||||
# The MusicServer to use when the game starts. Another one can still be picked in
|
|
||||||
# Settings for as long as the game runs. Debug mode uses http://localhost:8080.
|
|
||||||
#url = https://music.sanplex.xyz
|
|
||||||
|
|
||||||
# Password for getting an API token from the server.
|
|
||||||
#password =
|
|
||||||
|
|
||||||
[game]
|
|
||||||
# true or false. Debug mode uses the local server, starts with a winning score of 5 and
|
|
||||||
# shows DEBUG in the top bar. Without this setting, the game is in debug mode when run
|
|
||||||
# from the Godot editor and not when exported.
|
|
||||||
#debug = true
|
|
||||||
"""
|
|
||||||
|
|
||||||
var default_path: String = "https://music.sanplex.xyz"
|
|
||||||
## Password for getting an API token from the server. Any password works when the
|
|
||||||
## server has none set.
|
|
||||||
var api_password: String = ""
|
|
||||||
## The servers that can be picked in Settings: [constant SERVERS], plus the server from
|
|
||||||
## the config file when it is not one of them.
|
|
||||||
var servers: Array[String] = SERVERS.duplicate()
|
|
||||||
var selected_server: int = 0
|
|
||||||
var is_debug: bool = false
|
|
||||||
var stop_after_current: bool = true
|
|
||||||
var auto_repeat_song: bool = true
|
|
||||||
var hide_next_track: bool = true
|
|
||||||
var add_to_stats: bool = true
|
|
||||||
## How the server picks random songs. Not saved, like statistics counting.
|
|
||||||
var random_mode: RandomMode = RandomMode.NORMAL
|
|
||||||
var winning_score: int = 20
|
|
||||||
var fullscreen: bool = false
|
|
||||||
var inspiration_list_speed: int = 1
|
|
||||||
var hide_for_seconds: float = 5.0
|
|
||||||
var hide_beginning: bool = false
|
|
||||||
var hide_length: bool = false
|
|
||||||
var hide_ticks: bool = false
|
|
||||||
## Plays only [member short_song_seconds] of each song, from a random point, so more
|
|
||||||
## songs fit in a game.
|
|
||||||
var short_songs: bool = false
|
|
||||||
var short_song_seconds: int = 30
|
|
||||||
var player_array: Array[PlayerObject]
|
|
||||||
var edit_players: bool = false
|
|
||||||
var currently_syncing: bool = false
|
|
||||||
var character_select_open: bool = false
|
|
||||||
## The ID of the game's play queue on each server, by server URL, set by
|
|
||||||
## [code]MusicServerClient[/code]. Saved in the [code][queues][/code] section of
|
|
||||||
## [method saved_settings_path], so the game goes on with the same queue after a restart.
|
|
||||||
var queue_ids: Dictionary[String, String] = {}
|
|
||||||
## The version the game last showed the About popup for. When it is not [member version],
|
|
||||||
## this version is new and the main window opens the About popup to show what is new.
|
|
||||||
var last_seen_version: String = ""
|
|
||||||
## Turns every animation off, so nothing moves. See [Anim].
|
|
||||||
var reduce_animations: bool = false
|
|
||||||
## The id of the theme in use, one of [constant THEMES].
|
|
||||||
var theme_id: String = THEMES[0].id
|
|
||||||
## The folder songs are played from instead of the server, picked in the settings, or null
|
|
||||||
## to use the server. Kept for this session only.
|
|
||||||
var music_folder: MusicFolder = null
|
|
||||||
## The folder characters are read from instead of the server, picked in the settings, or
|
|
||||||
## empty. Kept for this session only. See [method characters_dir].
|
|
||||||
var character_folder: String = ""
|
|
||||||
|
|
||||||
## The game version, set in Project Settings > Application > Config > Version.
|
|
||||||
## Releases are made from it, see RELEASING.md.
|
|
||||||
var version: String = ProjectSettings.get_setting("application/config/version", "")
|
|
||||||
|
|
||||||
|
|
||||||
# Autoloads are ready before any scene, so the server and debug mode from the config file
|
|
||||||
# are used from the very first request. The saved settings come after, so the debug
|
|
||||||
# winning score is only used until another one is saved.
|
|
||||||
func _ready() -> void:
|
|
||||||
apply_config(load_config())
|
|
||||||
load_saved_settings()
|
|
||||||
_use_theme(theme_id)
|
|
||||||
if fullscreen:
|
|
||||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
|
|
||||||
|
|
||||||
|
|
||||||
## Uses the server, password and debug mode from the hand-edited [param config]. Settings
|
|
||||||
## left out of it get their defaults.
|
|
||||||
func apply_config(config: IniFile) -> void:
|
|
||||||
is_debug = config.get_bool("game", "debug", OS.has_feature("debug"))
|
|
||||||
default_path = DEBUG_SERVER if is_debug else SERVERS[0]
|
|
||||||
if is_debug:
|
|
||||||
winning_score = DEBUG_WINNING_SCORE
|
|
||||||
var url: String = config.get_string("server", "url")
|
|
||||||
if !url.is_empty():
|
|
||||||
default_path = url.trim_suffix("/")
|
|
||||||
if !servers.has(default_path):
|
|
||||||
servers.append(default_path)
|
|
||||||
selected_server = servers.find(default_path)
|
|
||||||
api_password = config.get_string("server", "password")
|
|
||||||
|
|
||||||
|
|
||||||
## The folder with the settings files: a MusicPlayer folder in the user's config folder,
|
|
||||||
## e.g. [code]~/.config[/code] on Linux and [code]%APPDATA%[/code] on Windows.
|
|
||||||
func config_dir() -> String:
|
|
||||||
var dir: String = OS.get_environment(CONFIG_DIR_ENV)
|
|
||||||
if dir.is_empty():
|
|
||||||
dir = OS.get_config_dir().path_join("MusicPlayer")
|
|
||||||
return dir
|
|
||||||
|
|
||||||
|
|
||||||
## Where the hand-edited config file is.
|
|
||||||
func config_path() -> String:
|
|
||||||
return config_dir().path_join(CONFIG_FILE_NAME)
|
|
||||||
|
|
||||||
|
|
||||||
## Where the game saves its settings. Debug mode uses a file of its own, so testing never
|
|
||||||
## changes the real settings.
|
|
||||||
func saved_settings_path() -> String:
|
|
||||||
var file_name: String = "settings_debug.cfg" if is_debug else "settings.cfg"
|
|
||||||
return config_dir().path_join(file_name)
|
|
||||||
|
|
||||||
|
|
||||||
## Reads the hand-edited config file. When it is missing, writes one with every setting
|
|
||||||
## commented out, so there is a file to edit.
|
|
||||||
func load_config() -> IniFile:
|
|
||||||
var path: String = config_path()
|
|
||||||
if !FileAccess.file_exists(path):
|
|
||||||
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
|
||||||
var template: FileAccess = FileAccess.open(path, FileAccess.WRITE)
|
|
||||||
if template != null:
|
|
||||||
template.store_string(CONFIG_TEMPLATE)
|
|
||||||
return IniFile.load_file(path)
|
|
||||||
|
|
||||||
|
|
||||||
## Loads the settings the game saved. Settings missing from the file, or saved with the
|
|
||||||
## wrong type, keep their current value.
|
|
||||||
func load_saved_settings() -> void:
|
|
||||||
var file: ConfigFile = ConfigFile.new()
|
|
||||||
if file.load(saved_settings_path()) != OK:
|
|
||||||
return
|
|
||||||
for section: String in SAVED_SETTINGS:
|
|
||||||
for setting: StringName in SAVED_SETTINGS[section]:
|
|
||||||
set(setting, _saved_value(file, section, setting, get(setting)))
|
|
||||||
if file.has_section("queues"):
|
|
||||||
queue_ids.clear()
|
|
||||||
for server: String in file.get_section_keys("queues"):
|
|
||||||
var id: Variant = file.get_value("queues", server)
|
|
||||||
if id is String:
|
|
||||||
queue_ids[server] = id
|
|
||||||
|
|
||||||
|
|
||||||
## Saves the settings in [constant SAVED_SETTINGS], so they are the same the next time
|
|
||||||
## the game starts.
|
|
||||||
func save_settings() -> void:
|
|
||||||
var path: String = saved_settings_path()
|
|
||||||
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
|
||||||
var file: ConfigFile = ConfigFile.new()
|
|
||||||
for section: String in SAVED_SETTINGS:
|
|
||||||
for setting: StringName in SAVED_SETTINGS[section]:
|
|
||||||
file.set_value(section, setting, get(setting))
|
|
||||||
for server: String in queue_ids:
|
|
||||||
file.set_value("queues", server, queue_ids[server])
|
|
||||||
var error: Error = file.save(path)
|
|
||||||
if error != OK:
|
|
||||||
GameLog.warning("Could not save the settings to %s: %s" % [path, error_string(error)])
|
|
||||||
|
|
||||||
|
|
||||||
## Sets the play queue ID for [param server] and saves it. Only the ID is changed in
|
|
||||||
## the file; the other settings in it stay as they were saved.
|
|
||||||
func set_queue_id(server: String, id: String) -> void:
|
|
||||||
queue_ids[server] = id
|
|
||||||
var path: String = saved_settings_path()
|
|
||||||
var file: ConfigFile = ConfigFile.new()
|
|
||||||
# A missing file is fine: it is written with only the queue ID
|
|
||||||
file.load(path)
|
|
||||||
file.set_value("queues", server, id)
|
|
||||||
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
|
||||||
var error: Error = file.save(path)
|
|
||||||
if error != OK:
|
|
||||||
GameLog.warning("Could not save the queue ID to %s: %s" % [path, error_string(error)])
|
|
||||||
|
|
||||||
|
|
||||||
## Turns fullscreen on or off and saves it.
|
|
||||||
func set_fullscreen(on: bool) -> void:
|
|
||||||
fullscreen = on
|
|
||||||
if on:
|
|
||||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
|
|
||||||
else:
|
|
||||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
|
||||||
save_settings()
|
|
||||||
|
|
||||||
|
|
||||||
# The saved value of [param key], or [param default] when it is missing or of another type
|
|
||||||
func _saved_value(file: ConfigFile, section: String, key: String, default: Variant) -> Variant:
|
|
||||||
var value: Variant = file.get_value(section, key, default)
|
|
||||||
if typeof(value) != typeof(default):
|
|
||||||
push_warning("%s in [%s] should be a %s, not %s" % [key, section,
|
|
||||||
type_string(typeof(default)), var_to_str(value)])
|
|
||||||
return default
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
## Whether songs come from a music folder instead of the server. Nothing but the
|
|
||||||
## characters is asked from the server then.
|
|
||||||
func is_offline() -> bool:
|
|
||||||
return music_folder != null
|
|
||||||
|
|
||||||
|
|
||||||
## Whether a prepared quiz is played from a music folder. It has no winning score: the
|
|
||||||
## game ends when every song has played.
|
|
||||||
func is_prepared_quiz() -> bool:
|
|
||||||
return music_folder != null && music_folder.kind == MusicFolder.Kind.PREPARED_QUIZ
|
|
||||||
|
|
||||||
|
|
||||||
## The folder the characters come from, or empty for the server's: the folder picked in
|
|
||||||
## the settings, otherwise the [code]characters[/code] folder of the music folder played.
|
|
||||||
func characters_dir() -> String:
|
|
||||||
if !character_folder.is_empty():
|
|
||||||
return character_folder
|
|
||||||
if music_folder != null:
|
|
||||||
return music_folder.characters_path
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
## Switches the whole game to the theme [param id] from [constant THEMES] and saves the
|
|
||||||
## choice. Controls without a theme of their own take it from the root window.
|
|
||||||
func apply_theme(id: String) -> void:
|
|
||||||
_use_theme(id)
|
|
||||||
save_settings()
|
|
||||||
|
|
||||||
|
|
||||||
## The theme in [constant THEMES] with [param id], or the default one if there is none.
|
|
||||||
func theme_entry(id: String) -> Dictionary:
|
|
||||||
for entry: Dictionary in THEMES:
|
|
||||||
if entry.id == id:
|
|
||||||
return entry
|
|
||||||
return THEMES[0]
|
|
||||||
|
|
||||||
|
|
||||||
func _use_theme(id: String) -> void:
|
|
||||||
var entry: Dictionary = theme_entry(id)
|
|
||||||
theme_id = entry.id
|
|
||||||
get_tree().root.theme = load(entry.path)
|
|
||||||
theme_changed.emit()
|
|
||||||
|
|
||||||
|
|
||||||
func delete_children(node: Node) -> void:
|
|
||||||
for n: Node in node.get_children():
|
|
||||||
node.remove_child(n)
|
|
||||||
n.queue_free()
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
extends RefCounted
|
|
||||||
|
|
||||||
class_name SongObject
|
|
||||||
|
|
||||||
var song_number: int
|
|
||||||
var song_title: String
|
|
||||||
var soundtrack_title: String
|
|
||||||
## The UUID of the song's soundtrack on the server, empty for songs from a music folder.
|
|
||||||
var soundtrack_uuid: 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
|
|
||||||
## The song's file: a download in [method Playlist.songs_folder], or a song in the music
|
|
||||||
## folder. Read the song with [method Playlist.get_song].
|
|
||||||
var song_path: String = ""
|
|
||||||
## The song itself, only when it could not be saved to a file.
|
|
||||||
var song: AudioStream
|
|
||||||
## Where the part played with Short songs starts, in seconds, or -1 before it is picked.
|
|
||||||
## Picked once, so restarting and repeating play the same part.
|
|
||||||
var short_start: float = -1.0
|
|
||||||
|
|
||||||
|
|
||||||
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 "??????"
|
|
||||||
|
|
||||||
|
|
||||||
## The soundtrack's UUID once the answer is shown, otherwise empty.
|
|
||||||
func get_soundtrack_uuid() -> String:
|
|
||||||
return soundtrack_uuid if is_answered else ""
|
|
||||||
|
|
||||||
|
|
||||||
func get_song_info() -> String:
|
|
||||||
if Settings.is_debug:
|
|
||||||
return "%d. %s | played: %s | answered: %s" % [(song_number + 1), _full_title(),
|
|
||||||
has_played, is_answered]
|
|
||||||
elif is_answered:
|
|
||||||
return "%d. %s" % [(song_number + 1), _full_title()]
|
|
||||||
else:
|
|
||||||
return "??? - ???"
|
|
||||||
|
|
||||||
|
|
||||||
# "Soundtrack - Song", or only the song in a prepared quiz, which has no soundtracks
|
|
||||||
func _full_title() -> String:
|
|
||||||
if soundtrack_title.is_empty():
|
|
||||||
return song_title
|
|
||||||
return "%s - %s" % [soundtrack_title, song_title]
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
## The song as a Dictionary that can be written as JSON, for [QuizSave]. A song kept only
|
|
||||||
## in memory is saved without its file.
|
|
||||||
func to_dict() -> Dictionary:
|
|
||||||
return {
|
|
||||||
"number": song_number, "soundtrack": soundtrack_title, "song": song_title,
|
|
||||||
"points": players_given_point.duplicate(), "answered": is_answered,
|
|
||||||
"played": has_played, "playing": is_playing, "path": song_path,
|
|
||||||
"soundtrack_uuid": soundtrack_uuid,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
## A song made from [param data], as given by [method to_dict], or null when [param data]
|
|
||||||
## is not a song.
|
|
||||||
static func from_dict(data: Variant) -> SongObject:
|
|
||||||
if !(data is Dictionary) || !(data.get("number") is float || data.get("number") is int):
|
|
||||||
return null
|
|
||||||
var song_object: SongObject = SongObject.new()
|
|
||||||
song_object.song_number = int(data.number)
|
|
||||||
song_object.soundtrack_title = str(data.get("soundtrack", ""))
|
|
||||||
song_object.song_title = str(data.get("song", ""))
|
|
||||||
song_object.soundtrack_uuid = str(data.get("soundtrack_uuid", ""))
|
|
||||||
if data.get("points") is Array:
|
|
||||||
for player_id: Variant in data.points:
|
|
||||||
song_object.add_point(str(player_id))
|
|
||||||
song_object.is_answered = bool(data.get("answered", false))
|
|
||||||
song_object.has_played = bool(data.get("played", false))
|
|
||||||
song_object.is_playing = bool(data.get("playing", false))
|
|
||||||
song_object.song_path = str(data.get("path", ""))
|
|
||||||
return song_object
|
|
||||||
|
|
||||||
|
|
||||||
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]
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
class_name UploadFolder
|
|
||||||
extends RefCounted
|
|
||||||
## A folder picked or dropped to upload as a new soundtrack: its MP3s, ready to send, and
|
|
||||||
## the other song files, which are left out because the server only takes MP3.
|
|
||||||
##
|
|
||||||
## The server wants one folder of songs, so songs in subfolders, such as disc folders,
|
|
||||||
## are taken too, with the disc's number in front of their names: "Disc 1/01 Title.mp3"
|
|
||||||
## is sent as "1-01 Title.mp3". With only one disc folder, its songs keep their names.
|
|
||||||
## Hidden files and folders are left out, and names get rid of the characters the server
|
|
||||||
## refuses.
|
|
||||||
|
|
||||||
## The only kind of song the server takes.
|
|
||||||
const UPLOAD_EXTENSION: String = "mp3"
|
|
||||||
## Other song files, listed as left out so the user knows.
|
|
||||||
const OTHER_SONG_EXTENSIONS: Array[String] = [
|
|
||||||
"ogg", "wav", "flac", "m4a", "aac", "opus", "wma", "ape", "aif", "aiff",
|
|
||||||
]
|
|
||||||
## Characters the server refuses in soundtrack and file names.
|
|
||||||
const INVALID_NAME_CHARACTERS: String = "/\\:*?\"<>|"
|
|
||||||
|
|
||||||
## The folder.
|
|
||||||
var path: String = ""
|
|
||||||
## The soundtrack's name to suggest: the folder's name.
|
|
||||||
var suggested_name: String = ""
|
|
||||||
## The songs to upload, in order: [code]{"path": String, "name": String, "size": int}[/code],
|
|
||||||
## with the name to send each one as.
|
|
||||||
var songs: Array[Dictionary] = []
|
|
||||||
## The song files left out, as paths in the folder, e.g. [code]"Disc 2/03 Song.flac"[/code].
|
|
||||||
var skipped: Array[String] = []
|
|
||||||
## The size of the songs to upload, in bytes.
|
|
||||||
var total_size: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
## Reads the folder at [param folder].
|
|
||||||
static func scan(folder: String) -> UploadFolder:
|
|
||||||
var result: UploadFolder = UploadFolder.new()
|
|
||||||
result.path = folder.trim_suffix("/").trim_suffix("\\")
|
|
||||||
result.suggested_name = clean_name(result.path.get_file())
|
|
||||||
result._add_files(result.path, "", "")
|
|
||||||
var discs: Array[String] = []
|
|
||||||
for disc: String in _visible(DirAccess.get_directories_at(result.path)):
|
|
||||||
if _has_songs(result.path.path_join(disc)):
|
|
||||||
discs.append(disc)
|
|
||||||
for disc: int in discs.size():
|
|
||||||
result._add_folder(result.path.path_join(discs[disc]), discs[disc],
|
|
||||||
"%d-" % (disc + 1) if discs.size() > 1 else "")
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
## [param name] without the characters the server refuses, a leading dot, or a trailing
|
|
||||||
## dot or space.
|
|
||||||
static func clean_name(name: String) -> String:
|
|
||||||
var cleaned: String = ""
|
|
||||||
for character: String in name:
|
|
||||||
var control: bool = character.unicode_at(0) < 32 || character.unicode_at(0) == 127
|
|
||||||
cleaned += "-" if INVALID_NAME_CHARACTERS.contains(character) || control \
|
|
||||||
else character
|
|
||||||
cleaned = cleaned.strip_edges().lstrip(".")
|
|
||||||
while cleaned.ends_with(".") || cleaned.ends_with(" "):
|
|
||||||
cleaned = cleaned.left(-1)
|
|
||||||
return cleaned
|
|
||||||
|
|
||||||
|
|
||||||
## Whether there is anything to upload.
|
|
||||||
func has_songs() -> bool:
|
|
||||||
return !songs.is_empty()
|
|
||||||
|
|
||||||
|
|
||||||
# Adds the files of [param folder] and of the folders in it, all with [param prefix]
|
|
||||||
func _add_folder(folder: String, relative: String, prefix: String) -> void:
|
|
||||||
_add_files(folder, relative, prefix)
|
|
||||||
for inner: String in _visible(DirAccess.get_directories_at(folder)):
|
|
||||||
_add_folder(folder.path_join(inner), relative.path_join(inner), prefix)
|
|
||||||
|
|
||||||
|
|
||||||
func _add_files(folder: String, relative: String, prefix: String) -> void:
|
|
||||||
for file_name: String in _visible(DirAccess.get_files_at(folder)):
|
|
||||||
var extension: String = file_name.get_extension().to_lower()
|
|
||||||
if extension == UPLOAD_EXTENSION:
|
|
||||||
var file_path: String = folder.path_join(file_name)
|
|
||||||
var size: int = FileAccess.get_size(file_path)
|
|
||||||
songs.append({"path": file_path, "name": _free_name(prefix + clean_name(file_name)),
|
|
||||||
"size": size})
|
|
||||||
total_size += size
|
|
||||||
elif extension in OTHER_SONG_EXTENSIONS:
|
|
||||||
skipped.append(relative.path_join(file_name) if !relative.is_empty() else file_name)
|
|
||||||
|
|
||||||
|
|
||||||
# [param name], or with " (2)" and so on before its extension when a song already has it
|
|
||||||
func _free_name(name: String) -> String:
|
|
||||||
var taken: Array = songs.map(func(song: Dictionary) -> String: return song.name.to_lower())
|
|
||||||
var candidate: String = name
|
|
||||||
var number: int = 2
|
|
||||||
while taken.has(candidate.to_lower()):
|
|
||||||
candidate = "%s (%d).%s" % [name.get_basename(), number, name.get_extension()]
|
|
||||||
number += 1
|
|
||||||
return candidate
|
|
||||||
|
|
||||||
|
|
||||||
# Whether [param folder] or a folder in it has a song, to upload or to list as left out
|
|
||||||
static func _has_songs(folder: String) -> bool:
|
|
||||||
for file_name: String in _visible(DirAccess.get_files_at(folder)):
|
|
||||||
var extension: String = file_name.get_extension().to_lower()
|
|
||||||
if extension == UPLOAD_EXTENSION || extension in OTHER_SONG_EXTENSIONS:
|
|
||||||
return true
|
|
||||||
for inner: String in _visible(DirAccess.get_directories_at(folder)):
|
|
||||||
if _has_songs(folder.path_join(inner)):
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
|
|
||||||
# The names that are not hidden, in natural order, so "2" comes before "10"
|
|
||||||
static func _visible(names: PackedStringArray) -> Array[String]:
|
|
||||||
var shown: Array[String] = []
|
|
||||||
for name: String in names:
|
|
||||||
if !name.begins_with("."):
|
|
||||||
shown.append(name)
|
|
||||||
shown.sort_custom(func(a: String, b: String) -> bool: return a.naturalnocasecmp_to(b) < 0)
|
|
||||||
return shown
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://3amk1cte2idb
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_resource type="Theme" load_steps=2 format=3 uid="uid://8bllart6xn1f"]
|
||||||
|
|
||||||
|
[ext_resource type="StyleBox" uid="uid://bsv6yb0yir04l" path="res://style_box_flat.tres" id="1_uierc"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
PanelContainer/styles/panel = ExtResource("1_uierc")
|
||||||
+269
-9
@@ -8,7 +8,7 @@ dedicated_server=false
|
|||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter="tests/*, addons/gdUnit4/*, linux/*"
|
exclude_filter="tests/*, addons/gdUnit4/*"
|
||||||
export_path="../../MusicPlayer_1.5.1.exe"
|
export_path="../../MusicPlayer_1.5.1.exe"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
@@ -75,16 +75,16 @@ debug/export_console_script=1
|
|||||||
|
|
||||||
[preset.1]
|
[preset.1]
|
||||||
|
|
||||||
name="Web"
|
name="macOS"
|
||||||
platform="Web"
|
platform="macOS"
|
||||||
runnable=false
|
runnable=true
|
||||||
advanced_options=false
|
advanced_options=false
|
||||||
dedicated_server=false
|
dedicated_server=false
|
||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter="tests/*, addons/gdUnit4/*, linux/*"
|
exclude_filter="tests/*, addons/gdUnit4/*"
|
||||||
export_path="web/index.html"
|
export_path="./MusicPlayer_0.9_Beta.dmg"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
@@ -95,6 +95,266 @@ script_export_mode=2
|
|||||||
|
|
||||||
[preset.1.options]
|
[preset.1.options]
|
||||||
|
|
||||||
|
export/distribution_type=1
|
||||||
|
binary_format/architecture="universal"
|
||||||
|
custom_template/debug=""
|
||||||
|
custom_template/release=""
|
||||||
|
debug/export_console_wrapper=1
|
||||||
|
application/icon=""
|
||||||
|
application/icon_interpolation=4
|
||||||
|
application/bundle_identifier="tech.sanplex.musicplayer"
|
||||||
|
application/signature=""
|
||||||
|
application/app_category="Music-games"
|
||||||
|
application/short_version=""
|
||||||
|
application/version=""
|
||||||
|
application/copyright=""
|
||||||
|
application/copyright_localized={}
|
||||||
|
application/min_macos_version_x86_64="10.12"
|
||||||
|
application/min_macos_version_arm64="11.00"
|
||||||
|
application/export_angle=0
|
||||||
|
display/high_res=true
|
||||||
|
application/additional_plist_content=""
|
||||||
|
xcode/platform_build="14C18"
|
||||||
|
xcode/sdk_version="13.1"
|
||||||
|
xcode/sdk_build="22C55"
|
||||||
|
xcode/sdk_name="macosx13.1"
|
||||||
|
xcode/xcode_version="1420"
|
||||||
|
xcode/xcode_build="14C18"
|
||||||
|
codesign/codesign=1
|
||||||
|
codesign/installer_identity=""
|
||||||
|
codesign/apple_team_id=""
|
||||||
|
codesign/identity=""
|
||||||
|
codesign/entitlements/custom_file=""
|
||||||
|
codesign/entitlements/allow_jit_code_execution=false
|
||||||
|
codesign/entitlements/allow_unsigned_executable_memory=false
|
||||||
|
codesign/entitlements/allow_dyld_environment_variables=false
|
||||||
|
codesign/entitlements/disable_library_validation=false
|
||||||
|
codesign/entitlements/audio_input=false
|
||||||
|
codesign/entitlements/camera=false
|
||||||
|
codesign/entitlements/location=false
|
||||||
|
codesign/entitlements/address_book=false
|
||||||
|
codesign/entitlements/calendars=false
|
||||||
|
codesign/entitlements/photos_library=false
|
||||||
|
codesign/entitlements/apple_events=false
|
||||||
|
codesign/entitlements/debugging=false
|
||||||
|
codesign/entitlements/app_sandbox/enabled=false
|
||||||
|
codesign/entitlements/app_sandbox/network_server=false
|
||||||
|
codesign/entitlements/app_sandbox/network_client=false
|
||||||
|
codesign/entitlements/app_sandbox/device_usb=false
|
||||||
|
codesign/entitlements/app_sandbox/device_bluetooth=false
|
||||||
|
codesign/entitlements/app_sandbox/files_downloads=0
|
||||||
|
codesign/entitlements/app_sandbox/files_pictures=0
|
||||||
|
codesign/entitlements/app_sandbox/files_music=0
|
||||||
|
codesign/entitlements/app_sandbox/files_movies=0
|
||||||
|
codesign/entitlements/app_sandbox/files_user_selected=0
|
||||||
|
codesign/entitlements/app_sandbox/helper_executables=[]
|
||||||
|
codesign/entitlements/additional=""
|
||||||
|
codesign/custom_options=PackedStringArray()
|
||||||
|
notarization/notarization=0
|
||||||
|
privacy/microphone_usage_description=""
|
||||||
|
privacy/microphone_usage_description_localized={}
|
||||||
|
privacy/camera_usage_description=""
|
||||||
|
privacy/camera_usage_description_localized={}
|
||||||
|
privacy/location_usage_description=""
|
||||||
|
privacy/location_usage_description_localized={}
|
||||||
|
privacy/address_book_usage_description=""
|
||||||
|
privacy/address_book_usage_description_localized={}
|
||||||
|
privacy/calendar_usage_description=""
|
||||||
|
privacy/calendar_usage_description_localized={}
|
||||||
|
privacy/photos_library_usage_description=""
|
||||||
|
privacy/photos_library_usage_description_localized={}
|
||||||
|
privacy/desktop_folder_usage_description=""
|
||||||
|
privacy/desktop_folder_usage_description_localized={}
|
||||||
|
privacy/documents_folder_usage_description=""
|
||||||
|
privacy/documents_folder_usage_description_localized={}
|
||||||
|
privacy/downloads_folder_usage_description=""
|
||||||
|
privacy/downloads_folder_usage_description_localized={}
|
||||||
|
privacy/network_volumes_usage_description=""
|
||||||
|
privacy/network_volumes_usage_description_localized={}
|
||||||
|
privacy/removable_volumes_usage_description=""
|
||||||
|
privacy/removable_volumes_usage_description_localized={}
|
||||||
|
privacy/tracking_enabled=false
|
||||||
|
privacy/tracking_domains=PackedStringArray()
|
||||||
|
privacy/collected_data/name/collected=false
|
||||||
|
privacy/collected_data/name/linked_to_user=false
|
||||||
|
privacy/collected_data/name/used_for_tracking=false
|
||||||
|
privacy/collected_data/name/collection_purposes=0
|
||||||
|
privacy/collected_data/email_address/collected=false
|
||||||
|
privacy/collected_data/email_address/linked_to_user=false
|
||||||
|
privacy/collected_data/email_address/used_for_tracking=false
|
||||||
|
privacy/collected_data/email_address/collection_purposes=0
|
||||||
|
privacy/collected_data/phone_number/collected=false
|
||||||
|
privacy/collected_data/phone_number/linked_to_user=false
|
||||||
|
privacy/collected_data/phone_number/used_for_tracking=false
|
||||||
|
privacy/collected_data/phone_number/collection_purposes=0
|
||||||
|
privacy/collected_data/physical_address/collected=false
|
||||||
|
privacy/collected_data/physical_address/linked_to_user=false
|
||||||
|
privacy/collected_data/physical_address/used_for_tracking=false
|
||||||
|
privacy/collected_data/physical_address/collection_purposes=0
|
||||||
|
privacy/collected_data/other_contact_info/collected=false
|
||||||
|
privacy/collected_data/other_contact_info/linked_to_user=false
|
||||||
|
privacy/collected_data/other_contact_info/used_for_tracking=false
|
||||||
|
privacy/collected_data/other_contact_info/collection_purposes=0
|
||||||
|
privacy/collected_data/health/collected=false
|
||||||
|
privacy/collected_data/health/linked_to_user=false
|
||||||
|
privacy/collected_data/health/used_for_tracking=false
|
||||||
|
privacy/collected_data/health/collection_purposes=0
|
||||||
|
privacy/collected_data/fitness/collected=false
|
||||||
|
privacy/collected_data/fitness/linked_to_user=false
|
||||||
|
privacy/collected_data/fitness/used_for_tracking=false
|
||||||
|
privacy/collected_data/fitness/collection_purposes=0
|
||||||
|
privacy/collected_data/payment_info/collected=false
|
||||||
|
privacy/collected_data/payment_info/linked_to_user=false
|
||||||
|
privacy/collected_data/payment_info/used_for_tracking=false
|
||||||
|
privacy/collected_data/payment_info/collection_purposes=0
|
||||||
|
privacy/collected_data/credit_info/collected=false
|
||||||
|
privacy/collected_data/credit_info/linked_to_user=false
|
||||||
|
privacy/collected_data/credit_info/used_for_tracking=false
|
||||||
|
privacy/collected_data/credit_info/collection_purposes=0
|
||||||
|
privacy/collected_data/other_financial_info/collected=false
|
||||||
|
privacy/collected_data/other_financial_info/linked_to_user=false
|
||||||
|
privacy/collected_data/other_financial_info/used_for_tracking=false
|
||||||
|
privacy/collected_data/other_financial_info/collection_purposes=0
|
||||||
|
privacy/collected_data/precise_location/collected=false
|
||||||
|
privacy/collected_data/precise_location/linked_to_user=false
|
||||||
|
privacy/collected_data/precise_location/used_for_tracking=false
|
||||||
|
privacy/collected_data/precise_location/collection_purposes=0
|
||||||
|
privacy/collected_data/coarse_location/collected=false
|
||||||
|
privacy/collected_data/coarse_location/linked_to_user=false
|
||||||
|
privacy/collected_data/coarse_location/used_for_tracking=false
|
||||||
|
privacy/collected_data/coarse_location/collection_purposes=0
|
||||||
|
privacy/collected_data/sensitive_info/collected=false
|
||||||
|
privacy/collected_data/sensitive_info/linked_to_user=false
|
||||||
|
privacy/collected_data/sensitive_info/used_for_tracking=false
|
||||||
|
privacy/collected_data/sensitive_info/collection_purposes=0
|
||||||
|
privacy/collected_data/contacts/collected=false
|
||||||
|
privacy/collected_data/contacts/linked_to_user=false
|
||||||
|
privacy/collected_data/contacts/used_for_tracking=false
|
||||||
|
privacy/collected_data/contacts/collection_purposes=0
|
||||||
|
privacy/collected_data/emails_or_text_messages/collected=false
|
||||||
|
privacy/collected_data/emails_or_text_messages/linked_to_user=false
|
||||||
|
privacy/collected_data/emails_or_text_messages/used_for_tracking=false
|
||||||
|
privacy/collected_data/emails_or_text_messages/collection_purposes=0
|
||||||
|
privacy/collected_data/photos_or_videos/collected=false
|
||||||
|
privacy/collected_data/photos_or_videos/linked_to_user=false
|
||||||
|
privacy/collected_data/photos_or_videos/used_for_tracking=false
|
||||||
|
privacy/collected_data/photos_or_videos/collection_purposes=0
|
||||||
|
privacy/collected_data/audio_data/collected=false
|
||||||
|
privacy/collected_data/audio_data/linked_to_user=false
|
||||||
|
privacy/collected_data/audio_data/used_for_tracking=false
|
||||||
|
privacy/collected_data/audio_data/collection_purposes=0
|
||||||
|
privacy/collected_data/gameplay_content/collected=false
|
||||||
|
privacy/collected_data/gameplay_content/linked_to_user=false
|
||||||
|
privacy/collected_data/gameplay_content/used_for_tracking=false
|
||||||
|
privacy/collected_data/gameplay_content/collection_purposes=0
|
||||||
|
privacy/collected_data/customer_support/collected=false
|
||||||
|
privacy/collected_data/customer_support/linked_to_user=false
|
||||||
|
privacy/collected_data/customer_support/used_for_tracking=false
|
||||||
|
privacy/collected_data/customer_support/collection_purposes=0
|
||||||
|
privacy/collected_data/other_user_content/collected=false
|
||||||
|
privacy/collected_data/other_user_content/linked_to_user=false
|
||||||
|
privacy/collected_data/other_user_content/used_for_tracking=false
|
||||||
|
privacy/collected_data/other_user_content/collection_purposes=0
|
||||||
|
privacy/collected_data/browsing_history/collected=false
|
||||||
|
privacy/collected_data/browsing_history/linked_to_user=false
|
||||||
|
privacy/collected_data/browsing_history/used_for_tracking=false
|
||||||
|
privacy/collected_data/browsing_history/collection_purposes=0
|
||||||
|
privacy/collected_data/search_hhistory/collected=false
|
||||||
|
privacy/collected_data/search_hhistory/linked_to_user=false
|
||||||
|
privacy/collected_data/search_hhistory/used_for_tracking=false
|
||||||
|
privacy/collected_data/search_hhistory/collection_purposes=0
|
||||||
|
privacy/collected_data/user_id/collected=false
|
||||||
|
privacy/collected_data/user_id/linked_to_user=false
|
||||||
|
privacy/collected_data/user_id/used_for_tracking=false
|
||||||
|
privacy/collected_data/user_id/collection_purposes=0
|
||||||
|
privacy/collected_data/device_id/collected=false
|
||||||
|
privacy/collected_data/device_id/linked_to_user=false
|
||||||
|
privacy/collected_data/device_id/used_for_tracking=false
|
||||||
|
privacy/collected_data/device_id/collection_purposes=0
|
||||||
|
privacy/collected_data/purchase_history/collected=false
|
||||||
|
privacy/collected_data/purchase_history/linked_to_user=false
|
||||||
|
privacy/collected_data/purchase_history/used_for_tracking=false
|
||||||
|
privacy/collected_data/purchase_history/collection_purposes=0
|
||||||
|
privacy/collected_data/product_interaction/collected=false
|
||||||
|
privacy/collected_data/product_interaction/linked_to_user=false
|
||||||
|
privacy/collected_data/product_interaction/used_for_tracking=false
|
||||||
|
privacy/collected_data/product_interaction/collection_purposes=0
|
||||||
|
privacy/collected_data/advertising_data/collected=false
|
||||||
|
privacy/collected_data/advertising_data/linked_to_user=false
|
||||||
|
privacy/collected_data/advertising_data/used_for_tracking=false
|
||||||
|
privacy/collected_data/advertising_data/collection_purposes=0
|
||||||
|
privacy/collected_data/other_usage_data/collected=false
|
||||||
|
privacy/collected_data/other_usage_data/linked_to_user=false
|
||||||
|
privacy/collected_data/other_usage_data/used_for_tracking=false
|
||||||
|
privacy/collected_data/other_usage_data/collection_purposes=0
|
||||||
|
privacy/collected_data/crash_data/collected=false
|
||||||
|
privacy/collected_data/crash_data/linked_to_user=false
|
||||||
|
privacy/collected_data/crash_data/used_for_tracking=false
|
||||||
|
privacy/collected_data/crash_data/collection_purposes=0
|
||||||
|
privacy/collected_data/performance_data/collected=false
|
||||||
|
privacy/collected_data/performance_data/linked_to_user=false
|
||||||
|
privacy/collected_data/performance_data/used_for_tracking=false
|
||||||
|
privacy/collected_data/performance_data/collection_purposes=0
|
||||||
|
privacy/collected_data/other_diagnostic_data/collected=false
|
||||||
|
privacy/collected_data/other_diagnostic_data/linked_to_user=false
|
||||||
|
privacy/collected_data/other_diagnostic_data/used_for_tracking=false
|
||||||
|
privacy/collected_data/other_diagnostic_data/collection_purposes=0
|
||||||
|
privacy/collected_data/environment_scanning/collected=false
|
||||||
|
privacy/collected_data/environment_scanning/linked_to_user=false
|
||||||
|
privacy/collected_data/environment_scanning/used_for_tracking=false
|
||||||
|
privacy/collected_data/environment_scanning/collection_purposes=0
|
||||||
|
privacy/collected_data/hands/collected=false
|
||||||
|
privacy/collected_data/hands/linked_to_user=false
|
||||||
|
privacy/collected_data/hands/used_for_tracking=false
|
||||||
|
privacy/collected_data/hands/collection_purposes=0
|
||||||
|
privacy/collected_data/head/collected=false
|
||||||
|
privacy/collected_data/head/linked_to_user=false
|
||||||
|
privacy/collected_data/head/used_for_tracking=false
|
||||||
|
privacy/collected_data/head/collection_purposes=0
|
||||||
|
privacy/collected_data/other_data_types/collected=false
|
||||||
|
privacy/collected_data/other_data_types/linked_to_user=false
|
||||||
|
privacy/collected_data/other_data_types/used_for_tracking=false
|
||||||
|
privacy/collected_data/other_data_types/collection_purposes=0
|
||||||
|
ssh_remote_deploy/enabled=false
|
||||||
|
ssh_remote_deploy/host="user@host_ip"
|
||||||
|
ssh_remote_deploy/port="22"
|
||||||
|
ssh_remote_deploy/extra_args_ssh=""
|
||||||
|
ssh_remote_deploy/extra_args_scp=""
|
||||||
|
ssh_remote_deploy/run_script="#!/usr/bin/env bash
|
||||||
|
unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
|
||||||
|
open \"{temp_dir}/{exe_name}.app\" --args {cmd_args}"
|
||||||
|
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
|
||||||
|
kill $(pgrep -x -f \"{temp_dir}/{exe_name}.app/Contents/MacOS/{exe_name} {cmd_args}\")
|
||||||
|
rm -rf \"{temp_dir}\""
|
||||||
|
dotnet/include_scripts_content=false
|
||||||
|
dotnet/include_debug_symbols=true
|
||||||
|
dotnet/embed_build_outputs=false
|
||||||
|
application/min_macos_version="10.12"
|
||||||
|
debug/export_console_script=1
|
||||||
|
notarization/apple_team_id=""
|
||||||
|
|
||||||
|
[preset.2]
|
||||||
|
|
||||||
|
name="Web"
|
||||||
|
platform="Web"
|
||||||
|
runnable=false
|
||||||
|
advanced_options=false
|
||||||
|
dedicated_server=false
|
||||||
|
custom_features=""
|
||||||
|
export_filter="all_resources"
|
||||||
|
include_filter=""
|
||||||
|
exclude_filter="tests/*, addons/gdUnit4/*"
|
||||||
|
export_path="web/index.html"
|
||||||
|
patches=PackedStringArray()
|
||||||
|
encryption_include_filters=""
|
||||||
|
encryption_exclude_filters=""
|
||||||
|
seed=0
|
||||||
|
encrypt_pck=false
|
||||||
|
encrypt_directory=false
|
||||||
|
script_export_mode=2
|
||||||
|
|
||||||
|
[preset.2.options]
|
||||||
|
|
||||||
custom_template/debug=""
|
custom_template/debug=""
|
||||||
custom_template/release=""
|
custom_template/release=""
|
||||||
variant/extensions_support=false
|
variant/extensions_support=false
|
||||||
@@ -120,7 +380,7 @@ dotnet/include_scripts_content=false
|
|||||||
dotnet/include_debug_symbols=true
|
dotnet/include_debug_symbols=true
|
||||||
dotnet/embed_build_outputs=false
|
dotnet/embed_build_outputs=false
|
||||||
|
|
||||||
[preset.2]
|
[preset.3]
|
||||||
|
|
||||||
name="Linux"
|
name="Linux"
|
||||||
platform="Linux"
|
platform="Linux"
|
||||||
@@ -130,7 +390,7 @@ dedicated_server=false
|
|||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter="tests/*, addons/gdUnit4/*, linux/*"
|
exclude_filter="tests/*, addons/gdUnit4/*"
|
||||||
export_path="./MusicPlayer_1.5.1.x86_64"
|
export_path="./MusicPlayer_1.5.1.x86_64"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
@@ -140,7 +400,7 @@ encrypt_pck=false
|
|||||||
encrypt_directory=false
|
encrypt_directory=false
|
||||||
script_export_mode=2
|
script_export_mode=2
|
||||||
|
|
||||||
[preset.2.options]
|
[preset.3.options]
|
||||||
|
|
||||||
custom_template/debug=""
|
custom_template/debug=""
|
||||||
custom_template/release=""
|
custom_template/release=""
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
This Font Software is licensed under the SIL Open Font License,
|
|
||||||
Version 1.1.
|
|
||||||
|
|
||||||
This license is copied below, and is also available with a FAQ at:
|
|
||||||
http://scripts.sil.org/OFL
|
|
||||||
|
|
||||||
-----------------------------------------------------------
|
|
||||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
||||||
-----------------------------------------------------------
|
|
||||||
|
|
||||||
PREAMBLE
|
|
||||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
||||||
development of collaborative font projects, to support the font
|
|
||||||
creation efforts of academic and linguistic communities, and to
|
|
||||||
provide a free and open framework in which fonts may be shared and
|
|
||||||
improved in partnership with others.
|
|
||||||
|
|
||||||
The OFL allows the licensed fonts to be used, studied, modified and
|
|
||||||
redistributed freely as long as they are not sold by themselves. The
|
|
||||||
fonts, including any derivative works, can be bundled, embedded,
|
|
||||||
redistributed and/or sold with any software provided that any reserved
|
|
||||||
names are not used by derivative works. The fonts and derivatives,
|
|
||||||
however, cannot be released under any other type of license. The
|
|
||||||
requirement for fonts to remain under this license does not apply to
|
|
||||||
any document created using the fonts or their derivatives.
|
|
||||||
|
|
||||||
DEFINITIONS
|
|
||||||
"Font Software" refers to the set of files released by the Copyright
|
|
||||||
Holder(s) under this license and clearly marked as such. This may
|
|
||||||
include source files, build scripts and documentation.
|
|
||||||
|
|
||||||
"Reserved Font Name" refers to any names specified as such after the
|
|
||||||
copyright statement(s).
|
|
||||||
|
|
||||||
"Original Version" refers to the collection of Font Software
|
|
||||||
components as distributed by the Copyright Holder(s).
|
|
||||||
|
|
||||||
"Modified Version" refers to any derivative made by adding to,
|
|
||||||
deleting, or substituting -- in part or in whole -- any of the
|
|
||||||
components of the Original Version, by changing formats or by porting
|
|
||||||
the Font Software to a new environment.
|
|
||||||
|
|
||||||
"Author" refers to any designer, engineer, programmer, technical
|
|
||||||
writer or other person who contributed to the Font Software.
|
|
||||||
|
|
||||||
PERMISSION & CONDITIONS
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
|
||||||
modify, redistribute, and sell modified and unmodified copies of the
|
|
||||||
Font Software, subject to the following conditions:
|
|
||||||
|
|
||||||
1) Neither the Font Software nor any of its individual components, in
|
|
||||||
Original or Modified Versions, may be sold by itself.
|
|
||||||
|
|
||||||
2) Original or Modified Versions of the Font Software may be bundled,
|
|
||||||
redistributed and/or sold with any software, provided that each copy
|
|
||||||
contains the above copyright notice and this license. These can be
|
|
||||||
included either as stand-alone text files, human-readable headers or
|
|
||||||
in the appropriate machine-readable metadata fields within text or
|
|
||||||
binary files as long as those fields can be easily viewed by the user.
|
|
||||||
|
|
||||||
3) No Modified Version of the Font Software may use the Reserved Font
|
|
||||||
Name(s) unless explicit written permission is granted by the
|
|
||||||
corresponding Copyright Holder. This restriction only applies to the
|
|
||||||
primary font name as presented to the users.
|
|
||||||
|
|
||||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
||||||
Software shall not be used to promote, endorse or advertise any
|
|
||||||
Modified Version, except to acknowledge the contribution(s) of the
|
|
||||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
||||||
permission.
|
|
||||||
|
|
||||||
5) The Font Software, modified or unmodified, in part or in whole,
|
|
||||||
must be distributed entirely under this license, and must not be
|
|
||||||
distributed under any other license. The requirement for fonts to
|
|
||||||
remain under this license does not apply to any document created using
|
|
||||||
the Font Software.
|
|
||||||
|
|
||||||
TERMINATION
|
|
||||||
This license becomes null and void if any of the above conditions are
|
|
||||||
not met.
|
|
||||||
|
|
||||||
DISCLAIMER
|
|
||||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
||||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
||||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
||||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
||||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
||||||
Binary file not shown.
@@ -1,36 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="font_data_dynamic"
|
|
||||||
type="FontFile"
|
|
||||||
uid="uid://c325cbuhy0n3d"
|
|
||||||
path="res://.godot/imported/NotoSansJP-Medium.otf-48029a81cfbb1e3a1e09d84067c2726b.fontdata"
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://fonts/NotoSansJP-Medium.otf"
|
|
||||||
dest_files=["res://.godot/imported/NotoSansJP-Medium.otf-48029a81cfbb1e3a1e09d84067c2726b.fontdata"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
Rendering=null
|
|
||||||
antialiasing=1
|
|
||||||
generate_mipmaps=false
|
|
||||||
disable_embedded_bitmaps=true
|
|
||||||
multichannel_signed_distance_field=false
|
|
||||||
msdf_pixel_range=8
|
|
||||||
msdf_size=48
|
|
||||||
allow_system_fallback=true
|
|
||||||
force_autohinter=false
|
|
||||||
modulate_color_glyphs=false
|
|
||||||
hinting=1
|
|
||||||
subpixel_positioning=1
|
|
||||||
keep_rounding_remainders=true
|
|
||||||
oversampling=0.0
|
|
||||||
Fallbacks=null
|
|
||||||
fallbacks=[]
|
|
||||||
Compress=null
|
|
||||||
compress=true
|
|
||||||
preload=[]
|
|
||||||
language_support={}
|
|
||||||
script_support={}
|
|
||||||
opentype_features={}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)
|
|
||||||
|
|
||||||
-----------------------------------------------------------
|
|
||||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
||||||
-----------------------------------------------------------
|
|
||||||
|
|
||||||
PREAMBLE
|
|
||||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
||||||
development of collaborative font projects, to support the font
|
|
||||||
creation efforts of academic and linguistic communities, and to
|
|
||||||
provide a free and open framework in which fonts may be shared and
|
|
||||||
improved in partnership with others.
|
|
||||||
|
|
||||||
The OFL allows the licensed fonts to be used, studied, modified and
|
|
||||||
redistributed freely as long as they are not sold by themselves. The
|
|
||||||
fonts, including any derivative works, can be bundled, embedded,
|
|
||||||
redistributed and/or sold with any software provided that any reserved
|
|
||||||
names are not used by derivative works. The fonts and derivatives,
|
|
||||||
however, cannot be released under any other type of license. The
|
|
||||||
requirement for fonts to remain under this license does not apply to
|
|
||||||
any document created using the fonts or their derivatives.
|
|
||||||
|
|
||||||
DEFINITIONS
|
|
||||||
"Font Software" refers to the set of files released by the Copyright
|
|
||||||
Holder(s) under this license and clearly marked as such. This may
|
|
||||||
include source files, build scripts and documentation.
|
|
||||||
|
|
||||||
"Reserved Font Name" refers to any names specified as such after the
|
|
||||||
copyright statement(s).
|
|
||||||
|
|
||||||
"Original Version" refers to the collection of Font Software
|
|
||||||
components as distributed by the Copyright Holder(s).
|
|
||||||
|
|
||||||
"Modified Version" refers to any derivative made by adding to,
|
|
||||||
deleting, or substituting -- in part or in whole -- any of the
|
|
||||||
components of the Original Version, by changing formats or by porting
|
|
||||||
the Font Software to a new environment.
|
|
||||||
|
|
||||||
"Author" refers to any designer, engineer, programmer, technical
|
|
||||||
writer or other person who contributed to the Font Software.
|
|
||||||
|
|
||||||
PERMISSION & CONDITIONS
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
|
||||||
modify, redistribute, and sell modified and unmodified copies of the
|
|
||||||
Font Software, subject to the following conditions:
|
|
||||||
|
|
||||||
1) Neither the Font Software nor any of its individual components, in
|
|
||||||
Original or Modified Versions, may be sold by itself.
|
|
||||||
|
|
||||||
2) Original or Modified Versions of the Font Software may be bundled,
|
|
||||||
redistributed and/or sold with any software, provided that each copy
|
|
||||||
contains the above copyright notice and this license. These can be
|
|
||||||
included either as stand-alone text files, human-readable headers or
|
|
||||||
in the appropriate machine-readable metadata fields within text or
|
|
||||||
binary files as long as those fields can be easily viewed by the user.
|
|
||||||
|
|
||||||
3) No Modified Version of the Font Software may use the Reserved Font
|
|
||||||
Name(s) unless explicit written permission is granted by the
|
|
||||||
corresponding Copyright Holder. This restriction only applies to the
|
|
||||||
primary font name as presented to the users.
|
|
||||||
|
|
||||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
||||||
Software shall not be used to promote, endorse or advertise any
|
|
||||||
Modified Version, except to acknowledge the contribution(s) of the
|
|
||||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
||||||
permission.
|
|
||||||
|
|
||||||
5) The Font Software, modified or unmodified, in part or in whole,
|
|
||||||
must be distributed entirely under this license, and must not be
|
|
||||||
distributed under any other license. The requirement for fonts to
|
|
||||||
remain under this license does not apply to any document created using
|
|
||||||
the Font Software.
|
|
||||||
|
|
||||||
TERMINATION
|
|
||||||
This license becomes null and void if any of the above conditions are
|
|
||||||
not met.
|
|
||||||
|
|
||||||
DISCLAIMER
|
|
||||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
||||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
||||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
||||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
||||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
||||||
Binary file not shown.
@@ -1,36 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="font_data_dynamic"
|
|
||||||
type="FontFile"
|
|
||||||
uid="uid://bu247t5hvmk2w"
|
|
||||||
path="res://.godot/imported/OpenSans-SemiBold.woff2-035244bfb0cae573e35cba9e3c76e652.fontdata"
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://fonts/OpenSans-SemiBold.woff2"
|
|
||||||
dest_files=["res://.godot/imported/OpenSans-SemiBold.woff2-035244bfb0cae573e35cba9e3c76e652.fontdata"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
Rendering=null
|
|
||||||
antialiasing=1
|
|
||||||
generate_mipmaps=false
|
|
||||||
disable_embedded_bitmaps=true
|
|
||||||
multichannel_signed_distance_field=false
|
|
||||||
msdf_pixel_range=8
|
|
||||||
msdf_size=48
|
|
||||||
allow_system_fallback=true
|
|
||||||
force_autohinter=false
|
|
||||||
modulate_color_glyphs=false
|
|
||||||
hinting=1
|
|
||||||
subpixel_positioning=1
|
|
||||||
keep_rounding_remainders=true
|
|
||||||
oversampling=0.0
|
|
||||||
Fallbacks=null
|
|
||||||
fallbacks=[Resource("uid://c325cbuhy0n3d", "res://fonts/NotoSansJP-Medium.otf")]
|
|
||||||
Compress=null
|
|
||||||
compress=true
|
|
||||||
preload=[]
|
|
||||||
language_support={}
|
|
||||||
script_support={}
|
|
||||||
opentype_features={}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# Fonts
|
|
||||||
|
|
||||||
The theme's default font (`themes/main_theme.tres`) is `OpenSans-SemiBold.woff2`, the
|
|
||||||
font Godot uses when a project sets none, saved from Godot so the game looks the same.
|
|
||||||
Text it has no letters for, such as Japanese, falls back to `NotoSansJP-Medium.otf`.
|
|
||||||
|
|
||||||
Both are under the SIL Open Font License, see `OpenSans-LICENSE.txt` and
|
|
||||||
`NotoSansJP-LICENSE.txt`.
|
|
||||||
|
|
||||||
`NotoSansJP-Medium.otf` is the Medium weight from
|
|
||||||
[noto-cjk](https://github.com/notofonts/noto-cjk/tree/main/Sans/SubsetOTF/JP), changed in
|
|
||||||
one way: its line metrics (`hhea` ascent and descent) are set to Open Sans's. Godot makes
|
|
||||||
every line as tall as the tallest font in the fallback chain, so with Noto's own, taller
|
|
||||||
metrics all text in the game would move down and rows would grow, even without any
|
|
||||||
Japanese on screen. The glyphs are unchanged. To update the font, download it again and
|
|
||||||
set the metrics with [fontTools](https://github.com/fonttools/fonttools):
|
|
||||||
|
|
||||||
```python
|
|
||||||
from fontTools.ttLib import TTFont
|
|
||||||
|
|
||||||
reference = TTFont("OpenSans-SemiBold.woff2") # needs the brotli package
|
|
||||||
noto = TTFont("NotoSansJP-Medium.otf")
|
|
||||||
scale = noto["head"].unitsPerEm / reference["head"].unitsPerEm
|
|
||||||
noto["hhea"].ascent = round(reference["hhea"].ascent * scale)
|
|
||||||
noto["hhea"].descent = round(reference["hhea"].descent * scale)
|
|
||||||
noto["hhea"].lineGap = 0
|
|
||||||
noto.save("NotoSansJP-Medium.otf")
|
|
||||||
```
|
|
||||||
|
|
||||||
`tests/integration/test_theme.gd` checks that Japanese has a font and that the line
|
|
||||||
height is still Godot's.
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
class_name HttpTransport
|
||||||
|
extends RefCounted
|
||||||
|
## Sends HTTP requests for [code]MusicServerClient[/code] using [HTTPRequest] nodes.
|
||||||
|
##
|
||||||
|
## Tests subclass this to answer requests without a network connection.
|
||||||
|
|
||||||
|
## 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 _owner: Node
|
||||||
|
|
||||||
|
|
||||||
|
func _init(owner: Node) -> void:
|
||||||
|
_owner = owner
|
||||||
|
|
||||||
|
|
||||||
|
## Requests [param url] and calls [param on_complete] with
|
||||||
|
## [code](response_code: int, body: PackedByteArray)[/code]. Connection errors and
|
||||||
|
## timeouts are reported with response code 0.
|
||||||
|
func request(url: String, method: int, on_complete: Callable) -> void:
|
||||||
|
var http_request: HTTPRequest = HTTPRequest.new()
|
||||||
|
http_request.timeout = timeout_seconds
|
||||||
|
_owner.add_child(http_request)
|
||||||
|
http_request.request_completed.connect(
|
||||||
|
func(
|
||||||
|
result: int,
|
||||||
|
response_code: int,
|
||||||
|
_headers: PackedStringArray,
|
||||||
|
body: PackedByteArray,
|
||||||
|
) -> void:
|
||||||
|
http_request.queue_free()
|
||||||
|
on_complete.call(response_code if result == HTTPRequest.RESULT_SUCCESS else 0, body)
|
||||||
|
)
|
||||||
|
var headers: PackedStringArray = []
|
||||||
|
if method == HTTPClient.METHOD_PUT:
|
||||||
|
headers.append("Content-Type: application/json")
|
||||||
|
var error: int = http_request.request(url, headers, method)
|
||||||
|
if error != OK:
|
||||||
|
http_request.queue_free()
|
||||||
|
push_error("Could not send request to %s (error %d)" % [url, error])
|
||||||
|
on_complete.call(0, PackedByteArray())
|
||||||
File renamed without changes.
@@ -1,116 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
<svg height="128" width="128" xmlns="http://www.w3.org/2000/svg"><g transform="translate(32 32)"><path d="m-16-32c-8.86 0-16 7.13-16 15.99v95.98c0 8.86 7.13 15.99 16 15.99h96c8.86 0 16-7.13 16-15.99v-95.98c0-8.85-7.14-15.99-16-15.99z" fill="#363d52"/><path d="m-16-32c-8.86 0-16 7.13-16 15.99v95.98c0 8.86 7.13 15.99 16 15.99h96c8.86 0 16-7.13 16-15.99v-95.98c0-8.85-7.14-15.99-16-15.99zm0 4h96c6.64 0 12 5.35 12 11.99v95.98c0 6.64-5.35 11.99-12 11.99h-96c-6.64 0-12-5.35-12-11.99v-95.98c0-6.64 5.36-11.99 12-11.99z" fill-opacity=".4"/></g><g stroke-width="9.92746" transform="matrix(.10073078 0 0 .10073078 12.425923 2.256365)"><path d="m0 0s-.325 1.994-.515 1.976l-36.182-3.491c-2.879-.278-5.115-2.574-5.317-5.459l-.994-14.247-27.992-1.997-1.904 12.912c-.424 2.872-2.932 5.037-5.835 5.037h-38.188c-2.902 0-5.41-2.165-5.834-5.037l-1.905-12.912-27.992 1.997-.994 14.247c-.202 2.886-2.438 5.182-5.317 5.46l-36.2 3.49c-.187.018-.324-1.978-.511-1.978l-.049-7.83 30.658-4.944 1.004-14.374c.203-2.91 2.551-5.263 5.463-5.472l38.551-2.75c.146-.01.29-.016.434-.016 2.897 0 5.401 2.166 5.825 5.038l1.959 13.286h28.005l1.959-13.286c.423-2.871 2.93-5.037 5.831-5.037.142 0 .284.005.423.015l38.556 2.75c2.911.209 5.26 2.562 5.463 5.472l1.003 14.374 30.645 4.966z" fill="#fff" transform="matrix(4.162611 0 0 -4.162611 919.24059 771.67186)"/><path d="m0 0v-47.514-6.035-5.492c.108-.001.216-.005.323-.015l36.196-3.49c1.896-.183 3.382-1.709 3.514-3.609l1.116-15.978 31.574-2.253 2.175 14.747c.282 1.912 1.922 3.329 3.856 3.329h38.188c1.933 0 3.573-1.417 3.855-3.329l2.175-14.747 31.575 2.253 1.115 15.978c.133 1.9 1.618 3.425 3.514 3.609l36.182 3.49c.107.01.214.014.322.015v4.711l.015.005v54.325c5.09692 6.4164715 9.92323 13.494208 13.621 19.449-5.651 9.62-12.575 18.217-19.976 26.182-6.864-3.455-13.531-7.369-19.828-11.534-3.151 3.132-6.7 5.694-10.186 8.372-3.425 2.751-7.285 4.768-10.946 7.118 1.09 8.117 1.629 16.108 1.846 24.448-9.446 4.754-19.519 7.906-29.708 10.17-4.068-6.837-7.788-14.241-11.028-21.479-3.842.642-7.702.88-11.567.926v.006c-.027 0-.052-.006-.075-.006-.024 0-.049.006-.073.006v-.006c-3.872-.046-7.729-.284-11.572-.926-3.238 7.238-6.956 14.642-11.03 21.479-10.184-2.264-20.258-5.416-29.703-10.17.216-8.34.755-16.331 1.848-24.448-3.668-2.35-7.523-4.367-10.949-7.118-3.481-2.678-7.036-5.24-10.188-8.372-6.297 4.165-12.962 8.079-19.828 11.534-7.401-7.965-14.321-16.562-19.974-26.182 4.4426579-6.973692 9.2079702-13.9828876 13.621-19.449z" fill="#478cbf" transform="matrix(4.162611 0 0 -4.162611 104.69892 525.90697)"/><path d="m0 0-1.121-16.063c-.135-1.936-1.675-3.477-3.611-3.616l-38.555-2.751c-.094-.007-.188-.01-.281-.01-1.916 0-3.569 1.406-3.852 3.33l-2.211 14.994h-31.459l-2.211-14.994c-.297-2.018-2.101-3.469-4.133-3.32l-38.555 2.751c-1.936.139-3.476 1.68-3.611 3.616l-1.121 16.063-32.547 3.138c.015-3.498.06-7.33.06-8.093 0-34.374 43.605-50.896 97.781-51.086h.066.067c54.176.19 97.766 16.712 97.766 51.086 0 .777.047 4.593.063 8.093z" fill="#478cbf" transform="matrix(4.162611 0 0 -4.162611 784.07144 817.24284)"/><path d="m0 0c0-12.052-9.765-21.815-21.813-21.815-12.042 0-21.81 9.763-21.81 21.815 0 12.044 9.768 21.802 21.81 21.802 12.048 0 21.813-9.758 21.813-21.802" fill="#fff" transform="matrix(4.162611 0 0 -4.162611 389.21484 625.67104)"/><path d="m0 0c0-7.994-6.479-14.473-14.479-14.473-7.996 0-14.479 6.479-14.479 14.473s6.483 14.479 14.479 14.479c8 0 14.479-6.485 14.479-14.479" fill="#414042" transform="matrix(4.162611 0 0 -4.162611 367.36686 631.05679)"/><path d="m0 0c-3.878 0-7.021 2.858-7.021 6.381v20.081c0 3.52 3.143 6.381 7.021 6.381s7.028-2.861 7.028-6.381v-20.081c0-3.523-3.15-6.381-7.028-6.381" fill="#fff" transform="matrix(4.162611 0 0 -4.162611 511.99336 724.73954)"/><path d="m0 0c0-12.052 9.765-21.815 21.815-21.815 12.041 0 21.808 9.763 21.808 21.815 0 12.044-9.767 21.802-21.808 21.802-12.05 0-21.815-9.758-21.815-21.802" fill="#fff" transform="matrix(4.162611 0 0 -4.162611 634.78706 625.67104)"/><path d="m0 0c0-7.994 6.477-14.473 14.471-14.473 8.002 0 14.479 6.479 14.479 14.473s-6.477 14.479-14.479 14.479c-7.994 0-14.471-6.485-14.471-14.479" fill="#414042" transform="matrix(4.162611 0 0 -4.162611 656.64056 631.05679)"/></g></svg>
|
||||||
<defs>
|
|
||||||
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#6fb1d6"/>
|
|
||||||
<stop offset="1" stop-color="#2e6a94"/>
|
|
||||||
</linearGradient>
|
|
||||||
<radialGradient id="plate" cx="0.42" cy="0.38" r="0.7">
|
|
||||||
<stop offset="0" stop-color="#ffffff"/>
|
|
||||||
<stop offset="1" stop-color="#e3ded6"/>
|
|
||||||
</radialGradient>
|
|
||||||
<radialGradient id="well" cx="0.45" cy="0.4" r="0.7">
|
|
||||||
<stop offset="0" stop-color="#fbf9f5"/>
|
|
||||||
<stop offset="1" stop-color="#e9e3d9"/>
|
|
||||||
</radialGradient>
|
|
||||||
<linearGradient id="band" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#4a4a4a"/>
|
|
||||||
<stop offset="1" stop-color="#1f1f1f"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="cup" x1="0" y1="0" x2="1" y2="1">
|
|
||||||
<stop offset="0" stop-color="#3d3d3d"/>
|
|
||||||
<stop offset="1" stop-color="#151515"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="cookie" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#d99a52"/>
|
|
||||||
<stop offset="1" stop-color="#b8742f"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="falu" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#c63b2f"/>
|
|
||||||
<stop offset="1" stop-color="#9e2a21"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Background -->
|
|
||||||
<rect x="16" y="16" width="480" height="480" rx="108" fill="url(#sky)"/>
|
|
||||||
|
|
||||||
<!-- Plate shadow, plate, rim band and well -->
|
|
||||||
<ellipse cx="262" cy="306" rx="178" ry="174" fill="#1d4a6b" opacity="0.35"/>
|
|
||||||
<circle cx="256" cy="294" r="172" fill="url(#plate)" stroke="#d6d0c6" stroke-width="3"/>
|
|
||||||
<circle cx="256" cy="294" r="150" fill="none" stroke="#3b6ea5" stroke-width="7"/>
|
|
||||||
<circle cx="256" cy="294" r="139" fill="none" stroke="#3b6ea5" stroke-width="2"/>
|
|
||||||
<circle cx="256" cy="294" r="116" fill="url(#well)" stroke="#ddd6cb" stroke-width="2"/>
|
|
||||||
|
|
||||||
<!-- Headphone band arching over the plate -->
|
|
||||||
<path d="M 92 286 A 164 164 0 0 1 420 286" fill="none" stroke="url(#band)" stroke-width="26" stroke-linecap="round"/>
|
|
||||||
<path d="M 104 262 A 152 152 0 0 1 408 262" fill="none" stroke="#6a6a6a" stroke-width="4" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
|
|
||||||
<!-- Ear cups on the plate's edges -->
|
|
||||||
<g>
|
|
||||||
<rect x="58" y="232" width="62" height="118" rx="28" fill="url(#cup)"/>
|
|
||||||
<rect x="104" y="246" width="28" height="90" rx="14" fill="#2c2c2c" stroke="#ff8c1a" stroke-width="4"/>
|
|
||||||
<rect x="70" y="252" width="10" height="56" rx="5" fill="#ffffff" opacity="0.15"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<rect x="392" y="232" width="62" height="118" rx="28" fill="url(#cup)"/>
|
|
||||||
<rect x="380" y="246" width="28" height="90" rx="14" fill="#2c2c2c" stroke="#ff8c1a" stroke-width="4"/>
|
|
||||||
<rect x="404" y="252" width="10" height="56" rx="5" fill="#ffffff" opacity="0.15"/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- Crumbs -->
|
|
||||||
<g fill="#c98a45">
|
|
||||||
<circle cx="178" cy="370" r="4"/>
|
|
||||||
<circle cx="190" cy="381" r="2.5"/>
|
|
||||||
<circle cx="336" cy="378" r="3.5"/>
|
|
||||||
<circle cx="326" cy="214" r="2.5"/>
|
|
||||||
<circle cx="184" cy="226" r="3"/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- House-shaped cookie: thick baked edge, then falu red icing -->
|
|
||||||
<g transform="translate(256 288) scale(1.12) translate(-256 -288)">
|
|
||||||
<!-- shadow -->
|
|
||||||
<path d="M 256 192 L 344 270 L 334 270 L 334 368 L 178 368 L 178 270 L 168 270 Z"
|
|
||||||
fill="#7a4a1e" opacity="0.28" stroke="#7a4a1e" stroke-width="16" stroke-linejoin="round"
|
|
||||||
transform="translate(5 7)"/>
|
|
||||||
<!-- chimney, baked -->
|
|
||||||
<rect x="290" y="206" width="22" height="40" rx="6" fill="#c98a45" stroke="#c98a45" stroke-width="10" stroke-linejoin="round"/>
|
|
||||||
<!-- cookie body with a thick, rounded baked edge -->
|
|
||||||
<path d="M 256 192 L 344 270 L 334 270 L 334 368 L 178 368 L 178 270 L 168 270 Z"
|
|
||||||
fill="url(#cookie)" stroke="#c98a45" stroke-width="16" stroke-linejoin="round"/>
|
|
||||||
<path d="M 256 192 L 344 270 L 334 270 L 334 368 L 178 368 L 178 270 L 168 270 Z"
|
|
||||||
fill="none" stroke="#a8682a" stroke-width="2" stroke-linejoin="round" transform="translate(0 0)" opacity="0.6"/>
|
|
||||||
<rect x="295" y="214" width="12" height="26" rx="2" fill="#3a3a3a"/>
|
|
||||||
|
|
||||||
<!-- roof icing -->
|
|
||||||
<path d="M 256 210 L 324 270 L 188 270 Z" fill="#3e3e46"/>
|
|
||||||
<!-- walls icing, falu red -->
|
|
||||||
<rect x="190" y="272" width="132" height="86" rx="4" fill="url(#falu)"/>
|
|
||||||
<!-- white roof edge piping -->
|
|
||||||
<path d="M 182 274 L 256 208 L 330 274" fill="none" stroke="#fdfbf7" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<!-- white corner boards -->
|
|
||||||
<rect x="190" y="272" width="8" height="86" fill="#fdfbf7"/>
|
|
||||||
<rect x="314" y="272" width="8" height="86" fill="#fdfbf7"/>
|
|
||||||
|
|
||||||
<!-- windows with white frames and crosses -->
|
|
||||||
<rect x="210" y="288" width="30" height="30" rx="2" fill="#2f5d86" stroke="#fdfbf7" stroke-width="5"/>
|
|
||||||
<path d="M 225 288 V 318 M 210 303 H 240" stroke="#fdfbf7" stroke-width="3"/>
|
|
||||||
<rect x="272" y="288" width="30" height="30" rx="2" fill="#2f5d86" stroke="#fdfbf7" stroke-width="5"/>
|
|
||||||
<path d="M 287 288 V 318 M 272 303 H 302" stroke="#fdfbf7" stroke-width="3"/>
|
|
||||||
<!-- door with white frame -->
|
|
||||||
<rect x="242" y="320" width="28" height="38" rx="2" fill="#7e1f18" stroke="#fdfbf7" stroke-width="4"/>
|
|
||||||
<circle cx="264" cy="340" r="2.5" fill="#f2c14e"/>
|
|
||||||
<!-- gable window in the roof -->
|
|
||||||
<circle cx="256" cy="246" r="8" fill="#f2c14e" stroke="#fdfbf7" stroke-width="3"/>
|
|
||||||
|
|
||||||
<!-- dotted icing along the cookie's bottom edge -->
|
|
||||||
<g fill="#fdfbf7">
|
|
||||||
<circle cx="196" cy="368" r="3"/>
|
|
||||||
<circle cx="214" cy="368" r="3"/>
|
|
||||||
<circle cx="232" cy="368" r="3"/>
|
|
||||||
<circle cx="250" cy="368" r="3"/>
|
|
||||||
<circle cx="268" cy="368" r="3"/>
|
|
||||||
<circle cx="286" cy="368" r="3"/>
|
|
||||||
<circle cx="304" cy="368" r="3"/>
|
|
||||||
<circle cx="322" cy="368" r="3"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 4.1 KiB |
@@ -18,8 +18,6 @@ dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.cte
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
compress/uastc_level=0
|
|
||||||
compress/rdo_quality_loss=0.0
|
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -27,10 +25,6 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
process/channel_remap/red=0
|
|
||||||
process/channel_remap/green=1
|
|
||||||
process/channel_remap/blue=2
|
|
||||||
process/channel_remap/alpha=3
|
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="416" height="416" viewBox="48 48 416 416">
|
|
||||||
<!-- The pause shape of pause_icon_light.svg in one colour -->
|
|
||||||
<path fill="#f9f9f9" d="M224,435.8V76.1c0-6.7-5.4-12.1-12.2-12.1h-71.6c-6.8,0-12.2,5.4-12.2,12.1v359.7c0,6.7,5.4,12.2,12.2,12.2h71.6 C218.6,448,224,442.6,224,435.8z"/>
|
|
||||||
<path fill="#f9f9f9" d="M371.8,64h-71.6c-6.7,0-12.2,5.4-12.2,12.1v359.7c0,6.7,5.4,12.2,12.2,12.2h71.6c6.7,0,12.2-5.4,12.2-12.2V76.1 C384,69.4,378.6,64,371.8,64z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 502 B |
@@ -1,43 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://djdsgbhw8gpj"
|
|
||||||
path="res://.godot/imported/pause_button_icon.svg-cf86876de39506a769c5610b329fdb75.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://icons/pause_button_icon.svg"
|
|
||||||
dest_files=["res://.godot/imported/pause_button_icon.svg-cf86876de39506a769c5610b329fdb75.ctex"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
compress/mode=0
|
|
||||||
compress/high_quality=false
|
|
||||||
compress/lossy_quality=0.7
|
|
||||||
compress/uastc_level=0
|
|
||||||
compress/rdo_quality_loss=0.0
|
|
||||||
compress/hdr_compression=1
|
|
||||||
compress/normal_map=0
|
|
||||||
compress/channel_pack=0
|
|
||||||
mipmaps/generate=false
|
|
||||||
mipmaps/limit=-1
|
|
||||||
roughness/mode=0
|
|
||||||
roughness/src_normal=""
|
|
||||||
process/channel_remap/red=0
|
|
||||||
process/channel_remap/green=1
|
|
||||||
process/channel_remap/blue=2
|
|
||||||
process/channel_remap/alpha=3
|
|
||||||
process/fix_alpha_border=true
|
|
||||||
process/premult_alpha=false
|
|
||||||
process/normal_map_invert_y=false
|
|
||||||
process/hdr_as_srgb=false
|
|
||||||
process/hdr_clamp_exposure=false
|
|
||||||
process/size_limit=0
|
|
||||||
detect_3d/compress_to=1
|
|
||||||
svg/scale=1.0
|
|
||||||
editor/scale_with_editor_scale=false
|
|
||||||
editor/convert_colors_with_editor_theme=false
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="416" height="416" viewBox="64 48 416 416">
|
|
||||||
<!-- The play shape of play_icon_light.svg in one colour, shifted right so it looks centred -->
|
|
||||||
<path fill="#f9f9f9" transform="translate(16 0)" d="M405.2,232.9L126.8,67.2c-3.4-2-6.9-3.2-10.9-3.2c-10.9,0-19.8,9-19.8,20H96v344h0.1c0,11,8.9,20,19.8,20 c4.1,0,7.5-1.4,11.2-3.4l278.1-165.5c6.6-5.5,10.8-13.8,10.8-23.1C416,246.7,411.8,238.5,405.2,232.9z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 454 B |
@@ -1,43 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://dkweun1f426gl"
|
|
||||||
path="res://.godot/imported/play_button_icon.svg-76b19c7150b176f215ad401b4dd9488d.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://icons/play_button_icon.svg"
|
|
||||||
dest_files=["res://.godot/imported/play_button_icon.svg-76b19c7150b176f215ad401b4dd9488d.ctex"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
compress/mode=0
|
|
||||||
compress/high_quality=false
|
|
||||||
compress/lossy_quality=0.7
|
|
||||||
compress/uastc_level=0
|
|
||||||
compress/rdo_quality_loss=0.0
|
|
||||||
compress/hdr_compression=1
|
|
||||||
compress/normal_map=0
|
|
||||||
compress/channel_pack=0
|
|
||||||
mipmaps/generate=false
|
|
||||||
mipmaps/limit=-1
|
|
||||||
roughness/mode=0
|
|
||||||
roughness/src_normal=""
|
|
||||||
process/channel_remap/red=0
|
|
||||||
process/channel_remap/green=1
|
|
||||||
process/channel_remap/blue=2
|
|
||||||
process/channel_remap/alpha=3
|
|
||||||
process/fix_alpha_border=true
|
|
||||||
process/premult_alpha=false
|
|
||||||
process/normal_map_invert_y=false
|
|
||||||
process/hdr_as_srgb=false
|
|
||||||
process/hdr_clamp_exposure=false
|
|
||||||
process/size_limit=0
|
|
||||||
detect_3d/compress_to=1
|
|
||||||
svg/scale=1.0
|
|
||||||
editor/scale_with_editor_scale=false
|
|
||||||
editor/convert_colors_with_editor_theme=false
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28">
|
|
||||||
<!-- reload_light_icon.svg mirrored, so the arrow turns back like a restart -->
|
|
||||||
<path fill="#f9f9f9" transform="translate(28 0) scale(-1 1)" d="M 12.273759,27.821814 C 8.301606,27.305128 4.6414458,25.025364 2.4172439,21.682595 -0.6740156,17.036719 -0.6987372,11.050866 2.3541959,6.4167276 6.4831292,0.1492961 14.7875,-1.7884148 21.095349,2.0437368 l 0.883303,0.5366251 0.09577,-0.6963111 c 0.07593,-0.5520493 0.198918,-0.7994615 0.593645,-1.1941885 0.682393,-0.6823936 1.29683,-0.7876518 2.135162,-0.3657721 1.11162,0.5594083 1.142977,0.6968977 1.099029,4.8188242 C 25.866598,8.48735 25.848168,8.714727 25.583585,9.075118 24.993194,9.879284 24.711316,9.931761 20.980939,9.931993 17.03905,9.932238 16.871859,9.891713 16.324094,8.803226 15.94065,8.041272 16.00249,7.4396205 16.528498,6.8144942 17.041282,6.2050862 17.302265,6.1106614 18.732442,6.0170974 l 1.29765,-0.084894 -0.711421,-0.4557502 C 16.407302,3.611373 12.42501,3.4754273 9.250891,5.1327637 6.9185825,6.3505563 5.186242,8.47902 4.3514391,11.152542 c -0.4596099,1.471937 -0.4596099,4.222979 0,5.694916 1.0532717,3.373185 3.4279179,5.747831 6.8011029,6.801103 0.874696,0.273122 1.291973,0.316826 2.983051,0.31243 1.791555,-0.0047 2.062404,-0.03855 3.050848,-0.381822 2.095349,-0.727675 4.004905,-2.225187 5.276499,-4.137935 0.712133,-1.071201 1.028577,-1.77101 1.500599,-3.318525 0.234713,-0.769505 0.466198,-1.242244 0.733848,-1.49867 0.768275,-0.736054 1.953395,-0.694987 2.67526,0.0927 0.56162,0.612834 0.678351,1.318164 0.398766,2.409492 -0.7664,2.991551 -3.210883,6.444937 -5.916304,8.35812 -2.688606,1.901292 -6.251776,2.77056 -9.58135,2.337459 z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
Loaded 100 of 412 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user