// 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}`; }