3 colonne
This commit is contained in:
228
app.js
228
app.js
@@ -40,6 +40,10 @@
|
|||||||
tick() {
|
tick() {
|
||||||
this.blip(1300, 0.03, "square", 0.12);
|
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() {
|
ding() {
|
||||||
this.blip(1318, 0.5, "sine", 0.3); // E6
|
this.blip(1318, 0.5, "sine", 0.3); // E6
|
||||||
this.blip(1568, 0.55, "sine", 0.25); // G6
|
this.blip(1568, 0.55, "sine", 0.25); // G6
|
||||||
@@ -281,140 +285,160 @@
|
|||||||
})(last);
|
})(last);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Rullo slot ---------- */
|
/* ---------- Rulli slot (3 colonne) ---------- */
|
||||||
const ITEM_H = 88;
|
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) {
|
function reelItemHTML(room) {
|
||||||
const jz = room.jacuzzi ? " jz" : "";
|
const jz = room.jacuzzi ? " jz" : "";
|
||||||
const icon = room.jacuzzi ? "🛁" : "🏠";
|
return `<div class="reel-item${jz}" data-room="${room.id}">${symbolOf(room)}</div>`;
|
||||||
return `<div class="reel-item${jz}"><span>${icon}</span><span>${room.nome}</span></div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Costruisce una striscia lunga che termina sulla stanza target (centrata
|
// Costruisce la striscia di un rullo che termina sulla stanza target
|
||||||
// nella riga di mezzo della finestra a 3 righe) e ritorna l'indice target.
|
// (centrata nella riga di mezzo); ritorna l'indice target.
|
||||||
function buildStrip(targetId) {
|
function buildStripIn(el, targetId) {
|
||||||
// azzera le animazioni del giro precedente (evita accumulo + fill forwards)
|
el.getAnimations().forEach((a) => a.cancel());
|
||||||
reelEl.getAnimations().forEach((a) => a.cancel());
|
|
||||||
const loops = 14;
|
const loops = 14;
|
||||||
const seq = [];
|
const seq = [];
|
||||||
for (let l = 0; l < loops; l++)
|
for (let l = 0; l < loops; l++) for (const r of ROOMS) seq.push(r);
|
||||||
for (const r of ROOMS) seq.push(r);
|
const start = Math.floor(Math.random() * ROOMS.length); // offset diverso per rullo
|
||||||
// offset iniziale casuale per non ripartire sempre uguale
|
|
||||||
const start = Math.floor(Math.random() * ROOMS.length);
|
|
||||||
const rotated = seq.slice(start).concat(seq.slice(0, start));
|
const rotated = seq.slice(start).concat(seq.slice(0, start));
|
||||||
const targetIndex = rotated.length; // la target va in coda
|
const targetIndex = rotated.length; // la target va in coda
|
||||||
rotated.push(roomById[targetId]);
|
rotated.push(roomById[targetId]);
|
||||||
// aggiungo 2 righe sotto per riempire la finestra
|
rotated.push(ROOMS[0], ROOMS[1]); // riempi sotto la finestra
|
||||||
rotated.push(ROOMS[0], ROOMS[1]);
|
el.innerHTML = rotated.map(reelItemHTML).join("");
|
||||||
reelEl.innerHTML = rotated.map(reelItemHTML).join("");
|
el.style.transform = "translateY(0)";
|
||||||
reelEl.style.transform = "translateY(0)";
|
|
||||||
return targetIndex;
|
return targetIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentReelIndex() {
|
// stato a riposo: mostra i simboli fermi invece di rulli vuoti
|
||||||
const t = getComputedStyle(reelEl).transform;
|
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;
|
if (!t || t === "none") return 0;
|
||||||
const y = parseFloat(t.split(",")[5]); // matrix(a,b,c,d,e,f) -> f = translateY
|
const y = parseFloat(t.split(",")[5]); // matrix(a,b,c,d,e,f) -> f = translateY
|
||||||
return Math.round(-y / ITEM_H);
|
return Math.round(-y / ITEM_H);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nome "pulito" (senza emoji) dell'elemento del rullo a un dato indice.
|
const roomAtIndex = (el, i) =>
|
||||||
function reelNameAt(i) {
|
el.children[i] ? roomById[el.children[i].dataset.room] : null;
|
||||||
const el = reelEl.children[i];
|
|
||||||
return el ? el.textContent.replace(/[🏠🛁]/g, "").trim() : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Probabilità di ragebait (finto stop sulla stanza sbagliata) per giro.
|
// Probabilità di ragebait (finto stop) sul rullo finale.
|
||||||
const TEASE_CHANCE = 0.3;
|
const TEASE_CHANCE = 0.3;
|
||||||
|
|
||||||
// GIRO. Nel ~30% dei casi: decelera e "si ferma" su una stanza sbagliata
|
function bounceReel(el, finalY) {
|
||||||
// (adiacente), tiene la suspense, poi scatta sulla stanza vera. Altrimenti
|
return el.animate(
|
||||||
// atterra direttamente con un normale assestamento.
|
[
|
||||||
async function spinReel(targetId) {
|
{ transform: `translateY(${finalY}px)` },
|
||||||
const targetIndex = buildStrip(targetId);
|
{ transform: `translateY(${finalY + 12}px)` },
|
||||||
const finalY = -(targetIndex - 1) * ITEM_H; // target centrata
|
{ transform: `translateY(${finalY}px)` },
|
||||||
const targetName = roomById[targetId].nome;
|
],
|
||||||
|
{ 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;
|
let dir = Math.random() < 0.5 ? -1 : 1;
|
||||||
if (reelNameAt(targetIndex + dir) === targetName) dir = -dir;
|
if (roomAtIndex(el, targetIndex + dir)?.id === targetRoom.id) dir = -dir;
|
||||||
if (reelNameAt(targetIndex + dir) === targetName) dir = 0; // niente adiacente valida
|
if (roomAtIndex(el, targetIndex + dir)?.id === targetRoom.id)
|
||||||
const wantTease = dir !== 0 && Math.random() < TEASE_CHANCE;
|
return reelLand(el, finalY, 4200); // niente adiacente valida
|
||||||
|
const fakeRoom = roomAtIndex(el, targetIndex + dir);
|
||||||
|
const fakeY = -(targetIndex + dir - 1) * ITEM_H;
|
||||||
|
|
||||||
// --- tick sincronizzati ---
|
// FASE 1: decelera e "si ferma" sulla stanza SBAGLIATA
|
||||||
let ticking = false;
|
await el.animate(
|
||||||
let lastIdx = 0;
|
[{ transform: "translateY(0)" }, { transform: `translateY(${fakeY}px)` }],
|
||||||
function startTicks() {
|
{ duration: 3600, easing: "cubic-bezier(0.08, 0.85, 0.14, 1)", fill: "forwards" }
|
||||||
ticking = true;
|
).finished;
|
||||||
lastIdx = currentReelIndex();
|
Audio.reelStop();
|
||||||
(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
|
// 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();
|
zoomMachineIn();
|
||||||
|
const mainDur = 4200;
|
||||||
|
setTimeout(zoomMachineSteady, mainDur / 2);
|
||||||
|
|
||||||
const stopRiser = Audio.riser(wantTease ? 4.6 : 4.2);
|
const stopRiser = Audio.riser(5.0);
|
||||||
startTicks();
|
|
||||||
|
|
||||||
if (wantTease) {
|
// tick sincronizzati sul 3° rullo (gira più a lungo)
|
||||||
const fakeY = -(targetIndex + dir - 1) * ITEM_H;
|
let ticking = true;
|
||||||
// FASE 1: decelera fino a "fermarsi" sulla stanza SBAGLIATA
|
let lastIdx = 0;
|
||||||
const a1 = reelEl.animate(
|
(function loop() {
|
||||||
[{ transform: "translateY(0)" }, { transform: `translateY(${fakeY}px)` }],
|
if (!ticking) return;
|
||||||
{ duration: 3600, easing: "cubic-bezier(0.08, 0.85, 0.14, 1)", fill: "forwards" }
|
const i = currentIndexOf(reelEls[2]);
|
||||||
);
|
if (i !== lastIdx) {
|
||||||
await a1.finished;
|
Audio.tick();
|
||||||
stopTicks();
|
lastIdx = i;
|
||||||
|
}
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
})();
|
||||||
|
|
||||||
// FASE 2: gaslighting — sembra tutto finito
|
const wantTease = Math.random() < TEASE_CHANCE;
|
||||||
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
|
// stop staggerato sinistra -> destra
|
||||||
Audio.blip(200, 0.14, "sawtooth", 0.22, 90); // whoosh
|
await Promise.all([
|
||||||
startTicks();
|
reelLand(reelEls[0], finalY, 2600),
|
||||||
const a2 = reelEl.animate(
|
reelLand(reelEls[1], finalY, 3300),
|
||||||
[{ transform: `translateY(${fakeY}px)` }, { transform: `translateY(${finalY}px)` }],
|
wantTease
|
||||||
{ duration: 520, easing: "cubic-bezier(0.34, 1.56, 0.64, 1)", fill: "forwards" }
|
? reelLandTease(reelEls[2], targetIndex, finalY)
|
||||||
);
|
: reelLand(reelEls[2], finalY, 4200),
|
||||||
await a2.finished;
|
]);
|
||||||
stopTicks();
|
|
||||||
win.classList.remove("teasing");
|
ticking = false;
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
stopRiser();
|
stopRiser();
|
||||||
// fine scroll: ferma il tremolio ma resta zoomata (il ritorno avviene
|
|
||||||
// dopo la vittoria, con una mini-animazione a molla — vedi zoomMachineOut)
|
|
||||||
zoomMachineSteady();
|
zoomMachineSteady();
|
||||||
|
$("#reel-result").innerHTML = `${symbolOf(targetRoom)} <b>${targetRoom.nome}</b> ${symbolOf(targetRoom)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Zoom macchina ---------- */
|
/* ---------- Zoom macchina ---------- */
|
||||||
@@ -490,6 +514,8 @@
|
|||||||
renderRooms();
|
renderRooms();
|
||||||
renderLeaderboard();
|
renderLeaderboard();
|
||||||
setProgress();
|
setProgress();
|
||||||
|
fillReelsIdle();
|
||||||
|
$("#reel-result").innerHTML = " ";
|
||||||
$("#player-name").textContent = "—";
|
$("#player-name").textContent = "—";
|
||||||
$("#odds").innerHTML = "";
|
$("#odds").innerHTML = "";
|
||||||
mc("Tira giù la leva per far salire il primo! ⬇️");
|
mc("Tira giù la leva per far salire il primo! ⬇️");
|
||||||
|
|||||||
@@ -61,7 +61,11 @@
|
|||||||
<div class="slot-side left"></div>
|
<div class="slot-side left"></div>
|
||||||
<div class="reel-window">
|
<div class="reel-window">
|
||||||
<div class="payline"></div>
|
<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 top"></div>
|
||||||
<div class="reel-fade bottom"></div>
|
<div class="reel-fade bottom"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,6 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="reel-result" class="reel-result"> </div>
|
||||||
<div id="mc" class="mc-line">Benvenuti alla lotteria delle stanze 🎲</div>
|
<div id="mc" class="mc-line">Benvenuti alla lotteria delle stanze 🎲</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
|||||||
33
styles.css
33
styles.css
@@ -324,6 +324,19 @@ body {
|
|||||||
border: 2px solid rgba(255, 255, 255, 0.15);
|
border: 2px solid rgba(255, 255, 255, 0.15);
|
||||||
box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.9);
|
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 {
|
.reel-strip {
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
@@ -332,17 +345,25 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 10px;
|
font-size: 54px;
|
||||||
font-family: "Anton", sans-serif;
|
line-height: 1;
|
||||||
font-size: 34px;
|
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.2));
|
||||||
letter-spacing: 1px;
|
|
||||||
color: #fff;
|
|
||||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.35);
|
|
||||||
}
|
}
|
||||||
.reel-item.jz {
|
.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);
|
color: var(--gold);
|
||||||
text-shadow: 0 0 14px var(--gold-2);
|
text-shadow: 0 0 14px var(--gold-2);
|
||||||
}
|
}
|
||||||
|
.reel-result b {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
.payline {
|
.payline {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user