Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
104 lines
6.2 KiB
JavaScript
104 lines
6.2 KiB
JavaScript
// 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: '<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();
|
||
}
|