Basket: la revisione di Codex, i punti gravi
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
This commit is contained in:
+27
-16
@@ -1,12 +1,13 @@
|
||||
import { FLOOR, RIM, BALL, BALLS, SPOTS, canThrow, left, spot, releasePoint, arc, status } from './basket.js';
|
||||
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, il salto appena lasciata la palla, l'esultanza quando entra.
|
||||
// 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' && c.ball.time < 0.45) return 'shoot';
|
||||
if ((c.step === 'settle' || (c.step === 'flight' && c.ball.scored)) && c.ball?.scored) return 'cheer';
|
||||
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) {
|
||||
@@ -37,21 +38,31 @@ function ball(ctx, b, r = BALL) {
|
||||
}
|
||||
function guide(ctx, c) {
|
||||
const r = releasePoint(c);
|
||||
// La freccia della mira c'è sempre; la parabola per quanto concede il livello.
|
||||
const a = c.angle * Math.PI / 180, len = 22 + c.power * 18;
|
||||
ctx.strokeStyle = 'rgba(255,246,210,0.9)'; ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.moveTo(r.x, r.y); ctx.lineTo(r.x + Math.cos(a) * len, r.y - Math.sin(a) * len); ctx.stroke();
|
||||
// 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) {
|
||||
ctx.setLineDash([2, 4]); ctx.strokeStyle = 'rgba(143,199,255,0.8)';
|
||||
ctx.beginPath();
|
||||
for (const [i, p] of arc(c, c.skill.preview).entries()) { if (p.y > FLOOR) break; if (i) ctx.lineTo(p.x, p.y); else ctx.moveTo(p.x, p.y); }
|
||||
ctx.stroke(); ctx.setLineDash([]);
|
||||
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([]);
|
||||
}
|
||||
}
|
||||
text(ctx, `${Math.round(c.angle)}°`, r.x - 6, r.y - 10, 6, '#fff4c9', 'right');
|
||||
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 (!b?.scored || b.time - (b.scoredAt ?? b.time) > 0.3) return;
|
||||
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(); }
|
||||
@@ -77,14 +88,14 @@ export function drawBasket(ctx, c, def, bg, sheet) {
|
||||
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, 156, 58);
|
||||
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} canestri · ${c.points} punti`, 26, 131, 6, '#d7ccb4');
|
||||
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);
|
||||
|
||||
+44
-23
@@ -2,21 +2,22 @@
|
||||
// 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 }, NET = 190, POLE = 562;
|
||||
export const BALL = 8, G = 560, V_MIN = 230, V_MAX = 560, SWING = 1.4, BALLS = 10;
|
||||
export const FIXED_STEP = 1 / 240, REST = 0.9, FLIGHT_MAX = 7;
|
||||
export const RELEASE = { dx: 16, dy: -86 }; // da dove parte la palla rispetto ai piedi di Andrea
|
||||
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: 420, name: 'da sotto', points: 2 },
|
||||
{ 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.
|
||||
// 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: 1.1 },
|
||||
bona: { goal: 3, preview: 3 },
|
||||
seria: { goal: 5, preview: 0.4 },
|
||||
male: { goal: 7, preview: 0 },
|
||||
};
|
||||
@@ -26,7 +27,7 @@ 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: 52, power: 0.5, holding: false, holdTime: 0, accumulator: 0, ball: null, events: [], trails: [], history: [] };
|
||||
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);
|
||||
@@ -58,10 +59,21 @@ export function arc(c, seconds, every = 0.04) {
|
||||
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, dead: false, spin: 0 };
|
||||
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;
|
||||
@@ -90,23 +102,29 @@ function rimEnd(c, b, px, py) {
|
||||
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; hit(c, 'basket-rim', -closing / 400);
|
||||
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. Il palo dietro ferma quel che passa oltre.
|
||||
if (b.vx > 0 && b.x + BALL > BOARD.x && b.x - BALL < BOARD.x + 8 && b.y + BALL > BOARD.top && b.y - BALL < BOARD.bottom) {
|
||||
b.x = BOARD.x - BALL; b.vx = -b.vx * 0.6; b.board = true; hit(c, 'basket-board', Math.abs(b.vx) / 300);
|
||||
// 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) { b.x = POLE - BALL; if (b.vx > 0) { b.vx = -b.vx * 0.5; hit(c, 'basket-board', 0.3); } }
|
||||
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.vx *= 0.35; b.vy *= 0.5;
|
||||
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) {
|
||||
@@ -114,16 +132,19 @@ function physics(c, dt) {
|
||||
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; }
|
||||
}
|
||||
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.dead = true;
|
||||
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, s = spot(c);
|
||||
const b = c.ball;
|
||||
c.history.push({ spot: c.spot, scored: b.scored, rim: b.rim, board: b.board });
|
||||
if (b.scored) { c.baskets++; c.points += s.points; c.spot = Math.min(c.spot + 1, SPOTS.length - 1); }
|
||||
if (c.thrown >= BALLS) {
|
||||
c.step = 'result';
|
||||
c.result = { winner: c.baskets >= c.skill.goal ? 'player' : 'cpu', baskets: c.baskets, points: c.points, goal: c.skill.goal };
|
||||
} else c.step = 'aim';
|
||||
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) {
|
||||
@@ -141,7 +162,7 @@ export function update(c, dt) {
|
||||
c.trails = c.trails.filter(t => t.age < 0.5);
|
||||
}
|
||||
export function status(c, names = { player: 'Andrea' }) {
|
||||
const tally = `Canestri ${c.baskets}, punti ${c.points}.`;
|
||||
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}`;
|
||||
|
||||
Reference in New Issue
Block a user