L'Ingorgo più da discoteca: fasci più forti e numerosi, colpo di colore a ogni battuta, strobo a raffica, macchie di luce sulla folla, laser anche in piazza, palla a specchi su tutta la scena; alla festa si clicca solo per spostarsi; il manifesto più facile da toccare col dito; il video breve dell'Ingorgo (Andrea)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9Quo9nEdXyjXYF8ma
This commit is contained in:
2026-10-30 22:59:54 +01:00
co-authored by Claude Opus 5.5
parent 9f2b045ab0
commit 40d5780255
7 changed files with 241 additions and 19 deletions
+165
View File
@@ -0,0 +1,165 @@
// Copione del video breve dell'Ingorgo Sonoro (Andrea, 30 ottobre: «45 secondi, che parta come... ci sono cose che non
// tornano..... forse..., e fai vedere il manifesto dell'Ingorgo e girare tra la musica»). Come i trailer
// (video/trailer.mjs): la scena a tutto schermo, scritte e stacchi al nero nella pagina, niente clic; la musica la
// sceglie il copione, una console per posto (out/ingorgo-sonoro-audio.json).
// Prova: OUT=/tmp/prova COPIONE=./ingorgo-sonoro.mjs node video/prova-locale.mjs
// Ripresa come la partita completa (video/SCALETTA.md), poi: python3 video/monta.py <cartella> --trailer .
import { mkdirSync, writeFileSync } from "node:fs";
const c = (id, it) => ({ id, it, en: it, top: true });
export const title = { it: "La Leggenda del Regolo · l'Ingorgo Sonoro", en: "La Leggenda del Regolo · l'Ingorgo Sonoro" };
export const cues = [
c("dubbio", "Ci sono cose che non tornano…"),
c("manifesto", "il manifesto"),
c("incanto", "l'incantesimo"),
c("festa", "la festa"),
c("titolo", "La Leggenda del Regolo"),
];
const URL_GIOCO = "http://127.0.0.1:8000/";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const H = "assets/audio/ingorgo-house.mp3", L = "assets/audio/ingorgo-latino.mp3", U = "assets/audio/ingorgo-underground.mp3",
T = "assets/audio/ingorgo-techno.mp3";
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])");
await p.click("#new-game");
await p.click('#rione [data-rione="campomigliaio"]');
await p.click("#skip-intro");
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; }
#title::before { background-image: url(assets/scenes/ingorgo-piazza.png) !important; }
#title::after { background: linear-gradient(rgba(20, 12, 30, .6), rgba(20, 12, 30, .1) 45%, rgba(20, 12, 30, .75)),
url(assets/scenes/ingorgo-piazza.png) center / contain no-repeat !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: 40px; line-height: 1.7; color: #ffe9b0; opacity: 0; white-space: pre-line;
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: 30px; }
` });
await p.evaluate(() => {
for (const id of ["tr-nero", "tr-scritta"]) { const e = document.createElement("div"); e.id = id; document.body.appendChild(e); }
});
await p.evaluate(() => setInterval(() => dispatchEvent(new PointerEvent("pointermove")), 10000));
await p.evaluate(async () => { const { settle } = await import('/tests/helpers.mjs'); settle(game); window.voices?.stop?.(); });
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 nei trailer: 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, s = 0.7) => p.evaluate(([on, s]) => { const e = document.getElementById("tr-nero"); e.style.transition = `opacity ${s}s`; e.style.opacity = on ? 1 : 0; }, [on, s]);
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]);
const vai = (scene, at, flags = {}) => p.evaluate(async ([scene, at, flags]) => {
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, flags);
game.go(scene, at); settle(game);
}, [scene, at, flags]);
const cammina = (a) => p.evaluate((a) => game.walkTo(a), a);
// 1. Il nero e il dubbio.
await musica("assets/audio/sera.mp3");
await d.cue("dubbio");
await sleep(500);
await scritta("Ci sono cose\nche non tornano…");
await sleep(2400);
await scritta(null);
await sleep(600);
await scritta("…forse…");
await sleep(1500);
await scritta(null);
await sleep(400);
// 2. La piazza di giorno: Andrea va al manifesto, che si apre ingrandito.
await d.cue("manifesto");
await musica("assets/audio/paese.mp3");
await vai("piazza", [380, 320]);
await nero(false);
await sleep(700);
await p.evaluate(() => game.act("look", "manifesto_ingorgo"));
await p.waitForFunction(() => game.closeup?.id === "manifesto_ingorgo" && game.closeup.t > 0.5, null, { timeout: 15000 });
await sleep(2000);
// Il tocco sul manifesto: la battuta sul manifesto si salta, resta il desiderio, a voce.
await p.evaluate(() => game.closeupTap());
for (let k = 0; k < 200 && !(await p.evaluate(() => game.speech?.text.startsWith("Quanto darei"))); k++) {
await p.evaluate(() => { if (game.speech) game.skip(); });
await sleep(40);
}
// 3. L'incantesimo: nel lampo parte la house della piazza.
await d.cue("incanto");
await p.waitForFunction(() => game.closeup?.id === "ingorgo", null, { timeout: 20000 });
await p.waitForFunction(() => game.closeup?.swapped, null, { timeout: 10000 });
await musica(H);
// 4. Il giro della festa: Andrea si accorge della festa camminando fra la gente (la seconda battuta si salta), poi una
// console per posto, a stacchi veloci.
await p.waitForFunction(() => !game.closeup && game.speech?.text.includes("Ingorgo"), null, { timeout: 10000 });
await d.cue("festa");
await p.evaluate(() => { game.queue.length = 0; game.walkTo([200, 300]); });
await p.waitForFunction(() => !game.speech?.text.includes("Ingorgo"), null, { timeout: 20000 });
await p.evaluate(() => { game.queue.length = 0; game.speech = null; });
await sleep(300);
for (const [scene, da, a, brano] of [["via_antica_posta", [330, 345], [300, 290], H], ["piazzetta_comune", [260, 350], [300, 300], L],
["antica_posta_alta", [240, 350], [380, 290], U], ["piazzetta", [330, 352], [420, 290], T]]) {
await nero(true, 0.2); await sleep(220);
await musica(brano);
await vai(scene, da);
await nero(false, 0.2);
await cammina(a);
await sleep(3200);
}
await sleep(300);
// 5. Il titolo sopra la piazza della festa.
await d.cue("titolo");
await nero(true, 0.9); await sleep(1000);
await p.evaluate(() => { game.closeup = null; document.getElementById("title").hidden = false; });
await nero(false, 1.2);
await sleep(1200);
await scritta("l'Ingorgo Sonoro", true);
await sleep(3000);
await scritta(null);
await nero(true, 1.4);
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("ingorgo-sonoro-audio.json", out), JSON.stringify({ eventi: audio }, null, 2));
}
+1 -1
View File
@@ -12,7 +12,7 @@ try {
page.goto = url => orig(url.replace('http://127.0.0.1:8000/', `http://127.0.0.1:${server.port}/`));
const mod = await import(process.env.COPIONE ?? './trailer.mjs');
const t0 = Date.now(); let n = 0;
const d = { page, vm: () => {}, cue: async id => { console.log(((Date.now() - t0) / 1000).toFixed(1), id); setTimeout(() => page.screenshot({ path: `${out}/tr-${String(++n).padStart(2, '0')}-${id}.png` }), id === 'buio' ? 5000 : 3000); } };
const d = { page, vm: () => {}, cue: async id => { console.log(((Date.now() - t0) / 1000).toFixed(1), id); setTimeout(() => page.screenshot({ path: `${out}/tr-${String(++n).padStart(2, '0')}-${id}.png` }), id === 'buio' ? 5000 : Number(process.env.SCATTO ?? 3000)); } };
await mod.setup(d);
await mod.run(d);
console.log('fine', ((Date.now() - t0) / 1000).toFixed(1));