Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
147 lines
8.6 KiB
JavaScript
147 lines
8.6 KiB
JavaScript
// La cerbottana (idea di Andrea, 29 ottobre): da ragazzi in piazza si comprava da Pierin di Frilli lo stucco verde,
|
||
// dall'odore inconfondibile, lo si metteva intorno alla cerbottana, si facevano i pallini e si «sparava» al muro.
|
||
// Qui: un muro della piazza visto di fronte, un bersaglio fatto col gesso, le lattine sul muretto e (a seria e a fassi
|
||
// male) un barattolo appeso che dondola. Si mira, si tiene premuto per prendere fiato e si lascia: con poco fiato il
|
||
// pallino cade sotto, con troppo va sopra; il mirino ondeggia col respiro, e chi resta troppo a fiato pieno tossisce.
|
||
// I pallini di stucco restano attaccati al muro. Unità: i pixel del primo piano 640×360.
|
||
|
||
export const WALL_BOTTOM = 244; // sotto comincia il muretto: lì il pallino non attacca
|
||
export const LEDGE = 244; // dove poggiano le lattine
|
||
export const PELLETS = 12, SWING = 1, TAP = 0.14, COUGH = 0.6, FLIGHT = 0.3, REST = 0.5, BEST = 0.7, DROP = 300;
|
||
export const TARGET = { x: 320, y: 118, rings: [[8, 10], [18, 5], [30, 2]] };
|
||
export const CAN = { w: 10, h: 16, points: 3 }, SWINGER = { x: 470, y: 8, length: 92, w: 12, h: 16, points: 8 };
|
||
// I livelli: quanto ondeggia il mirino (px), quanti punti servono, se c'è il barattolo appeso, se la barra del fiato
|
||
// mostra il punto giusto, e quanto sono grandi i cerchi del bersaglio.
|
||
export const LEVELS = {
|
||
bona: { sway: 3, goal: 30, swinger: false, sweet: true, rings: 1.3 },
|
||
seria: { sway: 6, goal: 40, swinger: true, sweet: false, rings: 1 },
|
||
male: { sway: 10, goal: 50, swinger: true, sweet: false, rings: 0.85 },
|
||
};
|
||
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 newCerbottana(seed = Date.now() >>> 0, def = {}) {
|
||
const level = LEVELS[def.level] ? def.level : 'seria';
|
||
return { step: 'aim', t: 0, seed, level, skill: LEVELS[level], aim: { x: TARGET.x, y: TARGET.y }, power: BEST,
|
||
holding: false, holdTime: 0, charging: false, left: PELLETS, points: 0, events: [], shot: null, stuck: [], line: null,
|
||
cans: [200, 250, 390, 440].map(x => ({ x, down: false, fall: 0 })), swing: { hits: 0, spin: 0 }, visual: { time: 0 } };
|
||
}
|
||
export const canThrow = c => c.step === 'aim' && c.left > 0;
|
||
export const canAdjust = canThrow;
|
||
export const powerAt = time => clamp(time / SWING, 0, 1);
|
||
export const rings = c => TARGET.rings.map(([r, p]) => [r * c.skill.rings, p]);
|
||
|
||
// Il respiro: il mirino gira piano intorno al punto voluto (due onde, non si ripete subito); trattenendo il fiato pieno
|
||
// trema di più.
|
||
export function sway(c) {
|
||
const t = c.visual.time, a = c.skill.sway * (1 + (c.overHold ?? 0) * 2);
|
||
return { x: a * (Math.sin(t * 1.7) + 0.5 * Math.sin(t * 3.1 + 1)), y: a * 0.8 * (Math.sin(t * 1.3 + 2) + 0.5 * Math.sin(t * 2.9)) };
|
||
}
|
||
export function reticle(c) { const s = sway(c); return { x: c.aim.x + s.x, y: c.aim.y + s.y }; }
|
||
// Dove arriva il pallino: dove punta il mirino, più su o più giù secondo il fiato (BEST è il fiato giusto).
|
||
export const landing = (c, power = c.power) => { const r = reticle(c); return { x: r.x, y: r.y + (BEST - power) * DROP }; };
|
||
// Il barattolo appeso: un pendolo che dondola sotto la gronda (e gira su sé stesso se colpito).
|
||
export function swinger(c) {
|
||
const a = 0.5 * Math.sin(c.visual.time * 1.6);
|
||
return { x: SWINGER.x + Math.sin(a) * SWINGER.length, y: SWINGER.y + Math.cos(a) * SWINGER.length, a };
|
||
}
|
||
|
||
export function shoot(c) {
|
||
if (!canThrow(c) || c.holding) return false;
|
||
return fire(c, c.power);
|
||
}
|
||
function fire(c, power, cough = false) {
|
||
const from = { x: 320, y: 380 }, to = cough ? { x: 300 + random(c) * 40, y: 330 } : landing(c, power);
|
||
c.left--; c.power = power;
|
||
c.shot = { from, to, t: 0, cough };
|
||
c.step = 'flying'; c.line = null;
|
||
c.events.push({ kind: cough ? 'cerbottana-tosse' : 'cerbottana-pff', power });
|
||
return true;
|
||
}
|
||
// Arrivato: prima il barattolo appeso, poi le lattine, poi il bersaglio; sul muro il pallino resta attaccato.
|
||
function land(c) {
|
||
const { x, y } = c.shot.to;
|
||
let got = 0, what = null;
|
||
if (c.shot.cough) what = 'Coff! Il pallino è finito per terra.';
|
||
if (!what && c.skill.swinger) {
|
||
const s = swinger(c);
|
||
if (Math.abs(x - s.x) <= SWINGER.w / 2 + 1 && Math.abs(y - s.y - SWINGER.h / 2) <= SWINGER.h / 2 + 1) {
|
||
got = SWINGER.points; c.swing.hits++; c.swing.spin = 1; what = `Il barattolo appeso! ${got} punti.`;
|
||
c.events.push({ kind: 'cerbottana-latta', power: 1 });
|
||
}
|
||
}
|
||
if (!what) {
|
||
const can = c.cans.find(k => !k.down && Math.abs(x - k.x) <= CAN.w / 2 + 1 && y >= LEDGE - CAN.h - 1 && y <= LEDGE);
|
||
if (can) { can.down = true; got = CAN.points; what = `Lattina giù! ${got} punti.`; c.events.push({ kind: 'cerbottana-latta', power: 0.8 }); }
|
||
}
|
||
if (!what) {
|
||
if (y < WALL_BOTTOM && y > 4 && x > 4 && x < 636) {
|
||
c.stuck.push({ x, y });
|
||
const d = Math.hypot(x - TARGET.x, y - TARGET.y), ring = rings(c).find(([r]) => d <= r);
|
||
got = ring ? ring[1] : 0;
|
||
what = got ? `Nel bersaglio: ${got} ${got === 1 ? 'punto' : 'punti'}.` : y < TARGET.y - 40 ? 'Troppo fiato: alto.' : 'Sul muro, fuori dal bersaglio.';
|
||
c.events.push({ kind: 'cerbottana-muro', power: 0.6 });
|
||
} else what = 'Poco fiato: il pallino è caduto corto.';
|
||
}
|
||
c.points += got; c.line = what;
|
||
c.shot = null; c.step = 'rest'; c.t = 0;
|
||
}
|
||
|
||
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;
|
||
for (const k of c.cans) if (k.down) k.fall = Math.min(1, k.fall + dt * 3);
|
||
if (c.swing.spin) c.swing.spin = Math.max(0, c.swing.spin - dt);
|
||
if (c.holding) {
|
||
c.holdTime += dt; c.charging = c.holdTime >= TAP;
|
||
c.power = powerAt(c.holdTime);
|
||
c.overHold = c.power >= 1 ? (c.overHold ?? 0) + dt : 0;
|
||
// Troppo a fiato pieno: tosse, e il pallino è perso.
|
||
if (c.overHold > COUGH) { c.holding = c.charging = false; c.overHold = 0; fire(c, 1, true); }
|
||
}
|
||
if (c.step === 'flying') { c.shot.t += dt; if (c.shot.t >= FLIGHT) land(c); }
|
||
else if (c.step === 'rest' && c.t >= REST) {
|
||
if (c.left > 0) c.step = 'aim';
|
||
else { c.step = 'result'; c.result = { winner: c.points >= c.skill.goal ? 'player' : 'cpu', points: c.points, goal: c.skill.goal }; }
|
||
}
|
||
}
|
||
|
||
// --- Comandi: il mirino segue il mouse o il dito; tenere premuto prende fiato, lasciare spara.
|
||
export function aimScreen(c, [x, y]) {
|
||
if (!canThrow(c) || c.holding || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||
c.aim = { x: clamp(x, 10, 630), y: clamp(y, 24, 300) }; return true;
|
||
}
|
||
export function nudge(c, dir) { if (!canThrow(c) || c.holding) return false; c.aim.x = clamp(c.aim.x + dir * 4, 10, 630); 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 key(c, key, down) {
|
||
const move = { ArrowLeft: [-4, 0], ArrowRight: [4, 0], ArrowUp: [0, -4], ArrowDown: [0, 4] }[key];
|
||
if (!move) return false;
|
||
if (down && canThrow(c) && !c.holding) c.aim = { x: clamp(c.aim.x + move[0], 10, 630), y: clamp(c.aim.y + move[1], 24, 300) };
|
||
return true;
|
||
}
|
||
export function press(c, pointerId) {
|
||
if (!canThrow(c) || c.holding) return false;
|
||
c.pointerId = pointerId; c.holding = true; c.charging = false; c.holdTime = 0; c.previousPower = c.power; c.power = 0; c.overHold = 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.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 < TAP) return cancel(c, pointerId); // il tocco secco mira soltanto
|
||
c.holding = c.charging = false; c.overHold = 0;
|
||
return fire(c, c.power);
|
||
}
|
||
// Trattenendo il fiato pieno: il disegno avvisa prima della tosse.
|
||
export const overHeld = c => c.holding && (c.overHold ?? 0) > 0;
|
||
|
||
export function button(c) { return c.step === 'result' ? 'Finita' : c.step === 'aim' ? 'Spara' : 'In volo…'; }
|
||
export function status(c) {
|
||
const score = `Punti ${c.points} su ${c.skill.goal}, pallini ${c.left} di ${PELLETS}.`;
|
||
if (c.step === 'result') return `${c.result.winner === 'player' ? 'Fatto! ' : 'Lo stucco è finito. '}${score}`;
|
||
return `${score} ${c.line ?? 'Mira, tieni premuto per prendere fiato e lascia.'}`;
|
||
}
|