Compare commits

...
17 Commits
Author SHA1 Message Date
manzolo bec7f067bc Versione 0.1.28 2026-10-28 12:24:07 +01:00
manzoloandClaude Opus 5.5 e50a3e80e9 Uscite ritoccate dopo la revisione di Codex: arrivo alla rotonda dalla strada dietro la farmacia, sguardo in giù in Via de' Medici e nel viale dei cipressi, «poi a sinistra/destra» in cima alla scalinata
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdCPptNRwbVzjzHbohi
2026-10-28 12:17:52 +01:00
manzolo afb0b931c5 Versione 0.1.27 2026-10-28 12:09:23 +01:00
manzoloandClaude Opus 5.5 fb1568259b Via dell'Antica Posta fra Piazza e Piazzetta, la Piazzetta del Comune e la scalinata; uscite ritoccate (Andrea)
Sfondi di Codex dalle foto di Andrea: Via dell'Antica Posta dalla Piazza, la Piazzetta del Comune (il retro del
municipio con la torre e le sculture), la cima della via sotto il muro di Villa Schifanoia col ponticello, le
panchine e due gatti, e la scalinata col cortile dei garage. Dalla Piazza, a sinistra, si sale in Via dell'Antica
Posta e poi in Piazzetta, che non porta più direttamente in Piazza; dalla Piazzetta (a destra del palazzo) e dalla
curva in cima alla via si scende per la scalinata ai giardini del Comune (lato sinistro). In Piazzetta tutto il lato
sinistro porta in Via de' Medici. Alla rotonda della farmacia l'uscita per la Pieve dove la strada gira dietro il
palazzo e quella per il Comune su tutto l'albero. Il campo del Giannini si collega al ponticino della Carza invece
che all'area camper. Battute provvisorie e voci.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdCPptNRwbVzjzHbohi
2026-10-28 12:03:05 +01:00
manzolo c1049bf69d Versione 0.1.26 2026-10-28 08:46:26 +01:00
manzoloandClaude Fable 5.1 b05fe5b302 La banca: settimane pari (XTTS non azzecca «dìspari»)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-28 08:38:44 +01:00
manzoloandClaude Fable 5.1 505eda01a3 Pronuncia: «dìspari» e «Commòdòr» (Andrea)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-28 08:36:46 +01:00
manzoloandClaude Fable 5.1 d329cbd6ef In Via Provinciale l'edicola delle cassette per il Commodore 64 (LOAD, play, e dieci minuti di cigolio); la banca quando Venere si allinea a Plutone; «dispari» letto dispàri (Andrea)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-28 08:27:16 +01:00
manzolo 1e4dcaaf3b Versione 0.1.25 2026-10-28 08:18:53 +01:00
manzoloandClaude Fable 5.1 4c5ec099d0 La banca in Via Provinciale: il bancomat funziona il mercoledì delle settimane dispari, negli anni bisestili, quando Venere incontra Saturno; not today (Andrea)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-28 08:11:37 +01:00
manzolo b2ec499590 Versione 0.1.24 2026-10-28 07:55:42 +01:00
manzoloandClaude Fable 5.1 75b532cda0 «S'ha andà a fallo?!?» si legge «saandà a fallo»; i ragazzi rispondono «Gnamo! La canna ti si dà noi.» (Andrea)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-28 07:48:50 +01:00
manzolo 68686efe03 Versione 0.1.23 2026-10-28 06:38:25 +01:00
manzoloandClaude Fable 5.1 208e1d9535 La pesca al lago dell'Anatre: «S'ha andà a fallo?!?» coi ragazzi delle canne
Minigioco nuovo (idea di Andrea, 28 ottobre): parlando coi ragazzi in Fortezza si va a pescare. Primo piano del lago
(sfondo di Codex) con Andrea sulla riva con la canna di bambù (foglio di Codex: aspetta, lancia, recupera, mostra il
pesce) e i ragazzi accanto. Si tiene premuto per lanciare (più lontano, pesci più grossi: lasca, persico, cavedano,
tinca, carpa; sotto riva anche lo stivale, che non conta), il galleggiante ha tocchi finti e un'abboccata vera, si
ferra premendo in tempo e, senza lasciare, si recupera; il pesce strattona, la lenza si tende e bisogna mollare un
attimo o si spezza. Sei lanci, tre livelli (2/3/4 pesci per la gloria, finestra di ferrata e forza dei pesci).
Suoni di lancio, tuffo, tocco, abboccata, lenza, rottura e pesce a riva; stessi comandi delle bocce; test del
motore e prova nel browser. Revisione critica di Codex 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; il resto in docs/STATO.md. Nelle idee, le lavandaie del Palio come le racconta Andrea.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-28 06:31:23 +01:00
manzoloandClaude Fable 5.1 f2a0f22f4a Idee: i minigiochi di paese, rivisti e raggruppati (Palio, bar e circolo, sport, giochi da bambini); tolto il doppione in fondo al file
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-28 01:14:51 +01:00
manzolo dea124933e Versione 0.1.22 2026-10-28 00:31:08 +01:00
manzoloandClaude Fable 5.1 51a46fe5a0 Effetti ambientali nelle dieci scene del 27 ottobre (Andrea: «applicale tutte»)
Nuvole, rondini e ombre di foglie dove ci sono alberi e cielo; ombra di nuvola sui campi di Gabbiano, Coldaia, le
Mozzète e la Provinciale; alberi al vento a Gabbiano e Via Massorondinaio; farfalle alla Piazzetta, a Gabbiano e
Coldaia, la libellula al ponticino delle Mozzète; piccioni e merli sulla Provinciale e alla falegnameria, col pulviscolo
nel fienile; di sera la lanterna della Piazzetta, il lampione di Via del Pignone, le lampade del Cinema Taiuti e il
cartellone del Rattattù illuminato.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-28 00:25:01 +01:00
83 changed files with 1220 additions and 51 deletions
+38
View File
@@ -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
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+197 -5
View File
@@ -211,9 +211,9 @@
"file": "andrea-22b8efe94b.mp3", "file": "andrea-22b8efe94b.mp3",
"sec": 1.75 "sec": 1.75
}, },
"pescatori|Si preparano le canne: domattina si va a pescare al lago dell'Anatre, dietro Cardetole!": { "pescatori|Si preparano le canne: si va a pescare al lago dell'Anatre, dietro Cardetole!": {
"file": "pescatori-486103d189.mp3", "file": "pescatori-e7edfae41f.mp3",
"sec": 3.74 "sec": 3.51
}, },
"pescatori|Guarda questa: la lenza la regge anche una carpa di dieci chili.": { "pescatori|Guarda questa: la lenza la regge anche una carpa di dieci chili.": {
"file": "pescatori-afda722e0c.mp3", "file": "pescatori-afda722e0c.mp3",
@@ -707,6 +707,42 @@
"file": "andrea-60f39f0dd7.mp3", "file": "andrea-60f39f0dd7.mp3",
"sec": 6.48 "sec": 6.48
}, },
"andrea|L'edicola. Qui compravo le cassette per il Commodore 64: trenta giochi a mille lire, diceva la copertina.": {
"file": "andrea-a8283c5640.mp3",
"sec": 8.27
},
"andrea|Si infilava la cassetta, si scriveva LOAD, si premeva play... e si aspettava dieci minuti col registratore che cigolava.": {
"file": "andrea-753071634e.mp3",
"sec": 8.67
},
"andrea|Poi, quando andava bene, partiva il gioco. Quando andava male: LOAD ERROR. E si riavvolgeva tutto. Bei tempi.": {
"file": "andrea-c99539e242.mp3",
"sec": 10.4
},
"andrea|Chiusa. E comunque le cassette non le fanno più. Neanche i Commodore.": {
"file": "andrea-4fb258d12c.mp3",
"sec": 5.79
},
"andrea|Vorrei un Topolino e trenta giochi a mille lire. ...Niente, parlo con una serranda.": {
"file": "andrea-0ad7d35251.mp3",
"sec": 5.97
},
"andrea|La banca. Il bancomat funziona solo il mercoledì delle settimane pari, negli anni bisestili, quando Venere si allinea a Plutone.": {
"file": "andrea-01433de83e.mp3",
"sec": 9.25
},
"andrea|Oggi? Not today.": {
"file": "andrea-a698d91684.mp3",
"sec": 2.87
},
"andrea|Chiusa. Come il bancomat, del resto.": {
"file": "andrea-f508d2ed4c.mp3",
"sec": 3.69
},
"andrea|Carta dentro, carta fuori. «Servizio momentaneamente non disponibile». Momentaneamente dal 1998.": {
"file": "andrea-d482267e5d.mp3",
"sec": 9.3
},
"andrea|Cistola. Il talento di via del Pignone.": { "andrea|Cistola. Il talento di via del Pignone.": {
"file": "andrea-2b47355c2c.mp3", "file": "andrea-2b47355c2c.mp3",
"sec": 3.81 "sec": 3.81
@@ -739,6 +775,98 @@
"file": "andrea-7fd73fbb77.mp3", "file": "andrea-7fd73fbb77.mp3",
"sec": 2.66 "sec": 2.66
}, },
"andrea|Le colonnine con le catene: le macchine di qua non passano.": {
"file": "andrea-7967c3d476.mp3",
"sec": 4.05
},
"andrea|Io sì. Scavalcare le catene è uno sport che pratico da quando avevo sei anni.": {
"file": "andrea-db2e00019a.mp3",
"sec": 6.2
},
"andrea|Una tenda a righe bianche e rosse. Fa tanto spiaggia: peccato che il mare sia di là dall'Appennino.": {
"file": "andrea-b8038d49dc.mp3",
"sec": 6.68
},
"andrea|Il muro di sasso con l'edera e la ringhiera in cima. Lassù c'è il verde della villa.": {
"file": "andrea-1ec3557612.mp3",
"sec": 5.8
},
"andrea|Via dell'Antica Posta. Chissà se la posta antica arrivava prima di quella moderna.": {
"file": "andrea-c0f51c8219.mp3",
"sec": 6.36
},
"andrea|La torre dell'orologio, vista dal retro. Anche da qui segna un'ora tutta sua.": {
"file": "andrea-a196260e7c.mp3",
"sec": 5.34
},
"andrea|La porta del Comune. Dal retro, ma sempre Comune.": {
"file": "andrea-66cab2e717.mp3",
"sec": 4.23
},
"andrea|Chiusa. Anche dalla porta di dietro, senza un modulo non si entra.": {
"file": "andrea-835648f0b6.mp3",
"sec": 4.54
},
"andrea|Una scultura moderna, lunga e appuntita. Secondo me è un'antenna per parlare coi marziani.": {
"file": "andrea-d7c8824e13.mp3",
"sec": 7.13
},
"andrea|Questa sembra un sasso. Ma sta su un piedistallo, quindi è arte.": {
"file": "andrea-d8a1173809.mp3",
"sec": 4.91
},
"andrea|Pesa più di me. E poi l'arte non si porta via in tasca.": {
"file": "andrea-51f23d7ac6.mp3",
"sec": 4.15
},
"andrea|Un vasone di fiori. Qualcuno se ne prende cura, al Comune.": {
"file": "andrea-1fa2d33c2f.mp3",
"sec": 4.49
},
"andrea|Due gatti che dormono sull'erba. Il Palio, per loro, può anche aspettare.": {
"file": "andrea-1c8aae44f9.mp3",
"sec": 6.01
},
"andrea|Micio, micio... Niente. Per meno di una scatoletta non aprono neanche un occhio.": {
"file": "andrea-7ea38df174.mp3",
"sec": 6.41
},
"andrea|Svegliare un gatto che dorme al sole? Non sono così coraggioso.": {
"file": "andrea-17ccb76f0c.mp3",
"sec": 4.55
},
"andrea|Due panchine all'ombra del muro. Un posto per chi non ha un palio da ritrovare.": {
"file": "andrea-8136bec81c.mp3",
"sec": 4.95
},
"andrea|Mi siederei volentieri, ma il Palio non si ritrova da solo.": {
"file": "andrea-735b8b0920.mp3",
"sec": 3.38
},
"andrea|Un ponticello sopra la strada, da una casa all'altra. Per andare dal vicino senza fare le scale.": {
"file": "andrea-04381e498b.mp3",
"sec": 7.67
},
"andrea|Il muro di Villa Schifanoia, alto alto, con l'edera che ormai lo tiene su lei.": {
"file": "andrea-4deca437c8.mp3",
"sec": 5.28
},
"andrea|Una saracinesca color ruggine. Dietro, scommetto, una macchina e trent'anni di roba da buttare.": {
"file": "andrea-b72a76810b.mp3",
"sec": 7.04
},
"andrea|Chiusa. E se la apro io, mi tocca anche fare ordine.": {
"file": "andrea-3b7d5860dc.mp3",
"sec": 4.36
},
"andrea|Panni stesi al sole. Se piove, qui lo sanno tutti prima del telegiornale.": {
"file": "andrea-1c6bb20507.mp3",
"sec": 5.41
},
"andrea|Rubare i panni stesi? Mi conoscono tutti, mi ritroverebbero prima del Palio.": {
"file": "andrea-5b0b04fef8.mp3",
"sec": 6.3
},
"andrea|L'area feste. D'estate ci montano la pista da ballo, le tavolate e lo stand della pasta.": { "andrea|L'area feste. D'estate ci montano la pista da ballo, le tavolate e lo stand della pasta.": {
"file": "andrea-a942b2723e.mp3", "file": "andrea-a942b2723e.mp3",
"sec": 6.77 "sec": 6.77
@@ -1984,8 +2112,8 @@
"sec": 1.24 "sec": 1.24
}, },
"andrea|S'ha andà a fallo!?!?": { "andrea|S'ha andà a fallo!?!?": {
"file": "andrea-b25aeefc97.mp3", "file": "andrea-bb61f7641e.mp3",
"sec": 1.16 "sec": 2.17
}, },
"andrea|Ci si becca da i' Trappola, ai' Barrettino!": { "andrea|Ci si becca da i' Trappola, ai' Barrettino!": {
"file": "andrea-fca6f59a70.mp3", "file": "andrea-fca6f59a70.mp3",
@@ -2035,6 +2163,50 @@
"file": "arcu-406cd02197.mp3", "file": "arcu-406cd02197.mp3",
"sec": 4.46 "sec": 4.46
}, },
"andrea|S'ha andà a fallo?!?": {
"file": "andrea-c80714aae7.mp3",
"sec": 1.08
},
"andrea|Quanti ne prendete, di solito?": {
"file": "andrea-1d758ed60c.mp3",
"sec": 1.84
},
"andrea|Buona pesca, ragazzi.": {
"file": "andrea-cb3510ac67.mp3",
"sec": 1.85
},
"pescatori|Gnamo! La canna ti si dà noi.": {
"file": "pescatori-32054cfa54.mp3",
"sec": 2.97
},
"pescatori|Come peschi?": {
"file": "pescatori-520fe85721.mp3",
"sec": 0.56
},
"pescatori|Dipende da chi racconta. Il Nanni dice dieci, il cestino dice due.": {
"file": "pescatori-901d65f3bc.mp3",
"sec": 4.09
},
"pescatori|Se vedi il palio, salutacelo!": {
"file": "pescatori-52125dc78f.mp3",
"sec": 1.74
},
"andrea|Alla bona, tanto per bagnare la lenza.": {
"file": "andrea-b74e348e25.mp3",
"sec": 2.55
},
"pescatori|Alla bona. Due pesci e si va a casa contenti.": {
"file": "pescatori-8af3768e38.mp3",
"sec": 3.26
},
"pescatori|Seria: tre pesci, e lo stivale non conta.": {
"file": "pescatori-e417a89a3a.mp3",
"sec": 2.82
},
"pescatori|A fassi male? Quattro pesci, e le carpe tirano come trattori.": {
"file": "pescatori-befa87eee8.mp3",
"sec": 4.17
},
"andrea|Due tiri alla bona, tanto per scaldarmi.": { "andrea|Due tiri alla bona, tanto per scaldarmi.": {
"file": "andrea-d564365fc2.mp3", "file": "andrea-d564365fc2.mp3",
"sec": 2.53 "sec": 2.53
@@ -2315,6 +2487,26 @@
"file": "andrea-ee42022092.mp3", "file": "andrea-ee42022092.mp3",
"sec": 3.98 "sec": 3.98
}, },
"pescatori|Oh, hai visto? Mica male, per una canna di bambù.": {
"file": "pescatori-2583bf891f.mp3",
"sec": 3.79
},
"andrea|Stasera si frigge. Ma prima il palio.": {
"file": "andrea-b00b469ef8.mp3",
"sec": 3.48
},
"pescatori|I pesci c'erano. Eri te che non c'eri.": {
"file": "pescatori-53d5588588.mp3",
"sec": 2.41
},
"andrea|Si rifà. Dopo il palio.": {
"file": "andrea-5ebf35bc74.mp3",
"sec": 2.37
},
"andrea|Lascio la canna ai ragazzi. Il lago non scappa, il palio sì.": {
"file": "andrea-72bf22a3f9.mp3",
"sec": 4.49
},
"ing_tozzi|Venti. Hai vinto te, stasera il caffè lo pago io.": { "ing_tozzi|Venti. Hai vinto te, stasera il caffè lo pago io.": {
"file": "ing_tozzi-51f855478a.mp3", "file": "ing_tozzi-51f855478a.mp3",
"sec": 4.42 "sec": 4.42
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+38 -13
View File
@@ -105,18 +105,43 @@ si segna qui con la data e il punto di `src/story/story.js` dove sta; le persone
- *Regia alla fessura*: col meccanismo del primo piano (`closeup`) il buio, la torcia, un istante di silenzio, - *Regia alla fessura*: col meccanismo del primo piano (`closeup`) il buio, la torcia, un istante di silenzio,
poi gli occhi. Due immagini di Codex. poi gli occhi. Due immagini di Codex.
- *Segreti facoltativi del paese* con pagine illustrate nel taccuino: funzione nuova, dopo la storia principale. - *Segreti facoltativi del paese* con pagine illustrate nel taccuino: funzione nuova, dopo la storia principale.
- **Altri minigiochi come le bocce** (25 ottobre 2026): ognuno è un primo piano (`closeup` con il suo `type`, come - **Altri minigiochi** (25 ottobre 2026, rivisti il 28 ottobre con le proposte di Claude che ad Andrea piacciono):
`bocce`), con comandi per dito, mouse e tastiera, i suoi test e una battuta per chi vince e per chi perde. ognuno è un primo piano (`closeup` con il suo `type`), con comandi per dito, mouse e tastiera, i suoi test, una
Il posto naturale sono le gare del Palio del secondo capitolo: battuta per chi vince e per chi perde, e a fine lavoro la revisione critica di Codex se Andrea la concede.
- *Passamano dei mattoni* (da fare per primo): un gioco di ritmo, i mattoni arrivano lungo la fila e si passano al Già fatti: briscola, tressette e scopa all'oratorio; le bocce al pallaio; il biliardo coi birilli al Circolo
momento giusto; sbagliando il tempo ne cade uno. Facile col dito, e chiude il cerchio col finale (i mattoni 4 Maggio; «Fai due tiri» al campo da basket del Circolo (27 ottobre). I prossimi due, se Andrea è d'accordo:
portati in Fortezza). il calcio balilla (riusa la fisica del biliardo, fa subito bar) e il tiro alla fune (è il Palio, e vale come
enigma del rione).
Le gare del Palio (il secondo capitolo: passamano, tiro alla fune, carrette, lavandaie, caccia al tesoro):
- *Passamano dei mattoni*: un gioco di ritmo, i mattoni arrivano lungo la fila e si passano al momento giusto,
un tasto per ogni passaggio; sbagliando il tempo ne cade uno. Facile col dito, e chiude il cerchio col finale
(i mattoni portati in Fortezza).
- *Tiro alla fune a Cardetole*: si batte a ritmo (spazio o tocco) seguendo una barra che oscilla, lo sforzo
della squadra; chi perde il tempo scivola. Semplice da fare, molto da palio; contro il bullo del rione, come in
`docs/PROGETTO.md`.
- *Le carrette*: corsa in discesa vista di lato, a destra e sinistra fra buche e balle di paglia. La più - *Le carrette*: corsa in discesa vista di lato, a destra e sinistra fra buche e balle di paglia. La più
spettacolare, la più lunga da disegnare. spettacolare, la più lunga da disegnare: il «gioco grosso», con Mitrugno (consenso).
- *Bandierine (lavanderine)*: correre a prendere la bandiera prima dell'avversario, di riflessi. - *Le lavandaie* (Andrea, 28 ottobre; prima era segnato male come «bandierine»): un vascone pieno d'acqua con
dentro gli stracci, pezzi di stoffa dei quattro colori dei rioni mescolati; ogni squadra deve ripescare i suoi.
Come minigioco: vista dall'alto sul vascone, gli stracci galleggiano e girano, si tocca il proprio colore per
tirarlo fuori, un errore (il colore di un altro) fa perdere tempo; contro il tempo o contro le altre squadre.
- *Caccia al tesoro*: un giro di indizi fra le scene che ci sono già, senza un minigioco nuovo. - *Caccia al tesoro*: un giro di indizi fra le scene che ci sono già, senza un minigioco nuovo.
Fuori dal Palio: Al bar e al circolo:
- *Briscola al circolino* con l'Arcu e il Barone (la gag del «Male male si divide»): una mano corta. - *Calcio balilla al River*: due stecche sole (difesa e attacco), vista dall'alto come il biliardo, che ha già
- *Rigori all'A. Bettini*: mira e forza come alle bocce, contro un portiere. Andrea ha ritrovato la foto di squadra la fisica delle sponde. Il gioco da bar più sampierino che c'è; il River «coi flipper e le figurine» lo cita già.
sul campo in terra (porta, rete, la torre della Pieve dietro, anni '90): lo sfondo del minigioco lo fa Codex da quella, - *Flipper del River*: più lavoro, ma la fisica delle biglie c'è già.
senza le persone (per i volti serve il consenso). Da salvare in `assets/raw/foto/campo-bettini-squadra.jpg`. - *Freccette al Circolo*: la mira vaga e si ferma al clic, tre freccette, si chiude il 301. Poco codice.
- *Morra*: niente fisica, tutto dialogo e tempismo; molto toscana, le grida dei numeri in voce XTTS.
Sport:
- *Rigori all'A. Bettini*: mira e forza come alle bocce, contro un portiere che si butta. Andrea ha ritrovato la
foto di squadra sul campo in terra (porta, rete, la torre della Pieve dietro, anni '90): lo sfondo lo fa Codex
da quella, senza le persone (per i volti serve il consenso). Da salvare in `assets/raw/foto/campo-bettini-squadra.jpg`.
- *Tennis o padel al campo secondario*: la macchina del tempo compare già quando Andrea dice di voler giocare a
padel; un «muro» a tempismo basterebbe.
- *Ping-pong al pallaio*: il tavolo verde c'è già nello sfondo.
Giochi da bambini del 2007 (nostalgia):
- *Le figurine al River*: un «batti-muro», o lo scambio «ce l'ho, ce l'ho, mi manca», più dialogo che gioco.
- *La pesca al lago dell'Anatre*: coi ragazzi delle canne in Fortezza (27 ottobre) la scusa c'è già. Tempismo:
la lenza vibra, si tira al momento giusto. Serve la scena del lago, dietro Cardetole.
- *Briscola al circolino* con l'Arcu e il Barone (la gag del «Male male si divide»): una mano corta; la briscola
c'è già all'oratorio, qui conta la gag.
+1 -1
View File
@@ -165,7 +165,7 @@ tipi riconoscibili che persone identificabili, salvo accordi.
Obiettivo: entrare nella Fortezza e trovare il palio. Serve l'aiuto dei tre rioni oltre a quello di Andrea; ogni Obiettivo: entrare nella Fortezza e trovare il palio. Serve l'aiuto dei tre rioni oltre a quello di Andrea; ogni
rione vuole una sfida del **Palietto** (i giochi dei bambini del Palio: passaggio dei mattoni, tiro alla fune, corsa rione vuole una sfida del **Palietto** (i giochi dei bambini del Palio: passaggio dei mattoni, tiro alla fune, corsa
coi carretti, gioco dell'elastico), qui trasformate in enigmi comici. coi carretti, gioco dell'elastico, le lavandaie: gli stracci dei quattro colori nel vascone, ognuno ripesca i suoi), qui trasformate in enigmi comici.
Le gare che Andrea ricorda davvero: Le gare che Andrea ricorda davvero:
+18
View File
@@ -204,6 +204,13 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso
Via del Pignone (sfondo di Codex da `assets/raw/foto/via-del-pignone.png`): sul muro di destra il Via del Pignone (sfondo di Codex da `assets/raw/foto/via-del-pignone.png`): sul muro di destra il
murale di Cistola (Davide Poggi), «il talento di via del Pignone», alla maniera dei murali di Maradona a Napoli murale di Cistola (Davide Poggi), «il talento di via del Pignone», alla maniera dei murali di Maradona a Napoli
(volto da `assets/raw/riferimenti/cistola.png`). Consenso di Davide confermato da Andrea il 2026-10-27. (volto da `assets/raw/riferimenti/cistola.png`). Consenso di Davide confermato da Andrea il 2026-10-27.
- Via dell'Antica Posta (foto di Andrea, 28 ottobre, sfondi di Codex): dalla Piazza, a sinistra, sale verso Piazzetta
Cambray Digny, che non è più collegata direttamente alla Piazza. A sinistra la Piazzetta del Comune (il retro del
municipio con la torre e le sculture); in cima il muro di Villa Schifanoia, il ponticello, le panchine e due gatti.
Dalla Piazzetta, a destra accanto al palazzo, si scende per la scalinata in un cortile coi garage che porta ai
giardini del Comune (lato sinistro della scena del Comune); la scalinata si prende anche dalla curva a sinistra in
cima a Via dell'Antica Posta. In Piazzetta tutto il lato sinistro porta in Via de' Medici. Da confermare con Andrea:
il nome vero della scalinata. Battute provvisorie.
- Via Pietro Cipriani (fra Piazza e campeggio): Cinema Taiuti aperto con la locandina del Rattattù 2007 (stile da - Via Pietro Cipriani (fra Piazza e campeggio): Cinema Taiuti aperto con la locandina del Rattattù 2007 (stile da
`assets/raw/riferimenti/rattattu-locandina.png`, scaletta illeggibile). Al campo da basket del Circolo, «Fai due tiri» `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 (`src/engine/basket.js`, vista di lato, sfondo e sprite di Andrea col pallone da Codex): da solo, dieci palloni, a ogni
@@ -218,6 +225,17 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso
da Via Cipriani, indietro, Via Massorondinaio → campeggio. Sfondi da Street View, battute provvisorie. da Via Cipriani, indietro, Via Massorondinaio → campeggio. Sfondi da Street View, battute provvisorie.
- In Fortezza quattro ragazzi inventati si fanno le canne da pesca col bambù per il lago dell'Anatre (sprite di Codex, - In Fortezza quattro ragazzi inventati si fanno le canne da pesca col bambù per il lago dell'Anatre (sprite di Codex,
`assets/raw/pescatori-sheet.png`, ritagliato 2x2 a mano: work, talk, laugh, proud); battuta finale di Andrea. `assets/raw/pescatori-sheet.png`, ritagliato 2x2 a mano: work, talk, laugh, proud); battuta finale di Andrea.
Parlandoci: «S'ha andà a fallo?!?» (Andrea, 28 ottobre) e si va a pescare: minigioco `src/engine/pesca.js`
(primo piano del lago, sfondo e Andrea con la canna di Codex; lancio tenendo premuto, tocchi finti e abboccata,
ferrata a tempo, recupero con la lenza che si può spezzare; sei lanci; pesci più grossi al largo, lo stivale sotto
riva; tre livelli). Battute provvisorie. Il lago come scena da visitare non c'è: serve una foto di Andrea.
Revisione di Codex (28 ottobre) applicata nei punti gravi (galleggiante che saltava, sesto lancio bloccato dal
pannello, invito a ferrare fuori tempo, lenza che non si spezzava mai, mira che si sbloccava) e in parecchi utili
(un gesto solo per ferrare e recuperare, pulsante Recupera/Molla, recupero breve per i pesci piccoli, attese ripetute
dimezzate, tocchi finti variabili, lenza dalla punta vera della canna, stivale con la sua battuta e senza peso,
risultato deciso all'ultimo pesce). Restano le sue idee: «Porta a casa la pescata» a gloria raggiunta; premio per
peso o varietà; i ragazzi che reagiscono (stivale, carpa, lenza rotta); paesaggio sonoro del lago (ora quello della
Fortezza); abboccata e tensione anche nei comandi HTML e un richiamo grande sul telefono; Esc col fuoco sui pulsanti.
- Andrea rifatto dalla sua foto del 2007 (`assets/raw/andrea-sheet-prova-2.png`; il vecchio foglio resta in `andrea-sheet.png`). - Andrea rifatto dalla sua foto del 2007 (`assets/raw/andrea-sheet-prova-2.png`; il vecchio foglio resta in `andrea-sheet.png`).
- Strada del campeggio (poi Bosco ai Frati) e strada della scuola: per ora battute, scene da fare con le foto di Andrea. - Strada del campeggio (poi Bosco ai Frati) e strada della scuola: per ora battute, scene da fare con le foto di Andrea.
+11
View File
@@ -219,6 +219,17 @@
palla: entra anche sporca, ma non da sotto. La gloria: 3 canestri alla bona, 5 seria, 7 a fassi male. Alla bona 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> guida mostra la parabola fino al primo urto, Seria solo l’inizio, A fassi male niente.</p>
</details> </details>
<details id="pesca-help" hidden><summary>Come si gioca</summary>
<p>Un tocco sull’acqua sceglie dove lanciare (o regola la forza: più lontano, pesci più grossi, ma attese più
lunghe). Tieni premuto sulla scena (o Spazio): la forza riparte da zero e sale, lascia per lanciare; oppure premi
Lancia. Poi si aspetta: il galleggiante ha dei tocchi finti (si abbassa appena) e un’abboccata vera (va sotto del
tutto): premi in quel momento per ferrare, e <b>resta premuto</b>: stai già recuperando. Premendo a vuoto si
ricupera la lenza vuota e il lancio è perso; se non ferri in tempo il pesce lo sputa, e riprova fino a tre volte.</p>
<p>Nel recupero guarda la lenza e la barra in basso: quando il pesce strattona (la lenza trema) la barra sale verso
il rosso, lascia andare un attimo, poi riprendi. I pesci piccoli vengono a riva in fretta, le carpe no. Dal pannello
il pulsante alterna Recupera e Molla. Sei lanci. La gloria: 2 pesci alla bona, 3 seria, 4 a fassi male (lo stivale
non conta, e non pesa). Frecce ← →: distanza del lancio; Esc esce.</p>
</details>
<details id="biliardo-help" hidden><summary>Come si gioca</summary> <details id="biliardo-help" hidden><summary>Come si gioca</summary>
<button class="tutorial-open" type="button" data-tutorial="biliardo" hidden>Guarda il video</button> <button class="tutorial-open" type="button" data-tutorial="biliardo" hidden>Guarda il video</button>
<p>Clic o tocco sul tavolo fissa la mira: puoi spostarti sui comandi senza cambiarla. <p>Clic o tocco sul tavolo fissa la mira: puoi spostarti sui comandi senza cambiarla.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "leggenda-del-regolo", "name": "leggenda-del-regolo",
"version": "0.1.21", "version": "0.1.28",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+2
View File
@@ -4,6 +4,7 @@ import { findPath, dist, inBox, boxDistance, nearestInside } from './geometry.js
import * as bocce from './bocce.js'; import * as bocce from './bocce.js';
import * as biliardo from './biliardo.js'; import * as biliardo from './biliardo.js';
import * as basket from './basket.js'; import * as basket from './basket.js';
import * as pesca from './pesca.js';
import * as briscola from './briscola.js'; import * as briscola from './briscola.js';
import * as tressette from './tressette.js'; import * as tressette from './tressette.js';
import * as scopa from './scopa.js'; import * as scopa from './scopa.js';
@@ -16,6 +17,7 @@ export const MINIGAMES = {
bocce: { create: (seed, level) => bocce.newBocce(seed, { level }), update: bocce.update, press: bocce.press, release: bocce.release, cancel: bocce.cancel, aim: bocce.aimScreen }, bocce: { create: (seed, level) => bocce.newBocce(seed, { level }), update: bocce.update, press: bocce.press, release: bocce.release, cancel: bocce.cancel, aim: bocce.aimScreen },
biliardo: { create: (seed, level) => biliardo.newBiliardo(seed, { level }), update: biliardo.update, press: biliardo.press, release: biliardo.release, cancel: biliardo.cancel, aim: biliardo.aimScreen }, biliardo: { create: (seed, level) => biliardo.newBiliardo(seed, { level }), update: biliardo.update, press: biliardo.press, release: biliardo.release, cancel: biliardo.cancel, aim: biliardo.aimScreen },
basket: { create: (seed, level) => basket.newBasket(seed, { level }), update: basket.update, press: basket.press, release: basket.release, cancel: basket.cancel, aim: basket.aimScreen }, basket: { create: (seed, level) => basket.newBasket(seed, { level }), update: basket.update, press: basket.press, release: basket.release, cancel: basket.cancel, aim: basket.aimScreen },
pesca: { create: (seed, level) => pesca.newPesca(seed, { level }), update: pesca.update, press: pesca.press, release: pesca.release, cancel: pesca.cancel, aim: pesca.aimScreen },
}; };
export const VERBS = ['walk', 'look', 'take', 'use', 'talk', 'open', 'give', 'drop']; export const VERBS = ['walk', 'look', 'take', 'use', 'talk', 'open', 'give', 'drop'];
+125
View File
@@ -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();
}
+199
View File
@@ -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' : '…';
+6
View File
@@ -6,6 +6,7 @@ import { drawTorch } from './torch.js';
import { drawBocce } from './bocce-render.js'; import { drawBocce } from './bocce-render.js';
import { drawBiliardo } from './biliardo-render.js'; import { drawBiliardo } from './biliardo-render.js';
import { drawBasket } from './basket-render.js'; import { drawBasket } from './basket-render.js';
import { drawPesca } from './pesca-render.js';
import { drawBriscola } from './briscola-render.js'; import { drawBriscola } from './briscola-render.js';
import { drawTressette } from './tressette-render.js'; import { drawTressette } from './tressette-render.js';
import { drawScopa } from './scopa-render.js'; import { drawScopa } from './scopa-render.js';
@@ -683,6 +684,11 @@ function drawCloseup(ctx, game, time) {
drawBocce(ctx, c, def, backgrounds.get(def.picture), time); drawBocce(ctx, c, def, backgrounds.get(def.picture), time);
return; return;
} }
if (def.type === '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 (def.type === 'basket') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture); if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawBasket(ctx, c, def, backgrounds.get(def.picture), sprites.get('andrea_basket')); drawBasket(ctx, c, def, backgrounds.get(def.picture), sprites.get('andrea_basket'));
+10
View File
@@ -125,6 +125,16 @@ export class Soundscape {
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-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-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 }); } else if (kind === 'basket-in') { this.tone(1300, 1000, 0.08, 0.04, 0, pan); this.noiseSound(0.22, 2800, 0.08, { paper: true, pan }); }
// La pesca: il lancio, il tuffo del galleggiante, il tocco, l'abboccata, la ferrata, la lenza che tira, lo strappo, il pesce a riva.
else if (kind === 'pesca-lancio') this.noiseSound(0.22, 1800, 0.05 + strength * 0.06, { pan });
else if (kind === 'pesca-tuffo') { this.tone(420, 180, 0.07, 0.07, 0, pan); this.noiseSound(0.12, 900, 0.06, { pan }); }
else if (kind === 'pesca-tocco') this.tone(900, 700, 0.04, 0.03, 0, pan);
else if (kind === 'pesca-abbocca') { this.tone(520, 260, 0.1, 0.08, 0, pan); this.noiseSound(0.1, 1100, 0.06, { pan }); }
else if (kind === 'pesca-ferrata') this.noiseSound(0.1, 2600, 0.08, { pan });
else if (kind === 'pesca-mulinello') this.tone(1800 + strength * 900, 1700 + strength * 900, 0.05, 0.015 + strength * 0.02, 0, pan);
else if (kind === 'pesca-rottura') { this.tone(2400, 300, 0.12, 0.1, 0, pan); this.noiseSound(0.08, 3000, 0.06, { pan }); }
else if (kind === 'pesca-vuoto') this.noiseSound(0.14, 1400, 0.05, { pan });
else if (kind === 'pesca-preso') { this.noiseSound(0.25, 1000, 0.1, { pan }); this.tone(660, 990, 0.18, 0.05, 0.1, pan); }
else if (kind === 'wood') this.noiseSound(0.3, 750, 0.13, { pan }); else if (kind === 'wood') this.noiseSound(0.3, 750, 0.13, { pan });
else if (kind === 'tavolo-lontano') { else if (kind === 'tavolo-lontano') {
// Su un altro tavolo: il colpo di stecca, poi la biglia che ne prende un'altra, piano, da lontano. // Su un altro tavolo: il colpo di stecca, poi la biglia che ne prende un'altra, piano, da lontano.
+10 -6
View File
@@ -11,6 +11,7 @@ import { soundscapeFor } from './story/soundscapes.js';
import * as bocce from './engine/bocce.js'; import * as bocce from './engine/bocce.js';
import * as biliardo from './engine/biliardo.js'; import * as biliardo from './engine/biliardo.js';
import * as basket from './engine/basket.js'; import * as basket from './engine/basket.js';
import * as pesca from './engine/pesca.js';
import * as briscola from './engine/briscola.js'; import * as briscola from './engine/briscola.js';
import { cardAt } from './engine/briscola-render.js'; import { cardAt } from './engine/briscola-render.js';
import * as tressette from './engine/tressette.js'; import * as tressette from './engine/tressette.js';
@@ -501,7 +502,7 @@ canvas.addEventListener('pointerdown', e => {
if (!aimed && !game.speech) return; // dove non si può mirare (dietro Andrea) non si carica nemmeno if (!aimed && !game.speech) return; // dove non si può mirare (dietro Andrea) non si carica nemmeno
if (aimed) game.closeup.aimLocked = true; // il clic fissa la mira if (aimed) game.closeup.aimLocked = true; // il clic fissa la mira
} }
if (['bocce', 'basket'].includes(story.closeups[game.closeup.id].type) && aimed) game.closeup.aimLocked = true; if (['bocce', 'basket', 'pesca'].includes(story.closeups[game.closeup.id].type) && aimed) game.closeup.aimLocked = true;
press = { closeup: true }; press = { closeup: true };
// Alle bocce si tiene premuto per caricare la forza (la battuta in corso, un tocco la salta). // Alle bocce si tiene premuto per caricare la forza (la battuta in corso, un tocco la salta).
if (!game.skip()) game.closeupPress(); if (!game.skip()) game.closeupPress();
@@ -551,7 +552,7 @@ canvas.addEventListener('pointermove', e => {
// Al biliardo e alle bocce la mira segue il mouse finché un clic (o una regolazione fine) non la fissa: dopo, // Al biliardo e alle bocce la mira segue il mouse finché un clic (o una regolazione fine) non la fissa: dopo,
// attraversare il campo per raggiungere la forza, «Tira» o «Lancia» non cambia il colpo. Al tiro dopo torna libera. // attraversare il campo per raggiungere la forza, «Tira» o «Lancia» non cambia il colpo. Al tiro dopo torna libera.
// (Bocce: scoperto girando il video «Come si gioca», la mira finiva a due passi andando verso il cursore.) // (Bocce: scoperto girando il video «Come si gioca», la mira finiva a due passi andando verso il cursore.)
const aiming = game.closeup && ['biliardo', 'bocce', 'basket'].includes(story.closeups[game.closeup.id].type); const aiming = game.closeup && ['biliardo', 'bocce', 'basket', 'pesca'].includes(story.closeups[game.closeup.id].type);
if (aiming) { if (e.pointerType === 'mouse' && !press && !game.closeup.aimLocked) game.closeupAim(scenePoint(e)); } if (aiming) { if (e.pointerType === 'mouse' && !press && !game.closeup.aimLocked) game.closeupAim(scenePoint(e)); }
else if (e.pointerType === 'mouse' || press?.closeup) game.closeupAim(scenePoint(e)); else if (e.pointerType === 'mouse' || press?.closeup) game.closeupAim(scenePoint(e));
}); });
@@ -565,6 +566,7 @@ const MINI = {
bocce: { ...bocce, nudge: (c, dir) => bocce.aim(c, c.aim.u, c.aim.w + dir * 0.3), back: 'Torna al pallaio', label: 'Bocce: tocca il campo o il dettaglio per mirare. Regola la forza e premi Lancia.' }, bocce: { ...bocce, nudge: (c, dir) => bocce.aim(c, c.aim.u, c.aim.w + dir * 0.3), back: 'Torna al pallaio', label: 'Bocce: tocca il campo o il dettaglio per mirare. Regola la forza e premi Lancia.' },
biliardo: { ...biliardo, back: 'Torna al bar', label: 'Biliardo: clic o tocco fissa la mira. Frecce per affinarla, Maiusc per passi più piccoli. Regola la forza e premi Tira.' }, biliardo: { ...biliardo, back: 'Torna al bar', label: 'Biliardo: clic o tocco fissa la mira. Frecce per affinarla, Maiusc per passi più piccoli. Regola la forza e premi Tira.' },
basket: { ...basket, back: 'Torna in strada', label: 'Fai due tiri: muovi il mouse o tocca per mirare, tieni premuto per caricare la forza, lascia per tirare.' }, basket: { ...basket, back: 'Torna in strada', label: 'Fai due tiri: muovi il mouse o tocca per mirare, tieni premuto per caricare la forza, lascia per tirare.' },
pesca: { ...pesca, back: 'Torna in Fortezza', label: 'La pesca: tieni premuto per lanciare, premi quando il galleggiante va sotto, tieni premuto per recuperare.' },
}; };
const miniState = () => running && game?.closeup && MINI[story.closeups[game.closeup.id].type] ? game.closeup : null; const miniState = () => running && game?.closeup && MINI[story.closeups[game.closeup.id].type] ? game.closeup : null;
const mini = () => MINI[story.closeups[game.closeup.id].type]; const mini = () => MINI[story.closeups[game.closeup.id].type];
@@ -906,18 +908,20 @@ function updateUi() {
} }
} }
if (mg && !cards) { if (mg && !cards) {
$('bocce-controls').setAttribute('aria-label', { biliardo: 'Comandi del biliardo', basket: 'Comandi del basket' }[def.type] ?? 'Comandi delle bocce'); $('bocce-controls').setAttribute('aria-label', { biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca' }[def.type] ?? 'Comandi delle bocce');
$('panel').hidden = $('choices').hidden = true; $('panel').hidden = $('choices').hidden = true;
const line = mg.status(c, def.names); const line = mg.status(c, def.names);
if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line; if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line;
$('bocce-power').value = Math.round(c.power * 100); $('bocce-power').value = Math.round(c.power * 100);
$('bocce-power-value').value = `${Math.round(c.power * 100)}%`; $('bocce-power-value').value = `${Math.round(c.power * 100)}%`;
for (const id of ['bocce-power', 'bocce-left', 'bocce-right', 'bocce-throw']) $(id).disabled = !mg.canThrow(c) || c.holding || !!game.speech; for (const id of ['bocce-power', 'bocce-left', 'bocce-right']) $(id).disabled = !(mg.canAdjust?.(c) ?? mg.canThrow(c)) || c.holding || !!game.speech;
$('bocce-throw').textContent = c.step === 'result' ? 'Finita' : c.step === 'rolling' ? 'In movimento…' : c.current === 'cpu' ? `Tira ${def.names.cpu}…` : def.type === 'bocce' ? 'Lancia' : 'Tira'; $('bocce-throw').disabled = !mg.canThrow(c) || c.holding || !!game.speech;
$('bocce-throw').textContent = mg.button?.(c) ?? (c.step === 'result' ? 'Finita' : c.step === 'rolling' ? 'In movimento…' : c.current === 'cpu' ? `Tira ${def.names.cpu}…` : def.type === 'bocce' ? 'Lancia' : 'Tira');
$('bocce-help').hidden = def.type !== 'bocce'; $('bocce-help').hidden = def.type !== 'bocce';
$('biliardo-help').hidden = def.type !== 'biliardo'; $('biliardo-help').hidden = def.type !== 'biliardo';
$('basket-help').hidden = def.type !== 'basket'; $('basket-help').hidden = def.type !== 'basket';
if (!mg.canThrow(c)) c.aimLocked = false; // a ogni nuovo tiro la mira torna libera $('pesca-help').hidden = def.type !== 'pesca';
if (mg.aimFree?.(c) ?? !mg.canThrow(c)) c.aimLocked = false; // a ogni nuovo tiro la mira torna libera
} }
if (mg) for (const event of c.events.splice(0)) ambience.play(event.kind, { pan: 0, strength: event.power }); if (mg) for (const event of c.events.splice(0)) ambience.play(event.kind, { pan: 0, strength: event.power });
// Il pulsante della mappa c'è solo in partita, fuori dai dialoghi, e quando c'è almeno un altro posto dove andare. // Il pulsante della mappa c'è solo in partita, fuori dai dialoghi, e quando c'è almeno un altro posto dove andare.
+74
View File
@@ -68,4 +68,78 @@ export const ambience = {
viale_cipressi: [{ type: 'cloudshadow', phase: day, area: [95, 225, 435, 135], offset: 11000, viale_cipressi: [{ type: 'cloudshadow', phase: day, area: [95, 225, 435, 135], offset: 11000,
clip: [[300, 225], [337, 225], [530, 360], [95, 360]] }], clip: [[300, 225], [337, 225], [530, 360], [95, 360]] }],
trebbio: [shade([278, 277, 112, 23], 6)], trebbio: [shade([278, 277, 112, 23], 6)],
// Le scene del 27 ottobre (Andrea: «applicale tutte»).
via_pignone: [
{ type: 'clouds', area: [100, 0, 440, 110], count: 3, big: true },
{ type: 'birds', phase: day, area: [200, 20, 300, 80], count: 3, every: 23000, duration: 5000, offset: 6000 },
shade([330, 282, 180, 50], 10),
{ type: 'lamps', phase: 'sera', points: [[250, 74]], glow: 10 },
],
piazzetta: [
shade([30, 290, 300, 60], 14),
butterfly([150, 170, 150, 60], 5000),
{ type: 'lamps', phase: 'sera', points: [[616, 118]], glow: 12 },
],
// Via dell'Antica Posta e dintorni (28 ottobre).
via_antica_posta: [
{ type: 'clouds', area: [180, 0, 300, 90], count: 2 },
shade([0, 300, 640, 60], 10),
{ type: 'lamps', phase: 'sera', points: [[62, 120], [366, 142]], glow: 10 },
],
piazzetta_comune: [
{ type: 'clouds', area: [60, 0, 520, 110], count: 3, big: true },
butterfly([510, 250, 110, 50], 4000),
{ type: 'lamps', phase: 'sera', points: [[436, 170]], glow: 10 },
],
antica_posta_alta: [
shade([0, 290, 360, 70], 12),
butterfly([480, 170, 120, 60], 6000),
{ type: 'lamps', phase: 'sera', points: [[184, 74]], glow: 10 },
],
provinciale_pignone: [
shade([80, 270, 220, 70], 14), shade([470, 290, 170, 60], 10),
{ type: 'cloudshadow', phase: day, area: [120, 230, 420, 130], offset: 8000,
clip: [[290, 232], [370, 232], [640, 330], [640, 360], [0, 360], [0, 330]] },
bird([410, 258, 70, 16], 9000),
],
gabbiano: [
{ type: 'clouds', area: [140, 0, 500, 140], count: 3, big: true },
{ type: 'birds', phase: day, area: [200, 40, 400, 100], count: 4, every: 21000, duration: 5500, offset: 3000 },
{ type: 'cloudshadow', phase: day, area: [140, 160, 500, 200], offset: 7000, clip: rect([140, 160, 500, 200]) },
{ type: 'sway', rects: [[430, 128, 70, 60]], amount: 2 },
butterfly([120, 250, 200, 60], 3000),
],
ponticino_mozzete: [
shade([0, 230, 640, 110], 22),
{ type: 'insect', kind: 'dragonfly', phase: day, area: [80, 230, 140, 50], every: 37000, duration: 8500, offset: 7000 },
],
via_cipriani: [
{ type: 'clouds', area: [0, 0, 420, 150], count: 3, big: true },
shade([300, 300, 160, 50], 10),
// Di sera le lampade del cinema e il cartellone del Rattattù illuminato.
{ type: 'lamps', phase: 'sera', points: [[545, 214], [620, 168]], glow: 12 },
{ type: 'lamps', phase: 'sera', points: [[490, 200]], glow: 34 },
],
via_massorondinaio: [
{ type: 'clouds', area: [0, 0, 640, 100], count: 3, big: true },
{ type: 'birds', phase: day, area: [100, 30, 400, 100], count: 3, every: 25000, duration: 5000, offset: 11000 },
{ type: 'sway', rects: [[200, 128, 90, 72]], amount: 2 },
],
coldaia: [
{ type: 'clouds', area: [200, 0, 440, 120], count: 2 },
{ type: 'cloudshadow', phase: day, area: [0, 150, 640, 210], offset: 15000, clip: rect([0, 160, 640, 200]) },
{ type: 'birds', phase: day, area: [250, 20, 350, 90], count: 3, every: 27000, duration: 5500, offset: 2000 },
butterfly([60, 280, 200, 50], 12000),
],
falegnameria_coldaia: [
{ type: 'clouds', area: [0, 0, 640, 110], count: 3, big: true },
{ type: 'dust', phase: day, area: [330, 150, 260, 70], count: 10 },
bird([250, 230, 90, 18], 8000, 'blackbird'),
],
mozzete: [
{ type: 'clouds', area: [100, 0, 540, 110], count: 3, big: true },
shade([0, 215, 230, 60], 12),
{ type: 'cloudshadow', phase: day, area: [60, 220, 520, 140], offset: 23000,
clip: [[0, 215], [400, 195], [640, 260], [640, 360], [0, 360]] },
],
}; };
+263 -17
View File
@@ -166,6 +166,24 @@ export const story = {
quit: [{ say: 'Lascio il pallone lì. Tanto lo sanno tutti che è mio.' }], quit: [{ say: 'Lascio il pallone lì. Tanto lo sanno tutti che è mio.' }],
draw: [], draw: [],
}, },
// La pesca al lago dell'Anatre coi ragazzi delle canne (idea di Andrea, 28 ottobre): sei lanci, da solo con la
// canna di bambù; la gloria dipende dal livello (pesca.LEVELS). [DA ANDREA: le sue battute]
pesca: {
type: 'pesca',
picture: 'assets/closeups/lago-anatre.png',
names: { player: 'Andrea', cpu: 'il lago' },
win: [
{ say: 'Oh, hai visto? Mica male, per una canna di bambù.', who: 'pescatori', pose: 'laugh' },
{ say: 'Stasera si frigge. Ma prima il palio.' },
{ set: 'pesca_vinta' },
],
lose: [
{ say: 'I pesci c\'erano. Eri te che non c\'eri.', who: 'pescatori', pose: 'talk' },
{ say: 'Si rifà. Dopo il palio.' },
],
quit: [{ say: 'Lascio la canna ai ragazzi. Il lago non scappa, il palio sì.' }],
draw: [],
},
// Il biliardo del Circolo 4 Maggio contro l'Ing. Tozzi: cinque birilli, un colpo a testa, si va a 20. [DA ANDREA: le sue battute] // Il biliardo del Circolo 4 Maggio contro l'Ing. Tozzi: cinque birilli, un colpo a testa, si va a 20. [DA ANDREA: le sue battute]
biliardo: { biliardo: {
type: 'biliardo', type: 'biliardo',
@@ -233,6 +251,7 @@ export const story = {
andrea: { name: 'Andrea', color: '#ffffff', sprite: 'assets/characters/andrea.json' }, andrea: { name: 'Andrea', color: '#ffffff', sprite: 'assets/characters/andrea.json' },
// Andrea col pallone, per «Fai due tiri» (foglio di Codex dal suo sprite): non parla, lo disegna basket-render. // Andrea 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_basket: { name: 'Andrea', sprite: 'assets/characters/andrea-basket.json' },
andrea_pesca: { name: 'Andrea', sprite: 'assets/characters/andrea-pesca.json' },
// Quattro ragazzi inventati (sprite di Codex, idea di Andrea del 27 ottobre): nessuna persona reale. // Quattro ragazzi inventati (sprite di Codex, idea di Andrea del 27 ottobre): nessuna persona reale.
pescatori: { name: 'I ragazzi delle canne', color: '#b8f08a', sprite: 'assets/characters/pescatori.json' }, pescatori: { name: 'I ragazzi delle canne', color: '#b8f08a', sprite: 'assets/characters/pescatori.json' },
vecchietta: { name: 'Vecchietta', color: '#ffd27a', sprite: 'assets/characters/vecchietta.json' }, vecchietta: { name: 'Vecchietta', color: '#ffd27a', sprite: 'assets/characters/vecchietta.json' },
@@ -496,11 +515,12 @@ export const story = {
take: [{ give: 'squama' }, { set: 'squama_presa' }, { say: 'Una squama! E bella grossa.' }], take: [{ give: 'squama' }, { set: 'squama_presa' }, { say: 'Una squama! E bella grossa.' }],
}, },
}, },
// Si compare in basso: la foto di Via Cipriani è presa dall'altro verso (Andrea, 28 ottobre, va bene così).
{ {
id: 'verso_campeggio', name: 'Via Cipriani, verso il cinema e il campeggio', box: [330, 190, 34, 48], at: [345, 234], exit: { scene: 'via_cipriani', at: [300, 345], face: 'up' }, id: 'verso_campeggio', name: 'Via Cipriani, verso il cinema e il campeggio', box: [330, 190, 34, 48], at: [345, 234], exit: { scene: 'via_cipriani', at: [300, 345], face: 'up' },
}, },
{ {
id: 'verso_piazzetta', name: 'Piazzetta Cambray Digny, verso Via de\' Medici', box: [0, 196, 146, 54], at: [92, 250], exit: { scene: 'piazzetta', at: [510, 274], face: 'down' }, id: 'verso_piazzetta', name: 'Via dell\'Antica Posta, verso Piazzetta Cambray Digny', box: [0, 196, 146, 54], at: [92, 250], exit: { scene: 'via_antica_posta', at: [320, 345], face: 'up' },
}, },
{ {
id: 'verso_calimara', name: 'Via Calimara, verso la farmacia', box: [180, 334, 380, 26], at: [360, 354], exit: { scene: 'via_calimara', at: [365, 212], face: 'down' }, id: 'verso_calimara', name: 'Via Calimara, verso la farmacia', box: [180, 334, 380, 26], at: [360, 354], exit: { scene: 'via_calimara', at: [365, 212], face: 'down' },
@@ -529,12 +549,13 @@ export const story = {
id: 'pescatori', name: 'ragazzi con le canne', box: [92, 236, 120, 86], at: [240, 334], face: 'left', default: 'talk', id: 'pescatori', name: 'ragazzi con le canne', box: [92, 236, 120, 86], at: [240, 334], face: 'left', default: 'talk',
actions: { actions: {
look: [{ say: 'Quattro ragazzi che affilano canne di bambù col coltellino e ci legano le lenze.' }], look: [{ say: 'Quattro ragazzi che affilano canne di bambù col coltellino e ci legano le lenze.' }],
talk: [ talk: [{ if: '!pescatori_parlato', then: [
{ set: 'pescatori_parlato' },
{ say: 'Oh, che fate?' }, { say: 'Oh, che fate?' },
{ say: 'Si preparano le canne: domattina si va a pescare al lago dell\'Anatre, dietro Cardetole!', who: 'pescatori', pose: 'talk' }, { say: 'Si preparano le canne: si va a pescare al lago dell\'Anatre, dietro Cardetole!', who: 'pescatori', pose: 'talk' },
{ say: 'Guarda questa: la lenza la regge anche una carpa di dieci chili.', who: 'pescatori', pose: 'proud' }, { say: 'Guarda questa: la lenza la regge anche una carpa di dieci chili.', who: 'pescatori', pose: 'proud' },
{ say: '(Vedi, mia mamma aveva ragione: in Fortezza si viene a farsi le canne.)' }, { say: '(Vedi, mia mamma aveva ragione: in Fortezza si viene a farsi le canne.)' },
], ] }, { dialogue: 'pescatori' }],
}, },
}, },
{ {
@@ -1182,6 +1203,9 @@ export const story = {
open: [{ say: 'All\'asilo? Ho già dato, grazie.' }], open: [{ say: 'All\'asilo? Ho già dato, grazie.' }],
}, },
}, },
{
id: 'alla_scalinata', name: 'a sinistra, la scalinata della Piazzetta', box: [0, 120, 150, 212], at: [40, 300], face: 'left', exit: { scene: 'scalinata', at: [300, 350], face: 'up' },
},
{ {
id: 'al_giardino', name: 'verso il giardino di Via Calimara', box: [596, 290, 44, 70], at: [600, 340], exit: { scene: 'via_calimara', at: [160, 290], face: 'right' }, id: 'al_giardino', name: 'verso il giardino di Via Calimara', box: [596, 290, 44, 70], at: [600, 340], exit: { scene: 'via_calimara', at: [160, 290], face: 'right' },
}, },
@@ -1279,13 +1303,13 @@ export const story = {
id: 'al_ponte', name: 'a sinistra, verso il campo sportivo', box: [0, 216, 60, 100], at: [12, 318], exit: { scene: 'ponte_sieve', at: [300, 350], face: 'up' }, id: 'al_ponte', name: 'a sinistra, verso il campo sportivo', box: [0, 216, 60, 100], at: [12, 318], exit: { scene: 'ponte_sieve', at: [300, 350], face: 'up' },
}, },
{ {
id: 'alla_pieve', name: 'dietro la farmacia, verso la Pieve', box: [470, 180, 120, 60], at: [560, 240], exit: { scene: 'provinciale_pignone', at: [300, 345], face: 'up' }, id: 'alla_pieve', name: 'dietro la farmacia, verso la Pieve', box: [420, 170, 118, 70], at: [480, 242], exit: { scene: 'provinciale_pignone', at: [300, 345], face: 'up' },
}, },
{ {
id: 'area_feste', name: 'qui sotto, verso l\'area feste', box: [0, 318, 300, 42], at: [120, 352], exit: { scene: 'area_feste', at: [44, 350], face: 'up' }, id: 'area_feste', name: 'qui sotto, verso l\'area feste', box: [0, 318, 300, 42], at: [120, 352], exit: { scene: 'area_feste', at: [44, 350], face: 'up' },
}, },
{ {
id: 'al_comune', name: 'verso il Comune', box: [592, 140, 48, 96], at: [622, 244], exit: { scene: 'comune', at: [320, 350], face: 'up' }, id: 'al_comune', name: 'verso il Comune', box: [540, 40, 100, 198], at: [622, 244], exit: { scene: 'comune', at: [320, 350], face: 'up' },
}, },
{ {
id: 'alla_calimara', name: 'Via Calimara, verso la Piazza', box: [520, 260, 120, 100], at: [600, 330], exit: { scene: 'via_calimara', at: [300, 350], face: 'up' }, id: 'alla_calimara', name: 'Via Calimara, verso la Piazza', box: [520, 260, 120, 100], at: [600, 330], exit: { scene: 'via_calimara', at: [300, 350], face: 'up' },
@@ -1307,6 +1331,31 @@ export const story = {
look: [{ say: 'Cartelli per la Pieve, per la stazione, per Firenze. Per il palio, nessuno.' }], look: [{ say: 'Cartelli per la Pieve, per la stazione, per Firenze. Per il palio, nessuno.' }],
}, },
}, },
{
// L'edicola, col chiosco chiuso: qui Andrea comprava le cassette per il Commodore 64 (Andrea, 28 ottobre).
id: 'edicola', name: 'edicola', box: [20, 160, 92, 100], at: [120, 278], face: 'left',
actions: {
look: [
{ say: 'L\'edicola. Qui compravo le cassette per il Commodore 64: trenta giochi a mille lire, diceva la copertina.' },
{ say: 'Si infilava la cassetta, si scriveva LOAD, si premeva play... e si aspettava dieci minuti col registratore che cigolava.' },
{ say: 'Poi, quando andava bene, partiva il gioco. Quando andava male: LOAD ERROR. E si riavvolgeva tutto. Bei tempi.' },
],
open: [{ say: 'Chiusa. E comunque le cassette non le fanno più. Neanche i Commodore.' }],
use: [{ say: 'Vorrei un Topolino e trenta giochi a mille lire. ...Niente, parlo con una serranda.' }],
},
},
{
// La banca all'angolo di Via del Pignone: il bancomat non funziona quasi mai (Andrea, 28 ottobre).
id: 'banca', name: 'banca', box: [368, 166, 80, 50], at: [400, 236], face: 'up',
actions: {
look: [
{ say: 'La banca. Il bancomat funziona solo il mercoledì delle settimane pari, negli anni bisestili, quando Venere si allinea a Plutone.' },
{ say: 'Oggi? Not today.' },
],
open: [{ say: 'Chiusa. Come il bancomat, del resto.' }],
use: [{ say: 'Carta dentro, carta fuori. «Servizio momentaneamente non disponibile». Momentaneamente dal 1998.' }],
},
},
{ {
id: 'alla_pieve', name: 'in fondo, verso la Pieve', box: [236, 150, 64, 64], at: [268, 216], exit: { scene: 'pieve', at: [24, 312], face: 'right' }, id: 'alla_pieve', name: 'in fondo, verso la Pieve', box: [236, 150, 64, 64], at: [268, 216], exit: { scene: 'pieve', at: [24, 312], face: 'right' },
}, },
@@ -1314,7 +1363,7 @@ export const story = {
id: 'via_pignone', name: 'a destra, Via del Pignone', box: [440, 196, 100, 56], at: [530, 245], face: 'right', exit: { scene: 'via_pignone', at: [300, 345], face: 'up' }, id: 'via_pignone', name: 'a destra, Via del Pignone', box: [440, 196, 100, 56], at: [530, 245], face: 'right', exit: { scene: 'via_pignone', at: [300, 345], face: 'up' },
}, },
{ {
id: 'alla_rotonda', name: 'indietro, verso la rotonda della farmacia', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'via_provinciale', at: [560, 240], face: 'left' }, id: 'alla_rotonda', name: 'indietro, verso la rotonda della farmacia', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'via_provinciale', at: [480, 246], face: 'down' },
}, },
], ],
}, },
@@ -1343,7 +1392,8 @@ export const story = {
}, },
], ],
}, },
// Piazzetta Cambray Digny, in cima a Via del Pignone, sotto Villa Schifanoia: a sinistra Via de' Medici, a diritto la Piazza. // Piazzetta Cambray Digny, in cima a Via del Pignone, sotto Villa Schifanoia: a sinistra Via de' Medici, a diritto
// Via dell'Antica Posta che scende in Piazza, a destra del palazzo la scalinata che scende al Comune (Andrea, 28 ottobre).
piazzetta: { piazzetta: {
name: 'Piazzetta Cambray Digny', name: 'Piazzetta Cambray Digny',
map: [40, 38], map: [40, 38],
@@ -1366,16 +1416,188 @@ export const story = {
}, },
}, },
{ {
id: 'alla_piazza', name: 'a diritto, verso la Piazza', box: [470, 226, 70, 48], at: [510, 274], exit: { scene: 'piazza', at: [92, 250], face: 'right' }, id: 'alla_piazza', name: 'a diritto, Via dell\'Antica Posta verso la Piazza', box: [470, 226, 70, 48], at: [510, 274], exit: { scene: 'antica_posta_alta', at: [235, 230], face: 'down' },
}, },
{ {
id: 'via_de_medici', name: 'a sinistra, Via de\' Medici', box: [0, 280, 40, 80], at: [20, 320], face: 'left', exit: { scene: 'via_de_medici', at: [300, 348], face: 'up' }, id: 'via_de_medici', name: 'a sinistra, Via de\' Medici', box: [0, 100, 60, 260], at: [20, 320], face: 'left', exit: { scene: 'via_de_medici', at: [300, 348], face: 'up' },
},
{
id: 'giu_scalinata', name: 'a destra, giù per la scalinata verso il Comune', box: [541, 226, 40, 60], at: [560, 285], exit: { scene: 'scalinata', at: [560, 325], face: 'down' },
}, },
{ {
id: 'giu_pignone', name: 'giù per Via del Pignone', box: [570, 300, 70, 60], at: [610, 345], exit: { scene: 'via_pignone', at: [343, 212], face: 'down' }, id: 'giu_pignone', name: 'giù per Via del Pignone', box: [570, 300, 70, 60], at: [610, 345], exit: { scene: 'via_pignone', at: [343, 212], face: 'down' },
}, },
], ],
}, },
// Via dell'Antica Posta (foto di Andrea, 28 ottobre): dalla Piazza, a sinistra, sale verso Piazzetta Cambray Digny.
// La Piazza è alle spalle; a sinistra, coi tre scalini, la Piazzetta del Comune; in fondo si sale.
via_antica_posta: {
name: 'Via dell\'Antica Posta',
map: [35, 41],
background: 'assets/scenes/via-antica-posta.png',
walk: [[0, 360], [0, 300], [120, 290], [220, 252], [300, 240], [372, 226], [396, 226], [470, 252], [630, 300], [640, 360]],
depth: [[226, 0.35], [360, 1.15]],
entry: [320, 345],
hotspots: [
{
id: 'colonnine', name: 'colonnine con le catene', box: [225, 225, 95, 68], at: [290, 300], face: 'left',
actions: {
look: [{ say: 'Le colonnine con le catene: le macchine di qua non passano.' }, { say: 'Io sì. Scavalcare le catene è uno sport che pratico da quando avevo sei anni.' }],
},
},
{
id: 'tenda', name: 'tenda a righe', box: [508, 182, 30, 28], at: [500, 268], face: 'right',
actions: {
look: [{ say: 'Una tenda a righe bianche e rosse. Fa tanto spiaggia: peccato che il mare sia di là dall\'Appennino.' }],
},
},
{
id: 'muro_edera', name: 'muro con l\'edera', box: [546, 140, 94, 150], at: [570, 296], face: 'right',
actions: {
look: [{ say: 'Il muro di sasso con l\'edera e la ringhiera in cima. Lassù c\'è il verde della villa.' }],
},
},
{
id: 'alla_piazzetta_comune', name: 'a sinistra, il Comune', box: [0, 90, 200, 204], at: [130, 292], face: 'left', exit: { scene: 'piazzetta_comune', at: [260, 350], face: 'up' },
},
{
id: 'su_antica_posta', name: 'su, in fondo a Via dell\'Antica Posta', box: [350, 200, 70, 30], at: [384, 230], exit: { scene: 'antica_posta_alta', at: [240, 350], face: 'up' },
},
{
id: 'alla_piazza', name: 'indietro, la Piazza', box: [0, 334, 640, 26], at: [320, 354], exit: { scene: 'piazza', at: [92, 250], face: 'right' },
},
],
enter: [{ if: '!antica_posta_vista', then: [{ set: 'antica_posta_vista' }, { say: 'Via dell\'Antica Posta. Chissà se la posta antica arrivava prima di quella moderna.' }] }],
},
// La Piazzetta del Comune, dietro il municipio: la torre dell'orologio, le sculture sui pilastrini di mattoni.
piazzetta_comune: {
name: 'Piazzetta del Comune',
map: [32, 39],
background: 'assets/scenes/piazzetta-comune.png',
walk: [[0, 360], [0, 318], [130, 292], [470, 288], [540, 300], [520, 360]],
depth: [[288, 0.55], [360, 1.15]],
entry: [260, 350],
hotspots: [
{
id: 'torre', name: 'torre dell\'orologio', box: [264, 16, 58, 104], at: [290, 294], face: 'up',
actions: {
look: [{ say: 'La torre dell\'orologio, vista dal retro. Anche da qui segna un\'ora tutta sua.' }],
},
},
{
id: 'porta_comune', name: 'porta del Comune', box: [286, 238, 26, 50], at: [298, 292], face: 'up',
actions: {
look: [{ say: 'La porta del Comune. Dal retro, ma sempre Comune.' }],
open: [{ say: 'Chiusa. Anche dalla porta di dietro, senza un modulo non si entra.' }],
},
},
{
id: 'scultura', name: 'scultura alta', box: [264, 248, 22, 68], at: [250, 320], face: 'right',
actions: {
look: [{ say: 'Una scultura moderna, lunga e appuntita. Secondo me è un\'antenna per parlare coi marziani.' }],
},
},
{
id: 'sasso', name: 'scultura a sasso', box: [92, 276, 42, 54], at: [150, 330], face: 'left',
actions: {
look: [{ say: 'Questa sembra un sasso. Ma sta su un piedistallo, quindi è arte.' }],
take: [{ say: 'Pesa più di me. E poi l\'arte non si porta via in tasca.' }],
},
},
{
id: 'vaso_fiori', name: 'vaso di fiori', box: [520, 264, 106, 82], at: [505, 330], face: 'right',
actions: {
look: [{ say: 'Un vasone di fiori. Qualcuno se ne prende cura, al Comune.' }],
},
},
{
id: 'alla_via', name: 'indietro, Via dell\'Antica Posta', box: [0, 336, 520, 24], at: [260, 354], exit: { scene: 'via_antica_posta', at: [140, 294], face: 'right' },
},
],
},
// In cima a Via dell'Antica Posta, prima della Piazzetta: il muro della villa con l'edera, il ponticello sopra la
// strada, il giardinetto con le panchine e due gatti al sole. A sinistra, sotto il ponticello, la scalinata (Andrea).
antica_posta_alta: {
name: 'In cima a Via dell\'Antica Posta',
map: [38, 39],
background: 'assets/scenes/via-antica-posta-alta.png',
walk: [[0, 360], [0, 322], [190, 226], [283, 222], [470, 360]],
depth: [[222, 0.35], [360, 1.15]],
entry: [240, 350],
hotspots: [
{
id: 'gatti', name: 'gatti', box: [490, 236, 100, 28], at: [420, 330], face: 'right',
actions: {
look: [{ say: 'Due gatti che dormono sull\'erba. Il Palio, per loro, può anche aspettare.' }],
talk: [{ say: 'Micio, micio... Niente. Per meno di una scatoletta non aprono neanche un occhio.' }],
take: [{ say: 'Svegliare un gatto che dorme al sole? Non sono così coraggioso.' }],
},
},
{
id: 'panchine', name: 'panchine', box: [358, 212, 130, 44], at: [375, 300], face: 'right',
actions: {
look: [{ say: 'Due panchine all\'ombra del muro. Un posto per chi non ha un palio da ritrovare.' }],
use: [{ say: 'Mi siederei volentieri, ma il Palio non si ritrova da solo.' }],
},
},
{
id: 'ponticello', name: 'ponticello', box: [187, 34, 92, 38], at: [235, 240], face: 'up',
actions: {
look: [{ say: 'Un ponticello sopra la strada, da una casa all\'altra. Per andare dal vicino senza fare le scale.' }],
},
},
{
id: 'muro_villa', name: 'muro della villa', box: [383, 0, 257, 205], at: [330, 280], face: 'right',
actions: {
look: [{ say: 'Il muro di Villa Schifanoia, alto alto, con l\'edera che ormai lo tiene su lei.' }],
},
},
{
id: 'su_piazzetta', name: 'su, Piazzetta Cambray Digny', box: [250, 196, 80, 30], at: [280, 226], exit: { scene: 'piazzetta', at: [510, 276], face: 'down' },
},
{
id: 'alla_scalinata', name: 'a sinistra, giù per la scalinata verso il Comune', box: [150, 190, 60, 36], at: [195, 228], exit: { scene: 'scalinata', at: [470, 312], face: 'down' },
},
{
id: 'giu_antica_posta', name: 'giù, Via dell\'Antica Posta', box: [0, 334, 470, 26], at: [240, 354], exit: { scene: 'via_antica_posta', at: [384, 234], face: 'down' },
},
],
},
// La scalinata (foto di Andrea, 28 ottobre): un cortile coi garage; su per le scale la Piazzetta Cambray Digny,
// alle spalle i giardini e il retro del Comune.
scalinata: {
name: 'La scalinata della Piazzetta',
map: [30, 42],
background: 'assets/scenes/scalinata-piazzetta.png',
walk: [[0, 360], [0, 290], [100, 282], [380, 278], [400, 300], [620, 330], [640, 360]],
depth: [[278, 0.55], [360, 1.15]],
entry: [300, 350],
hotspots: [
{
id: 'saracinesca', name: 'saracinesca', box: [268, 196, 62, 79], at: [300, 290], face: 'up',
actions: {
look: [{ say: 'Una saracinesca color ruggine. Dietro, scommetto, una macchina e trent\'anni di roba da buttare.' }],
open: [{ say: 'Chiusa. E se la apro io, mi tocca anche fare ordine.' }],
},
},
{
id: 'panni', name: 'panni stesi', box: [110, 108, 34, 30], at: [130, 290], face: 'up',
actions: {
look: [{ say: 'Panni stesi al sole. Se piove, qui lo sanno tutti prima del telegiornale.' }],
take: [{ say: 'Rubare i panni stesi? Mi conoscono tutti, mi ritroverebbero prima del Palio.' }],
},
},
{
id: 'su_antica_posta', name: 'su per le scale, poi a sinistra verso Via dell\'Antica Posta', box: [440, 196, 90, 70], at: [470, 312], face: 'up', exit: { scene: 'antica_posta_alta', at: [195, 228], face: 'right' },
},
{
id: 'su_piazzetta', name: 'su per le scale, poi a destra verso Piazzetta Cambray Digny', box: [530, 196, 80, 70], at: [560, 325], face: 'up', exit: { scene: 'piazzetta', at: [560, 285], face: 'left' },
},
{
id: 'al_comune', name: 'indietro, i giardini del Comune', box: [0, 334, 640, 26], at: [300, 354], exit: { scene: 'comune', at: [40, 300], face: 'right' },
},
],
},
// L'area feste (il parco Antonio Berti): dalla rotonda del River, a sinistra. // L'area feste (il parco Antonio Berti): dalla rotonda del River, a sinistra.
area_feste: { area_feste: {
name: 'Area feste', name: 'Area feste',
@@ -2102,7 +2324,7 @@ export const story = {
}, },
}, },
{ {
id: 'verso_piazza', name: 'Via de\' Medici, verso la Piazza', box: [20, 215, 50, 40], at: [45, 250], exit: { scene: 'via_de_medici', at: [300, 216], face: 'left' }, id: 'verso_piazza', name: 'Via de\' Medici, verso la Piazza', box: [20, 215, 50, 40], at: [45, 250], exit: { scene: 'via_de_medici', at: [300, 216], face: 'down' },
}, },
{ {
id: 'verso_pieve', name: 'discesa verso la Pieve', box: [560, 236, 60, 38], at: [600, 266], exit: { scene: 'pieve', at: [600, 350], face: 'left' }, id: 'verso_pieve', name: 'discesa verso la Pieve', box: [560, 236, 60, 38], at: [600, 266], exit: { scene: 'pieve', at: [600, 350], face: 'left' },
@@ -2275,6 +2497,9 @@ export const story = {
{ {
id: 'all_area_camper', name: 'a sinistra, verso l\'area camper', box: [320, 160, 110, 42], at: [442, 202], exit: { scene: 'area_camper', at: [600, 330], face: 'left' }, id: 'all_area_camper', name: 'a sinistra, verso l\'area camper', box: [320, 160, 110, 42], at: [442, 202], exit: { scene: 'area_camper', at: [600, 330], face: 'left' },
}, },
{
id: 'al_giannini', name: 'in fondo a sinistra, verso il campo del Giannini', box: [380, 165, 58, 30], at: [440, 200], exit: { scene: 'campo_giannini', at: [480, 218], face: 'left' },
},
{ {
id: 'alla_stazione', name: 'a destra, verso la stazione', box: [486, 160, 90, 40], at: [474, 202], exit: { scene: 'stazione', at: [20, 300], face: 'right' }, id: 'alla_stazione', name: 'a destra, verso la stazione', box: [486, 160, 90, 40], at: [474, 202], exit: { scene: 'stazione', at: [20, 300], face: 'right' },
}, },
@@ -2303,9 +2528,6 @@ export const story = {
look: [{ say: 'Le villette rosa coi balconi bianchi. Da qui a casa mia è un attimo.' }], look: [{ say: 'Le villette rosa coi balconi bianchi. Da qui a casa mia è un attimo.' }],
}, },
}, },
{
id: 'al_giannini', name: 'il campo del Giannini', box: [140, 160, 260, 70], at: [250, 238], exit: { scene: 'campo_giannini', at: [320, 350], face: 'up' },
},
{ {
id: 'a_cardetole', name: 'verso casa, a Cardetole', box: [0, 230, 80, 130], at: [20, 300], exit: { scene: 'cardetole', at: [300, 350], face: 'up' }, id: 'a_cardetole', name: 'verso casa, a Cardetole', box: [0, 230, 80, 130], at: [20, 300], exit: { scene: 'cardetole', at: [300, 350], face: 'up' },
}, },
@@ -2314,7 +2536,8 @@ export const story = {
}, },
], ],
}, },
// Il prato del parco dei camper visto dall'altra parte: per i ragazzi, «il campo del Giannini». // Il prato del parco dei camper visto dall'altra parte: per i ragazzi, «il campo del Giannini». Ci si arriva dal
// ponticino della Carza; niente uscita verso l'area camper, perché è già questa (Andrea, 28 ottobre).
campo_giannini: { campo_giannini: {
name: 'Il campo del Giannini', name: 'Il campo del Giannini',
map: [82, 96], map: [82, 96],
@@ -2337,7 +2560,7 @@ export const story = {
}, },
}, },
{ {
id: 'al_parco', name: 'indietro, verso l\'area camper', box: [0, 330, 640, 30], at: [320, 354], exit: { scene: 'area_camper', at: [250, 238], face: 'down' }, id: 'al_ponticino', name: 'verso il ponticino della Carza', box: [440, 150, 90, 60], at: [480, 216], exit: { scene: 'ponticino', at: [444, 204], face: 'right' },
}, },
], ],
}, },
@@ -2365,6 +2588,7 @@ export const story = {
use: [{ say: 'Arrampicarmi? Alla mia età, e davanti a tutto il condominio?' }], use: [{ say: 'Arrampicarmi? Alla mia età, e davanti a tutto il condominio?' }],
}, },
}, },
// Si compare a destra dell'area camper: inquadrature invertite rispetto alle foto (Andrea, 28 ottobre, va bene così).
{ {
id: 'alla_pieve', name: 'su verso la Pieve', box: [0, 330, 640, 30], at: [300, 354], exit: { scene: 'area_camper', at: [600, 330], face: 'left' }, id: 'alla_pieve', name: 'su verso la Pieve', box: [0, 330, 640, 30], at: [300, 354], exit: { scene: 'area_camper', at: [600, 330], face: 'left' },
}, },
@@ -2484,7 +2708,7 @@ export const story = {
}, },
}, },
{ {
id: 'verso_fortezza', name: 'viale dei cipressi, verso la Fortezza', box: [0, 250, 110, 110], at: [92, 345], exit: { scene: 'viale_cipressi', at: [300, 256], face: 'left' }, id: 'verso_fortezza', name: 'viale dei cipressi, verso la Fortezza', box: [0, 250, 110, 110], at: [92, 345], exit: { scene: 'viale_cipressi', at: [300, 256], face: 'down' },
}, },
], ],
}, },
@@ -2695,6 +2919,28 @@ export const story = {
}, },
}, },
// Mariano al pallaio: una mano a bocce, solo per gloria; se Andrea vince, la promessa di Brento Sanico. // Mariano al pallaio: una mano a bocce, solo per gloria; se Andrea vince, la promessa di Brento Sanico.
// I ragazzi delle canne: «S'ha andà a fallo?!?» (Andrea, 28 ottobre) e si va a pescare.
pescatori: {
nodes: {
start: {
options: [
{ text: 'S\'ha andà a fallo?!?', next: 'livello', then: [
{ say: 'Gnamo! La canna ti si dà noi.', who: 'pescatori', pose: 'laugh' },
{ say: 'Come peschi?', who: 'pescatori', pose: 'talk' },
] },
{ text: 'Quanti ne prendete, di solito?', then: [{ say: 'Dipende da chi racconta. Il Nanni dice dieci, il cestino dice due.', who: 'pescatori', pose: 'laugh' }] },
{ text: 'Buona pesca, ragazzi.', next: 'end', then: [{ say: 'Se vedi il palio, salutacelo!', who: 'pescatori', pose: 'talk' }] },
],
},
livello: {
options: [
{ text: 'Alla bona, tanto per bagnare la lenza.', next: 'end', then: [{ say: 'Alla bona. Due pesci e si va a casa contenti.', who: 'pescatori' }, { closeup: 'pesca', level: 'bona' }] },
{ text: 'Seria.', next: 'end', then: [{ say: 'Seria: tre pesci, e lo stivale non conta.', who: 'pescatori', pose: 'proud' }, { closeup: 'pesca', level: 'seria' }] },
{ text: 'A fassi male.', next: 'end', then: [{ say: 'A fassi male? Quattro pesci, e le carpe tirano come trattori.', who: 'pescatori', pose: 'laugh' }, { closeup: 'pesca', level: 'male' }] },
],
},
},
},
// Da solo al campo del Circolo: Andrea decide quanto farsi male. // Da solo al campo del Circolo: Andrea decide quanto farsi male.
basket: { basket: {
nodes: { nodes: {
+1 -1
View File
@@ -1,5 +1,5 @@
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json. // La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
export const VERSION = '0.1.21'; export const VERSION = '0.1.28';
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica, // Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica,
// mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf). // mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf).
+4 -4
View File
@@ -53,16 +53,16 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
.bocce-adjust button { min-height: 44px; padding: 8px 12px; border: 1px solid #897b57; border-radius: 4px; background: #38453c; color: #f2e1bf; cursor: pointer; font-size: 11px; } .bocce-adjust button { min-height: 44px; padding: 8px 12px; border: 1px solid #897b57; border-radius: 4px; background: #38453c; color: #f2e1bf; cursor: pointer; font-size: 11px; }
#bocce-throw { background: #ead09a; color: #202b25; min-width: 100px; } #bocce-throw { background: #ead09a; color: #202b25; min-width: 100px; }
.bocce-adjust button:disabled { opacity: .4; cursor: default; } .bocce-adjust button:disabled { opacity: .4; cursor: default; }
#bocce-help, #biliardo-help, #basket-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; } #bocce-help, #biliardo-help, #basket-help, #pesca-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
#bocce-help summary, #biliardo-help summary, #basket-help summary { cursor: pointer; padding: 6px 0; } #bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary { cursor: pointer; padding: 6px 0; }
#bocce-help p, #biliardo-help p, #basket-help p { max-width: 76em; line-height: 1.7; } #bocce-help p, #biliardo-help p, #basket-help p, #pesca-help p { max-width: 76em; line-height: 1.7; }
@media (max-width: 600px), (orientation: landscape) and (max-height: 520px) { @media (max-width: 600px), (orientation: landscape) and (max-height: 520px) {
#bocce-controls { padding: 8px; font-size: 10px; overflow-y: auto; min-height: 0; } #bocce-controls { padding: 8px; font-size: 10px; overflow-y: auto; min-height: 0; }
.bocce-adjust { flex-wrap: wrap; gap: 6px; } .bocce-adjust { flex-wrap: wrap; gap: 6px; }
.bocce-adjust label { order: -1; flex-basis: 100%; max-width: none; } .bocce-adjust label { order: -1; flex-basis: 100%; max-width: none; }
.bocce-adjust button { flex: 1; font-size: 9px; padding: 6px; } .bocce-adjust button { flex: 1; font-size: 9px; padding: 6px; }
#bocce-throw { flex-basis: 100%; } #bocce-throw { flex-basis: 100%; }
#bocce-help, #biliardo-help, #basket-help { font-size: 9px; } #bocce-help, #biliardo-help, #basket-help, #pesca-help { font-size: 9px; }
} }
.effects-control { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; font-size: clamp(9px, 1.2vw, 13px); } .effects-control { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; font-size: clamp(9px, 1.2vw, 13px); }
#effects-volume { width: 130px; accent-color: var(--accent); } #effects-volume { width: 130px; accent-color: var(--accent); }
+32
View File
@@ -483,6 +483,38 @@ try {
await k.close(); await k.close();
} }
// La pesca al lago dell'Anatre: coi ragazzi in Fortezza «S'ha andà a fallo?!?», poi il lancio tenendo premuto e l'attesa.
{
const k = await open({ width: 1280, height: 860 });
await k.click('#splash');
await k.click('#new-game');
await k.click('#rione [data-rione="campomigliaio"]');
await k.click('#skip-intro');
await settle(k);
await k.evaluate(() => game.go('fortezza', [240, 334]));
await settle(k);
await clickScene(k, await hotspotCenter(k, 'pescatori'));
await k.waitForFunction(() => game.dialogue && !game.busy, null, { timeout: 40000 });
await k.locator('#choices li', { hasText: 'S’ha andà a fallo?!?' }).or(k.locator('#choices li', { hasText: "S'ha andà a fallo?!?" })).first().click();
await k.waitForFunction(() => game.options().some(o => o.text === 'Seria.'), null, { timeout: 30000 });
await k.locator('#choices li', { hasText: 'Seria.' }).click();
await k.waitForFunction(() => game.closeup?.id === 'pesca' && game.closeup.step === 'aim' && !game.speech, null, { timeout: 30000 });
const r = await k.locator('#scene').boundingBox();
await k.mouse.move(r.x + r.width * 0.7, r.y + r.height * 0.7);
assert.ok(await k.evaluate(() => game.closeup.power > 0.5), 'toccando l\'acqua al largo la forza sale');
await k.mouse.down();
await k.waitForTimeout(500);
assert.ok(await k.evaluate(() => game.closeup.holding && game.closeup.power > 0.2), 'tenendo premuto si carica il lancio');
await k.mouse.up();
await k.waitForFunction(() => game.closeup.step === 'wait', null, { timeout: 5000 });
await k.waitForTimeout(600);
await k.screenshot({ path: `${OUT}/03g-pesca-attesa.png` });
assert.equal(await k.textContent('#bocce-throw'), 'Ferra');
await k.keyboard.press('Escape');
await k.waitForFunction(() => game.speech?.text.startsWith('Lascio la canna'), null, { timeout: 5000 });
await k.close();
}
// Il biliardo del Circolo 4 Maggio: la sala, poi la partita; un tocco sul tavolo mira, tenendo premuto la forza sale. // Il biliardo del Circolo 4 Maggio: la sala, poi la partita; un tocco sul tavolo mira, tenendo premuto la forza sale.
{ {
const b = await open({ width: 1280, height: 860 }); const b = await open({ width: 1280, height: 860 });
+1 -1
View File
@@ -15,7 +15,7 @@ test('clic intelligente: parla con le persone, guarda le cose, attraversa le usc
const { g, c, at } = setup(); const { g, c, at } = setup();
assert.equal(c.sentence('vecchietti'), 'Parla con vecchietti'); assert.equal(c.sentence('vecchietti'), 'Parla con vecchietti');
assert.equal(c.sentence('manifesto'), 'Guarda manifesto del Palio'); assert.equal(c.sentence('manifesto'), 'Guarda manifesto del Palio');
assert.equal(c.sentence('verso_piazzetta'), 'Vai a Piazzetta Cambray Digny, verso Via de\' Medici'); assert.equal(c.sentence('verso_piazzetta'), 'Vai a Via dell\'Antica Posta, verso Piazzetta Cambray Digny');
assert.equal(c.clickScene(at('vecchietti')), 'act'); assert.equal(c.clickScene(at('vecchietti')), 'act');
settle(g); settle(g);
assert.equal(g.dialogue?.id, 'vecchietti'); assert.equal(g.dialogue?.id, 'vecchietti');
+185
View File
@@ -0,0 +1,185 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { newPesca, press, release, cancel, update, cast, strike, shoot, setPower, aimScreen, nudge, status, canThrow, canCast, canAdjust,
distanceFor, biting, CASTS, LEVELS, FISHES, NEAR, FAR, BANK, SHORE, WATER, FLY, REST } from '../src/engine/pesca.js';
import { story } from '../src/story/story.js';
import { Game } from '../src/engine/game.js';
import { settle } from './helpers.mjs';
// Come il gioco: a ogni fotogramma gli eventi sonori si consumano (e qui si ricordano).
const heard = new Set();
const run = (c, seconds, dt = 1 / 60) => { for (let t = 0; t < seconds; t += dt) { update(c, dt); for (const e of c.events.splice(0)) heard.add(e.kind); } };
// Lancia e aspetta l'abboccata vera (non i tocchi), poi ferra: la mano ferma dei test.
function hook(c) {
assert.ok(cast(c)); run(c, FLY + 0.05);
assert.equal(c.step, 'wait');
for (let i = 0; i < 60 * 15 && !c.float.bitten; i++) update(c, 1 / 60);
assert.ok(c.float.bitten, 'prima o poi abbocca');
assert.ok(strike(c)); assert.equal(c.step, 'fight');
}
// Recupera con giudizio: tiene premuto finché la lenza non è quasi al limite, poi molla un attimo.
function land(c) {
for (let i = 0; i < 60 * 90 && c.step === 'fight'; i++) {
if (c.tension > 0.8 && c.reeling) release(c);
else if (c.tension < 0.55 && !c.reeling) press(c);
update(c, 1 / 60); for (const e of c.events.splice(0)) heard.add(e.kind);
}
assert.notEqual(c.step, 'fight', 'il recupero finisce');
}
test('la forza sale finché si tiene premuto; un tocco breve non spreca il lancio; dove si tocca l\'acqua si lancia', () => {
const c = newPesca(1);
assert.ok(press(c)); update(c, 0.05); assert.ok(release(c));
assert.equal(c.casts, 0); assert.equal(c.power, 0.5);
assert.ok(aimScreen(c, [FAR, 250])); assert.equal(c.power, 1);
assert.ok(aimScreen(c, [NEAR, 250])); assert.equal(c.power, 0);
assert.ok(!aimScreen(c, [BANK - 20, 250]), 'sulla riva non si lancia');
assert.ok(!aimScreen(c, [400, 60]), 'il cielo non è acqua');
assert.ok(nudge(c, 1)); assert.ok(Math.abs(c.power - 0.05) < 1e-9);
setPower(c, 0.5);
assert.ok(press(c)); update(c, 0.7); assert.ok(release(c));
assert.equal(c.casts, 1); assert.equal(c.step, 'cast');
assert.ok(Math.abs(c.float.x - distanceFor(c.power)) < 1e-9);
assert.ok(c.events.some(e => e.kind === 'pesca-lancio'));
assert.ok(!setPower(c, 0.2), 'a lenza in acqua la forza non si tocca');
});
test('il galleggiante: i tocchi finti abbassano appena, l\'abboccata lo porta sotto; ferrare a vuoto perde il lancio', () => {
const c = newPesca(2); cast(c); run(c, FLY + 0.05);
const f = c.float;
assert.ok(f.nibbles.length >= 1 && f.nibbles.every(n => n.at < f.biteAt), 'i tocchi vengono prima');
assert.ok(strike(c)); assert.equal(c.step, 'settle'); assert.equal(c.how, 'empty');
assert.match(c.last.text, /a vuoto|spaventato/);
run(c, REST + 0.1); assert.equal(c.step, 'aim'); assert.equal(c.casts, 1);
const d = newPesca(3); cast(d); run(d, FLY + 0.05);
const g = d.float;
for (let i = 0; i < 60 * 15 && !(g.t >= g.nibbles[0].at && g.t < g.nibbles[0].at + g.nibbles[0].for); i++) update(d, 1 / 60);
assert.ok(g.dip > 0 && g.dip < 5, `un tocco finto: ${g.dip}`);
for (let i = 0; i < 60 * 15 && !g.bitten; i++) update(d, 1 / 60);
assert.equal(g.dip, 7, 'abboccata: sotto');
assert.ok(d.events.some(e => e.kind === 'pesca-abbocca'));
});
test('non ferrata in tempo, l\'abboccata passa; alla terza il pesce se ne va', () => {
const c = newPesca(4, { level: 'male' }); cast(c); run(c, FLY + 0.05);
for (let i = 0; i < 60 * 15 && !c.float.bitten; i++) update(c, 1 / 60);
// Il segnale e la finestra coincidono: finché dice «Ferra!» si ferra; dopo, lo sta sputando.
assert.ok(biting(c) && c.float.dip === 7 && /Ferra!/.test(status(c)));
run(c, c.skill.window + 0.05);
assert.ok(!biting(c) && c.float.bitten && c.float.dip === 3 && /sputando/.test(status(c)));
const late = structuredClone(c); strike(late);
assert.equal(late.last.kind, 'late'); assert.match(late.last.text, /tardi/);
run(c, 0.5);
assert.ok(!c.float.bitten && c.float.missed === 1, 'persa la prima');
assert.equal(c.step, 'wait');
assert.match(status(c), /2 abboccate ancora/);
run(c, 60);
assert.equal(c.how, 'empty', 'dopo tre, o dopo quaranta secondi, si ricupera a vuoto');
assert.equal(c.last.kind, 'gone'); assert.equal(c.step, 'aim');
});
test('ferrato in tempo si combatte: tenendo premuto la lenza tira e il pesce si avvicina, mollando si allontana; troppo tesa si spezza', () => {
const c = newPesca(5, { level: 'bona' }); setPower(c, 0.5); hook(c);
assert.ok(c.fish && c.fish.dist === 1);
const x0 = c.float.x;
press(c); run(c, 1.5);
assert.ok(c.float.x < x0 && c.float.x > SHORE, `il galleggiante viene verso riva senza salti: ${x0} → ${c.float.x}`);
assert.ok(c.fish.dist < 1 && c.tension > 0.3, 'recuperando si avvicina');
const d0 = c.fish.dist;
release(c); run(c, 1.5);
assert.ok(c.fish.dist > d0 - 0.001, 'mollando non si avvicina');
land(c);
assert.equal(c.how, 'landed', 'con giudizio si tira a riva');
assert.equal(c.caught.length, 1); assert.ok(c.weight > 0);
assert.ok(heard.has('pesca-preso'));
// A fassi male, una carpa al largo tenuta sempre tesa spezza la lenza.
const d = newPesca(6, { level: 'male' }); setPower(d, 1); hook(d);
d.fish = { ...FISHES.at(-1), dist: 1, energy: 1, burst: 0, reel: 0.11 };
press(d); run(d, 8);
assert.equal(d.how, 'broken', `invece: ${d.step} ${d.how}`);
assert.ok(heard.has('pesca-rottura'));
});
test('un gesto solo: premendo all\'abboccata si ferra e si recupera; il pannello alterna recupera e molla; il sesto lancio si può finire', () => {
const c = newPesca(21, { level: 'bona' }); setPower(c, 0.3); cast(c); run(c, FLY + 0.05);
for (let i = 0; i < 60 * 15 && !c.float.bitten; i++) update(c, 1 / 60);
assert.ok(press(c)); assert.equal(c.step, 'fight'); assert.ok(c.reeling, 'ferrato e già in recupero');
assert.ok(release(c)); assert.ok(!c.reeling);
assert.ok(shoot(c)); assert.ok(c.reeling, 'dal pannello: recupera'); assert.ok(shoot(c)); assert.ok(!c.reeling, 'dal pannello: molla');
const d = newPesca(22); d.casts = CASTS - 1;
assert.ok(canCast(d)); cast(d); run(d, FLY + 0.05);
assert.ok(!canCast(d) && canThrow(d), 'al sesto lancio si può ancora ferrare');
assert.ok(!canAdjust(d), 'ma non regolare la distanza');
for (let i = 0; i < 60 * 15 && !d.float.bitten; i++) update(d, 1 / 60);
assert.ok(press(d)); assert.equal(d.step, 'fight'); assert.ok(canThrow(d), 'e recuperare');
d.fish = { ...FISHES[1], dist: 1, energy: 1, burst: 0, reel: 0.2 }; land(d);
assert.equal(d.how, 'landed');
assert.ok(d.result, 'il risultato è deciso appena il pesce è a riva, prima della posa');
run(d, REST + 0.1); assert.equal(d.step, 'result');
});
test('la lenza si spezza davvero: tenendo sempre premuto, a seria un cavedano o più grosso la rompe, una lasca no', () => {
const fight = (kind, level) => { const c = newPesca(23, { level }); setPower(c, 0.5); hook(c); c.fish = { ...FISHES.find(f => f.kind === kind), dist: 1, energy: 1, burst: 0, reel: 0.15 }; press(c); run(c, 20); return c.how; };
assert.equal(fight('lasca', 'seria'), 'landed');
assert.equal(fight('cavedano', 'seria'), 'broken');
assert.equal(fight('carpa', 'bona'), 'broken');
assert.equal(fight('cavedano', 'bona'), 'landed');
// Mollata e ripresa, la stessa carpa viene a riva.
const c = newPesca(24, { level: 'male' }); setPower(c, 1); hook(c); c.fish = { ...FISHES.at(-1), dist: 1, energy: 1, burst: 0, reel: 0.11 }; land(c);
assert.equal(c.how, 'landed');
});
test('al largo i pesci sono più grossi; lo stivale solo sotto riva e non conta per la gloria', () => {
const near = new Set(), far = new Set();
for (let seed = 1; seed < 40; seed++) {
for (const [power, set] of [[0, near], [1, far]]) {
const c = newPesca(seed); setPower(c, power); hook(c); set.add(c.fish.kind);
}
}
assert.ok(far.has('carpa') || far.has('tinca'), `al largo: ${[...far]}`);
assert.ok(!near.has('carpa'), 'sotto riva niente carpe');
assert.ok(!far.has('stivale'), 'lo stivale sta sotto riva');
const c = newPesca(7, { level: 'bona' }); c.caught = [{ ...FISHES[0] }, { ...FISHES[1] }]; c.weight = 1;
assert.match(status(c), /Pesci 1 /, 'lo stivale non è un pesce');
const d = newPesca(25); setPower(d, 0); hook(d); d.fish = { ...FISHES[0], dist: 1, energy: 1, burst: 0, reel: 0.3 }; land(d);
assert.equal(d.weight, 0, 'e non pesa'); assert.match(d.last.text, /stivale/);
});
test('sei lanci, poi il risultato: la gloria dipende dal livello', () => {
for (const [level, { goal }] of Object.entries(LEVELS)) {
const c = newPesca(8, { level }), fishes = [];
for (let i = 0; i < CASTS; i++) {
assert.ok(canThrow(c));
// Finché serve si pesca per davvero (con una lasca docile sotto riva), poi si ricupera a vuoto.
if (c.caught.length < goal) { setPower(c, 0); hook(c); c.fish = { ...FISHES[1], dist: 1, energy: 1, burst: 0, reel: 0.2 }; land(c); assert.equal(c.how, 'landed'); }
else { cast(c); run(c, FLY + 0.05); strike(c); }
run(c, REST + 0.1);
}
assert.equal(c.step, 'result'); assert.ok(!canThrow(c)); assert.ok(!cast(c));
assert.equal(c.result.fish, goal); assert.equal(c.result.winner, 'player', level);
assert.match(status(c), /pesci su 6 lanci/);
}
const miss = newPesca(9, { level: 'bona' });
for (let i = 0; i < CASTS; i++) { cast(miss); run(miss, FLY + 0.05); strike(miss); run(miss, REST + 0.1); }
assert.equal(miss.result.winner, 'cpu');
});
test('nella storia: coi ragazzi in Fortezza «S\'ha andà a fallo?!?», loro accettano, si sceglie e si pesca; la gloria lascia il flag', () => {
const g = new Game(story);
g.go('fortezza', [240, 334]); settle(g);
g.act('talk', 'pescatori'); const said = settle(g).map(s => s.text);
assert.ok(said.some(t => t.includes('mia mamma aveva ragione')), 'la prima volta le canne');
assert.ok(g.dialogue, 'poi si parla');
g.choose(g.options().findIndex(o => o.text === 'S\'ha andà a fallo?!?'));
const yes = settle(g).map(s => `${s.who ?? 'andrea'}: ${s.text}`);
assert.ok(yes.includes('pescatori: Gnamo! La canna ti si dà noi.'));
g.choose(g.options().findIndex(o => o.text === 'Seria.')); settle(g);
assert.equal(g.closeup?.id, 'pesca'); assert.equal(g.closeup.level, 'seria');
g.closeup.result = { winner: 'player', fish: 3, weight: 2.1, goal: 3, caught: [] }; g.closeup.step = 'result';
g.closeupClose(); const after = settle(g).map(s => s.text);
assert.ok(g.state.flags.pesca_vinta);
assert.ok(after.some(t => t.includes('Stasera si frigge')));
// La seconda volta si va dritti al dialogo.
g.act('talk', 'pescatori'); settle(g);
assert.ok(g.dialogue);
});
+4 -2
View File
@@ -5,6 +5,8 @@
"padel": "pàdel", "padel": "pàdel",
"l'A. Bettini": "l'Abbettini", "l'A. Bettini": "l'Abbettini",
"da i' Trappola, ai' Barrettino": "dai Trappola, ai Barrettino", "da i' Trappola, ai' Barrettino": "dai Trappola, ai Barrettino",
"S'ha andà a fallo": "Si va a farlo", "S'ha andà a fallo": "saandà a fallo",
"Ghiacciomobile": "Ghiacciomòbile" "Ghiacciomobile": "Ghiacciomòbile",
"Not today": "Nòt tudèi",
"Commodore": "Commòdòr"
} }