Compare commits

..
20 Commits
Author SHA1 Message Date
manzolo dea124933e Versione 0.1.22 2026-10-28 00:31:08 +01:00
manzoloandClaude Fable 5.1 51a46fe5a0 Effetti ambientali nelle dieci scene del 27 ottobre (Andrea: «applicale tutte»)
Nuvole, rondini e ombre di foglie dove ci sono alberi e cielo; ombra di nuvola sui campi di Gabbiano, Coldaia, le
Mozzète e la Provinciale; alberi al vento a Gabbiano e Via Massorondinaio; farfalle alla Piazzetta, a Gabbiano e
Coldaia, la libellula al ponticino delle Mozzète; piccioni e merli sulla Provinciale e alla falegnameria, col pulviscolo
nel fienile; di sera la lanterna della Piazzetta, il lampione di Via del Pignone, le lampade del Cinema Taiuti e il
cartellone del Rattattù illuminato.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-28 00:25:01 +01:00
manzolo f6fb8922af Versione 0.1.21 2026-10-27 23:56:31 +01:00
manzoloandClaude Fable 5.1 bd7fbc3003 Basket: alla gloria Andrea sfida Jordan, non Cistola (Andrea)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-27 23:50:10 +01:00
manzoloandClaude Fable 5.1 8b957c7cf7 Basket: la revisione di Codex, i punti gravi
Dalla mira iniziale (52°) da sotto non entrava con nessuna forza: ora 64°, forza minima più bassa e prima postazione
un passo indietro, così da ogni postazione entra in una fascia di forza contigua di almeno l'8% (test). Il palo
fermava anche sopra la sua cima e contava come tiro pulito; il tabellone lasciava passare da dietro. Il canestro
conta appena entra (anche il risultato all'ultimo pallone), la pausa fra un pallone e l'altro è più corta, la palla
entrata non rotola fino in fondo. Andrea resta a braccia alzate finché la palla vola (niente doppio pallone), il
rilascio parte sopra la sua testa. La guida si ferma al primo urto e ha un bordo scuro con la punta; il lampo del
canestro si spegne; l'obiettivo del livello è sempre in vista; i microurti sul ferro non tintinnano; dietro Andrea
non si carica; l'aiuto spiega il clic che fissa la mira e le soglie.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-27 23:39:30 +01:00
manzoloandClaude Fable 5.1 f1de62f01e «Fai due tiri» al campo da basket del Circolo 4 Maggio
Minigioco nuovo (idea di Andrea, 27 ottobre): vista di lato, Andrea da solo con dieci palloni; il mouse o il dito
danno l'angolo, tenendo premuto sale la forza, lasciando la palla vola in parabola, rimbalza sul ferro (davanti e
dietro dell'anello), sul tabellone e per terra, entra anche sporca; da sotto non vale. A ogni canestro si arretra
di una postazione fino a dietro la linea da tre (tre punti). Tre livelli: quanti canestri su dieci per la gloria
(3, 5, 7) e quanta parabola mostra la guida (quasi tutta, l'inizio, niente). Sfondo del campo e foglio di Andrea col
pallone di Codex; suoni di palla, ferro, tabellone e retina; stessi comandi delle bocce (pannello, tastiera, pressione);
dialogo di scelta dal campo di Via Cipriani, battute provvisorie e voci; test del motore (da ogni postazione esiste
un tiro che entra) e prova nel browser.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-27 23:16:35 +01:00
manzolo 64dc8e18e7 Versione 0.1.20 2026-10-27 23:02:27 +01:00
manzoloandClaude Fable 5.1 b262977117 Il carosello dei Ringraziamenti si ferma, si sfoglia e ingrandisce le foto
Tutti gli sfondi del paese (game.reelPictures, uno per scena, col nome del posto) e ogni ritratto una volta sola
(prima tre si ripetevano). Scorre da solo ma si ferma sotto il mouse, si trascina col mouse o col dito, si spinge
con la rotella, con le frecce ai lati e con quelle della tastiera; un tocco su una foto la mostra grande col nome
(il tocco si legge al rilascio: con la cattura del puntatore il clic non arriva all'immagine). Test della fila e
prova nel browser.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-27 22:57:01 +01:00
manzolo 3762f2b465 Versione 0.1.19 2026-10-27 22:40:04 +01:00
manzoloandClaude Fable 5.1 9f7a9812ed Via Cipriani col Cinema Taiuti e il Rattattù, Via Massorondinaio, i ragazzi delle canne in Fortezza, le Mozzète fino a Gabbiano
- Via Pietro Cipriani fra la Piazza e Via Massorondinaio (poi il campeggio): il Cinema Taiuti aperto come nel 2007
  con la locandina del Rattattù (stile della locandina vera di Andrea, scaletta illeggibile), il campo da basket
  del Circolo 4 Maggio (gioco da fare).
- In Fortezza quattro ragazzi inventati (sprite di Codex) si fanno le canne da pesca col bambù per il lago
  dell'Anatre dietro Cardetole; battuta di Andrea sulla mamma che aveva ragione.
- Piazzetta Cambray Digny: a sinistra è il muro di Villa Schifanoia, non la Fortezza (Andrea, con Wikipedia).
- Dal campo secondario, a destra: le Mozzète, il ponticino, Coldaia, la falegnameria, Gabbiano.
Sfondi di Codex da Street View (in IMMAGINI.md: da rifare con foto di Andrea prima di Pages); battute provvisorie e voci.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-27 22:34:41 +01:00
manzolo 151017d2e2 Versione 0.1.18 2026-10-27 21:25:07 +01:00
manzoloandClaude Opus 5.5 f193c6020f Via del Pignone col murale di Cistola, Via Provinciale verso la Pieve e Piazzetta Cambray Digny
Sfondi di Codex: Via del Pignone dalla foto di Andrea, con sul muro a destra il murale di Cistola (Davide Poggi,
«il talento di via del Pignone», consenso confermato da Andrea) alla maniera di quelli di Maradona, maglia a strisce
bianche e rosse; Via Provinciale e Piazzetta Cambray Digny da Street View (in IMMAGINI.md: da rifare prima di
pubblicarle su Pages). Il giro come il paese vero: rotonda della farmacia → Via Provinciale → Pieve, e a destra
Via del Pignone → Piazzetta → a sinistra Via de' Medici, a diritto la Piazza. Dalla Piazza, a sinistra, ora si va
in Piazzetta (non più al Comune né direttamente in Via de' Medici). Battute provvisorie e voci.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-27 21:19:45 +01:00
manzolo 8a2e49036d Versione 0.1.17 2026-10-26 23:44:06 +01:00
manzoloandClaude Opus 5.5 1e5f66c2b6 Cartello: finché il browser aspetta il tocco per il suono, le scritte restano ferme e si vede solo «Tocca per cominciare» (prima era invisibile); al tocco partono insieme alla musichetta, e «(Poca)» cade sul suo rumorino (Andrea: «era carino che il suono comparisse appena si vedeva»)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
2026-10-26 23:38:57 +01:00
manzolo b1640f1f12 Versione 0.1.16 2026-10-26 23:35:55 +01:00
manzoloandClaude Opus 5.5 0e7a50493c Il cartello d'apertura: online, su un sito mai visitato, il browser bloccava la musichetta senza un gesto (in locale no, ci si gioca da giorni); ora in quel caso il cartello aspetta un tocco («Tocca per cominciare») e poi parte coi suoi tempi. La Ghiacciomobile non ripete più le battute due volte (la scelta la dice già Andrea). Licenza (codice MIT, storia e grafica CC BY-NC-SA, eccezioni per persone vere, palio, Suno, voci XTTS con la CPML, font OFL, Wikipedia), README riscritto, scripts/check-public.sh e make check-public dentro make verify. Prove nel browser con l'autoplay permesso, più una per il caso bloccato
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
2026-10-26 23:30:48 +01:00
manzoloandClaude Fable 5.1 c2cb9b0194 A Campomigliaio, accanto all'Arcu, il Betino (motorino verde e crema con la scritta sul serbatoio) e la Ghiacciomobile (la Corsa nera): sprite di Codex dalle foto di riferimento di Andrea, battute di Andrea (guardandoli, e parlando alla macchina che non risponde), voci; «Ghiacciomòbile» nella pronuncia
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
2026-10-26 23:06:19 +01:00
manzoloandClaude Fable 5.1 5392f042b3 nginx: tipo MIME per jpg e mp4 (la foto del palio e i video arrivavano come application/octet-stream), cache lunga anche per loro e per il favicon; test che ogni estensione servita abbia il suo tipo
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
2026-10-26 22:47:32 +01:00
manzolo ca3035e33f Versione 0.1.15 2026-10-26 22:42:15 +01:00
manzoloandClaude Fable 5.1 22be1c306e I cinque video «Come si gioca» dentro il gioco: pulsante «Guarda il video» nell'aiuto di briscola, tressette, scopa, bocce e biliardo, player a tutto schermo con l'audio (il gioco e la musica si fermano, Esc torna al tavolo). video/pubblica.py li taglia dall'arrivo nel posto, li comprime per il web (~4,5 MB l'uno) e fa la copertina. Il favicon e i video copiati anche nell'immagine Docker (il Regolino online dava 404); test che ogni cartella di assets usata dal gioco sia nel Dockerfile
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
2026-10-26 22:37:19 +01:00
122 changed files with 1905 additions and 139 deletions
+2
View File
@@ -13,6 +13,8 @@ COPY assets/voices /usr/share/nginx/html/assets/voices
COPY assets/credits /usr/share/nginx/html/assets/credits COPY assets/credits /usr/share/nginx/html/assets/credits
COPY assets/effects /usr/share/nginx/html/assets/effects COPY assets/effects /usr/share/nginx/html/assets/effects
COPY assets/closeups /usr/share/nginx/html/assets/closeups COPY assets/closeups /usr/share/nginx/html/assets/closeups
COPY assets/favicon /usr/share/nginx/html/assets/favicon
COPY assets/video /usr/share/nginx/html/assets/video
# Le immagini e i font citati da index.html e style.css prendono ?v=<versione>, come quelli caricati dal codice. # Le immagini e i font citati da index.html e style.css prendono ?v=<versione>, come quelli caricati dal codice.
RUN cd /usr/share/nginx/html \ RUN cd /usr/share/nginx/html \
+71
View File
@@ -0,0 +1,71 @@
Licenze de «La Leggenda del Regolo»
Il progetto è un gioco gratuito e non commerciale. Ha due parti con licenze diverse: il codice, e tutto il resto.
================================================================================
1. IL CODICE: licenza MIT
================================================================================
Vale per tutto ciò che sta fuori dalle cartelle assets/, docs/ e video/: i sorgenti in src/, i test, gli strumenti
in tools/, gli script, index.html, style.css, i file di configurazione.
MIT License
Copyright (c) 2026 Andrea Manzi
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of
the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================================================
2. LA STORIA, LA GRAFICA, LA MUSICA: Creative Commons BY-NC-SA 4.0
================================================================================
Vale per la storia e i dialoghi (src/story/), i documenti in docs/, gli sfondi, i primi piani, le icone e gli sprite
dei personaggi inventati (assets/scenes, assets/closeups, assets/icons, assets/effects, assets/favicon, e in
assets/characters quelli che non ritraggono persone vere), le musiche composte e sintetizzate dagli script in tools/
(assets/audio/biliardo.mp3, carte.mp3, bocce.mp3, pieve.mp3, cartello.mp3) e i video «Come si gioca» (assets/video).
© 2026 Andrea Manzi. Si possono copiare, ridistribuire e rielaborare citando l'autore, per scopi non commerciali,
e distribuendo le opere derivate con la stessa licenza. Testo completo: https://creativecommons.org/licenses/by-nc-sa/4.0/
Gli sfondi sono stati disegnati con strumenti di generazione di immagini (OpenAI) a partire da fotografie di San
Piero a Sieve, quasi tutte di Andrea Manzi; la provenienza di ognuno è registrata in docs/IMMAGINI.md (la foto
dell'interno della Pieve è di Mariano Mirannalti, citato nei Ringraziamenti).
================================================================================
3. ECCEZIONI: cose che NON si possono riusare, o che hanno una licenza propria
================================================================================
a) Le persone vere. I ritratti in pixel nei Ringraziamenti (assets/credits/) e gli sprite dei personaggi ispirati a
persone vere (vecchietti, Mariano, l'Ing. Tozzi, il Tozzino, il BaronDeDuke, l'Arcu, Fra Peppino e gli altri
indicati nei commenti di src/story/story.js) sono pubblicati con il consenso delle persone ritratte, SOLO per
questo gioco: tutti i diritti riservati, non si possono riusare altrove. Lo stesso vale per i nomi e i ricordi
delle persone del paese che compaiono nella storia.
b) La fotografia del palio del 2007 (assets/closeups/palio-2007.jpg) ritrae il quadro dipinto dai ragazzi delle
scuole medie di San Piero a Sieve: tutti i diritti riservati agli autori, pubblicata qui per il gioco.
c) Le musiche di Andrea Manzi generate con Suno (assets/audio/titolo.mp3, paese.mp3, sera.mp3, fortezza.mp3,
ricordi.mp3) si usano alle condizioni d'uso di Suno e non sono coperte dalla licenza Creative Commons.
d) Le voci dei personaggi (assets/voices/) e la voce narrante dei video (assets/video/) sono sintetizzate con
XTTS-v2 di Coqui, distribuito con la Coqui Public Model License (https://coqui.ai/cpml, testo in
docs/LICENZA-CPML.txt): i file audio che ne derivano si possono usare SOLO per scopi non commerciali.
e) Il carattere Press Start 2P di CodeMan38 (assets/fonts/) è distribuito con la SIL Open Font License 1.1: il testo
della licenza è in assets/fonts/OFL-press-start-2p.txt.
f) L'aneddoto della Torre dello Scoronconcolo riprende, riformulati, i fatti della voce «Michele del Tavolaccino»
di Wikipedia in italiano (CC BY-SA 4.0): https://it.wikipedia.org/wiki/Michele_del_Tavolaccino
Monkey Island e La storia infinita sono citati come ricordi: i marchi e le opere restano dei rispettivi titolari.
+5 -2
View File
@@ -23,7 +23,7 @@ DST ?= assets/scenes/$(notdir $(SRC))
CROP ?= CROP ?=
COLORS ?= 64 COLORS ?= 64
.PHONY: help serve open test browser verify bump voci voci-elenco sfondi sfondi-genera sfondi-confronto background character \ .PHONY: help serve open test browser verify check-public bump voci voci-elenco sfondi sfondi-genera sfondi-confronto background character \
build up down restart logs ps shell image-size deploy deploy-logs clean distclean build up down restart logs ps shell image-size deploy deploy-logs clean distclean
## ------------------------------------------------------------ sviluppo ## ------------------------------------------------------------ sviluppo
@@ -50,7 +50,10 @@ test: ## Test della logica, della storia, della soluzione completa e del generat
browser: ## Partita completa nel browser con Playwright browser: ## Partita completa nel browser con Playwright
$(NODE) tests/browser.mjs $(NODE) tests/browser.mjs
verify: test browser ## Tutti i controlli verify: test browser check-public ## Tutti i controlli
check-public: ## Prima di pubblicare: niente prompt, fogli grezzi, segreti o nomi della rete di casa (tutta la storia)
bash scripts/check-public.sh
bump: ## Nuova versione (PARTE=patch|minor|major): verifica, commit e tag v<versione>; poi make deploy bump: ## Nuova versione (PARTE=patch|minor|major): verifica, commit e tag v<versione>; poi make deploy
scripts/bump.sh $(or $(PARTE),patch) scripts/bump.sh $(or $(PARTE),patch)
+46 -61
View File
@@ -1,81 +1,66 @@
# La Leggenda del Regolo # La Leggenda del Regolo
Avventura grafica punta-e-clicca ambientata a San Piero a Sieve, nella settimana del Palio della Fortezza del 2007. Un'avventura grafica punta-e-clicca, nello spirito delle avventure anni '90, ambientata a **San Piero a Sieve**
Il palio, il quadro dipinto dai ragazzi delle medie, è sparito: un ragazzo del paese deve ritrovarlo prima della festa. (Mugello) nella settimana del **Palio della Fortezza del 2007**. Il palio, il quadro dipinto dai ragazzi delle
medie, è sparito: Andrea, un ragazzo del paese, deve ritrovarlo prima della festa, fra vecchietti che litigano
sulle due leggende del Regolo, il serpente della Fortezza, e un paese disegnato dalle fotografie vere.
*Al Lele, che mi ha fatto conoscere Monkey Island.* *Al Lele, che mi ha fatto conoscere Monkey Island.*
Progetto in fase di ideazione: il documento di progetto è in [`docs/PROGETTO.md`](docs/PROGETTO.md). Si gioca online su **https://50037.manzolo.it** (50037 è il CAP di San Piero), nel browser, anche dal telefono.
Per ora c'è un prototipo del motore con due scene di prova e grafica provvisoria. È gratuito e non commerciale.
## Generare sfondi dalle fotografie ## Cosa c'è
Il generatore Python è `tools/genera.py`; istruzioni e prompt delle due nuove vedute - **Il primo capitolo**, dal pomeriggio alla sera: 38 scene, i dialoghi a scelte, l'inventario, la mappa del
sono descritti in [`docs/GENERAZIONE_SFONDI.md`](docs/GENERAZIONE_SFONDI.md). paese, il taccuino, il salvataggio automatico (e su file, per portarlo su un altro dispositivo).
Lavora sulle foto di **San Piero a Sieve**, conserva i master a 1536×864 e permette - **Le voci** di tutti i personaggi, in toscano, e le **musiche**.
di prepararne copie a 640×360 per il gioco. - **I giochi del paese**: la briscola, il tressette ai 21 e la scopa all'oratorio (con le carte toscane o le
napoletane), le bocce al pallaio e il biliardo a cinque birilli al circolo, ognuno con tre livelli e un video
```zsh «Come si gioca».
python3 tools/genera.py --dry-run # elenco delle foto, nessuna API e nessun file scritto - **Il paese vero**: gli sfondi sono disegnati dalle fotografie di San Piero; i personaggi, con il loro
``` consenso, da persone vere.
## Voci dei personaggi
Ogni battuta ha la sua voce, generata con XTTS-v2 sulla GPU (licenza solo non commerciale): un mp3 per battuta in
`assets/voices/`, la voce di ogni personaggio in `tools/voci.json` (castello e leone con altezza ed eco proprie).
```zsh
make voci # solo le battute nuove o cambiate; i file che non servono più si tolgono
make voci RIFAI='Ci sei?' # ritenta una battuta storpiata (XTTS è casuale: di solito il secondo tentativo va)
make voci-elenco # cosa manca, senza generare
```
Il test `tests/voices.test.mjs` fallisce se una battuta non ha la sua voce: dopo aver cambiato un testo, `make voci`.
## Giocare in locale ## Giocare in locale
```zsh Niente da installare e niente da compilare: HTML, CSS e moduli JavaScript ES.
python3 -m http.server 8000 # poi http://localhost:8000
```
Clic = azione più sensata (parla, guarda, vai); clic destro o pressione lunga = guarda; un verbo scelto vale per il
clic successivo. Spazio salta la battuta, i numeri scelgono nel dialogo, Esc annulla il verbo.
Il libro dell'oratorio si apre e si sfoglia con clic o tocco; Esc lo chiude. Nella fessura della Fortezza,
la torcia segue mouse o dito (anche trascinando): si cercano i dettagli tenendo brevemente la luce su di essi.
Si possono usare anche le frecce, oppure Invio per cercare il prossimo dettaglio. «Torna fuori» permette di
riprovare; dopo il ritrovamento, «Continua» conclude il capitolo.
Il volume dei suoni ambientali e del fruscio delle pagine si regola in **Pausa → Ambiente ed effetti**.
Al biliardo del Circolo 4 Maggio, clic o tocco fissa la mira; regola la forza e premi **Tira**.
Le frecce ← → affinano la direzione, con Maiusc per passi ancora più piccoli. La guida usa la stessa fisica
del tiro: mostra sponde, primo contatto e direzioni iniziali dopo l'urto (azzurro per l'avversaria, giallo
per la propria biglia). Tenendo premuto sul tavolo o Spazio si carica il colpo, con la mira bloccata.
## In Docker
```zsh ```zsh
make up # http://localhost:8080 (DOCKER_PORT=… per cambiarla) python3 -m http.server 8000 # poi http://localhost:8000
make down
``` ```
## Struttura Oppure `make up` (Docker, su http://localhost:8080). Non aprire `index.html` con `file://`: i moduli vogliono HTTP.
- `src/engine/`: motore senza storia (`game.js` logica e coda delle azioni, `geometry.js` area calpestabile e ## Come è fatto
percorsi, `command.js` verbi e clic, `render.js` disegno, `storage.js` salvataggio);
- `src/story/`: la storia come dati (`story.js`) e la sua soluzione completa (`solution.js`);
- `src/main.js`: collega tutto alla pagina;
- `assets/scenes/`: sfondi a 640×360, preparati con `tools/prepare_background.py` dagli originali in `assets/raw/`.
- `assets/characters/`: fogli dei personaggi, tagliati con `tools/slice_sprites.py` e allineati con
`tools/prepare_character.py` (istruzioni in testa al file).
## Test - `src/engine/`: il motore (camminata con i percorsi, verbi e clic intelligente, dialoghi, inventario,
salvataggi, musica, voci, suoni d'ambiente sintetizzati, effetti animati) e i minigiochi
(`bocce.js`, `biliardo.js`, `briscola.js`, `tressette.js`, `scopa.js`, ognuno col suo `*-render.js`).
- `src/story/`: la storia è dati, non codice: scene, oggetti, personaggi, dialoghi, soluzione (`solution.js`).
- `assets/`: sfondi, sprite, musiche, voci, carattere, video.
- `tools/`: gli strumenti (sprite dai fogli, voci con XTTS, musiche sintetizzate, favicon, taratura dei minigiochi).
- `tests/`: `npm test` (unità: regole, storia coerente, voci, salvataggi) e `npm run browser` (Playwright: percorre
tutta la soluzione dell'avventura, su desktop e telefono); `npm run browser:briscola`, `:tressette`, `:scopa`,
`:biliardo`, `:effects` per le parti singole.
- `video/`: i copioni dei video «Come si gioca» e della demo, girati in una macchina virtuale.
- `docs/`: `PROGETTO.md` (l'idea, i personaggi, la mappa) e `STATO.md` (dove siamo, cosa manca).
```zsh ```zsh
npm test # logica, storia coerente, soluzione percorsa dall'inizio alla fine make verify # sintassi, test, prova nel browser: quello che gira prima di ogni versione
npm install && npx playwright install chromium-headless-shell make voci # le voci delle battute nuove (XTTS-v2 su GPU)
npm run browser # partita completa con clic veri in Chromium, salvataggio, telefono make bump # nuova versione: verifica, commit e tag
npm run browser -- --media-only # libro, torcia e audio: controllo mirato desktop/telefono make deploy # pubblica l'ultimo commit (immagine Docker spedita al server)
npm run browser:biliardo # mira e tiro con mouse/touch, desktop e telefono; schermate in /tmp/regolo-biliardo
``` ```
## Chi ha fatto cosa
Un gioco di Andrea Manzi (Manzolo). Idee, ricordi, foto e volti: gli amici di San Piero, nei Ringraziamenti del
gioco. Programmazione con Claude e Codex; sfondi dalle fotografie di Andrea con gli strumenti di generazione di
immagini; musiche di Andrea con Suno e, per i giochi, sintetizzate da script; voci con XTTS-v2; carattere Press
Start 2P.
## Licenze
Il codice è MIT; storia, grafica e musiche sintetizzate sono Creative Commons BY-NC-SA 4.0; le persone vere
ritratte, la foto del palio, le musiche di Suno e le voci XTTS (solo uso non commerciale) hanno le loro
condizioni. Tutto in [`LICENSE`](LICENSE).
+38
View File
@@ -0,0 +1,38 @@
{
"image": "andrea-basket.png",
"height": 120,
"frames": {
"hold": {
"x": 0,
"y": 0,
"w": 51,
"h": 120,
"ax": 19.1,
"ay": 120
},
"load": {
"x": 53,
"y": 0,
"w": 62,
"h": 106,
"ax": 32.9,
"ay": 106
},
"shoot": {
"x": 117,
"y": 0,
"w": 69,
"h": 124,
"ax": 30.0,
"ay": 124
},
"cheer": {
"x": 188,
"y": 0,
"w": 59,
"h": 118,
"ax": 26.5,
"ay": 118
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

+14
View File
@@ -0,0 +1,14 @@
{
"image": "betino.png",
"height": 104,
"frames": {
"idle": {
"x": 0,
"y": 0,
"w": 161,
"h": 104,
"ax": 80.5,
"ay": 104
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+14
View File
@@ -0,0 +1,14 @@
{
"image": "ghiacciomobile.png",
"height": 144,
"frames": {
"idle": {
"x": 0,
"y": 0,
"w": 324,
"h": 144,
"ax": 162,
"ay": 144
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

+38
View File
@@ -0,0 +1,38 @@
{
"image": "pescatori.png",
"height": 110,
"frames": {
"work": {
"x": 0,
"y": 0,
"w": 170,
"h": 110,
"ax": 84.0,
"ay": 110
},
"talk": {
"x": 172,
"y": 0,
"w": 166,
"h": 112,
"ax": 79.2,
"ay": 112
},
"laugh": {
"x": 340,
"y": 0,
"w": 164,
"h": 113,
"ax": 79.7,
"ay": 113
},
"proud": {
"x": 506,
"y": 0,
"w": 166,
"h": 121,
"ax": 80.7,
"ay": 121
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+176
View File
@@ -203,6 +203,26 @@
"file": "andrea-69337973d0.mp3", "file": "andrea-69337973d0.mp3",
"sec": 5.94 "sec": 5.94
}, },
"andrea|Quattro ragazzi che affilano canne di bambù col coltellino e ci legano le lenze.": {
"file": "andrea-98ff3bf0a3.mp3",
"sec": 6.41
},
"andrea|Oh, che fate?": {
"file": "andrea-22b8efe94b.mp3",
"sec": 1.75
},
"pescatori|Si preparano le canne: domattina si va a pescare al lago dell'Anatre, dietro Cardetole!": {
"file": "pescatori-486103d189.mp3",
"sec": 3.74
},
"pescatori|Guarda questa: la lenza la regge anche una carpa di dieci chili.": {
"file": "pescatori-afda722e0c.mp3",
"sec": 3.2
},
"andrea|(Vedi, mia mamma aveva ragione: in Fortezza si viene a farsi le canne.)": {
"file": "andrea-e0c6360b96.mp3",
"sec": 4.35
},
"andrea|Nove bastioni, li volle Cosimo I. Per tenere fuori i nemici, dicono. Secondo me per tenere dentro il Regolo.": { "andrea|Nove bastioni, li volle Cosimo I. Per tenere fuori i nemici, dicono. Secondo me per tenere dentro il Regolo.": {
"file": "andrea-d31f6e291b.mp3", "file": "andrea-d31f6e291b.mp3",
"sec": 8.71 "sec": 8.71
@@ -507,6 +527,62 @@
"file": "andrea-4d6871c326.mp3", "file": "andrea-4d6871c326.mp3",
"sec": 4.11 "sec": 4.11
}, },
"andrea|Il Rattattù: la gara di canto del paese. Si sale sul palco e ci si sfida tutti, chi stona e chi no.": {
"file": "andrea-a9903d3ae0.mp3",
"sec": 8.33
},
"andrea|Quest'anno mi ci iscrivo. Forse.": {
"file": "andrea-146000c293.mp3",
"sec": 2.98
},
"andrea|Il Cinema Taiuti. Una sala, un proiettore e le poltroncine che cigolano a tempo.": {
"file": "andrea-3b310a5164.mp3",
"sec": 6.39
},
"andrea|A quest'ora è chiuso. Apre la sera, per il film o per il Rattattù.": {
"file": "andrea-5e37f70197.mp3",
"sec": 5.35
},
"andrea|Il campo da basket del Circolo, lassù oltre la rete. Due tiri ce li faccio sempre volentieri.": {
"file": "andrea-3c4ee692ad.mp3",
"sec": 7.55
},
"andrea|La fermata della corriera. Da qui si va dappertutto. Se passa.": {
"file": "andrea-ccd735c926.mp3",
"sec": 5.72
},
"andrea|Il ponticino delle Mozzète. Sotto ci passa un fossino che d'estate fa più zanzare che acqua.": {
"file": "andrea-98dc291bd3.mp3",
"sec": 7.21
},
"andrea|Terra appena arata. Profuma di campagna e di trattore.": {
"file": "andrea-a07bf26ea7.mp3",
"sec": 4.41
},
"andrea|La falegnameria. L'odore di segatura arriva fin qui.": {
"file": "andrea-1ccd8e2127.mp3",
"sec": 3.84
},
"andrea|Bussare a una falegnameria? Mi danno una scopa e mi fanno spazzare.": {
"file": "andrea-09ec24ef60.mp3",
"sec": 4.63
},
"andrea|Un fienile pieno di rotoballe. Fossi una mucca, sarebbe il paradiso.": {
"file": "andrea-5dfec13174.mp3",
"sec": 4.81
},
"andrea|La campagna verso Gabbiano. Da quassù si vede mezzo Mugello.": {
"file": "andrea-06e1b17635.mp3",
"sec": 5.31
},
"andrea|Il palio, però, no.": {
"file": "andrea-0e9c7a48de.mp3",
"sec": 1.6
},
"andrea|Un casolare vecchio, coi mattoni che spuntano dall'intonaco. Come le mie ginocchia.": {
"file": "andrea-06c5f35df7.mp3",
"sec": 5.66
},
"andrea|L'ingresso del campeggio. A giudicare dalle bandiere, ci dorme mezzo mondo.": { "andrea|L'ingresso del campeggio. A giudicare dalle bandiere, ci dorme mezzo mondo.": {
"file": "andrea-72dede01c0.mp3", "file": "andrea-72dede01c0.mp3",
"sec": 5.13 "sec": 5.13
@@ -627,6 +703,42 @@
"file": "andrea-e24cb31cbf.mp3", "file": "andrea-e24cb31cbf.mp3",
"sec": 2.62 "sec": 2.62
}, },
"andrea|Cartelli per la Pieve, per la stazione, per Firenze. Per il palio, nessuno.": {
"file": "andrea-60f39f0dd7.mp3",
"sec": 6.48
},
"andrea|Cistola. Il talento di via del Pignone.": {
"file": "andrea-2b47355c2c.mp3",
"sec": 3.81
},
"andrea|Col dieci, come i grandi. Se c'è un muro che se lo merita, è questo.": {
"file": "andrea-1bd111f29d.mp3",
"sec": 5.59
},
"andrea|Ciao Cistola! ...Almeno dipinto stai fermo ad ascoltarmi.": {
"file": "andrea-0e558e54fe.mp3",
"sec": 4.18
},
"andrea|Il muro di Villa Schifanoia, la villa dei Medici, mezzo coperto dall'edera.": {
"file": "andrea-6a873beff6.mp3",
"sec": 5.07
},
"andrea|Lassù una volta c'era una torre, per tenere d'occhio chi passava sulla strada per Bologna. Ne ha viste più di me.": {
"file": "andrea-d9ba9bd3ad.mp3",
"sec": 7.34
},
"andrea|E la piazzetta si chiama come i Cambray-Digny, gli ultimi signori della villa.": {
"file": "andrea-9d3ec66f60.mp3",
"sec": 4.64
},
"andrea|Una casetta piccina, appoggiata al muro della villa come un gatto al sole.": {
"file": "andrea-3cef067df6.mp3",
"sec": 4.1
},
"andrea|Chiusa. E poi non è casa mia.": {
"file": "andrea-7fd73fbb77.mp3",
"sec": 2.66
},
"andrea|L'area feste. D'estate ci montano la pista da ballo, le tavolate e lo stand della pasta.": { "andrea|L'area feste. D'estate ci montano la pista da ballo, le tavolate e lo stand della pasta.": {
"file": "andrea-a942b2723e.mp3", "file": "andrea-a942b2723e.mp3",
"sec": 6.77 "sec": 6.77
@@ -943,6 +1055,22 @@
"file": "andrea-00f2e70a4d.mp3", "file": "andrea-00f2e70a4d.mp3",
"sec": 2.95 "sec": 2.95
}, },
"andrea|Nessuno ha mai capito davvero il tuo immenso valore.": {
"file": "andrea-3e85200509.mp3",
"sec": 2.93
},
"andrea|O via, lascialo riposare. A pedalare oggi ci penso io.": {
"file": "andrea-34aad1ffe3.mp3",
"sec": 4.81
},
"andrea|La Ghiacciomobile nera. Hai visto cose che noi poveri umani non potremmo neppur lontanamente immaginare.": {
"file": "andrea-7be29434e0.mp3",
"sec": 7.25
},
"andrea|Con questo sole, di ghiaccio c'è rimasto giusto il nome. Vo a piedi.": {
"file": "andrea-def44809bc.mp3",
"sec": 5.08
},
"andrea|Il circolino: il Circolo SMS di Campomigliaio. Un'istituzione, qui i volontari hanno sempre fatto miracoli.": { "andrea|Il circolino: il Circolo SMS di Campomigliaio. Un'istituzione, qui i volontari hanno sempre fatto miracoli.": {
"file": "andrea-90bbad0de0.mp3", "file": "andrea-90bbad0de0.mp3",
"sec": 9.59 "sec": 9.59
@@ -1523,6 +1651,18 @@
"file": "andrea-60cdae2e35.mp3", "file": "andrea-60cdae2e35.mp3",
"sec": 5.1 "sec": 5.1
}, },
"andrea|Mi ricordi come avete fatto quella volta a tornare illesi dopo aver lasciato una fiancata a Campolungo?": {
"file": "andrea-2acf574bfa.mp3",
"sec": 5.36
},
"andrea|Riposa, Ghiacciomobile.": {
"file": "andrea-c79d2d1c81.mp3",
"sec": 2.19
},
"andrea|Ho sentito dire dal Trappola che pensavi di poter attraversare i muri. Ti stimo tanto.": {
"file": "andrea-6c6e2fd461.mp3",
"sec": 5.69
},
"andrea|Una briscola?": { "andrea|Una briscola?": {
"file": "andrea-d2f285f610.mp3", "file": "andrea-d2f285f610.mp3",
"sec": 1.04 "sec": 1.04
@@ -1895,6 +2035,30 @@
"file": "arcu-406cd02197.mp3", "file": "arcu-406cd02197.mp3",
"sec": 4.46 "sec": 4.46
}, },
"andrea|Due tiri alla bona, tanto per scaldarmi.": {
"file": "andrea-d564365fc2.mp3",
"sec": 2.53
},
"andrea|Due tiri seri.": {
"file": "andrea-ec8f0db709.mp3",
"sec": 1.32
},
"andrea|Lascia stare, ho il palio da trovare.": {
"file": "andrea-f5519518f2.mp3",
"sec": 1.99
},
"andrea|Tre su dieci e sono contento.": {
"file": "andrea-5e3315a040.mp3",
"sec": 2.33
},
"andrea|Cinque su dieci. Come ai tempi del Circolino.": {
"file": "andrea-1bc731bcdf.mp3",
"sec": 3.59
},
"andrea|Sette su dieci, senza guardare la parabola. Vediamo chi sono.": {
"file": "andrea-fa062d00bd.mp3",
"sec": 4.95
},
"andrea|Mariano! Facciamo una partita a bocce?": { "andrea|Mariano! Facciamo una partita a bocce?": {
"file": "andrea-83b1337b4d.mp3", "file": "andrea-83b1337b4d.mp3",
"sec": 3.31 "sec": 3.31
@@ -2139,6 +2303,18 @@
"file": "mariano-39e27158b7.mp3", "file": "mariano-39e27158b7.mp3",
"sec": 2.83 "sec": 2.83
}, },
"andrea|Che mano! Jordan, fatti da parte.": {
"file": "andrea-3a52789f04.mp3",
"sec": 2.98
},
"andrea|Il ferro oggi era stretto. Riprovo dopo.": {
"file": "andrea-2e9d8549c8.mp3",
"sec": 3.36
},
"andrea|Lascio il pallone lì. Tanto lo sanno tutti che è mio.": {
"file": "andrea-ee42022092.mp3",
"sec": 3.98
},
"ing_tozzi|Venti. Hai vinto te, stasera il caffè lo pago io.": { "ing_tozzi|Venti. Hai vinto te, stasera il caffè lo pago io.": {
"file": "ing_tozzi-51f855478a.mp3", "file": "ing_tozzi-51f855478a.mp3",
"sec": 4.42 "sec": 4.42
Binary file not shown.
Binary file not shown.
+2
View File
@@ -27,6 +27,8 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p
| `bosco-ai-frati.png` | Codex | `bosco-ai-frati.png` | [DA ANDREA] sembra una foto da Wikipedia/Wikimedia: verificare licenza | ? | | `bosco-ai-frati.png` | Codex | `bosco-ai-frati.png` | [DA ANDREA] sembra una foto da Wikipedia/Wikimedia: verificare licenza | ? |
| `campeggio.png` | Codex | `campeggio-ingresso.png` | [DA ANDREA] probabilmente dal sito del campeggio | ? | | `campeggio.png` | Codex | `campeggio-ingresso.png` | [DA ANDREA] probabilmente dal sito del campeggio | ? |
| `polisportiva-*.png`, `ponte-sieve.png`, `area-feste.png`, `via-provinciale.png`, `comune-asilo.png`, `giardini-poste.png`, `stazione.png` | Codex | foto omonime in `assets/raw/foto/` | Andrea (EXIF, 23 ottobre 2026) | sì | | `polisportiva-*.png`, `ponte-sieve.png`, `area-feste.png`, `via-provinciale.png`, `comune-asilo.png`, `giardini-poste.png`, `stazione.png` | Codex | foto omonime in `assets/raw/foto/` | Andrea (EXIF, 23 ottobre 2026) | sì |
| `via-del-pignone.png` | Codex | `via-del-pignone.png` (murale: `riferimenti/cistola.png`, `murale-maradona.png`) | Andrea | sì (Cistola consenziente) |
| `provinciale-pignone.png`, `piazzetta-cambray-digny.png`, `via-cipriani.png`, `via-massorondinaio.png`, `mozzete.png`, `ponticino-mozzete.png`, `coldaia.png`, `falegnameria-coldaia.png`, `gabbiano.png` | Codex | screenshot di Google Street View | Google | **no**: da rifare con foto di Andrea |
| `cardetole.png` | Codex | `cardetole.jpg` | Andrea (EXIF) | **da decidere**: è la casa vera di Andrea, pubblicarla dice dove abita | | `cardetole.png` | Codex | `cardetole.jpg` | Andrea (EXIF) | **da decidere**: è la casa vera di Andrea, pubblicarla dice dove abita |
| `pieve-interno.png` | Codex | `pieve-interno.png` | Mariano Mirannalti (mandata ad Andrea su WhatsApp) | sì, se Mariano è d'accordo; citarlo nei crediti | | `pieve-interno.png` | Codex | `pieve-interno.png` | Mariano Mirannalti (mandata ad Andrea su WhatsApp) | sì, se Mariano è d'accordo; citarlo nei crediti |
| `panorama-intro.png` | Codex | `altre/panorama-intro.png` | da internet (screenshot con l'icona di Google Lens) | **no**, finché non si sostituisce | | `panorama-intro.png` | Codex | `altre/panorama-intro.png` | da internet (screenshot con l'icona di Google Lens) | **no**, finché non si sostituisce |
+84
View File
@@ -0,0 +1,84 @@
# Coqui Public Model License 1.0.0
https://coqui.ai/cpml.txt
This license allows only non-commercial use of a machine learning model and its outputs.
## Acceptance
In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
## Licenses
The licensor grants you a copyright license to do everything you might do with the model that would otherwise infringe the licensor's copyright in it, for any non-commercial purpose. The licensor grants you a patent license that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the model in the form provided by
the licensor, for any non-commercial purpose.
## Non-commercial Purpose
Non-commercial purposes include any of the following uses of the model or its output, but only so far as you do not receive any direct or indirect payment arising from the use of the model or its output.
### Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious
observance.
### Use by commercial or for-profit entities for testing, evaluation, or non-commercial research and development. Use of the model to train other models for commercial use is not a non-commercial purpose.
### Use by any charitable organization for charitable purposes, or for testing or evaluation. Use for revenue-generating activity, including projects directly funded by government grants, is not a non-commercial purpose.
## Notices
You must ensure that anyone who gets a copy of any part of the model, or any modification of the model, or their output, from you also gets a copy of these terms or the URL for them above.
## No Other Rights
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply
any other licenses.
## Patent Defense
If you make any written claim that the model infringes or contributes to infringement of any patent, your licenses for the model granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
## Violations
The first time you are notified in writing that you have violated any of these terms, or done anything with the model or its output that is not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 30 days of receiving notice. Otherwise, all your licenses
end immediately.
## No Liability
***As far as the law allows, the model and its output come as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the model or its output, under any kind of legal claim. If this provision is not enforceable in your jurisdiction, your licenses are void.***
## Definitions
The **licensor** is the individual or entity offering these terms, and the **model** is the model the licensor makes available under these terms, including any documentation or similar information about the model.
**You** refers to the individual or entity agreeing to these terms.
**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
**Your licenses** are all the licenses granted to you under these terms.
**Use** means anything you do with the model or its output requiring one of your licenses.
+19
View File
@@ -199,6 +199,25 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso
sfondi da foto prese da internet (`docs/IMMAGINI.md`), Cardetole (dice dove abita Andrea). sfondi da foto prese da internet (`docs/IMMAGINI.md`), Cardetole (dice dove abita Andrea).
- Vecchietta e vecchietto sono in gioco sulla panchina della Piazza (`assets/characters/`, da - Vecchietta e vecchietto sono in gioco sulla panchina della Piazza (`assets/characters/`, da
`assets/raw/vecchiett?-sheet.png`): consenso delle persone ritratte confermato da Andrea il 2026-10-23; mancano i loro nomi. `assets/raw/vecchiett?-sheet.png`): consenso delle persone ritratte confermato da Andrea il 2026-10-23; mancano i loro nomi.
- Via Provinciale (dalla rotonda verso la Pieve) → a destra Via del Pignone → in cima Piazzetta Cambray Digny, che porta
in Piazza (a sinistra, dove prima c'era l'uscita per il Comune) e in Via de' Medici. Battute provvisorie sul murale.
Via del Pignone (sfondo di Codex da `assets/raw/foto/via-del-pignone.png`): sul muro di destra il
murale di Cistola (Davide Poggi), «il talento di via del Pignone», alla maniera dei murali di Maradona a Napoli
(volto da `assets/raw/riferimenti/cistola.png`). Consenso di Davide confermato da Andrea il 2026-10-27.
- Via Pietro Cipriani (fra Piazza e campeggio): Cinema Taiuti aperto con la locandina del Rattattù 2007 (stile da
`assets/raw/riferimenti/rattattu-locandina.png`, scaletta illeggibile). Al campo da basket del Circolo, «Fai due tiri»
(`src/engine/basket.js`, vista di lato, sfondo e sprite di Andrea col pallone da Codex): da solo, dieci palloni, a ogni
canestro si arretra fino alla linea da tre; tre livelli (quanti canestri per la gloria, quanta parabola mostra la
guida). Battute provvisorie. Idea di Andrea del 27 ottobre. Revisione critica di Codex (27 ottobre) applicata per
i punti gravi (angolo iniziale 64° con fascia di forza buona da ogni postazione, palo e tabellone da entrambi i lati,
canestro che conta subito, pose senza doppio pallone, guida fino al primo urto, obiettivo sempre in vista). Restano
le sue idee: «Riprova» dal risultato e record per livello; Andrea che arretra camminando; battute che dicono perché
è uscita (corto, lungo, ferro); l'angolo anche nei comandi HTML; testi del canvas piccoli sul telefono;
`basket_vinto` che lasci traccia nel paese; pannelli più compatti.
- Fuori paese, dal campo secondario (a destra): le Mozzète → il ponticino → Coldaia → la falegnameria → Gabbiano;
da Via Cipriani, indietro, Via Massorondinaio → campeggio. Sfondi da Street View, battute provvisorie.
- In Fortezza quattro ragazzi inventati si fanno le canne da pesca col bambù per il lago dell'Anatre (sprite di Codex,
`assets/raw/pescatori-sheet.png`, ritagliato 2x2 a mano: work, talk, laugh, proud); battuta finale di Andrea.
- Andrea rifatto dalla sua foto del 2007 (`assets/raw/andrea-sheet-prova-2.png`; il vecchio foglio resta in `andrea-sheet.png`). - Andrea rifatto dalla sua foto del 2007 (`assets/raw/andrea-sheet-prova-2.png`; il vecchio foglio resta in `andrea-sheet.png`).
- Strada del campeggio (poi Bosco ai Frati) e strada della scuola: per ora battute, scene da fare con le foto di Andrea. - Strada del campeggio (poi Bosco ai Frati) e strada della scuola: per ora battute, scene da fare con le foto di Andrea.
+13 -2
View File
@@ -33,6 +33,7 @@
<p class="splash-name">Manzolo</p> <p class="splash-name">Manzolo</p>
<p class="splash-art"><span>(Poca)</span> Art</p> <p class="splash-art"><span>(Poca)</span> Art</p>
<p class="splash-present">Presents</p> <p class="splash-present">Presents</p>
<p class="splash-hint">Tocca per cominciare</p>
</section> </section>
<section id="title" class="overlay" hidden> <section id="title" class="overlay" hidden>
<h1>La Leggenda<br>del <span>Regolo</span></h1> <h1>La Leggenda<br>del <span>Regolo</span></h1>
@@ -94,13 +95,13 @@
<section id="tutorial" class="overlay" hidden aria-labelledby="tutorial-title"> <section id="tutorial" class="overlay" hidden aria-labelledby="tutorial-title">
<h2 id="tutorial-title">Come si gioca</h2> <h2 id="tutorial-title">Come si gioca</h2>
<video id="tutorial-video" controls playsinline preload="metadata"></video> <video id="tutorial-video" controls playsinline preload="metadata"></video>
<button id="tutorial-close">Torna al gioco</button> <div class="buttons"><button id="tutorial-close">Torna al gioco</button></div>
</section> </section>
<section id="credits" class="overlay" hidden aria-label="Ringraziamenti"> <section id="credits" class="overlay" hidden aria-label="Ringraziamenti">
<h2>Ringraziamenti</h2> <h2>Ringraziamenti</h2>
<p class="dedication credits-dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p> <p class="dedication credits-dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p>
<dl id="credits-list"></dl> <dl id="credits-list"></dl>
<div id="credits-reel" aria-hidden="true"><div class="track"></div></div> <div id="credits-reel"><button class="arrow prev" type="button" aria-label="Foto precedente">‹</button><div class="track" aria-label="Le immagini del gioco: toccane una per vederla grande"></div><button class="arrow next" type="button" aria-label="Foto successiva">›</button></div>
<p class="legal">Monkey Island è un marchio di Lucasfilm Ltd., citato solo nella dedica: questo gioco non è affiliato né approvato da Lucasfilm.</p> <p class="legal">Monkey Island è un marchio di Lucasfilm Ltd., citato solo nella dedica: questo gioco non è affiliato né approvato da Lucasfilm.</p>
<div class="buttons"><button id="close-credits">Chiudi</button></div> <div class="buttons"><button id="close-credits">Chiudi</button></div>
<figure id="face" hidden><img alt=""><figcaption></figcaption></figure> <figure id="face" hidden><img alt=""><figcaption></figcaption></figure>
@@ -208,6 +209,16 @@
<p>Tre bocce a testa. Tira chi non ha il punto; segna ogni boccia più vicina della migliore avversaria. <p>Tre bocce a testa. Tira chi non ha il punto; segna ogni boccia più vicina della migliore avversaria.
Vince chi fa più punti nella mano.</p> Vince chi fa più punti nella mano.</p>
</details> </details>
<details id="basket-help" hidden><summary>Come si gioca</summary>
<p>Muovi il mouse sul campo per scegliere l’angolo del tiro: la freccia parte dalle mani di Andrea. Un clic (o un
tocco) fissa la mira, così puoi raggiungere i comandi senza cambiarla; un altro clic la sposta. Tieni premuto sul
campo (o Spazio) e la forza riparte da zero e sale: lascia per tirare. Oppure regola la forza e premi Tira.
Frecce ← →: angolo più teso o più alto; Maiusc + ← →: a mezzo grado; ↑ ↓: forza. Esc esce.</p>
<p>Dieci palloni. Si comincia da sotto; a ogni canestro si arretra di una postazione: tiro libero, media distanza,
da tre e da lontanissimo, dove un canestro vale tre punti invece di due. Il ferro e il tabellone rimandano la
palla: entra anche sporca, ma non da sotto. La gloria: 3 canestri alla bona, 5 seria, 7 a fassi male. Alla bona la
guida mostra la parabola fino al primo urto, Seria solo l’inizio, A fassi male niente.</p>
</details>
<details id="biliardo-help" hidden><summary>Come si gioca</summary> <details id="biliardo-help" hidden><summary>Come si gioca</summary>
<button class="tutorial-open" type="button" data-tutorial="biliardo" hidden>Guarda il video</button> <button class="tutorial-open" type="button" data-tutorial="biliardo" hidden>Guarda il video</button>
<p>Clic o tocco sul tavolo fissa la mira: puoi spostarti sui comandi senza cambiarla. <p>Clic o tocco sul tavolo fissa la mira: puoi spostarti sui comandi senza cambiarla.
+3 -1
View File
@@ -26,6 +26,8 @@ server {
audio/mpeg mp3; audio/mpeg mp3;
image/svg+xml svg; image/svg+xml svg;
image/x-icon ico; image/x-icon ico;
image/jpeg jpg jpeg;
video/mp4 mp4;
font/woff2 woff2; font/woff2 woff2;
} }
@@ -37,7 +39,7 @@ server {
location ~* \.(css|js|mjs|json)$ { location ~* \.(css|js|mjs|json)$ {
add_header Cache-Control "no-cache"; add_header Cache-Control "no-cache";
} }
location ~* \.(png|webp|mp3|woff2)$ { location ~* \.(png|webp|jpg|mp3|mp4|woff2|ico)$ {
add_header Cache-Control $media_cache; add_header Cache-Control $media_cache;
} }
location = /index.html { location = /index.html {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "leggenda-del-regolo", "name": "leggenda-del-regolo",
"version": "0.1.14", "version": "0.1.22",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+51
View File
@@ -0,0 +1,51 @@
#!/bin/bash
# Prima di pubblicare: niente prompt, fogli grezzi, log di Codex, segreti o nomi della rete di casa.
# Guarda tutti i percorsi raggiungibili da tutti i ref (la storia intera, non solo HEAD) e il testo di HEAD.
# `--self-test` costruisce un repo usa e getta, inietta un prompt e un host .lan e pretende che siano rifiutati:
# un controllo che non fallisce mai non prova niente.
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
FORBIDDEN_PATHS='(^|/)(PROMPT[A-Z_]*\.md|prompts/|assets/raw/|assets/generated/|codex[a-z.-]*\.log|deploy\.local\.mk)'
FORBIDDEN_PATHS+='|(^|/)\.env$|(^|/)id_(rsa|ed25519|ecdsa)(\.pub)?$|\.(pem|key|p12|pfx)$'
FORBIDDEN_TEXT='[a-z0-9-]+\.lan([^a-z0-9.-]|$)|192\.168\.[0-9]+\.[0-9]+|BEGIN (RSA|OPENSSH|EC|DSA) PRIVATE KEY'
scan_repo() {
local repo=$1 rc=0 hits
hits=$(git -C "$repo" rev-list --all --objects | awk 'NF>1{print $2}' | sort -u | grep -E "$FORBIDDEN_PATHS" || true)
if [[ -n $hits ]]; then
echo "FAIL: percorsi vietati nella storia (va riscritta, non basta cancellarli):"
echo "$hits" | head -40 | sed 's/^/ /'
rc=1
fi
hits=$(git -C "$repo" grep -nIE "$FORBIDDEN_TEXT" HEAD -- . ':!scripts/check-public.sh' 2>/dev/null | sed 's/^HEAD://' || true)
if [[ -n $hits ]]; then
echo "FAIL: testo vietato in HEAD:"
echo "$hits" | head -20 | sed 's/^/ /'
rc=1
fi
[[ $rc -eq 0 ]] && echo "PASS: niente prompt, fogli grezzi, segreti o host privati in $repo (tutti i ref)"
return $rc
}
self_test() {
local tmp
tmp=$(mktemp -d /tmp/check-public.XXXXXX)
trap 'rm -rf -- "$tmp"' RETURN
local -a g=(git -C "$tmp" -c user.name=check -c user.email=check@example.invalid -c commit.gpgsign=false)
git init -q "$tmp"
printf '# pulito\n' > "$tmp/README.md"
"${g[@]}" add README.md && "${g[@]}" commit -qm pulito
scan_repo "$tmp" >/dev/null || { echo 'self-test FAIL: un repo pulito deve passare'; return 1; }
mkdir -p "$tmp/prompts" && printf 'foto di famiglia\n' > "$tmp/prompts/character_x.md"
"${g[@]}" add prompts && "${g[@]}" commit -qm 'prompt iniettato'
"${g[@]}" rm -rq prompts && "${g[@]}" commit -qm 'cancellato (resta nella storia)'
if scan_repo "$tmp" >/dev/null 2>&1; then echo 'self-test FAIL: un prompt nella storia deve essere rifiutato'; return 1; fi
rm -rf "$tmp/.git" && git init -q "$tmp" && printf 'DEPLOY_HOST = root@casa.lan\n' > "$tmp/README.md"
"${g[@]}" add README.md && "${g[@]}" commit -qm 'host privato'
if scan_repo "$tmp" >/dev/null 2>&1; then echo 'self-test FAIL: un host .lan deve essere rifiutato'; return 1; fi
echo 'self-test PASS: repo pulito accettato, prompt nella storia e host .lan rifiutati'
}
[[ ${1:-} == --self-test ]] && { self_test; exit; }
scan_repo "$ROOT"
+113
View File
@@ -0,0 +1,113 @@
import { FLOOR, RIM, BALL, BALLS, SPOTS, canThrow, left, spot, releasePoint, guide as guidePoints, status } from './basket.js';
import { text, panel } from './bocce-render.js';
const ANDREA = { scale: 0.66 };
// Quale posa: carica mentre tiene premuto; finché la palla vola Andrea resta a braccia alzate (l'unica posa senza
// pallone), esulta quando entra, e a palla ferma ce l'ha di nuovo in mano.
export function andreaPose(c) {
if (c.holding) return 'load';
if (c.step === 'flight') return c.ball.scored ? 'cheer' : 'shoot';
if (c.step === 'settle' && c.ball?.scored) return 'cheer';
return 'hold';
}
function andrea(ctx, c, sheet) {
const s = spot(c), x = s.x, y = FLOOR;
ctx.fillStyle = 'rgba(8,10,9,0.5)'; ctx.beginPath(); ctx.ellipse(x + 4, y, 16, 3, 0, 0, Math.PI * 2); ctx.fill();
if (!sheet) return;
const pose = andreaPose(c), frame = sheet.atlas.frames[pose] ?? sheet.atlas.frames.hold, k = ANDREA.scale;
// Nel salto i piedi si staccano da terra per un attimo.
const lift = pose === 'shoot' ? Math.round(Math.sin(Math.min(1, c.ball.time / 0.45) * Math.PI) * 10) : 0;
ctx.drawImage(sheet.img, frame.x, frame.y, frame.w, frame.h,
Math.round(x - frame.ax * k), Math.round(y - frame.ay * k) - lift, Math.round(frame.w * k), Math.round(frame.h * k));
}
function ball(ctx, b, r = BALL) {
// L'ombra sul campo si stringe quando la palla è alta.
const h = Math.max(0, FLOOR - b.y - r), k = Math.max(0.35, 1 - h / 260);
ctx.fillStyle = `rgba(8,10,9,${0.45 * k})`; ctx.beginPath(); ctx.ellipse(b.x, FLOOR - 1, r * 1.2 * k, r * 0.4 * k, 0, 0, Math.PI * 2); ctx.fill();
const shade = ctx.createRadialGradient(b.x - r * 0.35, b.y - r * 0.35, 0, b.x, b.y, r * 1.05);
shade.addColorStop(0, '#ffb067'); shade.addColorStop(0.5, '#e8702a'); shade.addColorStop(1, '#8a3512');
ctx.fillStyle = shade; ctx.beginPath(); ctx.arc(b.x, b.y, r, 0, Math.PI * 2); ctx.fill();
// Le cuciture girano con la palla.
ctx.save(); ctx.beginPath(); ctx.arc(b.x, b.y, r - 0.3, 0, Math.PI * 2); ctx.clip();
ctx.strokeStyle = 'rgba(60,20,8,0.75)'; ctx.lineWidth = 1;
const a = b.spin ?? 0;
ctx.beginPath(); ctx.ellipse(b.x, b.y, r, Math.abs(Math.cos(a)) * r, 0, 0, Math.PI * 2); ctx.stroke();
ctx.beginPath(); ctx.ellipse(b.x, b.y, Math.abs(Math.sin(a)) * r, r, 0, 0, Math.PI * 2); ctx.stroke();
ctx.restore();
ctx.fillStyle = '#ffd9b0'; ctx.fillRect(Math.round(b.x - r * 0.4), Math.round(b.y - r * 0.45), 1, 1);
}
function guide(ctx, c) {
const r = releasePoint(c);
// La freccia della mira c'è sempre, con la punta e un bordo scuro per staccarsi da rete e alberi; la parabola per
// quanto concede il livello, fino al primo urto.
const a = c.angle * Math.PI / 180, len = 22 + c.power * 18, tip = { x: r.x + Math.cos(a) * len, y: r.y - Math.sin(a) * len };
const arrow = () => {
ctx.beginPath(); ctx.moveTo(r.x, r.y); ctx.lineTo(tip.x, tip.y);
for (const s of [-1, 1]) { ctx.moveTo(tip.x, tip.y); ctx.lineTo(tip.x - Math.cos(a - s * 0.5) * 6, tip.y + Math.sin(a - s * 0.5) * 6); }
ctx.stroke();
};
ctx.lineWidth = 3; ctx.strokeStyle = 'rgba(20,16,10,0.8)'; arrow();
ctx.lineWidth = 1; ctx.strokeStyle = '#fff6d2'; arrow();
if (c.skill.preview > 0) {
const pts = guidePoints(c, c.skill.preview);
for (const [width, color] of [[3, 'rgba(20,16,10,0.6)'], [1, 'rgba(143,199,255,0.95)']]) {
ctx.lineWidth = width; ctx.strokeStyle = color; ctx.setLineDash([2, 4]);
ctx.beginPath();
for (const [i, p] of pts.entries()) { if (i) ctx.lineTo(p.x, p.y); else ctx.moveTo(p.x, p.y); }
ctx.stroke(); ctx.setLineDash([]);
}
}
ctx.fillStyle = 'rgba(20,16,10,0.6)'; ctx.fillRect(r.x - 34, r.y - 16, 30, 11);
text(ctx, `${c.angle.toFixed(c.angle % 1 ? 1 : 0)}°`, r.x - 6, r.y - 10, 6, '#fff4c9', 'right');
}
function swish(ctx, c) {
// Un lampo di righe attorno al ferro quando la palla è appena entrata.
const b = c.ball; if (c.step !== 'flight' || !b?.scored || b.time - b.scoredAt > 0.3) return;
const cx = (RIM.x1 + RIM.x2) / 2;
ctx.strokeStyle = 'rgba(255,240,170,0.8)';
for (let i = 0; i < 6; i++) { const t = i / 6 * Math.PI * 2; ctx.beginPath(); ctx.moveTo(cx + Math.cos(t) * 20, RIM.y + Math.sin(t) * 8); ctx.lineTo(cx + Math.cos(t) * 30, RIM.y + Math.sin(t) * 13); ctx.stroke(); }
}
export function drawBasket(ctx, c, def, bg, sheet) {
ctx.save();
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
else { ctx.fillStyle = '#6d7a5a'; ctx.fillRect(0, 0, 640, 360); }
// Le postazioni sul campo: un segno chiaro dove si tira ora, uno spento dove si è già stati.
for (const [i, s] of SPOTS.entries()) {
ctx.fillStyle = i === c.spot ? 'rgba(255,244,201,0.9)' : i < c.spot ? 'rgba(255,244,201,0.35)' : 'rgba(255,244,201,0.15)';
ctx.fillRect(s.x - 1, FLOOR + 2, 3, 3);
}
andrea(ctx, c, sheet);
for (const [i, t] of c.trails.entries()) {
ctx.fillStyle = `rgba(255,190,120,${0.3 * (1 - t.age / 0.5)})`;
ctx.fillRect(Math.round(t.x) - 1, Math.round(t.y) - 1, i % 2 ? 2 : 3, i % 2 ? 2 : 3);
}
if (canThrow(c) || c.holding) guide(ctx, c);
if (c.ball && c.step === 'flight') ball(ctx, c.ball); // ferma, la palla è già tornata in mano ad Andrea
swish(ctx, c);
panel(ctx, 182, 50, 440, 24);
text(ctx, status(c, def.names), 402, 63, 6, '#fff0cb', 'center');
// I palloni rimasti e il conto.
panel(ctx, 18, 86, 196, 58);
text(ctx, 'PALLONI', 26, 98, 6, '#eed5a0');
for (let n = 0; n < BALLS; n++) {
const x = 30 + n * 14, y = 113;
if (n < left(c)) ball(ctx, { x, y, spin: 0 }, 4);
else { ctx.strokeStyle = '#766e5c'; ctx.strokeRect(x - 3, y - 3, 6, 6); }
}
text(ctx, `${c.baskets} su ${c.skill.goal} per la gloria · ${c.points} punti`, 26, 131, 6, c.baskets >= c.skill.goal ? '#b9e5b3' : '#d7ccb4');
// La forza in basso a sinistra: a destra c'è il pulsante per uscire.
if (canThrow(c) || c.holding) {
panel(ctx, 18, 306, 176, 37);
text(ctx, `FORZA ${Math.round(c.power * 100)}%`, 30, 318, 7);
ctx.fillStyle = '#3e3e36'; ctx.fillRect(30, 331, 150, 5);
ctx.fillStyle = '#b9d392'; ctx.fillRect(30, 331, Math.round(c.power * 150), 5);
}
if (c.step === 'result') {
panel(ctx, 206, 91, 397, 70);
text(ctx, c.result.winner === 'player' ? 'CHE MANO!' : 'DUE TIRI FATTI', 404, 108, 9, '#efd49e', 'center');
text(ctx, `${c.result.baskets} canestri su ${BALLS}, ${c.result.points} punti`, 404, 132, 9, c.result.winner === 'player' ? '#b9e5b3' : '#f1d9be', 'center');
text(ctx, c.result.winner === 'player' ? `Obiettivo ${c.result.goal}: fatto.` : `Per la gloria ne servivano ${c.result.goal}.`, 404, 150, 6, '#d6c7af', 'center');
}
ctx.restore();
}
+170
View File
@@ -0,0 +1,170 @@
// «Fai due tiri»: Andrea da solo al campo del Circolo 4 Maggio (idea di Andrea, 27 ottobre). Vista di lato, nelle
// coordinate dello schermo (640x360): x verso il canestro, y verso il basso. Un pixel è circa 2,3 cm: il ferro, a
// 3,05 m, sta 133 px sopra il campo. Dieci palloni; a ogni canestro si arretra di una postazione, fino a quella da
// tre. Niente avversario: il risultato è quanti canestri su dieci, e i punti (due, tre da dietro la linea).
export const FLOOR = 296, RIM = { x1: 482, x2: 518, y: 163, r: 2 }, BOARD = { x: 519, top: 118, bottom: 178, depth: 8 }, NET = 190, POLE = { x: 562, top: 112 };
export const BALL = 8, G = 560, V_MIN = 140, V_MAX = 590, SWING = 1.4, BALLS = 10;
export const FIXED_STEP = 1 / 240, REST = 0.55, FLIGHT_MAX = 7;
export const RELEASE = { dx: 18, dy: -72 }; // da dove parte la palla rispetto ai piedi di Andrea: sopra la testa, nel salto
export const ANGLE = { min: 25, max: 78 };
export const SPOTS = [
{ x: 400, name: 'da sotto', points: 2 },
{ x: 320, name: 'dal tiro libero', points: 2 },
{ x: 240, name: 'da media distanza', points: 2 },
{ x: 150, name: 'da tre', points: 3 },
{ x: 90, name: 'da lontanissimo', points: 3 },
];
// I livelli: quanti canestri su dieci valgono la gloria, e per quanto tempo di volo la guida mostra la parabola
// (alla bona fino al primo urto, che è quasi tutta).
export const LEVELS = {
bona: { goal: 3, preview: 3 },
seria: { goal: 5, preview: 0.4 },
male: { goal: 7, preview: 0 },
};
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const rad = deg => deg * Math.PI / 180;
export function newBasket(seed = Date.now() >>> 0, def = {}) {
const level = LEVELS[def.level] ? def.level : 'seria';
return { step: 'aim', t: 0, age: 0, seed, level, skill: LEVELS[level], current: 'player', spot: 0, thrown: 0, baskets: 0, points: 0,
angle: 64, power: 0.5, holding: false, holdTime: 0, accumulator: 0, ball: null, events: [], trails: [], history: [] };
}
export const canThrow = c => c.step === 'aim' && c.thrown < BALLS;
export const left = c => Math.max(0, BALLS - c.thrown);
export const spot = c => SPOTS[c.spot];
export const powerAt = time => clamp(time / SWING, 0, 1);
export const speedFor = power => V_MIN + (V_MAX - V_MIN) * clamp(power, 0, 1);
export function releasePoint(c) { const s = spot(c); return { x: s.x + RELEASE.dx, y: FLOOR + RELEASE.dy }; }
export function velocity(c) { const v = speedFor(c.power), a = rad(c.angle); return { vx: v * Math.cos(a), vy: -v * Math.sin(a) }; }
export function setPower(c, value) {
if (!canThrow(c) || c.holding || !Number.isFinite(value)) return false;
c.power = clamp(value, 0, 1); return true;
}
export function setAngle(c, deg) {
if (!canThrow(c) || c.holding || !Number.isFinite(deg)) return false;
c.angle = clamp(deg, ANGLE.min, ANGLE.max); return true;
}
export const nudge = (c, dir, fine = false) => setAngle(c, c.angle + dir * (fine ? 0.5 : 2));
// La mira segue il punto toccato: l'angolo fra la mano di Andrea e il dito. Dietro di lui non si tira.
export function aimScreen(c, [x, y]) {
if (!Number.isFinite(x) || !Number.isFinite(y)) return false;
const r = releasePoint(c);
if (x <= r.x + 4) return false;
return setAngle(c, Math.atan2(r.y - y, x - r.x) * 180 / Math.PI);
}
// La parabola libera (senza urti) per `seconds` di volo: la guida la mostra per il tempo che il livello concede.
export function arc(c, seconds, every = 0.04) {
const r = releasePoint(c), v = velocity(c), out = [];
for (let t = 0; t <= seconds; t += every) out.push({ x: r.x + v.vx * t, y: r.y + v.vy * t + 0.5 * G * t * t });
return out;
}
// La guida: la parabola libera finché non tocca qualcosa (ferro, tabellone, palo, campo), al massimo `seconds`.
export function guide(c, seconds) {
const out = [];
for (const p of arc(c, seconds)) {
const touches = p.y + BALL > FLOOR || [RIM.x1, RIM.x2].some(x => Math.hypot(p.x - x, p.y - RIM.y) < BALL + RIM.r)
|| (p.x + BALL > BOARD.x && p.y + BALL > BOARD.top && p.y - BALL < BOARD.bottom) || (p.x + BALL > POLE.x && p.y + BALL > POLE.top);
out.push(p);
if (touches) break;
}
return out;
}
export function shoot(c) {
if (!canThrow(c) || c.holding) return false;
const r = releasePoint(c), v = velocity(c);
c.ball = { x: r.x, y: r.y, vx: v.vx, vy: v.vy, time: 0, bounces: 0, rim: false, board: false, under: false, scored: false, scoredAt: null, dead: false, spin: 0 };
c.step = 'flight'; c.t = c.accumulator = 0; c.thrown++; c.trails = [];
c.events.push({ kind: 'basket-throw', power: c.power });
return true;
}
export function press(c) {
if (!canThrow(c) || c.holding) return false;
c.previousPower = c.power; c.holding = true; c.holdTime = c.power = 0; return true;
}
export function cancel(c) {
if (!c.holding) return false;
c.holding = false; c.power = c.previousPower; c.holdTime = 0; return true;
}
export function release(c) {
if (!c.holding) return false;
if (c.holdTime < 0.14) return cancel(c); // un tocco breve serve a mirare, non spreca un pallone
c.holding = false; return shoot(c);
}
function hit(c, kind, strength) { if (c.events.length < 12) c.events.push({ kind, power: clamp(strength, 0, 1) }); }
// Il ferro visto di lato sono due punti: il davanti e il dietro dell'anello. La palla ci rimbalza come su un tondino.
function rimEnd(c, b, px, py) {
const dx = b.x - px, dy = b.y - py, d = Math.hypot(dx, dy), r = BALL + RIM.r;
if (d >= r || d < 1e-9) return;
const nx = dx / d, ny = dy / d, closing = b.vx * nx + b.vy * ny;
b.x = px + nx * r; b.y = py + ny * r;
if (closing < 0) {
const tx = -ny, ty = nx, vt = (b.vx * tx + b.vy * ty) * 0.92, vn = -closing * 0.58;
b.vx = vn * nx + vt * tx; b.vy = vn * ny + vt * ty;
b.rim = true;
if (-closing > 25) hit(c, 'basket-rim', -closing / 400); // i microurti di una palla che si posa non tintinnano
}
}
function physics(c, dt) {
const b = c.ball, before = b.y;
b.vy += G * dt; b.x += b.vx * dt; b.y += b.vy * dt; b.time += dt; b.spin += b.vx * dt / BALL;
// Il tabellone: una lastra verticale che guarda Andrea, ma rimanda anche una palla che torna da dietro.
// Il palo dietro ferma quel che passa oltre, dalla sua cima in giù: sopra, la palla vola via e ricade.
if (b.y + BALL > BOARD.top && b.y - BALL < BOARD.bottom) {
if (b.vx > 0 && b.x + BALL > BOARD.x && b.x - BALL < BOARD.x + BOARD.depth) { b.x = BOARD.x - BALL; b.vx = -b.vx * 0.6; b.board = true; hit(c, 'basket-board', Math.abs(b.vx) / 300); }
else if (b.vx < 0 && b.x - BALL < BOARD.x + BOARD.depth && b.x + BALL > BOARD.x) { b.x = BOARD.x + BOARD.depth + BALL; b.vx = -b.vx * 0.6; b.board = true; hit(c, 'basket-board', Math.abs(b.vx) / 300); }
}
if (b.x + BALL > POLE.x && b.y + BALL > POLE.top) { b.x = POLE.x - BALL; if (b.vx > 0) { b.vx = -b.vx * 0.5; b.board = true; hit(c, 'basket-board', 0.3); } }
rimEnd(c, b, RIM.x1, RIM.y); rimEnd(c, b, RIM.x2, RIM.y);
// Il canestro: la palla attraversa l'anello scendendo, fra il davanti e il dietro, e non da sotto.
const inside = b.x > RIM.x1 + BALL * 0.6 && b.x < RIM.x2 - BALL * 0.6;
if (inside && before <= RIM.y && b.y > RIM.y && b.vy > 0 && !b.scored && !b.under) {
b.scored = true; b.scoredAt = b.time; b.vx *= 0.35; b.vy *= 0.5;
hit(c, b.rim || b.board ? 'basket-in' : 'basket-swish', 0.7);
// Il canestro conta subito: il punteggio e, all'ultimo pallone, il risultato non aspettano la palla ferma.
c.baskets++; c.points += spot(c).points;
if (c.thrown >= BALLS) finish(c);
} else if (inside && before >= RIM.y && b.y < RIM.y && b.vy < 0) b.under = true;
if (b.scored && b.y < NET + BALL) { b.vx *= 1 - 2.5 * dt; b.vy *= 1 - 1.2 * dt; }
if (b.y + BALL > FLOOR) {
b.y = FLOOR - BALL;
if (b.vy > 40) { b.vy = -b.vy * 0.55; b.vx *= 0.85; b.bounces++; hit(c, 'basket-bounce', b.vy / -400); }
else { b.vy = 0; b.vx *= 1 - 3 * dt; }
}
const still = Math.hypot(b.vx, b.vy) < 10;
// Entrata, la palla è finita: non si aspetta che rotoli. Una palla posata sul ferro, nemmeno.
if (b.x < -20 || b.time > FLIGHT_MAX || b.bounces > 6 || (b.y + BALL >= FLOOR - 0.5 && Math.abs(b.vx) < 6 && b.vy === 0)
|| (b.scored && b.y > NET + 30) || (still && b.time > 1.5)) b.dead = true;
}
function finish(c) {
c.result = { winner: c.baskets >= c.skill.goal ? 'player' : 'cpu', baskets: c.baskets, points: c.points, goal: c.skill.goal };
}
function nextBall(c) {
const b = c.ball;
c.history.push({ spot: c.spot, scored: b.scored, rim: b.rim, board: b.board });
if (b.scored) c.spot = Math.min(c.spot + 1, SPOTS.length - 1);
if (c.thrown >= BALLS) { if (!c.result) finish(c); c.step = 'result'; } else c.step = 'aim';
c.t = 0;
}
export function update(c, dt) {
if (!Number.isFinite(dt) || dt <= 0) return;
dt = Math.min(dt, 0.25); c.t += dt;
if (c.holding) { c.holdTime += dt; c.power = powerAt(c.holdTime); }
if (c.step === 'flight') {
c.accumulator += dt;
while (c.accumulator >= FIXED_STEP && !c.ball.dead) { physics(c, FIXED_STEP); c.accumulator -= FIXED_STEP; }
const b = c.ball;
if (!b.dead && Math.hypot(b.vx, b.vy) > 60) { c.trails.push({ x: b.x, y: b.y, age: 0 }); if (c.trails.length > 14) c.trails.shift(); }
if (b.dead) { c.step = 'settle'; c.t = 0; }
} else if (c.step === 'settle' && c.t >= REST) nextBall(c);
for (const t of c.trails) t.age += dt;
c.trails = c.trails.filter(t => t.age < 0.5);
}
export function status(c, names = { player: 'Andrea' }) {
const tally = `Canestri ${c.baskets} (ne servono ${c.skill.goal}), punti ${c.points}.`;
if (c.step === 'result') return `${c.result.baskets} canestri su ${BALLS}, ${c.result.points} punti. ${c.result.winner === 'player' ? `Obiettivo ${c.result.goal}: fatto!` : `Ne servivano ${c.result.goal}.`}`;
if (c.step === 'flight') return c.ball.scored ? `Dentro! ${tally}` : 'Palla in aria…';
if (c.step === 'settle') return c.ball.scored ? `Canestro! ${tally}` : `Fuori. ${tally}`;
return `${names.player}, pallone ${c.thrown + 1} di ${BALLS}, ${spot(c).name}. ${tally}`;
}
+18
View File
@@ -3,6 +3,7 @@
import { findPath, dist, inBox, boxDistance, nearestInside } from './geometry.js'; import { findPath, dist, inBox, boxDistance, nearestInside } from './geometry.js';
import * as bocce from './bocce.js'; import * as bocce from './bocce.js';
import * as biliardo from './biliardo.js'; import * as biliardo from './biliardo.js';
import * as basket from './basket.js';
import * as briscola from './briscola.js'; import * as briscola from './briscola.js';
import * as tressette from './tressette.js'; import * as tressette from './tressette.js';
import * as scopa from './scopa.js'; import * as scopa from './scopa.js';
@@ -14,6 +15,7 @@ export const MINIGAMES = {
scopa: { create: (seed, level, deck) => scopa.newScopa(seed, { level, deck }), update: scopa.update, play: scopa.play }, scopa: { create: (seed, level, deck) => scopa.newScopa(seed, { level, deck }), update: scopa.update, play: scopa.play },
bocce: { create: (seed, level) => bocce.newBocce(seed, { level }), update: bocce.update, press: bocce.press, release: bocce.release, cancel: bocce.cancel, aim: bocce.aimScreen }, bocce: { create: (seed, level) => bocce.newBocce(seed, { level }), update: bocce.update, press: bocce.press, release: bocce.release, cancel: bocce.cancel, aim: bocce.aimScreen },
biliardo: { create: (seed, level) => biliardo.newBiliardo(seed, { level }), update: biliardo.update, press: biliardo.press, release: biliardo.release, cancel: biliardo.cancel, aim: biliardo.aimScreen }, biliardo: { create: (seed, level) => biliardo.newBiliardo(seed, { level }), update: biliardo.update, press: biliardo.press, release: biliardo.release, cancel: biliardo.cancel, aim: biliardo.aimScreen },
basket: { create: (seed, level) => basket.newBasket(seed, { level }), update: basket.update, press: basket.press, release: basket.release, cancel: basket.cancel, aim: basket.aimScreen },
}; };
export const VERBS = ['walk', 'look', 'take', 'use', 'talk', 'open', 'give', 'drop']; export const VERBS = ['walk', 'look', 'take', 'use', 'talk', 'open', 'give', 'drop'];
@@ -70,6 +72,22 @@ export function shuffledCredits(credits, random = Math.random) {
}); });
} }
// Le immagini del carosello dei Ringraziamenti: tutti gli sfondi del paese (uno per scena, senza doppioni) col nome
// del posto, e un ritratto dei crediti ogni quattro sfondi; i ritratti che non trovano posto chiudono la fila.
export function reelPictures(story) {
const seen = new Set(), scenes = [];
for (const s of Object.values(story.scenes)) {
if (!s.background || seen.has(s.background)) continue;
seen.add(s.background);
scenes.push({ src: s.background, name: s.name, kind: 'scene' });
}
const faces = (story.credits ?? []).flatMap(c => c.names).filter(n => n.face).map(facePicture);
let next = 0;
const placed = scenes.flatMap((p, i) => i % 4 === 3 && next < faces.length ? [p, faces[next++]] : [p]);
return [...placed, ...faces.slice(next)];
}
export const facePicture = n => ({ src: n.face, name: n.name, kind: n.face.endsWith('palio.png') ? 'palio' : 'face' });
// La ricerca della mappa: ogni parola scritta deve stare nel nome del posto, senza badare a maiuscole, accenti e // La ricerca della mappa: ogni parola scritta deve stare nel nome del posto, senza badare a maiuscole, accenti e
// apostrofi («camp» trova Campomigliaio, «de medici» trova Via de' Medici). // apostrofi («camp» trova Campomigliaio, «de medici» trova Via de' Medici).
const plain = text => text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, ' ').trim(); const plain = text => text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, ' ').trim();
+6
View File
@@ -5,6 +5,7 @@ import { ambientEffects, ambientHash } from './ambient.js';
import { drawTorch } from './torch.js'; import { drawTorch } from './torch.js';
import { drawBocce } from './bocce-render.js'; import { drawBocce } from './bocce-render.js';
import { drawBiliardo } from './biliardo-render.js'; import { drawBiliardo } from './biliardo-render.js';
import { drawBasket } from './basket-render.js';
import { drawBriscola } from './briscola-render.js'; import { drawBriscola } from './briscola-render.js';
import { drawTressette } from './tressette-render.js'; import { drawTressette } from './tressette-render.js';
import { drawScopa } from './scopa-render.js'; import { drawScopa } from './scopa-render.js';
@@ -682,6 +683,11 @@ function drawCloseup(ctx, game, time) {
drawBocce(ctx, c, def, backgrounds.get(def.picture), time); drawBocce(ctx, c, def, backgrounds.get(def.picture), time);
return; return;
} }
if (def.type === 'basket') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawBasket(ctx, c, def, backgrounds.get(def.picture), sprites.get('andrea_basket'));
return;
}
if (def.type === 'biliardo') { if (def.type === 'biliardo') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture); if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawBiliardo(ctx, c, def, backgrounds.get(def.picture), sprites.get('ing_tozzi')); drawBiliardo(ctx, c, def, backgrounds.get(def.picture), sprites.get('ing_tozzi'));
+7
View File
@@ -118,6 +118,13 @@ export class Soundscape {
else if (kind === 'biliardo-hit') { this.tone(2600, 1500, 0.045, 0.03 + strength * 0.08, 0, pan); this.noiseSound(0.04, 3200, 0.03, { pan }); } else if (kind === 'biliardo-hit') { this.tone(2600, 1500, 0.045, 0.03 + strength * 0.08, 0, pan); this.noiseSound(0.04, 3200, 0.03, { pan }); }
else if (kind === 'biliardo-rail') this.noiseSound(0.12, 520, 0.03 + strength * 0.06, { pan }); else if (kind === 'biliardo-rail') this.noiseSound(0.12, 520, 0.03 + strength * 0.06, { pan });
else if (kind === 'biliardo-pin') { this.tone(3200, 2200, 0.03, 0.04 + strength * 0.05, 0, pan); this.noiseSound(0.08, 1800, 0.035, { pan }); } else if (kind === 'biliardo-pin') { this.tone(3200, 2200, 0.03, 0.04 + strength * 0.05, 0, pan); this.noiseSound(0.08, 1800, 0.035, { pan }); }
// Il basket: il tiro, la palla che batte per terra, il ferro, il tabellone, la retina.
else if (kind === 'basket-throw') this.noiseSound(0.08, 1200, 0.05, { pan });
else if (kind === 'basket-bounce') { this.tone(190, 95, 0.11, 0.05 + strength * 0.12, 0, pan); this.noiseSound(0.06, 500, 0.05, { pan }); }
else if (kind === 'basket-rim') { this.tone(1500, 1100, 0.14, 0.04 + strength * 0.1, 0, pan); this.tone(2300, 1900, 0.08, 0.03, 0, pan); }
else if (kind === 'basket-board') { this.tone(280, 170, 0.09, 0.05 + strength * 0.08, 0, pan); this.noiseSound(0.07, 900, 0.07, { pan }); }
else if (kind === 'basket-swish') this.noiseSound(0.28, 3200, 0.1, { paper: true, pan });
else if (kind === 'basket-in') { this.tone(1300, 1000, 0.08, 0.04, 0, pan); this.noiseSound(0.22, 2800, 0.08, { paper: true, pan }); }
else if (kind === 'wood') this.noiseSound(0.3, 750, 0.13, { pan }); else if (kind === 'wood') this.noiseSound(0.3, 750, 0.13, { pan });
else if (kind === 'tavolo-lontano') { else if (kind === 'tavolo-lontano') {
// Su un altro tavolo: il colpo di stecca, poi la biglia che ne prende un'altra, piano, da lontano. // Su un altro tavolo: il colpo di stecca, poi la biglia che ne prende un'altra, piano, da lontano.

Some files were not shown because too many files have changed in this diff Show More