+
@@ -27,6 +28,11 @@ else
}
+
+
+
+
+
+
+ @if (!IsLogRadarMinimized && Engine.State.MoveLog.Count > 0)
+ {
+
+ @foreach (var log in Engine.State.MoveLog.TakeLast(5).Reverse())
+ {
+
+
@log.Icon
+
+
@log.Text
+ @if (!string.IsNullOrEmpty(log.SubText))
+ {
+
@log.SubText
+ }
+
+
+ }
+
+ }
+
+
@if (CurrentJoke != null)
{
@@ -310,6 +342,7 @@ else
}
@@ -319,6 +352,7 @@ else
@code {
private bool IsInLobby = true;
private bool IsMuted = false;
+ private bool IsLogRadarMinimized = true;
private GameEngine? Engine;
private string LastEventMessage = "";
private System.Threading.CancellationTokenSource? _aiCts;
@@ -376,7 +410,7 @@ else
private void StartNewGame()
{
// Klony hráčov pre čistú hru
- var gamePlayers = ConfiguredPlayers.Select(p => new Player(p.Name, p.Type, p.Avatar, p.Difficulty)).ToList();
+ var gamePlayers = ConfiguredPlayers.Select(p => new Player(p.Name, p.Type, p.Avatar, p.Difficulty, p.PersonalityId)).ToList();
Engine = new GameEngine();
Engine.OnGameEvent += HandleGameEvent;
@@ -1024,6 +1058,11 @@ else
player.CurrentEmotion = emotion;
StateHasChanged();
+ if (player.Type == PlayerType.AI && !string.IsNullOrEmpty(player.PersonalityId))
+ {
+ _ = Fx.PlayBotSfxAsync(player.PersonalityId, emotion.ToString().ToLowerInvariant());
+ }
+
if (emotion != PlayerEmotion.Neutral && emotion != PlayerEmotion.Celebrating)
{
_ = Task.Run(async () =>
@@ -1038,6 +1077,38 @@ else
}
}
+ private void CycleTableTheme()
+ {
+ if (Engine == null) return;
+ Engine.State.Rules.TableTheme = Engine.State.Rules.TableTheme switch
+ {
+ TableTheme.ClassicGreen => TableTheme.RoyalBurgundy,
+ TableTheme.RoyalBurgundy => TableTheme.WoodCabin,
+ TableTheme.WoodCabin => TableTheme.CyberNeon,
+ TableTheme.CyberNeon => TableTheme.ClassicGreen,
+ _ => TableTheme.ClassicGreen
+ };
+ StateHasChanged();
+ }
+
+ private string GetThemeName(TableTheme theme) => theme switch
+ {
+ TableTheme.ClassicGreen => "Zelené plátno",
+ TableTheme.RoyalBurgundy => "Kráľovský bord",
+ TableTheme.WoodCabin => "Drevený zrub",
+ TableTheme.CyberNeon => "Cyberpunk neón",
+ _ => "Zelené plátno"
+ };
+
+ private string GetThemeIcon(TableTheme theme) => theme switch
+ {
+ TableTheme.ClassicGreen => "🍀",
+ TableTheme.RoyalBurgundy => "👑",
+ TableTheme.WoodCabin => "🪵",
+ TableTheme.CyberNeon => "⚡",
+ _ => "🍀"
+ };
+
private IEnumerable<(Player Player, string PositionClass)> GetOpponents()
{
if (Engine == null) return Enumerable.Empty<(Player, string)>();
@@ -1132,6 +1203,7 @@ else
{
PlayerThoughts.Clear();
CurrentJoke = null;
+ IsLogRadarMinimized = true;
StartNewGame();
}
@@ -1140,7 +1212,14 @@ else
_aiCts?.Cancel();
PlayerThoughts.Clear();
CurrentJoke = null;
+ IsLogRadarMinimized = true;
IsInLobby = true;
Engine = null;
}
+
+ public void Dispose()
+ {
+ _aiCts?.Cancel();
+ _aiCts?.Dispose();
+ }
}
diff --git a/Faraon.Client/Services/FxService.cs b/Faraon.Client/Services/FxService.cs
index 7d23a9d..5e9f764 100644
--- a/Faraon.Client/Services/FxService.cs
+++ b/Faraon.Client/Services/FxService.cs
@@ -198,6 +198,16 @@ public class FxService
try { await _js.InvokeVoidAsync("FaraonFx.triggerDopamineBanner", title, subtitle, type); } catch { }
}
+ public async ValueTask PlayCardWhooshAsync()
+ {
+ try { await _js.InvokeVoidAsync("FaraonFx.playCardWhoosh"); } catch { }
+ }
+
+ public async ValueTask PlayBotSfxAsync(string? personalityId, string? emotion = null)
+ {
+ try { await _js.InvokeVoidAsync("FaraonFx.playBotPersonalitySfx", personalityId, emotion); } 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 d31b4fd..4c97bfe 100644
--- a/Faraon.Client/wwwroot/css/app.css
+++ b/Faraon.Client/wwwroot/css/app.css
@@ -2501,4 +2501,295 @@ body, html {
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
+}
+
+/* --- 1. TABLE THEMES --- */
+#game-table-container.theme-classicgreen,
+#game-table-container.theme-classic-green {
+ background: radial-gradient(ellipse at center, #144933 0%, #0b2b1d 60%, #04120c 100%) !important;
+}
+#game-table-container.theme-classicgreen .poker-table-surface,
+#game-table-container.theme-classic-green .poker-table-surface {
+ border-color: #5a3818 !important;
+ background: radial-gradient(ellipse at center, #1e5c42 0%, #13422e 65%, #0d2e20 100%) !important;
+ box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.8), 0 0 30px rgba(0, 0, 0, 0.9) !important;
+}
+
+#game-table-container.theme-royalburgundy,
+#game-table-container.theme-royal-burgundy {
+ background: radial-gradient(ellipse at center, #420d18 0%, #24050b 60%, #0c0103 100%) !important;
+}
+#game-table-container.theme-royalburgundy .poker-table-surface,
+#game-table-container.theme-royal-burgundy .poker-table-surface {
+ border-color: #8c6227 !important;
+ background: radial-gradient(ellipse at center, #5c1422 0%, #3d0a14 65%, #200409 100%) !important;
+ box-shadow: inset 0 0 65px rgba(0, 0, 0, 0.85), 0 0 35px rgba(236, 72, 153, 0.25) !important;
+}
+
+#game-table-container.theme-woodcabin,
+#game-table-container.theme-wood-cabin {
+ background: radial-gradient(ellipse at center, #2e1d10 0%, #1a0f08 60%, #080402 100%) !important;
+}
+#game-table-container.theme-woodcabin .poker-table-surface,
+#game-table-container.theme-wood-cabin .poker-table-surface {
+ border-color: #3e2723 !important;
+ background: radial-gradient(ellipse at center, #3d2817 0%, #291a0e 65%, #180f07 100%) !important;
+ box-shadow: inset 0 0 70px rgba(0, 0, 0, 0.9), 0 0 30px rgba(245, 158, 11, 0.2) !important;
+}
+
+#game-table-container.theme-cyberneon,
+#game-table-container.theme-cyber-neon {
+ background: radial-gradient(ellipse at center, #0a1f30 0%, #050d14 60%, #010408 100%) !important;
+}
+#game-table-container.theme-cyberneon .poker-table-surface,
+#game-table-container.theme-cyber-neon .poker-table-surface {
+ border-color: #00e5ff !important;
+ background: radial-gradient(ellipse at center, #0e2a40 0%, #081724 65%, #03080d 100%) !important;
+ box-shadow: inset 0 0 60px rgba(0, 229, 255, 0.25), 0 0 40px rgba(0, 229, 255, 0.35) !important;
+}
+
+/* --- MOVE HISTORY RADAR (ŽIVÝ ZÁZNAMNÍK ŤAHOV) --- */
+.game-log-radar {
+ position: absolute;
+ top: 55px;
+ left: 14px;
+ z-index: 80;
+ max-width: 280px;
+ width: 260px;
+ pointer-events: auto;
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+}
+
+.game-log-radar.minimized {
+ width: auto;
+ max-width: 48px;
+}
+
+.log-radar-toggle-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ background: rgba(10, 25, 20, 0.85);
+ border: 1px solid rgba(245, 176, 65, 0.45);
+ color: var(--gold);
+ font-size: 0.76rem;
+ font-weight: 800;
+ padding: 4px 10px;
+ border-radius: 14px;
+ cursor: pointer;
+ backdrop-filter: blur(8px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
+ margin-bottom: 6px;
+ transition: all 0.2s ease;
+ user-select: none;
+}
+
+.log-radar-toggle-btn:hover {
+ background: rgba(245, 176, 65, 0.25);
+ border-color: var(--gold);
+ transform: scale(1.05);
+}
+
+.log-radar-items-container {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ background: rgba(8, 16, 12, 0.92);
+ border: 1px solid rgba(245, 176, 65, 0.3);
+ border-radius: 14px;
+ padding: 8px 10px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.85), inset 0 0 15px rgba(245, 176, 65, 0.08);
+ backdrop-filter: blur(10px);
+ max-height: 220px;
+ overflow-y: auto;
+}
+
+.log-radar-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 0.78rem;
+ color: #f1f5f9;
+ padding: 5px 8px;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.05);
+ border-left: 3px solid var(--gold);
+ animation: radar-item-in 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+}
+
+@keyframes radar-item-in {
+ 0% { transform: translateX(-15px); opacity: 0; }
+ 100% { transform: translateX(0); opacity: 1; }
+}
+
+.log-radar-icon {
+ font-size: 1rem;
+ flex-shrink: 0;
+}
+
+.log-radar-text {
+ flex: 1;
+ line-height: 1.25;
+ font-weight: 600;
+}
+
+.log-radar-sub {
+ font-size: 0.68rem;
+ opacity: 0.85;
+ color: var(--gold);
+}
+
+/* --- 4. 3D VICTORY PODIUM & MATCH STATS MODAL --- */
+.match-summary-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.88);
+ backdrop-filter: blur(14px);
+ z-index: 500;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 16px;
+ animation: fadeIn 0.35s ease;
+}
+
+.match-summary-card {
+ background: linear-gradient(135deg, rgba(20, 15, 35, 0.98) 0%, rgba(10, 25, 20, 0.98) 50%, rgba(30, 15, 10, 0.98) 100%);
+ border: 2px solid #ffd700;
+ border-radius: 28px;
+ box-shadow: 0 0 60px rgba(255, 215, 0, 0.45), 0 20px 50px rgba(0, 0, 0, 0.95);
+ max-width: 650px;
+ width: 100%;
+ padding: 24px 28px;
+ color: #ffffff;
+ text-align: center;
+ position: relative;
+ max-height: 90vh;
+ overflow-y: auto;
+}
+
+.podium-container {
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ gap: 14px;
+ margin: 18px 0 22px;
+ min-height: 180px;
+}
+
+.podium-column {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ flex: 1;
+ max-width: 130px;
+}
+
+.podium-avatar-box {
+ margin-bottom: 8px;
+ position: relative;
+}
+
+.podium-avatar {
+ font-size: 2.8rem;
+ filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.8));
+ animation: podium-bounce 1.5s infinite alternate ease-in-out;
+}
+
+.podium-crown {
+ position: absolute;
+ top: -16px;
+ left: 50%;
+ transform: translateX(-50%);
+ font-size: 1.5rem;
+ animation: float 2s infinite ease-in-out alternate;
+}
+
+.podium-name {
+ font-size: 0.88rem;
+ font-weight: 800;
+ color: #ffffff;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 120px;
+ margin-bottom: 6px;
+}
+
+.podium-step {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-family: 'Cinzel', serif;
+ font-size: 1.8rem;
+ font-weight: 900;
+ border-radius: 12px 12px 0 0;
+ box-shadow: inset 0 2px 6px rgba(255, 255, 255, 0.3), 0 10px 20px rgba(0, 0, 0, 0.6);
+}
+
+.step-gold {
+ height: 110px;
+ background: linear-gradient(180deg, #ffd700 0%, #b8860b 100%);
+ color: #1a1a1a;
+ border: 2px solid #fff;
+ box-shadow: 0 0 25px rgba(255, 215, 0, 0.6);
+}
+
+.step-silver {
+ height: 80px;
+ background: linear-gradient(180deg, #e2e8f0 0%, #94a3b8 100%);
+ color: #0f172a;
+ border: 2px solid #f8fafc;
+}
+
+.step-bronze {
+ height: 60px;
+ background: linear-gradient(180deg, #cd7f32 0%, #8b4513 100%);
+ color: #ffffff;
+ border: 2px solid #d97706;
+}
+
+@keyframes podium-bounce {
+ 0% { transform: translateY(0); }
+ 100% { transform: translateY(-8px); }
+}
+
+.match-stats-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 10px;
+ margin-bottom: 22px;
+}
+
+.stat-badge-box {
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(245, 176, 65, 0.25);
+ border-radius: 14px;
+ padding: 10px 14px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ text-align: left;
+}
+
+.stat-badge-icon {
+ font-size: 1.6rem;
+}
+
+.stat-badge-title {
+ font-size: 0.72rem;
+ color: #94a3b8;
+ text-transform: uppercase;
+ font-weight: 700;
+ letter-spacing: 0.5px;
+}
+
+.stat-badge-value {
+ font-size: 0.98rem;
+ font-weight: 800;
+ color: var(--gold);
}
\ No newline at end of file
diff --git a/Faraon.Client/wwwroot/js/gameFx.js b/Faraon.Client/wwwroot/js/gameFx.js
index 26a6c12..c6fd020 100644
--- a/Faraon.Client/wwwroot/js/gameFx.js
+++ b/Faraon.Client/wwwroot/js/gameFx.js
@@ -39,6 +39,17 @@ window.FaraonFx = (function () {
return audioCtx;
}
+ // Automatické prebudenie AudioContextu pri prvom kliknutí používateľa kdekoľvek
+ if (typeof window !== 'undefined') {
+ const unlockAudio = () => {
+ getAudioContext();
+ window.removeEventListener('pointerdown', unlockAudio);
+ window.removeEventListener('keydown', unlockAudio);
+ };
+ window.addEventListener('pointerdown', unlockAudio, { passive: true });
+ window.addEventListener('keydown', unlockAudio, { passive: true });
+ }
+
// --- PROCEDURAL SOUND GENERATORS ---
function playCardSnap() {
if (isMuted) return;
@@ -386,6 +397,182 @@ window.FaraonFx = (function () {
});
}
+ function playCardWhoosh() {
+ if (isMuted) return;
+ const ctx = getAudioContext();
+ if (!ctx) return;
+ const now = ctx.currentTime;
+ const bufferSize = Math.floor(ctx.sampleRate * 0.22);
+ 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.sin((i / bufferSize) * Math.PI);
+ }
+ const noise = ctx.createBufferSource();
+ noise.buffer = buffer;
+ const filter = ctx.createBiquadFilter();
+ filter.type = 'bandpass';
+ filter.frequency.setValueAtTime(450, now);
+ filter.frequency.exponentialRampToValueAtTime(1200, now + 0.1);
+ filter.frequency.exponentialRampToValueAtTime(350, now + 0.22);
+ filter.Q.value = 2.5;
+
+ const gain = ctx.createGain();
+ gain.gain.setValueAtTime(0.3, now);
+ gain.gain.exponentialRampToValueAtTime(0.001, now + 0.22);
+
+ noise.connect(filter);
+ filter.connect(gain);
+ gain.connect(ctx.destination);
+ noise.start(now);
+ }
+
+ function playBotPersonalitySfx(personalityId, emotion) {
+ if (isMuted) return;
+ const ctx = getAudioContext();
+ if (!ctx) return;
+ const now = ctx.currentTime;
+ const pId = (personalityId || 'fox').toLowerCase();
+ const emo = (emotion || 'thinking').toLowerCase();
+
+ // Mierny posun tóniny podľa emócie
+ let pitchMul = 1.0;
+ if (emo === 'shocked' || emo === 'desperate') pitchMul = 0.82;
+ else if (emo === 'confident' || emo === 'celebrating') pitchMul = 1.22;
+ else if (emo === 'freezing') pitchMul = 1.45;
+ else if (emo === 'angry') pitchMul = 0.92;
+
+ switch (pId) {
+ case 'fox': { // Lišiak Filip - sly playful rising arpeggio
+ const baseFreqs = [440, 554.37, 659.25, 880];
+ baseFreqs.forEach((freq, i) => {
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ const t = now + i * 0.045;
+ osc.type = 'triangle';
+ osc.frequency.setValueAtTime(freq * pitchMul, t);
+ gain.gain.setValueAtTime(0.28, t);
+ gain.gain.exponentialRampToValueAtTime(0.001, t + 0.18);
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ osc.start(t);
+ osc.stop(t + 0.19);
+ });
+ break;
+ }
+ case 'queen': { // Kráľovná Alžbeta - regal royal chime / bell chords
+ const baseFreqs = [523.25, 659.25, 783.99, 1046.50];
+ baseFreqs.forEach((freq, i) => {
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ const t = now + i * 0.055;
+ osc.type = 'sine';
+ osc.frequency.setValueAtTime(freq * pitchMul, t);
+ gain.gain.setValueAtTime(0.32, t);
+ gain.gain.exponentialRampToValueAtTime(0.001, t + 0.42);
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ osc.start(t);
+ osc.stop(t + 0.43);
+ });
+ break;
+ }
+ case 'robot': { // Kyber-Robo - 8-bit digital synth crunch
+ const tones = [880, 1320, 660, 1760];
+ tones.forEach((freq, i) => {
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ const t = now + i * 0.04;
+ osc.type = 'square';
+ osc.frequency.setValueAtTime(freq * pitchMul, t);
+ gain.gain.setValueAtTime(0.22, t);
+ gain.gain.exponentialRampToValueAtTime(0.001, t + 0.06);
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ osc.start(t);
+ osc.stop(t + 0.065);
+ });
+ break;
+ }
+ case 'grandpa': { // Dedo Jozef - warm vintage harmonic bell
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ osc.type = 'sine';
+ osc.frequency.setValueAtTime(987.77 * pitchMul, now);
+ gain.gain.setValueAtTime(0.35, now);
+ gain.gain.exponentialRampToValueAtTime(0.001, now + 0.55);
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ osc.start(now);
+ osc.stop(now + 0.56);
+ break;
+ }
+ case 'cyberkid': { // Hacker Miško - futuristic laser zap
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ osc.type = 'sawtooth';
+ osc.frequency.setValueAtTime(1500 * pitchMul, now);
+ osc.frequency.exponentialRampToValueAtTime(180 * pitchMul, now + 0.18);
+ gain.gain.setValueAtTime(0.3, now);
+ gain.gain.exponentialRampToValueAtTime(0.001, now + 0.18);
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ osc.start(now);
+ osc.stop(now + 0.19);
+ break;
+ }
+ case 'cat': { // Mačka Minda - playful synth meow
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ osc.type = 'sine';
+ osc.frequency.setValueAtTime(580 * pitchMul, now);
+ osc.frequency.exponentialRampToValueAtTime(980 * pitchMul, now + 0.11);
+ osc.frequency.exponentialRampToValueAtTime(680 * pitchMul, now + 0.28);
+ gain.gain.setValueAtTime(0.32, now);
+ gain.gain.exponentialRampToValueAtTime(0.001, now + 0.30);
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ osc.start(now);
+ osc.stop(now + 0.31);
+ break;
+ }
+ case 'wizard': { // Čarodejník Baltazár - mystical shimmering chord
+ [392.00, 587.33, 880.00, 1174.66].forEach((freq, i) => {
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ const t = now + i * 0.035;
+ osc.type = 'sine';
+ osc.frequency.setValueAtTime(freq * pitchMul, t);
+ gain.gain.setValueAtTime(0.24, t);
+ gain.gain.exponentialRampToValueAtTime(0.001, t + 0.45);
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ osc.start(t);
+ osc.stop(t + 0.46);
+ });
+ break;
+ }
+ case 'bear': { // Medveď Mišo - deep bass growl thud
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ osc.type = 'sawtooth';
+ osc.frequency.setValueAtTime(95 * pitchMul, now);
+ osc.frequency.exponentialRampToValueAtTime(38 * pitchMul, now + 0.35);
+ gain.gain.setValueAtTime(0.42, now);
+ gain.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ osc.start(now);
+ osc.stop(now + 0.36);
+ break;
+ }
+ default: {
+ playCardSnap();
+ break;
+ }
+ }
+ }
+
// --- SCREEN SHAKE ---
function triggerScreenShake(intensity = 5) {
const board = document.getElementById('game-table-container');
@@ -1197,6 +1384,8 @@ window.FaraonFx = (function () {
animateCardPlay,
animateCardDraw,
animateTurnTransition,
+ playCardWhoosh,
+ playBotPersonalitySfx,
setSpeedLevel,
toggleMute
};
diff --git a/Faraon.Core/Engine/GameEngine.cs b/Faraon.Core/Engine/GameEngine.cs
index c3941ad..1e3bdf3 100644
--- a/Faraon.Core/Engine/GameEngine.cs
+++ b/Faraon.Core/Engine/GameEngine.cs
@@ -770,6 +770,121 @@ public class GameEngine
private void EmitEvent(GameEvent e)
{
+ RecordStatsAndLog(e);
OnGameEvent?.Invoke(e);
}
+
+ private void RecordStatsAndLog(GameEvent e)
+ {
+ State.Stats.TotalTurns = State.TurnCount;
+ switch (e.Type)
+ {
+ case GameEventType.GameStarted:
+ AddLogItem(new GameLogItem("🎮", "Hra sa začala!", null, DateTime.UtcNow));
+ break;
+ case GameEventType.CardPlayed:
+ if (e.SourcePlayer != null)
+ {
+ State.Stats.RecordCardPlayed(e.SourcePlayer, e.Cards?.Count ?? 1);
+ if (e.Card?.Rank == Rank.Ace || (e.Cards != null && e.Cards.Any(c => c.Rank == Rank.Ace)))
+ {
+ State.Stats.TotalAcesPlayed++;
+ }
+ if (e.Card?.Rank == Rank.Seven || (e.Cards != null && e.Cards.Any(c => c.Rank == Rank.Seven)))
+ {
+ State.Stats.TotalSevensPlayed++;
+ }
+
+ string cardDesc = e.Cards != null && e.Cards.Count > 1
+ ? $"{e.Cards.Count}x {e.Cards[0].RankName}"
+ : (e.Card != null ? $"{e.Card.RankName} {e.Card.SuitSymbol}" : "karta");
+
+ AddLogItem(new GameLogItem("🎴", $"{e.SourcePlayer.Name} zahral {cardDesc}", null, DateTime.UtcNow, e.SourcePlayer.Name, e.SourcePlayer.Avatar));
+ }
+ break;
+ case GameEventType.CardDrawn:
+ if (e.SourcePlayer != null)
+ {
+ int count = e.ExtraData > 0 ? e.ExtraData : 1;
+ State.Stats.RecordCardDrawn(e.SourcePlayer, count);
+ string countText = count > 1 ? $"{count} kariet" : "z balíčka";
+ string? sub = count > 1 ? "Trest za sedmy" : null;
+ string? accent = count > 1 ? "#f59e0b" : null;
+ AddLogItem(new GameLogItem("📥", $"{e.SourcePlayer.Name} potiahol {countText}", sub, DateTime.UtcNow, e.SourcePlayer.Name, e.SourcePlayer.Avatar, accent));
+ }
+ break;
+ case GameEventType.SevenStacked:
+ if (e.TargetPlayer != null)
+ {
+ State.Stats.RecordCardDrawn(e.TargetPlayer, e.ExtraData);
+ AddLogItem(new GameLogItem("🎒", $"{e.TargetPlayer.Name} musel potiahnuť +{e.ExtraData} kariet!", "Trest zo sediem", DateTime.UtcNow, e.TargetPlayer.Name, e.TargetPlayer.Avatar, "#f59e0b"));
+ }
+ break;
+ case GameEventType.SevenCanceled:
+ if (e.SourcePlayer != null)
+ {
+ AddLogItem(new GameLogItem("🛡️", $"{e.SourcePlayer.Name} zrušil sedmy Zeleným dolníkom!", "Štít úspešný", DateTime.UtcNow, e.SourcePlayer.Name, e.SourcePlayer.Avatar, "#10b981"));
+ }
+ break;
+ case GameEventType.AceSkipped:
+ if (e.SourcePlayer != null)
+ {
+ AddLogItem(new GameLogItem("❄️", $"{e.SourcePlayer.Name} stojí toto kolo", "Zmrazený Esom", DateTime.UtcNow, e.SourcePlayer.Name, e.SourcePlayer.Avatar, "#06b6d4"));
+ }
+ break;
+ case GameEventType.AceCountered:
+ if (e.SourcePlayer != null)
+ {
+ AddLogItem(new GameLogItem("⚡", $"{e.SourcePlayer.Name} prebil Eso vlastným Esom!", "Protiútok", DateTime.UtcNow, e.SourcePlayer.Name, e.SourcePlayer.Avatar, "#3b82f6"));
+ }
+ break;
+ case GameEventType.CardsBurned:
+ if (e.SourcePlayer != null)
+ {
+ State.Stats.RecordBurn(e.SourcePlayer);
+ AddLogItem(new GameLogItem("🔥", $"{e.SourcePlayer.Name} SPÁLIL 4 KARTY!", "Hrá znova", DateTime.UtcNow, e.SourcePlayer.Name, e.SourcePlayer.Avatar, "#ef4444"));
+ }
+ break;
+ case GameEventType.SuitChanged:
+ if (e.SourcePlayer != null && e.ChosenSuit.HasValue)
+ {
+ AddLogItem(new GameLogItem("👑", $"{e.SourcePlayer.Name} zmenil farbu na {GetSuitName(e.ChosenSuit.Value)}", null, DateTime.UtcNow, e.SourcePlayer.Name, e.SourcePlayer.Avatar, "#eab308"));
+ }
+ break;
+ case GameEventType.PlayerRevived:
+ if (e.TargetPlayer != null)
+ {
+ AddLogItem(new GameLogItem("❤️", $"{e.TargetPlayer.Name} VSTAL Z MŔTVYCH!", "Červená sedma", DateTime.UtcNow, e.TargetPlayer.Name, e.TargetPlayer.Avatar, "#ec4899"));
+ }
+ break;
+ case GameEventType.PlayerFinished:
+ if (e.SourcePlayer != null)
+ {
+ AddLogItem(new GameLogItem("🏆", $"{e.SourcePlayer.Name} skončil na {e.ExtraData}. mieste!", "Vyprázdnil ruku", DateTime.UtcNow, e.SourcePlayer.Name, e.SourcePlayer.Avatar, "#ffd700"));
+ }
+ break;
+ case GameEventType.GameOver:
+ case GameEventType.MatchWon:
+ State.Stats.EndTime = DateTime.UtcNow;
+ break;
+ }
+ }
+
+ private void AddLogItem(GameLogItem item)
+ {
+ State.MoveLog.Add(item);
+ if (State.MoveLog.Count > 30)
+ {
+ State.MoveLog.RemoveAt(0);
+ }
+ }
+
+ private static string GetSuitName(Suit suit) => suit switch
+ {
+ Suit.Hearts => "Červeň ❤️",
+ Suit.Leaves => "Zeleň 🍃",
+ Suit.Acorns => "Žaluď 🌰",
+ Suit.Bells => "Guľu 🔔",
+ _ => suit.ToString()
+ };
}
diff --git a/Faraon.Core/Models/GameState.cs b/Faraon.Core/Models/GameState.cs
index b755422..aee1a19 100644
--- a/Faraon.Core/Models/GameState.cs
+++ b/Faraon.Core/Models/GameState.cs
@@ -40,4 +40,7 @@ public class GameState
public int ActivePlayersCount => Players.Where(p => !p.IsEliminated).Count(p => !p.HasFinished);
public List ActiveTournamentPlayers => Players.Where(p => !p.IsEliminated).ToList();
+
+ public GameMatchStats Stats { get; set; } = new();
+ public List MoveLog { get; set; } = new();
}
diff --git a/Faraon.Core/Models/GameStats.cs b/Faraon.Core/Models/GameStats.cs
new file mode 100644
index 0000000..8dab6b7
--- /dev/null
+++ b/Faraon.Core/Models/GameStats.cs
@@ -0,0 +1,70 @@
+namespace Faraon.Core.Models;
+
+using System;
+using System.Collections.Generic;
+
+public record GameLogItem(
+ string Icon,
+ string Text,
+ string? SubText,
+ DateTime Time,
+ string? PlayerName = null,
+ string? PlayerAvatar = null,
+ string? AccentColor = null
+);
+
+public class GameMatchStats
+{
+ public DateTime StartTime { get; set; } = DateTime.UtcNow;
+ public DateTime? EndTime { get; set; }
+ public TimeSpan Duration => (EndTime ?? DateTime.UtcNow) - StartTime;
+ public int TotalTurns { get; set; }
+ public int TotalAcesPlayed { get; set; }
+ public int TotalSevensPlayed { get; set; }
+ public int TotalCardsBurned { get; set; }
+ public int MaxPenaltyDrawnInOneTurn { get; set; }
+ public string? MaxPenaltyPlayerName { get; set; }
+
+ public Dictionary CardsPlayedPerPlayer { get; } = new();
+ public Dictionary CardsDrawnPerPlayer { get; } = new();
+ public Dictionary BurnsPerPlayer { get; } = new();
+
+ public void RecordCardPlayed(Player player, int count = 1)
+ {
+ CardsPlayedPerPlayer[player.Name] = CardsPlayedPerPlayer.GetValueOrDefault(player.Name) + count;
+ }
+
+ public void RecordCardDrawn(Player player, int count = 1)
+ {
+ CardsDrawnPerPlayer[player.Name] = CardsDrawnPerPlayer.GetValueOrDefault(player.Name) + count;
+ if (count > MaxPenaltyDrawnInOneTurn)
+ {
+ MaxPenaltyDrawnInOneTurn = count;
+ MaxPenaltyPlayerName = player.Name;
+ }
+ }
+
+ public void RecordBurn(Player player)
+ {
+ TotalCardsBurned += 4;
+ BurnsPerPlayer[player.Name] = BurnsPerPlayer.GetValueOrDefault(player.Name) + 1;
+ }
+
+ public string GetMvpPlayerName()
+ {
+ string best = "Nikto";
+ int bestScore = -999;
+ foreach (var (name, played) in CardsPlayedPerPlayer)
+ {
+ int burns = BurnsPerPlayer.GetValueOrDefault(name);
+ int drawn = CardsDrawnPerPlayer.GetValueOrDefault(name);
+ int score = (played * 2) + (burns * 10) - drawn;
+ if (score > bestScore)
+ {
+ bestScore = score;
+ best = name;
+ }
+ }
+ return best;
+ }
+}
\ No newline at end of file
diff --git a/Faraon.Core/Models/Player.cs b/Faraon.Core/Models/Player.cs
index 354d90a..68f803b 100644
--- a/Faraon.Core/Models/Player.cs
+++ b/Faraon.Core/Models/Player.cs
@@ -50,6 +50,11 @@ public class Player
Avatar = avatar;
Difficulty = difficulty;
PersonalityId = personalityId;
+ if (type == PlayerType.AI && string.IsNullOrEmpty(PersonalityId))
+ {
+ var pers = AIPersonality.All.FirstOrDefault(a => a.BaseAvatar == avatar);
+ PersonalityId = pers?.Id ?? "fox";
+ }
}
public string GetLiveAvatar()
diff --git a/Faraon.Core/Models/RuleOptions.cs b/Faraon.Core/Models/RuleOptions.cs
index b144f1e..d2c6430 100644
--- a/Faraon.Core/Models/RuleOptions.cs
+++ b/Faraon.Core/Models/RuleOptions.cs
@@ -14,8 +14,20 @@ public enum GreenUnderKnaveMode
UniversalWildcard // Ruší sedmy, ide na čokoľvek a súper naňho môže dať čokoľvek
}
+public enum TableTheme
+{
+ ClassicGreen = 0,
+ RoyalBurgundy = 1,
+ WoodCabin = 2,
+ CyberNeon = 3
+}
+
public class RuleOptions
{
+ ///
+ /// Vizuálna téma a atmosféra herného stola.
+ ///
+ public TableTheme TableTheme { get; set; } = TableTheme.ClassicGreen;
///
/// Počet kariet rozdaných každému hráčovi na začiatku (zvyčajne 4 alebo 5).
///
diff --git a/Faraon.Tests/GameEngineTests.cs b/Faraon.Tests/GameEngineTests.cs
index 31f69ef..8fa2001 100644
--- a/Faraon.Tests/GameEngineTests.cs
+++ b/Faraon.Tests/GameEngineTests.cs
@@ -504,4 +504,39 @@ public class GameEngineTests
Assert.Equal("😾", player.GetLiveAvatar());
Assert.Equal("🔥", player.GetEmotionBadge());
}
+
+ [Fact]
+ public void GameStatsAndMoveLog_RecordEventsProperly()
+ {
+ var engine = new GameEngine(seed: 42);
+ var p1 = new Player("Hráč 1", PlayerType.Human);
+ var p2 = new Player("AI 1", PlayerType.AI);
+ var rules = new RuleOptions { StackSevens = true };
+ engine.StartGame(new List { p1, p2 }, rules);
+
+ Assert.NotNull(engine.State.Stats);
+ Assert.NotNull(engine.State.MoveLog);
+ Assert.NotEmpty(engine.State.MoveLog);
+ Assert.Contains(engine.State.MoveLog, l => l.Text.Contains("Hra sa začala"));
+
+ // Play Seven
+ p1.Hand.Clear();
+ var seven = new Card(Suit.Hearts, Rank.Seven);
+ p1.Hand.Add(seven);
+ p1.Hand.Add(new Card(Suit.Hearts, Rank.Eight));
+ engine.State.DiscardPile.Clear();
+ engine.State.DiscardPile.Add(new Card(Suit.Hearts, Rank.Ten));
+
+ engine.PlayCard(p1, seven);
+
+ Assert.Equal(1, engine.State.Stats.TotalSevensPlayed);
+ Assert.Contains(engine.State.MoveLog, l => l.Text.Contains("zahral") && l.Text.Contains("Sedma"));
+
+ // Bot draws 3 cards
+ engine.DrawOrAcknowledge(p2);
+
+ Assert.True(engine.State.Stats.MaxPenaltyDrawnInOneTurn >= 3);
+ Assert.Contains(engine.State.MoveLog, l => l.Text.Contains("potiahol 3"));
+ }
}
+