diff --git a/README.md b/README.md
index 62ce581..244bbf2 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,12 @@ 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.
+**Effetto SHALOM:** vincere una stanza con idromassaggio (Argo, Eolo, "Quanto te
+la rischi") fa partire un effetto a caso tra 4 — fiammate + leone infuocato, video
+a schermo intero con `shalom.mp3`, il combo (flashbang di fuoco poi video), oppure
+Gesù che scende dall'alto con luce evangelica e `gesu_song.mp3`. Tap per saltare.
+Asset in `image/`, `video/`, `sounds/`.
+
**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.
diff --git a/app.js b/app.js
index 1503eae..8dba834 100644
--- a/app.js
+++ b/app.js
@@ -2,6 +2,17 @@
(function () {
"use strict";
+ /* ===== DEBUG / ANTEPRIMA EFFETTI =====================================
+ Per vedere gli effetti senza aspettare le vincite jacuzzi casuali.
+ Rimetti entrambi a null/false per il gioco normale. */
+ // Forza un effetto shalom fisso: "fire" | "video" | "combo" | "gesu" | null
+ const FORCE_SHALOM = null;
+ // Forza il risultato del giro su una stanza con idromassaggio (così l'effetto
+ // parte a ogni giro). true = jacuzzi a caso; oppure l'id di una stanza
+ // ("argo" | "eolo" | "rischio" | "zeus" | ...); false = risultato normale.
+ const FORCE_RESULT = false;
+ /* ==================================================================== */
+
const $ = (sel) => document.querySelector(sel);
const roomById = Object.fromEntries(ROOMS.map((r) => [r.id, r]));
const genderOf = Object.fromEntries(
@@ -161,6 +172,8 @@
win: "sounds/win.mp3", // jingle di vincita (match normale)
jackpot: "sounds/jackpot.mp3", // sirena di jackpot (idromassaggio)
coins: "sounds/coins.mp3", // pioggia di monete
+ shalom: "sounds/shalom.mp3", // musica dell'effetto "shalom" (vittoria jacuzzi)
+ gesuSong: "sounds/gesu_song.mp3", // canzone dell'effetto "Gesù"
},
el: {},
ready: {},
@@ -375,6 +388,146 @@
});
}
+ /* ---------- Effetto SHALOM (vittoria stanze con idromassaggio) ---------- */
+ let shalomOverlay = null;
+ function ensureShalomOverlay() {
+ if (shalomOverlay) return shalomOverlay;
+ const o = document.createElement("div");
+ o.className = "shalom-overlay";
+ o.innerHTML =
+ '
' +
+ '' +
+ '
' +
+ '' +
+ '
' +
+ '' +
+ 'tap per saltare ▸
';
+ document.body.appendChild(o);
+ shalomOverlay = o;
+ return o;
+ }
+ function showShalom(o, mode) {
+ o.classList.remove("mode-fire", "mode-video");
+ o.classList.add("on", mode);
+ }
+ function hideShalom(o) {
+ const v = o.querySelector(".shalom-video");
+ try {
+ v.pause();
+ } catch (e) {}
+ o.classList.remove("on", "mode-fire", "mode-video");
+ }
+ // Attende: fine video, tetto di tempo, oppure tap sull'overlay (skip).
+ function shalomWait(v, capMs, o) {
+ return new Promise((resolve) => {
+ let done = false;
+ const finish = () => {
+ if (done) return;
+ done = true;
+ clearTimeout(to);
+ if (v) v.removeEventListener("ended", finish);
+ o.removeEventListener("pointerdown", finish);
+ resolve();
+ };
+ const to = setTimeout(finish, capMs);
+ if (v) v.addEventListener("ended", finish);
+ o.addEventListener("pointerdown", finish);
+ });
+ }
+
+ // EFFETTO 1: fiammate dai due angoli in basso + leone (flashbang) ~3.5s
+ async function shalomFireLion() {
+ const o = ensureShalomOverlay();
+ showShalom(o, "mode-fire");
+ screenShake();
+ const stop = Samples.play("shalom", {
+ vol: 0.9,
+ fallback: () => Audio.jackpot(),
+ });
+ await shalomWait(null, 3500, o);
+ if (stop) stop(0.4);
+ hideShalom(o);
+ }
+
+ // EFFETTO 2: video a schermo intero con la musica shalom
+ async function shalomVideoOnly() {
+ const o = ensureShalomOverlay();
+ const v = o.querySelector(".shalom-video");
+ showShalom(o, "mode-video");
+ const stop = Samples.play("shalom", {
+ vol: 0.9,
+ fallback: () => Audio.jackpot(),
+ });
+ try {
+ v.currentTime = 0;
+ await v.play();
+ } catch (e) {}
+ await shalomWait(v, 14000, o);
+ if (stop) stop(0.4);
+ hideShalom(o);
+ }
+
+ // EFFETTO 3: combo — flashbang fuoco+leone 3s, poi parte il video con la musica
+ async function shalomCombo() {
+ const o = ensureShalomOverlay();
+ showShalom(o, "mode-fire");
+ screenShake();
+ await shalomWait(null, 3000, o);
+ const v = o.querySelector(".shalom-video");
+ o.classList.remove("mode-fire");
+ o.classList.add("mode-video");
+ const stop = Samples.play("shalom", {
+ vol: 0.9,
+ fallback: () => Audio.jackpot(),
+ });
+ try {
+ v.currentTime = 0;
+ await v.play();
+ } catch (e) {}
+ await shalomWait(v, 14000, o);
+ if (stop) stop(0.4);
+ hideShalom(o);
+ }
+
+ // EFFETTO 4: Gesù scende dall'alto con luce evangelica + canzone
+ async function shalomGesu() {
+ const o = ensureShalomOverlay();
+ showShalom(o, "mode-gesu");
+ const stop = Samples.play("gesuSong", {
+ vol: 0.9,
+ fallback: () => Audio.fanfare(),
+ });
+ await shalomWait(null, 14000, o);
+ if (stop) stop(0.5);
+ hideShalom(o);
+ }
+
+ const SHALOM_EFFECTS = [
+ shalomFireLion,
+ shalomVideoOnly,
+ shalomCombo,
+ shalomGesu,
+ ];
+ const SHALOM_BY_NAME = {
+ fire: shalomFireLion,
+ video: shalomVideoOnly,
+ combo: shalomCombo,
+ gesu: shalomGesu,
+ };
+ // "sacchetto" mescolato: a ogni vittoria jacuzzi esce un effetto DIVERSO
+ // (senza ripetizioni) finché non sono usciti tutti, poi si rimescola.
+ let shalomBag = [];
+ function playShalom() {
+ if (FORCE_SHALOM && SHALOM_BY_NAME[FORCE_SHALOM]) {
+ return SHALOM_BY_NAME[FORCE_SHALOM](); // effetto fisso (debug)
+ }
+ if (shalomBag.length === 0) {
+ shalomBag = Matching.shuffle(SHALOM_EFFECTS.slice(), Math.random);
+ }
+ const fx = shalomBag.shift();
+ return fx();
+ }
+
/* ---------- Sfondo three.js: pioggia di monete ---------- */
function initBackground() {
if (typeof THREE === "undefined") return;
@@ -744,7 +897,10 @@
function buildRevealOrder() {
// ordine completamente casuale: ogni persona (e quindi ogni stanza, anche
// quelle con idromassaggio) può uscire in qualsiasi momento
- return Matching.shuffle(PARTICIPANTS.map((p) => p.nome), Math.random);
+ return Matching.shuffle(
+ PARTICIPANTS.map((p) => p.nome),
+ Math.random,
+ );
}
function newGame(keepChips) {
@@ -755,6 +911,7 @@
cursor = 0;
currentBet = null;
noMatchStreak = 0;
+ shalomBag = [];
if (!keepChips) {
players = [
{ name: "TU", chips: 100, you: true },
@@ -932,7 +1089,15 @@
Audio.resume();
const person = revealQueue[cursor];
- const roomId = assignment.byPerson[person];
+ let roomId = assignment.byPerson[person];
+ if (FORCE_RESULT) {
+ // debug: forza il risultato su una jacuzzi (o su un id specifico)
+ const jac = ROOMS.filter((r) => r.jacuzzi);
+ roomId =
+ typeof FORCE_RESULT === "string"
+ ? FORCE_RESULT
+ : jac[Math.floor(Math.random() * jac.length)].id;
+ }
const room = roomById[roomId];
$("#player-name").textContent = person;
@@ -952,7 +1117,8 @@
// 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;
+ const noMatch =
+ !FORCE_RESULT && noMatchStreak < 2 && Math.random() < NOMATCH_CHANCE;
if (noMatch) {
noMatchStreak++;
@@ -1021,6 +1187,12 @@
// la macchina resta zoomata per godersi la vittoria, poi torna al posto
setTimeout(zoomMachineOut, isJackpot ? 1200 : 800);
+ // EFFETTO SHALOM: vittoria di una stanza con idromassaggio -> effetto a caso
+ if (isJackpot) {
+ await sleep(600); // lascia vedere un attimo i coriandoli
+ await playShalom();
+ }
+
cursor++;
setProgress();
renderLeaderboard();
@@ -1187,4 +1359,11 @@
// hook di debug (per test/verifica; innocuo)
window.__celebrate = celebrate;
+ window.__shalom = {
+ fire: shalomFireLion,
+ video: shalomVideoOnly,
+ combo: shalomCombo,
+ gesu: shalomGesu,
+ random: playShalom,
+ };
})();
diff --git a/styles.css b/styles.css
index 4da050c..57247ad 100644
--- a/styles.css
+++ b/styles.css
@@ -731,6 +731,174 @@ body {
text-shadow: 0 0 16px var(--red);
}
+/* ===== Effetto SHALOM (vittoria stanze con idromassaggio) ===== */
+.shalom-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 100;
+ display: none;
+ overflow: hidden;
+ background: rgba(4, 2, 9, 0.94);
+}
+.shalom-overlay.on {
+ display: block;
+ animation: shalomBang 0.5s ease; /* flashbang bianco iniziale */
+}
+@keyframes shalomBang {
+ 0% { background: #fff; }
+ 35% { background: #fff; }
+ 100% { background: rgba(4, 2, 9, 0.94); }
+}
+.shalom-skip {
+ position: absolute;
+ bottom: 16px;
+ right: 22px;
+ z-index: 3;
+ font-family: "Bebas Neue", sans-serif;
+ letter-spacing: 2px;
+ font-size: 16px;
+ color: rgba(255, 255, 255, 0.55);
+}
+
+/* fiammate dai due angoli in basso */
+.shalom-flames {
+ position: absolute;
+ bottom: -6%;
+ width: 50vw;
+ height: 92vh;
+ pointer-events: none;
+ opacity: 0;
+ filter: blur(2px);
+ mix-blend-mode: screen;
+ background:
+ radial-gradient(55% 42% at 50% 100%, rgba(255, 235, 140, 0.95), transparent 70%),
+ linear-gradient(0deg, #ff2d00 0%, #ff7a00 26%, #ffd23f 48%, rgba(255, 210, 63, 0) 74%);
+}
+.shalom-flames.left {
+ left: -8%;
+ transform-origin: bottom left;
+}
+.shalom-flames.right {
+ right: -8%;
+ transform-origin: bottom right;
+}
+.shalom-overlay.mode-fire .shalom-flames {
+ opacity: 1;
+ animation: flames 0.26s infinite alternate;
+}
+.shalom-overlay.mode-fire .shalom-flames.right {
+ animation-delay: 0.13s;
+}
+@keyframes flames {
+ 0% { transform: scaleY(1) scaleX(1) skewX(0deg); }
+ 100% { transform: scaleY(1.14) scaleX(0.92) skewX(4deg); }
+}
+
+/* leone che irrompe */
+.shalom-lion {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: min(46vw, 520px);
+ border-radius: 16px;
+ transform: translate(-50%, -50%) scale(0.2);
+ opacity: 0;
+ filter: drop-shadow(0 0 40px #ff6a00);
+}
+.shalom-overlay.mode-fire .shalom-lion {
+ animation: lionSlam 0.6s cubic-bezier(0.2, 1.5, 0.4, 1) forwards,
+ lionPulse 1.1s 0.6s infinite;
+}
+@keyframes lionSlam {
+ 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.2) rotate(-7deg); }
+ 60% { opacity: 1; transform: translate(-50%, -50%) scale(1.12) rotate(2deg); }
+ 100% { opacity: 1; transform: translate(-50%, -50%) scale(1) rotate(0deg); }
+}
+@keyframes lionPulse {
+ 0%, 100% { filter: drop-shadow(0 0 40px #ff6a00); }
+ 50% { filter: drop-shadow(0 0 75px #ff2e00); }
+}
+
+/* video a schermo intero */
+.shalom-video {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ opacity: 0;
+}
+.shalom-overlay.mode-video .shalom-flames,
+.shalom-overlay.mode-video .shalom-lion {
+ display: none;
+}
+.shalom-overlay.mode-video .shalom-video {
+ opacity: 1;
+}
+
+/* effetto GESÙ: raggi di luce + immagine che scende dall'alto */
+.shalom-rays {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 180vmax;
+ height: 180vmax;
+ transform: translate(-50%, -50%);
+ opacity: 0;
+ pointer-events: none;
+ mix-blend-mode: screen;
+ background: repeating-conic-gradient(
+ from 0deg,
+ rgba(255, 240, 200, 0) 0deg,
+ rgba(255, 240, 200, 0.3) 3.5deg,
+ rgba(255, 240, 200, 0) 7deg
+ );
+ -webkit-mask: radial-gradient(closest-side, #000 26%, transparent 72%);
+ mask: radial-gradient(closest-side, #000 26%, transparent 72%);
+}
+.shalom-overlay.mode-gesu .shalom-rays {
+ opacity: 1;
+ animation: shalomRays 26s linear infinite;
+}
+@keyframes shalomRays {
+ to { transform: translate(-50%, -50%) rotate(360deg); }
+}
+.shalom-gesu {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: min(42vw, 460px);
+ border-radius: 14px;
+ transform: translate(-50%, -160%);
+ opacity: 0;
+ filter: drop-shadow(0 0 55px rgba(255, 240, 180, 0.9));
+}
+.shalom-overlay.mode-gesu .shalom-gesu {
+ animation: gesuDescend 1.7s cubic-bezier(0.16, 0.8, 0.3, 1) forwards,
+ gesuFloat 4.5s 1.7s ease-in-out infinite;
+}
+@keyframes gesuDescend {
+ 0% { opacity: 0; transform: translate(-50%, -160%) scale(0.9); }
+ 55% { opacity: 1; }
+ 100% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
+}
+@keyframes gesuFloat {
+ 0%, 100% { transform: translate(-50%, -50%) translateY(0); }
+ 50% { transform: translate(-50%, -50%) translateY(-14px); }
+}
+/* mostra solo gli elementi giusti per ciascuna modalità */
+.shalom-overlay.mode-fire .shalom-rays,
+.shalom-overlay.mode-fire .shalom-gesu,
+.shalom-overlay.mode-video .shalom-rays,
+.shalom-overlay.mode-video .shalom-gesu {
+ display: none;
+}
+.shalom-overlay.mode-gesu .shalom-flames,
+.shalom-overlay.mode-gesu .shalom-lion,
+.shalom-overlay.mode-gesu .shalom-video {
+ display: none;
+}
+
@media (max-width: 980px) {
.stage {
grid-template-columns: 1fr;