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

196 lines
11 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 Condor al campone delle poste (idea di Andrea e del Taglia, 29 ottobre): la pistola ad aria compressa coi pallini
// gialli. Si gareggia col Taglia: lui ha già tirato, e il suo punteggio è quello da battere. Come la cerbottana: il
// mirino ondeggia col respiro; tenere premuto trattiene il fiato (il mirino si ferma), lasciare spara; chi tiene troppo
// resta senza fiato, il mirino trema e il colpo parte da solo. Il pallino cala con la distanza: l'albero è vicino, la
// panchina a metà, l'altalena e la porta in fondo (lì si mira più sopra). Bersagli: il cartone inchiodato all'albero, le lattine sulla panchina rossa, le
// bottigliette sulla traversa della porta, il palloncino legato all'altalena e (a seria e a fassi male) la lattina che
// il Taglia tira in aria. Unità: i pixel del primo piano 640×360.
export const PELLETS = 12, TAP = 0.14, REST = 0.6, STILL = 0.8, BREATH = 2.6, GASP = 1, DROP = 26;
// Dove stanno le cose nello sfondo di Codex (assets/closeups/campone-condor.png): il cartone sul tronco, il sedile
// della panchina, la traversa della porta, la sbarra dell'altalena dove è legato il palloncino.
export const CARTONE = { x: 98, y: 140, w: 46, h: 50, rings: [[5, 10], [11, 5], [17, 2]] };
export const BENCH = 210, CAN = { w: 10, h: 14, points: 3 };
export const BAR = 177, BOTTLE = { w: 6, h: 12, points: 5 };
export const BALLOON = { x: 428, y: 134, knot: 170, r: 8, points: 6 };
export const VOLO = { w: 12, h: 16, points: 8 };
// I livelli: quanto ondeggia il mirino (px), se si vede dove arriva il pallino, la lattina in aria (ogni quanti
// secondi, quanto ci mette ad attraversare), quanto sono grandi i cerchi del cartone e quanto ha fatto il Taglia.
export const LEVELS = {
bona: { sway: 3, guide: true, volo: null, rings: 1.25, taglia: 22 },
seria: { sway: 6, guide: false, volo: { every: 6, cross: 2.2 }, rings: 1, taglia: 34 },
male: { sway: 10, guide: false, volo: { every: 4.5, cross: 1.7 }, rings: 0.85, taglia: 44 },
};
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 newCondor(seed = Date.now() >>> 0, def = {}) {
const level = LEVELS[def.level] ? def.level : 'seria';
const c = { step: 'aim', t: 0, seed, level, skill: LEVELS[level], aim: { x: CARTONE.x, y: CARTONE.y }, power: 0,
holding: false, holdTime: 0, charging: false, left: PELLETS, points: 0, events: [], shot: null, holes: [], line: null,
cans: [246, 276, 306, 336].map(x => ({ x, down: false, fall: 0 })),
bottles: [538, 560, 582].map(x => ({ x, down: false, fall: 0 })),
balloon: { popped: false, pop: 0 }, volo: null, nextVolo: 2.5, visual: { time: 0 } };
// Il punteggio del Taglia: quello del livello, qualche punto in più o in meno.
c.taglia = c.skill.taglia + Math.floor(random(c) * 7) - 3;
return c;
}
export const canThrow = c => c.step === 'aim' && c.left > 0;
export const canAdjust = canThrow;
export const rings = c => CARTONE.rings.map(([r, p]) => [r * c.skill.rings, p]);
// Il fiato: fermo dopo STILL secondi di respiro trattenuto (il mirino ondeggia un terzo), poi dopo BREATH comincia a
// mancare e il mirino trema sempre di più; dopo GASP ancora il colpo parte da solo.
export function steady(c) {
if (!c.holding) return 1;
const h = c.holdTime;
if (h < BREATH) return 1 - 0.7 * Math.min(1, h / STILL);
return 0.3 + (h - BREATH) * 2.5;
}
export function sway(c) {
const t = c.visual.time, a = c.skill.sway * steady(c), shake = c.holding && c.holdTime > BREATH ? Math.sin(t * 31) * 0.4 : 0;
return { x: a * (Math.sin(t * 1.7) + 0.5 * Math.sin(t * 3.1 + 1) + shake), y: a * 0.8 * (Math.sin(t * 1.3 + 2) + 0.5 * Math.sin(t * 2.9) + shake) };
}
export function reticle(c) { const s = sway(c); return { x: c.aim.x + s.x, y: c.aim.y + s.y }; }
// Quanto è lontano un punto, a strisce: l'albero, la panchina, l'altalena, la porta in fondo.
export const far = x => x < 170 ? 0.45 : x < 380 ? 0.6 : x < 490 ? 0.85 : 1;
// Dove arriva il pallino: sotto il mirino, tanto più quanto il punto è lontano.
export function landing(c) { const r = reticle(c); return { x: r.x, y: r.y + DROP * far(r.x) ** 2 }; }
export const flight = x => 0.22 + 0.2 * far(x);
// Il palloncino legato all'altalena: dondola col vento.
export function balloon(c) {
const t = c.visual.time;
return { x: BALLOON.x + 7 * Math.sin(t * 1.1), y: BALLOON.y + 4 * Math.sin(t * 1.7 + 1) };
}
// La lattina che tira il Taglia: da destra in basso, una parabola verso sinistra.
export function voloAt(c, t = c.volo?.t) {
if (!c.volo) return null;
const k = t / c.volo.cross;
if (k < 0 || k > 1) return null;
return { x: 610 - 560 * k, y: 300 - 4 * 230 * k * (1 - k), a: k * 9 };
}
export function shoot(c) {
if (!canThrow(c) || c.holding) return false;
return fire(c);
}
function fire(c, gasp = false) {
const to = landing(c);
c.left--;
// La lattina in aria è vicina (la tira il Taglia, accanto): lì il pallino non fa in tempo a calare.
c.shot = { from: { x: 352, y: 372 }, to, aim: reticle(c), t: 0, time: flight(to.x), gasp };
c.step = 'flying'; c.line = null;
c.events.push({ kind: 'condor-pam', power: 1 });
return true;
}
const inBox = (x, y, cx, bottom, w, h) => Math.abs(x - cx) <= w / 2 + 1 && y >= bottom - h - 1 && y <= bottom + 1;
// Arrivato: prima quello che si muove (la lattina in aria, il palloncino), poi bottiglie, lattine e il cartone.
function land(c) {
const { x, y } = c.shot.to;
let got = 0, what = null;
const v = voloAt(c), a = c.shot.aim;
if (v && !c.volo.hit && Math.hypot(a.x - v.x, a.y - v.y) <= VOLO.w / 2 + 2) {
got = VOLO.points; c.volo.hit = true; c.volo.fall = { x: v.x, y: v.y, vy: -60 }; what = `Presa al volo! ${got} punti.`;
c.events.push({ kind: 'cerbottana-latta', power: 1 });
}
if (!what && !c.balloon.popped) {
const b = balloon(c);
if (Math.hypot(x - b.x, y - b.y) <= BALLOON.r + 1) {
c.balloon.popped = true; got = BALLOON.points; what = `Bum! Il palloncino. ${got} punti.`;
c.events.push({ kind: 'condor-palloncino', power: 1 });
}
}
if (!what) {
const bottle = c.bottles.find(k => !k.down && inBox(x, y, k.x, BAR, BOTTLE.w, BOTTLE.h));
if (bottle) { bottle.down = true; got = BOTTLE.points; what = `Bottiglietta giù! ${got} punti.`; c.events.push({ kind: 'condor-plastica', power: 0.8 }); }
}
if (!what) {
const can = c.cans.find(k => !k.down && inBox(x, y, k.x, BENCH, CAN.w, CAN.h));
if (can) { can.down = true; got = CAN.points; what = `Lattina giù! ${got} punti.`; c.events.push({ kind: 'cerbottana-latta', power: 0.8 }); }
}
if (!what) {
const inCard = Math.abs(x - CARTONE.x) <= CARTONE.w / 2 && Math.abs(y - CARTONE.y) <= CARTONE.h / 2;
if (inCard) {
c.holes.push({ x, y });
const d = Math.hypot(x - CARTONE.x, y - CARTONE.y), ring = rings(c).find(([r]) => d <= r);
got = ring ? ring[1] : 0;
what = got ? `Nel cartone: ${got} ${got === 1 ? 'punto' : 'punti'}.` : 'Nel cartone, fuori dai cerchi.';
c.events.push({ kind: 'cerbottana-muro', power: 0.5 });
} else what = 'Niente: il pallino giallo è finito nel prato.';
}
if (c.shot.gasp && !got) what = 'Senza fiato! ' + what;
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, ...c.bottles]) if (k.down) k.fall = Math.min(1, k.fall + dt * 3);
if (c.balloon.popped) c.balloon.pop = Math.min(1, c.balloon.pop + dt * 4);
// La lattina in aria: il Taglia la tira ogni tanto (non a fine partita).
const v = c.skill.volo;
if (v && c.step !== 'result') {
if (c.volo) {
c.volo.t += dt;
if (c.volo.fall) { c.volo.fall.vy += 600 * dt; c.volo.fall.y += c.volo.fall.vy * dt; c.volo.fall.x -= 40 * dt; }
if (c.volo.t > c.volo.cross + 0.6) { c.volo = null; c.nextVolo = v.every * (0.8 + random(c) * 0.4); }
} else if ((c.nextVolo -= dt) <= 0) {
c.volo = { t: 0, cross: v.cross, hit: false, fall: null };
c.events.push({ kind: 'condor-lancio', power: 0.6 });
}
}
if (c.holding) {
c.holdTime += dt; c.charging = c.holdTime >= TAP;
c.power = clamp(c.holdTime / STILL, 0, 1);
if (c.holdTime > BREATH + GASP) { c.holding = c.charging = false; c.holdTime = 0; c.power = 0; fire(c, true); }
}
if (c.step === 'flying') {
c.shot.t += dt; if (c.shot.t >= c.shot.time) land(c);
} else if (c.step === 'rest' && c.t >= REST) {
if (c.left > 0) c.step = 'aim';
else {
c.step = 'result';
const winner = c.points > c.taglia ? 'player' : c.points === c.taglia ? 'draw' : 'cpu';
c.result = { winner, points: c.points, taglia: c.taglia };
}
}
}
// --- Comandi: il mirino segue il mouse o il dito; tenere premuto trattiene il 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, 30, 320) }; 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 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], 30, 320) };
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.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; c.power = 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; c.power = 0;
return fire(c);
}
export const breathless = c => c.holding && c.holdTime > BREATH;
export function button(c) { return c.step === 'result' ? 'Finita' : c.step === 'aim' ? 'Spara' : c.step === 'flying' ? 'In volo…' : 'Ricarico…'; }
export function status(c) {
const score = `Punti ${c.points}, il Taglia ne ha fatti ${c.taglia}; pallini ${c.left} di ${PELLETS}.`;
if (c.step === 'result') return `${{ player: 'Battuto il Taglia! ', draw: 'Pari col Taglia. ', cpu: 'Ha vinto il Taglia. ' }[c.result.winner]}${score}`;
return `${score} ${c.line ?? 'Mira, tieni premuto per trattenere il fiato e lascia per sparare.'}`;
}