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