Compare commits

...
9 Commits
Author SHA1 Message Date
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
53 changed files with 787 additions and 17 deletions
+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

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 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.
+115 -3
View File
@@ -683,6 +683,10 @@
"file": "andrea-c716ab2381.mp3", "file": "andrea-c716ab2381.mp3",
"sec": 7.95 "sec": 7.95
}, },
"andrea|Il Taglia. Io sono un metro e ottanta, e lui mi guarda dall'alto. Con la Condor in mano.": {
"file": "andrea-f1663e7892.mp3",
"sec": 8.52
},
"andrea|Il giardino coi cedri e le panchine. All'ombra, d'estate, ci si fa la siesta a turno.": { "andrea|Il giardino coi cedri e le panchine. All'ombra, d'estate, ci si fa la siesta a turno.": {
"file": "andrea-4fd811d0c7.mp3", "file": "andrea-4fd811d0c7.mp3",
"sec": 6.43 "sec": 6.43
@@ -1883,6 +1887,38 @@
"file": "andrea-d38b6e49ef.mp3", "file": "andrea-d38b6e49ef.mp3",
"sec": 3.49 "sec": 3.49
}, },
"andrea|Lo zio Mario nel suo orto, col sole che va giù dietro la casa.": {
"file": "andrea-9309e50d5c.mp3",
"sec": 3.78
},
"zio_mario|Ti s'aspetta a cena, che c'è la vitella di maiale.": {
"file": "zio_mario-39ff0c666f.mp3",
"sec": 2.71
},
"andrea|Non ti avevo visto, pensavo tu fossi andato all'estero.": {
"file": "andrea-f847f224df.mp3",
"sec": 3.35
},
"zio_mario|Io ho girato tanto... il Falcione.": {
"file": "zio_mario-d0d592db52.mp3",
"sec": 2.73
},
"andrea|Zio, che sei stanco?": {
"file": "andrea-21195c6cac.mp3",
"sec": 2.31
},
"zio_mario|No, dormo!": {
"file": "zio_mario-a852e9c43f.mp3",
"sec": 1.19
},
"andrea|Zio, a me mi garba Michael Jackson!": {
"file": "andrea-599b75d7c7.mp3",
"sec": 2.6
},
"zio_mario|Mai col gesso!": {
"file": "zio_mario-5773e3350f.mp3",
"sec": 1.13
},
"andrea|Le case coi balconi pieni di vasi: qui ogni finestra è un giardino.": { "andrea|Le case coi balconi pieni di vasi: qui ogni finestra è un giardino.": {
"file": "andrea-ec81875c47.mp3", "file": "andrea-ec81875c47.mp3",
"sec": 5.22 "sec": 5.22
@@ -2791,6 +2827,54 @@
"file": "pierino_parigi-8104163151.mp3", "file": "pierino_parigi-8104163151.mp3",
"sec": 3.3 "sec": 3.3
}, },
"andrea|Taglia! Si fa una gara con la Condor?": {
"file": "andrea-99f683209e.mp3",
"sec": 3.41
},
"andrea|A cosa si tira?": {
"file": "andrea-3f77b83f35.mp3",
"sec": 0.87
},
"andrea|Ma quanto sei alto, Taglia?": {
"file": "andrea-01ecdf3ed8.mp3",
"sec": 1.96
},
"andrea|Ci si vede, Taglia.": {
"file": "andrea-5d4d911c06.mp3",
"sec": 1.42
},
"taglia|Io ho già tirato: ora tocca a te. Dodici pallini gialli, e devi fare più di me.": {
"file": "taglia-2658ec8482.mp3",
"sec": 6.05
},
"taglia|Come la vuoi?": {
"file": "taglia-32a067db10.mp3",
"sec": 0.62
},
"taglia|Il cartone sull'albero, le lattine sulla panchina, le bottigliette sulla porta e il palloncino all'altalena.": {
"file": "taglia-670f9b4ef8.mp3",
"sec": 7.15
},
"taglia|E se la vuoi seria, ogni tanto ti tiro una lattina in aria.": {
"file": "taglia-ac012a1066.mp3",
"sec": 4.21
},
"taglia|Abbastanza da vedere i pallini arrivare prima di te.": {
"file": "taglia-d99c875252.mp3",
"sec": 2.57
},
"taglia|Alla bona: ti faccio vedere dove arriva il pallino.": {
"file": "taglia-757564b65e.mp3",
"sec": 3.12
},
"taglia|Seria. Occhio alla lattina in aria.": {
"file": "taglia-06abe0c427.mp3",
"sec": 3.06
},
"taglia|A fassi male? Allora tiro più forte, e più spesso.": {
"file": "taglia-4117f6613d.mp3",
"sec": 4.84
},
"andrea|Si fa una gara alle macchinine?!?!": { "andrea|Si fa una gara alle macchinine?!?!": {
"file": "andrea-17216997e2.mp3", "file": "andrea-17216997e2.mp3",
"sec": 2.74 "sec": 2.74
@@ -3203,9 +3287,9 @@
"file": "bertovo-ca941bee91.mp3", "file": "bertovo-ca941bee91.mp3",
"sec": 5.41 "sec": 5.41
}, },
"vannino|M'hai pelato, ragazzo. Nel 1997 un ci si vedeva da queste parti, con quella faccia.": { "vannino|Tu m'ha pelato. La prossima volta porta il Barone.": {
"file": "vannino-e68fb40eba.mp3", "file": "vannino-2267bb5adf.mp3",
"sec": 7.85 "sec": 3.18
}, },
"pierino_parigi|Le fiches restano al banco. E tu torna quando hai imparato a bluffare.": { "pierino_parigi|Le fiches restano al banco. E tu torna quando hai imparato a bluffare.": {
"file": "pierino_parigi-a80d04fe5b.mp3", "file": "pierino_parigi-a80d04fe5b.mp3",
@@ -3231,6 +3315,34 @@
"file": "andrea-c7dcb11e66.mp3", "file": "andrea-c7dcb11e66.mp3",
"sec": 5.52 "sec": 5.52
}, },
"andrea|Battuto il Taglia! Con la sua Condor, per giunta.": {
"file": "andrea-3ff6547461.mp3",
"sec": 3.73
},
"taglia|Fortuna. Domani la rivinco.": {
"file": "taglia-39627c5b3a.mp3",
"sec": 2.78
},
"andrea|Il Taglia ha la mano ferma. Io ho il fiatone.": {
"file": "andrea-6eabf3443c.mp3",
"sec": 3.83
},
"taglia|Si torna quando vuoi. I pallini gialli un mancano mai.": {
"file": "taglia-c3c705219c.mp3",
"sec": 4.52
},
"andrea|Pari. Si rifà?": {
"file": "andrea-5625b5f1fd.mp3",
"sec": 2.21
},
"taglia|Si rifà.": {
"file": "taglia-18973374a5.mp3",
"sec": 0.76
},
"andrea|La Condor la rendo al Taglia. Per ora.": {
"file": "andrea-aba12c5241.mp3",
"sec": 3.1
},
"chiaru|Hai vinto te. Ma la mia aveva le ruote piene di gesso, sia chiaro.": { "chiaru|Hai vinto te. Ma la mia aveva le ruote piene di gesso, sia chiaro.": {
"file": "chiaru-91a4b23b22.mp3", "file": "chiaru-91a4b23b22.mp3",
"sec": 4.44 "sec": 4.44
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.
+2
View File
@@ -31,10 +31,12 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p
| `provinciale-pignone.png`, `piazzetta-cambray-digny.png`, `via-cipriani.png`, `via-massorondinaio.png`, `mozzete.png`, `ponticino-mozzete.png`, `coldaia.png`, `falegnameria-coldaia.png`, `gabbiano.png` | Codex | screenshot di Google Street View | Google | **no**: da rifare con foto di Andrea | | `provinciale-pignone.png`, `piazzetta-cambray-digny.png`, `via-cipriani.png`, `via-massorondinaio.png`, `mozzete.png`, `ponticino-mozzete.png`, `coldaia.png`, `falegnameria-coldaia.png`, `gabbiano.png` | Codex | screenshot di Google Street View | Google | **no**: da rifare con foto di Andrea |
| `closeups/micro-parcheggio.png`, `closeups/micro/*.png` (macchinine) | Codex, solo testo | nessuna | — | sì | | `closeups/micro-parcheggio.png`, `closeups/micro/*.png` (macchinine) | Codex, solo testo | nessuna | — | sì |
| `characters/chiaru.png` | Codex | `foto/chiaru.jpg` | Andrea (29 ottobre) | sì (il Chiaru consenziente) | | `characters/chiaru.png` | Codex | `foto/chiaru.jpg` | Andrea (29 ottobre) | sì (il Chiaru consenziente) |
| `characters/taglia.png` | Codex | `foto/taglia.png` | Andrea (29 ottobre) | sì (il Taglia consenziente) |
| `via-liguri-magelli-muro.png` | Codex, ritocco di `via-liguri-magelli.png` con la Ghiacciomobile nel muro | come `via-liguri-magelli.png` | Street View | **no**: come lo sfondo da cui viene | | `via-liguri-magelli-muro.png` | Codex, ritocco di `via-liguri-magelli.png` con la Ghiacciomobile nel muro | come `via-liguri-magelli.png` | Street View | **no**: come lo sfondo da cui viene |
| `characters/zio_mario.png` | Codex | `foto/zio-mario.png` | Andrea (29 ottobre) | sì (famiglia di Andrea) | | `characters/zio_mario.png` | Codex | `foto/zio-mario.png` | Andrea (29 ottobre) | sì (famiglia di Andrea) |
| `terrazza-cardetole.png` | Codex (dalla foto, poi il terrazzo davanti) | `cardetole-finestra.jpg` | Andrea (29 ottobre) | **da decidere**: come `cardetole.png`, dice dove abita Andrea | | `terrazza-cardetole.png` | Codex (dalla foto, poi il terrazzo davanti) | `cardetole-finestra.jpg` | Andrea (29 ottobre) | **da decidere**: come `cardetole.png`, dice dove abita Andrea |
| `closeups/muro-cerbottana.png` | Codex, solo testo (stile dalla piazza) | nessuna | — | sì | | `closeups/muro-cerbottana.png` | Codex, solo testo (stile dalla piazza) | nessuna | — | sì |
| `closeups/campone-condor.png` | Codex, dallo sfondo del campone delle poste | nessuna | — | sì |
| `barrettino-1997.png`, `closeups/cruscotto-tempo.png` | Codex, solo testo | 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) | | `characters/vannino.png`, `characters/pierino_parigi.png` | Codex, dalla descrizione di Andrea (niente foto) | nessuna | — | sì (Pierino: consenso del figlio Marco) |
| `cardetole.png` | Codex | `cardetole.jpg` | Andrea (EXIF) | **da decidere**: è la casa vera di Andrea, pubblicarla dice dove abita | | `cardetole.png` | Codex | `cardetole.jpg` | Andrea (EXIF) | **da decidere**: è la casa vera di Andrea, pubblicarla dice dove abita |
+10
View File
@@ -18,6 +18,16 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
## Dove siamo arrivati ## Dove siamo arrivati
- **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 - **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) 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 compone 10/07/1997 18:30, poi tremori, lampi e il bianco (primo piano `viaggio`, `src/engine/viaggio.js`, tempi in
+11
View File
@@ -272,6 +272,17 @@
il pulsante alterna Recupera e Molla. Sei lanci. La gloria: 2 pesci alla bona, 3 seria, 4 a fassi male (lo stivale il pulsante alterna Recupera e Molla. Sei lanci. La gloria: 2 pesci alla bona, 3 seria, 4 a fassi male (lo stivale
non conta, e non pesa). Frecce ← →: distanza del lancio; Esc esce.</p> non conta, e non pesa). Frecce ← →: distanza del lancio; Esc esce.</p>
</details> </details>
<details id="condor-help" hidden><summary>Come si gioca</summary>
<p>La Condor, la pistola ad aria compressa coi pallini gialli, al campone delle poste: dodici pallini, e il Taglia ha
già tirato. Il suo punteggio è in alto: per vincere bisogna fare di più. Il cartone sull'albero vale 10, 5 o 2 punti
secondo il cerchio, ogni lattina sulla panchina 3, ogni bottiglietta sulla traversa della porta 5, il palloncino
legato all'altalena 6, la lattina che il Taglia tira in aria (Seria e A fassi male) 8.</p>
<p>Muovi il mouse o tocca il campo per mirare: il mirino ondeggia col respiro (di più salendo di livello). Tieni
premuto per trattenere il fiato: il mirino si ferma (diventa verde); lascia per sparare. Se lo trattieni troppo il
fiato finisce, il mirino trema e il colpo parte da solo. Il pallino cala con la distanza: alla porta in fondo si
mira un po' più sopra; alla bona il puntino giallo segna dove arriva. Il palloncino e la lattina in aria si
muovono: si mira dove saranno. Si può anche premere Spara, senza trattenere il fiato. Frecce per spostare il mirino.</p>
</details>
<details id="cerbottana-help" hidden><summary>Come si gioca</summary> <details id="cerbottana-help" hidden><summary>Come si gioca</summary>
<p>La cerbottana, con lo stucco verde di Pierin di Frilli: dodici pallini contro il muro della piazza. Il bersaglio <p>La cerbottana, con lo stucco verde di Pierin di Frilli: dodici pallini contro il muro della piazza. Il bersaglio
di gesso vale 10, 5 o 2 punti secondo il cerchio, ogni lattina sul muretto 3, il barattolo appeso che dondola (Seria di gesso vale 10, 5 o 2 punti secondo il cerchio, ogni lattina sul muretto 3, il barattolo appeso che dondola (Seria
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "leggenda-del-regolo", "name": "leggenda-del-regolo",
"version": "0.1.53", "version": "0.1.55",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -10,6 +10,7 @@
"browser:micro": "node tests/micro.browser.mjs", "browser:micro": "node tests/micro.browser.mjs",
"browser:gara": "node tests/gara.browser.mjs", "browser:gara": "node tests/gara.browser.mjs",
"browser:cerbottana": "node tests/cerbottana.browser.mjs", "browser:cerbottana": "node tests/cerbottana.browser.mjs",
"browser:condor": "node tests/condor.browser.mjs",
"browser:tressette": "node tests/tressette.browser.mjs", "browser:tressette": "node tests/tressette.browser.mjs",
"browser:scopa": "node tests/scopa.browser.mjs", "browser:scopa": "node tests/scopa.browser.mjs",
"browser:briscola": "node tests/briscola.browser.mjs", "browser:briscola": "node tests/briscola.browser.mjs",
+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.'}`;
}
+2
View File
@@ -12,6 +12,7 @@ import * as scopa from './scopa.js';
import * as gara from './gara.js'; import * as gara from './gara.js';
import * as micro from './micro.js'; import * as micro from './micro.js';
import * as cerbottana from './cerbottana.js'; import * as cerbottana from './cerbottana.js';
import * as condor from './condor.js';
import * as poker from './poker.js'; import * as poker from './poker.js';
// I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta. // I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta.
@@ -26,6 +27,7 @@ export const MINIGAMES = {
pesca: { create: (seed, level) => pesca.newPesca(seed, { level }), update: pesca.update, press: pesca.press, release: pesca.release, cancel: pesca.cancel, aim: pesca.aimScreen }, 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 }, poker: { create: (seed, level) => poker.newPoker(seed, { level }), update: poker.update, play: poker.play },
cerbottana: { create: (seed, level) => cerbottana.newCerbottana(seed, { level }), update: cerbottana.update, press: cerbottana.press, release: cerbottana.release, cancel: cerbottana.cancel, aim: cerbottana.aimScreen }, cerbottana: { create: (seed, level) => cerbottana.newCerbottana(seed, { level }), update: cerbottana.update, press: cerbottana.press, release: cerbottana.release, cancel: cerbottana.cancel, aim: cerbottana.aimScreen },
condor: { create: (seed, level) => condor.newCondor(seed, { level }), update: condor.update, press: condor.press, release: condor.release, cancel: condor.cancel, aim: condor.aimScreen },
micro: { create: (seed, level) => micro.newMicro(seed, { level }), update: micro.update, press: micro.press, release: micro.release, cancel: micro.cancel, aim: micro.aimScreen }, micro: { create: (seed, level) => micro.newMicro(seed, { level }), update: micro.update, press: micro.press, release: micro.release, cancel: micro.cancel, aim: micro.aimScreen },
gara: { create: (seed, level, deck, def) => gara.newGara(seed, { level, riders: def.riders }), update: gara.update, press: gara.press, release: gara.release, cancel: gara.cancel, aim: gara.aimScreen }, gara: { create: (seed, level, deck, def) => gara.newGara(seed, { level, riders: def.riders }), update: gara.update, press: gara.press, release: gara.release, cancel: gara.cancel, aim: gara.aimScreen },
}; };
+6
View File
@@ -11,6 +11,7 @@ import { drawPesca } from './pesca-render.js';
import { drawGara } from './gara-render.js'; import { drawGara } from './gara-render.js';
import { drawMicro } from './micro-render.js'; import { drawMicro } from './micro-render.js';
import { drawCerbottana } from './cerbottana-render.js'; import { drawCerbottana } from './cerbottana-render.js';
import { drawCondor } from './condor-render.js';
import { drawPoker } from './poker-render.js'; import { drawPoker } from './poker-render.js';
import { drawViaggio } from './viaggio.js'; import { drawViaggio } from './viaggio.js';
import { drawBriscola } from './briscola-render.js'; import { drawBriscola } from './briscola-render.js';
@@ -701,6 +702,11 @@ function drawCloseup(ctx, game, time) {
drawCerbottana(ctx, c, def, backgrounds.get(def.picture)); drawCerbottana(ctx, c, def, backgrounds.get(def.picture));
return; return;
} }
if (def.type === 'condor') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawCondor(ctx, c, def, backgrounds.get(def.picture));
return;
}
if (def.type === 'micro') { if (def.type === 'micro') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture); if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawMicro(ctx, c, def, backgrounds.get(def.picture), src => { if (!backgrounds.has(src)) loadBackground(src); return backgrounds.get(src); }); drawMicro(ctx, c, def, backgrounds.get(def.picture), src => { if (!backgrounds.has(src)) loadBackground(src); return backgrounds.get(src); });
+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-pff') this.noiseSound(0.12, 2200, 0.05 + strength * 0.05, { pan });
else if (kind === 'cerbottana-muro') { this.tone(180, 90, 0.06, 0.05, 0, pan); this.noiseSound(0.05, 600, 0.04, { pan }); } else if (kind === 'cerbottana-muro') { this.tone(180, 90, 0.06, 0.05, 0, pan); this.noiseSound(0.05, 600, 0.04, { pan }); }
else if (kind === 'cerbottana-latta') { this.tone(2400, 1600, 0.08, 0.05 + strength * 0.04, 0, pan); this.tone(3400, 2600, 0.05, 0.03, 0.03, pan); } else if (kind === 'cerbottana-latta') { this.tone(2400, 1600, 0.08, 0.05 + strength * 0.04, 0, pan); this.tone(3400, 2600, 0.05, 0.03, 0.03, pan); }
// La Condor: lo scatto della molla, il palloncino che scoppia, la bottiglietta, il Taglia che tira la lattina.
else if (kind === 'condor-pam') { this.noiseSound(0.06, 3200, 0.07, { pan }); this.tone(520, 220, 0.05, 0.05, 0, pan); }
else if (kind === 'condor-palloncino') { this.noiseSound(0.12, 1800, 0.14, { pan }); this.tone(140, 60, 0.08, 0.08, 0, pan); }
else if (kind === 'condor-plastica') { this.tone(1300, 900, 0.06, 0.05, 0, pan); this.noiseSound(0.05, 2600, 0.03, { pan }); }
else if (kind === 'condor-lancio') this.noiseSound(0.2, 700, 0.03, { pan });
else if (kind === 'cerbottana-tosse') { this.noiseSound(0.18, 900, 0.08, { pan }); this.tone(260, 140, 0.12, 0.06, 0.25, pan); this.tone(240, 120, 0.1, 0.05, 0.3, pan); } else if (kind === 'cerbottana-tosse') { this.noiseSound(0.18, 900, 0.08, { pan }); this.tone(260, 140, 0.12, 0.06, 0.25, pan); this.tone(240, 120, 0.1, 0.05, 0.3, pan); }
// Le macchinine: il dito che spinge, le ruote sull'asfalto, l'urto di plastica, fuori pista, il traguardo. // Le macchinine: il dito che spinge, le ruote sull'asfalto, l'urto di plastica, fuori pista, il traguardo.
else if (kind === 'micro-spinta') { this.tone(1200, 500, 0.04, 0.04 + strength * 0.04, 0, pan); this.noiseSound(0.04, 2600, 0.04, { pan }); } else if (kind === 'micro-spinta') { this.tone(1200, 500, 0.04, 0.04 + strength * 0.04, 0, pan); this.noiseSound(0.04, 2600, 0.04, { pan }); }
+8 -5
View File
@@ -13,6 +13,7 @@ import * as biliardo from './engine/biliardo.js';
import * as rinquarto from './engine/rinquarto.js'; import * as rinquarto from './engine/rinquarto.js';
import * as micro from './engine/micro.js'; import * as micro from './engine/micro.js';
import * as cerbottana from './engine/cerbottana.js'; import * as cerbottana from './engine/cerbottana.js';
import * as condor from './engine/condor.js';
import * as poker from './engine/poker.js'; import * as poker from './engine/poker.js';
import { cardAt as pokerAt } from './engine/poker-render.js'; import { cardAt as pokerAt } from './engine/poker-render.js';
import * as basket from './engine/basket.js'; import * as basket from './engine/basket.js';
@@ -514,7 +515,7 @@ canvas.addEventListener('pointerdown', e => {
} }
if (press?.closeup) return; if (press?.closeup) return;
const aimed = game.closeupAim(scenePoint(e)); const aimed = game.closeupAim(scenePoint(e));
if (['biliardo', 'rinquarto', 'basket', 'micro', 'cerbottana'].includes(story.closeups[game.closeup.id].type)) { if (['biliardo', 'rinquarto', 'basket', 'micro', 'cerbottana', 'condor'].includes(story.closeups[game.closeup.id].type)) {
if (!aimed && !game.speech) return; // dove non si può mirare (dietro Andrea) non si carica nemmeno if (!aimed && !game.speech) return; // dove non si può mirare (dietro Andrea) non si carica nemmeno
if (aimed) game.closeup.aimLocked = true; // il clic fissa la mira if (aimed) game.closeup.aimLocked = true; // il clic fissa la mira
} }
@@ -586,7 +587,7 @@ canvas.addEventListener('pointermove', e => {
if (press?.gara === e.pointerId) gara.aimScreen(game.closeup, scenePoint(e), e.pointerId); if (press?.gara === e.pointerId) gara.aimScreen(game.closeup, scenePoint(e), e.pointerId);
return; return;
} }
const aiming = game.closeup && ['biliardo', 'rinquarto', 'bocce', 'basket', 'pesca', 'micro', 'cerbottana'].includes(story.closeups[game.closeup.id].type); const aiming = game.closeup && ['biliardo', 'rinquarto', 'bocce', 'basket', 'pesca', 'micro', 'cerbottana', 'condor'].includes(story.closeups[game.closeup.id].type);
if (aiming) { if (e.pointerType === 'mouse' && !press && !game.closeup.aimLocked) game.closeupAim(scenePoint(e)); } if (aiming) { if (e.pointerType === 'mouse' && !press && !game.closeup.aimLocked) game.closeupAim(scenePoint(e)); }
else if (e.pointerType === 'mouse' || press?.closeup) game.closeupAim(scenePoint(e)); else if (e.pointerType === 'mouse' || press?.closeup) game.closeupAim(scenePoint(e));
}); });
@@ -609,6 +610,7 @@ const MINI = {
rinquarto: { ...rinquarto, back: 'Torna da Trappizzio Berti', label: 'I rinquarti di Trappizzio Berti: clic o tocco fissa la mira, frecce o pulsanti Mira per girare la stecca (Maiusc per i passi fini), Tira per tirare. Dopo l’esito, Prossima.' }, 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.' }, poker: { ...poker, back: 'Torna al Barrettino', label: 'Il pokerino: tocca le carte da cambiare, poi Cambia; nei giri di puntate Lascio, Vedo o Parola, Apro o Rilancio. Dopo la mostra un tocco va avanti.' },
cerbottana: { ...cerbottana, back: 'Torna in piazza', label: 'La cerbottana: muovi il mouse o tocca il muro per mirare, tieni premuto per prendere fiato e lascia per sparare; oppure regola il fiato e premi Spara. Frecce per spostare il mirino.' }, cerbottana: { ...cerbottana, back: 'Torna in piazza', label: 'La cerbottana: muovi il mouse o tocca il muro per mirare, tieni premuto per prendere fiato e lascia per sparare; oppure regola il fiato e premi Spara. Frecce per spostare il mirino.' },
condor: { ...condor, back: 'Torna al campone', label: 'La Condor: muovi il mouse o tocca il campo per mirare, tieni premuto per trattenere il fiato e lascia per sparare; oppure premi Spara. Frecce per spostare il mirino.' },
micro: { ...micro, back: 'Torna dal Chiaru', label: 'La pista col gesso: muovi il mouse o tocca il parcheggio per mirare, tieni premuto per caricare la spinta e lascia; oppure regola la forza e premi Spingi. Frecce o pulsanti Mira per girare.' }, micro: { ...micro, back: 'Torna dal Chiaru', label: 'La pista col gesso: muovi il mouse o tocca il parcheggio per mirare, tieni premuto per caricare la spinta e lascia; oppure regola la forza e premi Spingi. Frecce o pulsanti Mira per girare.' },
basket: { ...basket, back: 'Torna in strada', label: 'Fai due tiri: muovi il mouse o tocca per mirare, tieni premuto per caricare la forza, lascia per tirare.' }, basket: { ...basket, back: 'Torna in strada', label: 'Fai due tiri: muovi il mouse o tocca per mirare, tieni premuto per caricare la forza, lascia per tirare.' },
// La gara: niente mira né forza, il pulsante alterna gas e freno (chi non può tenere premuto). // La gara: niente mira né forza, il pulsante alterna gas e freno (chi non può tenere premuto).
@@ -1038,7 +1040,7 @@ function updateUi() {
} }
} }
if (mg && !cards) { if (mg && !cards) {
setAttr($('bocce-controls'), 'aria-label', { rinquarto: 'Comandi dei rinquarti', biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca', gara: 'Comandi della gara', micro: 'Comandi delle macchinine', cerbottana: 'Comandi della cerbottana' }[def.type] ?? 'Comandi delle bocce'); setAttr($('bocce-controls'), 'aria-label', { rinquarto: 'Comandi dei rinquarti', biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca', gara: 'Comandi della gara', micro: 'Comandi delle macchinine', cerbottana: 'Comandi della cerbottana', condor: 'Comandi della Condor' }[def.type] ?? 'Comandi delle bocce');
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true); setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
const line = mg.status(c, def.names); const line = mg.status(c, def.names);
if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line; if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line;
@@ -1052,7 +1054,7 @@ function updateUi() {
// Ai rinquarti la forza è sempre piena: il cursore non serve. Nella guida semplice del GP (alla bona e seria) si // Ai rinquarti la forza è sempre piena: il cursore non serve. Nella guida semplice del GP (alla bona e seria) si
// usa solo il gas: niente sensibilità, sterzo, freno né riquadro dello swipe. // usa solo il gas: niente sensibilità, sterzo, freno né riquadro dello swipe.
const easy = def.type === 'gara' && gara.simple(c); const easy = def.type === 'gara' && gara.simple(c);
setProp(powerLabel, 'hidden', def.type === 'rinquarto' || easy); setProp(powerLabel, 'hidden', def.type === 'rinquarto' || def.type === 'condor' || easy);
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy); for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy);
const powerName = def.type === 'gara' ? 'Sensibilità ' : def.type === 'cerbottana' ? 'Fiato ' : 'Forza '; const powerName = def.type === 'gara' ? 'Sensibilità ' : def.type === 'cerbottana' ? 'Fiato ' : 'Forza ';
if (powerLabel.firstChild.nodeValue !== powerName) powerLabel.firstChild.nodeValue = powerName; if (powerLabel.firstChild.nodeValue !== powerName) powerLabel.firstChild.nodeValue = powerName;
@@ -1073,6 +1075,7 @@ function updateUi() {
setProp($('rinquarto-help'), 'hidden', def.type !== 'rinquarto'); setProp($('rinquarto-help'), 'hidden', def.type !== 'rinquarto');
setProp($('micro-help'), 'hidden', def.type !== 'micro'); setProp($('micro-help'), 'hidden', def.type !== 'micro');
setProp($('cerbottana-help'), 'hidden', def.type !== 'cerbottana'); setProp($('cerbottana-help'), 'hidden', def.type !== 'cerbottana');
setProp($('condor-help'), 'hidden', def.type !== 'condor');
setProp($('basket-help'), 'hidden', def.type !== 'basket'); setProp($('basket-help'), 'hidden', def.type !== 'basket');
setProp($('pesca-help'), 'hidden', def.type !== 'pesca'); setProp($('pesca-help'), 'hidden', def.type !== 'pesca');
setProp($('gara-help'), 'hidden', def.type !== 'gara'); setProp($('gara-help'), 'hidden', def.type !== 'gara');
@@ -1093,7 +1096,7 @@ function updateUi() {
canvas.classList.toggle('torch', !!torch); canvas.classList.toggle('torch', !!torch);
canvas.classList.toggle('bocce', !!mg); canvas.classList.toggle('bocce', !!mg);
// Biliardo e GP di Fortezza: audio, pausa e uscita fuori dal quadro (al GP la pista passa negli angoli). // Biliardo e GP di Fortezza: audio, pausa e uscita fuori dal quadro (al GP la pista passa negli angoli).
$('stage').classList.toggle('biliardo', !!c && ['biliardo', 'rinquarto', 'gara', 'micro', 'cerbottana'].includes(story.closeups[c.id].type)); $('stage').classList.toggle('biliardo', !!c && ['biliardo', 'rinquarto', 'gara', 'micro', 'cerbottana', 'condor'].includes(story.closeups[c.id].type));
$('stage').classList.toggle('briscola', !!cards); $('stage').classList.toggle('briscola', !!cards);
setAttr(canvas, 'aria-label', torch ? 'Fessura: sposta la luce con mouse, dito o frecce. Invio cerca il prossimo dettaglio; Esc esce.' : mg ? mg.label : "La scena dell'avventura"); setAttr(canvas, 'aria-label', torch ? 'Fessura: sposta la luce con mouse, dito o frecce. Invio cerca il prossimo dettaglio; Esc esce.' : mg ? mg.label : "La scena dell'avventura");
// La pagina che comincia a girare fa il suo fruscio. // La pagina che comincia a girare fa il suo fruscio.
+1 -1
View File
@@ -241,7 +241,7 @@ export const ambience = {
{ type: 'insect', kind: 'dragonfly', phase: day, area: [440, 228, 110, 30], every: 31000, duration: 8500, offset: 15000 }, { type: 'insect', kind: 'dragonfly', phase: day, area: [440, 228, 110, 30], every: 31000, duration: 8500, offset: 15000 },
], ],
orti_zio_mario: [ orti_zio_mario: [
{ type: 'clouds', area: [80, 20, 500, 90], count: 3, big: true }, { type: 'clouds', area: [60, 20, 330, 70], count: 3, big: true }, // solo fino all'abete: dentro le fronde che ondeggiano le nuvole si tagliano
{ type: 'birds', phase: day, area: [300, 30, 250, 60], count: 3, every: 24000, duration: 5000, offset: 2000 }, { type: 'birds', phase: day, area: [300, 30, 250, 60], count: 3, every: 24000, duration: 5000, offset: 2000 },
{ type: 'sway', rects: [[180, 10, 140, 180], [430, 10, 210, 240]], amount: 2 }, { type: 'sway', rects: [[180, 10, 140, 180], [430, 10, 210, 240]], amount: 2 },
butterfly([10, 240, 140, 50], 8000), butterfly([10, 240, 140, 50], 8000),
+70 -1
View File
@@ -255,7 +255,7 @@ export const story = {
poker: { poker: {
type: 'poker', type: 'poker',
names: { player: 'Andrea', cpu: 'Vannino e Pierino' }, names: { player: 'Andrea', cpu: 'Vannino e Pierino' },
win: [{ say: 'M\'hai pelato, ragazzo. Nel 1997 un ci si vedeva da queste parti, con quella faccia.', who: 'vannino', pose: 'shrug' }, { set: 'poker_vinto' }], win: [{ say: 'Tu m\'ha pelato. La prossima volta porta il Barone.', who: 'vannino', pose: 'shrug' }, { set: 'poker_vinto' }],
lose: [{ say: 'Le fiches restano al banco. E tu torna quando hai imparato a bluffare.', who: 'pierino_parigi', pose: 'laugh' }], 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' }], 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' }], draw: [{ say: 'Pari. Si rifà domani sera, alla stess\'ora.', who: 'pierino_parigi', pose: 'explain' }],
@@ -271,6 +271,16 @@ export const story = {
quit: [{ say: 'Il muro resta lì. E l\'odore dello stucco, sulle dita, pure.' }], quit: [{ say: 'Il muro resta lì. E l\'odore dello stucco, sulle dita, pure.' }],
draw: [], draw: [],
}, },
// La Condor coi pallini gialli al campone delle poste, in gara col Taglia (Andrea, 29 ottobre). [DA ANDREA: le battute]
condor: {
type: 'condor',
picture: 'assets/closeups/campone-condor.png',
names: { player: 'Andrea', cpu: 'il Taglia' },
win: [{ say: 'Battuto il Taglia! Con la sua Condor, per giunta.' }, { say: 'Fortuna. Domani la rivinco.', who: 'taglia', pose: 'shrug' }, { set: 'condor_vinta' }],
lose: [{ say: 'Il Taglia ha la mano ferma. Io ho il fiatone.' }, { say: 'Si torna quando vuoi. I pallini gialli un mancano mai.', who: 'taglia', pose: 'laugh' }],
draw: [{ say: 'Pari. Si rifà?' }, { say: 'Si rifà.', who: 'taglia', pose: 'explain' }],
quit: [{ say: 'La Condor la rendo al Taglia. Per ora.' }],
},
// Le macchinine sulla pista col gesso, sul parcheggio di Cardetole, contro il Chiaru (Andrea, 28 e 29 ottobre). // Le macchinine sulla pista col gesso, sul parcheggio di Cardetole, contro il Chiaru (Andrea, 28 e 29 ottobre).
// [DA ANDREA: le battute sono provvisorie] // [DA ANDREA: le battute sono provvisorie]
micro: { micro: {
@@ -358,6 +368,8 @@ export const story = {
// Lo zio Mario di Andrea (sprite di Codex dalla foto di Andrea, 29 ottobre), alla Ghiacciomobile nel muro. // Lo zio Mario di Andrea (sprite di Codex dalla foto di Andrea, 29 ottobre), alla Ghiacciomobile nel muro.
zio_mario: { name: 'Lo zio Mario', color: '#cfe3ff', sprite: 'assets/characters/zio_mario.json' }, zio_mario: { name: 'Lo zio Mario', color: '#cfe3ff', sprite: 'assets/characters/zio_mario.json' },
chiaru: { name: 'Il Chiaru', color: '#ffd6a0', sprite: 'assets/characters/chiaru.json' }, chiaru: { name: 'Il Chiaru', color: '#ffd6a0', sprite: 'assets/characters/chiaru.json' },
// Andrea Tagliaferri, il Taglia (consenso suo, 29 ottobre): sprite di Codex dalla sua foto.
taglia: { name: 'Il Taglia', color: '#ffe36a', sprite: 'assets/characters/taglia.json' },
barondeduke: { name: 'Il BaronDeDuke', color: '#e0c090', sprite: 'assets/characters/barondeduke.json' }, barondeduke: { name: 'Il BaronDeDuke', color: '#e0c090', sprite: 'assets/characters/barondeduke.json' },
mariano: { name: 'Mariano', color: '#ffb0a3', sprite: 'assets/characters/mariano.json' }, mariano: { name: 'Mariano', color: '#ffb0a3', sprite: 'assets/characters/mariano.json' },
// Non parla: è una cosa, disegnata come un personaggio per stare davanti o dietro ad Andrea. // Non parla: è una cosa, disegnata come un personaggio per stare davanti o dietro ad Andrea.
@@ -1327,6 +1339,9 @@ export const story = {
walk: [[0, 360], [0, 232], [640, 232], [640, 360]], walk: [[0, 360], [0, 232], [640, 232], [640, 360]],
depth: [[232, 0.55], [360, 1.15]], depth: [[232, 0.55], [360, 1.15]],
entry: [580, 340], entry: [580, 340],
speakers: { taglia: [470, 214] },
// Il Taglia con la sua Condor, più alto di Andrea (che è un metro e ottanta).
people: [{ who: 'taglia', at: [470, 296], facing: -1, size: 1.2, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 3.9 }],
hotspots: [ hotspots: [
{ {
id: 'giochi', name: 'giochi', box: [196, 120, 330, 100], at: [360, 250], face: 'up', id: 'giochi', name: 'giochi', box: [196, 120, 330, 100], at: [360, 250], face: 'up',
@@ -1357,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: 'alla_strada', name: 'indietro, sulla strada della stazione', box: [100, 336, 440, 24], at: [300, 352], exit: { scene: 'viale_stazione', at: [620, 280], face: 'left' },
}, },
{
id: 'taglia', name: 'il Taglia', box: [448, 224, 44, 72], at: [430, 306], face: 'right', default: 'talk',
actions: {
look: [{ say: 'Il Taglia. Io sono un metro e ottanta, e lui mi guarda dall\'alto. Con la Condor in mano.' }],
talk: [{ dialogue: 'taglia' }],
},
},
], ],
}, },
// Via Calimara: dalla Piazza scende alla farmacia, in Via Provinciale. // Via Calimara: dalla Piazza scende alla farmacia, in Via Provinciale.
@@ -3314,6 +3336,10 @@ export const story = {
walk: [[150, 360], [300, 250], [280, 236], [340, 236], [420, 248], [640, 330], [640, 360]], walk: [[150, 360], [300, 250], [280, 236], [340, 236], [420, 248], [640, 330], [640, 360]],
depth: [[236, 0.45], [360, 1.35]], depth: [[236, 0.45], [360, 1.35]],
entry: [400, 352], entry: [400, 352],
// Al tramonto, quando la Ghiacciomobile è già un ricordo con la targa, lo zio Mario è nel suo orto (Andrea,
// 30 ottobre), e ti aspetta a cena.
speakers: { zio_mario: [250, 240] },
people: [{ who: 'zio_mario', when: 'indizio_regolo', at: [250, 318], facing: -1, idle: 'stand', talk: ['talk', 'explain', 'point'] }],
hotspots: [ hotspots: [
{ {
id: 'casa_zio', name: 'casa dello zio Mario', box: [80, 90, 200, 90], at: [260, 290], face: 'left', id: 'casa_zio', name: 'casa dello zio Mario', box: [80, 90, 200, 90], at: [260, 290], face: 'left',
@@ -3333,6 +3359,22 @@ export const story = {
{ {
id: 'indietro', name: 'indietro, verso il ponte delle poste', box: [150, 336, 490, 24], at: [400, 352], exit: { scene: 'ponte_stazione', at: [158, 192], face: 'down' }, id: 'indietro', name: 'indietro, verso il ponte delle poste', box: [150, 336, 490, 24], at: [400, 352], exit: { scene: 'ponte_stazione', at: [158, 192], face: 'down' },
}, },
{
id: 'zio_mario', name: 'lo zio Mario', box: [228, 248, 44, 72], at: [300, 330], face: 'left', default: 'talk', when: 'indizio_regolo',
actions: {
look: [{ say: 'Lo zio Mario nel suo orto, col sole che va giù dietro la casa.' }],
// Le battute sono di Andrea (30 ottobre).
talk: [
{ say: 'Ti s\'aspetta a cena, che c\'è la vitella di maiale.', who: 'zio_mario', pose: 'explain' },
{ say: 'Non ti avevo visto, pensavo tu fossi andato all\'estero.' },
{ say: 'Io ho girato tanto... il Falcione.', who: 'zio_mario', pose: 'laugh' },
{ say: 'Zio, che sei stanco?' },
{ say: 'No, dormo!', who: 'zio_mario', pose: 'laugh' },
{ say: 'Zio, a me mi garba Michael Jackson!' },
{ say: 'Mai col gesso!', who: 'zio_mario', pose: 'laugh' },
],
},
},
], ],
}, },
// La strada fra il bivio della Provinciale (alle spalle) e la stazione (in fondo); a destra si va al campone delle // La strada fra il bivio della Provinciale (alle spalle) e la stazione (in fondo); a destra si va al campone delle
@@ -3900,6 +3942,33 @@ export const story = {
}, },
}, },
}, },
// Il Taglia al campone delle poste, con la Condor (Andrea, 29 ottobre). [DA ANDREA: battute provvisorie]
taglia: {
nodes: {
start: {
options: [
{ text: 'Taglia! Si fa una gara con la Condor?', next: 'livello', then: [
{ say: 'Io ho già tirato: ora tocca a te. Dodici pallini gialli, e devi fare più di me.', who: 'taglia', pose: 'explain' },
{ say: 'Come la vuoi?', who: 'taglia', pose: 'talk' },
] },
{ text: 'A cosa si tira?', then: [
{ say: 'Il cartone sull\'albero, le lattine sulla panchina, le bottigliette sulla porta e il palloncino all\'altalena.', who: 'taglia', pose: 'explain' },
{ say: 'E se la vuoi seria, ogni tanto ti tiro una lattina in aria.', who: 'taglia', pose: 'laugh' },
] },
{ text: 'Ma quanto sei alto, Taglia?', then: [{ say: 'Abbastanza da vedere i pallini arrivare prima di te.', who: 'taglia', pose: 'laugh' }] },
{ text: 'Ci si vede, Taglia.', next: 'end' },
],
},
livello: {
options: [
{ text: 'Alla bona.', next: 'end', then: [{ say: 'Alla bona: ti faccio vedere dove arriva il pallino.', who: 'taglia', pose: 'talk' }, { closeup: 'condor', level: 'bona' }] },
{ text: 'Seria.', next: 'end', then: [{ say: 'Seria. Occhio alla lattina in aria.', who: 'taglia', pose: 'explain' }, { closeup: 'condor', level: 'seria' }] },
{ text: 'A fassi male.', next: 'end', then: [{ say: 'A fassi male? Allora tiro più forte, e più spesso.', who: 'taglia', pose: 'laugh' }, { closeup: 'condor', level: 'male' }] },
{ text: 'No, dopo.', next: 'start' },
],
},
},
},
// Il Chiaru al parcheggio di Cardetole, accanto alla pista col gesso (Andrea, 29 ottobre). [DA ANDREA: battute provvisorie] // Il Chiaru al parcheggio di Cardetole, accanto alla pista col gesso (Andrea, 29 ottobre). [DA ANDREA: battute provvisorie]
chiaru: { chiaru: {
nodes: { nodes: {
+1 -1
View File
@@ -1,5 +1,5 @@
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json. // La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
export const VERSION = '0.1.53'; export const VERSION = '0.1.55';
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica, // Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica,
// mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf). // mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf).
+4 -4
View File
@@ -62,16 +62,16 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
/* La guida semplice del GP: il Gas si tiene premuto col pollice, più alto. */ /* La guida semplice del GP: il Gas si tiene premuto col pollice, più alto. */
#bocce-throw.hold { min-height: 72px; font-size: 14px; } #bocce-throw.hold { min-height: 72px; font-size: 14px; }
.bocce-adjust button:disabled { opacity: .4; cursor: default; } .bocce-adjust button:disabled { opacity: .4; cursor: default; }
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; } #bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary, #gara-help summary, #rinquarto-help summary, #micro-help summary, #cerbottana-help summary { cursor: pointer; padding: 6px 0; } #bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary, #gara-help summary, #rinquarto-help summary, #micro-help summary, #cerbottana-help, #condor-help summary { cursor: pointer; padding: 6px 0; }
#bocce-help p, #biliardo-help p, #basket-help p, #pesca-help p, #gara-help p, #rinquarto-help p, #micro-help p, #cerbottana-help p { max-width: 76em; line-height: 1.7; } #bocce-help p, #biliardo-help p, #basket-help p, #pesca-help p, #gara-help p, #rinquarto-help p, #micro-help p, #cerbottana-help, #condor-help p { max-width: 76em; line-height: 1.7; }
@media (max-width: 600px), (orientation: landscape) and (max-height: 520px) { @media (max-width: 600px), (orientation: landscape) and (max-height: 520px) {
#bocce-controls { padding: 8px; font-size: 10px; overflow-y: auto; min-height: 0; } #bocce-controls { padding: 8px; font-size: 10px; overflow-y: auto; min-height: 0; }
.bocce-adjust { flex-wrap: wrap; gap: 6px; } .bocce-adjust { flex-wrap: wrap; gap: 6px; }
.bocce-adjust label { order: -1; flex-basis: 100%; max-width: none; } .bocce-adjust label { order: -1; flex-basis: 100%; max-width: none; }
.bocce-adjust button { flex: 1; font-size: 9px; padding: 6px; } .bocce-adjust button { flex: 1; font-size: 9px; padding: 6px; }
#bocce-throw { flex-basis: 100%; } #bocce-throw { flex-basis: 100%; }
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help { font-size: 9px; } #bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help { font-size: 9px; }
} }
.effects-control { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; font-size: clamp(9px, 1.2vw, 13px); } .effects-control { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; font-size: clamp(9px, 1.2vw, 13px); }
#effects-volume { width: 130px; accent-color: var(--accent); } #effects-volume { width: 130px; accent-color: var(--accent); }
+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);
});
+14
View File
@@ -75,3 +75,17 @@ test('Via Liguri Magelli: prima la Ghiacciomobile nel muro (senza targa), dal tr
assert.ok(g.hotspot('targa') && !g.hotspot('ghiacciomobile')); assert.ok(g.hotspot('targa') && !g.hotspot('ghiacciomobile'));
assert.ok(existsSync(new URL('../assets/scenes/via-liguri-magelli-muro.png', import.meta.url))); assert.ok(existsSync(new URL('../assets/scenes/via-liguri-magelli-muro.png', import.meta.url)));
}); });
test('lo zio Mario: davanti alla Ghiacciomobile fino al tramonto, poi nel suo orto', () => {
const g = new Game(story);
g.go('via_liguri_magelli'); settle(g);
assert.ok(g.hotspot('zio_mario'));
g.go('orti_zio_mario'); settle(g);
assert.ok(!g.hotspot('zio_mario'));
g.state.flags.indizio_regolo = true;
assert.ok(g.hotspot('zio_mario'));
assert.ok(inside(story.scenes.orti_zio_mario.walk, g.hotspot('zio_mario').at));
g.act('talk', 'zio_mario'); settle(g);
g.go('via_liguri_magelli'); settle(g);
assert.ok(!g.hotspot('zio_mario'));
});
+2 -1
View File
@@ -18,5 +18,6 @@
"bluffare": "bleffare", "bluffare": "bleffare",
"bluffava": "bleffava", "bluffava": "bleffava",
"Imposto": "Impòsto", "Imposto": "Impòsto",
"Libici": "Lìbici" "Libici": "Lìbici",
"mancano": "màncano"
} }
+3
View File
@@ -70,5 +70,8 @@
"pescatori": { "pescatori": {
"speaker": "Dionisio Schuyler", "speaker": "Dionisio Schuyler",
"filtro": "asetrate=24000*1.08,aresample=24000,atempo=1.04" "filtro": "asetrate=24000*1.08,aresample=24000,atempo=1.04"
},
"taglia": {
"speaker": "Aaron Dreschner"
} }
} }