// Faraón Game FX Engine: Procedural Audio, Particles, Screen Shake & Animations window.FaraonFx = (function () { let audioCtx = null; let isMuted = false; let particleCanvas = null; let particleCtx = null; let particles = []; let animationFrameId = null; 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 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: 420, 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: 380, 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 }); } } // --- 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 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 }); } } function toggleMute() { isMuted = !isMuted; return isMuted; } return { playCardSnap, playCardDraw, playSevenStack, playAceFreeze, playHornikChime, playBurnExplosion, playVictory, playHeartbeat, triggerScreenShake, initParticles, triggerFireParticles, triggerIceParticles, triggerBurnExplosion, triggerConfetti, animateCardPlay, animateCardDraw, toggleMute }; })();