suono gambling
This commit is contained in:
@@ -23,7 +23,10 @@ python3 -m http.server 8080
|
|||||||
- **START / SPINNA!** — rivela la persona successiva con un giro di slot.
|
- **START / SPINNA!** — rivela la persona successiva con un giro di slot.
|
||||||
- **AUTO ▶** — rivela tutti in sequenza da solo (modalità proiettore).
|
- **AUTO ▶** — rivela tutti in sequenza da solo (modalità proiettore).
|
||||||
- **RE-ROLL 🎲** — rifà il sorteggio (nuova assegnazione).
|
- **RE-ROLL 🎲** — rifà il sorteggio (nuova assegnazione).
|
||||||
- **🔊** — on/off dei suoni (sintetizzati, nessun file audio).
|
- **🔊** — on/off dei suoni: **campioni reali stile slot** (whirr dei rulli nel
|
||||||
|
roll, jingle di vincita, sirena di jackpot, monete — in `sounds/`, royalty-free
|
||||||
|
[Mixkit](https://mixkit.co/free-sound-effects/slot-machine/)) con **fallback
|
||||||
|
sintetizzato** se i file non caricano (es. offline totale).
|
||||||
- **Scommesse** — prima di ogni giro clicca una stanza nel pannello a destra per
|
- **Scommesse** — prima di ogni giro clicca una stanza nel pannello a destra per
|
||||||
puntare 10 gettoni; se indovini vinci in base alla quota. Contro di te giocano
|
puntare 10 gettoni; se indovini vinci in base alla quota. Contro di te giocano
|
||||||
3 scommettitori simulati (leaderboard live).
|
3 scommettitori simulati (leaderboard live).
|
||||||
@@ -51,6 +54,7 @@ vengono annullate e la persona **ri-tira** (senza avanzare). Max 2 no-match di f
|
|||||||
| `data.js` | Stanze e partecipanti (sorgente dati) |
|
| `data.js` | Stanze e partecipanti (sorgente dati) |
|
||||||
| `matching.js` | Algoritmo di assegnazione (testabile in Node) |
|
| `matching.js` | Algoritmo di assegnazione (testabile in Node) |
|
||||||
| `app.js` | Motore reveal: rulli, audio, three.js, coriandoli, scommesse |
|
| `app.js` | Motore reveal: rulli, audio, three.js, coriandoli, scommesse |
|
||||||
|
| `sounds/` | SFX reali stile slot (Mixkit, royalty-free): reel-spin, win, jackpot, coins |
|
||||||
| `vendor/` | Librerie e font in locale (three.js, canvas-confetti, woff2) — app offline |
|
| `vendor/` | Librerie e font in locale (three.js, canvas-confetti, woff2) — app offline |
|
||||||
| `docs/` | Ricerca su stile gambling, suspense, interattività, implementazione |
|
| `docs/` | Ricerca su stile gambling, suspense, interattività, implementazione |
|
||||||
|
|
||||||
@@ -63,5 +67,4 @@ nomi, generi o stanze — l'app si adatta da sola.
|
|||||||
|
|
||||||
- **Scommesse multi-persona reali** dai telefoni (ora: singolo operatore + bot
|
- **Scommesse multi-persona reali** dai telefoni (ora: singolo operatore + bot
|
||||||
simulati). Servirebbe `BroadcastChannel` o un piccolo backend.
|
simulati). Servirebbe `BroadcastChannel` o un piccolo backend.
|
||||||
- **SFX reali** al posto dei suoni sintetizzati.
|
|
||||||
- **Gran finale** dedicato all'ultimo jackpot (ora tutti i jackpot sono uguali).
|
- **Gran finale** dedicato all'ultimo jackpot (ora tutti i jackpot sono uguali).
|
||||||
|
|||||||
74
app.js
74
app.js
@@ -142,6 +142,64 @@
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* ---------- Campioni reali stile slot (Mixkit, royalty-free) ----------
|
||||||
|
File in /sounds. Usiamo HTMLAudioElement (non fetch+decode) così
|
||||||
|
funziona anche aprendo l'app via file:// dove Chrome blocca fetch dei
|
||||||
|
file locali. Rispetta il mute globale (Audio.on) e fa fallback ai suoni
|
||||||
|
sintetizzati se un file non carica. */
|
||||||
|
const Samples = {
|
||||||
|
files: {
|
||||||
|
reelSpin: "sounds/reel-spin.mp3", // whirr dei rulli durante il roll
|
||||||
|
win: "sounds/win.mp3", // jingle di vincita (match normale)
|
||||||
|
jackpot: "sounds/jackpot.mp3", // sirena di jackpot (idromassaggio)
|
||||||
|
coins: "sounds/coins.mp3", // pioggia di monete
|
||||||
|
},
|
||||||
|
el: {},
|
||||||
|
ready: {},
|
||||||
|
preload() {
|
||||||
|
Object.entries(this.files).forEach(([k, url]) => {
|
||||||
|
const a = new window.Audio(url); // window.Audio: qui "Audio" è l'oggetto synth
|
||||||
|
a.preload = "auto";
|
||||||
|
a.addEventListener("canplaythrough", () => (this.ready[k] = true), { once: true });
|
||||||
|
a.addEventListener("error", () => (this.ready[k] = false));
|
||||||
|
this.el[k] = a;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// ritorna una funzione stop(fadeSec) per interromperlo (utile per il loop)
|
||||||
|
play(key, opts = {}) {
|
||||||
|
const { vol = 0.8, loop = false, fallback } = opts;
|
||||||
|
if (!Audio.on) return () => {};
|
||||||
|
const base = this.el[key];
|
||||||
|
if (!base || this.ready[key] === false) {
|
||||||
|
if (fallback) fallback();
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
const a = loop ? base : base.cloneNode(); // clone -> più play sovrapposti
|
||||||
|
a.loop = loop;
|
||||||
|
a.volume = vol;
|
||||||
|
try {
|
||||||
|
a.currentTime = 0;
|
||||||
|
} catch (e) {}
|
||||||
|
const p = a.play();
|
||||||
|
if (p && p.catch) p.catch(() => fallback && fallback());
|
||||||
|
return (fade = 0.2) => {
|
||||||
|
const steps = 8;
|
||||||
|
let i = 0;
|
||||||
|
const iv = setInterval(() => {
|
||||||
|
i++;
|
||||||
|
a.volume = Math.max(0, vol * (1 - i / steps));
|
||||||
|
if (i >= steps) {
|
||||||
|
clearInterval(iv);
|
||||||
|
try {
|
||||||
|
a.pause();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}, (fade * 1000) / steps);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Samples.preload();
|
||||||
|
|
||||||
/* ---------- Coriandoli (canvas a schermo intero, dimensionato a mano) ---------- */
|
/* ---------- Coriandoli (canvas a schermo intero, dimensionato a mano) ---------- */
|
||||||
// IMPORTANTE: niente useWorker + niente resize automatico. Dimensiono io il
|
// IMPORTANTE: niente useWorker + niente resize automatico. Dimensiono io il
|
||||||
// canvas alla viewport, altrimenti il buffer resta 300x150 e i coriandoli
|
// canvas alla viewport, altrimenti il buffer resta 300x150 e i coriandoli
|
||||||
@@ -216,7 +274,10 @@
|
|||||||
|
|
||||||
// EFFETTO PAZZO quando i rulli non combaciano: sirena + strobo + glitch + timbro
|
// EFFETTO PAZZO quando i rulli non combaciano: sirena + strobo + glitch + timbro
|
||||||
function crazyNoMatch() {
|
function crazyNoMatch() {
|
||||||
Audio.siren();
|
// buzz di errore "sbagliato!" (doppio, stile game-show) prima del caos
|
||||||
|
Audio.buzz();
|
||||||
|
setTimeout(() => Audio.buzz(), 180);
|
||||||
|
setTimeout(() => Audio.siren(), 320); // la sirena entra dopo il buzz
|
||||||
screenShake();
|
screenShake();
|
||||||
|
|
||||||
// strobo a schermo intero
|
// strobo a schermo intero
|
||||||
@@ -466,6 +527,8 @@
|
|||||||
setTimeout(zoomMachineSteady, mainDur / 2);
|
setTimeout(zoomMachineSteady, mainDur / 2);
|
||||||
|
|
||||||
const stopRiser = Audio.riser(5.6);
|
const stopRiser = Audio.riser(5.6);
|
||||||
|
// whirr reale dei rulli sotto ai tick/riser sintetizzati
|
||||||
|
const stopWheel = Samples.play("reelSpin", { vol: 0.65 });
|
||||||
|
|
||||||
// tick sincronizzati sul 3° rullo (gira più a lungo)
|
// tick sincronizzati sul 3° rullo (gira più a lungo)
|
||||||
let ticking = true;
|
let ticking = true;
|
||||||
@@ -491,6 +554,7 @@
|
|||||||
|
|
||||||
ticking = false;
|
ticking = false;
|
||||||
stopRiser();
|
stopRiser();
|
||||||
|
stopWheel(0.25); // taglia il whirr quando l'ultimo rullo atterra
|
||||||
zoomMachineSteady();
|
zoomMachineSteady();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,20 +851,22 @@
|
|||||||
|
|
||||||
const isJackpot = room.jacuzzi;
|
const isJackpot = room.jacuzzi;
|
||||||
Audio.ding(); // "click" di aggancio della stanza
|
Audio.ding(); // "click" di aggancio della stanza
|
||||||
Audio.fanfare(); // 🎺 trombe alla selezione
|
|
||||||
if (isJackpot) {
|
if (isJackpot) {
|
||||||
Audio.jackpot();
|
// sirena reale di jackpot; fallback: fanfara + jackpot sintetizzati
|
||||||
|
Samples.play("jackpot", { vol: 0.9, fallback: () => { Audio.fanfare(); Audio.jackpot(); } });
|
||||||
celebrate("jackpot");
|
celebrate("jackpot");
|
||||||
screenShake();
|
screenShake();
|
||||||
screenFlash();
|
screenFlash();
|
||||||
mc(`🎉 JACKPOT! ${person} conquista ${room.nome} con IDROMASSAGGIO! 🛁`);
|
mc(`🎉 JACKPOT! ${person} conquista ${room.nome} con IDROMASSAGGIO! 🛁`);
|
||||||
log(`JACKPOT: ${person} → ${room.nome} 🛁`, "jackpot");
|
log(`JACKPOT: ${person} → ${room.nome} 🛁`, "jackpot");
|
||||||
} else {
|
} else {
|
||||||
|
// jingle reale di vincita; fallback: fanfara sintetizzata
|
||||||
|
Samples.play("win", { vol: 0.85, fallback: () => Audio.fanfare() });
|
||||||
celebrate("normal");
|
celebrate("normal");
|
||||||
mc(`${person} va in ${room.nome}!`);
|
mc(`${person} va in ${room.nome}!`);
|
||||||
log(`${person} → ${room.nome}`);
|
log(`${person} → ${room.nome}`);
|
||||||
}
|
}
|
||||||
Audio.coin();
|
Samples.play("coins", { vol: 0.6, fallback: () => Audio.coin() });
|
||||||
|
|
||||||
// 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);
|
||||||
|
|||||||
BIN
sounds/coins.mp3
Normal file
BIN
sounds/coins.mp3
Normal file
Binary file not shown.
BIN
sounds/jackpot.mp3
Normal file
BIN
sounds/jackpot.mp3
Normal file
Binary file not shown.
BIN
sounds/reel-spin.mp3
Normal file
BIN
sounds/reel-spin.mp3
Normal file
Binary file not shown.
BIN
sounds/win.mp3
Normal file
BIN
sounds/win.mp3
Normal file
Binary file not shown.
Reference in New Issue
Block a user