diff --git a/README.md b/README.md
index 2b39b82..34b8ece 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,10 @@ python3 -m http.server 8080
I posti con **jacuzzi** (Argo, Eolo, "Quanto te la rischi") sono i jackpot e
vengono rivelati per ultimi, con celebrazione extra.
+**Nessun match (~30%):** ogni tanto i 3 rulli si fermano su stanze diverse →
+effetto pazzo (sirena + strobo + glitch + timbro "NO MATCH"), le puntate del giro
+vengono annullate e la persona **ri-tira** (senza avanzare). Max 2 no-match di fila.
+
## Regole del matching
- Ogni stanza è **mono-genere** (M con M, F con F).
diff --git a/app.js b/app.js
index 5690c99..50809bd 100644
--- a/app.js
+++ b/app.js
@@ -55,6 +55,22 @@
buzz() {
this.blip(140, 0.45, "sawtooth", 0.3, 70);
},
+ // sirena da allarme per il "nessun match"
+ siren() {
+ if (!this.on || !this.ctx) return;
+ const t = this.ctx.currentTime;
+ const o = this.ctx.createOscillator();
+ const g = this.ctx.createGain();
+ o.type = "sawtooth";
+ const freqs = [880, 440, 880, 440, 880, 440, 880];
+ freqs.forEach((f, i) => o.frequency.setValueAtTime(f, t + i * 0.16));
+ g.gain.setValueAtTime(0.16, t);
+ g.gain.setValueAtTime(0.16, t + 1.05);
+ g.gain.exponentialRampToValueAtTime(0.0001, t + 1.2);
+ o.connect(g).connect(this.master);
+ o.start(t);
+ o.stop(t + 1.25);
+ },
// nota di ottone per la fanfara
brass(freq, when, dur, vol = 0.25) {
if (!this.on || !this.ctx) return;
@@ -198,6 +214,38 @@
f.classList.add("go");
}
+ // EFFETTO PAZZO quando i rulli non combaciano: sirena + strobo + glitch + timbro
+ function crazyNoMatch() {
+ Audio.siren();
+ screenShake();
+
+ // strobo a schermo intero
+ let strobe = $(".strobe");
+ if (!strobe) {
+ strobe = document.createElement("div");
+ strobe.className = "strobe";
+ document.body.appendChild(strobe);
+ }
+ strobe.classList.remove("go");
+ void strobe.offsetWidth;
+ strobe.classList.add("go");
+
+ // glitch sui rulli
+ const win = $(".reel-window");
+ win.classList.add("glitch");
+ setTimeout(() => win.classList.remove("glitch"), 900);
+
+ // timbro "NESSUN MATCH"
+ const stamp = document.createElement("div");
+ stamp.className = "nomatch-stamp";
+ stamp.innerHTML = "❌ NO MATCH ❌";
+ document.body.appendChild(stamp);
+ setTimeout(() => stamp.remove(), 1300);
+
+ // coriandoli "sbagliati" (solo rossi) per rincarare la beffa
+ myConfetti({ particleCount: 120, spread: 180, startVelocity: 40, origin: { y: 0.4 }, colors: ["#ff3b5c", "#8b0022"] });
+ }
+
/* ---------- Sfondo three.js: pioggia di monete ---------- */
function initBackground() {
if (typeof THREE === "undefined") return;
@@ -332,8 +380,15 @@
const roomAtIndex = (el, i) =>
el.children[i] ? roomById[el.children[i].dataset.room] : null;
- // Probabilità di ragebait (finto stop) sul rullo finale.
+ // Probabilità di ragebait (finto stop) per colonna.
const TEASE_CHANCE = 0.3;
+ // Probabilità che il giro NON dia match (3 stanze diverse) -> respin.
+ const NOMATCH_CHANCE = 0.3;
+
+ // 3 stanze DIVERSE per un "nessun match" (mai una linea vincente)
+ function pickMismatch() {
+ return Matching.shuffle(ROOMS.map((r) => r.id), Math.random).slice(0, 3);
+ }
function bounceReel(el, finalY) {
return el.animate(
@@ -398,10 +453,11 @@
// allineandosi sulla stanza assegnata. OGNI colonna può fare il ragebait
// (in modo indipendente); le colonne più a destra tengono la suspense più a
// lungo, così il dramma "cascata" verso l'ultimo rullo.
- async function spinReel(targetId) {
- const targetIndex = reelEls.map((el) => buildStripIn(el, targetId))[0]; // uguale per tutti
+ async function spinReel(reelTargets, opts) {
+ const tease = !opts || opts.tease !== false;
+ // ogni rullo può atterrare su una stanza diversa (per il "nessun match")
+ const targetIndex = reelEls.map((el, i) => buildStripIn(el, reelTargets[i]))[0];
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)
@@ -428,7 +484,7 @@
await Promise.all(
reelEls.map((el, i) => {
// ogni colonna ha una sua chance di bluff; suspense crescente verso destra
- const teaseMs = 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);
})
);
@@ -436,7 +492,6 @@
ticking = false;
stopRiser();
zoomMachineSteady();
- $("#reel-result").innerHTML = `${targetRoom.jacuzzi ? "🛁 " : ""}${targetRoom.nome}${targetRoom.jacuzzi ? " 🛁" : ""}`;
}
/* ---------- Zoom macchina ---------- */
@@ -476,6 +531,7 @@
let auto = false;
let busy = false;
let leverArmed = false;
+ let noMatchStreak = 0; // no-match consecutivi (tetto per non incastrare)
// scommettitori (TU + banco simulato)
let players = [];
@@ -501,6 +557,7 @@
ROOMS.forEach((r) => (occupants[r.id] = []));
cursor = 0;
currentBet = null;
+ noMatchStreak = 0;
if (!keepChips) {
players = [
{ name: "TU", chips: 100, you: true },
@@ -686,7 +743,38 @@
setTimeout(() => $("#lever").classList.remove("pull"), 320);
if (window.__coinBoost) window.__coinBoost();
- await spinReel(roomId);
+ // 30% dei giri: NESSUN MATCH (3 stanze diverse) -> effetto pazzo + respin.
+ // Tetto di 2 no-match di fila per non bloccare nessuno.
+ const noMatch = noMatchStreak < 2 && Math.random() < NOMATCH_CHANCE;
+
+ if (noMatch) {
+ noMatchStreak++;
+ await spinReel(pickMismatch(), { tease: false });
+ $("#reel-result").innerHTML = `NESSUN MATCH`;
+ crazyNoMatch();
+ currentBet = null; // puntate annullate (push): nessun gettone cambia
+ log(`${person}: nessun match! Si ri-tira 💥`);
+ mc("💥 NESSUN MATCH! Ri-tira la leva! ⬇️");
+ setTimeout(zoomMachineOut, 1000);
+ busy = false;
+ // stesso giocatore: NON avanza il cursore
+ if (auto) {
+ setTimeout(nextTurn, 1600);
+ } else {
+ setTimeout(() => {
+ if (!busy && cursor < revealQueue.length && !auto) {
+ armLever(true);
+ mc("💥 NESSUN MATCH! Ri-tira la leva! ⬇️");
+ }
+ }, 1100);
+ }
+ return;
+ }
+
+ // --- MATCH: i 3 rulli si allineano sulla stanza assegnata ---
+ noMatchStreak = 0;
+ await spinReel([roomId, roomId, roomId], { tease: true });
+ $("#reel-result").innerHTML = `${room.jacuzzi ? "🛁 " : ""}${room.nome}${room.jacuzzi ? " 🛁" : ""}`;
// atterraggio
occupants[roomId].push(person);
diff --git a/styles.css b/styles.css
index 018ef3b..5b9c551 100644
--- a/styles.css
+++ b/styles.css
@@ -668,6 +668,68 @@ body {
}
}
+/* ===== NESSUN MATCH: effetto pazzo ===== */
+.strobe {
+ position: fixed;
+ inset: 0;
+ z-index: 58;
+ pointer-events: none;
+ opacity: 0;
+}
+.strobe.go {
+ animation: strobe 1s linear;
+}
+@keyframes strobe {
+ 0%, 100% { opacity: 0; }
+ 8% { opacity: 1; background: rgba(255, 0, 60, 0.45); }
+ 16% { opacity: 1; background: rgba(0, 229, 255, 0.4); }
+ 24% { opacity: 1; background: rgba(255, 210, 63, 0.4); }
+ 40% { opacity: 1; background: rgba(255, 0, 60, 0.4); }
+ 56% { opacity: 1; background: rgba(0, 229, 255, 0.38); }
+ 72% { opacity: 1; background: rgba(255, 46, 136, 0.4); }
+ 88% { opacity: 1; background: rgba(255, 0, 60, 0.35); }
+}
+.reel-window.glitch {
+ animation: glitch 0.08s steps(2) infinite;
+}
+@keyframes glitch {
+ 0% { transform: translate(0, 0) skewX(0); filter: hue-rotate(0deg); }
+ 25% { transform: translate(-7px, 3px) skewX(4deg); filter: hue-rotate(90deg); }
+ 50% { transform: translate(6px, -4px) skewX(-4deg); filter: hue-rotate(200deg); }
+ 75% { transform: translate(-5px, 5px) skewX(3deg); filter: hue-rotate(300deg); }
+ 100% { transform: translate(0, 0) skewX(0); filter: hue-rotate(0deg); }
+}
+.nomatch-stamp {
+ position: fixed;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 59;
+ pointer-events: none;
+}
+.nomatch-stamp span {
+ font-family: "Anton", sans-serif;
+ font-size: clamp(44px, 10vw, 140px);
+ letter-spacing: 3px;
+ color: var(--red);
+ text-shadow: 0 0 30px var(--red), 0 0 70px #ff004c;
+ transform: rotate(-8deg) scale(0.3);
+ opacity: 0;
+ animation: stampIn 1.3s ease;
+}
+@keyframes stampIn {
+ 0% { opacity: 0; transform: rotate(-8deg) scale(0.3); }
+ 15% { opacity: 1; transform: rotate(-8deg) scale(1.15); }
+ 28% { transform: rotate(-8deg) scale(1); }
+ 82% { opacity: 1; transform: rotate(-8deg) scale(1); }
+ 100% { opacity: 0; transform: rotate(-8deg) scale(1.05); }
+}
+.rr-nomatch {
+ color: var(--red);
+ text-shadow: 0 0 16px var(--red);
+}
+
@media (max-width: 980px) {
.stage {
grid-template-columns: 1fr;