diff --git a/video/SCALETTA.md b/video/SCALETTA.md index b1d1ed0..3cda878 100644 --- a/video/SCALETTA.md +++ b/video/SCALETTA.md @@ -74,6 +74,11 @@ Copioni `video/trailer.mjs` (circa 1 min 30 s) e `video/trailer-extended.mjs` (c del paese e i giochi giocati da soli). Niente clic: la scena è a tutto schermo, le scritte e gli stacchi al nero sono elementi aggiunti nella pagina, e la musica la sceglie il copione (`out/-audio.json`). Nel finale si cercano solo gli occhi gialli: il palio non si mostra (Andrea: si capirebbe dov'è). +La extended rifatta il 30 ottobre (circa 4 min 15 s), su richiesta di Andrea: non deve mai far capire che il Regolo è +in Fortezza. Quindi niente impronte e niente fessura: «la notte prima del Palio» è la piazza scurita con le finestre +accese, il finale ha due occhi gialli nel nero, e il titolo sta sopra il panorama, perché l'illustrazione del titolo ha +il Regolo fra le mura. In più ci sono Condor, campana, pesca, basket e pokerino: a ogni fotogramma gioca da solo +un giocatore automatico (`gioca(…)`). - Prova locale, senza VM: `OUT=/tmp/prova COPIONE=./trailer.mjs node video/prova-locale.mjs` (una schermata per battuta, e i tempi). diff --git a/video/trailer-extended.mjs b/video/trailer-extended.mjs index 01c222e..caad027 100644 --- a/video/trailer-extended.mjs +++ b/video/trailer-extended.mjs @@ -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 --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 = ""; }); // 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");