This commit is contained in:
2026-07-02 15:29:24 +02:00
parent efe2ef2a76
commit 7dcbc9582d
3 changed files with 356 additions and 3 deletions

View File

@@ -34,6 +34,12 @@ python3 -m http.server 8080
I posti con **jacuzzi** (Argo, Eolo, "Quanto te la rischi") sono i jackpot e I posti con **jacuzzi** (Argo, Eolo, "Quanto te la rischi") sono i jackpot e
vengono rivelati per ultimi, con celebrazione extra. 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 → **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 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. vengono annullate e la persona **ri-tira** (senza avanzare). Max 2 no-match di fila.

185
app.js
View File

@@ -2,6 +2,17 @@
(function () { (function () {
"use strict"; "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 $ = (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( const genderOf = Object.fromEntries(
@@ -161,6 +172,8 @@
win: "sounds/win.mp3", // jingle di vincita (match normale) win: "sounds/win.mp3", // jingle di vincita (match normale)
jackpot: "sounds/jackpot.mp3", // sirena di jackpot (idromassaggio) jackpot: "sounds/jackpot.mp3", // sirena di jackpot (idromassaggio)
coins: "sounds/coins.mp3", // pioggia di monete 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: {}, el: {},
ready: {}, 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 =
'<div class="shalom-flames left"></div>' +
'<div class="shalom-flames right"></div>' +
'<img class="shalom-lion" src="image/lion.jpg" alt="" />' +
'<div class="shalom-rays"></div>' +
'<img class="shalom-gesu" src="image/gesu.jpeg" alt="" />' +
'<video class="shalom-video" src="video/videoplayback.mp4" muted playsinline preload="auto"></video>' +
'<div class="shalom-skip">tap per saltare ▸</div>';
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 ---------- */ /* ---------- Sfondo three.js: pioggia di monete ---------- */
function initBackground() { function initBackground() {
if (typeof THREE === "undefined") return; if (typeof THREE === "undefined") return;
@@ -744,7 +897,10 @@
function buildRevealOrder() { function buildRevealOrder() {
// ordine completamente casuale: ogni persona (e quindi ogni stanza, anche // ordine completamente casuale: ogni persona (e quindi ogni stanza, anche
// quelle con idromassaggio) può uscire in qualsiasi momento // 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) { function newGame(keepChips) {
@@ -755,6 +911,7 @@
cursor = 0; cursor = 0;
currentBet = null; currentBet = null;
noMatchStreak = 0; noMatchStreak = 0;
shalomBag = [];
if (!keepChips) { if (!keepChips) {
players = [ players = [
{ name: "TU", chips: 100, you: true }, { name: "TU", chips: 100, you: true },
@@ -932,7 +1089,15 @@
Audio.resume(); Audio.resume();
const person = revealQueue[cursor]; 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]; const room = roomById[roomId];
$("#player-name").textContent = person; $("#player-name").textContent = person;
@@ -952,7 +1117,8 @@
// 30% dei giri: NESSUN MATCH (3 stanze diverse) -> effetto pazzo + respin. // 30% dei giri: NESSUN MATCH (3 stanze diverse) -> effetto pazzo + respin.
// Tetto di 2 no-match di fila per non bloccare nessuno. // 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) { if (noMatch) {
noMatchStreak++; noMatchStreak++;
@@ -1021,6 +1187,12 @@
// la macchina resta zoomata per godersi la vittoria, poi torna al posto // la macchina resta zoomata per godersi la vittoria, poi torna al posto
setTimeout(zoomMachineOut, isJackpot ? 1200 : 800); 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++; cursor++;
setProgress(); setProgress();
renderLeaderboard(); renderLeaderboard();
@@ -1187,4 +1359,11 @@
// hook di debug (per test/verifica; innocuo) // hook di debug (per test/verifica; innocuo)
window.__celebrate = celebrate; window.__celebrate = celebrate;
window.__shalom = {
fire: shalomFireLion,
video: shalomVideoOnly,
combo: shalomCombo,
gesu: shalomGesu,
random: playShalom,
};
})(); })();

View File

@@ -731,6 +731,174 @@ body {
text-shadow: 0 0 16px var(--red); 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) { @media (max-width: 980px) {
.stage { .stage {
grid-template-columns: 1fr; grid-template-columns: 1fr;