// Copione del trailer «La Leggenda del Regolo» (scaletta approvata da Andrea il 29 ottobre): circa 75 secondi, la scena a // tutto schermo, scritte grandi e stacchi al nero fatti dentro la pagina, niente puntatore e niente clic. // Si gira come la partita completa (video/SCALETTA.md), poi: python3 video/monta.py --trailer . // La musica non è quella che sceglierebbe il gioco: il copione annota i brani voluti, e le voci che partono, in // out/trailer-audio.json; monta.py 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 · trailer", en: "La Leggenda del Regolo · trailer" }; export const cues = [ c("apertura", "San Piero a Sieve, maggio 2007"), c("notte", "La notte prima del Palio…"), c("sparito", "…il palio è sparito."), c("paese", "il paese"), c("sfide", "Un paese pieno di sfide"), c("amici", "…e di vecchie conoscenze"), c("buio", "la fessura"), c("titolo", "La Leggenda del Regolo"), ]; 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 sleep(500); if (await p.evaluate(() => document.getElementById("splash").classList.contains("wait"))) await p.click("#splash"); await p.waitForSelector("#title:not([hidden])"); // Si entra in partita dietro le quinte: rione, introduzione saltata. await p.click("#new-game"); await p.click('#rione [data-rione="campomigliaio"]'); await p.click("#skip-intro"); // Il palcoscenico del trailer: solo la scena, a tutto schermo; sopra, il nero e le scritte. await p.addStyleTag({ content: ` #side, #corner, #sound, #closeup-close, #bocce-controls, #briscola-controls, #tressette-controls, #scopa-controls, #map-button, #pause-button, #spots-button, #notebook-button, #sentence { display: none !important; } body { background: #000 !important; overflow: hidden; } #app { position: fixed !important; inset: 0 !important; width: 100vw !important; margin: 0 !important; } #stage, #stage.biliardo, #stage.briscola { width: 100vw !important; height: 100vh !important; } #scene { width: 100vw !important; height: 100vh !important; } #title .buttons, #title .version, #title .dedication { display: none !important; } #tr-nero { position: fixed; inset: 0; background: #000; opacity: 1; transition: opacity .7s; z-index: 50; pointer-events: none; } #tr-scritta { position: fixed; left: 0; right: 0; top: 50%; transform: translateY(-50%); text-align: center; z-index: 51; font-family: "Press Start 2P", monospace; font-size: 42px; line-height: 1.6; color: #ffe9b0; opacity: 0; transition: opacity .8s; text-shadow: 4px 4px 0 #2a1608, 0 0 24px rgba(0,0,0,.9); pointer-events: none; padding: 0 60px; } #tr-scritta.basso { top: auto; bottom: 70px; transform: none; font-size: 32px; } ` }); await p.evaluate(() => { for (const id of ["tr-nero", "tr-scritta"]) { const e = document.createElement("div"); e.id = id; document.body.appendChild(e); } }); // Senza nessun tocco il gioco va in pausa da solo dopo un minuto: un finto movimento ogni tanto lo tiene sveglio. await p.evaluate(() => setInterval(() => dispatchEvent(new PointerEvent("pointermove")), 10000)); // Le battute d'ingresso in piazza si saltano e la voce si ferma: il trailer parte in silenzio. await p.evaluate(async () => { const { settle } = await import('/tests/helpers.mjs'); settle(game); window.voices?.stop?.(); }); // Il puntatore vero lontano, nell'angolo. d.vm("DISPLAY=:0 ~/lab/video/mv.py 1599 899 0.2"); await sleep(1200); } export async function run(d) { const p = d.page; // L'orologio dell'audio, come nella partita completa: le voci che partono; la musica la decide il copione. await p.evaluate(() => { window.__t0 = Date.now(); window.__audio = []; const t = () => (Date.now() - window.__t0) / 1000; const path = (src) => decodeURI(new URL(src, location.href).pathname.slice(1)); let voice = null; window.__musica = (src) => window.__audio.push({ t: t(), music: src }); const look = () => { 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(); }); const musica = (src) => p.evaluate((s) => window.__musica(s), src); const nero = (on) => p.evaluate((on) => { document.getElementById("tr-nero").style.opacity = on ? 1 : 0; }, on); const scritta = (testo, basso = false) => p.evaluate(([t, b]) => { const e = document.getElementById("tr-scritta"); if (t) { e.textContent = t; e.classList.toggle("basso", b); } e.style.opacity = t ? 1 : 0; }, [testo, basso]); // In ogni scena: niente primo piano, niente battute in coda, flag di partenza; Andrea dove serve (o fuori campo). const vai = (scene, at, flags = {}) => `async () => { const { settle } = await import('/tests/helpers.mjs'); game.closeup = null; game.queue.length = 0; game.speech = null; game.dialogue = null; game.path = []; game.walking = false; game.task = null; Object.assign(game.state.flags, ${JSON.stringify(flags)}); game.go('${scene}'${at ? `, ${JSON.stringify(at)}` : ""}); settle(game); ${at ? "" : "game.state.x = -400;"} }`; const prepara = (codice) => p.evaluate(`(${codice})()`); const fuoco = async (codice, veloce) => { await p.evaluate((v) => { document.getElementById("tr-nero").style.transition = `opacity ${v ? .18 : .7}s`; }, veloce); await nero(true); await sleep(veloce ? 200 : 750); await prepara(codice); await sleep(veloce ? 150 : 300); await nero(false); await sleep(veloce ? 200 : 700); }; const dice = (who, testo, pose) => p.evaluate(([w, t, po]) => game.run(w ? { say: t, who: w, pose: po } : { say: t }), [who, testo, pose]); const finitoDiDire = () => p.waitForFunction(() => !game.speech, null, { timeout: 20000 }); // 1. Il nero, poi il panorama di Monte Cacioli con le nuvole. await musica("assets/audio/sera.mp3"); await d.cue("apertura"); await sleep(800); await p.evaluate(() => { document.getElementById("tr-scritta").style.whiteSpace = "pre-line"; }); await scritta("San Piero a Sieve\nmaggio 2007"); await sleep(2400); await scritta(null); await fuoco(vai("via_cafaggio")); await sleep(3200); // 2. La Fortezza al tramonto, le impronte. await d.cue("notte"); await fuoco(vai("fortezza", null, { indizio_regolo: true })); await scritta("La notte prima del Palio…", true); await sleep(3000); await scritta(null); // 3. La scuola: il palio non c'è più. await d.cue("sparito"); await fuoco(vai("scuola", [300, 330], { indizio_regolo: false })); await scritta("…il palio è sparito.", true); await sleep(3200); await scritta(null); // 4. Il paese, a stacchi: Andrea cammina. await d.cue("paese"); await musica("assets/audio/gara.mp3"); for (const [scene, da, a] of [["piazza", [120, 330], [300, 300]], ["pieve", [40, 330], [260, 300]], ["passeggiata_carza", [330, 352], [420, 250]], ["terrazza", [300, 330], [420, 320]], ["ponte_stazione", [300, 352], [200, 260]]]) { await fuoco(vai(scene, da), true); await p.evaluate((a) => game.walkTo(a), a); await sleep(2100); } // 5. Le sfide: un lampo per minigioco. await d.cue("sfide"); await scritta("Un paese pieno di sfide"); await sleep(1600); await scritta(null); const sfida = async (closeup, level, dopo = "") => { await fuoco(`async () => { const { settle } = await import('/tests/helpers.mjs'); game.closeup = null; game.queue.length = 0; game.speech = null; game.dialogue = null; game.path = []; game.walking = false; game.task = null; game.run({ closeup: '${closeup}', level: '${level}' }); settle(game); ${dopo} }`, true); await sleep(1300); }; await sfida("gara", "seria", "const g = await import('/src/engine/gara.js'); while (game.closeup.step === 'intro') g.press(game.closeup);"); await sfida("biliardo", "seria", "const b = await import('/src/engine/biliardo.js'); const c = game.closeup; b.strike(c, 'player', Math.atan2(c.balls.cpu.y - c.balls.player.y, c.balls.cpu.x - c.balls.player.x), 0.8);"); await sfida("rinquarto", "allenamento"); await sfida("micro", "seria", "const m = await import('/src/engine/micro.js'); const c = game.closeup; m.pick(c, 0); m.strike(c, 'player', c.angle, 0.55);"); await sfida("cerbottana", "seria", "const m = await import('/src/engine/cerbottana.js'); const c = game.closeup; c.aim = { x: 320, y: 118 }; c.power = m.BEST; m.shoot(c);"); await sfida("briscola", "seria"); await sfida("basket", "seria", "const b = await import('/src/engine/basket.js'); const c = game.closeup; c.power = 0.62; b.shoot(c);"); // 6. Le vecchie conoscenze, con le loro battute. await d.cue("amici"); await fuoco(vai("via_liguri_magelli", [330, 300])); await scritta("…e di vecchie conoscenze", true); await dice(null, "Di chi è quell'Audi lì?"); await finitoDiDire(); await scritta(null); await dice("arcu", "Let it be.", "explain"); await finitoDiDire(); await sleep(300); await dice(null, "Zio, hai problemi?"); await finitoDiDire(); await dice("zio_mario", "I problemi io gli avevo quando andavo a scuola!", "laugh"); await finitoDiDire(); await fuoco(vai("cardetole", [470, 330]), true); await dice("chiaru", "Vorrai dire un giochino!", "laugh"); await finitoDiDire(); await sleep(400); // 7. Il buio: la sera, la torcia nella fessura, gli occhi gialli. await d.cue("buio"); // Sotto il buio una musica bassa di suspense (Andrea: «un po' di musichetta nel finale»). await musica("assets/audio/sera.mp3"); await fuoco(vai("fortezza", [462, 310], { indizio_regolo: true, casetta_aperta: true })); await sleep(900); await p.evaluate(() => { game.state.inventory.push("torcia"); game.act("use", "fessura", "torcia"); }); // «Accendo la torcia e la punto dentro…» si salta: il primo piano si apre subito. // Il palio non si vede (Andrea: si capirebbe che è in Fortezza): la luce parte da destra, nel buio, e i primi due // ritrovamenti si danno per fatti; si cercano solo gli occhi. for (let k = 0; k < 60 && await p.evaluate(() => game.closeup?.id !== "fessura"); k++) { await p.evaluate(() => game.skip()); await sleep(50); } await p.evaluate(() => { const c = game.closeup; c.found = 2; c.beam = [600, 300]; }); await sleep(1600); const targets = await p.evaluate(() => game.story.closeups.fessura.targets.map((t) => t.at)); for (let i = 2; i < targets.length; i++) { if (i === 2) await musica("assets/audio/cartello.mp3"); // La luce va piano verso il punto, come una mano che cerca: prima in basso, poi su verso gli occhi. const from = await p.evaluate(() => game.closeup.beam); for (let k = 1; k <= 30; k++) { const x = from[0] + (targets[i][0] - from[0]) * k / 30 + Math.sin(k / 4) * 12, y = from[1] + (targets[i][1] - from[1]) * k / 30; await p.evaluate(([x, y]) => game.closeupAim([x, y]), [x, y]); await sleep(50); } // Le battute dei primi ritrovamenti si saltano (partono prima che il ritrovamento conti, e lo fermano); resta solo // l'ultima: «…sono due occhi gialli!». for (let k = 0; k < 300 && await p.evaluate((i) => !(game.closeup?.found > i), i); k++) { if (i < 2) await p.evaluate(() => game.skip()); await sleep(100); } await p.waitForFunction(() => !game.speech && !game.closeup?.lines?.length, null, { timeout: 30000 }); await sleep(i === 2 ? 1400 : 100); } await nero(true); await sleep(1400); // 8. Il titolo. await d.cue("titolo"); await musica("assets/audio/titolo.mp3"); await p.evaluate(() => { game.closeup = null; document.getElementById("title").hidden = false; document.getElementById("tr-nero").style.transition = "opacity 1.6s"; }); await sleep(300); await nero(false); await sleep(7500); await nero(true); await sleep(1600); const audio = await p.evaluate(() => window.__audio); const out = new URL("./out/", import.meta.url); mkdirSync(out, { recursive: true }); writeFileSync(new URL("trailer-audio.json", out), JSON.stringify({ eventi: audio }, null, 2)); }