Suoni d'ambiente sintetizzati (vento, acqua, uccelli, grilli, campane) col loro volume in Pausa; la fessura in primo piano: si cerca col fascio della torcia il quadro e gli occhi (lavoro di Codex). La foto del palio leggibile da nginx, e un test che lo controlla per tutti i file serviti
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
This commit is contained in:
+35
-4
@@ -6,6 +6,7 @@ import { staticServer } from './helpers.mjs';
|
||||
import { mkdirSync, readFileSync } from 'node:fs';
|
||||
import assert from 'node:assert/strict';
|
||||
import { solution, route } from '../src/story/solution.js';
|
||||
import { checkMultimedia } from './multimedia.browser.mjs';
|
||||
|
||||
const OUT = process.env.OUT ?? '/tmp/regolo-shots';
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
@@ -47,6 +48,8 @@ const hotspotCenter = async (page, id) => {
|
||||
};
|
||||
|
||||
try {
|
||||
await checkMultimedia(browser, PORT, OUT, errors);
|
||||
if (!process.argv.includes('--media-only')) {
|
||||
const page = await open({ width: 1280, height: 860 });
|
||||
// Il cartello d'apertura, poi da solo il titolo.
|
||||
assert.match(await page.textContent('#splash'), /Manzolo\s*\(Poca\)\s*Art\s*Presents/);
|
||||
@@ -66,10 +69,22 @@ try {
|
||||
// Col codice lento ad arrivare (rete vera) il cartello aspetta il titolo: non sfuma prima per conto suo.
|
||||
{
|
||||
const slow = await browser.newPage({ viewport: { width: 1280, height: 860 } });
|
||||
// Misura al momento dell'apparizione: il polling di Playwright può arrivare dopo
|
||||
// diversi fotogrammi su una macchina occupata, quando la dissolvenza è già in corso.
|
||||
await slow.addInitScript(() => {
|
||||
const observer = new MutationObserver(() => {
|
||||
const title = document.getElementById('title'), splash = document.getElementById('splash');
|
||||
if (title && !title.hidden && splash) {
|
||||
window.titleRevealOpacity = Number(getComputedStyle(splash).opacity);
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
observer.observe(document, { subtree: true, childList: true, attributes: true, attributeFilter: ['hidden'] });
|
||||
});
|
||||
await slow.route('**/src/main.js', async route => { await new Promise(r => setTimeout(r, 2000)); await route.continue(); });
|
||||
await slow.goto(`http://127.0.0.1:${PORT}/`);
|
||||
await slow.waitForSelector('#title:not([hidden])', { timeout: 12000 });
|
||||
const opacity = await slow.evaluate(() => Number(getComputedStyle(document.getElementById('splash')).opacity));
|
||||
const opacity = await slow.evaluate(() => window.titleRevealOpacity);
|
||||
assert.ok(opacity > 0.9, `il cartello era già sfumato (${opacity}) quando è comparso il titolo`);
|
||||
await slow.close();
|
||||
}
|
||||
@@ -182,12 +197,13 @@ try {
|
||||
await page.click('#mute');
|
||||
await page.locator('#volume').fill('30');
|
||||
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.deepEqual(await page.evaluate(() => JSON.parse(localStorage.getItem('leggenda-regolo-audio'))), { volume: 0.3, muted: false, voices: 0.7, effects: 0.45 });
|
||||
assert.equal(await page.evaluate(() => voices.volume), 0.7);
|
||||
// Sul telefono si passa a un'altra app: la pagina diventa nascosta, la musica si ferma subito e la partita va in
|
||||
// pausa; tornando la musica riparte solo con Riprendi (la pausa la tiene ferma).
|
||||
const hide = hidden => page.evaluate(h => { Object.defineProperty(document, 'hidden', { value: h, configurable: true }); document.dispatchEvent(new Event('visibilitychange')); }, hidden);
|
||||
await hide(true);
|
||||
assert.equal(await page.evaluate(() => ambience.nodes.size), 0, 'nascosta la pagina, gli effetti si fermano subito');
|
||||
assert.ok(await page.evaluate(() => music.current.audio.paused), 'nascosta la pagina, la musica è ferma');
|
||||
assert.ok(await page.isVisible('#pause'), 'e la partita è in pausa');
|
||||
await hide(false);
|
||||
@@ -211,8 +227,21 @@ try {
|
||||
if (verb === 'talk') await page.screenshot({ path: `${OUT}/03-dialogo.png` });
|
||||
// La torcia nella fessura: il palio illuminato e i due occhi gialli, prima che spariscano.
|
||||
if (target === 'fessura' && item) {
|
||||
await page.waitForFunction(() => game.state.flags.occhi_regolo, null, { timeout: 30000 });
|
||||
await page.screenshot({ path: `${OUT}/05b-occhi.png` });
|
||||
await page.waitForFunction(() => game.closeup?.id === 'fessura', null, { timeout: 30000 });
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const at = await page.evaluate(i => game.story.closeups.fessura.targets[i].at, i);
|
||||
const box = await page.locator('#scene').boundingBox();
|
||||
await page.mouse.move(box.x + at[0] / 640 * box.width, box.y + at[1] / 360 * box.height);
|
||||
await page.waitForFunction(() => game.closeup?.step === 'reveal');
|
||||
await page.screenshot({ path: `${OUT}/05b-fessura-${i}.png` });
|
||||
for (let n = 0; n < 200; n++) {
|
||||
if (await page.evaluate(i => game.closeup.found > i, i)) break;
|
||||
if (await page.evaluate(() => !!game.speech)) await page.keyboard.press('Space');
|
||||
await page.waitForTimeout(30);
|
||||
}
|
||||
assert.equal(await page.evaluate(() => game.closeup.found), i + 1);
|
||||
}
|
||||
await page.click('#closeup-close');
|
||||
}
|
||||
await settle(page);
|
||||
if (target === 'cosa_luccicante') await page.screenshot({ path: `${OUT}/04-squama.png` });
|
||||
@@ -386,6 +415,8 @@ try {
|
||||
|
||||
assert.deepEqual(errors, [], 'errori nella console');
|
||||
console.log(`Browser OK: soluzione completa, ripresa del salvataggio, telefono. Schermate in ${OUT}`);
|
||||
}
|
||||
assert.deepEqual(errors, [], 'errori nella console');
|
||||
} finally {
|
||||
await browser.close();
|
||||
server.kill();
|
||||
|
||||
+13
-2
@@ -1,5 +1,5 @@
|
||||
// Fa avanzare il gioco come farebbe un giocatore impaziente: tempo che scorre e battute saltate.
|
||||
export function settle(game, { maxSteps = 20000 } = {}) {
|
||||
export function settle(game, { maxSteps = 20000, explore = false } = {}) {
|
||||
const said = [];
|
||||
for (let i = 0; i < maxSteps; i++) {
|
||||
if (game.speech) {
|
||||
@@ -7,7 +7,10 @@ export function settle(game, { maxSteps = 20000 } = {}) {
|
||||
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;
|
||||
if (game.closeup && !game.speech && game.reach <= 0) {
|
||||
if (!explore || game.story.closeups[game.closeup.id].type !== 'torch') return said;
|
||||
exploreTorch(game);
|
||||
}
|
||||
// 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);
|
||||
@@ -15,6 +18,14 @@ export function settle(game, { maxSteps = 20000 } = {}) {
|
||||
throw new Error('Il gioco non si ferma: ' + JSON.stringify(game.snapshot()));
|
||||
}
|
||||
|
||||
// Il collaudo della soluzione compie anche i gesti richiesti dal finale interattivo.
|
||||
export function exploreTorch(game) {
|
||||
const c = game.closeup, def = c && game.story.closeups[c.id];
|
||||
if (def?.type !== 'torch') return;
|
||||
if (c.step === 'done') game.closeupClose();
|
||||
else if (c.step === 'search') game.closeupAim(def.targets[c.found].at);
|
||||
}
|
||||
|
||||
export function memoryStorage() {
|
||||
return { data: {}, getItem(k) { return this.data[k] ?? null; }, setItem(k, v) { this.data[k] = String(v); }, removeItem(k) { delete this.data[k]; } };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
export async function checkMultimedia(browser, port, out, errors) {
|
||||
for (const mobile of [false, true]) {
|
||||
const page = await browser.newPage({ viewport: mobile ? { width: 844, height: 390 } : { width: 1280, height: 860 }, hasTouch: mobile, isMobile: mobile });
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
await page.goto(`http://127.0.0.1:${port}/`);
|
||||
await page.click('#splash'); await page.click('#new-game');
|
||||
await page.click('[data-rione="campomigliaio"]'); await page.click('#skip-intro');
|
||||
const settle = async () => {
|
||||
for (let i = 0; i < 200; i++) {
|
||||
if (await page.evaluate(() => !!game.speech)) await page.keyboard.press('Space');
|
||||
if (await page.evaluate(() => game.closeup || game.idle)) return;
|
||||
await page.waitForTimeout(30);
|
||||
}
|
||||
throw new Error('la sequenza non si ferma');
|
||||
};
|
||||
await settle();
|
||||
await page.waitForFunction(() => ambience.audible && ambience.context.state === 'running');
|
||||
await page.click('#mute');
|
||||
assert.equal(await page.evaluate(() => ambience.nodes.size), 0, 'muto immediato');
|
||||
await page.click('#mute');
|
||||
await page.click('#pause-button');
|
||||
await page.locator('#effects-volume').fill('65');
|
||||
assert.equal(await page.evaluate(() => JSON.parse(localStorage.getItem('leggenda-regolo-audio')).effects), 0.65);
|
||||
assert.equal(await page.evaluate(() => ambience.nodes.size), 0, 'in pausa nessun suono');
|
||||
await page.screenshot({ path: `${out}/media-pausa-${mobile ? 'telefono' : 'desktop'}.png` });
|
||||
await page.click('#resume');
|
||||
if (mobile) {
|
||||
// Il libro già esistente deve continuare a sfogliarsi anche sul telefono.
|
||||
await page.evaluate(() => { game.state.inventory.push('libro'); game.act('open', 'libro'); });
|
||||
await page.waitForFunction(() => game.closeup?.step === 'cover');
|
||||
const r = await page.locator('#scene').boundingBox();
|
||||
await page.touchscreen.tap(r.x + r.width / 2, r.y + r.height * 0.6);
|
||||
await page.waitForFunction(() => game.closeup.step === 'title');
|
||||
await page.touchscreen.tap(r.x + r.width / 2, r.y + r.height * 0.6);
|
||||
await page.waitForFunction(() => game.closeup.step === 'end');
|
||||
await page.screenshot({ path: `${out}/media-libro-telefono.png` });
|
||||
await page.tap('#closeup-close'); await settle();
|
||||
}
|
||||
await page.evaluate(() => {
|
||||
game.state.flags.indizio_regolo = true;
|
||||
game.state.flags.casetta_aperta = true;
|
||||
game.state.inventory.push('torcia');
|
||||
game.go('fortezza', [462, 310]);
|
||||
});
|
||||
await settle();
|
||||
const at = async ([x, y]) => {
|
||||
const r = await page.locator('#scene').boundingBox();
|
||||
return [r.x + x / 640 * r.width, r.y + y / 360 * r.height];
|
||||
};
|
||||
const openTorch = async () => {
|
||||
await page.click('#verbs [data-verb="use"]'); await page.click('#inventory [data-item="torcia"]');
|
||||
await page.mouse.click(...await at([501, 285]));
|
||||
await settle();
|
||||
await page.waitForFunction(() => game.closeup?.id === 'fessura');
|
||||
};
|
||||
await openTorch();
|
||||
const saved = await page.evaluate(() => game.savable());
|
||||
await page.keyboard.press('Escape'); await settle();
|
||||
assert.equal(await page.evaluate(() => game.state.ended), false, 'uscire presto non vince');
|
||||
await openTorch();
|
||||
await page.click('#pause-button');
|
||||
const age = await page.evaluate(() => game.closeup.age);
|
||||
await page.waitForTimeout(150);
|
||||
assert.equal(await page.evaluate(() => game.closeup.age), age, 'il primo piano è fermo in pausa');
|
||||
await page.click('#resume');
|
||||
const origin = await page.evaluate(() => [game.state.x, game.state.y]);
|
||||
if (mobile) {
|
||||
const cdp = await page.context().newCDPSession(page);
|
||||
const [x, y] = await at([320, 180]), [tx, ty] = await at([300, 90]);
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x, y }] });
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x: tx, y: ty }] });
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
|
||||
const beam = await page.evaluate(() => game.closeup.beam);
|
||||
assert.ok(Math.abs(beam[0] - 300) < 2 && Math.abs(beam[1] - 90) < 2, 'trascinare il dito sposta la torcia');
|
||||
await cdp.detach();
|
||||
}
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const target = await page.evaluate(i => game.story.closeups.fessura.targets[i].at, i);
|
||||
if (mobile) await page.touchscreen.tap(...await at(target));
|
||||
else if (i === 0) await page.keyboard.press('Enter');
|
||||
else await page.mouse.move(...await at(target));
|
||||
await page.waitForFunction(() => game.closeup?.step === 'reveal');
|
||||
await page.screenshot({ path: `${out}/media-torcia-${mobile ? 'telefono' : 'desktop'}-${i}.png` });
|
||||
for (let n = 0; n < 200; n++) {
|
||||
if (await page.evaluate(i => game.closeup.found > i, i)) break;
|
||||
if (await page.evaluate(() => !!game.speech)) await page.keyboard.press('Space');
|
||||
await page.waitForTimeout(25);
|
||||
}
|
||||
assert.equal(await page.evaluate(() => game.closeup.found), i + 1);
|
||||
}
|
||||
assert.deepEqual(await page.evaluate(() => [game.state.x, game.state.y]), origin, 'il puntatore non muove Andrea');
|
||||
assert.deepEqual(await page.evaluate(() => game.savable()), saved, 'durante il primo piano resta il punto fermo');
|
||||
if (mobile) await page.tap('#closeup-close'); else await page.keyboard.press('Enter');
|
||||
await settle();
|
||||
await page.waitForSelector('#ending:not([hidden])');
|
||||
assert.ok(await page.evaluate(() => game.checkpoint.ended));
|
||||
assert.equal(await page.evaluate(() => ambience.nodes.size), 0, 'alla fine non resta il vento della fessura');
|
||||
// Misura il segnale realmente prodotto da Web Audio: udibile, finito, senza saturazione.
|
||||
if (!mobile) {
|
||||
const levels = await page.evaluate(async () => {
|
||||
const { Soundscape } = await import('/src/engine/soundscape.js');
|
||||
const results = {};
|
||||
for (const kind of ['page', 'wood', 'drip', 'bird', 'cricket', 'bell', 'glass', 'water', 'wind']) {
|
||||
const ctx = new OfflineAudioContext(2, 44100 * 2, 44100);
|
||||
const s = new Soundscape({ createContext: () => ctx });
|
||||
s.unlock(); s.configure({ active: true });
|
||||
if (kind === 'water' || kind === 'wind') s.update(kind === 'water' ? 'acqua' : 'fortezza', 'sera', 0.1);
|
||||
else s.play(kind);
|
||||
const data = (await ctx.startRendering()).getChannelData(0);
|
||||
let peak = 0, sum = 0;
|
||||
for (const v of data) { peak = Math.max(peak, Math.abs(v)); sum += v * v; }
|
||||
results[kind] = { peak, rms: Math.sqrt(sum / data.length) };
|
||||
}
|
||||
return results;
|
||||
});
|
||||
for (const [kind, { peak, rms }] of Object.entries(levels)) {
|
||||
assert.ok(rms > 0.00001 && peak < 0.5, `${kind}: audio non valido ${JSON.stringify({ peak, rms })}`);
|
||||
}
|
||||
}
|
||||
await page.reload();
|
||||
await page.waitForFunction(() => window.ambience?.volume === 0.65);
|
||||
await page.close();
|
||||
}
|
||||
console.log('Multimedia OK: torcia con mouse, tastiera e touch; uscita e pausa; audio sintetizzato e volume salvato.');
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Game } from '../src/engine/game.js';
|
||||
import { story } from '../src/story/story.js';
|
||||
import { settle } from './helpers.mjs';
|
||||
import { Soundscape } from '../src/engine/soundscape.js';
|
||||
import { soundscapes, soundscapeFor } from '../src/story/soundscapes.js';
|
||||
|
||||
function finale() {
|
||||
const g = new Game(story, { state: { scene: 'fortezza', x: 462, y: 310, facing: 1, inventory: ['torcia'],
|
||||
flags: { indizio_regolo: true, casetta_aperta: true }, used: {}, ended: false } });
|
||||
g.act('use', 'fessura', 'torcia'); settle(g);
|
||||
return g;
|
||||
}
|
||||
|
||||
test('la torcia aspetta la ricerca, richiede una sosta e non scopre gli occhi prima del quadro', () => {
|
||||
const g = finale();
|
||||
assert.equal(g.closeup.step, 'search');
|
||||
assert.equal(g.act('look', 'bastione'), false);
|
||||
assert.equal(g.walkTo([100, 300]), false);
|
||||
assert.equal(g.travel('piazza'), false);
|
||||
const eyes = story.closeups.fessura.targets[2].at;
|
||||
g.closeupAim(eyes); g.update(2);
|
||||
assert.equal(g.closeup.found, 0);
|
||||
g.closeupAim(story.closeups.fessura.targets[0].at); g.update(0.3);
|
||||
g.closeupAim(eyes); g.update(0.2);
|
||||
g.closeupAim(story.closeups.fessura.targets[0].at); g.update(0.3);
|
||||
assert.equal(g.closeup.step, 'search', 'la sosta riparte quando si sposta il fascio');
|
||||
g.update(0.2);
|
||||
assert.equal(g.closeup.step, 'reveal');
|
||||
assert.equal(g.speech, null, 'la pietra non rivela il testo del quadro');
|
||||
assert.equal(g.state.flags.palio_trovato, undefined);
|
||||
const lines = settle(g, { explore: true }).map(s => s.text);
|
||||
assert.ok(lines.indexOf('È il palio! Il quadro dei ragazzi delle medie!') < lines.indexOf('E quelli... sono due occhi gialli!'));
|
||||
assert.ok(g.checkpoint.ended && g.checkpoint.flags.palio_trovato);
|
||||
});
|
||||
|
||||
test('uscire dalla fessura o ricaricare durante la ricerca non finisce la partita e permette di riprovare', () => {
|
||||
const g = finale(), saved = g.savable();
|
||||
g.closeupAim(story.closeups.fessura.targets[0].at); g.update(1);
|
||||
assert.deepEqual(g.savable(), saved);
|
||||
assert.ok(g.closeupClose()); settle(g);
|
||||
assert.ok(!g.state.ended && !g.state.flags.palio_trovato);
|
||||
assert.ok(g.state.inventory.includes('torcia'));
|
||||
const reload = new Game(story, { state: g.savable() });
|
||||
reload.act('use', 'fessura', 'torcia'); settle(reload);
|
||||
assert.equal(reload.closeup.found, 0);
|
||||
settle(reload, { explore: true });
|
||||
assert.ok(reload.state.ended);
|
||||
});
|
||||
|
||||
test('aprire un primo piano ferma gli effetti successivi fino alla sua chiusura', () => {
|
||||
const g = new Game(story);
|
||||
g.queue = [{ closeup: 'libro' }, { set: 'dopo_libro' }];
|
||||
g.update(0);
|
||||
assert.equal(g.state.flags.dopo_libro, undefined);
|
||||
g.closeupClose(); settle(g);
|
||||
assert.equal(g.state.flags.dopo_libro, true);
|
||||
});
|
||||
|
||||
function audioContext() {
|
||||
const param = () => ({ value: 0, setValueAtTime(v) { this.value = v; }, linearRampToValueAtTime(v) { this.value = v; },
|
||||
exponentialRampToValueAtTime(v) { this.value = v; }, setTargetAtTime(v) { this.value = v; }, cancelScheduledValues() {} });
|
||||
const node = () => ({ gain: param(), frequency: param(), Q: param(), pan: param(), connect() {}, disconnect() {}, start() {}, stop() {} });
|
||||
return { currentTime: 0, sampleRate: 100, destination: {}, state: 'running',
|
||||
createGain: node, createBufferSource: node, createBiquadFilter: node, createStereoPanner: node, createOscillator: node,
|
||||
createBuffer: (_, length) => ({ getChannelData: () => new Float32Array(length) }) };
|
||||
}
|
||||
|
||||
test('suoni: muto e pausa fermano anche gli eventi già avviati, poi il fondo riparte', () => {
|
||||
const s = new Soundscape({ createContext: audioContext, random: () => 0.3 });
|
||||
s.unlock(); s.configure({ active: true }); s.update('acqua', 'pomeriggio', 0.1); s.play('page');
|
||||
assert.ok(s.nodes.size > 1);
|
||||
s.configure({ muted: true });
|
||||
assert.equal(s.nodes.size, 0); assert.equal(s.master.gain.value, 0);
|
||||
s.play('bell'); assert.equal(s.nodes.size, 0);
|
||||
s.configure({ muted: false }); s.update('acqua', 'pomeriggio', 0.1);
|
||||
assert.ok(s.bed);
|
||||
s.configure({ active: false }); assert.equal(s.nodes.size, 0);
|
||||
s.configure({ active: true, volume: 0 }); s.play('page'); assert.equal(s.nodes.size, 0);
|
||||
});
|
||||
|
||||
test('suoni: profili distinti, niente uccelli di sera, nessun accumulo ai cambi di luogo', () => {
|
||||
assert.equal(soundscapeFor('fortezza', 'fessura'), 'fessura');
|
||||
assert.notEqual(soundscapeFor('campomigliaio_ponte'), soundscapeFor('pieve_interno'));
|
||||
for (const def of Object.values(soundscapes)) assert.ok(!def.night.includes('bird'));
|
||||
const s = new Soundscape({ createContext: audioContext, random: () => 0.3 });
|
||||
s.unlock(); s.configure({ active: true });
|
||||
for (let i = 0; i < 30; i++) {
|
||||
s.update(i % 2 ? 'acqua' : 'bosco', 'sera', 0.1);
|
||||
assert.equal(s.nodes.size, 1);
|
||||
}
|
||||
const events = []; s.play = kind => events.push(kind);
|
||||
s.update('paese', 'sera', 0.1); s.update('paese', 'sera', 10);
|
||||
assert.deepEqual(events, ['cricket']);
|
||||
s.update('pieve', 'sera', 0.1); assert.equal(s.nodes.size, 0);
|
||||
});
|
||||
|
||||
test('Web Audio mancante non interrompe la partita', () => {
|
||||
const s = new Soundscape({ createContext: () => { throw new Error('non disponibile'); } });
|
||||
assert.doesNotThrow(() => { s.unlock(); s.configure({ active: true }); s.update('paese', 'sera', 1); s.play('page'); });
|
||||
});
|
||||
@@ -27,6 +27,8 @@ test('volume della musica, volume delle voci e muto si ricordano', () => {
|
||||
assert.equal(readAudio(s), null);
|
||||
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, voices: 0.4, effects: 0.65 });
|
||||
assert.deepEqual(readAudio(s), { volume: 0.3, muted: false, voices: 0.4, effects: 0.65 });
|
||||
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');
|
||||
|
||||
+24
-5
@@ -6,12 +6,16 @@ import { solution } from '../src/story/solution.js';
|
||||
import { Game, VERBS, checkSave, introText, shuffledCredits, matchPlace } from '../src/engine/game.js';
|
||||
import { inside } from '../src/engine/geometry.js';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { settle } from './helpers.mjs';
|
||||
import { settle, exploreTorch } from './helpers.mjs';
|
||||
|
||||
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() {
|
||||
for (const [id, c] of Object.entries(story.closeups ?? {})) {
|
||||
for (const k of ['comment', 'closing', 'complete']) yield [`${id}.${k}`, c[k] ?? []];
|
||||
for (const t of c.targets ?? []) yield [`${id}.target`, t.lines ?? []];
|
||||
}
|
||||
for (const [id, scene] of Object.entries(story.scenes)) {
|
||||
yield [`${id}.enter`, scene.enter ?? []];
|
||||
for (const h of scene.hotspots ?? [])
|
||||
@@ -66,7 +70,7 @@ test('la storia è coerente: scene, oggetti, dialoghi e punti calpestabili esist
|
||||
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}`);
|
||||
for (const src of c.type === 'torch' ? [c.picture] : [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}`);
|
||||
}
|
||||
@@ -118,7 +122,7 @@ test('il taccuino: lungo la soluzione le tappe si spuntano in ordine, una alla v
|
||||
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);
|
||||
settle(g, { explore: true });
|
||||
const n = g.notebook().done.length;
|
||||
assert.ok(n === last || n === last + 1, `tappe saltate dopo ${verb} ${target}`);
|
||||
last = n;
|
||||
@@ -134,7 +138,7 @@ test('le fasi del giorno seguono la storia: pomeriggio, poi tramonto, poi sera,
|
||||
const ids = story.phases.map(p => p.id), seen = [g.phase().id];
|
||||
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);
|
||||
settle(g, { explore: true });
|
||||
if (seen.at(-1) !== g.phase().id) seen.push(g.phase().id);
|
||||
}
|
||||
assert.deepEqual(seen, ids);
|
||||
@@ -166,7 +170,7 @@ test('la soluzione si percorre tutta dall\'inizio alla fine', () => {
|
||||
if (item) assert.ok(g.state.inventory.includes(item), `manca ${item}`);
|
||||
assert.ok(g.act(verb, target, item), `comando rifiutato: ${verb} ${target}`);
|
||||
}
|
||||
settle(g);
|
||||
settle(g, { explore: true });
|
||||
}
|
||||
assert.ok(g.state.ended, 'la partita non è finita');
|
||||
assert.ok(g.state.flags.indizio_regolo && g.state.flags.palio_trovato, 'il palio visto nella fessura');
|
||||
@@ -367,6 +371,7 @@ function stepChecked(g, label, step) {
|
||||
for (let i = 0; i < 20000; i++) {
|
||||
seen.push(essence(g.savable()));
|
||||
if (g.speech) g.skip();
|
||||
exploreTorch(g);
|
||||
if (!g.busy && !g.walking && !g.queue.length && !g.task) break;
|
||||
g.update(0.05);
|
||||
}
|
||||
@@ -584,3 +589,17 @@ test('il pettegolezzo cambia con le scoperte: vecchietti, Arcu e Tosca', () => {
|
||||
assert.ok(texts().includes('Ho la torcia. Stasera entro nella fessura.'));
|
||||
leave();
|
||||
});
|
||||
|
||||
test('i file che il Docker copia sono leggibili da tutti (nginx gira con un altro utente)', async () => {
|
||||
const { readdirSync, statSync } = await import('node:fs');
|
||||
const root = new URL('../', import.meta.url);
|
||||
const dirs = readFileSync(new URL('Dockerfile', root), 'utf8').match(/^COPY (assets\/\S+|src)\s/gm).map(l => l.split(' ')[1]);
|
||||
const closed = [];
|
||||
const walk = dir => { for (const e of readdirSync(new URL(dir + '/', root), { withFileTypes: true })) {
|
||||
const path = `${dir}/${e.name}`;
|
||||
if (e.isDirectory()) walk(path);
|
||||
else if (!(statSync(new URL(path, root)).mode & 0o004)) closed.push(path);
|
||||
} };
|
||||
dirs.forEach(walk);
|
||||
assert.deepEqual(closed, [], 'file non leggibili da nginx (chmod 644)');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user