// Pezzi comuni dei copioni «Come si gioca» (video/SCALETTA-CARTE.md): briscola, tressette, scopa, bocce, biliardo. // Si girano come demo-prototipo.mjs (video/SCALETTA.md, «Come si rigira»); prima `python3 video/narra.py ` // per la voce del narratore, dopo `python3 video/monta.py --tutorial`. import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; export const URL_GIOCO = "http://127.0.0.1:8000/"; export const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); export const c = (id, it) => ({ id, it, en: it, top: true }); // La durata di ogni frase del narratore (video/narra.py): la battuta resta finché la voce non ha finito. export function narrazione(clip) { const f = new URL(`./out/${clip}-narrazione.json`, import.meta.url); return existsSync(f) ? Object.fromEntries(JSON.parse(readFileSync(f, "utf8")).voci.map((v) => [v.id, v.sec * 1000])) : {}; } export async function setup(d) { const p = d.page; await p.goto(URL_GIOCO); await p.evaluate(() => localStorage.clear()); await p.reload(); await p.waitForSelector("#title:not([hidden])"); await sleep(1000); } export function regista(d, clip) { const p = d.page, voce = narrazione(clip); let inizio = 0, dura = 0; const r = { // Una battuta del narratore: prima si lascia finire quella di prima. async cue(id) { const resto = inizio + dura + 350 - Date.now(); if (resto > 0) await sleep(resto); await d.cue(id); inizio = Date.now(); dura = voce[id] ?? 0; }, async fine() { const resto = inizio + dura + 600 - Date.now(); if (resto > 0) await sleep(resto); }, // Puntatore vero sulla scena (coordinate 640×360). async punto(x, y) { const b = await p.locator("#scene").boundingBox(), k = await p.evaluate(() => devicePixelRatio); return [(b.x + (x / 640) * b.width) * k, (b.y + (y / 360) * b.height) * k]; }, async clicca([x, y], dur = 0.8) { const [a, b] = await r.punto(x, y); d.vm(`DISPLAY=:0 ~/lab/video/mv.py ${a} ${b} ${dur} click`); await sleep(300); }, async muovi([x, y], dur = 0.8) { const [a, b] = await r.punto(x, y); d.vm(`DISPLAY=:0 ~/lab/video/mv.py ${a} ${b} ${dur}`); await sleep(200); }, // Aspetta che l'omino abbia finito di camminare e di parlare; un dialogo o un primo piano aperti fermano l'attesa. async fermi(pausa = 500) { await sleep(150); for (let i = 0; i < 1500; i++) { const ok = await p.evaluate(() => { if (game.speech || (game.queue.length && !game.closeup) || game.walking || game.reach > 0) return false; return !!game.dialogue || !!game.closeup || (!game.busy && !game.task); }); if (ok) break; await sleep(100); } await sleep(pausa); }, async scegli(testo) { await d.click(p.locator("#choices li", { hasText: testo }), 0.6); await r.fermi(); }, // Partita nuova per Campomigliaio, introduzione saltata, dritti nella scena del gioco. async vaiA(scena, at) { await d.click(p.locator("#new-game")); await sleep(1200); await d.click(p.locator('#rione [data-rione="campomigliaio"]')); await sleep(800); if (await p.isVisible("#skip-intro")) await d.click(p.locator("#skip-intro"), 0.5); await p.evaluate(([s, a]) => { game.go(s, a); }, [scena, at]); await r.fermi(1200); }, async parlaCon(id) { const [x, y] = await p.evaluate((id) => { const h = game.hotspot(id); return [h.box[0] + h.box[2] / 2, h.box[1] + h.box[3] / 2]; }, id); await r.clicca([x, y]); await r.fermi(); }, // L'audio annotato dentro la pagina (musica voluta e voci del gioco), per rimontarlo (monta.py). async ascolta() { await p.evaluate(() => { window.__t0 = Date.now(); window.__audio = []; let music = undefined, voice = null; const t = () => (Date.now() - window.__t0) / 1000; const path = (src) => decodeURI(new URL(src, location.href).pathname.slice(1)); const look = () => { if (window.music && window.music.want !== music) { music = window.music.want; window.__audio.push({ t: t(), music: music ?? null }); } const a = window.voices?.audio, now = a && !a.paused && !a.ended ? a : null; if (now !== voice) { if (voice) window.__audio.push({ t: t(), voiceEnd: true }); if (now) window.__audio.push({ t: t(), voice: path(now.src) }); voice = now; } requestAnimationFrame(look); }; look(); }); }, async salvaAudio() { const audio = await p.evaluate(() => window.__audio); const out = new URL("./out/", import.meta.url); mkdirSync(out, { recursive: true }); writeFileSync(new URL(`${clip}-audio.json`, out), JSON.stringify({ eventi: audio }, null, 2)); }, }; return r; }