import { FLOOR, RIM, BALL, BALLS, SPOTS, canThrow, left, spot, releasePoint, guide as guidePoints, status } from './basket.js'; import { text, panel } from './bocce-render.js'; const ANDREA = { scale: 0.66 }; // Quale posa: carica mentre tiene premuto; finché la palla vola Andrea resta a braccia alzate (l'unica posa senza // pallone), esulta quando entra, e a palla ferma ce l'ha di nuovo in mano. export function andreaPose(c) { if (c.holding) return 'load'; if (c.step === 'flight') return c.ball.scored ? 'cheer' : 'shoot'; if (c.step === 'settle' && c.ball?.scored) return 'cheer'; return 'hold'; } function andrea(ctx, c, sheet) { const s = spot(c), x = s.x, y = FLOOR; ctx.fillStyle = 'rgba(8,10,9,0.5)'; ctx.beginPath(); ctx.ellipse(x + 4, y, 16, 3, 0, 0, Math.PI * 2); ctx.fill(); if (!sheet) return; const pose = andreaPose(c), frame = sheet.atlas.frames[pose] ?? sheet.atlas.frames.hold, k = ANDREA.scale; // Nel salto i piedi si staccano da terra per un attimo. const lift = pose === 'shoot' ? Math.round(Math.sin(Math.min(1, c.ball.time / 0.45) * Math.PI) * 10) : 0; ctx.drawImage(sheet.img, frame.x, frame.y, frame.w, frame.h, Math.round(x - frame.ax * k), Math.round(y - frame.ay * k) - lift, Math.round(frame.w * k), Math.round(frame.h * k)); } function ball(ctx, b, r = BALL) { // L'ombra sul campo si stringe quando la palla è alta. const h = Math.max(0, FLOOR - b.y - r), k = Math.max(0.35, 1 - h / 260); ctx.fillStyle = `rgba(8,10,9,${0.45 * k})`; ctx.beginPath(); ctx.ellipse(b.x, FLOOR - 1, r * 1.2 * k, r * 0.4 * k, 0, 0, Math.PI * 2); ctx.fill(); const shade = ctx.createRadialGradient(b.x - r * 0.35, b.y - r * 0.35, 0, b.x, b.y, r * 1.05); shade.addColorStop(0, '#ffb067'); shade.addColorStop(0.5, '#e8702a'); shade.addColorStop(1, '#8a3512'); ctx.fillStyle = shade; ctx.beginPath(); ctx.arc(b.x, b.y, r, 0, Math.PI * 2); ctx.fill(); // Le cuciture girano con la palla. ctx.save(); ctx.beginPath(); ctx.arc(b.x, b.y, r - 0.3, 0, Math.PI * 2); ctx.clip(); ctx.strokeStyle = 'rgba(60,20,8,0.75)'; ctx.lineWidth = 1; const a = b.spin ?? 0; ctx.beginPath(); ctx.ellipse(b.x, b.y, r, Math.abs(Math.cos(a)) * r, 0, 0, Math.PI * 2); ctx.stroke(); ctx.beginPath(); ctx.ellipse(b.x, b.y, Math.abs(Math.sin(a)) * r, r, 0, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); ctx.fillStyle = '#ffd9b0'; ctx.fillRect(Math.round(b.x - r * 0.4), Math.round(b.y - r * 0.45), 1, 1); } function guide(ctx, c) { const r = releasePoint(c); // La freccia della mira c'è sempre, con la punta e un bordo scuro per staccarsi da rete e alberi; la parabola per // quanto concede il livello, fino al primo urto. const a = c.angle * Math.PI / 180, len = 22 + c.power * 18, tip = { x: r.x + Math.cos(a) * len, y: r.y - Math.sin(a) * len }; const arrow = () => { ctx.beginPath(); ctx.moveTo(r.x, r.y); ctx.lineTo(tip.x, tip.y); for (const s of [-1, 1]) { ctx.moveTo(tip.x, tip.y); ctx.lineTo(tip.x - Math.cos(a - s * 0.5) * 6, tip.y + Math.sin(a - s * 0.5) * 6); } ctx.stroke(); }; ctx.lineWidth = 3; ctx.strokeStyle = 'rgba(20,16,10,0.8)'; arrow(); ctx.lineWidth = 1; ctx.strokeStyle = '#fff6d2'; arrow(); if (c.skill.preview > 0) { const pts = guidePoints(c, c.skill.preview); for (const [width, color] of [[3, 'rgba(20,16,10,0.6)'], [1, 'rgba(143,199,255,0.95)']]) { ctx.lineWidth = width; ctx.strokeStyle = color; ctx.setLineDash([2, 4]); ctx.beginPath(); for (const [i, p] of pts.entries()) { if (i) ctx.lineTo(p.x, p.y); else ctx.moveTo(p.x, p.y); } ctx.stroke(); ctx.setLineDash([]); } } ctx.fillStyle = 'rgba(20,16,10,0.6)'; ctx.fillRect(r.x - 34, r.y - 16, 30, 11); text(ctx, `${c.angle.toFixed(c.angle % 1 ? 1 : 0)}°`, r.x - 6, r.y - 10, 6, '#fff4c9', 'right'); } function swish(ctx, c) { // Un lampo di righe attorno al ferro quando la palla è appena entrata. const b = c.ball; if (c.step !== 'flight' || !b?.scored || b.time - b.scoredAt > 0.3) return; const cx = (RIM.x1 + RIM.x2) / 2; ctx.strokeStyle = 'rgba(255,240,170,0.8)'; for (let i = 0; i < 6; i++) { const t = i / 6 * Math.PI * 2; ctx.beginPath(); ctx.moveTo(cx + Math.cos(t) * 20, RIM.y + Math.sin(t) * 8); ctx.lineTo(cx + Math.cos(t) * 30, RIM.y + Math.sin(t) * 13); ctx.stroke(); } } export function drawBasket(ctx, c, def, bg, sheet) { ctx.save(); if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#6d7a5a'; ctx.fillRect(0, 0, 640, 360); } // Le postazioni sul campo: un segno chiaro dove si tira ora, uno spento dove si è già stati. for (const [i, s] of SPOTS.entries()) { ctx.fillStyle = i === c.spot ? 'rgba(255,244,201,0.9)' : i < c.spot ? 'rgba(255,244,201,0.35)' : 'rgba(255,244,201,0.15)'; ctx.fillRect(s.x - 1, FLOOR + 2, 3, 3); } andrea(ctx, c, sheet); for (const [i, t] of c.trails.entries()) { ctx.fillStyle = `rgba(255,190,120,${0.3 * (1 - t.age / 0.5)})`; ctx.fillRect(Math.round(t.x) - 1, Math.round(t.y) - 1, i % 2 ? 2 : 3, i % 2 ? 2 : 3); } if (canThrow(c) || c.holding) guide(ctx, c); if (c.ball && c.step === 'flight') ball(ctx, c.ball); // ferma, la palla è già tornata in mano ad Andrea swish(ctx, c); panel(ctx, 182, 50, 440, 24); text(ctx, status(c, def.names), 402, 63, 6, '#fff0cb', 'center'); // I palloni rimasti e il conto. panel(ctx, 18, 86, 196, 58); text(ctx, 'PALLONI', 26, 98, 6, '#eed5a0'); for (let n = 0; n < BALLS; n++) { const x = 30 + n * 14, y = 113; if (n < left(c)) ball(ctx, { x, y, spin: 0 }, 4); else { ctx.strokeStyle = '#766e5c'; ctx.strokeRect(x - 3, y - 3, 6, 6); } } text(ctx, `${c.baskets} su ${c.skill.goal} per la gloria · ${c.points} punti`, 26, 131, 6, c.baskets >= c.skill.goal ? '#b9e5b3' : '#d7ccb4'); // La forza in basso a sinistra: a destra c'è il pulsante per uscire. if (canThrow(c) || c.holding) { panel(ctx, 18, 306, 176, 37); text(ctx, `FORZA ${Math.round(c.power * 100)}%`, 30, 318, 7); ctx.fillStyle = '#3e3e36'; ctx.fillRect(30, 331, 150, 5); ctx.fillStyle = '#b9d392'; ctx.fillRect(30, 331, Math.round(c.power * 150), 5); } if (c.step === 'result') { panel(ctx, 206, 91, 397, 70); text(ctx, c.result.winner === 'player' ? 'CHE MANO!' : 'DUE TIRI FATTI', 404, 108, 9, '#efd49e', 'center'); text(ctx, `${c.result.baskets} canestri su ${BALLS}, ${c.result.points} punti`, 404, 132, 9, c.result.winner === 'player' ? '#b9e5b3' : '#f1d9be', 'center'); text(ctx, c.result.winner === 'player' ? `Obiettivo ${c.result.goal}: fatto.` : `Per la gloria ne servivano ${c.result.goal}.`, 404, 150, 6, '#d6c7af', 'center'); } ctx.restore(); }