Primo capitolo: la torcia nella fessura del bastione mostra il palio e due occhi gialli
- le impronte portano a una fessura nel bastione; con la torcia si vede un angolo del palio (l'omino con la maglia rossa del quadro vero, già descritto sul manifesto) e due occhi gialli: fine del primo capitolo - la soluzione passa per indovinello, oratorio e catena del ghiacciolo; tragitti calcolati dalle uscite - il Trebbio ha un indizio per ogni tappa della torcia - tools/pronuncia.json: le parole che XTTS legge male, riscritte solo per la voce («celo» → «ce l'ho») - voci delle battute nuove Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
This commit is contained in:
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.
+124
-8
@@ -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
|
||||
|
||||
+9
-5
@@ -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?
|
||||
|
||||
+2
-2
@@ -41,8 +41,8 @@
|
||||
<button id="skip-intro">Salta</button>
|
||||
</section>
|
||||
<section id="ending" class="overlay" hidden>
|
||||
<h2>Fine del prototipo</h2>
|
||||
<p>Il Regolo è passato di qui. Il resto della storia è ancora da scrivere.</p>
|
||||
<h2>Fine del primo capitolo</h2>
|
||||
<p>Il palio è laggiù, nel buio della Fortezza. E il Regolo pure. Il resto della storia è ancora da scrivere.</p>
|
||||
<p class="dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p>
|
||||
<div class="buttons"><button id="restart">Ricomincia</button><button class="open-credits secondary">Crediti</button></div>
|
||||
</section>
|
||||
|
||||
+45
-6
@@ -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'],
|
||||
];
|
||||
|
||||
+99
-9
@@ -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 }],
|
||||
}],
|
||||
}],
|
||||
},
|
||||
],
|
||||
|
||||
+13
-2
@@ -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` });
|
||||
|
||||
+21
-1
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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(`(?<![\\p{L}'’])${word}(?![\\p{L}])`, 'giu'), m => capital(m) ? said[0].toUpperCase() + said.slice(1) : said);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
+9
-5
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user