Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f0f60d186 | ||
|
|
5135689293 | ||
|
|
8693bff0c7 | ||
|
|
c6812db291 | ||
|
|
670626afa5 | ||
|
|
9bf41d01af | ||
|
|
16252fb04a | ||
|
|
99d4dede80 | ||
|
|
36eda83fe4 |
Binary file not shown.
|
After Width: | Height: | Size: 324 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 71 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.
Binary file not shown.
@@ -1887,6 +1887,38 @@
|
|||||||
"file": "andrea-d38b6e49ef.mp3",
|
"file": "andrea-d38b6e49ef.mp3",
|
||||||
"sec": 3.49
|
"sec": 3.49
|
||||||
},
|
},
|
||||||
|
"andrea|Lo zio Mario nel suo orto, col sole che va giù dietro la casa.": {
|
||||||
|
"file": "andrea-9309e50d5c.mp3",
|
||||||
|
"sec": 3.78
|
||||||
|
},
|
||||||
|
"zio_mario|Ti s'aspetta a cena, che c'è la vitella di maiale.": {
|
||||||
|
"file": "zio_mario-39ff0c666f.mp3",
|
||||||
|
"sec": 2.71
|
||||||
|
},
|
||||||
|
"andrea|Non ti avevo visto, pensavo tu fossi andato all'estero.": {
|
||||||
|
"file": "andrea-f847f224df.mp3",
|
||||||
|
"sec": 3.35
|
||||||
|
},
|
||||||
|
"zio_mario|Io ho girato tanto... il Falcione.": {
|
||||||
|
"file": "zio_mario-d0d592db52.mp3",
|
||||||
|
"sec": 2.73
|
||||||
|
},
|
||||||
|
"andrea|Zio, che sei stanco?": {
|
||||||
|
"file": "andrea-21195c6cac.mp3",
|
||||||
|
"sec": 2.31
|
||||||
|
},
|
||||||
|
"zio_mario|No, dormo!": {
|
||||||
|
"file": "zio_mario-a852e9c43f.mp3",
|
||||||
|
"sec": 1.19
|
||||||
|
},
|
||||||
|
"andrea|Zio, a me mi garba Michael Jackson!": {
|
||||||
|
"file": "andrea-599b75d7c7.mp3",
|
||||||
|
"sec": 2.6
|
||||||
|
},
|
||||||
|
"zio_mario|Mai col gesso!": {
|
||||||
|
"file": "zio_mario-5773e3350f.mp3",
|
||||||
|
"sec": 1.13
|
||||||
|
},
|
||||||
"andrea|Le case coi balconi pieni di vasi: qui ogni finestra è un giardino.": {
|
"andrea|Le case coi balconi pieni di vasi: qui ogni finestra è un giardino.": {
|
||||||
"file": "andrea-ec81875c47.mp3",
|
"file": "andrea-ec81875c47.mp3",
|
||||||
"sec": 5.22
|
"sec": 5.22
|
||||||
@@ -2803,6 +2835,10 @@
|
|||||||
"file": "andrea-3f77b83f35.mp3",
|
"file": "andrea-3f77b83f35.mp3",
|
||||||
"sec": 0.87
|
"sec": 0.87
|
||||||
},
|
},
|
||||||
|
"andrea|E una partita a campana, Taglia?": {
|
||||||
|
"file": "andrea-595df0a426.mp3",
|
||||||
|
"sec": 2.23
|
||||||
|
},
|
||||||
"andrea|Ma quanto sei alto, Taglia?": {
|
"andrea|Ma quanto sei alto, Taglia?": {
|
||||||
"file": "andrea-01ecdf3ed8.mp3",
|
"file": "andrea-01ecdf3ed8.mp3",
|
||||||
"sec": 1.96
|
"sec": 1.96
|
||||||
@@ -2827,6 +2863,14 @@
|
|||||||
"file": "taglia-ac012a1066.mp3",
|
"file": "taglia-ac012a1066.mp3",
|
||||||
"sec": 4.21
|
"sec": 4.21
|
||||||
},
|
},
|
||||||
|
"taglia|A campana? Alla nostra età?": {
|
||||||
|
"file": "taglia-0a7f69770d.mp3",
|
||||||
|
"sec": 2.77
|
||||||
|
},
|
||||||
|
"taglia|Va bene: il gesso ce l'ho in tasca, il sassolino lo scelgo io. Sul vialetto, dalla 1 alla 8, e chi pesta la riga passa la mano.": {
|
||||||
|
"file": "taglia-c00be84faf.mp3",
|
||||||
|
"sec": 11.54
|
||||||
|
},
|
||||||
"taglia|Abbastanza da vedere i pallini arrivare prima di te.": {
|
"taglia|Abbastanza da vedere i pallini arrivare prima di te.": {
|
||||||
"file": "taglia-d99c875252.mp3",
|
"file": "taglia-d99c875252.mp3",
|
||||||
"sec": 2.57
|
"sec": 2.57
|
||||||
@@ -2843,6 +2887,18 @@
|
|||||||
"file": "taglia-4117f6613d.mp3",
|
"file": "taglia-4117f6613d.mp3",
|
||||||
"sec": 4.84
|
"sec": 4.84
|
||||||
},
|
},
|
||||||
|
"taglia|Alla bona: le righe un contano. Comincia te.": {
|
||||||
|
"file": "taglia-328d930124.mp3",
|
||||||
|
"sec": 4.12
|
||||||
|
},
|
||||||
|
"taglia|Seria: chi tocca la riga è fuori. Comincia te.": {
|
||||||
|
"file": "taglia-1266463538.mp3",
|
||||||
|
"sec": 4.23
|
||||||
|
},
|
||||||
|
"taglia|A fassi male? Io la campana la facevo a occhi chiusi.": {
|
||||||
|
"file": "taglia-7938d8d5e4.mp3",
|
||||||
|
"sec": 5.05
|
||||||
|
},
|
||||||
"andrea|Si fa una gara alle macchinine?!?!": {
|
"andrea|Si fa una gara alle macchinine?!?!": {
|
||||||
"file": "andrea-17216997e2.mp3",
|
"file": "andrea-17216997e2.mp3",
|
||||||
"sec": 2.74
|
"sec": 2.74
|
||||||
@@ -3255,9 +3311,9 @@
|
|||||||
"file": "bertovo-ca941bee91.mp3",
|
"file": "bertovo-ca941bee91.mp3",
|
||||||
"sec": 5.41
|
"sec": 5.41
|
||||||
},
|
},
|
||||||
"vannino|M'hai pelato, ragazzo. Nel 1997 un ci si vedeva da queste parti, con quella faccia.": {
|
"vannino|Tu m'ha pelato. La prossima volta porta il Barone.": {
|
||||||
"file": "vannino-e68fb40eba.mp3",
|
"file": "vannino-2267bb5adf.mp3",
|
||||||
"sec": 7.85
|
"sec": 3.18
|
||||||
},
|
},
|
||||||
"pierino_parigi|Le fiches restano al banco. E tu torna quando hai imparato a bluffare.": {
|
"pierino_parigi|Le fiches restano al banco. E tu torna quando hai imparato a bluffare.": {
|
||||||
"file": "pierino_parigi-a80d04fe5b.mp3",
|
"file": "pierino_parigi-a80d04fe5b.mp3",
|
||||||
@@ -3311,6 +3367,26 @@
|
|||||||
"file": "andrea-aba12c5241.mp3",
|
"file": "andrea-aba12c5241.mp3",
|
||||||
"sec": 3.1
|
"sec": 3.1
|
||||||
},
|
},
|
||||||
|
"andrea|Dalla 1 fino in cima, e ritorno. A ventisett'anni, davanti alle poste.": {
|
||||||
|
"file": "andrea-68378e4c21.mp3",
|
||||||
|
"sec": 6.15
|
||||||
|
},
|
||||||
|
"taglia|Te lo dico io: se ti vede qualcuno, domani lo sa tutto il paese.": {
|
||||||
|
"file": "taglia-1e6660932d.mp3",
|
||||||
|
"sec": 3.34
|
||||||
|
},
|
||||||
|
"andrea|Il Taglia, su un piede solo, è più alto di me su due.": {
|
||||||
|
"file": "andrea-3ed6165062.mp3",
|
||||||
|
"sec": 3.31
|
||||||
|
},
|
||||||
|
"taglia|È questione di gambe lunghe.": {
|
||||||
|
"file": "taglia-9782816264.mp3",
|
||||||
|
"sec": 1.71
|
||||||
|
},
|
||||||
|
"andrea|Il gesso lo lascio lì. Ci giocherà qualche bambino, che è più in allenamento di me.": {
|
||||||
|
"file": "andrea-356a1afe8d.mp3",
|
||||||
|
"sec": 5.91
|
||||||
|
},
|
||||||
"chiaru|Hai vinto te. Ma la mia aveva le ruote piene di gesso, sia chiaro.": {
|
"chiaru|Hai vinto te. Ma la mia aveva le ruote piene di gesso, sia chiaro.": {
|
||||||
"file": "chiaru-91a4b23b22.mp3",
|
"file": "chiaru-91a4b23b22.mp3",
|
||||||
"sec": 4.44
|
"sec": 4.44
|
||||||
|
|||||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -36,7 +36,9 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p
|
|||||||
| `characters/zio_mario.png` | Codex | `foto/zio-mario.png` | Andrea (29 ottobre) | sì (famiglia di Andrea) |
|
| `characters/zio_mario.png` | Codex | `foto/zio-mario.png` | Andrea (29 ottobre) | sì (famiglia di Andrea) |
|
||||||
| `terrazza-cardetole.png` | Codex (dalla foto, poi il terrazzo davanti) | `cardetole-finestra.jpg` | Andrea (29 ottobre) | **da decidere**: come `cardetole.png`, dice dove abita Andrea |
|
| `terrazza-cardetole.png` | Codex (dalla foto, poi il terrazzo davanti) | `cardetole-finestra.jpg` | Andrea (29 ottobre) | **da decidere**: come `cardetole.png`, dice dove abita Andrea |
|
||||||
| `closeups/muro-cerbottana.png` | Codex, solo testo (stile dalla piazza) | nessuna | — | sì |
|
| `closeups/muro-cerbottana.png` | Codex, solo testo (stile dalla piazza) | nessuna | — | sì |
|
||||||
|
| `closeups/macchina-dietro.png`, `macchina-lampi.png`, `macchina-davanti.png` | Codex, dallo sprite della macchina del tempo | nessuna | — | sì |
|
||||||
| `closeups/campone-condor.png` | Codex, dallo sfondo del campone delle poste | nessuna | — | sì |
|
| `closeups/campone-condor.png` | Codex, dallo sfondo del campone delle poste | nessuna | — | sì |
|
||||||
|
| `closeups/campone-campana.png` | Codex, solo testo (stile dal campone delle poste) | nessuna | — | sì |
|
||||||
| `barrettino-1997.png`, `closeups/cruscotto-tempo.png` | Codex, solo testo | nessuna | — | sì |
|
| `barrettino-1997.png`, `closeups/cruscotto-tempo.png` | Codex, solo testo | nessuna | — | sì |
|
||||||
| `characters/vannino.png`, `characters/pierino_parigi.png` | Codex, dalla descrizione di Andrea (niente foto) | nessuna | — | sì (Pierino: consenso del figlio Marco) |
|
| `characters/vannino.png`, `characters/pierino_parigi.png` | Codex, dalla descrizione di Andrea (niente foto) | nessuna | — | sì (Pierino: consenso del figlio Marco) |
|
||||||
| `cardetole.png` | Codex | `cardetole.jpg` | Andrea (EXIF) | **da decidere**: è la casa vera di Andrea, pubblicarla dice dove abita |
|
| `cardetole.png` | Codex | `cardetole.jpg` | Andrea (EXIF) | **da decidere**: è la casa vera di Andrea, pubblicarla dice dove abita |
|
||||||
|
|||||||
@@ -18,6 +18,22 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
|||||||
|
|
||||||
## Dove siamo arrivati
|
## Dove siamo arrivati
|
||||||
|
|
||||||
|
- **La campana al campone delle poste (30 ottobre)**: dal Taglia, «E una partita a campana, Taglia?», poi il livello.
|
||||||
|
Primo piano `campana` visto dall'alto sul vialetto (`campone-campana.png`, Codex); la campana col gesso la disegna il
|
||||||
|
codice (`campana.js`, `campana-render.js`): 1-2-3, coppia 4-5, 6, coppia 7-8 e in cima il mezzo cerchio con scritto
|
||||||
|
CAMPANA (Andrea: non «cielo»), ogni casella tinta tenue col suo gesso. Il sassolino: la mira dà la direzione, tenendo
|
||||||
|
premuto la forza va e torna, si lascia; deve cadere nella casella che tocca senza toccare le righe (alla bona le
|
||||||
|
righe non contano e il puntino giallo mostra dove cade). I salti: la lancetta dell'equilibrio attraversa la barra e
|
||||||
|
si preme nel verde (presto: giù l'altro piede; tardi: pestata la riga); si scavalca la casella col sassolino e
|
||||||
|
tornando lo si raccoglie. Chi ce la fa passa alla casella dopo e ritira, chi sbaglia passa la mano; il Taglia gioca
|
||||||
|
da solo (in simulazione gli servono circa 22, 10 e 6 turni secondo il livello). Vince chi finisce la 8 (flag
|
||||||
|
`campana_vinta`). Test `campana.test.mjs`, prova `npm run browser:campana`. [DA ANDREA] le battute.
|
||||||
|
- **La macchina del tempo da fuori (30 ottobre)**: dopo il cruscotto, la partenza giù per Via Liguri Magelli
|
||||||
|
(primo piano `corsa`, `src/engine/corsa.js`): accelera, il fulmine la prende, il bianco, e restano due sgommate di
|
||||||
|
fuoco che bruciano e si spengono; al ritorno dal 1997 l'arrivo: il fulmine in fondo alla strada, lei viene avanti e
|
||||||
|
ci passa accanto lasciando le strisce. La macchina da dietro e davanti (coi lampi) è di Codex.
|
||||||
|
- **Lo zio Mario negli orti al tramonto (30 ottobre)**: dal tramonto (`indizio_regolo`) non è più davanti alla
|
||||||
|
Ghiacciomobile ma accanto al suo orto (`orti_zio_mario`), con una battuta provvisoria [DA ANDREA].
|
||||||
- **La Condor al campone delle poste (29 ottobre)**: la gara col Taglia (Andrea Tagliaferri, consenso dato) con la
|
- **La Condor al campone delle poste (29 ottobre)**: la gara col Taglia (Andrea Tagliaferri, consenso dato) con la
|
||||||
pistola ad aria compressa coi pallini gialli (`condor.js`, `condor-render.js`, primo piano `condor`, sfondo di Codex).
|
pistola ad aria compressa coi pallini gialli (`condor.js`, `condor-render.js`, primo piano `condor`, sfondo di Codex).
|
||||||
Come la cerbottana: il mirino ondeggia col respiro; tenere premuto trattiene il fiato (il mirino si ferma), lasciare
|
Come la cerbottana: il mirino ondeggia col respiro; tenere premuto trattiene il fiato (il mirino si ferma), lasciare
|
||||||
|
|||||||
+11
@@ -283,6 +283,17 @@
|
|||||||
mira un po' più sopra; alla bona il puntino giallo segna dove arriva. Il palloncino e la lattina in aria si
|
mira un po' più sopra; alla bona il puntino giallo segna dove arriva. Il palloncino e la lattina in aria si
|
||||||
muovono: si mira dove saranno. Si può anche premere Spara, senza trattenere il fiato. Frecce per spostare il mirino.</p>
|
muovono: si mira dove saranno. Si può anche premere Spara, senza trattenere il fiato. Frecce per spostare il mirino.</p>
|
||||||
</details>
|
</details>
|
||||||
|
<details id="campana-help" hidden><summary>Come si gioca</summary>
|
||||||
|
<p>La campana col gesso sul vialetto del campone, a turni col Taglia. Si tira il sassolino nella casella che tocca,
|
||||||
|
dalla 1 alla 8: deve cadere dentro, senza toccare le righe (alla bona le righe non contano). Poi si salta tutta la
|
||||||
|
campana fino in cima (il mezzo cerchio) e indietro, su un piede nelle caselle sole e a gambe larghe nella 4-5 e nella 7-8, senza mai
|
||||||
|
mettere il piede dove sta il sassolino; tornando lo si raccoglie dalla casella prima. Chi ce la fa passa alla casella
|
||||||
|
dopo e ritira; chi sbaglia passa la mano. Vince chi finisce la 8.</p>
|
||||||
|
<p>Il tiro: muovi il mouse o tocca la campana per mirare, tieni premuto per la forza (sale e scende) e lascia; oppure
|
||||||
|
regola la forza e premi Tira. Alla bona il puntino giallo segna dove cade. Frecce su e giù per la mira, destra e
|
||||||
|
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>
|
||||||
|
</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
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "leggenda-del-regolo",
|
"name": "leggenda-del-regolo",
|
||||||
"version": "0.1.54",
|
"version": "0.1.57",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"browser:gara": "node tests/gara.browser.mjs",
|
"browser:gara": "node tests/gara.browser.mjs",
|
||||||
"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: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",
|
||||||
"browser:briscola": "node tests/briscola.browser.mjs",
|
"browser:briscola": "node tests/briscola.browser.mjs",
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { W, CY, START, CIELO, FROM, BOXES, LAST, FLIGHT, HOP, center, landing, needle, canAdjust } from './campana.js';
|
||||||
|
import { text, panel } from './bocce-render.js';
|
||||||
|
|
||||||
|
function rect(ctx, x, y, w, h, color) { ctx.fillStyle = color; ctx.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h)); }
|
||||||
|
// Il gesso: tratti a puntini un po' storti, con dei buchi (sempre gli stessi, da un piccolo hash: non tremano).
|
||||||
|
const hash = n => { const s = Math.sin(n * 127.1 + 311.7) * 43758.5453; return s - Math.floor(s); };
|
||||||
|
function chalkLine(ctx, x1, y1, x2, y2, seed) {
|
||||||
|
const n = Math.max(2, Math.round(Math.hypot(x2 - x1, y2 - y1)));
|
||||||
|
for (let i = 0; i <= n; i++) {
|
||||||
|
const k = i / n, h = hash(seed * 1000 + i);
|
||||||
|
if (h < 0.07) continue;
|
||||||
|
ctx.fillStyle = h > 0.8 ? 'rgba(255,255,250,0.95)' : 'rgba(240,238,228,0.78)';
|
||||||
|
ctx.fillRect(Math.round(x1 + (x2 - x1) * k + (hash(seed + i) - 0.5)), Math.round(y1 + (y2 - y1) * k + (hash(seed * 7 + i) - 0.5)), 2, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// I gessi colorati: ogni casella ripassata tenue col suo colore, a puntini come il gesso sull'asfalto (idea di Andrea).
|
||||||
|
const TINTS = { 1: [244, 160, 180], 2: [250, 222, 120], 3: [150, 200, 240], 4: [160, 220, 150], 5: [200, 170, 230], 6: [250, 180, 120], 7: [140, 220, 210], 8: [240, 150, 150], top: [255, 240, 200] };
|
||||||
|
function tint(ctx, [r, g, b], x, y, w, h, seed, inside = () => true) {
|
||||||
|
ctx.fillStyle = `rgba(${r},${g},${b},0.16)`;
|
||||||
|
for (let yy = y; yy < y + h; yy += 2) for (let xx = x; xx < x + w; xx += 2) {
|
||||||
|
if (!inside(xx, yy)) continue;
|
||||||
|
const k = hash(seed * 7777 + xx * 13 + yy * 7);
|
||||||
|
if (k < 0.18) continue; // l'asfalto che spunta fra il gesso
|
||||||
|
ctx.fillStyle = `rgba(${r},${g},${b},${k > 0.85 ? 0.42 : 0.24})`; ctx.fillRect(xx, yy, 2, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Il gesso non cambia mai: tinte e righe si disegnano una volta su una tela a parte (a puntini, a ogni fotogramma,
|
||||||
|
// rallentavano la pagina). Fuori dal browser (nei test) si disegnano direttamente.
|
||||||
|
let layer = null;
|
||||||
|
function chalkwork(ctx) {
|
||||||
|
let s = 1;
|
||||||
|
for (const [id, b] of Object.entries(BOXES)) tint(ctx, TINTS[id], b.x + 1, b.y + 1, W - 2, W - 2, Number(id));
|
||||||
|
tint(ctx, TINTS.top, CIELO.x + 1, CY - CIELO.r, CIELO.r, CIELO.r * 2, 9, (x, y) => Math.hypot(x + 1 - CIELO.x, y + 1 - CY) < CIELO.r - 2);
|
||||||
|
for (const b of Object.values(BOXES)) {
|
||||||
|
chalkLine(ctx, b.x, b.y, b.x + W, b.y, s++); chalkLine(ctx, b.x, b.y + W, b.x + W, b.y + W, s++);
|
||||||
|
chalkLine(ctx, b.x, b.y, b.x, b.y + W, s++); chalkLine(ctx, b.x + W, b.y, b.x + W, b.y + W, s++);
|
||||||
|
}
|
||||||
|
// In cima la campana: il mezzo cerchio in fondo, dove ci si gira.
|
||||||
|
for (let i = 0; i < 40; i++) {
|
||||||
|
const a1 = -Math.PI / 2 + i / 40 * Math.PI, a2 = -Math.PI / 2 + (i + 1) / 40 * Math.PI;
|
||||||
|
chalkLine(ctx, CIELO.x + Math.cos(a1) * CIELO.r, CY + Math.sin(a1) * CIELO.r, CIELO.x + Math.cos(a2) * CIELO.r, CY + Math.sin(a2) * CIELO.r, 200 + i);
|
||||||
|
}
|
||||||
|
// La riga di partenza.
|
||||||
|
chalkLine(ctx, START - 14, CY - W, START - 14, CY + W, 300);
|
||||||
|
}
|
||||||
|
function grid(ctx, c) {
|
||||||
|
if (typeof document === 'undefined') chalkwork(ctx);
|
||||||
|
else {
|
||||||
|
if (!layer) { layer = document.createElement('canvas'); layer.width = 640; layer.height = 360; chalkwork(layer.getContext('2d')); }
|
||||||
|
ctx.drawImage(layer, 0, 0);
|
||||||
|
}
|
||||||
|
// Il numero in alto nella casella: il mezzo resta libero per il sassolino e i piedi. Quella che tocca ha il bordo giallo.
|
||||||
|
for (const [id, b] of Object.entries(BOXES)) {
|
||||||
|
const p = center(id), next = Number(id) === c.target[c.current] && c.step !== 'result';
|
||||||
|
if (next) { ctx.strokeStyle = 'rgba(255,236,140,0.55)'; ctx.lineWidth = 2; ctx.strokeRect(b.x + 4, b.y + 4, W - 8, W - 8); }
|
||||||
|
text(ctx, id, p.x, p.y - 17, 14, next ? '#fff3a8' : 'rgba(245,243,232,0.88)', 'center');
|
||||||
|
}
|
||||||
|
text(ctx, 'CAMPANA', CIELO.x + W * 0.53, CY - 26, 8, 'rgba(245,243,232,0.88)', 'center');
|
||||||
|
}
|
||||||
|
// Il sassolino: grigio, col lume in alto; in volo, più su e con l'ombra sotto.
|
||||||
|
function stone(ctx, x, y, lift = 0) {
|
||||||
|
ctx.fillStyle = 'rgba(30,25,20,0.35)'; ctx.beginPath(); ctx.ellipse(x + 1, y + 2, 5, 3, 0, 0, Math.PI * 2); ctx.fill();
|
||||||
|
const s = 1 + lift / 60, yy = y - lift;
|
||||||
|
ctx.fillStyle = '#4a4744'; ctx.beginPath(); ctx.ellipse(x, yy, 5 * s, 4 * s, 0.3, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.fillStyle = '#8d8984'; ctx.beginPath(); ctx.ellipse(x - 0.5, yy - 0.5, 4 * s, 3 * s, 0.3, 0, Math.PI * 2); ctx.fill();
|
||||||
|
rect(ctx, x - 2 * s, yy - 2 * s, 2, 1, '#cfcac2');
|
||||||
|
}
|
||||||
|
// Dove stanno i piedi: a gambe larghe nelle coppie e nel cielo, su un piede nelle caselle sole, alla partenza tutti e due.
|
||||||
|
function feet(beat) {
|
||||||
|
if (!beat || beat.out) return [{ x: FROM.x + 2, y: CY - 9 }, { x: FROM.x + 2, y: CY + 9 }];
|
||||||
|
if (beat.boxes[0] === 'cielo') { const p = center('cielo'); return [{ x: p.x, y: p.y - 12 }, { x: p.x, y: p.y + 12 }]; }
|
||||||
|
return beat.boxes.map(id => center(id));
|
||||||
|
}
|
||||||
|
// Una scarpa vista dall'alto: la punta verso dove si va.
|
||||||
|
function shoe(ctx, x, y, dir, scale, [sole, upper, lace]) {
|
||||||
|
ctx.save(); ctx.translate(Math.round(x), Math.round(y)); ctx.scale(dir * scale, scale);
|
||||||
|
ctx.fillStyle = 'rgba(30,25,20,0.3)'; ctx.fillRect(-9, -3, 21, 9);
|
||||||
|
ctx.fillStyle = sole; ctx.fillRect(-10, -5, 20, 10); ctx.fillRect(10, -4, 2, 8);
|
||||||
|
ctx.fillStyle = upper; ctx.fillRect(-9, -4, 18, 8);
|
||||||
|
ctx.fillStyle = lace; ctx.fillRect(-1, -2, 7, 1); ctx.fillRect(-1, 1, 7, 1);
|
||||||
|
ctx.fillStyle = '#2a2420'; ctx.fillRect(-8, -2, 4, 4); // la caviglia, il buco della scarpa
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
const SHOES = { player: ['#e8e6e0', '#3d6fb8', '#ffffff'], cpu: ['#2b2b2b', '#b8322a', '#f0d8a8'] };
|
||||||
|
function walker(ctx, c) {
|
||||||
|
const beat = c.beats?.[c.beat], prev = c.beat > 0 ? c.beats[c.beat - 1] : null, now = feet(c.beat >= 0 ? beat : null);
|
||||||
|
const k = c.hop ? Math.min(1, c.hop.t / HOP) : 1, from = feet(c.beat > 0 ? prev : null);
|
||||||
|
const mid = list => list.reduce((a, p) => ({ x: a.x + p.x / list.length, y: a.y + p.y / list.length }), { x: 0, y: 0 });
|
||||||
|
const a = mid(from), b = mid(now), off = { x: (a.x - b.x) * (1 - k), y: (a.y - b.y) * (1 - k) };
|
||||||
|
const scale = (c.current === 'cpu' ? 1.45 : 1.3) * (1 + 0.35 * Math.sin(Math.PI * k));
|
||||||
|
const dir = beat?.back ? -1 : 1;
|
||||||
|
for (const p of now) shoe(ctx, p.x + off.x, p.y + off.y, dir, scale, SHOES[c.current]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawCampana(ctx, c, def, bg) {
|
||||||
|
ctx.save(); ctx.imageSmoothingEnabled = false;
|
||||||
|
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#9a948a'; ctx.fillRect(0, 0, 640, 360); }
|
||||||
|
grid(ctx, c);
|
||||||
|
if (c.stone) stone(ctx, c.stone.x, c.stone.y);
|
||||||
|
walker(ctx, c);
|
||||||
|
if (c.shot) {
|
||||||
|
const k = Math.min(1, c.shot.t / FLIGHT), { from, to } = c.shot;
|
||||||
|
stone(ctx, from.x + (to.x - from.x) * k, from.y + (to.y - from.y) * k, 40 * Math.sin(Math.PI * k));
|
||||||
|
}
|
||||||
|
// La mira: un cerchietto di gesso dove si punta; alla bona, il puntino dove cade il sassolino con questa forza.
|
||||||
|
if (canAdjust(c)) {
|
||||||
|
ctx.strokeStyle = '#fff8dc'; ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath(); ctx.arc(c.aim.x, c.aim.y, 6, 0, Math.PI * 2); ctx.stroke();
|
||||||
|
if (c.skill.guide) { const l = landing(c); rect(ctx, l.x - 2, l.y - 2, 4, 4, '#f5d02a'); rect(ctx, l.x - 1, l.y - 1, 2, 2, '#7a5a10'); }
|
||||||
|
// La forza: la barra in basso a sinistra.
|
||||||
|
const x = 16, y = 296, h = 50, f = Math.round(h * c.power);
|
||||||
|
rect(ctx, x - 3, y - 3, 18, h + 6, '#17160fd0');
|
||||||
|
rect(ctx, x, y + h - f, 12, f, c.holding ? '#ffd23c' : '#d9ccba');
|
||||||
|
text(ctx, 'FORZA', x + 6, y - 9, 6, '#efd49e', 'center');
|
||||||
|
}
|
||||||
|
// L'equilibrio: la lancetta attraversa la barra; si salta quando è nel verde.
|
||||||
|
if (c.step === 'hop' && c.meter) {
|
||||||
|
const x = 220, y = 318, w = 200, zone = c.skill.zone * w / 2, u = needle(c);
|
||||||
|
rect(ctx, x - 4, y - 14, w + 8, 30, '#17160fd8');
|
||||||
|
rect(ctx, x, y, w, 8, '#7a3a2a'); rect(ctx, x + w / 2 - zone, y, zone * 2, 8, '#5fae4a');
|
||||||
|
rect(ctx, x + w / 2 + u * w / 2 - 1, y - 3, 3, 14, '#fff3a8');
|
||||||
|
text(ctx, 'EQUILIBRIO', x + w / 2, y - 7, 6, '#efd49e', 'center');
|
||||||
|
}
|
||||||
|
// In alto: a che casella è ciascuno, di chi è il turno, cosa è successo.
|
||||||
|
rect(ctx, 0, 0, 640, 22, '#17160fe8');
|
||||||
|
text(ctx, 'LA CAMPANA', 8, 7, 8, '#efd49e');
|
||||||
|
text(ctx, { bona: 'alla bona', seria: 'seria', male: 'a fassi male' }[c.level], 8, 16, 7, '#d9ccba');
|
||||||
|
const at = n => n > LAST ? 'finita' : `alla ${n}`;
|
||||||
|
text(ctx, `Andrea ${at(c.target.player)}`, 220, 11, 9, c.current === 'player' ? '#fff0c8' : '#a8a08c', 'center');
|
||||||
|
text(ctx, `Il Taglia ${at(c.target.cpu)}`, 360, 11, 9, c.current === 'cpu' ? '#9fd0ff' : '#7f95a8', 'center');
|
||||||
|
text(ctx, c.current === 'player' ? 'tocca a te' : 'tocca al Taglia', 560, 11, 8, '#d9ccba', 'center');
|
||||||
|
if (c.line) { rect(ctx, 150, 24, 340, 14, '#17160fd0'); text(ctx, c.line, 320, 31, 8, /riga|fuori|Troppo|equilibrio|piede|non nella/i.test(c.line) ? '#ffbf9f' : '#9fd0ff', 'center'); }
|
||||||
|
if (c.step === 'result') {
|
||||||
|
const won = c.result.winner === 'player';
|
||||||
|
panel(ctx, 170, 140, 300, 76);
|
||||||
|
text(ctx, won ? 'BATTUTO IL TAGLIA!' : 'HA VINTO IL TAGLIA', 320, 160, 12, won ? '#b9e5b3' : '#efd49e', 'center');
|
||||||
|
text(ctx, won ? 'Tutta la campana, dalla 1 alla 8.' : `Tu eri arrivato alla ${c.target.player}.`, 320, 182, 8, '#efd49e', 'center');
|
||||||
|
text(ctx, 'Torna al campone per uscire.', 320, 200, 8, '#d9ccba', 'center');
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
// La campana al campone delle poste (idea di Andrea, 30 ottobre): disegnata col gesso sul vialetto, a turni col Taglia.
|
||||||
|
// Si tira il sassolino nella casella che tocca (dalla 1 alla 8): se cade dentro, senza toccare le righe, si salta tutta
|
||||||
|
// la campana fino in cima e indietro, su un piede nelle caselle sole e a gambe larghe nelle coppie, senza mai mettere
|
||||||
|
// il piede dove sta il sassolino; tornando, lo si raccoglie dalla casella prima. Chi sbaglia (sassolino fuori o sulla
|
||||||
|
// riga, salto fuori tempo) passa la mano; chi ce la fa va avanti alla casella dopo e ritira. Vince chi finisce la 8.
|
||||||
|
// Il tiro: la mira dà la direzione, tenere premuto carica la forza (va e torna), lasciare tira. I salti: a ogni salto
|
||||||
|
// la lancetta dell'equilibrio attraversa la barra; si preme quando passa nel verde. Unità: i pixel del primo piano
|
||||||
|
// 640×360, visto dall'alto (sfondo di Codex: assets/closeups/campone-campana.png).
|
||||||
|
|
||||||
|
export const W = 66, CY = 181, START = 80, CIELO = { x: START + 6 * W, r: W }, FROM = { x: 40, y: CY }, REACH = 520;
|
||||||
|
export const TAP = 0.14, CHARGE = 1.3, FLIGHT = 0.55, HOP = 0.22, REST = 1.6, DONE = 1.1, CPU_WAIT = 0.9, CPU_BEAT = 0.42;
|
||||||
|
// Le caselle: 1, 2, 3 da sole; 4 e 5 in coppia (la 4 a sinistra di chi salta, cioè in alto); la 6; 7 e 8 in coppia;
|
||||||
|
// in fondo la campana (il mezzo cerchio dove ci si gira; nel codice «cielo», come la chiamano in altri posti).
|
||||||
|
export const BOXES = {
|
||||||
|
1: { x: START, y: CY - W / 2 }, 2: { x: START + W, y: CY - W / 2 }, 3: { x: START + 2 * W, y: CY - W / 2 },
|
||||||
|
4: { x: START + 3 * W, y: CY - W }, 5: { x: START + 3 * W, y: CY }, 6: { x: START + 4 * W, y: CY - W / 2 },
|
||||||
|
7: { x: START + 5 * W, y: CY - W }, 8: { x: START + 5 * W, y: CY },
|
||||||
|
};
|
||||||
|
export const GROUPS = [[1], [2], [3], [4, 5], [6], [7, 8], ['cielo']];
|
||||||
|
export const LAST = 8;
|
||||||
|
// I livelli: quanto si sparpaglia il sassolino (px), se si vede dove cade, quanto è larga la riga che non si può
|
||||||
|
// toccare, quanto ci mette la lancetta dell'equilibrio e quanto è largo il verde; il Taglia: quante volte il sassolino
|
||||||
|
// va dove vuole e quanto spesso, a ogni salto, mette giù il piede.
|
||||||
|
export const LEVELS = {
|
||||||
|
bona: { scatter: 4, guide: true, line: 0, sweep: 1.25, zone: 0.32, cpu: { throw: 0.5, slip: 0.045 } },
|
||||||
|
seria: { scatter: 7, guide: false, line: 3, sweep: 1, zone: 0.2, cpu: { throw: 0.7, slip: 0.025 } },
|
||||||
|
male: { scatter: 9, guide: false, line: 4, sweep: 0.8, zone: 0.15, cpu: { throw: 0.8, slip: 0.018 } },
|
||||||
|
};
|
||||||
|
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||||
|
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
|
||||||
|
|
||||||
|
export function center(id) {
|
||||||
|
if (id === 'cielo') return { x: CIELO.x + W * 0.42, y: CY };
|
||||||
|
const b = BOXES[id]; return { x: b.x + W / 2, y: b.y + W / 2 };
|
||||||
|
}
|
||||||
|
// In che casella cade un punto, e se tocca la riga (entro `tol` pixel da un bordo).
|
||||||
|
export function boxAt([x, y], tol = 0) {
|
||||||
|
for (const [id, b] of Object.entries(BOXES)) {
|
||||||
|
if (x < b.x || x > b.x + W || y < b.y || y > b.y + W) continue;
|
||||||
|
return { id: Number(id), line: Math.min(x - b.x, b.x + W - x, y - b.y, b.y + W - y) < tol };
|
||||||
|
}
|
||||||
|
const d = Math.hypot(x - CIELO.x, y - CY);
|
||||||
|
if (x >= CIELO.x && d <= CIELO.r) return { id: 'cielo', line: Math.min(x - CIELO.x, CIELO.r - d) < tol };
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// I salti per la casella `n`: all'andata tutte le caselle, saltando quella col sassolino (nella coppia si mette un
|
||||||
|
// piede solo nell'altra); il cielo; al ritorno, dalla casella prima si raccoglie il sassolino; poi fuori.
|
||||||
|
export function beats(n) {
|
||||||
|
const step = (g, back) => g.includes(n) ? (g.length > 1 ? [{ boxes: g.filter(k => k !== n), back }] : []) : [{ boxes: g, back }];
|
||||||
|
const out = GROUPS.flatMap(g => step(g, false));
|
||||||
|
for (let i = GROUPS.length - 2; i >= 0; i--) {
|
||||||
|
if (GROUPS[i].includes(n)) out.push({ ...out[out.length - 1], back: true, pick: true });
|
||||||
|
out.push(...step(GROUPS[i], true));
|
||||||
|
}
|
||||||
|
out.push({ boxes: [], back: true, out: true });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newCampana(seed = Date.now() >>> 0, def = {}) {
|
||||||
|
const level = LEVELS[def.level] ? def.level : 'seria';
|
||||||
|
return { step: 'aim', t: 0, seed, level, skill: LEVELS[level], current: 'player', target: { player: 1, cpu: 1 },
|
||||||
|
aim: center(1), power: 0.2, holding: false, holdTime: 0, charging: false, stone: null, shot: null,
|
||||||
|
beats: null, beat: -1, meter: null, hop: null, line: null, next: null, events: [], visual: { time: 0 } };
|
||||||
|
}
|
||||||
|
export const canThrow = c => c.current === 'player' && (c.step === 'aim' || c.step === 'hop');
|
||||||
|
export const canAdjust = c => c.current === 'player' && c.step === 'aim';
|
||||||
|
export const aimFree = c => !canAdjust(c);
|
||||||
|
|
||||||
|
// Dove cade il sassolino tirato con questa forza, senza lo sparpaglio.
|
||||||
|
export function landing(c, power = c.power) {
|
||||||
|
const dx = c.aim.x - FROM.x, dy = c.aim.y - FROM.y, d = Math.hypot(dx, dy) || 1;
|
||||||
|
return { x: FROM.x + dx / d * power * REACH, y: FROM.y + dy / d * power * REACH };
|
||||||
|
}
|
||||||
|
function toss(c, to) {
|
||||||
|
c.shot = { from: { ...FROM }, to, t: 0 }; c.stone = null;
|
||||||
|
c.step = 'flying'; c.line = null;
|
||||||
|
c.events.push({ kind: 'campana-lancio', power: 0.6 });
|
||||||
|
}
|
||||||
|
function throwStone(c) {
|
||||||
|
const p = landing(c), s = c.skill.scatter, spread = () => (random(c) + random(c) + random(c) - 1.5) * s * 1.4;
|
||||||
|
toss(c, { x: p.x + spread(), y: p.y + spread() });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const who = c => c.current === 'cpu' ? 'Il Taglia: ' : '';
|
||||||
|
const cap = line => line[0].toUpperCase() + line.slice(1);
|
||||||
|
// Il sassolino è arrivato: nella casella giusta si salta, se no passa la mano.
|
||||||
|
function land(c) {
|
||||||
|
const to = c.shot.to, n = c.target[c.current], hit = boxAt([to.x, to.y], c.skill.line);
|
||||||
|
c.shot = null; c.stone = to;
|
||||||
|
c.events.push({ kind: 'campana-sasso', power: 0.8 });
|
||||||
|
if (hit && hit.id === n && !hit.line) {
|
||||||
|
c.line = cap(`${who(c)}il sassolino è nella ${n}. Salta!`);
|
||||||
|
c.beats = beats(n); c.beat = -1; c.hop = null; c.t = 0;
|
||||||
|
c.step = c.current === 'cpu' ? 'cpuHop' : 'hop';
|
||||||
|
if (c.step === 'hop') c.meter = { t: -0.5, dir: 1 };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fail(c, `${who(c)}${hit?.line ? 'sulla riga!' : hit ? `nella ${hit.id === 'cielo' ? 'campana in cima' : hit.id}, non nella ${n}.` : 'fuori dalla campana.'}`);
|
||||||
|
}
|
||||||
|
function fail(c, line) {
|
||||||
|
c.line = cap(line); c.step = 'rest'; c.t = 0; c.meter = null;
|
||||||
|
c.next = c.current === 'player' ? 'cpu' : 'player';
|
||||||
|
c.events.push({ kind: 'campana-riga', power: 0.7 });
|
||||||
|
}
|
||||||
|
// Un salto riuscito: la casella dopo, o il sassolino raccolto, o fuori dalla campana (e la casella è fatta).
|
||||||
|
function advance(c) {
|
||||||
|
c.beat++;
|
||||||
|
const b = c.beats[c.beat];
|
||||||
|
c.hop = { t: 0 };
|
||||||
|
if (b.pick) { c.stone = null; c.line = cap(`${who(c)}preso il sassolino!`); c.events.push({ kind: 'campana-sasso', power: 0.5 }); }
|
||||||
|
else c.events.push({ kind: 'campana-salto', power: 0.6 });
|
||||||
|
if (b.out) {
|
||||||
|
const n = c.target[c.current]++;
|
||||||
|
if (n >= LAST) {
|
||||||
|
c.step = 'result'; c.meter = null;
|
||||||
|
c.result = { winner: c.current, player: c.target.player - 1, cpu: c.target.cpu - 1 };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
c.line = cap(`${who(c)}fatta la ${n}! Ora la ${n + 1}.`); c.step = 'done'; c.t = 0; c.meter = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c.meter) c.meter = { t: 0, dir: -c.meter.dir };
|
||||||
|
}
|
||||||
|
// La lancetta dell'equilibrio, da -1 a 1 (o al contrario, un salto sì e uno no).
|
||||||
|
export function needle(c) {
|
||||||
|
if (!c.meter) return 0;
|
||||||
|
return c.meter.dir * (-1 + 2 * clamp(c.meter.t, 0, c.skill.sweep) / c.skill.sweep);
|
||||||
|
}
|
||||||
|
function jump(c) {
|
||||||
|
if (c.hop && c.hop.t < HOP) return true; // ancora in aria: il tocco non conta
|
||||||
|
const u = needle(c);
|
||||||
|
if (Math.abs(u) <= c.skill.zone) { advance(c); return true; }
|
||||||
|
const early = u * c.meter.dir < 0;
|
||||||
|
fail(c, early ? 'Troppo presto: giù l\'altro piede!' : 'Troppo tardi: pestata la riga!');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Tocca all'altro, o allo stesso per la casella dopo: dalla partenza, senza sassolino.
|
||||||
|
function turn(c, current) {
|
||||||
|
Object.assign(c, { current, stone: null, beats: null, beat: -1, hop: null, meter: null, t: 0, next: null,
|
||||||
|
step: current === 'player' ? 'aim' : 'cpuAim' });
|
||||||
|
if (current === 'player') c.aim = center(c.target.player);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.hop) c.hop.t += dt;
|
||||||
|
if (c.holding) {
|
||||||
|
c.holdTime += dt; c.charging = c.holdTime >= TAP;
|
||||||
|
const k = (c.holdTime / CHARGE) % 2; c.power = k <= 1 ? k : 2 - k;
|
||||||
|
}
|
||||||
|
if (c.step === 'flying') { c.shot.t += dt; if (c.shot.t >= FLIGHT) land(c); }
|
||||||
|
else if (c.step === 'hop') {
|
||||||
|
if (!c.hop || c.hop.t >= HOP) c.meter.t += dt;
|
||||||
|
if (c.meter.t > c.skill.sweep) fail(c, 'Hai perso l\'equilibrio!');
|
||||||
|
} else if (c.step === 'cpuAim' && c.t >= CPU_WAIT) {
|
||||||
|
// Il Taglia: o nella casella, ben dentro, o mezza casella più in là (sulla riga o nell'altra).
|
||||||
|
const n = c.target.cpu, p = center(n), ok = random(c) < c.skill.cpu.throw, j = () => (random(c) - 0.5) * 20;
|
||||||
|
toss(c, ok ? { x: p.x + j(), y: p.y + j() } : { x: p.x + (random(c) < 0.5 ? -1 : 1) * W * (0.5 + random(c) * 0.3), y: p.y + j() });
|
||||||
|
} else if (c.step === 'cpuHop' && c.t >= CPU_BEAT) {
|
||||||
|
c.t = 0;
|
||||||
|
if (random(c) < c.skill.cpu.slip) fail(c, random(c) < 0.5 ? 'Il Taglia ha pestato la riga!' : 'Il Taglia ha messo giù l\'altro piede!');
|
||||||
|
else advance(c);
|
||||||
|
} else if (c.step === 'rest' && c.t >= REST) turn(c, c.next);
|
||||||
|
else if (c.step === 'done' && c.t >= DONE) turn(c, c.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Comandi: la mira segue il mouse o il dito; tenere premuto carica la forza, lasciare tira; ai salti si preme.
|
||||||
|
export function aimScreen(c, [x, y]) {
|
||||||
|
if (!canAdjust(c) || c.holding || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||||
|
c.aim = { x: clamp(x, FROM.x + 30, 630), y: clamp(y, 80, 285) }; return true;
|
||||||
|
}
|
||||||
|
export function setPower(c, v) { if (!canAdjust(c) || c.holding) return false; c.power = clamp(v, 0, 1); return true; }
|
||||||
|
export function nudge(c, dir) { if (!canAdjust(c) || c.holding) return false; c.aim.y = clamp(c.aim.y + dir * 3, 80, 285); return true; }
|
||||||
|
// Frecce: su e giù la mira, destra e sinistra la forza (più in là, più in qua).
|
||||||
|
export function key(c, key, down) {
|
||||||
|
const move = { ArrowUp: [0, -3], ArrowDown: [0, 3], ArrowLeft: [-0.01, 0], ArrowRight: [0.01, 0] }[key];
|
||||||
|
if (!move) return false;
|
||||||
|
if (down && canAdjust(c) && !c.holding) { c.aim.y = clamp(c.aim.y + move[1], 80, 285); c.power = clamp(c.power + move[0], 0, 1); }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function press(c, pointerId) {
|
||||||
|
if (c.current !== 'player') return false;
|
||||||
|
if (c.step === 'hop') return jump(c);
|
||||||
|
if (!canAdjust(c) || c.holding) return false;
|
||||||
|
c.pointerId = pointerId; c.holding = true; c.charging = false; c.holdTime = 0; c.power = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function cancel(c, pointerId) {
|
||||||
|
if (pointerId !== undefined && c.pointerId !== pointerId) return false;
|
||||||
|
if (!c.holding) return false;
|
||||||
|
c.holding = c.charging = false; c.holdTime = 0; return true;
|
||||||
|
}
|
||||||
|
export function release(c, pointerId) {
|
||||||
|
if (c.pointerId !== pointerId || !c.holding) return false;
|
||||||
|
if (c.holdTime < TAP) return cancel(c, pointerId); // il tocco secco mira soltanto
|
||||||
|
c.holding = c.charging = false; c.holdTime = 0;
|
||||||
|
return throwStone(c);
|
||||||
|
}
|
||||||
|
// Il pulsante: tira con la forza del cursore, o salta.
|
||||||
|
export function shoot(c) {
|
||||||
|
if (c.current !== 'player' || c.holding) return false;
|
||||||
|
if (c.step === 'hop') return jump(c);
|
||||||
|
return canAdjust(c) && throwStone(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
const box = n => n > LAST ? 'finito' : `alla ${n}`;
|
||||||
|
export function button(c) {
|
||||||
|
if (c.step === 'result') return 'Finita';
|
||||||
|
if (c.current === 'cpu') return 'Tocca al Taglia…';
|
||||||
|
return { aim: 'Tira', flying: 'In volo…', hop: 'Salta' }[c.step] ?? 'Aspetta…';
|
||||||
|
}
|
||||||
|
export function status(c) {
|
||||||
|
const score = `Andrea ${box(c.target.player)}, il Taglia ${box(c.target.cpu)}.`;
|
||||||
|
if (c.step === 'result') return `${c.result.winner === 'player' ? 'Campana finita: battuto il Taglia! ' : 'Il Taglia ha finito la campana. '}${score}`;
|
||||||
|
const hint = c.current === 'cpu' ? 'Tocca al Taglia.' : c.step === 'hop' ? 'Salta quando la lancetta è nel verde.'
|
||||||
|
: `Tira il sassolino nella ${c.target.player}: mira, tieni premuto per la forza e lascia.`;
|
||||||
|
return `${score} ${c.line ?? hint}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// La macchina del tempo vista da fuori (story.closeups, type 'corsa'; Andrea, 30 ottobre). In partenza ('parti') va via
|
||||||
|
// giù per la strada accelerando, il fulmine la prende, il bianco, e restano due sgommate di fuoco che bruciano e si
|
||||||
|
// spengono. All'arrivo ('arriva') il fulmine in fondo alla strada, e lei viene avanti e ci passa accanto, lasciando le
|
||||||
|
// stesse strisce (davanti a noi: dietro alla macchina non si vedrebbero). Sopra lo sfondo della scena (`scene`), coi fotogrammi di Codex (`frames`: dietro, dietro coi lampi,
|
||||||
|
// davanti coi lampi). Unità: i pixel della scena 640×360.
|
||||||
|
|
||||||
|
// I tempi, in secondi. Partenza: ferma che si carica, la corsa, il fulmine, il bianco, le fiamme, la fine.
|
||||||
|
export const PARTI = { charge: 0.6, run: 2.2, bolt: 2.3, flash: 0.35, burn: 4.6, end: 5.2 };
|
||||||
|
// Arrivo: il fulmine in fondo, la corsa verso di noi fin fuori dal quadro, le fiamme, la fine.
|
||||||
|
export const ARRIVA = { bolt: 0.3, flash: 0.35, run: 1.5, burn: 4, end: 4.6 };
|
||||||
|
|
||||||
|
const lerp = (a, b, k) => a + (b - a) * k;
|
||||||
|
const clamp01 = k => Math.max(0, Math.min(1, k));
|
||||||
|
// Dove sta la macchina a un punto `k` del percorso (0 vicina, 1 in fondo): posizione delle ruote e grandezza.
|
||||||
|
export function along(def, k) {
|
||||||
|
const [a, b] = def.path;
|
||||||
|
return { x: lerp(a[0], b[0], k), y: lerp(a[1], b[1], k), scale: lerp(a[2], b[2], k) };
|
||||||
|
}
|
||||||
|
// Il punto del percorso al tempo t: in partenza accelera (da fermo); all'arrivo viene avanti dal fondo fin oltre il
|
||||||
|
// punto 0 (fuori dal quadro, accanto a noi).
|
||||||
|
export function progress(c, def) {
|
||||||
|
if (def.mode === 'arriva') return 1 - clamp01((c.t - ARRIVA.bolt) / ARRIVA.run);
|
||||||
|
const k = clamp01((c.t - PARTI.charge) / (PARTI.run - PARTI.charge));
|
||||||
|
return Math.min(0.92, k * k);
|
||||||
|
}
|
||||||
|
// Si vede la macchina? In partenza fino al fulmine; all'arrivo dal fulmine in poi.
|
||||||
|
export function carShown(c, def) {
|
||||||
|
return def.mode === 'arriva' ? c.t >= ARRIVA.bolt + 0.05 && c.t < ARRIVA.bolt + ARRIVA.run : c.t < PARTI.bolt + 0.05;
|
||||||
|
}
|
||||||
|
// Dove arriva il fulmine: sulla macchina in partenza, in fondo alla strada all'arrivo.
|
||||||
|
export function boltAt(def) { return def.mode === 'arriva' ? along(def, 1) : along(def, progress({ t: PARTI.bolt }, def)); }
|
||||||
|
const T = def => def.mode === 'arriva' ? ARRIVA : PARTI;
|
||||||
|
|
||||||
|
export function newCorsa(id) { return { id, t: 0, age: 0, events: [] }; }
|
||||||
|
// Il tempo che passa e i suoni; vero quando è finita.
|
||||||
|
export function updateCorsa(c, def, dt) {
|
||||||
|
c.t += dt;
|
||||||
|
const times = T(def);
|
||||||
|
if (def.mode !== 'arriva' && !c.charge && c.t >= 0.1) { c.charge = true; c.events.push({ kind: 'gara-turbo', power: 1 }); }
|
||||||
|
if (!c.boom && c.t >= times.bolt) { c.boom = true; c.events.push({ kind: 'corsa-tuono', power: 1 }); }
|
||||||
|
if (!c.fire && c.t >= (def.mode === 'arriva' ? ARRIVA.bolt + 0.2 : PARTI.charge + 0.3)) { c.fire = true; c.events.push({ kind: 'corsa-fuoco', power: 1 }); }
|
||||||
|
return c.t >= times.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Le due ruote posteriori (o anteriori) a un punto del percorso: una a sinistra e una a destra.
|
||||||
|
function wheels(def, k) {
|
||||||
|
const p = along(def, k), half = def.track * p.scale;
|
||||||
|
return [[p.x - half, p.y], [p.x + half, p.y]];
|
||||||
|
}
|
||||||
|
// Le sgommate di fuoco: da dove è partita a dove è arrivata, fiammelle che tremano, più piccole in fondo, che si
|
||||||
|
// spengono dopo `burn`.
|
||||||
|
function fire(ctx, c, def) {
|
||||||
|
const times = T(def), shown = def.mode === 'arriva' ? [Math.max(progress(c, def), 0), 1] : [0, progress({ t: Math.min(c.t, times.bolt) }, def)];
|
||||||
|
const fade = c.t < times.burn ? 1 : clamp01(1 - (c.t - times.burn) / (times.end - times.burn));
|
||||||
|
if (fade <= 0 || shown[1] - shown[0] < 0.005) return;
|
||||||
|
for (let side = 0; side < 2; side++) {
|
||||||
|
// Prima il segno nero bruciato, poi le fiamme sopra.
|
||||||
|
const [a, b] = [wheels(def, shown[0])[side], wheels(def, shown[1])[side]];
|
||||||
|
ctx.strokeStyle = `rgba(30,18,10,${0.7 * Math.max(fade, 0.5)})`; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke();
|
||||||
|
const n = 70;
|
||||||
|
for (let i = 0; i <= n; i++) {
|
||||||
|
const k = lerp(shown[0], shown[1], i / n), [x, y] = wheels(def, k)[side], s = Math.max(0.12, along(def, k).scale);
|
||||||
|
// Lingue di fuoco di altezza diversa, che tremano: rosso fuori, arancio, il cuore giallo.
|
||||||
|
const flick = 0.55 + 0.45 * Math.sin(c.t * 19 + i * 2.3 + side * 2) * Math.sin(c.t * 7 + i * 0.9);
|
||||||
|
const h = (16 + 22 * flick) * s * fade, w = Math.max(2, 9 * s);
|
||||||
|
if (h < 1.5) continue;
|
||||||
|
ctx.fillStyle = `rgba(255,120,30,${0.18 * fade})`; ctx.fillRect(Math.round(x - w), Math.round(y - h * 1.2), Math.ceil(w * 2), Math.ceil(h * 1.2));
|
||||||
|
ctx.fillStyle = `rgba(220,50,20,${0.9 * fade})`; ctx.fillRect(Math.round(x - w / 2), Math.round(y - h), Math.ceil(w), Math.ceil(h));
|
||||||
|
ctx.fillStyle = `rgba(255,${Math.round(140 + 60 * flick)},30,${0.95 * fade})`; ctx.fillRect(Math.round(x - w / 3), Math.round(y - h * 0.7), Math.ceil(w * 0.66), Math.ceil(h * 0.7));
|
||||||
|
ctx.fillStyle = `rgba(255,245,170,${fade})`; ctx.fillRect(Math.round(x - w / 6), Math.round(y - h * 0.35), Math.max(1, Math.ceil(w / 3)), Math.ceil(h * 0.35));
|
||||||
|
// Un filo di fumo che sale quando le fiamme calano.
|
||||||
|
if (c.t > times.burn - 1 && i % 8 === 0) {
|
||||||
|
ctx.fillStyle = `rgba(90,90,96,${0.25 * fade})`;
|
||||||
|
ctx.fillRect(Math.round(x - 2 * s), Math.round(y - h - 6 * s - ((c.t * 18 + i) % 14) * s), Math.ceil(4 * s), Math.ceil(4 * s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Il fulmine: una saetta spezzata dal cielo fino al punto, con due rami.
|
||||||
|
function bolt(ctx, c, def) {
|
||||||
|
const times = T(def), dt = c.t - times.bolt;
|
||||||
|
if (dt < -0.12 || dt > 0.28) return;
|
||||||
|
const p = boltAt(def), seed = Math.floor(c.t * 30);
|
||||||
|
ctx.save();
|
||||||
|
for (const [width, color] of [[6, 'rgba(140,200,255,0.5)'], [2, '#ffffff']]) {
|
||||||
|
ctx.strokeStyle = color; ctx.lineWidth = width; ctx.beginPath();
|
||||||
|
let x = p.x + 40, y = 0; ctx.moveTo(x, y);
|
||||||
|
const steps = 9;
|
||||||
|
for (let i = 1; i <= steps; i++) {
|
||||||
|
x = lerp(p.x + 40, p.x, i / steps) + (i < steps ? ((seed * (i + 5) * 53) % 30) - 15 : 0);
|
||||||
|
y = lerp(0, p.y - 12 * p.scale, i / steps); ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawCorsa(ctx, c, def, { bg, frames, tint }) {
|
||||||
|
const times = T(def);
|
||||||
|
ctx.save(); ctx.imageSmoothingEnabled = false;
|
||||||
|
// La strada trema un po' quando la macchina si carica e quando arriva il fulmine.
|
||||||
|
const shake = (def.mode !== 'arriva' && c.t < PARTI.charge + 0.4 ? 1.5 : 0) + (Math.abs(c.t - times.bolt) < 0.25 ? 3 : 0);
|
||||||
|
ctx.translate(Math.round(Math.sin(c.t * 57) * shake), Math.round(Math.cos(c.t * 43) * shake));
|
||||||
|
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#4a4a50'; ctx.fillRect(0, 0, 640, 360); }
|
||||||
|
if (tint) { ctx.fillStyle = tint; ctx.fillRect(0, 0, 640, 360); }
|
||||||
|
fire(ctx, c, def);
|
||||||
|
if (carShown(c, def)) {
|
||||||
|
const k = progress(c, def), p = along(def, k);
|
||||||
|
// Da dietro: coi lampi mentre si carica e quando corre forte; davanti (all'arrivo) coi lampi finché frena.
|
||||||
|
const img = def.mode === 'arriva' ? frames?.davanti : c.t > PARTI.charge * 0.5 && Math.floor(c.t * 14) % 3 !== 0 ? frames?.lampi : frames?.dietro;
|
||||||
|
if (img) {
|
||||||
|
const w = def.width * p.scale, h = w * img.height / img.width;
|
||||||
|
ctx.drawImage(img, Math.round(p.x - w / 2), Math.round(p.y - h), Math.round(w), Math.round(h));
|
||||||
|
} else { ctx.fillStyle = '#c8ccd2'; ctx.fillRect(Math.round(p.x - 40 * p.scale), Math.round(p.y - 40 * p.scale), Math.round(80 * p.scale), Math.round(40 * p.scale)); }
|
||||||
|
}
|
||||||
|
bolt(ctx, c, def);
|
||||||
|
ctx.restore();
|
||||||
|
// Il bianco del fulmine, che sfuma.
|
||||||
|
const f = c.t - times.bolt;
|
||||||
|
if (f >= 0 && f < times.flash) { ctx.fillStyle = `rgba(255,255,255,${1 - f / times.flash})`; ctx.fillRect(0, 0, 640, 360); }
|
||||||
|
}
|
||||||
+6
-1
@@ -12,7 +12,9 @@ import * as scopa from './scopa.js';
|
|||||||
import * as gara from './gara.js';
|
import * as gara from './gara.js';
|
||||||
import * as micro from './micro.js';
|
import * as micro from './micro.js';
|
||||||
import * as cerbottana from './cerbottana.js';
|
import * as cerbottana from './cerbottana.js';
|
||||||
|
import { newCorsa, updateCorsa } from './corsa.js';
|
||||||
import * as condor from './condor.js';
|
import * as condor from './condor.js';
|
||||||
|
import * as campana from './campana.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.
|
||||||
@@ -28,6 +30,7 @@ export const MINIGAMES = {
|
|||||||
poker: { create: (seed, level) => poker.newPoker(seed, { level }), update: poker.update, play: poker.play },
|
poker: { create: (seed, level) => poker.newPoker(seed, { level }), update: poker.update, play: poker.play },
|
||||||
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 },
|
||||||
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 },
|
||||||
};
|
};
|
||||||
@@ -325,7 +328,7 @@ export class Game {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Il viaggio si chiude da solo alla fine (o uscendo prima): si arriva comunque.
|
// Il viaggio si chiude da solo alla fine (o uscendo prima): si arriva comunque.
|
||||||
if (def?.type === 'viaggio') { this.queue.unshift(...def.complete); return true; }
|
if (def?.type === 'viaggio' || def?.type === 'corsa') { this.queue.unshift(...def.complete); return true; }
|
||||||
// I minigiochi: a partita finita (risultato deciso, anche se il conteggio si sta ancora mostrando) la battuta
|
// I minigiochi: a partita finita (risultato deciso, anche se il conteggio si sta ancora mostrando) la battuta
|
||||||
// di chi ha vinto; uscendo prima, quella di chi lascia.
|
// di chi ha vinto; uscendo prima, quella di chi lascia.
|
||||||
if (MINIGAMES[def?.type]) {
|
if (MINIGAMES[def?.type]) {
|
||||||
@@ -486,6 +489,7 @@ export class Game {
|
|||||||
if (c) {
|
if (c) {
|
||||||
if (this.story.closeups[c.id].type === 'torch') { this.updateTorch(dt); return; }
|
if (this.story.closeups[c.id].type === 'torch') { this.updateTorch(dt); return; }
|
||||||
if (this.story.closeups[c.id].type === 'viaggio') { this.updateTrip(dt); return; }
|
if (this.story.closeups[c.id].type === 'viaggio') { this.updateTrip(dt); return; }
|
||||||
|
if (this.story.closeups[c.id].type === 'corsa') { if (updateCorsa(c, this.story.closeups[c.id], dt)) this.closeupClose(); return; }
|
||||||
const m = this.minigame();
|
const m = this.minigame();
|
||||||
if (m) {
|
if (m) {
|
||||||
m.update(c, dt);
|
m.update(c, dt);
|
||||||
@@ -528,6 +532,7 @@ export class Game {
|
|||||||
const type = this.story.closeups[e.closeup].type;
|
const type = this.story.closeups[e.closeup].type;
|
||||||
this.closeup = type === 'torch' ? { id: e.closeup, step: 'search', t: 0, age: 0, found: 0, dwell: 0, beam: [320, 180] }
|
this.closeup = type === 'torch' ? { id: e.closeup, step: 'search', t: 0, age: 0, found: 0, dwell: 0, beam: [320, 180] }
|
||||||
: type === 'viaggio' ? { id: e.closeup, t: 0, age: 0, events: [], typed: 0 }
|
: type === 'viaggio' ? { id: e.closeup, t: 0, age: 0, events: [], typed: 0 }
|
||||||
|
: type === 'corsa' ? newCorsa(e.closeup)
|
||||||
// `level`: «Alla bona», «Seria» o «A fassi male», scelto nel dialogo con l'avversario.
|
// `level`: «Alla bona», «Seria» o «A fassi male», scelto nel dialogo con l'avversario.
|
||||||
// `decks`: il mazzo da usare quando il suo flag è acceso (briscola: le napoletane al posto delle toscane).
|
// `decks`: il mazzo da usare quando il suo flag è acceso (briscola: le napoletane al posto delle toscane).
|
||||||
: MINIGAMES[type] ? { id: e.closeup, ...MINIGAMES[type].create(Math.floor(Math.random() * 4294967296), e.level,
|
: MINIGAMES[type] ? { id: e.closeup, ...MINIGAMES[type].create(Math.floor(Math.random() * 4294967296), e.level,
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import { drawPesca } from './pesca-render.js';
|
|||||||
import { drawGara } from './gara-render.js';
|
import { drawGara } from './gara-render.js';
|
||||||
import { drawMicro } from './micro-render.js';
|
import { drawMicro } from './micro-render.js';
|
||||||
import { drawCerbottana } from './cerbottana-render.js';
|
import { drawCerbottana } from './cerbottana-render.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 { drawPoker } from './poker-render.js';
|
import { drawPoker } from './poker-render.js';
|
||||||
import { drawViaggio } from './viaggio.js';
|
import { drawViaggio } from './viaggio.js';
|
||||||
import { drawBriscola } from './briscola-render.js';
|
import { drawBriscola } from './briscola-render.js';
|
||||||
@@ -686,6 +688,16 @@ function drawCloseup(ctx, game, time) {
|
|||||||
drawViaggio(ctx, c, def, backgrounds.get(def.picture));
|
drawViaggio(ctx, c, def, backgrounds.get(def.picture));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// La macchina del tempo da fuori: lo sfondo di adesso della scena, con la luce della fase del giorno.
|
||||||
|
if (def.type === 'corsa') {
|
||||||
|
const bg = game.backdrop(game.story.scenes[def.scene]), frames = {};
|
||||||
|
for (const [k, src] of [[bg, bg], ...Object.entries(def.frames)]) {
|
||||||
|
if (!backgrounds.has(src)) loadBackground(src);
|
||||||
|
if (k !== bg) frames[k] = backgrounds.get(src);
|
||||||
|
}
|
||||||
|
drawCorsa(ctx, c, def, { bg: backgrounds.get(bg), frames, tint: game.phase()?.tint });
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (def.type === 'torch') {
|
if (def.type === 'torch') {
|
||||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
drawTorch(ctx, c, def, backgrounds.get(def.picture));
|
drawTorch(ctx, c, def, backgrounds.get(def.picture));
|
||||||
@@ -707,6 +719,11 @@ function drawCloseup(ctx, game, time) {
|
|||||||
drawCondor(ctx, c, def, backgrounds.get(def.picture));
|
drawCondor(ctx, c, def, backgrounds.get(def.picture));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (def.type === 'campana') {
|
||||||
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
|
drawCampana(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); });
|
||||||
|
|||||||
@@ -145,6 +145,14 @@ export class Soundscape {
|
|||||||
else if (kind === 'condor-palloncino') { this.noiseSound(0.12, 1800, 0.14, { pan }); this.tone(140, 60, 0.08, 0.08, 0, pan); }
|
else if (kind === 'condor-palloncino') { this.noiseSound(0.12, 1800, 0.14, { pan }); this.tone(140, 60, 0.08, 0.08, 0, pan); }
|
||||||
else if (kind === 'condor-plastica') { this.tone(1300, 900, 0.06, 0.05, 0, pan); this.noiseSound(0.05, 2600, 0.03, { pan }); }
|
else if (kind === 'condor-plastica') { this.tone(1300, 900, 0.06, 0.05, 0, pan); this.noiseSound(0.05, 2600, 0.03, { pan }); }
|
||||||
else if (kind === 'condor-lancio') this.noiseSound(0.2, 700, 0.03, { pan });
|
else if (kind === 'condor-lancio') this.noiseSound(0.2, 700, 0.03, { pan });
|
||||||
|
// La macchina del tempo: il tuono del fulmine e lo sfrigolio delle sgommate di fuoco.
|
||||||
|
else if (kind === 'corsa-tuono') { this.noiseSound(0.9, 400, 0.16, { pan }); this.tone(90, 40, 0.8, 0.1, 0, pan); }
|
||||||
|
else if (kind === 'corsa-fuoco') this.noiseSound(1.6, 2400, 0.05, { pan });
|
||||||
|
// La campana: il sassolino che vola e batte sull'asfalto, il salto, il piede giù.
|
||||||
|
else if (kind === 'campana-lancio') this.noiseSound(0.1, 1500, 0.025, { 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-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 === '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 }); }
|
||||||
|
|||||||
+7
-4
@@ -14,6 +14,7 @@ import * as rinquarto from './engine/rinquarto.js';
|
|||||||
import * as micro from './engine/micro.js';
|
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 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';
|
||||||
@@ -587,7 +588,7 @@ canvas.addEventListener('pointermove', e => {
|
|||||||
if (press?.gara === e.pointerId) gara.aimScreen(game.closeup, scenePoint(e), e.pointerId);
|
if (press?.gara === e.pointerId) gara.aimScreen(game.closeup, scenePoint(e), e.pointerId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const aiming = game.closeup && ['biliardo', 'rinquarto', 'bocce', 'basket', 'pesca', 'micro', 'cerbottana', 'condor'].includes(story.closeups[game.closeup.id].type);
|
const aiming = game.closeup && ['biliardo', 'rinquarto', 'bocce', 'basket', 'pesca', 'micro', 'cerbottana', 'condor', 'campana'].includes(story.closeups[game.closeup.id].type);
|
||||||
if (aiming) { if (e.pointerType === 'mouse' && !press && !game.closeup.aimLocked) game.closeupAim(scenePoint(e)); }
|
if (aiming) { if (e.pointerType === 'mouse' && !press && !game.closeup.aimLocked) game.closeupAim(scenePoint(e)); }
|
||||||
else if (e.pointerType === 'mouse' || press?.closeup) game.closeupAim(scenePoint(e));
|
else if (e.pointerType === 'mouse' || press?.closeup) game.closeupAim(scenePoint(e));
|
||||||
});
|
});
|
||||||
@@ -611,6 +612,7 @@ const MINI = {
|
|||||||
poker: { ...poker, back: 'Torna al Barrettino', label: 'Il pokerino: tocca le carte da cambiare, poi Cambia; nei giri di puntate Lascio, Vedo o Parola, Apro o Rilancio. Dopo la mostra un tocco va avanti.' },
|
poker: { ...poker, back: 'Torna al Barrettino', label: 'Il pokerino: tocca le carte da cambiare, poi Cambia; nei giri di puntate Lascio, Vedo o Parola, Apro o Rilancio. Dopo la mostra un tocco va avanti.' },
|
||||||
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.' },
|
||||||
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).
|
||||||
@@ -1040,7 +1042,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' }[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' }[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;
|
||||||
@@ -1055,7 +1057,7 @@ function updateUi() {
|
|||||||
// 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' || easy);
|
||||||
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy);
|
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy || def.type === 'campana');
|
||||||
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');
|
||||||
@@ -1076,6 +1078,7 @@ function updateUi() {
|
|||||||
setProp($('micro-help'), 'hidden', def.type !== 'micro');
|
setProp($('micro-help'), 'hidden', def.type !== 'micro');
|
||||||
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($('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');
|
||||||
@@ -1096,7 +1099,7 @@ function updateUi() {
|
|||||||
canvas.classList.toggle('torch', !!torch);
|
canvas.classList.toggle('torch', !!torch);
|
||||||
canvas.classList.toggle('bocce', !!mg);
|
canvas.classList.toggle('bocce', !!mg);
|
||||||
// Biliardo e GP di Fortezza: audio, pausa e uscita fuori dal quadro (al GP la pista passa negli angoli).
|
// Biliardo e GP di Fortezza: audio, pausa e uscita fuori dal quadro (al GP la pista passa negli angoli).
|
||||||
$('stage').classList.toggle('biliardo', !!c && ['biliardo', 'rinquarto', 'gara', 'micro', 'cerbottana', 'condor'].includes(story.closeups[c.id].type));
|
$('stage').classList.toggle('biliardo', !!c && ['biliardo', 'rinquarto', 'gara', 'micro', 'cerbottana', 'condor', 'campana'].includes(story.closeups[c.id].type));
|
||||||
$('stage').classList.toggle('briscola', !!cards);
|
$('stage').classList.toggle('briscola', !!cards);
|
||||||
setAttr(canvas, 'aria-label', torch ? 'Fessura: sposta la luce con mouse, dito o frecce. Invio cerca il prossimo dettaglio; Esc esce.' : mg ? mg.label : "La scena dell'avventura");
|
setAttr(canvas, 'aria-label', torch ? 'Fessura: sposta la luce con mouse, dito o frecce. Invio cerca il prossimo dettaglio; Esc esce.' : mg ? mg.label : "La scena dell'avventura");
|
||||||
// La pagina che comincia a girare fa il suo fruscio.
|
// La pagina che comincia a girare fa il suo fruscio.
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ export const ambience = {
|
|||||||
{ type: 'insect', kind: 'dragonfly', phase: day, area: [440, 228, 110, 30], every: 31000, duration: 8500, offset: 15000 },
|
{ type: 'insect', kind: 'dragonfly', phase: day, area: [440, 228, 110, 30], every: 31000, duration: 8500, offset: 15000 },
|
||||||
],
|
],
|
||||||
orti_zio_mario: [
|
orti_zio_mario: [
|
||||||
{ type: 'clouds', area: [80, 20, 500, 90], count: 3, big: true },
|
{ type: 'clouds', area: [60, 20, 330, 70], count: 3, big: true }, // solo fino all'abete: dentro le fronde che ondeggiano le nuvole si tagliano
|
||||||
{ type: 'birds', phase: day, area: [300, 30, 250, 60], count: 3, every: 24000, duration: 5000, offset: 2000 },
|
{ type: 'birds', phase: day, area: [300, 30, 250, 60], count: 3, every: 24000, duration: 5000, offset: 2000 },
|
||||||
{ type: 'sway', rects: [[180, 10, 140, 180], [430, 10, 210, 240]], amount: 2 },
|
{ type: 'sway', rects: [[180, 10, 140, 180], [430, 10, 210, 240]], amount: 2 },
|
||||||
butterfly([10, 240, 140, 50], 8000),
|
butterfly([10, 240, 140, 50], 8000),
|
||||||
|
|||||||
+64
-1
@@ -43,11 +43,19 @@ const torchHints = [{
|
|||||||
}],
|
}],
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
// La macchina del tempo che corre, vista da dietro e (all'arrivo) davanti: larga `width` pixel da vicino, le ruote a
|
||||||
|
// `track` della larghezza dal centro.
|
||||||
|
const MACCHINA_CORSA = {
|
||||||
|
frames: { dietro: 'assets/closeups/macchina-dietro.png', lampi: 'assets/closeups/macchina-lampi.png', davanti: 'assets/closeups/macchina-davanti.png' },
|
||||||
|
width: 220, track: 75,
|
||||||
|
};
|
||||||
|
|
||||||
// Il ritorno dal 1997, dalla porta o dal pulsante «Torna al 2007»: la macchina del tempo sparisce, e per rifare il
|
// Il ritorno dal 1997, dalla porta o dal pulsante «Torna al 2007»: la macchina del tempo sparisce, e per rifare il
|
||||||
// viaggio si richiede all'Arcu (Andrea, 29 ottobre).
|
// viaggio si richiede all'Arcu (Andrea, 29 ottobre).
|
||||||
const RITORNO_2007 = [
|
const RITORNO_2007 = [
|
||||||
{ say: 'Si torna nel 2007. Ciao Vannino, ciao Pierino.' },
|
{ say: 'Si torna nel 2007. Ciao Vannino, ciao Pierino.' },
|
||||||
{ go: 'via_liguri_magelli', at: [212, 280], face: 'down' },
|
{ go: 'via_liguri_magelli', at: [212, 280], face: 'down' },
|
||||||
|
{ closeup: 'arrivo' },
|
||||||
{ unset: 'macchina_trappola' },
|
{ unset: 'macchina_trappola' },
|
||||||
{ say: 'Riportata dove l\'ho presa. Se ti riviene voglia di pokerini, chiedi.', who: 'arcu', pose: 'explain' },
|
{ say: 'Riportata dove l\'ho presa. Se ti riviene voglia di pokerini, chiedi.', who: 'arcu', pose: 'explain' },
|
||||||
];
|
];
|
||||||
@@ -249,13 +257,25 @@ export const story = {
|
|||||||
picture: 'assets/closeups/cruscotto-tempo.png',
|
picture: 'assets/closeups/cruscotto-tempo.png',
|
||||||
display: [220, 158, 212, 62],
|
display: [220, 158, 212, 62],
|
||||||
date: '10/07/1997 18:30',
|
date: '10/07/1997 18:30',
|
||||||
|
complete: [{ closeup: 'partenza' }],
|
||||||
|
},
|
||||||
|
// La partenza vista da fuori: giù per Via Liguri Magelli, il fulmine e le sgommate di fuoco; e l'arrivo, al
|
||||||
|
// ritorno dal 1997 (Andrea, 30 ottobre; la macchina da dietro e davanti è di Codex).
|
||||||
|
partenza: {
|
||||||
|
type: 'corsa', mode: 'parti', scene: 'via_liguri_magelli', ...MACCHINA_CORSA,
|
||||||
|
path: [[215, 350, 1], [252, 192, 0.05]],
|
||||||
complete: [{ go: 'barrettino_1997', at: [320, 330], face: 'up' }],
|
complete: [{ go: 'barrettino_1997', at: [320, 330], face: 'up' }],
|
||||||
},
|
},
|
||||||
|
arrivo: {
|
||||||
|
type: 'corsa', mode: 'arriva', scene: 'via_liguri_magelli', ...MACCHINA_CORSA,
|
||||||
|
path: [[110, 470, 2.2], [252, 192, 0.05]],
|
||||||
|
complete: [],
|
||||||
|
},
|
||||||
// Il pokerino del Barrettino con Vannino e Pierino (Andrea, 29 ottobre). [DA ANDREA: le battute]
|
// Il pokerino del Barrettino con Vannino e Pierino (Andrea, 29 ottobre). [DA ANDREA: le battute]
|
||||||
poker: {
|
poker: {
|
||||||
type: 'poker',
|
type: 'poker',
|
||||||
names: { player: 'Andrea', cpu: 'Vannino e Pierino' },
|
names: { player: 'Andrea', cpu: 'Vannino e Pierino' },
|
||||||
win: [{ say: 'M\'hai pelato, ragazzo. Nel 1997 un ci si vedeva da queste parti, con quella faccia.', who: 'vannino', pose: 'shrug' }, { set: 'poker_vinto' }],
|
win: [{ say: 'Tu m\'ha pelato. La prossima volta porta il Barone.', who: 'vannino', pose: 'shrug' }, { set: 'poker_vinto' }],
|
||||||
lose: [{ say: 'Le fiches restano al banco. E tu torna quando hai imparato a bluffare.', who: 'pierino_parigi', pose: 'laugh' }],
|
lose: [{ say: 'Le fiches restano al banco. E tu torna quando hai imparato a bluffare.', who: 'pierino_parigi', pose: 'laugh' }],
|
||||||
quit: [{ say: 'Già finito? E il caffè chi lo paga?', who: 'vannino', pose: 'talk' }],
|
quit: [{ say: 'Già finito? E il caffè chi lo paga?', who: 'vannino', pose: 'talk' }],
|
||||||
draw: [{ say: 'Pari. Si rifà domani sera, alla stess\'ora.', who: 'pierino_parigi', pose: 'explain' }],
|
draw: [{ say: 'Pari. Si rifà domani sera, alla stess\'ora.', who: 'pierino_parigi', pose: 'explain' }],
|
||||||
@@ -281,6 +301,17 @@ export const story = {
|
|||||||
draw: [{ say: 'Pari. Si rifà?' }, { say: 'Si rifà.', who: 'taglia', pose: 'explain' }],
|
draw: [{ say: 'Pari. Si rifà?' }, { say: 'Si rifà.', who: 'taglia', pose: 'explain' }],
|
||||||
quit: [{ say: 'La Condor la rendo al Taglia. Per ora.' }],
|
quit: [{ say: 'La Condor la rendo al Taglia. Per ora.' }],
|
||||||
},
|
},
|
||||||
|
// La campana col gesso sul vialetto del campone delle poste, a turni col Taglia (Andrea, 30 ottobre).
|
||||||
|
// [DA ANDREA: le battute sono provvisorie]
|
||||||
|
campana: {
|
||||||
|
type: 'campana',
|
||||||
|
picture: 'assets/closeups/campone-campana.png',
|
||||||
|
names: { player: 'Andrea', cpu: 'il Taglia' },
|
||||||
|
win: [{ say: 'Dalla 1 fino in cima, e ritorno. A ventisett\'anni, davanti alle poste.' }, { say: 'Te lo dico io: se ti vede qualcuno, domani lo sa tutto il paese.', who: 'taglia', pose: 'laugh' }, { set: 'campana_vinta' }],
|
||||||
|
lose: [{ say: 'Il Taglia, su un piede solo, è più alto di me su due.' }, { say: 'È questione di gambe lunghe.', who: 'taglia', pose: 'explain' }],
|
||||||
|
draw: [],
|
||||||
|
quit: [{ say: 'Il gesso lo lascio lì. Ci giocherà qualche bambino, che è più in allenamento di me.' }],
|
||||||
|
},
|
||||||
// Le macchinine sulla pista col gesso, sul parcheggio di Cardetole, contro il Chiaru (Andrea, 28 e 29 ottobre).
|
// Le macchinine sulla pista col gesso, sul parcheggio di Cardetole, contro il Chiaru (Andrea, 28 e 29 ottobre).
|
||||||
// [DA ANDREA: le battute sono provvisorie]
|
// [DA ANDREA: le battute sono provvisorie]
|
||||||
micro: {
|
micro: {
|
||||||
@@ -3336,6 +3367,10 @@ export const story = {
|
|||||||
walk: [[150, 360], [300, 250], [280, 236], [340, 236], [420, 248], [640, 330], [640, 360]],
|
walk: [[150, 360], [300, 250], [280, 236], [340, 236], [420, 248], [640, 330], [640, 360]],
|
||||||
depth: [[236, 0.45], [360, 1.35]],
|
depth: [[236, 0.45], [360, 1.35]],
|
||||||
entry: [400, 352],
|
entry: [400, 352],
|
||||||
|
// Al tramonto, quando la Ghiacciomobile è già un ricordo con la targa, lo zio Mario è nel suo orto (Andrea,
|
||||||
|
// 30 ottobre), e ti aspetta a cena.
|
||||||
|
speakers: { zio_mario: [250, 240] },
|
||||||
|
people: [{ who: 'zio_mario', when: 'indizio_regolo', at: [250, 318], facing: -1, idle: 'stand', talk: ['talk', 'explain', 'point'] }],
|
||||||
hotspots: [
|
hotspots: [
|
||||||
{
|
{
|
||||||
id: 'casa_zio', name: 'casa dello zio Mario', box: [80, 90, 200, 90], at: [260, 290], face: 'left',
|
id: 'casa_zio', name: 'casa dello zio Mario', box: [80, 90, 200, 90], at: [260, 290], face: 'left',
|
||||||
@@ -3355,6 +3390,22 @@ export const story = {
|
|||||||
{
|
{
|
||||||
id: 'indietro', name: 'indietro, verso il ponte delle poste', box: [150, 336, 490, 24], at: [400, 352], exit: { scene: 'ponte_stazione', at: [158, 192], face: 'down' },
|
id: 'indietro', name: 'indietro, verso il ponte delle poste', box: [150, 336, 490, 24], at: [400, 352], exit: { scene: 'ponte_stazione', at: [158, 192], face: 'down' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'zio_mario', name: 'lo zio Mario', box: [228, 248, 44, 72], at: [300, 330], face: 'left', default: 'talk', when: 'indizio_regolo',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Lo zio Mario nel suo orto, col sole che va giù dietro la casa.' }],
|
||||||
|
// Le battute sono di Andrea (30 ottobre).
|
||||||
|
talk: [
|
||||||
|
{ say: 'Ti s\'aspetta a cena, che c\'è la vitella di maiale.', who: 'zio_mario', pose: 'explain' },
|
||||||
|
{ say: 'Non ti avevo visto, pensavo tu fossi andato all\'estero.' },
|
||||||
|
{ say: 'Io ho girato tanto... il Falcione.', who: 'zio_mario', pose: 'laugh' },
|
||||||
|
{ say: 'Zio, che sei stanco?' },
|
||||||
|
{ say: 'No, dormo!', who: 'zio_mario', pose: 'laugh' },
|
||||||
|
{ say: 'Zio, a me mi garba Michael Jackson!' },
|
||||||
|
{ say: 'Mai col gesso!', who: 'zio_mario', pose: 'laugh' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// La strada fra il bivio della Provinciale (alle spalle) e la stazione (in fondo); a destra si va al campone delle
|
// La strada fra il bivio della Provinciale (alle spalle) e la stazione (in fondo); a destra si va al campone delle
|
||||||
@@ -3935,6 +3986,10 @@ export const story = {
|
|||||||
{ say: 'Il cartone sull\'albero, le lattine sulla panchina, le bottigliette sulla porta e il palloncino all\'altalena.', who: 'taglia', pose: 'explain' },
|
{ say: 'Il cartone sull\'albero, le lattine sulla panchina, le bottigliette sulla porta e il palloncino all\'altalena.', who: 'taglia', pose: 'explain' },
|
||||||
{ say: 'E se la vuoi seria, ogni tanto ti tiro una lattina in aria.', who: 'taglia', pose: 'laugh' },
|
{ say: 'E se la vuoi seria, ogni tanto ti tiro una lattina in aria.', who: 'taglia', pose: 'laugh' },
|
||||||
] },
|
] },
|
||||||
|
{ text: 'E una partita a campana, Taglia?', next: 'campana', then: [
|
||||||
|
{ say: 'A campana? Alla nostra età?', who: 'taglia', pose: 'laugh' },
|
||||||
|
{ say: 'Va bene: il gesso ce l\'ho in tasca, il sassolino lo scelgo io. Sul vialetto, dalla 1 alla 8, e chi pesta la riga passa la mano.', who: 'taglia', pose: 'explain' },
|
||||||
|
] },
|
||||||
{ text: 'Ma quanto sei alto, Taglia?', then: [{ say: 'Abbastanza da vedere i pallini arrivare prima di te.', who: 'taglia', pose: 'laugh' }] },
|
{ text: 'Ma quanto sei alto, Taglia?', then: [{ say: 'Abbastanza da vedere i pallini arrivare prima di te.', who: 'taglia', pose: 'laugh' }] },
|
||||||
{ text: 'Ci si vede, Taglia.', next: 'end' },
|
{ text: 'Ci si vede, Taglia.', next: 'end' },
|
||||||
],
|
],
|
||||||
@@ -3947,6 +4002,14 @@ export const story = {
|
|||||||
{ text: 'No, dopo.', next: 'start' },
|
{ text: 'No, dopo.', next: 'start' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
campana: {
|
||||||
|
options: [
|
||||||
|
{ text: 'Alla bona.', next: 'end', then: [{ say: 'Alla bona: le righe un contano. Comincia te.', who: 'taglia', pose: 'talk' }, { closeup: 'campana', level: 'bona' }] },
|
||||||
|
{ text: 'Seria.', next: 'end', then: [{ say: 'Seria: chi tocca la riga è fuori. Comincia te.', who: 'taglia', pose: 'explain' }, { closeup: 'campana', level: 'seria' }] },
|
||||||
|
{ text: 'A fassi male.', next: 'end', then: [{ say: 'A fassi male? Io la campana la facevo a occhi chiusi.', who: 'taglia', pose: 'laugh' }, { closeup: 'campana', level: 'male' }] },
|
||||||
|
{ text: 'No, dopo.', next: 'start' },
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Il Chiaru al parcheggio di Cardetole, accanto alla pista col gesso (Andrea, 29 ottobre). [DA ANDREA: battute provvisorie]
|
// Il Chiaru al parcheggio di Cardetole, accanto alla pista col gesso (Andrea, 29 ottobre). [DA ANDREA: battute provvisorie]
|
||||||
|
|||||||
+1
-1
@@ -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.54';
|
export const VERSION = '0.1.57';
|
||||||
|
|
||||||
// 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).
|
||||||
|
|||||||
@@ -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 { 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 { 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 { 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 { 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 { 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 { 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 { font-size: 9px; }
|
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help, #campana-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,75 @@
|
|||||||
|
// La campana nel browser vero: dal Taglia al campone delle poste, il tiro col mouse e col dito, i salti.
|
||||||
|
// npm run browser:campana — schermate in /tmp/regolo-campana.
|
||||||
|
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-campana';
|
||||||
|
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('giardini_poste'); settle(game);
|
||||||
|
game.act('talk', 'taglia'); settle(game);
|
||||||
|
game.choose(game.options().findIndex(o => o.text.startsWith('E una partita a campana'))); 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/campone-campana.png'));
|
||||||
|
assert.equal(await page.locator('#bocce-throw').textContent(), 'Tira');
|
||||||
|
assert.ok(await page.locator('#campana-help').isVisible());
|
||||||
|
assert.ok(!(await page.locator('#bocce-left').isVisible()), 'niente pulsanti Mira');
|
||||||
|
const box = await page.locator('#scene').boundingBox(), P = (x, y) => [box.x + x / 640 * box.width, box.y + y / 360 * box.height];
|
||||||
|
// Mira alla 1, poi la forza giusta: col mouse tenendo premuto, col dito il cursore e Tira.
|
||||||
|
const one = await page.evaluate(async () => { const m = window.campanaModule = await import('/src/engine/campana.js'); const p = m.center(1); return [p.x, p.y, Math.hypot(p.x - m.FROM.x, p.y - m.FROM.y) / m.REACH]; });
|
||||||
|
if (mobile) {
|
||||||
|
await page.touchscreen.tap(...P(one[0], one[1]));
|
||||||
|
assert.equal(await page.evaluate(() => game.closeup.step), 'aim', 'un tocco mira soltanto');
|
||||||
|
await page.evaluate(p => { game.closeup.power = p; game.closeup.skill.scatter = 0; }, one[2]);
|
||||||
|
await page.locator('#bocce-throw').tap();
|
||||||
|
} else {
|
||||||
|
await page.mouse.move(...P(one[0], one[1]));
|
||||||
|
await page.evaluate(() => { game.closeup.skill.scatter = 0; });
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.waitForFunction(p => Math.abs(game.closeup.power - p) < 0.02, one[2], { polling: 'raf' });
|
||||||
|
await page.screenshot({ path: `${out}/${name}-forza.png` });
|
||||||
|
await page.mouse.up();
|
||||||
|
// Col mouse il momento giusto dipende da quanto è lesta la pagina: se è andato fuori, si ritira col pulsante.
|
||||||
|
await page.waitForFunction(() => !['aim', 'flying'].includes(game.closeup.step));
|
||||||
|
if (await page.evaluate(() => game.closeup.step) !== 'hop') {
|
||||||
|
await page.evaluate(p => { Object.assign(game.closeup, { step: 'aim', current: 'player', next: null, line: null, power: p }); }, one[2]);
|
||||||
|
await page.locator('#bocce-throw').click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await page.waitForFunction(() => game.closeup.step === 'hop', null, { timeout: 5000 }).catch(async e => { console.log(await page.evaluate(() => JSON.stringify({ ...game.closeup, events: 0 }))); await page.screenshot({ path: `${out}/${name}-errore.png` }); throw e; });
|
||||||
|
assert.equal(await page.locator('#bocce-throw').textContent(), 'Salta');
|
||||||
|
// Quanti salti riescono: la lancetta dopo una schermata (lenta, con swiftshader) può già essere scappata.
|
||||||
|
let hops = 0;
|
||||||
|
// I salti: si preme quando la lancetta passa nel verde (col dito sul pulsante Salta, col mouse sulla scena).
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
await page.waitForFunction(() => { const m = window.campanaModule, c = game.closeup; return c.step !== 'hop' || ((!c.hop || c.hop.t >= m.HOP) && c.meter.t > 0 && Math.abs(m.needle(c)) < 0.15); }, null, { polling: 'raf', timeout: 5000 });
|
||||||
|
if (await page.evaluate(() => game.closeup.step) !== 'hop') break;
|
||||||
|
if (mobile) await page.locator('#bocce-throw').tap(); else await page.mouse.click(...P(320, 200));
|
||||||
|
hops = Math.max(hops, await page.evaluate(() => game.closeup.beat + 1));
|
||||||
|
if (i === 1) await page.screenshot({ path: `${out}/${name}-salti.png` });
|
||||||
|
}
|
||||||
|
assert.ok(hops >= 2, `salti riusciti: ${hops}`);
|
||||||
|
// Poi tocca al Taglia: lo si fa sbagliare per vederlo saltare.
|
||||||
|
await page.evaluate(() => { const c = game.closeup; c.step = 'rest'; c.t = 0; c.next = 'cpu'; c.line = 'Prova: tocca al Taglia.'; });
|
||||||
|
await page.waitForFunction(() => game.closeup.step === 'cpuHop' && game.closeup.beat >= 3 || game.closeup.current === 'player' && game.closeup.step === 'aim', null, { timeout: 20000 });
|
||||||
|
await page.screenshot({ path: `${out}/${name}-taglia.png` });
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
console.log(`ok: schermate in ${out}`);
|
||||||
|
} finally { await browser?.close(); server.kill(); }
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as m from '../src/engine/campana.js';
|
||||||
|
import { drawCampana } from '../src/engine/campana-render.js';
|
||||||
|
import { Game, MINIGAMES } from '../src/engine/game.js';
|
||||||
|
import { story } from '../src/story/story.js';
|
||||||
|
import { settle } from './helpers.mjs';
|
||||||
|
|
||||||
|
// Tira senza sparpaglio, con la forza giusta per arrivare al punto, e aspetta che il sassolino cada.
|
||||||
|
function toss(c, [x, y]) {
|
||||||
|
c.skill = { ...c.skill, scatter: 0 }; c.aim = { x, y };
|
||||||
|
c.power = Math.hypot(x - m.FROM.x, y - m.FROM.y) / m.REACH;
|
||||||
|
assert.ok(m.shoot(c));
|
||||||
|
for (let i = 0; i < 200 && c.step === 'flying'; i++) m.update(c, 1 / 60);
|
||||||
|
}
|
||||||
|
// Salta ogni volta che la lancetta passa nel mezzo.
|
||||||
|
function hopAll(c) {
|
||||||
|
for (let i = 0; i < 20000 && c.step === 'hop'; i++) {
|
||||||
|
m.update(c, 1 / 120);
|
||||||
|
if (c.step === 'hop' && (!c.hop || c.hop.t >= m.HOP) && Math.abs(m.needle(c)) < 0.03 && c.meter.t > 0) m.press(c, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const at = id => { const p = m.center(id); return [p.x, p.y]; };
|
||||||
|
|
||||||
|
test('le caselle: 1-2-3, la coppia 4-5, la 6, la coppia 7-8 e il cielo; le righe', () => {
|
||||||
|
for (let n = 1; n <= 8; n++) assert.equal(m.boxAt(at(n), 3).id, n);
|
||||||
|
assert.equal(m.boxAt(at('cielo')).id, 'cielo');
|
||||||
|
assert.ok(m.center(4).y < m.center(5).y, 'la 4 a sinistra di chi salta');
|
||||||
|
const b = m.BOXES[2];
|
||||||
|
assert.ok(m.boxAt([b.x + 1, m.CY], 3).line, 'sul bordo si tocca la riga');
|
||||||
|
assert.ok(!m.boxAt([b.x + 1, m.CY], 0).line, 'alla bona le righe non contano');
|
||||||
|
assert.equal(m.boxAt([20, 20]), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('i salti: si scavalca la casella col sassolino, nella coppia un piede solo; tornando lo si raccoglie', () => {
|
||||||
|
const ids = list => list.map(b => b.pick ? 'prendi' : b.out ? 'fuori' : b.boxes.join('+'));
|
||||||
|
assert.deepEqual(ids(m.beats(1)), ['2', '3', '4+5', '6', '7+8', 'cielo', '7+8', '6', '4+5', '3', '2', 'prendi', 'fuori']);
|
||||||
|
assert.deepEqual(ids(m.beats(5)), ['1', '2', '3', '4', '6', '7+8', 'cielo', '7+8', '6', 'prendi', '4', '3', '2', '1', 'fuori']);
|
||||||
|
assert.deepEqual(ids(m.beats(8)), ['1', '2', '3', '4+5', '6', '7', 'cielo', 'prendi', '7', '6', '4+5', '3', '2', '1', 'fuori']);
|
||||||
|
assert.ok(m.beats(3).every(b => !b.boxes.includes(3)), 'mai il piede dove sta il sassolino');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il sassolino nella casella giusta fa saltare; tutta la campana fatta, si passa alla dopo', () => {
|
||||||
|
const c = m.newCampana(1, { level: 'seria' });
|
||||||
|
toss(c, at(1));
|
||||||
|
assert.equal(c.step, 'hop'); assert.match(c.line, /nella 1/);
|
||||||
|
hopAll(c);
|
||||||
|
assert.equal(c.step, 'done'); assert.equal(c.target.player, 2); assert.equal(c.stone, null, 'raccolto');
|
||||||
|
for (let i = 0; i < 120 && c.step !== 'aim'; i++) m.update(c, 1 / 60);
|
||||||
|
assert.equal(c.current, 'player', 'chi ce la fa ritira');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fuori, sulla riga o nella casella sbagliata passa la mano al Taglia', () => {
|
||||||
|
for (const [point, why] of [[at(2), /Nella 2, non nella 1/], [[m.START + 1, m.CY], /riga/], [[60, 40], /fuori/i]]) {
|
||||||
|
const c = m.newCampana(2, { level: 'seria' });
|
||||||
|
toss(c, point);
|
||||||
|
assert.equal(c.step, 'rest'); assert.match(c.line, why);
|
||||||
|
for (let i = 0; i < 200 && c.step === 'rest'; i++) m.update(c, 1 / 60);
|
||||||
|
assert.equal(c.current, 'cpu'); assert.equal(c.target.player, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('l\'equilibrio: troppo presto o troppo tardi si sbaglia', () => {
|
||||||
|
const early = m.newCampana(3, { level: 'male' });
|
||||||
|
toss(early, at(1));
|
||||||
|
while (early.meter.t < 0.05) m.update(early, 1 / 120);
|
||||||
|
m.press(early, 1); assert.equal(early.step, 'rest'); assert.match(early.line, /presto/);
|
||||||
|
const late = m.newCampana(3, { level: 'male' });
|
||||||
|
toss(late, at(1));
|
||||||
|
for (let i = 0; i < 1000 && late.step === 'hop'; i++) m.update(late, 1 / 60);
|
||||||
|
assert.match(late.line, /equilibrio/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il tiro col dito: un tocco secco mira soltanto; tenendo premuto la forza va e torna', () => {
|
||||||
|
const c = m.newCampana(4, { level: 'bona' });
|
||||||
|
assert.ok(m.aimScreen(c, [300, 150]));
|
||||||
|
m.press(c, 1); m.update(c, 0.05); m.release(c, 1); assert.equal(c.step, 'aim');
|
||||||
|
m.press(c, 1);
|
||||||
|
for (let i = 0; i < 60 * m.CHARGE; i++) m.update(c, 1 / 60);
|
||||||
|
assert.ok(c.power > 0.95);
|
||||||
|
for (let i = 0; i < 30 * m.CHARGE; i++) m.update(c, 1 / 60);
|
||||||
|
assert.ok(c.power < 0.6, 'e poi torna giù');
|
||||||
|
assert.ok(m.release(c, 1)); assert.equal(c.step, 'flying');
|
||||||
|
assert.ok(m.key(c, 'ArrowUp', true));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il Taglia gioca da solo; chi finisce la 8 vince', () => {
|
||||||
|
const c = m.newCampana(5, { level: 'seria' });
|
||||||
|
toss(c, [60, 40]);
|
||||||
|
let turns = 0;
|
||||||
|
for (let i = 0; i < 60 * 600 && c.step !== 'result'; i++) {
|
||||||
|
m.update(c, 1 / 60);
|
||||||
|
if (c.current === 'player' && c.step === 'aim') { turns++; toss(c, [60, 40]); }
|
||||||
|
}
|
||||||
|
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'cpu'); assert.equal(c.target.cpu, 9);
|
||||||
|
assert.ok(turns > 0, 'il Taglia ogni tanto sbaglia e ripassa la mano');
|
||||||
|
const w = m.newCampana(6, { level: 'bona' });
|
||||||
|
w.target.player = 8;
|
||||||
|
toss(w, at(8)); hopAll(w);
|
||||||
|
assert.equal(w.step, 'result'); assert.equal(w.result.winner, 'player');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il disegno della campana 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.newCampana(7, { level: 'bona' }), bg = {};
|
||||||
|
const draw = () => { const before = structuredClone(c); drawCampana(ctx, c, story.closeups.campana, 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 === 'LA CAMPANA'));
|
||||||
|
toss(c, at(1)); for (let i = 0; i < 40; i++) m.update(c, 1 / 60); draw();
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'EQUILIBRIO'));
|
||||||
|
c.step = 'result'; c.result = { winner: 'player' }; calls.length = 0; draw();
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'BATTUTO IL TAGLIA!'));
|
||||||
|
assert.ok(MINIGAMES.campana);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dal Taglia, al campone delle poste: la campana, il livello e la vittoria', () => {
|
||||||
|
const g = new Game(story); g.go('giardini_poste'); settle(g);
|
||||||
|
g.act('talk', 'taglia'); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text.startsWith('E una partita a campana'))); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'Seria.')); settle(g);
|
||||||
|
assert.equal(g.closeup?.id, 'campana'); assert.equal(g.closeup.level, 'seria');
|
||||||
|
g.closeup.step = 'result'; g.closeup.result = { winner: 'player' };
|
||||||
|
g.closeupClose(); settle(g);
|
||||||
|
assert.ok(g.state.flags.campana_vinta);
|
||||||
|
});
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import * as C from '../src/engine/corsa.js';
|
||||||
|
import { story } from '../src/story/story.js';
|
||||||
|
|
||||||
|
const run = (def, until) => { const c = C.newCorsa('x'); while (c.t < until) C.updateCorsa(c, def, 1 / 60); return c; };
|
||||||
|
|
||||||
|
test('la partenza: accelera giù per la strada, il fulmine la prende e sparisce; restano le sgommate', () => {
|
||||||
|
const def = story.closeups.partenza;
|
||||||
|
assert.equal(def.mode, 'parti');
|
||||||
|
for (const src of Object.values(def.frames)) assert.ok(existsSync(src), src);
|
||||||
|
const at = t => C.progress({ t }, def);
|
||||||
|
assert.equal(at(0), 0);
|
||||||
|
assert.ok(at(1.2) > 0 && at(1.6) - at(1.2) > at(1.2) - at(0.8), 'accelera');
|
||||||
|
assert.ok(C.carShown({ t: 2 }, def) && !C.carShown({ t: C.PARTI.bolt + 0.2 }, def));
|
||||||
|
const c = run(def, C.PARTI.bolt + 0.1);
|
||||||
|
assert.ok(c.events.some(e => e.kind === 'corsa-tuono') && c.events.some(e => e.kind === 'corsa-fuoco'));
|
||||||
|
assert.ok(!C.updateCorsa(c, def, 0.01));
|
||||||
|
assert.ok(C.updateCorsa(run(def, C.PARTI.end - 0.01), def, 0.05), 'finita');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('l\'arrivo: il fulmine in fondo, poi viene avanti e ci passa accanto, fuori dal quadro', () => {
|
||||||
|
const def = story.closeups.arrivo;
|
||||||
|
const at = t => C.progress({ t }, def);
|
||||||
|
assert.equal(at(0), 1); assert.equal(at(C.ARRIVA.bolt + C.ARRIVA.run + 0.1), 0);
|
||||||
|
assert.ok(!C.carShown({ t: 0.1 }, def) && C.carShown({ t: 1 }, def));
|
||||||
|
assert.ok(!C.carShown({ t: C.ARRIVA.bolt + C.ARRIVA.run + 0.1 }, def), 'passata');
|
||||||
|
assert.ok(at(1.2) < at(0.8));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il disegno della corsa non cambia lo stato e usa lo sfondo e i fotogrammi', () => {
|
||||||
|
const calls = [], ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => calls.push([k, ...args]) });
|
||||||
|
const bg = { width: 640, height: 360 }, frames = { dietro: { width: 220, height: 190 }, lampi: { width: 220, height: 190 }, davanti: { width: 220, height: 190 } };
|
||||||
|
for (const [id, t] of [['partenza', 1.4], ['partenza', 3.5], ['arrivo', 1.2], ['arrivo', 2.5]]) {
|
||||||
|
const c = { ...C.newCorsa(id), t }, before = structuredClone(c);
|
||||||
|
calls.length = 0;
|
||||||
|
C.drawCorsa(ctx, c, story.closeups[id], { bg, frames, tint: null });
|
||||||
|
assert.deepEqual(c, before);
|
||||||
|
assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
|
||||||
|
assert.equal(calls.some(([op, img]) => op === 'drawImage' && Object.values(frames).includes(img)), t < 2.4, 'dopo il fulmine (o passata) la macchina non c\'è');
|
||||||
|
assert.ok(calls.filter(([op]) => op === 'fillRect').length > 20, 'le fiamme');
|
||||||
|
}
|
||||||
|
});
|
||||||
+17
-6
@@ -5,6 +5,18 @@ import { drawPoker, cardAt, HAND } from '../src/engine/poker-render.js';
|
|||||||
import { Game, MINIGAMES, TRIP } from '../src/engine/game.js';
|
import { Game, MINIGAMES, TRIP } from '../src/engine/game.js';
|
||||||
import { story } from '../src/story/story.js';
|
import { story } from '../src/story/story.js';
|
||||||
import { settle } from './helpers.mjs';
|
import { settle } from './helpers.mjs';
|
||||||
|
import { PARTI, ARRIVA } from '../src/engine/corsa.js';
|
||||||
|
|
||||||
|
// I primi piani che vanno da soli (il cruscotto, la partenza, l'arrivo): si lascia scorrere il tempo finché finiscono.
|
||||||
|
function ride(g, seen = []) {
|
||||||
|
for (let t = 0; t < TRIP.end + PARTI.end + ARRIVA.end + 2; t += 0.05) {
|
||||||
|
if (g.closeup && seen.at(-1) !== g.closeup.id) seen.push(g.closeup.id);
|
||||||
|
if (!g.closeup && !g.queue.length && !g.speech) break;
|
||||||
|
if (g.speech) g.skip(); else g.update(0.05);
|
||||||
|
}
|
||||||
|
settle(g);
|
||||||
|
return seen;
|
||||||
|
}
|
||||||
|
|
||||||
const card = s => ({ rank: s.slice(0, -1), suit: { c: 'cuori', q: 'quadri', f: 'fiori', p: 'picche' }[s.at(-1)] });
|
const card = s => ({ rank: s.slice(0, -1), suit: { c: 'cuori', q: 'quadri', f: 'fiori', p: 'picche' }[s.at(-1)] });
|
||||||
const hand = s => s.split(' ').map(card);
|
const hand = s => s.split(' ').map(card);
|
||||||
@@ -71,8 +83,7 @@ test('la macchina del tempo: l\'Arcu la fa comparire, il viaggio porta al Barret
|
|||||||
assert.ok(!g.dialogue, 'finito il discorso, si è pronti a partire');
|
assert.ok(!g.dialogue, 'finito il discorso, si è pronti a partire');
|
||||||
g.act('use', 'macchina_tempo'); settle(g);
|
g.act('use', 'macchina_tempo'); settle(g);
|
||||||
assert.equal(g.closeup?.id, 'viaggio');
|
assert.equal(g.closeup?.id, 'viaggio');
|
||||||
for (let t = 0; t < TRIP.end + 0.5 && g.closeup; t += 0.05) g.update(0.05);
|
assert.deepEqual(ride(g), ['viaggio', 'partenza'], 'il cruscotto, poi la partenza da fuori');
|
||||||
settle(g);
|
|
||||||
assert.equal(g.state.scene, 'barrettino_1997');
|
assert.equal(g.state.scene, 'barrettino_1997');
|
||||||
g.act('talk', 'tavola'); settle(g);
|
g.act('talk', 'tavola'); settle(g);
|
||||||
g.choose(g.options().findIndex(o => o.text.startsWith('C\'è posto'))); settle(g);
|
g.choose(g.options().findIndex(o => o.text.startsWith('C\'è posto'))); settle(g);
|
||||||
@@ -82,7 +93,8 @@ test('la macchina del tempo: l\'Arcu la fa comparire, il viaggio porta al Barret
|
|||||||
g.closeupClose(); settle(g);
|
g.closeupClose(); settle(g);
|
||||||
assert.ok(g.state.flags.poker_vinto);
|
assert.ok(g.state.flags.poker_vinto);
|
||||||
assert.ok(g.hotspot('arcu'), 'l\'Arcu è venuto anche lui');
|
assert.ok(g.hotspot('arcu'), 'l\'Arcu è venuto anche lui');
|
||||||
g.act('walk', 'porta'); settle(g);
|
g.act('walk', 'porta');
|
||||||
|
assert.deepEqual(ride(g), ['arrivo'], 'al ritorno, l\'arrivo col fulmine');
|
||||||
assert.equal(g.state.scene, 'via_liguri_magelli');
|
assert.equal(g.state.scene, 'via_liguri_magelli');
|
||||||
assert.ok(!g.state.flags.macchina_trappola && !g.hotspot('macchina_tempo'), 'tornati, la macchina sparisce');
|
assert.ok(!g.state.flags.macchina_trappola && !g.hotspot('macchina_tempo'), 'tornati, la macchina sparisce');
|
||||||
assert.equal(g.goBack(), false, 'nel 2007 il pulsante non c\'è');
|
assert.equal(g.goBack(), false, 'nel 2007 il pulsante non c\'è');
|
||||||
@@ -91,11 +103,10 @@ test('la macchina del tempo: l\'Arcu la fa comparire, il viaggio porta al Barret
|
|||||||
g.choose(g.options().findIndex(o => o.text.startsWith('Bei tempi'))); settle(g);
|
g.choose(g.options().findIndex(o => o.text.startsWith('Bei tempi'))); settle(g);
|
||||||
assert.ok(!g.dialogue, 'finito il discorso, si è pronti a partire');
|
assert.ok(!g.dialogue, 'finito il discorso, si è pronti a partire');
|
||||||
g.act('use', 'macchina_tempo'); settle(g);
|
g.act('use', 'macchina_tempo'); settle(g);
|
||||||
for (let t = 0; t < TRIP.end + 0.5 && g.closeup; t += 0.05) g.update(0.05);
|
ride(g);
|
||||||
settle(g);
|
|
||||||
assert.equal(g.state.scene, 'barrettino_1997');
|
assert.equal(g.state.scene, 'barrettino_1997');
|
||||||
assert.equal(g.scene.back.label, 'Torna al 2007');
|
assert.equal(g.scene.back.label, 'Torna al 2007');
|
||||||
assert.ok(g.goBack()); settle(g);
|
assert.ok(g.goBack()); ride(g);
|
||||||
assert.equal(g.state.scene, 'via_liguri_magelli');
|
assert.equal(g.state.scene, 'via_liguri_magelli');
|
||||||
assert.ok(!g.hotspot('macchina_tempo'));
|
assert.ok(!g.hotspot('macchina_tempo'));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -75,3 +75,17 @@ test('Via Liguri Magelli: prima la Ghiacciomobile nel muro (senza targa), dal tr
|
|||||||
assert.ok(g.hotspot('targa') && !g.hotspot('ghiacciomobile'));
|
assert.ok(g.hotspot('targa') && !g.hotspot('ghiacciomobile'));
|
||||||
assert.ok(existsSync(new URL('../assets/scenes/via-liguri-magelli-muro.png', import.meta.url)));
|
assert.ok(existsSync(new URL('../assets/scenes/via-liguri-magelli-muro.png', import.meta.url)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('lo zio Mario: davanti alla Ghiacciomobile fino al tramonto, poi nel suo orto', () => {
|
||||||
|
const g = new Game(story);
|
||||||
|
g.go('via_liguri_magelli'); settle(g);
|
||||||
|
assert.ok(g.hotspot('zio_mario'));
|
||||||
|
g.go('orti_zio_mario'); settle(g);
|
||||||
|
assert.ok(!g.hotspot('zio_mario'));
|
||||||
|
g.state.flags.indizio_regolo = true;
|
||||||
|
assert.ok(g.hotspot('zio_mario'));
|
||||||
|
assert.ok(inside(story.scenes.orti_zio_mario.walk, g.hotspot('zio_mario').at));
|
||||||
|
g.act('talk', 'zio_mario'); settle(g);
|
||||||
|
g.go('via_liguri_magelli'); settle(g);
|
||||||
|
assert.ok(!g.hotspot('zio_mario'));
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user