From 9482f77eefb367a72d97cbdbd4b0065b3d209cbb Mon Sep 17 00:00:00 2001 From: Alberto Moretti Date: Thu, 2 Jul 2026 13:33:34 +0200 Subject: [PATCH] suoni --- app.js | 138 +++++++++++++++++++++++++++++++++++++++++------------ styles.css | 41 ++++++++++++++++ 2 files changed, 148 insertions(+), 31 deletions(-) diff --git a/app.js b/app.js index a5721c0..b42a7be 100644 --- a/app.js +++ b/app.js @@ -5,6 +5,7 @@ const $ = (sel) => document.querySelector(sel); const roomById = Object.fromEntries(ROOMS.map((r) => [r.id, r])); const genderOf = Object.fromEntries(PARTICIPANTS.map((p) => [p.nome, p.genere])); + const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); /* ---------- Audio sintetizzato (nessun file esterno) ---------- */ const Audio = { @@ -50,6 +51,41 @@ buzz() { this.blip(140, 0.45, "sawtooth", 0.3, 70); }, + // nota di ottone per la fanfara + brass(freq, when, dur, vol = 0.25) { + if (!this.on || !this.ctx) return; + const t = this.ctx.currentTime + when; + const o = this.ctx.createOscillator(); + const o2 = this.ctx.createOscillator(); + const g = this.ctx.createGain(); + const lp = this.ctx.createBiquadFilter(); + o.type = "sawtooth"; + o2.type = "square"; + o.frequency.value = freq; + o2.frequency.value = freq * 2; + lp.type = "lowpass"; + lp.frequency.value = 2800; + g.gain.setValueAtTime(0.0001, t); + g.gain.exponentialRampToValueAtTime(vol, t + 0.03); + g.gain.setValueAtTime(vol, t + dur * 0.65); + g.gain.exponentialRampToValueAtTime(0.0001, t + dur); + o.connect(g); + o2.connect(g); + g.connect(lp).connect(this.master); + o.start(t); + o2.start(t); + o.stop(t + dur + 0.02); + o2.stop(t + dur + 0.02); + }, + // trombe trionfali: "ta ta-ta-taaa" + fanfare() { + this.brass(392, 0.0, 0.12, 0.22); // G4 + this.brass(523, 0.14, 0.12, 0.24); // C5 + this.brass(659, 0.28, 0.12, 0.24); // E5 + this.brass(784, 0.42, 0.7, 0.3); // G5 tenuta + for (let i = 0; i < 6; i++) + setTimeout(() => this.blip(1568 + Math.random() * 700, 0.08, "sine", 0.1), 520 + i * 60); + }, jackpot() { const notes = [523, 659, 784, 1046, 1318, 1568]; notes.forEach((f, i) => @@ -265,47 +301,86 @@ return Math.round(-y / ITEM_H); } - function spinReel(targetId) { - return new Promise((resolve) => { - const targetIndex = buildStrip(targetId); - // centro la target nella riga di mezzo (finestra 3 righe) - const finalY = -(targetIndex - 1) * ITEM_H; - const duration = 4200; + // Nome "pulito" (senza emoji) dell'elemento del rullo a un dato indice. + function reelNameAt(i) { + const el = reelEl.children[i]; + return el ? el.textContent.replace(/[๐Ÿ ๐Ÿ›]/g, "").trim() : ""; + } - // tick sincronizzati al passaggio dei simboli - let lastIdx = 0; - let ticking = true; - (function tickLoop() { + // GIRO con RAGEBAIT: decelera e "si ferma" su una stanza sbagliata (adiacente), + // tiene la suspense, poi scatta di scatto sulla stanza vera. + 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 + 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; + + // --- tick sincronizzati --- + let ticking = false; + let lastIdx = 0; + function startTicks() { + ticking = true; + lastIdx = currentReelIndex(); + (function loop() { if (!ticking) return; const idx = currentReelIndex(); if (idx !== lastIdx) { Audio.tick(); lastIdx = idx; } - requestAnimationFrame(tickLoop); + requestAnimationFrame(loop); })(); + } + const stopTicks = () => (ticking = false); - const stopRiser = Audio.riser(duration / 1000); + const stopRiser = Audio.riser(4.6); - const anim = reelEl.animate( - [{ transform: "translateY(0)" }, { transform: `translateY(${finalY}px)` }], - { duration, easing: "cubic-bezier(0.12, 0.85, 0.18, 1)", fill: "forwards" } + // FASE 1: decelera fino a "fermarsi" sulla stanza SBAGLIATA + 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) { + // 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)}...? ๐Ÿคจ`); + await sleep(700); + + // FASE 3: SCATTO alla stanza vera (rabbia garantita) + Audio.blip(200, 0.14, "sawtooth", 0.22, 90); // whoosh + startTicks(); + const a2 = reelEl.animate( + [{ transform: `translateY(${fakeY}px)` }, { transform: `translateY(${finalY}px)` }], + { duration: 520, easing: "cubic-bezier(0.34, 1.56, 0.64, 1)", fill: "forwards" } ); - anim.onfinish = () => { - ticking = false; - stopRiser(); - // micro-bounce di assestamento (near-miss / lock-in) - reelEl.animate( - [ - { transform: `translateY(${finalY}px)` }, - { transform: `translateY(${finalY + 14}px)` }, - { transform: `translateY(${finalY}px)` }, - ], - { duration: 260, easing: "ease-out" } - ); - resolve(); - }; - }); + await a2.finished; + stopTicks(); + win.classList.remove("teasing"); + } else { + // nessun bluff possibile: piccolo assestamento + reelEl.animate( + [ + { transform: `translateY(${finalY}px)` }, + { transform: `translateY(${finalY + 12}px)` }, + { transform: `translateY(${finalY}px)` }, + ], + { duration: 260, easing: "ease-out", fill: "forwards" } + ); + await sleep(260); + } + stopRiser(); } /* ---------- Stato di gioco ---------- */ @@ -536,6 +611,8 @@ currentBet = null; const isJackpot = room.jacuzzi; + Audio.ding(); // "click" di aggancio della stanza + Audio.fanfare(); // ๐ŸŽบ trombe alla selezione if (isJackpot) { Audio.jackpot(); celebrate("jackpot"); @@ -544,7 +621,6 @@ mc(`๐ŸŽ‰ JACKPOT! ${person} conquista ${room.nome} con IDROMASSAGGIO! ๐Ÿ›`); log(`JACKPOT: ${person} โ†’ ${room.nome} ๐Ÿ›`, "jackpot"); } else { - Audio.ding(); celebrate("normal"); mc(`${person} va in ${room.nome}!`); log(`${person} โ†’ ${room.nome}`); diff --git a/styles.css b/styles.css index 841e446..0aa30a2 100644 --- a/styles.css +++ b/styles.css @@ -359,9 +359,36 @@ body { position: relative; transform-origin: bottom center; transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); + touch-action: none; +} +/* area di presa invisibile piu' ampia (facile da afferrare/toccare) */ +.lever::before { + content: ""; + position: absolute; + left: -22px; + right: -22px; + top: -22px; + bottom: -10px; +} +.lever.armed { + cursor: grab; + animation: leverBob 1.1s ease-in-out infinite; +} +.lever.armed .lever-knob { + box-shadow: 0 0 16px var(--red), 0 0 30px var(--red), 0 0 46px #ff004c; + animation: knobPulse 1.1s ease-in-out infinite; +} +@keyframes leverBob { + 0%, 100% { transform: rotate(0deg); } + 50% { transform: rotate(-5deg); } +} +@keyframes knobPulse { + 0%, 100% { filter: brightness(1); } + 50% { filter: brightness(1.5); } } .lever.pull { transform: rotate(28deg); + animation: none; } .lever-knob { position: absolute; @@ -375,6 +402,20 @@ body { box-shadow: 0 0 16px var(--red); } +/* effetto "gaslighting": mentre finge di essersi fermato sulla stanza sbagliata */ +.reel-window.teasing { + animation: teaseShake 0.12s linear infinite; +} +.reel-window.teasing .payline { + border-color: var(--red); + box-shadow: 0 0 26px rgba(255, 59, 92, 0.6); +} +@keyframes teaseShake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-2px); } + 75% { transform: translateX(2px); } +} + .mc-line { min-height: 26px; text-align: center;