- TypeScript 70.7%
- JavaScript 23.8%
- HTML 5.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .agents | ||
| .claude | ||
| .serena | ||
| .superpowers/sdd | ||
| client | ||
| docs | ||
| piano | ||
| preview | ||
| scripts | ||
| server/src | ||
| shared/src | ||
| skill-observations | ||
| .dockerignore | ||
| .gitignore | ||
| AGENTS.md | ||
| Caddyfile | ||
| CLAUDE.md | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
SPARA — FPS nelle città d'Italia
FPS multiplayer che gira interamente nel browser, ambientato tra i palazzi reali delle città italiane (impronte e altezze da OpenStreetMap): Via Sparano a Bari e i Quartieri Spagnoli di Napoli, con selezione mappa, server autoritativo Node.js a stanze e asset 3D/audio open source (CC0).
Arsenale (tasti 1-9)
Impostazione da arena: ogni giocatore ha tutte e nove le armi addosso e nessuno sceglie niente al menu — si entra tutti con la stessa dotazione. Tre famiglie, distinte per come consegnano il danno — e tutte e tre risolte dal server, mai dal client.
| arma | fuoco primario | fuoco secondario (tasto destro, caricato) | |
|---|---|---|---|
| 1 | AR-77 | assalto, versatile | SALVA PERFORANTE — 3 colpi che infilano 3 bersagli |
| 2 | VESPA-9 | SMG, cadenza alta | SCIAME — 12 colpi a ventaglio stretto |
| 3 | LUPARA | 8 pallettoni | DOPPIETTA — 18 pallettoni e un calcio che ti spara indietro |
| 4 | LONGSHOT | cecchino, headshot ×2 | PERFORANTE — 130 danni attraverso cinque corpi |
| 5 | FEDELE | pistola, munizioni infinite | COLPO DI GRAZIA — 75 danni, headshot ×3 |
| 6 | PALLA DI FUOCO | lenta e ad arco: si schiva | METEORA — 9 m di raggio, 120 di scoppio |
| 7 | LANCIARAZZI | area larga, salto col razzo | SALVA DI RAZZI — tre razzi a ventaglio |
| 8 | RAGGIO LASER | danno al secondo | LANCIA DI LUCE — istantanea, infila quattro bersagli |
| 9 | PLASMA | raffica di bolle | BOMBA AL PLASMA — 8 m di scoppio |
Il fuoco secondario è un colpo CARICATO
Si tiene premuto il tasto destro, l'arma si carica per uno o due secondi, e allo scadere il colpo parte da solo. Mollare prima annulla, senza consumare nulla.
È la regola che lo rende un'arma da attacco preparato e non da mischia: mentre carichi sei fermo, e la carica si vede addosso a te — chi ti guarda vede l'alone crescere e ha un secondo per coprirsi. Un colpo che cancella un avversario deve essere telegrafato, o non ha contromossa. In compenso costa munizioni vere e lascia una pausa dopo lo sparo.
I proiettili sono entità simulate dal server: viaggiano con velocità e gravità proprie, collidono lungo il segmento percorso (niente attraversamenti a velocità alta), e all'impatto fanno danno ad area con controllo della linea di vista — dietro un muro non ti arriva nulla. Chi spara si fa male da solo in misura ridotta, il che rende possibile il salto col razzo.
Ogni arma ha una riserva finita oltre al caricatore — poca sulle armi di potenza (6 razzi), infinita sulla pistola — e a secco il server impone la pistola invece di lasciarti un'arma inerte in mano. È la riserva a dare senso ai pickup di munizioni, e a fare dei tetti un posto per cui vale la pena litigare.
Raccolte e vie alte
14 pickup in strada (vita +25, casse di munizioni) e 2 mega-vita sulle terrazze, che sfondano il tetto dei 100 HP fino a 150 e poi colano via. Sono autoritativi: decide il server, la disponibilità viaggia in ogni snapshot, e chi è già a posto NON consuma il pickup.
Sui tetti ci si arriva: scale antincendio esterne a sei ballatoi sfalsati con un varco nel parapetto, passerelle fra terrazze vicine, altane in mezzo alla strada con una cassa per salirci, nicchie coperte dove sparire alla vista dall'alto. Le quote nascono dal movimento vero (il salto porta i piedi a 1,60 m), non a occhio.
E il doppio clic sul salto porta l'apice a 3,2 m: apre scorciatoie — il secondo ballatoio, il cofano di un'auto, una nicchia — senza cancellare il costo di salire.
Le due città non si giocano allo stesso modo
Il carattere non è dipinto sopra, è nella pianta. Bari murattiano ha strade larghe e diritte, quindi tiri lunghi: si compensa con più impalcature a due impalcati, che spezzano le linee di tiro in verticale. I Quartieri Spagnoli sono vicoli, quindi niente tiri lunghi: il carattere lo danno i sottopassi, il doppio rispetto a Bari, perché nei Quartieri i palazzi si bucano davvero.
Un sottopasso non è arredamento: un isolato che prima si girava attorno diventa attraversabile, e ogni arco crea due angoli ciechi e una via di fuga. Sopra le vetrine ci sono tettoie praticabili: ci si sale con un doppio salto o dal cofano di un'auto, e si corre lungo la facciata sopra la testa di tutti.
Teletrasporti
Tre coppie per mappa, bidirezionali e autoritative come i pickup: una di rotazione fra i due punti più lontani (chi respawna dalla parte sbagliata non perde la partita in tempo di cammino), una di salita verso una terrazza, e una che porta al Nido. Dopo un salto c'è una pausa, o si rimbalzerebbe stando fermi sulla piastra d'arrivo.
Tre segreti, tre modi di trovarli
| segreto | come ci si arriva | dov'è | premio |
|---|---|---|---|
| Santuario | strisciando in un cunicolo e accorgendosi che a metà si allarga | dentro un palazzo pieno | mega-vita |
| Nido del corvo | solo col teletrasporto — ed è anche l'unica uscita, perché saltare giù da venti metri è mortale | tetto del palazzo più alto | mega-vita e mezza mappa a vista |
| Ripostiglio | doppio salto dalla tettoia sottostante; da terra non si arriva | nicchia in facciata a 5,6 m | cassa di munizioni |
Nessuno dei tre è segnalato. Il Nido è l'unico che si vede da fuori — ed è inutile vederlo, se non si trova la piastra che ci porta.
Test di regressione
Con il server acceso:
npm run debug # PROVA TUTTO e stampa un rapporto solo (~10 min)
npm run debug -- veloce # solo controlli statici e prove brevi (~100 s)
I singoli, se serve isolare qualcosa:
npm run test:proiettili razzo # oppure: fuoco | plasma
npm run test:pickup vita # oppure: munizioni
npm run test:secondario 4 # fuoco caricato dell'arma indicata
npm run test:altura # scale antincendio e doppio salto
npm run test:teletrasporto # viaggio, pausa e ritorno
npm run misura:banda 8 20 # quanta banda e CPU chiede una partita
Il rapporto distingue tre esiti e non due: superato, fallito e non concludente. Un test che non riesce ad arrivare sul posto non dice nulla sul gioco, e contarlo come rosso insegna a ignorare i rossi.
I test collegano client veri al server e li fanno camminare fino al posto con un percorso calcolato sulla mappa vera, verificando la linea di tiro con la stessa funzione di raycast che usa il server. Girano in Node e non nel browser di proposito: la scheda in secondo piano strozza i timer a ~1 Hz e un bot pilotato da lì non arriva mai a destinazione.
Direzione artistica: stylized
La città è generata a parallelepipedi dalle impronte OpenStreetMap. Un tentativo precedente provava a nasconderlo con scansioni fotogrammetriche e post-processing pesante: costava 52 ms/frame, 1586 draw call e 244 milioni di triangoli per frame (716 serrande da 9.400 triangoli l'una, ridisegnate 5 volte per frame fra le cascate d'ombra) e sembrava comunque finto.
Ora la geometria a scatole è assunta come stile: superfici piatte, palette mediterranea, sole caldo contro ombre fredde, nebbia per la profondità.
| versione "realistica" | oggi | |
|---|---|---|
| ms/frame | 52,49 | 1,17 |
| draw call per frame | 1.586 | 255 |
| triangoli per frame | 244.026.521 | 689.534 |
| asset scaricati all'avvio | ~76 MB | ~5 MB |
Il numero di oggi è misurato dopo aver aggiunto pickup, teletrasporti, sottopassi, impalcature, tettoie e i tre segreti: la geometria nuova costa 13.000 triangoli in tutto, perché finisce negli stessi bucket fusi della città. Un frame a 60 FPS ne ha 16,7 di budget: siamo a un quattordicesimo.
Come ci si arriva:
- tutta la città opaca in una sola geometria fusa con i colori nei vertici → l'abitato costa 2 draw call, non 1.586;
- una sola luce direzionale la cui shadow camera insegue il giocatore (35 m su 2048 px ≈ 3,4 cm per texel, più nitida delle 4 cascate di prima) con aggancio alla griglia dei texel per non far sfarfallare i bordi;
- nessun post-processing: l'antialiasing è quello hardware, il carattere viene da luce, palette e nebbia;
- cielo a gradiente generato in codice e zero texture: niente HDRI da 5 MB né mappe PBR da 33 MB;
- props procedurali (lampioni, panchine, cassonetti, persiane, balconi, tende dei negozi) da decine di triangoli, fusi negli stessi bucket.
Le armi sono scalate alla misura reale (pistola 22 cm, cecchino 115 cm) e
il viewmodel è inquadrato misurando dove cade in coordinate schermo
(window.__spara.weaponScreen()).
Feature v2 (stile CoD): palazzi con interni accessibili (porte, finestre, scale, balconi, terrazze con parapetto), cunicoli percorribili solo accovacciati, auto parcheggiate come coperture, viewmodel animati (fuoco e ricarica dai rig Quaternius), granate con fisica e rimbalzi, headshot, falloff di danno, penetrazione dei materiali sottili (wallbang), spread dinamico, crouch, scivolata con slide-cancel, tac-sprint, mantle automatico sui bordi, danno da caduta, minimappa rotante con ping degli spari, mirino da cecchino, bossoli, decal dei fori con particelle per materiale, fischio dei proiettili, match a 20 kill con vittoria e reset.
Il tuo avatar
Nella prima schermata si compone il personaggio: colore del corpo (piatto, come tutto il resto) e maschera. Colore e maschera viaggiano nel protocollo e si vedono addosso agli avversari — a trenta metri il nome non si legge, il colore sì, ed è quello che ti dice se sparare.
Le maschere sono archetipi riconoscibili — il politico, la popstar, il
tecnologo, il calciatore, l'anchorman, il rivoluzionario, l'influencer, il
giudice — e non ritratti di persone reali identificabili: mettere la faccia di
qualcuno che esiste su un bersaglio da sparare è un problema di diritto
d'immagine, non una scelta di stile. Sono disegnate in codice
(client/src/avatar.ts): aggiungerne una è una funzione di disegno in più, e
la stessa funzione serve sia l'anteprima del menu sia il volto in 3D, così non
possono divergere.
Giocare con amici veri
Un hosting statico non basta: il client è fatto di file, ma il server di gioco è un processo che deve restare acceso. Misurato con otto giocatori in movimento e a fuoco: 87 KB/s in uscita, 4-6 % di un core, 50 MB di RAM — ci sta su un VPS da pochi euro al mese. Analisi completa, opzioni e passi di deploy in docs/HOSTING.md.
Avvio
npm install
npm run dev
Un solo comando: parte il server di gioco (ws://localhost:8080) e il
client Vite (http://localhost:5173). Apri più tab (o più computer sulla
stessa LAN, usando l'indirizzo Network stampato da Vite), inserisci uno
User ID e premi GIOCA.
Comandi
| Input | Azione |
|---|---|
WASD |
movimento |
SHIFT |
sprint (tenuto: tac-sprint) |
C / CTRL |
crouch — in corsa: scivolata (salto per slide-cancel) |
SPAZIO |
salto / mantle sui bordi — doppio clic: salto doppia altezza |
| mouse | click spara · tasto destro tenuto: fuoco secondario caricato |
TAB (tenuto) |
zoom ottico (FOV 75 → 26) con qualunque arma |
G |
granata |
R |
ricarica |
1 … 9 / rotella |
arma (le hai tutte addosso) |
P (tenuto) |
tabellone punteggi |
Deathmatch tutti-contro-tutti: 100 HP, headshot moltiplicati, respawn in 3 s, vince chi arriva a 20 kill (poi il match riparte). Al menu scegli mappa e arma primaria; la pistola FEDELE è sempre in fondina.
Architettura (in 10 righe)
/shared— protocollo, costanti, mappa e simulazione del movimento: lo stesso codice gira su client e server (prediction esatta)./server— autoritativo: i client inviano solo input (mai posizioni); tick a 30 Hz, snapshot a 30 Hz (29/s misurati), hitscan server-side con lag compensation (rewind delle posizioni al tempo di mira del client)./client— Three.js: client-side prediction + riconciliazione per sé (rebase sullo stato del server + replay degli input pendenti), entity interpolation a ~100 ms per gli altri, con stato animazione cablato alla rete (corsa/sprint/salto/morte).- Tab in background: il tick di simulazione viaggia sugli eventi WebSocket (i timer sono strozzati dal browser, la rete no).
- Le mappe sono cotte da OpenStreetMap in
shared/src/maps/*.jsondascripts/bake-map.mjs: rotazione sull'angolo della griglia urbana → collisioni AABB fedeli; generatore di interni (muri con aperture per colonna, scale con imbocco, solai bucati, terrazze), cunicoli scavati, auto e clutter posizionati con verifica di spazio; spawn calcolati e percorribilità verificata via BFS. Il server apre una stanza per mappa.
Asset
Tutti CC0, vendorizzati in client/public/assets (nessun download a
runtime), con fallback procedurale per ogni voce (un file mancante degrada
la resa, non blocca il gioco). Crediti completi in
CREDITS.md: Quaternius (soldato animato),
Kenney (armi e audio), Poly Haven (HDRI), ambientCG (texture PBR).
Dati mappa © OpenStreetMap contributors (ODbL).
Rigenerare la mappa
node scripts/bake-map.mjs bari --ascii # anteprima ASCII della pianta
node scripts/bake-map.mjs bari # riscrive shared/src/maps/bari.json
node scripts/bake-map.mjs napoli
Per aggiungere una città basta una nuova voce in CITIES in testa a
scripts/bake-map.mjs (bbox geografico + parametri interni/cunicoli/auto) e
una card nel menu (client/index.html). I props manuali di Bari
(impalcature di cantiere) sono in scripts/props.json.