From 130d6f944ad0872c7c6705cff3870aaefd01c7f2 Mon Sep 17 00:00:00 2001 From: Alberto Moretti Date: Thu, 2 Jul 2026 14:02:09 +0200 Subject: [PATCH] 3 colonne --- app.js | 228 +++++++++++++++++++++++++++++------------------------ index.html | 7 +- styles.css | 33 ++++++-- 3 files changed, 160 insertions(+), 108 deletions(-) diff --git a/app.js b/app.js index dcb864b..9a6a5f0 100644 --- a/app.js +++ b/app.js @@ -40,6 +40,10 @@ tick() { this.blip(1300, 0.03, "square", 0.12); }, + reelStop() { + this.blip(210, 0.08, "square", 0.28); + this.blip(120, 0.13, "sine", 0.2); + }, ding() { this.blip(1318, 0.5, "sine", 0.3); // E6 this.blip(1568, 0.55, "sine", 0.25); // G6 @@ -281,140 +285,160 @@ })(last); } - /* ---------- Rullo slot ---------- */ + /* ---------- Rulli slot (3 colonne) ---------- */ const ITEM_H = 88; - const reelEl = $("#reel"); + const reelEls = [$("#reel0"), $("#reel1"), $("#reel2")]; + + // simbolo (emoji) per ogni stanza — come i simboli di una slot vera + const ROOM_SYMBOL = { + zeus: "⚡", hera: "👑", ignota: "❓", scale: "🪜", + rischio: "🎲", eolo: "🌪️", idro: "💧", argo: "⛵", + }; + const symbolOf = (room) => ROOM_SYMBOL[room.id] || "🏠"; function reelItemHTML(room) { const jz = room.jacuzzi ? " jz" : ""; - const icon = room.jacuzzi ? "🛁" : "🏠"; - return `
${icon}${room.nome}
`; + return `
${symbolOf(room)}
`; } - // Costruisce una striscia lunga che termina sulla stanza target (centrata - // nella riga di mezzo della finestra a 3 righe) e ritorna l'indice target. - function buildStrip(targetId) { - // azzera le animazioni del giro precedente (evita accumulo + fill forwards) - reelEl.getAnimations().forEach((a) => a.cancel()); + // Costruisce la striscia di un rullo che termina sulla stanza target + // (centrata nella riga di mezzo); ritorna l'indice target. + function buildStripIn(el, targetId) { + el.getAnimations().forEach((a) => a.cancel()); const loops = 14; const seq = []; - for (let l = 0; l < loops; l++) - for (const r of ROOMS) seq.push(r); - // offset iniziale casuale per non ripartire sempre uguale - const start = Math.floor(Math.random() * ROOMS.length); + for (let l = 0; l < loops; l++) for (const r of ROOMS) seq.push(r); + const start = Math.floor(Math.random() * ROOMS.length); // offset diverso per rullo const rotated = seq.slice(start).concat(seq.slice(0, start)); const targetIndex = rotated.length; // la target va in coda rotated.push(roomById[targetId]); - // aggiungo 2 righe sotto per riempire la finestra - rotated.push(ROOMS[0], ROOMS[1]); - reelEl.innerHTML = rotated.map(reelItemHTML).join(""); - reelEl.style.transform = "translateY(0)"; + rotated.push(ROOMS[0], ROOMS[1]); // riempi sotto la finestra + el.innerHTML = rotated.map(reelItemHTML).join(""); + el.style.transform = "translateY(0)"; return targetIndex; } - function currentReelIndex() { - const t = getComputedStyle(reelEl).transform; + // stato a riposo: mostra i simboli fermi invece di rulli vuoti + function fillReelsIdle() { + reelEls.forEach((el, i) => { + el.getAnimations().forEach((a) => a.cancel()); + el.innerHTML = ROOMS.map(reelItemHTML).join(""); + const k = i + 1; // centra un simbolo diverso per rullo + el.style.transform = `translateY(${-(k - 1) * ITEM_H}px)`; + }); + } + + function currentIndexOf(el) { + const t = getComputedStyle(el).transform; if (!t || t === "none") return 0; const y = parseFloat(t.split(",")[5]); // matrix(a,b,c,d,e,f) -> f = translateY return Math.round(-y / ITEM_H); } - // 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() : ""; - } + const roomAtIndex = (el, i) => + el.children[i] ? roomById[el.children[i].dataset.room] : null; - // Probabilità di ragebait (finto stop sulla stanza sbagliata) per giro. + // Probabilità di ragebait (finto stop) sul rullo finale. 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; + function bounceReel(el, finalY) { + return el.animate( + [ + { transform: `translateY(${finalY}px)` }, + { transform: `translateY(${finalY + 12}px)` }, + { transform: `translateY(${finalY}px)` }, + ], + { duration: 220, easing: "ease-out", fill: "forwards" } + ).finished; + } - // direzione del bluff: la "finta" stanza deve essere diversa dalla vera + // rullo che atterra dritto sulla target + async function reelLand(el, finalY, duration) { + await el.animate( + [{ transform: "translateY(0)" }, { transform: `translateY(${finalY}px)` }], + { duration, easing: "cubic-bezier(0.12, 0.85, 0.18, 1)", fill: "forwards" } + ).finished; + await bounceReel(el, finalY); + 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 (reelNameAt(targetIndex + dir) === targetName) dir = -dir; - if (reelNameAt(targetIndex + dir) === targetName) dir = 0; // niente adiacente valida - const wantTease = dir !== 0 && Math.random() < TEASE_CHANCE; + 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; - // --- 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(loop); - })(); - } - const stopTicks = () => (ticking = false); + // 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(); - // ZOOM sulla macchina + shake mentre gira + // 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} ${symbolOf(fakeRoom)}...? 🤨`); + 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). + async function spinReel(targetId) { + const targetIndex = reelEls.map((el) => buildStripIn(el, targetId))[0]; // uguale per tutti + const finalY = -(targetIndex - 1) * ITEM_H; + const targetRoom = roomById[targetId]; + $("#reel-result").innerHTML = " "; + + // ZOOM + shake; lo shake si ferma a metà spin (quando inizia a rallentare) zoomMachineIn(); + const mainDur = 4200; + setTimeout(zoomMachineSteady, mainDur / 2); - const stopRiser = Audio.riser(wantTease ? 4.6 : 4.2); - startTicks(); + const stopRiser = Audio.riser(5.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(); + // tick sincronizzati sul 3° rullo (gira più a lungo) + let ticking = true; + let lastIdx = 0; + (function loop() { + if (!ticking) return; + const i = currentIndexOf(reelEls[2]); + if (i !== lastIdx) { + Audio.tick(); + lastIdx = i; + } + requestAnimationFrame(loop); + })(); - // 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(targetIndex + dir)}...? 🤨`); - await sleep(700); + const wantTease = Math.random() < TEASE_CHANCE; - // FASE 3: SCATTO alla stanza vera - 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" } - ); - await a2.finished; - stopTicks(); - win.classList.remove("teasing"); - } else { - // 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)` }, - { transform: `translateY(${finalY + 12}px)` }, - { transform: `translateY(${finalY}px)` }, - ], - { duration: 260, easing: "ease-out", fill: "forwards" } - ); - await sleep(260); - } + // 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), + ]); + + ticking = false; stopRiser(); - // fine scroll: ferma il tremolio ma resta zoomata (il ritorno avviene - // dopo la vittoria, con una mini-animazione a molla — vedi zoomMachineOut) zoomMachineSteady(); + $("#reel-result").innerHTML = `${symbolOf(targetRoom)} ${targetRoom.nome} ${symbolOf(targetRoom)}`; } /* ---------- Zoom macchina ---------- */ @@ -490,6 +514,8 @@ renderRooms(); renderLeaderboard(); setProgress(); + fillReelsIdle(); + $("#reel-result").innerHTML = " "; $("#player-name").textContent = "—"; $("#odds").innerHTML = ""; mc("Tira giù la leva per far salire il primo! ⬇️"); diff --git a/index.html b/index.html index 322c27a..8454a94 100644 --- a/index.html +++ b/index.html @@ -61,7 +61,11 @@
-
+
+
+
+
+
@@ -70,6 +74,7 @@ +
 
Benvenuti alla lotteria delle stanze 🎲
diff --git a/styles.css b/styles.css index 804a259..1a062a9 100644 --- a/styles.css +++ b/styles.css @@ -324,6 +324,19 @@ body { border: 2px solid rgba(255, 255, 255, 0.15); box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.9); } +/* tre colonne affiancate dentro la finestra */ +.reels { + display: flex; + height: 100%; +} +.reel-col { + flex: 1; + position: relative; + overflow: hidden; +} +.reel-col + .reel-col { + border-left: 2px solid rgba(255, 255, 255, 0.08); +} .reel-strip { will-change: transform; } @@ -332,17 +345,25 @@ body { display: flex; align-items: center; justify-content: center; - gap: 10px; - font-family: "Anton", sans-serif; - font-size: 34px; - letter-spacing: 1px; - color: #fff; - text-shadow: 0 0 10px rgba(255, 255, 255, 0.35); + font-size: 54px; + line-height: 1; + filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.2)); } .reel-item.jz { + filter: drop-shadow(0 0 14px var(--gold-2)); +} +/* caption con il nome della stanza risultante */ +.reel-result { + min-height: 34px; + font-family: "Anton", sans-serif; + font-size: 26px; + letter-spacing: 1px; color: var(--gold); text-shadow: 0 0 14px var(--gold-2); } +.reel-result b { + font-weight: 400; +} .payline { position: absolute; left: 0;