- 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
208 lines
8.9 KiB
JavaScript
208 lines
8.9 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { Game, check, restoreState, newState } from '../src/engine/game.js';
|
|
import { settle } from './helpers.mjs';
|
|
|
|
// Una storia minima, indipendente da quella vera: il motore si collauda con i suoi dati.
|
|
const mini = () => ({
|
|
start: { scene: 'a', at: [10, 50] },
|
|
items: {
|
|
chiave: { name: 'chiave', actions: { look: [{ say: 'Una chiave.' }] }, with: { sasso: [{ say: 'Chiave sul sasso.' }] } },
|
|
sasso: { name: 'sasso' },
|
|
},
|
|
scenes: {
|
|
a: {
|
|
walk: [[0, 0], [200, 0], [200, 100], [0, 100]],
|
|
entry: [100, 50],
|
|
hotspots: [
|
|
{ id: 'chiave_a_terra', name: 'chiave a terra', box: [90, 40, 10, 10], at: [95, 50], when: '!has:chiave', actions: { take: [{ give: 'chiave' }] } },
|
|
{ id: 'porta', name: 'porta', box: [180, 20, 20, 60], at: [190, 50], actions: { open: [{ if: 'has:chiave', then: [{ go: 'b' }], else: [{ say: 'Chiusa.' }] }] } },
|
|
{ id: 'uscita', name: 'uscita', box: [0, 0, 5, 100], at: [2, 50], exit: { scene: 'b', at: [20, 20] } },
|
|
{ id: 'tizio', name: 'tizio', box: [50, 0, 10, 20], at: [55, 30], default: 'talk', actions: { talk: [{ dialogue: 'd' }] }, give: { sasso: [{ say: 'Grazie del sasso.', who: 'tizio' }, { drop: 'sasso' }] } },
|
|
],
|
|
},
|
|
b: { walk: [[0, 0], [100, 0], [100, 100], [0, 100]], entry: [50, 50], enter: [{ set: 'visto_b' }] },
|
|
},
|
|
dialogues: {
|
|
d: { nodes: { start: { options: [
|
|
{ id: 'uno', once: true, text: 'Una volta sola', then: [{ give: 'sasso' }] },
|
|
{ text: 'Dimmi altro', next: 'altro' },
|
|
{ text: 'Ciao', next: 'end' },
|
|
] }, altro: { options: [{ text: 'Torna', next: 'start' }, { text: 'Segreto', when: 'has:sasso', then: [{ set: 'segreto' }], next: 'end' }] } } },
|
|
},
|
|
});
|
|
|
|
test('condizioni su flag e inventario', () => {
|
|
const s = { flags: { a: true }, inventory: ['x'] };
|
|
assert.ok(check('a', s) && check('!b', s) && check('has:x', s) && check('!has:y', s));
|
|
assert.ok(check(['a', 'has:x'], s) && !check(['a', 'b'], s) && check(undefined, s));
|
|
});
|
|
|
|
test('cammina fino all\'oggetto, lo prende e lo toglie dalla scena', () => {
|
|
const g = new Game(mini());
|
|
assert.ok(g.act('take', 'chiave_a_terra'));
|
|
settle(g);
|
|
assert.deepEqual([g.state.x, g.state.y], [95, 50]);
|
|
assert.deepEqual(g.state.inventory, ['chiave']);
|
|
assert.equal(g.hotspot('chiave_a_terra'), undefined);
|
|
});
|
|
|
|
test('le risposte di ripiego non lasciano mai il giocatore senza una frase', () => {
|
|
const g = new Game(mini());
|
|
for (const verb of ['look', 'take', 'use', 'talk', 'open', 'give']) assert.ok(g.resolve(verb, 'porta').length, verb);
|
|
assert.deepEqual(g.resolve('use', 'porta', 'sasso'), [{ say: 'Non funziona.' }]);
|
|
assert.deepEqual(g.resolve('take', 'sasso'), [{ say: 'Ce l\'ho già.' }]);
|
|
});
|
|
|
|
test('usa oggetto con oggetto in entrambi i versi', () => {
|
|
const g = new Game(mini());
|
|
assert.deepEqual(g.resolve('use', 'sasso', 'chiave'), [{ say: 'Chiave sul sasso.' }]);
|
|
assert.deepEqual(g.resolve('use', 'chiave', 'sasso'), [{ say: 'Chiave sul sasso.' }]);
|
|
});
|
|
|
|
test('porta chiusa, poi aperta con la chiave; il cambio scena salva ed esegue l\'ingresso', () => {
|
|
const saves = [];
|
|
const g = new Game(mini(), { onSave: s => saves.push(s) });
|
|
g.act('open', 'porta');
|
|
assert.deepEqual(settle(g).map(s => s.text), ['Chiusa.']);
|
|
g.act('take', 'chiave_a_terra'); settle(g);
|
|
g.act('open', 'porta'); settle(g);
|
|
assert.equal(g.state.scene, 'b');
|
|
assert.deepEqual([g.state.x, g.state.y], [50, 50]);
|
|
assert.ok(g.state.flags.visto_b);
|
|
assert.equal(saves.at(-1).scene, 'b');
|
|
});
|
|
|
|
test('l\'uscita porta nell\'altra scena', () => {
|
|
const g = new Game(mini());
|
|
g.act('walk', 'uscita'); settle(g);
|
|
assert.equal(g.state.scene, 'b');
|
|
assert.deepEqual([g.state.x, g.state.y], [20, 20]);
|
|
});
|
|
|
|
test('un clic durante la camminata cambia destinazione e scarta l\'azione', () => {
|
|
const g = new Game(mini());
|
|
g.act('take', 'chiave_a_terra');
|
|
g.update(0.1);
|
|
assert.ok(g.walking);
|
|
assert.ok(g.walkTo([10, 10]));
|
|
settle(g);
|
|
assert.deepEqual(g.state.inventory, []);
|
|
assert.deepEqual([g.state.x, g.state.y], [10, 10]);
|
|
});
|
|
|
|
test('mentre parla un personaggio i comandi sono ignorati', () => {
|
|
const g = new Game(mini());
|
|
g.queue.push({ say: 'Aspetta.' });
|
|
g.update(0.01);
|
|
assert.ok(g.speech && g.busy);
|
|
assert.equal(g.walkTo([10, 10]), false);
|
|
g.update(10);
|
|
assert.equal(g.speech, null);
|
|
});
|
|
|
|
test('dialogo: opzioni una tantum, nodi, condizioni e uscita', () => {
|
|
const g = new Game(mini());
|
|
g.act('talk', 'tizio'); settle(g);
|
|
assert.ok(g.dialogue);
|
|
assert.deepEqual(g.options().map(o => o.text), ['Una volta sola', 'Dimmi altro', 'Ciao']);
|
|
g.choose(0); settle(g);
|
|
assert.deepEqual(g.state.inventory, ['sasso']);
|
|
assert.deepEqual(g.options().map(o => o.text), ['Dimmi altro', 'Ciao']);
|
|
g.choose(0); settle(g);
|
|
assert.deepEqual(g.options().map(o => o.text), ['Torna', 'Segreto']);
|
|
g.choose(1); settle(g);
|
|
assert.equal(g.dialogue, null);
|
|
assert.ok(g.state.flags.segreto);
|
|
assert.equal(g.walkTo([20, 20]), true);
|
|
});
|
|
|
|
test('dare un oggetto a un personaggio', () => {
|
|
const g = new Game(mini());
|
|
g.state.inventory.push('sasso');
|
|
g.act('give', 'tizio', 'sasso');
|
|
const said = settle(g);
|
|
assert.deepEqual(said.map(s => [s.who, s.text]), [['tizio', 'Grazie del sasso.']]);
|
|
assert.deepEqual(g.state.inventory, []);
|
|
});
|
|
|
|
test('un salvataggio rovinato o vecchio non rompe la partita', () => {
|
|
const story = mini();
|
|
assert.deepEqual(restoreState(story, null), newState(story));
|
|
assert.deepEqual(restoreState(story, { scene: 'sparita' }), newState(story));
|
|
const s = restoreState(story, { scene: 'b', x: 500, y: 50, inventory: ['chiave', 'fantasma', 'chiave'], flags: { ok: true, strano: 3 }, used: 'x' });
|
|
assert.deepEqual(s, { scene: 'b', x: 100, y: 50, facing: 1, inventory: ['chiave'], flags: { ok: true }, used: {}, ended: false });
|
|
});
|
|
|
|
test('lo stato salvato si ricarica uguale', () => {
|
|
const g = new Game(mini());
|
|
g.act('take', 'chiave_a_terra'); settle(g);
|
|
const again = new Game(mini(), { state: JSON.parse(JSON.stringify(g.snapshot())) });
|
|
assert.deepEqual(again.snapshot(), g.snapshot());
|
|
});
|
|
|
|
test('il fotogramma segue quello che fa Andrea', () => {
|
|
const g = new Game(mini());
|
|
assert.equal(g.pose(), 'stand');
|
|
g.walkTo([150, 50]);
|
|
const seen = new Set();
|
|
for (let i = 0; i < 40; i++) { g.update(0.05); if (g.walking) seen.add(g.pose()); }
|
|
assert.deepEqual([...seen].sort(), ['walk_1', 'walk_2', 'walk_3', 'walk_4']);
|
|
g.queue.push({ face: 'up' });
|
|
settle(g);
|
|
assert.equal(g.pose(), 'back');
|
|
g.queue.push({ give: 'sasso' });
|
|
g.update(0.01);
|
|
assert.equal(g.pose(), 'take');
|
|
assert.ok(g.busy, 'mentre prende qualcosa non si interrompe');
|
|
settle(g);
|
|
assert.equal(g.pose(), 'stand');
|
|
g.queue.push({ say: 'Ciao a tutti.' });
|
|
g.update(0.01);
|
|
const poses = new Set();
|
|
for (let i = 0; i < 10; i++) { g.time += 0.1; poses.add(g.pose()); }
|
|
assert.deepEqual([...poses].sort(), ['stand', 'talk']);
|
|
});
|
|
|
|
test('i personaggi della scena: posa da fermi, battito di ciglia, parlato e posa della battuta', () => {
|
|
const story = mini();
|
|
story.scenes.a.people = [{ who: 'tizio', at: [50, 30], talk: ['talk', 'explain'], blink: 'blink', every: 2 }];
|
|
const g = new Game(story);
|
|
g.go('a');
|
|
const p = g.people()[0];
|
|
g.time = 0.5;
|
|
assert.equal(g.personPose(p), 'sit');
|
|
g.time = 1.9;
|
|
assert.equal(g.personPose(p), 'blink');
|
|
g.speech = { who: 'tizio', text: '...', left: 1 };
|
|
g.time = 0.1;
|
|
assert.equal(g.personPose(p), 'talk');
|
|
g.time = 0.4;
|
|
assert.equal(g.personPose(p), 'explain');
|
|
g.speech.pose = 'point';
|
|
assert.equal(g.personPose(p), 'point');
|
|
});
|
|
|
|
test('col doppio clic si cammina al doppio della velocità', () => {
|
|
const far = hurry => {
|
|
const g = new Game(mini());
|
|
g.go('a', [10, 50]);
|
|
g.hurry = hurry;
|
|
g.walkTo([190, 50]);
|
|
g.update(0.5);
|
|
return g.state.x - 10;
|
|
};
|
|
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');
|
|
});
|