Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bec7f067bc | ||
|
|
e50a3e80e9 | ||
|
|
afb0b931c5 | ||
|
|
fb1568259b | ||
|
|
c1049bf69d | ||
|
|
b05fe5b302 | ||
|
|
505eda01a3 | ||
|
|
d329cbd6ef | ||
|
|
1e4dcaaf3b | ||
|
|
4c5ec099d0 | ||
|
|
b2ec499590 | ||
|
|
75b532cda0 | ||
|
|
68686efe03 | ||
|
|
208e1d9535 | ||
|
|
f2a0f22f4a | ||
|
|
dea124933e | ||
|
|
51a46fe5a0 | ||
|
|
f6fb8922af | ||
|
|
bd7fbc3003 | ||
|
|
8b957c7cf7 | ||
|
|
f1de62f01e | ||
|
|
64dc8e18e7 | ||
|
|
b262977117 |
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"image": "andrea-basket.png",
|
||||||
|
"height": 120,
|
||||||
|
"frames": {
|
||||||
|
"hold": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 51,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 19.1,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"load": {
|
||||||
|
"x": 53,
|
||||||
|
"y": 0,
|
||||||
|
"w": 62,
|
||||||
|
"h": 106,
|
||||||
|
"ax": 32.9,
|
||||||
|
"ay": 106
|
||||||
|
},
|
||||||
|
"shoot": {
|
||||||
|
"x": 117,
|
||||||
|
"y": 0,
|
||||||
|
"w": 69,
|
||||||
|
"h": 124,
|
||||||
|
"ax": 30.0,
|
||||||
|
"ay": 124
|
||||||
|
},
|
||||||
|
"cheer": {
|
||||||
|
"x": 188,
|
||||||
|
"y": 0,
|
||||||
|
"w": 59,
|
||||||
|
"h": 118,
|
||||||
|
"ax": 26.5,
|
||||||
|
"ay": 118
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"image": "andrea-pesca.png",
|
||||||
|
"height": 120,
|
||||||
|
"frames": {
|
||||||
|
"hold": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 101,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 31.2,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"cast": {
|
||||||
|
"x": 103,
|
||||||
|
"y": 0,
|
||||||
|
"w": 145,
|
||||||
|
"h": 89,
|
||||||
|
"ax": 56.1,
|
||||||
|
"ay": 89
|
||||||
|
},
|
||||||
|
"reel": {
|
||||||
|
"x": 250,
|
||||||
|
"y": 0,
|
||||||
|
"w": 102,
|
||||||
|
"h": 107,
|
||||||
|
"ax": 32.5,
|
||||||
|
"ay": 107
|
||||||
|
},
|
||||||
|
"show": {
|
||||||
|
"x": 354,
|
||||||
|
"y": 0,
|
||||||
|
"w": 129,
|
||||||
|
"h": 113,
|
||||||
|
"ax": 87.9,
|
||||||
|
"ay": 113
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 233 KiB |
@@ -211,9 +211,9 @@
|
|||||||
"file": "andrea-22b8efe94b.mp3",
|
"file": "andrea-22b8efe94b.mp3",
|
||||||
"sec": 1.75
|
"sec": 1.75
|
||||||
},
|
},
|
||||||
"pescatori|Si preparano le canne: domattina si va a pescare al lago dell'Anatre, dietro Cardetole!": {
|
"pescatori|Si preparano le canne: si va a pescare al lago dell'Anatre, dietro Cardetole!": {
|
||||||
"file": "pescatori-486103d189.mp3",
|
"file": "pescatori-e7edfae41f.mp3",
|
||||||
"sec": 3.74
|
"sec": 3.51
|
||||||
},
|
},
|
||||||
"pescatori|Guarda questa: la lenza la regge anche una carpa di dieci chili.": {
|
"pescatori|Guarda questa: la lenza la regge anche una carpa di dieci chili.": {
|
||||||
"file": "pescatori-afda722e0c.mp3",
|
"file": "pescatori-afda722e0c.mp3",
|
||||||
@@ -543,9 +543,9 @@
|
|||||||
"file": "andrea-5e37f70197.mp3",
|
"file": "andrea-5e37f70197.mp3",
|
||||||
"sec": 5.35
|
"sec": 5.35
|
||||||
},
|
},
|
||||||
"andrea|Il campo da basket del Circolo, lassù oltre la rete. Prima o poi due tiri ce li faccio.": {
|
"andrea|Il campo da basket del Circolo, lassù oltre la rete. Due tiri ce li faccio sempre volentieri.": {
|
||||||
"file": "andrea-8cd2b5a36d.mp3",
|
"file": "andrea-3c4ee692ad.mp3",
|
||||||
"sec": 6.49
|
"sec": 7.55
|
||||||
},
|
},
|
||||||
"andrea|La fermata della corriera. Da qui si va dappertutto. Se passa.": {
|
"andrea|La fermata della corriera. Da qui si va dappertutto. Se passa.": {
|
||||||
"file": "andrea-ccd735c926.mp3",
|
"file": "andrea-ccd735c926.mp3",
|
||||||
@@ -707,6 +707,42 @@
|
|||||||
"file": "andrea-60f39f0dd7.mp3",
|
"file": "andrea-60f39f0dd7.mp3",
|
||||||
"sec": 6.48
|
"sec": 6.48
|
||||||
},
|
},
|
||||||
|
"andrea|L'edicola. Qui compravo le cassette per il Commodore 64: trenta giochi a mille lire, diceva la copertina.": {
|
||||||
|
"file": "andrea-a8283c5640.mp3",
|
||||||
|
"sec": 8.27
|
||||||
|
},
|
||||||
|
"andrea|Si infilava la cassetta, si scriveva LOAD, si premeva play... e si aspettava dieci minuti col registratore che cigolava.": {
|
||||||
|
"file": "andrea-753071634e.mp3",
|
||||||
|
"sec": 8.67
|
||||||
|
},
|
||||||
|
"andrea|Poi, quando andava bene, partiva il gioco. Quando andava male: LOAD ERROR. E si riavvolgeva tutto. Bei tempi.": {
|
||||||
|
"file": "andrea-c99539e242.mp3",
|
||||||
|
"sec": 10.4
|
||||||
|
},
|
||||||
|
"andrea|Chiusa. E comunque le cassette non le fanno più. Neanche i Commodore.": {
|
||||||
|
"file": "andrea-4fb258d12c.mp3",
|
||||||
|
"sec": 5.79
|
||||||
|
},
|
||||||
|
"andrea|Vorrei un Topolino e trenta giochi a mille lire. ...Niente, parlo con una serranda.": {
|
||||||
|
"file": "andrea-0ad7d35251.mp3",
|
||||||
|
"sec": 5.97
|
||||||
|
},
|
||||||
|
"andrea|La banca. Il bancomat funziona solo il mercoledì delle settimane pari, negli anni bisestili, quando Venere si allinea a Plutone.": {
|
||||||
|
"file": "andrea-01433de83e.mp3",
|
||||||
|
"sec": 9.25
|
||||||
|
},
|
||||||
|
"andrea|Oggi? Not today.": {
|
||||||
|
"file": "andrea-a698d91684.mp3",
|
||||||
|
"sec": 2.87
|
||||||
|
},
|
||||||
|
"andrea|Chiusa. Come il bancomat, del resto.": {
|
||||||
|
"file": "andrea-f508d2ed4c.mp3",
|
||||||
|
"sec": 3.69
|
||||||
|
},
|
||||||
|
"andrea|Carta dentro, carta fuori. «Servizio momentaneamente non disponibile». Momentaneamente dal 1998.": {
|
||||||
|
"file": "andrea-d482267e5d.mp3",
|
||||||
|
"sec": 9.3
|
||||||
|
},
|
||||||
"andrea|Cistola. Il talento di via del Pignone.": {
|
"andrea|Cistola. Il talento di via del Pignone.": {
|
||||||
"file": "andrea-2b47355c2c.mp3",
|
"file": "andrea-2b47355c2c.mp3",
|
||||||
"sec": 3.81
|
"sec": 3.81
|
||||||
@@ -739,6 +775,98 @@
|
|||||||
"file": "andrea-7fd73fbb77.mp3",
|
"file": "andrea-7fd73fbb77.mp3",
|
||||||
"sec": 2.66
|
"sec": 2.66
|
||||||
},
|
},
|
||||||
|
"andrea|Le colonnine con le catene: le macchine di qua non passano.": {
|
||||||
|
"file": "andrea-7967c3d476.mp3",
|
||||||
|
"sec": 4.05
|
||||||
|
},
|
||||||
|
"andrea|Io sì. Scavalcare le catene è uno sport che pratico da quando avevo sei anni.": {
|
||||||
|
"file": "andrea-db2e00019a.mp3",
|
||||||
|
"sec": 6.2
|
||||||
|
},
|
||||||
|
"andrea|Una tenda a righe bianche e rosse. Fa tanto spiaggia: peccato che il mare sia di là dall'Appennino.": {
|
||||||
|
"file": "andrea-b8038d49dc.mp3",
|
||||||
|
"sec": 6.68
|
||||||
|
},
|
||||||
|
"andrea|Il muro di sasso con l'edera e la ringhiera in cima. Lassù c'è il verde della villa.": {
|
||||||
|
"file": "andrea-1ec3557612.mp3",
|
||||||
|
"sec": 5.8
|
||||||
|
},
|
||||||
|
"andrea|Via dell'Antica Posta. Chissà se la posta antica arrivava prima di quella moderna.": {
|
||||||
|
"file": "andrea-c0f51c8219.mp3",
|
||||||
|
"sec": 6.36
|
||||||
|
},
|
||||||
|
"andrea|La torre dell'orologio, vista dal retro. Anche da qui segna un'ora tutta sua.": {
|
||||||
|
"file": "andrea-a196260e7c.mp3",
|
||||||
|
"sec": 5.34
|
||||||
|
},
|
||||||
|
"andrea|La porta del Comune. Dal retro, ma sempre Comune.": {
|
||||||
|
"file": "andrea-66cab2e717.mp3",
|
||||||
|
"sec": 4.23
|
||||||
|
},
|
||||||
|
"andrea|Chiusa. Anche dalla porta di dietro, senza un modulo non si entra.": {
|
||||||
|
"file": "andrea-835648f0b6.mp3",
|
||||||
|
"sec": 4.54
|
||||||
|
},
|
||||||
|
"andrea|Una scultura moderna, lunga e appuntita. Secondo me è un'antenna per parlare coi marziani.": {
|
||||||
|
"file": "andrea-d7c8824e13.mp3",
|
||||||
|
"sec": 7.13
|
||||||
|
},
|
||||||
|
"andrea|Questa sembra un sasso. Ma sta su un piedistallo, quindi è arte.": {
|
||||||
|
"file": "andrea-d8a1173809.mp3",
|
||||||
|
"sec": 4.91
|
||||||
|
},
|
||||||
|
"andrea|Pesa più di me. E poi l'arte non si porta via in tasca.": {
|
||||||
|
"file": "andrea-51f23d7ac6.mp3",
|
||||||
|
"sec": 4.15
|
||||||
|
},
|
||||||
|
"andrea|Un vasone di fiori. Qualcuno se ne prende cura, al Comune.": {
|
||||||
|
"file": "andrea-1fa2d33c2f.mp3",
|
||||||
|
"sec": 4.49
|
||||||
|
},
|
||||||
|
"andrea|Due gatti che dormono sull'erba. Il Palio, per loro, può anche aspettare.": {
|
||||||
|
"file": "andrea-1c8aae44f9.mp3",
|
||||||
|
"sec": 6.01
|
||||||
|
},
|
||||||
|
"andrea|Micio, micio... Niente. Per meno di una scatoletta non aprono neanche un occhio.": {
|
||||||
|
"file": "andrea-7ea38df174.mp3",
|
||||||
|
"sec": 6.41
|
||||||
|
},
|
||||||
|
"andrea|Svegliare un gatto che dorme al sole? Non sono così coraggioso.": {
|
||||||
|
"file": "andrea-17ccb76f0c.mp3",
|
||||||
|
"sec": 4.55
|
||||||
|
},
|
||||||
|
"andrea|Due panchine all'ombra del muro. Un posto per chi non ha un palio da ritrovare.": {
|
||||||
|
"file": "andrea-8136bec81c.mp3",
|
||||||
|
"sec": 4.95
|
||||||
|
},
|
||||||
|
"andrea|Mi siederei volentieri, ma il Palio non si ritrova da solo.": {
|
||||||
|
"file": "andrea-735b8b0920.mp3",
|
||||||
|
"sec": 3.38
|
||||||
|
},
|
||||||
|
"andrea|Un ponticello sopra la strada, da una casa all'altra. Per andare dal vicino senza fare le scale.": {
|
||||||
|
"file": "andrea-04381e498b.mp3",
|
||||||
|
"sec": 7.67
|
||||||
|
},
|
||||||
|
"andrea|Il muro di Villa Schifanoia, alto alto, con l'edera che ormai lo tiene su lei.": {
|
||||||
|
"file": "andrea-4deca437c8.mp3",
|
||||||
|
"sec": 5.28
|
||||||
|
},
|
||||||
|
"andrea|Una saracinesca color ruggine. Dietro, scommetto, una macchina e trent'anni di roba da buttare.": {
|
||||||
|
"file": "andrea-b72a76810b.mp3",
|
||||||
|
"sec": 7.04
|
||||||
|
},
|
||||||
|
"andrea|Chiusa. E se la apro io, mi tocca anche fare ordine.": {
|
||||||
|
"file": "andrea-3b7d5860dc.mp3",
|
||||||
|
"sec": 4.36
|
||||||
|
},
|
||||||
|
"andrea|Panni stesi al sole. Se piove, qui lo sanno tutti prima del telegiornale.": {
|
||||||
|
"file": "andrea-1c6bb20507.mp3",
|
||||||
|
"sec": 5.41
|
||||||
|
},
|
||||||
|
"andrea|Rubare i panni stesi? Mi conoscono tutti, mi ritroverebbero prima del Palio.": {
|
||||||
|
"file": "andrea-5b0b04fef8.mp3",
|
||||||
|
"sec": 6.3
|
||||||
|
},
|
||||||
"andrea|L'area feste. D'estate ci montano la pista da ballo, le tavolate e lo stand della pasta.": {
|
"andrea|L'area feste. D'estate ci montano la pista da ballo, le tavolate e lo stand della pasta.": {
|
||||||
"file": "andrea-a942b2723e.mp3",
|
"file": "andrea-a942b2723e.mp3",
|
||||||
"sec": 6.77
|
"sec": 6.77
|
||||||
@@ -1984,8 +2112,8 @@
|
|||||||
"sec": 1.24
|
"sec": 1.24
|
||||||
},
|
},
|
||||||
"andrea|S'ha andà a fallo!?!?": {
|
"andrea|S'ha andà a fallo!?!?": {
|
||||||
"file": "andrea-b25aeefc97.mp3",
|
"file": "andrea-bb61f7641e.mp3",
|
||||||
"sec": 1.16
|
"sec": 2.17
|
||||||
},
|
},
|
||||||
"andrea|Ci si becca da i' Trappola, ai' Barrettino!": {
|
"andrea|Ci si becca da i' Trappola, ai' Barrettino!": {
|
||||||
"file": "andrea-fca6f59a70.mp3",
|
"file": "andrea-fca6f59a70.mp3",
|
||||||
@@ -2035,6 +2163,74 @@
|
|||||||
"file": "arcu-406cd02197.mp3",
|
"file": "arcu-406cd02197.mp3",
|
||||||
"sec": 4.46
|
"sec": 4.46
|
||||||
},
|
},
|
||||||
|
"andrea|S'ha andà a fallo?!?": {
|
||||||
|
"file": "andrea-c80714aae7.mp3",
|
||||||
|
"sec": 1.08
|
||||||
|
},
|
||||||
|
"andrea|Quanti ne prendete, di solito?": {
|
||||||
|
"file": "andrea-1d758ed60c.mp3",
|
||||||
|
"sec": 1.84
|
||||||
|
},
|
||||||
|
"andrea|Buona pesca, ragazzi.": {
|
||||||
|
"file": "andrea-cb3510ac67.mp3",
|
||||||
|
"sec": 1.85
|
||||||
|
},
|
||||||
|
"pescatori|Gnamo! La canna ti si dà noi.": {
|
||||||
|
"file": "pescatori-32054cfa54.mp3",
|
||||||
|
"sec": 2.97
|
||||||
|
},
|
||||||
|
"pescatori|Come peschi?": {
|
||||||
|
"file": "pescatori-520fe85721.mp3",
|
||||||
|
"sec": 0.56
|
||||||
|
},
|
||||||
|
"pescatori|Dipende da chi racconta. Il Nanni dice dieci, il cestino dice due.": {
|
||||||
|
"file": "pescatori-901d65f3bc.mp3",
|
||||||
|
"sec": 4.09
|
||||||
|
},
|
||||||
|
"pescatori|Se vedi il palio, salutacelo!": {
|
||||||
|
"file": "pescatori-52125dc78f.mp3",
|
||||||
|
"sec": 1.74
|
||||||
|
},
|
||||||
|
"andrea|Alla bona, tanto per bagnare la lenza.": {
|
||||||
|
"file": "andrea-b74e348e25.mp3",
|
||||||
|
"sec": 2.55
|
||||||
|
},
|
||||||
|
"pescatori|Alla bona. Due pesci e si va a casa contenti.": {
|
||||||
|
"file": "pescatori-8af3768e38.mp3",
|
||||||
|
"sec": 3.26
|
||||||
|
},
|
||||||
|
"pescatori|Seria: tre pesci, e lo stivale non conta.": {
|
||||||
|
"file": "pescatori-e417a89a3a.mp3",
|
||||||
|
"sec": 2.82
|
||||||
|
},
|
||||||
|
"pescatori|A fassi male? Quattro pesci, e le carpe tirano come trattori.": {
|
||||||
|
"file": "pescatori-befa87eee8.mp3",
|
||||||
|
"sec": 4.17
|
||||||
|
},
|
||||||
|
"andrea|Due tiri alla bona, tanto per scaldarmi.": {
|
||||||
|
"file": "andrea-d564365fc2.mp3",
|
||||||
|
"sec": 2.53
|
||||||
|
},
|
||||||
|
"andrea|Due tiri seri.": {
|
||||||
|
"file": "andrea-ec8f0db709.mp3",
|
||||||
|
"sec": 1.32
|
||||||
|
},
|
||||||
|
"andrea|Lascia stare, ho il palio da trovare.": {
|
||||||
|
"file": "andrea-f5519518f2.mp3",
|
||||||
|
"sec": 1.99
|
||||||
|
},
|
||||||
|
"andrea|Tre su dieci e sono contento.": {
|
||||||
|
"file": "andrea-5e3315a040.mp3",
|
||||||
|
"sec": 2.33
|
||||||
|
},
|
||||||
|
"andrea|Cinque su dieci. Come ai tempi del Circolino.": {
|
||||||
|
"file": "andrea-1bc731bcdf.mp3",
|
||||||
|
"sec": 3.59
|
||||||
|
},
|
||||||
|
"andrea|Sette su dieci, senza guardare la parabola. Vediamo chi sono.": {
|
||||||
|
"file": "andrea-fa062d00bd.mp3",
|
||||||
|
"sec": 4.95
|
||||||
|
},
|
||||||
"andrea|Mariano! Facciamo una partita a bocce?": {
|
"andrea|Mariano! Facciamo una partita a bocce?": {
|
||||||
"file": "andrea-83b1337b4d.mp3",
|
"file": "andrea-83b1337b4d.mp3",
|
||||||
"sec": 3.31
|
"sec": 3.31
|
||||||
@@ -2279,6 +2475,38 @@
|
|||||||
"file": "mariano-39e27158b7.mp3",
|
"file": "mariano-39e27158b7.mp3",
|
||||||
"sec": 2.83
|
"sec": 2.83
|
||||||
},
|
},
|
||||||
|
"andrea|Che mano! Jordan, fatti da parte.": {
|
||||||
|
"file": "andrea-3a52789f04.mp3",
|
||||||
|
"sec": 2.98
|
||||||
|
},
|
||||||
|
"andrea|Il ferro oggi era stretto. Riprovo dopo.": {
|
||||||
|
"file": "andrea-2e9d8549c8.mp3",
|
||||||
|
"sec": 3.36
|
||||||
|
},
|
||||||
|
"andrea|Lascio il pallone lì. Tanto lo sanno tutti che è mio.": {
|
||||||
|
"file": "andrea-ee42022092.mp3",
|
||||||
|
"sec": 3.98
|
||||||
|
},
|
||||||
|
"pescatori|Oh, hai visto? Mica male, per una canna di bambù.": {
|
||||||
|
"file": "pescatori-2583bf891f.mp3",
|
||||||
|
"sec": 3.79
|
||||||
|
},
|
||||||
|
"andrea|Stasera si frigge. Ma prima il palio.": {
|
||||||
|
"file": "andrea-b00b469ef8.mp3",
|
||||||
|
"sec": 3.48
|
||||||
|
},
|
||||||
|
"pescatori|I pesci c'erano. Eri te che non c'eri.": {
|
||||||
|
"file": "pescatori-53d5588588.mp3",
|
||||||
|
"sec": 2.41
|
||||||
|
},
|
||||||
|
"andrea|Si rifà. Dopo il palio.": {
|
||||||
|
"file": "andrea-5ebf35bc74.mp3",
|
||||||
|
"sec": 2.37
|
||||||
|
},
|
||||||
|
"andrea|Lascio la canna ai ragazzi. Il lago non scappa, il palio sì.": {
|
||||||
|
"file": "andrea-72bf22a3f9.mp3",
|
||||||
|
"sec": 4.49
|
||||||
|
},
|
||||||
"ing_tozzi|Venti. Hai vinto te, stasera il caffè lo pago io.": {
|
"ing_tozzi|Venti. Hai vinto te, stasera il caffè lo pago io.": {
|
||||||
"file": "ing_tozzi-51f855478a.mp3",
|
"file": "ing_tozzi-51f855478a.mp3",
|
||||||
"sec": 4.42
|
"sec": 4.42
|
||||||
|
|||||||
@@ -105,18 +105,43 @@ si segna qui con la data e il punto di `src/story/story.js` dove sta; le persone
|
|||||||
- *Regia alla fessura*: col meccanismo del primo piano (`closeup`) il buio, la torcia, un istante di silenzio,
|
- *Regia alla fessura*: col meccanismo del primo piano (`closeup`) il buio, la torcia, un istante di silenzio,
|
||||||
poi gli occhi. Due immagini di Codex.
|
poi gli occhi. Due immagini di Codex.
|
||||||
- *Segreti facoltativi del paese* con pagine illustrate nel taccuino: funzione nuova, dopo la storia principale.
|
- *Segreti facoltativi del paese* con pagine illustrate nel taccuino: funzione nuova, dopo la storia principale.
|
||||||
- **Altri minigiochi come le bocce** (25 ottobre 2026): ognuno è un primo piano (`closeup` con il suo `type`, come
|
- **Altri minigiochi** (25 ottobre 2026, rivisti il 28 ottobre con le proposte di Claude che ad Andrea piacciono):
|
||||||
`bocce`), con comandi per dito, mouse e tastiera, i suoi test e una battuta per chi vince e per chi perde.
|
ognuno è un primo piano (`closeup` con il suo `type`), con comandi per dito, mouse e tastiera, i suoi test, una
|
||||||
Il posto naturale sono le gare del Palio del secondo capitolo:
|
battuta per chi vince e per chi perde, e a fine lavoro la revisione critica di Codex se Andrea la concede.
|
||||||
- *Passamano dei mattoni* (da fare per primo): un gioco di ritmo, i mattoni arrivano lungo la fila e si passano al
|
Già fatti: briscola, tressette e scopa all'oratorio; le bocce al pallaio; il biliardo coi birilli al Circolo
|
||||||
momento giusto; sbagliando il tempo ne cade uno. Facile col dito, e chiude il cerchio col finale (i mattoni
|
4 Maggio; «Fai due tiri» al campo da basket del Circolo (27 ottobre). I prossimi due, se Andrea è d'accordo:
|
||||||
portati in Fortezza).
|
il calcio balilla (riusa la fisica del biliardo, fa subito bar) e il tiro alla fune (è il Palio, e vale come
|
||||||
|
enigma del rione).
|
||||||
|
Le gare del Palio (il secondo capitolo: passamano, tiro alla fune, carrette, lavandaie, caccia al tesoro):
|
||||||
|
- *Passamano dei mattoni*: un gioco di ritmo, i mattoni arrivano lungo la fila e si passano al momento giusto,
|
||||||
|
un tasto per ogni passaggio; sbagliando il tempo ne cade uno. Facile col dito, e chiude il cerchio col finale
|
||||||
|
(i mattoni portati in Fortezza).
|
||||||
|
- *Tiro alla fune a Cardetole*: si batte a ritmo (spazio o tocco) seguendo una barra che oscilla, lo sforzo
|
||||||
|
della squadra; chi perde il tempo scivola. Semplice da fare, molto da palio; contro il bullo del rione, come in
|
||||||
|
`docs/PROGETTO.md`.
|
||||||
- *Le carrette*: corsa in discesa vista di lato, a destra e sinistra fra buche e balle di paglia. La più
|
- *Le carrette*: corsa in discesa vista di lato, a destra e sinistra fra buche e balle di paglia. La più
|
||||||
spettacolare, la più lunga da disegnare.
|
spettacolare, la più lunga da disegnare: il «gioco grosso», con Mitrugno (consenso).
|
||||||
- *Bandierine (lavanderine)*: correre a prendere la bandiera prima dell'avversario, di riflessi.
|
- *Le lavandaie* (Andrea, 28 ottobre; prima era segnato male come «bandierine»): un vascone pieno d'acqua con
|
||||||
|
dentro gli stracci, pezzi di stoffa dei quattro colori dei rioni mescolati; ogni squadra deve ripescare i suoi.
|
||||||
|
Come minigioco: vista dall'alto sul vascone, gli stracci galleggiano e girano, si tocca il proprio colore per
|
||||||
|
tirarlo fuori, un errore (il colore di un altro) fa perdere tempo; contro il tempo o contro le altre squadre.
|
||||||
- *Caccia al tesoro*: un giro di indizi fra le scene che ci sono già, senza un minigioco nuovo.
|
- *Caccia al tesoro*: un giro di indizi fra le scene che ci sono già, senza un minigioco nuovo.
|
||||||
Fuori dal Palio:
|
Al bar e al circolo:
|
||||||
- *Briscola al circolino* con l'Arcu e il Barone (la gag del «Male male si divide»): una mano corta.
|
- *Calcio balilla al River*: due stecche sole (difesa e attacco), vista dall'alto come il biliardo, che ha già
|
||||||
- *Rigori all'A. Bettini*: mira e forza come alle bocce, contro un portiere. Andrea ha ritrovato la foto di squadra
|
la fisica delle sponde. Il gioco da bar più sampierino che c'è; il River «coi flipper e le figurine» lo cita già.
|
||||||
sul campo in terra (porta, rete, la torre della Pieve dietro, anni '90): lo sfondo del minigioco lo fa Codex da quella,
|
- *Flipper del River*: più lavoro, ma la fisica delle biglie c'è già.
|
||||||
senza le persone (per i volti serve il consenso). Da salvare in `assets/raw/foto/campo-bettini-squadra.jpg`.
|
- *Freccette al Circolo*: la mira vaga e si ferma al clic, tre freccette, si chiude il 301. Poco codice.
|
||||||
|
- *Morra*: niente fisica, tutto dialogo e tempismo; molto toscana, le grida dei numeri in voce XTTS.
|
||||||
|
Sport:
|
||||||
|
- *Rigori all'A. Bettini*: mira e forza come alle bocce, contro un portiere che si butta. Andrea ha ritrovato la
|
||||||
|
foto di squadra sul campo in terra (porta, rete, la torre della Pieve dietro, anni '90): lo sfondo lo fa Codex
|
||||||
|
da quella, senza le persone (per i volti serve il consenso). Da salvare in `assets/raw/foto/campo-bettini-squadra.jpg`.
|
||||||
|
- *Tennis o padel al campo secondario*: la macchina del tempo compare già quando Andrea dice di voler giocare a
|
||||||
|
padel; un «muro» a tempismo basterebbe.
|
||||||
|
- *Ping-pong al pallaio*: il tavolo verde c'è già nello sfondo.
|
||||||
|
Giochi da bambini del 2007 (nostalgia):
|
||||||
|
- *Le figurine al River*: un «batti-muro», o lo scambio «ce l'ho, ce l'ho, mi manca», più dialogo che gioco.
|
||||||
|
- *La pesca al lago dell'Anatre*: coi ragazzi delle canne in Fortezza (27 ottobre) la scusa c'è già. Tempismo:
|
||||||
|
la lenza vibra, si tira al momento giusto. Serve la scena del lago, dietro Cardetole.
|
||||||
|
- *Briscola al circolino* con l'Arcu e il Barone (la gag del «Male male si divide»): una mano corta; la briscola
|
||||||
|
c'è già all'oratorio, qui conta la gag.
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ tipi riconoscibili che persone identificabili, salvo accordi.
|
|||||||
|
|
||||||
Obiettivo: entrare nella Fortezza e trovare il palio. Serve l'aiuto dei tre rioni oltre a quello di Andrea; ogni
|
Obiettivo: entrare nella Fortezza e trovare il palio. Serve l'aiuto dei tre rioni oltre a quello di Andrea; ogni
|
||||||
rione vuole una sfida del **Palietto** (i giochi dei bambini del Palio: passaggio dei mattoni, tiro alla fune, corsa
|
rione vuole una sfida del **Palietto** (i giochi dei bambini del Palio: passaggio dei mattoni, tiro alla fune, corsa
|
||||||
coi carretti, gioco dell'elastico), qui trasformate in enigmi comici.
|
coi carretti, gioco dell'elastico, le lavandaie: gli stracci dei quattro colori nel vascone, ognuno ripesca i suoi), qui trasformate in enigmi comici.
|
||||||
|
|
||||||
Le gare che Andrea ricorda davvero:
|
Le gare che Andrea ricorda davvero:
|
||||||
|
|
||||||
|
|||||||
@@ -204,13 +204,38 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso
|
|||||||
Via del Pignone (sfondo di Codex da `assets/raw/foto/via-del-pignone.png`): sul muro di destra il
|
Via del Pignone (sfondo di Codex da `assets/raw/foto/via-del-pignone.png`): sul muro di destra il
|
||||||
murale di Cistola (Davide Poggi), «il talento di via del Pignone», alla maniera dei murali di Maradona a Napoli
|
murale di Cistola (Davide Poggi), «il talento di via del Pignone», alla maniera dei murali di Maradona a Napoli
|
||||||
(volto da `assets/raw/riferimenti/cistola.png`). Consenso di Davide confermato da Andrea il 2026-10-27.
|
(volto da `assets/raw/riferimenti/cistola.png`). Consenso di Davide confermato da Andrea il 2026-10-27.
|
||||||
|
- Via dell'Antica Posta (foto di Andrea, 28 ottobre, sfondi di Codex): dalla Piazza, a sinistra, sale verso Piazzetta
|
||||||
|
Cambray Digny, che non è più collegata direttamente alla Piazza. A sinistra la Piazzetta del Comune (il retro del
|
||||||
|
municipio con la torre e le sculture); in cima il muro di Villa Schifanoia, il ponticello, le panchine e due gatti.
|
||||||
|
Dalla Piazzetta, a destra accanto al palazzo, si scende per la scalinata in un cortile coi garage che porta ai
|
||||||
|
giardini del Comune (lato sinistro della scena del Comune); la scalinata si prende anche dalla curva a sinistra in
|
||||||
|
cima a Via dell'Antica Posta. In Piazzetta tutto il lato sinistro porta in Via de' Medici. Da confermare con Andrea:
|
||||||
|
il nome vero della scalinata. Battute provvisorie.
|
||||||
- Via Pietro Cipriani (fra Piazza e campeggio): Cinema Taiuti aperto con la locandina del Rattattù 2007 (stile da
|
- Via Pietro Cipriani (fra Piazza e campeggio): Cinema Taiuti aperto con la locandina del Rattattù 2007 (stile da
|
||||||
`assets/raw/riferimenti/rattattu-locandina.png`, scaletta illeggibile), campo da basket del Circolo: **da fare il
|
`assets/raw/riferimenti/rattattu-locandina.png`, scaletta illeggibile). Al campo da basket del Circolo, «Fai due tiri»
|
||||||
gioco del basket** (idea di Andrea, 27 ottobre).
|
(`src/engine/basket.js`, vista di lato, sfondo e sprite di Andrea col pallone da Codex): da solo, dieci palloni, a ogni
|
||||||
|
canestro si arretra fino alla linea da tre; tre livelli (quanti canestri per la gloria, quanta parabola mostra la
|
||||||
|
guida). Battute provvisorie. Idea di Andrea del 27 ottobre. Revisione critica di Codex (27 ottobre) applicata per
|
||||||
|
i punti gravi (angolo iniziale 64° con fascia di forza buona da ogni postazione, palo e tabellone da entrambi i lati,
|
||||||
|
canestro che conta subito, pose senza doppio pallone, guida fino al primo urto, obiettivo sempre in vista). Restano
|
||||||
|
le sue idee: «Riprova» dal risultato e record per livello; Andrea che arretra camminando; battute che dicono perché
|
||||||
|
è uscita (corto, lungo, ferro); l'angolo anche nei comandi HTML; testi del canvas piccoli sul telefono;
|
||||||
|
`basket_vinto` che lasci traccia nel paese; pannelli più compatti.
|
||||||
- Fuori paese, dal campo secondario (a destra): le Mozzète → il ponticino → Coldaia → la falegnameria → Gabbiano;
|
- Fuori paese, dal campo secondario (a destra): le Mozzète → il ponticino → Coldaia → la falegnameria → Gabbiano;
|
||||||
da Via Cipriani, indietro, Via Massorondinaio → campeggio. Sfondi da Street View, battute provvisorie.
|
da Via Cipriani, indietro, Via Massorondinaio → campeggio. Sfondi da Street View, battute provvisorie.
|
||||||
- In Fortezza quattro ragazzi inventati si fanno le canne da pesca col bambù per il lago dell'Anatre (sprite di Codex,
|
- In Fortezza quattro ragazzi inventati si fanno le canne da pesca col bambù per il lago dell'Anatre (sprite di Codex,
|
||||||
`assets/raw/pescatori-sheet.png`, ritagliato 2x2 a mano: work, talk, laugh, proud); battuta finale di Andrea.
|
`assets/raw/pescatori-sheet.png`, ritagliato 2x2 a mano: work, talk, laugh, proud); battuta finale di Andrea.
|
||||||
|
Parlandoci: «S'ha andà a fallo?!?» (Andrea, 28 ottobre) e si va a pescare: minigioco `src/engine/pesca.js`
|
||||||
|
(primo piano del lago, sfondo e Andrea con la canna di Codex; lancio tenendo premuto, tocchi finti e abboccata,
|
||||||
|
ferrata a tempo, recupero con la lenza che si può spezzare; sei lanci; pesci più grossi al largo, lo stivale sotto
|
||||||
|
riva; tre livelli). Battute provvisorie. Il lago come scena da visitare non c'è: serve una foto di Andrea.
|
||||||
|
Revisione di Codex (28 ottobre) applicata nei punti gravi (galleggiante che saltava, sesto lancio bloccato dal
|
||||||
|
pannello, invito a ferrare fuori tempo, lenza che non si spezzava mai, mira che si sbloccava) e in parecchi utili
|
||||||
|
(un gesto solo per ferrare e recuperare, pulsante Recupera/Molla, recupero breve per i pesci piccoli, attese ripetute
|
||||||
|
dimezzate, tocchi finti variabili, lenza dalla punta vera della canna, stivale con la sua battuta e senza peso,
|
||||||
|
risultato deciso all'ultimo pesce). Restano le sue idee: «Porta a casa la pescata» a gloria raggiunta; premio per
|
||||||
|
peso o varietà; i ragazzi che reagiscono (stivale, carpa, lenza rotta); paesaggio sonoro del lago (ora quello della
|
||||||
|
Fortezza); abboccata e tensione anche nei comandi HTML e un richiamo grande sul telefono; Esc col fuoco sui pulsanti.
|
||||||
- Andrea rifatto dalla sua foto del 2007 (`assets/raw/andrea-sheet-prova-2.png`; il vecchio foglio resta in `andrea-sheet.png`).
|
- Andrea rifatto dalla sua foto del 2007 (`assets/raw/andrea-sheet-prova-2.png`; il vecchio foglio resta in `andrea-sheet.png`).
|
||||||
- Strada del campeggio (poi Bosco ai Frati) e strada della scuola: per ora battute, scene da fare con le foto di Andrea.
|
- Strada del campeggio (poi Bosco ai Frati) e strada della scuola: per ora battute, scene da fare con le foto di Andrea.
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
<h2>Ringraziamenti</h2>
|
<h2>Ringraziamenti</h2>
|
||||||
<p class="dedication credits-dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p>
|
<p class="dedication credits-dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p>
|
||||||
<dl id="credits-list"></dl>
|
<dl id="credits-list"></dl>
|
||||||
<div id="credits-reel" aria-hidden="true"><div class="track"></div></div>
|
<div id="credits-reel"><button class="arrow prev" type="button" aria-label="Foto precedente">‹</button><div class="track" aria-label="Le immagini del gioco: toccane una per vederla grande"></div><button class="arrow next" type="button" aria-label="Foto successiva">›</button></div>
|
||||||
<p class="legal">Monkey Island è un marchio di Lucasfilm Ltd., citato solo nella dedica: questo gioco non è affiliato né approvato da Lucasfilm.</p>
|
<p class="legal">Monkey Island è un marchio di Lucasfilm Ltd., citato solo nella dedica: questo gioco non è affiliato né approvato da Lucasfilm.</p>
|
||||||
<div class="buttons"><button id="close-credits">Chiudi</button></div>
|
<div class="buttons"><button id="close-credits">Chiudi</button></div>
|
||||||
<figure id="face" hidden><img alt=""><figcaption></figcaption></figure>
|
<figure id="face" hidden><img alt=""><figcaption></figcaption></figure>
|
||||||
@@ -209,6 +209,27 @@
|
|||||||
<p>Tre bocce a testa. Tira chi non ha il punto; segna ogni boccia più vicina della migliore avversaria.
|
<p>Tre bocce a testa. Tira chi non ha il punto; segna ogni boccia più vicina della migliore avversaria.
|
||||||
Vince chi fa più punti nella mano.</p>
|
Vince chi fa più punti nella mano.</p>
|
||||||
</details>
|
</details>
|
||||||
|
<details id="basket-help" hidden><summary>Come si gioca</summary>
|
||||||
|
<p>Muovi il mouse sul campo per scegliere l’angolo del tiro: la freccia parte dalle mani di Andrea. Un clic (o un
|
||||||
|
tocco) fissa la mira, così puoi raggiungere i comandi senza cambiarla; un altro clic la sposta. Tieni premuto sul
|
||||||
|
campo (o Spazio) e la forza riparte da zero e sale: lascia per tirare. Oppure regola la forza e premi Tira.
|
||||||
|
Frecce ← →: angolo più teso o più alto; Maiusc + ← →: a mezzo grado; ↑ ↓: forza. Esc esce.</p>
|
||||||
|
<p>Dieci palloni. Si comincia da sotto; a ogni canestro si arretra di una postazione: tiro libero, media distanza,
|
||||||
|
da tre e da lontanissimo, dove un canestro vale tre punti invece di due. Il ferro e il tabellone rimandano la
|
||||||
|
palla: entra anche sporca, ma non da sotto. La gloria: 3 canestri alla bona, 5 seria, 7 a fassi male. Alla bona la
|
||||||
|
guida mostra la parabola fino al primo urto, Seria solo l’inizio, A fassi male niente.</p>
|
||||||
|
</details>
|
||||||
|
<details id="pesca-help" hidden><summary>Come si gioca</summary>
|
||||||
|
<p>Un tocco sull’acqua sceglie dove lanciare (o regola la forza: più lontano, pesci più grossi, ma attese più
|
||||||
|
lunghe). Tieni premuto sulla scena (o Spazio): la forza riparte da zero e sale, lascia per lanciare; oppure premi
|
||||||
|
Lancia. Poi si aspetta: il galleggiante ha dei tocchi finti (si abbassa appena) e un’abboccata vera (va sotto del
|
||||||
|
tutto): premi in quel momento per ferrare, e <b>resta premuto</b>: stai già recuperando. Premendo a vuoto si
|
||||||
|
ricupera la lenza vuota e il lancio è perso; se non ferri in tempo il pesce lo sputa, e riprova fino a tre volte.</p>
|
||||||
|
<p>Nel recupero guarda la lenza e la barra in basso: quando il pesce strattona (la lenza trema) la barra sale verso
|
||||||
|
il rosso, lascia andare un attimo, poi riprendi. I pesci piccoli vengono a riva in fretta, le carpe no. Dal pannello
|
||||||
|
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>
|
||||||
|
</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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "leggenda-del-regolo",
|
"name": "leggenda-del-regolo",
|
||||||
"version": "0.1.19",
|
"version": "0.1.28",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { FLOOR, RIM, BALL, BALLS, SPOTS, canThrow, left, spot, releasePoint, guide as guidePoints, status } from './basket.js';
|
||||||
|
import { text, panel } from './bocce-render.js';
|
||||||
|
|
||||||
|
const ANDREA = { scale: 0.66 };
|
||||||
|
// Quale posa: carica mentre tiene premuto; finché la palla vola Andrea resta a braccia alzate (l'unica posa senza
|
||||||
|
// pallone), esulta quando entra, e a palla ferma ce l'ha di nuovo in mano.
|
||||||
|
export function andreaPose(c) {
|
||||||
|
if (c.holding) return 'load';
|
||||||
|
if (c.step === 'flight') return c.ball.scored ? 'cheer' : 'shoot';
|
||||||
|
if (c.step === 'settle' && c.ball?.scored) return 'cheer';
|
||||||
|
return 'hold';
|
||||||
|
}
|
||||||
|
function andrea(ctx, c, sheet) {
|
||||||
|
const s = spot(c), x = s.x, y = FLOOR;
|
||||||
|
ctx.fillStyle = 'rgba(8,10,9,0.5)'; ctx.beginPath(); ctx.ellipse(x + 4, y, 16, 3, 0, 0, Math.PI * 2); ctx.fill();
|
||||||
|
if (!sheet) return;
|
||||||
|
const pose = andreaPose(c), frame = sheet.atlas.frames[pose] ?? sheet.atlas.frames.hold, k = ANDREA.scale;
|
||||||
|
// Nel salto i piedi si staccano da terra per un attimo.
|
||||||
|
const lift = pose === 'shoot' ? Math.round(Math.sin(Math.min(1, c.ball.time / 0.45) * Math.PI) * 10) : 0;
|
||||||
|
ctx.drawImage(sheet.img, frame.x, frame.y, frame.w, frame.h,
|
||||||
|
Math.round(x - frame.ax * k), Math.round(y - frame.ay * k) - lift, Math.round(frame.w * k), Math.round(frame.h * k));
|
||||||
|
}
|
||||||
|
function ball(ctx, b, r = BALL) {
|
||||||
|
// L'ombra sul campo si stringe quando la palla è alta.
|
||||||
|
const h = Math.max(0, FLOOR - b.y - r), k = Math.max(0.35, 1 - h / 260);
|
||||||
|
ctx.fillStyle = `rgba(8,10,9,${0.45 * k})`; ctx.beginPath(); ctx.ellipse(b.x, FLOOR - 1, r * 1.2 * k, r * 0.4 * k, 0, 0, Math.PI * 2); ctx.fill();
|
||||||
|
const shade = ctx.createRadialGradient(b.x - r * 0.35, b.y - r * 0.35, 0, b.x, b.y, r * 1.05);
|
||||||
|
shade.addColorStop(0, '#ffb067'); shade.addColorStop(0.5, '#e8702a'); shade.addColorStop(1, '#8a3512');
|
||||||
|
ctx.fillStyle = shade; ctx.beginPath(); ctx.arc(b.x, b.y, r, 0, Math.PI * 2); ctx.fill();
|
||||||
|
// Le cuciture girano con la palla.
|
||||||
|
ctx.save(); ctx.beginPath(); ctx.arc(b.x, b.y, r - 0.3, 0, Math.PI * 2); ctx.clip();
|
||||||
|
ctx.strokeStyle = 'rgba(60,20,8,0.75)'; ctx.lineWidth = 1;
|
||||||
|
const a = b.spin ?? 0;
|
||||||
|
ctx.beginPath(); ctx.ellipse(b.x, b.y, r, Math.abs(Math.cos(a)) * r, 0, 0, Math.PI * 2); ctx.stroke();
|
||||||
|
ctx.beginPath(); ctx.ellipse(b.x, b.y, Math.abs(Math.sin(a)) * r, r, 0, 0, Math.PI * 2); ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
ctx.fillStyle = '#ffd9b0'; ctx.fillRect(Math.round(b.x - r * 0.4), Math.round(b.y - r * 0.45), 1, 1);
|
||||||
|
}
|
||||||
|
function guide(ctx, c) {
|
||||||
|
const r = releasePoint(c);
|
||||||
|
// La freccia della mira c'è sempre, con la punta e un bordo scuro per staccarsi da rete e alberi; la parabola per
|
||||||
|
// quanto concede il livello, fino al primo urto.
|
||||||
|
const a = c.angle * Math.PI / 180, len = 22 + c.power * 18, tip = { x: r.x + Math.cos(a) * len, y: r.y - Math.sin(a) * len };
|
||||||
|
const arrow = () => {
|
||||||
|
ctx.beginPath(); ctx.moveTo(r.x, r.y); ctx.lineTo(tip.x, tip.y);
|
||||||
|
for (const s of [-1, 1]) { ctx.moveTo(tip.x, tip.y); ctx.lineTo(tip.x - Math.cos(a - s * 0.5) * 6, tip.y + Math.sin(a - s * 0.5) * 6); }
|
||||||
|
ctx.stroke();
|
||||||
|
};
|
||||||
|
ctx.lineWidth = 3; ctx.strokeStyle = 'rgba(20,16,10,0.8)'; arrow();
|
||||||
|
ctx.lineWidth = 1; ctx.strokeStyle = '#fff6d2'; arrow();
|
||||||
|
if (c.skill.preview > 0) {
|
||||||
|
const pts = guidePoints(c, c.skill.preview);
|
||||||
|
for (const [width, color] of [[3, 'rgba(20,16,10,0.6)'], [1, 'rgba(143,199,255,0.95)']]) {
|
||||||
|
ctx.lineWidth = width; ctx.strokeStyle = color; ctx.setLineDash([2, 4]);
|
||||||
|
ctx.beginPath();
|
||||||
|
for (const [i, p] of pts.entries()) { if (i) ctx.lineTo(p.x, p.y); else ctx.moveTo(p.x, p.y); }
|
||||||
|
ctx.stroke(); ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.fillStyle = 'rgba(20,16,10,0.6)'; ctx.fillRect(r.x - 34, r.y - 16, 30, 11);
|
||||||
|
text(ctx, `${c.angle.toFixed(c.angle % 1 ? 1 : 0)}°`, r.x - 6, r.y - 10, 6, '#fff4c9', 'right');
|
||||||
|
}
|
||||||
|
function swish(ctx, c) {
|
||||||
|
// Un lampo di righe attorno al ferro quando la palla è appena entrata.
|
||||||
|
const b = c.ball; if (c.step !== 'flight' || !b?.scored || b.time - b.scoredAt > 0.3) return;
|
||||||
|
const cx = (RIM.x1 + RIM.x2) / 2;
|
||||||
|
ctx.strokeStyle = 'rgba(255,240,170,0.8)';
|
||||||
|
for (let i = 0; i < 6; i++) { const t = i / 6 * Math.PI * 2; ctx.beginPath(); ctx.moveTo(cx + Math.cos(t) * 20, RIM.y + Math.sin(t) * 8); ctx.lineTo(cx + Math.cos(t) * 30, RIM.y + Math.sin(t) * 13); ctx.stroke(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawBasket(ctx, c, def, bg, sheet) {
|
||||||
|
ctx.save();
|
||||||
|
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
|
||||||
|
else { ctx.fillStyle = '#6d7a5a'; ctx.fillRect(0, 0, 640, 360); }
|
||||||
|
// Le postazioni sul campo: un segno chiaro dove si tira ora, uno spento dove si è già stati.
|
||||||
|
for (const [i, s] of SPOTS.entries()) {
|
||||||
|
ctx.fillStyle = i === c.spot ? 'rgba(255,244,201,0.9)' : i < c.spot ? 'rgba(255,244,201,0.35)' : 'rgba(255,244,201,0.15)';
|
||||||
|
ctx.fillRect(s.x - 1, FLOOR + 2, 3, 3);
|
||||||
|
}
|
||||||
|
andrea(ctx, c, sheet);
|
||||||
|
for (const [i, t] of c.trails.entries()) {
|
||||||
|
ctx.fillStyle = `rgba(255,190,120,${0.3 * (1 - t.age / 0.5)})`;
|
||||||
|
ctx.fillRect(Math.round(t.x) - 1, Math.round(t.y) - 1, i % 2 ? 2 : 3, i % 2 ? 2 : 3);
|
||||||
|
}
|
||||||
|
if (canThrow(c) || c.holding) guide(ctx, c);
|
||||||
|
if (c.ball && c.step === 'flight') ball(ctx, c.ball); // ferma, la palla è già tornata in mano ad Andrea
|
||||||
|
swish(ctx, c);
|
||||||
|
panel(ctx, 182, 50, 440, 24);
|
||||||
|
text(ctx, status(c, def.names), 402, 63, 6, '#fff0cb', 'center');
|
||||||
|
// I palloni rimasti e il conto.
|
||||||
|
panel(ctx, 18, 86, 196, 58);
|
||||||
|
text(ctx, 'PALLONI', 26, 98, 6, '#eed5a0');
|
||||||
|
for (let n = 0; n < BALLS; n++) {
|
||||||
|
const x = 30 + n * 14, y = 113;
|
||||||
|
if (n < left(c)) ball(ctx, { x, y, spin: 0 }, 4);
|
||||||
|
else { ctx.strokeStyle = '#766e5c'; ctx.strokeRect(x - 3, y - 3, 6, 6); }
|
||||||
|
}
|
||||||
|
text(ctx, `${c.baskets} su ${c.skill.goal} per la gloria · ${c.points} punti`, 26, 131, 6, c.baskets >= c.skill.goal ? '#b9e5b3' : '#d7ccb4');
|
||||||
|
// La forza in basso a sinistra: a destra c'è il pulsante per uscire.
|
||||||
|
if (canThrow(c) || c.holding) {
|
||||||
|
panel(ctx, 18, 306, 176, 37);
|
||||||
|
text(ctx, `FORZA ${Math.round(c.power * 100)}%`, 30, 318, 7);
|
||||||
|
ctx.fillStyle = '#3e3e36'; ctx.fillRect(30, 331, 150, 5);
|
||||||
|
ctx.fillStyle = '#b9d392'; ctx.fillRect(30, 331, Math.round(c.power * 150), 5);
|
||||||
|
}
|
||||||
|
if (c.step === 'result') {
|
||||||
|
panel(ctx, 206, 91, 397, 70);
|
||||||
|
text(ctx, c.result.winner === 'player' ? 'CHE MANO!' : 'DUE TIRI FATTI', 404, 108, 9, '#efd49e', 'center');
|
||||||
|
text(ctx, `${c.result.baskets} canestri su ${BALLS}, ${c.result.points} punti`, 404, 132, 9, c.result.winner === 'player' ? '#b9e5b3' : '#f1d9be', 'center');
|
||||||
|
text(ctx, c.result.winner === 'player' ? `Obiettivo ${c.result.goal}: fatto.` : `Per la gloria ne servivano ${c.result.goal}.`, 404, 150, 6, '#d6c7af', 'center');
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
// «Fai due tiri»: Andrea da solo al campo del Circolo 4 Maggio (idea di Andrea, 27 ottobre). Vista di lato, nelle
|
||||||
|
// coordinate dello schermo (640x360): x verso il canestro, y verso il basso. Un pixel è circa 2,3 cm: il ferro, a
|
||||||
|
// 3,05 m, sta 133 px sopra il campo. Dieci palloni; a ogni canestro si arretra di una postazione, fino a quella da
|
||||||
|
// tre. Niente avversario: il risultato è quanti canestri su dieci, e i punti (due, tre da dietro la linea).
|
||||||
|
export const FLOOR = 296, RIM = { x1: 482, x2: 518, y: 163, r: 2 }, BOARD = { x: 519, top: 118, bottom: 178, depth: 8 }, NET = 190, POLE = { x: 562, top: 112 };
|
||||||
|
export const BALL = 8, G = 560, V_MIN = 140, V_MAX = 590, SWING = 1.4, BALLS = 10;
|
||||||
|
export const FIXED_STEP = 1 / 240, REST = 0.55, FLIGHT_MAX = 7;
|
||||||
|
export const RELEASE = { dx: 18, dy: -72 }; // da dove parte la palla rispetto ai piedi di Andrea: sopra la testa, nel salto
|
||||||
|
export const ANGLE = { min: 25, max: 78 };
|
||||||
|
export const SPOTS = [
|
||||||
|
{ x: 400, name: 'da sotto', points: 2 },
|
||||||
|
{ x: 320, name: 'dal tiro libero', points: 2 },
|
||||||
|
{ x: 240, name: 'da media distanza', points: 2 },
|
||||||
|
{ x: 150, name: 'da tre', points: 3 },
|
||||||
|
{ x: 90, name: 'da lontanissimo', points: 3 },
|
||||||
|
];
|
||||||
|
// I livelli: quanti canestri su dieci valgono la gloria, e per quanto tempo di volo la guida mostra la parabola
|
||||||
|
// (alla bona fino al primo urto, che è quasi tutta).
|
||||||
|
export const LEVELS = {
|
||||||
|
bona: { goal: 3, preview: 3 },
|
||||||
|
seria: { goal: 5, preview: 0.4 },
|
||||||
|
male: { goal: 7, preview: 0 },
|
||||||
|
};
|
||||||
|
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||||
|
const rad = deg => deg * Math.PI / 180;
|
||||||
|
|
||||||
|
export function newBasket(seed = Date.now() >>> 0, def = {}) {
|
||||||
|
const level = LEVELS[def.level] ? def.level : 'seria';
|
||||||
|
return { step: 'aim', t: 0, age: 0, seed, level, skill: LEVELS[level], current: 'player', spot: 0, thrown: 0, baskets: 0, points: 0,
|
||||||
|
angle: 64, power: 0.5, holding: false, holdTime: 0, accumulator: 0, ball: null, events: [], trails: [], history: [] };
|
||||||
|
}
|
||||||
|
export const canThrow = c => c.step === 'aim' && c.thrown < BALLS;
|
||||||
|
export const left = c => Math.max(0, BALLS - c.thrown);
|
||||||
|
export const spot = c => SPOTS[c.spot];
|
||||||
|
export const powerAt = time => clamp(time / SWING, 0, 1);
|
||||||
|
export const speedFor = power => V_MIN + (V_MAX - V_MIN) * clamp(power, 0, 1);
|
||||||
|
export function releasePoint(c) { const s = spot(c); return { x: s.x + RELEASE.dx, y: FLOOR + RELEASE.dy }; }
|
||||||
|
export function velocity(c) { const v = speedFor(c.power), a = rad(c.angle); return { vx: v * Math.cos(a), vy: -v * Math.sin(a) }; }
|
||||||
|
|
||||||
|
export function setPower(c, value) {
|
||||||
|
if (!canThrow(c) || c.holding || !Number.isFinite(value)) return false;
|
||||||
|
c.power = clamp(value, 0, 1); return true;
|
||||||
|
}
|
||||||
|
export function setAngle(c, deg) {
|
||||||
|
if (!canThrow(c) || c.holding || !Number.isFinite(deg)) return false;
|
||||||
|
c.angle = clamp(deg, ANGLE.min, ANGLE.max); return true;
|
||||||
|
}
|
||||||
|
export const nudge = (c, dir, fine = false) => setAngle(c, c.angle + dir * (fine ? 0.5 : 2));
|
||||||
|
// La mira segue il punto toccato: l'angolo fra la mano di Andrea e il dito. Dietro di lui non si tira.
|
||||||
|
export function aimScreen(c, [x, y]) {
|
||||||
|
if (!Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||||
|
const r = releasePoint(c);
|
||||||
|
if (x <= r.x + 4) return false;
|
||||||
|
return setAngle(c, Math.atan2(r.y - y, x - r.x) * 180 / Math.PI);
|
||||||
|
}
|
||||||
|
// La parabola libera (senza urti) per `seconds` di volo: la guida la mostra per il tempo che il livello concede.
|
||||||
|
export function arc(c, seconds, every = 0.04) {
|
||||||
|
const r = releasePoint(c), v = velocity(c), out = [];
|
||||||
|
for (let t = 0; t <= seconds; t += every) out.push({ x: r.x + v.vx * t, y: r.y + v.vy * t + 0.5 * G * t * t });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
// La guida: la parabola libera finché non tocca qualcosa (ferro, tabellone, palo, campo), al massimo `seconds`.
|
||||||
|
export function guide(c, seconds) {
|
||||||
|
const out = [];
|
||||||
|
for (const p of arc(c, seconds)) {
|
||||||
|
const touches = p.y + BALL > FLOOR || [RIM.x1, RIM.x2].some(x => Math.hypot(p.x - x, p.y - RIM.y) < BALL + RIM.r)
|
||||||
|
|| (p.x + BALL > BOARD.x && p.y + BALL > BOARD.top && p.y - BALL < BOARD.bottom) || (p.x + BALL > POLE.x && p.y + BALL > POLE.top);
|
||||||
|
out.push(p);
|
||||||
|
if (touches) break;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
export function shoot(c) {
|
||||||
|
if (!canThrow(c) || c.holding) return false;
|
||||||
|
const r = releasePoint(c), v = velocity(c);
|
||||||
|
c.ball = { x: r.x, y: r.y, vx: v.vx, vy: v.vy, time: 0, bounces: 0, rim: false, board: false, under: false, scored: false, scoredAt: null, dead: false, spin: 0 };
|
||||||
|
c.step = 'flight'; c.t = c.accumulator = 0; c.thrown++; c.trails = [];
|
||||||
|
c.events.push({ kind: 'basket-throw', power: c.power });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function press(c) {
|
||||||
|
if (!canThrow(c) || c.holding) return false;
|
||||||
|
c.previousPower = c.power; c.holding = true; c.holdTime = c.power = 0; return true;
|
||||||
|
}
|
||||||
|
export function cancel(c) {
|
||||||
|
if (!c.holding) return false;
|
||||||
|
c.holding = false; c.power = c.previousPower; c.holdTime = 0; return true;
|
||||||
|
}
|
||||||
|
export function release(c) {
|
||||||
|
if (!c.holding) return false;
|
||||||
|
if (c.holdTime < 0.14) return cancel(c); // un tocco breve serve a mirare, non spreca un pallone
|
||||||
|
c.holding = false; return shoot(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hit(c, kind, strength) { if (c.events.length < 12) c.events.push({ kind, power: clamp(strength, 0, 1) }); }
|
||||||
|
// Il ferro visto di lato sono due punti: il davanti e il dietro dell'anello. La palla ci rimbalza come su un tondino.
|
||||||
|
function rimEnd(c, b, px, py) {
|
||||||
|
const dx = b.x - px, dy = b.y - py, d = Math.hypot(dx, dy), r = BALL + RIM.r;
|
||||||
|
if (d >= r || d < 1e-9) return;
|
||||||
|
const nx = dx / d, ny = dy / d, closing = b.vx * nx + b.vy * ny;
|
||||||
|
b.x = px + nx * r; b.y = py + ny * r;
|
||||||
|
if (closing < 0) {
|
||||||
|
const tx = -ny, ty = nx, vt = (b.vx * tx + b.vy * ty) * 0.92, vn = -closing * 0.58;
|
||||||
|
b.vx = vn * nx + vt * tx; b.vy = vn * ny + vt * ty;
|
||||||
|
b.rim = true;
|
||||||
|
if (-closing > 25) hit(c, 'basket-rim', -closing / 400); // i microurti di una palla che si posa non tintinnano
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function physics(c, dt) {
|
||||||
|
const b = c.ball, before = b.y;
|
||||||
|
b.vy += G * dt; b.x += b.vx * dt; b.y += b.vy * dt; b.time += dt; b.spin += b.vx * dt / BALL;
|
||||||
|
// Il tabellone: una lastra verticale che guarda Andrea, ma rimanda anche una palla che torna da dietro.
|
||||||
|
// Il palo dietro ferma quel che passa oltre, dalla sua cima in giù: sopra, la palla vola via e ricade.
|
||||||
|
if (b.y + BALL > BOARD.top && b.y - BALL < BOARD.bottom) {
|
||||||
|
if (b.vx > 0 && b.x + BALL > BOARD.x && b.x - BALL < BOARD.x + BOARD.depth) { b.x = BOARD.x - BALL; b.vx = -b.vx * 0.6; b.board = true; hit(c, 'basket-board', Math.abs(b.vx) / 300); }
|
||||||
|
else if (b.vx < 0 && b.x - BALL < BOARD.x + BOARD.depth && b.x + BALL > BOARD.x) { b.x = BOARD.x + BOARD.depth + BALL; b.vx = -b.vx * 0.6; b.board = true; hit(c, 'basket-board', Math.abs(b.vx) / 300); }
|
||||||
|
}
|
||||||
|
if (b.x + BALL > POLE.x && b.y + BALL > POLE.top) { b.x = POLE.x - BALL; if (b.vx > 0) { b.vx = -b.vx * 0.5; b.board = true; hit(c, 'basket-board', 0.3); } }
|
||||||
|
rimEnd(c, b, RIM.x1, RIM.y); rimEnd(c, b, RIM.x2, RIM.y);
|
||||||
|
// Il canestro: la palla attraversa l'anello scendendo, fra il davanti e il dietro, e non da sotto.
|
||||||
|
const inside = b.x > RIM.x1 + BALL * 0.6 && b.x < RIM.x2 - BALL * 0.6;
|
||||||
|
if (inside && before <= RIM.y && b.y > RIM.y && b.vy > 0 && !b.scored && !b.under) {
|
||||||
|
b.scored = true; b.scoredAt = b.time; b.vx *= 0.35; b.vy *= 0.5;
|
||||||
|
hit(c, b.rim || b.board ? 'basket-in' : 'basket-swish', 0.7);
|
||||||
|
// Il canestro conta subito: il punteggio e, all'ultimo pallone, il risultato non aspettano la palla ferma.
|
||||||
|
c.baskets++; c.points += spot(c).points;
|
||||||
|
if (c.thrown >= BALLS) finish(c);
|
||||||
|
} else if (inside && before >= RIM.y && b.y < RIM.y && b.vy < 0) b.under = true;
|
||||||
|
if (b.scored && b.y < NET + BALL) { b.vx *= 1 - 2.5 * dt; b.vy *= 1 - 1.2 * dt; }
|
||||||
|
if (b.y + BALL > FLOOR) {
|
||||||
|
b.y = FLOOR - BALL;
|
||||||
|
if (b.vy > 40) { b.vy = -b.vy * 0.55; b.vx *= 0.85; b.bounces++; hit(c, 'basket-bounce', b.vy / -400); }
|
||||||
|
else { b.vy = 0; b.vx *= 1 - 3 * dt; }
|
||||||
|
}
|
||||||
|
const still = Math.hypot(b.vx, b.vy) < 10;
|
||||||
|
// Entrata, la palla è finita: non si aspetta che rotoli. Una palla posata sul ferro, nemmeno.
|
||||||
|
if (b.x < -20 || b.time > FLIGHT_MAX || b.bounces > 6 || (b.y + BALL >= FLOOR - 0.5 && Math.abs(b.vx) < 6 && b.vy === 0)
|
||||||
|
|| (b.scored && b.y > NET + 30) || (still && b.time > 1.5)) b.dead = true;
|
||||||
|
}
|
||||||
|
function finish(c) {
|
||||||
|
c.result = { winner: c.baskets >= c.skill.goal ? 'player' : 'cpu', baskets: c.baskets, points: c.points, goal: c.skill.goal };
|
||||||
|
}
|
||||||
|
function nextBall(c) {
|
||||||
|
const b = c.ball;
|
||||||
|
c.history.push({ spot: c.spot, scored: b.scored, rim: b.rim, board: b.board });
|
||||||
|
if (b.scored) c.spot = Math.min(c.spot + 1, SPOTS.length - 1);
|
||||||
|
if (c.thrown >= BALLS) { if (!c.result) finish(c); c.step = 'result'; } else c.step = 'aim';
|
||||||
|
c.t = 0;
|
||||||
|
}
|
||||||
|
export function update(c, dt) {
|
||||||
|
if (!Number.isFinite(dt) || dt <= 0) return;
|
||||||
|
dt = Math.min(dt, 0.25); c.t += dt;
|
||||||
|
if (c.holding) { c.holdTime += dt; c.power = powerAt(c.holdTime); }
|
||||||
|
if (c.step === 'flight') {
|
||||||
|
c.accumulator += dt;
|
||||||
|
while (c.accumulator >= FIXED_STEP && !c.ball.dead) { physics(c, FIXED_STEP); c.accumulator -= FIXED_STEP; }
|
||||||
|
const b = c.ball;
|
||||||
|
if (!b.dead && Math.hypot(b.vx, b.vy) > 60) { c.trails.push({ x: b.x, y: b.y, age: 0 }); if (c.trails.length > 14) c.trails.shift(); }
|
||||||
|
if (b.dead) { c.step = 'settle'; c.t = 0; }
|
||||||
|
} else if (c.step === 'settle' && c.t >= REST) nextBall(c);
|
||||||
|
for (const t of c.trails) t.age += dt;
|
||||||
|
c.trails = c.trails.filter(t => t.age < 0.5);
|
||||||
|
}
|
||||||
|
export function status(c, names = { player: 'Andrea' }) {
|
||||||
|
const tally = `Canestri ${c.baskets} (ne servono ${c.skill.goal}), punti ${c.points}.`;
|
||||||
|
if (c.step === 'result') return `${c.result.baskets} canestri su ${BALLS}, ${c.result.points} punti. ${c.result.winner === 'player' ? `Obiettivo ${c.result.goal}: fatto!` : `Ne servivano ${c.result.goal}.`}`;
|
||||||
|
if (c.step === 'flight') return c.ball.scored ? `Dentro! ${tally}` : 'Palla in aria…';
|
||||||
|
if (c.step === 'settle') return c.ball.scored ? `Canestro! ${tally}` : `Fuori. ${tally}`;
|
||||||
|
return `${names.player}, pallone ${c.thrown + 1} di ${BALLS}, ${spot(c).name}. ${tally}`;
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
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 basket from './basket.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';
|
||||||
@@ -14,6 +16,8 @@ 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 },
|
||||||
|
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 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VERBS = ['walk', 'look', 'take', 'use', 'talk', 'open', 'give', 'drop'];
|
export const VERBS = ['walk', 'look', 'take', 'use', 'talk', 'open', 'give', 'drop'];
|
||||||
@@ -70,6 +74,22 @@ export function shuffledCredits(credits, random = Math.random) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Le immagini del carosello dei Ringraziamenti: tutti gli sfondi del paese (uno per scena, senza doppioni) col nome
|
||||||
|
// del posto, e un ritratto dei crediti ogni quattro sfondi; i ritratti che non trovano posto chiudono la fila.
|
||||||
|
export function reelPictures(story) {
|
||||||
|
const seen = new Set(), scenes = [];
|
||||||
|
for (const s of Object.values(story.scenes)) {
|
||||||
|
if (!s.background || seen.has(s.background)) continue;
|
||||||
|
seen.add(s.background);
|
||||||
|
scenes.push({ src: s.background, name: s.name, kind: 'scene' });
|
||||||
|
}
|
||||||
|
const faces = (story.credits ?? []).flatMap(c => c.names).filter(n => n.face).map(facePicture);
|
||||||
|
let next = 0;
|
||||||
|
const placed = scenes.flatMap((p, i) => i % 4 === 3 && next < faces.length ? [p, faces[next++]] : [p]);
|
||||||
|
return [...placed, ...faces.slice(next)];
|
||||||
|
}
|
||||||
|
export const facePicture = n => ({ src: n.face, name: n.name, kind: n.face.endsWith('palio.png') ? 'palio' : 'face' });
|
||||||
|
|
||||||
// La ricerca della mappa: ogni parola scritta deve stare nel nome del posto, senza badare a maiuscole, accenti e
|
// La ricerca della mappa: ogni parola scritta deve stare nel nome del posto, senza badare a maiuscole, accenti e
|
||||||
// apostrofi («camp» trova Campomigliaio, «de medici» trova Via de' Medici).
|
// apostrofi («camp» trova Campomigliaio, «de medici» trova Via de' Medici).
|
||||||
const plain = text => text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, ' ').trim();
|
const plain = text => text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, ' ').trim();
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { BANK, WATER, CASTS, left, fishCount, distanceFor, status } from './pesca.js';
|
||||||
|
import { text, panel } from './bocce-render.js';
|
||||||
|
import { ambientHash as hash } from './ambient.js';
|
||||||
|
|
||||||
|
const ANDREA = { x: 92, y: 258, scale: 0.68 }, BOYS = { x: 44, y: 262, scale: 0.5 };
|
||||||
|
// La punta della canna in ogni posa, rispetto ai piedi di Andrea (misurata sul foglio di Codex): la lenza parte da lì.
|
||||||
|
const TIPS = { hold: [60, -81], cast: [80, -57], reel: [43, -66], show: [60, -81] };
|
||||||
|
const ROD_TIP = { x: ANDREA.x + TIPS.hold[0], y: ANDREA.y + TIPS.hold[1] };
|
||||||
|
export function andreaPose(c) {
|
||||||
|
if (c.step === 'cast') return 'cast';
|
||||||
|
if (c.step === 'fight') return 'reel';
|
||||||
|
if (c.step === 'settle' && c.how === 'landed') return c.fish?.gag ? 'hold' : 'show';
|
||||||
|
return 'hold';
|
||||||
|
}
|
||||||
|
function sprite(ctx, sheet, pose, { x, y, scale }) {
|
||||||
|
if (!sheet) return;
|
||||||
|
const frame = sheet.atlas.frames[pose] ?? Object.values(sheet.atlas.frames)[0];
|
||||||
|
ctx.drawImage(sheet.img, frame.x, frame.y, frame.w, frame.h,
|
||||||
|
Math.round(x - frame.ax * scale), Math.round(y - frame.ay * scale), Math.round(frame.w * scale), Math.round(frame.h * scale));
|
||||||
|
}
|
||||||
|
// Dove sta la punta della canna: segue la posa, così la lenza parte sempre dalla cima.
|
||||||
|
function rodTip(c) {
|
||||||
|
const [dx, dy] = TIPS[andreaPose(c)] ?? TIPS.hold;
|
||||||
|
return { x: ANDREA.x + dx, y: ANDREA.y + dy };
|
||||||
|
}
|
||||||
|
function floatAt(c) {
|
||||||
|
const f = c.float;
|
||||||
|
if (!f) return null;
|
||||||
|
if (c.step === 'cast') {
|
||||||
|
// La parabola del lancio dalla punta della canna all'acqua.
|
||||||
|
const tip = rodTip(c), k = f.fly, x = tip.x + (f.x - tip.x) * k, y = tip.y + (WATER - tip.y) * k - Math.sin(k * Math.PI) * 60;
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
return { x: f.x, y: WATER + f.dip + Math.sin(c.t * 2.2 + f.x) * 0.8 };
|
||||||
|
}
|
||||||
|
function fishShadow(ctx, c, p) {
|
||||||
|
const fish = c.fish; if (!fish || c.step !== 'fight') return;
|
||||||
|
const w = 10 + fish.weight * 9, h = 4 + fish.weight * 2.5, x = p.x + 8 + Math.sin(c.t * 3) * 6, y = WATER + 22 + Math.sin(c.t * 1.7) * 5;
|
||||||
|
ctx.fillStyle = 'rgba(20,40,38,0.55)';
|
||||||
|
ctx.beginPath(); ctx.ellipse(x, y, w / 2, h / 2, 0, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.beginPath(); ctx.moveTo(x + w / 2, y); ctx.lineTo(x + w / 2 + 5, y - 4); ctx.lineTo(x + w / 2 + 5, y + 4); ctx.fill();
|
||||||
|
}
|
||||||
|
function fishIcon(ctx, f, x, y) {
|
||||||
|
if (f.gag) { ctx.fillStyle = '#4a3a2a'; ctx.fillRect(x - 3, y - 5, 4, 8); ctx.fillRect(x - 3, y + 1, 8, 3); return; }
|
||||||
|
const w = 6 + f.weight * 3;
|
||||||
|
ctx.fillStyle = f.kind === 'carpa' ? '#c9a24a' : f.kind === 'tinca' ? '#5a7a3a' : f.kind === 'persico' ? '#d9b14a' : '#9fb3b8';
|
||||||
|
ctx.beginPath(); ctx.ellipse(x, y, w / 2, 2.5 + f.weight, 0, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.beginPath(); ctx.moveTo(x - w / 2, y); ctx.lineTo(x - w / 2 - 3, y - 3); ctx.lineTo(x - w / 2 - 3, y + 3); ctx.fill();
|
||||||
|
ctx.fillStyle = '#1a1a1a'; ctx.fillRect(Math.round(x + w / 2 - 3), Math.round(y - 1), 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawPesca(ctx, c, def, bg, sheet, boys) {
|
||||||
|
ctx.save();
|
||||||
|
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
|
||||||
|
else { ctx.fillStyle = '#5c7f7a'; ctx.fillRect(0, 0, 640, 360); ctx.fillStyle = '#7a9a4a'; ctx.fillRect(0, 240, BANK, 120); }
|
||||||
|
// I ragazzi sulla riva, dietro Andrea, intenti alle loro canne.
|
||||||
|
sprite(ctx, boys, 'work', BOYS);
|
||||||
|
sprite(ctx, sheet, andreaPose(c), ANDREA);
|
||||||
|
// I cerchi nell'acqua: al tuffo del galleggiante e quando il pesce abbocca.
|
||||||
|
for (const r of c.ripples) {
|
||||||
|
ctx.strokeStyle = `rgba(220,240,235,${0.6 * (1 - r.age / 1.6)})`; ctx.lineWidth = 1;
|
||||||
|
for (const k of [0, 0.4]) { const a = r.age - k; if (a < 0) continue; ctx.beginPath(); ctx.ellipse(r.x, WATER, 4 + a * 18, 1.5 + a * 5, 0, 0, Math.PI * 2); ctx.stroke(); }
|
||||||
|
}
|
||||||
|
// A lancio finito (pesce in mano, lenza rotta o vuota) niente galleggiante: la lenza è fuori dall'acqua.
|
||||||
|
const p = ['cast', 'wait', 'fight'].includes(c.step) ? floatAt(c) : null;
|
||||||
|
if (p) {
|
||||||
|
// La lenza, con un po' di pancia; tesa e tremante quando il pesce tira.
|
||||||
|
const tip = rodTip(c), sag = c.step === 'fight' ? 2 - c.tension * 2 : c.step === 'wait' ? 10 : 4;
|
||||||
|
// Trema già allo strattone, prima che la lenza si tenda: il tempo di mollare.
|
||||||
|
const shake = c.step === 'fight' && (c.tension > 0.75 || c.danger > 0.55) ? Math.sin(c.t * 60) * 1.2 : 0;
|
||||||
|
ctx.strokeStyle = c.step === 'fight' && c.tension > 0.85 ? '#ffb0a0' : 'rgba(240,240,230,0.85)'; ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath(); ctx.moveTo(tip.x, tip.y); ctx.quadraticCurveTo((tip.x + p.x) / 2, Math.max(tip.y, p.y) + sag + shake, p.x, p.y); ctx.stroke();
|
||||||
|
fishShadow(ctx, c, p);
|
||||||
|
// Il galleggiante: rosso sopra, bianco sotto; sott'acqua si vede appena.
|
||||||
|
const under = Math.max(0, p.y - WATER);
|
||||||
|
ctx.fillStyle = '#e2e2d8'; ctx.fillRect(Math.round(p.x) - 1, Math.round(p.y) - 1, 3, 4);
|
||||||
|
ctx.fillStyle = '#d8402a'; ctx.fillRect(Math.round(p.x) - 1, Math.round(p.y) - 6, 3, 5);
|
||||||
|
if (under > 0) { ctx.fillStyle = 'rgba(40,70,80,0.55)'; ctx.fillRect(Math.round(p.x) - 2, WATER, 5, Math.round(under) + 1); }
|
||||||
|
}
|
||||||
|
// La mira: dove cadrà il galleggiante, e il filo tratteggiato della parabola.
|
||||||
|
if (c.step === 'aim' && c.casts < CASTS) {
|
||||||
|
const x = distanceFor(c.power);
|
||||||
|
ctx.strokeStyle = 'rgba(255,246,210,0.9)'; ctx.setLineDash([2, 4]);
|
||||||
|
ctx.beginPath(); ctx.moveTo(ROD_TIP.x, ROD_TIP.y); ctx.quadraticCurveTo((ROD_TIP.x + x) / 2, ROD_TIP.y - 70, x, WATER); ctx.stroke(); ctx.setLineDash([]);
|
||||||
|
ctx.beginPath(); ctx.ellipse(x, WATER, 7, 2.5, 0, 0, Math.PI * 2); ctx.stroke();
|
||||||
|
ctx.beginPath(); ctx.moveTo(x, WATER - 8); ctx.lineTo(x, WATER + 8); ctx.stroke();
|
||||||
|
}
|
||||||
|
panel(ctx, 182, 50, 440, 24);
|
||||||
|
text(ctx, status(c, def.names), 402, 63, 6, '#fff0cb', 'center');
|
||||||
|
// I lanci rimasti, il cestino dei pesci.
|
||||||
|
panel(ctx, 18, 86, 196, 58);
|
||||||
|
text(ctx, 'LANCI', 26, 98, 6, '#eed5a0');
|
||||||
|
for (let n = 0; n < CASTS; n++) {
|
||||||
|
const x = 70 + n * 12, y = 98;
|
||||||
|
if (n < left(c)) { ctx.fillStyle = '#d8402a'; ctx.fillRect(x - 1, y - 4, 3, 4); ctx.fillStyle = '#e2e2d8'; ctx.fillRect(x - 1, y, 3, 3); }
|
||||||
|
else { ctx.strokeStyle = '#766e5c'; ctx.strokeRect(x - 2, y - 3, 5, 6); }
|
||||||
|
}
|
||||||
|
text(ctx, 'CESTINO', 26, 116, 6, '#eed5a0');
|
||||||
|
c.caught.forEach((f, i) => fishIcon(ctx, f, 80 + i * 22, 116));
|
||||||
|
const fish = fishCount(c);
|
||||||
|
text(ctx, `${fish} su ${c.skill.goal} per la gloria · ${c.weight} kg`, 26, 133, 6, fish >= c.skill.goal ? '#b9e5b3' : '#d7ccb4');
|
||||||
|
// In basso a sinistra: la forza del lancio, o la tensione della lenza.
|
||||||
|
if (c.step === 'aim' || c.holding) {
|
||||||
|
panel(ctx, 18, 306, 176, 37);
|
||||||
|
text(ctx, `FORZA ${Math.round(c.power * 100)}%`, 30, 318, 7);
|
||||||
|
ctx.fillStyle = '#3e3e36'; ctx.fillRect(30, 331, 150, 5);
|
||||||
|
ctx.fillStyle = '#b9d392'; ctx.fillRect(30, 331, Math.round(c.power * 150), 5);
|
||||||
|
} else if (c.step === 'fight') {
|
||||||
|
panel(ctx, 18, 306, 176, 37);
|
||||||
|
text(ctx, `LENZA ${Math.round(c.tension * 100)}%`, 30, 318, 7, c.tension > 0.85 ? '#ffb0a0' : '#f4e3c2');
|
||||||
|
text(ctx, `riva ${Math.round((1 - Math.min(1, c.fish.dist)) * 100)}%`, 182, 318, 6, '#d7ccb4', 'right');
|
||||||
|
ctx.fillStyle = '#3e3e36'; ctx.fillRect(30, 331, 150, 5);
|
||||||
|
ctx.fillStyle = 'rgba(224,87,74,0.35)'; ctx.fillRect(30 + 127, 331, 23, 5); // la zona rossa: qui si molla
|
||||||
|
ctx.fillStyle = c.tension > 0.85 ? '#e0574a' : c.tension > 0.65 ? '#e0b24a' : '#b9d392'; ctx.fillRect(30, 331, Math.round(c.tension * 150), 5);
|
||||||
|
ctx.fillStyle = '#fff'; ctx.fillRect(30 + 127, 329, 1, 9);
|
||||||
|
}
|
||||||
|
if (c.step === 'result') {
|
||||||
|
panel(ctx, 206, 91, 397, 78);
|
||||||
|
text(ctx, c.result.winner === 'player' ? 'BELLA PESCATA!' : 'ACQUA CHETA', 404, 108, 9, '#efd49e', 'center');
|
||||||
|
text(ctx, `${c.result.fish} pesci su ${CASTS} lanci, ${c.result.weight} kg`, 404, 130, 9, c.result.winner === 'player' ? '#b9e5b3' : '#f1d9be', 'center');
|
||||||
|
text(ctx, c.result.winner === 'player' ? `Obiettivo ${c.result.goal}: fatto.` : `Per la gloria ne servivano ${c.result.goal}.`, 404, 148, 6, '#d6c7af', 'center');
|
||||||
|
c.result.caught.forEach((f, i) => fishIcon(ctx, f, 404 - (c.result.caught.length - 1) * 11 + i * 22, 160));
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
// La pesca al lago dell'Anatre coi ragazzi delle canne (idea di Andrea, 28 ottobre). Vista di lato, coordinate dello
|
||||||
|
// schermo (640x360): Andrea sulla riva a sinistra, l'acqua a destra. Si tiene premuto per lanciare (più forza, più
|
||||||
|
// lontano: e al largo i pesci sono più grossi); il galleggiante balla, ai tocchi finti non si abbocca, quando va sotto
|
||||||
|
// davvero si ferra premendo in tempo e, senza lasciare, si recupera: quando il pesce strattona la lenza si tende, e
|
||||||
|
// bisogna mollare un attimo. Sei lanci; la gloria dipende da quanti pesci si tirano a riva (LEVELS).
|
||||||
|
// Rivista con la revisione di Codex del 28 ottobre (galleggiante che saltava, sesto lancio bloccato dal pannello,
|
||||||
|
// invito a ferrare fuori tempo, lenza che non si spezzava mai, mira che si sbloccava da sola).
|
||||||
|
export const BANK = 150, WATER = 250, NEAR = 190, FAR = 600, SHORE = NEAR - 20;
|
||||||
|
export const CASTS = 6, SWING = 1.4, FLY = 0.8, REST = 1.4, WAIT_MAX = 45;
|
||||||
|
export const LEVELS = {
|
||||||
|
bona: { goal: 2, window: 1.0, pull: 0.5 },
|
||||||
|
seria: { goal: 3, window: 0.62, pull: 0.78 },
|
||||||
|
male: { goal: 4, window: 0.38, pull: 1.0 },
|
||||||
|
};
|
||||||
|
// I pesci del lago, dal più piccolo al più grosso: al largo si pescano i grossi. Lo stivale è la beffa della riva.
|
||||||
|
export const FISHES = [
|
||||||
|
{ kind: 'stivale', name: 'uno stivale', weight: 0.9, strength: 0.25, stamina: 0.3, far: [0, 0.25], gag: true },
|
||||||
|
{ kind: 'lasca', name: 'una lasca', weight: 0.15, strength: 0.3, stamina: 0.5, far: [0, 0.55] },
|
||||||
|
{ kind: 'persico', name: 'un persico sole', weight: 0.3, strength: 0.45, stamina: 0.7, far: [0.1, 0.7] },
|
||||||
|
{ kind: 'cavedano', name: 'un cavedano', weight: 0.8, strength: 0.6, stamina: 0.9, far: [0.3, 0.9] },
|
||||||
|
{ kind: 'tinca', name: 'una tinca', weight: 1.4, strength: 0.75, stamina: 1.1, far: [0.45, 1] },
|
||||||
|
{ kind: 'carpa', name: 'una carpa', weight: 3.2, strength: 1, stamina: 1.4, far: [0.7, 1] },
|
||||||
|
];
|
||||||
|
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||||
|
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
|
||||||
|
|
||||||
|
export function newPesca(seed = Date.now() >>> 0, def = {}) {
|
||||||
|
const level = LEVELS[def.level] ? def.level : 'seria';
|
||||||
|
return { step: 'aim', t: 0, age: 0, seed, level, skill: LEVELS[level], current: 'player', casts: 0, caught: [], weight: 0,
|
||||||
|
power: 0.5, holding: false, holdTime: 0, reeling: false, float: null, fish: null, tension: 0, pull: 0, events: [], ripples: [], last: null };
|
||||||
|
}
|
||||||
|
export const left = c => Math.max(0, CASTS - c.casts);
|
||||||
|
export const fishCount = c => c.caught.filter(f => !f.gag).length;
|
||||||
|
// Tre domande diverse: si può lanciare, si può agire (ferrare, recuperare), si può regolare la distanza.
|
||||||
|
export const canCast = c => c.step === 'aim' && !c.holding && c.casts < CASTS;
|
||||||
|
export const canThrow = c => canCast(c) || ((c.step === 'wait' || c.step === 'fight') && !c.holding);
|
||||||
|
export const canAdjust = c => c.step === 'aim' && !c.holding && c.casts < CASTS;
|
||||||
|
export const aimFree = c => c.step !== 'aim';
|
||||||
|
export const powerAt = time => clamp(time / SWING, 0, 1);
|
||||||
|
export const distanceFor = power => NEAR + (FAR - NEAR) * clamp(power, 0, 1);
|
||||||
|
export function setPower(c, value) {
|
||||||
|
if (!canAdjust(c) || !Number.isFinite(value)) return false;
|
||||||
|
c.power = clamp(value, 0, 1); return true;
|
||||||
|
}
|
||||||
|
// Un tocco sull'acqua (non sul cielo, non sulla riva) sceglie dove lanciare: la forza che ci vuole per arrivarci.
|
||||||
|
export function aimScreen(c, [x, y]) {
|
||||||
|
if (!canAdjust(c) || !Number.isFinite(x) || !Number.isFinite(y) || x < BANK + 10 || y < WATER - 40) return false;
|
||||||
|
return setPower(c, (x - NEAR) / (FAR - NEAR));
|
||||||
|
}
|
||||||
|
export const nudge = (c, dir, fine = false) => setPower(c, c.power + dir * (fine ? 0.01 : 0.05));
|
||||||
|
|
||||||
|
function event(c, kind, strength = 0.5) { if (c.events.length < 12) c.events.push({ kind, power: clamp(strength, 0, 1) }); }
|
||||||
|
// Quando arriva il pesce: da uno a tre tocchi finti, a ritmo diverso, poi quello vero. Al largo si aspetta un po' di
|
||||||
|
// più; dopo un'abboccata mancata l'attesa è la metà.
|
||||||
|
function schedule(c) {
|
||||||
|
const f = c.float, far = (f.x - NEAR) / (FAR - NEAR);
|
||||||
|
f.biteAt = f.t + (2.5 + random(c) * 4 + far * 2) * (f.missed ? 0.5 : 1);
|
||||||
|
const n = 1 + Math.floor(random(c) * 3);
|
||||||
|
f.nibbles = Array.from({ length: n }, (_, i) => ({ at: f.biteAt - (n - i) * (0.6 + random(c) * 0.9), for: 0.18 + random(c) * 0.2 }));
|
||||||
|
f.bitten = false;
|
||||||
|
}
|
||||||
|
function pickFish(c, far) {
|
||||||
|
const pool = FISHES.filter(f => far >= f.far[0] && far <= f.far[1] && (!f.gag || random(c) < 0.35));
|
||||||
|
const f = pool[Math.floor(random(c) * pool.length)] ?? FISHES[1];
|
||||||
|
const size = 0.8 + random(c) * 0.5; // non tutti i cavedani sono uguali
|
||||||
|
return { ...f, weight: Math.round(f.weight * size * 100) / 100, stamina: f.stamina * size, dist: 1, energy: 1, burst: random(c) * 6,
|
||||||
|
reel: 0.22 / (0.7 + f.stamina * size) }; // i piccoli vengono a riva in fretta, le carpe no
|
||||||
|
}
|
||||||
|
export function cast(c) {
|
||||||
|
if (!canCast(c)) return false;
|
||||||
|
c.float = { x: distanceFor(c.power), y: WATER, t: 0, total: 0, fly: 0, dip: 0, missed: 0, from: null };
|
||||||
|
c.step = 'cast'; c.t = 0; c.casts++; c.fish = null; c.tension = 0; c.pull = 0; c.reeling = false; c.last = null;
|
||||||
|
event(c, 'pesca-lancio', c.power);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Il tasto dei comandi: in attesa ferra; nel recupero alterna recupera e molla (col dito si tiene premuto da sé).
|
||||||
|
export function shoot(c) {
|
||||||
|
if (c.step === 'aim') return cast(c);
|
||||||
|
if (c.step === 'wait') return strike(c);
|
||||||
|
if (c.step === 'fight') { c.reeling = !c.reeling; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Un gesto solo: premendo si ferra e, se il pesce c'è, si comincia subito a recuperare; lasciando si molla.
|
||||||
|
export function press(c) {
|
||||||
|
if (c.step === 'aim') { if (!canCast(c)) return false; c.previousPower = c.power; c.holding = true; c.holdTime = c.power = 0; return true; }
|
||||||
|
if (c.step === 'wait') { strike(c); if (c.step === 'fight') c.reeling = true; return true; }
|
||||||
|
if (c.step === 'fight') { c.reeling = true; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
export function cancel(c) {
|
||||||
|
if (c.holding) { c.holding = false; c.power = c.previousPower; c.holdTime = 0; return true; }
|
||||||
|
if (c.reeling) { c.reeling = false; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
export function release(c) {
|
||||||
|
if (c.holding) {
|
||||||
|
if (c.holdTime < 0.14) return cancel(c); // un tocco breve non spreca il lancio
|
||||||
|
c.holding = false; return cast(c);
|
||||||
|
}
|
||||||
|
if (c.reeling) { c.reeling = false; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
export const biting = c => c.step === 'wait' && c.float?.bitten && c.float.t - c.float.biteAt <= c.skill.window;
|
||||||
|
// Ferrare: dentro la finestra il pesce è preso; prima, o dopo, si recupera la lenza vuota (lancio sprecato).
|
||||||
|
export function strike(c) {
|
||||||
|
const f = c.float;
|
||||||
|
if (c.step !== 'wait' || !f) return false;
|
||||||
|
if (biting(c)) {
|
||||||
|
c.fish = pickFish(c, (f.x - NEAR) / (FAR - NEAR));
|
||||||
|
f.from = f.x;
|
||||||
|
c.step = 'fight'; c.t = 0; c.tension = 0.3; c.reeling = false; c.danger = 0;
|
||||||
|
event(c, 'pesca-ferrata', 0.8);
|
||||||
|
} else {
|
||||||
|
const late = f.bitten && f.t > f.biteAt;
|
||||||
|
const touched = f.nibbles.some(n => f.t >= n.at);
|
||||||
|
c.last = { kind: late ? 'late' : touched ? 'nibble' : 'early',
|
||||||
|
text: late ? 'Troppo tardi: l\'ha già sputato.' : touched ? 'Era solo un tocco: a tirare così l\'ho spaventato.' : 'Ho tirato a vuoto: non aveva ancora abboccato.' };
|
||||||
|
event(c, 'pesca-vuoto', 0.4);
|
||||||
|
end(c, 'empty');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function end(c, how) {
|
||||||
|
if (how === 'landed') {
|
||||||
|
c.caught.push(c.fish);
|
||||||
|
if (!c.fish.gag) c.weight = Math.round((c.weight + c.fish.weight) * 100) / 100;
|
||||||
|
event(c, 'pesca-preso', 0.9);
|
||||||
|
}
|
||||||
|
if (how === 'broken') event(c, 'pesca-rottura', 1);
|
||||||
|
if (how === 'free') event(c, 'pesca-vuoto', 0.6);
|
||||||
|
c.step = 'settle'; c.t = 0; c.how = how; c.reeling = false; c.pull = 0;
|
||||||
|
// L'ultimo lancio decide subito il risultato: uscire durante la posa non è abbandonare.
|
||||||
|
if (c.casts >= CASTS) finish(c);
|
||||||
|
}
|
||||||
|
function finish(c) {
|
||||||
|
c.result = { winner: fishCount(c) >= c.skill.goal ? 'player' : 'cpu', fish: fishCount(c), weight: c.weight, goal: c.skill.goal, caught: c.caught };
|
||||||
|
}
|
||||||
|
export function update(c, dt) {
|
||||||
|
if (!Number.isFinite(dt) || dt <= 0) return;
|
||||||
|
dt = Math.min(dt, 0.25); c.t += dt;
|
||||||
|
if (c.holding) { c.holdTime += dt; c.power = powerAt(c.holdTime); }
|
||||||
|
const f = c.float;
|
||||||
|
if (c.step === 'cast') {
|
||||||
|
f.fly = Math.min(1, c.t / FLY);
|
||||||
|
if (c.t >= FLY) { c.step = 'wait'; f.t = 0; schedule(c); c.ripples.push({ x: f.x, age: 0 }); event(c, 'pesca-tuffo', 0.4); }
|
||||||
|
} else if (c.step === 'wait') {
|
||||||
|
f.t += dt; f.total += dt;
|
||||||
|
const nibbling = f.nibbles.some(n => f.t >= n.at && f.t < n.at + n.for);
|
||||||
|
if (!f.bitten && f.t >= f.biteAt) { f.bitten = true; event(c, 'pesca-abbocca', 0.6); c.ripples.push({ x: f.x, age: 0 }); }
|
||||||
|
const since = f.bitten ? f.t - f.biteAt : -1;
|
||||||
|
if (f.bitten && since > c.skill.window + 0.4) {
|
||||||
|
// Non ferrato in tempo: il pesce se ne va, si riprova, ma non all'infinito.
|
||||||
|
f.missed++; f.t = 0; schedule(c);
|
||||||
|
if (f.missed >= 3) { c.last = { kind: 'gone', text: 'Tre abboccate e niente: qui non abbocca più nessuno.' }; end(c, 'empty'); }
|
||||||
|
} else if (nibbling) { if (!f.nibbleSound) { event(c, 'pesca-tocco', 0.3); f.nibbleSound = true; } }
|
||||||
|
else f.nibbleSound = false;
|
||||||
|
// Sotto finché si può ferrare; poi riaffiora mentre il pesce lo sputa; i tocchi lo abbassano appena.
|
||||||
|
f.dip = biting(c) ? 7 : f.bitten && since <= c.skill.window + 0.4 ? 3 : nibbling ? 2.5 : 0;
|
||||||
|
if (f.total > WAIT_MAX) { c.last = { kind: 'gone', text: 'Niente. Qui i pesci dormono.' }; end(c, 'empty'); }
|
||||||
|
} else if (c.step === 'fight') {
|
||||||
|
const fish = c.fish, k = c.skill.pull;
|
||||||
|
fish.burst += dt;
|
||||||
|
// Gli strattoni: brevi, a picco; la lenza tesa li sente poco dopo, e c'è il tempo di mollare.
|
||||||
|
const burst = 0.35 + 0.95 * Math.max(0, Math.sin(fish.burst * 2.1)) ** 3, peak = fish.strength * k * (0.3 + 0.7 * fish.energy);
|
||||||
|
c.pull = peak * burst;
|
||||||
|
c.danger = burst / 1.3; // quanto è forte lo strattone rispetto al massimo di questo pesce: la lenza trema prima
|
||||||
|
const target = c.reeling ? 0.42 + c.pull * 1.1 : c.pull * 0.3;
|
||||||
|
c.tension += (target - c.tension) * Math.min(1, dt * 6.5);
|
||||||
|
if (c.reeling) {
|
||||||
|
fish.dist -= dt * fish.reel * (1 - c.pull * 0.4); fish.energy = Math.max(0, fish.energy - dt * 0.09 / fish.stamina);
|
||||||
|
// Il cigolio della lenza tesa, non a ogni fotogramma.
|
||||||
|
c.reelSound = (c.reelSound ?? 0) - dt;
|
||||||
|
if (c.tension > 0.6 && c.reelSound <= 0) { event(c, 'pesca-mulinello', c.tension); c.reelSound = 0.14; }
|
||||||
|
} else { fish.dist += dt * (0.02 + 0.05 * c.pull); fish.energy = Math.max(0, fish.energy - dt * 0.03 / fish.stamina); }
|
||||||
|
f.x = SHORE + fish.dist * (f.from - SHORE);
|
||||||
|
f.dip = 5 + Math.sin(c.t * 7) * 2;
|
||||||
|
if (c.tension >= 1) { c.last = { kind: 'broken', text: 'Crac! La lenza non ha retto.' }; end(c, 'broken'); }
|
||||||
|
else if (fish.dist >= 1.35) { c.last = { kind: 'free', text: 'Ha preso troppo filo: si è slamato.' }; end(c, 'free'); }
|
||||||
|
else if (fish.dist <= 0) {
|
||||||
|
c.last = { kind: 'landed', text: fish.gag ? 'Uno stivale. Numero quarantatré. Di chi sarà?' : `${fish.name[0].toUpperCase()}${fish.name.slice(1)}, ${fish.weight} kg!` };
|
||||||
|
end(c, 'landed');
|
||||||
|
}
|
||||||
|
} else if (c.step === 'settle' && c.t >= REST) {
|
||||||
|
c.float = null; c.fish = null; c.tension = 0;
|
||||||
|
if (c.casts >= CASTS) c.step = 'result'; else { c.step = 'aim'; c.t = 0; }
|
||||||
|
}
|
||||||
|
for (const r of c.ripples) r.age += dt;
|
||||||
|
c.ripples = c.ripples.filter(r => r.age < 1.6);
|
||||||
|
}
|
||||||
|
export function status(c, names = { player: 'Andrea' }) {
|
||||||
|
const tally = `Pesci ${fishCount(c)} (ne servono ${c.skill.goal}), ${c.weight} kg.`;
|
||||||
|
if (c.step === 'result') return `${c.result.fish} pesci su ${CASTS} lanci, ${c.result.weight} kg. ${c.result.winner === 'player' ? `Obiettivo ${c.result.goal}: fatto!` : `Ne servivano ${c.result.goal}.`}`;
|
||||||
|
if (c.step === 'cast') return 'Lancio…';
|
||||||
|
if (c.step === 'wait') return biting(c) ? 'ABBOCCA! Ferra!' : c.float.bitten ? 'Lo sta sputando…' : c.float.dip > 0 ? 'Un tocco… aspetta.' : c.float.missed ? `Riprova: ${3 - c.float.missed} abboccate ancora.` : 'Il galleggiante è fermo. Si aspetta.';
|
||||||
|
if (c.step === 'fight') return c.tension > 0.85 ? 'La lenza tira troppo: MOLLA!' : c.danger > 0.55 ? 'Strattona…' : c.reeling ? 'Recupera…' : 'Tieni premuto per recuperare.';
|
||||||
|
if (c.step === 'settle') return c.last?.text ?? '';
|
||||||
|
return `${names.player}, lancio ${c.casts + 1} di ${CASTS}. ${tally}`;
|
||||||
|
}
|
||||||
|
export const button = c => c.step === 'result' ? 'Finita' : c.step === 'wait' ? 'Ferra' : c.step === 'fight' ? (c.reeling ? 'Molla' : 'Recupera') : c.step === 'aim' ? 'Lancia' : '…';
|
||||||
@@ -5,6 +5,8 @@ import { ambientEffects, ambientHash } from './ambient.js';
|
|||||||
import { drawTorch } from './torch.js';
|
import { drawTorch } from './torch.js';
|
||||||
import { drawBocce } from './bocce-render.js';
|
import { drawBocce } from './bocce-render.js';
|
||||||
import { drawBiliardo } from './biliardo-render.js';
|
import { drawBiliardo } from './biliardo-render.js';
|
||||||
|
import { drawBasket } from './basket-render.js';
|
||||||
|
import { drawPesca } from './pesca-render.js';
|
||||||
import { drawBriscola } from './briscola-render.js';
|
import { drawBriscola } from './briscola-render.js';
|
||||||
import { drawTressette } from './tressette-render.js';
|
import { drawTressette } from './tressette-render.js';
|
||||||
import { drawScopa } from './scopa-render.js';
|
import { drawScopa } from './scopa-render.js';
|
||||||
@@ -682,6 +684,16 @@ function drawCloseup(ctx, game, time) {
|
|||||||
drawBocce(ctx, c, def, backgrounds.get(def.picture), time);
|
drawBocce(ctx, c, def, backgrounds.get(def.picture), time);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (def.type === 'pesca') {
|
||||||
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
|
drawPesca(ctx, c, def, backgrounds.get(def.picture), sprites.get('andrea_pesca'), sprites.get('pescatori'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (def.type === 'basket') {
|
||||||
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
|
drawBasket(ctx, c, def, backgrounds.get(def.picture), sprites.get('andrea_basket'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (def.type === 'biliardo') {
|
if (def.type === 'biliardo') {
|
||||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
drawBiliardo(ctx, c, def, backgrounds.get(def.picture), sprites.get('ing_tozzi'));
|
drawBiliardo(ctx, c, def, backgrounds.get(def.picture), sprites.get('ing_tozzi'));
|
||||||
|
|||||||
@@ -118,6 +118,23 @@ export class Soundscape {
|
|||||||
else if (kind === 'biliardo-hit') { this.tone(2600, 1500, 0.045, 0.03 + strength * 0.08, 0, pan); this.noiseSound(0.04, 3200, 0.03, { pan }); }
|
else if (kind === 'biliardo-hit') { this.tone(2600, 1500, 0.045, 0.03 + strength * 0.08, 0, pan); this.noiseSound(0.04, 3200, 0.03, { pan }); }
|
||||||
else if (kind === 'biliardo-rail') this.noiseSound(0.12, 520, 0.03 + strength * 0.06, { pan });
|
else if (kind === 'biliardo-rail') this.noiseSound(0.12, 520, 0.03 + strength * 0.06, { pan });
|
||||||
else if (kind === 'biliardo-pin') { this.tone(3200, 2200, 0.03, 0.04 + strength * 0.05, 0, pan); this.noiseSound(0.08, 1800, 0.035, { pan }); }
|
else if (kind === 'biliardo-pin') { this.tone(3200, 2200, 0.03, 0.04 + strength * 0.05, 0, pan); this.noiseSound(0.08, 1800, 0.035, { pan }); }
|
||||||
|
// Il basket: il tiro, la palla che batte per terra, il ferro, il tabellone, la retina.
|
||||||
|
else if (kind === 'basket-throw') this.noiseSound(0.08, 1200, 0.05, { pan });
|
||||||
|
else if (kind === 'basket-bounce') { this.tone(190, 95, 0.11, 0.05 + strength * 0.12, 0, pan); this.noiseSound(0.06, 500, 0.05, { pan }); }
|
||||||
|
else if (kind === 'basket-rim') { this.tone(1500, 1100, 0.14, 0.04 + strength * 0.1, 0, pan); this.tone(2300, 1900, 0.08, 0.03, 0, pan); }
|
||||||
|
else if (kind === 'basket-board') { this.tone(280, 170, 0.09, 0.05 + strength * 0.08, 0, pan); this.noiseSound(0.07, 900, 0.07, { pan }); }
|
||||||
|
else if (kind === 'basket-swish') this.noiseSound(0.28, 3200, 0.1, { paper: true, pan });
|
||||||
|
else if (kind === 'basket-in') { this.tone(1300, 1000, 0.08, 0.04, 0, pan); this.noiseSound(0.22, 2800, 0.08, { paper: true, pan }); }
|
||||||
|
// La pesca: il lancio, il tuffo del galleggiante, il tocco, l'abboccata, la ferrata, la lenza che tira, lo strappo, il pesce a riva.
|
||||||
|
else if (kind === 'pesca-lancio') this.noiseSound(0.22, 1800, 0.05 + strength * 0.06, { pan });
|
||||||
|
else if (kind === 'pesca-tuffo') { this.tone(420, 180, 0.07, 0.07, 0, pan); this.noiseSound(0.12, 900, 0.06, { pan }); }
|
||||||
|
else if (kind === 'pesca-tocco') this.tone(900, 700, 0.04, 0.03, 0, pan);
|
||||||
|
else if (kind === 'pesca-abbocca') { this.tone(520, 260, 0.1, 0.08, 0, pan); this.noiseSound(0.1, 1100, 0.06, { pan }); }
|
||||||
|
else if (kind === 'pesca-ferrata') this.noiseSound(0.1, 2600, 0.08, { pan });
|
||||||
|
else if (kind === 'pesca-mulinello') this.tone(1800 + strength * 900, 1700 + strength * 900, 0.05, 0.015 + strength * 0.02, 0, pan);
|
||||||
|
else if (kind === 'pesca-rottura') { this.tone(2400, 300, 0.12, 0.1, 0, pan); this.noiseSound(0.08, 3000, 0.06, { pan }); }
|
||||||
|
else if (kind === 'pesca-vuoto') this.noiseSound(0.14, 1400, 0.05, { pan });
|
||||||
|
else if (kind === 'pesca-preso') { this.noiseSound(0.25, 1000, 0.1, { pan }); this.tone(660, 990, 0.18, 0.05, 0.1, pan); }
|
||||||
else if (kind === 'wood') this.noiseSound(0.3, 750, 0.13, { pan });
|
else if (kind === 'wood') this.noiseSound(0.3, 750, 0.13, { pan });
|
||||||
else if (kind === 'tavolo-lontano') {
|
else if (kind === 'tavolo-lontano') {
|
||||||
// Su un altro tavolo: il colpo di stecca, poi la biglia che ne prende un'altra, piano, da lontano.
|
// Su un altro tavolo: il colpo di stecca, poi la biglia che ne prende un'altra, piano, da lontano.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Avvio nel browser: collega motore, disegno e interfaccia HTML (verbi, inventario, dialoghi, fumetti).
|
// Avvio nel browser: collega motore, disegno e interfaccia HTML (verbi, inventario, dialoghi, fumetti).
|
||||||
import { story } from './story/story.js';
|
import { story } from './story/story.js';
|
||||||
import { Game, VERBS, VERB_LABELS, checkSave, introText, shuffledCredits, matchPlace } from './engine/game.js';
|
import { Game, VERBS, VERB_LABELS, checkSave, introText, shuffledCredits, matchPlace, reelPictures, facePicture } from './engine/game.js';
|
||||||
import { Command } from './engine/command.js';
|
import { Command } from './engine/command.js';
|
||||||
import { render, loadAssets, hasBackground, WIDTH, HEIGHT } from './engine/render.js';
|
import { render, loadAssets, hasBackground, WIDTH, HEIGHT } from './engine/render.js';
|
||||||
import { readSave, writeSave, clearSave, readAudio, writeAudio, saveFile, saveFileName, readSaveFile } from './engine/storage.js';
|
import { readSave, writeSave, clearSave, readAudio, writeAudio, saveFile, saveFileName, readSaveFile } from './engine/storage.js';
|
||||||
@@ -10,6 +10,8 @@ import { Soundscape } from './engine/soundscape.js';
|
|||||||
import { soundscapeFor } from './story/soundscapes.js';
|
import { soundscapeFor } from './story/soundscapes.js';
|
||||||
import * as bocce from './engine/bocce.js';
|
import * as bocce from './engine/bocce.js';
|
||||||
import * as biliardo from './engine/biliardo.js';
|
import * as biliardo from './engine/biliardo.js';
|
||||||
|
import * as basket from './engine/basket.js';
|
||||||
|
import * as pesca from './engine/pesca.js';
|
||||||
import * as briscola from './engine/briscola.js';
|
import * as briscola from './engine/briscola.js';
|
||||||
import { cardAt } from './engine/briscola-render.js';
|
import { cardAt } from './engine/briscola-render.js';
|
||||||
import * as tressette from './engine/tressette.js';
|
import * as tressette from './engine/tressette.js';
|
||||||
@@ -125,7 +127,7 @@ function openCredits() {
|
|||||||
if (typeof n === 'string') return Object.assign(document.createElement('span'), { textContent: n });
|
if (typeof n === 'string') return Object.assign(document.createElement('span'), { textContent: n });
|
||||||
// Chi ha il ritratto: il nome è un pulsante che lo mostra.
|
// Chi ha il ritratto: il nome è un pulsante che lo mostra.
|
||||||
const b = Object.assign(document.createElement('button'), { type: 'button', className: 'credit-face', textContent: n.name });
|
const b = Object.assign(document.createElement('button'), { type: 'button', className: 'credit-face', textContent: n.name });
|
||||||
b.addEventListener('click', () => showFace(n));
|
b.addEventListener('click', () => showFace(facePicture(n)));
|
||||||
return b;
|
return b;
|
||||||
}));
|
}));
|
||||||
return [dt, dd];
|
return [dt, dd];
|
||||||
@@ -133,22 +135,64 @@ function openCredits() {
|
|||||||
fillReel();
|
fillReel();
|
||||||
$('credits').hidden = false;
|
$('credits').hidden = false;
|
||||||
rollCredits();
|
rollCredits();
|
||||||
|
rollReel();
|
||||||
}
|
}
|
||||||
// Sotto i nomi, un carosello che scorre da solo: gli sfondi del paese e i ritratti, due giri uguali in fila per non
|
// Sotto i nomi, il carosello: tutti gli sfondi del paese e i ritratti (game.reelPictures), due giri uguali in fila per
|
||||||
// vedere mai la cucitura.
|
// non vedere mai la cucitura. Scorre da solo; si ferma sotto il mouse, si trascina, si spinge con la rotella, le
|
||||||
|
// frecce ai lati o quelle della tastiera, e toccando una foto la si vede grande (Andrea, 27 ottobre).
|
||||||
|
const reelTrack = $('credits-reel').querySelector('.track');
|
||||||
|
let reeling = 0, reelWait = 0, reelDrag = null, reelDragged = false, reelTap = null;
|
||||||
|
const reelPics = new WeakMap();
|
||||||
function fillReel() {
|
function fillReel() {
|
||||||
const track = $('credits-reel').querySelector('.track');
|
if (reelTrack.children.length) return;
|
||||||
if (track.children.length) return;
|
const all = reelPictures(story);
|
||||||
const pictures = [...new Set(Object.values(story.scenes).map(s => s.background).filter(Boolean))];
|
for (const p of [...all, ...all]) {
|
||||||
const faces = (story.credits ?? []).flatMap(c => c.names).filter(n => n.face).map(n => n.face);
|
const img = Object.assign(document.createElement('img'), { src: versioned(p.src), alt: p.name, title: p.name, className: p.kind, loading: 'lazy', decoding: 'async', draggable: false });
|
||||||
// I ritratti sparsi fra gli sfondi, uno ogni quattro.
|
reelPics.set(img, p);
|
||||||
const all = pictures.flatMap((p, i) => i % 4 === 3 && faces.length ? [p, faces[(i >> 2) % faces.length]] : [p]);
|
reelTrack.append(img);
|
||||||
for (const src of [...all, ...all]) {
|
|
||||||
const img = Object.assign(document.createElement('img'), { src, alt: '', loading: 'lazy', decoding: 'async' });
|
|
||||||
if (src.includes('/credits/')) img.className = src.endsWith('palio.png') ? 'palio' : 'face';
|
|
||||||
track.append(img);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function rollReel() {
|
||||||
|
cancelAnimationFrame(reeling);
|
||||||
|
reelTrack.scrollLeft = 0;
|
||||||
|
reelWait = 0;
|
||||||
|
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||||
|
let last = performance.now(), pos = 0;
|
||||||
|
const step = now => {
|
||||||
|
if ($('credits').hidden) return;
|
||||||
|
const dt = now - last; last = now;
|
||||||
|
const half = reelTrack.scrollWidth / 2;
|
||||||
|
if (reelTrack.matches(':hover') || reelDrag || !$('face').hidden || (reelWait -= dt) > 0) pos = reelTrack.scrollLeft;
|
||||||
|
else { pos += dt * 0.05; if (pos >= half) pos -= half; reelTrack.scrollLeft = pos; }
|
||||||
|
reeling = requestAnimationFrame(step);
|
||||||
|
};
|
||||||
|
reeling = requestAnimationFrame(step);
|
||||||
|
}
|
||||||
|
// Di una foto alla volta, con le frecce.
|
||||||
|
function nudgeReel(dir) {
|
||||||
|
reelWait = 4000;
|
||||||
|
reelTrack.scrollBy({ left: dir * ((reelTrack.firstElementChild?.offsetWidth ?? 300) + 10), behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
for (const b of $('credits-reel').querySelectorAll('.arrow')) b.addEventListener('click', () => nudgeReel(b.classList.contains('next') ? 1 : -1));
|
||||||
|
// Col dito si trascina da sé (touch-action: pan-x); col mouse lo facciamo noi. Un tocco fermo su una foto la mostra
|
||||||
|
// grande: si guarda al rilascio, perché con la cattura del puntatore il clic non arriva più all'immagine.
|
||||||
|
reelTrack.addEventListener('pointerdown', e => {
|
||||||
|
reelDragged = false;
|
||||||
|
reelTap = reelPics.get(e.target) ?? null;
|
||||||
|
if (e.pointerType !== 'mouse') { reelWait = 4000; return; }
|
||||||
|
reelDrag = { x: e.clientX, left: reelTrack.scrollLeft, id: e.pointerId };
|
||||||
|
reelTrack.setPointerCapture(e.pointerId);
|
||||||
|
});
|
||||||
|
reelTrack.addEventListener('pointermove', e => {
|
||||||
|
if (!reelDrag) return;
|
||||||
|
const dx = e.clientX - reelDrag.x;
|
||||||
|
if (Math.abs(dx) > 6) reelDragged = true;
|
||||||
|
reelTrack.scrollLeft = reelDrag.left - dx;
|
||||||
|
});
|
||||||
|
const endReelDrag = () => { if (reelDrag && reelDragged) reelWait = 4000; reelDrag = null; };
|
||||||
|
reelTrack.addEventListener('pointerup', () => { if (!reelDragged && reelTap) showFace(reelTap); reelTap = null; endReelDrag(); });
|
||||||
|
reelTrack.addEventListener('pointercancel', () => { reelTap = null; endReelDrag(); });
|
||||||
|
reelTrack.addEventListener('wheel', e => { reelWait = 4000; if (e.deltaY && !e.deltaX) { reelTrack.scrollLeft += e.deltaY; e.preventDefault(); } }, { passive: false });
|
||||||
// I nomi salgono piano da soli; si fermano se ci si passa sopra o si tocca, e ricominciano dall'alto in fondo.
|
// I nomi salgono piano da soli; si fermano se ci si passa sopra o si tocca, e ricominciano dall'alto in fondo.
|
||||||
let rolling = 0, rollWait = 0;
|
let rolling = 0, rollWait = 0;
|
||||||
$('credits-list').addEventListener('pointerdown', () => { rollWait = 4000; });
|
$('credits-list').addEventListener('pointerdown', () => { rollWait = 4000; });
|
||||||
@@ -170,9 +214,10 @@ function rollCredits() {
|
|||||||
};
|
};
|
||||||
rolling = requestAnimationFrame(step);
|
rolling = requestAnimationFrame(step);
|
||||||
}
|
}
|
||||||
function showFace({ name, face }) {
|
function showFace({ name, src, kind }) {
|
||||||
$('face').querySelector('img').src = versioned(face);
|
const img = $('face').querySelector('img');
|
||||||
$('face').querySelector('img').classList.toggle('palio', face.endsWith('palio.png'));
|
img.src = versioned(src);
|
||||||
|
img.className = kind;
|
||||||
$('face').querySelector('figcaption').textContent = name;
|
$('face').querySelector('figcaption').textContent = name;
|
||||||
$('face').hidden = false;
|
$('face').hidden = false;
|
||||||
}
|
}
|
||||||
@@ -453,11 +498,11 @@ canvas.addEventListener('pointerdown', e => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const aimed = game.closeupAim(scenePoint(e));
|
const aimed = game.closeupAim(scenePoint(e));
|
||||||
if (story.closeups[game.closeup.id].type === 'biliardo') {
|
if (['biliardo', 'basket'].includes(story.closeups[game.closeup.id].type)) {
|
||||||
if (!aimed && !game.speech) return;
|
if (!aimed && !game.speech) return; // dove non si può mirare (dietro Andrea) non si carica nemmeno
|
||||||
if (aimed) game.closeup.aimLocked = true; // il clic fissa la mira
|
if (aimed) game.closeup.aimLocked = true; // il clic fissa la mira
|
||||||
}
|
}
|
||||||
if (story.closeups[game.closeup.id].type === 'bocce' && aimed) game.closeup.aimLocked = true;
|
if (['bocce', 'basket', 'pesca'].includes(story.closeups[game.closeup.id].type) && aimed) game.closeup.aimLocked = true;
|
||||||
press = { closeup: true };
|
press = { closeup: true };
|
||||||
// Alle bocce si tiene premuto per caricare la forza (la battuta in corso, un tocco la salta).
|
// Alle bocce si tiene premuto per caricare la forza (la battuta in corso, un tocco la salta).
|
||||||
if (!game.skip()) game.closeupPress();
|
if (!game.skip()) game.closeupPress();
|
||||||
@@ -507,7 +552,7 @@ canvas.addEventListener('pointermove', e => {
|
|||||||
// Al biliardo e alle bocce la mira segue il mouse finché un clic (o una regolazione fine) non la fissa: dopo,
|
// Al biliardo e alle bocce la mira segue il mouse finché un clic (o una regolazione fine) non la fissa: dopo,
|
||||||
// attraversare il campo per raggiungere la forza, «Tira» o «Lancia» non cambia il colpo. Al tiro dopo torna libera.
|
// attraversare il campo per raggiungere la forza, «Tira» o «Lancia» non cambia il colpo. Al tiro dopo torna libera.
|
||||||
// (Bocce: scoperto girando il video «Come si gioca», la mira finiva a due passi andando verso il cursore.)
|
// (Bocce: scoperto girando il video «Come si gioca», la mira finiva a due passi andando verso il cursore.)
|
||||||
const aiming = game.closeup && ['biliardo', 'bocce'].includes(story.closeups[game.closeup.id].type);
|
const aiming = game.closeup && ['biliardo', 'bocce', 'basket', 'pesca'].includes(story.closeups[game.closeup.id].type);
|
||||||
if (aiming) { if (e.pointerType === 'mouse' && !press && !game.closeup.aimLocked) game.closeupAim(scenePoint(e)); }
|
if (aiming) { if (e.pointerType === 'mouse' && !press && !game.closeup.aimLocked) game.closeupAim(scenePoint(e)); }
|
||||||
else if (e.pointerType === 'mouse' || press?.closeup) game.closeupAim(scenePoint(e));
|
else if (e.pointerType === 'mouse' || press?.closeup) game.closeupAim(scenePoint(e));
|
||||||
});
|
});
|
||||||
@@ -520,6 +565,8 @@ const MINI = {
|
|||||||
briscola: { ...briscola, back: 'Torna all’oratorio', label: 'Briscola: tocca una carta per giocarla. Frecce o 1, 2, 3 scelgono; Invio gioca. Esc esce.' },
|
briscola: { ...briscola, back: 'Torna all’oratorio', label: 'Briscola: tocca una carta per giocarla. Frecce o 1, 2, 3 scelgono; Invio gioca. Esc esce.' },
|
||||||
bocce: { ...bocce, nudge: (c, dir) => bocce.aim(c, c.aim.u, c.aim.w + dir * 0.3), back: 'Torna al pallaio', label: 'Bocce: tocca il campo o il dettaglio per mirare. Regola la forza e premi Lancia.' },
|
bocce: { ...bocce, nudge: (c, dir) => bocce.aim(c, c.aim.u, c.aim.w + dir * 0.3), back: 'Torna al pallaio', label: 'Bocce: tocca il campo o il dettaglio per mirare. Regola la forza e premi Lancia.' },
|
||||||
biliardo: { ...biliardo, back: 'Torna al bar', label: 'Biliardo: clic o tocco fissa la mira. Frecce per affinarla, Maiusc per passi più piccoli. Regola la forza e premi Tira.' },
|
biliardo: { ...biliardo, back: 'Torna al bar', label: 'Biliardo: clic o tocco fissa la mira. Frecce per affinarla, Maiusc per passi più piccoli. Regola la forza e premi Tira.' },
|
||||||
|
basket: { ...basket, back: 'Torna in strada', label: 'Fai due tiri: muovi il mouse o tocca per mirare, tieni premuto per caricare la forza, lascia per tirare.' },
|
||||||
|
pesca: { ...pesca, back: 'Torna in Fortezza', label: 'La pesca: tieni premuto per lanciare, premi quando il galleggiante va sotto, tieni premuto per recuperare.' },
|
||||||
};
|
};
|
||||||
const miniState = () => running && game?.closeup && MINI[story.closeups[game.closeup.id].type] ? game.closeup : null;
|
const miniState = () => running && game?.closeup && MINI[story.closeups[game.closeup.id].type] ? game.closeup : null;
|
||||||
const mini = () => MINI[story.closeups[game.closeup.id].type];
|
const mini = () => MINI[story.closeups[game.closeup.id].type];
|
||||||
@@ -631,7 +678,11 @@ window.addEventListener('keydown', e => {
|
|||||||
}
|
}
|
||||||
if (debugKey(e)) return;
|
if (debugKey(e)) return;
|
||||||
if (!$('tutorial').hidden) { if (e.key === 'Escape') closeTutorial(); return; }
|
if (!$('tutorial').hidden) { if (e.key === 'Escape') closeTutorial(); return; }
|
||||||
if (!$('credits').hidden) { if (e.key === 'Escape') { if (!$('face').hidden) $('face').hidden = true; else $('credits').hidden = true; } return; }
|
if (!$('credits').hidden) {
|
||||||
|
if (e.key === 'Escape') { if (!$('face').hidden) $('face').hidden = true; else $('credits').hidden = true; }
|
||||||
|
else if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { if ($('face').hidden) nudgeReel(e.key === 'ArrowRight' ? 1 : -1); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!$('pause').hidden) { if (e.key === 'Escape') closePause(); return; }
|
if (!$('pause').hidden) { if (e.key === 'Escape') closePause(); return; }
|
||||||
if (!$('notebook').hidden) { if (['Escape', 'n', 'N'].includes(e.key)) $('notebook').hidden = true; return; }
|
if (!$('notebook').hidden) { if (['Escape', 'n', 'N'].includes(e.key)) $('notebook').hidden = true; return; }
|
||||||
if (!$('map').hidden) { if (e.key === 'Escape' || e.key === 'p' || e.key === 'P') $('map').hidden = true; return; }
|
if (!$('map').hidden) { if (e.key === 'Escape' || e.key === 'p' || e.key === 'P') $('map').hidden = true; return; }
|
||||||
@@ -857,17 +908,20 @@ function updateUi() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mg && !cards) {
|
if (mg && !cards) {
|
||||||
$('bocce-controls').setAttribute('aria-label', def.type === 'biliardo' ? 'Comandi del biliardo' : 'Comandi delle bocce');
|
$('bocce-controls').setAttribute('aria-label', { biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca' }[def.type] ?? 'Comandi delle bocce');
|
||||||
$('panel').hidden = $('choices').hidden = true;
|
$('panel').hidden = $('choices').hidden = true;
|
||||||
const line = mg.status(c, def.names);
|
const line = mg.status(c, def.names);
|
||||||
if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line;
|
if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line;
|
||||||
$('bocce-power').value = Math.round(c.power * 100);
|
$('bocce-power').value = Math.round(c.power * 100);
|
||||||
$('bocce-power-value').value = `${Math.round(c.power * 100)}%`;
|
$('bocce-power-value').value = `${Math.round(c.power * 100)}%`;
|
||||||
for (const id of ['bocce-power', 'bocce-left', 'bocce-right', 'bocce-throw']) $(id).disabled = !mg.canThrow(c) || c.holding || !!game.speech;
|
for (const id of ['bocce-power', 'bocce-left', 'bocce-right']) $(id).disabled = !(mg.canAdjust?.(c) ?? mg.canThrow(c)) || c.holding || !!game.speech;
|
||||||
$('bocce-throw').textContent = c.step === 'result' ? 'Finita' : c.step === 'rolling' ? 'In movimento…' : c.current === 'cpu' ? `Tira ${def.names.cpu}…` : def.type === 'bocce' ? 'Lancia' : 'Tira';
|
$('bocce-throw').disabled = !mg.canThrow(c) || c.holding || !!game.speech;
|
||||||
|
$('bocce-throw').textContent = mg.button?.(c) ?? (c.step === 'result' ? 'Finita' : c.step === 'rolling' ? 'In movimento…' : c.current === 'cpu' ? `Tira ${def.names.cpu}…` : def.type === 'bocce' ? 'Lancia' : 'Tira');
|
||||||
$('bocce-help').hidden = def.type !== 'bocce';
|
$('bocce-help').hidden = def.type !== 'bocce';
|
||||||
$('biliardo-help').hidden = def.type !== 'biliardo';
|
$('biliardo-help').hidden = def.type !== 'biliardo';
|
||||||
if (!mg.canThrow(c)) c.aimLocked = false; // a ogni nuovo tiro la mira torna libera
|
$('basket-help').hidden = def.type !== 'basket';
|
||||||
|
$('pesca-help').hidden = def.type !== 'pesca';
|
||||||
|
if (mg.aimFree?.(c) ?? !mg.canThrow(c)) c.aimLocked = false; // a ogni nuovo tiro la mira torna libera
|
||||||
}
|
}
|
||||||
if (mg) for (const event of c.events.splice(0)) ambience.play(event.kind, { pan: 0, strength: event.power });
|
if (mg) for (const event of c.events.splice(0)) ambience.play(event.kind, { pan: 0, strength: event.power });
|
||||||
// Il pulsante della mappa c'è solo in partita, fuori dai dialoghi, e quando c'è almeno un altro posto dove andare.
|
// Il pulsante della mappa c'è solo in partita, fuori dai dialoghi, e quando c'è almeno un altro posto dove andare.
|
||||||
|
|||||||
@@ -68,4 +68,78 @@ export const ambience = {
|
|||||||
viale_cipressi: [{ type: 'cloudshadow', phase: day, area: [95, 225, 435, 135], offset: 11000,
|
viale_cipressi: [{ type: 'cloudshadow', phase: day, area: [95, 225, 435, 135], offset: 11000,
|
||||||
clip: [[300, 225], [337, 225], [530, 360], [95, 360]] }],
|
clip: [[300, 225], [337, 225], [530, 360], [95, 360]] }],
|
||||||
trebbio: [shade([278, 277, 112, 23], 6)],
|
trebbio: [shade([278, 277, 112, 23], 6)],
|
||||||
|
// Le scene del 27 ottobre (Andrea: «applicale tutte»).
|
||||||
|
via_pignone: [
|
||||||
|
{ type: 'clouds', area: [100, 0, 440, 110], count: 3, big: true },
|
||||||
|
{ type: 'birds', phase: day, area: [200, 20, 300, 80], count: 3, every: 23000, duration: 5000, offset: 6000 },
|
||||||
|
shade([330, 282, 180, 50], 10),
|
||||||
|
{ type: 'lamps', phase: 'sera', points: [[250, 74]], glow: 10 },
|
||||||
|
],
|
||||||
|
piazzetta: [
|
||||||
|
shade([30, 290, 300, 60], 14),
|
||||||
|
butterfly([150, 170, 150, 60], 5000),
|
||||||
|
{ type: 'lamps', phase: 'sera', points: [[616, 118]], glow: 12 },
|
||||||
|
],
|
||||||
|
// Via dell'Antica Posta e dintorni (28 ottobre).
|
||||||
|
via_antica_posta: [
|
||||||
|
{ type: 'clouds', area: [180, 0, 300, 90], count: 2 },
|
||||||
|
shade([0, 300, 640, 60], 10),
|
||||||
|
{ type: 'lamps', phase: 'sera', points: [[62, 120], [366, 142]], glow: 10 },
|
||||||
|
],
|
||||||
|
piazzetta_comune: [
|
||||||
|
{ type: 'clouds', area: [60, 0, 520, 110], count: 3, big: true },
|
||||||
|
butterfly([510, 250, 110, 50], 4000),
|
||||||
|
{ type: 'lamps', phase: 'sera', points: [[436, 170]], glow: 10 },
|
||||||
|
],
|
||||||
|
antica_posta_alta: [
|
||||||
|
shade([0, 290, 360, 70], 12),
|
||||||
|
butterfly([480, 170, 120, 60], 6000),
|
||||||
|
{ type: 'lamps', phase: 'sera', points: [[184, 74]], glow: 10 },
|
||||||
|
],
|
||||||
|
provinciale_pignone: [
|
||||||
|
shade([80, 270, 220, 70], 14), shade([470, 290, 170, 60], 10),
|
||||||
|
{ type: 'cloudshadow', phase: day, area: [120, 230, 420, 130], offset: 8000,
|
||||||
|
clip: [[290, 232], [370, 232], [640, 330], [640, 360], [0, 360], [0, 330]] },
|
||||||
|
bird([410, 258, 70, 16], 9000),
|
||||||
|
],
|
||||||
|
gabbiano: [
|
||||||
|
{ type: 'clouds', area: [140, 0, 500, 140], count: 3, big: true },
|
||||||
|
{ type: 'birds', phase: day, area: [200, 40, 400, 100], count: 4, every: 21000, duration: 5500, offset: 3000 },
|
||||||
|
{ type: 'cloudshadow', phase: day, area: [140, 160, 500, 200], offset: 7000, clip: rect([140, 160, 500, 200]) },
|
||||||
|
{ type: 'sway', rects: [[430, 128, 70, 60]], amount: 2 },
|
||||||
|
butterfly([120, 250, 200, 60], 3000),
|
||||||
|
],
|
||||||
|
ponticino_mozzete: [
|
||||||
|
shade([0, 230, 640, 110], 22),
|
||||||
|
{ type: 'insect', kind: 'dragonfly', phase: day, area: [80, 230, 140, 50], every: 37000, duration: 8500, offset: 7000 },
|
||||||
|
],
|
||||||
|
via_cipriani: [
|
||||||
|
{ type: 'clouds', area: [0, 0, 420, 150], count: 3, big: true },
|
||||||
|
shade([300, 300, 160, 50], 10),
|
||||||
|
// Di sera le lampade del cinema e il cartellone del Rattattù illuminato.
|
||||||
|
{ type: 'lamps', phase: 'sera', points: [[545, 214], [620, 168]], glow: 12 },
|
||||||
|
{ type: 'lamps', phase: 'sera', points: [[490, 200]], glow: 34 },
|
||||||
|
],
|
||||||
|
via_massorondinaio: [
|
||||||
|
{ type: 'clouds', area: [0, 0, 640, 100], count: 3, big: true },
|
||||||
|
{ type: 'birds', phase: day, area: [100, 30, 400, 100], count: 3, every: 25000, duration: 5000, offset: 11000 },
|
||||||
|
{ type: 'sway', rects: [[200, 128, 90, 72]], amount: 2 },
|
||||||
|
],
|
||||||
|
coldaia: [
|
||||||
|
{ type: 'clouds', area: [200, 0, 440, 120], count: 2 },
|
||||||
|
{ type: 'cloudshadow', phase: day, area: [0, 150, 640, 210], offset: 15000, clip: rect([0, 160, 640, 200]) },
|
||||||
|
{ type: 'birds', phase: day, area: [250, 20, 350, 90], count: 3, every: 27000, duration: 5500, offset: 2000 },
|
||||||
|
butterfly([60, 280, 200, 50], 12000),
|
||||||
|
],
|
||||||
|
falegnameria_coldaia: [
|
||||||
|
{ type: 'clouds', area: [0, 0, 640, 110], count: 3, big: true },
|
||||||
|
{ type: 'dust', phase: day, area: [330, 150, 260, 70], count: 10 },
|
||||||
|
bird([250, 230, 90, 18], 8000, 'blackbird'),
|
||||||
|
],
|
||||||
|
mozzete: [
|
||||||
|
{ type: 'clouds', area: [100, 0, 540, 110], count: 3, big: true },
|
||||||
|
shade([0, 215, 230, 60], 12),
|
||||||
|
{ type: 'cloudshadow', phase: day, area: [60, 220, 520, 140], offset: 23000,
|
||||||
|
clip: [[0, 215], [400, 195], [640, 260], [640, 360], [0, 360]] },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -150,6 +150,40 @@ export const story = {
|
|||||||
quit: [{ say: 'Già stanco? Il pallaio ti aspetta.', who: 'mariano', pose: 'laugh' }],
|
quit: [{ say: 'Già stanco? Il pallaio ti aspetta.', who: 'mariano', pose: 'laugh' }],
|
||||||
draw: [{ say: 'Quando vuoi la rivincita, sono qui.', who: 'mariano' }],
|
draw: [{ say: 'Quando vuoi la rivincita, sono qui.', who: 'mariano' }],
|
||||||
},
|
},
|
||||||
|
// «Fai due tiri» al campo da basket del Circolo 4 Maggio (idea di Andrea, 27 ottobre): da solo, dieci palloni,
|
||||||
|
// si arretra a ogni canestro. La gloria dipende dal livello (basket.LEVELS). [DA ANDREA: le sue battute]
|
||||||
|
basket: {
|
||||||
|
type: 'basket',
|
||||||
|
picture: 'assets/closeups/basket-campo.png',
|
||||||
|
names: { player: 'Andrea', cpu: 'il canestro' },
|
||||||
|
win: [
|
||||||
|
{ say: 'Che mano! Jordan, fatti da parte.' },
|
||||||
|
{ set: 'basket_vinto' },
|
||||||
|
],
|
||||||
|
lose: [
|
||||||
|
{ say: 'Il ferro oggi era stretto. Riprovo dopo.' },
|
||||||
|
],
|
||||||
|
quit: [{ say: 'Lascio il pallone lì. Tanto lo sanno tutti che è mio.' }],
|
||||||
|
draw: [],
|
||||||
|
},
|
||||||
|
// La pesca al lago dell'Anatre coi ragazzi delle canne (idea di Andrea, 28 ottobre): sei lanci, da solo con la
|
||||||
|
// canna di bambù; la gloria dipende dal livello (pesca.LEVELS). [DA ANDREA: le sue battute]
|
||||||
|
pesca: {
|
||||||
|
type: 'pesca',
|
||||||
|
picture: 'assets/closeups/lago-anatre.png',
|
||||||
|
names: { player: 'Andrea', cpu: 'il lago' },
|
||||||
|
win: [
|
||||||
|
{ say: 'Oh, hai visto? Mica male, per una canna di bambù.', who: 'pescatori', pose: 'laugh' },
|
||||||
|
{ say: 'Stasera si frigge. Ma prima il palio.' },
|
||||||
|
{ set: 'pesca_vinta' },
|
||||||
|
],
|
||||||
|
lose: [
|
||||||
|
{ say: 'I pesci c\'erano. Eri te che non c\'eri.', who: 'pescatori', pose: 'talk' },
|
||||||
|
{ say: 'Si rifà. Dopo il palio.' },
|
||||||
|
],
|
||||||
|
quit: [{ say: 'Lascio la canna ai ragazzi. Il lago non scappa, il palio sì.' }],
|
||||||
|
draw: [],
|
||||||
|
},
|
||||||
// Il biliardo del Circolo 4 Maggio contro l'Ing. Tozzi: cinque birilli, un colpo a testa, si va a 20. [DA ANDREA: le sue battute]
|
// Il biliardo del Circolo 4 Maggio contro l'Ing. Tozzi: cinque birilli, un colpo a testa, si va a 20. [DA ANDREA: le sue battute]
|
||||||
biliardo: {
|
biliardo: {
|
||||||
type: 'biliardo',
|
type: 'biliardo',
|
||||||
@@ -215,6 +249,9 @@ export const story = {
|
|||||||
// [DA ANDREA: chi è] Figura inventata, nessuna persona reale.
|
// [DA ANDREA: chi è] Figura inventata, nessuna persona reale.
|
||||||
giocatore_carte: { name: 'Il giocatore di carte', color: '#edc476', sprite: 'assets/characters/giocatore-carte.json' },
|
giocatore_carte: { name: 'Il giocatore di carte', color: '#edc476', sprite: 'assets/characters/giocatore-carte.json' },
|
||||||
andrea: { name: 'Andrea', color: '#ffffff', sprite: 'assets/characters/andrea.json' },
|
andrea: { name: 'Andrea', color: '#ffffff', sprite: 'assets/characters/andrea.json' },
|
||||||
|
// Andrea col pallone, per «Fai due tiri» (foglio di Codex dal suo sprite): non parla, lo disegna basket-render.
|
||||||
|
andrea_basket: { name: 'Andrea', sprite: 'assets/characters/andrea-basket.json' },
|
||||||
|
andrea_pesca: { name: 'Andrea', sprite: 'assets/characters/andrea-pesca.json' },
|
||||||
// Quattro ragazzi inventati (sprite di Codex, idea di Andrea del 27 ottobre): nessuna persona reale.
|
// Quattro ragazzi inventati (sprite di Codex, idea di Andrea del 27 ottobre): nessuna persona reale.
|
||||||
pescatori: { name: 'I ragazzi delle canne', color: '#b8f08a', sprite: 'assets/characters/pescatori.json' },
|
pescatori: { name: 'I ragazzi delle canne', color: '#b8f08a', sprite: 'assets/characters/pescatori.json' },
|
||||||
vecchietta: { name: 'Vecchietta', color: '#ffd27a', sprite: 'assets/characters/vecchietta.json' },
|
vecchietta: { name: 'Vecchietta', color: '#ffd27a', sprite: 'assets/characters/vecchietta.json' },
|
||||||
@@ -478,11 +515,12 @@ export const story = {
|
|||||||
take: [{ give: 'squama' }, { set: 'squama_presa' }, { say: 'Una squama! E bella grossa.' }],
|
take: [{ give: 'squama' }, { set: 'squama_presa' }, { say: 'Una squama! E bella grossa.' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Si compare in basso: la foto di Via Cipriani è presa dall'altro verso (Andrea, 28 ottobre, va bene così).
|
||||||
{
|
{
|
||||||
id: 'verso_campeggio', name: 'Via Cipriani, verso il cinema e il campeggio', box: [330, 190, 34, 48], at: [345, 234], exit: { scene: 'via_cipriani', at: [300, 345], face: 'up' },
|
id: 'verso_campeggio', name: 'Via Cipriani, verso il cinema e il campeggio', box: [330, 190, 34, 48], at: [345, 234], exit: { scene: 'via_cipriani', at: [300, 345], face: 'up' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'verso_piazzetta', name: 'Piazzetta Cambray Digny, verso Via de\' Medici', box: [0, 196, 146, 54], at: [92, 250], exit: { scene: 'piazzetta', at: [510, 274], face: 'down' },
|
id: 'verso_piazzetta', name: 'Via dell\'Antica Posta, verso Piazzetta Cambray Digny', box: [0, 196, 146, 54], at: [92, 250], exit: { scene: 'via_antica_posta', at: [320, 345], face: 'up' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'verso_calimara', name: 'Via Calimara, verso la farmacia', box: [180, 334, 380, 26], at: [360, 354], exit: { scene: 'via_calimara', at: [365, 212], face: 'down' },
|
id: 'verso_calimara', name: 'Via Calimara, verso la farmacia', box: [180, 334, 380, 26], at: [360, 354], exit: { scene: 'via_calimara', at: [365, 212], face: 'down' },
|
||||||
@@ -511,12 +549,13 @@ export const story = {
|
|||||||
id: 'pescatori', name: 'ragazzi con le canne', box: [92, 236, 120, 86], at: [240, 334], face: 'left', default: 'talk',
|
id: 'pescatori', name: 'ragazzi con le canne', box: [92, 236, 120, 86], at: [240, 334], face: 'left', default: 'talk',
|
||||||
actions: {
|
actions: {
|
||||||
look: [{ say: 'Quattro ragazzi che affilano canne di bambù col coltellino e ci legano le lenze.' }],
|
look: [{ say: 'Quattro ragazzi che affilano canne di bambù col coltellino e ci legano le lenze.' }],
|
||||||
talk: [
|
talk: [{ if: '!pescatori_parlato', then: [
|
||||||
|
{ set: 'pescatori_parlato' },
|
||||||
{ say: 'Oh, che fate?' },
|
{ say: 'Oh, che fate?' },
|
||||||
{ say: 'Si preparano le canne: domattina si va a pescare al lago dell\'Anatre, dietro Cardetole!', who: 'pescatori', pose: 'talk' },
|
{ say: 'Si preparano le canne: si va a pescare al lago dell\'Anatre, dietro Cardetole!', who: 'pescatori', pose: 'talk' },
|
||||||
{ say: 'Guarda questa: la lenza la regge anche una carpa di dieci chili.', who: 'pescatori', pose: 'proud' },
|
{ say: 'Guarda questa: la lenza la regge anche una carpa di dieci chili.', who: 'pescatori', pose: 'proud' },
|
||||||
{ say: '(Vedi, mia mamma aveva ragione: in Fortezza si viene a farsi le canne.)' },
|
{ say: '(Vedi, mia mamma aveva ragione: in Fortezza si viene a farsi le canne.)' },
|
||||||
],
|
] }, { dialogue: 'pescatori' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -897,9 +936,11 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'campo_basket', name: 'campo da basket del Circolo', box: [250, 110, 170, 80], at: [340, 316], face: 'up',
|
id: 'campo_basket', name: 'campo da basket del Circolo', box: [250, 110, 170, 80], at: [340, 316], face: 'up', default: 'use',
|
||||||
actions: {
|
actions: {
|
||||||
look: [{ say: 'Il campo da basket del Circolo, lassù oltre la rete. Prima o poi due tiri ce li faccio.' }],
|
look: [{ say: 'Il campo da basket del Circolo, lassù oltre la rete. Due tiri ce li faccio sempre volentieri.' }],
|
||||||
|
use: [{ dialogue: 'basket' }],
|
||||||
|
walk: [{ dialogue: 'basket' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1162,6 +1203,9 @@ export const story = {
|
|||||||
open: [{ say: 'All\'asilo? Ho già dato, grazie.' }],
|
open: [{ say: 'All\'asilo? Ho già dato, grazie.' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'alla_scalinata', name: 'a sinistra, la scalinata della Piazzetta', box: [0, 120, 150, 212], at: [40, 300], face: 'left', exit: { scene: 'scalinata', at: [300, 350], face: 'up' },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'al_giardino', name: 'verso il giardino di Via Calimara', box: [596, 290, 44, 70], at: [600, 340], exit: { scene: 'via_calimara', at: [160, 290], face: 'right' },
|
id: 'al_giardino', name: 'verso il giardino di Via Calimara', box: [596, 290, 44, 70], at: [600, 340], exit: { scene: 'via_calimara', at: [160, 290], face: 'right' },
|
||||||
},
|
},
|
||||||
@@ -1259,13 +1303,13 @@ export const story = {
|
|||||||
id: 'al_ponte', name: 'a sinistra, verso il campo sportivo', box: [0, 216, 60, 100], at: [12, 318], exit: { scene: 'ponte_sieve', at: [300, 350], face: 'up' },
|
id: 'al_ponte', name: 'a sinistra, verso il campo sportivo', box: [0, 216, 60, 100], at: [12, 318], exit: { scene: 'ponte_sieve', at: [300, 350], face: 'up' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'alla_pieve', name: 'dietro la farmacia, verso la Pieve', box: [470, 180, 120, 60], at: [560, 240], exit: { scene: 'provinciale_pignone', at: [300, 345], face: 'up' },
|
id: 'alla_pieve', name: 'dietro la farmacia, verso la Pieve', box: [420, 170, 118, 70], at: [480, 242], exit: { scene: 'provinciale_pignone', at: [300, 345], face: 'up' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'area_feste', name: 'qui sotto, verso l\'area feste', box: [0, 318, 300, 42], at: [120, 352], exit: { scene: 'area_feste', at: [44, 350], face: 'up' },
|
id: 'area_feste', name: 'qui sotto, verso l\'area feste', box: [0, 318, 300, 42], at: [120, 352], exit: { scene: 'area_feste', at: [44, 350], face: 'up' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'al_comune', name: 'verso il Comune', box: [592, 140, 48, 96], at: [622, 244], exit: { scene: 'comune', at: [320, 350], face: 'up' },
|
id: 'al_comune', name: 'verso il Comune', box: [540, 40, 100, 198], at: [622, 244], exit: { scene: 'comune', at: [320, 350], face: 'up' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'alla_calimara', name: 'Via Calimara, verso la Piazza', box: [520, 260, 120, 100], at: [600, 330], exit: { scene: 'via_calimara', at: [300, 350], face: 'up' },
|
id: 'alla_calimara', name: 'Via Calimara, verso la Piazza', box: [520, 260, 120, 100], at: [600, 330], exit: { scene: 'via_calimara', at: [300, 350], face: 'up' },
|
||||||
@@ -1287,6 +1331,31 @@ export const story = {
|
|||||||
look: [{ say: 'Cartelli per la Pieve, per la stazione, per Firenze. Per il palio, nessuno.' }],
|
look: [{ say: 'Cartelli per la Pieve, per la stazione, per Firenze. Per il palio, nessuno.' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// L'edicola, col chiosco chiuso: qui Andrea comprava le cassette per il Commodore 64 (Andrea, 28 ottobre).
|
||||||
|
id: 'edicola', name: 'edicola', box: [20, 160, 92, 100], at: [120, 278], face: 'left',
|
||||||
|
actions: {
|
||||||
|
look: [
|
||||||
|
{ say: 'L\'edicola. Qui compravo le cassette per il Commodore 64: trenta giochi a mille lire, diceva la copertina.' },
|
||||||
|
{ say: 'Si infilava la cassetta, si scriveva LOAD, si premeva play... e si aspettava dieci minuti col registratore che cigolava.' },
|
||||||
|
{ say: 'Poi, quando andava bene, partiva il gioco. Quando andava male: LOAD ERROR. E si riavvolgeva tutto. Bei tempi.' },
|
||||||
|
],
|
||||||
|
open: [{ say: 'Chiusa. E comunque le cassette non le fanno più. Neanche i Commodore.' }],
|
||||||
|
use: [{ say: 'Vorrei un Topolino e trenta giochi a mille lire. ...Niente, parlo con una serranda.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// La banca all'angolo di Via del Pignone: il bancomat non funziona quasi mai (Andrea, 28 ottobre).
|
||||||
|
id: 'banca', name: 'banca', box: [368, 166, 80, 50], at: [400, 236], face: 'up',
|
||||||
|
actions: {
|
||||||
|
look: [
|
||||||
|
{ say: 'La banca. Il bancomat funziona solo il mercoledì delle settimane pari, negli anni bisestili, quando Venere si allinea a Plutone.' },
|
||||||
|
{ say: 'Oggi? Not today.' },
|
||||||
|
],
|
||||||
|
open: [{ say: 'Chiusa. Come il bancomat, del resto.' }],
|
||||||
|
use: [{ say: 'Carta dentro, carta fuori. «Servizio momentaneamente non disponibile». Momentaneamente dal 1998.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'alla_pieve', name: 'in fondo, verso la Pieve', box: [236, 150, 64, 64], at: [268, 216], exit: { scene: 'pieve', at: [24, 312], face: 'right' },
|
id: 'alla_pieve', name: 'in fondo, verso la Pieve', box: [236, 150, 64, 64], at: [268, 216], exit: { scene: 'pieve', at: [24, 312], face: 'right' },
|
||||||
},
|
},
|
||||||
@@ -1294,7 +1363,7 @@ export const story = {
|
|||||||
id: 'via_pignone', name: 'a destra, Via del Pignone', box: [440, 196, 100, 56], at: [530, 245], face: 'right', exit: { scene: 'via_pignone', at: [300, 345], face: 'up' },
|
id: 'via_pignone', name: 'a destra, Via del Pignone', box: [440, 196, 100, 56], at: [530, 245], face: 'right', exit: { scene: 'via_pignone', at: [300, 345], face: 'up' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'alla_rotonda', name: 'indietro, verso la rotonda della farmacia', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'via_provinciale', at: [560, 240], face: 'left' },
|
id: 'alla_rotonda', name: 'indietro, verso la rotonda della farmacia', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'via_provinciale', at: [480, 246], face: 'down' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -1323,7 +1392,8 @@ export const story = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Piazzetta Cambray Digny, in cima a Via del Pignone, sotto Villa Schifanoia: a sinistra Via de' Medici, a diritto la Piazza.
|
// Piazzetta Cambray Digny, in cima a Via del Pignone, sotto Villa Schifanoia: a sinistra Via de' Medici, a diritto
|
||||||
|
// Via dell'Antica Posta che scende in Piazza, a destra del palazzo la scalinata che scende al Comune (Andrea, 28 ottobre).
|
||||||
piazzetta: {
|
piazzetta: {
|
||||||
name: 'Piazzetta Cambray Digny',
|
name: 'Piazzetta Cambray Digny',
|
||||||
map: [40, 38],
|
map: [40, 38],
|
||||||
@@ -1346,16 +1416,188 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'alla_piazza', name: 'a diritto, verso la Piazza', box: [470, 226, 70, 48], at: [510, 274], exit: { scene: 'piazza', at: [92, 250], face: 'right' },
|
id: 'alla_piazza', name: 'a diritto, Via dell\'Antica Posta verso la Piazza', box: [470, 226, 70, 48], at: [510, 274], exit: { scene: 'antica_posta_alta', at: [235, 230], face: 'down' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'via_de_medici', name: 'a sinistra, Via de\' Medici', box: [0, 280, 40, 80], at: [20, 320], face: 'left', exit: { scene: 'via_de_medici', at: [300, 348], face: 'up' },
|
id: 'via_de_medici', name: 'a sinistra, Via de\' Medici', box: [0, 100, 60, 260], at: [20, 320], face: 'left', exit: { scene: 'via_de_medici', at: [300, 348], face: 'up' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'giu_scalinata', name: 'a destra, giù per la scalinata verso il Comune', box: [541, 226, 40, 60], at: [560, 285], exit: { scene: 'scalinata', at: [560, 325], face: 'down' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'giu_pignone', name: 'giù per Via del Pignone', box: [570, 300, 70, 60], at: [610, 345], exit: { scene: 'via_pignone', at: [343, 212], face: 'down' },
|
id: 'giu_pignone', name: 'giù per Via del Pignone', box: [570, 300, 70, 60], at: [610, 345], exit: { scene: 'via_pignone', at: [343, 212], face: 'down' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// Via dell'Antica Posta (foto di Andrea, 28 ottobre): dalla Piazza, a sinistra, sale verso Piazzetta Cambray Digny.
|
||||||
|
// La Piazza è alle spalle; a sinistra, coi tre scalini, la Piazzetta del Comune; in fondo si sale.
|
||||||
|
via_antica_posta: {
|
||||||
|
name: 'Via dell\'Antica Posta',
|
||||||
|
map: [35, 41],
|
||||||
|
background: 'assets/scenes/via-antica-posta.png',
|
||||||
|
walk: [[0, 360], [0, 300], [120, 290], [220, 252], [300, 240], [372, 226], [396, 226], [470, 252], [630, 300], [640, 360]],
|
||||||
|
depth: [[226, 0.35], [360, 1.15]],
|
||||||
|
entry: [320, 345],
|
||||||
|
hotspots: [
|
||||||
|
{
|
||||||
|
id: 'colonnine', name: 'colonnine con le catene', box: [225, 225, 95, 68], at: [290, 300], face: 'left',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Le colonnine con le catene: le macchine di qua non passano.' }, { say: 'Io sì. Scavalcare le catene è uno sport che pratico da quando avevo sei anni.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tenda', name: 'tenda a righe', box: [508, 182, 30, 28], at: [500, 268], face: 'right',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Una tenda a righe bianche e rosse. Fa tanto spiaggia: peccato che il mare sia di là dall\'Appennino.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'muro_edera', name: 'muro con l\'edera', box: [546, 140, 94, 150], at: [570, 296], face: 'right',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Il muro di sasso con l\'edera e la ringhiera in cima. Lassù c\'è il verde della villa.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alla_piazzetta_comune', name: 'a sinistra, il Comune', box: [0, 90, 200, 204], at: [130, 292], face: 'left', exit: { scene: 'piazzetta_comune', at: [260, 350], face: 'up' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'su_antica_posta', name: 'su, in fondo a Via dell\'Antica Posta', box: [350, 200, 70, 30], at: [384, 230], exit: { scene: 'antica_posta_alta', at: [240, 350], face: 'up' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alla_piazza', name: 'indietro, la Piazza', box: [0, 334, 640, 26], at: [320, 354], exit: { scene: 'piazza', at: [92, 250], face: 'right' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
enter: [{ if: '!antica_posta_vista', then: [{ set: 'antica_posta_vista' }, { say: 'Via dell\'Antica Posta. Chissà se la posta antica arrivava prima di quella moderna.' }] }],
|
||||||
|
},
|
||||||
|
// La Piazzetta del Comune, dietro il municipio: la torre dell'orologio, le sculture sui pilastrini di mattoni.
|
||||||
|
piazzetta_comune: {
|
||||||
|
name: 'Piazzetta del Comune',
|
||||||
|
map: [32, 39],
|
||||||
|
background: 'assets/scenes/piazzetta-comune.png',
|
||||||
|
walk: [[0, 360], [0, 318], [130, 292], [470, 288], [540, 300], [520, 360]],
|
||||||
|
depth: [[288, 0.55], [360, 1.15]],
|
||||||
|
entry: [260, 350],
|
||||||
|
hotspots: [
|
||||||
|
{
|
||||||
|
id: 'torre', name: 'torre dell\'orologio', box: [264, 16, 58, 104], at: [290, 294], face: 'up',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'La torre dell\'orologio, vista dal retro. Anche da qui segna un\'ora tutta sua.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'porta_comune', name: 'porta del Comune', box: [286, 238, 26, 50], at: [298, 292], face: 'up',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'La porta del Comune. Dal retro, ma sempre Comune.' }],
|
||||||
|
open: [{ say: 'Chiusa. Anche dalla porta di dietro, senza un modulo non si entra.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'scultura', name: 'scultura alta', box: [264, 248, 22, 68], at: [250, 320], face: 'right',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Una scultura moderna, lunga e appuntita. Secondo me è un\'antenna per parlare coi marziani.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sasso', name: 'scultura a sasso', box: [92, 276, 42, 54], at: [150, 330], face: 'left',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Questa sembra un sasso. Ma sta su un piedistallo, quindi è arte.' }],
|
||||||
|
take: [{ say: 'Pesa più di me. E poi l\'arte non si porta via in tasca.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vaso_fiori', name: 'vaso di fiori', box: [520, 264, 106, 82], at: [505, 330], face: 'right',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Un vasone di fiori. Qualcuno se ne prende cura, al Comune.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alla_via', name: 'indietro, Via dell\'Antica Posta', box: [0, 336, 520, 24], at: [260, 354], exit: { scene: 'via_antica_posta', at: [140, 294], face: 'right' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// In cima a Via dell'Antica Posta, prima della Piazzetta: il muro della villa con l'edera, il ponticello sopra la
|
||||||
|
// strada, il giardinetto con le panchine e due gatti al sole. A sinistra, sotto il ponticello, la scalinata (Andrea).
|
||||||
|
antica_posta_alta: {
|
||||||
|
name: 'In cima a Via dell\'Antica Posta',
|
||||||
|
map: [38, 39],
|
||||||
|
background: 'assets/scenes/via-antica-posta-alta.png',
|
||||||
|
walk: [[0, 360], [0, 322], [190, 226], [283, 222], [470, 360]],
|
||||||
|
depth: [[222, 0.35], [360, 1.15]],
|
||||||
|
entry: [240, 350],
|
||||||
|
hotspots: [
|
||||||
|
{
|
||||||
|
id: 'gatti', name: 'gatti', box: [490, 236, 100, 28], at: [420, 330], face: 'right',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Due gatti che dormono sull\'erba. Il Palio, per loro, può anche aspettare.' }],
|
||||||
|
talk: [{ say: 'Micio, micio... Niente. Per meno di una scatoletta non aprono neanche un occhio.' }],
|
||||||
|
take: [{ say: 'Svegliare un gatto che dorme al sole? Non sono così coraggioso.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'panchine', name: 'panchine', box: [358, 212, 130, 44], at: [375, 300], face: 'right',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Due panchine all\'ombra del muro. Un posto per chi non ha un palio da ritrovare.' }],
|
||||||
|
use: [{ say: 'Mi siederei volentieri, ma il Palio non si ritrova da solo.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ponticello', name: 'ponticello', box: [187, 34, 92, 38], at: [235, 240], face: 'up',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Un ponticello sopra la strada, da una casa all\'altra. Per andare dal vicino senza fare le scale.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'muro_villa', name: 'muro della villa', box: [383, 0, 257, 205], at: [330, 280], face: 'right',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Il muro di Villa Schifanoia, alto alto, con l\'edera che ormai lo tiene su lei.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'su_piazzetta', name: 'su, Piazzetta Cambray Digny', box: [250, 196, 80, 30], at: [280, 226], exit: { scene: 'piazzetta', at: [510, 276], face: 'down' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alla_scalinata', name: 'a sinistra, giù per la scalinata verso il Comune', box: [150, 190, 60, 36], at: [195, 228], exit: { scene: 'scalinata', at: [470, 312], face: 'down' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'giu_antica_posta', name: 'giù, Via dell\'Antica Posta', box: [0, 334, 470, 26], at: [240, 354], exit: { scene: 'via_antica_posta', at: [384, 234], face: 'down' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// La scalinata (foto di Andrea, 28 ottobre): un cortile coi garage; su per le scale la Piazzetta Cambray Digny,
|
||||||
|
// alle spalle i giardini e il retro del Comune.
|
||||||
|
scalinata: {
|
||||||
|
name: 'La scalinata della Piazzetta',
|
||||||
|
map: [30, 42],
|
||||||
|
background: 'assets/scenes/scalinata-piazzetta.png',
|
||||||
|
walk: [[0, 360], [0, 290], [100, 282], [380, 278], [400, 300], [620, 330], [640, 360]],
|
||||||
|
depth: [[278, 0.55], [360, 1.15]],
|
||||||
|
entry: [300, 350],
|
||||||
|
hotspots: [
|
||||||
|
{
|
||||||
|
id: 'saracinesca', name: 'saracinesca', box: [268, 196, 62, 79], at: [300, 290], face: 'up',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Una saracinesca color ruggine. Dietro, scommetto, una macchina e trent\'anni di roba da buttare.' }],
|
||||||
|
open: [{ say: 'Chiusa. E se la apro io, mi tocca anche fare ordine.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'panni', name: 'panni stesi', box: [110, 108, 34, 30], at: [130, 290], face: 'up',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Panni stesi al sole. Se piove, qui lo sanno tutti prima del telegiornale.' }],
|
||||||
|
take: [{ say: 'Rubare i panni stesi? Mi conoscono tutti, mi ritroverebbero prima del Palio.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'su_antica_posta', name: 'su per le scale, poi a sinistra verso Via dell\'Antica Posta', box: [440, 196, 90, 70], at: [470, 312], face: 'up', exit: { scene: 'antica_posta_alta', at: [195, 228], face: 'right' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'su_piazzetta', name: 'su per le scale, poi a destra verso Piazzetta Cambray Digny', box: [530, 196, 80, 70], at: [560, 325], face: 'up', exit: { scene: 'piazzetta', at: [560, 285], face: 'left' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'al_comune', name: 'indietro, i giardini del Comune', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'comune', at: [40, 300], face: 'right' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
// L'area feste (il parco Antonio Berti): dalla rotonda del River, a sinistra.
|
// L'area feste (il parco Antonio Berti): dalla rotonda del River, a sinistra.
|
||||||
area_feste: {
|
area_feste: {
|
||||||
name: 'Area feste',
|
name: 'Area feste',
|
||||||
@@ -2082,7 +2324,7 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'verso_piazza', name: 'Via de\' Medici, verso la Piazza', box: [20, 215, 50, 40], at: [45, 250], exit: { scene: 'via_de_medici', at: [300, 216], face: 'left' },
|
id: 'verso_piazza', name: 'Via de\' Medici, verso la Piazza', box: [20, 215, 50, 40], at: [45, 250], exit: { scene: 'via_de_medici', at: [300, 216], face: 'down' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'verso_pieve', name: 'discesa verso la Pieve', box: [560, 236, 60, 38], at: [600, 266], exit: { scene: 'pieve', at: [600, 350], face: 'left' },
|
id: 'verso_pieve', name: 'discesa verso la Pieve', box: [560, 236, 60, 38], at: [600, 266], exit: { scene: 'pieve', at: [600, 350], face: 'left' },
|
||||||
@@ -2255,6 +2497,9 @@ export const story = {
|
|||||||
{
|
{
|
||||||
id: 'all_area_camper', name: 'a sinistra, verso l\'area camper', box: [320, 160, 110, 42], at: [442, 202], exit: { scene: 'area_camper', at: [600, 330], face: 'left' },
|
id: 'all_area_camper', name: 'a sinistra, verso l\'area camper', box: [320, 160, 110, 42], at: [442, 202], exit: { scene: 'area_camper', at: [600, 330], face: 'left' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'al_giannini', name: 'in fondo a sinistra, verso il campo del Giannini', box: [380, 165, 58, 30], at: [440, 200], exit: { scene: 'campo_giannini', at: [480, 218], face: 'left' },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'alla_stazione', name: 'a destra, verso la stazione', box: [486, 160, 90, 40], at: [474, 202], exit: { scene: 'stazione', at: [20, 300], face: 'right' },
|
id: 'alla_stazione', name: 'a destra, verso la stazione', box: [486, 160, 90, 40], at: [474, 202], exit: { scene: 'stazione', at: [20, 300], face: 'right' },
|
||||||
},
|
},
|
||||||
@@ -2283,9 +2528,6 @@ export const story = {
|
|||||||
look: [{ say: 'Le villette rosa coi balconi bianchi. Da qui a casa mia è un attimo.' }],
|
look: [{ say: 'Le villette rosa coi balconi bianchi. Da qui a casa mia è un attimo.' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'al_giannini', name: 'il campo del Giannini', box: [140, 160, 260, 70], at: [250, 238], exit: { scene: 'campo_giannini', at: [320, 350], face: 'up' },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'a_cardetole', name: 'verso casa, a Cardetole', box: [0, 230, 80, 130], at: [20, 300], exit: { scene: 'cardetole', at: [300, 350], face: 'up' },
|
id: 'a_cardetole', name: 'verso casa, a Cardetole', box: [0, 230, 80, 130], at: [20, 300], exit: { scene: 'cardetole', at: [300, 350], face: 'up' },
|
||||||
},
|
},
|
||||||
@@ -2294,7 +2536,8 @@ export const story = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Il prato del parco dei camper visto dall'altra parte: per i ragazzi, «il campo del Giannini».
|
// Il prato del parco dei camper visto dall'altra parte: per i ragazzi, «il campo del Giannini». Ci si arriva dal
|
||||||
|
// ponticino della Carza; niente uscita verso l'area camper, perché è già questa (Andrea, 28 ottobre).
|
||||||
campo_giannini: {
|
campo_giannini: {
|
||||||
name: 'Il campo del Giannini',
|
name: 'Il campo del Giannini',
|
||||||
map: [82, 96],
|
map: [82, 96],
|
||||||
@@ -2317,7 +2560,7 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'al_parco', name: 'indietro, verso l\'area camper', box: [0, 330, 640, 30], at: [320, 354], exit: { scene: 'area_camper', at: [250, 238], face: 'down' },
|
id: 'al_ponticino', name: 'verso il ponticino della Carza', box: [440, 150, 90, 60], at: [480, 216], exit: { scene: 'ponticino', at: [444, 204], face: 'right' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -2345,6 +2588,7 @@ export const story = {
|
|||||||
use: [{ say: 'Arrampicarmi? Alla mia età, e davanti a tutto il condominio?' }],
|
use: [{ say: 'Arrampicarmi? Alla mia età, e davanti a tutto il condominio?' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Si compare a destra dell'area camper: inquadrature invertite rispetto alle foto (Andrea, 28 ottobre, va bene così).
|
||||||
{
|
{
|
||||||
id: 'alla_pieve', name: 'su verso la Pieve', box: [0, 330, 640, 30], at: [300, 354], exit: { scene: 'area_camper', at: [600, 330], face: 'left' },
|
id: 'alla_pieve', name: 'su verso la Pieve', box: [0, 330, 640, 30], at: [300, 354], exit: { scene: 'area_camper', at: [600, 330], face: 'left' },
|
||||||
},
|
},
|
||||||
@@ -2464,7 +2708,7 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'verso_fortezza', name: 'viale dei cipressi, verso la Fortezza', box: [0, 250, 110, 110], at: [92, 345], exit: { scene: 'viale_cipressi', at: [300, 256], face: 'left' },
|
id: 'verso_fortezza', name: 'viale dei cipressi, verso la Fortezza', box: [0, 250, 110, 110], at: [92, 345], exit: { scene: 'viale_cipressi', at: [300, 256], face: 'down' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -2675,6 +2919,41 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Mariano al pallaio: una mano a bocce, solo per gloria; se Andrea vince, la promessa di Brento Sanico.
|
// Mariano al pallaio: una mano a bocce, solo per gloria; se Andrea vince, la promessa di Brento Sanico.
|
||||||
|
// I ragazzi delle canne: «S'ha andà a fallo?!?» (Andrea, 28 ottobre) e si va a pescare.
|
||||||
|
pescatori: {
|
||||||
|
nodes: {
|
||||||
|
start: {
|
||||||
|
options: [
|
||||||
|
{ text: 'S\'ha andà a fallo?!?', next: 'livello', then: [
|
||||||
|
{ say: 'Gnamo! La canna ti si dà noi.', who: 'pescatori', pose: 'laugh' },
|
||||||
|
{ say: 'Come peschi?', who: 'pescatori', pose: 'talk' },
|
||||||
|
] },
|
||||||
|
{ text: 'Quanti ne prendete, di solito?', then: [{ say: 'Dipende da chi racconta. Il Nanni dice dieci, il cestino dice due.', who: 'pescatori', pose: 'laugh' }] },
|
||||||
|
{ text: 'Buona pesca, ragazzi.', next: 'end', then: [{ say: 'Se vedi il palio, salutacelo!', who: 'pescatori', pose: 'talk' }] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
livello: {
|
||||||
|
options: [
|
||||||
|
{ text: 'Alla bona, tanto per bagnare la lenza.', next: 'end', then: [{ say: 'Alla bona. Due pesci e si va a casa contenti.', who: 'pescatori' }, { closeup: 'pesca', level: 'bona' }] },
|
||||||
|
{ text: 'Seria.', next: 'end', then: [{ say: 'Seria: tre pesci, e lo stivale non conta.', who: 'pescatori', pose: 'proud' }, { closeup: 'pesca', level: 'seria' }] },
|
||||||
|
{ text: 'A fassi male.', next: 'end', then: [{ say: 'A fassi male? Quattro pesci, e le carpe tirano come trattori.', who: 'pescatori', pose: 'laugh' }, { closeup: 'pesca', level: 'male' }] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Da solo al campo del Circolo: Andrea decide quanto farsi male.
|
||||||
|
basket: {
|
||||||
|
nodes: {
|
||||||
|
start: {
|
||||||
|
options: [
|
||||||
|
{ text: 'Due tiri alla bona, tanto per scaldarmi.', next: 'end', then: [{ say: 'Tre su dieci e sono contento.' }, { closeup: 'basket', level: 'bona' }] },
|
||||||
|
{ text: 'Due tiri seri.', next: 'end', then: [{ say: 'Cinque su dieci. Come ai tempi del Circolino.' }, { closeup: 'basket', level: 'seria' }] },
|
||||||
|
{ text: 'A fassi male.', next: 'end', then: [{ say: 'Sette su dieci, senza guardare la parabola. Vediamo chi sono.' }, { closeup: 'basket', level: 'male' }] },
|
||||||
|
{ text: 'Lascia stare, ho il palio da trovare.', next: 'end' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
mariano: {
|
mariano: {
|
||||||
nodes: {
|
nodes: {
|
||||||
start: {
|
start: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
|
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
|
||||||
export const VERSION = '0.1.19';
|
export const VERSION = '0.1.28';
|
||||||
|
|
||||||
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica,
|
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica,
|
||||||
// mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf).
|
// mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf).
|
||||||
|
|||||||
@@ -53,16 +53,16 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
|
|||||||
.bocce-adjust button { min-height: 44px; padding: 8px 12px; border: 1px solid #897b57; border-radius: 4px; background: #38453c; color: #f2e1bf; cursor: pointer; font-size: 11px; }
|
.bocce-adjust button { min-height: 44px; padding: 8px 12px; border: 1px solid #897b57; border-radius: 4px; background: #38453c; color: #f2e1bf; cursor: pointer; font-size: 11px; }
|
||||||
#bocce-throw { background: #ead09a; color: #202b25; min-width: 100px; }
|
#bocce-throw { background: #ead09a; color: #202b25; min-width: 100px; }
|
||||||
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
|
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
|
||||||
#bocce-help, #biliardo-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
#bocce-help, #biliardo-help, #basket-help, #pesca-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
||||||
#bocce-help summary, #biliardo-help summary { cursor: pointer; padding: 6px 0; }
|
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary { cursor: pointer; padding: 6px 0; }
|
||||||
#bocce-help p, #biliardo-help p { max-width: 76em; line-height: 1.7; }
|
#bocce-help p, #biliardo-help p, #basket-help p, #pesca-help p { max-width: 76em; line-height: 1.7; }
|
||||||
@media (max-width: 600px), (orientation: landscape) and (max-height: 520px) {
|
@media (max-width: 600px), (orientation: landscape) and (max-height: 520px) {
|
||||||
#bocce-controls { padding: 8px; font-size: 10px; overflow-y: auto; min-height: 0; }
|
#bocce-controls { padding: 8px; font-size: 10px; overflow-y: auto; min-height: 0; }
|
||||||
.bocce-adjust { flex-wrap: wrap; gap: 6px; }
|
.bocce-adjust { flex-wrap: wrap; gap: 6px; }
|
||||||
.bocce-adjust label { order: -1; flex-basis: 100%; max-width: none; }
|
.bocce-adjust label { order: -1; flex-basis: 100%; max-width: none; }
|
||||||
.bocce-adjust button { flex: 1; font-size: 9px; padding: 6px; }
|
.bocce-adjust button { flex: 1; font-size: 9px; padding: 6px; }
|
||||||
#bocce-throw { flex-basis: 100%; }
|
#bocce-throw { flex-basis: 100%; }
|
||||||
#bocce-help, #biliardo-help { font-size: 9px; }
|
#bocce-help, #biliardo-help, #basket-help, #pesca-help { font-size: 9px; }
|
||||||
}
|
}
|
||||||
.effects-control { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; font-size: clamp(9px, 1.2vw, 13px); }
|
.effects-control { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; font-size: clamp(9px, 1.2vw, 13px); }
|
||||||
#effects-volume { width: 130px; accent-color: var(--accent); }
|
#effects-volume { width: 130px; accent-color: var(--accent); }
|
||||||
@@ -259,10 +259,18 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
|
|||||||
#credits dt { text-align: right; color: var(--dim); font-size: clamp(7.2px, 1.01vw, 10.08px); letter-spacing: .12em; text-transform: uppercase; }
|
#credits dt { text-align: right; color: var(--dim); font-size: clamp(7.2px, 1.01vw, 10.08px); letter-spacing: .12em; text-transform: uppercase; }
|
||||||
#credits dd span { display: block; }
|
#credits dd span { display: block; }
|
||||||
/* I nomi col ritratto: si toccano e compare il volto a pixel sopra i Ringraziamenti. */
|
/* I nomi col ritratto: si toccano e compare il volto a pixel sopra i Ringraziamenti. */
|
||||||
#credits-reel { flex: 0 0 auto; height: clamp(56px, 24vh, 200px); width: 100%; overflow: hidden; margin: .5em 0 .3em;
|
#credits-reel { flex: 0 0 auto; position: relative; height: clamp(56px, 24vh, 200px); width: 100%; margin: .5em 0 .3em; }
|
||||||
|
/* La fila scorre da sola (main.js) ed è anche un normale scorrimento orizzontale: dito, rotella, trascinamento col mouse. */
|
||||||
|
#credits-reel .track { display: flex; gap: 10px; height: 100%; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; touch-action: pan-x; cursor: grab;
|
||||||
-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
|
-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
|
||||||
#credits-reel .track { display: flex; gap: 10px; height: 100%; width: max-content; animation: reel 240s linear infinite; }
|
#credits-reel .track::-webkit-scrollbar { display: none; }
|
||||||
#credits-reel img { flex: none; height: 100%; aspect-ratio: 16 / 9; object-fit: cover; image-rendering: pixelated; border-radius: 6px; border: 2px solid #3a2d42; }
|
#credits-reel .track:active { cursor: grabbing; }
|
||||||
|
#credits-reel img { flex: none; height: 100%; aspect-ratio: 16 / 9; object-fit: cover; image-rendering: pixelated; border-radius: 6px; border: 2px solid #3a2d42; cursor: pointer; user-select: none; -webkit-user-drag: none; }
|
||||||
|
#credits-reel img:hover { border-color: #fff; }
|
||||||
|
#credits-reel .arrow { position: absolute; top: 50%; transform: translateY(-50%); z-index: 1; width: 1.8em; height: 2.4em; padding: 0; border: 1px solid var(--accent); border-radius: 6px;
|
||||||
|
background: rgba(15, 10, 20, .8); color: var(--accent); font: inherit; font-size: clamp(12px, 1.8vw, 20px); line-height: 1; cursor: pointer; opacity: .85; }
|
||||||
|
#credits-reel .arrow:hover, #credits-reel .arrow:focus-visible { opacity: 1; color: #fff; }
|
||||||
|
#credits-reel .arrow.prev { left: .3em; } #credits-reel .arrow.next { right: .3em; }
|
||||||
#credits-reel img.face { aspect-ratio: 1; border-color: var(--accent); }
|
#credits-reel img.face { aspect-ratio: 1; border-color: var(--accent); }
|
||||||
#credits-reel img.palio { aspect-ratio: auto; border: 0; }
|
#credits-reel img.palio { aspect-ratio: auto; border: 0; }
|
||||||
/* La fine: il quadro del palio accanto alle parole (sotto, sugli schermi stretti). */
|
/* La fine: il quadro del palio accanto alle parole (sotto, sugli schermi stretti). */
|
||||||
@@ -275,14 +283,14 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
|
|||||||
#ending figure.palio figcaption { font-size: clamp(6.5px, .85vw, 9px); color: var(--dim); max-width: 22em; }
|
#ending figure.palio figcaption { font-size: clamp(6.5px, .85vw, 9px); color: var(--dim); max-width: 22em; }
|
||||||
#ending .ending-row > div { max-width: 26em; text-align: center; }
|
#ending .ending-row > div { max-width: 26em; text-align: center; }
|
||||||
@media (max-aspect-ratio: 1/1) { #ending .ending-row { flex-direction: column; } #ending figure.palio img { height: 30vh; } }
|
@media (max-aspect-ratio: 1/1) { #ending .ending-row { flex-direction: column; } #ending figure.palio img { height: 30vh; } }
|
||||||
@keyframes reel { to { transform: translateX(-50%); } }
|
|
||||||
@media (prefers-reduced-motion: reduce) { #credits-reel .track { animation: none; } }
|
|
||||||
#credits .credit-face { all: unset; display: block; cursor: pointer; color: var(--accent); text-decoration: underline dotted; text-underline-offset: .25em; }
|
#credits .credit-face { all: unset; display: block; cursor: pointer; color: var(--accent); text-decoration: underline dotted; text-underline-offset: .25em; }
|
||||||
#credits .credit-face:hover, #credits .credit-face:focus-visible { color: #fff; }
|
#credits .credit-face:hover, #credits .credit-face:focus-visible { color: #fff; }
|
||||||
#face { position: absolute; inset: 0; margin: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .8em;
|
#face { position: absolute; inset: 0; margin: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .8em;
|
||||||
background: rgba(15, 10, 20, .92); cursor: pointer; z-index: 1; }
|
background: rgba(15, 10, 20, .92); cursor: pointer; z-index: 1; }
|
||||||
#face[hidden] { display: none; }
|
#face[hidden] { display: none; }
|
||||||
#face img { height: min(60vmin, 384px); width: auto; max-width: 90%; image-rendering: pixelated; border: 3px solid var(--accent); border-radius: 8px; }
|
#face img { height: min(60vmin, 384px); width: auto; max-width: 90%; image-rendering: pixelated; border: 3px solid var(--accent); border-radius: 8px; }
|
||||||
|
/* Uno sfondo ingrandito: largo quanto può restando 16:9 dentro lo schermo. */
|
||||||
|
#face img.scene { height: auto; width: min(92%, 128vh, 1024px); }
|
||||||
#face figcaption { color: var(--ink); font-size: clamp(9px, 1.4vw, 14px); text-align: center; padding: 0 1em; }
|
#face figcaption { color: var(--ink); font-size: clamp(9px, 1.4vw, 14px); text-align: center; padding: 0 1em; }
|
||||||
#credits dd { margin: 0; text-align: left; color: var(--ink); font-size: clamp(8px, 1.15vw, 12px); line-height: 1.6; font-weight: bold; }
|
#credits dd { margin: 0; text-align: left; color: var(--ink); font-size: clamp(8px, 1.15vw, 12px); line-height: 1.6; font-weight: bold; }
|
||||||
#credits .legal { color: var(--dim); font-size: clamp(7.2px, 0.86vw, 9.36px); opacity: .8; }
|
#credits .legal { color: var(--dim); font-size: clamp(7.2px, 0.86vw, 9.36px); opacity: .8; }
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { newBasket, press, release, cancel, update, shoot, setAngle, setPower, aimScreen, nudge, arc, guide, status, canThrow, releasePoint,
|
||||||
|
powerAt, SWING, BALLS, SPOTS, LEVELS, RIM, BOARD, POLE, FLOOR, BALL, ANGLE } from '../src/engine/basket.js';
|
||||||
|
import { story } from '../src/story/story.js';
|
||||||
|
import { Game } from '../src/engine/game.js';
|
||||||
|
import { settle } from './helpers.mjs';
|
||||||
|
|
||||||
|
const fly = c => { for (let i = 0; i < 2000 && c.step === 'flight'; i++) update(c, 1 / 120); return c.ball; };
|
||||||
|
const rest = c => { for (let i = 0; i < 200 && c.step === 'settle'; i++) update(c, 1 / 60); };
|
||||||
|
// Cerca un tiro che entra dalla postazione di adesso: la mano ferma dei test.
|
||||||
|
function goodShot(c) {
|
||||||
|
for (let angle = 40; angle <= 70; angle += 1) for (let power = 0.2; power <= 1; power += 0.02) {
|
||||||
|
const probe = structuredClone(c);
|
||||||
|
setAngle(probe, angle); setPower(probe, power); shoot(probe);
|
||||||
|
if (fly(probe).scored) return { angle, power };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('la forza sale finché si tiene premuto; un tocco breve non spreca il pallone', () => {
|
||||||
|
const c = newBasket(1);
|
||||||
|
assert.equal(powerAt(SWING / 2), 0.5); assert.equal(powerAt(SWING * 2), 1);
|
||||||
|
assert.ok(press(c)); update(c, 0.05); assert.ok(release(c)); // troppo breve: annullato
|
||||||
|
assert.equal(c.thrown, 0); assert.equal(c.power, 0.5, 'la forza torna com\'era');
|
||||||
|
assert.ok(press(c)); update(c, 0.7); assert.ok(release(c));
|
||||||
|
assert.equal(c.thrown, 1); assert.equal(c.step, 'flight');
|
||||||
|
assert.ok(Math.abs(c.ball.vx) > 0 && c.ball.vy < 0, 'la palla parte in alto verso il canestro');
|
||||||
|
assert.ok(!press(c), 'in volo non si carica');
|
||||||
|
assert.ok(!cancel(c));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la mira: l\'angolo segue il punto toccato, resta fra i limiti, non si tira all\'indietro', () => {
|
||||||
|
const c = newBasket(2), r = releasePoint(c);
|
||||||
|
assert.ok(aimScreen(c, [r.x + 100, r.y - 100])); assert.equal(Math.round(c.angle), 45);
|
||||||
|
assert.ok(aimScreen(c, [r.x + 10, r.y - 400])); assert.equal(c.angle, ANGLE.max, 'non oltre il massimo');
|
||||||
|
assert.ok(aimScreen(c, [r.x + 300, r.y])); assert.equal(c.angle, ANGLE.min, 'non sotto il minimo');
|
||||||
|
assert.ok(!aimScreen(c, [r.x - 50, r.y - 50]), 'dietro di lui niente');
|
||||||
|
assert.ok(nudge(c, 1)); assert.equal(c.angle, ANGLE.min + 2);
|
||||||
|
assert.ok(nudge(c, 1, true)); assert.equal(c.angle, ANGLE.min + 2.5);
|
||||||
|
press(c); assert.ok(!aimScreen(c, [r.x + 100, r.y - 100]), 'mentre si carica la mira è ferma');
|
||||||
|
assert.ok(!setPower(c, 0.3));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la parabola della guida comincia dalle mani e scende per la gravità', () => {
|
||||||
|
const c = newBasket(3), r = releasePoint(c), pts = arc(c, 1.5);
|
||||||
|
assert.deepEqual(pts[0], r);
|
||||||
|
assert.ok(pts[5].y < r.y, 'prima sale');
|
||||||
|
assert.ok(pts.at(-1).y > pts[5].y, 'poi scende');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('da ogni postazione esiste un tiro che entra, e un tiro entrato conta canestro e punti', () => {
|
||||||
|
for (const [i, s] of SPOTS.entries()) {
|
||||||
|
const c = newBasket(10 + i); c.spot = i;
|
||||||
|
const shot = goodShot(c);
|
||||||
|
assert.ok(shot, `nessun tiro entra ${s.name}`);
|
||||||
|
setAngle(c, shot.angle); setPower(c, shot.power); shoot(c);
|
||||||
|
const b = fly(c);
|
||||||
|
assert.ok(b.scored && b.dead);
|
||||||
|
assert.ok(c.events.some(e => e.kind === 'basket-swish' || e.kind === 'basket-in'), 'si sente la retina');
|
||||||
|
rest(c);
|
||||||
|
assert.equal(c.baskets, 1); assert.equal(c.points, s.points);
|
||||||
|
assert.equal(c.spot, Math.min(i + 1, SPOTS.length - 1), 'dopo un canestro si arretra');
|
||||||
|
assert.equal(c.step, 'aim');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('con l\'angolo iniziale, da ogni postazione entra in una fascia di forza contigua di almeno l\'8%', () => {
|
||||||
|
for (const [i, s] of SPOTS.entries()) {
|
||||||
|
const made = [];
|
||||||
|
for (let p = 0; p <= 1.0001; p += 0.005) {
|
||||||
|
const c = newBasket(1); c.spot = i; setPower(c, p); shoot(c);
|
||||||
|
made.push(fly(c).scored);
|
||||||
|
}
|
||||||
|
let best = 0, run = 0;
|
||||||
|
for (const v of made) { run = v ? run + 1 : 0; best = Math.max(best, run); }
|
||||||
|
assert.ok(best >= 16, `${s.name}: la fascia migliore è ${(best - 1) * 0.5}%`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il palo ferma solo dalla cima in giù; il tabellone rimanda anche da dietro; il canestro conta subito', () => {
|
||||||
|
const c = newBasket(11); setAngle(c, 78); setPower(c, 1); shoot(c);
|
||||||
|
let b = fly(c);
|
||||||
|
assert.ok(!c.events.some(e => e.kind === 'basket-swish'), 'sopra il palo la palla vola via: non è un tiro pulito');
|
||||||
|
const d = newBasket(12);
|
||||||
|
d.ball = { x: POLE.x - 30, y: FLOOR - 30, vx: 300, vy: 0, time: 0, bounces: 0, rim: false, board: false, under: false, scored: false, dead: false, spin: 0 };
|
||||||
|
d.step = 'flight'; for (let i = 0; i < 30; i++) update(d, 1 / 120);
|
||||||
|
assert.ok(d.ball.board && d.ball.vx < 0, 'il palo la rimanda indietro come un tabellone');
|
||||||
|
const e = newBasket(13);
|
||||||
|
e.ball = { x: BOARD.x + BOARD.depth + 30, y: (BOARD.top + BOARD.bottom) / 2, vx: -300, vy: 0, time: 0, bounces: 0, rim: false, board: false, under: false, scored: false, dead: false, spin: 0 };
|
||||||
|
e.step = 'flight'; for (let i = 0; i < 30; i++) update(e, 1 / 120);
|
||||||
|
assert.ok(e.ball.board && e.ball.x > BOARD.x, 'da dietro non lo attraversa');
|
||||||
|
const f = newBasket(14); f.thrown = BALLS - 1; f.skill = LEVELS.bona; f.baskets = 2;
|
||||||
|
const shot = goodShot(f); setAngle(f, shot.angle); setPower(f, shot.power); shoot(f);
|
||||||
|
for (let i = 0; i < 2000 && !f.ball.scored; i++) update(f, 1 / 120);
|
||||||
|
assert.equal(f.baskets, 3, 'il canestro conta appena entra');
|
||||||
|
assert.ok(typeof f.ball.scoredAt === 'number');
|
||||||
|
assert.equal(f.result?.winner, 'player', 'all\'ultimo pallone il risultato c\'è già mentre la palla scende');
|
||||||
|
fly(f); rest(f);
|
||||||
|
assert.equal(f.step, 'result'); assert.equal(f.result.baskets, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la guida si ferma al primo urto', () => {
|
||||||
|
const c = newBasket(15); c.spot = 1; setAngle(c, 30); setPower(c, 1);
|
||||||
|
const g = guide(c, 3), last = g.at(-1);
|
||||||
|
assert.ok(g.length < arc(c, 3).length, 'non arriva in fondo');
|
||||||
|
assert.ok(last.x + BALL > BOARD.x - 2 || last.y + BALL > FLOOR - 2 || Math.hypot(last.x - RIM.x1, last.y - RIM.y) < BALL + 3, 'si ferma su tabellone, campo o ferro');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('un tiro corto batte sul campo e rimbalza; uno lungo prende il tabellone e torna indietro', () => {
|
||||||
|
const c = newBasket(4); c.spot = 3; // da tre, piano: non arriva nemmeno al palo
|
||||||
|
setAngle(c, 45); setPower(c, 0); shoot(c);
|
||||||
|
let b = fly(c);
|
||||||
|
assert.ok(!b.scored && b.bounces > 0, 'rimbalza per terra');
|
||||||
|
assert.ok(c.events.some(e => e.kind === 'basket-bounce'));
|
||||||
|
assert.ok(b.y + BALL <= FLOOR + 0.01, 'non sprofonda');
|
||||||
|
const d = newBasket(5); d.spot = 1;
|
||||||
|
setAngle(d, 34); setPower(d, 1); shoot(d);
|
||||||
|
b = fly(d);
|
||||||
|
assert.ok(b.board, 'ha preso il tabellone');
|
||||||
|
assert.ok(c.events.every(e => e.kind !== 'basket-board') && d.events.some(e => e.kind === 'basket-board'));
|
||||||
|
assert.ok(b.x < BOARD.x, 'non lo attraversa');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la palla rimbalza sul ferro, e da sotto non è canestro', () => {
|
||||||
|
const c = newBasket(6);
|
||||||
|
c.ball = { x: RIM.x1 - 2, y: RIM.y - 60, vx: 0, vy: 150, time: 0, bounces: 0, rim: false, board: false, under: false, scored: false, dead: false, spin: 0 };
|
||||||
|
c.step = 'flight';
|
||||||
|
for (let i = 0; i < 60; i++) update(c, 1 / 120);
|
||||||
|
assert.ok(c.ball.rim, 'il davanti del ferro l\'ha toccata');
|
||||||
|
assert.ok(c.events.some(e => e.kind === 'basket-rim'));
|
||||||
|
const d = newBasket(7);
|
||||||
|
d.ball = { x: (RIM.x1 + RIM.x2) / 2, y: RIM.y + 20, vx: 0, vy: -200, time: 0, bounces: 0, rim: false, board: false, under: false, scored: false, dead: false, spin: 0 };
|
||||||
|
d.step = 'flight';
|
||||||
|
fly(d);
|
||||||
|
assert.ok(d.ball.under && !d.ball.scored, 'sale attraverso l\'anello e ricade: non vale');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dieci palloni, poi il risultato: la gloria dipende dal livello', () => {
|
||||||
|
for (const [level, { goal }] of Object.entries(LEVELS)) {
|
||||||
|
const c = newBasket(8, { level });
|
||||||
|
let made = 0;
|
||||||
|
for (let i = 0; i < BALLS; i++) {
|
||||||
|
assert.ok(canThrow(c));
|
||||||
|
const shot = made < goal ? goodShot(c) : { angle: 45, power: 0 }; // quanto basta per la gloria, poi a vuoto
|
||||||
|
setAngle(c, shot.angle); setPower(c, shot.power); shoot(c);
|
||||||
|
if (fly(c).scored) made++;
|
||||||
|
rest(c);
|
||||||
|
}
|
||||||
|
assert.equal(c.step, 'result'); assert.equal(c.thrown, BALLS); assert.ok(!canThrow(c));
|
||||||
|
assert.equal(c.result.baskets, goal); assert.equal(c.result.winner, 'player', level);
|
||||||
|
assert.match(status(c), /canestri su 10/);
|
||||||
|
assert.ok(!shoot(c), 'finita');
|
||||||
|
}
|
||||||
|
const miss = newBasket(9, { level: 'bona' });
|
||||||
|
for (let i = 0; i < BALLS; i++) { setAngle(miss, 45); setPower(miss, 0); shoot(miss); fly(miss); rest(miss); }
|
||||||
|
assert.equal(miss.result.winner, 'cpu'); assert.equal(miss.spot, 0, 'senza canestri non ci si muove');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nella storia: dal campo di Via Cipriani si sceglie il livello e parte il gioco; la gloria lascia il flag', () => {
|
||||||
|
const g = new Game(story);
|
||||||
|
g.go('via_cipriani', [340, 316]); settle(g);
|
||||||
|
g.act('use', 'campo_basket'); settle(g);
|
||||||
|
assert.ok(g.dialogue, 'si decide come tirare');
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'Due tiri seri.')); settle(g);
|
||||||
|
assert.equal(g.closeup?.id, 'basket'); assert.equal(g.closeup.level, 'seria');
|
||||||
|
g.closeup.result = { winner: 'player', baskets: 6, points: 13, goal: 5 }; g.closeup.step = 'result';
|
||||||
|
g.closeupClose(); settle(g);
|
||||||
|
assert.ok(g.state.flags.basket_vinto, 'la gloria resta');
|
||||||
|
assert.ok(!g.closeup);
|
||||||
|
});
|
||||||
@@ -6,6 +6,8 @@ import { staticServer } from './helpers.mjs';
|
|||||||
import { mkdirSync, readFileSync } from 'node:fs';
|
import { mkdirSync, readFileSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { solution, route } from '../src/story/solution.js';
|
import { solution, route } from '../src/story/solution.js';
|
||||||
|
import { story } from '../src/story/story.js';
|
||||||
|
import { reelPictures } from '../src/engine/game.js';
|
||||||
import { checkMultimedia } from './multimedia.browser.mjs';
|
import { checkMultimedia } from './multimedia.browser.mjs';
|
||||||
|
|
||||||
const OUT = process.env.OUT ?? '/tmp/regolo-shots';
|
const OUT = process.env.OUT ?? '/tmp/regolo-shots';
|
||||||
@@ -120,6 +122,35 @@ try {
|
|||||||
await page.screenshot({ path: `${OUT}/01d-ritratto.png` });
|
await page.screenshot({ path: `${OUT}/01d-ritratto.png` });
|
||||||
await page.click('#face');
|
await page.click('#face');
|
||||||
assert.ok(await page.isHidden('#face'));
|
assert.ok(await page.isHidden('#face'));
|
||||||
|
// Il carosello: tutte le immagini (due giri), scorre da solo, si ferma sotto il mouse, le frecce lo spingono di una
|
||||||
|
// foto, un tocco su una foto la mostra grande col nome del posto.
|
||||||
|
{
|
||||||
|
const track = page.locator('#credits-reel .track');
|
||||||
|
assert.equal(await track.locator('img').count(), reelPictures(story).length * 2, 'ogni sfondo e ogni ritratto, due giri');
|
||||||
|
await page.mouse.move(5, 5);
|
||||||
|
const x0 = await track.evaluate(t => t.scrollLeft);
|
||||||
|
await page.waitForTimeout(600);
|
||||||
|
const x1 = await track.evaluate(t => t.scrollLeft);
|
||||||
|
assert.ok(x1 > x0, `il carosello scorre da solo (${x0} → ${x1})`);
|
||||||
|
await track.hover({ position: { x: 20, y: 20 } });
|
||||||
|
const x2 = await track.evaluate(t => t.scrollLeft);
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
assert.equal(await track.evaluate(t => t.scrollLeft), x2, 'sotto il mouse si ferma');
|
||||||
|
await page.click('#credits-reel .arrow.next');
|
||||||
|
await page.waitForTimeout(800);
|
||||||
|
const x3 = await track.evaluate(t => t.scrollLeft);
|
||||||
|
assert.ok(x3 - x2 > 150, `la freccia spinge di una foto (${x2} → ${x3})`);
|
||||||
|
const third = track.locator('img').nth(2);
|
||||||
|
const name = await third.getAttribute('alt');
|
||||||
|
await third.click();
|
||||||
|
assert.ok(await page.isVisible('#face img.scene'), 'la foto si vede grande');
|
||||||
|
assert.equal(await page.textContent('#face figcaption'), name);
|
||||||
|
await page.waitForFunction(() => document.querySelector('#face img').naturalWidth > 0, null, { timeout: 5000 });
|
||||||
|
await page.screenshot({ path: `${OUT}/01e-carosello-zoom.png` });
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
assert.ok(await page.isHidden('#face'), 'Esc chiude la foto grande, non i crediti');
|
||||||
|
assert.ok(await page.isVisible('#credits'));
|
||||||
|
}
|
||||||
const onTop = await page.evaluate(() => { const r = document.getElementById('close-credits').getBoundingClientRect();
|
const onTop = await page.evaluate(() => { const r = document.getElementById('close-credits').getBoundingClientRect();
|
||||||
return document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2)?.id === 'close-credits'; });
|
return document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2)?.id === 'close-credits'; });
|
||||||
assert.ok(onTop, 'i crediti stanno sopra il titolo, non dietro');
|
assert.ok(onTop, 'i crediti stanno sopra il titolo, non dietro');
|
||||||
@@ -418,6 +449,72 @@ try {
|
|||||||
await bocce.close();
|
await bocce.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// «Fai due tiri» al campo del Circolo: il mouse mira, tenendo premuto la forza sale, lasciando la palla vola; Esc esce.
|
||||||
|
{
|
||||||
|
const k = await open({ width: 1280, height: 860 });
|
||||||
|
await k.click('#splash');
|
||||||
|
await k.click('#new-game');
|
||||||
|
await k.click('#rione [data-rione="campomigliaio"]');
|
||||||
|
await k.click('#skip-intro');
|
||||||
|
await settle(k);
|
||||||
|
await k.evaluate(() => game.go('via_cipriani', [340, 316]));
|
||||||
|
await settle(k);
|
||||||
|
await k.evaluate(() => game.act('use', 'campo_basket'));
|
||||||
|
await k.waitForFunction(() => game.dialogue && !game.busy, null, { timeout: 20000 });
|
||||||
|
await k.locator('#choices li', { hasText: 'Due tiri seri.' }).click();
|
||||||
|
await k.waitForFunction(() => game.closeup?.id === 'basket' && game.closeup.step === 'aim' && !game.speech, null, { timeout: 30000 });
|
||||||
|
const r = await k.locator('#scene').boundingBox();
|
||||||
|
const at = (x, y) => [r.x + x / 640 * r.width, r.y + y / 360 * r.height];
|
||||||
|
await k.mouse.move(...at(560, 60));
|
||||||
|
const high = await k.evaluate(() => game.closeup.angle);
|
||||||
|
await k.mouse.move(...at(600, 200));
|
||||||
|
assert.ok(await k.evaluate(() => game.closeup.angle) < high, 'la mira segue il mouse');
|
||||||
|
await k.mouse.down();
|
||||||
|
await k.waitForTimeout(500);
|
||||||
|
assert.ok(await k.evaluate(() => game.closeup.power > 0.2 && game.closeup.holding), 'tenendo premuto la forza sale');
|
||||||
|
await k.screenshot({ path: `${OUT}/03e-basket-carica.png` });
|
||||||
|
await k.mouse.up();
|
||||||
|
assert.equal(await k.evaluate(() => game.closeup.step), 'flight', 'lasciando, la palla parte');
|
||||||
|
await k.waitForTimeout(350);
|
||||||
|
await k.screenshot({ path: `${OUT}/03f-basket-volo.png` });
|
||||||
|
await k.waitForFunction(() => game.closeup.step === 'aim' && game.closeup.thrown === 1, null, { timeout: 20000 });
|
||||||
|
await k.keyboard.press('Escape');
|
||||||
|
await k.waitForFunction(() => game.speech?.text.startsWith('Lascio il pallone'), null, { timeout: 5000 });
|
||||||
|
await k.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// La pesca al lago dell'Anatre: coi ragazzi in Fortezza «S'ha andà a fallo?!?», poi il lancio tenendo premuto e l'attesa.
|
||||||
|
{
|
||||||
|
const k = await open({ width: 1280, height: 860 });
|
||||||
|
await k.click('#splash');
|
||||||
|
await k.click('#new-game');
|
||||||
|
await k.click('#rione [data-rione="campomigliaio"]');
|
||||||
|
await k.click('#skip-intro');
|
||||||
|
await settle(k);
|
||||||
|
await k.evaluate(() => game.go('fortezza', [240, 334]));
|
||||||
|
await settle(k);
|
||||||
|
await clickScene(k, await hotspotCenter(k, 'pescatori'));
|
||||||
|
await k.waitForFunction(() => game.dialogue && !game.busy, null, { timeout: 40000 });
|
||||||
|
await k.locator('#choices li', { hasText: 'S’ha andà a fallo?!?' }).or(k.locator('#choices li', { hasText: "S'ha andà a fallo?!?" })).first().click();
|
||||||
|
await k.waitForFunction(() => game.options().some(o => o.text === 'Seria.'), null, { timeout: 30000 });
|
||||||
|
await k.locator('#choices li', { hasText: 'Seria.' }).click();
|
||||||
|
await k.waitForFunction(() => game.closeup?.id === 'pesca' && game.closeup.step === 'aim' && !game.speech, null, { timeout: 30000 });
|
||||||
|
const r = await k.locator('#scene').boundingBox();
|
||||||
|
await k.mouse.move(r.x + r.width * 0.7, r.y + r.height * 0.7);
|
||||||
|
assert.ok(await k.evaluate(() => game.closeup.power > 0.5), 'toccando l\'acqua al largo la forza sale');
|
||||||
|
await k.mouse.down();
|
||||||
|
await k.waitForTimeout(500);
|
||||||
|
assert.ok(await k.evaluate(() => game.closeup.holding && game.closeup.power > 0.2), 'tenendo premuto si carica il lancio');
|
||||||
|
await k.mouse.up();
|
||||||
|
await k.waitForFunction(() => game.closeup.step === 'wait', null, { timeout: 5000 });
|
||||||
|
await k.waitForTimeout(600);
|
||||||
|
await k.screenshot({ path: `${OUT}/03g-pesca-attesa.png` });
|
||||||
|
assert.equal(await k.textContent('#bocce-throw'), 'Ferra');
|
||||||
|
await k.keyboard.press('Escape');
|
||||||
|
await k.waitForFunction(() => game.speech?.text.startsWith('Lascio la canna'), null, { timeout: 5000 });
|
||||||
|
await k.close();
|
||||||
|
}
|
||||||
|
|
||||||
// Il biliardo del Circolo 4 Maggio: la sala, poi la partita; un tocco sul tavolo mira, tenendo premuto la forza sale.
|
// Il biliardo del Circolo 4 Maggio: la sala, poi la partita; un tocco sul tavolo mira, tenendo premuto la forza sale.
|
||||||
{
|
{
|
||||||
const b = await open({ width: 1280, height: 860 });
|
const b = await open({ width: 1280, height: 860 });
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ test('clic intelligente: parla con le persone, guarda le cose, attraversa le usc
|
|||||||
const { g, c, at } = setup();
|
const { g, c, at } = setup();
|
||||||
assert.equal(c.sentence('vecchietti'), 'Parla con vecchietti');
|
assert.equal(c.sentence('vecchietti'), 'Parla con vecchietti');
|
||||||
assert.equal(c.sentence('manifesto'), 'Guarda manifesto del Palio');
|
assert.equal(c.sentence('manifesto'), 'Guarda manifesto del Palio');
|
||||||
assert.equal(c.sentence('verso_piazzetta'), 'Vai a Piazzetta Cambray Digny, verso Via de\' Medici');
|
assert.equal(c.sentence('verso_piazzetta'), 'Vai a Via dell\'Antica Posta, verso Piazzetta Cambray Digny');
|
||||||
assert.equal(c.clickScene(at('vecchietti')), 'act');
|
assert.equal(c.clickScene(at('vecchietti')), 'act');
|
||||||
settle(g);
|
settle(g);
|
||||||
assert.equal(g.dialogue?.id, 'vecchietti');
|
assert.equal(g.dialogue?.id, 'vecchietti');
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { newPesca, press, release, cancel, update, cast, strike, shoot, setPower, aimScreen, nudge, status, canThrow, canCast, canAdjust,
|
||||||
|
distanceFor, biting, CASTS, LEVELS, FISHES, NEAR, FAR, BANK, SHORE, WATER, FLY, REST } from '../src/engine/pesca.js';
|
||||||
|
import { story } from '../src/story/story.js';
|
||||||
|
import { Game } from '../src/engine/game.js';
|
||||||
|
import { settle } from './helpers.mjs';
|
||||||
|
|
||||||
|
// Come il gioco: a ogni fotogramma gli eventi sonori si consumano (e qui si ricordano).
|
||||||
|
const heard = new Set();
|
||||||
|
const run = (c, seconds, dt = 1 / 60) => { for (let t = 0; t < seconds; t += dt) { update(c, dt); for (const e of c.events.splice(0)) heard.add(e.kind); } };
|
||||||
|
// Lancia e aspetta l'abboccata vera (non i tocchi), poi ferra: la mano ferma dei test.
|
||||||
|
function hook(c) {
|
||||||
|
assert.ok(cast(c)); run(c, FLY + 0.05);
|
||||||
|
assert.equal(c.step, 'wait');
|
||||||
|
for (let i = 0; i < 60 * 15 && !c.float.bitten; i++) update(c, 1 / 60);
|
||||||
|
assert.ok(c.float.bitten, 'prima o poi abbocca');
|
||||||
|
assert.ok(strike(c)); assert.equal(c.step, 'fight');
|
||||||
|
}
|
||||||
|
// Recupera con giudizio: tiene premuto finché la lenza non è quasi al limite, poi molla un attimo.
|
||||||
|
function land(c) {
|
||||||
|
for (let i = 0; i < 60 * 90 && c.step === 'fight'; i++) {
|
||||||
|
if (c.tension > 0.8 && c.reeling) release(c);
|
||||||
|
else if (c.tension < 0.55 && !c.reeling) press(c);
|
||||||
|
update(c, 1 / 60); for (const e of c.events.splice(0)) heard.add(e.kind);
|
||||||
|
}
|
||||||
|
assert.notEqual(c.step, 'fight', 'il recupero finisce');
|
||||||
|
}
|
||||||
|
|
||||||
|
test('la forza sale finché si tiene premuto; un tocco breve non spreca il lancio; dove si tocca l\'acqua si lancia', () => {
|
||||||
|
const c = newPesca(1);
|
||||||
|
assert.ok(press(c)); update(c, 0.05); assert.ok(release(c));
|
||||||
|
assert.equal(c.casts, 0); assert.equal(c.power, 0.5);
|
||||||
|
assert.ok(aimScreen(c, [FAR, 250])); assert.equal(c.power, 1);
|
||||||
|
assert.ok(aimScreen(c, [NEAR, 250])); assert.equal(c.power, 0);
|
||||||
|
assert.ok(!aimScreen(c, [BANK - 20, 250]), 'sulla riva non si lancia');
|
||||||
|
assert.ok(!aimScreen(c, [400, 60]), 'il cielo non è acqua');
|
||||||
|
assert.ok(nudge(c, 1)); assert.ok(Math.abs(c.power - 0.05) < 1e-9);
|
||||||
|
setPower(c, 0.5);
|
||||||
|
assert.ok(press(c)); update(c, 0.7); assert.ok(release(c));
|
||||||
|
assert.equal(c.casts, 1); assert.equal(c.step, 'cast');
|
||||||
|
assert.ok(Math.abs(c.float.x - distanceFor(c.power)) < 1e-9);
|
||||||
|
assert.ok(c.events.some(e => e.kind === 'pesca-lancio'));
|
||||||
|
assert.ok(!setPower(c, 0.2), 'a lenza in acqua la forza non si tocca');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il galleggiante: i tocchi finti abbassano appena, l\'abboccata lo porta sotto; ferrare a vuoto perde il lancio', () => {
|
||||||
|
const c = newPesca(2); cast(c); run(c, FLY + 0.05);
|
||||||
|
const f = c.float;
|
||||||
|
assert.ok(f.nibbles.length >= 1 && f.nibbles.every(n => n.at < f.biteAt), 'i tocchi vengono prima');
|
||||||
|
assert.ok(strike(c)); assert.equal(c.step, 'settle'); assert.equal(c.how, 'empty');
|
||||||
|
assert.match(c.last.text, /a vuoto|spaventato/);
|
||||||
|
run(c, REST + 0.1); assert.equal(c.step, 'aim'); assert.equal(c.casts, 1);
|
||||||
|
const d = newPesca(3); cast(d); run(d, FLY + 0.05);
|
||||||
|
const g = d.float;
|
||||||
|
for (let i = 0; i < 60 * 15 && !(g.t >= g.nibbles[0].at && g.t < g.nibbles[0].at + g.nibbles[0].for); i++) update(d, 1 / 60);
|
||||||
|
assert.ok(g.dip > 0 && g.dip < 5, `un tocco finto: ${g.dip}`);
|
||||||
|
for (let i = 0; i < 60 * 15 && !g.bitten; i++) update(d, 1 / 60);
|
||||||
|
assert.equal(g.dip, 7, 'abboccata: sotto');
|
||||||
|
assert.ok(d.events.some(e => e.kind === 'pesca-abbocca'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non ferrata in tempo, l\'abboccata passa; alla terza il pesce se ne va', () => {
|
||||||
|
const c = newPesca(4, { level: 'male' }); cast(c); run(c, FLY + 0.05);
|
||||||
|
for (let i = 0; i < 60 * 15 && !c.float.bitten; i++) update(c, 1 / 60);
|
||||||
|
// Il segnale e la finestra coincidono: finché dice «Ferra!» si ferra; dopo, lo sta sputando.
|
||||||
|
assert.ok(biting(c) && c.float.dip === 7 && /Ferra!/.test(status(c)));
|
||||||
|
run(c, c.skill.window + 0.05);
|
||||||
|
assert.ok(!biting(c) && c.float.bitten && c.float.dip === 3 && /sputando/.test(status(c)));
|
||||||
|
const late = structuredClone(c); strike(late);
|
||||||
|
assert.equal(late.last.kind, 'late'); assert.match(late.last.text, /tardi/);
|
||||||
|
run(c, 0.5);
|
||||||
|
assert.ok(!c.float.bitten && c.float.missed === 1, 'persa la prima');
|
||||||
|
assert.equal(c.step, 'wait');
|
||||||
|
assert.match(status(c), /2 abboccate ancora/);
|
||||||
|
run(c, 60);
|
||||||
|
assert.equal(c.how, 'empty', 'dopo tre, o dopo quaranta secondi, si ricupera a vuoto');
|
||||||
|
assert.equal(c.last.kind, 'gone'); assert.equal(c.step, 'aim');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ferrato in tempo si combatte: tenendo premuto la lenza tira e il pesce si avvicina, mollando si allontana; troppo tesa si spezza', () => {
|
||||||
|
const c = newPesca(5, { level: 'bona' }); setPower(c, 0.5); hook(c);
|
||||||
|
assert.ok(c.fish && c.fish.dist === 1);
|
||||||
|
const x0 = c.float.x;
|
||||||
|
press(c); run(c, 1.5);
|
||||||
|
assert.ok(c.float.x < x0 && c.float.x > SHORE, `il galleggiante viene verso riva senza salti: ${x0} → ${c.float.x}`);
|
||||||
|
assert.ok(c.fish.dist < 1 && c.tension > 0.3, 'recuperando si avvicina');
|
||||||
|
const d0 = c.fish.dist;
|
||||||
|
release(c); run(c, 1.5);
|
||||||
|
assert.ok(c.fish.dist > d0 - 0.001, 'mollando non si avvicina');
|
||||||
|
land(c);
|
||||||
|
assert.equal(c.how, 'landed', 'con giudizio si tira a riva');
|
||||||
|
assert.equal(c.caught.length, 1); assert.ok(c.weight > 0);
|
||||||
|
assert.ok(heard.has('pesca-preso'));
|
||||||
|
// A fassi male, una carpa al largo tenuta sempre tesa spezza la lenza.
|
||||||
|
const d = newPesca(6, { level: 'male' }); setPower(d, 1); hook(d);
|
||||||
|
d.fish = { ...FISHES.at(-1), dist: 1, energy: 1, burst: 0, reel: 0.11 };
|
||||||
|
press(d); run(d, 8);
|
||||||
|
assert.equal(d.how, 'broken', `invece: ${d.step} ${d.how}`);
|
||||||
|
assert.ok(heard.has('pesca-rottura'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('un gesto solo: premendo all\'abboccata si ferra e si recupera; il pannello alterna recupera e molla; il sesto lancio si può finire', () => {
|
||||||
|
const c = newPesca(21, { level: 'bona' }); setPower(c, 0.3); cast(c); run(c, FLY + 0.05);
|
||||||
|
for (let i = 0; i < 60 * 15 && !c.float.bitten; i++) update(c, 1 / 60);
|
||||||
|
assert.ok(press(c)); assert.equal(c.step, 'fight'); assert.ok(c.reeling, 'ferrato e già in recupero');
|
||||||
|
assert.ok(release(c)); assert.ok(!c.reeling);
|
||||||
|
assert.ok(shoot(c)); assert.ok(c.reeling, 'dal pannello: recupera'); assert.ok(shoot(c)); assert.ok(!c.reeling, 'dal pannello: molla');
|
||||||
|
const d = newPesca(22); d.casts = CASTS - 1;
|
||||||
|
assert.ok(canCast(d)); cast(d); run(d, FLY + 0.05);
|
||||||
|
assert.ok(!canCast(d) && canThrow(d), 'al sesto lancio si può ancora ferrare');
|
||||||
|
assert.ok(!canAdjust(d), 'ma non regolare la distanza');
|
||||||
|
for (let i = 0; i < 60 * 15 && !d.float.bitten; i++) update(d, 1 / 60);
|
||||||
|
assert.ok(press(d)); assert.equal(d.step, 'fight'); assert.ok(canThrow(d), 'e recuperare');
|
||||||
|
d.fish = { ...FISHES[1], dist: 1, energy: 1, burst: 0, reel: 0.2 }; land(d);
|
||||||
|
assert.equal(d.how, 'landed');
|
||||||
|
assert.ok(d.result, 'il risultato è deciso appena il pesce è a riva, prima della posa');
|
||||||
|
run(d, REST + 0.1); assert.equal(d.step, 'result');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la lenza si spezza davvero: tenendo sempre premuto, a seria un cavedano o più grosso la rompe, una lasca no', () => {
|
||||||
|
const fight = (kind, level) => { const c = newPesca(23, { level }); setPower(c, 0.5); hook(c); c.fish = { ...FISHES.find(f => f.kind === kind), dist: 1, energy: 1, burst: 0, reel: 0.15 }; press(c); run(c, 20); return c.how; };
|
||||||
|
assert.equal(fight('lasca', 'seria'), 'landed');
|
||||||
|
assert.equal(fight('cavedano', 'seria'), 'broken');
|
||||||
|
assert.equal(fight('carpa', 'bona'), 'broken');
|
||||||
|
assert.equal(fight('cavedano', 'bona'), 'landed');
|
||||||
|
// Mollata e ripresa, la stessa carpa viene a riva.
|
||||||
|
const c = newPesca(24, { level: 'male' }); setPower(c, 1); hook(c); c.fish = { ...FISHES.at(-1), dist: 1, energy: 1, burst: 0, reel: 0.11 }; land(c);
|
||||||
|
assert.equal(c.how, 'landed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('al largo i pesci sono più grossi; lo stivale solo sotto riva e non conta per la gloria', () => {
|
||||||
|
const near = new Set(), far = new Set();
|
||||||
|
for (let seed = 1; seed < 40; seed++) {
|
||||||
|
for (const [power, set] of [[0, near], [1, far]]) {
|
||||||
|
const c = newPesca(seed); setPower(c, power); hook(c); set.add(c.fish.kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.ok(far.has('carpa') || far.has('tinca'), `al largo: ${[...far]}`);
|
||||||
|
assert.ok(!near.has('carpa'), 'sotto riva niente carpe');
|
||||||
|
assert.ok(!far.has('stivale'), 'lo stivale sta sotto riva');
|
||||||
|
const c = newPesca(7, { level: 'bona' }); c.caught = [{ ...FISHES[0] }, { ...FISHES[1] }]; c.weight = 1;
|
||||||
|
assert.match(status(c), /Pesci 1 /, 'lo stivale non è un pesce');
|
||||||
|
const d = newPesca(25); setPower(d, 0); hook(d); d.fish = { ...FISHES[0], dist: 1, energy: 1, burst: 0, reel: 0.3 }; land(d);
|
||||||
|
assert.equal(d.weight, 0, 'e non pesa'); assert.match(d.last.text, /stivale/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sei lanci, poi il risultato: la gloria dipende dal livello', () => {
|
||||||
|
for (const [level, { goal }] of Object.entries(LEVELS)) {
|
||||||
|
const c = newPesca(8, { level }), fishes = [];
|
||||||
|
for (let i = 0; i < CASTS; i++) {
|
||||||
|
assert.ok(canThrow(c));
|
||||||
|
// Finché serve si pesca per davvero (con una lasca docile sotto riva), poi si ricupera a vuoto.
|
||||||
|
if (c.caught.length < goal) { setPower(c, 0); hook(c); c.fish = { ...FISHES[1], dist: 1, energy: 1, burst: 0, reel: 0.2 }; land(c); assert.equal(c.how, 'landed'); }
|
||||||
|
else { cast(c); run(c, FLY + 0.05); strike(c); }
|
||||||
|
run(c, REST + 0.1);
|
||||||
|
}
|
||||||
|
assert.equal(c.step, 'result'); assert.ok(!canThrow(c)); assert.ok(!cast(c));
|
||||||
|
assert.equal(c.result.fish, goal); assert.equal(c.result.winner, 'player', level);
|
||||||
|
assert.match(status(c), /pesci su 6 lanci/);
|
||||||
|
}
|
||||||
|
const miss = newPesca(9, { level: 'bona' });
|
||||||
|
for (let i = 0; i < CASTS; i++) { cast(miss); run(miss, FLY + 0.05); strike(miss); run(miss, REST + 0.1); }
|
||||||
|
assert.equal(miss.result.winner, 'cpu');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nella storia: coi ragazzi in Fortezza «S\'ha andà a fallo?!?», loro accettano, si sceglie e si pesca; la gloria lascia il flag', () => {
|
||||||
|
const g = new Game(story);
|
||||||
|
g.go('fortezza', [240, 334]); settle(g);
|
||||||
|
g.act('talk', 'pescatori'); const said = settle(g).map(s => s.text);
|
||||||
|
assert.ok(said.some(t => t.includes('mia mamma aveva ragione')), 'la prima volta le canne');
|
||||||
|
assert.ok(g.dialogue, 'poi si parla');
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'S\'ha andà a fallo?!?'));
|
||||||
|
const yes = settle(g).map(s => `${s.who ?? 'andrea'}: ${s.text}`);
|
||||||
|
assert.ok(yes.includes('pescatori: Gnamo! La canna ti si dà noi.'));
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'Seria.')); settle(g);
|
||||||
|
assert.equal(g.closeup?.id, 'pesca'); assert.equal(g.closeup.level, 'seria');
|
||||||
|
g.closeup.result = { winner: 'player', fish: 3, weight: 2.1, goal: 3, caught: [] }; g.closeup.step = 'result';
|
||||||
|
g.closeupClose(); const after = settle(g).map(s => s.text);
|
||||||
|
assert.ok(g.state.flags.pesca_vinta);
|
||||||
|
assert.ok(after.some(t => t.includes('Stasera si frigge')));
|
||||||
|
// La seconda volta si va dritti al dialogo.
|
||||||
|
g.act('talk', 'pescatori'); settle(g);
|
||||||
|
assert.ok(g.dialogue);
|
||||||
|
});
|
||||||
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
|||||||
import { backgroundOrder } from '../src/engine/render.js';
|
import { backgroundOrder } from '../src/engine/render.js';
|
||||||
import { story } from '../src/story/story.js';
|
import { story } from '../src/story/story.js';
|
||||||
import { solution } from '../src/story/solution.js';
|
import { solution } from '../src/story/solution.js';
|
||||||
import { Game, VERBS, checkSave, introText, shuffledCredits, matchPlace } from '../src/engine/game.js';
|
import { Game, VERBS, checkSave, introText, shuffledCredits, matchPlace, reelPictures } from '../src/engine/game.js';
|
||||||
import { inside } from '../src/engine/geometry.js';
|
import { inside } from '../src/engine/geometry.js';
|
||||||
import { existsSync, readFileSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import { settle, exploreTorch } from './helpers.mjs';
|
import { settle, exploreTorch } from './helpers.mjs';
|
||||||
@@ -144,6 +144,20 @@ test('le fasi del giorno seguono la storia: pomeriggio, poi tramonto, poi sera,
|
|||||||
assert.deepEqual(seen, ids);
|
assert.deepEqual(seen, ids);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('il carosello dei Ringraziamenti: ogni sfondo una volta sola col nome del posto, tutti i ritratti, mai due ritratti vicini', () => {
|
||||||
|
const reel = reelPictures(story);
|
||||||
|
const backgrounds = new Set(Object.values(story.scenes).map(s => s.background).filter(Boolean));
|
||||||
|
assert.deepEqual(reel.filter(p => p.kind === 'scene').map(p => p.src).sort(), [...backgrounds].sort(), 'tutti gli sfondi, senza doppioni');
|
||||||
|
for (const p of reel) { assert.ok(p.name, `${p.src} senza nome`); assert.ok(existsSync(new URL('../' + p.src, import.meta.url)), `manca ${p.src}`); }
|
||||||
|
const faces = story.credits.flatMap(c => c.names).filter(n => n.face);
|
||||||
|
assert.deepEqual(reel.filter(p => p.kind !== 'scene').map(p => p.src).sort(), faces.map(n => n.face).sort(), 'tutti i ritratti, una volta');
|
||||||
|
assert.ok(reel.some(p => p.kind === 'palio'), 'il palio ha la sua cornice');
|
||||||
|
reel.forEach((p, i) => { if (i && p.kind !== 'scene') assert.equal(reel[i - 1].kind, 'scene', `due ritratti di fila: ${reel[i - 1].name}, ${p.name}`); });
|
||||||
|
// Poche scene: i ritratti che non trovano posto chiudono comunque la fila.
|
||||||
|
const few = reelPictures({ scenes: { a: { name: 'A', background: 'x.png' } }, credits: story.credits });
|
||||||
|
assert.equal(few.length, 1 + faces.length);
|
||||||
|
});
|
||||||
|
|
||||||
test('la ricerca della mappa: pezzi di parola, senza maiuscole, accenti e apostrofi', () => {
|
test('la ricerca della mappa: pezzi di parola, senza maiuscole, accenti e apostrofi', () => {
|
||||||
assert.ok(matchPlace('Campomigliaio', 'camp'));
|
assert.ok(matchPlace('Campomigliaio', 'camp'));
|
||||||
assert.ok(matchPlace('Via de\' Medici', 'de medici'));
|
assert.ok(matchPlace('Via de\' Medici', 'de medici'));
|
||||||
|
|||||||