Il trailer extended rifatto senza dire dove sta il Regolo: la notte prima del Palio è la piazza scurita, nel finale solo due occhi gialli nel buio, il titolo sopra il panorama; in più Condor, campana, pesca, basket e pokerino giocati da soli (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:17:21 +01:00
co-authored by Claude Opus 5.5
parent 9f0f60d186
commit 9cd8027256
2 changed files with 89 additions and 53 deletions
+84 -53
View File
@@ -1,6 +1,8 @@
// Copione del trailer lungo «La Leggenda del Regolo · extended» (scaletta approvata da Andrea il 29 ottobre): circa tre
// minuti, come il trailer (video/trailer.mjs) ma col giro completo del paese e i giochi giocati per qualche secondo:
// bocce, biliardo, GP di Fortezza, briscola, macchinine, cerbottana.
// Copione del trailer lungo «La Leggenda del Regolo · extended» (scaletta approvata da Andrea il 29 ottobre): circa
// quattro minuti, come il trailer (video/trailer.mjs) ma col giro completo del paese e i giochi giocati per qualche
// secondo: bocce, biliardo, GP di Fortezza, briscola, macchinine, cerbottana, Condor, campana, pesca, basket, pokerino.
// Rifatto il 30 ottobre (Andrea): niente che dica che il Regolo è in Fortezza. La notte prima del Palio è la piazza
// con le finestre accese, e nel finale ci sono solo due occhi gialli nel buio, senza muro e senza fessura.
// Si gira come la partita completa (video/SCALETTA.md), poi: python3 video/monta.py <cartella> --trailer .
import { mkdirSync, writeFileSync } from "node:fs";
@@ -18,8 +20,13 @@ export const cues = [
c("carte", "La briscola all'oratorio"),
c("macchinine", "Le macchinine col gesso"),
c("cerbottana", "La cerbottana"),
c("condor", "La Condor col Taglia"),
c("campana", "La campana"),
c("pesca", "La pesca al lago"),
c("basket", "Fai due tiri"),
c("poker", "Il pokerino del Barrettino"),
c("amici", "…e di vecchie conoscenze"),
c("buio", "la fessura"),
c("buio", "due occhi gialli"),
c("titolo", "La Leggenda del Regolo"),
];
@@ -52,9 +59,19 @@ export async function setup(d) {
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; }
#scene.notte { filter: brightness(.5) saturate(.85) hue-rotate(-8deg); }
/* Il titolo sopra il panorama: l'illustrazione del gioco ha il Regolo fra le mura della Fortezza. */
#title::before { background-image: url(assets/scenes/panorama-intro.png) !important; }
#title::after { background: linear-gradient(rgba(20, 12, 30, .55), rgba(20, 12, 30, .05) 45%, rgba(20, 12, 30, .7)),
url(assets/scenes/panorama-intro.png) center / contain no-repeat !important; }
#tr-occhi { position: fixed; left: 50%; top: 44%; z-index: 51; display: flex; gap: 46px; transform: translateX(-50%);
opacity: 0; transition: opacity 1.4s; pointer-events: none; }
#tr-occhi i { width: 22px; height: 22px; background: #ffe14a; box-shadow: 0 0 18px 6px rgba(255, 210, 40, .55); }
#tr-occhi.chiusi i { height: 3px; margin-top: 10px; }
` });
await p.evaluate(() => {
for (const id of ["tr-nero", "tr-scritta"]) { const e = document.createElement("div"); e.id = id; document.body.appendChild(e); }
for (const id of ["tr-nero", "tr-scritta", "tr-occhi"]) { const e = document.createElement("div"); e.id = id; document.body.appendChild(e); }
document.getElementById("tr-occhi").innerHTML = "<i></i><i></i>";
});
// 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));
@@ -122,16 +139,16 @@ export async function run(d) {
await fuoco(vai("via_cafaggio"));
await sleep(3200);
// 2. La Fortezza al tramonto, le impronte.
// 2. La piazza di sera, con le finestre accese (niente Fortezza e niente impronte: si capirebbe dov'è il Regolo).
await d.cue("notte");
await fuoco(vai("fortezza", null, { indizio_regolo: true }));
await fuoco(`async () => { await (${vai("piazza", null, { casetta_aperta: true })})(); document.getElementById("scene").classList.add("notte"); }`);
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 fuoco(`async () => { await (${vai("scuola", [300, 330], { casetta_aperta: false })})(); document.getElementById("scene").classList.remove("notte"); }`);
await scritta("…il palio è sparito.", true);
await sleep(3200);
await scritta(null);
@@ -145,7 +162,7 @@ export async function run(d) {
["orti_zio_mario", [400, 352], [330, 260]], ["stazione", [320, 350], [450, 290]], ["cardetole", [300, 350], [470, 300]], ["terrazza", [300, 330], [420, 320]]]) {
await fuoco(vai(scene, da), true);
await p.evaluate((a) => game.walkTo(a), a);
await sleep(3300);
await sleep(2800);
}
// 5. Le sfide, giocate davvero per qualche secondo: ogni gioco col suo nome in basso e la sua musica.
@@ -155,7 +172,8 @@ export async function run(d) {
await scritta("Un paese pieno di sfide");
await sleep(2000);
await scritta(null);
// Apre il primo piano, poi ogni 150 ms chiama `mossa` (il giocatore che gioca da solo) finché passano i secondi.
// Apre il primo piano, poi a ogni fotogramma chiama `mossa` (il giocatore che gioca da solo) finché passano i
// secondi. Dentro `mossa`, `pronto(ms)` dice se dall'ultima mossa è passato abbastanza: così non gioca a raffica.
const gioca = async (cue, nome, brano, closeup, level, modulo, prima, mossa, secondi) => {
await d.cue(cue);
await musica(brano);
@@ -165,27 +183,51 @@ export async function run(d) {
game.run({ closeup: '${closeup}', level: '${level}' }); settle(game);
window.__m = await import('${modulo}');
const M = window.__m, c = game.closeup; ${prima}
const mossa = new Function('M', 'c', 'pronto', ${JSON.stringify(mossa)});
let ultima = performance.now();
const pronto = (ms) => performance.now() - ultima > ms && (ultima = performance.now(), true);
const giro = () => { if (game.closeup !== c) return; mossa(M, c, pronto); requestAnimationFrame(giro); };
giro();
}`);
await scritta(nome, true);
const fine = Date.now() + secondi * 1000;
while (Date.now() < fine) {
await p.evaluate(`(() => { const M = window.__m, c = game.closeup; if (!c) return; ${mossa} })()`);
if (Date.now() > fine - (secondi - 3) * 1000) await scritta(null);
await sleep(150);
}
await sleep(3000);
await scritta(null);
await sleep((secondi - 3) * 1000);
};
await gioca("bocce", "Le bocce al pallaio", "assets/audio/bocce.mp3", "bocce", "seria", "/src/engine/bocce.js", "",
"if (M.canThrow(c)) { c.aim = { u: c.jack.u, w: c.jack.w }; c.power = M.powerFor(Math.hypot(c.jack.u, c.jack.w) - 1.5); M.shoot(c); }", 14);
"if (M.canThrow(c) && pronto(500)) { c.aim = { u: c.jack.u, w: c.jack.w }; c.power = M.powerFor(Math.hypot(c.jack.u, c.jack.w) - 1.5); M.shoot(c); }", 11);
await gioca("biliardo", "Il biliardo del Circolo", "assets/audio/biliardo.mp3", "biliardo", "seria", "/src/engine/biliardo.js", "",
"if (c.step === 'aim' && c.current === 'player' && !c.holding) M.strike(c, 'player', Math.atan2(c.balls.cpu.y - c.balls.player.y, c.balls.cpu.x - c.balls.player.x) + 0.04, 0.75);", 14);
"if (c.step === 'aim' && c.current === 'player' && !c.holding && pronto(400)) M.strike(c, 'player', Math.atan2(c.balls.cpu.y - c.balls.player.y, c.balls.cpu.x - c.balls.player.x) + 0.04, 0.75);", 11);
await gioca("gara", "Il GP di Fortezza", "assets/audio/gara.mp3", "gara", "bona", "/src/engine/gara.js",
"while (c.step === 'intro') M.press(c); M.release(c);", "c.holding = c.step === 'race' && !M.mustLift(c);", 16);
"while (c.step === 'intro') M.press(c); M.release(c);", "c.holding = c.step === 'race' && !M.mustLift(c);", 13);
await gioca("carte", "La briscola all'oratorio", "assets/audio/carte.mp3", "briscola", "seria", "/src/engine/briscola.js", "",
"if (M.canPlay(c)) M.play(c, 0);", 12);
"if (M.canPlay(c) && pronto(700)) M.play(c, 0);", 10);
await gioca("macchinine", "Le macchinine col gesso", "assets/audio/macchinine.mp3", "micro", "bona", "/src/engine/micro.js", "M.pick(c, 0);",
"if (M.canThrow(c)) { const car = c.cars.player, i = (car.idx + Math.round(110 / c.track.step)) % c.track.n, [x, y] = c.track.pts[i]; M.strike(c, 'player', Math.atan2(y - car.y, x - car.x), M.powerFor(Math.hypot(x - car.x, y - car.y))); }", 12);
"if (M.canThrow(c) && pronto(400)) { const car = c.cars.player, i = (car.idx + Math.round(110 / c.track.step)) % c.track.n, [x, y] = c.track.pts[i]; M.strike(c, 'player', Math.atan2(y - car.y, x - car.x), M.powerFor(Math.hypot(x - car.x, y - car.y))); }", 10);
await gioca("cerbottana", "La cerbottana", "assets/audio/gara.mp3", "cerbottana", "seria", "/src/engine/cerbottana.js", "",
"if (M.canThrow(c)) { const k = c.cans.find(k => !k.down); c.aim = k && c.left % 2 ? { x: k.x, y: M.LEDGE - 8 } : { x: 320 + (c.left % 3 - 1) * 6, y: 118 }; c.power = M.BEST; M.shoot(c); }", 10);
"if (M.canThrow(c) && pronto(400)) { const k = c.cans.find(k => !k.down); c.aim = k && c.left % 2 ? { x: k.x, y: M.LEDGE - 8 } : { x: 320 + (c.left % 3 - 1) * 6, y: 118 }; c.power = M.BEST; M.shoot(c); }", 9);
// La Condor: le lattine sulla panchina, le bottiglie, il palloncino; il pallino cala, si mira un po' sopra.
await gioca("condor", "La Condor col Taglia", "assets/audio/gara.mp3", "condor", "bona", "/src/engine/condor.js", "",
"if (!M.canThrow(c) || c.holding || !pronto(800)) return; const su = x => M.DROP * M.far(x) ** 2; const k = c.cans.find(k => !k.down), b = c.bottles.find(b => !b.down);" +
" if (k) c.aim = { x: k.x, y: M.BENCH - M.CAN.h / 2 - su(k.x) }; else if (b) c.aim = { x: b.x, y: M.BAR - M.BOTTLE.h / 2 - su(b.x) };" +
" else if (!c.balloon.popped) { const p = M.balloon(c); c.aim = { x: p.x, y: p.y - su(p.x) }; } else c.aim = { x: M.CARTONE.x, y: M.CARTONE.y - su(M.CARTONE.x) }; M.shoot(c);", 10);
// La campana: il sassolino nella casella giusta, poi i salti quando la lancetta passa nel verde; poi tocca al Taglia.
await gioca("campana", "La campana", "assets/audio/macchinine.mp3", "campana", "bona", "/src/engine/campana.js", "",
"if (c.step === 'aim' && c.current === 'player' && pronto(700)) { const p = M.center(c.target.player); c.skill.scatter = 0; c.aim = { x: p.x, y: p.y }; c.power = Math.hypot(p.x - M.FROM.x, p.y - M.FROM.y) / M.REACH; M.shoot(c); }" +
" else if (c.step === 'hop' && (!c.hop || c.hop.t >= M.HOP) && c.meter?.t > 0 && Math.abs(M.needle(c)) < 0.08) M.press(c, 1);", 15);
// La pesca: il galleggiante abbocca presto (nel trailer non si aspetta), si ferra e si recupera senza strappare.
await gioca("pesca", "La pesca al lago", "assets/audio/paese.mp3", "pesca", "bona", "/src/engine/pesca.js", "",
"const f = c.float; if (c.step === 'aim' && pronto(800)) { c.power = 0.45; M.shoot(c); }" +
" else if (c.step === 'wait' && f) { if (!f.svelto && f.biteAt) { const prima = Math.max(0, f.biteAt - f.t - 1.6); f.biteAt -= prima; f.nibbles.forEach(n => n.at -= prima); f.svelto = true; }" +
" if (M.biting(c)) { M.strike(c); c.reeling = c.step === 'fight'; } } else if (c.step === 'fight') c.reeling = c.tension < 0.8;", 15);
// Fai due tiri: per ogni postazione si cerca prima, su una copia, un tiro che entra.
await gioca("basket", "Fai due tiri", "assets/audio/gara.mp3", "basket", "bona", "/src/engine/basket.js", "",
"if (!M.canThrow(c) || c.holding || !pronto(600)) return; let best = { angle: 55, power: 0.7 };" +
" cerca: for (let angle = 46; angle <= 66; angle += 4) for (let power = 0.3; power <= 1; power += 0.02) { const p = structuredClone(c); M.setAngle(p, angle); M.setPower(p, power); M.shoot(p);" +
" for (let i = 0; i < 2000 && p.step === 'flight'; i++) M.update(p, 1 / 120); if (p.ball.scored) { best = { angle, power }; break cerca; } }" +
" M.setAngle(c, best.angle); M.setPower(c, best.power); M.shoot(c);", 11);
await gioca("poker", "Il pokerino del Barrettino", "assets/audio/carte.mp3", "poker", "seria", "/src/engine/poker.js", "",
"if (M.canDraw(c) && pronto(900)) M.choose(c, 'cambia'); else if (M.myTurn(c) && pronto(900)) M.choose(c, 'vedo'); else if (c.step === 'show' && pronto(2500)) M.advance(c);", 11);
await musica("assets/audio/paese.mp3");
// 6. Le vecchie conoscenze, con le loro battute.
@@ -207,40 +249,29 @@ export async function run(d) {
await finitoDiDire();
await sleep(400);
// 7. Il buio: la sera, la torcia nella fessura, gli occhi gialli.
// 7. Il buio: solo due occhi gialli che si aprono, guardano e sbattono (Andrea, 30 ottobre: niente Fortezza, niente
// fessura e niente palio, sennò si capisce dov'è il Regolo).
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 p.evaluate(() => { document.getElementById("tr-nero").style.transition = "opacity .9s"; });
await nero(true);
await sleep(1400);
await prepara(`async () => { game.closeup = null; game.queue.length = 0; game.speech = null; game.dialogue = null; game.state.x = -400; }`);
await sleep(1800);
await musica("assets/audio/cartello.mp3");
const occhi = (fn) => p.evaluate(fn);
await occhi(() => { document.getElementById("tr-occhi").style.opacity = 1; });
await sleep(2200);
for (const pausa of [140, 2200, 110, 140, 110]) {
await occhi(() => document.getElementById("tr-occhi").classList.toggle("chiusi"));
await sleep(pausa);
}
await dice(null, "E quelli... sono due occhi gialli!");
await finitoDiDire();
await sleep(900);
await occhi(() => { document.getElementById("tr-occhi").classList.add("chiusi"); });
await sleep(150);
await occhi(() => { document.getElementById("tr-occhi").style.opacity = 0; });
await sleep(1500);
// 8. Il titolo.
await d.cue("titolo");