Files
leggenda-del-regolo/src/engine/cerbottana.js
T

147 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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.'}`;
}