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
+17 -12
View File
@@ -27,11 +27,11 @@ File coinvolti:
| `services/scores-api/docker-compose.yml` | compose (volume, limiti, log rotation) |
| `tools/nginx-zombie-fps-api.conf` | snippet nginx del reverse proxy |
| `scripts/leaderboard.gd` | client HTTP Godot (lettura + invio, retry) |
| `scripts/player_profile.gd` | profilo locale: nome scelto, id giocatore, miglior punteggio |
| `scripts/player_profile.gd` | profilo locale: nome dell'ultima partita, id giocatore, miglior punteggio per nome |
| `scripts/start_screen.gd` | menu: nome, pulsante GIOCA, classifica |
| `tools/test_scores_api.py` | 42 controlli sul servizio (funzionalità, validazione, limiti) |
| `tools/test_leaderboard.gd` + `.sh` | 14 controlli sul client, contro un'API locale usa e getta |
| `tools/test_player_profile.gd` | 26 controlli sul profilo (nome, id, riparazioni, migrazione dopo rinomina) |
| `tools/test_player_profile.gd` | 30 controlli sul profilo (campo del nome sempre libero, id derivato dal nome, record per nome, riparazioni, migrazione dopo rinomina) |
| `tools/test_start_screen.gd` | 10 controlli sulle regole del menu |
## 2. Contratto dell'API
@@ -148,19 +148,24 @@ sudo nginx -t && sudo systemctl reload nginx # se -t fallisce: ripristinar
| Chiave | Contenuto |
| --- | --- |
| `zombie_fps_name` | nome scelto |
| `zombie_fps_player_id` | id anonimo, 32 caratteri esadecimali |
| `zombie_fps_best` | miglior punteggio locale |
| `zombie_fps_locked` | `1` se il nome è già stato scelto |
| `zombie_fps_name` | nome dell'ultima partita (solo come chiave del suo record) |
| `zombie_fps_player_id` | id anonimo, 32 caratteri esadecimali, o 16 derivato dal nome |
| `zombie_fps_best` | miglior punteggio del nome salvato |
| `bests` (sezione del file `.cfg`) | miglior punteggio **per nome** |
| `zombie_fps_locked` | sempre `0` in modalità arcade/kiosk |
* Sul Web si scrive **anche** in `localStorage` perché è la memoria del browser che
sopravvive davvero alla ricarica della pagina (l'IndexedDB di Godot può non essere
sincronizzata). Alla lettura i due archivi si fondono: l'id del giocatore resta
stabile, così i punteggi vecchi restano collegati.
* **Il nome, una volta scelto, non viene più richiesto**: se `locked` è attivo il menu
mostra `giocatore: Mat` e il campo di testo non esiste proprio.
* Un nome diverso da quello predefinito (`Giocatore`) vale come nome scelto, anche per
i profili salvati dalle versioni precedenti.
* **Modalità arcade/kiosk (`ALWAYS_ASK_NAME`)**: il nome non viene mai dato per scontato.
Il campo del menu è sempre presente e arriva vuoto a ogni avvio, riavvio o game over;
senza nome scritto la partita non parte (nessun punteggio anonimo in classifica).
Il profilo conserva solo il **miglior punteggio per nome**. Dettagli: `docs/KIOSK-ARCADE.md`.
* L'identità della partita nasce dal nome inserito: `player_id_for_name()` è un FNV-1a
deterministico (16 caratteri esadecimali, valido per il server). Stesso nome = stesso id
anche dopo un riavvio del kiosk, nomi diversi = identità diverse. Il backend non cambia.
* Un `locked` arrivato da profili o dal browser di versioni precedenti viene ignorato.
* **Riparazione automatica**: `str(null)` in GDScript produce la stringa `"null"`, e
una versione precedente l'ha salvata come nome. Alla lettura i valori `null`,
`undefined` e vuoti vengono scartati, il nome torna al predefinito e il blocco si
@@ -198,9 +203,9 @@ sh tools/test_leaderboard.sh # 14 controlli
# profilo e menu (menu: serve un display)
$HOME/.local/opt/godot-4.7.1/Godot_v4.7.1-stable_linux.x86_64 --headless --path . \
--script res://tools/test_player_profile.gd # 26 controlli
--script res://tools/test_player_profile.gd # 30 controlli
$HOME/.local/opt/godot-4.7.1/Godot_v4.7.1-stable_linux.x86_64 --path . \
--rendering-method gl_compatibility --script res://tools/test_start_screen.gd # 10 controlli
--rendering-method gl_compatibility --script res://tools/test_start_screen.gd # 11 controlli
```
In produzione si verifica a mano, con `curl`:
+115
View File
@@ -0,0 +1,115 @@
# Modalità arcade/kiosk (macchina condivisa) e build Linux
## 1. Regola di flusso
Il gioco è installato su un computer condiviso. Il profilo precedente **non viene mai
dato per scontato**:
* il campo del nome del menu è **sempre presente e vuoto** a ogni avvio e a ogni
riavvio;
* alla morte (game over) il profilo viene **sbloccato** e il nome liberato, così al
ritorno al menu — `R` o pulsante `RICOMINCIA` — viene richiesto il nome della nuova
partita;
* senza nome scritto la partita **non parte** (`GIOCA` non emette `started`), perché un
punteggio senza nome sarebbe anonimo in classifica.
Il `name lock` delle versioni precedenti (`zombie_fps_locked = 1` → menu con
`giocatore: Mat` e nessun campo) è ignorato: `Profile.is_name_locked()` restituisce
`false` con `ALWAYS_ASK_NAME`. I profili vecchi e i valori di `localStorage` arrivano
ancora letti, ma solo come chiave del miglior punteggio.
**Identità** (backend invariato, API pubblica così com'è): `player_id` nasce dal nome
inserito, `player_id_for_name()` = FNV-1a 32 bit con due semi → 16 caratteri
esadecimali, accettato dal server (`8–64 [A-Za-z0-9_-]`). Stesso nome = stessa persona
anche dopo un riavvio; nome diverso = identità diversa. Il record è tenuto **per nome**
(`bests` nel file `.cfg`).
Permalink del flusso: `start_screen.gd::_start()` → `main.gd::start_game()` (id e best
del nome, `leaderboard.player_id`, `player.begin_play()`) → morte → `main.gd::_on_player_died()`
(`record_best` con il nome della partita appena conclusa, `unlock_name`, salvataggio,
`reset_for_new_player()`) → reload → menu con campo vuoto.
## 1b. Cursore del mouse
Bug risolto: `player.gd::_ready()` catturava il pointer (`MOUSE_MODE_CAPTURED`) alla
costruzione del giocatore, cioè **prima** del menu, quindi il cursore era già bloccato
sulla schermata iniziale e i pulsanti non erano cliccabili.
* `player.gd::_ready()` imposta **sempre** `MOUSE_MODE_VISIBLE`.
* La cattura avviene solo in `player.gd::begin_play()`, chiamata da
`main.gd::start_game()` quando si preme GIOCA.
* Nuovo flag `player.playing`: `Esc` (pausa) e il click sul Web catturano il cursore
solo durante la partita, mai nel menu.
* In modalità touch il cursore non viene catturato (serve ai controlli on-screen).
* Alla morte `_die()` lo riporta visibile, quindi la schermata di morte è cliccabile.
### File toccati
| File | Modifica |
| --- | --- |
| `scripts/player_profile.gd` | `ALWAYS_ASK_NAME`, `player_id_for_name()`, `fnv1a32()`, `best_for_name()`, `record_best()`, `unlock_name()`; nome non presupposto, record per nome |
| `scripts/start_screen.gd` | campo del nome sempre mostrato e vuoto, `reset_for_new_player()`, `GIOCA` bloccato a campo vuoto |
| `scripts/main.gd` | `start_game()` deriva id e record dal nome e chiama `player.begin_play()`; `_on_player_died()` sblocca il profilo e invia il nome della partita conclusa |
| `scripts/player.gd` | cursore visibile al menu, `begin_play()` con la cattura, flag `playing` |
| `tools/test_player_profile.gd` | 30 controlli (campo libero, id derivato, record per nome, game over) |
| `tools/test_start_screen.gd` | 15 controlli (campo sempre richiesto, partenza bloccata a campo vuoto, campo liberato dopo il game over, cursore libero al menu e catturato solo in partita) |
## 2. Verifiche eseguite
```bash
G="$HOME/.local/opt/godot-4.7.1/Godot_v4.7.1-stable_linux.x86_64"
"$G" --headless --path . --script res://tools/test_player_profile.gd # 30/30 PASS
"$G" --path . --rendering-method gl_compatibility --script res://tools/test_start_screen.gd # 15/15 PASS
# suite precedenti, riuscite dopo la modifica:
"$G" --headless --path . --script res://tools/test_boss.gd # 50/50
"$G" --headless --path . --script res://tools/test_rebalance_branding.gd # 14/14
"$G" --headless --path . --script res://tools/test_spawn_windows.gd
"$G" --headless --path . --script res://tools/test_wave_growth.gd
"$G" --headless --path . --script res://tools/test_gameplay_improvements.gd
python3 tools/test_scores_api.py # 42/42
```
Nessuna modifica al backend: nessun cambiamento a nginx, Docker, API o database.
La release Web pubblicata (`https://enne2.net/pub/zombie-fps/`) resta quella precedente
finché non viene riesportata e rimpiantata con questa regola.
## 3. Build Linux portabile
`export_presets.cfg` → `preset.1` (name `Linux`, platform `Linux`):
```ini
export_filter="all_resources"
exclude_filter="addons/*,tools/*,docs/*,build/*,assets/**/*.mdl,assets/half_life_manifest.json"
export_path="build/linux/work_death_balance.x86_64"
binary_format/embed_pck=true
binary_format/architecture="x86_64"
```
* **PCK incorporato**: sul kiosk serve un solo file (`work_death_balance.x86_64`,
80.517.080 byte = template 73.470.264 + PCK 7.046.808), verificato eseguendolo da
una cartella vuota (`--headless -- --smoke` → `SMOKE: wave=0 alive=0 player_hp=100.0`).
* **Dipendenze dinamica minime**: `libc`, `libm`, `librt`, `libpthread`, `libdl` +
`ld-linux-x86-64.so.2`. Nessuna `libstdc++` (già linkata staticamente nel template).
* **Baseline glibc del template ufficiale 4.7.1**: simboli richiesti fino a
`GLIBC_2.28` (`readelf -V`) → il binario gira su glibc ≥ 2.28 (distro dal 2018 in poi).
È il miglior risultato ottenibile senza ricompilare l'engine.
* **musl statico scartato**: un binario completamente statico non carica le librerie
grafiche, perché i driver OpenGL/Vulkan linkano glibc (funziona solo headless). Non è
adatto a un gioco con finestra.
* Runtime richiesto sul kiosk (non linkato, caricato a runtime): driver OpenGL
(Compatibility) o Vulkan (Forward+, renderer nativo attuale), X11 **o** Wayland,
ALSA o PulseAudio. Sono `dlopen`, quindi non rompono la portabilità del binario.
* Attenzione: `#` dentro `export_presets.cfg` non è un commento per il parser → una riga
commentata dopo `binary_format/embed_pck=true` ha fatto esportare il PCK separato.
### Avvio sul kiosk
La build nativa non ha un'origine di pagina, quindi la classifica va indicata
esplicitamente (è la stessa API pubblica già in uso):
```bash
./work_death_balance.x86_64 --api=https://enne2.net/api/zombie-fps/
```
Senza `--api=` la classifica è disattiva di proposito (una build di prova non deve
poter scrivere alla classifica pubblica).