Video della partita completa (pomeriggio → sera) con voci e musiche rimontate dal registro audio del copione; inventario: nomi lunghi su due righe dentro il pulsante

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
This commit is contained in:
2026-10-25 17:36:06 +01:00
co-authored by Claude Opus 5.5
parent 32a3d8a7c5
commit 1fabb6e543
5 changed files with 249 additions and 22 deletions
+167
View File
@@ -0,0 +1,167 @@
// 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 <cartella> .
// 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);
};
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));
}