This commit is contained in:
2026-07-02 13:33:34 +02:00
parent c7911f670a
commit 9482f77eef
2 changed files with 148 additions and 31 deletions

138
app.js
View File

@@ -5,6 +5,7 @@
const $ = (sel) => document.querySelector(sel);
const roomById = Object.fromEntries(ROOMS.map((r) => [r.id, r]));
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) ---------- */
const Audio = {
@@ -50,6 +51,41 @@
buzz() {
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() {
const notes = [523, 659, 784, 1046, 1318, 1568];
notes.forEach((f, i) =>
@@ -265,47 +301,86 @@
return Math.round(-y / ITEM_H);
}
function spinReel(targetId) {
return new Promise((resolve) => {
const targetIndex = buildStrip(targetId);
// centro la target nella riga di mezzo (finestra 3 righe)
const finalY = -(targetIndex - 1) * ITEM_H;
const duration = 4200;
// 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() : "";
}
// tick sincronizzati al passaggio dei simboli
let lastIdx = 0;
let ticking = true;
(function tickLoop() {
// GIRO con RAGEBAIT: decelera e "si ferma" su una stanza sbagliata (adiacente),
// tiene la suspense, poi scatta di scatto sulla stanza vera.
async function spinReel(targetId) {
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;
const idx = currentReelIndex();
if (idx !== lastIdx) {
Audio.tick();
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(
[{ transform: "translateY(0)" }, { transform: `translateY(${finalY}px)` }],
{ duration, easing: "cubic-bezier(0.12, 0.85, 0.18, 1)", fill: "forwards" }
// FASE 1: decelera fino a "fermarsi" sulla stanza SBAGLIATA
startTicks();
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 = () => {
ticking = false;
stopRiser();
// micro-bounce di assestamento (near-miss / lock-in)
reelEl.animate(
[
{ transform: `translateY(${finalY}px)` },
{ transform: `translateY(${finalY + 14}px)` },
{ transform: `translateY(${finalY}px)` },
],
{ duration: 260, easing: "ease-out" }
);
resolve();
};
});
await a2.finished;
stopTicks();
win.classList.remove("teasing");
} else {
// nessun bluff possibile: piccolo assestamento
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();
}
/* ---------- Stato di gioco ---------- */
@@ -536,6 +611,8 @@
currentBet = null;
const isJackpot = room.jacuzzi;
Audio.ding(); // "click" di aggancio della stanza
Audio.fanfare(); // 🎺 trombe alla selezione
if (isJackpot) {
Audio.jackpot();
celebrate("jackpot");
@@ -544,7 +621,6 @@
mc(`🎉 JACKPOT! ${person} conquista ${room.nome} con IDROMASSAGGIO! 🛁`);
log(`JACKPOT: ${person}${room.nome} 🛁`, "jackpot");
} else {
Audio.ding();
celebrate("normal");
mc(`${person} va in ${room.nome}!`);
log(`${person}${room.nome}`);