// Copione del video «La Leggenda del Regolo · la partita completa» (primo capitolo, dal pomeriggio alla sera). // Si gira come demo-prototipo.mjs (video/SCALETTA.md, «Come si rigira»), poi: python3 video/monta.py . // Segue la soluzione vera (src/story/solution.js), capitolo per capitolo, con due aggiunte: il libro dell'oratorio // in primo piano e, a sera, i vecchietti che sparlano di Andrea. // L'audio non si registra (la VM prende solo lo schermo): la pagina annota ogni cambio di musica e ogni voce che // parte, col suo tempo, in out/partita-completa-audio.json; monta.py rimonta la colonna sonora dai file del gioco. import { mkdirSync, writeFileSync } from "node:fs"; import { solution, route } from "../src/story/solution.js"; const c = (id, it) => ({ id, it, en: it, top: true }); export const title = { it: "La Leggenda del Regolo · la partita completa", en: "La Leggenda del Regolo · la partita completa" }; export const cues = [ c("titolo", "La Leggenda del Regolo — il primo capitolo, dall'inizio alla fine"), c("intro", "Si corre per Campomigliaio, i blu. L'introduzione, sopra il panorama del Mugello"), c("piazza", "Pomeriggio, Piazza Colonna. I vecchietti della panchina sanno sempre tutto"), c("fortezza", "Sotto la Fortezza, impronte strane. E il sole comincia a calare"), c("pieve", "La Pieve: l'indovinello di San Pietro apre la porta dell'oratorio"), c("libro", "Nell'oratorio, sulle tavolate, un libro dimenticato. Bello spesso"), c("tosca", "Al tennis della chiesa c'è la Tosca: ghiacciolo al limone, inzuppato nella cedrata"), c("chiavetta", "Stecco e gomma ripescano una chiavetta. Nella casetta dell'area feste, una torcia"), c("sera", "È sera. In piazza, intanto, si chiacchiera"), c("finale", "Di corsa alla Fortezza: la torcia nella fessura del bastione"), c("fine", "Fine del primo capitolo"), ]; const URL_GIOCO = "http://127.0.0.1:8000/"; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const at = (verb, target) => solution.findIndex(([v, t]) => v === verb && t === target); 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; // --- L'audio, annotato dentro la pagina (orologio della VM, a ogni fotogramma): musica voluta e voce in corso. 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(); }); // --- 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 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 ascoltano). // Un dialogo aperto o un primo piano fermo aspettano il giocatore: lì ci si ferma. const fermi = async (pausa = 600) => { 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); }; 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); const scegli = async (testo) => { await d.click(p.locator("#choices li", { hasText: testo }), 0.6); await fermi(); }; // Un passo della soluzione, come lo farebbe un giocatore: il verbo (se il clic da solo non basta), l'oggetto, la cosa. const passo = async ([verb, target, item]) => { if (verb === "choose") return scegli(target); const smart = await p.evaluate(([v, t]) => game.defaultVerb(t) === v, [verb, target]); if (item) { await verbo(verb); await oggetto(item); } else if (!smart) await verbo(verb); if (await p.evaluate((t) => !!game.story.items[t], target)) await oggetto(target); else await clicca(await centro(target)); await fermi(verb === "walk" ? 900 : 700); if (await p.evaluate(() => game.closeup?.id === 'fessura')) { for (let i = 0; i < 3; i++) { const target = await p.evaluate(i => game.story.closeups.fessura.targets[i].at, i); const [x, y] = await punto(...target); d.vm(`DISPLAY=:0 ~/lab/video/mv.py ${x} ${y} 1.2`); await p.waitForFunction(i => game.closeup?.found > i, i, { timeout: 60000 }); } await d.click(p.locator('#closeup-close'), 0.6); await fermi(); } }; const passi = async (lista) => { for (const s of lista) await passo(s); }; // 1. Titolo, rione, introduzione (una riga per clic). await d.cue("titolo"); await d.hover(p.locator("#title .dedication"), 1.2); await sleep(4000); await d.click(p.locator("#new-game")); await sleep(2500); await d.click(p.locator('#rione [data-rione="campomigliaio"]')); await d.cue("intro"); for (let i = 0; i < 8 && (await p.isVisible("#intro")); i++) { await sleep(4200); if (await p.isVisible("#intro")) await d.click(p.locator("#intro-text"), 0.5); } await fermi(800); // 2-7. La soluzione, a capitoli. const capitoli = [ ["piazza", at("take", "cosa_luccicante")], ["fortezza", at("look", "fessura")], ["pieve", at("take", "gomma_terra")], ]; let da = 0; for (const [cue, fino] of capitoli) { await d.cue(cue); await passi(solution.slice(da, fino + 1)); da = fino + 1; } // Il libro dell'oratorio: preso in mano, si apre, si gira la pagina, «FINE»; poi si chiude. await d.cue("libro"); await verbo("take"); await clicca(await centro("libro_dimenticato")); await p.waitForFunction(() => game.closeup?.step === "cover", null, { timeout: 30000 }); await sleep(2800); await clicca([320, 200]); await sleep(2600); await clicca([320, 200]); await p.waitForFunction(() => game.closeup?.lines && !game.closeup.lines.length && !game.speech, null, { timeout: 30000 }); await sleep(1500); await d.click(p.locator("#closeup-close"), 0.6); await fermi(); for (const [cue, fino] of [["tosca", at("use", "gomma", "stecco")], ["chiavetta", at("open", "casetta")]]) { await d.cue(cue); await passi(solution.slice(da, fino + 1)); da = fino + 1; } // 8. Sera: una deviazione in piazza, dove i vecchietti parlano di Andrea prima di accorgersi che è lì. await d.cue("sera"); await passi(route("area_feste", "piazza")); await passo(["talk", "vecchietti"]); await scegli("Ho una torcia"); await scegli("Ci vediamo dopo"); // 9. Il finale: la torcia nella fessura. await d.cue("finale"); await passi(route("piazza", "fortezza")); await passi(solution.slice(-1)); await p.waitForSelector("#ending:not([hidden])", { timeout: 60000 }); await d.cue("fine"); await d.hover(p.locator("#ending .dedication"), 1.2); await sleep(7000); const audio = await p.evaluate(() => window.__audio); const out = new URL("./out/", import.meta.url); mkdirSync(out, { recursive: true }); writeFileSync(new URL("partita-completa-audio.json", out), JSON.stringify({ eventi: audio }, null, 2)); }