Compare commits
3
Commits
v1.0.0-kiosk
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c39d6235f | ||
|
|
b08ac8c6a1 | ||
|
|
323a7cdbae |
@@ -0,0 +1,95 @@
|
||||
# Build standalone per Raspberry Pi 5 (arm64)
|
||||
|
||||
Stato: **procedura verificata sulla generazione del binario** (export reale prodotto e
|
||||
validato); la prova di esecuzione va fatta sul Pi 5, che non è questa macchina.
|
||||
|
||||
Artefatto prodotto il 2026-10-24 con Godot 4.7.1.stable (a13da4feb), commit `4af402f8`
|
||||
(tag `v1.0.0-kiosk`).
|
||||
|
||||
## Esito
|
||||
|
||||
| Voce | Valore |
|
||||
| --- | --- |
|
||||
| File | `build/linux-arm64/work_death_balance.arm64` |
|
||||
| Tipo | ELF 64-bit LSB, ARM aarch64, interpreter `/lib/ld-linux-aarch64.so.1` |
|
||||
| Dimensione | 74.094.536 byte (≈70,7 MiB) = template 67.046.672 + PCK 7.047.864 |
|
||||
| PCK | incorporato (`embed_pck=true`), magic `GDPC` a offset 67.046.672, pack_version 4 |
|
||||
| Texture | `etc2_astc=true`, `s3tc_bptc=false` (v3d/V3D 7.1 non supporta S3TC/BPTC) |
|
||||
| Addon nativo | non incluso: il preset esclude `addons/*`; nessun `.so` arm64 necessario |
|
||||
|
||||
## Perché serve poco lavoro
|
||||
|
||||
- I **template ufficiali Godot includono Linux arm64** (le architetture ufficiali sono
|
||||
x86_32, x86_64, arm32, arm64). L'export è un **cross-export**: si esegue su x86_64,
|
||||
non serve toolchain aarch64 né compilare il motore.
|
||||
- La **GDExtension `goldsrc` non è nel gioco esportato**: `export_presets.cfg` ha
|
||||
`exclude_filter="addons/*"` e l'unico riferimento nel codice è un commento in
|
||||
`scripts/grenade_explosion.gd:3`. I modelli sono già `.scn` pre-estratti.
|
||||
(Nel repository i binari dell'addon sono solo x86_64: irrilevante per l'export.)
|
||||
|
||||
## Procedura riproducibile
|
||||
|
||||
```bash
|
||||
# 1. Godot 4.7.1 editor (x86_64) in ~/.local/opt/godot-4.7.1/
|
||||
# https://github.com/godotengine/godot-builds/releases/tag/4.7.1-stable
|
||||
# (solo il template che serve: templates/linux_release.arm64 dal tpz)
|
||||
|
||||
# 2. template installati in:
|
||||
# ~/.local/share/godot/export_templates/4.7.1.stable/{linux_release.arm64,version.txt}
|
||||
# version.txt deve contenere esattamente: 4.7.1.stable
|
||||
|
||||
# 3. import del progetto (genera .godot/), una volta sola
|
||||
godot --headless --path . --import
|
||||
|
||||
# 4. export
|
||||
godot --headless --path . --export-release "Linux ARM64" \
|
||||
build/linux-arm64/work_death_balance.arm64
|
||||
```
|
||||
|
||||
Il preset `Linux ARM64` è in `export_presets.cfg` (`[preset.2]`) ed è identico a
|
||||
`Linux` tranne per: `binary_format/architecture="arm64"`,
|
||||
`texture_format/etc2_astc=true`, `texture_format/s3tc_bptc=false`,
|
||||
`export_path="build/linux-arm64/work_death_balance.arm64"`.
|
||||
|
||||
### Scaricare solo il template che serve (1,28 GB → 27 MB)
|
||||
|
||||
Il `tpz` dei template è uno zip: `tools/zipfetch.py` legge EOCD e central directory
|
||||
via HTTP Range e scarica il solo membro richiesto. Comodo su connessioni lente.
|
||||
|
||||
## Sul Pi 5: aspettative e leve
|
||||
|
||||
GPU: Broadcom VideoCore VII (V3D 7.1). Mesa espone **OpenGL 3.1 desktop + OpenGL ES 3.1**
|
||||
(v3d) e **Vulkan 1.2/1.3 conformante** (v3dv).
|
||||
|
||||
- Il progetto è `forward_plus`. Su Vulkan il Forward+ **viene declassato automaticamente
|
||||
a Mobile** ("Platform supports less than 48 textures per stage … Defaulting to Mobile
|
||||
renderer"): è il comportamento atteso, non un errore.
|
||||
- Con `--rendering-driver opengl3` il driver desktop GL 3.3 non è disponibile su V3D e
|
||||
Godot **ripiega su OpenGL ES 3.1**: per questo servono le texture ETC2 (già abilitate
|
||||
nel preset arm64).
|
||||
- Nessuna aspettativa da 1080p: il target realistico è 960x540 (o 640x360) con
|
||||
`--max-fps 30`. Da misurare sul campo con `--print-fps` se compilato in debug.
|
||||
|
||||
Avvio consigliato (script fornito: `build/linux-arm64/run-arm64.sh`):
|
||||
|
||||
```bash
|
||||
./run-arm64.sh # GL/GLES3, 960x540, 30 fps, classifica attiva
|
||||
WDB_MODE=vulkan ./run-arm64.sh # percorso Vulkan (Mobile)
|
||||
WDB_RES=640x360 ./run-arm64.sh # se non regge
|
||||
WDB_API= ./run-arm64.sh # senza classifica
|
||||
```
|
||||
|
||||
## Insidie verificate
|
||||
|
||||
1. **`--api=` solo dopo `--`**: `leaderboard.gd` legge `OS.get_cmdline_user_args()`.
|
||||
Senza separatore il gioco stampa "Classifica disattivata".
|
||||
2. **`curl -C -` senza `-L`** su GitHub scarica il corpo del 302 (0 byte) e sembra
|
||||
"riuscito": il download resta fermo per sempre. Serve `-L`.
|
||||
3. **Resume fragile**: la CDN può restituire 206 anche dopo uno stallo; con
|
||||
`--speed-time 30 --speed-limit 20000` si riprende senza perdere i byte già scaricati.
|
||||
4. **L'addon `goldsrc` non si carica su host con glibc più vecchia del build**
|
||||
(`GLIBC_2.43 not found`): errore cosmetico in import/export, non incide sul PCK
|
||||
perché `addons/*` è escluso.
|
||||
5. **Verifica del PCK**: la controprova x86_64 esportata dallo stesso clone si avvia e
|
||||
gira senza risorse mancanti; per l'arm64 l'unica verifica possibile qui è strutturale
|
||||
(`file`, offset/size del PCK). La prova reale richiede il Pi.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Gamepad / controller
|
||||
|
||||
Supporto ai controller implementato il 2026-10-24. Il gameplay non è stato
|
||||
riscritto: tutte le azioni passano già dalla Input Map, quindi il lavoro è stato
|
||||
mappare i binding, aggiungere il ramo analogico della visuale e aprire i gate che
|
||||
prima pretendevano il mouse catturato.
|
||||
|
||||
## Mappatura (layout SDL/Xbox)
|
||||
|
||||
| Azione | Tastiera/mouse | Gamepad |
|
||||
| --- | --- | --- |
|
||||
| `move_forward` / `move_back` | W / S, frecce | stick sinistro Y (asse 1) |
|
||||
| `move_left` / `move_right` | A / D, frecce | stick sinistro X (asse 0) |
|
||||
| `jump` | Spazio | A (pulsante 0) |
|
||||
| `shoot` | tasto sinistro del mouse | grilletto destro (asse 5) · RB (10) |
|
||||
| `reload` | R | X (pulsante 2) |
|
||||
| `grenade` | tasto destro del mouse | B (1) · LB (9) |
|
||||
| `pause` | Esc | Start (6) |
|
||||
| `toggle_fullscreen` | Alt+F | Back/Select (4) |
|
||||
| `look_*` (nuove) | movimento del mouse | stick destro (assi 2 e 3) |
|
||||
|
||||
Tutti i binding joypad hanno `device = -1`, quindi funzionano con qualunque pad
|
||||
collegato, non solo con il primo.
|
||||
|
||||
## Dove sta il codice
|
||||
|
||||
- `project.godot` → `[input]`: eventi `InputEventJoypadButton` / `InputEventJoypadMotion`
|
||||
sulle azioni esistenti, quattro nuove azioni `look_left/right/up/down` (deadzone 0.2)
|
||||
e la ridefinizione di `ui_accept`/`ui_cancel`.
|
||||
- `scripts/player.gd`:
|
||||
- `gamepad_mode`: si accende da solo al primo pulsante/stick usato (`_is_gamepad_activity`);
|
||||
- `_can_aim()` sostituisce il gate `touch_mode or mouse_mode == CAPTURED` di sparo e
|
||||
granata: senza, col pad non si sarebbe potuto né sparare né lanciare;
|
||||
- `_handle_gamepad_look(dt)`: converte lo stick destro in "pixel relativi
|
||||
equivalenti" (900 px/s a fondo corsa, curva `m^1.8`) e li passa ad `apply_look()`,
|
||||
che resta l'unico punto con sensibilità e limiti di inclinazione;
|
||||
- a partita finita ricomincia la partita anche con A/X, e il messaggio di morte
|
||||
cambia testo in base al dispositivo in uso.
|
||||
- `scripts/start_screen.gd`: riga di aiuto che compare **solo** se c'è un pad
|
||||
collegato (aggiornata in hotplug via `Input.joy_connection_changed`).
|
||||
|
||||
## Perché `ui_accept` va ridefinito a mano
|
||||
|
||||
Nel motore 4.7.1 `ui_accept` è mappato su Invio, Invio del tastierino e Spazio, e
|
||||
`ui_cancel` solo su Esc: **nessun pulsante di gamepad**. Con il pad si poteva
|
||||
navigare fra i controlli (le `ui_up/down/left/right` hanno già dpad e stick) ma non
|
||||
confermare. La ridefinizione in `project.godot` rimette anche i tasti di tastiera:
|
||||
senza, Invio e Spazio avrebbero smesso di funzionare. Il test lo controlla.
|
||||
|
||||
## Verifica
|
||||
|
||||
```bash
|
||||
godot --headless --path . --script res://tools/test_gamepad.gd
|
||||
```
|
||||
|
||||
33 controlli: struttura della mappa, conservazione dei tasti su `ui_accept`/`ui_cancel`,
|
||||
pipeline eventi reale (`Input.parse_input_event` con un pad su device 0), accensione di
|
||||
`gamepad_mode`, rotazione della visuale con lo stick destro e limiti di inclinazione.
|
||||
|
||||
Con un pad vero conviene rifare la stessa prova a schermo reale
|
||||
(`--rendering-method gl_compatibility`) perché il puntatore e il focus non sono
|
||||
verificabili in headless. Suite esistenti riverificate: `test_fullscreen_shortcut`,
|
||||
`test_touch_controls -- --touch`, `test_grenade`, `test_boss`, `test_rebalance_branding`,
|
||||
`test_player_profile` tutte verdi. `test_start_screen` ha 2 FAIL preesistenti
|
||||
(pulsante GIOCA in headless), identici su HEAD senza queste modifiche.
|
||||
|
||||
## Limite noto: il nome del giocatore
|
||||
|
||||
La regola arcade/kiosk chiede il nome a ogni avvio, e il campo è un `LineEdit` con
|
||||
`virtual_keyboard_enabled`: la tastiera su schermo del motore però esiste solo su
|
||||
Android/iOS/Web. Con **solo** il pad, su desktop non c'è modo di scrivere il nome.
|
||||
Fase successiva: tastiera on-screen navigabile col dpad (griglia A-Z), oppure scelta
|
||||
del nome da una lista di profili recenti.
|
||||
+12
-8
@@ -125,13 +125,17 @@ sudo nginx -t && sudo systemctl reload nginx # se -t fallisce: ripristinar
|
||||
|
||||
## 5. Client Godot
|
||||
|
||||
* **Base URL** (`scripts/leaderboard.gd`, `_resolve_base()`):
|
||||
1. argomento `--api=http://host:porta` (test);
|
||||
2. su Web: `window.location.origin + /api/zombie-fps`;
|
||||
3. altrimenti **niente**: la classifica resta disattivata con
|
||||
`DISABLED_REASON = "classifica non attiva in questa build"`.
|
||||
Regola importante: una build desktop di prova **non deve** poter inviare punteggi
|
||||
alla classifica pubblica (è già successo: ci sono finite righe `Test`).
|
||||
* **Base URL** (`scripts/leaderboard.gd`, `resolve_base()`):
|
||||
1. argomento `--api=http://host:porta` (test o servizio alternativo);
|
||||
2. argomento `--offline` / `--no-api` → classifica **spenta** (partite di prova);
|
||||
3. su Web: `window.location.origin + /api/zombie-fps`;
|
||||
4. altrimenti il servizio pubblico `DEFAULT_API_BASE =
|
||||
https://enne2.net/api/zombie-fps`.
|
||||
La classifica è quindi **attiva di default**: non serve nesuna configurazione né
|
||||
argomento per leggere e inviare punteggi. L'opt-out esiste solo per le partite di
|
||||
prova, che non devono finire nella classifica pubblica (è già successo: ci sono
|
||||
finite righe `Test`). Le suite di test girano con `--script` e restano spente a
|
||||
meno di un `--api=` esplicito.
|
||||
* Due nodi `HTTPRequest` separati (lettura e invio non si sovrappongono); timeout 12 s,
|
||||
`body_size_limit` 256 KB / 64 KB.
|
||||
* Retry su 429/5xx/errore di trasporto: fino a 3 tentativi, 6 s di pausa, **stesso
|
||||
@@ -223,4 +227,4 @@ curl -s "https://enne2.net/api/zombie-fps/scores?limit=5"
|
||||
| `classifica non disponibile (rete non disponibile)` nel menu | servizio spento o nginx non ricaricato: `docker ps`, `curl 127.0.0.1:8791/health`, `sudo nginx -t` |
|
||||
| punteggio non inviato (`429`) | limite di frequenza per IP (20/ora, 5 s tra invii); i retry riusano lo stesso `submission_id`, quindi non raddoppiano |
|
||||
| la pagina resta su una release vecchia | `index.html` è servito con `cache-control: no-cache`; forzare il ricaricamento della pagina |
|
||||
| righe di prova nella classifica pubblica | una build desktop ha inviato senza `--api=`: verificare `leaderboard.disabled` (test `test_start_screen.gd`) |
|
||||
| righe di prova nella classifica pubblica | la classifica è attiva di default: le partite di prova vanno lanciate con `-- --offline` (verificare `leaderboard.disabled`, test `test_start_screen.gd`) |
|
||||
|
||||
@@ -54,3 +54,31 @@ binary_format/architecture="x86_64"
|
||||
texture_format/s3tc_bptc=true
|
||||
texture_format/etc2_astc=false
|
||||
ssh_remote_deploy/enabled=false
|
||||
|
||||
[preset.2]
|
||||
|
||||
name="Linux ARM64"
|
||||
platform="Linux"
|
||||
runnable=true
|
||||
advanced_options=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter="addons/*,tools/*,docs/*,build/*,assets/**/*.mdl,assets/half_life_manifest.json"
|
||||
export_path="build/linux-arm64/work_death_balance.arm64"
|
||||
patches=PackedStringArray()
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.2.options]
|
||||
|
||||
custom_template/release=""
|
||||
custom_template/debug=""
|
||||
debug/export_console_wrapper=0
|
||||
binary_format/embed_pck=true
|
||||
binary_format/architecture="arm64"
|
||||
texture_format/s3tc_bptc=false
|
||||
texture_format/etc2_astc=true
|
||||
ssh_remote_deploy/enabled=false
|
||||
|
||||
+51
-1
@@ -44,54 +44,104 @@ move_forward={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey, "resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventKey, "resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventJoypadMotion, "resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":-1.0,"script":null)
|
||||
]
|
||||
}
|
||||
move_back={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey, "resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventKey, "resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventJoypadMotion, "resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":1.0,"script":null)
|
||||
]
|
||||
}
|
||||
move_left={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey, "resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventJoypadMotion, "resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":-1.0,"script":null)
|
||||
]
|
||||
}
|
||||
move_right={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey, "resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventJoypadMotion, "resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":1.0,"script":null)
|
||||
]
|
||||
}
|
||||
jump={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey, "resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventJoypadButton, "resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":false,"script":null)
|
||||
]
|
||||
}
|
||||
shoot={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventMouseButton, "resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null)
|
||||
, Object(InputEventJoypadMotion, "resource_local_to_scene":false,"resource_name":"","device":-1,"axis":5,"axis_value":1.0,"script":null)
|
||||
, Object(InputEventJoypadButton, "resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":10,"pressure":0.0,"pressed":false,"script":null)
|
||||
]
|
||||
}
|
||||
reload={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey, "resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":82,"key_label":0,"unicode":114,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventJoypadButton, "resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":2,"pressure":0.0,"pressed":false,"script":null)
|
||||
]
|
||||
}
|
||||
pause={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey, "resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventJoypadButton, "resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":6,"pressure":0.0,"pressed":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
toggle_fullscreen={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey, "resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventJoypadButton, "resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":4,"pressure":0.0,"pressed":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
grenade={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventMouseButton, "button_index":2)]
|
||||
"events": [Object(InputEventMouseButton, "button_index":2), Object(InputEventJoypadButton, "resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":false,"script":null), Object(InputEventJoypadButton, "resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":9,"pressure":0.0,"pressed":false,"script":null)]
|
||||
}
|
||||
|
||||
look_left={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventJoypadMotion, "resource_local_to_scene":false,"resource_name":"","device":-1,"axis":2,"axis_value":-1.0,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
look_right={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventJoypadMotion, "resource_local_to_scene":false,"resource_name":"","device":-1,"axis":2,"axis_value":1.0,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
look_up={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventJoypadMotion, "resource_local_to_scene":false,"resource_name":"","device":-1,"axis":3,"axis_value":-1.0,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
look_down={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventJoypadMotion, "resource_local_to_scene":false,"resource_name":"","device":-1,"axis":3,"axis_value":1.0,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
ui_accept={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey, "resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194309,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventKey, "resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194310,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventKey, "resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventJoypadButton, "resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
ui_cancel={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey, "resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194305,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventJoypadButton, "resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[rendering]
|
||||
|
||||
+34
-10
@@ -3,8 +3,12 @@ extends Node
|
||||
##
|
||||
## - Web: stessa origine della pagina (window.location.origin) + /api/zombie-fps, cosi'
|
||||
## il browser non ha bisogno di CORS.
|
||||
## - Desktop: nessuna classifica, a meno di --api=http://... (serve ai test). Cosi' una
|
||||
## partita di prova non finisce MAI nella classifica pubblica.
|
||||
## - Desktop: servizio pubblico `DEFAULT_API_BASE`, quindi la classifica e' ATTIVA DI
|
||||
## DEFAULT: non serve nessuna configurazione ne' argomento per leggere e inviare.
|
||||
## - Override espliciti: `--api=<url>` punta a un altro servizio (test), `--offline`
|
||||
## (alias `--no-api`) spegne la classifica per le partite di prova.
|
||||
## - Suite di test (avviate con `--script`): spente, a meno di un `--api=` esplicito,
|
||||
## cosi' nessun test puo' scrivere nella classifica pubblica.
|
||||
## - Due nodi HTTPRequest separati: Godot non ammette due richieste insieme sullo
|
||||
## stesso nodo, e lettura e invio possono sovrapporsi (l'invio avviene alla morte).
|
||||
## - Un errore di rete non deve mai rompere il gioco: si segnala e basta.
|
||||
@@ -14,6 +18,8 @@ signal score_submitted(info: Dictionary)
|
||||
signal submit_failed(reason: String)
|
||||
|
||||
const API_PATH := "/api/zombie-fps"
|
||||
## Servizio pubblico: la classifica funziona senza alcuna configurazione.
|
||||
const DEFAULT_API_BASE := "https://enne2.net/api/zombie-fps"
|
||||
const TOP_LIMIT := 10
|
||||
const RETRY_DELAY := 6.0
|
||||
const RETRY_LIMIT := 3
|
||||
@@ -22,7 +28,7 @@ const DISABLED_REASON := "classifica non attiva in questa build"
|
||||
|
||||
var player_id := ""
|
||||
var base_url := ""
|
||||
## Vero quando non c'e' nessun servizio da usare (build desktop senza --api=).
|
||||
## Vero solo quando il servizio e' spento apposta (--offline) o in una suite di test.
|
||||
var disabled := false
|
||||
|
||||
var _get_request: HTTPRequest
|
||||
@@ -52,19 +58,37 @@ func _ready() -> void:
|
||||
add_child(_retry_timer)
|
||||
_retry_timer.timeout.connect(_send_pending)
|
||||
|
||||
## Ordine di priorita': --api=..., poi l'origine della pagina sul Web. Senza nessuno
|
||||
## dei due non si usa NIENTE: una build desktop di prova non deve poter inviare
|
||||
## punteggi alla classifica pubblica (e' successo: righe "Test" in produzione).
|
||||
func _resolve_base() -> String:
|
||||
for argument in OS.get_cmdline_user_args():
|
||||
## Ordine di priorita': `--api=<url>` (test), `--offline`/`--no-api` (spegne), origine
|
||||
## della pagina sul Web, altrimenti il servizio pubblico. La classifica e' quindi
|
||||
## abilitata di default e non dipende da configurazione; `--offline` esiste solo per
|
||||
## le partite di prova che non devono finire nella classifica pubblica.
|
||||
## `script_run` e' il guard delle suite di test: non e' configurazione utente, la
|
||||
## passa `_resolve_base()` quando il processo gira con `--script`.
|
||||
static func resolve_base(user_args: PackedStringArray, script_run := false) -> String:
|
||||
for argument in user_args:
|
||||
if argument.begins_with("--api="):
|
||||
return argument.substr(6).trim_suffix("/")
|
||||
for argument in user_args:
|
||||
if argument == "--offline" or argument == "--no-api":
|
||||
return ""
|
||||
if OS.has_feature("web") and ClassDB.class_exists("JavaScriptBridge"):
|
||||
var origin := str(JavaScriptBridge.eval("window.location.origin", true))
|
||||
if origin.begins_with("http"):
|
||||
return origin.trim_suffix("/") + API_PATH
|
||||
push_warning("Classifica disattivata: usa --api=http://host:porta per indicare il servizio.")
|
||||
return ""
|
||||
if script_run:
|
||||
return "" # suite di test: mai verso il servizio pubblico
|
||||
return DEFAULT_API_BASE
|
||||
|
||||
## Le suite di test girano come script (`--script ...`): in quel caso la classifica
|
||||
## resta spenta, a meno che il test non abbia passato un suo `--api=`.
|
||||
static func _is_script_run() -> bool:
|
||||
for argument in OS.get_cmdline_args():
|
||||
if argument == "--script" or argument == "-s":
|
||||
return true
|
||||
return false
|
||||
|
||||
func _resolve_base() -> String:
|
||||
return resolve_base(OS.get_cmdline_user_args(), _is_script_run())
|
||||
|
||||
## Godot non ammette due richieste insieme sullo stesso nodo: una lettura chiesta
|
||||
## mentre la precedente e' in volo viene rimandata a fine risposta invece di fallire
|
||||
|
||||
+57
-7
@@ -34,6 +34,18 @@ var death_overlay: ColorRect
|
||||
## True quando la partita e' guidata dai controlli on-screen (touch): il mouse
|
||||
## emulato dai tocchi non deve muovere la visuale ne' sparare al posto del dito.
|
||||
var touch_mode := false
|
||||
## True quando l'ultimo input utile arriva da un gamepad. Il pad non cattura il
|
||||
## puntatore, quindi serve un flag esplicito: senza, sparo e granata resterebbero
|
||||
## bloccati dal gate "mouse catturato" (la stessa trappola gia' corretta per il touch).
|
||||
var gamepad_mode := false
|
||||
## Punti-equivalenti di mouse al secondo con lo stick destro a fondo corsa: 900 px/s
|
||||
## per gli 0.0025 rad/px di apply_look danno 2.25 rad/s (~129 gradi/s).
|
||||
const GAMEPAD_LOOK_SPEED := 900.0
|
||||
## Risposta dello stick: con 1.8 i piccoli spostamenti restano fini per mirare e il
|
||||
## fondo corsa continua a girare veloce.
|
||||
const GAMEPAD_LOOK_EXPONENT := 1.8
|
||||
## Oltre questa ampiezza un movimento di stick conta come "sto usando il pad".
|
||||
const GAMEPAD_ACTIVITY := 0.3
|
||||
## Vero solo dopo l'inizio della partita: al menu il cursore deve restare libero,
|
||||
## altrimenti la schermata iniziale non e' cliccabile.
|
||||
var playing := false
|
||||
@@ -95,7 +107,7 @@ var death_note: Label
|
||||
## viene mai catturato.
|
||||
func begin_play() -> void:
|
||||
playing = true
|
||||
if not touch_mode and DisplayServer.get_name() != "headless":
|
||||
if not touch_mode and not gamepad_mode and DisplayServer.get_name() != "headless":
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
## Riga sotto il messaggio di morte: punteggio inviato, posizione in classifica o
|
||||
@@ -113,12 +125,29 @@ func _input(event: InputEvent) -> void:
|
||||
get_viewport().set_input_as_handled()
|
||||
get_tree().call_deferred("reload_current_scene")
|
||||
|
||||
## Riconosce l'attivita' del pad (pulsante premuto o stick fuori dalla zona morta):
|
||||
## cosi' il gioco passa da mouse a gamepad senza nessuna configurazione.
|
||||
func _is_gamepad_activity(event: InputEvent) -> bool:
|
||||
if event is InputEventJoypadButton:
|
||||
return event.pressed
|
||||
if event is InputEventJoypadMotion:
|
||||
return absf(event.axis_value) > GAMEPAD_ACTIVITY
|
||||
return false
|
||||
|
||||
## Il giocatore puo' mirare/sparare quando il puntatore e' catturato, oppure con touch
|
||||
## o gamepad, che non catturano nulla.
|
||||
func _can_aim() -> bool:
|
||||
return touch_mode or gamepad_mode or Input.mouse_mode == Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if _is_gamepad_activity(event):
|
||||
gamepad_mode = true
|
||||
if playing and OS.has_feature("web") and not touch_mode and event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT and not death_started:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
if event is InputEventMouseMotion and not touch_mode and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||
apply_look(event.relative)
|
||||
if playing and event.is_action_pressed("pause") and not death_started:
|
||||
# Con il pad il puntatore non viene catturato: il toggle del mouse non ha senso.
|
||||
if playing and event.is_action_pressed("pause") and not death_started and not gamepad_mode:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED else Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
## Ricarica: tempi invariati, riusata da tastiera (R) e dal pulsante on-screen.
|
||||
@@ -144,7 +173,7 @@ func start_reload() -> void:
|
||||
func _handle_actions() -> void:
|
||||
if Input.is_action_just_pressed("reload") and not reloading and ammo < 30:
|
||||
start_reload()
|
||||
if Input.is_action_just_pressed("grenade") and not death_started and (touch_mode or Input.mouse_mode == Input.MOUSE_MODE_CAPTURED):
|
||||
if Input.is_action_just_pressed("grenade") and not death_started and _can_aim():
|
||||
throw_grenade()
|
||||
|
||||
## Rotazione della visuale. Unico punto in cui vivono sensibilita' e limiti di
|
||||
@@ -155,19 +184,38 @@ func apply_look(relative: Vector2) -> void:
|
||||
rotate_y(-relative.x * 0.0025)
|
||||
camera.rotation.x = clampf(camera.rotation.x - relative.y * 0.0025, -1.4, 1.4)
|
||||
|
||||
## Visuale analogica: il mouse arriva come InputEventMouseMotion relativo, lo stick
|
||||
## destro come azioni. Qui lo stick diventa "pixel relativi equivalenti" e passa ad
|
||||
## apply_look(), che resta l'unico posto con sensibilita' e limiti di inclinazione.
|
||||
## La curva viene applicata sulla sola ampiezza, quindi la direzione resta esatta.
|
||||
func _handle_gamepad_look(dt: float) -> void:
|
||||
if not gamepad_mode or not playing or touch_mode:
|
||||
return
|
||||
if hp <= 0 or death_started:
|
||||
return
|
||||
# Deadzone esplicita 0.2, la stessa del movimento.
|
||||
var look := Input.get_vector("look_left", "look_right", "look_up", "look_down", 0.2)
|
||||
if look == Vector2.ZERO:
|
||||
return
|
||||
var magnitude := clampf(look.length(), 0.0, 1.0)
|
||||
var shaped := look.normalized() * pow(magnitude, GAMEPAD_LOOK_EXPONENT)
|
||||
apply_look(shaped * GAMEPAD_LOOK_SPEED * dt)
|
||||
|
||||
func _physics_process(dt: float) -> void:
|
||||
cooldown -= dt
|
||||
grenade_cooldown = maxf(0, grenade_cooldown - dt)
|
||||
flash.light_energy = maxf(0, flash.light_energy - dt * 60)
|
||||
gun.position.z = lerpf(gun.position.z, 0, dt * 15)
|
||||
if hp <= 0:
|
||||
if Input.is_action_just_pressed("reload"):
|
||||
get_tree().reload_current_scene() # R o pulsante Ricarica a partita finita
|
||||
# R (tastiera) oppure X/A del pad: stessi tempi, nessun menu da navigare.
|
||||
if Input.is_action_just_pressed("reload") or Input.is_action_just_pressed("jump"):
|
||||
get_tree().reload_current_scene() # R, X/A del pad o pulsante Ricarica a partita finita
|
||||
return
|
||||
_regenerate_health(dt)
|
||||
if ammo <= 0 and not reloading:
|
||||
start_reload()
|
||||
_handle_actions()
|
||||
_handle_gamepad_look(dt)
|
||||
# Deadzone esplicita piccola (0.2): con la 0.5 delle azioni la corsa utile del
|
||||
# joystick touch si dimezzerebbe, mentre per tastiera/joypad nulla cambia.
|
||||
var input := Input.get_vector("move_left", "move_right", "move_forward", "move_back", 0.2)
|
||||
@@ -184,7 +232,7 @@ func _physics_process(dt: float) -> void:
|
||||
Audio.play(self, "player/pl_metal%d.wav" % randi_range(1, 4), false, -18)
|
||||
if Input.is_action_just_pressed("shoot") and ammo <= 0 and not reloading:
|
||||
Audio.play(self, "weapons/dryfire1.wav")
|
||||
if Input.is_action_pressed("shoot") and cooldown <= 0 and ammo > 0 and not reloading and (touch_mode or Input.mouse_mode == Input.MOUSE_MODE_CAPTURED):
|
||||
if Input.is_action_pressed("shoot") and cooldown <= 0 and ammo > 0 and not reloading and _can_aim():
|
||||
shoot()
|
||||
|
||||
func shoot() -> void:
|
||||
@@ -264,7 +312,9 @@ func _die() -> void:
|
||||
tween.tween_property(camera, "rotation:x", 0.08, 0.9)
|
||||
tween.tween_property(death_overlay, "color:a", 1.0, 1.8).set_delay(0.7)
|
||||
var message := Label.new()
|
||||
message.text = "SEI MORTO" if touch_mode else "SEI MORTO\nPremi R per ricominciare"
|
||||
message.text = "SEI MORTO" if touch_mode else (
|
||||
"SEI MORTO\nPremi A o X per ricominciare" if gamepad_mode
|
||||
else "SEI MORTO\nPremi R per ricominciare")
|
||||
message.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
message.add_theme_font_size_override("font_size", 36)
|
||||
message.modulate.a = 0
|
||||
|
||||
@@ -16,6 +16,10 @@ var player: Node
|
||||
var leaderboard: Node
|
||||
var profile := {}
|
||||
var name_field: LineEdit
|
||||
var play_button: Button
|
||||
## Riga di aiuto che compare solo quando c'e' un gamepad collegato: con mouse e
|
||||
## tastiera la schermata resta identica a prima.
|
||||
var pad_hint: Label
|
||||
var rows_box: VBoxContainer
|
||||
var status_label: Label
|
||||
var me_label: Label
|
||||
@@ -29,6 +33,9 @@ func _ready() -> void:
|
||||
name_locked = Profile.is_name_locked(profile)
|
||||
rows_shown = ROWS_TOUCH if (player != null and player.touch_mode) else ROWS_DESKTOP
|
||||
_build_ui()
|
||||
# Hotplug: la riga di aiuto del pad compare/scompare senza riavviare il gioco.
|
||||
Input.joy_connection_changed.connect(_on_joy_connection_changed)
|
||||
_refresh_pad_hint()
|
||||
if leaderboard != null:
|
||||
leaderboard.fetch()
|
||||
|
||||
@@ -75,6 +82,13 @@ func _build_ui() -> void:
|
||||
play.custom_minimum_size = Vector2(0, 56)
|
||||
play.pressed.connect(_start)
|
||||
column.add_child(play)
|
||||
play_button = play
|
||||
|
||||
# Aiuto per il pad: visibile solo se un controller e' collegato.
|
||||
pad_hint = _label("", 13, DIM, HORIZONTAL_ALIGNMENT_CENTER)
|
||||
pad_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
pad_hint.visible = false
|
||||
column.add_child(pad_hint)
|
||||
|
||||
var panel := PanelContainer.new()
|
||||
var style := StyleBoxFlat.new()
|
||||
@@ -140,6 +154,21 @@ func _label(text: String, size: int, color: Color, align: int) -> Label:
|
||||
label.add_theme_font_size_override("font_size", size)
|
||||
return label
|
||||
|
||||
func _on_joy_connection_changed(_device: int, _connected: bool) -> void:
|
||||
_refresh_pad_hint()
|
||||
|
||||
## Testo e visibilita' dell'aiuto gamepad. Il nome resta da inserire: con il solo pad
|
||||
## serve una tastiera su schermo (fase successiva), qui si documentano i comandi.
|
||||
func _refresh_pad_hint() -> void:
|
||||
if pad_hint == null:
|
||||
return
|
||||
var pads := Input.get_connected_joypads()
|
||||
pad_hint.visible = not pads.is_empty()
|
||||
if not pads.is_empty():
|
||||
pad_hint.text = "PAD — stick sx muovi · stick dx visuale · RT spara · X ricarica · B/LB granata · A salta · Start pausa · Back schermo intero"
|
||||
if play_button != null and not play_button.has_focus() and name_field != null and not name_field.has_focus():
|
||||
play_button.grab_focus()
|
||||
|
||||
func _set_status(text: String) -> void:
|
||||
if status_label != null:
|
||||
status_label.text = text
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
extends SceneTree
|
||||
## Verifica il supporto gamepad:
|
||||
## - ogni azione di gioco ha almeno un binding joypad, con i tasti/gli assi attesi;
|
||||
## - gli assi di movimento sono sullo stick sinistro, la visuale sullo stick destro;
|
||||
## - ui_accept/ui_cancel accettano A/B del pad SENZA perdere Invio/Spazio/Esc;
|
||||
## - un pad reale (device != -1) attiva le azioni mappate con device -1;
|
||||
## - l'attivita' del pad accende player.gamepad_mode, il puntatore non viene
|
||||
## catturato e la visuale gira con lo stick destro passando da apply_look().
|
||||
##
|
||||
## Avviare con: --headless --path . --script res://tools/test_gamepad.gd
|
||||
## Con un display reale: --rendering-method gl_compatibility
|
||||
|
||||
## azione -> eventi joypad attesi come stringhe leggibili (il formato del controllo)
|
||||
const EXPECTED := {
|
||||
"move_forward": "axis1-1.0",
|
||||
"move_back": "axis1+1.0",
|
||||
"move_left": "axis0-1.0",
|
||||
"move_right": "axis0+1.0",
|
||||
"jump": "btn0",
|
||||
"reload": "btn2",
|
||||
"pause": "btn6",
|
||||
"toggle_fullscreen": "btn4",
|
||||
"grenade": "btn1+btn9",
|
||||
"shoot": "axis5+1.0+btn10",
|
||||
"look_left": "axis2-1.0",
|
||||
"look_right": "axis2+1.0",
|
||||
"look_up": "axis3-1.0",
|
||||
"look_down": "axis3+1.0",
|
||||
}
|
||||
|
||||
var world: Node3D
|
||||
var failures := 0
|
||||
var checks := 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run_test")
|
||||
|
||||
func check(label: String, condition: bool, detail: String = "") -> void:
|
||||
checks += 1
|
||||
if condition:
|
||||
print("PASS: " + label)
|
||||
else:
|
||||
failures += 1
|
||||
print("FAIL: %s %s" % [label, detail])
|
||||
|
||||
## Rappresentazione compatta degli eventi joypad di un'azione, ordinata.
|
||||
func _joypad_signature(action: String) -> String:
|
||||
var parts: Array[String] = []
|
||||
for event in InputMap.action_get_events(action):
|
||||
if event is InputEventJoypadButton:
|
||||
parts.append("btn%d" % event.button_index)
|
||||
elif event is InputEventJoypadMotion:
|
||||
parts.append("axis%d%+.1f" % [event.axis, event.axis_value])
|
||||
parts.sort()
|
||||
return "+".join(parts)
|
||||
|
||||
func _has_key(action: String, keycode: int) -> bool:
|
||||
for event in InputMap.action_get_events(action):
|
||||
if event is InputEventKey and (event.keycode == keycode or event.physical_keycode == keycode):
|
||||
return true
|
||||
return false
|
||||
|
||||
func _joypad_devices(action: String) -> Array[int]:
|
||||
var devices: Array[int] = []
|
||||
for event in InputMap.action_get_events(action):
|
||||
if event is InputEventJoypadButton or event is InputEventJoypadMotion:
|
||||
devices.append(event.device)
|
||||
return devices
|
||||
|
||||
## Pressione/rilascio di un pulsante del pad, come un controller vero.
|
||||
func _pad_button(index: int, pressed: bool, device := 0) -> void:
|
||||
var event := InputEventJoypadButton.new()
|
||||
event.device = device
|
||||
event.button_index = index
|
||||
event.pressed = pressed
|
||||
Input.parse_input_event(event)
|
||||
Input.flush_buffered_events()
|
||||
|
||||
## Movimento di un asse del pad (valore fra -1 e 1).
|
||||
func _pad_axis(axis: int, value: float, device := 0) -> void:
|
||||
var event := InputEventJoypadMotion.new()
|
||||
event.device = device
|
||||
event.axis = axis
|
||||
event.axis_value = value
|
||||
Input.parse_input_event(event)
|
||||
Input.flush_buffered_events()
|
||||
|
||||
func _frames(count: int) -> void:
|
||||
for i in count:
|
||||
await process_frame
|
||||
|
||||
func _steps(count: int) -> void:
|
||||
for i in count:
|
||||
await physics_frame
|
||||
|
||||
func run_test() -> void:
|
||||
# --- mappa dei comandi ---
|
||||
for action: String in EXPECTED:
|
||||
check("\"%s\" ha i binding joypad attesi" % action,
|
||||
_joypad_signature(action) == EXPECTED[action],
|
||||
"trovato \"%s\" atteso \"%s\"" % [_joypad_signature(action), EXPECTED[action]])
|
||||
|
||||
var all_any := true
|
||||
var offenders: Array[String] = []
|
||||
for action: String in EXPECTED:
|
||||
for device in _joypad_devices(action):
|
||||
if device != -1:
|
||||
all_any = false
|
||||
offenders.append("%s(device %d)" % [action, device])
|
||||
check("i binding joypad accettano qualunque pad (device -1)", all_any, str(offenders))
|
||||
|
||||
# ui_*: il motore 4.7.1 non mappa alcun pulsante su ui_accept/ui_cancel, quindi
|
||||
# vanno aggiunti a mano tenendo i tasti di tastiera gia' presenti.
|
||||
check("ui_accept accetta il pulsante A del pad",
|
||||
_joypad_signature("ui_accept") == "btn0", _joypad_signature("ui_accept"))
|
||||
check("ui_accept conserva Invio", _has_key("ui_accept", KEY_ENTER))
|
||||
check("ui_accept conserva Invio del tastierino", _has_key("ui_accept", KEY_KP_ENTER))
|
||||
check("ui_accept conserva Spazio", _has_key("ui_accept", KEY_SPACE))
|
||||
check("ui_cancel accetta il pulsante B del pad",
|
||||
_joypad_signature("ui_cancel") == "btn1", _joypad_signature("ui_cancel"))
|
||||
check("ui_cancel conserva Esc", _has_key("ui_cancel", KEY_ESCAPE))
|
||||
|
||||
# --- pipeline eventi: un pad qualsiasi (device 0) deve attivare le azioni ---
|
||||
_pad_button(JOY_BUTTON_A, true)
|
||||
await _frames(1)
|
||||
check("A del pad attiva l'azione \"jump\"", Input.is_action_pressed("jump"))
|
||||
check("A del pad attiva ui_accept", Input.is_action_pressed("ui_accept"))
|
||||
_pad_button(JOY_BUTTON_A, false)
|
||||
await _frames(1)
|
||||
check("il rilascio di A spegne \"jump\"", not Input.is_action_pressed("jump"))
|
||||
|
||||
_pad_axis(JOY_AXIS_LEFT_Y, -1.0)
|
||||
await _frames(1)
|
||||
check("stick sinistro in alto attiva \"move_forward\"",
|
||||
Input.is_action_pressed("move_forward"))
|
||||
_pad_axis(JOY_AXIS_LEFT_Y, 0.0)
|
||||
await _frames(1)
|
||||
|
||||
_pad_axis(JOY_AXIS_RIGHT_X, 1.0)
|
||||
await _frames(1)
|
||||
check("stick destro a destra attiva \"look_right\"", Input.is_action_pressed("look_right"))
|
||||
_pad_axis(JOY_AXIS_RIGHT_X, 0.0)
|
||||
await _frames(1)
|
||||
|
||||
# --- mondo di gioco: gamepad_mode e visuale analogica ---
|
||||
world = load("res://scenes/main.tscn").instantiate()
|
||||
root.add_child(world)
|
||||
await _frames(2)
|
||||
var player: Node = world.player
|
||||
var screen = world.start_screen
|
||||
var gioca: Button = null
|
||||
if screen != null:
|
||||
for button in screen.find_children("*", "Button", true, false):
|
||||
if button.text == "GIOCA":
|
||||
gioca = button
|
||||
if screen != null and screen.get("name_field") != null:
|
||||
screen.name_field.text = "PadTest"
|
||||
if gioca != null:
|
||||
gioca.pressed.emit()
|
||||
await _frames(2)
|
||||
check("la partita e' iniziata", player.playing == true, "playing=%s" % str(player.playing))
|
||||
check("senza attivita' di pad gamepad_mode resta falso", player.gamepad_mode == false)
|
||||
|
||||
_pad_button(JOY_BUTTON_A, true)
|
||||
_pad_button(JOY_BUTTON_A, false)
|
||||
await _frames(1)
|
||||
check("un pulsante del pad accende player.gamepad_mode", player.gamepad_mode == true)
|
||||
|
||||
var yaw_before: float = player.rotation.y
|
||||
_pad_axis(JOY_AXIS_RIGHT_X, 1.0)
|
||||
await _steps(12)
|
||||
_pad_axis(JOY_AXIS_RIGHT_X, 0.0)
|
||||
await _steps(2)
|
||||
check("lo stick destro fa ruotare la visuale verso destra",
|
||||
player.rotation.y < yaw_before - 0.01,
|
||||
"prima=%.3f dopo=%.3f" % [yaw_before, player.rotation.y])
|
||||
|
||||
var pitch_before: float = player.camera.rotation.x
|
||||
_pad_axis(JOY_AXIS_RIGHT_Y, 1.0)
|
||||
await _steps(8)
|
||||
_pad_axis(JOY_AXIS_RIGHT_Y, 0.0)
|
||||
await _steps(2)
|
||||
check("lo stick destro in basso abbassa l'inclinazione della camera",
|
||||
player.camera.rotation.x < pitch_before,
|
||||
"prima=%.3f dopo=%.3f" % [pitch_before, player.camera.rotation.x])
|
||||
check("l'inclinazione resta dentro i limiti di apply_look",
|
||||
player.camera.rotation.x >= -1.4 - 0.001, "pitch=%.3f" % player.camera.rotation.x)
|
||||
|
||||
# Il pad non deve catturare il puntatore (in headless non e' verificabile):
|
||||
# quello che conta e' che il gate di sparo/granata non dipenda dal mouse.
|
||||
check("con gamepad_mode il gate di mira e' aperto anche senza mouse catturato",
|
||||
player._can_aim() == true and player.touch_mode == false,
|
||||
"can_aim=%s touch=%s" % [str(player._can_aim()), str(player.touch_mode)])
|
||||
|
||||
print("%d controlli superati, %d falliti" % [checks - failures, failures])
|
||||
quit(1 if failures > 0 else 0)
|
||||
@@ -1,5 +1,6 @@
|
||||
extends SceneTree
|
||||
## Exact damage regression + visible game identity, no public score submissions.
|
||||
## Exact damage regression + visible game identity. La classifica e' attiva di default
|
||||
## nelle build di gioco; qui, girando come --script, resta spenta.
|
||||
const Player = preload("res://scripts/player.gd")
|
||||
const Grenade = preload("res://scripts/grenade.gd")
|
||||
const Boss = preload("res://scripts/alien_grunt.gd")
|
||||
@@ -41,7 +42,8 @@ func run_test() -> void:
|
||||
if label.text == "Work-Death Balance":
|
||||
title_found = true
|
||||
check(title_found, "menu mostra Work-Death Balance")
|
||||
check(world.leaderboard.disabled, "build di prova non invia punteggi pubblici")
|
||||
check(world.leaderboard.disabled and world.leaderboard.base_url == "",
|
||||
"suite di test: nessun invio alla classifica pubblica")
|
||||
var zombie = load("res://scripts/zombie.gd").new()
|
||||
zombie.player = world.player
|
||||
zombie.position = Vector3(0, 0.05, -3)
|
||||
|
||||
@@ -8,6 +8,7 @@ extends SceneTree
|
||||
## (con display, per gli eventi di puntamento: --rendering-method gl_compatibility)
|
||||
const Profile = preload("res://scripts/player_profile.gd")
|
||||
const StartScreen = preload("res://scripts/start_screen.gd")
|
||||
const Leaderboard = preload("res://scripts/leaderboard.gd")
|
||||
|
||||
var world: Node3D
|
||||
var screen: CanvasLayer
|
||||
@@ -101,10 +102,21 @@ func test_world_menu() -> void:
|
||||
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",
|
||||
world.leaderboard.disabled == true and world.leaderboard.base_url == "",
|
||||
# La classifica e' attiva di default (nessuna configurazione). Il nodo qui e' spento
|
||||
# perche' questa suite gira come --script: le regole si verificano su resolve_base().
|
||||
check("senza argomenti la classifica e' attiva sul servizio pubblico",
|
||||
Leaderboard.resolve_base(PackedStringArray(), false) == Leaderboard.DEFAULT_API_BASE
|
||||
and Leaderboard.DEFAULT_API_BASE == "https://enne2.net/api/zombie-fps",
|
||||
"base=%s" % str(Leaderboard.resolve_base(PackedStringArray(), false)))
|
||||
check("--offline (e --no-api) spengono la classifica per le partite di prova",
|
||||
Leaderboard.resolve_base(PackedStringArray(["--offline"]), false) == ""
|
||||
and Leaderboard.resolve_base(PackedStringArray(["--no-api"]), false) == "")
|
||||
check("--api= vince su --offline e normalizza la barra finale",
|
||||
Leaderboard.resolve_base(PackedStringArray(["--offline", "--api=http://127.0.0.1:8799/"]), false)
|
||||
== "http://127.0.0.1:8799")
|
||||
check("in una suite di test la classifica resta spenta",
|
||||
world.leaderboard.disabled == true and world.leaderboard.base_url == ""
|
||||
and Leaderboard.resolve_base(PackedStringArray(), true) == "",
|
||||
"base=%s disattivata=%s" % [str(world.leaderboard.base_url), str(world.leaderboard.disabled)])
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Estrae singoli file da uno ZIP remoto via HTTP Range (senza scaricare 1,28 GB).
|
||||
|
||||
Il tpz dei template Godot e' uno zip: serve solo templates/linux_release.arm64.
|
||||
Legge EOCD + central directory con Range, poi scarica solo il membro richiesto.
|
||||
"""
|
||||
import binascii
|
||||
import struct
|
||||
import sys
|
||||
import urllib.request
|
||||
import zlib
|
||||
|
||||
URL = ("https://github.com/godotengine/godot-builds/releases/download/"
|
||||
"4.7.1-stable/Godot_v4.7.1-stable_export_templates.tpz")
|
||||
|
||||
|
||||
def get_range(url, start, end=None):
|
||||
if end is None:
|
||||
rng = f"bytes={start}-"
|
||||
else:
|
||||
rng = f"bytes={start}-{end}"
|
||||
req = urllib.request.Request(url, headers={"Range": rng})
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
return r.read(), r.status
|
||||
|
||||
|
||||
def main(wanted, dest_dir):
|
||||
# 1. dimensione totale
|
||||
req = urllib.request.Request(URL, method="HEAD")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
total = int(r.headers["Content-Length"])
|
||||
print(f"zip remoto: {total} byte")
|
||||
|
||||
# 2. EOCD negli ultimi 64 KiB
|
||||
tail_len = min(65536, total)
|
||||
tail, _ = get_range(URL, total - tail_len)
|
||||
pos = tail.rfind(b"PK\x05\x06")
|
||||
if pos < 0:
|
||||
sys.exit("EOCD non trovato (zip64?)")
|
||||
(_, _, _, _, n_entries, cd_size, cd_off, _) = struct.unpack(
|
||||
"<IHHHHIIH", tail[pos:pos + 22])
|
||||
print(f"voci: {n_entries}, central directory: {cd_off}+{cd_size}")
|
||||
|
||||
# 3. central directory
|
||||
cd, _ = get_range(URL, cd_off, cd_off + cd_size - 1)
|
||||
|
||||
entries = {}
|
||||
i = 0
|
||||
while i + 46 <= len(cd) and cd[i:i + 4] == b"PK\x01\x02":
|
||||
(method, csize, usize, name_len, extra_len, comment_len,
|
||||
local_off) = (struct.unpack("<H", cd[i + 10:i + 12])[0],
|
||||
struct.unpack("<I", cd[i + 20:i + 24])[0],
|
||||
struct.unpack("<I", cd[i + 24:i + 28])[0],
|
||||
struct.unpack("<H", cd[i + 28:i + 30])[0],
|
||||
struct.unpack("<H", cd[i + 30:i + 32])[0],
|
||||
struct.unpack("<H", cd[i + 32:i + 34])[0],
|
||||
struct.unpack("<I", cd[i + 42:i + 46])[0])
|
||||
name = cd[i + 46:i + 46 + name_len].decode("utf-8", "replace")
|
||||
entries[name] = (method, csize, usize, local_off)
|
||||
i += 46 + name_len + extra_len + comment_len
|
||||
|
||||
print(f"membri indicizzati: {len(entries)}")
|
||||
if wanted not in entries:
|
||||
candidati = [n for n in entries if "arm64" in n]
|
||||
sys.exit(f"'{wanted}' non presente. Candidati arm64: {candidati}")
|
||||
|
||||
method, csize, usize, local_off = entries[wanted]
|
||||
print(f"{wanted}: metodo={method} compresso={csize} originale={usize}")
|
||||
|
||||
# 4. header locale per sapere dove iniziano davvero i dati
|
||||
lh, _ = get_range(URL, local_off, local_off + 29)
|
||||
if lh[:4] != b"PK\x03\x04":
|
||||
sys.exit("header locale inatteso")
|
||||
name_len = struct.unpack("<H", lh[26:28])[0]
|
||||
extra_len = struct.unpack("<H", lh[28:30])[0]
|
||||
data_start = local_off + 30 + name_len + extra_len
|
||||
|
||||
# 5. dati compressi del solo membro
|
||||
print(f"scarico {csize} byte da offset {data_start} ...")
|
||||
data, status = get_range(URL, data_start, data_start + csize - 1)
|
||||
print(f"ricevuti {len(data)} byte (http {status})")
|
||||
|
||||
if method == 8:
|
||||
raw = zlib.decompress(data, -15)
|
||||
elif method == 0:
|
||||
raw = data
|
||||
else:
|
||||
sys.exit(f"metodo di compressione non gestito: {method}")
|
||||
|
||||
print(f"decompresso: {len(raw)} byte (atteso {usize})")
|
||||
if len(raw) != usize:
|
||||
sys.exit("dimensione non corrispondente")
|
||||
out = f"{dest_dir}/{wanted.split('/')[-1]}"
|
||||
with open(out, "wb") as fh:
|
||||
fh.write(raw)
|
||||
print(f"scritto {out}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else "templates/linux_release.arm64"
|
||||
dest = sys.argv[2] if len(sys.argv) > 2 else "/tmp/godot-dl"
|
||||
main(target, dest)
|
||||
Reference in New Issue
Block a user