No description
  • TypeScript 70.7%
  • JavaScript 23.7%
  • HTML 3.6%
  • CSS 1.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-14 08:55:24 +02:00
.agents docs: le istruzioni di progetto e le skill di lavoro 2026-08-11 11:31:41 +02:00
.claude docs: le istruzioni di progetto e le skill di lavoro 2026-08-11 11:31:41 +02:00
.serena chore: ignora graphify-out, .omp e sessioni rr; traccia .serena 2026-08-28 18:58:13 +02:00
.superpowers/sdd docs: le istruzioni di progetto e le skill di lavoro 2026-08-11 11:31:41 +02:00
client feat(gameplay): coltello (V), barre vita, danno direzionale, verticalità crash-cod 2026-09-13 20:10:01 +02:00
docs feat(anteprima): Bari Vecchia intera in 3D vera - edifici vuoti ed esplorabili 2026-08-29 15:55:44 +02:00
piano docs(piano): spec mappe evolute - il censimento misurato e i lavori per mappa 2026-08-28 23:54:26 +02:00
preview feat(maps): aggiunge mappe originali RIG-7 e CORSAIR e rifinitura renderer 2026-08-29 20:39:32 +02:00
scripts feat(gameplay): coltello (V), barre vita, danno direzionale, verticalità crash-cod 2026-09-13 20:10:01 +02:00
server/src feat(gameplay): coltello (V), barre vita, danno direzionale, verticalità crash-cod 2026-09-13 20:10:01 +02:00
shared/src feat(gameplay): coltello (V), barre vita, danno direzionale, verticalità crash-cod 2026-09-13 20:10:01 +02:00
skill-observations feat(anteprima): Bari Vecchia intera in 3D vera - edifici vuoti ed esplorabili 2026-08-29 15:55:44 +02:00
.dockerignore build(deploy): Dockerfile + Caddy per Coolify 2026-08-29 21:08:32 +02:00
.gitignore chore: ignora graphify-out, .omp e sessioni rr; traccia .serena 2026-08-28 18:58:13 +02:00
AGENTS.md chore(gitnexus): contatori indice aggiornati (10059 simboli) 2026-09-14 08:55:24 +02:00
Caddyfile fix(deploy): /ws non arrivava mai al server di gioco 2026-08-29 22:27:40 +02:00
CLAUDE.md chore(gitnexus): contatori indice aggiornati (10059 simboli) 2026-09-14 08:55:24 +02:00
Dockerfile build(deploy): Dockerfile + Caddy per Coolify 2026-08-29 21:08:32 +02:00
new_plan.md docs: update agent rules and add map editor v1 implementation plan 2026-08-30 15:05:21 +02:00
package-lock.json feat: SPARA — FPS multiplayer nel browser, ambientato a Bari e Napoli 2026-08-01 09:42:43 +02:00
package.json chore: complete global non-regression test suite for all 19 engine subsystems 2026-08-30 14:44:02 +02:00
README.md perf: snapshot a 30 Hz, e il test che si e' rotto non era il gioco 2026-08-01 16:03:21 +02:00
tsconfig.json feat: il sentore — si sa che c'e' un dono, non quale 2026-08-02 13:09:16 +02:00

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

  1. /shared — protocollo, costanti, mappa e simulazione del movimento: lo stesso codice gira su client e server (prediction esatta).
  2. /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).
  3. /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).
  4. Tab in background: il tick di simulazione viaggia sugli eventi WebSocket (i timer sono strozzati dal browser, la rete no).
  5. Le mappe sono cotte da OpenStreetMap in shared/src/maps/*.json da scripts/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.