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
};
})();
+