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')); assert.equal(soundscapeFor('circolo_4_maggio'), 'biliardo', 'nella sala si sentono gli altri tavoli'); 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'); }); }); test('carte: fruscio, battuta e mescolata seguono volume effetti, muto e pausa', () => { const s = new Soundscape({ createContext: audioContext, random: () => 0.3 }); s.unlock(); for (const kind of ['card-slide', 'card-tap', 'card-shuffle']) { s.configure({ active: true, muted: false, volume: 0.27 }); s.play(kind); assert.ok(s.nodes.size > 0, kind); assert.equal(s.master.gain.value, 0.27); s.configure({ muted: true }); assert.equal(s.nodes.size, 0); s.play(kind); assert.equal(s.nodes.size, 0); s.configure({ muted: false, volume: 0 }); s.play(kind); assert.equal(s.nodes.size, 0); s.configure({ active: false, volume: 0.5 }); s.play(kind); assert.equal(s.nodes.size, 0); } });