// Illustrazioni vettoriali del backglass, riusate come inserti in lacca sul campo. import { activeBalls } from './flipper.js'; export const SCENES = { regolo: { color: '#91f5b1', title: 'IL REGOLO SI RISVEGLIA', note: 'IL CUSTODE DELLA SIEVE', at: [320, 193] }, fortezza: { color: '#65ddec', title: 'LA FORTEZZA SI ACCENDE', note: 'SOPRA I BASTIONI', at: [320, 119] }, trebbio: { color: '#ffc16c', title: 'LA TORRE DEL TREBBIO', note: 'UN PASSAGGIO NELLA STORIA', at: [365, 164] }, pieve: { color: '#ffaccc', title: 'LE LUCI DELLA PIEVE', note: 'NEL CUORE DEL PAESE', at: [240, 174] }, rioni: { color: '#ffe6a0', title: 'I COLORI DEI RIONI', note: 'QUATTRO COLORI, UN PAESE', at: [320, 88] }, sieve: { color: '#6fcfff', title: 'SULLE ONDE DELLA SIEVE', note: 'IL VIAGGIO CONTINUA', at: [320, 280] }, cosmo: { color: '#dba5ff', title: 'IL REGOLO DEL FUTURO', note: 'OLTRE IL TEMPO', at: [320, 193] }, spira: { color: '#ff729e', title: 'NELLA SPIRA AL NEON', note: 'ENERGIA IN MOVIMENTO', at: [376, 170] }, }; const RAMP_SCENE = { sinistra: 'fortezza', destra: 'trebbio', super: 'cosmo', orbita: 'cosmo', ponte: 'fortezza', spira: 'spira' }; // Solo lettura: nessun evento, punteggio o numero casuale della simulazione viene consumato dal disegno. export function flipperScene(c) { const balls = activeBalls(c).filter(b => !b.waiting && b.releaseAt <= c.sim); if (c.regolo.hit > 0 || balls.some(b => !b.ramp && Math.hypot(b.x - c.regolo.x, b.y - c.regolo.y) < 33)) return 'regolo'; const ramp = Object.keys(RAMP_SCENE).find(id => balls.some(b => b.ramp?.id === id)); if (ramp) return RAMP_SCENE[ramp]; if (!balls.length) return c.table === 'future' ? 'cosmo' : 'regolo'; if (c.bumpers.some(b => b.hit > 0)) return 'fortezza'; // Priorità stabile anche in multiball, indipendente dall'ordine delle palline. if (balls.some(b => b.y < 106)) return c.table === 'future' ? 'cosmo' : 'rioni'; if (balls.some(b => b.x < 242 && b.y < 218)) return 'pieve'; if (balls.some(b => b.x > 398 && b.x < 432 && b.y < 218)) return 'trebbio'; if (balls.some(b => b.y < 180)) return 'fortezza'; if (balls.some(b => b.y < 244)) return c.table === 'future' ? 'cosmo' : 'regolo'; return 'sieve'; } const TAU = Math.PI * 2; function stroke(g, pts, color, width = 1) { g.beginPath(); pts.forEach(([x, y], i) => i ? g.lineTo(x, y) : g.moveTo(x, y)); g.strokeStyle = color; g.lineWidth = width; g.stroke(); } function fill(g, pts, color) { g.beginPath(); pts.forEach(([x, y], i) => i ? g.lineTo(x, y) : g.moveTo(x, y)); g.closePath(); g.fillStyle = color; g.fill(); } function circle(g, x, y, r, color) { g.beginPath(); g.arc(x, y, r, 0, TAU); g.fillStyle = color; g.fill(); } export function paintRegolo(g, x, y, size, color = '#91f5b1') { g.save(); g.translate(x, y); g.scale(size / 30, size / 30); g.lineCap = 'round'; g.lineJoin = 'round'; // Un serpente a spire, con ventre dorato, cresta, occhio e corona: leggibile anche come piccolo emblema. const body = () => { g.beginPath(); g.moveTo(-29, 22); g.bezierCurveTo(-8, 28, 29, 23, 23, 7); g.bezierCurveTo(17, -9, -24, 0, -18, 13); g.bezierCurveTo(-13, 24, 13, 12, 9, 0); g.bezierCurveTo(5, -11, -9, -9, -4, -23); }; body(); g.strokeStyle = '#061d26'; g.lineWidth = 12; g.stroke(); body(); g.strokeStyle = color; g.lineWidth = 8; g.stroke(); body(); g.strokeStyle = '#153f43'; g.lineWidth = 4.6; g.stroke(); g.save(); g.translate(0, 2); body(); g.strokeStyle = '#ffe5a6'; g.lineWidth = 1.1; g.stroke(); g.restore(); for (let i = 0; i < 7; i++) { const a = i * 0.36; stroke(g, [[-16 + i * 5, 20 - Math.sin(a) * 3], [-15 + i * 5, 17 - Math.sin(a) * 3]], color, 1); } fill(g, [[-9, -26], [-4, -31], [5, -29], [12, -23], [9, -18], [0, -18], [-5, -14]], color); stroke(g, [[3, -18], [11, -19], [15, -17], [18, -19]], '#ff92a7', 1); circle(g, 4, -25, 2.1, '#071821'); circle(g, 4.5, -25.6, 0.8, '#ffffff'); fill(g, [[-8, -33], [-11, -43], [-4, -38], [0, -47], [5, -38], [12, -43], [8, -33]], '#ffdc88'); stroke(g, [[-8, -32], [8, -32]], '#fff5d5', 1.6); circle(g, 0, -38, 1.4, '#ff729e'); g.restore(); } export function paintZoneSymbol(g, id) { const { color, at: [x, y] } = SCENES[id]; g.save(); g.translate(x, y); g.lineJoin = 'round'; if (id === 'fortezza' || id === 'trebbio' || id === 'pieve') { const roof = id === 'pieve' ? [[-12, -2], [0, -13], [12, -2]] : id === 'trebbio' ? [[-7, 8], [-7, -14], [-3, -14], [-3, -10], [3, -10], [3, -14], [7, -14], [7, 8]] : [[-14, 7], [-14, -7], [-9, -7], [-9, -3], [-4, -3], [-4, -10], [4, -10], [4, -3], [9, -3], [9, -7], [14, -7], [14, 7]]; stroke(g, roof, color, 1.3); stroke(g, [[-10, 0], [-10, 9], [10, 9], [10, 0]], color, 0.8); stroke(g, [[-3, 8], [-3, 2], [0, -1], [3, 2], [3, 8]], '#fff1c1', 0.8); } else if (id === 'rioni') { for (let i = 0; i < 4; i++) { const px = -15 + i * 10; fill(g, [[px, -10], [px + 7, -10], [px + 7, 3], [px + 3.5, 7], [px, 3]], ['#f97980', '#f7eac9', '#78e2ad', '#7bb6ff'][i]); } } else if (id === 'sieve') { for (let i = 0; i < 3; i++) { g.beginPath(); g.moveTo(-19, -6 + i * 6); g.bezierCurveTo(-8, -14 + i * 6, 8, 2 + i * 6, 19, -6 + i * 6); g.strokeStyle = color; g.lineWidth = 1.2; g.stroke(); } } else if (id === 'spira') { for (let i = 0; i < 3; i++) { g.beginPath(); g.ellipse(0, 0, 6 + i * 5, 4 + i * 4, -0.5, 0, TAU); g.strokeStyle = color; g.lineWidth = 0.9; g.stroke(); } } g.restore(); } function fortress(g, tower, color) { fill(g, [[18, 69], [18, 43], [28, 39], [28, 32], [35, 32], [35, 41], [51, 40], [51, 29], [61, 29], [61, 39], [87, 37], [87, 26], [97, 26], [97, 36], [119, 34], [119, 29], [129, 29], [129, 65]], '#163648'); stroke(g, [[18, 43], [35, 41], [51, 40], [61, 39], [87, 37], [97, 36], [129, 34]], color, 1.5); if (tower) { fill(g, [[61, 63], [61, 14], [66, 14], [66, 19], [73, 19], [73, 12], [79, 12], [79, 19], [86, 19], [86, 14], [91, 14], [91, 63]], '#365062'); stroke(g, [[61, 63], [61, 14], [66, 14]], color, 1.3); for (const y of [27, 40, 53]) { g.fillStyle = '#ffd590'; g.fillRect(73, y, 5, 7); } } for (let y = 46; y < 67; y += 7) stroke(g, [[22, y], [126, y - 6]], '#426076', 0.5); for (const x of [33, 49, 101, 117]) { g.fillStyle = color; g.fillRect(x, 47, 2, 6); } fill(g, [[67, 70], [67, 55], [73, 50], [80, 55], [80, 70]], '#071927'); stroke(g, [[14, 72], [74, 65], [135, 70]], color, 1); } // Una scena di 150 × 88, interamente in cache nel renderer. export function paintFlipperScene(g, id) { const color = SCENES[id].color; const sky = g.createLinearGradient(0, 0, 150, 88); sky.addColorStop(0, '#071125'); sky.addColorStop(0.6, id === 'trebbio' ? '#38273b' : '#15364a'); sky.addColorStop(1, '#071925'); g.fillStyle = sky; g.fillRect(0, 0, 150, 88); circle(g, 120, 19, 13, color + '18'); circle(g, 120, 19, 8, '#fff0be'); circle(g, 117, 16, 7, '#153044'); for (let i = 0; i < 20; i++) circle(g, (i * 47 + 13) % 150, (i * 23 + 9) % 57, i % 3 ? 0.5 : 0.9, color + 'aa'); fill(g, [[0, 59], [21, 37], [44, 53], [77, 27], [108, 51], [139, 33], [150, 40], [150, 88], [0, 88]], '#142b3c'); fill(g, [[0, 74], [39, 53], [67, 64], [99, 44], [150, 62], [150, 88], [0, 88]], '#1f4350'); for (let i = 0; i < 5; i++) { g.beginPath(); g.moveTo(0, 74 + i * 3); g.bezierCurveTo(52, 58 + i * 3, 78, 91 + i * 2, 150, 67 + i * 4); g.strokeStyle = i % 2 ? '#69b6ce66' : '#102837'; g.lineWidth = 1; g.stroke(); } if (id === 'fortezza' || id === 'trebbio') fortress(g, id === 'trebbio', color); else if (id === 'pieve') { fill(g, [[30, 69], [30, 43], [59, 25], [87, 43], [87, 69]], '#406077'); fill(g, [[87, 68], [87, 23], [94, 13], [101, 23], [101, 68]], '#29495e'); stroke(g, [[26, 44], [59, 22], [90, 44]], color, 2); circle(g, 59, 43, 7, '#142a3e'); circle(g, 59, 43, 4, '#ffc8de'); fill(g, [[52, 69], [52, 58], [59, 53], [66, 58], [66, 69]], '#102334'); g.fillStyle = '#ffdfa2'; g.fillRect(91, 28, 6, 9); } else if (id === 'rioni') { ['#f97980', '#f7eac9', '#78e2ad', '#7bb6ff'].forEach((c, i) => { const x = 24 + i * 29, y = 23 + (i % 2) * 5; stroke(g, [[x, y - 4], [x, 69]], '#d5c7a1', 1.2); fill(g, [[x + 2, y], [x + 23, y + 3], [x + 23, y + 30], [x + 12, y + 38], [x + 2, y + 30]], c); stroke(g, [[x + 12, y + 7], [x + 12, y + 27]], '#213347', 2); stroke(g, [[x + 6, y + 17], [x + 18, y + 17]], '#213347', 2); }); } else if (id === 'sieve') { g.beginPath(); g.moveTo(10, 65); g.quadraticCurveTo(74, 9, 140, 65); g.strokeStyle = '#0a1b2c'; g.lineWidth = 10; g.stroke(); g.strokeStyle = color; g.lineWidth = 2; g.stroke(); for (const x of [28, 49, 74, 99, 122]) stroke(g, [[x, 49 - Math.sin(x / 150 * Math.PI) * 10], [x, 73]], '#5f8b9f', 3); paintRegolo(g, 77, 70, 12, '#91f5b1'); } else { for (let i = 0; i < 3; i++) { g.beginPath(); g.ellipse(75, 44, 28 + i * 12, 23 + i * 8, -0.4 + i * 0.3, 0, TAU); g.strokeStyle = color + (i ? '55' : 'aa'); g.lineWidth = 0.8; g.stroke(); } paintRegolo(g, 75, 51, id === 'spira' ? 26 : 30, color); } // Riflessi e retino del vetro inciso. for (let y = 2; y < 88; y += 3) stroke(g, [[0, y], [150, y]], '#050f1c22', 0.5); } export function paintPlayfieldArt(g, future = false) { const color = future ? '#ad8cff' : '#74c5bd'; g.save(); // Rosone centrale, smalti e serpente coronato sotto il piano di gioco. for (const r of [30, 33, 43]) { g.beginPath(); g.ellipse(320, 194, r, r * 0.83, 0, 0, TAU); g.strokeStyle = color + '55'; g.lineWidth = r === 33 ? 1.4 : 0.6; g.stroke(); } for (let i = 0; i < 16; i++) { const a = i / 16 * TAU; stroke(g, [[320 + Math.cos(a) * 37, 194 + Math.sin(a) * 31], [320 + Math.cos(a) * 41, 194 + Math.sin(a) * 34]], '#d6be7855', 0.8); } g.globalAlpha = 0.46; paintRegolo(g, 320, 202, 25, color); g.globalAlpha = 1; for (const [x, side, tint] of [[239, -1, '#65ddec'], [402, 1, '#ffc16c']]) { stroke(g, [[x, 217], [x - side * 10, 230], [x - side * 10, 242]], tint + '66', 1); for (let i = 0; i < 3; i++) fill(g, [[x, 209 + i * 8], [x + 2, 212 + i * 8], [x, 215 + i * 8], [x - 2, 212 + i * 8]], tint + '88'); } // Filetti dorati delle sponde e frecce smaltate verso le palette. for (const side of [-1, 1]) { const x = 320 + side * 67; stroke(g, [[x, 263], [x - side * 14, 279], [x - side * 22, 300]], '#e7c88955', 0.8); for (let i = 0; i < 3; i++) { const cx = x - side * i * 7, y = 274 + i * 10; stroke(g, [[cx - 3, y - 2], [cx, y + 1], [cx + 3, y - 2]], color + '77', 1); } } g.restore(); }