From 00aa94ea7cf23140d56f35f5bf24a1e71d8a1097 Mon Sep 17 00:00:00 2001 From: Michal Vrba Date: Thu, 3 Sep 2026 17:25:35 +0200 Subject: [PATCH] fixes --- Faraon.Client/Components/PlayerSlot.razor | 10 +- Faraon.Client/Pages/Home.razor | 37 +++++- Faraon.Client/Services/FxService.cs | 9 ++ Faraon.Client/wwwroot/css/app.css | 148 +++++++++++++++++++++- Faraon.Client/wwwroot/js/gameFx.js | 125 ++++++++++++++++++ Faraon.Core/Engine/GameEngine.cs | 6 +- Faraon.Core/Models/RuleOptions.cs | 2 +- Faraon.Tests/GameEngineTests.cs | 4 +- 8 files changed, 323 insertions(+), 18 deletions(-) diff --git a/Faraon.Client/Components/PlayerSlot.razor b/Faraon.Client/Components/PlayerSlot.razor index b723886..4ee4be7 100644 --- a/Faraon.Client/Components/PlayerSlot.razor +++ b/Faraon.Client/Components/PlayerSlot.razor @@ -1,7 +1,15 @@ @namespace Faraon.Client.Components -
+
+ @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 }
-
+
Ťah #@Engine.State.TurnCount • Na rade: - + @Engine.State.CurrentPlayer.Avatar @Engine.State.CurrentPlayer.Name
@@ -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); }