Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9Quo9nEdXyjXYF8ma
221 lines
12 KiB
JavaScript
221 lines
12 KiB
JavaScript
// La campana al campone delle poste (idea di Andrea, 30 ottobre): disegnata col gesso sul vialetto, a turni col Taglia.
|
||
// Si tira il sassolino nella casella che tocca (dalla 1 alla 8): se cade dentro, senza toccare le righe, si salta tutta
|
||
// la campana fino in cima e indietro, su un piede nelle caselle sole e a gambe larghe nelle coppie, senza mai mettere
|
||
// il piede dove sta il sassolino; tornando, lo si raccoglie dalla casella prima. Chi sbaglia (sassolino fuori o sulla
|
||
// riga, salto fuori tempo) passa la mano; chi ce la fa va avanti alla casella dopo e ritira. Vince chi finisce la 8.
|
||
// Il tiro: la mira dà la direzione, tenere premuto carica la forza (va e torna), lasciare tira. I salti: a ogni salto
|
||
// la lancetta dell'equilibrio attraversa la barra; si preme quando passa nel verde. Unità: i pixel del primo piano
|
||
// 640×360, visto dall'alto (sfondo di Codex: assets/closeups/campone-campana.png).
|
||
|
||
export const W = 66, CY = 181, START = 80, CIELO = { x: START + 6 * W, r: W }, FROM = { x: 40, y: CY }, REACH = 520;
|
||
export const TAP = 0.14, CHARGE = 1.3, FLIGHT = 0.55, HOP = 0.22, REST = 1.6, DONE = 1.1, CPU_WAIT = 0.9, CPU_BEAT = 0.42;
|
||
// Le caselle: 1, 2, 3 da sole; 4 e 5 in coppia (la 4 a sinistra di chi salta, cioè in alto); la 6; 7 e 8 in coppia;
|
||
// in fondo la campana (il mezzo cerchio dove ci si gira; nel codice «cielo», come la chiamano in altri posti).
|
||
export const BOXES = {
|
||
1: { x: START, y: CY - W / 2 }, 2: { x: START + W, y: CY - W / 2 }, 3: { x: START + 2 * W, y: CY - W / 2 },
|
||
4: { x: START + 3 * W, y: CY - W }, 5: { x: START + 3 * W, y: CY }, 6: { x: START + 4 * W, y: CY - W / 2 },
|
||
7: { x: START + 5 * W, y: CY - W }, 8: { x: START + 5 * W, y: CY },
|
||
};
|
||
export const GROUPS = [[1], [2], [3], [4, 5], [6], [7, 8], ['cielo']];
|
||
export const LAST = 8;
|
||
// I livelli: quanto si sparpaglia il sassolino (px), se si vede dove cade, quanto è larga la riga che non si può
|
||
// toccare, quanto ci mette la lancetta dell'equilibrio e quanto è largo il verde; il Taglia: quante volte il sassolino
|
||
// va dove vuole e quanto spesso, a ogni salto, mette giù il piede.
|
||
export const LEVELS = {
|
||
bona: { scatter: 4, guide: true, line: 0, sweep: 1.25, zone: 0.32, cpu: { throw: 0.5, slip: 0.045 } },
|
||
seria: { scatter: 7, guide: false, line: 3, sweep: 1, zone: 0.2, cpu: { throw: 0.7, slip: 0.025 } },
|
||
male: { scatter: 9, guide: false, line: 4, sweep: 0.8, zone: 0.15, cpu: { throw: 0.8, slip: 0.018 } },
|
||
};
|
||
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; }
|
||
|
||
export function center(id) {
|
||
if (id === 'cielo') return { x: CIELO.x + W * 0.42, y: CY };
|
||
const b = BOXES[id]; return { x: b.x + W / 2, y: b.y + W / 2 };
|
||
}
|
||
// In che casella cade un punto, e se tocca la riga (entro `tol` pixel da un bordo).
|
||
export function boxAt([x, y], tol = 0) {
|
||
for (const [id, b] of Object.entries(BOXES)) {
|
||
if (x < b.x || x > b.x + W || y < b.y || y > b.y + W) continue;
|
||
return { id: Number(id), line: Math.min(x - b.x, b.x + W - x, y - b.y, b.y + W - y) < tol };
|
||
}
|
||
const d = Math.hypot(x - CIELO.x, y - CY);
|
||
if (x >= CIELO.x && d <= CIELO.r) return { id: 'cielo', line: Math.min(x - CIELO.x, CIELO.r - d) < tol };
|
||
return null;
|
||
}
|
||
|
||
// I salti per la casella `n`: all'andata tutte le caselle, saltando quella col sassolino (nella coppia si mette un
|
||
// piede solo nell'altra); il cielo; al ritorno, dalla casella prima si raccoglie il sassolino; poi fuori.
|
||
export function beats(n) {
|
||
const step = (g, back) => g.includes(n) ? (g.length > 1 ? [{ boxes: g.filter(k => k !== n), back }] : []) : [{ boxes: g, back }];
|
||
const out = GROUPS.flatMap(g => step(g, false));
|
||
for (let i = GROUPS.length - 2; i >= 0; i--) {
|
||
if (GROUPS[i].includes(n)) out.push({ ...out[out.length - 1], back: true, pick: true });
|
||
out.push(...step(GROUPS[i], true));
|
||
}
|
||
out.push({ boxes: [], back: true, out: true });
|
||
return out;
|
||
}
|
||
|
||
export function newCampana(seed = Date.now() >>> 0, def = {}) {
|
||
const level = LEVELS[def.level] ? def.level : 'seria';
|
||
return { step: 'aim', t: 0, seed, level, skill: LEVELS[level], current: 'player', target: { player: 1, cpu: 1 },
|
||
aim: center(1), power: 0.2, holding: false, holdTime: 0, charging: false, stone: null, shot: null,
|
||
beats: null, beat: -1, meter: null, hop: null, line: null, next: null, events: [], visual: { time: 0 } };
|
||
}
|
||
export const canThrow = c => c.current === 'player' && (c.step === 'aim' || c.step === 'hop');
|
||
export const canAdjust = c => c.current === 'player' && c.step === 'aim';
|
||
export const aimFree = c => !canAdjust(c);
|
||
|
||
// Dove cade il sassolino tirato con questa forza, senza lo sparpaglio.
|
||
export function landing(c, power = c.power) {
|
||
const dx = c.aim.x - FROM.x, dy = c.aim.y - FROM.y, d = Math.hypot(dx, dy) || 1;
|
||
return { x: FROM.x + dx / d * power * REACH, y: FROM.y + dy / d * power * REACH };
|
||
}
|
||
function toss(c, to) {
|
||
c.shot = { from: { ...FROM }, to, t: 0 }; c.stone = null;
|
||
c.step = 'flying'; c.line = null;
|
||
c.events.push({ kind: 'campana-lancio', power: 0.6 });
|
||
}
|
||
function throwStone(c) {
|
||
const p = landing(c), s = c.skill.scatter, spread = () => (random(c) + random(c) + random(c) - 1.5) * s * 1.4;
|
||
toss(c, { x: p.x + spread(), y: p.y + spread() });
|
||
return true;
|
||
}
|
||
const who = c => c.current === 'cpu' ? 'Il Taglia: ' : '';
|
||
const cap = line => line[0].toUpperCase() + line.slice(1);
|
||
// Il sassolino è arrivato: nella casella giusta si salta, se no passa la mano.
|
||
function land(c) {
|
||
const to = c.shot.to, n = c.target[c.current], hit = boxAt([to.x, to.y], c.skill.line);
|
||
c.shot = null; c.stone = to;
|
||
c.events.push({ kind: 'campana-sasso', power: 0.8 });
|
||
if (hit && hit.id === n && !hit.line) {
|
||
c.line = cap(`${who(c)}il sassolino è nella ${n}. Salta!`);
|
||
c.beats = beats(n); c.beat = -1; c.hop = null; c.t = 0;
|
||
c.step = c.current === 'cpu' ? 'cpuHop' : 'hop';
|
||
if (c.step === 'hop') c.meter = { t: -0.5, dir: 1 };
|
||
return;
|
||
}
|
||
fail(c, `${who(c)}${hit?.line ? 'sulla riga!' : hit ? `nella ${hit.id === 'cielo' ? 'campana in cima' : hit.id}, non nella ${n}.` : 'fuori dalla campana.'}`);
|
||
}
|
||
function fail(c, line) {
|
||
c.line = cap(line); c.step = 'rest'; c.t = 0; c.meter = null;
|
||
c.next = c.current === 'player' ? 'cpu' : 'player';
|
||
c.events.push({ kind: 'campana-riga', power: 0.7 });
|
||
}
|
||
// Un salto riuscito: la casella dopo, o il sassolino raccolto, o fuori dalla campana (e la casella è fatta).
|
||
function advance(c) {
|
||
c.beat++;
|
||
const b = c.beats[c.beat];
|
||
c.hop = { t: 0 };
|
||
if (b.pick) { c.stone = null; c.line = cap(`${who(c)}preso il sassolino!`); c.events.push({ kind: 'campana-sasso', power: 0.5 }); }
|
||
else c.events.push({ kind: 'campana-salto', power: 0.6 });
|
||
if (b.out) {
|
||
const n = c.target[c.current]++;
|
||
if (n >= LAST) {
|
||
c.step = 'result'; c.meter = null;
|
||
c.result = { winner: c.current, player: c.target.player - 1, cpu: c.target.cpu - 1 };
|
||
return;
|
||
}
|
||
c.line = cap(`${who(c)}fatta la ${n}! Ora la ${n + 1}.`); c.step = 'done'; c.t = 0; c.meter = null;
|
||
return;
|
||
}
|
||
if (c.meter) c.meter = { t: 0, dir: -c.meter.dir };
|
||
}
|
||
// La lancetta dell'equilibrio, da -1 a 1 (o al contrario, un salto sì e uno no).
|
||
export function needle(c) {
|
||
if (!c.meter) return 0;
|
||
return c.meter.dir * (-1 + 2 * clamp(c.meter.t, 0, c.skill.sweep) / c.skill.sweep);
|
||
}
|
||
function jump(c) {
|
||
if (c.hop && c.hop.t < HOP) return true; // ancora in aria: il tocco non conta
|
||
const u = needle(c);
|
||
if (Math.abs(u) <= c.skill.zone) { advance(c); return true; }
|
||
const early = u * c.meter.dir < 0;
|
||
fail(c, early ? 'Troppo presto: giù l\'altro piede!' : 'Troppo tardi: pestata la riga!');
|
||
return true;
|
||
}
|
||
// Tocca all'altro, o allo stesso per la casella dopo: dalla partenza, senza sassolino.
|
||
function turn(c, current) {
|
||
Object.assign(c, { current, stone: null, beats: null, beat: -1, hop: null, meter: null, t: 0, next: null,
|
||
step: current === 'player' ? 'aim' : 'cpuAim' });
|
||
if (current === 'player') c.aim = center(c.target.player);
|
||
}
|
||
|
||
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.hop) c.hop.t += dt;
|
||
if (c.holding) {
|
||
c.holdTime += dt; c.charging = c.holdTime >= TAP;
|
||
const k = (c.holdTime / CHARGE) % 2; c.power = k <= 1 ? k : 2 - k;
|
||
}
|
||
if (c.step === 'flying') { c.shot.t += dt; if (c.shot.t >= FLIGHT) land(c); }
|
||
else if (c.step === 'hop') {
|
||
if (!c.hop || c.hop.t >= HOP) c.meter.t += dt;
|
||
if (c.meter.t > c.skill.sweep) fail(c, 'Hai perso l\'equilibrio!');
|
||
} else if (c.step === 'cpuAim' && c.t >= CPU_WAIT) {
|
||
// Il Taglia: o nella casella, ben dentro, o mezza casella più in là (sulla riga o nell'altra).
|
||
const n = c.target.cpu, p = center(n), ok = random(c) < c.skill.cpu.throw, j = () => (random(c) - 0.5) * 20;
|
||
toss(c, ok ? { x: p.x + j(), y: p.y + j() } : { x: p.x + (random(c) < 0.5 ? -1 : 1) * W * (0.5 + random(c) * 0.3), y: p.y + j() });
|
||
} else if (c.step === 'cpuHop' && c.t >= CPU_BEAT) {
|
||
c.t = 0;
|
||
if (random(c) < c.skill.cpu.slip) fail(c, random(c) < 0.5 ? 'Il Taglia ha pestato la riga!' : 'Il Taglia ha messo giù l\'altro piede!');
|
||
else advance(c);
|
||
} else if (c.step === 'rest' && c.t >= REST) turn(c, c.next);
|
||
else if (c.step === 'done' && c.t >= DONE) turn(c, c.current);
|
||
}
|
||
|
||
// --- Comandi: la mira segue il mouse o il dito; tenere premuto carica la forza, lasciare tira; ai salti si preme.
|
||
export function aimScreen(c, [x, y]) {
|
||
if (!canAdjust(c) || c.holding || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||
c.aim = { x: clamp(x, FROM.x + 30, 630), y: clamp(y, 80, 285) }; return true;
|
||
}
|
||
export function setPower(c, v) { if (!canAdjust(c) || c.holding) return false; c.power = clamp(v, 0, 1); return true; }
|
||
export function nudge(c, dir) { if (!canAdjust(c) || c.holding) return false; c.aim.y = clamp(c.aim.y + dir * 3, 80, 285); return true; }
|
||
// Frecce: su e giù la mira, destra e sinistra la forza (più in là, più in qua).
|
||
export function key(c, key, down) {
|
||
const move = { ArrowUp: [0, -3], ArrowDown: [0, 3], ArrowLeft: [-0.01, 0], ArrowRight: [0.01, 0] }[key];
|
||
if (!move) return false;
|
||
if (down && canAdjust(c) && !c.holding) { c.aim.y = clamp(c.aim.y + move[1], 80, 285); c.power = clamp(c.power + move[0], 0, 1); }
|
||
return true;
|
||
}
|
||
export function press(c, pointerId) {
|
||
if (c.current !== 'player') return false;
|
||
if (c.step === 'hop') return jump(c);
|
||
if (!canAdjust(c) || c.holding) return false;
|
||
c.pointerId = pointerId; c.holding = true; c.charging = false; c.holdTime = 0; c.power = 0;
|
||
return true;
|
||
}
|
||
export function cancel(c, pointerId) {
|
||
if (pointerId !== undefined && c.pointerId !== pointerId) return false;
|
||
if (!c.holding) return false;
|
||
c.holding = c.charging = false; c.holdTime = 0; return true;
|
||
}
|
||
export function release(c, pointerId) {
|
||
if (c.pointerId !== pointerId || !c.holding) return false;
|
||
if (c.holdTime < TAP) return cancel(c, pointerId); // il tocco secco mira soltanto
|
||
c.holding = c.charging = false; c.holdTime = 0;
|
||
return throwStone(c);
|
||
}
|
||
// Il pulsante: tira con la forza del cursore, o salta.
|
||
export function shoot(c) {
|
||
if (c.current !== 'player' || c.holding) return false;
|
||
if (c.step === 'hop') return jump(c);
|
||
return canAdjust(c) && throwStone(c);
|
||
}
|
||
|
||
const box = n => n > LAST ? 'finito' : `alla ${n}`;
|
||
export function button(c) {
|
||
if (c.step === 'result') return 'Finita';
|
||
if (c.current === 'cpu') return 'Tocca al Taglia…';
|
||
return { aim: 'Tira', flying: 'In volo…', hop: 'Salta' }[c.step] ?? 'Aspetta…';
|
||
}
|
||
export function status(c) {
|
||
const score = `Andrea ${box(c.target.player)}, il Taglia ${box(c.target.cpu)}.`;
|
||
if (c.step === 'result') return `${c.result.winner === 'player' ? 'Campana finita: battuto il Taglia! ' : 'Il Taglia ha finito la campana. '}${score}`;
|
||
const hint = c.current === 'cpu' ? 'Tocca al Taglia.' : c.step === 'hop' ? 'Salta quando la lancetta è nel verde.'
|
||
: `Tira il sassolino nella ${c.target.player}: mira, tieni premuto per la forza e lascia.`;
|
||
return `${score} ${c.line ?? hint}`;
|
||
}
|