Il flipper dopo la seconda revisione di Codex: flipper vivi anche sul ponte, imbocco solo in salita, ponti con la stessa energia su e giù e uscita nelle corsie di ritorno, combo che scade, scritte in coda col traguardo davanti, tabellone con l'esito giusto e le cifre che corrono in tempo fisso, tasti del mouse dalla maschera, glifi rifatti
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
+44
-27
@@ -15,7 +15,7 @@ export const PIVOT = { left: [280, 314], right: [360, 314] };
|
||||
export const SCORE = { bumper: 1000, sling: 500, lane: 2500, palio: 30000, target: 5000, bank: 25000, regolo: 10000, ramp: 15000, ponti: 50000 };
|
||||
// Le rampe (Andrea: «un paio di ponti sui quali sale la pallina»): si entra dal basso con abbastanza velocità, si
|
||||
// sale, si passa sopra la tavola e si scende dall'altra parte. Fatte tutte e due di fila: i PONTI.
|
||||
export const RAMP = { speed: 260, climb: 0.42, slope: 380, combo: 12 };
|
||||
export const RAMP = { speed: 300, slope: 260, drag: 30, combo: 12, blend: 30 };
|
||||
// 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
|
||||
@@ -60,12 +60,18 @@ export function layout() {
|
||||
// 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 }));
|
||||
// Le due rampe: l'imbocco (un segmento che si attraversa in salita), il percorso sopra la tavola, l'uscita in
|
||||
// fondo al percorso, dall'altra parte. Si incrociano in cima, come due ponti.
|
||||
// fondo al percorso, dall'altra parte, dritta nella corsia di ritorno verso il flipper. Si incrociano una volta sola,
|
||||
// in cima, come due ponti.
|
||||
const ramps = [
|
||||
{ id: 'sinistra', mouth: [[236, 236], [258, 236]], path: [[247, 236], [247, 150], [252, 96], [272, 56], [312, 36], [352, 44], [382, 72], [398, 112], [400, 134]], lit: 0, touching: false },
|
||||
{ id: 'destra', mouth: [[382, 236], [404, 236]], path: [[393, 236], [393, 150], [388, 96], [368, 56], [328, 36], [288, 44], [258, 72], [242, 112], [240, 134]], lit: 0, touching: false },
|
||||
{ id: 'sinistra', mouth: [[236, 236], [258, 236]], path: [[247, 236], [247, 150], [250, 96], [262, 52], [290, 30], [322, 34], [352, 52], [378, 84], [394, 116], [411, 128], [411, 142]], lit: 0, touching: false },
|
||||
{ id: 'destra', mouth: [[382, 236], [404, 236]], path: [[393, 236], [393, 150], [390, 96], [378, 52], [350, 30], [318, 34], [288, 52], [262, 84], [246, 116], [229, 128], [229, 142]], lit: 0, touching: false },
|
||||
];
|
||||
for (const r of ramps) { r.len = 0; r.cum = [0]; for (let i = 1; i < r.path.length; i++) { r.len += Math.hypot(r.path[i][0] - r.path[i - 1][0], r.path[i][1] - r.path[i - 1][1]); r.cum.push(r.len); } }
|
||||
// Lunghezze, e la cima (il punto più alto del disegno): fin lì si sale, poi si scende.
|
||||
for (const r of ramps) {
|
||||
r.len = 0; r.cum = [0];
|
||||
for (let i = 1; i < r.path.length; i++) { r.len += Math.hypot(r.path[i][0] - r.path[i - 1][0], r.path[i][1] - r.path[i - 1][1]); r.cum.push(r.len); }
|
||||
r.top = r.cum[r.path.reduce((best, p, i) => p[1] < r.path[best][1] ? i : best, 0)];
|
||||
}
|
||||
return { walls, targets, bumpers, regolo, lanes, ramps };
|
||||
}
|
||||
// Il punto a `s` pixel lungo il percorso di una rampa, e la direzione lì.
|
||||
@@ -81,7 +87,7 @@ export function newFlipper(seed = Date.now() >>> 0, def = {}) {
|
||||
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: [], 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, lastRamp: null, visual: { time: 0 }, ...layout() };
|
||||
ball: null, saveUntil: 0, still: 0, anchor: null, rest: 0, resets: {}, result: null, lastRamp: null, lines: [], lineId: 0, visual: { time: 0 }, ...layout() };
|
||||
newBall(c);
|
||||
return c;
|
||||
}
|
||||
@@ -96,11 +102,15 @@ 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; }
|
||||
// Le scritte del tabellone: una alla volta, due secondi l'una; il traguardo passa davanti a tutte.
|
||||
function say(c, line, front = false) {
|
||||
if (front || !c.line) { if (front && c.line) c.lines.unshift(c.line); c.line = line; c.lineT = 2; c.lineId++; }
|
||||
else c.lines.push(line);
|
||||
}
|
||||
// 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.score >= c.skill.goal) { c.result = { winner: 'player', score: c.score, goal: c.skill.goal }; say(c, 'TRAGUARDO!', true); c.events.push({ kind: 'flipper-palio', power: 1 }); }
|
||||
if (c.result) c.result.score = c.score;
|
||||
}
|
||||
|
||||
@@ -138,7 +148,7 @@ export function flipperSeg(c, side) {
|
||||
export function update(c, dt) {
|
||||
if (!Number.isFinite(dt) || dt <= 0) return;
|
||||
dt = Math.min(dt, 0.25); c.t += dt; c.visual.time += dt;
|
||||
if (c.lineT > 0 && (c.lineT -= dt) <= 0) c.line = null;
|
||||
if (c.lineT > 0 && (c.lineT -= dt) <= 0) { c.line = c.lines.shift() ?? null; if (c.line) { c.lineT = 2; c.lineId++; } }
|
||||
if (c.holding) { c.holdTime += dt; c.power = powerAt(c.holdTime); }
|
||||
for (const b of c.bumpers) b.hit = Math.max(0, b.hit - dt); c.regolo.hit = Math.max(0, c.regolo.hit - dt);
|
||||
c.accumulator += dt;
|
||||
@@ -157,10 +167,11 @@ function moveFlippers(c, h) {
|
||||
function step(c) {
|
||||
c.sim += STEP;
|
||||
const ball = c.ball;
|
||||
for (const r of c.ramps) r.lit = Math.max(0, r.lit - STEP);
|
||||
if (c.lastRamp && c.sim - c.lastRamp.at > RAMP.combo) c.lastRamp = null;
|
||||
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]; }
|
||||
for (const r of c.ramps) r.lit = Math.max(0, r.lit - STEP);
|
||||
if (ball.ramp) { rideRamp(c, ball); return; }
|
||||
if (ball.ramp) { moveFlippers(c, STEP); rideRamp(c, ball); return; }
|
||||
ball.vy += c.skill.g * STEP;
|
||||
limit(ball);
|
||||
// Sottopassi: pallina e punta del flipper insieme non devono fare più di un raggio per passo.
|
||||
@@ -205,36 +216,42 @@ function enterRamps(c, from, ball) {
|
||||
const [[x0, y], [x1]] = r.mouth;
|
||||
if (!(from.y > y && ball.y <= y && ball.vy < 0)) continue;
|
||||
const k = (y - from.y) / (ball.y - from.y), x = from.x + (ball.x - from.x) * k;
|
||||
if (x < x0 || x > x1) continue;
|
||||
const v = Math.hypot(ball.vx, ball.vy);
|
||||
if (v < RAMP.speed) continue;
|
||||
ball.ramp = { id: r.id, s: 0, v: v * 0.9 };
|
||||
if (x < x0 + R || x > x1 - R) continue;
|
||||
// Conta solo la velocità lungo il ponte (sfiorarlo di traverso non fa salire).
|
||||
const p = alongRamp(r, 0), forward = ball.vx * p.dx + ball.vy * p.dy;
|
||||
if (forward < RAMP.speed) continue;
|
||||
ball.ramp = { id: r.id, s: 0, v: forward * 0.95, off: x - p.x };
|
||||
c.events.push({ kind: 'flipper-rampa', power: 0.7 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Sul ponte: si sale fino alla cima perdendo velocità, si scende riprendendola (la stessa energia, meno un po'
|
||||
// d'attrito); in cima la quota è massima. Da lì la velocità lungo il percorso, la posizione e la quota (`z`, 0..1).
|
||||
export function rampHeight(r, s) { return s < r.top ? s / r.top : 1 - (s - r.top) / (r.len - r.top); }
|
||||
function rideRamp(c, ball) {
|
||||
const r = c.ramps.find(k => k.id === ball.ramp.id), on = ball.ramp, top = r.len * RAMP.climb;
|
||||
on.v += (on.s < top ? -RAMP.slope : RAMP.slope) * STEP;
|
||||
const r = c.ramps.find(k => k.id === ball.ramp.id), on = ball.ramp, uphill = on.s < r.top;
|
||||
on.v += (uphill ? -RAMP.slope : RAMP.slope * r.top / (r.len - r.top)) * STEP - Math.sign(on.v) * RAMP.drag * STEP;
|
||||
on.s += on.v * STEP;
|
||||
const p = alongRamp(r, on.s);
|
||||
ball.x = p.x; ball.y = p.y;
|
||||
const p = alongRamp(r, on.s), blend = Math.max(0, 1 - on.s / RAMP.blend);
|
||||
ball.x = p.x + on.off * blend; ball.y = p.y; on.z = rampHeight(r, on.s);
|
||||
if (on.s >= r.len) {
|
||||
// In fondo al ponte: giù dall'altra parte, con la velocità che ha.
|
||||
// In fondo al ponte: giù dall'altra parte, con la velocità che ha. Gli ostacoli lì sotto, se per caso la
|
||||
// pallina esce sopra uno di loro, non contano come urto nuovo.
|
||||
const v = Math.max(on.v, 150);
|
||||
ball.vx = p.dx * v; ball.vy = p.dy * v; ball.ramp = null;
|
||||
for (const o of [...c.bumpers, c.regolo, ...c.targets, ...c.walls]) o.touching = true;
|
||||
add(c, SCORE.ramp); r.lit = 3; c.events.push({ kind: 'flipper-bersaglio', power: 0.9 });
|
||||
if (c.lastRamp && c.lastRamp.id !== r.id && c.sim - c.lastRamp.at < RAMP.combo) { add(c, SCORE.ponti); say(c, 'I PONTI!'); c.events.push({ kind: 'flipper-palio', power: 1 }); c.lastRamp = null; }
|
||||
else { say(c, r.id === 'sinistra' ? 'PONTE SINISTRO' : 'PONTE DESTRO'); c.lastRamp = { id: r.id, at: c.sim }; }
|
||||
if (c.lastRamp && c.lastRamp.id !== r.id) { add(c, SCORE.ponti); say(c, 'I PONTI!'); say(c, `BONUS +${SCORE.ponti}`); c.events.push({ kind: 'flipper-palio', power: 1 }); c.lastRamp = null; }
|
||||
else { say(c, r.id === 'sinistra' ? 'PONTE SX' : 'PONTE DX'); say(c, `+${SCORE.ramp}`); c.lastRamp = { id: r.id, at: c.sim }; }
|
||||
} else if (on.s <= 0) {
|
||||
// Non ce l'ha fatta: ricasca dall'imbocco.
|
||||
ball.vx = 0; ball.vy = Math.max(60, -on.v); ball.ramp = null; ball.y = r.mouth[0][1] + 1;
|
||||
} else ball.vx = p.dx * on.v, ball.vy = p.dy * on.v;
|
||||
// Non ce l'ha fatta: ricasca dall'imbocco, lungo la direzione del ponte.
|
||||
ball.vx = -p.dx * on.v; ball.vy = Math.max(60, -p.dy * on.v); ball.ramp = null; ball.y = r.mouth[0][1] + 1;
|
||||
} else { ball.vx = p.dx * on.v; ball.vy = p.dy * on.v; }
|
||||
}
|
||||
function drain(c) {
|
||||
c.events.push({ kind: 'flipper-buco', power: 0.8 });
|
||||
if (c.sim < c.saveUntil) { say(c, 'SALVATA!'); c.saved = true; newBall(c); return; }
|
||||
c.balls--; c.saved = false;
|
||||
c.balls--; c.saved = false; c.lastRamp = null;
|
||||
if (c.balls > 0) { say(c, `PALLINE: ${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 };
|
||||
@@ -317,7 +334,7 @@ export function aimScreen(c, [x, y]) { if (!Number.isFinite(x) || !Number.isFini
|
||||
export function press(c, pointerId = 'ptr') {
|
||||
if (c.step !== 'play') return false;
|
||||
if (waiting(c)) { if (c.holding) return false; c.holding = true; c.holdTime = 0; c.pointerId = pointerId; c.power = 0; return true; }
|
||||
return hold(c, (c.cursor?.[0] ?? 320) < 320 ? 'left' : 'right', pointerId, true);
|
||||
return hold(c, pointerId === 'mouse-right' || (c.cursor?.[0] ?? 320) >= 320 ? 'right' : 'left', pointerId, true);
|
||||
}
|
||||
export function release(c, pointerId = 'ptr') {
|
||||
if (c.holding && c.pointerId === pointerId) return launch(c, powerAt(c.holdTime));
|
||||
|
||||
Reference in New Issue
Block a user