Il minigioco delle bruciate alla Smarronata: coi volontari della Misericordia si gira a mano la manovella del cilindro sul fuoco, facendo giri col dito o col mouse; troppo piano si carbonizzano sotto e restano crude sopra, troppo forte volano nel fuoco; cotte, nel sacco (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 226 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+71
-19
@@ -1939,9 +1939,9 @@
|
|||||||
"file": "andrea-2603f4db73.mp3",
|
"file": "andrea-2603f4db73.mp3",
|
||||||
"sec": 5.93
|
"sec": 5.93
|
||||||
},
|
},
|
||||||
"andrea|Si gira piano, sennò le bruciate si bruciano davvero.": {
|
"andrea|Posso girare io la manovella?": {
|
||||||
"file": "andrea-675499a046.mp3",
|
"file": "andrea-8febc84325.mp3",
|
||||||
"sec": 3.07
|
"sec": 1.75
|
||||||
},
|
},
|
||||||
"andrea|Una bruciata calda, presa al volo. Scotta! Ma è buonissima.": {
|
"andrea|Una bruciata calda, presa al volo. Scotta! Ma è buonissima.": {
|
||||||
"file": "andrea-ebba87b32c.mp3",
|
"file": "andrea-ebba87b32c.mp3",
|
||||||
@@ -1971,18 +1971,6 @@
|
|||||||
"file": "andrea-bfd09556ea.mp3",
|
"file": "andrea-bfd09556ea.mp3",
|
||||||
"sec": 5.63
|
"sec": 5.63
|
||||||
},
|
},
|
||||||
"andrea|Che Iddio ve ne renda merito!! A voi e agli alpini!": {
|
|
||||||
"file": "andrea-6933735b75.mp3",
|
|
||||||
"sec": 3.95
|
|
||||||
},
|
|
||||||
"volontari|E a te, figliolo. To', un cartoccio: son belle calde.": {
|
|
||||||
"file": "volontari-e8705fe901.mp3",
|
|
||||||
"sec": 5.14
|
|
||||||
},
|
|
||||||
"andrea|Scottano! Ma son buonissime.": {
|
|
||||||
"file": "andrea-ff81f048f5.mp3",
|
|
||||||
"sec": 2.79
|
|
||||||
},
|
|
||||||
"andrea|La fanfara degli alpini. Quando sfilavano per il paese suonando, s'affacciava tutto San Piero.": {
|
"andrea|La fanfara degli alpini. Quando sfilavano per il paese suonando, s'affacciava tutto San Piero.": {
|
||||||
"file": "andrea-a372a71849.mp3",
|
"file": "andrea-a372a71849.mp3",
|
||||||
"sec": 6.3
|
"sec": 6.3
|
||||||
@@ -2647,6 +2635,54 @@
|
|||||||
"file": "vecchietto-c8eae2e59e.mp3",
|
"file": "vecchietto-c8eae2e59e.mp3",
|
||||||
"sec": 4.88
|
"sec": 4.88
|
||||||
},
|
},
|
||||||
|
"andrea|Che Iddio ve ne renda merito!! A voi e agli alpini!": {
|
||||||
|
"file": "andrea-6933735b75.mp3",
|
||||||
|
"sec": 3.95
|
||||||
|
},
|
||||||
|
"andrea|Vi do una mano alla manovella?": {
|
||||||
|
"file": "andrea-1308ef4494.mp3",
|
||||||
|
"sec": 1.53
|
||||||
|
},
|
||||||
|
"andrea|Buon lavoro!": {
|
||||||
|
"file": "andrea-ade8043e82.mp3",
|
||||||
|
"sec": 0.71
|
||||||
|
},
|
||||||
|
"volontari|E a te, figliolo. To', un cartoccio: son belle calde.": {
|
||||||
|
"file": "volontari-e8705fe901.mp3",
|
||||||
|
"sec": 5.14
|
||||||
|
},
|
||||||
|
"andrea|Scottano! Ma son buonissime.": {
|
||||||
|
"file": "andrea-ff81f048f5.mp3",
|
||||||
|
"sec": 2.79
|
||||||
|
},
|
||||||
|
"volontari|Su, giovanotto, che noi s'ha le braccia stanche!": {
|
||||||
|
"file": "volontari-98d33dbfbd.mp3",
|
||||||
|
"sec": 2.43
|
||||||
|
},
|
||||||
|
"volontari|Si gira sempre uguale: piano piano sotto si bruciano, a furia volan nel fuoco. Quando son cotte, nel sacco.": {
|
||||||
|
"file": "volontari-7db485651b.mp3",
|
||||||
|
"sec": 8.77
|
||||||
|
},
|
||||||
|
"volontari|Come te la senti?": {
|
||||||
|
"file": "volontari-a104846cbf.mp3",
|
||||||
|
"sec": 0.88
|
||||||
|
},
|
||||||
|
"andrea|No, dopo.": {
|
||||||
|
"file": "andrea-78ad61a67a.mp3",
|
||||||
|
"sec": 1.22
|
||||||
|
},
|
||||||
|
"volontari|Alla bona: ti si guarda noi, e se si bruciano un si dice a nessuno.": {
|
||||||
|
"file": "volontari-5761a9776b.mp3",
|
||||||
|
"sec": 4.82
|
||||||
|
},
|
||||||
|
"volontari|Seria: la gente è in fila, e il cartoccio lo vole bello pieno.": {
|
||||||
|
"file": "volontari-dad04d4161.mp3",
|
||||||
|
"sec": 3.99
|
||||||
|
},
|
||||||
|
"volontari|A fassi male? Allora ci si mette anche il fuoco forte.": {
|
||||||
|
"file": "volontari-839aa118ab.mp3",
|
||||||
|
"sec": 4.1
|
||||||
|
},
|
||||||
"andrea|Barone! Che ci fai qui?": {
|
"andrea|Barone! Che ci fai qui?": {
|
||||||
"file": "andrea-83abae1419.mp3",
|
"file": "andrea-83abae1419.mp3",
|
||||||
"sec": 2.91
|
"sec": 2.91
|
||||||
@@ -2987,10 +3023,6 @@
|
|||||||
"file": "andrea-9cc0991d39.mp3",
|
"file": "andrea-9cc0991d39.mp3",
|
||||||
"sec": 3.32
|
"sec": 3.32
|
||||||
},
|
},
|
||||||
"andrea|No, dopo.": {
|
|
||||||
"file": "andrea-78ad61a67a.mp3",
|
|
||||||
"sec": 1.22
|
|
||||||
},
|
|
||||||
"andrea|C'è posto per uno?": {
|
"andrea|C'è posto per uno?": {
|
||||||
"file": "andrea-f7481e5519.mp3",
|
"file": "andrea-f7481e5519.mp3",
|
||||||
"sec": 1.81
|
"sec": 1.81
|
||||||
@@ -3619,6 +3651,26 @@
|
|||||||
"file": "andrea-356a1afe8d.mp3",
|
"file": "andrea-356a1afe8d.mp3",
|
||||||
"sec": 5.91
|
"sec": 5.91
|
||||||
},
|
},
|
||||||
|
"andrea|Bruciate coi fiocchi! Mezza Smarronata le mangia grazie a me.": {
|
||||||
|
"file": "andrea-145f20bff9.mp3",
|
||||||
|
"sec": 5.11
|
||||||
|
},
|
||||||
|
"volontari|Bravo, figliolo! Se ti va, l'anno prossimo la manovella è tua.": {
|
||||||
|
"file": "volontari-067aa2d701.mp3",
|
||||||
|
"sec": 4.42
|
||||||
|
},
|
||||||
|
"andrea|Un po' crude, un po' carbone... Le mangio io, va'.": {
|
||||||
|
"file": "andrea-6f3339acb0.mp3",
|
||||||
|
"sec": 4.06
|
||||||
|
},
|
||||||
|
"volontari|Ci vuole il polso, figliolo. Il polso e la pazienza.": {
|
||||||
|
"file": "volontari-3bb4fe1dc0.mp3",
|
||||||
|
"sec": 4.28
|
||||||
|
},
|
||||||
|
"andrea|Vi rendo la manovella. Mi fumano anche gli occhi.": {
|
||||||
|
"file": "andrea-367ddde0fe.mp3",
|
||||||
|
"sec": 3.61
|
||||||
|
},
|
||||||
"andrea|Il manifesto dell'Ingorgo Sonoro dell'anno scorso. Da piazza fino a Cambray Digny, una console in ogni angolo.": {
|
"andrea|Il manifesto dell'Ingorgo Sonoro dell'anno scorso. Da piazza fino a Cambray Digny, una console in ogni angolo.": {
|
||||||
"file": "andrea-0c26e34815.mp3",
|
"file": "andrea-0c26e34815.mp3",
|
||||||
"sec": 7.53
|
"sec": 7.53
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+9
-1
@@ -41,7 +41,15 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
|||||||
merito!! A voi e agli alpini!»), la fanfara degli alpini, gli alpini della polenta e del vino, la gente coi cartocci
|
merito!! A voi e agli alpini!»), la fanfara degli alpini, gli alpini della polenta e del vino, la gente coi cartocci
|
||||||
(anche nel tendone), le coppie del liscio che girano (`flourish` continuo).
|
(anche nel tendone), le coppie del liscio che girano (`flourish` continuo).
|
||||||
La musica di Andrea (Suno): il liscio fuori, più forte vicino alle coppie (`stages`), la mazurca nel tendone.
|
La musica di Andrea (Suno): il liscio fuori, più forte vicino alle coppie (`stages`), la mazurca nel tendone.
|
||||||
[DA ANDREA] le battute; un minigioco delle bruciate (girare il cilindro sul fuoco).
|
[DA ANDREA] le battute.
|
||||||
|
- **Il minigioco delle bruciate (31 ottobre)**: dai volontari della Misericordia («Vi do una mano alla manovella?»), o
|
||||||
|
usando il cilindro, si sceglie il livello e si gira la manovella a mano (`src/engine/bruciate.js`, disegno in
|
||||||
|
`bruciate-render.js`, sfondo di Codex `assets/closeups/smarronata-bruciatore.png`). Il gesto: tenendo premuto si
|
||||||
|
fanno giri, grandi o piccoli e dovunque (conta quanto gira la direzione del dito); Spazio, frecce: una spinta a
|
||||||
|
colpo. Troppo piano le castagne di sotto si carbonizzano e quelle di sopra restano crude, troppo forte volano nel
|
||||||
|
fuoco; quando son cotte, «Rovescia nel sacco». Contano le buone su 18 (alla bona 10, seria 15, a fassi male 16);
|
||||||
|
vincendo, il flag `bruciate_fatte`. Test in `bruciate.test.mjs`, prova `npm run browser:bruciate`.
|
||||||
|
[DA ANDREA] le battute dei volontari; un premio (un cartoccio da portare via?).
|
||||||
- **Dedica allo zio Mario (31 ottobre)**: nei Ringraziamenti, sotto quella al Lele e più piccola: «Al mio caro zio
|
- **Dedica allo zio Mario (31 ottobre)**: nei Ringraziamenti, sotto quella al Lele e più piccola: «Al mio caro zio
|
||||||
Mario, che mi ha fatto vivere tante avventure più belle della fantasia.»
|
Mario, che mi ha fatto vivere tante avventure più belle della fantasia.»
|
||||||
- **Il Taglia (31 ottobre)**: tolte le battute sulla sua altezza (Baldoria: non gli fa piacere).
|
- **Il Taglia (31 ottobre)**: tolte le battute sulla sua altezza (Baldoria: non gli fa piacere).
|
||||||
|
|||||||
@@ -295,6 +295,14 @@
|
|||||||
sinistra per la forza. I salti: la lancetta dell'equilibrio attraversa la barra; premi (o Spazio, o Salta) quando è
|
sinistra per la forza. I salti: la lancetta dell'equilibrio attraversa la barra; premi (o Spazio, o Salta) quando è
|
||||||
nel verde. Troppo presto metti giù l'altro piede, troppo tardi pesti la riga.</p>
|
nel verde. Troppo presto metti giù l'altro piede, troppo tardi pesti la riga.</p>
|
||||||
</details>
|
</details>
|
||||||
|
<details id="bruciate-help" hidden><summary>Come si gioca</summary>
|
||||||
|
<p>Le bruciate della Smarronata, coi volontari della Misericordia: le castagne sono nel cilindro a rete sopra il
|
||||||
|
fuoco e si gira la manovella. Tieni premuto e fai dei giri, grandi o piccoli, dove vuoi, col mouse o col dito:
|
||||||
|
il cilindro gira con te. Oppure Spazio o le frecce: una spinta a ogni colpo.</p>
|
||||||
|
<p>In alto, GIRO: tienilo nel verde. Troppo piano le castagne non si rimescolano, quelle di sotto si carbonizzano e
|
||||||
|
quelle di sopra restano crude; troppo forte volano fuori, nel fuoco. COTTURA: quando è nel verde, Rovescia nel
|
||||||
|
sacco (o Invio). Contano le bruciate buone: alla bona ne bastano 10 su 18, seria 15, a fassi male 16.</p>
|
||||||
|
</details>
|
||||||
<details id="cerbottana-help" hidden><summary>Come si gioca</summary>
|
<details id="cerbottana-help" hidden><summary>Come si gioca</summary>
|
||||||
<p>La cerbottana, con lo stucco verde di Pierin di Frilli: dodici pallini contro il muro della piazza. Il bersaglio
|
<p>La cerbottana, con lo stucco verde di Pierin di Frilli: dodici pallini contro il muro della piazza. Il bersaglio
|
||||||
di gesso vale 10, 5 o 2 punti secondo il cerchio, ogni lattina sul muretto 3, il barattolo appeso che dondola (Seria
|
di gesso vale 10, 5 o 2 punti secondo il cerchio, ogni lattina sul muretto 3, il barattolo appeso che dondola (Seria
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"browser:cerbottana": "node tests/cerbottana.browser.mjs",
|
"browser:cerbottana": "node tests/cerbottana.browser.mjs",
|
||||||
"browser:condor": "node tests/condor.browser.mjs",
|
"browser:condor": "node tests/condor.browser.mjs",
|
||||||
"browser:campana": "node tests/campana.browser.mjs",
|
"browser:campana": "node tests/campana.browser.mjs",
|
||||||
|
"browser:bruciate": "node tests/bruciate.browser.mjs",
|
||||||
"browser:ingorgo": "node tests/ingorgo.browser.mjs",
|
"browser:ingorgo": "node tests/ingorgo.browser.mjs",
|
||||||
"browser:tressette": "node tests/tressette.browser.mjs",
|
"browser:tressette": "node tests/tressette.browser.mjs",
|
||||||
"browser:scopa": "node tests/scopa.browser.mjs",
|
"browser:scopa": "node tests/scopa.browser.mjs",
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
// Le bruciate in primo piano (bruciate.js): il cilindro a rete sopra il fuoco, visto di fronte, con le castagne dentro
|
||||||
|
// che ricascano, la manovella a destra, le fiamme che lo lambiscono; in basso il giro della manovella e la cottura.
|
||||||
|
// Niente filtri e niente passate trasparenti a tutto schermo (vedi risveglio.js): solo forme piccole.
|
||||||
|
import { DRUM, CRANK, BAR, N, COOKED, BURNT, POUR, MAX_SPEED, inside, doneness } from './bruciate.js';
|
||||||
|
import { text, panel } from './bocce-render.js';
|
||||||
|
import { ambientHash as hash } from './ambient.js';
|
||||||
|
|
||||||
|
const SACK = { x: 604, y: 268 }, FIRE = 252, RODS = 18, RINGS = 9;
|
||||||
|
const lerp = (a, b, k) => a + (b - a) * k;
|
||||||
|
const mix = (p, q, k) => `rgb(${p.map((v, i) => Math.round(lerp(v, q[i], k))).join(',')})`;
|
||||||
|
// Il colore della buccia: lucida e rossiccia da cruda, scura da cotta, nera da carbone.
|
||||||
|
const RAW = [168, 96, 50], DONE = [118, 64, 30], COAL = [26, 20, 18];
|
||||||
|
const shell = cook => cook < COOKED ? mix(RAW, DONE, Math.max(0, cook) / COOKED) : mix(DONE, COAL, Math.min(1, (cook - COOKED) / (BURNT - COOKED)));
|
||||||
|
|
||||||
|
// Dove sta una castagna sullo schermo, e quanto è davanti (per l'ordine e la luce).
|
||||||
|
function place(k) {
|
||||||
|
return { x: lerp(DRUM.x0 + 8, DRUM.x1 - 8, k.x), y: DRUM.y + Math.sin(k.a) * DRUM.r * k.r, z: Math.cos(k.a) * k.r };
|
||||||
|
}
|
||||||
|
function chestnut(ctx, x, y, cook, z = 0) {
|
||||||
|
// Il bordino, perché si stacchino dal fondo del cilindro (chiaro su quelle carbone).
|
||||||
|
ctx.fillStyle = cook > BURNT ? '#6a5a4e' : '#1e120a';
|
||||||
|
ctx.beginPath(); ctx.ellipse(Math.round(x), Math.round(y), 7, 6, 0, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.fillStyle = shell(cook);
|
||||||
|
ctx.beginPath(); ctx.ellipse(Math.round(x), Math.round(y), 6, 5, 0, 0, Math.PI * 2); ctx.fill();
|
||||||
|
// La punta chiara e, da cotte, l'incisione aperta col giallo della castagna.
|
||||||
|
ctx.fillStyle = cook > BURNT ? '#2a2420' : '#d9b48a';
|
||||||
|
ctx.fillRect(Math.round(x) - 1, Math.round(y) - 4, 2, 1);
|
||||||
|
if (cook >= COOKED * 0.8 && cook <= BURNT) { ctx.fillStyle = '#e8c070'; ctx.fillRect(Math.round(x) - 2, Math.round(y) - 1, 4, 1); }
|
||||||
|
// Il riflesso del fuoco, più forte su quelle davanti.
|
||||||
|
if (cook < BURNT) { ctx.fillStyle = `rgba(255,190,120,${0.25 + 0.3 * Math.max(0, z)})`; ctx.fillRect(Math.round(x) + 1, Math.round(y) - 2, 2, 1); }
|
||||||
|
}
|
||||||
|
// Le fiamme che lambiscono il cilindro: lingue basse e storte, ognuna col suo passo, che ogni tanto si allungano.
|
||||||
|
function flames(ctx, time, front) {
|
||||||
|
for (let i = 0; i < 17; i++) {
|
||||||
|
if ((i % 3 === 0) !== front) continue;
|
||||||
|
const r = hash(i * 7.3), x = DRUM.x0 + 6 + i * (DRUM.x1 - DRUM.x0 - 12) / 16 + 6 * (hash(i * 1.9) - 0.5);
|
||||||
|
const lick = Math.max(0, Math.sin(time * (1.3 + r) + i * 2.1)) ** 4, h = front ? 5 + 6 * r + 2 * Math.sin(time * (6 + r * 5) + i) + 7 * lick : 18 + 16 * r + 7 * Math.sin(time * (6 + r * 5) + i) + 22 * lick;
|
||||||
|
const w = 5 + 5 * hash(i * 3.1), sway = 2.5 * Math.sin(time * (3 + r) + i * 1.7);
|
||||||
|
for (const [k, color] of [[1, '#b8301a'], [0.7, '#ea7a1a'], [0.4, '#ffd04a']]) {
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x - w * k, FIRE);
|
||||||
|
ctx.quadraticCurveTo(x - w * k * 0.2 + sway, FIRE - h * k * 0.6, x + sway * 2, FIRE - h * k);
|
||||||
|
ctx.quadraticCurveTo(x + w * k * 0.5 + sway, FIRE - h * k * 0.45, x + w * k, FIRE);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Il cilindro appeso: la sbarra sulle due forcelle e i due ferri che lo reggono per l'asse.
|
||||||
|
function hanger(ctx) {
|
||||||
|
ctx.fillStyle = '#2a2420'; ctx.fillRect(150, BAR - 3, 336, 6);
|
||||||
|
ctx.fillStyle = '#6a5a4a'; ctx.fillRect(150, BAR - 3, 336, 1);
|
||||||
|
for (const x of [DRUM.x0 - 12, DRUM.x1 + 12]) {
|
||||||
|
ctx.fillStyle = '#2a2420'; ctx.fillRect(x - 2, BAR, 4, DRUM.y - BAR + 4);
|
||||||
|
ctx.fillStyle = '#5a4a3e'; ctx.fillRect(x - 2, BAR, 1, DRUM.y - BAR + 4);
|
||||||
|
ctx.fillStyle = '#2a2420'; ctx.beginPath(); ctx.arc(x, DRUM.y, 5, 0, Math.PI * 2); ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// La rete: gli anelli e i tondini che girano col cilindro (quelli davanti più chiari).
|
||||||
|
function mesh(ctx, c, front) {
|
||||||
|
const { x0, x1, y, r } = DRUM;
|
||||||
|
for (let j = 0; j < RODS; j++) {
|
||||||
|
const a = c.drum + j * Math.PI * 2 / RODS, z = Math.cos(a);
|
||||||
|
if (front !== z > 0) continue;
|
||||||
|
ctx.fillStyle = front ? '#9a9088' : '#3a302a';
|
||||||
|
ctx.fillRect(x0, Math.round(y + Math.sin(a) * r), x1 - x0, 1);
|
||||||
|
}
|
||||||
|
ctx.strokeStyle = front ? '#8a8078' : '#3a302a'; ctx.lineWidth = 1;
|
||||||
|
for (let i = 0; i <= RINGS; i++) {
|
||||||
|
const x = Math.round(lerp(x0, x1, i / RINGS)) + 0.5, end = i === 0 || i === RINGS;
|
||||||
|
ctx.beginPath(); ctx.ellipse(x, y, end ? 9 : 4, r, 0, front ? -Math.PI / 2 : Math.PI / 2, front ? Math.PI / 2 : Math.PI * 1.5); ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function crank(ctx, c, time) {
|
||||||
|
const { x, y, rx, ry } = CRANK, a = c.drum;
|
||||||
|
// L'asse, da una forcella all'altra e fino alla manovella.
|
||||||
|
ctx.fillStyle = '#2a2420'; ctx.fillRect(DRUM.x0 - 16, y - 2, x - DRUM.x0 + 18, 4);
|
||||||
|
ctx.fillStyle = '#6a625a'; ctx.fillRect(DRUM.x0 - 16, y - 2, x - DRUM.x0 + 18, 1);
|
||||||
|
// Il giro da fare: un'ellisse tratteggiata che pulsa, finché il cilindro è fermo.
|
||||||
|
if (c.step === 'turn' && Math.abs(c.speed) < 0.5) {
|
||||||
|
ctx.strokeStyle = `rgba(255,224,140,${0.5 + 0.4 * Math.sin(time * 5)})`; ctx.setLineDash([3, 3]);
|
||||||
|
ctx.beginPath(); ctx.ellipse(x, y, rx + 6, ry + 6, 0, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]);
|
||||||
|
text(ctx, 'GIRA!', x, y + ry + 18, 6, '#ffe08c', 'center');
|
||||||
|
}
|
||||||
|
const hx = x + Math.cos(a) * rx, hy = y + Math.sin(a) * ry;
|
||||||
|
ctx.strokeStyle = '#5a5450'; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(hx, hy); ctx.stroke();
|
||||||
|
ctx.strokeStyle = '#b0a698'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, y - 1); ctx.lineTo(hx, hy - 1); ctx.stroke();
|
||||||
|
ctx.fillStyle = '#4a4440'; ctx.beginPath(); ctx.arc(x, y, 4, 0, Math.PI * 2); ctx.fill();
|
||||||
|
// L'impugnatura di legno, consumata dalle mani.
|
||||||
|
ctx.fillStyle = '#3a2414'; ctx.fillRect(Math.round(hx) - 4, Math.round(hy) - 8, 9, 16);
|
||||||
|
ctx.fillStyle = '#a0703c'; ctx.fillRect(Math.round(hx) - 3, Math.round(hy) - 7, 7, 14);
|
||||||
|
ctx.fillStyle = '#d6a062'; ctx.fillRect(Math.round(hx) - 2, Math.round(hy) - 6, 2, 12);
|
||||||
|
}
|
||||||
|
// In basso a sinistra (in alto ci sono i cursori del volume): il giro della manovella (rosso troppo piano, verde giusto, rosso troppo forte) e la cottura.
|
||||||
|
function gauges(ctx, c) {
|
||||||
|
panel(ctx, 10, 296, 196, 54);
|
||||||
|
const bar = (y, label, k, zones, mark) => {
|
||||||
|
text(ctx, label, 18, y, 6, '#efd49e');
|
||||||
|
const x0 = 82, w = 116;
|
||||||
|
for (const [a, b, color] of zones) { ctx.fillStyle = color; ctx.fillRect(x0 + Math.round(a * w), y - 3, Math.max(1, Math.round((b - a) * w)), 7); }
|
||||||
|
ctx.fillStyle = mark; ctx.fillRect(x0 + Math.round(Math.max(0, Math.min(1, k)) * w) - 1, y - 6, 3, 13);
|
||||||
|
};
|
||||||
|
const s = c.skill, lo = s.lo / MAX_SPEED, hi = s.hi / MAX_SPEED;
|
||||||
|
bar(312, 'GIRO', Math.abs(c.speed) / MAX_SPEED, [[0, lo, '#8a3a2a'], [lo, hi, '#4f8a3a'], [hi, 1, '#b3402a']], '#fff4c8');
|
||||||
|
const top = BURNT * 1.15;
|
||||||
|
bar(334, 'COTTURA', doneness(c) / top, [[0, COOKED / top, '#b07a4a'], [COOKED / top, BURNT / top, '#6a8a3a'], [BURNT / top, 1, '#2a2420']], '#fff4c8');
|
||||||
|
}
|
||||||
|
function result(ctx, c) {
|
||||||
|
const r = c.result;
|
||||||
|
panel(ctx, 170, 120, 300, 104);
|
||||||
|
text(ctx, r.winner === 'player' ? 'BRUCIATE COI FIOCCHI!' : 'BRUCIATE COSÌ COSÌ', 320, 140, 8, '#efd49e', 'center');
|
||||||
|
const rows = [['Buone', r.good, '#9fd27a'], ['Crude', r.raw, '#e0b07a'], ['Carbonizzate', r.burnt, '#c0b0a0'], ['Nel fuoco', r.spilled, '#f08a5a']];
|
||||||
|
rows.forEach(([label, n, color], i) => { text(ctx, label, 200, 162 + i * 13, 6, color); text(ctx, `${n}`, 440, 162 + i * 13, 6, color, 'right'); });
|
||||||
|
text(ctx, `Ne servivano ${r.goal} su ${N}`, 320, 216, 5, '#d7ccb4', 'center');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawBruciate(ctx, c, def, bg) {
|
||||||
|
const time = c.visual.time;
|
||||||
|
ctx.save();
|
||||||
|
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
|
||||||
|
else { ctx.fillStyle = '#6a5a40'; ctx.fillRect(0, 0, 640, 360); }
|
||||||
|
hanger(ctx);
|
||||||
|
// Il fondo del cilindro, scuro, col bagliore della brace sotto; poi le fiamme dietro e la rete di dietro.
|
||||||
|
ctx.fillStyle = 'rgba(74,46,30,0.72)'; ctx.fillRect(DRUM.x0, DRUM.y - DRUM.r, DRUM.x1 - DRUM.x0, DRUM.r * 2);
|
||||||
|
ctx.fillStyle = 'rgba(240,120,40,0.18)'; ctx.fillRect(DRUM.x0, DRUM.y + DRUM.r * 0.3, DRUM.x1 - DRUM.x0, DRUM.r * 0.7);
|
||||||
|
flames(ctx, time, false);
|
||||||
|
mesh(ctx, c, false);
|
||||||
|
// Le castagne: da dietro a davanti. Rovesciandole, scivolano tutte verso il sacco.
|
||||||
|
const pour = c.step === 'pour' ? Math.min(1, c.t / POUR) : c.step === 'result' ? 1 : 0;
|
||||||
|
const ks = (pour ? c.chestnuts.filter(k => !k.out) : inside(c)).map(k => ({ k, ...place(k) })).sort((p, q) => p.z - q.z);
|
||||||
|
for (const p of ks) {
|
||||||
|
if (pour) {
|
||||||
|
const d = Math.min(1, Math.max(0, pour * 1.6 - p.k.x * 0.6)), x = lerp(p.x, SACK.x, d), y = lerp(p.y, SACK.y, d) - Math.sin(d * Math.PI) * 40;
|
||||||
|
if (d < 1) chestnut(ctx, x, y, p.k.cook, 1);
|
||||||
|
} else chestnut(ctx, p.x, p.y, p.k.cook, p.z);
|
||||||
|
}
|
||||||
|
mesh(ctx, c, true);
|
||||||
|
flames(ctx, time, true);
|
||||||
|
crank(ctx, c, time);
|
||||||
|
// Quelle che volano nel fuoco, e lo sbuffo quando ci cascano.
|
||||||
|
for (const f of c.flying) {
|
||||||
|
const k = Math.min(1, f.t / 0.6), x = f.x + f.dir * 60 * k, y = lerp(f.y, FIRE - 6, k) - Math.sin(k * Math.PI) * 50;
|
||||||
|
if (f.t < 0.6) chestnut(ctx, x, y, 0.5);
|
||||||
|
else for (let i = 0; i < 4; i++) {
|
||||||
|
const s = (f.t - 0.6) / 0.6;
|
||||||
|
ctx.fillStyle = `rgba(200,195,185,${0.6 * (1 - s)})`;
|
||||||
|
ctx.beginPath(); ctx.arc(x + (i - 1.5) * 5 * (1 + s), FIRE - 10 - s * 30 - i * 3, 4 + s * 6, 0, Math.PI * 2); ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c.step === 'result') result(ctx, c); else gauges(ctx, c);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
// Le bruciate della Smarronata (Andrea, 31 ottobre: «a manovella, a mano, fallo così»): coi volontari della
|
||||||
|
// Misericordia si gira la manovella del cilindro a rete sopra il fuoco. Il gesto: tenendo premuto si fanno dei giri,
|
||||||
|
// grandi o piccoli, dove si vuole (conta quanto gira la direzione del dito, non dove sta), come la manovella vera.
|
||||||
|
// Le castagne stanno in fondo al cilindro: girando, la rete le alza e loro ricascano e si rimescolano. Troppo piano,
|
||||||
|
// quelle di sotto restano sulla brace e si carbonizzano mentre quelle di sopra restano crude; troppo forte, la rete
|
||||||
|
// le porta in cima e qualcuna salta fuori nel fuoco («mica si fa la centrifuga!»). Quando paiono cotte si rovesciano
|
||||||
|
// nel sacco: contano quelle buone, né crude né bruciate. Per chi non può fare il gesto: Spazio, le frecce o il
|
||||||
|
// pulsante Gira danno una spinta a ogni colpo. Unità: i pixel del primo piano 640×360, visto di fronte (sfondo di
|
||||||
|
// Codex: assets/closeups/smarronata-bruciatore.png).
|
||||||
|
|
||||||
|
// Il cilindro (fra le due forcelle dello sfondo) e la manovella, a destra: un giro visto di sbieco, un'ellisse.
|
||||||
|
export const DRUM = { x0: 182, x1: 456, y: 190, r: 46 }, CRANK = { x: 506, y: 190, rx: 14, ry: 30 }, BAR = 92;
|
||||||
|
export const N = 18, BOTTOM = Math.PI / 2;
|
||||||
|
// Tempi in secondi; velocità in radianti al secondo.
|
||||||
|
export const MIN_POUR = 4, POUR = 1.4, SPILL = 0.45, PUSH = 2.6, FRICTION = 1.4, LINE = 2.4, MAX_SPEED = 22;
|
||||||
|
// Quando una castagna è cotta e quando è carbonizzata.
|
||||||
|
export const COOKED = 0.85, BURNT = 1.6;
|
||||||
|
// I livelli: sotto `lo` il cilindro gira troppo piano (le castagne non si rimescolano abbastanza), sopra `hi` le
|
||||||
|
// castagne volano fuori; `time`, in quanti secondi cuociono girando bene; `goal`, quante buone ci vogliono.
|
||||||
|
export const LEVELS = {
|
||||||
|
bona: { lo: 1.6, hi: 10, time: 20, goal: 10 },
|
||||||
|
seria: { lo: 2.2, hi: 8, time: 17, goal: 15 },
|
||||||
|
male: { lo: 2.8, hi: 6.5, time: 14, goal: 16 },
|
||||||
|
};
|
||||||
|
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||||
|
const wrap = a => Math.atan2(Math.sin(a), Math.cos(a));
|
||||||
|
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
|
||||||
|
const between = (c, lo, hi) => lo + (hi - lo) * random(c);
|
||||||
|
|
||||||
|
// Dove ricasca una castagna: di nuovo in fondo, a un'altezza a caso nel mucchio, con la sua voglia di farsi alzare.
|
||||||
|
function drop(c, k, side = 1) { Object.assign(k, { a: BOTTOM - side * between(c, 0, 0.55), r: between(c, 0.5, 0.93), lift: between(c, 0.55, 1.5) }); }
|
||||||
|
|
||||||
|
export function newBruciate(seed = Date.now() >>> 0, def = {}) {
|
||||||
|
const level = LEVELS[def.level] ? def.level : 'seria';
|
||||||
|
const c = { step: 'turn', t: 0, seed, level, skill: LEVELS[level], current: 'player', power: 0, drum: 0, speed: 0,
|
||||||
|
pending: 0, grab: null, cursor: null, spin: 0, chestnuts: [], flying: [], line: null, lineT: 0, events: [], visual: { time: 0 } };
|
||||||
|
for (let i = 0; i < N; i++) { const k = { x: between(c, 0.06, 0.94), cook: between(c, 0, 0.08), out: false }; drop(c, k, random(c) < 0.5 ? 1 : -1); c.chestnuts.push(k); }
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
export const inside = c => c.chestnuts.filter(k => !k.out);
|
||||||
|
// Quanto stanno a contatto con la rete sopra la brace: le castagne in fondo e di fuori; quelle di sopra quasi niente.
|
||||||
|
export function contact(k) { return Math.max(0, Math.cos(k.a - BOTTOM)) ** 3 * clamp((k.r - 0.62) / 0.3, 0, 1); }
|
||||||
|
// La cottura media delle castagne ancora nel cilindro, da 0 (crude) in su (1: cotte).
|
||||||
|
export const doneness = c => { const ks = inside(c); return ks.length ? ks.reduce((s, k) => s + k.cook, 0) / ks.length : 0; };
|
||||||
|
export const tally = c => {
|
||||||
|
const t = { good: 0, raw: 0, burnt: 0, spilled: 0 };
|
||||||
|
for (const k of c.chestnuts) t[k.out ? 'spilled' : k.cook < COOKED ? 'raw' : k.cook > BURNT ? 'burnt' : 'good']++;
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
function say(c, line) { c.line = line; c.lineT = LINE; }
|
||||||
|
|
||||||
|
export function update(c, dt) {
|
||||||
|
if (!Number.isFinite(dt) || dt <= 0) return;
|
||||||
|
dt = Math.min(dt, 0.25); c.t += dt; c.visual.time += dt;
|
||||||
|
if (c.lineT > 0 && (c.lineT -= dt) <= 0) c.line = null;
|
||||||
|
for (const f of c.flying) f.t += dt;
|
||||||
|
c.flying = c.flying.filter(f => f.t < 1.2);
|
||||||
|
if (c.step === 'pour') { if (c.t >= POUR) finish(c); return; }
|
||||||
|
if (c.step !== 'turn') return;
|
||||||
|
// La velocità: tenendo la manovella è quella del gesto (smussata); lasciata, il cilindro rallenta da sé.
|
||||||
|
if (c.grab) c.speed += (c.pending / dt - c.speed) * (1 - Math.exp(-dt * 10));
|
||||||
|
else c.speed *= Math.exp(-FRICTION * dt);
|
||||||
|
c.pending = 0;
|
||||||
|
c.speed = clamp(c.speed, -MAX_SPEED, MAX_SPEED);
|
||||||
|
const w = c.speed, fast = Math.abs(w) > c.skill.hi, side = Math.sign(w) || 1, before = c.drum;
|
||||||
|
c.drum += w * dt;
|
||||||
|
// Il cigolio della manovella, a ogni mezzo giro.
|
||||||
|
if (Math.floor(before / Math.PI) !== Math.floor(c.drum / Math.PI)) c.events.push({ kind: 'bruciate-manovella', power: clamp(Math.abs(w) / 8, 0.3, 1) });
|
||||||
|
const rate = 1 / (c.skill.time * 0.45);
|
||||||
|
for (const k of inside(c)) {
|
||||||
|
// La rete se le porta dietro finché non ricascano in fondo; troppo forte le porta fino in cima e oltre.
|
||||||
|
k.a += w * dt * k.r;
|
||||||
|
if (!fast && (k.a - BOTTOM) * side > k.lift) drop(c, k, side);
|
||||||
|
const before = k.cook;
|
||||||
|
k.cook += dt * rate * (0.15 + 1.9 * contact(k));
|
||||||
|
if (before < COOKED && k.cook >= COOKED) c.events.push({ kind: 'bruciate-scoppio', power: 0.5 + 0.5 * random(c) });
|
||||||
|
}
|
||||||
|
// La centrifuga: ogni tanto ne vola fuori una, nel fuoco.
|
||||||
|
if (fast) {
|
||||||
|
c.spin += dt;
|
||||||
|
if (c.spin >= SPILL) {
|
||||||
|
c.spin = 0;
|
||||||
|
const k = inside(c).sort((p, q) => Math.cos(p.a - BOTTOM) - Math.cos(q.a - BOTTOM))[0];
|
||||||
|
if (k) { k.out = true; c.flying.push({ x: DRUM.x0 + k.x * (DRUM.x1 - DRUM.x0), y: DRUM.y + Math.sin(k.a) * DRUM.r * k.r, dir: side, t: 0 }); c.events.push({ kind: 'bruciate-cade', power: 0.8 }); }
|
||||||
|
say(c, random(c) < 0.5 ? 'Oh! Mica si fa la centrifuga!' : 'Piano, che le si butta nel fuoco!');
|
||||||
|
}
|
||||||
|
} else c.spin = Math.max(0, c.spin - dt);
|
||||||
|
if (!c.line && Math.abs(w) < c.skill.lo && c.t > 3 && inside(c).some(k => k.cook > BURNT - 0.3)) say(c, 'Gira, che sotto si bruciano!');
|
||||||
|
// Tutte nel fuoco o tutte carbone: si rovescia comunque.
|
||||||
|
if (!inside(c).length || inside(c).every(k => k.cook > BURNT)) pour(c);
|
||||||
|
}
|
||||||
|
function finish(c) {
|
||||||
|
const t = tally(c);
|
||||||
|
c.step = 'result'; c.speed = 0;
|
||||||
|
c.result = { winner: t.good >= c.skill.goal ? 'player' : 'cpu', ...t, goal: c.skill.goal };
|
||||||
|
}
|
||||||
|
export function pour(c) {
|
||||||
|
if (c.step !== 'turn') return false;
|
||||||
|
Object.assign(c, { step: 'pour', t: 0, grab: null });
|
||||||
|
c.events.push({ kind: 'bruciate-sacco', power: 0.8 });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Comandi. Il gesto: la direzione del movimento, a ogni tratto, quanto ha girato rispetto al tratto prima.
|
||||||
|
const STROKE = 3;
|
||||||
|
export function aimScreen(c, [x, y]) {
|
||||||
|
if (!Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||||
|
const g = c.grab;
|
||||||
|
if (g && c.step === 'turn') {
|
||||||
|
const [px, py] = g.last ?? [x, y], dx = x - px, dy = y - py;
|
||||||
|
if (Math.hypot(dx, dy) >= STROKE) {
|
||||||
|
const heading = Math.atan2(dy, dx);
|
||||||
|
if (g.heading !== null) c.pending += clamp(wrap(heading - g.heading), -1.2, 1.2);
|
||||||
|
g.heading = heading; g.last = [x, y];
|
||||||
|
} else if (!g.last) g.last = [x, y];
|
||||||
|
}
|
||||||
|
c.cursor = [x, y];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function press(c, pointerId) {
|
||||||
|
if (c.step !== 'turn' || c.grab) return false;
|
||||||
|
c.grab = { pointerId, heading: null, last: c.cursor };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function release(c, pointerId) {
|
||||||
|
if (!c.grab || c.grab.pointerId !== pointerId) return false;
|
||||||
|
c.grab = null; return true;
|
||||||
|
}
|
||||||
|
export const cancel = (c, pointerId) => (pointerId === undefined || c.grab?.pointerId === pointerId) && release(c, c.grab?.pointerId);
|
||||||
|
// Una spinta alla manovella (Spazio, frecce): per chi non fa il gesto.
|
||||||
|
export function push(c) {
|
||||||
|
if (c.step !== 'turn' || c.grab) return false;
|
||||||
|
c.speed = (Math.sign(c.speed) || 1) * (Math.abs(c.speed) + PUSH);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function key(c, k, down) {
|
||||||
|
if (k === 'Enter') { if (down) shoot(c); return true; }
|
||||||
|
if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(k)) return false;
|
||||||
|
if (down) push(c);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Il pulsante: rovescia nel sacco (dopo i primi secondi).
|
||||||
|
export const canThrow = c => c.step === 'turn' && c.t >= MIN_POUR;
|
||||||
|
export const canAdjust = () => false;
|
||||||
|
export const aimFree = () => true;
|
||||||
|
export const shoot = c => canThrow(c) && pour(c);
|
||||||
|
export const button = c => c.step === 'result' ? 'Finita' : c.step === 'pour' ? 'Nel sacco…' : 'Rovescia nel sacco';
|
||||||
|
export function speedWord(c) {
|
||||||
|
const w = Math.abs(c.speed);
|
||||||
|
return w < 0.3 ? 'ferma' : w < c.skill.lo ? 'troppo piano' : w > c.skill.hi ? 'troppo forte' : 'giusta';
|
||||||
|
}
|
||||||
|
export function status(c) {
|
||||||
|
if (c.step === 'result') {
|
||||||
|
const r = c.result, extra = [r.raw && `${r.raw} crude`, r.burnt && `${r.burnt} carbonizzate`, r.spilled && `${r.spilled} nel fuoco`].filter(Boolean).join(', ');
|
||||||
|
return `${r.good} bruciate buone su ${N}${extra ? ` (${extra})` : ''}. ${r.winner === 'player' ? 'I volontari approvano!' : `Ne servivano ${r.goal}.`}`;
|
||||||
|
}
|
||||||
|
if (c.step === 'pour') return 'Nel sacco di iuta, a sudare...';
|
||||||
|
const cooked = `Cottura ${Math.round(doneness(c) * 100)}%.`;
|
||||||
|
if (c.line) return `${c.line} ${cooked}`;
|
||||||
|
const hint = c.t < 3 && Math.abs(c.speed) < 0.3 ? 'Tieni premuto e fai dei giri per girare la manovella.' : `Manovella ${speedWord(c)}.`;
|
||||||
|
return `${hint} ${cooked} Quando son cotte: Rovescia nel sacco.`;
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import { newRisveglio, updateRisveglio } from './risveglio.js';
|
|||||||
import { newTelefono, updateTelefono, tapTelefono } from './telefono.js';
|
import { newTelefono, updateTelefono, tapTelefono } from './telefono.js';
|
||||||
import * as condor from './condor.js';
|
import * as condor from './condor.js';
|
||||||
import * as campana from './campana.js';
|
import * as campana from './campana.js';
|
||||||
|
import * as bruciate from './bruciate.js';
|
||||||
import * as poker from './poker.js';
|
import * as poker from './poker.js';
|
||||||
|
|
||||||
// I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta.
|
// I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta.
|
||||||
@@ -34,6 +35,7 @@ export const MINIGAMES = {
|
|||||||
cerbottana: { create: (seed, level) => cerbottana.newCerbottana(seed, { level }), update: cerbottana.update, press: cerbottana.press, release: cerbottana.release, cancel: cerbottana.cancel, aim: cerbottana.aimScreen },
|
cerbottana: { create: (seed, level) => cerbottana.newCerbottana(seed, { level }), update: cerbottana.update, press: cerbottana.press, release: cerbottana.release, cancel: cerbottana.cancel, aim: cerbottana.aimScreen },
|
||||||
condor: { create: (seed, level) => condor.newCondor(seed, { level }), update: condor.update, press: condor.press, release: condor.release, cancel: condor.cancel, aim: condor.aimScreen },
|
condor: { create: (seed, level) => condor.newCondor(seed, { level }), update: condor.update, press: condor.press, release: condor.release, cancel: condor.cancel, aim: condor.aimScreen },
|
||||||
campana: { create: (seed, level) => campana.newCampana(seed, { level }), update: campana.update, press: campana.press, release: campana.release, cancel: campana.cancel, aim: campana.aimScreen },
|
campana: { create: (seed, level) => campana.newCampana(seed, { level }), update: campana.update, press: campana.press, release: campana.release, cancel: campana.cancel, aim: campana.aimScreen },
|
||||||
|
bruciate: { create: (seed, level) => bruciate.newBruciate(seed, { level }), update: bruciate.update, press: bruciate.press, release: bruciate.release, cancel: bruciate.cancel, aim: bruciate.aimScreen },
|
||||||
micro: { create: (seed, level) => micro.newMicro(seed, { level }), update: micro.update, press: micro.press, release: micro.release, cancel: micro.cancel, aim: micro.aimScreen },
|
micro: { create: (seed, level) => micro.newMicro(seed, { level }), update: micro.update, press: micro.press, release: micro.release, cancel: micro.cancel, aim: micro.aimScreen },
|
||||||
gara: { create: (seed, level, deck, def) => gara.newGara(seed, { level, riders: def.riders }), update: gara.update, press: gara.press, release: gara.release, cancel: gara.cancel, aim: gara.aimScreen },
|
gara: { create: (seed, level, deck, def) => gara.newGara(seed, { level, riders: def.riders }), update: gara.update, press: gara.press, release: gara.release, cancel: gara.cancel, aim: gara.aimScreen },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { drawCerbottana } from './cerbottana-render.js';
|
|||||||
import { drawCorsa } from './corsa.js';
|
import { drawCorsa } from './corsa.js';
|
||||||
import { drawCondor } from './condor-render.js';
|
import { drawCondor } from './condor-render.js';
|
||||||
import { drawCampana } from './campana-render.js';
|
import { drawCampana } from './campana-render.js';
|
||||||
|
import { drawBruciate } from './bruciate-render.js';
|
||||||
import { drawPoker } from './poker-render.js';
|
import { drawPoker } from './poker-render.js';
|
||||||
import { drawViaggio } from './viaggio.js';
|
import { drawViaggio } from './viaggio.js';
|
||||||
import { drawIncanto } from './incanto.js';
|
import { drawIncanto } from './incanto.js';
|
||||||
@@ -1020,6 +1021,11 @@ function drawCloseup(ctx, game, time) {
|
|||||||
drawCampana(ctx, c, def, backgrounds.get(def.picture));
|
drawCampana(ctx, c, def, backgrounds.get(def.picture));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (def.type === 'bruciate') {
|
||||||
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
|
drawBruciate(ctx, c, def, backgrounds.get(def.picture));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (def.type === 'micro') {
|
if (def.type === 'micro') {
|
||||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
drawMicro(ctx, c, def, backgrounds.get(def.picture), src => { if (!backgrounds.has(src)) loadBackground(src); return backgrounds.get(src); });
|
drawMicro(ctx, c, def, backgrounds.get(def.picture), src => { if (!backgrounds.has(src)) loadBackground(src); return backgrounds.get(src); });
|
||||||
|
|||||||
@@ -156,6 +156,11 @@ export class Soundscape {
|
|||||||
else if (kind === 'campana-sasso') { this.tone(1900, 1100, 0.04, 0.05 * strength + 0.02, 0, pan); this.tone(1500, 900, 0.03, 0.03, 0.06, pan); this.noiseSound(0.04, 3000, 0.03, { pan }); }
|
else if (kind === 'campana-sasso') { this.tone(1900, 1100, 0.04, 0.05 * strength + 0.02, 0, pan); this.tone(1500, 900, 0.03, 0.03, 0.06, pan); this.noiseSound(0.04, 3000, 0.03, { pan }); }
|
||||||
else if (kind === 'campana-salto') { this.tone(110, 60, 0.07, 0.07, 0, pan); this.noiseSound(0.05, 500, 0.04, { pan }); }
|
else if (kind === 'campana-salto') { this.tone(110, 60, 0.07, 0.07, 0, pan); this.noiseSound(0.05, 500, 0.04, { pan }); }
|
||||||
else if (kind === 'campana-riga') { this.tone(300, 180, 0.12, 0.05, 0, pan); this.tone(220, 120, 0.16, 0.05, 0.1, pan); }
|
else if (kind === 'campana-riga') { this.tone(300, 180, 0.12, 0.05, 0, pan); this.tone(220, 120, 0.16, 0.05, 0.1, pan); }
|
||||||
|
// Le bruciate: il cigolio della manovella, la castagna che scoppietta cuocendo, quella che casca nel fuoco, il sacco.
|
||||||
|
else if (kind === 'bruciate-manovella') { this.tone(420, 380, 0.08, 0.025 * strength, 0, pan); this.noiseSound(0.06, 900, 0.02, { pan }); }
|
||||||
|
else if (kind === 'bruciate-scoppio') { this.noiseSound(0.04, 2600, 0.05 * strength, { pan }); this.tone(900, 500, 0.03, 0.03, 0, pan); }
|
||||||
|
else if (kind === 'bruciate-cade') { this.noiseSound(0.5, 3200, 0.09, { pan }); this.tone(200, 90, 0.12, 0.05, 0, pan); }
|
||||||
|
else if (kind === 'bruciate-sacco') this.noiseSound(0.7, 1100, 0.1, { paper: true, pan });
|
||||||
else if (kind === 'cerbottana-tosse') { this.noiseSound(0.18, 900, 0.08, { pan }); this.tone(260, 140, 0.12, 0.06, 0.25, pan); this.tone(240, 120, 0.1, 0.05, 0.3, pan); }
|
else if (kind === 'cerbottana-tosse') { this.noiseSound(0.18, 900, 0.08, { pan }); this.tone(260, 140, 0.12, 0.06, 0.25, pan); this.tone(240, 120, 0.1, 0.05, 0.3, pan); }
|
||||||
// Le macchinine: il dito che spinge, le ruote sull'asfalto, l'urto di plastica, fuori pista, il traguardo.
|
// Le macchinine: il dito che spinge, le ruote sull'asfalto, l'urto di plastica, fuori pista, il traguardo.
|
||||||
else if (kind === 'micro-spinta') { this.tone(1200, 500, 0.04, 0.04 + strength * 0.04, 0, pan); this.noiseSound(0.04, 2600, 0.04, { pan }); }
|
else if (kind === 'micro-spinta') { this.tone(1200, 500, 0.04, 0.04 + strength * 0.04, 0, pan); this.noiseSound(0.04, 2600, 0.04, { pan }); }
|
||||||
|
|||||||
+6
-3
@@ -15,6 +15,7 @@ import * as micro from './engine/micro.js';
|
|||||||
import * as cerbottana from './engine/cerbottana.js';
|
import * as cerbottana from './engine/cerbottana.js';
|
||||||
import * as condor from './engine/condor.js';
|
import * as condor from './engine/condor.js';
|
||||||
import * as campana from './engine/campana.js';
|
import * as campana from './engine/campana.js';
|
||||||
|
import * as bruciate from './engine/bruciate.js';
|
||||||
import * as poker from './engine/poker.js';
|
import * as poker from './engine/poker.js';
|
||||||
import { cardAt as pokerAt } from './engine/poker-render.js';
|
import { cardAt as pokerAt } from './engine/poker-render.js';
|
||||||
import * as basket from './engine/basket.js';
|
import * as basket from './engine/basket.js';
|
||||||
@@ -617,6 +618,7 @@ const MINI = {
|
|||||||
cerbottana: { ...cerbottana, back: 'Torna in piazza', label: 'La cerbottana: muovi il mouse o tocca il muro per mirare, tieni premuto per prendere fiato e lascia per sparare; oppure regola il fiato e premi Spara. Frecce per spostare il mirino.' },
|
cerbottana: { ...cerbottana, back: 'Torna in piazza', label: 'La cerbottana: muovi il mouse o tocca il muro per mirare, tieni premuto per prendere fiato e lascia per sparare; oppure regola il fiato e premi Spara. Frecce per spostare il mirino.' },
|
||||||
condor: { ...condor, back: 'Torna al campone', label: 'La Condor: muovi il mouse o tocca il campo per mirare, tieni premuto per trattenere il fiato e lascia per sparare; oppure premi Spara. Frecce per spostare il mirino.' },
|
condor: { ...condor, back: 'Torna al campone', label: 'La Condor: muovi il mouse o tocca il campo per mirare, tieni premuto per trattenere il fiato e lascia per sparare; oppure premi Spara. Frecce per spostare il mirino.' },
|
||||||
campana: { ...campana, back: 'Torna al campone', label: 'La campana: muovi il mouse o tocca la campana per mirare, tieni premuto per la forza e lascia per tirare il sassolino; oppure regola la forza e premi Tira (frecce su e giù per mirare, destra e sinistra per la forza). Ai salti premi, o Spazio, quando la lancetta è nel verde.' },
|
campana: { ...campana, back: 'Torna al campone', label: 'La campana: muovi il mouse o tocca la campana per mirare, tieni premuto per la forza e lascia per tirare il sassolino; oppure regola la forza e premi Tira (frecce su e giù per mirare, destra e sinistra per la forza). Ai salti premi, o Spazio, quando la lancetta è nel verde.' },
|
||||||
|
bruciate: { ...bruciate, back: 'Torna alla festa', label: 'Le bruciate: tieni premuto e fai dei giri, col mouse o col dito, per girare la manovella; oppure Spazio o le frecce, una spinta a ogni colpo. Né troppo piano né troppo forte; quando son cotte, Rovescia nel sacco (o Invio).' },
|
||||||
micro: { ...micro, back: 'Torna dal Chiaru', label: 'La pista col gesso: muovi il mouse o tocca il parcheggio per mirare, tieni premuto per caricare la spinta e lascia; oppure regola la forza e premi Spingi. Frecce o pulsanti Mira per girare.' },
|
micro: { ...micro, back: 'Torna dal Chiaru', label: 'La pista col gesso: muovi il mouse o tocca il parcheggio per mirare, tieni premuto per caricare la spinta e lascia; oppure regola la forza e premi Spingi. Frecce o pulsanti Mira per girare.' },
|
||||||
basket: { ...basket, back: 'Torna in strada', label: 'Fai due tiri: muovi il mouse o tocca per mirare, tieni premuto per caricare la forza, lascia per tirare.' },
|
basket: { ...basket, back: 'Torna in strada', label: 'Fai due tiri: muovi il mouse o tocca per mirare, tieni premuto per caricare la forza, lascia per tirare.' },
|
||||||
// La gara: niente mira né forza, il pulsante alterna gas e freno (chi non può tenere premuto).
|
// La gara: niente mira né forza, il pulsante alterna gas e freno (chi non può tenere premuto).
|
||||||
@@ -1046,7 +1048,7 @@ function updateUi() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mg && !cards) {
|
if (mg && !cards) {
|
||||||
setAttr($('bocce-controls'), 'aria-label', { rinquarto: 'Comandi dei rinquarti', biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca', gara: 'Comandi della gara', micro: 'Comandi delle macchinine', cerbottana: 'Comandi della cerbottana', condor: 'Comandi della Condor', campana: 'Comandi della campana' }[def.type] ?? 'Comandi delle bocce');
|
setAttr($('bocce-controls'), 'aria-label', { rinquarto: 'Comandi dei rinquarti', biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca', gara: 'Comandi della gara', micro: 'Comandi delle macchinine', cerbottana: 'Comandi della cerbottana', condor: 'Comandi della Condor', campana: 'Comandi della campana', bruciate: 'Comandi delle bruciate' }[def.type] ?? 'Comandi delle bocce');
|
||||||
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
|
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
|
||||||
const line = mg.status(c, def.names);
|
const line = mg.status(c, def.names);
|
||||||
if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line;
|
if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line;
|
||||||
@@ -1060,8 +1062,8 @@ function updateUi() {
|
|||||||
// Ai rinquarti la forza è sempre piena: il cursore non serve. Nella guida semplice del GP (alla bona e seria) si
|
// Ai rinquarti la forza è sempre piena: il cursore non serve. Nella guida semplice del GP (alla bona e seria) si
|
||||||
// usa solo il gas: niente sensibilità, sterzo, freno né riquadro dello swipe.
|
// usa solo il gas: niente sensibilità, sterzo, freno né riquadro dello swipe.
|
||||||
const easy = def.type === 'gara' && gara.simple(c);
|
const easy = def.type === 'gara' && gara.simple(c);
|
||||||
setProp(powerLabel, 'hidden', def.type === 'rinquarto' || def.type === 'condor' || easy);
|
setProp(powerLabel, 'hidden', def.type === 'rinquarto' || def.type === 'condor' || def.type === 'bruciate' || easy);
|
||||||
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy || def.type === 'campana');
|
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy || def.type === 'campana' || def.type === 'bruciate');
|
||||||
const powerName = def.type === 'gara' ? 'Sensibilità ' : def.type === 'cerbottana' ? 'Fiato ' : 'Forza ';
|
const powerName = def.type === 'gara' ? 'Sensibilità ' : def.type === 'cerbottana' ? 'Fiato ' : 'Forza ';
|
||||||
if (powerLabel.firstChild.nodeValue !== powerName) powerLabel.firstChild.nodeValue = powerName;
|
if (powerLabel.firstChild.nodeValue !== powerName) powerLabel.firstChild.nodeValue = powerName;
|
||||||
setAttr($('bocce-power'), 'aria-label', def.type === 'gara' ? 'Sensibilità dello sterzo col dito' : 'Forza del lancio');
|
setAttr($('bocce-power'), 'aria-label', def.type === 'gara' ? 'Sensibilità dello sterzo col dito' : 'Forza del lancio');
|
||||||
@@ -1083,6 +1085,7 @@ function updateUi() {
|
|||||||
setProp($('cerbottana-help'), 'hidden', def.type !== 'cerbottana');
|
setProp($('cerbottana-help'), 'hidden', def.type !== 'cerbottana');
|
||||||
setProp($('condor-help'), 'hidden', def.type !== 'condor');
|
setProp($('condor-help'), 'hidden', def.type !== 'condor');
|
||||||
setProp($('campana-help'), 'hidden', def.type !== 'campana');
|
setProp($('campana-help'), 'hidden', def.type !== 'campana');
|
||||||
|
setProp($('bruciate-help'), 'hidden', def.type !== 'bruciate');
|
||||||
setProp($('basket-help'), 'hidden', def.type !== 'basket');
|
setProp($('basket-help'), 'hidden', def.type !== 'basket');
|
||||||
setProp($('pesca-help'), 'hidden', def.type !== 'pesca');
|
setProp($('pesca-help'), 'hidden', def.type !== 'pesca');
|
||||||
setProp($('gara-help'), 'hidden', def.type !== 'gara');
|
setProp($('gara-help'), 'hidden', def.type !== 'gara');
|
||||||
|
|||||||
+38
-6
@@ -360,6 +360,17 @@ export const story = {
|
|||||||
draw: [],
|
draw: [],
|
||||||
quit: [{ say: 'Il gesso lo lascio lì. Ci giocherà qualche bambino, che è più in allenamento di me.' }],
|
quit: [{ say: 'Il gesso lo lascio lì. Ci giocherà qualche bambino, che è più in allenamento di me.' }],
|
||||||
},
|
},
|
||||||
|
// Le bruciate della Smarronata, 1993: la manovella del cilindro sul fuoco, coi volontari della Misericordia
|
||||||
|
// (Andrea, 31 ottobre: «a manovella, a mano»). [DA ANDREA: le battute sono provvisorie]
|
||||||
|
bruciate: {
|
||||||
|
type: 'bruciate',
|
||||||
|
picture: 'assets/closeups/smarronata-bruciatore.png',
|
||||||
|
names: { player: 'Andrea', cpu: 'i volontari' },
|
||||||
|
win: [{ say: 'Bruciate coi fiocchi! Mezza Smarronata le mangia grazie a me.' }, { say: 'Bravo, figliolo! Se ti va, l\'anno prossimo la manovella è tua.', who: 'volontari', pose: 'give' }, { set: 'bruciate_fatte' }],
|
||||||
|
lose: [{ say: 'Un po\' crude, un po\' carbone... Le mangio io, va\'.' }, { say: 'Ci vuole il polso, figliolo. Il polso e la pazienza.', who: 'volontari', pose: 'explain' }],
|
||||||
|
draw: [],
|
||||||
|
quit: [{ say: 'Vi rendo la manovella. Mi fumano anche gli occhi.' }],
|
||||||
|
},
|
||||||
// Il manifesto dell'Ingorgo, ingrandito (Andrea, 30 ottobre: «se mi avvicino compaia ingrandito, e poi parta
|
// Il manifesto dell'Ingorgo, ingrandito (Andrea, 30 ottobre: «se mi avvicino compaia ingrandito, e poi parta
|
||||||
// l'Ingorgo se ci clicco»). [DA ANDREA: il manifesto vero; questo è di Codex, provvisorio]
|
// l'Ingorgo se ci clicco»). [DA ANDREA: il manifesto vero; questo è di Codex, provvisorio]
|
||||||
manifesto_ingorgo: {
|
manifesto_ingorgo: {
|
||||||
@@ -3734,7 +3745,8 @@ export const story = {
|
|||||||
id: 'bruciatore', name: 'il cilindro delle bruciate', box: [192, 188, 68, 54], at: [226, 262], face: 'up',
|
id: 'bruciatore', name: 'il cilindro delle bruciate', box: [192, 188, 68, 54], at: [226, 262], face: 'up',
|
||||||
actions: {
|
actions: {
|
||||||
look: [{ say: 'Il cilindro delle bruciate: si gira la manovella sopra il fuoco, e le castagne ci ballano dentro.' }],
|
look: [{ say: 'Il cilindro delle bruciate: si gira la manovella sopra il fuoco, e le castagne ci ballano dentro.' }],
|
||||||
use: [{ say: 'Si gira piano, sennò le bruciate si bruciano davvero.' }],
|
// Il minigioco delle bruciate: la manovella si gira coi volontari (Andrea, 31 ottobre).
|
||||||
|
use: [{ say: 'Posso girare io la manovella?' }, { dialogue: 'volontari' }],
|
||||||
take: [{ say: 'Una bruciata calda, presa al volo. Scotta! Ma è buonissima.' }],
|
take: [{ say: 'Una bruciata calda, presa al volo. Scotta! Ma è buonissima.' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -3763,11 +3775,7 @@ export const story = {
|
|||||||
id: 'volontari', name: 'i volontari della Misericordia', box: [130, 148, 97, 114], at: [200, 272], face: 'left', default: 'talk',
|
id: 'volontari', name: 'i volontari della Misericordia', box: [130, 148, 97, 114], at: [200, 272], face: 'left', default: 'talk',
|
||||||
actions: {
|
actions: {
|
||||||
look: [{ say: 'I volontari della Misericordia, alle bruciate: uno rimescola, l\'altro riempie i cartocci.' }],
|
look: [{ say: 'I volontari della Misericordia, alle bruciate: uno rimescola, l\'altro riempie i cartocci.' }],
|
||||||
talk: [
|
talk: [{ dialogue: 'volontari' }],
|
||||||
{ say: 'Che Iddio ve ne renda merito!! A voi e agli alpini!' },
|
|
||||||
{ say: 'E a te, figliolo. To\', un cartoccio: son belle calde.', who: 'volontari', pose: 'give' },
|
|
||||||
{ say: 'Scottano! Ma son buonissime.' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -4237,6 +4245,30 @@ export const story = {
|
|||||||
},
|
},
|
||||||
// Il BaronDeDuke nella discesa verso la polisportiva: le mutande perse a poker, e il Muro del Sanpierino sul
|
// Il BaronDeDuke nella discesa verso la polisportiva: le mutande perse a poker, e il Muro del Sanpierino sul
|
||||||
// telefonino (Andrea, 31 ottobre: lui è il webmaster di sanpiero50037.it).
|
// telefonino (Andrea, 31 ottobre: lui è il webmaster di sanpiero50037.it).
|
||||||
|
// I volontari della Misericordia alle bruciate della Smarronata, 1993: il ringraziamento di Andrea e la manovella
|
||||||
|
// (il minigioco delle bruciate, Andrea, 31 ottobre). [DA ANDREA: le battute sono provvisorie]
|
||||||
|
volontari: {
|
||||||
|
nodes: {
|
||||||
|
start: { options: [
|
||||||
|
{ text: 'Che Iddio ve ne renda merito!! A voi e agli alpini!', then: [
|
||||||
|
{ say: 'E a te, figliolo. To\', un cartoccio: son belle calde.', who: 'volontari', pose: 'give' },
|
||||||
|
{ say: 'Scottano! Ma son buonissime.' },
|
||||||
|
] },
|
||||||
|
{ text: 'Vi do una mano alla manovella?', next: 'livello', then: [
|
||||||
|
{ say: 'Su, giovanotto, che noi s\'ha le braccia stanche!', who: 'volontari', pose: 'talk' },
|
||||||
|
{ say: 'Si gira sempre uguale: piano piano sotto si bruciano, a furia volan nel fuoco. Quando son cotte, nel sacco.', who: 'volontari', pose: 'explain' },
|
||||||
|
{ say: 'Come te la senti?', who: 'volontari', pose: 'talk' },
|
||||||
|
] },
|
||||||
|
{ text: 'Buon lavoro!', next: 'end' },
|
||||||
|
] },
|
||||||
|
livello: { options: [
|
||||||
|
{ text: 'Alla bona.', next: 'end', then: [{ say: 'Alla bona: ti si guarda noi, e se si bruciano un si dice a nessuno.', who: 'volontari', pose: 'talk' }, { closeup: 'bruciate', level: 'bona' }] },
|
||||||
|
{ text: 'Seria.', next: 'end', then: [{ say: 'Seria: la gente è in fila, e il cartoccio lo vole bello pieno.', who: 'volontari', pose: 'explain' }, { closeup: 'bruciate', level: 'seria' }] },
|
||||||
|
{ text: 'A fassi male.', next: 'end', then: [{ say: 'A fassi male? Allora ci si mette anche il fuoco forte.', who: 'volontari', pose: 'talk' }, { closeup: 'bruciate', level: 'male' }] },
|
||||||
|
{ text: 'No, dopo.', next: 'start' },
|
||||||
|
] },
|
||||||
|
},
|
||||||
|
},
|
||||||
barondeduke: {
|
barondeduke: {
|
||||||
nodes: {
|
nodes: {
|
||||||
start: { options: [
|
start: { options: [
|
||||||
|
|||||||
@@ -62,16 +62,16 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
|
|||||||
/* La guida semplice del GP: il Gas si tiene premuto col pollice, più alto. */
|
/* La guida semplice del GP: il Gas si tiene premuto col pollice, più alto. */
|
||||||
#bocce-throw.hold { min-height: 72px; font-size: 14px; }
|
#bocce-throw.hold { min-height: 72px; font-size: 14px; }
|
||||||
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
|
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
|
||||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help, #campana-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help, #campana-help, #bruciate-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
||||||
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary, #gara-help summary, #rinquarto-help summary, #micro-help summary, #cerbottana-help, #condor-help summary, #campana-help summary { cursor: pointer; padding: 6px 0; }
|
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary, #gara-help summary, #rinquarto-help summary, #micro-help summary, #cerbottana-help, #condor-help summary, #campana-help summary, #bruciate-help summary { cursor: pointer; padding: 6px 0; }
|
||||||
#bocce-help p, #biliardo-help p, #basket-help p, #pesca-help p, #gara-help p, #rinquarto-help p, #micro-help p, #cerbottana-help, #condor-help p, #campana-help p { max-width: 76em; line-height: 1.7; }
|
#bocce-help p, #biliardo-help p, #basket-help p, #pesca-help p, #gara-help p, #rinquarto-help p, #micro-help p, #cerbottana-help, #condor-help p, #campana-help p, #bruciate-help p { max-width: 76em; line-height: 1.7; }
|
||||||
@media (max-width: 600px), (orientation: landscape) and (max-height: 520px) {
|
@media (max-width: 600px), (orientation: landscape) and (max-height: 520px) {
|
||||||
#bocce-controls { padding: 8px; font-size: 10px; overflow-y: auto; min-height: 0; }
|
#bocce-controls { padding: 8px; font-size: 10px; overflow-y: auto; min-height: 0; }
|
||||||
.bocce-adjust { flex-wrap: wrap; gap: 6px; }
|
.bocce-adjust { flex-wrap: wrap; gap: 6px; }
|
||||||
.bocce-adjust label { order: -1; flex-basis: 100%; max-width: none; }
|
.bocce-adjust label { order: -1; flex-basis: 100%; max-width: none; }
|
||||||
.bocce-adjust button { flex: 1; font-size: 9px; padding: 6px; }
|
.bocce-adjust button { flex: 1; font-size: 9px; padding: 6px; }
|
||||||
#bocce-throw { flex-basis: 100%; }
|
#bocce-throw { flex-basis: 100%; }
|
||||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help, #campana-help { font-size: 9px; }
|
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help, #campana-help, #bruciate-help { font-size: 9px; }
|
||||||
}
|
}
|
||||||
.effects-control { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; font-size: clamp(9px, 1.2vw, 13px); }
|
.effects-control { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; font-size: clamp(9px, 1.2vw, 13px); }
|
||||||
#effects-volume { width: 130px; accent-color: var(--accent); }
|
#effects-volume { width: 130px; accent-color: var(--accent); }
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// Le bruciate nel browser vero: dai volontari della Smarronata, la manovella girata col mouse e col dito, il sacco.
|
||||||
|
// npm run browser:bruciate — schermate in /tmp/regolo-bruciate.
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import { mkdirSync } from 'node:fs';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { staticServer } from './helpers.mjs';
|
||||||
|
|
||||||
|
const out = process.env.OUT ?? '/tmp/regolo-bruciate';
|
||||||
|
mkdirSync(out, { recursive: true });
|
||||||
|
const server = await staticServer();
|
||||||
|
let browser;
|
||||||
|
const errors = [];
|
||||||
|
try {
|
||||||
|
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
||||||
|
for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) {
|
||||||
|
const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile });
|
||||||
|
page.on('pageerror', e => errors.push(e.message));
|
||||||
|
await page.goto(`http://127.0.0.1:${server.port}/`);
|
||||||
|
await page.click('#splash'); await page.click('#new-game'); await page.click('#rione [data-rione="campomigliaio"]'); await page.click('#skip-intro');
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
const { settle } = await import('/tests/helpers.mjs');
|
||||||
|
settle(game); game.go('smarronata_1993'); settle(game);
|
||||||
|
game.act('talk', 'volontari'); settle(game);
|
||||||
|
game.choose(game.options().findIndex(o => o.text === 'Vi do una mano alla manovella?')); settle(game);
|
||||||
|
game.choose(game.options().findIndex(o => o.text === 'Alla bona.')); settle(game);
|
||||||
|
});
|
||||||
|
await page.waitForSelector('#bocce-controls:not([hidden])');
|
||||||
|
await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/smarronata-bruciatore.png'));
|
||||||
|
assert.equal(await page.locator('#bocce-throw').textContent(), 'Rovescia nel sacco');
|
||||||
|
assert.ok(await page.locator('#bruciate-help').isVisible());
|
||||||
|
assert.ok(!(await page.locator('#bocce-left').isVisible()), 'niente pulsanti Mira');
|
||||||
|
assert.ok(!(await page.locator('#bocce-power').isVisible()), 'niente forza');
|
||||||
|
await page.screenshot({ path: `${out}/${name}-ferma.png` });
|
||||||
|
const box = await page.locator('#scene').boundingBox(), P = (x, y) => [box.x + x / 640 * box.width, box.y + y / 360 * box.height];
|
||||||
|
// I giri attorno alla manovella: col mouse tenendo premuto, col dito trascinando (eventi di puntatore veri).
|
||||||
|
const loops = async (turns, steps = 16) => {
|
||||||
|
const at = i => { const a = i / steps * Math.PI * 2; return P(506 + Math.cos(a) * 40, 190 + Math.sin(a) * 40); };
|
||||||
|
if (mobile) {
|
||||||
|
const cdp = await page.context().newCDPSession(page), touch = (type, [x, y]) => cdp.send('Input.dispatchTouchEvent', { type, touchPoints: type === 'touchEnd' ? [] : [{ x, y }] });
|
||||||
|
await touch('touchStart', at(0));
|
||||||
|
for (let i = 1; i <= turns * steps; i++) await touch('touchMove', at(i));
|
||||||
|
await touch('touchEnd', at(0));
|
||||||
|
} else {
|
||||||
|
await page.mouse.move(...at(0)); await page.mouse.down();
|
||||||
|
for (let i = 1; i <= turns * steps; i++) await page.mouse.move(...at(i));
|
||||||
|
await page.mouse.up();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await loops(3);
|
||||||
|
const turned = await page.evaluate(() => Math.abs(game.closeup.drum));
|
||||||
|
assert.ok(turned > 6, `il cilindro ha girato: ${turned.toFixed(1)} rad`);
|
||||||
|
await page.screenshot({ path: `${out}/${name}-gira.png` });
|
||||||
|
// Il resto della cottura lo si fa fare al gioco a velocità giusta (le schermate con swiftshader sono lente).
|
||||||
|
await page.evaluate(() => { const c = game.closeup; c.speed = 4.5; for (const k of c.chestnuts) k.cook = 1.05; c.t = Math.max(c.t, 5); });
|
||||||
|
await page.waitForFunction(() => !document.querySelector('#bocce-throw').disabled);
|
||||||
|
await page.screenshot({ path: `${out}/${name}-cotte.png` });
|
||||||
|
if (mobile) await page.locator('#bocce-throw').tap(); else await page.locator('#bocce-throw').click();
|
||||||
|
await page.waitForFunction(() => game.closeup?.step === 'result', null, { timeout: 10000 });
|
||||||
|
assert.match(await page.locator('#bocce-status').textContent(), /bruciate buone su 18/);
|
||||||
|
await page.screenshot({ path: `${out}/${name}-sacco.png` });
|
||||||
|
await page.locator('#closeup-close').click();
|
||||||
|
await page.waitForFunction(() => game.state.flags.bruciate_fatte);
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
console.log(`ok: schermate in ${out}`);
|
||||||
|
} finally { await browser?.close(); server.kill(); }
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as m from '../src/engine/bruciate.js';
|
||||||
|
import { drawBruciate } from '../src/engine/bruciate-render.js';
|
||||||
|
import { Game } from '../src/engine/game.js';
|
||||||
|
import { story } from '../src/story/story.js';
|
||||||
|
import { settle } from './helpers.mjs';
|
||||||
|
|
||||||
|
// Gira la manovella tenendola, a `w` radianti al secondo, finché le castagne non paiono cotte (o per `secs` secondi).
|
||||||
|
function crank(c, w, { secs = 60, until = 1.05 } = {}) {
|
||||||
|
c.grab ??= { pointerId: 1, heading: null, last: null };
|
||||||
|
for (let i = 0; i < secs * 60 && c.step === 'turn' && m.doneness(c) < until; i++) { c.pending = w / 60; m.update(c, 1 / 60); }
|
||||||
|
}
|
||||||
|
function pourAndWait(c) {
|
||||||
|
assert.ok(m.shoot(c), 'si rovescia nel sacco');
|
||||||
|
for (let i = 0; i < 200 && c.step === 'pour'; i++) m.update(c, 1 / 60);
|
||||||
|
assert.equal(c.step, 'result');
|
||||||
|
return c.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('girando a modo le bruciate vengono tutte buone, nel tempo del livello', () => {
|
||||||
|
for (const level of ['bona', 'seria', 'male']) {
|
||||||
|
const c = m.newBruciate(42, { level });
|
||||||
|
crank(c, 4.5);
|
||||||
|
assert.ok(c.t > 6 && c.t < m.LEVELS[level].time, `${level}: cotte in ${c.t.toFixed(1)} s`);
|
||||||
|
const r = pourAndWait(c);
|
||||||
|
assert.equal(r.winner, 'player', `${level}: ${JSON.stringify(r)}`);
|
||||||
|
assert.equal(r.spilled, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ferme sul fuoco: quelle di sotto carbone, quelle di sopra crude', () => {
|
||||||
|
const c = m.newBruciate(42, { level: 'seria' });
|
||||||
|
crank(c, 0);
|
||||||
|
const r = pourAndWait(c);
|
||||||
|
assert.equal(r.winner, 'cpu');
|
||||||
|
assert.ok(r.burnt > 0 && r.raw > 0, JSON.stringify(r));
|
||||||
|
assert.match(m.status(c), /Ne servivano 15/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('troppo forte è una centrifuga: le castagne volano nel fuoco', () => {
|
||||||
|
const c = m.newBruciate(42, { level: 'seria' });
|
||||||
|
crank(c, 12, { secs: 4, until: 9 });
|
||||||
|
assert.ok(c.chestnuts.some(k => k.out), 'qualcuna è volata fuori');
|
||||||
|
assert.ok(c.flying.length > 0);
|
||||||
|
assert.match(c.line, /centrifuga|fuoco/);
|
||||||
|
assert.ok(c.events.some(e => e.kind === 'bruciate-cade'));
|
||||||
|
assert.equal(m.speedWord(c), 'troppo forte');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rovesciarle troppo presto: crude; e il pulsante aspetta i primi secondi', () => {
|
||||||
|
const c = m.newBruciate(42, { level: 'bona' });
|
||||||
|
assert.ok(!m.canThrow(c) && !m.shoot(c), 'appena cominciato non si rovescia');
|
||||||
|
crank(c, 4.5, { until: 0.4 });
|
||||||
|
const r = pourAndWait(c);
|
||||||
|
assert.equal(r.winner, 'cpu'); assert.ok(r.raw > 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il gesto: i giri, grandi o piccoli e dovunque, fanno girare il cilindro; una riga dritta no', () => {
|
||||||
|
const circles = (c, cx, cy, radius, turns, secs) => {
|
||||||
|
assert.ok(m.press(c, 7));
|
||||||
|
const steps = Math.round(secs * 60);
|
||||||
|
for (let i = 0; i <= steps; i++) {
|
||||||
|
const a = i / steps * turns * Math.PI * 2;
|
||||||
|
m.aimScreen(c, [cx + Math.cos(a) * radius, cy + Math.sin(a) * radius]);
|
||||||
|
m.update(c, 1 / 60);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const [x, y, r] of [[506, 190, 30], [200, 300, 80], [320, 180, 15]]) {
|
||||||
|
const c = m.newBruciate(1);
|
||||||
|
m.aimScreen(c, [x + r, y]);
|
||||||
|
circles(c, x, y, r, 2, 2);
|
||||||
|
assert.ok(Math.abs(c.speed) > 4 && Math.abs(c.speed) < 9, `giri di raggio ${r}: ${c.speed.toFixed(2)} rad/s`);
|
||||||
|
assert.ok(Math.abs(c.drum) > 6);
|
||||||
|
// Lasciata la manovella, il cilindro rallenta da sé.
|
||||||
|
assert.ok(m.release(c, 7));
|
||||||
|
const w = Math.abs(c.speed);
|
||||||
|
for (let i = 0; i < 60; i++) m.update(c, 1 / 60);
|
||||||
|
assert.ok(Math.abs(c.speed) < w * 0.5);
|
||||||
|
}
|
||||||
|
const c = m.newBruciate(1);
|
||||||
|
m.aimScreen(c, [100, 200]); m.press(c, 1);
|
||||||
|
for (let i = 0; i < 120; i++) { m.aimScreen(c, [100 + i * 3, 200]); m.update(c, 1 / 60); }
|
||||||
|
assert.ok(Math.abs(c.speed) < 0.2, 'trascinare dritto non gira');
|
||||||
|
assert.ok(!m.release(c, 99), 'un altro dito non lascia la manovella');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('senza gesto: Spazio e le frecce danno una spinta, Invio rovescia', () => {
|
||||||
|
const c = m.newBruciate(3);
|
||||||
|
assert.ok(m.key(c, ' ', true));
|
||||||
|
assert.ok(Math.abs(c.speed) >= m.PUSH);
|
||||||
|
assert.ok(m.key(c, 'ArrowRight', true)); assert.ok(Math.abs(c.speed) >= m.PUSH * 1.5);
|
||||||
|
assert.ok(m.key(c, ' ', false), 'anche lasciando il tasto, il tasto è suo');
|
||||||
|
assert.ok(!m.key(c, 'x', true));
|
||||||
|
for (let i = 0; i < 60 * m.MIN_POUR + 2; i++) m.update(c, 1 / 60);
|
||||||
|
m.key(c, 'Enter', true);
|
||||||
|
assert.equal(c.step, 'pour');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il disegno delle bruciate non cambia lo stato', () => {
|
||||||
|
const calls = [], ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => calls.push([k, ...args]) });
|
||||||
|
const c = m.newBruciate(7, { level: 'bona' }), bg = {};
|
||||||
|
const draw = () => { const before = structuredClone(c); drawBruciate(ctx, c, story.closeups.bruciate, bg); assert.deepEqual(c, before); };
|
||||||
|
draw();
|
||||||
|
assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'GIRA!'), 'da fermo, l\'invito a girare');
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'COTTURA'));
|
||||||
|
crank(c, 12, { secs: 2, until: 9 }); draw();
|
||||||
|
crank(c, 4.5); pourAndWait(c); calls.length = 0; draw();
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && /BRUCIATE/.test(t)));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('alla Smarronata: dai volontari, «Vi do una mano alla manovella?», il livello e le bruciate', () => {
|
||||||
|
const g = new Game(story); g.go('smarronata_1993'); settle(g);
|
||||||
|
g.act('talk', 'volontari'); settle(g);
|
||||||
|
assert.ok(g.options().some(o => o.text.startsWith('Che Iddio ve ne renda merito')), 'il ringraziamento resta');
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'Vi do una mano alla manovella?')); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'Seria.')); settle(g);
|
||||||
|
assert.equal(g.closeup?.id, 'bruciate'); assert.equal(g.closeup.level, 'seria');
|
||||||
|
crank(g.closeup, 4.5); pourAndWait(g.closeup);
|
||||||
|
g.closeupClose(); settle(g);
|
||||||
|
assert.ok(g.state.flags.bruciate_fatte);
|
||||||
|
// Anche usando il cilindro si chiede la manovella; uscendo prima, la battuta di chi lascia.
|
||||||
|
g.act('use', 'bruciatore'); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'Vi do una mano alla manovella?')); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'Alla bona.')); settle(g);
|
||||||
|
assert.equal(g.closeup?.id, 'bruciate');
|
||||||
|
g.closeupClose();
|
||||||
|
assert.equal(g.queue[0]?.say, story.closeups.bruciate.quit[0].say);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user