Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fde68603cb | ||
|
|
686fee043e | ||
|
|
4b709ffd80 | ||
|
|
733680301c | ||
|
|
0ba8cfc800 | ||
|
|
84ad19a47e | ||
|
|
058acd6325 | ||
|
|
497209fc9a | ||
|
|
2289480812 | ||
|
|
de418c0da8 | ||
|
|
713e328138 | ||
|
|
6a5a80d7af | ||
|
|
88f0439f79 | ||
|
|
c244424af9 | ||
|
|
c26d888263 | ||
|
|
f8dbd01358 | ||
|
|
e3d5d8b0d3 | ||
|
|
1b7511f6ca | ||
|
|
9d2cc2fcb3 | ||
|
|
44480221db |
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"image": "zio_mario.png",
|
||||||
|
"height": 120,
|
||||||
|
"frames": {
|
||||||
|
"explain": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 67,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 33.8,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"talk": {
|
||||||
|
"x": 69,
|
||||||
|
"y": 0,
|
||||||
|
"w": 79,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 44.8,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"blink": {
|
||||||
|
"x": 150,
|
||||||
|
"y": 0,
|
||||||
|
"w": 57,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 28.1,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"stand": {
|
||||||
|
"x": 209,
|
||||||
|
"y": 0,
|
||||||
|
"w": 57,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 27.8,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"shrug": {
|
||||||
|
"x": 268,
|
||||||
|
"y": 0,
|
||||||
|
"w": 94,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 48.1,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"point": {
|
||||||
|
"x": 364,
|
||||||
|
"y": 0,
|
||||||
|
"w": 87,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 51.0,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"think": {
|
||||||
|
"x": 453,
|
||||||
|
"y": 0,
|
||||||
|
"w": 58,
|
||||||
|
"h": 121,
|
||||||
|
"ax": 29.2,
|
||||||
|
"ay": 121
|
||||||
|
},
|
||||||
|
"laugh": {
|
||||||
|
"x": 513,
|
||||||
|
"y": 0,
|
||||||
|
"w": 59,
|
||||||
|
"h": 121,
|
||||||
|
"ax": 30.1,
|
||||||
|
"ay": 121
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 228 KiB |
@@ -95,6 +95,10 @@
|
|||||||
"file": "andrea-a9df2e6bbb.mp3",
|
"file": "andrea-a9df2e6bbb.mp3",
|
||||||
"sec": 4.98
|
"sec": 4.98
|
||||||
},
|
},
|
||||||
|
"andrea|La bottega di Pierin di Frilli, sotto la tenda rossa. Da ragazzi ci si comprava lo stucco per la cerbottana.": {
|
||||||
|
"file": "andrea-cb15f8f919.mp3",
|
||||||
|
"sec": 7.29
|
||||||
|
},
|
||||||
"andrea|Il Circolo 4 Maggio. Dentro, dietro il bar, la sala dei biliardi: tre tavoli e il silenzio delle grandi occasioni.": {
|
"andrea|Il Circolo 4 Maggio. Dentro, dietro il bar, la sala dei biliardi: tre tavoli e il silenzio delle grandi occasioni.": {
|
||||||
"file": "andrea-f0aef83c80.mp3",
|
"file": "andrea-f0aef83c80.mp3",
|
||||||
"sec": 9.43
|
"sec": 9.43
|
||||||
@@ -1187,14 +1191,6 @@
|
|||||||
"file": "andrea-00f2e70a4d.mp3",
|
"file": "andrea-00f2e70a4d.mp3",
|
||||||
"sec": 2.95
|
"sec": 2.95
|
||||||
},
|
},
|
||||||
"andrea|La Ghiacciomobile nera. Hai visto cose che noi poveri umani non potremmo neppur lontanamente immaginare.": {
|
|
||||||
"file": "andrea-7be29434e0.mp3",
|
|
||||||
"sec": 7.25
|
|
||||||
},
|
|
||||||
"andrea|Con questo sole, di ghiaccio c'è rimasto giusto il nome. Vo a piedi.": {
|
|
||||||
"file": "andrea-def44809bc.mp3",
|
|
||||||
"sec": 5.08
|
|
||||||
},
|
|
||||||
"andrea|Il circolino: il Circolo SMS di Campomigliaio. Un'istituzione, qui i volontari hanno sempre fatto miracoli.": {
|
"andrea|Il circolino: il Circolo SMS di Campomigliaio. Un'istituzione, qui i volontari hanno sempre fatto miracoli.": {
|
||||||
"file": "andrea-90bbad0de0.mp3",
|
"file": "andrea-90bbad0de0.mp3",
|
||||||
"sec": 9.59
|
"sec": 9.59
|
||||||
@@ -1627,6 +1623,14 @@
|
|||||||
"file": "andrea-7c17aa384c.mp3",
|
"file": "andrea-7c17aa384c.mp3",
|
||||||
"sec": 3.2
|
"sec": 3.2
|
||||||
},
|
},
|
||||||
|
"andrea|La Ghiacciomobile nera, mezza dentro il muro. Hai visto cose che noi poveri umani non potremmo neppur lontanamente immaginare.": {
|
||||||
|
"file": "andrea-decbc1c807.mp3",
|
||||||
|
"sec": 9.49
|
||||||
|
},
|
||||||
|
"andrea|Per tirarla fuori di lì ci vuole altro che le mie braccia.": {
|
||||||
|
"file": "andrea-a193a43f3e.mp3",
|
||||||
|
"sec": 3.78
|
||||||
|
},
|
||||||
"andrea|Nessuno ha mai capito davvero il tuo immenso valore.": {
|
"andrea|Nessuno ha mai capito davvero il tuo immenso valore.": {
|
||||||
"file": "andrea-3e85200509.mp3",
|
"file": "andrea-3e85200509.mp3",
|
||||||
"sec": 2.93
|
"sec": 2.93
|
||||||
@@ -1639,9 +1643,17 @@
|
|||||||
"file": "andrea-3d15dd47bb.mp3",
|
"file": "andrea-3d15dd47bb.mp3",
|
||||||
"sec": 4.82
|
"sec": 4.82
|
||||||
},
|
},
|
||||||
"andrea|Il Chiaru, con le ginocchia bianche di gesso e una Formula 1 blu in mano.": {
|
"andrea|Lo zio Mario. Guarda il muro, guarda la macchina, e si gratta la testa sotto il berretto.": {
|
||||||
"file": "andrea-8d5911c8e7.mp3",
|
"file": "andrea-9da3d936d3.mp3",
|
||||||
"sec": 4.53
|
"sec": 6.14
|
||||||
|
},
|
||||||
|
"andrea|Zio, hai problemi?": {
|
||||||
|
"file": "andrea-c36f0dd2c5.mp3",
|
||||||
|
"sec": 1.52
|
||||||
|
},
|
||||||
|
"zio_mario|I problemi io gli avevo quando andavo a scuola!": {
|
||||||
|
"file": "zio_mario-b3e99c241a.mp3",
|
||||||
|
"sec": 2.68
|
||||||
},
|
},
|
||||||
"andrea|Il parcheggio sotto casa. Col gesso ci si disegnava la pista delle macchinine, e si giocava finché non chiamavano per cena.": {
|
"andrea|Il parcheggio sotto casa. Col gesso ci si disegnava la pista delle macchinine, e si giocava finché non chiamavano per cena.": {
|
||||||
"file": "andrea-8670e6ce99.mp3",
|
"file": "andrea-8670e6ce99.mp3",
|
||||||
@@ -1651,10 +1663,6 @@
|
|||||||
"file": "andrea-6c9e7f684e.mp3",
|
"file": "andrea-6c9e7f684e.mp3",
|
||||||
"sec": 8.72
|
"sec": 8.72
|
||||||
},
|
},
|
||||||
"andrea|Se salgo, mi mettono ad apparecchiare. Prima il palio.": {
|
|
||||||
"file": "andrea-8945f1786d.mp3",
|
|
||||||
"sec": 3.89
|
|
||||||
},
|
|
||||||
"andrea|Il prato sotto casa. Qui le porte le facevamo con le maglie per terra: due maglie, un palo ciascuna.": {
|
"andrea|Il prato sotto casa. Qui le porte le facevamo con le maglie per terra: due maglie, un palo ciascuna.": {
|
||||||
"file": "andrea-4708fda5ef.mp3",
|
"file": "andrea-4708fda5ef.mp3",
|
||||||
"sec": 6.92
|
"sec": 6.92
|
||||||
@@ -1675,6 +1683,54 @@
|
|||||||
"file": "andrea-f6c10fd559.mp3",
|
"file": "andrea-f6c10fd559.mp3",
|
||||||
"sec": 6.28
|
"sec": 6.28
|
||||||
},
|
},
|
||||||
|
"andrea|Il Chiaru, con le ginocchia bianche di gesso e una Formula 1 blu in mano.": {
|
||||||
|
"file": "andrea-8d5911c8e7.mp3",
|
||||||
|
"sec": 4.53
|
||||||
|
},
|
||||||
|
"andrea|La vista di casa. Da qui si vede mezzo paese, e lassù la Fortezza.": {
|
||||||
|
"file": "andrea-8a63e250ae.mp3",
|
||||||
|
"sec": 5.64
|
||||||
|
},
|
||||||
|
"andrea|Lassù in cima, fra gli alberi, la Fortezza. Da qui pare piccina.": {
|
||||||
|
"file": "andrea-6544c0d778.mp3",
|
||||||
|
"sec": 6.17
|
||||||
|
},
|
||||||
|
"andrea|Il paese che sale verso la collina, coi tetti uno sull'altro e il campanile in mezzo.": {
|
||||||
|
"file": "andrea-b082638d5a.mp3",
|
||||||
|
"sec": 5.93
|
||||||
|
},
|
||||||
|
"andrea|Il palazzo accanto, di mattoni come il nostro.": {
|
||||||
|
"file": "andrea-8d0507f514.mp3",
|
||||||
|
"sec": 2.45
|
||||||
|
},
|
||||||
|
"andrea|I gerani. Guai a chi li tocca.": {
|
||||||
|
"file": "andrea-54d8dfd381.mp3",
|
||||||
|
"sec": 2.86
|
||||||
|
},
|
||||||
|
"andrea|Se ne strappo uno, il palio lo guardo dal letto.": {
|
||||||
|
"file": "andrea-02fe387f5d.mp3",
|
||||||
|
"sec": 3.45
|
||||||
|
},
|
||||||
|
"andrea|L'edera nel vaso: cresce da sola, e si arrampica dove le pare.": {
|
||||||
|
"file": "andrea-ab2bb4cae8.mp3",
|
||||||
|
"sec": 4.68
|
||||||
|
},
|
||||||
|
"andrea|Una sedia di plastica, piegata.": {
|
||||||
|
"file": "andrea-8b314f556d.mp3",
|
||||||
|
"sec": 2.05
|
||||||
|
},
|
||||||
|
"andrea|Sedermi qui a guardare il paese? Prima il palio.": {
|
||||||
|
"file": "andrea-7593ef5d63.mp3",
|
||||||
|
"sec": 3.41
|
||||||
|
},
|
||||||
|
"andrea|La porta-finestra di casa, con la tapparella a metà.": {
|
||||||
|
"file": "andrea-be7c2b9a45.mp3",
|
||||||
|
"sec": 3.16
|
||||||
|
},
|
||||||
|
"andrea|Se rientro, mi mettono ad apparecchiare. Prima il palio.": {
|
||||||
|
"file": "andrea-2e64b2d5c2.mp3",
|
||||||
|
"sec": 4.8
|
||||||
|
},
|
||||||
"andrea|Un pino a ombrello enorme. Alla ricreazione, tutti all'ombra lì sotto. Sembra ieri.": {
|
"andrea|Un pino a ombrello enorme. Alla ricreazione, tutti all'ombra lì sotto. Sembra ieri.": {
|
||||||
"file": "andrea-0a073729fd.mp3",
|
"file": "andrea-0a073729fd.mp3",
|
||||||
"sec": 7.3
|
"sec": 7.3
|
||||||
@@ -2220,8 +2276,8 @@
|
|||||||
"sec": 4.42
|
"sec": 4.42
|
||||||
},
|
},
|
||||||
"arcu|Fammi vedere... Verde. Come la Panda di mi' zio.": {
|
"arcu|Fammi vedere... Verde. Come la Panda di mi' zio.": {
|
||||||
"file": "arcu-62e981592d.mp3",
|
"file": "arcu-82526ee9fb.mp3",
|
||||||
"sec": 6.29
|
"sec": 4.52
|
||||||
},
|
},
|
||||||
"arcu|Verde! Allora il Regolo tifa Cardetole. Tradimento.": {
|
"arcu|Verde! Allora il Regolo tifa Cardetole. Tradimento.": {
|
||||||
"file": "arcu-9e5b7c6e95.mp3",
|
"file": "arcu-9e5b7c6e95.mp3",
|
||||||
@@ -2435,9 +2491,29 @@
|
|||||||
"file": "mariano-d65fb6e871.mp3",
|
"file": "mariano-d65fb6e871.mp3",
|
||||||
"sec": 3.42
|
"sec": 3.42
|
||||||
},
|
},
|
||||||
"andrea|Si corre?": {
|
"andrea|Una cerbottana e un po' di stucco, Pierino!": {
|
||||||
"file": "andrea-8052393bfd.mp3",
|
"file": "andrea-eaaf19eef0.mp3",
|
||||||
"sec": 0.7
|
"sec": 2.77
|
||||||
|
},
|
||||||
|
"andrea|Niente, grazie: guardavo.": {
|
||||||
|
"file": "andrea-12cbe6f9a8.mp3",
|
||||||
|
"sec": 2.26
|
||||||
|
},
|
||||||
|
"andrea|Lo stucco verde di Pierino: quell'odore non te lo levi più dalle dita.": {
|
||||||
|
"file": "andrea-2ede7e7324.mp3",
|
||||||
|
"sec": 3.82
|
||||||
|
},
|
||||||
|
"andrea|Ora un muro. Quello della piazza va benissimo.": {
|
||||||
|
"file": "andrea-9cc0991d39.mp3",
|
||||||
|
"sec": 3.32
|
||||||
|
},
|
||||||
|
"andrea|No, dopo.": {
|
||||||
|
"file": "andrea-78ad61a67a.mp3",
|
||||||
|
"sec": 1.22
|
||||||
|
},
|
||||||
|
"andrea|Si fa una gara alle macchinine?!?!": {
|
||||||
|
"file": "andrea-17216997e2.mp3",
|
||||||
|
"sec": 2.74
|
||||||
},
|
},
|
||||||
"andrea|Che pista hai disegnato?": {
|
"andrea|Che pista hai disegnato?": {
|
||||||
"file": "andrea-3dd737ed1b.mp3",
|
"file": "andrea-3dd737ed1b.mp3",
|
||||||
@@ -2447,9 +2523,9 @@
|
|||||||
"file": "andrea-ee03433b9c.mp3",
|
"file": "andrea-ee03433b9c.mp3",
|
||||||
"sec": 1.6
|
"sec": 1.6
|
||||||
},
|
},
|
||||||
"andrea|Dopo, Chiaru.": {
|
"andrea|Vado a fare un girino alle case rosse.": {
|
||||||
"file": "andrea-75b58c99e6.mp3",
|
"file": "andrea-cb2fa74266.mp3",
|
||||||
"sec": 1.32
|
"sec": 1.97
|
||||||
},
|
},
|
||||||
"chiaru|Un giro, una spinta a testa, prima te. Tutta fuori dal gesso, si torna indietro. Come la vuoi?": {
|
"chiaru|Un giro, una spinta a testa, prima te. Tutta fuori dal gesso, si torna indietro. Come la vuoi?": {
|
||||||
"file": "chiaru-df4b1e3a0e.mp3",
|
"file": "chiaru-df4b1e3a0e.mp3",
|
||||||
@@ -2475,9 +2551,13 @@
|
|||||||
"file": "chiaru-a7ab8d241f.mp3",
|
"file": "chiaru-a7ab8d241f.mp3",
|
||||||
"sec": 2.01
|
"sec": 2.01
|
||||||
},
|
},
|
||||||
"chiaru|La pista è qui. Finché non piove.": {
|
"chiaru|Vorrai dire un giochino!": {
|
||||||
"file": "chiaru-841aa1efd7.mp3",
|
"file": "chiaru-c106f9e9c4.mp3",
|
||||||
"sec": 2.39
|
"sec": 1.14
|
||||||
|
},
|
||||||
|
"andrea|Ciao Mattè.": {
|
||||||
|
"file": "andrea-fc5a08bbd6.mp3",
|
||||||
|
"sec": 0.73
|
||||||
},
|
},
|
||||||
"andrea|No, aspetta.": {
|
"andrea|No, aspetta.": {
|
||||||
"file": "andrea-04890c1d8e.mp3",
|
"file": "andrea-04890c1d8e.mp3",
|
||||||
@@ -2499,9 +2579,9 @@
|
|||||||
"file": "chiaru-a0d7dbb4c1.mp3",
|
"file": "chiaru-a0d7dbb4c1.mp3",
|
||||||
"sec": 2.15
|
"sec": 2.15
|
||||||
},
|
},
|
||||||
"andrea|Trappizzio Berti! Che fai, giochi da solo?": {
|
"andrea|Fabri! Che fai, giochi da solo?": {
|
||||||
"file": "andrea-da5fb02d21.mp3",
|
"file": "andrea-44037129d8.mp3",
|
||||||
"sec": 3.4
|
"sec": 2.71
|
||||||
},
|
},
|
||||||
"andrea|Come fai a prendere le biglie dove nessuno ci arriva?": {
|
"andrea|Come fai a prendere le biglie dove nessuno ci arriva?": {
|
||||||
"file": "andrea-c6670ffb48.mp3",
|
"file": "andrea-c6670ffb48.mp3",
|
||||||
@@ -2515,9 +2595,9 @@
|
|||||||
"file": "andrea-fe39c8f30c.mp3",
|
"file": "andrea-fe39c8f30c.mp3",
|
||||||
"sec": 1.49
|
"sec": 1.49
|
||||||
},
|
},
|
||||||
"andrea|Ci si vede, Trappizzio Berti.": {
|
"andrea|Ci si vede, Fabri.": {
|
||||||
"file": "andrea-4089f348c4.mp3",
|
"file": "andrea-d48ea04521.mp3",
|
||||||
"sec": 2.57
|
"sec": 1.37
|
||||||
},
|
},
|
||||||
"bertovo|Da solo? Io gioco contro le sponde. E vinco quasi sempre.": {
|
"bertovo|Da solo? Io gioco contro le sponde. E vinco quasi sempre.": {
|
||||||
"file": "bertovo-5e771069ff.mp3",
|
"file": "bertovo-5e771069ff.mp3",
|
||||||
@@ -2847,6 +2927,18 @@
|
|||||||
"file": "bertovo-ca941bee91.mp3",
|
"file": "bertovo-ca941bee91.mp3",
|
||||||
"sec": 5.41
|
"sec": 5.41
|
||||||
},
|
},
|
||||||
|
"andrea|Tutti sul muro, verdi in fila. Lo stucco di Pierino un tradisce mai.": {
|
||||||
|
"file": "andrea-585cd65f93.mp3",
|
||||||
|
"sec": 5.2
|
||||||
|
},
|
||||||
|
"andrea|Finito lo stucco. Domani si ripassa da Pierino.": {
|
||||||
|
"file": "andrea-62c5d74b82.mp3",
|
||||||
|
"sec": 3.29
|
||||||
|
},
|
||||||
|
"andrea|Il muro resta lì. E l'odore dello stucco, sulle dita, pure.": {
|
||||||
|
"file": "andrea-c7dcb11e66.mp3",
|
||||||
|
"sec": 5.52
|
||||||
|
},
|
||||||
"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
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p
|
|||||||
| `provinciale-pignone.png`, `piazzetta-cambray-digny.png`, `via-cipriani.png`, `via-massorondinaio.png`, `mozzete.png`, `ponticino-mozzete.png`, `coldaia.png`, `falegnameria-coldaia.png`, `gabbiano.png` | Codex | screenshot di Google Street View | Google | **no**: da rifare con foto di Andrea |
|
| `provinciale-pignone.png`, `piazzetta-cambray-digny.png`, `via-cipriani.png`, `via-massorondinaio.png`, `mozzete.png`, `ponticino-mozzete.png`, `coldaia.png`, `falegnameria-coldaia.png`, `gabbiano.png` | Codex | screenshot di Google Street View | Google | **no**: da rifare con foto di Andrea |
|
||||||
| `closeups/micro-parcheggio.png`, `closeups/micro/*.png` (macchinine) | Codex, solo testo | nessuna | — | sì |
|
| `closeups/micro-parcheggio.png`, `closeups/micro/*.png` (macchinine) | Codex, solo testo | nessuna | — | sì |
|
||||||
| `characters/chiaru.png` | Codex | `foto/chiaru.jpg` | Andrea (29 ottobre) | sì (il Chiaru consenziente) |
|
| `characters/chiaru.png` | Codex | `foto/chiaru.jpg` | Andrea (29 ottobre) | sì (il Chiaru consenziente) |
|
||||||
|
| `via-liguri-magelli-muro.png` | Codex, ritocco di `via-liguri-magelli.png` con la Ghiacciomobile nel muro | come `via-liguri-magelli.png` | Street View | **no**: come lo sfondo da cui viene |
|
||||||
|
| `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 |
|
||||||
|
| `closeups/muro-cerbottana.png` | Codex, solo testo (stile dalla piazza) | nessuna | — | sì |
|
||||||
| `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 |
|
||||||
| `pieve-interno.png` | Codex | `pieve-interno.png` | Mariano Mirannalti (mandata ad Andrea su WhatsApp) | sì, se Mariano è d'accordo; citarlo nei crediti |
|
| `pieve-interno.png` | Codex | `pieve-interno.png` | Mariano Mirannalti (mandata ad Andrea su WhatsApp) | sì, se Mariano è d'accordo; citarlo nei crediti |
|
||||||
| `panorama-intro.png` | Codex | `altre/panorama-intro.png` | da internet (screenshot con l'icona di Google Lens) | **no**, finché non si sostituisce |
|
| `panorama-intro.png` | Codex | `altre/panorama-intro.png` | da internet (screenshot con l'icona di Google Lens) | **no**, finché non si sostituisce |
|
||||||
|
|||||||
@@ -18,6 +18,30 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
|||||||
|
|
||||||
## Dove siamo arrivati
|
## Dove siamo arrivati
|
||||||
|
|
||||||
|
- **La cerbottana (29 ottobre)**: in piazza, la bottega di Pierin di Frilli sotto la tenda rossa; si compra lo stucco
|
||||||
|
verde e si tira al muro della piazza (`muro-cerbottana.png`, Codex). Motore `cerbottana.js`, disegno
|
||||||
|
`cerbottana-render.js`: si mira, si tiene premuto per il fiato (giusto al 70%: poco cade sotto, troppo va sopra), il
|
||||||
|
mirino ondeggia col respiro, troppo a fiato pieno si tossisce. Bersaglio di gesso (10/5/2), lattine sul muretto
|
||||||
|
(3), barattolo appeso (8, seria e a fassi male); 12 pallini, servono 30/40/50 punti; i pallini restano sul muro.
|
||||||
|
Test in `cerbottana.test.mjs` e `npm run browser:cerbottana`. [DA ANDREA] le battute, com'era la bottega, se far
|
||||||
|
comparire Pierino (è scomparso da anni: decide Andrea).
|
||||||
|
- **Il terrazzo di casa a Cardetole (29 ottobre)**: dalla foto di Andrea dalla finestra (il paese che sale e lassù
|
||||||
|
la Fortezza), Codex l'ha dipinta e ci ha messo davanti il terrazzo (`terrazza-cardetole.png`). Ci si sale da
|
||||||
|
«casa di Andrea» a Cardetole; la battuta dell'apparecchiare ora è sulla porta-finestra. Battute provvisorie
|
||||||
|
[DA ANDREA]. Come Cardetole, dice dove abita Andrea: da decidere prima di Pages.
|
||||||
|
- **La Ghiacciomobile nel muro (29 ottobre)**: non è più al circolino di Campomigliaio. In Via Liguri Magelli, prima
|
||||||
|
del tramonto (`!indizio_regolo`), è schiantata dentro il muro giallo coi mattoni rotti e senza targa
|
||||||
|
(`via-liguri-magelli-muro.png`, ritocco di Codex); dal tramonto il muro è rifatto e c'è la targa. Il motore ha le
|
||||||
|
varianti di sfondo per scena (`backdrops`, con `when`; `game.backdrop()`). Lo zio Mario (sprite di Codex dalla
|
||||||
|
foto di Andrea) davanti alla macchina che si gratta la testa; scala della scena rifatta (le persone erano troppo
|
||||||
|
piccole). [DA ANDREA] le battute dello zio (scherzava su tutto, anche sulla morte) e quella dello schianto.
|
||||||
|
- **GP di Fortezza, guida semplice (29 ottobre)**: alla bona e Seria si tiene premuto per il gas e lasciando si
|
||||||
|
frena; lo sterzo segue la strada da solo (scansa le buche, sorpassa, prende la miscela), ma oltre la velocità che la
|
||||||
|
curva regge il motorino scivola fuori verso la paglia (`SIMPLE` in `gara.js`). Ginocchio in terra resta con lo
|
||||||
|
sterzo vero. Il pannello nasconde Sterza, Frena, swipe e sensibilità. Test in `gara.test.mjs` e
|
||||||
|
`npm run browser:gara`. Seria più difficile (parere di Codex, simulato): sopra il 110% del limite in curva si esce
|
||||||
|
di sicuro (stun 1,2 s), la curva pulita dà una spinta, rivali a 0,81; CURVA! (preavviso) e LASCIA! sul motorino.
|
||||||
|
In simulazione: chi frena giusto 1° in 46 s, tutto gas 4°-5° in 55-59 s.
|
||||||
- **La pista col gesso (29 ottobre, modifiche locali)**: le Micro Machines sul parcheggio di Cardetole contro il
|
- **La pista col gesso (29 ottobre, modifiche locali)**: le Micro Machines sul parcheggio di Cardetole contro il
|
||||||
Chiaru (Matteo Chiarusi, sprite di Codex dalla foto di Andrea, accanto alla pista). Motore in `micro.js`, disegno in
|
Chiaru (Matteo Chiarusi, sprite di Codex dalla foto di Andrea, accanto alla pista). Motore in `micro.js`, disegno in
|
||||||
`micro-render.js`: pista disegnata a gesso dal codice sopra l'asfalto di Codex (`micro-parcheggio.png`), un giro,
|
`micro-render.js`: pista disegnata a gesso dal codice sopra l'asfalto di Codex (`micro-parcheggio.png`), un giro,
|
||||||
@@ -26,6 +50,9 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
|||||||
cappottè (urto forte), accompagnè (dito tenuto oltre la carica piena). Tre livelli: alla bona la guida mostra dove
|
cappottè (urto forte), accompagnè (dito tenuto oltre la carica piena). Tre livelli: alla bona la guida mostra dove
|
||||||
ci si ferma. Test in `tests/micro.test.mjs` e nel browser (`npm run browser:micro`). Battute del Chiaru provvisorie
|
ci si ferma. Test in `tests/micro.test.mjs` e nel browser (`npm run browser:micro`). Battute del Chiaru provvisorie
|
||||||
[DA ANDREA]; il Chiaru ha dato il consenso (29 ottobre).
|
[DA ANDREA]; il Chiaru ha dato il consenso (29 ottobre).
|
||||||
|
Poi: si sceglie la macchinina (sei, il Chiaru tiene la blu), la forza parte da zero appena si preme e la spinta
|
||||||
|
parte quando si lascia; saluto «Vado a fare un girino alle case rosse» / «Vorrai dire un giochino!» / «Ciao Mattè»;
|
||||||
|
Andrea chiama Bertovo «Fabri». Musica sua: `assets/audio/macchinine.mp3` (Andrea con Suno).
|
||||||
- **Bertovo ora si chiama «Trappizzio Berti»** in tutte le scritte (Andrea, 29 ottobre); l'id resta `bertovo`.
|
- **Bertovo ora si chiama «Trappizzio Berti»** in tutte le scritte (Andrea, 29 ottobre); l'id resta `bertovo`.
|
||||||
Fabrizio Berti e il suo libro «Memorie di sport e di paese» nei Ringraziamenti; l'idea delle cartoline in `IDEE.md`.
|
Fabrizio Berti e il suo libro «Memorie di sport e di paese» nei Ringraziamenti; l'idea delle cartoline in `IDEE.md`.
|
||||||
- **I rinquarti di Bertovo (28 ottobre, modifiche locali)**: variante del biliardo in `rinquarto.js` e
|
- **I rinquarti di Bertovo (28 ottobre, modifiche locali)**: variante del biliardo in `rinquarto.js` e
|
||||||
|
|||||||
@@ -225,7 +225,13 @@
|
|||||||
guida mostra la parabola fino al primo urto, Seria solo l’inizio, A fassi male niente.</p>
|
guida mostra la parabola fino al primo urto, Seria solo l’inizio, A fassi male niente.</p>
|
||||||
</details>
|
</details>
|
||||||
<details id="gara-help" hidden><summary>Come si gioca</summary>
|
<details id="gara-help" hidden><summary>Come si gioca</summary>
|
||||||
<p>Il GP di Fortezza: tre giri attorno alle mura coi cinquantini. Con un solo dito: tieni premuto sulla scena per
|
<p>Il GP di Fortezza: tre giri attorno alle mura coi cinquantini. <b>Alla bona e Seria</b> la guida è semplice:
|
||||||
|
tieni premuto sulla scena (o Spazio, o il pulsante Gas) per accelerare e lascia per frenare; lo sterzo segue la
|
||||||
|
strada da solo, scansa le buche e sorpassa. Ma se entri in curva troppo forte scivoli fuori, e nella paglia ti
|
||||||
|
fermi: molla prima delle curve. Sopra il motorino «CURVA!» avvisa che ne arriva una, «LASCIA!» dice quando
|
||||||
|
mollare. A Seria chi entra in curva troppo forte va fuori di sicuro, e chi la prende giusta fa la «curva pulita»
|
||||||
|
ed esce con una spinta in più.</p>
|
||||||
|
<p><b>Ginocchio in terra</b> si guida davvero. Con un solo dito: tieni premuto sulla scena per
|
||||||
il gas e trascina verso la direzione in cui vuoi andare sullo schermo. A destra vai verso destra, anche quando
|
il gas e trascina verso la direzione in cui vuoi andare sullo schermo. A destra vai verso destra, anche quando
|
||||||
il motorino scende. I piccoli movimenti non sterzano; il cerchietto segue gli swipe lunghi. Alza il dito per
|
il motorino scende. I piccoli movimenti non sterzano; il cerchietto segue gli swipe lunghi. Alza il dito per
|
||||||
mollare il gas e raddrizzare lo sterzo. Funziona anche col mouse premuto.</p>
|
mollare il gas e raddrizzare lo sterzo. Funziona anche col mouse premuto.</p>
|
||||||
@@ -249,6 +255,15 @@
|
|||||||
il pulsante alterna Recupera e Molla. Sei lanci. La gloria: 2 pesci alla bona, 3 seria, 4 a fassi male (lo stivale
|
il pulsante alterna Recupera e Molla. Sei lanci. La gloria: 2 pesci alla bona, 3 seria, 4 a fassi male (lo stivale
|
||||||
non conta, e non pesa). Frecce ← →: distanza del lancio; Esc esce.</p>
|
non conta, e non pesa). Frecce ← →: distanza del lancio; Esc esce.</p>
|
||||||
</details>
|
</details>
|
||||||
|
<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
|
||||||
|
di gesso vale 10, 5 o 2 punti secondo il cerchio, ogni lattina sul muretto 3, il barattolo appeso che dondola (Seria
|
||||||
|
e A fassi male) 8. Servono 30 punti alla bona, 40 Seria, 50 A fassi male. I pallini restano attaccati al muro.</p>
|
||||||
|
<p>Muovi il mouse o tocca il muro per mirare: il mirino ondeggia col respiro (di più salendo di livello). Tieni
|
||||||
|
premuto per prendere fiato e lascia per sparare: col fiato giusto il pallino va dove punti, con poco cade sotto,
|
||||||
|
con troppo va sopra. Alla bona la barra del fiato segna il punto giusto. Se resti troppo a fiato pieno tossisci, e
|
||||||
|
il pallino è perso. Si può anche regolare il fiato col cursore e premere Spara. Frecce per spostare il mirino.</p>
|
||||||
|
</details>
|
||||||
<details id="micro-help" hidden><summary>Come si gioca</summary>
|
<details id="micro-help" hidden><summary>Come si gioca</summary>
|
||||||
<p>La pista col gesso sul parcheggio di Cardetole: un giro contro il Chiaru, una spinta a testa (prima tu, poi
|
<p>La pista col gesso sul parcheggio di Cardetole: un giro contro il Chiaru, una spinta a testa (prima tu, poi
|
||||||
lui). La tua macchinina è la rossa. Vince chi taglia per primo il traguardo a scacchi; se ci arrivate tutti e due
|
lui). La tua macchinina è la rossa. Vince chi taglia per primo il traguardo a scacchi; se ci arrivate tutti e due
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "leggenda-del-regolo",
|
"name": "leggenda-del-regolo",
|
||||||
"version": "0.1.39",
|
"version": "0.1.47",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
"browser": "node tests/browser.mjs",
|
"browser": "node tests/browser.mjs",
|
||||||
"browser:biliardo": "node tests/biliardo.browser.mjs",
|
"browser:biliardo": "node tests/biliardo.browser.mjs",
|
||||||
"browser:micro": "node tests/micro.browser.mjs",
|
"browser:micro": "node tests/micro.browser.mjs",
|
||||||
|
"browser:gara": "node tests/gara.browser.mjs",
|
||||||
|
"browser:cerbottana": "node tests/cerbottana.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,102 @@
|
|||||||
|
import { TARGET, CAN, SWINGER, LEDGE, PELLETS, FLIGHT, BEST, rings, reticle, swinger, canThrow, overHeld } from './cerbottana.js';
|
||||||
|
import { text, panel } from './bocce-render.js';
|
||||||
|
|
||||||
|
// Il gesso sul muro: tre cerchi un po' storti (sempre gli stessi) e i numeri dei punti, in alto, ognuno nel suo anello.
|
||||||
|
function chalkTarget(ctx, c) {
|
||||||
|
let inner = 0;
|
||||||
|
for (const [r, p] of rings(c)) {
|
||||||
|
ctx.strokeStyle = 'rgba(250,248,236,0.85)'; ctx.lineWidth = 1.5; ctx.beginPath();
|
||||||
|
for (let k = 0; k <= 48; k++) {
|
||||||
|
const a = k / 48 * Math.PI * 2, j = 0.6 * Math.sin(k * 2.3 + r);
|
||||||
|
const x = TARGET.x + Math.cos(a) * (r + j), y = TARGET.y + Math.sin(a) * (r + j);
|
||||||
|
if (k) ctx.lineTo(x, y); else ctx.moveTo(x, y);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
if (inner) text(ctx, String(p), TARGET.x, TARGET.y - (inner + r) / 2, 6, 'rgba(250,248,236,0.8)', 'center');
|
||||||
|
inner = r;
|
||||||
|
}
|
||||||
|
ctx.fillStyle = 'rgba(250,248,236,0.9)'; ctx.fillRect(TARGET.x - 1, TARGET.y - 1, 2, 2);
|
||||||
|
}
|
||||||
|
// Una lattina: rossa con la fascia chiara, i bordi d'alluminio.
|
||||||
|
function can(ctx, x, y, angle = 0, color = '#c8321e') {
|
||||||
|
ctx.save(); ctx.translate(Math.round(x), Math.round(y)); ctx.rotate(angle);
|
||||||
|
ctx.fillStyle = 'rgba(20,16,10,0.35)'; ctx.fillRect(-CAN.w / 2 + 2, -1, CAN.w, 2);
|
||||||
|
ctx.fillStyle = color; ctx.fillRect(-CAN.w / 2, -CAN.h, CAN.w, CAN.h);
|
||||||
|
ctx.fillStyle = '#f2e6c8'; ctx.fillRect(-CAN.w / 2, -CAN.h + 6, CAN.w, 4);
|
||||||
|
ctx.fillStyle = '#c9cdd2'; ctx.fillRect(-CAN.w / 2, -CAN.h, CAN.w, 2); ctx.fillRect(-CAN.w / 2, -2, CAN.w, 2);
|
||||||
|
ctx.fillStyle = 'rgba(255,255,255,0.35)'; ctx.fillRect(-CAN.w / 2 + 2, -CAN.h + 2, 1, CAN.h - 4);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
// Un pallino di stucco: verdastro, un po' schiacciato sul muro.
|
||||||
|
function pellet(ctx, x, y, r = 2.5) {
|
||||||
|
ctx.fillStyle = '#2b3a1c'; ctx.beginPath(); ctx.arc(x, y, r + 0.8, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.fillStyle = '#8fa05a'; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.fillStyle = '#c3cf8e'; ctx.fillRect(Math.round(x - r / 2), Math.round(y - r / 2), 1, 1);
|
||||||
|
}
|
||||||
|
// La cerbottana in basso, in prospettiva: grossa vicino alla bocca, più fine in punta, girata verso il mirino, col
|
||||||
|
// pallino di stucco in punta.
|
||||||
|
function blowpipe(ctx, c, to) {
|
||||||
|
const from = { x: 320, y: 376 }, a = Math.atan2(to.y - from.y, to.x - from.x), len = 64;
|
||||||
|
const tip = { x: from.x + Math.cos(a) * len, y: from.y + Math.sin(a) * len }, nx = -Math.sin(a), ny = Math.cos(a);
|
||||||
|
const tube = (w0, w1, color) => {
|
||||||
|
ctx.fillStyle = color; ctx.beginPath();
|
||||||
|
ctx.moveTo(from.x + nx * w0, from.y + ny * w0); ctx.lineTo(tip.x + nx * w1, tip.y + ny * w1);
|
||||||
|
ctx.lineTo(tip.x - nx * w1, tip.y - ny * w1); ctx.lineTo(from.x - nx * w0, from.y - ny * w0); ctx.fill();
|
||||||
|
};
|
||||||
|
tube(9, 4, '#2a2018'); tube(7, 3, '#6b5236');
|
||||||
|
ctx.fillStyle = '#8a6a44'; ctx.fillRect(Math.round(tip.x - 1), Math.round(tip.y - 1), 2, 2);
|
||||||
|
if (canThrow(c)) pellet(ctx, tip.x, tip.y - 1, 3.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawCerbottana(ctx, c, def, bg) {
|
||||||
|
ctx.save(); ctx.imageSmoothingEnabled = false;
|
||||||
|
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#d9a640'; ctx.fillRect(0, 0, 640, 360); }
|
||||||
|
chalkTarget(ctx, c);
|
||||||
|
for (const p of c.stuck) pellet(ctx, p.x, p.y);
|
||||||
|
// Le lattine sul muretto; colpite, cadono all'indietro.
|
||||||
|
for (const k of c.cans) { if (k.fall < 1) can(ctx, k.x, LEDGE + k.fall * 6, -k.fall * 1.4); }
|
||||||
|
if (c.skill.swinger) {
|
||||||
|
const s = swinger(c);
|
||||||
|
ctx.strokeStyle = '#3a3024'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(SWINGER.x, SWINGER.y); ctx.lineTo(s.x, s.y); ctx.stroke();
|
||||||
|
can(ctx, s.x, s.y + SWINGER.h, s.a + c.swing.spin * Math.sin(c.visual.time * 30) * 0.6, '#2f6fb0');
|
||||||
|
}
|
||||||
|
// Il pallino in volo: parte grande dalla cerbottana e rimpicciolisce andando verso il muro.
|
||||||
|
if (c.shot) {
|
||||||
|
const k = Math.min(1, c.shot.t / FLIGHT), { from, to } = c.shot;
|
||||||
|
pellet(ctx, from.x + (to.x - from.x) * k, from.y + (to.y - from.y) * k - Math.sin(k * Math.PI) * 18, 6 - 3.5 * k);
|
||||||
|
}
|
||||||
|
const r = reticle(c);
|
||||||
|
if (canThrow(c)) {
|
||||||
|
const over = overHeld(c);
|
||||||
|
ctx.strokeStyle = over ? '#ff6a4a' : '#fff8dc'; ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath(); ctx.arc(r.x, r.y, 7, 0, Math.PI * 2); ctx.stroke();
|
||||||
|
ctx.beginPath(); ctx.moveTo(r.x - 11, r.y); ctx.lineTo(r.x - 3, r.y); ctx.moveTo(r.x + 3, r.y); ctx.lineTo(r.x + 11, r.y);
|
||||||
|
ctx.moveTo(r.x, r.y - 11); ctx.lineTo(r.x, r.y - 3); ctx.moveTo(r.x, r.y + 3); ctx.lineTo(r.x, r.y + 11); ctx.stroke();
|
||||||
|
}
|
||||||
|
blowpipe(ctx, c, r);
|
||||||
|
// Il fiato: una barra in basso a sinistra (alla bona col punto giusto segnato).
|
||||||
|
if (canThrow(c)) {
|
||||||
|
const x = 16, y = 300, h = 50;
|
||||||
|
ctx.fillStyle = '#17160fd0'; ctx.fillRect(x - 3, y - 3, 18, h + 6);
|
||||||
|
ctx.fillStyle = overHeld(c) ? '#ff6a4a' : c.power > BEST + 0.08 ? '#ffd23c' : '#9be07c';
|
||||||
|
const f = Math.round(h * c.power); ctx.fillRect(x, y + h - f, 12, f);
|
||||||
|
if (c.skill.sweet) { ctx.fillStyle = '#fff8dc'; ctx.fillRect(x - 3, Math.round(y + h - h * BEST) - 1, 18, 2); }
|
||||||
|
text(ctx, 'FIATO', x + 6, y - 9, 6, '#efd49e', 'center');
|
||||||
|
}
|
||||||
|
// In alto: punti, pallini rimasti e cosa è successo.
|
||||||
|
ctx.fillStyle = '#17160fe8'; ctx.fillRect(0, 0, 640, 22);
|
||||||
|
text(ctx, 'LA CERBOTTANA', 8, 7, 8, '#efd49e');
|
||||||
|
text(ctx, { bona: 'alla bona', seria: 'seria', male: 'a fassi male' }[c.level], 8, 16, 7, '#d9ccba');
|
||||||
|
text(ctx, `Punti ${c.points} / ${c.skill.goal}`, 260, 11, 9, c.points >= c.skill.goal ? '#b9e5b3' : '#fff0c8', 'center');
|
||||||
|
for (let i = 0; i < PELLETS; i++) pellet(ctx, 430 + i * 15, 11, i < c.left ? 3 : 1.2);
|
||||||
|
const line = overHeld(c) ? 'Lascia, sennò tossisci!' : c.line;
|
||||||
|
if (line) { ctx.fillStyle = '#17160fd0'; ctx.fillRect(170, 24, 300, 14); text(ctx, line, 320, 31, 8, overHeld(c) || /corto|alto|Coff/.test(line) ? '#ffbf9f' : '#9fd0ff', 'center'); }
|
||||||
|
if (c.step === 'result') {
|
||||||
|
const won = c.result.winner === 'player';
|
||||||
|
panel(ctx, 170, 140, 300, 76);
|
||||||
|
text(ctx, won ? 'CENTRATO!' : 'LO STUCCO È FINITO', 320, 160, 12, won ? '#b9e5b3' : '#efd49e', 'center');
|
||||||
|
text(ctx, `${c.result.points} punti, ne servivano ${c.result.goal}`, 320, 182, 8, '#efd49e', 'center');
|
||||||
|
text(ctx, 'Torna in piazza per uscire.', 320, 200, 8, '#d9ccba', 'center');
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
// La cerbottana (idea di Andrea, 29 ottobre): da ragazzi in piazza si comprava da Pierin di Frilli lo stucco verde,
|
||||||
|
// dall'odore inconfondibile, lo si metteva intorno alla cerbottana, si facevano i pallini e si «sparava» al muro.
|
||||||
|
// Qui: un muro della piazza visto di fronte, un bersaglio fatto col gesso, le lattine sul muretto e (a seria e a fassi
|
||||||
|
// male) un barattolo appeso che dondola. Si mira, si tiene premuto per prendere fiato e si lascia: con poco fiato il
|
||||||
|
// pallino cade sotto, con troppo va sopra; il mirino ondeggia col respiro, e chi resta troppo a fiato pieno tossisce.
|
||||||
|
// I pallini di stucco restano attaccati al muro. Unità: i pixel del primo piano 640×360.
|
||||||
|
|
||||||
|
export const WALL_BOTTOM = 244; // sotto comincia il muretto: lì il pallino non attacca
|
||||||
|
export const LEDGE = 244; // dove poggiano le lattine
|
||||||
|
export const PELLETS = 12, SWING = 1, TAP = 0.14, COUGH = 0.6, FLIGHT = 0.3, REST = 0.5, BEST = 0.7, DROP = 300;
|
||||||
|
export const TARGET = { x: 320, y: 118, rings: [[8, 10], [18, 5], [30, 2]] };
|
||||||
|
export const CAN = { w: 10, h: 16, points: 3 }, SWINGER = { x: 470, y: 8, length: 92, w: 12, h: 16, points: 8 };
|
||||||
|
// I livelli: quanto ondeggia il mirino (px), quanti punti servono, se c'è il barattolo appeso, se la barra del fiato
|
||||||
|
// mostra il punto giusto, e quanto sono grandi i cerchi del bersaglio.
|
||||||
|
export const LEVELS = {
|
||||||
|
bona: { sway: 3, goal: 30, swinger: false, sweet: true, rings: 1.3 },
|
||||||
|
seria: { sway: 6, goal: 40, swinger: true, sweet: false, rings: 1 },
|
||||||
|
male: { sway: 10, goal: 50, swinger: true, sweet: false, rings: 0.85 },
|
||||||
|
};
|
||||||
|
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 newCerbottana(seed = Date.now() >>> 0, def = {}) {
|
||||||
|
const level = LEVELS[def.level] ? def.level : 'seria';
|
||||||
|
return { step: 'aim', t: 0, seed, level, skill: LEVELS[level], aim: { x: TARGET.x, y: TARGET.y }, power: BEST,
|
||||||
|
holding: false, holdTime: 0, charging: false, left: PELLETS, points: 0, events: [], shot: null, stuck: [], line: null,
|
||||||
|
cans: [200, 250, 390, 440].map(x => ({ x, down: false, fall: 0 })), swing: { hits: 0, spin: 0 }, visual: { time: 0 } };
|
||||||
|
}
|
||||||
|
export const canThrow = c => c.step === 'aim' && c.left > 0;
|
||||||
|
export const canAdjust = canThrow;
|
||||||
|
export const powerAt = time => clamp(time / SWING, 0, 1);
|
||||||
|
export const rings = c => TARGET.rings.map(([r, p]) => [r * c.skill.rings, p]);
|
||||||
|
|
||||||
|
// Il respiro: il mirino gira piano intorno al punto voluto (due onde, non si ripete subito); trattenendo il fiato pieno
|
||||||
|
// trema di più.
|
||||||
|
export function sway(c) {
|
||||||
|
const t = c.visual.time, a = c.skill.sway * (1 + (c.overHold ?? 0) * 2);
|
||||||
|
return { x: a * (Math.sin(t * 1.7) + 0.5 * Math.sin(t * 3.1 + 1)), y: a * 0.8 * (Math.sin(t * 1.3 + 2) + 0.5 * Math.sin(t * 2.9)) };
|
||||||
|
}
|
||||||
|
export function reticle(c) { const s = sway(c); return { x: c.aim.x + s.x, y: c.aim.y + s.y }; }
|
||||||
|
// Dove arriva il pallino: dove punta il mirino, più su o più giù secondo il fiato (BEST è il fiato giusto).
|
||||||
|
export const landing = (c, power = c.power) => { const r = reticle(c); return { x: r.x, y: r.y + (BEST - power) * DROP }; };
|
||||||
|
// Il barattolo appeso: un pendolo che dondola sotto la gronda (e gira su sé stesso se colpito).
|
||||||
|
export function swinger(c) {
|
||||||
|
const a = 0.5 * Math.sin(c.visual.time * 1.6);
|
||||||
|
return { x: SWINGER.x + Math.sin(a) * SWINGER.length, y: SWINGER.y + Math.cos(a) * SWINGER.length, a };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shoot(c) {
|
||||||
|
if (!canThrow(c) || c.holding) return false;
|
||||||
|
return fire(c, c.power);
|
||||||
|
}
|
||||||
|
function fire(c, power, cough = false) {
|
||||||
|
const from = { x: 320, y: 380 }, to = cough ? { x: 300 + random(c) * 40, y: 330 } : landing(c, power);
|
||||||
|
c.left--; c.power = power;
|
||||||
|
c.shot = { from, to, t: 0, cough };
|
||||||
|
c.step = 'flying'; c.line = null;
|
||||||
|
c.events.push({ kind: cough ? 'cerbottana-tosse' : 'cerbottana-pff', power });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Arrivato: prima il barattolo appeso, poi le lattine, poi il bersaglio; sul muro il pallino resta attaccato.
|
||||||
|
function land(c) {
|
||||||
|
const { x, y } = c.shot.to;
|
||||||
|
let got = 0, what = null;
|
||||||
|
if (c.shot.cough) what = 'Coff! Il pallino è finito per terra.';
|
||||||
|
if (!what && c.skill.swinger) {
|
||||||
|
const s = swinger(c);
|
||||||
|
if (Math.abs(x - s.x) <= SWINGER.w / 2 + 1 && Math.abs(y - s.y - SWINGER.h / 2) <= SWINGER.h / 2 + 1) {
|
||||||
|
got = SWINGER.points; c.swing.hits++; c.swing.spin = 1; what = `Il barattolo appeso! ${got} punti.`;
|
||||||
|
c.events.push({ kind: 'cerbottana-latta', power: 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!what) {
|
||||||
|
const can = c.cans.find(k => !k.down && Math.abs(x - k.x) <= CAN.w / 2 + 1 && y >= LEDGE - CAN.h - 1 && y <= LEDGE);
|
||||||
|
if (can) { can.down = true; got = CAN.points; what = `Lattina giù! ${got} punti.`; c.events.push({ kind: 'cerbottana-latta', power: 0.8 }); }
|
||||||
|
}
|
||||||
|
if (!what) {
|
||||||
|
if (y < WALL_BOTTOM && y > 4 && x > 4 && x < 636) {
|
||||||
|
c.stuck.push({ x, y });
|
||||||
|
const d = Math.hypot(x - TARGET.x, y - TARGET.y), ring = rings(c).find(([r]) => d <= r);
|
||||||
|
got = ring ? ring[1] : 0;
|
||||||
|
what = got ? `Nel bersaglio: ${got} ${got === 1 ? 'punto' : 'punti'}.` : y < TARGET.y - 40 ? 'Troppo fiato: alto.' : 'Sul muro, fuori dal bersaglio.';
|
||||||
|
c.events.push({ kind: 'cerbottana-muro', power: 0.6 });
|
||||||
|
} else what = 'Poco fiato: il pallino è caduto corto.';
|
||||||
|
}
|
||||||
|
c.points += got; c.line = what;
|
||||||
|
c.shot = null; c.step = 'rest'; c.t = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
for (const k of c.cans) if (k.down) k.fall = Math.min(1, k.fall + dt * 3);
|
||||||
|
if (c.swing.spin) c.swing.spin = Math.max(0, c.swing.spin - dt);
|
||||||
|
if (c.holding) {
|
||||||
|
c.holdTime += dt; c.charging = c.holdTime >= TAP;
|
||||||
|
c.power = powerAt(c.holdTime);
|
||||||
|
c.overHold = c.power >= 1 ? (c.overHold ?? 0) + dt : 0;
|
||||||
|
// Troppo a fiato pieno: tosse, e il pallino è perso.
|
||||||
|
if (c.overHold > COUGH) { c.holding = c.charging = false; c.overHold = 0; fire(c, 1, true); }
|
||||||
|
}
|
||||||
|
if (c.step === 'flying') { c.shot.t += dt; if (c.shot.t >= FLIGHT) land(c); }
|
||||||
|
else if (c.step === 'rest' && c.t >= REST) {
|
||||||
|
if (c.left > 0) c.step = 'aim';
|
||||||
|
else { c.step = 'result'; c.result = { winner: c.points >= c.skill.goal ? 'player' : 'cpu', points: c.points, goal: c.skill.goal }; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Comandi: il mirino segue il mouse o il dito; tenere premuto prende fiato, lasciare spara.
|
||||||
|
export function aimScreen(c, [x, y]) {
|
||||||
|
if (!canThrow(c) || c.holding || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||||
|
c.aim = { x: clamp(x, 10, 630), y: clamp(y, 24, 300) }; return true;
|
||||||
|
}
|
||||||
|
export function nudge(c, dir) { if (!canThrow(c) || c.holding) return false; c.aim.x = clamp(c.aim.x + dir * 4, 10, 630); return true; }
|
||||||
|
export function setPower(c, value) { if (!canThrow(c) || c.holding || !Number.isFinite(value)) return false; c.power = clamp(value, 0, 1); return true; }
|
||||||
|
export function key(c, key, down) {
|
||||||
|
const move = { ArrowLeft: [-4, 0], ArrowRight: [4, 0], ArrowUp: [0, -4], ArrowDown: [0, 4] }[key];
|
||||||
|
if (!move) return false;
|
||||||
|
if (down && canThrow(c) && !c.holding) c.aim = { x: clamp(c.aim.x + move[0], 10, 630), y: clamp(c.aim.y + move[1], 24, 300) };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function press(c, pointerId) {
|
||||||
|
if (!canThrow(c) || c.holding) return false;
|
||||||
|
c.pointerId = pointerId; c.holding = true; c.charging = false; c.holdTime = 0; c.previousPower = c.power; c.power = 0; c.overHold = 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.power = c.previousPower; c.holdTime = 0; c.overHold = 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.overHold = 0;
|
||||||
|
return fire(c, c.power);
|
||||||
|
}
|
||||||
|
// Trattenendo il fiato pieno: il disegno avvisa prima della tosse.
|
||||||
|
export const overHeld = c => c.holding && (c.overHold ?? 0) > 0;
|
||||||
|
|
||||||
|
export function button(c) { return c.step === 'result' ? 'Finita' : c.step === 'aim' ? 'Spara' : 'In volo…'; }
|
||||||
|
export function status(c) {
|
||||||
|
const score = `Punti ${c.points} su ${c.skill.goal}, pallini ${c.left} di ${PELLETS}.`;
|
||||||
|
if (c.step === 'result') return `${c.result.winner === 'player' ? 'Fatto! ' : 'Lo stucco è finito. '}${score}`;
|
||||||
|
return `${score} ${c.line ?? 'Mira, tieni premuto per prendere fiato e lascia.'}`;
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import * as tressette from './tressette.js';
|
|||||||
import * as scopa from './scopa.js';
|
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';
|
||||||
|
|
||||||
// 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.
|
||||||
export const MINIGAMES = {
|
export const MINIGAMES = {
|
||||||
@@ -22,6 +23,7 @@ export const MINIGAMES = {
|
|||||||
rinquarto: { create: (seed, level) => rinquarto.newRinquarto(seed, { level }), update: rinquarto.update, press: rinquarto.press, release: rinquarto.release, cancel: rinquarto.cancel, aim: rinquarto.aimScreen },
|
rinquarto: { create: (seed, level) => rinquarto.newRinquarto(seed, { level }), update: rinquarto.update, press: rinquarto.press, release: rinquarto.release, cancel: rinquarto.cancel, aim: rinquarto.aimScreen },
|
||||||
basket: { create: (seed, level) => basket.newBasket(seed, { level }), update: basket.update, press: basket.press, release: basket.release, cancel: basket.cancel, aim: basket.aimScreen },
|
basket: { create: (seed, level) => basket.newBasket(seed, { level }), update: basket.update, press: basket.press, release: basket.release, cancel: basket.cancel, aim: basket.aimScreen },
|
||||||
pesca: { create: (seed, level) => pesca.newPesca(seed, { level }), update: pesca.update, press: pesca.press, release: pesca.release, cancel: pesca.cancel, aim: pesca.aimScreen },
|
pesca: { create: (seed, level) => pesca.newPesca(seed, { level }), update: pesca.update, press: pesca.press, release: pesca.release, cancel: pesca.cancel, aim: pesca.aimScreen },
|
||||||
|
cerbottana: { create: (seed, level) => cerbottana.newCerbottana(seed, { level }), update: cerbottana.update, press: cerbottana.press, release: cerbottana.release, cancel: cerbottana.cancel, aim: cerbottana.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 },
|
||||||
};
|
};
|
||||||
@@ -162,6 +164,9 @@ export class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get scene() { return this.story.scenes[this.state.scene]; }
|
get scene() { return this.story.scenes[this.state.scene]; }
|
||||||
|
// Lo sfondo di adesso: la prima variante della scena (`backdrops`, ognuna con la sua condizione `when`) che vale,
|
||||||
|
// sennò il suo `background`. Così un posto cambia con la storia (la Ghiacciomobile nel muro, poi la targa).
|
||||||
|
backdrop(scene = this.scene) { return scene.backdrops?.find(b => check(b.when, this.state))?.src ?? scene.background; }
|
||||||
// Occupato: c'è qualcosa in corso che il giocatore non può interrompere. La camminata verso un punto
|
// Occupato: c'è qualcosa in corso che il giocatore non può interrompere. La camminata verso un punto
|
||||||
// o verso un oggetto si può sempre cambiare con un altro clic: la coda che la seguiva viene scartata.
|
// o verso un oggetto si può sempre cambiare con un altro clic: la coda che la seguiva viene scartata.
|
||||||
get busy() { return !!this.speech || !!this.closeup || this.reach > 0 || (this.task ? !this.task.interruptible : this.queue.length > 0); }
|
get busy() { return !!this.speech || !!this.closeup || this.reach > 0 || (this.task ? !this.task.interruptible : this.queue.length > 0); }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TRACK, CURVES, PICKUPS, BRAKE, LAPS, COUNTDOWN, ROAD, VERGE, CAN, SWIPE, pointAt, standings, player, lapOf, presenting, placeOf, INTRO } from './gara.js';
|
import { TRACK, CURVES, PICKUPS, BRAKE, LAPS, COUNTDOWN, ROAD, VERGE, CAN, SWIPE, pointAt, standings, player, lapOf, presenting, placeOf, INTRO, mustLift, PREAVVISO } from './gara.js';
|
||||||
import { text, panel } from './bocce-render.js';
|
import { text, panel } from './bocce-render.js';
|
||||||
import { ambientHash as hash } from './ambient.js';
|
import { ambientHash as hash } from './ambient.js';
|
||||||
|
|
||||||
@@ -216,6 +216,12 @@ function result(ctx, c) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Una scritta sopra il motorino di Andrea, su un fondo scuro perché si legga anche sullo sterrato e sul telefono.
|
||||||
|
function tag(ctx, line, r, color) {
|
||||||
|
const x = Math.round(r.x), y = Math.round(r.y) - 20, w = line.length * 9 + 10;
|
||||||
|
ctx.fillStyle = 'rgba(23,22,15,0.85)'; ctx.fillRect(x - w / 2, y - 7, w, 14);
|
||||||
|
text(ctx, line, x, y, 10, color, 'center');
|
||||||
|
}
|
||||||
export function drawGara(ctx, c, def, bg, look = () => null, sheet = () => null) {
|
export function drawGara(ctx, c, def, bg, look = () => null, sheet = () => null) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
|
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
|
||||||
@@ -232,6 +238,10 @@ export function drawGara(ctx, c, def, bg, look = () => null, sheet = () => null)
|
|||||||
const me = player(c);
|
const me = player(c);
|
||||||
if (c.step === 'race' && me.out > 0) text(ctx, me.lastHole !== undefined ? 'NELLA BUCA!' : 'NELLA PAGLIA!', 320, 180, 10, '#ffe28a', 'center');
|
if (c.step === 'race' && me.out > 0) text(ctx, me.lastHole !== undefined ? 'NELLA BUCA!' : 'NELLA PAGLIA!', 320, 180, 10, '#ffe28a', 'center');
|
||||||
if (c.step === 'race' && me.boost > 0 && me.out <= 0) text(ctx, 'MISCELA!', 320, 180, 8, '#ffb030', 'center');
|
if (c.step === 'race' && me.boost > 0 && me.out <= 0) text(ctx, 'MISCELA!', 320, 180, 8, '#ffb030', 'center');
|
||||||
|
// La guida semplice: LASCIA! sopra il motorino quando serve frenare, e la curva pulita.
|
||||||
|
if (c.step === 'race' && c.holding && mustLift(c)) { if (Math.floor(c.age * 8) % 2) tag(ctx, 'LASCIA!', me, '#ff6a4a'); }
|
||||||
|
else if (c.step === 'race' && c.holding && mustLift(c, PREAVVISO)) tag(ctx, 'CURVA!', me, '#ffe28a');
|
||||||
|
if (c.step === 'race' && c.clean !== undefined && c.t - c.clean < 1.2) text(ctx, 'CURVA PULITA!', 320, 180, 9, '#b9e5b3', 'center');
|
||||||
if (c.step === 'result') result(ctx, c);
|
if (c.step === 'result') result(ctx, c);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,12 +50,20 @@ export const RIDERS = [
|
|||||||
{ id: 'andrea', name: 'Andrea', bike: 'il Fifty', motore: 3, testa: 3, color: '#d8402f', trim: '#1c1c1c' },
|
{ id: 'andrea', name: 'Andrea', bike: 'il Fifty', motore: 3, testa: 3, color: '#d8402f', trim: '#1c1c1c' },
|
||||||
{ id: 'arcu', name: 'L\'Arcu', bike: 'il Betino', motore: 1, testa: 5, color: '#2f7a3a', trim: '#e8d9a0' },
|
{ id: 'arcu', name: 'L\'Arcu', bike: 'il Betino', motore: 1, testa: 5, color: '#2f7a3a', trim: '#e8d9a0' },
|
||||||
];
|
];
|
||||||
// I livelli: quanto vanno gli altri, quanto si resta fermi nella paglia, e la guida disegnata in pista.
|
// I livelli: quanto vanno gli altri, quanto si resta fermi nella paglia, e la guida disegnata in pista. `simple`: la
|
||||||
|
// guida semplice (Andrea, 29 ottobre: dal telefono lo sterzo era ingiocabile): si tiene premuto per il gas e lasciando
|
||||||
|
// si frena, lo sterzo segue la strada da solo, e chi entra in curva troppo forte scivola fuori. Ginocchio in terra
|
||||||
|
// tiene lo sterzo vero.
|
||||||
export const LEVELS = {
|
export const LEVELS = {
|
||||||
bona: { rivals: 0.74, stun: 0.4, guide: 'zone' },
|
bona: { rivals: 0.74, stun: 0.4, guide: 'zone', simple: true },
|
||||||
seria: { rivals: 0.84, stun: 0.9, guide: 'coni' },
|
// Seria (Andrea, 29 ottobre: «chi non rallenta in curva deve andare fuori»; valori proposti da Codex e simulati):
|
||||||
|
// si scivola di più, sopra il limite in curva si esce per forza, e la curva pulita dà una spinta.
|
||||||
|
seria: { rivals: 0.81, stun: 1.2, guide: 'coni', simple: true,
|
||||||
|
tune: { slide: 12, clean: { min: 0.85, slack: 2, time: 0.6, accel: 1.2 }, force: { over: 1.1, hold: 0.18, apex: 1.15, time: 0.3, guard: 0.6 } } },
|
||||||
male: { rivals: 0.93, stun: 1.4, guide: null },
|
male: { rivals: 0.93, stun: 1.4, guide: null },
|
||||||
};
|
};
|
||||||
|
// `def.simple` la forza (i test della guida vera la spengono anche alla bona).
|
||||||
|
export const simple = c => !!c.simple;
|
||||||
|
|
||||||
function catmull(p0, p1, p2, p3, t) {
|
function catmull(p0, p1, p2, p3, t) {
|
||||||
const t2 = t * t, t3 = t2 * t;
|
const t2 = t * t, t3 = t2 * t;
|
||||||
@@ -154,7 +162,7 @@ export function newGara(seed = Date.now() >>> 0, def = {}) {
|
|||||||
});
|
});
|
||||||
if (!riders.some(r => r.id === 'andrea')) throw new Error('Nella gara manca Andrea');
|
if (!riders.some(r => r.id === 'andrea')) throw new Error('Nella gara manca Andrea');
|
||||||
for (const r of riders) { r.grid = r.s; r.shown = false; }
|
for (const r of riders) { r.grid = r.s; r.shown = false; }
|
||||||
return { step: 'intro', t: 0, age: 0, seed, level, skill, riders, holding: false, braking: false, keys: { left: false, right: false, gas: new Set(), brake: false },
|
return { step: 'intro', t: 0, age: 0, seed, level, skill, simple: def.simple ?? !!skill.simple, tune: { ...SIMPLE, ...skill.tune }, riders, holding: false, braking: false, keys: { left: false, right: false, gas: new Set(), brake: false },
|
||||||
pointer: null, toggle: false, brakeButton: false, drag: 0, buttons: 0, anchor: null, finger: null, sens: SWIPE.sens,
|
pointer: null, toggle: false, brakeButton: false, drag: 0, buttons: 0, anchor: null, finger: null, sens: SWIPE.sens,
|
||||||
power: SWIPE.sens, accumulator: 0, events: [], result: null, order: riders.map(r => r.id), taken: {} };
|
power: SWIPE.sens, accumulator: 0, events: [], result: null, order: riders.map(r => r.id), taken: {} };
|
||||||
}
|
}
|
||||||
@@ -258,12 +266,75 @@ export function steering(c) {
|
|||||||
// La distanza davanti, sul giro, da s a un punto della pista (sempre fra 0 e la lunghezza del giro).
|
// La distanza davanti, sul giro, da s a un punto della pista (sempre fra 0 e la lunghezza del giro).
|
||||||
export const ahead = (from, to) => ((((to - from) % TRACK.length) + TRACK.length) % TRACK.length);
|
export const ahead = (from, to) => ((((to - from) % TRACK.length) + TRACK.length) % TRACK.length);
|
||||||
|
|
||||||
|
// Lo sterzo che segue la strada: si guarda un punto più avanti sulla propria corsia e ci si gira (gli avversari, e
|
||||||
|
// Andrea nella guida semplice). Andando troppo forte in curva lo sterzo non basta e si scivola fuori.
|
||||||
|
const follow = r => { const aim = pointAt(r.s + 14 + r.v * 0.18, r.lane * 0.6); return clamp(wrapAngle(Math.atan2(aim.y - r.y, aim.x - r.x) - r.a) * 2.6, -1, 1); };
|
||||||
|
// La guida semplice di Andrea: lo sterzo scansa le buche, sorpassa e va sulle lattine di miscela; ma oltre la velocità
|
||||||
|
// che la curva regge (la più stretta nei SIMPLE.window pixel davanti) il motorino scivola verso l'esterno, tanto più
|
||||||
|
// quanto si esagera (SIMPLE.slide): a tutto gas, nelle curve vere si finisce nella paglia.
|
||||||
|
// Ogni livello può stringere (LEVELS.*.tune). `clean`: il premio della curva pulita, presa senza mai passare il
|
||||||
|
// limite (`slack` px/s di tolleranza) e senza frenare troppo (almeno `min` del limite): per `time` secondi in uscita
|
||||||
|
// si accelera di più (`accel`), senza superare la velocità massima. `force`: l'uscita sicura, sopra `over` volte il
|
||||||
|
// limite per `hold` secondi (o `apex` volte nel punto più stretto) si va nella paglia in `time` secondi, una volta per
|
||||||
|
// curva; rientrati, `guard` secondi di tregua.
|
||||||
|
export const SIMPLE = { grip: 1, slide: 8, window: 50, clean: null, force: null };
|
||||||
|
export function simpleLimit(r, s = r.s, tune = SIMPLE) {
|
||||||
|
let low = Infinity;
|
||||||
|
for (let d = -10; d <= tune.window; d += STEP) low = Math.min(low, limitAt(s + d));
|
||||||
|
return low * r.grip * tune.grip;
|
||||||
|
}
|
||||||
|
// La curva in corso: dentro si tiene il peggio dell'eccesso e la velocità più bassa (per il premio), e si guarda se
|
||||||
|
// si è andati troppo forte abbastanza a lungo (per l'uscita sicura).
|
||||||
|
function judgeCurve(c, r, dt) {
|
||||||
|
const tune = c.tune, s = ((r.s % TRACK.length) + TRACK.length) % TRACK.length;
|
||||||
|
const k = CURVES.find(k => s >= k.start - 20 && s <= k.end + 10);
|
||||||
|
if (k) {
|
||||||
|
const w = r.curve?.k === k ? r.curve : (r.curve = { k, excess: 0, slow: Infinity, over: 0, fault: false });
|
||||||
|
w.excess = Math.max(w.excess, r.excess); w.slow = Math.min(w.slow, r.v);
|
||||||
|
const f = tune.force, limit = limitAt(k.apex) * r.grip * tune.grip;
|
||||||
|
if (f && !w.fault && !(r.guard > 0) && r.out === 0) {
|
||||||
|
w.over = r.v > limit * f.over ? w.over + dt : 0;
|
||||||
|
if (w.over >= f.hold || (Math.abs(s - k.apex) < STEP * 2 && r.v > limit * f.apex)) { w.fault = true; r.forced = f.time; }
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const w = r.curve; r.curve = null;
|
||||||
|
if (!w || !tune.clean || w.fault) return;
|
||||||
|
const limit = limitAt(w.k.apex) * r.grip * tune.grip;
|
||||||
|
if (w.excess <= tune.clean.slack && w.slow >= limit * tune.clean.min && r.out === 0) {
|
||||||
|
r.push = tune.clean.time; r.cleans = (r.cleans ?? 0) + 1;
|
||||||
|
c.events.push({ kind: 'gara-giro', who: r.id, power: 1 }); c.clean = c.t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Andrea, nella guida semplice, deve lasciare entro `lead` secondi per arrivare alle curve davanti sotto il limite.
|
||||||
|
// Sopra il motorino: CURVA! in giallo con PREAVVISO secondi d'anticipo, poi LASCIA! in rosso quando è il momento (sul
|
||||||
|
// telefono non serve l'istante preciso, e lasciare troppo presto, che qui vuol dire frenare, non conviene).
|
||||||
|
export const PREAVVISO = 0.7, LIFT = 0;
|
||||||
|
export function mustLift(c, lead = LIFT) {
|
||||||
|
const r = player(c);
|
||||||
|
if (!simple(c) || c.step !== 'race' || r.out > 0) return false;
|
||||||
|
const horizon = r.v * r.v / (2 * BRAKE) + r.v * lead + 10;
|
||||||
|
for (let d = 0; d <= horizon; d += 4) {
|
||||||
|
const a = simpleLimit(r, r.s + d, c.tune), room = Math.max(0, d - r.v * lead);
|
||||||
|
if (r.v > a && r.v * r.v > a * a + 2 * BRAKE * room) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function autopilot(c, r) {
|
||||||
|
const next = PICKUPS.find(p => p.kind !== 'boost' && ahead(r.s, p.s) < 30 + r.v * 0.6);
|
||||||
|
// Chi sta davanti più lento si passa dall'altra parte; le buche si scansano, la miscela si prende.
|
||||||
|
const slow = c.riders.find(o => o !== r && o.s - r.s > 0 && o.s - r.s < 26 && Math.abs(o.lat - r.lat) < 7 && o.v < r.v + 8);
|
||||||
|
if (next?.kind === 'buca') r.lane = next.lane > 0 ? -5 : 5;
|
||||||
|
else if (slow) r.lane = slow.lat > 0 ? -6 : 6;
|
||||||
|
else if (next?.kind === 'miscela') r.lane = next.lane;
|
||||||
|
r.excess = Math.max(0, r.v - simpleLimit(r, r.s, c.tune));
|
||||||
|
return follow(r);
|
||||||
|
}
|
||||||
// Gli avversari: guardano un punto più avanti sulla loro corsia e ci sterzano; danno gas finché, frenando da qui,
|
// Gli avversari: guardano un punto più avanti sulla loro corsia e ci sterzano; danno gas finché, frenando da qui,
|
||||||
// arriverebbero a ogni curva davanti sotto il limite (un po' meno, quanto si fidano). Chi ha poca testa ogni tanto
|
// arriverebbero a ogni curva davanti sotto il limite (un po' meno, quanto si fidano). Chi ha poca testa ogni tanto
|
||||||
// si fida troppo.
|
// si fida troppo.
|
||||||
function drive(c, r) {
|
function drive(c, r) {
|
||||||
const aim = pointAt(r.s + 14 + r.v * 0.18, r.lane * 0.6);
|
r.steer = follow(r);
|
||||||
r.steer = clamp(wrapAngle(Math.atan2(aim.y - r.y, aim.x - r.x) - r.a) * 2.6, -1, 1);
|
|
||||||
const curve = CURVES.find(k => { const s = ((r.s % TRACK.length) + TRACK.length) % TRACK.length; return s >= k.start - 60 && s < k.start - 57; });
|
const curve = CURVES.find(k => { const s = ((r.s % TRACK.length) + TRACK.length) % TRACK.length; return s >= k.start - 60 && s < k.start - 57; });
|
||||||
if (curve && r.lastCurve !== curve) { r.lastCurve = curve; r.risk = random(c) < r.slip ? 1.15 : 1; }
|
if (curve && r.lastCurve !== curve) { r.lastCurve = curve; r.risk = random(c) < r.slip ? 1.15 : 1; }
|
||||||
const horizon = (r.v * r.v) / (2 * BRAKE) + 20;
|
const horizon = (r.v * r.v) / (2 * BRAKE) + 20;
|
||||||
@@ -283,22 +354,28 @@ function drive(c, r) {
|
|||||||
}
|
}
|
||||||
function stepRider(c, r, dt) {
|
function stepRider(c, r, dt) {
|
||||||
if (r.finished !== null) { r.gas = false; r.brake = false; drive(c, r); r.gas = false; }
|
if (r.finished !== null) { r.gas = false; r.brake = false; drive(c, r); r.gas = false; }
|
||||||
else if (r.out > 0) { r.out = Math.max(0, r.out - dt); r.v = 0; r.u = 0; return; }
|
else if (r.out > 0) { r.out = Math.max(0, r.out - dt); r.v = 0; r.u = 0; if (!r.out && c.tune.force) r.guard = c.tune.force.guard; return; }
|
||||||
|
else if (r.id === 'andrea' && simple(c)) { r.gas = c.holding; r.brake = !c.holding; r.steer = autopilot(c, r); judgeCurve(c, r, dt); }
|
||||||
else if (r.id === 'andrea') { r.gas = c.holding; r.brake = c.braking; r.steer = steering(c); }
|
else if (r.id === 'andrea') { r.gas = c.holding; r.brake = c.braking; r.steer = steering(c); }
|
||||||
else drive(c, r);
|
else drive(c, r);
|
||||||
if (r.finished !== null) r.gas = false;
|
if (r.finished !== null) r.gas = false;
|
||||||
const grass = Math.abs(r.lat) > ROAD, top = r.vmax * (r.boost > 0 ? BOOST.speed : 1) * (grass ? 0.5 : 1);
|
const grass = Math.abs(r.lat) > ROAD, top = r.vmax * (r.boost > 0 ? BOOST.speed : 1) * (grass ? 0.5 : 1);
|
||||||
// Il freno vince sul gas: chi li tiene tutti e due frena.
|
// Il freno vince sul gas: chi li tiene tutti e due frena.
|
||||||
if (r.brake) r.v -= BRAKE * dt;
|
if (r.brake) { r.v -= BRAKE * dt; r.push = 0; }
|
||||||
else if (r.gas) r.v += r.accel * dt * (1 - 0.4 * r.v / top) * (r.boost > 0 ? 1.6 : 1);
|
else if (r.gas) r.v += r.accel * dt * (1 - 0.4 * r.v / top) * (r.boost > 0 ? 1.6 : 1) * (r.push > 0 ? c.tune.clean.accel : 1);
|
||||||
else r.v -= COAST * dt;
|
else r.v -= COAST * dt;
|
||||||
if (r.v > top) r.v = Math.max(top, r.v - 120 * dt);
|
if (r.v > top) r.v = Math.max(top, r.v - 120 * dt);
|
||||||
r.v = Math.max(0, r.v);
|
r.v = Math.max(0, r.v);
|
||||||
r.boost = Math.max(0, r.boost - dt);
|
r.boost = Math.max(0, r.boost - dt);
|
||||||
|
if (r.push) r.push = Math.max(0, r.push - dt);
|
||||||
|
if (r.guard) r.guard = Math.max(0, r.guard - dt);
|
||||||
// Lo sterzo: da fermi non si gira; in curva una parte della velocità resta per traverso e si scivola fuori.
|
// Lo sterzo: da fermi non si gira; in curva una parte della velocità resta per traverso e si scivola fuori.
|
||||||
const turn = r.steer * TURN * Math.min(1, r.v / 22) * dt;
|
const turn = r.steer * TURN * Math.min(1, r.v / 22) * dt;
|
||||||
r.a = wrapAngle(r.a + turn);
|
r.a = wrapAngle(r.a + turn);
|
||||||
r.u -= r.v * Math.sin(turn) * SLIDE * (r.v / r.vmax) / r.grip;
|
r.u -= r.v * Math.sin(turn) * SLIDE * (r.v / r.vmax) / r.grip;
|
||||||
|
if (r.excess) r.u -= TRACK.side[r.idx] * r.excess * c.tune.slide * dt; // la guida semplice: troppo forte, si allarga
|
||||||
|
// L'uscita sicura: verso l'esterno della curva, e lo sterzo non la salva.
|
||||||
|
if (r.forced > 0) { r.forced = Math.max(0, r.forced - dt); r.u = -(r.curve?.k.side ?? TRACK.side[r.idx]) * 90; r.v = Math.max(r.v, 30); }
|
||||||
r.u *= Math.exp(-GRIP * r.grip * dt);
|
r.u *= Math.exp(-GRIP * r.grip * dt);
|
||||||
r.x += (Math.cos(r.a) * r.v - Math.sin(r.a) * r.u) * dt;
|
r.x += (Math.cos(r.a) * r.v - Math.sin(r.a) * r.u) * dt;
|
||||||
r.y += (Math.sin(r.a) * r.v + Math.cos(r.a) * r.u) * dt;
|
r.y += (Math.sin(r.a) * r.v + Math.cos(r.a) * r.u) * dt;
|
||||||
@@ -307,7 +384,8 @@ function stepRider(c, r, dt) {
|
|||||||
if (Math.abs(r.lat) > VERGE) {
|
if (Math.abs(r.lat) > VERGE) {
|
||||||
const h = TRACK.heading[r.idx], p = TRACK.pts[r.idx], side = Math.sign(r.lat), back = side * (VERGE - 1);
|
const h = TRACK.heading[r.idx], p = TRACK.pts[r.idx], side = Math.sign(r.lat), back = side * (VERGE - 1);
|
||||||
r.x = p[0] - Math.sin(h) * back; r.y = p[1] + Math.cos(h) * back; r.lat = back; r.u = 0;
|
r.x = p[0] - Math.sin(h) * back; r.y = p[1] + Math.cos(h) * back; r.lat = back; r.u = 0;
|
||||||
if (r.v > 40 && r.finished === null) {
|
if ((r.v > 40 || r.forced > 0) && r.finished === null) {
|
||||||
|
r.forced = 0;
|
||||||
r.out = c.skill.stun * (r.id === 'andrea' ? 1 : 0.7); r.offs++; r.risk = 1;
|
r.out = c.skill.stun * (r.id === 'andrea' ? 1 : 0.7); r.offs++; r.risk = 1;
|
||||||
c.events.push({ kind: 'gara-fuori', who: r.id, power: r.id === 'andrea' ? 1 : 0.4 });
|
c.events.push({ kind: 'gara-fuori', who: r.id, power: r.id === 'andrea' ? 1 : 0.4 });
|
||||||
}
|
}
|
||||||
@@ -406,7 +484,7 @@ export function status(c) {
|
|||||||
const r = presenting(c);
|
const r = presenting(c);
|
||||||
return r ? `${r.name} con ${r.bike}. (Un tocco: il prossimo.)` : 'Tutti in griglia!';
|
return r ? `${r.name} con ${r.bike}. (Un tocco: il prossimo.)` : 'Tutti in griglia!';
|
||||||
}
|
}
|
||||||
if (c.step === 'start') return c.t < COUNTDOWN - 0.01 ? `GP di Fortezza: si parte fra ${Math.ceil(COUNTDOWN - c.t)}… Tieni premuto per il gas, sterza con ← →.` : 'Via!';
|
if (c.step === 'start') return c.t < COUNTDOWN - 0.01 ? `GP di Fortezza: si parte fra ${Math.ceil(COUNTDOWN - c.t)}… Tieni premuto per il gas, ${simple(c) ? 'lascia per frenare' : 'sterza con ← →'}.` : 'Via!';
|
||||||
if (c.step === 'result') {
|
if (c.step === 'result') {
|
||||||
const who = c.riders.find(r => r.id === c.result.order[0]).name;
|
const who = c.riders.find(r => r.id === c.result.order[0]).name;
|
||||||
return c.result.place === 1 ? `Primo al GP di Fortezza! ${LAPS} giri in ${c.result.times.andrea.toFixed(1)} secondi.` : `${c.result.place}° posto. Ha vinto ${who}.`;
|
return c.result.place === 1 ? `Primo al GP di Fortezza! ${LAPS} giri in ${c.result.times.andrea.toFixed(1)} secondi.` : `${c.result.place}° posto. Ha vinto ${who}.`;
|
||||||
@@ -414,6 +492,9 @@ export function status(c) {
|
|||||||
const me = player(c), place = placeOf(c);
|
const me = player(c), place = placeOf(c);
|
||||||
if (me.out > 0) return me.lastHole !== undefined ? 'Nella buca! Ci si rialza…' : 'Nella paglia! Si riparte…';
|
if (me.out > 0) return me.lastHole !== undefined ? 'Nella buca! Ci si rialza…' : 'Nella paglia! Si riparte…';
|
||||||
if (me.boost > 0) return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. Miscela!`;
|
if (me.boost > 0) return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. Miscela!`;
|
||||||
|
if (simple(c)) return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. ${c.clean !== undefined && c.t - c.clean < 1.5 ? 'Curva pulita!' : mustLift(c) && c.holding ? 'Lascia!' : mustLift(c, PREAVVISO) && c.holding ? 'Curva!' : c.holding ? 'Gas!' : 'Freno.'}`;
|
||||||
return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. ${c.braking ? 'Freno!' : c.holding ? 'Gas!' : 'Senza gas.'}`;
|
return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. ${c.braking ? 'Freno!' : c.holding ? 'Gas!' : 'Senza gas.'}`;
|
||||||
}
|
}
|
||||||
export const button = c => c.step === 'intro' ? 'Avanti' : c.step === 'result' ? 'Finita' : c.holding ? 'Molla' : 'Gas';
|
// Nella guida semplice il pulsante si tiene premuto: la scritta resta Gas (su iPhone un pulsante che cambia sotto il dito
|
||||||
|
// perde il tocco).
|
||||||
|
export const button = c => c.step === 'intro' ? 'Avanti' : c.step === 'result' ? 'Finita' : simple(c) ? 'Gas' : c.holding ? 'Molla' : 'Gas';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HALF, CAR, canThrow, guide, done, overHeld } from './micro.js';
|
import { HALF, CAR, CARS, canThrow, guide, done, overHeld, carImage, choiceAt, why, rank } from './micro.js';
|
||||||
import { text, panel } from './bocce-render.js';
|
import { text, panel } from './bocce-render.js';
|
||||||
|
|
||||||
const NAME = { player: 'Andrea', cpu: 'Chiaru' };
|
const NAME = { player: 'Andrea', cpu: 'Chiaru' };
|
||||||
@@ -68,6 +68,12 @@ function aim(ctx, c, img) {
|
|||||||
ctx.fillStyle = over ? '#ff6a4a' : '#e8b48f'; ctx.strokeStyle = '#6b3d26'; ctx.lineWidth = 1;
|
ctx.fillStyle = over ? '#ff6a4a' : '#e8b48f'; ctx.strokeStyle = '#6b3d26'; ctx.lineWidth = 1;
|
||||||
ctx.beginPath(); ctx.arc(fx, fy, 4, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
ctx.beginPath(); ctx.arc(fx, fy, 4, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||||
ctx.fillStyle = '#f7d9c4'; ctx.fillRect(Math.round(fx + ux * 1.5) - 1, Math.round(fy + uy * 1.5) - 1, 2, 2); // l'unghia
|
ctx.fillStyle = '#f7d9c4'; ctx.fillRect(Math.round(fx + ux * 1.5) - 1, Math.round(fy + uy * 1.5) - 1, 2, 2); // l'unghia
|
||||||
|
// La forza che sale mentre si tiene premuto: una barretta sopra la macchinina.
|
||||||
|
if (c.holding) {
|
||||||
|
const x = Math.round(me.x - 15), y = Math.round(me.y - 19);
|
||||||
|
ctx.fillStyle = '#17160fd0'; ctx.fillRect(x - 1, y - 1, 32, 6);
|
||||||
|
ctx.fillStyle = over ? '#ff6a4a' : c.power > 0.75 ? '#ffd23c' : '#9be07c'; ctx.fillRect(x, y, Math.round(30 * c.power), 4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Dove una macchinina ha fatto il fallo: una croce di gesso rosso che sfuma, e la macchinina (ribaltata, se ha cappottato)
|
// Dove una macchinina ha fatto il fallo: una croce di gesso rosso che sfuma, e la macchinina (ribaltata, se ha cappottato)
|
||||||
// che sparisce per tornare al suo posto.
|
// che sparisce per tornare al suo posto.
|
||||||
@@ -83,11 +89,27 @@ function returns(ctx, c, images) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// La scelta della macchinina: le sei in grande sul parcheggio, quella scelta cerchiata di gesso e un po' sollevata.
|
||||||
|
function choice(ctx, c, image) {
|
||||||
|
ctx.fillStyle = '#17160fc8'; ctx.fillRect(60, 56, 520, 250);
|
||||||
|
text(ctx, 'SCEGLI LA TUA MACCHININA', 320, 76, 12, '#efd49e', 'center');
|
||||||
|
CARS.forEach((car, i) => {
|
||||||
|
const [x, y] = choiceAt(i), on = i === c.choice, lift = on ? Math.sin(c.visual.time * 5) * 1.5 - 2 : 0, img = image?.(carImage(car.id, true));
|
||||||
|
if (on) { ctx.strokeStyle = 'rgba(246,246,238,0.9)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.ellipse(x, y, 50, 28, 0, 0, Math.PI * 2); ctx.stroke(); }
|
||||||
|
ctx.fillStyle = 'rgba(10,12,16,0.35)'; ctx.fillRect(x - 37, y - 16, 78, 38); // l'ombra
|
||||||
|
if (img) ctx.drawImage(img, Math.round(x - img.width / 2), Math.round(y - img.height / 2 + lift));
|
||||||
|
text(ctx, car.name, x, y + 34, 8, on ? '#fff0c8' : '#a99d86', 'center');
|
||||||
|
});
|
||||||
|
const cpu = CARS[c.choice].id === 'blu' ? 'la rossa' : 'la blu e gialla';
|
||||||
|
text(ctx, `Il Chiaru prende ${cpu}. Tocca la tua, o frecce e Scegli.`, 320, 292, 8, '#d9ccba', 'center');
|
||||||
|
}
|
||||||
|
|
||||||
export function drawMicro(ctx, c, def, bg, image) {
|
export function drawMicro(ctx, c, def, bg, image) {
|
||||||
ctx.save(); ctx.imageSmoothingEnabled = false;
|
ctx.save(); ctx.imageSmoothingEnabled = false;
|
||||||
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#4b4d50'; ctx.fillRect(0, 0, 640, 360); }
|
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#4b4d50'; ctx.fillRect(0, 0, 640, 360); }
|
||||||
drawChalk(ctx, c.track);
|
drawChalk(ctx, c.track);
|
||||||
const images = { player: image?.(def.cars?.player), cpu: image?.(def.cars?.cpu) };
|
if (c.step === 'choose') { choice(ctx, c, image); ctx.restore(); return; }
|
||||||
|
const images = { player: image?.(carImage(c.colors.player)), cpu: image?.(carImage(c.colors.cpu)) };
|
||||||
if (c.step === 'rolling' && c.trail.length > 1) {
|
if (c.step === 'rolling' && c.trail.length > 1) {
|
||||||
ctx.strokeStyle = 'rgba(255,248,220,0.35)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(...c.trail[0]);
|
ctx.strokeStyle = 'rgba(255,248,220,0.35)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(...c.trail[0]);
|
||||||
for (const p of c.trail.slice(1)) ctx.lineTo(...p);
|
for (const p of c.trail.slice(1)) ctx.lineTo(...p);
|
||||||
@@ -101,21 +123,28 @@ export function drawMicro(ctx, c, def, bg, image) {
|
|||||||
text(ctx, 'LA PISTA COL GESSO', 8, 7, 8, '#efd49e');
|
text(ctx, 'LA PISTA COL GESSO', 8, 7, 8, '#efd49e');
|
||||||
const level = { bona: 'alla bona', seria: 'seria', male: 'a fassi male' }[c.level];
|
const level = { bona: 'alla bona', seria: 'seria', male: 'a fassi male' }[c.level];
|
||||||
text(ctx, `${level} · spinte ${c.flicks.player}`, 8, 16, 7, '#d9ccba');
|
text(ctx, `${level} · spinte ${c.flicks.player}`, 8, 16, 7, '#d9ccba');
|
||||||
for (const [who, x0, color] of [['player', 168, '#e0543c'], ['cpu', 404, '#3c66d8']]) {
|
const tint = id => CARS.find(x => x.id === id).color;
|
||||||
|
for (const [who, x0, color] of [['player', 200, tint(c.colors.player)], ['cpu', 430, tint(c.colors.cpu)]]) {
|
||||||
|
// Primo o secondo, mentre si va (Andrea, 29 ottobre).
|
||||||
|
const place = rank(c, who);
|
||||||
|
text(ctx, `${place}°`, x0 - 4, 11, 9, place === 1 ? '#ffe28a' : '#a99d86', 'right');
|
||||||
text(ctx, NAME[who], x0, 11, 8, '#fff0c8');
|
text(ctx, NAME[who], x0, 11, 8, '#fff0c8');
|
||||||
ctx.fillStyle = '#3a372e'; ctx.fillRect(x0 + 52, 7, 150, 8);
|
ctx.fillStyle = '#3a372e'; ctx.fillRect(x0 + 52, 7, 140, 8);
|
||||||
ctx.fillStyle = color; ctx.fillRect(x0 + 52, 7, Math.round(150 * done(c, who)), 8);
|
ctx.fillStyle = color; ctx.fillRect(x0 + 52, 7, Math.round(140 * done(c, who)), 8);
|
||||||
ctx.strokeStyle = '#897b57'; ctx.lineWidth = 1; ctx.strokeRect(x0 + 52.5, 7.5, 149, 7);
|
ctx.strokeStyle = '#897b57'; ctx.lineWidth = 1; ctx.strokeRect(x0 + 52.5, 7.5, 139, 7);
|
||||||
}
|
}
|
||||||
const turn = c.step === 'result' ? null : c.current === 'cpu' ? 'Spinge il Chiaru…' : c.step === 'aim' ? 'Tocca a te: mira e spingi' : null;
|
const turn = c.step === 'result' ? null : c.current === 'cpu' ? 'Spinge il Chiaru…' : c.step === 'aim' ? 'Tocca a te: mira e spingi' : null;
|
||||||
const line = overHeld(c) ? 'Lascia! Sennò è accompagnè' : c.line ?? turn;
|
const line = overHeld(c) ? 'Lascia! Sennò è accompagnè' : c.line ?? turn;
|
||||||
if (line) { ctx.fillStyle = '#17160fd0'; ctx.fillRect(170, 24, 300, 14); text(ctx, line, 320, 31, 8, /^(Fuori|Tagliè|Cappottè|Accompagnè)!/.test(c.line ?? '') ? '#ffbf9f' : '#9fd0ff', 'center'); }
|
if (line) { ctx.fillStyle = '#17160fd0'; ctx.fillRect(170, 24, 300, 14); text(ctx, line, 320, 31, 8, /^(Fuori|Tagliè|Cappottè|Accompagnè)!/.test(c.line ?? '') ? '#ffbf9f' : '#9fd0ff', 'center'); }
|
||||||
if (c.step === 'result') {
|
if (c.step === 'result') {
|
||||||
const w = c.result.winner, title = w === 'player' ? 'HAI BATTUTO IL CHIARU!' : w === 'cpu' ? 'HA VINTO IL CHIARU' : 'ARRIVATI PARI';
|
const w = c.result.winner, title = w === 'player' ? 'HAI BATTUTO IL CHIARU!' : w === 'cpu' ? 'HA VINTO IL CHIARU' : 'ARRIVATI PARI';
|
||||||
panel(ctx, 170, 140, 300, 72);
|
// Il perché, in due righe: chi è arrivato a quale spinta, e di quanto è andato più in là.
|
||||||
text(ctx, title, 320, 160, 12, w === 'player' ? '#b9e5b3' : '#efd49e', 'center');
|
const [first, second] = why(c);
|
||||||
text(ctx, `Un giro, ${c.result.flicks.player} spinte a testa`, 320, 182, 8, '#efd49e', 'center');
|
panel(ctx, 130, 132, 380, 92);
|
||||||
text(ctx, 'Torna dal Chiaru per uscire.', 320, 198, 8, '#d9ccba', 'center');
|
text(ctx, title, 320, 150, 12, w === 'player' ? '#b9e5b3' : '#efd49e', 'center');
|
||||||
|
text(ctx, first, 320, 172, 8, '#efd49e', 'center');
|
||||||
|
text(ctx, second, 320, 186, 8, '#efd49e', 'center');
|
||||||
|
text(ctx, 'Torna dal Chiaru per uscire.', 320, 208, 8, '#d9ccba', 'center');
|
||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const CONTROL = [
|
|||||||
];
|
];
|
||||||
export const HALF = 22, CAR = { len: 26, wid: 13, r: 6 }, LAPS = 1;
|
export const HALF = 22, CAR = { len: 26, wid: 13, r: 6 }, LAPS = 1;
|
||||||
// La spinta: velocità di partenza da MIN_SPEED a MAX_SPEED, e l'asfalto che frena di DECEL px/s².
|
// La spinta: velocità di partenza da MIN_SPEED a MAX_SPEED, e l'asfalto che frena di DECEL px/s².
|
||||||
export const MIN_SPEED = 60, MAX_SPEED = 400, DECEL = 420, SWING = 1.2, LONG_PRESS = 0.6, FIXED_STEP = 1 / 120;
|
export const MIN_SPEED = 60, MAX_SPEED = 400, DECEL = 420, SWING = 1.2, TAP = 0.14, FIXED_STEP = 1 / 120;
|
||||||
const REST = 0.7, THINK = 1, RESTITUTION = 0.6, OUT = HALF + CAR.r, WINDOW = 30;
|
const REST = 0.7, THINK = 1, RESTITUTION = 0.6, OUT = HALF + CAR.r, WINDOW = 30;
|
||||||
// FLIP: la velocità d'urto (px/s) che ribalta la macchinina colpita; ACCOMPAGNA: quanti secondi si può restare alla
|
// FLIP: la velocità d'urto (px/s) che ribalta la macchinina colpita; ACCOMPAGNA: quanti secondi si può restare alla
|
||||||
// carica piena prima che diventi un accompagnè.
|
// carica piena prima che diventi un accompagnè.
|
||||||
@@ -144,14 +144,43 @@ function step(track, list, dt, events) {
|
|||||||
for (const car of list) if (car.vx || car.vy) follow(track, car);
|
for (const car of list) if (car.vx || car.vy) follow(track, car);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Le macchinine fra cui scegliere (Andrea, 29 ottobre: «quando si parte fammi scegliere la macchinina»): il disegno di
|
||||||
|
// Codex in `assets/closeups/micro/<id>.png` (e `<id>-grande.png` per la scelta) e il colore della barra in alto.
|
||||||
|
// Il Chiaru tiene la sua blu e gialla; se la prende Andrea, lui prende la rossa.
|
||||||
|
export const CARS = [
|
||||||
|
{ id: 'rossa', name: 'Rossa e bianca', color: '#e0543c' },
|
||||||
|
{ id: 'blu', name: 'Blu e gialla', color: '#3c66d8' },
|
||||||
|
{ id: 'nera', name: 'Nera e oro', color: '#c9a23a' },
|
||||||
|
{ id: 'azzurra', name: 'Bianca e azzurra', color: '#6fb7ff' },
|
||||||
|
{ id: 'arancio', name: 'Arancione', color: '#f08a2c' },
|
||||||
|
{ id: 'viola', name: 'Viola e argento', color: '#9a5ad8' },
|
||||||
|
];
|
||||||
|
export const carImage = (id, big = false) => `assets/closeups/micro/${id}${big ? '-grande' : ''}.png`;
|
||||||
|
// Dove stanno le macchinine nella scelta: due file da tre, al centro del parcheggio.
|
||||||
|
export const choiceAt = i => [160 + (i % 3) * 160, 132 + Math.floor(i / 3) * 88];
|
||||||
|
export function pick(c, i = c.choice) {
|
||||||
|
if (c.step !== 'choose' || !CARS[i]) return false;
|
||||||
|
const mine = CARS[i].id;
|
||||||
|
c.colors = { player: mine, cpu: mine === 'blu' ? 'rossa' : 'blu' };
|
||||||
|
c.step = 'aim'; c.t = 0; c.aimLocked = false; // il clic che sceglie non deve fissare anche la mira
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function choose(c, i) {
|
||||||
|
if (c.step !== 'choose' || !Number.isFinite(i)) return false;
|
||||||
|
c.choice = (i + CARS.length) % CARS.length; return true;
|
||||||
|
}
|
||||||
|
const carUnder = (x, y) => CARS.findIndex((_, i) => { const [cx, cy] = choiceAt(i); return Math.abs(x - cx) < 44 && Math.abs(y - cy) < 30; });
|
||||||
|
|
||||||
export function newMicro(seed = Date.now() >>> 0, def = {}) {
|
export function newMicro(seed = Date.now() >>> 0, def = {}) {
|
||||||
const level = LEVELS[def.level] ? def.level : 'seria', track = TRACK;
|
const level = LEVELS[def.level] ? def.level : 'seria', track = TRACK;
|
||||||
const c = { step: 'aim', t: 0, seed, level, skill: LEVELS[level], current: 'player', power: 0.5, angle: 0,
|
const c = { step: 'choose', choice: 0, colors: null, t: 0, seed, level, skill: LEVELS[level], current: 'player', power: 0.5, angle: 0,
|
||||||
holding: false, holdTime: 0, accumulator: 0, events: [], track, laps: LAPS, flicks: { player: 0, cpu: 0 },
|
holding: false, holdTime: 0, accumulator: 0, events: [], track, laps: LAPS, flicks: { player: 0, cpu: 0 },
|
||||||
line: null, trail: [], returns: [], before: null, plan: null, visual: { time: 0 } };
|
line: null, trail: [], returns: [], before: null, plan: null, visual: { time: 0 } };
|
||||||
// In griglia: Andrea all'esterno, il Chiaru all'interno, poco prima del traguardo.
|
// In griglia: Andrea all'esterno, il Chiaru all'interno, poco prima del traguardo.
|
||||||
c.cars = { player: newCar(track, 'player', 18, -11), cpu: newCar(track, 'cpu', 18, 11) };
|
c.cars = { player: newCar(track, 'player', 18, -11), cpu: newCar(track, 'cpu', 18, 11) };
|
||||||
c.angle = c.cars.player.heading;
|
c.angle = c.cars.player.heading;
|
||||||
|
// Si parte scegliendo la macchinina, se non è già data (`def.car`).
|
||||||
|
if (def.car) pick(c, CARS.findIndex(x => x.id === def.car));
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +265,7 @@ function settle(c) {
|
|||||||
const end = c.laps * c.track.length, over = { player: c.cars.player.progress - end, cpu: c.cars.cpu.progress - end };
|
const end = c.laps * c.track.length, over = { player: c.cars.player.progress - end, cpu: c.cars.cpu.progress - end };
|
||||||
if (c.shooter === 'cpu' && (over.player >= 0 || over.cpu >= 0)) {
|
if (c.shooter === 'cpu' && (over.player >= 0 || over.cpu >= 0)) {
|
||||||
const winner = over.player >= 0 && over.cpu >= 0 ? (Math.abs(over.player - over.cpu) < 1 ? 'draw' : over.player > over.cpu ? 'player' : 'cpu') : over.player >= 0 ? 'player' : 'cpu';
|
const winner = over.player >= 0 && over.cpu >= 0 ? (Math.abs(over.player - over.cpu) < 1 ? 'draw' : over.player > over.cpu ? 'player' : 'cpu') : over.player >= 0 ? 'player' : 'cpu';
|
||||||
c.step = 'result'; c.result = { winner, flicks: { ...c.flicks } };
|
c.step = 'result'; c.result = { winner, flicks: { ...c.flicks }, over };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Chi ha già tagliato il traguardo aspetta; spinge l'altro.
|
// Chi ha già tagliato il traguardo aspetta; spinge l'altro.
|
||||||
@@ -285,7 +314,10 @@ export function aimAt(c, x, y) {
|
|||||||
if (Math.hypot(x - car.x, y - car.y) < CAR.r) return false;
|
if (Math.hypot(x - car.x, y - car.y) < CAR.r) return false;
|
||||||
c.angle = Math.atan2(y - car.y, x - car.x); return true;
|
c.angle = Math.atan2(y - car.y, x - car.x); return true;
|
||||||
}
|
}
|
||||||
export const aimScreen = (c, [x, y]) => x >= 0 && x <= 640 && y >= 24 && y <= 360 && aimAt(c, x, y);
|
export function aimScreen(c, [x, y]) {
|
||||||
|
if (c.step === 'choose') { const i = carUnder(x, y); return i >= 0 && choose(c, i); }
|
||||||
|
return x >= 0 && x <= 640 && y >= 24 && y <= 360 && aimAt(c, x, y);
|
||||||
|
}
|
||||||
export function nudge(c, dir, fine = false) {
|
export function nudge(c, dir, fine = false) {
|
||||||
if (!canThrow(c) || c.holding || !Number.isFinite(dir)) return false;
|
if (!canThrow(c) || c.holding || !Number.isFinite(dir)) return false;
|
||||||
c.angle += dir * (fine ? 0.004 : 0.03); return true;
|
c.angle += dir * (fine ? 0.004 : 0.03); return true;
|
||||||
@@ -294,8 +326,9 @@ export function setPower(c, value) {
|
|||||||
if (!canThrow(c) || c.holding || !Number.isFinite(value)) return false;
|
if (!canThrow(c) || c.holding || !Number.isFinite(value)) return false;
|
||||||
c.power = clamp(value, 0, 1); return true;
|
c.power = clamp(value, 0, 1); return true;
|
||||||
}
|
}
|
||||||
export function shoot(c) { return canThrow(c) && !c.holding && strike(c, 'player', c.angle, c.power); }
|
export function shoot(c) { return c.step === 'choose' ? pick(c) : canThrow(c) && !c.holding && strike(c, 'player', c.angle, c.power); }
|
||||||
export function turn(c, dir, owner, fine = false) {
|
export function turn(c, dir, owner, fine = false) {
|
||||||
|
if (c.step === 'choose') { choose(c, c.choice + dir); return false; } // nella scelta frecce e pulsanti cambiano macchinina
|
||||||
if (!canThrow(c) || c.holding || c.turn) return false;
|
if (!canThrow(c) || c.holding || c.turn) return false;
|
||||||
c.turn = { dir, owner, fine, time: 0 }; c.angle += dir * (fine ? 0.004 : 0.03);
|
c.turn = { dir, owner, fine, time: 0 }; c.angle += dir * (fine ? 0.004 : 0.03);
|
||||||
c.aimLocked = true; return true;
|
c.aimLocked = true; return true;
|
||||||
@@ -311,10 +344,11 @@ export function key(c, key, down, fine = false) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
export function press(c, pointerId) {
|
export function press(c, pointerId) {
|
||||||
|
if (c.step === 'choose') return pick(c);
|
||||||
if (!canThrow(c) || c.holding) return false;
|
if (!canThrow(c) || c.holding) return false;
|
||||||
c.turn = null; c.pointerId = pointerId; c.holdDelay = pointerId === undefined ? 0 : LONG_PRESS;
|
// Premuto, la forza parte da zero e sale finché non si lascia (Andrea, 29 ottobre): come il dito che si carica.
|
||||||
c.previousPower = c.power; c.holding = true; c.charging = false; c.holdTime = 0;
|
c.turn = null; c.pointerId = pointerId; c.holdDelay = 0;
|
||||||
if (!c.holdDelay) c.power = 0;
|
c.previousPower = c.power; c.holding = true; c.charging = false; c.holdTime = 0; c.power = 0;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
export function cancel(c, pointerId) {
|
export function cancel(c, pointerId) {
|
||||||
@@ -325,7 +359,7 @@ export function cancel(c, pointerId) {
|
|||||||
}
|
}
|
||||||
export function release(c, pointerId) {
|
export function release(c, pointerId) {
|
||||||
if (c.pointerId !== pointerId || !c.holding) return false;
|
if (c.pointerId !== pointerId || !c.holding) return false;
|
||||||
if (c.holdTime < Math.max(0.14, c.holdDelay)) return cancel(c, pointerId); // il tocco mira soltanto
|
if (c.holdTime < TAP) return cancel(c, pointerId); // il tocco secco mira soltanto
|
||||||
const accompagne = (c.overHold ?? 0) > ACCOMPAGNA;
|
const accompagne = (c.overHold ?? 0) > ACCOMPAGNA;
|
||||||
c.holding = c.charging = false; c.overHold = 0;
|
c.holding = c.charging = false; c.overHold = 0;
|
||||||
strike(c, 'player', c.angle, c.power);
|
strike(c, 'player', c.angle, c.power);
|
||||||
@@ -346,16 +380,35 @@ export function guide(c) {
|
|||||||
return { x: car.x + ux * length, y: car.y + uy * length, length, out };
|
return { x: car.x + ux * length, y: car.y + uy * length, length, out };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Il pulsante del pannello vale anche nella scelta della macchinina.
|
||||||
|
export const canPress = c => canThrow(c) || c.step === 'choose';
|
||||||
|
export const canAdjust = canPress;
|
||||||
export function button(c) {
|
export function button(c) {
|
||||||
|
if (c.step === 'choose') return 'Scegli';
|
||||||
if (c.step === 'result') return 'Finita';
|
if (c.step === 'result') return 'Finita';
|
||||||
if (c.step === 'rolling' || c.step === 'settle') return 'In movimento…';
|
if (c.step === 'rolling' || c.step === 'settle') return 'In movimento…';
|
||||||
return c.current === 'cpu' ? 'Spinge il Chiaru…' : 'Spingi';
|
return c.current === 'cpu' ? 'Spinge il Chiaru…' : 'Spingi';
|
||||||
}
|
}
|
||||||
|
// Il perché del risultato, da scrivere (Andrea, 29 ottobre: «ho tagliato prima io, ma ha vinto il Chiaru»). Le
|
||||||
|
// macchinine vere sono lunghe 4 centimetri e mezzo: un pixel è quasi due millimetri.
|
||||||
|
export const CM = 4.5 / CAR.len;
|
||||||
|
export function why(c) {
|
||||||
|
const { winner, flicks, over } = c.result, n = flicks.player;
|
||||||
|
if (over.player >= 0 && over.cpu >= 0) {
|
||||||
|
const cm = Math.max(1, Math.round(Math.abs(over.player - over.cpu) * CM));
|
||||||
|
if (winner === 'draw') return [`Tutti e due al traguardo alla spinta ${n},`, 'pari al millimetro.'];
|
||||||
|
return [`Tutti e due al traguardo alla spinta ${n}:`, `vince chi va più in là, ${winner === 'player' ? 'Andrea' : 'il Chiaru'} di ${cm} ${cm === 1 ? 'centimetro' : 'centimetri'}.`];
|
||||||
|
}
|
||||||
|
return winner === 'player' ? [`Andrea al traguardo alla spinta ${n};`, 'al Chiaru non è bastata la sua.'] : [`Il Chiaru al traguardo alla spinta ${n};`, 'Andrea non c\'è arrivato.'];
|
||||||
|
}
|
||||||
|
// Chi è davanti adesso (1 o 2), per strada fatta.
|
||||||
|
export const rank = (c, who) => c.cars[who].progress >= c.cars[OTHER[who]].progress ? 1 : 2;
|
||||||
// Quanta pista manca al traguardo, in percentuale, per il tabellone.
|
// Quanta pista manca al traguardo, in percentuale, per il tabellone.
|
||||||
export const done = (c, who) => clamp(c.cars[who].progress / (c.laps * c.track.length), 0, 1);
|
export const done = (c, who) => clamp(c.cars[who].progress / (c.laps * c.track.length), 0, 1);
|
||||||
export function status(c) {
|
export function status(c) {
|
||||||
const where = `Andrea ${Math.round(done(c, 'player') * 100)}%, Chiaru ${Math.round(done(c, 'cpu') * 100)}% del giro.`;
|
if (c.step === 'choose') return `Scegli la macchinina: ${CARS[c.choice].name}. Tocca quella che vuoi, o frecce e Scegli.`;
|
||||||
if (c.step === 'result') return `${where} ${c.result.winner === 'player' ? 'Hai battuto il Chiaru!' : c.result.winner === 'cpu' ? 'Ha vinto il Chiaru.' : 'Arrivati pari.'}`;
|
const where = `Andrea ${rank(c, 'player')}° (${Math.round(done(c, 'player') * 100)}%), Chiaru ${rank(c, 'cpu')}° (${Math.round(done(c, 'cpu') * 100)}%).`;
|
||||||
|
if (c.step === 'result') return `${c.result.winner === 'player' ? 'Hai battuto il Chiaru!' : c.result.winner === 'cpu' ? 'Ha vinto il Chiaru.' : 'Arrivati pari.'} ${why(c).join(' ')}`;
|
||||||
if (c.step === 'rolling' || c.step === 'settle') return `${where} ${c.line ?? ''}`.trim();
|
if (c.step === 'rolling' || c.step === 'settle') return `${where} ${c.line ?? ''}`.trim();
|
||||||
if (c.current === 'cpu') return `${where} Spinge il Chiaru…`;
|
if (c.current === 'cpu') return `${where} Spinge il Chiaru…`;
|
||||||
return `${where} ${c.line ? c.line + ' ' : ''}Tocca a te: mira e spingi.`;
|
return `${where} ${c.line ? c.line + ' ' : ''}Tocca a te: mira e spingi.`;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { drawBasket } from './basket-render.js';
|
|||||||
import { drawPesca } from './pesca-render.js';
|
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 { drawBriscola } from './briscola-render.js';
|
import { drawBriscola } from './briscola-render.js';
|
||||||
import { drawTressette } from './tressette-render.js';
|
import { drawTressette } from './tressette-render.js';
|
||||||
import { drawScopa } from './scopa-render.js';
|
import { drawScopa } from './scopa-render.js';
|
||||||
@@ -131,7 +132,7 @@ export function backgroundOrder(story, first) {
|
|||||||
for (const h of story.scenes[queue[i]]?.hotspots ?? [])
|
for (const h of story.scenes[queue[i]]?.hotspots ?? [])
|
||||||
if (h.exit && story.scenes[h.exit.scene] && !seen.has(h.exit.scene)) { seen.add(h.exit.scene); queue.push(h.exit.scene); }
|
if (h.exit && story.scenes[h.exit.scene] && !seen.has(h.exit.scene)) { seen.add(h.exit.scene); queue.push(h.exit.scene); }
|
||||||
for (const id of Object.keys(story.scenes)) if (!seen.has(id)) queue.push(id);
|
for (const id of Object.keys(story.scenes)) if (!seen.has(id)) queue.push(id);
|
||||||
return [...new Set(queue.map(id => story.scenes[id].background).filter(Boolean))];
|
return [...new Set(queue.flatMap(id => [story.scenes[id].background, ...(story.scenes[id].backdrops ?? []).map(b => b.src)]).filter(Boolean))];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Il personaggio alto PERSON pixel a grandezza 1 (per `size` 1), con i piedi in (x, y); rivolto a sinistra si specchia.
|
// Il personaggio alto PERSON pixel a grandezza 1 (per `size` 1), con i piedi in (x, y); rivolto a sinistra si specchia.
|
||||||
@@ -153,8 +154,8 @@ function drawSprite(ctx, { atlas, img }, frame, x, y, scale, facing, size = 1) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function render(ctx, game, { hover, spots, debug, time = performance.now() } = {}) {
|
export function render(ctx, game, { hover, spots, debug, time = performance.now() } = {}) {
|
||||||
const scene = game.scene, s = game.state, bg = backgrounds.get(scene.background);
|
const scene = game.scene, s = game.state, backdrop = game.backdrop(), bg = backgrounds.get(backdrop);
|
||||||
if (!bg && scene.background) loadBackground(scene.background);
|
if (!bg && backdrop) loadBackground(backdrop);
|
||||||
ctx.imageSmoothingEnabled = false;
|
ctx.imageSmoothingEnabled = false;
|
||||||
ctx.clearRect(0, 0, WIDTH, HEIGHT);
|
ctx.clearRect(0, 0, WIDTH, HEIGHT);
|
||||||
if (bg) ctx.drawImage(bg, 0, 0, WIDTH, HEIGHT);
|
if (bg) ctx.drawImage(bg, 0, 0, WIDTH, HEIGHT);
|
||||||
@@ -395,7 +396,7 @@ export const EFFECTS = {
|
|||||||
// Alberi al vento (i cipressi): il pezzo di sfondo si piega tutto insieme dalla base, di frazioni di pixel e con
|
// Alberi al vento (i cipressi): il pezzo di sfondo si piega tutto insieme dalla base, di frazioni di pixel e con
|
||||||
// due onde lente sovrapposte, così il movimento è morbido e non a scatti.
|
// due onde lente sovrapposte, così il movimento è morbido e non a scatti.
|
||||||
sway(ctx, e, time, game) {
|
sway(ctx, e, time, game) {
|
||||||
const bg = backgrounds.get(game.scene.background);
|
const bg = backgrounds.get(game.backdrop());
|
||||||
if (!bg) return;
|
if (!bg) return;
|
||||||
const k = bg.width / WIDTH, amount = e.amount ?? 3.2;
|
const k = bg.width / WIDTH, amount = e.amount ?? 3.2;
|
||||||
for (const [i, [x, y, w, h]] of e.rects.entries()) {
|
for (const [i, [x, y, w, h]] of e.rects.entries()) {
|
||||||
@@ -412,7 +413,7 @@ export const EFFECTS = {
|
|||||||
// ognuna a una sua altezza e velocità, uscita da un lato rientra dall'altro. Si vedono solo dove lo sfondo è
|
// ognuna a una sua altezza e velocità, uscita da un lato rientra dall'altro. Si vedono solo dove lo sfondo è
|
||||||
// cielo (skyMask): passano dietro alberi, torri e tetti. Le due grandi solo coi cieli larghi (`big`).
|
// cielo (skyMask): passano dietro alberi, torri e tetti. Le due grandi solo coi cieli larghi (`big`).
|
||||||
clouds(ctx, e, time, game) {
|
clouds(ctx, e, time, game) {
|
||||||
const bg = backgrounds.get(game.scene.background), mask = bg && skyMask(bg);
|
const bg = backgrounds.get(game.backdrop()), mask = bg && skyMask(bg);
|
||||||
if (!mask) return;
|
if (!mask) return;
|
||||||
const layer = scratch(), lc = layer.getContext('2d'), [ax, ay, aw, ah] = e.area;
|
const layer = scratch(), lc = layer.getContext('2d'), [ax, ay, aw, ah] = e.area;
|
||||||
const pool = e.big ? CLOUDS : CLOUDS.slice(2);
|
const pool = e.big ? CLOUDS : CLOUDS.slice(2);
|
||||||
@@ -452,7 +453,7 @@ export const EFFECTS = {
|
|||||||
// Una bandiera dipinta nello sfondo che sventola: a strisce verticali che salgono e scendono, ferme dalla parte
|
// Una bandiera dipinta nello sfondo che sventola: a strisce verticali che salgono e scendono, ferme dalla parte
|
||||||
// dell'asta (`pole`: 'left' o 'right') e sempre più mosse verso il lembo libero.
|
// dell'asta (`pole`: 'left' o 'right') e sempre più mosse verso il lembo libero.
|
||||||
cloth(ctx, e, time, game) {
|
cloth(ctx, e, time, game) {
|
||||||
const bg = backgrounds.get(game.scene.background);
|
const bg = backgrounds.get(game.backdrop());
|
||||||
if (!bg) return;
|
if (!bg) return;
|
||||||
const k = bg.width / WIDTH;
|
const k = bg.width / WIDTH;
|
||||||
for (const [i, [x, y, w, h]] of e.rects.entries()) {
|
for (const [i, [x, y, w, h]] of e.rects.entries()) {
|
||||||
@@ -687,6 +688,11 @@ function drawCloseup(ctx, game, time) {
|
|||||||
drawBocce(ctx, c, def, backgrounds.get(def.picture), time);
|
drawBocce(ctx, c, def, backgrounds.get(def.picture), time);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (def.type === 'cerbottana') {
|
||||||
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
|
drawCerbottana(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); });
|
||||||
|
|||||||
@@ -136,6 +136,11 @@ export class Soundscape {
|
|||||||
else if (kind === 'gara-sobbalzo') this.tone(160, 90, 0.1, 0.05, 0, pan);
|
else if (kind === 'gara-sobbalzo') this.tone(160, 90, 0.1, 0.05, 0, pan);
|
||||||
else if (kind === 'gara-giro') this.tone(660, 990, 0.15, 0.05, 0, pan);
|
else if (kind === 'gara-giro') this.tone(660, 990, 0.15, 0.05, 0, pan);
|
||||||
else if (kind === 'gara-arrivo') { this.tone(660, 660, 0.12, 0.06 * strength, 0, pan); this.tone(990, 990, 0.3, 0.06 * strength, 0.14, pan); }
|
else if (kind === 'gara-arrivo') { this.tone(660, 660, 0.12, 0.06 * strength, 0, pan); this.tone(990, 990, 0.3, 0.06 * strength, 0.14, pan); }
|
||||||
|
// La cerbottana: il soffio, il pallino che si schiaccia sul muro, la lattina, la tosse.
|
||||||
|
else if (kind === 'cerbottana-pff') this.noiseSound(0.12, 2200, 0.05 + strength * 0.05, { pan });
|
||||||
|
else if (kind === 'cerbottana-muro') { this.tone(180, 90, 0.06, 0.05, 0, pan); this.noiseSound(0.05, 600, 0.04, { pan }); }
|
||||||
|
else if (kind === 'cerbottana-latta') { this.tone(2400, 1600, 0.08, 0.05 + strength * 0.04, 0, pan); this.tone(3400, 2600, 0.05, 0.03, 0.03, 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 }); }
|
||||||
else if (kind === 'micro-strisciata') this.noiseSound(0.3 + strength * 0.7, 900, 0.03 + strength * 0.04, { pan });
|
else if (kind === 'micro-strisciata') this.noiseSound(0.3 + strength * 0.7, 900, 0.03 + strength * 0.04, { pan });
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import * as bocce from './engine/bocce.js';
|
|||||||
import * as biliardo from './engine/biliardo.js';
|
import * as biliardo from './engine/biliardo.js';
|
||||||
import * as rinquarto from './engine/rinquarto.js';
|
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 basket from './engine/basket.js';
|
import * as basket from './engine/basket.js';
|
||||||
import * as pesca from './engine/pesca.js';
|
import * as pesca from './engine/pesca.js';
|
||||||
import * as gara from './engine/gara.js';
|
import * as gara from './engine/gara.js';
|
||||||
@@ -510,7 +511,7 @@ canvas.addEventListener('pointerdown', e => {
|
|||||||
}
|
}
|
||||||
if (press?.closeup) return;
|
if (press?.closeup) return;
|
||||||
const aimed = game.closeupAim(scenePoint(e));
|
const aimed = game.closeupAim(scenePoint(e));
|
||||||
if (['biliardo', 'rinquarto', 'basket', 'micro'].includes(story.closeups[game.closeup.id].type)) {
|
if (['biliardo', 'rinquarto', 'basket', 'micro', 'cerbottana'].includes(story.closeups[game.closeup.id].type)) {
|
||||||
if (!aimed && !game.speech) return; // dove non si può mirare (dietro Andrea) non si carica nemmeno
|
if (!aimed && !game.speech) return; // dove non si può mirare (dietro Andrea) non si carica nemmeno
|
||||||
if (aimed) game.closeup.aimLocked = true; // il clic fissa la mira
|
if (aimed) game.closeup.aimLocked = true; // il clic fissa la mira
|
||||||
}
|
}
|
||||||
@@ -582,7 +583,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'].includes(story.closeups[game.closeup.id].type);
|
const aiming = game.closeup && ['biliardo', 'rinquarto', 'bocce', 'basket', 'pesca', 'micro', 'cerbottana'].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));
|
||||||
});
|
});
|
||||||
@@ -603,10 +604,11 @@ const MINI = {
|
|||||||
bocce: { ...bocce, nudge: (c, dir) => bocce.aim(c, c.aim.u, c.aim.w + dir * 0.3), back: 'Torna al pallaio', label: 'Bocce: tocca il campo o il dettaglio per mirare. Regola la forza e premi Lancia.' },
|
bocce: { ...bocce, nudge: (c, dir) => bocce.aim(c, c.aim.u, c.aim.w + dir * 0.3), back: 'Torna al pallaio', label: 'Bocce: tocca il campo o il dettaglio per mirare. Regola la forza e premi Lancia.' },
|
||||||
biliardo: { ...biliardo, back: 'Torna al bar', label: 'Biliardo: clic o tocco fissa la mira. Frecce per affinarla, Maiusc per passi più piccoli. Regola la forza e premi Tira.' },
|
biliardo: { ...biliardo, back: 'Torna al bar', label: 'Biliardo: clic o tocco fissa la mira. Frecce per affinarla, Maiusc per passi più piccoli. Regola la forza e premi Tira.' },
|
||||||
rinquarto: { ...rinquarto, back: 'Torna da Trappizzio Berti', label: 'I rinquarti di Trappizzio Berti: clic o tocco fissa la mira, frecce o pulsanti Mira per girare la stecca (Maiusc per i passi fini), Tira per tirare. Dopo l’esito, Prossima.' },
|
rinquarto: { ...rinquarto, back: 'Torna da Trappizzio Berti', label: 'I rinquarti di Trappizzio Berti: clic o tocco fissa la mira, frecce o pulsanti Mira per girare la stecca (Maiusc per i passi fini), Tira per tirare. Dopo l’esito, Prossima.' },
|
||||||
|
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.' },
|
||||||
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).
|
||||||
gara: { ...gara, toggle: true, back: 'Torna dal Trappola', label: 'GP di Fortezza: tieni premuto sulla scena per il gas e trascina verso la direzione in cui vuoi andare sullo schermo; alza il dito per mollare. Anche col mouse. Oppure Spazio, Invio o ↑ per il gas, ← → per sterzare, ↓ per frenare; disponibili anche i pulsanti Gas, Sterza e Frena.' },
|
gara: { ...gara, toggle: true, back: 'Torna dal Trappola', label: 'GP di Fortezza. Alla bona e Seria: tieni premuto sulla scena (o Spazio) per il gas e lascia per frenare, lo sterzo va da solo; in curva troppo forte si esce. Ginocchio in terra: tieni premuto sulla scena per il gas e trascina verso la direzione in cui vuoi andare sullo schermo; alza il dito per mollare. Anche col mouse. Oppure Spazio, Invio o ↑ per il gas, ← → per sterzare, ↓ per frenare; disponibili anche i pulsanti Gas, Sterza e Frena.' },
|
||||||
pesca: { ...pesca, back: 'Torna in Fortezza', label: 'La pesca: tieni premuto per lanciare, premi quando il galleggiante va sotto, tieni premuto per recuperare.' },
|
pesca: { ...pesca, back: 'Torna in Fortezza', label: 'La pesca: tieni premuto per lanciare, premi quando il galleggiante va sotto, tieni premuto per recuperare.' },
|
||||||
};
|
};
|
||||||
const miniState = () => running && game?.closeup && MINI[story.closeups[game.closeup.id].type] ? game.closeup : null;
|
const miniState = () => running && game?.closeup && MINI[story.closeups[game.closeup.id].type] ? game.closeup : null;
|
||||||
@@ -634,7 +636,17 @@ for (const ev of ['pointerup', 'pointercancel', 'lostpointercapture']) $('gara-p
|
|||||||
padPointer = null; $('gara-pad').classList.remove('active');
|
padPointer = null; $('gara-pad').classList.remove('active');
|
||||||
});
|
});
|
||||||
$('gara-pad').addEventListener('contextmenu', e => e.preventDefault());
|
$('gara-pad').addEventListener('contextmenu', e => e.preventDefault());
|
||||||
$('bocce-throw').addEventListener('click', () => { const c = miniState(); if (c && !game.speech) mini().shoot?.(c); });
|
// GP, guida semplice: il pulsante Gas si tiene premuto (giù gas, su freno) invece di accendersi e spegnersi al clic;
|
||||||
|
// sul telefono, alla bona, il clic arrivava solo alzando il dito (Andrea, 29 ottobre).
|
||||||
|
const holdGas = c => !!c && story.closeups[c.id].type === 'gara' && gara.simple(c) && c.step !== 'intro' && c.step !== 'result';
|
||||||
|
$('bocce-throw').addEventListener('click', () => { const c = miniState(); if (c && !game.speech && !holdGas(c)) mini().shoot?.(c); });
|
||||||
|
$('bocce-throw').addEventListener('pointerdown', e => {
|
||||||
|
const c = miniState(); if (!holdGas(c) || game.speech) return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (gara.press(c, 'gas-button')) try { $('bocce-throw').setPointerCapture(e.pointerId); } catch { /* un tocco sintetico: va bene lo stesso */ }
|
||||||
|
});
|
||||||
|
for (const ev of ['pointerup', 'pointercancel', 'lostpointercapture']) $('bocce-throw').addEventListener(ev, () => { const c = miniState(); if (holdGas(c)) gara.release(c, 'gas-button'); });
|
||||||
|
$('bocce-throw').addEventListener('contextmenu', e => { if (holdGas(miniState())) e.preventDefault(); });
|
||||||
// Le carte da gioco: dove si tocca la carta sul tavolo, gioco per gioco.
|
// Le carte da gioco: dove si tocca la carta sul tavolo, gioco per gioco.
|
||||||
const CARD_AT = { briscola: cardAt, tressette: tressetteAt, scopa: scopaAt };
|
const CARD_AT = { briscola: cardAt, tressette: tressetteAt, scopa: scopaAt };
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
@@ -935,7 +947,10 @@ function updateUi() {
|
|||||||
updateSpeech();
|
updateSpeech();
|
||||||
const mg = miniState() && mini(), def = mg && story.closeups[c.id];
|
const mg = miniState() && mini(), def = mg && story.closeups[c.id];
|
||||||
const cards = ['briscola', 'tressette', 'scopa'].includes(def?.type);
|
const cards = ['briscola', 'tressette', 'scopa'].includes(def?.type);
|
||||||
setProp($('bocce-controls'), 'hidden', !mg || cards);
|
// La presentazione dei piloti del GP a tutto schermo: i comandi arrivano col semaforo (Andrea, 29 ottobre).
|
||||||
|
const presenta = !!mg && story.closeups[c.id].type === 'gara' && c.step === 'intro';
|
||||||
|
$('app').classList.toggle('presenta', presenta);
|
||||||
|
setProp($('bocce-controls'), 'hidden', !mg || cards || presenta);
|
||||||
setProp($('bocce-controls'), 'inert', !running);
|
setProp($('bocce-controls'), 'inert', !running);
|
||||||
setProp($('briscola-controls'), 'hidden', def?.type !== 'briscola');
|
setProp($('briscola-controls'), 'hidden', def?.type !== 'briscola');
|
||||||
setProp($('briscola-controls'), 'inert', !running);
|
setProp($('briscola-controls'), 'inert', !running);
|
||||||
@@ -1000,7 +1015,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' }[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' }[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;
|
||||||
@@ -1011,25 +1026,30 @@ function updateUi() {
|
|||||||
// Il GP di Fortezza: niente forza, e i due pulsanti di mira diventano lo sterzo (si tengono premuti).
|
// Il GP di Fortezza: niente forza, e i due pulsanti di mira diventano lo sterzo (si tengono premuti).
|
||||||
// Nel GP di Fortezza il cursore della forza diventa la sensibilità dello sterzo col dito (ricordata).
|
// Nel GP di Fortezza il cursore della forza diventa la sensibilità dello sterzo col dito (ricordata).
|
||||||
const powerLabel = $('bocce-power').closest('label');
|
const powerLabel = $('bocce-power').closest('label');
|
||||||
// Ai rinquarti la forza è sempre piena: il cursore non serve.
|
// Ai rinquarti la forza è sempre piena: il cursore non serve. Nella guida semplice del GP (alla bona e seria) si
|
||||||
setProp(powerLabel, 'hidden', def.type === 'rinquarto');
|
// usa solo il gas: niente sensibilità, sterzo, freno né riquadro dello swipe.
|
||||||
const powerName = def.type === 'gara' ? 'Sensibilità ' : 'Forza ';
|
const easy = def.type === 'gara' && gara.simple(c);
|
||||||
|
setProp(powerLabel, 'hidden', def.type === 'rinquarto' || easy);
|
||||||
|
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy);
|
||||||
|
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');
|
||||||
setProp($('gara-pad'), 'hidden', def.type !== 'gara');
|
setProp($('gara-pad'), 'hidden', def.type !== 'gara' || easy);
|
||||||
if (def.type === 'gara' && c.sensLoaded === undefined) {
|
if (def.type === 'gara' && c.sensLoaded === undefined) {
|
||||||
c.sensLoaded = true;
|
c.sensLoaded = true;
|
||||||
try { const v = localStorage.getItem(GARA_SENS); if (v !== null) gara.setPower(c, Number(v) / 100); } catch { /* si resta al valore di partenza */ }
|
try { const v = localStorage.getItem(GARA_SENS); if (v !== null) gara.setPower(c, Number(v) / 100); } catch { /* si resta al valore di partenza */ }
|
||||||
}
|
}
|
||||||
setText($('bocce-left'), def.type === 'gara' ? '← Sterza' : '← Mira');
|
setText($('bocce-left'), def.type === 'gara' ? '← Sterza' : '← Mira');
|
||||||
setText($('bocce-right'), def.type === 'gara' ? 'Sterza →' : 'Mira →');
|
setText($('bocce-right'), def.type === 'gara' ? 'Sterza →' : 'Mira →');
|
||||||
setProp($('gara-brake'), 'hidden', def.type !== 'gara');
|
setProp($('gara-brake'), 'hidden', def.type !== 'gara' || easy);
|
||||||
|
$('bocce-throw').classList.toggle('hold', easy);
|
||||||
setProp($('gara-brake'), 'disabled', !(mg.canAdjust?.(c) ?? false) || !!game.speech);
|
setProp($('gara-brake'), 'disabled', !(mg.canAdjust?.(c) ?? false) || !!game.speech);
|
||||||
setText($('bocce-throw'), mg.button?.(c) ?? (c.step === 'result' ? 'Finita' : c.step === 'rolling' ? 'In movimento…' : c.current === 'cpu' ? `Tira ${def.names.cpu}…` : def.type === 'bocce' ? 'Lancia' : 'Tira'));
|
setText($('bocce-throw'), mg.button?.(c) ?? (c.step === 'result' ? 'Finita' : c.step === 'rolling' ? 'In movimento…' : c.current === 'cpu' ? `Tira ${def.names.cpu}…` : def.type === 'bocce' ? 'Lancia' : 'Tira'));
|
||||||
setProp($('bocce-help'), 'hidden', def.type !== 'bocce');
|
setProp($('bocce-help'), 'hidden', def.type !== 'bocce');
|
||||||
setProp($('biliardo-help'), 'hidden', def.type !== 'biliardo');
|
setProp($('biliardo-help'), 'hidden', def.type !== 'biliardo');
|
||||||
setProp($('rinquarto-help'), 'hidden', def.type !== 'rinquarto');
|
setProp($('rinquarto-help'), 'hidden', def.type !== 'rinquarto');
|
||||||
setProp($('micro-help'), 'hidden', def.type !== 'micro');
|
setProp($('micro-help'), 'hidden', def.type !== 'micro');
|
||||||
|
setProp($('cerbottana-help'), 'hidden', def.type !== 'cerbottana');
|
||||||
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');
|
||||||
@@ -1047,7 +1067,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'].includes(story.closeups[c.id].type));
|
$('stage').classList.toggle('biliardo', !!c && ['biliardo', 'rinquarto', 'gara', 'micro', 'cerbottana'].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.
|
||||||
@@ -1082,7 +1102,7 @@ function frame(now) {
|
|||||||
ambience.update(soundscapeFor(game?.state.scene, game?.closeup?.id), game?.phase()?.id, dt);
|
ambience.update(soundscapeFor(game?.state.scene, game?.closeup?.id), game?.phase()?.id, dt);
|
||||||
// A gioco fermo (titolo, pausa, fine) la scena non cambia: un ultimo disegno e poi niente, per la batteria del
|
// A gioco fermo (titolo, pausa, fine) la scena non cambia: un ultimo disegno e poi niente, per la batteria del
|
||||||
// telefono. Si ridisegna solo se nel frattempo arriva lo sfondo che mancava.
|
// telefono. Si ridisegna solo se nel frattempo arriva lo sfondo che mancava.
|
||||||
const still = game && !running && `${game.state.scene}|${hasBackground(game.scene.background)}`;
|
const still = game && !running && `${game.state.scene}|${hasBackground(game.backdrop())}`;
|
||||||
if (game && (!still || still !== drawnStill)) {
|
if (game && (!still || still !== drawnStill)) {
|
||||||
if (running) game.update(dt);
|
if (running) game.update(dt);
|
||||||
render(ctx, game, { hover: debug && hoverTarget ? game.hotspot(hoverTarget) : null, spots: running && now < spotsUntil, debug: debugView() });
|
render(ctx, game, { hover: debug && hoverTarget ? game.hotspot(hoverTarget) : null, spots: running && now < spotsUntil, debug: debugView() });
|
||||||
|
|||||||
@@ -233,13 +233,23 @@ export const story = {
|
|||||||
quit: [{ say: 'Questo me lo tengo per la prossima.' }],
|
quit: [{ say: 'Questo me lo tengo per la prossima.' }],
|
||||||
draw: [{ say: 'Visto? Geometrie celestiali. Si riprova quando vuoi.', who: 'bertovo', pose: 'explain' }],
|
draw: [{ say: 'Visto? Geometrie celestiali. Si riprova quando vuoi.', who: 'bertovo', pose: 'explain' }],
|
||||||
},
|
},
|
||||||
|
// La cerbottana con lo stucco verde di Pierin di Frilli, contro il muro della piazza (Andrea, 29 ottobre).
|
||||||
|
// [DA ANDREA: le battute sono provvisorie]
|
||||||
|
cerbottana: {
|
||||||
|
type: 'cerbottana',
|
||||||
|
picture: 'assets/closeups/muro-cerbottana.png',
|
||||||
|
names: { player: 'Andrea' },
|
||||||
|
win: [{ say: 'Tutti sul muro, verdi in fila. Lo stucco di Pierino un tradisce mai.' }, { set: 'cerbottana_vinta' }],
|
||||||
|
lose: [{ say: 'Finito lo stucco. Domani si ripassa da Pierino.' }],
|
||||||
|
quit: [{ say: 'Il muro resta lì. E l\'odore dello stucco, sulle dita, pure.' }],
|
||||||
|
draw: [],
|
||||||
|
},
|
||||||
// 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: {
|
||||||
type: 'micro',
|
type: 'micro', music: 'assets/audio/macchinine.mp3', // di Andrea (Suno, 29 ottobre)
|
||||||
picture: 'assets/closeups/micro-parcheggio.png',
|
picture: 'assets/closeups/micro-parcheggio.png',
|
||||||
names: { player: 'Andrea', cpu: 'Chiaru' },
|
names: { player: 'Andrea', cpu: 'Chiaru' },
|
||||||
cars: { player: 'assets/closeups/micro/rossa.png', cpu: 'assets/closeups/micro/blu.png' },
|
|
||||||
win: [{ say: 'Hai vinto te. Ma la mia aveva le ruote piene di gesso, sia chiaro.', who: 'chiaru', pose: 'shrug' }, { set: 'micro_vinto' }],
|
win: [{ say: 'Hai vinto te. Ma la mia aveva le ruote piene di gesso, sia chiaro.', who: 'chiaru', pose: 'shrug' }, { set: 'micro_vinto' }],
|
||||||
lose: [{ say: 'Visto? La blu non perde mai. Mai.', who: 'chiaru', pose: 'cheer' }],
|
lose: [{ say: 'Visto? La blu non perde mai. Mai.', who: 'chiaru', pose: 'cheer' }],
|
||||||
quit: [{ say: 'Ti ritiri? Allora la vinco a tavolino.', who: 'chiaru', pose: 'laugh' }],
|
quit: [{ say: 'Ti ritiri? Allora la vinco a tavolino.', who: 'chiaru', pose: 'laugh' }],
|
||||||
@@ -313,6 +323,8 @@ export const story = {
|
|||||||
tozzino: { name: 'Il Tozzino', color: '#c8f5b0', sprite: 'assets/characters/tozzino.json' },
|
tozzino: { name: 'Il Tozzino', color: '#c8f5b0', sprite: 'assets/characters/tozzino.json' },
|
||||||
// Il Chiaru (Matteo Chiarusi, consenso dato ad Andrea il 29 ottobre), alla pista col gesso di Cardetole: sprite di
|
// Il Chiaru (Matteo Chiarusi, consenso dato ad Andrea il 29 ottobre), alla pista col gesso di Cardetole: sprite di
|
||||||
// Codex dalla foto di Andrea.
|
// Codex dalla foto di Andrea.
|
||||||
|
// Lo zio Mario di Andrea (sprite di Codex dalla foto di Andrea, 29 ottobre), alla Ghiacciomobile nel muro.
|
||||||
|
zio_mario: { name: 'Lo zio Mario', color: '#cfe3ff', sprite: 'assets/characters/zio_mario.json' },
|
||||||
chiaru: { name: 'Il Chiaru', color: '#ffd6a0', sprite: 'assets/characters/chiaru.json' },
|
chiaru: { name: 'Il Chiaru', color: '#ffd6a0', sprite: 'assets/characters/chiaru.json' },
|
||||||
barondeduke: { name: 'Il BaronDeDuke', color: '#e0c090', sprite: 'assets/characters/barondeduke.json' },
|
barondeduke: { name: 'Il BaronDeDuke', color: '#e0c090', sprite: 'assets/characters/barondeduke.json' },
|
||||||
mariano: { name: 'Mariano', color: '#ffb0a3', sprite: 'assets/characters/mariano.json' },
|
mariano: { name: 'Mariano', color: '#ffb0a3', sprite: 'assets/characters/mariano.json' },
|
||||||
@@ -504,6 +516,17 @@ export const story = {
|
|||||||
look: [{ say: 'La strada che porta al Fiocco di Neve. Già sento il profumo di cialda.' }],
|
look: [{ say: 'La strada che porta al Fiocco di Neve. Già sento il profumo di cialda.' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// La bottega di Pierin di Frilli sotto la tenda rossa: da ragazzi ci si comprava lo stucco verde per la
|
||||||
|
// cerbottana (Andrea, 29 ottobre). [DA ANDREA: com'era dentro, e cosa diceva Pierino]
|
||||||
|
id: 'pierin', name: 'la bottega di Pierin di Frilli', box: [598, 176, 42, 92], at: [612, 306], face: 'right', default: 'talk',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'La bottega di Pierin di Frilli, sotto la tenda rossa. Da ragazzi ci si comprava lo stucco per la cerbottana.' }],
|
||||||
|
talk: [{ dialogue: 'pierin' }],
|
||||||
|
open: [{ dialogue: 'pierin' }],
|
||||||
|
use: [{ dialogue: 'pierin' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// Il Circolo 4 Maggio: il portone ad arco del palazzo arancione porta nella sala dei biliardi.
|
// Il Circolo 4 Maggio: il portone ad arco del palazzo arancione porta nella sala dei biliardi.
|
||||||
id: 'circolo', name: 'Circolo 4 Maggio', box: [188, 190, 28, 58], at: [202, 252], face: 'up',
|
id: 'circolo', name: 'Circolo 4 Maggio', box: [188, 190, 28, 58], at: [202, 252], face: 'up',
|
||||||
@@ -2117,23 +2140,11 @@ export const story = {
|
|||||||
entry: [320, 330],
|
entry: [320, 330],
|
||||||
// L'Arcu davanti al circolino, accanto alla fontanella (sprite di Codex, 25 ottobre).
|
// L'Arcu davanti al circolino, accanto alla fontanella (sprite di Codex, 25 ottobre).
|
||||||
speakers: { arcu: [418, 214] },
|
speakers: { arcu: [418, 214] },
|
||||||
|
// La Ghiacciomobile non è più qui: sta nel muro di Via Liguri Magelli (Andrea, 29 ottobre).
|
||||||
people: [
|
people: [
|
||||||
{ who: 'arcu', at: [418, 292], facing: -1, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.2 },
|
{ who: 'arcu', at: [418, 292], facing: -1, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.2 },
|
||||||
// Oggetti statici: ordinati per profondità come la macchina del tempo, senza voce.
|
|
||||||
// Con la depth della scena misura circa 160 px; resta sopra il bordo di walk. (Il Betino dell'Arcu sta in
|
|
||||||
// cima alla salita di Via Liguri Magelli, Andrea 28 ottobre.)
|
|
||||||
{ who: 'ghiacciomobile', at: [176, 286], size: 0.986, idle: 'idle', talk: [] },
|
|
||||||
],
|
],
|
||||||
hotspots: [
|
hotspots: [
|
||||||
{
|
|
||||||
id: 'ghiacciomobile', name: 'la Ghiacciomobile', box: [96, 215, 160, 71], at: [240, 308], face: 'up', default: 'look',
|
|
||||||
// La Corsa nera degli amici (sprite di Codex, 26 ottobre). Parlandole, Andrea ricorda; lei non risponde.
|
|
||||||
actions: {
|
|
||||||
look: [{ say: 'La Ghiacciomobile nera. Hai visto cose che noi poveri umani non potremmo neppur lontanamente immaginare.' }],
|
|
||||||
talk: [{ dialogue: 'ghiacciomobile' }],
|
|
||||||
use: [{ say: 'Con questo sole, di ghiaccio c\'è rimasto giusto il nome. Vo a piedi.' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'circolo', name: 'il circolino', box: [100, 0, 380, 205], at: [300, 290], face: 'up',
|
id: 'circolo', name: 'il circolino', box: [100, 0, 380, 205], at: [300, 290], face: 'up',
|
||||||
actions: {
|
actions: {
|
||||||
@@ -2711,14 +2722,21 @@ export const story = {
|
|||||||
name: 'Via Liguri Magelli',
|
name: 'Via Liguri Magelli',
|
||||||
map: [76, 97],
|
map: [76, 97],
|
||||||
background: 'assets/scenes/via-liguri-magelli.png',
|
background: 'assets/scenes/via-liguri-magelli.png',
|
||||||
|
// All'inizio la Ghiacciomobile è dentro il muro giallo, coi mattoni rotti (Codex, 29 ottobre); dal tramonto
|
||||||
|
// (le impronte del Regolo) il muro è rifatto e c'è la sua targa (Andrea, 29 ottobre).
|
||||||
|
backdrops: [{ when: '!indizio_regolo', src: 'assets/scenes/via-liguri-magelli-muro.png' }],
|
||||||
// La strada gira intorno all'Audi nera parcheggiata davanti al Trappola.
|
// La strada gira intorno all'Audi nera parcheggiata davanti al Trappola.
|
||||||
walk: [[0, 360], [0, 320], [140, 262], [226, 198], [272, 198], [300, 242], [370, 246], [400, 252], [580, 318], [640, 330], [640, 360]],
|
walk: [[0, 360], [0, 320], [140, 262], [226, 198], [272, 198], [300, 242], [370, 246], [400, 252], [580, 318], [640, 330], [640, 360]],
|
||||||
depth: [[198, 0.3], [360, 1.1]],
|
// Le persone in proporzione con la Ghiacciomobile e l'Audi (Andrea, 29 ottobre: erano troppo piccole).
|
||||||
|
depth: [[198, 0.4], [360, 2.4]],
|
||||||
entry: [320, 340],
|
entry: [320, 340],
|
||||||
speakers: { arcu: [374, 176] },
|
speakers: { arcu: [374, 176], zio_mario: [440, 196] },
|
||||||
people: [
|
people: [
|
||||||
// L'Arcu sotto la tenda del Trappola: due minuti fa era a Campomigliaio (Andrea, 28 ottobre).
|
// L'Arcu sotto la tenda del Trappola: due minuti fa era a Campomigliaio (Andrea, 28 ottobre).
|
||||||
{ who: 'arcu', at: [374, 248], facing: -1, size: 1.3, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.2 },
|
{ who: 'arcu', at: [374, 248], facing: -1, size: 0.75, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.2 },
|
||||||
|
// Lo zio Mario davanti alla Ghiacciomobile nel muro, che si gratta la testa (sprite di Codex dalla sua foto,
|
||||||
|
// 29 ottobre). Solo finché la macchina è lì. [DA ANDREA: le sue battute]
|
||||||
|
{ who: 'zio_mario', when: '!indizio_regolo', at: [440, 330], facing: 1, idle: 'think', talk: ['talk', 'explain', 'point'] },
|
||||||
],
|
],
|
||||||
hotspots: [
|
hotspots: [
|
||||||
{
|
{
|
||||||
@@ -2746,11 +2764,21 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'targa', name: 'targa della Ghiacciomobile', box: [570, 130, 62, 78], at: [565, 336], face: 'right', default: 'look',
|
id: 'targa', name: 'targa della Ghiacciomobile', box: [570, 130, 62, 78], at: [565, 336], face: 'right', default: 'look', when: 'indizio_regolo',
|
||||||
actions: {
|
actions: {
|
||||||
look: [{ say: '«Qui riposa la Ghiacciomobile, che si è schiantata lì.»' }, { say: 'C\'è anche la fotina. Pareva sempre nuova.' }],
|
look: [{ say: '«Qui riposa la Ghiacciomobile, che si è schiantata lì.»' }, { say: 'C\'è anche la fotina. Pareva sempre nuova.' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// La Ghiacciomobile nel muro (prima del tramonto). Le battute erano del circolino (Andrea, 26 ottobre);
|
||||||
|
// [DA ANDREA] quella dello schianto. Parlandole, Andrea ricorda; lei non risponde.
|
||||||
|
id: 'ghiacciomobile', name: 'la Ghiacciomobile', box: [462, 192, 140, 92], at: [440, 300], face: 'right', default: 'look', when: '!indizio_regolo',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'La Ghiacciomobile nera, mezza dentro il muro. Hai visto cose che noi poveri umani non potremmo neppur lontanamente immaginare.' }],
|
||||||
|
talk: [{ dialogue: 'ghiacciomobile' }],
|
||||||
|
use: [{ say: 'Per tirarla fuori di lì ci vuole altro che le mie braccia.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// La prima battuta è di Andrea (dal circolino, 26 ottobre).
|
// La prima battuta è di Andrea (dal circolino, 26 ottobre).
|
||||||
id: 'betino', name: 'il Betino dell\'Arcu', box: [232, 172, 40, 24], at: [250, 206], face: 'up', default: 'look',
|
id: 'betino', name: 'il Betino dell\'Arcu', box: [232, 172, 40, 24], at: [250, 206], face: 'up', default: 'look',
|
||||||
@@ -2765,7 +2793,16 @@ export const story = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Il campo del Giannini è laggiù a destra, oltre la siepe (Andrea, 28 ottobre).
|
// Il campo del Giannini è laggiù a destra, oltre la siepe (Andrea, 28 ottobre).
|
||||||
id: 'al_giannini', name: 'verso il campo del Giannini', box: [430, 150, 132, 100], at: [520, 306], exit: { scene: 'campo_giannini', at: [60, 250], face: 'right' },
|
id: 'al_giannini', name: 'verso il campo del Giannini', box: [404, 150, 58, 50], at: [520, 306], exit: { scene: 'campo_giannini', at: [60, 250], face: 'right' },
|
||||||
|
},
|
||||||
|
// In fondo all'elenco, così sta sopra all'uscita e alla siepe.
|
||||||
|
{
|
||||||
|
id: 'zio_mario', name: 'lo zio Mario', box: [418, 206, 46, 124], at: [362, 334], face: 'right', default: 'look', when: '!indizio_regolo',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Lo zio Mario. Guarda il muro, guarda la macchina, e si gratta la testa sotto il berretto.' }],
|
||||||
|
// Il primo scambio con lo zio (Andrea, 29 ottobre).
|
||||||
|
talk: [{ say: 'Zio, hai problemi?' }, { say: 'I problemi io gli avevo quando andavo a scuola!', who: 'zio_mario', pose: 'laugh' }],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -2788,11 +2825,10 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'casa', name: 'casa di Andrea', box: [330, 20, 220, 212], at: [470, 290], face: 'up',
|
// Si sale in terrazza (Andrea, 29 ottobre).
|
||||||
|
id: 'casa', name: 'casa di Andrea, su in terrazza', box: [330, 20, 220, 212], at: [470, 290], face: 'up', exit: { scene: 'terrazza', at: [300, 330], face: 'up' },
|
||||||
actions: {
|
actions: {
|
||||||
look: [{ say: 'Casa mia. Secondo piano: quella con la tapparella a metà è la camera di mia sorella, e a destra quella dei miei.' }],
|
look: [{ say: 'Casa mia. Secondo piano: quella con la tapparella a metà è la camera di mia sorella, e a destra quella dei miei.' }],
|
||||||
open: [{ say: 'Se salgo, mi mettono ad apparecchiare. Prima il palio.' }],
|
|
||||||
walk: [{ say: 'Se salgo, mi mettono ad apparecchiare. Prima il palio.' }],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -2833,6 +2869,54 @@ export const story = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// Il terrazzo di casa a Cardetole: la vista dalla finestra di Andrea (foto sua, 29 ottobre), il paese che sale e
|
||||||
|
// lassù la Fortezza. Codex l'ha dipinta dalla foto e ci ha messo il terrazzo davanti. [DA ANDREA: le battute]
|
||||||
|
terrazza: {
|
||||||
|
name: 'Il terrazzo di casa',
|
||||||
|
map: [92, 81],
|
||||||
|
background: 'assets/scenes/terrazza-cardetole.png',
|
||||||
|
walk: [[120, 298], [500, 298], [560, 330], [604, 360], [60, 360], [100, 322]],
|
||||||
|
depth: [[290, 1.8], [360, 2.3]],
|
||||||
|
entry: [300, 330],
|
||||||
|
enter: [{ if: '!terrazza_vista', then: [{ say: 'La vista di casa. Da qui si vede mezzo paese, e lassù la Fortezza.' }, { set: 'terrazza_vista' }] }],
|
||||||
|
hotspots: [
|
||||||
|
{
|
||||||
|
id: 'fortezza', name: 'la Fortezza, lassù', box: [176, 44, 52, 30], at: [220, 304], face: 'up',
|
||||||
|
actions: { look: [{ say: 'Lassù in cima, fra gli alberi, la Fortezza. Da qui pare piccina.' }] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'paese', name: 'il paese', box: [196, 96, 220, 60], at: [300, 304], face: 'up',
|
||||||
|
actions: { look: [{ say: 'Il paese che sale verso la collina, coi tetti uno sull\'altro e il campanile in mezzo.' }] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'palazzo', name: 'il palazzo accanto', box: [504, 54, 136, 140], at: [470, 304], face: 'up',
|
||||||
|
actions: { look: [{ say: 'Il palazzo accanto, di mattoni come il nostro.' }] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gerani', name: 'gerani', box: [500, 226, 100, 94], at: [480, 330], face: 'right',
|
||||||
|
actions: { look: [{ say: 'I gerani. Guai a chi li tocca.' }], take: [{ say: 'Se ne strappo uno, il palio lo guardo dal letto.' }] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edera', name: 'edera', box: [54, 176, 136, 134], at: [150, 330], face: 'left',
|
||||||
|
actions: { look: [{ say: 'L\'edera nel vaso: cresce da sola, e si arrampica dove le pare.' }] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sedia', name: 'sedia di plastica', box: [592, 208, 48, 116], at: [560, 340], face: 'right',
|
||||||
|
actions: { look: [{ say: 'Una sedia di plastica, piegata.' }], use: [{ say: 'Sedermi qui a guardare il paese? Prima il palio.' }] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'in_casa', name: 'dentro casa', box: [0, 0, 54, 340], at: [112, 340], face: 'left',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'La porta-finestra di casa, con la tapparella a metà.' }],
|
||||||
|
walk: [{ say: 'Se rientro, mi mettono ad apparecchiare. Prima il palio.' }],
|
||||||
|
open: [{ say: 'Se rientro, mi mettono ad apparecchiare. Prima il palio.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'giu_cardetole', name: 'giù, sotto casa', box: [120, 340, 400, 20], at: [300, 352], exit: { scene: 'cardetole', at: [470, 290], face: 'down' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
// L'ingresso della scuola, giù per la strada dalla salita di Villa Adami: dal cancello alle porte a vetri.
|
// L'ingresso della scuola, giù per la strada dalla salita di Villa Adami: dal cancello alle porte a vetri.
|
||||||
scuola: {
|
scuola: {
|
||||||
name: 'La scuola',
|
name: 'La scuola',
|
||||||
@@ -3249,12 +3333,34 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Da Pierin di Frilli: la cerbottana e lo stucco verde, poi il muro della piazza (Andrea, 29 ottobre).
|
||||||
|
pierin: {
|
||||||
|
nodes: {
|
||||||
|
start: {
|
||||||
|
options: [
|
||||||
|
{ text: 'Una cerbottana e un po\' di stucco, Pierino!', next: 'livello', then: [
|
||||||
|
{ say: 'Lo stucco verde di Pierino: quell\'odore non te lo levi più dalle dita.' },
|
||||||
|
{ say: 'Ora un muro. Quello della piazza va benissimo.' },
|
||||||
|
] },
|
||||||
|
{ text: 'Niente, grazie: guardavo.', next: 'end' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
livello: {
|
||||||
|
options: [
|
||||||
|
{ text: 'Alla bona.', next: 'end', then: [{ closeup: 'cerbottana', level: 'bona' }] },
|
||||||
|
{ text: 'Seria.', next: 'end', then: [{ closeup: 'cerbottana', level: 'seria' }] },
|
||||||
|
{ text: 'A fassi male.', next: 'end', then: [{ closeup: 'cerbottana', level: 'male' }] },
|
||||||
|
{ text: 'No, dopo.', next: 'end' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
// 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]
|
||||||
chiaru: {
|
chiaru: {
|
||||||
nodes: {
|
nodes: {
|
||||||
start: {
|
start: {
|
||||||
options: [
|
options: [
|
||||||
{ text: 'Si corre?', next: 'livello', then: [{ say: 'Un giro, una spinta a testa, prima te. Tutta fuori dal gesso, si torna indietro. Come la vuoi?', who: 'chiaru', pose: 'explain' }] },
|
{ text: 'Si fa una gara alle macchinine?!?!', next: 'livello', then: [{ say: 'Un giro, una spinta a testa, prima te. Tutta fuori dal gesso, si torna indietro. Come la vuoi?', who: 'chiaru', pose: 'explain' }] },
|
||||||
{ text: 'Che pista hai disegnato?', then: [
|
{ text: 'Che pista hai disegnato?', then: [
|
||||||
{ say: 'Rettilineo, curvone, rettilineo, e in fondo la esse. Nella esse chi ha fretta esce.', who: 'chiaru', pose: 'explain' },
|
{ say: 'Rettilineo, curvone, rettilineo, e in fondo la esse. Nella esse chi ha fretta esce.', who: 'chiaru', pose: 'explain' },
|
||||||
{ say: 'Il tombino l\'ho lasciato in pista: porta fortuna.', who: 'chiaru', pose: 'talk' },
|
{ say: 'Il tombino l\'ho lasciato in pista: porta fortuna.', who: 'chiaru', pose: 'talk' },
|
||||||
@@ -3265,7 +3371,8 @@ export const story = {
|
|||||||
{ say: 'Chi taglia la strada: tagliè. Chi si ribalta: cappottè. E chi esce tutto dal gesso, fuori.', who: 'chiaru', pose: 'talk' },
|
{ say: 'Chi taglia la strada: tagliè. Chi si ribalta: cappottè. E chi esce tutto dal gesso, fuori.', who: 'chiaru', pose: 'talk' },
|
||||||
{ say: 'In tutti i casi, si torna dov\'eri prima.', who: 'chiaru', pose: 'shrug' },
|
{ say: 'In tutti i casi, si torna dov\'eri prima.', who: 'chiaru', pose: 'shrug' },
|
||||||
] },
|
] },
|
||||||
{ text: 'Dopo, Chiaru.', next: 'end', then: [{ say: 'La pista è qui. Finché non piove.', who: 'chiaru', pose: 'stand' }] },
|
// Il saluto (Andrea, 29 ottobre).
|
||||||
|
{ text: 'Vado a fare un girino alle case rosse.', next: 'end', then: [{ say: 'Vorrai dire un giochino!', who: 'chiaru', pose: 'laugh' }, { say: 'Ciao Mattè.' }] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
livello: {
|
livello: {
|
||||||
@@ -3280,11 +3387,12 @@ export const story = {
|
|||||||
},
|
},
|
||||||
// Trappizzio Berti al tavolo di mezzo: le biglie nascoste e «Oh tirami questo rinquarto!?» (Andrea, 28 ottobre).
|
// Trappizzio Berti al tavolo di mezzo: le biglie nascoste e «Oh tirami questo rinquarto!?» (Andrea, 28 ottobre).
|
||||||
// [DA ANDREA: le battute sono provvisorie, tranne la sua sfida]
|
// [DA ANDREA: le battute sono provvisorie, tranne la sua sfida]
|
||||||
|
// Andrea, parlandogli, lo chiama Fabri (29 ottobre).
|
||||||
bertovo: {
|
bertovo: {
|
||||||
nodes: {
|
nodes: {
|
||||||
start: {
|
start: {
|
||||||
options: [
|
options: [
|
||||||
{ text: 'Trappizzio Berti! Che fai, giochi da solo?', then: [
|
{ text: 'Fabri! Che fai, giochi da solo?', then: [
|
||||||
{ say: 'Da solo? Io gioco contro le sponde. E vinco quasi sempre.', who: 'bertovo', pose: 'explain' },
|
{ say: 'Da solo? Io gioco contro le sponde. E vinco quasi sempre.', who: 'bertovo', pose: 'explain' },
|
||||||
] },
|
] },
|
||||||
{ text: 'Come fai a prendere le biglie dove nessuno ci arriva?', then: [
|
{ text: 'Come fai a prendere le biglie dove nessuno ci arriva?', then: [
|
||||||
@@ -3299,7 +3407,7 @@ export const story = {
|
|||||||
{ text: 'Si fa una sfida a rinquarti?', next: 'sfida', then: [
|
{ text: 'Si fa una sfida a rinquarti?', next: 'sfida', then: [
|
||||||
{ say: 'Le biglie le metto io, nascoste per bene. Tiri te, poi tiro io. Oh tirami questo rinquarto!?', who: 'bertovo', pose: 'explain' },
|
{ say: 'Le biglie le metto io, nascoste per bene. Tiri te, poi tiro io. Oh tirami questo rinquarto!?', who: 'bertovo', pose: 'explain' },
|
||||||
] },
|
] },
|
||||||
{ text: 'Ci si vede, Trappizzio Berti.', next: 'end', then: [{ say: 'Occhio alle sponde, fuori.', who: 'bertovo', pose: 'shrug' }] },
|
{ text: 'Ci si vede, Fabri.', next: 'end', then: [{ say: 'Occhio alle sponde, fuori.', who: 'bertovo', pose: 'shrug' }] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sfida: {
|
sfida: {
|
||||||
|
|||||||
@@ -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.39';
|
export const VERSION = '0.1.47';
|
||||||
|
|
||||||
// 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).
|
||||||
|
|||||||
@@ -56,18 +56,22 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
|
|||||||
text-align: center; color: #c5c6b6; border: 2px dashed #897b57; border-radius: 10px; background: #1d2320; }
|
text-align: center; color: #c5c6b6; border: 2px dashed #897b57; border-radius: 10px; background: #1d2320; }
|
||||||
#gara-pad.active { border-style: solid; color: #f3dcac; }
|
#gara-pad.active { border-style: solid; color: #f3dcac; }
|
||||||
#stage, #bocce-controls, #gara-pad { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
|
#stage, #bocce-controls, #gara-pad { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
|
||||||
#bocce-throw { background: #ead09a; color: #202b25; min-width: 100px; }
|
#bocce-throw { background: #ead09a; color: #202b25; min-width: 100px; touch-action: none; }
|
||||||
|
/* La presentazione dei piloti del GP: la scena da sola, i comandi arrivano col semaforo. */
|
||||||
|
#app.presenta #side { display: none; }
|
||||||
|
/* La guida semplice del GP: il Gas si tiene premuto col pollice, più alto. */
|
||||||
|
#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 { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-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 { 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 summary { cursor: pointer; padding: 6px 0; }
|
||||||
#bocce-help p, #biliardo-help p, #basket-help p, #pesca-help p, #gara-help p, #rinquarto-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 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 { font-size: 9px; }
|
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-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); }
|
||||||
@@ -318,6 +322,8 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
|
|||||||
#app { width: 100vw; flex-direction: row; }
|
#app { width: 100vw; flex-direction: row; }
|
||||||
#stage { flex: none; width: min(calc(100dvh * 16 / 9), 68vw); align-self: flex-start; }
|
#stage { flex: none; width: min(calc(100dvh * 16 / 9), 68vw); align-self: flex-start; }
|
||||||
#stage.biliardo { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); }
|
#stage.biliardo { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); }
|
||||||
|
#app.presenta { justify-content: center; }
|
||||||
|
#app.presenta #stage.biliardo { width: min(calc((100dvh - 44px) * 16 / 9), 100vw); }
|
||||||
#stage.briscola { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); }
|
#stage.briscola { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); }
|
||||||
#briscola-controls, #tressette-controls, #scopa-controls { overflow-y: auto; min-height: 0; padding: 8px; font-size: 9px; }
|
#briscola-controls, #tressette-controls, #scopa-controls { overflow-y: auto; min-height: 0; padding: 8px; font-size: 9px; }
|
||||||
/* Nella colonna stretta accanto al tavolo: carte e segni su due colonne, parole che vanno a capo. */
|
/* Nella colonna stretta accanto al tavolo: carte e segni su due colonne, parole che vanno a capo. */
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// La cerbottana nel browser vero: dalla bottega di Pierin di Frilli al muro della piazza; mira, fiato e tiro col mouse
|
||||||
|
// e col dito. npm run browser:cerbottana — schermate in /tmp/regolo-cerbottana.
|
||||||
|
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-cerbottana';
|
||||||
|
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('piazza'); settle(game);
|
||||||
|
game.act('talk', 'pierin'); settle(game);
|
||||||
|
game.choose(game.options().findIndex(o => o.text.startsWith('Una cerbottana'))); settle(game);
|
||||||
|
game.choose(game.options().findIndex(o => o.text === 'Seria.')); settle(game);
|
||||||
|
});
|
||||||
|
await page.waitForSelector('#bocce-controls:not([hidden])');
|
||||||
|
await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/muro-cerbottana.png'));
|
||||||
|
assert.equal(await page.locator('#bocce-throw').textContent(), 'Spara');
|
||||||
|
assert.ok(await page.locator('#cerbottana-help').isVisible());
|
||||||
|
await page.waitForTimeout(1600);
|
||||||
|
const box = await page.locator('#scene').boundingBox(), P = (x, y) => [box.x + x / 640 * box.width, box.y + y / 360 * box.height];
|
||||||
|
if (mobile) await page.touchscreen.tap(...P(320, 118)); else { await page.mouse.move(...P(320, 118)); await page.mouse.click(...P(320, 118)); }
|
||||||
|
assert.equal(await page.evaluate(() => game.closeup.step), 'aim', 'un tocco mira soltanto');
|
||||||
|
await page.screenshot({ path: `${out}/${name}-mira.png` });
|
||||||
|
if (mobile) { await page.locator('#bocce-power').fill('70'); await page.locator('#bocce-throw').tap(); }
|
||||||
|
else { await page.mouse.down(); await page.waitForFunction(() => game.closeup.power >= 0.6); await page.mouse.up(); }
|
||||||
|
await page.waitForFunction(() => game.closeup.left === 11 && game.closeup.step === 'aim', null, { timeout: 10000 });
|
||||||
|
await page.screenshot({ path: `${out}/${name}-dopo.png` });
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
console.log(`ok: schermate in ${out}`);
|
||||||
|
} finally { await browser?.close(); server.kill(); }
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as m from '../src/engine/cerbottana.js';
|
||||||
|
import { drawCerbottana } from '../src/engine/cerbottana-render.js';
|
||||||
|
import { Game, MINIGAMES } from '../src/engine/game.js';
|
||||||
|
import { story } from '../src/story/story.js';
|
||||||
|
import { settle } from './helpers.mjs';
|
||||||
|
|
||||||
|
// Ferma il respiro (il mirino va dove si punta), spara col fiato dato e aspetta che il pallino arrivi.
|
||||||
|
function shot(c, x, y, power = m.BEST) {
|
||||||
|
c.skill = { ...c.skill, sway: 0 }; c.aim = { x, y };
|
||||||
|
assert.ok(m.setPower(c, power)); assert.ok(m.shoot(c));
|
||||||
|
for (let i = 0; i < 200 && c.step === 'flying'; i++) m.update(c, 1 / 60);
|
||||||
|
return c.line;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('col fiato giusto il pallino va dove si punta; poco fiato cade sotto, troppo va sopra; resta attaccato al muro', () => {
|
||||||
|
const c = m.newCerbottana(1, { level: 'seria' });
|
||||||
|
assert.match(shot(c, m.TARGET.x, m.TARGET.y), /10 punti/);
|
||||||
|
assert.equal(c.points, 10); assert.deepEqual(c.stuck.at(-1), { x: m.TARGET.x, y: m.TARGET.y });
|
||||||
|
for (let i = 0; i < 60; i++) m.update(c, 1 / 60);
|
||||||
|
assert.ok(m.landing(c, 0.5).y > m.TARGET.y + 40, 'poco fiato: sotto');
|
||||||
|
assert.ok(m.landing(c, 0.9).y < m.TARGET.y - 40, 'troppo fiato: sopra');
|
||||||
|
shot(c, 320, 200, 0.2);
|
||||||
|
assert.match(c.line, /corto/); assert.equal(c.stuck.length, 1, 'il pallino corto non resta sul muro');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('le lattine cadono, il barattolo appeso c\'è solo a seria e a fassi male; finito lo stucco si guarda il punteggio', () => {
|
||||||
|
const c = m.newCerbottana(2, { level: 'bona' });
|
||||||
|
const k = c.cans[0];
|
||||||
|
assert.match(shot(c, k.x, m.LEDGE - 8), /Lattina giù/);
|
||||||
|
assert.ok(k.down); assert.equal(c.points, m.CAN.points);
|
||||||
|
assert.equal(m.LEVELS.bona.swinger, false); assert.equal(m.LEVELS.seria.swinger, true);
|
||||||
|
// Il barattolo dondola mentre il pallino vola: si mira dove sarà all'arrivo.
|
||||||
|
const s = m.newCerbottana(3, { level: 'seria' }), p = m.swinger({ visual: { time: s.visual.time + m.FLIGHT + 1 / 120 } });
|
||||||
|
assert.match(shot(s, p.x, p.y + m.SWINGER.h / 2), /barattolo appeso/);
|
||||||
|
for (let i = 0; i < 60; i++) m.update(c, 1 / 60);
|
||||||
|
while (c.left > 0) { for (let i = 0; i < 60 && !m.canThrow(c); i++) m.update(c, 1 / 60); shot(c, m.TARGET.x, m.TARGET.y); }
|
||||||
|
for (let i = 0; i < 60; i++) m.update(c, 1 / 60);
|
||||||
|
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'player');
|
||||||
|
assert.equal(c.stuck.length, m.PELLETS - 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('i comandi: un tocco mira, tenere premuto prende fiato da zero, lasciare spara; troppo a fiato pieno si tossisce', () => {
|
||||||
|
const c = m.newCerbottana(4);
|
||||||
|
assert.ok(m.aimScreen(c, [200, 100])); assert.deepEqual(c.aim, { x: 200, y: 100 });
|
||||||
|
assert.ok(m.press(c, 1)); m.update(c, 0.05); assert.equal(m.release(c, 1), true); assert.equal(c.step, 'aim', 'il tocco breve non spara');
|
||||||
|
assert.ok(m.press(c, 1)); assert.equal(c.power, 0);
|
||||||
|
for (let i = 0; i < 40; i++) m.update(c, 1 / 60);
|
||||||
|
assert.ok(c.charging && c.power > 0.5 && c.power < 0.8);
|
||||||
|
assert.ok(m.release(c, 1)); assert.equal(c.step, 'flying'); assert.equal(c.left, m.PELLETS - 1);
|
||||||
|
const d = m.newCerbottana(5); m.press(d, 1);
|
||||||
|
for (let i = 0; i < 60 * (m.SWING + m.COUGH + 0.2); i++) m.update(d, 1 / 60);
|
||||||
|
assert.equal(d.holding, false); assert.ok(d.events.some(e => e.kind === 'cerbottana-tosse'));
|
||||||
|
for (let i = 0; i < 60; i++) m.update(d, 1 / 60);
|
||||||
|
assert.match(d.line, /Coff/); assert.equal(d.left, m.PELLETS - 1);
|
||||||
|
assert.ok(m.key(d, 'ArrowUp', true)); assert.equal(d.aim.y, m.TARGET.y - 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il disegno: muro, gesso, lattine, pallini; lo stato non cambia', () => {
|
||||||
|
const calls = [], ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => calls.push([k, ...args]) });
|
||||||
|
const c = m.newCerbottana(6, { level: 'seria' }), bg = {};
|
||||||
|
const draw = () => { const before = structuredClone(c); drawCerbottana(ctx, c, story.closeups.cerbottana, 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 CERBOTTANA'));
|
||||||
|
c.step = 'result'; c.result = { winner: 'player', points: 44, goal: 40 }; calls.length = 0; draw();
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'CENTRATO!'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('da Pierin di Frilli, in piazza: lo stucco, il livello e il muro', () => {
|
||||||
|
assert.ok(MINIGAMES.cerbottana);
|
||||||
|
const g = new Game(story); g.go('piazza'); settle(g);
|
||||||
|
const h = g.hotspot('pierin');
|
||||||
|
assert.equal(g.hotspotAt([h.box[0] + h.box[2] / 2, h.box[1] + h.box[3] / 2]).id, 'pierin');
|
||||||
|
g.act('talk', 'pierin'); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text.startsWith('Una cerbottana'))); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g);
|
||||||
|
assert.equal(g.closeup?.id, 'cerbottana'); assert.equal(g.closeup.level, 'male');
|
||||||
|
g.closeup.step = 'result'; g.closeup.result = { winner: 'player', points: 52, goal: 50 };
|
||||||
|
g.closeupClose(); settle(g);
|
||||||
|
assert.ok(g.state.flags.cerbottana_vinta);
|
||||||
|
});
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// Il GP di Fortezza nel browser vero: alla bona e seria la guida semplice (solo gas, lasciando si frena), a ginocchio
|
||||||
|
// in terra lo sterzo vero. npm run browser:gara — schermate in /tmp/regolo-gara.
|
||||||
|
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-gara';
|
||||||
|
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, level] of [
|
||||||
|
['touch', true, { width: 844, height: 390 }, 'seria'],
|
||||||
|
['portrait', true, { width: 390, height: 844 }, 'bona'],
|
||||||
|
['desktop', false, { width: 1280, height: 860 }, 'male'],
|
||||||
|
]) {
|
||||||
|
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 level => {
|
||||||
|
const { settle } = await import('/tests/helpers.mjs');
|
||||||
|
settle(game); game.run({ closeup: 'gara', level }); settle(game);
|
||||||
|
}, level);
|
||||||
|
// La presentazione dei piloti a tutto schermo: niente comandi finché non c'è il semaforo.
|
||||||
|
await page.waitForFunction(() => game.closeup?.step === 'intro');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
assert.equal(await page.locator('#bocce-controls').isVisible(), false, `${name}: comandi nascosti nella presentazione`);
|
||||||
|
assert.equal(await page.locator('#side').isVisible(), false, `${name}: la scena da sola`);
|
||||||
|
await page.screenshot({ path: `${out}/${name}-presentazione.png` });
|
||||||
|
// Salta la presentazione dei piloti e aspetta il via.
|
||||||
|
await page.evaluate(async () => { const g = await import('/src/engine/gara.js'); while (game.closeup.step === 'intro') g.press(game.closeup); });
|
||||||
|
await page.waitForFunction(() => game.closeup.step === 'race', null, { timeout: 15000 });
|
||||||
|
await page.waitForSelector('#bocce-controls:not([hidden])');
|
||||||
|
const easy = level !== 'male';
|
||||||
|
for (const id of ['#bocce-left', '#bocce-right', '#gara-brake', '#gara-pad'])
|
||||||
|
assert.equal(await page.locator(id).isVisible(), !easy, `${name}: ${id} ${easy ? 'nascosto' : 'visibile'}`);
|
||||||
|
assert.equal(await page.locator('#bocce-power').isVisible(), !easy, 'la sensibilità solo con lo sterzo vero');
|
||||||
|
// Nella guida semplice il pulsante Gas si tiene premuto (giù gas, su freno) e la scritta non cambia; con lo
|
||||||
|
// sterzo vero un tocco accende il gas e un altro lo spegne.
|
||||||
|
const gas = page.locator('#bocce-throw');
|
||||||
|
if (easy) await gas.dispatchEvent('pointerdown', { pointerId: 5, pointerType: mobile ? 'touch' : 'mouse', isPrimary: true });
|
||||||
|
else await gas.click();
|
||||||
|
await page.waitForFunction(() => game.closeup.riders.find(r => r.id === 'andrea').v > 40, null, { timeout: 15000 });
|
||||||
|
assert.equal(await gas.textContent(), easy ? 'Gas' : 'Molla');
|
||||||
|
await page.screenshot({ path: `${out}/${name}-${level}.png` });
|
||||||
|
if (easy) await gas.dispatchEvent('pointerup', { pointerId: 5, pointerType: mobile ? 'touch' : 'mouse', isPrimary: true });
|
||||||
|
else await gas.click();
|
||||||
|
const v = await page.evaluate(() => game.closeup.riders.find(r => r.id === 'andrea').v);
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
const after = await page.evaluate(() => game.closeup.riders.find(r => r.id === 'andrea').v);
|
||||||
|
assert.ok(after < v, `${name}: senza gas rallenta (${v.toFixed(0)} → ${after.toFixed(0)})`);
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
console.log(`ok: schermate in ${out}`);
|
||||||
|
} finally {
|
||||||
|
await browser?.close();
|
||||||
|
server.kill();
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { SWIPE, setPower, newGara, update, press, presenting, INTRO, release, cancel, shoot, key, steerButton, brakeButton, aimScreen, steering, status, button, standings, player, rider, limitAt, pointAt, TRACK, CURVES, PICKUPS, BRAKE, VERGE, LAPS, COUNTDOWN, RIDERS } from '../src/engine/gara.js';
|
import { SIMPLE, simple, simpleLimit, mustLift, PREAVVISO, SWIPE, setPower, newGara, update, press, presenting, INTRO, release, cancel, shoot, key, steerButton, brakeButton, aimScreen, steering, status, button, standings, player, rider, limitAt, pointAt, TRACK, CURVES, PICKUPS, BRAKE, VERGE, LAPS, COUNTDOWN, RIDERS } from '../src/engine/gara.js';
|
||||||
import { piloti } from '../src/story/piloti.js';
|
import { piloti } from '../src/story/piloti.js';
|
||||||
import { story } from '../src/story/story.js';
|
import { story } from '../src/story/story.js';
|
||||||
import { Game } from '../src/engine/game.js';
|
import { Game } from '../src/engine/game.js';
|
||||||
@@ -58,7 +58,7 @@ test('i piloti sono dati: motore da 1 a 6 e testa da 1 a 5 danno velocità, acce
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('il semaforo, poi il gas: tenendo premuto si va, lasciando si rallenta, il freno vince sul gas', () => {
|
test('il semaforo, poi il gas: tenendo premuto si va, lasciando si rallenta, il freno vince sul gas', () => {
|
||||||
const c = newGara(3, { level: 'seria', riders: RIDERS });
|
const c = newGara(3, { level: 'seria', riders: RIDERS, simple: false });
|
||||||
for (const r of RIDERS) assert.ok(press(c, 7), r.id);
|
for (const r of RIDERS) assert.ok(press(c, 7), r.id);
|
||||||
assert.equal(c.step, 'start', 'un tocco per pilota e la presentazione è finita');
|
assert.equal(c.step, 'start', 'un tocco per pilota e la presentazione è finita');
|
||||||
assert.equal(c.holding, false, 'senza dare gas');
|
assert.equal(c.holding, false, 'senza dare gas');
|
||||||
@@ -161,7 +161,7 @@ test('l’origine segue gli swipe lunghi e si può correggere; rilascio e annull
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('alla bona si completano tre giri usando solo un dito, senza tasti o pulsanti', () => {
|
test('alla bona si completano tre giri usando solo un dito, senza tasti o pulsanti', () => {
|
||||||
const c = race(newGara(5, { level: 'bona', riders: piloti.filter(p => p.consenso !== null) }));
|
const c = race(newGara(5, { level: 'bona', riders: piloti.filter(p => p.consenso !== null), simple: false }));
|
||||||
aimScreen(c, [320, 180], 'bot'); press(c, 'bot');
|
aimScreen(c, [320, 180], 'bot'); press(c, 'bot');
|
||||||
for (let t = 0; t < 180 && c.step === 'race'; t += 1 / 60) {
|
for (let t = 0; t < 180 && c.step === 'race'; t += 1 / 60) {
|
||||||
swipeTo(c, pointAt(player(c).s + 50));
|
swipeTo(c, pointAt(player(c).s + 50));
|
||||||
@@ -198,7 +198,7 @@ test('la presentazione: uno alla volta arrivano in griglia col motorino, poi il
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('sempre a gas e dritto si finisce nella paglia; con giudizio alla bona si vince', () => {
|
test('sempre a gas e dritto si finisce nella paglia; con giudizio alla bona si vince', () => {
|
||||||
const wild = newGara(5, { level: 'bona', riders: piloti.filter(p => p.consenso !== null) });
|
const wild = newGara(5, { level: 'bona', riders: piloti.filter(p => p.consenso !== null), simple: false });
|
||||||
skip(wild);
|
skip(wild);
|
||||||
run(wild, COUNTDOWN + 60, () => true);
|
run(wild, COUNTDOWN + 60, () => true);
|
||||||
assert.ok(wild.heard.has('gara-fuori:andrea'));
|
assert.ok(wild.heard.has('gara-fuori:andrea'));
|
||||||
@@ -206,7 +206,7 @@ test('sempre a gas e dritto si finisce nella paglia; con giudizio alla bona si v
|
|||||||
assert.equal(wild.step, 'race', 'dritto non si arriva');
|
assert.equal(wild.step, 'race', 'dritto non si arriva');
|
||||||
const heard = new Set();
|
const heard = new Set();
|
||||||
for (const seed of [1, 2, 3, 4]) {
|
for (const seed of [1, 2, 3, 4]) {
|
||||||
const c = newGara(seed, { level: 'bona', riders: piloti.filter(p => p.consenso !== null) });
|
const c = newGara(seed, { level: 'bona', riders: piloti.filter(p => p.consenso !== null), simple: false });
|
||||||
skip(c); setPower(c, 1); // a sensibilità piena: il dito risponde subito
|
skip(c); setPower(c, 1); // a sensibilità piena: il dito risponde subito
|
||||||
for (const k of run(c, COUNTDOWN + 120, careful)) heard.add(k);
|
for (const k of run(c, COUNTDOWN + 120, careful)) heard.add(k);
|
||||||
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'player', `seme ${seed}`);
|
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'player', `seme ${seed}`);
|
||||||
@@ -219,7 +219,7 @@ test('sempre a gas e dritto si finisce nella paglia; con giudizio alla bona si v
|
|||||||
|
|
||||||
test('alla sensibilità di partenza (bassa) lo stesso pilota non finisce mai nella paglia', () => {
|
test('alla sensibilità di partenza (bassa) lo stesso pilota non finisce mai nella paglia', () => {
|
||||||
for (const seed of [1, 2, 3]) {
|
for (const seed of [1, 2, 3]) {
|
||||||
const c = newGara(seed, { level: 'bona', riders: piloti.filter(p => p.consenso !== null) });
|
const c = newGara(seed, { level: 'bona', riders: piloti.filter(p => p.consenso !== null), simple: false });
|
||||||
skip(c); assert.equal(c.sens, SWIPE.sens);
|
skip(c); assert.equal(c.sens, SWIPE.sens);
|
||||||
run(c, COUNTDOWN + 120, careful);
|
run(c, COUNTDOWN + 120, careful);
|
||||||
assert.equal(c.step, 'result'); assert.equal(c.result.offs, 0, `seme ${seed}`);
|
assert.equal(c.step, 'result'); assert.equal(c.result.offs, 0, `seme ${seed}`);
|
||||||
@@ -270,3 +270,64 @@ test('nella storia: dal Trappola l\'Arcu scherza sul Betino, poi la sfida; prima
|
|||||||
// Il Betino, lassù: solo Andrea ne parla.
|
// Il Betino, lassù: solo Andrea ne parla.
|
||||||
g.act('use', 'betino'); assert.ok(settle(g).every(s => s.who === 'andrea'));
|
g.act('use', 'betino'); assert.ok(settle(g).every(s => s.who === 'andrea'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// La guida semplice (Andrea, 29 ottobre: dal telefono lo sterzo era ingiocabile): alla bona e seria si tiene premuto
|
||||||
|
// per il gas e lasciando si frena, lo sterzo va da solo; troppo forte in curva si scivola fuori. Ginocchio in terra no.
|
||||||
|
test('guida semplice: alla bona e seria sì, a ginocchio in terra no; lasciando si frena e lo sterzo va da solo', () => {
|
||||||
|
const riders = piloti.filter(p => p.consenso !== null);
|
||||||
|
assert.equal(simple(newGara(1, { level: 'bona', riders })), true);
|
||||||
|
assert.equal(simple(newGara(1, { level: 'seria', riders })), true);
|
||||||
|
assert.equal(simple(newGara(1, { level: 'male', riders })), false);
|
||||||
|
const c = race(newGara(1, { level: 'bona', riders }));
|
||||||
|
run(c, 2, () => true);
|
||||||
|
const v = player(c).v; assert.ok(v > 40);
|
||||||
|
run(c, 0.3, () => false);
|
||||||
|
assert.ok(player(c).v < v - BRAKE * 0.3 + 5, 'lasciando frena, non solo rallenta');
|
||||||
|
assert.match(status(c), /Freno\./);
|
||||||
|
// Le frecce non sterzano: lo sterzo segue la strada.
|
||||||
|
key(c, 'ArrowLeft', true); run(c, 2, () => true); key(c, 'ArrowLeft', false);
|
||||||
|
assert.ok(Math.abs(player(c).lat) < 9, 'resta in strada');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('guida semplice: sempre a tutto gas si finisce nella paglia; mollando prima delle curve no, e alla bona si vince', () => {
|
||||||
|
const riders = piloti.filter(p => p.consenso !== null);
|
||||||
|
// Molla quando, frenando da qui, arriverebbe a una curva sopra il limite della guida semplice.
|
||||||
|
const wise = c => { const r = player(c), h = r.v * r.v / (2 * BRAKE) + 10; for (let d = 0; d <= h; d += 4) { const a = simpleLimit(r, r.s + d); if (r.v * r.v > a * a + 2 * BRAKE * d) return false; } return true; };
|
||||||
|
for (const seed of [1, 2]) {
|
||||||
|
const wild = race(newGara(seed, { level: 'bona', riders }));
|
||||||
|
run(wild, 120, () => true);
|
||||||
|
assert.ok(wild.result.offs > 0, 'a tutto gas si esce');
|
||||||
|
const c = race(newGara(seed, { level: 'bona', riders }));
|
||||||
|
run(c, 120, wise);
|
||||||
|
assert.equal(c.result.offs, 0); assert.equal(c.result.falls, 0, 'le buche si scansano');
|
||||||
|
assert.equal(c.result.winner, 'player', `seme ${seed}`);
|
||||||
|
}
|
||||||
|
assert.ok(SIMPLE.slide > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Seria semplice: chi non rallenta in curva va fuori e perde, chi frena giusto fa le curve pulite e vince; LASCIA! dice quando', () => {
|
||||||
|
const riders = piloti.filter(p => p.consenso !== null);
|
||||||
|
const wise = c => { const r = player(c), h = r.v * r.v / (2 * BRAKE) + 10; for (let d = 0; d <= h; d += 4) { const a = simpleLimit(r, r.s + d, c.tune); if (r.v * r.v > a * a + 2 * BRAKE * d) return false; } return true; };
|
||||||
|
for (const seed of [1, 2]) {
|
||||||
|
const wild = race(newGara(seed, { level: 'seria', riders }));
|
||||||
|
run(wild, 120, () => true);
|
||||||
|
const good = race(newGara(seed, { level: 'seria', riders }));
|
||||||
|
run(good, 120, wise);
|
||||||
|
assert.ok(wild.result.offs >= 4, `a tutto gas fuori in quasi ogni curva (${wild.result.offs})`);
|
||||||
|
assert.ok(wild.result.place >= 3);
|
||||||
|
assert.equal(good.result.offs, 0); assert.equal(good.result.winner, 'player', `seme ${seed}`);
|
||||||
|
assert.ok(player(good).cleans >= 5, 'le curve pulite');
|
||||||
|
assert.ok(wild.result.times.andrea - good.result.times.andrea > 6, 'tutto gas costa più di 6 secondi');
|
||||||
|
}
|
||||||
|
// Chi segue LASCIA! guida come chi frena giusto; CURVA! arriva prima.
|
||||||
|
const c = race(newGara(1, { level: 'seria', riders }));
|
||||||
|
let early = 0, said = false;
|
||||||
|
for (let t = 0; t < 20; t += 1 / 60) {
|
||||||
|
c.holding = !mustLift(c);
|
||||||
|
if (mustLift(c, PREAVVISO) && !mustLift(c)) early++;
|
||||||
|
update(c, 1 / 60);
|
||||||
|
if (c.holding && mustLift(c)) said ||= /Lascia!/.test(status(c));
|
||||||
|
}
|
||||||
|
assert.ok(early > 20, `il preavviso CURVA! viene prima di LASCIA! (${early} fotogrammi)`);
|
||||||
|
assert.ok(said, 'il pannello dice Lascia!');
|
||||||
|
});
|
||||||
|
|||||||
@@ -34,19 +34,31 @@ try {
|
|||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
const { settle } = await import('/tests/helpers.mjs');
|
const { settle } = await import('/tests/helpers.mjs');
|
||||||
game.act('talk', 'chiaru'); settle(game);
|
game.act('talk', 'chiaru'); settle(game);
|
||||||
game.choose(game.options().findIndex(o => o.text === 'Si corre?')); settle(game);
|
game.choose(game.options().findIndex(o => o.text === 'Si fa una gara alle macchinine?!?!')); settle(game);
|
||||||
game.choose(game.options().findIndex(o => o.text === 'Alla bona.')); settle(game);
|
game.choose(game.options().findIndex(o => o.text === 'Alla bona.')); settle(game);
|
||||||
});
|
});
|
||||||
await page.waitForSelector('#bocce-controls:not([hidden])');
|
await page.waitForSelector('#bocce-controls:not([hidden])');
|
||||||
await page.waitForFunction(async () => {
|
await page.waitForFunction(async () => {
|
||||||
const { hasBackground } = await import('/src/engine/render.js');
|
const { hasBackground } = await import('/src/engine/render.js');
|
||||||
return hasBackground('assets/closeups/micro-parcheggio.png') && hasBackground('assets/closeups/micro/rossa.png');
|
return hasBackground('assets/closeups/micro-parcheggio.png');
|
||||||
});
|
});
|
||||||
assert.equal(await page.locator('#bocce-throw').textContent(), 'Spingi');
|
// Prima si sceglie la macchinina: la viola, toccandola.
|
||||||
|
assert.equal(await page.locator('#bocce-throw').textContent(), 'Scegli');
|
||||||
|
await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/micro/viola-grande.png'));
|
||||||
|
if (name === 'desktop') { await page.waitForTimeout(1600); await page.screenshot({ path: `${out}/${name}-sceglie.png` }); }
|
||||||
|
{
|
||||||
|
const box = await page.locator('#scene').boundingBox(), [vx, vy] = [480, 220];
|
||||||
|
const at = { x: box.x + vx / 640 * box.width, y: box.y + vy / 360 * box.height };
|
||||||
|
if (mobile) await page.touchscreen.tap(at.x, at.y); else await page.mouse.click(at.x, at.y);
|
||||||
|
}
|
||||||
|
assert.deepEqual(await page.evaluate(() => game.closeup.colors), { player: 'viola', cpu: 'blu' });
|
||||||
|
await page.waitForTimeout(1600);
|
||||||
|
if (name === 'desktop') await page.screenshot({ path: `${out}/${name}-scelta.png` });
|
||||||
|
await page.waitForFunction(() => document.querySelector('#bocce-throw').textContent === 'Spingi');
|
||||||
assert.ok(await page.locator('#micro-help').isVisible(), 'il «Come si gioca» delle macchinine');
|
assert.ok(await page.locator('#micro-help').isVisible(), 'il «Come si gioca» delle macchinine');
|
||||||
const exit = await page.locator('#closeup-close').boundingBox(), canvas = await page.locator('#scene').boundingBox();
|
const exit = await page.locator('#closeup-close').boundingBox(), canvas = await page.locator('#scene').boundingBox();
|
||||||
assert.ok(exit.y >= canvas.y + canvas.height, 'l\'uscita sta fuori dalla pista');
|
assert.ok(exit.y >= canvas.y + canvas.height, 'l\'uscita sta fuori dalla pista');
|
||||||
await page.waitForTimeout(1600);
|
await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/micro/viola.png'));
|
||||||
const state = () => page.evaluate(() => ({ angle: game.closeup.angle, power: game.closeup.power, step: game.closeup.step, x: game.closeup.cars.player.x }));
|
const state = () => page.evaluate(() => ({ angle: game.closeup.angle, power: game.closeup.power, step: game.closeup.step, x: game.closeup.cars.player.x }));
|
||||||
// Un punto della pista davanti alla macchinina di Andrea, in pixel della pagina.
|
// Un punto della pista davanti alla macchinina di Andrea, in pixel della pagina.
|
||||||
const ahead = await page.evaluate(() => { const c = game.closeup, i = (c.cars.player.idx + 50) % c.track.n; return c.track.pts[i]; });
|
const ahead = await page.evaluate(() => { const c = game.closeup, i = (c.cars.player.idx + 50) % c.track.n; return c.track.pts[i]; });
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import * as m from '../src/engine/micro.js';
|
import * as m from '../src/engine/micro.js';
|
||||||
|
import * as micro from '../src/engine/micro.js';
|
||||||
import { drawMicro } from '../src/engine/micro-render.js';
|
import { drawMicro } from '../src/engine/micro-render.js';
|
||||||
import { Game, MINIGAMES } from '../src/engine/game.js';
|
import { Game, MINIGAMES } 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';
|
||||||
|
|
||||||
|
// Una gara con la macchinina già scelta (la rossa), per provare il resto.
|
||||||
|
const newMicro = (seed, def = {}) => micro.newMicro(seed, { car: 'rossa', ...def });
|
||||||
|
|
||||||
// Fa andare la spinta in corso (e quella del Chiaru, se tocca a lui) finché non si torna a mirare o è finita.
|
// Fa andare la spinta in corso (e quella del Chiaru, se tocca a lui) finché non si torna a mirare o è finita.
|
||||||
function run(c, limit = 20000) {
|
function run(c, limit = 20000) {
|
||||||
for (let i = 0; i < limit && !(m.canThrow(c) || c.step === 'result'); i++) m.update(c, 1 / 60);
|
for (let i = 0; i < limit && !(m.canThrow(c) || c.step === 'result'); i++) m.update(c, 1 / 60);
|
||||||
@@ -44,7 +49,7 @@ test('la pista: chiusa, larga uguale, e le macchinine partono in pista poco prim
|
|||||||
const d = Math.hypot(t.pts[i][0] - t.pts[j][0], t.pts[i][1] - t.pts[j][1]);
|
const d = Math.hypot(t.pts[i][0] - t.pts[j][0], t.pts[i][1] - t.pts[j][1]);
|
||||||
assert.ok(d > 2 * m.HALF + 2 * m.CAR.r, `tratti troppo vicini: ${i} e ${j} (${d.toFixed(1)} px)`);
|
assert.ok(d > 2 * m.HALF + 2 * m.CAR.r, `tratti troppo vicini: ${i} e ${j} (${d.toFixed(1)} px)`);
|
||||||
}
|
}
|
||||||
const c = m.newMicro(1);
|
const c = newMicro(1);
|
||||||
for (const car of Object.values(c.cars)) {
|
for (const car of Object.values(c.cars)) {
|
||||||
assert.ok(!m.isOut(m.project(t, car.x, car.y).d));
|
assert.ok(!m.isOut(m.project(t, car.x, car.y).d));
|
||||||
assert.ok(car.progress < 0 && car.progress > -30, 'prima del traguardo');
|
assert.ok(car.progress < 0 && car.progress > -30, 'prima del traguardo');
|
||||||
@@ -52,10 +57,10 @@ test('la pista: chiusa, larga uguale, e le macchinine partono in pista poco prim
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('stesso seme: stessa partita; la spinta va lontano quanto dice la guida', () => {
|
test('stesso seme: stessa partita; la spinta va lontano quanto dice la guida', () => {
|
||||||
const a = m.newMicro(4, { level: 'male' }), b = m.newMicro(4, { level: 'male' });
|
const a = newMicro(4, { level: 'male' }), b = newMicro(4, { level: 'male' });
|
||||||
for (const c of [a, b]) { flick(c); run(c); }
|
for (const c of [a, b]) { flick(c); run(c); }
|
||||||
assert.deepEqual(a.cars, b.cars);
|
assert.deepEqual(a.cars, b.cars);
|
||||||
const c = m.newMicro(2, { level: 'bona' }), start = { ...c.cars.player };
|
const c = newMicro(2, { level: 'bona' }), start = { ...c.cars.player };
|
||||||
c.power = 0.6; const g = m.guide(c);
|
c.power = 0.6; const g = m.guide(c);
|
||||||
assert.ok(m.shoot(c)); run(c);
|
assert.ok(m.shoot(c)); run(c);
|
||||||
// Il Chiaru ha già spinto: la macchinina di Andrea sta dove diceva la guida (se lui non l'ha urtata).
|
// Il Chiaru ha già spinto: la macchinina di Andrea sta dove diceva la guida (se lui non l'ha urtata).
|
||||||
@@ -64,7 +69,7 @@ test('stesso seme: stessa partita; la spinta va lontano quanto dice la guida', (
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('tutta fuori: si torna dove si era prima della spinta, e la guida lo diceva', () => {
|
test('tutta fuori: si torna dove si era prima della spinta, e la guida lo diceva', () => {
|
||||||
const c = m.newMicro(3, { level: 'bona' }), start = { x: c.cars.player.x, y: c.cars.player.y };
|
const c = newMicro(3, { level: 'bona' }), start = { x: c.cars.player.x, y: c.cars.player.y };
|
||||||
c.angle = -Math.PI / 2; c.power = 1; // dritto verso l'erba in alto
|
c.angle = -Math.PI / 2; c.power = 1; // dritto verso l'erba in alto
|
||||||
assert.equal(m.guide(c).out, true);
|
assert.equal(m.guide(c).out, true);
|
||||||
assert.ok(m.shoot(c));
|
assert.ok(m.shoot(c));
|
||||||
@@ -78,9 +83,35 @@ test('tutta fuori: si torna dove si era prima della spinta, e la guida lo diceva
|
|||||||
assert.ok(c.events.some(e => e.kind === 'micro-fuori'));
|
assert.ok(c.events.some(e => e.kind === 'micro-fuori'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('si parte scegliendo la macchinina: tocco, frecce, pulsante; il Chiaru tiene la blu, o la rossa se la blu è presa', () => {
|
||||||
|
const c = micro.newMicro(1);
|
||||||
|
assert.equal(c.step, 'choose'); assert.equal(m.canThrow(c), false); assert.equal(m.button(c), 'Scegli');
|
||||||
|
assert.ok(m.aimScreen(c, m.choiceAt(4))); assert.equal(c.choice, 4);
|
||||||
|
assert.equal(m.turn(c, 1, 'x'), false); assert.equal(c.choice, 5, 'la freccia passa alla dopo');
|
||||||
|
m.turn(c, 1, 'x'); assert.equal(c.choice, 0, 'e ricomincia dalla prima');
|
||||||
|
assert.ok(m.shoot(c)); assert.deepEqual(c.colors, { player: 'rossa', cpu: 'blu' }); assert.equal(c.step, 'aim');
|
||||||
|
const d = micro.newMicro(1); m.aimScreen(d, m.choiceAt(1)); assert.ok(m.press(d, 7));
|
||||||
|
assert.deepEqual(d.colors, { player: 'blu', cpu: 'rossa' });
|
||||||
|
assert.equal(d.holding, false, 'il tocco che sceglie non carica la spinta');
|
||||||
|
for (const car of m.CARS) assert.ok(existsSync(new URL('../' + m.carImage(car.id, true), import.meta.url)) && existsSync(new URL('../' + m.carImage(car.id), import.meta.url)));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('primo e secondo mentre si va; nel risultato il perché', () => {
|
||||||
|
const c = newMicro(1);
|
||||||
|
c.cars.player.progress = 100; c.cars.cpu.progress = 80;
|
||||||
|
assert.equal(m.rank(c, 'player'), 1); assert.equal(m.rank(c, 'cpu'), 2);
|
||||||
|
assert.match(m.status(c), /Andrea 1°.*Chiaru 2°/);
|
||||||
|
c.result = { winner: 'cpu', flicks: { player: 20, cpu: 20 }, over: { player: 5, cpu: 22 } };
|
||||||
|
assert.deepEqual(m.why(c), ['Tutti e due al traguardo alla spinta 20:', 'vince chi va più in là, il Chiaru di 3 centimetri.']);
|
||||||
|
c.result = { winner: 'player', flicks: { player: 9, cpu: 9 }, over: { player: 5, cpu: -40 } };
|
||||||
|
assert.deepEqual(m.why(c), ['Andrea al traguardo alla spinta 9;', 'al Chiaru non è bastata la sua.']);
|
||||||
|
c.result = { winner: 'draw', flicks: { player: 9, cpu: 9 }, over: { player: 5, cpu: 5.4 } };
|
||||||
|
assert.match(m.why(c).join(' '), /pari/);
|
||||||
|
});
|
||||||
|
|
||||||
test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si torna indietro', () => {
|
test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si torna indietro', () => {
|
||||||
// Tagliè: dal tratto di mezzo della esse, dritti in su attraverso l'erba verso il tratto di sopra.
|
// Tagliè: dal tratto di mezzo della esse, dritti in su attraverso l'erba verso il tratto di sopra.
|
||||||
const t = m.newMicro(1, { level: 'bona' }), [sx, sy] = t.track.pts[Math.round(t.track.n * 0.48)];
|
const t = newMicro(1, { level: 'bona' }), [sx, sy] = t.track.pts[Math.round(t.track.n * 0.48)];
|
||||||
const p = m.project(t.track, sx, sy); Object.assign(t.cars.player, { x: sx, y: sy, idx: p.i, s: p.s, progress: p.s });
|
const p = m.project(t.track, sx, sy); Object.assign(t.cars.player, { x: sx, y: sy, idx: p.i, s: p.s, progress: p.s });
|
||||||
t.angle = -Math.PI / 2; t.power = 0.6; assert.ok(m.shoot(t));
|
t.angle = -Math.PI / 2; t.power = 0.6; assert.ok(m.shoot(t));
|
||||||
for (let i = 0; i < 2000 && t.step !== 'settle'; i++) m.update(t, 1 / 60);
|
for (let i = 0; i < 2000 && t.step !== 'settle'; i++) m.update(t, 1 / 60);
|
||||||
@@ -88,7 +119,7 @@ test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si
|
|||||||
assert.match(t.line, /^Tagliè! Andrea torna dov'era\./);
|
assert.match(t.line, /^Tagliè! Andrea torna dov'era\./);
|
||||||
assert.deepEqual([t.cars.player.x, t.cars.player.y], [sx, sy]);
|
assert.deepEqual([t.cars.player.x, t.cars.player.y], [sx, sy]);
|
||||||
// Cappottè: Andrea a tutta forza contro il Chiaru fermo davanti.
|
// Cappottè: Andrea a tutta forza contro il Chiaru fermo davanti.
|
||||||
const k = m.newMicro(2, { level: 'bona' }), me = k.cars.player, him = k.cars.cpu, before = { x: him.x, y: him.y };
|
const k = newMicro(2, { level: 'bona' }), me = k.cars.player, him = k.cars.cpu, before = { x: him.x, y: him.y };
|
||||||
k.angle = Math.atan2(him.y - me.y, him.x - me.x); k.power = 1; assert.ok(m.shoot(k));
|
k.angle = Math.atan2(him.y - me.y, him.x - me.x); k.power = 1; assert.ok(m.shoot(k));
|
||||||
for (let i = 0; i < 2000 && k.step !== 'settle'; i++) m.update(k, 1 / 60);
|
for (let i = 0; i < 2000 && k.step !== 'settle'; i++) m.update(k, 1 / 60);
|
||||||
assert.equal(him.fault, 'cappotte');
|
assert.equal(him.fault, 'cappotte');
|
||||||
@@ -97,7 +128,7 @@ test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si
|
|||||||
// Torna al suo posto; se lì c'è ora Andrea, si scosta appena quanto basta.
|
// Torna al suo posto; se lì c'è ora Andrea, si scosta appena quanto basta.
|
||||||
assert.ok(Math.hypot(him.x - before.x, him.y - before.y) <= 2 * m.CAR.r + 1e-9);
|
assert.ok(Math.hypot(him.x - before.x, him.y - before.y) <= 2 * m.CAR.r + 1e-9);
|
||||||
// Accompagnè: tenuto premuto ben oltre la carica piena.
|
// Accompagnè: tenuto premuto ben oltre la carica piena.
|
||||||
const a = m.newMicro(3), start = { x: a.cars.player.x, y: a.cars.player.y };
|
const a = newMicro(3), start = { x: a.cars.player.x, y: a.cars.player.y };
|
||||||
assert.ok(m.press(a));
|
assert.ok(m.press(a));
|
||||||
for (let i = 0; i < 120; i++) m.update(a, 1 / 60);
|
for (let i = 0; i < 120; i++) m.update(a, 1 / 60);
|
||||||
assert.ok(m.overHeld(a));
|
assert.ok(m.overHeld(a));
|
||||||
@@ -106,7 +137,7 @@ test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si
|
|||||||
assert.match(a.line, /^Accompagnè! Andrea torna dov'era\./);
|
assert.match(a.line, /^Accompagnè! Andrea torna dov'era\./);
|
||||||
assert.deepEqual([a.cars.player.x, a.cars.player.y], [start.x, start.y]);
|
assert.deepEqual([a.cars.player.x, a.cars.player.y], [start.x, start.y]);
|
||||||
// Lasciando in tempo, la spinta piena vale.
|
// Lasciando in tempo, la spinta piena vale.
|
||||||
const ok = m.newMicro(3); m.press(ok);
|
const ok = newMicro(3); m.press(ok);
|
||||||
for (let i = 0; i < Math.round((m.SWING + 0.2) * 60); i++) m.update(ok, 1 / 60);
|
for (let i = 0; i < Math.round((m.SWING + 0.2) * 60); i++) m.update(ok, 1 / 60);
|
||||||
m.release(ok); assert.equal(ok.cars.player.fault, null);
|
m.release(ok); assert.equal(ok.cars.player.fault, null);
|
||||||
});
|
});
|
||||||
@@ -114,7 +145,7 @@ test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si
|
|||||||
test('una partita intera: stesse spinte per tutti e due, un vincitore, e il Chiaru alla bona perde contro chi usa la guida', () => {
|
test('una partita intera: stesse spinte per tutti e due, un vincitore, e il Chiaru alla bona perde contro chi usa la guida', () => {
|
||||||
const wins = { player: 0, cpu: 0, draw: 0 };
|
const wins = { player: 0, cpu: 0, draw: 0 };
|
||||||
for (const seed of [1, 2, 3]) {
|
for (const seed of [1, 2, 3]) {
|
||||||
const c = m.newMicro(seed, { level: 'bona' });
|
const c = newMicro(seed, { level: 'bona' });
|
||||||
for (let n = 0; n < 80 && c.step !== 'result'; n++) { if (m.canThrow(c)) guided(c); run(c); }
|
for (let n = 0; n < 80 && c.step !== 'result'; n++) { if (m.canThrow(c)) guided(c); run(c); }
|
||||||
assert.equal(c.step, 'result');
|
assert.equal(c.step, 'result');
|
||||||
assert.equal(c.result.flicks.player, c.result.flicks.cpu);
|
assert.equal(c.result.flicks.player, c.result.flicks.cpu);
|
||||||
@@ -124,7 +155,7 @@ test('una partita intera: stesse spinte per tutti e due, un vincitore, e il Chia
|
|||||||
assert.ok(wins.player >= 2, `alla bona Andrea, con la guida, deve vincere quasi sempre (${JSON.stringify(wins)})`);
|
assert.ok(wins.player >= 2, `alla bona Andrea, con la guida, deve vincere quasi sempre (${JSON.stringify(wins)})`);
|
||||||
// A fassi male il Chiaru fa il giro con meno spinte che alla bona.
|
// A fassi male il Chiaru fa il giro con meno spinte che alla bona.
|
||||||
const flicks = level => {
|
const flicks = level => {
|
||||||
const c = m.newMicro(7, { level });
|
const c = newMicro(7, { level });
|
||||||
for (let n = 0; n < 80 && c.step !== 'result'; n++) { if (m.canThrow(c)) { c.angle = c.cars.player.heading; c.power = 0; m.shoot(c); } run(c); }
|
for (let n = 0; n < 80 && c.step !== 'result'; n++) { if (m.canThrow(c)) { c.angle = c.cars.player.heading; c.power = 0; m.shoot(c); } run(c); }
|
||||||
return c.flicks.cpu;
|
return c.flicks.cpu;
|
||||||
};
|
};
|
||||||
@@ -132,14 +163,18 @@ test('una partita intera: stesse spinte per tutti e due, un vincitore, e il Chia
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('i comandi: un tocco mira soltanto, tenere premuto carica, lasciare spinge; la mira si gira con le frecce', () => {
|
test('i comandi: un tocco mira soltanto, tenere premuto carica, lasciare spinge; la mira si gira con le frecce', () => {
|
||||||
const c = m.newMicro(5);
|
const c = newMicro(5);
|
||||||
assert.ok(m.press(c, 1)); m.update(c, 0.05); assert.ok(m.release(c, 1) === true && c.step === 'aim', 'il tocco breve non spinge');
|
assert.ok(m.press(c, 1)); m.update(c, 0.05); assert.ok(m.release(c, 1) === true && c.step === 'aim', 'il tocco breve non spinge');
|
||||||
assert.ok(m.press(c, 1)); for (let i = 0; i < 60; i++) m.update(c, 1 / 60); assert.ok(c.charging);
|
// Premuto, la forza riparte da zero e sale; un secondo dopo è quasi piena.
|
||||||
|
assert.ok(m.press(c, 1)); assert.equal(c.power, 0);
|
||||||
|
m.update(c, 0.3); const early = c.power; assert.ok(c.charging && early > 0.2 && early < 0.3, `forza ${early}`);
|
||||||
|
for (let i = 0; i < 42; i++) m.update(c, 1 / 60);
|
||||||
|
assert.ok(c.power > early && c.power < 1);
|
||||||
const before = c.cars.player.x;
|
const before = c.cars.player.x;
|
||||||
assert.ok(m.release(c, 1)); assert.equal(c.step, 'rolling');
|
assert.ok(m.release(c, 1)); assert.equal(c.step, 'rolling');
|
||||||
m.update(c, 0.2); assert.notEqual(c.cars.player.x, before);
|
m.update(c, 0.2); assert.notEqual(c.cars.player.x, before);
|
||||||
assert.equal(m.press(c, 2), false, 'mentre corre non si spinge');
|
assert.equal(m.press(c, 2), false, 'mentre corre non si spinge');
|
||||||
const d = m.newMicro(5), a0 = d.angle;
|
const d = newMicro(5), a0 = d.angle;
|
||||||
m.key(d, 'ArrowRight', true); m.update(d, 0.5); m.key(d, 'ArrowRight', false);
|
m.key(d, 'ArrowRight', true); m.update(d, 0.5); m.key(d, 'ArrowRight', false);
|
||||||
assert.ok(d.angle > a0);
|
assert.ok(d.angle > a0);
|
||||||
assert.ok(m.aimScreen(d, [320, 200])); assert.equal(m.aimScreen(d, [320, 10]), false, 'il tabellone non si mira');
|
assert.ok(m.aimScreen(d, [320, 200])); assert.equal(m.aimScreen(d, [320, 10]), false, 'il tabellone non si mira');
|
||||||
@@ -147,7 +182,7 @@ test('i comandi: un tocco mira soltanto, tenere premuto carica, lasciare spinge;
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('il Chiaru pensa a pezzi: nessun fotogramma lo ferma troppo', () => {
|
test('il Chiaru pensa a pezzi: nessun fotogramma lo ferma troppo', () => {
|
||||||
const c = m.newMicro(6, { level: 'male' });
|
const c = newMicro(6, { level: 'male' });
|
||||||
flick(c);
|
flick(c);
|
||||||
let worst = 0;
|
let worst = 0;
|
||||||
for (let i = 0; i < 600 && !m.canThrow(c); i++) { const t = performance.now(); m.update(c, 1 / 60); worst = Math.max(worst, performance.now() - t); }
|
for (let i = 0; i < 600 && !m.canThrow(c); i++) { const t = performance.now(); m.update(c, 1 / 60); worst = Math.max(worst, performance.now() - t); }
|
||||||
@@ -156,13 +191,14 @@ test('il Chiaru pensa a pezzi: nessun fotogramma lo ferma troppo', () => {
|
|||||||
|
|
||||||
test('il disegno: gesso, macchinine, guida e risultato; lo stato non cambia', () => {
|
test('il disegno: gesso, macchinine, guida e risultato; lo stato non cambia', () => {
|
||||||
const calls = [], ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => calls.push([k, ...args]) });
|
const calls = [], ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => calls.push([k, ...args]) });
|
||||||
const c = m.newMicro(8, { level: 'bona' }), bg = {}, cars = { [story.closeups.micro.cars.player]: { car: 'player' } };
|
const c = newMicro(8, { level: 'bona' }), bg = {}, cars = { [m.carImage('rossa')]: { car: 'player' } };
|
||||||
const draw = () => { const before = structuredClone(c); drawMicro(ctx, c, story.closeups.micro, bg, src => cars[src]); assert.deepEqual(c, before); };
|
const draw = () => { const before = structuredClone(c); drawMicro(ctx, c, story.closeups.micro, bg, src => cars[src]); assert.deepEqual(c, before); };
|
||||||
draw();
|
draw();
|
||||||
assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
|
assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
|
||||||
assert.ok(calls.filter(([op, img]) => op === 'drawImage' && img?.car === 'player').length >= 2, 'la macchinina e il suo fantasma');
|
assert.ok(calls.filter(([op, img]) => op === 'drawImage' && img?.car === 'player').length >= 2, 'la macchinina e il suo fantasma');
|
||||||
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'Chiaru'));
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'Chiaru'));
|
||||||
c.step = 'result'; c.result = { winner: 'cpu', flicks: { player: 12, cpu: 12 } }; calls.length = 0; draw();
|
c.step = 'result'; c.result = { winner: 'cpu', flicks: { player: 12, cpu: 12 }, over: { player: 3, cpu: 20 } }; calls.length = 0; draw();
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'vince chi va più in là, il Chiaru di 3 centimetri.'));
|
||||||
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'HA VINTO IL CHIARU'));
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'HA VINTO IL CHIARU'));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -173,13 +209,13 @@ test('il Chiaru al parcheggio di Cardetole: si chiacchiera, si sceglie il livell
|
|||||||
for (const y of [222, 260, 300, 314]) assert.equal(g.hotspotAt([520, y])?.id, 'chiaru', `clic a y=${y}`);
|
for (const y of [222, 260, 300, 314]) assert.equal(g.hotspotAt([520, y])?.id, 'chiaru', `clic a y=${y}`);
|
||||||
g.act('talk', 'chiaru'); settle(g);
|
g.act('talk', 'chiaru'); settle(g);
|
||||||
assert.ok(!g.options().some(o => o.text === 'Seria.'), 'il livello non si sceglie subito');
|
assert.ok(!g.options().some(o => o.text === 'Seria.'), 'il livello non si sceglie subito');
|
||||||
g.choose(g.options().findIndex(o => o.text === 'Si corre?')); settle(g);
|
g.choose(g.options().findIndex(o => o.text === 'Si fa una gara alle macchinine?!?!')); settle(g);
|
||||||
g.choose(g.options().findIndex(o => o.text === 'No, aspetta.')); settle(g);
|
g.choose(g.options().findIndex(o => o.text === 'No, aspetta.')); settle(g);
|
||||||
assert.equal(g.closeup, null);
|
assert.equal(g.closeup, null);
|
||||||
g.choose(g.options().findIndex(o => o.text === 'Si corre?')); settle(g);
|
g.choose(g.options().findIndex(o => o.text === 'Si fa una gara alle macchinine?!?!')); settle(g);
|
||||||
g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g);
|
g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g);
|
||||||
assert.equal(g.closeup?.id, 'micro'); assert.equal(g.closeup.level, 'male');
|
assert.equal(g.closeup?.id, 'micro'); assert.equal(g.closeup.level, 'male');
|
||||||
g.closeup.step = 'result'; g.closeup.result = { winner: 'player', flicks: { player: 11, cpu: 11 } };
|
g.closeup.step = 'result'; g.closeup.result = { winner: 'player', flicks: { player: 11, cpu: 11 }, over: { player: 4, cpu: -30 } };
|
||||||
g.closeupClose(); settle(g);
|
g.closeupClose(); settle(g);
|
||||||
assert.ok(g.state.flags.micro_vinto);
|
assert.ok(g.state.flags.micro_vinto);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -437,7 +437,7 @@ test('interrompere la catena del ghiacciolo non perde la torcia né la chiavetta
|
|||||||
|
|
||||||
test('gli sfondi si caricano dalla scena di partenza verso le più lontane, tutti e una volta sola', () => {
|
test('gli sfondi si caricano dalla scena di partenza verso le più lontane, tutti e una volta sola', () => {
|
||||||
const order = backgroundOrder(story, story.start.scene);
|
const order = backgroundOrder(story, story.start.scene);
|
||||||
const all = new Set(Object.values(story.scenes).map(s => s.background).filter(Boolean));
|
const all = new Set(Object.values(story.scenes).flatMap(s => [s.background, ...(s.backdrops ?? []).map(b => b.src)]).filter(Boolean));
|
||||||
assert.equal(order.length, all.size);
|
assert.equal(order.length, all.size);
|
||||||
assert.deepEqual(new Set(order), all);
|
assert.deepEqual(new Set(order), all);
|
||||||
assert.equal(order[0], story.scenes[story.start.scene].background, 'prima la piazza');
|
assert.equal(order[0], story.scenes[story.start.scene].background, 'prima la piazza');
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import { Game } from '../src/engine/game.js';
|
|||||||
import { inside } from '../src/engine/geometry.js';
|
import { inside } from '../src/engine/geometry.js';
|
||||||
import { settle } from './helpers.mjs';
|
import { settle } from './helpers.mjs';
|
||||||
|
|
||||||
// La Ghiacciomobile al circolino è uno sprite; il Betino in cima alla salita di Via Liguri Magelli è dipinto nel quadro.
|
// I veicoli sono dipinti nel quadro: il Betino in cima alla salita di Via Liguri Magelli e, prima del tramonto, la
|
||||||
const SPRITES = [['campomigliaio_circolo', 'ghiacciomobile', [150, 170]]];
|
// Ghiacciomobile dentro il muro giallo (Andrea, 29 ottobre). Nessuno è più uno sprite.
|
||||||
const VEHICLES = [...SPRITES, ['via_liguri_magelli', 'betino']];
|
const SPRITES = [];
|
||||||
|
const VEHICLES = [['via_liguri_magelli', 'betino'], ['via_liguri_magelli', 'ghiacciomobile']];
|
||||||
test('i veicoli restano fuori dal percorso e lasciano liberi gli altri hotspot', () => {
|
test('i veicoli restano fuori dal percorso e lasciano liberi gli altri hotspot', () => {
|
||||||
for (const [scene, id, [lo, hi]] of SPRITES) {
|
for (const [scene, id, [lo, hi]] of SPRITES) {
|
||||||
const g = new Game(story);
|
const g = new Game(story);
|
||||||
@@ -62,3 +63,15 @@ test('guarda e usa i veicoli: parla solo Andrea e non cambia il progresso', () =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Via Liguri Magelli: prima la Ghiacciomobile nel muro (senza targa), dal tramonto il muro rifatto con la targa', () => {
|
||||||
|
const g = new Game(story);
|
||||||
|
g.go('via_liguri_magelli'); settle(g);
|
||||||
|
assert.equal(g.backdrop(), 'assets/scenes/via-liguri-magelli-muro.png');
|
||||||
|
assert.ok(g.hotspot('ghiacciomobile') && !g.hotspot('targa'));
|
||||||
|
assert.ok(!story.scenes.campomigliaio_circolo.hotspots.some(h => h.id === 'ghiacciomobile'), 'al circolino non c\'è più');
|
||||||
|
g.state.flags.indizio_regolo = true;
|
||||||
|
assert.equal(g.backdrop(), 'assets/scenes/via-liguri-magelli.png');
|
||||||
|
assert.ok(g.hotspot('targa') && !g.hotspot('ghiacciomobile'));
|
||||||
|
assert.ok(existsSync(new URL('../assets/scenes/via-liguri-magelli-muro.png', import.meta.url)));
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"_nota": "Parole che XTTS legge male in italiano: a sinistra come sono scritte nella storia (e nel fumetto), a destra come vanno lette dalla voce. Si cerca la parola intera, senza badare alle maiuscole. Cambiare una riga rigenera solo le battute che la contengono (make voci). Esempio: «celo» (ce l'ho, delle figurine) XTTS lo legge «cielo».",
|
"_nota": "Parole che XTTS legge male in italiano: a sinistra come sono scritte nella storia (e nel fumetto), a destra come vanno lette dalla voce. Si cerca la parola intera, senza badare alle maiuscole. Cambiare una riga rigenera solo le battute che la contengono (make voci). Esempio: «celo» (ce l'ho, delle figurine) XTTS lo legge «cielo».",
|
||||||
"celo": "ce l'ho",
|
"celo": "ce l'ho",
|
||||||
"Trappizzio": "Trappìzzio",
|
"Trappizzio": "Trappìzzio",
|
||||||
|
"Zio": "Zìo",
|
||||||
"Cosimo I": "Cosimo primo",
|
"Cosimo I": "Cosimo primo",
|
||||||
"padel": "pàdel",
|
"padel": "pàdel",
|
||||||
"l'A. Bettini": "l'Abbettini",
|
"l'A. Bettini": "l'Abbettini",
|
||||||
|
|||||||
@@ -35,6 +35,10 @@
|
|||||||
"speaker": "Xavier Hayasaka",
|
"speaker": "Xavier Hayasaka",
|
||||||
"filtro": "asetrate=24000*1.12,aresample=24000,atempo=0.92"
|
"filtro": "asetrate=24000*1.12,aresample=24000,atempo=0.92"
|
||||||
},
|
},
|
||||||
|
"zio_mario": {
|
||||||
|
"speaker": "Viktor Menelaos",
|
||||||
|
"filtro": "asetrate=24000*0.95,aresample=24000,atempo=1.04"
|
||||||
|
},
|
||||||
"chiaru": {
|
"chiaru": {
|
||||||
"speaker": "Luis Moray"
|
"speaker": "Luis Moray"
|
||||||
},
|
},
|
||||||
|
|||||||