Il flipper coi due ponti (le rampe che si incrociano sopra la tavola, i PONTI se fatti di fila), i punti ×10 come sui flipper degli anni 2000, il tabellone a punti luminosi con le cifre che corrono e le scritte che lampeggiano, e un clic sul River che entra e va al flipper (Andrea)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
+64
-9
@@ -11,15 +11,19 @@
|
||||
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, 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 punti, generosi come sui flipper degli anni 2000 (Andrea: «aumenta i punti»).
|
||||
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 };
|
||||
// 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: 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 },
|
||||
bona: { g: 680, save: 8, kick: 340, goal: 60000 },
|
||||
seria: { g: 740, save: 4, kick: 340, goal: 120000 },
|
||||
male: { g: 800, save: 1, kick: 340, goal: 200000 },
|
||||
};
|
||||
export const RIONI = ['piazza', 'pieve', 'cardetole', 'campomigliaio'];
|
||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
@@ -55,7 +59,21 @@ export function layout() {
|
||||
// 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 };
|
||||
// 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.
|
||||
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 },
|
||||
];
|
||||
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); } }
|
||||
return { walls, targets, bumpers, regolo, lanes, ramps };
|
||||
}
|
||||
// Il punto a `s` pixel lungo il percorso di una rampa, e la direzione lì.
|
||||
export function alongRamp(r, s) {
|
||||
s = clamp(s, 0, r.len);
|
||||
let i = 1; while (i < r.cum.length - 1 && r.cum[i] < s) i++;
|
||||
const [ax, ay] = r.path[i - 1], [bx, by] = r.path[i], L = r.cum[i] - r.cum[i - 1] || 1, k = (s - r.cum[i - 1]) / L;
|
||||
return { x: ax + (bx - ax) * k, y: ay + (by - ay) * k, dx: (bx - ax) / L, dy: (by - ay) / L };
|
||||
}
|
||||
|
||||
export function newFlipper(seed = Date.now() >>> 0, def = {}) {
|
||||
@@ -63,11 +81,11 @@ 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, visual: { time: 0 }, ...layout() };
|
||||
ball: null, saveUntil: 0, still: 0, anchor: null, rest: 0, resets: {}, result: null, lastRamp: 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; c.rest = 0; }
|
||||
function newBall(c) { c.ball = { x: (LANE.x0 + LANE.x1) / 2, y: 318, vx: 0, vy: 0, waiting: true, ramp: null }; 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;
|
||||
@@ -141,6 +159,8 @@ function step(c) {
|
||||
const ball = c.ball;
|
||||
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; }
|
||||
ball.vy += c.skill.g * STEP;
|
||||
limit(ball);
|
||||
// Sottopassi: pallina e punta del flipper insieme non devono fare più di un raggio per passo.
|
||||
@@ -157,6 +177,7 @@ function step(c) {
|
||||
limit(ball);
|
||||
}
|
||||
crossLanes(c, from, ball);
|
||||
enterRamps(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; }
|
||||
@@ -176,11 +197,45 @@ function crossLanes(c, from, ball) {
|
||||
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; }
|
||||
}
|
||||
// L'imbocco di una rampa: la pallina lo attraversa in salita, abbastanza forte, e sale sul ponte: da lì segue il
|
||||
// percorso (niente urti con la tavola sotto), rallentando in salita e riprendendo in discesa. Troppo piano, torna
|
||||
// indietro e ricasca dall'imbocco.
|
||||
function enterRamps(c, from, ball) {
|
||||
for (const r of c.ramps) {
|
||||
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 };
|
||||
c.events.push({ kind: 'flipper-rampa', power: 0.7 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
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;
|
||||
on.s += on.v * STEP;
|
||||
const p = alongRamp(r, on.s);
|
||||
ball.x = p.x; ball.y = p.y;
|
||||
if (on.s >= r.len) {
|
||||
// In fondo al ponte: giù dall'altra parte, con la velocità che ha.
|
||||
const v = Math.max(on.v, 150);
|
||||
ball.vx = p.dx * v; ball.vy = p.dy * v; ball.ramp = null;
|
||||
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 }; }
|
||||
} 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;
|
||||
}
|
||||
function drain(c) {
|
||||
c.events.push({ kind: 'flipper-buco', power: 0.8 });
|
||||
if (c.sim < c.saveUntil) { say(c, 'Pallina salvata!'); c.saved = true; newBall(c); return; }
|
||||
if (c.sim < c.saveUntil) { say(c, 'SALVATA!'); c.saved = true; newBall(c); return; }
|
||||
c.balls--; c.saved = false;
|
||||
if (c.balls > 0) { say(c, `Persa: ne restano ${c.balls}.`); newBall(c); return; }
|
||||
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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user