Compare commits

...
20 Commits
Author SHA1 Message Date
manzolo 58174e68d2 Versione 0.1.54 2026-10-30 00:17:03 +01:00
manzoloandClaude Opus 5.5 ff770af3b5 Voci: «mancano» si legge «màncano» (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-30 00:08:37 +01:00
manzoloandClaude Opus 5.5 41e5fe2f6b Il Taglia (Andrea Tagliaferri, col suo consenso) al campone delle poste, più alto di Andrea, sprite di Codex dalla sua foto: si gareggia con la Condor parlandogli (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-30 00:04:38 +01:00
manzoloandClaude Opus 5.5 9d823375aa La Condor al campone delle poste: la gara col Taglia con la pistola ad aria compressa coi pallini gialli (fiato trattenuto, il pallino che cala con la distanza, cartone, lattine, bottigliette, palloncino e la lattina in aria), sfondo di Codex (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-29 23:57:08 +01:00
manzolo 00f9f15370 Versione 0.1.53 2026-10-29 23:48:11 +01:00
manzoloandClaude Opus 5.5 9fe6e8d0ff Voci: «Libici» si legge «Lìbici» (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-29 23:39:07 +01:00
manzoloandClaude Opus 5.5 0f8081d8f0 Voce dell'Arcu rifatta
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-29 23:36:48 +01:00
manzoloandClaude Opus 5.5 500885ffe0 L'Arcu: «stavano arrivando i Libici» (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-29 23:36:48 +01:00
manzoloandClaude Opus 5.5 259be2c8b0 La macchina del tempo: dopo «Bei tempi…» il discorso con l'Arcu finisce e si è pronti a partire; il plutonio l'ha trovato da Alvarino, prima dei libici; «Imposto» si legge «Impòsto» (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-29 23:34:27 +01:00
manzoloandClaude Opus 5.5 1706592b81 Voci: «bluffo», «bluffare» e «bluffava» si leggono «bleffo», «bleffare», «bleffava» (Pierino e l'Arcu; Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-29 23:30:33 +01:00
manzoloandClaude Opus 5.5 6758e6287e Il 1997: l'Arcu viene anche lui al Barrettino, e il pulsante «Torna al 2007» (come la porta) riporta in Via Liguri Magelli e fa sparire la macchina del tempo; per rifare il viaggio si richiede all'Arcu (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-29 23:25:17 +01:00
manzoloandClaude Opus 5.5 a699115a03 La macchina del tempo: dall'Arcu («Bei tempi quando qui si faceva i pokerini…») al Barrettino del 10 luglio 1997 col viaggio sul cruscotto, Vannino e Pierino Parigi, e il pokerino a cinque carte col cambio; l'Arcu in Via Liguri Magelli più grande (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-29 20:30:05 +01:00
manzolo bb8e6a8885 Versione 0.1.52 2026-10-29 19:11:43 +01:00
manzoloandClaude Opus 5.5 81a1dd1bc3 Video: la partita completa rigirata sul gioco di oggi, il trailer e la versione extended (copioni, prova locale, montaggio --trailer) (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-29 18:47:44 +01:00
manzoloandClaude Opus 5.5 0f1740a7ad Effetti nei posti nuovi: nuvole, uccelli, la Carza che scorre e luccica alla cascatella e sotto il ponte, fronde, ombre, farfalle e libellule, lampioni la sera; i panorami del terrazzo e di Via Cafaggio con le ombre delle nuvole sulle colline (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-29 17:34:37 +01:00
manzolo 3d61fc3954 Versione 0.1.51 2026-10-29 17:30:42 +01:00
manzoloandClaude Opus 5.5 6226de3795 La voce di «Di chi è quell'Audi lì?» (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-29 17:23:59 +01:00
manzoloandClaude Opus 5.5 17824963aa Sotto il ponte delle poste: il viottolo passa sotto l'arco verso gli orti dello zio Mario, alla stazione si sale dalla parte destra del ponte (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-29 17:19:16 +01:00
manzoloandClaude Opus 5.5 75cbe8250f Via della Compagnia: l'arco porta al forno, alla Pieve si scende fra le due case gialle (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-29 17:16:40 +01:00
manzoloandClaude Opus 5.5 6b17288962 Via Liguri Magelli: «Di chi è quell'Audi lì?» (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-29 17:14:59 +01:00
96 changed files with 2427 additions and 38 deletions
+70
View File
@@ -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
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

+70
View File
@@ -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
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

+70
View File
@@ -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
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+231 -7
View File
@@ -683,6 +683,10 @@
"file": "andrea-c716ab2381.mp3",
"sec": 7.95
},
"andrea|Il Taglia. Io sono un metro e ottanta, e lui mi guarda dall'alto. Con la Condor in mano.": {
"file": "andrea-f1663e7892.mp3",
"sec": 8.52
},
"andrea|Il giardino coi cedri e le panchine. All'ombra, d'estate, ci si fa la siesta a turno.": {
"file": "andrea-4fd811d0c7.mp3",
"sec": 6.43
@@ -1603,9 +1607,9 @@
"file": "andrea-a93c65234e.mp3",
"sec": 4.28
},
"andrea|Di chi è quell'Audi nera?": {
"file": "andrea-b9a784137e.mp3",
"sec": 1.63
"andrea|Di chi è quell'Audi lì?": {
"file": "andrea-cc8a61a779.mp3",
"sec": 1.4
},
"arcu|Let it be.": {
"file": "arcu-6ba2f5285a.mp3",
@@ -1631,6 +1635,18 @@
"file": "andrea-a193a43f3e.mp3",
"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.": {
"file": "andrea-3e85200509.mp3",
"sec": 2.93
@@ -1879,6 +1895,54 @@
"file": "andrea-d32e1a2a87.mp3",
"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.": {
"file": "andrea-0a073729fd.mp3",
"sec": 7.3
@@ -2511,6 +2575,10 @@
"file": "andrea-a960f89e7a.mp3",
"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.": {
"file": "andrea-191021ad7c.mp3",
"sec": 1.68
@@ -2539,6 +2607,30 @@
"file": "arcu-fcaee733df.mp3",
"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.": {
"file": "andrea-b35a4d8cc0.mp3",
"sec": 2.15
@@ -2659,6 +2751,98 @@
"file": "andrea-78ad61a67a.mp3",
"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?!?!": {
"file": "andrea-17216997e2.mp3",
"sec": 2.74
@@ -2707,10 +2891,6 @@
"file": "andrea-fc5a08bbd6.mp3",
"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.": {
"file": "chiaru-d31cc223f6.mp3",
"sec": 5.08
@@ -3075,6 +3255,22 @@
"file": "bertovo-ca941bee91.mp3",
"sec": 5.41
},
"vannino|M'hai pelato, ragazzo. Nel 1997 un ci si vedeva da queste parti, con quella faccia.": {
"file": "vannino-e68fb40eba.mp3",
"sec": 7.85
},
"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.": {
"file": "andrea-585cd65f93.mp3",
"sec": 5.2
@@ -3087,6 +3283,34 @@
"file": "andrea-c7dcb11e66.mp3",
"sec": 5.52
},
"andrea|Battuto il Taglia! Con la sua Condor, per giunta.": {
"file": "andrea-3ff6547461.mp3",
"sec": 3.73
},
"taglia|Fortuna. Domani la rivinco.": {
"file": "taglia-39627c5b3a.mp3",
"sec": 2.78
},
"andrea|Il Taglia ha la mano ferma. Io ho il fiatone.": {
"file": "andrea-6eabf3443c.mp3",
"sec": 3.83
},
"taglia|Si torna quando vuoi. I pallini gialli un mancano mai.": {
"file": "taglia-c3c705219c.mp3",
"sec": 4.52
},
"andrea|Pari. Si rifà?": {
"file": "andrea-5625b5f1fd.mp3",
"sec": 2.21
},
"taglia|Si rifà.": {
"file": "taglia-18973374a5.mp3",
"sec": 0.76
},
"andrea|La Condor la rendo al Taglia. Per ora.": {
"file": "andrea-aba12c5241.mp3",
"sec": 3.1
},
"chiaru|Hai vinto te. Ma la mia aveva le ruote piene di gesso, sia chiaro.": {
"file": "chiaru-91a4b23b22.mp3",
"sec": 4.44
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4
View File
@@ -31,10 +31,14 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p
| `provinciale-pignone.png`, `piazzetta-cambray-digny.png`, `via-cipriani.png`, `via-massorondinaio.png`, `mozzete.png`, `ponticino-mozzete.png`, `coldaia.png`, `falegnameria-coldaia.png`, `gabbiano.png` | Codex | screenshot di Google Street View | Google | **no**: da rifare con foto di Andrea |
| `closeups/micro-parcheggio.png`, `closeups/micro/*.png` (macchinine) | Codex, solo testo | nessuna | — | sì |
| `characters/chiaru.png` | Codex | `foto/chiaru.jpg` | Andrea (29 ottobre) | sì (il Chiaru consenziente) |
| `characters/taglia.png` | Codex | `foto/taglia.png` | Andrea (29 ottobre) | sì (il Taglia consenziente) |
| `via-liguri-magelli-muro.png` | Codex, ritocco di `via-liguri-magelli.png` con la Ghiacciomobile nel muro | come `via-liguri-magelli.png` | Street View | **no**: come lo sfondo da cui viene |
| `characters/zio_mario.png` | Codex | `foto/zio-mario.png` | Andrea (29 ottobre) | sì (famiglia di Andrea) |
| `terrazza-cardetole.png` | Codex (dalla foto, poi il terrazzo davanti) | `cardetole-finestra.jpg` | Andrea (29 ottobre) | **da decidere**: come `cardetole.png`, dice dove abita Andrea |
| `closeups/muro-cerbottana.png` | Codex, solo testo (stile dalla piazza) | nessuna | — | sì |
| `closeups/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 |
| `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 |
+17
View File
@@ -18,6 +18,23 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
## Dove siamo arrivati
- **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
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
+28
View File
@@ -22,6 +22,7 @@
<button id="map-button" hidden title="Mappa (P)">Mappa</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="back-button" hidden></button>
</div>
<div id="sound">
<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>
</details>
</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">
<p id="briscola-status" role="status"></p>
<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
non conta, e non pesa). Frecce ← →: distanza del lancio; Esc esce.</p>
</details>
<details id="condor-help" hidden><summary>Come si gioca</summary>
<p>La Condor, la pistola ad aria compressa coi pallini gialli, al campone delle poste: dodici pallini, e il Taglia ha
già tirato. Il suo punteggio è in alto: per vincere bisogna fare di più. Il cartone sull'albero vale 10, 5 o 2 punti
secondo il cerchio, ogni lattina sulla panchina 3, ogni bottiglietta sulla traversa della porta 5, il palloncino
legato all'altalena 6, la lattina che il Taglia tira in aria (Seria e A fassi male) 8.</p>
<p>Muovi il mouse o tocca il campo per mirare: il mirino ondeggia col respiro (di più salendo di livello). Tieni
premuto per trattenere il fiato: il mirino si ferma (diventa verde); lascia per sparare. Se lo trattieni troppo il
fiato finisce, il mirino trema e il colpo parte da solo. Il pallino cala con la distanza: alla porta in fondo si
mira un po' più sopra; alla bona il puntino giallo segna dove arriva. Il palloncino e la lattina in aria si
muovono: si mira dove saranno. Si può anche premere Spara, senza trattenere il fiato. Frecce per spostare il mirino.</p>
</details>
<details id="cerbottana-help" hidden><summary>Come si gioca</summary>
<p>La cerbottana, con lo stucco verde di Pierin di Frilli: dodici pallini contro il muro della piazza. Il bersaglio
di gesso vale 10, 5 o 2 punti secondo il cerchio, ogni lattina sul muretto 3, il barattolo appeso che dondola (Seria
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "leggenda-del-regolo",
"version": "0.1.50",
"version": "0.1.54",
"private": true,
"type": "module",
"scripts": {
@@ -10,6 +10,7 @@
"browser:micro": "node tests/micro.browser.mjs",
"browser:gara": "node tests/gara.browser.mjs",
"browser:cerbottana": "node tests/cerbottana.browser.mjs",
"browser:condor": "node tests/condor.browser.mjs",
"browser:tressette": "node tests/tressette.browser.mjs",
"browser:scopa": "node tests/scopa.browser.mjs",
"browser:briscola": "node tests/briscola.browser.mjs",
+118
View File
@@ -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();
}
+195
View File
@@ -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.'}`;
}
+34
View File
@@ -12,6 +12,8 @@ import * as scopa from './scopa.js';
import * as gara from './gara.js';
import * as micro from './micro.js';
import * as cerbottana from './cerbottana.js';
import * 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.
export const MINIGAMES = {
@@ -23,7 +25,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 },
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 },
poker: { create: (seed, level) => poker.newPoker(seed, { level }), update: poker.update, play: poker.play },
cerbottana: { create: (seed, level) => cerbottana.newCerbottana(seed, { level }), update: cerbottana.update, press: cerbottana.press, release: cerbottana.release, cancel: cerbottana.cancel, aim: cerbottana.aimScreen },
condor: { create: (seed, level) => condor.newCondor(seed, { level }), update: condor.update, press: condor.press, release: condor.release, cancel: condor.cancel, aim: condor.aimScreen },
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 },
};
@@ -51,6 +55,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 →
// l'ultima pagina, poi dopo una pausa il commento. Tempi in secondi.
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).
export function check(cond, state) {
@@ -265,6 +272,17 @@ export class Game {
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() {
if (this.speech) { this.speech = null; return true; }
return false;
@@ -306,6 +324,8 @@ export class Game {
if (c.step === 'done') this.queue.unshift(...def.complete);
return true;
}
// Il viaggio si chiude da solo alla fine (o uscendo prima): si arriva comunque.
if (def?.type === 'viaggio') { this.queue.unshift(...def.complete); return true; }
// I minigiochi: a partita finita (risultato deciso, anche se il conteggio si sta ancora mostrando) la battuta
// di chi ha vinto; uscendo prima, quella di chi lascia.
if (MINIGAMES[def?.type]) {
@@ -336,6 +356,18 @@ export class Game {
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) {
const c = this.closeup, def = this.story.closeups[c.id];
c.t += dt;
@@ -453,6 +485,7 @@ export class Game {
const c = this.closeup;
if (c) {
if (this.story.closeups[c.id].type === 'torch') { this.updateTorch(dt); return; }
if (this.story.closeups[c.id].type === 'viaggio') { this.updateTrip(dt); return; }
const m = this.minigame();
if (m) {
m.update(c, dt);
@@ -494,6 +527,7 @@ export class Game {
else if (e.closeup) {
const type = this.story.closeups[e.closeup].type;
this.closeup = type === 'torch' ? { id: e.closeup, step: 'search', t: 0, age: 0, found: 0, dwell: 0, beam: [320, 180] }
: type === 'viaggio' ? { id: e.closeup, t: 0, age: 0, events: [], typed: 0 }
// `level`: «Alla bona», «Seria» o «A fassi male», scelto nel dialogo con l'avversario.
// `decks`: il mazzo da usare quando il suo flag è acceso (briscola: le napoletane al posto delle toscane).
: MINIGAMES[type] ? { id: e.closeup, ...MINIGAMES[type].create(Math.floor(Math.random() * 4294967296), e.level,
+89
View File
@@ -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();
}
+239
View File
@@ -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();
}
+14
View File
@@ -11,6 +11,9 @@ import { drawPesca } from './pesca-render.js';
import { drawGara } from './gara-render.js';
import { drawMicro } from './micro-render.js';
import { drawCerbottana } from './cerbottana-render.js';
import { drawCondor } from './condor-render.js';
import { drawPoker } from './poker-render.js';
import { drawViaggio } from './viaggio.js';
import { drawBriscola } from './briscola-render.js';
import { drawTressette } from './tressette-render.js';
import { drawScopa } from './scopa-render.js';
@@ -678,6 +681,11 @@ function drawCloseup(ctx, game, time) {
if (def.type === 'briscola') { drawBriscola(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 === 'viaggio') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawViaggio(ctx, c, def, backgrounds.get(def.picture));
return;
}
if (def.type === 'torch') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawTorch(ctx, c, def, backgrounds.get(def.picture));
@@ -688,11 +696,17 @@ function drawCloseup(ctx, game, time) {
drawBocce(ctx, c, def, backgrounds.get(def.picture), time);
return;
}
if (def.type === 'poker') { drawPoker(ctx, c); return; }
if (def.type === 'cerbottana') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawCerbottana(ctx, c, def, backgrounds.get(def.picture));
return;
}
if (def.type === 'condor') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawCondor(ctx, c, def, backgrounds.get(def.picture));
return;
}
if (def.type === 'micro') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawMicro(ctx, c, def, backgrounds.get(def.picture), src => { if (!backgrounds.has(src)) loadBackground(src); return backgrounds.get(src); });
+5
View File
@@ -140,6 +140,11 @@ export class Soundscape {
else if (kind === 'cerbottana-pff') this.noiseSound(0.12, 2200, 0.05 + strength * 0.05, { pan });
else if (kind === 'cerbottana-muro') { this.tone(180, 90, 0.06, 0.05, 0, pan); this.noiseSound(0.05, 600, 0.04, { pan }); }
else if (kind === 'cerbottana-latta') { this.tone(2400, 1600, 0.08, 0.05 + strength * 0.04, 0, pan); this.tone(3400, 2600, 0.05, 0.03, 0.03, pan); }
// La Condor: lo scatto della molla, il palloncino che scoppia, la bottiglietta, il Taglia che tira la lattina.
else if (kind === 'condor-pam') { this.noiseSound(0.06, 3200, 0.07, { pan }); this.tone(520, 220, 0.05, 0.05, 0, pan); }
else if (kind === 'condor-palloncino') { this.noiseSound(0.12, 1800, 0.14, { pan }); this.tone(140, 60, 0.08, 0.08, 0, pan); }
else if (kind === 'condor-plastica') { this.tone(1300, 900, 0.06, 0.05, 0, pan); this.noiseSound(0.05, 2600, 0.03, { pan }); }
else if (kind === 'condor-lancio') this.noiseSound(0.2, 700, 0.03, { pan });
else if (kind === 'cerbottana-tosse') { this.noiseSound(0.18, 900, 0.08, { pan }); this.tone(260, 140, 0.12, 0.06, 0.25, pan); this.tone(240, 120, 0.1, 0.05, 0.3, pan); }
// Le macchinine: il dito che spinge, le ruote sull'asfalto, l'urto di plastica, fuori pista, il traguardo.
else if (kind === 'micro-spinta') { this.tone(1200, 500, 0.04, 0.04 + strength * 0.04, 0, pan); this.noiseSound(0.04, 2600, 0.04, { pan }); }
+51
View File
@@ -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); }
}
+37 -8
View File
@@ -13,6 +13,9 @@ import * as biliardo from './engine/biliardo.js';
import * as rinquarto from './engine/rinquarto.js';
import * as micro from './engine/micro.js';
import * as cerbottana from './engine/cerbottana.js';
import * as condor from './engine/condor.js';
import * as poker from './engine/poker.js';
import { cardAt as pokerAt } from './engine/poker-render.js';
import * as basket from './engine/basket.js';
import * as pesca from './engine/pesca.js';
import * as gara from './engine/gara.js';
@@ -311,6 +314,7 @@ function drawRoads() {
}
addEventListener('resize', () => { if (!$('map').hidden) drawRoads(); });
$('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).
const SPOTS_TIME = 3500;
@@ -511,7 +515,7 @@ canvas.addEventListener('pointerdown', e => {
}
if (press?.closeup) return;
const aimed = game.closeupAim(scenePoint(e));
if (['biliardo', 'rinquarto', 'basket', 'micro', 'cerbottana'].includes(story.closeups[game.closeup.id].type)) {
if (['biliardo', 'rinquarto', 'basket', 'micro', 'cerbottana', 'condor'].includes(story.closeups[game.closeup.id].type)) {
if (!aimed && !game.speech) return; // dove non si può mirare (dietro Andrea) non si carica nemmeno
if (aimed) game.closeup.aimLocked = true; // il clic fissa la mira
}
@@ -583,7 +587,7 @@ canvas.addEventListener('pointermove', e => {
if (press?.gara === e.pointerId) gara.aimScreen(game.closeup, scenePoint(e), e.pointerId);
return;
}
const aiming = game.closeup && ['biliardo', 'rinquarto', 'bocce', 'basket', 'pesca', 'micro', 'cerbottana'].includes(story.closeups[game.closeup.id].type);
const aiming = game.closeup && ['biliardo', 'rinquarto', 'bocce', 'basket', 'pesca', 'micro', 'cerbottana', 'condor'].includes(story.closeups[game.closeup.id].type);
if (aiming) { if (e.pointerType === 'mouse' && !press && !game.closeup.aimLocked) game.closeupAim(scenePoint(e)); }
else if (e.pointerType === 'mouse' || press?.closeup) game.closeupAim(scenePoint(e));
});
@@ -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.' },
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.' },
poker: { ...poker, back: 'Torna al Barrettino', label: 'Il pokerino: tocca le carte da cambiare, poi Cambia; nei giri di puntate Lascio, Vedo o Parola, Apro o Rilancio. Dopo la mostra un tocco va avanti.' },
cerbottana: { ...cerbottana, back: 'Torna in piazza', label: 'La cerbottana: muovi il mouse o tocca il muro per mirare, tieni premuto per prendere fiato e lascia per sparare; oppure regola il fiato e premi Spara. Frecce per spostare il mirino.' },
condor: { ...condor, back: 'Torna al campone', label: 'La Condor: muovi il mouse o tocca il campo per mirare, tieni premuto per trattenere il fiato e lascia per sparare; oppure premi Spara. Frecce per spostare il mirino.' },
micro: { ...micro, back: 'Torna dal Chiaru', label: 'La pista col gesso: muovi il mouse o tocca il parcheggio per mirare, tieni premuto per caricare la spinta e lascia; oppure regola la forza e premi Spingi. Frecce o pulsanti Mira per girare.' },
basket: { ...basket, back: 'Torna in strada', label: 'Fai due tiri: muovi il mouse o tocca per mirare, tieni premuto per caricare la forza, lascia per tirare.' },
// La gara: niente mira né forza, il pulsante alterna gas e freno (chi non può tenere premuto).
@@ -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'); });
$('bocce-throw').addEventListener('contextmenu', e => { if (holdGas(miniState())) e.preventDefault(); });
// 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++) {
const button = $('scopa-card-' + i);
button.addEventListener('click', () => game?.closeupPlay(i));
@@ -946,7 +957,7 @@ function updateUi() {
updateChoices();
updateSpeech();
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).
const presenta = !!mg && story.closeups[c.id].type === 'gara' && c.step === 'intro';
$('app').classList.toggle('presenta', presenta);
@@ -956,6 +967,20 @@ function updateUi() {
setProp($('briscola-controls'), 'inert', !running);
setProp($('scopa-controls'), 'hidden', def?.type !== 'scopa');
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') {
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
const line = scopa.status(c, def.names);
@@ -1015,7 +1040,7 @@ function updateUi() {
}
}
if (mg && !cards) {
setAttr($('bocce-controls'), 'aria-label', { rinquarto: 'Comandi dei rinquarti', biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca', gara: 'Comandi della gara', micro: 'Comandi delle macchinine', cerbottana: 'Comandi della cerbottana' }[def.type] ?? 'Comandi delle bocce');
setAttr($('bocce-controls'), 'aria-label', { rinquarto: 'Comandi dei rinquarti', biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca', gara: 'Comandi della gara', micro: 'Comandi delle macchinine', cerbottana: 'Comandi della cerbottana', condor: 'Comandi della Condor' }[def.type] ?? 'Comandi delle bocce');
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
const line = mg.status(c, def.names);
if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line;
@@ -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
// usa solo il gas: niente sensibilità, sterzo, freno né riquadro dello swipe.
const easy = def.type === 'gara' && gara.simple(c);
setProp(powerLabel, 'hidden', def.type === 'rinquarto' || easy);
setProp(powerLabel, 'hidden', def.type === 'rinquarto' || def.type === 'condor' || easy);
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy);
const powerName = def.type === 'gara' ? 'Sensibilità ' : def.type === 'cerbottana' ? 'Fiato ' : 'Forza ';
if (powerLabel.firstChild.nodeValue !== powerName) powerLabel.firstChild.nodeValue = powerName;
@@ -1050,14 +1075,18 @@ function updateUi() {
setProp($('rinquarto-help'), 'hidden', def.type !== 'rinquarto');
setProp($('micro-help'), 'hidden', def.type !== 'micro');
setProp($('cerbottana-help'), 'hidden', def.type !== 'cerbottana');
setProp($('condor-help'), 'hidden', def.type !== 'condor');
setProp($('basket-help'), 'hidden', def.type !== 'basket');
setProp($('pesca-help'), 'hidden', def.type !== 'pesca');
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) 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.
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($('spots-button'), 'hidden', !running || !!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('bocce', !!mg);
// Biliardo e GP di Fortezza: audio, pausa e uscita fuori dal quadro (al GP la pista passa negli angoli).
$('stage').classList.toggle('biliardo', !!c && ['biliardo', 'rinquarto', 'gara', 'micro', 'cerbottana'].includes(story.closeups[c.id].type));
$('stage').classList.toggle('biliardo', !!c && ['biliardo', 'rinquarto', 'gara', 'micro', 'cerbottana', 'condor'].includes(story.closeups[c.id].type));
$('stage').classList.toggle('briscola', !!cards);
setAttr(canvas, 'aria-label', torch ? 'Fessura: sposta la luce con mouse, dito o frecce. Invio cerca il prossimo dettaglio; Esc esce.' : mg ? mg.label : "La scena dell'avventura");
// La pagina che comincia a girare fa il suo fruscio.
+113
View File
@@ -151,4 +151,117 @@ export const ambience = {
{ type: 'cloudshadow', phase: day, area: [60, 220, 520, 140], offset: 23000,
clip: [[0, 215], [400, 195], [640, 260], [640, 360], [0, 360]] },
],
// I posti nuovi del 29 ottobre (foto di Andrea): nuvole, uccelli, la Carza che scorre e luccica, fronde, ombre.
via_compagnia: [
{ type: 'clouds', area: [130, 0, 340, 80], count: 2 },
{ type: 'lamps', phase: 'sera', points: [[115, 104]], glow: 10 },
bird([250, 300, 70, 16], 9000),
],
via_cafaggio: [
// Il panorama: Monte Cacioli e le colline, con le ombre delle nuvole che ci scorrono sopra.
{ type: 'clouds', area: [0, 0, 640, 80], count: 4, big: true },
{ type: 'cloudshadow', phase: day, area: [0, 80, 640, 90], offset: 6000, clip: rect([0, 80, 640, 90]) },
{ type: 'cloudshadow', phase: day, area: [0, 80, 640, 90], offset: 32000, clip: rect([0, 80, 640, 90]) },
{ type: 'birds', phase: day, area: [80, 20, 420, 70], count: 4, every: 21000, duration: 6000, offset: 3000 },
{ type: 'sway', rects: [[560, 0, 80, 90]], amount: 2 },
butterfly([40, 300, 160, 40], 15000),
],
via_cafaggio_fondo: [
{ type: 'clouds', area: [0, 10, 380, 80], count: 3, big: true },
{ type: 'birds', phase: day, area: [150, 20, 250, 60], count: 2, every: 26000, duration: 5000, offset: 8000 },
{ type: 'sway', rects: [[390, 0, 150, 160]], amount: 1.5 },
shade([300, 270, 220, 70], 12),
{ type: 'lamps', phase: 'sera', points: [[252, 83], [319, 121]], glow: 9 },
],
via_cafaggio_avanti: [
{ type: 'clouds', area: [150, 10, 400, 80], count: 3, big: true },
{ type: 'sway', rects: [[400, 60, 200, 90]], amount: 1.5 },
shade([180, 250, 260, 90], 12),
bird([120, 300, 80, 16], 14000),
],
rotonda_cafaggio: [
{ type: 'clouds', area: [200, 20, 420, 90], count: 4, big: true },
{ type: 'birds', phase: day, area: [250, 20, 300, 60], count: 3, every: 23000, duration: 5500, offset: 5000 },
{ type: 'sway', rects: [[0, 0, 140, 240], [350, 110, 200, 100], [560, 0, 80, 230]], amount: 2 },
shade([0, 250, 200, 60], 12),
],
provinciale_luna: [
{ type: 'clouds', area: [80, 20, 330, 80], count: 3, big: true },
{ type: 'birds', phase: day, area: [100, 20, 280, 60], count: 3, every: 24000, duration: 5000, offset: 12000 },
{ type: 'sway', rects: [[400, 0, 240, 170]], amount: 2 },
shade([380, 230, 170, 100], 14),
],
passeggiata_carza: [
// La cascatella che si vede dalla Luna: l'acqua che scende luccica, e la Carza scorre verso valle.
{ type: 'ripples', area: [150, 200, 196, 55], count: 14, flow: [-10, 4],
clip: [[150, 242], [210, 226], [290, 204], [346, 200], [346, 212], [300, 224], [225, 246], [160, 256]] },
{ type: 'sparkle', area: [296, 184, 46, 18], count: 5 },
{ type: 'sparkle', area: [170, 222, 120, 26], count: 3 },
{ type: 'clouds', area: [380, 40, 140, 50], count: 2 },
{ type: 'sway', rects: [[0, 0, 180, 250], [300, 0, 100, 200], [440, 60, 100, 120]], amount: 2 },
shade([200, 240, 300, 60], 14),
{ type: 'insect', kind: 'dragonfly', phase: day, area: [180, 214, 110, 30], every: 33000, duration: 9000, offset: 4000 },
butterfly([380, 228, 120, 40], 17000),
{ type: 'lamps', phase: 'sera', points: [[519, 77]], glow: 10 },
],
passeggiata_carza_avanti: [
{ type: 'ripples', area: [480, 236, 160, 70], count: 10, flow: [10, 4],
clip: [[480, 240], [560, 250], [640, 272], [640, 318], [560, 292], [500, 270]] },
{ type: 'sparkle', area: [520, 252, 100, 40], count: 3 },
{ type: 'clouds', area: [0, 0, 640, 90], count: 4, big: true },
{ type: 'birds', phase: day, area: [150, 20, 350, 60], count: 3, every: 22000, duration: 5500, offset: 7000 },
{ type: 'sway', rects: [[250, 10, 110, 170], [360, 40, 90, 120], [500, 60, 140, 120]], amount: 2 },
butterfly([40, 200, 160, 50], 9000),
],
passeggiata_poste: [
{ type: 'ripples', area: [470, 270, 170, 88], count: 12, flow: [10, 3],
clip: [[470, 278], [560, 272], [640, 300], [640, 358], [560, 358], [480, 302]] },
{ type: 'sparkle', area: [500, 290, 120, 50], count: 3 },
{ type: 'clouds', area: [380, 0, 260, 90], count: 3, big: true },
{ type: 'sway', rects: [[150, 50, 170, 150], [400, 120, 240, 90]], amount: 2 },
shade([180, 250, 130, 60], 10),
{ type: 'lamps', phase: 'sera', points: [[337, 144]], glow: 10 },
],
passeggiata_ponte: [
{ type: 'ripples', area: [480, 250, 160, 108], count: 12, flow: [10, 4],
clip: [[480, 256], [560, 250], [640, 272], [640, 358], [580, 358], [500, 300]] },
{ type: 'sparkle', area: [500, 270, 120, 70], count: 4 },
{ type: 'clouds', area: [240, 20, 300, 80], count: 3, big: true },
{ type: 'birds', phase: day, area: [250, 30, 250, 50], count: 2, every: 26000, duration: 5000, offset: 16000 },
{ type: 'sway', rects: [[0, 0, 250, 190], [270, 80, 370, 160]], amount: 2 },
butterfly([60, 220, 100, 40], 11000),
],
ponte_stazione: [
{ type: 'ripples', area: [300, 214, 340, 102], count: 18, flow: [12, 4],
clip: [[300, 218], [380, 214], [520, 240], [640, 280], [640, 316], [560, 300], [420, 252], [320, 228]] },
{ type: 'sparkle', area: [420, 240, 200, 60], count: 5 },
{ type: 'clouds', area: [180, 10, 280, 80], count: 3, big: true },
{ type: 'sway', rects: [[0, 0, 170, 190], [390, 0, 250, 210]], amount: 2 },
butterfly([150, 240, 150, 50], 6000),
{ type: 'insect', kind: 'dragonfly', phase: day, area: [440, 228, 110, 30], every: 31000, duration: 8500, offset: 15000 },
],
orti_zio_mario: [
{ type: 'clouds', area: [80, 20, 500, 90], count: 3, big: true },
{ type: 'birds', phase: day, area: [300, 30, 250, 60], count: 3, every: 24000, duration: 5000, offset: 2000 },
{ type: 'sway', rects: [[180, 10, 140, 180], [430, 10, 210, 240]], amount: 2 },
butterfly([10, 240, 140, 50], 8000),
bird([280, 290, 80, 18], 13000, 'blackbird'),
{ type: 'lamps', phase: 'sera', points: [[352, 110]], glow: 10 },
],
viale_stazione: [
{ type: 'clouds', area: [230, 0, 380, 110], count: 4, big: true },
{ type: 'birds', phase: day, area: [250, 20, 300, 60], count: 3, every: 22000, duration: 5500, offset: 9000 },
{ type: 'sway', rects: [[270, 110, 90, 110], [330, 160, 140, 70]], amount: 1.5 },
{ type: 'lamps', phase: 'sera', points: [[396, 117], [405, 155]], glow: 9 },
bird([520, 260, 60, 16], 18000),
],
terrazza: [
// Il panorama di casa: il paese e la Fortezza sotto un cielo che si muove, le ombre delle nuvole sulla collina.
{ type: 'clouds', area: [0, 0, 640, 80], count: 5, big: true },
{ type: 'cloudshadow', phase: day, area: [0, 40, 640, 200], offset: 4000, clip: rect([0, 40, 640, 200]) },
{ type: 'cloudshadow', phase: day, area: [0, 40, 640, 200], offset: 30000, clip: rect([0, 40, 640, 200]) },
{ type: 'birds', phase: day, area: [60, 20, 480, 80], count: 5, every: 19000, duration: 6500, offset: 1000 },
{ type: 'sway', rects: [[54, 176, 136, 100]], amount: 1.5 },
butterfly([500, 230, 100, 40], 10000),
],
};
+209 -10
View File
@@ -43,6 +43,15 @@ const torchHints = [{
}],
}];
// 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' },
{ unset: 'macchina_trappola' },
{ say: 'Riportata dove l\'ho presa. Se ti riviene voglia di pokerini, chiedi.', who: 'arcu', pose: 'explain' },
];
export const story = {
title: 'La Leggenda del Regolo',
// La mappa (pulsante «Mappa»): ogni scena con `map: [x, y]` (percentuali della pianta) ci compare quando è
@@ -233,6 +242,24 @@ export const story = {
quit: [{ say: 'Questo me lo tengo per la prossima.' }],
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: [{ go: 'barrettino_1997', at: [320, 330], face: 'up' }],
},
// Il pokerino del Barrettino con Vannino e Pierino (Andrea, 29 ottobre). [DA ANDREA: le battute]
poker: {
type: 'poker',
names: { player: 'Andrea', cpu: 'Vannino e Pierino' },
win: [{ say: 'M\'hai pelato, ragazzo. Nel 1997 un ci si vedeva da queste parti, con quella faccia.', who: 'vannino', pose: 'shrug' }, { set: 'poker_vinto' }],
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).
// [DA ANDREA: le battute sono provvisorie]
cerbottana: {
@@ -244,6 +271,16 @@ export const story = {
quit: [{ say: 'Il muro resta lì. E l\'odore dello stucco, sulle dita, pure.' }],
draw: [],
},
// La Condor coi pallini gialli al campone delle poste, in gara col Taglia (Andrea, 29 ottobre). [DA ANDREA: le battute]
condor: {
type: 'condor',
picture: 'assets/closeups/campone-condor.png',
names: { player: 'Andrea', cpu: 'il Taglia' },
win: [{ say: 'Battuto il Taglia! Con la sua Condor, per giunta.' }, { say: 'Fortuna. Domani la rivinco.', who: 'taglia', pose: 'shrug' }, { set: 'condor_vinta' }],
lose: [{ say: 'Il Taglia ha la mano ferma. Io ho il fiatone.' }, { say: 'Si torna quando vuoi. I pallini gialli un mancano mai.', who: 'taglia', pose: 'laugh' }],
draw: [{ say: 'Pari. Si rifà?' }, { say: 'Si rifà.', who: 'taglia', pose: 'explain' }],
quit: [{ say: 'La Condor la rendo al Taglia. Per ora.' }],
},
// Le macchinine sulla pista col gesso, sul parcheggio di Cardetole, contro il Chiaru (Andrea, 28 e 29 ottobre).
// [DA ANDREA: le battute sono provvisorie]
micro: {
@@ -323,9 +360,16 @@ export const story = {
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
// 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.
zio_mario: { name: 'Lo zio Mario', color: '#cfe3ff', sprite: 'assets/characters/zio_mario.json' },
chiaru: { name: 'Il Chiaru', color: '#ffd6a0', sprite: 'assets/characters/chiaru.json' },
// Andrea Tagliaferri, il Taglia (consenso suo, 29 ottobre): sprite di Codex dalla sua foto.
taglia: { name: 'Il Taglia', color: '#ffe36a', sprite: 'assets/characters/taglia.json' },
barondeduke: { name: 'Il BaronDeDuke', color: '#e0c090', sprite: 'assets/characters/barondeduke.json' },
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.
@@ -790,7 +834,7 @@ export const story = {
id: 'verso_forno', name: 'a destra, verso il forno', box: [570, 176, 70, 76], at: [588, 250], exit: { scene: 'forno', at: [628, 300], face: 'left' },
},
{
id: 'salita_villa_adami', name: 'su per Via della Compagnia, verso Villa Adami', box: [440, 330, 200, 30], at: [600, 350], exit: { scene: 'via_compagnia', at: [222, 296], face: 'right' },
id: 'salita_villa_adami', name: 'su per Via della Compagnia, verso Villa Adami', box: [440, 330, 200, 30], at: [600, 350], exit: { scene: 'via_compagnia', at: [94, 316], face: 'right' },
},
],
},
@@ -1295,6 +1339,9 @@ export const story = {
walk: [[0, 360], [0, 232], [640, 232], [640, 360]],
depth: [[232, 0.55], [360, 1.15]],
entry: [580, 340],
speakers: { taglia: [470, 214] },
// Il Taglia con la sua Condor, più alto di Andrea (che è un metro e ottanta).
people: [{ who: 'taglia', at: [470, 296], facing: -1, size: 1.2, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 3.9 }],
hotspots: [
{
id: 'giochi', name: 'giochi', box: [196, 120, 330, 100], at: [360, 250], face: 'up',
@@ -1325,6 +1372,13 @@ export const story = {
{
id: 'alla_strada', name: 'indietro, sulla strada della stazione', box: [100, 336, 440, 24], at: [300, 352], exit: { scene: 'viale_stazione', at: [620, 280], face: 'left' },
},
{
id: 'taglia', name: 'il Taglia', box: [448, 224, 44, 72], at: [430, 306], face: 'right', default: 'talk',
actions: {
look: [{ say: 'Il Taglia. Io sono un metro e ottanta, e lui mi guarda dall\'alto. Con la Condor in mano.' }],
talk: [{ dialogue: 'taglia' }],
},
},
],
},
// Via Calimara: dalla Piazza scende alla farmacia, in Via Provinciale.
@@ -1957,6 +2011,10 @@ export const story = {
{
id: 'alla_pieve', name: 'a destra, verso la Pieve', box: [590, 220, 50, 140], at: [628, 300], exit: { scene: 'pieve', at: [588, 250], face: 'left' },
},
{
// Dietro, su per l'arco di Via della Compagnia (Andrea, 29 ottobre).
id: 'all_arco', name: 'dietro, su per l\'arco di Via della Compagnia', box: [60, 334, 520, 26], at: [320, 350], exit: { scene: 'via_compagnia', at: [222, 296], face: 'up' },
},
],
},
stazione: {
@@ -1991,7 +2049,7 @@ export const story = {
},
{
// Dal ponte delle poste si scende sulla passeggiata della Carza (Andrea, 29 ottobre).
id: 'giu_carza', name: 'giù dal ponte, la passeggiata sulla Carza', box: [590, 296, 50, 38], at: [620, 320], exit: { scene: 'ponte_stazione', at: [158, 192], face: 'down' },
id: 'giu_carza', name: 'giù dal ponte, la passeggiata sulla Carza', box: [590, 296, 50, 38], at: [620, 320], exit: { scene: 'ponte_stazione', at: [440, 252], face: 'left' },
},
],
},
@@ -2745,7 +2803,9 @@ export const story = {
speakers: { arcu: [374, 176], zio_mario: [440, 196] },
people: [
// L'Arcu sotto la tenda del Trappola: due minuti fa era a Campomigliaio (Andrea, 28 ottobre).
{ who: 'arcu', at: [374, 248], facing: -1, size: 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,
// 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'] },
@@ -2767,11 +2827,11 @@ export const story = {
},
},
{
// «Di chi è quell'Audi nera?» «Let it be» (che si legge «L'è di Bi»: è di Bini). Andrea, 28 ottobre.
// «Di chi è quell'Audi lì?» «Let it be» (che si legge «L'è di Bi»: è di Bini). Andrea, 28 ottobre.
id: 'audi', name: 'Audi nera', box: [292, 198, 68, 40], at: [330, 254], face: 'up', default: 'look',
actions: {
look: [{ say: 'Di chi è quell\'Audi nera?' }, { say: 'Let it be.', who: 'arcu', pose: 'explain' }],
talk: [{ say: 'Di chi è quell\'Audi nera?' }, { say: 'Let it be.', who: 'arcu', pose: 'explain' }],
look: [{ say: 'Di chi è quell\'Audi lì?' }, { say: 'Let it be.', who: 'arcu', pose: 'explain' }],
talk: [{ say: 'Di chi è quell\'Audi lì?' }, { say: 'Let it be.', who: 'arcu', pose: 'explain' }],
use: [{ say: 'Non è mica mia. Mi garba, però, nera fiammante.' }],
},
},
@@ -2791,6 +2851,14 @@ export const story = {
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).
id: 'betino', name: 'il Betino dell\'Arcu', box: [232, 172, 40, 24], at: [250, 206], face: 'up', default: 'look',
@@ -2948,7 +3016,11 @@ export const story = {
actions: { look: [{ say: 'Il lampione di ferro, attaccato al muro. La sera fa una luce gialla che pare d\'altri tempi.' }] },
},
{
id: 'arco_pieve', name: 'sotto l\'arco, giù alla Pieve', box: [198, 222, 50, 66], at: [222, 296], exit: { scene: 'pieve', at: [600, 350], face: 'left' },
// L'arco porta al forno; alla Pieve si scende fra le due case gialle (Andrea, 29 ottobre).
id: 'arco_forno', name: 'sotto l\'arco, giù al forno', box: [198, 222, 50, 66], at: [222, 296], exit: { scene: 'forno', at: [320, 350], face: 'up' },
},
{
id: 'alla_pieve', name: 'fra le case gialle, giù alla Pieve', box: [80, 196, 40, 100], at: [94, 316], exit: { scene: 'pieve', at: [600, 350], face: 'left' },
},
{
id: 'su_villa_adami', name: 'su verso Villa Adami', box: [420, 196, 42, 56], at: [440, 258], exit: { scene: 'villa_adami', at: [600, 266], face: 'left' },
@@ -3243,10 +3315,12 @@ export const story = {
actions: { look: [{ say: 'La Carza passa sotto gli archi e se ne va verso la Sieve.' }] },
},
{
id: 'avanti_carza', name: 'avanti lungo la Carza, oltre il ponte', box: [586, 296, 54, 40], at: [612, 320], exit: { scene: 'orti_zio_mario', at: [400, 352], face: 'up' },
// Il viottolo passa sotto l'arco e va avanti lungo la Carza; alla stazione si sale dalla parte destra del ponte
// (Andrea, 29 ottobre).
id: 'avanti_carza', name: 'avanti per il viottolo, sotto il ponte', box: [136, 158, 46, 34], at: [158, 192], exit: { scene: 'orti_zio_mario', at: [400, 352], face: 'up' },
},
{
id: 'su_ponte', name: 'su per il viottolo, al ponte e alla stazione', box: [136, 158, 46, 34], at: [158, 192], exit: { scene: 'stazione', at: [620, 320], face: 'left' },
id: 'su_ponte', name: 'su sul ponte, alla stazione', box: [410, 86, 100, 60], at: [440, 252], exit: { scene: 'stazione', at: [620, 320], face: 'left' },
},
{
id: 'indietro', name: 'indietro lungo la Carza', box: [0, 336, 640, 24], at: [300, 352], exit: { scene: 'passeggiata_ponte', at: [234, 204], face: 'down' },
@@ -3279,7 +3353,7 @@ export const story = {
actions: { look: [{ say: 'Laggiù la passerella coperta sulla Carza.' }], walk: [{ say: 'Di là ci passo un\'altra volta: prima il palio.' }] },
},
{
id: 'indietro', name: 'indietro, verso il ponte delle poste', box: [150, 336, 490, 24], at: [400, 352], exit: { scene: 'ponte_stazione', at: [612, 320], face: 'left' },
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' },
},
],
},
@@ -3312,6 +3386,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.
scuola: {
name: 'La scuola',
@@ -3675,6 +3812,18 @@ export const story = {
{ 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' },
] },
// 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' },
],
},
@@ -3750,6 +3899,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]
chiaru: {
nodes: {
+1 -1
View File
@@ -1,5 +1,5 @@
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
export const VERSION = '0.1.50';
export const VERSION = '0.1.54';
// 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).
+10 -9
View File
@@ -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-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; }
#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; }
.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[aria-pressed="true"] { border-color: #ffdb76; }
.card-hand button:disabled { opacity: .5; cursor: default; }
#briscola-controls summary { cursor: pointer; padding: 10px 0; }
#briscola-controls p { max-width: 76em; }
#poker-controls summary, #briscola-controls summary { cursor: pointer; padding: 10px 0; }
#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-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; }
@@ -62,16 +62,16 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
/* La guida semplice del GP: il Gas si tiene premuto col pollice, più alto. */
#bocce-throw.hold { min-height: 72px; font-size: 14px; }
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary, #gara-help summary, #rinquarto-help summary, #micro-help summary, #cerbottana-help summary { cursor: pointer; padding: 6px 0; }
#bocce-help p, #biliardo-help p, #basket-help p, #pesca-help p, #gara-help p, #rinquarto-help p, #micro-help p, #cerbottana-help p { max-width: 76em; line-height: 1.7; }
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary, #gara-help summary, #rinquarto-help summary, #micro-help summary, #cerbottana-help, #condor-help summary { cursor: pointer; padding: 6px 0; }
#bocce-help p, #biliardo-help p, #basket-help p, #pesca-help p, #gara-help p, #rinquarto-help p, #micro-help p, #cerbottana-help, #condor-help p { max-width: 76em; line-height: 1.7; }
@media (max-width: 600px), (orientation: landscape) and (max-height: 520px) {
#bocce-controls { padding: 8px; font-size: 10px; overflow-y: auto; min-height: 0; }
.bocce-adjust { flex-wrap: wrap; gap: 6px; }
.bocce-adjust label { order: -1; flex-basis: 100%; max-width: none; }
.bocce-adjust button { flex: 1; font-size: 9px; padding: 6px; }
#bocce-throw { flex-basis: 100%; }
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help { font-size: 9px; }
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help { font-size: 9px; }
}
.effects-control { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; font-size: clamp(9px, 1.2vw, 13px); }
#effects-volume { width: 130px; accent-color: var(--accent); }
@@ -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. */
/* In alto a sinistra: Pausa e Mappa. */
#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; }
#back-button { color: #1b1420; background: #9fd0ff; }
#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ò. */
#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 #stage.biliardo { width: min(calc((100dvh - 44px) * 16 / 9), 100vw); }
#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. */
.card-hand-many button, .card-buttons button { flex-basis: 40%; overflow-wrap: anywhere; }
.card-hand { flex-direction: column; gap: 4px; }
+45
View File
@@ -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(); }
+103
View File
@@ -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);
});
+101
View File
@@ -0,0 +1,101 @@
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';
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');
for (let t = 0; t < TRIP.end + 0.5 && g.closeup; t += 0.05) g.update(0.05);
settle(g);
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'); settle(g);
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);
for (let t = 0; t < TRIP.end + 0.5 && g.closeup; t += 0.05) g.update(0.05);
settle(g);
assert.equal(g.state.scene, 'barrettino_1997');
assert.equal(g.scene.back.label, 'Torna al 2007');
assert.ok(g.goBack()); settle(g);
assert.equal(g.state.scene, 'via_liguri_magelli');
assert.ok(!g.hotspot('macchina_tempo'));
});
+10 -1
View File
@@ -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', () => {
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(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}`);
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); }
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', () => {
+7 -1
View File
@@ -13,5 +13,11 @@
"Commodore": "Commòdòr",
"Let it be": "Leddibì",
"tirami": "tìrami",
"Bertovo": "Bertòvo"
"Bertovo": "Bertòvo",
"bluffo": "bleffo",
"bluffare": "bleffare",
"bluffava": "bleffava",
"Imposto": "Impòsto",
"Libici": "Lìbici",
"mancano": "màncano"
}
+10
View File
@@ -35,6 +35,13 @@
"speaker": "Xavier Hayasaka",
"filtro": "asetrate=24000*1.12,aresample=24000,atempo=0.92"
},
"vannino": {
"speaker": "Kumar Dahl"
},
"pierino_parigi": {
"speaker": "Eugenio Mataracı",
"filtro": "asetrate=24000*0.97,aresample=24000"
},
"zio_mario": {
"speaker": "Viktor Menelaos",
"filtro": "asetrate=24000*0.95,aresample=24000,atempo=1.04"
@@ -63,5 +70,8 @@
"pescatori": {
"speaker": "Dionisio Schuyler",
"filtro": "asetrate=24000*1.08,aresample=24000,atempo=1.04"
},
"taglia": {
"speaker": "Aaron Dreschner"
}
}
+15
View File
@@ -67,3 +67,18 @@ python3 video/monta.py ~/Workspaces/qemu/qemu-iso-lab/artifacts/tour/out/partita
- **Il ritaglio** (`TAGLIO` in `monta.py`) va rimisurato quando cambia l'impaginazione: `convert -trim` su una
schermata della prova a vuoto.
- **Per Telegram** (50 MB al massimo): due passate x264 `-tune animation` a 470 kbit/s → circa 45 MB.
## Il trailer e la extended (29 ottobre 2026)
Copioni `video/trailer.mjs` (circa 1 min 30 s) e `video/trailer-extended.mjs` (circa 3 min 25 s, col giro completo
del paese e i giochi giocati da soli). Niente clic: la scena è a tutto schermo, le scritte e gli stacchi al nero
sono elementi aggiunti nella pagina, e la musica la sceglie il copione (`out/<clip>-audio.json`). Nel finale si
cercano solo gli occhi gialli: il palio non si mostra (Andrea: si capirebbe dov'è).
- Prova locale, senza VM: `OUT=/tmp/prova COPIONE=./trailer.mjs node video/prova-locale.mjs` (una schermata per
battuta, e i tempi).
- Ripresa: come la partita completa, poi `python3 video/monta.py <cartella> --trailer`.
- Il gioco va in pausa da solo dopo un minuto senza tocchi: il copione manda un finto `pointermove` ogni 10 s.
- Una camminata lascia `game.task` in sospeso, e cambiando scena di colpo il gioco non si ferma più: si azzera.
- Le battute saltate lasciano spezzoni di voce nel registro: prima di montare si tolgono quelli sotto il mezzo secondo
(attenzione a «Let it be.», che dura un secondo ed è giusta).
+9
View File
@@ -4,6 +4,7 @@ copione -> video/out/<clip>.mp4.
python3 video/monta.py ~/Workspaces/qemu/qemu-iso-lab/artifacts/tour/out/demo-prototipo
python3 video/monta.py ~/Workspaces/qemu/qemu-iso-lab/artifacts/tour/out/partita-completa
python3 video/monta.py ~/Workspaces/qemu/qemu-iso-lab/artifacts/tour/out/trailer --trailer
- ritaglia lo schermo 1600x900 della VM sull'area del gioco e lascia sopra una fascia per le didascalie;
- le didascalie (cues.json, solo «it») restano a schermo il tempo di leggerle, non fino alla successiva;
@@ -31,6 +32,9 @@ VOLUME = 0.5 # musica della demo-prototipo (senza voci)
VOLUME_SOTTO = 0.22 # musica sotto le voci: bassa (Andrea, 25 ottobre)
VOLUME_VOCI = 1.0
VOLUME_NARRATORE = 1.15
# Il trailer (--trailer): tutto lo schermo della VM (la scena è a tutto schermo), niente fascia né didascalie (le
# scritte sono nella pagina), la musica in primo piano.
VOLUME_TRAILER = 0.6
VOLUME_TUTORIAL = 0.07 # musica dei video «Come si gioca»: sottofondo # il narratore dei video «Come si gioca», sopra la musica bassa
FADE = 1.2
@@ -49,6 +53,7 @@ def ts(s: float) -> str:
def main() -> None:
d = Path(sys.argv[1]).expanduser()
tutorial = "--tutorial" in sys.argv[2:]
trailer = "--trailer" in sys.argv[2:]
taglio, fascia = (TAGLIO_TUTORIAL, FASCIA_TUTORIAL) if tutorial else (TAGLIO, FASCIA)
meta = json.loads((d / "cues.json").read_text())
nome = meta["clip"]
@@ -71,6 +76,8 @@ def main() -> None:
# «troppo alta la musica del gioco»).
if (OUT / f"{nome}-narrazione.json").exists():
volume = VOLUME_TUTORIAL
if trailer:
volume = VOLUME_TRAILER
else:
musica = json.loads((OUT / "musica.json").read_text())
volume = VOLUME
@@ -131,6 +138,8 @@ def main() -> None:
mix = "".join(pezzi) + f"amix=inputs={len(pezzi)}:normalize=0:duration=longest[a]"
stile = f"FontName=DejaVu Sans,FontSize=11,Bold=1,MarginV=4,Outline=1,Shadow=0,PrimaryColour=&H007AD2FF"
video = f"[0:v]{taglio},pad=iw:ih+{fascia}:0:{fascia}:{FONDO},subtitles={srt}:force_style='{stile}'[v]"
if trailer:
video = "[0:v]scale=1600:900:flags=lanczos,fade=t=in:d=0.5[v]"
finale = OUT / f"{nome}.mp4"
subprocess.run(["ffmpeg", "-loglevel", "error", "-y", "-i", str(raw), *ingressi,
"-filter_complex", ";".join([video, *catene, mix]), "-map", "[v]", "-map", "[a]",
+19
View File
@@ -0,0 +1,19 @@
// Prova locale di un copione del trailer, senza la VM: un finto «d» col solo necessario, una schermata per battuta.
// OUT=/tmp/prova COPIONE=./trailer-extended.mjs node video/prova-locale.mjs
import { chromium } from 'playwright';
import { staticServer } from '../tests/helpers.mjs';
const server = await staticServer();
const browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
const out = process.env.OUT;
try {
const page = await browser.newPage({ viewport: { width: 1600, height: 900 } });
page.on('pageerror', e => console.log('ERR', e.message));
const orig = page.goto.bind(page);
page.goto = url => orig(url.replace('http://127.0.0.1:8000/', `http://127.0.0.1:${server.port}/`));
const mod = await import(process.env.COPIONE ?? './trailer.mjs');
const t0 = Date.now(); let n = 0;
const d = { page, vm: () => {}, cue: async id => { console.log(((Date.now() - t0) / 1000).toFixed(1), id); setTimeout(() => page.screenshot({ path: `${out}/tr-${String(++n).padStart(2, '0')}-${id}.png` }), id === 'buio' ? 5000 : 3000); } };
await mod.setup(d);
await mod.run(d);
console.log('fine', ((Date.now() - t0) / 1000).toFixed(1));
} finally { await browser.close(); server.kill(); }
+263
View File
@@ -0,0 +1,263 @@
// Copione del trailer lungo «La Leggenda del Regolo · extended» (scaletta approvata da Andrea il 29 ottobre): circa tre
// minuti, come il trailer (video/trailer.mjs) ma col giro completo del paese e i giochi giocati per qualche secondo:
// bocce, biliardo, GP di Fortezza, briscola, macchinine, cerbottana.
// Si gira come la partita completa (video/SCALETTA.md), poi: python3 video/monta.py <cartella> --trailer .
import { mkdirSync, writeFileSync } from "node:fs";
const c = (id, it) => ({ id, it, en: it, top: true });
export const title = { it: "La Leggenda del Regolo · extended", en: "La Leggenda del Regolo · extended" };
export const cues = [
c("apertura", "San Piero a Sieve, maggio 2007"),
c("notte", "La notte prima del Palio…"),
c("sparito", "…il palio è sparito."),
c("giro", "il giro del paese"),
c("sfide", "Un paese pieno di sfide"),
c("bocce", "Le bocce al pallaio"),
c("biliardo", "Il biliardo del Circolo"),
c("gara", "Il GP di Fortezza"),
c("carte", "La briscola all'oratorio"),
c("macchinine", "Le macchinine col gesso"),
c("cerbottana", "La cerbottana"),
c("amici", "…e di vecchie conoscenze"),
c("buio", "la fessura"),
c("titolo", "La Leggenda del Regolo"),
];
const URL_GIOCO = "http://127.0.0.1:8000/";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function setup(d) {
const p = d.page;
await p.goto(URL_GIOCO);
await p.evaluate(() => localStorage.clear());
await p.reload();
await sleep(500);
if (await p.evaluate(() => document.getElementById("splash").classList.contains("wait"))) await p.click("#splash");
await p.waitForSelector("#title:not([hidden])");
// Si entra in partita dietro le quinte: rione, introduzione saltata.
await p.click("#new-game");
await p.click('#rione [data-rione="campomigliaio"]');
await p.click("#skip-intro");
// Il palcoscenico del trailer: solo la scena, a tutto schermo; sopra, il nero e le scritte.
await p.addStyleTag({ content: `
#side, #corner, #sound, #closeup-close, #bocce-controls, #briscola-controls, #tressette-controls, #scopa-controls,
#map-button, #pause-button, #spots-button, #notebook-button, #sentence { display: none !important; }
body { background: #000 !important; overflow: hidden; }
#app { position: fixed !important; inset: 0 !important; width: 100vw !important; margin: 0 !important; }
#stage, #stage.biliardo, #stage.briscola { width: 100vw !important; height: 100vh !important; }
#scene { width: 100vw !important; height: 100vh !important; }
#title .buttons, #title .version, #title .dedication { display: none !important; }
#tr-nero { position: fixed; inset: 0; background: #000; opacity: 1; transition: opacity .7s; z-index: 50; pointer-events: none; }
#tr-scritta { position: fixed; left: 0; right: 0; top: 50%; transform: translateY(-50%); text-align: center; z-index: 51;
font-family: "Press Start 2P", monospace; font-size: 42px; line-height: 1.6; color: #ffe9b0; opacity: 0;
transition: opacity .8s; text-shadow: 4px 4px 0 #2a1608, 0 0 24px rgba(0,0,0,.9); pointer-events: none; padding: 0 60px; }
#tr-scritta.basso { top: auto; bottom: 70px; transform: none; font-size: 32px; }
` });
await p.evaluate(() => {
for (const id of ["tr-nero", "tr-scritta"]) { const e = document.createElement("div"); e.id = id; document.body.appendChild(e); }
});
// Senza nessun tocco il gioco va in pausa da solo dopo un minuto: un finto movimento ogni tanto lo tiene sveglio.
await p.evaluate(() => setInterval(() => dispatchEvent(new PointerEvent("pointermove")), 10000));
// Le battute d'ingresso in piazza si saltano e la voce si ferma: il trailer parte in silenzio.
await p.evaluate(async () => { const { settle } = await import('/tests/helpers.mjs'); settle(game); window.voices?.stop?.(); });
// Il puntatore vero lontano, nell'angolo.
d.vm("DISPLAY=:0 ~/lab/video/mv.py 1599 899 0.2");
await sleep(1200);
}
export async function run(d) {
const p = d.page;
// L'orologio dell'audio, come nella partita completa: le voci che partono; la musica la decide il copione.
await p.evaluate(() => {
window.__t0 = Date.now(); window.__audio = [];
const t = () => (Date.now() - window.__t0) / 1000;
const path = (src) => decodeURI(new URL(src, location.href).pathname.slice(1));
let voice = null;
window.__musica = (src) => window.__audio.push({ t: t(), music: src });
const look = () => {
const a = window.voices?.audio, now = a && !a.paused && !a.ended ? a : null;
if (now !== voice) {
if (voice) window.__audio.push({ t: t(), voiceEnd: true });
if (now) window.__audio.push({ t: t(), voice: path(now.src) });
voice = now;
}
requestAnimationFrame(look);
};
look();
});
const musica = (src) => p.evaluate((s) => window.__musica(s), src);
const nero = (on) => p.evaluate((on) => { document.getElementById("tr-nero").style.opacity = on ? 1 : 0; }, on);
const scritta = (testo, basso = false) => p.evaluate(([t, b]) => {
const e = document.getElementById("tr-scritta");
if (t) { e.textContent = t; e.classList.toggle("basso", b); }
e.style.opacity = t ? 1 : 0;
}, [testo, basso]);
// In ogni scena: niente primo piano, niente battute in coda, flag di partenza; Andrea dove serve (o fuori campo).
const vai = (scene, at, flags = {}) => `async () => {
const { settle } = await import('/tests/helpers.mjs');
game.closeup = null; game.queue.length = 0; game.speech = null; game.dialogue = null; game.path = []; game.walking = false; game.task = null;
Object.assign(game.state.flags, ${JSON.stringify(flags)});
game.go('${scene}'${at ? `, ${JSON.stringify(at)}` : ""}); settle(game);
${at ? "" : "game.state.x = -400;"}
}`;
const prepara = (codice) => p.evaluate(`(${codice})()`);
const fuoco = async (codice, veloce) => {
await p.evaluate((v) => { document.getElementById("tr-nero").style.transition = `opacity ${v ? .18 : .7}s`; }, veloce);
await nero(true); await sleep(veloce ? 200 : 750);
await prepara(codice);
await sleep(veloce ? 150 : 300);
await nero(false); await sleep(veloce ? 200 : 700);
};
const dice = (who, testo, pose) => p.evaluate(([w, t, po]) => game.run(w ? { say: t, who: w, pose: po } : { say: t }), [who, testo, pose]);
const finitoDiDire = () => p.waitForFunction(() => !game.speech, null, { timeout: 20000 });
// 1. Il nero, poi il panorama di Monte Cacioli con le nuvole.
await musica("assets/audio/sera.mp3");
await d.cue("apertura");
await sleep(800);
await p.evaluate(() => { document.getElementById("tr-scritta").style.whiteSpace = "pre-line"; });
await scritta("San Piero a Sieve\nmaggio 2007");
await sleep(2400);
await scritta(null);
await fuoco(vai("via_cafaggio"));
await sleep(3200);
// 2. La Fortezza al tramonto, le impronte.
await d.cue("notte");
await fuoco(vai("fortezza", null, { indizio_regolo: true }));
await scritta("La notte prima del Palio…", true);
await sleep(3000);
await scritta(null);
// 3. La scuola: il palio non c'è più.
await d.cue("sparito");
await fuoco(vai("scuola", [300, 330], { indizio_regolo: false }));
await scritta("…il palio è sparito.", true);
await sleep(3200);
await scritta(null);
// 4. Il giro completo del paese: Andrea cammina di scena in scena, con la musica del paese.
await d.cue("giro");
await musica("assets/audio/paese.mp3");
for (const [scene, da, a] of [["piazza", [120, 330], [300, 300]], ["via_de_medici", [300, 348], [300, 230]], ["via_compagnia", [300, 350], [430, 262]],
["villa_adami", [300, 345], [560, 272]], ["fortezza", [175, 352], [400, 300]], ["pieve", [40, 330], [260, 300]], ["via_cafaggio", [150, 352], [420, 250]],
["rotonda_cafaggio", [390, 352], [540, 230]], ["passeggiata_carza", [330, 352], [420, 250]], ["ponte_stazione", [300, 352], [200, 260]],
["orti_zio_mario", [400, 352], [330, 260]], ["stazione", [320, 350], [450, 290]], ["cardetole", [300, 350], [470, 300]], ["terrazza", [300, 330], [420, 320]]]) {
await fuoco(vai(scene, da), true);
await p.evaluate((a) => game.walkTo(a), a);
await sleep(3300);
}
// 5. Le sfide, giocate davvero per qualche secondo: ogni gioco col suo nome in basso e la sua musica.
await d.cue("sfide");
await musica(null);
await fuoco(`async () => { game.state.x = -400; }`);
await scritta("Un paese pieno di sfide");
await sleep(2000);
await scritta(null);
// Apre il primo piano, poi ogni 150 ms chiama `mossa` (il giocatore che gioca da solo) finché passano i secondi.
const gioca = async (cue, nome, brano, closeup, level, modulo, prima, mossa, secondi) => {
await d.cue(cue);
await musica(brano);
await fuoco(`async () => {
const { settle } = await import('/tests/helpers.mjs');
game.closeup = null; game.queue.length = 0; game.speech = null; game.dialogue = null; game.path = []; game.walking = false; game.task = null;
game.run({ closeup: '${closeup}', level: '${level}' }); settle(game);
window.__m = await import('${modulo}');
const M = window.__m, c = game.closeup; ${prima}
}`);
await scritta(nome, true);
const fine = Date.now() + secondi * 1000;
while (Date.now() < fine) {
await p.evaluate(`(() => { const M = window.__m, c = game.closeup; if (!c) return; ${mossa} })()`);
if (Date.now() > fine - (secondi - 3) * 1000) await scritta(null);
await sleep(150);
}
};
await gioca("bocce", "Le bocce al pallaio", "assets/audio/bocce.mp3", "bocce", "seria", "/src/engine/bocce.js", "",
"if (M.canThrow(c)) { c.aim = { u: c.jack.u, w: c.jack.w }; c.power = M.powerFor(Math.hypot(c.jack.u, c.jack.w) - 1.5); M.shoot(c); }", 14);
await gioca("biliardo", "Il biliardo del Circolo", "assets/audio/biliardo.mp3", "biliardo", "seria", "/src/engine/biliardo.js", "",
"if (c.step === 'aim' && c.current === 'player' && !c.holding) M.strike(c, 'player', Math.atan2(c.balls.cpu.y - c.balls.player.y, c.balls.cpu.x - c.balls.player.x) + 0.04, 0.75);", 14);
await gioca("gara", "Il GP di Fortezza", "assets/audio/gara.mp3", "gara", "bona", "/src/engine/gara.js",
"while (c.step === 'intro') M.press(c); M.release(c);", "c.holding = c.step === 'race' && !M.mustLift(c);", 16);
await gioca("carte", "La briscola all'oratorio", "assets/audio/carte.mp3", "briscola", "seria", "/src/engine/briscola.js", "",
"if (M.canPlay(c)) M.play(c, 0);", 12);
await gioca("macchinine", "Le macchinine col gesso", "assets/audio/macchinine.mp3", "micro", "bona", "/src/engine/micro.js", "M.pick(c, 0);",
"if (M.canThrow(c)) { const car = c.cars.player, i = (car.idx + Math.round(110 / c.track.step)) % c.track.n, [x, y] = c.track.pts[i]; M.strike(c, 'player', Math.atan2(y - car.y, x - car.x), M.powerFor(Math.hypot(x - car.x, y - car.y))); }", 12);
await gioca("cerbottana", "La cerbottana", "assets/audio/gara.mp3", "cerbottana", "seria", "/src/engine/cerbottana.js", "",
"if (M.canThrow(c)) { const k = c.cans.find(k => !k.down); c.aim = k && c.left % 2 ? { x: k.x, y: M.LEDGE - 8 } : { x: 320 + (c.left % 3 - 1) * 6, y: 118 }; c.power = M.BEST; M.shoot(c); }", 10);
await musica("assets/audio/paese.mp3");
// 6. Le vecchie conoscenze, con le loro battute.
await d.cue("amici");
await fuoco(vai("via_liguri_magelli", [330, 300]));
await scritta("…e di vecchie conoscenze", true);
await dice(null, "Di chi è quell'Audi lì?");
await finitoDiDire();
await scritta(null);
await dice("arcu", "Let it be.", "explain");
await finitoDiDire();
await sleep(300);
await dice(null, "Zio, hai problemi?");
await finitoDiDire();
await dice("zio_mario", "I problemi io gli avevo quando andavo a scuola!", "laugh");
await finitoDiDire();
await fuoco(vai("cardetole", [470, 330]), true);
await dice("chiaru", "Vorrai dire un giochino!", "laugh");
await finitoDiDire();
await sleep(400);
// 7. Il buio: la sera, la torcia nella fessura, gli occhi gialli.
await d.cue("buio");
// Sotto il buio una musica bassa di suspense (Andrea: «un po' di musichetta nel finale»).
await musica("assets/audio/sera.mp3");
await fuoco(vai("fortezza", [462, 310], { indizio_regolo: true, casetta_aperta: true }));
await sleep(900);
await p.evaluate(() => { game.state.inventory.push("torcia"); game.act("use", "fessura", "torcia"); });
// «Accendo la torcia e la punto dentro…» si salta: il primo piano si apre subito.
// Il palio non si vede (Andrea: si capirebbe che è in Fortezza): la luce parte da destra, nel buio, e i primi due
// ritrovamenti si danno per fatti; si cercano solo gli occhi.
for (let k = 0; k < 60 && await p.evaluate(() => game.closeup?.id !== "fessura"); k++) { await p.evaluate(() => game.skip()); await sleep(50); }
await p.evaluate(() => { const c = game.closeup; c.found = 2; c.beam = [600, 300]; });
await sleep(1600);
const targets = await p.evaluate(() => game.story.closeups.fessura.targets.map((t) => t.at));
for (let i = 2; i < targets.length; i++) {
if (i === 2) await musica("assets/audio/cartello.mp3");
// La luce va piano verso il punto, come una mano che cerca: prima in basso, poi su verso gli occhi.
const from = await p.evaluate(() => game.closeup.beam);
for (let k = 1; k <= 30; k++) {
const x = from[0] + (targets[i][0] - from[0]) * k / 30 + Math.sin(k / 4) * 12, y = from[1] + (targets[i][1] - from[1]) * k / 30;
await p.evaluate(([x, y]) => game.closeupAim([x, y]), [x, y]);
await sleep(50);
}
// Le battute dei primi ritrovamenti si saltano (partono prima che il ritrovamento conti, e lo fermano); resta solo
// l'ultima: «…sono due occhi gialli!».
for (let k = 0; k < 300 && await p.evaluate((i) => !(game.closeup?.found > i), i); k++) {
if (i < 2) await p.evaluate(() => game.skip());
await sleep(100);
}
await p.waitForFunction(() => !game.speech && !game.closeup?.lines?.length, null, { timeout: 30000 });
await sleep(i === 2 ? 1400 : 100);
}
await nero(true);
await sleep(1400);
// 8. Il titolo.
await d.cue("titolo");
await musica("assets/audio/titolo.mp3");
await p.evaluate(() => {
game.closeup = null;
document.getElementById("title").hidden = false;
document.getElementById("tr-nero").style.transition = "opacity 1.6s";
});
await sleep(300);
await nero(false);
await sleep(7500);
await nero(true);
await sleep(1600);
const audio = await p.evaluate(() => window.__audio);
const out = new URL("./out/", import.meta.url);
mkdirSync(out, { recursive: true });
writeFileSync(new URL("trailer-extended-audio.json", out), JSON.stringify({ eventi: audio }, null, 2));
}
+238
View File
@@ -0,0 +1,238 @@
// Copione del trailer «La Leggenda del Regolo» (scaletta approvata da Andrea il 29 ottobre): circa 75 secondi, la scena a
// tutto schermo, scritte grandi e stacchi al nero fatti dentro la pagina, niente puntatore e niente clic.
// Si gira come la partita completa (video/SCALETTA.md), poi: python3 video/monta.py <cartella> --trailer .
// La musica non è quella che sceglierebbe il gioco: il copione annota i brani voluti, e le voci che partono, in
// out/trailer-audio.json; monta.py rimonta la colonna sonora.
import { mkdirSync, writeFileSync } from "node:fs";
const c = (id, it) => ({ id, it, en: it, top: true });
export const title = { it: "La Leggenda del Regolo · trailer", en: "La Leggenda del Regolo · trailer" };
export const cues = [
c("apertura", "San Piero a Sieve, maggio 2007"),
c("notte", "La notte prima del Palio…"),
c("sparito", "…il palio è sparito."),
c("paese", "il paese"),
c("sfide", "Un paese pieno di sfide"),
c("amici", "…e di vecchie conoscenze"),
c("buio", "la fessura"),
c("titolo", "La Leggenda del Regolo"),
];
const URL_GIOCO = "http://127.0.0.1:8000/";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function setup(d) {
const p = d.page;
await p.goto(URL_GIOCO);
await p.evaluate(() => localStorage.clear());
await p.reload();
await sleep(500);
if (await p.evaluate(() => document.getElementById("splash").classList.contains("wait"))) await p.click("#splash");
await p.waitForSelector("#title:not([hidden])");
// Si entra in partita dietro le quinte: rione, introduzione saltata.
await p.click("#new-game");
await p.click('#rione [data-rione="campomigliaio"]');
await p.click("#skip-intro");
// Il palcoscenico del trailer: solo la scena, a tutto schermo; sopra, il nero e le scritte.
await p.addStyleTag({ content: `
#side, #corner, #sound, #closeup-close, #bocce-controls, #briscola-controls, #tressette-controls, #scopa-controls,
#map-button, #pause-button, #spots-button, #notebook-button, #sentence { display: none !important; }
body { background: #000 !important; overflow: hidden; }
#app { position: fixed !important; inset: 0 !important; width: 100vw !important; margin: 0 !important; }
#stage, #stage.biliardo, #stage.briscola { width: 100vw !important; height: 100vh !important; }
#scene { width: 100vw !important; height: 100vh !important; }
#title .buttons, #title .version, #title .dedication { display: none !important; }
#tr-nero { position: fixed; inset: 0; background: #000; opacity: 1; transition: opacity .7s; z-index: 50; pointer-events: none; }
#tr-scritta { position: fixed; left: 0; right: 0; top: 50%; transform: translateY(-50%); text-align: center; z-index: 51;
font-family: "Press Start 2P", monospace; font-size: 42px; line-height: 1.6; color: #ffe9b0; opacity: 0;
transition: opacity .8s; text-shadow: 4px 4px 0 #2a1608, 0 0 24px rgba(0,0,0,.9); pointer-events: none; padding: 0 60px; }
#tr-scritta.basso { top: auto; bottom: 70px; transform: none; font-size: 32px; }
` });
await p.evaluate(() => {
for (const id of ["tr-nero", "tr-scritta"]) { const e = document.createElement("div"); e.id = id; document.body.appendChild(e); }
});
// Senza nessun tocco il gioco va in pausa da solo dopo un minuto: un finto movimento ogni tanto lo tiene sveglio.
await p.evaluate(() => setInterval(() => dispatchEvent(new PointerEvent("pointermove")), 10000));
// Le battute d'ingresso in piazza si saltano e la voce si ferma: il trailer parte in silenzio.
await p.evaluate(async () => { const { settle } = await import('/tests/helpers.mjs'); settle(game); window.voices?.stop?.(); });
// Il puntatore vero lontano, nell'angolo.
d.vm("DISPLAY=:0 ~/lab/video/mv.py 1599 899 0.2");
await sleep(1200);
}
export async function run(d) {
const p = d.page;
// L'orologio dell'audio, come nella partita completa: le voci che partono; la musica la decide il copione.
await p.evaluate(() => {
window.__t0 = Date.now(); window.__audio = [];
const t = () => (Date.now() - window.__t0) / 1000;
const path = (src) => decodeURI(new URL(src, location.href).pathname.slice(1));
let voice = null;
window.__musica = (src) => window.__audio.push({ t: t(), music: src });
const look = () => {
const a = window.voices?.audio, now = a && !a.paused && !a.ended ? a : null;
if (now !== voice) {
if (voice) window.__audio.push({ t: t(), voiceEnd: true });
if (now) window.__audio.push({ t: t(), voice: path(now.src) });
voice = now;
}
requestAnimationFrame(look);
};
look();
});
const musica = (src) => p.evaluate((s) => window.__musica(s), src);
const nero = (on) => p.evaluate((on) => { document.getElementById("tr-nero").style.opacity = on ? 1 : 0; }, on);
const scritta = (testo, basso = false) => p.evaluate(([t, b]) => {
const e = document.getElementById("tr-scritta");
if (t) { e.textContent = t; e.classList.toggle("basso", b); }
e.style.opacity = t ? 1 : 0;
}, [testo, basso]);
// In ogni scena: niente primo piano, niente battute in coda, flag di partenza; Andrea dove serve (o fuori campo).
const vai = (scene, at, flags = {}) => `async () => {
const { settle } = await import('/tests/helpers.mjs');
game.closeup = null; game.queue.length = 0; game.speech = null; game.dialogue = null; game.path = []; game.walking = false; game.task = null;
Object.assign(game.state.flags, ${JSON.stringify(flags)});
game.go('${scene}'${at ? `, ${JSON.stringify(at)}` : ""}); settle(game);
${at ? "" : "game.state.x = -400;"}
}`;
const prepara = (codice) => p.evaluate(`(${codice})()`);
const fuoco = async (codice, veloce) => {
await p.evaluate((v) => { document.getElementById("tr-nero").style.transition = `opacity ${v ? .18 : .7}s`; }, veloce);
await nero(true); await sleep(veloce ? 200 : 750);
await prepara(codice);
await sleep(veloce ? 150 : 300);
await nero(false); await sleep(veloce ? 200 : 700);
};
const dice = (who, testo, pose) => p.evaluate(([w, t, po]) => game.run(w ? { say: t, who: w, pose: po } : { say: t }), [who, testo, pose]);
const finitoDiDire = () => p.waitForFunction(() => !game.speech, null, { timeout: 20000 });
// 1. Il nero, poi il panorama di Monte Cacioli con le nuvole.
await musica("assets/audio/sera.mp3");
await d.cue("apertura");
await sleep(800);
await p.evaluate(() => { document.getElementById("tr-scritta").style.whiteSpace = "pre-line"; });
await scritta("San Piero a Sieve\nmaggio 2007");
await sleep(2400);
await scritta(null);
await fuoco(vai("via_cafaggio"));
await sleep(3200);
// 2. La Fortezza al tramonto, le impronte.
await d.cue("notte");
await fuoco(vai("fortezza", null, { indizio_regolo: true }));
await scritta("La notte prima del Palio…", true);
await sleep(3000);
await scritta(null);
// 3. La scuola: il palio non c'è più.
await d.cue("sparito");
await fuoco(vai("scuola", [300, 330], { indizio_regolo: false }));
await scritta("…il palio è sparito.", true);
await sleep(3200);
await scritta(null);
// 4. Il paese, a stacchi: Andrea cammina.
await d.cue("paese");
await musica("assets/audio/gara.mp3");
for (const [scene, da, a] of [["piazza", [120, 330], [300, 300]], ["pieve", [40, 330], [260, 300]], ["passeggiata_carza", [330, 352], [420, 250]],
["terrazza", [300, 330], [420, 320]], ["ponte_stazione", [300, 352], [200, 260]]]) {
await fuoco(vai(scene, da), true);
await p.evaluate((a) => game.walkTo(a), a);
await sleep(2100);
}
// 5. Le sfide: un lampo per minigioco.
await d.cue("sfide");
await scritta("Un paese pieno di sfide");
await sleep(1600);
await scritta(null);
const sfida = async (closeup, level, dopo = "") => {
await fuoco(`async () => {
const { settle } = await import('/tests/helpers.mjs');
game.closeup = null; game.queue.length = 0; game.speech = null; game.dialogue = null; game.path = []; game.walking = false; game.task = null;
game.run({ closeup: '${closeup}', level: '${level}' }); settle(game);
${dopo}
}`, true);
await sleep(1300);
};
await sfida("gara", "seria", "const g = await import('/src/engine/gara.js'); while (game.closeup.step === 'intro') g.press(game.closeup);");
await sfida("biliardo", "seria", "const b = await import('/src/engine/biliardo.js'); const c = game.closeup; b.strike(c, 'player', Math.atan2(c.balls.cpu.y - c.balls.player.y, c.balls.cpu.x - c.balls.player.x), 0.8);");
await sfida("rinquarto", "allenamento");
await sfida("micro", "seria", "const m = await import('/src/engine/micro.js'); const c = game.closeup; m.pick(c, 0); m.strike(c, 'player', c.angle, 0.55);");
await sfida("cerbottana", "seria", "const m = await import('/src/engine/cerbottana.js'); const c = game.closeup; c.aim = { x: 320, y: 118 }; c.power = m.BEST; m.shoot(c);");
await sfida("briscola", "seria");
await sfida("basket", "seria", "const b = await import('/src/engine/basket.js'); const c = game.closeup; c.power = 0.62; b.shoot(c);");
// 6. Le vecchie conoscenze, con le loro battute.
await d.cue("amici");
await fuoco(vai("via_liguri_magelli", [330, 300]));
await scritta("…e di vecchie conoscenze", true);
await dice(null, "Di chi è quell'Audi lì?");
await finitoDiDire();
await scritta(null);
await dice("arcu", "Let it be.", "explain");
await finitoDiDire();
await sleep(300);
await dice(null, "Zio, hai problemi?");
await finitoDiDire();
await dice("zio_mario", "I problemi io gli avevo quando andavo a scuola!", "laugh");
await finitoDiDire();
await fuoco(vai("cardetole", [470, 330]), true);
await dice("chiaru", "Vorrai dire un giochino!", "laugh");
await finitoDiDire();
await sleep(400);
// 7. Il buio: la sera, la torcia nella fessura, gli occhi gialli.
await d.cue("buio");
// Sotto il buio una musica bassa di suspense (Andrea: «un po' di musichetta nel finale»).
await musica("assets/audio/sera.mp3");
await fuoco(vai("fortezza", [462, 310], { indizio_regolo: true, casetta_aperta: true }));
await sleep(900);
await p.evaluate(() => { game.state.inventory.push("torcia"); game.act("use", "fessura", "torcia"); });
// «Accendo la torcia e la punto dentro…» si salta: il primo piano si apre subito.
// Il palio non si vede (Andrea: si capirebbe che è in Fortezza): la luce parte da destra, nel buio, e i primi due
// ritrovamenti si danno per fatti; si cercano solo gli occhi.
for (let k = 0; k < 60 && await p.evaluate(() => game.closeup?.id !== "fessura"); k++) { await p.evaluate(() => game.skip()); await sleep(50); }
await p.evaluate(() => { const c = game.closeup; c.found = 2; c.beam = [600, 300]; });
await sleep(1600);
const targets = await p.evaluate(() => game.story.closeups.fessura.targets.map((t) => t.at));
for (let i = 2; i < targets.length; i++) {
if (i === 2) await musica("assets/audio/cartello.mp3");
// La luce va piano verso il punto, come una mano che cerca: prima in basso, poi su verso gli occhi.
const from = await p.evaluate(() => game.closeup.beam);
for (let k = 1; k <= 30; k++) {
const x = from[0] + (targets[i][0] - from[0]) * k / 30 + Math.sin(k / 4) * 12, y = from[1] + (targets[i][1] - from[1]) * k / 30;
await p.evaluate(([x, y]) => game.closeupAim([x, y]), [x, y]);
await sleep(50);
}
// Le battute dei primi ritrovamenti si saltano (partono prima che il ritrovamento conti, e lo fermano); resta solo
// l'ultima: «…sono due occhi gialli!».
for (let k = 0; k < 300 && await p.evaluate((i) => !(game.closeup?.found > i), i); k++) {
if (i < 2) await p.evaluate(() => game.skip());
await sleep(100);
}
await p.waitForFunction(() => !game.speech && !game.closeup?.lines?.length, null, { timeout: 30000 });
await sleep(i === 2 ? 1400 : 100);
}
await nero(true);
await sleep(1400);
// 8. Il titolo.
await d.cue("titolo");
await musica("assets/audio/titolo.mp3");
await p.evaluate(() => {
game.closeup = null;
document.getElementById("title").hidden = false;
document.getElementById("tr-nero").style.transition = "opacity 1.6s";
});
await sleep(300);
await nero(false);
await sleep(7500);
await nero(true);
await sleep(1600);
const audio = await p.evaluate(() => window.__audio);
const out = new URL("./out/", import.meta.url);
mkdirSync(out, { recursive: true });
writeFileSync(new URL("trailer-audio.json", out), JSON.stringify({ eventi: audio }, null, 2));
}