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;