Compare commits

...
4 Commits
34 changed files with 902 additions and 17 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 324 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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+44
View File
@@ -2835,6 +2835,10 @@
"file": "andrea-3f77b83f35.mp3", "file": "andrea-3f77b83f35.mp3",
"sec": 0.87 "sec": 0.87
}, },
"andrea|E una partita a campana, Taglia?": {
"file": "andrea-595df0a426.mp3",
"sec": 2.23
},
"andrea|Ma quanto sei alto, Taglia?": { "andrea|Ma quanto sei alto, Taglia?": {
"file": "andrea-01ecdf3ed8.mp3", "file": "andrea-01ecdf3ed8.mp3",
"sec": 1.96 "sec": 1.96
@@ -2859,6 +2863,14 @@
"file": "taglia-ac012a1066.mp3", "file": "taglia-ac012a1066.mp3",
"sec": 4.21 "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.": { "taglia|Abbastanza da vedere i pallini arrivare prima di te.": {
"file": "taglia-d99c875252.mp3", "file": "taglia-d99c875252.mp3",
"sec": 2.57 "sec": 2.57
@@ -2875,6 +2887,18 @@
"file": "taglia-4117f6613d.mp3", "file": "taglia-4117f6613d.mp3",
"sec": 4.84 "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?!?!": { "andrea|Si fa una gara alle macchinine?!?!": {
"file": "andrea-17216997e2.mp3", "file": "andrea-17216997e2.mp3",
"sec": 2.74 "sec": 2.74
@@ -3343,6 +3367,26 @@
"file": "andrea-aba12c5241.mp3", "file": "andrea-aba12c5241.mp3",
"sec": 3.1 "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.": { "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.
+2
View File
@@ -36,7 +36,9 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p
| `characters/zio_mario.png` | Codex | `foto/zio-mario.png` | Andrea (29 ottobre) | sì (famiglia di Andrea) | | `characters/zio_mario.png` | Codex | `foto/zio-mario.png` | Andrea (29 ottobre) | sì (famiglia di Andrea) |
| `terrazza-cardetole.png` | Codex (dalla foto, poi il terrazzo davanti) | `cardetole-finestra.jpg` | Andrea (29 ottobre) | **da decidere**: come `cardetole.png`, dice dove abita Andrea | | `terrazza-cardetole.png` | Codex (dalla foto, poi il terrazzo davanti) | `cardetole-finestra.jpg` | Andrea (29 ottobre) | **da decidere**: come `cardetole.png`, dice dove abita Andrea |
| `closeups/muro-cerbottana.png` | Codex, solo testo (stile dalla piazza) | nessuna | — | sì | | `closeups/muro-cerbottana.png` | Codex, solo testo (stile dalla piazza) | nessuna | — | sì |
| `closeups/macchina-dietro.png`, `macchina-lampi.png`, `macchina-davanti.png` | Codex, dallo sprite della macchina del tempo | nessuna | — | sì |
| `closeups/campone-condor.png` | Codex, dallo sfondo del campone delle poste | nessuna | — | sì | | `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ì | | `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 |
+14
View File
@@ -18,6 +18,20 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
## Dove siamo arrivati ## 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 - **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]. 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 - **La Condor al campone delle poste (29 ottobre)**: la gara col Taglia (Andrea Tagliaferri, consenso dato) con la
+11
View File
@@ -283,6 +283,17 @@
mira un po' più sopra; alla bona il puntino giallo segna dove arriva. Il palloncino e la lattina in aria 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> muovono: si mira dove saranno. Si può anche premere Spara, senza trattenere il fiato. Frecce per spostare il mirino.</p>
</details> </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> <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.55", "version": "0.1.57",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -11,6 +11,7 @@
"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:condor": "node tests/condor.browser.mjs",
"browser:campana": "node tests/campana.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",
+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}`;
}
+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); }
}
+6 -1
View File
@@ -12,7 +12,9 @@ import * as scopa from './scopa.js';
import * as gara from './gara.js'; import * as gara from './gara.js';
import * as micro from './micro.js'; import * as micro from './micro.js';
import * as cerbottana from './cerbottana.js'; import * as cerbottana from './cerbottana.js';
import { newCorsa, updateCorsa } from './corsa.js';
import * as condor from './condor.js'; import * as condor from './condor.js';
import * as campana from './campana.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.
@@ -28,6 +30,7 @@ export const MINIGAMES = {
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 }, 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 }, 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 },
}; };
@@ -325,7 +328,7 @@ export class Game {
return true; return true;
} }
// Il viaggio si chiude da solo alla fine (o uscendo prima): si arriva comunque. // Il viaggio si chiude da solo alla fine (o uscendo prima): si arriva comunque.
if (def?.type === 'viaggio') { this.queue.unshift(...def.complete); return true; } if (def?.type === 'viaggio' || def?.type === 'corsa') { this.queue.unshift(...def.complete); return true; }
// I minigiochi: a partita finita (risultato deciso, anche se il conteggio si sta ancora mostrando) la battuta // I minigiochi: a partita finita (risultato deciso, anche se il conteggio si sta ancora mostrando) la battuta
// di chi ha vinto; uscendo prima, quella di chi lascia. // di chi ha vinto; uscendo prima, quella di chi lascia.
if (MINIGAMES[def?.type]) { if (MINIGAMES[def?.type]) {
@@ -486,6 +489,7 @@ export class Game {
if (c) { if (c) {
if (this.story.closeups[c.id].type === 'torch') { this.updateTorch(dt); return; } if (this.story.closeups[c.id].type === 'torch') { this.updateTorch(dt); return; }
if (this.story.closeups[c.id].type === 'viaggio') { this.updateTrip(dt); return; } if (this.story.closeups[c.id].type === 'viaggio') { this.updateTrip(dt); return; }
if (this.story.closeups[c.id].type === 'corsa') { if (updateCorsa(c, this.story.closeups[c.id], dt)) this.closeupClose(); return; }
const m = this.minigame(); const m = this.minigame();
if (m) { if (m) {
m.update(c, dt); m.update(c, dt);
@@ -528,6 +532,7 @@ export class Game {
const type = this.story.closeups[e.closeup].type; const type = this.story.closeups[e.closeup].type;
this.closeup = type === 'torch' ? { id: e.closeup, step: 'search', t: 0, age: 0, found: 0, dwell: 0, beam: [320, 180] } this.closeup = type === 'torch' ? { id: e.closeup, step: 'search', t: 0, age: 0, found: 0, dwell: 0, beam: [320, 180] }
: type === 'viaggio' ? { id: e.closeup, t: 0, age: 0, events: [], typed: 0 } : type === 'viaggio' ? { id: e.closeup, t: 0, age: 0, events: [], typed: 0 }
: type === 'corsa' ? newCorsa(e.closeup)
// `level`: «Alla bona», «Seria» o «A fassi male», scelto nel dialogo con l'avversario. // `level`: «Alla bona», «Seria» o «A fassi male», scelto nel dialogo con l'avversario.
// `decks`: il mazzo da usare quando il suo flag è acceso (briscola: le napoletane al posto delle toscane). // `decks`: il mazzo da usare quando il suo flag è acceso (briscola: le napoletane al posto delle toscane).
: MINIGAMES[type] ? { id: e.closeup, ...MINIGAMES[type].create(Math.floor(Math.random() * 4294967296), e.level, : MINIGAMES[type] ? { id: e.closeup, ...MINIGAMES[type].create(Math.floor(Math.random() * 4294967296), e.level,
+17
View File
@@ -11,7 +11,9 @@ import { drawPesca } from './pesca-render.js';
import { drawGara } from './gara-render.js'; import { drawGara } from './gara-render.js';
import { drawMicro } from './micro-render.js'; import { drawMicro } from './micro-render.js';
import { drawCerbottana } from './cerbottana-render.js'; import { drawCerbottana } from './cerbottana-render.js';
import { drawCorsa } from './corsa.js';
import { drawCondor } from './condor-render.js'; import { drawCondor } from './condor-render.js';
import { drawCampana } from './campana-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';
@@ -686,6 +688,16 @@ function drawCloseup(ctx, game, time) {
drawViaggio(ctx, c, def, backgrounds.get(def.picture)); drawViaggio(ctx, c, def, backgrounds.get(def.picture));
return; return;
} }
// La macchina del tempo da fuori: lo sfondo di adesso della scena, con la luce della fase del giorno.
if (def.type === 'corsa') {
const bg = game.backdrop(game.story.scenes[def.scene]), frames = {};
for (const [k, src] of [[bg, bg], ...Object.entries(def.frames)]) {
if (!backgrounds.has(src)) loadBackground(src);
if (k !== bg) frames[k] = backgrounds.get(src);
}
drawCorsa(ctx, c, def, { bg: backgrounds.get(bg), frames, tint: game.phase()?.tint });
return;
}
if (def.type === 'torch') { if (def.type === 'torch') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture); if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawTorch(ctx, c, def, backgrounds.get(def.picture)); drawTorch(ctx, c, def, backgrounds.get(def.picture));
@@ -707,6 +719,11 @@ function drawCloseup(ctx, game, time) {
drawCondor(ctx, c, def, backgrounds.get(def.picture)); drawCondor(ctx, c, def, backgrounds.get(def.picture));
return; return;
} }
if (def.type === 'campana') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawCampana(ctx, c, def, backgrounds.get(def.picture));
return;
}
if (def.type === 'micro') { if (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); });
+8
View File
@@ -145,6 +145,14 @@ export class Soundscape {
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-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-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 === 'condor-lancio') this.noiseSound(0.2, 700, 0.03, { pan });
// La macchina del tempo: il tuono del fulmine e lo sfrigolio delle sgommate di fuoco.
else if (kind === 'corsa-tuono') { this.noiseSound(0.9, 400, 0.16, { pan }); this.tone(90, 40, 0.8, 0.1, 0, pan); }
else if (kind === 'corsa-fuoco') this.noiseSound(1.6, 2400, 0.05, { pan });
// La campana: il sassolino che vola e batte sull'asfalto, il salto, il piede giù.
else if (kind === 'campana-lancio') this.noiseSound(0.1, 1500, 0.025, { pan });
else if (kind === 'campana-sasso') { this.tone(1900, 1100, 0.04, 0.05 * strength + 0.02, 0, pan); this.tone(1500, 900, 0.03, 0.03, 0.06, pan); this.noiseSound(0.04, 3000, 0.03, { pan }); }
else if (kind === 'campana-salto') { this.tone(110, 60, 0.07, 0.07, 0, pan); this.noiseSound(0.05, 500, 0.04, { pan }); }
else if (kind === 'campana-riga') { this.tone(300, 180, 0.12, 0.05, 0, pan); this.tone(220, 120, 0.16, 0.05, 0.1, pan); }
else if (kind === 'cerbottana-tosse') { this.noiseSound(0.18, 900, 0.08, { pan }); this.tone(260, 140, 0.12, 0.06, 0.25, pan); this.tone(240, 120, 0.1, 0.05, 0.3, pan); } 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 }); }
+7 -4
View File
@@ -14,6 +14,7 @@ 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 condor from './engine/condor.js';
import * as campana from './engine/campana.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';
@@ -587,7 +588,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', 'condor'].includes(story.closeups[game.closeup.id].type); const aiming = game.closeup && ['biliardo', 'rinquarto', 'bocce', 'basket', 'pesca', 'micro', 'cerbottana', 'condor', 'campana'].includes(story.closeups[game.closeup.id].type);
if (aiming) { if (e.pointerType === 'mouse' && !press && !game.closeup.aimLocked) game.closeupAim(scenePoint(e)); } 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));
}); });
@@ -611,6 +612,7 @@ const MINI = {
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.' }, condor: { ...condor, back: 'Torna al campone', label: 'La Condor: muovi il mouse o tocca il campo per mirare, tieni premuto per trattenere il fiato e lascia per sparare; oppure premi Spara. Frecce per spostare il mirino.' },
campana: { ...campana, back: 'Torna al campone', label: 'La campana: muovi il mouse o tocca la campana per mirare, tieni premuto per la forza e lascia per tirare il sassolino; oppure regola la forza e premi Tira (frecce su e giù per mirare, destra e sinistra per la forza). Ai salti premi, o Spazio, quando la lancetta è nel verde.' },
micro: { ...micro, back: 'Torna dal Chiaru', label: 'La pista col gesso: muovi il mouse o tocca il parcheggio per mirare, tieni premuto per caricare la spinta e lascia; oppure regola la forza e premi Spingi. Frecce o pulsanti Mira per girare.' }, 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).
@@ -1040,7 +1042,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', condor: 'Comandi della Condor' }[def.type] ?? 'Comandi delle bocce'); setAttr($('bocce-controls'), 'aria-label', { rinquarto: 'Comandi dei rinquarti', biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca', gara: 'Comandi della gara', micro: 'Comandi delle macchinine', cerbottana: 'Comandi della cerbottana', condor: 'Comandi della Condor', campana: 'Comandi della campana' }[def.type] ?? 'Comandi delle bocce');
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true); 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;
@@ -1055,7 +1057,7 @@ function updateUi() {
// 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' || def.type === 'condor' || 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 || def.type === 'campana');
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;
setAttr($('bocce-power'), 'aria-label', def.type === 'gara' ? 'Sensibilità dello sterzo col dito' : 'Forza del lancio'); setAttr($('bocce-power'), 'aria-label', def.type === 'gara' ? 'Sensibilità dello sterzo col dito' : 'Forza del lancio');
@@ -1076,6 +1078,7 @@ function updateUi() {
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($('condor-help'), 'hidden', def.type !== 'condor');
setProp($('campana-help'), 'hidden', def.type !== 'campana');
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');
@@ -1096,7 +1099,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', 'condor'].includes(story.closeups[c.id].type)); $('stage').classList.toggle('biliardo', !!c && ['biliardo', 'rinquarto', 'gara', 'micro', 'cerbottana', 'condor', 'campana'].includes(story.closeups[c.id].type));
$('stage').classList.toggle('briscola', !!cards); $('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.
+43
View File
@@ -43,11 +43,19 @@ const torchHints = [{
}], }],
}]; }];
// La macchina del tempo che corre, vista da dietro e (all'arrivo) davanti: larga `width` pixel da vicino, le ruote a
// `track` della larghezza dal centro.
const MACCHINA_CORSA = {
frames: { dietro: 'assets/closeups/macchina-dietro.png', lampi: 'assets/closeups/macchina-lampi.png', davanti: 'assets/closeups/macchina-davanti.png' },
width: 220, track: 75,
};
// Il ritorno dal 1997, dalla porta o dal pulsante «Torna al 2007»: la macchina del tempo sparisce, e per rifare il // Il ritorno dal 1997, dalla porta o dal pulsante «Torna al 2007»: la macchina del tempo sparisce, e per rifare il
// viaggio si richiede all'Arcu (Andrea, 29 ottobre). // viaggio si richiede all'Arcu (Andrea, 29 ottobre).
const RITORNO_2007 = [ const RITORNO_2007 = [
{ say: 'Si torna nel 2007. Ciao Vannino, ciao Pierino.' }, { say: 'Si torna nel 2007. Ciao Vannino, ciao Pierino.' },
{ go: 'via_liguri_magelli', at: [212, 280], face: 'down' }, { go: 'via_liguri_magelli', at: [212, 280], face: 'down' },
{ closeup: 'arrivo' },
{ unset: 'macchina_trappola' }, { unset: 'macchina_trappola' },
{ say: 'Riportata dove l\'ho presa. Se ti riviene voglia di pokerini, chiedi.', who: 'arcu', pose: 'explain' }, { say: 'Riportata dove l\'ho presa. Se ti riviene voglia di pokerini, chiedi.', who: 'arcu', pose: 'explain' },
]; ];
@@ -249,8 +257,20 @@ export const story = {
picture: 'assets/closeups/cruscotto-tempo.png', picture: 'assets/closeups/cruscotto-tempo.png',
display: [220, 158, 212, 62], display: [220, 158, 212, 62],
date: '10/07/1997 18:30', date: '10/07/1997 18:30',
complete: [{ closeup: 'partenza' }],
},
// La partenza vista da fuori: giù per Via Liguri Magelli, il fulmine e le sgommate di fuoco; e l'arrivo, al
// ritorno dal 1997 (Andrea, 30 ottobre; la macchina da dietro e davanti è di Codex).
partenza: {
type: 'corsa', mode: 'parti', scene: 'via_liguri_magelli', ...MACCHINA_CORSA,
path: [[215, 350, 1], [252, 192, 0.05]],
complete: [{ go: 'barrettino_1997', at: [320, 330], face: 'up' }], complete: [{ go: 'barrettino_1997', at: [320, 330], face: 'up' }],
}, },
arrivo: {
type: 'corsa', mode: 'arriva', scene: 'via_liguri_magelli', ...MACCHINA_CORSA,
path: [[110, 470, 2.2], [252, 192, 0.05]],
complete: [],
},
// Il pokerino del Barrettino con Vannino e Pierino (Andrea, 29 ottobre). [DA ANDREA: le battute] // Il pokerino del Barrettino con Vannino e Pierino (Andrea, 29 ottobre). [DA ANDREA: le battute]
poker: { poker: {
type: 'poker', type: 'poker',
@@ -281,6 +301,17 @@ export const story = {
draw: [{ say: 'Pari. Si rifà?' }, { say: 'Si rifà.', who: 'taglia', pose: 'explain' }], draw: [{ say: 'Pari. Si rifà?' }, { say: 'Si rifà.', who: 'taglia', pose: 'explain' }],
quit: [{ say: 'La Condor la rendo al Taglia. Per ora.' }], quit: [{ say: 'La Condor la rendo al Taglia. Per ora.' }],
}, },
// La campana col gesso sul vialetto del campone delle poste, a turni col Taglia (Andrea, 30 ottobre).
// [DA ANDREA: le battute sono provvisorie]
campana: {
type: 'campana',
picture: 'assets/closeups/campone-campana.png',
names: { player: 'Andrea', cpu: 'il Taglia' },
win: [{ say: 'Dalla 1 fino in cima, e ritorno. A ventisett\'anni, davanti alle poste.' }, { say: 'Te lo dico io: se ti vede qualcuno, domani lo sa tutto il paese.', who: 'taglia', pose: 'laugh' }, { set: 'campana_vinta' }],
lose: [{ say: 'Il Taglia, su un piede solo, è più alto di me su due.' }, { say: 'È questione di gambe lunghe.', who: 'taglia', pose: 'explain' }],
draw: [],
quit: [{ say: 'Il gesso lo lascio lì. Ci giocherà qualche bambino, che è più in allenamento di me.' }],
},
// 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: {
@@ -3955,6 +3986,10 @@ export const story = {
{ say: 'Il cartone sull\'albero, le lattine sulla panchina, le bottigliette sulla porta e il palloncino all\'altalena.', who: 'taglia', pose: 'explain' }, { 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' }, { say: 'E se la vuoi seria, ogni tanto ti tiro una lattina in aria.', who: 'taglia', pose: 'laugh' },
] }, ] },
{ text: 'E una partita a campana, Taglia?', next: 'campana', then: [
{ say: 'A campana? Alla nostra età?', who: 'taglia', pose: 'laugh' },
{ say: 'Va bene: il gesso ce l\'ho in tasca, il sassolino lo scelgo io. Sul vialetto, dalla 1 alla 8, e chi pesta la riga passa la mano.', who: 'taglia', pose: 'explain' },
] },
{ text: 'Ma quanto sei alto, Taglia?', then: [{ say: 'Abbastanza da vedere i pallini arrivare prima di te.', who: 'taglia', pose: 'laugh' }] }, { text: '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' }, { text: 'Ci si vede, Taglia.', next: 'end' },
], ],
@@ -3967,6 +4002,14 @@ export const story = {
{ text: 'No, dopo.', next: 'start' }, { text: 'No, dopo.', next: 'start' },
], ],
}, },
campana: {
options: [
{ text: 'Alla bona.', next: 'end', then: [{ say: 'Alla bona: le righe un contano. Comincia te.', who: 'taglia', pose: 'talk' }, { closeup: 'campana', level: 'bona' }] },
{ text: 'Seria.', next: 'end', then: [{ say: 'Seria: chi tocca la riga è fuori. Comincia te.', who: 'taglia', pose: 'explain' }, { closeup: 'campana', level: 'seria' }] },
{ text: 'A fassi male.', next: 'end', then: [{ say: 'A fassi male? Io la campana la facevo a occhi chiusi.', who: 'taglia', pose: 'laugh' }, { closeup: 'campana', level: 'male' }] },
{ text: 'No, dopo.', next: 'start' },
],
},
}, },
}, },
// Il Chiaru al parcheggio di Cardetole, accanto alla pista col gesso (Andrea, 29 ottobre). [DA ANDREA: battute provvisorie] // Il Chiaru al parcheggio di Cardetole, accanto alla pista col gesso (Andrea, 29 ottobre). [DA ANDREA: battute provvisorie]
+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.55'; export const VERSION = '0.1.57';
// 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, #condor-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, #campana-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary, #gara-help summary, #rinquarto-help summary, #micro-help summary, #cerbottana-help, #condor-help summary { 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, #campana-help summary { cursor: pointer; padding: 6px 0; }
#bocce-help p, #biliardo-help p, #basket-help p, #pesca-help p, #gara-help p, #rinquarto-help p, #micro-help p, #cerbottana-help, #condor-help p { 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, #campana-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, #condor-help { font-size: 9px; } #bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help, #campana-help { font-size: 9px; }
} }
.effects-control { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; font-size: clamp(9px, 1.2vw, 13px); } .effects-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); }
+75
View File
@@ -0,0 +1,75 @@
// La campana nel browser vero: dal Taglia al campone delle poste, il tiro col mouse e col dito, i salti.
// npm run browser:campana — schermate in /tmp/regolo-campana.
import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs';
const out = process.env.OUT ?? '/tmp/regolo-campana';
mkdirSync(out, { recursive: true });
const server = await staticServer();
let browser;
const errors = [];
try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) {
const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile });
page.on('pageerror', e => errors.push(e.message));
await page.goto(`http://127.0.0.1:${server.port}/`);
await page.click('#splash'); await page.click('#new-game'); await page.click('#rione [data-rione="campomigliaio"]'); await page.click('#skip-intro');
await page.evaluate(async () => {
const { settle } = await import('/tests/helpers.mjs');
settle(game); game.go('giardini_poste'); settle(game);
game.act('talk', 'taglia'); settle(game);
game.choose(game.options().findIndex(o => o.text.startsWith('E una partita a campana'))); settle(game);
game.choose(game.options().findIndex(o => o.text === 'Alla bona.')); settle(game);
});
await page.waitForSelector('#bocce-controls:not([hidden])');
await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/campone-campana.png'));
assert.equal(await page.locator('#bocce-throw').textContent(), 'Tira');
assert.ok(await page.locator('#campana-help').isVisible());
assert.ok(!(await page.locator('#bocce-left').isVisible()), 'niente pulsanti Mira');
const box = await page.locator('#scene').boundingBox(), P = (x, y) => [box.x + x / 640 * box.width, box.y + y / 360 * box.height];
// Mira alla 1, poi la forza giusta: col mouse tenendo premuto, col dito il cursore e Tira.
const one = await page.evaluate(async () => { const m = window.campanaModule = await import('/src/engine/campana.js'); const p = m.center(1); return [p.x, p.y, Math.hypot(p.x - m.FROM.x, p.y - m.FROM.y) / m.REACH]; });
if (mobile) {
await page.touchscreen.tap(...P(one[0], one[1]));
assert.equal(await page.evaluate(() => game.closeup.step), 'aim', 'un tocco mira soltanto');
await page.evaluate(p => { game.closeup.power = p; game.closeup.skill.scatter = 0; }, one[2]);
await page.locator('#bocce-throw').tap();
} else {
await page.mouse.move(...P(one[0], one[1]));
await page.evaluate(() => { game.closeup.skill.scatter = 0; });
await page.mouse.down();
await page.waitForFunction(p => Math.abs(game.closeup.power - p) < 0.02, one[2], { polling: 'raf' });
await page.screenshot({ path: `${out}/${name}-forza.png` });
await page.mouse.up();
// Col mouse il momento giusto dipende da quanto è lesta la pagina: se è andato fuori, si ritira col pulsante.
await page.waitForFunction(() => !['aim', 'flying'].includes(game.closeup.step));
if (await page.evaluate(() => game.closeup.step) !== 'hop') {
await page.evaluate(p => { Object.assign(game.closeup, { step: 'aim', current: 'player', next: null, line: null, power: p }); }, one[2]);
await page.locator('#bocce-throw').click();
}
}
await page.waitForFunction(() => game.closeup.step === 'hop', null, { timeout: 5000 }).catch(async e => { console.log(await page.evaluate(() => JSON.stringify({ ...game.closeup, events: 0 }))); await page.screenshot({ path: `${out}/${name}-errore.png` }); throw e; });
assert.equal(await page.locator('#bocce-throw').textContent(), 'Salta');
// Quanti salti riescono: la lancetta dopo una schermata (lenta, con swiftshader) può già essere scappata.
let hops = 0;
// I salti: si preme quando la lancetta passa nel verde (col dito sul pulsante Salta, col mouse sulla scena).
for (let i = 0; i < 4; i++) {
await page.waitForFunction(() => { const m = window.campanaModule, c = game.closeup; return c.step !== 'hop' || ((!c.hop || c.hop.t >= m.HOP) && c.meter.t > 0 && Math.abs(m.needle(c)) < 0.15); }, null, { polling: 'raf', timeout: 5000 });
if (await page.evaluate(() => game.closeup.step) !== 'hop') break;
if (mobile) await page.locator('#bocce-throw').tap(); else await page.mouse.click(...P(320, 200));
hops = Math.max(hops, await page.evaluate(() => game.closeup.beat + 1));
if (i === 1) await page.screenshot({ path: `${out}/${name}-salti.png` });
}
assert.ok(hops >= 2, `salti riusciti: ${hops}`);
// Poi tocca al Taglia: lo si fa sbagliare per vederlo saltare.
await page.evaluate(() => { const c = game.closeup; c.step = 'rest'; c.t = 0; c.next = 'cpu'; c.line = 'Prova: tocca al Taglia.'; });
await page.waitForFunction(() => game.closeup.step === 'cpuHop' && game.closeup.beat >= 3 || game.closeup.current === 'player' && game.closeup.step === 'aim', null, { timeout: 20000 });
await page.screenshot({ path: `${out}/${name}-taglia.png` });
await page.close();
}
assert.deepEqual(errors, []);
console.log(`ok: schermate in ${out}`);
} finally { await browser?.close(); server.kill(); }
+125
View File
@@ -0,0 +1,125 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as m from '../src/engine/campana.js';
import { drawCampana } from '../src/engine/campana-render.js';
import { Game, MINIGAMES } from '../src/engine/game.js';
import { story } from '../src/story/story.js';
import { settle } from './helpers.mjs';
// Tira senza sparpaglio, con la forza giusta per arrivare al punto, e aspetta che il sassolino cada.
function toss(c, [x, y]) {
c.skill = { ...c.skill, scatter: 0 }; c.aim = { x, y };
c.power = Math.hypot(x - m.FROM.x, y - m.FROM.y) / m.REACH;
assert.ok(m.shoot(c));
for (let i = 0; i < 200 && c.step === 'flying'; i++) m.update(c, 1 / 60);
}
// Salta ogni volta che la lancetta passa nel mezzo.
function hopAll(c) {
for (let i = 0; i < 20000 && c.step === 'hop'; i++) {
m.update(c, 1 / 120);
if (c.step === 'hop' && (!c.hop || c.hop.t >= m.HOP) && Math.abs(m.needle(c)) < 0.03 && c.meter.t > 0) m.press(c, 1);
}
}
const at = id => { const p = m.center(id); return [p.x, p.y]; };
test('le caselle: 1-2-3, la coppia 4-5, la 6, la coppia 7-8 e il cielo; le righe', () => {
for (let n = 1; n <= 8; n++) assert.equal(m.boxAt(at(n), 3).id, n);
assert.equal(m.boxAt(at('cielo')).id, 'cielo');
assert.ok(m.center(4).y < m.center(5).y, 'la 4 a sinistra di chi salta');
const b = m.BOXES[2];
assert.ok(m.boxAt([b.x + 1, m.CY], 3).line, 'sul bordo si tocca la riga');
assert.ok(!m.boxAt([b.x + 1, m.CY], 0).line, 'alla bona le righe non contano');
assert.equal(m.boxAt([20, 20]), null);
});
test('i salti: si scavalca la casella col sassolino, nella coppia un piede solo; tornando lo si raccoglie', () => {
const ids = list => list.map(b => b.pick ? 'prendi' : b.out ? 'fuori' : b.boxes.join('+'));
assert.deepEqual(ids(m.beats(1)), ['2', '3', '4+5', '6', '7+8', 'cielo', '7+8', '6', '4+5', '3', '2', 'prendi', 'fuori']);
assert.deepEqual(ids(m.beats(5)), ['1', '2', '3', '4', '6', '7+8', 'cielo', '7+8', '6', 'prendi', '4', '3', '2', '1', 'fuori']);
assert.deepEqual(ids(m.beats(8)), ['1', '2', '3', '4+5', '6', '7', 'cielo', 'prendi', '7', '6', '4+5', '3', '2', '1', 'fuori']);
assert.ok(m.beats(3).every(b => !b.boxes.includes(3)), 'mai il piede dove sta il sassolino');
});
test('il sassolino nella casella giusta fa saltare; tutta la campana fatta, si passa alla dopo', () => {
const c = m.newCampana(1, { level: 'seria' });
toss(c, at(1));
assert.equal(c.step, 'hop'); assert.match(c.line, /nella 1/);
hopAll(c);
assert.equal(c.step, 'done'); assert.equal(c.target.player, 2); assert.equal(c.stone, null, 'raccolto');
for (let i = 0; i < 120 && c.step !== 'aim'; i++) m.update(c, 1 / 60);
assert.equal(c.current, 'player', 'chi ce la fa ritira');
});
test('fuori, sulla riga o nella casella sbagliata passa la mano al Taglia', () => {
for (const [point, why] of [[at(2), /Nella 2, non nella 1/], [[m.START + 1, m.CY], /riga/], [[60, 40], /fuori/i]]) {
const c = m.newCampana(2, { level: 'seria' });
toss(c, point);
assert.equal(c.step, 'rest'); assert.match(c.line, why);
for (let i = 0; i < 200 && c.step === 'rest'; i++) m.update(c, 1 / 60);
assert.equal(c.current, 'cpu'); assert.equal(c.target.player, 1);
}
});
test('l\'equilibrio: troppo presto o troppo tardi si sbaglia', () => {
const early = m.newCampana(3, { level: 'male' });
toss(early, at(1));
while (early.meter.t < 0.05) m.update(early, 1 / 120);
m.press(early, 1); assert.equal(early.step, 'rest'); assert.match(early.line, /presto/);
const late = m.newCampana(3, { level: 'male' });
toss(late, at(1));
for (let i = 0; i < 1000 && late.step === 'hop'; i++) m.update(late, 1 / 60);
assert.match(late.line, /equilibrio/);
});
test('il tiro col dito: un tocco secco mira soltanto; tenendo premuto la forza va e torna', () => {
const c = m.newCampana(4, { level: 'bona' });
assert.ok(m.aimScreen(c, [300, 150]));
m.press(c, 1); m.update(c, 0.05); m.release(c, 1); assert.equal(c.step, 'aim');
m.press(c, 1);
for (let i = 0; i < 60 * m.CHARGE; i++) m.update(c, 1 / 60);
assert.ok(c.power > 0.95);
for (let i = 0; i < 30 * m.CHARGE; i++) m.update(c, 1 / 60);
assert.ok(c.power < 0.6, 'e poi torna giù');
assert.ok(m.release(c, 1)); assert.equal(c.step, 'flying');
assert.ok(m.key(c, 'ArrowUp', true));
});
test('il Taglia gioca da solo; chi finisce la 8 vince', () => {
const c = m.newCampana(5, { level: 'seria' });
toss(c, [60, 40]);
let turns = 0;
for (let i = 0; i < 60 * 600 && c.step !== 'result'; i++) {
m.update(c, 1 / 60);
if (c.current === 'player' && c.step === 'aim') { turns++; toss(c, [60, 40]); }
}
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'cpu'); assert.equal(c.target.cpu, 9);
assert.ok(turns > 0, 'il Taglia ogni tanto sbaglia e ripassa la mano');
const w = m.newCampana(6, { level: 'bona' });
w.target.player = 8;
toss(w, at(8)); hopAll(w);
assert.equal(w.step, 'result'); assert.equal(w.result.winner, 'player');
});
test('il disegno della campana non cambia lo stato', () => {
const calls = [], ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => calls.push([k, ...args]) });
const c = m.newCampana(7, { level: 'bona' }), bg = {};
const draw = () => { const before = structuredClone(c); drawCampana(ctx, c, story.closeups.campana, bg); assert.deepEqual(c, before); };
draw(); assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'LA CAMPANA'));
toss(c, at(1)); for (let i = 0; i < 40; i++) m.update(c, 1 / 60); draw();
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'EQUILIBRIO'));
c.step = 'result'; c.result = { winner: 'player' }; calls.length = 0; draw();
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'BATTUTO IL TAGLIA!'));
assert.ok(MINIGAMES.campana);
});
test('dal Taglia, al campone delle poste: la campana, il livello e la vittoria', () => {
const g = new Game(story); g.go('giardini_poste'); settle(g);
g.act('talk', 'taglia'); settle(g);
g.choose(g.options().findIndex(o => o.text.startsWith('E una partita a campana'))); settle(g);
g.choose(g.options().findIndex(o => o.text === 'Seria.')); settle(g);
assert.equal(g.closeup?.id, 'campana'); assert.equal(g.closeup.level, 'seria');
g.closeup.step = 'result'; g.closeup.result = { winner: 'player' };
g.closeupClose(); settle(g);
assert.ok(g.state.flags.campana_vinta);
});
+44
View File
@@ -0,0 +1,44 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { existsSync } from 'node:fs';
import * as C from '../src/engine/corsa.js';
import { story } from '../src/story/story.js';
const run = (def, until) => { const c = C.newCorsa('x'); while (c.t < until) C.updateCorsa(c, def, 1 / 60); return c; };
test('la partenza: accelera giù per la strada, il fulmine la prende e sparisce; restano le sgommate', () => {
const def = story.closeups.partenza;
assert.equal(def.mode, 'parti');
for (const src of Object.values(def.frames)) assert.ok(existsSync(src), src);
const at = t => C.progress({ t }, def);
assert.equal(at(0), 0);
assert.ok(at(1.2) > 0 && at(1.6) - at(1.2) > at(1.2) - at(0.8), 'accelera');
assert.ok(C.carShown({ t: 2 }, def) && !C.carShown({ t: C.PARTI.bolt + 0.2 }, def));
const c = run(def, C.PARTI.bolt + 0.1);
assert.ok(c.events.some(e => e.kind === 'corsa-tuono') && c.events.some(e => e.kind === 'corsa-fuoco'));
assert.ok(!C.updateCorsa(c, def, 0.01));
assert.ok(C.updateCorsa(run(def, C.PARTI.end - 0.01), def, 0.05), 'finita');
});
test('l\'arrivo: il fulmine in fondo, poi viene avanti e ci passa accanto, fuori dal quadro', () => {
const def = story.closeups.arrivo;
const at = t => C.progress({ t }, def);
assert.equal(at(0), 1); assert.equal(at(C.ARRIVA.bolt + C.ARRIVA.run + 0.1), 0);
assert.ok(!C.carShown({ t: 0.1 }, def) && C.carShown({ t: 1 }, def));
assert.ok(!C.carShown({ t: C.ARRIVA.bolt + C.ARRIVA.run + 0.1 }, def), 'passata');
assert.ok(at(1.2) < at(0.8));
});
test('il disegno della corsa non cambia lo stato e usa lo sfondo e i fotogrammi', () => {
const calls = [], ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => calls.push([k, ...args]) });
const bg = { width: 640, height: 360 }, frames = { dietro: { width: 220, height: 190 }, lampi: { width: 220, height: 190 }, davanti: { width: 220, height: 190 } };
for (const [id, t] of [['partenza', 1.4], ['partenza', 3.5], ['arrivo', 1.2], ['arrivo', 2.5]]) {
const c = { ...C.newCorsa(id), t }, before = structuredClone(c);
calls.length = 0;
C.drawCorsa(ctx, c, story.closeups[id], { bg, frames, tint: null });
assert.deepEqual(c, before);
assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
assert.equal(calls.some(([op, img]) => op === 'drawImage' && Object.values(frames).includes(img)), t < 2.4, 'dopo il fulmine (o passata) la macchina non c\'è');
assert.ok(calls.filter(([op]) => op === 'fillRect').length > 20, 'le fiamme');
}
});
+17 -6
View File
@@ -5,6 +5,18 @@ import { drawPoker, cardAt, HAND } from '../src/engine/poker-render.js';
import { Game, MINIGAMES, TRIP } from '../src/engine/game.js'; import { Game, MINIGAMES, TRIP } from '../src/engine/game.js';
import { story } from '../src/story/story.js'; import { story } from '../src/story/story.js';
import { settle } from './helpers.mjs'; import { settle } from './helpers.mjs';
import { PARTI, ARRIVA } from '../src/engine/corsa.js';
// I primi piani che vanno da soli (il cruscotto, la partenza, l'arrivo): si lascia scorrere il tempo finché finiscono.
function ride(g, seen = []) {
for (let t = 0; t < TRIP.end + PARTI.end + ARRIVA.end + 2; t += 0.05) {
if (g.closeup && seen.at(-1) !== g.closeup.id) seen.push(g.closeup.id);
if (!g.closeup && !g.queue.length && !g.speech) break;
if (g.speech) g.skip(); else g.update(0.05);
}
settle(g);
return seen;
}
const card = s => ({ rank: s.slice(0, -1), suit: { c: 'cuori', q: 'quadri', f: 'fiori', p: 'picche' }[s.at(-1)] }); const card = s => ({ rank: s.slice(0, -1), suit: { c: 'cuori', q: 'quadri', f: 'fiori', p: 'picche' }[s.at(-1)] });
const hand = s => s.split(' ').map(card); const hand = s => s.split(' ').map(card);
@@ -71,8 +83,7 @@ test('la macchina del tempo: l\'Arcu la fa comparire, il viaggio porta al Barret
assert.ok(!g.dialogue, 'finito il discorso, si è pronti a partire'); assert.ok(!g.dialogue, 'finito il discorso, si è pronti a partire');
g.act('use', 'macchina_tempo'); settle(g); g.act('use', 'macchina_tempo'); settle(g);
assert.equal(g.closeup?.id, 'viaggio'); assert.equal(g.closeup?.id, 'viaggio');
for (let t = 0; t < TRIP.end + 0.5 && g.closeup; t += 0.05) g.update(0.05); assert.deepEqual(ride(g), ['viaggio', 'partenza'], 'il cruscotto, poi la partenza da fuori');
settle(g);
assert.equal(g.state.scene, 'barrettino_1997'); assert.equal(g.state.scene, 'barrettino_1997');
g.act('talk', 'tavola'); settle(g); g.act('talk', 'tavola'); settle(g);
g.choose(g.options().findIndex(o => o.text.startsWith('C\'è posto'))); settle(g); g.choose(g.options().findIndex(o => o.text.startsWith('C\'è posto'))); settle(g);
@@ -82,7 +93,8 @@ test('la macchina del tempo: l\'Arcu la fa comparire, il viaggio porta al Barret
g.closeupClose(); settle(g); g.closeupClose(); settle(g);
assert.ok(g.state.flags.poker_vinto); assert.ok(g.state.flags.poker_vinto);
assert.ok(g.hotspot('arcu'), 'l\'Arcu è venuto anche lui'); assert.ok(g.hotspot('arcu'), 'l\'Arcu è venuto anche lui');
g.act('walk', 'porta'); settle(g); g.act('walk', 'porta');
assert.deepEqual(ride(g), ['arrivo'], 'al ritorno, l\'arrivo col fulmine');
assert.equal(g.state.scene, 'via_liguri_magelli'); assert.equal(g.state.scene, 'via_liguri_magelli');
assert.ok(!g.state.flags.macchina_trappola && !g.hotspot('macchina_tempo'), 'tornati, la macchina sparisce'); assert.ok(!g.state.flags.macchina_trappola && !g.hotspot('macchina_tempo'), 'tornati, la macchina sparisce');
assert.equal(g.goBack(), false, 'nel 2007 il pulsante non c\'è'); assert.equal(g.goBack(), false, 'nel 2007 il pulsante non c\'è');
@@ -91,11 +103,10 @@ test('la macchina del tempo: l\'Arcu la fa comparire, il viaggio porta al Barret
g.choose(g.options().findIndex(o => o.text.startsWith('Bei tempi'))); settle(g); g.choose(g.options().findIndex(o => o.text.startsWith('Bei tempi'))); settle(g);
assert.ok(!g.dialogue, 'finito il discorso, si è pronti a partire'); assert.ok(!g.dialogue, 'finito il discorso, si è pronti a partire');
g.act('use', 'macchina_tempo'); settle(g); g.act('use', 'macchina_tempo'); settle(g);
for (let t = 0; t < TRIP.end + 0.5 && g.closeup; t += 0.05) g.update(0.05); ride(g);
settle(g);
assert.equal(g.state.scene, 'barrettino_1997'); assert.equal(g.state.scene, 'barrettino_1997');
assert.equal(g.scene.back.label, 'Torna al 2007'); assert.equal(g.scene.back.label, 'Torna al 2007');
assert.ok(g.goBack()); settle(g); assert.ok(g.goBack()); ride(g);
assert.equal(g.state.scene, 'via_liguri_magelli'); assert.equal(g.state.scene, 'via_liguri_magelli');
assert.ok(!g.hotspot('macchina_tempo')); assert.ok(!g.hotspot('macchina_tempo'));
}); });