dopamine effects

This commit is contained in:
Michal Vrba
2026-08-28 16:57:34 +02:00
parent 02bca7c34a
commit 16bc48ab34
5 changed files with 489 additions and 8 deletions
@@ -1,4 +1,5 @@
@namespace Faraon.Client.Components
@inject FxService Fx
<div class="player-hand-container">
@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();
}
+34 -2
View File
@@ -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<Card> { decision.Card } : new List<Card>());
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);
+48
View File
@@ -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<bool> ToggleMuteAsync()
{
try { return await _js.InvokeAsync<bool>("FaraonFx.toggleMute"); } catch { return false; }
}
}
+124 -4
View File
@@ -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 {
+274
View File
@@ -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 = `
<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;
@@ -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
};
})();