Campomigliaio collegato (strada, circolino, ponte), il Poggio verso la Fortezza; Ringraziamenti coi ritratti e il carosello

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
This commit is contained in:
2026-10-24 19:43:28 +02:00
co-authored by Claude Opus 5.5
parent 3e99fb094c
commit b76e2ad1b8
55 changed files with 390 additions and 76 deletions
+1
View File
@@ -10,6 +10,7 @@ COPY assets/audio /usr/share/nginx/html/assets/audio
COPY assets/icons /usr/share/nginx/html/assets/icons
COPY assets/fonts /usr/share/nginx/html/assets/fonts
COPY assets/voices /usr/share/nginx/html/assets/voices
COPY assets/credits /usr/share/nginx/html/assets/credits
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://127.0.0.1/ >/dev/null || exit 1
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 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.
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.
+116 -32
View File
@@ -547,9 +547,13 @@
"file": "andrea-78a5ef65f0.mp3",
"sec": 7.84
},
"andrea|Sotto il ponte la Sieve. Nel 1758 la piena arrivò fino a Cardetole: in casa mia, di sicuro, c'erano i pesci.": {
"file": "andrea-7301f8df3d.mp3",
"sec": 11.86
"andrea|Sotto il ponte la Carza si butta nella Sieve, e da qui vanno avanti insieme.": {
"file": "andrea-f322731846.mp3",
"sec": 5.04
},
"andrea|Nel 1758 la piena arrivò fino a Cardetole: in casa mia, di sicuro, c'erano i pesci.": {
"file": "andrea-5bf82d4aaa.mp3",
"sec": 6.78
},
"andrea|Il campo secondario della polisportiva. Qui si allenano i piccoli, e i grandi fanno finta di non guardare.": {
"file": "andrea-86efb05e99.mp3",
@@ -635,26 +639,6 @@
"file": "andrea-5bd253332d.mp3",
"sec": 6.36
},
"andrea|Più avanti c'è La Luna, e poi Campomigliaio, il mio rione: i blu.": {
"file": "andrea-a96e07a9f3.mp3",
"sec": 5.92
},
"andrea|Ci torno quando ho ritrovato il palio. A mani vuote non mi ci faccio vedere.": {
"file": "andrea-df05ecea32.mp3",
"sec": 4.54
},
"andrea|Più avanti c'è La Luna, e poi Campomigliaio, il rione dei blu.": {
"file": "andrea-85707a4fca.mp3",
"sec": 4.33
},
"andrea|Prima il palio. Le gite fuori porta dopo.": {
"file": "andrea-f753b8c79c.mp3",
"sec": 3.69
},
"andrea|La Provinciale va avanti verso La Luna e Campomigliaio.": {
"file": "andrea-7883437f7d.mp3",
"sec": 3.46
},
"andrea|La Carza, la stessa che passa sotto il ponticino per Cardetole. Più sassi che acqua.": {
"file": "andrea-6248354f0f.mp3",
"sec": 6.38
@@ -683,6 +667,86 @@
"file": "andrea-dc49e0c100.mp3",
"sec": 6.08
},
"andrea|In cima alla salita, la casa con la torretta. Da quaggiù sembra il castello di Campomigliaio.": {
"file": "andrea-b3120ac9f3.mp3",
"sec": 6.3
},
"andrea|Un muro di sassi a secco, con la ringhiera sopra. Ne ha visti passare di pali.": {
"file": "andrea-9f914e2a8b.mp3",
"sec": 5.35
},
"andrea|Una porta marrone, chiusa.": {
"file": "andrea-29c2c930b4.mp3",
"sec": 1.79
},
"andrea|Chiusa. Saranno tutti al circolino.": {
"file": "andrea-00f2e70a4d.mp3",
"sec": 2.95
},
"andrea|Il circolino: il Circolo SMS di Campomigliaio. Un'istituzione, qui i volontari hanno sempre fatto miracoli.": {
"file": "andrea-90bbad0de0.mp3",
"sec": 9.59
},
"andrea|Il sabato sera ci si mangia una pizza ottima, e si sta meglio che a casa. Si respira fratellanza.": {
"file": "andrea-d44bcc0c68.mp3",
"sec": 6.8
},
"andrea|Se entro ora, mi chiedono tutti del palio. Prima lo ritrovo, poi entro a testa alta.": {
"file": "andrea-33857840d8.mp3",
"sec": 6.55
},
"andrea|Il circolino dei blu, il giorno prima del palio? Meglio di no.": {
"file": "andrea-1b9468d963.mp3",
"sec": 5.05
},
"andrea|Una fontanella di ghisa. Acqua fresca e gratis, la cosa più preziosa di maggio.": {
"file": "andrea-8d9bda2db8.mp3",
"sec": 5.87
},
"andrea|Un sorso d'acqua. Ora sì che si ragiona.": {
"file": "andrea-78ffacf27b.mp3",
"sec": 3.47
},
"andrea|L'insegna del circolino. Senza scritte: tanto qui lo sanno tutti dov'è.": {
"file": "andrea-04c0898047.mp3",
"sec": 5.92
},
"andrea|La panchina davanti al circolino. Di solito è occupata da chi sa tutto di tutti.": {
"file": "andrea-023905aed9.mp3",
"sec": 5.14
},
"andrea|Non è il momento di sedersi. Il palio non si cerca da solo.": {
"file": "andrea-914e32fcca.mp3",
"sec": 4.04
},
"andrea|A diritto si va a Tagliaferro. Il palio non sarà mica scappato fin laggiù.": {
"file": "andrea-fea8986ed0.mp3",
"sec": 5.97
},
"andrea|La strada va avanti verso Tagliaferro.": {
"file": "andrea-bb70dea2a6.mp3",
"sec": 2.15
},
"andrea|Il ponte vecchio, coi suoi due archi. Chissà quante piene ha visto passare: io al primo temporale cerco riparo.": {
"file": "andrea-01ec1d03ec.mp3",
"sec": 7.67
},
"andrea|L'acqua è bassa e limpida. Si vedono i sassi del fondo, e nessun palio.": {
"file": "andrea-93acb42cf7.mp3",
"sec": 5.98
},
"andrea|Mi bagno le scarpe e poi chi la sente, la mamma.": {
"file": "andrea-74470f90d0.mp3",
"sec": 3.9
},
"andrea|Un tronco bianco portato giù dalla piena, piantato in mezzo all'acqua.": {
"file": "andrea-f07cb45320.mp3",
"sec": 4.08
},
"andrea|Troppo pesante, e troppo bagnato. Lo lascio ai girini.": {
"file": "andrea-b814f7d1a0.mp3",
"sec": 4.38
},
"andrea|Il pallaio, dove si gioca a bocce. Qui un punto si discute più di un rigore.": {
"file": "andrea-e94382adb6.mp3",
"sec": 5.76
@@ -723,13 +787,13 @@
"file": "andrea-2de291f241.mp3",
"sec": 4.09
},
"andrea|Laggiù c'è la casa del maestro Bacci. Mezzo paese ha imparato a leggere e scrivere da lui.": {
"file": "andrea-57fb036fc1.mp3",
"sec": 6.54
"andrea|Giù a destra, oltre le vigne, c'è la casa del maestro Bacci. Mezzo paese ha imparato a leggere e scrivere da lui.": {
"file": "andrea-060513a152.mp3",
"sec": 8.65
},
"andrea|Lassù in cima, la torre del Trebbio. Da qui sembra piccola come una pedina.": {
"file": "andrea-5536ba1bf5.mp3",
"sec": 5.65
"andrea|Eccolo lì, il castello del Trebbio, in tutto il suo splendore. Da quaggiù sembra una pedina.": {
"file": "andrea-4f89e093b5.mp3",
"sec": 7.07
},
"andrea|Le vigne in fila. A settembre qui si vendemmia, e tutto il paese ha le mani appiccicose.": {
"file": "andrea-b987450513.mp3",
@@ -771,6 +835,26 @@
"file": "andrea-35430efef3.mp3",
"sec": 6.34
},
"andrea|Da quassù si vede tutto: i tetti del paese, i campi, e in fondo i monti del Mugello.": {
"file": "andrea-23d87e6493.mp3",
"sec": 5.69
},
"andrea|Il palio però da qui non si vede. Ci avevo sperato.": {
"file": "andrea-2e9330c511.mp3",
"sec": 4.45
},
"andrea|Un fico che si sporge dal muretto. A fine maggio i fichi sono ancora duri: torno ad agosto.": {
"file": "andrea-86d675f890.mp3",
"sec": 7.04
},
"andrea|Acerbi. E poi non sono miei.": {
"file": "andrea-2f1517d79d.mp3",
"sec": 3.77
},
"andrea|Le case del Poggio, che salgono verso la Fortezza coi fiori alle finestre e i muri di sasso.": {
"file": "andrea-e044dea7f2.mp3",
"sec": 6.29
},
"andrea|Un leone di terracotta che fa la guardia alla salita. Di serpenti, dice, non ne ha visti passare.": {
"file": "andrea-59a60dc5a7.mp3",
"sec": 6.83
@@ -855,9 +939,9 @@
"file": "andrea-e549a3c205.mp3",
"sec": 4.69
},
"andrea|La piccola edicola in fondo al ponticino. C'è sempre un fiore fresco, ma nessuno l'ha mai visto metterlo.": {
"file": "andrea-59afc50d80.mp3",
"sec": 7.04
"andrea|Il piccolo tabernacolo in fondo al ponticino. C'è sempre un fiore fresco, ma nessuno l'ha mai visto metterlo.": {
"file": "andrea-ed03a4f29a.mp3",
"sec": 7.41
},
"andrea|La panchina del parco. Ci si sedevano i camperisti a guardare le montagne, con la cartina al contrario.": {
"file": "andrea-e7f720b94d.mp3",
+7 -5
View File
@@ -30,7 +30,7 @@
<section id="title" class="overlay" hidden>
<h1>La Leggenda<br>del <span>Regolo</span></h1>
<p class="subtitle">San Piero a Sieve · Palio della Fortezza · 2007</p>
<div class="buttons"><button id="continue" hidden>Continua</button><button id="new-game">Nuova partita</button><button id="load-game" class="secondary">Carica</button><button class="open-credits secondary">Crediti</button></div>
<div class="buttons"><button id="continue" hidden>Continua</button><button id="new-game">Nuova partita</button><button id="load-game" class="secondary">Carica</button><button class="open-credits secondary">Ringraziamenti</button></div>
<input id="load-file" type="file" accept=".json,application/json" hidden>
<p id="load-error" class="load-error" hidden role="alert"></p>
<p class="dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p>
@@ -49,7 +49,7 @@
<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>
<div class="buttons"><button id="restart">Ricomincia</button><button class="open-credits secondary">Ringraziamenti</button></div>
</section>
<section id="map" class="overlay" hidden aria-label="Mappa del paese">
<div id="map-head"><h2>San Piero a Sieve</h2><input id="map-search" type="search" placeholder="Cerca un posto…" autocomplete="off" spellcheck="false" aria-label="Cerca un posto"></div>
@@ -63,17 +63,19 @@
<button id="resume">Riprendi</button>
<button id="pause-map" class="secondary">Mappa</button>
<button id="save-file" class="secondary" title="Scarica la partita in un file da aprire su un altro dispositivo">Salva su file</button>
<button class="open-credits secondary">Crediti</button>
<button class="open-credits secondary">Ringraziamenti</button>
<button id="to-title" class="secondary">Torna al titolo</button>
</div>
<p class="hint">La partita è salvata: dal titolo si riprende con «Continua». Con «Salva su file» la porti su un altro dispositivo e la apri con «Carica».</p>
</section>
<section id="credits" class="overlay" hidden aria-label="Crediti">
<h2>Crediti</h2>
<section id="credits" class="overlay" hidden aria-label="Ringraziamenti">
<h2>Ringraziamenti</h2>
<p class="dedication credits-dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p>
<dl id="credits-list"></dl>
<div id="credits-reel" aria-hidden="true"><div class="track"></div></div>
<p class="legal">Monkey Island è un marchio di Lucasfilm Ltd., citato solo nella dedica: questo gioco non è affiliato né approvato da Lucasfilm.</p>
<div class="buttons"><button id="close-credits">Chiudi</button></div>
<figure id="face" hidden><img alt=""><figcaption></figcaption></figure>
</section>
</div>
<div id="side">
+52 -2
View File
@@ -106,11 +106,61 @@ function openCredits() {
$('credits-list').replaceChildren(...shuffledCredits(story.credits ?? []).flatMap(({ role, names }) => {
const dt = document.createElement('dt'), dd = document.createElement('dd');
dt.textContent = role;
dd.replaceChildren(...names.map(n => Object.assign(document.createElement('span'), { textContent: n })));
dd.replaceChildren(...names.map(n => {
if (typeof n === 'string') return Object.assign(document.createElement('span'), { textContent: n });
// Chi ha il ritratto: il nome è un pulsante che lo mostra.
const b = Object.assign(document.createElement('button'), { type: 'button', className: 'credit-face', textContent: n.name });
b.addEventListener('click', () => showFace(n));
return b;
}));
return [dt, dd];
}));
fillReel();
$('credits').hidden = false;
rollCredits();
}
// Sotto i nomi, un carosello che scorre da solo: gli sfondi del paese e i ritratti, due giri uguali in fila per non
// vedere mai la cucitura.
function fillReel() {
const track = $('credits-reel').querySelector('.track');
if (track.children.length) return;
const pictures = [...new Set(Object.values(story.scenes).map(s => s.background).filter(Boolean))];
const faces = (story.credits ?? []).flatMap(c => c.names).filter(n => n.face).map(n => n.face);
// I ritratti sparsi fra gli sfondi, uno ogni quattro.
const all = pictures.flatMap((p, i) => i % 4 === 3 && faces.length ? [p, faces[(i >> 2) % faces.length]] : [p]);
for (const src of [...all, ...all]) {
const img = Object.assign(document.createElement('img'), { src, alt: '', loading: 'lazy', decoding: 'async' });
if (src.includes('/credits/')) img.className = 'face';
track.append(img);
}
}
// I nomi salgono piano da soli; si fermano se ci si passa sopra o si tocca, e ricominciano dall'alto in fondo.
let rolling = 0, rollWait = 0;
$('credits-list').addEventListener('pointerdown', () => { rollWait = 4000; });
$('credits-list').addEventListener('wheel', () => { rollWait = 4000; }, { passive: true });
function rollCredits() {
const list = $('credits-list');
list.scrollTop = 0;
cancelAnimationFrame(rolling);
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
let last = performance.now(), pos = 0;
rollWait = 1500;
const step = now => {
if ($('credits').hidden) return;
const dt = now - last; last = now;
if (list.matches(':hover') || !$('face').hidden || (rollWait -= dt) > 0) pos = list.scrollTop;
else if (list.scrollTop + list.clientHeight >= list.scrollHeight - 1) { rollWait = 2500; list.scrollTop = pos = 0; }
else list.scrollTop = pos += dt * 0.025;
rolling = requestAnimationFrame(step);
};
rolling = requestAnimationFrame(step);
}
function showFace({ name, face }) {
$('face').querySelector('img').src = face;
$('face').querySelector('figcaption').textContent = name;
$('face').hidden = false;
}
$('face').addEventListener('click', () => { $('face').hidden = true; });
for (const b of document.querySelectorAll('.open-credits')) b.addEventListener('click', openCredits);
$('close-credits').addEventListener('click', () => { $('credits').hidden = true; });
@@ -414,7 +464,7 @@ window.addEventListener('keydown', e => {
return;
}
if (debugKey(e)) return;
if (!$('credits').hidden) { if (e.key === 'Escape') $('credits').hidden = true; return; }
if (!$('credits').hidden) { if (e.key === 'Escape') { if (!$('face').hidden) $('face').hidden = true; else $('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; }
+179 -27
View File
@@ -63,18 +63,19 @@ export const story = {
// splash: la musichetta del cartello d'apertura (tools/jingle.py), suonata una volta sola.
music: { splash: 'assets/audio/cartello.mp3', title: 'assets/audio/titolo.mp3', game: 'assets/audio/paese.mp3', ending: 'assets/audio/ricordi.mp3' },
// I crediti, schermata «Crediti» dal titolo e dalla fine: un ruolo e chi l'ha fatto. Arriveranno man mano.
// Un nome può avere il suo ritratto a pixel (face): nei Ringraziamenti si tocca il nome e compare il volto.
credits: [
{ role: 'Un gioco di', names: ['Andrea Manzi «Manzolo»'] },
{ role: 'Idee e consigli', names: ['Yuri Baldi', 'BaronDeDuke (Gabriele Rossi)', 'Tozzino (Francesco Tozzi)', 'Marco Arcuri «l\'Arcu»'] },
{ role: 'Storia e aneddoti di San Piero', names: ['Francesco Bacci'] },
{ role: 'Musica', names: ['Andrea Manzi, con Suno'] },
{ role: 'Un gioco di', names: [{ name: 'Manzolo (Andrea Manzi)', face: 'assets/credits/manzolo.png' }] },
{ role: 'Idee e consigli', names: [{ name: 'Baldoria (Yuri Baldi)', face: 'assets/credits/baldoria.png' }, 'BaronDeDuke (Gabriele Rossi)', { name: 'Tozzino (Francesco Tozzi)', face: 'assets/credits/tozzino.png' }, { name: 'l\'Arcu (Marco Arcuri)', face: 'assets/credits/arcu.png' }] },
{ role: 'Storia e aneddoti di San Piero', names: [{ name: 'Francesco Bacci', face: 'assets/credits/bacci.png' }] },
{ role: 'Musica', names: ['Manzolo, con Suno'] },
{ role: 'Sfondi', names: ['dalle fotografie di San Piero di Andrea'] },
// Chi ci ha mandato foto, ricordi o idee: una riga per contributo, si allunga man mano.
{ role: 'Fotografie', names: ['Mariano Mirannalti (l\'interno della Pieve)'] },
{ role: 'Fotografie', names: [{ name: 'Mariano Mirannalti (l\'interno della Pieve)', face: 'assets/credits/mariano.png' }] },
{ role: 'Opere d\'arte nel gioco', names: ['Simone Rocchi (l\'omino di ferro del tennis della chiesa)'] },
{ role: 'Voci', names: ['XTTS-v2 di Coqui'] },
{ role: 'Carattere', names: ['Press Start 2P di CodeMan38 (licenza OFL)'] },
{ role: 'Programmazione', names: ['Andrea Manzi, con Claude e Codex'] },
{ role: 'Programmazione', names: ['Manzolo, con Claude e Codex'] },
],
characters: {
andrea: { name: 'Andrea', color: '#ffffff', sprite: 'assets/characters/andrea.json' },
@@ -406,7 +407,7 @@ export const story = {
id: 'sentiero_trebbio', name: 'sentiero per il Trebbio', box: [0, 200, 90, 160], at: [20, 330], exit: { scene: 'casa_maestro_bacci', at: [120, 350], face: 'up' },
},
{
id: 'verso_villa_adami', name: 'discesa verso Villa Adami', box: [180, 326, 340, 34], at: [330, 352], exit: { scene: 'salita_villa_adami', at: [330, 300], face: 'left' },
id: 'verso_villa_adami', name: 'giù per il Poggio, verso Villa Adami', box: [180, 326, 340, 34], at: [330, 352], exit: { scene: 'salita_fortezza', at: [150, 318], face: 'up' },
},
],
},
@@ -866,7 +867,7 @@ export const story = {
entry: [300, 350],
hotspots: [
{
id: 'edicola', name: 'edicola', box: [138, 148, 36, 60], at: [180, 236], face: 'up',
id: 'tabernacolo', name: 'tabernacolo', box: [138, 148, 36, 60], at: [180, 236], face: 'up',
actions: {
look: [{ say: 'Il tabernacolo prima del ponte. Chi passa per andare via dal paese, un saluto glielo fa sempre.' }],
},
@@ -874,7 +875,7 @@ export const story = {
{
id: 'sieve', name: 'la Sieve', box: [380, 170, 260, 40], at: [340, 210], face: 'right',
actions: {
look: [{ say: 'Sotto il ponte la Sieve. Nel 1758 la piena arrivò fino a Cardetole: in casa mia, di sicuro, c\'erano i pesci.' }],
look: [{ say: 'Sotto il ponte la Carza si butta nella Sieve, e da qui vanno avanti insieme.' }, { say: 'Nel 1758 la piena arrivò fino a Cardetole: in casa mia, di sicuro, c\'erano i pesci.' }],
},
},
{
@@ -1088,15 +1089,7 @@ export const story = {
entry: [150, 206],
hotspots: [
{
id: 'verso_la_luna', name: 'avanti, verso La Luna e Campomigliaio', box: [0, 334, 330, 26], at: [150, 352], face: 'down', default: 'walk',
actions: {
walk: [{
if: 'rione:campomigliaio',
then: [{ say: 'Più avanti c\'è La Luna, e poi Campomigliaio, il mio rione: i blu.' }, { say: 'Ci torno quando ho ritrovato il palio. A mani vuote non mi ci faccio vedere.' }],
else: [{ say: 'Più avanti c\'è La Luna, e poi Campomigliaio, il rione dei blu.' }, { say: 'Prima il palio. Le gite fuori porta dopo.' }],
}],
look: [{ say: 'La Provinciale va avanti verso La Luna e Campomigliaio.' }],
},
id: 'verso_la_luna', name: 'avanti, verso La Luna e Campomigliaio', box: [0, 334, 330, 26], at: [150, 352], exit: { scene: 'campomigliaio_strada', at: [300, 350], face: 'up' },
},
{
id: 'carza', name: 'la Carza', box: [340, 205, 300, 155], at: [250, 300], face: 'right',
@@ -1129,6 +1122,129 @@ export const story = {
},
],
},
// Campomigliaio, il rione dei blu (foto dell'Arcu): la strada in salita, il Circolo, il ponte vecchio.
campomigliaio_strada: {
name: 'Campomigliaio, la strada',
map: [60, 78],
background: 'assets/scenes/campomigliaio-strada.png',
walk: [[0, 360], [0, 300], [250, 222], [300, 210], [350, 210], [420, 240], [640, 345], [640, 360]],
depth: [[210, 0.35], [360, 1.7]],
entry: [300, 350],
hotspots: [
{
id: 'al_circolo', name: 'su, verso il circolino', box: [270, 195, 90, 25], at: [320, 214], exit: { scene: 'campomigliaio_circolo', at: [150, 345], face: 'up' },
},
{
id: 'torretta', name: 'casa con la torretta', box: [280, 120, 80, 62], at: [320, 222], face: 'up',
actions: {
look: [{ say: 'In cima alla salita, la casa con la torretta. Da quaggiù sembra il castello di Campomigliaio.' }],
},
},
{
id: 'muro', name: 'muro di pietra', box: [430, 190, 210, 140], at: [420, 300], face: 'right',
actions: {
look: [{ say: 'Un muro di sassi a secco, con la ringhiera sopra. Ne ha visti passare di pali.' }],
},
},
{
id: 'porta', name: 'porta marrone', box: [150, 165, 32, 56], at: [180, 270], face: 'left',
actions: {
look: [{ say: 'Una porta marrone, chiusa.' }],
open: [{ say: 'Chiusa. Saranno tutti al circolino.' }],
},
},
{
id: 'alla_carza', name: 'indietro, verso la Provinciale', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'verso_luna', at: [150, 330], face: 'up' },
},
],
},
campomigliaio_circolo: {
name: 'Campomigliaio, il circolino',
map: [55, 82],
background: 'assets/scenes/campomigliaio-circolo.png',
walk: [[0, 360], [0, 335], [60, 300], [400, 280], [640, 250], [640, 360]],
depth: [[250, 1.0], [360, 1.5]],
entry: [320, 330],
hotspots: [
{
id: 'circolo', name: 'il circolino', box: [100, 0, 380, 205], at: [300, 290], face: 'up',
actions: {
look: [{ say: 'Il circolino: il Circolo SMS di Campomigliaio. Un\'istituzione, qui i volontari hanno sempre fatto miracoli.' }, { say: 'Il sabato sera ci si mangia una pizza ottima, e si sta meglio che a casa. Si respira fratellanza.' }],
open: [{
if: 'rione:campomigliaio',
then: [{ say: 'Se entro ora, mi chiedono tutti del palio. Prima lo ritrovo, poi entro a testa alta.' }],
else: [{ say: 'Il circolino dei blu, il giorno prima del palio? Meglio di no.' }],
}],
},
},
{
id: 'fontanella', name: 'fontanella', box: [364, 212, 26, 58], at: [376, 286], face: 'up',
actions: {
look: [{ say: 'Una fontanella di ghisa. Acqua fresca e gratis, la cosa più preziosa di maggio.' }],
use: [{ say: 'Un sorso d\'acqua. Ora sì che si ragiona.' }],
},
},
{
id: 'insegna', name: 'insegna del circolino', box: [484, 86, 28, 62], at: [500, 272], face: 'up',
actions: {
look: [{ say: 'L\'insegna del circolino. Senza scritte: tanto qui lo sanno tutti dov\'è.' }],
},
},
{
id: 'panchina', name: 'panchina', box: [520, 206, 52, 30], at: [545, 262], face: 'up',
actions: {
look: [{ say: 'La panchina davanti al circolino. Di solito è occupata da chi sa tutto di tutti.' }],
use: [{ say: 'Non è il momento di sedersi. Il palio non si cerca da solo.' }],
},
},
{
id: 'alla_strada', name: 'giù per la strada', box: [0, 334, 300, 26], at: [150, 352], exit: { scene: 'campomigliaio_strada', at: [320, 216], face: 'down' },
},
{
id: 'al_ponte', name: 'a sinistra, verso il ponte', box: [44, 220, 40, 80], at: [70, 312], exit: { scene: 'campomigliaio_ponte', at: [80, 330], face: 'right' },
},
{
id: 'tagliaferro', name: 'a diritto, verso Tagliaferro', box: [596, 210, 44, 60], at: [622, 258], face: 'right', default: 'walk',
actions: {
walk: [{ say: 'A diritto si va a Tagliaferro. Il palio non sarà mica scappato fin laggiù.' }],
look: [{ say: 'La strada va avanti verso Tagliaferro.' }],
},
},
],
},
campomigliaio_ponte: {
name: 'Campomigliaio, il ponte vecchio',
map: [50, 86],
background: 'assets/scenes/campomigliaio-ponte.png',
walk: [[0, 360], [0, 232], [100, 225], [140, 240], [170, 275], [260, 330], [300, 360]],
depth: [[225, 0.6], [360, 1.5]],
entry: [80, 330],
hotspots: [
{
id: 'ponte', name: 'il ponte vecchio', box: [60, 100, 290, 95], at: [110, 240], face: 'up',
actions: {
look: [{ say: 'Il ponte vecchio, coi suoi due archi. Chissà quante piene ha visto passare: io al primo temporale cerco riparo.' }],
},
},
{
id: 'torrente', name: 'il torrente', box: [150, 200, 320, 160], at: [200, 300], face: 'right',
actions: {
look: [{ say: 'L\'acqua è bassa e limpida. Si vedono i sassi del fondo, e nessun palio.' }],
use: [{ say: 'Mi bagno le scarpe e poi chi la sente, la mamma.' }],
},
},
{
id: 'tronco', name: 'tronco', box: [318, 230, 74, 56], at: [215, 315], face: 'right',
actions: {
look: [{ say: 'Un tronco bianco portato giù dalla piena, piantato in mezzo all\'acqua.' }],
take: [{ say: 'Troppo pesante, e troppo bagnato. Lo lascio ai girini.' }],
},
},
{
id: 'al_circolo', name: 'indietro, verso il circolino', box: [0, 334, 280, 26], at: [120, 354], exit: { scene: 'campomigliaio_circolo', at: [76, 314], face: 'right' },
},
],
},
// «Il tennis della chiesa»: il campo dietro la Pieve, dal cancello verde della Carza.
tennis_chiesa: {
name: 'Il tennis della chiesa',
@@ -1185,26 +1301,26 @@ export const story = {
entry: [120, 350],
hotspots: [
{
id: 'casa', name: 'casa del maestro Bacci', box: [186, 186, 100, 30], at: [140, 240], face: 'up',
id: 'casa', name: 'giù a destra, la casa del maestro Bacci', box: [500, 296, 140, 64], at: [200, 340], face: 'right',
actions: {
look: [{ say: 'Laggiù c\'è la casa del maestro Bacci. Mezzo paese ha imparato a leggere e scrivere da lui.' }],
look: [{ say: 'Giù a destra, oltre le vigne, c\'è la casa del maestro Bacci. Mezzo paese ha imparato a leggere e scrivere da lui.' }],
},
},
{
id: 'trebbio', name: 'castello del Trebbio', box: [400, 112, 50, 30], at: [140, 240], face: 'up',
id: 'trebbio', name: 'castello del Trebbio', box: [298, 76, 36, 22], at: [140, 240], face: 'up',
actions: {
look: [{ say: 'Lassù in cima, la torre del Trebbio. Da qui sembra piccola come una pedina.' }],
look: [{ say: 'Eccolo lì, il castello del Trebbio, in tutto il suo splendore. Da quaggiù sembra una pedina.' }],
},
},
{
id: 'vigne', name: 'vigne', box: [160, 250, 480, 110], at: [190, 320], face: 'right',
id: 'vigne', name: 'vigne', box: [160, 250, 340, 110], at: [190, 320], face: 'right',
actions: {
look: [{ say: 'Le vigne in fila. A settembre qui si vendemmia, e tutto il paese ha le mani appiccicose.' }],
take: [{ say: 'L\'uva a maggio? È ancora un\'idea d\'uva.' }],
},
},
{
id: 'ai_cipressi', name: 'avanti, verso i cipressi e il Trebbio', box: [110, 200, 60, 34], at: [138, 226], exit: { scene: 'viale_cipressi', at: [300, 350], face: 'up' },
id: 'ai_cipressi', name: 'avanti, verso i cipressi e il Trebbio', box: [120, 150, 56, 30], at: [138, 226], exit: { scene: 'viale_cipressi', at: [300, 350], face: 'up' },
},
{
id: 'alla_fortezza', name: 'indietro, verso la Fortezza', box: [0, 330, 240, 30], at: [120, 354], exit: { scene: 'fortezza', at: [20, 330], face: 'right' },
@@ -1275,6 +1391,42 @@ export const story = {
},
],
},
// Il Poggio, dalla salita del leone alla Fortezza: la stradina col muretto e tutto il Mugello sotto (foto di Andrea, 24 ottobre).
salita_fortezza: {
name: 'Il Poggio',
map: [37, 12],
background: 'assets/scenes/salita-fortezza.png',
walk: [[0, 360], [0, 322], [80, 264], [150, 246], [195, 246], [222, 262], [296, 360]],
depth: [[246, 0.45], [360, 1.4]],
entry: [150, 345],
hotspots: [
{
id: 'giu_leone', name: 'giù, verso Villa Adami', box: [130, 222, 90, 30], at: [175, 250], exit: { scene: 'salita_villa_adami', at: [326, 259], face: 'right' },
},
{
id: 'su_fortezza', name: 'su, alla Fortezza', box: [0, 334, 296, 26], at: [150, 352], exit: { scene: 'fortezza', at: [330, 300], face: 'left' },
},
{
id: 'panorama', name: 'il Mugello', box: [300, 120, 340, 80], at: [190, 300], face: 'right',
actions: {
look: [{ say: 'Da quassù si vede tutto: i tetti del paese, i campi, e in fondo i monti del Mugello.' }, { say: 'Il palio però da qui non si vede. Ci avevo sperato.' }],
},
},
{
id: 'fico', name: 'fico', box: [460, 200, 180, 160], at: [240, 320], face: 'right',
actions: {
look: [{ say: 'Un fico che si sporge dal muretto. A fine maggio i fichi sono ancora duri: torno ad agosto.' }],
take: [{ say: 'Acerbi. E poi non sono miei.' }],
},
},
{
id: 'case', name: 'case', box: [40, 0, 100, 240], at: [60, 320], face: 'left',
actions: {
look: [{ say: 'Le case del Poggio, che salgono verso la Fortezza coi fiori alle finestre e i muri di sasso.' }],
},
},
],
},
// Tra il livello 1 e il 2: la salita di Villa Adami, con la casa del leone. Si sale alla Fortezza.
salita_villa_adami: {
name: 'Salita di Villa Adami',
@@ -1300,7 +1452,7 @@ export const story = {
},
},
{
id: 'su_fortezza', name: 'salita verso la Fortezza', box: [168, 196, 182, 46], at: [326, 259], face: 'left', exit: { scene: 'fortezza', at: [175, 352], face: 'right' },
id: 'su_fortezza', name: 'su per il Poggio, verso la Fortezza', box: [168, 196, 182, 46], at: [326, 259], face: 'left', exit: { scene: 'salita_fortezza', at: [175, 252], face: 'down' },
},
{
id: 'verso_villa_adami', name: 'discesa verso Villa Adami', box: [200, 334, 280, 26], at: [340, 356], exit: { scene: 'villa_adami', at: [300, 345], face: 'right' },
@@ -1365,9 +1517,9 @@ export const story = {
},
},
{
id: 'edicola', name: 'edicola della Madonna', box: [442, 148, 36, 44], at: [458, 204], face: 'up',
id: 'tabernacolo', name: 'tabernacolo della Madonna', box: [442, 148, 36, 44], at: [458, 204], face: 'up',
actions: {
look: [{ say: 'La piccola edicola in fondo al ponticino. C\'è sempre un fiore fresco, ma nessuno l\'ha mai visto metterlo.' }],
look: [{ say: 'Il piccolo tabernacolo in fondo al ponticino. C\'è sempre un fiore fresco, ma nessuno l\'ha mai visto metterlo.' }],
},
},
{
+16 -1
View File
@@ -173,9 +173,24 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
#credits { justify-content: flex-start; overflow: hidden; padding-top: 4%; background: rgba(15, 10, 20, .96); }
#credits .credits-dedication { margin: .2em 0 .5em !important; font-size: clamp(10px, 1.6vw, 17px); }
/* Due colonne: il ruolo a destra della sua colonna, il nome a sinistra della sua, allineati sulla riga. */
#credits dl { flex: 1 1 auto; min-height: 0; overflow-y: auto; align-content: start; padding-right: .4em; margin: .2em 0 0; display: grid; grid-template-columns: 1fr 1fr; column-gap: 1em; row-gap: .45em; align-items: baseline; width: min(100%, 44em); }
#credits dl { flex: 1 1 auto; min-height: 0; overflow-y: auto; align-content: start; padding-right: .4em; margin: .2em 0 0; display: grid; grid-template-columns: 1fr 1fr; column-gap: 1em; row-gap: .45em; align-items: baseline; width: min(100%, 44em); scrollbar-width: thin; }
#credits dt { text-align: right; color: var(--dim); font-size: clamp(7.2px, 1.01vw, 10.08px); letter-spacing: .12em; text-transform: uppercase; }
#credits dd span { display: block; }
/* I nomi col ritratto: si toccano e compare il volto a pixel sopra i Ringraziamenti. */
#credits-reel { flex: 0 0 auto; height: clamp(56px, 24vh, 200px); width: 100%; overflow: hidden; margin: .5em 0 .3em;
-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
#credits-reel .track { display: flex; gap: 10px; height: 100%; width: max-content; animation: reel 240s linear infinite; }
#credits-reel img { flex: none; height: 100%; aspect-ratio: 16 / 9; object-fit: cover; image-rendering: pixelated; border-radius: 6px; border: 2px solid #3a2d42; }
#credits-reel img.face { aspect-ratio: 1; border-color: var(--accent); }
@keyframes reel { to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) { #credits-reel .track { animation: none; } }
#credits .credit-face { all: unset; display: block; cursor: pointer; color: var(--accent); text-decoration: underline dotted; text-underline-offset: .25em; }
#credits .credit-face:hover, #credits .credit-face:focus-visible { color: #fff; }
#face { position: absolute; inset: 0; margin: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .8em;
background: rgba(15, 10, 20, .92); cursor: pointer; z-index: 1; }
#face[hidden] { display: none; }
#face img { width: min(60vmin, 384px); aspect-ratio: 1; image-rendering: pixelated; border: 3px solid var(--accent); border-radius: 8px; }
#face figcaption { color: var(--ink); font-size: clamp(9px, 1.4vw, 14px); text-align: center; padding: 0 1em; }
#credits dd { margin: 0; text-align: left; color: var(--ink); font-size: clamp(8px, 1.15vw, 12px); line-height: 1.6; font-weight: bold; }
#credits .legal { color: var(--dim); font-size: clamp(7.2px, 0.86vw, 9.36px); opacity: .8; }
+11 -4
View File
@@ -5,7 +5,7 @@ import { chromium } from 'playwright';
import { spawn } from 'node:child_process';
import { mkdirSync, readFileSync } from 'node:fs';
import assert from 'node:assert/strict';
import { solution } from '../src/story/solution.js';
import { solution, route } from '../src/story/solution.js';
const OUT = process.env.OUT ?? '/tmp/regolo-shots', PORT = 8000 + Math.floor(Math.random() * 900);
mkdirSync(OUT, { recursive: true });
@@ -64,9 +64,16 @@ try {
await page.screenshot({ path: `${OUT}/01-titolo.png` });
// I crediti dal titolo: si aprono, mostrano i nomi, Esc li chiude.
await page.click('#title .open-credits');
assert.match(await page.textContent('#credits'), /Crediti\s*Al Lele/, 'la dedica al Lele viene per prima');
assert.match(await page.textContent('#credits'), /Yuri Baldi/);
assert.match(await page.textContent('#credits'), /Ringraziamenti\s*Al Lele/, 'la dedica al Lele viene per prima');
assert.match(await page.textContent('#credits'), /Baldoria \(Yuri Baldi\)/);
assert.match(await page.textContent('#credits'), /Francesco Bacci/);
// I nomi col ritratto: si tocca il nome, compare il volto; un altro tocco lo chiude.
await page.click('#credits .credit-face >> text=Mariano');
assert.ok(await page.isVisible('#face img'));
await page.waitForFunction(() => document.querySelector('#face img').naturalWidth > 0, null, { timeout: 5000 });
await page.screenshot({ path: `${OUT}/01d-ritratto.png` });
await page.click('#face');
assert.ok(await page.isHidden('#face'));
const onTop = await page.evaluate(() => { const r = document.getElementById('close-credits').getBoundingClientRect();
return document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2)?.id === 'close-credits'; });
assert.ok(onTop, 'i crediti stanno sopra il titolo, non dietro');
@@ -214,7 +221,7 @@ try {
// Una partita a metà riprende da dove era rimasta.
await page.click('#restart');
await settle(page);
for (const exit of ['verso_villa_adami', 'su_villa_adami', 'salita', 'su_fortezza', 'sentiero_trebbio', 'ai_cipressi', 'al_trebbio']) {
for (const [, exit] of route('piazza', 'trebbio')) {
await clickScene(page, await hotspotCenter(page, exit));
await settle(page);
}
+8 -5
View File
@@ -77,14 +77,17 @@ test('la storia è coerente: scene, oggetti, dialoghi e punti calpestabili esist
}
});
const nameOf = n => typeof n === 'string' ? n : n.name;
test('i crediti: ogni voce ha un ruolo e almeno un nome', () => {
assert.ok(story.credits.length > 0);
for (const c of story.credits) {
assert.ok(c.role?.trim(), 'ruolo vuoto');
assert.ok(c.names?.length && c.names.every(n => n.trim()), `nomi vuoti in ${c.role}`);
assert.ok(c.names?.length && c.names.every(n => nameOf(n).trim()), `nomi vuoti in ${c.role}`);
for (const n of c.names) if (n.face) assert.ok(existsSync(new URL('../' + n.face, import.meta.url)), `manca il ritratto ${n.face}`);
}
const all = story.credits.flatMap(c => c.names);
for (const name of ['Yuri Baldi', 'Francesco Bacci']) assert.ok(all.includes(name), `manca ${name}`);
const all = story.credits.flatMap(c => c.names.map(nameOf));
for (const name of ['Baldoria (Yuri Baldi)', 'l\'Arcu (Marco Arcuri)', 'Francesco Bacci']) assert.ok(all.includes(name), `manca ${name}`);
});
test('i crediti: i nomi di ogni voce escono in ordine casuale, sempre tutti', () => {
@@ -95,8 +98,8 @@ test('i crediti: i nomi di ogni voce escono in ordine casuale, sempre tutti', ()
const random = () => ((x = (x * 16807) % 2147483647) / 2147483647);
const out = shuffledCredits(story.credits, random);
assert.deepEqual(out.map(c => c.role), story.credits.map(c => c.role), 'le voci restano nel loro ordine');
out.forEach((c, i) => assert.deepEqual([...c.names].sort(), [...story.credits[i].names].sort()));
orders.add(out.find(c => c.role === idee).names.join('|'));
out.forEach((c, i) => assert.deepEqual(c.names.map(nameOf).sort(), story.credits[i].names.map(nameOf).sort()));
orders.add(out.find(c => c.role === idee).names.map(nameOf).join('|'));
}
assert.ok(orders.size > 3, `${idee}: l'ordine non cambia`);
});