Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8693bff0c7 | ||
|
|
c6812db291 | ||
|
|
670626afa5 | ||
|
|
9bf41d01af | ||
|
|
16252fb04a | ||
|
|
99d4dede80 | ||
|
|
36eda83fe4 | ||
|
|
58174e68d2 | ||
|
|
ff770af3b5 | ||
|
|
41e5fe2f6b | ||
|
|
9d823375aa | ||
|
|
00f9f15370 | ||
|
|
9fe6e8d0ff | ||
|
|
0f8081d8f0 | ||
|
|
500885ffe0 | ||
|
|
259be2c8b0 | ||
|
|
1706592b81 | ||
|
|
6758e6287e | ||
|
|
a699115a03 |
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"image": "pierino_parigi.png",
|
||||||
|
"height": 120,
|
||||||
|
"frames": {
|
||||||
|
"explain": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 61,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 29.3,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"talk": {
|
||||||
|
"x": 63,
|
||||||
|
"y": 0,
|
||||||
|
"w": 69,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 26.6,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"blink": {
|
||||||
|
"x": 134,
|
||||||
|
"y": 0,
|
||||||
|
"w": 44,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 25.0,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"stand": {
|
||||||
|
"x": 180,
|
||||||
|
"y": 0,
|
||||||
|
"w": 44,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 24.9,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"cheer": {
|
||||||
|
"x": 226,
|
||||||
|
"y": 0,
|
||||||
|
"w": 66,
|
||||||
|
"h": 128,
|
||||||
|
"ax": 34.6,
|
||||||
|
"ay": 128
|
||||||
|
},
|
||||||
|
"shrug": {
|
||||||
|
"x": 294,
|
||||||
|
"y": 0,
|
||||||
|
"w": 89,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 43.8,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"cards": {
|
||||||
|
"x": 385,
|
||||||
|
"y": 0,
|
||||||
|
"w": 50,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 26.2,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"laugh": {
|
||||||
|
"x": 437,
|
||||||
|
"y": 0,
|
||||||
|
"w": 54,
|
||||||
|
"h": 123,
|
||||||
|
"ax": 24.4,
|
||||||
|
"ay": 123
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 105 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 |
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"image": "vannino.png",
|
||||||
|
"height": 120,
|
||||||
|
"frames": {
|
||||||
|
"explain": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 56,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 28.6,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"talk": {
|
||||||
|
"x": 58,
|
||||||
|
"y": 0,
|
||||||
|
"w": 64,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 25.6,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"blink": {
|
||||||
|
"x": 124,
|
||||||
|
"y": 0,
|
||||||
|
"w": 45,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 25.0,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"stand": {
|
||||||
|
"x": 171,
|
||||||
|
"y": 0,
|
||||||
|
"w": 46,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 25.3,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"cheer": {
|
||||||
|
"x": 219,
|
||||||
|
"y": 0,
|
||||||
|
"w": 52,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 26.8,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"shrug": {
|
||||||
|
"x": 273,
|
||||||
|
"y": 0,
|
||||||
|
"w": 80,
|
||||||
|
"h": 120,
|
||||||
|
"ax": 40.7,
|
||||||
|
"ay": 120
|
||||||
|
},
|
||||||
|
"cards": {
|
||||||
|
"x": 355,
|
||||||
|
"y": 0,
|
||||||
|
"w": 45,
|
||||||
|
"h": 121,
|
||||||
|
"ax": 22.7,
|
||||||
|
"ay": 121
|
||||||
|
},
|
||||||
|
"laugh": {
|
||||||
|
"x": 402,
|
||||||
|
"y": 0,
|
||||||
|
"w": 46,
|
||||||
|
"h": 121,
|
||||||
|
"ax": 22.7,
|
||||||
|
"ay": 121
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 537 KiB |
|
After Width: | Height: | Size: 233 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 190 KiB |
@@ -683,6 +683,10 @@
|
|||||||
"file": "andrea-c716ab2381.mp3",
|
"file": "andrea-c716ab2381.mp3",
|
||||||
"sec": 7.95
|
"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.": {
|
"andrea|Il giardino coi cedri e le panchine. All'ombra, d'estate, ci si fa la siesta a turno.": {
|
||||||
"file": "andrea-4fd811d0c7.mp3",
|
"file": "andrea-4fd811d0c7.mp3",
|
||||||
"sec": 6.43
|
"sec": 6.43
|
||||||
@@ -1631,6 +1635,18 @@
|
|||||||
"file": "andrea-a193a43f3e.mp3",
|
"file": "andrea-a193a43f3e.mp3",
|
||||||
"sec": 3.78
|
"sec": 3.78
|
||||||
},
|
},
|
||||||
|
"andrea|La macchina del tempo, parcheggiata davanti al Trappola. Sul cruscotto la data è già pronta.": {
|
||||||
|
"file": "andrea-5e16aca755.mp3",
|
||||||
|
"sec": 6.33
|
||||||
|
},
|
||||||
|
"andrea|Imposto: 10 luglio 1997, ore 18 e 30. Il pokerino da Vannino.": {
|
||||||
|
"file": "andrea-3e60a383c4.mp3",
|
||||||
|
"sec": 8.2
|
||||||
|
},
|
||||||
|
"arcu|Monto anch'io: davanti, però.": {
|
||||||
|
"file": "arcu-664d65b6d1.mp3",
|
||||||
|
"sec": 2.34
|
||||||
|
},
|
||||||
"andrea|Nessuno ha mai capito davvero il tuo immenso valore.": {
|
"andrea|Nessuno ha mai capito davvero il tuo immenso valore.": {
|
||||||
"file": "andrea-3e85200509.mp3",
|
"file": "andrea-3e85200509.mp3",
|
||||||
"sec": 2.93
|
"sec": 2.93
|
||||||
@@ -1871,6 +1887,38 @@
|
|||||||
"file": "andrea-d38b6e49ef.mp3",
|
"file": "andrea-d38b6e49ef.mp3",
|
||||||
"sec": 3.49
|
"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.": {
|
"andrea|Le case coi balconi pieni di vasi: qui ogni finestra è un giardino.": {
|
||||||
"file": "andrea-ec81875c47.mp3",
|
"file": "andrea-ec81875c47.mp3",
|
||||||
"sec": 5.22
|
"sec": 5.22
|
||||||
@@ -1879,6 +1927,54 @@
|
|||||||
"file": "andrea-d32e1a2a87.mp3",
|
"file": "andrea-d32e1a2a87.mp3",
|
||||||
"sec": 3.45
|
"sec": 3.45
|
||||||
},
|
},
|
||||||
|
"andrea|Si torna nel 2007. Ciao Vannino, ciao Pierino.": {
|
||||||
|
"file": "andrea-baa95acb36.mp3",
|
||||||
|
"sec": 4.41
|
||||||
|
},
|
||||||
|
"arcu|Riportata dove l'ho presa. Se ti riviene voglia di pokerini, chiedi.": {
|
||||||
|
"file": "arcu-5df7e4b863.mp3",
|
||||||
|
"sec": 4.24
|
||||||
|
},
|
||||||
|
"andrea|Il Barrettino. Nel 1997. Si sente ancora l'odore del caffè e delle sigarette.": {
|
||||||
|
"file": "andrea-e9ea220852.mp3",
|
||||||
|
"sec": 7.94
|
||||||
|
},
|
||||||
|
"andrea|Il panno verde, le fiches in pila, il mazzo e il posacenere pieno. Una sedia è vuota: è per me.": {
|
||||||
|
"file": "andrea-8c283ea588.mp3",
|
||||||
|
"sec": 8.13
|
||||||
|
},
|
||||||
|
"andrea|Vannino, alto e secco, col canovaccio sulla spalla. E quella fossetta nel mento.": {
|
||||||
|
"file": "andrea-c342d764ea.mp3",
|
||||||
|
"sec": 6.77
|
||||||
|
},
|
||||||
|
"andrea|Pierino Parigi, con gli occhiali sul naso, che guarda le carte come se lo tradissero.": {
|
||||||
|
"file": "andrea-a1771b44e8.mp3",
|
||||||
|
"sec": 5.22
|
||||||
|
},
|
||||||
|
"andrea|L'Arcu, venuto con me nel 1997. Guarda le carte di tutti e non dice nulla. Per ora.": {
|
||||||
|
"file": "andrea-4175b94c54.mp3",
|
||||||
|
"sec": 8.75
|
||||||
|
},
|
||||||
|
"andrea|Arcu, tu non giochi?": {
|
||||||
|
"file": "andrea-833a07768c.mp3",
|
||||||
|
"sec": 1.81
|
||||||
|
},
|
||||||
|
"arcu|Io guardo. Chi guarda un perde mai.": {
|
||||||
|
"file": "arcu-8a1840569a.mp3",
|
||||||
|
"sec": 3.46
|
||||||
|
},
|
||||||
|
"arcu|E quando vuoi si torna: la macchina è fuori, basta dirlo.": {
|
||||||
|
"file": "arcu-89d19833e7.mp3",
|
||||||
|
"sec": 3.71
|
||||||
|
},
|
||||||
|
"andrea|Il televisore a tubo. È il 1997: niente smartphone, solo il televideo.": {
|
||||||
|
"file": "andrea-64b8a38c86.mp3",
|
||||||
|
"sec": 7.87
|
||||||
|
},
|
||||||
|
"andrea|Fuori c'è la sera del 1997. E parcheggiata lì, la macchina del tempo.": {
|
||||||
|
"file": "andrea-1efd47285d.mp3",
|
||||||
|
"sec": 5.94
|
||||||
|
},
|
||||||
"andrea|Un pino a ombrello enorme. Alla ricreazione, tutti all'ombra lì sotto. Sembra ieri.": {
|
"andrea|Un pino a ombrello enorme. Alla ricreazione, tutti all'ombra lì sotto. Sembra ieri.": {
|
||||||
"file": "andrea-0a073729fd.mp3",
|
"file": "andrea-0a073729fd.mp3",
|
||||||
"sec": 7.3
|
"sec": 7.3
|
||||||
@@ -2511,6 +2607,10 @@
|
|||||||
"file": "andrea-a960f89e7a.mp3",
|
"file": "andrea-a960f89e7a.mp3",
|
||||||
"sec": 3.45
|
"sec": 3.45
|
||||||
},
|
},
|
||||||
|
"andrea|Bei tempi quando qui si faceva i pokerini...": {
|
||||||
|
"file": "andrea-2e7cda51c4.mp3",
|
||||||
|
"sec": 2.55
|
||||||
|
},
|
||||||
"andrea|Ci si vede dopo, Arcu.": {
|
"andrea|Ci si vede dopo, Arcu.": {
|
||||||
"file": "andrea-191021ad7c.mp3",
|
"file": "andrea-191021ad7c.mp3",
|
||||||
"sec": 1.68
|
"sec": 1.68
|
||||||
@@ -2539,6 +2639,30 @@
|
|||||||
"file": "arcu-fcaee733df.mp3",
|
"file": "arcu-fcaee733df.mp3",
|
||||||
"sec": 0.92
|
"sec": 0.92
|
||||||
},
|
},
|
||||||
|
"arcu|La macchina è sempre lì. La data è già impostata.": {
|
||||||
|
"file": "arcu-2ddbc3341d.mp3",
|
||||||
|
"sec": 3.69
|
||||||
|
},
|
||||||
|
"arcu|I pokerini da Vannino, col Pierino che bluffava anche a briscola.": {
|
||||||
|
"file": "arcu-9924ed4281.mp3",
|
||||||
|
"sec": 3.83
|
||||||
|
},
|
||||||
|
"arcu|Ci vuoi tornare? Aspetta.": {
|
||||||
|
"file": "arcu-00f5e0a28f.mp3",
|
||||||
|
"sec": 3.73
|
||||||
|
},
|
||||||
|
"andrea|Ma quella è... la macchina del tempo!": {
|
||||||
|
"file": "andrea-a748d9ad25.mp3",
|
||||||
|
"sec": 3.13
|
||||||
|
},
|
||||||
|
"arcu|Il plutonio l'ho trovato da Alvarino. Per un pelo stavano arrivando i Libici a prenderglielo tutto.": {
|
||||||
|
"file": "arcu-c294047de1.mp3",
|
||||||
|
"sec": 6.09
|
||||||
|
},
|
||||||
|
"arcu|Basta salire e mettere la data.": {
|
||||||
|
"file": "arcu-dd10a321b8.mp3",
|
||||||
|
"sec": 2.9
|
||||||
|
},
|
||||||
"andrea|Alla bona, un giro di paese.": {
|
"andrea|Alla bona, un giro di paese.": {
|
||||||
"file": "andrea-b35a4d8cc0.mp3",
|
"file": "andrea-b35a4d8cc0.mp3",
|
||||||
"sec": 2.15
|
"sec": 2.15
|
||||||
@@ -2659,6 +2783,98 @@
|
|||||||
"file": "andrea-78ad61a67a.mp3",
|
"file": "andrea-78ad61a67a.mp3",
|
||||||
"sec": 1.22
|
"sec": 1.22
|
||||||
},
|
},
|
||||||
|
"andrea|C'è posto per uno?": {
|
||||||
|
"file": "andrea-f7481e5519.mp3",
|
||||||
|
"sec": 1.81
|
||||||
|
},
|
||||||
|
"andrea|Vannino, un caffè?": {
|
||||||
|
"file": "andrea-23e4ec2f09.mp3",
|
||||||
|
"sec": 1.86
|
||||||
|
},
|
||||||
|
"andrea|Pierino, come va la mano?": {
|
||||||
|
"file": "andrea-34d248b998.mp3",
|
||||||
|
"sec": 1.59
|
||||||
|
},
|
||||||
|
"andrea|Dopo.": {
|
||||||
|
"file": "andrea-b410f40be6.mp3",
|
||||||
|
"sec": 1.73
|
||||||
|
},
|
||||||
|
"vannino|La sedia è lì. Invito due, punto cinque. Come la vuoi?": {
|
||||||
|
"file": "vannino-225c8700a1.mp3",
|
||||||
|
"sec": 5.37
|
||||||
|
},
|
||||||
|
"vannino|Subito. Ma prima una mano, sennò che ci fai qui?": {
|
||||||
|
"file": "vannino-cd3ce69939.mp3",
|
||||||
|
"sec": 3.21
|
||||||
|
},
|
||||||
|
"pierino_parigi|Bene. O male. Con me un si sa mai, ed è questo il bello.": {
|
||||||
|
"file": "pierino_parigi-46d59a6a61.mp3",
|
||||||
|
"sec": 5.7
|
||||||
|
},
|
||||||
|
"andrea|No, aspetta.": {
|
||||||
|
"file": "andrea-04890c1d8e.mp3",
|
||||||
|
"sec": 1.39
|
||||||
|
},
|
||||||
|
"pierino_parigi|Alla bona: si gioca per il caffè.": {
|
||||||
|
"file": "pierino_parigi-9b57763042.mp3",
|
||||||
|
"sec": 2.33
|
||||||
|
},
|
||||||
|
"pierino_parigi|Seria. Carte, Vannino.": {
|
||||||
|
"file": "pierino_parigi-259f48a09a.mp3",
|
||||||
|
"sec": 2.55
|
||||||
|
},
|
||||||
|
"pierino_parigi|A fassi male? Allora io bluffo.": {
|
||||||
|
"file": "pierino_parigi-8104163151.mp3",
|
||||||
|
"sec": 3.3
|
||||||
|
},
|
||||||
|
"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|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|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
|
||||||
|
},
|
||||||
"andrea|Si fa una gara alle macchinine?!?!": {
|
"andrea|Si fa una gara alle macchinine?!?!": {
|
||||||
"file": "andrea-17216997e2.mp3",
|
"file": "andrea-17216997e2.mp3",
|
||||||
"sec": 2.74
|
"sec": 2.74
|
||||||
@@ -2707,10 +2923,6 @@
|
|||||||
"file": "andrea-fc5a08bbd6.mp3",
|
"file": "andrea-fc5a08bbd6.mp3",
|
||||||
"sec": 0.73
|
"sec": 0.73
|
||||||
},
|
},
|
||||||
"andrea|No, aspetta.": {
|
|
||||||
"file": "andrea-04890c1d8e.mp3",
|
|
||||||
"sec": 1.39
|
|
||||||
},
|
|
||||||
"chiaru|Alla bona: ti faccio vedere dove si ferma. E io vado piano.": {
|
"chiaru|Alla bona: ti faccio vedere dove si ferma. E io vado piano.": {
|
||||||
"file": "chiaru-d31cc223f6.mp3",
|
"file": "chiaru-d31cc223f6.mp3",
|
||||||
"sec": 5.08
|
"sec": 5.08
|
||||||
@@ -3075,6 +3287,22 @@
|
|||||||
"file": "bertovo-ca941bee91.mp3",
|
"file": "bertovo-ca941bee91.mp3",
|
||||||
"sec": 5.41
|
"sec": 5.41
|
||||||
},
|
},
|
||||||
|
"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",
|
||||||
|
"sec": 5.41
|
||||||
|
},
|
||||||
|
"vannino|Già finito? E il caffè chi lo paga?": {
|
||||||
|
"file": "vannino-3307746bb8.mp3",
|
||||||
|
"sec": 3.98
|
||||||
|
},
|
||||||
|
"pierino_parigi|Pari. Si rifà domani sera, alla stess'ora.": {
|
||||||
|
"file": "pierino_parigi-75a2b84953.mp3",
|
||||||
|
"sec": 3.8
|
||||||
|
},
|
||||||
"andrea|Tutti sul muro, verdi in fila. Lo stucco di Pierino un tradisce mai.": {
|
"andrea|Tutti sul muro, verdi in fila. Lo stucco di Pierino un tradisce mai.": {
|
||||||
"file": "andrea-585cd65f93.mp3",
|
"file": "andrea-585cd65f93.mp3",
|
||||||
"sec": 5.2
|
"sec": 5.2
|
||||||
@@ -3087,6 +3315,34 @@
|
|||||||
"file": "andrea-c7dcb11e66.mp3",
|
"file": "andrea-c7dcb11e66.mp3",
|
||||||
"sec": 5.52
|
"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
|
||||||
|
},
|
||||||
"chiaru|Hai vinto te. Ma la mia aveva le ruote piene di gesso, sia chiaro.": {
|
"chiaru|Hai vinto te. Ma la mia aveva le ruote piene di gesso, sia chiaro.": {
|
||||||
"file": "chiaru-91a4b23b22.mp3",
|
"file": "chiaru-91a4b23b22.mp3",
|
||||||
"sec": 4.44
|
"sec": 4.44
|
||||||
|
|||||||
@@ -31,10 +31,15 @@ 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 |
|
| `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ì |
|
| `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/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 |
|
| `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) |
|
| `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 |
|
| `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/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ì |
|
||||||
|
| `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 |
|
| `cardetole.png` | Codex | `cardetole.jpg` | Andrea (EXIF) | **da decidere**: è la casa vera di Andrea, pubblicarla dice dove abita |
|
||||||
| `pieve-interno.png` | Codex | `pieve-interno.png` | Mariano Mirannalti (mandata ad Andrea su WhatsApp) | sì, se Mariano è d'accordo; citarlo nei crediti |
|
| `pieve-interno.png` | Codex | `pieve-interno.png` | Mariano Mirannalti (mandata ad Andrea su WhatsApp) | sì, se Mariano è d'accordo; citarlo nei crediti |
|
||||||
| `panorama-intro.png` | Codex | `altre/panorama-intro.png` | da internet (screenshot con l'icona di Google Lens) | **no**, finché non si sostituisce |
|
| `panorama-intro.png` | Codex | `altre/panorama-intro.png` | da internet (screenshot con l'icona di Google Lens) | **no**, finché non si sostituisce |
|
||||||
|
|||||||
@@ -18,6 +18,29 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
|||||||
|
|
||||||
## Dove siamo arrivati
|
## Dove siamo arrivati
|
||||||
|
|
||||||
|
- **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
|
||||||
|
`TRIP` di `game.js`): si arriva al Barrettino del 1997 (`barrettino_1997`, scena `viaggio: true`: niente mappa né
|
||||||
|
strade; la porta o il pulsante «Torna al 2007», `scene.back` e `game.goBack()`, riportano nel 2007 e la macchina
|
||||||
|
sparisce: per rifare il viaggio si richiede all'Arcu), con l'Arcu che viene a guardare, Vannino (dalla descrizione di Andrea) e Pierino Parigi (consenso del figlio
|
||||||
|
Marco, 29 ottobre), sprite di Codex. Il pokerino (`poker.js`, `poker-render.js`): cinque carte col cambio, mazzo
|
||||||
|
corto dal 7 all'asso (colore sopra il full, A-7-8-9-10 scala), invito 2, punto 5, cinque mani. [DA ANDREA] l'interno
|
||||||
|
del Barrettino (è una proposta di Codex) e le battute.
|
||||||
- **Nuovi posti dalle foto di Andrea (29 ottobre, sfondi di Codex)**: Via della Compagnia (fra Via de' Medici, la
|
- **Nuovi posti dalle foto di Andrea (29 ottobre, sfondi di Codex)**: Via della Compagnia (fra Via de' Medici, la
|
||||||
Pieve e Villa Adami), Via Cafaggio in tre tratti fino alla rotonda, la Provinciale sotto i platani, la passeggiata
|
Pieve e Villa Adami), Via Cafaggio in tre tratti fino alla rotonda, la Provinciale sotto i platani, la passeggiata
|
||||||
sulla Carza (cascatella, orti, dietro le poste, sotto il ponte delle poste, gli orti dello zio Mario), la strada fra
|
sulla Carza (cascatella, orti, dietro le poste, sotto il ponte delle poste, gli orti dello zio Mario), la strada fra
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<button id="map-button" hidden title="Mappa (P)">Mappa</button>
|
<button id="map-button" hidden title="Mappa (P)">Mappa</button>
|
||||||
<button id="notebook-button" hidden title="Taccuino (N)">Taccuino</button>
|
<button id="notebook-button" hidden title="Taccuino (N)">Taccuino</button>
|
||||||
<button id="spots-button" hidden title="Mostra cosa si può toccare (H)">?</button>
|
<button id="spots-button" hidden title="Mostra cosa si può toccare (H)">?</button>
|
||||||
|
<button id="back-button" hidden></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="sound">
|
<div id="sound">
|
||||||
<button id="mute" aria-label="Togli l'audio" title="Audio (M)">♪</button>
|
<button id="mute" aria-label="Togli l'audio" title="Audio (M)">♪</button>
|
||||||
@@ -171,6 +172,22 @@
|
|||||||
Tastiera: B, S, V; frecce e 1–0 scelgono, Invio gioca, Esc lascia il tavolo.</p>
|
Tastiera: B, S, V; frecce e 1–0 scelgono, Invio gioca, Esc lascia il tavolo.</p>
|
||||||
</details>
|
</details>
|
||||||
</section>
|
</section>
|
||||||
|
<section id="poker-controls" hidden aria-label="Comandi del pokerino">
|
||||||
|
<p id="poker-status" role="status"></p>
|
||||||
|
<div class="card-buttons">
|
||||||
|
<button id="poker-move-0" type="button" hidden></button>
|
||||||
|
<button id="poker-move-1" type="button" hidden></button>
|
||||||
|
<button id="poker-move-2" type="button" hidden></button>
|
||||||
|
</div>
|
||||||
|
<details><summary>Come si gioca</summary>
|
||||||
|
<p>Il pokerino del Barrettino: cinque carte coperte col cambio, mazzo corto dal 7 all'asso. Ognuno mette l'invito
|
||||||
|
(2 fiches) nel piatto. Primo giro di puntate: Parola se non c'è niente da mettere, Apro per puntare 5, Vedo per
|
||||||
|
mettere quanto gli altri, Rilancio per alzare di 5, Lascio per uscire dalla mano. Poi il cambio: tocca le carte da
|
||||||
|
cambiare (al massimo 4) e premi Cambia (Servito se le tieni tutte). Secondo giro di puntate, poi chi resta mostra.</p>
|
||||||
|
<p>Punti, dal più basso: carta alta, coppia, doppia coppia, tris, scala, full, colore, poker, scala colore. Col mazzo
|
||||||
|
corto il colore batte il full, e A-7-8-9-10 è la scala più bassa. Cinque mani: vince chi ha più fiches. Esc esce.</p>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
<section id="briscola-controls" hidden aria-label="Comandi della briscola">
|
<section id="briscola-controls" hidden aria-label="Comandi della briscola">
|
||||||
<p id="briscola-status" role="status"></p>
|
<p id="briscola-status" role="status"></p>
|
||||||
<div class="card-hand" aria-label="La mano di Andrea">
|
<div class="card-hand" aria-label="La mano di Andrea">
|
||||||
@@ -255,6 +272,17 @@
|
|||||||
il pulsante alterna Recupera e Molla. Sei lanci. La gloria: 2 pesci alla bona, 3 seria, 4 a fassi male (lo stivale
|
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>
|
non conta, e non pesa). Frecce ← →: distanza del lancio; Esc esce.</p>
|
||||||
</details>
|
</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="cerbottana-help" hidden><summary>Come si gioca</summary>
|
<details id="cerbottana-help" hidden><summary>Come si gioca</summary>
|
||||||
<p>La cerbottana, con lo stucco verde di Pierin di Frilli: dodici pallini contro il muro della piazza. Il bersaglio
|
<p>La cerbottana, con lo stucco verde di Pierin di Frilli: dodici pallini contro il muro della piazza. Il bersaglio
|
||||||
di gesso vale 10, 5 o 2 punti secondo il cerchio, ogni lattina sul muretto 3, il barattolo appeso che dondola (Seria
|
di gesso vale 10, 5 o 2 punti secondo il cerchio, ogni lattina sul muretto 3, il barattolo appeso che dondola (Seria
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "leggenda-del-regolo",
|
"name": "leggenda-del-regolo",
|
||||||
"version": "0.1.52",
|
"version": "0.1.56",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"browser:micro": "node tests/micro.browser.mjs",
|
"browser:micro": "node tests/micro.browser.mjs",
|
||||||
"browser:gara": "node tests/gara.browser.mjs",
|
"browser:gara": "node tests/gara.browser.mjs",
|
||||||
"browser:cerbottana": "node tests/cerbottana.browser.mjs",
|
"browser:cerbottana": "node tests/cerbottana.browser.mjs",
|
||||||
|
"browser:condor": "node tests/condor.browser.mjs",
|
||||||
"browser:tressette": "node tests/tressette.browser.mjs",
|
"browser:tressette": "node tests/tressette.browser.mjs",
|
||||||
"browser:scopa": "node tests/scopa.browser.mjs",
|
"browser:scopa": "node tests/scopa.browser.mjs",
|
||||||
"browser:briscola": "node tests/briscola.browser.mjs",
|
"browser:briscola": "node tests/briscola.browser.mjs",
|
||||||
|
|||||||
@@ -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,9 @@ import * as scopa from './scopa.js';
|
|||||||
import * as gara from './gara.js';
|
import * as gara from './gara.js';
|
||||||
import * as micro from './micro.js';
|
import * as micro from './micro.js';
|
||||||
import * as cerbottana from './cerbottana.js';
|
import * as cerbottana from './cerbottana.js';
|
||||||
|
import { newCorsa, updateCorsa } from './corsa.js';
|
||||||
|
import * as condor from './condor.js';
|
||||||
|
import * as poker from './poker.js';
|
||||||
|
|
||||||
// I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta.
|
// I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta.
|
||||||
export const MINIGAMES = {
|
export const MINIGAMES = {
|
||||||
@@ -23,7 +26,9 @@ export const MINIGAMES = {
|
|||||||
rinquarto: { create: (seed, level) => rinquarto.newRinquarto(seed, { level }), update: rinquarto.update, press: rinquarto.press, release: rinquarto.release, cancel: rinquarto.cancel, aim: rinquarto.aimScreen },
|
rinquarto: { create: (seed, level) => rinquarto.newRinquarto(seed, { level }), update: rinquarto.update, press: rinquarto.press, release: rinquarto.release, cancel: rinquarto.cancel, aim: rinquarto.aimScreen },
|
||||||
basket: { create: (seed, level) => basket.newBasket(seed, { level }), update: basket.update, press: basket.press, release: basket.release, cancel: basket.cancel, aim: basket.aimScreen },
|
basket: { create: (seed, level) => basket.newBasket(seed, { level }), update: basket.update, press: basket.press, release: basket.release, cancel: basket.cancel, aim: basket.aimScreen },
|
||||||
pesca: { create: (seed, level) => pesca.newPesca(seed, { level }), update: pesca.update, press: pesca.press, release: pesca.release, cancel: pesca.cancel, aim: pesca.aimScreen },
|
pesca: { create: (seed, level) => pesca.newPesca(seed, { level }), update: pesca.update, press: pesca.press, release: pesca.release, cancel: pesca.cancel, aim: pesca.aimScreen },
|
||||||
|
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 },
|
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 },
|
||||||
micro: { create: (seed, level) => micro.newMicro(seed, { level }), update: micro.update, press: micro.press, release: micro.release, cancel: micro.cancel, aim: micro.aimScreen },
|
micro: { create: (seed, level) => micro.newMicro(seed, { level }), update: micro.update, press: micro.press, release: micro.release, cancel: micro.cancel, aim: micro.aimScreen },
|
||||||
gara: { create: (seed, level, deck, def) => gara.newGara(seed, { level, riders: def.riders }), update: gara.update, press: gara.press, release: gara.release, cancel: gara.cancel, aim: gara.aimScreen },
|
gara: { create: (seed, level, deck, def) => gara.newGara(seed, { level, riders: def.riders }), update: gara.update, press: gara.press, release: gara.release, cancel: gara.cancel, aim: gara.aimScreen },
|
||||||
};
|
};
|
||||||
@@ -51,6 +56,9 @@ const WALK = ['walk_1', 'walk_2', 'walk_3', 'walk_4'], STEP = 15, REACH = 0.45;
|
|||||||
// Il primo piano di un libro (story.closeups, effetto `closeup`): copertina → frontespizio → una pagina girata →
|
// Il primo piano di un libro (story.closeups, effetto `closeup`): copertina → frontespizio → una pagina girata →
|
||||||
// l'ultima pagina, poi dopo una pausa il commento. Tempi in secondi.
|
// l'ultima pagina, poi dopo una pausa il commento. Tempi in secondi.
|
||||||
export const PAGE_TURN = 0.7, CLOSEUP_PAUSE = 1.4;
|
export const PAGE_TURN = 0.7, CLOSEUP_PAUSE = 1.4;
|
||||||
|
// I tempi del viaggio nel tempo, in secondi: quando comincia a comparire la data, quanto dura ogni tasto, quando si
|
||||||
|
// carica (tremori e lampi), quando arriva il bianco, quando si chiude.
|
||||||
|
export const TRIP = { type: 0.8, key: 0.16, charge: 3.6, flash: 5.4, end: 6.6 };
|
||||||
|
|
||||||
// Condizioni: 'flag', '!flag', 'has:oggetto', '!has:oggetto', oppure un elenco (tutte vere).
|
// Condizioni: 'flag', '!flag', 'has:oggetto', '!has:oggetto', oppure un elenco (tutte vere).
|
||||||
export function check(cond, state) {
|
export function check(cond, state) {
|
||||||
@@ -265,6 +273,17 @@ export class Game {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Il pulsante delle scene fuori dal tempo (scene.back, «Torna al 2007»): le sue azioni partono da dove si è.
|
||||||
|
goBack() {
|
||||||
|
const back = this.scene?.back;
|
||||||
|
if (!back || this.busy || this.dialogue || this.state.ended) return false;
|
||||||
|
this.task = null;
|
||||||
|
this.path = [];
|
||||||
|
this.walking = false;
|
||||||
|
this.queue = [...back.actions];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
skip() {
|
skip() {
|
||||||
if (this.speech) { this.speech = null; return true; }
|
if (this.speech) { this.speech = null; return true; }
|
||||||
return false;
|
return false;
|
||||||
@@ -306,6 +325,8 @@ export class Game {
|
|||||||
if (c.step === 'done') this.queue.unshift(...def.complete);
|
if (c.step === 'done') this.queue.unshift(...def.complete);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Il viaggio si chiude da solo alla fine (o uscendo prima): si arriva comunque.
|
||||||
|
if (def?.type === 'viaggio' || def?.type === 'corsa') { this.queue.unshift(...def.complete); return true; }
|
||||||
// I minigiochi: a partita finita (risultato deciso, anche se il conteggio si sta ancora mostrando) la battuta
|
// 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.
|
// di chi ha vinto; uscendo prima, quella di chi lascia.
|
||||||
if (MINIGAMES[def?.type]) {
|
if (MINIGAMES[def?.type]) {
|
||||||
@@ -336,6 +357,18 @@ export class Game {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Il viaggio nel tempo (story.closeups, type 'viaggio'): la data si compone sul cruscotto una cifra alla volta, poi
|
||||||
|
// tremori, lampi e il bianco; alla fine si chiude da solo e parte `complete` (di solito il `go` nella scena d'arrivo).
|
||||||
|
updateTrip(dt) {
|
||||||
|
const c = this.closeup, def = this.story.closeups[c.id], T = TRIP;
|
||||||
|
c.t += dt;
|
||||||
|
const typed = Math.max(0, Math.min(def.date.length, Math.floor((c.t - T.type) / T.key) + 1));
|
||||||
|
for (; c.typed < typed; c.typed++) if (def.date[c.typed] !== ' ') c.events.push({ kind: 'card-tap', power: 0.7 });
|
||||||
|
if (!c.charge && c.t >= T.charge) { c.charge = true; c.events.push({ kind: 'gara-turbo', power: 1 }); }
|
||||||
|
if (!c.boom && c.t >= T.flash) { c.boom = true; c.events.push({ kind: 'gara-buca', power: 1 }); }
|
||||||
|
if (c.t >= T.end) this.closeupClose();
|
||||||
|
}
|
||||||
|
|
||||||
updateTorch(dt) {
|
updateTorch(dt) {
|
||||||
const c = this.closeup, def = this.story.closeups[c.id];
|
const c = this.closeup, def = this.story.closeups[c.id];
|
||||||
c.t += dt;
|
c.t += dt;
|
||||||
@@ -453,6 +486,8 @@ export class Game {
|
|||||||
const c = this.closeup;
|
const c = this.closeup;
|
||||||
if (c) {
|
if (c) {
|
||||||
if (this.story.closeups[c.id].type === 'torch') { this.updateTorch(dt); return; }
|
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; }
|
||||||
const m = this.minigame();
|
const m = this.minigame();
|
||||||
if (m) {
|
if (m) {
|
||||||
m.update(c, dt);
|
m.update(c, dt);
|
||||||
@@ -494,6 +529,8 @@ export class Game {
|
|||||||
else if (e.closeup) {
|
else if (e.closeup) {
|
||||||
const type = this.story.closeups[e.closeup].type;
|
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] }
|
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)
|
||||||
// `level`: «Alla bona», «Seria» o «A fassi male», scelto nel dialogo con l'avversario.
|
// `level`: «Alla bona», «Seria» o «A fassi male», scelto nel dialogo con l'avversario.
|
||||||
// `decks`: il mazzo da usare quando il suo flag è acceso (briscola: le napoletane al posto delle toscane).
|
// `decks`: il mazzo da usare quando il suo flag è acceso (briscola: le napoletane al posto delle toscane).
|
||||||
: MINIGAMES[type] ? { id: e.closeup, ...MINIGAMES[type].create(Math.floor(Math.random() * 4294967296), e.level,
|
: MINIGAMES[type] ? { id: e.closeup, ...MINIGAMES[type].create(Math.floor(Math.random() * 4294967296), e.level,
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { SEATS, NAMES, MANI, canDraw, myTurn, evaluate, CATEGORIES, red } from './poker.js';
|
||||||
|
import { drawTable, drawCard } from './briscola-render.js';
|
||||||
|
import { text, panel } from './bocce-render.js';
|
||||||
|
|
||||||
|
// Le carte di Andrea, in basso: coordinate condivise da disegno e tocco (bersagli larghi anche sul telefono).
|
||||||
|
export const HAND = { x: 150, y: 222, gap: 70, w: 62, h: 90 };
|
||||||
|
export function cardAt(c, [x, y]) {
|
||||||
|
if (!canDraw(c)) return -1;
|
||||||
|
return c.cards.andrea.findIndex((_, i) => x >= HAND.x + i * HAND.gap && x < HAND.x + i * HAND.gap + HAND.w && y >= HAND.y - 14 && y < HAND.y + HAND.h);
|
||||||
|
}
|
||||||
|
const ink = '#2a1f1a', paper = '#fff4dc';
|
||||||
|
function rect(ctx, x, y, w, h, color) { ctx.fillStyle = color; ctx.fillRect(Math.round(x), Math.round(y), w, h); }
|
||||||
|
// I semi francesi a pixel (il carattere del gioco non ha ♥♦♣♠): cuore, quadro, fiore, picca, larghi 2·s.
|
||||||
|
function pip(ctx, suit, x, y, s) {
|
||||||
|
const color = suit === 'cuori' || suit === 'quadri' ? '#c0392b' : ink;
|
||||||
|
ctx.fillStyle = color; ctx.beginPath();
|
||||||
|
if (suit === 'quadri') { ctx.moveTo(x, y - s); ctx.lineTo(x + s * 0.75, y); ctx.lineTo(x, y + s); ctx.lineTo(x - s * 0.75, y); }
|
||||||
|
else if (suit === 'cuori') {
|
||||||
|
ctx.moveTo(x, y + s); ctx.lineTo(x - s, y - s * 0.1); ctx.arc(x - s * 0.5, y - s * 0.35, s * 0.52, Math.PI * 0.85, Math.PI * 1.95);
|
||||||
|
ctx.arc(x + s * 0.5, y - s * 0.35, s * 0.52, Math.PI * 1.05, Math.PI * 0.15); ctx.lineTo(x, y + s);
|
||||||
|
} else if (suit === 'picche') {
|
||||||
|
ctx.moveTo(x, y - s); ctx.lineTo(x + s, y + s * 0.15); ctx.arc(x + s * 0.5, y + s * 0.3, s * 0.5, 0, Math.PI * 0.9);
|
||||||
|
ctx.lineTo(x, y + s * 0.2); ctx.arc(x - s * 0.5, y + s * 0.3, s * 0.5, Math.PI * 0.1, Math.PI); ctx.closePath();
|
||||||
|
ctx.fill(); ctx.beginPath(); ctx.moveTo(x, y + s * 0.3); ctx.lineTo(x + s * 0.35, y + s); ctx.lineTo(x - s * 0.35, y + s);
|
||||||
|
} else {
|
||||||
|
for (const [dx, dy] of [[0, -0.45], [-0.48, 0.15], [0.48, 0.15]]) { ctx.moveTo(x + dx * s + s * 0.42, y + dy * s); ctx.arc(x + dx * s, y + dy * s, s * 0.42, 0, Math.PI * 2); }
|
||||||
|
ctx.fill(); ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + s * 0.35, y + s); ctx.lineTo(x - s * 0.35, y + s);
|
||||||
|
}
|
||||||
|
ctx.closePath(); ctx.fill();
|
||||||
|
}
|
||||||
|
// Una carta francese: angolo col valore e il seme, il seme grande in mezzo (le figure con una cornice).
|
||||||
|
export function drawFrench(ctx, card, x, y, w, h, back = false) {
|
||||||
|
if (back) { drawCard(ctx, null, x, y, w, h, true); return; }
|
||||||
|
rect(ctx, x + 2, y + 3, w, h, '#15231d'); rect(ctx, x, y, w, h, ink); rect(ctx, x + 1, y + 1, w - 2, h - 2, paper);
|
||||||
|
const color = red(card) ? '#c0392b' : ink, k = w / 62;
|
||||||
|
text(ctx, card.rank, x + 5 * k, y + 9 * k, Math.round(9 * k), color);
|
||||||
|
pip(ctx, card.suit, x + 9 * k, y + 22 * k, 5 * k);
|
||||||
|
if (['J', 'Q', 'K'].includes(card.rank)) {
|
||||||
|
rect(ctx, x + 14 * k, y + 28 * k, 34 * k, 50 * k, red(card) ? '#f3d6c4' : '#d8dfe6');
|
||||||
|
ctx.strokeStyle = color; ctx.lineWidth = 1; ctx.strokeRect(Math.round(x + 14 * k) + 0.5, Math.round(y + 28 * k) + 0.5, Math.round(34 * k), Math.round(50 * k));
|
||||||
|
text(ctx, card.rank, x + 31 * k, y + 46 * k, Math.round(16 * k), color, 'center');
|
||||||
|
pip(ctx, card.suit, x + 31 * k, y + 66 * k, 7 * k);
|
||||||
|
} else pip(ctx, card.suit, x + 31 * k, y + 52 * k, 14 * k);
|
||||||
|
}
|
||||||
|
// Le fiches del piatto: pile colorate, una ogni cinque.
|
||||||
|
function chips(ctx, n, x, y) {
|
||||||
|
const colors = ['#c0392b', '#2f6fb0', '#e8e0c8', '#3a8a4a'];
|
||||||
|
for (let i = 0; i < Math.min(24, Math.ceil(n / 5)); i++) {
|
||||||
|
const col = i % 6, row = Math.floor(i / 6), cx = x + col * 14 - 35, cy = y - row * 3;
|
||||||
|
rect(ctx, cx, cy, 12, 4, '#1a1410'); rect(ctx, cx, cy - 1, 12, 3, colors[i % colors.length]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Dove siedono: Vannino a sinistra, Pierino a destra, Andrea in basso.
|
||||||
|
const SPOTS = { vannino: { x: 40, y: 70 }, pierino: { x: 420, y: 70 } };
|
||||||
|
|
||||||
|
export function drawPoker(ctx, c) {
|
||||||
|
ctx.save(); ctx.imageSmoothingEnabled = false;
|
||||||
|
drawTable(ctx, 'IL POKERINO DEL BARRETTINO · 10 LUGLIO 1997');
|
||||||
|
const show = c.step === 'show' || c.step === 'result';
|
||||||
|
for (const who of ['vannino', 'pierino']) {
|
||||||
|
const { x, y } = SPOTS[who], out = !c.in?.[who];
|
||||||
|
text(ctx, `${NAMES[who]} · ${c.chips[who]}`, x + 90, y - 22, 7, c.current === who ? '#ffe28a' : '#fff0c8', 'center');
|
||||||
|
c.cards[who].forEach((card, i) => drawFrench(ctx, card, x + i * 36, y - 10, 42, 62, !(show && c.in[who])));
|
||||||
|
if (out && c.step !== 'result') { ctx.fillStyle = 'rgba(20,16,12,0.55)'; ctx.fillRect(x - 2, y - 12, 190, 66); text(ctx, 'lascia', x + 90, y + 22, 7, '#d9ccba', 'center'); }
|
||||||
|
if (show && c.in[who] && c.cards[who].length) text(ctx, CATEGORIES[evaluate(c.cards[who])[0]], x + 90, y + 62, 6, c.winners?.includes(who) ? '#b9e5b3' : '#d9ccba', 'center');
|
||||||
|
if (c.changed?.[who] !== undefined && c.step !== 'show') text(ctx, c.changed[who] ? `cambia ${c.changed[who]}` : 'servito', x + 90, y + 62, 6, '#9fd0ff', 'center');
|
||||||
|
}
|
||||||
|
// Il piatto in mezzo.
|
||||||
|
chips(ctx, c.pot ?? 0, 320, 178);
|
||||||
|
text(ctx, `Piatto ${c.pot ?? 0}`, 320, 192, 7, '#ffe09b', 'center');
|
||||||
|
// Le carte di Andrea: quelle scelte per il cambio si alzano.
|
||||||
|
c.cards.andrea.forEach((card, i) => {
|
||||||
|
const up = c.selected?.has(i) ? -14 : 0;
|
||||||
|
drawFrench(ctx, card, HAND.x + i * HAND.gap, HAND.y + up, HAND.w, HAND.h);
|
||||||
|
if (up) text(ctx, 'cambia', HAND.x + i * HAND.gap + HAND.w / 2, HAND.y + up - 7, 5, '#ffe28a', 'center');
|
||||||
|
});
|
||||||
|
text(ctx, `Andrea · ${c.chips.andrea}`, 320, 210, 7, myTurn(c) || canDraw(c) ? '#ffe28a' : '#fff0c8', 'center');
|
||||||
|
if (c.cards.andrea.length) text(ctx, CATEGORIES[evaluate(c.cards.andrea)[0]], 590, 300, 6, '#d9ccba', 'center');
|
||||||
|
text(ctx, `Mano ${Math.min(c.hand, MANI)}/${MANI}`, 40, 300, 6, '#d9ccba');
|
||||||
|
if (c.line && c.step !== 'result') { ctx.fillStyle = '#17160fd0'; ctx.fillRect(120, 146, 400, 16); text(ctx, c.line, 320, 154, 7, c.step === 'show' ? '#b9e5b3' : '#9fd0ff', 'center'); }
|
||||||
|
if (c.step === 'result') {
|
||||||
|
const r = c.result, title = r.winner === 'player' ? 'HAI VINTO IL POKERINO!' : r.winner === 'draw' ? 'PARI' : `HA VINTO ${NAMES[r.top[0]].toUpperCase()}`;
|
||||||
|
panel(ctx, 150, 110, 340, 84);
|
||||||
|
text(ctx, title, 320, 132, 11, r.winner === 'player' ? '#b9e5b3' : '#efd49e', 'center');
|
||||||
|
text(ctx, SEATS.map(s => `${NAMES[s]} ${r.chips[s]}`).join(' · '), 320, 156, 7, '#efd49e', 'center');
|
||||||
|
text(ctx, 'Torna al Barrettino per uscire.', 320, 176, 7, '#d9ccba', 'center');
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
// Il pokerino del Barrettino, luglio 1997 (idea di Andrea, 29 ottobre): poker a cinque carte coperte col cambio, col
|
||||||
|
// mazzo corto da 32 (dal 7 all'asso, come nei bar), Andrea contro Vannino e Pierino Parigi. Si gioca a fiches.
|
||||||
|
// Col mazzo corto il colore è più raro del full e lo batte; la scala più bassa è A-7-8-9-10.
|
||||||
|
// Una mano: il piatto (ognuno mette l'invito), cinque carte, un giro di puntate, il cambio, un altro giro, chi resta
|
||||||
|
// mostra. Puntate fisse (PUNTO) e al massimo RILANCI rilanci a giro. Dopo MANI mani vince chi ha più fiches.
|
||||||
|
|
||||||
|
export const RANKS = ['7', '8', '9', '10', 'J', 'Q', 'K', 'A'];
|
||||||
|
export const SUITS = ['cuori', 'quadri', 'fiori', 'picche'];
|
||||||
|
export const SYMBOL = { cuori: '♥', quadri: '♦', fiori: '♣', picche: '♠' };
|
||||||
|
export const NAMES = { andrea: 'Andrea', vannino: 'Vannino', pierino: 'Pierino' };
|
||||||
|
export const SEATS = ['andrea', 'vannino', 'pierino'];
|
||||||
|
export const START = 100, INVITO = 2, PUNTO = 5, RILANCI = 2, MANI = 5, CPU_TIME = 0.9;
|
||||||
|
export const CATEGORIES = ['carta alta', 'coppia', 'doppia coppia', 'tris', 'scala', 'full', 'colore', 'poker', 'scala colore'];
|
||||||
|
// I livelli: quanto ci pensano gli altri (0 = a caso, 1 = col conto giusto) e quanto bluffano.
|
||||||
|
export const LEVELS = {
|
||||||
|
bona: { skill: 0.35, bluff: 0.04 },
|
||||||
|
seria: { skill: 0.7, bluff: 0.08 },
|
||||||
|
male: { skill: 1, bluff: 0.14 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const value = card => RANKS.indexOf(card.rank) + 7;
|
||||||
|
export const cardName = card => `${card.rank}${SYMBOL[card.suit]}`;
|
||||||
|
export const red = card => card.suit === 'cuori' || card.suit === 'quadri';
|
||||||
|
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
|
||||||
|
function deck(c) {
|
||||||
|
const cards = SUITS.flatMap(suit => RANKS.map(rank => ({ rank, suit })));
|
||||||
|
for (let i = cards.length - 1; i > 0; i--) { const j = Math.floor(random(c) * (i + 1)); [cards[i], cards[j]] = [cards[j], cards[i]]; }
|
||||||
|
return cards;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Il punto di una mano: [categoria, poi i valori per gli spareggi], da confrontare in ordine.
|
||||||
|
export function evaluate(hand) {
|
||||||
|
const values = hand.map(value).sort((a, b) => b - a);
|
||||||
|
const counts = new Map();
|
||||||
|
for (const v of values) counts.set(v, (counts.get(v) ?? 0) + 1);
|
||||||
|
const groups = [...counts].sort((a, b) => b[1] - a[1] || b[0] - a[0]);
|
||||||
|
const flush = hand.every(card => card.suit === hand[0].suit);
|
||||||
|
const distinct = [...counts.keys()].sort((a, b) => b - a);
|
||||||
|
let straight = null;
|
||||||
|
if (distinct.length === 5) {
|
||||||
|
if (distinct[0] - distinct[4] === 4) straight = distinct[0];
|
||||||
|
else if (distinct.join() === '14,10,9,8,7') straight = 10; // A-7-8-9-10: l'asso fa da 6
|
||||||
|
}
|
||||||
|
const shape = groups.map(g => g[1]).join('');
|
||||||
|
const tie = groups.map(g => g[0]);
|
||||||
|
if (straight && flush) return [8, straight];
|
||||||
|
if (shape === '41') return [7, ...tie];
|
||||||
|
if (flush) return [6, ...values];
|
||||||
|
if (shape === '32') return [5, ...tie];
|
||||||
|
if (straight) return [4, straight];
|
||||||
|
if (shape === '311') return [3, ...tie];
|
||||||
|
if (shape === '221') return [2, ...tie];
|
||||||
|
if (shape === '2111') return [1, ...tie];
|
||||||
|
return [0, ...values];
|
||||||
|
}
|
||||||
|
export const compare = (a, b) => { for (let i = 0; i < Math.max(a.length, b.length); i++) if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) - (b[i] ?? 0); return 0; };
|
||||||
|
export const handName = hand => CATEGORIES[evaluate(hand)[0]];
|
||||||
|
|
||||||
|
export function newPoker(seed = Date.now() >>> 0, def = {}) {
|
||||||
|
const level = LEVELS[def.level] ? def.level : 'seria';
|
||||||
|
const c = { step: 'deal', t: 0, seed, level, skill: LEVELS[level], chips: Object.fromEntries(SEATS.map(s => [s, START])),
|
||||||
|
hand: 0, dealer: 2, events: [], line: null, selected: new Set(), history: [], result: null };
|
||||||
|
newHand(c);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
const alive = c => SEATS.filter(s => c.chips[s] >= INVITO);
|
||||||
|
const active = c => SEATS.filter(s => c.in[s]);
|
||||||
|
|
||||||
|
// Una mano nuova: l'invito, il mazzo mischiato, cinque carte a testa; parla per primo chi sta dopo il mazziere.
|
||||||
|
function newHand(c) {
|
||||||
|
c.hand++;
|
||||||
|
c.dealer = (c.dealer + 1) % SEATS.length;
|
||||||
|
c.deck = deck(c);
|
||||||
|
c.in = Object.fromEntries(SEATS.map(s => [s, c.chips[s] >= INVITO]));
|
||||||
|
c.pot = 0;
|
||||||
|
for (const s of active(c)) { c.chips[s] -= INVITO; c.pot += INVITO; }
|
||||||
|
c.cards = Object.fromEntries(SEATS.map(s => [s, c.in[s] ? c.deck.splice(0, 5) : []]));
|
||||||
|
c.changed = {}; c.selected = new Set(); c.winners = null; c.line = null;
|
||||||
|
c.events.push({ kind: 'card-shuffle', power: 1 });
|
||||||
|
startBetting(c, 'bet1');
|
||||||
|
}
|
||||||
|
function startBetting(c, step) {
|
||||||
|
c.step = step; c.t = 0;
|
||||||
|
c.bets = Object.fromEntries(SEATS.map(s => [s, 0]));
|
||||||
|
c.acted = Object.fromEntries(SEATS.map(s => [s, false]));
|
||||||
|
c.raises = 0; c.toBet = 0;
|
||||||
|
c.current = firstAfter(c, c.dealer);
|
||||||
|
skipIfDone(c);
|
||||||
|
}
|
||||||
|
function firstAfter(c, i) {
|
||||||
|
for (let k = 1; k <= SEATS.length; k++) { const s = SEATS[(i + k) % SEATS.length]; if (c.in[s]) return s; }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Un giro di puntate finisce quando tutti quelli rimasti hanno parlato e messo quanto il più alto.
|
||||||
|
const roundOver = c => active(c).every(s => c.acted[s] && (c.bets[s] === c.toBet || c.chips[s] === 0));
|
||||||
|
function skipIfDone(c) {
|
||||||
|
if (active(c).length === 1) return showdown(c);
|
||||||
|
if (roundOver(c)) return endBetting(c);
|
||||||
|
while (c.current && (!c.in[c.current] || c.chips[c.current] === 0 && c.acted[c.current])) c.current = firstAfter(c, SEATS.indexOf(c.current));
|
||||||
|
}
|
||||||
|
function endBetting(c) {
|
||||||
|
if (c.step === 'bet1') { c.step = 'draw'; c.t = 0; c.current = firstAfter(c, c.dealer); return; }
|
||||||
|
showdown(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toCall = (c, who = 'andrea') => Math.max(0, c.toBet - c.bets[who]);
|
||||||
|
export const myTurn = c => (c.step === 'bet1' || c.step === 'bet2') && c.current === 'andrea' && c.in.andrea;
|
||||||
|
export const canRaise = (c, who = 'andrea') => c.raises < RILANCI && c.chips[who] > toCall(c, who);
|
||||||
|
export const canDraw = c => c.step === 'draw' && c.current === 'andrea';
|
||||||
|
|
||||||
|
// Le mosse: «lascio» (si esce dalla mano), «vedo» (o «parola» se non c'è niente da mettere), «punto»/«rilancio».
|
||||||
|
function put(c, who, amount) {
|
||||||
|
const n = Math.min(amount, c.chips[who]);
|
||||||
|
c.chips[who] -= n; c.bets[who] += n; c.pot += n;
|
||||||
|
if (n) c.events.push({ kind: 'card-tap', power: 0.6 });
|
||||||
|
}
|
||||||
|
export function act(c, who, move) {
|
||||||
|
if (c.current !== who || !(c.step === 'bet1' || c.step === 'bet2')) return false;
|
||||||
|
const due = toCall(c, who);
|
||||||
|
if (move === 'lascio' && due === 0) move = 'vedo'; // lasciare gratis non ha senso: si dice parola
|
||||||
|
if (move === 'rilancio' && !canRaise(c, who)) move = 'vedo';
|
||||||
|
if (move === 'lascio') { c.in[who] = false; c.line = `${NAMES[who]}: lascio.`; }
|
||||||
|
else if (move === 'vedo') { put(c, who, due); c.line = `${NAMES[who]}: ${due ? `vedo (${due})` : 'parola'}.`; }
|
||||||
|
else { put(c, who, due + PUNTO); c.toBet = c.bets[who]; c.raises++; for (const s of SEATS) if (s !== who) c.acted[s] = false; c.line = `${NAMES[who]}: ${due ? 'rilancio' : 'apro'} di ${PUNTO}.`; }
|
||||||
|
c.acted[who] = true;
|
||||||
|
c.history.push({ hand: c.hand, step: c.step, who, move });
|
||||||
|
c.current = firstAfter(c, SEATS.indexOf(who));
|
||||||
|
c.t = 0;
|
||||||
|
skipIfDone(c);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Il cambio: si scartano le carte scelte (al massimo 4) e se ne pescano altrettante.
|
||||||
|
export function draw(c, who, indexes) {
|
||||||
|
if (c.step !== 'draw' || c.current !== who) return false;
|
||||||
|
const keep = c.cards[who].filter((_, i) => !indexes.includes(i));
|
||||||
|
c.cards[who] = [...keep, ...c.deck.splice(0, 5 - keep.length)];
|
||||||
|
c.changed[who] = 5 - keep.length;
|
||||||
|
c.line = `${NAMES[who]}: ${c.changed[who] ? `cambio ${c.changed[who]}` : 'servito'}.`;
|
||||||
|
if (c.changed[who]) c.events.push({ kind: 'card-slide', power: 0.6 });
|
||||||
|
if (who === 'andrea') c.selected = new Set();
|
||||||
|
c.current = firstAfter(c, SEATS.indexOf(who));
|
||||||
|
c.t = 0;
|
||||||
|
// Il cambio è finito quando tutti quelli rimasti hanno cambiato.
|
||||||
|
if (active(c).every(s => c.changed[s] !== undefined)) startBetting(c, 'bet2');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function showdown(c) {
|
||||||
|
const left = active(c);
|
||||||
|
const best = left.map(s => ({ s, score: evaluate(c.cards[s]) })).sort((a, b) => compare(b.score, a.score));
|
||||||
|
const winners = left.length === 1 ? left : best.filter(b => compare(b.score, best[0].score) === 0).map(b => b.s);
|
||||||
|
const share = Math.floor(c.pot / winners.length);
|
||||||
|
for (const w of winners) c.chips[w] += share;
|
||||||
|
c.chips[winners[0]] += c.pot - share * winners.length;
|
||||||
|
c.winners = winners;
|
||||||
|
c.line = left.length === 1 ? `Tutti lasciano: il piatto (${c.pot}) va a ${NAMES[winners[0]]}.`
|
||||||
|
: `${winners.map(w => NAMES[w]).join(' e ')} ${winners.length > 1 ? 'dividono' : 'vince'} con ${handName(c.cards[winners[0]])}: ${c.pot} fiches.`;
|
||||||
|
c.step = 'show'; c.t = 0; c.current = null;
|
||||||
|
c.events.push({ kind: 'card-shuffle', power: 0.5 });
|
||||||
|
}
|
||||||
|
// Dopo la mostra un tocco (o «Prossima mano») va avanti; finite le mani, o senza più fiches, il risultato.
|
||||||
|
export function advance(c) {
|
||||||
|
if (c.step !== 'show') return false;
|
||||||
|
if (c.hand >= MANI || c.chips.andrea < INVITO || alive(c).length < 2) return finish(c);
|
||||||
|
newHand(c);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function finish(c) {
|
||||||
|
const max = Math.max(...SEATS.map(s => c.chips[s])), top = SEATS.filter(s => c.chips[s] === max);
|
||||||
|
c.step = 'result';
|
||||||
|
c.result = { winner: top.includes('andrea') ? (top.length > 1 ? 'draw' : 'player') : 'cpu', chips: { ...c.chips }, top };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Gli altri due: quanto vale la mano (0..1), cosa cambiare, come puntare.
|
||||||
|
export function strength(hand) {
|
||||||
|
const [cat, ...rest] = evaluate(hand);
|
||||||
|
return Math.min(1, cat / 8 + ((rest[0] ?? 7) - 7) / 7 / 9);
|
||||||
|
}
|
||||||
|
export function cpuDiscards(c, who) {
|
||||||
|
const hand = c.cards[who], values = hand.map(value), [cat] = evaluate(hand);
|
||||||
|
if (cat >= 4 && cat !== 7) return []; // scala, full, colore, scala colore: servito
|
||||||
|
const counts = new Map(); values.forEach(v => counts.set(v, (counts.get(v) ?? 0) + 1));
|
||||||
|
if (cat >= 1) return hand.map((_, i) => i).filter(i => counts.get(values[i]) === 1 && !(cat === 7)); // tiene le coppie, i tris, il poker
|
||||||
|
const bySuit = SUITS.map(s => hand.map((card, i) => card.suit === s ? i : -1).filter(i => i >= 0)).find(l => l.length === 4);
|
||||||
|
if (bySuit && random(c) < c.skill.skill) return hand.map((_, i) => i).filter(i => !bySuit.includes(i)); // quattro a colore
|
||||||
|
// Altrimenti tiene le due carte più alte (con poca testa, una a caso).
|
||||||
|
const order = hand.map((_, i) => i).sort((a, b) => values[b] - values[a]);
|
||||||
|
return order.slice(random(c) < c.skill.skill ? 2 : 1);
|
||||||
|
}
|
||||||
|
export function cpuMove(c, who) {
|
||||||
|
const s = strength(c.cards[who]), due = toCall(c, who), k = c.skill, noise = (random(c) - 0.5) * (1 - k.skill) * 0.5;
|
||||||
|
const value = s + noise, bluff = random(c) < k.bluff;
|
||||||
|
if (value > 0.42 || bluff) return canRaise(c, who) && (value > 0.55 || bluff) ? 'rilancio' : 'vedo';
|
||||||
|
if (due === 0) return 'vedo';
|
||||||
|
return value > 0.13 + due / 60 ? 'vedo' : 'lascio';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function update(c, dt) {
|
||||||
|
if (!Number.isFinite(dt) || dt <= 0) return;
|
||||||
|
c.t += dt;
|
||||||
|
if (!c.current || c.current === 'andrea' || c.t < CPU_TIME) return;
|
||||||
|
if (c.step === 'bet1' || c.step === 'bet2') act(c, c.current, cpuMove(c, c.current));
|
||||||
|
else if (c.step === 'draw') draw(c, c.current, cpuDiscards(c, c.current));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- I comandi di Andrea: toccare una carta la segna per il cambio; i pulsanti fanno il resto.
|
||||||
|
export function play(c, index) {
|
||||||
|
if (canDraw(c)) {
|
||||||
|
if (c.selected.has(index)) c.selected.delete(index);
|
||||||
|
else if (c.selected.size < 4) c.selected.add(index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
export const change = c => canDraw(c) && draw(c, 'andrea', [...c.selected]);
|
||||||
|
export function moves(c) {
|
||||||
|
if (canDraw(c)) return [{ id: 'cambia', label: c.selected.size ? `Cambia ${c.selected.size}` : 'Servito' }];
|
||||||
|
if (!myTurn(c)) return [];
|
||||||
|
const due = toCall(c);
|
||||||
|
return [
|
||||||
|
{ id: 'lascio', label: 'Lascio', disabled: due === 0 },
|
||||||
|
{ id: 'vedo', label: due ? `Vedo ${due}` : 'Parola' },
|
||||||
|
{ id: 'rilancio', label: due ? `Rilancio ${PUNTO}` : `Apro ${PUNTO}`, disabled: !canRaise(c) },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
export function choose(c, id) {
|
||||||
|
if (id === 'cambia') return change(c);
|
||||||
|
return myTurn(c) && act(c, 'andrea', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function status(c) {
|
||||||
|
const chips = SEATS.map(s => `${NAMES[s]} ${c.chips[s]}`).join(', ');
|
||||||
|
if (c.step === 'result') return `Fine del pokerino. ${chips}. ${c.result.winner === 'player' ? 'Hai vinto tu!' : c.result.winner === 'draw' ? 'Pari.' : `Ha vinto ${NAMES[c.result.top[0]]}.`}`;
|
||||||
|
const where = `Mano ${c.hand} di ${MANI}, piatto ${c.pot}. ${chips}.`;
|
||||||
|
if (c.step === 'show') return `${where} ${c.line} Tocca per la prossima mano.`;
|
||||||
|
if (canDraw(c)) return `${where} Cambio: tocca le carte da cambiare (al massimo 4), poi Cambia.`;
|
||||||
|
if (myTurn(c)) return `${where} ${c.line ?? ''} Tocca a te: ${toCall(c) ? `per vedere servono ${toCall(c)}` : 'parola o apri'}.`.replace(' ', ' ');
|
||||||
|
return `${where} ${c.line ?? ''} ${c.current ? `Parla ${NAMES[c.current]}…` : ''}`.trim();
|
||||||
|
}
|
||||||
@@ -11,6 +11,10 @@ import { drawPesca } from './pesca-render.js';
|
|||||||
import { drawGara } from './gara-render.js';
|
import { drawGara } from './gara-render.js';
|
||||||
import { drawMicro } from './micro-render.js';
|
import { drawMicro } from './micro-render.js';
|
||||||
import { drawCerbottana } from './cerbottana-render.js';
|
import { drawCerbottana } from './cerbottana-render.js';
|
||||||
|
import { drawCorsa } from './corsa.js';
|
||||||
|
import { drawCondor } from './condor-render.js';
|
||||||
|
import { drawPoker } from './poker-render.js';
|
||||||
|
import { drawViaggio } from './viaggio.js';
|
||||||
import { drawBriscola } from './briscola-render.js';
|
import { drawBriscola } from './briscola-render.js';
|
||||||
import { drawTressette } from './tressette-render.js';
|
import { drawTressette } from './tressette-render.js';
|
||||||
import { drawScopa } from './scopa-render.js';
|
import { drawScopa } from './scopa-render.js';
|
||||||
@@ -678,6 +682,21 @@ function drawCloseup(ctx, game, time) {
|
|||||||
if (def.type === 'briscola') { drawBriscola(ctx, c, def); return; }
|
if (def.type === 'briscola') { drawBriscola(ctx, c, def); return; }
|
||||||
if (def.type === 'tressette') { drawTressette(ctx, c, def); return; }
|
if (def.type === 'tressette') { drawTressette(ctx, c, def); return; }
|
||||||
if (def.type === 'scopa') { drawScopa(ctx, c, def); return; }
|
if (def.type === 'scopa') { drawScopa(ctx, c, def); return; }
|
||||||
|
if (def.type === 'viaggio') {
|
||||||
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
|
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;
|
||||||
|
}
|
||||||
if (def.type === 'torch') {
|
if (def.type === 'torch') {
|
||||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
drawTorch(ctx, c, def, backgrounds.get(def.picture));
|
drawTorch(ctx, c, def, backgrounds.get(def.picture));
|
||||||
@@ -688,11 +707,17 @@ function drawCloseup(ctx, game, time) {
|
|||||||
drawBocce(ctx, c, def, backgrounds.get(def.picture), time);
|
drawBocce(ctx, c, def, backgrounds.get(def.picture), time);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (def.type === 'poker') { drawPoker(ctx, c); return; }
|
||||||
if (def.type === 'cerbottana') {
|
if (def.type === 'cerbottana') {
|
||||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
drawCerbottana(ctx, c, def, backgrounds.get(def.picture));
|
drawCerbottana(ctx, c, def, backgrounds.get(def.picture));
|
||||||
return;
|
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 === 'micro') {
|
if (def.type === 'micro') {
|
||||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
drawMicro(ctx, c, def, backgrounds.get(def.picture), src => { if (!backgrounds.has(src)) loadBackground(src); return backgrounds.get(src); });
|
drawMicro(ctx, c, def, backgrounds.get(def.picture), src => { if (!backgrounds.has(src)) loadBackground(src); return backgrounds.get(src); });
|
||||||
|
|||||||
@@ -140,6 +140,14 @@ export class Soundscape {
|
|||||||
else if (kind === 'cerbottana-pff') this.noiseSound(0.12, 2200, 0.05 + strength * 0.05, { pan });
|
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-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); }
|
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 });
|
||||||
else if (kind === 'cerbottana-tosse') { this.noiseSound(0.18, 900, 0.08, { pan }); this.tone(260, 140, 0.12, 0.06, 0.25, pan); this.tone(240, 120, 0.1, 0.05, 0.3, pan); }
|
else if (kind === 'cerbottana-tosse') { this.noiseSound(0.18, 900, 0.08, { pan }); this.tone(260, 140, 0.12, 0.06, 0.25, pan); this.tone(240, 120, 0.1, 0.05, 0.3, pan); }
|
||||||
// Le macchinine: il dito che spinge, le ruote sull'asfalto, l'urto di plastica, fuori pista, il traguardo.
|
// Le macchinine: il dito che spinge, le ruote sull'asfalto, l'urto di plastica, fuori pista, il traguardo.
|
||||||
else if (kind === 'micro-spinta') { this.tone(1200, 500, 0.04, 0.04 + strength * 0.04, 0, pan); this.noiseSound(0.04, 2600, 0.04, { pan }); }
|
else if (kind === 'micro-spinta') { this.tone(1200, 500, 0.04, 0.04 + strength * 0.04, 0, pan); this.noiseSound(0.04, 2600, 0.04, { pan }); }
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Il disegno del viaggio nel tempo (story.closeups, type 'viaggio'; i tempi stanno in game.js, TRIP): il cruscotto,
|
||||||
|
// la data che si compone nel display con le cifre a segmenti che brillano, poi tremori, lampi e il bianco.
|
||||||
|
import { TRIP } from './game.js';
|
||||||
|
|
||||||
|
// Le cifre a sette segmenti: quali segmenti sono accesi (a b c d e f g).
|
||||||
|
const SEG = { 0: 'abcdef', 1: 'bc', 2: 'abged', 3: 'abgcd', 4: 'fgbc', 5: 'afgcd', 6: 'afgedc', 7: 'abc', 8: 'abcdefg', 9: 'abcdfg', '/': '', ':': '', ' ': '' };
|
||||||
|
function digit(ctx, ch, x, y, w, h, color) {
|
||||||
|
const t = Math.max(2, Math.round(w / 6)), on = SEG[ch] ?? '';
|
||||||
|
const bars = { a: [x + t, y, w - 2 * t, t], b: [x + w - t, y + t, t, h / 2 - t], c: [x + w - t, y + h / 2, t, h / 2 - t],
|
||||||
|
d: [x + t, y + h - t, w - 2 * t, t], e: [x, y + h / 2, t, h / 2 - t], f: [x, y + t, t, h / 2 - t], g: [x + t, y + h / 2 - t / 2, w - 2 * t, t] };
|
||||||
|
for (const [k, r] of Object.entries(bars)) { ctx.fillStyle = on.includes(k) ? color : 'rgba(255,90,40,0.08)'; ctx.fillRect(...r.map(Math.round)); }
|
||||||
|
if (ch === ':') { ctx.fillStyle = color; ctx.fillRect(x + w / 2 - t / 2, y + h * 0.3, t, t); ctx.fillRect(x + w / 2 - t / 2, y + h * 0.65, t, t); }
|
||||||
|
if (ch === '/') { ctx.fillStyle = color; for (let i = 0; i < 6; i++) ctx.fillRect(x + w * 0.7 - i * w * 0.08, y + h * 0.15 + i * h * 0.13, t, t); }
|
||||||
|
}
|
||||||
|
export function drawViaggio(ctx, c, def, bg) {
|
||||||
|
const t = c.t, T = TRIP, [dx, dy, dw, dh] = def.display;
|
||||||
|
// Tremori: crescono dalla carica al bianco.
|
||||||
|
const shake = t > T.charge ? Math.min(1, (t - T.charge) / (T.flash - T.charge)) * 5 : 0;
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(Math.round((Math.sin(t * 61) + Math.sin(t * 37)) * shake), Math.round(Math.cos(t * 53) * shake));
|
||||||
|
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#111'; ctx.fillRect(0, 0, 640, 360); }
|
||||||
|
// La data, un tasto alla volta, in due righe: giorno/mese/anno e ora.
|
||||||
|
const lines = def.date.split(' '), shown = c.typed ?? 0;
|
||||||
|
let n = 0;
|
||||||
|
const glow = t > T.charge ? 0.75 + 0.25 * Math.sin(t * 30) : 1;
|
||||||
|
ctx.shadowColor = '#ff5a28'; ctx.shadowBlur = 8;
|
||||||
|
lines.forEach((line, row) => {
|
||||||
|
const ch = 14, cw = 13, gap = 4, total = line.length * (cw + gap) - gap;
|
||||||
|
const x0 = dx + (dw - total) / 2, y0 = dy + 8 + row * (ch * 2 + 2);
|
||||||
|
[...line].forEach((d, i) => {
|
||||||
|
const lit = n++ < shown;
|
||||||
|
digit(ctx, lit ? d : ' ', x0 + i * (cw + gap), y0, cw, ch * 2 - 6, `rgba(255,${Math.round(110 * glow)},40,${lit ? 1 : 0})`);
|
||||||
|
});
|
||||||
|
n++; // lo spazio fra le due righe
|
||||||
|
});
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
ctx.restore();
|
||||||
|
// I lampi: archi azzurri che attraversano il parabrezza, a scatti.
|
||||||
|
if (t > T.charge + 0.6) {
|
||||||
|
const k = Math.floor(t * 14);
|
||||||
|
if (k % 3 !== 0) {
|
||||||
|
ctx.strokeStyle = 'rgba(160,220,255,0.9)'; ctx.lineWidth = 2; ctx.beginPath();
|
||||||
|
let x = 40 + (k * 97) % 560, y = 0; ctx.moveTo(x, y);
|
||||||
|
for (let i = 0; i < 7; i++) { x += ((k * (i + 3) * 37) % 50) - 25; y += 18; ctx.lineTo(x, y); }
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.fillStyle = 'rgba(180,230,255,0.12)'; ctx.fillRect(0, 0, 640, 360);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Il bianco, poi resta bianco fino alla fine.
|
||||||
|
if (t > T.flash) { ctx.fillStyle = `rgba(255,255,255,${Math.min(1, (t - T.flash) / 0.5)})`; ctx.fillRect(0, 0, 640, 360); }
|
||||||
|
}
|
||||||
@@ -13,6 +13,9 @@ import * as biliardo from './engine/biliardo.js';
|
|||||||
import * as rinquarto from './engine/rinquarto.js';
|
import * as rinquarto from './engine/rinquarto.js';
|
||||||
import * as micro from './engine/micro.js';
|
import * as micro from './engine/micro.js';
|
||||||
import * as cerbottana from './engine/cerbottana.js';
|
import * as cerbottana from './engine/cerbottana.js';
|
||||||
|
import * as condor from './engine/condor.js';
|
||||||
|
import * as poker from './engine/poker.js';
|
||||||
|
import { cardAt as pokerAt } from './engine/poker-render.js';
|
||||||
import * as basket from './engine/basket.js';
|
import * as basket from './engine/basket.js';
|
||||||
import * as pesca from './engine/pesca.js';
|
import * as pesca from './engine/pesca.js';
|
||||||
import * as gara from './engine/gara.js';
|
import * as gara from './engine/gara.js';
|
||||||
@@ -311,6 +314,7 @@ function drawRoads() {
|
|||||||
}
|
}
|
||||||
addEventListener('resize', () => { if (!$('map').hidden) drawRoads(); });
|
addEventListener('resize', () => { if (!$('map').hidden) drawRoads(); });
|
||||||
$('map-button').addEventListener('click', openMap);
|
$('map-button').addEventListener('click', openMap);
|
||||||
|
$('back-button').addEventListener('click', () => game.goBack());
|
||||||
|
|
||||||
// --- «?»: per qualche secondo si vede tutto quello che si può toccare (sul telefono non c'è il mouse da passarci sopra).
|
// --- «?»: per qualche secondo si vede tutto quello che si può toccare (sul telefono non c'è il mouse da passarci sopra).
|
||||||
const SPOTS_TIME = 3500;
|
const SPOTS_TIME = 3500;
|
||||||
@@ -511,7 +515,7 @@ canvas.addEventListener('pointerdown', e => {
|
|||||||
}
|
}
|
||||||
if (press?.closeup) return;
|
if (press?.closeup) return;
|
||||||
const aimed = game.closeupAim(scenePoint(e));
|
const aimed = game.closeupAim(scenePoint(e));
|
||||||
if (['biliardo', 'rinquarto', 'basket', 'micro', 'cerbottana'].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.speech) return; // dove non si può mirare (dietro Andrea) non si carica nemmeno
|
||||||
if (aimed) game.closeup.aimLocked = true; // il clic fissa la mira
|
if (aimed) game.closeup.aimLocked = true; // il clic fissa la mira
|
||||||
}
|
}
|
||||||
@@ -583,7 +587,7 @@ canvas.addEventListener('pointermove', e => {
|
|||||||
if (press?.gara === e.pointerId) gara.aimScreen(game.closeup, scenePoint(e), e.pointerId);
|
if (press?.gara === e.pointerId) gara.aimScreen(game.closeup, scenePoint(e), e.pointerId);
|
||||||
return;
|
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'].includes(story.closeups[game.closeup.id].type);
|
||||||
if (aiming) { if (e.pointerType === 'mouse' && !press && !game.closeup.aimLocked) game.closeupAim(scenePoint(e)); }
|
if (aiming) { if (e.pointerType === 'mouse' && !press && !game.closeup.aimLocked) game.closeupAim(scenePoint(e)); }
|
||||||
else if (e.pointerType === 'mouse' || press?.closeup) game.closeupAim(scenePoint(e));
|
else if (e.pointerType === 'mouse' || press?.closeup) game.closeupAim(scenePoint(e));
|
||||||
});
|
});
|
||||||
@@ -604,7 +608,9 @@ const MINI = {
|
|||||||
bocce: { ...bocce, nudge: (c, dir) => bocce.aim(c, c.aim.u, c.aim.w + dir * 0.3), back: 'Torna al pallaio', label: 'Bocce: tocca il campo o il dettaglio per mirare. Regola la forza e premi Lancia.' },
|
bocce: { ...bocce, nudge: (c, dir) => bocce.aim(c, c.aim.u, c.aim.w + dir * 0.3), back: 'Torna al pallaio', label: 'Bocce: tocca il campo o il dettaglio per mirare. Regola la forza e premi Lancia.' },
|
||||||
biliardo: { ...biliardo, back: 'Torna al bar', label: 'Biliardo: clic o tocco fissa la mira. Frecce per affinarla, Maiusc per passi più piccoli. Regola la forza e premi Tira.' },
|
biliardo: { ...biliardo, back: 'Torna al bar', label: 'Biliardo: clic o tocco fissa la mira. Frecce per affinarla, Maiusc per passi più piccoli. Regola la forza e premi Tira.' },
|
||||||
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.' },
|
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.' },
|
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.' },
|
||||||
micro: { ...micro, back: 'Torna dal Chiaru', label: 'La pista col gesso: muovi il mouse o tocca il parcheggio per mirare, tieni premuto per caricare la spinta e lascia; oppure regola la forza e premi Spingi. Frecce o pulsanti Mira per girare.' },
|
micro: { ...micro, back: 'Torna dal Chiaru', label: 'La pista col gesso: muovi il mouse o tocca il parcheggio per mirare, tieni premuto per caricare la spinta e lascia; oppure regola la forza e premi Spingi. Frecce o pulsanti Mira per girare.' },
|
||||||
basket: { ...basket, back: 'Torna in strada', label: 'Fai due tiri: muovi il mouse o tocca per mirare, tieni premuto per caricare la forza, lascia per tirare.' },
|
basket: { ...basket, back: 'Torna in strada', label: 'Fai due tiri: muovi il mouse o tocca per mirare, tieni premuto per caricare la forza, lascia per tirare.' },
|
||||||
// La gara: niente mira né forza, il pulsante alterna gas e freno (chi non può tenere premuto).
|
// La gara: niente mira né forza, il pulsante alterna gas e freno (chi non può tenere premuto).
|
||||||
@@ -648,7 +654,12 @@ $('bocce-throw').addEventListener('pointerdown', e => {
|
|||||||
for (const ev of ['pointerup', 'pointercancel', 'lostpointercapture']) $('bocce-throw').addEventListener(ev, () => { const c = miniState(); if (holdGas(c)) gara.release(c, 'gas-button'); });
|
for (const ev of ['pointerup', 'pointercancel', 'lostpointercapture']) $('bocce-throw').addEventListener(ev, () => { const c = miniState(); if (holdGas(c)) gara.release(c, 'gas-button'); });
|
||||||
$('bocce-throw').addEventListener('contextmenu', e => { if (holdGas(miniState())) e.preventDefault(); });
|
$('bocce-throw').addEventListener('contextmenu', e => { if (holdGas(miniState())) e.preventDefault(); });
|
||||||
// Le carte da gioco: dove si tocca la carta sul tavolo, gioco per gioco.
|
// Le carte da gioco: dove si tocca la carta sul tavolo, gioco per gioco.
|
||||||
const CARD_AT = { briscola: cardAt, tressette: tressetteAt, scopa: scopaAt };
|
const CARD_AT = { briscola: cardAt, tressette: tressetteAt, scopa: scopaAt, poker: pokerAt };
|
||||||
|
// Il pokerino: tre pulsanti fissi (su iPhone un pulsante rifatto a ogni fotogramma perde il tocco), la scritta cambia.
|
||||||
|
for (let i = 0; i < 3; i++) $('poker-move-' + i).addEventListener('click', e => {
|
||||||
|
const c = game?.closeup; if (!c || story.closeups[c.id].type !== 'poker' || game.speech) return;
|
||||||
|
if (c.step === 'show') poker.advance(c); else poker.choose(c, e.currentTarget.dataset.move);
|
||||||
|
});
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
const button = $('scopa-card-' + i);
|
const button = $('scopa-card-' + i);
|
||||||
button.addEventListener('click', () => game?.closeupPlay(i));
|
button.addEventListener('click', () => game?.closeupPlay(i));
|
||||||
@@ -946,7 +957,7 @@ function updateUi() {
|
|||||||
updateChoices();
|
updateChoices();
|
||||||
updateSpeech();
|
updateSpeech();
|
||||||
const mg = miniState() && mini(), def = mg && story.closeups[c.id];
|
const mg = miniState() && mini(), def = mg && story.closeups[c.id];
|
||||||
const cards = ['briscola', 'tressette', 'scopa'].includes(def?.type);
|
const cards = ['briscola', 'tressette', 'scopa', 'poker'].includes(def?.type);
|
||||||
// La presentazione dei piloti del GP a tutto schermo: i comandi arrivano col semaforo (Andrea, 29 ottobre).
|
// La presentazione dei piloti del GP a tutto schermo: i comandi arrivano col semaforo (Andrea, 29 ottobre).
|
||||||
const presenta = !!mg && story.closeups[c.id].type === 'gara' && c.step === 'intro';
|
const presenta = !!mg && story.closeups[c.id].type === 'gara' && c.step === 'intro';
|
||||||
$('app').classList.toggle('presenta', presenta);
|
$('app').classList.toggle('presenta', presenta);
|
||||||
@@ -956,6 +967,20 @@ function updateUi() {
|
|||||||
setProp($('briscola-controls'), 'inert', !running);
|
setProp($('briscola-controls'), 'inert', !running);
|
||||||
setProp($('scopa-controls'), 'hidden', def?.type !== 'scopa');
|
setProp($('scopa-controls'), 'hidden', def?.type !== 'scopa');
|
||||||
setProp($('scopa-controls'), 'inert', !running);
|
setProp($('scopa-controls'), 'inert', !running);
|
||||||
|
setProp($('poker-controls'), 'hidden', def?.type !== 'poker');
|
||||||
|
setProp($('poker-controls'), 'inert', !running);
|
||||||
|
if (def?.type === 'poker') {
|
||||||
|
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
|
||||||
|
setText($('poker-status'), poker.status(c));
|
||||||
|
const moves = c.step === 'show' ? [{ id: 'avanti', label: 'Prossima mano' }] : poker.moves(c);
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const b = $('poker-move-' + i), m = moves[i];
|
||||||
|
setProp(b, 'hidden', !m);
|
||||||
|
if (!m) continue;
|
||||||
|
setText(b, m.label); b.dataset.move = m.id;
|
||||||
|
setProp(b, 'disabled', !!m.disabled || !!game.speech);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (def?.type === 'scopa') {
|
if (def?.type === 'scopa') {
|
||||||
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
|
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
|
||||||
const line = scopa.status(c, def.names);
|
const line = scopa.status(c, def.names);
|
||||||
@@ -1015,7 +1040,7 @@ function updateUi() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mg && !cards) {
|
if (mg && !cards) {
|
||||||
setAttr($('bocce-controls'), 'aria-label', { rinquarto: 'Comandi dei rinquarti', biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca', gara: 'Comandi della gara', micro: 'Comandi delle macchinine', cerbottana: 'Comandi della cerbottana' }[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' }[def.type] ?? 'Comandi delle bocce');
|
||||||
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
|
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
|
||||||
const line = mg.status(c, def.names);
|
const line = mg.status(c, def.names);
|
||||||
if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line;
|
if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line;
|
||||||
@@ -1029,7 +1054,7 @@ function updateUi() {
|
|||||||
// Ai rinquarti la forza è sempre piena: il cursore non serve. Nella guida semplice del GP (alla bona e seria) si
|
// Ai rinquarti la forza è sempre piena: il cursore non serve. Nella guida semplice del GP (alla bona e seria) si
|
||||||
// usa solo il gas: niente sensibilità, sterzo, freno né riquadro dello swipe.
|
// usa solo il gas: niente sensibilità, sterzo, freno né riquadro dello swipe.
|
||||||
const easy = def.type === 'gara' && gara.simple(c);
|
const easy = def.type === 'gara' && gara.simple(c);
|
||||||
setProp(powerLabel, 'hidden', def.type === 'rinquarto' || easy);
|
setProp(powerLabel, 'hidden', def.type === 'rinquarto' || def.type === 'condor' || easy);
|
||||||
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy);
|
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy);
|
||||||
const powerName = def.type === 'gara' ? 'Sensibilità ' : def.type === 'cerbottana' ? 'Fiato ' : 'Forza ';
|
const powerName = def.type === 'gara' ? 'Sensibilità ' : def.type === 'cerbottana' ? 'Fiato ' : 'Forza ';
|
||||||
if (powerLabel.firstChild.nodeValue !== powerName) powerLabel.firstChild.nodeValue = powerName;
|
if (powerLabel.firstChild.nodeValue !== powerName) powerLabel.firstChild.nodeValue = powerName;
|
||||||
@@ -1050,14 +1075,18 @@ function updateUi() {
|
|||||||
setProp($('rinquarto-help'), 'hidden', def.type !== 'rinquarto');
|
setProp($('rinquarto-help'), 'hidden', def.type !== 'rinquarto');
|
||||||
setProp($('micro-help'), 'hidden', def.type !== 'micro');
|
setProp($('micro-help'), 'hidden', def.type !== 'micro');
|
||||||
setProp($('cerbottana-help'), 'hidden', def.type !== 'cerbottana');
|
setProp($('cerbottana-help'), 'hidden', def.type !== 'cerbottana');
|
||||||
|
setProp($('condor-help'), 'hidden', def.type !== 'condor');
|
||||||
setProp($('basket-help'), 'hidden', def.type !== 'basket');
|
setProp($('basket-help'), 'hidden', def.type !== 'basket');
|
||||||
setProp($('pesca-help'), 'hidden', def.type !== 'pesca');
|
setProp($('pesca-help'), 'hidden', def.type !== 'pesca');
|
||||||
setProp($('gara-help'), 'hidden', def.type !== 'gara');
|
setProp($('gara-help'), 'hidden', def.type !== 'gara');
|
||||||
if (mg.aimFree?.(c) ?? !mg.canThrow(c)) c.aimLocked = false; // a ogni nuovo tiro la mira torna libera
|
if (mg.aimFree?.(c) ?? !mg.canThrow(c)) c.aimLocked = false; // a ogni nuovo tiro la mira torna libera
|
||||||
}
|
}
|
||||||
if (mg) for (const event of c.events.splice(0)) ambience.play(event.kind, { pan: 0, strength: event.power });
|
if (mg || c?.events) for (const event of c.events.splice(0)) ambience.play(event.kind, { pan: 0, strength: event.power });
|
||||||
// Il pulsante della mappa c'è solo in partita, fuori dai dialoghi, e quando c'è almeno un altro posto dove andare.
|
// Il pulsante della mappa c'è solo in partita, fuori dai dialoghi, e quando c'è almeno un altro posto dove andare.
|
||||||
setProp($('map-button'), 'hidden', !running || !!game.dialogue || !!game.closeup || game.mapPlaces().length < 2);
|
setProp($('map-button'), 'hidden', !running || !!game.dialogue || !!game.closeup || game.mapPlaces().length < 2);
|
||||||
|
// «Torna al 2007»: nelle scene fuori dal tempo, finché non si parla con qualcuno.
|
||||||
|
setProp($('back-button'), 'hidden', !running || !game.scene?.back || !!game.dialogue || !!game.closeup);
|
||||||
|
setText($('back-button'), game.scene?.back?.label ?? '');
|
||||||
setProp($('pause-button'), 'hidden', !running);
|
setProp($('pause-button'), 'hidden', !running);
|
||||||
setProp($('spots-button'), 'hidden', !running || !!game.closeup);
|
setProp($('spots-button'), 'hidden', !running || !!game.closeup);
|
||||||
setProp($('notebook-button'), 'hidden', !running || !story.notebook || !!game.closeup);
|
setProp($('notebook-button'), 'hidden', !running || !story.notebook || !!game.closeup);
|
||||||
@@ -1067,7 +1096,7 @@ function updateUi() {
|
|||||||
canvas.classList.toggle('torch', !!torch);
|
canvas.classList.toggle('torch', !!torch);
|
||||||
canvas.classList.toggle('bocce', !!mg);
|
canvas.classList.toggle('bocce', !!mg);
|
||||||
// Biliardo e GP di Fortezza: audio, pausa e uscita fuori dal quadro (al GP la pista passa negli angoli).
|
// Biliardo e GP di Fortezza: audio, pausa e uscita fuori dal quadro (al GP la pista passa negli angoli).
|
||||||
$('stage').classList.toggle('biliardo', !!c && ['biliardo', 'rinquarto', 'gara', 'micro', 'cerbottana'].includes(story.closeups[c.id].type));
|
$('stage').classList.toggle('biliardo', !!c && ['biliardo', 'rinquarto', 'gara', 'micro', 'cerbottana', 'condor'].includes(story.closeups[c.id].type));
|
||||||
$('stage').classList.toggle('briscola', !!cards);
|
$('stage').classList.toggle('briscola', !!cards);
|
||||||
setAttr(canvas, 'aria-label', torch ? 'Fessura: sposta la luce con mouse, dito o frecce. Invio cerca il prossimo dettaglio; Esc esce.' : mg ? mg.label : "La scena dell'avventura");
|
setAttr(canvas, 'aria-label', torch ? 'Fessura: sposta la luce con mouse, dito o frecce. Invio cerca il prossimo dettaglio; Esc esce.' : mg ? mg.label : "La scena dell'avventura");
|
||||||
// La pagina che comincia a girare fa il suo fruscio.
|
// La pagina che comincia a girare fa il suo fruscio.
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ export const ambience = {
|
|||||||
{ type: 'insect', kind: 'dragonfly', phase: day, area: [440, 228, 110, 30], every: 31000, duration: 8500, offset: 15000 },
|
{ type: 'insect', kind: 'dragonfly', phase: day, area: [440, 228, 110, 30], every: 31000, duration: 8500, offset: 15000 },
|
||||||
],
|
],
|
||||||
orti_zio_mario: [
|
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: '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 },
|
{ type: 'sway', rects: [[180, 10, 140, 180], [430, 10, 210, 240]], amount: 2 },
|
||||||
butterfly([10, 240, 140, 50], 8000),
|
butterfly([10, 240, 140, 50], 8000),
|
||||||
|
|||||||
@@ -43,6 +43,23 @@ 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' },
|
||||||
|
];
|
||||||
|
|
||||||
export const story = {
|
export const story = {
|
||||||
title: 'La Leggenda del Regolo',
|
title: 'La Leggenda del Regolo',
|
||||||
// La mappa (pulsante «Mappa»): ogni scena con `map: [x, y]` (percentuali della pianta) ci compare quando è
|
// La mappa (pulsante «Mappa»): ogni scena con `map: [x, y]` (percentuali della pianta) ci compare quando è
|
||||||
@@ -233,6 +250,36 @@ export const story = {
|
|||||||
quit: [{ say: 'Questo me lo tengo per la prossima.' }],
|
quit: [{ say: 'Questo me lo tengo per la prossima.' }],
|
||||||
draw: [{ say: 'Visto? Geometrie celestiali. Si riprova quando vuoi.', who: 'bertovo', pose: 'explain' }],
|
draw: [{ say: 'Visto? Geometrie celestiali. Si riprova quando vuoi.', who: 'bertovo', pose: 'explain' }],
|
||||||
},
|
},
|
||||||
|
// Il viaggio nel tempo dalla macchina del tempo in Via Liguri Magelli: la data sul cruscotto (Codex), poi il Barrettino
|
||||||
|
// del 10 luglio 1997 (Andrea, 29 ottobre).
|
||||||
|
viaggio: {
|
||||||
|
type: 'viaggio',
|
||||||
|
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: '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' }],
|
||||||
|
},
|
||||||
// La cerbottana con lo stucco verde di Pierin di Frilli, contro il muro della piazza (Andrea, 29 ottobre).
|
// La cerbottana con lo stucco verde di Pierin di Frilli, contro il muro della piazza (Andrea, 29 ottobre).
|
||||||
// [DA ANDREA: le battute sono provvisorie]
|
// [DA ANDREA: le battute sono provvisorie]
|
||||||
cerbottana: {
|
cerbottana: {
|
||||||
@@ -244,6 +291,16 @@ export const story = {
|
|||||||
quit: [{ say: 'Il muro resta lì. E l\'odore dello stucco, sulle dita, pure.' }],
|
quit: [{ say: 'Il muro resta lì. E l\'odore dello stucco, sulle dita, pure.' }],
|
||||||
draw: [],
|
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.' }],
|
||||||
|
},
|
||||||
// Le macchinine sulla pista col gesso, sul parcheggio di Cardetole, contro il Chiaru (Andrea, 28 e 29 ottobre).
|
// Le macchinine sulla pista col gesso, sul parcheggio di Cardetole, contro il Chiaru (Andrea, 28 e 29 ottobre).
|
||||||
// [DA ANDREA: le battute sono provvisorie]
|
// [DA ANDREA: le battute sono provvisorie]
|
||||||
micro: {
|
micro: {
|
||||||
@@ -323,9 +380,16 @@ export const story = {
|
|||||||
tozzino: { name: 'Il Tozzino', color: '#c8f5b0', sprite: 'assets/characters/tozzino.json' },
|
tozzino: { name: 'Il Tozzino', color: '#c8f5b0', sprite: 'assets/characters/tozzino.json' },
|
||||||
// Il Chiaru (Matteo Chiarusi, consenso dato ad Andrea il 29 ottobre), alla pista col gesso di Cardetole: sprite di
|
// Il Chiaru (Matteo Chiarusi, consenso dato ad Andrea il 29 ottobre), alla pista col gesso di Cardetole: sprite di
|
||||||
// Codex dalla foto di Andrea.
|
// Codex dalla foto di Andrea.
|
||||||
|
// Al Barrettino del 1997 (Andrea, 29 ottobre; senza foto, dalla descrizione di Andrea): Vannino, alto, magro, coi
|
||||||
|
// capelli corti e la fossetta nel mento; Pierino Parigi (consenso del figlio Marco, 29 ottobre), brizzolato, con gli
|
||||||
|
// occhiali e la barba rada. Sprite di Codex.
|
||||||
|
vannino: { name: 'Vannino', color: '#ffe6a8', sprite: 'assets/characters/vannino.json' },
|
||||||
|
pierino_parigi: { name: 'Pierino', color: '#c8e0ff', sprite: 'assets/characters/pierino_parigi.json' },
|
||||||
// Lo zio Mario di Andrea (sprite di Codex dalla foto di Andrea, 29 ottobre), alla Ghiacciomobile nel muro.
|
// 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' },
|
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' },
|
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' },
|
barondeduke: { name: 'Il BaronDeDuke', color: '#e0c090', sprite: 'assets/characters/barondeduke.json' },
|
||||||
mariano: { name: 'Mariano', color: '#ffb0a3', sprite: 'assets/characters/mariano.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.
|
// Non parla: è una cosa, disegnata come un personaggio per stare davanti o dietro ad Andrea.
|
||||||
@@ -1295,6 +1359,9 @@ export const story = {
|
|||||||
walk: [[0, 360], [0, 232], [640, 232], [640, 360]],
|
walk: [[0, 360], [0, 232], [640, 232], [640, 360]],
|
||||||
depth: [[232, 0.55], [360, 1.15]],
|
depth: [[232, 0.55], [360, 1.15]],
|
||||||
entry: [580, 340],
|
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: [
|
hotspots: [
|
||||||
{
|
{
|
||||||
id: 'giochi', name: 'giochi', box: [196, 120, 330, 100], at: [360, 250], face: 'up',
|
id: 'giochi', name: 'giochi', box: [196, 120, 330, 100], at: [360, 250], face: 'up',
|
||||||
@@ -1325,6 +1392,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: '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.
|
// Via Calimara: dalla Piazza scende alla farmacia, in Via Provinciale.
|
||||||
@@ -2749,7 +2823,9 @@ export const story = {
|
|||||||
speakers: { arcu: [374, 176], zio_mario: [440, 196] },
|
speakers: { arcu: [374, 176], zio_mario: [440, 196] },
|
||||||
people: [
|
people: [
|
||||||
// L'Arcu sotto la tenda del Trappola: due minuti fa era a Campomigliaio (Andrea, 28 ottobre).
|
// L'Arcu sotto la tenda del Trappola: due minuti fa era a Campomigliaio (Andrea, 28 ottobre).
|
||||||
{ who: 'arcu', at: [374, 248], facing: -1, size: 0.75, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.2 },
|
// La macchina del tempo, dopo i pokerini dell'Arcu.
|
||||||
|
{ who: 'macchina', when: 'macchina_trappola', at: [200, 262], size: 1.1, idle: 'stand', talk: [] },
|
||||||
|
{ who: 'arcu', at: [374, 248], facing: -1, size: 1.05, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.2 },
|
||||||
// Lo zio Mario davanti alla Ghiacciomobile nel muro, che si gratta la testa (sprite di Codex dalla sua foto,
|
// Lo zio Mario davanti alla Ghiacciomobile nel muro, che si gratta la testa (sprite di Codex dalla sua foto,
|
||||||
// 29 ottobre). Solo finché la macchina è lì. [DA ANDREA: le sue battute]
|
// 29 ottobre). Solo finché la macchina è lì. [DA ANDREA: le sue battute]
|
||||||
{ who: 'zio_mario', when: '!indizio_regolo', at: [440, 330], facing: 1, idle: 'think', talk: ['talk', 'explain', 'point'] },
|
{ who: 'zio_mario', when: '!indizio_regolo', at: [440, 330], facing: 1, idle: 'think', talk: ['talk', 'explain', 'point'] },
|
||||||
@@ -2795,6 +2871,14 @@ export const story = {
|
|||||||
use: [{ say: 'Per tirarla fuori di lì ci vuole altro che le mie braccia.' }],
|
use: [{ say: 'Per tirarla fuori di lì ci vuole altro che le mie braccia.' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'macchina_tempo', name: 'macchina del tempo', box: [150, 214, 104, 48], at: [212, 280], face: 'up', default: 'use', when: 'macchina_trappola',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'La macchina del tempo, parcheggiata davanti al Trappola. Sul cruscotto la data è già pronta.' }],
|
||||||
|
use: [{ say: 'Imposto: 10 luglio 1997, ore 18 e 30. Il pokerino da Vannino.' }, { say: 'Monto anch\'io: davanti, però.', who: 'arcu', pose: 'laugh' }, { closeup: 'viaggio' }],
|
||||||
|
open: [{ say: 'Imposto: 10 luglio 1997, ore 18 e 30. Il pokerino da Vannino.' }, { say: 'Monto anch\'io: davanti, però.', who: 'arcu', pose: 'laugh' }, { closeup: 'viaggio' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// La prima battuta è di Andrea (dal circolino, 26 ottobre).
|
// La prima battuta è di Andrea (dal circolino, 26 ottobre).
|
||||||
id: 'betino', name: 'il Betino dell\'Arcu', box: [232, 172, 40, 24], at: [250, 206], face: 'up', default: 'look',
|
id: 'betino', name: 'il Betino dell\'Arcu', box: [232, 172, 40, 24], at: [250, 206], face: 'up', default: 'look',
|
||||||
@@ -3272,6 +3356,10 @@ export const story = {
|
|||||||
walk: [[150, 360], [300, 250], [280, 236], [340, 236], [420, 248], [640, 330], [640, 360]],
|
walk: [[150, 360], [300, 250], [280, 236], [340, 236], [420, 248], [640, 330], [640, 360]],
|
||||||
depth: [[236, 0.45], [360, 1.35]],
|
depth: [[236, 0.45], [360, 1.35]],
|
||||||
entry: [400, 352],
|
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: [
|
hotspots: [
|
||||||
{
|
{
|
||||||
id: 'casa_zio', name: 'casa dello zio Mario', box: [80, 90, 200, 90], at: [260, 290], face: 'left',
|
id: 'casa_zio', name: 'casa dello zio Mario', box: [80, 90, 200, 90], at: [260, 290], face: 'left',
|
||||||
@@ -3291,6 +3379,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: '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
|
// La strada fra il bivio della Provinciale (alle spalle) e la stazione (in fondo); a destra si va al campone delle
|
||||||
@@ -3322,6 +3426,69 @@ export const story = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// Il Barrettino il 10 luglio 1997 alle 18:30 (Andrea, 29 ottobre; l'interno è una proposta di Codex, [DA ANDREA]):
|
||||||
|
// Vannino e Pierino Parigi con la tavola apparecchiata da poker. Si torna nel 2007 dalla porta.
|
||||||
|
barrettino_1997: {
|
||||||
|
name: 'Il Barrettino, 10 luglio 1997',
|
||||||
|
// Ci si arriva solo con la macchina del tempo: niente mappa, niente strade (la porta riporta nel 2007).
|
||||||
|
viaggio: true,
|
||||||
|
// Il pulsante «Torna al 2007», sempre lì in alto: per uscire da quel tempo senza cercare la porta (Andrea, 29 ottobre).
|
||||||
|
back: { label: 'Torna al 2007', actions: RITORNO_2007 },
|
||||||
|
background: 'assets/scenes/barrettino-1997.png',
|
||||||
|
indoor: true,
|
||||||
|
walk: [[0, 360], [0, 300], [110, 282], [540, 282], [640, 300], [640, 360]],
|
||||||
|
depth: [[282, 2.1], [360, 2.6]],
|
||||||
|
entry: [320, 330],
|
||||||
|
enter: [{ if: '!visto_1997', then: [{ set: 'visto_1997' }, { say: 'Il Barrettino. Nel 1997. Si sente ancora l\'odore del caffè e delle sigarette.' }] }],
|
||||||
|
speakers: { vannino: [160, 120], pierino_parigi: [470, 120], arcu: [580, 130] },
|
||||||
|
people: [
|
||||||
|
{ who: 'vannino', at: [160, 300], facing: 1, size: 1, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.1 },
|
||||||
|
{ who: 'pierino_parigi', at: [470, 300], facing: -1, size: 1, idle: 'cards', talk: ['talk', 'explain'], blink: 'blink', every: 3.6 },
|
||||||
|
// L'Arcu è venuto con me, a guardare la partita (Andrea, 29 ottobre). [DA ANDREA: le battute]
|
||||||
|
{ who: 'arcu', at: [585, 318], facing: -1, size: 1.05, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.2 },
|
||||||
|
],
|
||||||
|
hotspots: [
|
||||||
|
{
|
||||||
|
id: 'tavola', name: 'la tavola da poker', box: [236, 186, 150, 70], at: [320, 300], face: 'up', default: 'talk',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Il panno verde, le fiches in pila, il mazzo e il posacenere pieno. Una sedia è vuota: è per me.' }],
|
||||||
|
talk: [{ dialogue: 'pokerino' }],
|
||||||
|
use: [{ dialogue: 'pokerino' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vannino', name: 'Vannino', box: [130, 160, 60, 140], at: [220, 320], face: 'left', default: 'talk',
|
||||||
|
actions: { look: [{ say: 'Vannino, alto e secco, col canovaccio sulla spalla. E quella fossetta nel mento.' }], talk: [{ dialogue: 'pokerino' }] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pierino', name: 'Pierino', box: [440, 160, 60, 140], at: [410, 320], face: 'right', default: 'talk',
|
||||||
|
actions: { look: [{ say: 'Pierino Parigi, con gli occhiali sul naso, che guarda le carte come se lo tradissero.' }], talk: [{ dialogue: 'pokerino' }] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'arcu', name: 'l\'Arcu', box: [566, 170, 40, 150], at: [540, 320], face: 'right', default: 'talk',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'L\'Arcu, venuto con me nel 1997. Guarda le carte di tutti e non dice nulla. Per ora.' }],
|
||||||
|
talk: [
|
||||||
|
{ say: 'Arcu, tu non giochi?' },
|
||||||
|
{ say: 'Io guardo. Chi guarda un perde mai.', who: 'arcu', pose: 'explain' },
|
||||||
|
{ say: 'E quando vuoi si torna: la macchina è fuori, basta dirlo.', who: 'arcu', pose: 'talk' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'televisore', name: 'televisore', box: [440, 20, 60, 50], at: [480, 300], face: 'up',
|
||||||
|
actions: { look: [{ say: 'Il televisore a tubo. È il 1997: niente smartphone, solo il televideo.' }] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'porta', name: 'la porta: si torna nel 2007', box: [70, 30, 70, 230], at: [120, 300], face: 'left', default: 'walk',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'Fuori c\'è la sera del 1997. E parcheggiata lì, la macchina del tempo.' }],
|
||||||
|
walk: RITORNO_2007,
|
||||||
|
open: RITORNO_2007,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
// L'ingresso della scuola, giù per la strada dalla salita di Villa Adami: dal cancello alle porte a vetri.
|
// L'ingresso della scuola, giù per la strada dalla salita di Villa Adami: dal cancello alle porte a vetri.
|
||||||
scuola: {
|
scuola: {
|
||||||
name: 'La scuola',
|
name: 'La scuola',
|
||||||
@@ -3685,6 +3852,18 @@ export const story = {
|
|||||||
{ say: 'Col Betino? Io un perdo mai: un vo mai forte abbastanza da cadere.', who: 'arcu', pose: 'explain' },
|
{ say: 'Col Betino? Io un perdo mai: un vo mai forte abbastanza da cadere.', who: 'arcu', pose: 'explain' },
|
||||||
{ say: 'Come la si fa?', who: 'arcu', pose: 'talk' },
|
{ say: 'Come la si fa?', who: 'arcu', pose: 'talk' },
|
||||||
] },
|
] },
|
||||||
|
// I pokerini da Vannino: l'Arcu fa comparire la macchina del tempo (Andrea, 29 ottobre). [DA ANDREA: le battute dell'Arcu]
|
||||||
|
{ text: 'Bei tempi quando qui si faceva i pokerini...', next: 'end', then: [
|
||||||
|
{ if: 'macchina_trappola', then: [{ say: 'La macchina è sempre lì. La data è già impostata.', who: 'arcu', pose: 'explain' }], else: [
|
||||||
|
{ say: 'I pokerini da Vannino, col Pierino che bluffava anche a briscola.', who: 'arcu', pose: 'laugh' },
|
||||||
|
{ say: 'Ci vuoi tornare? Aspetta.', who: 'arcu', pose: 'explain' },
|
||||||
|
{ set: 'macchina_trappola' },
|
||||||
|
{ say: 'Ma quella è... la macchina del tempo!' },
|
||||||
|
// Andrea, 29 ottobre.
|
||||||
|
{ say: 'Il plutonio l\'ho trovato da Alvarino. Per un pelo stavano arrivando i Libici a prenderglielo tutto.', who: 'arcu', pose: 'talk' },
|
||||||
|
{ say: 'Basta salire e mettere la data.', who: 'arcu', pose: 'explain' },
|
||||||
|
] },
|
||||||
|
] },
|
||||||
{ text: 'Ci si vede dopo, Arcu.', next: 'end' },
|
{ text: 'Ci si vede dopo, Arcu.', next: 'end' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -3760,6 +3939,56 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Al Barrettino del 1997: il pokerino con Vannino e Pierino. [DA ANDREA: le battute]
|
||||||
|
pokerino: {
|
||||||
|
nodes: {
|
||||||
|
start: {
|
||||||
|
options: [
|
||||||
|
{ text: 'C\'è posto per uno?', next: 'livello', then: [
|
||||||
|
{ say: 'La sedia è lì. Invito due, punto cinque. Come la vuoi?', who: 'vannino', pose: 'explain' },
|
||||||
|
] },
|
||||||
|
{ text: 'Vannino, un caffè?', then: [{ say: 'Subito. Ma prima una mano, sennò che ci fai qui?', who: 'vannino', pose: 'talk' }] },
|
||||||
|
{ text: 'Pierino, come va la mano?', then: [{ say: 'Bene. O male. Con me un si sa mai, ed è questo il bello.', who: 'pierino_parigi', pose: 'laugh' }] },
|
||||||
|
{ text: 'Dopo.', next: 'end' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
livello: {
|
||||||
|
options: [
|
||||||
|
{ text: 'Alla bona.', next: 'end', then: [{ say: 'Alla bona: si gioca per il caffè.', who: 'pierino_parigi', pose: 'talk' }, { closeup: 'poker', level: 'bona' }] },
|
||||||
|
{ text: 'Seria.', next: 'end', then: [{ say: 'Seria. Carte, Vannino.', who: 'pierino_parigi', pose: 'cards' }, { closeup: 'poker', level: 'seria' }] },
|
||||||
|
{ text: 'A fassi male.', next: 'end', then: [{ say: 'A fassi male? Allora io bluffo.', who: 'pierino_parigi', pose: 'laugh' }, { closeup: 'poker', level: 'male' }] },
|
||||||
|
{ text: 'No, aspetta.', next: 'start' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Il Taglia al campone delle poste, con la Condor (Andrea, 29 ottobre). [DA ANDREA: battute provvisorie]
|
||||||
|
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: '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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
// Il Chiaru al parcheggio di Cardetole, accanto alla pista col gesso (Andrea, 29 ottobre). [DA ANDREA: battute provvisorie]
|
// Il Chiaru al parcheggio di Cardetole, accanto alla pista col gesso (Andrea, 29 ottobre). [DA ANDREA: battute provvisorie]
|
||||||
chiaru: {
|
chiaru: {
|
||||||
nodes: {
|
nodes: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
|
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
|
||||||
export const VERSION = '0.1.52';
|
export const VERSION = '0.1.56';
|
||||||
|
|
||||||
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica,
|
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica,
|
||||||
// mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf).
|
// mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf).
|
||||||
|
|||||||
@@ -37,14 +37,14 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
|
|||||||
#scopa-controls p, #tressette-controls p { max-width: 76em; }
|
#scopa-controls p, #tressette-controls p { max-width: 76em; }
|
||||||
#scopa-next { width: 100%; min-height: 40px; margin-bottom: 8px; background: #38453c; color: #fff0cd; border: 2px solid #897b57; font-size: 9px; cursor: pointer; }
|
#scopa-next { width: 100%; min-height: 40px; margin-bottom: 8px; background: #38453c; color: #fff0cd; border: 2px solid #897b57; font-size: 9px; cursor: pointer; }
|
||||||
.card-hand + .card-buttons { margin-top: 8px; }
|
.card-hand + .card-buttons { margin-top: 8px; }
|
||||||
#briscola-controls { background: #242b28; padding: 10px 14px; border-top: 2px solid #897b57; font-size: 11px; line-height: 1.7; }
|
#poker-controls, #briscola-controls { background: #242b28; padding: 10px 14px; border-top: 2px solid #897b57; font-size: 11px; line-height: 1.7; }
|
||||||
#briscola-status { color: #f3dcac; min-height: 2em; margin: 0 0 10px; }
|
#briscola-status { color: #f3dcac; min-height: 2em; margin: 0 0 10px; }
|
||||||
.card-hand { display: flex; gap: 8px; }
|
.card-hand { display: flex; gap: 8px; }
|
||||||
.card-hand button { flex: 1; min-width: 0; min-height: 48px; padding: 8px; background: #38453c; color: #fff0cd; border: 2px solid #897b57; font-size: 10px; line-height: 1.7; cursor: pointer; }
|
.card-hand button { flex: 1; min-width: 0; min-height: 48px; padding: 8px; background: #38453c; color: #fff0cd; border: 2px solid #897b57; font-size: 10px; line-height: 1.7; cursor: pointer; }
|
||||||
.card-hand button[aria-pressed="true"] { border-color: #ffdb76; }
|
.card-hand button[aria-pressed="true"] { border-color: #ffdb76; }
|
||||||
.card-hand button:disabled { opacity: .5; cursor: default; }
|
.card-hand button:disabled { opacity: .5; cursor: default; }
|
||||||
#briscola-controls summary { cursor: pointer; padding: 10px 0; }
|
#poker-controls summary, #briscola-controls summary { cursor: pointer; padding: 10px 0; }
|
||||||
#briscola-controls p { max-width: 76em; }
|
#poker-controls p, #briscola-controls p { max-width: 76em; }
|
||||||
#bocce-controls { background: #242b28; padding: 10px 14px; border-top: 2px solid #897b57; font-size: 11px; }
|
#bocce-controls { background: #242b28; padding: 10px 14px; border-top: 2px solid #897b57; font-size: 11px; }
|
||||||
#bocce-status { color: #f3dcac; margin: 0 0 10px; line-height: 1.5; min-height: 1.5em; }
|
#bocce-status { color: #f3dcac; margin: 0 0 10px; line-height: 1.5; min-height: 1.5em; }
|
||||||
.bocce-adjust { display: flex; align-items: center; justify-content: center; gap: 12px; }
|
.bocce-adjust { display: flex; align-items: center; justify-content: center; gap: 12px; }
|
||||||
@@ -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. */
|
/* La guida semplice del GP: il Gas si tiene premuto col pollice, più alto. */
|
||||||
#bocce-throw.hold { min-height: 72px; font-size: 14px; }
|
#bocce-throw.hold { min-height: 72px; font-size: 14px; }
|
||||||
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
|
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
|
||||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-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 summary, #biliardo-help summary, #basket-help summary, #pesca-help summary, #gara-help summary, #rinquarto-help summary, #micro-help summary, #cerbottana-help, #condor-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 p, #biliardo-help p, #basket-help p, #pesca-help p, #gara-help p, #rinquarto-help p, #micro-help p, #cerbottana-help, #condor-help p { max-width: 76em; line-height: 1.7; }
|
||||||
@media (max-width: 600px), (orientation: landscape) and (max-height: 520px) {
|
@media (max-width: 600px), (orientation: landscape) and (max-height: 520px) {
|
||||||
#bocce-controls { padding: 8px; font-size: 10px; overflow-y: auto; min-height: 0; }
|
#bocce-controls { padding: 8px; font-size: 10px; overflow-y: auto; min-height: 0; }
|
||||||
.bocce-adjust { flex-wrap: wrap; gap: 6px; }
|
.bocce-adjust { flex-wrap: wrap; gap: 6px; }
|
||||||
.bocce-adjust label { order: -1; flex-basis: 100%; max-width: none; }
|
.bocce-adjust label { order: -1; flex-basis: 100%; max-width: none; }
|
||||||
.bocce-adjust button { flex: 1; font-size: 9px; padding: 6px; }
|
.bocce-adjust button { flex: 1; font-size: 9px; padding: 6px; }
|
||||||
#bocce-throw { flex-basis: 100%; }
|
#bocce-throw { flex-basis: 100%; }
|
||||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help { font-size: 9px; }
|
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help { font-size: 9px; }
|
||||||
}
|
}
|
||||||
.effects-control { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; font-size: clamp(9px, 1.2vw, 13px); }
|
.effects-control { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; font-size: clamp(9px, 1.2vw, 13px); }
|
||||||
#effects-volume { width: 130px; accent-color: var(--accent); }
|
#effects-volume { width: 130px; accent-color: var(--accent); }
|
||||||
@@ -214,8 +214,9 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
|
|||||||
/* Mappa: la pianta del paese a gradini, i posti visti come cartellini, le strade come linee. */
|
/* Mappa: la pianta del paese a gradini, i posti visti come cartellini, le strade come linee. */
|
||||||
/* In alto a sinistra: Pausa e Mappa. */
|
/* In alto a sinistra: Pausa e Mappa. */
|
||||||
#corner { position: absolute; top: 6px; left: 6px; z-index: 2; display: flex; gap: 6px; }
|
#corner { position: absolute; top: 6px; left: 6px; z-index: 2; display: flex; gap: 6px; }
|
||||||
#map-button, #pause-button, #notebook-button, #spots-button { font: bold clamp(8.64px, 1.15vw, 11.52px) var(--font); padding: .35em .8em;
|
#map-button, #pause-button, #notebook-button, #spots-button, #back-button { font: bold clamp(8.64px, 1.15vw, 11.52px) var(--font); padding: .35em .8em;
|
||||||
color: var(--accent); background: rgba(27, 20, 32, .62); border: 0; border-radius: 18px; cursor: pointer; }
|
color: var(--accent); background: rgba(27, 20, 32, .62); border: 0; border-radius: 18px; cursor: pointer; }
|
||||||
|
#back-button { color: #1b1420; background: #9fd0ff; }
|
||||||
#pause { z-index: 3; background: rgba(15, 10, 20, .9); }
|
#pause { z-index: 3; background: rgba(15, 10, 20, .9); }
|
||||||
/* Il player dei video «Come si gioca»: sopra a tutto, il video grande quanto può. */
|
/* Il player dei video «Come si gioca»: sopra a tutto, il video grande quanto può. */
|
||||||
#tutorial { position: fixed; inset: 0; z-index: 20; background: rgba(10, 8, 14, .97); padding: 8px; }
|
#tutorial { position: fixed; inset: 0; z-index: 20; background: rgba(10, 8, 14, .97); padding: 8px; }
|
||||||
@@ -325,7 +326,7 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
|
|||||||
#app.presenta { justify-content: center; }
|
#app.presenta { justify-content: center; }
|
||||||
#app.presenta #stage.biliardo { width: min(calc((100dvh - 44px) * 16 / 9), 100vw); }
|
#app.presenta #stage.biliardo { width: min(calc((100dvh - 44px) * 16 / 9), 100vw); }
|
||||||
#stage.briscola { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); }
|
#stage.briscola { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); }
|
||||||
#briscola-controls, #tressette-controls, #scopa-controls { overflow-y: auto; min-height: 0; padding: 8px; font-size: 9px; }
|
#briscola-controls, #tressette-controls, #scopa-controls, #poker-controls { overflow-y: auto; min-height: 0; padding: 8px; font-size: 9px; }
|
||||||
/* Nella colonna stretta accanto al tavolo: carte e segni su due colonne, parole che vanno a capo. */
|
/* Nella colonna stretta accanto al tavolo: carte e segni su due colonne, parole che vanno a capo. */
|
||||||
.card-hand-many button, .card-buttons button { flex-basis: 40%; overflow-wrap: anywhere; }
|
.card-hand-many button, .card-buttons button { flex-basis: 40%; overflow-wrap: anywhere; }
|
||||||
.card-hand { flex-direction: column; gap: 4px; }
|
.card-hand { flex-direction: column; gap: 4px; }
|
||||||
|
|||||||
@@ -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(); }
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as m from '../src/engine/condor.js';
|
||||||
|
import { drawCondor } from '../src/engine/condor-render.js';
|
||||||
|
import { Game, MINIGAMES } from '../src/engine/game.js';
|
||||||
|
import { story } from '../src/story/story.js';
|
||||||
|
import { settle } from './helpers.mjs';
|
||||||
|
|
||||||
|
// Ferma il respiro (il mirino va dove si punta), spara e aspetta che il pallino arrivi.
|
||||||
|
function shot(c, x, y) {
|
||||||
|
c.skill = { ...c.skill, sway: 0 }; c.aim = { x, y };
|
||||||
|
assert.ok(m.shoot(c));
|
||||||
|
for (let i = 0; i < 200 && c.step === 'flying'; i++) m.update(c, 1 / 60);
|
||||||
|
return c.line;
|
||||||
|
}
|
||||||
|
// Aspetta la ricarica.
|
||||||
|
const reload = c => { for (let i = 0; i < 120 && !m.canThrow(c) && c.step !== 'result'; i++) m.update(c, 1 / 60); };
|
||||||
|
// Mira sopra quanto cala il pallino a quella distanza.
|
||||||
|
const over = (x, y) => [x, y - m.DROP * m.far(x) ** 2];
|
||||||
|
|
||||||
|
test('il pallino cala con la distanza: al cartone poco, alla porta di più; si mira sopra', () => {
|
||||||
|
const c = m.newCondor(1, { level: 'seria' });
|
||||||
|
assert.ok(m.far(m.CARTONE.x) < m.far(560));
|
||||||
|
assert.match(shot(c, ...over(m.CARTONE.x, m.CARTONE.y)), /10 punti/);
|
||||||
|
assert.equal(c.holes.length, 1); reload(c);
|
||||||
|
const b = c.bottles[1];
|
||||||
|
assert.match(shot(c, b.x, m.BAR - 6), /prato/, 'mirando dritto alla bottiglietta in fondo si va sotto');
|
||||||
|
reload(c);
|
||||||
|
assert.match(shot(c, ...over(b.x, m.BAR - 6)), /Bottiglietta giù/);
|
||||||
|
assert.ok(b.down); reload(c);
|
||||||
|
const k = c.cans[0];
|
||||||
|
assert.match(shot(c, ...over(k.x, m.BENCH - 6)), /Lattina giù/);
|
||||||
|
assert.equal(c.points, 10 + m.BOTTLE.points + m.CAN.points);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il palloncino scoppia; la lattina in aria del Taglia c\'è solo a seria e a fassi male', () => {
|
||||||
|
const c = m.newCondor(2, { level: 'bona' });
|
||||||
|
const at = m.balloon({ visual: { time: c.visual.time + m.flight(m.BALLOON.x) } });
|
||||||
|
assert.match(shot(c, ...over(at.x, at.y)), /palloncino/);
|
||||||
|
assert.ok(c.balloon.popped);
|
||||||
|
for (let i = 0; i < 60 * 20; i++) m.update(c, 1 / 60);
|
||||||
|
assert.equal(c.volo, null, 'alla bona il Taglia non tira niente in aria');
|
||||||
|
const s = m.newCondor(3, { level: 'seria' });
|
||||||
|
while (!s.volo) m.update(s, 1 / 60);
|
||||||
|
// Si mira dove sarà la lattina quando arriva il pallino (lì, vicina, non cala).
|
||||||
|
while (s.volo.t < 0.6) m.update(s, 1 / 60);
|
||||||
|
s.skill = { ...s.skill, sway: 0 };
|
||||||
|
let v = m.voloAt(s, s.volo.t + 0.3);
|
||||||
|
for (let i = 0; i < 3; i++) v = m.voloAt(s, s.volo.t + m.flight(v.x));
|
||||||
|
s.aim = { x: v.x, y: v.y }; m.shoot(s);
|
||||||
|
for (let i = 0; i < 200 && s.step === 'flying'; i++) m.update(s, 1 / 60);
|
||||||
|
assert.match(s.line, /al volo/); assert.equal(s.points, m.VOLO.points);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('trattenere il fiato ferma il mirino; troppo e il colpo parte da solo; il tocco secco mira soltanto', () => {
|
||||||
|
const c = m.newCondor(4, { level: 'male' });
|
||||||
|
assert.ok(m.aimScreen(c, [300, 150])); assert.deepEqual(c.aim, { x: 300, y: 150 });
|
||||||
|
assert.ok(m.press(c, 1)); m.update(c, 0.05); assert.equal(m.release(c, 1), true); assert.equal(c.step, 'aim');
|
||||||
|
assert.equal(m.steady(c), 1);
|
||||||
|
m.press(c, 1);
|
||||||
|
for (let i = 0; i < 60 * m.STILL; i++) m.update(c, 1 / 60);
|
||||||
|
assert.ok(m.steady(c) < 0.35, 'fermo');
|
||||||
|
assert.ok(m.release(c, 1)); assert.equal(c.step, 'flying'); assert.equal(c.left, m.PELLETS - 1);
|
||||||
|
reload(c);
|
||||||
|
m.press(c, 2);
|
||||||
|
for (let i = 0; i < 60 * (m.BREATH + m.GASP + 0.2); i++) m.update(c, 1 / 60);
|
||||||
|
assert.equal(c.holding, false); assert.equal(c.left, m.PELLETS - 2, 'senza fiato il colpo è partito');
|
||||||
|
assert.ok(m.key(c, 'ArrowUp', true));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('finiti i pallini si confronta col Taglia: vince chi ha di più, pari se uguali', () => {
|
||||||
|
for (const [points, winner] of [[50, 'player'], [null, 'draw'], [0, 'cpu']]) {
|
||||||
|
const c = m.newCondor(5, { level: 'seria' });
|
||||||
|
c.left = 1; c.points = points ?? c.taglia;
|
||||||
|
shot(c, 600, 340); reload(c);
|
||||||
|
assert.equal(c.step, 'result'); assert.equal(c.result.winner, winner);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il disegno della Condor 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.newCondor(6, { level: 'male' }), bg = {};
|
||||||
|
while (!c.volo) m.update(c, 1 / 60);
|
||||||
|
const draw = () => { const before = structuredClone(c); drawCondor(ctx, c, story.closeups.condor, 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 CONDOR'));
|
||||||
|
c.step = 'result'; c.result = { winner: 'player', points: 50, taglia: 44 }; calls.length = 0; draw();
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'BATTUTO IL TAGLIA!'));
|
||||||
|
assert.ok(MINIGAMES.condor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dal Taglia, al campone delle poste: la gara, il livello e la Condor', () => {
|
||||||
|
const g = new Game(story); g.go('giardini_poste'); settle(g);
|
||||||
|
const h = g.hotspot('taglia');
|
||||||
|
assert.equal(g.hotspotAt([h.box[0] + h.box[2] / 2, h.box[1] + h.box[3] / 2]).id, 'taglia');
|
||||||
|
g.act('talk', 'taglia'); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text.startsWith('Taglia! Si fa una gara'))); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g);
|
||||||
|
assert.equal(g.closeup?.id, 'condor'); assert.equal(g.closeup.level, 'male');
|
||||||
|
g.closeup.step = 'result'; g.closeup.result = { winner: 'player', points: 50, taglia: 44 };
|
||||||
|
g.closeupClose(); settle(g);
|
||||||
|
assert.ok(g.state.flags.condor_vinta);
|
||||||
|
});
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import * as C from '../src/engine/corsa.js';
|
||||||
|
import { story } from '../src/story/story.js';
|
||||||
|
|
||||||
|
const run = (def, until) => { const c = C.newCorsa('x'); while (c.t < until) C.updateCorsa(c, def, 1 / 60); return c; };
|
||||||
|
|
||||||
|
test('la partenza: accelera giù per la strada, il fulmine la prende e sparisce; restano le sgommate', () => {
|
||||||
|
const def = story.closeups.partenza;
|
||||||
|
assert.equal(def.mode, 'parti');
|
||||||
|
for (const src of Object.values(def.frames)) assert.ok(existsSync(src), src);
|
||||||
|
const at = t => C.progress({ t }, def);
|
||||||
|
assert.equal(at(0), 0);
|
||||||
|
assert.ok(at(1.2) > 0 && at(1.6) - at(1.2) > at(1.2) - at(0.8), 'accelera');
|
||||||
|
assert.ok(C.carShown({ t: 2 }, def) && !C.carShown({ t: C.PARTI.bolt + 0.2 }, def));
|
||||||
|
const c = run(def, C.PARTI.bolt + 0.1);
|
||||||
|
assert.ok(c.events.some(e => e.kind === 'corsa-tuono') && c.events.some(e => e.kind === 'corsa-fuoco'));
|
||||||
|
assert.ok(!C.updateCorsa(c, def, 0.01));
|
||||||
|
assert.ok(C.updateCorsa(run(def, C.PARTI.end - 0.01), def, 0.05), 'finita');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('l\'arrivo: il fulmine in fondo, poi viene avanti e ci passa accanto, fuori dal quadro', () => {
|
||||||
|
const def = story.closeups.arrivo;
|
||||||
|
const at = t => C.progress({ t }, def);
|
||||||
|
assert.equal(at(0), 1); assert.equal(at(C.ARRIVA.bolt + C.ARRIVA.run + 0.1), 0);
|
||||||
|
assert.ok(!C.carShown({ t: 0.1 }, def) && C.carShown({ t: 1 }, def));
|
||||||
|
assert.ok(!C.carShown({ t: C.ARRIVA.bolt + C.ARRIVA.run + 0.1 }, def), 'passata');
|
||||||
|
assert.ok(at(1.2) < at(0.8));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il disegno della corsa non cambia lo stato e usa lo sfondo e i fotogrammi', () => {
|
||||||
|
const calls = [], ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => calls.push([k, ...args]) });
|
||||||
|
const bg = { width: 640, height: 360 }, frames = { dietro: { width: 220, height: 190 }, lampi: { width: 220, height: 190 }, davanti: { width: 220, height: 190 } };
|
||||||
|
for (const [id, t] of [['partenza', 1.4], ['partenza', 3.5], ['arrivo', 1.2], ['arrivo', 2.5]]) {
|
||||||
|
const c = { ...C.newCorsa(id), t }, before = structuredClone(c);
|
||||||
|
calls.length = 0;
|
||||||
|
C.drawCorsa(ctx, c, story.closeups[id], { bg, frames, tint: null });
|
||||||
|
assert.deepEqual(c, before);
|
||||||
|
assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
|
||||||
|
assert.equal(calls.some(([op, img]) => op === 'drawImage' && Object.values(frames).includes(img)), t < 2.4, 'dopo il fulmine (o passata) la macchina non c\'è');
|
||||||
|
assert.ok(calls.filter(([op]) => op === 'fillRect').length > 20, 'le fiamme');
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as P from '../src/engine/poker.js';
|
||||||
|
import { drawPoker, cardAt, HAND } from '../src/engine/poker-render.js';
|
||||||
|
import { Game, MINIGAMES, TRIP } from '../src/engine/game.js';
|
||||||
|
import { story } from '../src/story/story.js';
|
||||||
|
import { settle } from './helpers.mjs';
|
||||||
|
import { PARTI, ARRIVA } from '../src/engine/corsa.js';
|
||||||
|
|
||||||
|
// I primi piani che vanno da soli (il cruscotto, la partenza, l'arrivo): si lascia scorrere il tempo finché finiscono.
|
||||||
|
function ride(g, seen = []) {
|
||||||
|
for (let t = 0; t < TRIP.end + PARTI.end + ARRIVA.end + 2; t += 0.05) {
|
||||||
|
if (g.closeup && seen.at(-1) !== g.closeup.id) seen.push(g.closeup.id);
|
||||||
|
if (!g.closeup && !g.queue.length && !g.speech) break;
|
||||||
|
if (g.speech) g.skip(); else g.update(0.05);
|
||||||
|
}
|
||||||
|
settle(g);
|
||||||
|
return seen;
|
||||||
|
}
|
||||||
|
|
||||||
|
const card = s => ({ rank: s.slice(0, -1), suit: { c: 'cuori', q: 'quadri', f: 'fiori', p: 'picche' }[s.at(-1)] });
|
||||||
|
const hand = s => s.split(' ').map(card);
|
||||||
|
|
||||||
|
test('i punti del mazzo corto: il colore batte il full, A-7-8-9-10 è la scala più bassa', () => {
|
||||||
|
const order = ['Ac Kf 9p 8q 7c', '7c 7f Kp 9q 8c', '7c 7f 9p 9q Ac', 'Jc Jf Jp 9q 8c', 'Ac 7f 8p 9q 10c', '7c 8f 9p 10q Jc',
|
||||||
|
'Kc Kf Kp 9q 9c', 'Ac Kc 9c 8c 7c', 'Qc Qf Qp Qq 7c', '7c 8c 9c 10c Jc'];
|
||||||
|
const scores = order.map(h => P.evaluate(hand(h)));
|
||||||
|
for (let i = 1; i < scores.length; i++) assert.ok(P.compare(scores[i], scores[i - 1]) > 0, `${order[i]} batte ${order[i - 1]}`);
|
||||||
|
assert.equal(P.handName(hand('Ac Kc 9c 8c 7c')), 'colore');
|
||||||
|
assert.ok(P.compare(P.evaluate(hand('Ac Af 9p 8q 7c')), P.evaluate(hand('Ac Af 9p 8q Kc'))) < 0, 'a parità di coppia contano le carte spaiate');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('una partita intera: le fiches non si perdono, finisce sempre, e chi sa giocare vince più di chi vede sempre', () => {
|
||||||
|
const play = (seed, level, brain) => {
|
||||||
|
const c = P.newPoker(seed, { level });
|
||||||
|
for (let i = 0; i < 20000 && c.step !== 'result'; i++) {
|
||||||
|
if (P.myTurn(c)) P.choose(c, brain(c)); else if (P.canDraw(c)) { for (const k of P.cpuDiscards({ ...c, skill: P.LEVELS.male }, 'andrea')) P.play(c, k); P.choose(c, 'cambia'); }
|
||||||
|
else if (c.step === 'show') P.advance(c); else P.update(c, 1);
|
||||||
|
const sum = P.SEATS.reduce((n, s) => n + c.chips[s], 0) + (c.step === 'show' || c.step === 'result' ? 0 : c.pot);
|
||||||
|
assert.equal(sum, P.START * 3);
|
||||||
|
}
|
||||||
|
assert.equal(c.step, 'result');
|
||||||
|
return c.result.chips.andrea;
|
||||||
|
};
|
||||||
|
const smart = c => { const s = P.strength(c.cards.andrea); return s > 0.5 && P.canRaise(c) ? 'rilancio' : s > 0.15 || !P.toCall(c) ? 'vedo' : 'lascio'; };
|
||||||
|
let a = 0, b = 0;
|
||||||
|
for (let seed = 1; seed <= 60; seed++) { a += play(seed, 'bona', smart); b += play(seed, 'bona', () => 'vedo'); }
|
||||||
|
assert.ok(a > b, `con giudizio ${a}, vedendo sempre ${b}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('i comandi di Andrea: il cambio si sceglie toccando (al massimo 4), le puntate coi pulsanti', () => {
|
||||||
|
const c = P.newPoker(3);
|
||||||
|
while (!P.myTurn(c)) P.update(c, 1);
|
||||||
|
const moves = P.moves(c).map(m => m.id);
|
||||||
|
assert.deepEqual(moves, ['lascio', 'vedo', 'rilancio']);
|
||||||
|
assert.ok(P.choose(c, 'vedo'));
|
||||||
|
while (!P.canDraw(c) && c.step !== 'show') P.update(c, 1);
|
||||||
|
if (P.canDraw(c)) {
|
||||||
|
assert.equal(cardAt(c, [HAND.x + 5, HAND.y + 5]), 0);
|
||||||
|
for (const i of [0, 1, 2, 3, 4]) P.play(c, i);
|
||||||
|
assert.equal(c.selected.size, 4, 'al massimo quattro');
|
||||||
|
assert.ok(P.choose(c, 'cambia'));
|
||||||
|
assert.equal(c.changed.andrea, 4);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il disegno del pokerino non cambia lo stato', () => {
|
||||||
|
const calls = [], ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => calls.push([k, ...args]) });
|
||||||
|
const c = P.newPoker(9);
|
||||||
|
const before = JSON.stringify({ ...c, selected: [...c.selected] });
|
||||||
|
drawPoker(ctx, c);
|
||||||
|
assert.equal(JSON.stringify({ ...c, selected: [...c.selected] }), before);
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && /POKERINO/.test(t)));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la macchina del tempo: l\'Arcu la fa comparire, il viaggio porta al Barrettino del 1997 e la porta riporta nel 2007', () => {
|
||||||
|
assert.ok(MINIGAMES.poker);
|
||||||
|
const g = new Game(story); g.go('via_liguri_magelli'); settle(g);
|
||||||
|
assert.ok(!g.hotspot('macchina_tempo'));
|
||||||
|
g.act('talk', 'arcu'); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text.startsWith('Bei tempi'))); settle(g);
|
||||||
|
assert.ok(g.state.flags.macchina_trappola && g.hotspot('macchina_tempo'));
|
||||||
|
assert.ok(!g.dialogue, 'finito il discorso, si è pronti a partire');
|
||||||
|
g.act('use', 'macchina_tempo'); settle(g);
|
||||||
|
assert.equal(g.closeup?.id, 'viaggio');
|
||||||
|
assert.deepEqual(ride(g), ['viaggio', 'partenza'], 'il cruscotto, poi la partenza da fuori');
|
||||||
|
assert.equal(g.state.scene, 'barrettino_1997');
|
||||||
|
g.act('talk', 'tavola'); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text.startsWith('C\'è posto'))); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g);
|
||||||
|
assert.equal(g.closeup?.id, 'poker'); assert.equal(g.closeup.level, 'male');
|
||||||
|
g.closeup.step = 'result'; g.closeup.result = { winner: 'player', chips: { andrea: 200, vannino: 60, pierino: 40 }, top: ['andrea'] };
|
||||||
|
g.closeupClose(); settle(g);
|
||||||
|
assert.ok(g.state.flags.poker_vinto);
|
||||||
|
assert.ok(g.hotspot('arcu'), 'l\'Arcu è venuto anche lui');
|
||||||
|
g.act('walk', 'porta');
|
||||||
|
assert.deepEqual(ride(g), ['arrivo'], 'al ritorno, l\'arrivo col fulmine');
|
||||||
|
assert.equal(g.state.scene, 'via_liguri_magelli');
|
||||||
|
assert.ok(!g.state.flags.macchina_trappola && !g.hotspot('macchina_tempo'), 'tornati, la macchina sparisce');
|
||||||
|
assert.equal(g.goBack(), false, 'nel 2007 il pulsante non c\'è');
|
||||||
|
// Per rifare il viaggio si richiede all'Arcu; dal 1997 si torna anche col pulsante «Torna al 2007».
|
||||||
|
g.act('talk', 'arcu'); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text.startsWith('Bei tempi'))); settle(g);
|
||||||
|
assert.ok(!g.dialogue, 'finito il discorso, si è pronti a partire');
|
||||||
|
g.act('use', 'macchina_tempo'); settle(g);
|
||||||
|
ride(g);
|
||||||
|
assert.equal(g.state.scene, 'barrettino_1997');
|
||||||
|
assert.equal(g.scene.back.label, 'Torna al 2007');
|
||||||
|
assert.ok(g.goBack()); ride(g);
|
||||||
|
assert.equal(g.state.scene, 'via_liguri_magelli');
|
||||||
|
assert.ok(!g.hotspot('macchina_tempo'));
|
||||||
|
});
|
||||||
@@ -260,6 +260,8 @@ test('il libro dell\'oratorio: il primo piano si sfoglia, l\'ultima pagina è «
|
|||||||
|
|
||||||
test('la mappa: ogni scena ci sta, i posti compaiono solo visti, il viaggio rispetta i dialoghi', () => {
|
test('la mappa: ogni scena ci sta, i posti compaiono solo visti, il viaggio rispetta i dialoghi', () => {
|
||||||
for (const [id, sc] of Object.entries(story.scenes)) {
|
for (const [id, sc] of Object.entries(story.scenes)) {
|
||||||
|
// Le scene del viaggio nel tempo (`viaggio`) non sono sulla mappa: ci si va solo con la macchina.
|
||||||
|
if (sc.viaggio) { assert.ok(!sc.map, `${id}: nel passato la mappa non porta`); continue; }
|
||||||
assert.ok(Array.isArray(sc.map), `${id} non è sulla mappa`);
|
assert.ok(Array.isArray(sc.map), `${id} non è sulla mappa`);
|
||||||
assert.ok(sc.map.every(v => v >= 0 && v <= 100), `${id} fuori dalla pianta`);
|
assert.ok(sc.map.every(v => v >= 0 && v <= 100), `${id} fuori dalla pianta`);
|
||||||
}
|
}
|
||||||
@@ -341,7 +343,14 @@ test('le strade vanno e tornano: niente sensi unici, e ogni posto si raggiunge d
|
|||||||
for (const b of to) assert.ok(exits[b]?.has(a), `da ${a} si va a ${b}, ma da ${b} non si torna a ${a}`);
|
for (const b of to) assert.ok(exits[b]?.has(a), `da ${a} si va a ${b}, ma da ${b} non si torna a ${a}`);
|
||||||
const seen = new Set([story.start.scene]), queue = [story.start.scene];
|
const seen = new Set([story.start.scene]), queue = [story.start.scene];
|
||||||
while (queue.length) for (const b of exits[queue.shift()] ?? []) if (!seen.has(b)) { seen.add(b); queue.push(b); }
|
while (queue.length) for (const b of exits[queue.shift()] ?? []) if (!seen.has(b)) { seen.add(b); queue.push(b); }
|
||||||
for (const id of Object.keys(story.scenes)) assert.ok(seen.has(id), `${id} non si raggiunge`);
|
for (const [id, sc] of Object.entries(story.scenes)) {
|
||||||
|
if (!sc.viaggio) { assert.ok(seen.has(id), `${id} non si raggiunge`); continue; }
|
||||||
|
// Una scena del viaggio nel tempo: ci porta un `go` (la fine del viaggio), e un `go` riporta in una scena vera.
|
||||||
|
const text = JSON.stringify(story);
|
||||||
|
assert.ok(text.includes(`"go":"${id}"`), `${id}: nessun viaggio ci porta`);
|
||||||
|
const back = JSON.stringify(sc.hotspots).match(/"go":"([a-z_0-9]+)"/g)?.map(m => m.slice(6, -1)) ?? [];
|
||||||
|
assert.ok(back.some(b => seen.has(b)), `${id}: non si torna indietro`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('la catena del ghiacciolo: Tosca, cedrata, stecco, gomma, griglia, chiavetta, casetta, torcia', () => {
|
test('la catena del ghiacciolo: Tosca, cedrata, stecco, gomma, griglia, chiavetta, casetta, torcia', () => {
|
||||||
|
|||||||