suoni
This commit is contained in:
138
app.js
138
app.js
@@ -5,6 +5,7 @@
|
|||||||
const $ = (sel) => document.querySelector(sel);
|
const $ = (sel) => document.querySelector(sel);
|
||||||
const roomById = Object.fromEntries(ROOMS.map((r) => [r.id, r]));
|
const roomById = Object.fromEntries(ROOMS.map((r) => [r.id, r]));
|
||||||
const genderOf = Object.fromEntries(PARTICIPANTS.map((p) => [p.nome, p.genere]));
|
const genderOf = Object.fromEntries(PARTICIPANTS.map((p) => [p.nome, p.genere]));
|
||||||
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
/* ---------- Audio sintetizzato (nessun file esterno) ---------- */
|
/* ---------- Audio sintetizzato (nessun file esterno) ---------- */
|
||||||
const Audio = {
|
const Audio = {
|
||||||
@@ -50,6 +51,41 @@
|
|||||||
buzz() {
|
buzz() {
|
||||||
this.blip(140, 0.45, "sawtooth", 0.3, 70);
|
this.blip(140, 0.45, "sawtooth", 0.3, 70);
|
||||||
},
|
},
|
||||||
|
// nota di ottone per la fanfara
|
||||||
|
brass(freq, when, dur, vol = 0.25) {
|
||||||
|
if (!this.on || !this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime + when;
|
||||||
|
const o = this.ctx.createOscillator();
|
||||||
|
const o2 = this.ctx.createOscillator();
|
||||||
|
const g = this.ctx.createGain();
|
||||||
|
const lp = this.ctx.createBiquadFilter();
|
||||||
|
o.type = "sawtooth";
|
||||||
|
o2.type = "square";
|
||||||
|
o.frequency.value = freq;
|
||||||
|
o2.frequency.value = freq * 2;
|
||||||
|
lp.type = "lowpass";
|
||||||
|
lp.frequency.value = 2800;
|
||||||
|
g.gain.setValueAtTime(0.0001, t);
|
||||||
|
g.gain.exponentialRampToValueAtTime(vol, t + 0.03);
|
||||||
|
g.gain.setValueAtTime(vol, t + dur * 0.65);
|
||||||
|
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
|
||||||
|
o.connect(g);
|
||||||
|
o2.connect(g);
|
||||||
|
g.connect(lp).connect(this.master);
|
||||||
|
o.start(t);
|
||||||
|
o2.start(t);
|
||||||
|
o.stop(t + dur + 0.02);
|
||||||
|
o2.stop(t + dur + 0.02);
|
||||||
|
},
|
||||||
|
// trombe trionfali: "ta ta-ta-taaa"
|
||||||
|
fanfare() {
|
||||||
|
this.brass(392, 0.0, 0.12, 0.22); // G4
|
||||||
|
this.brass(523, 0.14, 0.12, 0.24); // C5
|
||||||
|
this.brass(659, 0.28, 0.12, 0.24); // E5
|
||||||
|
this.brass(784, 0.42, 0.7, 0.3); // G5 tenuta
|
||||||
|
for (let i = 0; i < 6; i++)
|
||||||
|
setTimeout(() => this.blip(1568 + Math.random() * 700, 0.08, "sine", 0.1), 520 + i * 60);
|
||||||
|
},
|
||||||
jackpot() {
|
jackpot() {
|
||||||
const notes = [523, 659, 784, 1046, 1318, 1568];
|
const notes = [523, 659, 784, 1046, 1318, 1568];
|
||||||
notes.forEach((f, i) =>
|
notes.forEach((f, i) =>
|
||||||
@@ -265,47 +301,86 @@
|
|||||||
return Math.round(-y / ITEM_H);
|
return Math.round(-y / ITEM_H);
|
||||||
}
|
}
|
||||||
|
|
||||||
function spinReel(targetId) {
|
// Nome "pulito" (senza emoji) dell'elemento del rullo a un dato indice.
|
||||||
return new Promise((resolve) => {
|
function reelNameAt(i) {
|
||||||
const targetIndex = buildStrip(targetId);
|
const el = reelEl.children[i];
|
||||||
// centro la target nella riga di mezzo (finestra 3 righe)
|
return el ? el.textContent.replace(/[🏠🛁]/g, "").trim() : "";
|
||||||
const finalY = -(targetIndex - 1) * ITEM_H;
|
}
|
||||||
const duration = 4200;
|
|
||||||
|
|
||||||
// tick sincronizzati al passaggio dei simboli
|
// GIRO con RAGEBAIT: decelera e "si ferma" su una stanza sbagliata (adiacente),
|
||||||
let lastIdx = 0;
|
// tiene la suspense, poi scatta di scatto sulla stanza vera.
|
||||||
let ticking = true;
|
async function spinReel(targetId) {
|
||||||
(function tickLoop() {
|
const targetIndex = buildStrip(targetId);
|
||||||
|
const finalY = -(targetIndex - 1) * ITEM_H; // target centrata
|
||||||
|
const targetName = roomById[targetId].nome;
|
||||||
|
|
||||||
|
// scegli la 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; // fallback: niente bluff
|
||||||
|
const fakeIndex = targetIndex + dir;
|
||||||
|
const fakeY = -(fakeIndex - 1) * ITEM_H;
|
||||||
|
|
||||||
|
// --- tick sincronizzati ---
|
||||||
|
let ticking = false;
|
||||||
|
let lastIdx = 0;
|
||||||
|
function startTicks() {
|
||||||
|
ticking = true;
|
||||||
|
lastIdx = currentReelIndex();
|
||||||
|
(function loop() {
|
||||||
if (!ticking) return;
|
if (!ticking) return;
|
||||||
const idx = currentReelIndex();
|
const idx = currentReelIndex();
|
||||||
if (idx !== lastIdx) {
|
if (idx !== lastIdx) {
|
||||||
Audio.tick();
|
Audio.tick();
|
||||||
lastIdx = idx;
|
lastIdx = idx;
|
||||||
}
|
}
|
||||||
requestAnimationFrame(tickLoop);
|
requestAnimationFrame(loop);
|
||||||
})();
|
})();
|
||||||
|
}
|
||||||
|
const stopTicks = () => (ticking = false);
|
||||||
|
|
||||||
const stopRiser = Audio.riser(duration / 1000);
|
const stopRiser = Audio.riser(4.6);
|
||||||
|
|
||||||
const anim = reelEl.animate(
|
// FASE 1: decelera fino a "fermarsi" sulla stanza SBAGLIATA
|
||||||
[{ transform: "translateY(0)" }, { transform: `translateY(${finalY}px)` }],
|
startTicks();
|
||||||
{ duration, easing: "cubic-bezier(0.12, 0.85, 0.18, 1)", fill: "forwards" }
|
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();
|
||||||
|
|
||||||
|
if (dir !== 0) {
|
||||||
|
// 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(fakeIndex)}...? 🤨`);
|
||||||
|
await sleep(700);
|
||||||
|
|
||||||
|
// FASE 3: SCATTO alla stanza vera (rabbia garantita)
|
||||||
|
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" }
|
||||||
);
|
);
|
||||||
anim.onfinish = () => {
|
await a2.finished;
|
||||||
ticking = false;
|
stopTicks();
|
||||||
stopRiser();
|
win.classList.remove("teasing");
|
||||||
// micro-bounce di assestamento (near-miss / lock-in)
|
} else {
|
||||||
reelEl.animate(
|
// nessun bluff possibile: piccolo assestamento
|
||||||
[
|
reelEl.animate(
|
||||||
{ transform: `translateY(${finalY}px)` },
|
[
|
||||||
{ transform: `translateY(${finalY + 14}px)` },
|
{ transform: `translateY(${finalY}px)` },
|
||||||
{ transform: `translateY(${finalY}px)` },
|
{ transform: `translateY(${finalY + 12}px)` },
|
||||||
],
|
{ transform: `translateY(${finalY}px)` },
|
||||||
{ duration: 260, easing: "ease-out" }
|
],
|
||||||
);
|
{ duration: 260, easing: "ease-out", fill: "forwards" }
|
||||||
resolve();
|
);
|
||||||
};
|
await sleep(260);
|
||||||
});
|
}
|
||||||
|
stopRiser();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Stato di gioco ---------- */
|
/* ---------- Stato di gioco ---------- */
|
||||||
@@ -536,6 +611,8 @@
|
|||||||
currentBet = null;
|
currentBet = null;
|
||||||
|
|
||||||
const isJackpot = room.jacuzzi;
|
const isJackpot = room.jacuzzi;
|
||||||
|
Audio.ding(); // "click" di aggancio della stanza
|
||||||
|
Audio.fanfare(); // 🎺 trombe alla selezione
|
||||||
if (isJackpot) {
|
if (isJackpot) {
|
||||||
Audio.jackpot();
|
Audio.jackpot();
|
||||||
celebrate("jackpot");
|
celebrate("jackpot");
|
||||||
@@ -544,7 +621,6 @@
|
|||||||
mc(`🎉 JACKPOT! ${person} conquista ${room.nome} con IDROMASSAGGIO! 🛁`);
|
mc(`🎉 JACKPOT! ${person} conquista ${room.nome} con IDROMASSAGGIO! 🛁`);
|
||||||
log(`JACKPOT: ${person} → ${room.nome} 🛁`, "jackpot");
|
log(`JACKPOT: ${person} → ${room.nome} 🛁`, "jackpot");
|
||||||
} else {
|
} else {
|
||||||
Audio.ding();
|
|
||||||
celebrate("normal");
|
celebrate("normal");
|
||||||
mc(`${person} va in ${room.nome}!`);
|
mc(`${person} va in ${room.nome}!`);
|
||||||
log(`${person} → ${room.nome}`);
|
log(`${person} → ${room.nome}`);
|
||||||
|
|||||||
41
styles.css
41
styles.css
@@ -359,9 +359,36 @@ body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
transform-origin: bottom center;
|
transform-origin: bottom center;
|
||||||
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
/* area di presa invisibile piu' ampia (facile da afferrare/toccare) */
|
||||||
|
.lever::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: -22px;
|
||||||
|
right: -22px;
|
||||||
|
top: -22px;
|
||||||
|
bottom: -10px;
|
||||||
|
}
|
||||||
|
.lever.armed {
|
||||||
|
cursor: grab;
|
||||||
|
animation: leverBob 1.1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.lever.armed .lever-knob {
|
||||||
|
box-shadow: 0 0 16px var(--red), 0 0 30px var(--red), 0 0 46px #ff004c;
|
||||||
|
animation: knobPulse 1.1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes leverBob {
|
||||||
|
0%, 100% { transform: rotate(0deg); }
|
||||||
|
50% { transform: rotate(-5deg); }
|
||||||
|
}
|
||||||
|
@keyframes knobPulse {
|
||||||
|
0%, 100% { filter: brightness(1); }
|
||||||
|
50% { filter: brightness(1.5); }
|
||||||
}
|
}
|
||||||
.lever.pull {
|
.lever.pull {
|
||||||
transform: rotate(28deg);
|
transform: rotate(28deg);
|
||||||
|
animation: none;
|
||||||
}
|
}
|
||||||
.lever-knob {
|
.lever-knob {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -375,6 +402,20 @@ body {
|
|||||||
box-shadow: 0 0 16px var(--red);
|
box-shadow: 0 0 16px var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* effetto "gaslighting": mentre finge di essersi fermato sulla stanza sbagliata */
|
||||||
|
.reel-window.teasing {
|
||||||
|
animation: teaseShake 0.12s linear infinite;
|
||||||
|
}
|
||||||
|
.reel-window.teasing .payline {
|
||||||
|
border-color: var(--red);
|
||||||
|
box-shadow: 0 0 26px rgba(255, 59, 92, 0.6);
|
||||||
|
}
|
||||||
|
@keyframes teaseShake {
|
||||||
|
0%, 100% { transform: translateX(0); }
|
||||||
|
25% { transform: translateX(-2px); }
|
||||||
|
75% { transform: translateX(2px); }
|
||||||
|
}
|
||||||
|
|
||||||
.mc-line {
|
.mc-line {
|
||||||
min-height: 26px;
|
min-height: 26px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user