// 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(); }