diff --git a/Faraon.Client/Components/PlayerHandView.razor b/Faraon.Client/Components/PlayerHandView.razor index f38c077..177a3d3 100644 --- a/Faraon.Client/Components/PlayerHandView.razor +++ b/Faraon.Client/Components/PlayerHandView.razor @@ -1,4 +1,5 @@ @namespace Faraon.Client.Components +@inject FxService Fx
@if (SelectedCards.Count > 0 && IsMyTurn) @@ -171,6 +172,7 @@ // Ak má viac kariet rovnakej hodnoty -> označíme túto ako 1. a vysunú sa všetky karty danej hodnoty! SelectedCards.Add(card); + await Fx.PlaySelectStepAsync(1); StateHasChanged(); return; } @@ -193,6 +195,7 @@ } SelectedCards.Add(card); + await Fx.PlaySelectStepAsync(1); StateHasChanged(); return; } @@ -209,25 +212,28 @@ { // Odznačíme kartu zo zoznamu SelectedCards.Remove(card); + await Fx.PlaySelectStepAsync(Math.Max(1, SelectedCards.Count)); } } else { // Pridáme do poradia ako 2., 3., 4. kartu SelectedCards.Add(card); + await Fx.PlaySelectStepAsync(SelectedCards.Count); } StateHasChanged(); } - private void ReverseSelectionOrder() + private async Task ReverseSelectionOrder() { if (SelectedCards.Count <= 1) return; SelectedCards.Reverse(); + await Fx.PlaySelectStepAsync(SelectedCards.Count); StateHasChanged(); } - private void SelectAllOfRank() + private async Task SelectAllOfRank() { if (SelectedCards.Count == 0) return; var rank = SelectedCards[0].Rank; @@ -238,6 +244,7 @@ SelectedCards.Add(c); } } + await Fx.PlaySelectStepAsync(SelectedCards.Count); StateHasChanged(); } diff --git a/Faraon.Client/Pages/Home.razor b/Faraon.Client/Pages/Home.razor index 2dc533e..58502d1 100644 --- a/Faraon.Client/Pages/Home.razor +++ b/Faraon.Client/Pages/Home.razor @@ -283,20 +283,26 @@ else break; case GameEventType.SevenStacked: await Fx.PlaySevenStackAsync(Math.Max(1, e.ExtraData / 2)); + await Fx.TriggerDopamineBannerAsync($"🔥 +{e.ExtraData} KARIET!", "Trestná reťaz sediem", "triple"); break; case GameEventType.AceSkipped: case GameEventType.AceCountered: - await Fx.PlayAceFreezeAsync(); + await Fx.TriggerFreezeEffectAsync(); + break; + case GameEventType.PlayerRevived: + await Fx.TriggerReviveEffectAsync(e.TargetPlayer?.Name ?? e.SourcePlayer?.Name ?? "Hráč"); break; case GameEventType.SuitChanged: await Fx.PlayHornikChimeAsync(); break; case GameEventType.CardsBurned: await Fx.PlayBurnExplosionAsync(); + await Fx.TriggerDopamineBannerAsync("💥 SPÁLENÉ!", "Zhoda 4 kariet na stole", "quad"); break; case GameEventType.PlayerFinished: + case GameEventType.MatchWon: case GameEventType.GameOver: - await Fx.PlayVictoryAsync(); + await Fx.TriggerJackpotWinAsync(e.SourcePlayer?.Name ?? "Víťaz"); break; } @@ -349,6 +355,19 @@ else string pos = GetPlayerPosition(ViewingPlayer); string deck = Engine.State.Rules.SelectedDeck; + if (cards.Count == 2) + { + _ = Fx.TriggerComboSlamAsync(2, "⚡ DVOJIČKA!"); + } + else if (cards.Count == 3) + { + _ = Fx.TriggerComboSlamAsync(3, "🔥 TROJITÝ ÚDER!"); + } + else if (cards.Count >= 4) + { + _ = Fx.TriggerComboSlamAsync(4, "👑 KVADRA / MEGA COMBO!"); + } + foreach (var card in cards) { _ = Fx.AnimateCardPlayAsync(pos, card, deck); @@ -466,6 +485,19 @@ else var cardsToPlay = decision.Cards ?? (decision.Card != null ? new List { decision.Card } : new List()); if (cardsToPlay.Count > 0) { + if (cardsToPlay.Count == 2) + { + _ = Fx.TriggerComboSlamAsync(2, $"⚡ {aiPlayer.Name}: DVOJIČKA!"); + } + else if (cardsToPlay.Count == 3) + { + _ = Fx.TriggerComboSlamAsync(3, $"🔥 {aiPlayer.Name}: TROJITÝ ÚDER!"); + } + else if (cardsToPlay.Count >= 4) + { + _ = Fx.TriggerComboSlamAsync(4, $"👑 {aiPlayer.Name}: KVADRA!"); + } + foreach (var c in cardsToPlay) { _ = Fx.AnimateCardPlayAsync(pos, c, deck); diff --git a/Faraon.Client/Services/FxService.cs b/Faraon.Client/Services/FxService.cs index 29c619c..29a76c9 100644 --- a/Faraon.Client/Services/FxService.cs +++ b/Faraon.Client/Services/FxService.cs @@ -114,9 +114,57 @@ public class FxService catch { } } + public async ValueTask PlaySelectStepAsync(int step) + { + try { await _js.InvokeVoidAsync("FaraonFx.playSelectStep", step); } catch { } + } + + public async ValueTask TriggerComboSlamAsync(int count, string title, string sub = "") + { + try + { + string type = count >= 4 ? "quad" : (count == 3 ? "triple" : "double"); + await _js.InvokeVoidAsync("FaraonFx.triggerDopamineBanner", title, sub, type); + } + catch { } + } + + public async ValueTask TriggerFreezeEffectAsync() + { + try + { + await _js.InvokeVoidAsync("FaraonFx.triggerDopamineBanner", "❄️ ZMRAZENÝ!", "Stojíš jedno kolo", "freeze"); + } + catch { } + } + + public async ValueTask TriggerReviveEffectAsync(string playerName) + { + try + { + await _js.InvokeVoidAsync("FaraonFx.triggerDopamineBanner", "⚡ VSTÁVAJ Z MŔTVYCH!", $"{playerName} je späť v hre!", "revive"); + } + catch { } + } + + public async ValueTask TriggerJackpotWinAsync(string winnerName) + { + try + { + await _js.InvokeVoidAsync("FaraonFx.triggerDopamineBanner", "🏆 FARAÓN!", $"{winnerName} vyhráva hru!", "jackpot"); + } + catch { } + } + + public async ValueTask TriggerDopamineBannerAsync(string title, string subtitle, string type) + { + try { await _js.InvokeVoidAsync("FaraonFx.triggerDopamineBanner", title, subtitle, type); } catch { } + } + public async ValueTask ToggleMuteAsync() { try { return await _js.InvokeAsync("FaraonFx.toggleMute"); } catch { return false; } } } + diff --git a/Faraon.Client/wwwroot/css/app.css b/Faraon.Client/wwwroot/css/app.css index f4ec8e8..75cec8e 100644 --- a/Faraon.Client/wwwroot/css/app.css +++ b/Faraon.Client/wwwroot/css/app.css @@ -1417,19 +1417,139 @@ body, html { /* Event Ticker Message */ .event-ticker { position: absolute; - bottom: 210px; + bottom: calc(var(--card-height) + 65px); left: 50%; transform: translateX(-50%); - background: rgba(0, 0, 0, 0.7); - border: 1px solid rgba(245, 176, 65, 0.4); + background: rgba(0, 0, 0, 0.75); + border: 1px solid rgba(245, 176, 65, 0.5); color: #fff; font-weight: 600; font-size: 0.9rem; - padding: 5px 16px; + padding: 6px 18px; border-radius: 20px; pointer-events: none; z-index: 40; transition: opacity 0.3s ease; + backdrop-filter: blur(6px); +} + +/* --- DOPAMINE POPUP BANNER --- */ +.dopamine-banner { + position: absolute; + top: 38%; + left: 50%; + transform: translate(-50%, -50%) scale(0.6); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 500; + pointer-events: none; + animation: dopamine-pop-in 0.45s cubic-bezier(0.175, 0.885, 0.32, 1.35) forwards; +} + +.dopamine-content { + position: relative; + padding: 12px 32px; + border-radius: 40px; + background: rgba(10, 15, 25, 0.92); + border: 3px solid #ffd700; + box-shadow: 0 0 50px rgba(255, 215, 0, 0.6), inset 0 0 25px rgba(255, 215, 0, 0.3); + text-align: center; + backdrop-filter: blur(12px); +} + +.dopamine-title { + font-family: 'Cinzel', serif; + font-size: 2.2rem; + font-weight: 900; + letter-spacing: 2px; + color: #fff; + text-shadow: 0 0 20px #ffd700, 0 4px 10px rgba(0,0,0,0.8); + white-space: nowrap; +} + +.dopamine-sub { + font-size: 1.1rem; + font-weight: 800; + color: #ffd700; + letter-spacing: 1px; + margin-top: 2px; + text-shadow: 0 2px 6px rgba(0,0,0,0.9); +} + +/* Types */ +.dopamine-double .dopamine-content { + border-color: #00e5ff; + box-shadow: 0 0 45px rgba(0, 229, 255, 0.7), inset 0 0 20px rgba(0, 229, 255, 0.3); +} +.dopamine-double .dopamine-title { + text-shadow: 0 0 25px #00e5ff, 0 0 40px #00e5ff; +} + +.dopamine-triple .dopamine-content { + border-color: #ff4500; + box-shadow: 0 0 55px rgba(255, 69, 0, 0.8), inset 0 0 25px rgba(255, 140, 0, 0.4); +} +.dopamine-triple .dopamine-title { + text-shadow: 0 0 30px #ff4500, 0 0 50px #ff8c00; +} + +.dopamine-quad .dopamine-content, .dopamine-mega .dopamine-content { + border-color: #ffd700; + background: linear-gradient(135deg, rgba(30, 20, 5, 0.95), rgba(15, 10, 0, 0.95)); + box-shadow: 0 0 80px rgba(255, 215, 0, 0.9), inset 0 0 35px rgba(255, 215, 0, 0.5); + transform: scale(1.15); +} +.dopamine-quad .dopamine-title { + font-size: 2.7rem; + text-shadow: 0 0 35px #ffd700, 0 0 60px #ff8c00, 0 0 90px #ffd700; +} + +.dopamine-freeze .dopamine-content { + border-color: #00ffff; + box-shadow: 0 0 60px rgba(0, 255, 255, 0.8), inset 0 0 25px rgba(0, 255, 255, 0.4); +} + +.dopamine-revive .dopamine-content { + border-color: #ff0055; + box-shadow: 0 0 65px rgba(255, 0, 85, 0.9), inset 0 0 30px rgba(255, 215, 0, 0.5); +} + +.dopamine-jackpot .dopamine-content { + border-color: #ffd700; + box-shadow: 0 0 90px rgba(255, 215, 0, 1), inset 0 0 40px rgba(255, 215, 0, 0.6); +} + +.dopamine-fadeout { + opacity: 0; + transform: translate(-50%, -70%) scale(1.2); + transition: all 0.35s ease; +} + +@keyframes dopamine-pop-in { + 0% { transform: translate(-50%, -50%) scale(0.3); opacity: 0; } + 60% { transform: translate(-50%, -50%) scale(1.15); opacity: 1; } + 100% { transform: translate(-50%, -50%) scale(1); opacity: 1; } +} + +/* HOLOGRAPHIC FOIL SHINE ON CARDS */ +.game-card.playable::before { + content: ''; + position: absolute; + inset: 0; + border-radius: var(--card-radius); + background: linear-gradient(115deg, transparent 20%, rgba(255, 255, 255, 0.4) 40%, rgba(255, 215, 0, 0.5) 50%, rgba(0, 229, 255, 0.4) 60%, transparent 80%); + background-size: 200% 200%; + animation: holo-shine 2.8s infinite linear; + pointer-events: none; + z-index: 10; + opacity: 0.6; +} + +@keyframes holo-shine { + 0% { background-position: -200% -200%; } + 100% { background-position: 200% 200%; } } @keyframes fadeIn { diff --git a/Faraon.Client/wwwroot/js/gameFx.js b/Faraon.Client/wwwroot/js/gameFx.js index 88c8701..cb0db08 100644 --- a/Faraon.Client/wwwroot/js/gameFx.js +++ b/Faraon.Client/wwwroot/js/gameFx.js @@ -617,6 +617,271 @@ window.FaraonFx = (function () { } } + // --- 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; @@ -631,14 +896,23 @@ window.FaraonFx = (function () { playBurnExplosion, playVictory, playHeartbeat, + playSelectStep, + playComboSlam, + playJackpotCoins, + playReviveSound, + triggerDopamineBanner, triggerScreenShake, initParticles, triggerFireParticles, triggerIceParticles, triggerBurnExplosion, triggerConfetti, + triggerCoinJackpot, + triggerLightningSparks, + triggerGoldStarExplosion, animateCardPlay, animateCardDraw, toggleMute }; })(); +