// «Fai due tiri»: Andrea da solo al campo del Circolo 4 Maggio (idea di Andrea, 27 ottobre). Vista di lato, nelle // coordinate dello schermo (640x360): x verso il canestro, y verso il basso. Un pixel è circa 2,3 cm: il ferro, a // 3,05 m, sta 133 px sopra il campo. Dieci palloni; a ogni canestro si arretra di una postazione, fino a quella da // tre. Niente avversario: il risultato è quanti canestri su dieci, e i punti (due, tre da dietro la linea). export const FLOOR = 296, RIM = { x1: 482, x2: 518, y: 163, r: 2 }, BOARD = { x: 519, top: 118, bottom: 178, depth: 8 }, NET = 190, POLE = { x: 562, top: 112 }; export const BALL = 8, G = 560, V_MIN = 140, V_MAX = 590, SWING = 1.4, BALLS = 10; export const FIXED_STEP = 1 / 240, REST = 0.55, FLIGHT_MAX = 7; export const RELEASE = { dx: 18, dy: -72 }; // da dove parte la palla rispetto ai piedi di Andrea: sopra la testa, nel salto export const ANGLE = { min: 25, max: 78 }; export const SPOTS = [ { x: 400, name: 'da sotto', points: 2 }, { x: 320, name: 'dal tiro libero', points: 2 }, { x: 240, name: 'da media distanza', points: 2 }, { x: 150, name: 'da tre', points: 3 }, { x: 90, name: 'da lontanissimo', points: 3 }, ]; // I livelli: quanti canestri su dieci valgono la gloria, e per quanto tempo di volo la guida mostra la parabola // (alla bona fino al primo urto, che è quasi tutta). export const LEVELS = { bona: { goal: 3, preview: 3 }, seria: { goal: 5, preview: 0.4 }, male: { goal: 7, preview: 0 }, }; const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); const rad = deg => deg * Math.PI / 180; export function newBasket(seed = Date.now() >>> 0, def = {}) { const level = LEVELS[def.level] ? def.level : 'seria'; return { step: 'aim', t: 0, age: 0, seed, level, skill: LEVELS[level], current: 'player', spot: 0, thrown: 0, baskets: 0, points: 0, angle: 64, power: 0.5, holding: false, holdTime: 0, accumulator: 0, ball: null, events: [], trails: [], history: [] }; } export const canThrow = c => c.step === 'aim' && c.thrown < BALLS; export const left = c => Math.max(0, BALLS - c.thrown); export const spot = c => SPOTS[c.spot]; export const powerAt = time => clamp(time / SWING, 0, 1); export const speedFor = power => V_MIN + (V_MAX - V_MIN) * clamp(power, 0, 1); export function releasePoint(c) { const s = spot(c); return { x: s.x + RELEASE.dx, y: FLOOR + RELEASE.dy }; } export function velocity(c) { const v = speedFor(c.power), a = rad(c.angle); return { vx: v * Math.cos(a), vy: -v * Math.sin(a) }; } export function setPower(c, value) { if (!canThrow(c) || c.holding || !Number.isFinite(value)) return false; c.power = clamp(value, 0, 1); return true; } export function setAngle(c, deg) { if (!canThrow(c) || c.holding || !Number.isFinite(deg)) return false; c.angle = clamp(deg, ANGLE.min, ANGLE.max); return true; } export const nudge = (c, dir, fine = false) => setAngle(c, c.angle + dir * (fine ? 0.5 : 2)); // La mira segue il punto toccato: l'angolo fra la mano di Andrea e il dito. Dietro di lui non si tira. export function aimScreen(c, [x, y]) { if (!Number.isFinite(x) || !Number.isFinite(y)) return false; const r = releasePoint(c); if (x <= r.x + 4) return false; return setAngle(c, Math.atan2(r.y - y, x - r.x) * 180 / Math.PI); } // La parabola libera (senza urti) per `seconds` di volo: la guida la mostra per il tempo che il livello concede. export function arc(c, seconds, every = 0.04) { const r = releasePoint(c), v = velocity(c), out = []; for (let t = 0; t <= seconds; t += every) out.push({ x: r.x + v.vx * t, y: r.y + v.vy * t + 0.5 * G * t * t }); return out; } // La guida: la parabola libera finché non tocca qualcosa (ferro, tabellone, palo, campo), al massimo `seconds`. export function guide(c, seconds) { const out = []; for (const p of arc(c, seconds)) { const touches = p.y + BALL > FLOOR || [RIM.x1, RIM.x2].some(x => Math.hypot(p.x - x, p.y - RIM.y) < BALL + RIM.r) || (p.x + BALL > BOARD.x && p.y + BALL > BOARD.top && p.y - BALL < BOARD.bottom) || (p.x + BALL > POLE.x && p.y + BALL > POLE.top); out.push(p); if (touches) break; } return out; } export function shoot(c) { if (!canThrow(c) || c.holding) return false; const r = releasePoint(c), v = velocity(c); c.ball = { x: r.x, y: r.y, vx: v.vx, vy: v.vy, time: 0, bounces: 0, rim: false, board: false, under: false, scored: false, scoredAt: null, dead: false, spin: 0 }; c.step = 'flight'; c.t = c.accumulator = 0; c.thrown++; c.trails = []; c.events.push({ kind: 'basket-throw', power: c.power }); return true; } export function press(c) { if (!canThrow(c) || c.holding) return false; c.previousPower = c.power; c.holding = true; c.holdTime = c.power = 0; return true; } export function cancel(c) { if (!c.holding) return false; c.holding = false; c.power = c.previousPower; c.holdTime = 0; return true; } export function release(c) { if (!c.holding) return false; if (c.holdTime < 0.14) return cancel(c); // un tocco breve serve a mirare, non spreca un pallone c.holding = false; return shoot(c); } function hit(c, kind, strength) { if (c.events.length < 12) c.events.push({ kind, power: clamp(strength, 0, 1) }); } // Il ferro visto di lato sono due punti: il davanti e il dietro dell'anello. La palla ci rimbalza come su un tondino. function rimEnd(c, b, px, py) { const dx = b.x - px, dy = b.y - py, d = Math.hypot(dx, dy), r = BALL + RIM.r; if (d >= r || d < 1e-9) return; const nx = dx / d, ny = dy / d, closing = b.vx * nx + b.vy * ny; b.x = px + nx * r; b.y = py + ny * r; if (closing < 0) { const tx = -ny, ty = nx, vt = (b.vx * tx + b.vy * ty) * 0.92, vn = -closing * 0.58; b.vx = vn * nx + vt * tx; b.vy = vn * ny + vt * ty; b.rim = true; if (-closing > 25) hit(c, 'basket-rim', -closing / 400); // i microurti di una palla che si posa non tintinnano } } function physics(c, dt) { const b = c.ball, before = b.y; b.vy += G * dt; b.x += b.vx * dt; b.y += b.vy * dt; b.time += dt; b.spin += b.vx * dt / BALL; // Il tabellone: una lastra verticale che guarda Andrea, ma rimanda anche una palla che torna da dietro. // Il palo dietro ferma quel che passa oltre, dalla sua cima in giù: sopra, la palla vola via e ricade. if (b.y + BALL > BOARD.top && b.y - BALL < BOARD.bottom) { if (b.vx > 0 && b.x + BALL > BOARD.x && b.x - BALL < BOARD.x + BOARD.depth) { b.x = BOARD.x - BALL; b.vx = -b.vx * 0.6; b.board = true; hit(c, 'basket-board', Math.abs(b.vx) / 300); } else if (b.vx < 0 && b.x - BALL < BOARD.x + BOARD.depth && b.x + BALL > BOARD.x) { b.x = BOARD.x + BOARD.depth + BALL; b.vx = -b.vx * 0.6; b.board = true; hit(c, 'basket-board', Math.abs(b.vx) / 300); } } if (b.x + BALL > POLE.x && b.y + BALL > POLE.top) { b.x = POLE.x - BALL; if (b.vx > 0) { b.vx = -b.vx * 0.5; b.board = true; hit(c, 'basket-board', 0.3); } } rimEnd(c, b, RIM.x1, RIM.y); rimEnd(c, b, RIM.x2, RIM.y); // Il canestro: la palla attraversa l'anello scendendo, fra il davanti e il dietro, e non da sotto. const inside = b.x > RIM.x1 + BALL * 0.6 && b.x < RIM.x2 - BALL * 0.6; if (inside && before <= RIM.y && b.y > RIM.y && b.vy > 0 && !b.scored && !b.under) { b.scored = true; b.scoredAt = b.time; b.vx *= 0.35; b.vy *= 0.5; hit(c, b.rim || b.board ? 'basket-in' : 'basket-swish', 0.7); // Il canestro conta subito: il punteggio e, all'ultimo pallone, il risultato non aspettano la palla ferma. c.baskets++; c.points += spot(c).points; if (c.thrown >= BALLS) finish(c); } else if (inside && before >= RIM.y && b.y < RIM.y && b.vy < 0) b.under = true; if (b.scored && b.y < NET + BALL) { b.vx *= 1 - 2.5 * dt; b.vy *= 1 - 1.2 * dt; } if (b.y + BALL > FLOOR) { b.y = FLOOR - BALL; if (b.vy > 40) { b.vy = -b.vy * 0.55; b.vx *= 0.85; b.bounces++; hit(c, 'basket-bounce', b.vy / -400); } else { b.vy = 0; b.vx *= 1 - 3 * dt; } } const still = Math.hypot(b.vx, b.vy) < 10; // Entrata, la palla è finita: non si aspetta che rotoli. Una palla posata sul ferro, nemmeno. if (b.x < -20 || b.time > FLIGHT_MAX || b.bounces > 6 || (b.y + BALL >= FLOOR - 0.5 && Math.abs(b.vx) < 6 && b.vy === 0) || (b.scored && b.y > NET + 30) || (still && b.time > 1.5)) b.dead = true; } function finish(c) { c.result = { winner: c.baskets >= c.skill.goal ? 'player' : 'cpu', baskets: c.baskets, points: c.points, goal: c.skill.goal }; } function nextBall(c) { const b = c.ball; c.history.push({ spot: c.spot, scored: b.scored, rim: b.rim, board: b.board }); if (b.scored) c.spot = Math.min(c.spot + 1, SPOTS.length - 1); if (c.thrown >= BALLS) { if (!c.result) finish(c); c.step = 'result'; } else c.step = 'aim'; c.t = 0; } export function update(c, dt) { if (!Number.isFinite(dt) || dt <= 0) return; dt = Math.min(dt, 0.25); c.t += dt; if (c.holding) { c.holdTime += dt; c.power = powerAt(c.holdTime); } if (c.step === 'flight') { c.accumulator += dt; while (c.accumulator >= FIXED_STEP && !c.ball.dead) { physics(c, FIXED_STEP); c.accumulator -= FIXED_STEP; } const b = c.ball; if (!b.dead && Math.hypot(b.vx, b.vy) > 60) { c.trails.push({ x: b.x, y: b.y, age: 0 }); if (c.trails.length > 14) c.trails.shift(); } if (b.dead) { c.step = 'settle'; c.t = 0; } } else if (c.step === 'settle' && c.t >= REST) nextBall(c); for (const t of c.trails) t.age += dt; c.trails = c.trails.filter(t => t.age < 0.5); } export function status(c, names = { player: 'Andrea' }) { const tally = `Canestri ${c.baskets} (ne servono ${c.skill.goal}), punti ${c.points}.`; if (c.step === 'result') return `${c.result.baskets} canestri su ${BALLS}, ${c.result.points} punti. ${c.result.winner === 'player' ? `Obiettivo ${c.result.goal}: fatto!` : `Ne servivano ${c.result.goal}.`}`; if (c.step === 'flight') return c.ball.scored ? `Dentro! ${tally}` : 'Palla in aria…'; if (c.step === 'settle') return c.ball.scored ? `Canestro! ${tally}` : `Fuori. ${tally}`; return `${names.player}, pallone ${c.thrown + 1} di ${BALLS}, ${spot(c).name}. ${tally}`; }