// Faraón Game FX Engine: Procedural Audio, Particles, Screen Shake & Animations window.FaraonFx = (function () { let audioCtx = null; let isMuted = false; let speedMultiplier = 1.0; let particleCanvas = null; let particleCtx = null; let particles = []; let animationFrameId = null; function setSpeedLevel(level) { switch (level) { case -5: speedMultiplier = 0.35; break; // 0.2x Zen case -4: speedMultiplier = 0.45; break; // 0.35x Veľmi pomalá case -3: speedMultiplier = 0.60; break; // 0.5x Pomalá case -2: speedMultiplier = 0.75; break; // 0.65x Pokojná case -1: speedMultiplier = 0.88; break; // 0.8x Zvoľnená case 0: speedMultiplier = 1.0; break; // 1x Normálna case 1: speedMultiplier = 1.25; break; // 1.25x Svižná case 2: speedMultiplier = 1.55; break; // 1.5x Rýchla case 3: speedMultiplier = 2.0; break; // 2x Veľmi rýchla case 4: speedMultiplier = 2.75; break; // 2.75x Turbo case 5: speedMultiplier = 3.8; break; // 4x Blesková default: speedMultiplier = 1.0; break; } } function getAudioContext() { if (!audioCtx) { const AudioContext = window.AudioContext || window.webkitAudioContext; if (AudioContext) { audioCtx = new AudioContext(); } } if (audioCtx && audioCtx.state === 'suspended') { audioCtx.resume(); } return audioCtx; } // --- PROCEDURAL SOUND GENERATORS --- function playCardSnap() { if (isMuted) return; const ctx = getAudioContext(); if (!ctx) return; const now = ctx.currentTime; const bufferSize = ctx.sampleRate * 0.05; const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (ctx.sampleRate * 0.01)); } const noise = ctx.createBufferSource(); noise.buffer = buffer; const filter = ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.setValueAtTime(1400 + Math.random() * 400, now); filter.Q.setValueAtTime(3, now); const osc = ctx.createOscillator(); const oscGain = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(160, now); osc.frequency.exponentialRampToValueAtTime(40, now + 0.08); oscGain.gain.setValueAtTime(0.3, now); oscGain.gain.exponentialRampToValueAtTime(0.001, now + 0.08); const noiseGain = ctx.createGain(); noiseGain.gain.setValueAtTime(0.5, now); noiseGain.gain.exponentialRampToValueAtTime(0.001, now + 0.05); noise.connect(filter); filter.connect(noiseGain); noiseGain.connect(ctx.destination); osc.connect(oscGain); oscGain.connect(ctx.destination); noise.start(now); osc.start(now); osc.stop(now + 0.09); } function playCardDraw() { if (isMuted) return; const ctx = getAudioContext(); if (!ctx) return; const now = ctx.currentTime; const bufferSize = ctx.sampleRate * 0.08; const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { data[i] = (Math.random() * 2 - 1) * (1 - i / bufferSize); } const noise = ctx.createBufferSource(); noise.buffer = buffer; const filter = ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.setValueAtTime(2200, now); filter.frequency.exponentialRampToValueAtTime(800, now + 0.08); filter.Q.setValueAtTime(2, now); const gain = ctx.createGain(); gain.gain.setValueAtTime(0.25, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08); noise.connect(filter); filter.connect(gain); gain.connect(ctx.destination); noise.start(now); } function playSevenStack(level) { if (isMuted) return; const ctx = getAudioContext(); if (!ctx) return; const now = ctx.currentTime; const rootFreq = 180 + Math.min(level, 4) * 80; const osc1 = ctx.createOscillator(); const osc2 = ctx.createOscillator(); const gain = ctx.createGain(); osc1.type = 'sawtooth'; osc1.frequency.setValueAtTime(rootFreq, now); osc1.frequency.exponentialRampToValueAtTime(rootFreq * 0.5, now + 0.4); osc2.type = 'triangle'; osc2.frequency.setValueAtTime(rootFreq * 1.5, now); osc2.frequency.exponentialRampToValueAtTime(rootFreq * 0.75, now + 0.4); const filter = ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.setValueAtTime(2000, now); filter.frequency.exponentialRampToValueAtTime(200, now + 0.4); gain.gain.setValueAtTime(0.4 + Math.min(level * 0.1, 0.4), now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.45); osc1.connect(filter); osc2.connect(filter); filter.connect(gain); gain.connect(ctx.destination); osc1.start(now); osc2.start(now); osc1.stop(now + 0.45); osc2.stop(now + 0.45); triggerScreenShake(level * 3 + 2); } function playAceFreeze() { if (isMuted) return; const ctx = getAudioContext(); if (!ctx) return; const now = ctx.currentTime; const freqs = [880, 1174, 1567, 2093]; freqs.forEach((freq, idx) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(freq, now + idx * 0.04); gain.gain.setValueAtTime(0.15, now + idx * 0.04); gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.04 + 0.35); osc.connect(gain); gain.connect(ctx.destination); osc.start(now + idx * 0.04); osc.stop(now + idx * 0.04 + 0.36); }); triggerScreenShake(3); } function playIceCrack() { if (isMuted) return; const ctx = getAudioContext(); if (!ctx) return; const now = ctx.currentTime; // Sharp ice snap noise const bufferSize = ctx.sampleRate * 0.12; const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (ctx.sampleRate * 0.02)); } const noise = ctx.createBufferSource(); noise.buffer = buffer; const filter = ctx.createBiquadFilter(); filter.type = 'highpass'; filter.frequency.setValueAtTime(3200, now); const gain = ctx.createGain(); gain.gain.setValueAtTime(0.45, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12); noise.connect(filter); filter.connect(gain); gain.connect(ctx.destination); noise.start(now); // Resonant high crystal ringing [2400, 3600, 4800].forEach((freq, idx) => { const osc = ctx.createOscillator(); const oscGain = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(freq + Math.random() * 200, now + idx * 0.02); oscGain.gain.setValueAtTime(0.12, now + idx * 0.02); oscGain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.02 + 0.22); osc.connect(oscGain); oscGain.connect(ctx.destination); osc.start(now + idx * 0.02); osc.stop(now + idx * 0.02 + 0.23); }); triggerScreenShake(4); } function playLeafShield() { if (isMuted) return; const ctx = getAudioContext(); if (!ctx) return; const now = ctx.currentTime; [523.25, 659.25, 783.99, 1046.50].forEach((freq, idx) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'triangle'; osc.frequency.setValueAtTime(freq, now + idx * 0.05); gain.gain.setValueAtTime(0.2, now + idx * 0.05); gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.05 + 0.35); osc.connect(gain); gain.connect(ctx.destination); osc.start(now + idx * 0.05); osc.stop(now + idx * 0.05 + 0.36); }); triggerScreenShake(2); } function playHornikChime() { if (isMuted) return; const ctx = getAudioContext(); if (!ctx) return; const now = ctx.currentTime; const notes = [523.25, 659.25, 783.99, 1046.50]; notes.forEach((freq, i) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(freq, now + i * 0.05); gain.gain.setValueAtTime(0.2, now + i * 0.05); gain.gain.exponentialRampToValueAtTime(0.001, now + i * 0.05 + 0.6); osc.connect(gain); gain.connect(ctx.destination); osc.start(now + i * 0.05); osc.stop(now + i * 0.05 + 0.61); }); } function playBurnExplosion() { if (isMuted) return; const ctx = getAudioContext(); if (!ctx) return; const now = ctx.currentTime; const osc = ctx.createOscillator(); const oscGain = ctx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(140, now); osc.frequency.exponentialRampToValueAtTime(30, now + 0.7); oscGain.gain.setValueAtTime(0.6, now); oscGain.gain.exponentialRampToValueAtTime(0.001, now + 0.7); const bufferSize = ctx.sampleRate * 0.6; const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (ctx.sampleRate * 0.15)); } const noise = ctx.createBufferSource(); noise.buffer = buffer; const filter = ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.setValueAtTime(800, now); filter.frequency.exponentialRampToValueAtTime(100, now + 0.6); const noiseGain = ctx.createGain(); noiseGain.gain.setValueAtTime(0.6, now); noiseGain.gain.exponentialRampToValueAtTime(0.001, now + 0.6); noise.connect(filter); filter.connect(noiseGain); noiseGain.connect(ctx.destination); osc.connect(oscGain); oscGain.connect(ctx.destination); noise.start(now); osc.start(now); osc.stop(now + 0.71); triggerScreenShake(12); } function playVictory() { if (isMuted) return; const ctx = getAudioContext(); if (!ctx) return; const now = ctx.currentTime; const melody = [ { f: 261.63, t: 0.00, d: 0.15 }, { f: 392.00, t: 0.15, d: 0.15 }, { f: 523.25, t: 0.30, d: 0.20 }, { f: 659.25, t: 0.50, d: 0.20 }, { f: 783.99, t: 0.70, d: 0.60 } ]; melody.forEach(note => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'triangle'; osc.frequency.setValueAtTime(note.f, now + note.t); gain.gain.setValueAtTime(0.3, now + note.t); gain.gain.exponentialRampToValueAtTime(0.001, now + note.t + note.d); osc.connect(gain); gain.connect(ctx.destination); osc.start(now + note.t); osc.stop(now + note.t + note.d + 0.05); }); triggerConfetti(); } function playHeartbeat() { if (isMuted) return; const ctx = getAudioContext(); if (!ctx) return; const now = ctx.currentTime; [0, 0.18].forEach(offset => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(80, now + offset); osc.frequency.exponentialRampToValueAtTime(45, now + offset + 0.1); gain.gain.setValueAtTime(0.4, now + offset); gain.gain.exponentialRampToValueAtTime(0.001, now + offset + 0.12); osc.connect(gain); gain.connect(ctx.destination); osc.start(now + offset); osc.stop(now + offset + 0.13); }); } // --- SCREEN SHAKE --- function triggerScreenShake(intensity = 5) { const board = document.getElementById('game-table-container'); if (!board) return; board.classList.remove('shake-fx'); void board.offsetWidth; board.style.setProperty('--shake-intensity', `${intensity}px`); board.classList.add('shake-fx'); setTimeout(() => { board.classList.remove('shake-fx'); }, 400); } // --- 3D CARD PLAY & DRAW ANIMATIONS --- function getElementCenter(elem) { if (!elem) return { x: window.innerWidth / 2, y: window.innerHeight / 2 }; const rect = elem.getBoundingClientRect(); return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; } function getPositionCoords(position) { const w = window.innerWidth; const h = window.innerHeight; switch (position) { case 'left': case 'slot-left': return { x: w * 0.12, y: h * 0.45 }; case 'top': case 'slot-top': return { x: w * 0.50, y: h * 0.14 }; case 'right': case 'slot-right': return { x: w * 0.88, y: h * 0.45 }; case 'bottom': case 'slot-bottom': default: return { x: w * 0.50, y: h * 0.82 }; } } function animateCardPlay(sourcePosition, cardInfo) { return new Promise((resolve) => { const container = document.getElementById('game-table-container') || document.body; const discardTargetElem = document.querySelector('.discard-pile-container'); const targetPos = discardTargetElem ? getElementCenter(discardTargetElem) : { x: window.innerWidth / 2 + 50, y: window.innerHeight / 2 }; const startPos = getPositionCoords(sourcePosition); // Vytvoríme lietajúcu kartu const flyingCard = document.createElement('div'); flyingCard.className = 'game-card marias-card flying-card-anim'; flyingCard.style.position = 'fixed'; flyingCard.style.left = `${startPos.x}px`; flyingCard.style.top = `${startPos.y}px`; flyingCard.style.transform = 'translate(-50%, -50%)'; flyingCard.style.zIndex = '999'; flyingCard.style.pointerEvents = 'none'; flyingCard.style.boxShadow = '0 20px 40px rgba(0,0,0,0.8), 0 0 30px rgba(245, 176, 65, 0.9)'; const deck = cardInfo.deck || 'original'; const imgPath = (cardInfo.suit && cardInfo.rank) ? `assets/cards/${deck}/${cardInfo.suit}_${cardInfo.rank}.webp` : `assets/cards/${deck}/Card_Back.webp`; flyingCard.innerHTML = ``; container.appendChild(flyingCard); const dx = targetPos.x - startPos.x; const dy = targetPos.y - startPos.y; const randomRotation = (Math.random() - 0.5) * 16; // -8 to +8 deg landing tilt const animation = flyingCard.animate([ { transform: 'translate(-50%, -50%) translate3d(0, 0, 0) scale(0.85) rotate(0deg)', opacity: 0.9 }, { transform: `translate(-50%, -50%) translate3d(${dx * 0.45}px, ${dy * 0.45 - 60}px, 0) scale(1.35) rotate(${randomRotation * 0.5}deg)`, opacity: 1, offset: 0.5 }, { transform: `translate(-50%, -50%) translate3d(${dx}px, ${dy}px, 0) scale(1) rotate(${randomRotation}deg)`, opacity: 1 } ], { duration: Math.round(420 / speedMultiplier), easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.2)' }); animation.onfinish = () => { playCardSnap(); triggerImpactRipple(targetPos.x, targetPos.y); flyingCard.remove(); resolve(); }; }); } function animateCardDraw(targetPosition, deck = 'original') { return new Promise((resolve) => { const container = document.getElementById('game-table-container') || document.body; const deckElem = document.querySelector('.deck-card-container'); const startPos = deckElem ? getElementCenter(deckElem) : { x: window.innerWidth / 2 - 50, y: window.innerHeight / 2 }; const targetPos = getPositionCoords(targetPosition); const flyingBack = document.createElement('div'); flyingBack.className = 'game-card marias-card flying-card-anim'; flyingBack.style.position = 'fixed'; flyingBack.style.left = `${startPos.x}px`; flyingBack.style.top = `${startPos.y}px`; flyingBack.style.transform = 'translate(-50%, -50%)'; flyingBack.style.zIndex = '999'; flyingBack.style.pointerEvents = 'none'; flyingBack.style.boxShadow = '0 10px 25px rgba(0,0,0,0.7)'; flyingBack.innerHTML = ``; container.appendChild(flyingBack); playCardDraw(); const dx = targetPos.x - startPos.x; const dy = targetPos.y - startPos.y; const animation = flyingBack.animate([ { transform: 'translate(-50%, -50%) translate3d(0, 0, 0) scale(1) rotate(0deg)', opacity: 1 }, { transform: `translate(-50%, -50%) translate3d(${dx * 0.5}px, ${dy * 0.5 - 30}px, 0) scale(1.15) rotate(10deg)`, opacity: 1, offset: 0.5 }, { transform: `translate(-50%, -50%) translate3d(${dx}px, ${dy}px, 0) scale(0.6) rotate(0deg)`, opacity: 0.2 } ], { duration: Math.round(380 / speedMultiplier), easing: 'cubic-bezier(0.25, 1, 0.5, 1)' }); animation.onfinish = () => { flyingBack.remove(); resolve(); }; }); } function triggerImpactRipple(x, y) { if (!particleCanvas) return; for (let i = 0; i < 12; i++) { const angle = (i / 12) * Math.PI * 2; const speed = 2 + Math.random() * 3; particles.push({ x: x, y: y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, gravity: 0, size: 3 + Math.random() * 3, color: '#ffd700', shape: 'circle', life: 0.6, decay: 0.04, rotation: 0 }); } } // --- TURN TRANSITION ANIMATION --- function getSlotElement(pos) { if (!pos) return null; const normalized = pos.replace('slot-', ''); if (normalized === 'bottom') { return document.querySelector('.slot-bottom .avatar-badge') || document.querySelector('.slot-bottom'); } return document.querySelector('.slot-' + normalized + ' .avatar-badge') || document.querySelector('.slot-' + normalized); } function playTurnPassSound() { if (isMuted) return; const ctx = getAudioContext(); if (!ctx) return; const now = ctx.currentTime; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(540, now); osc.frequency.exponentialRampToValueAtTime(820, now + 0.11); gain.gain.setValueAtTime(0.07, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.13); osc.connect(gain); gain.connect(ctx.destination); osc.start(now); osc.stop(now + 0.14); } function animateTurnTransition(fromPosition, toPosition) { if (!fromPosition || !toPosition || fromPosition === toPosition) return Promise.resolve(); return new Promise((resolve) => { const fromElem = getSlotElement(fromPosition); const toElem = getSlotElement(toPosition); const startPos = fromElem ? getElementCenter(fromElem) : getPositionCoords(fromPosition); const endPos = toElem ? getElementCenter(toElem) : getPositionCoords(toPosition); const container = document.getElementById('game-table-container') || document.body; const orb = document.createElement('div'); orb.className = 'turn-transfer-orb'; orb.style.left = `${startPos.x}px`; orb.style.top = `${startPos.y}px`; container.appendChild(orb); const midX = (startPos.x + endPos.x) / 2; const midY = Math.min(startPos.y, endPos.y) - 35; const duration = Math.round(380 / speedMultiplier); const startTime = performance.now(); function step(currentTime) { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); const t = 1 - Math.pow(1 - progress, 3); // Ease out cubic // Quadratic Bezier curve const currentX = (1 - t) * (1 - t) * startPos.x + 2 * (1 - t) * t * midX + t * t * endPos.x; const currentY = (1 - t) * (1 - t) * startPos.y + 2 * (1 - t) * t * midY + t * t * endPos.y; orb.style.left = `${currentX}px`; orb.style.top = `${currentY}px`; if (particleCanvas && Math.random() < 0.4) { particles.push({ x: currentX, y: currentY, vx: (Math.random() - 0.5) * 1.5, vy: (Math.random() - 0.5) * 1.5, gravity: 0, size: 3 + Math.random() * 3, color: Math.random() > 0.4 ? '#ffd700' : '#f5b041', shape: 'spark', life: 0.6, decay: 0.04, rotation: Math.random() * Math.PI }); } if (progress < 1) { requestAnimationFrame(step); } else { orb.remove(); if (toElem) { toElem.classList.remove('turn-arrived-pulse'); void toElem.offsetWidth; toElem.classList.add('turn-arrived-pulse'); setTimeout(() => toElem.classList.remove('turn-arrived-pulse'), 600); } if (particleCanvas) { for (let i = 0; i < 8; i++) { const angle = Math.random() * Math.PI * 2; const spd = 1.5 + Math.random() * 2.5; particles.push({ x: endPos.x, y: endPos.y, vx: Math.cos(angle) * spd, vy: Math.sin(angle) * spd, gravity: 0.02, size: 3 + Math.random() * 3, color: '#ffd700', shape: 'spark', life: 0.7, decay: 0.035, rotation: Math.random() * Math.PI }); } } playTurnPassSound(); resolve(); } } requestAnimationFrame(step); }); } // --- PARTICLE ENGINE --- function initParticles(canvasId) { particleCanvas = document.getElementById(canvasId); if (!particleCanvas) return; particleCtx = particleCanvas.getContext('2d'); function resize() { if (particleCanvas) { particleCanvas.width = window.innerWidth; particleCanvas.height = window.innerHeight; } } window.addEventListener('resize', resize); resize(); if (!animationFrameId) { renderParticles(); } } function renderParticles() { if (particleCtx && particleCanvas) { particleCtx.clearRect(0, 0, particleCanvas.width, particleCanvas.height); for (let i = particles.length - 1; i >= 0; i--) { const p = particles[i]; p.x += p.vx; p.y += p.vy; p.vy += p.gravity || 0; p.rotation += p.vRot || 0; p.life -= p.decay || 0.02; if (p.life <= 0) { particles.splice(i, 1); continue; } particleCtx.save(); particleCtx.translate(p.x, p.y); particleCtx.rotate(p.rotation); particleCtx.globalAlpha = Math.max(0, p.life); if (p.shape === 'circle') { particleCtx.fillStyle = p.color; particleCtx.beginPath(); particleCtx.arc(0, 0, p.size, 0, Math.PI * 2); particleCtx.fill(); } else if (p.shape === 'rect') { particleCtx.fillStyle = p.color; particleCtx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size * (p.aspect || 1)); } else if (p.shape === 'spark') { particleCtx.strokeStyle = p.color; particleCtx.lineWidth = p.size; particleCtx.beginPath(); particleCtx.moveTo(-p.vx * 3, -p.vy * 3); particleCtx.lineTo(0, 0); particleCtx.stroke(); } particleCtx.restore(); } } animationFrameId = requestAnimationFrame(renderParticles); } function triggerFireParticles(x, y, count = 25) { if (!particleCanvas) return; const colors = ['#ff4500', '#ff8c00', '#ffd700', '#ffffff']; for (let i = 0; i < count; i++) { const angle = -Math.PI / 2 + (Math.random() - 0.5) * 1.5; const speed = 3 + Math.random() * 6; particles.push({ x: x || window.innerWidth / 2, y: y || window.innerHeight / 2, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, gravity: -0.05, size: 4 + Math.random() * 6, color: colors[Math.floor(Math.random() * colors.length)], shape: 'circle', life: 1.0, decay: 0.03 + Math.random() * 0.03, rotation: 0 }); } } function triggerIceParticles(x, y, count = 30) { if (!particleCanvas) return; const colors = ['#00ffff', '#e0ffff', '#87cefa', '#ffffff']; for (let i = 0; i < count; i++) { const angle = Math.random() * Math.PI * 2; const speed = 2 + Math.random() * 7; particles.push({ x: x || window.innerWidth / 2, y: y || window.innerHeight / 2, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, gravity: 0.1, size: 3 + Math.random() * 5, color: colors[Math.floor(Math.random() * colors.length)], shape: 'spark', life: 1.0, decay: 0.02 + Math.random() * 0.02, rotation: Math.random() * Math.PI }); } } function triggerIceShatter(position) { if (!particleCanvas) return; let x = window.innerWidth / 2; let y = window.innerHeight / 2; if (typeof position === 'string') { const elem = getSlotElement(position); const coords = elem ? getElementCenter(elem) : getPositionCoords(position); x = coords.x; y = coords.y; } else if (position && position.x && position.y) { x = position.x; y = position.y; } playIceCrack(); const colors = ['#00ffff', '#80d8ff', '#e0f7fa', '#ffffff', '#b2ebf2', '#87cefa']; for (let i = 0; i < 40; i++) { const angle = Math.random() * Math.PI * 2; const speed = 3 + Math.random() * 10; particles.push({ x: x + (Math.random() - 0.5) * 50, y: y + (Math.random() - 0.5) * 40, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 2.5, gravity: 0.22, size: 6 + Math.random() * 9, aspect: 0.35 + Math.random() * 1.5, color: colors[Math.floor(Math.random() * colors.length)], shape: Math.random() > 0.35 ? 'rect' : 'spark', life: 1.1, decay: 0.022 + Math.random() * 0.02, rotation: Math.random() * Math.PI * 2, vRot: (Math.random() - 0.5) * 0.4 }); } } function triggerLeafParticles(x, y, count = 35) { if (!particleCanvas) return; const startX = x || window.innerWidth / 2; const startY = y || window.innerHeight / 2; const colors = ['#2d6a4f', '#52b788', '#74c69d', '#b7e4c7', '#d8f3dc', '#ffd700']; for (let i = 0; i < count; i++) { const angle = Math.random() * Math.PI * 2; const speed = 2.5 + Math.random() * 8; particles.push({ x: startX + (Math.random() - 0.5) * 30, y: startY + (Math.random() - 0.5) * 30, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 1.5, gravity: 0.12, size: 5 + Math.random() * 7, aspect: 0.4 + Math.random() * 1.5, color: colors[Math.floor(Math.random() * colors.length)], shape: 'spark', life: 1.0, decay: 0.025 + Math.random() * 0.02, rotation: Math.random() * Math.PI * 2, vRot: (Math.random() - 0.5) * 0.3 }); } } function triggerBurnExplosion(x, y) { if (!particleCanvas) return; const colors = ['#ff0000', '#ff4500', '#ffaa00', '#333333', '#ffffff']; for (let i = 0; i < 60; i++) { const angle = Math.random() * Math.PI * 2; const speed = 4 + Math.random() * 10; particles.push({ x: x || window.innerWidth / 2, y: y || window.innerHeight / 2, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, gravity: 0.15, size: 5 + Math.random() * 8, color: colors[Math.floor(Math.random() * colors.length)], shape: Math.random() > 0.5 ? 'rect' : 'circle', aspect: 1 + Math.random() * 2, life: 1.0, decay: 0.02 + Math.random() * 0.02, rotation: Math.random() * Math.PI, vRot: (Math.random() - 0.5) * 0.2 }); } } function triggerConfetti() { if (!particleCanvas) return; const colors = ['#ffd700', '#ff4500', '#00ffcc', '#ff007f', '#ffffff', '#7b2cbf']; for (let i = 0; i < 150; i++) { const x = Math.random() * window.innerWidth; const y = -20 - Math.random() * 50; particles.push({ x: x, y: y, vx: (Math.random() - 0.5) * 4, vy: 3 + Math.random() * 5, gravity: 0.08, size: 8 + Math.random() * 6, aspect: 0.6 + Math.random() * 0.8, color: colors[Math.floor(Math.random() * colors.length)], shape: 'rect', life: 1.0, decay: 0.005 + Math.random() * 0.005, rotation: Math.random() * Math.PI * 2, vRot: (Math.random() - 0.5) * 0.15 }); } } // --- DOPAMINE PROCEDURAL AUDIO --- function playSelectStep(step = 1) { if (isMuted) return; const ctx = getAudioContext(); if (!ctx) return; const now = ctx.currentTime; // Pentatonic rising scale (C5, E5, G5, C6, E6) const freqs = [523.25, 659.25, 783.99, 1046.50, 1318.51]; const freq = freqs[Math.min(step - 1, freqs.length - 1)] || 523.25; const osc = ctx.createOscillator(); const harm = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'triangle'; osc.frequency.setValueAtTime(freq, now); osc.frequency.exponentialRampToValueAtTime(freq * 1.02, now + 0.12); harm.type = 'sine'; harm.frequency.setValueAtTime(freq * 2, now); gain.gain.setValueAtTime(0.28, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.14); osc.connect(gain); harm.connect(gain); gain.connect(ctx.destination); osc.start(now); harm.start(now); osc.stop(now + 0.15); harm.stop(now + 0.15); } function playComboSlam(count = 2) { if (isMuted) return; const ctx = getAudioContext(); if (!ctx) return; const now = ctx.currentTime; // Heavy 808 Sub-bass drop const sub = ctx.createOscillator(); const subGain = ctx.createGain(); sub.type = 'sine'; sub.frequency.setValueAtTime(140 + count * 20, now); sub.frequency.exponentialRampToValueAtTime(32, now + 0.45); subGain.gain.setValueAtTime(0.7, now); subGain.gain.exponentialRampToValueAtTime(0.001, now + 0.45); sub.connect(subGain); subGain.connect(ctx.destination); sub.start(now); sub.stop(now + 0.46); // Metallic punch noise const bufferSize = ctx.sampleRate * 0.12; const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (ctx.sampleRate * 0.03)); } const noise = ctx.createBufferSource(); noise.buffer = buffer; const filter = ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.setValueAtTime(1800, now); filter.Q.setValueAtTime(4, now); const noiseGain = ctx.createGain(); noiseGain.gain.setValueAtTime(0.5, now); noiseGain.gain.exponentialRampToValueAtTime(0.001, now + 0.12); noise.connect(filter); filter.connect(noiseGain); noiseGain.connect(ctx.destination); noise.start(now); // Escalating screen shake triggerScreenShake(count >= 4 ? 16 : (count === 3 ? 10 : 6)); } function playJackpotCoins() { if (isMuted) return; const ctx = getAudioContext(); if (!ctx) return; const now = ctx.currentTime; const notes = [987.77, 1174.66, 1318.51, 1567.98, 1760.00, 2093.00, 2349.32, 2793.83]; notes.forEach((freq, idx) => { const time = now + idx * 0.06; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(freq, time); gain.gain.setValueAtTime(0.22, time); gain.gain.exponentialRampToValueAtTime(0.001, time + 0.22); osc.connect(gain); gain.connect(ctx.destination); osc.start(time); osc.stop(time + 0.23); }); } function playReviveSound() { if (isMuted) return; const ctx = getAudioContext(); if (!ctx) return; const now = ctx.currentTime; const chord = [440, 554.37, 659.25, 880, 1108.73]; chord.forEach((freq, i) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(freq, now + i * 0.04); osc.frequency.linearRampToValueAtTime(freq * 1.5, now + 0.5); const filter = ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.setValueAtTime(800, now); filter.frequency.exponentialRampToValueAtTime(3500, now + 0.4); gain.gain.setValueAtTime(0.25, now + i * 0.04); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.6); osc.connect(filter); filter.connect(gain); gain.connect(ctx.destination); osc.start(now + i * 0.04); osc.stop(now + 0.62); }); triggerScreenShake(8); } // --- DOPAMINE BANNER SYSTEM --- function triggerDopamineBanner(title, subtitle = '', type = 'combo') { const container = document.getElementById('game-table-container') || document.body; // Remove existing banner if any const oldBanner = document.querySelector('.dopamine-banner'); if (oldBanner) oldBanner.remove(); const banner = document.createElement('div'); banner.className = `dopamine-banner dopamine-${type}`; banner.innerHTML = `
${title}
${subtitle ? `
${subtitle}
` : ''}
`; container.appendChild(banner); // Sound & Particles based on type if (type === 'quad' || type === 'mega') { playComboSlam(4); triggerGoldStarExplosion(); triggerConfetti(); } else if (type === 'triple') { playComboSlam(3); triggerFireParticles(window.innerWidth / 2, window.innerHeight / 2, 45); } else if (type === 'double') { playComboSlam(2); triggerLightningSparks(); } else if (type === 'freeze') { playAceFreeze(); triggerIceParticles(window.innerWidth / 2, window.innerHeight / 2, 50); } else if (type === 'revive') { playReviveSound(); triggerLightningSparks(window.innerWidth / 2, window.innerHeight / 2, 60); } else if (type === 'jackpot') { playJackpotCoins(); triggerCoinJackpot(); triggerConfetti(); } // Auto remove banner after animation setTimeout(() => { banner.classList.add('dopamine-fadeout'); setTimeout(() => banner.remove(), 400); }, 1800); } // --- ADVANCED PARTICLES --- function triggerCoinJackpot(x, y, count = 70) { if (!particleCanvas) return; const startX = x || window.innerWidth / 2; const startY = y || window.innerHeight / 2; for (let i = 0; i < count; i++) { const angle = -Math.PI / 2 + (Math.random() - 0.5) * 2.2; const speed = 6 + Math.random() * 12; particles.push({ x: startX, y: startY, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, gravity: 0.28, size: 10 + Math.random() * 6, aspect: 0.5 + Math.random() * 0.5, color: Math.random() > 0.3 ? '#ffd700' : '#f5b041', shape: 'rect', life: 1.2, decay: 0.012 + Math.random() * 0.008, rotation: Math.random() * Math.PI * 2, vRot: (Math.random() - 0.5) * 0.25 }); } } function triggerLightningSparks(x, y, count = 40) { if (!particleCanvas) return; const colors = ['#00e5ff', '#ffd700', '#ffffff', '#e040fb']; const startX = x || window.innerWidth / 2; const startY = y || window.innerHeight / 2; for (let i = 0; i < count; i++) { const angle = Math.random() * Math.PI * 2; const speed = 4 + Math.random() * 10; particles.push({ x: startX, y: startY, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, gravity: 0, size: 3 + Math.random() * 4, color: colors[Math.floor(Math.random() * colors.length)], shape: 'spark', life: 0.8, decay: 0.035, rotation: Math.random() * Math.PI }); } } function triggerGoldStarExplosion(x, y, count = 60) { if (!particleCanvas) return; const startX = x || window.innerWidth / 2; const startY = y || window.innerHeight / 2; for (let i = 0; i < count; i++) { const angle = Math.random() * Math.PI * 2; const speed = 3 + Math.random() * 11; particles.push({ x: startX, y: startY, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, gravity: 0.06, size: 6 + Math.random() * 6, color: '#ffd700', shape: 'circle', life: 1.1, decay: 0.02, rotation: 0 }); } } function toggleMute() { isMuted = !isMuted; return isMuted; } return { playCardSnap, playCardDraw, playSevenStack, playAceFreeze, playIceCrack, playLeafShield, playHornikChime, playBurnExplosion, playVictory, playHeartbeat, playSelectStep, playComboSlam, playJackpotCoins, playReviveSound, triggerDopamineBanner, triggerScreenShake, initParticles, triggerFireParticles, triggerIceParticles, triggerIceShatter, triggerLeafParticles, triggerBurnExplosion, triggerConfetti, triggerCoinJackpot, triggerLightningSparks, triggerGoldStarExplosion, animateCardPlay, animateCardDraw, animateTurnTransition, setSpeedLevel, toggleMute }; })();