// Copione del video «La Leggenda del Regolo · demo del prototipo» (scaletta in video/SCALETTA.md). // Si gira con la catena di qemu-iso-lab (tools/tour/rec.mjs) sulla VM lubuntu-studio, dopo video/prepara-ripresa.sh. // Solo sottotitoli in italiano: «en» ripete l'italiano perché la catena vuole due lingue. Le battute del gioco // restano nei fumetti; le didascalie stanno in alto e spiegano come si gioca. // Ogni cambio di brano (music.current) finisce in out/musica.json: monta.py ci rimonta la colonna sonora. import { mkdirSync, writeFileSync } from "node:fs"; const c = (id, it) => ({ id, it, en: it, top: true }); export const title = { it: "La Leggenda del Regolo · demo del prototipo", en: "La Leggenda del Regolo · demo del prototipo" }; export const cues = [ c("titolo", "La Leggenda del Regolo — demo del prototipo, ottobre 2026"), c("intro", "L'introduzione: un clic per riga, sopra il panorama del paese"), c("piazza", "Piazza Colonna. Un clic fa l'azione più sensata; a destra i verbi, come nelle vecchie avventure"), c("guarda", "Si sceglie un verbo, poi la cosa: «Guarda colonna»"), c("gelato", "Le uscite si prendono con un clic. Questa però porta in tentazione"), c("vecchietti", "I vecchietti della panchina: un clic su di loro e si parla"), c("scelte", "I dialoghi sono a scelte. Il Regolo? Ognuno ha la sua leggenda"), c("panchina", "Un indizio: guardiamo sotto la panchina"), c("squama", "Prendi: la cosa luccicante finisce nell'inventario"), c("inventario", "Gli oggetti si guardano, si usano con le cose, si danno alle persone"), c("giro", "Ora un giro del paese a gradini, uno sfondo per ogni posto vero"), c("campeggio", "L'ingresso del campeggio e, più su, il Bosco ai Frati"), c("pieve", "Dall'altra parte della piazza, la Pieve di San Pietro"), c("oratorio", "Dietro la porta, il cortile dell'oratorio, pronto per la festa"), c("villa", "Si sale: Villa Adami, gialla come un tuorlo"), c("salita", "La salita col leone di terracotta, verso la Fortezza"), c("fortezza", "Sotto le mura della Fortezza: porta incatenata e strane impronte"), c("trebbio", "Bloccati? Lungo il viale dei cipressi c'è il Castello del Trebbio"), c("psicologo", "Il castello fa da psicologo: ascolta e dà un indizio in base a quanto si è fatto"), c("ritorno", "Il consiglio è chiaro: si torna alla Fortezza"), c("finale", "Usa la squama con le impronte..."), c("fine", "Fine del prototipo: il resto della storia è ancora da scrivere"), ]; const URL_GIOCO = "http://127.0.0.1:8000/"; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); 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(1500); } export async function run(d) { const p = d.page; // --- Colonna sonora: si annota ogni cambio di brano, con il tempo dall'inizio della ripresa. const t0 = Date.now(), musica = []; let fermo = false; const ascolta = (async () => { let ultimo = null; while (!fermo) { try { const src = await p.evaluate(() => window.music?.want ?? null); if (src !== ultimo) { musica.push({ t: (Date.now() - t0) / 1000, src }); ultimo = src; } } catch {} await sleep(250); } })(); // --- Puntatore vero sulla scena (coordinate 640×360) e sui pulsanti. const punto = async (x, y) => { const r = await p.locator("#scene").boundingBox(), k = await p.evaluate(() => devicePixelRatio); return [(r.x + (x / 640) * r.width) * k, (r.y + (y / 360) * r.height) * k]; }; const verso = async ([x, y], dur = 0.8) => { const [a, b] = await punto(x, y); d.vm(`DISPLAY=:0 ~/lab/video/mv.py ${a} ${b} ${dur}`); }; const clicca = async ([x, y], dur = 0.8) => { const [a, b] = await punto(x, y); d.vm(`DISPLAY=:0 ~/lab/video/mv.py ${a} ${b} ${dur} click`); await sleep(300); }; const centro = (id) => p.evaluate((id) => { const h = game.hotspot(id); return [h.box[0] + h.box[2] / 2, h.box[1] + h.box[3] / 2]; }, id); // Aspetta che l'omino abbia finito di camminare e di parlare (le battute non si saltano: si leggono). const fermi = async (pausa = 600) => { await sleep(150); for (let i = 0; i < 1200; i++) { const ok = await p.evaluate(() => !game.speech && !game.busy && !game.walking && !game.queue.length && !game.task && !game.dialogue); const dialogo = await p.evaluate(() => !!game.dialogue && !game.speech && !game.queue.length); if (ok || dialogo) break; await sleep(100); } await sleep(pausa); }; const verbo = (v) => d.click(p.locator(`#verbs [data-verb="${v}"]`), 0.6); const oggetto = (id) => d.click(p.locator(`#inventory [data-item="${id}"]`), 0.6); // Un'azione completa: [verbo], [oggetto], la cosa; poi si aspetta. const fai = async (v, id, item) => { if (v) await verbo(v); if (item) await oggetto(item); await clicca(await centro(id)); await fermi(); }; const sfiora = async (id, ms = 900) => { await verso(await centro(id)); await sleep(ms); }; const scegli = async (testo) => { await d.click(p.locator("#choices li", { hasText: testo }), 0.6); await fermi(); }; const vai = async (uscita) => { await clicca(await centro(uscita)); await fermi(900); }; // 1. Titolo. await d.cue("titolo"); await d.hover(p.locator("#title .dedication"), 1.2); await sleep(4500); await d.click(p.locator("#new-game")); await sleep(2500); await d.click(p.locator('#rione [data-rione="campomigliaio"]')); // 2. Introduzione: una riga per clic. await d.cue("intro"); for (let i = 0; i < 6 && (await p.isVisible("#intro")); i++) { await sleep(4200); if (await p.isVisible("#intro")) await d.click(p.locator("#intro-text"), 0.5); } // 3. Piazza Colonna: le battute d'arrivo, poi colonna e manifesto. await d.cue("piazza"); await fermi(800); await sfiora("colonna"); await d.cue("guarda"); await fai("look", "colonna"); await fai("look", "manifesto"); await d.cue("gelato"); await fai(null, "verso_gelateria"); // 4. I vecchietti. await d.cue("vecchietti"); await sfiora("vecchietti"); await fai(null, "vecchietti"); await d.cue("scelte"); await scegli("chi ha preso il palio"); await scegli("Palio di quest"); await scegli("niente di strano stanotte"); await scegli("Ci vediamo dopo"); // 5. La squama. await d.cue("panchina"); await fai("look", "panchina"); await d.cue("squama"); await fai("take", "cosa_luccicante"); await d.cue("inventario"); await verbo("look"); await oggetto("squama"); await fermi(); await fai("give", "vecchietti", "squama"); // 6. Il giro: campeggio e Bosco ai Frati. await d.cue("giro"); await clicca([300, 330]); await fermi(400); await d.cue("campeggio"); await vai("verso_campeggio"); await fai("look", "bandiere"); await vai("bosco_ai_frati"); await fai("look", "tiglio"); await vai("giu_campeggio"); await vai("verso_piazza"); // 7. Pieve e oratorio. await d.cue("pieve"); await vai("verso_pieve"); await fai("look", "chiesa"); await fai("look", "campanile"); await fai("talk", "statua"); await d.cue("oratorio"); await vai("oratorio"); await fai("look", "palco"); await fai("use", "tavolate"); await vai("verso_pieve"); // 8. Villa Adami e la salita. await d.cue("villa"); await vai("salita_villa_adami"); await fai("open", "villa"); await d.cue("salita"); await vai("salita"); await fai("talk", "leone"); await fai(null, "verso_scuola"); // 9. Fortezza. await d.cue("fortezza"); await vai("su_fortezza"); await fai("look", "mura"); await fai("open", "porta"); await fai("look", "impronte"); // 10. Trebbio. await d.cue("trebbio"); await vai("sentiero_trebbio"); await fai("look", "cipressi"); await vai("al_trebbio"); await fai("look", "torre"); await d.cue("psicologo"); await fai(null, "torre"); await scegli("bloccato"); await scegli("un po' di pace"); // 11. Il finale. await d.cue("ritorno"); await vai("verso_fortezza"); await vai("alla_fortezza"); await d.cue("finale"); await fai("use", "impronte", "squama"); await p.waitForSelector("#ending:not([hidden])", { timeout: 30000 }); await d.cue("fine"); await d.hover(p.locator("#ending .dedication"), 1.2); await sleep(7000); fermo = true; await ascolta; const out = new URL("./out/", import.meta.url); mkdirSync(out, { recursive: true }); writeFileSync(new URL("musica.json", out), JSON.stringify({ durata: (Date.now() - t0) / 1000, cambi: musica }, null, 2)); }