diff --git a/app.js b/app.js index 1ae2985..5690c99 100644 --- a/app.js +++ b/app.js @@ -346,8 +346,46 @@ ).finished; } - // rullo che atterra dritto sulla target - async function reelLand(el, finalY, duration) { + // Atterraggio di un rullo sulla target. Se teaseMs > 0 fa il RAGEBAIT: + // si ferma su una stanza adiacente sbagliata, tiene la suspense (teaseMs), + // poi scatta sulla vera. Funziona su QUALSIASI colonna. + async function reelLand(el, targetIndex, finalY, duration, teaseMs) { + // eventuale bluff: scegli una stanza adiacente diversa dalla vera + if (teaseMs > 0) { + const targetRoom = roomAtIndex(el, targetIndex); + let dir = Math.random() < 0.5 ? -1 : 1; + if (roomAtIndex(el, targetIndex + dir)?.id === targetRoom.id) dir = -dir; + if (roomAtIndex(el, targetIndex + dir)?.id === targetRoom.id) teaseMs = 0; // no adiacente + if (teaseMs > 0) { + const fakeRoom = roomAtIndex(el, targetIndex + dir); + const fakeY = -(targetIndex + dir - 1) * ITEM_H; + const col = el.parentElement; + + // decelera e "si ferma" sulla stanza SBAGLIATA + await el.animate( + [{ transform: "translateY(0)" }, { transform: `translateY(${fakeY}px)` }], + { duration, easing: "cubic-bezier(0.08, 0.85, 0.14, 1)", fill: "forwards" } + ).finished; + Audio.reelStop(); + + // suspense su quella colonna + Audio.blip(330, 0.13, "sine", 0.14); + col.classList.add("teasing"); + mc(`${fakeRoom.nome}...? 🤨`); + await sleep(teaseMs); + + // SCATTO alla stanza vera + Audio.blip(200, 0.12, "sawtooth", 0.2, 90); // whoosh + await el.animate( + [{ transform: `translateY(${fakeY}px)` }, { transform: `translateY(${finalY}px)` }], + { duration: 480, easing: "cubic-bezier(0.34, 1.56, 0.64, 1)", fill: "forwards" } + ).finished; + col.classList.remove("teasing"); + Audio.reelStop(); + return; + } + } + // atterraggio diretto + micro-assestamento await el.animate( [{ transform: "translateY(0)" }, { transform: `translateY(${finalY}px)` }], { duration, easing: "cubic-bezier(0.12, 0.85, 0.18, 1)", fill: "forwards" } @@ -356,42 +394,10 @@ Audio.reelStop(); } - // rullo con RAGEBAIT: si ferma sulla stanza sbagliata, suspense, poi scatta - async function reelLandTease(el, targetIndex, finalY) { - const targetRoom = roomAtIndex(el, targetIndex); - let dir = Math.random() < 0.5 ? -1 : 1; - if (roomAtIndex(el, targetIndex + dir)?.id === targetRoom.id) dir = -dir; - if (roomAtIndex(el, targetIndex + dir)?.id === targetRoom.id) - return reelLand(el, finalY, 4200); // niente adiacente valida - const fakeRoom = roomAtIndex(el, targetIndex + dir); - const fakeY = -(targetIndex + dir - 1) * ITEM_H; - - // FASE 1: decelera e "si ferma" sulla stanza SBAGLIATA - await el.animate( - [{ transform: "translateY(0)" }, { transform: `translateY(${fakeY}px)` }], - { duration: 3600, easing: "cubic-bezier(0.08, 0.85, 0.14, 1)", fill: "forwards" } - ).finished; - Audio.reelStop(); - - // FASE 2: gaslighting — sembra fatta - Audio.blip(320, 0.18, "sine", 0.16); - const win = $(".reel-window"); - win.classList.add("teasing"); - mc(`Ferma su ${fakeRoom.nome}...? 🤨`); - await sleep(750); - - // FASE 3: SCATTO alla stanza vera - Audio.blip(200, 0.14, "sawtooth", 0.22, 90); // whoosh - await el.animate( - [{ transform: `translateY(${fakeY}px)` }, { transform: `translateY(${finalY}px)` }], - { duration: 520, easing: "cubic-bezier(0.34, 1.56, 0.64, 1)", fill: "forwards" } - ).finished; - win.classList.remove("teasing"); - Audio.reelStop(); - } - // GIRO: i 3 rulli partono insieme e si fermano da sinistra a destra, - // allineandosi tutti sulla stanza assegnata (l'ultimo può fare il ragebait). + // allineandosi sulla stanza assegnata. OGNI colonna può fare il ragebait + // (in modo indipendente); le colonne più a destra tengono la suspense più a + // lungo, così il dramma "cascata" verso l'ultimo rullo. async function spinReel(targetId) { const targetIndex = reelEls.map((el) => buildStripIn(el, targetId))[0]; // uguale per tutti const finalY = -(targetIndex - 1) * ITEM_H; @@ -403,7 +409,7 @@ const mainDur = 4200; setTimeout(zoomMachineSteady, mainDur / 2); - const stopRiser = Audio.riser(5.0); + const stopRiser = Audio.riser(5.6); // tick sincronizzati sul 3° rullo (gira più a lungo) let ticking = true; @@ -418,16 +424,14 @@ requestAnimationFrame(loop); })(); - const wantTease = Math.random() < TEASE_CHANCE; - - // stop staggerato sinistra -> destra - await Promise.all([ - reelLand(reelEls[0], finalY, 2600), - reelLand(reelEls[1], finalY, 3300), - wantTease - ? reelLandTease(reelEls[2], targetIndex, finalY) - : reelLand(reelEls[2], finalY, 4200), - ]); + const durs = [2600, 3300, 4200]; + await Promise.all( + reelEls.map((el, i) => { + // ogni colonna ha una sua chance di bluff; suspense crescente verso destra + const teaseMs = Math.random() < TEASE_CHANCE ? 380 + i * 260 : 0; + return reelLand(el, targetIndex, finalY, durs[i], teaseMs); + }) + ); ticking = false; stopRiser(); diff --git a/styles.css b/styles.css index 4debef1..018ef3b 100644 --- a/styles.css +++ b/styles.css @@ -458,18 +458,22 @@ 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; +/* effetto "gaslighting" sulla SINGOLA colonna che finge di fermarsi sbagliata */ +.reel-col.teasing { + animation: teaseShake 0.1s linear infinite; + z-index: 2; } -.reel-window.teasing .payline { - border-color: var(--red); - box-shadow: 0 0 26px rgba(255, 59, 92, 0.6); +.reel-col.teasing::after { + content: ""; + position: absolute; + inset: 0; + box-shadow: inset 0 0 34px rgba(255, 59, 92, 0.6); + pointer-events: none; } @keyframes teaseShake { 0%, 100% { transform: translateX(0); } - 25% { transform: translateX(-2px); } - 75% { transform: translateX(2px); } + 25% { transform: translateX(-3px); } + 75% { transform: translateX(3px); } } .mc-line {