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.
213 lines
8.4 KiB
GDScript
213 lines
8.4 KiB
GDScript
extends CanvasLayer
|
|
## Schermata iniziale (modalita' arcade/kiosk): nome del giocatore, pulsante di avvio
|
|
## e classifica globale. Il campo del nome viene mostrato VUOTO a ogni partita, quindi
|
|
## a ogni avvio, riavvio o game over viene richiesto un nome nuovo: il profilo
|
|
## precedente non viene mai dato per scontato. Il pulsante di avvio e' anche il gesto
|
|
## che sblocca l'audio nel browser, quindi la partita parte solo da qui.
|
|
signal started(player_name: String)
|
|
|
|
const Profile = preload("res://scripts/player_profile.gd")
|
|
const AMBER := Color(1.0, 0.66, 0.16)
|
|
const DIM := Color(0.85, 0.78, 0.6)
|
|
const ROWS_DESKTOP := 10
|
|
const ROWS_TOUCH := 5
|
|
|
|
var player: Node
|
|
var leaderboard: Node
|
|
var profile := {}
|
|
var name_field: LineEdit
|
|
var rows_box: VBoxContainer
|
|
var status_label: Label
|
|
var me_label: Label
|
|
var rows_shown := ROWS_DESKTOP
|
|
## Con la regola arcade il nome non e' mai bloccato: il campo c'e' sempre. La variabile
|
|
## resta per i profili scritti dalle versioni precedenti (arrivano gia' "bloccati").
|
|
var name_locked := false
|
|
|
|
func _ready() -> void:
|
|
layer = 4
|
|
name_locked = Profile.is_name_locked(profile)
|
|
rows_shown = ROWS_TOUCH if (player != null and player.touch_mode) else ROWS_DESKTOP
|
|
_build_ui()
|
|
if leaderboard != null:
|
|
leaderboard.fetch()
|
|
|
|
## Nome con cui si giochera': solo quello scritto nel campo della partita corrente.
|
|
func _stored_name() -> String:
|
|
if name_field != null:
|
|
return name_field.text.strip_edges()
|
|
return ""
|
|
|
|
## Chiamato da main.gd quando si torna al menu dopo il game over: campo liberato,
|
|
## cosi' il giocatore della partita successiva deve scrivere il proprio nome.
|
|
func reset_for_new_player() -> void:
|
|
if name_field != null:
|
|
name_field.text = ""
|
|
name_field.grab_focus()
|
|
_set_status("nome per la nuova partita")
|
|
|
|
func _build_ui() -> void:
|
|
var touch_mode: bool = player != null and player.touch_mode
|
|
var viewport := get_viewport().get_visible_rect().size
|
|
var width := minf(620.0, viewport.x * 0.92)
|
|
var backdrop := ColorRect.new()
|
|
backdrop.color = Color(0.01, 0.01, 0.02, 0.92)
|
|
backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
add_child(backdrop)
|
|
var center := CenterContainer.new()
|
|
center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
add_child(center)
|
|
var column := VBoxContainer.new()
|
|
column.custom_minimum_size = Vector2(width, 0)
|
|
column.add_theme_constant_override("separation", 10 if touch_mode else 14)
|
|
center.add_child(column)
|
|
|
|
var title := _label("Work-Death Balance", 34 if touch_mode else 44, AMBER, HORIZONTAL_ALIGNMENT_CENTER)
|
|
column.add_child(title)
|
|
column.add_child(_label("MatGames — classifica globale", 15, DIM, HORIZONTAL_ALIGNMENT_CENTER))
|
|
|
|
# Regola arcade/kiosk: il nome si chiede sempre, campo vuoto all'apertura.
|
|
column.add_child(_build_name_row())
|
|
|
|
var play := Button.new()
|
|
play.text = "GIOCA"
|
|
play.add_theme_font_size_override("font_size", 30)
|
|
play.custom_minimum_size = Vector2(0, 56)
|
|
play.pressed.connect(_start)
|
|
column.add_child(play)
|
|
|
|
var panel := PanelContainer.new()
|
|
var style := StyleBoxFlat.new()
|
|
style.bg_color = Color(0.03, 0.025, 0.01, 0.9)
|
|
style.border_color = Color(1.0, 0.66, 0.16, 0.4)
|
|
style.set_border_width_all(1)
|
|
style.set_corner_radius_all(4)
|
|
style.set_content_margin_all(10)
|
|
panel.add_theme_stylebox_override("panel", style)
|
|
column.add_child(panel)
|
|
var board_column := VBoxContainer.new()
|
|
board_column.add_theme_constant_override("separation", 4)
|
|
panel.add_child(board_column)
|
|
var header := HBoxContainer.new()
|
|
board_column.add_child(header)
|
|
header.add_child(_label("CLASSIFICA", 17, AMBER, HORIZONTAL_ALIGNMENT_LEFT))
|
|
var spacer := Control.new()
|
|
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
header.add_child(spacer)
|
|
var refresh := Button.new()
|
|
refresh.text = "aggiorna"
|
|
refresh.add_theme_font_size_override("font_size", 15)
|
|
refresh.pressed.connect(func(): _set_status("aggiornamento...") ; leaderboard.fetch())
|
|
header.add_child(refresh)
|
|
rows_box = VBoxContainer.new()
|
|
rows_box.add_theme_constant_override("separation", 2)
|
|
board_column.add_child(rows_box)
|
|
me_label = _label("", 15, DIM, HORIZONTAL_ALIGNMENT_LEFT)
|
|
board_column.add_child(me_label)
|
|
status_label = _label("caricamento classifica...", 14, DIM, HORIZONTAL_ALIGNMENT_LEFT)
|
|
board_column.add_child(status_label)
|
|
_show_rows([])
|
|
|
|
## Riga del nome: sempre presente, campo vuoto (nessun nome dato per scontato).
|
|
func _build_name_row() -> HBoxContainer:
|
|
var name_row := HBoxContainer.new()
|
|
name_row.add_theme_constant_override("separation", 8)
|
|
name_field = LineEdit.new()
|
|
name_field.text = ""
|
|
name_field.max_length = Profile.NAME_MAX
|
|
name_field.placeholder_text = "il tuo nome"
|
|
name_field.virtual_keyboard_enabled = true # necessario su mobile Web
|
|
name_field.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
name_field.add_theme_font_size_override("font_size", 22)
|
|
# Invio / tasto "Vai" della tastiera del telefono: conferma il nome, NON avvia
|
|
# la partita (prima lo faceva e su mobile partiva la partita per sbaglio).
|
|
name_field.text_submitted.connect(func(text): name_field.text = Profile.sanitize_name(text))
|
|
name_row.add_child(name_field)
|
|
var edit := Button.new()
|
|
edit.text = "nome…"
|
|
edit.tooltip_text = "tastiera del telefono / finestra del browser"
|
|
edit.add_theme_font_size_override("font_size", 18)
|
|
edit.pressed.connect(_ask_name)
|
|
name_row.add_child(edit)
|
|
return name_row
|
|
|
|
func _label(text: String, size: int, color: Color, align: int) -> Label:
|
|
var label := Label.new()
|
|
label.text = text
|
|
label.horizontal_alignment = align
|
|
label.add_theme_color_override("font_color", color)
|
|
label.add_theme_color_override("font_shadow_color", Color.BLACK)
|
|
label.add_theme_font_size_override("font_size", size)
|
|
return label
|
|
|
|
func _set_status(text: String) -> void:
|
|
if status_label != null:
|
|
status_label.text = text
|
|
|
|
## Righe della classifica: tre colonne, per restare leggibile senza font monospaziato.
|
|
func _show_rows(rows: Array) -> void:
|
|
for child in rows_box.get_children():
|
|
child.queue_free()
|
|
if rows.is_empty():
|
|
rows_box.add_child(_label("nessun punteggio registrato: gioca la prima partita", 15, DIM, HORIZONTAL_ALIGNMENT_LEFT))
|
|
return
|
|
var mine := str(profile.get("player_id", ""))
|
|
for i in mini(rows_shown, rows.size()):
|
|
var row: Dictionary = rows[i]
|
|
var line := HBoxContainer.new()
|
|
line.add_theme_constant_override("separation", 6)
|
|
var rank := _label("%d." % int(row.get("rank", i + 1)), 17, AMBER, HORIZONTAL_ALIGNMENT_RIGHT)
|
|
rank.custom_minimum_size = Vector2(38, 0)
|
|
line.add_child(rank)
|
|
var is_mine := str(row.get("player_id", "")) == mine and mine != ""
|
|
var who := _label(str(row.get("name", "?")), 17, AMBER if is_mine else Color.WHITE,
|
|
HORIZONTAL_ALIGNMENT_LEFT)
|
|
who.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
who.clip_text = true
|
|
line.add_child(who)
|
|
var score_label := _label(str(int(row.get("score", 0))), 17, AMBER, HORIZONTAL_ALIGNMENT_RIGHT)
|
|
score_label.custom_minimum_size = Vector2(80, 0)
|
|
line.add_child(score_label)
|
|
rows_box.add_child(line)
|
|
|
|
func show_leaderboard(rows: Array, total: int, me: Dictionary) -> void:
|
|
if rows_box == null:
|
|
return
|
|
_show_rows(rows)
|
|
_set_status("%d partite registrate" % total if total > 0 else "nessuna partita registrata")
|
|
if me.is_empty():
|
|
me_label.text = ""
|
|
else:
|
|
me_label.text = "il tuo miglior punteggio: %d (%d° posto, %d partite)" % [
|
|
int(me.get("best", 0)), int(me.get("rank", 0)), int(me.get("games", 0))]
|
|
|
|
func show_leaderboard_error(reason: String) -> void:
|
|
_set_status("classifica non disponibile (%s)" % reason)
|
|
|
|
## Su Web la tastiera virtuale di Godot e' sperimentale: in aggiunta al campo di testo
|
|
## si offre il prompt nativo del browser, che ha sempre la tastiera del telefono.
|
|
func _ask_name() -> void:
|
|
if name_field == null:
|
|
return
|
|
var current := Profile.js_escape(name_field.text)
|
|
if OS.has_feature("web") and ClassDB.class_exists("JavaScriptBridge"):
|
|
var answer := str(JavaScriptBridge.eval(
|
|
"window.prompt('Il tuo nome (max 16 caratteri):', '%s') || ''" % current, true))
|
|
if answer.strip_edges() != "":
|
|
name_field.text = Profile.sanitize_name(answer)
|
|
return
|
|
name_field.grab_focus()
|
|
|
|
## La partita parte SOLO da qui, cioe' dal pulsante GIOCA: un click o un tocco
|
|
## altrove (sfondo, campo del nome, tasto Invio della tastiera del telefono) non deve
|
|
## avviare nulla.
|
|
func _start() -> void:
|
|
if name_field == null:
|
|
return
|
|
var name := name_field.text.strip_edges()
|
|
if name == "":
|
|
# Su macchina condivisa un punteggio senza nome sarebbe anonimo: non si parte.
|
|
_set_status("inserisci il tuo nome per la nuova partita")
|
|
return
|
|
started.emit(Profile.sanitize_name(name))
|