From dfa70a9620aee17711c321cf35a9ce32094f9316 Mon Sep 17 00:00:00 2001 From: Andrea Manzi Date: Sat, 31 Oct 2026 22:52:53 +0100 Subject: [PATCH] Il Boccino (Gabriele Chiesi, consenso del 31 ottobre) corre il GP di Fortezza col suo NRG bianco e rosso numero 1 (motorino e ritratto di Codex dalle sue foto) (Andrea) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T --- assets/closeups/gp/boccino.png | Bin 0 -> 11777 bytes assets/closeups/gp/nrg.png | Bin 0 -> 26516 bytes docs/STATO.md | 6 +- index.html | 3 +- src/engine/flipper-render.js | 153 +++++++++++++++++++++++++-------- src/engine/flipper.js | 12 +-- src/story/piloti.js | 3 + tests/flipper.browser.mjs | 4 + tests/flipper.test.mjs | 35 ++++++-- 9 files changed, 163 insertions(+), 53 deletions(-) create mode 100644 assets/closeups/gp/boccino.png create mode 100644 assets/closeups/gp/nrg.png diff --git a/assets/closeups/gp/boccino.png b/assets/closeups/gp/boccino.png new file mode 100644 index 0000000000000000000000000000000000000000..00b12dac531da5cbf9e8b20b29d9b0ccf3181365 GIT binary patch literal 11777 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 j^8HGmg_YXZNO%Ra$00000NkvXXu0mjfB@)Q1 literal 0 HcmV?d00001 diff --git a/assets/closeups/gp/nrg.png b/assets/closeups/gp/nrg.png new file mode 100644 index 0000000000000000000000000000000000000000..f6d2e9b9712b31fd109f0a203c34529828992331 GIT binary patch literal 26516 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

270!9a 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

