La pista col gesso: le macchinine sul parcheggio di Cardetole contro il Chiaru (consenso del 29 ottobre, sprite di Codex dalla foto), un giro a spinte con le penalità alla francese (fuori, tagliè, cappottè, accompagnè: si torna dov'eri); Bertovo diventa Trappizzio Berti; Fabrizio Berti e il suo libro nei Ringraziamenti (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
// Le macchinine sulla pista col gesso (Andrea, 28 ottobre): sul parcheggio sotto casa, a Cardetole, si disegnava col
|
||||
// gesso la pista e si davano spinte a turno alle Micro Machines; la macchinina doveva restare in pista: tutta fuori, si
|
||||
// torna da dove si era partiti. Si gioca contro il Chiaru, un giro; vince chi taglia per primo il traguardo.
|
||||
//
|
||||
// Regole: una spinta a testa, prima Andrea e poi il Chiaru, così a fine giro hanno spinto lo stesso numero di volte.
|
||||
// Se tagliano il traguardo tutti e due nello stesso giro di spinte vince chi è andato più in là. Le penalità, coi nomi
|
||||
// alla francese che si usavano (Andrea, 29 ottobre), e per tutte si torna dove si era prima della spinta:
|
||||
// - fuori: la macchinina esce tutta dal gesso (anche se ce la manda l'altra, urtandola);
|
||||
// - tagliè: esce dalla parte di dentro di una curva, o salta l'erba di mezzo per tagliare la strada;
|
||||
// - cappottè: un urto forte la ribalta;
|
||||
// - accompagnè: il dito l'ha accompagnata invece di darle il colpetto (tenuto premuto oltre la carica piena).
|
||||
// Unità: i pixel del primo piano 640×360; la pista si disegna sopra al parcheggio di Codex.
|
||||
|
||||
// La linea di mezzo, in senso orario a partire dal traguardo; la pista è larga 2·HALF, col gesso sui due bordi.
|
||||
export const CONTROL = [
|
||||
[150, 68], [300, 62], [450, 66], [540, 80], [586, 122], [580, 172], [536, 196], [470, 196], [414, 178],
|
||||
[356, 168], [318, 196], [326, 240], [388, 262], [480, 260], [556, 262], [598, 290], [588, 324], [530, 334],
|
||||
[400, 334], [250, 332], [130, 326], [74, 296], [58, 230], [62, 150], [88, 96],
|
||||
];
|
||||
export const HALF = 22, CAR = { len: 26, wid: 13, r: 6 }, LAPS = 1;
|
||||
// La spinta: velocità di partenza da MIN_SPEED a MAX_SPEED, e l'asfalto che frena di DECEL px/s².
|
||||
export const MIN_SPEED = 60, MAX_SPEED = 400, DECEL = 420, SWING = 1.2, LONG_PRESS = 0.6, FIXED_STEP = 1 / 120;
|
||||
const REST = 0.7, THINK = 1, RESTITUTION = 0.6, OUT = HALF + CAR.r, WINDOW = 30;
|
||||
// FLIP: la velocità d'urto (px/s) che ribalta la macchinina colpita; ACCOMPAGNA: quanti secondi si può restare alla
|
||||
// carica piena prima che diventi un accompagnè.
|
||||
export const FLIP = 260, ACCOMPAGNA = 0.45;
|
||||
export const FAULTS = { fuori: 'Fuori', taglie: 'Tagliè', cappotte: 'Cappottè', accompagne: 'Accompagnè' };
|
||||
export const OTHER = { player: 'cpu', cpu: 'player' };
|
||||
|
||||
// Quanto è bravo il Chiaru: quanto gli trema il dito (radianti e forza) e fin dove osa spingere (px). `guide`: cosa
|
||||
// vede Andrea mentre mira (tutto il tiro col punto d'arrivo, solo la linea, solo la freccia).
|
||||
export const LEVELS = {
|
||||
bona: { wobble: 0.1, powerNoise: 0.16, ahead: 120, guide: 'full' },
|
||||
seria: { wobble: 0.05, powerNoise: 0.08, ahead: 170, guide: 'line' },
|
||||
male: { wobble: 0.022, powerNoise: 0.035, ahead: 200, guide: 'arrow' },
|
||||
};
|
||||
|
||||
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 deviation = c => random(c) + random(c) + random(c) - 1.5;
|
||||
|
||||
export const powerAt = time => clamp(time / SWING, 0, 1);
|
||||
export const speedFor = power => MIN_SPEED + (MAX_SPEED - MIN_SPEED) * clamp(power, 0, 1);
|
||||
// Quanta strada fa una macchinina spinta a velocità v (l'asfalto frena sempre uguale).
|
||||
export const reach = v => v * v / (2 * DECEL);
|
||||
export const powerFor = distance => clamp((Math.sqrt(2 * DECEL * Math.max(0, distance)) - MIN_SPEED) / (MAX_SPEED - MIN_SPEED), 0, 1);
|
||||
|
||||
// --- La pista: una curva chiusa (Catmull-Rom sui punti di controllo), ricampionata a passo costante.
|
||||
export function buildTrack(control = CONTROL, step = 2) {
|
||||
const n = control.length, dense = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const [p0, p1, p2, p3] = [control[(i - 1 + n) % n], control[i], control[(i + 1) % n], control[(i + 2) % n]];
|
||||
for (let k = 0; k < 24; k++) {
|
||||
const t = k / 24, t2 = t * t, t3 = t2 * t;
|
||||
const f = j => 0.5 * (2 * p1[j] + (p2[j] - p0[j]) * t + (2 * p0[j] - 5 * p1[j] + 4 * p2[j] - p3[j]) * t2 + (3 * p1[j] - p0[j] - 3 * p2[j] + p3[j]) * t3);
|
||||
dense.push([f(0), f(1)]);
|
||||
}
|
||||
}
|
||||
const cum = [0];
|
||||
for (let i = 1; i <= dense.length; i++) cum.push(cum[i - 1] + Math.hypot(dense[i % dense.length][0] - dense[i - 1][0], dense[i % dense.length][1] - dense[i - 1][1]));
|
||||
const total = cum.at(-1), count = Math.round(total / step), pts = [];
|
||||
for (let j = 0, i = 0; j < count; j++) {
|
||||
const at = j * total / count;
|
||||
while (cum[i + 1] < at) i++;
|
||||
const a = dense[i], b = dense[(i + 1) % dense.length], t = (at - cum[i]) / (cum[i + 1] - cum[i] || 1);
|
||||
pts.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
|
||||
}
|
||||
const tan = pts.map((p, i) => {
|
||||
const a = pts[(i - 1 + count) % count], b = pts[(i + 1) % count], d = Math.hypot(b[0] - a[0], b[1] - a[1]) || 1;
|
||||
return [(b[0] - a[0]) / d, (b[1] - a[1]) / d];
|
||||
});
|
||||
return { pts, tan, n: count, step: total / count, length: total };
|
||||
}
|
||||
export const TRACK = buildTrack();
|
||||
|
||||
// Il punto della linea di mezzo più vicino: vicino all'ultimo noto (`hint`), così nei tornanti non si salta dall'altra
|
||||
// parte; senza `hint`, su tutta la pista. `s` è la distanza lungo la pista dal traguardo, `d` quella dalla linea.
|
||||
export function project(track, x, y, hint) {
|
||||
const { pts, n } = track;
|
||||
const from = hint === undefined ? 0 : hint - WINDOW, to = hint === undefined ? n : hint + WINDOW;
|
||||
let best = null;
|
||||
for (let k = from; k < to; k++) {
|
||||
const i = ((k % n) + n) % n, a = pts[i], b = pts[(i + 1) % n];
|
||||
const dx = b[0] - a[0], dy = b[1] - a[1], len2 = dx * dx + dy * dy || 1;
|
||||
const t = clamp(((x - a[0]) * dx + (y - a[1]) * dy) / len2, 0, 1);
|
||||
const d = Math.hypot(x - a[0] - dx * t, y - a[1] - dy * t);
|
||||
if (!best || d < best.d) best = { i, t, d };
|
||||
}
|
||||
return { i: best.i, d: best.d, s: (best.i + best.t) * track.step };
|
||||
}
|
||||
// Tutta fuori: nessun pezzo della macchinina tocca più la pista.
|
||||
export const isOut = d => d > OUT;
|
||||
// Uscita dalla parte di dentro di una curva (verso dove gira la pista): è un tagliè.
|
||||
function inside(track, i, x, y) {
|
||||
const a = track.tan[(i - 6 + track.n) % track.n], b = track.tan[(i + 6) % track.n], turn = a[0] * b[1] - a[1] * b[0];
|
||||
const [px, py] = track.pts[i], [tx, ty] = track.tan[i], side = tx * (y - py) - ty * (x - px);
|
||||
return Math.abs(turn) > 0.05 && Math.sign(turn) === Math.sign(side);
|
||||
}
|
||||
|
||||
// La macchinina tiene il conto della strada fatta (`progress`, anche oltre il giro) seguendo la pista da vicino.
|
||||
function follow(track, car) {
|
||||
const p = project(track, car.x, car.y, car.idx);
|
||||
let delta = p.s - car.s;
|
||||
if (delta > track.length / 2) delta -= track.length; else if (delta < -track.length / 2) delta += track.length;
|
||||
car.progress += delta; car.s = p.s; car.idx = p.i;
|
||||
if (isOut(p.d) && !car.fault) car.fault = inside(track, p.i, car.x, car.y) ? 'taglie' : 'fuori';
|
||||
}
|
||||
|
||||
function newCar(track, role, back, side) {
|
||||
const i = (track.n - Math.round(back / track.step)) % track.n, [x, y] = track.pts[i], [tx, ty] = track.tan[i];
|
||||
const car = { role, x: x - ty * side, y: y + tx * side, vx: 0, vy: 0, heading: Math.atan2(ty, tx), idx: i, s: 0, progress: 0, fault: null };
|
||||
const p = project(track, car.x, car.y);
|
||||
car.idx = p.i; car.s = p.s; car.progress = p.s > track.length / 2 ? p.s - track.length : p.s;
|
||||
return car;
|
||||
}
|
||||
const cars = c => [c.cars.player, c.cars.cpu];
|
||||
export const moving = c => cars(c).some(car => car.vx || car.vy);
|
||||
|
||||
// L'urto fra le due macchinine: due dischi uguali, un po' di rimbalzo. Se l'urto è forte, quella colpita (la più
|
||||
// lenta delle due) si ribalta.
|
||||
function collide(a, b, events) {
|
||||
const dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy), min = CAR.r * 2;
|
||||
if (d >= min || d < 1e-6) return;
|
||||
const nx = dx / d, ny = dy / d, push = (min - d) / 2;
|
||||
a.x -= nx * push; a.y -= ny * push; b.x += nx * push; b.y += ny * push;
|
||||
const rel = (a.vx - b.vx) * nx + (a.vy - b.vy) * ny;
|
||||
if (rel <= 0) return;
|
||||
const j = rel * (1 + RESTITUTION) / 2;
|
||||
const hit = Math.hypot(a.vx, a.vy) < Math.hypot(b.vx, b.vy) ? a : b;
|
||||
a.vx -= j * nx; a.vy -= j * ny; b.vx += j * nx; b.vy += j * ny;
|
||||
if (rel > FLIP) hit.fault = 'cappotte';
|
||||
events?.push({ kind: 'micro-urto', power: Math.min(1, rel / 300) });
|
||||
}
|
||||
function step(track, list, dt, events) {
|
||||
for (const car of list) {
|
||||
const v = Math.hypot(car.vx, car.vy);
|
||||
if (!v) continue;
|
||||
const nv = Math.max(0, v - DECEL * dt);
|
||||
if (nv < 2) { car.vx = car.vy = 0; continue; }
|
||||
car.vx *= nv / v; car.vy *= nv / v;
|
||||
car.x += car.vx * dt; car.y += car.vy * dt;
|
||||
}
|
||||
collide(list[0], list[1], events);
|
||||
for (const car of list) if (car.vx || car.vy) follow(track, car);
|
||||
}
|
||||
|
||||
export function newMicro(seed = Date.now() >>> 0, def = {}) {
|
||||
const level = LEVELS[def.level] ? def.level : 'seria', track = TRACK;
|
||||
const c = { step: 'aim', t: 0, seed, level, skill: LEVELS[level], current: 'player', power: 0.5, angle: 0,
|
||||
holding: false, holdTime: 0, accumulator: 0, events: [], track, laps: LAPS, flicks: { player: 0, cpu: 0 },
|
||||
line: null, trail: [], returns: [], before: null, plan: null, visual: { time: 0 } };
|
||||
// In griglia: Andrea all'esterno, il Chiaru all'interno, poco prima del traguardo.
|
||||
c.cars = { player: newCar(track, 'player', 18, -11), cpu: newCar(track, 'cpu', 18, 11) };
|
||||
c.angle = c.cars.player.heading;
|
||||
return c;
|
||||
}
|
||||
|
||||
export const canThrow = c => c.step === 'aim' && c.current === 'player';
|
||||
export const finished = (c, car) => car.progress >= c.laps * c.track.length;
|
||||
const snapshot = car => ({ x: car.x, y: car.y, heading: car.heading, idx: car.idx, s: car.s, progress: car.progress });
|
||||
|
||||
export function strike(c, who, angle, power) {
|
||||
c.turn = null;
|
||||
const car = c.cars[who], v = speedFor(power);
|
||||
c.before = { player: snapshot(c.cars.player), cpu: snapshot(c.cars.cpu) };
|
||||
for (const x of cars(c)) x.fault = null;
|
||||
car.vx = Math.cos(angle) * v; car.vy = Math.sin(angle) * v; car.heading = angle;
|
||||
c.returns = c.returns.filter(r => c.visual.time - r.at < 4);
|
||||
if (who === 'player') c.line = null;
|
||||
c.shooter = who; c.angle = angle; c.power = power; c.plan = null; c.trail = [[car.x, car.y]];
|
||||
c.step = 'rolling'; c.t = c.accumulator = 0; c.flicks[who]++;
|
||||
c.events.push({ kind: 'micro-spinta', power }, { kind: 'micro-strisciata', power });
|
||||
return true;
|
||||
}
|
||||
|
||||
// La spinta provata a vuoto su una copia: dove arriva ciascuna macchinina e se qualcuna esce.
|
||||
export function simulate(c, who, angle, power) {
|
||||
const list = cars(c).map(x => ({ ...x, fault: null })), car = list.find(x => x.role === who), v = speedFor(power);
|
||||
car.vx = Math.cos(angle) * v; car.vy = Math.sin(angle) * v;
|
||||
for (let i = 0; i < 6 / FIXED_STEP && list.some(x => x.vx || x.vy); i++) step(c.track, list, FIXED_STEP);
|
||||
return Object.fromEntries(list.map(x => [x.role, x]));
|
||||
}
|
||||
|
||||
// Il Chiaru guarda la pista davanti a sé: prova dei punti più o meno lontani, al centro e verso i bordi, e per ognuno
|
||||
// anche le spinte storte che gli possono scappare; sceglie quello che in media lo porta più avanti senza uscire.
|
||||
// Le prove si fanno a pezzi (BUDGET per fotogramma): tutte insieme fermerebbero il gioco per un decimo di secondo.
|
||||
const BUDGET = 20;
|
||||
function cpuCandidates(c) {
|
||||
const me = c.cars.cpu, k = c.skill, list = [];
|
||||
for (let ahead = 30; ahead <= k.ahead; ahead += 10) for (const side of [-10, 0, 10]) {
|
||||
const i = (me.idx + Math.round(ahead / c.track.step)) % c.track.n, [px, py] = c.track.pts[i], [tx, ty] = c.track.tan[i];
|
||||
const x = px - ty * side, y = py + tx * side, angle = Math.atan2(y - me.y, x - me.x), power = powerFor(Math.hypot(x - me.x, y - me.y));
|
||||
for (const [da, dp] of [[0, 0], [k.wobble, 0], [-k.wobble, 0], [0, k.powerNoise], [0, -k.powerNoise]])
|
||||
list.push({ angle, power, try: [angle + da, clamp(power + dp, 0, 1)] });
|
||||
}
|
||||
return list;
|
||||
}
|
||||
export function cpuThink(c, budget = BUDGET) {
|
||||
const t = c.thinking ??= { todo: cpuCandidates(c), scores: new Map() }, me = c.cars.cpu;
|
||||
for (let i = 0; i < budget && t.todo.length; i++) {
|
||||
const x = t.todo.pop(), r = simulate(c, 'cpu', ...x.try).cpu, key = x.angle + '|' + x.power;
|
||||
t.scores.set(key, { angle: x.angle, power: x.power, score: (t.scores.get(key)?.score ?? 0) + (r.fault ? -80 : r.progress - me.progress) });
|
||||
}
|
||||
if (t.todo.length) return false;
|
||||
const best = [...t.scores.values()].reduce((a, b) => b.score > a.score ? b : a, { score: -Infinity, angle: me.heading, power: 0.3 });
|
||||
const k = c.skill;
|
||||
c.plan = { angle: best.angle + deviation(c) * k.wobble, power: clamp(best.power + deviation(c) * k.powerNoise, 0.02, 1) };
|
||||
c.angle = c.plan.angle; c.power = c.plan.power; c.thinking = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Finita la spinta: chi ha fatto un fallo torna al suo posto, e alla fine di ogni giro di spinte si guarda il traguardo.
|
||||
function settle(c) {
|
||||
const names = { player: 'Andrea', cpu: 'il Chiaru' }, outs = cars(c).filter(car => car.fault);
|
||||
// Uscita e ripresa la pista più in là: ha saltato l'erba di mezzo, è un tagliè.
|
||||
for (const car of outs) if (car.fault === 'fuori' && !isOut(project(c.track, car.x, car.y).d)) car.fault = 'taglie';
|
||||
// Le scritte di un giro di spinte restano insieme fino alla spinta dopo di Andrea: così vede anche le sue.
|
||||
const said = outs.map(car => `${FAULTS[car.fault]}! ${names[car.role][0].toUpperCase() + names[car.role].slice(1)} torna dov'era.`);
|
||||
// Dove è uscita resta una croce, e la macchinina si vede tornare indietro (solo disegno).
|
||||
for (const car of outs) {
|
||||
c.returns.push({ role: car.role, x: car.x, y: car.y, heading: car.heading, fault: car.fault, at: c.visual.time });
|
||||
Object.assign(car, c.before[car.role], { vx: 0, vy: 0, fault: null });
|
||||
c.events.push({ kind: 'micro-fuori', power: car.role === 'player' ? 1 : 0.5 });
|
||||
}
|
||||
// Rimessa al suo posto, non deve stare sopra all'altra.
|
||||
const [a, b] = cars(c), d = Math.hypot(b.x - a.x, b.y - a.y);
|
||||
if (outs.length === 1 && d < CAR.r * 2) {
|
||||
const back = outs[0], other = back === a ? b : a, nx = d ? (back.x - other.x) / d : 1, ny = d ? (back.y - other.y) / d : 0;
|
||||
back.x = other.x + nx * CAR.r * 2; back.y = other.y + ny * CAR.r * 2;
|
||||
}
|
||||
const mine = c.cars[c.shooter];
|
||||
if (!outs.length && finished(c, mine)) said.push(`${c.shooter === 'player' ? 'Andrea' : 'Il Chiaru'} taglia il traguardo!`);
|
||||
c.line = [c.line, ...said].filter(Boolean).join(' ') || null;
|
||||
if (!outs.includes(mine) && finished(c, mine)) c.events.push({ kind: 'micro-arrivo', power: c.shooter === 'player' ? 1 : 0.5 });
|
||||
// Il giro di spinte si chiude col Chiaru: solo allora si decide, così hanno spinto tutti e due lo stesso numero di volte.
|
||||
const end = c.laps * c.track.length, over = { player: c.cars.player.progress - end, cpu: c.cars.cpu.progress - end };
|
||||
if (c.shooter === 'cpu' && (over.player >= 0 || over.cpu >= 0)) {
|
||||
const winner = over.player >= 0 && over.cpu >= 0 ? (Math.abs(over.player - over.cpu) < 1 ? 'draw' : over.player > over.cpu ? 'player' : 'cpu') : over.player >= 0 ? 'player' : 'cpu';
|
||||
c.step = 'result'; c.result = { winner, flicks: { ...c.flicks } };
|
||||
return;
|
||||
}
|
||||
// Chi ha già tagliato il traguardo aspetta; spinge l'altro.
|
||||
c.current = OTHER[c.shooter];
|
||||
c.step = 'aim'; c.t = 0;
|
||||
if (c.current === 'player') c.angle = aimAhead(c);
|
||||
}
|
||||
// Quando tocca ad Andrea la mira parte già lungo la pista, un po' più avanti.
|
||||
function aimAhead(c) {
|
||||
const car = c.cars.player, i = (car.idx + Math.round(60 / c.track.step)) % c.track.n, [x, y] = c.track.pts[i];
|
||||
return Math.atan2(y - car.y, x - car.x);
|
||||
}
|
||||
|
||||
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.turn && canThrow(c) && !c.holding) {
|
||||
c.turn.time += dt;
|
||||
c.angle += c.turn.dir * dt * (c.turn.fine ? 0.05 : 0.6 + 1.8 * Math.min(1, c.turn.time / 1.2));
|
||||
}
|
||||
if (c.holding) {
|
||||
c.holdTime += dt;
|
||||
c.charging = c.holdTime >= c.holdDelay;
|
||||
if (c.charging) c.power = c.holdDelay ? Math.min(1, c.previousPower + powerAt(c.holdTime - c.holdDelay)) : powerAt(c.holdTime);
|
||||
// Alla carica piena il dito non deve restare: si conta quanto ci sta, per l'accompagnè.
|
||||
c.overHold = c.charging && c.power >= 1 ? (c.overHold ?? 0) + dt : 0;
|
||||
}
|
||||
if (c.step === 'rolling') {
|
||||
c.accumulator += dt;
|
||||
const list = cars(c), car = c.cars[c.shooter];
|
||||
while (c.accumulator >= FIXED_STEP) { step(c.track, list, FIXED_STEP, c.events); c.accumulator -= FIXED_STEP; }
|
||||
const last = c.trail.at(-1);
|
||||
if (!last || Math.hypot(last[0] - car.x, last[1] - car.y) > 3) c.trail.push([car.x, car.y]);
|
||||
if (!moving(c)) { c.step = 'settle'; c.t = 0; }
|
||||
} else if (c.step === 'settle' && c.t >= REST) settle(c);
|
||||
else if (c.step === 'aim' && c.current === 'cpu') {
|
||||
if (!c.plan && c.t >= THINK * 0.3) cpuThink(c);
|
||||
if (c.plan && c.t >= THINK) strike(c, 'cpu', c.plan.angle, c.plan.power);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Comandi del giocatore: gli stessi del biliardo (mira col mouse o col dito, si tiene premuto per la forza).
|
||||
export function aimAt(c, x, y) {
|
||||
if (!canThrow(c) || c.holding || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||
const car = c.cars.player;
|
||||
if (Math.hypot(x - car.x, y - car.y) < CAR.r) return false;
|
||||
c.angle = Math.atan2(y - car.y, x - car.x); return true;
|
||||
}
|
||||
export const aimScreen = (c, [x, y]) => x >= 0 && x <= 640 && y >= 24 && y <= 360 && aimAt(c, x, y);
|
||||
export function nudge(c, dir, fine = false) {
|
||||
if (!canThrow(c) || c.holding || !Number.isFinite(dir)) return false;
|
||||
c.angle += dir * (fine ? 0.004 : 0.03); return true;
|
||||
}
|
||||
export function setPower(c, value) {
|
||||
if (!canThrow(c) || c.holding || !Number.isFinite(value)) return false;
|
||||
c.power = clamp(value, 0, 1); return true;
|
||||
}
|
||||
export function shoot(c) { return canThrow(c) && !c.holding && strike(c, 'player', c.angle, c.power); }
|
||||
export function turn(c, dir, owner, fine = false) {
|
||||
if (!canThrow(c) || c.holding || c.turn) return false;
|
||||
c.turn = { dir, owner, fine, time: 0 }; c.angle += dir * (fine ? 0.004 : 0.03);
|
||||
c.aimLocked = true; return true;
|
||||
}
|
||||
export function stopTurn(c, owner) {
|
||||
if (c.turn?.owner !== owner) return false;
|
||||
c.turn = null; return true;
|
||||
}
|
||||
export function key(c, key, down, fine = false) {
|
||||
if (!['ArrowLeft', 'ArrowRight'].includes(key)) return false;
|
||||
if (down) { if (c.turn?.owner === key) c.turn.fine = fine; else turn(c, key === 'ArrowLeft' ? -1 : 1, key, fine); }
|
||||
else stopTurn(c, key);
|
||||
return true;
|
||||
}
|
||||
export function press(c, pointerId) {
|
||||
if (!canThrow(c) || c.holding) return false;
|
||||
c.turn = null; c.pointerId = pointerId; c.holdDelay = pointerId === undefined ? 0 : LONG_PRESS;
|
||||
c.previousPower = c.power; c.holding = true; c.charging = false; c.holdTime = 0;
|
||||
if (!c.holdDelay) c.power = 0;
|
||||
return true;
|
||||
}
|
||||
export function cancel(c, pointerId) {
|
||||
if (pointerId !== undefined && c.pointerId !== pointerId) return false;
|
||||
c.turn = null; c.charging = false;
|
||||
if (!c.holding) return false;
|
||||
c.holding = false; c.power = c.previousPower; c.holdTime = 0; c.overHold = 0; return true;
|
||||
}
|
||||
export function release(c, pointerId) {
|
||||
if (c.pointerId !== pointerId || !c.holding) return false;
|
||||
if (c.holdTime < Math.max(0.14, c.holdDelay)) return cancel(c, pointerId); // il tocco mira soltanto
|
||||
const accompagne = (c.overHold ?? 0) > ACCOMPAGNA;
|
||||
c.holding = c.charging = false; c.overHold = 0;
|
||||
strike(c, 'player', c.angle, c.power);
|
||||
if (accompagne) c.cars.player.fault = 'accompagne';
|
||||
return true;
|
||||
}
|
||||
// Sta tenendo il dito oltre la carica piena: il disegno lo avvisa prima che diventi un accompagnè.
|
||||
export const overHeld = c => c.holding && (c.overHold ?? 0) > 0;
|
||||
|
||||
// La guida: dove si fermerebbe la macchinina, se nessuno la tocca, e se ci arriva restando in pista.
|
||||
export function guide(c) {
|
||||
const car = c.cars.player, length = reach(speedFor(c.power)), ux = Math.cos(c.angle), uy = Math.sin(c.angle);
|
||||
let idx = car.idx, out = false;
|
||||
for (let d = 0; d <= length; d += 2) {
|
||||
const p = project(c.track, car.x + ux * d, car.y + uy * d, idx);
|
||||
idx = p.i; if (isOut(p.d)) { out = true; break; }
|
||||
}
|
||||
return { x: car.x + ux * length, y: car.y + uy * length, length, out };
|
||||
}
|
||||
|
||||
export function button(c) {
|
||||
if (c.step === 'result') return 'Finita';
|
||||
if (c.step === 'rolling' || c.step === 'settle') return 'In movimento…';
|
||||
return c.current === 'cpu' ? 'Spinge il Chiaru…' : 'Spingi';
|
||||
}
|
||||
// Quanta pista manca al traguardo, in percentuale, per il tabellone.
|
||||
export const done = (c, who) => clamp(c.cars[who].progress / (c.laps * c.track.length), 0, 1);
|
||||
export function status(c) {
|
||||
const where = `Andrea ${Math.round(done(c, 'player') * 100)}%, Chiaru ${Math.round(done(c, 'cpu') * 100)}% del giro.`;
|
||||
if (c.step === 'result') return `${where} ${c.result.winner === 'player' ? 'Hai battuto il Chiaru!' : c.result.winner === 'cpu' ? 'Ha vinto il Chiaru.' : 'Arrivati pari.'}`;
|
||||
if (c.step === 'rolling' || c.step === 'settle') return `${where} ${c.line ?? ''}`.trim();
|
||||
if (c.current === 'cpu') return `${where} Spinge il Chiaru…`;
|
||||
return `${where} ${c.line ? c.line + ' ' : ''}Tocca a te: mira e spingi.`;
|
||||
}
|
||||
Reference in New Issue
Block a user