1394 lines
51 KiB
JavaScript
1394 lines
51 KiB
JavaScript
// 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;
|
|
}
|
|
|
|
// Automatické prebudenie AudioContextu pri prvom kliknutí používateľa kdekoľvek
|
|
if (typeof window !== 'undefined') {
|
|
const unlockAudio = () => {
|
|
getAudioContext();
|
|
window.removeEventListener('pointerdown', unlockAudio);
|
|
window.removeEventListener('keydown', unlockAudio);
|
|
};
|
|
window.addEventListener('pointerdown', unlockAudio, { passive: true });
|
|
window.addEventListener('keydown', unlockAudio, { passive: true });
|
|
}
|
|
|
|
// --- 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);
|
|
});
|
|
}
|
|
|
|
function playCardWhoosh() {
|
|
if (isMuted) return;
|
|
const ctx = getAudioContext();
|
|
if (!ctx) return;
|
|
const now = ctx.currentTime;
|
|
const bufferSize = Math.floor(ctx.sampleRate * 0.22);
|
|
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.sin((i / bufferSize) * Math.PI);
|
|
}
|
|
const noise = ctx.createBufferSource();
|
|
noise.buffer = buffer;
|
|
const filter = ctx.createBiquadFilter();
|
|
filter.type = 'bandpass';
|
|
filter.frequency.setValueAtTime(450, now);
|
|
filter.frequency.exponentialRampToValueAtTime(1200, now + 0.1);
|
|
filter.frequency.exponentialRampToValueAtTime(350, now + 0.22);
|
|
filter.Q.value = 2.5;
|
|
|
|
const gain = ctx.createGain();
|
|
gain.gain.setValueAtTime(0.3, now);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.22);
|
|
|
|
noise.connect(filter);
|
|
filter.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
noise.start(now);
|
|
}
|
|
|
|
function playBotPersonalitySfx(personalityId, emotion) {
|
|
if (isMuted) return;
|
|
const ctx = getAudioContext();
|
|
if (!ctx) return;
|
|
const now = ctx.currentTime;
|
|
const pId = (personalityId || 'fox').toLowerCase();
|
|
const emo = (emotion || 'thinking').toLowerCase();
|
|
|
|
// Mierny posun tóniny podľa emócie
|
|
let pitchMul = 1.0;
|
|
if (emo === 'shocked' || emo === 'desperate') pitchMul = 0.82;
|
|
else if (emo === 'confident' || emo === 'celebrating') pitchMul = 1.22;
|
|
else if (emo === 'freezing') pitchMul = 1.45;
|
|
else if (emo === 'angry') pitchMul = 0.92;
|
|
|
|
switch (pId) {
|
|
case 'fox': { // Lišiak Filip - sly playful rising arpeggio
|
|
const baseFreqs = [440, 554.37, 659.25, 880];
|
|
baseFreqs.forEach((freq, i) => {
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
const t = now + i * 0.045;
|
|
osc.type = 'triangle';
|
|
osc.frequency.setValueAtTime(freq * pitchMul, t);
|
|
gain.gain.setValueAtTime(0.28, t);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.18);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start(t);
|
|
osc.stop(t + 0.19);
|
|
});
|
|
break;
|
|
}
|
|
case 'queen': { // Kráľovná Alžbeta - regal royal chime / bell chords
|
|
const baseFreqs = [523.25, 659.25, 783.99, 1046.50];
|
|
baseFreqs.forEach((freq, i) => {
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
const t = now + i * 0.055;
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(freq * pitchMul, t);
|
|
gain.gain.setValueAtTime(0.32, t);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.42);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start(t);
|
|
osc.stop(t + 0.43);
|
|
});
|
|
break;
|
|
}
|
|
case 'robot': { // Kyber-Robo - 8-bit digital synth crunch
|
|
const tones = [880, 1320, 660, 1760];
|
|
tones.forEach((freq, i) => {
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
const t = now + i * 0.04;
|
|
osc.type = 'square';
|
|
osc.frequency.setValueAtTime(freq * pitchMul, t);
|
|
gain.gain.setValueAtTime(0.22, t);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.06);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start(t);
|
|
osc.stop(t + 0.065);
|
|
});
|
|
break;
|
|
}
|
|
case 'grandpa': { // Dedo Jozef - warm vintage harmonic bell
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(987.77 * pitchMul, now);
|
|
gain.gain.setValueAtTime(0.35, now);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.55);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start(now);
|
|
osc.stop(now + 0.56);
|
|
break;
|
|
}
|
|
case 'cyberkid': { // Hacker Miško - futuristic laser zap
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.type = 'sawtooth';
|
|
osc.frequency.setValueAtTime(1500 * pitchMul, now);
|
|
osc.frequency.exponentialRampToValueAtTime(180 * pitchMul, now + 0.18);
|
|
gain.gain.setValueAtTime(0.3, now);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.18);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start(now);
|
|
osc.stop(now + 0.19);
|
|
break;
|
|
}
|
|
case 'cat': { // Mačka Minda - playful synth meow
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(580 * pitchMul, now);
|
|
osc.frequency.exponentialRampToValueAtTime(980 * pitchMul, now + 0.11);
|
|
osc.frequency.exponentialRampToValueAtTime(680 * pitchMul, now + 0.28);
|
|
gain.gain.setValueAtTime(0.32, now);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.30);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start(now);
|
|
osc.stop(now + 0.31);
|
|
break;
|
|
}
|
|
case 'wizard': { // Čarodejník Baltazár - mystical shimmering chord
|
|
[392.00, 587.33, 880.00, 1174.66].forEach((freq, i) => {
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
const t = now + i * 0.035;
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(freq * pitchMul, t);
|
|
gain.gain.setValueAtTime(0.24, t);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.45);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start(t);
|
|
osc.stop(t + 0.46);
|
|
});
|
|
break;
|
|
}
|
|
case 'bear': { // Medveď Mišo - deep bass growl thud
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.type = 'sawtooth';
|
|
osc.frequency.setValueAtTime(95 * pitchMul, now);
|
|
osc.frequency.exponentialRampToValueAtTime(38 * pitchMul, now + 0.35);
|
|
gain.gain.setValueAtTime(0.42, now);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start(now);
|
|
osc.stop(now + 0.36);
|
|
break;
|
|
}
|
|
default: {
|
|
playCardSnap();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 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 = `<img src="${imgPath}" class="marias-card-img" />`;
|
|
|
|
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 = `<img src="assets/cards/${deck || 'original'}/Card_Back.webp" class="marias-card-img" />`;
|
|
|
|
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 = `
|
|
<div class="dopamine-halo"></div>
|
|
<div class="dopamine-content">
|
|
<div class="dopamine-title">${title}</div>
|
|
${subtitle ? `<div class="dopamine-sub">${subtitle}</div>` : ''}
|
|
</div>
|
|
`;
|
|
|
|
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,
|
|
playCardWhoosh,
|
|
playBotPersonalitySfx,
|
|
setSpeedLevel,
|
|
toggleMute
|
|
};
|
|
})();
|
|
|