Controllato: test, partita completa e il giro di schermate degli effetti (npm run browser:effects). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
107 lines
6.0 KiB
JavaScript
107 lines
6.0 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { loadBackground, hasBackground, RETRIES, RETRY_WAIT, flameFrame } 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;
|
|
}
|
|
});
|
|
|
|
test('i nomi del «?» non si sovrappongono, in nessuna scena', async () => {
|
|
const { placeLabels } = await import('../src/engine/render.js');
|
|
const { story } = await import('../src/story/story.js');
|
|
const H = 8, W = 6; // Press Start 2P a 6 px: 6 px a lettera
|
|
for (const [id, sc] of Object.entries(story.scenes)) {
|
|
const spots = (sc.hotspots ?? []).map(h => ({ x: Math.round(h.box[0] + h.box[2] / 2), y: Math.round(h.box[1] + h.box[3] / 2), w: h.name.length * W, name: h.name }));
|
|
const boxes = placeLabels(spots).map((p, i) => ({ l: p.x - spots[i].w / 2 - 1, r: p.x + spots[i].w / 2 + 1, t: p.y - H, b: p.y, name: spots[i].name }));
|
|
for (const [i, a] of boxes.entries()) {
|
|
assert.ok(a.l >= 0 && a.r <= 640 && a.t >= 0 && a.b <= 360, `${id}: «${a.name}» esce dalla scena`);
|
|
for (const b of boxes.slice(i + 1))
|
|
assert.ok(!(a.l < b.r && b.l < a.r && a.t < b.b && b.t < a.b), `${id}: «${a.name}» e «${b.name}» si sovrappongono`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('le candele tremolano: due disegni che si alternano a caso, non tutte insieme, e stanno dentro la scena', async () => {
|
|
const { story } = await import('../src/story/story.js');
|
|
const frames = Array.from({ length: 60 }, (_, k) => [0, 1, 2].map(i => flameFrame(k * 140, i)));
|
|
for (const i of [0, 1, 2]) assert.deepEqual(new Set(frames.map(f => f[i])), new Set([0, 1]), `la candela ${i} non cambia mai`);
|
|
assert.ok(frames.some(f => new Set(f).size > 1), 'le fiamme cambiano tutte insieme');
|
|
for (const [id, sc] of Object.entries(story.scenes)) for (const { at: [x, y] } of sc.lights ?? [])
|
|
assert.ok(x >= 0 && x < 640 && y >= 4 && y < 360, `${id}: candela fuori scena`);
|
|
});
|
|
|
|
test('le apparizioni (lo spirito del Trebbio) hanno la loro immagine e parlano con una voce del gioco', async () => {
|
|
const { story } = await import('../src/story/story.js');
|
|
const { existsSync } = await import('node:fs');
|
|
for (const [id, sc] of Object.entries(story.scenes)) for (const a of sc.apparitions ?? []) {
|
|
assert.ok(existsSync(new URL('../' + a.src, import.meta.url)), `${id}: manca ${a.src}`);
|
|
assert.ok(story.characters[a.who], `${id}: ${a.who} non è un personaggio`);
|
|
assert.ok(sc.speakers?.[a.who], `${id}: il fumetto di ${a.who} non ha un posto`);
|
|
}
|
|
});
|
|
|
|
test('gli effetti delle scene: tipi che il motore conosce, e dentro la scena', async () => {
|
|
const { story } = await import('../src/story/story.js');
|
|
const { EFFECTS } = await import('../src/engine/render.js');
|
|
const phases = new Set((story.phases ?? []).map(p => p.id));
|
|
for (const [id, sc] of Object.entries(story.scenes)) for (const e of sc.effects ?? []) {
|
|
assert.ok(EFFECTS[e.type], `${id}: effetto sconosciuto ${e.type}`);
|
|
for (const ph of [e.phase ?? []].flat()) assert.ok(phases.has(ph), `${id}: fase sconosciuta ${ph}`);
|
|
const pts = [...(e.points ?? []), ...(e.at ? [e.at] : []), ...(e.area ? [e.area.slice(0, 2), [e.area[0] + e.area[2], e.area[1] + e.area[3]]] : []), ...(e.rects ?? []).map(r => r.slice(0, 2))];
|
|
for (const [x, y] of pts) assert.ok(x >= 0 && x <= 640 && y >= 0 && y <= 360, `${id}: ${e.type} fuori scena`);
|
|
if (e.clip) {
|
|
assert.ok(e.clip.length >= 3, `${id}: ritaglio senza area`);
|
|
for (const [x, y] of e.clip) assert.ok(Number.isFinite(x) && Number.isFinite(y) && x >= 0 && x <= 640 && y >= 0 && y <= 360, `${id}: ritaglio fuori scena`);
|
|
}
|
|
if (e.duration) assert.ok(e.duration > 0 && e.duration < e.every, `${id}: manca la pausa fra gli eventi`);
|
|
}
|
|
});
|
|
|
|
test('gli animali hanno vere pause e ingressi graduali, anche con offset diversi', async () => {
|
|
const { ambientMoment } = await import('../src/engine/ambient.js');
|
|
const timing = { every: 40000, duration: 10000, offset: 3000 };
|
|
assert.equal(ambientMoment(8000, timing), null);
|
|
assert.equal(ambientMoment(36000, timing), null);
|
|
assert.deepEqual(ambientMoment(37000, timing), { t: 0, alpha: 0 });
|
|
assert.equal(ambientMoment(42000, timing).alpha, 1);
|
|
assert.ok(ambientMoment(46900, timing).alpha < 0.2);
|
|
assert.equal(ambientMoment(47000, timing), null);
|
|
assert.deepEqual(ambientMoment(42000, timing), ambientMoment(82000, timing));
|
|
});
|