// 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
});
}
}
// --- 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 = `