Compare commits

...
28 Commits
Author SHA1 Message Date
manzolo 1205192e8a Versione 0.1.30 2026-10-28 13:16:23 +01:00
manzoloandClaude Opus 5.5 aa2f547856 La salita dei cipressi fra la casa del maestro Bacci e il viale del Trebbio (Andrea)
Sfondo di Codex dalla foto di Andrea: la strada bianca in salita col muretto, il cartello e lassù i cipressi che
spuntano. Alle spalle la casa del maestro Bacci, in fondo il viale dei cipressi, che non sono più collegati
direttamente. Battute provvisorie e voci; paesaggio sonoro del bosco.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdCPptNRwbVzjzHbohi
2026-10-28 13:09:57 +01:00
manzoloandClaude Opus 5.5 57e72de021 make deploy-vps: la stessa immagine già provata da make deploy anche sul VPS (VPS_HOST, VPS_DIR in deploy.local.mk)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdCPptNRwbVzjzHbohi
2026-10-28 12:53:45 +01:00
manzolo 15ae2b8221 Versione 0.1.29 2026-10-28 12:50:24 +01:00
manzoloandClaude Opus 5.5 f895b7979d Il sentiero della Fortezza fra la casa del maestro Bacci e la Fortezza, con la pozza che c'è anche il 15 d'agosto (Andrea)
Sfondo di Codex dalla foto di Andrea: la carrareccia nel bosco con la rete, la scarpata e la pozza in mezzo. Alle
spalle la casa del maestro Bacci, a diritto la Fortezza, che non sono più collegate direttamente. Battute
provvisorie e voci; paesaggio sonoro del bosco.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdCPptNRwbVzjzHbohi
2026-10-28 12:44:06 +01:00
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
manzolo f6fb8922af Versione 0.1.21 2026-10-27 23:56:31 +01:00
manzoloandClaude Fable 5.1 bd7fbc3003 Basket: alla gloria Andrea sfida Jordan, non Cistola (Andrea)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-27 23:50:10 +01:00
manzoloandClaude Fable 5.1 8b957c7cf7 Basket: la revisione di Codex, i punti gravi
Dalla mira iniziale (52°) da sotto non entrava con nessuna forza: ora 64°, forza minima più bassa e prima postazione
un passo indietro, così da ogni postazione entra in una fascia di forza contigua di almeno l'8% (test). Il palo
fermava anche sopra la sua cima e contava come tiro pulito; il tabellone lasciava passare da dietro. Il canestro
conta appena entra (anche il risultato all'ultimo pallone), la pausa fra un pallone e l'altro è più corta, la palla
entrata non rotola fino in fondo. Andrea resta a braccia alzate finché la palla vola (niente doppio pallone), il
rilascio parte sopra la sua testa. La guida si ferma al primo urto e ha un bordo scuro con la punta; il lampo del
canestro si spegne; l'obiettivo del livello è sempre in vista; i microurti sul ferro non tintinnano; dietro Andrea
non si carica; l'aiuto spiega il clic che fissa la mira e le soglie.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-27 23:39:30 +01:00
manzoloandClaude Fable 5.1 f1de62f01e «Fai due tiri» al campo da basket del Circolo 4 Maggio
Minigioco nuovo (idea di Andrea, 27 ottobre): vista di lato, Andrea da solo con dieci palloni; il mouse o il dito
danno l'angolo, tenendo premuto sale la forza, lasciando la palla vola in parabola, rimbalza sul ferro (davanti e
dietro dell'anello), sul tabellone e per terra, entra anche sporca; da sotto non vale. A ogni canestro si arretra
di una postazione fino a dietro la linea da tre (tre punti). Tre livelli: quanti canestri su dieci per la gloria
(3, 5, 7) e quanta parabola mostra la guida (quasi tutta, l'inizio, niente). Sfondo del campo e foglio di Andrea col
pallone di Codex; suoni di palla, ferro, tabellone e retina; stessi comandi delle bocce (pannello, tastiera, pressione);
dialogo di scelta dal campo di Via Cipriani, battute provvisorie e voci; test del motore (da ogni postazione esiste
un tiro che entra) e prova nel browser.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-27 23:16:35 +01:00
manzolo 64dc8e18e7 Versione 0.1.20 2026-10-27 23:02:27 +01:00
manzoloandClaude Fable 5.1 b262977117 Il carosello dei Ringraziamenti si ferma, si sfoglia e ingrandisce le foto
Tutti gli sfondi del paese (game.reelPictures, uno per scena, col nome del posto) e ogni ritratto una volta sola
(prima tre si ripetevano). Scorre da solo ma si ferma sotto il mouse, si trascina col mouse o col dito, si spinge
con la rotella, con le frecce ai lati e con quelle della tastiera; un tocco su una foto la mostra grande col nome
(il tocco si legge al rilascio: con la cattura del puntatore il clic non arriva all'immagine). Test della fila e
prova nel browser.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
2026-10-27 22:57:01 +01:00
115 changed files with 2134 additions and 91 deletions
+13 -1
View File
@@ -24,7 +24,7 @@ CROP ?=
COLORS ?= 64
.PHONY: help serve open test browser verify check-public bump voci voci-elenco sfondi sfondi-genera sfondi-confronto background character \
build up down restart logs ps shell image-size deploy deploy-logs clean distclean
build up down restart logs ps shell image-size deploy deploy-vps deploy-logs clean distclean
## ------------------------------------------------------------ sviluppo
@@ -124,6 +124,8 @@ image-size: ## Dimensione dell'immagine
# Host e indirizzo stanno in deploy.local.mk, fuori da git, per esempio:
# DEPLOY_HOST = root@mio-server
# DEPLOY_URL = https://gioco.esempio
# VPS_HOST = mio-vps (facoltativo: una seconda copia, con make deploy-vps)
# VPS_DIR = docker/leggenda-regolo
-include deploy.local.mk
DEPLOY_DIR ?= /root/leggenda-regolo
DEPLOY_TMP := $(shell mktemp -d -u /tmp/regolo-deploy.XXXXXX)
@@ -151,6 +153,16 @@ deploy: ## Pubblica l'ultimo commit: test, immagine costruita e provata qui, spe
xargs -r -I{} docker image rm -f $(IMAGE):{} >/dev/null
@echo "→ $(DEPLOY_URL) ($$(git log -1 --format='%h %s'))"
deploy-vps: ## Spedisce al VPS (VPS_HOST, VPS_DIR in deploy.local.mk) l'immagine dell'ultimo commit già provata da make deploy
@test -n "$(VPS_HOST)" -a -n "$(VPS_DIR)" || { echo "mancano VPS_HOST e VPS_DIR: scrivili in deploy.local.mk"; exit 2; }
@docker image inspect $(IMAGE):$$(git rev-parse --short HEAD) >/dev/null 2>&1 || \
{ echo "manca l'immagine dell'ultimo commit ($$(git rev-parse --short HEAD)): prima make deploy"; exit 1; }
ssh $(VPS_HOST) 'mkdir -p $(VPS_DIR)'
git show HEAD:docker-compose.homeserver.yml | ssh $(VPS_HOST) 'cat > $(VPS_DIR)/docker-compose.yml'
docker save $(IMAGE):$$(git rev-parse --short HEAD) $(IMAGE):latest | gzip | \
ssh $(VPS_HOST) 'gunzip | docker load -q && cd $(VPS_DIR) && docker compose up -d && docker image prune -f >/dev/null'
@ssh $(VPS_HOST) "docker exec $(IMAGE) grep -o \"VERSION = '[^']*'\" /usr/share/nginx/html/src/version.js" | sed 's/^/→ $(VPS_HOST): /'
deploy-logs: ## Log del container sul server di deploy
@test -n "$(DEPLOY_HOST)" || { echo "manca DEPLOY_HOST: scrivilo in deploy.local.mk"; exit 2; }
ssh $(DEPLOY_HOST) 'cd $(DEPLOY_DIR) && docker compose logs -f --tail 50'
+38
View File
@@ -0,0 +1,38 @@
{
"image": "andrea-basket.png",
"height": 120,
"frames": {
"hold": {
"x": 0,
"y": 0,
"w": 51,
"h": 120,
"ax": 19.1,
"ay": 120
},
"load": {
"x": 53,
"y": 0,
"w": 62,
"h": 106,
"ax": 32.9,
"ay": 106
},
"shoot": {
"x": 117,
"y": 0,
"w": 69,
"h": 124,
"ax": 30.0,
"ay": 124
},
"cheer": {
"x": 188,
"y": 0,
"w": 59,
"h": 118,
"ax": 26.5,
"ay": 118
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

+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: 276 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: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 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: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 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.
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.
+268 -8
View File
@@ -211,9 +211,9 @@
"file": "andrea-22b8efe94b.mp3",
"sec": 1.75
},
"pescatori|Si preparano le canne: domattina si va a pescare al lago dell'Anatre, dietro Cardetole!": {
"file": "pescatori-486103d189.mp3",
"sec": 3.74
"pescatori|Si preparano le canne: si va a pescare al lago dell'Anatre, dietro Cardetole!": {
"file": "pescatori-e7edfae41f.mp3",
"sec": 3.51
},
"pescatori|Guarda questa: la lenza la regge anche una carpa di dieci chili.": {
"file": "pescatori-afda722e0c.mp3",
@@ -543,9 +543,9 @@
"file": "andrea-5e37f70197.mp3",
"sec": 5.35
},
"andrea|Il campo da basket del Circolo, lassù oltre la rete. Prima o poi due tiri ce li faccio.": {
"file": "andrea-8cd2b5a36d.mp3",
"sec": 6.49
"andrea|Il campo da basket del Circolo, lassù oltre la rete. Due tiri ce li faccio sempre volentieri.": {
"file": "andrea-3c4ee692ad.mp3",
"sec": 7.55
},
"andrea|La fermata della corriera. Da qui si va dappertutto. Se passa.": {
"file": "andrea-ccd735c926.mp3",
@@ -707,6 +707,42 @@
"file": "andrea-60f39f0dd7.mp3",
"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.": {
"file": "andrea-2b47355c2c.mp3",
"sec": 3.81
@@ -739,6 +775,98 @@
"file": "andrea-7fd73fbb77.mp3",
"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.": {
"file": "andrea-a942b2723e.mp3",
"sec": 6.77
@@ -1207,6 +1335,38 @@
"file": "andrea-3e2f9b4424.mp3",
"sec": 6.42
},
"andrea|La pozza. Anche il quindici d'agosto è sempre bella piena.": {
"file": "andrea-8c0ccd6cd7.mp3",
"sec": 5.09
},
"andrea|Un giorno ci casco dentro e ci affogo: il primo annegato in mezzo al bosco.": {
"file": "andrea-d7a9731b0f.mp3",
"sec": 4.59
},
"andrea|Un bagno nella pozza? No, grazie: non so quanto è fonda, e non lo voglio sapere.": {
"file": "andrea-97a7504f87.mp3",
"sec": 6.15
},
"andrea|Una rete vecchia, tenuta su coi pali di legno e con la speranza.": {
"file": "andrea-bbd7781659.mp3",
"sec": 4.13
},
"andrea|Lassù spuntano i cipressi: il viale del Trebbio. Ancora un pezzo di salita.": {
"file": "andrea-429dbaf7e2.mp3",
"sec": 5.52
},
"andrea|Un cartello bianco. Bianco e basta: chi l'ha messo s'è scordato cosa ci voleva scrivere.": {
"file": "andrea-d1d027b75a.mp3",
"sec": 6.82
},
"andrea|Un muretto di sassi e mattoni: buono per riprendere fiato a metà salita.": {
"file": "andrea-9b8f4b41ad.mp3",
"sec": 4.59
},
"andrea|Mi siederei, ma se mi siedo ora non mi rialzo più.": {
"file": "andrea-093f6aff9c.mp3",
"sec": 3.56
},
"andrea|Giù a destra, oltre le vigne, c'è la casa del maestro Bacci. Mezzo paese ha imparato a leggere e scrivere da lui.": {
"file": "andrea-060513a152.mp3",
"sec": 8.65
@@ -1984,8 +2144,8 @@
"sec": 1.24
},
"andrea|S'ha andà a fallo!?!?": {
"file": "andrea-b25aeefc97.mp3",
"sec": 1.16
"file": "andrea-bb61f7641e.mp3",
"sec": 2.17
},
"andrea|Ci si becca da i' Trappola, ai' Barrettino!": {
"file": "andrea-fca6f59a70.mp3",
@@ -2035,6 +2195,74 @@
"file": "arcu-406cd02197.mp3",
"sec": 4.46
},
"andrea|S'ha andà a fallo?!?": {
"file": "andrea-c80714aae7.mp3",
"sec": 1.08
},
"andrea|Quanti ne prendete, di solito?": {
"file": "andrea-1d758ed60c.mp3",
"sec": 1.84
},
"andrea|Buona pesca, ragazzi.": {
"file": "andrea-cb3510ac67.mp3",
"sec": 1.85
},
"pescatori|Gnamo! La canna ti si dà noi.": {
"file": "pescatori-32054cfa54.mp3",
"sec": 2.97
},
"pescatori|Come peschi?": {
"file": "pescatori-520fe85721.mp3",
"sec": 0.56
},
"pescatori|Dipende da chi racconta. Il Nanni dice dieci, il cestino dice due.": {
"file": "pescatori-901d65f3bc.mp3",
"sec": 4.09
},
"pescatori|Se vedi il palio, salutacelo!": {
"file": "pescatori-52125dc78f.mp3",
"sec": 1.74
},
"andrea|Alla bona, tanto per bagnare la lenza.": {
"file": "andrea-b74e348e25.mp3",
"sec": 2.55
},
"pescatori|Alla bona. Due pesci e si va a casa contenti.": {
"file": "pescatori-8af3768e38.mp3",
"sec": 3.26
},
"pescatori|Seria: tre pesci, e lo stivale non conta.": {
"file": "pescatori-e417a89a3a.mp3",
"sec": 2.82
},
"pescatori|A fassi male? Quattro pesci, e le carpe tirano come trattori.": {
"file": "pescatori-befa87eee8.mp3",
"sec": 4.17
},
"andrea|Due tiri alla bona, tanto per scaldarmi.": {
"file": "andrea-d564365fc2.mp3",
"sec": 2.53
},
"andrea|Due tiri seri.": {
"file": "andrea-ec8f0db709.mp3",
"sec": 1.32
},
"andrea|Lascia stare, ho il palio da trovare.": {
"file": "andrea-f5519518f2.mp3",
"sec": 1.99
},
"andrea|Tre su dieci e sono contento.": {
"file": "andrea-5e3315a040.mp3",
"sec": 2.33
},
"andrea|Cinque su dieci. Come ai tempi del Circolino.": {
"file": "andrea-1bc731bcdf.mp3",
"sec": 3.59
},
"andrea|Sette su dieci, senza guardare la parabola. Vediamo chi sono.": {
"file": "andrea-fa062d00bd.mp3",
"sec": 4.95
},
"andrea|Mariano! Facciamo una partita a bocce?": {
"file": "andrea-83b1337b4d.mp3",
"sec": 3.31
@@ -2279,6 +2507,38 @@
"file": "mariano-39e27158b7.mp3",
"sec": 2.83
},
"andrea|Che mano! Jordan, fatti da parte.": {
"file": "andrea-3a52789f04.mp3",
"sec": 2.98
},
"andrea|Il ferro oggi era stretto. Riprovo dopo.": {
"file": "andrea-2e9d8549c8.mp3",
"sec": 3.36
},
"andrea|Lascio il pallone lì. Tanto lo sanno tutti che è mio.": {
"file": "andrea-ee42022092.mp3",
"sec": 3.98
},
"pescatori|Oh, hai visto? Mica male, per una canna di bambù.": {
"file": "pescatori-2583bf891f.mp3",
"sec": 3.79
},
"andrea|Stasera si frigge. Ma prima il palio.": {
"file": "andrea-b00b469ef8.mp3",
"sec": 3.48
},
"pescatori|I pesci c'erano. Eri te che non c'eri.": {
"file": "pescatori-53d5588588.mp3",
"sec": 2.41
},
"andrea|Si rifà. Dopo il palio.": {
"file": "andrea-5ebf35bc74.mp3",
"sec": 2.37
},
"andrea|Lascio la canna ai ragazzi. Il lago non scappa, il palio sì.": {
"file": "andrea-72bf22a3f9.mp3",
"sec": 4.49
},
"ing_tozzi|Venti. Hai vinto te, stasera il caffè lo pago io.": {
"file": "ing_tozzi-51f855478a.mp3",
"sec": 4.42
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,
poi gli occhi. Due immagini di Codex.
- *Segreti facoltativi del paese* con pagine illustrate nel taccuino: funzione nuova, dopo la storia principale.
- **Altri minigiochi come le bocce** (25 ottobre 2026): ognuno è un primo piano (`closeup` con il suo `type`, come
`bocce`), con comandi per dito, mouse e tastiera, i suoi test e una battuta per chi vince e per chi perde.
Il posto naturale sono le gare del Palio del secondo capitolo:
- *Passamano dei mattoni* (da fare per primo): un gioco di ritmo, i mattoni arrivano lungo la fila e si passano al
momento giusto; sbagliando il tempo ne cade uno. Facile col dito, e chiude il cerchio col finale (i mattoni
portati in Fortezza).
- **Altri minigiochi** (25 ottobre 2026, rivisti il 28 ottobre con le proposte di Claude che ad Andrea piacciono):
ognuno è un primo piano (`closeup` con il suo `type`), con comandi per dito, mouse e tastiera, i suoi test, una
battuta per chi vince e per chi perde, e a fine lavoro la revisione critica di Codex se Andrea la concede.
Già fatti: briscola, tressette e scopa all'oratorio; le bocce al pallaio; il biliardo coi birilli al Circolo
4 Maggio; «Fai due tiri» al campo da basket del Circolo (27 ottobre). I prossimi due, se Andrea è d'accordo:
il calcio balilla (riusa la fisica del biliardo, fa subito bar) e il tiro alla fune (è il Palio, e vale come
enigma del rione).
Le gare del Palio (il secondo capitolo: passamano, tiro alla fune, carrette, lavandaie, caccia al tesoro):
- *Passamano dei mattoni*: un gioco di ritmo, i mattoni arrivano lungo la fila e si passano al momento giusto,
un tasto per ogni passaggio; sbagliando il tempo ne cade uno. Facile col dito, e chiude il cerchio col finale
(i mattoni portati in Fortezza).
- *Tiro alla fune a Cardetole*: si batte a ritmo (spazio o tocco) seguendo una barra che oscilla, lo sforzo
della squadra; chi perde il tempo scivola. Semplice da fare, molto da palio; contro il bullo del rione, come in
`docs/PROGETTO.md`.
- *Le carrette*: corsa in discesa vista di lato, a destra e sinistra fra buche e balle di paglia. La più
spettacolare, la più lunga da disegnare.
- *Bandierine (lavanderine)*: correre a prendere la bandiera prima dell'avversario, di riflessi.
spettacolare, la più lunga da disegnare: il «gioco grosso», con Mitrugno (consenso).
- *Le lavandaie* (Andrea, 28 ottobre; prima era segnato male come «bandierine»): un vascone pieno d'acqua con
dentro gli stracci, pezzi di stoffa dei quattro colori dei rioni mescolati; ogni squadra deve ripescare i suoi.
Come minigioco: vista dall'alto sul vascone, gli stracci galleggiano e girano, si tocca il proprio colore per
tirarlo fuori, un errore (il colore di un altro) fa perdere tempo; contro il tempo o contro le altre squadre.
- *Caccia al tesoro*: un giro di indizi fra le scene che ci sono già, senza un minigioco nuovo.
Fuori dal Palio:
- *Briscola al circolino* con l'Arcu e il Barone (la gag del «Male male si divide»): una mano corta.
- *Rigori all'A. Bettini*: mira e forza come alle bocce, contro un portiere. Andrea ha ritrovato la foto di squadra
sul campo in terra (porta, rete, la torre della Pieve dietro, anni '90): lo sfondo del minigioco lo fa Codex da quella,
senza le persone (per i volti serve il consenso). Da salvare in `assets/raw/foto/campo-bettini-squadra.jpg`.
Al bar e al circolo:
- *Calcio balilla al River*: due stecche sole (difesa e attacco), vista dall'alto come il biliardo, che ha già
la fisica delle sponde. Il gioco da bar più sampierino che c'è; il River «coi flipper e le figurine» lo cita già.
- *Flipper del River*: più lavoro, ma la fisica delle biglie c'è già.
- *Freccette al Circolo*: la mira vaga e si ferma al clic, tre freccette, si chiude il 301. Poco codice.
- *Morra*: niente fisica, tutto dialogo e tempismo; molto toscana, le grida dei numeri in voce XTTS.
Sport:
- *Rigori all'A. Bettini*: mira e forza come alle bocce, contro un portiere che si butta. Andrea ha ritrovato la
foto di squadra sul campo in terra (porta, rete, la torre della Pieve dietro, anni '90): lo sfondo lo fa Codex
da quella, senza le persone (per i volti serve il consenso). Da salvare in `assets/raw/foto/campo-bettini-squadra.jpg`.
- *Tennis o padel al campo secondario*: la macchina del tempo compare già quando Andrea dice di voler giocare a
padel; un «muro» a tempismo basterebbe.
- *Ping-pong al pallaio*: il tavolo verde c'è già nello sfondo.
Giochi da bambini del 2007 (nostalgia):
- *Le figurine al River*: un «batti-muro», o lo scambio «ce l'ho, ce l'ho, mi manca», più dialogo che gioco.
- *La pesca al lago dell'Anatre*: coi ragazzi delle canne in Fortezza (27 ottobre) la scusa c'è già. Tempismo:
la lenza vibra, si tira al momento giusto. Serve la scena del lago, dietro Cardetole.
- *Briscola al circolino* con l'Arcu e il Barone (la gag del «Male male si divide»): una mano corta; la briscola
c'è già all'oratorio, qui conta la gag.
+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
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:
+34 -2
View File
@@ -204,13 +204,45 @@ 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
murale di Cistola (Davide Poggi), «il talento di via del Pignone», alla maniera dei murali di Maradona a Napoli
(volto da `assets/raw/riferimenti/cistola.png`). Consenso di Davide confermato da Andrea il 2026-10-27.
- Via 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.
- Il sentiero della Fortezza (foto di Andrea, 28 ottobre, sfondo di Codex): fra la Fortezza (a diritto) e la casa del
maestro Bacci (alle spalle), che non sono più collegate direttamente. La pozza c'è anche il 15 d'agosto (Andrea).
Il nome del sentiero è provvisorio.
- La salita dei cipressi (foto di Andrea, 28 ottobre, sfondo di Codex): la strada bianca fra la casa del maestro
Bacci (alle spalle) e il viale dei cipressi (lassù spuntano i cipressi). Nome provvisorio, battute provvisorie.
- Seconda copia del gioco sul VPS Contabo con `make deploy-vps` (dopo `make deploy`). Per renderla pubblica serve
nel proxy del VPS la regola per 50037.manzolo.it; poi si sposta il DNS su Aruba.
- Via Pietro Cipriani (fra Piazza e campeggio): Cinema Taiuti aperto con la locandina del Rattattù 2007 (stile da
`assets/raw/riferimenti/rattattu-locandina.png`, scaletta illeggibile), campo da basket del Circolo: **da fare il
gioco del basket** (idea di Andrea, 27 ottobre).
`assets/raw/riferimenti/rattattu-locandina.png`, scaletta illeggibile). Al campo da basket del Circolo, «Fai due tiri»
(`src/engine/basket.js`, vista di lato, sfondo e sprite di Andrea col pallone da Codex): da solo, dieci palloni, a ogni
canestro si arretra fino alla linea da tre; tre livelli (quanti canestri per la gloria, quanta parabola mostra la
guida). Battute provvisorie. Idea di Andrea del 27 ottobre. Revisione critica di Codex (27 ottobre) applicata per
i punti gravi (angolo iniziale 64° con fascia di forza buona da ogni postazione, palo e tabellone da entrambi i lati,
canestro che conta subito, pose senza doppio pallone, guida fino al primo urto, obiettivo sempre in vista). Restano
le sue idee: «Riprova» dal risultato e record per livello; Andrea che arretra camminando; battute che dicono perché
è uscita (corto, lungo, ferro); l'angolo anche nei comandi HTML; testi del canvas piccoli sul telefono;
`basket_vinto` che lasci traccia nel paese; pannelli più compatti.
- Fuori paese, dal campo secondario (a destra): le Mozzète → il ponticino → Coldaia → la falegnameria → Gabbiano;
da Via Cipriani, indietro, Via Massorondinaio → campeggio. Sfondi da Street View, battute provvisorie.
- In Fortezza quattro ragazzi inventati si fanno le canne da pesca col bambù per il lago dell'Anatre (sprite di Codex,
`assets/raw/pescatori-sheet.png`, ritagliato 2x2 a mano: work, talk, laugh, proud); battuta finale di Andrea.
Parlandoci: «S'ha andà a fallo?!?» (Andrea, 28 ottobre) e si va a pescare: minigioco `src/engine/pesca.js`
(primo piano del lago, sfondo e Andrea con la canna di Codex; lancio tenendo premuto, tocchi finti e abboccata,
ferrata a tempo, recupero con la lenza che si può spezzare; sei lanci; pesci più grossi al largo, lo stivale sotto
riva; tre livelli). Battute provvisorie. Il lago come scena da visitare non c'è: serve una foto di Andrea.
Revisione di Codex (28 ottobre) applicata nei punti gravi (galleggiante che saltava, sesto lancio bloccato dal
pannello, invito a ferrare fuori tempo, lenza che non si spezzava mai, mira che si sbloccava) e in parecchi utili
(un gesto solo per ferrare e recuperare, pulsante Recupera/Molla, recupero breve per i pesci piccoli, attese ripetute
dimezzate, tocchi finti variabili, lenza dalla punta vera della canna, stivale con la sua battuta e senza peso,
risultato deciso all'ultimo pesce). Restano le sue idee: «Porta a casa la pescata» a gloria raggiunta; premio per
peso o varietà; i ragazzi che reagiscono (stivale, carpa, lenza rotta); paesaggio sonoro del lago (ora quello della
Fortezza); abboccata e tensione anche nei comandi HTML e un richiamo grande sul telefono; Esc col fuoco sui pulsanti.
- Andrea rifatto dalla sua foto del 2007 (`assets/raw/andrea-sheet-prova-2.png`; il vecchio foglio resta in `andrea-sheet.png`).
- Strada del campeggio (poi Bosco ai Frati) e strada della scuola: per ora battute, scene da fare con le foto di Andrea.
+22 -1
View File
@@ -101,7 +101,7 @@
<h2>Ringraziamenti</h2>
<p class="dedication credits-dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p>
<dl id="credits-list"></dl>
<div id="credits-reel" aria-hidden="true"><div class="track"></div></div>
<div id="credits-reel"><button class="arrow prev" type="button" aria-label="Foto precedente">‹</button><div class="track" aria-label="Le immagini del gioco: toccane una per vederla grande"></div><button class="arrow next" type="button" aria-label="Foto successiva">›</button></div>
<p class="legal">Monkey Island è un marchio di Lucasfilm Ltd., citato solo nella dedica: questo gioco non è affiliato né approvato da Lucasfilm.</p>
<div class="buttons"><button id="close-credits">Chiudi</button></div>
<figure id="face" hidden><img alt=""><figcaption></figcaption></figure>
@@ -209,6 +209,27 @@
<p>Tre bocce a testa. Tira chi non ha il punto; segna ogni boccia più vicina della migliore avversaria.
Vince chi fa più punti nella mano.</p>
</details>
<details id="basket-help" hidden><summary>Come si gioca</summary>
<p>Muovi il mouse sul campo per scegliere l’angolo del tiro: la freccia parte dalle mani di Andrea. Un clic (o un
tocco) fissa la mira, così puoi raggiungere i comandi senza cambiarla; un altro clic la sposta. Tieni premuto sul
campo (o Spazio) e la forza riparte da zero e sale: lascia per tirare. Oppure regola la forza e premi Tira.
Frecce ← →: angolo più teso o più alto; Maiusc + ← →: a mezzo grado; ↑ ↓: forza. Esc esce.</p>
<p>Dieci palloni. Si comincia da sotto; a ogni canestro si arretra di una postazione: tiro libero, media distanza,
da tre e da lontanissimo, dove un canestro vale tre punti invece di due. Il ferro e il tabellone rimandano la
palla: entra anche sporca, ma non da sotto. La gloria: 3 canestri alla bona, 5 seria, 7 a fassi male. Alla bona la
guida mostra la parabola fino al primo urto, Seria solo l’inizio, A fassi male niente.</p>
</details>
<details id="pesca-help" hidden><summary>Come si gioca</summary>
<p>Un tocco sull’acqua sceglie dove lanciare (o regola la forza: più lontano, pesci più grossi, ma attese più
lunghe). Tieni premuto sulla scena (o Spazio): la forza riparte da zero e sale, lascia per lanciare; oppure premi
Lancia. Poi si aspetta: il galleggiante ha dei tocchi finti (si abbassa appena) e un’abboccata vera (va sotto del
tutto): premi in quel momento per ferrare, e <b>resta premuto</b>: stai già recuperando. Premendo a vuoto si
ricupera la lenza vuota e il lancio è perso; se non ferri in tempo il pesce lo sputa, e riprova fino a tre volte.</p>
<p>Nel recupero guarda la lenza e la barra in basso: quando il pesce strattona (la lenza trema) la barra sale verso
il rosso, lascia andare un attimo, poi riprendi. I pesci piccoli vengono a riva in fretta, le carpe no. Dal pannello
il pulsante alterna Recupera e Molla. Sei lanci. La gloria: 2 pesci alla bona, 3 seria, 4 a fassi male (lo stivale
non conta, e non pesa). Frecce ← →: distanza del lancio; Esc esce.</p>
</details>
<details id="biliardo-help" hidden><summary>Come si gioca</summary>
<button class="tutorial-open" type="button" data-tutorial="biliardo" hidden>Guarda il video</button>
<p>Clic o tocco sul tavolo fissa la mira: puoi spostarti sui comandi senza cambiarla.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "leggenda-del-regolo",
"version": "0.1.19",
"version": "0.1.30",
"private": true,
"type": "module",
"scripts": {
+113
View File
@@ -0,0 +1,113 @@
import { FLOOR, RIM, BALL, BALLS, SPOTS, canThrow, left, spot, releasePoint, guide as guidePoints, status } from './basket.js';
import { text, panel } from './bocce-render.js';
const ANDREA = { scale: 0.66 };
// Quale posa: carica mentre tiene premuto; finché la palla vola Andrea resta a braccia alzate (l'unica posa senza
// pallone), esulta quando entra, e a palla ferma ce l'ha di nuovo in mano.
export function andreaPose(c) {
if (c.holding) return 'load';
if (c.step === 'flight') return c.ball.scored ? 'cheer' : 'shoot';
if (c.step === 'settle' && c.ball?.scored) return 'cheer';
return 'hold';
}
function andrea(ctx, c, sheet) {
const s = spot(c), x = s.x, y = FLOOR;
ctx.fillStyle = 'rgba(8,10,9,0.5)'; ctx.beginPath(); ctx.ellipse(x + 4, y, 16, 3, 0, 0, Math.PI * 2); ctx.fill();
if (!sheet) return;
const pose = andreaPose(c), frame = sheet.atlas.frames[pose] ?? sheet.atlas.frames.hold, k = ANDREA.scale;
// Nel salto i piedi si staccano da terra per un attimo.
const lift = pose === 'shoot' ? Math.round(Math.sin(Math.min(1, c.ball.time / 0.45) * Math.PI) * 10) : 0;
ctx.drawImage(sheet.img, frame.x, frame.y, frame.w, frame.h,
Math.round(x - frame.ax * k), Math.round(y - frame.ay * k) - lift, Math.round(frame.w * k), Math.round(frame.h * k));
}
function ball(ctx, b, r = BALL) {
// L'ombra sul campo si stringe quando la palla è alta.
const h = Math.max(0, FLOOR - b.y - r), k = Math.max(0.35, 1 - h / 260);
ctx.fillStyle = `rgba(8,10,9,${0.45 * k})`; ctx.beginPath(); ctx.ellipse(b.x, FLOOR - 1, r * 1.2 * k, r * 0.4 * k, 0, 0, Math.PI * 2); ctx.fill();
const shade = ctx.createRadialGradient(b.x - r * 0.35, b.y - r * 0.35, 0, b.x, b.y, r * 1.05);
shade.addColorStop(0, '#ffb067'); shade.addColorStop(0.5, '#e8702a'); shade.addColorStop(1, '#8a3512');
ctx.fillStyle = shade; ctx.beginPath(); ctx.arc(b.x, b.y, r, 0, Math.PI * 2); ctx.fill();
// Le cuciture girano con la palla.
ctx.save(); ctx.beginPath(); ctx.arc(b.x, b.y, r - 0.3, 0, Math.PI * 2); ctx.clip();
ctx.strokeStyle = 'rgba(60,20,8,0.75)'; ctx.lineWidth = 1;
const a = b.spin ?? 0;
ctx.beginPath(); ctx.ellipse(b.x, b.y, r, Math.abs(Math.cos(a)) * r, 0, 0, Math.PI * 2); ctx.stroke();
ctx.beginPath(); ctx.ellipse(b.x, b.y, Math.abs(Math.sin(a)) * r, r, 0, 0, Math.PI * 2); ctx.stroke();
ctx.restore();
ctx.fillStyle = '#ffd9b0'; ctx.fillRect(Math.round(b.x - r * 0.4), Math.round(b.y - r * 0.45), 1, 1);
}
function guide(ctx, c) {
const r = releasePoint(c);
// La freccia della mira c'è sempre, con la punta e un bordo scuro per staccarsi da rete e alberi; la parabola per
// quanto concede il livello, fino al primo urto.
const a = c.angle * Math.PI / 180, len = 22 + c.power * 18, tip = { x: r.x + Math.cos(a) * len, y: r.y - Math.sin(a) * len };
const arrow = () => {
ctx.beginPath(); ctx.moveTo(r.x, r.y); ctx.lineTo(tip.x, tip.y);
for (const s of [-1, 1]) { ctx.moveTo(tip.x, tip.y); ctx.lineTo(tip.x - Math.cos(a - s * 0.5) * 6, tip.y + Math.sin(a - s * 0.5) * 6); }
ctx.stroke();
};
ctx.lineWidth = 3; ctx.strokeStyle = 'rgba(20,16,10,0.8)'; arrow();
ctx.lineWidth = 1; ctx.strokeStyle = '#fff6d2'; arrow();
if (c.skill.preview > 0) {
const pts = guidePoints(c, c.skill.preview);
for (const [width, color] of [[3, 'rgba(20,16,10,0.6)'], [1, 'rgba(143,199,255,0.95)']]) {
ctx.lineWidth = width; ctx.strokeStyle = color; ctx.setLineDash([2, 4]);
ctx.beginPath();
for (const [i, p] of pts.entries()) { if (i) ctx.lineTo(p.x, p.y); else ctx.moveTo(p.x, p.y); }
ctx.stroke(); ctx.setLineDash([]);
}
}
ctx.fillStyle = 'rgba(20,16,10,0.6)'; ctx.fillRect(r.x - 34, r.y - 16, 30, 11);
text(ctx, `${c.angle.toFixed(c.angle % 1 ? 1 : 0)}°`, r.x - 6, r.y - 10, 6, '#fff4c9', 'right');
}
function swish(ctx, c) {
// Un lampo di righe attorno al ferro quando la palla è appena entrata.
const b = c.ball; if (c.step !== 'flight' || !b?.scored || b.time - b.scoredAt > 0.3) return;
const cx = (RIM.x1 + RIM.x2) / 2;
ctx.strokeStyle = 'rgba(255,240,170,0.8)';
for (let i = 0; i < 6; i++) { const t = i / 6 * Math.PI * 2; ctx.beginPath(); ctx.moveTo(cx + Math.cos(t) * 20, RIM.y + Math.sin(t) * 8); ctx.lineTo(cx + Math.cos(t) * 30, RIM.y + Math.sin(t) * 13); ctx.stroke(); }
}
export function drawBasket(ctx, c, def, bg, sheet) {
ctx.save();
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
else { ctx.fillStyle = '#6d7a5a'; ctx.fillRect(0, 0, 640, 360); }
// Le postazioni sul campo: un segno chiaro dove si tira ora, uno spento dove si è già stati.
for (const [i, s] of SPOTS.entries()) {
ctx.fillStyle = i === c.spot ? 'rgba(255,244,201,0.9)' : i < c.spot ? 'rgba(255,244,201,0.35)' : 'rgba(255,244,201,0.15)';
ctx.fillRect(s.x - 1, FLOOR + 2, 3, 3);
}
andrea(ctx, c, sheet);
for (const [i, t] of c.trails.entries()) {
ctx.fillStyle = `rgba(255,190,120,${0.3 * (1 - t.age / 0.5)})`;
ctx.fillRect(Math.round(t.x) - 1, Math.round(t.y) - 1, i % 2 ? 2 : 3, i % 2 ? 2 : 3);
}
if (canThrow(c) || c.holding) guide(ctx, c);
if (c.ball && c.step === 'flight') ball(ctx, c.ball); // ferma, la palla è già tornata in mano ad Andrea
swish(ctx, c);
panel(ctx, 182, 50, 440, 24);
text(ctx, status(c, def.names), 402, 63, 6, '#fff0cb', 'center');
// I palloni rimasti e il conto.
panel(ctx, 18, 86, 196, 58);
text(ctx, 'PALLONI', 26, 98, 6, '#eed5a0');
for (let n = 0; n < BALLS; n++) {
const x = 30 + n * 14, y = 113;
if (n < left(c)) ball(ctx, { x, y, spin: 0 }, 4);
else { ctx.strokeStyle = '#766e5c'; ctx.strokeRect(x - 3, y - 3, 6, 6); }
}
text(ctx, `${c.baskets} su ${c.skill.goal} per la gloria · ${c.points} punti`, 26, 131, 6, c.baskets >= c.skill.goal ? '#b9e5b3' : '#d7ccb4');
// La forza in basso a sinistra: a destra c'è il pulsante per uscire.
if (canThrow(c) || c.holding) {
panel(ctx, 18, 306, 176, 37);
text(ctx, `FORZA ${Math.round(c.power * 100)}%`, 30, 318, 7);
ctx.fillStyle = '#3e3e36'; ctx.fillRect(30, 331, 150, 5);
ctx.fillStyle = '#b9d392'; ctx.fillRect(30, 331, Math.round(c.power * 150), 5);
}
if (c.step === 'result') {
panel(ctx, 206, 91, 397, 70);
text(ctx, c.result.winner === 'player' ? 'CHE MANO!' : 'DUE TIRI FATTI', 404, 108, 9, '#efd49e', 'center');
text(ctx, `${c.result.baskets} canestri su ${BALLS}, ${c.result.points} punti`, 404, 132, 9, c.result.winner === 'player' ? '#b9e5b3' : '#f1d9be', 'center');
text(ctx, c.result.winner === 'player' ? `Obiettivo ${c.result.goal}: fatto.` : `Per la gloria ne servivano ${c.result.goal}.`, 404, 150, 6, '#d6c7af', 'center');
}
ctx.restore();
}
+170
View File
@@ -0,0 +1,170 @@
// «Fai due tiri»: Andrea da solo al campo del Circolo 4 Maggio (idea di Andrea, 27 ottobre). Vista di lato, nelle
// coordinate dello schermo (640x360): x verso il canestro, y verso il basso. Un pixel è circa 2,3 cm: il ferro, a
// 3,05 m, sta 133 px sopra il campo. Dieci palloni; a ogni canestro si arretra di una postazione, fino a quella da
// tre. Niente avversario: il risultato è quanti canestri su dieci, e i punti (due, tre da dietro la linea).
export const FLOOR = 296, RIM = { x1: 482, x2: 518, y: 163, r: 2 }, BOARD = { x: 519, top: 118, bottom: 178, depth: 8 }, NET = 190, POLE = { x: 562, top: 112 };
export const BALL = 8, G = 560, V_MIN = 140, V_MAX = 590, SWING = 1.4, BALLS = 10;
export const FIXED_STEP = 1 / 240, REST = 0.55, FLIGHT_MAX = 7;
export const RELEASE = { dx: 18, dy: -72 }; // da dove parte la palla rispetto ai piedi di Andrea: sopra la testa, nel salto
export const ANGLE = { min: 25, max: 78 };
export const SPOTS = [
{ x: 400, name: 'da sotto', points: 2 },
{ x: 320, name: 'dal tiro libero', points: 2 },
{ x: 240, name: 'da media distanza', points: 2 },
{ x: 150, name: 'da tre', points: 3 },
{ x: 90, name: 'da lontanissimo', points: 3 },
];
// I livelli: quanti canestri su dieci valgono la gloria, e per quanto tempo di volo la guida mostra la parabola
// (alla bona fino al primo urto, che è quasi tutta).
export const LEVELS = {
bona: { goal: 3, preview: 3 },
seria: { goal: 5, preview: 0.4 },
male: { goal: 7, preview: 0 },
};
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const rad = deg => deg * Math.PI / 180;
export function newBasket(seed = Date.now() >>> 0, def = {}) {
const level = LEVELS[def.level] ? def.level : 'seria';
return { step: 'aim', t: 0, age: 0, seed, level, skill: LEVELS[level], current: 'player', spot: 0, thrown: 0, baskets: 0, points: 0,
angle: 64, power: 0.5, holding: false, holdTime: 0, accumulator: 0, ball: null, events: [], trails: [], history: [] };
}
export const canThrow = c => c.step === 'aim' && c.thrown < BALLS;
export const left = c => Math.max(0, BALLS - c.thrown);
export const spot = c => SPOTS[c.spot];
export const powerAt = time => clamp(time / SWING, 0, 1);
export const speedFor = power => V_MIN + (V_MAX - V_MIN) * clamp(power, 0, 1);
export function releasePoint(c) { const s = spot(c); return { x: s.x + RELEASE.dx, y: FLOOR + RELEASE.dy }; }
export function velocity(c) { const v = speedFor(c.power), a = rad(c.angle); return { vx: v * Math.cos(a), vy: -v * Math.sin(a) }; }
export function setPower(c, value) {
if (!canThrow(c) || c.holding || !Number.isFinite(value)) return false;
c.power = clamp(value, 0, 1); return true;
}
export function setAngle(c, deg) {
if (!canThrow(c) || c.holding || !Number.isFinite(deg)) return false;
c.angle = clamp(deg, ANGLE.min, ANGLE.max); return true;
}
export const nudge = (c, dir, fine = false) => setAngle(c, c.angle + dir * (fine ? 0.5 : 2));
// La mira segue il punto toccato: l'angolo fra la mano di Andrea e il dito. Dietro di lui non si tira.
export function aimScreen(c, [x, y]) {
if (!Number.isFinite(x) || !Number.isFinite(y)) return false;
const r = releasePoint(c);
if (x <= r.x + 4) return false;
return setAngle(c, Math.atan2(r.y - y, x - r.x) * 180 / Math.PI);
}
// La parabola libera (senza urti) per `seconds` di volo: la guida la mostra per il tempo che il livello concede.
export function arc(c, seconds, every = 0.04) {
const r = releasePoint(c), v = velocity(c), out = [];
for (let t = 0; t <= seconds; t += every) out.push({ x: r.x + v.vx * t, y: r.y + v.vy * t + 0.5 * G * t * t });
return out;
}
// La guida: la parabola libera finché non tocca qualcosa (ferro, tabellone, palo, campo), al massimo `seconds`.
export function guide(c, seconds) {
const out = [];
for (const p of arc(c, seconds)) {
const touches = p.y + BALL > FLOOR || [RIM.x1, RIM.x2].some(x => Math.hypot(p.x - x, p.y - RIM.y) < BALL + RIM.r)
|| (p.x + BALL > BOARD.x && p.y + BALL > BOARD.top && p.y - BALL < BOARD.bottom) || (p.x + BALL > POLE.x && p.y + BALL > POLE.top);
out.push(p);
if (touches) break;
}
return out;
}
export function shoot(c) {
if (!canThrow(c) || c.holding) return false;
const r = releasePoint(c), v = velocity(c);
c.ball = { x: r.x, y: r.y, vx: v.vx, vy: v.vy, time: 0, bounces: 0, rim: false, board: false, under: false, scored: false, scoredAt: null, dead: false, spin: 0 };
c.step = 'flight'; c.t = c.accumulator = 0; c.thrown++; c.trails = [];
c.events.push({ kind: 'basket-throw', power: c.power });
return true;
}
export function press(c) {
if (!canThrow(c) || c.holding) return false;
c.previousPower = c.power; c.holding = true; c.holdTime = c.power = 0; return true;
}
export function cancel(c) {
if (!c.holding) return false;
c.holding = false; c.power = c.previousPower; c.holdTime = 0; return true;
}
export function release(c) {
if (!c.holding) return false;
if (c.holdTime < 0.14) return cancel(c); // un tocco breve serve a mirare, non spreca un pallone
c.holding = false; return shoot(c);
}
function hit(c, kind, strength) { if (c.events.length < 12) c.events.push({ kind, power: clamp(strength, 0, 1) }); }
// Il ferro visto di lato sono due punti: il davanti e il dietro dell'anello. La palla ci rimbalza come su un tondino.
function rimEnd(c, b, px, py) {
const dx = b.x - px, dy = b.y - py, d = Math.hypot(dx, dy), r = BALL + RIM.r;
if (d >= r || d < 1e-9) return;
const nx = dx / d, ny = dy / d, closing = b.vx * nx + b.vy * ny;
b.x = px + nx * r; b.y = py + ny * r;
if (closing < 0) {
const tx = -ny, ty = nx, vt = (b.vx * tx + b.vy * ty) * 0.92, vn = -closing * 0.58;
b.vx = vn * nx + vt * tx; b.vy = vn * ny + vt * ty;
b.rim = true;
if (-closing > 25) hit(c, 'basket-rim', -closing / 400); // i microurti di una palla che si posa non tintinnano
}
}
function physics(c, dt) {
const b = c.ball, before = b.y;
b.vy += G * dt; b.x += b.vx * dt; b.y += b.vy * dt; b.time += dt; b.spin += b.vx * dt / BALL;
// Il tabellone: una lastra verticale che guarda Andrea, ma rimanda anche una palla che torna da dietro.
// Il palo dietro ferma quel che passa oltre, dalla sua cima in giù: sopra, la palla vola via e ricade.
if (b.y + BALL > BOARD.top && b.y - BALL < BOARD.bottom) {
if (b.vx > 0 && b.x + BALL > BOARD.x && b.x - BALL < BOARD.x + BOARD.depth) { b.x = BOARD.x - BALL; b.vx = -b.vx * 0.6; b.board = true; hit(c, 'basket-board', Math.abs(b.vx) / 300); }
else if (b.vx < 0 && b.x - BALL < BOARD.x + BOARD.depth && b.x + BALL > BOARD.x) { b.x = BOARD.x + BOARD.depth + BALL; b.vx = -b.vx * 0.6; b.board = true; hit(c, 'basket-board', Math.abs(b.vx) / 300); }
}
if (b.x + BALL > POLE.x && b.y + BALL > POLE.top) { b.x = POLE.x - BALL; if (b.vx > 0) { b.vx = -b.vx * 0.5; b.board = true; hit(c, 'basket-board', 0.3); } }
rimEnd(c, b, RIM.x1, RIM.y); rimEnd(c, b, RIM.x2, RIM.y);
// Il canestro: la palla attraversa l'anello scendendo, fra il davanti e il dietro, e non da sotto.
const inside = b.x > RIM.x1 + BALL * 0.6 && b.x < RIM.x2 - BALL * 0.6;
if (inside && before <= RIM.y && b.y > RIM.y && b.vy > 0 && !b.scored && !b.under) {
b.scored = true; b.scoredAt = b.time; b.vx *= 0.35; b.vy *= 0.5;
hit(c, b.rim || b.board ? 'basket-in' : 'basket-swish', 0.7);
// Il canestro conta subito: il punteggio e, all'ultimo pallone, il risultato non aspettano la palla ferma.
c.baskets++; c.points += spot(c).points;
if (c.thrown >= BALLS) finish(c);
} else if (inside && before >= RIM.y && b.y < RIM.y && b.vy < 0) b.under = true;
if (b.scored && b.y < NET + BALL) { b.vx *= 1 - 2.5 * dt; b.vy *= 1 - 1.2 * dt; }
if (b.y + BALL > FLOOR) {
b.y = FLOOR - BALL;
if (b.vy > 40) { b.vy = -b.vy * 0.55; b.vx *= 0.85; b.bounces++; hit(c, 'basket-bounce', b.vy / -400); }
else { b.vy = 0; b.vx *= 1 - 3 * dt; }
}
const still = Math.hypot(b.vx, b.vy) < 10;
// Entrata, la palla è finita: non si aspetta che rotoli. Una palla posata sul ferro, nemmeno.
if (b.x < -20 || b.time > FLIGHT_MAX || b.bounces > 6 || (b.y + BALL >= FLOOR - 0.5 && Math.abs(b.vx) < 6 && b.vy === 0)
|| (b.scored && b.y > NET + 30) || (still && b.time > 1.5)) b.dead = true;
}
function finish(c) {
c.result = { winner: c.baskets >= c.skill.goal ? 'player' : 'cpu', baskets: c.baskets, points: c.points, goal: c.skill.goal };
}
function nextBall(c) {
const b = c.ball;
c.history.push({ spot: c.spot, scored: b.scored, rim: b.rim, board: b.board });
if (b.scored) c.spot = Math.min(c.spot + 1, SPOTS.length - 1);
if (c.thrown >= BALLS) { if (!c.result) finish(c); c.step = 'result'; } else c.step = 'aim';
c.t = 0;
}
export function update(c, dt) {
if (!Number.isFinite(dt) || dt <= 0) return;
dt = Math.min(dt, 0.25); c.t += dt;
if (c.holding) { c.holdTime += dt; c.power = powerAt(c.holdTime); }
if (c.step === 'flight') {
c.accumulator += dt;
while (c.accumulator >= FIXED_STEP && !c.ball.dead) { physics(c, FIXED_STEP); c.accumulator -= FIXED_STEP; }
const b = c.ball;
if (!b.dead && Math.hypot(b.vx, b.vy) > 60) { c.trails.push({ x: b.x, y: b.y, age: 0 }); if (c.trails.length > 14) c.trails.shift(); }
if (b.dead) { c.step = 'settle'; c.t = 0; }
} else if (c.step === 'settle' && c.t >= REST) nextBall(c);
for (const t of c.trails) t.age += dt;
c.trails = c.trails.filter(t => t.age < 0.5);
}
export function status(c, names = { player: 'Andrea' }) {
const tally = `Canestri ${c.baskets} (ne servono ${c.skill.goal}), punti ${c.points}.`;
if (c.step === 'result') return `${c.result.baskets} canestri su ${BALLS}, ${c.result.points} punti. ${c.result.winner === 'player' ? `Obiettivo ${c.result.goal}: fatto!` : `Ne servivano ${c.result.goal}.`}`;
if (c.step === 'flight') return c.ball.scored ? `Dentro! ${tally}` : 'Palla in aria…';
if (c.step === 'settle') return c.ball.scored ? `Canestro! ${tally}` : `Fuori. ${tally}`;
return `${names.player}, pallone ${c.thrown + 1} di ${BALLS}, ${spot(c).name}. ${tally}`;
}
+20
View File
@@ -3,6 +3,8 @@
import { findPath, dist, inBox, boxDistance, nearestInside } from './geometry.js';
import * as bocce from './bocce.js';
import * as biliardo from './biliardo.js';
import * as basket from './basket.js';
import * as pesca from './pesca.js';
import * as briscola from './briscola.js';
import * as tressette from './tressette.js';
import * as scopa from './scopa.js';
@@ -14,6 +16,8 @@ export const MINIGAMES = {
scopa: { create: (seed, level, deck) => scopa.newScopa(seed, { level, deck }), update: scopa.update, play: scopa.play },
bocce: { create: (seed, level) => bocce.newBocce(seed, { level }), update: bocce.update, press: bocce.press, release: bocce.release, cancel: bocce.cancel, aim: bocce.aimScreen },
biliardo: { create: (seed, level) => biliardo.newBiliardo(seed, { level }), update: biliardo.update, press: biliardo.press, release: biliardo.release, cancel: biliardo.cancel, aim: biliardo.aimScreen },
basket: { create: (seed, level) => basket.newBasket(seed, { level }), update: basket.update, press: basket.press, release: basket.release, cancel: basket.cancel, aim: basket.aimScreen },
pesca: { create: (seed, level) => pesca.newPesca(seed, { level }), update: pesca.update, press: pesca.press, release: pesca.release, cancel: pesca.cancel, aim: pesca.aimScreen },
};
export const VERBS = ['walk', 'look', 'take', 'use', 'talk', 'open', 'give', 'drop'];
@@ -70,6 +74,22 @@ export function shuffledCredits(credits, random = Math.random) {
});
}
// Le immagini del carosello dei Ringraziamenti: tutti gli sfondi del paese (uno per scena, senza doppioni) col nome
// del posto, e un ritratto dei crediti ogni quattro sfondi; i ritratti che non trovano posto chiudono la fila.
export function reelPictures(story) {
const seen = new Set(), scenes = [];
for (const s of Object.values(story.scenes)) {
if (!s.background || seen.has(s.background)) continue;
seen.add(s.background);
scenes.push({ src: s.background, name: s.name, kind: 'scene' });
}
const faces = (story.credits ?? []).flatMap(c => c.names).filter(n => n.face).map(facePicture);
let next = 0;
const placed = scenes.flatMap((p, i) => i % 4 === 3 && next < faces.length ? [p, faces[next++]] : [p]);
return [...placed, ...faces.slice(next)];
}
export const facePicture = n => ({ src: n.face, name: n.name, kind: n.face.endsWith('palio.png') ? 'palio' : 'face' });
// La ricerca della mappa: ogni parola scritta deve stare nel nome del posto, senza badare a maiuscole, accenti e
// apostrofi («camp» trova Campomigliaio, «de medici» trova Via de' Medici).
const plain = text => text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, ' ').trim();
+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();
}

Some files were not shown because too many files have changed in this diff Show More