I flipper: tieni premuto sulla metà sinistra o destra della scena (anche con due dita insieme, o col tasto diff --git a/src/engine/flipper-render.js b/src/engine/flipper-render.js index 4e47293..3366aa4 100644 --- a/src/engine/flipper-render.js +++ b/src/engine/flipper-render.js @@ -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, rampHeight, waiting, canThrow, saveLeft } from './flipper.js'; +import { FIELD, LANE, R, SCORE, flipperSeg, alongRamp, rampHeight, waiting, canThrow, saveLeft } from './flipper.js'; import { ambientHash as hash } from './ambient.js'; import { text, panel } from './bocce-render.js'; @@ -16,47 +16,99 @@ const rail = (ctx, [ax, ay], [bx, by], dark = '#2a2622', light = '#c9c3b6') => { }; 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(); + ctx.fillStyle = fill; ctx.beginPath(); ctx.arc(x, y, r - Math.min(2, r * 0.45), 0, Math.PI * 2); ctx.fill(); } 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); + const r = b.r, lit = b.hit > 0; + disc(ctx, b.x + 1, b.y + 3, r + 1, '#172127', '#131a1e'); + const metal = ctx.createLinearGradient(b.x - r, b.y - r, b.x + r, b.y + r); + metal.addColorStop(0, '#fff3cc'); metal.addColorStop(0.35, '#bda16d'); + metal.addColorStop(0.55, '#fff1cc'); metal.addColorStop(1, '#665238'); + disc(ctx, b.x, b.y, r, metal, '#111d25'); + disc(ctx, b.x, b.y, r - 2.5, lit ? '#fff3bd' : '#e3b658', '#654223'); + const cap = ctx.createRadialGradient(b.x - 3, b.y - 4, 1, b.x, b.y, r - 4); + cap.addColorStop(0, lit ? '#fffef0' : '#e87c61'); cap.addColorStop(0.45, lit ? '#ffe39a' : '#ba4037'); cap.addColorStop(1, '#612b2b'); + disc(ctx, b.x, b.y, r - 4, cap, '#602c27'); + label(ctx, `${SCORE.bumper / 1000}K`, b.x, b.y + 0.5, 5, '#fff2d5'); + ctx.strokeStyle = '#ffe6ae'; ctx.lineWidth = 0.8; + ctx.beginPath(); ctx.arc(b.x, b.y, r - 1, Math.PI * 1.08, Math.PI * 1.65); ctx.stroke(); +} +// Incisioni con un carattere compatto, leggibile alla risoluzione della scena. +function label(ctx, line, x, y, size = 6, color = '#f8e6bc', align = 'center') { + ctx.font = `bold ${size}px monospace`; ctx.textAlign = align; ctx.textBaseline = 'middle'; + ctx.fillStyle = color; ctx.fillText(line, x, y); +} +function polygon(ctx, points, fill, stroke) { + ctx.beginPath(); points.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y)); ctx.closePath(); + ctx.fillStyle = fill; ctx.fill(); + if (stroke) { ctx.strokeStyle = stroke; ctx.lineWidth = 1; ctx.stroke(); } +} +function screw(ctx, x, y) { + disc(ctx, x, y, 1.7, '#d1d9d5', '#25333a'); + ctx.strokeStyle = '#53636b'; ctx.lineWidth = 0.6; ctx.beginPath(); ctx.moveTo(x - 0.7, y + 0.7); ctx.lineTo(x + 0.7, y - 0.7); ctx.stroke(); } // 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); + const gem = ctx.createRadialGradient(o.x - 2, o.y - 2, 0, o.x, o.y, r - 1); + gem.addColorStop(0, '#bceca0'); gem.addColorStop(0.35, '#3f9c77'); gem.addColorStop(1, '#153f42'); + disc(ctx, o.x, o.y, r - 1.5, gem, '#65522c'); + polygon(ctx, [[o.x - 5, o.y - r - 1], [o.x - 6, o.y - r - 6], [o.x - 2, o.y - r - 4], [o.x, o.y - r - 8], [o.x + 2, o.y - r - 4], [o.x + 6, o.y - r - 6], [o.x + 5, o.y - r - 1]], '#edc877', '#65522c'); + label(ctx, `${SCORE.regolo / 1000}K`, o.x, o.y + 0.5, 5, '#fff4c8'); } -// I ponti: una striscia di pietra lungo il percorso (il sinistro caldo, il destro freddo, per seguirli), più alta -// in cima (la quota), con l'ombra, il bordo scuro e la luce; accesi, brillano; l'altro, in attesa della combo, invita. +// I ponti: canaline metalliche, ambra a sinistra e azzurre a destra, sollevate secondo la quota della pallina. +// Accesi, brillano; l'altro, in attesa della combo, invita. // Si dipingono una volta per stato su una tela a parte e poi si copiano (i tratti larghi e trasparenti a ogni // fotogramma lasciavano alla grafica un arretrato di secondi, vedi effetti-leggeri). -const RAMP_LOOK = { sinistra: ['#c9b89a', '#8a7a5a'], destra: ['#b8c0c9', '#6a7a8a'] }; +const RAMP_LOOK = { sinistra: ['#ffd57a', '#95713d'], destra: ['#9ce9f2', '#497d92'] }; function rampPoint(r, s) { const p = alongRamp(r, s); return { ...p, y: p.y - 2 - 4 * rampHeight(r, s) }; } function paintRamp(ctx, r, state) { - 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]; + const [light, dark] = RAMP_LOOK[r.id], active = state !== 'base'; + // Il fondo è una canalina scura; i due parapetti sono fili cromati separati. + // Si segue esattamente il percorso della fisica, anche ai cambi di direzione. + const points = r.cum.map(s => rampPoint(r, s)); + const path = () => { ctx.beginPath(); points.forEach((p, i) => i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)); }; + const stroke = (width, color) => { ctx.lineWidth = width; ctx.strokeStyle = color; path(); ctx.stroke(); }; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; - ctx.globalAlpha = 0.45; 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 = 1; - ctx.strokeStyle = state === 'lit' ? '#ffd45a' : state === '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.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(); } - // 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 = state === '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(); + ctx.save(); ctx.translate(2, 4); stroke(17, 'rgba(3,12,19,0.48)'); ctx.restore(); + stroke(16, '#12212a'); stroke(14, active ? light : dark); + stroke(12, '#e0e4d6'); stroke(10, '#32454e'); stroke(7, '#192e39'); + stroke(2, r.id === 'sinistra' ? '#4e4940' : '#284957'); + // Traverse distanziate e piccoli inserti luminosi sui parapetti. + for (let s = 18; s < r.len - 8; s += 26) { + const p = rampPoint(r, s); + ctx.strokeStyle = '#62727a'; ctx.lineWidth = 0.7; + ctx.beginPath(); ctx.moveTo(p.x - p.dy * 4, p.y + p.dx * 4); ctx.lineTo(p.x + p.dy * 4, p.y - p.dx * 4); ctx.stroke(); + for (const side of [-1, 1]) { + ctx.fillStyle = active ? '#fff6d8' : light; + ctx.beginPath(); ctx.arc(p.x + p.dy * 6.5 * side, p.y - p.dx * 6.5 * side, 1, 0, Math.PI * 2); ctx.fill(); + } + } + // L'apertura coincide con il segmento d'ingresso: una pedana svasata e una soglia a terra. + const [[x0, y], [x1]] = r.mouth, cx = (x0 + x1) / 2; + const deck = ctx.createLinearGradient(0, y - 30, 0, y); + deck.addColorStop(0, '#1b303b'); deck.addColorStop(0.75, '#455860'); deck.addColorStop(1, '#b8c3bd'); + polygon(ctx, [[cx - 5, y - 30], [cx + 5, y - 30], [x1, y], [x0, y]], deck, '#18232b'); + for (const side of [-1, 1]) { + rail(ctx, [cx + side * 6, y - 30], [cx + side * (x1 - x0) / 2, y], '#18232b', light); + screw(ctx, cx + side * (x1 - x0) / 2, y); + } + ctx.fillStyle = '#edf0d9'; ctx.fillRect(x0 + 1, y - 1, x1 - x0 - 2, 1); + label(ctx, `${SCORE.ramp / 1000}K`, cx, y - 18, 5, light); +} +function rampEntrance(ctx, r, time, who) { + const [[x0, y], [x1]] = r.mouth, x = (x0 + x1) / 2; + const color = RAMP_LOOK[r.id][0], invite = who && who !== r.id; + // Un inserto scuro isola le frecce dai dettagli del ponte dipinto sotto. + polygon(ctx, [[x - 6, y + 1], [x + 6, y + 1], [x + 7, y + 22], [x - 7, y + 22]], '#10222e', color); + // Una freccia piena, leggibile anche sul telefono; pulsa più rapida per la combo. + const on = Math.sin(time * (invite ? 9 : 4)) > 0; + polygon(ctx, [[x, y + 4], [x + 5, y + 11], [x + 2, y + 11], [x + 2, y + 20], [x - 2, y + 20], [x - 2, y + 11], [x - 5, y + 11]], on ? '#fff6d6' : color); + ctx.fillStyle = '#112530'; ctx.fillRect(x - 14, y + 22, 28, 10); + ctx.strokeStyle = color; ctx.lineWidth = 0.7; ctx.strokeRect(x - 14, y + 22, 28, 10); + label(ctx, 'RAMPA', x, y + 27, 6, color); } const rampCache = new Map(); function ramp(ctx, r, time, who) { @@ -73,19 +125,34 @@ function ramp(ctx, r, time, who) { } let railLayer = null; function rails(ctx, c) { - const paint = g => { for (const w of c.walls) rail(g, w.a, w.b, w.sling ? '#5a1d16' : '#2a2622', w.sling ? '#e8584a' : '#c9c3b6'); }; + const paint = g => { + // Plastiche smaltate sopra gli elastici, con bordi e viti di fissaggio. + for (const side of [-1, 1]) { + const x = side < 0 ? 236 : 404, tip = x - side * 26; + polygon(g, [[x, 246], [x, 284], [tip, 290]], '#152f3c', '#c1a273'); + polygon(g, [[x - side * 4, 260], [x - side * 4, 280], [tip + side * 7, 284]], '#294c5b', '#597c83'); + screw(g, x, 250); screw(g, x, 281); screw(g, tip + side * 4, 286); + } + for (const w of c.walls) rail(g, w.a, w.b, w.sling ? '#582723' : '#18262d', w.sling ? '#f09a70' : '#d2d9cb'); + }; if (typeof document === 'undefined') { paint(ctx); return; } if (!railLayer) { railLayer = Object.assign(document.createElement('canvas'), { width: 640, height: 360 }); paint(railLayer.getContext('2d')); } ctx.drawImage(railLayer, 0, 0); } function flipper(ctx, c, side) { 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 = 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'); + ctx.save(); ctx.translate(px, py); ctx.rotate(Math.atan2(ty - py, tx - px)); + const len = Math.hypot(tx - px, ty - py); + const shape = () => { + ctx.beginPath(); ctx.moveTo(0, -3); ctx.lineTo(len, -2.5); + ctx.quadraticCurveTo(len + 3, 0, len, 2.5); ctx.lineTo(0, 3); + ctx.arc(0, 0, 3, Math.PI / 2, Math.PI * 1.5); ctx.closePath(); + }; + ctx.save(); ctx.translate(1, 3); shape(); ctx.fillStyle = '#142029'; ctx.fill(); ctx.restore(); + shape(); ctx.fillStyle = f.held.length ? '#fff6da' : '#f0e4c6'; ctx.fill(); + ctx.strokeStyle = '#bb503c'; ctx.lineWidth = 1.5; ctx.stroke(); + ctx.strokeStyle = '#fffdf0'; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.moveTo(3, -1.7); ctx.lineTo(len - 2, -1.2); ctx.stroke(); + screw(ctx, 0, 0); ctx.restore(); } function plunger(ctx, c) { const x = (LANE.x0 + LANE.x1) / 2, top = 326, pull = c.holding ? c.power * 14 : canThrow(c) ? c.power * 6 : 0; @@ -194,6 +261,13 @@ function board(ctx, c, time, glass) { 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(); + // Il traguardo resta leggibile anche mentre il display celebra un colpo. + const progress = Math.min(1, c.score / c.skill.goal), remaining = Math.max(0, c.skill.goal - c.score); + panel(ctx, 20, 134, 160, 31); + label(ctx, `OBIETTIVO ${c.skill.goal.toLocaleString('it-IT')}`, 28, 142, 7, '#f1d7a1', 'left'); + label(ctx, remaining ? `MANCANO ${remaining.toLocaleString('it-IT')}` : 'TRAGUARDO RAGGIUNTO', 28, 152, 6, remaining ? '#becbd0' : '#a9e7bf', 'left'); + ctx.fillStyle = '#30434a'; ctx.fillRect(28, 159, 144, 2); + ctx.fillStyle = '#edc777'; ctx.fillRect(28, 159, 144 * progress, 2); // 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. @@ -294,10 +368,12 @@ export function drawFlipper(ctx, c, def, bg, glass) { if (sx || sy) { ctx.fillStyle = '#0c0806'; ctx.fillRect(0, 0, 640, 360); ctx.translate(sx, sy); } 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); } + // La stampa resta visibile, ma le parti giocabili emergono dal paesaggio. + ctx.fillStyle = 'rgba(8,25,34,0.22)'; ctx.fillRect(201, 73, 230, 261); // 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'); + disc(ctx, l.x, l.y, 6, l.lit ? COLORS[l.rione] : '#273a3e', l.lit ? '#fff4c8' : '#b5a278'); + disc(ctx, l.x, l.y, 3.5, l.lit ? '#fff2c6' : COLORS[l.rione], '#182c32'); } // Le sponde: tutte le pareti della fisica, cromate; le elastiche con la gomma rossa. Non cambiano mai: dipinte // una volta su una tela a parte. @@ -309,9 +385,12 @@ export function drawFlipper(ctx, c, def, bg, glass) { 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); + ctx.fillStyle = '#fff8d7'; ctx.fillRect(x - 2, y0, 4, 1); + ctx.fillStyle = '#3a5864'; ctx.fillRect(x - 1, y0 + 3, 2, 4); } for (const b of c.bumpers) bumper(ctx, b); regolo(ctx, c.regolo); + for (const r of c.ramps) rampEntrance(ctx, r, time, c.lastRamp?.id); for (const side of ['left', 'right']) flipper(ctx, c, side); plunger(ctx, c); // La pallina (con la cometa) sotto i ponti, se è sulla tavola; sopra, se è su un ponte; poi gli effetti. diff --git a/src/engine/flipper.js b/src/engine/flipper.js index 4c606a1..7a7507f 100644 --- a/src/engine/flipper.js +++ b/src/engine/flipper.js @@ -15,7 +15,7 @@ export const PIVOT = { left: [280, 314], right: [360, 314] }; export const SCORE = { bumper: 1000, sling: 500, lane: 2500, palio: 30000, target: 5000, bank: 25000, regolo: 10000, ramp: 15000, ponti: 50000 }; // Le rampe (Andrea: «un paio di ponti sui quali sale la pallina»): si entra dal basso con abbastanza velocità, si // sale, si passa sopra la tavola e si scende dall'altra parte. Fatte tutte e due di fila: i PONTI. -export const RAMP = { speed: 300, slope: 260, drag: 30, combo: 12, blend: 30 }; +export const RAMP = { speed: 240, slope: 155, drag: 20, combo: 12, blend: 30 }; // 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 @@ -44,8 +44,9 @@ export function layout() { 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 (la guida passa sopra il perno e continua un // 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([418, 240], [418, 290]), seg([418, 290], [348, 314], { e: 0.3 })); + // Le uscite lasciano solo 14/12 pixel; lo spazio recuperato allarga le corsie di ritorno. + walls.push(seg([214, 240], [214, 290]), seg([214, 290], [292, 314], { e: 0.3 })); + walls.push(seg([420, 240], [420, 290]), seg([420, 290], [348, 314], { e: 0.3 })); // Le sponde elastiche sopra i flipper: il lato lungo spinge. walls.push(seg([236, 246], [236, 284]), seg([236, 284], [262, 290]), seg([236, 246], [262, 290], { sling: true })); walls.push(seg([404, 246], [404, 284]), seg([404, 284], [378, 290]), seg([404, 246], [378, 290], { sling: true })); @@ -63,8 +64,9 @@ export function layout() { // fondo al percorso, dall'altra parte, dritta nella corsia di ritorno verso il flipper. Si incrociano una volta sola, // in cima, come due ponti. const ramps = [ - { id: 'sinistra', mouth: [[236, 236], [258, 236]], path: [[247, 236], [247, 150], [250, 96], [262, 52], [290, 30], [322, 34], [352, 52], [378, 84], [394, 116], [411, 128], [411, 142]], lit: 0, touching: false }, - { id: 'destra', mouth: [[382, 236], [404, 236]], path: [[393, 236], [393, 150], [390, 96], [378, 52], [350, 30], [318, 34], [288, 52], [262, 84], [246, 116], [229, 128], [229, 142]], lit: 0, touching: false }, + // Imbocchi verso il centro, davanti alle sponde elastiche: 33 pixel utili alla pallina anziché 13. + { id: 'sinistra', mouth: [[252, 226], [294, 226]], path: [[273, 226], [273, 196], [247, 150], [250, 96], [262, 52], [290, 30], [322, 34], [352, 52], [378, 84], [394, 116], [411, 128], [411, 142]], lit: 0, touching: false }, + { id: 'destra', mouth: [[346, 226], [388, 226]], path: [[367, 226], [367, 196], [393, 150], [390, 96], [378, 52], [350, 30], [318, 34], [288, 52], [262, 84], [246, 116], [229, 128], [229, 142]], lit: 0, touching: false }, ]; // Lunghezze, e la cima (il punto più alto del disegno): fin lì si sale, poi si scende. for (const r of ramps) { diff --git a/src/story/piloti.js b/src/story/piloti.js index e6f9dc7..7625cb0 100644 --- a/src/story/piloti.js +++ b/src/story/piloti.js @@ -23,6 +23,9 @@ export const piloti = [ casco: 'assets/closeups/gp/casco-tozzino.png', helmet: '#131c3c', stripe: '#f4f4f4' }, // Yuri de' Motori (Juri Lascialfari, consenso del 28 ottobre) col suo Yamaha Spy bianco e rosso, il «Mazinga Spy» (foto di Andrea). { id: 'yuri', name: 'Yuri de\' Motori', bike: 'il Mazinga Spy', motore: 3, testa: 5, color: '#f0f0ee', trim: '#d8302a', moto: 'assets/closeups/gp/spy.png', face: 'assets/closeups/gp/yuri.png', consenso: '2026-10-28' }, + // Il Boccino (Gabriele Chiesi, consenso del 31 ottobre) col suo Piaggio NRG bianco e rosso col numero 1 (foto di + // Andrea); il ritratto di Codex dalla sua foto. [DA ANDREA: motore e testa] + { id: 'boccino', name: 'Il Boccino', bike: 'l\'NRG numero 1', motore: 3, testa: 3, color: '#f4f4f2', trim: '#d8302a', helmet: '#d8302a', stripe: '#f4f4f2', moto: 'assets/closeups/gp/nrg.png', face: 'assets/closeups/gp/boccino.png', consenso: '2026-10-31' }, // Il Gianno (il Giannini, consenso del 28 ottobre) col suo Piaggio Typhoon rosso e nero (foto di Andrea). { id: 'gianno', name: 'Il Gianno', bike: 'il Typhoon', motore: 3, testa: 3, color: '#9e1b22', trim: '#151515', moto: 'assets/closeups/gp/typhoon.png', face: 'assets/closeups/gp/gianno.png', consenso: '2026-10-28' }, // La Brauda (Giody Brunetti, consenso del 28 ottobre) col suo Aprilia Rally nero (foto di Andrea). [DA ANDREA: motore e testa] diff --git a/tests/flipper.browser.mjs b/tests/flipper.browser.mjs index 2249a11..55d5c08 100644 --- a/tests/flipper.browser.mjs +++ b/tests/flipper.browser.mjs @@ -26,12 +26,16 @@ try { }); await page.waitForSelector('#bocce-controls:not([hidden])'); await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/river-flipper.png')); + // Aspetta che finisca la dissolvenza d'ingresso: le immagini devono mostrare i colori della tavola. + const readyAt = await page.evaluate(() => game.closeup.visual.time); + await page.waitForFunction(t => game.closeup.visual.time > t + 1, readyAt); assert.equal(await page.locator('#bocce-throw').textContent(), 'Lancia'); assert.ok(await page.locator('#flipper-help').isVisible()); 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` }); const box = await page.locator('#scene').boundingBox(), P = (x, y) => [box.x + x / 640 * box.width, box.y + y / 360 * box.height]; + if (!mobile) await page.screenshot({ path: `${out}/desktop-dettaglio.png`, clip: { x: P(200, 0)[0], y: box.y, width: 248 / 640 * box.width, height: box.height } }); const cdp = mobile ? await page.context().newCDPSession(page) : null; const touch = (type, points) => cdp.send('Input.dispatchTouchEvent', { type, touchPoints: points.map(([x, y]) => ({ x, y })) }); // La molla: tenendo premuto sulla scena, col mouse e col dito. diff --git a/tests/flipper.test.mjs b/tests/flipper.test.mjs index 2ce65b0..26db920 100644 --- a/tests/flipper.test.mjs +++ b/tests/flipper.test.mjs @@ -9,6 +9,17 @@ import { existsSync } from 'node:fs'; 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); +// Osserva il percorso fino all'uscita, senza includere il tiro successivo sulla tavola. +function ride(c, initial) { + put(c, initial); + let frames = 0, top = Infinity; + for (let i = 0; i < 4 / m.STEP; i++) { + m.update(c, m.STEP); + if (c.ball?.ramp) { frames++; top = Math.min(top, c.ball.y); } + else if (frames) return { frames, top, exit: { ...c.ball } }; + } + assert.fail('la pallina non ha completato o abbandonato la rampa entro quattro secondi'); +} // Il giocatore automatico: alza il flipper quando la pallina gli scende addosso, come un umano attento. function autoplay(c, secs = 600) { for (let i = 0; i < secs * 60 && c.step === 'play'; i++) { @@ -161,28 +172,27 @@ test('il traguardo è acquisito appena raggiunto: anche uscendo prima, si vince; test('i ponti: imboccati forte si sale, si passa sopra la tavola e si scende nella corsia di ritorno; piano si ricasca; di traverso no; tutti e due di fila, i PONTI', () => { const c = m.newFlipper(13, { level: 'bona' }); m.launch(c, 0.9); c.score = 0; - put(c, { x: 247, y: 250, vx: 0, vy: -470 }); - let onRamp = 0, top = 999, exit = null; for (let i = 0; i < 150; i++) { m.update(c, 1 / 60); if (c.ball.ramp) { onRamp++; top = Math.min(top, c.ball.y); } else if (onRamp && !exit) exit = { x: c.ball.x, y: c.ball.y }; } + const { frames: onRamp, top, exit } = ride(c, { x: 273, y: 240, vx: 0, vy: -470 }); assert.ok(onRamp > 20, 'è salita sul ponte'); assert.ok(top < 40, 'fin sopra la tavola'); assert.ok(exit && exit.x > 400 && exit.x < 420, `scesa nella corsia di ritorno di destra (${exit?.x.toFixed(0)})`); assert.equal(c.score, m.SCORE.ramp); assert.equal(c.line, 'PONTE SX'); assert.deepEqual(c.lines, ['+15000']); assert.ok(c.ramps[0].lit > 0); assert.ok(c.events.some(e => e.kind === 'flipper-rampa')); // Mentre è sul ponte i flipper rispondono lo stesso (Codex: restavano congelati). - const f = m.newFlipper(13, { level: 'bona' }); m.launch(f, 0.9); put(f, { x: 247, y: 250, vx: 0, vy: -470 }); run(f, 0.3); + const f = m.newFlipper(13, { level: 'bona' }); m.launch(f, 0.9); put(f, { x: 273, y: 240, vx: 0, vy: -470 }); run(f, 0.3); assert.ok(f.ball.ramp, 'sul ponte'); m.turn(f, -1, 'click'); run(f, 0.1); assert.ok(f.flippers.left.angle < 0.3, `il flipper si è alzato (${f.flippers.left.angle.toFixed(2)})`); // Il secondo ponte subito dopo: i PONTI, col bonus. - put(c, { x: 393, y: 250, vx: 0, vy: -470, ramp: null }); run(c, 2.5); + ride(c, { x: 367, y: 240, vx: 0, vy: -470, ramp: null }); assert.equal(c.score, 2 * m.SCORE.ramp + m.SCORE.ponti, `i ponti (${c.score})`); assert.ok([c.line, ...c.lines].some(l => l === 'I PONTI!') && [c.line, ...c.lines].some(l => /BONUS/.test(l ?? ''))); assert.equal(c.lastRamp, null, 'la combo è spesa'); // Troppo piano: sale un po', non ce la fa, ricasca dall'imbocco senza punti. const d = m.newFlipper(14, { level: 'bona' }); m.launch(d, 0.9); d.score = 0; - put(d, { x: 247, y: 250, vx: 0, vy: -335 }); let climbed = false; for (let i = 0; i < 300; i++) { m.update(d, 1 / 60); if (d.ball.ramp) climbed = true; } - assert.ok(climbed, 'è salita'); assert.ok(!d.ball.ramp && d.ball.y > 236, 'ricaduta'); assert.ok(d.score < m.SCORE.ramp, 'niente punti del ponte'); + put(d, { x: 273, y: 240, vx: 0, vy: -280 }); let climbed = false; for (let i = 0; i < 300; i++) { m.update(d, 1 / 60); if (d.ball.ramp) climbed = true; } + assert.ok(climbed, 'è salita'); assert.ok(!d.ball.ramp && d.ball.y > 226, 'ricaduta'); assert.ok(d.score < m.SCORE.ramp, 'niente punti del ponte'); // Di traverso (Codex: sfiorando l'imbocco quasi in orizzontale saliva lo stesso): no. const e = m.newFlipper(15, { level: 'bona' }); m.launch(e, 0.9); e.score = 0; - put(e, { x: 247, y: 236.1, vx: 500, vy: -30 }); run(e, 0.5); + put(e, { x: 273, y: 226.1, vx: 500, vy: -30 }); run(e, 0.5); assert.ok(!e.ball.ramp && e.score < m.SCORE.ramp, 'non è salita'); // La combo scade dopo dodici secondi, e si azzera perdendo la pallina. const g = m.newFlipper(16, { level: 'bona' }); m.launch(g, 0.9); g.lastRamp = { id: 'sinistra', at: g.sim }; put(g, { x: 320, y: 200, vx: 0, vy: 0 }); @@ -199,7 +209,7 @@ test('gli effetti: ogni colpo lascia il suo segno (anello, scintille, punti che assert.equal(c.fx.find(f => f.kind === 'pop').text, '+1000'); c.fx.length = 0; c.score = c.skill.goal - 100; put(c, { x: c.regolo.x, y: c.regolo.y + 12, vx: 0, vy: -200 }); run(c, 0.05); assert.ok(kinds().includes('burst') && kinds().includes('wash') && kinds().includes('shake'), `traguardo: ${kinds()}`); - c.fx.length = 0; put(c, { x: 247, y: 250, vx: 0, vy: -470 }); run(c, 2.5); + c.fx.length = 0; ride(c, { x: 273, y: 240, vx: 0, vy: -470 }); assert.ok(kinds().includes('chase'), `ponte: ${kinds()}`); assert.ok(c.trail.length > 5, 'la cometa'); const ball = c.ball; c.ball = null; run(c, m.FX_LIFE + 0.1); c.ball = ball; @@ -207,7 +217,7 @@ test('gli effetti: ogni colpo lascia il suo segno (anello, scintille, punti che // Disegnarli non cambia niente. const calls = [], gradient = { addColorStop() {} }; const ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => { calls.push([k, ...args]); return gradient; }, set: (o, k, v) => { o[k] = v; return true; } }); - put(c, { x: 247, y: 250, vx: 0, vy: -470 }); run(c, 0.4); c.fx.push({ kind: 'burst', x: 320, y: 100, t: c.visual.time, big: true }, { kind: 'shake', x: 0, y: 0, t: c.visual.time }); + put(c, { x: 273, y: 240, vx: 0, vy: -470 }); run(c, 0.4); c.fx.push({ kind: 'burst', x: 320, y: 100, t: c.visual.time, big: true }, { kind: 'shake', x: 0, y: 0, t: c.visual.time }); const before = structuredClone(c); drawFlipper(ctx, c, story.closeups.flipper, {}); assert.deepEqual(c, before); assert.ok(calls.some(([op]) => op === 'translate'), 'la scossa sposta la scena'); }); @@ -229,6 +239,13 @@ test('il disegno del flipper non cambia lo stato; il tabellone dice l\'esito giu const c = m.newFlipper(11, { level: 'bona' }), bg = {}; 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)); + assert.ok(calls.filter(([op]) => op === 'arc').every(([, , , r]) => r > 0), 'anche viti e inserti piccoli hanno raggi validi per Canvas'); + for (const r of c.ramps) { + const x = (r.mouth[0][0] + r.mouth[1][0]) / 2; + assert.ok(calls.some(([op, line, tx, ty]) => op === 'fillText' && line === 'RAMPA' && tx === x && ty > r.mouth[0][1]), 'la targhetta indica l\'imbocco dal basso'); + } + assert.ok(calls.some(([op, line]) => op === 'fillText' && line === 'OBIETTIVO 60.000')); + assert.ok(calls.some(([op, line]) => op === 'fillText' && line === 'MANCANO 60.000')); assert.ok(existsSync(new URL('../' + story.closeups.flipper.backglass, import.meta.url)), 'il fondale del tabellone esiste'); const glass = {}; drawFlipper(ctx, c, story.closeups.flipper, bg, glass); assert.ok(calls.some(([op, img, x, y, w, h]) => op === 'drawImage' && img === glass && w === 200 && h === 360), 'il fondale sul fianco'); m.launch(c, 0.8); run(c, 1); calls.length = 0; draw();