Mappa del paese per spostarsi veloci; Codex nei crediti

Il pulsante «Mappa» (o il tasto P) apre la pianta del paese a gradini: ci sono i posti già visti,
le strade sono le uscite fra loro, un clic porta lì (game.travel, mai durante un dialogo o una
battuta). Ogni scena ha la sua posizione in `map`; un posto nuovo entra con una riga. Test: posti
solo visti, strade, viaggio rifiutato in un dialogo; nel browser dal Trebbio alla Piazza.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UdeKd62opy9PKgpQXtVctF
This commit is contained in:
2026-10-23 15:59:29 +02:00
co-authored by Claude Opus 5.5
parent e743315f0f
commit ce4da39c8d
8 changed files with 123 additions and 1 deletions
+4
View File
@@ -25,6 +25,10 @@ Ultimo aggiornamento: 23 ottobre 2026. Da leggere insieme a `PROGETTO.md` all'in
- **Voci** (`make voci`, XTTS-v2 sulla GeForce): 112 battute, cinque voci (Andrea, vecchietta, vecchietto, castello, - **Voci** (`make voci`, XTTS-v2 sulla GeForce): 112 battute, cinque voci (Andrea, vecchietta, vecchietto, castello,
leone); il fumetto dura quanto la voce, la musica si abbassa mentre si parla, il muto spegne anche le voci. leone); il fumetto dura quanto la voce, la musica si abbassa mentre si parla, il muto spegne anche le voci.
Controllate tutte con Whisper il 2026-10-23. Il leone ora parla (battute provvisorie). Controllate tutte con Whisper il 2026-10-23. Il leone ora parla (battute provvisorie).
- **Mappa** (pulsante in alto a sinistra o tasto P): i posti già visti, con le strade fra loro; un clic ci porta.
Un posto nuovo entra con `map: [x, y]` nella sua scena (percentuali della pianta, disposta come «il paese a gradini»).
- **Crediti** dal titolo e dalla fine (`story.credits`), dedica al Lele in testa; musichetta a 8 bit del cartello
(`tools/jingle.py`).
- **Il libro della storia infinita** sulle tavolate dell'oratorio; le idee in attesa in `docs/IDEE.md`. - **Il libro della storia infinita** sulle tavolate dell'oratorio; le idee in attesa in `docs/IDEE.md`.
- **Test**: `make test` (29 test, compresa la soluzione completa da `src/story/solution.js`) e `make browser` - **Test**: `make test` (29 test, compresa la soluzione completa da `src/story/solution.js`) e `make browser`
(partita completa con clic veri in Chromium, ripresa del salvataggio, telefono). (partita completa con clic veri in Chromium, ripresa del salvataggio, telefono).
+6
View File
@@ -11,6 +11,7 @@
<div id="stage"> <div id="stage">
<canvas id="scene" width="640" height="360" aria-label="La scena dell'avventura"></canvas> <canvas id="scene" width="640" height="360" aria-label="La scena dell'avventura"></canvas>
<div id="speech" aria-live="polite"></div> <div id="speech" aria-live="polite"></div>
<button id="map-button" hidden title="Mappa (P)">Mappa</button>
<div id="sound"> <div id="sound">
<button id="mute" aria-label="Musica: togli l'audio" title="Musica (M)">♪</button> <button id="mute" aria-label="Musica: togli l'audio" title="Musica (M)">♪</button>
<input id="volume" type="range" min="0" max="100" step="5" aria-label="Volume della musica"> <input id="volume" type="range" min="0" max="100" step="5" aria-label="Volume della musica">
@@ -37,6 +38,11 @@
<p class="dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p> <p class="dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p>
<div class="buttons"><button id="restart">Ricomincia</button><button class="open-credits secondary">Crediti</button></div> <div class="buttons"><button id="restart">Ricomincia</button><button class="open-credits secondary">Crediti</button></div>
</section> </section>
<section id="map" class="overlay" hidden aria-label="Mappa del paese">
<h2>San Piero a Sieve</h2>
<div id="map-board"><svg id="map-roads" viewBox="0 0 100 100" preserveAspectRatio="none"></svg></div>
<div class="buttons"><button id="close-map">Chiudi</button></div>
</section>
<section id="credits" class="overlay" hidden aria-label="Crediti"> <section id="credits" class="overlay" hidden aria-label="Crediti">
<h2>Crediti</h2> <h2>Crediti</h2>
<p class="dedication credits-dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p> <p class="dedication credits-dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p>
+23
View File
@@ -255,6 +255,28 @@ export class Game {
else throw new Error('Effetto sconosciuto: ' + JSON.stringify(e)); else throw new Error('Effetto sconosciuto: ' + JSON.stringify(e));
} }
// La mappa: i posti già visti, con la posizione sulla pianta, e le strade che li collegano (uscite fra due posti visti).
mapPlaces() {
return Object.entries(this.story.scenes)
.filter(([id, sc]) => sc.map && this.state.flags['visto:' + id])
.map(([id, sc]) => ({ id, name: sc.name, at: sc.map, here: id === this.state.scene }));
}
mapRoads() {
const seen = new Set(this.mapPlaces().map(p => p.id)), roads = new Map();
for (const id of seen)
for (const h of this.story.scenes[id].hotspots ?? [])
if (h.exit && seen.has(h.exit.scene) && h.exit.scene !== id) roads.set([id, h.exit.scene].sort().join('|'), [id, h.exit.scene].sort());
return [...roads.values()];
}
// Viaggio dalla mappa: solo verso un posto già visto, e non mentre si parla, si cammina o si sceglie in un dialogo.
travel(scene) {
const sc = this.story.scenes[scene];
if (!sc?.map || !this.state.flags['visto:' + scene] || scene === this.state.scene) return false;
if (this.busy || this.walking || this.dialogue || this.state.ended) return false;
this.go(scene, sc.entry);
return true;
}
go(scene, at, face) { go(scene, at, face) {
if (!this.story.scenes[scene]) throw new Error('Scena sconosciuta: ' + scene); if (!this.story.scenes[scene]) throw new Error('Scena sconosciuta: ' + scene);
this.state.scene = scene; this.state.scene = scene;
@@ -263,6 +285,7 @@ export class Game {
this.path = []; this.path = [];
this.walking = false; this.walking = false;
this.back = false; this.back = false;
this.state.flags['visto:' + scene] = true; // per la mappa
this.queue.unshift(...(this.scene.enter ?? [])); this.queue.unshift(...(this.scene.enter ?? []));
this.onSave(this.snapshot()); this.onSave(this.snapshot());
} }
+29
View File
@@ -78,6 +78,31 @@ $('credits-list').replaceChildren(...(story.credits ?? []).flatMap(({ role, name
for (const b of document.querySelectorAll('.open-credits')) b.addEventListener('click', () => { $('credits').hidden = false; }); for (const b of document.querySelectorAll('.open-credits')) b.addEventListener('click', () => { $('credits').hidden = false; });
$('close-credits').addEventListener('click', () => { $('credits').hidden = true; }); $('close-credits').addEventListener('click', () => { $('credits').hidden = true; });
// --- Mappa: i posti visti e le strade fra loro; un clic su un posto ci porta (game.travel).
function openMap() {
if (!running || game.dialogue) return;
const board = $('map-board'), svg = $('map-roads'), places = game.mapPlaces(), at = Object.fromEntries(places.map(p => [p.id, p.at]));
svg.replaceChildren(...game.mapRoads().map(([a, b]) => {
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
[['x1', at[a][0]], ['y1', at[a][1]], ['x2', at[b][0]], ['y2', at[b][1]]].forEach(([k, v]) => line.setAttribute(k, v));
return line;
}));
board.replaceChildren(svg, ...places.map(p => {
const b = document.createElement('button');
b.type = 'button';
b.dataset.scene = p.id;
b.textContent = p.name;
b.classList.toggle('here', p.here);
b.style.left = p.at[0] + '%';
b.style.top = p.at[1] + '%';
b.addEventListener('click', () => { if (game.travel(p.id)) $('map').hidden = true; });
return b;
}));
$('map').hidden = false;
}
$('map-button').addEventListener('click', openMap);
$('close-map').addEventListener('click', () => { $('map').hidden = true; });
function showTitle() { function showTitle() {
running = false; running = false;
const saved = storage && readSave(storage); const saved = storage && readSave(storage);
@@ -170,6 +195,8 @@ function updateChoices() {
window.addEventListener('keydown', e => { window.addEventListener('keydown', e => {
if (e.target === $('volume')) return; if (e.target === $('volume')) return;
if (!$('credits').hidden) { if (e.key === 'Escape') $('credits').hidden = true; return; } if (!$('credits').hidden) { if (e.key === 'Escape') $('credits').hidden = true; return; }
if (!$('map').hidden) { if (e.key === 'Escape' || e.key === 'p' || e.key === 'P') $('map').hidden = true; return; }
if ((e.key === 'p' || e.key === 'P') && running) { openMap(); return; }
if (!$('intro').hidden && [' ', 'Enter'].includes(e.key)) { e.preventDefault(); nextIntro(); return; } if (!$('intro').hidden && [' ', 'Enter'].includes(e.key)) { e.preventDefault(); nextIntro(); return; }
if (e.key === 'm' || e.key === 'M') { music.toggle(); saveAudio(); showSound(); return; } if (e.key === 'm' || e.key === 'M') { music.toggle(); saveAudio(); showSound(); return; }
if (!running) return; if (!running) return;
@@ -204,6 +231,8 @@ function updateUi() {
updateInventory(); updateInventory();
updateChoices(); updateChoices();
updateSpeech(); updateSpeech();
// Il pulsante della mappa c'è solo in partita, fuori dai dialoghi, e quando c'è almeno un altro posto dove andare.
$('map-button').hidden = !running || !!game.dialogue || game.mapPlaces().length < 2;
if (game.state.ended && !game.speech && !game.queue.length) { if (game.state.ended && !game.speech && !game.queue.length) {
running = false; running = false;
$('ending').hidden = false; $('ending').hidden = false;
+14 -1
View File
@@ -4,6 +4,8 @@
export const story = { export const story = {
title: 'La Leggenda del Regolo', title: 'La Leggenda del Regolo',
// La mappa (pulsante «Mappa»): ogni scena con `map: [x, y]` (percentuali della pianta) ci compare quando è
// stata visitata; le strade sono le uscite fra scene. Un posto nuovo entra con la sua riga `map`.
start: { scene: 'piazza', at: [250, 320] }, start: { scene: 'piazza', at: [250, 320] },
// Le righe dell'introduzione, sopra il panorama del paese, prima della prima scena. // Le righe dell'introduzione, sopra il panorama del paese, prima della prima scena.
intro: [ intro: [
@@ -23,7 +25,7 @@ export const story = {
{ role: 'Musica', names: ['Andrea Manzi, con Suno'] }, { role: 'Musica', names: ['Andrea Manzi, con Suno'] },
{ role: 'Sfondi', names: ['dalle fotografie di San Piero di Andrea'] }, { role: 'Sfondi', names: ['dalle fotografie di San Piero di Andrea'] },
{ role: 'Voci', names: ['XTTS-v2 di Coqui'] }, { role: 'Voci', names: ['XTTS-v2 di Coqui'] },
{ role: 'Programmazione', names: ['Andrea Manzi, con Claude'] }, { role: 'Programmazione', names: ['Andrea Manzi, con Claude e Codex'] },
], ],
characters: { characters: {
andrea: { name: 'Andrea', color: '#ffffff', sprite: 'assets/characters/andrea.json' }, andrea: { name: 'Andrea', color: '#ffffff', sprite: 'assets/characters/andrea.json' },
@@ -59,6 +61,7 @@ export const story = {
scenes: { scenes: {
piazza: { piazza: {
name: 'Piazza Colonna', name: 'Piazza Colonna',
map: [16, 70],
background: 'assets/scenes/piazza-colonna.png', background: 'assets/scenes/piazza-colonna.png',
walk: [[84, 244], [330, 238], [345, 230], [362, 238], [462, 246], [466, 276], [544, 276], [548, 248], [588, 250], [600, 290], [640, 300], [640, 360], [0, 360], [0, 330], [84, 318]], walk: [[84, 244], [330, 238], [345, 230], [362, 238], [462, 246], [466, 276], [544, 276], [548, 248], [588, 250], [600, 290], [640, 300], [640, 360], [0, 360], [0, 330], [84, 318]],
depth: [[236, 0.5], [360, 1.35]], depth: [[236, 0.5], [360, 1.35]],
@@ -140,6 +143,7 @@ export const story = {
}, },
fortezza: { fortezza: {
name: 'Sotto le mura della Fortezza', name: 'Sotto le mura della Fortezza',
map: [30, 15],
background: 'assets/scenes/fortezza-esterno.png', background: 'assets/scenes/fortezza-esterno.png',
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]],
@@ -190,6 +194,7 @@ export const story = {
// Livello 0: la Pieve vista dalla salita, con la salita per Villa Adami alle spalle. // Livello 0: la Pieve vista dalla salita, con la salita per Villa Adami alle spalle.
pieve: { pieve: {
name: 'La Pieve', name: 'La Pieve',
map: [46, 70],
background: 'assets/scenes/pieve-dalla-salita.png', background: 'assets/scenes/pieve-dalla-salita.png',
// La rampa in primo piano e, dietro il muretto, il sagrato (ci si arriva da sinistra). // La rampa in primo piano e, dietro il muretto, il sagrato (ci si arriva da sinistra).
walk: [[0, 270], [80, 266], [86, 252], [100, 222], [270, 215], [300, 224], [480, 234], [560, 242], [600, 252], [610, 268], [276, 240], [276, 270], [292, 278], [640, 352], [640, 360], [0, 360]], walk: [[0, 270], [80, 266], [86, 252], [100, 222], [270, 215], [300, 224], [480, 234], [560, 242], [600, 252], [610, 268], [276, 240], [276, 270], [292, 278], [640, 352], [640, 360], [0, 360]],
@@ -230,6 +235,7 @@ export const story = {
// Il cortile dell'oratorio, dietro la porta della canonica: palco, tettoia, tavolate della festa. // Il cortile dell'oratorio, dietro la porta della canonica: palco, tettoia, tavolate della festa.
oratorio: { oratorio: {
name: 'Oratorio della Pieve', name: 'Oratorio della Pieve',
map: [74, 70],
background: 'assets/scenes/oratorio.png', background: 'assets/scenes/oratorio.png',
walk: [[0, 236], [300, 226], [480, 222], [500, 232], [640, 254], [640, 360], [0, 360]], walk: [[0, 236], [300, 226], [480, 222], [500, 232], [640, 254], [640, 360], [0, 360]],
depth: [[222, 0.5], [360, 1.15]], depth: [[222, 0.5], [360, 1.15]],
@@ -279,6 +285,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 Piazza sale verso Villa Adami (la Piazza è alle spalle, in fondo si va su).
via_de_medici: { via_de_medici: {
name: 'Via de\' Medici', name: 'Via de\' Medici',
map: [14, 52],
background: 'assets/scenes/via-de-medici.png', background: 'assets/scenes/via-de-medici.png',
walk: [[0, 360], [0, 336], [160, 290], [262, 240], [288, 212], [312, 208], [340, 222], [470, 300], [545, 360]], walk: [[0, 360], [0, 336], [160, 290], [262, 240], [288, 212], [312, 208], [340, 222], [470, 300], [545, 360]],
depth: [[208, 0.32], [360, 1.25]], depth: [[208, 0.32], [360, 1.25]],
@@ -302,6 +309,7 @@ export const story = {
// L'ingresso del campeggio: dalla Piazza a diritto; poco dopo, a destra, si sale al Bosco ai Frati. // L'ingresso del campeggio: dalla Piazza a diritto; poco dopo, a destra, si sale al Bosco ai Frati.
campeggio: { campeggio: {
name: 'Ingresso del campeggio', name: 'Ingresso del campeggio',
map: [16, 88],
background: 'assets/scenes/campeggio.png', background: 'assets/scenes/campeggio.png',
walk: [[150, 360], [165, 262], [420, 266], [640, 286], [640, 360]], walk: [[150, 360], [165, 262], [420, 266], [640, 286], [640, 360]],
depth: [[262, 0.6], [360, 1.15]], depth: [[262, 0.6], [360, 1.15]],
@@ -332,6 +340,7 @@ export const story = {
// Il convento del Bosco ai Frati, nel bosco sopra il campeggio. // Il convento del Bosco ai Frati, nel bosco sopra il campeggio.
bosco_ai_frati: { bosco_ai_frati: {
name: 'Bosco ai Frati', name: 'Bosco ai Frati',
map: [42, 88],
background: 'assets/scenes/bosco-ai-frati.png', background: 'assets/scenes/bosco-ai-frati.png',
walk: [[0, 304], [100, 298], [480, 292], [640, 300], [640, 360], [0, 360]], walk: [[0, 304], [100, 298], [480, 292], [640, 300], [640, 360], [0, 360]],
depth: [[292, 0.78], [360, 1.1]], depth: [[292, 0.78], [360, 1.1]],
@@ -366,6 +375,7 @@ export const story = {
// Livello 1: Villa Adami, vista dalla salita della Fortezza guardando in discesa. // Livello 1: Villa Adami, vista dalla salita della Fortezza guardando in discesa.
villa_adami: { villa_adami: {
name: 'Villa Adami', name: 'Villa Adami',
map: [34, 48],
background: 'assets/scenes/villa-adami.png', background: 'assets/scenes/villa-adami.png',
walk: [[0, 272], [30, 246], [60, 244], [110, 256], [190, 268], [360, 296], [440, 288], [520, 276], [560, 268], [612, 262], [614, 272], [520, 288], [500, 292], [500, 360], [0, 360]], walk: [[0, 272], [30, 246], [60, 244], [110, 256], [190, 268], [360, 296], [440, 288], [520, 276], [560, 268], [612, 262], [614, 272], [520, 288], [500, 292], [500, 360], [0, 360]],
depth: [[244, 0.5], [360, 1.2]], depth: [[244, 0.5], [360, 1.2]],
@@ -392,6 +402,7 @@ export const story = {
// Tra il livello 1 e il 2: la salita di Villa Adami, con la casa del leone. Si sale alla Fortezza. // Tra il livello 1 e il 2: la salita di Villa Adami, con la casa del leone. Si sale alla Fortezza.
salita_villa_adami: { salita_villa_adami: {
name: 'Salita di Villa Adami', name: 'Salita di Villa Adami',
map: [30, 32],
background: 'assets/scenes/salita-villa-adami.png', background: 'assets/scenes/salita-villa-adami.png',
walk: [[0, 318], [100, 312], [282, 318], [300, 300], [318, 262], [336, 252], [420, 266], [640, 296], [640, 360], [0, 360]], walk: [[0, 318], [100, 312], [282, 318], [300, 300], [318, 262], [336, 252], [420, 266], [640, 296], [640, 360], [0, 360]],
depth: [[252, 0.5], [360, 1.1]], depth: [[252, 0.5], [360, 1.1]],
@@ -424,6 +435,7 @@ export const story = {
// Il viale dei cipressi, la strada bianca tra la Fortezza e il Trebbio: in fondo c'è il castello. // Il viale dei cipressi, la strada bianca tra la Fortezza e il Trebbio: in fondo c'è il castello.
viale_cipressi: { viale_cipressi: {
name: 'Viale dei cipressi', name: 'Viale dei cipressi',
map: [55, 15],
background: 'assets/scenes/viale-cipressi.png', background: 'assets/scenes/viale-cipressi.png',
walk: [[30, 360], [200, 300], [288, 252], [312, 250], [420, 300], [600, 360]], walk: [[30, 360], [200, 300], [288, 252], [312, 250], [420, 300], [600, 360]],
depth: [[250, 0.16], [360, 1.2]], depth: [[250, 0.16], [360, 1.2]],
@@ -446,6 +458,7 @@ export const story = {
}, },
trebbio: { trebbio: {
name: 'Castello del Trebbio', name: 'Castello del Trebbio',
map: [80, 15],
background: 'assets/scenes/trebbio.png', background: 'assets/scenes/trebbio.png',
music: 'assets/audio/ricordi.mp3', music: 'assets/audio/ricordi.mp3',
walk: [[86, 308], [300, 306], [356, 308], [470, 312], [500, 324], [486, 360], [88, 360], [80, 330]], walk: [[86, 308], [300, 306], [356, 308], [470, 312], [500, 324], [486, 360], [88, 360], [80, 330]],
+14
View File
@@ -95,6 +95,20 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
#continue { background: #7fe08a; } #continue { background: #7fe08a; }
.buttons button.secondary { color: var(--ink); background: rgba(42, 32, 48, .85); box-shadow: inset 0 0 0 2px var(--dim); } .buttons button.secondary { color: var(--ink); background: rgba(42, 32, 48, .85); box-shadow: inset 0 0 0 2px var(--dim); }
/* Crediti: sopra titolo e fine (z-index), una voce per ruolo; scorrono se non ci stanno. */ /* Crediti: sopra titolo e fine (z-index), una voce per ruolo; scorrono se non ci stanno. */
/* Mappa: la pianta del paese a gradini, i posti visti come cartellini, le strade come linee. */
#map-button { position: absolute; top: 6px; left: 6px; z-index: 2; font: bold clamp(12px, 1.6vw, 16px) var(--font); padding: .35em .8em;
color: var(--accent); background: rgba(27, 20, 32, .62); border: 0; border-radius: 18px; cursor: pointer; }
#map { z-index: 3; justify-content: flex-start; padding-top: 2.5%; background: rgba(15, 10, 20, .94); }
#map h2 { font-size: clamp(18px, 3vw, 32px); }
#map-board { position: relative; width: 92%; flex: 1; min-height: 0; margin: .3em 0;
background: repeating-linear-gradient(transparent 0 calc(25% - 1px), rgba(185, 169, 194, .12) calc(25% - 1px) 25%), radial-gradient(circle at 50% 60%, #3a2d42, #231a29); border-radius: 8px; }
#map-roads { position: absolute; inset: 0; width: 100%; height: 100%; }
#map-roads line { stroke: #b9a9c2; stroke-width: 2.5; stroke-dasharray: 5 4; vector-effect: non-scaling-stroke; opacity: .7; }
#map-board button { position: absolute; transform: translate(-50%, -50%); font: bold clamp(10px, 1.5vw, 15px) var(--font); padding: .3em .6em;
color: var(--bg); background: var(--ink); border: 2px solid #000; border-radius: 6px; cursor: pointer; white-space: nowrap; }
#map-board button:hover { background: var(--accent); }
#map-board button.here { color: var(--ink); background: #2f5fb3; cursor: default; }
#map-board button.here::after { content: ' · sei qui'; font-weight: normal; }
#credits { z-index: 3; justify-content: flex-start; overflow-y: auto; padding-top: 4%; background: rgba(15, 10, 20, .96); } #credits { z-index: 3; justify-content: flex-start; overflow-y: auto; padding-top: 4%; background: rgba(15, 10, 20, .96); }
#credits .credits-dedication { margin: .2em 0 .5em !important; font-size: clamp(16px, 2.6vw, 28px); } #credits .credits-dedication { margin: .2em 0 .5em !important; font-size: clamp(16px, 2.6vw, 28px); }
/* Due colonne: il ruolo a destra della sua colonna, il nome a sinistra della sua, allineati sulla riga. */ /* Due colonne: il ruolo a destra della sua colonna, il nome a sinistra della sua, allineati sulla riga. */
+9
View File
@@ -136,6 +136,15 @@ try {
await page.waitForSelector('#continue:not([hidden])'); await page.waitForSelector('#continue:not([hidden])');
await page.click('#continue'); await page.click('#continue');
assert.equal(await page.evaluate(() => game.state.scene), 'trebbio'); assert.equal(await page.evaluate(() => game.state.scene), 'trebbio');
// La mappa: dal Trebbio alla Piazza con due clic; i posti mai visti (la Pieve) non ci sono.
await settle(page);
await page.click('#map-button');
assert.equal(await page.locator('#map-board button[data-scene="pieve"]').count(), 0, 'la Pieve non è ancora vista');
assert.ok(await page.isVisible('#map-board button.here[data-scene="trebbio"]'));
await page.screenshot({ path: `${OUT}/07b-mappa.png` });
await page.click('#map-board button[data-scene="piazza"]');
assert.ok(await page.isHidden('#map'));
assert.equal(await page.evaluate(() => game.state.scene), 'piazza');
const phone = await open({ width: 390, height: 844 }); const phone = await open({ width: 390, height: 844 });
await phone.click('#splash'); // un clic salta il cartello await phone.click('#splash'); // un clic salta il cartello
+24
View File
@@ -145,3 +145,27 @@ test('il libro dell\'oratorio: «La storia infinita» ha una pagina sola', () =>
const said = settle(g).map(s => s.text); const said = settle(g).map(s => s.text);
assert.ok(said.some(t => t.includes('FINE')), 'aperto, dice FINE'); assert.ok(said.some(t => t.includes('FINE')), 'aperto, dice FINE');
}); });
test('la mappa: ogni scena ci sta, i posti compaiono solo visti, il viaggio rispetta i dialoghi', () => {
for (const [id, sc] of Object.entries(story.scenes)) {
assert.ok(Array.isArray(sc.map), `${id} non è sulla mappa`);
assert.ok(sc.map.every(v => v >= 0 && v <= 100), `${id} fuori dalla pianta`);
}
const g = new Game(story);
g.go('piazza', story.start.at);
settle(g);
assert.deepEqual(g.mapPlaces().map(p => p.id), ['piazza']);
assert.equal(g.travel('trebbio'), false, 'non si va dove non si è mai stati');
g.go('pieve', story.scenes.pieve.entry);
settle(g);
assert.deepEqual(g.mapRoads(), [['piazza', 'pieve']], 'la strada fra due posti visti');
assert.ok(g.mapPlaces().find(p => p.id === 'pieve').here);
assert.ok(g.travel('piazza'));
settle(g);
assert.equal(g.state.scene, 'piazza');
assert.equal(g.travel('piazza'), false, 'già qui');
g.act('talk', 'vecchietti');
settle(g);
assert.ok(g.dialogue);
assert.equal(g.travel('pieve'), false, 'non si scappa da un dialogo');
});