Il tabellone del flipper è un vero backglass: il fondale di Codex col Castello del Trebbio, la Fortezza, il Regolo e i fuochi dei rioni; le cifre nella finestra nera, le lampadine sui bordi, i dischi delle palline e dei rioni, i ponti ai lati, il fondale che lampeggia ai colpi grossi (Andrea)

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 22:10:55 +01:00
co-authored by Claude Fable 5.1
parent 4d4aa8aeea
commit 3079c6ea8a
6 changed files with 53 additions and 42 deletions
+42 -39
View File
@@ -158,7 +158,11 @@ function dots(ctx, line, x, y, pitch, on, off = 'rgba(255,170,60,0.12)', align =
// Il tabellone ricorda i punti mostrati (che corrono dietro a quelli veri) e la scritta che lampeggia: stato di sola
// presentazione, fuori dalla partita (così disegnare non cambia niente).
const shown = new WeakMap();
function board(ctx, c, time) {
// Il tabellone è il fondale luminoso di Codex (il «backglass»: il Trebbio, la Fortezza, il Regolo, i fuochi) sul
// fianco sinistro, 200×360: le cifre nella finestra nera, le lampadine sui bordi che girano, i cinque dischi in basso
// per le palline e i rioni, i ponti ai lati della finestra. Senza il fondale (tests), un pannello scuro.
const GLASS = { window: [20, 78, 160, 46], sockets: [[23, 329], [67, 329], [100, 329], [133, 329], [169, 329]], lampX: [8, 192], lampY0: 16, lampStep: 20 };
function board(ctx, c, time, glass) {
let d = shown.get(c);
if (!d) { d = { from: c.score, to: c.score, t0: time, flash: 0, lineId: 0 }; shown.set(c, d); }
// Le cifre corrono: da dove erano a dove devono arrivare, in mezzo secondo, a scatti di dieci; a partita finita
@@ -166,55 +170,54 @@ function board(ctx, c, time) {
if (d.to !== c.score) { d.from = shownScore(d, time); d.to = c.score; d.t0 = time; }
const score = c.step === 'result' ? c.score : shownScore(d, time), hot = score !== c.score;
if (c.lineId !== d.lineId) { d.lineId = c.lineId; d.flash = time; }
const x0 = 16, y0 = 50, w = 172, h = 180;
ctx.fillStyle = '#0c0806'; ctx.fillRect(x0, y0, w, h);
ctx.strokeStyle = '#5a4a30'; ctx.lineWidth = 2; ctx.strokeRect(x0 + 1, y0 + 1, w - 2, h - 2);
// Le lampadine intorno: tranquille di solito, impazzite per due secondi a ogni scritta.
const party = c.line && time - d.flash < 2, beat = time * (party ? 14 : 2);
const party = c.line && time - d.flash < 2, [wx, wy, ww, wh] = GLASS.window;
if (glass) ctx.drawImage(glass, 0, 0, 200, 360);
else { ctx.fillStyle = '#0c0806'; ctx.fillRect(0, 0, 200, 360); ctx.strokeStyle = '#5a4a30'; ctx.lineWidth = 2; ctx.strokeRect(wx - 3, wy - 3, ww + 6, wh + 6); }
// Alla festa il fondale lampeggia (un lampo breve, solo sul fianco: una passata piccola).
if (party && Math.sin(time * 12) > 0.6) { ctx.globalAlpha = 0.25; ctx.fillStyle = '#fff6c8'; ctx.fillRect(0, 0, 200, 360); ctx.globalAlpha = 1; }
// Le lampadine sui bordi: tranquille di solito, impazzite per due secondi a ogni scritta.
const beat = time * (party ? 14 : 2);
let n = 0;
const lamp = (x, y) => { const k = (n++ + Math.floor(beat)) % 3; ctx.fillStyle = k === 0 ? '#ffd45a' : k === 1 ? (party ? '#ff5a3a' : '#c8842a') : '#5a3a1e'; ctx.fillRect(x - 1, y - 1, 3, 3); };
for (let x = x0 + 6; x < x0 + w - 4; x += 8) lamp(x, y0 + 4);
for (let y = y0 + 12; y < y0 + h - 4; y += 8) lamp(x0 + w - 5, y);
for (let x = x0 + w - 10; x > x0 + 4; x -= 8) lamp(x, y0 + h - 5);
for (let y = y0 + h - 12; y > y0 + 4; y -= 8) lamp(x0 + 4, y);
text(ctx, 'FLIPPER DEL RIVER', x0 + w / 2, y0 + 16, 7, '#efd49e', 'center');
// I punti, a cifre grosse color ambra col bagliore caldo dietro: sei cifre larghe, da un milione in su più fitte.
ctx.save(); ctx.beginPath(); ctx.rect(x0 + 8, y0 + 24, w - 16, h - 32); ctx.clip();
const lamp = (x, y) => { const k = (n++ + Math.floor(beat)) % 3; ctx.fillStyle = k === 0 ? '#ffd45a' : k === 1 ? (party ? '#ff5a3a' : '#c8842a') : '#5a3a1e'; ctx.fillRect(x - 2, y - 2, 4, 4); };
for (let y = GLASS.lampY0; y < 300; y += GLASS.lampStep) lamp(GLASS.lampX[0], y);
for (let y = 300 - ((300 - GLASS.lampY0) % GLASS.lampStep); y >= GLASS.lampY0; y -= GLASS.lampStep) lamp(GLASS.lampX[1], y);
// La finestra: le cifre grosse color ambra col bagliore caldo dietro (sei cifre larghe, da un milione in su più
// fitte) e sotto la scritta: l'ultimo evento (lampeggia per due secondi), se no l'esito o il traguardo.
ctx.save(); ctx.beginPath(); ctx.rect(wx, wy, ww, wh); ctx.clip();
ctx.fillStyle = '#000'; ctx.fillRect(wx, wy, ww, wh);
const digits = String(Math.round(score)).padStart(6, '0'), pitch = digits.length > 6 ? 3 : 4;
ctx.fillStyle = hot ? 'rgba(255,120,30,0.22)' : 'rgba(255,120,30,0.12)'; ctx.fillRect(x0 + 10, y0 + 26, w - 20, 34);
dots(ctx, digits, x0 + w / 2, y0 + 29 + (4 - pitch) * 3, pitch, hot ? '#fff0a0' : '#ffb13a', 'rgba(255,170,60,0.1)', 'center');
// La scritta grossa: l'ultimo evento (lampeggia per due secondi), se no l'esito o il traguardo.
ctx.fillStyle = hot ? 'rgba(255,120,30,0.25)' : 'rgba(255,120,30,0.12)'; ctx.fillRect(wx + 2, wy + 1, ww - 4, 29);
dots(ctx, digits, wx + ww / 2, wy + 1 + (4 - pitch) * 3, pitch, hot ? '#fff0a0' : '#ffb13a', 'rgba(255,170,60,0.1)', 'center');
const won = c.result?.winner === 'player';
const line = c.step === 'result' ? (won ? 'TRAGUARDO!' : 'FINITA') : c.line ?? (won ? 'TRAGUARDO!' : `TRAG. ${c.skill.goal}`);
const flashing = c.line && c.step !== 'result' && time - d.flash < 2, visible = !flashing || Math.sin(time * 14) > -0.3;
if (visible) dots(ctx, line.slice(0, 12), x0 + w / 2, y0 + 68, line.length <= 8 ? 3 : 2, flashing ? '#fff6c8' : '#f0a24a', 'rgba(255,170,60,0.08)', 'center');
if (flashing) regia(ctx, c.line, time - d.flash, x0, y0, w);
if (visible) dots(ctx, line.slice(0, 12), wx + ww / 2, wy + 31, 2, flashing ? '#fff6c8' : '#f0a24a', 'rgba(255,170,60,0.08)', 'center');
if (flashing) regia(ctx, c.line, time - d.flash, wx, wy, ww, wh);
ctx.restore();
// Palline, salvataggio, rioni e ponti: in chiaro, leggibili anche sul telefono.
text(ctx, `PALLINE ${c.step === 'result' ? 0 : c.balls}`, x0 + 10, y0 + 102, 7, '#f0a24a');
// I ponti: due lampade ai lati della finestra (accese se fatte; quella da fare lampeggia).
c.ramps.forEach((r, i) => disc(ctx, i ? wx + ww + 8 : wx - 8, wy + wh / 2, 6, r.lit > 0 || c.lastRamp?.id === r.id ? '#ffd45a' : c.lastRamp && Math.sin(time * 6) > 0 ? '#fff6c8' : '#2a2420', '#8a7a5a'));
// I cinque dischi in basso: le palline che restano, e i quattro rioni accesi.
const [[bx, by], ...rioni] = GLASS.sockets;
disc(ctx, bx, by, 11, '#2a2420', '#8a7a5a'); text(ctx, String(c.step === 'result' ? 0 : c.balls), bx, by + 1, 9, '#ffd45a', 'center');
rioni.forEach(([x, y], i) => { const l = c.lanes[i]; disc(ctx, x, y, 11, l.lit ? COLORS[l.rione] : '#2a2420', l.lit ? '#fff4c8' : '#8a7a5a'); });
// Sotto, in piccolo: il salvataggio o la combo.
const save = saveLeft(c);
if (save > 0) text(ctx, `SALVA ${Math.ceil(save)} s`, x0 + w - 10, y0 + 102, 7, '#9fd27a', 'right');
else if (c.ball && !c.ball.waiting && c.saved) text(ctx, 'SENZA RETE', x0 + w - 10, y0 + 102, 6, '#a08a60', 'right');
text(ctx, 'RIONI', x0 + 10, y0 + 124, 7, '#f0a24a');
c.lanes.forEach((l, i) => disc(ctx, x0 + 86 + i * 22, y0 + 124, 7, l.lit ? COLORS[l.rione] : '#2a2420', l.lit ? '#fff4c8' : '#5a5040'));
text(ctx, 'PONTI', x0 + 10, y0 + 146, 7, '#f0a24a');
c.ramps.forEach((r, i) => disc(ctx, x0 + 86 + i * 22, y0 + 146, 7, r.lit > 0 || c.lastRamp?.id === r.id ? '#ffd45a' : '#2a2420', r.lit > 0 ? '#fff4c8' : '#5a5040'));
if (c.lastRamp) text(ctx, `L'ALTRO ENTRO ${Math.ceil(12 - (c.sim - c.lastRamp.at))} s`, x0 + w - 10, y0 + 164, 6, '#ffe08c', 'right');
else text(ctx, 'ANDREA', x0 + 10, y0 + 164, 6, '#a9a08a');
const note = save > 0 ? `PALLINA SALVA ${Math.ceil(save)} s` : c.lastRamp ? `L'ALTRO PONTE ENTRO ${Math.ceil(12 - (c.sim - c.lastRamp.at))} s` : c.ball && !c.ball.waiting && c.saved ? 'SENZA RETE' : '';
if (note) text(ctx, note, 100, 352, 6, save > 0 ? '#9fd27a' : '#ffe08c', 'center');
}
// La piccola regia del tabellone ai colpi grossi: al traguardo la corona del Regolo che si compone a colonne, al
// Palio le quattro bandiere dei rioni che sfilano, ai ponti due archi di punti che si uniscono.
const CROWN = ['10001000100010001', '11011101110111011', '11111111111111111', '01111111111111110', '01111111111111110'];
function regia(ctx, line, age, x0, y0, w) {
const cx = x0 + w / 2;
function regia(ctx, line, age, wx, wy, ww, wh) {
const cx = wx + ww / 2;
if (line === 'TRAGUARDO!') {
const cols = Math.min(17, Math.floor(age * 12));
for (let row = 0; row < 5; row++) for (let col = 0; col < cols; col++) if (CROWN[row][col] === '1') { ctx.fillStyle = row < 2 ? '#fff6c8' : '#ffd45a'; ctx.fillRect(cx - 34 + col * 4, y0 + 24 + row * 4, 3, 3); }
for (let row = 0; row < 5; row++) for (let col = 0; col < cols; col++) if (CROWN[row][col] === '1') { ctx.fillStyle = row < 2 ? '#fff6c8' : '#ffd45a'; ctx.fillRect(cx - 34 + col * 4, wy + 2 + row * 3, 3, 2); }
} else if (line === 'PALIO!') {
['#d0453a', '#f2eee2', '#3fa34d', '#2f6fd0'].forEach((color, i) => { const x = x0 + 10 + ((age * 140 + i * 40) % (w - 20)); ctx.fillStyle = color; ctx.fillRect(Math.round(x), y0 + 60, 8, 6); ctx.fillRect(Math.round(x), y0 + 88, 8, 6); });
['#d0453a', '#f2eee2', '#3fa34d', '#2f6fd0'].forEach((color, i) => { const x = wx + 2 + ((age * 140 + i * 40) % (ww - 4)); ctx.fillStyle = color; ctx.fillRect(Math.round(x), wy + 1, 8, 3); ctx.fillRect(Math.round(x), wy + wh - 4, 8, 3); });
} else if (line === 'I PONTI!') {
const k = Math.min(1, age / 0.8);
for (let i = 0; i <= 16 * k; i++) { const a = Math.PI + i / 16 * Math.PI; ctx.fillStyle = i % 2 ? '#ffd45a' : '#fff6c8'; ctx.fillRect(Math.round(cx - 60 + Math.cos(a) * 30 + 30), Math.round(y0 + 94 + Math.sin(a) * 10), 3, 3); ctx.fillRect(Math.round(cx + 60 - Math.cos(a) * 30 - 30), Math.round(y0 + 94 + Math.sin(a) * 10), 3, 3); }
for (let i = 0; i <= 16 * k; i++) { const a = Math.PI + i / 16 * Math.PI; ctx.fillStyle = i % 2 ? '#ffd45a' : '#fff6c8'; ctx.fillRect(Math.round(cx - 60 + Math.cos(a) * 30 + 30), Math.round(wy + wh - 3 + Math.sin(a) * 6), 3, 2); ctx.fillRect(Math.round(cx + 60 - Math.cos(a) * 30 - 30), Math.round(wy + wh - 3 + Math.sin(a) * 6), 3, 2); }
}
}
// I punti mostrati dal tabellone, che corrono da `from` a `to` in mezzo secondo.
@@ -280,12 +283,12 @@ function trail(ctx, c) {
ctx.globalAlpha = 1;
}
function result(ctx, c) {
panel(ctx, 16, 238, 172, 56);
text(ctx, c.result.winner === 'player' ? 'TRAGUARDO FATTO!' : 'FINE PARTITA', 102, 254, 8, '#efd49e', 'center');
text(ctx, `${c.result.score} su ${c.result.goal}`, 102, 274, 8, c.result.winner === 'player' ? '#9fd27a' : '#e0b07a', 'center');
panel(ctx, 14, 236, 172, 56);
text(ctx, c.result.winner === 'player' ? 'TRAGUARDO FATTO!' : 'FINE PARTITA', 100, 252, 8, '#efd49e', 'center');
text(ctx, `${c.result.score} su ${c.result.goal}`, 100, 272, 8, c.result.winner === 'player' ? '#9fd27a' : '#e0b07a', 'center');
}
export function drawFlipper(ctx, c, def, bg) {
export function drawFlipper(ctx, c, def, bg, glass) {
const time = c.visual.time, [sx, sy] = shake(c, time);
ctx.save();
if (sx || sy) { ctx.fillStyle = '#0c0806'; ctx.fillRect(0, 0, 640, 360); ctx.translate(sx, sy); }
@@ -319,7 +322,7 @@ export function drawFlipper(ctx, c, def, bg) {
drawFx(ctx, c, time);
// 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, time);
board(ctx, c, time, glass);
if (c.step === 'result') result(ctx, c);
ctx.restore();
}