- 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
98 lines
5.0 KiB
JavaScript
98 lines
5.0 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { existsSync, readFileSync } from 'node:fs';
|
|
import { story } from '../src/story/story.js';
|
|
import { Game } from '../src/engine/game.js';
|
|
import { voiceLines, voiceKey, Voices } from '../src/engine/voices.js';
|
|
import { settle } from './helpers.mjs';
|
|
|
|
const index = JSON.parse(readFileSync(new URL('../assets/voices/index.json', import.meta.url), 'utf8'));
|
|
const voci = JSON.parse(readFileSync(new URL('../tools/voci.json', import.meta.url), 'utf8'));
|
|
|
|
test('le battute si trovano ovunque: introduzione, scene, oggetti, dialoghi e rami if', () => {
|
|
const lines = voiceLines(story), has = (who, part) => lines.some(l => l.who === who && l.text.includes(part));
|
|
assert.ok(has('andrea', 'Fine maggio 2007'), 'introduzione');
|
|
assert.ok(has('andrea', 'Il palio è sparito!'), 'ingresso in una scena, dentro un if');
|
|
assert.ok(has('vecchietta', 'cadde dal cielo'), 'dialogo, con chi parla');
|
|
assert.ok(has('castello', 'vicino alle sue impronte'), 'ramo else annidato');
|
|
assert.ok(has('andrea', 'pagina sola'), 'azione di un oggetto');
|
|
assert.ok(!lines.some(l => /^[.…\s]+$/.test(l.text)), 'i silenzi («...») non hanno voce');
|
|
});
|
|
|
|
test('ogni battuta ha la sua voce (sennò: make voci) e ogni personaggio una voce in tools/voci.json', () => {
|
|
for (const { who, text } of voiceLines(story)) {
|
|
assert.ok(voci[who]?.speaker, `manca la voce di ${who} in tools/voci.json`);
|
|
const entry = index[voiceKey(who, text)];
|
|
assert.ok(entry, `senza voce: ${who}: ${text}`);
|
|
assert.ok(existsSync(new URL(`../assets/voices/${entry.file}`, import.meta.url)), `manca il file ${entry.file}`);
|
|
assert.ok(entry.sec > 0.3 && entry.sec < 30, `durata strana: ${entry.sec} s per «${text}»`);
|
|
}
|
|
});
|
|
|
|
test('il castello e il leone hanno voci diverse da tutti gli altri', () => {
|
|
for (const special of ['castello', 'leone'])
|
|
for (const [who, v] of Object.entries(voci))
|
|
if (who !== special && !who.startsWith('_')) assert.notEqual(v.speaker, voci[special].speaker, `${special} e ${who}`);
|
|
});
|
|
|
|
test('il fumetto resta a schermo almeno quanto la voce', () => {
|
|
const g = new Game(story, { voiceTime: (who, text) => (text.startsWith('Il palio') ? 9 : undefined) });
|
|
g.go(story.start.scene, story.start.at);
|
|
g.update(0.05);
|
|
assert.ok(g.speech.text.startsWith('Il palio'));
|
|
assert.ok(g.speech.left > 9, 'la voce lunga allunga il fumetto');
|
|
settle(g);
|
|
const quiet = new Game(story);
|
|
quiet.go(story.start.scene, story.start.at);
|
|
quiet.update(0.05);
|
|
assert.ok(quiet.speech.left < 9, 'senza voce vale la durata di lettura');
|
|
});
|
|
|
|
test('le voci si cercano per chi parla e testo; senza indice il gioco resta muto', () => {
|
|
const v = new Voices({ [voiceKey('leone', 'Grr')]: { file: 'leone-x.mp3', sec: 1.5 } });
|
|
assert.equal(v.seconds('leone', 'Grr'), 1.5);
|
|
assert.equal(v.seconds('andrea', 'Grr'), undefined);
|
|
assert.equal(new Voices().play('andrea', 'Ciao'), false);
|
|
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]);
|
|
assert.deepEqual(g.speakerPoint('leone'), story.scenes.salita_villa_adami.speakers.leone);
|
|
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;
|
|
}
|
|
});
|