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

+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)