Compare commits

..
6 Commits
15 changed files with 391 additions and 39 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+7 -3
View File
@@ -3439,9 +3439,13 @@
"file": "andrea-366c066d68.mp3", "file": "andrea-366c066d68.mp3",
"sec": 2.28 "sec": 2.28
}, },
"andrea|Ma... è l'Ingorgo! Le console, la gente, le spine della birra!": { "andrea|Mahhh... è l'INGORGOOOO!!!": {
"file": "andrea-bc7d5f4dc0.mp3", "file": "andrea-5694cd3779.mp3",
"sec": 7.5 "sec": 3.74
},
"andrea|Le console, la gente, le spine della birra!": {
"file": "andrea-a6bceec795.mp3",
"sec": 2.57
}, },
"andrea|Il palio può aspettare una sera. O no?": { "andrea|Il palio può aspettare una sera. O no?": {
"file": "andrea-24e5ac53da.mp3", "file": "andrea-24e5ac53da.mp3",
+5 -1
View File
@@ -24,7 +24,11 @@ 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 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 `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. 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.
In cima a Via dell'Antica Posta le luci sono quelle proposte da Codex (`pixelbeams`, `mirrorball`, `stagechase`,
`pixelspots`): Andrea ha tenuto la sua palla a specchi lì, e le nostre «caciarone» altrove. 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 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 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 dell'Antica Posta, tecno a Cambray Digny; i brani sono di Andrea (Suno), portati al volume del gioco. Gli sfondi sono
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "leggenda-del-regolo", "name": "leggenda-del-regolo",
"version": "0.1.58", "version": "0.1.61",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+7 -1
View File
@@ -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 }; 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); } hotspot(id) { return this.hotspots().find(h => h.id === id); }
people() { return (this.scene.people ?? []).filter(p => check(p.when, this.state)); } 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 // Col mouse vale il punto preciso (l'ultimo della lista sta sopra). Col dito, `reach` allarga l'area: prima gli
+163 -22
View File
@@ -279,6 +279,35 @@ function drawPhase(ctx, game) {
// Un numero «a caso» ma ripetibile: lo stesso istante dà sempre lo stesso disegno (e i test lo possono controllare). // Un numero «a caso» ma ripetibile: lo stesso istante dà sempre lo stesso disegno (e i test lo possono controllare).
const hash = n => (((n | 0) * 2654435761) >>> 0) / 4294967296; const hash = n => (((n | 0) * 2654435761) >>> 0) / 4294967296;
// Fasci della festa: alone e nucleo sulla griglia dei pixel, attenuati in tre fasce nette lungo il percorso.
// Bastano sei poligoni per fascio, senza sfocature o letture del canvas.
function discoCone(ctx, x, y, angle, len, width, color, power) {
for (let layer = 0; layer < 2; layer++) {
const half = width * (layer ? 0.32 : 1);
const lx = Math.cos(angle - half), ly = Math.sin(angle - half), rx = Math.cos(angle + half), ry = Math.sin(angle + half);
for (let band = 0; band < 3; band++) {
const near = len * band / 3, far = len * (band + 1) / 3;
ctx.fillStyle = `rgba(${color},${power * (layer ? 0.8 : 0.45) * (1 - band * 0.36)})`;
ctx.beginPath();
ctx.moveTo(Math.round(x + lx * near), Math.round(y + ly * near));
ctx.lineTo(Math.round(x + lx * far), Math.round(y + ly * far));
ctx.lineTo(Math.round(x + rx * far), Math.round(y + ry * far));
ctx.lineTo(Math.round(x + rx * near), Math.round(y + ry * near));
ctx.closePath(); ctx.fill();
}
}
}
// Una chiazza schiacciata a scalini: cinque strisce bastano, anche sulla folla in primo piano.
function discoPool(ctx, x, y, rx, ry, color, power) {
ctx.fillStyle = `rgba(${color},${power})`;
for (let row = -2; row <= 2; row++) {
const w = Math.round(rx * (Math.abs(row) === 2 ? 0.6 : Math.abs(row) === 1 ? 0.88 : 1));
const top = Math.round(y + (row - 0.5) * ry * 0.4), bottom = Math.round(y + (row + 0.5) * ry * 0.4);
ctx.fillRect(Math.round(x) - w, top, w * 2, bottom - top);
}
}
// Gli effetti delle scene (`effects`), ognuno col suo `type`: piccole animazioni disegnate sopra lo sfondo. // Gli effetti delle scene (`effects`), ognuno col suo `type`: piccole animazioni disegnate sopra lo sfondo.
export const EFFECTS = { export const EFFECTS = {
...ambientEffects, ...ambientEffects,
@@ -497,18 +526,18 @@ export const EFFECTS = {
if (e.wash) { if (e.wash) {
const [x, y, r] = e.wash, c = colors[bar % colors.length]; const [x, y, r] = e.wash, c = colors[bar % colors.length];
const g = ctx.createRadialGradient(x, y, 0, x, y, r); 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)`); g.addColorStop(1, `rgba(${c},0)`);
ctx.fillStyle = g; ctx.fillStyle = g;
ctx.fillRect(x - r, y - r, r * 2, r * 2); ctx.fillRect(x - r, y - r, r * 2, r * 2);
} }
for (const [k, [x, y]] of (e.from ?? []).entries()) { for (const [k, [x, y]] of (e.from ?? []).entries()) {
for (let i = 0; i < (e.beams ?? 2); i++) { for (let i = 0; i < (e.beams ?? 3); i++) {
const n = k * 3 + i, swing = Math.sin(time / (900 + n * 170) + n * 1.9) * (e.spread ?? 0.75); 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 ?? 260, half = 0.045; 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 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); 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)`); g.addColorStop(1, `rgba(${c},0)`);
ctx.fillStyle = g; ctx.fillStyle = g;
ctx.beginPath(); ctx.beginPath();
@@ -521,19 +550,46 @@ export const EFFECTS = {
ctx.fillRect(Math.round(x) - 1, Math.round(y) - 1, 3, 3); ctx.fillRect(Math.round(x) - 1, Math.round(y) - 1, 3, 3);
} }
} }
ctx.restore(); // Il colpo di colore su tutta la scena a ogni battuta (`pulse`: quanto forte).
// Lo strobo: un lampo bianco brevissimo ogni `strobe` battute. if (e.pulse) {
if (e.strobe && Math.floor(beat) % e.strobe === 0 && beat % 1 < 0.12) { ctx.fillStyle = `rgba(${colors[Math.floor(beat) % colors.length]},${e.pulse * hit ** 3})`;
ctx.fillStyle = 'rgba(255,255,255,0.13)';
ctx.fillRect(0, 0, WIDTH, HEIGHT); 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. // I laser della tecno: righe sottili verdi (o del colore dato) che si aprono a ventaglio e si richiudono.
lasers(ctx, e, time) { 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; 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.save();
ctx.globalCompositeOperation = 'lighter'; 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.lineWidth = 1;
ctx.beginPath(); ctx.beginPath();
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
@@ -544,22 +600,107 @@ export const EFFECTS = {
ctx.stroke(); ctx.stroke();
ctx.restore(); ctx.restore();
}, },
// La palla a specchi: puntini di luce che girano sui muri intorno (`area`), più chiari vicino alla palla. // La palla a specchi «di Codex» (Andrea, 30 ottobre: «l'effetto qui mi piaceva molto»): appesa al suo filo, un faro
// la colpisce, e i riflessi girano a tempo sui muri e sulla folla (`surfaces`), con qualche scintilla.
mirrorball(ctx, e, time) { mirrorball(ctx, e, time) {
const [bx, by] = e.at, [ax, ay, aw, ah] = e.area, turn = time / 4200; const [bx, by] = e.at, beat = time * (e.bpm ?? 122) / 60000, turn = beat / 32;
const surfaces = e.surfaces ?? [e.area];
ctx.save();
ctx.fillStyle = '#71718d'; ctx.fillRect(bx, e.hang ?? by - 26, 1, by - (e.hang ?? by - 26) - 7);
ctx.globalCompositeOperation = 'lighter';
if (e.from) {
const [x, y] = e.from, dx = bx - x, dy = by - y;
discoCone(ctx, x, y, Math.atan2(dy, dx), Math.hypot(dx, dy), 0.035, '195,215,255', 0.32);
}
for (let i = 0; i < (e.count ?? 60); i++) {
const [ax, ay, aw, ah] = surfaces[i % surfaces.length];
const u = (hash(i * 13) + turn) % 1, v = 0.5 + 0.42 * Math.sin(hash(i * 7 + 3) * Math.PI * 2 + turn * Math.PI * 2);
const x = ax + Math.floor(u * (aw - 4)), y = ay + Math.floor(v * (ah - 3));
const glint = Math.max(0, Math.sin(turn * Math.PI * 4 + i * 2.3)) ** 4;
ctx.fillStyle = `rgba(${e.color ?? '200,215,255'},${0.13 + 0.65 * glint})`;
ctx.fillRect(x, y, glint > 0.7 ? 3 : 2, 2);
if (glint > 0.95) ctx.fillRect(x + 1, y - 1, 1, 4);
}
ctx.globalCompositeOperation = 'source-over';
for (let row = -3; row <= 3; row++) {
for (let col = -3; col <= 3; col++) {
if (row * row + col * col > 11) continue;
const light = Math.sin(col * 0.8 + turn * Math.PI * 2) * 0.65 - row * 0.12;
ctx.fillStyle = light > 0.55 ? '#f3f5ff' : light > 0.1 ? '#b9bcdf' : light > -0.4 ? '#6e78ab' : '#343856';
ctx.fillRect(bx + col * 2, by + row * 2, 2, 2);
}
}
ctx.fillStyle = '#ffffff'; ctx.fillRect(bx - 3, by - 4, 3, 1); ctx.fillRect(bx - 2, by - 5, 1, 3);
ctx.restore();
},
// Le teste mobili «di Codex» (30 ottobre, usate all'underground): incrocio, ventaglio, passaggio parallelo. Il programma cambia ogni otto battiti,
// sempre ricavato dal tempo: anche tornando indietro a un istante si ritrova esattamente la stessa luce.
pixelbeams(ctx, e, time) {
const colors = e.colors ?? ['255,45,170', '45,190,255'], beat = time * (e.bpm ?? 124) / 60000;
const hit = (1 - beat % 1) ** 3, bar = Math.floor(beat / 4), cue = Math.floor(beat / 8) % 3;
const from = e.from ?? [], swing = Math.sin(beat * Math.PI / (e.sweep ?? 4));
ctx.save(); ctx.save();
ctx.globalCompositeOperation = 'lighter'; ctx.globalCompositeOperation = 'lighter';
for (let i = 0; i < (e.count ?? 40); i++) { if (e.wash) {
const u = (hash(i * 13) + turn) % 1, x = ax + Math.round(u * aw), y = ay + Math.round(hash(i * 7 + 3) * ah); const [x, y, r] = e.wash, c = colors[bar % colors.length];
const glint = 0.5 + 0.5 * Math.sin(time / 160 + i * 2.3); discoPool(ctx, x, y, r, r * 0.32, c, 0.045 + 0.045 * hit);
ctx.fillStyle = `rgba(${e.color ?? '220,200,255'},${0.35 + 0.5 * glint})`; discoPool(ctx, x, y, r * 0.55, r * 0.18, c, 0.04 + 0.07 * hit);
ctx.fillRect(x, y, 2, 2); }
for (let k = 0; k < from.length; k++) {
const [x, y] = from[k], side = k % 2 ? 1 : -1;
const fan = from.length > 1 ? k / (from.length - 1) * 2 - 1 : 0;
const pan = cue === 0 ? side * swing : cue === 1 ? fan * (0.6 + 0.25 * swing) : swing;
const angle = -Math.PI / 2 + (e.aim ?? 0) + pan * (e.spread ?? 0.7);
const color = colors[(k + bar) % colors.length];
// Un breve buio di fine frase rende leggibile il rientro della cassa; il latino resta più continuo.
const power = (e.power ?? 0.38) * (0.65 + 0.35 * hit) * (e.shutter && beat % 16 > 15.5 ? 0.15 : 1);
for (let i = 0; i < (e.beams ?? 1); i++) {
const offset = (i - ((e.beams ?? 1) - 1) / 2) * 0.15;
discoCone(ctx, x, y, angle + offset, e.len ?? 270, e.width ?? 0.055, color, power);
}
ctx.fillStyle = `rgb(${color})`;
ctx.fillRect(x - 2, y - 1, 5, 3);
ctx.fillStyle = '#fff5df'; ctx.fillRect(x, y, 1, 1);
}
ctx.restore();
},
// Lampade sulla traversa: inseguimento a ottavi, poi accensione insieme sul primo colpo della frase.
// I blinder illuminano solo davanti al palco: nessun velo bianco su tutta la scena.
stagechase(ctx, e, time) {
const beat = time * (e.bpm ?? 124) / 60000, step = Math.floor(beat * 2), points = e.points;
const colors = e.colors ?? ['255,45,170', '45,190,255'];
const flash = e.blinder && (beat % 16 < 0.14 || (Math.floor(beat) % 16 === 15 && beat * 2 % 1 < 0.16));
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for (let i = 0; i < points.length; i++) {
const [x, y] = points[i], on = step % points.length === i || beat % 8 < 0.22;
const color = flash ? '255,240,210' : colors[(i + Math.floor(beat / 4)) % colors.length];
if (on || flash) {
ctx.fillStyle = `rgba(${color},0.18)`; ctx.fillRect(x - 4, y - 3, 9, 7);
if (flash) discoCone(ctx, x, y, Math.PI / 2, e.len ?? 65, 0.3, color, 0.2);
}
ctx.fillStyle = `rgba(${color},${on || flash ? 0.95 : 0.15})`;
ctx.fillRect(x - 2, y - 1, 5, 3);
if (on || flash) { ctx.fillStyle = '#fff5df'; ctx.fillRect(x, y, 1, 1); }
}
ctx.restore();
},
// Gobo sulla folla «di Codex»: macchie a gradini, con tagli scuri, più grandi verso il primo piano. L'area le contiene tutte.
pixelspots(ctx, e, time) {
const [ax, ay, aw, ah] = e.area, colors = e.colors ?? ['255,45,170', '45,190,255'];
const beat = time * (e.bpm ?? 124) / 60000, hit = (1 - beat % 1) ** 2;
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for (let i = 0; i < (e.count ?? 6); i++) {
const phase = beat * Math.PI / 8 + i * 2.4;
const v = 0.5 + 0.5 * Math.sin(phase * 0.7 + i), u = 0.5 + 0.5 * Math.sin(phase);
const rx = Math.min((e.size ?? 24) * (0.55 + 0.45 * v), aw / 2), ry = Math.min(rx * 0.3, ah / 2);
const x = ax + rx + u * (aw - rx * 2), y = ay + ry + v * (ah - ry * 2);
const c = colors[(i + Math.floor(beat / 4)) % colors.length];
// Le tre impronte separate ricordano un disco sagomato davanti al proiettore.
for (let part = -1; part <= 1; part++)
discoPool(ctx, x + part * rx * 0.65, y, rx * 0.25, ry * (part ? 0.7 : 1), c, 0.12 + 0.16 * hit);
} }
const g = ctx.createRadialGradient(bx, by, 0, bx, by, 9);
g.addColorStop(0, 'rgba(255,255,255,0.8)');
g.addColorStop(1, 'rgba(200,180,255,0)');
ctx.fillStyle = g;
ctx.fillRect(bx - 9, by - 9, 18, 18);
ctx.restore(); ctx.restore();
}, },
// Lampioni: un alone caldo che si accende (di solito solo di sera, con `phase`). // Lampioni: un alone caldo che si accende (di solito solo di sera, con `phase`).
+17 -7
View File
@@ -82,6 +82,7 @@ export const story = {
zones: [ zones: [
{ {
flag: 'ingorgo', scenes: ['piazza', 'via_antica_posta', 'piazzetta_comune', 'antica_posta_alta', 'piazzetta'], flag: 'ingorgo', scenes: ['piazza', 'via_antica_posta', 'piazzetta_comune', 'antica_posta_alta', 'piazzetta'],
exitsOnly: true, // alla festa si clicca solo per spostarsi (Andrea)
leave: [ leave: [
{ say: 'Mi stropiccio gli occhi. La musica non c\'è più: è di nuovo maggio.' }, { say: 'Mi stropiccio gli occhi. La musica non c\'è più: è di nuovo maggio.' },
{ say: 'E il palio è sempre sparito. Però che serata.' }, { say: 'E il palio è sempre sparito. Però che serata.' },
@@ -344,7 +345,9 @@ export const story = {
type: 'incanto', type: 'incanto',
swap: [{ set: 'ingorgo' }], swap: [{ set: 'ingorgo' }],
complete: [ complete: [
{ say: 'Ma... è l\'Ingorgo! Le console, la gente, le spine della birra!' }, // Più emozione (Andrea: «MAHHH E' L'INGORGOOOOO!!!!!»); la voce legge la versione di tools/pronuncia.json.
{ say: 'Mahhh... è l\'INGORGOOOO!!!' },
{ say: 'Le console, la gente, le spine della birra!' },
{ say: 'Il palio può aspettare una sera. O no?' }, { say: 'Il palio può aspettare una sera. O no?' },
], ],
}, },
@@ -588,7 +591,7 @@ export const story = {
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [195, 235] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [195, 235] },
{ when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [-160, 250], far: 600 }, { 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]], 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]], depth: [[236, 0.5], [360, 1.35]],
entry: [345, 240], entry: [345, 240],
@@ -672,7 +675,7 @@ export const story = {
{ {
// Il manifesto dell'Ingorgo Sonoro dell'estate prima, mezzo strappato: guardandolo, Andrea esprime il // 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] // 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. // 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' }, 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' }, { rect: [229, 205, 10, 4], fill: '#d8262e' }, { rect: [232, 205, 4, 3], fill: '#f4f1e8' }, { rect: [228, 211, 12, 2], fill: '#f4f1e8' },
@@ -1625,7 +1628,7 @@ export const story = {
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza] // 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. // 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). // 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: [ stages: [
{ when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [495, 245] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [495, 245] },
{ when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [700, 300], far: 650 }, { when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [700, 300], far: 650 },
@@ -1671,7 +1674,7 @@ export const story = {
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza] // 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. // 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). // 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: [ stages: [
{ when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [-110, 250] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [-110, 250] },
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [320, 560] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [320, 560] },
@@ -1720,7 +1723,7 @@ export const story = {
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza] // 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. // 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). // 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: [ stages: [
{ when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [290, 235] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [290, 235] },
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [260, 640], far: 700 }, { when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [260, 640], far: 700 },
@@ -1776,7 +1779,14 @@ export const story = {
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza] // 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. // 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). // 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'] }], // Qui le luci sono quelle di Codex (Andrea, 30 ottobre: «l'effetto qui mi piaceva molto»): teste sulle casse a
// destra, la palla a specchi che proietta sui due muri e sulla folla, le lampade a inseguimento sulla traversa.
effects: [
{ type: 'pixelbeams', phase: 'ingorgo', from: [[499, 177], [620, 153]], wash: [568, 220, 70], bpm: 122, aim: -0.35, spread: 0.55, sweep: 8, power: 0.28, colors: ['155,65,255', '65,110,255'] },
{ type: 'mirrorball', phase: 'ingorgo', at: [550, 131], hang: 91, from: [499, 177], surfaces: [[430, 60, 205, 140], [18, 105, 155, 85], [35, 245, 570, 105]], bpm: 122, count: 72 },
{ type: 'stagechase', phase: 'ingorgo', points: [[533, 210], [557, 210], [583, 209], [609, 208]], bpm: 122, colors: ['155,65,255', '65,110,255'] },
{ type: 'pixelspots', phase: 'ingorgo', area: [130, 242, 490, 108], bpm: 122, count: 5, size: 20, colors: ['155,65,255', '65,110,255'] },
],
stages: [ stages: [
{ when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [565, 235] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [565, 235] },
{ when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [270, 120], far: 600 }, { when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [270, 120], far: 600 },
+1 -1
View File
@@ -1,5 +1,5 @@
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json. // La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
export const VERSION = '0.1.58'; export const VERSION = '0.1.61';
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica, // Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica,
// mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf). // mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf).
+1 -1
View File
@@ -36,7 +36,7 @@ try {
await page.screenshot({ path: `${out}/2-fasci.png` }); await page.screenshot({ path: `${out}/2-fasci.png` });
await page.waitForFunction(() => game.closeup?.t > 2.5, null, { timeout: 5000 }); await page.waitForFunction(() => game.closeup?.t > 2.5, null, { timeout: 5000 });
await page.screenshot({ path: `${out}/3-coriandoli.png` }); await page.screenshot({ path: `${out}/3-coriandoli.png` });
await page.waitForFunction(() => !game.closeup && game.state.flags.ingorgo && game.speech?.text.includes('Ingorgo'), null, { timeout: 5000 }); await page.waitForFunction(() => !game.closeup && game.state.flags.ingorgo && game.speech?.text.toLowerCase().includes('ingorgo'), null, { timeout: 5000 });
await page.screenshot({ path: `${out}/3b-festa.png` }); await page.screenshot({ path: `${out}/3b-festa.png` });
await page.evaluate(() => settle(game)); await page.evaluate(() => settle(game));
// La festa, scena per scena: lo sfondo di notte, e la musica di più console insieme. // La festa, scena per scena: lo sfondo di notte, e la musica di più console insieme.
+21
View File
@@ -96,6 +96,15 @@ test('chiudendo l\'effetto prima del lampo, la festa arriva lo stesso', () => {
assert.ok(g.state.flags.ingorgo); 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', () => { test('il manifesto ingrandito si chiude senza fare niente: niente festa', () => {
const g = new Game(story); g.go('piazza'); settle(g); const g = new Game(story); g.go('piazza'); settle(g);
g.act('look', 'manifesto_ingorgo'); 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); 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', () => { test('alla festa le luci da palco si muovono, e solo alla festa', () => {
globalThis.Image = class { set src(v) {} }; globalThis.Image = class { set src(v) {} };
try { try {
+2 -1
View File
@@ -20,5 +20,6 @@
"Imposto": "Impòsto", "Imposto": "Impòsto",
"Libici": "Lìbici", "Libici": "Lìbici",
"mancano": "màncano", "mancano": "màncano",
"console": "consòl" "console": "consòl",
"Mahhh... è l'INGORGOOOO!!!": "Maaaa, è l'Ingorgo! L'Ingorgooo!"
} }
+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.toLowerCase().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.toLowerCase().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}/`)); 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 mod = await import(process.env.COPIONE ?? './trailer.mjs');
const t0 = Date.now(); let n = 0; 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.setup(d);
await mod.run(d); await mod.run(d);
console.log('fine', ((Date.now() - t0) / 1000).toFixed(1)); console.log('fine', ((Date.now() - t0) / 1000).toFixed(1));