diff --git a/CLAUDE.md b/CLAUDE.md index bcfdb86..2d6fb85 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # La Leggenda del Regolo — note per Claude Avventura grafica punta-e-clicca nello spirito delle avventure LucasArts, ambientata a San Piero a Sieve (Mugello) -nel 2007, durante il Palio della Fortezza. Protagonista: Andrea (l'utente) da ragazzo. Tono comico con nostalgia. +nel 2007, durante il Palio della Fortezza. Protagonista: Andrea (l'utente), che nel 2007 ha 27 anni (è nato nel 1980). Tono comico con nostalgia. **Il gioco è dedicato al Lele**, l'amico di Andrea che gli fece conoscere Monkey Island e che è scomparso troppo presto. Trattare questo tema con delicatezza; come e quanto farlo comparire nel gioco lo decide Andrea. diff --git a/assets/scenes/forno-conti.png b/assets/scenes/forno-conti.png new file mode 100644 index 0000000..11c7f47 Binary files /dev/null and b/assets/scenes/forno-conti.png differ diff --git a/assets/scenes/pieve-interno.png b/assets/scenes/pieve-interno.png new file mode 100644 index 0000000..14d1c12 Binary files /dev/null and b/assets/scenes/pieve-interno.png differ diff --git a/assets/voices/andrea-018183c4dd.mp3 b/assets/voices/andrea-018183c4dd.mp3 new file mode 100644 index 0000000..ee029dd Binary files /dev/null and b/assets/voices/andrea-018183c4dd.mp3 differ diff --git a/assets/voices/andrea-055b06b557.mp3 b/assets/voices/andrea-055b06b557.mp3 new file mode 100644 index 0000000..70c78a3 Binary files /dev/null and b/assets/voices/andrea-055b06b557.mp3 differ diff --git a/assets/voices/andrea-0a073729fd.mp3 b/assets/voices/andrea-0a073729fd.mp3 new file mode 100644 index 0000000..38a37ed Binary files /dev/null and b/assets/voices/andrea-0a073729fd.mp3 differ diff --git a/assets/voices/andrea-0eea4f27c6.mp3 b/assets/voices/andrea-0eea4f27c6.mp3 deleted file mode 100644 index 98b1b55..0000000 Binary files a/assets/voices/andrea-0eea4f27c6.mp3 and /dev/null differ diff --git a/assets/voices/andrea-11b12d551f.mp3 b/assets/voices/andrea-11b12d551f.mp3 new file mode 100644 index 0000000..55e39cc Binary files /dev/null and b/assets/voices/andrea-11b12d551f.mp3 differ diff --git a/assets/voices/andrea-1baedec826.mp3 b/assets/voices/andrea-1baedec826.mp3 new file mode 100644 index 0000000..3f48b6a Binary files /dev/null and b/assets/voices/andrea-1baedec826.mp3 differ diff --git a/assets/voices/andrea-436034d37e.mp3 b/assets/voices/andrea-436034d37e.mp3 new file mode 100644 index 0000000..fecc45d Binary files /dev/null and b/assets/voices/andrea-436034d37e.mp3 differ diff --git a/assets/voices/andrea-47b4e5ab68.mp3 b/assets/voices/andrea-47b4e5ab68.mp3 new file mode 100644 index 0000000..ed78cc1 Binary files /dev/null and b/assets/voices/andrea-47b4e5ab68.mp3 differ diff --git a/assets/voices/andrea-4a7bfec8b2.mp3 b/assets/voices/andrea-4a7bfec8b2.mp3 deleted file mode 100644 index 43f614c..0000000 Binary files a/assets/voices/andrea-4a7bfec8b2.mp3 and /dev/null differ diff --git a/assets/voices/andrea-51f5bdb47f.mp3 b/assets/voices/andrea-51f5bdb47f.mp3 new file mode 100644 index 0000000..a189ff7 Binary files /dev/null and b/assets/voices/andrea-51f5bdb47f.mp3 differ diff --git a/assets/voices/andrea-54884b5fa9.mp3 b/assets/voices/andrea-54884b5fa9.mp3 deleted file mode 100644 index 6693171..0000000 Binary files a/assets/voices/andrea-54884b5fa9.mp3 and /dev/null differ diff --git a/assets/voices/andrea-5d014ec41b.mp3 b/assets/voices/andrea-5d014ec41b.mp3 new file mode 100644 index 0000000..0b1225c Binary files /dev/null and b/assets/voices/andrea-5d014ec41b.mp3 differ diff --git a/assets/voices/andrea-5df5f2d69e.mp3 b/assets/voices/andrea-5df5f2d69e.mp3 new file mode 100644 index 0000000..ce35996 Binary files /dev/null and b/assets/voices/andrea-5df5f2d69e.mp3 differ diff --git a/assets/voices/andrea-611215643c.mp3 b/assets/voices/andrea-611215643c.mp3 new file mode 100644 index 0000000..70f18b7 Binary files /dev/null and b/assets/voices/andrea-611215643c.mp3 differ diff --git a/assets/voices/andrea-62fb1fdbf2.mp3 b/assets/voices/andrea-62fb1fdbf2.mp3 new file mode 100644 index 0000000..31e4824 Binary files /dev/null and b/assets/voices/andrea-62fb1fdbf2.mp3 differ diff --git a/assets/voices/andrea-647ffbf576.mp3 b/assets/voices/andrea-647ffbf576.mp3 new file mode 100644 index 0000000..2907a4a Binary files /dev/null and b/assets/voices/andrea-647ffbf576.mp3 differ diff --git a/assets/voices/andrea-69eaa84de0.mp3 b/assets/voices/andrea-69eaa84de0.mp3 new file mode 100644 index 0000000..2595613 Binary files /dev/null and b/assets/voices/andrea-69eaa84de0.mp3 differ diff --git a/assets/voices/andrea-70ecfbf3f3.mp3 b/assets/voices/andrea-70ecfbf3f3.mp3 new file mode 100644 index 0000000..5b6a1a1 Binary files /dev/null and b/assets/voices/andrea-70ecfbf3f3.mp3 differ diff --git a/assets/voices/andrea-72e4ebb412.mp3 b/assets/voices/andrea-72e4ebb412.mp3 deleted file mode 100644 index f5d7cd1..0000000 Binary files a/assets/voices/andrea-72e4ebb412.mp3 and /dev/null differ diff --git a/assets/voices/andrea-7655ebd550.mp3 b/assets/voices/andrea-7655ebd550.mp3 deleted file mode 100644 index c2ac9fb..0000000 Binary files a/assets/voices/andrea-7655ebd550.mp3 and /dev/null differ diff --git a/assets/voices/andrea-8ae8691b63.mp3 b/assets/voices/andrea-8ae8691b63.mp3 new file mode 100644 index 0000000..2c009a5 Binary files /dev/null and b/assets/voices/andrea-8ae8691b63.mp3 differ diff --git a/assets/voices/andrea-a16136da3a.mp3 b/assets/voices/andrea-a16136da3a.mp3 new file mode 100644 index 0000000..4da7baa Binary files /dev/null and b/assets/voices/andrea-a16136da3a.mp3 differ diff --git a/assets/voices/andrea-a79f677fc1.mp3 b/assets/voices/andrea-a79f677fc1.mp3 new file mode 100644 index 0000000..4ea7ae0 Binary files /dev/null and b/assets/voices/andrea-a79f677fc1.mp3 differ diff --git a/assets/voices/andrea-a964de1d7b.mp3 b/assets/voices/andrea-a964de1d7b.mp3 new file mode 100644 index 0000000..497f0b5 Binary files /dev/null and b/assets/voices/andrea-a964de1d7b.mp3 differ diff --git a/assets/voices/andrea-ac2407893f.mp3 b/assets/voices/andrea-ac2407893f.mp3 new file mode 100644 index 0000000..2f352aa Binary files /dev/null and b/assets/voices/andrea-ac2407893f.mp3 differ diff --git a/assets/voices/andrea-b00aebf83a.mp3 b/assets/voices/andrea-b00aebf83a.mp3 new file mode 100644 index 0000000..ad1efed Binary files /dev/null and b/assets/voices/andrea-b00aebf83a.mp3 differ diff --git a/assets/voices/andrea-b3acca1e65.mp3 b/assets/voices/andrea-b3acca1e65.mp3 deleted file mode 100644 index 948a9d8..0000000 Binary files a/assets/voices/andrea-b3acca1e65.mp3 and /dev/null differ diff --git a/assets/voices/andrea-b915fb9eba.mp3 b/assets/voices/andrea-b915fb9eba.mp3 new file mode 100644 index 0000000..474d9c3 Binary files /dev/null and b/assets/voices/andrea-b915fb9eba.mp3 differ diff --git a/assets/voices/andrea-bed4ce97bd.mp3 b/assets/voices/andrea-bed4ce97bd.mp3 new file mode 100644 index 0000000..320e1dd Binary files /dev/null and b/assets/voices/andrea-bed4ce97bd.mp3 differ diff --git a/assets/voices/andrea-c30bff01c2.mp3 b/assets/voices/andrea-c30bff01c2.mp3 deleted file mode 100644 index 7e8826e..0000000 Binary files a/assets/voices/andrea-c30bff01c2.mp3 and /dev/null differ diff --git a/assets/voices/andrea-c73c366e50.mp3 b/assets/voices/andrea-c73c366e50.mp3 deleted file mode 100644 index 7527471..0000000 Binary files a/assets/voices/andrea-c73c366e50.mp3 and /dev/null differ diff --git a/assets/voices/andrea-d309ba9dfc.mp3 b/assets/voices/andrea-d309ba9dfc.mp3 new file mode 100644 index 0000000..429c37c Binary files /dev/null and b/assets/voices/andrea-d309ba9dfc.mp3 differ diff --git a/assets/voices/andrea-daced50ca3.mp3 b/assets/voices/andrea-daced50ca3.mp3 deleted file mode 100644 index f43140c..0000000 Binary files a/assets/voices/andrea-daced50ca3.mp3 and /dev/null differ diff --git a/assets/voices/andrea-df05ecea32.mp3 b/assets/voices/andrea-df05ecea32.mp3 new file mode 100644 index 0000000..5a13cc0 Binary files /dev/null and b/assets/voices/andrea-df05ecea32.mp3 differ diff --git a/assets/voices/andrea-e47b3cebb5.mp3 b/assets/voices/andrea-e47b3cebb5.mp3 new file mode 100644 index 0000000..070d275 Binary files /dev/null and b/assets/voices/andrea-e47b3cebb5.mp3 differ diff --git a/assets/voices/andrea-f66b1e4179.mp3 b/assets/voices/andrea-f66b1e4179.mp3 new file mode 100644 index 0000000..fdf4590 Binary files /dev/null and b/assets/voices/andrea-f66b1e4179.mp3 differ diff --git a/assets/voices/andrea-f6fa5d6d6d.mp3 b/assets/voices/andrea-f6fa5d6d6d.mp3 new file mode 100644 index 0000000..41d90d5 Binary files /dev/null and b/assets/voices/andrea-f6fa5d6d6d.mp3 differ diff --git a/assets/voices/andrea-fe5b32c079.mp3 b/assets/voices/andrea-fe5b32c079.mp3 deleted file mode 100644 index 7c8ae10..0000000 Binary files a/assets/voices/andrea-fe5b32c079.mp3 and /dev/null differ diff --git a/assets/voices/index.json b/assets/voices/index.json index dbb59da..8d807d3 100644 --- a/assets/voices/index.json +++ b/assets/voices/index.json @@ -219,6 +219,34 @@ "file": "andrea-d7882fce87.mp3", "sec": 3.35 }, + "andrea|L'altare, coi candelieri dorati e i fiori freschi. Qualcuno li cambia ogni mattina, prima di tutti.": { + "file": "andrea-a79f677fc1.mp3", + "sec": 8.38 + }, + "andrea|Un candeliere? Mi sa che San Pietro le chiavi del Paradiso me le toglie per sempre.": { + "file": "andrea-5df5f2d69e.mp3", + "sec": 6.94 + }, + "andrea|Il grande crocifisso di legno, davanti alla statua di San Pietro.": { + "file": "andrea-055b06b557.mp3", + "sec": 4.22 + }, + "andrea|Qui si abbassa la voce senza che nessuno lo chieda.": { + "file": "andrea-62fb1fdbf2.mp3", + "sec": 3.36 + }, + "andrea|San Pietro, il patrono. Quello fuori sul sagrato parla. Questo, per fortuna, no.": { + "file": "andrea-51f5bdb47f.mp3", + "sec": 8.54 + }, + "andrea|Le panche dove ci siamo seduti tutti, dal battesimo in poi. Le più scomode del Mugello.": { + "file": "andrea-b915fb9eba.mp3", + "sec": 6.28 + }, + "andrea|Un minuto seduto... no, se mi siedo qui mi addormento come alla messa delle undici.": { + "file": "andrea-d309ba9dfc.mp3", + "sec": 5.72 + }, "andrea|Il palco dell'oratorio. D'estate ci fanno le recite, la tombola e i concerti.": { "file": "andrea-058d10ea48.mp3", "sec": 7.18 @@ -327,9 +355,9 @@ "file": "andrea-69dce8d64f.mp3", "sec": 3.6 }, - "andrea|Lo scivolo e le altalene. Sono troppo grande, ufficialmente. Ufficiosamente, un giro ci starebbe.": { - "file": "andrea-4a7bfec8b2.mp3", - "sec": 7.99 + "andrea|Lo scivolo e le altalene. Ho ventisette anni, ufficialmente. Ufficiosamente, un giro ci starebbe.": { + "file": "andrea-611215643c.mp3", + "sec": 7.56 }, "andrea|Un giro sull'altalena! ... No, c'è troppa gente che mi conosce.": { "file": "andrea-40c93c1f52.mp3", @@ -399,17 +427,25 @@ "file": "andrea-ea4d85e1ca.mp3", "sec": 5.15 }, - "andrea|Più avanti ci sono i campi da tennis e da padel.": { - "file": "andrea-54884b5fa9.mp3", - "sec": 3.54 + "andrea|Più avanti ci sono i campi da tennis. Racchette, palline e gente che si lamenta del sole.": { + "file": "andrea-69eaa84de0.mp3", + "sec": 7.13 }, - "andrea|Il padel, nel 2007, non sapevo nemmeno cosa fosse. Lo scopriranno i miei genitori, tra qualche anno.": { - "file": "andrea-c30bff01c2.mp3", - "sec": 7.54 + "andrea|Laggiù i campi da tennis.": { + "file": "andrea-11b12d551f.mp3", + "sec": 2.18 }, - "andrea|Laggiù i campi da tennis e da padel. Racchette, palline e gente che si lamenta del sole.": { - "file": "andrea-daced50ca3.mp3", - "sec": 7.5 + "andrea|Il capannone verde del padel.": { + "file": "andrea-a16136da3a.mp3", + "sec": 1.63 + }, + "andrea|Il padel, nel 2007, non so nemmeno cosa sia. Tra qualche anno ci giocheranno tutti, me compreso.": { + "file": "andrea-1baedec826.mp3", + "sec": 7.77 + }, + "andrea|Chiuso. E poi non saprei nemmeno da che parte si tiene la racchetta.": { + "file": "andrea-e47b3cebb5.mp3", + "sec": 4.87 }, "andrea|La tribuna bianca e rossa. La domenica qui c'è più tifo che al Palio. Quasi.": { "file": "andrea-a50071085e.mp3", @@ -423,6 +459,42 @@ "file": "andrea-40b7bf59ae.mp3", "sec": 4.99 }, + "andrea|Il forno. Il profumo del pane arriva fino alla Pieve, e la domenica mattina fino a casa mia.": { + "file": "andrea-436034d37e.mp3", + "sec": 6.62 + }, + "andrea|Una schiacciata calda... no, prima il palio. Però l'odore me lo porto dietro.": { + "file": "andrea-ac2407893f.mp3", + "sec": 6.62 + }, + "andrea|Più avanti c'è «alla Luna», e poi Campomigliaio, il mio rione: i blu.": { + "file": "andrea-f66b1e4179.mp3", + "sec": 5.22 + }, + "andrea|Ci torno quando ho ritrovato il palio. A mani vuote non mi ci faccio vedere.": { + "file": "andrea-df05ecea32.mp3", + "sec": 4.54 + }, + "andrea|La Provinciale va avanti verso «alla Luna» e Campomigliaio.": { + "file": "andrea-a964de1d7b.mp3", + "sec": 2.95 + }, + "andrea|La stazione della Faentina. Da qui i treni vanno a Firenze e a Faenza, con calma.": { + "file": "andrea-647ffbf576.mp3", + "sec": 6.3 + }, + "andrea|Molta calma.": { + "file": "andrea-47b4e5ab68.mp3", + "sec": 1.04 + }, + "andrea|Il bar della stazione. Qui si aspetta il treno, e quando il treno è in ritardo si aspetta un altro caffè.": { + "file": "andrea-5d014ec41b.mp3", + "sec": 6.97 + }, + "andrea|Non ho tempo per un caffè. E poi il caffè mi fa tremare come la carretta del Palio.": { + "file": "andrea-b00aebf83a.mp3", + "sec": 6.31 + }, "andrea|Villa Adami. Gialla come un tuorlo, con le persiane verdi.": { "file": "andrea-225e29ed43.mp3", "sec": 5.12 @@ -455,9 +527,9 @@ "file": "andrea-468fe91000.mp3", "sec": 3.28 }, - "andrea|Di là si va alla scuola. Ci passo già abbastanza tempo durante l'anno.": { - "file": "andrea-b3acca1e65.mp3", - "sec": 5.34 + "andrea|Di là si va alla scuola. Ci ho passato abbastanza anni, grazie.": { + "file": "andrea-f6fa5d6d6d.mp3", + "sec": 4.03 }, "andrea|Il muro vecchio di sassi, mezzo coperto dai rampicanti. Qui ci si nascondeva a nascondino.": { "file": "andrea-ecc1aee547.mp3", @@ -519,29 +591,29 @@ "file": "andrea-ea832da7b0.mp3", "sec": 4.01 }, - "andrea|Arrampicarmi? Mia mamma mi vede dalla finestra.": { - "file": "andrea-c73c366e50.mp3", - "sec": 3.99 + "andrea|Arrampicarmi? Alla mia età, e davanti a tutto il condominio?": { + "file": "andrea-018183c4dd.mp3", + "sec": 5.82 }, - "andrea|Un pino a ombrello enorme. Alla ricreazione, tutti all'ombra lì sotto.": { - "file": "andrea-0eea4f27c6.mp3", - "sec": 5.3 + "andrea|Un pino a ombrello enorme. Alla ricreazione, tutti all'ombra lì sotto. Sembra ieri.": { + "file": "andrea-0a073729fd.mp3", + "sec": 7.3 }, "andrea|Il tricolore e la bandiera europea. Almeno loro a scuola ci stanno volentieri.": { "file": "andrea-929dc868d7.mp3", "sec": 5.91 }, - "andrea|Rubare una bandiera davanti alla scuola? Poi chi la sente la preside.": { - "file": "andrea-7655ebd550.mp3", - "sec": 5.53 + "andrea|Rubare una bandiera davanti alla scuola? Ci manca solo che mi richiamino in presidenza, a ventisette anni.": { + "file": "andrea-bed4ce97bd.mp3", + "sec": 6.99 }, "andrea|Le porte a vetri della scuola. Di là ci sono i banchi, la lavagna e la campanella.": { "file": "andrea-a24a09d869.mp3", "sec": 7.5 }, - "andrea|A scuola? A maggio? Nemmeno per sogno.": { - "file": "andrea-72e4ebb412.mp3", - "sec": 3.88 + "andrea|Tornare a scuola? A ventisette anni? Nemmeno per sogno.": { + "file": "andrea-8ae8691b63.mp3", + "sec": 5.44 }, "andrea|Le palazzine gialle accanto alla scuola, con la scala arancione.": { "file": "andrea-528eda3257.mp3", @@ -579,9 +651,9 @@ "file": "andrea-658f69bfec.mp3", "sec": 4.35 }, - "andrea|Una vite? Ci vuole la vendemmia, non un ragazzo.": { - "file": "andrea-fe5b32c079.mp3", - "sec": 4.13 + "andrea|Una vite? Ci vuole la vendemmia, non uno con le mani in tasca.": { + "file": "andrea-70ecfbf3f3.mp3", + "sec": 4.85 }, "andrea|La scala che sale al pergolato.": { "file": "andrea-e8fba1f859.mp3", diff --git a/docs/IMMAGINI.md b/docs/IMMAGINI.md index ef815df..190d528 100644 --- a/docs/IMMAGINI.md +++ b/docs/IMMAGINI.md @@ -24,6 +24,7 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p | `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ì | | `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 | | `panorama-intro.png` | Codex | `altre/panorama-intro.png` | da internet (screenshot con l'icona di Google Lens) | **no**, finché non si sostituisce | Per uno sfondo «da internet» le strade sono tre: diff --git a/docs/PROGETTO.md b/docs/PROGETTO.md index a5a98b8..6e70919 100644 --- a/docs/PROGETTO.md +++ b/docs/PROGETTO.md @@ -4,7 +4,7 @@ Avventura grafica punta-e-clicca nello spirito delle avventure LucasArts dei primi anni '90, ambientata a **San Piero a Sieve** (Mugello) durante la settimana del **Palio della Fortezza** del **2007**. -Tono **comico con nostalgia**. Protagonista: **Andrea da ragazzo**. +Tono **comico con nostalgia**. Protagonista: **Andrea a 27 anni** (è nato nel 1980; nel 2007 ne ha 27). Questo documento è la prima bozza: tutto quello che è segnato **[DA ANDREA]** aspetta ricordi veri. @@ -119,7 +119,7 @@ Livello 0 Piazza Colonna ───────────────── Per Andrea il Trebbio è il posto dove salire a cercare pace (scherzando: il suo psicologo). Nel gioco diventa il **sistema di aiuti**: -- quando sei bloccato, **sali al Trebbio**: il ragazzo si siede sotto il pergolato, guarda il Mugello dall'alto, +- quando sei bloccato, **sali al Trebbio**: Andrea si siede sotto il pergolato, guarda il Mugello dall'alto, la musica si calma, e nella pace gli viene l'idea giusta (un indizio graduato: prima vago, poi più preciso); - il lato comico: la "seduta" è un dialogo con il castello stesso, che ascolta paziente e risponde con calma toscana; - **contrasto voluto**: il Trebbio è la luce e la pace, la Fortezza il buio e la leggenda. Il gioco vive tra questi @@ -141,7 +141,7 @@ Storia vera da usare per battute e segreti: | Personaggio | Dove | Idea | Stato | | --- | --- | --- | --- | -| **Andrea** (ragazzo) | ovunque | il protagonista: curioso, pigro quando serve, testardo quando conta | aspetto dal riferimento di Andrea: capelli castani ricci, maglietta blu con il triangolo azzurro, jeans chiari, scarpe da ginnastica bianche; battute [DA ANDREA] | +| **Andrea** (27 anni nel 2007) | ovunque | il protagonista: curioso, pigro quando serve, testardo quando conta | aspetto dal riferimento di Andrea: capelli castani ricci, maglietta blu con il triangolo azzurro, jeans chiari, scarpe da ginnastica bianche; battute [DA ANDREA] | | L'Arcu (Marco Arcuri) | ovunque | il migliore amico di Andrea, compagno di rione nei blu: la spalla dell'avventura? | aspetto dal riferimento: capelli lunghi raccolti, maglietta blu, guanti bianchi, jeans scuri; persona vera: [DA ANDREA: consenso di Marco] | | Il barista | Piazza | sa tutto di tutti, non dice niente gratis | [DA ANDREA: chi c'era nel 2007?] | | I vecchietti della panchina | Piazza | il "coro" del paese: commentano tutto, litigano sulla leggenda | [DA ANDREA] | diff --git a/src/engine/command.js b/src/engine/command.js index 2d11a1d..b8992af 100644 --- a/src/engine/command.js +++ b/src/engine/command.js @@ -26,9 +26,10 @@ export class Command { return isItem ? `Usa ${name} con…` : `${VERB_LABELS[g.defaultVerb(target)]} ${name}`; } - // Clic sulla scena: `look` per il clic destro o la pressione lunga. - clickScene(point, { look = false } = {}) { + // Clic sulla scena: `look` per il clic destro o la pressione lunga, `hurry` per il doppio clic (passo doppio). + clickScene(point, { look = false, hurry = false } = {}) { const g = this.game; + g.hurry = hurry; if (g.skip()) return 'skip'; if (g.dialogue || g.busy) return null; const h = g.hotspotAt(point); diff --git a/src/engine/game.js b/src/engine/game.js index efae77c..07d905b 100644 --- a/src/engine/game.js +++ b/src/engine/game.js @@ -62,6 +62,7 @@ export class Game { this.walking = false; this.time = 0; this.stride = 0; // strada fatta, per scegliere il passo della camminata + this.hurry = false; // doppio clic: si cammina al doppio della velocità this.reach = 0; // secondi di braccio teso quando prende qualcosa this.back = false; // di spalle davanti a una porta o a una bacheca } @@ -213,7 +214,7 @@ export class Game { } if (this.walking) { const scale = this.scaleAt(this.state.y); - let step = SPEED * scale * dt; + let step = SPEED * (this.hurry ? 2 : 1) * scale * dt; this.stride += step / scale; while (step > 0 && this.path.length) { const target = this.path[0], d = dist([this.state.x, this.state.y], target); diff --git a/src/engine/music.js b/src/engine/music.js index 875e414..7042861 100644 --- a/src/engine/music.js +++ b/src/engine/music.js @@ -23,6 +23,7 @@ export class Music { this.current = null; // { src, audio } this.old = []; this.duck = 1; // 1 = pieno; meno di 1 mentre parla qualcuno + this.hold = false; // in pausa: il brano sfuma e si ferma, poi riprende da dov'era } // Chiamata a ogni fotogramma con il brano voluto: avvia, cambia e sfuma. @@ -33,10 +34,12 @@ export class Music { this.current = src ? { src, audio: this.start(src) } : null; } // Il brano in corso sale (o scende) verso il volume scelto; quelli vecchi sfumano fino a zero. - const step = dt / FADE, target = this.muted ? 0 : this.volume * this.duck; + const step = dt / FADE, target = this.muted || this.hold ? 0 : this.volume * this.duck; if (this.current) { const a = this.current.audio; a.volume = a.volume < target ? Math.min(target, a.volume + step) : Math.max(target, a.volume - step); + if (this.hold && a.volume === 0 && !a.paused) a.pause(); + else if (!this.hold && a.paused && a !== this.blocked) a.play().catch(() => { this.blocked = a; }); } for (const a of this.old) a.volume = Math.max(0, a.volume - step); this.old = this.old.filter(a => a.volume > 0 || (a.pause(), false)); diff --git a/src/main.js b/src/main.js index 09f9867..9331bc4 100644 --- a/src/main.js +++ b/src/main.js @@ -10,7 +10,10 @@ import { Voices } from './engine/voices.js'; const $ = id => document.getElementById(id); const canvas = $('scene'), ctx = canvas.getContext('2d'); const storage = (() => { try { return window.localStorage; } catch { return null; } })(); -const LONG_PRESS = 450; +const LONG_PRESS = 450, DOUBLE_CLICK = 350, IDLE_PAUSE = 60000; +let idleSince = performance.now(); +for (const ev of ['pointerdown', 'pointermove', 'keydown', 'wheel']) addEventListener(ev, () => { idleSince = performance.now(); }, { capture: true, passive: true }); +let lastTap = null; const debug = new URLSearchParams(location.search).has('debug'); let game = null, command = null, hoverTarget = null, mouse = null, running = false; @@ -106,12 +109,16 @@ $('map-button').addEventListener('click', openMap); function openPause() { if (!running) return; running = false; + music.hold = true; + voices.stop(); command.reset(); $('pause-map').hidden = game.mapPlaces().length < 2; $('pause').hidden = false; } function closePause() { $('pause').hidden = true; + music.hold = false; + idleSince = performance.now(); running = true; } $('pause-button').addEventListener('click', openPause); @@ -119,6 +126,7 @@ $('resume').addEventListener('click', closePause); $('pause-map').addEventListener('click', () => { closePause(); openMap(); }); $('to-title').addEventListener('click', () => { $('pause').hidden = true; + music.hold = false; game.onSave(game.snapshot()); showTitle(); }); @@ -145,7 +153,13 @@ canvas.addEventListener('pointerdown', e => { canvas.addEventListener('pointerup', e => { if (!press) return; clearTimeout(press.timer); - if (!press.long) command.clickScene(scenePoint(e)); + // Il secondo clic vicino al primo, entro poco, è un doppio clic: stesso comando, al doppio della velocità. + if (!press.long) { + const point = scenePoint(e), now = performance.now(); + const hurry = !!lastTap && now - lastTap.time < DOUBLE_CLICK && Math.hypot(point[0] - lastTap.point[0], point[1] - lastTap.point[1]) < 24; + command.clickScene(point, { hurry }); + lastTap = hurry ? null : { time: now, point }; + } press = null; }); canvas.addEventListener('pointercancel', () => { if (press) clearTimeout(press.timer); press = null; }); @@ -281,6 +295,8 @@ function frame(now) { const screen = !$('title').hidden ? 'title' : !$('splash').hidden ? 'splash' : !$('intro').hidden ? 'intro' : !$('ending').hidden ? 'ending' : 'game'; music.duck = voices.playing ? 0.45 : 1; music.update(trackFor(story, { screen, scene: game?.state.scene }), dt); + // Un minuto senza toccare niente, a gioco fermo: pausa da sola. + if (running && now - idleSince > IDLE_PAUSE && !game.speech && !game.walking && !game.queue.length) openPause(); if (game) { if (running) game.update(dt); render(ctx, game, { hover: debug && hoverTarget ? game.hotspot(hoverTarget) : null }); diff --git a/src/story/story.js b/src/story/story.js index 285ab0d..24f15ba 100644 --- a/src/story/story.js +++ b/src/story/story.js @@ -24,6 +24,8 @@ export const story = { { role: 'Storia e aneddoti di San Piero', names: ['Francesco Bacci'] }, { role: 'Musica', names: ['Andrea Manzi, con Suno'] }, { role: 'Sfondi', names: ['dalle fotografie di San Piero di Andrea'] }, + // Chi ci ha mandato foto, ricordi o idee: una riga per contributo, si allunga man mano. + { role: 'Fotografie', names: ['Mariano Mirannalti (l\'interno della Pieve)'] }, { role: 'Voci', names: ['XTTS-v2 di Coqui'] }, { role: 'Programmazione', names: ['Andrea Manzi, con Claude e Codex'] }, ], @@ -72,7 +74,7 @@ export const story = { scenes: { piazza: { name: 'Piazza Colonna', - map: [16, 70], + map: [32, 45], background: 'assets/scenes/piazza-colonna.png', walk: [[84, 244], [330, 238], [345, 230], [362, 238], [462, 246], [466, 276], [544, 276], [548, 248], [588, 250], [600, 290], [640, 300], [640, 360], [0, 360], [0, 330], [84, 318]], depth: [[236, 0.5], [360, 1.35]], @@ -150,7 +152,7 @@ export const story = { id: 'verso_comune', name: 'a sinistra, verso il Comune', box: [96, 196, 50, 54], at: [120, 252], exit: { scene: 'comune', at: [20, 300], face: 'right' }, }, { - id: 'verso_calimara', name: 'Via Calimara, verso la farmacia', box: [180, 334, 380, 26], at: [360, 354], exit: { scene: 'via_calimara', at: [300, 350], face: 'up' }, + id: 'verso_calimara', name: 'Via Calimara, verso la farmacia', box: [180, 334, 380, 26], at: [360, 354], exit: { scene: 'via_calimara', at: [365, 212], face: 'down' }, }, { id: 'verso_pieve', name: 'verso la Pieve', box: [612, 292, 28, 68], at: [628, 330], exit: { scene: 'pieve', at: [30, 320], face: 'right' }, @@ -160,7 +162,7 @@ export const story = { }, fortezza: { name: 'Sotto le mura della Fortezza', - map: [30, 15], + map: [36, 6], background: 'assets/scenes/fortezza-esterno.png', walk: [[110, 284], [300, 280], [330, 272], [348, 274], [400, 292], [442, 294], [640, 352], [640, 360], [0, 360], [0, 315], [60, 310], [105, 295]], depth: [[272, 0.55], [360, 1.15]], @@ -211,7 +213,7 @@ export const story = { // Livello 0: la Pieve vista dalla salita, con la salita per Villa Adami alle spalle. pieve: { name: 'La Pieve', - map: [46, 70], + map: [56, 45], background: 'assets/scenes/pieve-dalla-salita.png', speakers: { san_pietro: [343, 166] }, // La rampa in primo piano e, dietro il muretto, il sagrato (ci si arriva da sinistra). @@ -226,6 +228,9 @@ export const story = { open: [{ say: 'Il prete è a benedire le carrette del Palio. Torno dopo.' }], }, }, + { + id: 'portone', name: 'portone della Pieve', box: [196, 160, 46, 56], at: [218, 222], face: 'up', exit: { scene: 'pieve_interno', at: [320, 352], face: 'up' }, + }, { id: 'campanile', name: 'campanile', box: [272, 8, 60, 140], at: [300, 300], face: 'up', actions: { @@ -277,20 +282,63 @@ export const story = { }, }, { - id: 'verso_provinciale', name: 'Via Provinciale, verso il River', box: [30, 330, 200, 30], at: [130, 350], exit: { scene: 'via_provinciale', at: [100, 350], face: 'up' }, + id: 'verso_provinciale', name: 'Via Provinciale, verso il River', box: [30, 330, 200, 30], at: [130, 350], exit: { scene: 'via_provinciale', at: [560, 240], face: 'left' }, }, { id: 'verso_piazza', name: 'verso la Piazza', box: [0, 262, 26, 98], at: [8, 320], exit: { scene: 'piazza', at: [610, 320], face: 'left' }, }, + { + id: 'verso_forno', name: 'a destra, verso il forno', box: [570, 176, 70, 76], at: [588, 250], exit: { scene: 'forno', at: [40, 330], face: 'right' }, + }, { id: 'salita_villa_adami', name: 'salita verso Villa Adami', box: [440, 330, 200, 30], at: [600, 350], exit: { scene: 'villa_adami', at: [595, 268], face: 'left' }, }, ], }, + // Dentro la Pieve (dalla foto di Mariano Mirannalti): la navata, l'altare, il crocifisso e San Pietro. + pieve_interno: { + name: 'Dentro la Pieve', + map: [56, 39], + background: 'assets/scenes/pieve-interno.png', + walk: [[262, 360], [290, 256], [350, 256], [380, 360]], + depth: [[256, 0.5], [360, 1.15]], + entry: [320, 352], + hotspots: [ + { + id: 'altare', name: 'altare', box: [270, 186, 100, 64], at: [320, 262], face: 'up', + actions: { + look: [{ say: 'L\'altare, coi candelieri dorati e i fiori freschi. Qualcuno li cambia ogni mattina, prima di tutti.' }], + take: [{ say: 'Un candeliere? Mi sa che San Pietro le chiavi del Paradiso me le toglie per sempre.' }], + }, + }, + { + id: 'crocifisso', name: 'crocifisso', box: [296, 100, 48, 70], at: [320, 262], face: 'up', + actions: { + look: [{ say: 'Il grande crocifisso di legno, davanti alla statua di San Pietro.' }, { say: 'Qui si abbassa la voce senza che nessuno lo chieda.' }], + }, + }, + { + id: 'san_pietro_statua', name: 'statua di San Pietro', box: [298, 64, 44, 40], at: [320, 262], face: 'up', + actions: { + look: [{ say: 'San Pietro, il patrono. Quello fuori sul sagrato parla. Questo, per fortuna, no.' }], + }, + }, + { + id: 'panche', name: 'panche', box: [0, 240, 250, 120], at: [280, 320], face: 'left', + actions: { + look: [{ say: 'Le panche dove ci siamo seduti tutti, dal battesimo in poi. Le più scomode del Mugello.' }], + use: [{ say: 'Un minuto seduto... no, se mi siedo qui mi addormento come alla messa delle undici.' }], + }, + }, + { + id: 'uscita', name: 'esci sul sagrato', box: [240, 334, 160, 26], at: [320, 356], exit: { scene: 'pieve', at: [218, 222], face: 'down' }, + }, + ], + }, // Il cortile dell'oratorio, dietro la porta della canonica: palco, tettoia, tavolate della festa. oratorio: { name: 'Oratorio della Pieve', - map: [74, 70], + map: [82, 45], background: 'assets/scenes/oratorio.png', walk: [[0, 236], [300, 226], [480, 222], [500, 232], [640, 254], [640, 360], [0, 360]], depth: [[222, 0.5], [360, 1.15]], @@ -340,7 +388,7 @@ export const story = { // Via de' Medici: dalla Piazza sale verso Villa Adami (la Piazza è alle spalle, in fondo si va su). via_de_medici: { name: 'Via de\' Medici', - map: [14, 52], + map: [22, 32], background: 'assets/scenes/via-de-medici.png', walk: [[0, 360], [0, 336], [160, 290], [262, 240], [288, 212], [312, 208], [340, 222], [470, 300], [545, 360]], depth: [[208, 0.32], [360, 1.25]], @@ -364,7 +412,7 @@ export const story = { // L'ingresso del campeggio: dalla Piazza a diritto; poco dopo, a destra, si sale al Bosco ai Frati. campeggio: { name: 'Ingresso del campeggio', - map: [16, 88], + map: [12, 84], background: 'assets/scenes/campeggio.png', walk: [[150, 360], [165, 262], [420, 266], [640, 286], [640, 360]], depth: [[262, 0.6], [360, 1.15]], @@ -395,7 +443,7 @@ export const story = { // Il convento del Bosco ai Frati, nel bosco sopra il campeggio. bosco_ai_frati: { name: 'Bosco ai Frati', - map: [42, 88], + map: [12, 96], background: 'assets/scenes/bosco-ai-frati.png', walk: [[0, 304], [100, 298], [480, 292], [640, 300], [640, 360], [0, 360]], depth: [[292, 0.78], [360, 1.1]], @@ -431,7 +479,7 @@ export const story = { // strada di fronte alla farmacia; da qui si scende ai giardini delle poste. comune: { name: 'Il Comune', - map: [6, 62], + map: [11, 45], background: 'assets/scenes/comune-asilo.png', walk: [[0, 360], [0, 262], [200, 256], [330, 250], [420, 268], [440, 305], [640, 330], [640, 360]], depth: [[250, 0.5], [360, 1.15]], @@ -464,14 +512,14 @@ export const story = { id: 'ai_giardini', name: 'giù, verso i giardini', box: [596, 300, 44, 60], at: [620, 345], exit: { scene: 'giardini_poste', at: [580, 340], face: 'left' }, }, { - id: 'alla_farmacia', name: 'giù, verso la farmacia', box: [60, 334, 520, 26], at: [320, 354], exit: { scene: 'via_provinciale', at: [320, 345], face: 'up' }, + id: 'alla_farmacia', name: 'giù, verso la farmacia', box: [60, 334, 520, 26], at: [320, 354], exit: { scene: 'via_provinciale', at: [622, 244], face: 'left' }, }, ], }, // I giardini delle poste: giù dal Comune; in fondo, le poste. giardini_poste: { name: 'Giardini delle poste', - map: [6, 78], + map: [11, 58], background: 'assets/scenes/giardini-poste.png', walk: [[0, 360], [0, 232], [640, 232], [640, 360]], depth: [[232, 0.55], [360, 1.15]], @@ -480,7 +528,7 @@ export const story = { { id: 'giochi', name: 'giochi', box: [196, 120, 330, 100], at: [360, 250], face: 'up', actions: { - look: [{ say: 'Lo scivolo e le altalene. Sono troppo grande, ufficialmente. Ufficiosamente, un giro ci starebbe.' }], + look: [{ say: 'Lo scivolo e le altalene. Ho ventisette anni, ufficialmente. Ufficiosamente, un giro ci starebbe.' }], use: [{ say: 'Un giro sull\'altalena! ... No, c\'è troppa gente che mi conosce.' }], }, }, @@ -504,7 +552,7 @@ export const story = { // Via Calimara: dalla Piazza scende alla farmacia, in Via Provinciale. via_calimara: { name: 'Via Calimara', - map: [26, 80], + map: [36, 58], background: 'assets/scenes/via-calimara.png', walk: [[0, 360], [0, 318], [330, 206], [400, 206], [560, 300], [600, 360]], depth: [[206, 0.3], [360, 1.15]], @@ -523,17 +571,17 @@ export const story = { }, }, { - id: 'alla_provinciale', name: 'giù, verso la farmacia', box: [330, 176, 90, 40], at: [365, 210], exit: { scene: 'via_provinciale', at: [540, 345], face: 'up' }, + id: 'alla_piazza', name: 'in fondo, la Piazza', box: [330, 176, 90, 40], at: [365, 210], exit: { scene: 'piazza', at: [360, 350], face: 'up' }, }, { - id: 'alla_piazza', name: 'su, verso la Piazza', box: [0, 330, 640, 30], at: [300, 354], exit: { scene: 'piazza', at: [360, 350], face: 'up' }, + id: 'alla_provinciale', name: 'verso la farmacia', box: [0, 330, 640, 30], at: [300, 354], exit: { scene: 'via_provinciale', at: [600, 330], face: 'left' }, }, ], }, // Via Provinciale alla rotonda: il River e la farmacia. Andando avanti, la Pieve. via_provinciale: { name: 'Via Provinciale', - map: [36, 84], + map: [40, 71], background: 'assets/scenes/via-provinciale.png', walk: [[0, 360], [0, 312], [300, 302], [440, 262], [470, 238], [640, 238], [640, 360]], depth: [[238, 0.45], [360, 1.15]], @@ -554,26 +602,26 @@ export const story = { }, }, { - id: 'area_feste', name: 'a sinistra, l\'area feste', box: [0, 250, 60, 110], at: [12, 330], exit: { scene: 'area_feste', at: [300, 350], face: 'up' }, + id: 'al_ponte', name: 'a sinistra, verso il campo sportivo', box: [0, 230, 60, 130], at: [12, 320], exit: { scene: 'ponte_sieve', at: [300, 350], face: 'up' }, }, { - id: 'al_ponte', name: 'a diritto, verso il campo sportivo', box: [560, 222, 80, 46], at: [600, 248], exit: { scene: 'ponte_sieve', at: [300, 350], face: 'up' }, + id: 'alla_pieve', name: 'dietro la farmacia, verso la Pieve', box: [470, 180, 120, 60], at: [560, 240], exit: { scene: 'pieve', at: [130, 345], face: 'right' }, }, { - id: 'alla_pieve', name: 'verso la Pieve', box: [0, 330, 213, 30], at: [100, 352], exit: { scene: 'pieve', at: [130, 345], face: 'right' }, + id: 'area_feste', name: 'verso l\'area feste', box: [60, 330, 440, 30], at: [260, 352], exit: { scene: 'area_feste', at: [300, 350], face: 'up' }, }, { - id: 'al_comune', name: 'di fronte alla farmacia, verso il Comune', box: [213, 330, 214, 30], at: [320, 352], exit: { scene: 'comune', at: [320, 350], face: 'up' }, + id: 'al_comune', name: 'verso il Comune', box: [592, 140, 48, 96], at: [622, 244], exit: { scene: 'comune', at: [320, 350], face: 'up' }, }, { - id: 'alla_calimara', name: 'Via Calimara, verso la Piazza', box: [427, 330, 213, 30], at: [540, 352], exit: { scene: 'via_calimara', at: [365, 210], face: 'down' }, + id: 'alla_calimara', name: 'Via Calimara, verso la Piazza', box: [520, 260, 120, 100], at: [600, 330], exit: { scene: 'via_calimara', at: [300, 350], face: 'up' }, }, ], }, // L'area feste (il parco Antonio Berti): dalla rotonda del River, a sinistra. area_feste: { name: 'Area feste', - map: [40, 99], + map: [30, 84], background: 'assets/scenes/area-feste.png', walk: [[0, 360], [0, 250], [260, 190], [420, 190], [640, 240], [640, 360]], depth: [[190, 0.35], [360, 1.15]], @@ -593,14 +641,17 @@ export const story = { }, }, { - id: 'alla_rotonda', name: 'indietro, verso la rotonda', box: [0, 330, 640, 30], at: [300, 354], exit: { scene: 'via_provinciale', at: [12, 330], face: 'right' }, + id: 'alla_polisportiva', name: 'in fondo, verso il campo sportivo', box: [360, 168, 110, 34], at: [400, 198], exit: { scene: 'polisportiva', at: [320, 265], face: 'down' }, + }, + { + id: 'alla_rotonda', name: 'indietro, verso la rotonda', box: [0, 330, 640, 30], at: [300, 354], exit: { scene: 'via_provinciale', at: [260, 345], face: 'up' }, }, ], }, // Il ponte sulla Sieve, proseguendo la Provinciale dalla farmacia; di là dal ponte, la polisportiva. ponte_sieve: { name: 'Ponte sulla Sieve', - map: [30, 94], + map: [48, 84], background: 'assets/scenes/ponte-sieve.png', walk: [[0, 360], [0, 252], [180, 226], [300, 202], [342, 202], [380, 214], [330, 238], [330, 300], [460, 330], [640, 318], [640, 360]], depth: [[202, 0.4], [360, 1.15]], @@ -622,14 +673,14 @@ export const story = { id: 'alla_polisportiva', name: 'di là dal ponte, il campo sportivo', box: [250, 180, 110, 36], at: [320, 206], exit: { scene: 'polisportiva_principale', at: [40, 330], face: 'right' }, }, { - id: 'alla_provinciale', name: 'indietro, verso la farmacia', box: [0, 330, 640, 30], at: [300, 354], exit: { scene: 'via_provinciale', at: [600, 248], face: 'left' }, + id: 'alla_provinciale', name: 'indietro, verso la farmacia', box: [0, 330, 640, 30], at: [300, 354], exit: { scene: 'via_provinciale', at: [12, 320], face: 'right' }, }, ], }, // La polisportiva: passato il ponte, prima il campo principale con la tribuna; proseguendo il secondario, coi campi da padel e tennis. polisportiva: { - name: 'Polisportiva', - map: [12, 99], + name: 'Campo secondario', + map: [62, 96], background: 'assets/scenes/polisportiva-secondario.png', walk: [[0, 360], [0, 330], [300, 262], [430, 222], [600, 210], [640, 212], [640, 360]], depth: [[210, 0.4], [360, 1.15]], @@ -648,12 +699,22 @@ export const story = { }, }, { - id: 'padel', name: 'campi da padel e da tennis', box: [520, 190, 120, 40], at: [600, 214], face: 'right', default: 'walk', + id: 'tennis', name: 'campi da tennis', box: [520, 190, 120, 40], at: [600, 214], face: 'right', default: 'walk', actions: { - walk: [{ say: 'Più avanti ci sono i campi da tennis e da padel.' }, { say: 'Il padel, nel 2007, non sapevo nemmeno cosa fosse. Lo scopriranno i miei genitori, tra qualche anno.' }], - look: [{ say: 'Laggiù i campi da tennis e da padel. Racchette, palline e gente che si lamenta del sole.' }], + walk: [{ say: 'Più avanti ci sono i campi da tennis. Racchette, palline e gente che si lamenta del sole.' }], + look: [{ say: 'Laggiù i campi da tennis.' }], }, }, + { + id: 'padel', name: 'capannone del padel', box: [0, 110, 90, 180], at: [40, 345], face: 'left', + actions: { + look: [{ say: 'Il capannone verde del padel.' }, { say: 'Il padel, nel 2007, non so nemmeno cosa sia. Tra qualche anno ci giocheranno tutti, me compreso.' }], + open: [{ say: 'Chiuso. E poi non saprei nemmeno da che parte si tiene la racchetta.' }], + }, + }, + { + id: 'all_area_feste', name: 'dietro il campo, verso l\'area feste', box: [150, 130, 190, 50], at: [320, 265], exit: { scene: 'area_feste', at: [400, 198], face: 'down' }, + }, { id: 'al_principale', name: 'indietro, verso il campo principale', box: [0, 330, 640, 30], at: [100, 354], exit: { scene: 'polisportiva_principale', at: [620, 330], face: 'left' }, }, @@ -661,7 +722,7 @@ export const story = { }, polisportiva_principale: { name: 'Campo principale', - map: [22, 99], + map: [40, 96], background: 'assets/scenes/polisportiva-principale.png', walk: [[0, 298], [640, 298], [640, 360], [0, 360]], depth: [[298, 1.0], [360, 1.2]], @@ -688,10 +749,67 @@ export const story = { }, ], }, + // Via Provinciale, a destra dalla Pieve: il forno; poi a sinistra la stazione, avanti «alla Luna» e Campomigliaio. + forno: { + name: 'Il forno', + map: [86, 58], + background: 'assets/scenes/forno-conti.png', + walk: [[0, 360], [0, 268], [640, 268], [640, 360]], + depth: [[268, 0.8], [360, 1.15]], + entry: [40, 330], + hotspots: [ + { + id: 'forno', name: 'forno', box: [100, 40, 420, 220], at: [320, 290], face: 'up', + actions: { + look: [{ say: 'Il forno. Il profumo del pane arriva fino alla Pieve, e la domenica mattina fino a casa mia.' }], + open: [{ say: 'Una schiacciata calda... no, prima il palio. Però l\'odore me lo porto dietro.' }], + }, + }, + { + id: 'alla_stazione', name: 'a sinistra, la stazione', box: [0, 220, 40, 110], at: [16, 300], exit: { scene: 'stazione', at: [320, 340], face: 'up' }, + }, + { + id: 'alla_luna', name: 'avanti, verso «alla Luna» e Campomigliaio', box: [600, 230, 40, 130], at: [630, 330], face: 'right', default: 'walk', + actions: { + walk: [{ say: 'Più avanti c\'è «alla Luna», e poi Campomigliaio, il mio rione: i blu.' }, { say: 'Ci torno quando ho ritrovato il palio. A mani vuote non mi ci faccio vedere.' }], + look: [{ say: 'La Provinciale va avanti verso «alla Luna» e Campomigliaio.' }], + }, + }, + { + id: 'alla_pieve', name: 'indietro, verso la Pieve', box: [60, 334, 520, 26], at: [320, 354], exit: { scene: 'pieve', at: [588, 250], face: 'left' }, + }, + ], + }, + stazione: { + name: 'La stazione', + map: [88, 71], + background: 'assets/scenes/stazione.png', + walk: [[0, 360], [0, 270], [640, 270], [640, 360]], + depth: [[270, 0.75], [360, 1.15]], + entry: [320, 340], + hotspots: [ + { + id: 'stazione', name: 'stazione', box: [200, 30, 440, 200], at: [400, 290], face: 'up', + actions: { + look: [{ say: 'La stazione della Faentina. Da qui i treni vanno a Firenze e a Faenza, con calma.' }, { say: 'Molta calma.' }], + }, + }, + { + id: 'bar', name: 'bar della stazione', box: [360, 196, 50, 66], at: [385, 290], face: 'up', + actions: { + look: [{ say: 'Il bar della stazione. Qui si aspetta il treno, e quando il treno è in ritardo si aspetta un altro caffè.' }], + open: [{ say: 'Non ho tempo per un caffè. E poi il caffè mi fa tremare come la carretta del Palio.' }], + }, + }, + { + id: 'al_forno', name: 'indietro, verso il forno', box: [0, 330, 640, 30], at: [320, 354], exit: { scene: 'forno', at: [16, 320], face: 'right' }, + }, + ], + }, // Livello 1: Villa Adami, vista dalla salita della Fortezza guardando in discesa. villa_adami: { name: 'Villa Adami', - map: [34, 48], + map: [46, 32], background: 'assets/scenes/villa-adami.png', walk: [[0, 272], [30, 246], [60, 244], [110, 256], [190, 268], [360, 296], [440, 288], [520, 276], [560, 268], [612, 262], [614, 272], [520, 288], [500, 292], [500, 360], [0, 360]], depth: [[244, 0.5], [360, 1.2]], @@ -718,7 +836,7 @@ export const story = { // Tra il livello 1 e il 2: la salita di Villa Adami, con la casa del leone. Si sale alla Fortezza. salita_villa_adami: { name: 'Salita di Villa Adami', - map: [30, 32], + map: [38, 19], background: 'assets/scenes/salita-villa-adami.png', walk: [[0, 318], [100, 312], [282, 318], [300, 300], [318, 262], [336, 252], [420, 266], [640, 296], [640, 360], [0, 360]], depth: [[252, 0.5], [360, 1.1]], @@ -736,7 +854,7 @@ export const story = { { id: 'verso_scuola', name: 'strada verso la scuola', box: [0, 286, 120, 44], at: [40, 336], face: 'left', exit: { scene: 'scuola', at: [300, 350], face: 'up' }, actions: { - look: [{ say: 'Di là si va alla scuola. Ci passo già abbastanza tempo durante l\'anno.' }], + look: [{ say: 'Di là si va alla scuola. Ci ho passato abbastanza anni, grazie.' }], }, }, { @@ -750,7 +868,7 @@ export const story = { // La strada da Pieve a Cardetole: il posticino della Carza, il ponticino, l'area camper. carza: { name: 'Il posticino della Carza', - map: [54, 80], + map: [60, 58], background: 'assets/scenes/carza.png', walk: [[60, 360], [250, 262], [300, 218], [350, 218], [420, 262], [600, 360]], depth: [[218, 0.4], [360, 1.15]], @@ -779,7 +897,7 @@ export const story = { }, ponticino: { name: 'Il ponticino sulla Carza', - map: [62, 86], + map: [64, 71], background: 'assets/scenes/ponticino-carza.png', walk: [[250, 360], [438, 198], [478, 198], [560, 360]], depth: [[198, 0.3], [360, 1.15]], @@ -808,7 +926,7 @@ export const story = { }, area_camper: { name: 'Area camper', - map: [70, 92], + map: [70, 84], background: 'assets/scenes/area-camper.png', walk: [[0, 360], [0, 260], [200, 232], [380, 240], [450, 248], [640, 300], [640, 360]], depth: [[232, 0.5], [360, 1.15]], @@ -840,7 +958,7 @@ export const story = { // Il prato del parco dei camper visto dall'altra parte: per i ragazzi, «il campo del Giannini». campo_giannini: { name: 'Il campo del Giannini', - map: [66, 98], + map: [82, 96], background: 'assets/scenes/campo-giannini.png', walk: [[0, 360], [0, 214], [300, 208], [460, 210], [640, 220], [640, 360]], depth: [[208, 0.4], [360, 1.15]], @@ -867,7 +985,7 @@ export const story = { // Cardetole, il rione dei verdi, dove abita Andrea: giù dalla Pieve. cardetole: { name: 'Cardetole', - map: [78, 96], + map: [90, 84], background: 'assets/scenes/cardetole.png', walk: [[0, 360], [0, 344], [120, 332], [200, 304], [262, 292], [330, 292], [372, 296], [640, 266], [640, 360]], depth: [[266, 0.55], [360, 1.15]], @@ -885,7 +1003,7 @@ export const story = { id: 'platano', name: 'platano', box: [90, 0, 170, 216], at: [200, 306], face: 'up', actions: { look: [{ say: 'Il prato sotto casa. Qui le porte le facevamo con le maglie per terra: due maglie, un palo ciascuna.' }, { say: 'E ogni gol era «Era fuori!», «No, era dentro!».' }], - use: [{ say: 'Arrampicarmi? Mia mamma mi vede dalla finestra.' }], + use: [{ say: 'Arrampicarmi? Alla mia età, e davanti a tutto il condominio?' }], }, }, { @@ -896,7 +1014,7 @@ export const story = { // L'ingresso della scuola, giù per la strada dalla salita di Villa Adami: dal cancello alle porte a vetri. scuola: { name: 'La scuola', - map: [8, 32], + map: [14, 19], background: 'assets/scenes/scuola.png', walk: [[110, 360], [118, 226], [140, 222], [268, 220], [290, 232], [520, 360]], depth: [[220, 0.42], [360, 1.32]], @@ -905,22 +1023,22 @@ export const story = { { id: 'pino', name: 'pino', box: [222, 40, 120, 70], at: [250, 232], face: 'up', actions: { - look: [{ say: 'Un pino a ombrello enorme. Alla ricreazione, tutti all\'ombra lì sotto.' }], + look: [{ say: 'Un pino a ombrello enorme. Alla ricreazione, tutti all\'ombra lì sotto. Sembra ieri.' }], }, }, { id: 'bandiere', name: 'bandiere', box: [110, 110, 170, 52], at: [200, 230], face: 'up', actions: { look: [{ say: 'Il tricolore e la bandiera europea. Almeno loro a scuola ci stanno volentieri.' }], - take: [{ say: 'Rubare una bandiera davanti alla scuola? Poi chi la sente la preside.' }], + take: [{ say: 'Rubare una bandiera davanti alla scuola? Ci manca solo che mi richiamino in presidenza, a ventisette anni.' }], }, }, { id: 'porte', name: 'porte della scuola', box: [130, 164, 136, 56], at: [200, 230], face: 'up', actions: { look: [{ say: 'Le porte a vetri della scuola. Di là ci sono i banchi, la lavagna e la campanella.' }], - open: [{ say: 'A scuola? A maggio? Nemmeno per sogno.' }], - walk: [{ say: 'A scuola? A maggio? Nemmeno per sogno.' }], + open: [{ say: 'Tornare a scuola? A ventisette anni? Nemmeno per sogno.' }], + walk: [{ say: 'Tornare a scuola? A ventisette anni? Nemmeno per sogno.' }], }, }, { @@ -937,7 +1055,7 @@ export const story = { // Il viale dei cipressi, la strada bianca tra la Fortezza e il Trebbio: in fondo c'è il castello. viale_cipressi: { name: 'Viale dei cipressi', - map: [55, 15], + map: [60, 6], background: 'assets/scenes/viale-cipressi.png', walk: [[30, 360], [200, 300], [288, 252], [312, 250], [420, 300], [600, 360]], depth: [[250, 0.16], [360, 1.2]], @@ -960,7 +1078,7 @@ export const story = { }, trebbio: { name: 'Castello del Trebbio', - map: [80, 15], + map: [84, 6], background: 'assets/scenes/trebbio.png', music: 'assets/audio/ricordi.mp3', walk: [[86, 308], [300, 306], [356, 308], [470, 312], [500, 324], [486, 360], [88, 360], [80, 330]], @@ -981,7 +1099,7 @@ export const story = { id: 'pergolato', name: 'pergolato', box: [278, 222, 122, 52], at: [328, 312], face: 'up', actions: { look: [{ say: 'Il pergolato, coi suoi pilastri. Un giorno li conto tutti.' }], - take: [{ say: 'Una vite? Ci vuole la vendemmia, non un ragazzo.' }], + take: [{ say: 'Una vite? Ci vuole la vendemmia, non uno con le mani in tasca.' }], }, }, { diff --git a/style.css b/style.css index bbbabf3..96bf3f4 100644 --- a/style.css +++ b/style.css @@ -113,10 +113,11 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h #map-board button:hover { background: var(--accent); } #map-board button.here { color: var(--ink); background: #2f5fb3; cursor: default; } #map-board button.here::after { content: ' · sei qui'; font-weight: normal; } -#credits { z-index: 3; justify-content: flex-start; overflow-y: auto; padding-top: 4%; background: rgba(15, 10, 20, .96); } +/* I crediti crescono con chi contribuisce: scorre solo l'elenco, dedica e Chiudi restano sempre in vista. */ +#credits { z-index: 3; justify-content: flex-start; overflow: hidden; padding-top: 4%; background: rgba(15, 10, 20, .96); } #credits .credits-dedication { margin: .2em 0 .5em !important; font-size: clamp(16px, 2.6vw, 28px); } /* Due colonne: il ruolo a destra della sua colonna, il nome a sinistra della sua, allineati sulla riga. */ -#credits dl { margin: .2em 0 0; display: grid; grid-template-columns: 1fr 1fr; column-gap: 1em; row-gap: .45em; align-items: baseline; width: min(100%, 44em); } +#credits dl { flex: 1 1 auto; min-height: 0; overflow-y: auto; align-content: start; padding-right: .4em; margin: .2em 0 0; display: grid; grid-template-columns: 1fr 1fr; column-gap: 1em; row-gap: .45em; align-items: baseline; width: min(100%, 44em); } #credits dt { text-align: right; color: var(--dim); font-size: clamp(10px, 1.4vw, 14px); letter-spacing: .12em; text-transform: uppercase; } #credits dd { margin: 0; text-align: left; color: var(--ink); font-size: clamp(13px, 2vw, 20px); font-weight: bold; } #credits .legal { color: var(--dim); font-size: clamp(10px, 1.2vw, 13px); opacity: .8; } diff --git a/tests/browser.mjs b/tests/browser.mjs index fe0b310..659eb87 100644 --- a/tests/browser.mjs +++ b/tests/browser.mjs @@ -64,8 +64,9 @@ try { assert.match(await page.textContent('#credits'), /Yuri Baldi/); assert.match(await page.textContent('#credits'), /Francesco Bacci/); await page.screenshot({ path: `${OUT}/01c-crediti.png` }); - const fitsCredits = await page.evaluate(() => { const c = document.getElementById('credits'); return c.scrollHeight <= c.clientHeight; }); - assert.ok(fitsCredits, 'i crediti stanno in una schermata'); + // L'elenco può scorrere; la dedica e il pulsante Chiudi devono restare dentro lo schermo. + const fitsCredits = await page.evaluate(() => { const c = document.getElementById('credits').getBoundingClientRect(), b = document.getElementById('close-credits').getBoundingClientRect(); return b.bottom <= c.bottom && b.top >= c.top; }); + assert.ok(fitsCredits, 'il pulsante Chiudi dei crediti è in vista'); await page.keyboard.press('Escape'); assert.ok(await page.isHidden('#credits'), 'Esc chiude i crediti'); await page.click('#new-game'); @@ -84,6 +85,11 @@ try { assert.ok(await fits(page), 'verbi e inventario fuori dallo schermo'); await settle(page); + // Doppio clic su un punto della scena: Andrea ci va di corsa. + const r = await page.locator('#scene').boundingBox(); + await page.mouse.dblclick(r.x + 300 / 640 * r.width, r.y + 330 / 360 * r.height); + assert.ok(await page.evaluate(() => game.hurry), 'il doppio clic fa correre'); + await settle(page); // Pausa: Esc o il pulsante fermano il gioco; Riprendi torna dov'eri; il titolo offre «Continua». await page.keyboard.press('Escape'); await page.waitForSelector('#pause:not([hidden])'); diff --git a/tests/logic.test.mjs b/tests/logic.test.mjs index c430ae3..e1b23bd 100644 --- a/tests/logic.test.mjs +++ b/tests/logic.test.mjs @@ -182,3 +182,16 @@ test('i personaggi della scena: posa da fermi, battito di ciglia, parlato e posa g.speech.pose = 'point'; assert.equal(g.personPose(p), 'point'); }); + +test('col doppio clic si cammina al doppio della velocità', () => { + const far = hurry => { + const g = new Game(mini()); + g.go('a', [10, 50]); + g.hurry = hurry; + g.walkTo([190, 50]); + g.update(0.5); + return g.state.x - 10; + }; + const slow = far(false), fast = far(true); + assert.ok(slow > 0 && Math.abs(fast - 2 * slow) < 1, `normale ${slow}, doppio ${fast}`); +}); diff --git a/tests/music.test.mjs b/tests/music.test.mjs index 938afa6..f0420b9 100644 --- a/tests/music.test.mjs +++ b/tests/music.test.mjs @@ -42,3 +42,19 @@ test('mentre parla qualcuno la musica si abbassa, poi risale', () => { for (let i = 0; i < 40; i++) m.update('g.mp3', 0.1); assert.equal(m.current.audio.volume, 0.6); }); + +test('in pausa la musica sfuma fino a zero e si ferma', () => { + const played = [], paused = []; + globalThis.Audio = class { constructor(src) { this.src = src; this.volume = 0; this.paused = true; } + play() { this.paused = false; played.push(this.src); return Promise.resolve(); } pause() { this.paused = true; paused.push(this.src); } }; + const m = new Music({ volume: 0.5 }); + for (let i = 0; i < 40; i++) m.update('a.mp3', 0.1); + assert.equal(m.current.audio.volume, 0.5); + m.hold = true; + for (let i = 0; i < 40; i++) m.update('a.mp3', 0.1); + assert.ok(m.current.audio.paused && m.current.audio.volume === 0, 'ferma a volume zero'); + m.hold = false; + m.update('a.mp3', 0.1); + assert.ok(!m.current.audio.paused && played.length === 2, 'riprende lo stesso brano'); + delete globalThis.Audio; +});