Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64dc8e18e7 | ||
|
|
b262977117 | ||
|
|
3762f2b465 | ||
|
|
9f7a9812ed | ||
|
|
151017d2e2 | ||
|
|
f193c6020f |
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 399 KiB |
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 416 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 257 KiB |
@@ -203,6 +203,26 @@
|
|||||||
"file": "andrea-69337973d0.mp3",
|
"file": "andrea-69337973d0.mp3",
|
||||||
"sec": 5.94
|
"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.": {
|
"andrea|Nove bastioni, li volle Cosimo I. Per tenere fuori i nemici, dicono. Secondo me per tenere dentro il Regolo.": {
|
||||||
"file": "andrea-d31f6e291b.mp3",
|
"file": "andrea-d31f6e291b.mp3",
|
||||||
"sec": 8.71
|
"sec": 8.71
|
||||||
@@ -507,6 +527,62 @@
|
|||||||
"file": "andrea-4d6871c326.mp3",
|
"file": "andrea-4d6871c326.mp3",
|
||||||
"sec": 4.11
|
"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.": {
|
"andrea|L'ingresso del campeggio. A giudicare dalle bandiere, ci dorme mezzo mondo.": {
|
||||||
"file": "andrea-72dede01c0.mp3",
|
"file": "andrea-72dede01c0.mp3",
|
||||||
"sec": 5.13
|
"sec": 5.13
|
||||||
@@ -627,6 +703,42 @@
|
|||||||
"file": "andrea-e24cb31cbf.mp3",
|
"file": "andrea-e24cb31cbf.mp3",
|
||||||
"sec": 2.62
|
"sec": 2.62
|
||||||
},
|
},
|
||||||
|
"andrea|Cartelli per la Pieve, per la stazione, per Firenze. Per il palio, nessuno.": {
|
||||||
|
"file": "andrea-60f39f0dd7.mp3",
|
||||||
|
"sec": 6.48
|
||||||
|
},
|
||||||
|
"andrea|Cistola. Il talento di via del Pignone.": {
|
||||||
|
"file": "andrea-2b47355c2c.mp3",
|
||||||
|
"sec": 3.81
|
||||||
|
},
|
||||||
|
"andrea|Col dieci, come i grandi. Se c'è un muro che se lo merita, è questo.": {
|
||||||
|
"file": "andrea-1bd111f29d.mp3",
|
||||||
|
"sec": 5.59
|
||||||
|
},
|
||||||
|
"andrea|Ciao Cistola! ...Almeno dipinto stai fermo ad ascoltarmi.": {
|
||||||
|
"file": "andrea-0e558e54fe.mp3",
|
||||||
|
"sec": 4.18
|
||||||
|
},
|
||||||
|
"andrea|Il muro di Villa Schifanoia, la villa dei Medici, mezzo coperto dall'edera.": {
|
||||||
|
"file": "andrea-6a873beff6.mp3",
|
||||||
|
"sec": 5.07
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
"sec": 2.66
|
||||||
|
},
|
||||||
"andrea|L'area feste. D'estate ci montano la pista da ballo, le tavolate e lo stand della pasta.": {
|
"andrea|L'area feste. D'estate ci montano la pista da ballo, le tavolate e lo stand della pasta.": {
|
||||||
"file": "andrea-a942b2723e.mp3",
|
"file": "andrea-a942b2723e.mp3",
|
||||||
"sec": 6.77
|
"sec": 6.77
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p
|
|||||||
| `bosco-ai-frati.png` | Codex | `bosco-ai-frati.png` | [DA ANDREA] sembra una foto da Wikipedia/Wikimedia: verificare licenza | ? |
|
| `bosco-ai-frati.png` | Codex | `bosco-ai-frati.png` | [DA ANDREA] sembra una foto da Wikipedia/Wikimedia: verificare licenza | ? |
|
||||||
| `campeggio.png` | Codex | `campeggio-ingresso.png` | [DA ANDREA] probabilmente dal sito del campeggio | ? |
|
| `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ì |
|
| `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`, `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 |
|
| `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 |
|
| `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 |
|
| `panorama-intro.png` | Codex | `altre/panorama-intro.png` | da internet (screenshot con l'icona di Google Lens) | **no**, finché non si sostituisce |
|
||||||
|
|||||||
@@ -199,6 +199,18 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso
|
|||||||
sfondi da foto prese da internet (`docs/IMMAGINI.md`), Cardetole (dice dove abita Andrea).
|
sfondi da foto prese da internet (`docs/IMMAGINI.md`), Cardetole (dice dove abita Andrea).
|
||||||
- Vecchietta e vecchietto sono in gioco sulla panchina della Piazza (`assets/characters/`, da
|
- Vecchietta e vecchietto sono in gioco sulla panchina della Piazza (`assets/characters/`, da
|
||||||
`assets/raw/vecchiett?-sheet.png`): consenso delle persone ritratte confermato da Andrea il 2026-10-23; mancano i loro nomi.
|
`assets/raw/vecchiett?-sheet.png`): consenso delle persone ritratte confermato da Andrea il 2026-10-23; mancano i loro nomi.
|
||||||
|
- Via Provinciale (dalla rotonda verso la Pieve) → a destra Via del Pignone → in cima Piazzetta Cambray Digny, che porta
|
||||||
|
in Piazza (a sinistra, dove prima c'era l'uscita per il Comune) e in Via de' Medici. Battute provvisorie sul murale.
|
||||||
|
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`).
|
- 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.
|
- Strada del campeggio (poi Bosco ai Frati) e strada della scuola: per ora battute, scene da fare con le foto di Andrea.
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
<h2>Ringraziamenti</h2>
|
<h2>Ringraziamenti</h2>
|
||||||
<p class="dedication credits-dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p>
|
<p class="dedication credits-dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p>
|
||||||
<dl id="credits-list"></dl>
|
<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>
|
<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>
|
<div class="buttons"><button id="close-credits">Chiudi</button></div>
|
||||||
<figure id="face" hidden><img alt=""><figcaption></figcaption></figure>
|
<figure id="face" hidden><img alt=""><figcaption></figcaption></figure>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "leggenda-del-regolo",
|
"name": "leggenda-del-regolo",
|
||||||
"version": "0.1.17",
|
"version": "0.1.20",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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
|
// 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).
|
// 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();
|
const plain = text => text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, ' ').trim();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Avvio nel browser: collega motore, disegno e interfaccia HTML (verbi, inventario, dialoghi, fumetti).
|
// Avvio nel browser: collega motore, disegno e interfaccia HTML (verbi, inventario, dialoghi, fumetti).
|
||||||
import { story } from './story/story.js';
|
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 { Command } from './engine/command.js';
|
||||||
import { render, loadAssets, hasBackground, WIDTH, HEIGHT } from './engine/render.js';
|
import { render, loadAssets, hasBackground, WIDTH, HEIGHT } from './engine/render.js';
|
||||||
import { readSave, writeSave, clearSave, readAudio, writeAudio, saveFile, saveFileName, readSaveFile } from './engine/storage.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 });
|
if (typeof n === 'string') return Object.assign(document.createElement('span'), { textContent: n });
|
||||||
// Chi ha il ritratto: il nome è un pulsante che lo mostra.
|
// 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 });
|
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 b;
|
||||||
}));
|
}));
|
||||||
return [dt, dd];
|
return [dt, dd];
|
||||||
@@ -133,22 +133,64 @@ function openCredits() {
|
|||||||
fillReel();
|
fillReel();
|
||||||
$('credits').hidden = false;
|
$('credits').hidden = false;
|
||||||
rollCredits();
|
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
|
// Sotto i nomi, il carosello: tutti gli sfondi del paese e i ritratti (game.reelPictures), due giri uguali in fila per
|
||||||
// vedere mai la cucitura.
|
// 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() {
|
function fillReel() {
|
||||||
const track = $('credits-reel').querySelector('.track');
|
if (reelTrack.children.length) return;
|
||||||
if (track.children.length) return;
|
const all = reelPictures(story);
|
||||||
const pictures = [...new Set(Object.values(story.scenes).map(s => s.background).filter(Boolean))];
|
for (const p of [...all, ...all]) {
|
||||||
const faces = (story.credits ?? []).flatMap(c => c.names).filter(n => n.face).map(n => n.face);
|
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 });
|
||||||
// I ritratti sparsi fra gli sfondi, uno ogni quattro.
|
reelPics.set(img, p);
|
||||||
const all = pictures.flatMap((p, i) => i % 4 === 3 && faces.length ? [p, faces[(i >> 2) % faces.length]] : [p]);
|
reelTrack.append(img);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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.
|
// 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;
|
let rolling = 0, rollWait = 0;
|
||||||
$('credits-list').addEventListener('pointerdown', () => { rollWait = 4000; });
|
$('credits-list').addEventListener('pointerdown', () => { rollWait = 4000; });
|
||||||
@@ -170,9 +212,10 @@ function rollCredits() {
|
|||||||
};
|
};
|
||||||
rolling = requestAnimationFrame(step);
|
rolling = requestAnimationFrame(step);
|
||||||
}
|
}
|
||||||
function showFace({ name, face }) {
|
function showFace({ name, src, kind }) {
|
||||||
$('face').querySelector('img').src = versioned(face);
|
const img = $('face').querySelector('img');
|
||||||
$('face').querySelector('img').classList.toggle('palio', face.endsWith('palio.png'));
|
img.src = versioned(src);
|
||||||
|
img.className = kind;
|
||||||
$('face').querySelector('figcaption').textContent = name;
|
$('face').querySelector('figcaption').textContent = name;
|
||||||
$('face').hidden = false;
|
$('face').hidden = false;
|
||||||
}
|
}
|
||||||
@@ -631,7 +674,11 @@ window.addEventListener('keydown', e => {
|
|||||||
}
|
}
|
||||||
if (debugKey(e)) return;
|
if (debugKey(e)) return;
|
||||||
if (!$('tutorial').hidden) { if (e.key === 'Escape') closeTutorial(); 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 (!$('pause').hidden) { if (e.key === 'Escape') closePause(); return; }
|
||||||
if (!$('notebook').hidden) { if (['Escape', 'n', 'N'].includes(e.key)) $('notebook').hidden = true; 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; }
|
if (!$('map').hidden) { if (e.key === 'Escape' || e.key === 'p' || e.key === 'P') $('map').hidden = true; return; }
|
||||||
|
|||||||
@@ -215,6 +215,8 @@ export const story = {
|
|||||||
// [DA ANDREA: chi è] Figura inventata, nessuna persona reale.
|
// [DA ANDREA: chi è] Figura inventata, nessuna persona reale.
|
||||||
giocatore_carte: { name: 'Il giocatore di carte', color: '#edc476', sprite: 'assets/characters/giocatore-carte.json' },
|
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' },
|
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' },
|
vecchietta: { name: 'Vecchietta', color: '#ffd27a', sprite: 'assets/characters/vecchietta.json' },
|
||||||
vecchietto: { name: 'Vecchietto', color: '#9fe3ff', sprite: 'assets/characters/vecchietto.json' },
|
vecchietto: { name: 'Vecchietto', color: '#9fe3ff', sprite: 'assets/characters/vecchietto.json' },
|
||||||
castello: { name: 'Lo spirito del Trebbio', color: '#c8f0ff' },
|
castello: { name: 'Lo spirito del Trebbio', color: '#c8f0ff' },
|
||||||
@@ -477,13 +479,10 @@ 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_villa_adami', name: 'Via de\' Medici, verso Villa Adami', box: [0, 196, 90, 50], at: [92, 250], exit: { scene: 'via_de_medici', at: [300, 348], 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' },
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'verso_comune', name: 'a sinistra, verso il Comune', box: [96, 196, 50, 54], at: [120, 252], exit: { scene: 'comune', at: [20, 300], face: 'right' },
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'verso_calimara', name: 'Via Calimara, verso la farmacia', box: [180, 334, 380, 26], at: [360, 354], exit: { scene: 'via_calimara', at: [365, 212], face: 'down' },
|
id: 'verso_calimara', name: 'Via Calimara, verso la farmacia', box: [180, 334, 380, 26], at: [360, 354], exit: { scene: 'via_calimara', at: [365, 212], face: 'down' },
|
||||||
@@ -504,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]],
|
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]],
|
depth: [[272, 0.55], [360, 1.15]],
|
||||||
entry: [175, 352],
|
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: [
|
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],
|
id: 'mura', name: 'bastione', box: [420, 0, 220, 290],
|
||||||
actions: {
|
actions: {
|
||||||
@@ -658,7 +672,7 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'verso_provinciale', name: 'Via Provinciale, verso il River', box: [0, 270, 46, 70], at: [14, 312], face: 'left', exit: { scene: 'via_provinciale', at: [560, 240], face: 'left' },
|
id: 'verso_provinciale', name: 'Via Provinciale, verso il River', box: [0, 270, 46, 70], at: [14, 312], face: 'left', exit: { scene: 'provinciale_pignone', at: [268, 216], face: 'down' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'verso_forno', name: 'a destra, verso il forno', box: [570, 176, 70, 76], at: [588, 250], exit: { scene: 'forno', at: [628, 300], face: 'left' },
|
id: 'verso_forno', name: 'a destra, verso il forno', box: [570, 176, 70, 76], at: [588, 250], exit: { scene: 'forno', at: [628, 300], face: 'left' },
|
||||||
@@ -835,7 +849,7 @@ export const story = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Via de' Medici: dalla Piazza sale verso Villa Adami (la Piazza è alle spalle, in fondo si va su).
|
// Via de' Medici: dalla Piazzetta Cambray Digny sale verso Villa Adami (la Piazzetta è alle spalle, in fondo si va su).
|
||||||
via_de_medici: {
|
via_de_medici: {
|
||||||
name: 'Via de\' Medici',
|
name: 'Via de\' Medici',
|
||||||
map: [22, 32],
|
map: [22, 32],
|
||||||
@@ -855,11 +869,190 @@ export const story = {
|
|||||||
id: 'su_villa_adami', name: 'su verso Villa Adami', box: [272, 186, 60, 34], at: [300, 216], exit: { scene: 'villa_adami', at: [50, 252], face: 'right' },
|
id: 'su_villa_adami', name: 'su verso Villa Adami', box: [272, 186, 60, 34], at: [300, 216], exit: { scene: 'villa_adami', at: [50, 252], face: 'right' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'giu_piazza', name: 'giù verso la Piazza', box: [0, 334, 560, 26], at: [300, 354], exit: { scene: 'piazza', at: [92, 250], face: 'right' },
|
id: 'giu_piazzetta', name: 'giù verso Piazzetta Cambray Digny', box: [0, 334, 545, 26], at: [300, 354], exit: { scene: 'piazzetta', at: [20, 320], face: 'right' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// 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: {
|
campeggio: {
|
||||||
name: 'Ingresso del campeggio',
|
name: 'Ingresso del campeggio',
|
||||||
map: [12, 84],
|
map: [12, 84],
|
||||||
@@ -887,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: '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' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -939,8 +1132,8 @@ export const story = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Il Comune e l'asilo infantile Antonio Frilli: dalla Piazza (a sinistra, prima di Via de' Medici) o dalla
|
// Il Comune e l'asilo infantile Antonio Frilli: dalla strada di fronte alla farmacia o da Via Calimara
|
||||||
// strada di fronte alla farmacia.
|
// (a sinistra in Piazza, dove una volta si veniva qui, c'è la Piazzetta Cambray Digny: Andrea, 27 ottobre).
|
||||||
comune: {
|
comune: {
|
||||||
name: 'Il Comune',
|
name: 'Il Comune',
|
||||||
map: [11, 45],
|
map: [11, 45],
|
||||||
@@ -969,9 +1162,6 @@ export const story = {
|
|||||||
open: [{ say: 'All\'asilo? Ho già dato, grazie.' }],
|
open: [{ say: 'All\'asilo? Ho già dato, grazie.' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'alla_piazza', name: 'su, verso la Piazza', box: [0, 250, 40, 110], at: [14, 300], exit: { scene: 'piazza', at: [120, 252], face: 'right' },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'al_giardino', name: 'verso il giardino di Via Calimara', box: [596, 290, 44, 70], at: [600, 340], exit: { scene: 'via_calimara', at: [160, 290], face: 'right' },
|
id: 'al_giardino', name: 'verso il giardino di Via Calimara', box: [596, 290, 44, 70], at: [600, 340], exit: { scene: 'via_calimara', at: [160, 290], face: 'right' },
|
||||||
},
|
},
|
||||||
@@ -1069,7 +1259,7 @@ export const story = {
|
|||||||
id: 'al_ponte', name: 'a sinistra, verso il campo sportivo', box: [0, 216, 60, 100], at: [12, 318], exit: { scene: 'ponte_sieve', at: [300, 350], face: 'up' },
|
id: 'al_ponte', name: 'a sinistra, verso il campo sportivo', box: [0, 216, 60, 100], at: [12, 318], exit: { scene: 'ponte_sieve', at: [300, 350], face: 'up' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'alla_pieve', name: 'dietro la farmacia, verso la Pieve', box: [470, 180, 120, 60], at: [560, 240], exit: { scene: 'pieve', at: [24, 312], face: 'right' },
|
id: 'alla_pieve', name: 'dietro la farmacia, verso la Pieve', box: [470, 180, 120, 60], at: [560, 240], exit: { scene: 'provinciale_pignone', at: [300, 345], face: 'up' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'area_feste', name: 'qui sotto, verso l\'area feste', box: [0, 318, 300, 42], at: [120, 352], exit: { scene: 'area_feste', at: [44, 350], face: 'up' },
|
id: 'area_feste', name: 'qui sotto, verso l\'area feste', box: [0, 318, 300, 42], at: [120, 352], exit: { scene: 'area_feste', at: [44, 350], face: 'up' },
|
||||||
@@ -1082,6 +1272,90 @@ export const story = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// Via Provinciale fra la rotonda (alle spalle) e la Pieve (in fondo); a destra sale Via del Pignone.
|
||||||
|
provinciale_pignone: {
|
||||||
|
name: 'Via Provinciale',
|
||||||
|
map: [50, 64],
|
||||||
|
background: 'assets/scenes/provinciale-pignone.png',
|
||||||
|
walk: [[30, 360], [30, 330], [130, 270], [245, 212], [290, 212], [340, 228], [540, 232], [540, 258], [640, 300], [640, 360]],
|
||||||
|
depth: [[212, 0.32], [360, 1.2]],
|
||||||
|
entry: [300, 345],
|
||||||
|
hotspots: [
|
||||||
|
{
|
||||||
|
id: 'cartelli', name: 'cartelli', box: [570, 162, 52, 28], at: [560, 290], face: 'right',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Cartelli per la Pieve, per la stazione, per Firenze. Per il palio, nessuno.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alla_pieve', name: 'in fondo, verso la Pieve', box: [236, 150, 64, 64], at: [268, 216], exit: { scene: 'pieve', at: [24, 312], face: 'right' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'via_pignone', name: 'a destra, Via del Pignone', box: [440, 196, 100, 56], at: [530, 245], face: 'right', exit: { scene: 'via_pignone', at: [300, 345], face: 'up' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alla_rotonda', name: 'indietro, verso la rotonda della farmacia', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'via_provinciale', at: [560, 240], face: 'left' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Via del Pignone dal basso: sul muro a destra il murale di Cistola (Davide Poggi, consenso del 27 ottobre).
|
||||||
|
// In cima si arriva in Piazzetta Cambray Digny.
|
||||||
|
via_pignone: {
|
||||||
|
name: 'Via del Pignone',
|
||||||
|
map: [46, 54],
|
||||||
|
background: 'assets/scenes/via-del-pignone.png',
|
||||||
|
walk: [[0, 360], [0, 300], [150, 258], [290, 226], [335, 206], [352, 206], [350, 240], [345, 282], [640, 290], [640, 360]],
|
||||||
|
depth: [[206, 0.3], [360, 1.2]],
|
||||||
|
entry: [300, 345],
|
||||||
|
hotspots: [
|
||||||
|
{
|
||||||
|
id: 'murale', name: 'murale di Cistola', box: [430, 36, 170, 240], at: [470, 300], face: 'right',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Cistola. Il talento di via del Pignone.' }, { say: 'Col dieci, come i grandi. Se c\'è un muro che se lo merita, è questo.' }],
|
||||||
|
talk: [{ say: 'Ciao Cistola! ...Almeno dipinto stai fermo ad ascoltarmi.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'su_piazzetta', name: 'su, verso Piazzetta Cambray Digny', box: [316, 176, 56, 40], at: [343, 210], exit: { scene: 'piazzetta', at: [610, 345], face: 'left' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'giu_provinciale', name: 'giù, in Via Provinciale', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'provinciale_pignone', at: [530, 245], face: 'left' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 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],
|
||||||
|
background: 'assets/scenes/piazzetta-cambray-digny.png',
|
||||||
|
walk: [[0, 360], [0, 288], [300, 284], [470, 276], [495, 268], [540, 268], [600, 300], [640, 330], [640, 360]],
|
||||||
|
depth: [[268, 0.55], [360, 1.15]],
|
||||||
|
entry: [320, 330],
|
||||||
|
hotspots: [
|
||||||
|
{
|
||||||
|
id: 'villa_schifanoia', name: 'Villa Schifanoia', box: [60, 110, 230, 170], at: [180, 300], face: 'up',
|
||||||
|
actions: {
|
||||||
|
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 al muro della villa come un gatto al sole.' }],
|
||||||
|
open: [{ say: 'Chiusa. E poi non è casa mia.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alla_piazza', name: 'a diritto, verso la Piazza', box: [470, 226, 70, 48], at: [510, 274], exit: { scene: 'piazza', at: [92, 250], face: 'right' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'via_de_medici', name: 'a sinistra, Via de\' Medici', box: [0, 280, 40, 80], at: [20, 320], face: 'left', exit: { scene: 'via_de_medici', at: [300, 348], face: 'up' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'giu_pignone', name: 'giù per Via del Pignone', box: [570, 300, 70, 60], at: [610, 345], exit: { scene: 'via_pignone', at: [343, 212], face: 'down' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
// L'area feste (il parco Antonio Berti): dalla rotonda del River, a sinistra.
|
// L'area feste (il parco Antonio Berti): dalla rotonda del River, a sinistra.
|
||||||
area_feste: {
|
area_feste: {
|
||||||
name: 'Area feste',
|
name: 'Area feste',
|
||||||
@@ -1275,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: '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',
|
id: 'barondeduke', name: 'il BaronDeDuke', box: [212, 245, 36, 55], at: [190, 316], face: 'right', default: 'talk',
|
||||||
give: {
|
give: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
|
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
|
||||||
export const VERSION = '0.1.17';
|
export const VERSION = '0.1.20';
|
||||||
|
|
||||||
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica,
|
// 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).
|
// mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf).
|
||||||
|
|||||||
@@ -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 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; }
|
#credits dd span { display: block; }
|
||||||
/* I nomi col ritratto: si toccano e compare il volto a pixel sopra i Ringraziamenti. */
|
/* 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); }
|
-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 .track::-webkit-scrollbar { display: none; }
|
||||||
#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: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.face { aspect-ratio: 1; border-color: var(--accent); }
|
||||||
#credits-reel img.palio { aspect-ratio: auto; border: 0; }
|
#credits-reel img.palio { aspect-ratio: auto; border: 0; }
|
||||||
/* La fine: il quadro del palio accanto alle parole (sotto, sugli schermi stretti). */
|
/* 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 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; }
|
#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; } }
|
@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 { 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; }
|
#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;
|
#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; }
|
background: rgba(15, 10, 20, .92); cursor: pointer; z-index: 1; }
|
||||||
#face[hidden] { display: none; }
|
#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; }
|
#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; }
|
#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 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; }
|
#credits .legal { color: var(--dim); font-size: clamp(7.2px, 0.86vw, 9.36px); opacity: .8; }
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { staticServer } from './helpers.mjs';
|
|||||||
import { mkdirSync, readFileSync } from 'node:fs';
|
import { mkdirSync, readFileSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { solution, route } from '../src/story/solution.js';
|
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';
|
import { checkMultimedia } from './multimedia.browser.mjs';
|
||||||
|
|
||||||
const OUT = process.env.OUT ?? '/tmp/regolo-shots';
|
const OUT = process.env.OUT ?? '/tmp/regolo-shots';
|
||||||
@@ -120,6 +122,35 @@ try {
|
|||||||
await page.screenshot({ path: `${OUT}/01d-ritratto.png` });
|
await page.screenshot({ path: `${OUT}/01d-ritratto.png` });
|
||||||
await page.click('#face');
|
await page.click('#face');
|
||||||
assert.ok(await page.isHidden('#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();
|
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'; });
|
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');
|
assert.ok(onTop, 'i crediti stanno sopra il titolo, non dietro');
|
||||||
@@ -316,7 +347,7 @@ try {
|
|||||||
assert.ok(await page.isVisible('#map-none'), 'nessun posto: lo dice');
|
assert.ok(await page.isVisible('#map-none'), 'nessun posto: lo dice');
|
||||||
await page.fill('#map-search', '');
|
await page.fill('#map-search', '');
|
||||||
await page.click('#map-search');
|
await page.click('#map-search');
|
||||||
await page.keyboard.type('piaz');
|
await page.keyboard.type('piazza col'); // «piaz» da solo trova anche la Piazzetta Cambray Digny
|
||||||
assert.ok(await page.isVisible('#map'), 'scrivere «p» nella ricerca non chiude la mappa');
|
assert.ok(await page.isVisible('#map'), 'scrivere «p» nella ricerca non chiude la mappa');
|
||||||
assert.ok(await page.isHidden('#map-none'));
|
assert.ok(await page.isHidden('#map-none'));
|
||||||
const shown = await page.locator('#map-board button:visible').evaluateAll(bs => bs.map(b => b.dataset.scene));
|
const shown = await page.locator('#map-board button:visible').evaluateAll(bs => bs.map(b => b.dataset.scene));
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ test('clic intelligente: parla con le persone, guarda le cose, attraversa le usc
|
|||||||
const { g, c, at } = setup();
|
const { g, c, at } = setup();
|
||||||
assert.equal(c.sentence('vecchietti'), 'Parla con vecchietti');
|
assert.equal(c.sentence('vecchietti'), 'Parla con vecchietti');
|
||||||
assert.equal(c.sentence('manifesto'), 'Guarda manifesto del Palio');
|
assert.equal(c.sentence('manifesto'), 'Guarda manifesto del Palio');
|
||||||
assert.equal(c.sentence('verso_villa_adami'), 'Vai a Via de\' Medici, verso Villa Adami');
|
assert.equal(c.sentence('verso_piazzetta'), 'Vai a Piazzetta Cambray Digny, verso Via de\' Medici');
|
||||||
assert.equal(c.clickScene(at('vecchietti')), 'act');
|
assert.equal(c.clickScene(at('vecchietti')), 'act');
|
||||||
settle(g);
|
settle(g);
|
||||||
assert.equal(g.dialogue?.id, 'vecchietti');
|
assert.equal(g.dialogue?.id, 'vecchietti');
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
|||||||
import { backgroundOrder } from '../src/engine/render.js';
|
import { backgroundOrder } from '../src/engine/render.js';
|
||||||
import { story } from '../src/story/story.js';
|
import { story } from '../src/story/story.js';
|
||||||
import { solution } from '../src/story/solution.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 { inside } from '../src/engine/geometry.js';
|
||||||
import { existsSync, readFileSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import { settle, exploreTorch } from './helpers.mjs';
|
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);
|
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', () => {
|
test('la ricerca della mappa: pezzi di parola, senza maiuscole, accenti e apostrofi', () => {
|
||||||
assert.ok(matchPlace('Campomigliaio', 'camp'));
|
assert.ok(matchPlace('Campomigliaio', 'camp'));
|
||||||
assert.ok(matchPlace('Via de\' Medici', 'de medici'));
|
assert.ok(matchPlace('Via de\' Medici', 'de medici'));
|
||||||
|
|||||||
@@ -49,5 +49,9 @@
|
|||||||
},
|
},
|
||||||
"ing_tozzi": {
|
"ing_tozzi": {
|
||||||
"speaker": "Badr Odhiambo"
|
"speaker": "Badr Odhiambo"
|
||||||
|
},
|
||||||
|
"pescatori": {
|
||||||
|
"speaker": "Dionisio Schuyler",
|
||||||
|
"filtro": "asetrate=24000*1.08,aresample=24000,atempo=1.04"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||