Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
196 lines
11 KiB
JavaScript
196 lines
11 KiB
JavaScript
// 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.'}`;
|
||
}
|