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:
2026-10-27 23:39:30 +01:00
co-authored by Claude Fable 5.1
parent f1de62f01e
commit 8b957c7cf7
6 changed files with 133 additions and 52 deletions
+6 -1
View File
@@ -208,7 +208,12 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso
`assets/raw/riferimenti/rattattu-locandina.png`, scaletta illeggibile). Al campo da basket del Circolo, «Fai due tiri» `assets/raw/riferimenti/rattattu-locandina.png`, scaletta illeggibile). Al campo da basket del Circolo, «Fai due tiri»
(`src/engine/basket.js`, vista di lato, sfondo e sprite di Andrea col pallone da Codex): da solo, dieci palloni, a ogni (`src/engine/basket.js`, vista di lato, sfondo e sprite di Andrea col pallone da Codex): da solo, dieci palloni, a ogni
canestro si arretra fino alla linea da tre; tre livelli (quanti canestri per la gloria, quanta parabola mostra la canestro si arretra fino alla linea da tre; tre livelli (quanti canestri per la gloria, quanta parabola mostra la
guida). Battute provvisorie. Idea di Andrea del 27 ottobre. guida). Battute provvisorie. Idea di Andrea del 27 ottobre. Revisione critica di Codex (27 ottobre) applicata per
i punti gravi (angolo iniziale 64° con fascia di forza buona da ogni postazione, palo e tabellone da entrambi i lati,
canestro che conta subito, pose senza doppio pallone, guida fino al primo urto, obiettivo sempre in vista). Restano
le sue idee: «Riprova» dal risultato e record per livello; Andrea che arretra camminando; battute che dicono perché
è uscita (corto, lungo, ferro); l'angolo anche nei comandi HTML; testi del canvas piccoli sul telefono;
`basket_vinto` che lasci traccia nel paese; pannelli più compatti.
- Fuori paese, dal campo secondario (a destra): le Mozzète → il ponticino → Coldaia → la falegnameria → Gabbiano; - Fuori paese, dal campo secondario (a destra): le Mozzète → il ponticino → Coldaia → la falegnameria → Gabbiano;
da Via Cipriani, indietro, Via Massorondinaio → campeggio. Sfondi da Street View, battute provvisorie. da Via Cipriani, indietro, Via Massorondinaio → campeggio. Sfondi da Street View, battute provvisorie.
- In Fortezza quattro ragazzi inventati si fanno le canne da pesca col bambù per il lago dell'Anatre (sprite di Codex, - In Fortezza quattro ragazzi inventati si fanno le canne da pesca col bambù per il lago dell'Anatre (sprite di Codex,
+8 -6
View File
@@ -210,12 +210,14 @@
Vince chi fa più punti nella mano.</p> Vince chi fa più punti nella mano.</p>
</details> </details>
<details id="basket-help" hidden><summary>Come si gioca</summary> <details id="basket-help" hidden><summary>Come si gioca</summary>
<p>Muovi il mouse o tocca il campo per scegliere l’angolo del tiro: la freccia parte dalle mani di Andrea. <p>Muovi il mouse sul campo per scegliere l’angolo del tiro: la freccia parte dalle mani di Andrea. Un clic (o un
Tieni premuto sul campo (o Spazio) per caricare la forza e lascia per tirare; oppure regola la forza e premi Tira. tocco) fissa la mira, così puoi raggiungere i comandi senza cambiarla; un altro clic la sposta. Tieni premuto sul
Frecce ← →: angolo; Maiusc + ← →: a mezzo grado; ↑ ↓: forza. Esc esce.</p> campo (o Spazio) e la forza riparte da zero e sale: lascia per tirare. Oppure regola la forza e premi Tira.
<p>Dieci palloni. Si comincia da sotto; a ogni canestro si arretra di una postazione, fino a dietro la linea da tre, Frecce ← →: angolo più teso o più alto; Maiusc + ← →: a mezzo grado; ↑ ↓: forza. Esc esce.</p>
dove un canestro vale tre punti invece di due. Il ferro e il tabellone rimandano la palla: entra anche sporca. <p>Dieci palloni. Si comincia da sotto; a ogni canestro si arretra di una postazione: tiro libero, media distanza,
Alla bona la guida mostra quasi tutta la parabola, Seria solo l’inizio, A fassi male niente.</p> da tre e da lontanissimo, dove un canestro vale tre punti invece di due. Il ferro e il tabellone rimandano la
palla: entra anche sporca, ma non da sotto. La gloria: 3 canestri alla bona, 5 seria, 7 a fassi male. Alla bona la
guida mostra la parabola fino al primo urto, Seria solo l’inizio, A fassi male niente.</p>
</details> </details>
<details id="biliardo-help" hidden><summary>Come si gioca</summary> <details id="biliardo-help" hidden><summary>Come si gioca</summary>
<button class="tutorial-open" type="button" data-tutorial="biliardo" hidden>Guarda il video</button> <button class="tutorial-open" type="button" data-tutorial="biliardo" hidden>Guarda il video</button>
+27 -16
View File
@@ -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'; import { text, panel } from './bocce-render.js';
const ANDREA = { scale: 0.66 }; 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) { export function andreaPose(c) {
if (c.holding) return 'load'; if (c.holding) return 'load';
if (c.step === 'flight' && c.ball.time < 0.45) return 'shoot'; if (c.step === 'flight') return c.ball.scored ? 'cheer' : 'shoot';
if ((c.step === 'settle' || (c.step === 'flight' && c.ball.scored)) && c.ball?.scored) return 'cheer'; if (c.step === 'settle' && c.ball?.scored) return 'cheer';
return 'hold'; return 'hold';
} }
function andrea(ctx, c, sheet) { function andrea(ctx, c, sheet) {
@@ -37,21 +38,31 @@ function ball(ctx, b, r = BALL) {
} }
function guide(ctx, c) { function guide(ctx, c) {
const r = releasePoint(c); const r = releasePoint(c);
// La freccia della mira c'è sempre; la parabola per quanto concede il livello. // La freccia della mira c'è sempre, con la punta e un bordo scuro per staccarsi da rete e alberi; la parabola per
const a = c.angle * Math.PI / 180, len = 22 + c.power * 18; // quanto concede il livello, fino al primo urto.
ctx.strokeStyle = 'rgba(255,246,210,0.9)'; ctx.lineWidth = 1; 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 };
ctx.beginPath(); ctx.moveTo(r.x, r.y); ctx.lineTo(r.x + Math.cos(a) * len, r.y - Math.sin(a) * len); ctx.stroke(); 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) { if (c.skill.preview > 0) {
ctx.setLineDash([2, 4]); ctx.strokeStyle = 'rgba(143,199,255,0.8)'; const pts = guidePoints(c, c.skill.preview);
ctx.beginPath(); for (const [width, color] of [[3, 'rgba(20,16,10,0.6)'], [1, 'rgba(143,199,255,0.95)']]) {
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.lineWidth = width; ctx.strokeStyle = color; ctx.setLineDash([2, 4]);
ctx.stroke(); ctx.setLineDash([]); 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) { function swish(ctx, c) {
// Un lampo di righe attorno al ferro quando la palla è appena entrata. // 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; const cx = (RIM.x1 + RIM.x2) / 2;
ctx.strokeStyle = 'rgba(255,240,170,0.8)'; 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(); } 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); panel(ctx, 182, 50, 440, 24);
text(ctx, status(c, def.names), 402, 63, 6, '#fff0cb', 'center'); text(ctx, status(c, def.names), 402, 63, 6, '#fff0cb', 'center');
// I palloni rimasti e il conto. // 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'); text(ctx, 'PALLONI', 26, 98, 6, '#eed5a0');
for (let n = 0; n < BALLS; n++) { for (let n = 0; n < BALLS; n++) {
const x = 30 + n * 14, y = 113; const x = 30 + n * 14, y = 113;
if (n < left(c)) ball(ctx, { x, y, spin: 0 }, 4); if (n < left(c)) ball(ctx, { x, y, spin: 0 }, 4);
else { ctx.strokeStyle = '#766e5c'; ctx.strokeRect(x - 3, y - 3, 6, 6); } 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. // La forza in basso a sinistra: a destra c'è il pulsante per uscire.
if (canThrow(c) || c.holding) { if (canThrow(c) || c.holding) {
panel(ctx, 18, 306, 176, 37); panel(ctx, 18, 306, 176, 37);
+44 -23
View File
@@ -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 // 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 // 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). // 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 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 = 230, V_MAX = 560, SWING = 1.4, BALLS = 10; 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.9, FLIGHT_MAX = 7; export const FIXED_STEP = 1 / 240, REST = 0.55, FLIGHT_MAX = 7;
export const RELEASE = { dx: 16, dy: -86 }; // da dove parte la palla rispetto ai piedi di Andrea 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 ANGLE = { min: 25, max: 78 };
export const SPOTS = [ 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: 320, name: 'dal tiro libero', points: 2 },
{ x: 240, name: 'da media distanza', points: 2 }, { x: 240, name: 'da media distanza', points: 2 },
{ x: 150, name: 'da tre', points: 3 }, { x: 150, name: 'da tre', points: 3 },
{ x: 90, name: 'da lontanissimo', 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 = { export const LEVELS = {
bona: { goal: 3, preview: 1.1 }, bona: { goal: 3, preview: 3 },
seria: { goal: 5, preview: 0.4 }, seria: { goal: 5, preview: 0.4 },
male: { goal: 7, preview: 0 }, male: { goal: 7, preview: 0 },
}; };
@@ -26,7 +27,7 @@ const rad = deg => deg * Math.PI / 180;
export function newBasket(seed = Date.now() >>> 0, def = {}) { export function newBasket(seed = Date.now() >>> 0, def = {}) {
const level = LEVELS[def.level] ? def.level : 'seria'; 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, 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 canThrow = c => c.step === 'aim' && c.thrown < BALLS;
export const left = c => Math.max(0, BALLS - c.thrown); 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 }); 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; 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) { export function shoot(c) {
if (!canThrow(c) || c.holding) return false; if (!canThrow(c) || c.holding) return false;
const r = releasePoint(c), v = velocity(c); 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.step = 'flight'; c.t = c.accumulator = 0; c.thrown++; c.trails = [];
c.events.push({ kind: 'basket-throw', power: c.power }); c.events.push({ kind: 'basket-throw', power: c.power });
return true; return true;
@@ -90,23 +102,29 @@ function rimEnd(c, b, px, py) {
if (closing < 0) { if (closing < 0) {
const tx = -ny, ty = nx, vt = (b.vx * tx + b.vy * ty) * 0.92, vn = -closing * 0.58; 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.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) { function physics(c, dt) {
const b = c.ball, before = b.y; 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; 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. // Il tabellone: una lastra verticale che guarda Andrea, ma rimanda anche una palla che torna da dietro.
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) { // Il palo dietro ferma quel che passa oltre, dalla sua cima in giù: sopra, la palla vola via e ricade.
b.x = BOARD.x - BALL; b.vx = -b.vx * 0.6; b.board = true; hit(c, 'basket-board', Math.abs(b.vx) / 300); 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); 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. // 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; 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) { 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); 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; } 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.scored && b.y < NET + BALL) { b.vx *= 1 - 2.5 * dt; b.vy *= 1 - 1.2 * dt; }
if (b.y + BALL > FLOOR) { 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); } 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; } 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) { 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 }); 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 (b.scored) c.spot = Math.min(c.spot + 1, SPOTS.length - 1);
if (c.thrown >= BALLS) { if (c.thrown >= BALLS) { if (!c.result) finish(c); c.step = 'result'; } else c.step = 'aim';
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';
c.t = 0; c.t = 0;
} }
export function update(c, dt) { export function update(c, dt) {
@@ -141,7 +162,7 @@ export function update(c, dt) {
c.trails = c.trails.filter(t => t.age < 0.5); c.trails = c.trails.filter(t => t.age < 0.5);
} }
export function status(c, names = { player: 'Andrea' }) { 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 === '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 === 'flight') return c.ball.scored ? `Dentro! ${tally}` : 'Palla in aria…';
if (c.step === 'settle') return c.ball.scored ? `Canestro! ${tally}` : `Fuori. ${tally}`; if (c.step === 'settle') return c.ball.scored ? `Canestro! ${tally}` : `Fuori. ${tally}`;
+2 -2
View File
@@ -497,8 +497,8 @@ canvas.addEventListener('pointerdown', e => {
return; return;
} }
const aimed = game.closeupAim(scenePoint(e)); const aimed = game.closeupAim(scenePoint(e));
if (story.closeups[game.closeup.id].type === 'biliardo') { if (['biliardo', 'basket'].includes(story.closeups[game.closeup.id].type)) {
if (!aimed && !game.speech) return; if (!aimed && !game.speech) return; // dove non si può mirare (dietro Andrea) non si carica nemmeno
if (aimed) game.closeup.aimLocked = true; // il clic fissa la mira if (aimed) game.closeup.aimLocked = true; // il clic fissa la mira
} }
if (['bocce', 'basket'].includes(story.closeups[game.closeup.id].type) && aimed) game.closeup.aimLocked = true; if (['bocce', 'basket'].includes(story.closeups[game.closeup.id].type) && aimed) game.closeup.aimLocked = true;
+46 -4
View File
@@ -1,7 +1,7 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { newBasket, press, release, cancel, update, shoot, setAngle, setPower, aimScreen, nudge, arc, status, canThrow, releasePoint, import { newBasket, press, release, cancel, update, shoot, setAngle, setPower, aimScreen, nudge, arc, guide, status, canThrow, releasePoint,
powerAt, SWING, BALLS, SPOTS, LEVELS, RIM, BOARD, FLOOR, BALL, ANGLE } from '../src/engine/basket.js'; powerAt, SWING, BALLS, SPOTS, LEVELS, RIM, BOARD, POLE, FLOOR, BALL, ANGLE } from '../src/engine/basket.js';
import { story } from '../src/story/story.js'; import { story } from '../src/story/story.js';
import { Game } from '../src/engine/game.js'; import { Game } from '../src/engine/game.js';
import { settle } from './helpers.mjs'; import { settle } from './helpers.mjs';
@@ -43,7 +43,7 @@ test('la mira: l\'angolo segue il punto toccato, resta fra i limiti, non si tira
}); });
test('la parabola della guida comincia dalle mani e scende per la gravità', () => { test('la parabola della guida comincia dalle mani e scende per la gravità', () => {
const c = newBasket(3), r = releasePoint(c), pts = arc(c, 1); const c = newBasket(3), r = releasePoint(c), pts = arc(c, 1.5);
assert.deepEqual(pts[0], r); assert.deepEqual(pts[0], r);
assert.ok(pts[5].y < r.y, 'prima sale'); assert.ok(pts[5].y < r.y, 'prima sale');
assert.ok(pts.at(-1).y > pts[5].y, 'poi scende'); assert.ok(pts.at(-1).y > pts[5].y, 'poi scende');
@@ -65,6 +65,48 @@ test('da ogni postazione esiste un tiro che entra, e un tiro entrato conta canes
} }
}); });
test('con l\'angolo iniziale, da ogni postazione entra in una fascia di forza contigua di almeno l\'8%', () => {
for (const [i, s] of SPOTS.entries()) {
const made = [];
for (let p = 0; p <= 1.0001; p += 0.005) {
const c = newBasket(1); c.spot = i; setPower(c, p); shoot(c);
made.push(fly(c).scored);
}
let best = 0, run = 0;
for (const v of made) { run = v ? run + 1 : 0; best = Math.max(best, run); }
assert.ok(best >= 16, `${s.name}: la fascia migliore è ${(best - 1) * 0.5}%`);
}
});
test('il palo ferma solo dalla cima in giù; il tabellone rimanda anche da dietro; il canestro conta subito', () => {
const c = newBasket(11); setAngle(c, 78); setPower(c, 1); shoot(c);
let b = fly(c);
assert.ok(!c.events.some(e => e.kind === 'basket-swish'), 'sopra il palo la palla vola via: non è un tiro pulito');
const d = newBasket(12);
d.ball = { x: POLE.x - 30, y: FLOOR - 30, vx: 300, vy: 0, time: 0, bounces: 0, rim: false, board: false, under: false, scored: false, dead: false, spin: 0 };
d.step = 'flight'; for (let i = 0; i < 30; i++) update(d, 1 / 120);
assert.ok(d.ball.board && d.ball.vx < 0, 'il palo la rimanda indietro come un tabellone');
const e = newBasket(13);
e.ball = { x: BOARD.x + BOARD.depth + 30, y: (BOARD.top + BOARD.bottom) / 2, vx: -300, vy: 0, time: 0, bounces: 0, rim: false, board: false, under: false, scored: false, dead: false, spin: 0 };
e.step = 'flight'; for (let i = 0; i < 30; i++) update(e, 1 / 120);
assert.ok(e.ball.board && e.ball.x > BOARD.x, 'da dietro non lo attraversa');
const f = newBasket(14); f.thrown = BALLS - 1; f.skill = LEVELS.bona; f.baskets = 2;
const shot = goodShot(f); setAngle(f, shot.angle); setPower(f, shot.power); shoot(f);
for (let i = 0; i < 2000 && !f.ball.scored; i++) update(f, 1 / 120);
assert.equal(f.baskets, 3, 'il canestro conta appena entra');
assert.ok(typeof f.ball.scoredAt === 'number');
assert.equal(f.result?.winner, 'player', 'all\'ultimo pallone il risultato c\'è già mentre la palla scende');
fly(f); rest(f);
assert.equal(f.step, 'result'); assert.equal(f.result.baskets, 3);
});
test('la guida si ferma al primo urto', () => {
const c = newBasket(15); c.spot = 1; setAngle(c, 30); setPower(c, 1);
const g = guide(c, 3), last = g.at(-1);
assert.ok(g.length < arc(c, 3).length, 'non arriva in fondo');
assert.ok(last.x + BALL > BOARD.x - 2 || last.y + BALL > FLOOR - 2 || Math.hypot(last.x - RIM.x1, last.y - RIM.y) < BALL + 3, 'si ferma su tabellone, campo o ferro');
});
test('un tiro corto batte sul campo e rimbalza; uno lungo prende il tabellone e torna indietro', () => { test('un tiro corto batte sul campo e rimbalza; uno lungo prende il tabellone e torna indietro', () => {
const c = newBasket(4); c.spot = 3; // da tre, piano: non arriva nemmeno al palo const c = newBasket(4); c.spot = 3; // da tre, piano: non arriva nemmeno al palo
setAngle(c, 45); setPower(c, 0); shoot(c); setAngle(c, 45); setPower(c, 0); shoot(c);
@@ -73,7 +115,7 @@ test('un tiro corto batte sul campo e rimbalza; uno lungo prende il tabellone e
assert.ok(c.events.some(e => e.kind === 'basket-bounce')); assert.ok(c.events.some(e => e.kind === 'basket-bounce'));
assert.ok(b.y + BALL <= FLOOR + 0.01, 'non sprofonda'); assert.ok(b.y + BALL <= FLOOR + 0.01, 'non sprofonda');
const d = newBasket(5); d.spot = 1; const d = newBasket(5); d.spot = 1;
setAngle(d, 30); setPower(d, 1); shoot(d); setAngle(d, 34); setPower(d, 1); shoot(d);
b = fly(d); b = fly(d);
assert.ok(b.board, 'ha preso il tabellone'); assert.ok(b.board, 'ha preso il tabellone');
assert.ok(c.events.every(e => e.kind !== 'basket-board') && d.events.some(e => e.kind === 'basket-board')); assert.ok(c.events.every(e => e.kind !== 'basket-board') && d.events.some(e => e.kind === 'basket-board'));