Il minigioco delle bruciate alla Smarronata: coi volontari della Misericordia si gira a mano la manovella del cilindro sul fuoco, facendo giri col dito o col mouse; troppo piano si carbonizzano sotto e restano crude sopra, troppo forte volano nel fuoco; cotte, nel sacco (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
// Le bruciate della Smarronata (Andrea, 31 ottobre: «a manovella, a mano, fallo così»): coi volontari della
|
||||
// Misericordia si gira la manovella del cilindro a rete sopra il fuoco. Il gesto: tenendo premuto si fanno dei giri,
|
||||
// grandi o piccoli, dove si vuole (conta quanto gira la direzione del dito, non dove sta), come la manovella vera.
|
||||
// Le castagne stanno in fondo al cilindro: girando, la rete le alza e loro ricascano e si rimescolano. Troppo piano,
|
||||
// quelle di sotto restano sulla brace e si carbonizzano mentre quelle di sopra restano crude; troppo forte, la rete
|
||||
// le porta in cima e qualcuna salta fuori nel fuoco («mica si fa la centrifuga!»). Quando paiono cotte si rovesciano
|
||||
// nel sacco: contano quelle buone, né crude né bruciate. Per chi non può fare il gesto: Spazio, le frecce o il
|
||||
// pulsante Gira danno una spinta a ogni colpo. Unità: i pixel del primo piano 640×360, visto di fronte (sfondo di
|
||||
// Codex: assets/closeups/smarronata-bruciatore.png).
|
||||
|
||||
// Il cilindro (fra le due forcelle dello sfondo) e la manovella, a destra: un giro visto di sbieco, un'ellisse.
|
||||
export const DRUM = { x0: 182, x1: 456, y: 190, r: 46 }, CRANK = { x: 506, y: 190, rx: 14, ry: 30 }, BAR = 92;
|
||||
export const N = 18, BOTTOM = Math.PI / 2;
|
||||
// Tempi in secondi; velocità in radianti al secondo.
|
||||
export const MIN_POUR = 4, POUR = 1.4, SPILL = 0.45, PUSH = 2.6, FRICTION = 1.4, LINE = 2.4, MAX_SPEED = 22;
|
||||
// Quando una castagna è cotta e quando è carbonizzata.
|
||||
export const COOKED = 0.85, BURNT = 1.6;
|
||||
// I livelli: sotto `lo` il cilindro gira troppo piano (le castagne non si rimescolano abbastanza), sopra `hi` le
|
||||
// castagne volano fuori; `time`, in quanti secondi cuociono girando bene; `goal`, quante buone ci vogliono.
|
||||
export const LEVELS = {
|
||||
bona: { lo: 1.6, hi: 10, time: 20, goal: 10 },
|
||||
seria: { lo: 2.2, hi: 8, time: 17, goal: 15 },
|
||||
male: { lo: 2.8, hi: 6.5, time: 14, goal: 16 },
|
||||
};
|
||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
const wrap = a => Math.atan2(Math.sin(a), Math.cos(a));
|
||||
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
|
||||
const between = (c, lo, hi) => lo + (hi - lo) * random(c);
|
||||
|
||||
// Dove ricasca una castagna: di nuovo in fondo, a un'altezza a caso nel mucchio, con la sua voglia di farsi alzare.
|
||||
function drop(c, k, side = 1) { Object.assign(k, { a: BOTTOM - side * between(c, 0, 0.55), r: between(c, 0.5, 0.93), lift: between(c, 0.55, 1.5) }); }
|
||||
|
||||
export function newBruciate(seed = Date.now() >>> 0, def = {}) {
|
||||
const level = LEVELS[def.level] ? def.level : 'seria';
|
||||
const c = { step: 'turn', t: 0, seed, level, skill: LEVELS[level], current: 'player', power: 0, drum: 0, speed: 0,
|
||||
pending: 0, grab: null, cursor: null, spin: 0, chestnuts: [], flying: [], line: null, lineT: 0, events: [], visual: { time: 0 } };
|
||||
for (let i = 0; i < N; i++) { const k = { x: between(c, 0.06, 0.94), cook: between(c, 0, 0.08), out: false }; drop(c, k, random(c) < 0.5 ? 1 : -1); c.chestnuts.push(k); }
|
||||
return c;
|
||||
}
|
||||
export const inside = c => c.chestnuts.filter(k => !k.out);
|
||||
// Quanto stanno a contatto con la rete sopra la brace: le castagne in fondo e di fuori; quelle di sopra quasi niente.
|
||||
export function contact(k) { return Math.max(0, Math.cos(k.a - BOTTOM)) ** 3 * clamp((k.r - 0.62) / 0.3, 0, 1); }
|
||||
// La cottura media delle castagne ancora nel cilindro, da 0 (crude) in su (1: cotte).
|
||||
export const doneness = c => { const ks = inside(c); return ks.length ? ks.reduce((s, k) => s + k.cook, 0) / ks.length : 0; };
|
||||
export const tally = c => {
|
||||
const t = { good: 0, raw: 0, burnt: 0, spilled: 0 };
|
||||
for (const k of c.chestnuts) t[k.out ? 'spilled' : k.cook < COOKED ? 'raw' : k.cook > BURNT ? 'burnt' : 'good']++;
|
||||
return t;
|
||||
};
|
||||
function say(c, line) { c.line = line; c.lineT = LINE; }
|
||||
|
||||
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.lineT > 0 && (c.lineT -= dt) <= 0) c.line = null;
|
||||
for (const f of c.flying) f.t += dt;
|
||||
c.flying = c.flying.filter(f => f.t < 1.2);
|
||||
if (c.step === 'pour') { if (c.t >= POUR) finish(c); return; }
|
||||
if (c.step !== 'turn') return;
|
||||
// La velocità: tenendo la manovella è quella del gesto (smussata); lasciata, il cilindro rallenta da sé.
|
||||
if (c.grab) c.speed += (c.pending / dt - c.speed) * (1 - Math.exp(-dt * 10));
|
||||
else c.speed *= Math.exp(-FRICTION * dt);
|
||||
c.pending = 0;
|
||||
c.speed = clamp(c.speed, -MAX_SPEED, MAX_SPEED);
|
||||
const w = c.speed, fast = Math.abs(w) > c.skill.hi, side = Math.sign(w) || 1, before = c.drum;
|
||||
c.drum += w * dt;
|
||||
// Il cigolio della manovella, a ogni mezzo giro.
|
||||
if (Math.floor(before / Math.PI) !== Math.floor(c.drum / Math.PI)) c.events.push({ kind: 'bruciate-manovella', power: clamp(Math.abs(w) / 8, 0.3, 1) });
|
||||
const rate = 1 / (c.skill.time * 0.45);
|
||||
for (const k of inside(c)) {
|
||||
// La rete se le porta dietro finché non ricascano in fondo; troppo forte le porta fino in cima e oltre.
|
||||
k.a += w * dt * k.r;
|
||||
if (!fast && (k.a - BOTTOM) * side > k.lift) drop(c, k, side);
|
||||
const before = k.cook;
|
||||
k.cook += dt * rate * (0.15 + 1.9 * contact(k));
|
||||
if (before < COOKED && k.cook >= COOKED) c.events.push({ kind: 'bruciate-scoppio', power: 0.5 + 0.5 * random(c) });
|
||||
}
|
||||
// La centrifuga: ogni tanto ne vola fuori una, nel fuoco.
|
||||
if (fast) {
|
||||
c.spin += dt;
|
||||
if (c.spin >= SPILL) {
|
||||
c.spin = 0;
|
||||
const k = inside(c).sort((p, q) => Math.cos(p.a - BOTTOM) - Math.cos(q.a - BOTTOM))[0];
|
||||
if (k) { k.out = true; c.flying.push({ x: DRUM.x0 + k.x * (DRUM.x1 - DRUM.x0), y: DRUM.y + Math.sin(k.a) * DRUM.r * k.r, dir: side, t: 0 }); c.events.push({ kind: 'bruciate-cade', power: 0.8 }); }
|
||||
say(c, random(c) < 0.5 ? 'Oh! Mica si fa la centrifuga!' : 'Piano, che le si butta nel fuoco!');
|
||||
}
|
||||
} else c.spin = Math.max(0, c.spin - dt);
|
||||
if (!c.line && Math.abs(w) < c.skill.lo && c.t > 3 && inside(c).some(k => k.cook > BURNT - 0.3)) say(c, 'Gira, che sotto si bruciano!');
|
||||
// Tutte nel fuoco o tutte carbone: si rovescia comunque.
|
||||
if (!inside(c).length || inside(c).every(k => k.cook > BURNT)) pour(c);
|
||||
}
|
||||
function finish(c) {
|
||||
const t = tally(c);
|
||||
c.step = 'result'; c.speed = 0;
|
||||
c.result = { winner: t.good >= c.skill.goal ? 'player' : 'cpu', ...t, goal: c.skill.goal };
|
||||
}
|
||||
export function pour(c) {
|
||||
if (c.step !== 'turn') return false;
|
||||
Object.assign(c, { step: 'pour', t: 0, grab: null });
|
||||
c.events.push({ kind: 'bruciate-sacco', power: 0.8 });
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Comandi. Il gesto: la direzione del movimento, a ogni tratto, quanto ha girato rispetto al tratto prima.
|
||||
const STROKE = 3;
|
||||
export function aimScreen(c, [x, y]) {
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||
const g = c.grab;
|
||||
if (g && c.step === 'turn') {
|
||||
const [px, py] = g.last ?? [x, y], dx = x - px, dy = y - py;
|
||||
if (Math.hypot(dx, dy) >= STROKE) {
|
||||
const heading = Math.atan2(dy, dx);
|
||||
if (g.heading !== null) c.pending += clamp(wrap(heading - g.heading), -1.2, 1.2);
|
||||
g.heading = heading; g.last = [x, y];
|
||||
} else if (!g.last) g.last = [x, y];
|
||||
}
|
||||
c.cursor = [x, y];
|
||||
return true;
|
||||
}
|
||||
export function press(c, pointerId) {
|
||||
if (c.step !== 'turn' || c.grab) return false;
|
||||
c.grab = { pointerId, heading: null, last: c.cursor };
|
||||
return true;
|
||||
}
|
||||
export function release(c, pointerId) {
|
||||
if (!c.grab || c.grab.pointerId !== pointerId) return false;
|
||||
c.grab = null; return true;
|
||||
}
|
||||
export const cancel = (c, pointerId) => (pointerId === undefined || c.grab?.pointerId === pointerId) && release(c, c.grab?.pointerId);
|
||||
// Una spinta alla manovella (Spazio, frecce): per chi non fa il gesto.
|
||||
export function push(c) {
|
||||
if (c.step !== 'turn' || c.grab) return false;
|
||||
c.speed = (Math.sign(c.speed) || 1) * (Math.abs(c.speed) + PUSH);
|
||||
return true;
|
||||
}
|
||||
export function key(c, k, down) {
|
||||
if (k === 'Enter') { if (down) shoot(c); return true; }
|
||||
if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(k)) return false;
|
||||
if (down) push(c);
|
||||
return true;
|
||||
}
|
||||
// Il pulsante: rovescia nel sacco (dopo i primi secondi).
|
||||
export const canThrow = c => c.step === 'turn' && c.t >= MIN_POUR;
|
||||
export const canAdjust = () => false;
|
||||
export const aimFree = () => true;
|
||||
export const shoot = c => canThrow(c) && pour(c);
|
||||
export const button = c => c.step === 'result' ? 'Finita' : c.step === 'pour' ? 'Nel sacco…' : 'Rovescia nel sacco';
|
||||
export function speedWord(c) {
|
||||
const w = Math.abs(c.speed);
|
||||
return w < 0.3 ? 'ferma' : w < c.skill.lo ? 'troppo piano' : w > c.skill.hi ? 'troppo forte' : 'giusta';
|
||||
}
|
||||
export function status(c) {
|
||||
if (c.step === 'result') {
|
||||
const r = c.result, extra = [r.raw && `${r.raw} crude`, r.burnt && `${r.burnt} carbonizzate`, r.spilled && `${r.spilled} nel fuoco`].filter(Boolean).join(', ');
|
||||
return `${r.good} bruciate buone su ${N}${extra ? ` (${extra})` : ''}. ${r.winner === 'player' ? 'I volontari approvano!' : `Ne servivano ${r.goal}.`}`;
|
||||
}
|
||||
if (c.step === 'pour') return 'Nel sacco di iuta, a sudare...';
|
||||
const cooked = `Cottura ${Math.round(doneness(c) * 100)}%.`;
|
||||
if (c.line) return `${c.line} ${cooked}`;
|
||||
const hint = c.t < 3 && Math.abs(c.speed) < 0.3 ? 'Tieni premuto e fai dei giri per girare la manovella.' : `Manovella ${speedWord(c)}.`;
|
||||
return `${hint} ${cooked} Quando son cotte: Rovescia nel sacco.`;
|
||||
}
|
||||
Reference in New Issue
Block a user