Files
leggenda-del-regolo/src/engine/flipper-art.js
T

186 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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();
}