Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68686efe03 | ||
|
|
208e1d9535 | ||
|
|
f2a0f22f4a | ||
|
|
dea124933e | ||
|
|
51a46fe5a0 | ||
|
|
f6fb8922af | ||
|
|
bd7fbc3003 | ||
|
|
8b957c7cf7 | ||
|
|
f1de62f01e | ||
|
|
64dc8e18e7 | ||
|
|
b262977117 | ||
|
|
3762f2b465 | ||
|
|
9f7a9812ed | ||
|
|
151017d2e2 | ||
|
|
f193c6020f |
@@ -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 |
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"image": "pescatori.png",
|
||||
"height": 110,
|
||||
"frames": {
|
||||
"work": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 170,
|
||||
"h": 110,
|
||||
"ax": 84.0,
|
||||
"ay": 110
|
||||
},
|
||||
"talk": {
|
||||
"x": 172,
|
||||
"y": 0,
|
||||
"w": 166,
|
||||
"h": 112,
|
||||
"ax": 79.2,
|
||||
"ay": 112
|
||||
},
|
||||
"laugh": {
|
||||
"x": 340,
|
||||
"y": 0,
|
||||
"w": 164,
|
||||
"h": 113,
|
||||
"ax": 79.7,
|
||||
"ay": 113
|
||||
},
|
||||
"proud": {
|
||||
"x": 506,
|
||||
"y": 0,
|
||||
"w": 166,
|
||||
"h": 121,
|
||||
"ax": 80.7,
|
||||
"ay": 121
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 399 KiB |
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 416 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 257 KiB |
@@ -203,6 +203,26 @@
|
||||
"file": "andrea-69337973d0.mp3",
|
||||
"sec": 5.94
|
||||
},
|
||||
"andrea|Quattro ragazzi che affilano canne di bambù col coltellino e ci legano le lenze.": {
|
||||
"file": "andrea-98ff3bf0a3.mp3",
|
||||
"sec": 6.41
|
||||
},
|
||||
"andrea|Oh, che fate?": {
|
||||
"file": "andrea-22b8efe94b.mp3",
|
||||
"sec": 1.75
|
||||
},
|
||||
"pescatori|Si preparano le canne: si va a pescare al lago dell'Anatre, dietro Cardetole!": {
|
||||
"file": "pescatori-e7edfae41f.mp3",
|
||||
"sec": 3.51
|
||||
},
|
||||
"pescatori|Guarda questa: la lenza la regge anche una carpa di dieci chili.": {
|
||||
"file": "pescatori-afda722e0c.mp3",
|
||||
"sec": 3.2
|
||||
},
|
||||
"andrea|(Vedi, mia mamma aveva ragione: in Fortezza si viene a farsi le canne.)": {
|
||||
"file": "andrea-e0c6360b96.mp3",
|
||||
"sec": 4.35
|
||||
},
|
||||
"andrea|Nove bastioni, li volle Cosimo I. Per tenere fuori i nemici, dicono. Secondo me per tenere dentro il Regolo.": {
|
||||
"file": "andrea-d31f6e291b.mp3",
|
||||
"sec": 8.71
|
||||
@@ -507,6 +527,62 @@
|
||||
"file": "andrea-4d6871c326.mp3",
|
||||
"sec": 4.11
|
||||
},
|
||||
"andrea|Il Rattattù: la gara di canto del paese. Si sale sul palco e ci si sfida tutti, chi stona e chi no.": {
|
||||
"file": "andrea-a9903d3ae0.mp3",
|
||||
"sec": 8.33
|
||||
},
|
||||
"andrea|Quest'anno mi ci iscrivo. Forse.": {
|
||||
"file": "andrea-146000c293.mp3",
|
||||
"sec": 2.98
|
||||
},
|
||||
"andrea|Il Cinema Taiuti. Una sala, un proiettore e le poltroncine che cigolano a tempo.": {
|
||||
"file": "andrea-3b310a5164.mp3",
|
||||
"sec": 6.39
|
||||
},
|
||||
"andrea|A quest'ora è chiuso. Apre la sera, per il film o per il Rattattù.": {
|
||||
"file": "andrea-5e37f70197.mp3",
|
||||
"sec": 5.35
|
||||
},
|
||||
"andrea|Il campo da basket del Circolo, lassù oltre la rete. Due tiri ce li faccio sempre volentieri.": {
|
||||
"file": "andrea-3c4ee692ad.mp3",
|
||||
"sec": 7.55
|
||||
},
|
||||
"andrea|La fermata della corriera. Da qui si va dappertutto. Se passa.": {
|
||||
"file": "andrea-ccd735c926.mp3",
|
||||
"sec": 5.72
|
||||
},
|
||||
"andrea|Il ponticino delle Mozzète. Sotto ci passa un fossino che d'estate fa più zanzare che acqua.": {
|
||||
"file": "andrea-98dc291bd3.mp3",
|
||||
"sec": 7.21
|
||||
},
|
||||
"andrea|Terra appena arata. Profuma di campagna e di trattore.": {
|
||||
"file": "andrea-a07bf26ea7.mp3",
|
||||
"sec": 4.41
|
||||
},
|
||||
"andrea|La falegnameria. L'odore di segatura arriva fin qui.": {
|
||||
"file": "andrea-1ccd8e2127.mp3",
|
||||
"sec": 3.84
|
||||
},
|
||||
"andrea|Bussare a una falegnameria? Mi danno una scopa e mi fanno spazzare.": {
|
||||
"file": "andrea-09ec24ef60.mp3",
|
||||
"sec": 4.63
|
||||
},
|
||||
"andrea|Un fienile pieno di rotoballe. Fossi una mucca, sarebbe il paradiso.": {
|
||||
"file": "andrea-5dfec13174.mp3",
|
||||
"sec": 4.81
|
||||
},
|
||||
"andrea|La campagna verso Gabbiano. Da quassù si vede mezzo Mugello.": {
|
||||
"file": "andrea-06e1b17635.mp3",
|
||||
"sec": 5.31
|
||||
},
|
||||
"andrea|Il palio, però, no.": {
|
||||
"file": "andrea-0e9c7a48de.mp3",
|
||||
"sec": 1.6
|
||||
},
|
||||
"andrea|Un casolare vecchio, coi mattoni che spuntano dall'intonaco. Come le mie ginocchia.": {
|
||||
"file": "andrea-06c5f35df7.mp3",
|
||||
"sec": 5.66
|
||||
},
|
||||
"andrea|L'ingresso del campeggio. A giudicare dalle bandiere, ci dorme mezzo mondo.": {
|
||||
"file": "andrea-72dede01c0.mp3",
|
||||
"sec": 5.13
|
||||
@@ -627,6 +703,42 @@
|
||||
"file": "andrea-e24cb31cbf.mp3",
|
||||
"sec": 2.62
|
||||
},
|
||||
"andrea|Cartelli per la Pieve, per la stazione, per Firenze. Per il palio, nessuno.": {
|
||||
"file": "andrea-60f39f0dd7.mp3",
|
||||
"sec": 6.48
|
||||
},
|
||||
"andrea|Cistola. Il talento di via del Pignone.": {
|
||||
"file": "andrea-2b47355c2c.mp3",
|
||||
"sec": 3.81
|
||||
},
|
||||
"andrea|Col dieci, come i grandi. Se c'è un muro che se lo merita, è questo.": {
|
||||
"file": "andrea-1bd111f29d.mp3",
|
||||
"sec": 5.59
|
||||
},
|
||||
"andrea|Ciao Cistola! ...Almeno dipinto stai fermo ad ascoltarmi.": {
|
||||
"file": "andrea-0e558e54fe.mp3",
|
||||
"sec": 4.18
|
||||
},
|
||||
"andrea|Il muro di Villa Schifanoia, la villa dei Medici, mezzo coperto dall'edera.": {
|
||||
"file": "andrea-6a873beff6.mp3",
|
||||
"sec": 5.07
|
||||
},
|
||||
"andrea|Lassù una volta c'era una torre, per tenere d'occhio chi passava sulla strada per Bologna. Ne ha viste più di me.": {
|
||||
"file": "andrea-d9ba9bd3ad.mp3",
|
||||
"sec": 7.34
|
||||
},
|
||||
"andrea|E la piazzetta si chiama come i Cambray-Digny, gli ultimi signori della villa.": {
|
||||
"file": "andrea-9d3ec66f60.mp3",
|
||||
"sec": 4.64
|
||||
},
|
||||
"andrea|Una casetta piccina, appoggiata al muro della villa come un gatto al sole.": {
|
||||
"file": "andrea-3cef067df6.mp3",
|
||||
"sec": 4.1
|
||||
},
|
||||
"andrea|Chiusa. E poi non è casa mia.": {
|
||||
"file": "andrea-7fd73fbb77.mp3",
|
||||
"sec": 2.66
|
||||
},
|
||||
"andrea|L'area feste. D'estate ci montano la pista da ballo, le tavolate e lo stand della pasta.": {
|
||||
"file": "andrea-a942b2723e.mp3",
|
||||
"sec": 6.77
|
||||
@@ -1923,6 +2035,74 @@
|
||||
"file": "arcu-406cd02197.mp3",
|
||||
"sec": 4.46
|
||||
},
|
||||
"andrea|S'ha andà a fallo?!?": {
|
||||
"file": "andrea-1c668beaef.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|S'ha andà! La canna te la diamo noi.": {
|
||||
"file": "pescatori-519ac990d8.mp3",
|
||||
"sec": 3.16
|
||||
},
|
||||
"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?": {
|
||||
"file": "andrea-83b1337b4d.mp3",
|
||||
"sec": 3.31
|
||||
@@ -2167,6 +2347,38 @@
|
||||
"file": "mariano-39e27158b7.mp3",
|
||||
"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.": {
|
||||
"file": "ing_tozzi-51f855478a.mp3",
|
||||
"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,
|
||||
poi gli occhi. Due immagini di Codex.
|
||||
- *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
|
||||
`bocce`), con comandi per dito, mouse e tastiera, i suoi test e una battuta per chi vince e per chi perde.
|
||||
Il posto naturale sono le gare del Palio del secondo capitolo:
|
||||
- *Passamano dei mattoni* (da fare per primo): un gioco di ritmo, i mattoni arrivano lungo la fila e si passano al
|
||||
momento giusto; sbagliando il tempo ne cade uno. Facile col dito, e chiude il cerchio col finale (i mattoni
|
||||
portati in Fortezza).
|
||||
- **Altri minigiochi** (25 ottobre 2026, rivisti il 28 ottobre con le proposte di Claude che ad Andrea piacciono):
|
||||
ognuno è un primo piano (`closeup` con il suo `type`), con comandi per dito, mouse e tastiera, i suoi test, una
|
||||
battuta per chi vince e per chi perde, e a fine lavoro la revisione critica di Codex se Andrea la concede.
|
||||
Già fatti: briscola, tressette e scopa all'oratorio; le bocce al pallaio; il biliardo coi birilli al Circolo
|
||||
4 Maggio; «Fai due tiri» al campo da basket del Circolo (27 ottobre). I prossimi due, se Andrea è d'accordo:
|
||||
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ù
|
||||
spettacolare, la più lunga da disegnare.
|
||||
- *Bandierine (lavanderine)*: correre a prendere la bandiera prima dell'avversario, di riflessi.
|
||||
spettacolare, la più lunga da disegnare: il «gioco grosso», con Mitrugno (consenso).
|
||||
- *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.
|
||||
Fuori dal Palio:
|
||||
- *Briscola al circolino* con l'Arcu e il Barone (la gag del «Male male si divide»): una mano corta.
|
||||
- *Rigori all'A. Bettini*: mira e forza come alle bocce, contro un portiere. Andrea ha ritrovato la foto di squadra
|
||||
sul campo in terra (porta, rete, la torre della Pieve dietro, anni '90): lo sfondo del minigioco lo fa Codex da quella,
|
||||
senza le persone (per i volti serve il consenso). Da salvare in `assets/raw/foto/campo-bettini-squadra.jpg`.
|
||||
Al bar e al circolo:
|
||||
- *Calcio balilla al River*: due stecche sole (difesa e attacco), vista dall'alto come il biliardo, che ha già
|
||||
la fisica delle sponde. Il gioco da bar più sampierino che c'è; il River «coi flipper e le figurine» lo cita già.
|
||||
- *Flipper del River*: più lavoro, ma la fisica delle biglie c'è già.
|
||||
- *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.
|
||||
|
||||
@@ -27,6 +27,8 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p
|
||||
| `bosco-ai-frati.png` | Codex | `bosco-ai-frati.png` | [DA ANDREA] sembra una foto da Wikipedia/Wikimedia: verificare licenza | ? |
|
||||
| `campeggio.png` | Codex | `campeggio-ingresso.png` | [DA ANDREA] probabilmente dal sito del campeggio | ? |
|
||||
| `polisportiva-*.png`, `ponte-sieve.png`, `area-feste.png`, `via-provinciale.png`, `comune-asilo.png`, `giardini-poste.png`, `stazione.png` | Codex | foto omonime in `assets/raw/foto/` | Andrea (EXIF, 23 ottobre 2026) | sì |
|
||||
| `via-del-pignone.png` | Codex | `via-del-pignone.png` (murale: `riferimenti/cistola.png`, `murale-maradona.png`) | Andrea | sì (Cistola consenziente) |
|
||||
| `provinciale-pignone.png`, `piazzetta-cambray-digny.png`, `via-cipriani.png`, `via-massorondinaio.png`, `mozzete.png`, `ponticino-mozzete.png`, `coldaia.png`, `falegnameria-coldaia.png`, `gabbiano.png` | Codex | screenshot di Google Street View | Google | **no**: da rifare con foto di Andrea |
|
||||
| `cardetole.png` | Codex | `cardetole.jpg` | Andrea (EXIF) | **da decidere**: è la casa vera di Andrea, pubblicarla dice dove abita |
|
||||
| `pieve-interno.png` | Codex | `pieve-interno.png` | Mariano Mirannalti (mandata ad Andrea su WhatsApp) | sì, se Mariano è d'accordo; citarlo nei crediti |
|
||||
| `panorama-intro.png` | Codex | `altre/panorama-intro.png` | da internet (screenshot con l'icona di Google Lens) | **no**, finché non si sostituisce |
|
||||
|
||||
@@ -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
|
||||
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:
|
||||
|
||||
|
||||
@@ -199,6 +199,36 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso
|
||||
sfondi da foto prese da internet (`docs/IMMAGINI.md`), Cardetole (dice dove abita Andrea).
|
||||
- Vecchietta e vecchietto sono in gioco sulla panchina della Piazza (`assets/characters/`, da
|
||||
`assets/raw/vecchiett?-sheet.png`): consenso delle persone ritratte confermato da Andrea il 2026-10-23; mancano i loro nomi.
|
||||
- Via Provinciale (dalla rotonda verso la Pieve) → a destra Via del Pignone → in cima Piazzetta Cambray Digny, che porta
|
||||
in Piazza (a sinistra, dove prima c'era l'uscita per il Comune) e in Via de' Medici. Battute provvisorie sul murale.
|
||||
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
|
||||
(volto da `assets/raw/riferimenti/cistola.png`). Consenso di Davide confermato da Andrea il 2026-10-27.
|
||||
- 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). Al campo da basket del Circolo, «Fai due tiri»
|
||||
(`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;
|
||||
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,
|
||||
`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`).
|
||||
- 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>
|
||||
<p class="dedication credits-dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p>
|
||||
<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>
|
||||
<div class="buttons"><button id="close-credits">Chiudi</button></div>
|
||||
<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.
|
||||
Vince chi fa più punti nella mano.</p>
|
||||
</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>
|
||||
<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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "leggenda-del-regolo",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.23",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"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 * as bocce from './bocce.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 tressette from './tressette.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 },
|
||||
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 },
|
||||
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'];
|
||||
@@ -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
|
||||
// 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();
|
||||
|
||||
@@ -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 { drawBocce } from './bocce-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 { drawTressette } from './tressette-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);
|
||||
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 (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||
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-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 }); }
|
||||
// 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 === 'tavolo-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).
|
||||
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 { render, loadAssets, hasBackground, WIDTH, HEIGHT } from './engine/render.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 * as bocce from './engine/bocce.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 { cardAt } from './engine/briscola-render.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 });
|
||||
// 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 });
|
||||
b.addEventListener('click', () => showFace(n));
|
||||
b.addEventListener('click', () => showFace(facePicture(n)));
|
||||
return b;
|
||||
}));
|
||||
return [dt, dd];
|
||||
@@ -133,22 +135,64 @@ function openCredits() {
|
||||
fillReel();
|
||||
$('credits').hidden = false;
|
||||
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
|
||||
// vedere mai la cucitura.
|
||||
// Sotto i nomi, il carosello: tutti gli sfondi del paese e i ritratti (game.reelPictures), due giri uguali in fila per
|
||||
// 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() {
|
||||
const track = $('credits-reel').querySelector('.track');
|
||||
if (track.children.length) return;
|
||||
const pictures = [...new Set(Object.values(story.scenes).map(s => s.background).filter(Boolean))];
|
||||
const faces = (story.credits ?? []).flatMap(c => c.names).filter(n => n.face).map(n => n.face);
|
||||
// I ritratti sparsi fra gli sfondi, uno ogni quattro.
|
||||
const all = pictures.flatMap((p, i) => i % 4 === 3 && faces.length ? [p, faces[(i >> 2) % faces.length]] : [p]);
|
||||
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);
|
||||
if (reelTrack.children.length) return;
|
||||
const all = reelPictures(story);
|
||||
for (const p of [...all, ...all]) {
|
||||
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 });
|
||||
reelPics.set(img, p);
|
||||
reelTrack.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.
|
||||
let rolling = 0, rollWait = 0;
|
||||
$('credits-list').addEventListener('pointerdown', () => { rollWait = 4000; });
|
||||
@@ -170,9 +214,10 @@ function rollCredits() {
|
||||
};
|
||||
rolling = requestAnimationFrame(step);
|
||||
}
|
||||
function showFace({ name, face }) {
|
||||
$('face').querySelector('img').src = versioned(face);
|
||||
$('face').querySelector('img').classList.toggle('palio', face.endsWith('palio.png'));
|
||||
function showFace({ name, src, kind }) {
|
||||
const img = $('face').querySelector('img');
|
||||
img.src = versioned(src);
|
||||
img.className = kind;
|
||||
$('face').querySelector('figcaption').textContent = name;
|
||||
$('face').hidden = false;
|
||||
}
|
||||
@@ -453,11 +498,11 @@ canvas.addEventListener('pointerdown', e => {
|
||||
return;
|
||||
}
|
||||
const aimed = game.closeupAim(scenePoint(e));
|
||||
if (story.closeups[game.closeup.id].type === 'biliardo') {
|
||||
if (!aimed && !game.speech) return;
|
||||
if (['biliardo', 'basket'].includes(story.closeups[game.closeup.id].type)) {
|
||||
if (!aimed && !game.speech) return; // dove non si può mirare (dietro Andrea) non si carica nemmeno
|
||||
if (aimed) game.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 };
|
||||
// Alle bocce si tiene premuto per caricare la forza (la battuta in corso, un tocco la salta).
|
||||
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,
|
||||
// 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.)
|
||||
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)); }
|
||||
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.' },
|
||||
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.' },
|
||||
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 mini = () => MINI[story.closeups[game.closeup.id].type];
|
||||
@@ -631,7 +678,11 @@ window.addEventListener('keydown', e => {
|
||||
}
|
||||
if (debugKey(e)) 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 (!$('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; }
|
||||
@@ -857,17 +908,20 @@ function updateUi() {
|
||||
}
|
||||
}
|
||||
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;
|
||||
const line = mg.status(c, def.names);
|
||||
if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line;
|
||||
$('bocce-power').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;
|
||||
$('bocce-throw').textContent = c.step === 'result' ? 'Finita' : c.step === 'rolling' ? 'In movimento…' : c.current === 'cpu' ? `Tira ${def.names.cpu}…` : def.type === 'bocce' ? 'Lancia' : 'Tira';
|
||||
for (const id of ['bocce-power', 'bocce-left', 'bocce-right']) $(id).disabled = !(mg.canAdjust?.(c) ?? mg.canThrow(c)) || c.holding || !!game.speech;
|
||||
$('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';
|
||||
$('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 });
|
||||
// Il pulsante della mappa c'è solo in partita, fuori dai dialoghi, e quando c'è almeno un altro posto dove andare.
|
||||
|
||||
@@ -68,4 +68,62 @@ export const ambience = {
|
||||
viale_cipressi: [{ type: 'cloudshadow', phase: day, area: [95, 225, 435, 135], offset: 11000,
|
||||
clip: [[300, 225], [337, 225], [530, 360], [95, 360]] }],
|
||||
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 },
|
||||
],
|
||||
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' }],
|
||||
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]
|
||||
biliardo: {
|
||||
type: 'biliardo',
|
||||
@@ -215,6 +249,11 @@ export const story = {
|
||||
// [DA ANDREA: chi è] Figura inventata, nessuna persona reale.
|
||||
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 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.
|
||||
pescatori: { name: 'I ragazzi delle canne', color: '#b8f08a', sprite: 'assets/characters/pescatori.json' },
|
||||
vecchietta: { name: 'Vecchietta', color: '#ffd27a', sprite: 'assets/characters/vecchietta.json' },
|
||||
vecchietto: { name: 'Vecchietto', color: '#9fe3ff', sprite: 'assets/characters/vecchietto.json' },
|
||||
castello: { name: 'Lo spirito del Trebbio', color: '#c8f0ff' },
|
||||
@@ -477,13 +516,10 @@ export const story = {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'verso_campeggio', name: 'strada verso il campeggio', box: [330, 190, 34, 48], at: [345, 234], exit: { scene: 'campeggio', at: [200, 345], face: 'right' },
|
||||
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_villa_adami', name: 'Via de\' Medici, verso Villa Adami', box: [0, 196, 90, 50], at: [92, 250], exit: { scene: 'via_de_medici', at: [300, 348], face: 'up' },
|
||||
},
|
||||
{
|
||||
id: 'verso_comune', name: 'a sinistra, verso il Comune', box: [96, 196, 50, 54], at: [120, 252], exit: { scene: 'comune', at: [20, 300], face: 'right' },
|
||||
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_calimara', name: 'Via Calimara, verso la farmacia', box: [180, 334, 380, 26], at: [360, 354], exit: { scene: 'via_calimara', at: [365, 212], face: 'down' },
|
||||
@@ -504,7 +540,23 @@ export const story = {
|
||||
walk: [[110, 284], [300, 280], [330, 272], [348, 274], [400, 292], [442, 294], [640, 352], [640, 360], [0, 360], [0, 315], [60, 310], [105, 295]],
|
||||
depth: [[272, 0.55], [360, 1.15]],
|
||||
entry: [175, 352],
|
||||
speakers: { pescatori: [150, 225] },
|
||||
// Quattro ragazzi sull'erba si fanno le canne da pesca col bambù.
|
||||
people: [{ who: 'pescatori', at: [150, 322], idle: 'work', talk: ['work', 'talk'], blink: 'laugh', every: 6, size: 1.25 }],
|
||||
hotspots: [
|
||||
{
|
||||
id: 'pescatori', name: 'ragazzi con le canne', box: [92, 236, 120, 86], at: [240, 334], face: 'left', default: 'talk',
|
||||
actions: {
|
||||
look: [{ say: 'Quattro ragazzi che affilano canne di bambù col coltellino e ci legano le lenze.' }],
|
||||
talk: [{ if: '!pescatori_parlato', then: [
|
||||
{ set: 'pescatori_parlato' },
|
||||
{ say: 'Oh, che fate?' },
|
||||
{ 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: '(Vedi, mia mamma aveva ragione: in Fortezza si viene a farsi le canne.)' },
|
||||
] }, { dialogue: 'pescatori' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mura', name: 'bastione', box: [420, 0, 220, 290],
|
||||
actions: {
|
||||
@@ -658,7 +710,7 @@ export const story = {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'verso_provinciale', name: 'Via Provinciale, verso il River', box: [0, 270, 46, 70], at: [14, 312], face: 'left', exit: { scene: 'via_provinciale', at: [560, 240], face: 'left' },
|
||||
id: 'verso_provinciale', name: 'Via Provinciale, verso il River', box: [0, 270, 46, 70], at: [14, 312], face: 'left', exit: { scene: 'provinciale_pignone', at: [268, 216], face: 'down' },
|
||||
},
|
||||
{
|
||||
id: 'verso_forno', name: 'a destra, verso il forno', box: [570, 176, 70, 76], at: [588, 250], exit: { scene: 'forno', at: [628, 300], face: 'left' },
|
||||
@@ -835,7 +887,7 @@ export const story = {
|
||||
},
|
||||
],
|
||||
},
|
||||
// Via de' Medici: dalla Piazza sale verso Villa Adami (la Piazza è alle spalle, in fondo si va su).
|
||||
// Via de' Medici: dalla Piazzetta Cambray Digny sale verso Villa Adami (la Piazzetta è alle spalle, in fondo si va su).
|
||||
via_de_medici: {
|
||||
name: 'Via de\' Medici',
|
||||
map: [22, 32],
|
||||
@@ -855,11 +907,192 @@ export const story = {
|
||||
id: 'su_villa_adami', name: 'su verso Villa Adami', box: [272, 186, 60, 34], at: [300, 216], exit: { scene: 'villa_adami', at: [50, 252], face: 'right' },
|
||||
},
|
||||
{
|
||||
id: 'giu_piazza', name: 'giù verso la Piazza', box: [0, 334, 560, 26], at: [300, 354], exit: { scene: 'piazza', at: [92, 250], face: 'right' },
|
||||
id: 'giu_piazzetta', name: 'giù verso Piazzetta Cambray Digny', box: [0, 334, 545, 26], at: [300, 354], exit: { scene: 'piazzetta', at: [20, 320], face: 'right' },
|
||||
},
|
||||
],
|
||||
},
|
||||
// L'ingresso del campeggio: dalla Piazza a diritto; poco dopo, a destra, si sale al Bosco ai Frati.
|
||||
// Via Pietro Cipriani, fra Via Massorondinaio (alle spalle, verso il campeggio) e la Piazza (in fondo, col Circolo 4 Maggio arancione).
|
||||
// A destra il Cinema Taiuti, aperto come nel 2007, col cartellone del Rattattù; lassù il campo da basket del Circolo.
|
||||
via_cipriani: {
|
||||
name: 'Via Pietro Cipriani',
|
||||
map: [22, 64],
|
||||
background: 'assets/scenes/via-cipriani.png',
|
||||
walk: [[40, 360], [40, 320], [60, 280], [100, 272], [150, 276], [300, 300], [360, 318], [640, 345], [640, 360]],
|
||||
depth: [[272, 0.45], [360, 1.15]],
|
||||
entry: [300, 345],
|
||||
hotspots: [
|
||||
{
|
||||
id: 'rattattu', name: 'cartellone del Rattattù', box: [460, 152, 58, 110], at: [480, 336], face: 'up',
|
||||
actions: {
|
||||
look: [{ say: 'Il Rattattù: la gara di canto del paese. Si sale sul palco e ci si sfida tutti, chi stona e chi no.' }, { say: 'Quest\'anno mi ci iscrivo. Forse.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cinema', name: 'Cinema Taiuti', box: [420, 40, 220, 220], at: [520, 340], face: 'up',
|
||||
actions: {
|
||||
look: [{ say: 'Il Cinema Taiuti. Una sala, un proiettore e le poltroncine che cigolano a tempo.' }],
|
||||
open: [{ say: 'A quest\'ora è chiuso. Apre la sera, per il film o per il Rattattù.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'campo_basket', name: 'campo da basket del Circolo', box: [250, 110, 170, 80], at: [340, 316], face: 'up', default: 'use',
|
||||
actions: {
|
||||
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' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'alla_piazza', name: 'in fondo, la Piazza', box: [80, 230, 120, 50], at: [120, 280], exit: { scene: 'piazza', at: [345, 240], face: 'down' },
|
||||
},
|
||||
{
|
||||
id: 'al_campeggio', name: 'indietro, verso il campeggio', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'via_massorondinaio', at: [30, 300], face: 'right' },
|
||||
},
|
||||
],
|
||||
},
|
||||
// Via Massorondinaio: a sinistra si riprende Via Cipriani (verso la Piazza), a diritto si va al campeggio.
|
||||
via_massorondinaio: {
|
||||
name: 'Via Massorondinaio',
|
||||
map: [17, 74],
|
||||
background: 'assets/scenes/via-massorondinaio.png',
|
||||
walk: [[0, 360], [0, 266], [200, 258], [400, 240], [520, 226], [566, 226], [552, 250], [545, 290], [600, 360]],
|
||||
depth: [[226, 0.4], [360, 1.2]],
|
||||
entry: [300, 330],
|
||||
hotspots: [
|
||||
{
|
||||
id: 'a_via_cipriani', name: 'a sinistra, Via Cipriani', box: [0, 262, 40, 80], at: [16, 300], face: 'left', exit: { scene: 'via_cipriani', at: [300, 345], face: 'up' },
|
||||
},
|
||||
{
|
||||
id: 'al_campeggio', name: 'a diritto, verso il campeggio', box: [496, 196, 74, 34], at: [535, 232], exit: { scene: 'campeggio', at: [200, 345], face: 'right' },
|
||||
},
|
||||
],
|
||||
},
|
||||
// Le Mozzète (Via Donatello): in fondo il campo secondario, in basso a destra si scende al ponticino.
|
||||
mozzete: {
|
||||
name: 'Le Mozzète',
|
||||
map: [72, 92],
|
||||
background: 'assets/scenes/mozzete.png',
|
||||
walk: [[0, 360], [0, 280], [40, 232], [120, 212], [300, 200], [335, 198], [350, 210], [420, 270], [640, 330], [640, 360]],
|
||||
depth: [[198, 0.3], [360, 1.2]],
|
||||
entry: [320, 330],
|
||||
hotspots: [
|
||||
{
|
||||
id: 'fermata', name: 'fermata della corriera', box: [450, 170, 110, 90], at: [470, 300], face: 'right',
|
||||
actions: {
|
||||
look: [{ say: 'La fermata della corriera. Da qui si va dappertutto. Se passa.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'al_secondario', name: 'in fondo, il campo secondario', box: [300, 176, 60, 30], at: [330, 206], exit: { scene: 'polisportiva', at: [628, 290], face: 'left' },
|
||||
},
|
||||
{
|
||||
id: 'al_ponticino', name: 'giù a destra, il ponticino delle Mozzète', box: [580, 316, 60, 44], at: [612, 345], face: 'right', exit: { scene: 'ponticino_mozzete', at: [300, 196], face: 'down' },
|
||||
},
|
||||
],
|
||||
},
|
||||
// Il ponticino delle Mozzète: lassù la congiunzione con le Mozzète, indietro si va verso Coldaia.
|
||||
ponticino_mozzete: {
|
||||
name: 'Il ponticino delle Mozzète',
|
||||
map: [78, 88],
|
||||
background: 'assets/scenes/ponticino-mozzete.png',
|
||||
walk: [[0, 360], [0, 300], [200, 270], [280, 190], [320, 186], [380, 230], [520, 290], [640, 310], [640, 360]],
|
||||
depth: [[186, 0.3], [360, 1.2]],
|
||||
entry: [300, 340],
|
||||
hotspots: [
|
||||
{
|
||||
id: 'parapetto', name: 'parapetto del ponticino', box: [460, 230, 120, 50], at: [470, 300], face: 'right',
|
||||
actions: {
|
||||
look: [{ say: 'Il ponticino delle Mozzète. Sotto ci passa un fossino che d\'estate fa più zanzare che acqua.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'alle_mozzete', name: 'lassù, le Mozzète', box: [270, 166, 60, 30], at: [300, 196], exit: { scene: 'mozzete', at: [612, 345], face: 'left' },
|
||||
},
|
||||
{
|
||||
id: 'a_coldaia', name: 'indietro, verso Coldaia', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'coldaia', at: [450, 345], face: 'up' },
|
||||
},
|
||||
],
|
||||
},
|
||||
// Coldaia: dietro il ponticino delle Mozzète; avanti la falegnameria e poi Gabbiano.
|
||||
coldaia: {
|
||||
name: 'Coldaia',
|
||||
map: [84, 89],
|
||||
background: 'assets/scenes/coldaia.png',
|
||||
walk: [[230, 360], [300, 250], [318, 200], [290, 166], [330, 162], [460, 205], [640, 268], [640, 360]],
|
||||
depth: [[162, 0.3], [360, 1.2]],
|
||||
entry: [450, 345],
|
||||
hotspots: [
|
||||
{
|
||||
id: 'campo_arato', name: 'campo arato', box: [0, 160, 220, 130], at: [280, 330], face: 'left',
|
||||
actions: {
|
||||
look: [{ say: 'Terra appena arata. Profuma di campagna e di trattore.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'alla_falegnameria', name: 'avanti, verso la falegnameria', box: [280, 140, 60, 34], at: [305, 168], exit: { scene: 'falegnameria_coldaia', at: [300, 340], face: 'up' },
|
||||
},
|
||||
{
|
||||
id: 'al_ponticino', name: 'indietro, il ponticino delle Mozzète', box: [220, 334, 420, 26], at: [450, 354], exit: { scene: 'ponticino_mozzete', at: [300, 340], face: 'up' },
|
||||
},
|
||||
],
|
||||
},
|
||||
// La falegnameria di Coldaia, col fienile delle rotoballe; a diritto si va a Gabbiano.
|
||||
falegnameria_coldaia: {
|
||||
name: 'La falegnameria',
|
||||
map: [90, 91],
|
||||
background: 'assets/scenes/falegnameria-coldaia.png',
|
||||
walk: [[0, 360], [0, 300], [100, 260], [160, 220], [185, 220], [230, 250], [640, 250], [640, 360]],
|
||||
depth: [[220, 0.35], [360, 1.2]],
|
||||
entry: [300, 340],
|
||||
hotspots: [
|
||||
{
|
||||
id: 'falegnameria', name: 'falegnameria', box: [160, 176, 120, 44], at: [220, 260], face: 'up',
|
||||
actions: {
|
||||
look: [{ say: 'La falegnameria. L\'odore di segatura arriva fin qui.' }],
|
||||
open: [{ say: 'Bussare a una falegnameria? Mi danno una scopa e mi fanno spazzare.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'fienile', name: 'fienile', box: [290, 90, 320, 140], at: [440, 280], face: 'up',
|
||||
actions: {
|
||||
look: [{ say: 'Un fienile pieno di rotoballe. Fossi una mucca, sarebbe il paradiso.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'a_gabbiano', name: 'a diritto, verso Gabbiano', box: [140, 190, 64, 32], at: [172, 222], exit: { scene: 'gabbiano', at: [300, 340], face: 'up' },
|
||||
},
|
||||
{
|
||||
id: 'a_coldaia', name: 'indietro, verso Coldaia', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'coldaia', at: [305, 170], face: 'down' },
|
||||
},
|
||||
],
|
||||
},
|
||||
// La strada di Gabbiano, in mezzo alla campagna del Mugello.
|
||||
gabbiano: {
|
||||
name: 'Gabbiano',
|
||||
map: [95, 93],
|
||||
background: 'assets/scenes/gabbiano.png',
|
||||
walk: [[0, 360], [0, 300], [200, 260], [400, 215], [430, 186], [445, 186], [520, 240], [640, 280], [640, 360]],
|
||||
depth: [[186, 0.3], [360, 1.2]],
|
||||
entry: [300, 340],
|
||||
hotspots: [
|
||||
{
|
||||
id: 'valle', name: 'campagna', box: [150, 140, 260, 60], at: [330, 300], face: 'up',
|
||||
actions: {
|
||||
look: [{ say: 'La campagna verso Gabbiano. Da quassù si vede mezzo Mugello.' }, { say: 'Il palio, però, no.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'casolare', name: 'casolare', box: [0, 30, 140, 220], at: [150, 320], face: 'left',
|
||||
actions: {
|
||||
look: [{ say: 'Un casolare vecchio, coi mattoni che spuntano dall\'intonaco. Come le mie ginocchia.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'alla_falegnameria', name: 'indietro, verso la falegnameria', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'falegnameria_coldaia', at: [172, 224], face: 'down' },
|
||||
},
|
||||
],
|
||||
},
|
||||
// L'ingresso del campeggio: dalla Piazza per Via Cipriani e Via Massorondinaio; poco dopo, a destra, si sale al Bosco ai Frati.
|
||||
campeggio: {
|
||||
name: 'Ingresso del campeggio',
|
||||
map: [12, 84],
|
||||
@@ -887,7 +1120,7 @@ export const story = {
|
||||
id: 'bosco_ai_frati', name: 'salita verso il Bosco ai Frati', box: [570, 250, 70, 110], at: [628, 320], exit: { scene: 'bosco_ai_frati', at: [320, 345], face: 'right' },
|
||||
},
|
||||
{
|
||||
id: 'verso_piazza', name: 'torna in Piazza', box: [150, 290, 100, 70], at: [175, 345], exit: { scene: 'piazza', at: [345, 240], face: 'left' },
|
||||
id: 'verso_piazza', name: 'Via Massorondinaio, verso la Piazza', box: [150, 290, 100, 70], at: [175, 345], exit: { scene: 'via_massorondinaio', at: [530, 236], face: 'down' },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -939,8 +1172,8 @@ export const story = {
|
||||
},
|
||||
],
|
||||
},
|
||||
// Il Comune e l'asilo infantile Antonio Frilli: dalla Piazza (a sinistra, prima di Via de' Medici) o dalla
|
||||
// strada di fronte alla farmacia.
|
||||
// Il Comune e l'asilo infantile Antonio Frilli: dalla strada di fronte alla farmacia o da Via Calimara
|
||||
// (a sinistra in Piazza, dove una volta si veniva qui, c'è la Piazzetta Cambray Digny: Andrea, 27 ottobre).
|
||||
comune: {
|
||||
name: 'Il Comune',
|
||||
map: [11, 45],
|
||||
@@ -969,9 +1202,6 @@ export const story = {
|
||||
open: [{ say: 'All\'asilo? Ho già dato, grazie.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'alla_piazza', name: 'su, verso la Piazza', box: [0, 250, 40, 110], at: [14, 300], exit: { scene: 'piazza', at: [120, 252], 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' },
|
||||
},
|
||||
@@ -1069,7 +1299,7 @@ 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: 'alla_pieve', name: 'dietro la farmacia, verso la Pieve', box: [470, 180, 120, 60], at: [560, 240], exit: { scene: 'pieve', at: [24, 312], face: 'right' },
|
||||
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: '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' },
|
||||
@@ -1082,6 +1312,90 @@ export const story = {
|
||||
},
|
||||
],
|
||||
},
|
||||
// Via Provinciale fra la rotonda (alle spalle) e la Pieve (in fondo); a destra sale Via del Pignone.
|
||||
provinciale_pignone: {
|
||||
name: 'Via Provinciale',
|
||||
map: [50, 64],
|
||||
background: 'assets/scenes/provinciale-pignone.png',
|
||||
walk: [[30, 360], [30, 330], [130, 270], [245, 212], [290, 212], [340, 228], [540, 232], [540, 258], [640, 300], [640, 360]],
|
||||
depth: [[212, 0.32], [360, 1.2]],
|
||||
entry: [300, 345],
|
||||
hotspots: [
|
||||
{
|
||||
id: 'cartelli', name: 'cartelli', box: [570, 162, 52, 28], at: [560, 290], face: 'right',
|
||||
actions: {
|
||||
look: [{ say: 'Cartelli per la Pieve, per la stazione, per Firenze. Per il palio, nessuno.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
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: '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' },
|
||||
},
|
||||
],
|
||||
},
|
||||
// Via del Pignone dal basso: sul muro a destra il murale di Cistola (Davide Poggi, consenso del 27 ottobre).
|
||||
// In cima si arriva in Piazzetta Cambray Digny.
|
||||
via_pignone: {
|
||||
name: 'Via del Pignone',
|
||||
map: [46, 54],
|
||||
background: 'assets/scenes/via-del-pignone.png',
|
||||
walk: [[0, 360], [0, 300], [150, 258], [290, 226], [335, 206], [352, 206], [350, 240], [345, 282], [640, 290], [640, 360]],
|
||||
depth: [[206, 0.3], [360, 1.2]],
|
||||
entry: [300, 345],
|
||||
hotspots: [
|
||||
{
|
||||
id: 'murale', name: 'murale di Cistola', box: [430, 36, 170, 240], at: [470, 300], face: 'right',
|
||||
actions: {
|
||||
look: [{ say: 'Cistola. Il talento di via del Pignone.' }, { say: 'Col dieci, come i grandi. Se c\'è un muro che se lo merita, è questo.' }],
|
||||
talk: [{ say: 'Ciao Cistola! ...Almeno dipinto stai fermo ad ascoltarmi.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'su_piazzetta', name: 'su, verso Piazzetta Cambray Digny', box: [316, 176, 56, 40], at: [343, 210], exit: { scene: 'piazzetta', at: [610, 345], face: 'left' },
|
||||
},
|
||||
{
|
||||
id: 'giu_provinciale', name: 'giù, in Via Provinciale', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'provinciale_pignone', at: [530, 245], face: 'left' },
|
||||
},
|
||||
],
|
||||
},
|
||||
// Piazzetta Cambray Digny, in cima a Via del Pignone, sotto Villa Schifanoia: a sinistra Via de' Medici, a diritto la Piazza.
|
||||
piazzetta: {
|
||||
name: 'Piazzetta Cambray Digny',
|
||||
map: [40, 38],
|
||||
background: 'assets/scenes/piazzetta-cambray-digny.png',
|
||||
walk: [[0, 360], [0, 288], [300, 284], [470, 276], [495, 268], [540, 268], [600, 300], [640, 330], [640, 360]],
|
||||
depth: [[268, 0.55], [360, 1.15]],
|
||||
entry: [320, 330],
|
||||
hotspots: [
|
||||
{
|
||||
id: 'villa_schifanoia', name: 'Villa Schifanoia', box: [60, 110, 230, 170], at: [180, 300], face: 'up',
|
||||
actions: {
|
||||
look: [{ say: 'Il muro di Villa Schifanoia, la villa dei Medici, mezzo coperto dall\'edera.' }, { say: 'Lassù una volta c\'era una torre, per tenere d\'occhio chi passava sulla strada per Bologna. Ne ha viste più di me.' }, { say: 'E la piazzetta si chiama come i Cambray-Digny, gli ultimi signori della villa.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'casetta_bianca', name: 'casetta bianca', box: [296, 196, 160, 90], at: [380, 292], face: 'up',
|
||||
actions: {
|
||||
look: [{ say: 'Una casetta piccina, appoggiata al muro della villa come un gatto al sole.' }],
|
||||
open: [{ say: 'Chiusa. E poi non è casa mia.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
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: '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: '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' },
|
||||
},
|
||||
],
|
||||
},
|
||||
// L'area feste (il parco Antonio Berti): dalla rotonda del River, a sinistra.
|
||||
area_feste: {
|
||||
name: 'Area feste',
|
||||
@@ -1275,6 +1589,9 @@ export const story = {
|
||||
{
|
||||
id: 'al_principale', name: 'indietro, verso il campo principale', box: [0, 330, 640, 30], at: [100, 354], exit: { scene: 'polisportiva_principale', at: [620, 330], face: 'left' },
|
||||
},
|
||||
{
|
||||
id: 'alle_mozzete', name: 'a destra, verso le Mozzète', box: [610, 226, 30, 100], at: [628, 290], face: 'right', exit: { scene: 'mozzete', at: [330, 206], face: 'down' },
|
||||
},
|
||||
{
|
||||
id: 'barondeduke', name: 'il BaronDeDuke', box: [212, 245, 36, 55], at: [190, 316], face: 'right', default: 'talk',
|
||||
give: {
|
||||
@@ -2398,6 +2715,41 @@ export const story = {
|
||||
},
|
||||
},
|
||||
// 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: 'S\'ha andà! La canna te la diamo 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: {
|
||||
nodes: {
|
||||
start: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
|
||||
export const VERSION = '0.1.17';
|
||||
export const VERSION = '0.1.23';
|
||||
|
||||
// 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).
|
||||
|
||||
@@ -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-throw { background: #ead09a; color: #202b25; min-width: 100px; }
|
||||
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
|
||||
#bocce-help, #biliardo-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
||||
#bocce-help summary, #biliardo-help summary { cursor: pointer; padding: 6px 0; }
|
||||
#bocce-help p, #biliardo-help p { max-width: 76em; line-height: 1.7; }
|
||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
||||
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary { cursor: pointer; padding: 6px 0; }
|
||||
#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) {
|
||||
#bocce-controls { padding: 8px; font-size: 10px; overflow-y: auto; min-height: 0; }
|
||||
.bocce-adjust { flex-wrap: wrap; gap: 6px; }
|
||||
.bocce-adjust label { order: -1; flex-basis: 100%; max-width: none; }
|
||||
.bocce-adjust button { flex: 1; font-size: 9px; padding: 6px; }
|
||||
#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-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 dd span { display: block; }
|
||||
/* 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); }
|
||||
#credits-reel .track { display: flex; gap: 10px; height: 100%; width: max-content; animation: reel 240s linear infinite; }
|
||||
#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::-webkit-scrollbar { display: none; }
|
||||
#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.palio { aspect-ratio: auto; border: 0; }
|
||||
/* 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 .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; } }
|
||||
@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: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;
|
||||
background: rgba(15, 10, 20, .92); cursor: pointer; z-index: 1; }
|
||||
#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; }
|
||||
/* 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; }
|
||||
#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; }
|
||||
|
||||