Il libro dell'oratorio in primo piano: copertina, frontespizio, una pagina che gira col fruscio e «FINE» (immagini di Codex, testi disegnati dal codice); l'Arcu a scelte (Borgo e la Casa dell'Armando); il cancello dell'oratorio e l'A. Bettini

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
This commit is contained in:
2026-10-25 14:06:12 +01:00
co-authored by Claude Opus 5.5
parent 9d9dc81bcb
commit f6fd2fdfdc
31 changed files with 359 additions and 53 deletions
+40
View File
@@ -293,6 +293,46 @@ try {
await page.click('#close-map');
await page.setViewportSize({ width: 1280, height: 860 });
// Il libro dell'oratorio: preso, il primo piano; si sfoglia coi clic veri, «FINE» senza che nessuno lo dica,
// Esc chiude; dall'inventario «Apri» lo riapre e il pulsante Chiudi lo chiude. Intanto la scena dietro non risponde.
{
const book = await open({ width: 1280, height: 860 });
await book.click('#splash');
await book.click('#new-game');
await book.click('#rione [data-rione="campomigliaio"]');
await book.click('#skip-intro');
await settle(book);
await book.evaluate(() => game.go('oratorio', [540, 300]));
await settle(book);
await book.click('#verbs [data-verb="take"]');
await clickScene(book, await hotspotCenter(book, 'libro_dimenticato'));
await book.waitForFunction(() => game.closeup?.step === 'cover', null, { timeout: 15000 });
await book.waitForTimeout(600);
await book.screenshot({ path: `${OUT}/03a-libro-copertina.png` });
const where = await book.evaluate(() => [game.state.x, game.state.y]);
await clickScene(book, [320, 200]);
assert.equal(await book.evaluate(() => game.closeup.step), 'title', 'un clic apre il libro');
await clickScene(book, [320, 200]);
assert.equal(await book.evaluate(() => game.closeup.step), 'turn', 'un altro gira la pagina');
await book.waitForFunction(() => game.closeup?.step === 'end');
assert.equal(await book.evaluate(() => game.speech), null, '«FINE» non lo dice nessuno');
await book.screenshot({ path: `${OUT}/03b-libro-fine.png` });
await book.waitForFunction(() => game.speech?.text === 'Mi ricordavo un finale diverso.', null, { timeout: 5000 });
assert.deepEqual(await book.evaluate(() => [game.state.x, game.state.y]), where, 'i clic sul libro non muovono Andrea');
await book.keyboard.press('Space');
await book.keyboard.press('Escape');
assert.equal(await book.evaluate(() => game.closeup), null, 'Esc chiude il libro');
await book.waitForSelector('#closeup-close', { state: 'hidden' });
await settle(book);
await book.click('#verbs [data-verb="open"]');
await book.click('#inventory button');
await book.waitForFunction(() => game.closeup?.step === 'cover');
assert.ok(await book.isVisible('#closeup-close'), 'si vede il pulsante Chiudi');
await book.click('#closeup-close');
assert.equal(await book.evaluate(() => game.closeup), null, 'Chiudi chiude il libro');
await book.close();
}
const phone = await open({ width: 390, height: 844 });
await phone.click('#splash'); // un clic salta il cartello
assert.ok(await phone.isVisible('#title'), 'il clic sul cartello porta al titolo');
+2
View File
@@ -6,6 +6,8 @@ export function settle(game, { maxSteps = 20000 } = {}) {
said.push(game.speech);
game.skip();
}
// Un primo piano aperto aspetta il giocatore (sfogliare, chiudere): qui ci si ferma e decide il test.
if (game.closeup && !game.speech && game.reach <= 0) return said;
// Un ultimo fotogramma, come nel gioco vero: a sequenza finita il motore prende il punto fermo e salva.
if (!game.busy && !game.walking && !game.queue.length && !game.task) { game.update(0); return said; }
game.update(0.05);
+44 -6
View File
@@ -8,7 +8,7 @@ import { inside } from '../src/engine/geometry.js';
import { existsSync, readFileSync } from 'node:fs';
import { settle } from './helpers.mjs';
const EFFECTS = ['say', 'walk', 'face', 'give', 'drop', 'set', 'unset', 'if', 'dialogue', 'node', 'endDialogue', 'go', 'end'];
const EFFECTS = ['say', 'walk', 'face', 'give', 'drop', 'set', 'unset', 'if', 'dialogue', 'node', 'endDialogue', 'go', 'end', 'closeup'];
// Visita ogni elenco di effetti della storia, dentro scene, oggetti, dialoghi e rami `if`.
function* effectLists() {
@@ -63,6 +63,11 @@ test('la storia è coerente: scene, oggetti, dialoghi e punti calpestabili esist
if (e.give || e.drop) assert.ok(story.items[e.give ?? e.drop], `${where}: oggetto ${e.give ?? e.drop}`);
if (e.dialogue) assert.ok(story.dialogues[e.dialogue], `${where}: dialogo ${e.dialogue}`);
if (e.go) assert.ok(story.scenes[e.go], `${where}: scena ${e.go}`);
if (e.closeup) {
const c = story.closeups?.[e.closeup];
assert.ok(c, `${where}: primo piano ${e.closeup}`);
for (const src of [c.closed, c.open]) assert.ok(existsSync(new URL('../' + src, import.meta.url)), `${where}: manca ${src}`);
}
if (e.who) assert.ok(story.characters[e.who], `${where}: personaggio ${e.who}`);
}
for (const [id, d] of Object.entries(story.dialogues)) {
@@ -192,18 +197,46 @@ test('il foglio di Andrea ha tutti i fotogrammi e l\'immagine', () => {
}
});
test('il libro dell\'oratorio: «La storia infinita» ha una pagina sola', () => {
test('il libro dell\'oratorio: il primo piano si sfoglia, l\'ultima pagina è «FINE» e nessuno la dice prima', () => {
const g = new Game(story);
g.go('oratorio', [320, 345]);
settle(g);
assert.ok(g.hotspotAt([570, 205])?.id === 'libro_dimenticato', 'sulle tavolate il clic prende il libro, non le tavolate');
assert.ok(g.act('take', 'libro_dimenticato'));
settle(g);
const before = settle(g).map(s => s.text);
assert.deepEqual(before, ['Ricordo di aver visto il film da piccolo... era stupendo!']);
assert.ok(g.state.inventory.includes('libro'));
assert.equal(g.hotspot('libro_dimenticato'), undefined, 'preso, il libro non resta sul tavolo');
assert.equal(g.closeup?.step, 'cover', 'preso in mano, il primo piano sulla copertina');
// Durante il primo piano gli oggetti dell'oratorio non rispondono.
assert.equal(g.act('take', 'gomma_terra'), false);
assert.equal(g.walkTo([300, 340]), false);
assert.ok(g.closeupTap());
assert.equal(g.closeup.step, 'title');
assert.ok(g.closeupTap());
assert.equal(g.closeup.step, 'turn');
assert.equal(g.closeupTap(), false, 'una pagina sola: mentre gira non se ne gira un\'altra');
for (let i = 0; i < 20; i++) g.update(0.05);
assert.equal(g.closeup.step, 'end');
assert.equal(g.speech, null, '«FINE» si scopre guardando: nessuna battuta subito');
for (let i = 0; i < 40 && !g.speech; i++) g.update(0.05);
assert.equal(g.speech?.text, 'Mi ricordavo un finale diverso.');
assert.deepEqual(g.speakerPoint('andrea'), [320, 34], 'nel primo piano Andrea parla da sopra il libro');
for (let i = 0; i < 200; i++) g.update(0.05);
assert.equal(g.closeup?.step, 'end', '«FINE» resta finché non si chiude');
assert.equal(g.closeupTap(), false);
assert.ok(g.closeupClose());
assert.deepEqual(settle(g).map(s => s.text), ['Il Nulla sta viaggiando troppo forte... devo fermarlo!']);
assert.equal(g.closeup, null);
// Dall'inventario si riapre; chiuso prima della fine, nessun commento.
assert.ok(g.act('open', 'libro'));
const said = settle(g).map(s => s.text);
assert.ok(said.some(t => t.includes('FINE')), 'aperto, dice FINE');
settle(g);
assert.equal(g.closeup?.step, 'cover');
assert.ok(g.closeupClose());
assert.deepEqual(settle(g), []);
// Nessuna battuta racconta la gag: «FINE» c'è solo nel disegno.
const lines = JSON.stringify([story.items.libro, story.scenes.oratorio]);
assert.ok(!lines.includes('FINE'), 'nessuna battuta anticipa «FINE»');
});
test('la mappa: ogni scena ci sta, i posti compaiono solo visti, il viaggio rispetta i dialoghi', () => {
@@ -475,7 +508,12 @@ test('gli amici in giro: l\'Arcu al circolino, il Tozzino al cancello del tennis
const g = new Game(story);
settle(g);
const talk = (scene, who) => { g.go(scene); settle(g); g.act('talk', who); return settle(g).map(s => `${s.who ?? 'andrea'}: ${s.text}`); };
assert.deepEqual(talk('campomigliaio_circolo', 'arcu'), ['andrea: Arcu! Ma che è sempre là, Borgo?', 'arcu: E chi lo sposta???']);
talk('campomigliaio_circolo', 'arcu');
const ask = text => { g.choose(g.options().findIndex(o => o.text === text)); return settle(g).map(s => `${s.who ?? 'andrea'}: ${s.text}`); };
assert.deepEqual(ask('Ma che è sempre là, Borgo?'), ['andrea: Ma che è sempre là, Borgo?', 'arcu: E chi lo sposta???']);
assert.deepEqual(ask('Come va alla Casa dell\'Armando?'), ['andrea: Come va alla Casa dell\'Armando?', 'arcu: Mah... un si fa una sega...']);
ask('Ci si vede, Arcu.');
assert.equal(g.dialogue, null);
assert.ok(talk('carza', 'tozzino').includes('tozzino: E chi sennò? Ha chiamato ora: è imbottigliato in Bolognese.'));
const baron = talk('polisportiva', 'barondeduke');
assert.ok(baron.includes('barondeduke: Avevo due donne e due jack. Entra la terza donna, e penso: «Male male si divide».'));