Files

104 lines
6.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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: ['--autoplay-policy=no-user-gesture-required', '--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: '<style>body{margin:0}canvas{display:block}</style><canvas width="640" height="360"></canvas>' }));
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();
}