Revisione di Codex: salvataggi a punti fermi, salva e carica da file, volume delle voci
- si salva solo a sequenza finita: uscire a metà di una risposta o della casetta riporta a prima; la squama presa resta salvata anche senza cambiare scena; test di interruzione su soluzione e catena del ghiacciolo - «Salva su file» nella pausa e «Carica» nel titolo, con controllo di versione e contenuto prima di sostituire il salvataggio del browser - cursore del volume delle voci («bla»), 50% di partenza; il muto zittisce subito la voce, la pausa la riprende - col dito un oggetto mancato di poco si prende, e i piccoli vincono sui grandi che li contengono (la squama); verbi su due colonne nel telefono in orizzontale - sfondi caricati per vicinanza alla scena di partenza, con tentativi limitati se falliscono; a gioco fermo non si ridisegna - modalità ?debug per scrivere le scene: area calpestabile, hotspot, punti, coordinate copiabili - corretto: i crediti dal titolo restavano dietro il titolo Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
This commit is contained in:
+54
-5
@@ -3,7 +3,7 @@
|
||||
// npm run browser
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { mkdirSync, readFileSync } from 'node:fs';
|
||||
import assert from 'node:assert/strict';
|
||||
import { solution } from '../src/story/solution.js';
|
||||
|
||||
@@ -13,12 +13,12 @@ const server = spawn('python3', ['-m', 'http.server', String(PORT), '--bind', '1
|
||||
const browser = await chromium.launch({ args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
||||
const errors = [];
|
||||
|
||||
async function open(viewport) {
|
||||
const page = await browser.newPage({ viewport });
|
||||
async function open(viewport, query = '') {
|
||||
const page = await browser.newPage({ viewport, acceptDownloads: true });
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
page.on('console', m => m.type() === 'error' && errors.push(m.text()));
|
||||
for (let i = 0; i < 50; i++) {
|
||||
try { await page.goto(`http://127.0.0.1:${PORT}/`); break; } catch { await page.waitForTimeout(100); }
|
||||
try { await page.goto(`http://127.0.0.1:${PORT}/${query}`); break; } catch { await page.waitForTimeout(100); }
|
||||
}
|
||||
return page;
|
||||
}
|
||||
@@ -63,6 +63,9 @@ try {
|
||||
assert.match(await page.textContent('#credits'), /Crediti\s*Al Lele/, 'la dedica al Lele viene per prima');
|
||||
assert.match(await page.textContent('#credits'), /Yuri Baldi/);
|
||||
assert.match(await page.textContent('#credits'), /Francesco Bacci/);
|
||||
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'; });
|
||||
assert.ok(onTop, 'i crediti stanno sopra il titolo, non dietro');
|
||||
await page.screenshot({ path: `${OUT}/01c-crediti.png` });
|
||||
// L'elenco può scorrere; la dedica e il pulsante Chiudi devono restare dentro lo schermo.
|
||||
const fitsCredits = await page.evaluate(() => { const c = document.getElementById('credits').getBoundingClientRect(), b = document.getElementById('close-credits').getBoundingClientRect(); return b.bottom <= c.bottom && b.top >= c.top; });
|
||||
@@ -108,6 +111,32 @@ try {
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForSelector('#pause:not([hidden])');
|
||||
await page.screenshot({ path: `${OUT}/02b-pausa.png` });
|
||||
// Salva su file: si scarica la partita; dal titolo «Carica» la riapre anche senza il salvataggio del browser.
|
||||
const [download] = await Promise.all([page.waitForEvent('download'), page.click('#save-file')]);
|
||||
assert.match(download.suggestedFilename(), /^regolo-\d{4}-\d\d-\d\d-\d{4}\.json$/);
|
||||
const savedFile = `${OUT}/partita.json`;
|
||||
await download.saveAs(savedFile);
|
||||
const before = await page.evaluate(() => game.snapshot());
|
||||
await page.click('#to-title');
|
||||
await page.evaluate(() => localStorage.removeItem('leggenda-regolo-save'));
|
||||
await page.setInputFiles('#load-file', { name: 'altro.json', mimeType: 'application/json', buffer: Buffer.from('{"ciao":1}') });
|
||||
await page.waitForSelector('#load-error:not([hidden])');
|
||||
assert.ok(await page.isVisible('#title'), 'un file sbagliato lascia il titolo dov\'è');
|
||||
// Una scena che non esiste: rifiutato, e il salvataggio del browser resta com'era.
|
||||
await page.evaluate(() => localStorage.setItem('leggenda-regolo-save', '{"scene":"piazza","resta":true}'));
|
||||
const lost = JSON.parse(readFileSync(savedFile, 'utf8'));
|
||||
lost.state.scene = 'luna';
|
||||
await page.setInputFiles('#load-file', { name: 'luna.json', mimeType: 'application/json', buffer: Buffer.from(JSON.stringify(lost)) });
|
||||
await page.waitForFunction(() => /non torna/.test(document.getElementById('load-error').textContent));
|
||||
assert.ok(await page.isVisible('#title'));
|
||||
assert.equal(await page.evaluate(() => JSON.parse(localStorage.getItem('leggenda-regolo-save')).resta), true, 'il salvataggio del browser non è toccato');
|
||||
await page.evaluate(() => localStorage.removeItem('leggenda-regolo-save'));
|
||||
await page.setInputFiles('#load-file', savedFile);
|
||||
await page.waitForSelector('#title', { state: 'hidden' });
|
||||
assert.deepEqual(await page.evaluate(() => game.snapshot()), before, 'la partita caricata è quella salvata');
|
||||
assert.ok(await page.evaluate(() => !!localStorage.getItem('leggenda-regolo-save')), 'e diventa quella di «Continua»');
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForSelector('#pause:not([hidden])');
|
||||
await page.click('#resume');
|
||||
assert.ok(await page.isHidden('#pause'));
|
||||
await page.click('#pause-button');
|
||||
@@ -121,7 +150,9 @@ try {
|
||||
assert.ok(playing, 'musica del paese: ' + await page.evaluate(() => music.current?.src));
|
||||
await page.click('#mute');
|
||||
await page.locator('#volume').fill('30');
|
||||
assert.deepEqual(await page.evaluate(() => JSON.parse(localStorage.getItem('leggenda-regolo-audio'))), { volume: 0.3, muted: false });
|
||||
await page.locator('#voice-volume').fill('70');
|
||||
assert.deepEqual(await page.evaluate(() => JSON.parse(localStorage.getItem('leggenda-regolo-audio'))), { volume: 0.3, muted: false, voices: 0.7 });
|
||||
assert.equal(await page.evaluate(() => voices.volume), 0.7);
|
||||
|
||||
for (const [verb, target, item] of solution) {
|
||||
if (verb === 'choose') {
|
||||
@@ -144,6 +175,8 @@ try {
|
||||
await page.screenshot({ path: `${OUT}/06-fine.png` });
|
||||
await page.click('#ending .open-credits');
|
||||
assert.ok(await page.isVisible('#credits'), 'crediti anche dalla fine');
|
||||
assert.ok(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'; }), 'i crediti stanno sopra la fine');
|
||||
await page.click('#close-credits');
|
||||
assert.ok(await page.isHidden('#credits'));
|
||||
const saved = await page.evaluate(() => JSON.parse(localStorage.getItem('leggenda-regolo-save')));
|
||||
@@ -208,8 +241,24 @@ try {
|
||||
await landscape.click('#skip-intro');
|
||||
await settle(landscape);
|
||||
assert.equal(await landscape.evaluate(() => document.documentElement.scrollWidth > innerWidth), false);
|
||||
assert.ok(await landscape.evaluate(() => [...document.querySelectorAll('#verbs button')].every(b => b.scrollWidth <= b.clientWidth + 1 && b.getBoundingClientRect().height >= 26)), 'in orizzontale i verbi si leggono interi e il dito li prende');
|
||||
await landscape.screenshot({ path: `${OUT}/09-orizzontale.png`, fullPage: true });
|
||||
|
||||
// Debug (?debug): area calpestabile e hotspot disegnati; Maiusc+clic segna punti senza muovere Andrea, X li azzera.
|
||||
const dbg = await open({ width: 1280, height: 860 }, '?debug');
|
||||
await dbg.click('#new-game');
|
||||
await dbg.click('#skip-intro');
|
||||
await settle(dbg);
|
||||
const start = await dbg.evaluate(() => [game.state.x, game.state.y]);
|
||||
await dbg.keyboard.down('Shift');
|
||||
await clickScene(dbg, [100, 300]);
|
||||
await clickScene(dbg, [200, 320]);
|
||||
await dbg.keyboard.up('Shift');
|
||||
await dbg.mouse.move(700, 400);
|
||||
await dbg.waitForTimeout(200);
|
||||
assert.deepEqual(await dbg.evaluate(() => [game.state.x, game.state.y]), start, 'Maiusc+clic non muove Andrea');
|
||||
await dbg.screenshot({ path: `${OUT}/10-debug.png` });
|
||||
|
||||
assert.deepEqual(errors, [], 'errori nella console');
|
||||
console.log(`Browser OK: soluzione completa, ripresa del salvataggio, telefono. Schermate in ${OUT}`);
|
||||
} finally {
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@ export function settle(game, { maxSteps = 20000 } = {}) {
|
||||
said.push(game.speech);
|
||||
game.skip();
|
||||
}
|
||||
if (!game.busy && !game.walking && !game.queue.length && !game.task) 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);
|
||||
}
|
||||
throw new Error('Il gioco non si ferma: ' + JSON.stringify(game.snapshot()));
|
||||
|
||||
@@ -195,3 +195,13 @@ test('col doppio clic si cammina al doppio della velocità', () => {
|
||||
const slow = far(false), fast = far(true);
|
||||
assert.ok(slow > 0 && Math.abs(fast - 2 * slow) < 1, `normale ${slow}, doppio ${fast}`);
|
||||
});
|
||||
|
||||
test('col dito un oggetto mancato di poco si prende lo stesso; il punto preciso vince sempre', () => {
|
||||
const g = new Game(mini());
|
||||
assert.equal(g.hotspotAt([95, 45])?.id, 'chiave_a_terra', 'dentro');
|
||||
assert.equal(g.hotspotAt([95, 60]), null, 'col mouse, fuori è fuori');
|
||||
assert.equal(g.hotspotAt([95, 60], 12)?.id, 'chiave_a_terra', 'col dito, 10 sotto vale');
|
||||
assert.equal(g.hotspotAt([95, 75], 12), null, 'ma non troppo lontano');
|
||||
assert.equal(g.hotspotAt([62, 15], 20)?.id, 'tizio', 'vince il più vicino');
|
||||
assert.equal(g.hotspotAt([3, 50], 20)?.id, 'uscita', 'chi è preso in pieno vince sui vicini');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { loadBackground, hasBackground, RETRIES, RETRY_WAIT } from '../src/engine/render.js';
|
||||
|
||||
// Un Image finto: fallisce le prime `fails` volte per ogni indirizzo, e conta le richieste.
|
||||
function fakeImage(fails) {
|
||||
const asked = new Map();
|
||||
globalThis.Image = class {
|
||||
set src(v) {
|
||||
const n = (asked.get(v) ?? 0) + 1;
|
||||
asked.set(v, n);
|
||||
queueMicrotask(() => (n <= fails ? this.onerror() : this.onload()));
|
||||
}
|
||||
};
|
||||
return asked;
|
||||
}
|
||||
|
||||
test('uno sfondo che non arriva si riprova dopo un po\', non a ogni fotogramma, e alla fine arriva', async () => {
|
||||
const asked = fakeImage(2), src = 'x/rete-ballerina.png';
|
||||
try {
|
||||
assert.equal(await loadBackground(src), false);
|
||||
assert.equal(await loadBackground(src), false, 'subito dopo non si riprova');
|
||||
assert.equal(asked.get(src), 1);
|
||||
assert.equal(await loadBackground(src, Date.now() + RETRY_WAIT + 1), false, 'dopo l\'attesa si riprova');
|
||||
assert.equal(asked.get(src), 2);
|
||||
assert.equal(await loadBackground(src, Date.now() + RETRY_WAIT + 1), false, 'la seconda attesa è più lunga');
|
||||
assert.equal(asked.get(src), 2);
|
||||
assert.equal(await loadBackground(src, Date.now() + 2 * RETRY_WAIT + 1), true);
|
||||
assert.ok(hasBackground(src));
|
||||
} finally {
|
||||
delete globalThis.Image;
|
||||
}
|
||||
});
|
||||
|
||||
test('uno sfondo che non c\'è proprio si lascia stare dopo pochi tentativi', async () => {
|
||||
const asked = fakeImage(Infinity), src = 'x/non-esiste.png', later = () => Date.now() + 3600e3;
|
||||
try {
|
||||
for (let i = 0; i < 10; i++) await loadBackground(src, later());
|
||||
assert.equal(asked.get(src), RETRIES + 1);
|
||||
} finally {
|
||||
delete globalThis.Image;
|
||||
}
|
||||
});
|
||||
+20
-4
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readSave, writeSave, clearSave, readAudio, writeAudio } from '../src/engine/storage.js';
|
||||
import { readSave, writeSave, clearSave, readAudio, writeAudio, saveFile, saveFileName, readSaveFile } from '../src/engine/storage.js';
|
||||
import { memoryStorage } from './helpers.mjs';
|
||||
|
||||
test('scrive, rilegge e cancella il salvataggio', () => {
|
||||
@@ -22,11 +22,27 @@ test('un archivio rotto o bloccato non fa cadere il gioco', () => {
|
||||
assert.equal(readSave(garbage), null);
|
||||
});
|
||||
|
||||
test('volume e muto della musica si ricordano', () => {
|
||||
test('volume della musica, volume delle voci e muto si ricordano', () => {
|
||||
const s = memoryStorage();
|
||||
assert.equal(readAudio(s), null);
|
||||
assert.ok(writeAudio(s, { volume: 0.3, muted: true }));
|
||||
assert.deepEqual(readAudio(s), { volume: 0.3, muted: true });
|
||||
assert.ok(writeAudio(s, { volume: 0.3, muted: true, voices: 0.4 }));
|
||||
assert.deepEqual(readAudio(s), { volume: 0.3, muted: true, voices: 0.4 });
|
||||
writeAudio(s, { volume: 0.3, muted: false });
|
||||
assert.deepEqual(readAudio(s), { volume: 0.3, muted: false }, 'un salvataggio di prima delle voci resta valido');
|
||||
s.setItem('leggenda-regolo-audio', '{rotto');
|
||||
assert.equal(readAudio(s), null);
|
||||
});
|
||||
|
||||
test('la partita va in un file e torna indietro uguale; un file qualsiasi non si carica', () => {
|
||||
const state = { scene: 'piazza', x: 300, y: 340, facing: -1, inventory: ['chiave'], flags: { 'visto:piazza': true }, used: {}, ended: false };
|
||||
const date = new Date(2007, 4, 27, 9, 5);
|
||||
assert.deepEqual(readSaveFile(saveFile(state, date)), { state });
|
||||
assert.equal(saveFileName(date), 'regolo-2007-05-27-0905.json');
|
||||
const not = { error: 'not-save' }, env = extra => JSON.stringify({ game: 'la-leggenda-del-regolo', version: 1, state, ...extra });
|
||||
assert.deepEqual(readSaveFile('{non è json'), not);
|
||||
assert.deepEqual(readSaveFile(JSON.stringify(state)), not, 'senza la busta del gioco no');
|
||||
assert.deepEqual(readSaveFile(JSON.stringify({ game: 'altro-gioco', version: 1, state })), not);
|
||||
assert.deepEqual(readSaveFile(env({ state: [] })), not);
|
||||
assert.deepEqual(readSaveFile(env({ version: undefined })), not, 'senza versione no');
|
||||
assert.deepEqual(readSaveFile(env({ version: 2 })), { error: 'newer' }, 'da una versione più nuova no');
|
||||
});
|
||||
|
||||
+100
-1
@@ -1,8 +1,9 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { backgroundOrder } from '../src/engine/render.js';
|
||||
import { story } from '../src/story/story.js';
|
||||
import { solution } from '../src/story/solution.js';
|
||||
import { Game, VERBS } from '../src/engine/game.js';
|
||||
import { Game, VERBS, checkSave } from '../src/engine/game.js';
|
||||
import { inside } from '../src/engine/geometry.js';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { settle } from './helpers.mjs';
|
||||
@@ -264,3 +265,101 @@ test('la catena del ghiacciolo: Tosca, cedrata, stecco, gomma, griglia, chiavett
|
||||
g.act('open', 'casetta'); say();
|
||||
assert.ok(g.state.flags.casetta_aperta && g.state.inventory.includes('torcia') && !g.state.inventory.includes('chiavetta'));
|
||||
});
|
||||
|
||||
// Uscire dal gioco in un istante qualsiasi di un passo deve riportare a prima del passo o a dopo, mai a metà:
|
||||
// si fa il passo fotogramma per fotogramma e a ogni fotogramma si guarda cosa verrebbe salvato.
|
||||
const essence = s => s && JSON.stringify({ ...s, x: 0, y: 0, facing: 0 });
|
||||
function stepChecked(g, label, step) {
|
||||
const before = essence(g.checkpoint), seen = [];
|
||||
step();
|
||||
for (let i = 0; i < 20000; i++) {
|
||||
seen.push(essence(g.savable()));
|
||||
if (g.speech) g.skip();
|
||||
if (!g.busy && !g.walking && !g.queue.length && !g.task) break;
|
||||
g.update(0.05);
|
||||
}
|
||||
g.update(0);
|
||||
const after = essence(g.savable());
|
||||
for (const s of seen) assert.ok(s === before || s === after, `${label}: interrompendo a metà si salverebbe uno stato a metà`);
|
||||
assert.equal(essence(g.checkpoint), after, `${label}: a passo finito il punto fermo è aggiornato`);
|
||||
}
|
||||
|
||||
test('interrompere la soluzione in qualsiasi istante non lascia enigmi risolti a metà', () => {
|
||||
const saves = [];
|
||||
const g = new Game(story, { onSave: s => saves.push(s) });
|
||||
stepChecked(g, 'inizio', () => g.go(story.start.scene, story.start.at));
|
||||
for (const [verb, target, item] of solution)
|
||||
stepChecked(g, `${verb} ${target}`, () => verb === 'choose' ? g.choose(g.options().findIndex(o => o.text === target)) : g.act(verb, target, item));
|
||||
assert.ok(saves.at(-1).ended);
|
||||
});
|
||||
|
||||
test('la squama presa resta salvata anche senza cambiare scena', () => {
|
||||
const saves = [];
|
||||
const g = new Game(story, { onSave: s => saves.push(s) });
|
||||
g.go(story.start.scene, story.start.at); settle(g);
|
||||
for (const [verb, target, item] of solution.slice(0, 5)) {
|
||||
if (verb === 'choose') g.choose(g.options().findIndex(o => o.text === target)); else g.act(verb, target, item);
|
||||
settle(g);
|
||||
}
|
||||
assert.ok(g.state.inventory.includes('squama'));
|
||||
assert.ok(saves.at(-1).inventory.includes('squama'), 'il salvataggio ha la squama');
|
||||
const again = new Game(story, { state: JSON.parse(JSON.stringify(saves.at(-1))) });
|
||||
assert.ok(again.state.inventory.includes('squama'));
|
||||
});
|
||||
|
||||
test('interrompere la catena del ghiacciolo non perde la torcia né la chiavetta', () => {
|
||||
const g = new Game(story);
|
||||
const steps = [
|
||||
['go tennis', () => g.go('tennis_chiesa')], ['parla con Tosca', () => g.act('talk', 'chiosco')],
|
||||
...['Una Coca.', 'Un ghiacciolo al limone.', 'Una cedrata, da portare via.', 'Niente, grazie Tosca.']
|
||||
.map(t => [t, () => g.choose(g.options().findIndex(o => o.text === t))]),
|
||||
['bevi la Coca', () => g.act('use', 'coca')], ['cedrata e ghiacciolo', () => g.act('use', 'cedrata', 'ghiacciolo')],
|
||||
['go oratorio', () => g.go('oratorio')], ['prendi la gomma', () => g.act('take', 'gomma_terra')],
|
||||
['gomma e stecco', () => g.act('use', 'gomma', 'stecco')], ['go ponticino', () => g.go('ponticino')],
|
||||
['griglia', () => g.act('use', 'griglia', 'stecco_gomma')], ['go area feste', () => g.go('area_feste')],
|
||||
['apri la casetta', () => g.act('open', 'casetta')],
|
||||
];
|
||||
for (const [label, step] of steps) stepChecked(g, label, step);
|
||||
assert.ok(g.checkpoint.inventory.includes('torcia') && g.checkpoint.flags.casetta_aperta);
|
||||
});
|
||||
|
||||
test('gli sfondi si caricano dalla scena di partenza verso le più lontane, tutti e una volta sola', () => {
|
||||
const order = backgroundOrder(story, story.start.scene);
|
||||
const all = new Set(Object.values(story.scenes).map(s => s.background).filter(Boolean));
|
||||
assert.equal(order.length, all.size);
|
||||
assert.deepEqual(new Set(order), all);
|
||||
assert.equal(order[0], story.scenes[story.start.scene].background, 'prima la piazza');
|
||||
const near = story.scenes[story.start.scene].hotspots.filter(h => h.exit).map(h => story.scenes[h.exit.scene].background);
|
||||
for (const bg of near) assert.ok(order.indexOf(bg) <= near.length, `${bg} è vicina e deve venire presto`);
|
||||
});
|
||||
|
||||
test('col dito la squama si prende anche toccando la panchina tutt\'intorno; col mouse vale il punto preciso', () => {
|
||||
const g = new Game(story);
|
||||
g.go('piazza'); settle(g);
|
||||
g.state.flags.luccichio = true;
|
||||
const sq = g.hotspot('cosa_luccicante');
|
||||
assert.ok(sq, 'la squama è in scena');
|
||||
const [x, y, w, h] = sq.box, cx = x + w / 2, cy = y + h / 2, reach = 14;
|
||||
for (const p of [[x - 8, cy], [x + w + 8, cy], [cx, y - 8], [cx, y + h + 3]])
|
||||
assert.equal(g.hotspotAt(p, reach)?.id, 'cosa_luccicante', `col dito in ${p}`);
|
||||
assert.notEqual(g.hotspotAt([x - 8, cy])?.id, 'cosa_luccicante', 'col mouse fuori dalla squama non la prende');
|
||||
assert.equal(g.hotspotAt([cx, cy])?.id, 'cosa_luccicante');
|
||||
});
|
||||
|
||||
test('un salvataggio da file si accetta solo se torna in tutto con la storia', () => {
|
||||
const g = new Game(story);
|
||||
g.go(story.start.scene, story.start.at); settle(g);
|
||||
const good = JSON.parse(JSON.stringify(g.snapshot()));
|
||||
assert.equal(checkSave(story, good), null);
|
||||
const bad = change => checkSave(story, { ...good, ...change });
|
||||
assert.match(bad({ scene: 'luna' }), /scena sconosciuta/);
|
||||
assert.match(bad({ scene: 'toString' }), /scena sconosciuta/);
|
||||
assert.ok(bad({ x: 'qui' }));
|
||||
assert.ok(bad({ facing: 0 }));
|
||||
assert.ok(bad({ inventory: ['astronave'] }), 'oggetto che non esiste');
|
||||
assert.ok(bad({ inventory: 'squama' }));
|
||||
assert.ok(bad({ flags: { a: 1 } }));
|
||||
assert.ok(bad({ used: null }));
|
||||
assert.ok(bad({ ended: 'no' }));
|
||||
assert.ok(checkSave(story, null));
|
||||
});
|
||||
|
||||
@@ -56,6 +56,16 @@ test('le voci si cercano per chi parla e testo; senza indice il gioco resta muto
|
||||
assert.equal(v.playing, false);
|
||||
});
|
||||
|
||||
test('le voci hanno il loro volume, più basso della musica finché non lo si cambia', () => {
|
||||
const v = new Voices();
|
||||
assert.equal(v.volume, 0.5);
|
||||
assert.equal(v.setVolume(1.7), 1);
|
||||
assert.equal(v.setVolume(-1), 0);
|
||||
const quiet = new Voices({ [voiceKey('leone', 'Grr')]: { file: 'leone-x.mp3', sec: 1.5 } }, 'x/', 0);
|
||||
assert.equal(quiet.play('leone', 'Grr'), false, 'a volume zero non parte niente');
|
||||
assert.equal(new Voices({ [voiceKey('leone', 'Grr')]: { file: 'leone-x.mp3', sec: 1.5 } }).play('leone', 'Grr', true), false, 'col muto non parte niente');
|
||||
});
|
||||
|
||||
test('il leone e il castello parlano dalla loro posizione, non da quella di Andrea', () => {
|
||||
const g = new Game(story);
|
||||
g.go('salita_villa_adami', [340, 352]);
|
||||
@@ -63,3 +73,25 @@ test('il leone e il castello parlano dalla loro posizione, non da quella di Andr
|
||||
g.go('trebbio', [110, 345]);
|
||||
assert.deepEqual(g.speakerPoint('castello'), story.scenes.trebbio.speakers.castello);
|
||||
});
|
||||
|
||||
test('la pausa ferma la voce e riprendendo continua; stop la toglie', () => {
|
||||
globalThis.Audio = class { constructor(src) { this.src = src; this.paused = true; this.ended = false; } play() { this.paused = false; return Promise.resolve(); } pause() { this.paused = true; } };
|
||||
try {
|
||||
const v = new Voices({ [voiceKey('leone', 'Grr')]: { file: 'leone-x.mp3', sec: 1.5 } }, 'x/', 0.4);
|
||||
assert.ok(v.play('leone', 'Grr'));
|
||||
assert.equal(v.audio.volume, 0.4);
|
||||
assert.ok(v.playing);
|
||||
v.pause();
|
||||
assert.ok(!v.playing && v.audio, 'in pausa la voce resta, ferma');
|
||||
v.resume();
|
||||
assert.ok(v.playing, 'riprende la stessa battuta');
|
||||
v.setVolume(0.8);
|
||||
assert.equal(v.audio.volume, 0.8, 'il cursore cambia anche la voce che sta parlando');
|
||||
v.stop();
|
||||
assert.ok(!v.playing && !v.audio);
|
||||
v.resume();
|
||||
assert.ok(!v.playing, 'dopo stop non riparte niente');
|
||||
} finally {
|
||||
delete globalThis.Audio;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user