Arcade/kiosk name flow, free cursor in menu and portable Linux export

Every game over unlocks the profile and frees the name, so the menu always asks
for a new player name: the field is empty at every boot, restart or game over and
the game does not start without a name (no anonymous scores). The player identity
is derived from the entered name (deterministic FNV-1a, 16 hex chars valid for the
public API) and the profile keeps the best score per name. Backend untouched.

Mouse: player._ready() captured the pointer before the menu existed, so the start
screen was not clickable. The cursor now always starts visible; the capture happens
only in player.begin_play(), called by start_game(). New `playing` flag gates Esc
and the Web click capture to the actual match.

Linux export preset: single-file x86_64 binary with the PCK embedded, no
libstdc++ dependency, official 4.7.1 template (glibc >= 2.28). Notes and the
export_presets.cfg `#`-is-not-a-comment trap documented in docs/KIOSK-ARCADE.md.

Tests: profile 30/30, start screen 15/15 (cursor free at menu, captured only in
game), boss 50/50, rebalance 14/14, touch 14 PASS, API 42/42, all other suites pass.
This commit is contained in:
enne2
2026-10-24 12:50:03 +02:00
parent 21a1d5bf79
commit cb14fd5400
9 changed files with 397 additions and 106 deletions
+42 -14
View File
@@ -1,8 +1,9 @@
extends SceneTree
## Verifica le regole della schermata iniziale:
## Verifica le regole della schermata iniziale in modalita' arcade/kiosk:
## - la partita parte SOLO dal pulsante GIOCA: click o tocco sullo sfondo, oppure
## Invio/"Vai" nel campo del nome, non devono avviare nulla;
## - il nome scelto una volta non viene piu' richiesto.
## - il campo del nome c'e' sempre, arriva vuoto e senza nome la partita non parte;
## - al ritorno al menu dopo il game over il campo viene liberato per il nuovo giocatore.
## Avviare con: --headless --path . --script res://tools/test_start_screen.gd
## (con display, per gli eventi di puntamento: --rendering-method gl_compatibility)
const Profile = preload("res://scripts/player_profile.gd")
@@ -28,7 +29,7 @@ func check(label: String, condition: bool, detail: String = "") -> void:
func run_test() -> void:
await test_world_menu()
await test_locked_name()
await test_name_always_asked()
await test_unlocked_name()
print("%d controlli superati, %d falliti" % [checks - failures, failures])
quit(1 if failures > 0 else 0)
@@ -54,6 +55,20 @@ func test_world_menu() -> void:
gioca != null and not gioca.get_global_rect().has_point(outside),
"pulsante=%s" % str(gioca))
## Bug risoluto: al menu il cursore deve essere visibile e libero, la cattura
## avviene solo all'inizio della partita (begin_play()).
check("al menu il giocatore non e' ancora in partita (playing=false)",
world.player.playing == false, "playing=%s" % str(world.player.playing))
check("al menu il cursore e' visibile e libero (schermata cliccabile)",
Input.mouse_mode == Input.MOUSE_MODE_VISIBLE, "mode=%s" % str(Input.mouse_mode))
var esc := InputEventKey.new()
esc.physical_keycode = KEY_ESCAPE
esc.pressed = true
Input.parse_input_event(esc)
await process_frame
check("Esc al menu non cattura il cursore (solo durante la partita)",
Input.mouse_mode == Input.MOUSE_MODE_VISIBLE, "mode=%s" % str(Input.mouse_mode))
_click(outside)
await process_frame
await process_frame
@@ -81,6 +96,11 @@ func test_world_menu() -> void:
await process_frame
check("il pulsante GIOCA avvia la partita", start_events == 1 and world.start_screen == null,
"avvii=%d schermata=%s" % [start_events, "presente" if world.start_screen != null else "via"])
check("all'inizio della partita il giocatore e' in partita e il cursore viene catturato",
world.player.playing == true and (DisplayServer.get_name() == "headless"
or Input.mouse_mode == Input.MOUSE_MODE_CAPTURED),
"playing=%s mode=%s display=%s" % [str(world.player.playing), str(Input.mouse_mode),
DisplayServer.get_name()])
# Regressione: senza --api= questa e' una build desktop di prova e non deve poter
# inviare punteggi alla classifica pubblica (ci sono finite righe "Test").
check("senza --api= la build di prova non invia punteggi alla classifica pubblica",
@@ -93,7 +113,7 @@ func _on_started(player_name: String) -> void:
emitted = player_name
start_events += 1
## --- nome bloccato e non ------------------------------------------------------
## --- nome sempre richiesto (macchina condivisa) ------------------------------
func menu_with(profile: Dictionary) -> CanvasLayer:
var layer := CanvasLayer.new()
@@ -103,26 +123,34 @@ func menu_with(profile: Dictionary) -> CanvasLayer:
layer.started.connect(_on_started)
return layer
func test_locked_name() -> void:
func test_name_always_asked() -> void:
emitted = ""
start_events = 0
# Un profilo scritto da una versione precedente arriva con il nome gia' scelto e
# bloccato: in modalita' arcade il campo deve esserci comunque, vuoto.
var menu := menu_with({"name": "Mat", "player_id": "abcdef0123456789", "best": 300, "locked": true})
await process_frame
var label_text := ""
for label in menu.find_children("*", "Label", true, false):
if str(label.text).begins_with("giocatore:"):
label_text = str(label.text)
check("con nome scelto il campo non viene richiesto",
menu.get("name_field") == null and menu.get("name_locked") == true,
"campo=%s bloccato=%s" % [str(menu.get("name_field")), str(menu.get("name_locked"))])
check("il nome scelto e' mostrato al giocatore", label_text == "giocatore: Mat", label_text)
var field: LineEdit = menu.get("name_field")
var field_text := "" if field == null else str(field.text)
check("regola arcade: campo del nome presente e vuoto anche con profilo precedente",
field != null and menu.get("name_locked") == false and field_text == "",
"campo=%s testo=%s bloccato=%s" % [str(field), field_text, str(menu.get("name_locked"))])
var gioca: Button = null
for button in menu.find_children("*", "Button", true, false):
if button.text == "GIOCA":
gioca = button
if gioca != null:
gioca.pressed.emit()
check("la partita usa il nome memorizzato", emitted == "Mat", "nome=%s" % emitted)
check("senza nome scritto la partita non parte (nessun punteggio anonimo)",
start_events == 0 and emitted == "", "avvii=%d nome=%s" % [start_events, emitted])
if field != null and gioca != null:
field.text = "Neo"
gioca.pressed.emit()
check("con il nome scritto la partita parte con quel nome", emitted == "Neo", "nome=%s" % emitted)
if field != null:
menu.reset_for_new_player()
check("game over: campo liberato per il giocatore successivo", str(field.text) == "",
"testo=%s" % str(field.text))
menu.queue_free()
await process_frame