diff --git a/app.js b/app.js index b42a7be..c2230c8 100644 --- a/app.js +++ b/app.js @@ -122,40 +122,58 @@ }, }; - /* ---------- Coriandoli ---------- */ + /* ---------- Coriandoli (canvas a schermo intero, dimensionato a mano) ---------- */ + // IMPORTANTE: niente useWorker + niente resize automatico. Dimensiono io il + // canvas alla viewport, altrimenti il buffer resta 300x150 e i coriandoli + // finiscono tutti ammassati nell'angolo in alto a sinistra. + const confettiCanvas = $("#confetti"); + function sizeConfetti() { + confettiCanvas.width = window.innerWidth; + confettiCanvas.height = window.innerHeight; + } + sizeConfetti(); + window.addEventListener("resize", sizeConfetti); const myConfetti = typeof confetti !== "undefined" - ? confetti.create($("#confetti"), { resize: true, useWorker: true }) + ? confetti.create(confettiCanvas, { resize: false, useWorker: false }) : () => {}; - const CONFETTI_COLORS = ["#ffd23f", "#ff9e00", "#ff2e88", "#00e5ff", "#29e08a", "#ffffff"]; + const CONFETTI_COLORS = ["#ffd23f", "#ff9e00", "#ff2e88", "#00e5ff", "#29e08a", "#ff3b5c", "#ffffff"]; + // VALANGA di coriandoli su TUTTO lo schermo function bigBurst() { - // esplosione centrale abbondante - myConfetti({ particleCount: 220, spread: 110, startVelocity: 48, origin: { y: 0.55 }, colors: CONFETTI_COLORS }); - myConfetti({ particleCount: 140, spread: 130, startVelocity: 40, decay: 0.92, scalar: 1.1, origin: { y: 0.6 }, colors: CONFETTI_COLORS }); - // due cannoni laterali - myConfetti({ particleCount: 80, angle: 60, spread: 75, origin: { x: 0, y: 0.7 }, colors: CONFETTI_COLORS }); - myConfetti({ particleCount: 80, angle: 120, spread: 75, origin: { x: 1, y: 0.7 }, colors: CONFETTI_COLORS }); + const C = CONFETTI_COLORS; + // esplosione centrale gigante a 360° + myConfetti({ particleCount: 400, spread: 360, startVelocity: 45, scalar: 1.1, origin: { x: 0.5, y: 0.5 }, colors: C }); + // fila di esplosioni lungo TUTTA la larghezza + for (let x = 0; x <= 1.0001; x += 0.1) { + myConfetti({ particleCount: 60, spread: 110, startVelocity: 55, origin: { x, y: 0.55 }, colors: C }); + } + // cannoni laterali potenti dal basso + myConfetti({ particleCount: 250, angle: 60, spread: 120, startVelocity: 75, origin: { x: 0, y: 0.85 }, colors: C }); + myConfetti({ particleCount: 250, angle: 120, spread: 120, startVelocity: 75, origin: { x: 1, y: 0.85 }, colors: C }); + // pioggia dall'alto su tutta la larghezza + for (let x = 0; x <= 1.0001; x += 0.1) { + myConfetti({ particleCount: 40, spread: 160, startVelocity: 35, gravity: 1, origin: { x, y: -0.1 }, colors: C }); + } + } + + // pioggia continua a schermo pieno per N ms + function confettiRain(durationMs) { + const end = Date.now() + durationMs; + (function frame() { + for (let x = 0; x <= 1.0001; x += 0.12) { + myConfetti({ particleCount: 10, spread: 130, startVelocity: 45, gravity: 1, origin: { x, y: -0.05 }, colors: CONFETTI_COLORS }); + } + if (Date.now() < end) requestAnimationFrame(frame); + })(); } function celebrate(kind) { bigBurst(); - if (kind === "jackpot") { - // pioggia prolungata extra per il jackpot - const end = Date.now() + 2600; - (function frame() { - myConfetti({ particleCount: 12, angle: 60, spread: 80, origin: { x: 0 }, colors: CONFETTI_COLORS }); - myConfetti({ particleCount: 12, angle: 120, spread: 80, origin: { x: 1 }, colors: CONFETTI_COLORS }); - myConfetti({ particleCount: 10, spread: 140, startVelocity: 60, origin: { y: 0.25 }, colors: CONFETTI_COLORS }); - if (Date.now() < end) requestAnimationFrame(frame); - })(); - } else { - // anche un reveal normale ha una seconda ondata - setTimeout(() => { - myConfetti({ particleCount: 120, spread: 100, startVelocity: 40, origin: { y: 0.5 }, colors: CONFETTI_COLORS }); - }, 220); - } + // seconda ondata immediata per raddoppiare la densità + setTimeout(bigBurst, 180); + confettiRain(kind === "jackpot" ? 3500 : 1400); } function screenShake() { @@ -307,19 +325,22 @@ return el ? el.textContent.replace(/[🏠🛁]/g, "").trim() : ""; } - // GIRO con RAGEBAIT: decelera e "si ferma" su una stanza sbagliata (adiacente), - // tiene la suspense, poi scatta di scatto sulla stanza vera. + // Probabilità di ragebait (finto stop sulla stanza sbagliata) per giro. + const TEASE_CHANCE = 0.3; + + // GIRO. Nel ~30% dei casi: decelera e "si ferma" su una stanza sbagliata + // (adiacente), tiene la suspense, poi scatta sulla stanza vera. Altrimenti + // atterra direttamente con un normale assestamento. async function spinReel(targetId) { const targetIndex = buildStrip(targetId); const finalY = -(targetIndex - 1) * ITEM_H; // target centrata const targetName = roomById[targetId].nome; - // scegli la direzione del bluff: la "finta" stanza deve essere diversa dalla vera + // direzione del bluff: la "finta" stanza deve essere diversa dalla vera let dir = Math.random() < 0.5 ? -1 : 1; if (reelNameAt(targetIndex + dir) === targetName) dir = -dir; - if (reelNameAt(targetIndex + dir) === targetName) dir = 0; // fallback: niente bluff - const fakeIndex = targetIndex + dir; - const fakeY = -(fakeIndex - 1) * ITEM_H; + if (reelNameAt(targetIndex + dir) === targetName) dir = 0; // niente adiacente valida + const wantTease = dir !== 0 && Math.random() < TEASE_CHANCE; // --- tick sincronizzati --- let ticking = false; @@ -339,26 +360,27 @@ } const stopTicks = () => (ticking = false); - const stopRiser = Audio.riser(4.6); - - // FASE 1: decelera fino a "fermarsi" sulla stanza SBAGLIATA + const stopRiser = Audio.riser(wantTease ? 4.6 : 4.2); startTicks(); - const a1 = reelEl.animate( - [{ transform: "translateY(0)" }, { transform: `translateY(${fakeY}px)` }], - { duration: 3600, easing: "cubic-bezier(0.08, 0.85, 0.14, 1)", fill: "forwards" } - ); - await a1.finished; - stopTicks(); - if (dir !== 0) { + if (wantTease) { + const fakeY = -(targetIndex + dir - 1) * ITEM_H; + // FASE 1: decelera fino a "fermarsi" sulla stanza SBAGLIATA + const a1 = reelEl.animate( + [{ transform: "translateY(0)" }, { transform: `translateY(${fakeY}px)` }], + { duration: 3600, easing: "cubic-bezier(0.08, 0.85, 0.14, 1)", fill: "forwards" } + ); + await a1.finished; + stopTicks(); + // FASE 2: gaslighting — sembra tutto finito Audio.blip(320, 0.18, "sine", 0.16); const win = $(".reel-window"); win.classList.add("teasing"); - mc(`Ferma su ${reelNameAt(fakeIndex)}...? 🤨`); + mc(`Ferma su ${reelNameAt(targetIndex + dir)}...? 🤨`); await sleep(700); - // FASE 3: SCATTO alla stanza vera (rabbia garantita) + // FASE 3: SCATTO alla stanza vera Audio.blip(200, 0.14, "sawtooth", 0.22, 90); // whoosh startTicks(); const a2 = reelEl.animate( @@ -369,7 +391,13 @@ stopTicks(); win.classList.remove("teasing"); } else { - // nessun bluff possibile: piccolo assestamento + // atterraggio diretto + micro-assestamento + const a = reelEl.animate( + [{ transform: "translateY(0)" }, { transform: `translateY(${finalY}px)` }], + { duration: 4200, easing: "cubic-bezier(0.12, 0.85, 0.18, 1)", fill: "forwards" } + ); + await a.finished; + stopTicks(); reelEl.animate( [ { transform: `translateY(${finalY}px)` }, @@ -780,4 +808,7 @@ initMarquee(); bindControls(); newGame(false); + + // hook di debug (per test/verifica; innocuo) + window.__celebrate = celebrate; })(); diff --git a/styles.css b/styles.css index 0aa30a2..43e0221 100644 --- a/styles.css +++ b/styles.css @@ -36,6 +36,8 @@ body { #confetti { position: fixed; inset: 0; + width: 100vw; + height: 100vh; z-index: 60; pointer-events: none; }