Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f0f60d186 | ||
|
|
5135689293 |
Binary file not shown.
|
After Width: | Height: | Size: 324 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2835,6 +2835,10 @@
|
||||
"file": "andrea-3f77b83f35.mp3",
|
||||
"sec": 0.87
|
||||
},
|
||||
"andrea|E una partita a campana, Taglia?": {
|
||||
"file": "andrea-595df0a426.mp3",
|
||||
"sec": 2.23
|
||||
},
|
||||
"andrea|Ma quanto sei alto, Taglia?": {
|
||||
"file": "andrea-01ecdf3ed8.mp3",
|
||||
"sec": 1.96
|
||||
@@ -2859,6 +2863,14 @@
|
||||
"file": "taglia-ac012a1066.mp3",
|
||||
"sec": 4.21
|
||||
},
|
||||
"taglia|A campana? Alla nostra età?": {
|
||||
"file": "taglia-0a7f69770d.mp3",
|
||||
"sec": 2.77
|
||||
},
|
||||
"taglia|Va bene: il gesso ce l'ho in tasca, il sassolino lo scelgo io. Sul vialetto, dalla 1 alla 8, e chi pesta la riga passa la mano.": {
|
||||
"file": "taglia-c00be84faf.mp3",
|
||||
"sec": 11.54
|
||||
},
|
||||
"taglia|Abbastanza da vedere i pallini arrivare prima di te.": {
|
||||
"file": "taglia-d99c875252.mp3",
|
||||
"sec": 2.57
|
||||
@@ -2875,6 +2887,18 @@
|
||||
"file": "taglia-4117f6613d.mp3",
|
||||
"sec": 4.84
|
||||
},
|
||||
"taglia|Alla bona: le righe un contano. Comincia te.": {
|
||||
"file": "taglia-328d930124.mp3",
|
||||
"sec": 4.12
|
||||
},
|
||||
"taglia|Seria: chi tocca la riga è fuori. Comincia te.": {
|
||||
"file": "taglia-1266463538.mp3",
|
||||
"sec": 4.23
|
||||
},
|
||||
"taglia|A fassi male? Io la campana la facevo a occhi chiusi.": {
|
||||
"file": "taglia-7938d8d5e4.mp3",
|
||||
"sec": 5.05
|
||||
},
|
||||
"andrea|Si fa una gara alle macchinine?!?!": {
|
||||
"file": "andrea-17216997e2.mp3",
|
||||
"sec": 2.74
|
||||
@@ -3343,6 +3367,26 @@
|
||||
"file": "andrea-aba12c5241.mp3",
|
||||
"sec": 3.1
|
||||
},
|
||||
"andrea|Dalla 1 fino in cima, e ritorno. A ventisett'anni, davanti alle poste.": {
|
||||
"file": "andrea-68378e4c21.mp3",
|
||||
"sec": 6.15
|
||||
},
|
||||
"taglia|Te lo dico io: se ti vede qualcuno, domani lo sa tutto il paese.": {
|
||||
"file": "taglia-1e6660932d.mp3",
|
||||
"sec": 3.34
|
||||
},
|
||||
"andrea|Il Taglia, su un piede solo, è più alto di me su due.": {
|
||||
"file": "andrea-3ed6165062.mp3",
|
||||
"sec": 3.31
|
||||
},
|
||||
"taglia|È questione di gambe lunghe.": {
|
||||
"file": "taglia-9782816264.mp3",
|
||||
"sec": 1.71
|
||||
},
|
||||
"andrea|Il gesso lo lascio lì. Ci giocherà qualche bambino, che è più in allenamento di me.": {
|
||||
"file": "andrea-356a1afe8d.mp3",
|
||||
"sec": 5.91
|
||||
},
|
||||
"chiaru|Hai vinto te. Ma la mia aveva le ruote piene di gesso, sia chiaro.": {
|
||||
"file": "chiaru-91a4b23b22.mp3",
|
||||
"sec": 4.44
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -38,6 +38,7 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p
|
||||
| `closeups/muro-cerbottana.png` | Codex, solo testo (stile dalla piazza) | nessuna | — | sì |
|
||||
| `closeups/macchina-dietro.png`, `macchina-lampi.png`, `macchina-davanti.png` | Codex, dallo sprite della macchina del tempo | nessuna | — | sì |
|
||||
| `closeups/campone-condor.png` | Codex, dallo sfondo del campone delle poste | nessuna | — | sì |
|
||||
| `closeups/campone-campana.png` | Codex, solo testo (stile dal campone delle poste) | nessuna | — | sì |
|
||||
| `barrettino-1997.png`, `closeups/cruscotto-tempo.png` | Codex, solo testo | nessuna | — | sì |
|
||||
| `characters/vannino.png`, `characters/pierino_parigi.png` | Codex, dalla descrizione di Andrea (niente foto) | nessuna | — | sì (Pierino: consenso del figlio Marco) |
|
||||
| `cardetole.png` | Codex | `cardetole.jpg` | Andrea (EXIF) | **da decidere**: è la casa vera di Andrea, pubblicarla dice dove abita |
|
||||
|
||||
@@ -18,6 +18,16 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
||||
|
||||
## Dove siamo arrivati
|
||||
|
||||
- **La campana al campone delle poste (30 ottobre)**: dal Taglia, «E una partita a campana, Taglia?», poi il livello.
|
||||
Primo piano `campana` visto dall'alto sul vialetto (`campone-campana.png`, Codex); la campana col gesso la disegna il
|
||||
codice (`campana.js`, `campana-render.js`): 1-2-3, coppia 4-5, 6, coppia 7-8 e in cima il mezzo cerchio con scritto
|
||||
CAMPANA (Andrea: non «cielo»), ogni casella tinta tenue col suo gesso. Il sassolino: la mira dà la direzione, tenendo
|
||||
premuto la forza va e torna, si lascia; deve cadere nella casella che tocca senza toccare le righe (alla bona le
|
||||
righe non contano e il puntino giallo mostra dove cade). I salti: la lancetta dell'equilibrio attraversa la barra e
|
||||
si preme nel verde (presto: giù l'altro piede; tardi: pestata la riga); si scavalca la casella col sassolino e
|
||||
tornando lo si raccoglie. Chi ce la fa passa alla casella dopo e ritira, chi sbaglia passa la mano; il Taglia gioca
|
||||
da solo (in simulazione gli servono circa 22, 10 e 6 turni secondo il livello). Vince chi finisce la 8 (flag
|
||||
`campana_vinta`). Test `campana.test.mjs`, prova `npm run browser:campana`. [DA ANDREA] le battute.
|
||||
- **La macchina del tempo da fuori (30 ottobre)**: dopo il cruscotto, la partenza giù per Via Liguri Magelli
|
||||
(primo piano `corsa`, `src/engine/corsa.js`): accelera, il fulmine la prende, il bianco, e restano due sgommate di
|
||||
fuoco che bruciano e si spengono; al ritorno dal 1997 l'arrivo: il fulmine in fondo alla strada, lei viene avanti e
|
||||
|
||||
+11
@@ -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
|
||||
muovono: si mira dove saranno. Si può anche premere Spara, senza trattenere il fiato. Frecce per spostare il mirino.</p>
|
||||
</details>
|
||||
<details id="campana-help" hidden><summary>Come si gioca</summary>
|
||||
<p>La campana col gesso sul vialetto del campone, a turni col Taglia. Si tira il sassolino nella casella che tocca,
|
||||
dalla 1 alla 8: deve cadere dentro, senza toccare le righe (alla bona le righe non contano). Poi si salta tutta la
|
||||
campana fino in cima (il mezzo cerchio) e indietro, su un piede nelle caselle sole e a gambe larghe nella 4-5 e nella 7-8, senza mai
|
||||
mettere il piede dove sta il sassolino; tornando lo si raccoglie dalla casella prima. Chi ce la fa passa alla casella
|
||||
dopo e ritira; chi sbaglia passa la mano. Vince chi finisce la 8.</p>
|
||||
<p>Il tiro: muovi il mouse o tocca la campana per mirare, tieni premuto per la forza (sale e scende) e lascia; oppure
|
||||
regola la forza e premi Tira. Alla bona il puntino giallo segna dove cade. Frecce su e giù per la mira, destra e
|
||||
sinistra per la forza. I salti: la lancetta dell'equilibrio attraversa la barra; premi (o Spazio, o Salta) quando è
|
||||
nel verde. Troppo presto metti giù l'altro piede, troppo tardi pesti la riga.</p>
|
||||
</details>
|
||||
<details id="cerbottana-help" hidden><summary>Come si gioca</summary>
|
||||
<p>La cerbottana, con lo stucco verde di Pierin di Frilli: dodici pallini contro il muro della piazza. Il bersaglio
|
||||
di gesso vale 10, 5 o 2 punti secondo il cerchio, ogni lattina sul muretto 3, il barattolo appeso che dondola (Seria
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "leggenda-del-regolo",
|
||||
"version": "0.1.56",
|
||||
"version": "0.1.57",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -11,6 +11,7 @@
|
||||
"browser:gara": "node tests/gara.browser.mjs",
|
||||
"browser:cerbottana": "node tests/cerbottana.browser.mjs",
|
||||
"browser:condor": "node tests/condor.browser.mjs",
|
||||
"browser:campana": "node tests/campana.browser.mjs",
|
||||
"browser:tressette": "node tests/tressette.browser.mjs",
|
||||
"browser:scopa": "node tests/scopa.browser.mjs",
|
||||
"browser:briscola": "node tests/briscola.browser.mjs",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import * as micro from './micro.js';
|
||||
import * as cerbottana from './cerbottana.js';
|
||||
import { newCorsa, updateCorsa } from './corsa.js';
|
||||
import * as condor from './condor.js';
|
||||
import * as campana from './campana.js';
|
||||
import * as poker from './poker.js';
|
||||
|
||||
// I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta.
|
||||
@@ -29,6 +30,7 @@ export const MINIGAMES = {
|
||||
poker: { create: (seed, level) => poker.newPoker(seed, { level }), update: poker.update, play: poker.play },
|
||||
cerbottana: { create: (seed, level) => cerbottana.newCerbottana(seed, { level }), update: cerbottana.update, press: cerbottana.press, release: cerbottana.release, cancel: cerbottana.cancel, aim: cerbottana.aimScreen },
|
||||
condor: { create: (seed, level) => condor.newCondor(seed, { level }), update: condor.update, press: condor.press, release: condor.release, cancel: condor.cancel, aim: condor.aimScreen },
|
||||
campana: { create: (seed, level) => campana.newCampana(seed, { level }), update: campana.update, press: campana.press, release: campana.release, cancel: campana.cancel, aim: campana.aimScreen },
|
||||
micro: { create: (seed, level) => micro.newMicro(seed, { level }), update: micro.update, press: micro.press, release: micro.release, cancel: micro.cancel, aim: micro.aimScreen },
|
||||
gara: { create: (seed, level, deck, def) => gara.newGara(seed, { level, riders: def.riders }), update: gara.update, press: gara.press, release: gara.release, cancel: gara.cancel, aim: gara.aimScreen },
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import { drawMicro } from './micro-render.js';
|
||||
import { drawCerbottana } from './cerbottana-render.js';
|
||||
import { drawCorsa } from './corsa.js';
|
||||
import { drawCondor } from './condor-render.js';
|
||||
import { drawCampana } from './campana-render.js';
|
||||
import { drawPoker } from './poker-render.js';
|
||||
import { drawViaggio } from './viaggio.js';
|
||||
import { drawBriscola } from './briscola-render.js';
|
||||
@@ -718,6 +719,11 @@ function drawCloseup(ctx, game, time) {
|
||||
drawCondor(ctx, c, def, backgrounds.get(def.picture));
|
||||
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 (!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); });
|
||||
|
||||
@@ -148,6 +148,11 @@ export class Soundscape {
|
||||
// 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); }
|
||||
// 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 }); }
|
||||
|
||||
+7
-4
@@ -14,6 +14,7 @@ import * as rinquarto from './engine/rinquarto.js';
|
||||
import * as micro from './engine/micro.js';
|
||||
import * as cerbottana from './engine/cerbottana.js';
|
||||
import * as condor from './engine/condor.js';
|
||||
import * as campana from './engine/campana.js';
|
||||
import * as poker from './engine/poker.js';
|
||||
import { cardAt as pokerAt } from './engine/poker-render.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);
|
||||
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)); }
|
||||
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.' },
|
||||
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.' },
|
||||
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.' },
|
||||
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).
|
||||
@@ -1040,7 +1042,7 @@ function updateUi() {
|
||||
}
|
||||
}
|
||||
if (mg && !cards) {
|
||||
setAttr($('bocce-controls'), 'aria-label', { rinquarto: 'Comandi dei rinquarti', biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca', gara: 'Comandi della gara', micro: 'Comandi delle macchinine', cerbottana: 'Comandi della cerbottana', 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);
|
||||
const line = mg.status(c, def.names);
|
||||
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.
|
||||
const easy = def.type === 'gara' && gara.simple(c);
|
||||
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 ';
|
||||
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');
|
||||
@@ -1076,6 +1078,7 @@ function updateUi() {
|
||||
setProp($('micro-help'), 'hidden', def.type !== 'micro');
|
||||
setProp($('cerbottana-help'), 'hidden', def.type !== 'cerbottana');
|
||||
setProp($('condor-help'), 'hidden', def.type !== 'condor');
|
||||
setProp($('campana-help'), 'hidden', def.type !== 'campana');
|
||||
setProp($('basket-help'), 'hidden', def.type !== 'basket');
|
||||
setProp($('pesca-help'), 'hidden', def.type !== 'pesca');
|
||||
setProp($('gara-help'), 'hidden', def.type !== 'gara');
|
||||
@@ -1096,7 +1099,7 @@ function updateUi() {
|
||||
canvas.classList.toggle('torch', !!torch);
|
||||
canvas.classList.toggle('bocce', !!mg);
|
||||
// Biliardo e GP di Fortezza: audio, pausa e uscita fuori dal quadro (al GP la pista passa negli angoli).
|
||||
$('stage').classList.toggle('biliardo', !!c && ['biliardo', 'rinquarto', 'gara', 'micro', 'cerbottana', '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);
|
||||
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.
|
||||
|
||||
@@ -301,6 +301,17 @@ export const story = {
|
||||
draw: [{ say: 'Pari. Si rifà?' }, { say: 'Si rifà.', who: 'taglia', pose: 'explain' }],
|
||||
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).
|
||||
// [DA ANDREA: le battute sono provvisorie]
|
||||
micro: {
|
||||
@@ -3975,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: '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: 'Ci si vede, Taglia.', next: 'end' },
|
||||
],
|
||||
@@ -3987,6 +4002,14 @@ export const story = {
|
||||
{ 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]
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
|
||||
export const VERSION = '0.1.56';
|
||||
export const VERSION = '0.1.57';
|
||||
|
||||
// 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).
|
||||
|
||||
@@ -62,16 +62,16 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
|
||||
/* La guida semplice del GP: il Gas si tiene premuto col pollice, più alto. */
|
||||
#bocce-throw.hold { min-height: 72px; font-size: 14px; }
|
||||
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
|
||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
||||
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary, #gara-help summary, #rinquarto-help summary, #micro-help summary, #cerbottana-help, #condor-help summary { cursor: pointer; padding: 6px 0; }
|
||||
#bocce-help p, #biliardo-help p, #basket-help p, #pesca-help p, #gara-help p, #rinquarto-help p, #micro-help p, #cerbottana-help, #condor-help p { max-width: 76em; line-height: 1.7; }
|
||||
#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, #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, #campana-help p { max-width: 76em; line-height: 1.7; }
|
||||
@media (max-width: 600px), (orientation: landscape) and (max-height: 520px) {
|
||||
#bocce-controls { padding: 8px; font-size: 10px; overflow-y: auto; min-height: 0; }
|
||||
.bocce-adjust { flex-wrap: wrap; gap: 6px; }
|
||||
.bocce-adjust label { order: -1; flex-basis: 100%; max-width: none; }
|
||||
.bocce-adjust button { flex: 1; font-size: 9px; padding: 6px; }
|
||||
#bocce-throw { flex-basis: 100%; }
|
||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #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-volume { width: 130px; accent-color: var(--accent); }
|
||||
|
||||
@@ -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(); }
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user