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:
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user