3 colonne

This commit is contained in:
2026-07-02 14:02:09 +02:00
parent be284db25c
commit 130d6f944a
3 changed files with 160 additions and 108 deletions

228
app.js
View File

@@ -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;
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 = "&nbsp;";
// 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)} <b>${targetRoom.nome}</b> ${symbolOf(targetRoom)}`;
}
/* ---------- Zoom macchina ---------- */
@@ -490,6 +514,8 @@
renderRooms();
renderLeaderboard();
setProgress();
fillReelsIdle();
$("#reel-result").innerHTML = "&nbsp;";
$("#player-name").textContent = "—";
$("#odds").innerHTML = "";
mc("Tira giù la leva per far salire il primo! ⬇️");