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:
2026-10-31 20:24:01 +01:00
co-authored by Claude Fable 5.1
parent 7076123fcf
commit 1fc446d02a
14 changed files with 386 additions and 208 deletions
+50 -42
View File
@@ -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
// meccaniche disegnate dal gioco, nelle stesse coordinate della fisica: sponde cromate, corsie dei rioni con le luci,
// funghi, bersagli a caduta, il Regolo, sponde elastiche, flipper, molla e pallina. A sinistra, sul fianco nero del
// mobile, il quadro dei punti. Niente filtri né passate a tutto schermo (vedi risveglio.js).
import { FIELD, LANE, R, FLIP, PIVOT, SCORE, waiting, canThrow } from './flipper.js';
// meccaniche disegnate dal gioco, nelle stesse coordinate della fisica: sponde cromate (larghe quanto collidono),
// corsie dei rioni con le luci sulla riga che conta, funghi, bersagli a caduta, il Regolo, sponde elastiche, flipper,
// molla e pallina. A sinistra, sul fianco nero del mobile, il quadro dei punti. Niente filtri né passate a tutto
// schermo (vedi risveglio.js).
import { FIELD, LANE, R, PIVOT, flipperSeg, waiting, canThrow, saveLeft } from './flipper.js';
import { text, panel } from './bocce-render.js';
const COLORS = { piazza: '#d0453a', pieve: '#f2eee2', cardetole: '#3fa34d', campomigliaio: '#2f6fd0' };
const rail = (ctx, [ax, ay], [bx, by], dark = '#2a2622', light = '#c9c3b6') => {
ctx.lineCap = 'round';
ctx.strokeStyle = dark; ctx.lineWidth = 4; 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 = dark; ctx.lineWidth = 3; 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) {
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();
}
function bumper(ctx, b, time) {
const k = b.hit > 0 ? 1 + b.hit * 1.2 : 1, r = b.r * k;
function bumper(ctx, b) {
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();
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');
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) {
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.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 = '#3a2a1a'; ctx.lineWidth = FLIP.r * 2 + 2; 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 = '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 = 7; 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();
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');
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.
function board(ctx, c, def) {
panel(ctx, 22, 60, 160, 150);
text(ctx, 'FLIPPER DEL RIVER', 102, 76, 7, '#efd49e', 'center');
text(ctx, String(c.step === 'result' ? c.result.score : c.score).padStart(6, '0'), 102, 100, 14, '#fff4c8', 'center');
text(ctx, 'PUNTI', 102, 116, 5, '#a9a08a', 'center');
text(ctx, `PALLINE ${c.step === 'result' ? 0 : c.balls}`, 34, 136, 6, '#d7ccb4');
text(ctx, `TRAGUARDO ${c.skill.goal}`, 34, 150, 6, '#d7ccb4');
text(ctx, 'RIONI', 34, 168, 6, '#d7ccb4');
c.lanes.forEach((l, i) => disc(ctx, 80 + i * 20, 168, 6, l.lit ? COLORS[l.rione] : '#3a3630', l.lit ? '#fff4c8' : '#5a5650'));
text(ctx, (def.names?.player ?? 'Andrea').toUpperCase(), 34, 190, 6, '#efd49e');
if (c.line) text(ctx, c.line.toUpperCase(), 102, 203, 5, '#ffe08c', 'center');
// Il quadro dei punti sul fianco del mobile: punti, palline, i rioni accesi, il traguardo, il salvataggio, l'avviso.
function board(ctx, c) {
panel(ctx, 16, 56, 172, 168);
text(ctx, 'FLIPPER DEL RIVER', 102, 72, 7, '#efd49e', 'center');
text(ctx, String(c.score).padStart(6, '0'), 102, 98, 16, '#fff4c8', 'center');
text(ctx, `PALLINE ${c.step === 'result' ? 0 : c.balls}`, 28, 124, 7, '#d7ccb4');
text(ctx, c.result ? 'TRAGUARDO FATTO' : `TRAGUARDO ${c.skill.goal}`, 28, 140, 7, c.result ? '#9fd27a' : '#d7ccb4');
text(ctx, 'RIONI', 28, 160, 7, '#d7ccb4');
c.lanes.forEach((l, i) => disc(ctx, 86 + i * 22, 160, 7, l.lit ? COLORS[l.rione] : '#3a3630', l.lit ? '#fff4c8' : '#5a5650'));
const save = saveLeft(c);
if (save > 0) text(ctx, `SALVATA ${Math.ceil(save)} s`, 28, 180, 7, '#9fd27a');
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) {
panel(ctx, 20, 230, 164, 56);
text(ctx, c.result.winner === 'player' ? 'TRAGUARDO FATTO!' : 'FINE PARTITA', 102, 246, 7, '#efd49e', 'center');
text(ctx, `${c.result.score} su ${c.result.goal}`, 102, 266, 7, c.result.winner === 'player' ? '#9fd27a' : '#e0b07a', 'center');
panel(ctx, 16, 236, 172, 56);
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, 272, 8, c.result.winner === 'player' ? '#9fd27a' : '#e0b07a', 'center');
}
export function drawFlipper(ctx, c, def, bg) {
@@ -70,32 +81,29 @@ export function drawFlipper(ctx, c, def, bg) {
ctx.save();
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); }
// Le luci delle corsie dei rioni, 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)');
// Le sponde: tutte le pareti della fisica, cromate; le elastiche con la gomma rossa.
for (const w of c.walls) {
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);
// Le luci delle corsie dei rioni, sulla riga che conta, sotto le bandiere.
for (const l of c.lanes) {
ctx.fillStyle = 'rgba(0,0,0,0.35)'; ctx.fillRect(l.x - 7, l.y - 3, 14, 7);
disc(ctx, l.x, l.y, 6, l.lit ? COLORS[l.rione] : '#2a2622', l.lit ? '#fff4c8' : '#6a6050');
}
// 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) {
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; }
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 = 'rgba(0,0,0,0.25)'; ctx.fillRect(x - 2, y1 - 3, 4, 3);
}
for (const b of c.bumpers) bumper(ctx, b, time);
// Il Regolo: il bersaglio verde con la corona, che brilla quando è colpito.
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 b of c.bumpers) bumper(ctx, b);
regolo(ctx, c.regolo);
for (const side of ['left', 'right']) flipper(ctx, c, side);
plunger(ctx, c);
ball(ctx, c.ball);
// 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');
board(ctx, c, def);
board(ctx, c);
if (c.step === 'result') result(ctx, c);
ctx.restore();
}
export { SCORE };