Fix and tidy the Character select, Shortcuts, Log and QR popups
Build / test (push) Successful in 41s
Build / build (push) Successful in 1m13s

Character select
- A card only picks its character when it is clicked. Before, every card
  watched every click, so clicking below the panel could pick a card
  scrolled out of view there.
- Six columns of fixed-size cards that scroll for any number of
  characters, with a gap between the cards and the scrollbar.
- Pictures are read as PNG, JPG or WebP by their first bytes, so JPGs no
  longer print an error. Names are split into words ("Paper Mario") and
  the card under the mouse is highlighted.

Shortcuts
- Alt+Z restarts the song; it was listed as "Reset".
- Grouped by what the keys do, in sentence case, and the panel fits the
  list. A test checks that every button shortcut is listed.

Log
- Scrolls to the newest row, alternates row colours and says "Nothing
  has happened yet" when empty.

QR
- A title, and the code is set up in one place.
This commit is contained in:
Sansan committed 2026-10-07 15:24:43 +02:00
1 parent b2d547b650
commit 8d21b0617d
23 files changed
+4694 -5349

No files matched your search

+21 -22
View File
@@ -3,40 +3,39 @@
[ext_resource type="Script" uid="uid://i35b3scbpwpu" path="res://popups/character_card.gd" id="1_pjt0s"]
[ext_resource type="Texture2D" uid="uid://cv3hdd6bsruau" path="res://icon.svg" id="2_pjt0s"]
[node name="CharacterCard" type="Control"]
layout_mode = 3
anchors_preset = 0
[node name="CharacterCard" type="Panel"]
custom_minimum_size = Vector2(240, 150)
mouse_default_cursor_shape = 2
theme_type_variation = &"CharacterCard"
script = ExtResource("1_pjt0s")
[node name="Panel" type="Panel" parent="."]
layout_mode = 0
offset_right = 240.0
offset_bottom = 145.0
theme_type_variation = &"CharacterCard"
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 10.0
offset_top = 10.0
offset_right = -10.0
offset_bottom = -6.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
[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"]
[node name="CharacterTextureRect" type="TextureRect" parent="VBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(200, 100)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
mouse_filter = 2
texture = ExtResource("2_pjt0s")
expand_mode = 3
expand_mode = 1
stretch_mode = 5
[node name="CharacterName" type="Label" parent="Panel/MarginContainer/VBoxContainer"]
[node name="CharacterName" type="Label" parent="VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Aloy"
horizontal_alignment = 1
text_overrun_behavior = 3
+27 -30
View File
@@ -11,45 +11,42 @@ panel = NodePath("CharacterPanel")
[node name="CharacterPanel" type="Panel" parent="."]
unique_name_in_owner = true
layout_mode = 0
offset_right = 1290.0
offset_bottom = 665.0
offset_right = 1580.0
offset_bottom = 720.0
theme_type_variation = &"PopupBackground"
[node name="VBoxContainer" type="VBoxContainer" parent="CharacterPanel"]
layout_mode = 0
offset_right = 1290.0
offset_bottom = 659.0
size_flags_horizontal = 3
[node name="TitleLabel" type="Label" parent="CharacterPanel/VBoxContainer"]
[node name="TitleLabel" type="Label" parent="CharacterPanel"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 0
layout_mode = 0
offset_right = 1580.0
offset_bottom = 56.0
theme_type_variation = &"Title"
text = "Character Select"
horizontal_alignment = 1
vertical_alignment = 1
[node name="MarginContainer" type="MarginContainer" parent="CharacterPanel/VBoxContainer"]
layout_mode = 2
theme_override_constants/margin_left = 20
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 20
theme_override_constants/margin_bottom = 10
[node name="ScrollContainer" type="ScrollContainer" parent="CharacterPanel/VBoxContainer/MarginContainer"]
custom_minimum_size = Vector2(0, 600)
layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
[node name="CharacterGridContainer" type="GridContainer" parent="CharacterPanel/VBoxContainer/MarginContainer/ScrollContainer"]
[node name="ScrollContainer" type="ScrollContainer" parent="CharacterPanel"]
unique_name_in_owner = true
custom_minimum_size = Vector2(1150, 600)
layout_mode = 0
offset_left = 24.0
offset_top = 60.0
offset_right = 1564.0
offset_bottom = 704.0
horizontal_scroll_mode = 0
vertical_scroll_mode = 2
[node name="ScrollbarGap" type="MarginContainer" parent="CharacterPanel/ScrollContainer"]
layout_mode = 2
size_flags_vertical = 3
theme_override_constants/h_separation = 10
theme_override_constants/v_separation = 10
columns = 5
size_flags_horizontal = 3
theme_override_constants/margin_right = 24
[node name="CharacterGridContainer" type="GridContainer" parent="CharacterPanel/ScrollContainer/ScrollbarGap"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 0
theme_override_constants/h_separation = 12
theme_override_constants/v_separation = 12
columns = 6
[node name="CloseTimer" type="Timer" parent="CharacterPanel"]
unique_name_in_owner = true
+12
View File
@@ -31,7 +31,19 @@ theme_type_variation = &"Title"
text = "Log"
horizontal_alignment = 1
[node name="EmptyLabel" type="Label" parent="LogPanel"]
unique_name_in_owner = true
layout_mode = 0
offset_left = 20.0
offset_top = 80.0
offset_right = 680.0
offset_bottom = 110.0
theme_type_variation = &"TextLarge"
text = "Nothing has happened yet"
horizontal_alignment = 1
[node name="LogScrollContainer" type="ScrollContainer" parent="LogPanel"]
unique_name_in_owner = true
layout_mode = 0
offset_left = 20.0
offset_top = 56.0
+15 -5
View File
@@ -17,16 +17,26 @@ panel = NodePath("QRPanel")
unique_name_in_owner = true
layout_mode = 0
offset_right = 450.0
offset_bottom = 520.0
offset_bottom = 540.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]
unique_name_in_owner = true
layout_mode = 0
offset_left = 20.0
offset_top = 20.0
offset_top = 60.0
offset_right = 430.0
offset_bottom = 430.0
offset_bottom = 470.0
script = ExtResource("2_jpsxt")
auto_update = true
mode = 4
@@ -47,9 +57,9 @@ metadata/_custom_type_script = "uid://dynulhnyq5hpq"
unique_name_in_owner = true
layout_mode = 0
offset_left = 20.0
offset_top = 450.0
offset_top = 482.0
offset_right = 430.0
offset_bottom = 490.0
offset_bottom = 522.0
text = "URL"
horizontal_alignment = 1
vertical_alignment = 1
+17 -24
View File
@@ -3,7 +3,6 @@
[ext_resource type="Script" uid="uid://rolypd0cqd6h" path="res://popups/shortcuts_popup.gd" id="1_12pxi"]
[node name="ShortcutsPopup" type="Control" node_paths=PackedStringArray("panel")]
custom_minimum_size = Vector2(700, 700)
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
@@ -15,35 +14,29 @@ panel = NodePath("ShortcutsPanel")
[node name="ShortcutsPanel" type="Panel" parent="."]
unique_name_in_owner = true
custom_minimum_size = Vector2(700, 700)
layout_mode = 0
offset_right = 40.0
offset_bottom = 40.0
offset_right = 700.0
offset_bottom = 706.0
theme_type_variation = &"PopupBackground"
[node name="ScrollContainer" type="ScrollContainer" parent="ShortcutsPanel"]
custom_minimum_size = Vector2(700, 700)
[node name="TitleLabel" type="Label" parent="ShortcutsPanel"]
layout_mode = 0
offset_right = 40.0
offset_bottom = 40.0
offset_right = 700.0
offset_bottom = 56.0
theme_type_variation = &"Title"
text = "Shortcuts"
horizontal_alignment = 1
vertical_alignment = 1
[node name="ScrollContainer" type="ScrollContainer" parent="ShortcutsPanel"]
layout_mode = 0
offset_left = 24.0
offset_top = 60.0
offset_right = 676.0
offset_bottom = 690.0
horizontal_scroll_mode = 0
[node name="shortcutsVBox" type="VBoxContainer" parent="ShortcutsPanel/ScrollContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(700, 700)
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="ShortcutsPanel/ScrollContainer/shortcutsVBox"]
layout_mode = 2
alignment = 1
[node name="Label" type="Label" parent="ShortcutsPanel/ScrollContainer/shortcutsVBox/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "testar"
horizontal_alignment = 1
[node name="Label2" type="Label" parent="ShortcutsPanel/ScrollContainer/shortcutsVBox/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "testera 2"
horizontal_alignment = 1
+46 -31
View File
@@ -1,48 +1,63 @@
class_name CharacterCard
extends Control
extends Panel
## One character in the character select popup: its picture and name. Clicking it picks
## the character.
## Emitted with the character's picture when the card is clicked.
signal new_character_selected(img_texture: ImageTexture)
@onready var panel: Panel = $Panel
@onready var character_image: TextureRect = %CharacterTextureRect
@onready var character_name_label: Label = %CharacterName
func _input(event: InputEvent) -> void:
if visible && Settings.character_select_open:
if event is InputEventMouseButton && event.is_pressed() \
&& event.get_button_index() == MOUSE_BUTTON_LEFT:
var local_event: InputEvent = make_input_local(event)
if Rect2(Vector2(0, 0), panel.size).has_point(local_event.position):
GameLog.debug(character_name_label.text)
new_character_selected.emit(character_image.texture)
func _ready() -> void:
gui_input.connect(_on_gui_input)
mouse_entered.connect(_set_hovered.bind(true))
mouse_exited.connect(_set_hovered.bind(false))
## Shows [param character_name] and fetches its picture from the server.
func set_character_name(character_name: String) -> void:
panel.size = Vector2(240, 145)
character_name_label.text = character_name.split(".")[0]
character_name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
character_name_label.text = display_name(character_name)
tooltip_text = character_name_label.text
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:
GameLog.debug("Character image is not a PNG, trying JPG: ", error_string(err))
var err2: Error = img.load_jpg_from_buffer(image)
if err2 != OK:
GameLog.warning("Could not load the image for ", character_name, ": ",
error_string(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
var picture: Image = image_from_bytes(image)
if picture == null:
GameLog.warning("Could not load the picture of ", character_name)
return
character_image.texture = ImageTexture.create_from_image(picture)
MusicServerClient.fetch_character_image(character_name, image_fetched)
## The name to show for a picture's [param file_name]: without the file extension, and
## with words that are written together split, e.g. "PaperMario.png" is "Paper Mario".
static func display_name(file_name: String) -> String:
var name_part: String = file_name.get_basename() if "." in file_name else file_name
var words: RegEx = RegEx.create_from_string("([a-z])([A-Z])")
return words.sub(name_part, "$1 $2", true)
## Reads a PNG, JPG or WebP picture, telling them apart by their first bytes so that no
## error is printed for trying the wrong format. Returns null if it is none of them.
static func image_from_bytes(bytes: PackedByteArray) -> Image:
var image: Image = Image.new()
var error: Error = ERR_FILE_UNRECOGNIZED
if bytes.size() >= 4 && bytes.slice(0, 4) == PackedByteArray([0x89, 0x50, 0x4E, 0x47]):
error = image.load_png_from_buffer(bytes)
elif bytes.size() >= 3 && bytes.slice(0, 3) == PackedByteArray([0xFF, 0xD8, 0xFF]):
error = image.load_jpg_from_buffer(bytes)
elif bytes.size() >= 12 && bytes.slice(8, 12).get_string_from_ascii() == "WEBP":
error = image.load_webp_from_buffer(bytes)
return image if error == OK else null
func _set_hovered(hovered: bool) -> void:
theme_type_variation = &"CharacterCardHover" if hovered else &"CharacterCard"
func _on_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton && event.pressed \
&& event.button_index == MOUSE_BUTTON_LEFT && Settings.character_select_open:
accept_event()
GameLog.debug(character_name_label.text)
new_character_selected.emit(character_image.texture)
-2
View File
@@ -33,8 +33,6 @@ func load_characters() -> void:
GameLog.debug("character_name ", character_name)
var character_box: CharacterCard = character.instantiate()
character_grid.add_child(character_box)
character_box.custom_minimum_size = Vector2(235, 145)
character_box.size = Vector2(235, 145)
character_box.set_character_name(character_name)
character_box.new_character_selected.connect(_on_character_selected)
MusicServerClient.fetch_character_list(fetch_character_list)
+24 -14
View File
@@ -1,28 +1,38 @@
class_name LogPopup
extends BasePopup
## The game log: a row with the time for each thing that happens in the quiz, such as a
## player getting a point. Opened with Alt+L.
var log_rows: Array
## player getting a point. Opened with Alt+L. The newest row is at the bottom and is
## scrolled into view.
@onready var log_panel: Panel = %LogPanel
@onready var log_box: VBoxContainer = %LogVBoxContainer
@onready var log_scroll: ScrollContainer = %LogScrollContainer
@onready var empty_label: Label = %EmptyLabel
func _ready() -> void:
visibility_changed.connect(_on_visibility_changed)
func add_log_row(text: String) -> void:
var log_row: String = get_time() + text
log_rows.append(log_row)
log_box.add_child(make_log_label(log_row))
func make_log_label(row: String) -> Label:
var log_label: Label = Label.new()
log_label.theme_type_variation = &"TextLarge"
log_label.text = row
log_label.autowrap_mode = TextServer.AUTOWRAP_WORD
return log_label
var row: int = log_box.get_child_count()
log_box.add_child(ListRow.create(get_time() + text, row, &"TextLarge"))
empty_label.visible = false
_scroll_to_newest.call_deferred()
func get_time() -> String:
var time: Dictionary = Time.get_time_dict_from_system()
return "%02d:%02d:%02d " % [time.hour, time.minute, time.second]
func _on_visibility_changed() -> void:
if visible:
empty_label.visible = log_box.get_child_count() == 0
_scroll_to_newest.call_deferred()
# Deferred, as the list only knows its new height once the new row is laid out
func _scroll_to_newest() -> void:
await get_tree().process_frame
log_scroll.scroll_vertical = int(log_scroll.get_v_scroll_bar().max_value)
+10 -13
View File
@@ -9,21 +9,18 @@ extends BasePopup
func _ready() -> void:
qr_code_rect.light_module_color = Color.WHITE
qr_code_rect.dark_module_color = Color.BLACK
qr_code_rect.quiet_zone_size = 4
# Generate QR code for the search URL
var url: String = Settings.default_path + "/search"
qr_code_rect.data = url
url_label.text = qr_code_rect.data
GameLog.debug("QR code generated for: ", url)
_show_search_url()
func open() -> void:
super()
# 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
# Another server may have been picked in the settings since last time
_show_search_url()
func _show_search_url() -> void:
var url: String = Settings.default_path + "/search"
if qr_code_rect.data != url:
qr_code_rect.data = url
GameLog.debug("QR code generated for: ", url)
url_label.text = url
+62 -48
View File
@@ -1,65 +1,79 @@
class_name ShortcutsPopup
extends BasePopup
## Lists the keyboard shortcuts. Opened with Alt+K or the Shortcuts button.
## Lists the keyboard shortcuts, grouped by what they do. Opened with Alt+K or the
## Shortcuts button.
var shortcuts_dic: Dictionary = {
"Alt + S": "Search",
"Alt + A": "Add Players",
"Alt + Z": "Reset",
"Alt + X": "Play/Pause",
"Alt + C": "Next Song",
"Alt + V": "Show Answer",
"Alt + L": "Show Debug Log",
"Alt + K": "Show shortcuts",
"Alt + Enter": "Fullscreen",
"Alt + UP": "Volume up",
"Alt + DOWN": "Volume down",
"Alt + LEFT": "Jump back in song",
"Alt + Ctrl + LEFT": "Restart song",
"Alt + RIGHT": "Jump forward in song",
"Alt + 1 - 9": "Give player point",
"Alt + Ctrl + 1 - 9": "Take point from player",
}
## Each group's heading and its shortcuts, as [code][keys, what they do][/code].
## [code]tests/integration/test_shortcuts_popup.gd[/code] checks that every button
## shortcut in the game is here with the right keys.
const SHORTCUTS: Array = [
["Players and points", [
["Alt + 1–9", "Give a player a point"],
["Alt + Ctrl + 1–9", "Take a point from a player"],
["Alt + A", "Add players"],
]],
["Songs", [
["Alt + C", "Next song"],
["Alt + V", "Show answer"],
["Alt + X", "Play or pause"],
["Alt + Z", "Restart song"],
["Alt + Ctrl + Left", "Restart song"],
["Alt + Left", "Jump back 5 seconds"],
["Alt + Right", "Jump forward 5 seconds"],
["Alt + Up", "Volume up"],
["Alt + Down", "Volume down"],
]],
["Windows", [
["Alt + S", "Search"],
["Alt + K", "Shortcuts"],
["Alt + L", "Log"],
["Alt + Enter", "Fullscreen"],
]],
]
## Space above each group but the first.
const GROUP_GAP: Vector2 = Vector2(0, 8)
@onready var shortcuts_vbox: VBoxContainer = %shortcutsVBox
@onready var shortcuts_panel: Panel = %ShortcutsPanel
func _ready() -> void:
shortcuts_dic.sort()
Settings.delete_children(shortcuts_vbox)
for group: Array in SHORTCUTS:
if shortcuts_vbox.get_child_count() > 0:
var gap: Control = Control.new()
gap.custom_minimum_size = GROUP_GAP
shortcuts_vbox.add_child(gap)
var heading: Label = Label.new()
heading.theme_type_variation = &"HighlightLarge"
heading.text = group[0]
shortcuts_vbox.add_child(heading)
var row: int = 0
for shortcut: Array in group[1]:
shortcuts_vbox.add_child(_make_row(shortcut[0], shortcut[1], row))
row += 1
var title: Label = Label.new()
title.theme_type_variation = &"TextLarge"
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.text = "Shortcuts"
shortcuts_vbox.add_child(title)
var count: int = 0
for shortcut: String in shortcuts_dic.keys():
var panel: PanelContainer = PanelContainer.new()
var hbox: HBoxContainer = HBoxContainer.new()
hbox.alignment = BoxContainer.ALIGNMENT_CENTER
## Every key combination in the list, e.g. "Alt + C".
static func listed_keys() -> Array[String]:
var keys: Array[String] = []
for group: Array in SHORTCUTS:
for shortcut: Array in group[1]:
keys.append(shortcut[0])
return keys
func _make_row(keys: String, action: String, row: int) -> PanelContainer:
var panel: PanelContainer = PanelContainer.new()
if row % 2 == 0:
panel.theme_type_variation = &"ShortcutRowDark"
var hbox: HBoxContainer = HBoxContainer.new()
for text: String in [keys, action]:
var label: Label = Label.new()
label.theme_type_variation = &"TextLarge"
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.text = shortcut
label.autowrap_mode = TextServer.AUTOWRAP_WORD
label.text = text
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var label2: Label = Label.new()
label2.theme_type_variation = &"TextLarge"
label2.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label2.text = shortcuts_dic[shortcut]
label2.autowrap_mode = TextServer.AUTOWRAP_WORD
label2.size_flags_horizontal = Control.SIZE_EXPAND_FILL
if count % 2 == 0:
panel.theme_type_variation = &"ShortcutRowDark"
hbox.add_child(label)
hbox.add_child(label2)
panel.add_child(hbox)
shortcuts_vbox.add_child(panel)
count += 1
panel.add_child(hbox)
return panel