// Controllo grafico degli ambienti, senza titolo/audio: tutte le scene nelle tre fasi,
// ritagli dei nuovi effetti, pause degli animali e nessuna modifica alla partita.
// npm run browser:effects — immagini in /tmp/regolo-effects (oppure $OUT).
import { chromium } from 'playwright';
import { staticServer } from './helpers.mjs';
import { mkdirSync, writeFileSync } from 'node:fs';
import assert from 'node:assert/strict';
const out = process.env.OUT ?? '/tmp/regolo-effects';
mkdirSync(out, { recursive: true });
const server = await staticServer(), port = server.port;
let browser;
try {
browser = await chromium.launch({ args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
const page = await browser.newPage({ viewport: { width: 640, height: 360 } });
const errors = [];
page.on('pageerror', e => errors.push(e.message));
page.on('response', r => { if (r.status() >= 400) errors.push(`${r.status()} ${r.url()}`); });
// La pagina vive sullo stesso server del gioco, ma non avvia main.js e il suo RAF.
await page.route('**/__effects', route => route.fulfill({ contentType: 'text/html', body: '' }));
await page.goto(`http://127.0.0.1:${port}/__effects`);
await page.evaluate(async () => {
const [{ story }, { Game }, renderer, { ambience }] = await Promise.all([
import('/src/story/story.js'), import('/src/engine/game.js'), import('/src/engine/render.js'), import('/src/story/ambience.js'),
]);
// Attende davvero il server prima delle immagini, senza dipendere da una pausa fissa.
const loaded = await renderer.loadAssets(story);
if (loaded.some(ok => !ok)) throw new Error('Uno sfondo/personaggio non è arrivato');
await Promise.all(Array.from({ length: 8 }, (_, i) => renderer.loadBackground(`assets/effects/nuvola-${i + 1}.png`)));
Object.assign(window, { story, Game, renderer, ambience, ctx: document.querySelector('canvas').getContext('2d') });
});
const scenes = await page.evaluate(() => Object.keys(story.scenes));
for (const phase of ['pomeriggio', 'tramonto', 'sera']) {
for (const id of scenes) {
const result = await page.evaluate(({ id, phase }) => {
const g = new Game(story), sc = story.scenes[id];
g.state.scene = id;
[g.state.x, g.state.y] = sc.entry;
g.state.flags = phase === 'sera' ? { casetta_aperta: true } : phase === 'tramonto' ? { indizio_regolo: true } : {};
g.enteredAt = 0;
const saved = JSON.stringify(g.snapshot()), times = [8, 15, 28, 48];
let first;
for (const time of times) {
g.time = time;
renderer.render(ctx, g, { time: time * 1000 });
if (time === 8) first = ctx.canvas.toDataURL();
if (ctx.globalAlpha !== 1 || !ctx.getTransform().isIdentity) throw new Error(`${id}: stato canvas non ripristinato`);
}
return { first, last: ctx.canvas.toDataURL(), unchanged: JSON.stringify(g.snapshot()) === saved };
}, { id, phase });
assert.ok(result.unchanged, `${id}: gli effetti modificano il salvataggio`);
writeFileSync(`${out}/${id}-${phase}.png`, Buffer.from(result.first.split(',')[1], 'base64'));
if (phase === 'pomeriggio') writeFileSync(`${out}/${id}-later.png`, Buffer.from(result.last.split(',')[1], 'base64'));
}
}
const checked = await page.evaluate(() => {
let clips = 0, phases = 0, pauses = 0;
const scene = { entry: [620, 355], effects: [], hotspots: [] };
const testStory = { ...story, start: { scene: 'test', at: scene.entry }, scenes: { test: scene } };
const g = new Game(testStory);
const pixels = effects => {
scene.effects = effects;
renderer.render(ctx, g, { time: g.time * 1000 });
return ctx.getImageData(0, 0, 640, 360).data;
};
const changed = (a, b) => { for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return true; return false; };
const mask = document.createElement('canvas'); mask.width = 640; mask.height = 360;
const mc = mask.getContext('2d');
for (const [id, effects] of Object.entries(ambience)) for (const e of effects) {
g.time = ((e.every ?? 0) + (e.duration ?? 12000) * 0.5 - (e.offset ?? 0)) / 1000;
g.state.flags = e.phase === 'sera' ? { casetta_aperta: true } : {};
const before = pixels([]), after = pixels([e]);
if (e.clip) {
mc.clearRect(0, 0, 640, 360); mc.beginPath();
e.clip.forEach(([x, y], i) => i ? mc.lineTo(x, y) : mc.moveTo(x, y));
mc.closePath(); mc.fill();
const allowed = mc.getImageData(0, 0, 640, 360).data;
for (let i = 0; i < before.length; i += 4) {
if (allowed[i + 3]) continue;
for (let c = 0; c < 4; c++) if (before[i + c] !== after[i + c]) throw new Error(`${id}/${e.type}: disegna fuori dal ritaglio`);
}
clips++;
}
if (['cat', 'groundbird', 'insect'].includes(e.type)) {
if (!changed(before, after)) throw new Error(`${id}/${e.type}: animale invisibile`);
g.time = (e.every + e.duration + 100 - (e.offset ?? 0)) / 1000;
if (changed(pixels([]), pixels([e]))) throw new Error(`${id}/${e.type}: manca la pausa`);
pauses++;
}
if (e.phase) {
g.state.flags = e.phase === 'sera' ? {} : { casetta_aperta: true };
if (changed(pixels([]), pixels([e]))) throw new Error(`${id}/${e.type}: appare nella fase sbagliata`);
phases++;
}
}
return { clips, phases, pauses };
});
assert.deepEqual(errors, []);
console.log(`${scenes.length} scene × 3 fasi × 4 istanti; ${checked.clips} ritagli, ${checked.phases} condizioni giorno/sera, ${checked.pauses} pause animali. Immagini: ${out}`);
} finally {
await browser?.close();
server.kill();
}