Compare commits

...
4 Commits
Author SHA1 Message Date
manzolo 64dc8e18e7 Versione 0.1.20 2026-10-27 23:02:27 +01:00
manzoloandClaude Fable 5.1 b262977117 Il carosello dei Ringraziamenti si ferma, si sfoglia e ingrandisce le foto
Tutti gli sfondi del paese (game.reelPictures, uno per scena, col nome del posto) e ogni ritratto una volta sola
(prima tre si ripetevano). Scorre da solo ma si ferma sotto il mouse, si trascina col mouse o col dito, si spinge
con la rotella, con le frecce ai lati e con quelle della tastiera; un tocco su una foto la mostra grande col nome
(il tocco si legge al rilascio: con la cattura del puntatore il clic non arriva all'immagine). Test della fila e
prova nel browser.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-27 22:57:01 +01:00
manzolo 3762f2b465 Versione 0.1.19 2026-10-27 22:40:04 +01:00
manzoloandClaude Fable 5.1 9f7a9812ed Via Cipriani col Cinema Taiuti e il Rattattù, Via Massorondinaio, i ragazzi delle canne in Fortezza, le Mozzète fino a Gabbiano
- Via Pietro Cipriani fra la Piazza e Via Massorondinaio (poi il campeggio): il Cinema Taiuti aperto come nel 2007
  con la locandina del Rattattù (stile della locandina vera di Andrea, scaletta illeggibile), il campo da basket
  del Circolo 4 Maggio (gioco da fare).
- In Fortezza quattro ragazzi inventati (sprite di Codex) si fanno le canne da pesca col bambù per il lago
  dell'Anatre dietro Cardetole; battuta di Andrea sulla mamma che aveva ragione.
- Piazzetta Cambray Digny: a sinistra è il muro di Villa Schifanoia, non la Fortezza (Andrea, con Wikipedia).
- Dal campo secondario, a destra: le Mozzète, il ponticino, Coldaia, la falegnameria, Gabbiano.
Sfondi di Codex da Street View (in IMMAGINI.md: da rifare con foto di Andrea prima di Pages); battute provvisorie e voci.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-27 22:34:41 +01:00
54 changed files with 489 additions and 41 deletions
+38
View File
@@ -0,0 +1,38 @@
{
"image": "pescatori.png",
"height": 110,
"frames": {
"work": {
"x": 0,
"y": 0,
"w": 170,
"h": 110,
"ax": 84.0,
"ay": 110
},
"talk": {
"x": 172,
"y": 0,
"w": 166,
"h": 112,
"ax": 79.2,
"ay": 112
},
"laugh": {
"x": 340,
"y": 0,
"w": 164,
"h": 113,
"ax": 79.7,
"ay": 113
},
"proud": {
"x": 506,
"y": 0,
"w": 166,
"h": 121,
"ax": 80.7,
"ay": 121
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

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.
Binary file not shown.
Binary file not shown.
+90 -6
View File
@@ -203,6 +203,26 @@
"file": "andrea-69337973d0.mp3",
"sec": 5.94
},
"andrea|Quattro ragazzi che affilano canne di bambù col coltellino e ci legano le lenze.": {
"file": "andrea-98ff3bf0a3.mp3",
"sec": 6.41
},
"andrea|Oh, che fate?": {
"file": "andrea-22b8efe94b.mp3",
"sec": 1.75
},
"pescatori|Si preparano le canne: domattina si va a pescare al lago dell'Anatre, dietro Cardetole!": {
"file": "pescatori-486103d189.mp3",
"sec": 3.74
},
"pescatori|Guarda questa: la lenza la regge anche una carpa di dieci chili.": {
"file": "pescatori-afda722e0c.mp3",
"sec": 3.2
},
"andrea|(Vedi, mia mamma aveva ragione: in Fortezza si viene a farsi le canne.)": {
"file": "andrea-e0c6360b96.mp3",
"sec": 4.35
},
"andrea|Nove bastioni, li volle Cosimo I. Per tenere fuori i nemici, dicono. Secondo me per tenere dentro il Regolo.": {
"file": "andrea-d31f6e291b.mp3",
"sec": 8.71
@@ -507,6 +527,62 @@
"file": "andrea-4d6871c326.mp3",
"sec": 4.11
},
"andrea|Il Rattattù: la gara di canto del paese. Si sale sul palco e ci si sfida tutti, chi stona e chi no.": {
"file": "andrea-a9903d3ae0.mp3",
"sec": 8.33
},
"andrea|Quest'anno mi ci iscrivo. Forse.": {
"file": "andrea-146000c293.mp3",
"sec": 2.98
},
"andrea|Il Cinema Taiuti. Una sala, un proiettore e le poltroncine che cigolano a tempo.": {
"file": "andrea-3b310a5164.mp3",
"sec": 6.39
},
"andrea|A quest'ora è chiuso. Apre la sera, per il film o per il Rattattù.": {
"file": "andrea-5e37f70197.mp3",
"sec": 5.35
},
"andrea|Il campo da basket del Circolo, lassù oltre la rete. Prima o poi due tiri ce li faccio.": {
"file": "andrea-8cd2b5a36d.mp3",
"sec": 6.49
},
"andrea|La fermata della corriera. Da qui si va dappertutto. Se passa.": {
"file": "andrea-ccd735c926.mp3",
"sec": 5.72
},
"andrea|Il ponticino delle Mozzète. Sotto ci passa un fossino che d'estate fa più zanzare che acqua.": {
"file": "andrea-98dc291bd3.mp3",
"sec": 7.21
},
"andrea|Terra appena arata. Profuma di campagna e di trattore.": {
"file": "andrea-a07bf26ea7.mp3",
"sec": 4.41
},
"andrea|La falegnameria. L'odore di segatura arriva fin qui.": {
"file": "andrea-1ccd8e2127.mp3",
"sec": 3.84
},
"andrea|Bussare a una falegnameria? Mi danno una scopa e mi fanno spazzare.": {
"file": "andrea-09ec24ef60.mp3",
"sec": 4.63
},
"andrea|Un fienile pieno di rotoballe. Fossi una mucca, sarebbe il paradiso.": {
"file": "andrea-5dfec13174.mp3",
"sec": 4.81
},
"andrea|La campagna verso Gabbiano. Da quassù si vede mezzo Mugello.": {
"file": "andrea-06e1b17635.mp3",
"sec": 5.31
},
"andrea|Il palio, però, no.": {
"file": "andrea-0e9c7a48de.mp3",
"sec": 1.6
},
"andrea|Un casolare vecchio, coi mattoni che spuntano dall'intonaco. Come le mie ginocchia.": {
"file": "andrea-06c5f35df7.mp3",
"sec": 5.66
},
"andrea|L'ingresso del campeggio. A giudicare dalle bandiere, ci dorme mezzo mondo.": {
"file": "andrea-72dede01c0.mp3",
"sec": 5.13
@@ -643,13 +719,21 @@
"file": "andrea-0e558e54fe.mp3",
"sec": 4.18
},
"andrea|Le mura della Fortezza medicea, mezze coperte dall'edera. Ne hanno viste più di me.": {
"file": "andrea-7b3b36edc6.mp3",
"sec": 5.93
"andrea|Il muro di Villa Schifanoia, la villa dei Medici, mezzo coperto dall'edera.": {
"file": "andrea-6a873beff6.mp3",
"sec": 5.07
},
"andrea|Una casetta piccina, appoggiata alle mura come un gatto al sole.": {
"file": "andrea-c3279de3ab.mp3",
"sec": 3.93
"andrea|Lassù una volta c'era una torre, per tenere d'occhio chi passava sulla strada per Bologna. Ne ha viste più di me.": {
"file": "andrea-d9ba9bd3ad.mp3",
"sec": 7.34
},
"andrea|E la piazzetta si chiama come i Cambray-Digny, gli ultimi signori della villa.": {
"file": "andrea-9d3ec66f60.mp3",
"sec": 4.64
},
"andrea|Una casetta piccina, appoggiata al muro della villa come un gatto al sole.": {
"file": "andrea-3cef067df6.mp3",
"sec": 4.1
},
"andrea|Chiusa. E poi non è casa mia.": {
"file": "andrea-7fd73fbb77.mp3",
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -28,7 +28,7 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p
| `campeggio.png` | Codex | `campeggio-ingresso.png` | [DA ANDREA] probabilmente dal sito del campeggio | ? |
| `polisportiva-*.png`, `ponte-sieve.png`, `area-feste.png`, `via-provinciale.png`, `comune-asilo.png`, `giardini-poste.png`, `stazione.png` | Codex | foto omonime in `assets/raw/foto/` | Andrea (EXIF, 23 ottobre 2026) | sì |
| `via-del-pignone.png` | Codex | `via-del-pignone.png` (murale: `riferimenti/cistola.png`, `murale-maradona.png`) | Andrea | sì (Cistola consenziente) |
| `provinciale-pignone.png`, `piazzetta-cambray-digny.png` | Codex | screenshot di Google Street View | Google | **no**: da rifare con foto di Andrea |
| `provinciale-pignone.png`, `piazzetta-cambray-digny.png`, `via-cipriani.png`, `via-massorondinaio.png`, `mozzete.png`, `ponticino-mozzete.png`, `coldaia.png`, `falegnameria-coldaia.png`, `gabbiano.png` | Codex | screenshot di Google Street View | Google | **no**: da rifare con foto di Andrea |
| `cardetole.png` | Codex | `cardetole.jpg` | Andrea (EXIF) | **da decidere**: è la casa vera di Andrea, pubblicarla dice dove abita |
| `pieve-interno.png` | Codex | `pieve-interno.png` | Mariano Mirannalti (mandata ad Andrea su WhatsApp) | sì, se Mariano è d'accordo; citarlo nei crediti |
| `panorama-intro.png` | Codex | `altre/panorama-intro.png` | da internet (screenshot con l'icona di Google Lens) | **no**, finché non si sostituisce |
+7
View File
@@ -204,6 +204,13 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso
Via del Pignone (sfondo di Codex da `assets/raw/foto/via-del-pignone.png`): sul muro di destra il
murale di Cistola (Davide Poggi), «il talento di via del Pignone», alla maniera dei murali di Maradona a Napoli
(volto da `assets/raw/riferimenti/cistola.png`). Consenso di Davide confermato da Andrea il 2026-10-27.
- Via Pietro Cipriani (fra Piazza e campeggio): Cinema Taiuti aperto con la locandina del Rattattù 2007 (stile da
`assets/raw/riferimenti/rattattu-locandina.png`, scaletta illeggibile), campo da basket del Circolo: **da fare il
gioco del basket** (idea di Andrea, 27 ottobre).
- Fuori paese, dal campo secondario (a destra): le Mozzète → il ponticino → Coldaia → la falegnameria → Gabbiano;
da Via Cipriani, indietro, Via Massorondinaio → campeggio. Sfondi da Street View, battute provvisorie.
- In Fortezza quattro ragazzi inventati si fanno le canne da pesca col bambù per il lago dell'Anatre (sprite di Codex,
`assets/raw/pescatori-sheet.png`, ritagliato 2x2 a mano: work, talk, laugh, proud); battuta finale di Andrea.
- Andrea rifatto dalla sua foto del 2007 (`assets/raw/andrea-sheet-prova-2.png`; il vecchio foglio resta in `andrea-sheet.png`).
- Strada del campeggio (poi Bosco ai Frati) e strada della scuola: per ora battute, scene da fare con le foto di Andrea.
+1 -1
View File
@@ -101,7 +101,7 @@
<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>
<div id="credits-reel"><button class="arrow prev" type="button" aria-label="Foto precedente">‹</button><div class="track" aria-label="Le immagini del gioco: toccane una per vederla grande"></div><button class="arrow next" type="button" aria-label="Foto successiva">›</button></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>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "leggenda-del-regolo",
"version": "0.1.18",
"version": "0.1.20",
"private": true,
"type": "module",
"scripts": {
+16
View File
@@ -70,6 +70,22 @@ export function shuffledCredits(credits, random = Math.random) {
});
}
// Le immagini del carosello dei Ringraziamenti: tutti gli sfondi del paese (uno per scena, senza doppioni) col nome
// del posto, e un ritratto dei crediti ogni quattro sfondi; i ritratti che non trovano posto chiudono la fila.
export function reelPictures(story) {
const seen = new Set(), scenes = [];
for (const s of Object.values(story.scenes)) {
if (!s.background || seen.has(s.background)) continue;
seen.add(s.background);
scenes.push({ src: s.background, name: s.name, kind: 'scene' });
}
const faces = (story.credits ?? []).flatMap(c => c.names).filter(n => n.face).map(facePicture);
let next = 0;
const placed = scenes.flatMap((p, i) => i % 4 === 3 && next < faces.length ? [p, faces[next++]] : [p]);
return [...placed, ...faces.slice(next)];
}
export const facePicture = n => ({ src: n.face, name: n.name, kind: n.face.endsWith('palio.png') ? 'palio' : 'face' });
// La ricerca della mappa: ogni parola scritta deve stare nel nome del posto, senza badare a maiuscole, accenti e
// apostrofi («camp» trova Campomigliaio, «de medici» trova Via de' Medici).
const plain = text => text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, ' ').trim();
+65 -18
View File
@@ -1,6 +1,6 @@
// Avvio nel browser: collega motore, disegno e interfaccia HTML (verbi, inventario, dialoghi, fumetti).
import { story } from './story/story.js';
import { Game, VERBS, VERB_LABELS, checkSave, introText, shuffledCredits, matchPlace } from './engine/game.js';
import { Game, VERBS, VERB_LABELS, checkSave, introText, shuffledCredits, matchPlace, reelPictures, facePicture } from './engine/game.js';
import { Command } from './engine/command.js';
import { render, loadAssets, hasBackground, WIDTH, HEIGHT } from './engine/render.js';
import { readSave, writeSave, clearSave, readAudio, writeAudio, saveFile, saveFileName, readSaveFile } from './engine/storage.js';
@@ -125,7 +125,7 @@ function openCredits() {
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));
b.addEventListener('click', () => showFace(facePicture(n)));
return b;
}));
return [dt, dd];
@@ -133,22 +133,64 @@ function openCredits() {
fillReel();
$('credits').hidden = false;
rollCredits();
rollReel();
}
// 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.
// Sotto i nomi, il carosello: tutti gli sfondi del paese e i ritratti (game.reelPictures), due giri uguali in fila per
// non vedere mai la cucitura. Scorre da solo; si ferma sotto il mouse, si trascina, si spinge con la rotella, le
// frecce ai lati o quelle della tastiera, e toccando una foto la si vede grande (Andrea, 27 ottobre).
const reelTrack = $('credits-reel').querySelector('.track');
let reeling = 0, reelWait = 0, reelDrag = null, reelDragged = false, reelTap = null;
const reelPics = new WeakMap();
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 = src.endsWith('palio.png') ? 'palio' : 'face';
track.append(img);
if (reelTrack.children.length) return;
const all = reelPictures(story);
for (const p of [...all, ...all]) {
const img = Object.assign(document.createElement('img'), { src: versioned(p.src), alt: p.name, title: p.name, className: p.kind, loading: 'lazy', decoding: 'async', draggable: false });
reelPics.set(img, p);
reelTrack.append(img);
}
}
function rollReel() {
cancelAnimationFrame(reeling);
reelTrack.scrollLeft = 0;
reelWait = 0;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
let last = performance.now(), pos = 0;
const step = now => {
if ($('credits').hidden) return;
const dt = now - last; last = now;
const half = reelTrack.scrollWidth / 2;
if (reelTrack.matches(':hover') || reelDrag || !$('face').hidden || (reelWait -= dt) > 0) pos = reelTrack.scrollLeft;
else { pos += dt * 0.05; if (pos >= half) pos -= half; reelTrack.scrollLeft = pos; }
reeling = requestAnimationFrame(step);
};
reeling = requestAnimationFrame(step);
}
// Di una foto alla volta, con le frecce.
function nudgeReel(dir) {
reelWait = 4000;
reelTrack.scrollBy({ left: dir * ((reelTrack.firstElementChild?.offsetWidth ?? 300) + 10), behavior: 'smooth' });
}
for (const b of $('credits-reel').querySelectorAll('.arrow')) b.addEventListener('click', () => nudgeReel(b.classList.contains('next') ? 1 : -1));
// Col dito si trascina da sé (touch-action: pan-x); col mouse lo facciamo noi. Un tocco fermo su una foto la mostra
// grande: si guarda al rilascio, perché con la cattura del puntatore il clic non arriva più all'immagine.
reelTrack.addEventListener('pointerdown', e => {
reelDragged = false;
reelTap = reelPics.get(e.target) ?? null;
if (e.pointerType !== 'mouse') { reelWait = 4000; return; }
reelDrag = { x: e.clientX, left: reelTrack.scrollLeft, id: e.pointerId };
reelTrack.setPointerCapture(e.pointerId);
});
reelTrack.addEventListener('pointermove', e => {
if (!reelDrag) return;
const dx = e.clientX - reelDrag.x;
if (Math.abs(dx) > 6) reelDragged = true;
reelTrack.scrollLeft = reelDrag.left - dx;
});
const endReelDrag = () => { if (reelDrag && reelDragged) reelWait = 4000; reelDrag = null; };
reelTrack.addEventListener('pointerup', () => { if (!reelDragged && reelTap) showFace(reelTap); reelTap = null; endReelDrag(); });
reelTrack.addEventListener('pointercancel', () => { reelTap = null; endReelDrag(); });
reelTrack.addEventListener('wheel', e => { reelWait = 4000; if (e.deltaY && !e.deltaX) { reelTrack.scrollLeft += e.deltaY; e.preventDefault(); } }, { passive: false });
// 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; });
@@ -170,9 +212,10 @@ function rollCredits() {
};
rolling = requestAnimationFrame(step);
}
function showFace({ name, face }) {
$('face').querySelector('img').src = versioned(face);
$('face').querySelector('img').classList.toggle('palio', face.endsWith('palio.png'));
function showFace({ name, src, kind }) {
const img = $('face').querySelector('img');
img.src = versioned(src);
img.className = kind;
$('face').querySelector('figcaption').textContent = name;
$('face').hidden = false;
}
@@ -631,7 +674,11 @@ window.addEventListener('keydown', e => {
}
if (debugKey(e)) return;
if (!$('tutorial').hidden) { if (e.key === 'Escape') closeTutorial(); return; }
if (!$('credits').hidden) { if (e.key === 'Escape') { if (!$('face').hidden) $('face').hidden = true; else $('credits').hidden = true; } return; }
if (!$('credits').hidden) {
if (e.key === 'Escape') { if (!$('face').hidden) $('face').hidden = true; else $('credits').hidden = true; }
else if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { if ($('face').hidden) nudgeReel(e.key === 'ArrowRight' ? 1 : -1); }
return;
}
if (!$('pause').hidden) { if (e.key === 'Escape') closePause(); return; }
if (!$('notebook').hidden) { if (['Escape', 'n', 'N'].includes(e.key)) $('notebook').hidden = true; return; }
if (!$('map').hidden) { if (e.key === 'Escape' || e.key === 'p' || e.key === 'P') $('map').hidden = true; return; }
+206 -7
View File
@@ -215,6 +215,8 @@ export const story = {
// [DA ANDREA: chi è] Figura inventata, nessuna persona reale.
giocatore_carte: { name: 'Il giocatore di carte', color: '#edc476', sprite: 'assets/characters/giocatore-carte.json' },
andrea: { name: 'Andrea', color: '#ffffff', sprite: 'assets/characters/andrea.json' },
// Quattro ragazzi inventati (sprite di Codex, idea di Andrea del 27 ottobre): nessuna persona reale.
pescatori: { name: 'I ragazzi delle canne', color: '#b8f08a', sprite: 'assets/characters/pescatori.json' },
vecchietta: { name: 'Vecchietta', color: '#ffd27a', sprite: 'assets/characters/vecchietta.json' },
vecchietto: { name: 'Vecchietto', color: '#9fe3ff', sprite: 'assets/characters/vecchietto.json' },
castello: { name: 'Lo spirito del Trebbio', color: '#c8f0ff' },
@@ -477,7 +479,7 @@ export const story = {
},
},
{
id: 'verso_campeggio', name: 'strada verso il campeggio', box: [330, 190, 34, 48], at: [345, 234], exit: { scene: 'campeggio', at: [200, 345], face: 'right' },
id: 'verso_campeggio', name: 'Via Cipriani, verso il cinema e il campeggio', box: [330, 190, 34, 48], at: [345, 234], exit: { scene: 'via_cipriani', at: [300, 345], face: 'up' },
},
{
id: 'verso_piazzetta', name: 'Piazzetta Cambray Digny, verso Via de\' Medici', box: [0, 196, 146, 54], at: [92, 250], exit: { scene: 'piazzetta', at: [510, 274], face: 'down' },
@@ -501,7 +503,22 @@ export const story = {
walk: [[110, 284], [300, 280], [330, 272], [348, 274], [400, 292], [442, 294], [640, 352], [640, 360], [0, 360], [0, 315], [60, 310], [105, 295]],
depth: [[272, 0.55], [360, 1.15]],
entry: [175, 352],
speakers: { pescatori: [150, 225] },
// Quattro ragazzi sull'erba si fanno le canne da pesca col bambù.
people: [{ who: 'pescatori', at: [150, 322], idle: 'work', talk: ['work', 'talk'], blink: 'laugh', every: 6, size: 1.25 }],
hotspots: [
{
id: 'pescatori', name: 'ragazzi con le canne', box: [92, 236, 120, 86], at: [240, 334], face: 'left', default: 'talk',
actions: {
look: [{ say: 'Quattro ragazzi che affilano canne di bambù col coltellino e ci legano le lenze.' }],
talk: [
{ say: 'Oh, che fate?' },
{ say: 'Si preparano le canne: domattina si va a pescare al lago dell\'Anatre, dietro Cardetole!', who: 'pescatori', pose: 'talk' },
{ say: 'Guarda questa: la lenza la regge anche una carpa di dieci chili.', who: 'pescatori', pose: 'proud' },
{ say: '(Vedi, mia mamma aveva ragione: in Fortezza si viene a farsi le canne.)' },
],
},
},
{
id: 'mura', name: 'bastione', box: [420, 0, 220, 290],
actions: {
@@ -856,7 +873,186 @@ export const story = {
},
],
},
// L'ingresso del campeggio: dalla Piazza a diritto; poco dopo, a destra, si sale al Bosco ai Frati.
// Via Pietro Cipriani, fra Via Massorondinaio (alle spalle, verso il campeggio) e la Piazza (in fondo, col Circolo 4 Maggio arancione).
// A destra il Cinema Taiuti, aperto come nel 2007, col cartellone del Rattattù; lassù il campo da basket del Circolo.
via_cipriani: {
name: 'Via Pietro Cipriani',
map: [22, 64],
background: 'assets/scenes/via-cipriani.png',
walk: [[40, 360], [40, 320], [60, 280], [100, 272], [150, 276], [300, 300], [360, 318], [640, 345], [640, 360]],
depth: [[272, 0.45], [360, 1.15]],
entry: [300, 345],
hotspots: [
{
id: 'rattattu', name: 'cartellone del Rattattù', box: [460, 152, 58, 110], at: [480, 336], face: 'up',
actions: {
look: [{ say: 'Il Rattattù: la gara di canto del paese. Si sale sul palco e ci si sfida tutti, chi stona e chi no.' }, { say: 'Quest\'anno mi ci iscrivo. Forse.' }],
},
},
{
id: 'cinema', name: 'Cinema Taiuti', box: [420, 40, 220, 220], at: [520, 340], face: 'up',
actions: {
look: [{ say: 'Il Cinema Taiuti. Una sala, un proiettore e le poltroncine che cigolano a tempo.' }],
open: [{ say: 'A quest\'ora è chiuso. Apre la sera, per il film o per il Rattattù.' }],
},
},
{
id: 'campo_basket', name: 'campo da basket del Circolo', box: [250, 110, 170, 80], at: [340, 316], face: 'up',
actions: {
look: [{ say: 'Il campo da basket del Circolo, lassù oltre la rete. Prima o poi due tiri ce li faccio.' }],
},
},
{
id: 'alla_piazza', name: 'in fondo, la Piazza', box: [80, 230, 120, 50], at: [120, 280], exit: { scene: 'piazza', at: [345, 240], face: 'down' },
},
{
id: 'al_campeggio', name: 'indietro, verso il campeggio', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'via_massorondinaio', at: [30, 300], face: 'right' },
},
],
},
// Via Massorondinaio: a sinistra si riprende Via Cipriani (verso la Piazza), a diritto si va al campeggio.
via_massorondinaio: {
name: 'Via Massorondinaio',
map: [17, 74],
background: 'assets/scenes/via-massorondinaio.png',
walk: [[0, 360], [0, 266], [200, 258], [400, 240], [520, 226], [566, 226], [552, 250], [545, 290], [600, 360]],
depth: [[226, 0.4], [360, 1.2]],
entry: [300, 330],
hotspots: [
{
id: 'a_via_cipriani', name: 'a sinistra, Via Cipriani', box: [0, 262, 40, 80], at: [16, 300], face: 'left', exit: { scene: 'via_cipriani', at: [300, 345], face: 'up' },
},
{
id: 'al_campeggio', name: 'a diritto, verso il campeggio', box: [496, 196, 74, 34], at: [535, 232], exit: { scene: 'campeggio', at: [200, 345], face: 'right' },
},
],
},
// Le Mozzète (Via Donatello): in fondo il campo secondario, in basso a destra si scende al ponticino.
mozzete: {
name: 'Le Mozzète',
map: [72, 92],
background: 'assets/scenes/mozzete.png',
walk: [[0, 360], [0, 280], [40, 232], [120, 212], [300, 200], [335, 198], [350, 210], [420, 270], [640, 330], [640, 360]],
depth: [[198, 0.3], [360, 1.2]],
entry: [320, 330],
hotspots: [
{
id: 'fermata', name: 'fermata della corriera', box: [450, 170, 110, 90], at: [470, 300], face: 'right',
actions: {
look: [{ say: 'La fermata della corriera. Da qui si va dappertutto. Se passa.' }],
},
},
{
id: 'al_secondario', name: 'in fondo, il campo secondario', box: [300, 176, 60, 30], at: [330, 206], exit: { scene: 'polisportiva', at: [628, 290], face: 'left' },
},
{
id: 'al_ponticino', name: 'giù a destra, il ponticino delle Mozzète', box: [580, 316, 60, 44], at: [612, 345], face: 'right', exit: { scene: 'ponticino_mozzete', at: [300, 196], face: 'down' },
},
],
},
// Il ponticino delle Mozzète: lassù la congiunzione con le Mozzète, indietro si va verso Coldaia.
ponticino_mozzete: {
name: 'Il ponticino delle Mozzète',
map: [78, 88],
background: 'assets/scenes/ponticino-mozzete.png',
walk: [[0, 360], [0, 300], [200, 270], [280, 190], [320, 186], [380, 230], [520, 290], [640, 310], [640, 360]],
depth: [[186, 0.3], [360, 1.2]],
entry: [300, 340],
hotspots: [
{
id: 'parapetto', name: 'parapetto del ponticino', box: [460, 230, 120, 50], at: [470, 300], face: 'right',
actions: {
look: [{ say: 'Il ponticino delle Mozzète. Sotto ci passa un fossino che d\'estate fa più zanzare che acqua.' }],
},
},
{
id: 'alle_mozzete', name: 'lassù, le Mozzète', box: [270, 166, 60, 30], at: [300, 196], exit: { scene: 'mozzete', at: [612, 345], face: 'left' },
},
{
id: 'a_coldaia', name: 'indietro, verso Coldaia', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'coldaia', at: [450, 345], face: 'up' },
},
],
},
// Coldaia: dietro il ponticino delle Mozzète; avanti la falegnameria e poi Gabbiano.
coldaia: {
name: 'Coldaia',
map: [84, 89],
background: 'assets/scenes/coldaia.png',
walk: [[230, 360], [300, 250], [318, 200], [290, 166], [330, 162], [460, 205], [640, 268], [640, 360]],
depth: [[162, 0.3], [360, 1.2]],
entry: [450, 345],
hotspots: [
{
id: 'campo_arato', name: 'campo arato', box: [0, 160, 220, 130], at: [280, 330], face: 'left',
actions: {
look: [{ say: 'Terra appena arata. Profuma di campagna e di trattore.' }],
},
},
{
id: 'alla_falegnameria', name: 'avanti, verso la falegnameria', box: [280, 140, 60, 34], at: [305, 168], exit: { scene: 'falegnameria_coldaia', at: [300, 340], face: 'up' },
},
{
id: 'al_ponticino', name: 'indietro, il ponticino delle Mozzète', box: [220, 334, 420, 26], at: [450, 354], exit: { scene: 'ponticino_mozzete', at: [300, 340], face: 'up' },
},
],
},
// La falegnameria di Coldaia, col fienile delle rotoballe; a diritto si va a Gabbiano.
falegnameria_coldaia: {
name: 'La falegnameria',
map: [90, 91],
background: 'assets/scenes/falegnameria-coldaia.png',
walk: [[0, 360], [0, 300], [100, 260], [160, 220], [185, 220], [230, 250], [640, 250], [640, 360]],
depth: [[220, 0.35], [360, 1.2]],
entry: [300, 340],
hotspots: [
{
id: 'falegnameria', name: 'falegnameria', box: [160, 176, 120, 44], at: [220, 260], face: 'up',
actions: {
look: [{ say: 'La falegnameria. L\'odore di segatura arriva fin qui.' }],
open: [{ say: 'Bussare a una falegnameria? Mi danno una scopa e mi fanno spazzare.' }],
},
},
{
id: 'fienile', name: 'fienile', box: [290, 90, 320, 140], at: [440, 280], face: 'up',
actions: {
look: [{ say: 'Un fienile pieno di rotoballe. Fossi una mucca, sarebbe il paradiso.' }],
},
},
{
id: 'a_gabbiano', name: 'a diritto, verso Gabbiano', box: [140, 190, 64, 32], at: [172, 222], exit: { scene: 'gabbiano', at: [300, 340], face: 'up' },
},
{
id: 'a_coldaia', name: 'indietro, verso Coldaia', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'coldaia', at: [305, 170], face: 'down' },
},
],
},
// La strada di Gabbiano, in mezzo alla campagna del Mugello.
gabbiano: {
name: 'Gabbiano',
map: [95, 93],
background: 'assets/scenes/gabbiano.png',
walk: [[0, 360], [0, 300], [200, 260], [400, 215], [430, 186], [445, 186], [520, 240], [640, 280], [640, 360]],
depth: [[186, 0.3], [360, 1.2]],
entry: [300, 340],
hotspots: [
{
id: 'valle', name: 'campagna', box: [150, 140, 260, 60], at: [330, 300], face: 'up',
actions: {
look: [{ say: 'La campagna verso Gabbiano. Da quassù si vede mezzo Mugello.' }, { say: 'Il palio, però, no.' }],
},
},
{
id: 'casolare', name: 'casolare', box: [0, 30, 140, 220], at: [150, 320], face: 'left',
actions: {
look: [{ say: 'Un casolare vecchio, coi mattoni che spuntano dall\'intonaco. Come le mie ginocchia.' }],
},
},
{
id: 'alla_falegnameria', name: 'indietro, verso la falegnameria', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'falegnameria_coldaia', at: [172, 224], face: 'down' },
},
],
},
// L'ingresso del campeggio: dalla Piazza per Via Cipriani e Via Massorondinaio; poco dopo, a destra, si sale al Bosco ai Frati.
campeggio: {
name: 'Ingresso del campeggio',
map: [12, 84],
@@ -884,7 +1080,7 @@ export const story = {
id: 'bosco_ai_frati', name: 'salita verso il Bosco ai Frati', box: [570, 250, 70, 110], at: [628, 320], exit: { scene: 'bosco_ai_frati', at: [320, 345], face: 'right' },
},
{
id: 'verso_piazza', name: 'torna in Piazza', box: [150, 290, 100, 70], at: [175, 345], exit: { scene: 'piazza', at: [345, 240], face: 'left' },
id: 'verso_piazza', name: 'Via Massorondinaio, verso la Piazza', box: [150, 290, 100, 70], at: [175, 345], exit: { scene: 'via_massorondinaio', at: [530, 236], face: 'down' },
},
],
},
@@ -1127,7 +1323,7 @@ export const story = {
},
],
},
// Piazzetta Cambray Digny, in cima a Via del Pignone: a sinistra Via de' Medici, a diritto la Piazza.
// Piazzetta Cambray Digny, in cima a Via del Pignone, sotto Villa Schifanoia: a sinistra Via de' Medici, a diritto la Piazza.
piazzetta: {
name: 'Piazzetta Cambray Digny',
map: [40, 38],
@@ -1137,15 +1333,15 @@ export const story = {
entry: [320, 330],
hotspots: [
{
id: 'mura', name: 'mura della Fortezza', box: [60, 110, 230, 170], at: [180, 300], face: 'up',
id: 'villa_schifanoia', name: 'Villa Schifanoia', box: [60, 110, 230, 170], at: [180, 300], face: 'up',
actions: {
look: [{ say: 'Le mura della Fortezza medicea, mezze coperte dall\'edera. Ne hanno viste più di me.' }],
look: [{ say: 'Il muro di Villa Schifanoia, la villa dei Medici, mezzo coperto dall\'edera.' }, { say: 'Lassù una volta c\'era una torre, per tenere d\'occhio chi passava sulla strada per Bologna. Ne ha viste più di me.' }, { say: 'E la piazzetta si chiama come i Cambray-Digny, gli ultimi signori della villa.' }],
},
},
{
id: 'casetta_bianca', name: 'casetta bianca', box: [296, 196, 160, 90], at: [380, 292], face: 'up',
actions: {
look: [{ say: 'Una casetta piccina, appoggiata alle mura come un gatto al sole.' }],
look: [{ say: 'Una casetta piccina, appoggiata al muro della villa come un gatto al sole.' }],
open: [{ say: 'Chiusa. E poi non è casa mia.' }],
},
},
@@ -1353,6 +1549,9 @@ export const story = {
{
id: 'al_principale', name: 'indietro, verso il campo principale', box: [0, 330, 640, 30], at: [100, 354], exit: { scene: 'polisportiva_principale', at: [620, 330], face: 'left' },
},
{
id: 'alle_mozzete', name: 'a destra, verso le Mozzète', box: [610, 226, 30, 100], at: [628, 290], face: 'right', exit: { scene: 'mozzete', at: [330, 206], face: 'down' },
},
{
id: 'barondeduke', name: 'il BaronDeDuke', box: [212, 245, 36, 55], at: [190, 316], face: 'right', default: 'talk',
give: {
+1 -1
View File
@@ -1,5 +1,5 @@
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
export const VERSION = '0.1.18';
export const VERSION = '0.1.20';
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica,
// mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf).
+13 -5
View File
@@ -259,10 +259,18 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
#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;
#credits-reel { flex: 0 0 auto; position: relative; height: clamp(56px, 24vh, 200px); width: 100%; margin: .5em 0 .3em; }
/* La fila scorre da sola (main.js) ed è anche un normale scorrimento orizzontale: dito, rotella, trascinamento col mouse. */
#credits-reel .track { display: flex; gap: 10px; height: 100%; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; touch-action: pan-x; cursor: grab;
-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 .track::-webkit-scrollbar { display: none; }
#credits-reel .track:active { cursor: grabbing; }
#credits-reel img { flex: none; height: 100%; aspect-ratio: 16 / 9; object-fit: cover; image-rendering: pixelated; border-radius: 6px; border: 2px solid #3a2d42; cursor: pointer; user-select: none; -webkit-user-drag: none; }
#credits-reel img:hover { border-color: #fff; }
#credits-reel .arrow { position: absolute; top: 50%; transform: translateY(-50%); z-index: 1; width: 1.8em; height: 2.4em; padding: 0; border: 1px solid var(--accent); border-radius: 6px;
background: rgba(15, 10, 20, .8); color: var(--accent); font: inherit; font-size: clamp(12px, 1.8vw, 20px); line-height: 1; cursor: pointer; opacity: .85; }
#credits-reel .arrow:hover, #credits-reel .arrow:focus-visible { opacity: 1; color: #fff; }
#credits-reel .arrow.prev { left: .3em; } #credits-reel .arrow.next { right: .3em; }
#credits-reel img.face { aspect-ratio: 1; border-color: var(--accent); }
#credits-reel img.palio { aspect-ratio: auto; border: 0; }
/* La fine: il quadro del palio accanto alle parole (sotto, sugli schermi stretti). */
@@ -275,14 +283,14 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
#ending figure.palio figcaption { font-size: clamp(6.5px, .85vw, 9px); color: var(--dim); max-width: 22em; }
#ending .ending-row > div { max-width: 26em; text-align: center; }
@media (max-aspect-ratio: 1/1) { #ending .ending-row { flex-direction: column; } #ending figure.palio img { height: 30vh; } }
@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 { height: min(60vmin, 384px); width: auto; max-width: 90%; image-rendering: pixelated; border: 3px solid var(--accent); border-radius: 8px; }
/* Uno sfondo ingrandito: largo quanto può restando 16:9 dentro lo schermo. */
#face img.scene { height: auto; width: min(92%, 128vh, 1024px); }
#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; }
+31
View File
@@ -6,6 +6,8 @@ import { staticServer } from './helpers.mjs';
import { mkdirSync, readFileSync } from 'node:fs';
import assert from 'node:assert/strict';
import { solution, route } from '../src/story/solution.js';
import { story } from '../src/story/story.js';
import { reelPictures } from '../src/engine/game.js';
import { checkMultimedia } from './multimedia.browser.mjs';
const OUT = process.env.OUT ?? '/tmp/regolo-shots';
@@ -120,6 +122,35 @@ try {
await page.screenshot({ path: `${OUT}/01d-ritratto.png` });
await page.click('#face');
assert.ok(await page.isHidden('#face'));
// Il carosello: tutte le immagini (due giri), scorre da solo, si ferma sotto il mouse, le frecce lo spingono di una
// foto, un tocco su una foto la mostra grande col nome del posto.
{
const track = page.locator('#credits-reel .track');
assert.equal(await track.locator('img').count(), reelPictures(story).length * 2, 'ogni sfondo e ogni ritratto, due giri');
await page.mouse.move(5, 5);
const x0 = await track.evaluate(t => t.scrollLeft);
await page.waitForTimeout(600);
const x1 = await track.evaluate(t => t.scrollLeft);
assert.ok(x1 > x0, `il carosello scorre da solo (${x0} → ${x1})`);
await track.hover({ position: { x: 20, y: 20 } });
const x2 = await track.evaluate(t => t.scrollLeft);
await page.waitForTimeout(500);
assert.equal(await track.evaluate(t => t.scrollLeft), x2, 'sotto il mouse si ferma');
await page.click('#credits-reel .arrow.next');
await page.waitForTimeout(800);
const x3 = await track.evaluate(t => t.scrollLeft);
assert.ok(x3 - x2 > 150, `la freccia spinge di una foto (${x2} → ${x3})`);
const third = track.locator('img').nth(2);
const name = await third.getAttribute('alt');
await third.click();
assert.ok(await page.isVisible('#face img.scene'), 'la foto si vede grande');
assert.equal(await page.textContent('#face figcaption'), name);
await page.waitForFunction(() => document.querySelector('#face img').naturalWidth > 0, null, { timeout: 5000 });
await page.screenshot({ path: `${OUT}/01e-carosello-zoom.png` });
await page.keyboard.press('Escape');
assert.ok(await page.isHidden('#face'), 'Esc chiude la foto grande, non i crediti');
assert.ok(await page.isVisible('#credits'));
}
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');
+15 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { backgroundOrder } from '../src/engine/render.js';
import { story } from '../src/story/story.js';
import { solution } from '../src/story/solution.js';
import { Game, VERBS, checkSave, introText, shuffledCredits, matchPlace } from '../src/engine/game.js';
import { Game, VERBS, checkSave, introText, shuffledCredits, matchPlace, reelPictures } from '../src/engine/game.js';
import { inside } from '../src/engine/geometry.js';
import { existsSync, readFileSync } from 'node:fs';
import { settle, exploreTorch } from './helpers.mjs';
@@ -144,6 +144,20 @@ test('le fasi del giorno seguono la storia: pomeriggio, poi tramonto, poi sera,
assert.deepEqual(seen, ids);
});
test('il carosello dei Ringraziamenti: ogni sfondo una volta sola col nome del posto, tutti i ritratti, mai due ritratti vicini', () => {
const reel = reelPictures(story);
const backgrounds = new Set(Object.values(story.scenes).map(s => s.background).filter(Boolean));
assert.deepEqual(reel.filter(p => p.kind === 'scene').map(p => p.src).sort(), [...backgrounds].sort(), 'tutti gli sfondi, senza doppioni');
for (const p of reel) { assert.ok(p.name, `${p.src} senza nome`); assert.ok(existsSync(new URL('../' + p.src, import.meta.url)), `manca ${p.src}`); }
const faces = story.credits.flatMap(c => c.names).filter(n => n.face);
assert.deepEqual(reel.filter(p => p.kind !== 'scene').map(p => p.src).sort(), faces.map(n => n.face).sort(), 'tutti i ritratti, una volta');
assert.ok(reel.some(p => p.kind === 'palio'), 'il palio ha la sua cornice');
reel.forEach((p, i) => { if (i && p.kind !== 'scene') assert.equal(reel[i - 1].kind, 'scene', `due ritratti di fila: ${reel[i - 1].name}, ${p.name}`); });
// Poche scene: i ritratti che non trovano posto chiudono comunque la fila.
const few = reelPictures({ scenes: { a: { name: 'A', background: 'x.png' } }, credits: story.credits });
assert.equal(few.length, 1 + faces.length);
});
test('la ricerca della mappa: pezzi di parola, senza maiuscole, accenti e apostrofi', () => {
assert.ok(matchPlace('Campomigliaio', 'camp'));
assert.ok(matchPlace('Via de\' Medici', 'de medici'));
+4
View File
@@ -49,5 +49,9 @@
},
"ing_tozzi": {
"speaker": "Badr Odhiambo"
},
"pescatori": {
"speaker": "Dionisio Schuyler",
"filtro": "asetrate=24000*1.08,aresample=24000,atempo=1.04"
}
}