gaslighting luoghi
This commit is contained in:
309
app.js
309
app.js
@@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
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));
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
/* ---------- Audio sintetizzato (nessun file esterno) ---------- */
|
/* ---------- Audio sintetizzato (nessun file esterno) ---------- */
|
||||||
@@ -104,16 +106,22 @@
|
|||||||
this.brass(659, 0.28, 0.12, 0.24); // E5
|
this.brass(659, 0.28, 0.12, 0.24); // E5
|
||||||
this.brass(784, 0.42, 0.7, 0.3); // G5 tenuta
|
this.brass(784, 0.42, 0.7, 0.3); // G5 tenuta
|
||||||
for (let i = 0; i < 6; i++)
|
for (let i = 0; i < 6; i++)
|
||||||
setTimeout(() => this.blip(1568 + Math.random() * 700, 0.08, "sine", 0.1), 520 + i * 60);
|
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) =>
|
||||||
setTimeout(() => this.blip(f, 0.35, "square", 0.28), i * 90)
|
setTimeout(() => this.blip(f, 0.35, "square", 0.28), i * 90),
|
||||||
);
|
);
|
||||||
// scintillio
|
// scintillio
|
||||||
for (let i = 0; i < 10; i++)
|
for (let i = 0; i < 10; i++)
|
||||||
setTimeout(() => this.blip(1800 + Math.random() * 1400, 0.08, "sine", 0.12), 600 + i * 70);
|
setTimeout(
|
||||||
|
() => this.blip(1800 + Math.random() * 1400, 0.08, "sine", 0.12),
|
||||||
|
600 + i * 70,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
// riser di tensione: ritorna una funzione stop()
|
// riser di tensione: ritorna una funzione stop()
|
||||||
riser(duration) {
|
riser(duration) {
|
||||||
@@ -160,7 +168,9 @@
|
|||||||
Object.entries(this.files).forEach(([k, url]) => {
|
Object.entries(this.files).forEach(([k, url]) => {
|
||||||
const a = new window.Audio(url); // window.Audio: qui "Audio" è l'oggetto synth
|
const a = new window.Audio(url); // window.Audio: qui "Audio" è l'oggetto synth
|
||||||
a.preload = "auto";
|
a.preload = "auto";
|
||||||
a.addEventListener("canplaythrough", () => (this.ready[k] = true), { once: true });
|
a.addEventListener("canplaythrough", () => (this.ready[k] = true), {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
a.addEventListener("error", () => (this.ready[k] = false));
|
a.addEventListener("error", () => (this.ready[k] = false));
|
||||||
this.el[k] = a;
|
this.el[k] = a;
|
||||||
});
|
});
|
||||||
@@ -185,7 +195,8 @@
|
|||||||
return (fade = 0.2) => {
|
return (fade = 0.2) => {
|
||||||
const steps = 8;
|
const steps = 8;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const iv = setInterval(() => {
|
const iv = setInterval(
|
||||||
|
() => {
|
||||||
i++;
|
i++;
|
||||||
a.volume = Math.max(0, vol * (1 - i / steps));
|
a.volume = Math.max(0, vol * (1 - i / steps));
|
||||||
if (i >= steps) {
|
if (i >= steps) {
|
||||||
@@ -194,7 +205,9 @@
|
|||||||
a.pause();
|
a.pause();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
}, (fade * 1000) / steps);
|
},
|
||||||
|
(fade * 1000) / steps,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -216,23 +229,65 @@
|
|||||||
? confetti.create(confettiCanvas, { resize: false, useWorker: false })
|
? confetti.create(confettiCanvas, { resize: false, useWorker: false })
|
||||||
: () => {};
|
: () => {};
|
||||||
|
|
||||||
const CONFETTI_COLORS = ["#ffd23f", "#ff9e00", "#ff2e88", "#00e5ff", "#29e08a", "#ff3b5c", "#ffffff"];
|
const CONFETTI_COLORS = [
|
||||||
|
"#ffd23f",
|
||||||
|
"#ff9e00",
|
||||||
|
"#ff2e88",
|
||||||
|
"#00e5ff",
|
||||||
|
"#29e08a",
|
||||||
|
"#ff3b5c",
|
||||||
|
"#ffffff",
|
||||||
|
];
|
||||||
|
|
||||||
// VALANGA di coriandoli su TUTTO lo schermo
|
// VALANGA di coriandoli su TUTTO lo schermo
|
||||||
function bigBurst() {
|
function bigBurst() {
|
||||||
const C = CONFETTI_COLORS;
|
const C = CONFETTI_COLORS;
|
||||||
// esplosione centrale gigante a 360°
|
// esplosione centrale gigante a 360°
|
||||||
myConfetti({ particleCount: 400, spread: 360, startVelocity: 45, scalar: 1.1, origin: { x: 0.5, y: 0.5 }, colors: C });
|
myConfetti({
|
||||||
|
particleCount: 400,
|
||||||
|
spread: 360,
|
||||||
|
startVelocity: 45,
|
||||||
|
scalar: 1.1,
|
||||||
|
origin: { x: 0.5, y: 0.5 },
|
||||||
|
colors: C,
|
||||||
|
});
|
||||||
// fila di esplosioni lungo TUTTA la larghezza
|
// fila di esplosioni lungo TUTTA la larghezza
|
||||||
for (let x = 0; x <= 1.0001; x += 0.1) {
|
for (let x = 0; x <= 1.0001; x += 0.1) {
|
||||||
myConfetti({ particleCount: 60, spread: 110, startVelocity: 55, origin: { x, y: 0.55 }, colors: C });
|
myConfetti({
|
||||||
|
particleCount: 60,
|
||||||
|
spread: 110,
|
||||||
|
startVelocity: 55,
|
||||||
|
origin: { x, y: 0.55 },
|
||||||
|
colors: C,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// cannoni laterali potenti dal basso
|
// cannoni laterali potenti dal basso
|
||||||
myConfetti({ particleCount: 250, angle: 60, spread: 120, startVelocity: 75, origin: { x: 0, y: 0.85 }, colors: C });
|
myConfetti({
|
||||||
myConfetti({ particleCount: 250, angle: 120, spread: 120, startVelocity: 75, origin: { x: 1, y: 0.85 }, colors: C });
|
particleCount: 250,
|
||||||
|
angle: 60,
|
||||||
|
spread: 120,
|
||||||
|
startVelocity: 75,
|
||||||
|
origin: { x: 0, y: 0.85 },
|
||||||
|
colors: C,
|
||||||
|
});
|
||||||
|
myConfetti({
|
||||||
|
particleCount: 250,
|
||||||
|
angle: 120,
|
||||||
|
spread: 120,
|
||||||
|
startVelocity: 75,
|
||||||
|
origin: { x: 1, y: 0.85 },
|
||||||
|
colors: C,
|
||||||
|
});
|
||||||
// pioggia dall'alto su tutta la larghezza
|
// pioggia dall'alto su tutta la larghezza
|
||||||
for (let x = 0; x <= 1.0001; x += 0.1) {
|
for (let x = 0; x <= 1.0001; x += 0.1) {
|
||||||
myConfetti({ particleCount: 40, spread: 160, startVelocity: 35, gravity: 1, origin: { x, y: -0.1 }, colors: C });
|
myConfetti({
|
||||||
|
particleCount: 40,
|
||||||
|
spread: 160,
|
||||||
|
startVelocity: 35,
|
||||||
|
gravity: 1,
|
||||||
|
origin: { x, y: -0.1 },
|
||||||
|
colors: C,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +296,14 @@
|
|||||||
const end = Date.now() + durationMs;
|
const end = Date.now() + durationMs;
|
||||||
(function frame() {
|
(function frame() {
|
||||||
for (let x = 0; x <= 1.0001; x += 0.12) {
|
for (let x = 0; x <= 1.0001; x += 0.12) {
|
||||||
myConfetti({ particleCount: 10, spread: 130, startVelocity: 45, gravity: 1, origin: { x, y: -0.05 }, colors: CONFETTI_COLORS });
|
myConfetti({
|
||||||
|
particleCount: 10,
|
||||||
|
spread: 130,
|
||||||
|
startVelocity: 45,
|
||||||
|
gravity: 1,
|
||||||
|
origin: { x, y: -0.05 },
|
||||||
|
colors: CONFETTI_COLORS,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (Date.now() < end) requestAnimationFrame(frame);
|
if (Date.now() < end) requestAnimationFrame(frame);
|
||||||
})();
|
})();
|
||||||
@@ -304,7 +366,13 @@
|
|||||||
setTimeout(() => stamp.remove(), 1300);
|
setTimeout(() => stamp.remove(), 1300);
|
||||||
|
|
||||||
// coriandoli "sbagliati" (solo rossi) per rincarare la beffa
|
// coriandoli "sbagliati" (solo rossi) per rincarare la beffa
|
||||||
myConfetti({ particleCount: 120, spread: 180, startVelocity: 40, origin: { y: 0.4 }, colors: ["#ff3b5c", "#8b0022"] });
|
myConfetti({
|
||||||
|
particleCount: 120,
|
||||||
|
spread: 180,
|
||||||
|
startVelocity: 40,
|
||||||
|
origin: { y: 0.4 },
|
||||||
|
colors: ["#ff3b5c", "#8b0022"],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Sfondo three.js: pioggia di monete ---------- */
|
/* ---------- Sfondo three.js: pioggia di monete ---------- */
|
||||||
@@ -313,7 +381,11 @@
|
|||||||
const canvas = $("#bg3d");
|
const canvas = $("#bg3d");
|
||||||
let renderer;
|
let renderer;
|
||||||
try {
|
try {
|
||||||
renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
renderer = new THREE.WebGLRenderer({
|
||||||
|
canvas,
|
||||||
|
antialias: true,
|
||||||
|
alpha: true,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return; // WebGL non disponibile: l'app funziona lo stesso senza sfondo 3D
|
return; // WebGL non disponibile: l'app funziona lo stesso senza sfondo 3D
|
||||||
}
|
}
|
||||||
@@ -398,35 +470,59 @@
|
|||||||
const ITEM_H = 88;
|
const ITEM_H = 88;
|
||||||
const reelEls = [$("#reel0"), $("#reel1"), $("#reel2")];
|
const reelEls = [$("#reel0"), $("#reel1"), $("#reel2")];
|
||||||
|
|
||||||
function reelItemHTML(room) {
|
// Stanze FANTASMA: scorrono nei rulli solo per gaslightare, non escono MAI
|
||||||
const jz = room.jacuzzi ? " jz" : "";
|
// come risultato (né nel match né nel no-match). Al massimo compaiono in un
|
||||||
const icon = room.jacuzzi ? '<span class="jz-badge">🛁</span>' : "";
|
// finto stop, poi scattano via.
|
||||||
return `<div class="reel-item${jz}" data-room="${room.id}">${icon}<span>${room.nome}</span></div>`;
|
const FAKE_ROOMS = [
|
||||||
|
"Piscina",
|
||||||
|
"Nel baule di una macchina",
|
||||||
|
"QG di Allianz",
|
||||||
|
"Cucina",
|
||||||
|
"Bagno",
|
||||||
|
"Garlasco",
|
||||||
|
"Predappio",
|
||||||
|
"Backrooms",
|
||||||
|
].map((nome) => ({ nome, fake: true }));
|
||||||
|
const FAKES_PER_LOOP = 3; // costante -> indice target uguale su tutti i rulli
|
||||||
|
|
||||||
|
function reelItemHTML(entry) {
|
||||||
|
if (entry.fake) {
|
||||||
|
return `<div class="reel-item fake" data-fake="1"><span>${entry.nome}</span></div>`;
|
||||||
|
}
|
||||||
|
const jz = entry.jacuzzi ? " jz" : "";
|
||||||
|
const icon = entry.jacuzzi ? '<span class="jz-badge">🛁</span>' : "";
|
||||||
|
return `<div class="reel-item${jz}" data-room="${entry.id}">${icon}<span>${entry.nome}</span></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Costruisce la striscia di un rullo che termina sulla stanza target
|
// Costruisce la striscia di un rullo che termina sulla stanza target
|
||||||
// (centrata nella riga di mezzo); ritorna l'indice target.
|
// (centrata nella riga di mezzo); ritorna l'indice target. Ogni "giro" della
|
||||||
|
// sequenza contiene le stanze reali + alcune fantasma (mischiate).
|
||||||
function buildStripIn(el, targetId) {
|
function buildStripIn(el, targetId) {
|
||||||
el.getAnimations().forEach((a) => a.cancel());
|
el.getAnimations().forEach((a) => a.cancel());
|
||||||
const loops = 14;
|
const loops = 12;
|
||||||
const seq = [];
|
const seq = [];
|
||||||
for (let l = 0; l < loops; l++) for (const r of ROOMS) seq.push(r);
|
for (let l = 0; l < loops; l++) {
|
||||||
const start = Math.floor(Math.random() * ROOMS.length); // offset diverso per rullo
|
const fakes = Matching.shuffle(FAKE_ROOMS, Math.random).slice(
|
||||||
const rotated = seq.slice(start).concat(seq.slice(0, start));
|
0,
|
||||||
const targetIndex = rotated.length; // la target va in coda
|
FAKES_PER_LOOP,
|
||||||
rotated.push(roomById[targetId]);
|
);
|
||||||
rotated.push(ROOMS[0], ROOMS[1]); // riempi sotto la finestra
|
const chunk = Matching.shuffle([...ROOMS, ...fakes], Math.random);
|
||||||
el.innerHTML = rotated.map(reelItemHTML).join("");
|
seq.push(...chunk);
|
||||||
|
}
|
||||||
|
const targetIndex = seq.length; // la target (reale) va alla payline
|
||||||
|
seq.push(roomById[targetId]);
|
||||||
|
seq.push(ROOMS[0], ROOMS[1]); // riempi sotto la finestra (stanze reali)
|
||||||
|
el.innerHTML = seq.map(reelItemHTML).join("");
|
||||||
el.style.transform = "translateY(0)";
|
el.style.transform = "translateY(0)";
|
||||||
return targetIndex;
|
return targetIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// stato a riposo: mostra i simboli fermi invece di rulli vuoti
|
// stato a riposo: mostra le stanze reali ferme invece di rulli vuoti
|
||||||
function fillReelsIdle() {
|
function fillReelsIdle() {
|
||||||
reelEls.forEach((el, i) => {
|
reelEls.forEach((el, i) => {
|
||||||
el.getAnimations().forEach((a) => a.cancel());
|
el.getAnimations().forEach((a) => a.cancel());
|
||||||
el.innerHTML = ROOMS.map(reelItemHTML).join("");
|
el.innerHTML = ROOMS.map(reelItemHTML).join("");
|
||||||
const k = i + 1; // centra un simbolo diverso per rullo
|
const k = i + 1; // centra una stanza diversa per rullo
|
||||||
el.style.transform = `translateY(${-(k - 1) * ITEM_H}px)`;
|
el.style.transform = `translateY(${-(k - 1) * ITEM_H}px)`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -438,8 +534,19 @@
|
|||||||
return Math.round(-y / ITEM_H);
|
return Math.round(-y / ITEM_H);
|
||||||
}
|
}
|
||||||
|
|
||||||
const roomAtIndex = (el, i) =>
|
// stanza reale a un indice (null se è una fantasma o non esiste)
|
||||||
el.children[i] ? roomById[el.children[i].dataset.room] : null;
|
const roomAtIndex = (el, i) => {
|
||||||
|
const c = el.children[i];
|
||||||
|
return c && c.dataset.room ? roomById[c.dataset.room] : null;
|
||||||
|
};
|
||||||
|
// nome mostrato a un indice (reale o fantasma)
|
||||||
|
const nameAtIndex = (el, i) => {
|
||||||
|
const c = el.children[i];
|
||||||
|
if (!c) return "";
|
||||||
|
return c.dataset.room
|
||||||
|
? roomById[c.dataset.room].nome
|
||||||
|
: c.textContent.trim();
|
||||||
|
};
|
||||||
|
|
||||||
// Probabilità di ragebait (finto stop) per colonna.
|
// Probabilità di ragebait (finto stop) per colonna.
|
||||||
const TEASE_CHANCE = 0.3;
|
const TEASE_CHANCE = 0.3;
|
||||||
@@ -448,7 +555,10 @@
|
|||||||
|
|
||||||
// 3 stanze DIVERSE per un "nessun match" (mai una linea vincente)
|
// 3 stanze DIVERSE per un "nessun match" (mai una linea vincente)
|
||||||
function pickMismatch() {
|
function pickMismatch() {
|
||||||
return Matching.shuffle(ROOMS.map((r) => r.id), Math.random).slice(0, 3);
|
return Matching.shuffle(
|
||||||
|
ROOMS.map((r) => r.id),
|
||||||
|
Math.random,
|
||||||
|
).slice(0, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
function bounceReel(el, finalY) {
|
function bounceReel(el, finalY) {
|
||||||
@@ -458,7 +568,7 @@
|
|||||||
{ transform: `translateY(${finalY + 12}px)` },
|
{ transform: `translateY(${finalY + 12}px)` },
|
||||||
{ transform: `translateY(${finalY}px)` },
|
{ transform: `translateY(${finalY}px)` },
|
||||||
],
|
],
|
||||||
{ duration: 220, easing: "ease-out", fill: "forwards" }
|
{ duration: 220, easing: "ease-out", fill: "forwards" },
|
||||||
).finished;
|
).finished;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,35 +576,53 @@
|
|||||||
// si ferma su una stanza adiacente sbagliata, tiene la suspense (teaseMs),
|
// si ferma su una stanza adiacente sbagliata, tiene la suspense (teaseMs),
|
||||||
// poi scatta sulla vera. Funziona su QUALSIASI colonna.
|
// poi scatta sulla vera. Funziona su QUALSIASI colonna.
|
||||||
async function reelLand(el, targetIndex, finalY, duration, teaseMs) {
|
async function reelLand(el, targetIndex, finalY, duration, teaseMs) {
|
||||||
// eventuale bluff: scegli una stanza adiacente diversa dalla vera
|
// eventuale bluff: fermati su una casella adiacente diversa dalla vera
|
||||||
|
// (può essere anche una stanza FANTASMA -> gaslighting massimo)
|
||||||
if (teaseMs > 0) {
|
if (teaseMs > 0) {
|
||||||
const targetRoom = roomAtIndex(el, targetIndex);
|
const targetRoom = roomAtIndex(el, targetIndex);
|
||||||
|
// true se l'adiacente è proprio la stanza vera (da evitare come finta)
|
||||||
|
const adjIsTarget = (d) =>
|
||||||
|
roomAtIndex(el, targetIndex + d)?.id === targetRoom.id;
|
||||||
let dir = Math.random() < 0.5 ? -1 : 1;
|
let dir = Math.random() < 0.5 ? -1 : 1;
|
||||||
if (roomAtIndex(el, targetIndex + dir)?.id === targetRoom.id) dir = -dir;
|
if (adjIsTarget(dir)) dir = -dir;
|
||||||
if (roomAtIndex(el, targetIndex + dir)?.id === targetRoom.id) teaseMs = 0; // no adiacente
|
if (adjIsTarget(dir)) teaseMs = 0; // entrambe le adiacenti sono la target
|
||||||
if (teaseMs > 0) {
|
if (teaseMs > 0) {
|
||||||
const fakeRoom = roomAtIndex(el, targetIndex + dir);
|
const fakeName = nameAtIndex(el, targetIndex + dir);
|
||||||
const fakeY = -(targetIndex + dir - 1) * ITEM_H;
|
const fakeY = -(targetIndex + dir - 1) * ITEM_H;
|
||||||
const col = el.parentElement;
|
const col = el.parentElement;
|
||||||
|
|
||||||
// decelera e "si ferma" sulla stanza SBAGLIATA
|
// decelera e "si ferma" sulla stanza SBAGLIATA
|
||||||
await el.animate(
|
await el.animate(
|
||||||
[{ transform: "translateY(0)" }, { transform: `translateY(${fakeY}px)` }],
|
[
|
||||||
{ duration, easing: "cubic-bezier(0.08, 0.85, 0.14, 1)", fill: "forwards" }
|
{ transform: "translateY(0)" },
|
||||||
|
{ transform: `translateY(${fakeY}px)` },
|
||||||
|
],
|
||||||
|
{
|
||||||
|
duration,
|
||||||
|
easing: "cubic-bezier(0.05, 0.92, 0.1, 1)",
|
||||||
|
fill: "forwards",
|
||||||
|
},
|
||||||
).finished;
|
).finished;
|
||||||
Audio.reelStop();
|
Audio.reelStop();
|
||||||
|
|
||||||
// suspense su quella colonna
|
// suspense su quella colonna
|
||||||
Audio.blip(330, 0.13, "sine", 0.14);
|
Audio.blip(330, 0.13, "sine", 0.14);
|
||||||
col.classList.add("teasing");
|
col.classList.add("teasing");
|
||||||
mc(`${fakeRoom.nome}...? 🤨`);
|
mc(`${fakeName}...? 🤨`);
|
||||||
await sleep(teaseMs);
|
await sleep(teaseMs);
|
||||||
|
|
||||||
// SCATTO alla stanza vera
|
// SCATTO alla stanza vera
|
||||||
Audio.blip(200, 0.12, "sawtooth", 0.2, 90); // whoosh
|
Audio.blip(200, 0.12, "sawtooth", 0.2, 90); // whoosh
|
||||||
await el.animate(
|
await el.animate(
|
||||||
[{ transform: `translateY(${fakeY}px)` }, { transform: `translateY(${finalY}px)` }],
|
[
|
||||||
{ duration: 480, easing: "cubic-bezier(0.34, 1.56, 0.64, 1)", fill: "forwards" }
|
{ transform: `translateY(${fakeY}px)` },
|
||||||
|
{ transform: `translateY(${finalY}px)` },
|
||||||
|
],
|
||||||
|
{
|
||||||
|
duration: 480,
|
||||||
|
easing: "cubic-bezier(0.34, 1.56, 0.64, 1)",
|
||||||
|
fill: "forwards",
|
||||||
|
},
|
||||||
).finished;
|
).finished;
|
||||||
col.classList.remove("teasing");
|
col.classList.remove("teasing");
|
||||||
Audio.reelStop();
|
Audio.reelStop();
|
||||||
@@ -503,8 +631,15 @@
|
|||||||
}
|
}
|
||||||
// atterraggio diretto + micro-assestamento
|
// atterraggio diretto + micro-assestamento
|
||||||
await el.animate(
|
await el.animate(
|
||||||
[{ transform: "translateY(0)" }, { transform: `translateY(${finalY}px)` }],
|
[
|
||||||
{ duration, easing: "cubic-bezier(0.12, 0.85, 0.18, 1)", fill: "forwards" }
|
{ transform: "translateY(0)" },
|
||||||
|
{ transform: `translateY(${finalY}px)` },
|
||||||
|
],
|
||||||
|
{
|
||||||
|
duration,
|
||||||
|
easing: "cubic-bezier(0.05, 0.92, 0.1, 1)",
|
||||||
|
fill: "forwards",
|
||||||
|
},
|
||||||
).finished;
|
).finished;
|
||||||
await bounceReel(el, finalY);
|
await bounceReel(el, finalY);
|
||||||
Audio.reelStop();
|
Audio.reelStop();
|
||||||
@@ -517,16 +652,19 @@
|
|||||||
async function spinReel(reelTargets, opts) {
|
async function spinReel(reelTargets, opts) {
|
||||||
const tease = !opts || opts.tease !== false;
|
const tease = !opts || opts.tease !== false;
|
||||||
// ogni rullo può atterrare su una stanza diversa (per il "nessun match")
|
// ogni rullo può atterrare su una stanza diversa (per il "nessun match")
|
||||||
const targetIndex = reelEls.map((el, i) => buildStripIn(el, reelTargets[i]))[0];
|
const targetIndex = reelEls.map((el, i) =>
|
||||||
|
buildStripIn(el, reelTargets[i]),
|
||||||
|
)[0];
|
||||||
const finalY = -(targetIndex - 1) * ITEM_H;
|
const finalY = -(targetIndex - 1) * ITEM_H;
|
||||||
$("#reel-result").innerHTML = " ";
|
$("#reel-result").innerHTML = " ";
|
||||||
|
|
||||||
// ZOOM + shake; lo shake si ferma a metà spin (quando inizia a rallentare)
|
// ZOOM + shake; lo shake si ferma presto, così la parte lenta è calma e
|
||||||
|
// leggibile mentre i nomi scorrono piano
|
||||||
zoomMachineIn();
|
zoomMachineIn();
|
||||||
const mainDur = 4200;
|
const mainDur = 6400;
|
||||||
setTimeout(zoomMachineSteady, mainDur / 2);
|
setTimeout(zoomMachineSteady, 2500);
|
||||||
|
|
||||||
const stopRiser = Audio.riser(5.6);
|
const stopRiser = Audio.riser(7.0);
|
||||||
// whirr reale dei rulli sotto ai tick/riser sintetizzati
|
// whirr reale dei rulli sotto ai tick/riser sintetizzati
|
||||||
const stopWheel = Samples.play("reelSpin", { vol: 0.65 });
|
const stopWheel = Samples.play("reelSpin", { vol: 0.65 });
|
||||||
|
|
||||||
@@ -543,13 +681,14 @@
|
|||||||
requestAnimationFrame(loop);
|
requestAnimationFrame(loop);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const durs = [2600, 3300, 4200];
|
const durs = [4200, 5300, 6400];
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
reelEls.map((el, i) => {
|
reelEls.map((el, i) => {
|
||||||
// ogni colonna ha una sua chance di bluff; suspense crescente verso destra
|
// ogni colonna ha una sua chance di bluff; suspense crescente verso destra
|
||||||
const teaseMs = tease && Math.random() < TEASE_CHANCE ? 380 + i * 260 : 0;
|
const teaseMs =
|
||||||
|
tease && Math.random() < TEASE_CHANCE ? 380 + i * 260 : 0;
|
||||||
return reelLand(el, targetIndex, finalY, durs[i], teaseMs);
|
return reelLand(el, targetIndex, finalY, durs[i], teaseMs);
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
ticking = false;
|
ticking = false;
|
||||||
@@ -583,7 +722,7 @@
|
|||||||
{ transform: "scale(0.985)" },
|
{ transform: "scale(0.985)" },
|
||||||
{ transform: "scale(1)" },
|
{ transform: "scale(1)" },
|
||||||
],
|
],
|
||||||
{ duration: 620, easing: "cubic-bezier(0.34, 1.56, 0.64, 1)" }
|
{ duration: 620, easing: "cubic-bezier(0.34, 1.56, 0.64, 1)" },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,7 +749,8 @@
|
|||||||
// svelati per prestigio crescente: le stanze con jacuzzi (jackpot) per ultime
|
// svelati per prestigio crescente: le stanze con jacuzzi (jackpot) per ultime
|
||||||
const people = PARTICIPANTS.map((p) => p.nome);
|
const people = PARTICIPANTS.map((p) => p.nome);
|
||||||
return Matching.shuffle(people, Math.random).sort(
|
return Matching.shuffle(people, Math.random).sort(
|
||||||
(a, b) => prestige(assignment.byPerson[a]) - prestige(assignment.byPerson[b])
|
(a, b) =>
|
||||||
|
prestige(assignment.byPerson[a]) - prestige(assignment.byPerson[b]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,7 +790,9 @@
|
|||||||
board.innerHTML = ROOMS.map((r) => {
|
board.innerHTML = ROOMS.map((r) => {
|
||||||
const g = assignment.roomGender[r.id];
|
const g = assignment.roomGender[r.id];
|
||||||
const occ = occupants[r.id];
|
const occ = occupants[r.id];
|
||||||
const chips = occ.map((n) => `<span class="occ-chip">${n}</span>`).join("");
|
const chips = occ
|
||||||
|
.map((n) => `<span class="occ-chip">${n}</span>`)
|
||||||
|
.join("");
|
||||||
const emptyCount = r.posti - occ.length;
|
const emptyCount = r.posti - occ.length;
|
||||||
const empties = Array.from({ length: emptyCount })
|
const empties = Array.from({ length: emptyCount })
|
||||||
.map(() => `<span class="occ-slot-empty"></span>`)
|
.map(() => `<span class="occ-slot-empty"></span>`)
|
||||||
@@ -671,7 +813,7 @@
|
|||||||
$("#leaderboard").innerHTML = sorted
|
$("#leaderboard").innerHTML = sorted
|
||||||
.map(
|
.map(
|
||||||
(p) =>
|
(p) =>
|
||||||
`<div class="lb-row ${p.you ? "you" : ""}"><span>${p.you ? "⭐ " : ""}${p.name}</span><span class="lb-chips">${p.chips}</span></div>`
|
`<div class="lb-row ${p.you ? "you" : ""}"><span>${p.you ? "⭐ " : ""}${p.name}</span><span class="lb-chips">${p.chips}</span></div>`,
|
||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
$("#balance").textContent = players.find((p) => p.you).chips;
|
$("#balance").textContent = players.find((p) => p.you).chips;
|
||||||
@@ -697,9 +839,13 @@
|
|||||||
function computeOdds(person) {
|
function computeOdds(person) {
|
||||||
const g = genderOf[person];
|
const g = genderOf[person];
|
||||||
const rooms = ROOMS.filter(
|
const rooms = ROOMS.filter(
|
||||||
(r) => assignment.roomGender[r.id] === g && occupants[r.id].length < r.posti
|
(r) =>
|
||||||
|
assignment.roomGender[r.id] === g && occupants[r.id].length < r.posti,
|
||||||
|
);
|
||||||
|
const freeTotal = rooms.reduce(
|
||||||
|
(s, r) => s + (r.posti - occupants[r.id].length),
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
const freeTotal = rooms.reduce((s, r) => s + (r.posti - occupants[r.id].length), 0);
|
|
||||||
return rooms.map((r) => {
|
return rooms.map((r) => {
|
||||||
const free = r.posti - occupants[r.id].length;
|
const free = r.posti - occupants[r.id].length;
|
||||||
let mult = freeTotal / free;
|
let mult = freeTotal / free;
|
||||||
@@ -720,7 +866,7 @@
|
|||||||
`<div class="odd-row ${currentBet && currentBet.roomId === o.room.id ? "picked" : ""}" data-bet="${o.room.id}">
|
`<div class="odd-row ${currentBet && currentBet.roomId === o.room.id ? "picked" : ""}" data-bet="${o.room.id}">
|
||||||
<span class="odd-name">${o.room.jacuzzi ? "🛁 " : ""}${o.room.nome}</span>
|
<span class="odd-name">${o.room.jacuzzi ? "🛁 " : ""}${o.room.nome}</span>
|
||||||
<span class="odd-mult">x${o.mult.toFixed(1)}</span>
|
<span class="odd-mult">x${o.mult.toFixed(1)}</span>
|
||||||
</div>`
|
</div>`,
|
||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
$("#odds")
|
$("#odds")
|
||||||
@@ -732,7 +878,7 @@
|
|||||||
currentBet = { roomId: rid };
|
currentBet = { roomId: rid };
|
||||||
Audio.coin();
|
Audio.coin();
|
||||||
renderOdds(person, odds);
|
renderOdds(person, odds);
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -761,7 +907,10 @@
|
|||||||
if (currentBet.roomId === landedRoomId) {
|
if (currentBet.roomId === landedRoomId) {
|
||||||
const win = Math.round(10 * o);
|
const win = Math.round(10 * o);
|
||||||
you.chips += win - 10;
|
you.chips += win - 10;
|
||||||
log(`Hai vinto ${win} gettoni! (${roomById[landedRoomId].nome})`, "win");
|
log(
|
||||||
|
`Hai vinto ${win} gettoni! (${roomById[landedRoomId].nome})`,
|
||||||
|
"win",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
you.chips -= 10;
|
you.chips -= 10;
|
||||||
log(`Hai perso la puntata su ${roomById[currentBet.roomId].nome}.`);
|
log(`Hai perso la puntata su ${roomById[currentBet.roomId].nome}.`);
|
||||||
@@ -771,7 +920,8 @@
|
|||||||
players
|
players
|
||||||
.filter((p) => !p.you && p._bet)
|
.filter((p) => !p.you && p._bet)
|
||||||
.forEach((p) => {
|
.forEach((p) => {
|
||||||
if (p._bet.roomId === landedRoomId) p.chips += Math.round(10 * p._bet.mult) - 10;
|
if (p._bet.roomId === landedRoomId)
|
||||||
|
p.chips += Math.round(10 * p._bet.mult) - 10;
|
||||||
else p.chips -= 10;
|
else p.chips -= 10;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -838,7 +988,8 @@
|
|||||||
// --- MATCH: i 3 rulli si allineano sulla stanza assegnata ---
|
// --- MATCH: i 3 rulli si allineano sulla stanza assegnata ---
|
||||||
noMatchStreak = 0;
|
noMatchStreak = 0;
|
||||||
await spinReel([roomId, roomId, roomId], { tease: true });
|
await spinReel([roomId, roomId, roomId], { tease: true });
|
||||||
$("#reel-result").innerHTML = `${room.jacuzzi ? "🛁 " : ""}<b>${room.nome}</b>${room.jacuzzi ? " 🛁" : ""}`;
|
$("#reel-result").innerHTML =
|
||||||
|
`${room.jacuzzi ? "🛁 " : ""}<b>${room.nome}</b>${room.jacuzzi ? " 🛁" : ""}`;
|
||||||
|
|
||||||
// atterraggio
|
// atterraggio
|
||||||
occupants[roomId].push(person);
|
occupants[roomId].push(person);
|
||||||
@@ -853,7 +1004,13 @@
|
|||||||
Audio.ding(); // "click" di aggancio della stanza
|
Audio.ding(); // "click" di aggancio della stanza
|
||||||
if (isJackpot) {
|
if (isJackpot) {
|
||||||
// sirena reale di jackpot; fallback: fanfara + jackpot sintetizzati
|
// sirena reale di jackpot; fallback: fanfara + jackpot sintetizzati
|
||||||
Samples.play("jackpot", { vol: 0.9, fallback: () => { Audio.fanfare(); Audio.jackpot(); } });
|
Samples.play("jackpot", {
|
||||||
|
vol: 0.9,
|
||||||
|
fallback: () => {
|
||||||
|
Audio.fanfare();
|
||||||
|
Audio.jackpot();
|
||||||
|
},
|
||||||
|
});
|
||||||
celebrate("jackpot");
|
celebrate("jackpot");
|
||||||
screenShake();
|
screenShake();
|
||||||
screenFlash();
|
screenFlash();
|
||||||
@@ -892,7 +1049,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function highlightRoom(roomId) {
|
function highlightRoom(roomId) {
|
||||||
document.querySelectorAll(".room-card").forEach((c) => c.classList.remove("hot"));
|
document
|
||||||
|
.querySelectorAll(".room-card")
|
||||||
|
.forEach((c) => c.classList.remove("hot"));
|
||||||
if (roomId) {
|
if (roomId) {
|
||||||
const el = document.querySelector(`.room-card[data-room="${roomId}"]`);
|
const el = document.querySelector(`.room-card[data-room="${roomId}"]`);
|
||||||
if (el) el.classList.add("hot");
|
if (el) el.classList.add("hot");
|
||||||
@@ -903,8 +1062,13 @@
|
|||||||
armLever(false);
|
armLever(false);
|
||||||
const winner = players.slice().sort((a, b) => b.chips - a.chips)[0];
|
const winner = players.slice().sort((a, b) => b.chips - a.chips)[0];
|
||||||
const you = players.find((p) => p.you);
|
const you = players.find((p) => p.you);
|
||||||
mc(`🏁 Tutti sistemati! Vincitore scommesse: ${winner.name} (${winner.chips} gettoni).`);
|
mc(
|
||||||
log(`Fine. I tuoi gettoni: ${you.chips}. Miglior scommettitore: ${winner.name}.`, "win");
|
`🏁 Tutti sistemati! Vincitore scommesse: ${winner.name} (${winner.chips} gettoni).`,
|
||||||
|
);
|
||||||
|
log(
|
||||||
|
`Fine. I tuoi gettoni: ${you.chips}. Miglior scommettitore: ${winner.name}.`,
|
||||||
|
"win",
|
||||||
|
);
|
||||||
Audio.fanfare();
|
Audio.fanfare();
|
||||||
celebrate("jackpot");
|
celebrate("jackpot");
|
||||||
$("#btn-primary").style.display = "";
|
$("#btn-primary").style.display = "";
|
||||||
@@ -919,7 +1083,10 @@
|
|||||||
function initMarquee() {
|
function initMarquee() {
|
||||||
const m = $("#marquee");
|
const m = $("#marquee");
|
||||||
m.innerHTML = Array.from({ length: 16 })
|
m.innerHTML = Array.from({ length: 16 })
|
||||||
.map((_, i) => `<span class="bulb" style="animation-delay:${(i % 8) * 0.12}s"></span>`)
|
.map(
|
||||||
|
(_, i) =>
|
||||||
|
`<span class="bulb" style="animation-delay:${(i % 8) * 0.12}s"></span>`,
|
||||||
|
)
|
||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1011,7 +1178,7 @@
|
|||||||
Audio.init();
|
Audio.init();
|
||||||
Audio.resume();
|
Audio.resume();
|
||||||
},
|
},
|
||||||
{ once: true }
|
{ once: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -365,6 +365,7 @@ body {
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
/* le stanze fantasma sono identiche alle reali (indistinguibili) */
|
||||||
/* caption con il nome della stanza risultante */
|
/* caption con il nome della stanza risultante */
|
||||||
.reel-result {
|
.reel-result {
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
|
|||||||
Reference in New Issue
Block a user