Dalla mira iniziale (52°) da sotto non entrava con nessuna forza: ora 64°, forza minima più bassa e prima postazione un passo indietro, così da ogni postazione entra in una fascia di forza contigua di almeno l'8% (test). Il palo fermava anche sopra la sua cima e contava come tiro pulito; il tabellone lasciava passare da dietro. Il canestro conta appena entra (anche il risultato all'ultimo pallone), la pausa fra un pallone e l'altro è più corta, la palla entrata non rotola fino in fondo. Andrea resta a braccia alzate finché la palla vola (niente doppio pallone), il rilascio parte sopra la sua testa. La guida si ferma al primo urto e ha un bordo scuro con la punta; il lampo del canestro si spegne; l'obiettivo del livello è sempre in vista; i microurti sul ferro non tintinnano; dietro Andrea non si carica; l'aiuto spiega il clic che fissa la mira e le soglie. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
114 lines
6.7 KiB
JavaScript
114 lines
6.7 KiB
JavaScript
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();
|
|
}
|