Il taccuino: le tappe del capitolo e l'obiettivo di adesso (tasto N)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
This commit is contained in:
2026-10-24 21:36:33 +02:00
co-authored by Claude Opus 5.5
parent b24a27b0e8
commit 7dc89c154b
8 changed files with 81 additions and 2 deletions
+4
View File
@@ -108,6 +108,10 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso
secca da quando hanno scavato le gallerie dell'alta velocità, d'inverno torna viva). In basso si va verso La Luna e secca da quando hanno scavato le gallerie dell'alta velocità, d'inverno torna viva). In basso si va verso La Luna e
Campomigliaio (per ora solo battute). Da mettere: le case vecchie con la curva, il rettilineo col cartello della Campomigliaio (per ora solo battute). Da mettere: le case vecchie con la curva, il rettilineo col cartello della
rotonda, l'incrocio coi cartelli (`assets/raw/foto/luna-*.jpg`): ordine da chiedere ad Andrea. rotonda, l'incrocio coi cartelli (`assets/raw/foto/luna-*.jpg`): ordine da chiedere ad Andrea.
- **Taccuino** (24 ottobre, dalla revisione di Codex): pulsante «Taccuino» e tasto N; le tappe in `story.notebook`
(fatte barrate, quella di adesso in evidenza) e in fondo il promemoria del Trebbio. Un test controlla che lungo la
soluzione si spuntino in ordine. Da decidere con Andrea: dove era custodito il palio e chi ha scoperto il furto;
se Andrea vive il 2007 o lo racconta da adulto (al bar c'è ancora il segnaposto «Questo lo sa solo Andrea»).
- **Ricerca nella mappa**: casella «Cerca un posto…», filtra mentre si scrive (`matchPlace`), Invio porta al primo. - **Ricerca nella mappa**: casella «Cerca un posto…», filtra mentre si scrive (`matchPlace`), Invio porta al primo.
- **Scelta del rione** (24 ottobre): dopo «Nuova partita», «Per chi corri quest'anno?» fra i quattro rioni - **Scelta del rione** (24 ottobre): dopo «Nuova partita», «Per chi corri quest'anno?» fra i quattro rioni
(`story.rioni`: Piazza rossi, Pieve bianchi, Cardetole verdi, Campomigliaio blu); resta nella partita come flag (`story.rioni`: Piazza rossi, Pieve bianchi, Cardetole verdi, Campomigliaio blu); resta nella partita come flag
+7
View File
@@ -14,6 +14,7 @@
<div id="corner"> <div id="corner">
<button id="pause-button" hidden title="Pausa (Esc)">Pausa</button> <button id="pause-button" hidden title="Pausa (Esc)">Pausa</button>
<button id="map-button" hidden title="Mappa (P)">Mappa</button> <button id="map-button" hidden title="Mappa (P)">Mappa</button>
<button id="notebook-button" hidden title="Taccuino (N)">Taccuino</button>
<button id="spots-button" hidden title="Mostra cosa si può toccare (H)">?</button> <button id="spots-button" hidden title="Mostra cosa si può toccare (H)">?</button>
</div> </div>
<div id="sound"> <div id="sound">
@@ -51,6 +52,12 @@
<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">Ringraziamenti</button></div> <div class="buttons"><button id="restart">Ricomincia</button><button class="open-credits secondary">Ringraziamenti</button></div>
</section> </section>
<section id="notebook" class="overlay" hidden aria-label="Taccuino">
<h2>Taccuino</h2>
<ol id="notebook-list"></ol>
<p id="notebook-help"></p>
<div class="buttons"><button id="close-notebook">Chiudi</button></div>
</section>
<section id="map" class="overlay" hidden aria-label="Mappa del paese"> <section id="map" class="overlay" hidden aria-label="Mappa del paese">
<div id="map-head"><h2>San Piero a Sieve</h2><input id="map-search" type="search" placeholder="Cerca un posto…" autocomplete="off" spellcheck="false" aria-label="Cerca un posto"></div> <div id="map-head"><h2>San Piero a Sieve</h2><input id="map-search" type="search" placeholder="Cerca un posto…" autocomplete="off" spellcheck="false" aria-label="Cerca un posto"></div>
<p id="map-none" hidden></p> <p id="map-none" hidden></p>
+5
View File
@@ -339,6 +339,11 @@ export class Game {
} }
// La mappa: i posti già visti, con la posizione sulla pianta, e le strade che li collegano (uscite fra due posti visti). // La mappa: i posti già visti, con la posizione sulla pianta, e le strade che li collegano (uscite fra due posti visti).
// Il taccuino: le tappe fatte e quella di adesso (la prima non fatta).
notebook() {
const steps = this.story.notebook ?? [], at = steps.findIndex(s => !check(s.done, this.state));
return { done: (at < 0 ? steps : steps.slice(0, at)).map(s => s.text), now: at < 0 ? null : steps[at].text };
}
mapPlaces() { mapPlaces() {
return Object.entries(this.story.scenes) return Object.entries(this.story.scenes)
.filter(([id, sc]) => sc.map && this.state.flags['visto:' + id]) .filter(([id, sc]) => sc.map && this.state.flags['visto:' + id])
+17
View File
@@ -292,6 +292,20 @@ $('to-title').addEventListener('click', () => {
}); });
$('close-map').addEventListener('click', () => { $('map').hidden = true; }); $('close-map').addEventListener('click', () => { $('map').hidden = true; });
// --- Taccuino: le tappe fatte, barrate, e quella di adesso; in fondo dove chiedere aiuto.
function openNotebook() {
if (!running) return;
const { done, now } = game.notebook();
$('notebook-list').replaceChildren(...done.map(t => Object.assign(document.createElement('li'), { textContent: t, className: 'done' })),
...(now ? [Object.assign(document.createElement('li'), { textContent: now, className: 'now' })] : []));
$('notebook-help').textContent = story.notebookHelp ?? '';
$('notebook').hidden = false;
// Su uno schermo basso il foglio scorre: si parte dalla tappa di adesso, in fondo.
$('notebook-list').scrollTop = $('notebook-list').scrollHeight;
}
$('notebook-button').addEventListener('click', openNotebook);
$('close-notebook').addEventListener('click', () => { $('notebook').hidden = true; });
// --- Salva su file / Carica: la partita in un file .json da aprire su un altro dispositivo. // --- Salva su file / Carica: la partita in un file .json da aprire su un altro dispositivo.
$('save-file').addEventListener('click', () => { $('save-file').addEventListener('click', () => {
const data = game.savable(); const data = game.savable();
@@ -466,9 +480,11 @@ window.addEventListener('keydown', e => {
if (debugKey(e)) return; if (debugKey(e)) 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; } 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 (!$('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; }
if ((e.key === 'p' || e.key === 'P') && running) { openMap(); return; } if ((e.key === 'p' || e.key === 'P') && running) { openMap(); return; }
if ((e.key === 'h' || e.key === 'H') && running) { showSpots(); return; } if ((e.key === 'h' || e.key === 'H') && running) { showSpots(); return; }
if ((e.key === 'n' || e.key === 'N') && running) { openNotebook(); 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') { toggleMute(); return; } if (e.key === 'm' || e.key === 'M') { toggleMute(); return; }
if (!running) return; if (!running) return;
@@ -514,6 +530,7 @@ function updateUi() {
$('map-button').hidden = !running || !!game.dialogue || game.mapPlaces().length < 2; $('map-button').hidden = !running || !!game.dialogue || game.mapPlaces().length < 2;
$('pause-button').hidden = !running; $('pause-button').hidden = !running;
$('spots-button').hidden = !running; $('spots-button').hidden = !running;
$('notebook-button').hidden = !running || !story.notebook;
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;
+10
View File
@@ -44,6 +44,16 @@ export const story = {
// La mappa (pulsante «Mappa»): ogni scena con `map: [x, y]` (percentuali della pianta) ci compare quando è // 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`. // 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] },
// Il taccuino (pulsante «Taccuino», tasto N): le tappe del capitolo in ordine. Una tappa è fatta quando la sua
// condizione `done` è vera; la prima non fatta è l'obiettivo di adesso. Solo testo: non si sente a voce.
notebook: [
{ done: 'luccichio', text: 'Il palio è sparito, e la festa è domani. Chiedo in giro: i vecchietti in piazza sanno sempre tutto.' },
{ done: 'squama_presa', text: 'La vecchietta ha visto luccicare qualcosa sotto la panchina, in piazza.' },
{ done: 'indizio_regolo', text: 'Una squama, grossa e luccicante. Chi l\'ha persa avrà lasciato altre tracce: lassù sotto la Fortezza, magari.' },
{ done: 'casetta_aperta', text: 'Le tracce finiscono in una fessura del bastione, buia pesta. Mi serve una torcia: nella casetta dell\'area feste ce n\'è una, ma è chiusa col lucchetto.' },
{ done: 'palio_trovato', text: 'Ho la torcia: di corsa alla fessura sotto la Fortezza!' },
],
notebookHelp: 'Se non so più cosa fare: al Trebbio, la torre dà sempre un consiglio.',
// I quattro rioni del Palio: dopo «Nuova partita» si sceglie per chi corre Andrea, e la partita lo ricorda col // I quattro rioni del Palio: dopo «Nuova partita» si sceglie per chi corre Andrea, e la partita lo ricorda col
// flag `rione:<id>`. Le battute che parlano del rione di Andrea lo controllano con `if`. // flag `rione:<id>`. Le battute che parlano del rione di Andrea lo controllano con `if`.
// Per ora si corre solo per Campomigliaio (decisione di Andrea, 24 ottobre: ogni rione vuol dire battute e voci // Per ora si corre solo per Campomigliaio (decisione di Andrea, 24 ottobre: ogni rione vuol dire battute e voci
+12 -1
View File
@@ -138,12 +138,23 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
/* Mappa: la pianta del paese a gradini, i posti visti come cartellini, le strade come linee. */ /* Mappa: la pianta del paese a gradini, i posti visti come cartellini, le strade come linee. */
/* In alto a sinistra: Pausa e Mappa. */ /* In alto a sinistra: Pausa e Mappa. */
#corner { position: absolute; top: 6px; left: 6px; z-index: 2; display: flex; gap: 6px; } #corner { position: absolute; top: 6px; left: 6px; z-index: 2; display: flex; gap: 6px; }
#map-button, #pause-button, #spots-button { font: bold clamp(8.64px, 1.15vw, 11.52px) var(--font); padding: .35em .8em; #map-button, #pause-button, #notebook-button, #spots-button { font: bold clamp(8.64px, 1.15vw, 11.52px) var(--font); padding: .35em .8em;
color: var(--accent); background: rgba(27, 20, 32, .62); border: 0; border-radius: 18px; cursor: pointer; } color: var(--accent); background: rgba(27, 20, 32, .62); border: 0; border-radius: 18px; cursor: pointer; }
#pause { z-index: 3; background: rgba(15, 10, 20, .9); } #pause { z-index: 3; background: rgba(15, 10, 20, .9); }
.pause-buttons { flex-direction: column; align-items: stretch; min-width: min(70%, 16em); } .pause-buttons { flex-direction: column; align-items: stretch; min-width: min(70%, 16em); }
#pause .hint { color: var(--dim); max-width: 34em; text-align: center; } #pause .hint { color: var(--dim); max-width: 34em; text-align: center; }
.load-error { margin: .6em 0 0; padding: .4em .8em; color: #fff; background: #7a1f1f; border-radius: 6px; font-size: max(9px, calc(var(--u) * 1.4)); } .load-error { margin: .6em 0 0; padding: .4em .8em; color: #fff; background: #7a1f1f; border-radius: 6px; font-size: max(9px, calc(var(--u) * 1.4)); }
/* Il taccuino: un foglio a righe, le tappe fatte barrate e quella di adesso in evidenza. */
#notebook { z-index: 3; background: rgba(15, 10, 20, .94); }
#notebook h2 { font-size: clamp(12.96px, 2.16vw, 23.04px); margin: 0; }
#notebook-list { width: min(92%, 40em); max-height: 60%; overflow-y: auto; margin: .4em 0; padding: .8em 1em .8em 3em; box-sizing: border-box; text-align: left;
font-size: clamp(8px, 1.15vw, 12px); line-height: 1.7; color: #2a1f30; border-radius: 6px;
background-color: #f4ecd6; background-image: repeating-linear-gradient(transparent 0 calc(1.7em - 1px), #c9d6e8 calc(1.7em - 1px) 1.7em);
background-origin: content-box; background-clip: padding-box; background-attachment: local; }
#notebook-list li { margin: 0; }
#notebook-list li.done { text-decoration: line-through; opacity: .55; }
#notebook-list li.now { font-weight: bold; color: #7a2a1a; }
#notebook-help { margin: 0; color: var(--dim); font-size: clamp(7.2px, 1vw, 10px); }
#map { z-index: 3; justify-content: flex-start; padding-top: 2.5%; background: rgba(15, 10, 20, .94); } #map { z-index: 3; justify-content: flex-start; padding-top: 2.5%; background: rgba(15, 10, 20, .94); }
#map h2 { font-size: clamp(12.96px, 2.16vw, 23.04px); margin: 0; } #map h2 { font-size: clamp(12.96px, 2.16vw, 23.04px); margin: 0; }
#map-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .4em 1.2em; width: 94%; margin: .3em 0; } #map-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .4em 1.2em; width: 94%; margin: .3em 0; }
+8 -1
View File
@@ -238,8 +238,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 settle(page);
// Il taccuino (N): la tappa di adesso in evidenza, Esc lo chiude.
await page.keyboard.press('n');
assert.ok(await page.isVisible('#notebook'));
assert.ok((await page.textContent('#notebook-list li.now')).length > 10, 'c\'è l\'obiettivo di adesso');
await page.screenshot({ path: `${OUT}/07a-taccuino.png` });
await page.keyboard.press('Escape');
assert.ok(await page.isHidden('#notebook'));
// La mappa: dal Trebbio alla Piazza con due clic; i posti mai visti (la Pieve) non ci sono.
await page.click('#map-button'); await page.click('#map-button');
assert.equal(await page.locator('#map-board button[data-scene="pieve"]').count(), 0, 'la Pieve non è ancora vista'); 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"]')); assert.ok(await page.isVisible('#map-board button.here[data-scene="trebbio"]'));
+18
View File
@@ -104,6 +104,24 @@ test('i crediti: i nomi di ogni voce escono in ordine casuale, sempre tutti', ()
assert.ok(orders.size > 3, `${idee}: l'ordine non cambia`); assert.ok(orders.size > 3, `${idee}: l'ordine non cambia`);
}); });
test('il taccuino: lungo la soluzione le tappe si spuntano in ordine, una alla volta, e alla fine sono tutte fatte', () => {
const g = new Game(story);
g.go(story.start.scene, story.start.at);
settle(g);
assert.equal(g.notebook().done.length, 0);
assert.match(g.notebook().now, /vecchietti/);
let last = 0;
for (const [verb, target, item] of solution) {
if (verb === 'choose') g.choose(g.options().findIndex(o => o.text === target)); else g.act(verb, target, item);
settle(g);
const n = g.notebook().done.length;
assert.ok(n === last || n === last + 1, `tappe saltate dopo ${verb} ${target}`);
last = n;
}
assert.equal(g.notebook().now, null);
assert.equal(g.notebook().done.length, story.notebook.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'));