Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f49486870 | ||
|
|
06f7cfbfad | ||
|
|
e1b92278a5 | ||
|
|
b3c50f4694 | ||
|
|
7dd05ac92f | ||
|
|
800a82045d | ||
|
|
c96dec4692 | ||
|
|
417c126777 | ||
|
|
1205192e8a | ||
|
|
aa2f547856 | ||
|
|
57e72de021 |
@@ -17,9 +17,15 @@ COPY assets/favicon /usr/share/nginx/html/assets/favicon
|
|||||||
COPY assets/video /usr/share/nginx/html/assets/video
|
COPY assets/video /usr/share/nginx/html/assets/video
|
||||||
|
|
||||||
# Le immagini e i font citati da index.html e style.css prendono ?v=<versione>, come quelli caricati dal codice.
|
# Le immagini e i font citati da index.html e style.css prendono ?v=<versione>, come quelli caricati dal codice.
|
||||||
|
# Alla versione si aggiunge l'impronta del contenuto di assets/: uno sfondo rifatto senza cambiare versione (in
|
||||||
|
# locale, fra un bump e l'altro) ha un indirizzo nuovo, e il browser non tiene quello vecchio dalla cache lunga.
|
||||||
RUN cd /usr/share/nginx/html \
|
RUN cd /usr/share/nginx/html \
|
||||||
&& V=$(sed -n "s/^export const VERSION = '\([0-9.]*\)';$/\1/p" src/version.js) \
|
&& V=$(sed -n "s/^export const VERSION = '\([0-9.]*\)';$/\1/p" src/version.js) \
|
||||||
&& test -n "$V" \
|
&& test -n "$V" \
|
||||||
|
&& H=$(find assets -type f -exec md5sum {} + | sort -k2 | md5sum | cut -c1-8) \
|
||||||
|
&& sed -i -E 's#v=\$\{VERSION\}#v=${VERSION}-'"$H"'#' src/version.js \
|
||||||
|
&& grep -q "VERSION}-$H" src/version.js \
|
||||||
|
&& V="$V-$H" \
|
||||||
&& sed -i -E "s#url\((assets/[^)]*)\)#url(\1?v=$V)#g" style.css \
|
&& sed -i -E "s#url\((assets/[^)]*)\)#url(\1?v=$V)#g" style.css \
|
||||||
&& sed -i -E "s#src=\"(assets/[^\"]*)\"#src=\"\1?v=$V\"#g" index.html \
|
&& sed -i -E "s#src=\"(assets/[^\"]*)\"#src=\"\1?v=$V\"#g" index.html \
|
||||||
&& grep -q "?v=$V" style.css && grep -q "?v=$V" index.html
|
&& grep -q "?v=$V" style.css && grep -q "?v=$V" index.html
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ CROP ?=
|
|||||||
COLORS ?= 64
|
COLORS ?= 64
|
||||||
|
|
||||||
.PHONY: help serve open test browser verify check-public bump voci voci-elenco sfondi sfondi-genera sfondi-confronto background character \
|
.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
|
## ------------------------------------------------------------ sviluppo
|
||||||
|
|
||||||
@@ -124,6 +124,8 @@ image-size: ## Dimensione dell'immagine
|
|||||||
# Host e indirizzo stanno in deploy.local.mk, fuori da git, per esempio:
|
# Host e indirizzo stanno in deploy.local.mk, fuori da git, per esempio:
|
||||||
# DEPLOY_HOST = root@mio-server
|
# DEPLOY_HOST = root@mio-server
|
||||||
# DEPLOY_URL = https://gioco.esempio
|
# 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
|
-include deploy.local.mk
|
||||||
DEPLOY_DIR ?= /root/leggenda-regolo
|
DEPLOY_DIR ?= /root/leggenda-regolo
|
||||||
DEPLOY_TMP := $(shell mktemp -d -u /tmp/regolo-deploy.XXXXXX)
|
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
|
xargs -r -I{} docker image rm -f $(IMAGE):{} >/dev/null
|
||||||
@echo "→ $(DEPLOY_URL) ($$(git log -1 --format='%h %s'))"
|
@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
|
deploy-logs: ## Log del container sul server di deploy
|
||||||
@test -n "$(DEPLOY_HOST)" || { echo "manca DEPLOY_HOST: scrivilo in deploy.local.mk"; exit 2; }
|
@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'
|
ssh $(DEPLOY_HOST) 'cd $(DEPLOY_DIR) && docker compose logs -f --tail 50'
|
||||||
|
|||||||
|
After Width: | Height: | Size: 526 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 475 KiB |
|
After Width: | Height: | Size: 228 KiB |
@@ -1183,14 +1183,6 @@
|
|||||||
"file": "andrea-00f2e70a4d.mp3",
|
"file": "andrea-00f2e70a4d.mp3",
|
||||||
"sec": 2.95
|
"sec": 2.95
|
||||||
},
|
},
|
||||||
"andrea|Nessuno ha mai capito davvero il tuo immenso valore.": {
|
|
||||||
"file": "andrea-3e85200509.mp3",
|
|
||||||
"sec": 2.93
|
|
||||||
},
|
|
||||||
"andrea|O via, lascialo riposare. A pedalare oggi ci penso io.": {
|
|
||||||
"file": "andrea-34aad1ffe3.mp3",
|
|
||||||
"sec": 4.81
|
|
||||||
},
|
|
||||||
"andrea|La Ghiacciomobile nera. Hai visto cose che noi poveri umani non potremmo neppur lontanamente immaginare.": {
|
"andrea|La Ghiacciomobile nera. Hai visto cose che noi poveri umani non potremmo neppur lontanamente immaginare.": {
|
||||||
"file": "andrea-7be29434e0.mp3",
|
"file": "andrea-7be29434e0.mp3",
|
||||||
"sec": 7.25
|
"sec": 7.25
|
||||||
@@ -1244,16 +1236,16 @@
|
|||||||
"sec": 2.15
|
"sec": 2.15
|
||||||
},
|
},
|
||||||
"arcu|Che è, la mancia? Una squama? Grazie, la metto in cassaforte.": {
|
"arcu|Che è, la mancia? Una squama? Grazie, la metto in cassaforte.": {
|
||||||
"file": "arcu-8c7f1850a3.mp3",
|
"file": "arcu-b1332b8d66.mp3",
|
||||||
"sec": 5.84
|
"sec": 6.34
|
||||||
},
|
},
|
||||||
"arcu|Tienila te, che io di roba verde ne ho già abbastanza sul lavoro.": {
|
"arcu|Tienila te, che io di roba verde ne ho già abbastanza sul lavoro.": {
|
||||||
"file": "arcu-bb69d88654.mp3",
|
"file": "arcu-7c69d6366a.mp3",
|
||||||
"sec": 3.23
|
"sec": 3.61
|
||||||
},
|
},
|
||||||
"arcu|Un libro? A me? Lo sai che io leggo solo le etichette degli armadi.": {
|
"arcu|Un libro? A me? Lo sai che io leggo solo le etichette degli armadi.": {
|
||||||
"file": "arcu-1da81f6f98.mp3",
|
"file": "arcu-f067aab43c.mp3",
|
||||||
"sec": 6.44
|
"sec": 6.47
|
||||||
},
|
},
|
||||||
"andrea|L'Arcu, il mio amico di sempre. Davanti al circolino, come se ci fosse nato.": {
|
"andrea|L'Arcu, il mio amico di sempre. Davanti al circolino, come se ci fosse nato.": {
|
||||||
"file": "andrea-8ddab2d024.mp3",
|
"file": "andrea-8ddab2d024.mp3",
|
||||||
@@ -1351,6 +1343,22 @@
|
|||||||
"file": "andrea-bbd7781659.mp3",
|
"file": "andrea-bbd7781659.mp3",
|
||||||
"sec": 4.13
|
"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.": {
|
"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",
|
"file": "andrea-060513a152.mp3",
|
||||||
"sec": 8.65
|
"sec": 8.65
|
||||||
@@ -1575,13 +1583,49 @@
|
|||||||
"file": "andrea-ee455fdc33.mp3",
|
"file": "andrea-ee455fdc33.mp3",
|
||||||
"sec": 3.67
|
"sec": 3.67
|
||||||
},
|
},
|
||||||
|
"andrea|Qui dietro ci stanno i camper. Ma per noi questo resta il campo del Giannini.": {
|
||||||
|
"file": "andrea-195cf2b022.mp3",
|
||||||
|
"sec": 4.86
|
||||||
|
},
|
||||||
"andrea|Gli scivoli dei bambini. Una volta il pallone finiva lì una volta su tre.": {
|
"andrea|Gli scivoli dei bambini. Una volta il pallone finiva lì una volta su tre.": {
|
||||||
"file": "andrea-a3e36c12a1.mp3",
|
"file": "andrea-a3e36c12a1.mp3",
|
||||||
"sec": 5.4
|
"sec": 5.4
|
||||||
},
|
},
|
||||||
"andrea|Casa mia. Terzo piano, quello con la tapparella a metà: è la mia camera.": {
|
"andrea|L'Arcu, sotto la tenda del Trappola. Due minuti fa era a Campomigliaio.": {
|
||||||
"file": "andrea-acc7949f4c.mp3",
|
"file": "andrea-a837c17bc7.mp3",
|
||||||
"sec": 5.91
|
"sec": 5.75
|
||||||
|
},
|
||||||
|
"andrea|La tenda è il Trappola: sotto c'è il Barrettino.": {
|
||||||
|
"file": "andrea-3648b1b08e.mp3",
|
||||||
|
"sec": 2.98
|
||||||
|
},
|
||||||
|
"andrea|Dal Trappola ci si passa dopo. Prima il palio.": {
|
||||||
|
"file": "andrea-a93c65234e.mp3",
|
||||||
|
"sec": 4.28
|
||||||
|
},
|
||||||
|
"andrea|«Qui riposa la Ghiacciomobile, che si è schiantata lì.»": {
|
||||||
|
"file": "andrea-a9e7f3f9ad.mp3",
|
||||||
|
"sec": 3.11
|
||||||
|
},
|
||||||
|
"andrea|C'è anche la fotina. Pareva sempre nuova.": {
|
||||||
|
"file": "andrea-7c17aa384c.mp3",
|
||||||
|
"sec": 3.2
|
||||||
|
},
|
||||||
|
"andrea|Nessuno ha mai capito davvero il tuo immenso valore.": {
|
||||||
|
"file": "andrea-3e85200509.mp3",
|
||||||
|
"sec": 2.93
|
||||||
|
},
|
||||||
|
"andrea|Lassù in cima, di traverso: per partire ha bisogno della discesa. E intanto ferma il traffico.": {
|
||||||
|
"file": "andrea-96de1a155d.mp3",
|
||||||
|
"sec": 7.66
|
||||||
|
},
|
||||||
|
"andrea|È il Betino dell'Arcu. Senza una discesa davanti, un si muove.": {
|
||||||
|
"file": "andrea-3d15dd47bb.mp3",
|
||||||
|
"sec": 4.82
|
||||||
|
},
|
||||||
|
"andrea|Casa mia. Secondo piano: quella con la tapparella a metà è la camera di mia sorella, e a destra quella dei miei.": {
|
||||||
|
"file": "andrea-6c9e7f684e.mp3",
|
||||||
|
"sec": 8.72
|
||||||
},
|
},
|
||||||
"andrea|Se salgo, mi mettono ad apparecchiare. Prima il palio.": {
|
"andrea|Se salgo, mi mettono ad apparecchiare. Prima il palio.": {
|
||||||
"file": "andrea-8945f1786d.mp3",
|
"file": "andrea-8945f1786d.mp3",
|
||||||
@@ -1599,6 +1643,14 @@
|
|||||||
"file": "andrea-018183c4dd.mp3",
|
"file": "andrea-018183c4dd.mp3",
|
||||||
"sec": 5.82
|
"sec": 5.82
|
||||||
},
|
},
|
||||||
|
"andrea|Su questo campo ci abbiamo passato trentacinque anni, giorno, pomeriggio e sera, a giocare a calcio.": {
|
||||||
|
"file": "andrea-5550b0890f.mp3",
|
||||||
|
"sec": 7.74
|
||||||
|
},
|
||||||
|
"andrea|Le maglie a fare da pali, e i palloni tagliati che entravano nelle terrazze, ovunque.": {
|
||||||
|
"file": "andrea-f6c10fd559.mp3",
|
||||||
|
"sec": 6.28
|
||||||
|
},
|
||||||
"andrea|Un pino a ombrello enorme. Alla ricreazione, tutti all'ombra lì sotto. Sembra ieri.": {
|
"andrea|Un pino a ombrello enorme. Alla ricreazione, tutti all'ombra lì sotto. Sembra ieri.": {
|
||||||
"file": "andrea-0a073729fd.mp3",
|
"file": "andrea-0a073729fd.mp3",
|
||||||
"sec": 7.3
|
"sec": 7.3
|
||||||
@@ -2136,32 +2188,32 @@
|
|||||||
"sec": 3.41
|
"sec": 3.41
|
||||||
},
|
},
|
||||||
"arcu|E chi lo sposta???": {
|
"arcu|E chi lo sposta???": {
|
||||||
"file": "arcu-64735a5925.mp3",
|
"file": "arcu-6ecbd65952.mp3",
|
||||||
"sec": 0.97
|
"sec": 0.83
|
||||||
},
|
},
|
||||||
"arcu|Mah... un si fa una sega...": {
|
"arcu|Mah... un si fa una sega...": {
|
||||||
"file": "arcu-5d07c15776.mp3",
|
"file": "arcu-f086ae9a20.mp3",
|
||||||
"sec": 2.89
|
"sec": 4.42
|
||||||
},
|
},
|
||||||
"arcu|Fammi vedere... Verde. Come la Panda di mi' zio.": {
|
"arcu|Fammi vedere... Verde. Come la Panda di mi' zio.": {
|
||||||
"file": "arcu-af7afda339.mp3",
|
"file": "arcu-62e981592d.mp3",
|
||||||
"sec": 4.18
|
"sec": 6.29
|
||||||
},
|
},
|
||||||
"arcu|Verde! Allora il Regolo tifa Cardetole. Tradimento.": {
|
"arcu|Verde! Allora il Regolo tifa Cardetole. Tradimento.": {
|
||||||
"file": "arcu-85c7d0b8f6.mp3",
|
"file": "arcu-9e5b7c6e95.mp3",
|
||||||
"sec": 5.57
|
"sec": 4.75
|
||||||
},
|
},
|
||||||
"arcu|E te ci vai da solo? Io ti aspetto qui.": {
|
"arcu|E te ci vai da solo? Io ti aspetto qui.": {
|
||||||
"file": "arcu-e157a4b70b.mp3",
|
"file": "arcu-838aefc39e.mp3",
|
||||||
"sec": 2.73
|
"sec": 4.03
|
||||||
},
|
},
|
||||||
"arcu|Qualcuno deve raccontarlo, dopo.": {
|
"arcu|Qualcuno deve raccontarlo, dopo.": {
|
||||||
"file": "arcu-5d1fa73d32.mp3",
|
"file": "arcu-f19f64320f.mp3",
|
||||||
"sec": 1.77
|
"sec": 2.57
|
||||||
},
|
},
|
||||||
"arcu|Fai una cosa: se vedi due occhi gialli, un sono i fari della Provinciale.": {
|
"arcu|Fai una cosa: se vedi due occhi gialli, un sono i fari della Provinciale.": {
|
||||||
"file": "arcu-fdd1ab5f5b.mp3",
|
"file": "arcu-56966d2a60.mp3",
|
||||||
"sec": 4.37
|
"sec": 5.27
|
||||||
},
|
},
|
||||||
"andrea|Grazie, Arcu. Molto utile.": {
|
"andrea|Grazie, Arcu. Molto utile.": {
|
||||||
"file": "andrea-2caf0f40f2.mp3",
|
"file": "andrea-2caf0f40f2.mp3",
|
||||||
@@ -2172,12 +2224,12 @@
|
|||||||
"sec": 2.98
|
"sec": 2.98
|
||||||
},
|
},
|
||||||
"arcu|...I pompieri per cosa?": {
|
"arcu|...I pompieri per cosa?": {
|
||||||
"file": "arcu-5148dffe36.mp3",
|
"file": "arcu-5fa87608b0.mp3",
|
||||||
"sec": 1.7
|
"sec": 1.26
|
||||||
},
|
},
|
||||||
"arcu|Gnamo! Ma prima mi fo una segarettina di quelle mi fo io!": {
|
"arcu|Gnamo! Ma prima mi fo una segarettina di quelle mi fo io!": {
|
||||||
"file": "arcu-406cd02197.mp3",
|
"file": "arcu-ac54e4bfa2.mp3",
|
||||||
"sec": 4.46
|
"sec": 4.81
|
||||||
},
|
},
|
||||||
"andrea|S'ha andà a fallo?!?": {
|
"andrea|S'ha andà a fallo?!?": {
|
||||||
"file": "andrea-c80714aae7.mp3",
|
"file": "andrea-c80714aae7.mp3",
|
||||||
@@ -2223,6 +2275,70 @@
|
|||||||
"file": "pescatori-befa87eee8.mp3",
|
"file": "pescatori-befa87eee8.mp3",
|
||||||
"sec": 4.17
|
"sec": 4.17
|
||||||
},
|
},
|
||||||
|
"andrea|Ma ti ho visto du' minuti fa a Campomigliaio!": {
|
||||||
|
"file": "andrea-c2dca9d75b.mp3",
|
||||||
|
"sec": 3.99
|
||||||
|
},
|
||||||
|
"andrea|Si fa una gara coi motorini? Il giro della Fortezza!": {
|
||||||
|
"file": "andrea-a960f89e7a.mp3",
|
||||||
|
"sec": 3.45
|
||||||
|
},
|
||||||
|
"andrea|Ci si vede dopo, Arcu.": {
|
||||||
|
"file": "andrea-191021ad7c.mp3",
|
||||||
|
"sec": 1.68
|
||||||
|
},
|
||||||
|
"arcu|E allora? Col Betino ci metto un attimo.": {
|
||||||
|
"file": "arcu-3b540fe807.mp3",
|
||||||
|
"sec": 3.02
|
||||||
|
},
|
||||||
|
"arcu|In discesa.": {
|
||||||
|
"file": "arcu-a17af74745.mp3",
|
||||||
|
"sec": 0.67
|
||||||
|
},
|
||||||
|
"andrea|E in salita?": {
|
||||||
|
"file": "andrea-75c55bf1e7.mp3",
|
||||||
|
"sec": 1.57
|
||||||
|
},
|
||||||
|
"arcu|In salita si spinge.": {
|
||||||
|
"file": "arcu-747a365229.mp3",
|
||||||
|
"sec": 1.6
|
||||||
|
},
|
||||||
|
"arcu|Col Betino? Io un perdo mai: un vo mai forte abbastanza da cadere.": {
|
||||||
|
"file": "arcu-d2b7d66066.mp3",
|
||||||
|
"sec": 6
|
||||||
|
},
|
||||||
|
"arcu|Come la si fa?": {
|
||||||
|
"file": "arcu-fcaee733df.mp3",
|
||||||
|
"sec": 0.92
|
||||||
|
},
|
||||||
|
"andrea|Alla bona, un giro di paese.": {
|
||||||
|
"file": "andrea-b35a4d8cc0.mp3",
|
||||||
|
"sec": 2.15
|
||||||
|
},
|
||||||
|
"andrea|Ginocchio in terra.": {
|
||||||
|
"file": "andrea-e2ed3ea2f2.mp3",
|
||||||
|
"sec": 1.19
|
||||||
|
},
|
||||||
|
"andrea|No, lascia perdere.": {
|
||||||
|
"file": "andrea-98f155a986.mp3",
|
||||||
|
"sec": 1.09
|
||||||
|
},
|
||||||
|
"arcu|Alla bona. Chi arriva ultimo paga le paste.": {
|
||||||
|
"file": "arcu-19aa71408a.mp3",
|
||||||
|
"sec": 3.33
|
||||||
|
},
|
||||||
|
"arcu|Aspettate: prendo la rincorsa, che il Betino s'accende solo in discesa.": {
|
||||||
|
"file": "arcu-1a64606a56.mp3",
|
||||||
|
"sec": 4.74
|
||||||
|
},
|
||||||
|
"arcu|Seria. Tre giri, e la paglia in curva è vera.": {
|
||||||
|
"file": "arcu-5990b22d1d.mp3",
|
||||||
|
"sec": 3.95
|
||||||
|
},
|
||||||
|
"arcu|Ginocchio in terra? Puntella un vede l'ora.": {
|
||||||
|
"file": "arcu-26a5fb0d0c.mp3",
|
||||||
|
"sec": 3.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
|
||||||
@@ -2523,6 +2639,26 @@
|
|||||||
"file": "andrea-72bf22a3f9.mp3",
|
"file": "andrea-72bf22a3f9.mp3",
|
||||||
"sec": 4.49
|
"sec": 4.49
|
||||||
},
|
},
|
||||||
|
"andrea|Primo! E col Fifty di serie, o quasi.": {
|
||||||
|
"file": "andrea-f3d1a2d353.mp3",
|
||||||
|
"sec": 4.13
|
||||||
|
},
|
||||||
|
"arcu|Si rifà! Questa un vale, avevo la catena lenta.": {
|
||||||
|
"file": "arcu-98e193584b.mp3",
|
||||||
|
"sec": 5.34
|
||||||
|
},
|
||||||
|
"andrea|Ho preso tutta la polvere di quelli davanti.": {
|
||||||
|
"file": "andrea-4ca44c5405.mp3",
|
||||||
|
"sec": 2.18
|
||||||
|
},
|
||||||
|
"arcu|Il Fifty è bono. È chi ci sta sopra.": {
|
||||||
|
"file": "arcu-1c6016d941.mp3",
|
||||||
|
"sec": 3.2
|
||||||
|
},
|
||||||
|
"andrea|Lascio stare. Il Fifty mi serve intero, per il palio.": {
|
||||||
|
"file": "andrea-33e57caee9.mp3",
|
||||||
|
"sec": 4.61
|
||||||
|
},
|
||||||
"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
|
||||||
|
|||||||
@@ -145,3 +145,16 @@ si segna qui con la data e il punto di `src/story/story.js` dove sta; le persone
|
|||||||
la lenza vibra, si tira al momento giusto. Serve la scena del lago, dietro Cardetole.
|
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
|
- *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.
|
c'è già all'oratorio, qui conta la gag.
|
||||||
|
|
||||||
|
## La gara dei cinquantini (idea di Andrea, 28 ottobre)
|
||||||
|
|
||||||
|
Una gara di motorini fra amici sul circuito che negli anni '70 c'era dentro il campeggio Mugello Verde (Via
|
||||||
|
Massorondinaio 39, nel gioco c'è già la scena del campeggio): Puntella (Marco Villani, **consenso da chiedere**, fuori
|
||||||
|
dai deploy finché non c'è) con lo scooter SR, il più truccato di tutti; il Betino dell'Arcu; il Fifty di Andrea. Il
|
||||||
|
circuito lo disegna Codex.
|
||||||
|
|
||||||
|
Ricerca del 28 ottobre: sul web la pista del campeggio non risulta; il sito del campeggio elenca tennis, pesca,
|
||||||
|
equitazione, minigolf. Risulta invece il circuito cittadino della «I Coppa di San Piero a Sieve», Formula 3,
|
||||||
|
11 giugno 1967: 5.600 m in senso orario attorno al poggio della Fortezza, partenza davanti alla scuola in Via
|
||||||
|
Provinciale, con la strada di Massorondinaio asfaltata apposta come bretella (fonte: CAMET, «storia circuito San
|
||||||
|
Piero»). Il disegno della pista del campeggio va preso dai ricordi di Andrea **[DA ANDREA]**.
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ Si aggiorna con `make deploy`: pubblica l'ultimo commit, immagine costruita e pr
|
|||||||
- **Altre scene** (ottobre 2026, dalle foto di Andrea con Codex): Via de' Medici, Oratorio, Ingresso del campeggio,
|
- **Altre scene** (ottobre 2026, dalle foto di Andrea con Codex): Via de' Medici, Oratorio, Ingresso del campeggio,
|
||||||
Bosco ai Frati, viale dei cipressi. Schermata del titolo (Fortezza al tramonto col Regolo) e introduzione a righe sul
|
Bosco ai Frati, viale dei cipressi. Schermata del titolo (Fortezza al tramonto col Regolo) e introduzione a righe sul
|
||||||
panorama del Mugello.
|
panorama del Mugello.
|
||||||
- **Musica** di Andrea (Suno) in `assets/audio/`: titolo e introduzione, paese, ricordi (Trebbio e fine); volume e muto.
|
- **Musica** di Andrea (Suno) in `assets/audio/`: titolo e introduzione, paese, ricordi (Trebbio e fine), il GP di Fortezza (`gara.mp3`); volume e muto.
|
||||||
- **Andrea vero** dal foglio sprite (`assets/characters/`, `tools/slice_sprites.py` + `tools/prepare_character.py`):
|
- **Andrea vero** dal foglio sprite (`assets/characters/`, `tools/slice_sprites.py` + `tools/prepare_character.py`):
|
||||||
cammina, parla, si china a prendere, di spalle davanti alle porte.
|
cammina, parla, si china a prendere, di spalle davanti alle porte.
|
||||||
- **Storia di prova** (`src/story/story.js`): il palio è sparito, i vecchietti litigano sulle due leggende, la squama
|
- **Storia di prova** (`src/story/story.js`): il palio è sparito, i vecchietti litigano sulle due leggende, la squama
|
||||||
@@ -214,6 +214,10 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso
|
|||||||
- Il sentiero della Fortezza (foto di Andrea, 28 ottobre, sfondo di Codex): fra la Fortezza (a diritto) e la casa del
|
- 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).
|
maestro Bacci (alle spalle), che non sono più collegate direttamente. La pozza c'è anche il 15 d'agosto (Andrea).
|
||||||
Il nome del sentiero è provvisorio.
|
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
|
- 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
|
||||||
@@ -239,6 +243,35 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso
|
|||||||
risultato deciso all'ultimo pesce). Restano le sue idee: «Porta a casa la pescata» a gloria raggiunta; premio per
|
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
|
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.
|
Fortezza); abboccata e tensione anche nei comandi HTML e un richiamo grande sul telefono; Esc col fuoco sui pulsanti.
|
||||||
|
- Il GP di Fortezza (Andrea, 28 ottobre): la gara dei cinquantini attorno alle mura della Fortezza, vista dall'alto
|
||||||
|
come le vecchie gare da sala giochi. Dall'Arcu sotto la tenda del Trappola in Via Liguri Magelli «Si fa una gara coi
|
||||||
|
motorini?», poi Alla bona, Seria o Ginocchio in terra; prima del via l'Arcu prende la rincorsa per accendere il Betino
|
||||||
|
in discesa. Minigioco `src/engine/gara.js`: gas tenendo premuto (Spazio, ↑, mouse, dito), sterzo con ← →,
|
||||||
|
trascinando di lato o coi pulsanti Sterza, ↓ frena; erba che frena, paglia che ferma, acceleratori, lattine di miscela
|
||||||
|
(sprint), buche (forte si casca); gli avversari sorpassano e scansano le buche. Prima del semaforo i piloti
|
||||||
|
arrivano in griglia uno alla volta col cartellino: il personaggio (sprite del gioco, o ritratto: Cistola dal murale,
|
||||||
|
Puntella sagoma finché manca il consenso), il motorino dipinto da Codex dalle foto (`assets/closeups/gp/`, foglio
|
||||||
|
`assets/raw/motorini-gp-sheet.png`, prompt `prompts/personaggi/motorini-gp-sheet.txt`), motore e testa; un tocco la salta. Gas, freno e sterzo hanno
|
||||||
|
fonti indipendenti (tasti, un dito sulla scena, pulsanti Gas, Sterza e Frena), il freno vince sul gas. Quadro di Codex
|
||||||
|
`assets/closeups/gp-fortezza.png` (prompt `prompts/closeups/gp-fortezza.txt`, guida dalla planimetria
|
||||||
|
`assets/raw/fortezza-planimetria.png`, satellite in `assets/raw/fortezza-satellite.png`); il tracciato è ricalcato
|
||||||
|
sulla strada dipinta. Disegno `gara-render.js`, test `tests/gara.test.mjs`. I piloti sono dati in
|
||||||
|
`src/story/piloti.js`: una riga per amico, `motore` e `testa` da 1 a 5, colori dalle foto. Corrono Puntella (SR,
|
||||||
|
colori Aprilia da GP provvisori), Cistola (CR 50 Z), Andrea (Malaguti Fifty rosso e nero), Tozzino (Booster blu
|
||||||
|
scuro, di serie, casco alla Damon Hill di Codex: `prompts/personaggi/casco-tozzino.txt`), l'Arcu (Betino). Col telefono
|
||||||
|
si guida con un dito: premuto è gas, lo swipe indica dove andare sullo schermo (Codex, 28 ottobre). **Puntella (Marco Villani) non corre finché manca il suo consenso** (`consenso: null` in piloti.js: quando arriva, la data). Mancano: quanto erano
|
||||||
|
truccati gli altri, le battute di Andrea, il rumore del motore (c'è solo
|
||||||
|
quello dell'arrivo in griglia); i confini della paglia non si vedono sul quadro (revisione di Codex, punto 9).
|
||||||
|
- Strade attorno a casa (Andrea, 28 ottobre): la scena `area_camper` ora si chiama «Le villette del Giannini» (ci abita
|
||||||
|
lui; i camper stanno dietro il suo campo). Cardetole: in basso a destra «dal Giannini» (le villette), a destra il
|
||||||
|
ponticino della Carza, a sinistra il Trappola. Il campo del Giannini: a destra le villette, a sinistra il Trappola;
|
||||||
|
dal ponticino non ci si va. Via Liguri Magelli: lassù vicino al Betino si torna a casa, laggiù a destra il campo.
|
||||||
|
Da sapere: dove si gira, in fondo al ponticino, per Cardetole [DA ANDREA].
|
||||||
|
- Via Liguri Magelli (Street View di Andrea, 28 ottobre), a sinistra dal campo del Giannini: a destra la casa salmone
|
||||||
|
con la tenda del Trappola (il Barrettino, «Vai dal Trappola»: niente interno, [DA ANDREA]), l'Arcu sotto la tenda
|
||||||
|
(scherzo: «ti ho visto du' minuti fa a Campomigliaio» col Betino, poi la sfida del GP), davanti la targa «Qui riposa la
|
||||||
|
Ghiacciomobile, che si è schiantata lì», in cima alla salita il Betino dell'Arcu (tolto dal circolino di
|
||||||
|
Campomigliaio), che per partire ha bisogno della discesa. Prompt `prompts/backgrounds/via-liguri-magelli.txt`.
|
||||||
- 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.
|
||||||
|
|
||||||
|
|||||||
@@ -199,6 +199,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<button id="bocce-right" type="button" aria-label="Sposta la mira a destra">Mira →</button>
|
<button id="bocce-right" type="button" aria-label="Sposta la mira a destra">Mira →</button>
|
||||||
<button id="bocce-throw" type="button">Lancia</button>
|
<button id="bocce-throw" type="button">Lancia</button>
|
||||||
|
<button id="gara-brake" type="button" hidden>Frena</button>
|
||||||
</div>
|
</div>
|
||||||
<details id="bocce-help"><summary>Come si gioca</summary>
|
<details id="bocce-help"><summary>Come si gioca</summary>
|
||||||
<button class="tutorial-open" type="button" data-tutorial="bocce" hidden>Guarda il video</button>
|
<button class="tutorial-open" type="button" data-tutorial="bocce" hidden>Guarda il video</button>
|
||||||
@@ -219,6 +220,18 @@
|
|||||||
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="gara-help" hidden><summary>Come si gioca</summary>
|
||||||
|
<p>Il GP di Fortezza: tre giri attorno alle mura coi cinquantini. Con un solo dito: tieni premuto sulla scena per
|
||||||
|
il gas e trascina verso la direzione in cui vuoi andare sullo schermo. A destra vai verso destra, anche quando
|
||||||
|
il motorino scende. I piccoli movimenti non sterzano; il cerchietto segue gli swipe lunghi. Alza il dito per
|
||||||
|
mollare il gas e raddrizzare lo sterzo. Funziona anche col mouse premuto.</p>
|
||||||
|
<p>Con la tastiera: Spazio, Invio o ↑ per il gas, ← → per sterzare; oppure tieni premuti i pulsanti Sterza.
|
||||||
|
↓ (o il pulsante Frena, tenuto premuto) frena, anche col gas dato. Dal pannello il
|
||||||
|
pulsante alterna Gas e Molla. Durante la presentazione dei piloti un tocco passa al prossimo.</p>
|
||||||
|
<p>Le frecce gialle in terra sono acceleratori; le lattine rosse di miscela danno uno sprint (e ricompaiono); nelle
|
||||||
|
buche, se ci passi forte, caschi. Fuori strada l'erba frena, e nella paglia ci si ferma. Alla bona la pista mostra
|
||||||
|
in rosso dove frenare, Seria solo i coni, Ginocchio in terra niente. Esc esce.</p>
|
||||||
|
</details>
|
||||||
<details id="pesca-help" hidden><summary>Come si gioca</summary>
|
<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ù
|
<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
|
lunghe). Tieni premuto sulla scena (o Spazio): la forza riparte da zero e sale, lascia per lanciare; oppure premi
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "leggenda-del-regolo",
|
"name": "leggenda-del-regolo",
|
||||||
"version": "0.1.29",
|
"version": "0.1.33",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
// Senza verbo scelto vale il clic intelligente: l'azione più sensata per quel punto.
|
// Senza verbo scelto vale il clic intelligente: l'azione più sensata per quel punto.
|
||||||
import { VERB_LABELS } from './game.js';
|
import { VERB_LABELS } from './game.js';
|
||||||
|
|
||||||
|
// «Vai a» davanti a un nome che dice già dove («dal Trappola», «a destra, …», «verso …», «indietro, …»): solo «Vai».
|
||||||
|
const WHERE = /^(da(l|lla|i|gli|ll')?|a|verso|indietro|su|giù|in|laggiù|lassù|dietro|oltre)(?=[\s,]|$)/i;
|
||||||
|
const verbLabel = (verb, name) => verb === 'walk' && WHERE.test(name ?? '') ? 'Vai' : VERB_LABELS[verb];
|
||||||
|
|
||||||
export class Command {
|
export class Command {
|
||||||
constructor(game) {
|
constructor(game) {
|
||||||
this.game = game;
|
this.game = game;
|
||||||
@@ -21,10 +25,10 @@ export class Command {
|
|||||||
const g = this.game, name = target && g.targetName(target);
|
const g = this.game, name = target && g.targetName(target);
|
||||||
if (this.item && target === this.item && this.verb === 'use') return `${VERB_LABELS.use} ${g.itemName(this.item)}`;
|
if (this.item && target === this.item && this.verb === 'use') return `${VERB_LABELS.use} ${g.itemName(this.item)}`;
|
||||||
if (this.item) return `${VERB_LABELS[this.verb]} ${g.itemName(this.item)} ${this.verb === 'give' ? 'a' : 'con'}${name ? ' ' + name : '…'}`;
|
if (this.item) return `${VERB_LABELS[this.verb]} ${g.itemName(this.item)} ${this.verb === 'give' ? 'a' : 'con'}${name ? ' ' + name : '…'}`;
|
||||||
if (this.verb) return `${VERB_LABELS[this.verb]}${name ? ' ' + name : '…'}`;
|
if (this.verb) return `${verbLabel(this.verb, name)}${name ? ' ' + name : '…'}`;
|
||||||
if (!target) return '';
|
if (!target) return '';
|
||||||
const isItem = !!g.story.items[target];
|
const isItem = !!g.story.items[target];
|
||||||
return isItem ? `Usa ${name} con…` : `${VERB_LABELS[g.defaultVerb(target)]} ${name}`;
|
return isItem ? `Usa ${name} con…` : `${verbLabel(g.defaultVerb(target), name)} ${name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clic sulla scena: `look` per il clic destro o la pressione lunga, `hurry` per il doppio clic (passo doppio),
|
// Clic sulla scena: `look` per il clic destro o la pressione lunga, `hurry` per il doppio clic (passo doppio),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import * as pesca from './pesca.js';
|
|||||||
import * as briscola from './briscola.js';
|
import * as briscola from './briscola.js';
|
||||||
import * as tressette from './tressette.js';
|
import * as tressette from './tressette.js';
|
||||||
import * as scopa from './scopa.js';
|
import * as scopa from './scopa.js';
|
||||||
|
import * as gara from './gara.js';
|
||||||
|
|
||||||
// I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta.
|
// I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta.
|
||||||
export const MINIGAMES = {
|
export const MINIGAMES = {
|
||||||
@@ -18,6 +19,7 @@ export const MINIGAMES = {
|
|||||||
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 },
|
pesca: { create: (seed, level) => pesca.newPesca(seed, { level }), update: pesca.update, press: pesca.press, release: pesca.release, cancel: pesca.cancel, aim: pesca.aimScreen },
|
||||||
|
gara: { create: (seed, level, deck, def) => gara.newGara(seed, { level, riders: def.riders }), update: gara.update, press: gara.press, release: gara.release, cancel: gara.cancel, aim: gara.aimScreen },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VERBS = ['walk', 'look', 'take', 'use', 'talk', 'open', 'give', 'drop'];
|
export const VERBS = ['walk', 'look', 'take', 'use', 'talk', 'open', 'give', 'drop'];
|
||||||
@@ -486,7 +488,7 @@ export class Game {
|
|||||||
// `level`: «Alla bona», «Seria» o «A fassi male», scelto nel dialogo con l'avversario.
|
// `level`: «Alla bona», «Seria» o «A fassi male», scelto nel dialogo con l'avversario.
|
||||||
// `decks`: il mazzo da usare quando il suo flag è acceso (briscola: le napoletane al posto delle toscane).
|
// `decks`: il mazzo da usare quando il suo flag è acceso (briscola: le napoletane al posto delle toscane).
|
||||||
: MINIGAMES[type] ? { id: e.closeup, ...MINIGAMES[type].create(Math.floor(Math.random() * 4294967296), e.level,
|
: MINIGAMES[type] ? { id: e.closeup, ...MINIGAMES[type].create(Math.floor(Math.random() * 4294967296), e.level,
|
||||||
Object.keys(this.story.closeups[e.closeup].decks ?? {}).find(d => s.flags[this.story.closeups[e.closeup].decks[d]])) }
|
Object.keys(this.story.closeups[e.closeup].decks ?? {}).find(d => s.flags[this.story.closeups[e.closeup].decks[d]]), this.story.closeups[e.closeup]) }
|
||||||
: { id: e.closeup, step: 'cover', t: 0, age: 0 };
|
: { id: e.closeup, step: 'cover', t: 0, age: 0 };
|
||||||
}
|
}
|
||||||
else throw new Error('Effetto sconosciuto: ' + JSON.stringify(e));
|
else throw new Error('Effetto sconosciuto: ' + JSON.stringify(e));
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
import { TRACK, CURVES, PICKUPS, BRAKE, LAPS, COUNTDOWN, ROAD, VERGE, CAN, SWIPE, pointAt, standings, player, lapOf, presenting, placeOf, INTRO } from './gara.js';
|
||||||
|
import { text, panel } from './bocce-render.js';
|
||||||
|
import { ambientHash as hash } from './ambient.js';
|
||||||
|
|
||||||
|
// Il GP di Fortezza visto dall'alto. Col quadro di Codex (def.picture) la strada e la Fortezza sono già dipinte; senza,
|
||||||
|
// si disegnano qui dai dati del tracciato. Sopra, sempre: la guida, gli acceleratori, le buche, le lattine, i motorini.
|
||||||
|
function stroke(ctx, from, to, width, color, lane = 0) {
|
||||||
|
ctx.strokeStyle = color; ctx.lineWidth = width; ctx.lineCap = 'round';
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let s = from; s <= to; s += 2) { const p = pointAt(s, lane); s === from ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y); }
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
function loop(ctx, width, color) {
|
||||||
|
ctx.strokeStyle = color; ctx.lineWidth = width; ctx.lineJoin = 'round';
|
||||||
|
ctx.beginPath();
|
||||||
|
TRACK.pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
|
||||||
|
ctx.closePath(); ctx.stroke();
|
||||||
|
}
|
||||||
|
function ground(ctx) {
|
||||||
|
ctx.fillStyle = '#6f8f3e'; ctx.fillRect(0, 0, 640, 360);
|
||||||
|
for (let i = 0; i < 260; i++) {
|
||||||
|
ctx.fillStyle = hash(i * 13 + 1) > 0.5 ? '#7c9b47' : '#638236';
|
||||||
|
ctx.fillRect(Math.floor(hash(i * 13 + 2) * 640), Math.floor(hash(i * 13 + 3) * 360), 2 + Math.floor(hash(i * 13 + 4) * 6), 2);
|
||||||
|
}
|
||||||
|
// Dentro il giro, le mura della Fortezza e il prato dei bastioni.
|
||||||
|
ctx.beginPath();
|
||||||
|
TRACK.pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = '#8a7a5a'; ctx.fill();
|
||||||
|
loop(ctx, VERGE * 2 + 8, '#6f8f3e');
|
||||||
|
loop(ctx, VERGE * 2, '#7c9b47');
|
||||||
|
loop(ctx, ROAD * 2 + 2, '#7a6446');
|
||||||
|
loop(ctx, ROAD * 2, '#b59b72');
|
||||||
|
for (let i = 0; i < TRACK.pts.length; i += 3) {
|
||||||
|
const [x, y] = TRACK.pts[i], o = (hash(i) - 0.5) * ROAD * 1.6;
|
||||||
|
ctx.fillStyle = hash(i + 7) > 0.5 ? '#a88d64' : '#c2a87e'; ctx.fillRect(Math.round(x + o * 0.5), Math.round(y + o * 0.5), 2, 1);
|
||||||
|
}
|
||||||
|
// La paglia fuori dalle curve.
|
||||||
|
for (const k of CURVES) for (let s = k.start; s <= k.end; s += 8) {
|
||||||
|
const p = pointAt(s, -(VERGE + 3) * k.side);
|
||||||
|
ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.heading);
|
||||||
|
ctx.fillStyle = '#d9b75a'; ctx.fillRect(-3.5, -2.5, 7, 5); ctx.fillStyle = '#b8953f'; ctx.fillRect(-3.5, -0.5, 7, 1);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
const p = pointAt(0);
|
||||||
|
ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.heading);
|
||||||
|
for (let i = -6; i < 6; i++) for (let j = 0; j < 2; j++) { ctx.fillStyle = (i + j) % 2 ? '#111' : '#f4f4f4'; ctx.fillRect(j * 2 - 2, i * 2, 2, 2); }
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
function guide(ctx, c) {
|
||||||
|
const v = player(c).vmax;
|
||||||
|
// Alla bona, in rosso dove si frena per arrivare giusti in curva; Seria, un cono all'inizio della frenata.
|
||||||
|
for (const k of CURVES) {
|
||||||
|
const from = k.apex - (v * v - k.speed * k.speed * 0.7) / (2 * BRAKE);
|
||||||
|
if (c.skill.guide === 'zone') stroke(ctx, from, k.apex, 4, 'rgba(214,64,47,0.6)');
|
||||||
|
if (c.skill.guide === 'coni') {
|
||||||
|
const p = pointAt(from, (ROAD + 3) * k.side);
|
||||||
|
ctx.fillStyle = '#f07a1a'; ctx.beginPath(); ctx.moveTo(p.x, p.y - 5); ctx.lineTo(p.x + 3, p.y + 2); ctx.lineTo(p.x - 3, p.y + 2); ctx.fill();
|
||||||
|
ctx.fillStyle = '#fff'; ctx.fillRect(p.x - 1.5, p.y - 2, 3, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function things(ctx, c) {
|
||||||
|
for (const p of PICKUPS) {
|
||||||
|
ctx.save(); ctx.translate(Math.round(p.x), Math.round(p.y)); ctx.rotate(p.heading);
|
||||||
|
if (p.kind === 'boost') {
|
||||||
|
// L'acceleratore: tre frecce gialle che lampeggiano in fila.
|
||||||
|
for (let k = 0; k < 3; k++) {
|
||||||
|
ctx.fillStyle = Math.floor(c.age * 6 - k) % 3 === 0 ? '#fff27a' : '#e0b020';
|
||||||
|
ctx.beginPath(); ctx.moveTo(-6 + k * 5, -5); ctx.lineTo(-2 + k * 5, 0); ctx.lineTo(-6 + k * 5, 5); ctx.lineTo(-4 + k * 5, 0); ctx.fill();
|
||||||
|
}
|
||||||
|
} else if (p.kind === 'buca') {
|
||||||
|
ctx.fillStyle = '#5a4630'; ctx.beginPath(); ctx.ellipse(0, 0, 6, 4.5, 0, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.fillStyle = '#2e2418'; ctx.beginPath(); ctx.ellipse(0.5, 0.5, 4.5, 3, 0, 0, Math.PI * 2); ctx.fill();
|
||||||
|
} else if ((c.taken[p.id] ?? -Infinity) <= c.t - CAN.respawn) {
|
||||||
|
// La lattina della miscela, che saltella.
|
||||||
|
const y = Math.round(Math.sin(c.age * 5 + p.id) * 1);
|
||||||
|
ctx.rotate(-p.heading);
|
||||||
|
ctx.fillStyle = '#f4e3c2'; ctx.fillRect(-3, -4 + y, 6, 8);
|
||||||
|
ctx.fillStyle = '#c8321e'; ctx.fillRect(-2, -3 + y, 4, 6);
|
||||||
|
ctx.fillStyle = '#f0c23a'; ctx.fillRect(-2, -1 + y, 4, 1); ctx.fillRect(-1, -5 + y, 2, 1);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function moped(ctx, r, c) {
|
||||||
|
const out = r.out > 0;
|
||||||
|
ctx.save(); ctx.translate(Math.round(r.x), Math.round(r.y)); ctx.rotate(r.a + (out ? Math.sin(c.age * 22) * 0.6 : 0));
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.3)'; ctx.fillRect(-6, -1, 13, 4);
|
||||||
|
ctx.fillStyle = '#151515'; ctx.fillRect(-7, -1, 3, 2); ctx.fillRect(5, -1, 3, 2); // le ruote
|
||||||
|
ctx.fillStyle = '#f4e3c2'; ctx.fillRect(-5, -3, 11, 6); // il bordo chiaro: anche i motorini scuri si vedono
|
||||||
|
ctx.fillStyle = r.color; ctx.fillRect(-4, -2, 9, 4);
|
||||||
|
ctx.fillStyle = r.trim ?? '#1c1c1c'; ctx.fillRect(-4, -0.5, 9, 1);
|
||||||
|
ctx.fillStyle = '#2a2a2a'; ctx.fillRect(4, -3, 1, 6); // il manubrio
|
||||||
|
ctx.fillStyle = r.helmet ?? (r.id === 'andrea' ? '#f4e3c2' : '#2a2a2a'); ctx.beginPath(); ctx.arc(-1, 0, 2, 0, Math.PI * 2); ctx.fill(); // il casco
|
||||||
|
if (r.stripe) { ctx.fillStyle = r.stripe; ctx.fillRect(-2, -0.5, 2, 1); }
|
||||||
|
// La miscela: una fiammata dietro.
|
||||||
|
if (r.boost > 0 && Math.floor(c.age * 20) % 2) { ctx.fillStyle = '#ffb030'; ctx.fillRect(-10, -1, 3, 2); }
|
||||||
|
ctx.restore();
|
||||||
|
if (r.id === 'andrea') { ctx.strokeStyle = '#f4e3c2'; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(r.x, r.y, 10, 0, Math.PI * 2); ctx.stroke(); }
|
||||||
|
}
|
||||||
|
// In basso a sinistra, lontano dalla pausa e dal pulsante per uscire: il giro, la classifica e il tachimetro.
|
||||||
|
function hud(ctx, c) {
|
||||||
|
const me = player(c), order = standings(c), h = 34 + order.length * 11, y = 354 - h;
|
||||||
|
panel(ctx, 4, y, 116, h);
|
||||||
|
text(ctx, `GIRO ${lapOf(me)}/${LAPS}`, 10, y + 9, 7);
|
||||||
|
order.forEach((r, i) => {
|
||||||
|
ctx.fillStyle = '#f4e3c2'; ctx.fillRect(9, y + 18 + i * 11, 8, 8);
|
||||||
|
ctx.fillStyle = r.color; ctx.fillRect(10, y + 19 + i * 11, 6, 6);
|
||||||
|
text(ctx, `${i + 1}. ${r.name}`, 21, y + 23 + i * 11, 6, r.id === 'andrea' ? '#ffe28a' : '#f4e3c2');
|
||||||
|
});
|
||||||
|
const k = Math.min(1, me.v / me.vmax), bar = y + h - 11;
|
||||||
|
ctx.fillStyle = '#2a2f2c'; ctx.fillRect(10, bar, 50, 5);
|
||||||
|
ctx.fillStyle = me.boost > 0 ? '#ffb030' : k > 0.9 ? '#e0583a' : '#b9d392'; ctx.fillRect(10, bar, Math.round(50 * k), 5);
|
||||||
|
text(ctx, `${Math.round(me.v * 0.5)} km/h`, 116, bar + 3, 6, '#f4e3c2', 'right');
|
||||||
|
}
|
||||||
|
// L'origine mobile dello swipe e la direzione scelta col dito (o col mouse premuto).
|
||||||
|
function swipe(ctx, c) {
|
||||||
|
if (c.pointer === null || !c.anchor || !c.finger) return;
|
||||||
|
const [x, y] = c.anchor, [fx, fy] = c.finger, dx = fx - x, dy = fy - y, length = Math.hypot(dx, dy);
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = 'rgba(244,227,194,0.55)'; ctx.lineWidth = 1.5; ctx.lineCap = 'round';
|
||||||
|
ctx.beginPath(); ctx.arc(x, y, 4, 0, Math.PI * 2); ctx.stroke();
|
||||||
|
if (length >= SWIPE.dead) {
|
||||||
|
const ux = dx / length, uy = dy / length;
|
||||||
|
ctx.beginPath(); ctx.moveTo(x + ux * 6, y + uy * 6); ctx.lineTo(fx, fy);
|
||||||
|
ctx.moveTo(fx - ux * 6 - uy * 3, fy - uy * 6 + ux * 3); ctx.lineTo(fx, fy);
|
||||||
|
ctx.lineTo(fx - ux * 6 + uy * 3, fy - uy * 6 - ux * 3); ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
// La presentazione: il cartellino di chi arriva. A sinistra il personaggio (lo sprite del gioco, o il ritratto), a
|
||||||
|
// destra il suo motorino dipinto, sotto quanto va (motore) e quanto rischia (testa). `look(src)` dà l'immagine caricata,
|
||||||
|
// `sheet(id)` lo sprite di un personaggio.
|
||||||
|
function figure(ctx, r, x, y, look, sheet) {
|
||||||
|
const s = r.sprite && sheet(r.sprite);
|
||||||
|
if (s) {
|
||||||
|
const f = s.atlas.frames.stand ?? Object.values(s.atlas.frames)[0], k = Math.min(1, 84 / f.h);
|
||||||
|
ctx.drawImage(s.img, f.x, f.y, f.w, f.h, Math.round(x - f.w * k / 2), Math.round(y + 88 - f.h * k), Math.round(f.w * k), Math.round(f.h * k));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const img = r.face && look(r.face);
|
||||||
|
ctx.fillStyle = '#f4e3c2'; ctx.fillRect(x - 31, y + 13, 62, 62);
|
||||||
|
if (img) { ctx.drawImage(img, x - 30, y + 14, 60, 60); return; }
|
||||||
|
// Nessun ritratto (ancora): la sagoma.
|
||||||
|
ctx.fillStyle = '#2a2f2c'; ctx.fillRect(x - 30, y + 14, 60, 60);
|
||||||
|
ctx.fillStyle = '#4a504c'; ctx.beginPath(); ctx.arc(x, y + 36, 11, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.fillRect(x - 18, y + 52, 36, 22);
|
||||||
|
text(ctx, '?', x, y + 37, 10, '#8f9488', 'center');
|
||||||
|
}
|
||||||
|
function card(ctx, c, look, sheet) {
|
||||||
|
const r = presenting(c);
|
||||||
|
if (!r) { panel(ctx, 220, 130, 200, 40); text(ctx, 'GP DI FORTEZZA', 320, 142, 7, '#ffe28a', 'center'); text(ctx, 'TUTTI IN GRIGLIA!', 320, 158, 7, '#f4e3c2', 'center'); return; }
|
||||||
|
const x = 150, y = 92, w = 340, h = 150;
|
||||||
|
panel(ctx, x, y, w, h);
|
||||||
|
text(ctx, 'GP DI FORTEZZA', x + w / 2, y + 10, 6, '#ffe28a', 'center');
|
||||||
|
figure(ctx, r, x + 46, y + 14, look, sheet);
|
||||||
|
// Il casco di chi ce l'ha disegnato, accanto al personaggio.
|
||||||
|
const casco = r.casco && look(r.casco);
|
||||||
|
if (casco) { const k = 30 / Math.max(casco.width, casco.height); ctx.drawImage(casco, Math.round(x + 66), Math.round(y + 20), Math.round(casco.width * k), Math.round(casco.height * k)); }
|
||||||
|
// Il motorino, entra da sinistra e si ferma (come in pista).
|
||||||
|
const moto = r.moto && look(r.moto), t = Math.min(1, (c.t % INTRO.each) / 0.6), e = 1 - (1 - t) ** 3;
|
||||||
|
if (moto) {
|
||||||
|
const k = Math.min(150 / moto.width, 80 / moto.height), mw = moto.width * k, mh = moto.height * k;
|
||||||
|
ctx.save(); ctx.beginPath(); ctx.rect(x + 92, y + 18, w - 100, 92); ctx.clip();
|
||||||
|
ctx.drawImage(moto, Math.round(x + 92 + (w - 100 - mw) / 2 - (1 - e) * 120), Math.round(y + 104 - mh), Math.round(mw), Math.round(mh));
|
||||||
|
ctx.restore();
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = r.color; ctx.fillRect(x + 160, y + 70, 100, 22);
|
||||||
|
ctx.fillStyle = r.trim ?? '#1c1c1c'; ctx.fillRect(x + 160, y + 80, 100, 3);
|
||||||
|
}
|
||||||
|
text(ctx, r.name, x + 100, y + 118, 8, r.id === 'andrea' ? '#ffe28a' : '#f4e3c2');
|
||||||
|
text(ctx, r.bike, x + 100, y + 130, 6, '#c5c6b6');
|
||||||
|
for (const [k, [label, value]] of [['MOTORE', r.motore], ['TESTA', r.testa]].entries()) {
|
||||||
|
const yy = y + 116 + k * 12;
|
||||||
|
text(ctx, label, x + 222, yy, 5, '#c5c6b6');
|
||||||
|
for (let i = 0; i < 5; i++) { ctx.fillStyle = i < value ? (k ? '#b9d392' : '#e0583a') : '#3a3f3c'; ctx.fillRect(x + 262 + i * 13, yy - 3, 10, 6); }
|
||||||
|
}
|
||||||
|
text(ctx, 'TOCCA: IL PROSSIMO', x + w / 2, y + h - 6, 5, '#8f9488', 'center');
|
||||||
|
}
|
||||||
|
// La posizione in corsa, grande in alto a sinistra, e un cartellino sopra il Fifty.
|
||||||
|
function position(ctx, c) {
|
||||||
|
const me = player(c), place = placeOf(c), n = c.riders.length, first = place === 1;
|
||||||
|
panel(ctx, 4, 4, 70, 40);
|
||||||
|
text(ctx, 'POSIZIONE', 39, 11, 5, '#c5c6b6', 'center');
|
||||||
|
text(ctx, `${place}°`, 11, 29, 14, first ? '#ffe28a' : '#f4e3c2');
|
||||||
|
text(ctx, `su ${n}`, 68, 33, 6, '#c5c6b6', 'right');
|
||||||
|
if (me.shown === false) return;
|
||||||
|
const x = Math.round(me.x), y = Math.round(me.y) - 17;
|
||||||
|
ctx.fillStyle = 'rgba(22,25,23,0.85)'; ctx.fillRect(x - 8, y - 5, 16, 10);
|
||||||
|
ctx.fillStyle = first ? '#ffe28a' : '#f4e3c2'; ctx.fillRect(x - 1, y + 5, 2, 2);
|
||||||
|
text(ctx, `${place}°`, x, y + 0.5, 6, first ? '#ffe28a' : '#f4e3c2', 'center');
|
||||||
|
}
|
||||||
|
function lights(ctx, c) {
|
||||||
|
const lit = Math.min(3, Math.floor(c.t) + 1);
|
||||||
|
panel(ctx, 270, 140, 100, 50);
|
||||||
|
text(ctx, 'GP DI FORTEZZA', 320, 150, 6, '#ffe28a', 'center');
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
ctx.fillStyle = i < lit ? '#e0402a' : '#3a2a28';
|
||||||
|
ctx.beginPath(); ctx.arc(300 + i * 20, 170, 7, 0, Math.PI * 2); ctx.fill();
|
||||||
|
}
|
||||||
|
if (c.t >= COUNTDOWN - 0.01) text(ctx, 'VIA!', 320, 200, 10, '#b9d392', 'center');
|
||||||
|
}
|
||||||
|
function result(ctx, c) {
|
||||||
|
const order = c.result.order.map(id => c.riders.find(r => r.id === id));
|
||||||
|
panel(ctx, 200, 96, 240, 44 + order.length * 14);
|
||||||
|
text(ctx, 'GP DI FORTEZZA', 320, 108, 6, '#c5c6b6', 'center');
|
||||||
|
text(ctx, c.result.place === 1 ? 'PRIMO!' : `${c.result.place}° POSTO`, 320, 122, 10, c.result.place === 1 ? '#ffe28a' : '#f4e3c2', 'center');
|
||||||
|
order.forEach((r, i) => {
|
||||||
|
ctx.fillStyle = '#f4e3c2'; ctx.fillRect(213, 137 + i * 14, 10, 10);
|
||||||
|
ctx.fillStyle = r.color; ctx.fillRect(214, 138 + i * 14, 8, 8);
|
||||||
|
text(ctx, `${i + 1}. ${r.name}`, 228, 142 + i * 14, 7, r.id === 'andrea' ? '#ffe28a' : '#f4e3c2');
|
||||||
|
text(ctx, `${c.result.times[r.id].toFixed(1)}s`, 428, 142 + i * 14, 7, '#c5c6b6', 'right');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawGara(ctx, c, def, bg, look = () => null, sheet = () => null) {
|
||||||
|
ctx.save();
|
||||||
|
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
|
||||||
|
else ground(ctx);
|
||||||
|
guide(ctx, c);
|
||||||
|
things(ctx, c);
|
||||||
|
// Prima chi è dietro, così chi è davanti resta sopra; Andrea sempre sopra a tutti.
|
||||||
|
for (const r of [...standings(c)].reverse().sort((a, b) => (a.id === 'andrea') - (b.id === 'andrea'))) if (r.shown !== false) moped(ctx, r, c);
|
||||||
|
if (c.step === 'intro') { card(ctx, c, look, sheet); ctx.restore(); return; }
|
||||||
|
if (c.step === 'race' || c.step === 'start') swipe(ctx, c);
|
||||||
|
hud(ctx, c);
|
||||||
|
if (c.step === 'race') position(ctx, c);
|
||||||
|
if (c.step === 'start') lights(ctx, c);
|
||||||
|
const me = player(c);
|
||||||
|
if (c.step === 'race' && me.out > 0) text(ctx, me.lastHole !== undefined ? 'NELLA BUCA!' : 'NELLA PAGLIA!', 320, 180, 10, '#ffe28a', 'center');
|
||||||
|
if (c.step === 'race' && me.boost > 0 && me.out <= 0) text(ctx, 'MISCELA!', 320, 180, 8, '#ffb030', 'center');
|
||||||
|
if (c.step === 'result') result(ctx, c);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
// Il GP di Fortezza (idea di Andrea, 28 ottobre): la gara dei cinquantini sul giro delle mura della Fortezza di San
|
||||||
|
// Martino, vista dall'alto come le vecchie gare da sala giochi con tutta la pista in una schermata. Il tracciato segue
|
||||||
|
// la planimetria generale 1:500 che ha mandato Andrea (`assets/raw/fortezza-planimetria.png`), schiacciata nel
|
||||||
|
// formato del gioco (640x360): x = 90 + (x0 - 215)·0,7, y = 20 + (y0 - 42)·0,49.
|
||||||
|
//
|
||||||
|
// Si tiene premuto per il gas (mouse, dito, Spazio o ↑), si sterza con ← → (o indicando la direzione con uno swipe, o coi
|
||||||
|
// pulsanti), ↓ frena. Sulla pista: acceleratori, buche (andandoci forte si casca) e lattine di miscela da raccogliere.
|
||||||
|
// Fuori strada c'è l'erba che frena, e poi la paglia: chi ci arriva forte si ferma.
|
||||||
|
|
||||||
|
// La linea di mezzo della strada, in senso orario a partire dal traguardo sul rettilineo delle mura a levante: prima
|
||||||
|
// tracciata sulla planimetria, poi ricalcata sulla strada del quadro di Codex (assets/closeups/gp-fortezza.png).
|
||||||
|
export const CONTROL = [
|
||||||
|
[570, 210], [572, 222], [574, 233], [577, 245], [580, 256], [583, 268], [585, 280], [589, 291], [589, 303], [589, 315],
|
||||||
|
[585, 327], [576, 335], [565, 340], [553, 341], [541, 340], [529, 339], [518, 336], [506, 334], [495, 330], [483, 327],
|
||||||
|
[471, 324], [460, 321], [448, 318], [436, 316], [424, 315], [412, 314], [400, 314], [389, 317], [378, 320], [372, 331],
|
||||||
|
[363, 339], [352, 345], [340, 344], [328, 341], [317, 337], [305, 334], [294, 329], [284, 324], [273, 319], [262, 314],
|
||||||
|
[251, 308], [241, 303], [231, 296], [220, 291], [210, 284], [199, 280], [188, 274], [178, 269], [167, 263], [157, 256],
|
||||||
|
[148, 248], [143, 237], [147, 225], [149, 214], [148, 203], [142, 192], [135, 182], [127, 174], [118, 166], [110, 156],
|
||||||
|
[104, 146], [100, 135], [98, 123], [101, 112], [106, 100], [113, 91], [124, 86], [136, 81], [147, 77], [157, 70],
|
||||||
|
[165, 61], [173, 53], [185, 49], [197, 47], [208, 45], [220, 41], [232, 39], [243, 36], [255, 33], [266, 30],
|
||||||
|
[278, 26], [290, 24], [301, 22], [313, 20], [325, 20], [337, 19], [349, 20], [361, 23], [372, 26], [384, 29],
|
||||||
|
[396, 32], [407, 36], [418, 40], [430, 43], [441, 47], [453, 50], [464, 55], [475, 59], [486, 63], [498, 67],
|
||||||
|
[509, 71], [521, 74], [532, 76], [544, 75], [557, 77], [566, 85], [570, 96], [572, 108], [574, 120], [574, 132],
|
||||||
|
[574, 144], [573, 156], [573, 168], [572, 180], [571, 192], [570, 204],
|
||||||
|
];
|
||||||
|
export const LAPS = 3, STEP = 2, ROAD = 9, VERGE = 15, COUNTDOWN = 3, FIXED_STEP = 1 / 120;
|
||||||
|
// La presentazione prima del semaforo: uno alla volta i piloti arrivano in griglia col loro motorino (INTRO.each
|
||||||
|
// secondi ciascuno, da INTRO.from pixel di strada più indietro, in INTRO.ride secondi); un tocco passa al prossimo.
|
||||||
|
export const INTRO = { each: 4, ride: 1.8, from: 110, after: 1 };
|
||||||
|
// Sterzo (rad/s a velocità piena di sterzo), freno, rallentamento senza gas, e quanto si scivola di lato in curva.
|
||||||
|
export const TURN = 3.4, BRAKE = 150, COAST = 45, GRIP = 9, SLIDE = 0.5;
|
||||||
|
export const SWIPE = { dead: 10, reach: 60, gain: 2.6 };
|
||||||
|
export const BOOST = { speed: 1.35, time: 1.2 }, CAN = { time: 1.6, respawn: 6 }, HOLE = { fast: 45, out: 1.1 };
|
||||||
|
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||||
|
const wrapAngle = a => Math.atan2(Math.sin(a), Math.cos(a));
|
||||||
|
|
||||||
|
// I piloti sono dati (story.closeups.gara.riders, da piloti.js): `motore` da 1 (di serie) a 5 (truccato da far
|
||||||
|
// paura), `testa` da 1 (matto) a 5 (prudente). Ne vengono velocità massima e accelerazione in px/s, tenuta, e per gli
|
||||||
|
// altri quanto si fidano delle curve (`caution`) e quanto spesso rischiano troppo (`slip`).
|
||||||
|
export function rider({ motore = 3, testa = 3, ...r }) {
|
||||||
|
const m = clamp(motore, 1, 5), t = clamp(testa, 1, 5);
|
||||||
|
return { ...r, motore: m, testa: t, vmax: 74 + 6.5 * m, accel: 40 + 5 * m, grip: 1.1 - 0.03 * m, caution: 1.0025 - 0.0125 * t, slip: 0.4275 - 0.0775 * t };
|
||||||
|
}
|
||||||
|
// Il gruppo di prova, quando la storia non ne dà uno.
|
||||||
|
export const RIDERS = [
|
||||||
|
{ id: 'andrea', name: 'Andrea', bike: 'il Fifty', motore: 3, testa: 3, color: '#d8402f', trim: '#1c1c1c' },
|
||||||
|
{ id: 'arcu', name: 'L\'Arcu', bike: 'il Betino', motore: 1, testa: 5, color: '#2f7a3a', trim: '#e8d9a0' },
|
||||||
|
];
|
||||||
|
// I livelli: quanto vanno gli altri, quanto si resta fermi nella paglia, e la guida disegnata in pista.
|
||||||
|
export const LEVELS = {
|
||||||
|
bona: { rivals: 0.78, stun: 0.4, guide: 'zone' },
|
||||||
|
seria: { rivals: 0.84, stun: 0.9, guide: 'coni' },
|
||||||
|
male: { rivals: 0.93, stun: 1.4, guide: null },
|
||||||
|
};
|
||||||
|
|
||||||
|
function catmull(p0, p1, p2, p3, t) {
|
||||||
|
const t2 = t * t, t3 = t2 * t;
|
||||||
|
return [0, 1].map(k => 0.5 * (2 * p1[k] + (-p0[k] + p2[k]) * t + (2 * p0[k] - 5 * p1[k] + 4 * p2[k] - p3[k]) * t2 + (-p0[k] + 3 * p1[k] - 3 * p2[k] + p3[k]) * t3));
|
||||||
|
}
|
||||||
|
// Il tracciato campionato ogni STEP pixel: posizione, direzione, raggio di curva e velocità massima di ogni punto.
|
||||||
|
function buildTrack() {
|
||||||
|
const dense = [], n = CONTROL.length;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const p = [CONTROL[(i - 1 + n) % n], CONTROL[i], CONTROL[(i + 1) % n], CONTROL[(i + 2) % n]];
|
||||||
|
for (let k = 0; k < 40; k++) dense.push(catmull(...p, k / 40));
|
||||||
|
}
|
||||||
|
const pts = [dense[0]];
|
||||||
|
let carry = 0;
|
||||||
|
for (let i = 1; i <= dense.length; i++) {
|
||||||
|
const a = dense[i - 1], b = dense[i % dense.length];
|
||||||
|
let seg = Math.hypot(b[0] - a[0], b[1] - a[1]), t = 0;
|
||||||
|
while (carry + seg >= STEP) {
|
||||||
|
const need = STEP - carry; t += need / Math.hypot(b[0] - a[0], b[1] - a[1]); seg -= need; carry = 0;
|
||||||
|
pts.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
|
||||||
|
}
|
||||||
|
carry += seg;
|
||||||
|
}
|
||||||
|
if (Math.hypot(pts.at(-1)[0] - pts[0][0], pts.at(-1)[1] - pts[0][1]) < STEP * 0.5) pts.pop();
|
||||||
|
const m = pts.length, heading = pts.map((p, i) => { const q = pts[(i + 1) % m]; return Math.atan2(q[1] - p[1], q[0] - p[0]); });
|
||||||
|
const W = 6, radius = pts.map((_, i) => (2 * W * STEP) / Math.max(Math.abs(wrapAngle(heading[(i + W) % m] - heading[(i - W + m) % m])), 1e-6));
|
||||||
|
// A sterzo pieno si gira di TURN rad/s: in una curva di raggio R si passa al massimo a TURN·R.
|
||||||
|
const limit = radius.map(r => TURN * r);
|
||||||
|
// Da che parte gira (+1 destra, -1 sinistra).
|
||||||
|
const side = pts.map((_, i) => Math.sign(wrapAngle(heading[(i + W) % m] - heading[(i - W + m) % m])) || 1);
|
||||||
|
return { pts, heading, radius, limit, side, length: m * STEP };
|
||||||
|
}
|
||||||
|
export const TRACK = buildTrack();
|
||||||
|
const N = TRACK.pts.length;
|
||||||
|
const at = s => Math.floor((((s % TRACK.length) + TRACK.length) % TRACK.length) / STEP) % N;
|
||||||
|
export const limitAt = s => TRACK.limit[at(s)];
|
||||||
|
// Il punto dello schermo a distanza s dalla partenza, spostato di lato (positivo: a destra della direzione di marcia).
|
||||||
|
export function pointAt(s, lane = 0) {
|
||||||
|
const f = (((s % TRACK.length) + TRACK.length) % TRACK.length) / STEP, i = Math.floor(f) % N, t = f - Math.floor(f);
|
||||||
|
const a = TRACK.pts[i], b = TRACK.pts[(i + 1) % N], h = TRACK.heading[i];
|
||||||
|
return { x: a[0] + (b[0] - a[0]) * t - Math.sin(h) * lane, y: a[1] + (b[1] - a[1]) * t + Math.cos(h) * lane, heading: h };
|
||||||
|
}
|
||||||
|
// Le curve vere (dove il Fifty a tutto gas non passa): per la guida, i coni e la paglia.
|
||||||
|
export const CURVES = (() => {
|
||||||
|
const out = [], v = rider({ motore: 3 }).vmax * 0.85;
|
||||||
|
let inside = false, start = 0, min = Infinity, apex = 0;
|
||||||
|
TRACK.limit.forEach((l, i) => {
|
||||||
|
if (l < v && !inside) { inside = true; start = i; min = l; apex = i; }
|
||||||
|
else if (l < v) { if (l < min) { min = l; apex = i; } }
|
||||||
|
else if (inside) { inside = false; out.push({ start: start * STEP, apex: apex * STEP, end: i * STEP, speed: min, side: TRACK.side[apex] }); }
|
||||||
|
});
|
||||||
|
if (inside) out.push({ start: start * STEP, apex: apex * STEP, end: TRACK.length, speed: min, side: TRACK.side[apex] });
|
||||||
|
return out;
|
||||||
|
})();
|
||||||
|
// Le cose in pista, a distanza dal traguardo (s) e di lato (lane): acceleratori e buche fissi, lattine che ricompaiono.
|
||||||
|
// Sui rettilinei, lontano dalle curve, perché si vedano arrivare.
|
||||||
|
function straights() {
|
||||||
|
const out = [];
|
||||||
|
let from = 0;
|
||||||
|
for (const k of [...CURVES, { start: TRACK.length + (CURVES[0]?.start ?? 0) }]) { if (k.start - from > 70) out.push([from + 20, k.start - 20]); from = k.end ?? from; }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
export const PICKUPS = (() => {
|
||||||
|
const list = [], parts = straights();
|
||||||
|
parts.forEach(([a, b], i) => {
|
||||||
|
const mid = (a + b) / 2, len = b - a;
|
||||||
|
if (i % 3 === 0) list.push({ kind: 'boost', s: a + len * 0.3, lane: 0 });
|
||||||
|
else if (i % 3 === 1) list.push({ kind: 'buca', s: mid, lane: i % 2 ? 4 : -4 });
|
||||||
|
else list.push({ kind: 'miscela', s: mid, lane: i % 2 ? -4 : 4 });
|
||||||
|
if (len > 160) list.push({ kind: i % 2 ? 'buca' : 'miscela', s: a + len * 0.75, lane: i % 2 ? -4 : 4 });
|
||||||
|
});
|
||||||
|
return list.map((p, k) => ({ ...p, s: ((p.s % TRACK.length) + TRACK.length) % TRACK.length, id: k, ...pointAt(p.s, p.lane) }));
|
||||||
|
})();
|
||||||
|
|
||||||
|
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
|
||||||
|
// La posizione sulla strada: il punto del tracciato più vicino (cercato vicino all'ultimo, così non si salta di lato).
|
||||||
|
function locate(r) {
|
||||||
|
let best = r.idx, bd = Infinity;
|
||||||
|
for (let d = -12; d <= 24; d++) {
|
||||||
|
const i = (r.idx + d + N) % N, p = TRACK.pts[i], q = (p[0] - r.x) ** 2 + (p[1] - r.y) ** 2;
|
||||||
|
if (q < bd) { bd = q; best = i; }
|
||||||
|
}
|
||||||
|
let delta = best - r.idx; if (delta > N / 2) delta -= N; if (delta < -N / 2) delta += N;
|
||||||
|
r.idx = best; r.s += delta * STEP;
|
||||||
|
const h = TRACK.heading[best], p = TRACK.pts[best];
|
||||||
|
r.lat = -Math.sin(h) * (r.x - p[0]) + Math.cos(h) * (r.y - p[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newGara(seed = Date.now() >>> 0, def = {}) {
|
||||||
|
const level = LEVELS[def.level] ? def.level : 'seria', skill = LEVELS[level];
|
||||||
|
// In griglia nell'ordine della lista, due per fila, dietro il traguardo.
|
||||||
|
const riders = (def.riders ?? RIDERS).map((r, k) => {
|
||||||
|
const base = rider(r), factor = r.id === 'andrea' ? 1 : skill.rivals, s = -16 - 14 * Math.floor(k / 2), lane = k % 2 ? 4 : -4, p = pointAt(s, lane);
|
||||||
|
return { ...base, vmax: base.vmax * factor, accel: base.accel * factor, lane, x: p.x, y: p.y, a: p.heading, v: 0, u: 0, s, idx: at(s), lat: lane,
|
||||||
|
gas: false, brake: false, steer: 0, out: 0, boost: 0, laps: 0, finished: null, offs: 0, falls: 0, risk: 1 };
|
||||||
|
});
|
||||||
|
if (!riders.some(r => r.id === 'andrea')) throw new Error('Nella gara manca Andrea');
|
||||||
|
for (const r of riders) { r.grid = r.s; r.shown = false; }
|
||||||
|
return { step: 'intro', t: 0, age: 0, seed, level, skill, riders, holding: false, braking: false, keys: { left: false, right: false, gas: new Set(), brake: false },
|
||||||
|
pointer: null, toggle: false, brakeButton: false, drag: 0, buttons: 0, anchor: null, finger: null,
|
||||||
|
power: 0, accumulator: 0, events: [], result: null, order: riders.map(r => r.id), taken: {} };
|
||||||
|
}
|
||||||
|
export const player = c => c.riders.find(r => r.id === 'andrea');
|
||||||
|
export const lapOf = r => clamp(Math.floor(Math.max(0, r.s) / TRACK.length) + 1, 1, LAPS);
|
||||||
|
export const canThrow = c => c.step === 'intro' || c.step === 'start' || c.step === 'race';
|
||||||
|
export const canAdjust = c => c.step === 'race'; // lo sterzo del pannello
|
||||||
|
export const aimFree = () => true;
|
||||||
|
export const nudge = () => false;
|
||||||
|
export const setPower = () => false;
|
||||||
|
|
||||||
|
// Il gas ha più fonti, ognuna con la sua: i tasti tenuti giù (Spazio, Invio, ↑), il dito o il mouse premuto sulla
|
||||||
|
// scena (uno solo, col suo pointerId) e il pulsante Gas del pannello, che resta acceso finché non lo si ritocca.
|
||||||
|
// Lasciarne una non spegne le altre. `holding` è la loro somma.
|
||||||
|
function sync(c) {
|
||||||
|
c.holding = c.keys.gas.size > 0 || c.pointer !== null || c.toggle;
|
||||||
|
c.braking = c.keys.brake || c.brakeButton;
|
||||||
|
if (c.pointer === null) { c.drag = 0; c.anchor = null; c.finger = null; }
|
||||||
|
}
|
||||||
|
// Il primo tocco (o tasto) salta la presentazione, senza dare gas.
|
||||||
|
export function press(c, id = 'press') {
|
||||||
|
if (!canThrow(c)) return false;
|
||||||
|
if (c.step === 'intro') return nextIntro(c);
|
||||||
|
if (c.pointer !== null && c.pointer !== id) return false; // un secondo dito sulla scena non conta
|
||||||
|
c.pointer = id; sync(c);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function release(c, id) {
|
||||||
|
if (c.pointer === null || (id !== undefined && id !== c.pointer)) return false;
|
||||||
|
c.pointer = null; sync(c);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function cancel(c) {
|
||||||
|
Object.assign(c, { pointer: null, toggle: false, brakeButton: false, drag: 0, buttons: 0 });
|
||||||
|
Object.assign(c.keys, { left: false, right: false, brake: false }); c.keys.gas.clear();
|
||||||
|
sync(c);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Un tocco durante la presentazione: chi arrivava è già al suo posto, e tocca al prossimo (dopo l'ultimo, il semaforo).
|
||||||
|
function nextIntro(c) {
|
||||||
|
const k = Math.floor(c.t / INTRO.each) + 1;
|
||||||
|
if (k >= c.riders.length) return skipIntro(c);
|
||||||
|
c.t = k * INTRO.each;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Finita (o saltata) la presentazione tutti sono in griglia, e si va al semaforo.
|
||||||
|
function skipIntro(c) {
|
||||||
|
for (const r of c.riders) place(r, r.grid, true);
|
||||||
|
c.step = 'start'; c.t = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function place(r, s, shown) {
|
||||||
|
const p = pointAt(s, r.lane);
|
||||||
|
Object.assign(r, { s, x: p.x, y: p.y, a: p.heading, idx: at(s), lat: r.lane, shown });
|
||||||
|
}
|
||||||
|
// Chi si sta presentando adesso (o null fra uno e l'altro, e alla fine).
|
||||||
|
export const presenting = c => c.step === 'intro' ? c.riders[Math.floor(c.t / INTRO.each)] ?? null : null;
|
||||||
|
// Il pulsante Gas del pannello: un tocco lo accende, un altro lo spegne (per chi non può tenere premuto).
|
||||||
|
export function shoot(c) { if (!canThrow(c)) return false; if (c.step === 'intro') return nextIntro(c); c.toggle = !c.toggle; sync(c); return true; }
|
||||||
|
// Lo swipe indica dove andare sullo schermo. L'origine segue il dito oltre i 60 px.
|
||||||
|
export function aimScreen(c, [x, y], id = 'press') {
|
||||||
|
if (!Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||||
|
if (c.pointer !== null && c.pointer !== id) return false;
|
||||||
|
if (c.pointer === null || !c.anchor) c.anchor = [x, y];
|
||||||
|
c.finger = [x, y];
|
||||||
|
const dx = x - c.anchor[0], dy = y - c.anchor[1], distance = Math.hypot(dx, dy);
|
||||||
|
if (distance > SWIPE.reach) c.anchor = [x - dx * SWIPE.reach / distance, y - dy * SWIPE.reach / distance];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// La tastiera: ← → sterzano, ↑ Spazio e Invio sono il gas, ↓ il freno; finché si tengono giù.
|
||||||
|
const GAS_KEYS = ['ArrowUp', ' ', 'Enter'];
|
||||||
|
export function key(c, name, down) {
|
||||||
|
if (name === 'ArrowLeft') c.keys.left = down;
|
||||||
|
else if (name === 'ArrowRight') c.keys.right = down;
|
||||||
|
else if (name === 'ArrowDown') c.keys.brake = down;
|
||||||
|
else if (GAS_KEYS.includes(name)) {
|
||||||
|
if (down && c.step === 'intro') { nextIntro(c); return true; }
|
||||||
|
if (down && canThrow(c)) c.keys.gas.add(name); else c.keys.gas.delete(name);
|
||||||
|
} else return false;
|
||||||
|
sync(c);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// I pulsanti del pannello tenuti premuti: Sterza (-1, 0, 1) e Frena.
|
||||||
|
export function steerButton(c, dir) { c.buttons = dir; return true; }
|
||||||
|
export function brakeButton(c, down) { c.brakeButton = down; sync(c); return true; }
|
||||||
|
export function steering(c) {
|
||||||
|
c.drag = 0;
|
||||||
|
if (c.pointer !== null && c.anchor && c.finger) {
|
||||||
|
const dx = c.finger[0] - c.anchor[0], dy = c.finger[1] - c.anchor[1];
|
||||||
|
// Si ricalcola anche col dito fermo: il motorino intanto cambia direzione.
|
||||||
|
if (Math.hypot(dx, dy) >= SWIPE.dead) c.drag = clamp(wrapAngle(Math.atan2(dy, dx) - player(c).a) * SWIPE.gain, -1, 1);
|
||||||
|
}
|
||||||
|
return clamp((c.keys.right ? 1 : 0) - (c.keys.left ? 1 : 0) + c.buttons + c.drag, -1, 1);
|
||||||
|
}
|
||||||
|
// La distanza davanti, sul giro, da s a un punto della pista (sempre fra 0 e la lunghezza del giro).
|
||||||
|
export const ahead = (from, to) => ((((to - from) % TRACK.length) + TRACK.length) % TRACK.length);
|
||||||
|
|
||||||
|
// Gli avversari: guardano un punto più avanti sulla loro corsia e ci sterzano; danno gas finché, frenando da qui,
|
||||||
|
// arriverebbero a ogni curva davanti sotto il limite (un po' meno, quanto si fidano). Chi ha poca testa ogni tanto
|
||||||
|
// si fida troppo.
|
||||||
|
function drive(c, r) {
|
||||||
|
const aim = pointAt(r.s + 14 + r.v * 0.18, r.lane * 0.6);
|
||||||
|
r.steer = clamp(wrapAngle(Math.atan2(aim.y - r.y, aim.x - r.x) - r.a) * 2.6, -1, 1);
|
||||||
|
const curve = CURVES.find(k => { const s = ((r.s % TRACK.length) + TRACK.length) % TRACK.length; return s >= k.start - 60 && s < k.start - 57; });
|
||||||
|
if (curve && r.lastCurve !== curve) { r.lastCurve = curve; r.risk = random(c) < r.slip ? 1.15 : 1; }
|
||||||
|
const horizon = (r.v * r.v) / (2 * BRAKE) + 20;
|
||||||
|
r.gas = true; r.brake = false;
|
||||||
|
for (let d = 0; d <= horizon; d += STEP * 2) {
|
||||||
|
const allowed = limitAt(r.s + d) * 0.82 * r.grip * r.caution * r.risk;
|
||||||
|
if (r.v * r.v > allowed * allowed + 2 * BRAKE * d) { r.gas = false; r.brake = d < 30; break; }
|
||||||
|
}
|
||||||
|
// Le buche si scansano (chi ha testa); una lattina ogni tanto la vanno a prendere (deciso una volta per lattina).
|
||||||
|
const look = 30 + r.v * 0.6, next = PICKUPS.find(p => p.kind !== 'boost' && ahead(r.s, p.s) < look);
|
||||||
|
if (next && r.seen !== next.id) { r.seen = next.id; r.wants = random(c) < 0.35; }
|
||||||
|
// Chi ha davanti uno più lento lo passa dall'altra parte.
|
||||||
|
const slow = c.riders.find(o => o !== r && o.s - r.s > 0 && o.s - r.s < 22 && Math.abs(o.lat - r.lat) < 7 && o.v < r.v + 5);
|
||||||
|
if (slow) r.lane = slow.lat > 0 ? -5 : 5;
|
||||||
|
if (next?.kind === 'buca' && r.testa >= 3) r.lane = next.lane > 0 ? -5 : 5;
|
||||||
|
else if (next?.kind === 'miscela' && r.wants) r.lane = next.lane;
|
||||||
|
}
|
||||||
|
function stepRider(c, r, dt) {
|
||||||
|
if (r.finished !== null) { r.gas = false; r.brake = false; drive(c, r); r.gas = false; }
|
||||||
|
else if (r.out > 0) { r.out = Math.max(0, r.out - dt); r.v = 0; r.u = 0; return; }
|
||||||
|
else if (r.id === 'andrea') { r.gas = c.holding; r.brake = c.braking; r.steer = steering(c); }
|
||||||
|
else drive(c, r);
|
||||||
|
if (r.finished !== null) r.gas = false;
|
||||||
|
const grass = Math.abs(r.lat) > ROAD, top = r.vmax * (r.boost > 0 ? BOOST.speed : 1) * (grass ? 0.5 : 1);
|
||||||
|
// Il freno vince sul gas: chi li tiene tutti e due frena.
|
||||||
|
if (r.brake) r.v -= BRAKE * dt;
|
||||||
|
else if (r.gas) r.v += r.accel * dt * (1 - 0.4 * r.v / top) * (r.boost > 0 ? 1.6 : 1);
|
||||||
|
else r.v -= COAST * dt;
|
||||||
|
if (r.v > top) r.v = Math.max(top, r.v - 120 * dt);
|
||||||
|
r.v = Math.max(0, r.v);
|
||||||
|
r.boost = Math.max(0, r.boost - dt);
|
||||||
|
// Lo sterzo: da fermi non si gira; in curva una parte della velocità resta per traverso e si scivola fuori.
|
||||||
|
const turn = r.steer * TURN * Math.min(1, r.v / 22) * dt;
|
||||||
|
r.a = wrapAngle(r.a + turn);
|
||||||
|
r.u -= r.v * Math.sin(turn) * SLIDE * (r.v / r.vmax) / r.grip;
|
||||||
|
r.u *= Math.exp(-GRIP * r.grip * dt);
|
||||||
|
r.x += (Math.cos(r.a) * r.v - Math.sin(r.a) * r.u) * dt;
|
||||||
|
r.y += (Math.sin(r.a) * r.v + Math.cos(r.a) * r.u) * dt;
|
||||||
|
locate(r);
|
||||||
|
// La paglia (fuori) e le mura (dentro): si rimbalza in strada; arrivandoci forte ci si ferma.
|
||||||
|
if (Math.abs(r.lat) > VERGE) {
|
||||||
|
const h = TRACK.heading[r.idx], p = TRACK.pts[r.idx], side = Math.sign(r.lat), back = side * (VERGE - 1);
|
||||||
|
r.x = p[0] - Math.sin(h) * back; r.y = p[1] + Math.cos(h) * back; r.lat = back; r.u = 0;
|
||||||
|
if (r.v > 40 && r.finished === null) {
|
||||||
|
r.out = c.skill.stun * (r.id === 'andrea' ? 1 : 0.7); r.offs++; r.risk = 1;
|
||||||
|
c.events.push({ kind: 'gara-fuori', who: r.id, power: r.id === 'andrea' ? 1 : 0.4 });
|
||||||
|
}
|
||||||
|
r.v *= 0.35; r.a = wrapAngle(h + clamp(wrapAngle(r.a - h), -0.5, 0.5));
|
||||||
|
}
|
||||||
|
if (r.finished === null) pickups(c, r);
|
||||||
|
const laps = Math.floor(Math.max(0, r.s) / TRACK.length);
|
||||||
|
if (laps > r.laps && r.finished === null) {
|
||||||
|
r.laps = laps;
|
||||||
|
if (laps >= LAPS) { r.finished = c.t; c.events.push({ kind: 'gara-arrivo', who: r.id, power: r.id === 'andrea' ? 1 : 0.5 }); }
|
||||||
|
else if (r.id === 'andrea') c.events.push({ kind: 'gara-giro', who: r.id, power: 0.6 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Acceleratori (una spinta), lattine di miscela (spinta più lunga, poi la lattina ricompare) e buche (forte si casca).
|
||||||
|
function pickups(c, r) {
|
||||||
|
for (const p of PICKUPS) {
|
||||||
|
if (Math.hypot(p.x - r.x, p.y - r.y) > 6) continue;
|
||||||
|
if (p.kind === 'boost') { if (r.boost < BOOST.time * 0.5) { r.boost = BOOST.time; c.events.push({ kind: 'gara-turbo', who: r.id, power: r.id === 'andrea' ? 1 : 0.3 }); } }
|
||||||
|
else if (p.kind === 'miscela') {
|
||||||
|
if ((c.taken[p.id] ?? -Infinity) > c.t - CAN.respawn) continue;
|
||||||
|
c.taken[p.id] = c.t; r.boost = CAN.time;
|
||||||
|
c.events.push({ kind: 'gara-miscela', who: r.id, power: r.id === 'andrea' ? 1 : 0.3 });
|
||||||
|
} else if (p.kind === 'buca' && r.lastHole !== p.id) {
|
||||||
|
r.lastHole = p.id;
|
||||||
|
if (r.v > HOLE.fast) { r.out = HOLE.out; r.falls++; r.v = 0; c.events.push({ kind: 'gara-buca', who: r.id, power: r.id === 'andrea' ? 1 : 0.4 }); }
|
||||||
|
else { r.v *= 0.7; c.events.push({ kind: 'gara-sobbalzo', who: r.id, power: 0.3 }); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (r.lastHole !== undefined && Math.hypot(PICKUPS[r.lastHole].x - r.x, PICKUPS[r.lastHole].y - r.y) > 14) r.lastHole = undefined;
|
||||||
|
}
|
||||||
|
// I motorini si urtano: si allontanano un po', e chi tampona perde velocità.
|
||||||
|
function bump(c) {
|
||||||
|
for (let i = 0; i < c.riders.length; i++) for (let j = i + 1; j < c.riders.length; j++) {
|
||||||
|
const a = c.riders[i], b = c.riders[j], dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy);
|
||||||
|
if (d >= 7 || d < 1e-6) continue;
|
||||||
|
const push = (7 - d) / 2, nx = dx / d, ny = dy / d;
|
||||||
|
a.x -= nx * push; a.y -= ny * push; b.x += nx * push; b.y += ny * push;
|
||||||
|
const behind = a.s < b.s ? a : b; behind.v *= 0.97;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// La classifica: chi è arrivato, in ordine d'arrivo; poi chi corre ancora, per strada fatta.
|
||||||
|
export function standings(c) {
|
||||||
|
return [...c.riders].sort((a, b) => (a.finished ?? Infinity) - (b.finished ?? Infinity) || b.s - a.s);
|
||||||
|
}
|
||||||
|
export const placeOf = (c, id = 'andrea') => standings(c).findIndex(r => r.id === id) + 1;
|
||||||
|
|
||||||
|
export function update(c, dt) {
|
||||||
|
c.age += dt;
|
||||||
|
if (c.step === 'intro') {
|
||||||
|
c.t += dt;
|
||||||
|
c.riders.forEach((r, k) => {
|
||||||
|
const t = (c.t - k * INTRO.each) / INTRO.ride;
|
||||||
|
if (t < 0) return;
|
||||||
|
if (!r.shown) c.events.push({ kind: 'gara-motore', who: r.id, power: 0.3 + 0.14 * r.motore });
|
||||||
|
const e = 1 - (1 - Math.min(1, t)) ** 2; // arriva forte e rallenta
|
||||||
|
place(r, r.grid - INTRO.from * (1 - e), true);
|
||||||
|
r.v = t < 1 ? INTRO.from * 2 * (1 - Math.min(1, t)) / INTRO.ride : 0;
|
||||||
|
});
|
||||||
|
if (c.t >= c.riders.length * INTRO.each + INTRO.after) { skipIntro(c); for (const r of c.riders) r.v = 0; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c.step === 'start') {
|
||||||
|
const before = Math.ceil(COUNTDOWN - c.t);
|
||||||
|
c.t += dt;
|
||||||
|
const now = Math.ceil(COUNTDOWN - c.t);
|
||||||
|
if (now !== before && now > 0) c.events.push({ kind: 'gara-semaforo', power: 0.4 });
|
||||||
|
if (c.t >= COUNTDOWN) { c.step = 'race'; c.t = 0; c.events.push({ kind: 'gara-via', power: 1 }); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c.step !== 'race') return;
|
||||||
|
c.accumulator += Math.min(dt, 0.1);
|
||||||
|
while (c.accumulator >= FIXED_STEP) {
|
||||||
|
c.accumulator -= FIXED_STEP;
|
||||||
|
c.t += FIXED_STEP;
|
||||||
|
for (const r of c.riders) stepRider(c, r, FIXED_STEP);
|
||||||
|
bump(c);
|
||||||
|
for (const r of c.riders) locate(r);
|
||||||
|
if (player(c).finished !== null) { finish(c); return; }
|
||||||
|
}
|
||||||
|
c.order = standings(c).map(r => r.id);
|
||||||
|
}
|
||||||
|
function finish(c) {
|
||||||
|
const order = standings(c);
|
||||||
|
// Chi non è ancora arrivato: il tempo stimato con la sua media fin qui.
|
||||||
|
const times = Object.fromEntries(order.map(r => [r.id, r.finished ?? c.t + (LAPS * TRACK.length - r.s) / Math.max(20, r.s / Math.max(c.t, 1))]));
|
||||||
|
const ranked = [...order].sort((a, b) => times[a.id] - times[b.id]);
|
||||||
|
const place = ranked.findIndex(r => r.id === 'andrea') + 1;
|
||||||
|
c.step = 'result';
|
||||||
|
cancel(c);
|
||||||
|
c.order = ranked.map(r => r.id);
|
||||||
|
c.result = { winner: place === 1 ? 'player' : 'cpu', place, times, order: c.order, offs: player(c).offs, falls: player(c).falls };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function status(c) {
|
||||||
|
if (c.step === 'intro') {
|
||||||
|
const r = presenting(c);
|
||||||
|
return r ? `${r.name} con ${r.bike}. (Un tocco: il prossimo.)` : 'Tutti in griglia!';
|
||||||
|
}
|
||||||
|
if (c.step === 'start') return c.t < COUNTDOWN - 0.01 ? `GP di Fortezza: si parte fra ${Math.ceil(COUNTDOWN - c.t)}… Tieni premuto per il gas, sterza con ← →.` : 'Via!';
|
||||||
|
if (c.step === 'result') {
|
||||||
|
const who = c.riders.find(r => r.id === c.result.order[0]).name;
|
||||||
|
return c.result.place === 1 ? `Primo al GP di Fortezza! ${LAPS} giri in ${c.result.times.andrea.toFixed(1)} secondi.` : `${c.result.place}° posto. Ha vinto ${who}.`;
|
||||||
|
}
|
||||||
|
const me = player(c), place = placeOf(c);
|
||||||
|
if (me.out > 0) return me.lastHole !== undefined ? 'Nella buca! Ci si rialza…' : 'Nella paglia! Si riparte…';
|
||||||
|
if (me.boost > 0) return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. Miscela!`;
|
||||||
|
return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. ${c.braking ? 'Freno!' : c.holding ? 'Gas!' : 'Senza gas.'}`;
|
||||||
|
}
|
||||||
|
export const button = c => c.step === 'intro' ? 'Avanti' : c.step === 'result' ? 'Finita' : c.holding ? 'Molla' : 'Gas';
|
||||||
@@ -7,6 +7,7 @@ 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 { drawPesca } from './pesca-render.js';
|
||||||
|
import { drawGara } from './gara-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';
|
||||||
@@ -684,6 +685,12 @@ 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 === 'gara') {
|
||||||
|
if (def.picture && !backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
|
drawGara(ctx, c, def, def.picture ? backgrounds.get(def.picture) : null,
|
||||||
|
src => { if (!backgrounds.has(src)) loadBackground(src); return backgrounds.get(src); }, id => sprites.get(id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (def.type === 'pesca') {
|
if (def.type === 'pesca') {
|
||||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
drawPesca(ctx, c, def, backgrounds.get(def.picture), sprites.get('andrea_pesca'), sprites.get('pescatori'));
|
drawPesca(ctx, c, def, backgrounds.get(def.picture), sprites.get('andrea_pesca'), sprites.get('pescatori'));
|
||||||
|
|||||||
@@ -125,6 +125,17 @@ 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 }); }
|
||||||
|
// Il GP di Fortezza: il semaforo, il via, la paglia, acceleratori, miscela, buche, il giro, il traguardo.
|
||||||
|
else if (kind === 'gara-semaforo') this.tone(440, 440, 0.18, 0.06, 0, pan);
|
||||||
|
else if (kind === 'gara-via') this.tone(880, 880, 0.4, 0.08, 0, pan);
|
||||||
|
else if (kind === 'gara-fuori') { this.noiseSound(0.35, 700, 0.1, { pan }); this.tone(300, 120, 0.2, 0.05, 0, pan); }
|
||||||
|
else if (kind === 'gara-motore') { this.tone(110, 190 + strength * 160, 0.9, 0.03 + strength * 0.03, 0, pan); this.tone(220, 380 + strength * 320, 0.9, 0.015 + strength * 0.015, 0, pan); }
|
||||||
|
else if (kind === 'gara-turbo') this.tone(300, 900, 0.25, 0.04 + strength * 0.04, 0, pan);
|
||||||
|
else if (kind === 'gara-miscela') { this.tone(500, 1200, 0.2, 0.04 + strength * 0.04, 0, pan); this.tone(800, 1600, 0.2, 0.03 * strength, 0.12, pan); }
|
||||||
|
else if (kind === 'gara-buca') { this.tone(220, 60, 0.3, 0.05 + strength * 0.07, 0, pan); this.noiseSound(0.3, 500, 0.08 * strength + 0.02, { pan }); }
|
||||||
|
else if (kind === 'gara-sobbalzo') this.tone(160, 90, 0.1, 0.05, 0, pan);
|
||||||
|
else if (kind === 'gara-giro') this.tone(660, 990, 0.15, 0.05, 0, pan);
|
||||||
|
else if (kind === 'gara-arrivo') { this.tone(660, 660, 0.12, 0.06 * strength, 0, pan); this.tone(990, 990, 0.3, 0.06 * strength, 0.14, 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.
|
// 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-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-tuffo') { this.tone(420, 180, 0.07, 0.07, 0, pan); this.noiseSound(0.12, 900, 0.06, { pan }); }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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 pesca from './engine/pesca.js';
|
||||||
|
import * as gara from './engine/gara.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';
|
||||||
@@ -497,6 +498,14 @@ canvas.addEventListener('pointerdown', e => {
|
|||||||
canvas.setPointerCapture(e.pointerId);
|
canvas.setPointerCapture(e.pointerId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Il GP di Fortezza: un dito solo dà gas e indica la direzione sullo schermo; gli altri tocchi non contano
|
||||||
|
// (sterzo e freno hanno i loro pulsanti, che si tengono insieme al gas).
|
||||||
|
if (type === 'gara') {
|
||||||
|
if (press || game.skip()) return;
|
||||||
|
gara.aimScreen(game.closeup, scenePoint(e), e.pointerId);
|
||||||
|
if (gara.press(game.closeup, e.pointerId)) { press = { gara: e.pointerId }; canvas.setPointerCapture(e.pointerId); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
const aimed = game.closeupAim(scenePoint(e));
|
const aimed = game.closeupAim(scenePoint(e));
|
||||||
if (['biliardo', 'basket'].includes(story.closeups[game.closeup.id].type)) {
|
if (['biliardo', 'basket'].includes(story.closeups[game.closeup.id].type)) {
|
||||||
if (!aimed && !game.speech) return; // dove non si può mirare (dietro Andrea) non si carica nemmeno
|
if (!aimed && !game.speech) return; // dove non si può mirare (dietro Andrea) non si carica nemmeno
|
||||||
@@ -529,6 +538,10 @@ canvas.addEventListener('pointerup', e => {
|
|||||||
if (running && game.closeup === p.state && p.card >= 0 && p.at(game.closeup, scenePoint(e)) === p.card) game.closeupPlay(p.card);
|
if (running && game.closeup === p.state && p.card >= 0 && p.at(game.closeup, scenePoint(e)) === p.card) game.closeupPlay(p.card);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (press.gara !== undefined) {
|
||||||
|
if (e.pointerId === press.gara) { if (game?.closeup) gara.release(game.closeup, e.pointerId); press = null; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (press.closeup) { press = null; if (running && !game.closeupRelease() && !game.skip()) game.closeupTap(); return; }
|
if (press.closeup) { press = null; if (running && !game.closeupRelease() && !game.skip()) game.closeupTap(); return; }
|
||||||
clearTimeout(press.timer);
|
clearTimeout(press.timer);
|
||||||
// Il secondo clic vicino al primo, entro poco, è un doppio clic: stesso comando, al doppio della velocità.
|
// Il secondo clic vicino al primo, entro poco, è un doppio clic: stesso comando, al doppio della velocità.
|
||||||
@@ -541,7 +554,7 @@ canvas.addEventListener('pointerup', e => {
|
|||||||
press = null;
|
press = null;
|
||||||
});
|
});
|
||||||
canvas.addEventListener('pointercancel', () => { if (press) clearTimeout(press.timer); press = null; game?.closeupCancel(); });
|
canvas.addEventListener('pointercancel', () => { if (press) clearTimeout(press.timer); press = null; game?.closeupCancel(); });
|
||||||
canvas.addEventListener('lostpointercapture', () => { if (press?.closeup || press?.card !== undefined || press?.table !== undefined) { press = null; game?.closeupCancel(); } });
|
canvas.addEventListener('lostpointercapture', () => { if (press?.closeup || press?.gara !== undefined || press?.card !== undefined || press?.table !== undefined) { press = null; game?.closeupCancel(); } });
|
||||||
canvas.addEventListener('contextmenu', e => {
|
canvas.addEventListener('contextmenu', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (running && !game.closeup) command.clickScene(scenePoint(e), { look: true });
|
if (running && !game.closeup) command.clickScene(scenePoint(e), { look: true });
|
||||||
@@ -552,6 +565,10 @@ 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.)
|
||||||
|
if (game.closeup && story.closeups[game.closeup.id].type === 'gara') {
|
||||||
|
if (press?.gara === e.pointerId) gara.aimScreen(game.closeup, scenePoint(e), e.pointerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const aiming = game.closeup && ['biliardo', 'bocce', 'basket', 'pesca'].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));
|
||||||
@@ -566,6 +583,8 @@ 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.' },
|
||||||
|
// La gara: niente mira né forza, il pulsante alterna gas e freno (chi non può tenere premuto).
|
||||||
|
gara: { ...gara, toggle: true, back: 'Torna dal Trappola', label: 'GP di Fortezza: tieni premuto sulla scena per il gas e trascina verso la direzione in cui vuoi andare sullo schermo; alza il dito per mollare. Anche col mouse. Oppure Spazio, Invio o ↑ per il gas, ← → per sterzare, ↓ per frenare; disponibili anche i pulsanti Gas, Sterza e Frena.' },
|
||||||
pesca: { ...pesca, back: 'Torna in Fortezza', label: 'La pesca: tieni premuto per lanciare, premi quando il galleggiante va sotto, tieni premuto per recuperare.' },
|
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;
|
||||||
@@ -594,8 +613,15 @@ for (let i = 0; i < 3; i++) {
|
|||||||
button.addEventListener('click', () => game?.closeupPlay(i));
|
button.addEventListener('click', () => game?.closeupPlay(i));
|
||||||
button.addEventListener('focus', () => { if (game?.closeup?.id === 'briscola') briscola.select(game.closeup, i); });
|
button.addEventListener('focus', () => { if (game?.closeup?.id === 'briscola') briscola.select(game.closeup, i); });
|
||||||
}
|
}
|
||||||
for (const [id, dir] of [['bocce-left', -1], ['bocce-right', 1]])
|
for (const [id, dir] of [['bocce-left', -1], ['bocce-right', 1]]) {
|
||||||
$(id).addEventListener('click', () => { const c = miniState(); if (c && mini().nudge(c, dir)) c.aimLocked = true; });
|
$(id).addEventListener('click', () => { const c = miniState(); if (c && mini().nudge(c, dir)) c.aimLocked = true; });
|
||||||
|
// Al GP di Fortezza si sterza finché il pulsante resta premuto (anche col dito, insieme a Gas).
|
||||||
|
$(id).addEventListener('pointerdown', () => { const c = miniState(); if (c) mini().steerButton?.(c, dir); });
|
||||||
|
for (const ev of ['pointerup', 'pointerleave', 'pointercancel']) $(id).addEventListener(ev, () => { const c = miniState(); if (c) mini().steerButton?.(c, 0); });
|
||||||
|
}
|
||||||
|
// Il freno del GP, tenuto premuto (sul telefono, insieme a Gas e Sterza).
|
||||||
|
$('gara-brake').addEventListener('pointerdown', () => { const c = miniState(); if (c) mini().brakeButton?.(c, true); });
|
||||||
|
for (const ev of ['pointerup', 'pointerleave', 'pointercancel']) $('gara-brake').addEventListener(ev, () => { const c = miniState(); if (c) mini().brakeButton?.(c, false); });
|
||||||
addEventListener('blur', () => { if (press?.card !== undefined) press = null; game?.closeupCancel(); });
|
addEventListener('blur', () => { if (press?.card !== undefined) press = null; game?.closeupCancel(); });
|
||||||
|
|
||||||
// --- Verbi.
|
// --- Verbi.
|
||||||
@@ -757,7 +783,12 @@ window.addEventListener('keydown', e => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (miniState()) {
|
if (miniState()) {
|
||||||
if (e.target.closest?.('#bocce-controls') && ['BUTTON', 'SUMMARY'].includes(e.target.tagName)) return;
|
// Esc esce anche col fuoco sui pulsanti del pannello.
|
||||||
|
if (e.key === 'Escape') { game.closeupClose(); return; }
|
||||||
|
const onButton = ['BUTTON', 'SUMMARY'].includes(e.target.tagName), onControl = onButton && e.target.closest?.('#bocce-controls');
|
||||||
|
// Il GP di Fortezza: frecce, Spazio e Invio finché si tengono giù (su un pulsante Spazio e Invio lo premono).
|
||||||
|
if (!(onButton && [' ', 'Enter'].includes(e.key)) && mini().key?.(game.closeup, e.key, true)) { e.preventDefault(); return; }
|
||||||
|
if (onControl) return;
|
||||||
const c = game.closeup, m = mini();
|
const c = game.closeup, m = mini();
|
||||||
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
|
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -801,7 +832,11 @@ window.addEventListener('keydown', e => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Alle bocce e al biliardo, lasciando spazio o Invio parte il tiro.
|
// Alle bocce e al biliardo, lasciando spazio o Invio parte il tiro.
|
||||||
window.addEventListener('keyup', e => { if (running && [' ', 'Enter'].includes(e.key)) game?.closeupRelease(); });
|
window.addEventListener('keyup', e => {
|
||||||
|
// Il GP di Fortezza tiene conto di ogni tasto per conto suo: lasciarne uno non toglie il gas del dito o degli altri.
|
||||||
|
if (miniState() && mini().key) { mini().key(game.closeup, e.key, false); return; }
|
||||||
|
if (running && [' ', 'Enter'].includes(e.key)) game?.closeupRelease();
|
||||||
|
});
|
||||||
|
|
||||||
// --- Il primo piano: si chiude col pulsante; la pagina che gira fruscia (rumore filtrato, niente file audio).
|
// --- Il primo piano: si chiude col pulsante; la pagina che gira fruscia (rumore filtrato, niente file audio).
|
||||||
$('closeup-close').addEventListener('click', () => game?.closeupClose());
|
$('closeup-close').addEventListener('click', () => game?.closeupClose());
|
||||||
@@ -908,19 +943,26 @@ function updateUi() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mg && !cards) {
|
if (mg && !cards) {
|
||||||
$('bocce-controls').setAttribute('aria-label', { biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca' }[def.type] ?? 'Comandi delle bocce');
|
$('bocce-controls').setAttribute('aria-label', { biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca', gara: 'Comandi della gara' }[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']) $(id).disabled = !(mg.canAdjust?.(c) ?? 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 && !mg.toggle) || !!game.speech;
|
||||||
$('bocce-throw').disabled = !mg.canThrow(c) || c.holding || !!game.speech;
|
$('bocce-throw').disabled = !mg.canThrow(c) || (c.holding && !mg.toggle) || !!game.speech;
|
||||||
|
// Il GP di Fortezza: niente forza, e i due pulsanti di mira diventano lo sterzo (si tengono premuti).
|
||||||
|
$('bocce-power').closest('label').hidden = def.type === 'gara';
|
||||||
|
$('bocce-left').textContent = def.type === 'gara' ? '← Sterza' : '← Mira';
|
||||||
|
$('bocce-right').textContent = def.type === 'gara' ? 'Sterza →' : 'Mira →';
|
||||||
|
$('gara-brake').hidden = def.type !== 'gara';
|
||||||
|
$('gara-brake').disabled = !(mg.canAdjust?.(c) ?? false) || !!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-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';
|
||||||
$('pesca-help').hidden = def.type !== 'pesca';
|
$('pesca-help').hidden = def.type !== 'pesca';
|
||||||
|
$('gara-help').hidden = def.type !== 'gara';
|
||||||
if (mg.aimFree?.(c) ?? !mg.canThrow(c)) c.aimLocked = false; // a ogni nuovo tiro la mira torna libera
|
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 });
|
||||||
@@ -934,7 +976,8 @@ function updateUi() {
|
|||||||
$('panel').inert = !!c;
|
$('panel').inert = !!c;
|
||||||
canvas.classList.toggle('torch', !!torch);
|
canvas.classList.toggle('torch', !!torch);
|
||||||
canvas.classList.toggle('bocce', !!mg);
|
canvas.classList.toggle('bocce', !!mg);
|
||||||
$('stage').classList.toggle('biliardo', !!c && story.closeups[c.id].type === 'biliardo');
|
// Biliardo e GP di Fortezza: audio, pausa e uscita fuori dal quadro (al GP la pista passa negli angoli).
|
||||||
|
$('stage').classList.toggle('biliardo', !!c && ['biliardo', 'gara'].includes(story.closeups[c.id].type));
|
||||||
$('stage').classList.toggle('briscola', !!cards);
|
$('stage').classList.toggle('briscola', !!cards);
|
||||||
canvas.setAttribute('aria-label', torch ? 'Fessura: sposta la luce con mouse, dito o frecce. Invio cerca il prossimo dettaglio; Esc esce.' : mg ? mg.label : "La scena dell'avventura");
|
canvas.setAttribute('aria-label', torch ? 'Fessura: sposta la luce con mouse, dito o frecce. Invio cerca il prossimo dettaglio; Esc esce.' : mg ? mg.label : "La scena dell'avventura");
|
||||||
// La pagina che comincia a girare fa il suo fruscio.
|
// La pagina che comincia a girare fa il suo fruscio.
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ export const ambience = {
|
|||||||
butterfly([150, 170, 150, 60], 5000),
|
butterfly([150, 170, 150, 60], 5000),
|
||||||
{ type: 'lamps', phase: 'sera', points: [[616, 118]], glow: 12 },
|
{ type: 'lamps', phase: 'sera', points: [[616, 118]], glow: 12 },
|
||||||
],
|
],
|
||||||
|
salita_cipressi: [
|
||||||
|
{ type: 'clouds', area: [260, 0, 200, 110], count: 2 },
|
||||||
|
{ type: 'sway', rects: [[312, 118, 24, 70]], amount: 1 },
|
||||||
|
butterfly([380, 220, 180, 60], 4000),
|
||||||
|
],
|
||||||
sentiero_fortezza: [
|
sentiero_fortezza: [
|
||||||
{ type: 'cloudshadow', phase: day, area: [0, 180, 640, 180], offset: 5000, clip: rect([0, 180, 640, 180]) },
|
{ type: 'cloudshadow', phase: day, area: [0, 180, 640, 180], offset: 5000, clip: rect([0, 180, 640, 180]) },
|
||||||
butterfly([380, 200, 160, 70], 8000),
|
butterfly([380, 200, 160, 70], 8000),
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// La gara dei cinquantini (idea di Andrea, 28 ottobre): chi corre, in ordine di griglia (due per fila).
|
||||||
|
// Ogni amico che manda la foto del suo motorino si aggiunge con una riga. `motore`: da 1 (di serie) a 5 (truccato da
|
||||||
|
// far paura); `testa`: da 1 (matto, in curva rischia) a 5 (prudente). `color` e `trim`: i colori del motorino in pista.
|
||||||
|
// Per la presentazione: `moto`, il disegno del motorino (Codex, dalla foto); `sprite`, il personaggio del gioco (un
|
||||||
|
// nome di story.characters), oppure `face`, un ritratto quadrato; senza nessuno dei due, una sagoma. `casco`: il disegno
|
||||||
|
// del casco per il cartellino, `helmet` e `stripe` i suoi colori visti dall'alto in pista.
|
||||||
|
// Persone vere solo col loro consenso (`consenso`: chi l'ha dato e quando).
|
||||||
|
export const piloti = [
|
||||||
|
// Lo SR di Puntella era il più truccato di tutti; per ora i colori dell'Aprilia SR 50 da GP (foto di Andrea).
|
||||||
|
{ id: 'puntella', name: 'Puntella', bike: 'lo SR', motore: 5, testa: 1, color: '#5a3fb5', trim: '#e8442e', moto: 'assets/closeups/gp/sr.png', consenso: null }, // [DA ANDREA: consenso di Marco Villani]
|
||||||
|
// Cistola (Davide Poggi) col suo CR 50 Z, blu notte e argento, dalla foto che ha mandato; il volto dal suo murale.
|
||||||
|
{ id: 'cistola', name: 'Cistola', bike: 'il CR 50 Z', motore: 4, testa: 3, color: '#1d2440', trim: '#a9adb5', moto: 'assets/closeups/gp/cr50z.png', face: 'assets/closeups/gp/cistola.png', consenso: '2026-10-27' },
|
||||||
|
// Il Malaguti Fifty di Andrea: rosso con le rifiniture nere.
|
||||||
|
{ id: 'andrea', name: 'Andrea', bike: 'il Fifty', motore: 3, testa: 3, color: '#d8402f', trim: '#1c1c1c', moto: 'assets/closeups/gp/fifty.png', sprite: 'andrea' },
|
||||||
|
// Tozzino (Francesco Tozzi) col suo Booster blu scuro, di serie, dalle foto che ha mandato (l'adesivo sul manubrio compreso).
|
||||||
|
{ id: 'tozzino', name: 'Tozzino', bike: 'il Booster', motore: 1, testa: 3, color: '#1a2a52', trim: '#111318', moto: 'assets/closeups/gp/booster.png', sprite: 'tozzino',
|
||||||
|
// Il casco alla Damon Hill: blu scuro con le righe bianche dei remi (Andrea, 28 ottobre).
|
||||||
|
casco: 'assets/closeups/gp/casco-tozzino.png', helmet: '#1b2a5c', stripe: '#f4f4f4' },
|
||||||
|
// Il Betino dell'Arcu: verde e crema, lento ma non cade mai.
|
||||||
|
{ id: 'arcu', name: 'L\'Arcu', bike: 'il Betino', motore: 1, testa: 5, color: '#2f7a3a', trim: '#e8d9a0', moto: 'assets/closeups/gp/betino.png', sprite: 'arcu' },
|
||||||
|
];
|
||||||
@@ -12,7 +12,7 @@ export const soundscapes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const groups = {
|
const groups = {
|
||||||
bosco: ['bosco_ai_frati', 'sentiero_fortezza', 'viale_cipressi', 'trebbio', 'salita_fortezza', 'casa_maestro_bacci', 'campo_giannini', 'area_camper', 'giardini_poste'],
|
bosco: ['bosco_ai_frati', 'sentiero_fortezza', 'salita_cipressi', 'viale_cipressi', 'trebbio', 'salita_fortezza', 'casa_maestro_bacci', 'campo_giannini', 'area_camper', 'giardini_poste'],
|
||||||
acqua: ['ponte_sieve', 'campomigliaio_ponte', 'verso_luna'],
|
acqua: ['ponte_sieve', 'campomigliaio_ponte', 'verso_luna'],
|
||||||
pieve: ['pieve', 'pieve_interno', 'oratorio'],
|
pieve: ['pieve', 'pieve_interno', 'oratorio'],
|
||||||
circolo: ['campomigliaio_circolo', 'tennis_chiesa', 'area_feste_festa'],
|
circolo: ['campomigliaio_circolo', 'tennis_chiesa', 'area_feste_festa'],
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
// Testi, personaggi ed enigmi sono provvisori e verranno sostituiti con i ricordi di Andrea (docs/PROGETTO.md).
|
// Testi, personaggi ed enigmi sono provvisori e verranno sostituiti con i ricordi di Andrea (docs/PROGETTO.md).
|
||||||
// Le forme in `art` sono segnaposto disegnati sopra lo sfondo (vecchietti, squama, catena) in attesa degli sprite.
|
// Le forme in `art` sono segnaposto disegnati sopra lo sfondo (vecchietti, squama, catena) in attesa degli sprite.
|
||||||
import { ambience } from './ambience.js';
|
import { ambience } from './ambience.js';
|
||||||
|
import { piloti } from './piloti.js';
|
||||||
|
|
||||||
// Gli indizi del Trebbio per la catena della torcia (dopo le impronte, finché la torcia non c'è): uno per tappa,
|
// Gli indizi del Trebbio per la catena della torcia (dopo le impronte, finché la torcia non c'è): uno per tappa,
|
||||||
// dalla più avanti alla prima.
|
// dalla più avanti alla prima.
|
||||||
const castello = say => [{ say, who: 'castello' }];
|
const castello = say => [{ say, who: 'castello' }];
|
||||||
|
// Prima del GP di Fortezza l'Arcu va a prendere il Betino in cima alla salita (Andrea, 28 ottobre).
|
||||||
|
const rincorsa = level => [{ say: 'Aspettate: prendo la rincorsa, che il Betino s\'accende solo in discesa.', who: 'arcu', pose: 'point' }, { closeup: 'gara', level }];
|
||||||
const torchHints = [{
|
const torchHints = [{
|
||||||
if: 'has:chiavetta',
|
if: 'has:chiavetta',
|
||||||
then: castello('Una chiavetta piccola apre una cosa piccola. All\'area feste c\'è una casetta che aspetta.'),
|
then: castello('Una chiavetta piccola apre una cosa piccola. All\'area feste c\'è una casetta che aspetta.'),
|
||||||
@@ -184,6 +187,26 @@ export const story = {
|
|||||||
quit: [{ say: 'Lascio la canna ai ragazzi. Il lago non scappa, il palio sì.' }],
|
quit: [{ say: 'Lascio la canna ai ragazzi. Il lago non scappa, il palio sì.' }],
|
||||||
draw: [],
|
draw: [],
|
||||||
},
|
},
|
||||||
|
// Il GP di Fortezza (idea di Andrea, 28 ottobre): tre giri coi cinquantini attorno alle mura, gas e sterzo come
|
||||||
|
// nelle vecchie gare da sala giochi viste dall'alto. Chi corre sta in piloti.js. [DA ANDREA: le battute; la musica]
|
||||||
|
gara: {
|
||||||
|
type: 'gara', music: 'assets/audio/gara.mp3', // di Andrea (Suno)
|
||||||
|
picture: 'assets/closeups/gp-fortezza.png', // Codex, dalla planimetria di Andrea; la strada è il tracciato
|
||||||
|
// Corre solo chi ha dato il consenso (`consenso: null` vuol dire «da chiedere»: per ora Puntella).
|
||||||
|
riders: piloti.filter(p => p.consenso !== null),
|
||||||
|
names: { player: 'Andrea', cpu: 'gli altri' },
|
||||||
|
win: [
|
||||||
|
{ say: 'Primo! E col Fifty di serie, o quasi.' },
|
||||||
|
{ say: 'Si rifà! Questa un vale, avevo la catena lenta.', who: 'arcu', pose: 'shrug' },
|
||||||
|
{ set: 'gara_vinta' },
|
||||||
|
],
|
||||||
|
lose: [
|
||||||
|
{ say: 'Ho preso tutta la polvere di quelli davanti.' },
|
||||||
|
{ say: 'Il Fifty è bono. È chi ci sta sopra.', who: 'arcu', pose: 'laugh' },
|
||||||
|
],
|
||||||
|
quit: [{ say: 'Lascio stare. Il Fifty mi serve intero, per il palio.' }],
|
||||||
|
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',
|
||||||
@@ -2059,19 +2082,11 @@ export const story = {
|
|||||||
people: [
|
people: [
|
||||||
{ who: 'arcu', at: [418, 292], facing: -1, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.2 },
|
{ who: 'arcu', at: [418, 292], facing: -1, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.2 },
|
||||||
// Oggetti statici: ordinati per profondità come la macchina del tempo, senza voce.
|
// Oggetti statici: ordinati per profondità come la macchina del tempo, senza voce.
|
||||||
// Con la depth della scena misurano circa 78 e 160 px; restano sopra il bordo di walk.
|
// Con la depth della scena misura circa 160 px; resta sopra il bordo di walk. (Il Betino dell'Arcu sta in
|
||||||
{ who: 'betino', at: [480, 263], size: 0.767, idle: 'idle', talk: [] },
|
// cima alla salita di Via Liguri Magelli, Andrea 28 ottobre.)
|
||||||
{ who: 'ghiacciomobile', at: [176, 286], size: 0.986, idle: 'idle', talk: [] },
|
{ who: 'ghiacciomobile', at: [176, 286], size: 0.986, idle: 'idle', talk: [] },
|
||||||
],
|
],
|
||||||
hotspots: [
|
hotspots: [
|
||||||
{
|
|
||||||
id: 'betino', name: 'il Betino', box: [441, 212, 78, 51], at: [480, 282], face: 'up', default: 'look',
|
|
||||||
// Il motorino di Andrea (sprite di Codex, 26 ottobre); la battuta è sua.
|
|
||||||
actions: {
|
|
||||||
look: [{ say: 'Nessuno ha mai capito davvero il tuo immenso valore.' }],
|
|
||||||
use: [{ say: 'O via, lascialo riposare. A pedalare oggi ci penso io.' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'ghiacciomobile', name: 'la Ghiacciomobile', box: [96, 215, 160, 71], at: [240, 308], face: 'up', default: 'look',
|
id: 'ghiacciomobile', name: 'la Ghiacciomobile', box: [96, 215, 160, 71], at: [240, 308], face: 'up', default: 'look',
|
||||||
// La Corsa nera degli amici (sprite di Codex, 26 ottobre). Parlandole, Andrea ricorda; lei non risponde.
|
// La Corsa nera degli amici (sprite di Codex, 26 ottobre). Parlandole, Andrea ricorda; lei non risponde.
|
||||||
@@ -2259,6 +2274,43 @@ export const story = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// La salita bianca fra la casa del maestro Bacci (alle spalle) e il viale dei cipressi (lassù spuntano i
|
||||||
|
// cipressi): foto di Andrea, 28 ottobre.
|
||||||
|
salita_cipressi: {
|
||||||
|
name: 'La salita dei cipressi',
|
||||||
|
map: [55, 10],
|
||||||
|
background: 'assets/scenes/salita-cipressi.png',
|
||||||
|
walk: [[0, 360], [0, 317], [208, 258], [298, 190], [333, 188], [417, 233], [583, 300], [640, 325], [640, 360]],
|
||||||
|
depth: [[188, 0.3], [360, 1.15]],
|
||||||
|
entry: [320, 350],
|
||||||
|
hotspots: [
|
||||||
|
{
|
||||||
|
id: 'cipressi', name: 'cipressi', box: [310, 118, 26, 70], at: [316, 200], face: 'up',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Lassù spuntano i cipressi: il viale del Trebbio. Ancora un pezzo di salita.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cartello', name: 'cartello', box: [104, 144, 26, 26], at: [150, 280], face: 'left',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Un cartello bianco. Bianco e basta: chi l\'ha messo s\'è scordato cosa ci voleva scrivere.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'muretto', name: 'muretto', box: [0, 232, 112, 60], at: [120, 300], face: 'left',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Un muretto di sassi e mattoni: buono per riprendere fiato a metà salita.' }],
|
||||||
|
use: [{ say: 'Mi siederei, ma se mi siedo ora non mi rialzo più.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai_cipressi', name: 'su, verso i cipressi', box: [290, 162, 66, 32], at: [316, 194], exit: { scene: 'viale_cipressi', at: [300, 350], face: 'up' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alla_casa_bacci', name: 'indietro, verso la casa del maestro Bacci', box: [0, 334, 640, 26], at: [320, 354], exit: { scene: 'casa_maestro_bacci', at: [138, 226], face: 'down' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
// Fra la Fortezza e i cipressi: la stradina fra le vigne, la casa del maestro Bacci, e lassù il Trebbio.
|
// Fra la Fortezza e i cipressi: la stradina fra le vigne, la casa del maestro Bacci, e lassù il Trebbio.
|
||||||
casa_maestro_bacci: {
|
casa_maestro_bacci: {
|
||||||
name: 'La casa del maestro Bacci',
|
name: 'La casa del maestro Bacci',
|
||||||
@@ -2288,7 +2340,7 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ai_cipressi', name: 'avanti, verso i cipressi e il Trebbio', box: [120, 150, 56, 30], at: [138, 226], exit: { scene: 'viale_cipressi', at: [300, 350], face: 'up' },
|
id: 'ai_cipressi', name: 'avanti, verso i cipressi e il Trebbio', box: [120, 150, 56, 30], at: [138, 226], exit: { scene: 'salita_cipressi', at: [320, 350], face: 'up' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'alla_fortezza', name: 'indietro, verso la Fortezza', box: [0, 330, 240, 30], at: [120, 354], exit: { scene: 'sentiero_fortezza', at: [320, 350], face: 'up' },
|
id: 'alla_fortezza', name: 'indietro, verso la Fortezza', box: [0, 330, 240, 30], at: [120, 354], exit: { scene: 'sentiero_fortezza', at: [320, 350], face: 'up' },
|
||||||
@@ -2526,10 +2578,11 @@ 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 le villette del Giannini', 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' },
|
// [DA ANDREA: dove si gira per Cardetole, in fondo al ponticino]
|
||||||
|
id: 'a_cardetole', name: 'in fondo a destra, verso Cardetole', box: [578, 80, 62, 90], at: [476, 204], exit: { scene: 'cardetole', at: [628, 290], 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' },
|
||||||
@@ -2540,7 +2593,8 @@ export const story = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
area_camper: {
|
area_camper: {
|
||||||
name: 'Area camper',
|
// Le villette dove abita il Giannini: da qui si va al suo campo (Andrea, 28 ottobre). L'id resta area_camper.
|
||||||
|
name: 'Le villette del Giannini',
|
||||||
map: [70, 84],
|
map: [70, 84],
|
||||||
background: 'assets/scenes/area-camper.png',
|
background: 'assets/scenes/area-camper.png',
|
||||||
walk: [[0, 360], [0, 260], [200, 232], [380, 240], [450, 248], [640, 300], [640, 360]],
|
walk: [[0, 360], [0, 260], [200, 232], [380, 240], [450, 248], [640, 300], [640, 360]],
|
||||||
@@ -2554,13 +2608,18 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'villette', name: 'villette', box: [500, 80, 140, 120], at: [560, 280], face: 'right',
|
id: 'villette', name: 'villette', box: [500, 80, 140, 106], at: [560, 280], face: 'right',
|
||||||
actions: {
|
actions: {
|
||||||
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: 'a_cardetole', name: 'verso casa, a Cardetole', box: [0, 230, 80, 130], at: [20, 300], exit: { scene: 'cardetole', at: [300, 350], face: 'up' },
|
// Casa è a destra, per la strada (Andrea, 28 ottobre).
|
||||||
|
id: 'a_cardetole', name: 'verso casa, a Cardetole', box: [572, 188, 68, 96], at: [618, 300], exit: { scene: 'cardetole', at: [560, 348], face: 'up' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Laggiù, in fondo al vialetto vicino alla siepe, il campo del Giannini (Andrea, 28 ottobre).
|
||||||
|
id: 'al_giannini', name: 'laggiù, il campo del Giannini', box: [330, 150, 96, 74], at: [380, 246], exit: { scene: 'campo_giannini', at: [480, 216], face: 'left' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'al_ponticino', name: 'indietro, il ponticino', box: [560, 300, 80, 60], at: [620, 330], exit: { scene: 'ponticino', at: [442, 202], face: 'down' },
|
id: 'al_ponticino', name: 'indietro, il ponticino', box: [560, 300, 80, 60], at: [620, 330], exit: { scene: 'ponticino', at: [442, 202], face: 'down' },
|
||||||
@@ -2568,7 +2627,8 @@ export const story = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Il prato del parco dei camper visto dall'altra parte: per i ragazzi, «il campo del Giannini». Ci si arriva dal
|
// 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).
|
// lato delle villette dove abita il Giannini (fra gli alberi a destra) e dal Trappola; dal ponticino non si viene
|
||||||
|
// qui. L'area camper è il campo stesso: i camper stanno qui dietro (Andrea, 28 ottobre).
|
||||||
campo_giannini: {
|
campo_giannini: {
|
||||||
name: 'Il campo del Giannini',
|
name: 'Il campo del Giannini',
|
||||||
map: [82, 96],
|
map: [82, 96],
|
||||||
@@ -2584,6 +2644,12 @@ export const story = {
|
|||||||
use: [{ say: 'Senza pallone, e senza la squadra, non vale.' }],
|
use: [{ say: 'Senza pallone, e senza la squadra, non vale.' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'camper', name: 'qui dietro, i camper', box: [520, 320, 120, 40], at: [560, 340], face: 'down', default: 'look',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Qui dietro ci stanno i camper. Ma per noi questo resta il campo del Giannini.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'giochi', name: 'giochi dei bambini', box: [220, 170, 80, 40], at: [260, 216], face: 'up',
|
id: 'giochi', name: 'giochi dei bambini', box: [220, 170, 80, 40], at: [260, 216], face: 'up',
|
||||||
actions: {
|
actions: {
|
||||||
@@ -2591,7 +2657,67 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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' },
|
// Fra gli alberi a destra si arriva alle villette del Giannini, in fondo al vialetto (Andrea, 28 ottobre).
|
||||||
|
id: 'all_area_camper', name: 'verso le villette del Giannini', box: [440, 150, 90, 60], at: [480, 216], exit: { scene: 'area_camper', at: [380, 246], face: 'down' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'via_liguri_magelli', name: 'dal Trappola, in Via Liguri Magelli', box: [0, 100, 70, 260], at: [30, 260], exit: { scene: 'via_liguri_magelli', at: [520, 306], face: 'left' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Via Liguri Magelli (Street View mandato da Andrea, 28 ottobre): a sinistra dal campo del Giannini. A destra la
|
||||||
|
// casa salmone con la tenda del Trappola (il Barrettino), sul muro giallo la targa della Ghiacciomobile con la sua
|
||||||
|
// fotina, e in cima alla salita il Betino dell'Arcu (dipinto da Codex nel quadro), di traverso verso la discesa:
|
||||||
|
// per partire ne ha bisogno, e intanto ferma il traffico.
|
||||||
|
via_liguri_magelli: {
|
||||||
|
name: 'Via Liguri Magelli',
|
||||||
|
map: [76, 97],
|
||||||
|
background: 'assets/scenes/via-liguri-magelli.png',
|
||||||
|
walk: [[0, 360], [0, 320], [140, 262], [226, 198], [272, 198], [400, 250], [580, 318], [640, 330], [640, 360]],
|
||||||
|
depth: [[198, 0.3], [360, 1.1]],
|
||||||
|
entry: [320, 340],
|
||||||
|
speakers: { arcu: [374, 176] },
|
||||||
|
people: [
|
||||||
|
// L'Arcu sotto la tenda del Trappola: due minuti fa era a Campomigliaio (Andrea, 28 ottobre).
|
||||||
|
{ who: 'arcu', at: [374, 248], facing: -1, size: 1.3, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.2 },
|
||||||
|
],
|
||||||
|
hotspots: [
|
||||||
|
{
|
||||||
|
id: 'arcu', name: 'l\'Arcu', box: [362, 180, 26, 68], at: [352, 256], face: 'right', default: 'talk',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'L\'Arcu, sotto la tenda del Trappola. Due minuti fa era a Campomigliaio.' }],
|
||||||
|
talk: [{ dialogue: 'arcu_trappola' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// «Vai dal Trappola, la tenda è il Trappola» (Andrea). [DA ANDREA: com'è dentro, e chi c'è]
|
||||||
|
id: 'trappola', name: 'dal Trappola', box: [316, 150, 46, 72], at: [340, 238], face: 'right', default: 'walk',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'La tenda è il Trappola: sotto c\'è il Barrettino.' }],
|
||||||
|
walk: [{ say: 'Dal Trappola ci si passa dopo. Prima il palio.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'targa', name: 'targa della Ghiacciomobile', box: [570, 130, 62, 78], at: [565, 336], face: 'right', default: 'look',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: '«Qui riposa la Ghiacciomobile, che si è schiantata lì.»' }, { say: 'C\'è anche la fotina. Pareva sempre nuova.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// La prima battuta è di Andrea (dal circolino, 26 ottobre).
|
||||||
|
id: 'betino', name: 'il Betino dell\'Arcu', box: [232, 172, 40, 24], at: [250, 206], face: 'up', default: 'look',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Nessuno ha mai capito davvero il tuo immenso valore.' }, { say: 'Lassù in cima, di traverso: per partire ha bisogno della discesa. E intanto ferma il traffico.' }],
|
||||||
|
use: [{ say: 'È il Betino dell\'Arcu. Senza una discesa davanti, un si muove.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Casa di Andrea è lassù, in cima alla salita, a destra del Betino (Andrea, 28 ottobre).
|
||||||
|
id: 'a_cardetole', name: 'lassù, verso casa a Cardetole', box: [274, 148, 40, 50], at: [264, 202], exit: { scene: 'cardetole', at: [20, 352], face: 'right' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Il campo del Giannini è laggiù a destra, oltre la siepe (Andrea, 28 ottobre).
|
||||||
|
id: 'al_giannini', name: 'verso il campo del Giannini', box: [430, 150, 132, 100], at: [520, 306], exit: { scene: 'campo_giannini', at: [60, 250], face: 'right' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -2607,21 +2733,38 @@ export const story = {
|
|||||||
{
|
{
|
||||||
id: 'casa', name: 'casa di Andrea', box: [330, 20, 220, 212], at: [470, 290], face: 'up',
|
id: 'casa', name: 'casa di Andrea', box: [330, 20, 220, 212], at: [470, 290], face: 'up',
|
||||||
actions: {
|
actions: {
|
||||||
look: [{ say: 'Casa mia. Terzo piano, quello con la tapparella a metà: è la mia camera.' }],
|
look: [{ say: 'Casa mia. Secondo piano: quella con la tapparella a metà è la camera di mia sorella, e a destra quella dei miei.' }],
|
||||||
open: [{ say: 'Se salgo, mi mettono ad apparecchiare. Prima il palio.' }],
|
open: [{ say: 'Se salgo, mi mettono ad apparecchiare. Prima il palio.' }],
|
||||||
walk: [{ say: 'Se salgo, mi mettono ad apparecchiare. Prima il palio.' }],
|
walk: [{ say: 'Se salgo, mi mettono ad apparecchiare. Prima il palio.' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'platano', name: 'platano', box: [90, 0, 170, 216], at: [200, 306], face: 'up',
|
id: 'platano', name: 'platano', box: [90, 0, 170, 206], at: [200, 306], face: 'up',
|
||||||
actions: {
|
actions: {
|
||||||
look: [{ say: 'Il prato sotto casa. Qui le porte le facevamo con le maglie per terra: due maglie, un palo ciascuna.' }, { say: 'E ogni gol era «Era fuori!», «No, era dentro!».' }],
|
look: [{ say: 'Il prato sotto casa. Qui le porte le facevamo con le maglie per terra: due maglie, un palo ciascuna.' }, { say: 'E ogni gol era «Era fuori!», «No, era dentro!».' }],
|
||||||
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' },
|
// Il prato in salita sotto casa: le parole sono di Andrea (28 ottobre).
|
||||||
|
id: 'prato', name: 'prato', box: [52, 208, 278, 76], at: [262, 300], face: 'up', default: 'look',
|
||||||
|
actions: {
|
||||||
|
look: [
|
||||||
|
{ say: 'Su questo campo ci abbiamo passato trentacinque anni, giorno, pomeriggio e sera, a giocare a calcio.' },
|
||||||
|
{ say: 'Le maglie a fare da pali, e i palloni tagliati che entravano nelle terrazze, ovunque.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Le uscite di casa (Andrea, 28 ottobre): in basso a destra il Giannini (le sue villette, dalla strada delle
|
||||||
|
// villette), a destra il ponticino della Carza, a sinistra il Trappola.
|
||||||
|
{
|
||||||
|
id: 'al_giannini', name: 'dal Giannini', box: [440, 322, 200, 38], at: [560, 348], exit: { scene: 'area_camper', at: [618, 300], face: 'left' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'al_ponticino', name: 'a destra, il ponticino della Carza', box: [596, 150, 44, 110], at: [628, 290], exit: { scene: 'ponticino', at: [476, 204], face: 'down' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'al_trappola', name: 'a sinistra, dal Trappola', box: [0, 236, 50, 104], at: [20, 352], exit: { scene: 'via_liguri_magelli', at: [264, 202], face: 'down' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -2692,7 +2835,7 @@ export const story = {
|
|||||||
id: 'al_trebbio', name: 'in fondo, verso il Trebbio', box: [262, 214, 76, 50], at: [300, 256], exit: { scene: 'trebbio', at: [110, 345], face: 'right' },
|
id: 'al_trebbio', name: 'in fondo, verso il Trebbio', box: [262, 214, 76, 50], at: [300, 256], exit: { scene: 'trebbio', at: [110, 345], face: 'right' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'alla_fortezza', name: 'indietro, verso la casa del maestro Bacci', box: [0, 330, 640, 30], at: [300, 352], exit: { scene: 'casa_maestro_bacci', at: [138, 226], face: 'down' },
|
id: 'alla_fortezza', name: 'indietro, verso la casa del maestro Bacci', box: [0, 330, 640, 30], at: [300, 352], exit: { scene: 'salita_cipressi', at: [316, 196], face: 'down' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -2972,6 +3115,35 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// L'Arcu dal Trappola (Andrea, 28 ottobre): lo scherzo sulla velocità del Betino, poi la sfida: «il giro della Fortezza» (Andrea), cioè il GP di Fortezza;
|
||||||
|
// prima del via prende la rincorsa per accenderlo in discesa. [DA ANDREA: le battute sono provvisorie]
|
||||||
|
arcu_trappola: {
|
||||||
|
nodes: {
|
||||||
|
start: {
|
||||||
|
options: [
|
||||||
|
{ text: 'Ma ti ho visto du\' minuti fa a Campomigliaio!', then: [
|
||||||
|
{ say: 'E allora? Col Betino ci metto un attimo.', who: 'arcu', pose: 'explain' },
|
||||||
|
{ say: 'In discesa.', who: 'arcu', pose: 'laugh' },
|
||||||
|
{ say: 'E in salita?' },
|
||||||
|
{ say: 'In salita si spinge.', who: 'arcu', pose: 'shrug' },
|
||||||
|
] },
|
||||||
|
{ text: 'Si fa una gara coi motorini? Il giro della Fortezza!', next: 'gara', then: [
|
||||||
|
{ say: 'Col Betino? Io un perdo mai: un vo mai forte abbastanza da cadere.', who: 'arcu', pose: 'explain' },
|
||||||
|
{ say: 'Come la si fa?', who: 'arcu', pose: 'talk' },
|
||||||
|
] },
|
||||||
|
{ text: 'Ci si vede dopo, Arcu.', next: 'end' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
gara: {
|
||||||
|
options: [
|
||||||
|
{ text: 'Alla bona, un giro di paese.', next: 'end', then: [{ say: 'Alla bona. Chi arriva ultimo paga le paste.', who: 'arcu', pose: 'laugh' }, ...rincorsa('bona')] },
|
||||||
|
{ text: 'Seria.', next: 'end', then: [{ say: 'Seria. Tre giri, e la paglia in curva è vera.', who: 'arcu', pose: 'explain' }, ...rincorsa('seria')] },
|
||||||
|
{ text: 'Ginocchio in terra.', next: 'end', then: [{ say: 'Ginocchio in terra? Puntella un vede l\'ora.', who: 'arcu', pose: 'shrug' }, ...rincorsa('male')] },
|
||||||
|
{ text: 'No, lascia perdere.', next: 'start' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
// 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,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.29';
|
export const VERSION = '0.1.33';
|
||||||
|
|
||||||
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica,
|
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica,
|
||||||
// mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf).
|
// mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf).
|
||||||
|
|||||||
@@ -53,16 +53,16 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
|
|||||||
.bocce-adjust button { min-height: 44px; padding: 8px 12px; border: 1px solid #897b57; border-radius: 4px; background: #38453c; color: #f2e1bf; cursor: pointer; font-size: 11px; }
|
.bocce-adjust button { min-height: 44px; padding: 8px 12px; border: 1px solid #897b57; border-radius: 4px; background: #38453c; color: #f2e1bf; cursor: pointer; font-size: 11px; }
|
||||||
#bocce-throw { background: #ead09a; color: #202b25; min-width: 100px; }
|
#bocce-throw { background: #ead09a; color: #202b25; min-width: 100px; }
|
||||||
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
|
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
|
||||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
||||||
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary { cursor: pointer; padding: 6px 0; }
|
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary, #gara-help summary { cursor: pointer; padding: 6px 0; }
|
||||||
#bocce-help p, #biliardo-help p, #basket-help p, #pesca-help p { max-width: 76em; line-height: 1.7; }
|
#bocce-help p, #biliardo-help p, #basket-help p, #pesca-help p, #gara-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, #pesca-help { font-size: 9px; }
|
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-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); }
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ test('clic intelligente: parla con le persone, guarda le cose, attraversa le usc
|
|||||||
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 Via dell\'Antica Posta, verso Piazzetta Cambray Digny');
|
assert.equal(c.sentence('verso_piazzetta'), 'Vai a Via dell\'Antica Posta, verso Piazzetta Cambray Digny');
|
||||||
|
// Quando il nome dice già dove, niente «a» in più: «Vai dal Trappola», «Vai a destra, …».
|
||||||
|
const casa = new Game(story), cc = new Command(casa); casa.go('cardetole');
|
||||||
|
assert.equal(cc.sentence('al_giannini'), 'Vai dal Giannini');
|
||||||
|
assert.equal(cc.sentence('al_ponticino'), 'Vai a destra, il ponticino della Carza');
|
||||||
|
casa.go('via_liguri_magelli');
|
||||||
|
assert.equal(cc.sentence('a_cardetole'), 'Vai lassù, verso casa a Cardetole');
|
||||||
|
assert.equal(cc.sentence('trappola'), 'Vai dal Trappola');
|
||||||
assert.equal(c.clickScene(at('vecchietti')), 'act');
|
assert.equal(c.clickScene(at('vecchietti')), 'act');
|
||||||
settle(g);
|
settle(g);
|
||||||
assert.equal(g.dialogue?.id, 'vecchietti');
|
assert.equal(g.dialogue?.id, 'vecchietti');
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { newGara, update, press, presenting, INTRO, release, cancel, shoot, key, steerButton, brakeButton, aimScreen, steering, status, button, standings, player, rider, limitAt, pointAt, TRACK, CURVES, PICKUPS, BRAKE, VERGE, LAPS, COUNTDOWN, RIDERS } from '../src/engine/gara.js';
|
||||||
|
import { piloti } from '../src/story/piloti.js';
|
||||||
|
import { story } from '../src/story/story.js';
|
||||||
|
import { Game } from '../src/engine/game.js';
|
||||||
|
import { settle } from './helpers.mjs';
|
||||||
|
|
||||||
|
// Gli eventi sonori di ogni gara si ricordano nella gara stessa (`c.heard`, «tipo» e «tipo:pilota»): un test non
|
||||||
|
// vede quelli degli altri.
|
||||||
|
const run = (c, seconds, drive, dt = 1 / 60) => {
|
||||||
|
c.heard ??= new Set();
|
||||||
|
for (let t = 0; t < seconds && c.step !== 'result'; t += dt) {
|
||||||
|
if (drive && c.step === 'race') c.holding = drive(c);
|
||||||
|
update(c, dt); for (const e of c.events.splice(0)) { c.heard.add(e.kind); c.heard.add(`${e.kind}:${e.who}`); }
|
||||||
|
}
|
||||||
|
return c.heard;
|
||||||
|
};
|
||||||
|
const skip = c => { while (c.step === 'intro') press(c); return c; };
|
||||||
|
const race = c => { skip(c); assert.equal(c.step, 'start'); run(c, COUNTDOWN + 0.01); assert.equal(c.step, 'race'); return c; };
|
||||||
|
// Un dito al centro della scena indica la direzione del punto davanti al motorino.
|
||||||
|
const swipeTo = (c, target) => {
|
||||||
|
const r = player(c), dx = target.x - r.x, dy = target.y - r.y, length = Math.hypot(dx, dy);
|
||||||
|
if (c.pointer === null) { aimScreen(c, [320, 180], 'bot'); press(c, 'bot'); }
|
||||||
|
aimScreen(c, [c.anchor[0] + dx / length * 50, c.anchor[1] + dy / length * 50], 'bot');
|
||||||
|
};
|
||||||
|
// Il pilota di giudizio: guarda avanti e ci sterza (scansando buche e chi gli sta davanti), e dà gas finché,
|
||||||
|
// frenando, arriverebbe a ogni curva un po' sotto il limite. Usa i comandi del giocatore (il trascinamento).
|
||||||
|
const careful = c => {
|
||||||
|
const r = player(c), here = ((r.s % TRACK.length) + TRACK.length) % TRACK.length;
|
||||||
|
const next = PICKUPS.find(p => p.kind !== 'boost' && p.s - here > -8 && p.s - here < 60);
|
||||||
|
const slow = c.riders.find(o => o !== r && o.s - r.s > 0 && o.s - r.s < 26 && Math.abs(o.lat - r.lat) < 7);
|
||||||
|
const lane = next?.kind === 'buca' ? (next.lane > 0 ? -6 : 6) : slow ? (slow.lat > 0 ? -6 : 6) : next ? next.lane : 0;
|
||||||
|
swipeTo(c, pointAt(r.s + 14 + r.v * 0.18, lane));
|
||||||
|
const horizon = r.v * r.v / (2 * BRAKE) + 20;
|
||||||
|
let brake = false;
|
||||||
|
for (let d = 0; d <= horizon; d += 4) { const a = limitAt(r.s + d) * 0.85; if (r.v * r.v > a * a + 2 * BRAKE * d) { brake = true; break; } }
|
||||||
|
key(c, 'ArrowDown', brake);
|
||||||
|
return c.holding;
|
||||||
|
};
|
||||||
|
|
||||||
|
test('i piloti sono dati: motore e testa da 1 a 5 danno velocità, accelerazione e prudenza', () => {
|
||||||
|
const slow = rider({ motore: 1, testa: 5 }), fast = rider({ motore: 5, testa: 1 });
|
||||||
|
assert.ok(fast.vmax > slow.vmax && fast.accel > slow.accel && fast.grip < slow.grip);
|
||||||
|
assert.ok(fast.slip > slow.slip && fast.caution > slow.caution);
|
||||||
|
assert.deepEqual(rider({ motore: 9, testa: -2 }).motore, 5, 'fuori scala si ferma ai bordi');
|
||||||
|
// Ogni pilota della storia ha nome, motorino e colore; Andrea c'è una volta sola.
|
||||||
|
for (const p of piloti) { assert.ok(p.id && p.name && p.bike && /^#[0-9a-f]{6}$/i.test(p.color), p.id); }
|
||||||
|
assert.equal(piloti.filter(p => p.id === 'andrea').length, 1);
|
||||||
|
assert.throws(() => newGara(1, { riders: [{ id: 'arcu', name: 'L\'Arcu' }] }), /manca Andrea/);
|
||||||
|
const c = newGara(1, { riders: piloti });
|
||||||
|
assert.equal(c.riders.length, piloti.length);
|
||||||
|
assert.deepEqual(c.riders.map(r => r.s), piloti.map((_, k) => -16 - 14 * Math.floor(k / 2)), 'due per fila, dietro il traguardo');
|
||||||
|
assert.deepEqual(c.riders.map(r => Math.sign(r.lat)), piloti.map((_, k) => k % 2 ? 1 : -1));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il semaforo, poi il gas: tenendo premuto si va, lasciando si rallenta, il freno vince sul gas', () => {
|
||||||
|
const c = newGara(3, { level: 'seria', riders: RIDERS });
|
||||||
|
for (const r of RIDERS) assert.ok(press(c, 7), r.id);
|
||||||
|
assert.equal(c.step, 'start', 'un tocco per pilota e la presentazione è finita');
|
||||||
|
assert.equal(c.holding, false, 'senza dare gas');
|
||||||
|
assert.ok(press(c, 7)); assert.equal(c.holding, true);
|
||||||
|
const start = player(c).s;
|
||||||
|
const heard = run(c, COUNTDOWN - 0.1);
|
||||||
|
assert.equal(c.step, 'start'); assert.equal(player(c).s, start, 'prima del via non si muove nessuno');
|
||||||
|
run(c, 0.15);
|
||||||
|
assert.equal(c.step, 'race'); assert.ok(heard.has('gara-semaforo') && heard.has('gara-via'));
|
||||||
|
run(c, 1);
|
||||||
|
const v = player(c).v; assert.ok(v > 20);
|
||||||
|
release(c, 7); run(c, 0.2);
|
||||||
|
assert.ok(player(c).v < v, 'senza gas rallenta');
|
||||||
|
assert.match(status(c), /Giro 1 di 3/); assert.equal(button(c), 'Gas');
|
||||||
|
// Gas e freno insieme: frena (P1 della revisione di Codex).
|
||||||
|
key(c, 'ArrowUp', true); run(c, 0.5); const fast = player(c).v;
|
||||||
|
key(c, 'ArrowDown', true); run(c, 0.1);
|
||||||
|
assert.ok(c.holding && c.braking); assert.ok(player(c).v < fast - 10, 'col gas tenuto il freno frena');
|
||||||
|
key(c, 'ArrowDown', false); key(c, 'ArrowUp', false);
|
||||||
|
shoot(c); assert.equal(c.holding, true); assert.equal(button(c), 'Molla');
|
||||||
|
shoot(c); assert.equal(c.holding, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('le fonti del gas sono indipendenti: lasciare un tasto non toglie quello del dito o degli altri tasti', () => {
|
||||||
|
const c = race(newGara(3, { riders: RIDERS }));
|
||||||
|
key(c, ' ', true); key(c, 'ArrowUp', true); key(c, 'ArrowUp', false);
|
||||||
|
assert.equal(c.holding, true, 'Spazio ancora giù');
|
||||||
|
press(c, 1); key(c, ' ', false);
|
||||||
|
assert.equal(c.holding, true, 'il dito ancora giù');
|
||||||
|
assert.equal(press(c, 2), false, 'un secondo dito sulla scena non conta');
|
||||||
|
release(c, 2); assert.equal(c.holding, true, 'e lasciandolo non toglie il gas al primo');
|
||||||
|
release(c, 1); assert.equal(c.holding, false);
|
||||||
|
shoot(c); key(c, ' ', true); key(c, ' ', false); assert.equal(c.holding, true, 'il pulsante Gas resta acceso');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lo sterzo: frecce, pulsanti e il dito che trascina; il mouse che passa senza premere non sterza', () => {
|
||||||
|
const c = race(newGara(3, { riders: RIDERS }));
|
||||||
|
assert.ok(key(c, 'ArrowLeft', true)); assert.equal(steering(c), -1);
|
||||||
|
key(c, 'ArrowLeft', false); assert.equal(steering(c), 0);
|
||||||
|
steerButton(c, 1); assert.equal(steering(c), 1); steerButton(c, 0);
|
||||||
|
// Gas da tastiera e sinistra, il mouse si muove a destra senza premere: lo sterzo resta a sinistra (P1).
|
||||||
|
key(c, 'ArrowUp', true); key(c, 'ArrowLeft', true);
|
||||||
|
aimScreen(c, [300, 100], 5); aimScreen(c, [360, 100], 5);
|
||||||
|
assert.equal(steering(c), -1);
|
||||||
|
key(c, 'ArrowUp', false); key(c, 'ArrowLeft', false);
|
||||||
|
// Il dito indica una direzione; un altro puntatore non sposta niente.
|
||||||
|
player(c).a = -0.5 / 2.6;
|
||||||
|
aimScreen(c, [300, 100], 9); press(c, 9);
|
||||||
|
aimScreen(c, [314, 100], 9); assert.ok(Math.abs(steering(c) - 0.5) < 1e-10);
|
||||||
|
aimScreen(c, [250, 100], 4); assert.ok(Math.abs(steering(c) - 0.5) < 1e-10);
|
||||||
|
release(c, 9); assert.equal(steering(c), 0, 'lasciando il dito lo sterzo torna dritto');
|
||||||
|
const a = player(c).a; key(c, 'ArrowUp', true); key(c, 'ArrowRight', true); run(c, 0.4); assert.notEqual(player(c).a, a, 'sterzando si gira');
|
||||||
|
assert.equal(key(c, 'x', true), false, 'gli altri tasti non sono suoi');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lo swipe segue lo schermo: scendendo, destra sterza negativo; avanti e piccoli movimenti vanno dritto', () => {
|
||||||
|
const c = race(newGara(3)), r = player(c);
|
||||||
|
r.a = Math.PI / 2;
|
||||||
|
aimScreen(c, [320, 180], 9); press(c, 9);
|
||||||
|
aimScreen(c, [370, 180], 9);
|
||||||
|
assert.equal(steering(c), -1, 'scendendo, la destra dello schermo è a sinistra del motorino');
|
||||||
|
r.a = 0;
|
||||||
|
assert.ok(Math.abs(steering(c)) < 1e-10, 'col dito fermo lo sterzo si annulla quando il motorino è allineato');
|
||||||
|
r.a = Math.PI / 2;
|
||||||
|
aimScreen(c, [320, 230], 9); assert.ok(Math.abs(steering(c)) < 1e-10, 'swipe in avanti');
|
||||||
|
aimScreen(c, [323, 184], 9); assert.equal(steering(c), 0, '5 px non fanno tremare lo sterzo');
|
||||||
|
aimScreen(c, [330, 230], 9);
|
||||||
|
assert.ok(Math.abs(steering(c) + Math.atan2(10, 50) * 2.6) < 1e-10, 'sterzo proporzionale all’angolo');
|
||||||
|
r.a = -Math.PI + 0.1;
|
||||||
|
aimScreen(c, [270, 180], 9);
|
||||||
|
assert.ok(Math.abs(steering(c) + 0.26) < 1e-10, 'si gira dalla parte più breve anche passando da π a -π');
|
||||||
|
release(c, 9);
|
||||||
|
assert.equal(c.holding, false); assert.equal(steering(c), 0);
|
||||||
|
assert.equal(c.anchor, null); assert.equal(c.finger, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('l’origine segue gli swipe lunghi e si può correggere; rilascio e annullamento puliscono il dito', () => {
|
||||||
|
const c = race(newGara(3));
|
||||||
|
player(c).a = 0;
|
||||||
|
aimScreen(c, [100, 100], 9); press(c, 9);
|
||||||
|
for (const x of [200, 300, 400]) {
|
||||||
|
aimScreen(c, [x, 100], 9);
|
||||||
|
assert.deepEqual(c.anchor, [x - 60, 100]);
|
||||||
|
assert.ok(Math.abs(steering(c)) < 1e-10);
|
||||||
|
}
|
||||||
|
aimScreen(c, [400, 160], 9);
|
||||||
|
assert.ok(Math.abs(Math.hypot(c.finger[0] - c.anchor[0], c.finger[1] - c.anchor[1]) - 60) < 1e-10);
|
||||||
|
assert.equal(steering(c), 1, 'si corregge verso il basso senza arrivare al bordo');
|
||||||
|
key(c, 'ArrowLeft', true); shoot(c); brakeButton(c, true);
|
||||||
|
release(c, 9);
|
||||||
|
assert.equal(steering(c), -1, 'lasciare il dito conserva lo sterzo della tastiera');
|
||||||
|
assert.ok(c.holding && c.braking, 'Gas e Frena del pannello restano attivi');
|
||||||
|
key(c, 'ArrowDown', true); brakeButton(c, false);
|
||||||
|
assert.ok(c.braking, 'il freno della tastiera resta attivo');
|
||||||
|
aimScreen(c, [200, 200], 10); press(c, 10);
|
||||||
|
assert.equal(c.drag, 0, 'il nuovo tocco non eredita lo swipe');
|
||||||
|
cancel(c);
|
||||||
|
assert.equal(c.holding, false); assert.equal(c.braking, false); assert.equal(steering(c), 0);
|
||||||
|
assert.equal(c.anchor, null); assert.equal(c.finger, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('alla bona si completano tre giri usando solo un dito, senza tasti o pulsanti', () => {
|
||||||
|
const c = race(newGara(5, { level: 'bona', riders: piloti }));
|
||||||
|
aimScreen(c, [320, 180], 'bot'); press(c, 'bot');
|
||||||
|
for (let t = 0; t < 180 && c.step === 'race'; t += 1 / 60) {
|
||||||
|
swipeTo(c, pointAt(player(c).s + 50));
|
||||||
|
assert.ok(c.holding && c.pointer === 'bot');
|
||||||
|
assert.equal(c.keys.gas.size, 0); assert.equal(c.toggle, false);
|
||||||
|
assert.equal(c.keys.left || c.keys.right || c.braking || !!c.buttons, false);
|
||||||
|
update(c, 1 / 60);
|
||||||
|
}
|
||||||
|
assert.equal(c.step, 'result');
|
||||||
|
assert.equal(player(c).laps, LAPS);
|
||||||
|
assert.ok(player(c).finished > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la presentazione: uno alla volta arrivano in griglia col motorino, poi il semaforo', () => {
|
||||||
|
const c = newGara(2, { riders: piloti });
|
||||||
|
assert.equal(c.step, 'intro'); assert.equal(presenting(c).id, piloti[0].id);
|
||||||
|
assert.ok(c.riders.every(r => !r.shown), 'all\'inizio la griglia è vuota');
|
||||||
|
assert.match(status(c), new RegExp(`${piloti[0].name} con ${piloti[0].bike}`));
|
||||||
|
run(c, INTRO.each + INTRO.ride / 2);
|
||||||
|
assert.ok(c.riders[0].shown && c.riders[1].shown && !c.riders[2].shown);
|
||||||
|
assert.equal(c.riders[0].s, c.riders[0].grid, 'il primo è già al suo posto');
|
||||||
|
assert.ok(c.riders[1].s < c.riders[1].grid, 'il secondo sta arrivando');
|
||||||
|
assert.equal(presenting(c).id, piloti[1].id);
|
||||||
|
assert.ok(c.heard.has('gara-motore:' + piloti[0].id) && c.heard.has('gara-motore:' + piloti[1].id));
|
||||||
|
run(c, piloti.length * INTRO.each + INTRO.after - (INTRO.each + INTRO.ride / 2) + 0.05);
|
||||||
|
assert.equal(c.step, 'start');
|
||||||
|
assert.ok(c.riders.every(r => r.shown && r.s === r.grid && r.v === 0), 'tutti fermi in griglia');
|
||||||
|
// Un tocco: chi arriva è subito al suo posto, e tocca al prossimo.
|
||||||
|
const d = newGara(2, { riders: piloti });
|
||||||
|
run(d, 0.3); assert.ok(d.riders[0].s < d.riders[0].grid);
|
||||||
|
assert.ok(press(d)); run(d, 0.01);
|
||||||
|
assert.equal(presenting(d).id, piloti[1].id); assert.equal(d.riders[0].s, d.riders[0].grid);
|
||||||
|
assert.equal(d.step, 'intro'); assert.equal(button(d), 'Avanti');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sempre a gas e dritto si finisce nella paglia; con giudizio alla bona si vince', () => {
|
||||||
|
const wild = newGara(5, { level: 'bona', riders: piloti });
|
||||||
|
skip(wild);
|
||||||
|
run(wild, COUNTDOWN + 60, () => true);
|
||||||
|
assert.ok(wild.heard.has('gara-fuori:andrea'));
|
||||||
|
assert.ok(Math.abs(player(wild).lat) <= VERGE, 'la paglia rimanda in strada');
|
||||||
|
assert.equal(wild.step, 'race', 'dritto non si arriva');
|
||||||
|
const heard = new Set();
|
||||||
|
for (const seed of [1, 2, 3, 4]) {
|
||||||
|
const c = newGara(seed, { level: 'bona', riders: piloti });
|
||||||
|
skip(c);
|
||||||
|
for (const k of run(c, COUNTDOWN + 120, careful)) heard.add(k);
|
||||||
|
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'player', `seme ${seed}`);
|
||||||
|
assert.equal(c.result.order[0], 'andrea'); assert.match(status(c), /Primo al GP di Fortezza!/);
|
||||||
|
assert.equal(Object.keys(c.result.times).length, piloti.length);
|
||||||
|
}
|
||||||
|
assert.ok(['gara-giro:andrea', 'gara-arrivo:andrea', 'gara-turbo:andrea', 'gara-miscela:andrea'].every(k => heard.has(k)), [...heard].join());
|
||||||
|
assert.ok(CURVES.length >= 2 && TRACK.length > 600, 'una pista con delle curve vere');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('le buche: piano si sobbalza, forte si casca; la lattina di miscela dà lo sprint', () => {
|
||||||
|
const hole = PICKUPS.find(p => p.kind === 'buca'), can = PICKUPS.find(p => p.kind === 'miscela');
|
||||||
|
for (const [speed, fell] of [[20, false], [80, true]]) {
|
||||||
|
const c = newGara(1, { riders: RIDERS }), r = player(c);
|
||||||
|
c.step = 'race'; Object.assign(r, { s: hole.s - 4, idx: Math.floor(hole.s / 2), v: speed, a: hole.heading });
|
||||||
|
const p = pointAt(hole.s - 4, hole.lane); r.x = p.x; r.y = p.y;
|
||||||
|
const heard = run(c, 0.2);
|
||||||
|
assert.equal(r.falls, fell ? 1 : 0, `a ${speed}`);
|
||||||
|
assert.ok(heard.has(fell ? 'gara-buca:andrea' : 'gara-sobbalzo:andrea'));
|
||||||
|
}
|
||||||
|
const c = newGara(1, { riders: RIDERS }), r = player(c);
|
||||||
|
c.step = 'race'; Object.assign(r, { s: can.s - 3, idx: Math.floor(can.s / 2), v: 40, a: can.heading });
|
||||||
|
const p = pointAt(can.s - 3, can.lane); r.x = p.x; r.y = p.y;
|
||||||
|
run(c, 0.2);
|
||||||
|
assert.ok(r.boost > 0 && c.taken[can.id] !== undefined, 'la miscela dà lo sprint');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la classifica: chi è arrivato davanti, poi per strada fatta', () => {
|
||||||
|
const c = newGara(1, { riders: piloti });
|
||||||
|
c.riders[0].s = 50; c.riders[1].s = 80; c.riders[2].finished = 30;
|
||||||
|
assert.deepEqual(standings(c).slice(0, 3).map(r => r.id), [c.riders[2].id, c.riders[1].id, c.riders[0].id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nella storia: dal Trappola l\'Arcu scherza sul Betino, poi la sfida; prima del via prende la rincorsa; la vittoria lascia il flag', () => {
|
||||||
|
const g = new Game(story);
|
||||||
|
g.go('via_liguri_magelli', [352, 256]); settle(g);
|
||||||
|
g.act('talk', 'arcu'); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text.startsWith('Ma ti ho visto du')));
|
||||||
|
const joke = settle(g).map(s => `${s.who ?? 'andrea'}: ${s.text}`);
|
||||||
|
assert.ok(joke.includes('arcu: In salita si spinge.'));
|
||||||
|
g.choose(g.options().findIndex(o => o.text.startsWith('Si fa una gara coi motorini?'))); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'Seria.'));
|
||||||
|
const before = settle(g).map(s => s.text);
|
||||||
|
assert.ok(before.some(t => t.includes('prendo la rincorsa')));
|
||||||
|
assert.equal(g.closeup?.id, 'gara'); assert.equal(g.closeup.level, 'seria');
|
||||||
|
assert.deepEqual(g.closeup.riders.map(r => r.id), piloti.filter(p => p.consenso !== null).map(p => p.id));
|
||||||
|
assert.ok(!g.closeup.riders.some(r => r.id === 'puntella'), 'senza consenso non corre');
|
||||||
|
g.closeup.result = { winner: 'player', place: 1, times: {}, order: ['andrea'], offs: 0 }; g.closeup.step = 'result';
|
||||||
|
g.closeupClose(); const after = settle(g).map(s => s.text);
|
||||||
|
assert.ok(g.state.flags.gara_vinta);
|
||||||
|
assert.ok(after.some(t => t.includes('Primo!')));
|
||||||
|
// Il Betino, lassù: solo Andrea ne parla.
|
||||||
|
g.act('use', 'betino'); assert.ok(settle(g).every(s => s.who === 'andrea'));
|
||||||
|
});
|
||||||
@@ -6,12 +6,16 @@ import { Game } from '../src/engine/game.js';
|
|||||||
import { inside } from '../src/engine/geometry.js';
|
import { inside } from '../src/engine/geometry.js';
|
||||||
import { settle } from './helpers.mjs';
|
import { settle } from './helpers.mjs';
|
||||||
|
|
||||||
test('i veicoli del circolino restano fuori dal percorso e lasciano liberi gli altri hotspot', () => {
|
// La Ghiacciomobile al circolino è uno sprite; il Betino in cima alla salita di Via Liguri Magelli è dipinto nel quadro.
|
||||||
|
const SPRITES = [['campomigliaio_circolo', 'ghiacciomobile', [150, 170]]];
|
||||||
|
const VEHICLES = [...SPRITES, ['via_liguri_magelli', 'betino']];
|
||||||
|
test('i veicoli restano fuori dal percorso e lasciano liberi gli altri hotspot', () => {
|
||||||
|
for (const [scene, id, [lo, hi]] of SPRITES) {
|
||||||
const g = new Game(story);
|
const g = new Game(story);
|
||||||
g.go('campomigliaio_circolo');
|
g.go(scene);
|
||||||
settle(g);
|
settle(g);
|
||||||
const vehicles = ['betino', 'ghiacciomobile'];
|
const vehicles = [id];
|
||||||
for (const id of vehicles) {
|
{
|
||||||
const person = g.people().find(p => p.who === id), h = g.hotspot(id);
|
const person = g.people().find(p => p.who === id), h = g.hotspot(id);
|
||||||
const url = new URL('../' + story.characters[id].sprite, import.meta.url);
|
const url = new URL('../' + story.characters[id].sprite, import.meta.url);
|
||||||
const atlas = JSON.parse(readFileSync(url, 'utf8')), frame = atlas.frames.idle;
|
const atlas = JSON.parse(readFileSync(url, 'utf8')), frame = atlas.frames.idle;
|
||||||
@@ -19,7 +23,7 @@ test('i veicoli del circolino restano fuori dal percorso e lasciano liberi gli a
|
|||||||
assert.deepEqual(Object.keys(atlas.frames), ['idle']);
|
assert.deepEqual(Object.keys(atlas.frames), ['idle']);
|
||||||
const k = 62 * g.scaleAt(person.at[1]) * person.size / atlas.height;
|
const k = 62 * g.scaleAt(person.at[1]) * person.size / atlas.height;
|
||||||
const width = frame.w * k;
|
const width = frame.w * k;
|
||||||
assert.ok(id === 'betino' ? width >= 70 && width <= 80 : width >= 150 && width <= 170);
|
assert.ok(width >= lo && width <= hi, `${id}: ${width}`);
|
||||||
// Tutto l'ingombro (non soltanto l'ancora) deve restare fuori dall'area calpestabile.
|
// Tutto l'ingombro (non soltanto l'ancora) deve restare fuori dall'area calpestabile.
|
||||||
for (let x = h.box[0]; x <= h.box[0] + h.box[2]; x++)
|
for (let x = h.box[0]; x <= h.box[0] + h.box[2]; x++)
|
||||||
for (let y = h.box[1]; y <= h.box[1] + h.box[3]; y++)
|
for (let y = h.box[1]; y <= h.box[1] + h.box[3]; y++)
|
||||||
@@ -34,14 +38,16 @@ test('i veicoli del circolino restano fuori dal percorso e lasciano liberi gli a
|
|||||||
assert.equal(g.personPose(person), 'idle');
|
assert.equal(g.personPose(person), 'idle');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('guarda e usa i veicoli: parla solo Andrea e non cambia il progresso', () => {
|
test('guarda e usa i veicoli: parla solo Andrea e non cambia il progresso', () => {
|
||||||
|
for (const [scene, id] of VEHICLES) {
|
||||||
const g = new Game(story);
|
const g = new Game(story);
|
||||||
g.go('campomigliaio_circolo');
|
g.go(scene);
|
||||||
settle(g);
|
settle(g);
|
||||||
const flags = { ...g.state.flags }, inventory = [...g.state.inventory];
|
const flags = { ...g.state.flags }, inventory = [...g.state.inventory];
|
||||||
for (const id of ['betino', 'ghiacciomobile']) {
|
{
|
||||||
const h = g.hotspot(id);
|
const h = g.hotspot(id);
|
||||||
assert.equal(g.hotspotAt([h.box[0] + h.box[2] / 2, h.box[1] + h.box[3] / 2]).id, id);
|
assert.equal(g.hotspotAt([h.box[0] + h.box[2] / 2, h.box[1] + h.box[3] / 2]).id, id);
|
||||||
for (const verb of ['look', 'use']) {
|
for (const verb of ['look', 'use']) {
|
||||||
@@ -51,7 +57,8 @@ test('guarda e usa i veicoli: parla solo Andrea e non cambia il progresso', () =
|
|||||||
assert.deepEqual([g.state.x, g.state.y], h.at);
|
assert.deepEqual([g.state.x, g.state.y], h.at);
|
||||||
assert.deepEqual(g.state.flags, flags);
|
assert.deepEqual(g.state.flags, flags);
|
||||||
assert.deepEqual(g.state.inventory, inventory);
|
assert.deepEqual(g.state.inventory, inventory);
|
||||||
assert.equal(g.state.scene, 'campomigliaio_circolo');
|
assert.equal(g.state.scene, scene);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"speaker": "Gitta Nikolina"
|
"speaker": "Gitta Nikolina"
|
||||||
},
|
},
|
||||||
"arcu": {
|
"arcu": {
|
||||||
"speaker": "Luis Moray"
|
"speaker": "Ferran Simen"
|
||||||
},
|
},
|
||||||
"tozzino": {
|
"tozzino": {
|
||||||
"speaker": "Xavier Hayasaka",
|
"speaker": "Xavier Hayasaka",
|
||||||
|
|||||||