Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de418c0da8 | ||
|
|
713e328138 | ||
|
|
6a5a80d7af | ||
|
|
88f0439f79 | ||
|
|
c244424af9 | ||
|
|
c26d888263 | ||
|
|
f8dbd01358 | ||
|
|
e3d5d8b0d3 | ||
|
|
1b7511f6ca | ||
|
|
9d2cc2fcb3 | ||
|
|
44480221db | ||
|
|
4d13a4ef25 | ||
|
|
22f408cc60 | ||
|
|
927459344b | ||
|
|
08ab9ba069 | ||
|
|
1918c8d9c7 | ||
|
|
381254ed4d | ||
|
|
a11a6ef59f | ||
|
|
086e4682d4 | ||
|
|
bf22dc2511 | ||
|
|
6585c9ffcb | ||
|
|
13585491a4 | ||
|
|
72a629e88b | ||
|
|
f57bec560a | ||
|
|
1f49486870 | ||
|
|
06f7cfbfad | ||
|
|
e1b92278a5 | ||
|
|
b3c50f4694 |
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"image": "bertovo.png",
|
||||||
|
"height": 120,
|
||||||
|
"frames": {
|
||||||
|
"stand": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 53,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 27.4,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"blink": {
|
||||||
|
"x": 55,
|
||||||
|
"y": 0,
|
||||||
|
"w": 52,
|
||||||
|
"h": 121,
|
||||||
|
"ax": 26.1,
|
||||||
|
"ay": 121
|
||||||
|
},
|
||||||
|
"talk": {
|
||||||
|
"x": 109,
|
||||||
|
"y": 0,
|
||||||
|
"w": 79,
|
||||||
|
"h": 121,
|
||||||
|
"ax": 31.3,
|
||||||
|
"ay": 121
|
||||||
|
},
|
||||||
|
"explain": {
|
||||||
|
"x": 190,
|
||||||
|
"y": 0,
|
||||||
|
"w": 58,
|
||||||
|
"h": 121,
|
||||||
|
"ax": 30.2,
|
||||||
|
"ay": 121
|
||||||
|
},
|
||||||
|
"laugh": {
|
||||||
|
"x": 250,
|
||||||
|
"y": 0,
|
||||||
|
"w": 54,
|
||||||
|
"h": 122,
|
||||||
|
"ax": 28.6,
|
||||||
|
"ay": 122
|
||||||
|
},
|
||||||
|
"chalk": {
|
||||||
|
"x": 306,
|
||||||
|
"y": 0,
|
||||||
|
"w": 55,
|
||||||
|
"h": 121,
|
||||||
|
"ax": 25.0,
|
||||||
|
"ay": 121
|
||||||
|
},
|
||||||
|
"shrug": {
|
||||||
|
"x": 363,
|
||||||
|
"y": 0,
|
||||||
|
"w": 91,
|
||||||
|
"h": 122,
|
||||||
|
"ax": 45.4,
|
||||||
|
"ay": 122
|
||||||
|
},
|
||||||
|
"cheer": {
|
||||||
|
"x": 456,
|
||||||
|
"y": 0,
|
||||||
|
"w": 58,
|
||||||
|
"h": 125,
|
||||||
|
"ax": 31.0,
|
||||||
|
"ay": 125
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 95 KiB |
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"image": "chiaru.png",
|
||||||
|
"height": 120,
|
||||||
|
"frames": {
|
||||||
|
"explain": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 56,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 31.5,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"talk": {
|
||||||
|
"x": 58,
|
||||||
|
"y": 0,
|
||||||
|
"w": 64,
|
||||||
|
"h": 119,
|
||||||
|
"ax": 38.5,
|
||||||
|
"ay": 119
|
||||||
|
},
|
||||||
|
"blink": {
|
||||||
|
"x": 124,
|
||||||
|
"y": 0,
|
||||||
|
"w": 44,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 24.2,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"stand": {
|
||||||
|
"x": 170,
|
||||||
|
"y": 0,
|
||||||
|
"w": 44,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 24.2,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"cheer": {
|
||||||
|
"x": 216,
|
||||||
|
"y": 0,
|
||||||
|
"w": 66,
|
||||||
|
"h": 118,
|
||||||
|
"ax": 31.5,
|
||||||
|
"ay": 118
|
||||||
|
},
|
||||||
|
"shrug": {
|
||||||
|
"x": 284,
|
||||||
|
"y": 0,
|
||||||
|
"w": 79,
|
||||||
|
"h": 119,
|
||||||
|
"ax": 42.2,
|
||||||
|
"ay": 119
|
||||||
|
},
|
||||||
|
"watch": {
|
||||||
|
"x": 365,
|
||||||
|
"y": 0,
|
||||||
|
"w": 63,
|
||||||
|
"h": 98,
|
||||||
|
"ax": 30.3,
|
||||||
|
"ay": 98
|
||||||
|
},
|
||||||
|
"laugh": {
|
||||||
|
"x": 430,
|
||||||
|
"y": 0,
|
||||||
|
"w": 52,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 30.3,
|
||||||
|
"ay": 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 388 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 1006 B |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 235 KiB |
@@ -935,6 +935,10 @@
|
|||||||
"file": "andrea-127a90af53.mp3",
|
"file": "andrea-127a90af53.mp3",
|
||||||
"sec": 9.37
|
"sec": 9.37
|
||||||
},
|
},
|
||||||
|
"andrea|Trappizzio Berti, al tavolo di mezzo. Le biglie le mette dove nessuno ci arriva, e poi ci arriva lui.": {
|
||||||
|
"file": "andrea-089ce1d608.mp3",
|
||||||
|
"sec": 7.92
|
||||||
|
},
|
||||||
"andrea|Le stecche del circolo, in fila. Ognuna ha il suo proprietario, e guai a sbagliare.": {
|
"andrea|Le stecche del circolo, in fila. Ognuna ha il suo proprietario, e guai a sbagliare.": {
|
||||||
"file": "andrea-1eae1a8577.mp3",
|
"file": "andrea-1eae1a8577.mp3",
|
||||||
"sec": 6.1
|
"sec": 6.1
|
||||||
@@ -1183,14 +1187,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
|
||||||
@@ -1583,6 +1579,10 @@
|
|||||||
"file": "andrea-ee455fdc33.mp3",
|
"file": "andrea-ee455fdc33.mp3",
|
||||||
"sec": 3.67
|
"sec": 3.67
|
||||||
},
|
},
|
||||||
|
"andrea|Qui dietro ci stanno i camper. Ma per noi questo resta il campo del Giannini.": {
|
||||||
|
"file": "andrea-195cf2b022.mp3",
|
||||||
|
"sec": 4.86
|
||||||
|
},
|
||||||
"andrea|Gli scivoli dei bambini. Una volta il pallone finiva lì una volta su tre.": {
|
"andrea|Gli scivoli dei bambini. Una volta il pallone finiva lì una volta su tre.": {
|
||||||
"file": "andrea-a3e36c12a1.mp3",
|
"file": "andrea-a3e36c12a1.mp3",
|
||||||
"sec": 5.4
|
"sec": 5.4
|
||||||
@@ -1599,6 +1599,18 @@
|
|||||||
"file": "andrea-a93c65234e.mp3",
|
"file": "andrea-a93c65234e.mp3",
|
||||||
"sec": 4.28
|
"sec": 4.28
|
||||||
},
|
},
|
||||||
|
"andrea|Di chi è quell'Audi nera?": {
|
||||||
|
"file": "andrea-b9a784137e.mp3",
|
||||||
|
"sec": 1.63
|
||||||
|
},
|
||||||
|
"arcu|Let it be.": {
|
||||||
|
"file": "arcu-6ba2f5285a.mp3",
|
||||||
|
"sec": 0.69
|
||||||
|
},
|
||||||
|
"andrea|Non è mica mia. Mi garba, però, nera fiammante.": {
|
||||||
|
"file": "andrea-ae928089b9.mp3",
|
||||||
|
"sec": 4.03
|
||||||
|
},
|
||||||
"andrea|«Qui riposa la Ghiacciomobile, che si è schiantata lì.»": {
|
"andrea|«Qui riposa la Ghiacciomobile, che si è schiantata lì.»": {
|
||||||
"file": "andrea-a9e7f3f9ad.mp3",
|
"file": "andrea-a9e7f3f9ad.mp3",
|
||||||
"sec": 3.11
|
"sec": 3.11
|
||||||
@@ -1607,6 +1619,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
|
||||||
@@ -1619,9 +1639,13 @@
|
|||||||
"file": "andrea-3d15dd47bb.mp3",
|
"file": "andrea-3d15dd47bb.mp3",
|
||||||
"sec": 4.82
|
"sec": 4.82
|
||||||
},
|
},
|
||||||
"andrea|Casa mia. Terzo piano, quello con la tapparella a metà: è la mia camera.": {
|
"andrea|Il parcheggio sotto casa. Col gesso ci si disegnava la pista delle macchinine, e si giocava finché non chiamavano per cena.": {
|
||||||
"file": "andrea-acc7949f4c.mp3",
|
"file": "andrea-8670e6ce99.mp3",
|
||||||
"sec": 5.91
|
"sec": 7.6
|
||||||
|
},
|
||||||
|
"andrea|Casa mia. Secondo piano: quella con la tapparella a metà è la camera di mia sorella, e a destra quella dei miei.": {
|
||||||
|
"file": "andrea-6c9e7f684e.mp3",
|
||||||
|
"sec": 8.72
|
||||||
},
|
},
|
||||||
"andrea|Se salgo, mi mettono ad apparecchiare. Prima il palio.": {
|
"andrea|Se salgo, mi mettono ad apparecchiare. Prima il palio.": {
|
||||||
"file": "andrea-8945f1786d.mp3",
|
"file": "andrea-8945f1786d.mp3",
|
||||||
@@ -1639,6 +1663,18 @@
|
|||||||
"file": "andrea-018183c4dd.mp3",
|
"file": "andrea-018183c4dd.mp3",
|
||||||
"sec": 5.82
|
"sec": 5.82
|
||||||
},
|
},
|
||||||
|
"andrea|Su questo campo ci abbiamo passato trentacinque anni, giorno, pomeriggio e sera, a giocare a calcio.": {
|
||||||
|
"file": "andrea-5550b0890f.mp3",
|
||||||
|
"sec": 7.74
|
||||||
|
},
|
||||||
|
"andrea|Le maglie a fare da pali, e i palloni tagliati che entravano nelle terrazze, ovunque.": {
|
||||||
|
"file": "andrea-f6c10fd559.mp3",
|
||||||
|
"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|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
|
||||||
@@ -2399,6 +2435,142 @@
|
|||||||
"file": "mariano-d65fb6e871.mp3",
|
"file": "mariano-d65fb6e871.mp3",
|
||||||
"sec": 3.42
|
"sec": 3.42
|
||||||
},
|
},
|
||||||
|
"andrea|Si fa una gara alle macchinine?!?!": {
|
||||||
|
"file": "andrea-17216997e2.mp3",
|
||||||
|
"sec": 2.74
|
||||||
|
},
|
||||||
|
"andrea|Che pista hai disegnato?": {
|
||||||
|
"file": "andrea-3dd737ed1b.mp3",
|
||||||
|
"sec": 1.46
|
||||||
|
},
|
||||||
|
"andrea|Le regole sono quelle solite?": {
|
||||||
|
"file": "andrea-ee03433b9c.mp3",
|
||||||
|
"sec": 1.6
|
||||||
|
},
|
||||||
|
"andrea|Vado a fare un girino alle case rosse.": {
|
||||||
|
"file": "andrea-cb2fa74266.mp3",
|
||||||
|
"sec": 1.97
|
||||||
|
},
|
||||||
|
"chiaru|Un giro, una spinta a testa, prima te. Tutta fuori dal gesso, si torna indietro. Come la vuoi?": {
|
||||||
|
"file": "chiaru-df4b1e3a0e.mp3",
|
||||||
|
"sec": 7.32
|
||||||
|
},
|
||||||
|
"chiaru|Rettilineo, curvone, rettilineo, e in fondo la esse. Nella esse chi ha fretta esce.": {
|
||||||
|
"file": "chiaru-a7d83e9454.mp3",
|
||||||
|
"sec": 7.35
|
||||||
|
},
|
||||||
|
"chiaru|Il tombino l'ho lasciato in pista: porta fortuna.": {
|
||||||
|
"file": "chiaru-3810b7d228.mp3",
|
||||||
|
"sec": 3.56
|
||||||
|
},
|
||||||
|
"chiaru|Le solite. Chi accompagna la macchinina col dito: accompagnè.": {
|
||||||
|
"file": "chiaru-bb17a500b5.mp3",
|
||||||
|
"sec": 4.39
|
||||||
|
},
|
||||||
|
"chiaru|Chi taglia la strada: tagliè. Chi si ribalta: cappottè. E chi esce tutto dal gesso, fuori.": {
|
||||||
|
"file": "chiaru-1db3c53c04.mp3",
|
||||||
|
"sec": 7.9
|
||||||
|
},
|
||||||
|
"chiaru|In tutti i casi, si torna dov'eri prima.": {
|
||||||
|
"file": "chiaru-a7ab8d241f.mp3",
|
||||||
|
"sec": 2.01
|
||||||
|
},
|
||||||
|
"chiaru|Vorrai dire un giochino!": {
|
||||||
|
"file": "chiaru-c106f9e9c4.mp3",
|
||||||
|
"sec": 1.14
|
||||||
|
},
|
||||||
|
"andrea|Ciao Mattè.": {
|
||||||
|
"file": "andrea-fc5a08bbd6.mp3",
|
||||||
|
"sec": 0.73
|
||||||
|
},
|
||||||
|
"andrea|No, aspetta.": {
|
||||||
|
"file": "andrea-04890c1d8e.mp3",
|
||||||
|
"sec": 1.39
|
||||||
|
},
|
||||||
|
"chiaru|Alla bona: ti faccio vedere dove si ferma. E io vado piano.": {
|
||||||
|
"file": "chiaru-d31cc223f6.mp3",
|
||||||
|
"sec": 5.08
|
||||||
|
},
|
||||||
|
"chiaru|Seria. Così mi piace.": {
|
||||||
|
"file": "chiaru-89bf2bb9f9.mp3",
|
||||||
|
"sec": 2.31
|
||||||
|
},
|
||||||
|
"chiaru|A fassi male. Poi non piangere.": {
|
||||||
|
"file": "chiaru-f551f6a268.mp3",
|
||||||
|
"sec": 2.5
|
||||||
|
},
|
||||||
|
"chiaru|Io intanto ripasso il gesso.": {
|
||||||
|
"file": "chiaru-a0d7dbb4c1.mp3",
|
||||||
|
"sec": 2.15
|
||||||
|
},
|
||||||
|
"andrea|Fabri! Che fai, giochi da solo?": {
|
||||||
|
"file": "andrea-44037129d8.mp3",
|
||||||
|
"sec": 2.71
|
||||||
|
},
|
||||||
|
"andrea|Come fai a prendere le biglie dove nessuno ci arriva?": {
|
||||||
|
"file": "andrea-c6670ffb48.mp3",
|
||||||
|
"sec": 2.52
|
||||||
|
},
|
||||||
|
"andrea|Dai, fammi vedere un rinquarto.": {
|
||||||
|
"file": "andrea-728f589a77.mp3",
|
||||||
|
"sec": 1.68
|
||||||
|
},
|
||||||
|
"andrea|Si fa una sfida a rinquarti?": {
|
||||||
|
"file": "andrea-fe39c8f30c.mp3",
|
||||||
|
"sec": 1.49
|
||||||
|
},
|
||||||
|
"andrea|Ci si vede, Fabri.": {
|
||||||
|
"file": "andrea-d48ea04521.mp3",
|
||||||
|
"sec": 1.37
|
||||||
|
},
|
||||||
|
"bertovo|Da solo? Io gioco contro le sponde. E vinco quasi sempre.": {
|
||||||
|
"file": "bertovo-5e771069ff.mp3",
|
||||||
|
"sec": 5.67
|
||||||
|
},
|
||||||
|
"bertovo|Geometria. La biglia va dove le dici, basta chiederglielo per bene.": {
|
||||||
|
"file": "bertovo-914554e2ce.mp3",
|
||||||
|
"sec": 4.82
|
||||||
|
},
|
||||||
|
"bertovo|E con un po' di gesso.": {
|
||||||
|
"file": "bertovo-6c52f3fa0d.mp3",
|
||||||
|
"sec": 1.52
|
||||||
|
},
|
||||||
|
"bertovo|Guarda bene: prima lo tiro io, poi lo rifai te. Tre posizioni, una sponda.": {
|
||||||
|
"file": "bertovo-ad20035f2e.mp3",
|
||||||
|
"sec": 6.6
|
||||||
|
},
|
||||||
|
"bertovo|Le biglie le metto io, nascoste per bene. Tiri te, poi tiro io. Oh tirami questo rinquarto!?": {
|
||||||
|
"file": "bertovo-80c18678cd.mp3",
|
||||||
|
"sec": 8.82
|
||||||
|
},
|
||||||
|
"bertovo|Occhio alle sponde, fuori.": {
|
||||||
|
"file": "bertovo-4d4d630038.mp3",
|
||||||
|
"sec": 1.43
|
||||||
|
},
|
||||||
|
"andrea|Alla bona, una sponda e basta.": {
|
||||||
|
"file": "andrea-8b3082981a.mp3",
|
||||||
|
"sec": 2.47
|
||||||
|
},
|
||||||
|
"andrea|No, mi tremano le mani.": {
|
||||||
|
"file": "andrea-28610c8178.mp3",
|
||||||
|
"sec": 1.23
|
||||||
|
},
|
||||||
|
"bertovo|Una sponda. Roba da principianti, ma va bene.": {
|
||||||
|
"file": "bertovo-d14be7e027.mp3",
|
||||||
|
"sec": 3.6
|
||||||
|
},
|
||||||
|
"bertovo|Due sponde. Così mi piace.": {
|
||||||
|
"file": "bertovo-7e1952241d.mp3",
|
||||||
|
"sec": 2.55
|
||||||
|
},
|
||||||
|
"bertovo|Tre sponde e niente aiuti. Qui nascono le geometrie celestiali.": {
|
||||||
|
"file": "bertovo-b582e39e44.mp3",
|
||||||
|
"sec": 4.86
|
||||||
|
},
|
||||||
|
"bertovo|Il tavolo è qui. Le sponde non scappano.": {
|
||||||
|
"file": "bertovo-f0d200f449.mp3",
|
||||||
|
"sec": 2.92
|
||||||
|
},
|
||||||
"andrea|Ingegnere, una partita ai birilli?": {
|
"andrea|Ingegnere, una partita ai birilli?": {
|
||||||
"file": "andrea-dbcf4d1083.mp3",
|
"file": "andrea-dbcf4d1083.mp3",
|
||||||
"sec": 2.49
|
"sec": 2.49
|
||||||
@@ -2663,6 +2835,38 @@
|
|||||||
"file": "ing_tozzi-282062c119.mp3",
|
"file": "ing_tozzi-282062c119.mp3",
|
||||||
"sec": 3.64
|
"sec": 3.64
|
||||||
},
|
},
|
||||||
|
"bertovo|M'hai battuto ai rinquarti. Però un lo dire a nessuno.": {
|
||||||
|
"file": "bertovo-9aa247cffe.mp3",
|
||||||
|
"sec": 4.2
|
||||||
|
},
|
||||||
|
"bertovo|Le sponde ci sono per qualcosa! Basta chiederglielo per bene.": {
|
||||||
|
"file": "bertovo-5d93de3623.mp3",
|
||||||
|
"sec": 4.13
|
||||||
|
},
|
||||||
|
"andrea|Questo me lo tengo per la prossima.": {
|
||||||
|
"file": "andrea-c64bb3be5a.mp3",
|
||||||
|
"sec": 1.82
|
||||||
|
},
|
||||||
|
"bertovo|Visto? Geometrie celestiali. Si riprova quando vuoi.": {
|
||||||
|
"file": "bertovo-ca941bee91.mp3",
|
||||||
|
"sec": 5.41
|
||||||
|
},
|
||||||
|
"chiaru|Hai vinto te. Ma la mia aveva le ruote piene di gesso, sia chiaro.": {
|
||||||
|
"file": "chiaru-91a4b23b22.mp3",
|
||||||
|
"sec": 4.44
|
||||||
|
},
|
||||||
|
"chiaru|Visto? La blu non perde mai. Mai.": {
|
||||||
|
"file": "chiaru-f8afa97da1.mp3",
|
||||||
|
"sec": 4.95
|
||||||
|
},
|
||||||
|
"chiaru|Ti ritiri? Allora la vinco a tavolino.": {
|
||||||
|
"file": "chiaru-3e949c1519.mp3",
|
||||||
|
"sec": 4.06
|
||||||
|
},
|
||||||
|
"chiaru|Pari. Si rifà, ma la pista la ridisegno io.": {
|
||||||
|
"file": "chiaru-91c549bb83.mp3",
|
||||||
|
"sec": 3.39
|
||||||
|
},
|
||||||
"andrea|Laggiù, appoggiato al muro, c'è un angolo di tela dipinta: una strada bianca, e un omino con la maglia rossa e le braccia alzate.": {
|
"andrea|Laggiù, appoggiato al muro, c'è un angolo di tela dipinta: una strada bianca, e un omino con la maglia rossa e le braccia alzate.": {
|
||||||
"file": "andrea-6484641c58.mp3",
|
"file": "andrea-6484641c58.mp3",
|
||||||
"sec": 8.67
|
"sec": 8.67
|
||||||
|
|||||||
@@ -158,3 +158,20 @@ equitazione, minigolf. Risulta invece il circuito cittadino della «I Coppa di S
|
|||||||
11 giugno 1967: 5.600 m in senso orario attorno al poggio della Fortezza, partenza davanti alla scuola in Via
|
11 giugno 1967: 5.600 m in senso orario attorno al poggio della Fortezza, partenza davanti alla scuola in Via
|
||||||
Provinciale, con la strada di Massorondinaio asfaltata apposta come bretella (fonte: CAMET, «storia circuito San
|
Provinciale, con la strada di Massorondinaio asfaltata apposta come bretella (fonte: CAMET, «storia circuito San
|
||||||
Piero»). Il disegno della pista del campeggio va preso dai ricordi di Andrea **[DA ANDREA]**.
|
Piero»). Il disegno della pista del campeggio va preso dai ricordi di Andrea **[DA ANDREA]**.
|
||||||
|
|
||||||
|
## La pista col gesso delle Micro Machines (Andrea, 28 ottobre)
|
||||||
|
|
||||||
|
Sul parcheggio sotto casa, a Cardetole (le righe gialle), si disegnava col gesso la pista delle macchinine; si davano
|
||||||
|
spinte a turno e la macchinina doveva restare in pista: tutta fuori, si torna da dove si era partiti. Si gioca contro
|
||||||
|
Tozzino. Le macchinine: Micro Machines, le Formula 1 degli anni 80/90 con gli alettoni e i loro colori (senza marchi
|
||||||
|
né nomi veri). Proposta: vista dall'alto sull'asfalto, mira e spinta come al biliardo (riusarne la fisica), a turno,
|
||||||
|
vince chi taglia per primo il traguardo. Il quadro della pista e le macchinine da Codex (crediti finiti il 28: domani).
|
||||||
|
|
||||||
|
## Il libro di Fabrizio Berti: le cartoline dei vecchi posti (idea di Fabrizio Berti, 29 ottobre)
|
||||||
|
|
||||||
|
Fabrizio Berti (Bertovo nel gioco) ha scritto «Memorie di sport e di paese». Nel gioco il libro diventa un oggetto
|
||||||
|
dell'inventario: aprendolo, a seconda della scena in cui si è, compare la cartolina d'epoca di quel posto (San Piero
|
||||||
|
com'era). Si può riusare il primo piano del libro che c'è già (`libro-chiuso.png` / `libro-aperto.png`, effetto
|
||||||
|
`closeup`). Servono le cartoline **[DA ANDREA]** (dal libro, con il permesso dell'autore) e la scelta di quali scene ne
|
||||||
|
hanno una; dove non c'è, una battuta («Questo posto nel libro non c'è»). Come lo si ottiene: proposta, lo regala
|
||||||
|
Bertovo dopo una sfida ai rinquarti. Fabrizio Berti e il libro sono già nei Ringraziamenti.
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p
|
|||||||
| `polisportiva-*.png`, `ponte-sieve.png`, `area-feste.png`, `via-provinciale.png`, `comune-asilo.png`, `giardini-poste.png`, `stazione.png` | Codex | foto omonime in `assets/raw/foto/` | Andrea (EXIF, 23 ottobre 2026) | sì |
|
| `polisportiva-*.png`, `ponte-sieve.png`, `area-feste.png`, `via-provinciale.png`, `comune-asilo.png`, `giardini-poste.png`, `stazione.png` | Codex | foto omonime in `assets/raw/foto/` | Andrea (EXIF, 23 ottobre 2026) | sì |
|
||||||
| `via-del-pignone.png` | Codex | `via-del-pignone.png` (murale: `riferimenti/cistola.png`, `murale-maradona.png`) | Andrea | sì (Cistola consenziente) |
|
| `via-del-pignone.png` | Codex | `via-del-pignone.png` (murale: `riferimenti/cistola.png`, `murale-maradona.png`) | Andrea | sì (Cistola consenziente) |
|
||||||
| `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ì |
|
||||||
|
| `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 |
|
||||||
| `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,45 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
|||||||
|
|
||||||
## Dove siamo arrivati
|
## Dove siamo arrivati
|
||||||
|
|
||||||
|
- **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()`). [DA ANDREA] lo zio Mario che guarda
|
||||||
|
come tirarla fuori, e la battuta 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
|
||||||
|
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,
|
||||||
|
una spinta a testa (mira e carica come al biliardo), le macchinine di Codex (`assets/closeups/micro/`). Penalità
|
||||||
|
alla francese, per tutte si torna dov'eri: fuori, tagliè (curva presa dalla parte di dentro o erba saltata),
|
||||||
|
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
|
||||||
|
[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 in prestito dal GP (gara.mp3): [DA ANDREA] la sua, con Suno.
|
||||||
|
- **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`.
|
||||||
|
- **I rinquarti di Bertovo (28 ottobre, modifiche locali)**: variante del biliardo in `rinquarto.js` e
|
||||||
|
`rinquarto-render.js`, con fisica, mira, carica e suoni del biliardo. Sei posizioni riproducibili dal seme,
|
||||||
|
diretto coperto da una biglia e almeno una soluzione di sponda verificata con `simulate` per ciascuna.
|
||||||
|
Un tiro per posizione: punto al primo contatto col bersaglio dopo almeno 1/2/3 sponde (bona/seria/male),
|
||||||
|
senza prima toccare biglie viola o birilli. Guide: fino alla seconda sponda / 65 cm e prima sponda / nessuna;
|
||||||
|
vittoria con 3/3/2 punti su 6. Esito del tiro, contatore delle sponde e riepilogo finale persistente.
|
||||||
|
Integrati primo piano, pannello, aiuto e dialogo `bertovo` con scelta del livello; Bertovo resta senza sprite
|
||||||
|
e senza scena: **Andrea decide dove collocarlo**. Battute provvisorie segnate `[DA ANDREA]`.
|
||||||
|
Ripresa verificata con `node --test tests/*.test.mjs` (22 file, nessun rosso, comprese le voci già presenti)
|
||||||
|
e 14 test mirati in `tests/rinquarto.test.mjs`. Completata l'animazione dei birilli durante l'esito
|
||||||
|
e nel riepilogo finale: anche un urto appena prima dell'arresto termina la caduta senza cambiare punti o risultato.
|
||||||
|
Verifica visiva Playwright/SwiftShader ancora da eseguire: in questa sessione la sandbox impedisce l'avvio
|
||||||
|
del server locale e di Chromium (`Operation not permitted`). Nessun `make voci`, commit o pubblicazione.
|
||||||
|
|
||||||
- **Briscola all'oratorio (26 ottobre)**: alle tavolate siede «Il giocatore di carte» (segnaposto, [DA ANDREA: chi è],
|
- **Briscola all'oratorio (26 ottobre)**: alle tavolate siede «Il giocatore di carte» (segnaposto, [DA ANDREA: chi è],
|
||||||
sprite e battute provvisorie di Codex). Parlandoci si sceglie il mazzo (toscane o napoletane) e il livello
|
sprite e battute provvisorie di Codex). Parlandoci si sceglie il mazzo (toscane o napoletane) e il livello
|
||||||
(«Alla bona», «Seria», «A fassi male»), poi briscola a due fedele alle regole, conteggio delle prese a fine
|
(«Alla bona», «Seria», «A fassi male»), poi briscola a due fedele alle regole, conteggio delle prese a fine
|
||||||
@@ -256,11 +295,24 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso
|
|||||||
`assets/closeups/gp-fortezza.png` (prompt `prompts/closeups/gp-fortezza.txt`, guida dalla planimetria
|
`assets/closeups/gp-fortezza.png` (prompt `prompts/closeups/gp-fortezza.txt`, guida dalla planimetria
|
||||||
`assets/raw/fortezza-planimetria.png`, satellite in `assets/raw/fortezza-satellite.png`); il tracciato è ricalcato
|
`assets/raw/fortezza-planimetria.png`, satellite in `assets/raw/fortezza-satellite.png`); il tracciato è ricalcato
|
||||||
sulla strada dipinta. Disegno `gara-render.js`, test `tests/gara.test.mjs`. I piloti sono dati in
|
sulla strada dipinta. Disegno `gara-render.js`, test `tests/gara.test.mjs`. I piloti sono dati in
|
||||||
`src/story/piloti.js`: una riga per amico, `motore` e `testa` da 1 a 5, colori dalle foto. Corrono Puntella (SR,
|
`src/story/piloti.js`: una riga per amico, `motore` e `testa` da 1 a 5, colori dalle foto. Corrono Puntella (SR
|
||||||
colori Aprilia da GP provvisori), Cistola (CR 50 Z), Andrea (Malaguti Fifty rosso e nero), Tozzino (Booster blu
|
truccatissimo, motore 6, viola e giallo dalla sua foto, ritratto di Codex, testa «non pervenuta»), Bini (Andrea
|
||||||
scuro, di serie), l'Arcu (Betino). **Puntella (Marco Villani) non corre finché manca il suo consenso** (`consenso: null` in piloti.js: quando arriva, la data). Mancano: quanto erano
|
Bini, consenso del 28 ottobre: Italjet Formula giallo, motore 5, ritratto di Codex), Cistola (CR 50 Z), Andrea (Malaguti Fifty rosso e nero), Tozzino (Booster blu
|
||||||
|
scuro, di serie, casco alla Damon Hill di Codex: `prompts/personaggi/casco-tozzino.txt`), l'Arcu (Betino). Col telefono
|
||||||
|
si guida con un dito: premuto è gas, lo swipe indica dove andare sullo schermo (Codex, 28 ottobre). Puntella (Marco Villani) ha dato il consenso il 28 ottobre; chi ha `consenso: null` in piloti.js non corre. Mancano: quanto erano
|
||||||
truccati gli altri, le battute di Andrea, il rumore del motore (c'è solo
|
truccati gli altri, le battute di Andrea, il rumore del motore (c'è solo
|
||||||
quello dell'arrivo in griglia); i confini della paglia non si vedono sul quadro (revisione di Codex, punto 9).
|
quello dell'arrivo in griglia); i confini della paglia non si vedono sul quadro (revisione di Codex, punto 9).
|
||||||
|
- Strade attorno a casa (Andrea, 28 ottobre): la scena `area_camper` ora si chiama «Le villette del Giannini» (ci abita
|
||||||
|
lui; i camper stanno dietro il suo campo). Cardetole: in basso a destra «dal Giannini» (le villette), a destra il
|
||||||
|
ponticino della Carza, a sinistra il Trappola. Il campo del Giannini: a destra le villette, a sinistra il Trappola;
|
||||||
|
dal ponticino non ci si va. Via Liguri Magelli: lassù vicino al Betino si torna a casa, laggiù a destra il campo.
|
||||||
|
Da sapere: dove si gira, in fondo al ponticino, per Cardetole [DA ANDREA].
|
||||||
|
- Bertovo (Fabrizio Berti, consenso del 28 ottobre) al tavolo di mezzo del Circolo 4 Maggio, sprite di Codex dalla sua
|
||||||
|
foto (`prompts/personaggi/bertovo-sheet.txt`): parlandoci, i suoi rinquarti (`src/engine/rinquarto.js`). Dopo la
|
||||||
|
revisione di Codex: gara a punti (sulla stessa posizione tira prima Andrea, poi il tavolo torna com'era e tira
|
||||||
|
Bertovo, preciso secondo il livello), allenamento («Fammi vedere un rinquarto»: tira prima lui), posizioni misurate
|
||||||
|
(finestra di angoli buoni sempre più stretta), forza sempre piena, traiettoria con le sponde numerate e Prossima,
|
||||||
|
tocco che mira senza tirare, un dito solo per volta, frecce che girano finché tenute.
|
||||||
- Via Liguri Magelli (Street View di Andrea, 28 ottobre), a sinistra dal campo del Giannini: a destra la casa salmone
|
- Via Liguri Magelli (Street View di Andrea, 28 ottobre), a sinistra dal campo del Giannini: a destra la casa salmone
|
||||||
con la tenda del Trappola (il Barrettino, «Vai dal Trappola»: niente interno, [DA ANDREA]), l'Arcu sotto la tenda
|
con la tenda del Trappola (il Barrettino, «Vai dal Trappola»: niente interno, [DA ANDREA]), l'Arcu sotto la tenda
|
||||||
(scherzo: «ti ho visto du' minuti fa a Campomigliaio» col Betino, poi la sfida del GP), davanti la targa «Qui riposa la
|
(scherzo: «ti ho visto du' minuti fa a Campomigliaio» col Betino, poi la sfida del GP), davanti la targa «Qui riposa la
|
||||||
@@ -279,6 +331,7 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso
|
|||||||
6. Il nonno: confermare il nome (Settimo Villani) e decidere come usare la sua leggenda nel finale.
|
6. Il nonno: confermare il nome (Settimo Villani) e decidere come usare la sua leggenda nel finale.
|
||||||
7. Il Lele: come e quanto farlo comparire (con calma).
|
7. Il Lele: come e quanto farlo comparire (con calma).
|
||||||
8. Battute e modi di dire del paese.
|
8. Battute e modi di dire del paese.
|
||||||
|
9. ~~Consenso del Chiaru~~ Dato il 29 ottobre. Le sue battute vanno bene?
|
||||||
|
|
||||||
### Grafica
|
### Grafica
|
||||||
- Sfondi puliti 16:9 di Pieve, Cardetole, Campomigliaio, interno della Fortezza (sotterranei).
|
- Sfondi puliti 16:9 di Pieve, Cardetole, Campomigliaio, interno della Fortezza (sotterranei).
|
||||||
|
|||||||
@@ -192,6 +192,10 @@
|
|||||||
</section>
|
</section>
|
||||||
<section id="bocce-controls" hidden aria-label="Comandi delle bocce">
|
<section id="bocce-controls" hidden aria-label="Comandi delle bocce">
|
||||||
<p id="bocce-status" role="status"></p>
|
<p id="bocce-status" role="status"></p>
|
||||||
|
<!-- Il GP di Fortezza sul telefono: lo swipe qui, così il dito non copre la pista. -->
|
||||||
|
<div id="gara-pad" hidden role="application" aria-label="Zona dello swipe: tieni premuto per il gas e trascina verso dove vuoi andare">
|
||||||
|
Tieni premuto qui per il gas e trascina verso dove vuoi andare
|
||||||
|
</div>
|
||||||
<div class="bocce-adjust">
|
<div class="bocce-adjust">
|
||||||
<button id="bocce-left" type="button" aria-label="Sposta la mira a sinistra">← Mira</button>
|
<button id="bocce-left" type="button" aria-label="Sposta la mira a sinistra">← Mira</button>
|
||||||
<label for="bocce-power">Forza <output id="bocce-power-value">50%</output>
|
<label for="bocce-power">Forza <output id="bocce-power-value">50%</output>
|
||||||
@@ -221,10 +225,21 @@
|
|||||||
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. Tieni premuto sulla scena (o Spazio, o ↑) per il
|
<p>Il GP di Fortezza: tre giri attorno alle mura coi cinquantini. <b>Alla bona e Seria</b> la guida è semplice:
|
||||||
gas; sterza con le frecce ← →, oppure tenendo premuto e trascinando di lato col dito o col mouse, oppure tenendo
|
tieni premuto sulla scena (o Spazio, o il pulsante Gas) per accelerare e lascia per frenare; lo sterzo segue la
|
||||||
premuti i pulsanti Sterza. ↓ (o il pulsante Frena, tenuto premuto) frena, anche col gas dato. Dal pannello il
|
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 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>
|
||||||
|
<p>Con la tastiera: Spazio, Invio o ↑ per il gas, ← → per sterzare; oppure tieni premuti i pulsanti Sterza.
|
||||||
|
↓ (o il pulsante Frena, tenuto premuto) frena, anche col gas dato. Dal pannello il
|
||||||
pulsante alterna Gas e Molla. Durante la presentazione dei piloti un tocco passa al prossimo.</p>
|
pulsante alterna Gas e Molla. Durante la presentazione dei piloti un tocco passa al prossimo.</p>
|
||||||
|
<p>Sul telefono lo swipe si fa anche nel riquadro sotto la pista, così il dito non la copre. Il cursore
|
||||||
|
Sensibilità decide quanto sterza lo swipe: parte basso, alzalo se il motorino gira poco.</p>
|
||||||
<p>Le frecce gialle in terra sono acceleratori; le lattine rosse di miscela danno uno sprint (e ricompaiono); nelle
|
<p>Le frecce gialle in terra sono acceleratori; le lattine rosse di miscela danno uno sprint (e ricompaiono); nelle
|
||||||
buche, se ci passi forte, caschi. Fuori strada l'erba frena, e nella paglia ci si ferma. Alla bona la pista mostra
|
buche, se ci passi forte, caschi. Fuori strada l'erba frena, e nella paglia ci si ferma. Alla bona la pista mostra
|
||||||
in rosso dove frenare, Seria solo i coni, Ginocchio in terra niente. Esc esce.</p>
|
in rosso dove frenare, Seria solo i coni, Ginocchio in terra niente. Esc esce.</p>
|
||||||
@@ -240,6 +255,32 @@
|
|||||||
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="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
|
||||||
|
lui). La tua macchinina è la rossa. Vince chi taglia per primo il traguardo a scacchi; se ci arrivate tutti e due
|
||||||
|
nello stesso giro di spinte, vince chi è andato più in là.</p>
|
||||||
|
<p>Se durante la corsa la macchinina esce <b>tutta</b> dal gesso, torna dov'era prima della spinta: vale anche se
|
||||||
|
ce la manda l'altra urtandola, e vale per i tornanti, dove non si salta dall'altra parte. Alla bona vedi dove si
|
||||||
|
ferma la spinta (verde dentro, rossa fuori), Seria solo la direzione, A fassi male una freccina. Il Chiaru è più
|
||||||
|
preciso a ogni livello.</p>
|
||||||
|
<p>Muovi il mouse o tocca il parcheggio per mirare: un tocco fissa solo la mira. Per spingere tieni premuto (o
|
||||||
|
Spazio) finché il dito si carica e lascia, oppure regola la forza e premi Spingi. Frecce ← → o i pulsanti Mira
|
||||||
|
girano la mira (Maiusc per i passi fini); Esc esce.</p>
|
||||||
|
</details>
|
||||||
|
<details id="rinquarto-help" hidden><summary>Come si gioca</summary>
|
||||||
|
<p>I rinquarti di Trappizzio Berti: sei posizioni. Su ognuna tiri prima tu, poi il tavolo torna com'era e tira Trappizzio Berti; un
|
||||||
|
punto per ogni rinquarto riuscito, vince chi ne fa di più (si può anche pareggiare). La tua biglia è la bianca, il
|
||||||
|
bersaglio la gialla con l'anello. Le biglie viola con la croce e i birilli sono disturbi: toccarne uno prima del
|
||||||
|
bersaglio annulla il tiro. La diretta è sempre coperta, ma c'è sempre un rinquarto possibile.</p>
|
||||||
|
<p>Punto se la bianca tocca la gialla dopo almeno le sponde richieste: conta il primo contatto. Alla bona una
|
||||||
|
sponda e la guida fino al secondo rimbalzo, Seria due sponde e la guida corta, A fassi male tre sponde e niente
|
||||||
|
guida. Le posizioni si fanno più strette man mano, e Trappizzio Berti è più preciso a ogni livello. La forza è sempre piena:
|
||||||
|
è un rompicapo di angoli. Nell'allenamento («Fammi vedere un rinquarto») tira prima lui, poi lo rifai tu.</p>
|
||||||
|
<p>Muovi il mouse o tocca il tavolo per mirare: un tocco fissa solo la mira. Per tirare premi Tira, oppure tieni
|
||||||
|
premuto sul tavolo (o Spazio) finché la stecca si carica e lascia. Frecce ← → o i pulsanti Mira (tenuti
|
||||||
|
premuti) girano la stecca, sempre più svelti; con Maiusc a passi fini. Dopo il tiro la traiettoria resta sul tavolo
|
||||||
|
con le sponde numerate e il punto del contatto: tocca o premi Prossima per andare avanti. Esc esce.</p>
|
||||||
|
</details>
|
||||||
<details id="biliardo-help" hidden><summary>Come si gioca</summary>
|
<details id="biliardo-help" hidden><summary>Come si gioca</summary>
|
||||||
<button class="tutorial-open" type="button" data-tutorial="biliardo" hidden>Guarda il video</button>
|
<button class="tutorial-open" type="button" data-tutorial="biliardo" hidden>Guarda il video</button>
|
||||||
<p>Clic o tocco sul tavolo fissa la mira: puoi spostarti sui comandi senza cambiarla.
|
<p>Clic o tocco sul tavolo fissa la mira: puoi spostarti sui comandi senza cambiarla.
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "leggenda-del-regolo",
|
"name": "leggenda-del-regolo",
|
||||||
"version": "0.1.31",
|
"version": "0.1.44",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --test tests/*.test.mjs",
|
"test": "node --test tests/*.test.mjs",
|
||||||
"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:gara": "node tests/gara.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",
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export function tablePatches() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ombra rivolta verso l'esterno rispetto alla lampada centrale.
|
// Ombra rivolta verso l'esterno rispetto alla lampada centrale.
|
||||||
function sphere(ctx, role, x, y, r, shadow = true) {
|
export function sphere(ctx, role, x, y, r, shadow = true) {
|
||||||
const [dark, mid, light] = BALL[role];
|
const [dark, mid, light] = BALL[role] ?? ['#43285f', '#bd83ef', '#f7e4ff'];
|
||||||
const lx = shadow ? (sx(LEN / 2) - x) / (LEN * VIEW.k / 2) : -0.6;
|
const lx = shadow ? (sx(LEN / 2) - x) / (LEN * VIEW.k / 2) : -0.6;
|
||||||
const ly = shadow ? (sy(WID / 2) - y) / (WID * VIEW.k / 2) - 0.25 : -0.6;
|
const ly = shadow ? (sy(WID / 2) - y) / (WID * VIEW.k / 2) - 0.25 : -0.6;
|
||||||
if (shadow) {
|
if (shadow) {
|
||||||
@@ -50,7 +50,7 @@ export function pinFall(p, time) {
|
|||||||
if (p.visualDownAt === undefined) return 1;
|
if (p.visualDownAt === undefined) return 1;
|
||||||
return Math.min(1, Math.max(0, (time - p.visualDownAt) / 0.24));
|
return Math.min(1, Math.max(0, (time - p.visualDownAt) / 0.24));
|
||||||
}
|
}
|
||||||
function pin(ctx, p, time) {
|
export function pin(ctx, p, time) {
|
||||||
const x = sx(p.x), y = sy(p.y), fall = pinFall(p, time);
|
const x = sx(p.x), y = sy(p.y), fall = pinFall(p, time);
|
||||||
const tilt = Math.sin(fall * Math.PI / 2), lift = Math.cos(fall * Math.PI / 2);
|
const tilt = Math.sin(fall * Math.PI / 2), lift = Math.cos(fall * Math.PI / 2);
|
||||||
const tx = x + Math.cos(p.dir ?? 0) * 7 * tilt;
|
const tx = x + Math.cos(p.dir ?? 0) * 7 * tilt;
|
||||||
@@ -65,7 +65,7 @@ function pin(ctx, p, time) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let lighting;
|
let lighting;
|
||||||
function table(ctx, bg) {
|
export function table(ctx, bg) {
|
||||||
if (bg) for (const patch of tablePatches()) ctx.drawImage(bg, ...patch);
|
if (bg) for (const patch of tablePatches()) ctx.drawImage(bg, ...patch);
|
||||||
else {
|
else {
|
||||||
ctx.fillStyle = '#21170f'; ctx.fillRect(0, 0, 640, 360);
|
ctx.fillStyle = '#21170f'; ctx.fillRect(0, 0, 640, 360);
|
||||||
@@ -117,7 +117,7 @@ function aimGuide(ctx, c) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cue(ctx, x, y, angle, back, alpha = 1) {
|
export function cue(ctx, x, y, angle, back, alpha = 1) {
|
||||||
ctx.save(); ctx.globalAlpha = alpha;
|
ctx.save(); ctx.globalAlpha = alpha;
|
||||||
ctx.translate(sx(x), sy(y)); ctx.rotate(angle);
|
ctx.translate(sx(x), sy(y)); ctx.rotate(angle);
|
||||||
ctx.fillStyle = '#25170e'; ctx.fillRect(-back - 150, -2, 150, 5);
|
ctx.fillStyle = '#25170e'; ctx.fillRect(-back - 150, -2, 150, 5);
|
||||||
@@ -206,7 +206,7 @@ export function drawBiliardo(ctx, c, def, bg, sheet) {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
scoreboard(ctx, c, def);
|
scoreboard(ctx, c, def);
|
||||||
const hint = canThrow(c) ? predict(c).hit : null;
|
const hint = canThrow(c) ? predict(c).hit : null;
|
||||||
const instruction = hint === 'pin' ? 'Attenzione: la tua biglia va sui birilli (fallo).'
|
const instruction = c.holding ? c.charging ? 'Carica iniziata: rilascia per tirare.' : 'Tieni 0,6 s per caricare; un tocco fissa la mira.' : hint === 'pin' ? 'Attenzione: la tua biglia va sui birilli (fallo).'
|
||||||
: hint === 'pallino' ? 'Attenzione: tocchi prima il pallino (fallo).'
|
: hint === 'pallino' ? 'Attenzione: tocchi prima il pallino (fallo).'
|
||||||
: c.aimLocked ? 'Mira fissata: un altro clic la sposta.' : 'La stecca segue il mouse: clic per fissare la mira.';
|
: c.aimLocked ? 'Mira fissata: un altro clic la sposta.' : 'La stecca segue il mouse: clic per fissare la mira.';
|
||||||
ctx.fillStyle = '#17160fd9'; ctx.fillRect(54, 53, 532, 15);
|
ctx.fillStyle = '#17160fd9'; ctx.fillRect(54, 53, 532, 15);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const MIN_SPEED = 50, MAX_SPEED = 520, FRICTION = 70, DRAG = 0.35, CUSHIO
|
|||||||
export const SWING = 1.4, FIXED_STEP = 1 / 240;
|
export const SWING = 1.4, FIXED_STEP = 1 / 240;
|
||||||
// La steccata: la stecca va avanti e tocca la biglia; solo allora parte la fisica (e il suono del colpo).
|
// La steccata: la stecca va avanti e tocca la biglia; solo allora parte la fisica (e il suono del colpo).
|
||||||
// Così il disegno mostra sempre le posizioni vere e i suoni degli urti cadono insieme alle immagini.
|
// Così il disegno mostra sempre le posizioni vere e i suoni degli urti cadono insieme alle immagini.
|
||||||
export const WINDUP = 0.18;
|
export const WINDUP = 0.18, LONG_PRESS = 0.6;
|
||||||
const EPS = 0.5, REST = 0.6, CPU_THINK = 0.2, CPU_SHOT = 1.5;
|
const EPS = 0.5, REST = 0.6, CPU_THINK = 0.2, CPU_SHOT = 1.5;
|
||||||
const CONTACT_EPS = 1e-7, BALL_RESTITUTION = 0.95;
|
const CONTACT_EPS = 1e-7, BALL_RESTITUTION = 0.95;
|
||||||
export const OTHER = { player: 'cpu', cpu: 'player' };
|
export const OTHER = { player: 'cpu', cpu: 'player' };
|
||||||
@@ -20,7 +20,7 @@ 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; }
|
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
|
||||||
const deviation = c => random(c) + random(c) + random(c) - 1.5;
|
const deviation = c => random(c) + random(c) + random(c) - 1.5;
|
||||||
const ball = (role, x, y) => ({ role, x, y, vx: 0, vy: 0 });
|
const ball = (role, x, y) => ({ role, x, y, vx: 0, vy: 0 });
|
||||||
const list = c => [c.balls.player, c.balls.cpu, c.balls.pallino];
|
const list = c => Object.values(c.balls);
|
||||||
const angleTo = (a, b) => Math.atan2(b.y - a.y, b.x - a.x);
|
const angleTo = (a, b) => Math.atan2(b.y - a.y, b.x - a.x);
|
||||||
|
|
||||||
// Il castello: il rosso in mezzo al tavolo, i quattro bianchi a croce intorno.
|
// Il castello: il rosso in mezzo al tavolo, i quattro bianchi a croce intorno.
|
||||||
@@ -30,7 +30,10 @@ export function castle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const powerAt = time => clamp(time / SWING, 0, 1);
|
export const powerAt = time => clamp(time / SWING, 0, 1);
|
||||||
export const speedFor = power => MIN_SPEED + (MAX_SPEED - MIN_SPEED) * power;
|
// Il tiro più forte: al biliardo dell'Ing. Tozzi si arriva a STRONG (Andrea, 29 ottobre: «più potenza»); i rinquarti
|
||||||
|
// restano a MAX_SPEED, perché le loro posizioni sono misurate a quella forza.
|
||||||
|
export const STRONG = 720;
|
||||||
|
export const speedFor = (power, max = MAX_SPEED) => MIN_SPEED + (max - MIN_SPEED) * power;
|
||||||
// Quanta strada fa una biglia lanciata a velocità v, senza urti (attrito costante più un po' di resistenza).
|
// Quanta strada fa una biglia lanciata a velocità v, senza urti (attrito costante più un po' di resistenza).
|
||||||
export function reach(v) {
|
export function reach(v) {
|
||||||
const k = FRICTION / DRAG, t = Math.log((v + k) / k) / DRAG;
|
const k = FRICTION / DRAG, t = Math.log((v + k) / k) / DRAG;
|
||||||
@@ -48,7 +51,7 @@ export const LEVELS = {
|
|||||||
};
|
};
|
||||||
export function newBiliardo(seed = Date.now() >>> 0, def = {}) {
|
export function newBiliardo(seed = Date.now() >>> 0, def = {}) {
|
||||||
const level = LEVELS[def.level] ? def.level : 'seria';
|
const level = LEVELS[def.level] ? def.level : 'seria';
|
||||||
const c = { step: 'aim', t: 0, seed, level, skill: LEVELS[level], current: 'player', target: def.target ?? TARGET, power: 0.45, angle: 0,
|
const c = { step: 'aim', t: 0, seed, level, skill: LEVELS[level], current: 'player', target: def.target ?? TARGET, maxSpeed: def.maxSpeed ?? STRONG, power: 0.45, angle: 0,
|
||||||
holding: false, holdTime: 0, accumulator: 0, events: [], points: { player: 0, cpu: 0 }, last: null, shot: null, plan: null };
|
holding: false, holdTime: 0, accumulator: 0, events: [], points: { player: 0, cpu: 0 }, last: null, shot: null, plan: null };
|
||||||
c.balls = { player: ball('player', LEN * 0.18, WID * 0.5), cpu: ball('cpu', LEN * 0.81, WID * 0.25), pallino: ball('pallino', LEN * 0.88, WID * 0.7) };
|
c.balls = { player: ball('player', LEN * 0.18, WID * 0.5), cpu: ball('cpu', LEN * 0.81, WID * 0.25), pallino: ball('pallino', LEN * 0.88, WID * 0.7) };
|
||||||
c.pins = castle();
|
c.pins = castle();
|
||||||
@@ -169,8 +172,9 @@ function step(w, dt) {
|
|||||||
w.events.push({ kind: hit >= rail ? 'biliardo-hit' : 'biliardo-rail', power: Math.min(1, Math.max(hit, rail) / 400) });
|
w.events.push({ kind: hit >= rail ? 'biliardo-hit' : 'biliardo-rail', power: Math.min(1, Math.max(hit, rail) / 400) });
|
||||||
}
|
}
|
||||||
|
|
||||||
function strike(c, who, angle, power) {
|
export function strike(c, who, angle, power) {
|
||||||
const b = c.balls[who], v = speedFor(power);
|
c.turn = null;
|
||||||
|
const b = c.balls[who], v = speedFor(power, c.maxSpeed);
|
||||||
c.visual.shot = { at: c.visual.time, angle, power, x: b.x, y: b.y };
|
c.visual.shot = { at: c.visual.time, angle, power, x: b.x, y: b.y };
|
||||||
c.pending = { ball: b, vx: Math.cos(angle) * v, vy: Math.sin(angle) * v, power };
|
c.pending = { ball: b, vx: Math.cos(angle) * v, vy: Math.sin(angle) * v, power };
|
||||||
c.windup = WINDUP;
|
c.windup = WINDUP;
|
||||||
@@ -179,12 +183,12 @@ function strike(c, who, angle, power) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Il colpo provato a vuoto, su una copia del tavolo: serve all'avversario per scegliere.
|
// Il colpo provato a vuoto, su una copia del tavolo: serve all'avversario per scegliere.
|
||||||
export function simulate(c, who, angle, power) {
|
export function simulate(c, who, angle, power, { onContact, stop } = {}) {
|
||||||
const balls = list(c).map(b => ({ ...b })), pins = c.pins.map(p => ({ ...p }));
|
const balls = list(c).map(b => ({ ...b })), pins = c.pins.map(p => ({ ...p }));
|
||||||
const w = { balls, pins, shot: { shooter: who, first: null, pallino: false, pins: [] } };
|
const w = { balls, pins, onContact, shot: { shooter: who, first: null, pallino: false, pins: [] } };
|
||||||
const b = balls.find(x => x.role === who), v = speedFor(power);
|
const b = balls.find(x => x.role === who), v = speedFor(power, c.maxSpeed);
|
||||||
b.vx = Math.cos(angle) * v; b.vy = Math.sin(angle) * v;
|
b.vx = Math.cos(angle) * v; b.vy = Math.sin(angle) * v;
|
||||||
for (let i = 0; i < 15 / FIXED_STEP && balls.some(x => x.vx || x.vy); i++) step(w, FIXED_STEP);
|
for (let i = 0; i < 15 / FIXED_STEP && !stop?.() && balls.some(x => x.vx || x.vy); i++) step(w, FIXED_STEP);
|
||||||
return judge(w.shot);
|
return judge(w.shot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,12 +241,20 @@ function cpuThink(c, budget = 12) {
|
|||||||
c.angle = c.plan.angle; c.power = c.plan.power; c.thinking = null;
|
c.angle = c.plan.angle; c.power = c.plan.power; c.thinking = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function update(c, dt) {
|
export function update(c, dt, onContact) {
|
||||||
if (!Number.isFinite(dt) || dt <= 0) return;
|
if (!Number.isFinite(dt) || dt <= 0) return;
|
||||||
dt = Math.min(dt, 0.25); c.t += dt;
|
dt = Math.min(dt, 0.25); c.t += dt;
|
||||||
c.visual.time += dt;
|
c.visual.time += dt;
|
||||||
if (c.last) c.last.age += dt;
|
if (c.last) c.last.age += dt;
|
||||||
if (c.holding) { c.holdTime += dt; c.power = powerAt(c.holdTime); }
|
if (c.turn && canThrow(c) && !c.holding) {
|
||||||
|
c.turn.time += dt;
|
||||||
|
c.angle += c.turn.dir * dt * (c.turn.fine ? 0.03 : 0.6 + 1.8 * Math.min(1, c.turn.time / 1.2));
|
||||||
|
}
|
||||||
|
if (c.holding) {
|
||||||
|
c.holdTime += dt;
|
||||||
|
c.charging = c.holdTime >= c.holdDelay;
|
||||||
|
if (c.charging) c.power = c.fixedPower ?? (c.holdDelay ? Math.min(1, c.previousPower + powerAt(c.holdTime - c.holdDelay)) : powerAt(c.holdTime));
|
||||||
|
}
|
||||||
if (c.step === 'rolling' && c.windup > 0) {
|
if (c.step === 'rolling' && c.windup > 0) {
|
||||||
// La stecca sta arrivando: la biglia parte quando la tocca; il resto del fotogramma è già moto.
|
// La stecca sta arrivando: la biglia parte quando la tocca; il resto del fotogramma è già moto.
|
||||||
c.windup -= dt;
|
c.windup -= dt;
|
||||||
@@ -255,7 +267,7 @@ export function update(c, dt) {
|
|||||||
}
|
}
|
||||||
if (c.step === 'rolling' && !c.windup) {
|
if (c.step === 'rolling' && !c.windup) {
|
||||||
c.accumulator += dt;
|
c.accumulator += dt;
|
||||||
const w = { balls: list(c), pins: c.pins, shot: c.shot, events: c.events };
|
const w = { balls: list(c), pins: c.pins, shot: c.shot, events: c.events, onContact };
|
||||||
while (c.accumulator >= FIXED_STEP) { step(w, FIXED_STEP); c.accumulator -= FIXED_STEP; }
|
while (c.accumulator >= FIXED_STEP) { step(w, FIXED_STEP); c.accumulator -= FIXED_STEP; }
|
||||||
if (!moving(c)) { c.step = 'settle'; c.t = 0; }
|
if (!moving(c)) { c.step = 'settle'; c.t = 0; }
|
||||||
} else if (c.step === 'settle' && c.t >= REST) finish(c);
|
} else if (c.step === 'settle' && c.t >= REST) finish(c);
|
||||||
@@ -288,18 +300,40 @@ export function setPower(c, value) {
|
|||||||
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 canThrow(c) && !c.holding && strike(c, 'player', c.angle, c.power); }
|
||||||
export function press(c) {
|
// Una pressione continua ruota senza dipendere dalla ripetizione della tastiera.
|
||||||
if (!canThrow(c) || c.holding) return false;
|
export function turn(c, dir, owner, fine = false) {
|
||||||
c.previousPower = c.power; c.holding = true; c.holdTime = c.power = 0; return true;
|
if (!canThrow(c) || c.holding || c.turn) return false;
|
||||||
|
c.turn = { dir, owner, fine, time: 0 }; c.angle += dir * (fine ? 0.0005 : 0.03);
|
||||||
|
c.aimLocked = true; return true;
|
||||||
}
|
}
|
||||||
export function cancel(c) {
|
export function stopTurn(c, owner) {
|
||||||
|
if (c.turn?.owner !== owner) return false;
|
||||||
|
c.turn = null; return true;
|
||||||
|
}
|
||||||
|
export function key(c, key, down, fine = false) {
|
||||||
|
if (!['ArrowLeft', 'ArrowRight'].includes(key)) return false;
|
||||||
|
if (down) { if (c.turn?.owner === key) c.turn.fine = fine; else turn(c, key === 'ArrowLeft' ? -1 : 1, key, fine); }
|
||||||
|
else stopTurn(c, key);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function press(c, pointerId) {
|
||||||
|
if (!canThrow(c) || c.holding) return false;
|
||||||
|
c.turn = null; c.pointerId = pointerId; c.holdDelay = pointerId === undefined ? 0 : LONG_PRESS;
|
||||||
|
c.previousPower = c.power; c.holding = true; c.charging = false; c.holdTime = 0;
|
||||||
|
if (!c.holdDelay && c.fixedPower === undefined) c.power = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function cancel(c, pointerId) {
|
||||||
|
if (pointerId !== undefined && c.pointerId !== pointerId) return false;
|
||||||
|
c.turn = null; c.charging = false;
|
||||||
if (!c.holding) return false;
|
if (!c.holding) return false;
|
||||||
c.holding = false; c.power = c.previousPower; c.holdTime = 0; return true;
|
c.holding = false; c.power = c.previousPower; c.holdTime = 0; return true;
|
||||||
}
|
}
|
||||||
export function release(c) {
|
export function release(c, pointerId) {
|
||||||
|
if (c.pointerId !== pointerId) return false;
|
||||||
if (!c.holding) return false;
|
if (!c.holding) return false;
|
||||||
if (c.holdTime < 0.14) return cancel(c); // un tocco breve mira soltanto
|
if (c.holdTime < Math.max(0.14, c.holdDelay)) return cancel(c, pointerId); // il tocco mira soltanto
|
||||||
c.holding = false; return strike(c, 'player', c.angle, c.power);
|
c.holding = c.charging = false; return strike(c, 'player', c.angle, c.power);
|
||||||
}
|
}
|
||||||
|
|
||||||
// La guida riproduce il tiro su una copia, con gli stessi urti e lo stesso attrito.
|
// La guida riproduce il tiro su una copia, con gli stessi urti e lo stesso attrito.
|
||||||
@@ -312,7 +346,7 @@ export function predict(c) {
|
|||||||
const cached = predictions.get(c);
|
const cached = predictions.get(c);
|
||||||
if (cached?.key === key) return cached.result;
|
if (cached?.key === key) return cached.result;
|
||||||
const balls = list(c).map(b => ({ ...b })), me = balls.find(b => b.role === c.current);
|
const balls = list(c).map(b => ({ ...b })), me = balls.find(b => b.role === c.current);
|
||||||
const v = speedFor(c.power);
|
const v = speedFor(c.power, c.maxSpeed);
|
||||||
me.vx = Math.cos(c.angle) * v; me.vy = Math.sin(c.angle) * v;
|
me.vx = Math.cos(c.angle) * v; me.vy = Math.sin(c.angle) * v;
|
||||||
const result = { points: [[me.x, me.y]], ghost: null, hit: null, dir: null, outgoing: [] };
|
const result = { points: [[me.x, me.y]], ghost: null, hit: null, dir: null, outgoing: [] };
|
||||||
const tracks = [];
|
const tracks = [];
|
||||||
@@ -363,6 +397,7 @@ export function status(c, names) {
|
|||||||
if (c.step === 'result') return `Partita a ${names[c.result.winner]}: ${c.points.player} a ${c.points.cpu}.`;
|
if (c.step === 'result') return `Partita a ${names[c.result.winner]}: ${c.points.player} a ${c.points.cpu}.`;
|
||||||
if (c.step === 'rolling') return 'Biglie in movimento…';
|
if (c.step === 'rolling') return 'Biglie in movimento…';
|
||||||
if (c.step === 'settle') return 'Si contano i birilli…';
|
if (c.step === 'settle') return 'Si contano i birilli…';
|
||||||
|
if (c.holding) return c.charging ? 'Carica iniziata: rilascia per tirare.' : 'Mira fissata. Tieni 0,6 s per iniziare la carica.';
|
||||||
const last = lastLine(c, names);
|
const last = lastLine(c, names);
|
||||||
return `${last ? last + ' ' : ''}${names[c.current]} al tiro. (${c.points.player} a ${c.points.cpu}, si va a ${c.target})`;
|
return `${last ? last + ' ' : ''}${names[c.current]} al tiro. (${c.points.player} a ${c.points.cpu}, si va a ${c.target})`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
// Senza verbo scelto vale il clic intelligente: l'azione più sensata per quel punto.
|
// Senza verbo scelto vale il clic intelligente: l'azione più sensata per quel punto.
|
||||||
import { VERB_LABELS } from './game.js';
|
import { VERB_LABELS } from './game.js';
|
||||||
|
|
||||||
// «Vai a» davanti a un nome che comincia con «dal», «dalla»…: «Vai dal Trappola», non «Vai a dal Trappola».
|
// «Vai a» davanti a un nome che dice già dove («dal Trappola», «a destra, …», «verso …», «indietro, …»): solo «Vai».
|
||||||
const verbLabel = (verb, name) => verb === 'walk' && /^da(l|lla|i|gli|ll')?\b/i.test(name ?? '') ? 'Vai' : VERB_LABELS[verb];
|
const WHERE = /^(da(l|lla|i|gli|ll')?|a|verso|indietro|su|giù|in|laggiù|lassù|dietro|oltre)(?=[\s,]|$)/i;
|
||||||
|
const verbLabel = (verb, name) => verb === 'walk' && WHERE.test(name ?? '') ? 'Vai' : VERB_LABELS[verb];
|
||||||
|
|
||||||
export class Command {
|
export class Command {
|
||||||
constructor(game) {
|
constructor(game) {
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
import { findPath, dist, inBox, boxDistance, nearestInside } from './geometry.js';
|
import { findPath, dist, inBox, boxDistance, nearestInside } from './geometry.js';
|
||||||
import * as bocce from './bocce.js';
|
import * as bocce from './bocce.js';
|
||||||
import * as biliardo from './biliardo.js';
|
import * as biliardo from './biliardo.js';
|
||||||
|
import * as rinquarto from './rinquarto.js';
|
||||||
import * as basket from './basket.js';
|
import * as basket from './basket.js';
|
||||||
import * as pesca from './pesca.js';
|
import * as pesca from './pesca.js';
|
||||||
import * as briscola from './briscola.js';
|
import * as briscola from './briscola.js';
|
||||||
import * as tressette from './tressette.js';
|
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';
|
||||||
|
|
||||||
// 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 = {
|
||||||
@@ -17,8 +19,10 @@ export const MINIGAMES = {
|
|||||||
scopa: { create: (seed, level, deck) => scopa.newScopa(seed, { level, deck }), update: scopa.update, play: scopa.play },
|
scopa: { create: (seed, level, deck) => scopa.newScopa(seed, { level, deck }), update: scopa.update, play: scopa.play },
|
||||||
bocce: { create: (seed, level) => bocce.newBocce(seed, { level }), update: bocce.update, press: bocce.press, release: bocce.release, cancel: bocce.cancel, aim: bocce.aimScreen },
|
bocce: { create: (seed, level) => bocce.newBocce(seed, { level }), update: bocce.update, press: bocce.press, release: bocce.release, cancel: bocce.cancel, aim: bocce.aimScreen },
|
||||||
biliardo: { create: (seed, level) => biliardo.newBiliardo(seed, { level }), update: biliardo.update, press: biliardo.press, release: biliardo.release, cancel: biliardo.cancel, aim: biliardo.aimScreen },
|
biliardo: { create: (seed, level) => biliardo.newBiliardo(seed, { level }), update: biliardo.update, press: biliardo.press, release: biliardo.release, cancel: biliardo.cancel, aim: biliardo.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 },
|
||||||
|
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 },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -158,6 +162,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); }
|
||||||
@@ -311,9 +318,9 @@ export class Game {
|
|||||||
|
|
||||||
// Bocce e biliardo: si tiene premuto per caricare la forza e si lascia per tirare (mouse, dito o spazio).
|
// Bocce e biliardo: si tiene premuto per caricare la forza e si lascia per tirare (mouse, dito o spazio).
|
||||||
minigame() { const c = this.closeup; return c ? MINIGAMES[this.story.closeups[c.id].type] ?? null : null; }
|
minigame() { const c = this.closeup; return c ? MINIGAMES[this.story.closeups[c.id].type] ?? null : null; }
|
||||||
closeupPress() { const m = this.minigame(); return !!m && !this.speech && !!m.press?.(this.closeup); }
|
closeupPress(pointerId) { const m = this.minigame(); return !!m && !this.speech && !!m.press?.(this.closeup, pointerId); }
|
||||||
closeupRelease() { return !!this.minigame()?.release?.(this.closeup); }
|
closeupRelease(pointerId) { return !!this.minigame()?.release?.(this.closeup, pointerId); }
|
||||||
closeupCancel() { return !!this.minigame()?.cancel?.(this.closeup); }
|
closeupCancel(pointerId) { return !!this.minigame()?.cancel?.(this.closeup, pointerId); }
|
||||||
closeupPlay(index) { return !this.speech && !!this.minigame()?.play?.(this.closeup, index); }
|
closeupPlay(index) { return !this.speech && !!this.minigame()?.play?.(this.closeup, index); }
|
||||||
|
|
||||||
// Solo luce, nessun clic sugli oggetti dietro al primo piano. Il progresso è transitorio:
|
// Solo luce, nessun clic sugli oggetti dietro al primo piano. Il progresso è transitorio:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TRACK, CURVES, PICKUPS, BRAKE, LAPS, COUNTDOWN, ROAD, VERGE, CAN, 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';
|
||||||
|
|
||||||
@@ -92,7 +92,8 @@ function moped(ctx, r, c) {
|
|||||||
ctx.fillStyle = r.color; ctx.fillRect(-4, -2, 9, 4);
|
ctx.fillStyle = r.color; ctx.fillRect(-4, -2, 9, 4);
|
||||||
ctx.fillStyle = r.trim ?? '#1c1c1c'; ctx.fillRect(-4, -0.5, 9, 1);
|
ctx.fillStyle = r.trim ?? '#1c1c1c'; ctx.fillRect(-4, -0.5, 9, 1);
|
||||||
ctx.fillStyle = '#2a2a2a'; ctx.fillRect(4, -3, 1, 6); // il manubrio
|
ctx.fillStyle = '#2a2a2a'; ctx.fillRect(4, -3, 1, 6); // il manubrio
|
||||||
ctx.fillStyle = r.id === 'andrea' ? '#f4e3c2' : '#2a2a2a'; ctx.beginPath(); ctx.arc(-1, 0, 2, 0, Math.PI * 2); ctx.fill(); // il casco
|
ctx.fillStyle = r.helmet ?? (r.id === 'andrea' ? '#f4e3c2' : '#2a2a2a'); ctx.beginPath(); ctx.arc(-1, 0, 2, 0, Math.PI * 2); ctx.fill(); // il casco
|
||||||
|
if (r.stripe) { ctx.fillStyle = r.stripe; ctx.fillRect(-2, -0.5, 2, 1); }
|
||||||
// La miscela: una fiammata dietro.
|
// La miscela: una fiammata dietro.
|
||||||
if (r.boost > 0 && Math.floor(c.age * 20) % 2) { ctx.fillStyle = '#ffb030'; ctx.fillRect(-10, -1, 3, 2); }
|
if (r.boost > 0 && Math.floor(c.age * 20) % 2) { ctx.fillStyle = '#ffb030'; ctx.fillRect(-10, -1, 3, 2); }
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
@@ -113,6 +114,21 @@ function hud(ctx, c) {
|
|||||||
ctx.fillStyle = me.boost > 0 ? '#ffb030' : k > 0.9 ? '#e0583a' : '#b9d392'; ctx.fillRect(10, bar, Math.round(50 * k), 5);
|
ctx.fillStyle = me.boost > 0 ? '#ffb030' : k > 0.9 ? '#e0583a' : '#b9d392'; ctx.fillRect(10, bar, Math.round(50 * k), 5);
|
||||||
text(ctx, `${Math.round(me.v * 0.5)} km/h`, 116, bar + 3, 6, '#f4e3c2', 'right');
|
text(ctx, `${Math.round(me.v * 0.5)} km/h`, 116, bar + 3, 6, '#f4e3c2', 'right');
|
||||||
}
|
}
|
||||||
|
// L'origine mobile dello swipe e la direzione scelta col dito (o col mouse premuto).
|
||||||
|
function swipe(ctx, c) {
|
||||||
|
if (c.pointer === null || !c.anchor || !c.finger) return;
|
||||||
|
const [x, y] = c.anchor, [fx, fy] = c.finger, dx = fx - x, dy = fy - y, length = Math.hypot(dx, dy);
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = 'rgba(244,227,194,0.55)'; ctx.lineWidth = 1.5; ctx.lineCap = 'round';
|
||||||
|
ctx.beginPath(); ctx.arc(x, y, 4, 0, Math.PI * 2); ctx.stroke();
|
||||||
|
if (length >= SWIPE.dead) {
|
||||||
|
const ux = dx / length, uy = dy / length;
|
||||||
|
ctx.beginPath(); ctx.moveTo(x + ux * 6, y + uy * 6); ctx.lineTo(fx, fy);
|
||||||
|
ctx.moveTo(fx - ux * 6 - uy * 3, fy - uy * 6 + ux * 3); ctx.lineTo(fx, fy);
|
||||||
|
ctx.lineTo(fx - ux * 6 + uy * 3, fy - uy * 6 - ux * 3); ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
// La presentazione: il cartellino di chi arriva. A sinistra il personaggio (lo sprite del gioco, o il ritratto), a
|
// La presentazione: il cartellino di chi arriva. A sinistra il personaggio (lo sprite del gioco, o il ritratto), a
|
||||||
// destra il suo motorino dipinto, sotto quanto va (motore) e quanto rischia (testa). `look(src)` dà l'immagine caricata,
|
// destra il suo motorino dipinto, sotto quanto va (motore) e quanto rischia (testa). `look(src)` dà l'immagine caricata,
|
||||||
// `sheet(id)` lo sprite di un personaggio.
|
// `sheet(id)` lo sprite di un personaggio.
|
||||||
@@ -139,6 +155,9 @@ function card(ctx, c, look, sheet) {
|
|||||||
panel(ctx, x, y, w, h);
|
panel(ctx, x, y, w, h);
|
||||||
text(ctx, 'GP DI FORTEZZA', x + w / 2, y + 10, 6, '#ffe28a', 'center');
|
text(ctx, 'GP DI FORTEZZA', x + w / 2, y + 10, 6, '#ffe28a', 'center');
|
||||||
figure(ctx, r, x + 46, y + 14, look, sheet);
|
figure(ctx, r, x + 46, y + 14, look, sheet);
|
||||||
|
// Il casco di chi ce l'ha disegnato, accanto al personaggio.
|
||||||
|
const casco = r.casco && look(r.casco);
|
||||||
|
if (casco) { const k = 30 / Math.max(casco.width, casco.height); ctx.drawImage(casco, Math.round(x + 66), Math.round(y + 20), Math.round(casco.width * k), Math.round(casco.height * k)); }
|
||||||
// Il motorino, entra da sinistra e si ferma (come in pista).
|
// Il motorino, entra da sinistra e si ferma (come in pista).
|
||||||
const moto = r.moto && look(r.moto), t = Math.min(1, (c.t % INTRO.each) / 0.6), e = 1 - (1 - t) ** 3;
|
const moto = r.moto && look(r.moto), t = Math.min(1, (c.t % INTRO.each) / 0.6), e = 1 - (1 - t) ** 3;
|
||||||
if (moto) {
|
if (moto) {
|
||||||
@@ -155,7 +174,9 @@ function card(ctx, c, look, sheet) {
|
|||||||
for (const [k, [label, value]] of [['MOTORE', r.motore], ['TESTA', r.testa]].entries()) {
|
for (const [k, [label, value]] of [['MOTORE', r.motore], ['TESTA', r.testa]].entries()) {
|
||||||
const yy = y + 116 + k * 12;
|
const yy = y + 116 + k * 12;
|
||||||
text(ctx, label, x + 222, yy, 5, '#c5c6b6');
|
text(ctx, label, x + 222, yy, 5, '#c5c6b6');
|
||||||
for (let i = 0; i < 5; i++) { ctx.fillStyle = i < value ? (k ? '#b9d392' : '#e0583a') : '#3a3f3c'; ctx.fillRect(x + 262 + i * 13, yy - 3, 10, 6); }
|
// Cinque caselle; il motore truccatissimo (6) ne accende una sesta, gialla. Per qualcuno la testa è una scritta.
|
||||||
|
if (k && r.testaNota) { text(ctx, r.testaNota, x + 262, yy, 5, '#e0583a'); continue; }
|
||||||
|
for (let i = 0; i < Math.max(5, value); i++) { ctx.fillStyle = i >= 5 ? '#ffe28a' : i < value ? (k ? '#b9d392' : '#e0583a') : '#3a3f3c'; ctx.fillRect(x + 262 + i * 12, yy - 3, 9, 6); }
|
||||||
}
|
}
|
||||||
text(ctx, 'TOCCA: IL PROSSIMO', x + w / 2, y + h - 6, 5, '#8f9488', 'center');
|
text(ctx, 'TOCCA: IL PROSSIMO', x + w / 2, y + h - 6, 5, '#8f9488', 'center');
|
||||||
}
|
}
|
||||||
@@ -195,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);
|
||||||
@@ -204,12 +231,17 @@ export function drawGara(ctx, c, def, bg, look = () => null, sheet = () => null)
|
|||||||
// Prima chi è dietro, così chi è davanti resta sopra; Andrea sempre sopra a tutti.
|
// Prima chi è dietro, così chi è davanti resta sopra; Andrea sempre sopra a tutti.
|
||||||
for (const r of [...standings(c)].reverse().sort((a, b) => (a.id === 'andrea') - (b.id === 'andrea'))) if (r.shown !== false) moped(ctx, r, c);
|
for (const r of [...standings(c)].reverse().sort((a, b) => (a.id === 'andrea') - (b.id === 'andrea'))) if (r.shown !== false) moped(ctx, r, c);
|
||||||
if (c.step === 'intro') { card(ctx, c, look, sheet); ctx.restore(); return; }
|
if (c.step === 'intro') { card(ctx, c, look, sheet); ctx.restore(); return; }
|
||||||
|
if (c.step === 'race' || c.step === 'start') swipe(ctx, c);
|
||||||
hud(ctx, c);
|
hud(ctx, c);
|
||||||
if (c.step === 'race') position(ctx, c);
|
if (c.step === 'race') position(ctx, c);
|
||||||
if (c.step === 'start') lights(ctx, c);
|
if (c.step === 'start') lights(ctx, c);
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// la planimetria generale 1:500 che ha mandato Andrea (`assets/raw/fortezza-planimetria.png`), schiacciata nel
|
// la planimetria generale 1:500 che ha mandato Andrea (`assets/raw/fortezza-planimetria.png`), schiacciata nel
|
||||||
// formato del gioco (640x360): x = 90 + (x0 - 215)·0,7, y = 20 + (y0 - 42)·0,49.
|
// formato del gioco (640x360): x = 90 + (x0 - 215)·0,7, y = 20 + (y0 - 42)·0,49.
|
||||||
//
|
//
|
||||||
// Si tiene premuto per il gas (mouse, dito, Spazio o ↑), si sterza con ← → (o trascinando di lato col dito, o coi
|
// Si tiene premuto per il gas (mouse, dito, Spazio o ↑), si sterza con ← → (o indicando la direzione con uno swipe, o coi
|
||||||
// pulsanti), ↓ frena. Sulla pista: acceleratori, buche (andandoci forte si casca) e lattine di miscela da raccogliere.
|
// pulsanti), ↓ frena. Sulla pista: acceleratori, buche (andandoci forte si casca) e lattine di miscela da raccogliere.
|
||||||
// Fuori strada c'è l'erba che frena, e poi la paglia: chi ci arriva forte si ferma.
|
// Fuori strada c'è l'erba che frena, e poi la paglia: chi ci arriva forte si ferma.
|
||||||
|
|
||||||
@@ -29,15 +29,20 @@ export const LAPS = 3, STEP = 2, ROAD = 9, VERGE = 15, COUNTDOWN = 3, FIXED_STEP
|
|||||||
export const INTRO = { each: 4, ride: 1.8, from: 110, after: 1 };
|
export const INTRO = { each: 4, ride: 1.8, from: 110, after: 1 };
|
||||||
// Sterzo (rad/s a velocità piena di sterzo), freno, rallentamento senza gas, e quanto si scivola di lato in curva.
|
// Sterzo (rad/s a velocità piena di sterzo), freno, rallentamento senza gas, e quanto si scivola di lato in curva.
|
||||||
export const TURN = 3.4, BRAKE = 150, COAST = 45, GRIP = 9, SLIDE = 0.5;
|
export const TURN = 3.4, BRAKE = 150, COAST = 45, GRIP = 9, SLIDE = 0.5;
|
||||||
|
// Lo swipe: zona morta e portata in pixel della scena; la sensibilità (0..1, cursore del pannello) va dal guadagno
|
||||||
|
// `soft` al `hard`, e lo sterzo del dito insegue quello voluto a `follow` al secondo (niente scatti). Sul telefono
|
||||||
|
// la prima versione era ingiocabile: si parte poco sensibili.
|
||||||
|
export const SWIPE = { dead: 14, reach: 60, soft: 1.2, hard: 3, follow: 7, sens: 0.3 };
|
||||||
export const BOOST = { speed: 1.35, time: 1.2 }, CAN = { time: 1.6, respawn: 6 }, HOLE = { fast: 45, out: 1.1 };
|
export const BOOST = { speed: 1.35, time: 1.2 }, CAN = { time: 1.6, respawn: 6 }, HOLE = { fast: 45, out: 1.1 };
|
||||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||||
const wrapAngle = a => Math.atan2(Math.sin(a), Math.cos(a));
|
const wrapAngle = a => Math.atan2(Math.sin(a), Math.cos(a));
|
||||||
|
|
||||||
// I piloti sono dati (story.closeups.gara.riders, da piloti.js): `motore` da 1 (di serie) a 5 (truccato da far
|
// I piloti sono dati (story.closeups.gara.riders, da piloti.js): `motore` da 1 (di serie) a 5 (truccato da far
|
||||||
// paura), `testa` da 1 (matto) a 5 (prudente). Ne vengono velocità massima e accelerazione in px/s, tenuta, e per gli
|
// paura, 6 truccatissimo), `testa` da 1 (matto) a 5 (prudente). Ne vengono velocità massima e accelerazione in px/s, tenuta, e per gli
|
||||||
// altri quanto si fidano delle curve (`caution`) e quanto spesso rischiano troppo (`slip`).
|
// altri quanto si fidano delle curve (`caution`) e quanto spesso rischiano troppo (`slip`).
|
||||||
export function rider({ motore = 3, testa = 3, ...r }) {
|
export function rider({ motore = 3, testa = 3, ...r }) {
|
||||||
const m = clamp(motore, 1, 5), t = clamp(testa, 1, 5);
|
// Il 6 è il truccatissimo: lo SR di Puntella (Andrea, 28 ottobre).
|
||||||
|
const m = clamp(motore, 1, 6), t = clamp(testa, 1, 5);
|
||||||
return { ...r, motore: m, testa: t, vmax: 74 + 6.5 * m, accel: 40 + 5 * m, grip: 1.1 - 0.03 * m, caution: 1.0025 - 0.0125 * t, slip: 0.4275 - 0.0775 * t };
|
return { ...r, motore: m, testa: t, vmax: 74 + 6.5 * m, accel: 40 + 5 * m, grip: 1.1 - 0.03 * m, caution: 1.0025 - 0.0125 * t, slip: 0.4275 - 0.0775 * t };
|
||||||
}
|
}
|
||||||
// Il gruppo di prova, quando la storia non ne dà uno.
|
// Il gruppo di prova, quando la storia non ne dà uno.
|
||||||
@@ -45,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.78, 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;
|
||||||
@@ -149,17 +162,19 @@ 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,
|
pointer: null, toggle: false, brakeButton: false, drag: 0, buttons: 0, anchor: null, finger: null, sens: SWIPE.sens,
|
||||||
power: 0, 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: {} };
|
||||||
}
|
}
|
||||||
export const player = c => c.riders.find(r => r.id === 'andrea');
|
export const player = c => c.riders.find(r => r.id === 'andrea');
|
||||||
export const lapOf = r => clamp(Math.floor(Math.max(0, r.s) / TRACK.length) + 1, 1, LAPS);
|
export const lapOf = r => clamp(Math.floor(Math.max(0, r.s) / TRACK.length) + 1, 1, LAPS);
|
||||||
export const canThrow = c => c.step === 'intro' || c.step === 'start' || c.step === 'race';
|
export const canThrow = c => c.step === 'intro' || c.step === 'start' || c.step === 'race';
|
||||||
export const canAdjust = c => c.step === 'race'; // lo sterzo del pannello
|
// Lo sterzo e il freno del pannello, e il cursore della sensibilità (regolabile già prima del via).
|
||||||
|
export const canAdjust = c => c.step !== 'result';
|
||||||
export const aimFree = () => true;
|
export const aimFree = () => true;
|
||||||
export const nudge = () => false;
|
export const nudge = () => false;
|
||||||
export const setPower = () => false;
|
// Il cursore del pannello, nella gara, è la sensibilità dello sterzo col dito.
|
||||||
|
export function setPower(c, value) { c.sens = c.power = clamp(Number(value) || 0, 0, 1); return true; }
|
||||||
|
|
||||||
// Il gas ha più fonti, ognuna con la sua: i tasti tenuti giù (Spazio, Invio, ↑), il dito o il mouse premuto sulla
|
// Il gas ha più fonti, ognuna con la sua: i tasti tenuti giù (Spazio, Invio, ↑), il dito o il mouse premuto sulla
|
||||||
// scena (uno solo, col suo pointerId) e il pulsante Gas del pannello, che resta acceso finché non lo si ritocca.
|
// scena (uno solo, col suo pointerId) e il pulsante Gas del pannello, che resta acceso finché non lo si ritocca.
|
||||||
@@ -167,7 +182,7 @@ export const setPower = () => false;
|
|||||||
function sync(c) {
|
function sync(c) {
|
||||||
c.holding = c.keys.gas.size > 0 || c.pointer !== null || c.toggle;
|
c.holding = c.keys.gas.size > 0 || c.pointer !== null || c.toggle;
|
||||||
c.braking = c.keys.brake || c.brakeButton;
|
c.braking = c.keys.brake || c.brakeButton;
|
||||||
if (c.pointer === null) c.drag = 0;
|
if (c.pointer === null) { c.drag = 0; c.anchor = null; c.finger = null; }
|
||||||
}
|
}
|
||||||
// Il primo tocco (o tasto) salta la presentazione, senza dare gas.
|
// Il primo tocco (o tasto) salta la presentazione, senza dare gas.
|
||||||
export function press(c, id = 'press') {
|
export function press(c, id = 'press') {
|
||||||
@@ -209,12 +224,14 @@ function place(r, s, shown) {
|
|||||||
export const presenting = c => c.step === 'intro' ? c.riders[Math.floor(c.t / INTRO.each)] ?? null : null;
|
export const presenting = c => c.step === 'intro' ? c.riders[Math.floor(c.t / INTRO.each)] ?? null : null;
|
||||||
// Il pulsante Gas del pannello: un tocco lo accende, un altro lo spegne (per chi non può tenere premuto).
|
// Il pulsante Gas del pannello: un tocco lo accende, un altro lo spegne (per chi non può tenere premuto).
|
||||||
export function shoot(c) { if (!canThrow(c)) return false; if (c.step === 'intro') return nextIntro(c); c.toggle = !c.toggle; sync(c); return true; }
|
export function shoot(c) { if (!canThrow(c)) return false; if (c.step === 'intro') return nextIntro(c); c.toggle = !c.toggle; sync(c); return true; }
|
||||||
// Il dito o il mouse premuto sulla scena: quanto ci si sposta di lato da dove si è premuto è lo sterzo. Il mouse che
|
// Lo swipe indica dove andare sullo schermo. L'origine segue il dito oltre i 60 px.
|
||||||
// passa senza premere, o col gas dato da tastiera o dal pannello, non sterza.
|
export function aimScreen(c, [x, y], id = 'press') {
|
||||||
export function aimScreen(c, [x], id = 'press') {
|
if (!Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||||
if (!Number.isFinite(x)) return false;
|
if (c.pointer !== null && c.pointer !== id) return false;
|
||||||
if (c.pointer === null) c.anchor = x;
|
if (c.pointer === null || !c.anchor) c.anchor = [x, y];
|
||||||
else if (c.pointer === id) c.drag = clamp((x - c.anchor) / 28, -1, 1);
|
c.finger = [x, y];
|
||||||
|
const dx = x - c.anchor[0], dy = y - c.anchor[1], distance = Math.hypot(dx, dy);
|
||||||
|
if (distance > SWIPE.reach) c.anchor = [x - dx * SWIPE.reach / distance, y - dy * SWIPE.reach / distance];
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// La tastiera: ← → sterzano, ↑ Spazio e Invio sono il gas, ↓ il freno; finché si tengono giù.
|
// La tastiera: ← → sterzano, ↑ Spazio e Invio sono il gas, ↓ il freno; finché si tengono giù.
|
||||||
@@ -233,16 +250,91 @@ export function key(c, name, down) {
|
|||||||
// I pulsanti del pannello tenuti premuti: Sterza (-1, 0, 1) e Frena.
|
// I pulsanti del pannello tenuti premuti: Sterza (-1, 0, 1) e Frena.
|
||||||
export function steerButton(c, dir) { c.buttons = dir; return true; }
|
export function steerButton(c, dir) { c.buttons = dir; return true; }
|
||||||
export function brakeButton(c, down) { c.brakeButton = down; sync(c); return true; }
|
export function brakeButton(c, down) { c.brakeButton = down; sync(c); return true; }
|
||||||
export const steering = c => clamp((c.keys.right ? 1 : 0) - (c.keys.left ? 1 : 0) + c.buttons + c.drag, -1, 1);
|
export function steering(c) {
|
||||||
|
let want = 0;
|
||||||
|
if (c.pointer !== null && c.anchor && c.finger) {
|
||||||
|
const dx = c.finger[0] - c.anchor[0], dy = c.finger[1] - c.anchor[1];
|
||||||
|
const gain = SWIPE.soft + (SWIPE.hard - SWIPE.soft) * (c.sens ?? SWIPE.sens);
|
||||||
|
// Si ricalcola anche col dito fermo: il motorino intanto cambia direzione.
|
||||||
|
if (Math.hypot(dx, dy) >= SWIPE.dead) want = clamp(wrapAngle(Math.atan2(dy, dx) - player(c).a) * gain, -1, 1);
|
||||||
|
}
|
||||||
|
// Lo sterzo del dito si avvicina a quello voluto un po' per volta (a ogni passo fisso), e si azzera subito alzandolo.
|
||||||
|
c.drag = c.pointer === null ? 0 : c.drag + (want - c.drag) * Math.min(1, SWIPE.follow * FIXED_STEP);
|
||||||
|
if (Math.abs(want - c.drag) < 1e-6) c.drag = want;
|
||||||
|
return clamp((c.keys.right ? 1 : 0) - (c.keys.left ? 1 : 0) + c.buttons + c.drag, -1, 1);
|
||||||
|
}
|
||||||
// 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;
|
||||||
@@ -262,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;
|
||||||
@@ -286,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 });
|
||||||
}
|
}
|
||||||
@@ -385,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}.`;
|
||||||
@@ -393,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';
|
||||||
|
|||||||