Files
leggenda-del-regolo/video/partita-completa.mjs
T

178 lines
8.2 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.
// 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);
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));
}