diff --git a/assets/scenes/scuola.png b/assets/scenes/scuola.png
new file mode 100644
index 0000000..a63cc4d
Binary files /dev/null and b/assets/scenes/scuola.png differ
diff --git a/assets/voices/andrea-05d9f5aea0.mp3 b/assets/voices/andrea-05d9f5aea0.mp3
deleted file mode 100644
index 5cf8dec..0000000
Binary files a/assets/voices/andrea-05d9f5aea0.mp3 and /dev/null differ
diff --git a/assets/voices/andrea-0663f64ddf.mp3 b/assets/voices/andrea-0663f64ddf.mp3
new file mode 100644
index 0000000..3c0c086
Binary files /dev/null and b/assets/voices/andrea-0663f64ddf.mp3 differ
diff --git a/assets/voices/andrea-0875a7bce9.mp3 b/assets/voices/andrea-0875a7bce9.mp3
new file mode 100644
index 0000000..797ab61
Binary files /dev/null and b/assets/voices/andrea-0875a7bce9.mp3 differ
diff --git a/assets/voices/andrea-0eea4f27c6.mp3 b/assets/voices/andrea-0eea4f27c6.mp3
new file mode 100644
index 0000000..98b1b55
Binary files /dev/null and b/assets/voices/andrea-0eea4f27c6.mp3 differ
diff --git a/assets/voices/andrea-149f790fa1.mp3 b/assets/voices/andrea-149f790fa1.mp3
new file mode 100644
index 0000000..31ded29
Binary files /dev/null and b/assets/voices/andrea-149f790fa1.mp3 differ
diff --git a/assets/voices/andrea-36af18a0c4.mp3 b/assets/voices/andrea-36af18a0c4.mp3
deleted file mode 100644
index 340f028..0000000
Binary files a/assets/voices/andrea-36af18a0c4.mp3 and /dev/null differ
diff --git a/assets/voices/andrea-528eda3257.mp3 b/assets/voices/andrea-528eda3257.mp3
new file mode 100644
index 0000000..2dad937
Binary files /dev/null and b/assets/voices/andrea-528eda3257.mp3 differ
diff --git a/assets/voices/andrea-60cdae2e35.mp3 b/assets/voices/andrea-60cdae2e35.mp3
new file mode 100644
index 0000000..69c5738
Binary files /dev/null and b/assets/voices/andrea-60cdae2e35.mp3 differ
diff --git a/assets/voices/andrea-615bda9e52.mp3 b/assets/voices/andrea-615bda9e52.mp3
new file mode 100644
index 0000000..cd1a009
Binary files /dev/null and b/assets/voices/andrea-615bda9e52.mp3 differ
diff --git a/assets/voices/andrea-61c6a33b63.mp3 b/assets/voices/andrea-61c6a33b63.mp3
new file mode 100644
index 0000000..9592e45
Binary files /dev/null and b/assets/voices/andrea-61c6a33b63.mp3 differ
diff --git a/assets/voices/andrea-63221c502a.mp3 b/assets/voices/andrea-63221c502a.mp3
new file mode 100644
index 0000000..4e90223
Binary files /dev/null and b/assets/voices/andrea-63221c502a.mp3 differ
diff --git a/assets/voices/andrea-7655ebd550.mp3 b/assets/voices/andrea-7655ebd550.mp3
new file mode 100644
index 0000000..c2ac9fb
Binary files /dev/null and b/assets/voices/andrea-7655ebd550.mp3 differ
diff --git a/assets/voices/andrea-77f8aee592.mp3 b/assets/voices/andrea-77f8aee592.mp3
new file mode 100644
index 0000000..b523cd2
Binary files /dev/null and b/assets/voices/andrea-77f8aee592.mp3 differ
diff --git a/assets/voices/andrea-88a6f5518b.mp3 b/assets/voices/andrea-88a6f5518b.mp3
deleted file mode 100644
index dbaa081..0000000
Binary files a/assets/voices/andrea-88a6f5518b.mp3 and /dev/null differ
diff --git a/assets/voices/andrea-929dc868d7.mp3 b/assets/voices/andrea-929dc868d7.mp3
new file mode 100644
index 0000000..457929c
Binary files /dev/null and b/assets/voices/andrea-929dc868d7.mp3 differ
diff --git a/assets/voices/andrea-a24a09d869.mp3 b/assets/voices/andrea-a24a09d869.mp3
new file mode 100644
index 0000000..019fb33
Binary files /dev/null and b/assets/voices/andrea-a24a09d869.mp3 differ
diff --git a/assets/voices/andrea-b12fde7bf4.mp3 b/assets/voices/andrea-b12fde7bf4.mp3
new file mode 100644
index 0000000..081be6f
Binary files /dev/null and b/assets/voices/andrea-b12fde7bf4.mp3 differ
diff --git a/assets/voices/andrea-b25b3d6b24.mp3 b/assets/voices/andrea-b25b3d6b24.mp3
new file mode 100644
index 0000000..72a1cc1
Binary files /dev/null and b/assets/voices/andrea-b25b3d6b24.mp3 differ
diff --git a/assets/voices/andrea-d7882fce87.mp3 b/assets/voices/andrea-d7882fce87.mp3
new file mode 100644
index 0000000..1e8e053
Binary files /dev/null and b/assets/voices/andrea-d7882fce87.mp3 differ
diff --git a/assets/voices/andrea-f07f295fa4.mp3 b/assets/voices/andrea-f07f295fa4.mp3
new file mode 100644
index 0000000..8d83474
Binary files /dev/null and b/assets/voices/andrea-f07f295fa4.mp3 differ
diff --git a/assets/voices/andrea-fd737a321a.mp3 b/assets/voices/andrea-fd737a321a.mp3
new file mode 100644
index 0000000..1a0c6f2
Binary files /dev/null and b/assets/voices/andrea-fd737a321a.mp3 differ
diff --git a/assets/voices/index.json b/assets/voices/index.json
index 493da57..1e694a9 100644
--- a/assets/voices/index.json
+++ b/assets/voices/index.json
@@ -175,17 +175,57 @@
"file": "andrea-6012288000.mp3",
"sec": 7.97
},
- "andrea|Una statua sul piedistallo. Lei il palio l'ha visto passare di sicuro, ma non parla.": {
- "file": "andrea-36af18a0c4.mp3",
- "sec": 5.98
+ "andrea|La statua di San Pietro, con le chiavi in mano. Lui il palio l'ha visto passare di sicuro.": {
+ "file": "andrea-0663f64ddf.mp3",
+ "sec": 5.97
},
- "andrea|Buongiorno.": {
- "file": "andrea-05d9f5aea0.mp3",
- "sec": 0.63
+ "san_pietro|La chiave te l'ho già data, figliolo. Di solito ne do di più importanti.": {
+ "file": "san_pietro-513a9c35e6.mp3",
+ "sec": 5.77
},
- "andrea|Come i vecchietti quando gli chiedi dei debiti.": {
- "file": "andrea-88a6f5518b.mp3",
- "sec": 2.62
+ "andrea|Buongiorno, San Pietro.": {
+ "file": "andrea-0875a7bce9.mp3",
+ "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.": {
"file": "andrea-058d10ea48.mp3",
@@ -307,13 +347,37 @@
"file": "andrea-468fe91000.mp3",
"sec": 3.28
},
+ "andrea|Di là si va alla scuola. Ci passo già abbastanza tempo durante l'anno.": {
+ "file": "andrea-b3acca1e65.mp3",
+ "sec": 5.34
+ },
+ "andrea|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.": {
"file": "andrea-72e4ebb412.mp3",
"sec": 3.88
},
- "andrea|Di là si va alla scuola. Ci passo già abbastanza tempo durante l'anno.": {
- "file": "andrea-b3acca1e65.mp3",
- "sec": 5.34
+ "andrea|Le palazzine gialle accanto alla scuola, con la scala arancione.": {
+ "file": "andrea-528eda3257.mp3",
+ "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.": {
"file": "andrea-bd1ea0e1d2.mp3",
@@ -363,6 +427,10 @@
"file": "andrea-fc32e07a5a.mp3",
"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.": {
"file": "andrea-23e40a2495.mp3",
"sec": 5.74
@@ -383,6 +451,38 @@
"file": "andrea-3a6af3897f.mp3",
"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.": {
"file": "vecchietta-e35443db85.mp3",
"sec": 5.13
diff --git a/assets/voices/san_pietro-3a2e3a543b.mp3 b/assets/voices/san_pietro-3a2e3a543b.mp3
new file mode 100644
index 0000000..52f2d7c
Binary files /dev/null and b/assets/voices/san_pietro-3a2e3a543b.mp3 differ
diff --git a/assets/voices/san_pietro-407bef3025.mp3 b/assets/voices/san_pietro-407bef3025.mp3
new file mode 100644
index 0000000..ae7502b
Binary files /dev/null and b/assets/voices/san_pietro-407bef3025.mp3 differ
diff --git a/assets/voices/san_pietro-4f6f201875.mp3 b/assets/voices/san_pietro-4f6f201875.mp3
new file mode 100644
index 0000000..abd205b
Binary files /dev/null and b/assets/voices/san_pietro-4f6f201875.mp3 differ
diff --git a/assets/voices/san_pietro-513a9c35e6.mp3 b/assets/voices/san_pietro-513a9c35e6.mp3
new file mode 100644
index 0000000..6f64bb2
Binary files /dev/null and b/assets/voices/san_pietro-513a9c35e6.mp3 differ
diff --git a/assets/voices/san_pietro-6c85ba44c5.mp3 b/assets/voices/san_pietro-6c85ba44c5.mp3
new file mode 100644
index 0000000..1711372
Binary files /dev/null and b/assets/voices/san_pietro-6c85ba44c5.mp3 differ
diff --git a/assets/voices/san_pietro-7be2fedd9e.mp3 b/assets/voices/san_pietro-7be2fedd9e.mp3
new file mode 100644
index 0000000..18edfc3
Binary files /dev/null and b/assets/voices/san_pietro-7be2fedd9e.mp3 differ
diff --git a/assets/voices/san_pietro-96b58aedde.mp3 b/assets/voices/san_pietro-96b58aedde.mp3
new file mode 100644
index 0000000..b0d2726
Binary files /dev/null and b/assets/voices/san_pietro-96b58aedde.mp3 differ
diff --git a/assets/voices/san_pietro-9b9c57ff08.mp3 b/assets/voices/san_pietro-9b9c57ff08.mp3
new file mode 100644
index 0000000..41ecda6
Binary files /dev/null and b/assets/voices/san_pietro-9b9c57ff08.mp3 differ
diff --git a/assets/voices/san_pietro-9e24f3914e.mp3 b/assets/voices/san_pietro-9e24f3914e.mp3
new file mode 100644
index 0000000..eb03152
Binary files /dev/null and b/assets/voices/san_pietro-9e24f3914e.mp3 differ
diff --git a/assets/voices/san_pietro-a8ebf578e0.mp3 b/assets/voices/san_pietro-a8ebf578e0.mp3
new file mode 100644
index 0000000..1e3a0e9
Binary files /dev/null and b/assets/voices/san_pietro-a8ebf578e0.mp3 differ
diff --git a/docs/STATO.md b/docs/STATO.md
index e197105..5999e60 100644
--- a/docs/STATO.md
+++ b/docs/STATO.md
@@ -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»).
- **Crediti** dal titolo e dalla fine (`story.credits`), dedica al Lele in testa; musichetta a 8 bit del cartello
(`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`.
- **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).
diff --git a/index.html b/index.html
index 9858ccd..3827f8a 100644
--- a/index.html
+++ b/index.html
@@ -11,7 +11,10 @@
-
+
+
+
+
@@ -43,6 +46,16 @@
+
+
Pausa
+
+
+
+
+
+
+
La partita è salvata: dal titolo si riprende con «Continua».
+
Crediti
Al Lele, che mi ha fatto conoscere Monkey Island.
diff --git a/src/engine/game.js b/src/engine/game.js
index 9e27e83..efae77c 100644
--- a/src/engine/game.js
+++ b/src/engine/game.js
@@ -2,8 +2,8 @@
// 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';
-export const VERBS = ['walk', 'look', 'take', 'use', 'talk', 'open', 'give'];
-export const VERB_LABELS = { walk: 'Vai a', look: 'Guarda', take: 'Prendi', use: 'Usa', talk: 'Parla con', open: 'Apri', give: 'Dai' };
+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', drop: 'Butta' };
const DEFAULT_LINES = {
walk: null,
look: 'Niente di speciale.',
@@ -12,6 +12,7 @@ const DEFAULT_LINES = {
talk: 'Non mi sembra un gran chiacchierone.',
open: 'Non si apre.',
give: 'Non credo che lo voglia.',
+ drop: 'Non è roba mia da buttare.',
with: 'Non funziona.',
};
const SPEED = 95;
@@ -106,6 +107,8 @@ export class Game {
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 === '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] }] : [];
}
diff --git a/src/main.js b/src/main.js
index 1cea6b2..e51987a 100644
--- a/src/main.js
+++ b/src/main.js
@@ -101,6 +101,27 @@ function openMap() {
$('map').hidden = false;
}
$('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; });
function showTitle() {
@@ -195,13 +216,15 @@ function updateChoices() {
window.addEventListener('keydown', e => {
if (e.target === $('volume')) 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 ((e.key === 'p' || e.key === 'P') && running) { openMap(); 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 (!running) 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);
if (game.dialogue && n >= 1 && n <= 9) game.choose(n - 1);
});
@@ -233,6 +256,7 @@ function updateUi() {
updateSpeech();
// 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;
+ $('pause-button').hidden = !running;
if (game.state.ended && !game.speech && !game.queue.length) {
running = false;
$('ending').hidden = false;
diff --git a/src/story/story.js b/src/story/story.js
index af2977d..89c62ec 100644
--- a/src/story/story.js
+++ b/src/story/story.js
@@ -33,6 +33,7 @@ export const story = {
vecchietto: { name: 'Vecchietto', color: '#9fe3ff', sprite: 'assets/characters/vecchietto.json' },
castello: { name: 'Il Castello', color: '#c8f0a0' },
leone: { name: 'Il leone', color: '#f0a070' },
+ san_pietro: { name: 'San Pietro', color: '#e8e0c8' },
},
items: {
squama: {
@@ -47,7 +48,9 @@ export const story = {
libro: {
name: 'libro',
icon: { fill: '#c46a3a', shape: 'book' },
+ droppable: true, // non serve alla storia: si può buttare
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.' }],
open: [
{ 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.' }],
},
},
+ // 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: {
piazza: {
@@ -196,6 +207,7 @@ export const story = {
name: 'La Pieve',
map: [46, 70],
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).
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]],
@@ -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: {
- look: [{ say: 'Una statua sul piedistallo. Lei il palio l\'ha visto passare di sicuro, ma non parla.' }],
- talk: [{ say: 'Buongiorno.' }, { say: '...' }, { say: 'Come i vecchietti quando gli chiedi dei debiti.' }],
+ look: [{ say: 'La statua di San Pietro, con le chiavi in mano. Lui il palio l\'ha visto passare di sicuro.' }],
+ 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' },
@@ -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: {
- 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.' }],
},
},
@@ -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.
viale_cipressi: {
name: 'Viale dei cipressi',
@@ -496,6 +584,28 @@ export const story = {
},
},
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: {
nodes: {
start: {
diff --git a/style.css b/style.css
index 0b092b6..d638761 100644
--- a/style.css
+++ b/style.css
@@ -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); }
#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). */
-#verbs { display: grid; grid-template-columns: repeat(12, 1fr); gap: 4px; align-content: start; }
-#verbs button { grid-column: span 4; }
-#verbs button:nth-child(-n+4) { grid-column: span 3; }
+/* Otto verbi su due righe piene, quattro per riga. */
+#verbs { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; align-content: start; }
#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; }
#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); }
/* 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. */
-#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; }
+#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 h2 { font-size: clamp(18px, 3vw, 32px); }
#map-board { position: relative; width: 92%; flex: 1; min-height: 0; margin: .3em 0;
diff --git a/tests/browser.mjs b/tests/browser.mjs
index cf90377..d633b51 100644
--- a/tests/browser.mjs
+++ b/tests/browser.mjs
@@ -84,6 +84,17 @@ try {
assert.ok(await fits(page), 'verbi e inventario fuori dallo schermo');
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.
assert.ok((await page.evaluate(() => music.current?.src)).endsWith('paese.mp3'), 'musica del paese');
await page.click('#mute');
diff --git a/tests/story.test.mjs b/tests/story.test.mjs
index 5daede8..1591f60 100644
--- a/tests/story.test.mjs
+++ b/tests/story.test.mjs
@@ -169,3 +169,46 @@ test('la mappa: ogni scena ci sta, i posti compaiono solo visti, il viaggio risp
assert.ok(g.dialogue);
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`);
+});
diff --git a/tools/voci.json b/tools/voci.json
index 4fa9f38..8b10ee0 100644
--- a/tools/voci.json
+++ b/tools/voci.json
@@ -4,5 +4,6 @@
"vecchietta": { "speaker": "Tammy Grit" },
"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" },
- "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" }
}