Scuola, strada per Cardetole, indovinello di San Pietro con la chiave dell'oratorio, verbo Butta e Pausa

- La scuola (sfondo di Codex dalla foto di Andrea): dalla salita di Villa Adami, dal cancello alle porte a vetri.
- Alla Pieve la strada per Cardetole (per ora una battuta).
- La statua di San Pietro chiede quanti bastioni ha la Fortezza: con la risposta giusta dà la chiave
  dell'oratorio; la porta, aperta una volta, resta aperta e la chiave sparisce. Voce propria per San Pietro.
- Verbo Butta: solo gli oggetti droppable (il libro), quelli che servono alla storia no.
- Pulsante Pausa (o Esc): Riprendi, Mappa, Crediti, Torna al titolo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017egeN2amL84ErjfgQFtEDn
This commit is contained in:
2026-10-23 17:48:49 +02:00
co-authored by Claude Opus 5.5
parent 1ea83d71c5
commit ab03ee0b5a
42 changed files with 339 additions and 28 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+112 -12
View File
@@ -175,17 +175,57 @@
"file": "andrea-6012288000.mp3", "file": "andrea-6012288000.mp3",
"sec": 7.97 "sec": 7.97
}, },
"andrea|Una statua sul piedistallo. Lei il palio l'ha visto passare di sicuro, ma non parla.": { "andrea|La statua di San Pietro, con le chiavi in mano. Lui il palio l'ha visto passare di sicuro.": {
"file": "andrea-36af18a0c4.mp3", "file": "andrea-0663f64ddf.mp3",
"sec": 5.98 "sec": 5.97
}, },
"andrea|Buongiorno.": { "san_pietro|La chiave te l'ho già data, figliolo. Di solito ne do di più importanti.": {
"file": "andrea-05d9f5aea0.mp3", "file": "san_pietro-513a9c35e6.mp3",
"sec": 0.63 "sec": 5.77
}, },
"andrea|Come i vecchietti quando gli chiedi dei debiti.": { "andrea|Buongiorno, San Pietro.": {
"file": "andrea-88a6f5518b.mp3", "file": "andrea-0875a7bce9.mp3",
"sec": 2.62 "sec": 1.31
},
"san_pietro|Buongiorno a te, Andrea.": {
"file": "san_pietro-9b9c57ff08.mp3",
"sec": 1.97
},
"andrea|La statua parla?!": {
"file": "andrea-63221c502a.mp3",
"sec": 1.66
},
"san_pietro|Tengo le chiavi, è il mio mestiere. Quella dell'oratorio te la do se rispondi a un indovinello su San Piero.": {
"file": "san_pietro-7be2fedd9e.mp3",
"sec": 8.11
},
"san_pietro|Quanti bastioni ha la Fortezza di San Martino?": {
"file": "san_pietro-407bef3025.mp3",
"sec": 2.67
},
"andrea|La porta che dà sull'oratorio. Chiusa a chiave.": {
"file": "andrea-fd737a321a.mp3",
"sec": 3.61
},
"andrea|Clic! Aperta. La chiave la lascio nella serratura: così resta aperta.": {
"file": "andrea-61c6a33b63.mp3",
"sec": 7.02
},
"andrea|Chiusa a chiave. E le chiavi, da queste parti, chi le tiene?": {
"file": "andrea-f07f295fa4.mp3",
"sec": 4.53
},
"andrea|Di là si scende a Cardetole, a casa mia.": {
"file": "andrea-77f8aee592.mp3",
"sec": 3.26
},
"andrea|Ma se torno a casa ora, mi mettono ad apparecchiare. Prima il palio.": {
"file": "andrea-b25b3d6b24.mp3",
"sec": 5.05
},
"andrea|La strada che scende a Cardetole, il rione dei verdi.": {
"file": "andrea-d7882fce87.mp3",
"sec": 3.35
}, },
"andrea|Il palco dell'oratorio. D'estate ci fanno le recite, la tombola e i concerti.": { "andrea|Il palco dell'oratorio. D'estate ci fanno le recite, la tombola e i concerti.": {
"file": "andrea-058d10ea48.mp3", "file": "andrea-058d10ea48.mp3",
@@ -307,13 +347,37 @@
"file": "andrea-468fe91000.mp3", "file": "andrea-468fe91000.mp3",
"sec": 3.28 "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|Un pino a ombrello enorme. Alla ricreazione, tutti all'ombra lì sotto.": {
"file": "andrea-0eea4f27c6.mp3",
"sec": 5.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|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.": { "andrea|A scuola? A maggio? Nemmeno per sogno.": {
"file": "andrea-72e4ebb412.mp3", "file": "andrea-72e4ebb412.mp3",
"sec": 3.88 "sec": 3.88
}, },
"andrea|Di là si va alla scuola. Ci passo già abbastanza tempo durante l'anno.": { "andrea|Le palazzine gialle accanto alla scuola, con la scala arancione.": {
"file": "andrea-b3acca1e65.mp3", "file": "andrea-528eda3257.mp3",
"sec": 5.34 "sec": 3.95
},
"andrea|Da lassù la ricreazione si vede tutta.": {
"file": "andrea-b12fde7bf4.mp3",
"sec": 3.11
}, },
"andrea|I cipressi in fila, dritti come soldatini. Da piccolo li contavo. Non sono mai arrivato in fondo.": { "andrea|I cipressi in fila, dritti come soldatini. Da piccolo li contavo. Non sono mai arrivato in fondo.": {
"file": "andrea-bd1ea0e1d2.mp3", "file": "andrea-bd1ea0e1d2.mp3",
@@ -363,6 +427,10 @@
"file": "andrea-fc32e07a5a.mp3", "file": "andrea-fc32e07a5a.mp3",
"sec": 3.74 "sec": 3.74
}, },
"andrea|Una pagina sola, e pure con la fine. Lo lascio qui per il prossimo lettore.": {
"file": "andrea-615bda9e52.mp3",
"sec": 5.2
},
"andrea|«La storia infinita». Bello spesso: questo mi tiene compagnia fino a settembre.": { "andrea|«La storia infinita». Bello spesso: questo mi tiene compagnia fino a settembre.": {
"file": "andrea-23e40a2495.mp3", "file": "andrea-23e40a2495.mp3",
"sec": 5.74 "sec": 5.74
@@ -383,6 +451,38 @@
"file": "andrea-3a6af3897f.mp3", "file": "andrea-3a6af3897f.mp3",
"sec": 6.52 "sec": 6.52
}, },
"andrea|Una chiave grossa e antica. Quella dell'oratorio, parola di San Pietro.": {
"file": "andrea-60cdae2e35.mp3",
"sec": 5.1
},
"san_pietro|Cinque? Ne hai contati la metà. Vai a guardarli bene, lassù.": {
"file": "san_pietro-4f6f201875.mp3",
"sec": 6.53
},
"san_pietro|Sette sono i peccati. I bastioni sono di più.": {
"file": "san_pietro-96b58aedde.mp3",
"sec": 4.44
},
"san_pietro|Bravo! Nove, come li volle Cosimo I.": {
"file": "san_pietro-3a2e3a543b.mp3",
"sec": 4.25
},
"san_pietro|Eccoti la chiave dell'oratorio. Trattala bene: è più vecchia di tuo nonno.": {
"file": "san_pietro-9e24f3914e.mp3",
"sec": 6.14
},
"andrea|Grazie, San Pietro!": {
"file": "andrea-149f790fa1.mp3",
"sec": 1.25
},
"san_pietro|Dodici erano gli apostoli. Io lo so bene: c'ero.": {
"file": "san_pietro-a8ebf578e0.mp3",
"sec": 5.2
},
"san_pietro|Le chiavi non scappano. E io nemmeno.": {
"file": "san_pietro-6c85ba44c5.mp3",
"sec": 3.63
},
"vecchietta|È stato il Regolo! Quello che cadde dal cielo quando scavavano le fondamenta.": { "vecchietta|È stato il Regolo! Quello che cadde dal cielo quando scavavano le fondamenta.": {
"file": "vecchietta-e35443db85.mp3", "file": "vecchietta-e35443db85.mp3",
"sec": 5.13 "sec": 5.13
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.
+3
View File
@@ -29,6 +29,9 @@ Ultimo aggiornamento: 23 ottobre 2026. Da leggere insieme a `PROGETTO.md` all'in
Un posto nuovo entra con `map: [x, y]` nella sua scena (percentuali della pianta, disposta come «il paese a gradini»). Un posto nuovo entra con `map: [x, y]` nella sua scena (percentuali della pianta, disposta come «il paese a gradini»).
- **Crediti** dal titolo e dalla fine (`story.credits`), dedica al Lele in testa; musichetta a 8 bit del cartello - **Crediti** dal titolo e dalla fine (`story.credits`), dedica al Lele in testa; musichetta a 8 bit del cartello
(`tools/jingle.py`). (`tools/jingle.py`).
- **Primo indovinello**: la statua di San Pietro alla Pieve chiede quanti bastioni ha la Fortezza (la risposta la dà il
bastione stesso); la chiave apre l'oratorio, che da lì resta aperto, e la chiave sparisce. Verbo **Butta**: solo gli
oggetti `droppable` (per ora il libro), quelli che servono alla storia no. Pulsante **Pausa** (o Esc).
- **Il libro della storia infinita** sulle tavolate dell'oratorio; le idee in attesa in `docs/IDEE.md`. - **Il libro della storia infinita** sulle tavolate dell'oratorio; le idee in attesa in `docs/IDEE.md`.
- **Test**: `make test` (29 test, compresa la soluzione completa da `src/story/solution.js`) e `make browser` - **Test**: `make test` (29 test, compresa la soluzione completa da `src/story/solution.js`) e `make browser`
(partita completa con clic veri in Chromium, ripresa del salvataggio, telefono). (partita completa con clic veri in Chromium, ripresa del salvataggio, telefono).
+14 -1
View File
@@ -11,7 +11,10 @@
<div id="stage"> <div id="stage">
<canvas id="scene" width="640" height="360" aria-label="La scena dell'avventura"></canvas> <canvas id="scene" width="640" height="360" aria-label="La scena dell'avventura"></canvas>
<div id="speech" aria-live="polite"></div> <div id="speech" aria-live="polite"></div>
<button id="map-button" hidden title="Mappa (P)">Mappa</button> <div id="corner">
<button id="pause-button" hidden title="Pausa (Esc)">Pausa</button>
<button id="map-button" hidden title="Mappa (P)">Mappa</button>
</div>
<div id="sound"> <div id="sound">
<button id="mute" aria-label="Musica: togli l'audio" title="Musica (M)">♪</button> <button id="mute" aria-label="Musica: togli l'audio" title="Musica (M)">♪</button>
<input id="volume" type="range" min="0" max="100" step="5" aria-label="Volume della musica"> <input id="volume" type="range" min="0" max="100" step="5" aria-label="Volume della musica">
@@ -43,6 +46,16 @@
<div id="map-board"><svg id="map-roads" viewBox="0 0 100 100" preserveAspectRatio="none"></svg></div> <div id="map-board"><svg id="map-roads" viewBox="0 0 100 100" preserveAspectRatio="none"></svg></div>
<div class="buttons"><button id="close-map">Chiudi</button></div> <div class="buttons"><button id="close-map">Chiudi</button></div>
</section> </section>
<section id="pause" class="overlay" hidden aria-label="Pausa">
<h2>Pausa</h2>
<div class="buttons pause-buttons">
<button id="resume">Riprendi</button>
<button id="pause-map" class="secondary">Mappa</button>
<button class="open-credits secondary">Crediti</button>
<button id="to-title" class="secondary">Torna al titolo</button>
</div>
<p class="hint">La partita è salvata: dal titolo si riprende con «Continua».</p>
</section>
<section id="credits" class="overlay" hidden aria-label="Crediti"> <section id="credits" class="overlay" hidden aria-label="Crediti">
<h2>Crediti</h2> <h2>Crediti</h2>
<p class="dedication credits-dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p> <p class="dedication credits-dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p>
+5 -2
View File
@@ -2,8 +2,8 @@
// La storia (scene, oggetti, dialoghi) arriva tutta dai dati: qui non c'è nessun enigma scritto a mano. // La storia (scene, oggetti, dialoghi) arriva tutta dai dati: qui non c'è nessun enigma scritto a mano.
import { findPath, dist, inBox, nearestInside } from './geometry.js'; import { findPath, dist, inBox, nearestInside } from './geometry.js';
export const VERBS = ['walk', 'look', 'take', 'use', 'talk', 'open', 'give']; export const VERBS = ['walk', 'look', 'take', 'use', 'talk', 'open', 'give', 'drop'];
export const VERB_LABELS = { walk: 'Vai a', look: 'Guarda', take: 'Prendi', use: 'Usa', talk: 'Parla con', open: 'Apri', give: 'Dai' }; export const VERB_LABELS = { walk: 'Vai a', look: 'Guarda', take: 'Prendi', use: 'Usa', talk: 'Parla con', open: 'Apri', give: 'Dai', drop: 'Butta' };
const DEFAULT_LINES = { const DEFAULT_LINES = {
walk: null, walk: null,
look: 'Niente di speciale.', look: 'Niente di speciale.',
@@ -12,6 +12,7 @@ const DEFAULT_LINES = {
talk: 'Non mi sembra un gran chiacchierone.', talk: 'Non mi sembra un gran chiacchierone.',
open: 'Non si apre.', open: 'Non si apre.',
give: 'Non credo che lo voglia.', give: 'Non credo che lo voglia.',
drop: 'Non è roba mia da buttare.',
with: 'Non funziona.', with: 'Non funziona.',
}; };
const SPEED = 95; const SPEED = 95;
@@ -106,6 +107,8 @@ export class Game {
if (def.actions?.[verb]) return def.actions[verb]; if (def.actions?.[verb]) return def.actions[verb];
if (verb === 'walk') return h?.exit ? [{ go: h.exit.scene, at: h.exit.at, face: h.exit.face }] : []; if (verb === 'walk') return h?.exit ? [{ go: h.exit.scene, at: h.exit.at, face: h.exit.face }] : [];
if (verb === 'take' && !h) return [{ say: 'Ce l\'ho già.' }]; if (verb === 'take' && !h) return [{ say: 'Ce l\'ho già.' }];
// Si butta solo quello che non serve alla storia (`droppable`): gli oggetti indispensabili restano.
if (verb === 'drop' && !h) return def.droppable ? [{ say: 'Questo non mi serve: via.' }, { drop: target }] : [{ say: 'Meglio tenerlo: potrebbe servirmi.' }];
return DEFAULT_LINES[verb] ? [{ say: DEFAULT_LINES[verb] }] : []; return DEFAULT_LINES[verb] ? [{ say: DEFAULT_LINES[verb] }] : [];
} }
+25 -1
View File
@@ -101,6 +101,27 @@ function openMap() {
$('map').hidden = false; $('map').hidden = false;
} }
$('map-button').addEventListener('click', openMap); $('map-button').addEventListener('click', openMap);
// --- Pausa: ferma il gioco; da qui mappa, crediti o ritorno al titolo (la partita resta salvata per «Continua»).
function openPause() {
if (!running) return;
running = false;
command.reset();
$('pause-map').hidden = game.mapPlaces().length < 2;
$('pause').hidden = false;
}
function closePause() {
$('pause').hidden = true;
running = true;
}
$('pause-button').addEventListener('click', openPause);
$('resume').addEventListener('click', closePause);
$('pause-map').addEventListener('click', () => { closePause(); openMap(); });
$('to-title').addEventListener('click', () => {
$('pause').hidden = true;
game.onSave(game.snapshot());
showTitle();
});
$('close-map').addEventListener('click', () => { $('map').hidden = true; }); $('close-map').addEventListener('click', () => { $('map').hidden = true; });
function showTitle() { function showTitle() {
@@ -195,13 +216,15 @@ function updateChoices() {
window.addEventListener('keydown', e => { window.addEventListener('keydown', e => {
if (e.target === $('volume')) return; if (e.target === $('volume')) return;
if (!$('credits').hidden) { if (e.key === 'Escape') $('credits').hidden = true; return; } if (!$('credits').hidden) { if (e.key === 'Escape') $('credits').hidden = true; return; }
if (!$('pause').hidden) { if (e.key === 'Escape') closePause(); return; }
if (!$('map').hidden) { if (e.key === 'Escape' || e.key === 'p' || e.key === 'P') $('map').hidden = true; return; } if (!$('map').hidden) { if (e.key === 'Escape' || e.key === 'p' || e.key === 'P') $('map').hidden = true; return; }
if ((e.key === 'p' || e.key === 'P') && running) { openMap(); return; } if ((e.key === 'p' || e.key === 'P') && running) { openMap(); return; }
if (!$('intro').hidden && [' ', 'Enter'].includes(e.key)) { e.preventDefault(); nextIntro(); return; } if (!$('intro').hidden && [' ', 'Enter'].includes(e.key)) { e.preventDefault(); nextIntro(); return; }
if (e.key === 'm' || e.key === 'M') { music.toggle(); saveAudio(); showSound(); return; } if (e.key === 'm' || e.key === 'M') { music.toggle(); saveAudio(); showSound(); return; }
if (!running) return; if (!running) return;
if ([' ', '.', 'Enter'].includes(e.key) && game.speech) { e.preventDefault(); game.skip(); return; } if ([' ', '.', 'Enter'].includes(e.key) && game.speech) { e.preventDefault(); game.skip(); return; }
if (e.key === 'Escape') command.reset(); // Esc annulla il comando a metà; se non c'è niente da annullare, mette in pausa.
if (e.key === 'Escape') { if (command.verb || command.item || game.dialogue) command.reset(); else openPause(); }
const n = Number(e.key); const n = Number(e.key);
if (game.dialogue && n >= 1 && n <= 9) game.choose(n - 1); if (game.dialogue && n >= 1 && n <= 9) game.choose(n - 1);
}); });
@@ -233,6 +256,7 @@ function updateUi() {
updateSpeech(); updateSpeech();
// Il pulsante della mappa c'è solo in partita, fuori dai dialoghi, e quando c'è almeno un altro posto dove andare. // Il pulsante della mappa c'è solo in partita, fuori dai dialoghi, e quando c'è almeno un altro posto dove andare.
$('map-button').hidden = !running || !!game.dialogue || game.mapPlaces().length < 2; $('map-button').hidden = !running || !!game.dialogue || game.mapPlaces().length < 2;
$('pause-button').hidden = !running;
if (game.state.ended && !game.speech && !game.queue.length) { if (game.state.ended && !game.speech && !game.queue.length) {
running = false; running = false;
$('ending').hidden = false; $('ending').hidden = false;
+116 -6
View File
@@ -33,6 +33,7 @@ export const story = {
vecchietto: { name: 'Vecchietto', color: '#9fe3ff', sprite: 'assets/characters/vecchietto.json' }, vecchietto: { name: 'Vecchietto', color: '#9fe3ff', sprite: 'assets/characters/vecchietto.json' },
castello: { name: 'Il Castello', color: '#c8f0a0' }, castello: { name: 'Il Castello', color: '#c8f0a0' },
leone: { name: 'Il leone', color: '#f0a070' }, leone: { name: 'Il leone', color: '#f0a070' },
san_pietro: { name: 'San Pietro', color: '#e8e0c8' },
}, },
items: { items: {
squama: { squama: {
@@ -47,7 +48,9 @@ export const story = {
libro: { libro: {
name: 'libro', name: 'libro',
icon: { fill: '#c46a3a', shape: 'book' }, icon: { fill: '#c46a3a', shape: 'book' },
droppable: true, // non serve alla storia: si può buttare
actions: { actions: {
drop: [{ say: 'Una pagina sola, e pure con la fine. Lo lascio qui per il prossimo lettore.' }, { drop: 'libro' }],
look: [{ say: '«La storia infinita». Bello spesso: questo mi tiene compagnia fino a settembre.' }], look: [{ say: '«La storia infinita». Bello spesso: questo mi tiene compagnia fino a settembre.' }],
open: [ open: [
{ say: 'Vediamo da dove comincia...' }, { say: 'Vediamo da dove comincia...' },
@@ -57,6 +60,14 @@ export const story = {
use: [{ say: 'Leggere adesso? Prima il palio. E poi, a quanto pare, si fa presto.' }], use: [{ say: 'Leggere adesso? Prima il palio. E poi, a quanto pare, si fa presto.' }],
}, },
}, },
// La chiave dell'oratorio: la dà San Pietro a chi risolve il suo indovinello. Indispensabile: non si butta.
chiave_oratorio: {
name: 'chiave dell\'oratorio',
icon: { fill: '#d9b54a', shape: 'key' },
actions: {
look: [{ say: 'Una chiave grossa e antica. Quella dell\'oratorio, parola di San Pietro.' }],
},
},
}, },
scenes: { scenes: {
piazza: { piazza: {
@@ -196,6 +207,7 @@ export const story = {
name: 'La Pieve', name: 'La Pieve',
map: [46, 70], map: [46, 70],
background: 'assets/scenes/pieve-dalla-salita.png', 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). // La rampa in primo piano e, dietro il muretto, il sagrato (ci si arriva da sinistra).
walk: [[0, 270], [80, 266], [86, 252], [100, 222], [270, 215], [300, 224], [480, 234], [560, 242], [600, 252], [610, 268], [276, 240], [276, 270], [292, 278], [640, 352], [640, 360], [0, 360]], walk: [[0, 270], [80, 266], [86, 252], [100, 222], [270, 215], [300, 224], [480, 234], [560, 242], [600, 252], [610, 268], [276, 240], [276, 270], [292, 278], [640, 352], [640, 360], [0, 360]],
depth: [[215, 0.42], [360, 1.15]], depth: [[215, 0.42], [360, 1.15]],
@@ -215,14 +227,50 @@ export const story = {
}, },
}, },
{ {
id: 'statua', name: 'statua', box: [334, 172, 18, 52], at: [330, 300], face: 'up', // San Pietro, quello delle chiavi: a chi risolve l'indovinello dà la chiave dell'oratorio.
id: 'statua', name: 'statua di San Pietro', box: [330, 170, 26, 56], at: [345, 238], face: 'up', default: 'talk',
actions: { actions: {
look: [{ say: 'Una statua sul piedistallo. Lei il palio l\'ha visto passare di sicuro, ma non parla.' }], look: [{ say: 'La statua di San Pietro, con le chiavi in mano. Lui il palio l\'ha visto passare di sicuro.' }],
talk: [{ say: 'Buongiorno.' }, { say: '...' }, { say: 'Come i vecchietti quando gli chiedi dei debiti.' }], talk: [{
if: 'chiave_data',
then: [{ say: 'La chiave te l\'ho già data, figliolo. Di solito ne do di più importanti.', who: 'san_pietro' }],
else: [
{ say: 'Buongiorno, San Pietro.' },
{ say: 'Buongiorno a te, Andrea.', who: 'san_pietro' },
{ say: 'La statua parla?!' },
{ say: 'Tengo le chiavi, è il mio mestiere. Quella dell\'oratorio te la do se rispondi a un indovinello su San Piero.', who: 'san_pietro' },
{ say: 'Quanti bastioni ha la Fortezza di San Martino?', who: 'san_pietro' },
{ dialogue: 'indovinello' },
],
}],
}, },
}, },
{ {
id: 'oratorio', name: 'porta dell\'oratorio', box: [452, 180, 42, 56], at: [473, 240], face: 'up', exit: { scene: 'oratorio', at: [320, 345], face: 'right' }, // La porta resta chiusa finché non si usa la chiave; poi è aperta per sempre e la chiave sparisce.
id: 'porta_oratorio', name: 'porta dell\'oratorio', box: [452, 180, 42, 56], at: [473, 240], face: 'up', default: 'open', when: '!oratorio_aperto',
actions: {
look: [{ say: 'La porta che dà sull\'oratorio. Chiusa a chiave.' }],
open: [{
if: 'has:chiave_oratorio',
then: [{ drop: 'chiave_oratorio' }, { set: 'oratorio_aperto' }, { say: 'Clic! Aperta. La chiave la lascio nella serratura: così resta aperta.' }, { go: 'oratorio', at: [320, 345], face: 'right' }],
else: [{ say: 'Chiusa a chiave. E le chiavi, da queste parti, chi le tiene?' }],
}],
walk: [{ say: 'Chiusa a chiave. E le chiavi, da queste parti, chi le tiene?' }],
},
with: {
chiave_oratorio: [{ drop: 'chiave_oratorio' }, { set: 'oratorio_aperto' }, { say: 'Clic! Aperta. La chiave la lascio nella serratura: così resta aperta.' }, { go: 'oratorio', at: [320, 345], face: 'right' }],
},
},
{
id: 'oratorio', name: 'porta dell\'oratorio', box: [452, 180, 42, 56], at: [473, 240], face: 'up', when: 'oratorio_aperto', exit: { scene: 'oratorio', at: [320, 345], face: 'right' },
},
{
// Per ora solo una battuta: la scena di Cardetole arriverà con le foto di Andrea.
id: 'verso_cardetole', name: 'strada per Cardetole', box: [0, 204, 82, 54], at: [95, 250], face: 'left', default: 'walk',
actions: {
walk: [{ say: 'Di là si scende a Cardetole, a casa mia.' }, { say: 'Ma se torno a casa ora, mi mettono ad apparecchiare. Prima il palio.' }],
look: [{ say: 'La strada che scende a Cardetole, il rione dei verdi.' }],
},
}, },
{ {
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_piazza', name: 'verso la Piazza', box: [0, 262, 26, 98], at: [8, 320], exit: { scene: 'piazza', at: [610, 320], face: 'left' },
@@ -418,9 +466,8 @@ export const story = {
}, },
}, },
{ {
id: 'verso_scuola', name: 'strada verso la scuola', box: [0, 286, 120, 44], at: [40, 336], face: 'left', default: 'walk', 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: { actions: {
walk: [{ say: 'A scuola? A maggio? Nemmeno per sogno.' }],
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 passo già abbastanza tempo durante l\'anno.' }],
}, },
}, },
@@ -432,6 +479,47 @@ 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],
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]],
entry: [300, 350],
hotspots: [
{
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.' }],
},
},
{
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.' }],
},
},
{
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.' }],
},
},
{
id: 'palazzine', name: 'palazzine gialle', box: [340, 30, 300, 196], at: [330, 262], face: 'right',
actions: {
look: [{ say: 'Le palazzine gialle accanto alla scuola, con la scala arancione.' }, { say: 'Da lassù la ricreazione si vede tutta.' }],
},
},
{
id: 'alla_salita', name: 'indietro, verso la salita', box: [0, 330, 640, 30], at: [300, 352], exit: { scene: 'salita_villa_adami', at: [40, 336], face: 'right' },
},
],
},
// Il viale dei cipressi, la strada bianca tra la Fortezza e il Trebbio: in fondo c'è il castello. // Il viale dei cipressi, la strada bianca tra la Fortezza e il Trebbio: in fondo c'è il castello.
viale_cipressi: { viale_cipressi: {
name: 'Viale dei cipressi', name: 'Viale dei cipressi',
@@ -496,6 +584,28 @@ export const story = {
}, },
}, },
dialogues: { dialogues: {
// L'indovinello di San Pietro. La risposta si trova nel gioco: il bastione della Fortezza la dice.
indovinello: {
nodes: {
start: {
options: [
{ text: 'Cinque.', then: [{ say: 'Cinque? Ne hai contati la metà. Vai a guardarli bene, lassù.', who: 'san_pietro' }] },
{ text: 'Sette.', then: [{ say: 'Sette sono i peccati. I bastioni sono di più.', who: 'san_pietro' }] },
{
text: 'Nove.', next: 'end',
then: [
{ say: 'Bravo! Nove, come li volle Cosimo I.', who: 'san_pietro' },
{ say: 'Eccoti la chiave dell\'oratorio. Trattala bene: è più vecchia di tuo nonno.', who: 'san_pietro' },
{ give: 'chiave_oratorio' }, { set: 'chiave_data' },
{ say: 'Grazie, San Pietro!' },
],
},
{ text: 'Dodici.', then: [{ say: 'Dodici erano gli apostoli. Io lo so bene: c\'ero.', who: 'san_pietro' }] },
{ text: 'Ci penso e torno.', next: 'end', then: [{ say: 'Le chiavi non scappano. E io nemmeno.', who: 'san_pietro' }] },
],
},
},
},
vecchietti: { vecchietti: {
nodes: { nodes: {
start: { start: {
+8 -5
View File
@@ -31,10 +31,8 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
#sentence { min-height: 1.8em; padding: .25em .6em; text-align: center; color: var(--blue); font-size: clamp(15px, 2vw, 22px); font-weight: bold; background: var(--panel); } #sentence { min-height: 1.8em; padding: .25em .6em; text-align: center; color: var(--blue); font-size: clamp(15px, 2vw, 22px); font-weight: bold; background: var(--panel); }
#panel { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); gap: 8px; padding: 8px; background: var(--panel); } #panel { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); gap: 8px; padding: 8px; background: var(--panel); }
/* Sette verbi su due righe piene: quattro sopra (3/12 ciascuno) e tre sotto (4/12). */ /* Otto verbi su due righe piene, quattro per riga. */
#verbs { display: grid; grid-template-columns: repeat(12, 1fr); gap: 4px; align-content: start; } #verbs { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; align-content: start; }
#verbs button { grid-column: span 4; }
#verbs button:nth-child(-n+4) { grid-column: span 3; }
#verbs button, #inventory button { font: inherit; color: var(--dim); background: #3a2d42; border: 2px solid #4b3a55; border-radius: 4px; #verbs button, #inventory button { font: inherit; color: var(--dim); background: #3a2d42; border: 2px solid #4b3a55; border-radius: 4px;
padding: .35em .2em; font-size: clamp(13px, 1.7vw, 19px); font-weight: bold; cursor: pointer; } padding: .35em .2em; font-size: clamp(13px, 1.7vw, 19px); font-weight: bold; cursor: pointer; }
#verbs button:hover, #inventory button:hover { color: var(--ink); } #verbs button:hover, #inventory button:hover { color: var(--ink); }
@@ -96,8 +94,13 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
.buttons button.secondary { color: var(--ink); background: rgba(42, 32, 48, .85); box-shadow: inset 0 0 0 2px var(--dim); } .buttons button.secondary { color: var(--ink); background: rgba(42, 32, 48, .85); box-shadow: inset 0 0 0 2px var(--dim); }
/* Crediti: sopra titolo e fine (z-index), una voce per ruolo; scorrono se non ci stanno. */ /* Crediti: sopra titolo e fine (z-index), una voce per ruolo; scorrono se non ci stanno. */
/* Mappa: la pianta del paese a gradini, i posti visti come cartellini, le strade come linee. */ /* Mappa: la pianta del paese a gradini, i posti visti come cartellini, le strade come linee. */
#map-button { position: absolute; top: 6px; left: 6px; z-index: 2; font: bold clamp(12px, 1.6vw, 16px) var(--font); padding: .35em .8em; /* In alto a sinistra: Pausa e Mappa. */
#corner { position: absolute; top: 6px; left: 6px; z-index: 2; display: flex; gap: 6px; }
#map-button, #pause-button { font: bold clamp(12px, 1.6vw, 16px) var(--font); padding: .35em .8em;
color: var(--accent); background: rgba(27, 20, 32, .62); border: 0; border-radius: 18px; cursor: pointer; } color: var(--accent); background: rgba(27, 20, 32, .62); border: 0; border-radius: 18px; cursor: pointer; }
#pause { z-index: 3; background: rgba(15, 10, 20, .9); }
.pause-buttons { flex-direction: column; align-items: stretch; min-width: min(70%, 16em); }
#pause .hint { color: var(--dim); }
#map { z-index: 3; justify-content: flex-start; padding-top: 2.5%; background: rgba(15, 10, 20, .94); } #map { z-index: 3; justify-content: flex-start; padding-top: 2.5%; background: rgba(15, 10, 20, .94); }
#map h2 { font-size: clamp(18px, 3vw, 32px); } #map h2 { font-size: clamp(18px, 3vw, 32px); }
#map-board { position: relative; width: 92%; flex: 1; min-height: 0; margin: .3em 0; #map-board { position: relative; width: 92%; flex: 1; min-height: 0; margin: .3em 0;
+11
View File
@@ -84,6 +84,17 @@ try {
assert.ok(await fits(page), 'verbi e inventario fuori dallo schermo'); assert.ok(await fits(page), 'verbi e inventario fuori dallo schermo');
await settle(page); 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])');
await page.screenshot({ path: `${OUT}/02b-pausa.png` });
await page.click('#resume');
assert.ok(await page.isHidden('#pause'));
await page.click('#pause-button');
await page.click('#to-title');
await page.waitForSelector('#continue:not([hidden])');
await page.click('#continue');
await settle(page);
// Musica: dopo il primo clic suona il brano del paese; muto e volume si ricordano. // Musica: dopo il primo clic suona il brano del paese; muto e volume si ricordano.
assert.ok((await page.evaluate(() => music.current?.src)).endsWith('paese.mp3'), 'musica del paese'); assert.ok((await page.evaluate(() => music.current?.src)).endsWith('paese.mp3'), 'musica del paese');
await page.click('#mute'); await page.click('#mute');
+43
View File
@@ -169,3 +169,46 @@ test('la mappa: ogni scena ci sta, i posti compaiono solo visti, il viaggio risp
assert.ok(g.dialogue); assert.ok(g.dialogue);
assert.equal(g.travel('pieve'), false, 'non si scappa da un dialogo'); assert.equal(g.travel('pieve'), false, 'non si scappa da un dialogo');
}); });
test('l\'indovinello di San Pietro dà la chiave; l\'oratorio, aperto, resta aperto e la chiave sparisce', () => {
const g = new Game(story);
g.go('pieve', [345, 238]);
settle(g);
assert.ok(!g.hotspot('oratorio') && g.hotspot('porta_oratorio'), 'all\'inizio la porta è chiusa');
g.act('open', 'porta_oratorio');
settle(g);
assert.equal(g.state.scene, 'pieve', 'senza chiave non si entra');
g.act('talk', 'statua');
settle(g);
const answer = text => { g.choose(g.options().findIndex(o => o.text === text)); return settle(g); };
answer('Sette.');
assert.ok(g.dialogue && !g.state.inventory.includes('chiave_oratorio'), 'risposta sbagliata: niente chiave, si riprova');
answer('Nove.');
assert.ok(!g.dialogue && g.state.inventory.includes('chiave_oratorio'), 'risposta giusta: la chiave');
g.act('drop', 'chiave_oratorio');
settle(g);
assert.ok(g.state.inventory.includes('chiave_oratorio'), 'la chiave serve: non si butta');
g.act('use', 'porta_oratorio', 'chiave_oratorio');
settle(g);
assert.equal(g.state.scene, 'oratorio');
assert.ok(!g.state.inventory.includes('chiave_oratorio'), 'la chiave resta nella serratura');
g.go('pieve', [345, 238]);
assert.ok(g.hotspot('oratorio')?.exit && !g.hotspot('porta_oratorio'), 'da ora la porta è aperta');
g.act('talk', 'statua');
settle(g);
assert.ok(!g.dialogue, 'San Pietro non ridà la chiave');
});
test('si buttano solo gli oggetti che non servono', () => {
const g = new Game(story);
g.go('oratorio');
settle(g);
g.state.inventory.push('libro', 'squama');
g.act('drop', 'libro');
settle(g);
g.act('drop', 'squama');
settle(g);
assert.deepEqual(g.state.inventory, ['squama']);
for (const [id, item] of Object.entries(story.items))
if (id !== 'libro') assert.ok(!item.droppable, `${id} serve alla storia: non deve potersi buttare`);
});
+2 -1
View File
@@ -4,5 +4,6 @@
"vecchietta": { "speaker": "Tammy Grit" }, "vecchietta": { "speaker": "Tammy Grit" },
"vecchietto": { "speaker": "Viktor Eka" }, "vecchietto": { "speaker": "Viktor Eka" },
"castello": { "speaker": "Damien Black", "filtro": "asetrate=24000*0.9,aresample=24000,atempo=0.95,aecho=0.8:0.7:90|180:0.25|0.15" }, "castello": { "speaker": "Damien Black", "filtro": "asetrate=24000*0.9,aresample=24000,atempo=0.95,aecho=0.8:0.7:90|180:0.25|0.15" },
"leone": { "speaker": "Baldur Sanjin", "filtro": "asetrate=24000*0.84,aresample=24000,atempo=1.08" } "leone": { "speaker": "Baldur Sanjin", "filtro": "asetrate=24000*0.84,aresample=24000,atempo=1.08" },
"san_pietro": { "speaker": "Wulf Carlevaro", "filtro": "asetrate=24000*0.92,aresample=24000,atempo=0.95,aecho=0.8:0.6:60:0.2" }
} }