Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
721df30937 | ||
|
|
40d5780255 | ||
|
|
9f2b045ab0 | ||
|
|
187b4c48cb | ||
|
|
c132e94ed8 | ||
|
|
9cd8027256 | ||
|
|
9f0f60d186 | ||
|
|
5135689293 | ||
|
|
8693bff0c7 | ||
|
|
c6812db291 | ||
|
|
670626afa5 | ||
|
|
9bf41d01af | ||
|
|
16252fb04a | ||
|
|
99d4dede80 | ||
|
|
36eda83fe4 | ||
|
|
58174e68d2 | ||
|
|
ff770af3b5 | ||
|
|
41e5fe2f6b | ||
|
|
9d823375aa |
@@ -87,11 +87,11 @@ background: ## Porta uno sfondo a 640x360 nel gioco (SRC=..., DST=..., CROP=x,y,
|
||||
@test -n "$(SRC)" || { echo "uso: make background SRC=assets/generated/nome.png [CROP=x,y,w,h]"; exit 2; }
|
||||
$(PYTHON) tools/prepare_background.py $(SRC) $(DST) $(if $(CROP),--crop $(CROP)) --colors $(COLORS)
|
||||
|
||||
character: ## Ritaglia un foglio sprite 4x2 e prepara il personaggio (NAME=andrea, SHEET=..., HEIGHT=120, FRAMES=..., REF=stand)
|
||||
character: ## Ritaglia un foglio sprite 4x2 e prepara il personaggio (NAME=andrea, SHEET=..., HEIGHT=120, FRAMES=..., REF=stand, FIX=wave=1.3)
|
||||
$(PYTHON) tools/slice_sprites.py --sheet $(SHEET) --out /tmp/regolo-$(NAME) --cols 4 --rows 2 \
|
||||
--despill --names $(FRAMES) --prefix $(NAME)_
|
||||
$(PYTHON) tools/prepare_character.py /tmp/regolo-$(NAME) $(NAME) assets/characters --height $(HEIGHT) \
|
||||
--frames $(FRAMES) --ref $(REF)
|
||||
--frames $(FRAMES) --ref $(REF) $(if $(FIX),--fix $(FIX))
|
||||
|
||||
## ------------------------------------------------------------ docker
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"image": "baldoria.png",
|
||||
"height": 120,
|
||||
"frames": {
|
||||
"stand": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 59,
|
||||
"h": 120,
|
||||
"ax": 28.6,
|
||||
"ay": 120
|
||||
},
|
||||
"blink": {
|
||||
"x": 61,
|
||||
"y": 0,
|
||||
"w": 59,
|
||||
"h": 120,
|
||||
"ax": 28.4,
|
||||
"ay": 120
|
||||
},
|
||||
"talk": {
|
||||
"x": 122,
|
||||
"y": 0,
|
||||
"w": 59,
|
||||
"h": 120,
|
||||
"ax": 28.6,
|
||||
"ay": 120
|
||||
},
|
||||
"explain": {
|
||||
"x": 183,
|
||||
"y": 0,
|
||||
"w": 67,
|
||||
"h": 122,
|
||||
"ax": 29.4,
|
||||
"ay": 122
|
||||
},
|
||||
"wave": {
|
||||
"x": 252,
|
||||
"y": 0,
|
||||
"w": 101,
|
||||
"h": 156,
|
||||
"ax": 48.9,
|
||||
"ay": 156
|
||||
},
|
||||
"wave2": {
|
||||
"x": 355,
|
||||
"y": 0,
|
||||
"w": 127,
|
||||
"h": 156,
|
||||
"ax": 81.8,
|
||||
"ay": 156
|
||||
},
|
||||
"cheer": {
|
||||
"x": 484,
|
||||
"y": 0,
|
||||
"w": 70,
|
||||
"h": 120,
|
||||
"ax": 33.6,
|
||||
"ay": 120
|
||||
},
|
||||
"laugh": {
|
||||
"x": 556,
|
||||
"y": 0,
|
||||
"w": 78,
|
||||
"h": 113,
|
||||
"ax": 31.1,
|
||||
"ay": 113
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 142 KiB |
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"image": "taglia.png",
|
||||
"height": 120,
|
||||
"frames": {
|
||||
"stand": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 54,
|
||||
"h": 120,
|
||||
"ax": 27.7,
|
||||
"ay": 120
|
||||
},
|
||||
"blink": {
|
||||
"x": 56,
|
||||
"y": 0,
|
||||
"w": 54,
|
||||
"h": 120,
|
||||
"ax": 27.7,
|
||||
"ay": 120
|
||||
},
|
||||
"talk": {
|
||||
"x": 112,
|
||||
"y": 0,
|
||||
"w": 75,
|
||||
"h": 120,
|
||||
"ax": 32.2,
|
||||
"ay": 120
|
||||
},
|
||||
"explain": {
|
||||
"x": 189,
|
||||
"y": 0,
|
||||
"w": 64,
|
||||
"h": 120,
|
||||
"ax": 33.6,
|
||||
"ay": 120
|
||||
},
|
||||
"laugh": {
|
||||
"x": 255,
|
||||
"y": 0,
|
||||
"w": 59,
|
||||
"h": 121,
|
||||
"ax": 25.2,
|
||||
"ay": 121
|
||||
},
|
||||
"aim": {
|
||||
"x": 316,
|
||||
"y": 0,
|
||||
"w": 97,
|
||||
"h": 114,
|
||||
"ax": 55.6,
|
||||
"ay": 114
|
||||
},
|
||||
"shrug": {
|
||||
"x": 415,
|
||||
"y": 0,
|
||||
"w": 91,
|
||||
"h": 117,
|
||||
"ax": 44.2,
|
||||
"ay": 117
|
||||
},
|
||||
"throw": {
|
||||
"x": 508,
|
||||
"y": 0,
|
||||
"w": 71,
|
||||
"h": 120,
|
||||
"ax": 30.3,
|
||||
"ay": 120
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 324 KiB |
|
After Width: | Height: | Size: 537 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 307 KiB |
|
After Width: | Height: | Size: 253 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 221 KiB |
|
After Width: | Height: | Size: 301 KiB |
@@ -119,6 +119,10 @@
|
||||
"file": "andrea-559fbc00c4.mp3",
|
||||
"sec": 4.2
|
||||
},
|
||||
"andrea|Lo lascio dov'è: è l'unica cosa che ricorda luglio, qui, a maggio.": {
|
||||
"file": "andrea-9f315e91e5.mp3",
|
||||
"sec": 4.4
|
||||
},
|
||||
"andrea|I vecchietti della panchina. Sanno tutto di tutti, soprattutto quello che non è successo.": {
|
||||
"file": "andrea-af4d717814.mp3",
|
||||
"sec": 6.44
|
||||
@@ -683,6 +687,10 @@
|
||||
"file": "andrea-c716ab2381.mp3",
|
||||
"sec": 7.95
|
||||
},
|
||||
"andrea|Il Taglia. Io sono un metro e ottanta, e lui mi guarda dall'alto. Con la Condor in mano.": {
|
||||
"file": "andrea-f1663e7892.mp3",
|
||||
"sec": 8.52
|
||||
},
|
||||
"andrea|Il giardino coi cedri e le panchine. All'ombra, d'estate, ci si fa la siesta a turno.": {
|
||||
"file": "andrea-4fd811d0c7.mp3",
|
||||
"sec": 6.43
|
||||
@@ -1091,6 +1099,14 @@
|
||||
"file": "barondeduke-238f525535.mp3",
|
||||
"sec": 1.65
|
||||
},
|
||||
"andrea|Baldoria, con la maglia blucerchiata e un bandierone della Sampdoria grande come lui. Qui a San Piero.": {
|
||||
"file": "andrea-a85f0e0a9a.mp3",
|
||||
"sec": 6.47
|
||||
},
|
||||
"andrea|Il bandierone? Prima dovrei passare sul suo cadavere. E non è un modo di dire.": {
|
||||
"file": "andrea-4faef889c8.mp3",
|
||||
"sec": 5.63
|
||||
},
|
||||
"andrea|La tribuna bianca e rossa. La domenica qui c'è più tifo che al Palio. Quasi.": {
|
||||
"file": "andrea-a50071085e.mp3",
|
||||
"sec": 6.84
|
||||
@@ -1883,6 +1899,38 @@
|
||||
"file": "andrea-d38b6e49ef.mp3",
|
||||
"sec": 3.49
|
||||
},
|
||||
"andrea|Lo zio Mario nel suo orto, col sole che va giù dietro la casa.": {
|
||||
"file": "andrea-9309e50d5c.mp3",
|
||||
"sec": 3.78
|
||||
},
|
||||
"zio_mario|Ti s'aspetta a cena, che c'è la vitella di maiale.": {
|
||||
"file": "zio_mario-39ff0c666f.mp3",
|
||||
"sec": 2.71
|
||||
},
|
||||
"andrea|Non ti avevo visto, pensavo tu fossi andato all'estero.": {
|
||||
"file": "andrea-f847f224df.mp3",
|
||||
"sec": 3.35
|
||||
},
|
||||
"zio_mario|Io ho girato tanto... il Falcione.": {
|
||||
"file": "zio_mario-d0d592db52.mp3",
|
||||
"sec": 2.73
|
||||
},
|
||||
"andrea|Zio, che sei stanco?": {
|
||||
"file": "andrea-21195c6cac.mp3",
|
||||
"sec": 2.31
|
||||
},
|
||||
"zio_mario|No, dormo!": {
|
||||
"file": "zio_mario-a852e9c43f.mp3",
|
||||
"sec": 1.19
|
||||
},
|
||||
"andrea|Zio, a me mi garba Michael Jackson!": {
|
||||
"file": "andrea-599b75d7c7.mp3",
|
||||
"sec": 2.6
|
||||
},
|
||||
"zio_mario|Mai col gesso!": {
|
||||
"file": "zio_mario-5773e3350f.mp3",
|
||||
"sec": 1.13
|
||||
},
|
||||
"andrea|Le case coi balconi pieni di vasi: qui ogni finestra è un giardino.": {
|
||||
"file": "andrea-ec81875c47.mp3",
|
||||
"sec": 5.22
|
||||
@@ -2791,6 +2839,110 @@
|
||||
"file": "pierino_parigi-8104163151.mp3",
|
||||
"sec": 3.3
|
||||
},
|
||||
"andrea|Baldoria! Ma che ci fai col bandierone al campo?": {
|
||||
"file": "andrea-3433db09f3.mp3",
|
||||
"sec": 3.79
|
||||
},
|
||||
"andrea|Guarda che qui la Sampdoria non ci gioca.": {
|
||||
"file": "andrea-408197f33d.mp3",
|
||||
"sec": 3.85
|
||||
},
|
||||
"andrea|Hai visto passare il palio, per caso?": {
|
||||
"file": "andrea-a0ac302eb3.mp3",
|
||||
"sec": 1.76
|
||||
},
|
||||
"andrea|Ciao, Baldoria.": {
|
||||
"file": "andrea-6697e2ce25.mp3",
|
||||
"sec": 0.97
|
||||
},
|
||||
"baldoria|Mi alleno. Il bandierone va sventolato tutti i giorni, sennò si arrugginisce il braccio.": {
|
||||
"file": "baldoria-954f26beb7.mp3",
|
||||
"sec": 6.08
|
||||
},
|
||||
"baldoria|Forza Doria!": {
|
||||
"file": "baldoria-97a6835622.mp3",
|
||||
"sec": 1.03
|
||||
},
|
||||
"baldoria|La Samp gioca dovunque ci sia un blucerchiato. E oggi ci sono io.": {
|
||||
"file": "baldoria-1eb6f19203.mp3",
|
||||
"sec": 5.54
|
||||
},
|
||||
"baldoria|L'unico palio che conosco è blu, bianco, rosso e nero. E ce l'ho in mano.": {
|
||||
"file": "baldoria-0accd6c002.mp3",
|
||||
"sec": 6.1
|
||||
},
|
||||
"andrea|Taglia! Si fa una gara con la Condor?": {
|
||||
"file": "andrea-99f683209e.mp3",
|
||||
"sec": 3.41
|
||||
},
|
||||
"andrea|A cosa si tira?": {
|
||||
"file": "andrea-3f77b83f35.mp3",
|
||||
"sec": 0.87
|
||||
},
|
||||
"andrea|E una partita a campana, Taglia?": {
|
||||
"file": "andrea-595df0a426.mp3",
|
||||
"sec": 2.23
|
||||
},
|
||||
"andrea|Ma quanto sei alto, Taglia?": {
|
||||
"file": "andrea-01ecdf3ed8.mp3",
|
||||
"sec": 1.96
|
||||
},
|
||||
"andrea|Ci si vede, Taglia.": {
|
||||
"file": "andrea-5d4d911c06.mp3",
|
||||
"sec": 1.42
|
||||
},
|
||||
"taglia|Io ho già tirato: ora tocca a te. Dodici pallini gialli, e devi fare più di me.": {
|
||||
"file": "taglia-2658ec8482.mp3",
|
||||
"sec": 6.05
|
||||
},
|
||||
"taglia|Come la vuoi?": {
|
||||
"file": "taglia-32a067db10.mp3",
|
||||
"sec": 0.62
|
||||
},
|
||||
"taglia|Il cartone sull'albero, le lattine sulla panchina, le bottigliette sulla porta e il palloncino all'altalena.": {
|
||||
"file": "taglia-670f9b4ef8.mp3",
|
||||
"sec": 7.15
|
||||
},
|
||||
"taglia|E se la vuoi seria, ogni tanto ti tiro una lattina in aria.": {
|
||||
"file": "taglia-ac012a1066.mp3",
|
||||
"sec": 4.21
|
||||
},
|
||||
"taglia|A campana? Alla nostra età?": {
|
||||
"file": "taglia-0a7f69770d.mp3",
|
||||
"sec": 2.77
|
||||
},
|
||||
"taglia|Va bene: il gesso ce l'ho in tasca, il sassolino lo scelgo io. Sul vialetto, dalla 1 alla 8, e chi pesta la riga passa la mano.": {
|
||||
"file": "taglia-c00be84faf.mp3",
|
||||
"sec": 11.54
|
||||
},
|
||||
"taglia|Abbastanza da vedere i pallini arrivare prima di te.": {
|
||||
"file": "taglia-d99c875252.mp3",
|
||||
"sec": 2.57
|
||||
},
|
||||
"taglia|Alla bona: ti faccio vedere dove arriva il pallino.": {
|
||||
"file": "taglia-757564b65e.mp3",
|
||||
"sec": 3.12
|
||||
},
|
||||
"taglia|Seria. Occhio alla lattina in aria.": {
|
||||
"file": "taglia-06abe0c427.mp3",
|
||||
"sec": 3.06
|
||||
},
|
||||
"taglia|A fassi male? Allora tiro più forte, e più spesso.": {
|
||||
"file": "taglia-4117f6613d.mp3",
|
||||
"sec": 4.84
|
||||
},
|
||||
"taglia|Alla bona: le righe un contano. Comincia te.": {
|
||||
"file": "taglia-328d930124.mp3",
|
||||
"sec": 4.12
|
||||
},
|
||||
"taglia|Seria: chi tocca la riga è fuori. Comincia te.": {
|
||||
"file": "taglia-1266463538.mp3",
|
||||
"sec": 4.23
|
||||
},
|
||||
"taglia|A fassi male? Io la campana la facevo a occhi chiusi.": {
|
||||
"file": "taglia-7938d8d5e4.mp3",
|
||||
"sec": 5.05
|
||||
},
|
||||
"andrea|Si fa una gara alle macchinine?!?!": {
|
||||
"file": "andrea-17216997e2.mp3",
|
||||
"sec": 2.74
|
||||
@@ -3203,9 +3355,9 @@
|
||||
"file": "bertovo-ca941bee91.mp3",
|
||||
"sec": 5.41
|
||||
},
|
||||
"vannino|M'hai pelato, ragazzo. Nel 1997 un ci si vedeva da queste parti, con quella faccia.": {
|
||||
"file": "vannino-e68fb40eba.mp3",
|
||||
"sec": 7.85
|
||||
"vannino|Tu m'ha pelato. La prossima volta porta il Barone.": {
|
||||
"file": "vannino-2267bb5adf.mp3",
|
||||
"sec": 3.18
|
||||
},
|
||||
"pierino_parigi|Le fiches restano al banco. E tu torna quando hai imparato a bluffare.": {
|
||||
"file": "pierino_parigi-a80d04fe5b.mp3",
|
||||
@@ -3231,6 +3383,70 @@
|
||||
"file": "andrea-c7dcb11e66.mp3",
|
||||
"sec": 5.52
|
||||
},
|
||||
"andrea|Battuto il Taglia! Con la sua Condor, per giunta.": {
|
||||
"file": "andrea-3ff6547461.mp3",
|
||||
"sec": 3.73
|
||||
},
|
||||
"taglia|Fortuna. Domani la rivinco.": {
|
||||
"file": "taglia-39627c5b3a.mp3",
|
||||
"sec": 2.78
|
||||
},
|
||||
"andrea|Il Taglia ha la mano ferma. Io ho il fiatone.": {
|
||||
"file": "andrea-6eabf3443c.mp3",
|
||||
"sec": 3.83
|
||||
},
|
||||
"taglia|Si torna quando vuoi. I pallini gialli un mancano mai.": {
|
||||
"file": "taglia-c3c705219c.mp3",
|
||||
"sec": 4.52
|
||||
},
|
||||
"andrea|Pari. Si rifà?": {
|
||||
"file": "andrea-5625b5f1fd.mp3",
|
||||
"sec": 2.21
|
||||
},
|
||||
"taglia|Si rifà.": {
|
||||
"file": "taglia-18973374a5.mp3",
|
||||
"sec": 0.76
|
||||
},
|
||||
"andrea|La Condor la rendo al Taglia. Per ora.": {
|
||||
"file": "andrea-aba12c5241.mp3",
|
||||
"sec": 3.1
|
||||
},
|
||||
"andrea|Dalla 1 fino in cima, e ritorno. A ventisett'anni, davanti alle poste.": {
|
||||
"file": "andrea-68378e4c21.mp3",
|
||||
"sec": 6.15
|
||||
},
|
||||
"taglia|Te lo dico io: se ti vede qualcuno, domani lo sa tutto il paese.": {
|
||||
"file": "taglia-1e6660932d.mp3",
|
||||
"sec": 3.34
|
||||
},
|
||||
"andrea|Il Taglia, su un piede solo, è più alto di me su due.": {
|
||||
"file": "andrea-3ed6165062.mp3",
|
||||
"sec": 3.31
|
||||
},
|
||||
"taglia|È questione di gambe lunghe.": {
|
||||
"file": "taglia-9782816264.mp3",
|
||||
"sec": 1.71
|
||||
},
|
||||
"andrea|Il gesso lo lascio lì. Ci giocherà qualche bambino, che è più in allenamento di me.": {
|
||||
"file": "andrea-356a1afe8d.mp3",
|
||||
"sec": 5.91
|
||||
},
|
||||
"andrea|Il manifesto dell'Ingorgo Sonoro dell'anno scorso. Da piazza fino a Cambray Digny, una console in ogni angolo.": {
|
||||
"file": "andrea-0c26e34815.mp3",
|
||||
"sec": 7.53
|
||||
},
|
||||
"andrea|Quanto darei per essere all'Ingorgo stasera?!?": {
|
||||
"file": "andrea-366c066d68.mp3",
|
||||
"sec": 2.28
|
||||
},
|
||||
"andrea|Ma... è l'Ingorgo! Le console, la gente, le spine della birra!": {
|
||||
"file": "andrea-bc7d5f4dc0.mp3",
|
||||
"sec": 7.5
|
||||
},
|
||||
"andrea|Il palio può aspettare una sera. O no?": {
|
||||
"file": "andrea-24e5ac53da.mp3",
|
||||
"sec": 3.07
|
||||
},
|
||||
"chiaru|Hai vinto te. Ma la mia aveva le ruote piene di gesso, sia chiaro.": {
|
||||
"file": "chiaru-91a4b23b22.mp3",
|
||||
"sec": 4.44
|
||||
|
||||
@@ -31,10 +31,16 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p
|
||||
| `provinciale-pignone.png`, `piazzetta-cambray-digny.png`, `via-cipriani.png`, `via-massorondinaio.png`, `mozzete.png`, `ponticino-mozzete.png`, `coldaia.png`, `falegnameria-coldaia.png`, `gabbiano.png` | Codex | screenshot di Google Street View | Google | **no**: da rifare con foto di Andrea |
|
||||
| `closeups/micro-parcheggio.png`, `closeups/micro/*.png` (macchinine) | Codex, solo testo | nessuna | — | sì |
|
||||
| `characters/chiaru.png` | Codex | `foto/chiaru.jpg` | Andrea (29 ottobre) | sì (il Chiaru consenziente) |
|
||||
| `characters/taglia.png` | Codex | `foto/taglia.png` | Andrea (29 ottobre) | sì (il Taglia consenziente) |
|
||||
| `via-liguri-magelli-muro.png` | Codex, ritocco di `via-liguri-magelli.png` con la Ghiacciomobile nel muro | come `via-liguri-magelli.png` | Street View | **no**: come lo sfondo da cui viene |
|
||||
| `characters/zio_mario.png` | Codex | `foto/zio-mario.png` | Andrea (29 ottobre) | sì (famiglia di Andrea) |
|
||||
| `terrazza-cardetole.png` | Codex (dalla foto, poi il terrazzo davanti) | `cardetole-finestra.jpg` | Andrea (29 ottobre) | **da decidere**: come `cardetole.png`, dice dove abita Andrea |
|
||||
| `closeups/muro-cerbottana.png` | Codex, solo testo (stile dalla piazza) | nessuna | — | sì |
|
||||
| `closeups/macchina-dietro.png`, `macchina-lampi.png`, `macchina-davanti.png` | Codex, dallo sprite della macchina del tempo | nessuna | — | sì |
|
||||
| `closeups/campone-condor.png` | Codex, dallo sfondo del campone delle poste | nessuna | — | sì |
|
||||
| `closeups/campone-campana.png` | Codex, solo testo (stile dal campone delle poste) | nessuna | — | sì |
|
||||
| `closeups/manifesto-ingorgo.png` | Codex, dal manifesto vero (2014) senza sponsor, loghi e data | `assets/raw/riferimenti/manifesto-ingorgo-2014.png` | Pro Loco San Piero a Sieve (manifesto), Andrea | sì |
|
||||
| `ingorgo-piazza.png`, `ingorgo-antica-posta.png`, `ingorgo-piazzetta-comune.png`, `ingorgo-antica-posta-alta.png`, `ingorgo-piazzetta.png` | Codex, dagli sfondi di giorno delle stesse scene | foto della festa di Andrea (`assets/raw/foto/ingorgo-*.png`), solo per l'atmosfera | Andrea | sì |
|
||||
| `barrettino-1997.png`, `closeups/cruscotto-tempo.png` | Codex, solo testo | nessuna | — | sì |
|
||||
| `characters/vannino.png`, `characters/pierino_parigi.png` | Codex, dalla descrizione di Andrea (niente foto) | nessuna | — | sì (Pierino: consenso del figlio Marco) |
|
||||
| `cardetole.png` | Codex | `cardetole.jpg` | Andrea (EXIF) | **da decidere**: è la casa vera di Andrea, pubblicarla dice dove abita |
|
||||
|
||||
@@ -18,6 +18,50 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
||||
|
||||
## Dove siamo arrivati
|
||||
|
||||
- **L'Ingorgo Sonoro (30 ottobre, extra)**: in piazza il vecchio manifesto dell'Ingorgo (`manifesto_ingorgo`); guardandolo
|
||||
si apre ingrandito (primo piano `poster`, `manifesto-ingorgo.png`: Codex dal manifesto vero del 2014, senza sponsor né
|
||||
data); toccandolo, «Quanto darei per essere all'Ingorgo stasera?!?», poi l'effetto speciale (primo piano `incanto`, `incanto.js`: fasci di
|
||||
luce, cassa che accelera, lampo bianco, coriandoli) e il centro diventa la notte della festa (flag `ingorgo`, fase
|
||||
`ingorgo`, sfondi di notte di Codex in `backdrops`). La zona (`story.zones`): Piazza, Via dell'Antica Posta, Piazzetta
|
||||
del Comune, cima di Via dell'Antica Posta, Piazzetta Cambray Digny; uscendo (a piedi o con la mappa) si torna a maggio.
|
||||
Alla festa si clicca solo per spostarsi (zona con `exitsOnly`: panchine, porte e statue non rispondono).
|
||||
Le luci da discoteca animate solo alla festa (`stagelights`, `lasers`, `mirrorball`, `floorspots` in render.js), rinforzate
|
||||
su richiesta di Andrea: colpo di colore a ogni battuta, strobo a raffica a fine frase, macchie che corrono sulla folla. Fasci che spazzano, bagliore a
|
||||
tempo, strobo, laser alla tecno, palla a specchi all'underground. Le console (`stages` delle scene, `music.js` `mix`): più brani insieme, ognuno più forte vicino alla sua console, anche
|
||||
da fuori quadro (la piazza accanto). Generi provvisori: house in Piazza, latino al Comune, underground in cima a Via
|
||||
dell'Antica Posta, tecno a Cambray Digny; i brani sono di Andrea (Suno), portati al volume del gioco. Gli sfondi sono
|
||||
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`).
|
||||
[DA ANDREA] i generi veri per piazza, le battute, i bar a tema; da fare: spillare la birra (minigioco) e bere.
|
||||
- **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).
|
||||
Sprite di Codex (`assets/raw/baldoria-sheet.png`; `make character … FIX=wave=1.2,…` perché Codex ha disegnato più
|
||||
piccole le figure col bandierone in alto). Un dialogo breve. [DA ANDREA: le battute; la somiglianza va rivista con
|
||||
una sua foto]
|
||||
- **La campana al campone delle poste (30 ottobre)**: dal Taglia, «E una partita a campana, Taglia?», poi il livello.
|
||||
Primo piano `campana` visto dall'alto sul vialetto (`campone-campana.png`, Codex); la campana col gesso la disegna il
|
||||
codice (`campana.js`, `campana-render.js`): 1-2-3, coppia 4-5, 6, coppia 7-8 e in cima il mezzo cerchio con scritto
|
||||
CAMPANA (Andrea: non «cielo»), ogni casella tinta tenue col suo gesso. Il sassolino: la mira dà la direzione, tenendo
|
||||
premuto la forza va e torna, si lascia; deve cadere nella casella che tocca senza toccare le righe (alla bona le
|
||||
righe non contano e il puntino giallo mostra dove cade). I salti: la lancetta dell'equilibrio attraversa la barra e
|
||||
si preme nel verde (presto: giù l'altro piede; tardi: pestata la riga); si scavalca la casella col sassolino e
|
||||
tornando lo si raccoglie. Chi ce la fa passa alla casella dopo e ritira, chi sbaglia passa la mano; il Taglia gioca
|
||||
da solo (in simulazione gli servono circa 22, 10 e 6 turni secondo il livello). Vince chi finisce la 8 (flag
|
||||
`campana_vinta`). Test `campana.test.mjs`, prova `npm run browser:campana`. [DA ANDREA] le battute.
|
||||
- **La macchina del tempo da fuori (30 ottobre)**: dopo il cruscotto, la partenza giù per Via Liguri Magelli
|
||||
(primo piano `corsa`, `src/engine/corsa.js`): accelera, il fulmine la prende, il bianco, e restano due sgommate di
|
||||
fuoco che bruciano e si spengono; al ritorno dal 1997 l'arrivo: il fulmine in fondo alla strada, lei viene avanti e
|
||||
ci passa accanto lasciando le strisce. La macchina da dietro e davanti (coi lampi) è di Codex.
|
||||
- **Lo zio Mario negli orti al tramonto (30 ottobre)**: dal tramonto (`indizio_regolo`) non è più davanti alla
|
||||
Ghiacciomobile ma accanto al suo orto (`orti_zio_mario`), con una battuta provvisoria [DA ANDREA].
|
||||
- **La Condor al campone delle poste (29 ottobre)**: la gara col Taglia (Andrea Tagliaferri, consenso dato) con la
|
||||
pistola ad aria compressa coi pallini gialli (`condor.js`, `condor-render.js`, primo piano `condor`, sfondo di Codex).
|
||||
Come la cerbottana: il mirino ondeggia col respiro; tenere premuto trattiene il fiato (il mirino si ferma), lasciare
|
||||
spara, troppo e il colpo parte da solo. Il pallino cala con la distanza (alla porta in fondo si mira sopra). Il
|
||||
cartone sull'albero (10/5/2), le lattine sulla panchina (3), le bottigliette sulla traversa (5), il palloncino
|
||||
all'altalena (6), la lattina tirata in aria dal Taglia a seria e a fassi male (8). Si deve battere il punteggio del
|
||||
Taglia (22, 34, 44 secondo il livello, ±3). Il Taglia sta al campone (sprite di Codex dalla sua foto, più alto di Andrea); si gareggia
|
||||
parlandogli. [DA ANDREA] le battute.
|
||||
- **La macchina del tempo e il pokerino del 1997 (29 ottobre)**: dall'Arcu sotto la tenda del Trappola, «Bei tempi
|
||||
quando qui si faceva i pokerini…»: compare la macchina del tempo in Via Liguri Magelli. Dentro, il cruscotto (Codex)
|
||||
compone 10/07/1997 18:30, poi tremori, lampi e il bianco (primo piano `viaggio`, `src/engine/viaggio.js`, tempi in
|
||||
|
||||
@@ -272,6 +272,28 @@
|
||||
il pulsante alterna Recupera e Molla. Sei lanci. La gloria: 2 pesci alla bona, 3 seria, 4 a fassi male (lo stivale
|
||||
non conta, e non pesa). Frecce ← →: distanza del lancio; Esc esce.</p>
|
||||
</details>
|
||||
<details id="condor-help" hidden><summary>Come si gioca</summary>
|
||||
<p>La Condor, la pistola ad aria compressa coi pallini gialli, al campone delle poste: dodici pallini, e il Taglia ha
|
||||
già tirato. Il suo punteggio è in alto: per vincere bisogna fare di più. Il cartone sull'albero vale 10, 5 o 2 punti
|
||||
secondo il cerchio, ogni lattina sulla panchina 3, ogni bottiglietta sulla traversa della porta 5, il palloncino
|
||||
legato all'altalena 6, la lattina che il Taglia tira in aria (Seria e A fassi male) 8.</p>
|
||||
<p>Muovi il mouse o tocca il campo per mirare: il mirino ondeggia col respiro (di più salendo di livello). Tieni
|
||||
premuto per trattenere il fiato: il mirino si ferma (diventa verde); lascia per sparare. Se lo trattieni troppo il
|
||||
fiato finisce, il mirino trema e il colpo parte da solo. Il pallino cala con la distanza: alla porta in fondo si
|
||||
mira un po' più sopra; alla bona il puntino giallo segna dove arriva. Il palloncino e la lattina in aria si
|
||||
muovono: si mira dove saranno. Si può anche premere Spara, senza trattenere il fiato. Frecce per spostare il mirino.</p>
|
||||
</details>
|
||||
<details id="campana-help" hidden><summary>Come si gioca</summary>
|
||||
<p>La campana col gesso sul vialetto del campone, a turni col Taglia. Si tira il sassolino nella casella che tocca,
|
||||
dalla 1 alla 8: deve cadere dentro, senza toccare le righe (alla bona le righe non contano). Poi si salta tutta la
|
||||
campana fino in cima (il mezzo cerchio) e indietro, su un piede nelle caselle sole e a gambe larghe nella 4-5 e nella 7-8, senza mai
|
||||
mettere il piede dove sta il sassolino; tornando lo si raccoglie dalla casella prima. Chi ce la fa passa alla casella
|
||||
dopo e ritira; chi sbaglia passa la mano. Vince chi finisce la 8.</p>
|
||||
<p>Il tiro: muovi il mouse o tocca la campana per mirare, tieni premuto per la forza (sale e scende) e lascia; oppure
|
||||
regola la forza e premi Tira. Alla bona il puntino giallo segna dove cade. Frecce su e giù per la mira, destra e
|
||||
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>
|
||||
</details>
|
||||
<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
|
||||
di gesso vale 10, 5 o 2 punti secondo il cerchio, ogni lattina sul muretto 3, il barattolo appeso che dondola (Seria
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "leggenda-del-regolo",
|
||||
"version": "0.1.53",
|
||||
"version": "0.1.59",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -10,6 +10,9 @@
|
||||
"browser:micro": "node tests/micro.browser.mjs",
|
||||
"browser:gara": "node tests/gara.browser.mjs",
|
||||
"browser:cerbottana": "node tests/cerbottana.browser.mjs",
|
||||
"browser:condor": "node tests/condor.browser.mjs",
|
||||
"browser:campana": "node tests/campana.browser.mjs",
|
||||
"browser:ingorgo": "node tests/ingorgo.browser.mjs",
|
||||
"browser:tressette": "node tests/tressette.browser.mjs",
|
||||
"browser:scopa": "node tests/scopa.browser.mjs",
|
||||
"browser:briscola": "node tests/briscola.browser.mjs",
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { W, CY, START, CIELO, FROM, BOXES, LAST, FLIGHT, HOP, center, landing, needle, canAdjust } from './campana.js';
|
||||
import { text, panel } from './bocce-render.js';
|
||||
|
||||
function rect(ctx, x, y, w, h, color) { ctx.fillStyle = color; ctx.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h)); }
|
||||
// Il gesso: tratti a puntini un po' storti, con dei buchi (sempre gli stessi, da un piccolo hash: non tremano).
|
||||
const hash = n => { const s = Math.sin(n * 127.1 + 311.7) * 43758.5453; return s - Math.floor(s); };
|
||||
function chalkLine(ctx, x1, y1, x2, y2, seed) {
|
||||
const n = Math.max(2, Math.round(Math.hypot(x2 - x1, y2 - y1)));
|
||||
for (let i = 0; i <= n; i++) {
|
||||
const k = i / n, h = hash(seed * 1000 + i);
|
||||
if (h < 0.07) continue;
|
||||
ctx.fillStyle = h > 0.8 ? 'rgba(255,255,250,0.95)' : 'rgba(240,238,228,0.78)';
|
||||
ctx.fillRect(Math.round(x1 + (x2 - x1) * k + (hash(seed + i) - 0.5)), Math.round(y1 + (y2 - y1) * k + (hash(seed * 7 + i) - 0.5)), 2, 2);
|
||||
}
|
||||
}
|
||||
// I gessi colorati: ogni casella ripassata tenue col suo colore, a puntini come il gesso sull'asfalto (idea di Andrea).
|
||||
const TINTS = { 1: [244, 160, 180], 2: [250, 222, 120], 3: [150, 200, 240], 4: [160, 220, 150], 5: [200, 170, 230], 6: [250, 180, 120], 7: [140, 220, 210], 8: [240, 150, 150], top: [255, 240, 200] };
|
||||
function tint(ctx, [r, g, b], x, y, w, h, seed, inside = () => true) {
|
||||
ctx.fillStyle = `rgba(${r},${g},${b},0.16)`;
|
||||
for (let yy = y; yy < y + h; yy += 2) for (let xx = x; xx < x + w; xx += 2) {
|
||||
if (!inside(xx, yy)) continue;
|
||||
const k = hash(seed * 7777 + xx * 13 + yy * 7);
|
||||
if (k < 0.18) continue; // l'asfalto che spunta fra il gesso
|
||||
ctx.fillStyle = `rgba(${r},${g},${b},${k > 0.85 ? 0.42 : 0.24})`; ctx.fillRect(xx, yy, 2, 2);
|
||||
}
|
||||
}
|
||||
// Il gesso non cambia mai: tinte e righe si disegnano una volta su una tela a parte (a puntini, a ogni fotogramma,
|
||||
// rallentavano la pagina). Fuori dal browser (nei test) si disegnano direttamente.
|
||||
let layer = null;
|
||||
function chalkwork(ctx) {
|
||||
let s = 1;
|
||||
for (const [id, b] of Object.entries(BOXES)) tint(ctx, TINTS[id], b.x + 1, b.y + 1, W - 2, W - 2, Number(id));
|
||||
tint(ctx, TINTS.top, CIELO.x + 1, CY - CIELO.r, CIELO.r, CIELO.r * 2, 9, (x, y) => Math.hypot(x + 1 - CIELO.x, y + 1 - CY) < CIELO.r - 2);
|
||||
for (const b of Object.values(BOXES)) {
|
||||
chalkLine(ctx, b.x, b.y, b.x + W, b.y, s++); chalkLine(ctx, b.x, b.y + W, b.x + W, b.y + W, s++);
|
||||
chalkLine(ctx, b.x, b.y, b.x, b.y + W, s++); chalkLine(ctx, b.x + W, b.y, b.x + W, b.y + W, s++);
|
||||
}
|
||||
// In cima la campana: il mezzo cerchio in fondo, dove ci si gira.
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const a1 = -Math.PI / 2 + i / 40 * Math.PI, a2 = -Math.PI / 2 + (i + 1) / 40 * Math.PI;
|
||||
chalkLine(ctx, CIELO.x + Math.cos(a1) * CIELO.r, CY + Math.sin(a1) * CIELO.r, CIELO.x + Math.cos(a2) * CIELO.r, CY + Math.sin(a2) * CIELO.r, 200 + i);
|
||||
}
|
||||
// La riga di partenza.
|
||||
chalkLine(ctx, START - 14, CY - W, START - 14, CY + W, 300);
|
||||
}
|
||||
function grid(ctx, c) {
|
||||
if (typeof document === 'undefined') chalkwork(ctx);
|
||||
else {
|
||||
if (!layer) { layer = document.createElement('canvas'); layer.width = 640; layer.height = 360; chalkwork(layer.getContext('2d')); }
|
||||
ctx.drawImage(layer, 0, 0);
|
||||
}
|
||||
// Il numero in alto nella casella: il mezzo resta libero per il sassolino e i piedi. Quella che tocca ha il bordo giallo.
|
||||
for (const [id, b] of Object.entries(BOXES)) {
|
||||
const p = center(id), next = Number(id) === c.target[c.current] && c.step !== 'result';
|
||||
if (next) { ctx.strokeStyle = 'rgba(255,236,140,0.55)'; ctx.lineWidth = 2; ctx.strokeRect(b.x + 4, b.y + 4, W - 8, W - 8); }
|
||||
text(ctx, id, p.x, p.y - 17, 14, next ? '#fff3a8' : 'rgba(245,243,232,0.88)', 'center');
|
||||
}
|
||||
text(ctx, 'CAMPANA', CIELO.x + W * 0.53, CY - 26, 8, 'rgba(245,243,232,0.88)', 'center');
|
||||
}
|
||||
// Il sassolino: grigio, col lume in alto; in volo, più su e con l'ombra sotto.
|
||||
function stone(ctx, x, y, lift = 0) {
|
||||
ctx.fillStyle = 'rgba(30,25,20,0.35)'; ctx.beginPath(); ctx.ellipse(x + 1, y + 2, 5, 3, 0, 0, Math.PI * 2); ctx.fill();
|
||||
const s = 1 + lift / 60, yy = y - lift;
|
||||
ctx.fillStyle = '#4a4744'; ctx.beginPath(); ctx.ellipse(x, yy, 5 * s, 4 * s, 0.3, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillStyle = '#8d8984'; ctx.beginPath(); ctx.ellipse(x - 0.5, yy - 0.5, 4 * s, 3 * s, 0.3, 0, Math.PI * 2); ctx.fill();
|
||||
rect(ctx, x - 2 * s, yy - 2 * s, 2, 1, '#cfcac2');
|
||||
}
|
||||
// Dove stanno i piedi: a gambe larghe nelle coppie e nel cielo, su un piede nelle caselle sole, alla partenza tutti e due.
|
||||
function feet(beat) {
|
||||
if (!beat || beat.out) return [{ x: FROM.x + 2, y: CY - 9 }, { x: FROM.x + 2, y: CY + 9 }];
|
||||
if (beat.boxes[0] === 'cielo') { const p = center('cielo'); return [{ x: p.x, y: p.y - 12 }, { x: p.x, y: p.y + 12 }]; }
|
||||
return beat.boxes.map(id => center(id));
|
||||
}
|
||||
// Una scarpa vista dall'alto: la punta verso dove si va.
|
||||
function shoe(ctx, x, y, dir, scale, [sole, upper, lace]) {
|
||||
ctx.save(); ctx.translate(Math.round(x), Math.round(y)); ctx.scale(dir * scale, scale);
|
||||
ctx.fillStyle = 'rgba(30,25,20,0.3)'; ctx.fillRect(-9, -3, 21, 9);
|
||||
ctx.fillStyle = sole; ctx.fillRect(-10, -5, 20, 10); ctx.fillRect(10, -4, 2, 8);
|
||||
ctx.fillStyle = upper; ctx.fillRect(-9, -4, 18, 8);
|
||||
ctx.fillStyle = lace; ctx.fillRect(-1, -2, 7, 1); ctx.fillRect(-1, 1, 7, 1);
|
||||
ctx.fillStyle = '#2a2420'; ctx.fillRect(-8, -2, 4, 4); // la caviglia, il buco della scarpa
|
||||
ctx.restore();
|
||||
}
|
||||
const SHOES = { player: ['#e8e6e0', '#3d6fb8', '#ffffff'], cpu: ['#2b2b2b', '#b8322a', '#f0d8a8'] };
|
||||
function walker(ctx, c) {
|
||||
const beat = c.beats?.[c.beat], prev = c.beat > 0 ? c.beats[c.beat - 1] : null, now = feet(c.beat >= 0 ? beat : null);
|
||||
const k = c.hop ? Math.min(1, c.hop.t / HOP) : 1, from = feet(c.beat > 0 ? prev : null);
|
||||
const mid = list => list.reduce((a, p) => ({ x: a.x + p.x / list.length, y: a.y + p.y / list.length }), { x: 0, y: 0 });
|
||||
const a = mid(from), b = mid(now), off = { x: (a.x - b.x) * (1 - k), y: (a.y - b.y) * (1 - k) };
|
||||
const scale = (c.current === 'cpu' ? 1.45 : 1.3) * (1 + 0.35 * Math.sin(Math.PI * k));
|
||||
const dir = beat?.back ? -1 : 1;
|
||||
for (const p of now) shoe(ctx, p.x + off.x, p.y + off.y, dir, scale, SHOES[c.current]);
|
||||
}
|
||||
|
||||
export function drawCampana(ctx, c, def, bg) {
|
||||
ctx.save(); ctx.imageSmoothingEnabled = false;
|
||||
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#9a948a'; ctx.fillRect(0, 0, 640, 360); }
|
||||
grid(ctx, c);
|
||||
if (c.stone) stone(ctx, c.stone.x, c.stone.y);
|
||||
walker(ctx, c);
|
||||
if (c.shot) {
|
||||
const k = Math.min(1, c.shot.t / FLIGHT), { from, to } = c.shot;
|
||||
stone(ctx, from.x + (to.x - from.x) * k, from.y + (to.y - from.y) * k, 40 * Math.sin(Math.PI * k));
|
||||
}
|
||||
// La mira: un cerchietto di gesso dove si punta; alla bona, il puntino dove cade il sassolino con questa forza.
|
||||
if (canAdjust(c)) {
|
||||
ctx.strokeStyle = '#fff8dc'; ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.arc(c.aim.x, c.aim.y, 6, 0, Math.PI * 2); ctx.stroke();
|
||||
if (c.skill.guide) { const l = landing(c); rect(ctx, l.x - 2, l.y - 2, 4, 4, '#f5d02a'); rect(ctx, l.x - 1, l.y - 1, 2, 2, '#7a5a10'); }
|
||||
// La forza: la barra in basso a sinistra.
|
||||
const x = 16, y = 296, h = 50, f = Math.round(h * c.power);
|
||||
rect(ctx, x - 3, y - 3, 18, h + 6, '#17160fd0');
|
||||
rect(ctx, x, y + h - f, 12, f, c.holding ? '#ffd23c' : '#d9ccba');
|
||||
text(ctx, 'FORZA', x + 6, y - 9, 6, '#efd49e', 'center');
|
||||
}
|
||||
// L'equilibrio: la lancetta attraversa la barra; si salta quando è nel verde.
|
||||
if (c.step === 'hop' && c.meter) {
|
||||
const x = 220, y = 318, w = 200, zone = c.skill.zone * w / 2, u = needle(c);
|
||||
rect(ctx, x - 4, y - 14, w + 8, 30, '#17160fd8');
|
||||
rect(ctx, x, y, w, 8, '#7a3a2a'); rect(ctx, x + w / 2 - zone, y, zone * 2, 8, '#5fae4a');
|
||||
rect(ctx, x + w / 2 + u * w / 2 - 1, y - 3, 3, 14, '#fff3a8');
|
||||
text(ctx, 'EQUILIBRIO', x + w / 2, y - 7, 6, '#efd49e', 'center');
|
||||
}
|
||||
// In alto: a che casella è ciascuno, di chi è il turno, cosa è successo.
|
||||
rect(ctx, 0, 0, 640, 22, '#17160fe8');
|
||||
text(ctx, 'LA CAMPANA', 8, 7, 8, '#efd49e');
|
||||
text(ctx, { bona: 'alla bona', seria: 'seria', male: 'a fassi male' }[c.level], 8, 16, 7, '#d9ccba');
|
||||
const at = n => n > LAST ? 'finita' : `alla ${n}`;
|
||||
text(ctx, `Andrea ${at(c.target.player)}`, 220, 11, 9, c.current === 'player' ? '#fff0c8' : '#a8a08c', 'center');
|
||||
text(ctx, `Il Taglia ${at(c.target.cpu)}`, 360, 11, 9, c.current === 'cpu' ? '#9fd0ff' : '#7f95a8', 'center');
|
||||
text(ctx, c.current === 'player' ? 'tocca a te' : 'tocca al Taglia', 560, 11, 8, '#d9ccba', 'center');
|
||||
if (c.line) { rect(ctx, 150, 24, 340, 14, '#17160fd0'); text(ctx, c.line, 320, 31, 8, /riga|fuori|Troppo|equilibrio|piede|non nella/i.test(c.line) ? '#ffbf9f' : '#9fd0ff', 'center'); }
|
||||
if (c.step === 'result') {
|
||||
const won = c.result.winner === 'player';
|
||||
panel(ctx, 170, 140, 300, 76);
|
||||
text(ctx, won ? 'BATTUTO IL TAGLIA!' : 'HA VINTO IL TAGLIA', 320, 160, 12, won ? '#b9e5b3' : '#efd49e', 'center');
|
||||
text(ctx, won ? 'Tutta la campana, dalla 1 alla 8.' : `Tu eri arrivato alla ${c.target.player}.`, 320, 182, 8, '#efd49e', 'center');
|
||||
text(ctx, 'Torna al campone per uscire.', 320, 200, 8, '#d9ccba', 'center');
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
// La campana al campone delle poste (idea di Andrea, 30 ottobre): disegnata col gesso sul vialetto, a turni col Taglia.
|
||||
// Si tira il sassolino nella casella che tocca (dalla 1 alla 8): se cade dentro, senza toccare le righe, si salta tutta
|
||||
// la campana fino in cima e indietro, su un piede nelle caselle sole e a gambe larghe nelle coppie, senza mai mettere
|
||||
// il piede dove sta il sassolino; tornando, lo si raccoglie dalla casella prima. Chi sbaglia (sassolino fuori o sulla
|
||||
// riga, salto fuori tempo) passa la mano; chi ce la fa va avanti alla casella dopo e ritira. Vince chi finisce la 8.
|
||||
// Il tiro: la mira dà la direzione, tenere premuto carica la forza (va e torna), lasciare tira. I salti: a ogni salto
|
||||
// la lancetta dell'equilibrio attraversa la barra; si preme quando passa nel verde. Unità: i pixel del primo piano
|
||||
// 640×360, visto dall'alto (sfondo di Codex: assets/closeups/campone-campana.png).
|
||||
|
||||
export const W = 66, CY = 181, START = 80, CIELO = { x: START + 6 * W, r: W }, FROM = { x: 40, y: CY }, REACH = 520;
|
||||
export const TAP = 0.14, CHARGE = 1.3, FLIGHT = 0.55, HOP = 0.22, REST = 1.6, DONE = 1.1, CPU_WAIT = 0.9, CPU_BEAT = 0.42;
|
||||
// Le caselle: 1, 2, 3 da sole; 4 e 5 in coppia (la 4 a sinistra di chi salta, cioè in alto); la 6; 7 e 8 in coppia;
|
||||
// in fondo la campana (il mezzo cerchio dove ci si gira; nel codice «cielo», come la chiamano in altri posti).
|
||||
export const BOXES = {
|
||||
1: { x: START, y: CY - W / 2 }, 2: { x: START + W, y: CY - W / 2 }, 3: { x: START + 2 * W, y: CY - W / 2 },
|
||||
4: { x: START + 3 * W, y: CY - W }, 5: { x: START + 3 * W, y: CY }, 6: { x: START + 4 * W, y: CY - W / 2 },
|
||||
7: { x: START + 5 * W, y: CY - W }, 8: { x: START + 5 * W, y: CY },
|
||||
};
|
||||
export const GROUPS = [[1], [2], [3], [4, 5], [6], [7, 8], ['cielo']];
|
||||
export const LAST = 8;
|
||||
// I livelli: quanto si sparpaglia il sassolino (px), se si vede dove cade, quanto è larga la riga che non si può
|
||||
// toccare, quanto ci mette la lancetta dell'equilibrio e quanto è largo il verde; il Taglia: quante volte il sassolino
|
||||
// va dove vuole e quanto spesso, a ogni salto, mette giù il piede.
|
||||
export const LEVELS = {
|
||||
bona: { scatter: 4, guide: true, line: 0, sweep: 1.25, zone: 0.32, cpu: { throw: 0.5, slip: 0.045 } },
|
||||
seria: { scatter: 7, guide: false, line: 3, sweep: 1, zone: 0.2, cpu: { throw: 0.7, slip: 0.025 } },
|
||||
male: { scatter: 9, guide: false, line: 4, sweep: 0.8, zone: 0.15, cpu: { throw: 0.8, slip: 0.018 } },
|
||||
};
|
||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
|
||||
|
||||
export function center(id) {
|
||||
if (id === 'cielo') return { x: CIELO.x + W * 0.42, y: CY };
|
||||
const b = BOXES[id]; return { x: b.x + W / 2, y: b.y + W / 2 };
|
||||
}
|
||||
// In che casella cade un punto, e se tocca la riga (entro `tol` pixel da un bordo).
|
||||
export function boxAt([x, y], tol = 0) {
|
||||
for (const [id, b] of Object.entries(BOXES)) {
|
||||
if (x < b.x || x > b.x + W || y < b.y || y > b.y + W) continue;
|
||||
return { id: Number(id), line: Math.min(x - b.x, b.x + W - x, y - b.y, b.y + W - y) < tol };
|
||||
}
|
||||
const d = Math.hypot(x - CIELO.x, y - CY);
|
||||
if (x >= CIELO.x && d <= CIELO.r) return { id: 'cielo', line: Math.min(x - CIELO.x, CIELO.r - d) < tol };
|
||||
return null;
|
||||
}
|
||||
|
||||
// I salti per la casella `n`: all'andata tutte le caselle, saltando quella col sassolino (nella coppia si mette un
|
||||
// piede solo nell'altra); il cielo; al ritorno, dalla casella prima si raccoglie il sassolino; poi fuori.
|
||||
export function beats(n) {
|
||||
const step = (g, back) => g.includes(n) ? (g.length > 1 ? [{ boxes: g.filter(k => k !== n), back }] : []) : [{ boxes: g, back }];
|
||||
const out = GROUPS.flatMap(g => step(g, false));
|
||||
for (let i = GROUPS.length - 2; i >= 0; i--) {
|
||||
if (GROUPS[i].includes(n)) out.push({ ...out[out.length - 1], back: true, pick: true });
|
||||
out.push(...step(GROUPS[i], true));
|
||||
}
|
||||
out.push({ boxes: [], back: true, out: true });
|
||||
return out;
|
||||
}
|
||||
|
||||
export function newCampana(seed = Date.now() >>> 0, def = {}) {
|
||||
const level = LEVELS[def.level] ? def.level : 'seria';
|
||||
return { step: 'aim', t: 0, seed, level, skill: LEVELS[level], current: 'player', target: { player: 1, cpu: 1 },
|
||||
aim: center(1), power: 0.2, holding: false, holdTime: 0, charging: false, stone: null, shot: null,
|
||||
beats: null, beat: -1, meter: null, hop: null, line: null, next: null, events: [], visual: { time: 0 } };
|
||||
}
|
||||
export const canThrow = c => c.current === 'player' && (c.step === 'aim' || c.step === 'hop');
|
||||
export const canAdjust = c => c.current === 'player' && c.step === 'aim';
|
||||
export const aimFree = c => !canAdjust(c);
|
||||
|
||||
// Dove cade il sassolino tirato con questa forza, senza lo sparpaglio.
|
||||
export function landing(c, power = c.power) {
|
||||
const dx = c.aim.x - FROM.x, dy = c.aim.y - FROM.y, d = Math.hypot(dx, dy) || 1;
|
||||
return { x: FROM.x + dx / d * power * REACH, y: FROM.y + dy / d * power * REACH };
|
||||
}
|
||||
function toss(c, to) {
|
||||
c.shot = { from: { ...FROM }, to, t: 0 }; c.stone = null;
|
||||
c.step = 'flying'; c.line = null;
|
||||
c.events.push({ kind: 'campana-lancio', power: 0.6 });
|
||||
}
|
||||
function throwStone(c) {
|
||||
const p = landing(c), s = c.skill.scatter, spread = () => (random(c) + random(c) + random(c) - 1.5) * s * 1.4;
|
||||
toss(c, { x: p.x + spread(), y: p.y + spread() });
|
||||
return true;
|
||||
}
|
||||
const who = c => c.current === 'cpu' ? 'Il Taglia: ' : '';
|
||||
const cap = line => line[0].toUpperCase() + line.slice(1);
|
||||
// Il sassolino è arrivato: nella casella giusta si salta, se no passa la mano.
|
||||
function land(c) {
|
||||
const to = c.shot.to, n = c.target[c.current], hit = boxAt([to.x, to.y], c.skill.line);
|
||||
c.shot = null; c.stone = to;
|
||||
c.events.push({ kind: 'campana-sasso', power: 0.8 });
|
||||
if (hit && hit.id === n && !hit.line) {
|
||||
c.line = cap(`${who(c)}il sassolino è nella ${n}. Salta!`);
|
||||
c.beats = beats(n); c.beat = -1; c.hop = null; c.t = 0;
|
||||
c.step = c.current === 'cpu' ? 'cpuHop' : 'hop';
|
||||
if (c.step === 'hop') c.meter = { t: -0.5, dir: 1 };
|
||||
return;
|
||||
}
|
||||
fail(c, `${who(c)}${hit?.line ? 'sulla riga!' : hit ? `nella ${hit.id === 'cielo' ? 'campana in cima' : hit.id}, non nella ${n}.` : 'fuori dalla campana.'}`);
|
||||
}
|
||||
function fail(c, line) {
|
||||
c.line = cap(line); c.step = 'rest'; c.t = 0; c.meter = null;
|
||||
c.next = c.current === 'player' ? 'cpu' : 'player';
|
||||
c.events.push({ kind: 'campana-riga', power: 0.7 });
|
||||
}
|
||||
// Un salto riuscito: la casella dopo, o il sassolino raccolto, o fuori dalla campana (e la casella è fatta).
|
||||
function advance(c) {
|
||||
c.beat++;
|
||||
const b = c.beats[c.beat];
|
||||
c.hop = { t: 0 };
|
||||
if (b.pick) { c.stone = null; c.line = cap(`${who(c)}preso il sassolino!`); c.events.push({ kind: 'campana-sasso', power: 0.5 }); }
|
||||
else c.events.push({ kind: 'campana-salto', power: 0.6 });
|
||||
if (b.out) {
|
||||
const n = c.target[c.current]++;
|
||||
if (n >= LAST) {
|
||||
c.step = 'result'; c.meter = null;
|
||||
c.result = { winner: c.current, player: c.target.player - 1, cpu: c.target.cpu - 1 };
|
||||
return;
|
||||
}
|
||||
c.line = cap(`${who(c)}fatta la ${n}! Ora la ${n + 1}.`); c.step = 'done'; c.t = 0; c.meter = null;
|
||||
return;
|
||||
}
|
||||
if (c.meter) c.meter = { t: 0, dir: -c.meter.dir };
|
||||
}
|
||||
// La lancetta dell'equilibrio, da -1 a 1 (o al contrario, un salto sì e uno no).
|
||||
export function needle(c) {
|
||||
if (!c.meter) return 0;
|
||||
return c.meter.dir * (-1 + 2 * clamp(c.meter.t, 0, c.skill.sweep) / c.skill.sweep);
|
||||
}
|
||||
function jump(c) {
|
||||
if (c.hop && c.hop.t < HOP) return true; // ancora in aria: il tocco non conta
|
||||
const u = needle(c);
|
||||
if (Math.abs(u) <= c.skill.zone) { advance(c); return true; }
|
||||
const early = u * c.meter.dir < 0;
|
||||
fail(c, early ? 'Troppo presto: giù l\'altro piede!' : 'Troppo tardi: pestata la riga!');
|
||||
return true;
|
||||
}
|
||||
// Tocca all'altro, o allo stesso per la casella dopo: dalla partenza, senza sassolino.
|
||||
function turn(c, current) {
|
||||
Object.assign(c, { current, stone: null, beats: null, beat: -1, hop: null, meter: null, t: 0, next: null,
|
||||
step: current === 'player' ? 'aim' : 'cpuAim' });
|
||||
if (current === 'player') c.aim = center(c.target.player);
|
||||
}
|
||||
|
||||
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.hop) c.hop.t += dt;
|
||||
if (c.holding) {
|
||||
c.holdTime += dt; c.charging = c.holdTime >= TAP;
|
||||
const k = (c.holdTime / CHARGE) % 2; c.power = k <= 1 ? k : 2 - k;
|
||||
}
|
||||
if (c.step === 'flying') { c.shot.t += dt; if (c.shot.t >= FLIGHT) land(c); }
|
||||
else if (c.step === 'hop') {
|
||||
if (!c.hop || c.hop.t >= HOP) c.meter.t += dt;
|
||||
if (c.meter.t > c.skill.sweep) fail(c, 'Hai perso l\'equilibrio!');
|
||||
} else if (c.step === 'cpuAim' && c.t >= CPU_WAIT) {
|
||||
// Il Taglia: o nella casella, ben dentro, o mezza casella più in là (sulla riga o nell'altra).
|
||||
const n = c.target.cpu, p = center(n), ok = random(c) < c.skill.cpu.throw, j = () => (random(c) - 0.5) * 20;
|
||||
toss(c, ok ? { x: p.x + j(), y: p.y + j() } : { x: p.x + (random(c) < 0.5 ? -1 : 1) * W * (0.5 + random(c) * 0.3), y: p.y + j() });
|
||||
} else if (c.step === 'cpuHop' && c.t >= CPU_BEAT) {
|
||||
c.t = 0;
|
||||
if (random(c) < c.skill.cpu.slip) fail(c, random(c) < 0.5 ? 'Il Taglia ha pestato la riga!' : 'Il Taglia ha messo giù l\'altro piede!');
|
||||
else advance(c);
|
||||
} else if (c.step === 'rest' && c.t >= REST) turn(c, c.next);
|
||||
else if (c.step === 'done' && c.t >= DONE) turn(c, c.current);
|
||||
}
|
||||
|
||||
// --- Comandi: la mira segue il mouse o il dito; tenere premuto carica la forza, lasciare tira; ai salti si preme.
|
||||
export function aimScreen(c, [x, y]) {
|
||||
if (!canAdjust(c) || c.holding || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||
c.aim = { x: clamp(x, FROM.x + 30, 630), y: clamp(y, 80, 285) }; return true;
|
||||
}
|
||||
export function setPower(c, v) { if (!canAdjust(c) || c.holding) return false; c.power = clamp(v, 0, 1); return true; }
|
||||
export function nudge(c, dir) { if (!canAdjust(c) || c.holding) return false; c.aim.y = clamp(c.aim.y + dir * 3, 80, 285); return true; }
|
||||
// Frecce: su e giù la mira, destra e sinistra la forza (più in là, più in qua).
|
||||
export function key(c, key, down) {
|
||||
const move = { ArrowUp: [0, -3], ArrowDown: [0, 3], ArrowLeft: [-0.01, 0], ArrowRight: [0.01, 0] }[key];
|
||||
if (!move) return false;
|
||||
if (down && canAdjust(c) && !c.holding) { c.aim.y = clamp(c.aim.y + move[1], 80, 285); c.power = clamp(c.power + move[0], 0, 1); }
|
||||
return true;
|
||||
}
|
||||
export function press(c, pointerId) {
|
||||
if (c.current !== 'player') return false;
|
||||
if (c.step === 'hop') return jump(c);
|
||||
if (!canAdjust(c) || c.holding) return false;
|
||||
c.pointerId = pointerId; c.holding = true; c.charging = false; c.holdTime = 0; c.power = 0;
|
||||
return true;
|
||||
}
|
||||
export function cancel(c, pointerId) {
|
||||
if (pointerId !== undefined && c.pointerId !== pointerId) return false;
|
||||
if (!c.holding) return false;
|
||||
c.holding = c.charging = false; c.holdTime = 0; return true;
|
||||
}
|
||||
export function release(c, pointerId) {
|
||||
if (c.pointerId !== pointerId || !c.holding) return false;
|
||||
if (c.holdTime < TAP) return cancel(c, pointerId); // il tocco secco mira soltanto
|
||||
c.holding = c.charging = false; c.holdTime = 0;
|
||||
return throwStone(c);
|
||||
}
|
||||
// Il pulsante: tira con la forza del cursore, o salta.
|
||||
export function shoot(c) {
|
||||
if (c.current !== 'player' || c.holding) return false;
|
||||
if (c.step === 'hop') return jump(c);
|
||||
return canAdjust(c) && throwStone(c);
|
||||
}
|
||||
|
||||
const box = n => n > LAST ? 'finito' : `alla ${n}`;
|
||||
export function button(c) {
|
||||
if (c.step === 'result') return 'Finita';
|
||||
if (c.current === 'cpu') return 'Tocca al Taglia…';
|
||||
return { aim: 'Tira', flying: 'In volo…', hop: 'Salta' }[c.step] ?? 'Aspetta…';
|
||||
}
|
||||
export function status(c) {
|
||||
const score = `Andrea ${box(c.target.player)}, il Taglia ${box(c.target.cpu)}.`;
|
||||
if (c.step === 'result') return `${c.result.winner === 'player' ? 'Campana finita: battuto il Taglia! ' : 'Il Taglia ha finito la campana. '}${score}`;
|
||||
const hint = c.current === 'cpu' ? 'Tocca al Taglia.' : c.step === 'hop' ? 'Salta quando la lancetta è nel verde.'
|
||||
: `Tira il sassolino nella ${c.target.player}: mira, tieni premuto per la forza e lascia.`;
|
||||
return `${score} ${c.line ?? hint}`;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { CARTONE, BENCH, CAN, BAR, BOTTLE, BALLOON, VOLO, PELLETS, STILL, BREATH, GASP, rings, reticle, landing, balloon, voloAt, canThrow, breathless } from './condor.js';
|
||||
import { text, panel } from './bocce-render.js';
|
||||
|
||||
function rect(ctx, x, y, w, h, color) { ctx.fillStyle = color; ctx.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h)); }
|
||||
// Il cartone inchiodato al tronco: tre cerchi col pennarello, i chiodini in alto e i buchi dei pallini.
|
||||
function cartone(ctx, c) {
|
||||
const { x, y, w, h } = CARTONE;
|
||||
rect(ctx, x - w / 2 + 2, y - h / 2 + 2, w, h, 'rgba(30,20,10,0.35)');
|
||||
rect(ctx, x - w / 2, y - h / 2, w, h, '#c9a46a');
|
||||
rect(ctx, x - w / 2, y - h / 2, w, 2, '#e0bf86'); rect(ctx, x - w / 2, y + h / 2 - 2, w, 2, '#a8844e');
|
||||
rect(ctx, x - w / 2 + 3, y - h / 2 + 2, 2, 2, '#5a5a5a'); rect(ctx, x + w / 2 - 5, y - h / 2 + 2, 2, 2, '#5a5a5a');
|
||||
ctx.strokeStyle = '#2a1f1a'; ctx.lineWidth = 1.2;
|
||||
for (const [r] of rings(c)) { ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.stroke(); }
|
||||
ctx.fillStyle = '#2a1f1a'; ctx.beginPath(); ctx.arc(x, y, 2, 0, Math.PI * 2); ctx.fill();
|
||||
for (const p of c.holes) { rect(ctx, p.x - 1, p.y - 1, 2, 2, '#1a120c'); rect(ctx, p.x, p.y - 1, 1, 1, '#f2d24a'); }
|
||||
}
|
||||
// Una lattina (sulla panchina o in aria), rossa con la fascia chiara.
|
||||
function can(ctx, x, bottom, angle = 0, w = CAN.w, h = CAN.h, color = '#c8321e') {
|
||||
ctx.save(); ctx.translate(Math.round(x), Math.round(bottom)); ctx.rotate(angle);
|
||||
ctx.fillStyle = color; ctx.fillRect(-w / 2, -h, w, h);
|
||||
ctx.fillStyle = '#f2e6c8'; ctx.fillRect(-w / 2, -h + Math.round(h * 0.35), w, 3);
|
||||
ctx.fillStyle = '#c9cdd2'; ctx.fillRect(-w / 2, -h, w, 2); ctx.fillRect(-w / 2, -2, w, 2);
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.35)'; ctx.fillRect(-w / 2 + 2, -h + 2, 1, h - 4);
|
||||
ctx.restore();
|
||||
}
|
||||
// Una bottiglietta d'acqua, vuota: plastica azzurrina e il tappo blu.
|
||||
function bottle(ctx, x, bottom, angle = 0) {
|
||||
const { w, h } = BOTTLE;
|
||||
ctx.save(); ctx.translate(Math.round(x), Math.round(bottom)); ctx.rotate(angle);
|
||||
ctx.fillStyle = 'rgba(190,228,240,0.85)'; ctx.fillRect(-w / 2, -h + 4, w, h - 4); ctx.fillRect(-w / 2 + 1, -h + 2, w - 2, 2);
|
||||
ctx.fillStyle = '#2f6fb0'; ctx.fillRect(-1, -h, 3, 2);
|
||||
ctx.fillStyle = '#ffffff'; ctx.fillRect(-w / 2 + 1, -h + 5, 1, h - 7);
|
||||
ctx.restore();
|
||||
}
|
||||
// Il pallino giallo.
|
||||
function pellet(ctx, x, y, r) {
|
||||
ctx.fillStyle = '#7a5a10'; ctx.beginPath(); ctx.arc(x, y, r + 0.7, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillStyle = '#f5d02a'; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
|
||||
if (r > 1.5) rect(ctx, x - r / 2, y - r / 2, 1, 1, '#fff6b0');
|
||||
}
|
||||
// La Condor in basso, nera, puntata verso il mirino: il calcio di legno dritto in giù (la mano lo tiene così), la canna
|
||||
// che gira verso il mirino, il mirino in punta.
|
||||
function pistol(ctx, to) {
|
||||
const from = { x: 430, y: 330 }, a = Math.atan2(to.y - from.y, to.x - from.x);
|
||||
rect(ctx, from.x - 13, from.y + 2, 26, 34, '#1a120c'); rect(ctx, from.x - 12, from.y + 2, 24, 32, '#5a3a22');
|
||||
rect(ctx, from.x - 9, from.y + 5, 4, 26, '#7a5232'); rect(ctx, from.x - 2, from.y + 8, 2, 20, '#3e2816');
|
||||
ctx.save(); ctx.translate(from.x, from.y); ctx.rotate(a);
|
||||
ctx.fillStyle = '#0b0b0e'; ctx.fillRect(-16, -11, 84, 20);
|
||||
ctx.fillStyle = '#26252c'; ctx.fillRect(-14, -9, 80, 5);
|
||||
ctx.fillStyle = '#4a4955'; ctx.fillRect(-14, -9, 80, 1);
|
||||
ctx.fillStyle = '#0b0b0e'; ctx.fillRect(62, -14, 5, 4); ctx.fillRect(66, -5, 6, 10);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
export function drawCondor(ctx, c, def, bg) {
|
||||
ctx.save(); ctx.imageSmoothingEnabled = false;
|
||||
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#7fb24a'; ctx.fillRect(0, 0, 640, 360); }
|
||||
cartone(ctx, c);
|
||||
for (const k of c.bottles) if (k.fall < 1) bottle(ctx, k.x + k.fall * 6, BAR + k.fall * 10, k.fall * 1.6);
|
||||
for (const k of c.cans) if (k.fall < 1) can(ctx, k.x, BENCH + k.fall * 8, -k.fall * 1.4);
|
||||
// Il palloncino rosso legato alla sbarra dell'altalena; scoppiato, resta lo spago e qualche brandello.
|
||||
const b = balloon(c);
|
||||
ctx.strokeStyle = '#e8e2d0'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(BALLOON.x, BALLOON.knot);
|
||||
ctx.quadraticCurveTo(BALLOON.x + (b.x - BALLOON.x) * 0.3 + 3, (BALLOON.knot + b.y) / 2, b.x, c.balloon.popped ? b.y + 14 : b.y + BALLOON.r); ctx.stroke();
|
||||
if (!c.balloon.popped) {
|
||||
ctx.fillStyle = '#d8283a'; ctx.beginPath(); ctx.ellipse(b.x, b.y, BALLOON.r, BALLOON.r * 1.2, 0, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillStyle = '#ff8a8a'; ctx.fillRect(Math.round(b.x - 4), Math.round(b.y - 6), 2, 3);
|
||||
rect(ctx, b.x - 1, b.y + BALLOON.r * 1.2 - 1, 3, 2, '#a01828');
|
||||
} else if (c.balloon.pop < 1) {
|
||||
ctx.fillStyle = '#d8283a';
|
||||
for (let i = 0; i < 6; i++) { const a = i / 6 * Math.PI * 2, d = 4 + c.balloon.pop * 14; ctx.fillRect(Math.round(b.x + Math.cos(a) * d), Math.round(b.y + Math.sin(a) * d), 2, 2); }
|
||||
}
|
||||
// La lattina tirata in aria dal Taglia (o che ricade, colpita).
|
||||
const v = voloAt(c);
|
||||
if (c.volo?.fall) can(ctx, c.volo.fall.x, c.volo.fall.y + VOLO.h / 2, c.visual.time * 14, VOLO.w, VOLO.h, '#2f6fb0');
|
||||
else if (v) can(ctx, v.x, v.y + VOLO.h / 2, v.a, VOLO.w, VOLO.h, '#2f6fb0');
|
||||
// Il pallino in volo: grande vicino alla pistola, piccolo verso il bersaglio.
|
||||
if (c.shot) {
|
||||
const k = Math.min(1, c.shot.t / c.shot.time), { from, to } = c.shot;
|
||||
pellet(ctx, from.x + (to.x - from.x) * k, from.y + (to.y - from.y) * k, 4 - 2.8 * k);
|
||||
}
|
||||
const r = reticle(c);
|
||||
if (canThrow(c)) {
|
||||
// Alla bona: dove arriva il pallino, un puntino giallo sotto il mirino.
|
||||
if (c.skill.guide) { const l = landing(c); rect(ctx, l.x - 1, l.y - 1, 3, 3, '#f5d02a'); }
|
||||
const out = breathless(c);
|
||||
ctx.strokeStyle = out ? '#ff6a4a' : c.holding && c.holdTime >= STILL ? '#b9e5b3' : '#fff8dc'; ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.arc(r.x, r.y, 7, 0, Math.PI * 2); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(r.x - 11, r.y); ctx.lineTo(r.x - 3, r.y); ctx.moveTo(r.x + 3, r.y); ctx.lineTo(r.x + 11, r.y);
|
||||
ctx.moveTo(r.x, r.y - 11); ctx.lineTo(r.x, r.y - 3); ctx.moveTo(r.x, r.y + 3); ctx.lineTo(r.x, r.y + 11); ctx.stroke();
|
||||
}
|
||||
pistol(ctx, r);
|
||||
// Il fiato trattenuto: la barra si riempie fino a «fermo» (verde), poi si svuota in rosso finché il colpo parte.
|
||||
if (canThrow(c) && c.holding) {
|
||||
const x = 16, y = 300, h = 50, still = Math.min(1, c.holdTime / STILL), left = c.holdTime > BREATH ? 1 - (c.holdTime - BREATH) / GASP : 1;
|
||||
rect(ctx, x - 3, y - 3, 18, h + 6, '#17160fd0');
|
||||
const f = Math.round(h * (c.holdTime > BREATH ? left : still));
|
||||
rect(ctx, x, y + h - f, 12, f, c.holdTime > BREATH ? '#ff6a4a' : still >= 1 ? '#9be07c' : '#ffd23c');
|
||||
text(ctx, 'FIATO', x + 6, y - 9, 6, '#efd49e', 'center');
|
||||
}
|
||||
// In alto: i punti, quelli del Taglia, i pallini rimasti e cosa è successo.
|
||||
rect(ctx, 0, 0, 640, 22, '#17160fe8');
|
||||
text(ctx, 'LA CONDOR', 8, 7, 8, '#efd49e');
|
||||
text(ctx, { bona: 'alla bona', seria: 'seria', male: 'a fassi male' }[c.level], 8, 16, 7, '#d9ccba');
|
||||
text(ctx, `Punti ${c.points}`, 200, 11, 9, c.points > c.taglia ? '#b9e5b3' : '#fff0c8', 'center');
|
||||
text(ctx, `Il Taglia ${c.taglia}`, 310, 11, 9, '#9fd0ff', 'center');
|
||||
for (let i = 0; i < PELLETS; i++) pellet(ctx, 430 + i * 15, 11, i < c.left ? 3 : 1.2);
|
||||
const line = breathless(c) ? 'Il fiato finisce: spara!' : c.line;
|
||||
if (line) { rect(ctx, 170, 24, 300, 14, '#17160fd0'); text(ctx, line, 320, 31, 8, breathless(c) || /Niente|fuori|Senza/.test(line) ? '#ffbf9f' : '#9fd0ff', 'center'); }
|
||||
if (c.step === 'result') {
|
||||
const w = c.result.winner;
|
||||
panel(ctx, 170, 140, 300, 76);
|
||||
text(ctx, { player: 'BATTUTO IL TAGLIA!', draw: 'PARI COL TAGLIA', cpu: 'HA VINTO IL TAGLIA' }[w], 320, 160, 12, w === 'player' ? '#b9e5b3' : '#efd49e', 'center');
|
||||
text(ctx, `Tu ${c.result.points}, il Taglia ${c.result.taglia}`, 320, 182, 8, '#efd49e', 'center');
|
||||
text(ctx, 'Torna al campone per uscire.', 320, 200, 8, '#d9ccba', 'center');
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// La Condor al campone delle poste (idea di Andrea e del Taglia, 29 ottobre): la pistola ad aria compressa coi pallini
|
||||
// gialli. Si gareggia col Taglia: lui ha già tirato, e il suo punteggio è quello da battere. Come la cerbottana: il
|
||||
// mirino ondeggia col respiro; tenere premuto trattiene il fiato (il mirino si ferma), lasciare spara; chi tiene troppo
|
||||
// resta senza fiato, il mirino trema e il colpo parte da solo. Il pallino cala con la distanza: l'albero è vicino, la
|
||||
// panchina a metà, l'altalena e la porta in fondo (lì si mira più sopra). Bersagli: il cartone inchiodato all'albero, le lattine sulla panchina rossa, le
|
||||
// bottigliette sulla traversa della porta, il palloncino legato all'altalena e (a seria e a fassi male) la lattina che
|
||||
// il Taglia tira in aria. Unità: i pixel del primo piano 640×360.
|
||||
|
||||
export const PELLETS = 12, TAP = 0.14, REST = 0.6, STILL = 0.8, BREATH = 2.6, GASP = 1, DROP = 26;
|
||||
// Dove stanno le cose nello sfondo di Codex (assets/closeups/campone-condor.png): il cartone sul tronco, il sedile
|
||||
// della panchina, la traversa della porta, la sbarra dell'altalena dove è legato il palloncino.
|
||||
export const CARTONE = { x: 98, y: 140, w: 46, h: 50, rings: [[5, 10], [11, 5], [17, 2]] };
|
||||
export const BENCH = 210, CAN = { w: 10, h: 14, points: 3 };
|
||||
export const BAR = 177, BOTTLE = { w: 6, h: 12, points: 5 };
|
||||
export const BALLOON = { x: 428, y: 134, knot: 170, r: 8, points: 6 };
|
||||
export const VOLO = { w: 12, h: 16, points: 8 };
|
||||
// I livelli: quanto ondeggia il mirino (px), se si vede dove arriva il pallino, la lattina in aria (ogni quanti
|
||||
// secondi, quanto ci mette ad attraversare), quanto sono grandi i cerchi del cartone e quanto ha fatto il Taglia.
|
||||
export const LEVELS = {
|
||||
bona: { sway: 3, guide: true, volo: null, rings: 1.25, taglia: 22 },
|
||||
seria: { sway: 6, guide: false, volo: { every: 6, cross: 2.2 }, rings: 1, taglia: 34 },
|
||||
male: { sway: 10, guide: false, volo: { every: 4.5, cross: 1.7 }, rings: 0.85, taglia: 44 },
|
||||
};
|
||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
|
||||
|
||||
export function newCondor(seed = Date.now() >>> 0, def = {}) {
|
||||
const level = LEVELS[def.level] ? def.level : 'seria';
|
||||
const c = { step: 'aim', t: 0, seed, level, skill: LEVELS[level], aim: { x: CARTONE.x, y: CARTONE.y }, power: 0,
|
||||
holding: false, holdTime: 0, charging: false, left: PELLETS, points: 0, events: [], shot: null, holes: [], line: null,
|
||||
cans: [246, 276, 306, 336].map(x => ({ x, down: false, fall: 0 })),
|
||||
bottles: [538, 560, 582].map(x => ({ x, down: false, fall: 0 })),
|
||||
balloon: { popped: false, pop: 0 }, volo: null, nextVolo: 2.5, visual: { time: 0 } };
|
||||
// Il punteggio del Taglia: quello del livello, qualche punto in più o in meno.
|
||||
c.taglia = c.skill.taglia + Math.floor(random(c) * 7) - 3;
|
||||
return c;
|
||||
}
|
||||
export const canThrow = c => c.step === 'aim' && c.left > 0;
|
||||
export const canAdjust = canThrow;
|
||||
export const rings = c => CARTONE.rings.map(([r, p]) => [r * c.skill.rings, p]);
|
||||
|
||||
// Il fiato: fermo dopo STILL secondi di respiro trattenuto (il mirino ondeggia un terzo), poi dopo BREATH comincia a
|
||||
// mancare e il mirino trema sempre di più; dopo GASP ancora il colpo parte da solo.
|
||||
export function steady(c) {
|
||||
if (!c.holding) return 1;
|
||||
const h = c.holdTime;
|
||||
if (h < BREATH) return 1 - 0.7 * Math.min(1, h / STILL);
|
||||
return 0.3 + (h - BREATH) * 2.5;
|
||||
}
|
||||
export function sway(c) {
|
||||
const t = c.visual.time, a = c.skill.sway * steady(c), shake = c.holding && c.holdTime > BREATH ? Math.sin(t * 31) * 0.4 : 0;
|
||||
return { x: a * (Math.sin(t * 1.7) + 0.5 * Math.sin(t * 3.1 + 1) + shake), y: a * 0.8 * (Math.sin(t * 1.3 + 2) + 0.5 * Math.sin(t * 2.9) + shake) };
|
||||
}
|
||||
export function reticle(c) { const s = sway(c); return { x: c.aim.x + s.x, y: c.aim.y + s.y }; }
|
||||
// Quanto è lontano un punto, a strisce: l'albero, la panchina, l'altalena, la porta in fondo.
|
||||
export const far = x => x < 170 ? 0.45 : x < 380 ? 0.6 : x < 490 ? 0.85 : 1;
|
||||
// Dove arriva il pallino: sotto il mirino, tanto più quanto il punto è lontano.
|
||||
export function landing(c) { const r = reticle(c); return { x: r.x, y: r.y + DROP * far(r.x) ** 2 }; }
|
||||
export const flight = x => 0.22 + 0.2 * far(x);
|
||||
// Il palloncino legato all'altalena: dondola col vento.
|
||||
export function balloon(c) {
|
||||
const t = c.visual.time;
|
||||
return { x: BALLOON.x + 7 * Math.sin(t * 1.1), y: BALLOON.y + 4 * Math.sin(t * 1.7 + 1) };
|
||||
}
|
||||
// La lattina che tira il Taglia: da destra in basso, una parabola verso sinistra.
|
||||
export function voloAt(c, t = c.volo?.t) {
|
||||
if (!c.volo) return null;
|
||||
const k = t / c.volo.cross;
|
||||
if (k < 0 || k > 1) return null;
|
||||
return { x: 610 - 560 * k, y: 300 - 4 * 230 * k * (1 - k), a: k * 9 };
|
||||
}
|
||||
|
||||
export function shoot(c) {
|
||||
if (!canThrow(c) || c.holding) return false;
|
||||
return fire(c);
|
||||
}
|
||||
function fire(c, gasp = false) {
|
||||
const to = landing(c);
|
||||
c.left--;
|
||||
// La lattina in aria è vicina (la tira il Taglia, accanto): lì il pallino non fa in tempo a calare.
|
||||
c.shot = { from: { x: 352, y: 372 }, to, aim: reticle(c), t: 0, time: flight(to.x), gasp };
|
||||
c.step = 'flying'; c.line = null;
|
||||
c.events.push({ kind: 'condor-pam', power: 1 });
|
||||
return true;
|
||||
}
|
||||
const inBox = (x, y, cx, bottom, w, h) => Math.abs(x - cx) <= w / 2 + 1 && y >= bottom - h - 1 && y <= bottom + 1;
|
||||
// Arrivato: prima quello che si muove (la lattina in aria, il palloncino), poi bottiglie, lattine e il cartone.
|
||||
function land(c) {
|
||||
const { x, y } = c.shot.to;
|
||||
let got = 0, what = null;
|
||||
const v = voloAt(c), a = c.shot.aim;
|
||||
if (v && !c.volo.hit && Math.hypot(a.x - v.x, a.y - v.y) <= VOLO.w / 2 + 2) {
|
||||
got = VOLO.points; c.volo.hit = true; c.volo.fall = { x: v.x, y: v.y, vy: -60 }; what = `Presa al volo! ${got} punti.`;
|
||||
c.events.push({ kind: 'cerbottana-latta', power: 1 });
|
||||
}
|
||||
if (!what && !c.balloon.popped) {
|
||||
const b = balloon(c);
|
||||
if (Math.hypot(x - b.x, y - b.y) <= BALLOON.r + 1) {
|
||||
c.balloon.popped = true; got = BALLOON.points; what = `Bum! Il palloncino. ${got} punti.`;
|
||||
c.events.push({ kind: 'condor-palloncino', power: 1 });
|
||||
}
|
||||
}
|
||||
if (!what) {
|
||||
const bottle = c.bottles.find(k => !k.down && inBox(x, y, k.x, BAR, BOTTLE.w, BOTTLE.h));
|
||||
if (bottle) { bottle.down = true; got = BOTTLE.points; what = `Bottiglietta giù! ${got} punti.`; c.events.push({ kind: 'condor-plastica', power: 0.8 }); }
|
||||
}
|
||||
if (!what) {
|
||||
const can = c.cans.find(k => !k.down && inBox(x, y, k.x, BENCH, CAN.w, CAN.h));
|
||||
if (can) { can.down = true; got = CAN.points; what = `Lattina giù! ${got} punti.`; c.events.push({ kind: 'cerbottana-latta', power: 0.8 }); }
|
||||
}
|
||||
if (!what) {
|
||||
const inCard = Math.abs(x - CARTONE.x) <= CARTONE.w / 2 && Math.abs(y - CARTONE.y) <= CARTONE.h / 2;
|
||||
if (inCard) {
|
||||
c.holes.push({ x, y });
|
||||
const d = Math.hypot(x - CARTONE.x, y - CARTONE.y), ring = rings(c).find(([r]) => d <= r);
|
||||
got = ring ? ring[1] : 0;
|
||||
what = got ? `Nel cartone: ${got} ${got === 1 ? 'punto' : 'punti'}.` : 'Nel cartone, fuori dai cerchi.';
|
||||
c.events.push({ kind: 'cerbottana-muro', power: 0.5 });
|
||||
} else what = 'Niente: il pallino giallo è finito nel prato.';
|
||||
}
|
||||
if (c.shot.gasp && !got) what = 'Senza fiato! ' + what;
|
||||
c.points += got; c.line = what;
|
||||
c.shot = null; c.step = 'rest'; c.t = 0;
|
||||
}
|
||||
|
||||
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;
|
||||
for (const k of [...c.cans, ...c.bottles]) if (k.down) k.fall = Math.min(1, k.fall + dt * 3);
|
||||
if (c.balloon.popped) c.balloon.pop = Math.min(1, c.balloon.pop + dt * 4);
|
||||
// La lattina in aria: il Taglia la tira ogni tanto (non a fine partita).
|
||||
const v = c.skill.volo;
|
||||
if (v && c.step !== 'result') {
|
||||
if (c.volo) {
|
||||
c.volo.t += dt;
|
||||
if (c.volo.fall) { c.volo.fall.vy += 600 * dt; c.volo.fall.y += c.volo.fall.vy * dt; c.volo.fall.x -= 40 * dt; }
|
||||
if (c.volo.t > c.volo.cross + 0.6) { c.volo = null; c.nextVolo = v.every * (0.8 + random(c) * 0.4); }
|
||||
} else if ((c.nextVolo -= dt) <= 0) {
|
||||
c.volo = { t: 0, cross: v.cross, hit: false, fall: null };
|
||||
c.events.push({ kind: 'condor-lancio', power: 0.6 });
|
||||
}
|
||||
}
|
||||
if (c.holding) {
|
||||
c.holdTime += dt; c.charging = c.holdTime >= TAP;
|
||||
c.power = clamp(c.holdTime / STILL, 0, 1);
|
||||
if (c.holdTime > BREATH + GASP) { c.holding = c.charging = false; c.holdTime = 0; c.power = 0; fire(c, true); }
|
||||
}
|
||||
if (c.step === 'flying') {
|
||||
c.shot.t += dt; if (c.shot.t >= c.shot.time) land(c);
|
||||
} else if (c.step === 'rest' && c.t >= REST) {
|
||||
if (c.left > 0) c.step = 'aim';
|
||||
else {
|
||||
c.step = 'result';
|
||||
const winner = c.points > c.taglia ? 'player' : c.points === c.taglia ? 'draw' : 'cpu';
|
||||
c.result = { winner, points: c.points, taglia: c.taglia };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Comandi: il mirino segue il mouse o il dito; tenere premuto trattiene il fiato, lasciare spara.
|
||||
export function aimScreen(c, [x, y]) {
|
||||
if (!canThrow(c) || c.holding || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||
c.aim = { x: clamp(x, 10, 630), y: clamp(y, 30, 320) }; return true;
|
||||
}
|
||||
export function nudge(c, dir) { if (!canThrow(c) || c.holding) return false; c.aim.x = clamp(c.aim.x + dir * 4, 10, 630); return true; }
|
||||
export function key(c, key, down) {
|
||||
const move = { ArrowLeft: [-4, 0], ArrowRight: [4, 0], ArrowUp: [0, -4], ArrowDown: [0, 4] }[key];
|
||||
if (!move) return false;
|
||||
if (down && canThrow(c) && !c.holding) c.aim = { x: clamp(c.aim.x + move[0], 10, 630), y: clamp(c.aim.y + move[1], 30, 320) };
|
||||
return true;
|
||||
}
|
||||
export function press(c, pointerId) {
|
||||
if (!canThrow(c) || c.holding) return false;
|
||||
c.pointerId = pointerId; c.holding = true; c.charging = false; c.holdTime = 0; c.power = 0;
|
||||
return true;
|
||||
}
|
||||
export function cancel(c, pointerId) {
|
||||
if (pointerId !== undefined && c.pointerId !== pointerId) return false;
|
||||
if (!c.holding) return false;
|
||||
c.holding = c.charging = false; c.holdTime = 0; c.power = 0; return true;
|
||||
}
|
||||
export function release(c, pointerId) {
|
||||
if (c.pointerId !== pointerId || !c.holding) return false;
|
||||
if (c.holdTime < TAP) return cancel(c, pointerId); // il tocco secco mira soltanto
|
||||
c.holding = c.charging = false; c.holdTime = 0; c.power = 0;
|
||||
return fire(c);
|
||||
}
|
||||
export const breathless = c => c.holding && c.holdTime > BREATH;
|
||||
|
||||
export function button(c) { return c.step === 'result' ? 'Finita' : c.step === 'aim' ? 'Spara' : c.step === 'flying' ? 'In volo…' : 'Ricarico…'; }
|
||||
export function status(c) {
|
||||
const score = `Punti ${c.points}, il Taglia ne ha fatti ${c.taglia}; pallini ${c.left} di ${PELLETS}.`;
|
||||
if (c.step === 'result') return `${{ player: 'Battuto il Taglia! ', draw: 'Pari col Taglia. ', cpu: 'Ha vinto il Taglia. ' }[c.result.winner]}${score}`;
|
||||
return `${score} ${c.line ?? 'Mira, tieni premuto per trattenere il fiato e lascia per sparare.'}`;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// La macchina del tempo vista da fuori (story.closeups, type 'corsa'; Andrea, 30 ottobre). In partenza ('parti') va via
|
||||
// giù per la strada accelerando, il fulmine la prende, il bianco, e restano due sgommate di fuoco che bruciano e si
|
||||
// spengono. All'arrivo ('arriva') il fulmine in fondo alla strada, e lei viene avanti e ci passa accanto, lasciando le
|
||||
// stesse strisce (davanti a noi: dietro alla macchina non si vedrebbero). Sopra lo sfondo della scena (`scene`), coi fotogrammi di Codex (`frames`: dietro, dietro coi lampi,
|
||||
// davanti coi lampi). Unità: i pixel della scena 640×360.
|
||||
|
||||
// I tempi, in secondi. Partenza: ferma che si carica, la corsa, il fulmine, il bianco, le fiamme, la fine.
|
||||
export const PARTI = { charge: 0.6, run: 2.2, bolt: 2.3, flash: 0.35, burn: 4.6, end: 5.2 };
|
||||
// Arrivo: il fulmine in fondo, la corsa verso di noi fin fuori dal quadro, le fiamme, la fine.
|
||||
export const ARRIVA = { bolt: 0.3, flash: 0.35, run: 1.5, burn: 4, end: 4.6 };
|
||||
|
||||
const lerp = (a, b, k) => a + (b - a) * k;
|
||||
const clamp01 = k => Math.max(0, Math.min(1, k));
|
||||
// Dove sta la macchina a un punto `k` del percorso (0 vicina, 1 in fondo): posizione delle ruote e grandezza.
|
||||
export function along(def, k) {
|
||||
const [a, b] = def.path;
|
||||
return { x: lerp(a[0], b[0], k), y: lerp(a[1], b[1], k), scale: lerp(a[2], b[2], k) };
|
||||
}
|
||||
// Il punto del percorso al tempo t: in partenza accelera (da fermo); all'arrivo viene avanti dal fondo fin oltre il
|
||||
// punto 0 (fuori dal quadro, accanto a noi).
|
||||
export function progress(c, def) {
|
||||
if (def.mode === 'arriva') return 1 - clamp01((c.t - ARRIVA.bolt) / ARRIVA.run);
|
||||
const k = clamp01((c.t - PARTI.charge) / (PARTI.run - PARTI.charge));
|
||||
return Math.min(0.92, k * k);
|
||||
}
|
||||
// Si vede la macchina? In partenza fino al fulmine; all'arrivo dal fulmine in poi.
|
||||
export function carShown(c, def) {
|
||||
return def.mode === 'arriva' ? c.t >= ARRIVA.bolt + 0.05 && c.t < ARRIVA.bolt + ARRIVA.run : c.t < PARTI.bolt + 0.05;
|
||||
}
|
||||
// Dove arriva il fulmine: sulla macchina in partenza, in fondo alla strada all'arrivo.
|
||||
export function boltAt(def) { return def.mode === 'arriva' ? along(def, 1) : along(def, progress({ t: PARTI.bolt }, def)); }
|
||||
const T = def => def.mode === 'arriva' ? ARRIVA : PARTI;
|
||||
|
||||
export function newCorsa(id) { return { id, t: 0, age: 0, events: [] }; }
|
||||
// Il tempo che passa e i suoni; vero quando è finita.
|
||||
export function updateCorsa(c, def, dt) {
|
||||
c.t += dt;
|
||||
const times = T(def);
|
||||
if (def.mode !== 'arriva' && !c.charge && c.t >= 0.1) { c.charge = true; c.events.push({ kind: 'gara-turbo', power: 1 }); }
|
||||
if (!c.boom && c.t >= times.bolt) { c.boom = true; c.events.push({ kind: 'corsa-tuono', power: 1 }); }
|
||||
if (!c.fire && c.t >= (def.mode === 'arriva' ? ARRIVA.bolt + 0.2 : PARTI.charge + 0.3)) { c.fire = true; c.events.push({ kind: 'corsa-fuoco', power: 1 }); }
|
||||
return c.t >= times.end;
|
||||
}
|
||||
|
||||
// Le due ruote posteriori (o anteriori) a un punto del percorso: una a sinistra e una a destra.
|
||||
function wheels(def, k) {
|
||||
const p = along(def, k), half = def.track * p.scale;
|
||||
return [[p.x - half, p.y], [p.x + half, p.y]];
|
||||
}
|
||||
// Le sgommate di fuoco: da dove è partita a dove è arrivata, fiammelle che tremano, più piccole in fondo, che si
|
||||
// spengono dopo `burn`.
|
||||
function fire(ctx, c, def) {
|
||||
const times = T(def), shown = def.mode === 'arriva' ? [Math.max(progress(c, def), 0), 1] : [0, progress({ t: Math.min(c.t, times.bolt) }, def)];
|
||||
const fade = c.t < times.burn ? 1 : clamp01(1 - (c.t - times.burn) / (times.end - times.burn));
|
||||
if (fade <= 0 || shown[1] - shown[0] < 0.005) return;
|
||||
for (let side = 0; side < 2; side++) {
|
||||
// Prima il segno nero bruciato, poi le fiamme sopra.
|
||||
const [a, b] = [wheels(def, shown[0])[side], wheels(def, shown[1])[side]];
|
||||
ctx.strokeStyle = `rgba(30,18,10,${0.7 * Math.max(fade, 0.5)})`; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke();
|
||||
const n = 70;
|
||||
for (let i = 0; i <= n; i++) {
|
||||
const k = lerp(shown[0], shown[1], i / n), [x, y] = wheels(def, k)[side], s = Math.max(0.12, along(def, k).scale);
|
||||
// Lingue di fuoco di altezza diversa, che tremano: rosso fuori, arancio, il cuore giallo.
|
||||
const flick = 0.55 + 0.45 * Math.sin(c.t * 19 + i * 2.3 + side * 2) * Math.sin(c.t * 7 + i * 0.9);
|
||||
const h = (16 + 22 * flick) * s * fade, w = Math.max(2, 9 * s);
|
||||
if (h < 1.5) continue;
|
||||
ctx.fillStyle = `rgba(255,120,30,${0.18 * fade})`; ctx.fillRect(Math.round(x - w), Math.round(y - h * 1.2), Math.ceil(w * 2), Math.ceil(h * 1.2));
|
||||
ctx.fillStyle = `rgba(220,50,20,${0.9 * fade})`; ctx.fillRect(Math.round(x - w / 2), Math.round(y - h), Math.ceil(w), Math.ceil(h));
|
||||
ctx.fillStyle = `rgba(255,${Math.round(140 + 60 * flick)},30,${0.95 * fade})`; ctx.fillRect(Math.round(x - w / 3), Math.round(y - h * 0.7), Math.ceil(w * 0.66), Math.ceil(h * 0.7));
|
||||
ctx.fillStyle = `rgba(255,245,170,${fade})`; ctx.fillRect(Math.round(x - w / 6), Math.round(y - h * 0.35), Math.max(1, Math.ceil(w / 3)), Math.ceil(h * 0.35));
|
||||
// Un filo di fumo che sale quando le fiamme calano.
|
||||
if (c.t > times.burn - 1 && i % 8 === 0) {
|
||||
ctx.fillStyle = `rgba(90,90,96,${0.25 * fade})`;
|
||||
ctx.fillRect(Math.round(x - 2 * s), Math.round(y - h - 6 * s - ((c.t * 18 + i) % 14) * s), Math.ceil(4 * s), Math.ceil(4 * s));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Il fulmine: una saetta spezzata dal cielo fino al punto, con due rami.
|
||||
function bolt(ctx, c, def) {
|
||||
const times = T(def), dt = c.t - times.bolt;
|
||||
if (dt < -0.12 || dt > 0.28) return;
|
||||
const p = boltAt(def), seed = Math.floor(c.t * 30);
|
||||
ctx.save();
|
||||
for (const [width, color] of [[6, 'rgba(140,200,255,0.5)'], [2, '#ffffff']]) {
|
||||
ctx.strokeStyle = color; ctx.lineWidth = width; ctx.beginPath();
|
||||
let x = p.x + 40, y = 0; ctx.moveTo(x, y);
|
||||
const steps = 9;
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
x = lerp(p.x + 40, p.x, i / steps) + (i < steps ? ((seed * (i + 5) * 53) % 30) - 15 : 0);
|
||||
y = lerp(0, p.y - 12 * p.scale, i / steps); ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
export function drawCorsa(ctx, c, def, { bg, frames, tint }) {
|
||||
const times = T(def);
|
||||
ctx.save(); ctx.imageSmoothingEnabled = false;
|
||||
// La strada trema un po' quando la macchina si carica e quando arriva il fulmine.
|
||||
const shake = (def.mode !== 'arriva' && c.t < PARTI.charge + 0.4 ? 1.5 : 0) + (Math.abs(c.t - times.bolt) < 0.25 ? 3 : 0);
|
||||
ctx.translate(Math.round(Math.sin(c.t * 57) * shake), Math.round(Math.cos(c.t * 43) * shake));
|
||||
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#4a4a50'; ctx.fillRect(0, 0, 640, 360); }
|
||||
if (tint) { ctx.fillStyle = tint; ctx.fillRect(0, 0, 640, 360); }
|
||||
fire(ctx, c, def);
|
||||
if (carShown(c, def)) {
|
||||
const k = progress(c, def), p = along(def, k);
|
||||
// Da dietro: coi lampi mentre si carica e quando corre forte; davanti (all'arrivo) coi lampi finché frena.
|
||||
const img = def.mode === 'arriva' ? frames?.davanti : c.t > PARTI.charge * 0.5 && Math.floor(c.t * 14) % 3 !== 0 ? frames?.lampi : frames?.dietro;
|
||||
if (img) {
|
||||
const w = def.width * p.scale, h = w * img.height / img.width;
|
||||
ctx.drawImage(img, Math.round(p.x - w / 2), Math.round(p.y - h), Math.round(w), Math.round(h));
|
||||
} else { ctx.fillStyle = '#c8ccd2'; ctx.fillRect(Math.round(p.x - 40 * p.scale), Math.round(p.y - 40 * p.scale), Math.round(80 * p.scale), Math.round(40 * p.scale)); }
|
||||
}
|
||||
bolt(ctx, c, def);
|
||||
ctx.restore();
|
||||
// Il bianco del fulmine, che sfuma.
|
||||
const f = c.t - times.bolt;
|
||||
if (f >= 0 && f < times.flash) { ctx.fillStyle = `rgba(255,255,255,${1 - f / times.flash})`; ctx.fillRect(0, 0, 640, 360); }
|
||||
}
|
||||
@@ -12,6 +12,10 @@ import * as scopa from './scopa.js';
|
||||
import * as gara from './gara.js';
|
||||
import * as micro from './micro.js';
|
||||
import * as cerbottana from './cerbottana.js';
|
||||
import { newCorsa, updateCorsa } from './corsa.js';
|
||||
import { newIncanto, updateIncanto } from './incanto.js';
|
||||
import * as condor from './condor.js';
|
||||
import * as campana from './campana.js';
|
||||
import * as poker from './poker.js';
|
||||
|
||||
// I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta.
|
||||
@@ -26,6 +30,8 @@ export const MINIGAMES = {
|
||||
pesca: { create: (seed, level) => pesca.newPesca(seed, { level }), update: pesca.update, press: pesca.press, release: pesca.release, cancel: pesca.cancel, aim: pesca.aimScreen },
|
||||
poker: { create: (seed, level) => poker.newPoker(seed, { level }), update: poker.update, play: poker.play },
|
||||
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 },
|
||||
campana: { create: (seed, level) => campana.newCampana(seed, { level }), update: campana.update, press: campana.press, release: campana.release, cancel: campana.cancel, aim: campana.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 },
|
||||
};
|
||||
@@ -143,6 +149,9 @@ export function restoreState(story, data) {
|
||||
return { scene: data.scene, x, y, facing: data.facing === -1 ? -1 : 1, inventory, flags: pick(data.flags), used: pick(data.used), ended: data.ended === true };
|
||||
}
|
||||
|
||||
// Il manifesto ingrandito si apre con uno zoom: un tocco prima che sia aperto non conta.
|
||||
export const POSTER_READY = 0.35;
|
||||
|
||||
export class Game {
|
||||
// voiceTime(who, text): secondi della voce registrata per la battuta, se c'è (il fumetto non finisce prima).
|
||||
constructor(story, { state, onSave, voiceTime } = {}) {
|
||||
@@ -194,7 +203,13 @@ export class Game {
|
||||
return { scene, x: Math.round(x), y: Math.round(y), facing, inventory: [...inventory], flags: { ...flags }, used: { ...used }, ended };
|
||||
}
|
||||
|
||||
hotspots() { return (this.scene.hotspots ?? []).filter(h => check(h.when, this.state)); }
|
||||
// In una zona accesa con `exitsOnly` (la festa dell'Ingorgo: Andrea, 30 ottobre) si clicca solo per andare da un
|
||||
// posto all'altro: panchine, porte e statue restano dove sono, ma non rispondono.
|
||||
hotspots() {
|
||||
const list = (this.scene.hotspots ?? []).filter(h => check(h.when, this.state));
|
||||
const busy = (this.story.zones ?? []).some(z => z.exitsOnly && this.state.flags[z.flag] && z.scenes.includes(this.state.scene));
|
||||
return busy ? list.filter(h => h.exit) : list;
|
||||
}
|
||||
hotspot(id) { return this.hotspots().find(h => h.id === id); }
|
||||
people() { return (this.scene.people ?? []).filter(p => check(p.when, this.state)); }
|
||||
// Col mouse vale il punto preciso (l'ultimo della lista sta sopra). Col dito, `reach` allarga l'area: prima gli
|
||||
@@ -290,6 +305,13 @@ export class Game {
|
||||
closeupTap() {
|
||||
const c = this.closeup;
|
||||
if (!c || this.speech) return false;
|
||||
// Un manifesto ingrandito (type 'poster'): toccarlo lo chiude e fa partire `tap` (il desiderio dell'Ingorgo).
|
||||
if (this.story.closeups[c.id].type === 'poster') {
|
||||
if (c.t < POSTER_READY) return false;
|
||||
this.closeup = null;
|
||||
this.queue.unshift(...(this.story.closeups[c.id].tap ?? []));
|
||||
return true;
|
||||
}
|
||||
if (this.story.closeups[c.id].type) return false; // torcia e minigiochi non si sfogliano
|
||||
if (c.step === 'cover') Object.assign(c, { step: 'title', t: 0 });
|
||||
else if (c.step === 'title') Object.assign(c, { step: 'turn', t: 0 });
|
||||
@@ -323,7 +345,13 @@ export class Game {
|
||||
return true;
|
||||
}
|
||||
// Il viaggio si chiude da solo alla fine (o uscendo prima): si arriva comunque.
|
||||
if (def?.type === 'viaggio') { this.queue.unshift(...def.complete); return true; }
|
||||
if (def?.type === 'viaggio' || def?.type === 'corsa') { this.queue.unshift(...def.complete); return true; }
|
||||
// L'incantesimo, chiuso prima del lampo, fa comunque il suo cambio.
|
||||
if (def?.type === 'incanto') {
|
||||
if (!c.swapped) for (const e of def.swap ?? []) this.run(e);
|
||||
this.queue.unshift(...(def.complete ?? []));
|
||||
return true;
|
||||
}
|
||||
// I minigiochi: a partita finita (risultato deciso, anche se il conteggio si sta ancora mostrando) la battuta
|
||||
// di chi ha vinto; uscendo prima, quella di chi lascia.
|
||||
if (MINIGAMES[def?.type]) {
|
||||
@@ -425,15 +453,23 @@ export class Game {
|
||||
const talk = p.talk ?? ['talk'];
|
||||
return talk[Math.floor(this.time / 0.3) % talk.length];
|
||||
}
|
||||
// `flourish`: ogni tanto un gesto lungo a più fotogrammi (Baldoria che sventola il bandierone).
|
||||
const f = p.flourish;
|
||||
if (f) {
|
||||
const ft = (this.time + (p.offset ?? 0)) % f.every;
|
||||
if (ft < f.for) return f.poses[Math.floor(ft / (f.step ?? 0.35)) % f.poses.length];
|
||||
}
|
||||
const every = p.every ?? 4, t = (this.time + (p.offset ?? 0)) % every;
|
||||
return p.blink && t > every - 0.25 ? p.blink : idle;
|
||||
}
|
||||
|
||||
// La grandezza a quella altezza della scena; una fase può ingrandire tutti (`scale`: alla notte dell'Ingorgo la folla
|
||||
// dipinta è grande, e Andrea ci si perdeva in mezzo).
|
||||
scaleAt(y) {
|
||||
const d = this.scene.depth;
|
||||
if (!d) return 1;
|
||||
const d = this.scene.depth, k = this.phase()?.scale ?? 1;
|
||||
if (!d) return k;
|
||||
const [[y0, s0], [y1, s1]] = d, t = Math.max(0, Math.min(1, (y - y0) / (y1 - y0)));
|
||||
return s0 + (s1 - s0) * t;
|
||||
return (s0 + (s1 - s0) * t) * k;
|
||||
}
|
||||
|
||||
speakerPoint(who) {
|
||||
@@ -484,6 +520,13 @@ export class Game {
|
||||
if (c) {
|
||||
if (this.story.closeups[c.id].type === 'torch') { this.updateTorch(dt); return; }
|
||||
if (this.story.closeups[c.id].type === 'viaggio') { this.updateTrip(dt); return; }
|
||||
if (this.story.closeups[c.id].type === 'corsa') { if (updateCorsa(c, this.story.closeups[c.id], dt)) this.closeupClose(); return; }
|
||||
if (this.story.closeups[c.id].type === 'incanto') {
|
||||
const now = updateIncanto(c, dt);
|
||||
if (now === 'swap') for (const e of this.story.closeups[c.id].swap ?? []) this.run(e);
|
||||
if (now === 'end') this.closeupClose();
|
||||
return;
|
||||
}
|
||||
const m = this.minigame();
|
||||
if (m) {
|
||||
m.update(c, dt);
|
||||
@@ -526,6 +569,9 @@ export class Game {
|
||||
const type = this.story.closeups[e.closeup].type;
|
||||
this.closeup = type === 'torch' ? { id: e.closeup, step: 'search', t: 0, age: 0, found: 0, dwell: 0, beam: [320, 180] }
|
||||
: type === 'viaggio' ? { id: e.closeup, t: 0, age: 0, events: [], typed: 0 }
|
||||
: type === 'corsa' ? newCorsa(e.closeup)
|
||||
: type === 'incanto' ? newIncanto(e.closeup)
|
||||
: type === 'poster' ? { id: e.closeup, step: 'poster', t: 0, age: 0 }
|
||||
// `level`: «Alla bona», «Seria» o «A fassi male», scelto nel dialogo con l'avversario.
|
||||
// `decks`: il mazzo da usare quando il suo flag è acceso (briscola: le napoletane al posto delle toscane).
|
||||
: MINIGAMES[type] ? { id: e.closeup, ...MINIGAMES[type].create(Math.floor(Math.random() * 4294967296), e.level,
|
||||
@@ -579,6 +625,13 @@ export class Game {
|
||||
this.back = false;
|
||||
this.state.flags['visto:' + scene] = true; // per la mappa
|
||||
this.queue.unshift(...(this.scene.enter ?? []));
|
||||
// Le zone (story.zones): un flag che vale solo in certi posti, come la festa dell'Ingorgo. Uscendo (a piedi o
|
||||
// con la mappa) il flag si spegne e partono le battute del risveglio.
|
||||
for (const z of this.story.zones ?? []) {
|
||||
if (!this.state.flags[z.flag] || z.scenes.includes(scene)) continue;
|
||||
delete this.state.flags[z.flag];
|
||||
this.queue.unshift(...(z.leave ?? []));
|
||||
}
|
||||
this.dirty = true; // si salva quando l'ingresso nella scena è finito
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// L'incantesimo (story.closeups, type 'incanto'; Andrea, 30 ottobre: «Quanto darei per essere all'Ingorgo stasera?!?
|
||||
// Effetto speciale e compare»). Sopra la scena di adesso, con Andrea dentro: i fasci delle luci da palco che girano
|
||||
// sempre più svelti, i colpi di cassa che fanno pulsare l'aria, il lampo bianco; nel bianco cambia tutto (`swap`, di
|
||||
// solito il flag della festa: lo sfondo diventa quello della notte), poi il bianco sfuma e piovono i coriandoli.
|
||||
// Unità: i pixel della scena 640×360.
|
||||
|
||||
// I tempi, in secondi: i fasci, il lampo che sale, il cambio (a bianco pieno), il bianco che sfuma, la fine.
|
||||
export const T = { beams: 0.2, flash: 1.9, swap: 2.2, clear: 3.0, end: 3.8 };
|
||||
const COLORS = ['255,60,200', '60,220,255', '120,255,90', '255,190,40', '170,90,255'];
|
||||
const clamp01 = k => Math.max(0, Math.min(1, k));
|
||||
// Un numero a caso ma sempre lo stesso per lo stesso indice: i coriandoli non tremano da un fotogramma all'altro.
|
||||
const hash = n => { const x = Math.sin(n * 127.1 + 311.7) * 43758.5453; return x - Math.floor(x); };
|
||||
|
||||
export function newIncanto(id) { return { id, t: 0, age: 0, events: [], beats: 0, swapped: false }; }
|
||||
|
||||
// Il tempo che passa e i suoni (un colpo di cassa a ogni battito, sempre più fitti; il lampo). Torna 'swap' nel
|
||||
// momento del cambio, 'end' alla fine.
|
||||
export function updateIncanto(c, dt) {
|
||||
c.t += dt;
|
||||
const beats = Math.floor(beatAt(Math.min(c.t, T.flash)));
|
||||
for (; c.beats < beats; c.beats++) c.events.push({ kind: 'incanto-cassa', power: 0.5 + 0.5 * c.beats / 8 });
|
||||
if (!c.boom && c.t >= T.flash) { c.boom = true; c.events.push({ kind: 'incanto-lampo', power: 1 }); }
|
||||
if (!c.swapped && c.t >= T.swap) { c.swapped = true; return 'swap'; }
|
||||
return c.t >= T.end ? 'end' : null;
|
||||
}
|
||||
// Quanti battiti sono passati: accelerano, da uno ogni mezzo secondo a uno ogni decimo.
|
||||
export const beatAt = t => t <= 0 ? 0 : 2 * t + 1.6 * t * t;
|
||||
|
||||
// Quanto è bianco lo schermo: sale fino al cambio, resta pieno un attimo, poi sfuma.
|
||||
export function whiteness(t) {
|
||||
if (t < T.flash) return 0;
|
||||
if (t < T.swap) return clamp01((t - T.flash) / (T.swap - T.flash));
|
||||
return clamp01(1 - (t - T.swap) / (T.clear - T.swap));
|
||||
}
|
||||
|
||||
export function drawIncanto(ctx, c) {
|
||||
const t = c.t, before = t < T.swap;
|
||||
ctx.save();
|
||||
// Il colpo di cassa: un velo scuro che si apre e si chiude, l'aria che pulsa.
|
||||
if (before) {
|
||||
const b = beatAt(t), pulse = 1 - (b - Math.floor(b));
|
||||
ctx.fillStyle = `rgba(10,0,30,${0.25 * clamp01(t / 0.6) * pulse})`;
|
||||
ctx.fillRect(0, 0, 640, 360);
|
||||
}
|
||||
// I fasci: partono da sotto, ai lati, e spazzano il cielo; più passa il tempo, più sono e più girano svelti.
|
||||
const strength = before ? clamp01((t - T.beams) / 0.5) : clamp01(1 - (t - T.swap) / 0.6);
|
||||
if (strength > 0) {
|
||||
ctx.globalCompositeOperation = 'lighter';
|
||||
const n = 2 + Math.min(6, Math.floor(t * 3));
|
||||
for (let i = 0; i < n; i++) {
|
||||
const from = [i % 2 ? 560 : 80, 372], speed = 0.9 + i * 0.35 + t * 0.9;
|
||||
const a = -Math.PI / 2 + Math.sin(t * speed + i * 1.7) * 0.9, len = 520, half = 0.07;
|
||||
ctx.fillStyle = `rgba(${COLORS[i % COLORS.length]},${0.22 * strength})`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(...from);
|
||||
ctx.lineTo(from[0] + Math.cos(a - half) * len, from[1] + Math.sin(a - half) * len);
|
||||
ctx.lineTo(from[0] + Math.cos(a + half) * len, from[1] + Math.sin(a + half) * len);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
// Le scintille che salgono da terra, a quadratini.
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const life = (t * (0.5 + hash(i)) + hash(i + 50)) % 1, x = 20 + hash(i + 100) * 600, y = 360 - life * 300;
|
||||
ctx.fillStyle = `rgba(${COLORS[i % COLORS.length]},${0.9 * strength * (1 - life)})`;
|
||||
ctx.fillRect(Math.round(x), Math.round(y), 2, 2);
|
||||
}
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
}
|
||||
// Il lampo.
|
||||
const white = whiteness(t);
|
||||
if (white > 0) { ctx.fillStyle = `rgba(255,255,250,${white})`; ctx.fillRect(0, 0, 640, 360); }
|
||||
// Dopo il cambio, i coriandoli che cadono sulla festa.
|
||||
if (!before) {
|
||||
const k = (t - T.swap) / (T.end - T.swap);
|
||||
for (let i = 0; i < 70; i++) {
|
||||
const x = (hash(i) * 640 + Math.sin(t * 3 + i) * 8) % 640, y = -10 + (hash(i + 7) * 120 + k * 420 * (0.6 + hash(i + 3) * 0.6));
|
||||
if (y > 360) continue;
|
||||
ctx.fillStyle = `rgba(${COLORS[i % COLORS.length]},${clamp01(1.3 - k)})`;
|
||||
ctx.fillRect(Math.round(x), Math.round(y), 3, 2);
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -9,22 +9,39 @@ import { versioned } from '../version.js';
|
||||
// Quale brano: quello della scena se ce l'ha; altrimenti, all'aperto, quello della fase del giorno (la sera ha il suo);
|
||||
// altrimenti quello generale del paese.
|
||||
// Un primo piano con la sua musica (i tavoli da carte, col valzer) la porta con sé finché resta aperto.
|
||||
export function trackFor(story, { screen, scene, phase, closeup }) {
|
||||
// Una scena con le console accese (`stages`, ognuna col suo `when`) non ha un brano solo ma una miscela: ogni console
|
||||
// suona il suo, più forte quanto più Andrea (`at`) le è vicino (mix). Le console possono stare anche fuori dal quadro:
|
||||
// è la musica della piazza accanto, che arriva da lontano.
|
||||
// `ok(cond)` dice se una condizione della storia vale adesso (game.js, check).
|
||||
export function trackFor(story, { screen, scene, phase, closeup, at, ok = () => false }) {
|
||||
const m = story.music ?? {};
|
||||
if (screen === 'splash') return null;
|
||||
if (screen === 'title' || screen === 'intro') return m.title ?? null;
|
||||
if (screen === 'ending') return m.ending ?? m.game ?? null;
|
||||
if (closeup && story.closeups?.[closeup]?.music) return story.closeups[closeup].music;
|
||||
const sc = story.scenes[scene];
|
||||
const stages = (sc?.stages ?? []).filter(st => ok(st.when));
|
||||
if (stages.length) return mix(stages, at ?? sc.entry ?? [320, 300]);
|
||||
return sc?.music ?? (sc?.indoor ? null : phase?.music) ?? m.game ?? null;
|
||||
}
|
||||
|
||||
// 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.
|
||||
export function mix(stages, [x, y]) {
|
||||
return stages.map(st => {
|
||||
const d = Math.hypot(x - st.at[0], (y - st.at[1]) * 1.5), near = st.near ?? 90, far = st.far ?? 520;
|
||||
const k = Math.max(0, Math.min(1, (d - near) / (far - near)));
|
||||
return { src: st.music, gain: Math.max(st.floor ?? 0.08, (1 - k) ** 1.6) };
|
||||
}).sort((a, b) => b.gain - a.gain);
|
||||
}
|
||||
|
||||
const FADE = 1.2;
|
||||
|
||||
// Dove il volume non si può cambiare (iOS), almeno il muto e lo zero funzionano: `muted` lì si rispetta.
|
||||
function setLevel(a) {
|
||||
// Dove il volume non si può cambiare (iOS), almeno il muto e lo zero funzionano: `muted` lì si rispetta. Lì, nella
|
||||
// miscela delle console, suona solo la più forte (`alone`): le altre a volume pieno coprirebbero tutto.
|
||||
function setLevel(a, alone = false) {
|
||||
a.volume = a.level;
|
||||
a.muted = a.level === 0;
|
||||
a.muted = a.level === 0 || (alone && a.level < 1 && Math.abs(a.volume - a.level) > 0.01);
|
||||
}
|
||||
|
||||
export class Music {
|
||||
@@ -33,40 +50,46 @@ export class Music {
|
||||
this.volume = volume;
|
||||
this.blocked = null;
|
||||
this.want = null;
|
||||
this.current = null; // { src, audio }
|
||||
this.old = [];
|
||||
this.tracks = new Map(); // src -> { src, audio, gain }: quelli voluti e quelli che sfumano
|
||||
this.wanted = []; // i src voluti adesso, il più forte per primo
|
||||
this.duck = 1; // 1 = pieno; meno di 1 mentre parla qualcuno
|
||||
this.hold = false; // in pausa: il brano sfuma e si ferma, poi riprende da dov'era
|
||||
this.away = false; // pagina nascosta (sul telefono si è passati a un'altra app): tutto fermo subito
|
||||
}
|
||||
|
||||
// Il brano principale (il più forte) e quelli che stanno sfumando.
|
||||
get current() { return this.tracks.get(this.wanted[0]) ?? null; }
|
||||
get old() { return [...this.tracks.values()].filter(t => !this.wanted.includes(t.src)).map(t => t.audio); }
|
||||
|
||||
// Nascosta la pagina il browser non chiama più update (niente fotogrammi): i brani si fermano qui, di colpo.
|
||||
// Tornati, update li fa ripartire da dov'erano (se non c'è la pausa).
|
||||
setAway(away) {
|
||||
this.away = away;
|
||||
if (away) for (const a of [this.current?.audio, ...this.old]) a?.pause();
|
||||
if (away) for (const t of this.tracks.values()) t.audio.pause();
|
||||
}
|
||||
|
||||
// Chiamata a ogni fotogramma con il brano voluto: avvia, cambia e sfuma.
|
||||
update(src, dt) {
|
||||
this.want = src;
|
||||
if (src !== this.current?.src) {
|
||||
if (this.current) this.old.push(this.current.audio);
|
||||
this.current = src ? { src, audio: this.start(src) } : null;
|
||||
// Chiamata a ogni fotogramma con il brano voluto (o la miscela delle console, [{ src, gain }]): avvia, cambia e sfuma.
|
||||
update(want, dt) {
|
||||
this.want = want;
|
||||
const list = want == null ? [] : typeof want === 'string' ? [{ src: want, gain: 1 }] : want;
|
||||
this.wanted = list.map(w => w.src);
|
||||
for (const w of list) {
|
||||
const t = this.tracks.get(w.src) ?? { src: w.src, audio: this.start(w.src) };
|
||||
t.gain = w.gain;
|
||||
this.tracks.set(w.src, t);
|
||||
}
|
||||
// Il brano in corso sale (o scende) verso il volume scelto; quelli vecchi sfumano fino a zero, e lì si fermano.
|
||||
// Ogni brano voluto sale (o scende) verso il suo volume; quelli lasciati sfumano fino a zero, e lì si fermano.
|
||||
// Il livello si tiene in `level`, non si rilegge da audio.volume: su iPhone e iPad quello è di sola lettura e
|
||||
// resta sempre 1, e un brano vecchio non arrivava mai a zero (in chiesa l'organo suonava insieme al paese).
|
||||
const step = dt / FADE, target = this.muted || this.hold ? 0 : this.volume * this.duck;
|
||||
if (this.current) {
|
||||
const a = this.current.audio;
|
||||
const step = dt / FADE, full = this.muted || this.hold ? 0 : this.volume * this.duck;
|
||||
for (const t of [...this.tracks.values()]) {
|
||||
const a = t.audio, on = this.wanted.includes(t.src), target = on ? full * t.gain : 0;
|
||||
a.level = a.level < target ? Math.min(target, a.level + step) : Math.max(target, a.level - step);
|
||||
setLevel(a);
|
||||
setLevel(a, list.length > 1 && t.src !== this.wanted[0]);
|
||||
if (!on) { if (a.level === 0) { a.pause(); this.tracks.delete(t.src); } continue; }
|
||||
if (this.hold && a.level === 0 && !a.paused) a.pause();
|
||||
else if (!this.hold && !this.away && a.paused && a !== this.blocked) a.play().catch(() => { this.blocked = a; });
|
||||
}
|
||||
for (const a of this.old) { a.level = Math.max(0, a.level - step); setLevel(a); }
|
||||
this.old = this.old.filter(a => a.level > 0 || (a.pause(), false));
|
||||
}
|
||||
|
||||
start(src) {
|
||||
@@ -81,7 +104,7 @@ export class Music {
|
||||
unlock() {
|
||||
const a = this.blocked;
|
||||
this.blocked = null;
|
||||
if (a && a === this.current?.audio) a.play().catch(() => {});
|
||||
if (a && [...this.tracks.values()].some(t => t.audio === a && this.wanted.includes(t.src))) a.play().catch(() => {});
|
||||
}
|
||||
|
||||
toggle() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Disegno della scena su un canvas 640×360, ingrandito dal CSS senza sfocatura.
|
||||
// Le forme provvisorie (`art`) verranno sostituite da sfondi e sprite in pixel art.
|
||||
import { check, PAGE_TURN } from './game.js';
|
||||
import { check, PAGE_TURN, POSTER_READY } from './game.js';
|
||||
import { ambientEffects, ambientHash } from './ambient.js';
|
||||
import { drawTorch } from './torch.js';
|
||||
import { drawBocce } from './bocce-render.js';
|
||||
@@ -11,8 +11,12 @@ import { drawPesca } from './pesca-render.js';
|
||||
import { drawGara } from './gara-render.js';
|
||||
import { drawMicro } from './micro-render.js';
|
||||
import { drawCerbottana } from './cerbottana-render.js';
|
||||
import { drawCorsa } from './corsa.js';
|
||||
import { drawCondor } from './condor-render.js';
|
||||
import { drawCampana } from './campana-render.js';
|
||||
import { drawPoker } from './poker-render.js';
|
||||
import { drawViaggio } from './viaggio.js';
|
||||
import { drawIncanto } from './incanto.js';
|
||||
import { drawBriscola } from './briscola-render.js';
|
||||
import { drawTressette } from './tressette-render.js';
|
||||
import { drawScopa } from './scopa-render.js';
|
||||
@@ -128,13 +132,15 @@ export async function loadAssets(story, first = story.start.scene, { together =
|
||||
}
|
||||
|
||||
// Gli sfondi in ordine di distanza dalla scena di partenza, seguendo le uscite; in fondo quelle non raggiungibili.
|
||||
// Prima tutti gli sfondi veri, poi le varianti (`backdrops`, come la notte dell'Ingorgo): quando servono prima, la
|
||||
// scena se le carica da sé.
|
||||
export function backgroundOrder(story, first) {
|
||||
const seen = new Set([first]), queue = [first];
|
||||
for (let i = 0; i < queue.length; i++)
|
||||
for (const h of story.scenes[queue[i]]?.hotspots ?? [])
|
||||
if (h.exit && story.scenes[h.exit.scene] && !seen.has(h.exit.scene)) { seen.add(h.exit.scene); queue.push(h.exit.scene); }
|
||||
for (const id of Object.keys(story.scenes)) if (!seen.has(id)) queue.push(id);
|
||||
return [...new Set(queue.flatMap(id => [story.scenes[id].background, ...(story.scenes[id].backdrops ?? []).map(b => b.src)]).filter(Boolean))];
|
||||
return [...new Set([...queue.map(id => story.scenes[id].background), ...queue.flatMap(id => (story.scenes[id].backdrops ?? []).map(b => b.src))].filter(Boolean))];
|
||||
}
|
||||
|
||||
// Il personaggio alto PERSON pixel a grandezza 1 (per `size` 1), con i piedi in (x, y); rivolto a sinistra si specchia.
|
||||
@@ -481,6 +487,108 @@ export const EFFECTS = {
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
},
|
||||
// Le luci da palco dell'Ingorgo (Andrea, 30 ottobre): dalle teste mobili (`from`) i fasci colorati che spazzano il
|
||||
// cielo; sul palco un bagliore che pulsa a tempo (`bpm`) e cambia colore a ogni battuta; ogni tanto lo strobo.
|
||||
stagelights(ctx, e, time) {
|
||||
const colors = e.colors ?? ['255,60,200', '60,220,255', '120,255,90', '255,190,40'], beat = time / (60000 / (e.bpm ?? 124));
|
||||
const hit = 1 - (beat % 1), bar = Math.floor(beat / 4);
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'lighter';
|
||||
if (e.wash) {
|
||||
const [x, y, r] = e.wash, c = colors[bar % colors.length];
|
||||
const g = ctx.createRadialGradient(x, y, 0, x, y, r);
|
||||
g.addColorStop(0, `rgba(${c},${0.16 + 0.3 * hit ** 2})`);
|
||||
g.addColorStop(1, `rgba(${c},0)`);
|
||||
ctx.fillStyle = g;
|
||||
ctx.fillRect(x - r, y - r, r * 2, r * 2);
|
||||
}
|
||||
for (const [k, [x, y]] of (e.from ?? []).entries()) {
|
||||
for (let i = 0; i < (e.beams ?? 3); i++) {
|
||||
const n = k * 3 + i, swing = Math.sin(time / (700 + n * 150) + n * 1.9) * (e.spread ?? 0.85);
|
||||
const a = -Math.PI / 2 + swing + (k % 2 ? 0.25 : -0.25), len = e.len ?? 340, half = 0.055;
|
||||
const c = colors[(n + bar) % colors.length], tipX = x + Math.cos(a) * len, tipY = y + Math.sin(a) * len;
|
||||
const g = ctx.createLinearGradient(x, y, tipX, tipY);
|
||||
g.addColorStop(0, `rgba(${c},${0.38 + 0.2 * hit})`);
|
||||
g.addColorStop(1, `rgba(${c},0)`);
|
||||
ctx.fillStyle = g;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(x + Math.cos(a - half) * len, y + Math.sin(a - half) * len);
|
||||
ctx.lineTo(x + Math.cos(a + half) * len, y + Math.sin(a + half) * len);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fillStyle = `rgba(${c},0.9)`;
|
||||
ctx.fillRect(Math.round(x) - 1, Math.round(y) - 1, 3, 3);
|
||||
}
|
||||
}
|
||||
// Il colpo di colore su tutta la scena a ogni battuta (`pulse`: quanto forte).
|
||||
if (e.pulse) {
|
||||
ctx.fillStyle = `rgba(${colors[Math.floor(beat) % colors.length]},${e.pulse * hit ** 3})`;
|
||||
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
||||
}
|
||||
ctx.restore();
|
||||
// Lo strobo: un lampo bianco brevissimo ogni `strobe` battute, a raffica nell'ultima battuta di ogni frase.
|
||||
const roll = Math.floor(beat) % 16 === 15 && (beat * 4) % 1 < 0.3;
|
||||
if (e.strobe && ((Math.floor(beat) % e.strobe === 0 && beat % 1 < 0.12) || roll)) {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.18)';
|
||||
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
||||
}
|
||||
},
|
||||
// Le macchie di luce che corrono sulla folla (`area`), colorate, a tempo: la pista da ballo.
|
||||
floorspots(ctx, e, time) {
|
||||
const [ax, ay, aw, ah] = e.area, colors = e.colors ?? ['255,60,200', '60,220,255', '120,255,90', '255,190,40', '170,90,255'];
|
||||
const beat = time / (60000 / (e.bpm ?? 124)), hit = 1 - (beat % 1);
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'lighter';
|
||||
for (let i = 0; i < (e.count ?? 6); i++) {
|
||||
const u = 0.5 + 0.5 * Math.sin(time / (1300 + i * 230) + i * 2.1), v = 0.5 + 0.5 * Math.sin(time / (1700 + i * 190) + i * 1.3);
|
||||
const x = ax + u * aw, y = ay + v * ah, r = (e.size ?? 34) * (0.7 + 0.5 * v);
|
||||
const c = colors[(i + Math.floor(beat / 2)) % colors.length];
|
||||
const g = ctx.createRadialGradient(x, y, 0, x, y, r);
|
||||
g.addColorStop(0, `rgba(${c},${0.22 + 0.2 * hit})`);
|
||||
g.addColorStop(1, `rgba(${c},0)`);
|
||||
ctx.fillStyle = g;
|
||||
ctx.save();
|
||||
ctx.translate(x, y); ctx.scale(1, 0.45); ctx.translate(-x, -y);
|
||||
ctx.fillRect(x - r, y - r, r * 2, r * 2);
|
||||
ctx.restore();
|
||||
}
|
||||
ctx.restore();
|
||||
},
|
||||
// I laser della tecno: righe sottili verdi (o del colore dato) che si aprono a ventaglio e si richiudono.
|
||||
lasers(ctx, e, time) {
|
||||
const [x, y] = e.at, n = e.count ?? 7, open = 0.35 + 0.35 * Math.sin(time / 1300), tilt = Math.sin(time / 2100) * 0.4;
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'lighter';
|
||||
ctx.strokeStyle = `rgba(${e.color ?? '90,255,110'},${0.45 + 0.25 * Math.sin(time / 90) ** 2})`;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < n; i++) {
|
||||
const a = -Math.PI / 2 + tilt + (i / (n - 1) - 0.5) * 2 * open * 1.4;
|
||||
ctx.moveTo(x + 0.5, y + 0.5);
|
||||
ctx.lineTo(x + Math.cos(a) * (e.len ?? 400), y + Math.sin(a) * (e.len ?? 400));
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
},
|
||||
// La palla a specchi: puntini di luce che girano sui muri intorno (`area`), più chiari vicino alla palla.
|
||||
mirrorball(ctx, e, time) {
|
||||
const [bx, by] = e.at, [ax, ay, aw, ah] = e.area, turn = time / 4200;
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'lighter';
|
||||
for (let i = 0; i < (e.count ?? 40); i++) {
|
||||
const u = (hash(i * 13) + turn) % 1, x = ax + Math.round(u * aw), y = ay + Math.round(hash(i * 7 + 3) * ah);
|
||||
const glint = 0.5 + 0.5 * Math.sin(time / 160 + i * 2.3);
|
||||
ctx.fillStyle = `rgba(${e.color ?? '220,200,255'},${0.35 + 0.5 * glint})`;
|
||||
ctx.fillRect(x, y, 2, 2);
|
||||
}
|
||||
const g = ctx.createRadialGradient(bx, by, 0, bx, by, 9);
|
||||
g.addColorStop(0, 'rgba(255,255,255,0.8)');
|
||||
g.addColorStop(1, 'rgba(200,180,255,0)');
|
||||
ctx.fillStyle = g;
|
||||
ctx.fillRect(bx - 9, by - 9, 18, 18);
|
||||
ctx.restore();
|
||||
},
|
||||
// Lampioni: un alone caldo che si accende (di solito solo di sera, con `phase`).
|
||||
lamps(ctx, e, time) {
|
||||
ctx.save();
|
||||
@@ -685,6 +793,39 @@ function drawCloseup(ctx, game, time) {
|
||||
drawViaggio(ctx, c, def, backgrounds.get(def.picture));
|
||||
return;
|
||||
}
|
||||
// La macchina del tempo da fuori: lo sfondo di adesso della scena, con la luce della fase del giorno.
|
||||
if (def.type === 'corsa') {
|
||||
const bg = game.backdrop(game.story.scenes[def.scene]), frames = {};
|
||||
for (const [k, src] of [[bg, bg], ...Object.entries(def.frames)]) {
|
||||
if (!backgrounds.has(src)) loadBackground(src);
|
||||
if (k !== bg) frames[k] = backgrounds.get(src);
|
||||
}
|
||||
drawCorsa(ctx, c, def, { bg: backgrounds.get(bg), frames, tint: game.phase()?.tint });
|
||||
return;
|
||||
}
|
||||
// L'incantesimo va sopra la scena (con Andrea dentro), già disegnata.
|
||||
if (def.type === 'incanto') { drawIncanto(ctx, c); return; }
|
||||
// Il manifesto ingrandito: la scena si scurisce e lui viene avanti, poi l'invito a toccarlo.
|
||||
if (def.type === 'poster') {
|
||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||
const img = backgrounds.get(def.picture), k = Math.min(1, c.t / POSTER_READY), ease = 1 - (1 - k) ** 3;
|
||||
ctx.fillStyle = `rgba(8, 4, 16, ${0.75 * ease})`;
|
||||
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
||||
if (img) {
|
||||
const fit = Math.min(600 / img.width, 300 / img.height) * (0.55 + 0.45 * ease), w = img.width * fit, h = img.height * fit;
|
||||
ctx.globalAlpha = ease;
|
||||
ctx.drawImage(img, Math.round((WIDTH - w) / 2), Math.round((HEIGHT - 26 - h) / 2), Math.round(w), Math.round(h));
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
if (k >= 1 && Math.floor(time / 600) % 2 === 0) {
|
||||
ctx.font = '8px "Press Start 2P", monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = '#ffe9b0';
|
||||
ctx.fillText(def.hint ?? 'Tocca il manifesto', WIDTH / 2, HEIGHT - 12);
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (def.type === 'torch') {
|
||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||
drawTorch(ctx, c, def, backgrounds.get(def.picture));
|
||||
@@ -701,6 +842,16 @@ function drawCloseup(ctx, game, time) {
|
||||
drawCerbottana(ctx, c, def, backgrounds.get(def.picture));
|
||||
return;
|
||||
}
|
||||
if (def.type === 'condor') {
|
||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||
drawCondor(ctx, c, def, backgrounds.get(def.picture));
|
||||
return;
|
||||
}
|
||||
if (def.type === 'campana') {
|
||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||
drawCampana(ctx, c, def, backgrounds.get(def.picture));
|
||||
return;
|
||||
}
|
||||
if (def.type === 'micro') {
|
||||
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); });
|
||||
|
||||
@@ -140,6 +140,22 @@ export class Soundscape {
|
||||
else if (kind === 'cerbottana-pff') this.noiseSound(0.12, 2200, 0.05 + strength * 0.05, { pan });
|
||||
else if (kind === 'cerbottana-muro') { this.tone(180, 90, 0.06, 0.05, 0, pan); this.noiseSound(0.05, 600, 0.04, { pan }); }
|
||||
else if (kind === 'cerbottana-latta') { this.tone(2400, 1600, 0.08, 0.05 + strength * 0.04, 0, pan); this.tone(3400, 2600, 0.05, 0.03, 0.03, pan); }
|
||||
// La Condor: lo scatto della molla, il palloncino che scoppia, la bottiglietta, il Taglia che tira la lattina.
|
||||
else if (kind === 'condor-pam') { this.noiseSound(0.06, 3200, 0.07, { pan }); this.tone(520, 220, 0.05, 0.05, 0, pan); }
|
||||
else if (kind === 'condor-palloncino') { this.noiseSound(0.12, 1800, 0.14, { pan }); this.tone(140, 60, 0.08, 0.08, 0, pan); }
|
||||
else if (kind === 'condor-plastica') { this.tone(1300, 900, 0.06, 0.05, 0, pan); this.noiseSound(0.05, 2600, 0.03, { pan }); }
|
||||
else if (kind === 'condor-lancio') this.noiseSound(0.2, 700, 0.03, { pan });
|
||||
// La macchina del tempo: il tuono del fulmine e lo sfrigolio delle sgommate di fuoco.
|
||||
else if (kind === 'corsa-tuono') { this.noiseSound(0.9, 400, 0.16, { pan }); this.tone(90, 40, 0.8, 0.1, 0, pan); }
|
||||
else if (kind === 'corsa-fuoco') this.noiseSound(1.6, 2400, 0.05, { pan });
|
||||
// L'incantesimo dell'Ingorgo: i colpi di cassa che accelerano, poi il lampo con lo spazzolone del piatto.
|
||||
else if (kind === 'incanto-cassa') this.tone(150, 45, 0.22, 0.12 + strength * 0.1, 0, pan);
|
||||
else if (kind === 'incanto-lampo') { this.noiseSound(1.4, 5200, 0.12, { pan }); this.tone(60, 35, 1.2, 0.16, 0, pan); this.tone(880, 1760, 0.6, 0.05, 0.05, pan); }
|
||||
// La campana: il sassolino che vola e batte sull'asfalto, il salto, il piede giù.
|
||||
else if (kind === 'campana-lancio') this.noiseSound(0.1, 1500, 0.025, { 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-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 === '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.
|
||||
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 }); }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Avvio nel browser: collega motore, disegno e interfaccia HTML (verbi, inventario, dialoghi, fumetti).
|
||||
import { story } from './story/story.js';
|
||||
import { Game, VERBS, VERB_LABELS, checkSave, introText, shuffledCredits, matchPlace, reelPictures, facePicture } from './engine/game.js';
|
||||
import { Game, check, VERBS, VERB_LABELS, checkSave, introText, shuffledCredits, matchPlace, reelPictures, facePicture } from './engine/game.js';
|
||||
import { Command } from './engine/command.js';
|
||||
import { render, loadAssets, hasBackground, WIDTH, HEIGHT } from './engine/render.js';
|
||||
import { readSave, writeSave, clearSave, readAudio, writeAudio, saveFile, saveFileName, readSaveFile } from './engine/storage.js';
|
||||
@@ -13,6 +13,8 @@ import * as biliardo from './engine/biliardo.js';
|
||||
import * as rinquarto from './engine/rinquarto.js';
|
||||
import * as micro from './engine/micro.js';
|
||||
import * as cerbottana from './engine/cerbottana.js';
|
||||
import * as condor from './engine/condor.js';
|
||||
import * as campana from './engine/campana.js';
|
||||
import * as poker from './engine/poker.js';
|
||||
import { cardAt as pokerAt } from './engine/poker-render.js';
|
||||
import * as basket from './engine/basket.js';
|
||||
@@ -514,7 +516,7 @@ canvas.addEventListener('pointerdown', e => {
|
||||
}
|
||||
if (press?.closeup) return;
|
||||
const aimed = game.closeupAim(scenePoint(e));
|
||||
if (['biliardo', 'rinquarto', 'basket', 'micro', 'cerbottana'].includes(story.closeups[game.closeup.id].type)) {
|
||||
if (['biliardo', 'rinquarto', 'basket', 'micro', 'cerbottana', 'condor'].includes(story.closeups[game.closeup.id].type)) {
|
||||
if (!aimed && !game.speech) return; // dove non si può mirare (dietro Andrea) non si carica nemmeno
|
||||
if (aimed) game.closeup.aimLocked = true; // il clic fissa la mira
|
||||
}
|
||||
@@ -586,7 +588,7 @@ canvas.addEventListener('pointermove', e => {
|
||||
if (press?.gara === e.pointerId) gara.aimScreen(game.closeup, scenePoint(e), e.pointerId);
|
||||
return;
|
||||
}
|
||||
const aiming = game.closeup && ['biliardo', 'rinquarto', 'bocce', 'basket', 'pesca', 'micro', 'cerbottana'].includes(story.closeups[game.closeup.id].type);
|
||||
const aiming = game.closeup && ['biliardo', 'rinquarto', 'bocce', 'basket', 'pesca', 'micro', 'cerbottana', 'condor', 'campana'].includes(story.closeups[game.closeup.id].type);
|
||||
if (aiming) { if (e.pointerType === 'mouse' && !press && !game.closeup.aimLocked) game.closeupAim(scenePoint(e)); }
|
||||
else if (e.pointerType === 'mouse' || press?.closeup) game.closeupAim(scenePoint(e));
|
||||
});
|
||||
@@ -609,6 +611,8 @@ const MINI = {
|
||||
rinquarto: { ...rinquarto, back: 'Torna da Trappizzio Berti', label: 'I rinquarti di Trappizzio Berti: clic o tocco fissa la mira, frecce o pulsanti Mira per girare la stecca (Maiusc per i passi fini), Tira per tirare. Dopo l’esito, Prossima.' },
|
||||
poker: { ...poker, back: 'Torna al Barrettino', label: 'Il pokerino: tocca le carte da cambiare, poi Cambia; nei giri di puntate Lascio, Vedo o Parola, Apro o Rilancio. Dopo la mostra un tocco va avanti.' },
|
||||
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.' },
|
||||
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.' },
|
||||
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.' },
|
||||
// La gara: niente mira né forza, il pulsante alterna gas e freno (chi non può tenere premuto).
|
||||
@@ -1038,7 +1042,7 @@ function updateUi() {
|
||||
}
|
||||
}
|
||||
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' }[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' }[def.type] ?? 'Comandi delle bocce');
|
||||
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
|
||||
const line = mg.status(c, def.names);
|
||||
if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line;
|
||||
@@ -1052,8 +1056,8 @@ function updateUi() {
|
||||
// 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.
|
||||
const easy = def.type === 'gara' && gara.simple(c);
|
||||
setProp(powerLabel, 'hidden', def.type === 'rinquarto' || easy);
|
||||
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy);
|
||||
setProp(powerLabel, 'hidden', def.type === 'rinquarto' || def.type === 'condor' || easy);
|
||||
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy || def.type === 'campana');
|
||||
const powerName = def.type === 'gara' ? 'Sensibilità ' : def.type === 'cerbottana' ? 'Fiato ' : 'Forza ';
|
||||
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');
|
||||
@@ -1073,6 +1077,8 @@ function updateUi() {
|
||||
setProp($('rinquarto-help'), 'hidden', def.type !== 'rinquarto');
|
||||
setProp($('micro-help'), 'hidden', def.type !== 'micro');
|
||||
setProp($('cerbottana-help'), 'hidden', def.type !== 'cerbottana');
|
||||
setProp($('condor-help'), 'hidden', def.type !== 'condor');
|
||||
setProp($('campana-help'), 'hidden', def.type !== 'campana');
|
||||
setProp($('basket-help'), 'hidden', def.type !== 'basket');
|
||||
setProp($('pesca-help'), 'hidden', def.type !== 'pesca');
|
||||
setProp($('gara-help'), 'hidden', def.type !== 'gara');
|
||||
@@ -1093,7 +1099,7 @@ function updateUi() {
|
||||
canvas.classList.toggle('torch', !!torch);
|
||||
canvas.classList.toggle('bocce', !!mg);
|
||||
// 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'].includes(story.closeups[c.id].type));
|
||||
$('stage').classList.toggle('biliardo', !!c && ['biliardo', 'rinquarto', 'gara', 'micro', 'cerbottana', 'condor', 'campana'].includes(story.closeups[c.id].type));
|
||||
$('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");
|
||||
// La pagina che comincia a girare fa il suo fruscio.
|
||||
@@ -1121,7 +1127,8 @@ function frame(now) {
|
||||
// Mentre si parla la musica si abbassa; quando compaiono gli occhi del Regolo quasi si spegne, per la sorpresa.
|
||||
const searching = game?.closeup?.id === 'fessura';
|
||||
music.duck = searching ? game.closeup.found >= 2 ? 0.05 : 0.25 : game?.state.flags.occhi_regolo ? 0.12 : voices.playing ? 0.45 : 1;
|
||||
music.update(trackFor(story, { screen, scene: game?.state.scene, phase: game?.phase(), closeup: game?.closeup?.id }), dt);
|
||||
music.update(trackFor(story, { screen, scene: game?.state.scene, phase: game?.phase(), closeup: game?.closeup?.id,
|
||||
at: game && [game.state.x, game.state.y], ok: cond => !!game && check(cond, game.state) }), dt);
|
||||
// Un minuto senza toccare niente, a gioco fermo: pausa da sola.
|
||||
if (running && now - idleSince > IDLE_PAUSE && !game.speech && !game.walking && !game.queue.length) openPause();
|
||||
ambience.configure({ active: screen === 'game' && running && !document.hidden && $('credits').hidden, muted: music.muted, duck: voices.playing ? 0.4 : 1 });
|
||||
|
||||
@@ -241,7 +241,7 @@ export const ambience = {
|
||||
{ type: 'insect', kind: 'dragonfly', phase: day, area: [440, 228, 110, 30], every: 31000, duration: 8500, offset: 15000 },
|
||||
],
|
||||
orti_zio_mario: [
|
||||
{ type: 'clouds', area: [80, 20, 500, 90], count: 3, big: true },
|
||||
{ type: 'clouds', area: [60, 20, 330, 70], count: 3, big: true }, // solo fino all'abete: dentro le fronde che ondeggiano le nuvole si tagliano
|
||||
{ type: 'birds', phase: day, area: [300, 30, 250, 60], count: 3, every: 24000, duration: 5000, offset: 2000 },
|
||||
{ type: 'sway', rects: [[180, 10, 140, 180], [430, 10, 210, 240]], amount: 2 },
|
||||
butterfly([10, 240, 140, 50], 8000),
|
||||
|
||||
@@ -43,11 +43,19 @@ const torchHints = [{
|
||||
}],
|
||||
}];
|
||||
|
||||
// La macchina del tempo che corre, vista da dietro e (all'arrivo) davanti: larga `width` pixel da vicino, le ruote a
|
||||
// `track` della larghezza dal centro.
|
||||
const MACCHINA_CORSA = {
|
||||
frames: { dietro: 'assets/closeups/macchina-dietro.png', lampi: 'assets/closeups/macchina-lampi.png', davanti: 'assets/closeups/macchina-davanti.png' },
|
||||
width: 220, track: 75,
|
||||
};
|
||||
|
||||
// Il ritorno dal 1997, dalla porta o dal pulsante «Torna al 2007»: la macchina del tempo sparisce, e per rifare il
|
||||
// viaggio si richiede all'Arcu (Andrea, 29 ottobre).
|
||||
const RITORNO_2007 = [
|
||||
{ say: 'Si torna nel 2007. Ciao Vannino, ciao Pierino.' },
|
||||
{ go: 'via_liguri_magelli', at: [212, 280], face: 'down' },
|
||||
{ closeup: 'arrivo' },
|
||||
{ unset: 'macchina_trappola' },
|
||||
{ say: 'Riportata dove l\'ho presa. Se ti riviene voglia di pokerini, chiedi.', who: 'arcu', pose: 'explain' },
|
||||
];
|
||||
@@ -64,6 +72,22 @@ export const story = {
|
||||
{ id: 'pomeriggio' },
|
||||
{ id: 'tramonto', when: 'indizio_regolo', tint: 'rgba(255, 110, 40, 0.16)' },
|
||||
{ id: 'sera', when: 'casetta_aperta', tint: 'rgba(24, 28, 90, 0.42)', music: 'assets/audio/sera.mp3' },
|
||||
// La notte dell'Ingorgo Sonoro (Andrea, 30 ottobre): gli sfondi sono già di notte, il velo serve solo ad Andrea.
|
||||
// La musica la fanno le console delle scene (`stages`).
|
||||
{ id: 'ingorgo', when: 'ingorgo', tint: 'rgba(40, 20, 90, 0.16)', scale: 1.6 },
|
||||
],
|
||||
// Le zone: un flag che vale solo in certi posti. L'Ingorgo Sonoro (Andrea, 30 ottobre: «era la festa del paese
|
||||
// fatta una delle prime sere di luglio, da piazza fino a Cambray Digny, compresa piazza del Comune») si accende
|
||||
// col manifesto in piazza e si spegne uscendo dal centro. [DA ANDREA: le battute sono provvisorie]
|
||||
zones: [
|
||||
{
|
||||
flag: 'ingorgo', scenes: ['piazza', 'via_antica_posta', 'piazzetta_comune', 'antica_posta_alta', 'piazzetta'],
|
||||
exitsOnly: true, // alla festa si clicca solo per spostarsi (Andrea)
|
||||
leave: [
|
||||
{ say: 'Mi stropiccio gli occhi. La musica non c\'è più: è di nuovo maggio.' },
|
||||
{ say: 'E il palio è sempre sparito. Però che serata.' },
|
||||
],
|
||||
},
|
||||
],
|
||||
// Il taccuino (pulsante «Taccuino», tasto N): le tappe del capitolo in ordine. Una tappa è fatta quando la sua
|
||||
// condizione `done` è vera; la prima non fatta è l'obiettivo di adesso. Solo testo: non si sente a voce.
|
||||
@@ -249,13 +273,25 @@ export const story = {
|
||||
picture: 'assets/closeups/cruscotto-tempo.png',
|
||||
display: [220, 158, 212, 62],
|
||||
date: '10/07/1997 18:30',
|
||||
complete: [{ closeup: 'partenza' }],
|
||||
},
|
||||
// La partenza vista da fuori: giù per Via Liguri Magelli, il fulmine e le sgommate di fuoco; e l'arrivo, al
|
||||
// ritorno dal 1997 (Andrea, 30 ottobre; la macchina da dietro e davanti è di Codex).
|
||||
partenza: {
|
||||
type: 'corsa', mode: 'parti', scene: 'via_liguri_magelli', ...MACCHINA_CORSA,
|
||||
path: [[215, 350, 1], [252, 192, 0.05]],
|
||||
complete: [{ go: 'barrettino_1997', at: [320, 330], face: 'up' }],
|
||||
},
|
||||
arrivo: {
|
||||
type: 'corsa', mode: 'arriva', scene: 'via_liguri_magelli', ...MACCHINA_CORSA,
|
||||
path: [[110, 470, 2.2], [252, 192, 0.05]],
|
||||
complete: [],
|
||||
},
|
||||
// Il pokerino del Barrettino con Vannino e Pierino (Andrea, 29 ottobre). [DA ANDREA: le battute]
|
||||
poker: {
|
||||
type: 'poker',
|
||||
names: { player: 'Andrea', cpu: 'Vannino e Pierino' },
|
||||
win: [{ say: 'M\'hai pelato, ragazzo. Nel 1997 un ci si vedeva da queste parti, con quella faccia.', who: 'vannino', pose: 'shrug' }, { set: 'poker_vinto' }],
|
||||
win: [{ say: 'Tu m\'ha pelato. La prossima volta porta il Barone.', who: 'vannino', pose: 'shrug' }, { set: 'poker_vinto' }],
|
||||
lose: [{ say: 'Le fiches restano al banco. E tu torna quando hai imparato a bluffare.', who: 'pierino_parigi', pose: 'laugh' }],
|
||||
quit: [{ say: 'Già finito? E il caffè chi lo paga?', who: 'vannino', pose: 'talk' }],
|
||||
draw: [{ say: 'Pari. Si rifà domani sera, alla stess\'ora.', who: 'pierino_parigi', pose: 'explain' }],
|
||||
@@ -271,6 +307,48 @@ export const story = {
|
||||
quit: [{ say: 'Il muro resta lì. E l\'odore dello stucco, sulle dita, pure.' }],
|
||||
draw: [],
|
||||
},
|
||||
// La Condor coi pallini gialli al campone delle poste, in gara col Taglia (Andrea, 29 ottobre). [DA ANDREA: le battute]
|
||||
condor: {
|
||||
type: 'condor',
|
||||
picture: 'assets/closeups/campone-condor.png',
|
||||
names: { player: 'Andrea', cpu: 'il Taglia' },
|
||||
win: [{ say: 'Battuto il Taglia! Con la sua Condor, per giunta.' }, { say: 'Fortuna. Domani la rivinco.', who: 'taglia', pose: 'shrug' }, { set: 'condor_vinta' }],
|
||||
lose: [{ say: 'Il Taglia ha la mano ferma. Io ho il fiatone.' }, { say: 'Si torna quando vuoi. I pallini gialli un mancano mai.', who: 'taglia', pose: 'laugh' }],
|
||||
draw: [{ say: 'Pari. Si rifà?' }, { say: 'Si rifà.', who: 'taglia', pose: 'explain' }],
|
||||
quit: [{ say: 'La Condor la rendo al Taglia. Per ora.' }],
|
||||
},
|
||||
// La campana col gesso sul vialetto del campone delle poste, a turni col Taglia (Andrea, 30 ottobre).
|
||||
// [DA ANDREA: le battute sono provvisorie]
|
||||
campana: {
|
||||
type: 'campana',
|
||||
picture: 'assets/closeups/campone-campana.png',
|
||||
names: { player: 'Andrea', cpu: 'il Taglia' },
|
||||
win: [{ say: 'Dalla 1 fino in cima, e ritorno. A ventisett\'anni, davanti alle poste.' }, { say: 'Te lo dico io: se ti vede qualcuno, domani lo sa tutto il paese.', who: 'taglia', pose: 'laugh' }, { set: 'campana_vinta' }],
|
||||
lose: [{ say: 'Il Taglia, su un piede solo, è più alto di me su due.' }, { say: 'È questione di gambe lunghe.', who: 'taglia', pose: 'explain' }],
|
||||
draw: [],
|
||||
quit: [{ say: 'Il gesso lo lascio lì. Ci giocherà qualche bambino, che è più in allenamento di me.' }],
|
||||
},
|
||||
// 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]
|
||||
manifesto_ingorgo: {
|
||||
type: 'poster',
|
||||
picture: 'assets/closeups/manifesto-ingorgo.png',
|
||||
hint: 'Tocca il manifesto',
|
||||
tap: [
|
||||
{ say: 'Il manifesto dell\'Ingorgo Sonoro dell\'anno scorso. Da piazza fino a Cambray Digny, una console in ogni angolo.' },
|
||||
{ say: 'Quanto darei per essere all\'Ingorgo stasera?!?' },
|
||||
{ closeup: 'ingorgo' },
|
||||
],
|
||||
},
|
||||
// L'incantesimo dell'Ingorgo Sonoro, dal manifesto in piazza: luci, cassa, lampo, ed è la notte della festa.
|
||||
ingorgo: {
|
||||
type: 'incanto',
|
||||
swap: [{ set: 'ingorgo' }],
|
||||
complete: [
|
||||
{ say: 'Ma... è l\'Ingorgo! Le console, la gente, le spine della birra!' },
|
||||
{ say: 'Il palio può aspettare una sera. O no?' },
|
||||
],
|
||||
},
|
||||
// Le macchinine sulla pista col gesso, sul parcheggio di Cardetole, contro il Chiaru (Andrea, 28 e 29 ottobre).
|
||||
// [DA ANDREA: le battute sono provvisorie]
|
||||
micro: {
|
||||
@@ -358,7 +436,11 @@ export const story = {
|
||||
// Lo zio Mario di Andrea (sprite di Codex dalla foto di Andrea, 29 ottobre), alla Ghiacciomobile nel muro.
|
||||
zio_mario: { name: 'Lo zio Mario', color: '#cfe3ff', sprite: 'assets/characters/zio_mario.json' },
|
||||
chiaru: { name: 'Il Chiaru', color: '#ffd6a0', sprite: 'assets/characters/chiaru.json' },
|
||||
// Andrea Tagliaferri, il Taglia (consenso suo, 29 ottobre): sprite di Codex dalla sua foto.
|
||||
taglia: { name: 'Il Taglia', color: '#ffe36a', sprite: 'assets/characters/taglia.json' },
|
||||
barondeduke: { name: 'Il BaronDeDuke', color: '#e0c090', sprite: 'assets/characters/barondeduke.json' },
|
||||
// Baldoria (Yuri Baldi, nei Ringraziamenti), tifoso della Sampdoria col bandierone (Andrea, 30 ottobre; sprite di Codex).
|
||||
baldoria: { name: 'Baldoria', color: '#7fb2ff', sprite: 'assets/characters/baldoria.json' },
|
||||
mariano: { name: 'Mariano', color: '#ffb0a3', sprite: 'assets/characters/mariano.json' },
|
||||
// Non parla: è una cosa, disegnata come un personaggio per stare davanti o dietro ad Andrea.
|
||||
fra_peppino: { name: 'Fra Peppino', color: '#e8c89a', sprite: 'assets/characters/fra-peppino.json' },
|
||||
@@ -500,15 +582,22 @@ export const story = {
|
||||
name: 'Piazza Colonna',
|
||||
map: [32, 45],
|
||||
background: 'assets/scenes/piazza-colonna.png',
|
||||
effects: [{ type: 'clouds', area: [160, 0, 440, 80], count: 3, big: true }, { type: 'windows', phase: 'sera', rects: [[16, 37, 8, 18], [48, 42, 8, 18], [50, 110, 8, 16], [97, 77, 8, 12], [122, 80, 8, 12], [170, 90, 8, 12], [204, 103, 8, 12], [239, 111, 8, 12], [268, 117, 7, 14], [287, 125, 6, 14], [97, 130, 9, 14], [127, 133, 9, 14], [170, 142, 9, 14], [204, 150, 8, 12], [239, 155, 8, 12], [268, 160, 7, 14], [287, 163, 6, 14], [386, 163, 14, 16], [424, 160, 14, 16], [472, 157, 14, 16], [524, 155, 14, 16], [390, 212, 14, 16], [526, 208, 14, 16]] }, { type: 'birds', phase: ['pomeriggio', 'tramonto'], area: [150, 20, 460, 80] }],
|
||||
backdrops: [{ when: 'ingorgo', src: 'assets/scenes/ingorgo-piazza.png' }], // Codex, la notte dell'Ingorgo (30 ottobre)
|
||||
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza]
|
||||
// La console grande davanti al palazzo; da Via dell'Antica Posta, a sinistra, arriva il latino del Comune.
|
||||
stages: [
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [195, 235] },
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [-160, 250], far: 600 },
|
||||
],
|
||||
effects: [{ type: 'stagelights', phase: 'ingorgo', from: [[116, 146], [276, 146], [196, 140]], wash: [200, 215, 170], bpm: 124, beams: 3, strobe: 4, pulse: 0.08 }, { type: 'lasers', phase: 'ingorgo', at: [196, 150], count: 7, color: '255,60,200', len: 360 }, { type: 'floorspots', phase: 'ingorgo', area: [40, 240, 560, 110], bpm: 124, count: 7 }, { type: 'clouds', area: [160, 0, 440, 80], count: 3, big: true, phase: ['pomeriggio', 'tramonto', 'sera'] }, { type: 'windows', phase: 'sera', rects: [[16, 37, 8, 18], [48, 42, 8, 18], [50, 110, 8, 16], [97, 77, 8, 12], [122, 80, 8, 12], [170, 90, 8, 12], [204, 103, 8, 12], [239, 111, 8, 12], [268, 117, 7, 14], [287, 125, 6, 14], [97, 130, 9, 14], [127, 133, 9, 14], [170, 142, 9, 14], [204, 150, 8, 12], [239, 155, 8, 12], [268, 160, 7, 14], [287, 163, 6, 14], [386, 163, 14, 16], [424, 160, 14, 16], [472, 157, 14, 16], [524, 155, 14, 16], [390, 212, 14, 16], [526, 208, 14, 16]] }, { type: 'birds', phase: ['pomeriggio', 'tramonto'], area: [150, 20, 460, 80] }],
|
||||
walk: [[84, 244], [330, 238], [345, 230], [362, 238], [462, 246], [466, 276], [544, 276], [548, 248], [588, 250], [600, 290], [640, 300], [640, 360], [0, 360], [0, 330], [84, 318]],
|
||||
depth: [[236, 0.5], [360, 1.35]],
|
||||
entry: [345, 240],
|
||||
speakers: { vecchietta: [24, 250], vecchietto: [56, 244] },
|
||||
// I vecchietti seduti sulla panchina: fotogrammi in assets/characters (pose e battute in game.personPose).
|
||||
people: [
|
||||
{ who: 'vecchietta', at: [24, 317], size: 0.92, talk: ['talk', 'explain'], blink: 'blink', every: 3.7 },
|
||||
{ who: 'vecchietto', at: [56, 304], size: 0.88, talk: ['talk', 'explain'], blink: 'doze', every: 5.3, offset: 2 },
|
||||
{ who: 'vecchietta', at: [24, 317], when: '!ingorgo', size: 0.92, talk: ['talk', 'explain'], blink: 'blink', every: 3.7 },
|
||||
{ who: 'vecchietto', at: [56, 304], when: '!ingorgo', size: 0.88, talk: ['talk', 'explain'], blink: 'doze', every: 5.3, offset: 2 },
|
||||
],
|
||||
hotspots: [
|
||||
{
|
||||
@@ -571,7 +660,7 @@ export const story = {
|
||||
exit: { scene: 'circolo_4_maggio', at: [590, 202], face: 'left' },
|
||||
},
|
||||
{
|
||||
id: 'manifesto', name: 'manifesto del Palio', box: [256, 196, 18, 24], at: [265, 250], face: 'up',
|
||||
id: 'manifesto', name: 'manifesto del Palio', box: [256, 196, 18, 24], at: [265, 250], face: 'up', when: '!ingorgo',
|
||||
art: [{ rect: [256, 196, 18, 24], fill: '#f4efe4' }, { rect: [258, 199, 14, 5], fill: '#b7342e' }, { rect: [258, 207, 12, 2], fill: '#3b3b44' }, { rect: [258, 211, 10, 2], fill: '#3b3b44' }, { rect: [258, 215, 12, 2], fill: '#2f5fb3' }],
|
||||
actions: {
|
||||
look: [
|
||||
@@ -582,7 +671,20 @@ export const story = {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'vecchietti', name: 'vecchietti', box: [0, 250, 72, 52], at: [100, 320], face: 'left', default: 'talk',
|
||||
// Il manifesto dell'Ingorgo Sonoro dell'estate prima, mezzo strappato: guardandolo, Andrea esprime il
|
||||
// desiderio, e la festa arriva (Andrea, 30 ottobre). [DA ANDREA: le battute sono provvisorie]
|
||||
id: 'manifesto_ingorgo', name: 'vecchio manifesto dell\'Ingorgo Sonoro', box: [223, 195, 22, 26], at: [234, 250], face: 'up', when: '!ingorgo',
|
||||
// Come quello vero (assets/raw/riferimenti/manifesto-ingorgo-2014.png): blu, il cartello bianco e rosso col DJ.
|
||||
art: [{ rect: [226, 198, 16, 22], fill: '#1c2050' }, { rect: [233, 201, 2, 2], fill: '#d8262e' }, { rect: [231, 203, 6, 2], fill: '#d8262e' },
|
||||
{ rect: [229, 205, 10, 4], fill: '#d8262e' }, { rect: [232, 205, 4, 3], fill: '#f4f1e8' }, { rect: [228, 211, 12, 2], fill: '#f4f1e8' },
|
||||
{ rect: [229, 215, 7, 2], fill: '#ffd23a' }, { rect: [238, 198, 4, 4], fill: '#cdbb9a' }],
|
||||
actions: {
|
||||
look: [{ closeup: 'manifesto_ingorgo' }],
|
||||
take: [{ say: 'Lo lascio dov\'è: è l\'unica cosa che ricorda luglio, qui, a maggio.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'vecchietti', name: 'vecchietti', box: [0, 250, 72, 52], at: [100, 320], face: 'left', default: 'talk', when: '!ingorgo',
|
||||
actions: {
|
||||
look: [{ say: 'I vecchietti della panchina. Sanno tutto di tutti, soprattutto quello che non è successo.' }],
|
||||
talk: [
|
||||
@@ -1327,6 +1429,9 @@ export const story = {
|
||||
walk: [[0, 360], [0, 232], [640, 232], [640, 360]],
|
||||
depth: [[232, 0.55], [360, 1.15]],
|
||||
entry: [580, 340],
|
||||
speakers: { taglia: [470, 214] },
|
||||
// Il Taglia con la sua Condor, più alto di Andrea (che è un metro e ottanta).
|
||||
people: [{ who: 'taglia', at: [470, 296], facing: -1, size: 1.2, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 3.9 }],
|
||||
hotspots: [
|
||||
{
|
||||
id: 'giochi', name: 'giochi', box: [196, 120, 330, 100], at: [360, 250], face: 'up',
|
||||
@@ -1357,6 +1462,13 @@ export const story = {
|
||||
{
|
||||
id: 'alla_strada', name: 'indietro, sulla strada della stazione', box: [100, 336, 440, 24], at: [300, 352], exit: { scene: 'viale_stazione', at: [620, 280], face: 'left' },
|
||||
},
|
||||
{
|
||||
id: 'taglia', name: 'il Taglia', box: [448, 224, 44, 72], at: [430, 306], face: 'right', default: 'talk',
|
||||
actions: {
|
||||
look: [{ say: 'Il Taglia. Io sono un metro e ottanta, e lui mi guarda dall\'alto. Con la Condor in mano.' }],
|
||||
talk: [{ dialogue: 'taglia' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// Via Calimara: dalla Piazza scende alla farmacia, in Via Provinciale.
|
||||
@@ -1510,6 +1622,15 @@ export const story = {
|
||||
name: 'Piazzetta Cambray Digny',
|
||||
map: [40, 38],
|
||||
background: 'assets/scenes/piazzetta-cambray-digny.png',
|
||||
backdrops: [{ when: 'ingorgo', src: 'assets/scenes/ingorgo-piazzetta.png' }], // Codex, la notte dell'Ingorgo (30 ottobre)
|
||||
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza]
|
||||
// La tecno davanti alla casetta bianca; l'underground arriva da giù, da Via dell'Antica Posta.
|
||||
// Le luci da palco della festa (render.js: stagelights, lasers, mirrorball).
|
||||
effects: [{ type: 'lasers', phase: 'ingorgo', at: [400, 198], count: 13, len: 460 }, { type: 'lasers', phase: 'ingorgo', at: [370, 200], count: 7, color: '60,220,255' }, { type: 'stagelights', phase: 'ingorgo', from: [[356, 192], [460, 192], [408, 186]], wash: [408, 225, 180], bpm: 132, beams: 3, strobe: 2, pulse: 0.1, colors: ['90,255,110', '255,255,255', '60,220,255'] }, { type: 'floorspots', phase: 'ingorgo', area: [40, 250, 560, 100], bpm: 132, count: 8, colors: ['90,255,110', '255,255,255', '60,220,255'] }],
|
||||
stages: [
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [495, 245] },
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [700, 300], far: 650 },
|
||||
],
|
||||
walk: [[0, 360], [0, 288], [300, 284], [470, 276], [495, 268], [540, 268], [600, 300], [640, 330], [640, 360]],
|
||||
depth: [[268, 0.55], [360, 1.15]],
|
||||
entry: [320, 330],
|
||||
@@ -1547,6 +1668,16 @@ export const story = {
|
||||
name: 'Via dell\'Antica Posta',
|
||||
map: [35, 41],
|
||||
background: 'assets/scenes/via-antica-posta.png',
|
||||
backdrops: [{ when: 'ingorgo', src: 'assets/scenes/ingorgo-antica-posta.png' }], // Codex, la notte dell'Ingorgo (30 ottobre)
|
||||
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza]
|
||||
// Il bar della birra sui gradini; il latino dal Comune a sinistra, la house giù dalla piazza, l'underground su in cima.
|
||||
// Le luci da palco della festa (render.js: stagelights, lasers, mirrorball).
|
||||
effects: [{ type: 'stagelights', phase: 'ingorgo', wash: [90, 190, 130], bpm: 98, colors: ['255,170,60', '255,90,160'], pulse: 0.05 }, { type: 'stagelights', phase: 'ingorgo', from: [[430, 205], [130, 150]], beams: 2, len: 260, bpm: 98 }, { type: 'floorspots', phase: 'ingorgo', area: [40, 230, 560, 120], bpm: 98, count: 5 }],
|
||||
stages: [
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [-110, 250] },
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [320, 560] },
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [384, 40], far: 600 },
|
||||
],
|
||||
walk: [[0, 360], [0, 300], [120, 290], [220, 252], [300, 240], [372, 226], [396, 226], [470, 252], [630, 300], [640, 360]],
|
||||
depth: [[226, 0.35], [360, 1.15]],
|
||||
entry: [320, 345],
|
||||
@@ -1586,6 +1717,15 @@ export const story = {
|
||||
name: 'Piazzetta del Comune',
|
||||
map: [32, 39],
|
||||
background: 'assets/scenes/piazzetta-comune.png',
|
||||
backdrops: [{ when: 'ingorgo', src: 'assets/scenes/ingorgo-piazzetta-comune.png' }], // Codex, la notte dell'Ingorgo (30 ottobre)
|
||||
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza]
|
||||
// La console del latino davanti al Comune; la house arriva da giù, dalla piazza.
|
||||
// Le luci da palco della festa (render.js: stagelights, lasers, mirrorball).
|
||||
effects: [{ type: 'stagelights', phase: 'ingorgo', from: [[232, 212], [332, 212], [282, 205]], wash: [282, 228, 170], bpm: 98, beams: 3, strobe: 8, pulse: 0.08, colors: ['255,120,40', '255,60,120', '255,200,60', '200,80,255'] }, { type: 'floorspots', phase: 'ingorgo', area: [40, 250, 560, 100], bpm: 98, count: 7, colors: ['255,120,40', '255,60,120', '255,200,60', '200,80,255'] }],
|
||||
stages: [
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [290, 235] },
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [260, 640], far: 700 },
|
||||
],
|
||||
walk: [[0, 360], [0, 318], [130, 292], [470, 288], [540, 300], [520, 360]],
|
||||
depth: [[288, 0.55], [360, 1.15]],
|
||||
entry: [260, 350],
|
||||
@@ -1633,6 +1773,16 @@ export const story = {
|
||||
name: 'In cima a Via dell\'Antica Posta',
|
||||
map: [38, 39],
|
||||
background: 'assets/scenes/via-antica-posta-alta.png',
|
||||
backdrops: [{ when: 'ingorgo', src: 'assets/scenes/ingorgo-antica-posta-alta.png' }], // Codex, la notte dell'Ingorgo (30 ottobre)
|
||||
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza]
|
||||
// L'underground sotto il muro della villa; su, in fondo, la tecno di Cambray Digny; giù, lontano, la house.
|
||||
// Le luci da palco della festa (render.js: stagelights, lasers, mirrorball).
|
||||
effects: [{ type: 'mirrorball', phase: 'ingorgo', at: [550, 131], area: [0, 20, 640, 300], count: 110 }, { type: 'stagelights', phase: 'ingorgo', from: [[520, 200], [600, 200]], wash: [552, 205, 160], bpm: 122, beams: 2, strobe: 8, pulse: 0.07, colors: ['170,60,255', '90,40,220', '220,60,255'] }, { type: 'floorspots', phase: 'ingorgo', area: [40, 240, 560, 110], bpm: 122, count: 6, colors: ['170,60,255', '60,120,255', '220,60,255'] }],
|
||||
stages: [
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [565, 235] },
|
||||
{ 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 },
|
||||
],
|
||||
walk: [[0, 360], [0, 322], [190, 226], [283, 222], [470, 360]],
|
||||
depth: [[222, 0.35], [360, 1.15]],
|
||||
entry: [240, 350],
|
||||
@@ -1945,7 +2095,19 @@ export const story = {
|
||||
walk: [[0, 298], [640, 298], [640, 360], [0, 360]],
|
||||
depth: [[298, 1.0], [360, 1.2]],
|
||||
entry: [40, 330],
|
||||
// Baldoria sotto la tribuna col bandierone della Sampdoria: ogni tanto lo sventola (Andrea, 30 ottobre).
|
||||
speakers: { baldoria: [490, 196] },
|
||||
people: [{ who: 'baldoria', at: [490, 318], facing: -1, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.3,
|
||||
flourish: { poses: ['wave', 'wave2'], every: 9, for: 2.1, step: 0.35 } }],
|
||||
hotspots: [
|
||||
{
|
||||
id: 'baldoria', name: 'Baldoria', box: [462, 200, 56, 124], at: [430, 326], face: 'right', default: 'talk',
|
||||
actions: {
|
||||
look: [{ say: 'Baldoria, con la maglia blucerchiata e un bandierone della Sampdoria grande come lui. Qui a San Piero.' }],
|
||||
talk: [{ dialogue: 'baldoria' }],
|
||||
take: [{ say: 'Il bandierone? Prima dovrei passare sul suo cadavere. E non è un modo di dire.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'tribuna', name: 'tribuna', box: [254, 172, 116, 40], at: [310, 320], face: 'up',
|
||||
actions: {
|
||||
@@ -3314,6 +3476,10 @@ export const story = {
|
||||
walk: [[150, 360], [300, 250], [280, 236], [340, 236], [420, 248], [640, 330], [640, 360]],
|
||||
depth: [[236, 0.45], [360, 1.35]],
|
||||
entry: [400, 352],
|
||||
// Al tramonto, quando la Ghiacciomobile è già un ricordo con la targa, lo zio Mario è nel suo orto (Andrea,
|
||||
// 30 ottobre), e ti aspetta a cena.
|
||||
speakers: { zio_mario: [250, 240] },
|
||||
people: [{ who: 'zio_mario', when: 'indizio_regolo', at: [250, 318], facing: -1, idle: 'stand', talk: ['talk', 'explain', 'point'] }],
|
||||
hotspots: [
|
||||
{
|
||||
id: 'casa_zio', name: 'casa dello zio Mario', box: [80, 90, 200, 90], at: [260, 290], face: 'left',
|
||||
@@ -3333,6 +3499,22 @@ export const story = {
|
||||
{
|
||||
id: 'indietro', name: 'indietro, verso il ponte delle poste', box: [150, 336, 490, 24], at: [400, 352], exit: { scene: 'ponte_stazione', at: [158, 192], face: 'down' },
|
||||
},
|
||||
{
|
||||
id: 'zio_mario', name: 'lo zio Mario', box: [228, 248, 44, 72], at: [300, 330], face: 'left', default: 'talk', when: 'indizio_regolo',
|
||||
actions: {
|
||||
look: [{ say: 'Lo zio Mario nel suo orto, col sole che va giù dietro la casa.' }],
|
||||
// Le battute sono di Andrea (30 ottobre).
|
||||
talk: [
|
||||
{ say: 'Ti s\'aspetta a cena, che c\'è la vitella di maiale.', who: 'zio_mario', pose: 'explain' },
|
||||
{ say: 'Non ti avevo visto, pensavo tu fossi andato all\'estero.' },
|
||||
{ say: 'Io ho girato tanto... il Falcione.', who: 'zio_mario', pose: 'laugh' },
|
||||
{ say: 'Zio, che sei stanco?' },
|
||||
{ say: 'No, dormo!', who: 'zio_mario', pose: 'laugh' },
|
||||
{ say: 'Zio, a me mi garba Michael Jackson!' },
|
||||
{ say: 'Mai col gesso!', who: 'zio_mario', pose: 'laugh' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// La strada fra il bivio della Provinciale (alle spalle) e la stazione (in fondo); a destra si va al campone delle
|
||||
@@ -3900,6 +4082,65 @@ export const story = {
|
||||
},
|
||||
},
|
||||
},
|
||||
// Il Taglia al campone delle poste, con la Condor (Andrea, 29 ottobre). [DA ANDREA: battute provvisorie]
|
||||
// Baldoria al campo sportivo. [DA ANDREA: le battute sono provvisorie]
|
||||
baldoria: {
|
||||
nodes: {
|
||||
start: {
|
||||
options: [
|
||||
{ text: 'Baldoria! Ma che ci fai col bandierone al campo?', then: [
|
||||
{ say: 'Mi alleno. Il bandierone va sventolato tutti i giorni, sennò si arrugginisce il braccio.', who: 'baldoria', pose: 'explain' },
|
||||
{ say: 'Forza Doria!', who: 'baldoria', pose: 'wave' },
|
||||
] },
|
||||
{ text: 'Guarda che qui la Sampdoria non ci gioca.', then: [
|
||||
{ say: 'La Samp gioca dovunque ci sia un blucerchiato. E oggi ci sono io.', who: 'baldoria', pose: 'cheer' },
|
||||
] },
|
||||
{ text: 'Hai visto passare il palio, per caso?', then: [
|
||||
{ say: 'L\'unico palio che conosco è blu, bianco, rosso e nero. E ce l\'ho in mano.', who: 'baldoria', pose: 'laugh' },
|
||||
] },
|
||||
{ text: 'Ciao, Baldoria.', next: 'end' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
taglia: {
|
||||
nodes: {
|
||||
start: {
|
||||
options: [
|
||||
{ text: 'Taglia! Si fa una gara con la Condor?', next: 'livello', then: [
|
||||
{ say: 'Io ho già tirato: ora tocca a te. Dodici pallini gialli, e devi fare più di me.', who: 'taglia', pose: 'explain' },
|
||||
{ say: 'Come la vuoi?', who: 'taglia', pose: 'talk' },
|
||||
] },
|
||||
{ text: 'A cosa si tira?', then: [
|
||||
{ say: 'Il cartone sull\'albero, le lattine sulla panchina, le bottigliette sulla porta e il palloncino all\'altalena.', who: 'taglia', pose: 'explain' },
|
||||
{ say: 'E se la vuoi seria, ogni tanto ti tiro una lattina in aria.', who: 'taglia', pose: 'laugh' },
|
||||
] },
|
||||
{ text: 'E una partita a campana, Taglia?', next: 'campana', then: [
|
||||
{ say: 'A campana? Alla nostra età?', who: 'taglia', pose: 'laugh' },
|
||||
{ say: 'Va bene: il gesso ce l\'ho in tasca, il sassolino lo scelgo io. Sul vialetto, dalla 1 alla 8, e chi pesta la riga passa la mano.', who: 'taglia', pose: 'explain' },
|
||||
] },
|
||||
{ text: 'Ma quanto sei alto, Taglia?', then: [{ say: 'Abbastanza da vedere i pallini arrivare prima di te.', who: 'taglia', pose: 'laugh' }] },
|
||||
{ text: 'Ci si vede, Taglia.', next: 'end' },
|
||||
],
|
||||
},
|
||||
livello: {
|
||||
options: [
|
||||
{ text: 'Alla bona.', next: 'end', then: [{ say: 'Alla bona: ti faccio vedere dove arriva il pallino.', who: 'taglia', pose: 'talk' }, { closeup: 'condor', level: 'bona' }] },
|
||||
{ text: 'Seria.', next: 'end', then: [{ say: 'Seria. Occhio alla lattina in aria.', who: 'taglia', pose: 'explain' }, { closeup: 'condor', level: 'seria' }] },
|
||||
{ text: 'A fassi male.', next: 'end', then: [{ say: 'A fassi male? Allora tiro più forte, e più spesso.', who: 'taglia', pose: 'laugh' }, { closeup: 'condor', level: 'male' }] },
|
||||
{ text: 'No, dopo.', next: 'start' },
|
||||
],
|
||||
},
|
||||
campana: {
|
||||
options: [
|
||||
{ text: 'Alla bona.', next: 'end', then: [{ say: 'Alla bona: le righe un contano. Comincia te.', who: 'taglia', pose: 'talk' }, { closeup: 'campana', level: 'bona' }] },
|
||||
{ text: 'Seria.', next: 'end', then: [{ say: 'Seria: chi tocca la riga è fuori. Comincia te.', who: 'taglia', pose: 'explain' }, { closeup: 'campana', level: 'seria' }] },
|
||||
{ text: 'A fassi male.', next: 'end', then: [{ say: 'A fassi male? Io la campana la facevo a occhi chiusi.', who: 'taglia', pose: 'laugh' }, { closeup: 'campana', level: 'male' }] },
|
||||
{ text: 'No, dopo.', next: 'start' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
// Il Chiaru al parcheggio di Cardetole, accanto alla pista col gesso (Andrea, 29 ottobre). [DA ANDREA: battute provvisorie]
|
||||
chiaru: {
|
||||
nodes: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
|
||||
export const VERSION = '0.1.53';
|
||||
export const VERSION = '0.1.59';
|
||||
|
||||
// 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).
|
||||
|
||||
@@ -62,16 +62,16 @@ 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. */
|
||||
#bocce-throw.hold { min-height: 72px; font-size: 14px; }
|
||||
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
|
||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
||||
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary, #gara-help summary, #rinquarto-help summary, #micro-help summary, #cerbottana-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 p { max-width: 76em; line-height: 1.7; }
|
||||
#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-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 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; }
|
||||
@media (max-width: 600px), (orientation: landscape) and (max-height: 520px) {
|
||||
#bocce-controls { padding: 8px; font-size: 10px; overflow-y: auto; min-height: 0; }
|
||||
.bocce-adjust { flex-wrap: wrap; gap: 6px; }
|
||||
.bocce-adjust label { order: -1; flex-basis: 100%; max-width: none; }
|
||||
.bocce-adjust button { flex: 1; font-size: 9px; padding: 6px; }
|
||||
#bocce-throw { flex-basis: 100%; }
|
||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help { font-size: 9px; }
|
||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help, #campana-help { font-size: 9px; }
|
||||
}
|
||||
.effects-control { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; font-size: clamp(9px, 1.2vw, 13px); }
|
||||
#effects-volume { width: 130px; accent-color: var(--accent); }
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { Game } from '../src/engine/game.js';
|
||||
import { story } from '../src/story/story.js';
|
||||
import { settle } from './helpers.mjs';
|
||||
|
||||
test('Baldoria al campo principale: ogni tanto sventola il bandierone, sennò sta fermo', () => {
|
||||
const g = new Game(story); g.go('polisportiva_principale'); settle(g);
|
||||
const p = g.people().find(p => p.who === 'baldoria');
|
||||
assert.ok(p, 'Baldoria è in scena');
|
||||
const poses = new Set();
|
||||
for (let t = 0; t < 9; t += 0.05) { g.time = t; poses.add(g.personPose(p)); }
|
||||
assert.ok(poses.has('wave') && poses.has('wave2') && poses.has('stand') && poses.has('blink'));
|
||||
g.time = 3; assert.equal(g.personPose(p), 'stand', 'fuori dallo sventolio, fermo');
|
||||
const sheet = JSON.parse(readFileSync(new URL('../assets/characters/baldoria.json', import.meta.url)));
|
||||
for (const f of ['stand', 'blink', 'talk', 'explain', 'wave', 'wave2', 'cheer', 'laugh']) assert.ok(sheet.frames[f], f);
|
||||
assert.ok(existsSync(new URL('../assets/characters/baldoria.png', import.meta.url)));
|
||||
});
|
||||
|
||||
test('si parla con Baldoria: le risposte e il saluto', () => {
|
||||
const g = new Game(story); g.go('polisportiva_principale'); settle(g);
|
||||
g.act('talk', 'baldoria'); settle(g);
|
||||
assert.equal(g.dialogue?.id, 'baldoria');
|
||||
const said = [];
|
||||
for (const text of ['Baldoria! Ma che ci fai col bandierone al campo?', 'Guarda che qui la Sampdoria non ci gioca.', 'Hai visto passare il palio, per caso?']) {
|
||||
g.choose(g.options().findIndex(o => o.text === text));
|
||||
said.push(...settle(g).filter(s => s.who === 'baldoria').map(s => s.text));
|
||||
}
|
||||
assert.ok(said.includes('Forza Doria!'));
|
||||
assert.equal(said.length, 4);
|
||||
g.choose(g.options().findIndex(o => o.text === 'Ciao, Baldoria.')); settle(g);
|
||||
assert.equal(g.dialogue, null);
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
// La campana nel browser vero: dal Taglia al campone delle poste, il tiro col mouse e col dito, i salti.
|
||||
// npm run browser:campana — schermate in /tmp/regolo-campana.
|
||||
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-campana';
|
||||
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('giardini_poste'); settle(game);
|
||||
game.act('talk', 'taglia'); settle(game);
|
||||
game.choose(game.options().findIndex(o => o.text.startsWith('E una partita a campana'))); 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/campone-campana.png'));
|
||||
assert.equal(await page.locator('#bocce-throw').textContent(), 'Tira');
|
||||
assert.ok(await page.locator('#campana-help').isVisible());
|
||||
assert.ok(!(await page.locator('#bocce-left').isVisible()), 'niente pulsanti Mira');
|
||||
const box = await page.locator('#scene').boundingBox(), P = (x, y) => [box.x + x / 640 * box.width, box.y + y / 360 * box.height];
|
||||
// Mira alla 1, poi la forza giusta: col mouse tenendo premuto, col dito il cursore e Tira.
|
||||
const one = await page.evaluate(async () => { const m = window.campanaModule = await import('/src/engine/campana.js'); const p = m.center(1); return [p.x, p.y, Math.hypot(p.x - m.FROM.x, p.y - m.FROM.y) / m.REACH]; });
|
||||
if (mobile) {
|
||||
await page.touchscreen.tap(...P(one[0], one[1]));
|
||||
assert.equal(await page.evaluate(() => game.closeup.step), 'aim', 'un tocco mira soltanto');
|
||||
await page.evaluate(p => { game.closeup.power = p; game.closeup.skill.scatter = 0; }, one[2]);
|
||||
await page.locator('#bocce-throw').tap();
|
||||
} else {
|
||||
await page.mouse.move(...P(one[0], one[1]));
|
||||
await page.evaluate(() => { game.closeup.skill.scatter = 0; });
|
||||
await page.mouse.down();
|
||||
await page.waitForFunction(p => Math.abs(game.closeup.power - p) < 0.02, one[2], { polling: 'raf' });
|
||||
await page.screenshot({ path: `${out}/${name}-forza.png` });
|
||||
await page.mouse.up();
|
||||
// Col mouse il momento giusto dipende da quanto è lesta la pagina: se è andato fuori, si ritira col pulsante.
|
||||
await page.waitForFunction(() => !['aim', 'flying'].includes(game.closeup.step));
|
||||
if (await page.evaluate(() => game.closeup.step) !== 'hop') {
|
||||
await page.evaluate(p => { Object.assign(game.closeup, { step: 'aim', current: 'player', next: null, line: null, power: p }); }, one[2]);
|
||||
await page.locator('#bocce-throw').click();
|
||||
}
|
||||
}
|
||||
await page.waitForFunction(() => game.closeup.step === 'hop', null, { timeout: 5000 }).catch(async e => { console.log(await page.evaluate(() => JSON.stringify({ ...game.closeup, events: 0 }))); await page.screenshot({ path: `${out}/${name}-errore.png` }); throw e; });
|
||||
assert.equal(await page.locator('#bocce-throw').textContent(), 'Salta');
|
||||
// Quanti salti riescono: la lancetta dopo una schermata (lenta, con swiftshader) può già essere scappata.
|
||||
let hops = 0;
|
||||
// I salti: si preme quando la lancetta passa nel verde (col dito sul pulsante Salta, col mouse sulla scena).
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await page.waitForFunction(() => { const m = window.campanaModule, c = game.closeup; return c.step !== 'hop' || ((!c.hop || c.hop.t >= m.HOP) && c.meter.t > 0 && Math.abs(m.needle(c)) < 0.15); }, null, { polling: 'raf', timeout: 5000 });
|
||||
if (await page.evaluate(() => game.closeup.step) !== 'hop') break;
|
||||
if (mobile) await page.locator('#bocce-throw').tap(); else await page.mouse.click(...P(320, 200));
|
||||
hops = Math.max(hops, await page.evaluate(() => game.closeup.beat + 1));
|
||||
if (i === 1) await page.screenshot({ path: `${out}/${name}-salti.png` });
|
||||
}
|
||||
assert.ok(hops >= 2, `salti riusciti: ${hops}`);
|
||||
// Poi tocca al Taglia: lo si fa sbagliare per vederlo saltare.
|
||||
await page.evaluate(() => { const c = game.closeup; c.step = 'rest'; c.t = 0; c.next = 'cpu'; c.line = 'Prova: tocca al Taglia.'; });
|
||||
await page.waitForFunction(() => game.closeup.step === 'cpuHop' && game.closeup.beat >= 3 || game.closeup.current === 'player' && game.closeup.step === 'aim', null, { timeout: 20000 });
|
||||
await page.screenshot({ path: `${out}/${name}-taglia.png` });
|
||||
await page.close();
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(`ok: schermate in ${out}`);
|
||||
} finally { await browser?.close(); server.kill(); }
|
||||
@@ -0,0 +1,125 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as m from '../src/engine/campana.js';
|
||||
import { drawCampana } from '../src/engine/campana-render.js';
|
||||
import { Game, MINIGAMES } from '../src/engine/game.js';
|
||||
import { story } from '../src/story/story.js';
|
||||
import { settle } from './helpers.mjs';
|
||||
|
||||
// Tira senza sparpaglio, con la forza giusta per arrivare al punto, e aspetta che il sassolino cada.
|
||||
function toss(c, [x, y]) {
|
||||
c.skill = { ...c.skill, scatter: 0 }; c.aim = { x, y };
|
||||
c.power = Math.hypot(x - m.FROM.x, y - m.FROM.y) / m.REACH;
|
||||
assert.ok(m.shoot(c));
|
||||
for (let i = 0; i < 200 && c.step === 'flying'; i++) m.update(c, 1 / 60);
|
||||
}
|
||||
// Salta ogni volta che la lancetta passa nel mezzo.
|
||||
function hopAll(c) {
|
||||
for (let i = 0; i < 20000 && c.step === 'hop'; i++) {
|
||||
m.update(c, 1 / 120);
|
||||
if (c.step === 'hop' && (!c.hop || c.hop.t >= m.HOP) && Math.abs(m.needle(c)) < 0.03 && c.meter.t > 0) m.press(c, 1);
|
||||
}
|
||||
}
|
||||
const at = id => { const p = m.center(id); return [p.x, p.y]; };
|
||||
|
||||
test('le caselle: 1-2-3, la coppia 4-5, la 6, la coppia 7-8 e il cielo; le righe', () => {
|
||||
for (let n = 1; n <= 8; n++) assert.equal(m.boxAt(at(n), 3).id, n);
|
||||
assert.equal(m.boxAt(at('cielo')).id, 'cielo');
|
||||
assert.ok(m.center(4).y < m.center(5).y, 'la 4 a sinistra di chi salta');
|
||||
const b = m.BOXES[2];
|
||||
assert.ok(m.boxAt([b.x + 1, m.CY], 3).line, 'sul bordo si tocca la riga');
|
||||
assert.ok(!m.boxAt([b.x + 1, m.CY], 0).line, 'alla bona le righe non contano');
|
||||
assert.equal(m.boxAt([20, 20]), null);
|
||||
});
|
||||
|
||||
test('i salti: si scavalca la casella col sassolino, nella coppia un piede solo; tornando lo si raccoglie', () => {
|
||||
const ids = list => list.map(b => b.pick ? 'prendi' : b.out ? 'fuori' : b.boxes.join('+'));
|
||||
assert.deepEqual(ids(m.beats(1)), ['2', '3', '4+5', '6', '7+8', 'cielo', '7+8', '6', '4+5', '3', '2', 'prendi', 'fuori']);
|
||||
assert.deepEqual(ids(m.beats(5)), ['1', '2', '3', '4', '6', '7+8', 'cielo', '7+8', '6', 'prendi', '4', '3', '2', '1', 'fuori']);
|
||||
assert.deepEqual(ids(m.beats(8)), ['1', '2', '3', '4+5', '6', '7', 'cielo', 'prendi', '7', '6', '4+5', '3', '2', '1', 'fuori']);
|
||||
assert.ok(m.beats(3).every(b => !b.boxes.includes(3)), 'mai il piede dove sta il sassolino');
|
||||
});
|
||||
|
||||
test('il sassolino nella casella giusta fa saltare; tutta la campana fatta, si passa alla dopo', () => {
|
||||
const c = m.newCampana(1, { level: 'seria' });
|
||||
toss(c, at(1));
|
||||
assert.equal(c.step, 'hop'); assert.match(c.line, /nella 1/);
|
||||
hopAll(c);
|
||||
assert.equal(c.step, 'done'); assert.equal(c.target.player, 2); assert.equal(c.stone, null, 'raccolto');
|
||||
for (let i = 0; i < 120 && c.step !== 'aim'; i++) m.update(c, 1 / 60);
|
||||
assert.equal(c.current, 'player', 'chi ce la fa ritira');
|
||||
});
|
||||
|
||||
test('fuori, sulla riga o nella casella sbagliata passa la mano al Taglia', () => {
|
||||
for (const [point, why] of [[at(2), /Nella 2, non nella 1/], [[m.START + 1, m.CY], /riga/], [[60, 40], /fuori/i]]) {
|
||||
const c = m.newCampana(2, { level: 'seria' });
|
||||
toss(c, point);
|
||||
assert.equal(c.step, 'rest'); assert.match(c.line, why);
|
||||
for (let i = 0; i < 200 && c.step === 'rest'; i++) m.update(c, 1 / 60);
|
||||
assert.equal(c.current, 'cpu'); assert.equal(c.target.player, 1);
|
||||
}
|
||||
});
|
||||
|
||||
test('l\'equilibrio: troppo presto o troppo tardi si sbaglia', () => {
|
||||
const early = m.newCampana(3, { level: 'male' });
|
||||
toss(early, at(1));
|
||||
while (early.meter.t < 0.05) m.update(early, 1 / 120);
|
||||
m.press(early, 1); assert.equal(early.step, 'rest'); assert.match(early.line, /presto/);
|
||||
const late = m.newCampana(3, { level: 'male' });
|
||||
toss(late, at(1));
|
||||
for (let i = 0; i < 1000 && late.step === 'hop'; i++) m.update(late, 1 / 60);
|
||||
assert.match(late.line, /equilibrio/);
|
||||
});
|
||||
|
||||
test('il tiro col dito: un tocco secco mira soltanto; tenendo premuto la forza va e torna', () => {
|
||||
const c = m.newCampana(4, { level: 'bona' });
|
||||
assert.ok(m.aimScreen(c, [300, 150]));
|
||||
m.press(c, 1); m.update(c, 0.05); m.release(c, 1); assert.equal(c.step, 'aim');
|
||||
m.press(c, 1);
|
||||
for (let i = 0; i < 60 * m.CHARGE; i++) m.update(c, 1 / 60);
|
||||
assert.ok(c.power > 0.95);
|
||||
for (let i = 0; i < 30 * m.CHARGE; i++) m.update(c, 1 / 60);
|
||||
assert.ok(c.power < 0.6, 'e poi torna giù');
|
||||
assert.ok(m.release(c, 1)); assert.equal(c.step, 'flying');
|
||||
assert.ok(m.key(c, 'ArrowUp', true));
|
||||
});
|
||||
|
||||
test('il Taglia gioca da solo; chi finisce la 8 vince', () => {
|
||||
const c = m.newCampana(5, { level: 'seria' });
|
||||
toss(c, [60, 40]);
|
||||
let turns = 0;
|
||||
for (let i = 0; i < 60 * 600 && c.step !== 'result'; i++) {
|
||||
m.update(c, 1 / 60);
|
||||
if (c.current === 'player' && c.step === 'aim') { turns++; toss(c, [60, 40]); }
|
||||
}
|
||||
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'cpu'); assert.equal(c.target.cpu, 9);
|
||||
assert.ok(turns > 0, 'il Taglia ogni tanto sbaglia e ripassa la mano');
|
||||
const w = m.newCampana(6, { level: 'bona' });
|
||||
w.target.player = 8;
|
||||
toss(w, at(8)); hopAll(w);
|
||||
assert.equal(w.step, 'result'); assert.equal(w.result.winner, 'player');
|
||||
});
|
||||
|
||||
test('il disegno della campana 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.newCampana(7, { level: 'bona' }), bg = {};
|
||||
const draw = () => { const before = structuredClone(c); drawCampana(ctx, c, story.closeups.campana, 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 === 'LA CAMPANA'));
|
||||
toss(c, at(1)); for (let i = 0; i < 40; i++) m.update(c, 1 / 60); draw();
|
||||
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'EQUILIBRIO'));
|
||||
c.step = 'result'; c.result = { winner: 'player' }; calls.length = 0; draw();
|
||||
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'BATTUTO IL TAGLIA!'));
|
||||
assert.ok(MINIGAMES.campana);
|
||||
});
|
||||
|
||||
test('dal Taglia, al campone delle poste: la campana, il livello e la vittoria', () => {
|
||||
const g = new Game(story); g.go('giardini_poste'); settle(g);
|
||||
g.act('talk', 'taglia'); settle(g);
|
||||
g.choose(g.options().findIndex(o => o.text.startsWith('E una partita a campana'))); settle(g);
|
||||
g.choose(g.options().findIndex(o => o.text === 'Seria.')); settle(g);
|
||||
assert.equal(g.closeup?.id, 'campana'); assert.equal(g.closeup.level, 'seria');
|
||||
g.closeup.step = 'result'; g.closeup.result = { winner: 'player' };
|
||||
g.closeupClose(); settle(g);
|
||||
assert.ok(g.state.flags.campana_vinta);
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
// La Condor nel browser vero: dal Taglia al campone delle poste, mira, fiato trattenuto e tiro col mouse e col dito.
|
||||
// npm run browser:condor — schermate in /tmp/regolo-condor.
|
||||
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-condor';
|
||||
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('giardini_poste'); settle(game);
|
||||
game.act('talk', 'taglia'); settle(game);
|
||||
game.choose(game.options().findIndex(o => o.text.startsWith('Taglia! Si fa una gara'))); settle(game);
|
||||
game.choose(game.options().findIndex(o => o.text === 'Seria.')); settle(game);
|
||||
});
|
||||
await page.waitForSelector('#bocce-controls:not([hidden])');
|
||||
await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/campone-condor.png'));
|
||||
assert.equal(await page.locator('#bocce-throw').textContent(), 'Spara');
|
||||
assert.ok(await page.locator('#condor-help').isVisible());
|
||||
assert.ok(!(await page.locator('#bocce-power').isVisible()), 'niente cursore della forza');
|
||||
await page.waitForTimeout(1200);
|
||||
const box = await page.locator('#scene').boundingBox(), P = (x, y) => [box.x + x / 640 * box.width, box.y + y / 360 * box.height];
|
||||
if (mobile) await page.touchscreen.tap(...P(98, 138)); else { await page.mouse.move(...P(98, 138)); await page.mouse.click(...P(98, 138)); }
|
||||
assert.equal(await page.evaluate(() => game.closeup.step), 'aim', 'un tocco mira soltanto');
|
||||
if (mobile) await page.locator('#bocce-throw').tap();
|
||||
else { await page.mouse.down(); await page.waitForFunction(() => game.closeup.holdTime >= 0.9); await page.screenshot({ path: `${out}/${name}-fiato.png` }); await page.mouse.up(); }
|
||||
await page.waitForFunction(() => game.closeup.left === 11 && game.closeup.step === 'aim', null, { timeout: 10000 });
|
||||
await page.waitForFunction(() => game.closeup.volo && game.closeup.volo.t > 0.8, null, { timeout: 15000 });
|
||||
await page.screenshot({ path: `${out}/${name}-dopo.png` });
|
||||
await page.close();
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(`ok: schermate in ${out}`);
|
||||
} finally { await browser?.close(); server.kill(); }
|
||||