Il flipper rivisto con la revisione di Codex: punti solo a un urto nuovo, sottopassi contro gli attraversamenti, molla che arriva sempre in campo (senza cancelletto), salvataggio col tempo della simulazione e in vista, corsie che contano attraversando la riga, traguardo acquisito appena raggiunto, annullare non lancia, due dita e tasto destro, pulsanti dei flipper sempre attivi
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
+161
-87
@@ -1,23 +1,30 @@
|
||||
// Il flipper del River (Andrea, 31 ottobre: «una tavola bellissima, con San Piero dentro»). Visto dall'alto, la tavola
|
||||
// in verticale al centro del primo piano 640×360; la pallina parte dalla corsia di destra con la molla, fa il giro
|
||||
// dell'arco in cima, passa in una delle quattro corsie dei rioni, rimbalza sui tre funghi (i bastioni della Fortezza),
|
||||
// abbatte i bersagli della Pieve a sinistra e della torre a destra, tocca il Regolo in mezzo, e giù dalle sponde
|
||||
// elastiche ai due flipper, con la Sieve dipinta che scorre nel buco in mezzo. Tre palline; si vince arrivando al
|
||||
// traguardo di punti del livello. Unità: i pixel del primo piano. La fisica va a passo fisso (come il biliardo).
|
||||
// abbatte i bersagli della Pieve a sinistra e della torre a destra, tocca il Regolo, e giù dalle sponde elastiche ai
|
||||
// due flipper, con la Sieve dipinta che scorre nel buco in mezzo. Tre palline; si vince arrivando al traguardo di
|
||||
// punti del livello (e poi si può continuare per il record). Unità: i pixel del primo piano. La fisica va a passo
|
||||
// fisso (come il biliardo), con sottopassi quando pallina e flipper vanno forte.
|
||||
// Rivista con la revisione di Codex del 31 ottobre: punti solo a un urto nuovo (non al contatto che dura), due dita
|
||||
// sulla scena, annullare non lancia, niente cancelletto-ripiano, salvataggio col tempo della simulazione, corsie che
|
||||
// contano quando si attraversano, traguardo acquisito appena raggiunto.
|
||||
export const FIELD = { x0: 200, x1: 448, y0: 5, y1: 336 }, LANE = { x0: 432, x1: 448 }, R = 4.5;
|
||||
export const BALLS = 3, STEP = 1 / 240, MAX_SPEED = 980, LAUNCH = [420, 900], CHARGE = 1.2, FLIP = { len: 34, rest: 0.5, up: -0.46, speed: 26, r: 3 };
|
||||
export const BALLS = 3, STEP = 1 / 240, MAX_SPEED = 980, CHARGE = 1.2, FLIP = { len: 34, rest: 0.5, up: -0.46, speed: 26, r: 3 };
|
||||
export const PIVOT = { left: [280, 314], right: [360, 314] };
|
||||
export const SCORE = { bumper: 100, sling: 50, lane: 200, palio: 3000, target: 500, bank: 2000, regolo: 1000 };
|
||||
// I livelli: la gravità, quanti secondi dura il salvataggio della pallina appena lanciata, la spinta dei funghi, il traguardo.
|
||||
// Quanto deve picchiare un urto perché conti (velocità lungo la normale, px/s): il tremolio da gravità non conta.
|
||||
const IMPACT = 40, LANE_LINE = 104, BANK_RESET = 0.8, CLICK_HOLD = 0.16, NUDGE_AFTER = 2.5;
|
||||
// I livelli: la stessa fisica, quasi; cambiano il traguardo, il salvataggio della pallina appena lanciata e un po' la
|
||||
// gravità. La spinta dei funghi è uguale per tutti.
|
||||
export const LEVELS = {
|
||||
bona: { g: 620, save: 8, kick: 300, goal: 6000 },
|
||||
seria: { g: 780, save: 3, kick: 340, goal: 12000 },
|
||||
male: { g: 880, save: 0, kick: 380, goal: 25000 },
|
||||
bona: { g: 680, save: 8, kick: 340, goal: 6000 },
|
||||
seria: { g: 740, save: 4, kick: 340, goal: 10000 },
|
||||
male: { g: 800, save: 1, kick: 340, goal: 16000 },
|
||||
};
|
||||
export const RIONI = ['piazza', 'pieve', 'cardetole', 'campomigliaio'];
|
||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
|
||||
const seg = (a, b, extra = {}) => ({ a, b, e: 0.55, ...extra });
|
||||
const seg = (a, b, extra = {}) => ({ a, b, e: 0.55, touching: false, ...extra });
|
||||
|
||||
// La tavola: sponde, corsie, sponde elastiche, funghi, bersagli. Le coordinate seguono il disegno di Codex
|
||||
// (assets/closeups/river-flipper.png): i tre dischi dei funghi, le quattro bandiere dei rioni, la corsia della molla.
|
||||
@@ -26,12 +33,13 @@ export function layout() {
|
||||
const walls = [];
|
||||
for (let i = 1; i < arc.length; i++) walls.push(seg(arc[i - 1], arc[i], { e: 0.6 }));
|
||||
walls.push(seg([200, 72], [200, 336]), seg([448, 72], [448, 336]));
|
||||
// La corsia della molla, e in cima il cancelletto che non fa tornare indietro la pallina.
|
||||
walls.push(seg([432, 70], [432, 336]), seg([432, 70], [448, 70], { gate: true }), seg([432, 326], [448, 326], { e: 0.2 }));
|
||||
// La corsia della molla, col fondo. Niente cancelletto: una pallina che ricasca nella corsia si rilancia e basta
|
||||
// (il cancelletto a senso unico faceva da ripiano ai lanci a metà).
|
||||
walls.push(seg([432, 70], [432, 336]), seg([432, 326], [448, 326], { e: 0.2 }));
|
||||
// Le corsie dei rioni, in cima, fra le bandiere: tre guide.
|
||||
for (const x of [277, 319, 361]) walls.push(seg([x, 70], [x, 102], { e: 0.4 }));
|
||||
// I divisori delle uscite laterali e le guide che portano ai flipper.
|
||||
// (La guida passa sopra il perno e continua un po' lungo il flipper: finendo sul perno la pallina ci restava incastrata.)
|
||||
// I divisori delle uscite laterali e le guide che portano ai flipper (la guida passa sopra il perno e continua un
|
||||
// po' lungo il flipper: finendo sul perno la pallina ci restava incastrata).
|
||||
walls.push(seg([222, 240], [222, 290]), seg([222, 290], [292, 314], { e: 0.3 }));
|
||||
walls.push(seg([418, 240], [418, 290]), seg([418, 290], [348, 314], { e: 0.3 }));
|
||||
// Le sponde elastiche sopra i flipper: il lato lungo spinge.
|
||||
@@ -40,50 +48,73 @@ export function layout() {
|
||||
// I bersagli a caduta: la Pieve a sinistra, la torre a destra, con la sponda dietro.
|
||||
walls.push(seg([210, 146], [210, 198]), seg([429, 146], [429, 198]));
|
||||
const targets = [];
|
||||
for (const [bank, x] of [['pieve', 216], ['torre', 423]]) for (let i = 0; i < 3; i++) targets.push({ bank, i, a: [x, 150 + i * 16], b: [x, 162 + i * 16], down: false });
|
||||
const bumpers = [[281, 120], [360, 120], [320, 153]].map(([x, y]) => ({ x, y, r: 12, hit: 0 }));
|
||||
for (const [bank, x] of [['pieve', 216], ['torre', 423]]) for (let i = 0; i < 3; i++) targets.push({ bank, i, a: [x, 150 + i * 16], b: [x, 162 + i * 16], down: false, touching: false });
|
||||
const bumpers = [[281, 120], [360, 120], [320, 153]].map(([x, y]) => ({ x, y, r: 12, hit: 0, touching: false }));
|
||||
// Il Regolo, in alto a destra sotto l'arco, dove la pallina scende dal giro.
|
||||
const regolo = { x: 404, y: 62, r: 6, hit: 0 };
|
||||
const lanes = [257, 297, 340, 381].map((x, i) => ({ x, y: 88, rione: RIONI[i], lit: false }));
|
||||
const regolo = { x: 404, y: 62, r: 7, hit: 0, touching: false };
|
||||
// Le corsie: ognuna fra due guide (o la sponda), con la bandiera dipinta in `x`; si accende attraversando in
|
||||
// discesa la riga LANE_LINE.
|
||||
const lanes = [[200, 277, 257], [277, 319, 297], [319, 361, 340], [361, 432, 381]].map(([x0, x1, x], i) => ({ x0, x1, x, y: LANE_LINE, rione: RIONI[i], lit: false }));
|
||||
return { walls, targets, bumpers, regolo, lanes };
|
||||
}
|
||||
|
||||
export function newFlipper(seed = Date.now() >>> 0, def = {}) {
|
||||
const level = LEVELS[def.level] ? def.level : 'seria';
|
||||
const c = { step: 'play', t: 0, seed, level, skill: LEVELS[level], current: 'player', balls: BALLS, score: 0, power: 0.7,
|
||||
const c = { step: 'play', t: 0, sim: 0, seed, level, skill: LEVELS[level], current: 'player', balls: BALLS, score: 0, power: 0.7,
|
||||
holding: false, holdTime: 0, pointerId: null, cursor: null, accumulator: 0, events: [], line: null, lineT: 0, saved: false,
|
||||
flippers: { left: { angle: FLIP.rest, omega: 0, held: [] }, right: { angle: Math.PI - FLIP.rest, omega: 0, held: [] } },
|
||||
ball: null, saveUntil: 0, still: 0, visual: { time: 0 }, ...layout() };
|
||||
flippers: { left: { angle: FLIP.rest, omega: 0, held: [], click: 0 }, right: { angle: Math.PI - FLIP.rest, omega: 0, held: [], click: 0 } },
|
||||
ball: null, saveUntil: 0, still: 0, anchor: null, rest: 0, resets: {}, result: null, visual: { time: 0 }, ...layout() };
|
||||
newBall(c);
|
||||
return c;
|
||||
}
|
||||
function newBall(c) { c.ball = { x: (LANE.x0 + LANE.x1) / 2, y: 318, vx: 0, vy: 0, waiting: true }; c.still = 0; c.anchor = null; }
|
||||
function newBall(c) { c.ball = { x: (LANE.x0 + LANE.x1) / 2, y: 318, vx: 0, vy: 0, waiting: true }; c.still = 0; c.anchor = null; c.rest = 0; }
|
||||
export const waiting = c => c.step === 'play' && !!c.ball?.waiting;
|
||||
export const canThrow = c => waiting(c) && !c.holding;
|
||||
export const canPress = canThrow;
|
||||
export const canAdjust = c => c.step !== 'result';
|
||||
// Due permessi diversi: il cursore della molla solo prima del lancio, i flipper per tutta la partita.
|
||||
export const canAdjust = canThrow;
|
||||
export const canFlip = c => c.step === 'play';
|
||||
export const aimFree = () => true;
|
||||
export const powerAt = time => clamp(time / CHARGE, 0, 1);
|
||||
// Quanto resta del salvataggio della pallina, in secondi (0: non protetta).
|
||||
export const saveLeft = c => c.ball && !c.ball.waiting ? Math.max(0, c.saveUntil - c.sim) : 0;
|
||||
function say(c, line) { c.line = line; c.lineT = 2; }
|
||||
function add(c, points) { c.score += points; }
|
||||
// I punti; al traguardo la vittoria è acquisita (anche uscendo prima), poi si continua per il record.
|
||||
function add(c, points) {
|
||||
c.score += points;
|
||||
if (!c.result && c.score >= c.skill.goal) { c.result = { winner: 'player', score: c.score, goal: c.skill.goal }; say(c, 'TRAGUARDO!'); c.events.push({ kind: 'flipper-palio', power: 1 }); }
|
||||
if (c.result) c.result.score = c.score;
|
||||
}
|
||||
|
||||
// Il lancio: la molla spinge la pallina su per la corsia.
|
||||
// Il lancio: la molla spinge la pallina su per la corsia. Anche la molla più fiacca arriva all'arco (la velocità
|
||||
// minima dipende dalla gravità del livello); da lì in su, la forza decide quanto gira.
|
||||
export function launchSpeed(c, power) {
|
||||
const min = Math.sqrt(2 * c.skill.g * 340), max = Math.max(min + 150, 900);
|
||||
return min + (max - min) * clamp(power, 0, 1);
|
||||
}
|
||||
export function launch(c, power) {
|
||||
if (!waiting(c)) return false;
|
||||
c.ball.waiting = false; c.ball.vy = -(LAUNCH[0] + (LAUNCH[1] - LAUNCH[0]) * clamp(power, 0, 1)); c.ball.vx = 0;
|
||||
// La molla vera non è mai identica due volte: un pelo di caso, così la partita non si ripete uguale.
|
||||
c.ball.waiting = false; c.ball.vy = -launchSpeed(c, power) * (0.985 + random(c) * 0.03); c.ball.vx = 0;
|
||||
// Il salvataggio vale una volta per pallina: persa subito, torna; persa di nuovo, è persa.
|
||||
c.saveUntil = c.saved ? 0 : c.t + c.skill.save; c.holding = false; c.holdTime = 0;
|
||||
c.events.push({ kind: 'flipper-lancio', power: 0.5 + 0.5 * power });
|
||||
c.saveUntil = c.saved ? 0 : c.sim + c.skill.save; c.holding = false; c.holdTime = 0; c.pointerId = null;
|
||||
c.events.push({ kind: 'flipper-lancio', power: 0.5 + 0.5 * clamp(power, 0, 1) });
|
||||
return true;
|
||||
}
|
||||
// Il flipper: il tasto tenuto lo alza, lasciato ricade. Più dita o tasti insieme: resta su finché ne resta uno.
|
||||
// Un «clic» (il pulsante attivato da tastiera) lo tiene su per un attimo, abbastanza perché la fisica lo veda.
|
||||
export function hold(c, side, who, down) {
|
||||
const f = c.flippers[side]; if (!f) return false;
|
||||
const was = f.held.length > 0;
|
||||
f.held = f.held.filter(h => h !== who); if (down) f.held.push(who);
|
||||
if (who === 'click' && down) f.click = CLICK_HOLD;
|
||||
if (!was && f.held.length && c.step === 'play') c.events.push({ kind: 'flipper-flip', power: 0.6 });
|
||||
return true;
|
||||
}
|
||||
export function flipperSeg(c, side) {
|
||||
const f = c.flippers[side], [px, py] = PIVOT[side];
|
||||
return { a: [px, py], b: [px + Math.cos(f.angle) * FLIP.len, py + Math.sin(f.angle) * FLIP.len] };
|
||||
}
|
||||
|
||||
// La fisica, a passi fissi.
|
||||
export function update(c, dt) {
|
||||
@@ -95,89 +126,120 @@ export function update(c, dt) {
|
||||
c.accumulator += dt;
|
||||
while (c.accumulator >= STEP) { c.accumulator -= STEP; step(c); }
|
||||
}
|
||||
function flipperSeg(c, side) {
|
||||
const f = c.flippers[side], [px, py] = PIVOT[side];
|
||||
return { a: [px, py], b: [px + Math.cos(f.angle) * FLIP.len, py + Math.sin(f.angle) * FLIP.len] };
|
||||
}
|
||||
function step(c) {
|
||||
// I flipper: su se tenuti, giù se no; velocità angolare costante.
|
||||
function moveFlippers(c, h) {
|
||||
for (const side of ['left', 'right']) {
|
||||
const f = c.flippers[side];
|
||||
if (f.click > 0 && (f.click -= h) <= 0) f.held = f.held.filter(k => k !== 'click');
|
||||
const target = f.held.length ? (side === 'left' ? FLIP.up : Math.PI - FLIP.up) : (side === 'left' ? FLIP.rest : Math.PI - FLIP.rest);
|
||||
const d = target - f.angle, move = FLIP.speed * STEP;
|
||||
f.omega = Math.abs(d) <= move ? 0 : Math.sign(d) * FLIP.speed;
|
||||
f.angle = Math.abs(d) <= move ? target : f.angle + Math.sign(d) * move;
|
||||
const previous = f.angle;
|
||||
f.angle += clamp(target - f.angle, -FLIP.speed * h, FLIP.speed * h);
|
||||
f.omega = (f.angle - previous) / h;
|
||||
}
|
||||
}
|
||||
function step(c) {
|
||||
c.sim += STEP;
|
||||
const ball = c.ball;
|
||||
if (c.step !== 'play' || !ball || ball.waiting) return;
|
||||
if (c.step !== 'play' || !ball || ball.waiting) { moveFlippers(c, STEP); return; }
|
||||
for (const bank of Object.keys(c.resets)) if (c.sim >= c.resets[bank]) { for (const t of c.targets) if (t.bank === bank) t.down = false; delete c.resets[bank]; }
|
||||
ball.vy += c.skill.g * STEP;
|
||||
const v = Math.hypot(ball.vx, ball.vy);
|
||||
if (v > MAX_SPEED) { ball.vx *= MAX_SPEED / v; ball.vy *= MAX_SPEED / v; }
|
||||
ball.x += ball.vx * STEP; ball.y += ball.vy * STEP;
|
||||
for (const w of c.walls) collideSeg(c, ball, w);
|
||||
for (const t of c.targets) if (!t.down && collideSeg(c, ball, { a: t.a, b: t.b, e: 0.5 })) dropTarget(c, t);
|
||||
for (const b of c.bumpers) if (collideCircle(ball, b, 0)) { kick(c, ball, b, c.skill.kick); b.hit = 0.15; add(c, SCORE.bumper); c.events.push({ kind: 'flipper-fungo', power: 0.8 }); }
|
||||
if (collideCircle(ball, c.regolo, 0.7)) { c.regolo.hit = 0.3; add(c, SCORE.regolo); say(c, 'Il Regolo!'); c.events.push({ kind: 'flipper-bersaglio', power: 1 }); }
|
||||
for (const side of ['left', 'right']) collideFlipper(c, ball, side);
|
||||
// Le corsie si accendono entrando (non restandoci sopra: dopo il Palio ripartono spente).
|
||||
for (const l of c.lanes) {
|
||||
const inside = Math.hypot(ball.x - l.x, ball.y - l.y) < 7;
|
||||
if (inside && !l.inside && !l.lit) {
|
||||
l.lit = true; add(c, SCORE.lane); c.events.push({ kind: 'flipper-corsia', power: 0.6 });
|
||||
if (c.lanes.every(k => k.lit)) { add(c, SCORE.palio); say(c, 'PALIO! Tutti e quattro i rioni!'); c.events.push({ kind: 'flipper-palio', power: 1 }); for (const k of c.lanes) k.lit = false; }
|
||||
}
|
||||
l.inside = inside;
|
||||
limit(ball);
|
||||
// Sottopassi: pallina e punta del flipper insieme non devono fare più di un raggio per passo.
|
||||
const tip = FLIP.speed * FLIP.len, n = clamp(Math.ceil((Math.hypot(ball.vx, ball.vy) + tip) * STEP / R), 1, 6), h = STEP / n;
|
||||
const from = { x: ball.x, y: ball.y };
|
||||
for (let i = 0; i < n; i++) {
|
||||
moveFlippers(c, h);
|
||||
ball.x += ball.vx * h; ball.y += ball.vy * h;
|
||||
for (const w of c.walls) collideSeg(c, ball, w);
|
||||
for (const t of c.targets) if (!t.down) collideTarget(c, ball, t);
|
||||
for (const b of c.bumpers) collideBumper(c, ball, b);
|
||||
collideRegolo(c, ball);
|
||||
for (const side of ['left', 'right']) collideFlipper(c, ball, side);
|
||||
limit(ball);
|
||||
}
|
||||
// La pallina che resta nello stesso punto (ferma, o che trema in un incastro): dopo un po' una scossa.
|
||||
crossLanes(c, from, ball);
|
||||
// La pallina che resta nello stesso punto (ferma, o che trema in un incastro): dopo un po' una scossa. Non se è
|
||||
// trattenuta su un flipper alzato: quella è una mossa del giocatore.
|
||||
if (!c.anchor || Math.hypot(ball.x - c.anchor.x, ball.y - c.anchor.y) > 3) { c.anchor = { x: ball.x, y: ball.y }; c.still = 0; }
|
||||
else if ((c.still += STEP) > 2.5) { ball.vx += (random(c) - 0.5) * 160; ball.vy -= 90; c.still = 0; c.anchor = null; }
|
||||
// Un lancio fiacco ricasca sulla molla: si rilancia.
|
||||
if (ball.x > LANE.x0 && ball.y > 315 && Math.abs(ball.vy) < 8 && !ball.waiting) newBall(c);
|
||||
else if ((c.still += STEP) > NUDGE_AFTER) { c.still = 0; c.anchor = null; if (!cradled(c, ball)) { ball.vx += (random(c) - 0.5) * 160; ball.vy -= 90; } }
|
||||
// Un lancio fiacco (o una pallina ricaduta nella corsia) si assesta sul fondo e si rilancia.
|
||||
if (ball.x > LANE.x0 && ball.y > 318 && Math.hypot(ball.vx, ball.vy) < 12) { if ((c.rest += STEP) > 0.3) newBall(c); } else c.rest = 0;
|
||||
if (ball.y > FIELD.y1 + R * 2) drain(c);
|
||||
}
|
||||
function dropTarget(c, t) {
|
||||
t.down = true; add(c, SCORE.target); c.events.push({ kind: 'flipper-bersaglio', power: 0.7 });
|
||||
const bank = c.targets.filter(k => k.bank === t.bank);
|
||||
if (bank.every(k => k.down)) { add(c, SCORE.bank); say(c, t.bank === 'pieve' ? 'La Pieve!' : 'La torre!'); for (const k of bank) k.down = false; }
|
||||
function limit(ball) { const v = Math.hypot(ball.vx, ball.vy); if (v > MAX_SPEED) { ball.vx *= MAX_SPEED / v; ball.vy *= MAX_SPEED / v; } }
|
||||
const cradled = (c, ball) => ['left', 'right'].some(side => c.flippers[side].held.length && Math.hypot(ball.x - PIVOT[side][0], ball.y - PIVOT[side][1]) < FLIP.len + 2 * R);
|
||||
// Le corsie: contano attraversando la riga in discesa, dovunque nella corsia (la posizione orizzontale interpolata).
|
||||
function crossLanes(c, from, ball) {
|
||||
if (!(from.y < LANE_LINE && ball.y >= LANE_LINE)) return;
|
||||
const k = (LANE_LINE - from.y) / (ball.y - from.y), x = from.x + (ball.x - from.x) * k;
|
||||
const l = c.lanes.find(l => x >= l.x0 && x < l.x1);
|
||||
if (!l || l.lit) return;
|
||||
l.lit = true; add(c, SCORE.lane); c.events.push({ kind: 'flipper-corsia', power: 0.6 });
|
||||
if (c.lanes.every(k => k.lit)) { add(c, SCORE.palio); say(c, 'PALIO!'); c.events.push({ kind: 'flipper-palio', power: 1 }); for (const k of c.lanes) k.lit = false; }
|
||||
}
|
||||
function drain(c) {
|
||||
c.events.push({ kind: 'flipper-buco', power: 0.8 });
|
||||
if (c.t < c.saveUntil) { say(c, 'Pallina salvata!'); c.saved = true; newBall(c); return; }
|
||||
if (c.sim < c.saveUntil) { say(c, 'Pallina salvata!'); c.saved = true; newBall(c); return; }
|
||||
c.balls--; c.saved = false;
|
||||
if (c.balls > 0) { say(c, `Pallina persa. Ne restano ${c.balls}.`); newBall(c); return; }
|
||||
if (c.balls > 0) { say(c, `Persa: ne restano ${c.balls}.`); newBall(c); return; }
|
||||
c.ball = null; c.step = 'result';
|
||||
c.result = { winner: c.score >= c.skill.goal ? 'player' : 'cpu', score: c.score, goal: c.skill.goal };
|
||||
}
|
||||
// Pallina contro un segmento: la si spinge fuori e si riflette la velocità. Torna true se ha toccato.
|
||||
function collideSeg(c, ball, w) {
|
||||
// Pallina contro un segmento: la si spinge fuori e si riflette la velocità. Torna la velocità d'urto lungo la
|
||||
// normale (positiva se picchiava), o null se non tocca; `touching` ricorda il contatto, per contare solo gli urti nuovi.
|
||||
function hitSeg(ball, w, e) {
|
||||
const [ax, ay] = w.a, [bx, by] = w.b, dx = bx - ax, dy = by - ay, len2 = dx * dx + dy * dy || 1;
|
||||
const u = clamp(((ball.x - ax) * dx + (ball.y - ay) * dy) / len2, 0, 1), px = ax + dx * u, py = ay + dy * u;
|
||||
let nx = ball.x - px, ny = ball.y - py; const d = Math.hypot(nx, ny);
|
||||
if (d >= R) return false;
|
||||
if (d >= R) { w.touching = false; return null; }
|
||||
if (d < 1e-6) { nx = -dy; ny = dx; const n = Math.hypot(nx, ny); nx /= n; ny /= n; } else { nx /= d; ny /= d; }
|
||||
// Il cancelletto ferma solo chi scende dall'alto.
|
||||
if (w.gate && !(ny < 0 && ball.vy > 0)) return false;
|
||||
ball.x = px + nx * R; ball.y = py + ny * R;
|
||||
const vn = ball.vx * nx + ball.vy * ny;
|
||||
if (vn < 0) {
|
||||
// La sponda elastica spinge via, un po' a caso come la gomma vera (così la pallina non fa il ping-pong in eterno).
|
||||
if (w.sling) { const a = Math.atan2(ny, nx) + (random(c) - 0.5) * 0.5, s = 230 + random(c) * 40; ball.vx = Math.cos(a) * s + (ball.vx - vn * nx) * 0.3; ball.vy = Math.sin(a) * s + (ball.vy - vn * ny) * 0.3; add(c, SCORE.sling); c.events.push({ kind: 'flipper-sponda', power: 0.6 }); }
|
||||
else { ball.vx -= (1 + w.e) * vn * nx; ball.vy -= (1 + w.e) * vn * ny; }
|
||||
}
|
||||
return true;
|
||||
const vn = ball.vx * nx + ball.vy * ny, fresh = !w.touching;
|
||||
w.touching = true;
|
||||
if (vn < 0 && e !== null) { ball.vx -= (1 + e) * vn * nx; ball.vy -= (1 + e) * vn * ny; }
|
||||
return { vn: -vn, nx, ny, fresh };
|
||||
}
|
||||
function collideCircle(ball, o, e) {
|
||||
function collideSeg(c, ball, w) {
|
||||
if (!w.sling) { hitSeg(ball, w, w.e); return; }
|
||||
// La sponda elastica: spinge via lungo la normale, tenendo la componente di strisciamento, e un po' a caso come
|
||||
// la gomma vera; solo a un urto nuovo, e solo se picchia.
|
||||
const hit = hitSeg(ball, w, null);
|
||||
if (!hit || hit.vn <= 0) return;
|
||||
if (hit.fresh && hit.vn > IMPACT) {
|
||||
const a = Math.atan2(hit.ny, hit.nx) + (random(c) - 0.5) * 0.24, s = 250 + random(c) * 30;
|
||||
const tx = ball.vx + hit.vn * hit.nx, ty = ball.vy + hit.vn * hit.ny; // la parte lungo la sponda
|
||||
ball.vx = Math.cos(a) * s + tx * 0.6; ball.vy = Math.sin(a) * s + ty * 0.6;
|
||||
add(c, SCORE.sling); c.events.push({ kind: 'flipper-sponda', power: 0.6 });
|
||||
} else { ball.vx += 1.3 * hit.vn * hit.nx; ball.vy += 1.3 * hit.vn * hit.ny; }
|
||||
}
|
||||
function collideTarget(c, ball, t) {
|
||||
const hit = hitSeg(ball, t, 0.5);
|
||||
if (!hit || !hit.fresh || hit.vn < IMPACT) return;
|
||||
t.down = true; add(c, SCORE.target); c.events.push({ kind: 'flipper-bersaglio', power: 0.7 });
|
||||
const bank = c.targets.filter(k => k.bank === t.bank);
|
||||
if (bank.every(k => k.down)) { add(c, SCORE.bank); say(c, t.bank === 'pieve' ? 'LA PIEVE!' : 'LA TORRE!'); c.resets[t.bank] = c.sim + BANK_RESET; }
|
||||
}
|
||||
// Pallina contro un cerchio: spinta fuori; torna la velocità d'urto lungo la normale (null se non tocca).
|
||||
function hitCircle(ball, o, e) {
|
||||
const dx = ball.x - o.x, dy = ball.y - o.y, d = Math.hypot(dx, dy), min = R + o.r;
|
||||
if (d >= min) return false;
|
||||
if (d >= min) { o.touching = false; return null; }
|
||||
const nx = d > 1e-6 ? dx / d : 0, ny = d > 1e-6 ? dy / d : -1;
|
||||
ball.x = o.x + nx * min; ball.y = o.y + ny * min;
|
||||
const vn = ball.vx * nx + ball.vy * ny;
|
||||
const vn = ball.vx * nx + ball.vy * ny, fresh = !o.touching;
|
||||
o.touching = true;
|
||||
if (vn < 0) { ball.vx -= (1 + e) * vn * nx; ball.vy -= (1 + e) * vn * ny; }
|
||||
return true;
|
||||
return { vn: -vn, nx, ny, fresh };
|
||||
}
|
||||
function kick(c, ball, o, speed) {
|
||||
const a = Math.atan2(ball.y - o.y, ball.x - o.x) + (random(c) - 0.5) * 0.3;
|
||||
ball.vx = Math.cos(a) * speed; ball.vy = Math.sin(a) * speed;
|
||||
function collideBumper(c, ball, b) {
|
||||
const hit = hitCircle(ball, b, 0.2);
|
||||
if (!hit || !hit.fresh || hit.vn < IMPACT) return;
|
||||
const a = Math.atan2(hit.ny, hit.nx) + (random(c) - 0.5) * 0.2;
|
||||
ball.vx = Math.cos(a) * c.skill.kick; ball.vy = Math.sin(a) * c.skill.kick;
|
||||
b.hit = 0.15; add(c, SCORE.bumper); c.events.push({ kind: 'flipper-fungo', power: 0.8 });
|
||||
}
|
||||
function collideRegolo(c, ball) {
|
||||
const hit = hitCircle(ball, c.regolo, 0.7);
|
||||
if (!hit || !hit.fresh || hit.vn < IMPACT) return;
|
||||
c.regolo.hit = 0.3; add(c, SCORE.regolo); say(c, 'IL REGOLO!'); c.events.push({ kind: 'flipper-bersaglio', power: 1 });
|
||||
}
|
||||
// Il flipper è una capsula che gira: alla pallina si aggiunge la velocità del punto toccato.
|
||||
function collideFlipper(c, ball, side) {
|
||||
@@ -195,7 +257,7 @@ function collideFlipper(c, ball, side) {
|
||||
}
|
||||
|
||||
// --- Comandi: col mouse o col dito, prima del lancio tenere premuto carica la molla e lasciare lancia; in gioco
|
||||
// la metà sinistra della scena alza il flipper sinistro, la destra il destro, finché si tiene premuto.
|
||||
// la metà sinistra della scena alza il flipper sinistro, la destra il destro, finché si tiene premuto (anche due dita).
|
||||
export function aimScreen(c, [x, y]) { if (!Number.isFinite(x) || !Number.isFinite(y)) return false; c.cursor = [x, y]; return true; }
|
||||
export function press(c, pointerId = 'ptr') {
|
||||
if (c.step !== 'play') return false;
|
||||
@@ -208,13 +270,24 @@ export function release(c, pointerId = 'ptr') {
|
||||
for (const side of ['left', 'right']) if (c.flippers[side].held.includes(pointerId)) any = hold(c, side, pointerId, false) || any;
|
||||
return any;
|
||||
}
|
||||
export const cancel = (c, pointerId) => pointerId === undefined ? release(c, c.pointerId ?? 'ptr') : release(c, pointerId);
|
||||
// I pulsanti «← Flipper» e «Flipper →» (tenuti premuti), e le frecce.
|
||||
// Annullare interrompe senza fare niente: la molla si scarica, i flipper ricadono (tutti, senza puntatore).
|
||||
export function cancel(c, pointerId) {
|
||||
const all = pointerId === undefined;
|
||||
if (all || c.pointerId === pointerId) { c.holding = false; c.holdTime = 0; c.pointerId = null; }
|
||||
for (const f of Object.values(c.flippers)) f.held = all ? [] : f.held.filter(id => id !== pointerId);
|
||||
return true;
|
||||
}
|
||||
// I pulsanti «← Flipper» e «Flipper →» (tenuti premuti; da tastiera un colpo breve), e le frecce.
|
||||
export const turn = (c, dir, who = 'button') => c.step === 'play' && hold(c, dir < 0 ? 'left' : 'right', who, true);
|
||||
export const stopTurn = (c, who = 'button') => hold(c, 'left', who, false) | hold(c, 'right', who, false);
|
||||
export function stopTurn(c, who = 'button') {
|
||||
if (who === 'click') return false; // il colpo breve finisce da sé
|
||||
const l = hold(c, 'left', who, false), r = hold(c, 'right', who, false);
|
||||
return l || r;
|
||||
}
|
||||
export const nudge = () => false;
|
||||
export function setPower(c, v) { if (!canThrow(c) || !Number.isFinite(v)) return false; c.power = clamp(v, 0, 1); return true; }
|
||||
export function key(c, k, down) {
|
||||
if (c.step !== 'play') return false; // a partita finita Spazio e Invio chiudono il risultato (main.js)
|
||||
if (k === 'ArrowLeft' || k === 'ArrowRight') { hold(c, k === 'ArrowLeft' ? 'left' : 'right', 'key-' + k, down); return true; }
|
||||
if (k === ' ') { if (down) { if (!c.holding && waiting(c)) press(c, 'key'); } else if (c.holding && c.pointerId === 'key') release(c, 'key'); return true; }
|
||||
if (k === 'Enter') { if (down) shoot(c); return true; }
|
||||
@@ -225,7 +298,8 @@ export const shoot = c => canThrow(c) && launch(c, c.power);
|
||||
export const button = c => c.step === 'result' ? 'Finita' : waiting(c) ? 'Lancia' : 'In gioco…';
|
||||
export function status(c) {
|
||||
if (c.step === 'result') return `${c.result.score} punti. ${c.result.winner === 'player' ? `Traguardo ${c.result.goal}: fatto!` : `Ne servivano ${c.result.goal}.`}`;
|
||||
const tally = `Punti ${c.score} (traguardo ${c.skill.goal}), palline ${c.balls}.`;
|
||||
const goal = c.result ? 'traguardo fatto, ora il record' : `traguardo ${c.skill.goal}`;
|
||||
const tally = `Punti ${c.score} (${goal}), palline ${c.balls}.`;
|
||||
if (c.line) return `${c.line} ${tally}`;
|
||||
return waiting(c) ? `Tieni premuto per caricare la molla e lascia, o premi Lancia. ${tally}` : `Flipper: metà sinistra e destra della scena, o i pulsanti. ${tally}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user