Il flipper rivisto con la revisione di Codex: punti solo a un urto nuovo, sottopassi contro gli attraversamenti, molla che arriva sempre in campo (senza cancelletto), salvataggio col tempo della simulazione e in vista, corsie che contano attraversando la riga, traguardo acquisito appena raggiunto, annullare non lancia, due dita e tasto destro, pulsanti dei flipper sempre attivi
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2987,13 +2987,13 @@
|
|||||||
"file": "andrea-b6d9563c93.mp3",
|
"file": "andrea-b6d9563c93.mp3",
|
||||||
"sec": 2.02
|
"sec": 2.02
|
||||||
},
|
},
|
||||||
"andrea|Dodicimila. Come quando ci lasciavo le cinquecento lire.": {
|
"andrea|Diecimila. Come quando ci lasciavo le cinquecento lire.": {
|
||||||
"file": "andrea-01a2eb256d.mp3",
|
"file": "andrea-5811ae33d6.mp3",
|
||||||
"sec": 4.12
|
"sec": 3.91
|
||||||
},
|
},
|
||||||
"andrea|Venticinquemila. Il record del River, o quasi.": {
|
"andrea|Sedicimila. Il record del River, o quasi.": {
|
||||||
"file": "andrea-e455fd8483.mp3",
|
"file": "andrea-d420a5e136.mp3",
|
||||||
"sec": 4.2
|
"sec": 4.38
|
||||||
},
|
},
|
||||||
"andrea|Due tiri alla bona, tanto per scaldarmi.": {
|
"andrea|Due tiri alla bona, tanto per scaldarmi.": {
|
||||||
"file": "andrea-d564365fc2.mp3",
|
"file": "andrea-d564365fc2.mp3",
|
||||||
|
|||||||
+7
-1
@@ -58,7 +58,13 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
|||||||
salvataggio della pallina una volta sola. Comandi: molla tenendo premuto (o cursore + Lancia, o Spazio), flipper
|
salvataggio della pallina una volta sola. Comandi: molla tenendo premuto (o cursore + Lancia, o Spazio), flipper
|
||||||
con le due metà della scena, i pulsanti «← Flipper»/«Flipper →» tenuti premuti o le frecce. Tre palline; traguardo
|
con le due metà della scena, i pulsanti «← Flipper»/«Flipper →» tenuti premuti o le frecce. Tre palline; traguardo
|
||||||
6000/12000/25000. Test `flipper.test.mjs` (col giocatore automatico), prova `npm run browser:flipper`.
|
6000/12000/25000. Test `flipper.test.mjs` (col giocatore automatico), prova `npm run browser:flipper`.
|
||||||
[DA ANDREA] le battute; cosa si vince (`flipper_vinto` per ora non serve a niente).
|
Rivisto con la revisione critica di Codex (31 ottobre, chiesta da Andrea): punti solo a un urto nuovo (la pallina
|
||||||
|
appoggiata sul Regolo faceva 240.000 punti al secondo), sottopassi contro gli attraversamenti del flipper, niente
|
||||||
|
cancelletto-ripiano e molla che arriva sempre in campo, salvataggio col tempo della simulazione (e in vista sul
|
||||||
|
quadro), corsie che contano attraversando la riga, traguardo acquisito appena raggiunto (poi si continua per il
|
||||||
|
record), annullare non lancia, due dita sulla scena e tasto destro del mouse, pulsanti dei flipper sempre attivi.
|
||||||
|
[DA ANDREA] le battute; cosa si vince (`flipper_vinto` per ora non serve a niente). Da fare (Andrea): un paio di
|
||||||
|
rampe («ponti»), più punti, il tabellone luminoso coi punti che corrono, un ingresso più intuitivo del River.
|
||||||
- **Dedica allo zio Mario (31 ottobre)**: nei Ringraziamenti, sotto quella al Lele e più piccola: «Al mio caro zio
|
- **Dedica allo zio Mario (31 ottobre)**: nei Ringraziamenti, sotto quella al Lele e più piccola: «Al mio caro zio
|
||||||
Mario, che mi ha fatto vivere tante avventure più belle della fantasia.»
|
Mario, che mi ha fatto vivere tante avventure più belle della fantasia.»
|
||||||
- **Il Taglia (31 ottobre)**: tolte le battute sulla sua altezza (Baldoria: non gli fa piacere).
|
- **Il Taglia (31 ottobre)**: tolte le battute sulla sua altezza (Baldoria: non gli fa piacere).
|
||||||
|
|||||||
+7
-6
@@ -308,12 +308,13 @@
|
|||||||
per caricare la molla e lascia per lanciare (o regola la forza e premi Lancia, o Spazio). Fa il giro dell'arco e
|
per caricare la molla e lascia per lanciare (o regola la forza e premi Lancia, o Spazio). Fa il giro dell'arco e
|
||||||
scende per una delle quattro corsie dei rioni: accendile tutte e quattro per il PALIO (3000 punti). I tre funghi
|
scende per una delle quattro corsie dei rioni: accendile tutte e quattro per il PALIO (3000 punti). I tre funghi
|
||||||
sono i bastioni della Fortezza (100 a botta); i bersagli bianchi a sinistra sono la Pieve e quelli gialli a
|
sono i bastioni della Fortezza (100 a botta); i bersagli bianchi a sinistra sono la Pieve e quelli gialli a
|
||||||
destra la torre dell'orologio (500 l'uno, 2000 la fila intera); il bersaglio verde con la corona è il Regolo
|
destra la torre dell'orologio (500 l'uno, 2000 la fila intera); il bersaglio verde con la corona, in alto a
|
||||||
(1000).</p>
|
destra, è il Regolo (1000).</p>
|
||||||
<p>I flipper: tieni premuto sulla metà sinistra o destra della scena, o i pulsanti «← Flipper» e «Flipper →», o le
|
<p>I flipper: tieni premuto sulla metà sinistra o destra della scena (anche con due dita insieme, o col tasto
|
||||||
frecce sinistra e destra. Tre palline; la prima volta, appena lanciata, se la perdi subito torna (alla bona per
|
destro del mouse per quello di destra), o i pulsanti «← Flipper» e «Flipper →», o le frecce sinistra e destra.
|
||||||
8 secondi, seria per 3, a fassi male mai). Si vince arrivando al traguardo: alla bona 6000 punti, seria 12000,
|
Tre palline; la prima volta, appena lanciata, se la perdi subito torna (alla bona per 8 secondi, seria per 4, a
|
||||||
a fassi male 25000.</p>
|
fassi male per 1). Si vince arrivando al traguardo, anche se poi si esce: alla bona 6000 punti, seria 10000, a
|
||||||
|
fassi male 16000; raggiunto il traguardo, si continua per il record.</p>
|
||||||
</details>
|
</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
|
||||||
|
|||||||
@@ -1,34 +1,44 @@
|
|||||||
// Il flipper del River in primo piano (flipper.js): la tavola dipinta da Codex con San Piero, e sopra le parti
|
// Il flipper del River in primo piano (flipper.js): la tavola dipinta da Codex con San Piero, e sopra le parti
|
||||||
// meccaniche disegnate dal gioco, nelle stesse coordinate della fisica: sponde cromate, corsie dei rioni con le luci,
|
// meccaniche disegnate dal gioco, nelle stesse coordinate della fisica: sponde cromate (larghe quanto collidono),
|
||||||
// funghi, bersagli a caduta, il Regolo, sponde elastiche, flipper, molla e pallina. A sinistra, sul fianco nero del
|
// corsie dei rioni con le luci sulla riga che conta, funghi, bersagli a caduta, il Regolo, sponde elastiche, flipper,
|
||||||
// mobile, il quadro dei punti. Niente filtri né passate a tutto schermo (vedi risveglio.js).
|
// molla e pallina. A sinistra, sul fianco nero del mobile, il quadro dei punti. Niente filtri né passate a tutto
|
||||||
import { FIELD, LANE, R, FLIP, PIVOT, SCORE, waiting, canThrow } from './flipper.js';
|
// schermo (vedi risveglio.js).
|
||||||
|
import { FIELD, LANE, R, PIVOT, flipperSeg, waiting, canThrow, saveLeft } from './flipper.js';
|
||||||
import { text, panel } from './bocce-render.js';
|
import { text, panel } from './bocce-render.js';
|
||||||
|
|
||||||
const COLORS = { piazza: '#d0453a', pieve: '#f2eee2', cardetole: '#3fa34d', campomigliaio: '#2f6fd0' };
|
const COLORS = { piazza: '#d0453a', pieve: '#f2eee2', cardetole: '#3fa34d', campomigliaio: '#2f6fd0' };
|
||||||
const rail = (ctx, [ax, ay], [bx, by], dark = '#2a2622', light = '#c9c3b6') => {
|
const rail = (ctx, [ax, ay], [bx, by], dark = '#2a2622', light = '#c9c3b6') => {
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
ctx.strokeStyle = dark; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();
|
ctx.strokeStyle = dark; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();
|
||||||
ctx.strokeStyle = light; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();
|
ctx.strokeStyle = light; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();
|
||||||
};
|
};
|
||||||
function disc(ctx, x, y, r, fill, rim) {
|
function disc(ctx, x, y, r, fill, rim) {
|
||||||
ctx.fillStyle = rim; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
|
ctx.fillStyle = rim; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
|
||||||
ctx.fillStyle = fill; ctx.beginPath(); ctx.arc(x, y, r - 2, 0, Math.PI * 2); ctx.fill();
|
ctx.fillStyle = fill; ctx.beginPath(); ctx.arc(x, y, r - 2, 0, Math.PI * 2); ctx.fill();
|
||||||
}
|
}
|
||||||
function bumper(ctx, b, time) {
|
function bumper(ctx, b) {
|
||||||
const k = b.hit > 0 ? 1 + b.hit * 1.2 : 1, r = b.r * k;
|
const r = b.r * (b.hit > 0 ? 1 + b.hit * 1.2 : 1);
|
||||||
ctx.fillStyle = 'rgba(0,0,0,0.25)'; ctx.beginPath(); ctx.arc(b.x + 2, b.y + 3, r, 0, Math.PI * 2); ctx.fill();
|
ctx.fillStyle = 'rgba(0,0,0,0.25)'; ctx.beginPath(); ctx.arc(b.x + 2, b.y + 3, r, 0, Math.PI * 2); ctx.fill();
|
||||||
disc(ctx, b.x, b.y, r, b.hit > 0 ? '#ffe9a8' : '#d9463a', '#5a1d16');
|
disc(ctx, b.x, b.y, r, b.hit > 0 ? '#ffe9a8' : '#d9463a', '#5a1d16');
|
||||||
disc(ctx, b.x, b.y, r * 0.55, b.hit > 0 ? '#fff6d8' : '#f0a24a', '#8a3a1e');
|
disc(ctx, b.x, b.y, r * 0.55, b.hit > 0 ? '#fff6d8' : '#f0a24a', '#8a3a1e');
|
||||||
ctx.fillStyle = 'rgba(255,255,255,0.55)'; ctx.fillRect(Math.round(b.x - r * 0.4), Math.round(b.y - r * 0.6), 3, 2);
|
ctx.fillStyle = 'rgba(255,255,255,0.55)'; ctx.fillRect(Math.round(b.x - r * 0.4), Math.round(b.y - r * 0.6), 3, 2);
|
||||||
void time;
|
}
|
||||||
|
// Il Regolo: un bersaglio tondo d'oro con la corona e il bordo scuro, che non si confonde col drago dipinto.
|
||||||
|
function regolo(ctx, o) {
|
||||||
|
const r = o.r + (o.hit > 0 ? 2 : 0);
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.3)'; ctx.beginPath(); ctx.arc(o.x + 1, o.y + 2, r + 1, 0, Math.PI * 2); ctx.fill();
|
||||||
|
disc(ctx, o.x, o.y, r + 1, o.hit > 0 ? '#fff6c8' : '#f0c040', '#3a2a08');
|
||||||
|
ctx.fillStyle = '#3fa34d'; ctx.beginPath(); ctx.arc(o.x, o.y, r - 3, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.fillStyle = '#ffd45a';
|
||||||
|
for (const [dx, h] of [[-4, 4], [-1, 6], [2, 4]]) ctx.fillRect(o.x + dx, o.y - r - 2 - h, 2, h);
|
||||||
|
ctx.fillRect(o.x - 5, o.y - r - 2, 10, 2);
|
||||||
}
|
}
|
||||||
function flipper(ctx, c, side) {
|
function flipper(ctx, c, side) {
|
||||||
const f = c.flippers[side], [px, py] = PIVOT[side], tx = px + Math.cos(f.angle) * FLIP.len, ty = py + Math.sin(f.angle) * FLIP.len;
|
const f = c.flippers[side], { a: [px, py], b: [tx, ty] } = flipperSeg(c, side);
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
ctx.strokeStyle = 'rgba(0,0,0,0.3)'; ctx.lineWidth = FLIP.r * 2 + 2; ctx.beginPath(); ctx.moveTo(px + 1, py + 3); ctx.lineTo(tx + 1, ty + 3); ctx.stroke();
|
ctx.strokeStyle = 'rgba(0,0,0,0.3)'; ctx.lineWidth = 7; ctx.beginPath(); ctx.moveTo(px + 1, py + 3); ctx.lineTo(tx + 1, ty + 3); ctx.stroke();
|
||||||
ctx.strokeStyle = '#3a2a1a'; ctx.lineWidth = FLIP.r * 2 + 2; ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(tx, ty); ctx.stroke();
|
ctx.strokeStyle = '#3a2a1a'; ctx.lineWidth = 7; ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(tx, ty); ctx.stroke();
|
||||||
ctx.strokeStyle = f.held.length ? '#ffd45a' : '#e8b640'; ctx.lineWidth = FLIP.r * 2 - 1; ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(tx, ty); ctx.stroke();
|
ctx.strokeStyle = f.held.length ? '#ffd45a' : '#e8b640'; ctx.lineWidth = 5; ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(tx, ty); ctx.stroke();
|
||||||
ctx.strokeStyle = '#fff1b8'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(px, py - 1.5); ctx.lineTo(tx, ty - 1.5); ctx.stroke();
|
ctx.strokeStyle = '#fff1b8'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(px, py - 1.5); ctx.lineTo(tx, ty - 1.5); ctx.stroke();
|
||||||
disc(ctx, px, py, 4, '#8a7a6a', '#2a2622');
|
disc(ctx, px, py, 4, '#8a7a6a', '#2a2622');
|
||||||
}
|
}
|
||||||
@@ -46,23 +56,24 @@ function ball(ctx, b) {
|
|||||||
g.addColorStop(0, '#ffffff'); g.addColorStop(0.4, '#cfd3da'); g.addColorStop(1, '#5a6068');
|
g.addColorStop(0, '#ffffff'); g.addColorStop(0.4, '#cfd3da'); g.addColorStop(1, '#5a6068');
|
||||||
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(b.x, b.y, R, 0, Math.PI * 2); ctx.fill();
|
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(b.x, b.y, R, 0, Math.PI * 2); ctx.fill();
|
||||||
}
|
}
|
||||||
// Il quadro dei punti sul fianco del mobile: punti, palline, i rioni accesi, il traguardo, l'ultimo avviso.
|
// Il quadro dei punti sul fianco del mobile: punti, palline, i rioni accesi, il traguardo, il salvataggio, l'avviso.
|
||||||
function board(ctx, c, def) {
|
function board(ctx, c) {
|
||||||
panel(ctx, 22, 60, 160, 150);
|
panel(ctx, 16, 56, 172, 168);
|
||||||
text(ctx, 'FLIPPER DEL RIVER', 102, 76, 7, '#efd49e', 'center');
|
text(ctx, 'FLIPPER DEL RIVER', 102, 72, 7, '#efd49e', 'center');
|
||||||
text(ctx, String(c.step === 'result' ? c.result.score : c.score).padStart(6, '0'), 102, 100, 14, '#fff4c8', 'center');
|
text(ctx, String(c.score).padStart(6, '0'), 102, 98, 16, '#fff4c8', 'center');
|
||||||
text(ctx, 'PUNTI', 102, 116, 5, '#a9a08a', 'center');
|
text(ctx, `PALLINE ${c.step === 'result' ? 0 : c.balls}`, 28, 124, 7, '#d7ccb4');
|
||||||
text(ctx, `PALLINE ${c.step === 'result' ? 0 : c.balls}`, 34, 136, 6, '#d7ccb4');
|
text(ctx, c.result ? 'TRAGUARDO FATTO' : `TRAGUARDO ${c.skill.goal}`, 28, 140, 7, c.result ? '#9fd27a' : '#d7ccb4');
|
||||||
text(ctx, `TRAGUARDO ${c.skill.goal}`, 34, 150, 6, '#d7ccb4');
|
text(ctx, 'RIONI', 28, 160, 7, '#d7ccb4');
|
||||||
text(ctx, 'RIONI', 34, 168, 6, '#d7ccb4');
|
c.lanes.forEach((l, i) => disc(ctx, 86 + i * 22, 160, 7, l.lit ? COLORS[l.rione] : '#3a3630', l.lit ? '#fff4c8' : '#5a5650'));
|
||||||
c.lanes.forEach((l, i) => disc(ctx, 80 + i * 20, 168, 6, l.lit ? COLORS[l.rione] : '#3a3630', l.lit ? '#fff4c8' : '#5a5650'));
|
const save = saveLeft(c);
|
||||||
text(ctx, (def.names?.player ?? 'Andrea').toUpperCase(), 34, 190, 6, '#efd49e');
|
if (save > 0) text(ctx, `SALVATA ${Math.ceil(save)} s`, 28, 180, 7, '#9fd27a');
|
||||||
if (c.line) text(ctx, c.line.toUpperCase(), 102, 203, 5, '#ffe08c', 'center');
|
else if (c.ball && !c.ball.waiting) text(ctx, c.saved ? 'SALVATAGGIO USATO' : 'SENZA RETE', 28, 180, 6, '#a9a08a');
|
||||||
|
if (c.line) text(ctx, c.line, 102, 206, 8, '#ffe08c', 'center');
|
||||||
}
|
}
|
||||||
function result(ctx, c) {
|
function result(ctx, c) {
|
||||||
panel(ctx, 20, 230, 164, 56);
|
panel(ctx, 16, 236, 172, 56);
|
||||||
text(ctx, c.result.winner === 'player' ? 'TRAGUARDO FATTO!' : 'FINE PARTITA', 102, 246, 7, '#efd49e', 'center');
|
text(ctx, c.result.winner === 'player' ? 'TRAGUARDO FATTO!' : 'FINE PARTITA', 102, 252, 8, '#efd49e', 'center');
|
||||||
text(ctx, `${c.result.score} su ${c.result.goal}`, 102, 266, 7, c.result.winner === 'player' ? '#9fd27a' : '#e0b07a', 'center');
|
text(ctx, `${c.result.score} su ${c.result.goal}`, 102, 272, 8, c.result.winner === 'player' ? '#9fd27a' : '#e0b07a', 'center');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function drawFlipper(ctx, c, def, bg) {
|
export function drawFlipper(ctx, c, def, bg) {
|
||||||
@@ -70,32 +81,29 @@ export function drawFlipper(ctx, c, def, bg) {
|
|||||||
ctx.save();
|
ctx.save();
|
||||||
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
|
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
|
||||||
else { ctx.fillStyle = '#17110c'; ctx.fillRect(0, 0, 640, 360); ctx.fillStyle = '#e9d9b4'; ctx.fillRect(FIELD.x0, FIELD.y0, FIELD.x1 - FIELD.x0, FIELD.y1 - FIELD.y0); }
|
else { ctx.fillStyle = '#17110c'; ctx.fillRect(0, 0, 640, 360); ctx.fillStyle = '#e9d9b4'; ctx.fillRect(FIELD.x0, FIELD.y0, FIELD.x1 - FIELD.x0, FIELD.y1 - FIELD.y0); }
|
||||||
// Le luci delle corsie dei rioni, sotto le bandiere.
|
// Le luci delle corsie dei rioni, sulla riga che conta, sotto le bandiere.
|
||||||
for (const l of c.lanes) disc(ctx, l.x, l.y + 18, 4, l.lit ? COLORS[l.rione] : 'rgba(40,36,30,0.6)', l.lit ? '#fff4c8' : 'rgba(0,0,0,0.4)');
|
for (const l of c.lanes) {
|
||||||
// Le sponde: tutte le pareti della fisica, cromate; le elastiche con la gomma rossa.
|
ctx.fillStyle = 'rgba(0,0,0,0.35)'; ctx.fillRect(l.x - 7, l.y - 3, 14, 7);
|
||||||
for (const w of c.walls) {
|
disc(ctx, l.x, l.y, 6, l.lit ? COLORS[l.rione] : '#2a2622', l.lit ? '#fff4c8' : '#6a6050');
|
||||||
if (w.gate) { ctx.strokeStyle = '#8a8078'; ctx.lineWidth = 1; ctx.setLineDash([2, 2]); ctx.beginPath(); ctx.moveTo(...w.a); ctx.lineTo(...w.b); ctx.stroke(); ctx.setLineDash([]); continue; }
|
|
||||||
if (w.sling) { rail(ctx, w.a, w.b, '#5a1d16', '#e8584a'); continue; }
|
|
||||||
rail(ctx, w.a, w.b);
|
|
||||||
}
|
}
|
||||||
// I bersagli a caduta: in piedi colorati, caduti un segno a terra.
|
// Le sponde: tutte le pareti della fisica, cromate; le elastiche con la gomma rossa.
|
||||||
|
for (const w of c.walls) rail(ctx, w.a, w.b, w.sling ? '#5a1d16' : '#2a2622', w.sling ? '#e8584a' : '#c9c3b6');
|
||||||
|
// I bersagli a caduta: in piedi colorati col bordo scuro, caduti un segno a terra.
|
||||||
for (const t of c.targets) {
|
for (const t of c.targets) {
|
||||||
const [x, y0] = t.a, y1 = t.b[1];
|
const [x, y0] = t.a, y1 = t.b[1];
|
||||||
if (t.down) { ctx.fillStyle = 'rgba(0,0,0,0.35)'; ctx.fillRect(x - 2, y0, 4, y1 - y0); continue; }
|
if (t.down) { ctx.fillStyle = 'rgba(0,0,0,0.35)'; ctx.fillRect(x - 2, y0, 4, y1 - y0); continue; }
|
||||||
ctx.fillStyle = '#2a2622'; ctx.fillRect(x - 3, y0 - 1, 6, y1 - y0 + 2);
|
ctx.fillStyle = '#2a2622'; ctx.fillRect(x - 3, y0 - 1, 6, y1 - y0 + 2);
|
||||||
ctx.fillStyle = t.bank === 'pieve' ? '#f2eee2' : '#e8b640'; ctx.fillRect(x - 2, y0, 4, y1 - y0);
|
ctx.fillStyle = t.bank === 'pieve' ? '#f2eee2' : '#e8b640'; ctx.fillRect(x - 2, y0, 4, y1 - y0);
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.25)'; ctx.fillRect(x - 2, y1 - 3, 4, 3);
|
||||||
}
|
}
|
||||||
for (const b of c.bumpers) bumper(ctx, b, time);
|
for (const b of c.bumpers) bumper(ctx, b);
|
||||||
// Il Regolo: il bersaglio verde con la corona, che brilla quando è colpito.
|
regolo(ctx, c.regolo);
|
||||||
disc(ctx, c.regolo.x, c.regolo.y, c.regolo.r + (c.regolo.hit > 0 ? 2 : 0), c.regolo.hit > 0 ? '#c9ff9a' : '#3fa34d', '#1e4a24');
|
|
||||||
ctx.fillStyle = '#ffd45a'; ctx.fillRect(c.regolo.x - 3, c.regolo.y - c.regolo.r - 4, 6, 3);
|
|
||||||
for (const side of ['left', 'right']) flipper(ctx, c, side);
|
for (const side of ['left', 'right']) flipper(ctx, c, side);
|
||||||
plunger(ctx, c);
|
plunger(ctx, c);
|
||||||
ball(ctx, c.ball);
|
ball(ctx, c.ball);
|
||||||
// Da fermo, l'invito: la molla da caricare.
|
// Da fermo, l'invito: la molla da caricare.
|
||||||
if (waiting(c) && !c.holding && Math.sin(time * 5) > 0) text(ctx, 'LANCIA', (LANE.x0 + LANE.x1) / 2, 300, 5, '#ffe08c', 'center');
|
if (waiting(c) && !c.holding && Math.sin(time * 5) > 0) text(ctx, 'LANCIA', (LANE.x0 + LANE.x1) / 2, 300, 5, '#ffe08c', 'center');
|
||||||
board(ctx, c, def);
|
board(ctx, c);
|
||||||
if (c.step === 'result') result(ctx, c);
|
if (c.step === 'result') result(ctx, c);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
export { SCORE };
|
|
||||||
|
|||||||
+159
-85
@@ -1,23 +1,30 @@
|
|||||||
// Il flipper del River (Andrea, 31 ottobre: «una tavola bellissima, con San Piero dentro»). Visto dall'alto, la tavola
|
// Il flipper del River (Andrea, 31 ottobre: «una tavola bellissima, con San Piero dentro»). Visto dall'alto, la tavola
|
||||||
// in verticale al centro del primo piano 640×360; la pallina parte dalla corsia di destra con la molla, fa il giro
|
// in verticale al centro del primo piano 640×360; la pallina parte dalla corsia di destra con la molla, fa il giro
|
||||||
// dell'arco in cima, passa in una delle quattro corsie dei rioni, rimbalza sui tre funghi (i bastioni della Fortezza),
|
// dell'arco in cima, passa in una delle quattro corsie dei rioni, rimbalza sui tre funghi (i bastioni della Fortezza),
|
||||||
// abbatte i bersagli della Pieve a sinistra e della torre a destra, tocca il Regolo in mezzo, e giù dalle sponde
|
// abbatte i bersagli della Pieve a sinistra e della torre a destra, tocca il Regolo, e giù dalle sponde elastiche ai
|
||||||
// elastiche ai due flipper, con la Sieve dipinta che scorre nel buco in mezzo. Tre palline; si vince arrivando al
|
// due flipper, con la Sieve dipinta che scorre nel buco in mezzo. Tre palline; si vince arrivando al traguardo di
|
||||||
// traguardo di punti del livello. Unità: i pixel del primo piano. La fisica va a passo fisso (come il biliardo).
|
// punti del livello (e poi si può continuare per il record). Unità: i pixel del primo piano. La fisica va a passo
|
||||||
|
// fisso (come il biliardo), con sottopassi quando pallina e flipper vanno forte.
|
||||||
|
// Rivista con la revisione di Codex del 31 ottobre: punti solo a un urto nuovo (non al contatto che dura), due dita
|
||||||
|
// sulla scena, annullare non lancia, niente cancelletto-ripiano, salvataggio col tempo della simulazione, corsie che
|
||||||
|
// contano quando si attraversano, traguardo acquisito appena raggiunto.
|
||||||
export const FIELD = { x0: 200, x1: 448, y0: 5, y1: 336 }, LANE = { x0: 432, x1: 448 }, R = 4.5;
|
export const FIELD = { x0: 200, x1: 448, y0: 5, y1: 336 }, LANE = { x0: 432, x1: 448 }, R = 4.5;
|
||||||
export const BALLS = 3, STEP = 1 / 240, MAX_SPEED = 980, LAUNCH = [420, 900], CHARGE = 1.2, FLIP = { len: 34, rest: 0.5, up: -0.46, speed: 26, r: 3 };
|
export const BALLS = 3, STEP = 1 / 240, MAX_SPEED = 980, CHARGE = 1.2, FLIP = { len: 34, rest: 0.5, up: -0.46, speed: 26, r: 3 };
|
||||||
export const PIVOT = { left: [280, 314], right: [360, 314] };
|
export const PIVOT = { left: [280, 314], right: [360, 314] };
|
||||||
export const SCORE = { bumper: 100, sling: 50, lane: 200, palio: 3000, target: 500, bank: 2000, regolo: 1000 };
|
export const SCORE = { bumper: 100, sling: 50, lane: 200, palio: 3000, target: 500, bank: 2000, regolo: 1000 };
|
||||||
// I livelli: la gravità, quanti secondi dura il salvataggio della pallina appena lanciata, la spinta dei funghi, il traguardo.
|
// Quanto deve picchiare un urto perché conti (velocità lungo la normale, px/s): il tremolio da gravità non conta.
|
||||||
|
const IMPACT = 40, LANE_LINE = 104, BANK_RESET = 0.8, CLICK_HOLD = 0.16, NUDGE_AFTER = 2.5;
|
||||||
|
// I livelli: la stessa fisica, quasi; cambiano il traguardo, il salvataggio della pallina appena lanciata e un po' la
|
||||||
|
// gravità. La spinta dei funghi è uguale per tutti.
|
||||||
export const LEVELS = {
|
export const LEVELS = {
|
||||||
bona: { g: 620, save: 8, kick: 300, goal: 6000 },
|
bona: { g: 680, save: 8, kick: 340, goal: 6000 },
|
||||||
seria: { g: 780, save: 3, kick: 340, goal: 12000 },
|
seria: { g: 740, save: 4, kick: 340, goal: 10000 },
|
||||||
male: { g: 880, save: 0, kick: 380, goal: 25000 },
|
male: { g: 800, save: 1, kick: 340, goal: 16000 },
|
||||||
};
|
};
|
||||||
export const RIONI = ['piazza', 'pieve', 'cardetole', 'campomigliaio'];
|
export const RIONI = ['piazza', 'pieve', 'cardetole', 'campomigliaio'];
|
||||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
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; }
|
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
|
||||||
const seg = (a, b, extra = {}) => ({ a, b, e: 0.55, ...extra });
|
const seg = (a, b, extra = {}) => ({ a, b, e: 0.55, touching: false, ...extra });
|
||||||
|
|
||||||
// La tavola: sponde, corsie, sponde elastiche, funghi, bersagli. Le coordinate seguono il disegno di Codex
|
// La tavola: sponde, corsie, sponde elastiche, funghi, bersagli. Le coordinate seguono il disegno di Codex
|
||||||
// (assets/closeups/river-flipper.png): i tre dischi dei funghi, le quattro bandiere dei rioni, la corsia della molla.
|
// (assets/closeups/river-flipper.png): i tre dischi dei funghi, le quattro bandiere dei rioni, la corsia della molla.
|
||||||
@@ -26,12 +33,13 @@ export function layout() {
|
|||||||
const walls = [];
|
const walls = [];
|
||||||
for (let i = 1; i < arc.length; i++) walls.push(seg(arc[i - 1], arc[i], { e: 0.6 }));
|
for (let i = 1; i < arc.length; i++) walls.push(seg(arc[i - 1], arc[i], { e: 0.6 }));
|
||||||
walls.push(seg([200, 72], [200, 336]), seg([448, 72], [448, 336]));
|
walls.push(seg([200, 72], [200, 336]), seg([448, 72], [448, 336]));
|
||||||
// La corsia della molla, e in cima il cancelletto che non fa tornare indietro la pallina.
|
// La corsia della molla, col fondo. Niente cancelletto: una pallina che ricasca nella corsia si rilancia e basta
|
||||||
walls.push(seg([432, 70], [432, 336]), seg([432, 70], [448, 70], { gate: true }), seg([432, 326], [448, 326], { e: 0.2 }));
|
// (il cancelletto a senso unico faceva da ripiano ai lanci a metà).
|
||||||
|
walls.push(seg([432, 70], [432, 336]), seg([432, 326], [448, 326], { e: 0.2 }));
|
||||||
// Le corsie dei rioni, in cima, fra le bandiere: tre guide.
|
// Le corsie dei rioni, in cima, fra le bandiere: tre guide.
|
||||||
for (const x of [277, 319, 361]) walls.push(seg([x, 70], [x, 102], { e: 0.4 }));
|
for (const x of [277, 319, 361]) walls.push(seg([x, 70], [x, 102], { e: 0.4 }));
|
||||||
// I divisori delle uscite laterali e le guide che portano ai flipper.
|
// I divisori delle uscite laterali e le guide che portano ai flipper (la guida passa sopra il perno e continua un
|
||||||
// (La guida passa sopra il perno e continua un po' lungo il flipper: finendo sul perno la pallina ci restava incastrata.)
|
// po' lungo il flipper: finendo sul perno la pallina ci restava incastrata).
|
||||||
walls.push(seg([222, 240], [222, 290]), seg([222, 290], [292, 314], { e: 0.3 }));
|
walls.push(seg([222, 240], [222, 290]), seg([222, 290], [292, 314], { e: 0.3 }));
|
||||||
walls.push(seg([418, 240], [418, 290]), seg([418, 290], [348, 314], { e: 0.3 }));
|
walls.push(seg([418, 240], [418, 290]), seg([418, 290], [348, 314], { e: 0.3 }));
|
||||||
// Le sponde elastiche sopra i flipper: il lato lungo spinge.
|
// Le sponde elastiche sopra i flipper: il lato lungo spinge.
|
||||||
@@ -40,50 +48,73 @@ export function layout() {
|
|||||||
// I bersagli a caduta: la Pieve a sinistra, la torre a destra, con la sponda dietro.
|
// I bersagli a caduta: la Pieve a sinistra, la torre a destra, con la sponda dietro.
|
||||||
walls.push(seg([210, 146], [210, 198]), seg([429, 146], [429, 198]));
|
walls.push(seg([210, 146], [210, 198]), seg([429, 146], [429, 198]));
|
||||||
const targets = [];
|
const targets = [];
|
||||||
for (const [bank, x] of [['pieve', 216], ['torre', 423]]) for (let i = 0; i < 3; i++) targets.push({ bank, i, a: [x, 150 + i * 16], b: [x, 162 + i * 16], down: false });
|
for (const [bank, x] of [['pieve', 216], ['torre', 423]]) for (let i = 0; i < 3; i++) targets.push({ bank, i, a: [x, 150 + i * 16], b: [x, 162 + i * 16], down: false, touching: false });
|
||||||
const bumpers = [[281, 120], [360, 120], [320, 153]].map(([x, y]) => ({ x, y, r: 12, hit: 0 }));
|
const bumpers = [[281, 120], [360, 120], [320, 153]].map(([x, y]) => ({ x, y, r: 12, hit: 0, touching: false }));
|
||||||
// Il Regolo, in alto a destra sotto l'arco, dove la pallina scende dal giro.
|
// Il Regolo, in alto a destra sotto l'arco, dove la pallina scende dal giro.
|
||||||
const regolo = { x: 404, y: 62, r: 6, hit: 0 };
|
const regolo = { x: 404, y: 62, r: 7, hit: 0, touching: false };
|
||||||
const lanes = [257, 297, 340, 381].map((x, i) => ({ x, y: 88, rione: RIONI[i], lit: false }));
|
// Le corsie: ognuna fra due guide (o la sponda), con la bandiera dipinta in `x`; si accende attraversando in
|
||||||
|
// discesa la riga LANE_LINE.
|
||||||
|
const lanes = [[200, 277, 257], [277, 319, 297], [319, 361, 340], [361, 432, 381]].map(([x0, x1, x], i) => ({ x0, x1, x, y: LANE_LINE, rione: RIONI[i], lit: false }));
|
||||||
return { walls, targets, bumpers, regolo, lanes };
|
return { walls, targets, bumpers, regolo, lanes };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function newFlipper(seed = Date.now() >>> 0, def = {}) {
|
export function newFlipper(seed = Date.now() >>> 0, def = {}) {
|
||||||
const level = LEVELS[def.level] ? def.level : 'seria';
|
const level = LEVELS[def.level] ? def.level : 'seria';
|
||||||
const c = { step: 'play', t: 0, seed, level, skill: LEVELS[level], current: 'player', balls: BALLS, score: 0, power: 0.7,
|
const c = { step: 'play', t: 0, sim: 0, seed, level, skill: LEVELS[level], current: 'player', balls: BALLS, score: 0, power: 0.7,
|
||||||
holding: false, holdTime: 0, pointerId: null, cursor: null, accumulator: 0, events: [], line: null, lineT: 0, saved: false,
|
holding: false, holdTime: 0, pointerId: null, cursor: null, accumulator: 0, events: [], line: null, lineT: 0, saved: false,
|
||||||
flippers: { left: { angle: FLIP.rest, omega: 0, held: [] }, right: { angle: Math.PI - FLIP.rest, omega: 0, held: [] } },
|
flippers: { left: { angle: FLIP.rest, omega: 0, held: [], click: 0 }, right: { angle: Math.PI - FLIP.rest, omega: 0, held: [], click: 0 } },
|
||||||
ball: null, saveUntil: 0, still: 0, visual: { time: 0 }, ...layout() };
|
ball: null, saveUntil: 0, still: 0, anchor: null, rest: 0, resets: {}, result: null, visual: { time: 0 }, ...layout() };
|
||||||
newBall(c);
|
newBall(c);
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
function newBall(c) { c.ball = { x: (LANE.x0 + LANE.x1) / 2, y: 318, vx: 0, vy: 0, waiting: true }; c.still = 0; c.anchor = null; }
|
function newBall(c) { c.ball = { x: (LANE.x0 + LANE.x1) / 2, y: 318, vx: 0, vy: 0, waiting: true }; c.still = 0; c.anchor = null; c.rest = 0; }
|
||||||
export const waiting = c => c.step === 'play' && !!c.ball?.waiting;
|
export const waiting = c => c.step === 'play' && !!c.ball?.waiting;
|
||||||
export const canThrow = c => waiting(c) && !c.holding;
|
export const canThrow = c => waiting(c) && !c.holding;
|
||||||
export const canPress = canThrow;
|
export const canPress = canThrow;
|
||||||
export const canAdjust = c => c.step !== 'result';
|
// Due permessi diversi: il cursore della molla solo prima del lancio, i flipper per tutta la partita.
|
||||||
|
export const canAdjust = canThrow;
|
||||||
|
export const canFlip = c => c.step === 'play';
|
||||||
export const aimFree = () => true;
|
export const aimFree = () => true;
|
||||||
export const powerAt = time => clamp(time / CHARGE, 0, 1);
|
export const powerAt = time => clamp(time / CHARGE, 0, 1);
|
||||||
|
// Quanto resta del salvataggio della pallina, in secondi (0: non protetta).
|
||||||
|
export const saveLeft = c => c.ball && !c.ball.waiting ? Math.max(0, c.saveUntil - c.sim) : 0;
|
||||||
function say(c, line) { c.line = line; c.lineT = 2; }
|
function say(c, line) { c.line = line; c.lineT = 2; }
|
||||||
function add(c, points) { c.score += points; }
|
// I punti; al traguardo la vittoria è acquisita (anche uscendo prima), poi si continua per il record.
|
||||||
|
function add(c, points) {
|
||||||
|
c.score += points;
|
||||||
|
if (!c.result && c.score >= c.skill.goal) { c.result = { winner: 'player', score: c.score, goal: c.skill.goal }; say(c, 'TRAGUARDO!'); c.events.push({ kind: 'flipper-palio', power: 1 }); }
|
||||||
|
if (c.result) c.result.score = c.score;
|
||||||
|
}
|
||||||
|
|
||||||
// Il lancio: la molla spinge la pallina su per la corsia.
|
// Il lancio: la molla spinge la pallina su per la corsia. Anche la molla più fiacca arriva all'arco (la velocità
|
||||||
|
// minima dipende dalla gravità del livello); da lì in su, la forza decide quanto gira.
|
||||||
|
export function launchSpeed(c, power) {
|
||||||
|
const min = Math.sqrt(2 * c.skill.g * 340), max = Math.max(min + 150, 900);
|
||||||
|
return min + (max - min) * clamp(power, 0, 1);
|
||||||
|
}
|
||||||
export function launch(c, power) {
|
export function launch(c, power) {
|
||||||
if (!waiting(c)) return false;
|
if (!waiting(c)) return false;
|
||||||
c.ball.waiting = false; c.ball.vy = -(LAUNCH[0] + (LAUNCH[1] - LAUNCH[0]) * clamp(power, 0, 1)); c.ball.vx = 0;
|
// La molla vera non è mai identica due volte: un pelo di caso, così la partita non si ripete uguale.
|
||||||
|
c.ball.waiting = false; c.ball.vy = -launchSpeed(c, power) * (0.985 + random(c) * 0.03); c.ball.vx = 0;
|
||||||
// Il salvataggio vale una volta per pallina: persa subito, torna; persa di nuovo, è persa.
|
// Il salvataggio vale una volta per pallina: persa subito, torna; persa di nuovo, è persa.
|
||||||
c.saveUntil = c.saved ? 0 : c.t + c.skill.save; c.holding = false; c.holdTime = 0;
|
c.saveUntil = c.saved ? 0 : c.sim + c.skill.save; c.holding = false; c.holdTime = 0; c.pointerId = null;
|
||||||
c.events.push({ kind: 'flipper-lancio', power: 0.5 + 0.5 * power });
|
c.events.push({ kind: 'flipper-lancio', power: 0.5 + 0.5 * clamp(power, 0, 1) });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Il flipper: il tasto tenuto lo alza, lasciato ricade. Più dita o tasti insieme: resta su finché ne resta uno.
|
// Il flipper: il tasto tenuto lo alza, lasciato ricade. Più dita o tasti insieme: resta su finché ne resta uno.
|
||||||
|
// Un «clic» (il pulsante attivato da tastiera) lo tiene su per un attimo, abbastanza perché la fisica lo veda.
|
||||||
export function hold(c, side, who, down) {
|
export function hold(c, side, who, down) {
|
||||||
const f = c.flippers[side]; if (!f) return false;
|
const f = c.flippers[side]; if (!f) return false;
|
||||||
const was = f.held.length > 0;
|
const was = f.held.length > 0;
|
||||||
f.held = f.held.filter(h => h !== who); if (down) f.held.push(who);
|
f.held = f.held.filter(h => h !== who); if (down) f.held.push(who);
|
||||||
|
if (who === 'click' && down) f.click = CLICK_HOLD;
|
||||||
if (!was && f.held.length && c.step === 'play') c.events.push({ kind: 'flipper-flip', power: 0.6 });
|
if (!was && f.held.length && c.step === 'play') c.events.push({ kind: 'flipper-flip', power: 0.6 });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
export function flipperSeg(c, side) {
|
||||||
|
const f = c.flippers[side], [px, py] = PIVOT[side];
|
||||||
|
return { a: [px, py], b: [px + Math.cos(f.angle) * FLIP.len, py + Math.sin(f.angle) * FLIP.len] };
|
||||||
|
}
|
||||||
|
|
||||||
// La fisica, a passi fissi.
|
// La fisica, a passi fissi.
|
||||||
export function update(c, dt) {
|
export function update(c, dt) {
|
||||||
@@ -95,89 +126,120 @@ export function update(c, dt) {
|
|||||||
c.accumulator += dt;
|
c.accumulator += dt;
|
||||||
while (c.accumulator >= STEP) { c.accumulator -= STEP; step(c); }
|
while (c.accumulator >= STEP) { c.accumulator -= STEP; step(c); }
|
||||||
}
|
}
|
||||||
function flipperSeg(c, side) {
|
function moveFlippers(c, h) {
|
||||||
const f = c.flippers[side], [px, py] = PIVOT[side];
|
|
||||||
return { a: [px, py], b: [px + Math.cos(f.angle) * FLIP.len, py + Math.sin(f.angle) * FLIP.len] };
|
|
||||||
}
|
|
||||||
function step(c) {
|
|
||||||
// I flipper: su se tenuti, giù se no; velocità angolare costante.
|
|
||||||
for (const side of ['left', 'right']) {
|
for (const side of ['left', 'right']) {
|
||||||
const f = c.flippers[side];
|
const f = c.flippers[side];
|
||||||
|
if (f.click > 0 && (f.click -= h) <= 0) f.held = f.held.filter(k => k !== 'click');
|
||||||
const target = f.held.length ? (side === 'left' ? FLIP.up : Math.PI - FLIP.up) : (side === 'left' ? FLIP.rest : Math.PI - FLIP.rest);
|
const target = f.held.length ? (side === 'left' ? FLIP.up : Math.PI - FLIP.up) : (side === 'left' ? FLIP.rest : Math.PI - FLIP.rest);
|
||||||
const d = target - f.angle, move = FLIP.speed * STEP;
|
const previous = f.angle;
|
||||||
f.omega = Math.abs(d) <= move ? 0 : Math.sign(d) * FLIP.speed;
|
f.angle += clamp(target - f.angle, -FLIP.speed * h, FLIP.speed * h);
|
||||||
f.angle = Math.abs(d) <= move ? target : f.angle + Math.sign(d) * move;
|
f.omega = (f.angle - previous) / h;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
function step(c) {
|
||||||
|
c.sim += STEP;
|
||||||
const ball = c.ball;
|
const ball = c.ball;
|
||||||
if (c.step !== 'play' || !ball || ball.waiting) return;
|
if (c.step !== 'play' || !ball || ball.waiting) { moveFlippers(c, STEP); return; }
|
||||||
|
for (const bank of Object.keys(c.resets)) if (c.sim >= c.resets[bank]) { for (const t of c.targets) if (t.bank === bank) t.down = false; delete c.resets[bank]; }
|
||||||
ball.vy += c.skill.g * STEP;
|
ball.vy += c.skill.g * STEP;
|
||||||
const v = Math.hypot(ball.vx, ball.vy);
|
limit(ball);
|
||||||
if (v > MAX_SPEED) { ball.vx *= MAX_SPEED / v; ball.vy *= MAX_SPEED / v; }
|
// Sottopassi: pallina e punta del flipper insieme non devono fare più di un raggio per passo.
|
||||||
ball.x += ball.vx * STEP; ball.y += ball.vy * STEP;
|
const tip = FLIP.speed * FLIP.len, n = clamp(Math.ceil((Math.hypot(ball.vx, ball.vy) + tip) * STEP / R), 1, 6), h = STEP / n;
|
||||||
|
const from = { x: ball.x, y: ball.y };
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
moveFlippers(c, h);
|
||||||
|
ball.x += ball.vx * h; ball.y += ball.vy * h;
|
||||||
for (const w of c.walls) collideSeg(c, ball, w);
|
for (const w of c.walls) collideSeg(c, ball, w);
|
||||||
for (const t of c.targets) if (!t.down && collideSeg(c, ball, { a: t.a, b: t.b, e: 0.5 })) dropTarget(c, t);
|
for (const t of c.targets) if (!t.down) collideTarget(c, ball, t);
|
||||||
for (const b of c.bumpers) if (collideCircle(ball, b, 0)) { kick(c, ball, b, c.skill.kick); b.hit = 0.15; add(c, SCORE.bumper); c.events.push({ kind: 'flipper-fungo', power: 0.8 }); }
|
for (const b of c.bumpers) collideBumper(c, ball, b);
|
||||||
if (collideCircle(ball, c.regolo, 0.7)) { c.regolo.hit = 0.3; add(c, SCORE.regolo); say(c, 'Il Regolo!'); c.events.push({ kind: 'flipper-bersaglio', power: 1 }); }
|
collideRegolo(c, ball);
|
||||||
for (const side of ['left', 'right']) collideFlipper(c, ball, side);
|
for (const side of ['left', 'right']) collideFlipper(c, ball, side);
|
||||||
// Le corsie si accendono entrando (non restandoci sopra: dopo il Palio ripartono spente).
|
limit(ball);
|
||||||
for (const l of c.lanes) {
|
|
||||||
const inside = Math.hypot(ball.x - l.x, ball.y - l.y) < 7;
|
|
||||||
if (inside && !l.inside && !l.lit) {
|
|
||||||
l.lit = true; add(c, SCORE.lane); c.events.push({ kind: 'flipper-corsia', power: 0.6 });
|
|
||||||
if (c.lanes.every(k => k.lit)) { add(c, SCORE.palio); say(c, 'PALIO! Tutti e quattro i rioni!'); c.events.push({ kind: 'flipper-palio', power: 1 }); for (const k of c.lanes) k.lit = false; }
|
|
||||||
}
|
}
|
||||||
l.inside = inside;
|
crossLanes(c, from, ball);
|
||||||
}
|
// La pallina che resta nello stesso punto (ferma, o che trema in un incastro): dopo un po' una scossa. Non se è
|
||||||
// La pallina che resta nello stesso punto (ferma, o che trema in un incastro): dopo un po' una scossa.
|
// trattenuta su un flipper alzato: quella è una mossa del giocatore.
|
||||||
if (!c.anchor || Math.hypot(ball.x - c.anchor.x, ball.y - c.anchor.y) > 3) { c.anchor = { x: ball.x, y: ball.y }; c.still = 0; }
|
if (!c.anchor || Math.hypot(ball.x - c.anchor.x, ball.y - c.anchor.y) > 3) { c.anchor = { x: ball.x, y: ball.y }; c.still = 0; }
|
||||||
else if ((c.still += STEP) > 2.5) { ball.vx += (random(c) - 0.5) * 160; ball.vy -= 90; c.still = 0; c.anchor = null; }
|
else if ((c.still += STEP) > NUDGE_AFTER) { c.still = 0; c.anchor = null; if (!cradled(c, ball)) { ball.vx += (random(c) - 0.5) * 160; ball.vy -= 90; } }
|
||||||
// Un lancio fiacco ricasca sulla molla: si rilancia.
|
// Un lancio fiacco (o una pallina ricaduta nella corsia) si assesta sul fondo e si rilancia.
|
||||||
if (ball.x > LANE.x0 && ball.y > 315 && Math.abs(ball.vy) < 8 && !ball.waiting) newBall(c);
|
if (ball.x > LANE.x0 && ball.y > 318 && Math.hypot(ball.vx, ball.vy) < 12) { if ((c.rest += STEP) > 0.3) newBall(c); } else c.rest = 0;
|
||||||
if (ball.y > FIELD.y1 + R * 2) drain(c);
|
if (ball.y > FIELD.y1 + R * 2) drain(c);
|
||||||
}
|
}
|
||||||
function dropTarget(c, t) {
|
function limit(ball) { const v = Math.hypot(ball.vx, ball.vy); if (v > MAX_SPEED) { ball.vx *= MAX_SPEED / v; ball.vy *= MAX_SPEED / v; } }
|
||||||
t.down = true; add(c, SCORE.target); c.events.push({ kind: 'flipper-bersaglio', power: 0.7 });
|
const cradled = (c, ball) => ['left', 'right'].some(side => c.flippers[side].held.length && Math.hypot(ball.x - PIVOT[side][0], ball.y - PIVOT[side][1]) < FLIP.len + 2 * R);
|
||||||
const bank = c.targets.filter(k => k.bank === t.bank);
|
// Le corsie: contano attraversando la riga in discesa, dovunque nella corsia (la posizione orizzontale interpolata).
|
||||||
if (bank.every(k => k.down)) { add(c, SCORE.bank); say(c, t.bank === 'pieve' ? 'La Pieve!' : 'La torre!'); for (const k of bank) k.down = false; }
|
function crossLanes(c, from, ball) {
|
||||||
|
if (!(from.y < LANE_LINE && ball.y >= LANE_LINE)) return;
|
||||||
|
const k = (LANE_LINE - from.y) / (ball.y - from.y), x = from.x + (ball.x - from.x) * k;
|
||||||
|
const l = c.lanes.find(l => x >= l.x0 && x < l.x1);
|
||||||
|
if (!l || l.lit) return;
|
||||||
|
l.lit = true; add(c, SCORE.lane); c.events.push({ kind: 'flipper-corsia', power: 0.6 });
|
||||||
|
if (c.lanes.every(k => k.lit)) { add(c, SCORE.palio); say(c, 'PALIO!'); c.events.push({ kind: 'flipper-palio', power: 1 }); for (const k of c.lanes) k.lit = false; }
|
||||||
}
|
}
|
||||||
function drain(c) {
|
function drain(c) {
|
||||||
c.events.push({ kind: 'flipper-buco', power: 0.8 });
|
c.events.push({ kind: 'flipper-buco', power: 0.8 });
|
||||||
if (c.t < c.saveUntil) { say(c, 'Pallina salvata!'); c.saved = true; newBall(c); return; }
|
if (c.sim < c.saveUntil) { say(c, 'Pallina salvata!'); c.saved = true; newBall(c); return; }
|
||||||
c.balls--; c.saved = false;
|
c.balls--; c.saved = false;
|
||||||
if (c.balls > 0) { say(c, `Pallina persa. Ne restano ${c.balls}.`); newBall(c); return; }
|
if (c.balls > 0) { say(c, `Persa: ne restano ${c.balls}.`); newBall(c); return; }
|
||||||
c.ball = null; c.step = 'result';
|
c.ball = null; c.step = 'result';
|
||||||
c.result = { winner: c.score >= c.skill.goal ? 'player' : 'cpu', score: c.score, goal: c.skill.goal };
|
c.result = { winner: c.score >= c.skill.goal ? 'player' : 'cpu', score: c.score, goal: c.skill.goal };
|
||||||
}
|
}
|
||||||
// Pallina contro un segmento: la si spinge fuori e si riflette la velocità. Torna true se ha toccato.
|
// Pallina contro un segmento: la si spinge fuori e si riflette la velocità. Torna la velocità d'urto lungo la
|
||||||
function collideSeg(c, ball, w) {
|
// normale (positiva se picchiava), o null se non tocca; `touching` ricorda il contatto, per contare solo gli urti nuovi.
|
||||||
|
function hitSeg(ball, w, e) {
|
||||||
const [ax, ay] = w.a, [bx, by] = w.b, dx = bx - ax, dy = by - ay, len2 = dx * dx + dy * dy || 1;
|
const [ax, ay] = w.a, [bx, by] = w.b, dx = bx - ax, dy = by - ay, len2 = dx * dx + dy * dy || 1;
|
||||||
const u = clamp(((ball.x - ax) * dx + (ball.y - ay) * dy) / len2, 0, 1), px = ax + dx * u, py = ay + dy * u;
|
const u = clamp(((ball.x - ax) * dx + (ball.y - ay) * dy) / len2, 0, 1), px = ax + dx * u, py = ay + dy * u;
|
||||||
let nx = ball.x - px, ny = ball.y - py; const d = Math.hypot(nx, ny);
|
let nx = ball.x - px, ny = ball.y - py; const d = Math.hypot(nx, ny);
|
||||||
if (d >= R) return false;
|
if (d >= R) { w.touching = false; return null; }
|
||||||
if (d < 1e-6) { nx = -dy; ny = dx; const n = Math.hypot(nx, ny); nx /= n; ny /= n; } else { nx /= d; ny /= d; }
|
if (d < 1e-6) { nx = -dy; ny = dx; const n = Math.hypot(nx, ny); nx /= n; ny /= n; } else { nx /= d; ny /= d; }
|
||||||
// Il cancelletto ferma solo chi scende dall'alto.
|
|
||||||
if (w.gate && !(ny < 0 && ball.vy > 0)) return false;
|
|
||||||
ball.x = px + nx * R; ball.y = py + ny * R;
|
ball.x = px + nx * R; ball.y = py + ny * R;
|
||||||
const vn = ball.vx * nx + ball.vy * ny;
|
const vn = ball.vx * nx + ball.vy * ny, fresh = !w.touching;
|
||||||
if (vn < 0) {
|
w.touching = true;
|
||||||
// La sponda elastica spinge via, un po' a caso come la gomma vera (così la pallina non fa il ping-pong in eterno).
|
if (vn < 0 && e !== null) { ball.vx -= (1 + e) * vn * nx; ball.vy -= (1 + e) * vn * ny; }
|
||||||
if (w.sling) { const a = Math.atan2(ny, nx) + (random(c) - 0.5) * 0.5, s = 230 + random(c) * 40; ball.vx = Math.cos(a) * s + (ball.vx - vn * nx) * 0.3; ball.vy = Math.sin(a) * s + (ball.vy - vn * ny) * 0.3; add(c, SCORE.sling); c.events.push({ kind: 'flipper-sponda', power: 0.6 }); }
|
return { vn: -vn, nx, ny, fresh };
|
||||||
else { ball.vx -= (1 + w.e) * vn * nx; ball.vy -= (1 + w.e) * vn * ny; }
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
function collideCircle(ball, o, e) {
|
function collideSeg(c, ball, w) {
|
||||||
|
if (!w.sling) { hitSeg(ball, w, w.e); return; }
|
||||||
|
// La sponda elastica: spinge via lungo la normale, tenendo la componente di strisciamento, e un po' a caso come
|
||||||
|
// la gomma vera; solo a un urto nuovo, e solo se picchia.
|
||||||
|
const hit = hitSeg(ball, w, null);
|
||||||
|
if (!hit || hit.vn <= 0) return;
|
||||||
|
if (hit.fresh && hit.vn > IMPACT) {
|
||||||
|
const a = Math.atan2(hit.ny, hit.nx) + (random(c) - 0.5) * 0.24, s = 250 + random(c) * 30;
|
||||||
|
const tx = ball.vx + hit.vn * hit.nx, ty = ball.vy + hit.vn * hit.ny; // la parte lungo la sponda
|
||||||
|
ball.vx = Math.cos(a) * s + tx * 0.6; ball.vy = Math.sin(a) * s + ty * 0.6;
|
||||||
|
add(c, SCORE.sling); c.events.push({ kind: 'flipper-sponda', power: 0.6 });
|
||||||
|
} else { ball.vx += 1.3 * hit.vn * hit.nx; ball.vy += 1.3 * hit.vn * hit.ny; }
|
||||||
|
}
|
||||||
|
function collideTarget(c, ball, t) {
|
||||||
|
const hit = hitSeg(ball, t, 0.5);
|
||||||
|
if (!hit || !hit.fresh || hit.vn < IMPACT) return;
|
||||||
|
t.down = true; add(c, SCORE.target); c.events.push({ kind: 'flipper-bersaglio', power: 0.7 });
|
||||||
|
const bank = c.targets.filter(k => k.bank === t.bank);
|
||||||
|
if (bank.every(k => k.down)) { add(c, SCORE.bank); say(c, t.bank === 'pieve' ? 'LA PIEVE!' : 'LA TORRE!'); c.resets[t.bank] = c.sim + BANK_RESET; }
|
||||||
|
}
|
||||||
|
// Pallina contro un cerchio: spinta fuori; torna la velocità d'urto lungo la normale (null se non tocca).
|
||||||
|
function hitCircle(ball, o, e) {
|
||||||
const dx = ball.x - o.x, dy = ball.y - o.y, d = Math.hypot(dx, dy), min = R + o.r;
|
const dx = ball.x - o.x, dy = ball.y - o.y, d = Math.hypot(dx, dy), min = R + o.r;
|
||||||
if (d >= min) return false;
|
if (d >= min) { o.touching = false; return null; }
|
||||||
const nx = d > 1e-6 ? dx / d : 0, ny = d > 1e-6 ? dy / d : -1;
|
const nx = d > 1e-6 ? dx / d : 0, ny = d > 1e-6 ? dy / d : -1;
|
||||||
ball.x = o.x + nx * min; ball.y = o.y + ny * min;
|
ball.x = o.x + nx * min; ball.y = o.y + ny * min;
|
||||||
const vn = ball.vx * nx + ball.vy * ny;
|
const vn = ball.vx * nx + ball.vy * ny, fresh = !o.touching;
|
||||||
|
o.touching = true;
|
||||||
if (vn < 0) { ball.vx -= (1 + e) * vn * nx; ball.vy -= (1 + e) * vn * ny; }
|
if (vn < 0) { ball.vx -= (1 + e) * vn * nx; ball.vy -= (1 + e) * vn * ny; }
|
||||||
return true;
|
return { vn: -vn, nx, ny, fresh };
|
||||||
}
|
}
|
||||||
function kick(c, ball, o, speed) {
|
function collideBumper(c, ball, b) {
|
||||||
const a = Math.atan2(ball.y - o.y, ball.x - o.x) + (random(c) - 0.5) * 0.3;
|
const hit = hitCircle(ball, b, 0.2);
|
||||||
ball.vx = Math.cos(a) * speed; ball.vy = Math.sin(a) * speed;
|
if (!hit || !hit.fresh || hit.vn < IMPACT) return;
|
||||||
|
const a = Math.atan2(hit.ny, hit.nx) + (random(c) - 0.5) * 0.2;
|
||||||
|
ball.vx = Math.cos(a) * c.skill.kick; ball.vy = Math.sin(a) * c.skill.kick;
|
||||||
|
b.hit = 0.15; add(c, SCORE.bumper); c.events.push({ kind: 'flipper-fungo', power: 0.8 });
|
||||||
|
}
|
||||||
|
function collideRegolo(c, ball) {
|
||||||
|
const hit = hitCircle(ball, c.regolo, 0.7);
|
||||||
|
if (!hit || !hit.fresh || hit.vn < IMPACT) return;
|
||||||
|
c.regolo.hit = 0.3; add(c, SCORE.regolo); say(c, 'IL REGOLO!'); c.events.push({ kind: 'flipper-bersaglio', power: 1 });
|
||||||
}
|
}
|
||||||
// Il flipper è una capsula che gira: alla pallina si aggiunge la velocità del punto toccato.
|
// Il flipper è una capsula che gira: alla pallina si aggiunge la velocità del punto toccato.
|
||||||
function collideFlipper(c, ball, side) {
|
function collideFlipper(c, ball, side) {
|
||||||
@@ -195,7 +257,7 @@ function collideFlipper(c, ball, side) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Comandi: col mouse o col dito, prima del lancio tenere premuto carica la molla e lasciare lancia; in gioco
|
// --- Comandi: col mouse o col dito, prima del lancio tenere premuto carica la molla e lasciare lancia; in gioco
|
||||||
// la metà sinistra della scena alza il flipper sinistro, la destra il destro, finché si tiene premuto.
|
// la metà sinistra della scena alza il flipper sinistro, la destra il destro, finché si tiene premuto (anche due dita).
|
||||||
export function aimScreen(c, [x, y]) { if (!Number.isFinite(x) || !Number.isFinite(y)) return false; c.cursor = [x, y]; return true; }
|
export function aimScreen(c, [x, y]) { if (!Number.isFinite(x) || !Number.isFinite(y)) return false; c.cursor = [x, y]; return true; }
|
||||||
export function press(c, pointerId = 'ptr') {
|
export function press(c, pointerId = 'ptr') {
|
||||||
if (c.step !== 'play') return false;
|
if (c.step !== 'play') return false;
|
||||||
@@ -208,13 +270,24 @@ export function release(c, pointerId = 'ptr') {
|
|||||||
for (const side of ['left', 'right']) if (c.flippers[side].held.includes(pointerId)) any = hold(c, side, pointerId, false) || any;
|
for (const side of ['left', 'right']) if (c.flippers[side].held.includes(pointerId)) any = hold(c, side, pointerId, false) || any;
|
||||||
return any;
|
return any;
|
||||||
}
|
}
|
||||||
export const cancel = (c, pointerId) => pointerId === undefined ? release(c, c.pointerId ?? 'ptr') : release(c, pointerId);
|
// Annullare interrompe senza fare niente: la molla si scarica, i flipper ricadono (tutti, senza puntatore).
|
||||||
// I pulsanti «← Flipper» e «Flipper →» (tenuti premuti), e le frecce.
|
export function cancel(c, pointerId) {
|
||||||
|
const all = pointerId === undefined;
|
||||||
|
if (all || c.pointerId === pointerId) { c.holding = false; c.holdTime = 0; c.pointerId = null; }
|
||||||
|
for (const f of Object.values(c.flippers)) f.held = all ? [] : f.held.filter(id => id !== pointerId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// I pulsanti «← Flipper» e «Flipper →» (tenuti premuti; da tastiera un colpo breve), e le frecce.
|
||||||
export const turn = (c, dir, who = 'button') => c.step === 'play' && hold(c, dir < 0 ? 'left' : 'right', who, true);
|
export const turn = (c, dir, who = 'button') => c.step === 'play' && hold(c, dir < 0 ? 'left' : 'right', who, true);
|
||||||
export const stopTurn = (c, who = 'button') => hold(c, 'left', who, false) | hold(c, 'right', who, false);
|
export function stopTurn(c, who = 'button') {
|
||||||
|
if (who === 'click') return false; // il colpo breve finisce da sé
|
||||||
|
const l = hold(c, 'left', who, false), r = hold(c, 'right', who, false);
|
||||||
|
return l || r;
|
||||||
|
}
|
||||||
export const nudge = () => false;
|
export const nudge = () => false;
|
||||||
export function setPower(c, v) { if (!canThrow(c) || !Number.isFinite(v)) return false; c.power = clamp(v, 0, 1); return true; }
|
export function setPower(c, v) { if (!canThrow(c) || !Number.isFinite(v)) return false; c.power = clamp(v, 0, 1); return true; }
|
||||||
export function key(c, k, down) {
|
export function key(c, k, down) {
|
||||||
|
if (c.step !== 'play') return false; // a partita finita Spazio e Invio chiudono il risultato (main.js)
|
||||||
if (k === 'ArrowLeft' || k === 'ArrowRight') { hold(c, k === 'ArrowLeft' ? 'left' : 'right', 'key-' + k, down); return true; }
|
if (k === 'ArrowLeft' || k === 'ArrowRight') { hold(c, k === 'ArrowLeft' ? 'left' : 'right', 'key-' + k, down); return true; }
|
||||||
if (k === ' ') { if (down) { if (!c.holding && waiting(c)) press(c, 'key'); } else if (c.holding && c.pointerId === 'key') release(c, 'key'); return true; }
|
if (k === ' ') { if (down) { if (!c.holding && waiting(c)) press(c, 'key'); } else if (c.holding && c.pointerId === 'key') release(c, 'key'); return true; }
|
||||||
if (k === 'Enter') { if (down) shoot(c); return true; }
|
if (k === 'Enter') { if (down) shoot(c); return true; }
|
||||||
@@ -225,7 +298,8 @@ export const shoot = c => canThrow(c) && launch(c, c.power);
|
|||||||
export const button = c => c.step === 'result' ? 'Finita' : waiting(c) ? 'Lancia' : 'In gioco…';
|
export const button = c => c.step === 'result' ? 'Finita' : waiting(c) ? 'Lancia' : 'In gioco…';
|
||||||
export function status(c) {
|
export function status(c) {
|
||||||
if (c.step === 'result') return `${c.result.score} punti. ${c.result.winner === 'player' ? `Traguardo ${c.result.goal}: fatto!` : `Ne servivano ${c.result.goal}.`}`;
|
if (c.step === 'result') return `${c.result.score} punti. ${c.result.winner === 'player' ? `Traguardo ${c.result.goal}: fatto!` : `Ne servivano ${c.result.goal}.`}`;
|
||||||
const tally = `Punti ${c.score} (traguardo ${c.skill.goal}), palline ${c.balls}.`;
|
const goal = c.result ? 'traguardo fatto, ora il record' : `traguardo ${c.skill.goal}`;
|
||||||
|
const tally = `Punti ${c.score} (${goal}), palline ${c.balls}.`;
|
||||||
if (c.line) return `${c.line} ${tally}`;
|
if (c.line) return `${c.line} ${tally}`;
|
||||||
return waiting(c) ? `Tieni premuto per caricare la molla e lascia, o premi Lancia. ${tally}` : `Flipper: metà sinistra e destra della scena, o i pulsanti. ${tally}`;
|
return waiting(c) ? `Tieni premuto per caricare la molla e lascia, o premi Lancia. ${tally}` : `Flipper: metà sinistra e destra della scena, o i pulsanti. ${tally}`;
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-3
@@ -492,9 +492,12 @@ const debugView = () => debug && debugShown && game && { mouse, points: debugPoi
|
|||||||
const TOUCH_REACH = 22;
|
const TOUCH_REACH = 22;
|
||||||
const reachOf = e => e.pointerType === 'touch' || e.pointerType === 'pen' ? TOUCH_REACH * WIDTH / canvas.getBoundingClientRect().width : 0;
|
const reachOf = e => e.pointerType === 'touch' || e.pointerType === 'pen' ? TOUCH_REACH * WIDTH / canvas.getBoundingClientRect().width : 0;
|
||||||
let press = null;
|
let press = null;
|
||||||
|
// Le dita in più sul flipper (oltre a `press`): ognuna lascia il suo flipper quando si alza. Il tasto destro del
|
||||||
|
// mouse ha lo stesso pointerId del sinistro: per il flipper vale come un dito a parte.
|
||||||
|
const extra = new Set(), pointerKey = e => e.button === 2 ? 'mouse-right' : e.pointerId;
|
||||||
canvas.addEventListener('pointerdown', e => {
|
canvas.addEventListener('pointerdown', e => {
|
||||||
if (debug && e.shiftKey && e.button === 0) { debugPoint(scenePoint(e)); return; }
|
if (debug && e.shiftKey && e.button === 0) { debugPoint(scenePoint(e)); return; }
|
||||||
if (!running || e.button > 0) return;
|
if (!running || (e.button > 0 && !(e.button === 2 && game.closeup && story.closeups[game.closeup.id].type === 'flipper'))) return;
|
||||||
// Nel primo piano un tocco sfoglia (o salta la battuta): gli oggetti della scena dietro non si toccano.
|
// Nel primo piano un tocco sfoglia (o salta la battuta): gli oggetti della scena dietro non si toccano.
|
||||||
if (game.closeup) {
|
if (game.closeup) {
|
||||||
const type = story.closeups[game.closeup.id].type, deck = CARD_AT[type];
|
const type = story.closeups[game.closeup.id].type, deck = CARD_AT[type];
|
||||||
@@ -520,6 +523,12 @@ canvas.addEventListener('pointerdown', e => {
|
|||||||
if (gara.press(game.closeup, e.pointerId)) { press = { gara: e.pointerId }; canvas.setPointerCapture(e.pointerId); }
|
if (gara.press(game.closeup, e.pointerId)) { press = { gara: e.pointerId }; canvas.setPointerCapture(e.pointerId); }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Il flipper: un secondo dito (o il secondo pulsante del mouse) comanda l'altro flipper, per conto suo.
|
||||||
|
if (press?.closeup && type === 'flipper' && !extra.has(pointerKey(e))) {
|
||||||
|
game.closeupAim(scenePoint(e));
|
||||||
|
if (game.closeupPress(pointerKey(e))) { extra.add(pointerKey(e)); canvas.setPointerCapture(e.pointerId); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (press?.closeup) return;
|
if (press?.closeup) return;
|
||||||
const aimed = game.closeupAim(scenePoint(e));
|
const aimed = game.closeupAim(scenePoint(e));
|
||||||
if (['biliardo', 'rinquarto', 'basket', 'micro', 'cerbottana', 'condor'].includes(story.closeups[game.closeup.id].type)) {
|
if (['biliardo', 'rinquarto', 'basket', 'micro', 'cerbottana', 'condor'].includes(story.closeups[game.closeup.id].type)) {
|
||||||
@@ -537,6 +546,8 @@ canvas.addEventListener('pointerdown', e => {
|
|||||||
press = { point, reach, long: false, timer: setTimeout(() => { press.long = true; command.clickScene(point, { look: true, reach }); }, LONG_PRESS) };
|
press = { point, reach, long: false, timer: setTimeout(() => { press.long = true; command.clickScene(point, { look: true, reach }); }, LONG_PRESS) };
|
||||||
});
|
});
|
||||||
canvas.addEventListener('pointerup', e => {
|
canvas.addEventListener('pointerup', e => {
|
||||||
|
const key = e.button === 2 ? 'mouse-right' : e.pointerId;
|
||||||
|
if (extra.has(key)) { extra.delete(key); game?.closeupRelease(key); return; }
|
||||||
if (!press) return;
|
if (!press) return;
|
||||||
// Scopa: la carta toccata e lasciata sullo stesso punto. Se sta nella presa già scelta, si prende quella; se no
|
// Scopa: la carta toccata e lasciata sullo stesso punto. Se sta nella presa già scelta, si prende quella; se no
|
||||||
// si sceglie la prima presa che la contiene (un altro tocco conferma). Trascinando via non succede niente.
|
// si sceglie la prima presa che la contiene (un altro tocco conferma). Trascinando via non succede niente.
|
||||||
@@ -574,12 +585,13 @@ canvas.addEventListener('pointerup', e => {
|
|||||||
press = null;
|
press = null;
|
||||||
});
|
});
|
||||||
canvas.addEventListener('pointercancel', e => {
|
canvas.addEventListener('pointercancel', e => {
|
||||||
|
if (extra.has(e.pointerId)) { extra.delete(e.pointerId); game?.closeupCancel(e.pointerId); return; }
|
||||||
if (press?.closeup && e.pointerId !== press.pointerId) return;
|
if (press?.closeup && e.pointerId !== press.pointerId) return;
|
||||||
if (press) clearTimeout(press.timer);
|
if (press) clearTimeout(press.timer);
|
||||||
if (!press?.closeup || game?.closeup === press.state) game?.closeupCancel(e.pointerId);
|
if (!press?.closeup || game?.closeup === press.state) game?.closeupCancel(e.pointerId);
|
||||||
press = null;
|
press = null;
|
||||||
});
|
});
|
||||||
canvas.addEventListener('lostpointercapture', e => { if (press?.closeup && e.pointerId !== press.pointerId) return; if (press?.closeup || press?.gara !== undefined || press?.card !== undefined || press?.table !== undefined) { press = null; game?.closeupCancel(e.pointerId); } });
|
canvas.addEventListener('lostpointercapture', e => { if (extra.has(e.pointerId)) { extra.delete(e.pointerId); game?.closeupCancel(e.pointerId); return; } if (press?.closeup && e.pointerId !== press.pointerId) return; if (press?.closeup || press?.gara !== undefined || press?.card !== undefined || press?.table !== undefined) { press = null; game?.closeupCancel(e.pointerId); } });
|
||||||
canvas.addEventListener('contextmenu', e => {
|
canvas.addEventListener('contextmenu', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (running && !game.closeup) command.clickScene(scenePoint(e), { look: true });
|
if (running && !game.closeup) command.clickScene(scenePoint(e), { look: true });
|
||||||
@@ -587,6 +599,13 @@ canvas.addEventListener('contextmenu', e => {
|
|||||||
canvas.addEventListener('pointermove', e => {
|
canvas.addEventListener('pointermove', e => {
|
||||||
if (!running) return;
|
if (!running) return;
|
||||||
mouse = e.pointerType === 'mouse' ? scenePoint(e) : null;
|
mouse = e.pointerType === 'mouse' ? scenePoint(e) : null;
|
||||||
|
// Il tasto destro premuto o lasciato mentre il sinistro è giù arriva come pointermove (pulsanti «in accordo»):
|
||||||
|
// al flipper vale come un secondo dito, per il flipper di destra.
|
||||||
|
if (e.button === 2 && game.closeup && story.closeups[game.closeup.id].type === 'flipper') {
|
||||||
|
if (e.buttons & 2) { if (!extra.has('mouse-right')) { game.closeupAim(scenePoint(e)); if (game.closeupPress('mouse-right')) extra.add('mouse-right'); } }
|
||||||
|
else if (extra.has('mouse-right')) { extra.delete('mouse-right'); game.closeupRelease('mouse-right'); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Al biliardo e alle bocce la mira segue il mouse finché un clic (o una regolazione fine) non la fissa: dopo,
|
// Al biliardo e alle bocce la mira segue il mouse finché un clic (o una regolazione fine) non la fissa: dopo,
|
||||||
// attraversare il campo per raggiungere la forza, «Tira» o «Lancia» non cambia il colpo. Al tiro dopo torna libera.
|
// attraversare il campo per raggiungere la forza, «Tira» o «Lancia» non cambia il colpo. Al tiro dopo torna libera.
|
||||||
// (Bocce: scoperto girando il video «Come si gioca», la mira finiva a due passi andando verso il cursore.)
|
// (Bocce: scoperto girando il video «Come si gioca», la mira finiva a due passi andando verso il cursore.)
|
||||||
@@ -620,7 +639,7 @@ const MINI = {
|
|||||||
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.' },
|
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.' },
|
||||||
bruciate: { ...bruciate, back: 'Torna alla festa', label: 'Le bruciate: tieni premuto e fai dei giri, col mouse o col dito, per girare la manovella; oppure Spazio o le frecce, una spinta a ogni colpo. Né troppo piano né troppo forte; quando son cotte, Rovescia nel sacco (o Invio).' },
|
bruciate: { ...bruciate, back: 'Torna alla festa', label: 'Le bruciate: tieni premuto e fai dei giri, col mouse o col dito, per girare la manovella; oppure Spazio o le frecce, una spinta a ogni colpo. Né troppo piano né troppo forte; quando son cotte, Rovescia nel sacco (o Invio).' },
|
||||||
flipper: { ...flipper, toggle: true, back: 'Torna in strada', label: 'Il flipper del River: tieni premuto per caricare la molla e lascia per lanciare (o regola la forza e premi Lancia, o Spazio). In gioco, tieni premuto sulla metà sinistra o destra della scena per alzare il flipper di quel lato, oppure i pulsanti Flipper o le frecce sinistra e destra.' },
|
flipper: { ...flipper, toggle: true, back: 'Torna in strada', label: 'Il flipper del River: tieni premuto per caricare la molla e lascia per lanciare (o regola la forza e premi Lancia, o Spazio). In gioco, tieni premuto sulla metà sinistra o destra della scena per alzare il flipper di quel lato, anche con due dita o col tasto destro del mouse; oppure i pulsanti Flipper o le frecce sinistra e destra.' },
|
||||||
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).
|
||||||
@@ -1057,6 +1076,8 @@ function updateUi() {
|
|||||||
setValue($('bocce-power'), Math.round(c.power * 100));
|
setValue($('bocce-power'), Math.round(c.power * 100));
|
||||||
setValue($('bocce-power-value'), `${Math.round(c.power * 100)}%`);
|
setValue($('bocce-power-value'), `${Math.round(c.power * 100)}%`);
|
||||||
for (const id of ['bocce-power', 'bocce-left', 'bocce-right']) setProp($(id), 'disabled', !(mg.canAdjust?.(c) ?? mg.canThrow(c)) || (c.holding && !mg.toggle) || !!game.speech);
|
for (const id of ['bocce-power', 'bocce-left', 'bocce-right']) setProp($(id), 'disabled', !(mg.canAdjust?.(c) ?? mg.canThrow(c)) || (c.holding && !mg.toggle) || !!game.speech);
|
||||||
|
// Al flipper i due pulsanti valgono per tutta la partita, il cursore della molla solo prima del lancio.
|
||||||
|
if (mg.canFlip) for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'disabled', !mg.canFlip(c) || !!game.speech);
|
||||||
setProp($('bocce-throw'), 'disabled', !(mg.canPress?.(c) ?? mg.canThrow(c)) || (c.holding && !mg.toggle) || !!game.speech);
|
setProp($('bocce-throw'), 'disabled', !(mg.canPress?.(c) ?? mg.canThrow(c)) || (c.holding && !mg.toggle) || !!game.speech);
|
||||||
// Il GP di Fortezza: niente forza, e i due pulsanti di mira diventano lo sterzo (si tengono premuti).
|
// Il GP di Fortezza: niente forza, e i due pulsanti di mira diventano lo sterzo (si tengono premuti).
|
||||||
// Nel GP di Fortezza il cursore della forza diventa la sensibilità dello sterzo col dito (ricordata).
|
// Nel GP di Fortezza il cursore della forza diventa la sensibilità dello sterzo col dito (ricordata).
|
||||||
|
|||||||
+2
-2
@@ -4459,8 +4459,8 @@ export const story = {
|
|||||||
start: {
|
start: {
|
||||||
options: [
|
options: [
|
||||||
{ text: 'Una partita alla bona, tanto per sentire la molla.', next: 'end', then: [{ say: 'Seimila punti e sono contento.' }, { closeup: 'flipper', level: 'bona' }] },
|
{ text: 'Una partita alla bona, tanto per sentire la molla.', next: 'end', then: [{ say: 'Seimila punti e sono contento.' }, { closeup: 'flipper', level: 'bona' }] },
|
||||||
{ text: 'Una partita seria.', next: 'end', then: [{ say: 'Dodicimila. Come quando ci lasciavo le cinquecento lire.' }, { closeup: 'flipper', level: 'seria' }] },
|
{ text: 'Una partita seria.', next: 'end', then: [{ say: 'Diecimila. Come quando ci lasciavo le cinquecento lire.' }, { closeup: 'flipper', level: 'seria' }] },
|
||||||
{ text: 'A fassi male.', next: 'end', then: [{ say: 'Venticinquemila. Il record del River, o quasi.' }, { closeup: 'flipper', level: 'male' }] },
|
{ text: 'A fassi male.', next: 'end', then: [{ say: 'Sedicimila. Il record del River, o quasi.' }, { closeup: 'flipper', level: 'male' }] },
|
||||||
{ text: 'Un\'altra volta.', next: 'end' },
|
{ text: 'Un\'altra volta.', next: 'end' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ 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-left, #bocce-right { touch-action: none; }
|
||||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help, #campana-help, #bruciate-help, #flipper-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, #bruciate-help, #flipper-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, #bruciate-help summary, #flipper-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, #bruciate-help summary, #flipper-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, #bruciate-help p, #flipper-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, #bruciate-help p, #flipper-help p { max-width: 76em; line-height: 1.7; }
|
||||||
|
|||||||
+36
-15
@@ -1,5 +1,6 @@
|
|||||||
// Il flipper del River nel browser vero: dal bar, la molla col mouse e col dito, i flipper coi pulsanti e con le
|
// Il flipper del River nel browser vero: dal bar, la molla col mouse e col dito, i flipper con due dita sulla scena,
|
||||||
// frecce, il risultato. npm run browser:flipper — schermate in /tmp/regolo-flipper.
|
// coi pulsanti e con le frecce, la pausa che lascia tutto, il risultato. npm run browser:flipper — schermate in
|
||||||
|
// /tmp/regolo-flipper.
|
||||||
import { chromium } from 'playwright';
|
import { chromium } from 'playwright';
|
||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
@@ -28,43 +29,63 @@ try {
|
|||||||
assert.equal(await page.locator('#bocce-throw').textContent(), 'Lancia');
|
assert.equal(await page.locator('#bocce-throw').textContent(), 'Lancia');
|
||||||
assert.ok(await page.locator('#flipper-help').isVisible());
|
assert.ok(await page.locator('#flipper-help').isVisible());
|
||||||
assert.equal(await page.locator('#bocce-left').textContent(), '← Flipper');
|
assert.equal(await page.locator('#bocce-left').textContent(), '← Flipper');
|
||||||
|
assert.ok(!(await page.locator('#bocce-left').isDisabled()), 'i pulsanti dei flipper sono attivi anche prima del lancio');
|
||||||
await page.screenshot({ path: `${out}/${name}-tavola.png` });
|
await page.screenshot({ path: `${out}/${name}-tavola.png` });
|
||||||
const box = await page.locator('#scene').boundingBox(), P = (x, y) => [box.x + x / 640 * box.width, box.y + y / 360 * box.height];
|
const box = await page.locator('#scene').boundingBox(), P = (x, y) => [box.x + x / 640 * box.width, box.y + y / 360 * box.height];
|
||||||
// La molla: col mouse tenendo premuto sulla scena, col dito il pulsante Lancia.
|
const cdp = mobile ? await page.context().newCDPSession(page) : null;
|
||||||
if (mobile) await page.locator('#bocce-throw').tap();
|
const touch = (type, points) => cdp.send('Input.dispatchTouchEvent', { type, touchPoints: points.map(([x, y]) => ({ x, y })) });
|
||||||
else {
|
// La molla: tenendo premuto sulla scena, col mouse e col dito.
|
||||||
|
if (mobile) {
|
||||||
|
await touch('touchStart', [P(440, 320)]);
|
||||||
|
await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.3, null, { polling: 'raf' });
|
||||||
|
await touch('touchEnd', []);
|
||||||
|
} else {
|
||||||
await page.mouse.move(...P(440, 320)); await page.mouse.down();
|
await page.mouse.move(...P(440, 320)); await page.mouse.down();
|
||||||
await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.3, null, { polling: 'raf' });
|
await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.3, null, { polling: 'raf' });
|
||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
}
|
}
|
||||||
await page.waitForFunction(() => game.closeup && !game.closeup.ball?.waiting, null, { timeout: 5000 });
|
await page.waitForFunction(() => game.closeup && !game.closeup.ball?.waiting, null, { timeout: 5000 });
|
||||||
// I flipper: col dito i pulsanti tenuti premuti, col mouse la metà della scena e poi le frecce.
|
// Due dita sulla scena: sinistra, poi anche destra, poi si alza la sinistra e la destra resta su.
|
||||||
if (mobile) {
|
if (mobile) {
|
||||||
const b = await page.locator('#bocce-left').boundingBox();
|
await touch('touchStart', [P(250, 200)]);
|
||||||
const cdp = await page.context().newCDPSession(page);
|
|
||||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: b.x + b.width / 2, y: b.y + b.height / 2 }] });
|
|
||||||
await page.waitForFunction(() => game.closeup.flippers.left.held.length > 0);
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length > 0);
|
||||||
|
await touch('touchStart', [P(250, 200), P(400, 200)]);
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.right.held.length > 0 && game.closeup.flippers.left.held.length > 0, null, { timeout: 3000 });
|
||||||
await page.screenshot({ path: `${out}/${name}-flipper.png` });
|
await page.screenshot({ path: `${out}/${name}-flipper.png` });
|
||||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
|
await touch('touchEnd', [P(400, 200)]);
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length === 0 && game.closeup.flippers.right.held.length > 0, null, { timeout: 3000 });
|
||||||
|
await touch('touchEnd', []);
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.right.held.length === 0);
|
||||||
|
// I pulsanti tenuti premuti.
|
||||||
|
const b = await page.locator('#bocce-left').boundingBox();
|
||||||
|
await touch('touchStart', [[b.x + b.width / 2, b.y + b.height / 2]]);
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length > 0);
|
||||||
|
await touch('touchEnd', []);
|
||||||
await page.waitForFunction(() => game.closeup.flippers.left.held.length === 0);
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length === 0);
|
||||||
} else {
|
} else {
|
||||||
await page.mouse.move(...P(400, 200)); await page.mouse.down();
|
await page.mouse.move(...P(400, 200)); await page.mouse.down();
|
||||||
await page.waitForFunction(() => game.closeup.flippers.right.held.length > 0);
|
await page.waitForFunction(() => game.closeup.flippers.right.held.length > 0);
|
||||||
|
await page.mouse.down({ button: 'right' });
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.right.held.length > 1, null, { timeout: 3000 });
|
||||||
await page.screenshot({ path: `${out}/${name}-flipper.png` });
|
await page.screenshot({ path: `${out}/${name}-flipper.png` });
|
||||||
await page.mouse.up();
|
await page.mouse.up(); await page.mouse.up({ button: 'right' });
|
||||||
await page.waitForFunction(() => game.closeup.flippers.right.held.length === 0);
|
await page.waitForFunction(() => game.closeup.flippers.right.held.length === 0);
|
||||||
await page.keyboard.down('ArrowLeft');
|
await page.keyboard.down('ArrowLeft');
|
||||||
await page.waitForFunction(() => game.closeup.flippers.left.held.length > 0);
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length > 0);
|
||||||
await page.keyboard.up('ArrowLeft');
|
// La pausa lascia tutto: tornando, il flipper è giù anche se il tasto era premuto.
|
||||||
|
await page.click('#pause-button');
|
||||||
await page.waitForFunction(() => game.closeup.flippers.left.held.length === 0);
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length === 0);
|
||||||
|
await page.keyboard.up('ArrowLeft');
|
||||||
|
await page.click('#resume');
|
||||||
|
await page.waitForFunction(() => document.querySelector('#pause').hidden);
|
||||||
}
|
}
|
||||||
// La fine: si fa finire la partita e si guarda il quadro.
|
// La fine: si fa finire la partita e si guarda il quadro.
|
||||||
await page.evaluate(() => { const c = game.closeup; c.step = 'result'; c.ball = null; c.result = { winner: 'player', score: 7350, goal: c.skill.goal }; });
|
await page.evaluate(() => { const c = game.closeup; c.score = 7350; c.step = 'result'; c.ball = null; c.result = { winner: 'player', score: 7350, goal: c.skill.goal }; });
|
||||||
await page.waitForFunction(() => document.querySelector('#bocce-throw').textContent === 'Finita');
|
await page.waitForFunction(() => document.querySelector('#bocce-throw').textContent === 'Finita');
|
||||||
assert.match(await page.locator('#bocce-status').textContent(), /7350 punti/);
|
assert.match(await page.locator('#bocce-status').textContent(), /7350 punti/);
|
||||||
await page.screenshot({ path: `${out}/${name}-fine.png` });
|
await page.screenshot({ path: `${out}/${name}-fine.png` });
|
||||||
await page.locator('#closeup-close').click();
|
if (mobile) await page.locator('#closeup-close').click(); else await page.keyboard.press('Enter'); // Invio chiude il risultato
|
||||||
await page.waitForFunction(() => game.state.flags.flipper_vinto);
|
await page.waitForFunction(() => game.state.flags.flipper_vinto && !game.closeup);
|
||||||
await page.close();
|
await page.close();
|
||||||
}
|
}
|
||||||
assert.deepEqual(errors, []);
|
assert.deepEqual(errors, []);
|
||||||
|
|||||||
+92
-46
@@ -7,6 +7,7 @@ import { story } from '../src/story/story.js';
|
|||||||
import { settle } from './helpers.mjs';
|
import { settle } from './helpers.mjs';
|
||||||
|
|
||||||
const run = (c, secs) => { for (let i = 0; i < secs * 60; i++) m.update(c, 1 / 60); };
|
const run = (c, secs) => { for (let i = 0; i < secs * 60; i++) m.update(c, 1 / 60); };
|
||||||
|
const put = (c, ball) => Object.assign(c.ball, ball);
|
||||||
// Il giocatore automatico: alza il flipper quando la pallina gli scende addosso, come un umano attento.
|
// Il giocatore automatico: alza il flipper quando la pallina gli scende addosso, come un umano attento.
|
||||||
function autoplay(c, secs = 600) {
|
function autoplay(c, secs = 600) {
|
||||||
for (let i = 0; i < secs * 60 && c.step === 'play'; i++) {
|
for (let i = 0; i < secs * 60 && c.step === 'play'; i++) {
|
||||||
@@ -21,94 +22,140 @@ function autoplay(c, secs = 600) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('la tavola: tutto sta dentro il campo, le corsie sotto l\'arco, i funghi sui dischi del disegno', () => {
|
test('la tavola: tutto sta dentro il campo, le corsie coprono tutta la larghezza sotto l\'arco, i funghi sui dischi del disegno', () => {
|
||||||
const c = m.newFlipper(1);
|
const c = m.newFlipper(1);
|
||||||
for (const w of c.walls) for (const [x, y] of [w.a, w.b]) assert.ok(x >= m.FIELD.x0 && x <= m.FIELD.x1 && y >= m.FIELD.y0 && y <= m.FIELD.y1, `${w.a}-${w.b}`);
|
for (const w of c.walls) for (const [x, y] of [w.a, w.b]) assert.ok(x >= m.FIELD.x0 && x <= m.FIELD.x1 && y >= m.FIELD.y0 && y <= m.FIELD.y1, `${w.a}-${w.b}`);
|
||||||
for (const b of c.bumpers) assert.ok(b.x > m.FIELD.x0 + 20 && b.x < m.LANE.x0 - 20);
|
// I tre dischi dipinti da Codex (misurati sull'immagine): i funghi ci stanno sopra, a meno di 3 pixel.
|
||||||
|
for (const [b, [x, y]] of c.bumpers.map((b, i) => [b, [[281, 120], [360, 120], [320, 153]][i]])) assert.ok(Math.hypot(b.x - x, b.y - y) < 3);
|
||||||
assert.equal(c.lanes.length, 4); assert.deepEqual(c.lanes.map(l => l.rione), m.RIONI);
|
assert.equal(c.lanes.length, 4); assert.deepEqual(c.lanes.map(l => l.rione), m.RIONI);
|
||||||
|
assert.equal(c.lanes[0].x0, m.FIELD.x0); assert.equal(c.lanes[3].x1, m.LANE.x0);
|
||||||
|
for (let i = 1; i < 4; i++) assert.equal(c.lanes[i].x0, c.lanes[i - 1].x1, 'corsie contigue');
|
||||||
assert.equal(c.targets.length, 6); assert.equal(c.balls, m.BALLS);
|
assert.equal(c.targets.length, 6); assert.equal(c.balls, m.BALLS);
|
||||||
assert.ok(m.waiting(c) && m.canThrow(c) && c.ball.x > m.LANE.x0, 'la pallina aspetta sulla molla');
|
assert.ok(m.waiting(c) && m.canThrow(c) && c.ball.x > m.LANE.x0, 'la pallina aspetta sulla molla');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('il lancio: la molla manda la pallina su per la corsia, oltre il cancelletto, e non torna più nella corsia', () => {
|
test('il lancio: anche la molla più fiacca arriva in campo, quella forte fa il giro dell\'arco; la ricaduta si rilancia', () => {
|
||||||
const c = m.newFlipper(2, { level: 'bona' });
|
for (const level of ['bona', 'male']) for (const power of [0, 0.5, 1]) {
|
||||||
assert.ok(m.launch(c, 0.8)); assert.ok(!m.waiting(c));
|
const c = m.newFlipper(2, { level }); assert.ok(m.launch(c, power)); assert.ok(!m.waiting(c));
|
||||||
let top = 999, left = false;
|
let top = 999, out = false;
|
||||||
for (let i = 0; i < 180; i++) { m.update(c, 1 / 60); top = Math.min(top, c.ball?.y ?? 999); if (c.ball && c.ball.x < m.LANE.x0 - 30) left = true; }
|
for (let i = 0; i < 180; i++) { m.update(c, 1 / 60); top = Math.min(top, c.ball?.y ?? 999); if (c.ball && c.ball.x < m.LANE.x0 - 30) out = true; }
|
||||||
assert.ok(top < 40, `è arrivata in cima (${top.toFixed(0)})`);
|
assert.ok(top < 50, `${level} forza ${power}: in cima (${top.toFixed(0)})`);
|
||||||
assert.ok(left, 'è uscita dalla corsia');
|
assert.ok(out, `${level} forza ${power}: entrata in campo`);
|
||||||
// Un lancio fiacco ricasca sulla molla e si rilancia.
|
}
|
||||||
const d = m.newFlipper(3, { level: 'bona' }); m.launch(d, 0); run(d, 3);
|
// Una pallina ricaduta nella corsia si assesta e si rilancia, senza perderla.
|
||||||
|
const d = m.newFlipper(3, { level: 'bona' }); m.launch(d, 0.5); put(d, { x: 440, y: 200, vx: 0, vy: 50 }); run(d, 3);
|
||||||
assert.ok(m.waiting(d) && d.balls === m.BALLS, 'tornata sulla molla, nessuna pallina persa');
|
assert.ok(m.waiting(d) && d.balls === m.BALLS, 'tornata sulla molla, nessuna pallina persa');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('la pallina persa: tre palline, poi il risultato; il salvataggio vale una volta sola', () => {
|
test('la pallina persa: tre palline, poi il risultato; il salvataggio vale una volta sola e non dipende dal fotogramma', () => {
|
||||||
const c = m.newFlipper(4, { level: 'seria' });
|
const c = m.newFlipper(4, { level: 'seria' });
|
||||||
for (let n = 3; n >= 1; n--) {
|
for (let n = 3; n >= 1; n--) {
|
||||||
assert.equal(c.balls, n);
|
assert.equal(c.balls, n);
|
||||||
m.launch(c, 0.9); c.saveUntil = 0; // niente salvataggio: si prova sotto
|
m.launch(c, 0.9); c.saveUntil = 0; // niente salvataggio: si prova sotto
|
||||||
c.ball.x = 320; c.ball.y = 320; c.ball.vx = 0; c.ball.vy = 200; run(c, 1);
|
put(c, { x: 320, y: 320, vx: 0, vy: 200 }); run(c, 1);
|
||||||
}
|
}
|
||||||
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'cpu'); assert.ok(!m.canThrow(c));
|
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'cpu'); assert.ok(!m.canThrow(c));
|
||||||
assert.match(m.status(c), /Ne servivano 12000/);
|
assert.match(m.status(c), /Ne servivano 10000/);
|
||||||
const d = m.newFlipper(5, { level: 'bona' });
|
const d = m.newFlipper(5, { level: 'bona' });
|
||||||
m.launch(d, 0.9); d.ball.x = 320; d.ball.y = 320; d.ball.vx = 0; d.ball.vy = 200; run(d, 1);
|
m.launch(d, 0.9); assert.ok(m.saveLeft(d) > 7); put(d, { x: 320, y: 320, vx: 0, vy: 200 }); run(d, 1);
|
||||||
assert.ok(m.waiting(d) && d.balls === 3, 'salvata: la pallina torna sulla molla');
|
assert.ok(m.waiting(d) && d.balls === 3, 'salvata: la pallina torna sulla molla');
|
||||||
assert.match(d.line ?? '', /salvata/);
|
assert.match(d.line ?? '', /salvata/);
|
||||||
m.launch(d, 0.9); d.ball.x = 320; d.ball.y = 320; d.ball.vx = 0; d.ball.vy = 200; run(d, 1);
|
m.launch(d, 0.9); assert.equal(m.saveLeft(d), 0); put(d, { x: 320, y: 320, vx: 0, vy: 200 }); run(d, 1);
|
||||||
assert.equal(d.balls, 2, 'la seconda volta è persa davvero');
|
assert.equal(d.balls, 2, 'la seconda volta è persa davvero');
|
||||||
|
// Lo stesso quarto di secondo, a passi piccoli o in un fotogramma solo: stesso esito (Codex).
|
||||||
|
for (const dts of [[1 / 240, 60], [1 / 60, 15], [0.25, 1]]) {
|
||||||
|
const e = m.newFlipper(6, { level: 'seria' }); m.launch(e, 0.8); e.sim = 2.9; e.saveUntil = 3; put(e, { x: 320, y: 344, vx: 0, vy: 100 });
|
||||||
|
for (let i = 0; i < dts[1]; i++) m.update(e, dts[0]);
|
||||||
|
assert.equal(e.balls, 3, `salvata con passi da ${dts[0]}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('il flipper alzato rimanda su la pallina; abbassato la lascia scendere', () => {
|
test('il flipper alzato rimanda su la pallina, anche a tutta velocità (niente attraversamenti); abbassato la lascia scendere', () => {
|
||||||
const up = m.newFlipper(6, { level: 'seria' }); m.launch(up, 0.9);
|
const up = m.newFlipper(6, { level: 'seria' }); m.launch(up, 0.9);
|
||||||
Object.assign(up.ball, { x: 300, y: 300, vx: 0, vy: 150 });
|
put(up, { x: 300, y: 300, vx: 0, vy: 150 }); m.hold(up, 'left', 'test', true);
|
||||||
m.hold(up, 'left', 'test', true);
|
|
||||||
let best = 0; for (let i = 0; i < 60; i++) { m.update(up, 1 / 60); best = Math.min(best, up.ball?.vy ?? 0); }
|
let best = 0; for (let i = 0; i < 60; i++) { m.update(up, 1 / 60); best = Math.min(best, up.ball?.vy ?? 0); }
|
||||||
assert.ok(best < -250, `tirata su (vy ${best.toFixed(0)})`);
|
assert.ok(best < -250, `tirata su (vy ${best.toFixed(0)})`);
|
||||||
assert.ok(up.events.some(e => e.kind === 'flipper-flip'));
|
assert.ok(up.events.some(e => e.kind === 'flipper-flip'));
|
||||||
|
// Il caso riprodotto da Codex: pallina a 980 px/s sulla punta del flipper che si alza.
|
||||||
|
const fast = m.newFlipper(6, { level: 'seria' }); m.launch(fast, 0.9);
|
||||||
|
put(fast, { x: 313.438, y: 323.71, vx: -469.837, vy: 860.031 }); m.hold(fast, 'left', 'test', true);
|
||||||
|
m.update(fast, m.STEP);
|
||||||
|
assert.ok(fast.ball.vy < 0 && fast.ball.y < 324, `respinta, non passata sotto (y ${fast.ball.y.toFixed(1)}, vy ${fast.ball.vy.toFixed(0)})`);
|
||||||
const down = m.newFlipper(6, { level: 'seria' }); m.launch(down, 0.9);
|
const down = m.newFlipper(6, { level: 'seria' }); m.launch(down, 0.9);
|
||||||
Object.assign(down.ball, { x: 300, y: 300, vx: 0, vy: 150 }); down.saveUntil = 0;
|
put(down, { x: 300, y: 300, vx: 0, vy: 150 }); down.saveUntil = 0; run(down, 2);
|
||||||
run(down, 2);
|
|
||||||
assert.equal(down.balls, 2, 'senza flipper, giù nella Sieve');
|
assert.equal(down.balls, 2, 'senza flipper, giù nella Sieve');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('funghi, bersagli, Regolo e corsie fanno punti; le quattro corsie insieme il Palio', () => {
|
test('funghi, bersagli, Regolo e corsie fanno punti solo a un urto nuovo; le quattro corsie insieme il Palio', () => {
|
||||||
const c = m.newFlipper(7, { level: 'bona' }); m.launch(c, 0.9);
|
const c = m.newFlipper(7, { level: 'bona' }); m.launch(c, 0.9);
|
||||||
const b = c.bumpers[0];
|
const b = c.bumpers[0];
|
||||||
Object.assign(c.ball, { x: b.x, y: b.y + b.r + 6, vx: 0, vy: -200 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
|
put(c, { x: b.x, y: b.y + b.r + 6, vx: 0, vy: -200 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
|
||||||
assert.equal(c.score, m.SCORE.bumper); assert.ok(c.ball.vy > 0 && b.hit > 0, 'respinta');
|
assert.equal(c.score, m.SCORE.bumper); assert.ok(c.ball.vy > 0 && b.hit > 0, 'respinta');
|
||||||
assert.ok(c.events.some(e => e.kind === 'flipper-fungo'));
|
assert.ok(c.events.some(e => e.kind === 'flipper-fungo'));
|
||||||
c.score = 0;
|
// La pallina appoggiata sul Regolo (Codex: 240.000 punti in un secondo): non conta.
|
||||||
for (const t of c.targets.filter(k => k.bank === 'pieve')) { const before = c.score; Object.assign(c.ball, { x: t.a[0] + 10, y: (t.a[1] + t.b[1]) / 2, vx: -300, vy: 0 }); m.update(c, 1 / 60); m.update(c, 1 / 60); assert.ok(c.score > before, `caduto il ${t.i}`); }
|
c.score = 0; put(c, { x: c.regolo.x, y: c.regolo.y - c.regolo.r - m.R, vx: 0, vy: 0 });
|
||||||
assert.equal(c.score, 3 * m.SCORE.target + m.SCORE.bank);
|
for (let i = 0; i < 240; i++) m.update(c, m.STEP);
|
||||||
assert.ok(c.targets.filter(k => k.bank === 'pieve').every(t => !t.down), 'la fila si rialza');
|
assert.equal(c.score, 0, 'il contatto che dura non fa punti');
|
||||||
assert.match(c.line, /Pieve/);
|
put(c, { x: c.regolo.x, y: c.regolo.y + 12, vx: 0, vy: -200 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
|
||||||
c.score = 0;
|
|
||||||
Object.assign(c.ball, { x: c.regolo.x, y: c.regolo.y + 12, vx: 0, vy: -200 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
|
|
||||||
assert.equal(c.score, m.SCORE.regolo);
|
assert.equal(c.score, m.SCORE.regolo);
|
||||||
c.score = 0;
|
c.score = 0;
|
||||||
for (const l of c.lanes) { Object.assign(c.ball, { x: l.x, y: l.y - 10, vx: 0, vy: 100 }); m.update(c, 1 / 60); m.update(c, 1 / 60); assert.ok(l.lit || c.lanes.every(k => !k.lit), l.rione); }
|
for (const t of c.targets.filter(k => k.bank === 'pieve')) { const before = c.score; put(c, { x: t.a[0] + 10, y: (t.a[1] + t.b[1]) / 2, vx: -300, vy: 0 }); m.update(c, 1 / 60); m.update(c, 1 / 60); assert.ok(c.score > before, `caduto il ${t.i}`); }
|
||||||
assert.equal(c.score, 4 * m.SCORE.lane + m.SCORE.palio);
|
assert.equal(c.score, 3 * m.SCORE.target + m.SCORE.bank);
|
||||||
|
assert.ok(c.targets.filter(k => k.bank === 'pieve').every(t => t.down), 'la fila resta giù un attimo');
|
||||||
|
put(c, { x: 260, y: 230, vx: 0, vy: 0 }); run(c, 1);
|
||||||
|
assert.ok(c.targets.filter(k => k.bank === 'pieve').every(t => !t.down), 'poi si rialza');
|
||||||
|
assert.match(c.line ?? '', /PIEVE/);
|
||||||
|
// Le corsie: contano attraversando la riga, anche fuori centro (Codex: a x=287 non si accendeva).
|
||||||
|
c.score = 0;
|
||||||
|
for (const [i, x] of [[0, 230], [1, 287], [2, 335], [3, 420]]) {
|
||||||
|
put(c, { x, y: c.lanes[i].y - 3, vx: 0, vy: 150 }); run(c, 0.1);
|
||||||
|
assert.ok(c.lanes[i].lit || c.lanes.every(k => !k.lit), c.lanes[i].rione);
|
||||||
|
}
|
||||||
|
assert.ok(c.score >= 4 * m.SCORE.lane + m.SCORE.palio, `corsie e Palio (${c.score}; in più, al massimo, un fungo sfiorato)`);
|
||||||
assert.match(c.line, /PALIO/); assert.ok(c.lanes.every(l => !l.lit), 'si rispegne per un altro giro');
|
assert.match(c.line, /PALIO/); assert.ok(c.lanes.every(l => !l.lit), 'si rispegne per un altro giro');
|
||||||
|
// In salita non conta.
|
||||||
|
put(c, { x: 287, y: c.lanes[1].y + 6, vx: 0, vy: -150 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
|
||||||
|
assert.ok(!c.lanes[1].lit);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('comandi: tenere premuto carica la molla e lasciare lancia; in gioco la metà della scena è il flipper; tasti', () => {
|
test('comandi: la molla si carica e si lancia lasciando; annullare non lancia; due dita, pulsanti e tasti', () => {
|
||||||
const c = m.newFlipper(8, { level: 'bona' });
|
const c = m.newFlipper(8, { level: 'bona' });
|
||||||
m.aimScreen(c, [440, 320]); assert.ok(m.press(c, 1)); run(c, 0.6);
|
m.aimScreen(c, [440, 320]); assert.ok(m.press(c, 1)); run(c, 0.6);
|
||||||
assert.ok(c.holding && c.power > 0.4 && c.power < 0.6, `molla a ${c.power.toFixed(2)}`);
|
assert.ok(c.holding && c.power > 0.4 && c.power < 0.6, `molla a ${c.power.toFixed(2)}`);
|
||||||
assert.ok(!m.release(c, 2), 'un altro dito non lancia');
|
assert.ok(!m.release(c, 2), 'un altro dito non lancia');
|
||||||
assert.ok(m.release(c, 1)); assert.ok(!m.waiting(c));
|
m.cancel(c, 1); assert.ok(m.waiting(c) && !c.holding, 'annullata la carica: niente lancio');
|
||||||
|
m.aimScreen(c, [440, 320]); m.press(c, 1); run(c, 0.6); assert.ok(m.release(c, 1)); assert.ok(!m.waiting(c));
|
||||||
m.aimScreen(c, [250, 200]); assert.ok(m.press(c, 3)); assert.ok(c.flippers.left.held.includes(3));
|
m.aimScreen(c, [250, 200]); assert.ok(m.press(c, 3)); assert.ok(c.flippers.left.held.includes(3));
|
||||||
m.aimScreen(c, [400, 200]); assert.ok(m.press(c, 4)); assert.ok(c.flippers.right.held.includes(4));
|
m.aimScreen(c, [400, 200]); assert.ok(m.press(c, 4)); assert.ok(c.flippers.right.held.includes(4));
|
||||||
assert.ok(m.release(c, 3)); assert.ok(!c.flippers.left.held.length && c.flippers.right.held.includes(4));
|
assert.ok(m.release(c, 3)); assert.ok(!c.flippers.left.held.length && c.flippers.right.held.includes(4));
|
||||||
assert.ok(m.cancel(c, 4)); assert.ok(!c.flippers.right.held.length);
|
m.cancel(c); assert.ok(!c.flippers.right.held.length, 'annullare tutto (pausa) lascia i flipper');
|
||||||
// I pulsanti e le frecce.
|
// I pulsanti e le frecce; il pulsante da tastiera dà un colpo breve che la fisica vede.
|
||||||
assert.ok(m.turn(c, -1, 'button')); assert.ok(c.flippers.left.held.length); m.stopTurn(c, 'button'); assert.ok(!c.flippers.left.held.length);
|
assert.ok(m.turn(c, -1, 'button')); assert.ok(c.flippers.left.held.length); assert.ok(m.stopTurn(c, 'button')); assert.ok(!c.flippers.left.held.length);
|
||||||
|
m.turn(c, 1, 'click'); m.stopTurn(c, 'click'); assert.ok(c.flippers.right.held.includes('click'));
|
||||||
|
const a0 = c.flippers.right.angle; run(c, 0.1); assert.notEqual(c.flippers.right.angle, a0, 'il flipper si è mosso'); run(c, 0.3); assert.ok(!c.flippers.right.held.length);
|
||||||
assert.ok(m.key(c, 'ArrowRight', true)); assert.ok(c.flippers.right.held.length); m.key(c, 'ArrowRight', false); assert.ok(!c.flippers.right.held.length);
|
assert.ok(m.key(c, 'ArrowRight', true)); assert.ok(c.flippers.right.held.length); m.key(c, 'ArrowRight', false); assert.ok(!c.flippers.right.held.length);
|
||||||
assert.ok(!m.key(c, 'x', true));
|
assert.ok(!m.key(c, 'x', true));
|
||||||
// Spazio: carica e lancia; Invio: lancia con la forza del cursore.
|
assert.ok(!m.setPower(c, 0.3), 'in gioco il cursore della molla non vale');
|
||||||
|
// Spazio: carica e lancia; Invio: lancia con la forza del cursore; a partita finita i tasti non sono suoi.
|
||||||
const d = m.newFlipper(9); assert.ok(m.key(d, ' ', true)); run(d, 0.3); assert.ok(d.holding); m.key(d, ' ', false); assert.ok(!m.waiting(d));
|
const d = m.newFlipper(9); assert.ok(m.key(d, ' ', true)); run(d, 0.3); assert.ok(d.holding); m.key(d, ' ', false); assert.ok(!m.waiting(d));
|
||||||
const e = m.newFlipper(10); assert.ok(m.setPower(e, 0.5)); assert.equal(m.button(e), 'Lancia'); m.key(e, 'Enter', true); assert.ok(!m.waiting(e)); assert.equal(m.button(e), 'In gioco…');
|
const e = m.newFlipper(10); assert.ok(m.setPower(e, 0.5)); assert.equal(m.button(e), 'Lancia'); m.key(e, 'Enter', true); assert.ok(!m.waiting(e)); assert.equal(m.button(e), 'In gioco…');
|
||||||
|
e.step = 'result'; e.result = { winner: 'cpu', score: 0, goal: 10000 }; assert.ok(!m.key(e, ' ', true) && !m.key(e, 'Enter', true));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il traguardo è acquisito appena raggiunto: anche uscendo prima, si vince; la trattenuta sul flipper non viene scossa', () => {
|
||||||
|
const c = m.newFlipper(11, { level: 'bona' }); m.launch(c, 0.9);
|
||||||
|
c.score = c.skill.goal - 100; put(c, { x: c.regolo.x, y: c.regolo.y + 12, vx: 0, vy: -200 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
|
||||||
|
assert.ok(c.result && c.result.winner === 'player' && c.step === 'play', 'traguardo fatto, si continua');
|
||||||
|
assert.match(m.status(c), /record/);
|
||||||
|
const g = new Game(story); g.go('via_provinciale'); settle(g);
|
||||||
|
g.act('use', 'river'); settle(g); g.choose(g.options().findIndex(o => o.text.startsWith('Una partita alla bona'))); settle(g);
|
||||||
|
g.closeup.score = 7000; g.closeup.result = { winner: 'player', score: 7000, goal: 6000 };
|
||||||
|
g.closeupClose(); settle(g);
|
||||||
|
assert.ok(g.state.flags.flipper_vinto, 'uscito dopo il traguardo: vinto');
|
||||||
|
// La pallina trattenuta sul flipper alzato resta lì (niente scossa): è una mossa del giocatore.
|
||||||
|
const h = m.newFlipper(12, { level: 'bona' }); m.launch(h, 0.9); m.hold(h, 'left', 't', true); run(h, 0.5);
|
||||||
|
put(h, { x: 296, y: 300, vx: 0, vy: 0 }); run(h, 4);
|
||||||
|
assert.ok(h.ball && !h.ball.waiting && h.ball.y > 280 && h.ball.x < 320, `ancora sul flipper (${h.ball?.x.toFixed(0)}, ${h.ball?.y.toFixed(0)})`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('giocando con attenzione, alla bona si arriva al traguardo; nessuna partita si incastra', () => {
|
test('giocando con attenzione, alla bona si arriva al traguardo; nessuna partita si incastra', () => {
|
||||||
@@ -119,7 +166,7 @@ test('giocando con attenzione, alla bona si arriva al traguardo; nessuna partita
|
|||||||
assert.equal(c.step, 'result', `seme ${seed}: finita`);
|
assert.equal(c.step, 'result', `seme ${seed}: finita`);
|
||||||
if (c.result.winner === 'player') won++;
|
if (c.result.winner === 'player') won++;
|
||||||
}
|
}
|
||||||
assert.ok(won >= 4, `vinte ${won} su 8`);
|
assert.ok(won >= 5, `vinte ${won} su 8`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('il disegno del flipper non cambia lo stato', () => {
|
test('il disegno del flipper non cambia lo stato', () => {
|
||||||
@@ -129,23 +176,22 @@ test('il disegno del flipper non cambia lo stato', () => {
|
|||||||
const draw = () => { const before = structuredClone(c); drawFlipper(ctx, c, story.closeups.flipper, bg); assert.deepEqual(c, before); };
|
const draw = () => { const before = structuredClone(c); drawFlipper(ctx, c, story.closeups.flipper, bg); assert.deepEqual(c, before); };
|
||||||
draw(); assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
|
draw(); assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
|
||||||
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'FLIPPER DEL RIVER'));
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'FLIPPER DEL RIVER'));
|
||||||
m.launch(c, 0.8); run(c, 1); draw();
|
m.launch(c, 0.8); run(c, 1); calls.length = 0; draw();
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && /^SALVATA \d s$/.test(t)), 'il salvataggio si vede');
|
||||||
c.step = 'result'; c.result = { winner: 'player', score: 7000, goal: 6000 }; calls.length = 0; draw();
|
c.step = 'result'; c.result = { winner: 'player', score: 7000, goal: 6000 }; calls.length = 0; draw();
|
||||||
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'TRAGUARDO FATTO!'));
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'TRAGUARDO FATTO!'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('al River: «Usa» o «Apri» il bar, il livello, la partita; vincendo il flag', () => {
|
test('al River: «Usa» o «Apri» il bar, il livello, la partita; uscendo senza traguardo, la battuta di chi lascia', () => {
|
||||||
const g = new Game(story); g.go('via_provinciale'); settle(g);
|
const g = new Game(story); g.go('via_provinciale'); settle(g);
|
||||||
g.act('use', 'river'); settle(g);
|
g.act('use', 'river'); settle(g);
|
||||||
assert.ok(g.options().some(o => o.text === 'Una partita seria.'));
|
assert.ok(g.options().some(o => o.text === 'Una partita seria.'));
|
||||||
g.choose(g.options().findIndex(o => o.text === 'Una partita seria.')); settle(g);
|
g.choose(g.options().findIndex(o => o.text === 'Una partita seria.')); settle(g);
|
||||||
assert.equal(g.closeup?.id, 'flipper'); assert.equal(g.closeup.level, 'seria');
|
assert.equal(g.closeup?.id, 'flipper'); assert.equal(g.closeup.level, 'seria');
|
||||||
g.closeup.step = 'result'; g.closeup.result = { winner: 'player', score: 13000, goal: 12000 };
|
g.closeupClose();
|
||||||
g.closeupClose(); settle(g);
|
assert.equal(g.queue[0]?.say, story.closeups.flipper.quit[0].say);
|
||||||
assert.ok(g.state.flags.flipper_vinto);
|
settle(g);
|
||||||
g.act('open', 'river'); settle(g);
|
g.act('open', 'river'); settle(g);
|
||||||
g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g);
|
g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g);
|
||||||
assert.equal(g.closeup?.level, 'male');
|
assert.equal(g.closeup?.level, 'male');
|
||||||
g.closeupClose();
|
|
||||||
assert.equal(g.queue[0]?.say, story.closeups.flipper.quit[0].say);
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user