3 colonne
This commit is contained in:
234
app.js
234
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 `<div class="reel-item${jz}"><span>${icon}</span><span>${room.nome}</span></div>`;
|
||||
return `<div class="reel-item${jz}" data-room="${room.id}">${symbolOf(room)}</div>`;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// 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; // niente adiacente valida
|
||||
const wantTease = dir !== 0 && Math.random() < TEASE_CHANCE;
|
||||
|
||||
// --- 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);
|
||||
|
||||
// ZOOM sulla macchina + shake mentre gira
|
||||
zoomMachineIn();
|
||||
|
||||
const stopRiser = Audio.riser(wantTease ? 4.6 : 4.2);
|
||||
startTicks();
|
||||
|
||||
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(targetIndex + dir)}...? 🤨`);
|
||||
await sleep(700);
|
||||
|
||||
// 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(
|
||||
function bounceReel(el, finalY) {
|
||||
return el.animate(
|
||||
[
|
||||
{ transform: `translateY(${finalY}px)` },
|
||||
{ transform: `translateY(${finalY + 12}px)` },
|
||||
{ transform: `translateY(${finalY}px)` },
|
||||
],
|
||||
{ duration: 260, easing: "ease-out", fill: "forwards" }
|
||||
);
|
||||
await sleep(260);
|
||||
{ duration: 220, easing: "ease-out", fill: "forwards" }
|
||||
).finished;
|
||||
}
|
||||
|
||||
// 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 (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} ${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(5.0);
|
||||
|
||||
// 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);
|
||||
})();
|
||||
|
||||
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),
|
||||
]);
|
||||
|
||||
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)} <b>${targetRoom.nome}</b> ${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! ⬇️");
|
||||
|
||||
@@ -61,7 +61,11 @@
|
||||
<div class="slot-side left"></div>
|
||||
<div class="reel-window">
|
||||
<div class="payline"></div>
|
||||
<div id="reel" class="reel-strip"></div>
|
||||
<div class="reels">
|
||||
<div class="reel-col"><div id="reel0" class="reel-strip"></div></div>
|
||||
<div class="reel-col"><div id="reel1" class="reel-strip"></div></div>
|
||||
<div class="reel-col"><div id="reel2" class="reel-strip"></div></div>
|
||||
</div>
|
||||
<div class="reel-fade top"></div>
|
||||
<div class="reel-fade bottom"></div>
|
||||
</div>
|
||||
@@ -70,6 +74,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="reel-result" class="reel-result"> </div>
|
||||
<div id="mc" class="mc-line">Benvenuti alla lotteria delle stanze 🎲</div>
|
||||
|
||||
<div class="controls">
|
||||
|
||||
33
styles.css
33
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;
|
||||
|
||||
Reference in New Issue
Block a user