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:
2026-10-31 15:03:55 +01:00
co-authored by Claude Opus 5.5
parent 6b833341c5
commit 04f9a5cb12
30 changed files with 661 additions and 33 deletions
+152
View File
@@ -0,0 +1,152 @@
// Le bruciate in primo piano (bruciate.js): il cilindro a rete sopra il fuoco, visto di fronte, con le castagne dentro
// che ricascano, la manovella a destra, le fiamme che lo lambiscono; in basso il giro della manovella e la cottura.
// Niente filtri e niente passate trasparenti a tutto schermo (vedi risveglio.js): solo forme piccole.
import { DRUM, CRANK, BAR, N, COOKED, BURNT, POUR, MAX_SPEED, inside, doneness } from './bruciate.js';
import { text, panel } from './bocce-render.js';
import { ambientHash as hash } from './ambient.js';
const SACK = { x: 604, y: 268 }, FIRE = 252, RODS = 18, RINGS = 9;
const lerp = (a, b, k) => a + (b - a) * k;
const mix = (p, q, k) => `rgb(${p.map((v, i) => Math.round(lerp(v, q[i], k))).join(',')})`;
// Il colore della buccia: lucida e rossiccia da cruda, scura da cotta, nera da carbone.
const RAW = [168, 96, 50], DONE = [118, 64, 30], COAL = [26, 20, 18];
const shell = cook => cook < COOKED ? mix(RAW, DONE, Math.max(0, cook) / COOKED) : mix(DONE, COAL, Math.min(1, (cook - COOKED) / (BURNT - COOKED)));
// Dove sta una castagna sullo schermo, e quanto è davanti (per l'ordine e la luce).
function place(k) {
return { x: lerp(DRUM.x0 + 8, DRUM.x1 - 8, k.x), y: DRUM.y + Math.sin(k.a) * DRUM.r * k.r, z: Math.cos(k.a) * k.r };
}
function chestnut(ctx, x, y, cook, z = 0) {
// Il bordino, perché si stacchino dal fondo del cilindro (chiaro su quelle carbone).
ctx.fillStyle = cook > BURNT ? '#6a5a4e' : '#1e120a';
ctx.beginPath(); ctx.ellipse(Math.round(x), Math.round(y), 7, 6, 0, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = shell(cook);
ctx.beginPath(); ctx.ellipse(Math.round(x), Math.round(y), 6, 5, 0, 0, Math.PI * 2); ctx.fill();
// La punta chiara e, da cotte, l'incisione aperta col giallo della castagna.
ctx.fillStyle = cook > BURNT ? '#2a2420' : '#d9b48a';
ctx.fillRect(Math.round(x) - 1, Math.round(y) - 4, 2, 1);
if (cook >= COOKED * 0.8 && cook <= BURNT) { ctx.fillStyle = '#e8c070'; ctx.fillRect(Math.round(x) - 2, Math.round(y) - 1, 4, 1); }
// Il riflesso del fuoco, più forte su quelle davanti.
if (cook < BURNT) { ctx.fillStyle = `rgba(255,190,120,${0.25 + 0.3 * Math.max(0, z)})`; ctx.fillRect(Math.round(x) + 1, Math.round(y) - 2, 2, 1); }
}
// Le fiamme che lambiscono il cilindro: lingue basse e storte, ognuna col suo passo, che ogni tanto si allungano.
function flames(ctx, time, front) {
for (let i = 0; i < 17; i++) {
if ((i % 3 === 0) !== front) continue;
const r = hash(i * 7.3), x = DRUM.x0 + 6 + i * (DRUM.x1 - DRUM.x0 - 12) / 16 + 6 * (hash(i * 1.9) - 0.5);
const lick = Math.max(0, Math.sin(time * (1.3 + r) + i * 2.1)) ** 4, h = front ? 5 + 6 * r + 2 * Math.sin(time * (6 + r * 5) + i) + 7 * lick : 18 + 16 * r + 7 * Math.sin(time * (6 + r * 5) + i) + 22 * lick;
const w = 5 + 5 * hash(i * 3.1), sway = 2.5 * Math.sin(time * (3 + r) + i * 1.7);
for (const [k, color] of [[1, '#b8301a'], [0.7, '#ea7a1a'], [0.4, '#ffd04a']]) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(x - w * k, FIRE);
ctx.quadraticCurveTo(x - w * k * 0.2 + sway, FIRE - h * k * 0.6, x + sway * 2, FIRE - h * k);
ctx.quadraticCurveTo(x + w * k * 0.5 + sway, FIRE - h * k * 0.45, x + w * k, FIRE);
ctx.fill();
}
}
}
// Il cilindro appeso: la sbarra sulle due forcelle e i due ferri che lo reggono per l'asse.
function hanger(ctx) {
ctx.fillStyle = '#2a2420'; ctx.fillRect(150, BAR - 3, 336, 6);
ctx.fillStyle = '#6a5a4a'; ctx.fillRect(150, BAR - 3, 336, 1);
for (const x of [DRUM.x0 - 12, DRUM.x1 + 12]) {
ctx.fillStyle = '#2a2420'; ctx.fillRect(x - 2, BAR, 4, DRUM.y - BAR + 4);
ctx.fillStyle = '#5a4a3e'; ctx.fillRect(x - 2, BAR, 1, DRUM.y - BAR + 4);
ctx.fillStyle = '#2a2420'; ctx.beginPath(); ctx.arc(x, DRUM.y, 5, 0, Math.PI * 2); ctx.fill();
}
}
// La rete: gli anelli e i tondini che girano col cilindro (quelli davanti più chiari).
function mesh(ctx, c, front) {
const { x0, x1, y, r } = DRUM;
for (let j = 0; j < RODS; j++) {
const a = c.drum + j * Math.PI * 2 / RODS, z = Math.cos(a);
if (front !== z > 0) continue;
ctx.fillStyle = front ? '#9a9088' : '#3a302a';
ctx.fillRect(x0, Math.round(y + Math.sin(a) * r), x1 - x0, 1);
}
ctx.strokeStyle = front ? '#8a8078' : '#3a302a'; ctx.lineWidth = 1;
for (let i = 0; i <= RINGS; i++) {
const x = Math.round(lerp(x0, x1, i / RINGS)) + 0.5, end = i === 0 || i === RINGS;
ctx.beginPath(); ctx.ellipse(x, y, end ? 9 : 4, r, 0, front ? -Math.PI / 2 : Math.PI / 2, front ? Math.PI / 2 : Math.PI * 1.5); ctx.stroke();
}
}
function crank(ctx, c, time) {
const { x, y, rx, ry } = CRANK, a = c.drum;
// L'asse, da una forcella all'altra e fino alla manovella.
ctx.fillStyle = '#2a2420'; ctx.fillRect(DRUM.x0 - 16, y - 2, x - DRUM.x0 + 18, 4);
ctx.fillStyle = '#6a625a'; ctx.fillRect(DRUM.x0 - 16, y - 2, x - DRUM.x0 + 18, 1);
// Il giro da fare: un'ellisse tratteggiata che pulsa, finché il cilindro è fermo.
if (c.step === 'turn' && Math.abs(c.speed) < 0.5) {
ctx.strokeStyle = `rgba(255,224,140,${0.5 + 0.4 * Math.sin(time * 5)})`; ctx.setLineDash([3, 3]);
ctx.beginPath(); ctx.ellipse(x, y, rx + 6, ry + 6, 0, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]);
text(ctx, 'GIRA!', x, y + ry + 18, 6, '#ffe08c', 'center');
}
const hx = x + Math.cos(a) * rx, hy = y + Math.sin(a) * ry;
ctx.strokeStyle = '#5a5450'; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(hx, hy); ctx.stroke();
ctx.strokeStyle = '#b0a698'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, y - 1); ctx.lineTo(hx, hy - 1); ctx.stroke();
ctx.fillStyle = '#4a4440'; ctx.beginPath(); ctx.arc(x, y, 4, 0, Math.PI * 2); ctx.fill();
// L'impugnatura di legno, consumata dalle mani.
ctx.fillStyle = '#3a2414'; ctx.fillRect(Math.round(hx) - 4, Math.round(hy) - 8, 9, 16);
ctx.fillStyle = '#a0703c'; ctx.fillRect(Math.round(hx) - 3, Math.round(hy) - 7, 7, 14);
ctx.fillStyle = '#d6a062'; ctx.fillRect(Math.round(hx) - 2, Math.round(hy) - 6, 2, 12);
}
// In basso a sinistra (in alto ci sono i cursori del volume): il giro della manovella (rosso troppo piano, verde giusto, rosso troppo forte) e la cottura.
function gauges(ctx, c) {
panel(ctx, 10, 296, 196, 54);
const bar = (y, label, k, zones, mark) => {
text(ctx, label, 18, y, 6, '#efd49e');
const x0 = 82, w = 116;
for (const [a, b, color] of zones) { ctx.fillStyle = color; ctx.fillRect(x0 + Math.round(a * w), y - 3, Math.max(1, Math.round((b - a) * w)), 7); }
ctx.fillStyle = mark; ctx.fillRect(x0 + Math.round(Math.max(0, Math.min(1, k)) * w) - 1, y - 6, 3, 13);
};
const s = c.skill, lo = s.lo / MAX_SPEED, hi = s.hi / MAX_SPEED;
bar(312, 'GIRO', Math.abs(c.speed) / MAX_SPEED, [[0, lo, '#8a3a2a'], [lo, hi, '#4f8a3a'], [hi, 1, '#b3402a']], '#fff4c8');
const top = BURNT * 1.15;
bar(334, 'COTTURA', doneness(c) / top, [[0, COOKED / top, '#b07a4a'], [COOKED / top, BURNT / top, '#6a8a3a'], [BURNT / top, 1, '#2a2420']], '#fff4c8');
}
function result(ctx, c) {
const r = c.result;
panel(ctx, 170, 120, 300, 104);
text(ctx, r.winner === 'player' ? 'BRUCIATE COI FIOCCHI!' : 'BRUCIATE COSÌ COSÌ', 320, 140, 8, '#efd49e', 'center');
const rows = [['Buone', r.good, '#9fd27a'], ['Crude', r.raw, '#e0b07a'], ['Carbonizzate', r.burnt, '#c0b0a0'], ['Nel fuoco', r.spilled, '#f08a5a']];
rows.forEach(([label, n, color], i) => { text(ctx, label, 200, 162 + i * 13, 6, color); text(ctx, `${n}`, 440, 162 + i * 13, 6, color, 'right'); });
text(ctx, `Ne servivano ${r.goal} su ${N}`, 320, 216, 5, '#d7ccb4', 'center');
}
export function drawBruciate(ctx, c, def, bg) {
const time = c.visual.time;
ctx.save();
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
else { ctx.fillStyle = '#6a5a40'; ctx.fillRect(0, 0, 640, 360); }
hanger(ctx);
// Il fondo del cilindro, scuro, col bagliore della brace sotto; poi le fiamme dietro e la rete di dietro.
ctx.fillStyle = 'rgba(74,46,30,0.72)'; ctx.fillRect(DRUM.x0, DRUM.y - DRUM.r, DRUM.x1 - DRUM.x0, DRUM.r * 2);
ctx.fillStyle = 'rgba(240,120,40,0.18)'; ctx.fillRect(DRUM.x0, DRUM.y + DRUM.r * 0.3, DRUM.x1 - DRUM.x0, DRUM.r * 0.7);
flames(ctx, time, false);
mesh(ctx, c, false);
// Le castagne: da dietro a davanti. Rovesciandole, scivolano tutte verso il sacco.
const pour = c.step === 'pour' ? Math.min(1, c.t / POUR) : c.step === 'result' ? 1 : 0;
const ks = (pour ? c.chestnuts.filter(k => !k.out) : inside(c)).map(k => ({ k, ...place(k) })).sort((p, q) => p.z - q.z);
for (const p of ks) {
if (pour) {
const d = Math.min(1, Math.max(0, pour * 1.6 - p.k.x * 0.6)), x = lerp(p.x, SACK.x, d), y = lerp(p.y, SACK.y, d) - Math.sin(d * Math.PI) * 40;
if (d < 1) chestnut(ctx, x, y, p.k.cook, 1);
} else chestnut(ctx, p.x, p.y, p.k.cook, p.z);
}
mesh(ctx, c, true);
flames(ctx, time, true);
crank(ctx, c, time);
// Quelle che volano nel fuoco, e lo sbuffo quando ci cascano.
for (const f of c.flying) {
const k = Math.min(1, f.t / 0.6), x = f.x + f.dir * 60 * k, y = lerp(f.y, FIRE - 6, k) - Math.sin(k * Math.PI) * 50;
if (f.t < 0.6) chestnut(ctx, x, y, 0.5);
else for (let i = 0; i < 4; i++) {
const s = (f.t - 0.6) / 0.6;
ctx.fillStyle = `rgba(200,195,185,${0.6 * (1 - s)})`;
ctx.beginPath(); ctx.arc(x + (i - 1.5) * 5 * (1 + s), FIRE - 10 - s * 30 - i * 3, 4 + s * 6, 0, Math.PI * 2); ctx.fill();
}
}
if (c.step === 'result') result(ctx, c); else gauges(ctx, c);
ctx.restore();
}
+162
View File
@@ -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.`;
}
+2
View File
@@ -18,6 +18,7 @@ import { newRisveglio, updateRisveglio } from './risveglio.js';
import { newTelefono, updateTelefono, tapTelefono } from './telefono.js';
import * as condor from './condor.js';
import * as campana from './campana.js';
import * as bruciate from './bruciate.js';
import * as poker from './poker.js';
// I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta.
@@ -34,6 +35,7 @@ export const MINIGAMES = {
cerbottana: { create: (seed, level) => cerbottana.newCerbottana(seed, { level }), update: cerbottana.update, press: cerbottana.press, release: cerbottana.release, cancel: cerbottana.cancel, aim: cerbottana.aimScreen },
condor: { create: (seed, level) => condor.newCondor(seed, { level }), update: condor.update, press: condor.press, release: condor.release, cancel: condor.cancel, aim: condor.aimScreen },
campana: { create: (seed, level) => campana.newCampana(seed, { level }), update: campana.update, press: campana.press, release: campana.release, cancel: campana.cancel, aim: campana.aimScreen },
bruciate: { create: (seed, level) => bruciate.newBruciate(seed, { level }), update: bruciate.update, press: bruciate.press, release: bruciate.release, cancel: bruciate.cancel, aim: bruciate.aimScreen },
micro: { create: (seed, level) => micro.newMicro(seed, { level }), update: micro.update, press: micro.press, release: micro.release, cancel: micro.cancel, aim: micro.aimScreen },
gara: { create: (seed, level, deck, def) => gara.newGara(seed, { level, riders: def.riders }), update: gara.update, press: gara.press, release: gara.release, cancel: gara.cancel, aim: gara.aimScreen },
};
+6
View File
@@ -14,6 +14,7 @@ import { drawCerbottana } from './cerbottana-render.js';
import { drawCorsa } from './corsa.js';
import { drawCondor } from './condor-render.js';
import { drawCampana } from './campana-render.js';
import { drawBruciate } from './bruciate-render.js';
import { drawPoker } from './poker-render.js';
import { drawViaggio } from './viaggio.js';
import { drawIncanto } from './incanto.js';
@@ -1020,6 +1021,11 @@ function drawCloseup(ctx, game, time) {
drawCampana(ctx, c, def, backgrounds.get(def.picture));
return;
}
if (def.type === 'bruciate') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawBruciate(ctx, c, def, backgrounds.get(def.picture));
return;
}
if (def.type === 'micro') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawMicro(ctx, c, def, backgrounds.get(def.picture), src => { if (!backgrounds.has(src)) loadBackground(src); return backgrounds.get(src); });
+5
View File
@@ -156,6 +156,11 @@ export class Soundscape {
else if (kind === 'campana-sasso') { this.tone(1900, 1100, 0.04, 0.05 * strength + 0.02, 0, pan); this.tone(1500, 900, 0.03, 0.03, 0.06, pan); this.noiseSound(0.04, 3000, 0.03, { pan }); }
else if (kind === 'campana-salto') { this.tone(110, 60, 0.07, 0.07, 0, pan); this.noiseSound(0.05, 500, 0.04, { pan }); }
else if (kind === 'campana-riga') { this.tone(300, 180, 0.12, 0.05, 0, pan); this.tone(220, 120, 0.16, 0.05, 0.1, pan); }
// Le bruciate: il cigolio della manovella, la castagna che scoppietta cuocendo, quella che casca nel fuoco, il sacco.
else if (kind === 'bruciate-manovella') { this.tone(420, 380, 0.08, 0.025 * strength, 0, pan); this.noiseSound(0.06, 900, 0.02, { pan }); }
else if (kind === 'bruciate-scoppio') { this.noiseSound(0.04, 2600, 0.05 * strength, { pan }); this.tone(900, 500, 0.03, 0.03, 0, pan); }
else if (kind === 'bruciate-cade') { this.noiseSound(0.5, 3200, 0.09, { pan }); this.tone(200, 90, 0.12, 0.05, 0, pan); }
else if (kind === 'bruciate-sacco') this.noiseSound(0.7, 1100, 0.1, { paper: true, pan });
else if (kind === 'cerbottana-tosse') { this.noiseSound(0.18, 900, 0.08, { pan }); this.tone(260, 140, 0.12, 0.06, 0.25, pan); this.tone(240, 120, 0.1, 0.05, 0.3, pan); }
// Le macchinine: il dito che spinge, le ruote sull'asfalto, l'urto di plastica, fuori pista, il traguardo.
else if (kind === 'micro-spinta') { this.tone(1200, 500, 0.04, 0.04 + strength * 0.04, 0, pan); this.noiseSound(0.04, 2600, 0.04, { pan }); }
+6 -3
View File
@@ -15,6 +15,7 @@ import * as micro from './engine/micro.js';
import * as cerbottana from './engine/cerbottana.js';
import * as condor from './engine/condor.js';
import * as campana from './engine/campana.js';
import * as bruciate from './engine/bruciate.js';
import * as poker from './engine/poker.js';
import { cardAt as pokerAt } from './engine/poker-render.js';
import * as basket from './engine/basket.js';
@@ -617,6 +618,7 @@ const MINI = {
cerbottana: { ...cerbottana, back: 'Torna in piazza', label: 'La cerbottana: muovi il mouse o tocca il muro per mirare, tieni premuto per prendere fiato e lascia per sparare; oppure regola il fiato e premi Spara. Frecce per spostare il mirino.' },
condor: { ...condor, back: 'Torna al campone', label: 'La Condor: muovi il mouse o tocca il campo per mirare, tieni premuto per trattenere il fiato e lascia per sparare; oppure premi Spara. Frecce per spostare il mirino.' },
campana: { ...campana, back: 'Torna al campone', label: 'La campana: muovi il mouse o tocca la campana per mirare, tieni premuto per la forza e lascia per tirare il sassolino; oppure regola la forza e premi Tira (frecce su e giù per mirare, destra e sinistra per la forza). Ai salti premi, o Spazio, quando la lancetta è nel verde.' },
bruciate: { ...bruciate, back: 'Torna alla festa', label: 'Le bruciate: tieni premuto e fai dei giri, col mouse o col dito, per girare la manovella; oppure Spazio o le frecce, una spinta a ogni colpo. Né troppo piano né troppo forte; quando son cotte, Rovescia nel sacco (o Invio).' },
micro: { ...micro, back: 'Torna dal Chiaru', label: 'La pista col gesso: muovi il mouse o tocca il parcheggio per mirare, tieni premuto per caricare la spinta e lascia; oppure regola la forza e premi Spingi. Frecce o pulsanti Mira per girare.' },
basket: { ...basket, back: 'Torna in strada', label: 'Fai due tiri: muovi il mouse o tocca per mirare, tieni premuto per caricare la forza, lascia per tirare.' },
// La gara: niente mira né forza, il pulsante alterna gas e freno (chi non può tenere premuto).
@@ -1046,7 +1048,7 @@ function updateUi() {
}
}
if (mg && !cards) {
setAttr($('bocce-controls'), 'aria-label', { rinquarto: 'Comandi dei rinquarti', biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca', gara: 'Comandi della gara', micro: 'Comandi delle macchinine', cerbottana: 'Comandi della cerbottana', condor: 'Comandi della Condor', campana: 'Comandi della campana' }[def.type] ?? 'Comandi delle bocce');
setAttr($('bocce-controls'), 'aria-label', { rinquarto: 'Comandi dei rinquarti', biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca', gara: 'Comandi della gara', micro: 'Comandi delle macchinine', cerbottana: 'Comandi della cerbottana', condor: 'Comandi della Condor', campana: 'Comandi della campana', bruciate: 'Comandi delle bruciate' }[def.type] ?? 'Comandi delle bocce');
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
const line = mg.status(c, def.names);
if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line;
@@ -1060,8 +1062,8 @@ function updateUi() {
// Ai rinquarti la forza è sempre piena: il cursore non serve. Nella guida semplice del GP (alla bona e seria) si
// usa solo il gas: niente sensibilità, sterzo, freno né riquadro dello swipe.
const easy = def.type === 'gara' && gara.simple(c);
setProp(powerLabel, 'hidden', def.type === 'rinquarto' || def.type === 'condor' || easy);
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy || def.type === 'campana');
setProp(powerLabel, 'hidden', def.type === 'rinquarto' || def.type === 'condor' || def.type === 'bruciate' || easy);
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy || def.type === 'campana' || def.type === 'bruciate');
const powerName = def.type === 'gara' ? 'Sensibilità ' : def.type === 'cerbottana' ? 'Fiato ' : 'Forza ';
if (powerLabel.firstChild.nodeValue !== powerName) powerLabel.firstChild.nodeValue = powerName;
setAttr($('bocce-power'), 'aria-label', def.type === 'gara' ? 'Sensibilità dello sterzo col dito' : 'Forza del lancio');
@@ -1083,6 +1085,7 @@ function updateUi() {
setProp($('cerbottana-help'), 'hidden', def.type !== 'cerbottana');
setProp($('condor-help'), 'hidden', def.type !== 'condor');
setProp($('campana-help'), 'hidden', def.type !== 'campana');
setProp($('bruciate-help'), 'hidden', def.type !== 'bruciate');
setProp($('basket-help'), 'hidden', def.type !== 'basket');
setProp($('pesca-help'), 'hidden', def.type !== 'pesca');
setProp($('gara-help'), 'hidden', def.type !== 'gara');
+38 -6
View File
@@ -360,6 +360,17 @@ export const story = {
draw: [],
quit: [{ say: 'Il gesso lo lascio lì. Ci giocherà qualche bambino, che è più in allenamento di me.' }],
},
// Le bruciate della Smarronata, 1993: la manovella del cilindro sul fuoco, coi volontari della Misericordia
// (Andrea, 31 ottobre: «a manovella, a mano»). [DA ANDREA: le battute sono provvisorie]
bruciate: {
type: 'bruciate',
picture: 'assets/closeups/smarronata-bruciatore.png',
names: { player: 'Andrea', cpu: 'i volontari' },
win: [{ say: 'Bruciate coi fiocchi! Mezza Smarronata le mangia grazie a me.' }, { say: 'Bravo, figliolo! Se ti va, l\'anno prossimo la manovella è tua.', who: 'volontari', pose: 'give' }, { set: 'bruciate_fatte' }],
lose: [{ say: 'Un po\' crude, un po\' carbone... Le mangio io, va\'.' }, { say: 'Ci vuole il polso, figliolo. Il polso e la pazienza.', who: 'volontari', pose: 'explain' }],
draw: [],
quit: [{ say: 'Vi rendo la manovella. Mi fumano anche gli occhi.' }],
},
// Il manifesto dell'Ingorgo, ingrandito (Andrea, 30 ottobre: «se mi avvicino compaia ingrandito, e poi parta
// l'Ingorgo se ci clicco»). [DA ANDREA: il manifesto vero; questo è di Codex, provvisorio]
manifesto_ingorgo: {
@@ -3734,7 +3745,8 @@ export const story = {
id: 'bruciatore', name: 'il cilindro delle bruciate', box: [192, 188, 68, 54], at: [226, 262], face: 'up',
actions: {
look: [{ say: 'Il cilindro delle bruciate: si gira la manovella sopra il fuoco, e le castagne ci ballano dentro.' }],
use: [{ say: 'Si gira piano, sennò le bruciate si bruciano davvero.' }],
// Il minigioco delle bruciate: la manovella si gira coi volontari (Andrea, 31 ottobre).
use: [{ say: 'Posso girare io la manovella?' }, { dialogue: 'volontari' }],
take: [{ say: 'Una bruciata calda, presa al volo. Scotta! Ma è buonissima.' }],
},
},
@@ -3763,11 +3775,7 @@ export const story = {
id: 'volontari', name: 'i volontari della Misericordia', box: [130, 148, 97, 114], at: [200, 272], face: 'left', default: 'talk',
actions: {
look: [{ say: 'I volontari della Misericordia, alle bruciate: uno rimescola, l\'altro riempie i cartocci.' }],
talk: [
{ say: 'Che Iddio ve ne renda merito!! A voi e agli alpini!' },
{ say: 'E a te, figliolo. To\', un cartoccio: son belle calde.', who: 'volontari', pose: 'give' },
{ say: 'Scottano! Ma son buonissime.' },
],
talk: [{ dialogue: 'volontari' }],
},
},
{
@@ -4237,6 +4245,30 @@ export const story = {
},
// Il BaronDeDuke nella discesa verso la polisportiva: le mutande perse a poker, e il Muro del Sanpierino sul
// telefonino (Andrea, 31 ottobre: lui è il webmaster di sanpiero50037.it).
// I volontari della Misericordia alle bruciate della Smarronata, 1993: il ringraziamento di Andrea e la manovella
// (il minigioco delle bruciate, Andrea, 31 ottobre). [DA ANDREA: le battute sono provvisorie]
volontari: {
nodes: {
start: { options: [
{ text: 'Che Iddio ve ne renda merito!! A voi e agli alpini!', then: [
{ say: 'E a te, figliolo. To\', un cartoccio: son belle calde.', who: 'volontari', pose: 'give' },
{ say: 'Scottano! Ma son buonissime.' },
] },
{ text: 'Vi do una mano alla manovella?', next: 'livello', then: [
{ say: 'Su, giovanotto, che noi s\'ha le braccia stanche!', who: 'volontari', pose: 'talk' },
{ say: 'Si gira sempre uguale: piano piano sotto si bruciano, a furia volan nel fuoco. Quando son cotte, nel sacco.', who: 'volontari', pose: 'explain' },
{ say: 'Come te la senti?', who: 'volontari', pose: 'talk' },
] },
{ text: 'Buon lavoro!', next: 'end' },
] },
livello: { options: [
{ text: 'Alla bona.', next: 'end', then: [{ say: 'Alla bona: ti si guarda noi, e se si bruciano un si dice a nessuno.', who: 'volontari', pose: 'talk' }, { closeup: 'bruciate', level: 'bona' }] },
{ text: 'Seria.', next: 'end', then: [{ say: 'Seria: la gente è in fila, e il cartoccio lo vole bello pieno.', who: 'volontari', pose: 'explain' }, { closeup: 'bruciate', level: 'seria' }] },
{ text: 'A fassi male.', next: 'end', then: [{ say: 'A fassi male? Allora ci si mette anche il fuoco forte.', who: 'volontari', pose: 'talk' }, { closeup: 'bruciate', level: 'male' }] },
{ text: 'No, dopo.', next: 'start' },
] },
},
},
barondeduke: {
nodes: {
start: { options: [