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.
116 lines
6.2 KiB
Markdown
116 lines
6.2 KiB
Markdown
# 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).
|