diff --git a/assets/voices/andrea-0f5f2a4b2f.mp3 b/assets/voices/andrea-0f5f2a4b2f.mp3 new file mode 100644 index 0000000..ef762e0 Binary files /dev/null and b/assets/voices/andrea-0f5f2a4b2f.mp3 differ diff --git a/assets/voices/andrea-0ffcdd4977.mp3 b/assets/voices/andrea-0ffcdd4977.mp3 new file mode 100644 index 0000000..486fbc2 Binary files /dev/null and b/assets/voices/andrea-0ffcdd4977.mp3 differ diff --git a/assets/voices/andrea-1615291724.mp3 b/assets/voices/andrea-1615291724.mp3 new file mode 100644 index 0000000..c5739a9 Binary files /dev/null and b/assets/voices/andrea-1615291724.mp3 differ diff --git a/assets/voices/andrea-17b8f09af1.mp3 b/assets/voices/andrea-17b8f09af1.mp3 deleted file mode 100644 index da72da6..0000000 Binary files a/assets/voices/andrea-17b8f09af1.mp3 and /dev/null differ diff --git a/assets/voices/andrea-1b2601be96.mp3 b/assets/voices/andrea-1b2601be96.mp3 new file mode 100644 index 0000000..da42386 Binary files /dev/null and b/assets/voices/andrea-1b2601be96.mp3 differ diff --git a/assets/voices/andrea-2421a56a0e.mp3 b/assets/voices/andrea-2421a56a0e.mp3 new file mode 100644 index 0000000..35a9733 Binary files /dev/null and b/assets/voices/andrea-2421a56a0e.mp3 differ diff --git a/assets/voices/andrea-302d8996ec.mp3 b/assets/voices/andrea-302d8996ec.mp3 new file mode 100644 index 0000000..5bb4b8c Binary files /dev/null and b/assets/voices/andrea-302d8996ec.mp3 differ diff --git a/assets/voices/andrea-3381d6ce08.mp3 b/assets/voices/andrea-3381d6ce08.mp3 deleted file mode 100644 index b908276..0000000 Binary files a/assets/voices/andrea-3381d6ce08.mp3 and /dev/null differ diff --git a/assets/voices/andrea-464f75cf78.mp3 b/assets/voices/andrea-464f75cf78.mp3 new file mode 100644 index 0000000..0f78a8f Binary files /dev/null and b/assets/voices/andrea-464f75cf78.mp3 differ diff --git a/assets/voices/andrea-51800429c8.mp3 b/assets/voices/andrea-51800429c8.mp3 new file mode 100644 index 0000000..32e7d05 Binary files /dev/null and b/assets/voices/andrea-51800429c8.mp3 differ diff --git a/assets/voices/andrea-51ec04c16e.mp3 b/assets/voices/andrea-51ec04c16e.mp3 new file mode 100644 index 0000000..f0b4143 Binary files /dev/null and b/assets/voices/andrea-51ec04c16e.mp3 differ diff --git a/assets/voices/andrea-59a49e494b.mp3 b/assets/voices/andrea-59a49e494b.mp3 new file mode 100644 index 0000000..bad844f Binary files /dev/null and b/assets/voices/andrea-59a49e494b.mp3 differ diff --git a/assets/voices/andrea-5b0cf4f9cf.mp3 b/assets/voices/andrea-5b0cf4f9cf.mp3 new file mode 100644 index 0000000..dc3db6e Binary files /dev/null and b/assets/voices/andrea-5b0cf4f9cf.mp3 differ diff --git a/assets/voices/andrea-5cb4d0e1ba.mp3 b/assets/voices/andrea-5cb4d0e1ba.mp3 new file mode 100644 index 0000000..6397840 Binary files /dev/null and b/assets/voices/andrea-5cb4d0e1ba.mp3 differ diff --git a/assets/voices/andrea-6484641c58.mp3 b/assets/voices/andrea-6484641c58.mp3 new file mode 100644 index 0000000..778e997 Binary files /dev/null and b/assets/voices/andrea-6484641c58.mp3 differ diff --git a/assets/voices/andrea-68a845881b.mp3 b/assets/voices/andrea-68a845881b.mp3 new file mode 100644 index 0000000..6c38226 Binary files /dev/null and b/assets/voices/andrea-68a845881b.mp3 differ diff --git a/assets/voices/andrea-6b299cebd9.mp3 b/assets/voices/andrea-6b299cebd9.mp3 new file mode 100644 index 0000000..82a54a3 Binary files /dev/null and b/assets/voices/andrea-6b299cebd9.mp3 differ diff --git a/assets/voices/andrea-6cc844498a.mp3 b/assets/voices/andrea-6cc844498a.mp3 new file mode 100644 index 0000000..45b02f9 Binary files /dev/null and b/assets/voices/andrea-6cc844498a.mp3 differ diff --git a/assets/voices/andrea-84a2614ea0.mp3 b/assets/voices/andrea-84a2614ea0.mp3 new file mode 100644 index 0000000..5c82c98 Binary files /dev/null and b/assets/voices/andrea-84a2614ea0.mp3 differ diff --git a/assets/voices/andrea-904fed8f4d.mp3 b/assets/voices/andrea-904fed8f4d.mp3 new file mode 100644 index 0000000..314867a Binary files /dev/null and b/assets/voices/andrea-904fed8f4d.mp3 differ diff --git a/assets/voices/andrea-964b1dd57e.mp3 b/assets/voices/andrea-964b1dd57e.mp3 new file mode 100644 index 0000000..2da1477 Binary files /dev/null and b/assets/voices/andrea-964b1dd57e.mp3 differ diff --git a/assets/voices/andrea-9c66488e9d.mp3 b/assets/voices/andrea-9c66488e9d.mp3 new file mode 100644 index 0000000..88a6d5e Binary files /dev/null and b/assets/voices/andrea-9c66488e9d.mp3 differ diff --git a/assets/voices/andrea-aa9b313a43.mp3 b/assets/voices/andrea-aa9b313a43.mp3 new file mode 100644 index 0000000..7b06c8c Binary files /dev/null and b/assets/voices/andrea-aa9b313a43.mp3 differ diff --git a/assets/voices/andrea-bf55ca1485.mp3 b/assets/voices/andrea-bf55ca1485.mp3 new file mode 100644 index 0000000..0ea245e Binary files /dev/null and b/assets/voices/andrea-bf55ca1485.mp3 differ diff --git a/assets/voices/andrea-cea6462caa.mp3 b/assets/voices/andrea-cea6462caa.mp3 deleted file mode 100644 index 2b4e3fd..0000000 Binary files a/assets/voices/andrea-cea6462caa.mp3 and /dev/null differ diff --git a/assets/voices/andrea-dc84344445.mp3 b/assets/voices/andrea-dc84344445.mp3 new file mode 100644 index 0000000..b2a8408 Binary files /dev/null and b/assets/voices/andrea-dc84344445.mp3 differ diff --git a/assets/voices/andrea-f34cc00733.mp3 b/assets/voices/andrea-f34cc00733.mp3 new file mode 100644 index 0000000..88db767 Binary files /dev/null and b/assets/voices/andrea-f34cc00733.mp3 differ diff --git a/assets/voices/castello-0aead5ed46.mp3 b/assets/voices/castello-0aead5ed46.mp3 new file mode 100644 index 0000000..0710763 Binary files /dev/null and b/assets/voices/castello-0aead5ed46.mp3 differ diff --git a/assets/voices/castello-292fa417cf.mp3 b/assets/voices/castello-292fa417cf.mp3 new file mode 100644 index 0000000..01b5061 Binary files /dev/null and b/assets/voices/castello-292fa417cf.mp3 differ diff --git a/assets/voices/castello-4c2e50d84e.mp3 b/assets/voices/castello-4c2e50d84e.mp3 new file mode 100644 index 0000000..304ce3c Binary files /dev/null and b/assets/voices/castello-4c2e50d84e.mp3 differ diff --git a/assets/voices/castello-538be3b114.mp3 b/assets/voices/castello-538be3b114.mp3 new file mode 100644 index 0000000..7201f07 Binary files /dev/null and b/assets/voices/castello-538be3b114.mp3 differ diff --git a/assets/voices/castello-8f9b846004.mp3 b/assets/voices/castello-8f9b846004.mp3 new file mode 100644 index 0000000..af96598 Binary files /dev/null and b/assets/voices/castello-8f9b846004.mp3 differ diff --git a/assets/voices/castello-b87ef89eaf.mp3 b/assets/voices/castello-b87ef89eaf.mp3 new file mode 100644 index 0000000..2594c46 Binary files /dev/null and b/assets/voices/castello-b87ef89eaf.mp3 differ diff --git a/assets/voices/castello-f6d0f19ad6.mp3 b/assets/voices/castello-f6d0f19ad6.mp3 new file mode 100644 index 0000000..0fd1e90 Binary files /dev/null and b/assets/voices/castello-f6d0f19ad6.mp3 differ diff --git a/assets/voices/castello-fe24978204.mp3 b/assets/voices/castello-fe24978204.mp3 new file mode 100644 index 0000000..0b868d3 Binary files /dev/null and b/assets/voices/castello-fe24978204.mp3 differ diff --git a/assets/voices/index.json b/assets/voices/index.json index 8e60813..1e35292 100644 --- a/assets/voices/index.json +++ b/assets/voices/index.json @@ -59,9 +59,13 @@ "file": "andrea-a9df2e6bbb.mp3", "sec": 4.98 }, - "andrea|Il programma del Palio della Fortezza. Qualcuno ci ha scritto sopra col pennarello: «IL REGOLO SI È RIPRESO IL PALIO».": { - "file": "andrea-cea6462caa.mp3", - "sec": 7.61 + "andrea|Il programma del Palio della Fortezza, con la foto del palio: la strada bianca che sale alla Fortezza, i ragazzi in fila, e uno con la maglia rossa che alza le braccia.": { + "file": "andrea-6cc844498a.mp3", + "sec": 10.59 + }, + "andrea|Qualcuno ci ha scritto sopra col pennarello: «IL REGOLO SI È RIPRESO IL PALIO».": { + "file": "andrea-2421a56a0e.mp3", + "sec": 5.43 }, "andrea|Il programma lo so a memoria. Il problema è il palio.": { "file": "andrea-559fbc00c4.mp3", @@ -151,6 +155,10 @@ "file": "andrea-fd10e3f083.mp3", "sec": 3.04 }, + "andrea|Ormai lo so: è passato di qui. E le impronte piegano verso il bastione.": { + "file": "andrea-5b0cf4f9cf.mp3", + "sec": 5.03 + }, "andrea|Le metto vicine...": { "file": "andrea-73eec7b287.mp3", "sec": 0.91 @@ -159,9 +167,85 @@ "file": "andrea-847cbcd7f3.mp3", "sec": 5.66 }, - "andrea|Per stavolta finisce qui: il resto della storia lo dobbiamo ancora scrivere.": { - "file": "andrea-17b8f09af1.mp3", - "sec": 4.38 + "andrea|Ma la porta è incatenata da anni. E allora da dove è entrato?": { + "file": "andrea-964b1dd57e.mp3", + "sec": 4.24 + }, + "andrea|Le impronte non finiscono alla porta: piegano verso il bastione.": { + "file": "andrea-0ffcdd4977.mp3", + "sec": 4.07 + }, + "andrea|Ecco dove vanno le impronte: dentro questa fessura!": { + "file": "andrea-bf55ca1485.mp3", + "sec": 4.45 + }, + "andrea|Dentro è buio pesto. Senza luce non vedo niente.": { + "file": "andrea-51800429c8.mp3", + "sec": 3.87 + }, + "andrea|Una fessura alla base del bastione, larga quanto un gatto grasso.": { + "file": "andrea-464f75cf78.mp3", + "sec": 4.61 + }, + "andrea|Dentro è buio pesto. E ne esce un'aria fresca da cantina.": { + "file": "andrea-5cb4d0e1ba.mp3", + "sec": 4.25 + }, + "andrea|Infilarci la mano al buio? Neanche per sogno.": { + "file": "andrea-1615291724.mp3", + "sec": 4.67 + }, + "andrea|Una fessura non si prende. Al massimo ci si guarda dentro.": { + "file": "andrea-904fed8f4d.mp3", + "sec": 4.43 + }, + "andrea|È già aperta. Il problema è che è piccola.": { + "file": "andrea-6b299cebd9.mp3", + "sec": 3.75 + }, + "andrea|C'è nessuno?": { + "file": "andrea-9c66488e9d.mp3", + "sec": 0.79 + }, + "andrea|Solo l'eco. Spero.": { + "file": "andrea-aa9b313a43.mp3", + "sec": 2.22 + }, + "andrea|Accendo la torcia e la punto dentro...": { + "file": "andrea-68a845881b.mp3", + "sec": 1.94 + }, + "andrea|Laggiù, appoggiato al muro, c'è un angolo di tela dipinta: una strada bianca, e un omino con la maglia rossa e le braccia alzate.": { + "file": "andrea-6484641c58.mp3", + "sec": 8.67 + }, + "andrea|È il palio! Il quadro dei ragazzi delle medie!": { + "file": "andrea-51ec04c16e.mp3", + "sec": 3.54 + }, + "andrea|E quelli... sono due occhi gialli!": { + "file": "andrea-0f5f2a4b2f.mp3", + "sec": 2.63 + }, + "andrea|Spariti nel buio.": { + "file": "andrea-f34cc00733.mp3", + "sec": 1.17 + }, + "andrea|Il Regolo. E ha il nostro palio.": { + "file": "andrea-1b2601be96.mp3", + "sec": 2.77 + }, + "andrea|Domani c'è il Palio. Stanotte, in un modo o nell'altro, in quella Fortezza ci entro.": { + "file": "andrea-302d8996ec.mp3", + "sec": 6.95 + }, + "andrea|Accendo la torcia e guardo dentro... pietre, ragnatele, buio.": { + "file": "andrea-dc84344445.mp3", + "sec": 5.6 + }, + "andrea|Non so nemmeno cosa sto cercando. Prima devo capire chi è passato di qui.": { + "file": "andrea-59a49e494b.mp3", + "sec": 5.28 }, "andrea|La Pieve di San Pietro. Il rione Pieve, quelli in bianco.": { "file": "andrea-c41f9eaaf3.mp3", @@ -372,8 +456,8 @@ "sec": 4.69 }, "andrea|La panchina rossa. Qui si scambiavano le figurine: celo, celo, manca.": { - "file": "andrea-3381d6ce08.mp3", - "sec": 5.68 + "file": "andrea-84a2614ea0.mp3", + "sec": 5.7 }, "andrea|Laggiù le poste. Mia nonna ci andava a ritirare la pensione e tornava con tutte le notizie del paese.": { "file": "andrea-c716ab2381.mp3", @@ -1043,6 +1127,38 @@ "file": "castello-888f24e1fd.mp3", "sec": 8.81 }, + "castello|Hai la luce, e sotto la Fortezza c'è il buio. Mettili insieme.": { + "file": "castello-b87ef89eaf.mp3", + "sec": 6.25 + }, + "castello|Una chiavetta piccola apre una cosa piccola. All'area feste c'è una casetta che aspetta.": { + "file": "castello-f6d0f19ad6.mp3", + "sec": 5.95 + }, + "castello|Uno stecco che appiccica pesca dove le mani non arrivano. Al ponticino, sotto la griglia, qualcosa luccica.": { + "file": "castello-538be3b114.mp3", + "sec": 7 + }, + "castello|Lo stecco e la gomma, da soli, non servono a niente. Insieme, chissà.": { + "file": "castello-8f9b846004.mp3", + "sec": 6.88 + }, + "castello|Allo stecco serve qualcosa che appiccica. Guarda per terra all'oratorio: i ragazzi lasciano sempre qualcosa.": { + "file": "castello-0aead5ed46.mp3", + "sec": 8.15 + }, + "castello|Hai una chiave in tasca, e alla Pieve c'è una porta che la aspetta.": { + "file": "castello-292fa417cf.mp3", + "sec": 4.17 + }, + "castello|San Pietro tiene le chiavi, e un indovinello per chi le vuole. La risposta è scritta nelle mura della Fortezza.": { + "file": "castello-4c2e50d84e.mp3", + "sec": 7.75 + }, + "castello|Per il buio ci vuole una luce. Comincia dal fresco: la Tosca, al tennis della chiesa, ha un ghiacciolo che fa al caso tuo.": { + "file": "castello-fe24978204.mp3", + "sec": 9.4 + }, "castello|Io ho visto passare i Medici, la peste e Vespucci. Un serpente in più non mi spaventa.": { "file": "castello-a1dfd76497.mp3", "sec": 6.51 diff --git a/docs/STATO.md b/docs/STATO.md index d5531c8..f70e918 100644 --- a/docs/STATO.md +++ b/docs/STATO.md @@ -90,9 +90,13 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso - seconda passata di Codex: **Carica** controlla versione e contenuto (`checkSave`) prima di sostituire il salvataggio del browser; col dito gli **oggetti piccoli** (sotto i 24×24) vincono sui grandi che li contengono (la squama sulla panchina); uno **sfondo fallito** si riprova dopo 2, 4 e 8 secondi, poi basta. -- **Revisione di Codex, parte di storia (da decidere con Andrea)**: dare alla torcia un uso e una ricompensa, un obiettivo - riconoscibile (un taccuino?), indizi del Trebbio anche per gli enigmi nuovi; oggi la soluzione arriva alla fine in 10 passi - e lascia fuori indovinello, oratorio e catena del ghiacciolo. +- **Primo capitolo chiuso** (24 ottobre): squama e impronte portano a una fessura nel bastione; la torcia illumina un angolo + del palio (l'omino con la maglia rossa e le braccia alzate, dal quadro vero: foto in `assets/raw/palio-2007-foto.jpg`, già + descritto sul manifesto in piazza) e due occhi gialli che spariscono. La soluzione (40 passi, tragitti calcolati da + `route`) passa per indovinello, oratorio e catena del ghiacciolo; il Trebbio ha un indizio per ogni tappa della torcia. + Da fare: il disegno della fessura è provvisorio (forme in `art`); un taccuino con l'obiettivo corrente. +- **Pronuncia** (`tools/pronuncia.json`): le parole che XTTS legge male, scritte come vanno lette; vale solo per la voce. + Per ora «celo» → «ce l'ho». Le 32 voci nuove del 24 ottobre non sono state controllate con Whisper (qui non installato). - **Sessione del 23-24 ottobre**: il paese è quasi tutto in gioco (circa 40 scene, sfondi di Codex dalle foto di Andrea), collegato come quello vero; un test controlla che ogni strada vada e torni. Nuovi: carattere a pixel Press Start 2P, pulsante «?» per vedere cosa si tocca, mappa scorrevole sul telefono, titolo e introduzione a tutto @@ -112,8 +116,8 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso - Strada del campeggio (poi Bosco ai Frati) e strada della scuola: per ora battute, scene da fare con le foto di Andrea. ### Domande per Andrea -1. Cosa c'era dipinto sul palio del 2007? (Un'immagine nuova: quadro ingenuo dei ragazzi delle medie, senza - cornice, su sfondo verde.) +1. ~~Cosa c'era dipinto sul palio del 2007?~~ Risposto: foto in `assets/raw/palio-2007-foto.jpg` (la strada che sale alla + Fortezza con le bandiere, i ragazzi in fila, uno con la maglia rossa e le braccia alzate). 2. Il passamano dei mattoni: sul sentiero nel bosco o sulle mura della Fortezza? 3. Aggiungere i verbi **Spingi** e **Tira** (carretta, tiro alla fune)? 4. «La Colonna» era davvero il bar della piazza nel 2007? Chi c'era? diff --git a/index.html b/index.html index ed78e5f..27ff470 100644 --- a/index.html +++ b/index.html @@ -41,8 +41,8 @@ diff --git a/src/story/solution.js b/src/story/solution.js index 9a69a24..8432659 100644 --- a/src/story/solution.js +++ b/src/story/solution.js @@ -1,14 +1,53 @@ -// La soluzione completa, passo per passo: la percorre tests/story.test.mjs a ogni modifica della storia. -// Passi: [verbo, bersaglio, oggetto?] oppure ['choose', testo dell'opzione]. +// La soluzione completa, passo per passo: la percorre tests/story.test.mjs a ogni modifica della storia, +// e tests/browser.mjs con clic veri. Passi: [verbo, bersaglio, oggetto?] oppure ['choose', testo dell'opzione]. +// I tragitti fra le scene (`route`) seguono le uscite più brevi: cambiano da soli se cambia la mappa. +import { story } from './story.js'; + +// Le uscite che si prendono sempre col Vai a: senza condizioni e senza una battuta che le sostituisce. +const exits = id => (story.scenes[id].hotspots ?? []).filter(h => h.exit && !h.when && !h.actions?.walk); + +export function route(from, to) { + const back = new Map([[from, null]]), queue = [from]; + for (let i = 0; i < queue.length && !back.has(to); i++) + for (const h of exits(queue[i])) + if (!back.has(h.exit.scene)) { back.set(h.exit.scene, [queue[i], h.id]); queue.push(h.exit.scene); } + if (!back.has(to)) throw new Error(`nessuna strada da ${from} a ${to}`); + const steps = []; + for (let at = to; back.get(at); at = back.get(at)[0]) steps.unshift(['walk', back.get(at)[1]]); + return steps; +} + export const solution = [ + // La squama: i vecchietti raccontano del luccichio sotto la panchina. ['look', 'colonna'], ['talk', 'vecchietti'], ['choose', 'Avete notato niente di strano stanotte?'], ['choose', 'Ci vediamo dopo.'], ['take', 'cosa_luccicante'], - ['walk', 'verso_villa_adami'], - ['walk', 'su_villa_adami'], - ['walk', 'salita'], - ['walk', 'su_fortezza'], + // Le impronte sotto la Fortezza: il Regolo è passato di qui, e le impronte vanno verso il bastione. + ...route('piazza', 'fortezza'), ['use', 'impronte', 'squama'], + ['look', 'fessura'], + // La chiave dell'oratorio: l'indovinello di San Pietro (la risposta la dà il bastione). + ...route('fortezza', 'pieve'), + ['talk', 'statua'], + ['choose', 'Nove.'], + ['open', 'porta_oratorio'], + ['take', 'gomma_terra'], + // La Tosca: cedrata e ghiacciolo, e resta lo stecco; con la gomma diventa uno stecco che appiccica. + ...route('oratorio', 'tennis_chiesa'), + ['talk', 'chiosco'], + ['choose', 'Una cedrata, da portare via.'], + ['choose', 'Un ghiacciolo al limone.'], + ['choose', 'Niente, grazie Tosca.'], + ['use', 'cedrata', 'ghiacciolo'], + ['use', 'gomma', 'stecco'], + // La chiavetta sotto la griglia del ponticino, la casetta dell'area feste, la torcia. + ...route('tennis_chiesa', 'ponticino'), + ['use', 'griglia', 'stecco_gomma'], + ...route('ponticino', 'area_feste'), + ['open', 'casetta'], + // La torcia nella fessura: il palio, e due occhi gialli. Fine del primo capitolo. + ...route('area_feste', 'fortezza'), + ['use', 'fessura', 'torcia'], ]; diff --git a/src/story/story.js b/src/story/story.js index 91541e1..d4a71f4 100644 --- a/src/story/story.js +++ b/src/story/story.js @@ -2,6 +2,35 @@ // Testi, personaggi ed enigmi sono provvisori e verranno sostituiti con i ricordi di Andrea (docs/PROGETTO.md). // Le forme in `art` sono segnaposto disegnati sopra lo sfondo (vecchietti, squama, catena) in attesa degli sprite. +// Gli indizi del Trebbio per la catena della torcia (dopo le impronte, finché la torcia non c'è): uno per tappa, +// dalla più avanti alla prima. +const castello = say => [{ say, who: 'castello' }]; +const torchHints = [{ + if: 'has:chiavetta', + then: castello('Una chiavetta piccola apre una cosa piccola. All\'area feste c\'è una casetta che aspetta.'), + else: [{ + if: 'has:stecco_gomma', + then: castello('Uno stecco che appiccica pesca dove le mani non arrivano. Al ponticino, sotto la griglia, qualcosa luccica.'), + else: [{ + if: ['has:stecco', 'has:gomma'], + then: castello('Lo stecco e la gomma, da soli, non servono a niente. Insieme, chissà.'), + else: [{ + if: 'has:stecco', + then: [{ + if: 'oratorio_aperto', + then: castello('Allo stecco serve qualcosa che appiccica. Guarda per terra all\'oratorio: i ragazzi lasciano sempre qualcosa.'), + else: [{ + if: 'has:chiave_oratorio', + then: castello('Hai una chiave in tasca, e alla Pieve c\'è una porta che la aspetta.'), + else: castello('San Pietro tiene le chiavi, e un indovinello per chi le vuole. La risposta è scritta nelle mura della Fortezza.'), + }], + }], + else: castello('Per il buio ci vuole una luce. Comincia dal fresco: la Tosca, al tennis della chiesa, ha un ghiacciolo che fa al caso tuo.'), + }], + }], + }], +}]; + export const story = { title: 'La Leggenda del Regolo', // La mappa (pulsante «Mappa»): ogni scena con `map: [x, y]` (percentuali della pianta) ci compare quando è @@ -214,7 +243,10 @@ export const story = { id: 'manifesto', name: 'manifesto del Palio', box: [256, 196, 18, 24], at: [265, 250], face: 'up', art: [{ rect: [256, 196, 18, 24], fill: '#f4efe4' }, { rect: [258, 199, 14, 5], fill: '#b7342e' }, { rect: [258, 207, 12, 2], fill: '#3b3b44' }, { rect: [258, 211, 10, 2], fill: '#3b3b44' }, { rect: [258, 215, 12, 2], fill: '#2f5fb3' }], actions: { - look: [{ say: 'Il programma del Palio della Fortezza. Qualcuno ci ha scritto sopra col pennarello: «IL REGOLO SI È RIPRESO IL PALIO».' }], + look: [ + { say: 'Il programma del Palio della Fortezza, con la foto del palio: la strada bianca che sale alla Fortezza, i ragazzi in fila, e uno con la maglia rossa che alza le braccia.' }, + { say: 'Qualcuno ci ha scritto sopra col pennarello: «IL REGOLO SI È RIPRESO IL PALIO».' }, + ], take: [{ say: 'Il programma lo so a memoria. Il problema è il palio.' }], }, }, @@ -291,13 +323,67 @@ export const story = { take: [{ say: 'Le impronte non si prendono. Si seguono.' }], }, with: { - squama: [ - { say: 'Le metto vicine...' }, - { say: 'Stesso luccichio, stessa puzza di cantina. Il Regolo è passato di qui!' }, - { set: 'indizio_regolo' }, - { say: 'Per stavolta finisce qui: il resto della storia lo dobbiamo ancora scrivere.' }, - { end: true }, - ], + squama: [{ + if: 'indizio_regolo', + then: [{ say: 'Ormai lo so: è passato di qui. E le impronte piegano verso il bastione.' }], + else: [ + { say: 'Le metto vicine...' }, + { say: 'Stesso luccichio, stessa puzza di cantina. Il Regolo è passato di qui!' }, + { set: 'indizio_regolo' }, + { say: 'Ma la porta è incatenata da anni. E allora da dove è entrato?' }, + { say: 'Le impronte non finiscono alla porta: piegano verso il bastione.' }, + ], + }], + }, + }, + // Fine del primo capitolo: le impronte portano qui; con la torcia si vede il palio, e due occhi gialli. + // Il dettaglio che lo fa riconoscere è quello del quadro vero (foto di Andrea, assets/raw/palio-2007-foto.jpg), + // già visto sul manifesto in piazza: l'omino con la maglia rossa e le braccia alzate. + { + id: 'fessura', name: 'fessura nel bastione', box: [486, 266, 30, 38], at: [462, 310], face: 'right', + art: [ + { poly: [[490, 301], [493, 288], [497, 276], [502, 270], [507, 278], [510, 290], [512, 302]], fill: '#120c08' }, + { when: 'luce_fessura', poly: [[494, 299], [497, 286], [502, 278], [506, 286], [508, 299]], fill: 'rgba(255, 228, 140, 0.6)' }, + { when: 'luce_fessura', rect: [497, 291, 9, 8], fill: '#d9d2b0' }, + { when: 'luce_fessura', rect: [499, 293, 1, 3], fill: '#c0392b' }, + { when: 'luce_fessura', rect: [503, 294, 1, 3], fill: '#9bb04a' }, + { when: 'occhi_regolo', rect: [498, 282, 2, 2], fill: '#ffe14a' }, + { when: 'occhi_regolo', rect: [504, 282, 2, 2], fill: '#ffe14a' }, + ], + actions: { + look: [{ + if: 'indizio_regolo', + then: [{ say: 'Ecco dove vanno le impronte: dentro questa fessura!' }, { say: 'Dentro è buio pesto. Senza luce non vedo niente.' }], + else: [{ say: 'Una fessura alla base del bastione, larga quanto un gatto grasso.' }, { say: 'Dentro è buio pesto. E ne esce un\'aria fresca da cantina.' }], + }], + use: [{ say: 'Infilarci la mano al buio? Neanche per sogno.' }], + take: [{ say: 'Una fessura non si prende. Al massimo ci si guarda dentro.' }], + open: [{ say: 'È già aperta. Il problema è che è piccola.' }], + talk: [{ say: 'C\'è nessuno?' }, { say: '...' }, { say: 'Solo l\'eco. Spero.' }], + }, + with: { + torcia: [{ + if: 'indizio_regolo', + then: [ + { say: 'Accendo la torcia e la punto dentro...' }, + { set: 'luce_fessura' }, + { say: 'Laggiù, appoggiato al muro, c\'è un angolo di tela dipinta: una strada bianca, e un omino con la maglia rossa e le braccia alzate.' }, + { say: 'È il palio! Il quadro dei ragazzi delle medie!' }, + { set: 'occhi_regolo' }, + { say: 'E quelli... sono due occhi gialli!' }, + { unset: 'occhi_regolo' }, + { say: 'Spariti nel buio.' }, + { unset: 'luce_fessura' }, + { set: 'palio_trovato' }, + { say: 'Il Regolo. E ha il nostro palio.' }, + { say: 'Domani c\'è il Palio. Stanotte, in un modo o nell\'altro, in quella Fortezza ci entro.' }, + { end: true }, + ], + else: [ + { say: 'Accendo la torcia e guardo dentro... pietre, ragnatele, buio.' }, + { say: 'Non so nemmeno cosa sto cercando. Prima devo capire chi è passato di qui.' }, + ], + }], }, }, { @@ -1480,7 +1566,11 @@ export const story = { else: [{ if: ['!has:squama', '!squama_presa'], then: [{ say: 'Le cose che luccicano non stanno mai in alto. Guarda in basso, vicino a chi te ne ha parlato.', who: 'castello' }], - else: [{ say: 'Una squama da sola non dice niente. Ma vicino alle sue impronte, sotto la Fortezza, magari sì.', who: 'castello' }], + else: [{ + if: '!indizio_regolo', + then: [{ say: 'Una squama da sola non dice niente. Ma vicino alle sue impronte, sotto la Fortezza, magari sì.', who: 'castello' }], + else: [{ if: 'has:torcia', then: [{ say: 'Hai la luce, e sotto la Fortezza c\'è il buio. Mettili insieme.', who: 'castello' }], else: torchHints }], + }], }], }, ], diff --git a/tests/browser.mjs b/tests/browser.mjs index f321d53..86bd7c7 100644 --- a/tests/browser.mjs +++ b/tests/browser.mjs @@ -40,7 +40,11 @@ async function settle(page) { throw new Error('il gioco non si ferma'); } -const hotspotCenter = (page, id) => page.evaluate(id => { const h = game.hotspot(id); return [h.box[0] + h.box[2] / 2, h.box[1] + h.box[3] / 2]; }, id); +const hotspotCenter = async (page, id) => { + const c = await page.evaluate(id => { const h = game.hotspot(id); return h && [h.box[0] + h.box[2] / 2, h.box[1] + h.box[3] / 2]; }, id); + if (!c) throw new Error(`non c'è ${id} in ${await page.evaluate(() => game.state.scene)}`); + return c; +}; try { const page = await open({ width: 1280, height: 860 }); @@ -163,10 +167,17 @@ try { await page.click(`#verbs [data-verb="${verb}"]`); await page.click(`#inventory [data-item="${item}"]`); } else if (!smart) await page.click(`#verbs [data-verb="${verb}"]`); - await clickScene(page, await hotspotCenter(page, target)); + // Un oggetto dell'inventario su un altro (cedrata col ghiacciolo) si clicca nell'inventario, non nella scena. + if (await page.evaluate(t => !!game.story.items[t], target)) await page.click(`#inventory [data-item="${target}"]`); + else await clickScene(page, await hotspotCenter(page, target)); } await page.waitForTimeout(80); if (verb === 'talk') await page.screenshot({ path: `${OUT}/03-dialogo.png` }); + // La torcia nella fessura: il palio illuminato e i due occhi gialli, prima che spariscano. + if (target === 'fessura' && item) { + await page.waitForFunction(() => game.state.flags.occhi_regolo, null, { timeout: 30000 }); + await page.screenshot({ path: `${OUT}/05b-occhi.png` }); + } await settle(page); if (target === 'cosa_luccicante') await page.screenshot({ path: `${OUT}/04-squama.png` }); if (target === 'su_fortezza') await page.screenshot({ path: `${OUT}/05-fortezza.png` }); diff --git a/tests/story.test.mjs b/tests/story.test.mjs index 2fd66e0..198bc41 100644 --- a/tests/story.test.mjs +++ b/tests/story.test.mjs @@ -105,7 +105,7 @@ test('la soluzione si percorre tutta dall\'inizio alla fine', () => { settle(g); } assert.ok(g.state.ended, 'la partita non è finita'); - assert.ok(g.state.flags.indizio_regolo); + assert.ok(g.state.flags.indizio_regolo && g.state.flags.palio_trovato, 'il palio visto nella fessura'); assert.ok(saves.at(-1).ended, 'la fine non è salvata'); }); @@ -363,3 +363,23 @@ test('un salvataggio da file si accetta solo se torna in tutto con la storia', ( assert.ok(bad({ ended: 'no' })); assert.ok(checkSave(story, null)); }); + +test('il Trebbio ha un indizio per ogni tappa della torcia, e la torcia usata troppo presto non chiude niente', () => { + const hint = (inventory, flags) => { + const g = new Game(story, { state: { scene: 'trebbio', x: 330, y: 312, facing: 1, inventory, flags: { luccichio: true, squama_presa: true, indizio_regolo: true, ...flags }, used: {}, ended: false } }); + g.act('talk', 'torre'); settle(g); + g.choose(g.options().findIndex(o => o.text.startsWith('Sono bloccato'))); + return settle(g).map(s => s.text).at(-1); + }; + const stages = [ + hint([], {}), hint(['stecco'], {}), hint(['stecco', 'chiave_oratorio'], { chiave_data: true }), + hint(['stecco'], { chiave_data: true, oratorio_aperto: true }), hint(['stecco', 'gomma'], { oratorio_aperto: true }), + hint(['stecco_gomma'], {}), hint(['chiavetta'], {}), hint(['torcia'], { casetta_aperta: true }), + ]; + assert.equal(new Set(stages).size, stages.length, 'ogni tappa il suo indizio:\n' + stages.join('\n')); + assert.match(stages[0], /Tosca/); + assert.match(stages.at(-1), /luce/); + const early = new Game(story, { state: { scene: 'fortezza', x: 462, y: 310, facing: 1, inventory: ['torcia'], flags: {}, used: {}, ended: false } }); + early.act('use', 'fessura', 'torcia'); settle(early); + assert.ok(!early.state.ended && !early.state.flags.palio_trovato, 'senza le impronte la fessura è solo buia'); +}); diff --git a/tests/voices.test.mjs b/tests/voices.test.mjs index 066c15e..b280a24 100644 --- a/tests/voices.test.mjs +++ b/tests/voices.test.mjs @@ -95,3 +95,12 @@ test('la pausa ferma la voce e riprendendo continua; stop la toglie', () => { delete globalThis.Audio; } }); + +test('le parole che XTTS legge male si riscrivono solo per la voce, intere e con la maiuscola', async () => { + const { spoken, rules } = await import('../tools/pronuncia.mjs'); + assert.equal(spoken('Qui si scambiavano le figurine: celo, celo, manca.'), 'Qui si scambiavano le figurine: ce l\'ho, ce l\'ho, manca.'); + assert.equal(spoken('Celo!'), 'Ce l\'ho!'); + assert.equal(spoken('Guarda il cielo e il celofan.'), 'Guarda il cielo e il celofan.', 'non i pezzi di altre parole'); + assert.equal(spoken('Il Regolo', { regolo: 'Règolo' }), 'Il Règolo'); + for (const [w, s] of Object.entries(rules)) if (!w.startsWith('_')) assert.ok(typeof s === 'string' && s.trim(), `pronuncia vuota per ${w}`); +}); diff --git a/tools/pronuncia.json b/tools/pronuncia.json new file mode 100644 index 0000000..d816cc8 --- /dev/null +++ b/tools/pronuncia.json @@ -0,0 +1,4 @@ +{ + "_nota": "Parole che XTTS legge male in italiano: a sinistra come sono scritte nella storia (e nel fumetto), a destra come vanno lette dalla voce. Si cerca la parola intera, senza badare alle maiuscole. Cambiare una riga rigenera solo le battute che la contengono (make voci). Esempio: «celo» (ce l'ho, delle figurine) XTTS lo legge «cielo».", + "celo": "ce l'ho" +} diff --git a/tools/pronuncia.mjs b/tools/pronuncia.mjs new file mode 100644 index 0000000..ec2c3d3 --- /dev/null +++ b/tools/pronuncia.mjs @@ -0,0 +1,18 @@ +// Come la voce deve leggere una battuta: le parole di tools/pronuncia.json che XTTS sbaglia in italiano +// («celo» delle figurine diventa «cielo») si riscrivono come vanno lette. Solo per la voce: il fumetto resta com'è. +import { readFileSync } from 'node:fs'; + +export const rules = JSON.parse(readFileSync(new URL('./pronuncia.json', import.meta.url), 'utf8')); + +// Ogni parola intera (né pezzo di un'altra, né dopo un apostrofo), senza badare alle maiuscole; +// se nel testo cominciava con la maiuscola, la tiene. +export function spoken(text, list = rules) { + let out = text; + for (const [written, said] of Object.entries(list)) { + if (written.startsWith('_')) continue; + const word = written.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const capital = m => m[0] !== m[0].toLowerCase(); + out = out.replace(new RegExp(`(? capital(m) ? said[0].toUpperCase() + said.slice(1) : said); + } + return out; +} diff --git a/tools/voci.mjs b/tools/voci.mjs index a7a754c..ee2ca3b 100644 --- a/tools/voci.mjs +++ b/tools/voci.mjs @@ -5,6 +5,8 @@ // node tools/voci.mjs [--dry] (--dry: elenca cosa genererebbe, senza toccare niente) // node tools/voci.mjs --rifai 'Ci sei?' --rifai 'altro testo' (rifà le battute che contengono quel testo: // XTTS a volte storpia una frase, e un altro tentativo di solito la azzecca) +// Le parole che XTTS legge male (tools/pronuncia.json) si riscrivono come vanno lette solo nel testo per la voce: +// il fumetto resta com'è scritto. // XTTS gira nel suo Python (XTTS_PY, TTS_HOME): di default quello di ~/Workspaces/qemu/qemu-iso-lab. import { createHash } from 'node:crypto'; import { execFileSync } from 'node:child_process'; @@ -13,6 +15,7 @@ import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { story } from '../src/story/story.js'; import { voiceLines, voiceKey } from '../src/engine/voices.js'; +import { spoken } from './pronuncia.mjs'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const OUT = join(ROOT, 'assets', 'voices'); @@ -32,22 +35,23 @@ if (missing.length) throw new Error(`manca la voce in tools/voci.json per: ${mis const index = {}, todo = []; for (const { who, text } of lines) { - const voice = config[who]; - const hash = createHash('sha1').update(JSON.stringify([SINTESI, who, text, voice])).digest('hex').slice(0, 10); + const voice = config[who], said = spoken(text); + // L'impronta segue il testo letto: una regola nuova in pronuncia.json rifà solo le battute che tocca. + const hash = createHash('sha1').update(JSON.stringify(said === text ? [SINTESI, who, text, voice] : [SINTESI, who, text, voice, said])).digest('hex').slice(0, 10); const file = `${who}-${hash}.mp3`; index[voiceKey(who, text)] = { file }; const again = redo.some(r => text.includes(r)); if (again) { rmSync(join(OUT, file), { force: true }); rmSync(join(CACHE, `${who}-${hash}.wav`), { force: true }); } - if (again || !existsSync(join(OUT, file))) todo.push({ who, text, voice, file, wav: join(CACHE, `${who}-${hash}.wav`) }); + if (again || !existsSync(join(OUT, file))) todo.push({ who, text, said, voice, file, wav: join(CACHE, `${who}-${hash}.wav`) }); } console.log(`${lines.length} battute, ${todo.length} da generare`); -if (dry) { for (const t of todo) console.log(` ${t.who}: ${t.text}`); process.exit(0); } +if (dry) { for (const t of todo) console.log(` ${t.who}: ${t.said}${t.said !== t.text ? ` (scritto: ${t.text})` : ''}`); process.exit(0); } mkdirSync(OUT, { recursive: true }); mkdirSync(CACHE, { recursive: true }); if (todo.length) { const jobs = join(CACHE, 'lavori.json'); - writeFileSync(jobs, JSON.stringify(todo.map(t => ({ text: t.text, speaker: t.voice.speaker, out: t.wav })))); + writeFileSync(jobs, JSON.stringify(todo.map(t => ({ text: t.said, speaker: t.voice.speaker, out: t.wav })))); execFileSync(XTTS_PY, ['-I', join(ROOT, 'tools', 'voci_xtts.py'), jobs], { stdio: 'inherit', cwd: CACHE, env: { ...process.env, TTS_HOME } }); // Silenzio tagliato ai due capi, poi il filtro del personaggio; mp3 mono, leggero per il telefono. const trim = 'silenceremove=start_periods=1:start_threshold=-45dB,areverse,silenceremove=start_periods=1:start_threshold=-45dB,areverse';