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:
@@ -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');
|
||||
|
||||
@@ -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
@@ -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».'));
|
||||
|
||||
Reference in New Issue
Block a user