/* ===== ELSA GAMBLING β€” motore del reveal ===== */ (function () { "use strict"; const $ = (sel) => document.querySelector(sel); const roomById = Object.fromEntries(ROOMS.map((r) => [r.id, r])); const genderOf = Object.fromEntries(PARTICIPANTS.map((p) => [p.nome, p.genere])); /* ---------- Audio sintetizzato (nessun file esterno) ---------- */ const Audio = { ctx: null, master: null, on: true, init() { if (this.ctx) return; const AC = window.AudioContext || window.webkitAudioContext; this.ctx = new AC(); this.master = this.ctx.createGain(); this.master.gain.value = 0.5; this.master.connect(this.ctx.destination); }, resume() { if (this.ctx && this.ctx.state === "suspended") this.ctx.resume(); }, blip(freq, dur = 0.05, type = "square", vol = 0.3, glideTo) { if (!this.on || !this.ctx) return; const t = this.ctx.currentTime; const o = this.ctx.createOscillator(); const g = this.ctx.createGain(); o.type = type; o.frequency.setValueAtTime(freq, t); if (glideTo) o.frequency.exponentialRampToValueAtTime(glideTo, t + dur); g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.0001, t + dur); o.connect(g).connect(this.master); o.start(t); o.stop(t + dur + 0.02); }, tick() { this.blip(1300, 0.03, "square", 0.12); }, ding() { this.blip(1318, 0.5, "sine", 0.3); // E6 this.blip(1568, 0.55, "sine", 0.25); // G6 }, coin() { this.blip(1760, 0.06, "square", 0.2); setTimeout(() => this.blip(2637, 0.12, "square", 0.2), 60); }, buzz() { this.blip(140, 0.45, "sawtooth", 0.3, 70); }, jackpot() { const notes = [523, 659, 784, 1046, 1318, 1568]; notes.forEach((f, i) => setTimeout(() => this.blip(f, 0.35, "square", 0.28), i * 90) ); // scintillio for (let i = 0; i < 10; i++) setTimeout(() => this.blip(1800 + Math.random() * 1400, 0.08, "sine", 0.12), 600 + i * 70); }, // riser di tensione: ritorna una funzione stop() riser(duration) { if (!this.on || !this.ctx) return () => {}; const t = this.ctx.currentTime; const o = this.ctx.createOscillator(); const g = this.ctx.createGain(); o.type = "sawtooth"; o.frequency.setValueAtTime(180, t); o.frequency.exponentialRampToValueAtTime(900, t + duration); g.gain.setValueAtTime(0.0001, t); g.gain.exponentialRampToValueAtTime(0.12, t + duration * 0.7); g.gain.exponentialRampToValueAtTime(0.0001, t + duration + 0.1); const lp = this.ctx.createBiquadFilter(); lp.type = "lowpass"; lp.frequency.value = 1200; o.connect(g).connect(lp).connect(this.master); o.start(t); o.stop(t + duration + 0.2); return () => { try { g.gain.cancelScheduledValues(this.ctx.currentTime); g.gain.setTargetAtTime(0.0001, this.ctx.currentTime, 0.03); } catch (e) {} }; }, }; /* ---------- Coriandoli ---------- */ const myConfetti = typeof confetti !== "undefined" ? confetti.create($("#confetti"), { resize: true, useWorker: true }) : () => {}; function celebrate(kind) { if (kind === "jackpot") { const end = Date.now() + 1400; const colors = ["#ffd23f", "#ff9e00", "#ff2e88", "#00e5ff", "#ffffff"]; (function frame() { myConfetti({ particleCount: 6, angle: 60, spread: 70, origin: { x: 0 }, colors }); myConfetti({ particleCount: 6, angle: 120, spread: 70, origin: { x: 1 }, colors }); myConfetti({ particleCount: 4, spread: 120, startVelocity: 55, origin: { y: 0.3 }, colors }); if (Date.now() < end) requestAnimationFrame(frame); })(); } else { myConfetti({ particleCount: 90, spread: 80, origin: { y: 0.55 }, colors: ["#ffd23f", "#00e5ff", "#ff2e88", "#ffffff"], }); } } function screenShake() { const app = $("#app"); app.classList.remove("shake"); void app.offsetWidth; app.classList.add("shake"); } function screenFlash() { let f = $(".flash"); if (!f) { f = document.createElement("div"); f.className = "flash"; document.body.appendChild(f); } f.classList.remove("go"); void f.offsetWidth; f.classList.add("go"); } /* ---------- Sfondo three.js: pioggia di monete ---------- */ function initBackground() { if (typeof THREE === "undefined") return; const canvas = $("#bg3d"); let renderer; try { renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); } catch (e) { return; // WebGL non disponibile: l'app funziona lo stesso senza sfondo 3D } renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(70, 1, 0.1, 100); camera.position.z = 30; // texture moneta disegnata su canvas const tex = (() => { const c = document.createElement("canvas"); c.width = c.height = 64; const g = c.getContext("2d"); const grad = g.createRadialGradient(24, 20, 4, 32, 32, 30); grad.addColorStop(0, "#fff6cf"); grad.addColorStop(0.4, "#ffd23f"); grad.addColorStop(1, "#b36b00"); g.fillStyle = grad; g.beginPath(); g.arc(32, 32, 28, 0, Math.PI * 2); g.fill(); g.fillStyle = "rgba(179,107,0,0.9)"; g.font = "bold 30px Georgia"; g.textAlign = "center"; g.textBaseline = "middle"; g.fillText("€", 32, 34); return new THREE.CanvasTexture(c); })(); const N = 520; const geo = new THREE.BufferGeometry(); const pos = new Float32Array(N * 3); const speed = new Float32Array(N); for (let i = 0; i < N; i++) { pos[i * 3] = (Math.random() - 0.5) * 80; pos[i * 3 + 1] = (Math.random() - 0.5) * 80; pos[i * 3 + 2] = (Math.random() - 0.5) * 40; speed[i] = 6 + Math.random() * 12; } geo.setAttribute("position", new THREE.BufferAttribute(pos, 3)); const mat = new THREE.PointsMaterial({ size: 1.7, map: tex, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, }); const points = new THREE.Points(geo, mat); scene.add(points); function resize() { const w = innerWidth, h = innerHeight; renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix(); } addEventListener("resize", resize); resize(); let last = performance.now(); let boost = 0; window.__coinBoost = () => (boost = 1.6); (function loop(now) { const dt = Math.min((now - last) / 1000, 0.05); last = now; const arr = geo.attributes.position.array; const mul = 1 + boost; for (let i = 0; i < N; i++) { arr[i * 3 + 1] -= speed[i] * dt * mul; if (arr[i * 3 + 1] < -40) arr[i * 3 + 1] = 40; } geo.attributes.position.needsUpdate = true; points.rotation.y += 0.02 * dt; boost *= 0.94; renderer.render(scene, camera); requestAnimationFrame(loop); })(last); } /* ---------- Rullo slot ---------- */ const ITEM_H = 88; const reelEl = $("#reel"); function reelItemHTML(room) { const jz = room.jacuzzi ? " jz" : ""; const icon = room.jacuzzi ? "πŸ›" : "🏠"; return `
${icon}${room.nome}
`; } // Costruisce una striscia lunga che termina sulla stanza target (centrata // nella riga di mezzo della finestra a 3 righe) e ritorna l'indice target. function buildStrip(targetId) { // azzera le animazioni del giro precedente (evita accumulo + fill forwards) reelEl.getAnimations().forEach((a) => a.cancel()); const loops = 14; const seq = []; for (let l = 0; l < loops; l++) for (const r of ROOMS) seq.push(r); // offset iniziale casuale per non ripartire sempre uguale const start = Math.floor(Math.random() * ROOMS.length); const rotated = seq.slice(start).concat(seq.slice(0, start)); const targetIndex = rotated.length; // la target va in coda rotated.push(roomById[targetId]); // aggiungo 2 righe sotto per riempire la finestra rotated.push(ROOMS[0], ROOMS[1]); reelEl.innerHTML = rotated.map(reelItemHTML).join(""); reelEl.style.transform = "translateY(0)"; return targetIndex; } function currentReelIndex() { const t = getComputedStyle(reelEl).transform; if (!t || t === "none") return 0; const y = parseFloat(t.split(",")[5]); // matrix(a,b,c,d,e,f) -> f = translateY return Math.round(-y / ITEM_H); } function spinReel(targetId) { return new Promise((resolve) => { const targetIndex = buildStrip(targetId); // centro la target nella riga di mezzo (finestra 3 righe) const finalY = -(targetIndex - 1) * ITEM_H; const duration = 4200; // tick sincronizzati al passaggio dei simboli let lastIdx = 0; let ticking = true; (function tickLoop() { if (!ticking) return; const idx = currentReelIndex(); if (idx !== lastIdx) { Audio.tick(); lastIdx = idx; } requestAnimationFrame(tickLoop); })(); const stopRiser = Audio.riser(duration / 1000); const anim = reelEl.animate( [{ transform: "translateY(0)" }, { transform: `translateY(${finalY}px)` }], { duration, easing: "cubic-bezier(0.12, 0.85, 0.18, 1)", fill: "forwards" } ); anim.onfinish = () => { ticking = false; stopRiser(); // micro-bounce di assestamento (near-miss / lock-in) reelEl.animate( [ { transform: `translateY(${finalY}px)` }, { transform: `translateY(${finalY + 14}px)` }, { transform: `translateY(${finalY}px)` }, ], { duration: 260, easing: "ease-out" } ); resolve(); }; }); } /* ---------- Stato di gioco ---------- */ let assignment = null; // {byPerson, roomGender, fRooms, mRooms} let revealQueue = []; // nomi in ordine di reveal let occupants = {}; // roomId -> [nomi] (svelati finora) let cursor = 0; let auto = false; let busy = false; // scommettitori (TU + banco simulato) let players = []; let currentBet = null; // {roomId} function prestige(roomId) { const r = roomById[roomId]; return (r.jacuzzi ? 2 : 0) + (r.posti === 1 ? 1 : 0); } function buildRevealOrder() { // svelati per prestigio crescente: le stanze con jacuzzi (jackpot) per ultime const people = PARTICIPANTS.map((p) => p.nome); return Matching.shuffle(people, Math.random).sort( (a, b) => prestige(assignment.byPerson[a]) - prestige(assignment.byPerson[b]) ); } function newGame(keepChips) { assignment = Matching.assign(ROOMS, PARTICIPANTS); revealQueue = buildRevealOrder(); occupants = {}; ROOMS.forEach((r) => (occupants[r.id] = [])); cursor = 0; currentBet = null; if (!keepChips) { players = [ { name: "TU", chips: 100, you: true }, { name: "Il Banco", chips: 100 }, { name: "Madama Fortuna", chips: 100 }, { name: "Jinx", chips: 100 }, ]; } renderRooms(); renderLeaderboard(); setProgress(); $("#player-name").textContent = "β€”"; $("#odds").innerHTML = ""; mc("Si parte! Premi START 🎰"); $("#btn-primary").textContent = "START 🎰"; $("#btn-primary").disabled = false; } /* ---------- Rendering ---------- */ function renderRooms() { const board = $("#rooms-board"); board.innerHTML = ROOMS.map((r) => { const g = assignment.roomGender[r.id]; const occ = occupants[r.id]; const chips = occ.map((n) => `${n}`).join(""); const emptyCount = r.posti - occ.length; const empties = Array.from({ length: emptyCount }) .map(() => ``) .join(""); return `
${r.jacuzzi ? "πŸ› " : ""}${r.nome} ${occ.length}/${r.posti} Β· ${g}
${chips}${empties}
`; }).join(""); } function renderLeaderboard() { const sorted = players.slice().sort((a, b) => b.chips - a.chips); $("#leaderboard").innerHTML = sorted .map( (p) => `
${p.you ? "⭐ " : ""}${p.name}${p.chips}
` ) .join(""); $("#balance").textContent = players.find((p) => p.you).chips; } function setProgress() { $("#progress").textContent = `${cursor} / ${revealQueue.length}`; } function mc(text) { $("#mc").textContent = text; } function log(text, cls) { const e = document.createElement("div"); e.className = "log-entry " + (cls || ""); e.textContent = text; $("#log").prepend(e); } /* ---------- Scommesse ---------- */ // Quote cosmetiche: piΓΉ letti liberi in una stanza => piΓΉ probabile => quota bassa. function computeOdds(person) { const g = genderOf[person]; const rooms = ROOMS.filter( (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); return rooms.map((r) => { const free = r.posti - occupants[r.id].length; let mult = freeTotal / free; mult = Math.max(1.3, Math.round(mult * 10) / 10); return { room: r, mult }; }); } function renderOdds(person, odds) { const hint = currentBet ? `Hai puntato 10 gettoni su ${roomById[currentBet.roomId].nome}` : "Clicca una stanza per puntare 10 gettoni (o SPINNA)"; $("#odds").innerHTML = `
${hint}
` + odds .map( (o) => `
${o.room.jacuzzi ? "πŸ› " : ""}${o.room.nome} x${o.mult.toFixed(1)}
` ) .join(""); $("#odds") .querySelectorAll(".odd-row") .forEach((row) => row.addEventListener("click", () => { if (busy) return; const rid = row.getAttribute("data-bet"); currentBet = { roomId: rid }; Audio.coin(); renderOdds(person, odds); }) ); } // I bot puntano sul favorito (quota piΓΉ bassa) o a caso. function botsPlace(odds) { players .filter((p) => !p.you) .forEach((p) => { if (p.chips < 10) { p._bet = null; return; } const pick = Math.random() < 0.6 ? odds.slice().sort((a, b) => a.mult - b.mult)[0] : odds[Math.floor(Math.random() * odds.length)]; p._bet = pick ? { roomId: pick.room.id, mult: pick.mult } : null; }); } function resolveBets(person, landedRoomId, oddsMap) { // TU const you = players.find((p) => p.you); if (currentBet) { const o = oddsMap[currentBet.roomId]; if (currentBet.roomId === landedRoomId) { const win = Math.round(10 * o); you.chips += win - 10; log(`Hai vinto ${win} gettoni! (${roomById[landedRoomId].nome})`, "win"); } else { you.chips -= 10; log(`Hai perso la puntata su ${roomById[currentBet.roomId].nome}.`); } } // bot players .filter((p) => !p.you && p._bet) .forEach((p) => { if (p._bet.roomId === landedRoomId) p.chips += Math.round(10 * p._bet.mult) - 10; else p.chips -= 10; }); } /* ---------- Turno di reveal ---------- */ async function nextTurn() { if (busy) return; if (cursor >= revealQueue.length) { endGame(); return; } busy = true; Audio.init(); Audio.resume(); const person = revealQueue[cursor]; const roomId = assignment.byPerson[person]; const room = roomById[roomId]; $("#player-name").textContent = person; highlightRoom(null); // fase scommesse const odds = computeOdds(person); const oddsMap = Object.fromEntries(odds.map((o) => [o.room.id, o.mult])); renderOdds(person, odds); botsPlace(odds); mc(`Tocca a ${person}! Dove finirΓ ? 🎲`); // leva + spin $("#lever").classList.add("pull"); setTimeout(() => $("#lever").classList.remove("pull"), 300); if (window.__coinBoost) window.__coinBoost(); $("#btn-primary").disabled = true; await spinReel(roomId); // atterraggio occupants[roomId].push(person); highlightRoom(roomId); renderRooms(); highlightRoom(roomId); resolveBets(person, roomId, oddsMap); currentBet = null; const isJackpot = room.jacuzzi; if (isJackpot) { Audio.jackpot(); celebrate("jackpot"); screenShake(); screenFlash(); mc(`πŸŽ‰ JACKPOT! ${person} conquista ${room.nome} con IDROMASSAGGIO! πŸ›`); log(`JACKPOT: ${person} β†’ ${room.nome} πŸ›`, "jackpot"); } else { Audio.ding(); celebrate("normal"); mc(`${person} va in ${room.nome}!`); log(`${person} β†’ ${room.nome}`); } Audio.coin(); cursor++; setProgress(); renderLeaderboard(); busy = false; if (cursor >= revealQueue.length) { $("#btn-primary").textContent = "FINE 🏁"; $("#btn-primary").disabled = false; setTimeout(endGame, 900); } else { $("#btn-primary").textContent = "SPINNA! 🎰"; $("#btn-primary").disabled = false; if (auto) setTimeout(nextTurn, 1400); } } function highlightRoom(roomId) { document.querySelectorAll(".room-card").forEach((c) => c.classList.remove("hot")); if (roomId) { const el = document.querySelector(`.room-card[data-room="${roomId}"]`); if (el) el.classList.add("hot"); } } function endGame() { const winner = players.slice().sort((a, b) => b.chips - a.chips)[0]; const you = players.find((p) => p.you); mc(`🏁 Tutti sistemati! Vincitore scommesse: ${winner.name} (${winner.chips} gettoni).`); log(`Fine. I tuoi gettoni: ${you.chips}. Miglior scommettitore: ${winner.name}.`, "win"); celebrate("jackpot"); $("#btn-primary").textContent = "RIGIOCA πŸ”"; $("#btn-primary").disabled = false; $("#btn-primary").onclick = () => { newGame(false); $("#btn-primary").onclick = onPrimary; }; } /* ---------- Marquee ---------- */ function initMarquee() { const m = $("#marquee"); m.innerHTML = Array.from({ length: 16 }) .map((_, i) => ``) .join(""); } /* ---------- Controlli ---------- */ function onPrimary() { if (busy) return; if (cursor >= revealQueue.length) { endGame(); return; } nextTurn(); } function bindControls() { $("#btn-primary").onclick = onPrimary; $("#btn-auto").onclick = (e) => { auto = !auto; e.currentTarget.classList.toggle("active", auto); if (auto && !busy && cursor < revealQueue.length) nextTurn(); }; $("#btn-reroll").onclick = () => { if (busy) return; Audio.init(); Audio.resume(); newGame(true); mc("Nuovo sorteggio pronto! 🎲 Premi START."); }; $("#btn-sound").onclick = (e) => { Audio.on = !Audio.on; e.currentTarget.textContent = Audio.on ? "πŸ”Š" : "πŸ”‡"; }; // prima interazione: sblocca audio document.addEventListener( "pointerdown", () => { Audio.init(); Audio.resume(); }, { once: true } ); } /* ---------- Avvio ---------- */ try { initBackground(); } catch (e) { console.warn("Sfondo 3D disattivato:", e); } initMarquee(); bindControls(); newGame(false); })();