Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
112 lines
5.7 KiB
JavaScript
112 lines
5.7 KiB
JavaScript
// 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 <copione>`
|
||
// per la voce del narratore, dopo `python3 video/monta.py <cartella> --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;
|
||
// Niente cache: il Chromium della VM teneva i moduli JavaScript di una ripresa prima (il valzer non partiva).
|
||
const cdp = await p.context().newCDPSession(p);
|
||
await cdp.send("Network.enable");
|
||
await cdp.send("Network.setCacheDisabled", { cacheDisabled: true });
|
||
await cdp.send("Network.clearBrowserCache");
|
||
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, e chi sta parlando nel gioco.
|
||
async cue(id) {
|
||
await r.aspetta();
|
||
for (let i = 0; i < 300 && (await p.evaluate(() => !!game.speech || !!(window.voices?.audio && !window.voices.audio.paused && !window.voices.audio.ended))); i++) await sleep(100);
|
||
await sleep(250);
|
||
await d.cue(id);
|
||
inizio = Date.now(); dura = voce[id] ?? 0;
|
||
},
|
||
// Prima di far parlare un personaggio, il narratore deve aver finito: le due voci non si sovrappongono.
|
||
async aspetta() { const resto = inizio + dura + 350 - Date.now(); if (resto > 0) await sleep(resto); },
|
||
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 r.aspetta(); 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);
|
||
// Le battute d'apertura di Andrea (il palio sparito) qui non servono: si saltano tutte.
|
||
await p.evaluate(([s, a]) => { for (let i = 0; i < 50 && (game.speech || game.queue.length); i++) { game.skip(); game.update(0.05); } game.go(s, a); }, [scena, at]);
|
||
await r.fermi(1200);
|
||
},
|
||
async parlaCon(id) {
|
||
await r.aspetta();
|
||
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;
|
||
}
|