Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a50847da2b | ||
|
|
1fc446d02a | ||
|
|
7076123fcf | ||
|
|
449c9511e1 | ||
|
|
0ec49c4fe7 | ||
|
|
b41cadd95b | ||
|
|
0bd3e2255f | ||
|
|
dc17a7f963 | ||
|
|
995ad09cca | ||
|
|
b9e9976d34 | ||
|
|
d37dafd58c | ||
|
|
4d1611c6eb | ||
|
|
7268585ed9 | ||
|
|
1b929f1333 | ||
|
|
3ba108cf88 | ||
|
|
70f4e641dc | ||
|
|
23aa9c4788 | ||
|
|
04f9a5cb12 |
@@ -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-vps deploy-logs clean distclean
|
build up down restart logs ps shell image-size deploy deploy-logs clean distclean
|
||||||
|
|
||||||
## ------------------------------------------------------------ sviluppo
|
## ------------------------------------------------------------ sviluppo
|
||||||
|
|
||||||
@@ -124,8 +124,7 @@ 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)
|
# DEPLOY_DIR = docker/leggenda-regolo (la cartella del compose sul server)
|
||||||
# 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,18 +150,12 @@ deploy: ## Pubblica l'ultimo commit: test, immagine costruita e provata qui, spe
|
|||||||
@# Sul server il prune toglie l'immagine vecchia; qui restano solo latest e quella appena pubblicata.
|
@# Sul server il prune toglie l'immagine vecchia; qui restano solo latest e quella appena pubblicata.
|
||||||
@docker image ls $(IMAGE) --format '{{.Tag}}' | grep -v -x -e latest -e $$(git rev-parse --short HEAD) | \
|
@docker image ls $(IMAGE) --format '{{.Tag}}' | grep -v -x -e latest -e $$(git rev-parse --short HEAD) | \
|
||||||
xargs -r -I{} docker image rm -f $(IMAGE):{} >/dev/null
|
xargs -r -I{} docker image rm -f $(IMAGE):{} >/dev/null
|
||||||
|
@# L'indirizzo pubblico deve servire la versione appena spedita.
|
||||||
|
@v=$$(grep -o "VERSION = '[^']*'" src/version.js); \
|
||||||
|
for i in 1 2 3 4 5; do curl -s $(DEPLOY_URL)/src/version.js | grep -qF "$$v" && break; sleep 2; done; \
|
||||||
|
curl -s $(DEPLOY_URL)/src/version.js | grep -qF "$$v" || { echo "$(DEPLOY_URL) non serve $$v"; exit 1; }
|
||||||
@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'
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"image": "andrea-birra.png",
|
||||||
|
"height": 120,
|
||||||
|
"frames": {
|
||||||
|
"walk_1": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 68,
|
||||||
|
"h": 114,
|
||||||
|
"ax": 40.2,
|
||||||
|
"ay": 114
|
||||||
|
},
|
||||||
|
"walk_2": {
|
||||||
|
"x": 70,
|
||||||
|
"y": 0,
|
||||||
|
"w": 41,
|
||||||
|
"h": 114,
|
||||||
|
"ax": 20.7,
|
||||||
|
"ay": 114
|
||||||
|
},
|
||||||
|
"walk_3": {
|
||||||
|
"x": 113,
|
||||||
|
"y": 0,
|
||||||
|
"w": 67,
|
||||||
|
"h": 114,
|
||||||
|
"ax": 38.4,
|
||||||
|
"ay": 114
|
||||||
|
},
|
||||||
|
"walk_4": {
|
||||||
|
"x": 182,
|
||||||
|
"y": 0,
|
||||||
|
"w": 46,
|
||||||
|
"h": 114,
|
||||||
|
"ax": 25.3,
|
||||||
|
"ay": 114
|
||||||
|
},
|
||||||
|
"cheer_1": {
|
||||||
|
"x": 230,
|
||||||
|
"y": 0,
|
||||||
|
"w": 51,
|
||||||
|
"h": 133,
|
||||||
|
"ax": 21.1,
|
||||||
|
"ay": 133
|
||||||
|
},
|
||||||
|
"cheer_2": {
|
||||||
|
"x": 283,
|
||||||
|
"y": 0,
|
||||||
|
"w": 55,
|
||||||
|
"h": 130,
|
||||||
|
"ax": 24.5,
|
||||||
|
"ay": 130
|
||||||
|
},
|
||||||
|
"cheer_3": {
|
||||||
|
"x": 340,
|
||||||
|
"y": 0,
|
||||||
|
"w": 62,
|
||||||
|
"h": 130,
|
||||||
|
"ax": 23.8,
|
||||||
|
"ay": 130
|
||||||
|
},
|
||||||
|
"talk": {
|
||||||
|
"x": 404,
|
||||||
|
"y": 0,
|
||||||
|
"w": 58,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 21.7,
|
||||||
|
"ay": 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 226 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 382 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 286 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+146
-22
@@ -691,9 +691,13 @@
|
|||||||
"file": "andrea-b830f3f8ca.mp3",
|
"file": "andrea-b830f3f8ca.mp3",
|
||||||
"sec": 6.87
|
"sec": 6.87
|
||||||
},
|
},
|
||||||
"andrea|Un ghiacciolo ci starebbe. Ma prima il palio, poi i ghiaccioli.": {
|
"andrea|Un ghiacciolo ci starebbe. Ma prima... il flipper.": {
|
||||||
"file": "andrea-2669d0fea1.mp3",
|
"file": "andrea-737d8c7f64.mp3",
|
||||||
"sec": 5.23
|
"sec": 4.56
|
||||||
|
},
|
||||||
|
"andrea|Dentro, in fondo, fra le figurine: il flipper. Con San Piero dipinto sulla tavola.": {
|
||||||
|
"file": "andrea-61a0d27bbb.mp3",
|
||||||
|
"sec": 6.67
|
||||||
},
|
},
|
||||||
"andrea|La farmacia, con la croce verde. Mia nonna ci veniva per le pasticche e per le chiacchiere. Più per le chiacchiere.": {
|
"andrea|La farmacia, con la croce verde. Mia nonna ci veniva per le pasticche e per le chiacchiere. Più per le chiacchiere.": {
|
||||||
"file": "andrea-1447e3515d.mp3",
|
"file": "andrea-1447e3515d.mp3",
|
||||||
@@ -1667,6 +1671,22 @@
|
|||||||
"file": "zio_mario-b3e99c241a.mp3",
|
"file": "zio_mario-b3e99c241a.mp3",
|
||||||
"sec": 2.68
|
"sec": 2.68
|
||||||
},
|
},
|
||||||
|
"andrea|E con la Ghiacciomobile nel muro, come si fa?": {
|
||||||
|
"file": "andrea-e964cb256d.mp3",
|
||||||
|
"sec": 2.69
|
||||||
|
},
|
||||||
|
"zio_mario|Siamo a cavallo al ciuco!": {
|
||||||
|
"file": "zio_mario-e74a044fb6.mp3",
|
||||||
|
"sec": 1.22
|
||||||
|
},
|
||||||
|
"andrea|Menomale tu ci sei te, Zio!": {
|
||||||
|
"file": "andrea-eea2bd2097.mp3",
|
||||||
|
"sec": 2.29
|
||||||
|
},
|
||||||
|
"zio_mario|Alla terza tu capisci subito, al volo!": {
|
||||||
|
"file": "zio_mario-4317f4b5f3.mp3",
|
||||||
|
"sec": 2.03
|
||||||
|
},
|
||||||
"andrea|Il parcheggio sotto casa. Col gesso ci si disegnava la pista delle macchinine, e si giocava finché non chiamavano per cena.": {
|
"andrea|Il parcheggio sotto casa. Col gesso ci si disegnava la pista delle macchinine, e si giocava finché non chiamavano per cena.": {
|
||||||
"file": "andrea-8670e6ce99.mp3",
|
"file": "andrea-8670e6ce99.mp3",
|
||||||
"sec": 7.6
|
"sec": 7.6
|
||||||
@@ -1915,6 +1935,22 @@
|
|||||||
"file": "zio_mario-5773e3350f.mp3",
|
"file": "zio_mario-5773e3350f.mp3",
|
||||||
"sec": 1.13
|
"sec": 1.13
|
||||||
},
|
},
|
||||||
|
"andrea|Sai, Zio? Son stato in California!": {
|
||||||
|
"file": "andrea-d8f8c38415.mp3",
|
||||||
|
"sec": 3.33
|
||||||
|
},
|
||||||
|
"zio_mario|E dov'è?": {
|
||||||
|
"file": "zio_mario-1cb1e2ca51.mp3",
|
||||||
|
"sec": 0.43
|
||||||
|
},
|
||||||
|
"andrea|In America!": {
|
||||||
|
"file": "andrea-8c8a45db81.mp3",
|
||||||
|
"sec": 0.85
|
||||||
|
},
|
||||||
|
"zio_mario|Ahhh... sopra Firenzola!": {
|
||||||
|
"file": "zio_mario-67279d2518.mp3",
|
||||||
|
"sec": 2.73
|
||||||
|
},
|
||||||
"andrea|Le case coi balconi pieni di vasi: qui ogni finestra è un giardino.": {
|
"andrea|Le case coi balconi pieni di vasi: qui ogni finestra è un giardino.": {
|
||||||
"file": "andrea-ec81875c47.mp3",
|
"file": "andrea-ec81875c47.mp3",
|
||||||
"sec": 5.22
|
"sec": 5.22
|
||||||
@@ -1939,9 +1975,9 @@
|
|||||||
"file": "andrea-2603f4db73.mp3",
|
"file": "andrea-2603f4db73.mp3",
|
||||||
"sec": 5.93
|
"sec": 5.93
|
||||||
},
|
},
|
||||||
"andrea|Si gira piano, sennò le bruciate si bruciano davvero.": {
|
"andrea|Posso girare io la manovella?": {
|
||||||
"file": "andrea-675499a046.mp3",
|
"file": "andrea-8febc84325.mp3",
|
||||||
"sec": 3.07
|
"sec": 1.75
|
||||||
},
|
},
|
||||||
"andrea|Una bruciata calda, presa al volo. Scotta! Ma è buonissima.": {
|
"andrea|Una bruciata calda, presa al volo. Scotta! Ma è buonissima.": {
|
||||||
"file": "andrea-ebba87b32c.mp3",
|
"file": "andrea-ebba87b32c.mp3",
|
||||||
@@ -1971,18 +2007,6 @@
|
|||||||
"file": "andrea-bfd09556ea.mp3",
|
"file": "andrea-bfd09556ea.mp3",
|
||||||
"sec": 5.63
|
"sec": 5.63
|
||||||
},
|
},
|
||||||
"andrea|Che Iddio ve ne renda merito!! A voi e agli alpini!": {
|
|
||||||
"file": "andrea-6933735b75.mp3",
|
|
||||||
"sec": 3.95
|
|
||||||
},
|
|
||||||
"volontari|E a te, figliolo. To', un cartoccio: son belle calde.": {
|
|
||||||
"file": "volontari-e8705fe901.mp3",
|
|
||||||
"sec": 5.14
|
|
||||||
},
|
|
||||||
"andrea|Scottano! Ma son buonissime.": {
|
|
||||||
"file": "andrea-ff81f048f5.mp3",
|
|
||||||
"sec": 2.79
|
|
||||||
},
|
|
||||||
"andrea|La fanfara degli alpini. Quando sfilavano per il paese suonando, s'affacciava tutto San Piero.": {
|
"andrea|La fanfara degli alpini. Quando sfilavano per il paese suonando, s'affacciava tutto San Piero.": {
|
||||||
"file": "andrea-a372a71849.mp3",
|
"file": "andrea-a372a71849.mp3",
|
||||||
"sec": 6.3
|
"sec": 6.3
|
||||||
@@ -2647,6 +2671,54 @@
|
|||||||
"file": "vecchietto-c8eae2e59e.mp3",
|
"file": "vecchietto-c8eae2e59e.mp3",
|
||||||
"sec": 4.88
|
"sec": 4.88
|
||||||
},
|
},
|
||||||
|
"andrea|Che Iddio ve ne renda merito!! A voi e agli alpini!": {
|
||||||
|
"file": "andrea-6933735b75.mp3",
|
||||||
|
"sec": 3.95
|
||||||
|
},
|
||||||
|
"andrea|Vi do una mano alla manovella?": {
|
||||||
|
"file": "andrea-1308ef4494.mp3",
|
||||||
|
"sec": 1.53
|
||||||
|
},
|
||||||
|
"andrea|Buon lavoro!": {
|
||||||
|
"file": "andrea-ade8043e82.mp3",
|
||||||
|
"sec": 0.71
|
||||||
|
},
|
||||||
|
"volontari|E a te, figliolo. To', un cartoccio: son belle calde.": {
|
||||||
|
"file": "volontari-e8705fe901.mp3",
|
||||||
|
"sec": 5.14
|
||||||
|
},
|
||||||
|
"andrea|Scottano! Ma son buonissime.": {
|
||||||
|
"file": "andrea-ff81f048f5.mp3",
|
||||||
|
"sec": 2.79
|
||||||
|
},
|
||||||
|
"volontari|Su, giovanotto, che noi s'ha le braccia stanche!": {
|
||||||
|
"file": "volontari-98d33dbfbd.mp3",
|
||||||
|
"sec": 2.43
|
||||||
|
},
|
||||||
|
"volontari|Si gira sempre uguale: piano piano sotto si bruciano, a furia volan nel fuoco. Quando son cotte, nel sacco.": {
|
||||||
|
"file": "volontari-7db485651b.mp3",
|
||||||
|
"sec": 8.77
|
||||||
|
},
|
||||||
|
"volontari|Come te la senti?": {
|
||||||
|
"file": "volontari-a104846cbf.mp3",
|
||||||
|
"sec": 0.88
|
||||||
|
},
|
||||||
|
"andrea|No, dopo.": {
|
||||||
|
"file": "andrea-78ad61a67a.mp3",
|
||||||
|
"sec": 1.22
|
||||||
|
},
|
||||||
|
"volontari|Alla bona: ti si guarda noi, e se si bruciano un si dice a nessuno.": {
|
||||||
|
"file": "volontari-5761a9776b.mp3",
|
||||||
|
"sec": 4.82
|
||||||
|
},
|
||||||
|
"volontari|Seria: la gente è in fila, e il cartoccio lo vole bello pieno.": {
|
||||||
|
"file": "volontari-dad04d4161.mp3",
|
||||||
|
"sec": 3.99
|
||||||
|
},
|
||||||
|
"volontari|A fassi male? Allora ci si mette anche il fuoco forte.": {
|
||||||
|
"file": "volontari-839aa118ab.mp3",
|
||||||
|
"sec": 4.1
|
||||||
|
},
|
||||||
"andrea|Barone! Che ci fai qui?": {
|
"andrea|Barone! Che ci fai qui?": {
|
||||||
"file": "andrea-83abae1419.mp3",
|
"file": "andrea-83abae1419.mp3",
|
||||||
"sec": 2.91
|
"sec": 2.91
|
||||||
@@ -2899,6 +2971,30 @@
|
|||||||
"file": "arcu-26a5fb0d0c.mp3",
|
"file": "arcu-26a5fb0d0c.mp3",
|
||||||
"sec": 3.17
|
"sec": 3.17
|
||||||
},
|
},
|
||||||
|
"andrea|Una partita alla bona, tanto per sentire la molla.": {
|
||||||
|
"file": "andrea-ad00ae1145.mp3",
|
||||||
|
"sec": 3.38
|
||||||
|
},
|
||||||
|
"andrea|Una partita seria.": {
|
||||||
|
"file": "andrea-3479aca550.mp3",
|
||||||
|
"sec": 1.16
|
||||||
|
},
|
||||||
|
"andrea|Un'altra volta.": {
|
||||||
|
"file": "andrea-1b517ab1ef.mp3",
|
||||||
|
"sec": 0.9
|
||||||
|
},
|
||||||
|
"andrea|Seimila punti e sono contento.": {
|
||||||
|
"file": "andrea-b6d9563c93.mp3",
|
||||||
|
"sec": 2.02
|
||||||
|
},
|
||||||
|
"andrea|Diecimila. Come quando ci lasciavo le cinquecento lire.": {
|
||||||
|
"file": "andrea-5811ae33d6.mp3",
|
||||||
|
"sec": 3.91
|
||||||
|
},
|
||||||
|
"andrea|Sedicimila. Il record del River, o quasi.": {
|
||||||
|
"file": "andrea-d420a5e136.mp3",
|
||||||
|
"sec": 4.38
|
||||||
|
},
|
||||||
"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
|
||||||
@@ -2987,10 +3083,6 @@
|
|||||||
"file": "andrea-9cc0991d39.mp3",
|
"file": "andrea-9cc0991d39.mp3",
|
||||||
"sec": 3.32
|
"sec": 3.32
|
||||||
},
|
},
|
||||||
"andrea|No, dopo.": {
|
|
||||||
"file": "andrea-78ad61a67a.mp3",
|
|
||||||
"sec": 1.22
|
|
||||||
},
|
|
||||||
"andrea|C'è posto per uno?": {
|
"andrea|C'è posto per uno?": {
|
||||||
"file": "andrea-f7481e5519.mp3",
|
"file": "andrea-f7481e5519.mp3",
|
||||||
"sec": 1.81
|
"sec": 1.81
|
||||||
@@ -3619,6 +3711,38 @@
|
|||||||
"file": "andrea-356a1afe8d.mp3",
|
"file": "andrea-356a1afe8d.mp3",
|
||||||
"sec": 5.91
|
"sec": 5.91
|
||||||
},
|
},
|
||||||
|
"andrea|Traguardo! Se c'era la classifica, oggi ci mettevo le mie iniziali.": {
|
||||||
|
"file": "andrea-6fd4fcd345.mp3",
|
||||||
|
"sec": 4.64
|
||||||
|
},
|
||||||
|
"andrea|Tre palline, e tutte nella Sieve. Il flipper del River non perdona.": {
|
||||||
|
"file": "andrea-5c306f1589.mp3",
|
||||||
|
"sec": 5.57
|
||||||
|
},
|
||||||
|
"andrea|Lascio la pallina lì. Tanto il River non chiude.": {
|
||||||
|
"file": "andrea-dcbb849b12.mp3",
|
||||||
|
"sec": 4.02
|
||||||
|
},
|
||||||
|
"andrea|Bruciate coi fiocchi! Mezza Smarronata le mangia grazie a me.": {
|
||||||
|
"file": "andrea-145f20bff9.mp3",
|
||||||
|
"sec": 5.11
|
||||||
|
},
|
||||||
|
"volontari|Bravo, figliolo! Se ti va, l'anno prossimo la manovella è tua.": {
|
||||||
|
"file": "volontari-067aa2d701.mp3",
|
||||||
|
"sec": 4.42
|
||||||
|
},
|
||||||
|
"andrea|Un po' crude, un po' carbone... Le mangio io, va'.": {
|
||||||
|
"file": "andrea-6f3339acb0.mp3",
|
||||||
|
"sec": 4.06
|
||||||
|
},
|
||||||
|
"volontari|Ci vuole il polso, figliolo. Il polso e la pazienza.": {
|
||||||
|
"file": "volontari-3bb4fe1dc0.mp3",
|
||||||
|
"sec": 4.28
|
||||||
|
},
|
||||||
|
"andrea|Vi rendo la manovella. Mi fumano anche gli occhi.": {
|
||||||
|
"file": "andrea-367ddde0fe.mp3",
|
||||||
|
"sec": 3.61
|
||||||
|
},
|
||||||
"andrea|Il manifesto dell'Ingorgo Sonoro dell'anno scorso. Da piazza fino a Cambray Digny, una console in ogni angolo.": {
|
"andrea|Il manifesto dell'Ingorgo Sonoro dell'anno scorso. Da piazza fino a Cambray Digny, una console in ogni angolo.": {
|
||||||
"file": "andrea-0c26e34815.mp3",
|
"file": "andrea-0c26e34815.mp3",
|
||||||
"sec": 7.53
|
"sec": 7.53
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,5 @@
|
|||||||
# Deploy dietro un reverse proxy (make deploy): l'immagine arriva già costruita dal PC (docker load), niente porte esposte, il container sta sulla rete esterna nginx-net,
|
# Deploy dietro un reverse proxy (make deploy): l'immagine arriva già costruita dal PC (docker load), niente porte esposte, il container sta sulla rete esterna nginx-net,
|
||||||
# dove l'NPM interno di home-server lo pubblica col suo nome (50037.manzolo.it → leggenda-regolo:80).
|
# dove Nginx Proxy Manager del VPS lo pubblica col suo nome (50037.manzolo.it → leggenda-regolo:80).
|
||||||
services:
|
services:
|
||||||
leggenda-regolo:
|
leggenda-regolo:
|
||||||
image: leggenda-regolo:latest
|
image: leggenda-regolo:latest
|
||||||
|
|||||||
+1
-1
@@ -151,7 +151,7 @@ si segna qui con la data e il punto di `src/story/story.js` dove sta; le persone
|
|||||||
Al bar e al circolo:
|
Al bar e al circolo:
|
||||||
- *Calcio balilla al River*: due stecche sole (difesa e attacco), vista dall'alto come il biliardo, che ha già
|
- *Calcio balilla al River*: due stecche sole (difesa e attacco), vista dall'alto come il biliardo, che ha già
|
||||||
la fisica delle sponde. Il gioco da bar più sampierino che c'è; il River «coi flipper e le figurine» lo cita già.
|
la fisica delle sponde. Il gioco da bar più sampierino che c'è; il River «coi flipper e le figurine» lo cita già.
|
||||||
- *Flipper del River*: più lavoro, ma la fisica delle biglie c'è già.
|
- *Flipper del River*: fatto il 31 ottobre (vedi STATO.md).
|
||||||
- *Freccette al Circolo*: la mira vaga e si ferma al clic, tre freccette, si chiude il 301. Poco codice.
|
- *Freccette al Circolo*: la mira vaga e si ferma al clic, tre freccette, si chiude il 301. Poco codice.
|
||||||
- *Morra*: niente fisica, tutto dialogo e tempismo; molto toscana, le grida dei numeri in voce XTTS.
|
- *Morra*: niente fisica, tutto dialogo e tempismo; molto toscana, le grida dei numeri in voce XTTS.
|
||||||
Sport:
|
Sport:
|
||||||
|
|||||||
+37
-5
@@ -41,7 +41,30 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
|||||||
merito!! A voi e agli alpini!»), la fanfara degli alpini, gli alpini della polenta e del vino, la gente coi cartocci
|
merito!! A voi e agli alpini!»), la fanfara degli alpini, gli alpini della polenta e del vino, la gente coi cartocci
|
||||||
(anche nel tendone), le coppie del liscio che girano (`flourish` continuo).
|
(anche nel tendone), le coppie del liscio che girano (`flourish` continuo).
|
||||||
La musica di Andrea (Suno): il liscio fuori, più forte vicino alle coppie (`stages`), la mazurca nel tendone.
|
La musica di Andrea (Suno): il liscio fuori, più forte vicino alle coppie (`stages`), la mazurca nel tendone.
|
||||||
[DA ANDREA] le battute; un minigioco delle bruciate (girare il cilindro sul fuoco).
|
[DA ANDREA] le battute.
|
||||||
|
- **Il minigioco delle bruciate (31 ottobre)**: dai volontari della Misericordia («Vi do una mano alla manovella?»), o
|
||||||
|
usando il cilindro, si sceglie il livello e si gira la manovella a mano (`src/engine/bruciate.js`, disegno in
|
||||||
|
`bruciate-render.js`, sfondo di Codex `assets/closeups/smarronata-bruciatore.png`). Il gesto: tenendo premuto si
|
||||||
|
fanno giri, grandi o piccoli e dovunque (conta quanto gira la direzione del dito); Spazio, frecce: una spinta a
|
||||||
|
colpo. Troppo piano le castagne di sotto si carbonizzano e quelle di sopra restano crude, troppo forte volano nel
|
||||||
|
fuoco; quando son cotte, «Rovescia nel sacco». Contano le buone su 18 (alla bona 10, seria 15, a fassi male 16);
|
||||||
|
vincendo, il flag `bruciate_fatte`. Test in `bruciate.test.mjs`, prova `npm run browser:bruciate`.
|
||||||
|
[DA ANDREA] le battute dei volontari; un premio (un cartoccio da portare via?).
|
||||||
|
- **Il flipper del River (31 ottobre)**: «Usa» o «Apri» il River in Via Provinciale, si sceglie il livello e si gioca
|
||||||
|
(`src/engine/flipper.js`, disegno `flipper-render.js`, tavola di Codex `assets/closeups/river-flipper.png` con San
|
||||||
|
Piero dipinto: Fortezza, Pieve, torre, la Sieve che scende fra i flipper, il ponte, il Regolo con la corona, le
|
||||||
|
bandiere dei rioni). Fisica a passo fisso: molla, arco, quattro corsie dei rioni (tutte e quattro = PALIO), tre
|
||||||
|
funghi, bersagli a caduta della Pieve e della torre, il Regolo, sponde elastiche, due flipper, uscite laterali,
|
||||||
|
salvataggio della pallina una volta sola. Comandi: molla tenendo premuto (o cursore + Lancia, o Spazio), flipper
|
||||||
|
con le due metà della scena, i pulsanti «← Flipper»/«Flipper →» tenuti premuti o le frecce. Tre palline; traguardo
|
||||||
|
6000/12000/25000. Test `flipper.test.mjs` (col giocatore automatico), prova `npm run browser:flipper`.
|
||||||
|
Rivisto con la revisione critica di Codex (31 ottobre, chiesta da Andrea): punti solo a un urto nuovo (la pallina
|
||||||
|
appoggiata sul Regolo faceva 240.000 punti al secondo), sottopassi contro gli attraversamenti del flipper, niente
|
||||||
|
cancelletto-ripiano e molla che arriva sempre in campo, salvataggio col tempo della simulazione (e in vista sul
|
||||||
|
quadro), corsie che contano attraversando la riga, traguardo acquisito appena raggiunto (poi si continua per il
|
||||||
|
record), annullare non lancia, due dita sulla scena e tasto destro del mouse, pulsanti dei flipper sempre attivi.
|
||||||
|
[DA ANDREA] le battute; cosa si vince (`flipper_vinto` per ora non serve a niente). Da fare (Andrea): un paio di
|
||||||
|
rampe («ponti»), più punti, il tabellone luminoso coi punti che corrono, un ingresso più intuitivo del River.
|
||||||
- **Dedica allo zio Mario (31 ottobre)**: nei Ringraziamenti, sotto quella al Lele e più piccola: «Al mio caro zio
|
- **Dedica allo zio Mario (31 ottobre)**: nei Ringraziamenti, sotto quella al Lele e più piccola: «Al mio caro zio
|
||||||
Mario, che mi ha fatto vivere tante avventure più belle della fantasia.»
|
Mario, che mi ha fatto vivere tante avventure più belle della fantasia.»
|
||||||
- **Il Taglia (31 ottobre)**: tolte le battute sulla sua altezza (Baldoria: non gli fa piacere).
|
- **Il Taglia (31 ottobre)**: tolte le battute sulla sua altezza (Baldoria: non gli fa piacere).
|
||||||
@@ -81,6 +104,13 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
|||||||
pieni zeppi di gente fino in primo piano (Andrea); la prima versione, con la piazza vuota davanti, in
|
pieni zeppi di gente fino in primo piano (Andrea); la prima versione, con la piazza vuota davanti, in
|
||||||
`assets/raw/ingorgo-v1/`. La voce legge «console» come «consòl» (`tools/pronuncia.json`).
|
`assets/raw/ingorgo-v1/`. La voce legge «console» come «consòl» (`tools/pronuncia.json`).
|
||||||
[DA ANDREA] i generi veri per piazza, le battute, i bar a tema; da fare: spillare la birra (minigioco) e bere.
|
[DA ANDREA] i generi veri per piazza, le battute, i bar a tema; da fare: spillare la birra (minigioco) e bere.
|
||||||
|
Il 31 ottobre: la folla davanti ad Andrea (dalla vita in giù, `crowdFront` in render.js), Andrea con la birra alla
|
||||||
|
festa (foglio di Codex `andrea-birra`: cammina col bicchiere, fermo lo alza a tempo), sotto una console si sente solo
|
||||||
|
quella (`mix`), in cima a Via dell'Antica Posta si arriva sotto l'underground. Via Calimara entra nella festa (sfondo di
|
||||||
|
Codex, console nel giardino con la palla a specchi) con la musica anni '90 di Andrea (Suno, `ingorgo-anni90.mp3`);
|
||||||
|
da lontano la house della Piazza e il latino del Comune.
|
||||||
|
Anche l'inizio di Via Cipriani (sfondo di Codex, console sul campo da basket, laser rossi e strobo, folla che poga a
|
||||||
|
180 battiti) con la hardcore techno di Andrea (Suno, `ingorgo-hardcore.mp3`).
|
||||||
- **Baldoria al campo sportivo (30 ottobre)**: Yuri Baldi, dei Ringraziamenti, al Campo principale con la maglia
|
- **Baldoria al campo sportivo (30 ottobre)**: Yuri Baldi, dei Ringraziamenti, al Campo principale con la maglia
|
||||||
blucerchiata e un bandierone della Sampdoria grande come lui, che ogni tanto sventola (`flourish` delle persone).
|
blucerchiata e un bandierone della Sampdoria grande come lui, che ogni tanto sventola (`flourish` delle persone).
|
||||||
Sprite di Codex (`assets/raw/baldoria-sheet.png`; `make character … FIX=wave=1.2,…` perché Codex ha disegnato più
|
Sprite di Codex (`assets/raw/baldoria-sheet.png`; `make character … FIX=wave=1.2,…` perché Codex ha disegnato più
|
||||||
@@ -220,8 +250,10 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
|||||||
|
|
||||||
**Si gioca** in Docker su http://localhost:8080 (`make up`; Manzolo Rush è stato fermato per liberare la porta) e,
|
**Si gioca** in Docker su http://localhost:8080 (`make up`; Manzolo Rush è stato fermato per liberare la porta) e,
|
||||||
**online dal 2026-10-23**, su https://50037.manzolo.it (50037 è il CAP di San Piero), finché non va su GitHub Pages.
|
**online dal 2026-10-23**, su https://50037.manzolo.it (50037 è il CAP di San Piero), finché non va su GitHub Pages.
|
||||||
Si aggiorna con `make deploy`: pubblica l'ultimo commit, immagine costruita e provata sul PC e spedita al container
|
Dal 2026-10-31 il sito pubblico è sul **VPS Contabo** (CNAME `50037` → `vps.manzolo.it`, NPM del VPS col certificato
|
||||||
`leggenda-regolo` di home-server (NPM centrale → NPM interno di home-server → container).
|
Let's Encrypt → container `leggenda-regolo`); la copia su home-server è stata tolta. Si aggiorna con `make deploy`:
|
||||||
|
test, immagine costruita e provata sul PC, spedita al VPS, e il controllo che https://50037.manzolo.it serva la
|
||||||
|
versione nuova.
|
||||||
|
|
||||||
- **Motore** (`src/engine/`): camminata con percorsi su aree concave, profondità, sette verbi + clic intelligente,
|
- **Motore** (`src/engine/`): camminata con percorsi su aree concave, profondità, sette verbi + clic intelligente,
|
||||||
clic destro / pressione lunga = guarda, inventario con "usa… con" e "dai… a", dialoghi a scelte (anche una
|
clic destro / pressione lunga = guarda, inventario con "usa… con" e "dai… a", dialoghi a scelte (anche una
|
||||||
@@ -373,8 +405,8 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso
|
|||||||
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
|
- 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.
|
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
|
- Il gioco pubblico sta sul VPS Contabo (dal 31 ottobre: regola in NPM del VPS, DNS spostato su Aruba, certificato
|
||||||
nel proxy del VPS la regola per 50037.manzolo.it; poi si sposta il DNS su Aruba.
|
Let's Encrypt); la copia su home-server è stata tolta (Andrea: «la tengo fuori e basta»).
|
||||||
- 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
|
||||||
|
|||||||
+21
@@ -295,6 +295,27 @@
|
|||||||
sinistra per la forza. I salti: la lancetta dell'equilibrio attraversa la barra; premi (o Spazio, o Salta) quando è
|
sinistra per la forza. I salti: la lancetta dell'equilibrio attraversa la barra; premi (o Spazio, o Salta) quando è
|
||||||
nel verde. Troppo presto metti giù l'altro piede, troppo tardi pesti la riga.</p>
|
nel verde. Troppo presto metti giù l'altro piede, troppo tardi pesti la riga.</p>
|
||||||
</details>
|
</details>
|
||||||
|
<details id="bruciate-help" hidden><summary>Come si gioca</summary>
|
||||||
|
<p>Le bruciate della Smarronata, coi volontari della Misericordia: le castagne sono nel cilindro a rete sopra il
|
||||||
|
fuoco e si gira la manovella. Tieni premuto e fai dei giri, grandi o piccoli, dove vuoi, col mouse o col dito:
|
||||||
|
il cilindro gira con te. Oppure Spazio o le frecce: una spinta a ogni colpo.</p>
|
||||||
|
<p>In alto, GIRO: tienilo nel verde. Troppo piano le castagne non si rimescolano, quelle di sotto si carbonizzano e
|
||||||
|
quelle di sopra restano crude; troppo forte volano fuori, nel fuoco. COTTURA: quando è nel verde, Rovescia nel
|
||||||
|
sacco (o Invio). Contano le bruciate buone: alla bona ne bastano 10 su 18, seria 15, a fassi male 16.</p>
|
||||||
|
</details>
|
||||||
|
<details id="flipper-help" hidden><summary>Come si gioca</summary>
|
||||||
|
<p>Il flipper del River, con San Piero dipinto sulla tavola. La pallina parte dalla corsia di destra: tieni premuto
|
||||||
|
per caricare la molla e lascia per lanciare (o regola la forza e premi Lancia, o Spazio). Fa il giro dell'arco e
|
||||||
|
scende per una delle quattro corsie dei rioni: accendile tutte e quattro per il PALIO (3000 punti). I tre funghi
|
||||||
|
sono i bastioni della Fortezza (100 a botta); i bersagli bianchi a sinistra sono la Pieve e quelli gialli a
|
||||||
|
destra la torre dell'orologio (500 l'uno, 2000 la fila intera); il bersaglio verde con la corona, in alto a
|
||||||
|
destra, è il Regolo (1000).</p>
|
||||||
|
<p>I flipper: tieni premuto sulla metà sinistra o destra della scena (anche con due dita insieme, o col tasto
|
||||||
|
destro del mouse per quello di destra), o i pulsanti «← Flipper» e «Flipper →», o le frecce sinistra e destra.
|
||||||
|
Tre palline; la prima volta, appena lanciata, se la perdi subito torna (alla bona per 8 secondi, seria per 4, a
|
||||||
|
fassi male per 1). Si vince arrivando al traguardo, anche se poi si esce: alla bona 6000 punti, seria 10000, a
|
||||||
|
fassi male 16000; raggiunto il traguardo, si continua per il record.</p>
|
||||||
|
</details>
|
||||||
<details id="cerbottana-help" hidden><summary>Come si gioca</summary>
|
<details id="cerbottana-help" hidden><summary>Come si gioca</summary>
|
||||||
<p>La cerbottana, con lo stucco verde di Pierin di Frilli: dodici pallini contro il muro della piazza. Il bersaglio
|
<p>La cerbottana, con lo stucco verde di Pierin di Frilli: dodici pallini contro il muro della piazza. Il bersaglio
|
||||||
di gesso vale 10, 5 o 2 punti secondo il cerchio, ogni lattina sul muretto 3, il barattolo appeso che dondola (Seria
|
di gesso vale 10, 5 o 2 punti secondo il cerchio, ogni lattina sul muretto 3, il barattolo appeso che dondola (Seria
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "leggenda-del-regolo",
|
"name": "leggenda-del-regolo",
|
||||||
"version": "0.1.63",
|
"version": "0.1.66",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -12,6 +12,8 @@
|
|||||||
"browser:cerbottana": "node tests/cerbottana.browser.mjs",
|
"browser:cerbottana": "node tests/cerbottana.browser.mjs",
|
||||||
"browser:condor": "node tests/condor.browser.mjs",
|
"browser:condor": "node tests/condor.browser.mjs",
|
||||||
"browser:campana": "node tests/campana.browser.mjs",
|
"browser:campana": "node tests/campana.browser.mjs",
|
||||||
|
"browser:bruciate": "node tests/bruciate.browser.mjs",
|
||||||
|
"browser:flipper": "node tests/flipper.browser.mjs",
|
||||||
"browser:ingorgo": "node tests/ingorgo.browser.mjs",
|
"browser:ingorgo": "node tests/ingorgo.browser.mjs",
|
||||||
"browser:tressette": "node tests/tressette.browser.mjs",
|
"browser:tressette": "node tests/tressette.browser.mjs",
|
||||||
"browser:scopa": "node tests/scopa.browser.mjs",
|
"browser:scopa": "node tests/scopa.browser.mjs",
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
// Le bruciate in primo piano (bruciate.js): il cilindro a rete sopra il fuoco, visto di fronte, con le castagne dentro
|
||||||
|
// che ricascano, la manovella a destra, le fiamme che lo lambiscono; in basso il giro della manovella e la cottura.
|
||||||
|
// Niente filtri e niente passate trasparenti a tutto schermo (vedi risveglio.js): solo forme piccole.
|
||||||
|
import { DRUM, CRANK, BAR, N, COOKED, BURNT, POUR, MAX_SPEED, inside, doneness } from './bruciate.js';
|
||||||
|
import { text, panel } from './bocce-render.js';
|
||||||
|
import { ambientHash as hash } from './ambient.js';
|
||||||
|
|
||||||
|
const SACK = { x: 604, y: 268 }, FIRE = 252, RODS = 18, RINGS = 9;
|
||||||
|
const lerp = (a, b, k) => a + (b - a) * k;
|
||||||
|
const mix = (p, q, k) => `rgb(${p.map((v, i) => Math.round(lerp(v, q[i], k))).join(',')})`;
|
||||||
|
// Il colore della buccia: lucida e rossiccia da cruda, scura da cotta, nera da carbone.
|
||||||
|
const RAW = [168, 96, 50], DONE = [118, 64, 30], COAL = [26, 20, 18];
|
||||||
|
const shell = cook => cook < COOKED ? mix(RAW, DONE, Math.max(0, cook) / COOKED) : mix(DONE, COAL, Math.min(1, (cook - COOKED) / (BURNT - COOKED)));
|
||||||
|
|
||||||
|
// Dove sta una castagna sullo schermo, e quanto è davanti (per l'ordine e la luce).
|
||||||
|
function place(k) {
|
||||||
|
return { x: lerp(DRUM.x0 + 8, DRUM.x1 - 8, k.x), y: DRUM.y + Math.sin(k.a) * DRUM.r * k.r, z: Math.cos(k.a) * k.r };
|
||||||
|
}
|
||||||
|
function chestnut(ctx, x, y, cook, z = 0) {
|
||||||
|
// Il bordino, perché si stacchino dal fondo del cilindro (chiaro su quelle carbone).
|
||||||
|
ctx.fillStyle = cook > BURNT ? '#6a5a4e' : '#1e120a';
|
||||||
|
ctx.beginPath(); ctx.ellipse(Math.round(x), Math.round(y), 7, 6, 0, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.fillStyle = shell(cook);
|
||||||
|
ctx.beginPath(); ctx.ellipse(Math.round(x), Math.round(y), 6, 5, 0, 0, Math.PI * 2); ctx.fill();
|
||||||
|
// La punta chiara e, da cotte, l'incisione aperta col giallo della castagna.
|
||||||
|
ctx.fillStyle = cook > BURNT ? '#2a2420' : '#d9b48a';
|
||||||
|
ctx.fillRect(Math.round(x) - 1, Math.round(y) - 4, 2, 1);
|
||||||
|
if (cook >= COOKED * 0.8 && cook <= BURNT) { ctx.fillStyle = '#e8c070'; ctx.fillRect(Math.round(x) - 2, Math.round(y) - 1, 4, 1); }
|
||||||
|
// Il riflesso del fuoco, più forte su quelle davanti.
|
||||||
|
if (cook < BURNT) { ctx.fillStyle = `rgba(255,190,120,${0.25 + 0.3 * Math.max(0, z)})`; ctx.fillRect(Math.round(x) + 1, Math.round(y) - 2, 2, 1); }
|
||||||
|
}
|
||||||
|
// Le fiamme che lambiscono il cilindro: lingue basse e storte, ognuna col suo passo, che ogni tanto si allungano.
|
||||||
|
function flames(ctx, time, front) {
|
||||||
|
for (let i = 0; i < 17; i++) {
|
||||||
|
if ((i % 3 === 0) !== front) continue;
|
||||||
|
const r = hash(i * 7.3), x = DRUM.x0 + 6 + i * (DRUM.x1 - DRUM.x0 - 12) / 16 + 6 * (hash(i * 1.9) - 0.5);
|
||||||
|
const lick = Math.max(0, Math.sin(time * (1.3 + r) + i * 2.1)) ** 4, h = front ? 5 + 6 * r + 2 * Math.sin(time * (6 + r * 5) + i) + 7 * lick : 18 + 16 * r + 7 * Math.sin(time * (6 + r * 5) + i) + 22 * lick;
|
||||||
|
const w = 5 + 5 * hash(i * 3.1), sway = 2.5 * Math.sin(time * (3 + r) + i * 1.7);
|
||||||
|
for (const [k, color] of [[1, '#b8301a'], [0.7, '#ea7a1a'], [0.4, '#ffd04a']]) {
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x - w * k, FIRE);
|
||||||
|
ctx.quadraticCurveTo(x - w * k * 0.2 + sway, FIRE - h * k * 0.6, x + sway * 2, FIRE - h * k);
|
||||||
|
ctx.quadraticCurveTo(x + w * k * 0.5 + sway, FIRE - h * k * 0.45, x + w * k, FIRE);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Il cilindro appeso: la sbarra sulle due forcelle e i due ferri che lo reggono per l'asse.
|
||||||
|
function hanger(ctx) {
|
||||||
|
ctx.fillStyle = '#2a2420'; ctx.fillRect(150, BAR - 3, 336, 6);
|
||||||
|
ctx.fillStyle = '#6a5a4a'; ctx.fillRect(150, BAR - 3, 336, 1);
|
||||||
|
for (const x of [DRUM.x0 - 12, DRUM.x1 + 12]) {
|
||||||
|
ctx.fillStyle = '#2a2420'; ctx.fillRect(x - 2, BAR, 4, DRUM.y - BAR + 4);
|
||||||
|
ctx.fillStyle = '#5a4a3e'; ctx.fillRect(x - 2, BAR, 1, DRUM.y - BAR + 4);
|
||||||
|
ctx.fillStyle = '#2a2420'; ctx.beginPath(); ctx.arc(x, DRUM.y, 5, 0, Math.PI * 2); ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// La rete: gli anelli e i tondini che girano col cilindro (quelli davanti più chiari).
|
||||||
|
function mesh(ctx, c, front) {
|
||||||
|
const { x0, x1, y, r } = DRUM;
|
||||||
|
for (let j = 0; j < RODS; j++) {
|
||||||
|
const a = c.drum + j * Math.PI * 2 / RODS, z = Math.cos(a);
|
||||||
|
if (front !== z > 0) continue;
|
||||||
|
ctx.fillStyle = front ? '#9a9088' : '#3a302a';
|
||||||
|
ctx.fillRect(x0, Math.round(y + Math.sin(a) * r), x1 - x0, 1);
|
||||||
|
}
|
||||||
|
ctx.strokeStyle = front ? '#8a8078' : '#3a302a'; ctx.lineWidth = 1;
|
||||||
|
for (let i = 0; i <= RINGS; i++) {
|
||||||
|
const x = Math.round(lerp(x0, x1, i / RINGS)) + 0.5, end = i === 0 || i === RINGS;
|
||||||
|
ctx.beginPath(); ctx.ellipse(x, y, end ? 9 : 4, r, 0, front ? -Math.PI / 2 : Math.PI / 2, front ? Math.PI / 2 : Math.PI * 1.5); ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function crank(ctx, c, time) {
|
||||||
|
const { x, y, rx, ry } = CRANK, a = c.drum;
|
||||||
|
// L'asse, da una forcella all'altra e fino alla manovella.
|
||||||
|
ctx.fillStyle = '#2a2420'; ctx.fillRect(DRUM.x0 - 16, y - 2, x - DRUM.x0 + 18, 4);
|
||||||
|
ctx.fillStyle = '#6a625a'; ctx.fillRect(DRUM.x0 - 16, y - 2, x - DRUM.x0 + 18, 1);
|
||||||
|
// Il giro da fare: un'ellisse tratteggiata che pulsa, finché il cilindro è fermo.
|
||||||
|
if (c.step === 'turn' && Math.abs(c.speed) < 0.5) {
|
||||||
|
ctx.strokeStyle = `rgba(255,224,140,${0.5 + 0.4 * Math.sin(time * 5)})`; ctx.setLineDash([3, 3]);
|
||||||
|
ctx.beginPath(); ctx.ellipse(x, y, rx + 6, ry + 6, 0, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]);
|
||||||
|
text(ctx, 'GIRA!', x, y + ry + 18, 6, '#ffe08c', 'center');
|
||||||
|
}
|
||||||
|
const hx = x + Math.cos(a) * rx, hy = y + Math.sin(a) * ry;
|
||||||
|
ctx.strokeStyle = '#5a5450'; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(hx, hy); ctx.stroke();
|
||||||
|
ctx.strokeStyle = '#b0a698'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, y - 1); ctx.lineTo(hx, hy - 1); ctx.stroke();
|
||||||
|
ctx.fillStyle = '#4a4440'; ctx.beginPath(); ctx.arc(x, y, 4, 0, Math.PI * 2); ctx.fill();
|
||||||
|
// L'impugnatura di legno, consumata dalle mani.
|
||||||
|
ctx.fillStyle = '#3a2414'; ctx.fillRect(Math.round(hx) - 4, Math.round(hy) - 8, 9, 16);
|
||||||
|
ctx.fillStyle = '#a0703c'; ctx.fillRect(Math.round(hx) - 3, Math.round(hy) - 7, 7, 14);
|
||||||
|
ctx.fillStyle = '#d6a062'; ctx.fillRect(Math.round(hx) - 2, Math.round(hy) - 6, 2, 12);
|
||||||
|
}
|
||||||
|
// In basso a sinistra (in alto ci sono i cursori del volume): il giro della manovella (rosso troppo piano, verde giusto, rosso troppo forte) e la cottura.
|
||||||
|
function gauges(ctx, c) {
|
||||||
|
panel(ctx, 10, 296, 196, 54);
|
||||||
|
const bar = (y, label, k, zones, mark) => {
|
||||||
|
text(ctx, label, 18, y, 6, '#efd49e');
|
||||||
|
const x0 = 82, w = 116;
|
||||||
|
for (const [a, b, color] of zones) { ctx.fillStyle = color; ctx.fillRect(x0 + Math.round(a * w), y - 3, Math.max(1, Math.round((b - a) * w)), 7); }
|
||||||
|
ctx.fillStyle = mark; ctx.fillRect(x0 + Math.round(Math.max(0, Math.min(1, k)) * w) - 1, y - 6, 3, 13);
|
||||||
|
};
|
||||||
|
const s = c.skill, lo = s.lo / MAX_SPEED, hi = s.hi / MAX_SPEED;
|
||||||
|
bar(312, 'GIRO', Math.abs(c.speed) / MAX_SPEED, [[0, lo, '#8a3a2a'], [lo, hi, '#4f8a3a'], [hi, 1, '#b3402a']], '#fff4c8');
|
||||||
|
const top = BURNT * 1.15;
|
||||||
|
bar(334, 'COTTURA', doneness(c) / top, [[0, COOKED / top, '#b07a4a'], [COOKED / top, BURNT / top, '#6a8a3a'], [BURNT / top, 1, '#2a2420']], '#fff4c8');
|
||||||
|
}
|
||||||
|
function result(ctx, c) {
|
||||||
|
const r = c.result;
|
||||||
|
panel(ctx, 170, 120, 300, 104);
|
||||||
|
text(ctx, r.winner === 'player' ? 'BRUCIATE COI FIOCCHI!' : 'BRUCIATE COSÌ COSÌ', 320, 140, 8, '#efd49e', 'center');
|
||||||
|
const rows = [['Buone', r.good, '#9fd27a'], ['Crude', r.raw, '#e0b07a'], ['Carbonizzate', r.burnt, '#c0b0a0'], ['Nel fuoco', r.spilled, '#f08a5a']];
|
||||||
|
rows.forEach(([label, n, color], i) => { text(ctx, label, 200, 162 + i * 13, 6, color); text(ctx, `${n}`, 440, 162 + i * 13, 6, color, 'right'); });
|
||||||
|
text(ctx, `Ne servivano ${r.goal} su ${N}`, 320, 216, 5, '#d7ccb4', 'center');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawBruciate(ctx, c, def, bg) {
|
||||||
|
const time = c.visual.time;
|
||||||
|
ctx.save();
|
||||||
|
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
|
||||||
|
else { ctx.fillStyle = '#6a5a40'; ctx.fillRect(0, 0, 640, 360); }
|
||||||
|
hanger(ctx);
|
||||||
|
// Il fondo del cilindro, scuro, col bagliore della brace sotto; poi le fiamme dietro e la rete di dietro.
|
||||||
|
ctx.fillStyle = 'rgba(74,46,30,0.72)'; ctx.fillRect(DRUM.x0, DRUM.y - DRUM.r, DRUM.x1 - DRUM.x0, DRUM.r * 2);
|
||||||
|
ctx.fillStyle = 'rgba(240,120,40,0.18)'; ctx.fillRect(DRUM.x0, DRUM.y + DRUM.r * 0.3, DRUM.x1 - DRUM.x0, DRUM.r * 0.7);
|
||||||
|
flames(ctx, time, false);
|
||||||
|
mesh(ctx, c, false);
|
||||||
|
// Le castagne: da dietro a davanti. Rovesciandole, scivolano tutte verso il sacco.
|
||||||
|
const pour = c.step === 'pour' ? Math.min(1, c.t / POUR) : c.step === 'result' ? 1 : 0;
|
||||||
|
const ks = (pour ? c.chestnuts.filter(k => !k.out) : inside(c)).map(k => ({ k, ...place(k) })).sort((p, q) => p.z - q.z);
|
||||||
|
for (const p of ks) {
|
||||||
|
if (pour) {
|
||||||
|
const d = Math.min(1, Math.max(0, pour * 1.6 - p.k.x * 0.6)), x = lerp(p.x, SACK.x, d), y = lerp(p.y, SACK.y, d) - Math.sin(d * Math.PI) * 40;
|
||||||
|
if (d < 1) chestnut(ctx, x, y, p.k.cook, 1);
|
||||||
|
} else chestnut(ctx, p.x, p.y, p.k.cook, p.z);
|
||||||
|
}
|
||||||
|
mesh(ctx, c, true);
|
||||||
|
flames(ctx, time, true);
|
||||||
|
crank(ctx, c, time);
|
||||||
|
// Quelle che volano nel fuoco, e lo sbuffo quando ci cascano.
|
||||||
|
for (const f of c.flying) {
|
||||||
|
const k = Math.min(1, f.t / 0.6), x = f.x + f.dir * 60 * k, y = lerp(f.y, FIRE - 6, k) - Math.sin(k * Math.PI) * 50;
|
||||||
|
if (f.t < 0.6) chestnut(ctx, x, y, 0.5);
|
||||||
|
else for (let i = 0; i < 4; i++) {
|
||||||
|
const s = (f.t - 0.6) / 0.6;
|
||||||
|
ctx.fillStyle = `rgba(200,195,185,${0.6 * (1 - s)})`;
|
||||||
|
ctx.beginPath(); ctx.arc(x + (i - 1.5) * 5 * (1 + s), FIRE - 10 - s * 30 - i * 3, 4 + s * 6, 0, Math.PI * 2); ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c.step === 'result') result(ctx, c); else gauges(ctx, c);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
// Le bruciate della Smarronata (Andrea, 31 ottobre: «a manovella, a mano, fallo così»): coi volontari della
|
||||||
|
// Misericordia si gira la manovella del cilindro a rete sopra il fuoco. Il gesto: tenendo premuto si fanno dei giri,
|
||||||
|
// grandi o piccoli, dove si vuole (conta quanto gira la direzione del dito, non dove sta), come la manovella vera.
|
||||||
|
// Le castagne stanno in fondo al cilindro: girando, la rete le alza e loro ricascano e si rimescolano. Troppo piano,
|
||||||
|
// quelle di sotto restano sulla brace e si carbonizzano mentre quelle di sopra restano crude; troppo forte, la rete
|
||||||
|
// le porta in cima e qualcuna salta fuori nel fuoco («mica si fa la centrifuga!»). Quando paiono cotte si rovesciano
|
||||||
|
// nel sacco: contano quelle buone, né crude né bruciate. Per chi non può fare il gesto: Spazio, le frecce o il
|
||||||
|
// pulsante Gira danno una spinta a ogni colpo. Unità: i pixel del primo piano 640×360, visto di fronte (sfondo di
|
||||||
|
// Codex: assets/closeups/smarronata-bruciatore.png).
|
||||||
|
|
||||||
|
// Il cilindro (fra le due forcelle dello sfondo) e la manovella, a destra: un giro visto di sbieco, un'ellisse.
|
||||||
|
export const DRUM = { x0: 182, x1: 456, y: 190, r: 46 }, CRANK = { x: 506, y: 190, rx: 14, ry: 30 }, BAR = 92;
|
||||||
|
export const N = 18, BOTTOM = Math.PI / 2;
|
||||||
|
// Tempi in secondi; velocità in radianti al secondo.
|
||||||
|
export const MIN_POUR = 4, POUR = 1.4, SPILL = 0.45, PUSH = 2.6, FRICTION = 1.4, LINE = 2.4, MAX_SPEED = 22;
|
||||||
|
// Quando una castagna è cotta e quando è carbonizzata.
|
||||||
|
export const COOKED = 0.85, BURNT = 1.6;
|
||||||
|
// I livelli: sotto `lo` il cilindro gira troppo piano (le castagne non si rimescolano abbastanza), sopra `hi` le
|
||||||
|
// castagne volano fuori; `time`, in quanti secondi cuociono girando bene; `goal`, quante buone ci vogliono.
|
||||||
|
export const LEVELS = {
|
||||||
|
bona: { lo: 1.6, hi: 10, time: 20, goal: 10 },
|
||||||
|
seria: { lo: 2.2, hi: 8, time: 17, goal: 15 },
|
||||||
|
male: { lo: 2.8, hi: 6.5, time: 14, goal: 16 },
|
||||||
|
};
|
||||||
|
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||||
|
const wrap = a => Math.atan2(Math.sin(a), Math.cos(a));
|
||||||
|
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
|
||||||
|
const between = (c, lo, hi) => lo + (hi - lo) * random(c);
|
||||||
|
|
||||||
|
// Dove ricasca una castagna: di nuovo in fondo, a un'altezza a caso nel mucchio, con la sua voglia di farsi alzare.
|
||||||
|
function drop(c, k, side = 1) { Object.assign(k, { a: BOTTOM - side * between(c, 0, 0.55), r: between(c, 0.5, 0.93), lift: between(c, 0.55, 1.5) }); }
|
||||||
|
|
||||||
|
export function newBruciate(seed = Date.now() >>> 0, def = {}) {
|
||||||
|
const level = LEVELS[def.level] ? def.level : 'seria';
|
||||||
|
const c = { step: 'turn', t: 0, seed, level, skill: LEVELS[level], current: 'player', power: 0, drum: 0, speed: 0,
|
||||||
|
pending: 0, grab: null, cursor: null, spin: 0, chestnuts: [], flying: [], line: null, lineT: 0, events: [], visual: { time: 0 } };
|
||||||
|
for (let i = 0; i < N; i++) { const k = { x: between(c, 0.06, 0.94), cook: between(c, 0, 0.08), out: false }; drop(c, k, random(c) < 0.5 ? 1 : -1); c.chestnuts.push(k); }
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
export const inside = c => c.chestnuts.filter(k => !k.out);
|
||||||
|
// Quanto stanno a contatto con la rete sopra la brace: le castagne in fondo e di fuori; quelle di sopra quasi niente.
|
||||||
|
export function contact(k) { return Math.max(0, Math.cos(k.a - BOTTOM)) ** 3 * clamp((k.r - 0.62) / 0.3, 0, 1); }
|
||||||
|
// La cottura media delle castagne ancora nel cilindro, da 0 (crude) in su (1: cotte).
|
||||||
|
export const doneness = c => { const ks = inside(c); return ks.length ? ks.reduce((s, k) => s + k.cook, 0) / ks.length : 0; };
|
||||||
|
export const tally = c => {
|
||||||
|
const t = { good: 0, raw: 0, burnt: 0, spilled: 0 };
|
||||||
|
for (const k of c.chestnuts) t[k.out ? 'spilled' : k.cook < COOKED ? 'raw' : k.cook > BURNT ? 'burnt' : 'good']++;
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
function say(c, line) { c.line = line; c.lineT = LINE; }
|
||||||
|
|
||||||
|
export function update(c, dt) {
|
||||||
|
if (!Number.isFinite(dt) || dt <= 0) return;
|
||||||
|
dt = Math.min(dt, 0.25); c.t += dt; c.visual.time += dt;
|
||||||
|
if (c.lineT > 0 && (c.lineT -= dt) <= 0) c.line = null;
|
||||||
|
for (const f of c.flying) f.t += dt;
|
||||||
|
c.flying = c.flying.filter(f => f.t < 1.2);
|
||||||
|
if (c.step === 'pour') { if (c.t >= POUR) finish(c); return; }
|
||||||
|
if (c.step !== 'turn') return;
|
||||||
|
// La velocità: tenendo la manovella è quella del gesto (smussata); lasciata, il cilindro rallenta da sé.
|
||||||
|
if (c.grab) c.speed += (c.pending / dt - c.speed) * (1 - Math.exp(-dt * 10));
|
||||||
|
else c.speed *= Math.exp(-FRICTION * dt);
|
||||||
|
c.pending = 0;
|
||||||
|
c.speed = clamp(c.speed, -MAX_SPEED, MAX_SPEED);
|
||||||
|
const w = c.speed, fast = Math.abs(w) > c.skill.hi, side = Math.sign(w) || 1, before = c.drum;
|
||||||
|
c.drum += w * dt;
|
||||||
|
// Il cigolio della manovella, a ogni mezzo giro.
|
||||||
|
if (Math.floor(before / Math.PI) !== Math.floor(c.drum / Math.PI)) c.events.push({ kind: 'bruciate-manovella', power: clamp(Math.abs(w) / 8, 0.3, 1) });
|
||||||
|
const rate = 1 / (c.skill.time * 0.45);
|
||||||
|
for (const k of inside(c)) {
|
||||||
|
// La rete se le porta dietro finché non ricascano in fondo; troppo forte le porta fino in cima e oltre.
|
||||||
|
k.a += w * dt * k.r;
|
||||||
|
if (!fast && (k.a - BOTTOM) * side > k.lift) drop(c, k, side);
|
||||||
|
const before = k.cook;
|
||||||
|
k.cook += dt * rate * (0.15 + 1.9 * contact(k));
|
||||||
|
if (before < COOKED && k.cook >= COOKED) c.events.push({ kind: 'bruciate-scoppio', power: 0.5 + 0.5 * random(c) });
|
||||||
|
}
|
||||||
|
// La centrifuga: ogni tanto ne vola fuori una, nel fuoco.
|
||||||
|
if (fast) {
|
||||||
|
c.spin += dt;
|
||||||
|
if (c.spin >= SPILL) {
|
||||||
|
c.spin = 0;
|
||||||
|
const k = inside(c).sort((p, q) => Math.cos(p.a - BOTTOM) - Math.cos(q.a - BOTTOM))[0];
|
||||||
|
if (k) { k.out = true; c.flying.push({ x: DRUM.x0 + k.x * (DRUM.x1 - DRUM.x0), y: DRUM.y + Math.sin(k.a) * DRUM.r * k.r, dir: side, t: 0 }); c.events.push({ kind: 'bruciate-cade', power: 0.8 }); }
|
||||||
|
say(c, random(c) < 0.5 ? 'Oh! Mica si fa la centrifuga!' : 'Piano, che le si butta nel fuoco!');
|
||||||
|
}
|
||||||
|
} else c.spin = Math.max(0, c.spin - dt);
|
||||||
|
if (!c.line && Math.abs(w) < c.skill.lo && c.t > 3 && inside(c).some(k => k.cook > BURNT - 0.3)) say(c, 'Gira, che sotto si bruciano!');
|
||||||
|
// Tutte nel fuoco o tutte carbone: si rovescia comunque.
|
||||||
|
if (!inside(c).length || inside(c).every(k => k.cook > BURNT)) pour(c);
|
||||||
|
}
|
||||||
|
function finish(c) {
|
||||||
|
const t = tally(c);
|
||||||
|
c.step = 'result'; c.speed = 0;
|
||||||
|
c.result = { winner: t.good >= c.skill.goal ? 'player' : 'cpu', ...t, goal: c.skill.goal };
|
||||||
|
}
|
||||||
|
export function pour(c) {
|
||||||
|
if (c.step !== 'turn') return false;
|
||||||
|
Object.assign(c, { step: 'pour', t: 0, grab: null });
|
||||||
|
c.events.push({ kind: 'bruciate-sacco', power: 0.8 });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Comandi. Il gesto: la direzione del movimento, a ogni tratto, quanto ha girato rispetto al tratto prima.
|
||||||
|
const STROKE = 3;
|
||||||
|
export function aimScreen(c, [x, y]) {
|
||||||
|
if (!Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||||
|
const g = c.grab;
|
||||||
|
if (g && c.step === 'turn') {
|
||||||
|
const [px, py] = g.last ?? [x, y], dx = x - px, dy = y - py;
|
||||||
|
if (Math.hypot(dx, dy) >= STROKE) {
|
||||||
|
const heading = Math.atan2(dy, dx);
|
||||||
|
if (g.heading !== null) c.pending += clamp(wrap(heading - g.heading), -1.2, 1.2);
|
||||||
|
g.heading = heading; g.last = [x, y];
|
||||||
|
} else if (!g.last) g.last = [x, y];
|
||||||
|
}
|
||||||
|
c.cursor = [x, y];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function press(c, pointerId) {
|
||||||
|
if (c.step !== 'turn' || c.grab) return false;
|
||||||
|
c.grab = { pointerId, heading: null, last: c.cursor };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function release(c, pointerId) {
|
||||||
|
if (!c.grab || c.grab.pointerId !== pointerId) return false;
|
||||||
|
c.grab = null; return true;
|
||||||
|
}
|
||||||
|
export const cancel = (c, pointerId) => (pointerId === undefined || c.grab?.pointerId === pointerId) && release(c, c.grab?.pointerId);
|
||||||
|
// Una spinta alla manovella (Spazio, frecce): per chi non fa il gesto.
|
||||||
|
export function push(c) {
|
||||||
|
if (c.step !== 'turn' || c.grab) return false;
|
||||||
|
c.speed = (Math.sign(c.speed) || 1) * (Math.abs(c.speed) + PUSH);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function key(c, k, down) {
|
||||||
|
if (k === 'Enter') { if (down) shoot(c); return true; }
|
||||||
|
if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(k)) return false;
|
||||||
|
if (down) push(c);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Il pulsante: rovescia nel sacco (dopo i primi secondi).
|
||||||
|
export const canThrow = c => c.step === 'turn' && c.t >= MIN_POUR;
|
||||||
|
export const canAdjust = () => false;
|
||||||
|
export const aimFree = () => true;
|
||||||
|
export const shoot = c => canThrow(c) && pour(c);
|
||||||
|
export const button = c => c.step === 'result' ? 'Finita' : c.step === 'pour' ? 'Nel sacco…' : 'Rovescia nel sacco';
|
||||||
|
export function speedWord(c) {
|
||||||
|
const w = Math.abs(c.speed);
|
||||||
|
return w < 0.3 ? 'ferma' : w < c.skill.lo ? 'troppo piano' : w > c.skill.hi ? 'troppo forte' : 'giusta';
|
||||||
|
}
|
||||||
|
export function status(c) {
|
||||||
|
if (c.step === 'result') {
|
||||||
|
const r = c.result, extra = [r.raw && `${r.raw} crude`, r.burnt && `${r.burnt} carbonizzate`, r.spilled && `${r.spilled} nel fuoco`].filter(Boolean).join(', ');
|
||||||
|
return `${r.good} bruciate buone su ${N}${extra ? ` (${extra})` : ''}. ${r.winner === 'player' ? 'I volontari approvano!' : `Ne servivano ${r.goal}.`}`;
|
||||||
|
}
|
||||||
|
if (c.step === 'pour') return 'Nel sacco di iuta, a sudare...';
|
||||||
|
const cooked = `Cottura ${Math.round(doneness(c) * 100)}%.`;
|
||||||
|
if (c.line) return `${c.line} ${cooked}`;
|
||||||
|
const hint = c.t < 3 && Math.abs(c.speed) < 0.3 ? 'Tieni premuto e fai dei giri per girare la manovella.' : `Manovella ${speedWord(c)}.`;
|
||||||
|
return `${hint} ${cooked} Quando son cotte: Rovescia nel sacco.`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// Il flipper del River in primo piano (flipper.js): la tavola dipinta da Codex con San Piero, e sopra le parti
|
||||||
|
// meccaniche disegnate dal gioco, nelle stesse coordinate della fisica: sponde cromate (larghe quanto collidono),
|
||||||
|
// corsie dei rioni con le luci sulla riga che conta, funghi, bersagli a caduta, il Regolo, sponde elastiche, flipper,
|
||||||
|
// molla e pallina. A sinistra, sul fianco nero del mobile, il quadro dei punti. Niente filtri né passate a tutto
|
||||||
|
// schermo (vedi risveglio.js).
|
||||||
|
import { FIELD, LANE, R, PIVOT, flipperSeg, waiting, canThrow, saveLeft } from './flipper.js';
|
||||||
|
import { text, panel } from './bocce-render.js';
|
||||||
|
|
||||||
|
const COLORS = { piazza: '#d0453a', pieve: '#f2eee2', cardetole: '#3fa34d', campomigliaio: '#2f6fd0' };
|
||||||
|
const rail = (ctx, [ax, ay], [bx, by], dark = '#2a2622', light = '#c9c3b6') => {
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.strokeStyle = dark; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();
|
||||||
|
ctx.strokeStyle = light; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();
|
||||||
|
};
|
||||||
|
function disc(ctx, x, y, r, fill, rim) {
|
||||||
|
ctx.fillStyle = rim; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.fillStyle = fill; ctx.beginPath(); ctx.arc(x, y, r - 2, 0, Math.PI * 2); ctx.fill();
|
||||||
|
}
|
||||||
|
function bumper(ctx, b) {
|
||||||
|
const r = b.r * (b.hit > 0 ? 1 + b.hit * 1.2 : 1);
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.25)'; ctx.beginPath(); ctx.arc(b.x + 2, b.y + 3, r, 0, Math.PI * 2); ctx.fill();
|
||||||
|
disc(ctx, b.x, b.y, r, b.hit > 0 ? '#ffe9a8' : '#d9463a', '#5a1d16');
|
||||||
|
disc(ctx, b.x, b.y, r * 0.55, b.hit > 0 ? '#fff6d8' : '#f0a24a', '#8a3a1e');
|
||||||
|
ctx.fillStyle = 'rgba(255,255,255,0.55)'; ctx.fillRect(Math.round(b.x - r * 0.4), Math.round(b.y - r * 0.6), 3, 2);
|
||||||
|
}
|
||||||
|
// Il Regolo: un bersaglio tondo d'oro con la corona e il bordo scuro, che non si confonde col drago dipinto.
|
||||||
|
function regolo(ctx, o) {
|
||||||
|
const r = o.r + (o.hit > 0 ? 2 : 0);
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.3)'; ctx.beginPath(); ctx.arc(o.x + 1, o.y + 2, r + 1, 0, Math.PI * 2); ctx.fill();
|
||||||
|
disc(ctx, o.x, o.y, r + 1, o.hit > 0 ? '#fff6c8' : '#f0c040', '#3a2a08');
|
||||||
|
ctx.fillStyle = '#3fa34d'; ctx.beginPath(); ctx.arc(o.x, o.y, r - 3, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.fillStyle = '#ffd45a';
|
||||||
|
for (const [dx, h] of [[-4, 4], [-1, 6], [2, 4]]) ctx.fillRect(o.x + dx, o.y - r - 2 - h, 2, h);
|
||||||
|
ctx.fillRect(o.x - 5, o.y - r - 2, 10, 2);
|
||||||
|
}
|
||||||
|
function flipper(ctx, c, side) {
|
||||||
|
const f = c.flippers[side], { a: [px, py], b: [tx, ty] } = flipperSeg(c, side);
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.strokeStyle = 'rgba(0,0,0,0.3)'; ctx.lineWidth = 7; ctx.beginPath(); ctx.moveTo(px + 1, py + 3); ctx.lineTo(tx + 1, ty + 3); ctx.stroke();
|
||||||
|
ctx.strokeStyle = '#3a2a1a'; ctx.lineWidth = 7; ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(tx, ty); ctx.stroke();
|
||||||
|
ctx.strokeStyle = f.held.length ? '#ffd45a' : '#e8b640'; ctx.lineWidth = 5; ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(tx, ty); ctx.stroke();
|
||||||
|
ctx.strokeStyle = '#fff1b8'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(px, py - 1.5); ctx.lineTo(tx, ty - 1.5); ctx.stroke();
|
||||||
|
disc(ctx, px, py, 4, '#8a7a6a', '#2a2622');
|
||||||
|
}
|
||||||
|
function plunger(ctx, c) {
|
||||||
|
const x = (LANE.x0 + LANE.x1) / 2, top = 326, pull = c.holding ? c.power * 14 : canThrow(c) ? c.power * 6 : 0;
|
||||||
|
ctx.fillStyle = '#4a4440'; ctx.fillRect(Math.round(x) - 3, top + pull, 6, 336 - top - pull + 10);
|
||||||
|
ctx.fillStyle = '#9a9088';
|
||||||
|
for (let y = top + pull + 2; y < 346; y += 3) ctx.fillRect(Math.round(x) - 4, Math.round(y), 8, 1);
|
||||||
|
ctx.fillStyle = '#d9463a'; ctx.fillRect(Math.round(x) - 5, Math.round(top + pull) - 3, 10, 4);
|
||||||
|
}
|
||||||
|
function ball(ctx, b) {
|
||||||
|
if (!b) return;
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.35)'; ctx.beginPath(); ctx.arc(b.x + 2, b.y + 3, R, 0, Math.PI * 2); ctx.fill();
|
||||||
|
const g = ctx.createRadialGradient(b.x - 1.5, b.y - 1.5, 0.5, b.x, b.y, R);
|
||||||
|
g.addColorStop(0, '#ffffff'); g.addColorStop(0.4, '#cfd3da'); g.addColorStop(1, '#5a6068');
|
||||||
|
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(b.x, b.y, R, 0, Math.PI * 2); ctx.fill();
|
||||||
|
}
|
||||||
|
// Il quadro dei punti sul fianco del mobile: punti, palline, i rioni accesi, il traguardo, il salvataggio, l'avviso.
|
||||||
|
function board(ctx, c) {
|
||||||
|
panel(ctx, 16, 56, 172, 168);
|
||||||
|
text(ctx, 'FLIPPER DEL RIVER', 102, 72, 7, '#efd49e', 'center');
|
||||||
|
text(ctx, String(c.score).padStart(6, '0'), 102, 98, 16, '#fff4c8', 'center');
|
||||||
|
text(ctx, `PALLINE ${c.step === 'result' ? 0 : c.balls}`, 28, 124, 7, '#d7ccb4');
|
||||||
|
text(ctx, c.result ? 'TRAGUARDO FATTO' : `TRAGUARDO ${c.skill.goal}`, 28, 140, 7, c.result ? '#9fd27a' : '#d7ccb4');
|
||||||
|
text(ctx, 'RIONI', 28, 160, 7, '#d7ccb4');
|
||||||
|
c.lanes.forEach((l, i) => disc(ctx, 86 + i * 22, 160, 7, l.lit ? COLORS[l.rione] : '#3a3630', l.lit ? '#fff4c8' : '#5a5650'));
|
||||||
|
const save = saveLeft(c);
|
||||||
|
if (save > 0) text(ctx, `SALVATA ${Math.ceil(save)} s`, 28, 180, 7, '#9fd27a');
|
||||||
|
else if (c.ball && !c.ball.waiting) text(ctx, c.saved ? 'SALVATAGGIO USATO' : 'SENZA RETE', 28, 180, 6, '#a9a08a');
|
||||||
|
if (c.line) text(ctx, c.line, 102, 206, 8, '#ffe08c', 'center');
|
||||||
|
}
|
||||||
|
function result(ctx, c) {
|
||||||
|
panel(ctx, 16, 236, 172, 56);
|
||||||
|
text(ctx, c.result.winner === 'player' ? 'TRAGUARDO FATTO!' : 'FINE PARTITA', 102, 252, 8, '#efd49e', 'center');
|
||||||
|
text(ctx, `${c.result.score} su ${c.result.goal}`, 102, 272, 8, c.result.winner === 'player' ? '#9fd27a' : '#e0b07a', 'center');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawFlipper(ctx, c, def, bg) {
|
||||||
|
const time = c.visual.time;
|
||||||
|
ctx.save();
|
||||||
|
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
|
||||||
|
else { ctx.fillStyle = '#17110c'; ctx.fillRect(0, 0, 640, 360); ctx.fillStyle = '#e9d9b4'; ctx.fillRect(FIELD.x0, FIELD.y0, FIELD.x1 - FIELD.x0, FIELD.y1 - FIELD.y0); }
|
||||||
|
// Le luci delle corsie dei rioni, sulla riga che conta, sotto le bandiere.
|
||||||
|
for (const l of c.lanes) {
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.35)'; ctx.fillRect(l.x - 7, l.y - 3, 14, 7);
|
||||||
|
disc(ctx, l.x, l.y, 6, l.lit ? COLORS[l.rione] : '#2a2622', l.lit ? '#fff4c8' : '#6a6050');
|
||||||
|
}
|
||||||
|
// Le sponde: tutte le pareti della fisica, cromate; le elastiche con la gomma rossa.
|
||||||
|
for (const w of c.walls) rail(ctx, w.a, w.b, w.sling ? '#5a1d16' : '#2a2622', w.sling ? '#e8584a' : '#c9c3b6');
|
||||||
|
// I bersagli a caduta: in piedi colorati col bordo scuro, caduti un segno a terra.
|
||||||
|
for (const t of c.targets) {
|
||||||
|
const [x, y0] = t.a, y1 = t.b[1];
|
||||||
|
if (t.down) { ctx.fillStyle = 'rgba(0,0,0,0.35)'; ctx.fillRect(x - 2, y0, 4, y1 - y0); continue; }
|
||||||
|
ctx.fillStyle = '#2a2622'; ctx.fillRect(x - 3, y0 - 1, 6, y1 - y0 + 2);
|
||||||
|
ctx.fillStyle = t.bank === 'pieve' ? '#f2eee2' : '#e8b640'; ctx.fillRect(x - 2, y0, 4, y1 - y0);
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.25)'; ctx.fillRect(x - 2, y1 - 3, 4, 3);
|
||||||
|
}
|
||||||
|
for (const b of c.bumpers) bumper(ctx, b);
|
||||||
|
regolo(ctx, c.regolo);
|
||||||
|
for (const side of ['left', 'right']) flipper(ctx, c, side);
|
||||||
|
plunger(ctx, c);
|
||||||
|
ball(ctx, c.ball);
|
||||||
|
// Da fermo, l'invito: la molla da caricare.
|
||||||
|
if (waiting(c) && !c.holding && Math.sin(time * 5) > 0) text(ctx, 'LANCIA', (LANE.x0 + LANE.x1) / 2, 300, 5, '#ffe08c', 'center');
|
||||||
|
board(ctx, c);
|
||||||
|
if (c.step === 'result') result(ctx, c);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
// Il flipper del River (Andrea, 31 ottobre: «una tavola bellissima, con San Piero dentro»). Visto dall'alto, la tavola
|
||||||
|
// in verticale al centro del primo piano 640×360; la pallina parte dalla corsia di destra con la molla, fa il giro
|
||||||
|
// dell'arco in cima, passa in una delle quattro corsie dei rioni, rimbalza sui tre funghi (i bastioni della Fortezza),
|
||||||
|
// abbatte i bersagli della Pieve a sinistra e della torre a destra, tocca il Regolo, e giù dalle sponde elastiche ai
|
||||||
|
// due flipper, con la Sieve dipinta che scorre nel buco in mezzo. Tre palline; si vince arrivando al traguardo di
|
||||||
|
// punti del livello (e poi si può continuare per il record). Unità: i pixel del primo piano. La fisica va a passo
|
||||||
|
// fisso (come il biliardo), con sottopassi quando pallina e flipper vanno forte.
|
||||||
|
// Rivista con la revisione di Codex del 31 ottobre: punti solo a un urto nuovo (non al contatto che dura), due dita
|
||||||
|
// sulla scena, annullare non lancia, niente cancelletto-ripiano, salvataggio col tempo della simulazione, corsie che
|
||||||
|
// contano quando si attraversano, traguardo acquisito appena raggiunto.
|
||||||
|
export const FIELD = { x0: 200, x1: 448, y0: 5, y1: 336 }, LANE = { x0: 432, x1: 448 }, R = 4.5;
|
||||||
|
export const BALLS = 3, STEP = 1 / 240, MAX_SPEED = 980, CHARGE = 1.2, FLIP = { len: 34, rest: 0.5, up: -0.46, speed: 26, r: 3 };
|
||||||
|
export const PIVOT = { left: [280, 314], right: [360, 314] };
|
||||||
|
export const SCORE = { bumper: 100, sling: 50, lane: 200, palio: 3000, target: 500, bank: 2000, regolo: 1000 };
|
||||||
|
// Quanto deve picchiare un urto perché conti (velocità lungo la normale, px/s): il tremolio da gravità non conta.
|
||||||
|
const IMPACT = 40, LANE_LINE = 104, BANK_RESET = 0.8, CLICK_HOLD = 0.16, NUDGE_AFTER = 2.5;
|
||||||
|
// I livelli: la stessa fisica, quasi; cambiano il traguardo, il salvataggio della pallina appena lanciata e un po' la
|
||||||
|
// gravità. La spinta dei funghi è uguale per tutti.
|
||||||
|
export const LEVELS = {
|
||||||
|
bona: { g: 680, save: 8, kick: 340, goal: 6000 },
|
||||||
|
seria: { g: 740, save: 4, kick: 340, goal: 10000 },
|
||||||
|
male: { g: 800, save: 1, kick: 340, goal: 16000 },
|
||||||
|
};
|
||||||
|
export const RIONI = ['piazza', 'pieve', 'cardetole', 'campomigliaio'];
|
||||||
|
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||||
|
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
|
||||||
|
const seg = (a, b, extra = {}) => ({ a, b, e: 0.55, touching: false, ...extra });
|
||||||
|
|
||||||
|
// La tavola: sponde, corsie, sponde elastiche, funghi, bersagli. Le coordinate seguono il disegno di Codex
|
||||||
|
// (assets/closeups/river-flipper.png): i tre dischi dei funghi, le quattro bandiere dei rioni, la corsia della molla.
|
||||||
|
export function layout() {
|
||||||
|
const arc = [[200, 72], [203, 50], [212, 32], [228, 18], [252, 10], [288, 6], [324, 5], [360, 6], [396, 10], [420, 18], [436, 32], [445, 50], [448, 72]];
|
||||||
|
const walls = [];
|
||||||
|
for (let i = 1; i < arc.length; i++) walls.push(seg(arc[i - 1], arc[i], { e: 0.6 }));
|
||||||
|
walls.push(seg([200, 72], [200, 336]), seg([448, 72], [448, 336]));
|
||||||
|
// La corsia della molla, col fondo. Niente cancelletto: una pallina che ricasca nella corsia si rilancia e basta
|
||||||
|
// (il cancelletto a senso unico faceva da ripiano ai lanci a metà).
|
||||||
|
walls.push(seg([432, 70], [432, 336]), seg([432, 326], [448, 326], { e: 0.2 }));
|
||||||
|
// Le corsie dei rioni, in cima, fra le bandiere: tre guide.
|
||||||
|
for (const x of [277, 319, 361]) walls.push(seg([x, 70], [x, 102], { e: 0.4 }));
|
||||||
|
// I divisori delle uscite laterali e le guide che portano ai flipper (la guida passa sopra il perno e continua un
|
||||||
|
// po' lungo il flipper: finendo sul perno la pallina ci restava incastrata).
|
||||||
|
walls.push(seg([222, 240], [222, 290]), seg([222, 290], [292, 314], { e: 0.3 }));
|
||||||
|
walls.push(seg([418, 240], [418, 290]), seg([418, 290], [348, 314], { e: 0.3 }));
|
||||||
|
// Le sponde elastiche sopra i flipper: il lato lungo spinge.
|
||||||
|
walls.push(seg([236, 246], [236, 284]), seg([236, 284], [262, 290]), seg([236, 246], [262, 290], { sling: true }));
|
||||||
|
walls.push(seg([404, 246], [404, 284]), seg([404, 284], [378, 290]), seg([404, 246], [378, 290], { sling: true }));
|
||||||
|
// I bersagli a caduta: la Pieve a sinistra, la torre a destra, con la sponda dietro.
|
||||||
|
walls.push(seg([210, 146], [210, 198]), seg([429, 146], [429, 198]));
|
||||||
|
const targets = [];
|
||||||
|
for (const [bank, x] of [['pieve', 216], ['torre', 423]]) for (let i = 0; i < 3; i++) targets.push({ bank, i, a: [x, 150 + i * 16], b: [x, 162 + i * 16], down: false, touching: false });
|
||||||
|
const bumpers = [[281, 120], [360, 120], [320, 153]].map(([x, y]) => ({ x, y, r: 12, hit: 0, touching: false }));
|
||||||
|
// Il Regolo, in alto a destra sotto l'arco, dove la pallina scende dal giro.
|
||||||
|
const regolo = { x: 404, y: 62, r: 7, hit: 0, touching: false };
|
||||||
|
// Le corsie: ognuna fra due guide (o la sponda), con la bandiera dipinta in `x`; si accende attraversando in
|
||||||
|
// discesa la riga LANE_LINE.
|
||||||
|
const lanes = [[200, 277, 257], [277, 319, 297], [319, 361, 340], [361, 432, 381]].map(([x0, x1, x], i) => ({ x0, x1, x, y: LANE_LINE, rione: RIONI[i], lit: false }));
|
||||||
|
return { walls, targets, bumpers, regolo, lanes };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newFlipper(seed = Date.now() >>> 0, def = {}) {
|
||||||
|
const level = LEVELS[def.level] ? def.level : 'seria';
|
||||||
|
const c = { step: 'play', t: 0, sim: 0, seed, level, skill: LEVELS[level], current: 'player', balls: BALLS, score: 0, power: 0.7,
|
||||||
|
holding: false, holdTime: 0, pointerId: null, cursor: null, accumulator: 0, events: [], line: null, lineT: 0, saved: false,
|
||||||
|
flippers: { left: { angle: FLIP.rest, omega: 0, held: [], click: 0 }, right: { angle: Math.PI - FLIP.rest, omega: 0, held: [], click: 0 } },
|
||||||
|
ball: null, saveUntil: 0, still: 0, anchor: null, rest: 0, resets: {}, result: null, visual: { time: 0 }, ...layout() };
|
||||||
|
newBall(c);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
function newBall(c) { c.ball = { x: (LANE.x0 + LANE.x1) / 2, y: 318, vx: 0, vy: 0, waiting: true }; c.still = 0; c.anchor = null; c.rest = 0; }
|
||||||
|
export const waiting = c => c.step === 'play' && !!c.ball?.waiting;
|
||||||
|
export const canThrow = c => waiting(c) && !c.holding;
|
||||||
|
export const canPress = canThrow;
|
||||||
|
// Due permessi diversi: il cursore della molla solo prima del lancio, i flipper per tutta la partita.
|
||||||
|
export const canAdjust = canThrow;
|
||||||
|
export const canFlip = c => c.step === 'play';
|
||||||
|
export const aimFree = () => true;
|
||||||
|
export const powerAt = time => clamp(time / CHARGE, 0, 1);
|
||||||
|
// Quanto resta del salvataggio della pallina, in secondi (0: non protetta).
|
||||||
|
export const saveLeft = c => c.ball && !c.ball.waiting ? Math.max(0, c.saveUntil - c.sim) : 0;
|
||||||
|
function say(c, line) { c.line = line; c.lineT = 2; }
|
||||||
|
// I punti; al traguardo la vittoria è acquisita (anche uscendo prima), poi si continua per il record.
|
||||||
|
function add(c, points) {
|
||||||
|
c.score += points;
|
||||||
|
if (!c.result && c.score >= c.skill.goal) { c.result = { winner: 'player', score: c.score, goal: c.skill.goal }; say(c, 'TRAGUARDO!'); c.events.push({ kind: 'flipper-palio', power: 1 }); }
|
||||||
|
if (c.result) c.result.score = c.score;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Il lancio: la molla spinge la pallina su per la corsia. Anche la molla più fiacca arriva all'arco (la velocità
|
||||||
|
// minima dipende dalla gravità del livello); da lì in su, la forza decide quanto gira.
|
||||||
|
export function launchSpeed(c, power) {
|
||||||
|
const min = Math.sqrt(2 * c.skill.g * 340), max = Math.max(min + 150, 900);
|
||||||
|
return min + (max - min) * clamp(power, 0, 1);
|
||||||
|
}
|
||||||
|
export function launch(c, power) {
|
||||||
|
if (!waiting(c)) return false;
|
||||||
|
// La molla vera non è mai identica due volte: un pelo di caso, così la partita non si ripete uguale.
|
||||||
|
c.ball.waiting = false; c.ball.vy = -launchSpeed(c, power) * (0.985 + random(c) * 0.03); c.ball.vx = 0;
|
||||||
|
// Il salvataggio vale una volta per pallina: persa subito, torna; persa di nuovo, è persa.
|
||||||
|
c.saveUntil = c.saved ? 0 : c.sim + c.skill.save; c.holding = false; c.holdTime = 0; c.pointerId = null;
|
||||||
|
c.events.push({ kind: 'flipper-lancio', power: 0.5 + 0.5 * clamp(power, 0, 1) });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Il flipper: il tasto tenuto lo alza, lasciato ricade. Più dita o tasti insieme: resta su finché ne resta uno.
|
||||||
|
// Un «clic» (il pulsante attivato da tastiera) lo tiene su per un attimo, abbastanza perché la fisica lo veda.
|
||||||
|
export function hold(c, side, who, down) {
|
||||||
|
const f = c.flippers[side]; if (!f) return false;
|
||||||
|
const was = f.held.length > 0;
|
||||||
|
f.held = f.held.filter(h => h !== who); if (down) f.held.push(who);
|
||||||
|
if (who === 'click' && down) f.click = CLICK_HOLD;
|
||||||
|
if (!was && f.held.length && c.step === 'play') c.events.push({ kind: 'flipper-flip', power: 0.6 });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export function flipperSeg(c, side) {
|
||||||
|
const f = c.flippers[side], [px, py] = PIVOT[side];
|
||||||
|
return { a: [px, py], b: [px + Math.cos(f.angle) * FLIP.len, py + Math.sin(f.angle) * FLIP.len] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// La fisica, a passi fissi.
|
||||||
|
export function update(c, dt) {
|
||||||
|
if (!Number.isFinite(dt) || dt <= 0) return;
|
||||||
|
dt = Math.min(dt, 0.25); c.t += dt; c.visual.time += dt;
|
||||||
|
if (c.lineT > 0 && (c.lineT -= dt) <= 0) c.line = null;
|
||||||
|
if (c.holding) { c.holdTime += dt; c.power = powerAt(c.holdTime); }
|
||||||
|
for (const b of c.bumpers) b.hit = Math.max(0, b.hit - dt); c.regolo.hit = Math.max(0, c.regolo.hit - dt);
|
||||||
|
c.accumulator += dt;
|
||||||
|
while (c.accumulator >= STEP) { c.accumulator -= STEP; step(c); }
|
||||||
|
}
|
||||||
|
function moveFlippers(c, h) {
|
||||||
|
for (const side of ['left', 'right']) {
|
||||||
|
const f = c.flippers[side];
|
||||||
|
if (f.click > 0 && (f.click -= h) <= 0) f.held = f.held.filter(k => k !== 'click');
|
||||||
|
const target = f.held.length ? (side === 'left' ? FLIP.up : Math.PI - FLIP.up) : (side === 'left' ? FLIP.rest : Math.PI - FLIP.rest);
|
||||||
|
const previous = f.angle;
|
||||||
|
f.angle += clamp(target - f.angle, -FLIP.speed * h, FLIP.speed * h);
|
||||||
|
f.omega = (f.angle - previous) / h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function step(c) {
|
||||||
|
c.sim += STEP;
|
||||||
|
const ball = c.ball;
|
||||||
|
if (c.step !== 'play' || !ball || ball.waiting) { moveFlippers(c, STEP); return; }
|
||||||
|
for (const bank of Object.keys(c.resets)) if (c.sim >= c.resets[bank]) { for (const t of c.targets) if (t.bank === bank) t.down = false; delete c.resets[bank]; }
|
||||||
|
ball.vy += c.skill.g * STEP;
|
||||||
|
limit(ball);
|
||||||
|
// Sottopassi: pallina e punta del flipper insieme non devono fare più di un raggio per passo.
|
||||||
|
const tip = FLIP.speed * FLIP.len, n = clamp(Math.ceil((Math.hypot(ball.vx, ball.vy) + tip) * STEP / R), 1, 6), h = STEP / n;
|
||||||
|
const from = { x: ball.x, y: ball.y };
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
moveFlippers(c, h);
|
||||||
|
ball.x += ball.vx * h; ball.y += ball.vy * h;
|
||||||
|
for (const w of c.walls) collideSeg(c, ball, w);
|
||||||
|
for (const t of c.targets) if (!t.down) collideTarget(c, ball, t);
|
||||||
|
for (const b of c.bumpers) collideBumper(c, ball, b);
|
||||||
|
collideRegolo(c, ball);
|
||||||
|
for (const side of ['left', 'right']) collideFlipper(c, ball, side);
|
||||||
|
limit(ball);
|
||||||
|
}
|
||||||
|
crossLanes(c, from, ball);
|
||||||
|
// La pallina che resta nello stesso punto (ferma, o che trema in un incastro): dopo un po' una scossa. Non se è
|
||||||
|
// trattenuta su un flipper alzato: quella è una mossa del giocatore.
|
||||||
|
if (!c.anchor || Math.hypot(ball.x - c.anchor.x, ball.y - c.anchor.y) > 3) { c.anchor = { x: ball.x, y: ball.y }; c.still = 0; }
|
||||||
|
else if ((c.still += STEP) > NUDGE_AFTER) { c.still = 0; c.anchor = null; if (!cradled(c, ball)) { ball.vx += (random(c) - 0.5) * 160; ball.vy -= 90; } }
|
||||||
|
// Un lancio fiacco (o una pallina ricaduta nella corsia) si assesta sul fondo e si rilancia.
|
||||||
|
if (ball.x > LANE.x0 && ball.y > 318 && Math.hypot(ball.vx, ball.vy) < 12) { if ((c.rest += STEP) > 0.3) newBall(c); } else c.rest = 0;
|
||||||
|
if (ball.y > FIELD.y1 + R * 2) drain(c);
|
||||||
|
}
|
||||||
|
function limit(ball) { const v = Math.hypot(ball.vx, ball.vy); if (v > MAX_SPEED) { ball.vx *= MAX_SPEED / v; ball.vy *= MAX_SPEED / v; } }
|
||||||
|
const cradled = (c, ball) => ['left', 'right'].some(side => c.flippers[side].held.length && Math.hypot(ball.x - PIVOT[side][0], ball.y - PIVOT[side][1]) < FLIP.len + 2 * R);
|
||||||
|
// Le corsie: contano attraversando la riga in discesa, dovunque nella corsia (la posizione orizzontale interpolata).
|
||||||
|
function crossLanes(c, from, ball) {
|
||||||
|
if (!(from.y < LANE_LINE && ball.y >= LANE_LINE)) return;
|
||||||
|
const k = (LANE_LINE - from.y) / (ball.y - from.y), x = from.x + (ball.x - from.x) * k;
|
||||||
|
const l = c.lanes.find(l => x >= l.x0 && x < l.x1);
|
||||||
|
if (!l || l.lit) return;
|
||||||
|
l.lit = true; add(c, SCORE.lane); c.events.push({ kind: 'flipper-corsia', power: 0.6 });
|
||||||
|
if (c.lanes.every(k => k.lit)) { add(c, SCORE.palio); say(c, 'PALIO!'); c.events.push({ kind: 'flipper-palio', power: 1 }); for (const k of c.lanes) k.lit = false; }
|
||||||
|
}
|
||||||
|
function drain(c) {
|
||||||
|
c.events.push({ kind: 'flipper-buco', power: 0.8 });
|
||||||
|
if (c.sim < c.saveUntil) { say(c, 'Pallina salvata!'); c.saved = true; newBall(c); return; }
|
||||||
|
c.balls--; c.saved = false;
|
||||||
|
if (c.balls > 0) { say(c, `Persa: ne restano ${c.balls}.`); newBall(c); return; }
|
||||||
|
c.ball = null; c.step = 'result';
|
||||||
|
c.result = { winner: c.score >= c.skill.goal ? 'player' : 'cpu', score: c.score, goal: c.skill.goal };
|
||||||
|
}
|
||||||
|
// Pallina contro un segmento: la si spinge fuori e si riflette la velocità. Torna la velocità d'urto lungo la
|
||||||
|
// normale (positiva se picchiava), o null se non tocca; `touching` ricorda il contatto, per contare solo gli urti nuovi.
|
||||||
|
function hitSeg(ball, w, e) {
|
||||||
|
const [ax, ay] = w.a, [bx, by] = w.b, dx = bx - ax, dy = by - ay, len2 = dx * dx + dy * dy || 1;
|
||||||
|
const u = clamp(((ball.x - ax) * dx + (ball.y - ay) * dy) / len2, 0, 1), px = ax + dx * u, py = ay + dy * u;
|
||||||
|
let nx = ball.x - px, ny = ball.y - py; const d = Math.hypot(nx, ny);
|
||||||
|
if (d >= R) { w.touching = false; return null; }
|
||||||
|
if (d < 1e-6) { nx = -dy; ny = dx; const n = Math.hypot(nx, ny); nx /= n; ny /= n; } else { nx /= d; ny /= d; }
|
||||||
|
ball.x = px + nx * R; ball.y = py + ny * R;
|
||||||
|
const vn = ball.vx * nx + ball.vy * ny, fresh = !w.touching;
|
||||||
|
w.touching = true;
|
||||||
|
if (vn < 0 && e !== null) { ball.vx -= (1 + e) * vn * nx; ball.vy -= (1 + e) * vn * ny; }
|
||||||
|
return { vn: -vn, nx, ny, fresh };
|
||||||
|
}
|
||||||
|
function collideSeg(c, ball, w) {
|
||||||
|
if (!w.sling) { hitSeg(ball, w, w.e); return; }
|
||||||
|
// La sponda elastica: spinge via lungo la normale, tenendo la componente di strisciamento, e un po' a caso come
|
||||||
|
// la gomma vera; solo a un urto nuovo, e solo se picchia.
|
||||||
|
const hit = hitSeg(ball, w, null);
|
||||||
|
if (!hit || hit.vn <= 0) return;
|
||||||
|
if (hit.fresh && hit.vn > IMPACT) {
|
||||||
|
const a = Math.atan2(hit.ny, hit.nx) + (random(c) - 0.5) * 0.24, s = 250 + random(c) * 30;
|
||||||
|
const tx = ball.vx + hit.vn * hit.nx, ty = ball.vy + hit.vn * hit.ny; // la parte lungo la sponda
|
||||||
|
ball.vx = Math.cos(a) * s + tx * 0.6; ball.vy = Math.sin(a) * s + ty * 0.6;
|
||||||
|
add(c, SCORE.sling); c.events.push({ kind: 'flipper-sponda', power: 0.6 });
|
||||||
|
} else { ball.vx += 1.3 * hit.vn * hit.nx; ball.vy += 1.3 * hit.vn * hit.ny; }
|
||||||
|
}
|
||||||
|
function collideTarget(c, ball, t) {
|
||||||
|
const hit = hitSeg(ball, t, 0.5);
|
||||||
|
if (!hit || !hit.fresh || hit.vn < IMPACT) return;
|
||||||
|
t.down = true; add(c, SCORE.target); c.events.push({ kind: 'flipper-bersaglio', power: 0.7 });
|
||||||
|
const bank = c.targets.filter(k => k.bank === t.bank);
|
||||||
|
if (bank.every(k => k.down)) { add(c, SCORE.bank); say(c, t.bank === 'pieve' ? 'LA PIEVE!' : 'LA TORRE!'); c.resets[t.bank] = c.sim + BANK_RESET; }
|
||||||
|
}
|
||||||
|
// Pallina contro un cerchio: spinta fuori; torna la velocità d'urto lungo la normale (null se non tocca).
|
||||||
|
function hitCircle(ball, o, e) {
|
||||||
|
const dx = ball.x - o.x, dy = ball.y - o.y, d = Math.hypot(dx, dy), min = R + o.r;
|
||||||
|
if (d >= min) { o.touching = false; return null; }
|
||||||
|
const nx = d > 1e-6 ? dx / d : 0, ny = d > 1e-6 ? dy / d : -1;
|
||||||
|
ball.x = o.x + nx * min; ball.y = o.y + ny * min;
|
||||||
|
const vn = ball.vx * nx + ball.vy * ny, fresh = !o.touching;
|
||||||
|
o.touching = true;
|
||||||
|
if (vn < 0) { ball.vx -= (1 + e) * vn * nx; ball.vy -= (1 + e) * vn * ny; }
|
||||||
|
return { vn: -vn, nx, ny, fresh };
|
||||||
|
}
|
||||||
|
function collideBumper(c, ball, b) {
|
||||||
|
const hit = hitCircle(ball, b, 0.2);
|
||||||
|
if (!hit || !hit.fresh || hit.vn < IMPACT) return;
|
||||||
|
const a = Math.atan2(hit.ny, hit.nx) + (random(c) - 0.5) * 0.2;
|
||||||
|
ball.vx = Math.cos(a) * c.skill.kick; ball.vy = Math.sin(a) * c.skill.kick;
|
||||||
|
b.hit = 0.15; add(c, SCORE.bumper); c.events.push({ kind: 'flipper-fungo', power: 0.8 });
|
||||||
|
}
|
||||||
|
function collideRegolo(c, ball) {
|
||||||
|
const hit = hitCircle(ball, c.regolo, 0.7);
|
||||||
|
if (!hit || !hit.fresh || hit.vn < IMPACT) return;
|
||||||
|
c.regolo.hit = 0.3; add(c, SCORE.regolo); say(c, 'IL REGOLO!'); c.events.push({ kind: 'flipper-bersaglio', power: 1 });
|
||||||
|
}
|
||||||
|
// Il flipper è una capsula che gira: alla pallina si aggiunge la velocità del punto toccato.
|
||||||
|
function collideFlipper(c, ball, side) {
|
||||||
|
const f = c.flippers[side], { a, b } = flipperSeg(c, side), [ax, ay] = a, dx = b[0] - ax, dy = b[1] - ay, len2 = dx * dx + dy * dy;
|
||||||
|
const u = clamp(((ball.x - ax) * dx + (ball.y - ay) * dy) / len2, 0, 1), px = ax + dx * u, py = ay + dy * u;
|
||||||
|
let nx = ball.x - px, ny = ball.y - py; const d = Math.hypot(nx, ny), min = R + FLIP.r;
|
||||||
|
if (d >= min) return false;
|
||||||
|
if (d < 1e-6) { nx = 0; ny = -1; } else { nx /= d; ny /= d; }
|
||||||
|
ball.x = px + nx * min; ball.y = py + ny * min;
|
||||||
|
// La velocità della superficie: ω × r, col raggio dal perno al punto toccato.
|
||||||
|
const rx = px - ax, ry = py - ay, sx = -f.omega * ry, sy = f.omega * rx;
|
||||||
|
const vn = (ball.vx - sx) * nx + (ball.vy - sy) * ny;
|
||||||
|
if (vn < 0) { ball.vx -= (1 + 0.45) * vn * nx; ball.vy -= (1 + 0.45) * vn * ny; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Comandi: col mouse o col dito, prima del lancio tenere premuto carica la molla e lasciare lancia; in gioco
|
||||||
|
// la metà sinistra della scena alza il flipper sinistro, la destra il destro, finché si tiene premuto (anche due dita).
|
||||||
|
export function aimScreen(c, [x, y]) { if (!Number.isFinite(x) || !Number.isFinite(y)) return false; c.cursor = [x, y]; return true; }
|
||||||
|
export function press(c, pointerId = 'ptr') {
|
||||||
|
if (c.step !== 'play') return false;
|
||||||
|
if (waiting(c)) { if (c.holding) return false; c.holding = true; c.holdTime = 0; c.pointerId = pointerId; c.power = 0; return true; }
|
||||||
|
return hold(c, (c.cursor?.[0] ?? 320) < 320 ? 'left' : 'right', pointerId, true);
|
||||||
|
}
|
||||||
|
export function release(c, pointerId = 'ptr') {
|
||||||
|
if (c.holding && c.pointerId === pointerId) return launch(c, powerAt(c.holdTime));
|
||||||
|
let any = false;
|
||||||
|
for (const side of ['left', 'right']) if (c.flippers[side].held.includes(pointerId)) any = hold(c, side, pointerId, false) || any;
|
||||||
|
return any;
|
||||||
|
}
|
||||||
|
// Annullare interrompe senza fare niente: la molla si scarica, i flipper ricadono (tutti, senza puntatore).
|
||||||
|
export function cancel(c, pointerId) {
|
||||||
|
const all = pointerId === undefined;
|
||||||
|
if (all || c.pointerId === pointerId) { c.holding = false; c.holdTime = 0; c.pointerId = null; }
|
||||||
|
for (const f of Object.values(c.flippers)) f.held = all ? [] : f.held.filter(id => id !== pointerId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// I pulsanti «← Flipper» e «Flipper →» (tenuti premuti; da tastiera un colpo breve), e le frecce.
|
||||||
|
export const turn = (c, dir, who = 'button') => c.step === 'play' && hold(c, dir < 0 ? 'left' : 'right', who, true);
|
||||||
|
export function stopTurn(c, who = 'button') {
|
||||||
|
if (who === 'click') return false; // il colpo breve finisce da sé
|
||||||
|
const l = hold(c, 'left', who, false), r = hold(c, 'right', who, false);
|
||||||
|
return l || r;
|
||||||
|
}
|
||||||
|
export const nudge = () => false;
|
||||||
|
export function setPower(c, v) { if (!canThrow(c) || !Number.isFinite(v)) return false; c.power = clamp(v, 0, 1); return true; }
|
||||||
|
export function key(c, k, down) {
|
||||||
|
if (c.step !== 'play') return false; // a partita finita Spazio e Invio chiudono il risultato (main.js)
|
||||||
|
if (k === 'ArrowLeft' || k === 'ArrowRight') { hold(c, k === 'ArrowLeft' ? 'left' : 'right', 'key-' + k, down); return true; }
|
||||||
|
if (k === ' ') { if (down) { if (!c.holding && waiting(c)) press(c, 'key'); } else if (c.holding && c.pointerId === 'key') release(c, 'key'); return true; }
|
||||||
|
if (k === 'Enter') { if (down) shoot(c); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Il pulsante Lancia: con la forza del cursore.
|
||||||
|
export const shoot = c => canThrow(c) && launch(c, c.power);
|
||||||
|
export const button = c => c.step === 'result' ? 'Finita' : waiting(c) ? 'Lancia' : 'In gioco…';
|
||||||
|
export function status(c) {
|
||||||
|
if (c.step === 'result') return `${c.result.score} punti. ${c.result.winner === 'player' ? `Traguardo ${c.result.goal}: fatto!` : `Ne servivano ${c.result.goal}.`}`;
|
||||||
|
const goal = c.result ? 'traguardo fatto, ora il record' : `traguardo ${c.skill.goal}`;
|
||||||
|
const tally = `Punti ${c.score} (${goal}), palline ${c.balls}.`;
|
||||||
|
if (c.line) return `${c.line} ${tally}`;
|
||||||
|
return waiting(c) ? `Tieni premuto per caricare la molla e lascia, o premi Lancia. ${tally}` : `Flipper: metà sinistra e destra della scena, o i pulsanti. ${tally}`;
|
||||||
|
}
|
||||||
@@ -18,6 +18,8 @@ import { newRisveglio, updateRisveglio } from './risveglio.js';
|
|||||||
import { newTelefono, updateTelefono, tapTelefono } from './telefono.js';
|
import { newTelefono, updateTelefono, tapTelefono } from './telefono.js';
|
||||||
import * as condor from './condor.js';
|
import * as condor from './condor.js';
|
||||||
import * as campana from './campana.js';
|
import * as campana from './campana.js';
|
||||||
|
import * as bruciate from './bruciate.js';
|
||||||
|
import * as flipper from './flipper.js';
|
||||||
import * as poker from './poker.js';
|
import * as poker from './poker.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.
|
||||||
@@ -34,6 +36,8 @@ export const MINIGAMES = {
|
|||||||
cerbottana: { create: (seed, level) => cerbottana.newCerbottana(seed, { level }), update: cerbottana.update, press: cerbottana.press, release: cerbottana.release, cancel: cerbottana.cancel, aim: cerbottana.aimScreen },
|
cerbottana: { create: (seed, level) => cerbottana.newCerbottana(seed, { level }), update: cerbottana.update, press: cerbottana.press, release: cerbottana.release, cancel: cerbottana.cancel, aim: cerbottana.aimScreen },
|
||||||
condor: { create: (seed, level) => condor.newCondor(seed, { level }), update: condor.update, press: condor.press, release: condor.release, cancel: condor.cancel, aim: condor.aimScreen },
|
condor: { create: (seed, level) => condor.newCondor(seed, { level }), update: condor.update, press: condor.press, release: condor.release, cancel: condor.cancel, aim: condor.aimScreen },
|
||||||
campana: { create: (seed, level) => campana.newCampana(seed, { level }), update: campana.update, press: campana.press, release: campana.release, cancel: campana.cancel, aim: campana.aimScreen },
|
campana: { create: (seed, level) => campana.newCampana(seed, { level }), update: campana.update, press: campana.press, release: campana.release, cancel: campana.cancel, aim: campana.aimScreen },
|
||||||
|
bruciate: { create: (seed, level) => bruciate.newBruciate(seed, { level }), update: bruciate.update, press: bruciate.press, release: bruciate.release, cancel: bruciate.cancel, aim: bruciate.aimScreen },
|
||||||
|
flipper: { create: (seed, level) => flipper.newFlipper(seed, { level }), update: flipper.update, press: flipper.press, release: flipper.release, cancel: flipper.cancel, aim: flipper.aimScreen },
|
||||||
micro: { create: (seed, level) => micro.newMicro(seed, { level }), update: micro.update, press: micro.press, release: micro.release, cancel: micro.cancel, aim: micro.aimScreen },
|
micro: { create: (seed, level) => micro.newMicro(seed, { level }), update: micro.update, press: micro.press, release: micro.release, cancel: micro.cancel, aim: micro.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 },
|
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 },
|
||||||
};
|
};
|
||||||
|
|||||||
+10
-5
@@ -26,12 +26,17 @@ export function trackFor(story, { screen, scene, phase, closeup, at, ok = () =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Il volume di ogni console: pieno fino a `near` pixel, poi cala fino a `far` (non sotto un filo, `floor`: la festa
|
// Il volume di ogni console: pieno fino a `near` pixel, poi cala fino a `far` (non sotto un filo, `floor`: la festa
|
||||||
// si sente sempre). La più forte per prima.
|
// si sente sempre). Sotto una console (entro `near`, sfumando fino a una volta e mezzo) le altre calano fino al loro
|
||||||
|
// filo: lì si sente bene la sola musica di quella (Andrea, 31 ottobre, in cima a Via dell'Antica Posta). La più forte
|
||||||
|
// per prima.
|
||||||
export function mix(stages, [x, y]) {
|
export function mix(stages, [x, y]) {
|
||||||
return stages.map(st => {
|
const near = st => st.near ?? 90, floor = st => st.floor ?? 0.08;
|
||||||
const d = Math.hypot(x - st.at[0], (y - st.at[1]) * 1.5), near = st.near ?? 90, far = st.far ?? 520;
|
const d = stages.map(st => Math.hypot(x - st.at[0], (y - st.at[1]) * 1.5));
|
||||||
const k = Math.max(0, Math.min(1, (d - near) / (far - near)));
|
const under = stages.map((st, i) => Math.max(0, Math.min(1, (near(st) * 1.5 - d[i]) / (near(st) * 0.5))));
|
||||||
return { src: st.music, gain: Math.max(st.floor ?? 0.08, (1 - k) ** 1.6) };
|
return stages.map((st, i) => {
|
||||||
|
const far = st.far ?? 520, k = Math.max(0, Math.min(1, (d[i] - near(st)) / (far - near(st))));
|
||||||
|
const others = Math.max(0, ...under.filter((_, j) => j !== i));
|
||||||
|
return { src: st.music, gain: Math.max(floor(st), (1 - k) ** 1.6 * (1 - others)) };
|
||||||
}).sort((a, b) => b.gain - a.gain);
|
}).sort((a, b) => b.gain - a.gain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+47
-1
@@ -14,6 +14,8 @@ import { drawCerbottana } from './cerbottana-render.js';
|
|||||||
import { drawCorsa } from './corsa.js';
|
import { drawCorsa } from './corsa.js';
|
||||||
import { drawCondor } from './condor-render.js';
|
import { drawCondor } from './condor-render.js';
|
||||||
import { drawCampana } from './campana-render.js';
|
import { drawCampana } from './campana-render.js';
|
||||||
|
import { drawBruciate } from './bruciate-render.js';
|
||||||
|
import { drawFlipper } from './flipper-render.js';
|
||||||
import { drawPoker } from './poker-render.js';
|
import { drawPoker } from './poker-render.js';
|
||||||
import { drawViaggio } from './viaggio.js';
|
import { drawViaggio } from './viaggio.js';
|
||||||
import { drawIncanto } from './incanto.js';
|
import { drawIncanto } from './incanto.js';
|
||||||
@@ -178,7 +180,8 @@ export function render(ctx, game, { hover, spots, debug, time = performance.now(
|
|||||||
for (const shape of art) if (!shape.top) drawShape(ctx, shape);
|
for (const shape of art) if (!shape.top) drawShape(ctx, shape);
|
||||||
// Andrea e i personaggi della scena, dal più lontano al più vicino.
|
// Andrea e i personaggi della scena, dal più lontano al più vicino.
|
||||||
const andrea = sprites.get('andrea');
|
const andrea = sprites.get('andrea');
|
||||||
const drawAndreaNow = () => andrea ? drawSprite(ctx, andrea, game.pose(), s.x, s.y, game.scaleAt(s.y), s.facing)
|
const party = festa(game, time), drawAndreaNow = () => party ? drawSprite(ctx, party.sheet, party.pose, s.x, s.y, game.scaleAt(s.y), s.facing)
|
||||||
|
: andrea ? drawSprite(ctx, andrea, game.pose(), s.x, s.y, game.scaleAt(s.y), s.facing)
|
||||||
: drawAndrea(ctx, s.x, s.y, game.scaleAt(s.y), s.facing, game.walking ? game.stride / 6 : null);
|
: drawAndrea(ctx, s.x, s.y, game.scaleAt(s.y), s.facing, game.walking ? game.stride / 6 : null);
|
||||||
// Durante il risveglio si guarda dagli occhi di Andrea: lui non si vede.
|
// Durante il risveglio si guarda dagli occhi di Andrea: lui non si vede.
|
||||||
const pov = game.closeup && game.story.closeups?.[game.closeup.id]?.type === 'risveglio';
|
const pov = game.closeup && game.story.closeups?.[game.closeup.id]?.type === 'risveglio';
|
||||||
@@ -188,6 +191,7 @@ export function render(ctx, game, { hover, spots, debug, time = performance.now(
|
|||||||
if (sheet) queue.push({ y: p.at[1], draw: () => drawSprite(ctx, sheet, game.personPose(p), ...p.at, game.scaleAt(p.at[1]), p.facing ?? 1, p.size) });
|
if (sheet) queue.push({ y: p.at[1], draw: () => drawSprite(ctx, sheet, game.personPose(p), ...p.at, game.scaleAt(p.at[1]), p.facing ?? 1, p.size) });
|
||||||
}
|
}
|
||||||
for (const q of queue.sort((a, b) => a.y - b.y)) q.draw();
|
for (const q of queue.sort((a, b) => a.y - b.y)) q.draw();
|
||||||
|
if (!pov) crowdFront(ctx, game, time);
|
||||||
drawApparitions(ctx, game, time);
|
drawApparitions(ctx, game, time);
|
||||||
drawPhase(ctx, game);
|
drawPhase(ctx, game);
|
||||||
drawEffects(ctx, game, time);
|
drawEffects(ctx, game, time);
|
||||||
@@ -763,6 +767,38 @@ export const EFFECTS = {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
// All'Ingorgo Andrea ha la sua birra (Andrea, 31 ottobre): camminando la tiene davanti al petto, parlando pure; fermo
|
||||||
|
// alza il braccio col bicchiere e lo muove a tempo con la folla. Prendere e voltarsi restano quelli di sempre.
|
||||||
|
const CHEER = ['cheer_1', 'cheer_2', 'cheer_1', 'cheer_3'];
|
||||||
|
export function festa(game, time) {
|
||||||
|
const sheet = sprites.get('andrea_birra'), band = (game.scene.effects ?? []).find(e => e.type === 'crowd' && onNow(e, game));
|
||||||
|
if (!sheet || !band) return null;
|
||||||
|
const pose = game.pose(), beat = Math.floor(time / 60000 * (band.bpm ?? 120)) % CHEER.length;
|
||||||
|
const mine = pose === 'stand' ? CHEER[beat] : pose;
|
||||||
|
return sheet.atlas.frames[mine] ? { sheet, pose: mine } : null;
|
||||||
|
}
|
||||||
|
// Andrea dentro la folla dell'Ingorgo (Andrea, 31 ottobre: «sembra di volare fra le persone»): sopra di lui, dalla
|
||||||
|
// vita in giù, si ridisegna la gente dello sfondo che gli sta davanti. A strisce come l'effetto `crowd`, con lo stesso
|
||||||
|
// sobbalzo a tempo (così le teste davanti saltano con le altre) e il bordo in alto a teste, mai dritto.
|
||||||
|
export function crowdFront(ctx, game, time, bg = backgrounds.get(game.backdrop())) {
|
||||||
|
const bands = (game.scene.effects ?? []).filter(e => e.type === 'crowd' && onNow(e, game));
|
||||||
|
if (!bg || !bands.length) return;
|
||||||
|
const s = game.state, h = PERSON * game.scaleAt(s.y), k = bg.width / WIDTH, waist = s.y - h * 0.5, half = Math.ceil(h * 0.3);
|
||||||
|
for (const e of bands) {
|
||||||
|
const [ax, ay, aw, ah] = e.area, step = e.step ?? 5, amount = e.amount ?? 3, beats = time / 60000 * (e.bpm ?? 120);
|
||||||
|
for (let x = ax + Math.floor((s.x - half - ax) / step) * step; x < s.x + half; x += step) {
|
||||||
|
if (x < ax || x >= ax + aw) continue;
|
||||||
|
const i = (x - ax) / step, b = (beats / 2 + hash(i * 17 + ay)) % 1, jump = b < 0.5 ? Math.sin(b * Math.PI) : 0;
|
||||||
|
const dy = Math.round(amount * (0.4 + 0.6 * hash(i * 31 + 5)) * jump);
|
||||||
|
// La testa di chi sta davanti: ogni striscia alla sua altezza, e salta col battito.
|
||||||
|
const edge = Math.round(waist - h * 0.14 * hash(i * 7 + 3)) - dy;
|
||||||
|
const from = Math.max(edge, ay), to = Math.min(s.y + 3, ay + ah);
|
||||||
|
if (to <= from) continue;
|
||||||
|
const w = Math.min(step, ax + aw - x);
|
||||||
|
ctx.drawImage(bg, x * k, (from + dy) * k, w * k, (to - from) * k, x, from, w, to - from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const BACKGROUND_EFFECTS = new Set(['sway', 'clouds', 'cloth', 'leafshadow', 'crowd', ...Object.keys(ambientEffects)]);
|
const BACKGROUND_EFFECTS = new Set(['sway', 'clouds', 'cloth', 'leafshadow', 'crowd', ...Object.keys(ambientEffects)]);
|
||||||
function drawEffects(ctx, game, time, background = false) {
|
function drawEffects(ctx, game, time, background = false) {
|
||||||
for (const e of game.scene.effects ?? []) {
|
for (const e of game.scene.effects ?? []) {
|
||||||
@@ -1020,6 +1056,16 @@ function drawCloseup(ctx, game, time) {
|
|||||||
drawCampana(ctx, c, def, backgrounds.get(def.picture));
|
drawCampana(ctx, c, def, backgrounds.get(def.picture));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (def.type === 'flipper') {
|
||||||
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
|
drawFlipper(ctx, c, def, backgrounds.get(def.picture));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (def.type === 'bruciate') {
|
||||||
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
|
drawBruciate(ctx, c, def, backgrounds.get(def.picture));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (def.type === 'micro') {
|
if (def.type === 'micro') {
|
||||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
drawMicro(ctx, c, def, backgrounds.get(def.picture), src => { if (!backgrounds.has(src)) loadBackground(src); return backgrounds.get(src); });
|
drawMicro(ctx, c, def, backgrounds.get(def.picture), src => { if (!backgrounds.has(src)) loadBackground(src); return backgrounds.get(src); });
|
||||||
|
|||||||
@@ -156,6 +156,20 @@ export class Soundscape {
|
|||||||
else if (kind === 'campana-sasso') { this.tone(1900, 1100, 0.04, 0.05 * strength + 0.02, 0, pan); this.tone(1500, 900, 0.03, 0.03, 0.06, pan); this.noiseSound(0.04, 3000, 0.03, { pan }); }
|
else if (kind === 'campana-sasso') { this.tone(1900, 1100, 0.04, 0.05 * strength + 0.02, 0, pan); this.tone(1500, 900, 0.03, 0.03, 0.06, pan); this.noiseSound(0.04, 3000, 0.03, { pan }); }
|
||||||
else if (kind === 'campana-salto') { this.tone(110, 60, 0.07, 0.07, 0, pan); this.noiseSound(0.05, 500, 0.04, { pan }); }
|
else if (kind === 'campana-salto') { this.tone(110, 60, 0.07, 0.07, 0, pan); this.noiseSound(0.05, 500, 0.04, { pan }); }
|
||||||
else if (kind === 'campana-riga') { this.tone(300, 180, 0.12, 0.05, 0, pan); this.tone(220, 120, 0.16, 0.05, 0.1, pan); }
|
else if (kind === 'campana-riga') { this.tone(300, 180, 0.12, 0.05, 0, pan); this.tone(220, 120, 0.16, 0.05, 0.1, pan); }
|
||||||
|
// Le bruciate: il cigolio della manovella, la castagna che scoppietta cuocendo, quella che casca nel fuoco, il sacco.
|
||||||
|
else if (kind === 'bruciate-manovella') { this.tone(420, 380, 0.08, 0.025 * strength, 0, pan); this.noiseSound(0.06, 900, 0.02, { pan }); }
|
||||||
|
else if (kind === 'bruciate-scoppio') { this.noiseSound(0.04, 2600, 0.05 * strength, { pan }); this.tone(900, 500, 0.03, 0.03, 0, pan); }
|
||||||
|
else if (kind === 'bruciate-cade') { this.noiseSound(0.5, 3200, 0.09, { pan }); this.tone(200, 90, 0.12, 0.05, 0, pan); }
|
||||||
|
else if (kind === 'bruciate-sacco') this.noiseSound(0.7, 1100, 0.1, { paper: true, pan });
|
||||||
|
// Il flipper: la molla, il colpo del flipper, il fungo, la sponda elastica, il bersaglio, la corsia, il Palio, il buco.
|
||||||
|
else if (kind === 'flipper-lancio') { this.noiseSound(0.12, 1800, 0.05 * strength, { pan }); this.tone(180, 600, 0.12, 0.05, 0, pan); }
|
||||||
|
else if (kind === 'flipper-flip') { this.tone(240, 120, 0.05, 0.07, 0, pan); this.noiseSound(0.03, 2200, 0.04, { pan }); }
|
||||||
|
else if (kind === 'flipper-fungo') { this.tone(900, 500, 0.06, 0.06 * strength + 0.02, 0, pan); this.tone(1800, 900, 0.04, 0.03, 0, pan); }
|
||||||
|
else if (kind === 'flipper-sponda') { this.tone(520, 260, 0.05, 0.05, 0, pan); this.noiseSound(0.03, 1600, 0.03, { pan }); }
|
||||||
|
else if (kind === 'flipper-bersaglio') { this.tone(1400, 1200, 0.05, 0.05, 0, pan); this.tone(2100, 1800, 0.08, 0.04, 0.05, pan); }
|
||||||
|
else if (kind === 'flipper-corsia') { this.tone(1046, 1046, 0.08, 0.04, 0, pan); this.tone(1318, 1318, 0.1, 0.04, 0.08, pan); }
|
||||||
|
else if (kind === 'flipper-palio') for (const [i, f] of [523, 659, 784, 1046].entries()) this.tone(f, f, 0.14, 0.06, i * 0.1, pan);
|
||||||
|
else if (kind === 'flipper-buco') { this.tone(220, 80, 0.3, 0.06, 0, pan); this.noiseSound(0.2, 600, 0.04, { pan }); }
|
||||||
else if (kind === 'cerbottana-tosse') { this.noiseSound(0.18, 900, 0.08, { pan }); this.tone(260, 140, 0.12, 0.06, 0.25, pan); this.tone(240, 120, 0.1, 0.05, 0.3, pan); }
|
else if (kind === 'cerbottana-tosse') { this.noiseSound(0.18, 900, 0.08, { pan }); this.tone(260, 140, 0.12, 0.06, 0.25, pan); this.tone(240, 120, 0.1, 0.05, 0.3, pan); }
|
||||||
// Le macchinine: il dito che spinge, le ruote sull'asfalto, l'urto di plastica, fuori pista, il traguardo.
|
// Le macchinine: il dito che spinge, le ruote sull'asfalto, l'urto di plastica, fuori pista, il traguardo.
|
||||||
else if (kind === 'micro-spinta') { this.tone(1200, 500, 0.04, 0.04 + strength * 0.04, 0, pan); this.noiseSound(0.04, 2600, 0.04, { pan }); }
|
else if (kind === 'micro-spinta') { this.tone(1200, 500, 0.04, 0.04 + strength * 0.04, 0, pan); this.noiseSound(0.04, 2600, 0.04, { pan }); }
|
||||||
|
|||||||
+36
-9
@@ -15,6 +15,8 @@ import * as micro from './engine/micro.js';
|
|||||||
import * as cerbottana from './engine/cerbottana.js';
|
import * as cerbottana from './engine/cerbottana.js';
|
||||||
import * as condor from './engine/condor.js';
|
import * as condor from './engine/condor.js';
|
||||||
import * as campana from './engine/campana.js';
|
import * as campana from './engine/campana.js';
|
||||||
|
import * as bruciate from './engine/bruciate.js';
|
||||||
|
import * as flipper from './engine/flipper.js';
|
||||||
import * as poker from './engine/poker.js';
|
import * as poker from './engine/poker.js';
|
||||||
import { cardAt as pokerAt } from './engine/poker-render.js';
|
import { cardAt as pokerAt } from './engine/poker-render.js';
|
||||||
import * as basket from './engine/basket.js';
|
import * as basket from './engine/basket.js';
|
||||||
@@ -490,9 +492,12 @@ const debugView = () => debug && debugShown && game && { mouse, points: debugPoi
|
|||||||
const TOUCH_REACH = 22;
|
const TOUCH_REACH = 22;
|
||||||
const reachOf = e => e.pointerType === 'touch' || e.pointerType === 'pen' ? TOUCH_REACH * WIDTH / canvas.getBoundingClientRect().width : 0;
|
const reachOf = e => e.pointerType === 'touch' || e.pointerType === 'pen' ? TOUCH_REACH * WIDTH / canvas.getBoundingClientRect().width : 0;
|
||||||
let press = null;
|
let press = null;
|
||||||
|
// Le dita in più sul flipper (oltre a `press`): ognuna lascia il suo flipper quando si alza. Il tasto destro del
|
||||||
|
// mouse ha lo stesso pointerId del sinistro: per il flipper vale come un dito a parte.
|
||||||
|
const extra = new Set(), pointerKey = e => e.button === 2 ? 'mouse-right' : e.pointerId;
|
||||||
canvas.addEventListener('pointerdown', e => {
|
canvas.addEventListener('pointerdown', e => {
|
||||||
if (debug && e.shiftKey && e.button === 0) { debugPoint(scenePoint(e)); return; }
|
if (debug && e.shiftKey && e.button === 0) { debugPoint(scenePoint(e)); return; }
|
||||||
if (!running || e.button > 0) return;
|
if (!running || (e.button > 0 && !(e.button === 2 && game.closeup && story.closeups[game.closeup.id].type === 'flipper'))) return;
|
||||||
// Nel primo piano un tocco sfoglia (o salta la battuta): gli oggetti della scena dietro non si toccano.
|
// Nel primo piano un tocco sfoglia (o salta la battuta): gli oggetti della scena dietro non si toccano.
|
||||||
if (game.closeup) {
|
if (game.closeup) {
|
||||||
const type = story.closeups[game.closeup.id].type, deck = CARD_AT[type];
|
const type = story.closeups[game.closeup.id].type, deck = CARD_AT[type];
|
||||||
@@ -518,6 +523,12 @@ canvas.addEventListener('pointerdown', e => {
|
|||||||
if (gara.press(game.closeup, e.pointerId)) { press = { gara: e.pointerId }; canvas.setPointerCapture(e.pointerId); }
|
if (gara.press(game.closeup, e.pointerId)) { press = { gara: e.pointerId }; canvas.setPointerCapture(e.pointerId); }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Il flipper: un secondo dito (o il secondo pulsante del mouse) comanda l'altro flipper, per conto suo.
|
||||||
|
if (press?.closeup && type === 'flipper' && !extra.has(pointerKey(e))) {
|
||||||
|
game.closeupAim(scenePoint(e));
|
||||||
|
if (game.closeupPress(pointerKey(e))) { extra.add(pointerKey(e)); canvas.setPointerCapture(e.pointerId); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (press?.closeup) return;
|
if (press?.closeup) return;
|
||||||
const aimed = game.closeupAim(scenePoint(e));
|
const aimed = game.closeupAim(scenePoint(e));
|
||||||
if (['biliardo', 'rinquarto', 'basket', 'micro', 'cerbottana', 'condor'].includes(story.closeups[game.closeup.id].type)) {
|
if (['biliardo', 'rinquarto', 'basket', 'micro', 'cerbottana', 'condor'].includes(story.closeups[game.closeup.id].type)) {
|
||||||
@@ -535,6 +546,8 @@ canvas.addEventListener('pointerdown', e => {
|
|||||||
press = { point, reach, long: false, timer: setTimeout(() => { press.long = true; command.clickScene(point, { look: true, reach }); }, LONG_PRESS) };
|
press = { point, reach, long: false, timer: setTimeout(() => { press.long = true; command.clickScene(point, { look: true, reach }); }, LONG_PRESS) };
|
||||||
});
|
});
|
||||||
canvas.addEventListener('pointerup', e => {
|
canvas.addEventListener('pointerup', e => {
|
||||||
|
const key = e.button === 2 ? 'mouse-right' : e.pointerId;
|
||||||
|
if (extra.has(key)) { extra.delete(key); game?.closeupRelease(key); return; }
|
||||||
if (!press) return;
|
if (!press) return;
|
||||||
// Scopa: la carta toccata e lasciata sullo stesso punto. Se sta nella presa già scelta, si prende quella; se no
|
// Scopa: la carta toccata e lasciata sullo stesso punto. Se sta nella presa già scelta, si prende quella; se no
|
||||||
// si sceglie la prima presa che la contiene (un altro tocco conferma). Trascinando via non succede niente.
|
// si sceglie la prima presa che la contiene (un altro tocco conferma). Trascinando via non succede niente.
|
||||||
@@ -572,12 +585,13 @@ canvas.addEventListener('pointerup', e => {
|
|||||||
press = null;
|
press = null;
|
||||||
});
|
});
|
||||||
canvas.addEventListener('pointercancel', e => {
|
canvas.addEventListener('pointercancel', e => {
|
||||||
|
if (extra.has(e.pointerId)) { extra.delete(e.pointerId); game?.closeupCancel(e.pointerId); return; }
|
||||||
if (press?.closeup && e.pointerId !== press.pointerId) return;
|
if (press?.closeup && e.pointerId !== press.pointerId) return;
|
||||||
if (press) clearTimeout(press.timer);
|
if (press) clearTimeout(press.timer);
|
||||||
if (!press?.closeup || game?.closeup === press.state) game?.closeupCancel(e.pointerId);
|
if (!press?.closeup || game?.closeup === press.state) game?.closeupCancel(e.pointerId);
|
||||||
press = null;
|
press = null;
|
||||||
});
|
});
|
||||||
canvas.addEventListener('lostpointercapture', e => { if (press?.closeup && e.pointerId !== press.pointerId) return; if (press?.closeup || press?.gara !== undefined || press?.card !== undefined || press?.table !== undefined) { press = null; game?.closeupCancel(e.pointerId); } });
|
canvas.addEventListener('lostpointercapture', e => { if (extra.has(e.pointerId)) { extra.delete(e.pointerId); game?.closeupCancel(e.pointerId); return; } if (press?.closeup && e.pointerId !== press.pointerId) return; if (press?.closeup || press?.gara !== undefined || press?.card !== undefined || press?.table !== undefined) { press = null; game?.closeupCancel(e.pointerId); } });
|
||||||
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 });
|
||||||
@@ -585,6 +599,13 @@ canvas.addEventListener('contextmenu', e => {
|
|||||||
canvas.addEventListener('pointermove', e => {
|
canvas.addEventListener('pointermove', e => {
|
||||||
if (!running) return;
|
if (!running) return;
|
||||||
mouse = e.pointerType === 'mouse' ? scenePoint(e) : null;
|
mouse = e.pointerType === 'mouse' ? scenePoint(e) : null;
|
||||||
|
// Il tasto destro premuto o lasciato mentre il sinistro è giù arriva come pointermove (pulsanti «in accordo»):
|
||||||
|
// al flipper vale come un secondo dito, per il flipper di destra.
|
||||||
|
if (e.button === 2 && game.closeup && story.closeups[game.closeup.id].type === 'flipper') {
|
||||||
|
if (e.buttons & 2) { if (!extra.has('mouse-right')) { game.closeupAim(scenePoint(e)); if (game.closeupPress('mouse-right')) extra.add('mouse-right'); } }
|
||||||
|
else if (extra.has('mouse-right')) { extra.delete('mouse-right'); game.closeupRelease('mouse-right'); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 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.)
|
||||||
@@ -617,6 +638,8 @@ const MINI = {
|
|||||||
cerbottana: { ...cerbottana, back: 'Torna in piazza', label: 'La cerbottana: muovi il mouse o tocca il muro per mirare, tieni premuto per prendere fiato e lascia per sparare; oppure regola il fiato e premi Spara. Frecce per spostare il mirino.' },
|
cerbottana: { ...cerbottana, back: 'Torna in piazza', label: 'La cerbottana: muovi il mouse o tocca il muro per mirare, tieni premuto per prendere fiato e lascia per sparare; oppure regola il fiato e premi Spara. Frecce per spostare il mirino.' },
|
||||||
condor: { ...condor, back: 'Torna al campone', label: 'La Condor: muovi il mouse o tocca il campo per mirare, tieni premuto per trattenere il fiato e lascia per sparare; oppure premi Spara. Frecce per spostare il mirino.' },
|
condor: { ...condor, back: 'Torna al campone', label: 'La Condor: muovi il mouse o tocca il campo per mirare, tieni premuto per trattenere il fiato e lascia per sparare; oppure premi Spara. Frecce per spostare il mirino.' },
|
||||||
campana: { ...campana, back: 'Torna al campone', label: 'La campana: muovi il mouse o tocca la campana per mirare, tieni premuto per la forza e lascia per tirare il sassolino; oppure regola la forza e premi Tira (frecce su e giù per mirare, destra e sinistra per la forza). Ai salti premi, o Spazio, quando la lancetta è nel verde.' },
|
campana: { ...campana, back: 'Torna al campone', label: 'La campana: muovi il mouse o tocca la campana per mirare, tieni premuto per la forza e lascia per tirare il sassolino; oppure regola la forza e premi Tira (frecce su e giù per mirare, destra e sinistra per la forza). Ai salti premi, o Spazio, quando la lancetta è nel verde.' },
|
||||||
|
bruciate: { ...bruciate, back: 'Torna alla festa', label: 'Le bruciate: tieni premuto e fai dei giri, col mouse o col dito, per girare la manovella; oppure Spazio o le frecce, una spinta a ogni colpo. Né troppo piano né troppo forte; quando son cotte, Rovescia nel sacco (o Invio).' },
|
||||||
|
flipper: { ...flipper, toggle: true, back: 'Torna in strada', label: 'Il flipper del River: tieni premuto per caricare la molla e lascia per lanciare (o regola la forza e premi Lancia, o Spazio). In gioco, tieni premuto sulla metà sinistra o destra della scena per alzare il flipper di quel lato, anche con due dita o col tasto destro del mouse; oppure i pulsanti Flipper o le frecce sinistra e destra.' },
|
||||||
micro: { ...micro, back: 'Torna dal Chiaru', label: 'La pista col gesso: muovi il mouse o tocca il parcheggio per mirare, tieni premuto per caricare la spinta e lascia; oppure regola la forza e premi Spingi. Frecce o pulsanti Mira per girare.' },
|
micro: { ...micro, back: 'Torna dal Chiaru', label: 'La pista col gesso: muovi il mouse o tocca il parcheggio per mirare, tieni premuto per caricare la spinta e lascia; oppure regola la forza e premi Spingi. Frecce o pulsanti Mira per girare.' },
|
||||||
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).
|
// La gara: niente mira né forza, il pulsante alterna gas e freno (chi non può tenere premuto).
|
||||||
@@ -1046,13 +1069,15 @@ function updateUi() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mg && !cards) {
|
if (mg && !cards) {
|
||||||
setAttr($('bocce-controls'), 'aria-label', { rinquarto: 'Comandi dei rinquarti', biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca', gara: 'Comandi della gara', micro: 'Comandi delle macchinine', cerbottana: 'Comandi della cerbottana', condor: 'Comandi della Condor', campana: 'Comandi della campana' }[def.type] ?? 'Comandi delle bocce');
|
setAttr($('bocce-controls'), 'aria-label', { rinquarto: 'Comandi dei rinquarti', biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca', gara: 'Comandi della gara', micro: 'Comandi delle macchinine', cerbottana: 'Comandi della cerbottana', condor: 'Comandi della Condor', campana: 'Comandi della campana', bruciate: 'Comandi delle bruciate', flipper: 'Comandi del flipper' }[def.type] ?? 'Comandi delle bocce');
|
||||||
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
|
setProp($('panel'), 'hidden', true); setProp($('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;
|
||||||
setValue($('bocce-power'), Math.round(c.power * 100));
|
setValue($('bocce-power'), Math.round(c.power * 100));
|
||||||
setValue($('bocce-power-value'), `${Math.round(c.power * 100)}%`);
|
setValue($('bocce-power-value'), `${Math.round(c.power * 100)}%`);
|
||||||
for (const id of ['bocce-power', 'bocce-left', 'bocce-right']) setProp($(id), 'disabled', !(mg.canAdjust?.(c) ?? mg.canThrow(c)) || (c.holding && !mg.toggle) || !!game.speech);
|
for (const id of ['bocce-power', 'bocce-left', 'bocce-right']) setProp($(id), 'disabled', !(mg.canAdjust?.(c) ?? mg.canThrow(c)) || (c.holding && !mg.toggle) || !!game.speech);
|
||||||
|
// Al flipper i due pulsanti valgono per tutta la partita, il cursore della molla solo prima del lancio.
|
||||||
|
if (mg.canFlip) for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'disabled', !mg.canFlip(c) || !!game.speech);
|
||||||
setProp($('bocce-throw'), 'disabled', !(mg.canPress?.(c) ?? mg.canThrow(c)) || (c.holding && !mg.toggle) || !!game.speech);
|
setProp($('bocce-throw'), 'disabled', !(mg.canPress?.(c) ?? 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).
|
// Il GP di Fortezza: niente forza, e i due pulsanti di mira diventano lo sterzo (si tengono premuti).
|
||||||
// Nel GP di Fortezza il cursore della forza diventa la sensibilità dello sterzo col dito (ricordata).
|
// Nel GP di Fortezza il cursore della forza diventa la sensibilità dello sterzo col dito (ricordata).
|
||||||
@@ -1060,9 +1085,9 @@ function updateUi() {
|
|||||||
// Ai rinquarti la forza è sempre piena: il cursore non serve. Nella guida semplice del GP (alla bona e seria) si
|
// Ai rinquarti la forza è sempre piena: il cursore non serve. Nella guida semplice del GP (alla bona e seria) si
|
||||||
// usa solo il gas: niente sensibilità, sterzo, freno né riquadro dello swipe.
|
// usa solo il gas: niente sensibilità, sterzo, freno né riquadro dello swipe.
|
||||||
const easy = def.type === 'gara' && gara.simple(c);
|
const easy = def.type === 'gara' && gara.simple(c);
|
||||||
setProp(powerLabel, 'hidden', def.type === 'rinquarto' || def.type === 'condor' || easy);
|
setProp(powerLabel, 'hidden', def.type === 'rinquarto' || def.type === 'condor' || def.type === 'bruciate' || easy);
|
||||||
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy || def.type === 'campana');
|
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy || def.type === 'campana' || def.type === 'bruciate');
|
||||||
const powerName = def.type === 'gara' ? 'Sensibilità ' : def.type === 'cerbottana' ? 'Fiato ' : 'Forza ';
|
const powerName = def.type === 'gara' ? 'Sensibilità ' : def.type === 'cerbottana' ? 'Fiato ' : def.type === 'flipper' ? 'Molla ' : 'Forza ';
|
||||||
if (powerLabel.firstChild.nodeValue !== powerName) powerLabel.firstChild.nodeValue = powerName;
|
if (powerLabel.firstChild.nodeValue !== powerName) powerLabel.firstChild.nodeValue = powerName;
|
||||||
setAttr($('bocce-power'), 'aria-label', def.type === 'gara' ? 'Sensibilità dello sterzo col dito' : 'Forza del lancio');
|
setAttr($('bocce-power'), 'aria-label', def.type === 'gara' ? 'Sensibilità dello sterzo col dito' : 'Forza del lancio');
|
||||||
setProp($('gara-pad'), 'hidden', def.type !== 'gara' || easy);
|
setProp($('gara-pad'), 'hidden', def.type !== 'gara' || easy);
|
||||||
@@ -1070,8 +1095,8 @@ function updateUi() {
|
|||||||
c.sensLoaded = true;
|
c.sensLoaded = true;
|
||||||
try { const v = localStorage.getItem(GARA_SENS); if (v !== null) gara.setPower(c, Number(v) / 100); } catch { /* si resta al valore di partenza */ }
|
try { const v = localStorage.getItem(GARA_SENS); if (v !== null) gara.setPower(c, Number(v) / 100); } catch { /* si resta al valore di partenza */ }
|
||||||
}
|
}
|
||||||
setText($('bocce-left'), def.type === 'gara' ? '← Sterza' : '← Mira');
|
setText($('bocce-left'), def.type === 'gara' ? '← Sterza' : def.type === 'flipper' ? '← Flipper' : '← Mira');
|
||||||
setText($('bocce-right'), def.type === 'gara' ? 'Sterza →' : 'Mira →');
|
setText($('bocce-right'), def.type === 'gara' ? 'Sterza →' : def.type === 'flipper' ? 'Flipper →' : 'Mira →');
|
||||||
setProp($('gara-brake'), 'hidden', def.type !== 'gara' || easy);
|
setProp($('gara-brake'), 'hidden', def.type !== 'gara' || easy);
|
||||||
$('bocce-throw').classList.toggle('hold', easy);
|
$('bocce-throw').classList.toggle('hold', easy);
|
||||||
setProp($('gara-brake'), 'disabled', !(mg.canAdjust?.(c) ?? false) || !!game.speech);
|
setProp($('gara-brake'), 'disabled', !(mg.canAdjust?.(c) ?? false) || !!game.speech);
|
||||||
@@ -1083,6 +1108,8 @@ function updateUi() {
|
|||||||
setProp($('cerbottana-help'), 'hidden', def.type !== 'cerbottana');
|
setProp($('cerbottana-help'), 'hidden', def.type !== 'cerbottana');
|
||||||
setProp($('condor-help'), 'hidden', def.type !== 'condor');
|
setProp($('condor-help'), 'hidden', def.type !== 'condor');
|
||||||
setProp($('campana-help'), 'hidden', def.type !== 'campana');
|
setProp($('campana-help'), 'hidden', def.type !== 'campana');
|
||||||
|
setProp($('bruciate-help'), 'hidden', def.type !== 'bruciate');
|
||||||
|
setProp($('flipper-help'), 'hidden', def.type !== 'flipper');
|
||||||
setProp($('basket-help'), 'hidden', def.type !== 'basket');
|
setProp($('basket-help'), 'hidden', def.type !== 'basket');
|
||||||
setProp($('pesca-help'), 'hidden', def.type !== 'pesca');
|
setProp($('pesca-help'), 'hidden', def.type !== 'pesca');
|
||||||
setProp($('gara-help'), 'hidden', def.type !== 'gara');
|
setProp($('gara-help'), 'hidden', def.type !== 'gara');
|
||||||
@@ -1114,7 +1141,7 @@ function updateUi() {
|
|||||||
canvas.classList.toggle('torch', !!torch);
|
canvas.classList.toggle('torch', !!torch);
|
||||||
canvas.classList.toggle('bocce', !!mg);
|
canvas.classList.toggle('bocce', !!mg);
|
||||||
// Biliardo e GP di Fortezza: audio, pausa e uscita fuori dal quadro (al GP la pista passa negli angoli).
|
// 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', 'rinquarto', 'gara', 'micro', 'cerbottana', 'condor', 'campana'].includes(story.closeups[c.id].type));
|
$('stage').classList.toggle('biliardo', !!c && ['biliardo', 'rinquarto', 'gara', 'micro', 'cerbottana', 'condor', 'campana', 'flipper'].includes(story.closeups[c.id].type));
|
||||||
$('stage').classList.toggle('briscola', !!cards);
|
$('stage').classList.toggle('briscola', !!cards);
|
||||||
setAttr(canvas, '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");
|
setAttr(canvas, '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.
|
||||||
|
|||||||
+113
-12
@@ -93,7 +93,7 @@ export const story = {
|
|||||||
// col manifesto in piazza e si spegne uscendo dal centro. [DA ANDREA: le battute sono provvisorie]
|
// col manifesto in piazza e si spegne uscendo dal centro. [DA ANDREA: le battute sono provvisorie]
|
||||||
zones: [
|
zones: [
|
||||||
{
|
{
|
||||||
flag: 'ingorgo', scenes: ['piazza', 'via_antica_posta', 'piazzetta_comune', 'antica_posta_alta', 'piazzetta'],
|
flag: 'ingorgo', scenes: ['piazza', 'via_antica_posta', 'piazzetta_comune', 'antica_posta_alta', 'piazzetta', 'via_calimara', 'via_cipriani'],
|
||||||
exitsOnly: true, // alla festa si clicca solo per spostarsi (Andrea)
|
exitsOnly: true, // alla festa si clicca solo per spostarsi (Andrea)
|
||||||
leave: [
|
leave: [
|
||||||
{ say: 'Mi stropiccio gli occhi. La musica non c\'è più: è di nuovo maggio.' },
|
{ say: 'Mi stropiccio gli occhi. La musica non c\'è più: è di nuovo maggio.' },
|
||||||
@@ -360,6 +360,28 @@ export const story = {
|
|||||||
draw: [],
|
draw: [],
|
||||||
quit: [{ say: 'Il gesso lo lascio lì. Ci giocherà qualche bambino, che è più in allenamento di me.' }],
|
quit: [{ say: 'Il gesso lo lascio lì. Ci giocherà qualche bambino, che è più in allenamento di me.' }],
|
||||||
},
|
},
|
||||||
|
// Il flipper del River (Andrea, 31 ottobre: «una tavola bellissima, con San Piero dentro»): la tavola di Codex,
|
||||||
|
// fisica e parti meccaniche in flipper.js. [DA ANDREA: le battute sono provvisorie]
|
||||||
|
flipper: {
|
||||||
|
type: 'flipper',
|
||||||
|
picture: 'assets/closeups/river-flipper.png',
|
||||||
|
names: { player: 'Andrea', cpu: 'il flipper' },
|
||||||
|
win: [{ say: 'Traguardo! Se c\'era la classifica, oggi ci mettevo le mie iniziali.' }, { set: 'flipper_vinto' }],
|
||||||
|
lose: [{ say: 'Tre palline, e tutte nella Sieve. Il flipper del River non perdona.' }],
|
||||||
|
draw: [],
|
||||||
|
quit: [{ say: 'Lascio la pallina lì. Tanto il River non chiude.' }],
|
||||||
|
},
|
||||||
|
// Le bruciate della Smarronata, 1993: la manovella del cilindro sul fuoco, coi volontari della Misericordia
|
||||||
|
// (Andrea, 31 ottobre: «a manovella, a mano»). [DA ANDREA: le battute sono provvisorie]
|
||||||
|
bruciate: {
|
||||||
|
type: 'bruciate',
|
||||||
|
picture: 'assets/closeups/smarronata-bruciatore.png',
|
||||||
|
names: { player: 'Andrea', cpu: 'i volontari' },
|
||||||
|
win: [{ say: 'Bruciate coi fiocchi! Mezza Smarronata le mangia grazie a me.' }, { say: 'Bravo, figliolo! Se ti va, l\'anno prossimo la manovella è tua.', who: 'volontari', pose: 'give' }, { set: 'bruciate_fatte' }],
|
||||||
|
lose: [{ say: 'Un po\' crude, un po\' carbone... Le mangio io, va\'.' }, { say: 'Ci vuole il polso, figliolo. Il polso e la pazienza.', who: 'volontari', pose: 'explain' }],
|
||||||
|
draw: [],
|
||||||
|
quit: [{ say: 'Vi rendo la manovella. Mi fumano anche gli occhi.' }],
|
||||||
|
},
|
||||||
// Il manifesto dell'Ingorgo, ingrandito (Andrea, 30 ottobre: «se mi avvicino compaia ingrandito, e poi parta
|
// Il manifesto dell'Ingorgo, ingrandito (Andrea, 30 ottobre: «se mi avvicino compaia ingrandito, e poi parta
|
||||||
// l'Ingorgo se ci clicco»). [DA ANDREA: il manifesto vero; questo è di Codex, provvisorio]
|
// l'Ingorgo se ci clicco»). [DA ANDREA: il manifesto vero; questo è di Codex, provvisorio]
|
||||||
manifesto_ingorgo: {
|
manifesto_ingorgo: {
|
||||||
@@ -520,6 +542,7 @@ export const story = {
|
|||||||
// Andrea col pallone, per «Fai due tiri» (foglio di Codex dal suo sprite): non parla, lo disegna basket-render.
|
// Andrea col pallone, per «Fai due tiri» (foglio di Codex dal suo sprite): non parla, lo disegna basket-render.
|
||||||
andrea_basket: { name: 'Andrea', sprite: 'assets/characters/andrea-basket.json' },
|
andrea_basket: { name: 'Andrea', sprite: 'assets/characters/andrea-basket.json' },
|
||||||
andrea_pesca: { name: 'Andrea', sprite: 'assets/characters/andrea-pesca.json' },
|
andrea_pesca: { name: 'Andrea', sprite: 'assets/characters/andrea-pesca.json' },
|
||||||
|
andrea_birra: { name: 'Andrea', sprite: 'assets/characters/andrea-birra.json' }, // all'Ingorgo, con la birra
|
||||||
// Quattro ragazzi inventati (sprite di Codex, idea di Andrea del 27 ottobre): nessuna persona reale.
|
// Quattro ragazzi inventati (sprite di Codex, idea di Andrea del 27 ottobre): nessuna persona reale.
|
||||||
pescatori: { name: 'I ragazzi delle canne', color: '#b8f08a', sprite: 'assets/characters/pescatori.json' },
|
pescatori: { name: 'I ragazzi delle canne', color: '#b8f08a', sprite: 'assets/characters/pescatori.json' },
|
||||||
// La gente della Smarronata del 1993 (Andrea, 31 ottobre): tutti inventati, vestiti come nei fotogrammi del video.
|
// La gente della Smarronata del 1993 (Andrea, 31 ottobre): tutti inventati, vestiti come nei fotogrammi del video.
|
||||||
@@ -1240,6 +1263,19 @@ export const story = {
|
|||||||
name: 'Via Pietro Cipriani',
|
name: 'Via Pietro Cipriani',
|
||||||
map: [22, 64],
|
map: [22, 64],
|
||||||
background: 'assets/scenes/via-cipriani.png',
|
background: 'assets/scenes/via-cipriani.png',
|
||||||
|
backdrops: [{ when: 'ingorgo', src: 'assets/scenes/ingorgo-via-cipriani.png' }], // Codex, la notte dell'Ingorgo (31 ottobre)
|
||||||
|
// All'Ingorgo anche l'inizio di Via Cipriani (Andrea, 31 ottobre): la console sul campo da basket, le casse in
|
||||||
|
// cima alle scale, i laser rossi e lo strobo, e giù la gente che poga (la folla che salta, forte davanti).
|
||||||
|
// La hardcore techno di Andrea (Suno, 31 ottobre) alla console del campo; da lontano la house della Piazza e la tecno.
|
||||||
|
effects: [{ type: 'crowd', phase: 'ingorgo', area: [0, 235, 640, 55], bpm: 180, amount: 3 }, { type: 'crowd', phase: 'ingorgo', area: [0, 290, 640, 70], bpm: 180, amount: 6 },
|
||||||
|
{ type: 'lasers', phase: 'ingorgo', at: [300, 160], count: 9, color: '255,40,40', len: 440 },
|
||||||
|
{ type: 'stagelights', phase: 'ingorgo', from: [[262, 152], [338, 152]], wash: [300, 172, 160], bpm: 180, beams: 2, strobe: 8, pulse: 0.1, colors: ['255,40,40', '255,255,255', '255,90,40'] },
|
||||||
|
{ type: 'floorspots', phase: 'ingorgo', area: [40, 240, 560, 110], bpm: 180, count: 7, colors: ['255,40,40', '255,255,255', '255,90,40'] }],
|
||||||
|
stages: [
|
||||||
|
{ when: 'ingorgo', music: 'assets/audio/ingorgo-hardcore.mp3', at: [300, 250], near: 150, far: 800 },
|
||||||
|
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [140, 60], far: 700 },
|
||||||
|
{ when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [-250, 100], far: 800 },
|
||||||
|
],
|
||||||
walk: [[40, 360], [40, 320], [60, 280], [100, 272], [150, 276], [300, 300], [360, 318], [640, 345], [640, 360]],
|
walk: [[40, 360], [40, 320], [60, 280], [100, 272], [150, 276], [300, 300], [360, 318], [640, 345], [640, 360]],
|
||||||
depth: [[272, 0.45], [360, 1.15]],
|
depth: [[272, 0.45], [360, 1.15]],
|
||||||
entry: [300, 345],
|
entry: [300, 345],
|
||||||
@@ -1591,6 +1627,20 @@ export const story = {
|
|||||||
name: 'Via Calimara',
|
name: 'Via Calimara',
|
||||||
map: [36, 58],
|
map: [36, 58],
|
||||||
background: 'assets/scenes/via-calimara.png',
|
background: 'assets/scenes/via-calimara.png',
|
||||||
|
backdrops: [{ when: 'ingorgo', src: 'assets/scenes/ingorgo-via-calimara.png' }], // Codex, la notte dell'Ingorgo (31 ottobre)
|
||||||
|
// All'Ingorgo anche Via Calimara (Andrea, 31 ottobre): la console nel giardino sotto i cedri, con la palla a
|
||||||
|
// specchi appesa al ramo, e la strada piena fino in fondo, con la folla che salta (Andrea: «mi raccomando»).
|
||||||
|
// La musica anni '90 di Andrea (Suno, 31 ottobre) alla console del giardino; da lontano la house della Piazza
|
||||||
|
// e il latino del Comune.
|
||||||
|
effects: [{ type: 'crowd', phase: 'ingorgo', area: [0, 190, 640, 75], bpm: 130, amount: 3 }, { type: 'crowd', phase: 'ingorgo', area: [0, 265, 640, 95], bpm: 130, amount: 5 },
|
||||||
|
{ type: 'mirrorball', phase: 'ingorgo', at: [170, 123], hang: 96, from: [104, 178], surfaces: [[0, 40, 300, 140], [0, 190, 640, 170], [560, 90, 80, 110]], bpm: 130, count: 64 },
|
||||||
|
{ type: 'stagelights', phase: 'ingorgo', from: [[54, 168], [154, 168]], wash: [104, 185, 150], bpm: 130, beams: 2, strobe: 4, pulse: 0.08, colors: ['255,90,180', '255,170,60', '255,220,120', '200,90,255'] },
|
||||||
|
{ type: 'floorspots', phase: 'ingorgo', area: [40, 220, 560, 130], bpm: 130, count: 7, colors: ['255,90,180', '255,170,60', '255,220,120', '200,90,255'] }],
|
||||||
|
stages: [
|
||||||
|
{ when: 'ingorgo', music: 'assets/audio/ingorgo-anni90.mp3', at: [104, 230], near: 150, far: 800 },
|
||||||
|
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [365, 60], far: 700 },
|
||||||
|
{ when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [-300, 250], far: 800 },
|
||||||
|
],
|
||||||
walk: [[0, 360], [0, 318], [330, 206], [400, 206], [560, 300], [600, 360]],
|
walk: [[0, 360], [0, 318], [330, 206], [400, 206], [560, 300], [600, 360]],
|
||||||
depth: [[206, 0.3], [360, 1.15]],
|
depth: [[206, 0.3], [360, 1.15]],
|
||||||
entry: [300, 350],
|
entry: [300, 350],
|
||||||
@@ -1628,7 +1678,9 @@ export const story = {
|
|||||||
id: 'river', name: 'il River', box: [60, 128, 190, 110], at: [160, 320], face: 'up',
|
id: 'river', name: 'il River', box: [60, 128, 190, 110], at: [160, 320], face: 'up',
|
||||||
actions: {
|
actions: {
|
||||||
look: [{ say: 'Il River, il bar della rotonda. Fuori gli ombrelloni, dentro i flipper e le figurine.' }],
|
look: [{ say: 'Il River, il bar della rotonda. Fuori gli ombrelloni, dentro i flipper e le figurine.' }],
|
||||||
open: [{ say: 'Un ghiacciolo ci starebbe. Ma prima il palio, poi i ghiaccioli.' }],
|
// Dentro, il flipper (Andrea, 31 ottobre): si entra e si sceglie come giocarlo.
|
||||||
|
open: [{ say: 'Un ghiacciolo ci starebbe. Ma prima... il flipper.' }, { dialogue: 'flipper' }],
|
||||||
|
use: [{ say: 'Dentro, in fondo, fra le figurine: il flipper. Con San Piero dipinto sulla tavola.' }, { dialogue: 'flipper' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1901,11 +1953,13 @@ export const story = {
|
|||||||
{ type: 'pixelspots', phase: 'ingorgo', area: [130, 242, 490, 108], bpm: 122, count: 5, size: 20, colors: ['155,65,255', '65,110,255'] },
|
{ type: 'pixelspots', phase: 'ingorgo', area: [130, 242, 490, 108], bpm: 122, count: 5, size: 20, colors: ['155,65,255', '65,110,255'] },
|
||||||
],
|
],
|
||||||
stages: [
|
stages: [
|
||||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [565, 235] },
|
// Giù a destra, sul marciapiede sotto il giardinetto, si arriva sotto la console: lì solo l'underground.
|
||||||
|
{ when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [565, 235], near: 190 },
|
||||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [270, 120], far: 600 },
|
{ when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [270, 120], far: 600 },
|
||||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [240, 640], far: 750 },
|
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [240, 640], far: 750 },
|
||||||
],
|
],
|
||||||
walk: [[0, 360], [0, 322], [190, 226], [283, 222], [470, 360]],
|
// Col marciapiede di destra, fra le catenelle e il giardinetto, fino in fondo (Andrea, 31 ottobre).
|
||||||
|
walk: [[0, 360], [0, 322], [190, 226], [283, 222], [350, 236], [452, 272], [640, 335], [640, 360]],
|
||||||
depth: [[222, 0.35], [360, 1.15]],
|
depth: [[222, 0.35], [360, 1.15]],
|
||||||
entry: [240, 350],
|
entry: [240, 350],
|
||||||
hotspots: [
|
hotspots: [
|
||||||
@@ -3169,8 +3223,16 @@ export const story = {
|
|||||||
id: 'zio_mario', name: 'lo zio Mario', box: [418, 206, 46, 124], at: [362, 334], face: 'right', default: 'look', when: '!indizio_regolo',
|
id: 'zio_mario', name: 'lo zio Mario', box: [418, 206, 46, 124], at: [362, 334], face: 'right', default: 'look', when: '!indizio_regolo',
|
||||||
actions: {
|
actions: {
|
||||||
look: [{ say: 'Lo zio Mario. Guarda il muro, guarda la macchina, e si gratta la testa sotto il berretto.' }],
|
look: [{ say: 'Lo zio Mario. Guarda il muro, guarda la macchina, e si gratta la testa sotto il berretto.' }],
|
||||||
// Il primo scambio con lo zio (Andrea, 29 ottobre).
|
// Il primo scambio con lo zio (Andrea, 29 ottobre); «Siamo a cavallo al ciuco», «Menomale tu ci sei te» e
|
||||||
talk: [{ say: 'Zio, hai problemi?' }, { say: 'I problemi io gli avevo quando andavo a scuola!', who: 'zio_mario', pose: 'laugh' }],
|
// «Alla terza tu capisci subito» sono di Andrea (31 ottobre). [DA ANDREA: la domanda sulla Ghiacciomobile]
|
||||||
|
talk: [
|
||||||
|
{ say: 'Zio, hai problemi?' },
|
||||||
|
{ say: 'I problemi io gli avevo quando andavo a scuola!', who: 'zio_mario', pose: 'laugh' },
|
||||||
|
{ say: 'E con la Ghiacciomobile nel muro, come si fa?' },
|
||||||
|
{ say: 'Siamo a cavallo al ciuco!', who: 'zio_mario', pose: 'explain' },
|
||||||
|
{ say: 'Menomale tu ci sei te, Zio!' },
|
||||||
|
{ say: 'Alla terza tu capisci subito, al volo!', who: 'zio_mario', pose: 'laugh' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -3661,6 +3723,11 @@ export const story = {
|
|||||||
{ say: 'No, dormo!', who: 'zio_mario', pose: 'laugh' },
|
{ say: 'No, dormo!', who: 'zio_mario', pose: 'laugh' },
|
||||||
{ say: 'Zio, a me mi garba Michael Jackson!' },
|
{ say: 'Zio, a me mi garba Michael Jackson!' },
|
||||||
{ say: 'Mai col gesso!', who: 'zio_mario', pose: 'laugh' },
|
{ say: 'Mai col gesso!', who: 'zio_mario', pose: 'laugh' },
|
||||||
|
// Il posto lontanissimo dello zio (Andrea, 31 ottobre).
|
||||||
|
{ say: 'Sai, Zio? Son stato in California!' },
|
||||||
|
{ say: 'E dov\'è?', who: 'zio_mario', pose: 'think' },
|
||||||
|
{ say: 'In America!' },
|
||||||
|
{ say: 'Ahhh... sopra Firenzola!', who: 'zio_mario', pose: 'point' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -3734,7 +3801,8 @@ export const story = {
|
|||||||
id: 'bruciatore', name: 'il cilindro delle bruciate', box: [192, 188, 68, 54], at: [226, 262], face: 'up',
|
id: 'bruciatore', name: 'il cilindro delle bruciate', box: [192, 188, 68, 54], at: [226, 262], face: 'up',
|
||||||
actions: {
|
actions: {
|
||||||
look: [{ say: 'Il cilindro delle bruciate: si gira la manovella sopra il fuoco, e le castagne ci ballano dentro.' }],
|
look: [{ say: 'Il cilindro delle bruciate: si gira la manovella sopra il fuoco, e le castagne ci ballano dentro.' }],
|
||||||
use: [{ say: 'Si gira piano, sennò le bruciate si bruciano davvero.' }],
|
// Il minigioco delle bruciate: la manovella si gira coi volontari (Andrea, 31 ottobre).
|
||||||
|
use: [{ say: 'Posso girare io la manovella?' }, { dialogue: 'volontari' }],
|
||||||
take: [{ say: 'Una bruciata calda, presa al volo. Scotta! Ma è buonissima.' }],
|
take: [{ say: 'Una bruciata calda, presa al volo. Scotta! Ma è buonissima.' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -3763,11 +3831,7 @@ export const story = {
|
|||||||
id: 'volontari', name: 'i volontari della Misericordia', box: [130, 148, 97, 114], at: [200, 272], face: 'left', default: 'talk',
|
id: 'volontari', name: 'i volontari della Misericordia', box: [130, 148, 97, 114], at: [200, 272], face: 'left', default: 'talk',
|
||||||
actions: {
|
actions: {
|
||||||
look: [{ say: 'I volontari della Misericordia, alle bruciate: uno rimescola, l\'altro riempie i cartocci.' }],
|
look: [{ say: 'I volontari della Misericordia, alle bruciate: uno rimescola, l\'altro riempie i cartocci.' }],
|
||||||
talk: [
|
talk: [{ dialogue: 'volontari' }],
|
||||||
{ say: 'Che Iddio ve ne renda merito!! A voi e agli alpini!' },
|
|
||||||
{ say: 'E a te, figliolo. To\', un cartoccio: son belle calde.', who: 'volontari', pose: 'give' },
|
|
||||||
{ say: 'Scottano! Ma son buonissime.' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -4237,6 +4301,30 @@ export const story = {
|
|||||||
},
|
},
|
||||||
// Il BaronDeDuke nella discesa verso la polisportiva: le mutande perse a poker, e il Muro del Sanpierino sul
|
// Il BaronDeDuke nella discesa verso la polisportiva: le mutande perse a poker, e il Muro del Sanpierino sul
|
||||||
// telefonino (Andrea, 31 ottobre: lui è il webmaster di sanpiero50037.it).
|
// telefonino (Andrea, 31 ottobre: lui è il webmaster di sanpiero50037.it).
|
||||||
|
// I volontari della Misericordia alle bruciate della Smarronata, 1993: il ringraziamento di Andrea e la manovella
|
||||||
|
// (il minigioco delle bruciate, Andrea, 31 ottobre). [DA ANDREA: le battute sono provvisorie]
|
||||||
|
volontari: {
|
||||||
|
nodes: {
|
||||||
|
start: { options: [
|
||||||
|
{ text: 'Che Iddio ve ne renda merito!! A voi e agli alpini!', then: [
|
||||||
|
{ say: 'E a te, figliolo. To\', un cartoccio: son belle calde.', who: 'volontari', pose: 'give' },
|
||||||
|
{ say: 'Scottano! Ma son buonissime.' },
|
||||||
|
] },
|
||||||
|
{ text: 'Vi do una mano alla manovella?', next: 'livello', then: [
|
||||||
|
{ say: 'Su, giovanotto, che noi s\'ha le braccia stanche!', who: 'volontari', pose: 'talk' },
|
||||||
|
{ say: 'Si gira sempre uguale: piano piano sotto si bruciano, a furia volan nel fuoco. Quando son cotte, nel sacco.', who: 'volontari', pose: 'explain' },
|
||||||
|
{ say: 'Come te la senti?', who: 'volontari', pose: 'talk' },
|
||||||
|
] },
|
||||||
|
{ text: 'Buon lavoro!', next: 'end' },
|
||||||
|
] },
|
||||||
|
livello: { options: [
|
||||||
|
{ text: 'Alla bona.', next: 'end', then: [{ say: 'Alla bona: ti si guarda noi, e se si bruciano un si dice a nessuno.', who: 'volontari', pose: 'talk' }, { closeup: 'bruciate', level: 'bona' }] },
|
||||||
|
{ text: 'Seria.', next: 'end', then: [{ say: 'Seria: la gente è in fila, e il cartoccio lo vole bello pieno.', who: 'volontari', pose: 'explain' }, { closeup: 'bruciate', level: 'seria' }] },
|
||||||
|
{ text: 'A fassi male.', next: 'end', then: [{ say: 'A fassi male? Allora ci si mette anche il fuoco forte.', who: 'volontari', pose: 'talk' }, { closeup: 'bruciate', level: 'male' }] },
|
||||||
|
{ text: 'No, dopo.', next: 'start' },
|
||||||
|
] },
|
||||||
|
},
|
||||||
|
},
|
||||||
barondeduke: {
|
barondeduke: {
|
||||||
nodes: {
|
nodes: {
|
||||||
start: { options: [
|
start: { options: [
|
||||||
@@ -4365,6 +4453,19 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Da solo al campo del Circolo: Andrea decide quanto farsi male.
|
// Da solo al campo del Circolo: Andrea decide quanto farsi male.
|
||||||
|
// Il flipper del River: Andrea da solo davanti alla tavola, sceglie il livello. [DA ANDREA: battute provvisorie]
|
||||||
|
flipper: {
|
||||||
|
nodes: {
|
||||||
|
start: {
|
||||||
|
options: [
|
||||||
|
{ text: 'Una partita alla bona, tanto per sentire la molla.', next: 'end', then: [{ say: 'Seimila punti e sono contento.' }, { closeup: 'flipper', level: 'bona' }] },
|
||||||
|
{ text: 'Una partita seria.', next: 'end', then: [{ say: 'Diecimila. Come quando ci lasciavo le cinquecento lire.' }, { closeup: 'flipper', level: 'seria' }] },
|
||||||
|
{ text: 'A fassi male.', next: 'end', then: [{ say: 'Sedicimila. Il record del River, o quasi.' }, { closeup: 'flipper', level: 'male' }] },
|
||||||
|
{ text: 'Un\'altra volta.', next: 'end' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
basket: {
|
basket: {
|
||||||
nodes: {
|
nodes: {
|
||||||
start: {
|
start: {
|
||||||
|
|||||||
+1
-1
@@ -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.63';
|
export const VERSION = '0.1.66';
|
||||||
|
|
||||||
// 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).
|
||||||
|
|||||||
@@ -62,16 +62,17 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
|
|||||||
/* La guida semplice del GP: il Gas si tiene premuto col pollice, più alto. */
|
/* La guida semplice del GP: il Gas si tiene premuto col pollice, più alto. */
|
||||||
#bocce-throw.hold { min-height: 72px; font-size: 14px; }
|
#bocce-throw.hold { min-height: 72px; font-size: 14px; }
|
||||||
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
|
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
|
||||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help, #campana-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
#bocce-left, #bocce-right { touch-action: none; }
|
||||||
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary, #gara-help summary, #rinquarto-help summary, #micro-help summary, #cerbottana-help, #condor-help summary, #campana-help summary { cursor: pointer; padding: 6px 0; }
|
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help, #campana-help, #bruciate-help, #flipper-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
||||||
#bocce-help p, #biliardo-help p, #basket-help p, #pesca-help p, #gara-help p, #rinquarto-help p, #micro-help p, #cerbottana-help, #condor-help p, #campana-help p { max-width: 76em; line-height: 1.7; }
|
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary, #gara-help summary, #rinquarto-help summary, #micro-help summary, #cerbottana-help, #condor-help summary, #campana-help summary, #bruciate-help summary, #flipper-help summary { cursor: pointer; padding: 6px 0; }
|
||||||
|
#bocce-help p, #biliardo-help p, #basket-help p, #pesca-help p, #gara-help p, #rinquarto-help p, #micro-help p, #cerbottana-help, #condor-help p, #campana-help p, #bruciate-help p, #flipper-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, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help, #campana-help { font-size: 9px; }
|
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help, #campana-help, #bruciate-help, #flipper-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); }
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// Le bruciate nel browser vero: dai volontari della Smarronata, la manovella girata col mouse e col dito, il sacco.
|
||||||
|
// npm run browser:bruciate — schermate in /tmp/regolo-bruciate.
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import { mkdirSync } from 'node:fs';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { staticServer } from './helpers.mjs';
|
||||||
|
|
||||||
|
const out = process.env.OUT ?? '/tmp/regolo-bruciate';
|
||||||
|
mkdirSync(out, { recursive: true });
|
||||||
|
const server = await staticServer();
|
||||||
|
let browser;
|
||||||
|
const errors = [];
|
||||||
|
try {
|
||||||
|
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
||||||
|
for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) {
|
||||||
|
const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile });
|
||||||
|
page.on('pageerror', e => errors.push(e.message));
|
||||||
|
await page.goto(`http://127.0.0.1:${server.port}/`);
|
||||||
|
await page.click('#splash'); await page.click('#new-game'); await page.click('#rione [data-rione="campomigliaio"]'); await page.click('#skip-intro');
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
const { settle } = await import('/tests/helpers.mjs');
|
||||||
|
settle(game); game.go('smarronata_1993'); settle(game);
|
||||||
|
game.act('talk', 'volontari'); settle(game);
|
||||||
|
game.choose(game.options().findIndex(o => o.text === 'Vi do una mano alla manovella?')); settle(game);
|
||||||
|
game.choose(game.options().findIndex(o => o.text === 'Alla bona.')); settle(game);
|
||||||
|
});
|
||||||
|
await page.waitForSelector('#bocce-controls:not([hidden])');
|
||||||
|
await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/smarronata-bruciatore.png'));
|
||||||
|
assert.equal(await page.locator('#bocce-throw').textContent(), 'Rovescia nel sacco');
|
||||||
|
assert.ok(await page.locator('#bruciate-help').isVisible());
|
||||||
|
assert.ok(!(await page.locator('#bocce-left').isVisible()), 'niente pulsanti Mira');
|
||||||
|
assert.ok(!(await page.locator('#bocce-power').isVisible()), 'niente forza');
|
||||||
|
await page.screenshot({ path: `${out}/${name}-ferma.png` });
|
||||||
|
const box = await page.locator('#scene').boundingBox(), P = (x, y) => [box.x + x / 640 * box.width, box.y + y / 360 * box.height];
|
||||||
|
// I giri attorno alla manovella: col mouse tenendo premuto, col dito trascinando (eventi di puntatore veri).
|
||||||
|
const loops = async (turns, steps = 16) => {
|
||||||
|
const at = i => { const a = i / steps * Math.PI * 2; return P(506 + Math.cos(a) * 40, 190 + Math.sin(a) * 40); };
|
||||||
|
if (mobile) {
|
||||||
|
const cdp = await page.context().newCDPSession(page), touch = (type, [x, y]) => cdp.send('Input.dispatchTouchEvent', { type, touchPoints: type === 'touchEnd' ? [] : [{ x, y }] });
|
||||||
|
await touch('touchStart', at(0));
|
||||||
|
for (let i = 1; i <= turns * steps; i++) await touch('touchMove', at(i));
|
||||||
|
await touch('touchEnd', at(0));
|
||||||
|
} else {
|
||||||
|
await page.mouse.move(...at(0)); await page.mouse.down();
|
||||||
|
for (let i = 1; i <= turns * steps; i++) await page.mouse.move(...at(i));
|
||||||
|
await page.mouse.up();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await loops(3);
|
||||||
|
const turned = await page.evaluate(() => Math.abs(game.closeup.drum));
|
||||||
|
assert.ok(turned > 6, `il cilindro ha girato: ${turned.toFixed(1)} rad`);
|
||||||
|
await page.screenshot({ path: `${out}/${name}-gira.png` });
|
||||||
|
// Il resto della cottura lo si fa fare al gioco a velocità giusta (le schermate con swiftshader sono lente).
|
||||||
|
await page.evaluate(() => { const c = game.closeup; c.speed = 4.5; for (const k of c.chestnuts) k.cook = 1.05; c.t = Math.max(c.t, 5); });
|
||||||
|
await page.waitForFunction(() => !document.querySelector('#bocce-throw').disabled);
|
||||||
|
await page.screenshot({ path: `${out}/${name}-cotte.png` });
|
||||||
|
if (mobile) await page.locator('#bocce-throw').tap(); else await page.locator('#bocce-throw').click();
|
||||||
|
await page.waitForFunction(() => game.closeup?.step === 'result', null, { timeout: 10000 });
|
||||||
|
assert.match(await page.locator('#bocce-status').textContent(), /bruciate buone su 18/);
|
||||||
|
await page.screenshot({ path: `${out}/${name}-sacco.png` });
|
||||||
|
await page.locator('#closeup-close').click();
|
||||||
|
await page.waitForFunction(() => game.state.flags.bruciate_fatte);
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
console.log(`ok: schermate in ${out}`);
|
||||||
|
} finally { await browser?.close(); server.kill(); }
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as m from '../src/engine/bruciate.js';
|
||||||
|
import { drawBruciate } from '../src/engine/bruciate-render.js';
|
||||||
|
import { Game } from '../src/engine/game.js';
|
||||||
|
import { story } from '../src/story/story.js';
|
||||||
|
import { settle } from './helpers.mjs';
|
||||||
|
|
||||||
|
// Gira la manovella tenendola, a `w` radianti al secondo, finché le castagne non paiono cotte (o per `secs` secondi).
|
||||||
|
function crank(c, w, { secs = 60, until = 1.2 } = {}) {
|
||||||
|
c.grab ??= { pointerId: 1, heading: null, last: null };
|
||||||
|
for (let i = 0; i < secs * 60 && c.step === 'turn' && m.doneness(c) < until; i++) { c.pending = w / 60; m.update(c, 1 / 60); }
|
||||||
|
}
|
||||||
|
function pourAndWait(c) {
|
||||||
|
assert.ok(m.shoot(c), 'si rovescia nel sacco');
|
||||||
|
for (let i = 0; i < 200 && c.step === 'pour'; i++) m.update(c, 1 / 60);
|
||||||
|
assert.equal(c.step, 'result');
|
||||||
|
return c.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('girando a modo le bruciate vengono tutte buone, nel tempo del livello', () => {
|
||||||
|
for (const level of ['bona', 'seria', 'male']) {
|
||||||
|
const c = m.newBruciate(42, { level });
|
||||||
|
crank(c, 4.5);
|
||||||
|
assert.ok(c.t > 6 && c.t < m.LEVELS[level].time, `${level}: cotte in ${c.t.toFixed(1)} s`);
|
||||||
|
const r = pourAndWait(c);
|
||||||
|
assert.equal(r.winner, 'player', `${level}: ${JSON.stringify(r)}`);
|
||||||
|
assert.equal(r.spilled, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ferme sul fuoco: quelle di sotto carbone, quelle di sopra crude', () => {
|
||||||
|
const c = m.newBruciate(42, { level: 'seria' });
|
||||||
|
crank(c, 0);
|
||||||
|
const r = pourAndWait(c);
|
||||||
|
assert.equal(r.winner, 'cpu');
|
||||||
|
assert.ok(r.burnt > 0 && r.raw > 0, JSON.stringify(r));
|
||||||
|
assert.match(m.status(c), /Ne servivano 15/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('troppo forte è una centrifuga: le castagne volano nel fuoco', () => {
|
||||||
|
const c = m.newBruciate(42, { level: 'seria' });
|
||||||
|
crank(c, 12, { secs: 4, until: 9 });
|
||||||
|
assert.ok(c.chestnuts.some(k => k.out), 'qualcuna è volata fuori');
|
||||||
|
assert.ok(c.flying.length > 0);
|
||||||
|
assert.match(c.line, /centrifuga|fuoco/);
|
||||||
|
assert.ok(c.events.some(e => e.kind === 'bruciate-cade'));
|
||||||
|
assert.equal(m.speedWord(c), 'troppo forte');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rovesciarle troppo presto: crude; e il pulsante aspetta i primi secondi', () => {
|
||||||
|
const c = m.newBruciate(42, { level: 'bona' });
|
||||||
|
assert.ok(!m.canThrow(c) && !m.shoot(c), 'appena cominciato non si rovescia');
|
||||||
|
crank(c, 4.5, { until: 0.4 });
|
||||||
|
const r = pourAndWait(c);
|
||||||
|
assert.equal(r.winner, 'cpu'); assert.ok(r.raw > 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il gesto: i giri, grandi o piccoli e dovunque, fanno girare il cilindro; una riga dritta no', () => {
|
||||||
|
const circles = (c, cx, cy, radius, turns, secs) => {
|
||||||
|
assert.ok(m.press(c, 7));
|
||||||
|
const steps = Math.round(secs * 60);
|
||||||
|
for (let i = 0; i <= steps; i++) {
|
||||||
|
const a = i / steps * turns * Math.PI * 2;
|
||||||
|
m.aimScreen(c, [cx + Math.cos(a) * radius, cy + Math.sin(a) * radius]);
|
||||||
|
m.update(c, 1 / 60);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const [x, y, r] of [[506, 190, 30], [200, 300, 80], [320, 180, 15]]) {
|
||||||
|
const c = m.newBruciate(1);
|
||||||
|
m.aimScreen(c, [x + r, y]);
|
||||||
|
circles(c, x, y, r, 2, 2);
|
||||||
|
assert.ok(Math.abs(c.speed) > 4 && Math.abs(c.speed) < 9, `giri di raggio ${r}: ${c.speed.toFixed(2)} rad/s`);
|
||||||
|
assert.ok(Math.abs(c.drum) > 6);
|
||||||
|
// Lasciata la manovella, il cilindro rallenta da sé.
|
||||||
|
assert.ok(m.release(c, 7));
|
||||||
|
const w = Math.abs(c.speed);
|
||||||
|
for (let i = 0; i < 60; i++) m.update(c, 1 / 60);
|
||||||
|
assert.ok(Math.abs(c.speed) < w * 0.5);
|
||||||
|
}
|
||||||
|
const c = m.newBruciate(1);
|
||||||
|
m.aimScreen(c, [100, 200]); m.press(c, 1);
|
||||||
|
for (let i = 0; i < 120; i++) { m.aimScreen(c, [100 + i * 3, 200]); m.update(c, 1 / 60); }
|
||||||
|
assert.ok(Math.abs(c.speed) < 0.2, 'trascinare dritto non gira');
|
||||||
|
assert.ok(!m.release(c, 99), 'un altro dito non lascia la manovella');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('senza gesto: Spazio e le frecce danno una spinta, Invio rovescia', () => {
|
||||||
|
const c = m.newBruciate(3);
|
||||||
|
assert.ok(m.key(c, ' ', true));
|
||||||
|
assert.ok(Math.abs(c.speed) >= m.PUSH);
|
||||||
|
assert.ok(m.key(c, 'ArrowRight', true)); assert.ok(Math.abs(c.speed) >= m.PUSH * 1.5);
|
||||||
|
assert.ok(m.key(c, ' ', false), 'anche lasciando il tasto, il tasto è suo');
|
||||||
|
assert.ok(!m.key(c, 'x', true));
|
||||||
|
for (let i = 0; i < 60 * m.MIN_POUR + 2; i++) m.update(c, 1 / 60);
|
||||||
|
m.key(c, 'Enter', true);
|
||||||
|
assert.equal(c.step, 'pour');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il disegno delle bruciate non cambia lo stato', () => {
|
||||||
|
const calls = [], ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => calls.push([k, ...args]) });
|
||||||
|
const c = m.newBruciate(7, { level: 'bona' }), bg = {};
|
||||||
|
const draw = () => { const before = structuredClone(c); drawBruciate(ctx, c, story.closeups.bruciate, bg); assert.deepEqual(c, before); };
|
||||||
|
draw();
|
||||||
|
assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'GIRA!'), 'da fermo, l\'invito a girare');
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'COTTURA'));
|
||||||
|
crank(c, 12, { secs: 2, until: 9 }); draw();
|
||||||
|
crank(c, 4.5); pourAndWait(c); calls.length = 0; draw();
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && /BRUCIATE/.test(t)));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('alla Smarronata: dai volontari, «Vi do una mano alla manovella?», il livello e le bruciate', () => {
|
||||||
|
const g = new Game(story); g.go('smarronata_1993'); settle(g);
|
||||||
|
g.act('talk', 'volontari'); settle(g);
|
||||||
|
assert.ok(g.options().some(o => o.text.startsWith('Che Iddio ve ne renda merito')), 'il ringraziamento resta');
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'Vi do una mano alla manovella?')); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'Seria.')); settle(g);
|
||||||
|
assert.equal(g.closeup?.id, 'bruciate'); assert.equal(g.closeup.level, 'seria');
|
||||||
|
crank(g.closeup, 4.5); pourAndWait(g.closeup);
|
||||||
|
g.closeupClose(); settle(g);
|
||||||
|
assert.ok(g.state.flags.bruciate_fatte);
|
||||||
|
// Anche usando il cilindro si chiede la manovella; uscendo prima, la battuta di chi lascia.
|
||||||
|
g.act('use', 'bruciatore'); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'Vi do una mano alla manovella?')); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'Alla bona.')); settle(g);
|
||||||
|
assert.equal(g.closeup?.id, 'bruciate');
|
||||||
|
g.closeupClose();
|
||||||
|
assert.equal(g.queue[0]?.say, story.closeups.bruciate.quit[0].say);
|
||||||
|
});
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
// Il flipper del River nel browser vero: dal bar, la molla col mouse e col dito, i flipper con due dita sulla scena,
|
||||||
|
// coi pulsanti e con le frecce, la pausa che lascia tutto, il risultato. npm run browser:flipper — schermate in
|
||||||
|
// /tmp/regolo-flipper.
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import { mkdirSync } from 'node:fs';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { staticServer } from './helpers.mjs';
|
||||||
|
|
||||||
|
const out = process.env.OUT ?? '/tmp/regolo-flipper';
|
||||||
|
mkdirSync(out, { recursive: true });
|
||||||
|
const server = await staticServer();
|
||||||
|
let browser;
|
||||||
|
const errors = [];
|
||||||
|
try {
|
||||||
|
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
||||||
|
for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) {
|
||||||
|
const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile });
|
||||||
|
page.on('pageerror', e => errors.push(e.message));
|
||||||
|
await page.goto(`http://127.0.0.1:${server.port}/`);
|
||||||
|
await page.click('#splash'); await page.click('#new-game'); await page.click('#rione [data-rione="campomigliaio"]'); await page.click('#skip-intro');
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
const { settle } = await import('/tests/helpers.mjs');
|
||||||
|
settle(game); game.go('via_provinciale'); settle(game);
|
||||||
|
game.act('use', 'river'); settle(game);
|
||||||
|
game.choose(game.options().findIndex(o => o.text.startsWith('Una partita alla bona'))); settle(game);
|
||||||
|
});
|
||||||
|
await page.waitForSelector('#bocce-controls:not([hidden])');
|
||||||
|
await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/river-flipper.png'));
|
||||||
|
assert.equal(await page.locator('#bocce-throw').textContent(), 'Lancia');
|
||||||
|
assert.ok(await page.locator('#flipper-help').isVisible());
|
||||||
|
assert.equal(await page.locator('#bocce-left').textContent(), '← Flipper');
|
||||||
|
assert.ok(!(await page.locator('#bocce-left').isDisabled()), 'i pulsanti dei flipper sono attivi anche prima del lancio');
|
||||||
|
await page.screenshot({ path: `${out}/${name}-tavola.png` });
|
||||||
|
const box = await page.locator('#scene').boundingBox(), P = (x, y) => [box.x + x / 640 * box.width, box.y + y / 360 * box.height];
|
||||||
|
const cdp = mobile ? await page.context().newCDPSession(page) : null;
|
||||||
|
const touch = (type, points) => cdp.send('Input.dispatchTouchEvent', { type, touchPoints: points.map(([x, y]) => ({ x, y })) });
|
||||||
|
// La molla: tenendo premuto sulla scena, col mouse e col dito.
|
||||||
|
if (mobile) {
|
||||||
|
await touch('touchStart', [P(440, 320)]);
|
||||||
|
await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.3, null, { polling: 'raf' });
|
||||||
|
await touch('touchEnd', []);
|
||||||
|
} else {
|
||||||
|
await page.mouse.move(...P(440, 320)); await page.mouse.down();
|
||||||
|
await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.3, null, { polling: 'raf' });
|
||||||
|
await page.mouse.up();
|
||||||
|
}
|
||||||
|
await page.waitForFunction(() => game.closeup && !game.closeup.ball?.waiting, null, { timeout: 5000 });
|
||||||
|
// Due dita sulla scena: sinistra, poi anche destra, poi si alza la sinistra e la destra resta su.
|
||||||
|
if (mobile) {
|
||||||
|
await touch('touchStart', [P(250, 200)]);
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length > 0);
|
||||||
|
await touch('touchStart', [P(250, 200), P(400, 200)]);
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.right.held.length > 0 && game.closeup.flippers.left.held.length > 0, null, { timeout: 3000 });
|
||||||
|
await page.screenshot({ path: `${out}/${name}-flipper.png` });
|
||||||
|
await touch('touchEnd', [P(400, 200)]);
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length === 0 && game.closeup.flippers.right.held.length > 0, null, { timeout: 3000 });
|
||||||
|
await touch('touchEnd', []);
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.right.held.length === 0);
|
||||||
|
// I pulsanti tenuti premuti.
|
||||||
|
const b = await page.locator('#bocce-left').boundingBox();
|
||||||
|
await touch('touchStart', [[b.x + b.width / 2, b.y + b.height / 2]]);
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length > 0);
|
||||||
|
await touch('touchEnd', []);
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length === 0);
|
||||||
|
} else {
|
||||||
|
await page.mouse.move(...P(400, 200)); await page.mouse.down();
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.right.held.length > 0);
|
||||||
|
await page.mouse.down({ button: 'right' });
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.right.held.length > 1, null, { timeout: 3000 });
|
||||||
|
await page.screenshot({ path: `${out}/${name}-flipper.png` });
|
||||||
|
await page.mouse.up(); await page.mouse.up({ button: 'right' });
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.right.held.length === 0);
|
||||||
|
await page.keyboard.down('ArrowLeft');
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length > 0);
|
||||||
|
// La pausa lascia tutto: tornando, il flipper è giù anche se il tasto era premuto.
|
||||||
|
await page.click('#pause-button');
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length === 0);
|
||||||
|
await page.keyboard.up('ArrowLeft');
|
||||||
|
await page.click('#resume');
|
||||||
|
await page.waitForFunction(() => document.querySelector('#pause').hidden);
|
||||||
|
}
|
||||||
|
// La fine: si fa finire la partita e si guarda il quadro.
|
||||||
|
await page.evaluate(() => { const c = game.closeup; c.score = 7350; c.step = 'result'; c.ball = null; c.result = { winner: 'player', score: 7350, goal: c.skill.goal }; });
|
||||||
|
await page.waitForFunction(() => document.querySelector('#bocce-throw').textContent === 'Finita');
|
||||||
|
assert.match(await page.locator('#bocce-status').textContent(), /7350 punti/);
|
||||||
|
await page.screenshot({ path: `${out}/${name}-fine.png` });
|
||||||
|
if (mobile) await page.locator('#closeup-close').click(); else await page.keyboard.press('Enter'); // Invio chiude il risultato
|
||||||
|
await page.waitForFunction(() => game.state.flags.flipper_vinto && !game.closeup);
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
console.log(`ok: schermate in ${out}`);
|
||||||
|
} finally { await browser?.close(); server.kill(); }
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as m from '../src/engine/flipper.js';
|
||||||
|
import { drawFlipper } from '../src/engine/flipper-render.js';
|
||||||
|
import { Game } from '../src/engine/game.js';
|
||||||
|
import { story } from '../src/story/story.js';
|
||||||
|
import { settle } from './helpers.mjs';
|
||||||
|
|
||||||
|
const run = (c, secs) => { for (let i = 0; i < secs * 60; i++) m.update(c, 1 / 60); };
|
||||||
|
const put = (c, ball) => Object.assign(c.ball, ball);
|
||||||
|
// Il giocatore automatico: alza il flipper quando la pallina gli scende addosso, come un umano attento.
|
||||||
|
function autoplay(c, secs = 600) {
|
||||||
|
for (let i = 0; i < secs * 60 && c.step === 'play'; i++) {
|
||||||
|
if (m.waiting(c)) m.launch(c, 0.75);
|
||||||
|
const b = c.ball;
|
||||||
|
for (const side of ['left', 'right']) {
|
||||||
|
const [px, py] = m.PIVOT[side], dir = side === 'left' ? 1 : -1, rel = (b ? b.x - px : 0) * dir;
|
||||||
|
const near = b && !b.waiting && b.vy > 0 && b.y > py - 34 && b.y < py + 14 && rel > -6 && rel < 44;
|
||||||
|
if (near !== c.flippers[side].held.includes('ai')) m.hold(c, side, 'ai', near);
|
||||||
|
}
|
||||||
|
m.update(c, 1 / 60);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('la tavola: tutto sta dentro il campo, le corsie coprono tutta la larghezza sotto l\'arco, i funghi sui dischi del disegno', () => {
|
||||||
|
const c = m.newFlipper(1);
|
||||||
|
for (const w of c.walls) for (const [x, y] of [w.a, w.b]) assert.ok(x >= m.FIELD.x0 && x <= m.FIELD.x1 && y >= m.FIELD.y0 && y <= m.FIELD.y1, `${w.a}-${w.b}`);
|
||||||
|
// I tre dischi dipinti da Codex (misurati sull'immagine): i funghi ci stanno sopra, a meno di 3 pixel.
|
||||||
|
for (const [b, [x, y]] of c.bumpers.map((b, i) => [b, [[281, 120], [360, 120], [320, 153]][i]])) assert.ok(Math.hypot(b.x - x, b.y - y) < 3);
|
||||||
|
assert.equal(c.lanes.length, 4); assert.deepEqual(c.lanes.map(l => l.rione), m.RIONI);
|
||||||
|
assert.equal(c.lanes[0].x0, m.FIELD.x0); assert.equal(c.lanes[3].x1, m.LANE.x0);
|
||||||
|
for (let i = 1; i < 4; i++) assert.equal(c.lanes[i].x0, c.lanes[i - 1].x1, 'corsie contigue');
|
||||||
|
assert.equal(c.targets.length, 6); assert.equal(c.balls, m.BALLS);
|
||||||
|
assert.ok(m.waiting(c) && m.canThrow(c) && c.ball.x > m.LANE.x0, 'la pallina aspetta sulla molla');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il lancio: anche la molla più fiacca arriva in campo, quella forte fa il giro dell\'arco; la ricaduta si rilancia', () => {
|
||||||
|
for (const level of ['bona', 'male']) for (const power of [0, 0.5, 1]) {
|
||||||
|
const c = m.newFlipper(2, { level }); assert.ok(m.launch(c, power)); assert.ok(!m.waiting(c));
|
||||||
|
let top = 999, out = false;
|
||||||
|
for (let i = 0; i < 180; i++) { m.update(c, 1 / 60); top = Math.min(top, c.ball?.y ?? 999); if (c.ball && c.ball.x < m.LANE.x0 - 30) out = true; }
|
||||||
|
assert.ok(top < 50, `${level} forza ${power}: in cima (${top.toFixed(0)})`);
|
||||||
|
assert.ok(out, `${level} forza ${power}: entrata in campo`);
|
||||||
|
}
|
||||||
|
// Una pallina ricaduta nella corsia si assesta e si rilancia, senza perderla.
|
||||||
|
const d = m.newFlipper(3, { level: 'bona' }); m.launch(d, 0.5); put(d, { x: 440, y: 200, vx: 0, vy: 50 }); run(d, 3);
|
||||||
|
assert.ok(m.waiting(d) && d.balls === m.BALLS, 'tornata sulla molla, nessuna pallina persa');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la pallina persa: tre palline, poi il risultato; il salvataggio vale una volta sola e non dipende dal fotogramma', () => {
|
||||||
|
const c = m.newFlipper(4, { level: 'seria' });
|
||||||
|
for (let n = 3; n >= 1; n--) {
|
||||||
|
assert.equal(c.balls, n);
|
||||||
|
m.launch(c, 0.9); c.saveUntil = 0; // niente salvataggio: si prova sotto
|
||||||
|
put(c, { x: 320, y: 320, vx: 0, vy: 200 }); run(c, 1);
|
||||||
|
}
|
||||||
|
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'cpu'); assert.ok(!m.canThrow(c));
|
||||||
|
assert.match(m.status(c), /Ne servivano 10000/);
|
||||||
|
const d = m.newFlipper(5, { level: 'bona' });
|
||||||
|
m.launch(d, 0.9); assert.ok(m.saveLeft(d) > 7); put(d, { x: 320, y: 320, vx: 0, vy: 200 }); run(d, 1);
|
||||||
|
assert.ok(m.waiting(d) && d.balls === 3, 'salvata: la pallina torna sulla molla');
|
||||||
|
assert.match(d.line ?? '', /salvata/);
|
||||||
|
m.launch(d, 0.9); assert.equal(m.saveLeft(d), 0); put(d, { x: 320, y: 320, vx: 0, vy: 200 }); run(d, 1);
|
||||||
|
assert.equal(d.balls, 2, 'la seconda volta è persa davvero');
|
||||||
|
// Lo stesso quarto di secondo, a passi piccoli o in un fotogramma solo: stesso esito (Codex).
|
||||||
|
for (const dts of [[1 / 240, 60], [1 / 60, 15], [0.25, 1]]) {
|
||||||
|
const e = m.newFlipper(6, { level: 'seria' }); m.launch(e, 0.8); e.sim = 2.9; e.saveUntil = 3; put(e, { x: 320, y: 344, vx: 0, vy: 100 });
|
||||||
|
for (let i = 0; i < dts[1]; i++) m.update(e, dts[0]);
|
||||||
|
assert.equal(e.balls, 3, `salvata con passi da ${dts[0]}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il flipper alzato rimanda su la pallina, anche a tutta velocità (niente attraversamenti); abbassato la lascia scendere', () => {
|
||||||
|
const up = m.newFlipper(6, { level: 'seria' }); m.launch(up, 0.9);
|
||||||
|
put(up, { x: 300, y: 300, vx: 0, vy: 150 }); m.hold(up, 'left', 'test', true);
|
||||||
|
let best = 0; for (let i = 0; i < 60; i++) { m.update(up, 1 / 60); best = Math.min(best, up.ball?.vy ?? 0); }
|
||||||
|
assert.ok(best < -250, `tirata su (vy ${best.toFixed(0)})`);
|
||||||
|
assert.ok(up.events.some(e => e.kind === 'flipper-flip'));
|
||||||
|
// Il caso riprodotto da Codex: pallina a 980 px/s sulla punta del flipper che si alza.
|
||||||
|
const fast = m.newFlipper(6, { level: 'seria' }); m.launch(fast, 0.9);
|
||||||
|
put(fast, { x: 313.438, y: 323.71, vx: -469.837, vy: 860.031 }); m.hold(fast, 'left', 'test', true);
|
||||||
|
m.update(fast, m.STEP);
|
||||||
|
assert.ok(fast.ball.vy < 0 && fast.ball.y < 324, `respinta, non passata sotto (y ${fast.ball.y.toFixed(1)}, vy ${fast.ball.vy.toFixed(0)})`);
|
||||||
|
const down = m.newFlipper(6, { level: 'seria' }); m.launch(down, 0.9);
|
||||||
|
put(down, { x: 300, y: 300, vx: 0, vy: 150 }); down.saveUntil = 0; run(down, 2);
|
||||||
|
assert.equal(down.balls, 2, 'senza flipper, giù nella Sieve');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('funghi, bersagli, Regolo e corsie fanno punti solo a un urto nuovo; le quattro corsie insieme il Palio', () => {
|
||||||
|
const c = m.newFlipper(7, { level: 'bona' }); m.launch(c, 0.9);
|
||||||
|
const b = c.bumpers[0];
|
||||||
|
put(c, { x: b.x, y: b.y + b.r + 6, vx: 0, vy: -200 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
|
||||||
|
assert.equal(c.score, m.SCORE.bumper); assert.ok(c.ball.vy > 0 && b.hit > 0, 'respinta');
|
||||||
|
assert.ok(c.events.some(e => e.kind === 'flipper-fungo'));
|
||||||
|
// La pallina appoggiata sul Regolo (Codex: 240.000 punti in un secondo): non conta.
|
||||||
|
c.score = 0; put(c, { x: c.regolo.x, y: c.regolo.y - c.regolo.r - m.R, vx: 0, vy: 0 });
|
||||||
|
for (let i = 0; i < 240; i++) m.update(c, m.STEP);
|
||||||
|
assert.equal(c.score, 0, 'il contatto che dura non fa punti');
|
||||||
|
put(c, { x: c.regolo.x, y: c.regolo.y + 12, vx: 0, vy: -200 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
|
||||||
|
assert.equal(c.score, m.SCORE.regolo);
|
||||||
|
c.score = 0;
|
||||||
|
for (const t of c.targets.filter(k => k.bank === 'pieve')) { const before = c.score; put(c, { x: t.a[0] + 10, y: (t.a[1] + t.b[1]) / 2, vx: -300, vy: 0 }); m.update(c, 1 / 60); m.update(c, 1 / 60); assert.ok(c.score > before, `caduto il ${t.i}`); }
|
||||||
|
assert.equal(c.score, 3 * m.SCORE.target + m.SCORE.bank);
|
||||||
|
assert.ok(c.targets.filter(k => k.bank === 'pieve').every(t => t.down), 'la fila resta giù un attimo');
|
||||||
|
put(c, { x: 260, y: 230, vx: 0, vy: 0 }); run(c, 1);
|
||||||
|
assert.ok(c.targets.filter(k => k.bank === 'pieve').every(t => !t.down), 'poi si rialza');
|
||||||
|
assert.match(c.line ?? '', /PIEVE/);
|
||||||
|
// Le corsie: contano attraversando la riga, anche fuori centro (Codex: a x=287 non si accendeva).
|
||||||
|
c.score = 0;
|
||||||
|
for (const [i, x] of [[0, 230], [1, 287], [2, 335], [3, 420]]) {
|
||||||
|
put(c, { x, y: c.lanes[i].y - 3, vx: 0, vy: 150 }); run(c, 0.1);
|
||||||
|
assert.ok(c.lanes[i].lit || c.lanes.every(k => !k.lit), c.lanes[i].rione);
|
||||||
|
}
|
||||||
|
assert.ok(c.score >= 4 * m.SCORE.lane + m.SCORE.palio, `corsie e Palio (${c.score}; in più, al massimo, un fungo sfiorato)`);
|
||||||
|
assert.match(c.line, /PALIO/); assert.ok(c.lanes.every(l => !l.lit), 'si rispegne per un altro giro');
|
||||||
|
// In salita non conta.
|
||||||
|
put(c, { x: 287, y: c.lanes[1].y + 6, vx: 0, vy: -150 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
|
||||||
|
assert.ok(!c.lanes[1].lit);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('comandi: la molla si carica e si lancia lasciando; annullare non lancia; due dita, pulsanti e tasti', () => {
|
||||||
|
const c = m.newFlipper(8, { level: 'bona' });
|
||||||
|
m.aimScreen(c, [440, 320]); assert.ok(m.press(c, 1)); run(c, 0.6);
|
||||||
|
assert.ok(c.holding && c.power > 0.4 && c.power < 0.6, `molla a ${c.power.toFixed(2)}`);
|
||||||
|
assert.ok(!m.release(c, 2), 'un altro dito non lancia');
|
||||||
|
m.cancel(c, 1); assert.ok(m.waiting(c) && !c.holding, 'annullata la carica: niente lancio');
|
||||||
|
m.aimScreen(c, [440, 320]); m.press(c, 1); run(c, 0.6); assert.ok(m.release(c, 1)); assert.ok(!m.waiting(c));
|
||||||
|
m.aimScreen(c, [250, 200]); assert.ok(m.press(c, 3)); assert.ok(c.flippers.left.held.includes(3));
|
||||||
|
m.aimScreen(c, [400, 200]); assert.ok(m.press(c, 4)); assert.ok(c.flippers.right.held.includes(4));
|
||||||
|
assert.ok(m.release(c, 3)); assert.ok(!c.flippers.left.held.length && c.flippers.right.held.includes(4));
|
||||||
|
m.cancel(c); assert.ok(!c.flippers.right.held.length, 'annullare tutto (pausa) lascia i flipper');
|
||||||
|
// I pulsanti e le frecce; il pulsante da tastiera dà un colpo breve che la fisica vede.
|
||||||
|
assert.ok(m.turn(c, -1, 'button')); assert.ok(c.flippers.left.held.length); assert.ok(m.stopTurn(c, 'button')); assert.ok(!c.flippers.left.held.length);
|
||||||
|
m.turn(c, 1, 'click'); m.stopTurn(c, 'click'); assert.ok(c.flippers.right.held.includes('click'));
|
||||||
|
const a0 = c.flippers.right.angle; run(c, 0.1); assert.notEqual(c.flippers.right.angle, a0, 'il flipper si è mosso'); run(c, 0.3); assert.ok(!c.flippers.right.held.length);
|
||||||
|
assert.ok(m.key(c, 'ArrowRight', true)); assert.ok(c.flippers.right.held.length); m.key(c, 'ArrowRight', false); assert.ok(!c.flippers.right.held.length);
|
||||||
|
assert.ok(!m.key(c, 'x', true));
|
||||||
|
assert.ok(!m.setPower(c, 0.3), 'in gioco il cursore della molla non vale');
|
||||||
|
// Spazio: carica e lancia; Invio: lancia con la forza del cursore; a partita finita i tasti non sono suoi.
|
||||||
|
const d = m.newFlipper(9); assert.ok(m.key(d, ' ', true)); run(d, 0.3); assert.ok(d.holding); m.key(d, ' ', false); assert.ok(!m.waiting(d));
|
||||||
|
const e = m.newFlipper(10); assert.ok(m.setPower(e, 0.5)); assert.equal(m.button(e), 'Lancia'); m.key(e, 'Enter', true); assert.ok(!m.waiting(e)); assert.equal(m.button(e), 'In gioco…');
|
||||||
|
e.step = 'result'; e.result = { winner: 'cpu', score: 0, goal: 10000 }; assert.ok(!m.key(e, ' ', true) && !m.key(e, 'Enter', true));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il traguardo è acquisito appena raggiunto: anche uscendo prima, si vince; la trattenuta sul flipper non viene scossa', () => {
|
||||||
|
const c = m.newFlipper(11, { level: 'bona' }); m.launch(c, 0.9);
|
||||||
|
c.score = c.skill.goal - 100; put(c, { x: c.regolo.x, y: c.regolo.y + 12, vx: 0, vy: -200 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
|
||||||
|
assert.ok(c.result && c.result.winner === 'player' && c.step === 'play', 'traguardo fatto, si continua');
|
||||||
|
assert.match(m.status(c), /record/);
|
||||||
|
const g = new Game(story); g.go('via_provinciale'); settle(g);
|
||||||
|
g.act('use', 'river'); settle(g); g.choose(g.options().findIndex(o => o.text.startsWith('Una partita alla bona'))); settle(g);
|
||||||
|
g.closeup.score = 7000; g.closeup.result = { winner: 'player', score: 7000, goal: 6000 };
|
||||||
|
g.closeupClose(); settle(g);
|
||||||
|
assert.ok(g.state.flags.flipper_vinto, 'uscito dopo il traguardo: vinto');
|
||||||
|
// La pallina trattenuta sul flipper alzato resta lì (niente scossa): è una mossa del giocatore.
|
||||||
|
const h = m.newFlipper(12, { level: 'bona' }); m.launch(h, 0.9); m.hold(h, 'left', 't', true); run(h, 0.5);
|
||||||
|
put(h, { x: 296, y: 300, vx: 0, vy: 0 }); run(h, 4);
|
||||||
|
assert.ok(h.ball && !h.ball.waiting && h.ball.y > 280 && h.ball.x < 320, `ancora sul flipper (${h.ball?.x.toFixed(0)}, ${h.ball?.y.toFixed(0)})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('giocando con attenzione, alla bona si arriva al traguardo; nessuna partita si incastra', () => {
|
||||||
|
let won = 0;
|
||||||
|
for (let seed = 1; seed <= 8; seed++) {
|
||||||
|
const c = m.newFlipper(seed * 7919, { level: 'bona' });
|
||||||
|
autoplay(c);
|
||||||
|
assert.equal(c.step, 'result', `seme ${seed}: finita`);
|
||||||
|
if (c.result.winner === 'player') won++;
|
||||||
|
}
|
||||||
|
assert.ok(won >= 5, `vinte ${won} su 8`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il disegno del flipper non cambia lo stato', () => {
|
||||||
|
const calls = [], gradient = { addColorStop() {} };
|
||||||
|
const ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => { calls.push([k, ...args]); return gradient; }, set: (o, k, v) => { o[k] = v; return true; } });
|
||||||
|
const c = m.newFlipper(11, { level: 'bona' }), bg = {};
|
||||||
|
const draw = () => { const before = structuredClone(c); drawFlipper(ctx, c, story.closeups.flipper, bg); assert.deepEqual(c, before); };
|
||||||
|
draw(); assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'FLIPPER DEL RIVER'));
|
||||||
|
m.launch(c, 0.8); run(c, 1); calls.length = 0; draw();
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && /^SALVATA \d s$/.test(t)), 'il salvataggio si vede');
|
||||||
|
c.step = 'result'; c.result = { winner: 'player', score: 7000, goal: 6000 }; calls.length = 0; draw();
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'TRAGUARDO FATTO!'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('al River: «Usa» o «Apri» il bar, il livello, la partita; uscendo senza traguardo, la battuta di chi lascia', () => {
|
||||||
|
const g = new Game(story); g.go('via_provinciale'); settle(g);
|
||||||
|
g.act('use', 'river'); settle(g);
|
||||||
|
assert.ok(g.options().some(o => o.text === 'Una partita seria.'));
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'Una partita seria.')); settle(g);
|
||||||
|
assert.equal(g.closeup?.id, 'flipper'); assert.equal(g.closeup.level, 'seria');
|
||||||
|
g.closeupClose();
|
||||||
|
assert.equal(g.queue[0]?.say, story.closeups.flipper.quit[0].say);
|
||||||
|
settle(g);
|
||||||
|
g.act('open', 'river'); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g);
|
||||||
|
assert.equal(g.closeup?.level, 'male');
|
||||||
|
});
|
||||||
+46
-3
@@ -6,6 +6,7 @@ import { newIncanto, updateIncanto, drawIncanto, whiteness, T } from '../src/eng
|
|||||||
import { Game, check, POSTER_READY } from '../src/engine/game.js';
|
import { Game, check, POSTER_READY } from '../src/engine/game.js';
|
||||||
import { story } from '../src/story/story.js';
|
import { story } from '../src/story/story.js';
|
||||||
import { settle } from './helpers.mjs';
|
import { settle } from './helpers.mjs';
|
||||||
|
import { inside as inPolygon, findPath } from '../src/engine/geometry.js';
|
||||||
import { render } from '../src/engine/render.js';
|
import { render } from '../src/engine/render.js';
|
||||||
|
|
||||||
const ZONE = story.zones.find(z => z.flag === 'ingorgo');
|
const ZONE = story.zones.find(z => z.flag === 'ingorgo');
|
||||||
@@ -36,6 +37,18 @@ test('la miscela delle console: vicino a una si sente quella, fra due tutte e du
|
|||||||
assert.deepEqual(on.map(t => t.src), ['a.mp3', 'b.mp3']);
|
assert.deepEqual(on.map(t => t.src), ['a.mp3', 'b.mp3']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('sotto una console si sente la sola musica di quella: in cima a Via dell\'Antica Posta, il marciapiede sotto l\'underground', () => {
|
||||||
|
const sc = story.scenes.antica_posta_alta, ok = cond => cond === 'ingorgo';
|
||||||
|
const spot = [620, 350];
|
||||||
|
assert.ok(inPolygon(sc.walk, spot) && findPath(sc.walk, sc.entry, spot), 'ci si arriva a piedi');
|
||||||
|
const under = trackFor(story, { screen: 'game', scene: 'antica_posta_alta', at: spot, ok });
|
||||||
|
assert.equal(under[0].src, 'assets/audio/ingorgo-underground.mp3'); assert.equal(under[0].gain, 1);
|
||||||
|
for (const t of under.slice(1)) assert.ok(t.gain <= 0.1, `${t.src} solo un filo: ${t.gain.toFixed(2)}`);
|
||||||
|
// A metà della strada, invece, la miscela resta.
|
||||||
|
const mid = trackFor(story, { screen: 'game', scene: 'antica_posta_alta', at: [240, 300], ok });
|
||||||
|
assert.ok(mid.filter(t => t.gain > 0.15).length >= 2);
|
||||||
|
});
|
||||||
|
|
||||||
test('la musica suona più brani insieme e li fa sfumare quando si cambia piazza', () => {
|
test('la musica suona più brani insieme e li fa sfumare quando si cambia piazza', () => {
|
||||||
globalThis.Audio = class { constructor(src) { this.src = src; this.volume = 0; this.paused = true; } play() { this.paused = false; return Promise.resolve(); } pause() { this.paused = true; } };
|
globalThis.Audio = class { constructor(src) { this.src = src; this.volume = 0; this.paused = true; } play() { this.paused = false; return Promise.resolve(); } pause() { this.paused = true; } };
|
||||||
const m = new Music({ volume: 0.5 });
|
const m = new Music({ volume: 0.5 });
|
||||||
@@ -82,9 +95,9 @@ test('dal manifesto in piazza all\'Ingorgo: la battuta, l\'effetto, la festa; us
|
|||||||
const track = trackFor(story, { screen: 'game', scene, at: [g.state.x, g.state.y], ok: c => check(c, g.state) });
|
const track = trackFor(story, { screen: 'game', scene, at: [g.state.x, g.state.y], ok: c => check(c, g.state) });
|
||||||
assert.ok(Array.isArray(track) && track.length >= 2, `${scene}: più console insieme`);
|
assert.ok(Array.isArray(track) && track.length >= 2, `${scene}: più console insieme`);
|
||||||
}
|
}
|
||||||
g.go('via_cipriani'); settle(g);
|
g.go('via_massorondinaio'); settle(g);
|
||||||
assert.ok(!g.state.flags.ingorgo, 'fuori dalla zona la festa sparisce');
|
assert.ok(!g.state.flags.ingorgo, 'fuori dalla zona la festa sparisce');
|
||||||
assert.equal(g.backdrop(), story.scenes.via_cipriani.background);
|
assert.equal(g.backdrop(), story.scenes.via_massorondinaio.background);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('chiudendo l\'effetto prima del lampo, la festa arriva lo stesso', () => {
|
test('chiudendo l\'effetto prima del lampo, la festa arriva lo stesso', () => {
|
||||||
@@ -137,7 +150,7 @@ test('alla festa le luci da palco si muovono, e solo alla festa', () => {
|
|||||||
const frame = time => { ops.length = 0; render(ctx, g, { time }); return JSON.stringify(ops.filter(([op]) => op === 'lineTo' || op === 'fillRect')); };
|
const frame = time => { ops.length = 0; render(ctx, g, { time }); return JSON.stringify(ops.filter(([op]) => op === 'lineTo' || op === 'fillRect')); };
|
||||||
assert.notEqual(frame(1000), frame(1700), `${scene}: le luci si muovono`);
|
assert.notEqual(frame(1000), frame(1700), `${scene}: le luci si muovono`);
|
||||||
}
|
}
|
||||||
g.go('via_cipriani'); settle(g); g.go('piazza'); settle(g);
|
g.go('via_massorondinaio'); settle(g); g.go('piazza'); settle(g);
|
||||||
ops.length = 0; render(ctx, g, { time: 1000 });
|
ops.length = 0; render(ctx, g, { time: 1000 });
|
||||||
assert.ok(!ops.some(([op, x, y]) => op === 'moveTo' && x === 116 && y === 146), 'di giorno niente teste mobili');
|
assert.ok(!ops.some(([op, x, y]) => op === 'moveTo' && x === 116 && y === 146), 'di giorno niente teste mobili');
|
||||||
} finally { delete globalThis.Image; }
|
} finally { delete globalThis.Image; }
|
||||||
@@ -155,6 +168,36 @@ test('le musiche e gli sfondi della festa esistono', () => {
|
|||||||
for (const f of files) assert.ok(existsSync(new URL('../' + f, import.meta.url)), `manca ${f}`);
|
for (const f of files) assert.ok(existsSync(new URL('../' + f, import.meta.url)), `manca ${f}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('all\'Ingorgo Andrea ha la birra: il foglio coi passi, il braccio alzato a tempo e la battuta', async () => {
|
||||||
|
const { readFileSync } = await import('node:fs');
|
||||||
|
const atlas = JSON.parse(readFileSync(new URL('../assets/characters/andrea-birra.json', import.meta.url)));
|
||||||
|
for (const f of ['walk_1', 'walk_2', 'walk_3', 'walk_4', 'cheer_1', 'cheer_2', 'cheer_3', 'talk']) assert.ok(atlas.frames[f], f);
|
||||||
|
assert.equal(story.characters.andrea_birra.sprite, 'assets/characters/andrea-birra.json');
|
||||||
|
const { festa } = await import('../src/engine/render.js');
|
||||||
|
const g = new Game(story); g.go('piazza'); settle(g);
|
||||||
|
assert.equal(festa(g, 0), null, 'senza la festa (e senza il foglio caricato) Andrea è quello di sempre');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('alla festa Andrea sta dentro la folla: dalla vita in giù la gente dello sfondo gli passa davanti', async () => {
|
||||||
|
const { crowdFront } = await import('../src/engine/render.js');
|
||||||
|
const g = new Game(story); g.go('piazzetta', [330, 320]); settle(g);
|
||||||
|
const calls = [], ctx = { drawImage: (...a) => calls.push(a) }, bg = { width: 1280 };
|
||||||
|
crowdFront(ctx, g, 0, bg);
|
||||||
|
assert.equal(calls.length, 0, 'di giorno, senza la festa, niente');
|
||||||
|
g.state.flags.ingorgo = true;
|
||||||
|
const { x, y } = g.state, h = 62 * g.scaleAt(y);
|
||||||
|
for (const time of [0, 250, 500]) {
|
||||||
|
calls.length = 0; crowdFront(ctx, g, time, bg);
|
||||||
|
assert.ok(calls.length > 3, 'strisce di folla sopra Andrea');
|
||||||
|
for (const [img, , , , , dx, dy, dw, dh] of calls) {
|
||||||
|
assert.equal(img, bg);
|
||||||
|
assert.ok(dx + dw > x - h * 0.4 && dx < x + h * 0.4, 'solo davanti a lui');
|
||||||
|
assert.ok(dy >= y - h * 0.75 && dy + dh <= y + 3, 'dalla vita in giù, fino ai piedi');
|
||||||
|
}
|
||||||
|
assert.ok(new Set(calls.map(c => c[6])).size > 1, 'il bordo in alto a teste, mai dritto');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('alla festa la folla dipinta salta a tempo: l\'effetto crowd in tutte e cinque le scene, dietro piano e davanti forte', async () => {
|
test('alla festa la folla dipinta salta a tempo: l\'effetto crowd in tutte e cinque le scene, dietro piano e davanti forte', async () => {
|
||||||
const { EFFECTS } = await import('../src/engine/render.js');
|
const { EFFECTS } = await import('../src/engine/render.js');
|
||||||
assert.equal(typeof EFFECTS.crowd, 'function');
|
assert.equal(typeof EFFECTS.crowd, 'function');
|
||||||
|
|||||||
Reference in New Issue
Block a user