Il flipper dopo la seconda revisione di Codex: flipper vivi anche sul ponte, imbocco solo in salita, ponti con la stessa energia su e giù e uscita nelle corsie di ritorno, combo che scade, scritte in coda col traguardo davanti, tabellone con l'esito giusto e le cifre che corrono in tempo fisso, tasti del mouse dalla maschera, glifi rifatti
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
// ponti (le rampe), flipper, molla e pallina. A sinistra, sul fianco nero del mobile, il tabellone luminoso a punti
|
||||
// come sui flipper degli anni 2000 (Andrea): le cifre che corrono, le scritte che lampeggiano, le lampadine che
|
||||
// girano intorno. Niente filtri né passate a tutto schermo (vedi risveglio.js).
|
||||
import { FIELD, LANE, R, flipperSeg, alongRamp, waiting, canThrow, saveLeft } from './flipper.js';
|
||||
import { FIELD, LANE, R, flipperSeg, alongRamp, rampHeight, waiting, canThrow, saveLeft } from './flipper.js';
|
||||
import { text, panel } from './bocce-render.js';
|
||||
|
||||
const COLORS = { piazza: '#d0453a', pieve: '#f2eee2', cardetole: '#3fa34d', campomigliaio: '#2f6fd0' };
|
||||
@@ -34,23 +34,27 @@ function regolo(ctx, o) {
|
||||
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);
|
||||
}
|
||||
// I ponti: una striscia di pietra lungo il percorso, col bordo scuro e la luce in cima; accesi, brillano d'oro.
|
||||
function ramp(ctx, r, time) {
|
||||
const path = () => { ctx.beginPath(); r.path.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y)); };
|
||||
// I ponti: una striscia di pietra lungo il percorso (il sinistro caldo, il destro freddo, per seguirli), un po'
|
||||
// trasparente perché si veda la tavola sotto, più alta in cima (la quota), col bordo scuro e la luce; accesi, brillano.
|
||||
const RAMP_LOOK = { sinistra: ['#c9b89a', '#8a7a5a'], destra: ['#b8c0c9', '#6a7a8a'] };
|
||||
function rampPoint(r, s) { const p = alongRamp(r, s); return { ...p, y: p.y - 2 - 4 * rampHeight(r, s) }; }
|
||||
function ramp(ctx, r, time, who) {
|
||||
const path = () => { ctx.beginPath(); for (let s = 0; s <= r.len; s += 4) { const p = rampPoint(r, Math.min(s, r.len)); s ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y); } };
|
||||
const [light, dark] = RAMP_LOOK[r.id], invite = who && who !== r.id && Math.sin(time * 6) > 0;
|
||||
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0.35)'; ctx.lineWidth = 14; path(); ctx.stroke();
|
||||
ctx.translate(0, -3);
|
||||
ctx.strokeStyle = r.lit > 0 && Math.sin(time * 14) > 0 ? '#ffd45a' : '#3a3028'; ctx.lineWidth = 13; path(); ctx.stroke();
|
||||
ctx.strokeStyle = '#9a9088'; ctx.lineWidth = 9; path(); ctx.stroke();
|
||||
ctx.strokeStyle = '#c9c0b0'; ctx.lineWidth = 7; path(); ctx.stroke();
|
||||
ctx.globalAlpha = 0.5; ctx.strokeStyle = '#000'; ctx.lineWidth = 12; ctx.beginPath(); r.path.forEach(([x, y], i) => i ? ctx.lineTo(x, y + 4) : ctx.moveTo(x, y + 4)); ctx.stroke();
|
||||
ctx.globalAlpha = 0.8;
|
||||
ctx.strokeStyle = r.lit > 0 && Math.sin(time * 14) > 0 ? '#ffd45a' : invite ? '#ffe9a8' : '#2a2420'; ctx.lineWidth = 12; path(); ctx.stroke();
|
||||
ctx.strokeStyle = dark; ctx.lineWidth = 9; path(); ctx.stroke();
|
||||
ctx.strokeStyle = light; ctx.lineWidth = 6; path(); ctx.stroke();
|
||||
// Le pietre: trattini scuri a passo regolare lungo il ponte.
|
||||
ctx.strokeStyle = 'rgba(60,50,40,0.5)'; ctx.lineWidth = 1;
|
||||
for (let s = 6; s < r.len; s += 9) { const p = alongRamp(r, s); ctx.beginPath(); ctx.moveTo(p.x - p.dy * 3, p.y + p.dx * 3); ctx.lineTo(p.x + p.dy * 3, p.y - p.dx * 3); ctx.stroke(); }
|
||||
ctx.translate(0, 3);
|
||||
ctx.strokeStyle = 'rgba(60,50,40,0.45)'; ctx.lineWidth = 1;
|
||||
for (let s = 6; s < r.len; s += 9) { const p = rampPoint(r, s); ctx.beginPath(); ctx.moveTo(p.x - p.dy * 3, p.y + p.dx * 3); ctx.lineTo(p.x + p.dy * 3, p.y - p.dx * 3); ctx.stroke(); }
|
||||
ctx.globalAlpha = 1;
|
||||
// L'imbocco: la rampetta che sale, con la freccia.
|
||||
const [[x0, y], [x1]] = r.mouth;
|
||||
ctx.fillStyle = '#6a5a4a'; ctx.fillRect(x0, y - 2, x1 - x0, 6);
|
||||
ctx.fillStyle = '#ffe08c'; ctx.beginPath(); ctx.moveTo((x0 + x1) / 2, y - 1); ctx.lineTo((x0 + x1) / 2 - 3, y + 3); ctx.lineTo((x0 + x1) / 2 + 3, y + 3); ctx.closePath(); ctx.fill();
|
||||
ctx.fillStyle = invite ? '#fff6c8' : '#ffe08c'; ctx.beginPath(); ctx.moveTo((x0 + x1) / 2, y - 1); ctx.lineTo((x0 + x1) / 2 - 3, y + 3); ctx.lineTo((x0 + x1) / 2 + 3, y + 3); ctx.closePath(); ctx.fill();
|
||||
}
|
||||
function flipper(ctx, c, side) {
|
||||
const f = c.flippers[side], { a: [px, py], b: [tx, ty] } = flipperSeg(c, side);
|
||||
@@ -70,19 +74,19 @@ function plunger(ctx, c) {
|
||||
}
|
||||
function ball(ctx, b) {
|
||||
if (!b) return;
|
||||
// Sul ponte la pallina è più in alto: più grande, con l'ombra più lontana.
|
||||
const r = b.ramp ? R + 1.5 : R, sh = b.ramp ? 6 : 3;
|
||||
// Sul ponte la pallina sta più in alto, a seconda della quota: più su, più grande, con l'ombra più lontana.
|
||||
const z = b.ramp?.z ?? 0, r = R + 1.5 * z, y = b.y - 2 - 4 * z, sh = 3 + 4 * z;
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.35)'; ctx.beginPath(); ctx.arc(b.x + sh * 0.7, b.y + sh, r, 0, Math.PI * 2); ctx.fill();
|
||||
const g = ctx.createRadialGradient(b.x - 1.5, b.y - 1.5, 0.5, b.x, b.y, r);
|
||||
const g = ctx.createRadialGradient(b.x - 1.5, y - 1.5, 0.5, b.x, y, r);
|
||||
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, y, r, 0, Math.PI * 2); ctx.fill();
|
||||
}
|
||||
|
||||
// --- Il tabellone a punti luminosi: un carattere 5×7, cifre e lettere.
|
||||
const GLYPHS = {
|
||||
'0': ['01110', '10001', '10011', '10101', '11001', '10001', '01110'], '1': ['00100', '01100', '00100', '00100', '00100', '00100', '01110'],
|
||||
'2': ['01110', '10001', '00001', '00010', '00100', '01000', '11111'], '3': ['11111', '00010', '00100', '00010', '00001', '10001', '01110'],
|
||||
'4': ['00010', '00110', '01010', '10010', '11111', '00010', '00010'], '5': ['11111', '10000', '11110', '00001', '00001', '10001', '01110'],
|
||||
'2': ['01110', '10001', '00001', '00010', '00100', '01000', '11111'], '3': ['11110', '00001', '00001', '01110', '00001', '00001', '11110'],
|
||||
'4': ['00010', '00110', '01010', '10010', '11111', '00010', '00010'], '5': ['11111', '10000', '10000', '11110', '00001', '10001', '01110'],
|
||||
'6': ['00110', '01000', '10000', '11110', '10001', '10001', '01110'], '7': ['11111', '00001', '00010', '00100', '01000', '01000', '01000'],
|
||||
'8': ['01110', '10001', '10001', '01110', '10001', '10001', '01110'], '9': ['01110', '10001', '10001', '01111', '00001', '00010', '01100'],
|
||||
A: ['01110', '10001', '10001', '11111', '10001', '10001', '10001'], B: ['11110', '10001', '10001', '11110', '10001', '10001', '11110'],
|
||||
@@ -101,6 +105,7 @@ const GLYPHS = {
|
||||
'!': ['00100', '00100', '00100', '00100', '00100', '00000', '00100'], ':': ['00000', '00100', '00000', '00000', '00000', '00100', '00000'],
|
||||
',': ['00000', '00000', '00000', '00000', '00110', '00100', '01000'], '-': ['00000', '00000', '00000', '11111', '00000', '00000', '00000'],
|
||||
'.': ['00000', '00000', '00000', '00000', '00000', '00110', '00110'], ' ': ['00000', '00000', '00000', '00000', '00000', '00000', '00000'],
|
||||
'+': ['00000', '00100', '00100', '11111', '00100', '00100', '00000'], '/': ['00001', '00010', '00010', '00100', '01000', '01000', '10000'],
|
||||
};
|
||||
// Una scritta a punti: `pitch` pixel per punto, i punti spenti appena visibili, quelli accesi col colore.
|
||||
function dots(ctx, line, x, y, pitch, on, off = 'rgba(255,170,60,0.12)', align = 'left') {
|
||||
@@ -119,44 +124,51 @@ function dots(ctx, line, x, y, pitch, on, off = 'rgba(255,170,60,0.12)', align =
|
||||
const shown = new WeakMap();
|
||||
function board(ctx, c, time) {
|
||||
let d = shown.get(c);
|
||||
if (!d) { d = { score: c.score, t: time, flash: 0, line: null }; shown.set(c, d); }
|
||||
const dt = Math.min(0.1, Math.max(0, time - d.t)); d.t = time;
|
||||
// Le cifre corrono: in mezzo secondo raggiungono i punti veri, a scatti di almeno 10.
|
||||
if (d.score < c.score) d.score = Math.min(c.score, d.score + Math.max(10, Math.ceil((c.score - d.score) * dt * 6)));
|
||||
if (d.score > c.score) d.score = c.score;
|
||||
if (c.line && c.line !== d.line) { d.line = c.line; d.flash = time; }
|
||||
if (!c.line) d.line = null;
|
||||
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
|
||||
// subito il totale.
|
||||
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, che girano a tempo (e impazziscono alle scritte grosse).
|
||||
const beat = time * (d.flash && time - d.flash < 2 ? 12 : 3);
|
||||
// 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);
|
||||
let n = 0;
|
||||
const lamp = (x, y) => { const k = (n++ + Math.floor(beat)) % 3; ctx.fillStyle = k === 0 ? '#ffd45a' : k === 1 ? '#f0a24a' : '#6a3a1e'; ctx.fillRect(x - 1, y - 1, 3, 3); };
|
||||
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.
|
||||
const digits = String(Math.round(d.score)).padStart(7, '0'), hot = d.score < c.score;
|
||||
// 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 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 + 30, 4, hot ? '#fff0a0' : '#ffb13a', 'rgba(255,170,60,0.1)', 'center');
|
||||
// La scritta: lampeggia per due secondi, poi resta; altrimenti palline e traguardo.
|
||||
const flashing = d.line && time - d.flash < 2, visible = !flashing || Math.sin(time * 16) > -0.2;
|
||||
const line = d.line ?? (c.result ? 'TRAGUARDO OK' : `TRAG. ${c.skill.goal}`);
|
||||
if (visible) dots(ctx, line.slice(0, 12), x0 + w / 2, y0 + 68, 2, flashing ? '#fff6c8' : '#f0a24a', 'rgba(255,170,60,0.08)', 'center');
|
||||
dots(ctx, `PALLINE ${c.step === 'result' ? 0 : c.balls}`, x0 + 10, y0 + 88, 2, '#f0a24a');
|
||||
// Il salvataggio: quanti secondi restano, o se è già stato usato.
|
||||
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.
|
||||
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');
|
||||
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');
|
||||
const save = saveLeft(c);
|
||||
if (save > 0) text(ctx, `PALLINA SALVATA PER ${Math.ceil(save)} s`, x0 + 10, y0 + 108, 6, '#9fd27a');
|
||||
else if (c.ball && !c.ball.waiting && c.saved) text(ctx, 'SENZA RETE', x0 + 10, y0 + 108, 6, '#a08a60');
|
||||
// I rioni accesi, e i ponti fatti.
|
||||
dots(ctx, 'RIONI', x0 + 10, y0 + 120, 2, '#f0a24a');
|
||||
c.lanes.forEach((l, i) => disc(ctx, x0 + 86 + i * 22, y0 + 127, 7, l.lit ? COLORS[l.rione] : '#2a2420', l.lit ? '#fff4c8' : '#5a5040'));
|
||||
dots(ctx, 'PONTI', x0 + 10, y0 + 142, 2, '#f0a24a');
|
||||
c.ramps.forEach((r, i) => disc(ctx, x0 + 86 + i * 22, y0 + 149, 7, r.lit > 0 || c.lastRamp?.id === r.id ? '#ffd45a' : '#2a2420', r.lit > 0 ? '#fff4c8' : '#5a5040'));
|
||||
text(ctx, 'ANDREA', x0 + 10, y0 + 168, 6, '#a9a08a');
|
||||
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');
|
||||
}
|
||||
// I punti mostrati dal tabellone, che corrono da `from` a `to` in mezzo secondo.
|
||||
export function shownScore(d, time) {
|
||||
const k = Math.min(1, (time - d.t0) / 0.5);
|
||||
return k >= 1 ? d.to : d.from + Math.floor((d.to - d.from) * k / 10) * 10;
|
||||
}
|
||||
function result(ctx, c) {
|
||||
panel(ctx, 16, 238, 172, 56);
|
||||
@@ -190,7 +202,7 @@ export function drawFlipper(ctx, c, def, bg) {
|
||||
plunger(ctx, c);
|
||||
// La pallina sotto i ponti, se è sulla tavola; sopra, se è su un ponte.
|
||||
if (!c.ball?.ramp) ball(ctx, c.ball);
|
||||
for (const r of c.ramps) ramp(ctx, r, time);
|
||||
for (const r of c.ramps) ramp(ctx, r, time, c.lastRamp?.id);
|
||||
if (c.ball?.ramp) 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');
|
||||
|
||||
Reference in New Issue
Block a user