Dopo «Nuova partita» la domanda «Per chi corri quest'anno?» con i quattro rioni nei loro colori; la scelta resta nella partita (rione:<id>) e cambia la riga dell'introduzione e le battute sul rione di Andrea. Voci nuove. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
212 lines
8.4 KiB
JavaScript
212 lines
8.4 KiB
JavaScript
// 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));
|
||
}
|