+ @if (IsCurrentTurn && !Player.HasFinished && !Player.IsEliminated)
+ {
+
+
+ NA ŤAHU
+
+ }
+
@if (Player.HasFinished)
{
diff --git a/Faraon.Client/Pages/Home.razor b/Faraon.Client/Pages/Home.razor
index 58502d1..9bd04c1 100644
--- a/Faraon.Client/Pages/Home.razor
+++ b/Faraon.Client/Pages/Home.razor
@@ -24,9 +24,9 @@ else
}
-
+
@@ -98,13 +98,20 @@ else
-
+
+ @if (IsViewingPlayerTurn && !ViewingPlayer.HasFinished && !ViewingPlayer.IsEliminated)
+ {
+
+
+ SI NA ŤAHU
+
+ }
@ViewingPlayer.Avatar
@ViewingPlayer.Name
-
(@ViewingPlayer.Hand.Count kariet)
+
(@ViewingPlayer.Hand.Count @(ViewingPlayer.Hand.Count == 1 ? "karta" : (ViewingPlayer.Hand.Count < 5 ? "karty" : "kariet")))
@if (Engine.State.Rules.OlympicMode)
{
@@ -211,6 +218,7 @@ else
private List ConfiguredPlayers = new();
private RuleOptions ConfiguredRules = new();
+ private Player? _previousTurnPlayer;
private Player ViewingPlayer
{
@@ -260,6 +268,7 @@ else
Engine.OnGameEvent += HandleGameEvent;
Engine.StartGame(gamePlayers, ConfiguredRules);
+ _previousTurnPlayer = Engine.State.CurrentPlayer;
IsInLobby = false;
LastEventMessage = "Hra začala! Veľa šťastia.";
@@ -282,7 +291,7 @@ else
await Fx.PlayCardDrawAsync();
break;
case GameEventType.SevenStacked:
- await Fx.PlaySevenStackAsync(Math.Max(1, e.ExtraData / 2));
+ await Fx.PlaySevenStackAsync(Math.Max(1, e.ExtraData / 3));
await Fx.TriggerDopamineBannerAsync($"🔥 +{e.ExtraData} KARIET!", "Trestná reťaz sediem", "triple");
break;
case GameEventType.AceSkipped:
@@ -297,7 +306,6 @@ else
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:
@@ -311,10 +319,27 @@ else
await Fx.PlayHeartbeatAsync();
}
+ await CheckTurnTransitionAsync();
StateHasChanged();
});
}
+ private async Task CheckTurnTransitionAsync()
+ {
+ if (Engine == null || Engine.State.Players.Count == 0 || Engine.State.IsGameOver) return;
+ var current = Engine.State.CurrentPlayer;
+ if (_previousTurnPlayer != null && _previousTurnPlayer != current)
+ {
+ var prev = _previousTurnPlayer;
+ _previousTurnPlayer = current;
+ await Fx.AnimateTurnTransitionAsync(GetPlayerPosition(prev), GetPlayerPosition(current));
+ }
+ else
+ {
+ _previousTurnPlayer = current;
+ }
+ }
+
private string GetPlayerPosition(Player player)
{
if (player == ViewingPlayer) return "bottom";
diff --git a/Faraon.Client/Services/FxService.cs b/Faraon.Client/Services/FxService.cs
index 29a76c9..7b9b308 100644
--- a/Faraon.Client/Services/FxService.cs
+++ b/Faraon.Client/Services/FxService.cs
@@ -114,6 +114,15 @@ public class FxService
catch { }
}
+ public async ValueTask AnimateTurnTransitionAsync(string fromPosition, string toPosition)
+ {
+ try
+ {
+ await _js.InvokeVoidAsync("FaraonFx.animateTurnTransition", fromPosition, toPosition);
+ }
+ catch { }
+ }
+
public async ValueTask PlaySelectStepAsync(int step)
{
try { await _js.InvokeVoidAsync("FaraonFx.playSelectStep", step); } catch { }
diff --git a/Faraon.Client/wwwroot/css/app.css b/Faraon.Client/wwwroot/css/app.css
index 75cec8e..bdb14f3 100644
--- a/Faraon.Client/wwwroot/css/app.css
+++ b/Faraon.Client/wwwroot/css/app.css
@@ -190,6 +190,26 @@ body, html {
transition: all 0.3s ease;
}
+.player-slot.active-turn-slot::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 140%;
+ height: 140%;
+ background: radial-gradient(ellipse at center, rgba(245, 176, 65, 0.28) 0%, rgba(245, 176, 65, 0.08) 55%, transparent 75%);
+ pointer-events: none;
+ z-index: -1;
+ border-radius: 50%;
+ animation: aura-breathe 2.2s infinite ease-in-out alternate;
+}
+
+@keyframes aura-breathe {
+ 0% { transform: translate(-50%, -50%) scale(0.92); opacity: 0.6; }
+ 100% { transform: translate(-50%, -50%) scale(1.15); opacity: 1; }
+}
+
.slot-top {
grid-row: 1;
grid-column: 2;
@@ -208,6 +228,22 @@ body, html {
.slot-bottom {
grid-row: 3;
grid-column: 1 / span 3;
+ position: relative;
+}
+
+.slot-bottom.active-turn-slot::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 320px;
+ height: 120px;
+ background: radial-gradient(ellipse at center, rgba(245, 176, 65, 0.22) 0%, rgba(245, 176, 65, 0.05) 50%, transparent 75%);
+ pointer-events: none;
+ z-index: -1;
+ border-radius: 50%;
+ animation: aura-breathe 2.2s infinite ease-in-out alternate;
}
.slot-center {
@@ -219,6 +255,35 @@ body, html {
gap: 20px;
}
+/* TURN HEADER INFO */
+.turn-header-info {
+ font-size: 0.95rem;
+ font-weight: 700;
+ color: #a3b899;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.active-turn-header-player {
+ color: var(--gold);
+ font-weight: 900;
+ text-shadow: 0 0 10px rgba(245, 176, 65, 0.8);
+ background: rgba(245, 176, 65, 0.15);
+ padding: 2px 10px;
+ border-radius: 12px;
+ border: 1px solid rgba(245, 176, 65, 0.45);
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ animation: header-pulse 1.8s infinite alternate;
+}
+
+@keyframes header-pulse {
+ 0% { box-shadow: 0 0 4px rgba(245, 176, 65, 0.3); }
+ 100% { box-shadow: 0 0 16px rgba(245, 176, 65, 0.8); }
+}
+
/* AVATAR BADGE */
.avatar-badge {
position: relative;
@@ -230,19 +295,92 @@ body, html {
border-radius: 16px;
padding: 6px 12px;
backdrop-filter: blur(8px);
- transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.avatar-badge.active-turn {
border-color: var(--gold);
- box-shadow: 0 0 20px rgba(245, 176, 65, 0.8), inset 0 0 10px rgba(245, 176, 65, 0.3);
- transform: scale(1.08);
+ box-shadow: 0 0 25px rgba(245, 176, 65, 0.9), inset 0 0 12px rgba(245, 176, 65, 0.35);
+ transform: scale(1.09);
animation: turn-pulse 1.8s infinite;
}
@keyframes turn-pulse {
- 0%, 100% { box-shadow: 0 0 15px rgba(245, 176, 65, 0.7); }
- 50% { box-shadow: 0 0 25px rgba(245, 176, 65, 1), 0 0 40px rgba(245, 176, 65, 0.5); }
+ 0%, 100% { box-shadow: 0 0 16px rgba(245, 176, 65, 0.7); }
+ 50% { box-shadow: 0 0 30px rgba(245, 176, 65, 1), 0 0 45px rgba(245, 176, 65, 0.55); }
+}
+
+/* TURN INDICATOR BADGE (NA ŤAHU) */
+.turn-indicator-badge {
+ position: absolute;
+ top: -14px;
+ left: 50%;
+ transform: translateX(-50%);
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ background: linear-gradient(135deg, #ffd700, #f5b041);
+ color: #11281d;
+ font-size: 0.66rem;
+ font-weight: 900;
+ letter-spacing: 0.8px;
+ padding: 2px 9px;
+ border-radius: 12px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.6), 0 0 14px rgba(255, 215, 0, 0.8);
+ border: 1.5px solid #ffffff;
+ white-space: nowrap;
+ z-index: 20;
+ animation: turn-badge-float 1.8s ease-in-out infinite alternate;
+}
+
+@keyframes turn-badge-float {
+ 0% { transform: translateX(-50%) translateY(0); }
+ 100% { transform: translateX(-50%) translateY(-3px); }
+}
+
+.turn-indicator-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background-color: #0d3824;
+ box-shadow: 0 0 4px #0d3824;
+ animation: dot-blink 1s ease-in-out infinite alternate;
+}
+
+@keyframes dot-blink {
+ 0% { opacity: 0.4; transform: scale(0.75); }
+ 100% { opacity: 1; transform: scale(1.25); }
+}
+
+/* TURN ARRIVAL EFFECT */
+.turn-arrived-pulse {
+ animation: turn-arrival 0.55s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
+}
+
+@keyframes turn-arrival {
+ 0% { transform: scale(1); }
+ 40% { transform: scale(1.18); box-shadow: 0 0 35px rgba(245, 176, 65, 1), 0 0 55px rgba(255, 215, 0, 0.8); }
+ 100% { transform: scale(1.09); }
+}
+
+/* TURN TRANSFER ORB */
+.turn-transfer-orb {
+ position: fixed;
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ background: radial-gradient(circle at 35% 35%, #ffffff, #ffd700 45%, #ff8c00 85%, transparent);
+ box-shadow: 0 0 18px rgba(255, 215, 0, 0.95), 0 0 36px rgba(245, 176, 65, 0.7);
+ transform: translate(-50%, -50%);
+ pointer-events: none;
+ z-index: 998;
+ filter: drop-shadow(0 0 10px rgba(245, 176, 65, 0.85));
+ animation: orb-glow 0.38s infinite alternate;
+}
+
+@keyframes orb-glow {
+ 0% { transform: translate(-50%, -50%) scale(0.9); }
+ 100% { transform: translate(-50%, -50%) scale(1.25); }
}
.avatar-icon {
diff --git a/Faraon.Client/wwwroot/js/gameFx.js b/Faraon.Client/wwwroot/js/gameFx.js
index cb0db08..25c550f 100644
--- a/Faraon.Client/wwwroot/js/gameFx.js
+++ b/Faraon.Client/wwwroot/js/gameFx.js
@@ -461,6 +461,130 @@ window.FaraonFx = (function () {
}
}
+ // --- 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 = 380;
+ 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);
@@ -912,6 +1036,7 @@ window.FaraonFx = (function () {
triggerGoldStarExplosion,
animateCardPlay,
animateCardDraw,
+ animateTurnTransition,
toggleMute
};
})();
diff --git a/Faraon.Core/Engine/GameEngine.cs b/Faraon.Core/Engine/GameEngine.cs
index 0b4ddcf..d876e6a 100644
--- a/Faraon.Core/Engine/GameEngine.cs
+++ b/Faraon.Core/Engine/GameEngine.cs
@@ -201,7 +201,7 @@ public class GameEngine
// Efekt Sediem
else if (firstRank == Rank.Seven)
{
- int addedPenalty = cards.Count * 2;
+ int addedPenalty = cards.Count * 3;
State.PendingDrawCount += addedPenalty;
var redSeven = cards.FirstOrDefault(c => c.IsRedSeven);
@@ -325,7 +325,7 @@ public class GameEngine
// Efekt Sedmy
if (card.IsSeven)
{
- State.PendingDrawCount += 2;
+ State.PendingDrawCount += 3;
// Červená sedma vracia do hry
if (card.IsRedSeven && State.Rules.RedSevenRevives && State.LastFinishedPlayer != null && State.LastFinishedPlayer != player)
@@ -353,7 +353,7 @@ public class GameEngine
player,
Card: card,
ExtraData: State.PendingDrawCount,
- Message: $"⚡ Sedma! Ďalší hráč berie {State.PendingDrawCount} karty."
+ Message: $"⚡ Sedma! Ďalší hráč berie {State.PendingDrawCount} {(State.PendingDrawCount < 5 ? "karty" : "kariet")}."
));
}
}
diff --git a/Faraon.Core/Models/RuleOptions.cs b/Faraon.Core/Models/RuleOptions.cs
index 5627020..a974795 100644
--- a/Faraon.Core/Models/RuleOptions.cs
+++ b/Faraon.Core/Models/RuleOptions.cs
@@ -8,7 +8,7 @@ public class RuleOptions
public int InitialCardCount { get; set; } = 5;
///
- /// Ak je zapnuté, sedma na sedmu pridáva ďalšie +2 karty (kumulácia trestu).
+ /// Ak je zapnuté, sedma na sedmu pridáva ďalšie +3 karty (kumulácia trestu).
///
public bool StackSevens { get; set; } = true;
diff --git a/Faraon.Tests/GameEngineTests.cs b/Faraon.Tests/GameEngineTests.cs
index b02928a..c68367b 100644
--- a/Faraon.Tests/GameEngineTests.cs
+++ b/Faraon.Tests/GameEngineTests.cs
@@ -45,7 +45,7 @@ public class GameEngineTests
var played = engine.PlayCard(p1, sevenCard);
Assert.True(played);
- Assert.Equal(2, engine.State.PendingDrawCount);
+ Assert.Equal(3, engine.State.PendingDrawCount);
Assert.Equal(p2, engine.State.CurrentPlayer);
}
@@ -175,7 +175,7 @@ public class GameEngineTests
var played = engine.PlayCards(p1, sevens);
Assert.True(played);
- Assert.Equal(4, engine.State.PendingDrawCount); // 2x sedma = +4 karty
+ Assert.Equal(6, engine.State.PendingDrawCount); // 2x sedma = +6 kariet
Assert.Equal(p2, engine.State.CurrentPlayer);
}