Compare commits

...
21 Commits
Author SHA1 Message Date
manzolo 9f0f60d186 Versione 0.1.57 2026-10-30 11:27:15 +01:00
manzoloandClaude Opus 5.5 5135689293 La campana al campone delle poste col Taglia: il sassolino tirato con mira e forza, i salti a tempo con la lancetta dell'equilibrio, le caselle tinte coi gessi colorati e in cima la CAMPANA, sfondo del vialetto di Codex (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9Quo9nEdXyjXYF8ma
2026-10-30 11:20:33 +01:00
manzolo 8693bff0c7 Versione 0.1.56 2026-10-30 00:59:08 +01:00
manzoloandClaude Opus 5.5 c6812db291 La macchina del tempo da fuori: la partenza giù per Via Liguri Magelli col fulmine e le sgommate di fuoco, e l'arrivo al ritorno dal 1997 (macchina da dietro e davanti di Codex) (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-30 00:52:20 +01:00
manzolo 670626afa5 Versione 0.1.55 2026-10-30 00:44:15 +01:00
manzoloandClaude Opus 5.5 9bf41d01af Lo zio Mario nell'orto: «Zio, a me mi garba Michael Jackson!» «Mai col gesso!» (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-30 00:36:34 +01:00
manzoloandClaude Opus 5.5 16252fb04a Lo zio Mario nell'orto: la vitella di maiale, il Falcione e «No, dormo!» (battute di Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-30 00:34:21 +01:00
manzoloandClaude Opus 5.5 99d4dede80 Lo zio Mario al tramonto nel suo orto; negli orti le nuvole restano fuori dalle fronde che ondeggiano (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-30 00:29:38 +01:00
manzoloandClaude Opus 5.5 36eda83fe4 Vannino, se vinci il pokerino: «Tu m'ha pelato. La prossima volta porta il Barone.» (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
2026-10-30 00:24:34 +01:00
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
119 changed files with 2703 additions and 28 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: 324 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: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+304 -4
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
@@ -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
@@ -1871,6 +1887,38 @@
"file": "andrea-d38b6e49ef.mp3",
"sec": 3.49
},
"andrea|Lo zio Mario nel suo orto, col sole che va giù dietro la casa.": {
"file": "andrea-9309e50d5c.mp3",
"sec": 3.78
},
"zio_mario|Ti s'aspetta a cena, che c'è la vitella di maiale.": {
"file": "zio_mario-39ff0c666f.mp3",
"sec": 2.71
},
"andrea|Non ti avevo visto, pensavo tu fossi andato all'estero.": {
"file": "andrea-f847f224df.mp3",
"sec": 3.35
},
"zio_mario|Io ho girato tanto... il Falcione.": {
"file": "zio_mario-d0d592db52.mp3",
"sec": 2.73
},
"andrea|Zio, che sei stanco?": {
"file": "andrea-21195c6cac.mp3",
"sec": 2.31
},
"zio_mario|No, dormo!": {
"file": "zio_mario-a852e9c43f.mp3",
"sec": 1.19
},
"andrea|Zio, a me mi garba Michael Jackson!": {
"file": "andrea-599b75d7c7.mp3",
"sec": 2.6
},
"zio_mario|Mai col gesso!": {
"file": "zio_mario-5773e3350f.mp3",
"sec": 1.13
},
"andrea|Le case coi balconi pieni di vasi: qui ogni finestra è un giardino.": {
"file": "andrea-ec81875c47.mp3",
"sec": 5.22
@@ -1879,6 +1927,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 +2607,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 +2639,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 +2783,122 @@
"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|E una partita a campana, Taglia?": {
"file": "andrea-595df0a426.mp3",
"sec": 2.23
},
"andrea|Ma quanto sei alto, Taglia?": {
"file": "andrea-01ecdf3ed8.mp3",
"sec": 1.96
},
"andrea|Ci si vede, Taglia.": {
"file": "andrea-5d4d911c06.mp3",
"sec": 1.42
},
"taglia|Io ho già tirato: ora tocca a te. Dodici pallini gialli, e devi fare più di me.": {
"file": "taglia-2658ec8482.mp3",
"sec": 6.05
},
"taglia|Come la vuoi?": {
"file": "taglia-32a067db10.mp3",
"sec": 0.62
},
"taglia|Il cartone sull'albero, le lattine sulla panchina, le bottigliette sulla porta e il palloncino all'altalena.": {
"file": "taglia-670f9b4ef8.mp3",
"sec": 7.15
},
"taglia|E se la vuoi seria, ogni tanto ti tiro una lattina in aria.": {
"file": "taglia-ac012a1066.mp3",
"sec": 4.21
},
"taglia|A campana? Alla nostra età?": {
"file": "taglia-0a7f69770d.mp3",
"sec": 2.77
},
"taglia|Va bene: il gesso ce l'ho in tasca, il sassolino lo scelgo io. Sul vialetto, dalla 1 alla 8, e chi pesta la riga passa la mano.": {
"file": "taglia-c00be84faf.mp3",
"sec": 11.54
},
"taglia|Abbastanza da vedere i pallini arrivare prima di te.": {
"file": "taglia-d99c875252.mp3",
"sec": 2.57
},
"taglia|Alla bona: ti faccio vedere dove arriva il pallino.": {
"file": "taglia-757564b65e.mp3",
"sec": 3.12
},
"taglia|Seria. Occhio alla lattina in aria.": {
"file": "taglia-06abe0c427.mp3",
"sec": 3.06
},
"taglia|A fassi male? Allora tiro più forte, e più spesso.": {
"file": "taglia-4117f6613d.mp3",
"sec": 4.84
},
"taglia|Alla bona: le righe un contano. Comincia te.": {
"file": "taglia-328d930124.mp3",
"sec": 4.12
},
"taglia|Seria: chi tocca la riga è fuori. Comincia te.": {
"file": "taglia-1266463538.mp3",
"sec": 4.23
},
"taglia|A fassi male? Io la campana la facevo a occhi chiusi.": {
"file": "taglia-7938d8d5e4.mp3",
"sec": 5.05
},
"andrea|Si fa una gara alle macchinine?!?!": {
"file": "andrea-17216997e2.mp3",
"sec": 2.74
@@ -2707,10 +2947,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 +3311,22 @@
"file": "bertovo-ca941bee91.mp3",
"sec": 5.41
},
"vannino|Tu m'ha pelato. La prossima volta porta il Barone.": {
"file": "vannino-2267bb5adf.mp3",
"sec": 3.18
},
"pierino_parigi|Le fiches restano al banco. E tu torna quando hai imparato a bluffare.": {
"file": "pierino_parigi-a80d04fe5b.mp3",
"sec": 5.41
},
"vannino|Già finito? E il caffè chi lo paga?": {
"file": "vannino-3307746bb8.mp3",
"sec": 3.98
},
"pierino_parigi|Pari. Si rifà domani sera, alla stess'ora.": {
"file": "pierino_parigi-75a2b84953.mp3",
"sec": 3.8
},
"andrea|Tutti sul muro, verdi in fila. Lo stucco di Pierino un tradisce mai.": {
"file": "andrea-585cd65f93.mp3",
"sec": 5.2
@@ -3087,6 +3339,54 @@
"file": "andrea-c7dcb11e66.mp3",
"sec": 5.52
},
"andrea|Battuto il Taglia! Con la sua Condor, per giunta.": {
"file": "andrea-3ff6547461.mp3",
"sec": 3.73
},
"taglia|Fortuna. Domani la rivinco.": {
"file": "taglia-39627c5b3a.mp3",
"sec": 2.78
},
"andrea|Il Taglia ha la mano ferma. Io ho il fiatone.": {
"file": "andrea-6eabf3443c.mp3",
"sec": 3.83
},
"taglia|Si torna quando vuoi. I pallini gialli un mancano mai.": {
"file": "taglia-c3c705219c.mp3",
"sec": 4.52
},
"andrea|Pari. Si rifà?": {
"file": "andrea-5625b5f1fd.mp3",
"sec": 2.21
},
"taglia|Si rifà.": {
"file": "taglia-18973374a5.mp3",
"sec": 0.76
},
"andrea|La Condor la rendo al Taglia. Per ora.": {
"file": "andrea-aba12c5241.mp3",
"sec": 3.1
},
"andrea|Dalla 1 fino in cima, e ritorno. A ventisett'anni, davanti alle poste.": {
"file": "andrea-68378e4c21.mp3",
"sec": 6.15
},
"taglia|Te lo dico io: se ti vede qualcuno, domani lo sa tutto il paese.": {
"file": "taglia-1e6660932d.mp3",
"sec": 3.34
},
"andrea|Il Taglia, su un piede solo, è più alto di me su due.": {
"file": "andrea-3ed6165062.mp3",
"sec": 3.31
},
"taglia|È questione di gambe lunghe.": {
"file": "taglia-9782816264.mp3",
"sec": 1.71
},
"andrea|Il gesso lo lascio lì. Ci giocherà qualche bambino, che è più in allenamento di me.": {
"file": "andrea-356a1afe8d.mp3",
"sec": 5.91
},
"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.
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.
+6
View File
@@ -31,10 +31,16 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p
| `provinciale-pignone.png`, `piazzetta-cambray-digny.png`, `via-cipriani.png`, `via-massorondinaio.png`, `mozzete.png`, `ponticino-mozzete.png`, `coldaia.png`, `falegnameria-coldaia.png`, `gabbiano.png` | Codex | screenshot di Google Street View | Google | **no**: da rifare con foto di Andrea |
| `closeups/micro-parcheggio.png`, `closeups/micro/*.png` (macchinine) | Codex, solo testo | nessuna | — | sì |
| `characters/chiaru.png` | Codex | `foto/chiaru.jpg` | Andrea (29 ottobre) | sì (il Chiaru consenziente) |
| `characters/taglia.png` | Codex | `foto/taglia.png` | Andrea (29 ottobre) | sì (il Taglia consenziente) |
| `via-liguri-magelli-muro.png` | Codex, ritocco di `via-liguri-magelli.png` con la Ghiacciomobile nel muro | come `via-liguri-magelli.png` | Street View | **no**: come lo sfondo da cui viene |
| `characters/zio_mario.png` | Codex | `foto/zio-mario.png` | Andrea (29 ottobre) | sì (famiglia di Andrea) |
| `terrazza-cardetole.png` | Codex (dalla foto, poi il terrazzo davanti) | `cardetole-finestra.jpg` | Andrea (29 ottobre) | **da decidere**: come `cardetole.png`, dice dove abita Andrea |
| `closeups/muro-cerbottana.png` | Codex, solo testo (stile dalla piazza) | nessuna | — | sì |
| `closeups/macchina-dietro.png`, `macchina-lampi.png`, `macchina-davanti.png` | Codex, dallo sprite della macchina del tempo | nessuna | — | sì |
| `closeups/campone-condor.png` | Codex, dallo sfondo del campone delle poste | nessuna | — | sì |
| `closeups/campone-campana.png` | Codex, solo testo (stile dal campone delle poste) | nessuna | — | sì |
| `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 |
+33
View File
@@ -18,6 +18,39 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
## Dove siamo arrivati
- **La campana al campone delle poste (30 ottobre)**: dal Taglia, «E una partita a campana, Taglia?», poi il livello.
Primo piano `campana` visto dall'alto sul vialetto (`campone-campana.png`, Codex); la campana col gesso la disegna il
codice (`campana.js`, `campana-render.js`): 1-2-3, coppia 4-5, 6, coppia 7-8 e in cima il mezzo cerchio con scritto
CAMPANA (Andrea: non «cielo»), ogni casella tinta tenue col suo gesso. Il sassolino: la mira dà la direzione, tenendo
premuto la forza va e torna, si lascia; deve cadere nella casella che tocca senza toccare le righe (alla bona le
righe non contano e il puntino giallo mostra dove cade). I salti: la lancetta dell'equilibrio attraversa la barra e
si preme nel verde (presto: giù l'altro piede; tardi: pestata la riga); si scavalca la casella col sassolino e
tornando lo si raccoglie. Chi ce la fa passa alla casella dopo e ritira, chi sbaglia passa la mano; il Taglia gioca
da solo (in simulazione gli servono circa 22, 10 e 6 turni secondo il livello). Vince chi finisce la 8 (flag
`campana_vinta`). Test `campana.test.mjs`, prova `npm run browser:campana`. [DA ANDREA] le battute.
- **La macchina del tempo da fuori (30 ottobre)**: dopo il cruscotto, la partenza giù per Via Liguri Magelli
(primo piano `corsa`, `src/engine/corsa.js`): accelera, il fulmine la prende, il bianco, e restano due sgommate di
fuoco che bruciano e si spengono; al ritorno dal 1997 l'arrivo: il fulmine in fondo alla strada, lei viene avanti e
ci passa accanto lasciando le strisce. La macchina da dietro e davanti (coi lampi) è di Codex.
- **Lo zio Mario negli orti al tramonto (30 ottobre)**: dal tramonto (`indizio_regolo`) non è più davanti alla
Ghiacciomobile ma accanto al suo orto (`orti_zio_mario`), con una battuta provvisoria [DA ANDREA].
- **La Condor al campone delle poste (29 ottobre)**: la gara col Taglia (Andrea Tagliaferri, consenso dato) con la
pistola ad aria compressa coi pallini gialli (`condor.js`, `condor-render.js`, primo piano `condor`, sfondo di Codex).
Come la cerbottana: il mirino ondeggia col respiro; tenere premuto trattiene il fiato (il mirino si ferma), lasciare
spara, troppo e il colpo parte da solo. Il pallino cala con la distanza (alla porta in fondo si mira sopra). Il
cartone sull'albero (10/5/2), le lattine sulla panchina (3), le bottigliette sulla traversa (5), il palloncino
all'altalena (6), la lattina tirata in aria dal Taglia a seria e a fassi male (8). Si deve battere il punteggio del
Taglia (22, 34, 44 secondo il livello, ±3). Il Taglia sta al campone (sprite di Codex dalla sua foto, più alto di Andrea); si gareggia
parlandogli. [DA ANDREA] le battute.
- **La macchina del tempo e il pokerino del 1997 (29 ottobre)**: dall'Arcu sotto la tenda del Trappola, «Bei tempi
quando qui si faceva i pokerini…»: compare la macchina del tempo in Via Liguri Magelli. Dentro, il cruscotto (Codex)
compone 10/07/1997 18:30, poi tremori, lampi e il bianco (primo piano `viaggio`, `src/engine/viaggio.js`, tempi in
`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
+39
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,28 @@
il pulsante alterna Recupera e Molla. Sei lanci. La gloria: 2 pesci alla bona, 3 seria, 4 a fassi male (lo stivale
non conta, e non pesa). Frecce ← →: distanza del lancio; Esc esce.</p>
</details>
<details id="condor-help" hidden><summary>Come si gioca</summary>
<p>La Condor, la pistola ad aria compressa coi pallini gialli, al campone delle poste: dodici pallini, e il Taglia ha
già tirato. Il suo punteggio è in alto: per vincere bisogna fare di più. Il cartone sull'albero vale 10, 5 o 2 punti
secondo il cerchio, ogni lattina sulla panchina 3, ogni bottiglietta sulla traversa della porta 5, il palloncino
legato all'altalena 6, la lattina che il Taglia tira in aria (Seria e A fassi male) 8.</p>
<p>Muovi il mouse o tocca il campo per mirare: il mirino ondeggia col respiro (di più salendo di livello). Tieni
premuto per trattenere il fiato: il mirino si ferma (diventa verde); lascia per sparare. Se lo trattieni troppo il
fiato finisce, il mirino trema e il colpo parte da solo. Il pallino cala con la distanza: alla porta in fondo si
mira un po' più sopra; alla bona il puntino giallo segna dove arriva. Il palloncino e la lattina in aria si
muovono: si mira dove saranno. Si può anche premere Spara, senza trattenere il fiato. Frecce per spostare il mirino.</p>
</details>
<details id="campana-help" hidden><summary>Come si gioca</summary>
<p>La campana col gesso sul vialetto del campone, a turni col Taglia. Si tira il sassolino nella casella che tocca,
dalla 1 alla 8: deve cadere dentro, senza toccare le righe (alla bona le righe non contano). Poi si salta tutta la
campana fino in cima (il mezzo cerchio) e indietro, su un piede nelle caselle sole e a gambe larghe nella 4-5 e nella 7-8, senza mai
mettere il piede dove sta il sassolino; tornando lo si raccoglie dalla casella prima. Chi ce la fa passa alla casella
dopo e ritira; chi sbaglia passa la mano. Vince chi finisce la 8.</p>
<p>Il tiro: muovi il mouse o tocca la campana per mirare, tieni premuto per la forza (sale e scende) e lascia; oppure
regola la forza e premi Tira. Alla bona il puntino giallo segna dove cade. Frecce su e giù per la mira, destra e
sinistra per la forza. I salti: la lancetta dell'equilibrio attraversa la barra; premi (o Spazio, o Salta) quando è
nel verde. Troppo presto metti giù l'altro piede, troppo tardi pesti la riga.</p>
</details>
<details id="cerbottana-help" hidden><summary>Come si gioca</summary>
<p>La cerbottana, con lo stucco verde di Pierin di Frilli: dodici pallini contro il muro della piazza. Il bersaglio
di gesso vale 10, 5 o 2 punti secondo il cerchio, ogni lattina sul muretto 3, il barattolo appeso che dondola (Seria
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "leggenda-del-regolo",
"version": "0.1.52",
"version": "0.1.57",
"private": true,
"type": "module",
"scripts": {
@@ -10,6 +10,8 @@
"browser:micro": "node tests/micro.browser.mjs",
"browser:gara": "node tests/gara.browser.mjs",
"browser:cerbottana": "node tests/cerbottana.browser.mjs",
"browser:condor": "node tests/condor.browser.mjs",
"browser:campana": "node tests/campana.browser.mjs",
"browser:tressette": "node tests/tressette.browser.mjs",
"browser:scopa": "node tests/scopa.browser.mjs",
"browser:briscola": "node tests/briscola.browser.mjs",
+141
View File
@@ -0,0 +1,141 @@
import { W, CY, START, CIELO, FROM, BOXES, LAST, FLIGHT, HOP, center, landing, needle, canAdjust } from './campana.js';
import { text, panel } from './bocce-render.js';
function rect(ctx, x, y, w, h, color) { ctx.fillStyle = color; ctx.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h)); }
// Il gesso: tratti a puntini un po' storti, con dei buchi (sempre gli stessi, da un piccolo hash: non tremano).
const hash = n => { const s = Math.sin(n * 127.1 + 311.7) * 43758.5453; return s - Math.floor(s); };
function chalkLine(ctx, x1, y1, x2, y2, seed) {
const n = Math.max(2, Math.round(Math.hypot(x2 - x1, y2 - y1)));
for (let i = 0; i <= n; i++) {
const k = i / n, h = hash(seed * 1000 + i);
if (h < 0.07) continue;
ctx.fillStyle = h > 0.8 ? 'rgba(255,255,250,0.95)' : 'rgba(240,238,228,0.78)';
ctx.fillRect(Math.round(x1 + (x2 - x1) * k + (hash(seed + i) - 0.5)), Math.round(y1 + (y2 - y1) * k + (hash(seed * 7 + i) - 0.5)), 2, 2);
}
}
// I gessi colorati: ogni casella ripassata tenue col suo colore, a puntini come il gesso sull'asfalto (idea di Andrea).
const TINTS = { 1: [244, 160, 180], 2: [250, 222, 120], 3: [150, 200, 240], 4: [160, 220, 150], 5: [200, 170, 230], 6: [250, 180, 120], 7: [140, 220, 210], 8: [240, 150, 150], top: [255, 240, 200] };
function tint(ctx, [r, g, b], x, y, w, h, seed, inside = () => true) {
ctx.fillStyle = `rgba(${r},${g},${b},0.16)`;
for (let yy = y; yy < y + h; yy += 2) for (let xx = x; xx < x + w; xx += 2) {
if (!inside(xx, yy)) continue;
const k = hash(seed * 7777 + xx * 13 + yy * 7);
if (k < 0.18) continue; // l'asfalto che spunta fra il gesso
ctx.fillStyle = `rgba(${r},${g},${b},${k > 0.85 ? 0.42 : 0.24})`; ctx.fillRect(xx, yy, 2, 2);
}
}
// Il gesso non cambia mai: tinte e righe si disegnano una volta su una tela a parte (a puntini, a ogni fotogramma,
// rallentavano la pagina). Fuori dal browser (nei test) si disegnano direttamente.
let layer = null;
function chalkwork(ctx) {
let s = 1;
for (const [id, b] of Object.entries(BOXES)) tint(ctx, TINTS[id], b.x + 1, b.y + 1, W - 2, W - 2, Number(id));
tint(ctx, TINTS.top, CIELO.x + 1, CY - CIELO.r, CIELO.r, CIELO.r * 2, 9, (x, y) => Math.hypot(x + 1 - CIELO.x, y + 1 - CY) < CIELO.r - 2);
for (const b of Object.values(BOXES)) {
chalkLine(ctx, b.x, b.y, b.x + W, b.y, s++); chalkLine(ctx, b.x, b.y + W, b.x + W, b.y + W, s++);
chalkLine(ctx, b.x, b.y, b.x, b.y + W, s++); chalkLine(ctx, b.x + W, b.y, b.x + W, b.y + W, s++);
}
// In cima la campana: il mezzo cerchio in fondo, dove ci si gira.
for (let i = 0; i < 40; i++) {
const a1 = -Math.PI / 2 + i / 40 * Math.PI, a2 = -Math.PI / 2 + (i + 1) / 40 * Math.PI;
chalkLine(ctx, CIELO.x + Math.cos(a1) * CIELO.r, CY + Math.sin(a1) * CIELO.r, CIELO.x + Math.cos(a2) * CIELO.r, CY + Math.sin(a2) * CIELO.r, 200 + i);
}
// La riga di partenza.
chalkLine(ctx, START - 14, CY - W, START - 14, CY + W, 300);
}
function grid(ctx, c) {
if (typeof document === 'undefined') chalkwork(ctx);
else {
if (!layer) { layer = document.createElement('canvas'); layer.width = 640; layer.height = 360; chalkwork(layer.getContext('2d')); }
ctx.drawImage(layer, 0, 0);
}
// Il numero in alto nella casella: il mezzo resta libero per il sassolino e i piedi. Quella che tocca ha il bordo giallo.
for (const [id, b] of Object.entries(BOXES)) {
const p = center(id), next = Number(id) === c.target[c.current] && c.step !== 'result';
if (next) { ctx.strokeStyle = 'rgba(255,236,140,0.55)'; ctx.lineWidth = 2; ctx.strokeRect(b.x + 4, b.y + 4, W - 8, W - 8); }
text(ctx, id, p.x, p.y - 17, 14, next ? '#fff3a8' : 'rgba(245,243,232,0.88)', 'center');
}
text(ctx, 'CAMPANA', CIELO.x + W * 0.53, CY - 26, 8, 'rgba(245,243,232,0.88)', 'center');
}
// Il sassolino: grigio, col lume in alto; in volo, più su e con l'ombra sotto.
function stone(ctx, x, y, lift = 0) {
ctx.fillStyle = 'rgba(30,25,20,0.35)'; ctx.beginPath(); ctx.ellipse(x + 1, y + 2, 5, 3, 0, 0, Math.PI * 2); ctx.fill();
const s = 1 + lift / 60, yy = y - lift;
ctx.fillStyle = '#4a4744'; ctx.beginPath(); ctx.ellipse(x, yy, 5 * s, 4 * s, 0.3, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#8d8984'; ctx.beginPath(); ctx.ellipse(x - 0.5, yy - 0.5, 4 * s, 3 * s, 0.3, 0, Math.PI * 2); ctx.fill();
rect(ctx, x - 2 * s, yy - 2 * s, 2, 1, '#cfcac2');
}
// Dove stanno i piedi: a gambe larghe nelle coppie e nel cielo, su un piede nelle caselle sole, alla partenza tutti e due.
function feet(beat) {
if (!beat || beat.out) return [{ x: FROM.x + 2, y: CY - 9 }, { x: FROM.x + 2, y: CY + 9 }];
if (beat.boxes[0] === 'cielo') { const p = center('cielo'); return [{ x: p.x, y: p.y - 12 }, { x: p.x, y: p.y + 12 }]; }
return beat.boxes.map(id => center(id));
}
// Una scarpa vista dall'alto: la punta verso dove si va.
function shoe(ctx, x, y, dir, scale, [sole, upper, lace]) {
ctx.save(); ctx.translate(Math.round(x), Math.round(y)); ctx.scale(dir * scale, scale);
ctx.fillStyle = 'rgba(30,25,20,0.3)'; ctx.fillRect(-9, -3, 21, 9);
ctx.fillStyle = sole; ctx.fillRect(-10, -5, 20, 10); ctx.fillRect(10, -4, 2, 8);
ctx.fillStyle = upper; ctx.fillRect(-9, -4, 18, 8);
ctx.fillStyle = lace; ctx.fillRect(-1, -2, 7, 1); ctx.fillRect(-1, 1, 7, 1);
ctx.fillStyle = '#2a2420'; ctx.fillRect(-8, -2, 4, 4); // la caviglia, il buco della scarpa
ctx.restore();
}
const SHOES = { player: ['#e8e6e0', '#3d6fb8', '#ffffff'], cpu: ['#2b2b2b', '#b8322a', '#f0d8a8'] };
function walker(ctx, c) {
const beat = c.beats?.[c.beat], prev = c.beat > 0 ? c.beats[c.beat - 1] : null, now = feet(c.beat >= 0 ? beat : null);
const k = c.hop ? Math.min(1, c.hop.t / HOP) : 1, from = feet(c.beat > 0 ? prev : null);
const mid = list => list.reduce((a, p) => ({ x: a.x + p.x / list.length, y: a.y + p.y / list.length }), { x: 0, y: 0 });
const a = mid(from), b = mid(now), off = { x: (a.x - b.x) * (1 - k), y: (a.y - b.y) * (1 - k) };
const scale = (c.current === 'cpu' ? 1.45 : 1.3) * (1 + 0.35 * Math.sin(Math.PI * k));
const dir = beat?.back ? -1 : 1;
for (const p of now) shoe(ctx, p.x + off.x, p.y + off.y, dir, scale, SHOES[c.current]);
}
export function drawCampana(ctx, c, def, bg) {
ctx.save(); ctx.imageSmoothingEnabled = false;
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#9a948a'; ctx.fillRect(0, 0, 640, 360); }
grid(ctx, c);
if (c.stone) stone(ctx, c.stone.x, c.stone.y);
walker(ctx, c);
if (c.shot) {
const k = Math.min(1, c.shot.t / FLIGHT), { from, to } = c.shot;
stone(ctx, from.x + (to.x - from.x) * k, from.y + (to.y - from.y) * k, 40 * Math.sin(Math.PI * k));
}
// La mira: un cerchietto di gesso dove si punta; alla bona, il puntino dove cade il sassolino con questa forza.
if (canAdjust(c)) {
ctx.strokeStyle = '#fff8dc'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.arc(c.aim.x, c.aim.y, 6, 0, Math.PI * 2); ctx.stroke();
if (c.skill.guide) { const l = landing(c); rect(ctx, l.x - 2, l.y - 2, 4, 4, '#f5d02a'); rect(ctx, l.x - 1, l.y - 1, 2, 2, '#7a5a10'); }
// La forza: la barra in basso a sinistra.
const x = 16, y = 296, h = 50, f = Math.round(h * c.power);
rect(ctx, x - 3, y - 3, 18, h + 6, '#17160fd0');
rect(ctx, x, y + h - f, 12, f, c.holding ? '#ffd23c' : '#d9ccba');
text(ctx, 'FORZA', x + 6, y - 9, 6, '#efd49e', 'center');
}
// L'equilibrio: la lancetta attraversa la barra; si salta quando è nel verde.
if (c.step === 'hop' && c.meter) {
const x = 220, y = 318, w = 200, zone = c.skill.zone * w / 2, u = needle(c);
rect(ctx, x - 4, y - 14, w + 8, 30, '#17160fd8');
rect(ctx, x, y, w, 8, '#7a3a2a'); rect(ctx, x + w / 2 - zone, y, zone * 2, 8, '#5fae4a');
rect(ctx, x + w / 2 + u * w / 2 - 1, y - 3, 3, 14, '#fff3a8');
text(ctx, 'EQUILIBRIO', x + w / 2, y - 7, 6, '#efd49e', 'center');
}
// In alto: a che casella è ciascuno, di chi è il turno, cosa è successo.
rect(ctx, 0, 0, 640, 22, '#17160fe8');
text(ctx, 'LA CAMPANA', 8, 7, 8, '#efd49e');
text(ctx, { bona: 'alla bona', seria: 'seria', male: 'a fassi male' }[c.level], 8, 16, 7, '#d9ccba');
const at = n => n > LAST ? 'finita' : `alla ${n}`;
text(ctx, `Andrea ${at(c.target.player)}`, 220, 11, 9, c.current === 'player' ? '#fff0c8' : '#a8a08c', 'center');
text(ctx, `Il Taglia ${at(c.target.cpu)}`, 360, 11, 9, c.current === 'cpu' ? '#9fd0ff' : '#7f95a8', 'center');
text(ctx, c.current === 'player' ? 'tocca a te' : 'tocca al Taglia', 560, 11, 8, '#d9ccba', 'center');
if (c.line) { rect(ctx, 150, 24, 340, 14, '#17160fd0'); text(ctx, c.line, 320, 31, 8, /riga|fuori|Troppo|equilibrio|piede|non nella/i.test(c.line) ? '#ffbf9f' : '#9fd0ff', 'center'); }
if (c.step === 'result') {
const won = c.result.winner === 'player';
panel(ctx, 170, 140, 300, 76);
text(ctx, won ? 'BATTUTO IL TAGLIA!' : 'HA VINTO IL TAGLIA', 320, 160, 12, won ? '#b9e5b3' : '#efd49e', 'center');
text(ctx, won ? 'Tutta la campana, dalla 1 alla 8.' : `Tu eri arrivato alla ${c.target.player}.`, 320, 182, 8, '#efd49e', 'center');
text(ctx, 'Torna al campone per uscire.', 320, 200, 8, '#d9ccba', 'center');
}
ctx.restore();
}
+220
View File
@@ -0,0 +1,220 @@
// La campana al campone delle poste (idea di Andrea, 30 ottobre): disegnata col gesso sul vialetto, a turni col Taglia.
// Si tira il sassolino nella casella che tocca (dalla 1 alla 8): se cade dentro, senza toccare le righe, si salta tutta
// la campana fino in cima e indietro, su un piede nelle caselle sole e a gambe larghe nelle coppie, senza mai mettere
// il piede dove sta il sassolino; tornando, lo si raccoglie dalla casella prima. Chi sbaglia (sassolino fuori o sulla
// riga, salto fuori tempo) passa la mano; chi ce la fa va avanti alla casella dopo e ritira. Vince chi finisce la 8.
// Il tiro: la mira dà la direzione, tenere premuto carica la forza (va e torna), lasciare tira. I salti: a ogni salto
// la lancetta dell'equilibrio attraversa la barra; si preme quando passa nel verde. Unità: i pixel del primo piano
// 640×360, visto dall'alto (sfondo di Codex: assets/closeups/campone-campana.png).
export const W = 66, CY = 181, START = 80, CIELO = { x: START + 6 * W, r: W }, FROM = { x: 40, y: CY }, REACH = 520;
export const TAP = 0.14, CHARGE = 1.3, FLIGHT = 0.55, HOP = 0.22, REST = 1.6, DONE = 1.1, CPU_WAIT = 0.9, CPU_BEAT = 0.42;
// Le caselle: 1, 2, 3 da sole; 4 e 5 in coppia (la 4 a sinistra di chi salta, cioè in alto); la 6; 7 e 8 in coppia;
// in fondo la campana (il mezzo cerchio dove ci si gira; nel codice «cielo», come la chiamano in altri posti).
export const BOXES = {
1: { x: START, y: CY - W / 2 }, 2: { x: START + W, y: CY - W / 2 }, 3: { x: START + 2 * W, y: CY - W / 2 },
4: { x: START + 3 * W, y: CY - W }, 5: { x: START + 3 * W, y: CY }, 6: { x: START + 4 * W, y: CY - W / 2 },
7: { x: START + 5 * W, y: CY - W }, 8: { x: START + 5 * W, y: CY },
};
export const GROUPS = [[1], [2], [3], [4, 5], [6], [7, 8], ['cielo']];
export const LAST = 8;
// I livelli: quanto si sparpaglia il sassolino (px), se si vede dove cade, quanto è larga la riga che non si può
// toccare, quanto ci mette la lancetta dell'equilibrio e quanto è largo il verde; il Taglia: quante volte il sassolino
// va dove vuole e quanto spesso, a ogni salto, mette giù il piede.
export const LEVELS = {
bona: { scatter: 4, guide: true, line: 0, sweep: 1.25, zone: 0.32, cpu: { throw: 0.5, slip: 0.045 } },
seria: { scatter: 7, guide: false, line: 3, sweep: 1, zone: 0.2, cpu: { throw: 0.7, slip: 0.025 } },
male: { scatter: 9, guide: false, line: 4, sweep: 0.8, zone: 0.15, cpu: { throw: 0.8, slip: 0.018 } },
};
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
export function center(id) {
if (id === 'cielo') return { x: CIELO.x + W * 0.42, y: CY };
const b = BOXES[id]; return { x: b.x + W / 2, y: b.y + W / 2 };
}
// In che casella cade un punto, e se tocca la riga (entro `tol` pixel da un bordo).
export function boxAt([x, y], tol = 0) {
for (const [id, b] of Object.entries(BOXES)) {
if (x < b.x || x > b.x + W || y < b.y || y > b.y + W) continue;
return { id: Number(id), line: Math.min(x - b.x, b.x + W - x, y - b.y, b.y + W - y) < tol };
}
const d = Math.hypot(x - CIELO.x, y - CY);
if (x >= CIELO.x && d <= CIELO.r) return { id: 'cielo', line: Math.min(x - CIELO.x, CIELO.r - d) < tol };
return null;
}
// I salti per la casella `n`: all'andata tutte le caselle, saltando quella col sassolino (nella coppia si mette un
// piede solo nell'altra); il cielo; al ritorno, dalla casella prima si raccoglie il sassolino; poi fuori.
export function beats(n) {
const step = (g, back) => g.includes(n) ? (g.length > 1 ? [{ boxes: g.filter(k => k !== n), back }] : []) : [{ boxes: g, back }];
const out = GROUPS.flatMap(g => step(g, false));
for (let i = GROUPS.length - 2; i >= 0; i--) {
if (GROUPS[i].includes(n)) out.push({ ...out[out.length - 1], back: true, pick: true });
out.push(...step(GROUPS[i], true));
}
out.push({ boxes: [], back: true, out: true });
return out;
}
export function newCampana(seed = Date.now() >>> 0, def = {}) {
const level = LEVELS[def.level] ? def.level : 'seria';
return { step: 'aim', t: 0, seed, level, skill: LEVELS[level], current: 'player', target: { player: 1, cpu: 1 },
aim: center(1), power: 0.2, holding: false, holdTime: 0, charging: false, stone: null, shot: null,
beats: null, beat: -1, meter: null, hop: null, line: null, next: null, events: [], visual: { time: 0 } };
}
export const canThrow = c => c.current === 'player' && (c.step === 'aim' || c.step === 'hop');
export const canAdjust = c => c.current === 'player' && c.step === 'aim';
export const aimFree = c => !canAdjust(c);
// Dove cade il sassolino tirato con questa forza, senza lo sparpaglio.
export function landing(c, power = c.power) {
const dx = c.aim.x - FROM.x, dy = c.aim.y - FROM.y, d = Math.hypot(dx, dy) || 1;
return { x: FROM.x + dx / d * power * REACH, y: FROM.y + dy / d * power * REACH };
}
function toss(c, to) {
c.shot = { from: { ...FROM }, to, t: 0 }; c.stone = null;
c.step = 'flying'; c.line = null;
c.events.push({ kind: 'campana-lancio', power: 0.6 });
}
function throwStone(c) {
const p = landing(c), s = c.skill.scatter, spread = () => (random(c) + random(c) + random(c) - 1.5) * s * 1.4;
toss(c, { x: p.x + spread(), y: p.y + spread() });
return true;
}
const who = c => c.current === 'cpu' ? 'Il Taglia: ' : '';
const cap = line => line[0].toUpperCase() + line.slice(1);
// Il sassolino è arrivato: nella casella giusta si salta, se no passa la mano.
function land(c) {
const to = c.shot.to, n = c.target[c.current], hit = boxAt([to.x, to.y], c.skill.line);
c.shot = null; c.stone = to;
c.events.push({ kind: 'campana-sasso', power: 0.8 });
if (hit && hit.id === n && !hit.line) {
c.line = cap(`${who(c)}il sassolino è nella ${n}. Salta!`);
c.beats = beats(n); c.beat = -1; c.hop = null; c.t = 0;
c.step = c.current === 'cpu' ? 'cpuHop' : 'hop';
if (c.step === 'hop') c.meter = { t: -0.5, dir: 1 };
return;
}
fail(c, `${who(c)}${hit?.line ? 'sulla riga!' : hit ? `nella ${hit.id === 'cielo' ? 'campana in cima' : hit.id}, non nella ${n}.` : 'fuori dalla campana.'}`);
}
function fail(c, line) {
c.line = cap(line); c.step = 'rest'; c.t = 0; c.meter = null;
c.next = c.current === 'player' ? 'cpu' : 'player';
c.events.push({ kind: 'campana-riga', power: 0.7 });
}
// Un salto riuscito: la casella dopo, o il sassolino raccolto, o fuori dalla campana (e la casella è fatta).
function advance(c) {
c.beat++;
const b = c.beats[c.beat];
c.hop = { t: 0 };
if (b.pick) { c.stone = null; c.line = cap(`${who(c)}preso il sassolino!`); c.events.push({ kind: 'campana-sasso', power: 0.5 }); }
else c.events.push({ kind: 'campana-salto', power: 0.6 });
if (b.out) {
const n = c.target[c.current]++;
if (n >= LAST) {
c.step = 'result'; c.meter = null;
c.result = { winner: c.current, player: c.target.player - 1, cpu: c.target.cpu - 1 };
return;
}
c.line = cap(`${who(c)}fatta la ${n}! Ora la ${n + 1}.`); c.step = 'done'; c.t = 0; c.meter = null;
return;
}
if (c.meter) c.meter = { t: 0, dir: -c.meter.dir };
}
// La lancetta dell'equilibrio, da -1 a 1 (o al contrario, un salto sì e uno no).
export function needle(c) {
if (!c.meter) return 0;
return c.meter.dir * (-1 + 2 * clamp(c.meter.t, 0, c.skill.sweep) / c.skill.sweep);
}
function jump(c) {
if (c.hop && c.hop.t < HOP) return true; // ancora in aria: il tocco non conta
const u = needle(c);
if (Math.abs(u) <= c.skill.zone) { advance(c); return true; }
const early = u * c.meter.dir < 0;
fail(c, early ? 'Troppo presto: giù l\'altro piede!' : 'Troppo tardi: pestata la riga!');
return true;
}
// Tocca all'altro, o allo stesso per la casella dopo: dalla partenza, senza sassolino.
function turn(c, current) {
Object.assign(c, { current, stone: null, beats: null, beat: -1, hop: null, meter: null, t: 0, next: null,
step: current === 'player' ? 'aim' : 'cpuAim' });
if (current === 'player') c.aim = center(c.target.player);
}
export function update(c, dt) {
if (!Number.isFinite(dt) || dt <= 0) return;
dt = Math.min(dt, 0.25); c.t += dt; c.visual.time += dt;
if (c.hop) c.hop.t += dt;
if (c.holding) {
c.holdTime += dt; c.charging = c.holdTime >= TAP;
const k = (c.holdTime / CHARGE) % 2; c.power = k <= 1 ? k : 2 - k;
}
if (c.step === 'flying') { c.shot.t += dt; if (c.shot.t >= FLIGHT) land(c); }
else if (c.step === 'hop') {
if (!c.hop || c.hop.t >= HOP) c.meter.t += dt;
if (c.meter.t > c.skill.sweep) fail(c, 'Hai perso l\'equilibrio!');
} else if (c.step === 'cpuAim' && c.t >= CPU_WAIT) {
// Il Taglia: o nella casella, ben dentro, o mezza casella più in là (sulla riga o nell'altra).
const n = c.target.cpu, p = center(n), ok = random(c) < c.skill.cpu.throw, j = () => (random(c) - 0.5) * 20;
toss(c, ok ? { x: p.x + j(), y: p.y + j() } : { x: p.x + (random(c) < 0.5 ? -1 : 1) * W * (0.5 + random(c) * 0.3), y: p.y + j() });
} else if (c.step === 'cpuHop' && c.t >= CPU_BEAT) {
c.t = 0;
if (random(c) < c.skill.cpu.slip) fail(c, random(c) < 0.5 ? 'Il Taglia ha pestato la riga!' : 'Il Taglia ha messo giù l\'altro piede!');
else advance(c);
} else if (c.step === 'rest' && c.t >= REST) turn(c, c.next);
else if (c.step === 'done' && c.t >= DONE) turn(c, c.current);
}
// --- Comandi: la mira segue il mouse o il dito; tenere premuto carica la forza, lasciare tira; ai salti si preme.
export function aimScreen(c, [x, y]) {
if (!canAdjust(c) || c.holding || !Number.isFinite(x) || !Number.isFinite(y)) return false;
c.aim = { x: clamp(x, FROM.x + 30, 630), y: clamp(y, 80, 285) }; return true;
}
export function setPower(c, v) { if (!canAdjust(c) || c.holding) return false; c.power = clamp(v, 0, 1); return true; }
export function nudge(c, dir) { if (!canAdjust(c) || c.holding) return false; c.aim.y = clamp(c.aim.y + dir * 3, 80, 285); return true; }
// Frecce: su e giù la mira, destra e sinistra la forza (più in là, più in qua).
export function key(c, key, down) {
const move = { ArrowUp: [0, -3], ArrowDown: [0, 3], ArrowLeft: [-0.01, 0], ArrowRight: [0.01, 0] }[key];
if (!move) return false;
if (down && canAdjust(c) && !c.holding) { c.aim.y = clamp(c.aim.y + move[1], 80, 285); c.power = clamp(c.power + move[0], 0, 1); }
return true;
}
export function press(c, pointerId) {
if (c.current !== 'player') return false;
if (c.step === 'hop') return jump(c);
if (!canAdjust(c) || c.holding) return false;
c.pointerId = pointerId; c.holding = true; c.charging = false; c.holdTime = 0; c.power = 0;
return true;
}
export function cancel(c, pointerId) {
if (pointerId !== undefined && c.pointerId !== pointerId) return false;
if (!c.holding) return false;
c.holding = c.charging = false; c.holdTime = 0; return true;
}
export function release(c, pointerId) {
if (c.pointerId !== pointerId || !c.holding) return false;
if (c.holdTime < TAP) return cancel(c, pointerId); // il tocco secco mira soltanto
c.holding = c.charging = false; c.holdTime = 0;
return throwStone(c);
}
// Il pulsante: tira con la forza del cursore, o salta.
export function shoot(c) {
if (c.current !== 'player' || c.holding) return false;
if (c.step === 'hop') return jump(c);
return canAdjust(c) && throwStone(c);
}
const box = n => n > LAST ? 'finito' : `alla ${n}`;
export function button(c) {
if (c.step === 'result') return 'Finita';
if (c.current === 'cpu') return 'Tocca al Taglia…';
return { aim: 'Tira', flying: 'In volo…', hop: 'Salta' }[c.step] ?? 'Aspetta…';
}
export function status(c) {
const score = `Andrea ${box(c.target.player)}, il Taglia ${box(c.target.cpu)}.`;
if (c.step === 'result') return `${c.result.winner === 'player' ? 'Campana finita: battuto il Taglia! ' : 'Il Taglia ha finito la campana. '}${score}`;
const hint = c.current === 'cpu' ? 'Tocca al Taglia.' : c.step === 'hop' ? 'Salta quando la lancetta è nel verde.'
: `Tira il sassolino nella ${c.target.player}: mira, tieni premuto per la forza e lascia.`;
return `${score} ${c.line ?? hint}`;
}
+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.'}`;
}
+121
View File
@@ -0,0 +1,121 @@
// La macchina del tempo vista da fuori (story.closeups, type 'corsa'; Andrea, 30 ottobre). In partenza ('parti') va via
// giù per la strada accelerando, il fulmine la prende, il bianco, e restano due sgommate di fuoco che bruciano e si
// spengono. All'arrivo ('arriva') il fulmine in fondo alla strada, e lei viene avanti e ci passa accanto, lasciando le
// stesse strisce (davanti a noi: dietro alla macchina non si vedrebbero). Sopra lo sfondo della scena (`scene`), coi fotogrammi di Codex (`frames`: dietro, dietro coi lampi,
// davanti coi lampi). Unità: i pixel della scena 640×360.
// I tempi, in secondi. Partenza: ferma che si carica, la corsa, il fulmine, il bianco, le fiamme, la fine.
export const PARTI = { charge: 0.6, run: 2.2, bolt: 2.3, flash: 0.35, burn: 4.6, end: 5.2 };
// Arrivo: il fulmine in fondo, la corsa verso di noi fin fuori dal quadro, le fiamme, la fine.
export const ARRIVA = { bolt: 0.3, flash: 0.35, run: 1.5, burn: 4, end: 4.6 };
const lerp = (a, b, k) => a + (b - a) * k;
const clamp01 = k => Math.max(0, Math.min(1, k));
// Dove sta la macchina a un punto `k` del percorso (0 vicina, 1 in fondo): posizione delle ruote e grandezza.
export function along(def, k) {
const [a, b] = def.path;
return { x: lerp(a[0], b[0], k), y: lerp(a[1], b[1], k), scale: lerp(a[2], b[2], k) };
}
// Il punto del percorso al tempo t: in partenza accelera (da fermo); all'arrivo viene avanti dal fondo fin oltre il
// punto 0 (fuori dal quadro, accanto a noi).
export function progress(c, def) {
if (def.mode === 'arriva') return 1 - clamp01((c.t - ARRIVA.bolt) / ARRIVA.run);
const k = clamp01((c.t - PARTI.charge) / (PARTI.run - PARTI.charge));
return Math.min(0.92, k * k);
}
// Si vede la macchina? In partenza fino al fulmine; all'arrivo dal fulmine in poi.
export function carShown(c, def) {
return def.mode === 'arriva' ? c.t >= ARRIVA.bolt + 0.05 && c.t < ARRIVA.bolt + ARRIVA.run : c.t < PARTI.bolt + 0.05;
}
// Dove arriva il fulmine: sulla macchina in partenza, in fondo alla strada all'arrivo.
export function boltAt(def) { return def.mode === 'arriva' ? along(def, 1) : along(def, progress({ t: PARTI.bolt }, def)); }
const T = def => def.mode === 'arriva' ? ARRIVA : PARTI;
export function newCorsa(id) { return { id, t: 0, age: 0, events: [] }; }
// Il tempo che passa e i suoni; vero quando è finita.
export function updateCorsa(c, def, dt) {
c.t += dt;
const times = T(def);
if (def.mode !== 'arriva' && !c.charge && c.t >= 0.1) { c.charge = true; c.events.push({ kind: 'gara-turbo', power: 1 }); }
if (!c.boom && c.t >= times.bolt) { c.boom = true; c.events.push({ kind: 'corsa-tuono', power: 1 }); }
if (!c.fire && c.t >= (def.mode === 'arriva' ? ARRIVA.bolt + 0.2 : PARTI.charge + 0.3)) { c.fire = true; c.events.push({ kind: 'corsa-fuoco', power: 1 }); }
return c.t >= times.end;
}
// Le due ruote posteriori (o anteriori) a un punto del percorso: una a sinistra e una a destra.
function wheels(def, k) {
const p = along(def, k), half = def.track * p.scale;
return [[p.x - half, p.y], [p.x + half, p.y]];
}
// Le sgommate di fuoco: da dove è partita a dove è arrivata, fiammelle che tremano, più piccole in fondo, che si
// spengono dopo `burn`.
function fire(ctx, c, def) {
const times = T(def), shown = def.mode === 'arriva' ? [Math.max(progress(c, def), 0), 1] : [0, progress({ t: Math.min(c.t, times.bolt) }, def)];
const fade = c.t < times.burn ? 1 : clamp01(1 - (c.t - times.burn) / (times.end - times.burn));
if (fade <= 0 || shown[1] - shown[0] < 0.005) return;
for (let side = 0; side < 2; side++) {
// Prima il segno nero bruciato, poi le fiamme sopra.
const [a, b] = [wheels(def, shown[0])[side], wheels(def, shown[1])[side]];
ctx.strokeStyle = `rgba(30,18,10,${0.7 * Math.max(fade, 0.5)})`; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke();
const n = 70;
for (let i = 0; i <= n; i++) {
const k = lerp(shown[0], shown[1], i / n), [x, y] = wheels(def, k)[side], s = Math.max(0.12, along(def, k).scale);
// Lingue di fuoco di altezza diversa, che tremano: rosso fuori, arancio, il cuore giallo.
const flick = 0.55 + 0.45 * Math.sin(c.t * 19 + i * 2.3 + side * 2) * Math.sin(c.t * 7 + i * 0.9);
const h = (16 + 22 * flick) * s * fade, w = Math.max(2, 9 * s);
if (h < 1.5) continue;
ctx.fillStyle = `rgba(255,120,30,${0.18 * fade})`; ctx.fillRect(Math.round(x - w), Math.round(y - h * 1.2), Math.ceil(w * 2), Math.ceil(h * 1.2));
ctx.fillStyle = `rgba(220,50,20,${0.9 * fade})`; ctx.fillRect(Math.round(x - w / 2), Math.round(y - h), Math.ceil(w), Math.ceil(h));
ctx.fillStyle = `rgba(255,${Math.round(140 + 60 * flick)},30,${0.95 * fade})`; ctx.fillRect(Math.round(x - w / 3), Math.round(y - h * 0.7), Math.ceil(w * 0.66), Math.ceil(h * 0.7));
ctx.fillStyle = `rgba(255,245,170,${fade})`; ctx.fillRect(Math.round(x - w / 6), Math.round(y - h * 0.35), Math.max(1, Math.ceil(w / 3)), Math.ceil(h * 0.35));
// Un filo di fumo che sale quando le fiamme calano.
if (c.t > times.burn - 1 && i % 8 === 0) {
ctx.fillStyle = `rgba(90,90,96,${0.25 * fade})`;
ctx.fillRect(Math.round(x - 2 * s), Math.round(y - h - 6 * s - ((c.t * 18 + i) % 14) * s), Math.ceil(4 * s), Math.ceil(4 * s));
}
}
}
}
// Il fulmine: una saetta spezzata dal cielo fino al punto, con due rami.
function bolt(ctx, c, def) {
const times = T(def), dt = c.t - times.bolt;
if (dt < -0.12 || dt > 0.28) return;
const p = boltAt(def), seed = Math.floor(c.t * 30);
ctx.save();
for (const [width, color] of [[6, 'rgba(140,200,255,0.5)'], [2, '#ffffff']]) {
ctx.strokeStyle = color; ctx.lineWidth = width; ctx.beginPath();
let x = p.x + 40, y = 0; ctx.moveTo(x, y);
const steps = 9;
for (let i = 1; i <= steps; i++) {
x = lerp(p.x + 40, p.x, i / steps) + (i < steps ? ((seed * (i + 5) * 53) % 30) - 15 : 0);
y = lerp(0, p.y - 12 * p.scale, i / steps); ctx.lineTo(x, y);
}
ctx.stroke();
}
ctx.restore();
}
export function drawCorsa(ctx, c, def, { bg, frames, tint }) {
const times = T(def);
ctx.save(); ctx.imageSmoothingEnabled = false;
// La strada trema un po' quando la macchina si carica e quando arriva il fulmine.
const shake = (def.mode !== 'arriva' && c.t < PARTI.charge + 0.4 ? 1.5 : 0) + (Math.abs(c.t - times.bolt) < 0.25 ? 3 : 0);
ctx.translate(Math.round(Math.sin(c.t * 57) * shake), Math.round(Math.cos(c.t * 43) * shake));
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#4a4a50'; ctx.fillRect(0, 0, 640, 360); }
if (tint) { ctx.fillStyle = tint; ctx.fillRect(0, 0, 640, 360); }
fire(ctx, c, def);
if (carShown(c, def)) {
const k = progress(c, def), p = along(def, k);
// Da dietro: coi lampi mentre si carica e quando corre forte; davanti (all'arrivo) coi lampi finché frena.
const img = def.mode === 'arriva' ? frames?.davanti : c.t > PARTI.charge * 0.5 && Math.floor(c.t * 14) % 3 !== 0 ? frames?.lampi : frames?.dietro;
if (img) {
const w = def.width * p.scale, h = w * img.height / img.width;
ctx.drawImage(img, Math.round(p.x - w / 2), Math.round(p.y - h), Math.round(w), Math.round(h));
} else { ctx.fillStyle = '#c8ccd2'; ctx.fillRect(Math.round(p.x - 40 * p.scale), Math.round(p.y - 40 * p.scale), Math.round(80 * p.scale), Math.round(40 * p.scale)); }
}
bolt(ctx, c, def);
ctx.restore();
// Il bianco del fulmine, che sfuma.
const f = c.t - times.bolt;
if (f >= 0 && f < times.flash) { ctx.fillStyle = `rgba(255,255,255,${1 - f / times.flash})`; ctx.fillRect(0, 0, 640, 360); }
}
+39
View File
@@ -12,6 +12,10 @@ import * as scopa from './scopa.js';
import * as gara from './gara.js';
import * as micro from './micro.js';
import * as cerbottana from './cerbottana.js';
import { newCorsa, updateCorsa } from './corsa.js';
import * as condor from './condor.js';
import * as campana from './campana.js';
import * as poker from './poker.js';
// I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta.
export const MINIGAMES = {
@@ -23,7 +27,10 @@ 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 },
campana: { create: (seed, level) => campana.newCampana(seed, { level }), update: campana.update, press: campana.press, release: campana.release, cancel: campana.cancel, aim: campana.aimScreen },
micro: { create: (seed, level) => micro.newMicro(seed, { level }), update: micro.update, press: micro.press, release: micro.release, cancel: micro.cancel, aim: micro.aimScreen },
gara: { create: (seed, level, deck, def) => gara.newGara(seed, { level, riders: def.riders }), update: gara.update, press: gara.press, release: gara.release, cancel: gara.cancel, aim: gara.aimScreen },
};
@@ -51,6 +58,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 +275,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 +327,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' || def?.type === 'corsa') { this.queue.unshift(...def.complete); return true; }
// I minigiochi: a partita finita (risultato deciso, anche se il conteggio si sta ancora mostrando) la battuta
// di chi ha vinto; uscendo prima, quella di chi lascia.
if (MINIGAMES[def?.type]) {
@@ -336,6 +359,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 +488,8 @@ 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; }
if (this.story.closeups[c.id].type === 'corsa') { if (updateCorsa(c, this.story.closeups[c.id], dt)) this.closeupClose(); return; }
const m = this.minigame();
if (m) {
m.update(c, dt);
@@ -494,6 +531,8 @@ 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 }
: type === 'corsa' ? newCorsa(e.closeup)
// `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();
}

Some files were not shown because too many files have changed in this diff Show More