Files
leggenda-del-regolo/video/tutorial-comune.mjs
T
manzoloandClaude Opus 5.5 7e1e6f6144 Giochi di carte dopo la revisione di Codex sui tre insieme; player dei video «Come si gioca» e primo copione
Esito che vale e si salva appena deciso, per tutti i minigiochi (anche tornando al titolo dalla schermata del risultato); briscola: uscire durante il conteggio non perde più la vittoria; tressette: si esce ai 21 col primo evento che ci porta, accusa compresa (prima vinceva sempre Andrea se ci arrivavano tutte e due), il carico che fa vincere si dà anche se è il 2, accuse che non escono dal tavolo, convenzioni scritte nell'aiuto; scopa: primiera valutata per quel che migliora, finale esatto a mazzo finito, presa sulla tavola al rilascio con scelta e conferma, frecce e Invio sempre sulla presa scelta. Tastiera: Invio e Spazio giocano sempre la carta scelta. Tavola descritta a parole. Classi CSS comuni ai tre giochi.
Video: player nel gioco (pulsante «Guarda il video» nel «Come si gioca», compare quando il video esiste), video/narra.py per la voce del narratore, pezzi comuni dei copioni e il copione della briscola; scaletta in video/SCALETTA-CARTE.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
2026-10-26 16:39:22 +01:00

102 lines
4.8 KiB
JavaScript
Raw 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.
// 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;
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;
}