From 40d57802552846eb7bebf8718afa096a510bdec0 Mon Sep 17 00:00:00 2001 From: Andrea Manzi Date: Fri, 30 Oct 2026 22:59:54 +0100 Subject: [PATCH] =?UTF-8?q?L'Ingorgo=20pi=C3=B9=20da=20discoteca:=20fasci?= =?UTF-8?q?=20pi=C3=B9=20forti=20e=20numerosi,=20colpo=20di=20colore=20a?= =?UTF-8?q?=20ogni=20battuta,=20strobo=20a=20raffica,=20macchie=20di=20luc?= =?UTF-8?q?e=20sulla=20folla,=20laser=20anche=20in=20piazza,=20palla=20a?= =?UTF-8?q?=20specchi=20su=20tutta=20la=20scena;=20alla=20festa=20si=20cli?= =?UTF-8?q?cca=20solo=20per=20spostarsi;=20il=20manifesto=20pi=C3=B9=20fac?= =?UTF-8?q?ile=20da=20toccare=20col=20dito;=20il=20video=20breve=20dell'In?= =?UTF-8?q?gorgo=20(Andrea)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GiECd9Quo9nEdXyjXYF8ma --- docs/STATO.md | 4 +- src/engine/game.js | 8 +- src/engine/render.js | 47 ++++++++--- src/story/story.js | 13 +-- tests/ingorgo.test.mjs | 21 +++++ video/ingorgo-sonoro.mjs | 165 +++++++++++++++++++++++++++++++++++++++ video/prova-locale.mjs | 2 +- 7 files changed, 241 insertions(+), 19 deletions(-) create mode 100644 video/ingorgo-sonoro.mjs diff --git a/docs/STATO.md b/docs/STATO.md index 12e82d7..188e6e5 100644 --- a/docs/STATO.md +++ b/docs/STATO.md @@ -24,7 +24,9 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto, luce, cassa che accelera, lampo bianco, coriandoli) e il centro diventa la notte della festa (flag `ingorgo`, fase `ingorgo`, sfondi di notte di Codex in `backdrops`). La zona (`story.zones`): Piazza, Via dell'Antica Posta, Piazzetta del Comune, cima di Via dell'Antica Posta, Piazzetta Cambray Digny; uscendo (a piedi o con la mappa) si torna a maggio. - Le luci da palco animate solo alla festa (`stagelights`, `lasers`, `mirrorball` in render.js): fasci che spazzano, bagliore a + Alla festa si clicca solo per spostarsi (zona con `exitsOnly`: panchine, porte e statue non rispondono). + Le luci da discoteca animate solo alla festa (`stagelights`, `lasers`, `mirrorball`, `floorspots` in render.js), rinforzate + su richiesta di Andrea: colpo di colore a ogni battuta, strobo a raffica a fine frase, macchie che corrono sulla folla. Fasci che spazzano, bagliore a tempo, strobo, laser alla tecno, palla a specchi all'underground. Le console (`stages` delle scene, `music.js` `mix`): più brani insieme, ognuno più forte vicino alla sua console, anche da fuori quadro (la piazza accanto). Generi provvisori: house in Piazza, latino al Comune, underground in cima a Via dell'Antica Posta, tecno a Cambray Digny; i brani sono di Andrea (Suno), portati al volume del gioco. Gli sfondi sono diff --git a/src/engine/game.js b/src/engine/game.js index b8a7b25..6223891 100644 --- a/src/engine/game.js +++ b/src/engine/game.js @@ -203,7 +203,13 @@ export class Game { return { scene, x: Math.round(x), y: Math.round(y), facing, inventory: [...inventory], flags: { ...flags }, used: { ...used }, ended }; } - hotspots() { return (this.scene.hotspots ?? []).filter(h => check(h.when, this.state)); } + // In una zona accesa con `exitsOnly` (la festa dell'Ingorgo: Andrea, 30 ottobre) si clicca solo per andare da un + // posto all'altro: panchine, porte e statue restano dove sono, ma non rispondono. + hotspots() { + const list = (this.scene.hotspots ?? []).filter(h => check(h.when, this.state)); + const busy = (this.story.zones ?? []).some(z => z.exitsOnly && this.state.flags[z.flag] && z.scenes.includes(this.state.scene)); + return busy ? list.filter(h => h.exit) : list; + } hotspot(id) { return this.hotspots().find(h => h.id === id); } people() { return (this.scene.people ?? []).filter(p => check(p.when, this.state)); } // Col mouse vale il punto preciso (l'ultimo della lista sta sopra). Col dito, `reach` allarga l'area: prima gli diff --git a/src/engine/render.js b/src/engine/render.js index 92e2d26..8e790a7 100644 --- a/src/engine/render.js +++ b/src/engine/render.js @@ -497,18 +497,18 @@ export const EFFECTS = { if (e.wash) { const [x, y, r] = e.wash, c = colors[bar % colors.length]; const g = ctx.createRadialGradient(x, y, 0, x, y, r); - g.addColorStop(0, `rgba(${c},${0.1 + 0.18 * hit ** 2})`); + g.addColorStop(0, `rgba(${c},${0.16 + 0.3 * hit ** 2})`); g.addColorStop(1, `rgba(${c},0)`); ctx.fillStyle = g; ctx.fillRect(x - r, y - r, r * 2, r * 2); } for (const [k, [x, y]] of (e.from ?? []).entries()) { - for (let i = 0; i < (e.beams ?? 2); i++) { - const n = k * 3 + i, swing = Math.sin(time / (900 + n * 170) + n * 1.9) * (e.spread ?? 0.75); - const a = -Math.PI / 2 + swing + (k % 2 ? 0.25 : -0.25), len = e.len ?? 260, half = 0.045; + for (let i = 0; i < (e.beams ?? 3); i++) { + const n = k * 3 + i, swing = Math.sin(time / (700 + n * 150) + n * 1.9) * (e.spread ?? 0.85); + const a = -Math.PI / 2 + swing + (k % 2 ? 0.25 : -0.25), len = e.len ?? 340, half = 0.055; const c = colors[(n + bar) % colors.length], tipX = x + Math.cos(a) * len, tipY = y + Math.sin(a) * len; const g = ctx.createLinearGradient(x, y, tipX, tipY); - g.addColorStop(0, `rgba(${c},0.32)`); + g.addColorStop(0, `rgba(${c},${0.38 + 0.2 * hit})`); g.addColorStop(1, `rgba(${c},0)`); ctx.fillStyle = g; ctx.beginPath(); @@ -521,19 +521,46 @@ export const EFFECTS = { ctx.fillRect(Math.round(x) - 1, Math.round(y) - 1, 3, 3); } } - ctx.restore(); - // Lo strobo: un lampo bianco brevissimo ogni `strobe` battute. - if (e.strobe && Math.floor(beat) % e.strobe === 0 && beat % 1 < 0.12) { - ctx.fillStyle = 'rgba(255,255,255,0.13)'; + // Il colpo di colore su tutta la scena a ogni battuta (`pulse`: quanto forte). + if (e.pulse) { + ctx.fillStyle = `rgba(${colors[Math.floor(beat) % colors.length]},${e.pulse * hit ** 3})`; ctx.fillRect(0, 0, WIDTH, HEIGHT); } + ctx.restore(); + // Lo strobo: un lampo bianco brevissimo ogni `strobe` battute, a raffica nell'ultima battuta di ogni frase. + const roll = Math.floor(beat) % 16 === 15 && (beat * 4) % 1 < 0.3; + if (e.strobe && ((Math.floor(beat) % e.strobe === 0 && beat % 1 < 0.12) || roll)) { + ctx.fillStyle = 'rgba(255,255,255,0.18)'; + ctx.fillRect(0, 0, WIDTH, HEIGHT); + } + }, + // Le macchie di luce che corrono sulla folla (`area`), colorate, a tempo: la pista da ballo. + floorspots(ctx, e, time) { + const [ax, ay, aw, ah] = e.area, colors = e.colors ?? ['255,60,200', '60,220,255', '120,255,90', '255,190,40', '170,90,255']; + const beat = time / (60000 / (e.bpm ?? 124)), hit = 1 - (beat % 1); + ctx.save(); + ctx.globalCompositeOperation = 'lighter'; + for (let i = 0; i < (e.count ?? 6); i++) { + const u = 0.5 + 0.5 * Math.sin(time / (1300 + i * 230) + i * 2.1), v = 0.5 + 0.5 * Math.sin(time / (1700 + i * 190) + i * 1.3); + const x = ax + u * aw, y = ay + v * ah, r = (e.size ?? 34) * (0.7 + 0.5 * v); + const c = colors[(i + Math.floor(beat / 2)) % colors.length]; + const g = ctx.createRadialGradient(x, y, 0, x, y, r); + g.addColorStop(0, `rgba(${c},${0.22 + 0.2 * hit})`); + g.addColorStop(1, `rgba(${c},0)`); + ctx.fillStyle = g; + ctx.save(); + ctx.translate(x, y); ctx.scale(1, 0.45); ctx.translate(-x, -y); + ctx.fillRect(x - r, y - r, r * 2, r * 2); + ctx.restore(); + } + ctx.restore(); }, // I laser della tecno: righe sottili verdi (o del colore dato) che si aprono a ventaglio e si richiudono. lasers(ctx, e, time) { const [x, y] = e.at, n = e.count ?? 7, open = 0.35 + 0.35 * Math.sin(time / 1300), tilt = Math.sin(time / 2100) * 0.4; ctx.save(); ctx.globalCompositeOperation = 'lighter'; - ctx.strokeStyle = `rgba(${e.color ?? '90,255,110'},${0.35 + 0.2 * Math.sin(time / 90) ** 2})`; + ctx.strokeStyle = `rgba(${e.color ?? '90,255,110'},${0.45 + 0.25 * Math.sin(time / 90) ** 2})`; ctx.lineWidth = 1; ctx.beginPath(); for (let i = 0; i < n; i++) { diff --git a/src/story/story.js b/src/story/story.js index 42af8e0..ec7b1b4 100644 --- a/src/story/story.js +++ b/src/story/story.js @@ -82,6 +82,7 @@ export const story = { zones: [ { flag: 'ingorgo', scenes: ['piazza', 'via_antica_posta', 'piazzetta_comune', 'antica_posta_alta', 'piazzetta'], + exitsOnly: true, // alla festa si clicca solo per spostarsi (Andrea) leave: [ { say: 'Mi stropiccio gli occhi. La musica non c\'è più: è di nuovo maggio.' }, { say: 'E il palio è sempre sparito. Però che serata.' }, @@ -588,7 +589,7 @@ export const story = { { when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [195, 235] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [-160, 250], far: 600 }, ], - effects: [{ type: 'stagelights', phase: 'ingorgo', from: [[116, 146], [276, 146]], wash: [200, 215, 150], bpm: 124, beams: 2, strobe: 8 }, { type: 'clouds', area: [160, 0, 440, 80], count: 3, big: true, phase: ['pomeriggio', 'tramonto', 'sera'] }, { type: 'windows', phase: 'sera', rects: [[16, 37, 8, 18], [48, 42, 8, 18], [50, 110, 8, 16], [97, 77, 8, 12], [122, 80, 8, 12], [170, 90, 8, 12], [204, 103, 8, 12], [239, 111, 8, 12], [268, 117, 7, 14], [287, 125, 6, 14], [97, 130, 9, 14], [127, 133, 9, 14], [170, 142, 9, 14], [204, 150, 8, 12], [239, 155, 8, 12], [268, 160, 7, 14], [287, 163, 6, 14], [386, 163, 14, 16], [424, 160, 14, 16], [472, 157, 14, 16], [524, 155, 14, 16], [390, 212, 14, 16], [526, 208, 14, 16]] }, { type: 'birds', phase: ['pomeriggio', 'tramonto'], area: [150, 20, 460, 80] }], + effects: [{ type: 'stagelights', phase: 'ingorgo', from: [[116, 146], [276, 146], [196, 140]], wash: [200, 215, 170], bpm: 124, beams: 3, strobe: 4, pulse: 0.08 }, { type: 'lasers', phase: 'ingorgo', at: [196, 150], count: 7, color: '255,60,200', len: 360 }, { type: 'floorspots', phase: 'ingorgo', area: [40, 240, 560, 110], bpm: 124, count: 7 }, { type: 'clouds', area: [160, 0, 440, 80], count: 3, big: true, phase: ['pomeriggio', 'tramonto', 'sera'] }, { type: 'windows', phase: 'sera', rects: [[16, 37, 8, 18], [48, 42, 8, 18], [50, 110, 8, 16], [97, 77, 8, 12], [122, 80, 8, 12], [170, 90, 8, 12], [204, 103, 8, 12], [239, 111, 8, 12], [268, 117, 7, 14], [287, 125, 6, 14], [97, 130, 9, 14], [127, 133, 9, 14], [170, 142, 9, 14], [204, 150, 8, 12], [239, 155, 8, 12], [268, 160, 7, 14], [287, 163, 6, 14], [386, 163, 14, 16], [424, 160, 14, 16], [472, 157, 14, 16], [524, 155, 14, 16], [390, 212, 14, 16], [526, 208, 14, 16]] }, { type: 'birds', phase: ['pomeriggio', 'tramonto'], area: [150, 20, 460, 80] }], walk: [[84, 244], [330, 238], [345, 230], [362, 238], [462, 246], [466, 276], [544, 276], [548, 248], [588, 250], [600, 290], [640, 300], [640, 360], [0, 360], [0, 330], [84, 318]], depth: [[236, 0.5], [360, 1.35]], entry: [345, 240], @@ -672,7 +673,7 @@ export const story = { { // Il manifesto dell'Ingorgo Sonoro dell'estate prima, mezzo strappato: guardandolo, Andrea esprime il // desiderio, e la festa arriva (Andrea, 30 ottobre). [DA ANDREA: le battute sono provvisorie] - id: 'manifesto_ingorgo', name: 'vecchio manifesto dell\'Ingorgo Sonoro', box: [226, 198, 16, 22], at: [234, 250], face: 'up', when: '!ingorgo', + id: 'manifesto_ingorgo', name: 'vecchio manifesto dell\'Ingorgo Sonoro', box: [223, 195, 22, 26], at: [234, 250], face: 'up', when: '!ingorgo', // Come quello vero (assets/raw/riferimenti/manifesto-ingorgo-2014.png): blu, il cartello bianco e rosso col DJ. art: [{ rect: [226, 198, 16, 22], fill: '#1c2050' }, { rect: [233, 201, 2, 2], fill: '#d8262e' }, { rect: [231, 203, 6, 2], fill: '#d8262e' }, { rect: [229, 205, 10, 4], fill: '#d8262e' }, { rect: [232, 205, 4, 3], fill: '#f4f1e8' }, { rect: [228, 211, 12, 2], fill: '#f4f1e8' }, @@ -1625,7 +1626,7 @@ export const story = { // Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza] // La tecno davanti alla casetta bianca; l'underground arriva da giù, da Via dell'Antica Posta. // Le luci da palco della festa (render.js: stagelights, lasers, mirrorball). - effects: [{ type: 'lasers', phase: 'ingorgo', at: [400, 198], count: 9 }, { type: 'stagelights', phase: 'ingorgo', from: [[356, 192], [460, 192]], wash: [408, 225, 150], bpm: 132, beams: 2, strobe: 4, colors: ['90,255,110', '255,255,255', '60,220,255'] }], + effects: [{ type: 'lasers', phase: 'ingorgo', at: [400, 198], count: 13, len: 460 }, { type: 'lasers', phase: 'ingorgo', at: [370, 200], count: 7, color: '60,220,255' }, { type: 'stagelights', phase: 'ingorgo', from: [[356, 192], [460, 192], [408, 186]], wash: [408, 225, 180], bpm: 132, beams: 3, strobe: 2, pulse: 0.1, colors: ['90,255,110', '255,255,255', '60,220,255'] }, { type: 'floorspots', phase: 'ingorgo', area: [40, 250, 560, 100], bpm: 132, count: 8, colors: ['90,255,110', '255,255,255', '60,220,255'] }], stages: [ { when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [495, 245] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [700, 300], far: 650 }, @@ -1671,7 +1672,7 @@ export const story = { // Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza] // Il bar della birra sui gradini; il latino dal Comune a sinistra, la house giù dalla piazza, l'underground su in cima. // Le luci da palco della festa (render.js: stagelights, lasers, mirrorball). - effects: [{ type: 'stagelights', phase: 'ingorgo', wash: [90, 190, 110], bpm: 98, colors: ['255,170,60', '255,90,160'] }, { type: 'stagelights', phase: 'ingorgo', from: [[430, 205]], beams: 1, len: 160, bpm: 98 }], + effects: [{ type: 'stagelights', phase: 'ingorgo', wash: [90, 190, 130], bpm: 98, colors: ['255,170,60', '255,90,160'], pulse: 0.05 }, { type: 'stagelights', phase: 'ingorgo', from: [[430, 205], [130, 150]], beams: 2, len: 260, bpm: 98 }, { type: 'floorspots', phase: 'ingorgo', area: [40, 230, 560, 120], bpm: 98, count: 5 }], stages: [ { when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [-110, 250] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [320, 560] }, @@ -1720,7 +1721,7 @@ export const story = { // Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza] // La console del latino davanti al Comune; la house arriva da giù, dalla piazza. // Le luci da palco della festa (render.js: stagelights, lasers, mirrorball). - effects: [{ type: 'stagelights', phase: 'ingorgo', from: [[232, 212], [332, 212]], wash: [282, 228, 140], bpm: 98, beams: 2, colors: ['255,120,40', '255,60,120', '255,200,60', '200,80,255'] }], + effects: [{ type: 'stagelights', phase: 'ingorgo', from: [[232, 212], [332, 212], [282, 205]], wash: [282, 228, 170], bpm: 98, beams: 3, strobe: 8, pulse: 0.08, colors: ['255,120,40', '255,60,120', '255,200,60', '200,80,255'] }, { type: 'floorspots', phase: 'ingorgo', area: [40, 250, 560, 100], bpm: 98, count: 7, colors: ['255,120,40', '255,60,120', '255,200,60', '200,80,255'] }], stages: [ { when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [290, 235] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [260, 640], far: 700 }, @@ -1776,7 +1777,7 @@ export const story = { // Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza] // L'underground sotto il muro della villa; su, in fondo, la tecno di Cambray Digny; giù, lontano, la house. // Le luci da palco della festa (render.js: stagelights, lasers, mirrorball). - effects: [{ type: 'mirrorball', phase: 'ingorgo', at: [550, 131], area: [380, 40, 260, 200], count: 46 }, { type: 'stagelights', phase: 'ingorgo', wash: [552, 205, 120], bpm: 122, colors: ['170,60,255', '90,40,220', '220,60,255'] }], + effects: [{ type: 'mirrorball', phase: 'ingorgo', at: [550, 131], area: [0, 20, 640, 300], count: 110 }, { type: 'stagelights', phase: 'ingorgo', from: [[520, 200], [600, 200]], wash: [552, 205, 160], bpm: 122, beams: 2, strobe: 8, pulse: 0.07, colors: ['170,60,255', '90,40,220', '220,60,255'] }, { type: 'floorspots', phase: 'ingorgo', area: [40, 240, 560, 110], bpm: 122, count: 6, colors: ['170,60,255', '60,120,255', '220,60,255'] }], stages: [ { when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [565, 235] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [270, 120], far: 600 }, diff --git a/tests/ingorgo.test.mjs b/tests/ingorgo.test.mjs index 7ac3b4f..e83fca9 100644 --- a/tests/ingorgo.test.mjs +++ b/tests/ingorgo.test.mjs @@ -96,6 +96,15 @@ test('chiudendo l\'effetto prima del lampo, la festa arriva lo stesso', () => { assert.ok(g.state.flags.ingorgo); }); +test('col dito il manifesto dell\'Ingorgo si prende anche toccando vicino, e non si scambia con quello del Palio', () => { + const g = new Game(story); g.go('piazza'); settle(g); + const [x, y, w, h] = g.hotspot('manifesto_ingorgo').box, reach = 28; // 22 pixel di schermo su un iPhone in orizzontale + for (const p of [[x + w / 2, y + h / 2], [x - 6, y + h / 2], [x + w + 4, y + h / 2], [x + w / 2, y + h + 6]]) + assert.equal(g.hotspotAt(p, reach)?.id, 'manifesto_ingorgo', `tocco in ${p}`); + const palio = g.hotspot('manifesto').box; + assert.equal(g.hotspotAt([palio[0] + palio[2] / 2, palio[1] + palio[3] / 2], reach)?.id, 'manifesto'); +}); + test('il manifesto ingrandito si chiude senza fare niente: niente festa', () => { const g = new Game(story); g.go('piazza'); settle(g); g.act('look', 'manifesto_ingorgo'); settle(g); @@ -103,6 +112,18 @@ test('il manifesto ingrandito si chiude senza fare niente: niente festa', () => assert.ok(!g.state.flags.ingorgo && !g.closeup); }); +test('alla festa si clicca solo per andare da un posto all\'altro', () => { + const g = fromPiazza(); + for (const scene of ZONE.scenes) { + g.go(scene); settle(g); + const list = g.hotspots(); + assert.ok(list.length && list.every(h => h.exit), `${scene}: solo le uscite`); + assert.ok(story.scenes[scene].hotspots.some(h => !h.exit), `${scene}: di giorno c'è anche altro`); + } + g.go('piazza'); settle(g); + assert.equal(g.hotspotAt([505, 200]), null, 'la colonna, alla festa, non risponde'); +}); + test('alla festa le luci da palco si muovono, e solo alla festa', () => { globalThis.Image = class { set src(v) {} }; try { diff --git a/video/ingorgo-sonoro.mjs b/video/ingorgo-sonoro.mjs new file mode 100644 index 0000000..5fc6b75 --- /dev/null +++ b/video/ingorgo-sonoro.mjs @@ -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 --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)); +} diff --git a/video/prova-locale.mjs b/video/prova-locale.mjs index 431855f..0a97c67 100644 --- a/video/prova-locale.mjs +++ b/video/prova-locale.mjs @@ -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));