-
❤️
-
-
Červená sedma (Oživenie / Resurrect)
-
Spôsob vrátenia skončeného hráča späť do hry
+
+
+
+
+ @if (ShowRules)
+ {
+
+
+
+
+
+
❤️
+
+
Červená sedma (Oživenie / Resurrect)
+
Spôsob vrátenia skončeného hráča späť do hry
+
+
+
+
+
+
+
🍃
+
+
Zelený dolník (Rušenie sediem)
+
Zelený dolník zruší trest zo sedmy & pravidlá vykladania
+
+
+
+
+
+
+
+
+ Počiatočný počet kariet:
+
+
+
+ Rýchlosť hry:
+
-
-
-
-
-
🍃
-
-
Zelený dolník (Rušenie sediem)
-
Zelený dolník zruší trest zo sedmy & pravidlá vykladania
-
-
-
-
-
-
-
- Počiatočný počet kariet:
-
-
-
- Rýchlosť hry:
-
-
+ }
-
+
-
- 🎨 Vizuálny štýl balíčka kariet
-
-
-
Rules.SelectedDeck = "original"'>
-
-

-

-
-
-
Tradičný Mariáš 🎴
-
Autentický dvojhlavý Tell vzor (Piatnik)
-
+
@@ -244,6 +226,29 @@
private int SelectedPlayerCount = 4;
private RuleOptions Rules = new();
+ private bool ShowRules = false;
+ private bool ShowDeckSelector = false;
+
+ private record DeckInfo(string Id, string Name, string Desc, string Thumb, string Back);
+
+ private readonly DeckInfo[] AvailableDecks = new[]
+ {
+ new DeckInfo("original", "Tradičný Mariáš 🎴", "Autentický dvojhlavý Tell vzor (Piatnik)", "assets/cards/original/Hearts_Seven.webp", "assets/cards/original/Card_Back.webp"),
+ new DeckInfo("janosik", "Zbojníci & Legendy 🪓", "Jánošík, Uhorčík, výšivky a tatranský folklór", "assets/cards/janosik/Hearts_King.webp", "assets/cards/janosik/Card_Back.webp"),
+ new DeckInfo("spongebob", "SpongeBob Cartoon 🧽", "Bikini Bottom edícia (postavičky a veľké symboly)", "assets/cards/spongebob/Hearts_OverKnave.webp", "assets/cards/spongebob/Card_Back.webp"),
+ new DeckInfo("cyberpunk", "Cyberpunk 2077 ⚡", "Neónový holografický štýl a kybernetickí hrdinovia", "assets/cards/cyberpunk/Leaves_King.webp", "assets/cards/cyberpunk/Card_Back.webp"),
+ new DeckInfo("fantasy", "Fantasy RPG 🐉", "Dračí lordi, mágovia, runy a rytieri", "assets/cards/fantasy/Acorns_OverKnave.webp", "assets/cards/fantasy/Card_Back.webp"),
+ new DeckInfo("egypt", "Staroveký Egypt ⚔️", "Zlatý papyrus, Tutanchamón, Anubis a pyramídy", "assets/cards/egypt/Bells_King.webp", "assets/cards/egypt/Card_Back.webp"),
+ new DeckInfo("pets", "Kráľovské Zvieratká 🐱", "Mačičky a psíkovia v kráľovských korunkách", "assets/cards/pets/Hearts_OverKnave.webp", "assets/cards/pets/Card_Back.webp"),
+ new DeckInfo("pixel", "Retro 8-Bit Pixel 🕹️", "Nostalgický arkádový štýl klasických hier", "assets/cards/pixel/Leaves_Seven.webp", "assets/cards/pixel/Card_Back.webp")
+ };
+
+ private DeckInfo CurrentDeck => AvailableDecks.FirstOrDefault(d => d.Id == Rules.SelectedDeck) ?? AvailableDecks[0];
+
+ private void SelectDeck(string deckId)
+ {
+ Rules.SelectedDeck = deckId;
+ }
private readonly string[] Avatars = ["👑", "🧙♂️", "🦊", "🦁", "🐯", "🤖", "🐉", "🥷", "👸", "🤠", "🦅", "🐺"];
diff --git a/Faraon.Client/Components/PlayerSlot.razor b/Faraon.Client/Components/PlayerSlot.razor
index bc50868..6cd4b6a 100644
--- a/Faraon.Client/Components/PlayerSlot.razor
+++ b/Faraon.Client/Components/PlayerSlot.razor
@@ -75,6 +75,14 @@
}
+
+ @if (!string.IsNullOrEmpty(ThoughtBubble))
+ {
+
+ 💭
+ @ThoughtBubble
+
+ }
@@ -96,6 +104,8 @@
[Parameter] public bool CanBeRevived { get; set; } = false;
[Parameter] public int ReviveRoundsRemaining { get; set; } = 0;
[Parameter] public int ReviveTurnsRemaining { get; set; } = 0;
+ [Parameter] public string? ThoughtBubble { get; set; }
+ [Parameter] public bool IsThoughtFadingOut { get; set; } = false;
[Parameter] public bool IsOlympicMode { get; set; } = false;
[Parameter] public string PositionClass { get; set; } = "slot-top";
}
diff --git a/Faraon.Client/Pages/Home.razor b/Faraon.Client/Pages/Home.razor
index 76bb119..9860701 100644
--- a/Faraon.Client/Pages/Home.razor
+++ b/Faraon.Client/Pages/Home.razor
@@ -1,4 +1,5 @@
@page "/"
+@using Faraon.Core.AI
@inject FxService Fx
@inject IFaraonAI AI
@inject IJSRuntime JS
@@ -24,30 +25,46 @@ else
}
-
-
-
-
@@ -416,6 +447,34 @@ else
break;
}
+ if (Engine != null && Engine.State.Rules.AIHumanMode)
+ {
+ // Kartový vtip pri významných udalostiach (veľa sediem, spálenie, oživenie)
+ var joke = AIDialogueService.GetContextualJoke(e.Type, e.ExtraData, Engine.State.TurnCount);
+ if (joke != null)
+ {
+ var storyteller = Engine.State.Players.FirstOrDefault(p => p.Type == PlayerType.AI) ?? e.SourcePlayer ?? Engine.State.CurrentPlayer;
+ TriggerCardJoke(joke, storyteller);
+ }
+
+ if (e.SourcePlayer != null && e.SourcePlayer.Type == PlayerType.AI)
+ {
+ var reaction = AIDialogueService.GetEventReaction(e, e.SourcePlayer);
+ if (!string.IsNullOrEmpty(reaction))
+ {
+ SetThoughtBubble(e.SourcePlayer, reaction, 2800);
+ }
+ }
+ if (e.TargetPlayer != null && e.TargetPlayer.Type == PlayerType.AI && e.TargetPlayer != e.SourcePlayer)
+ {
+ var reaction = AIDialogueService.GetEventReaction(e, e.TargetPlayer);
+ if (!string.IsNullOrEmpty(reaction))
+ {
+ SetThoughtBubble(e.TargetPlayer, reaction, 2800);
+ }
+ }
+ }
+
if (Engine != null && Engine.State.CurrentPlayer.Hand.Count == 1 && !Engine.State.CurrentPlayer.HasFinished)
{
await Fx.PlayHeartbeatAsync();
@@ -457,17 +516,25 @@ else
private int MultiCardDelayMs => Engine?.State.Rules.SpeedLevel switch
{
- 1 => 130,
- 2 => 105,
- 4 => 50,
- 5 => 25,
- _ => 80
+ -5 => 350,
+ -4 => 280,
+ -3 => 220,
+ -2 => 180,
+ -1 => 140,
+ 0 => 110,
+ 1 => 85,
+ 2 => 65,
+ 3 => 45,
+ 4 => 30,
+ 5 => 15,
+ _ => 110
};
private async Task CycleSpeed()
{
if (Engine == null) return;
- int next = (Engine.State.Rules.SpeedLevel % 5) + 1;
+ int current = Engine.State.Rules.SpeedLevel;
+ int next = current >= 5 ? -5 : current + 1;
Engine.State.Rules.SpeedLevel = next;
await Fx.SetSpeedLevelAsync(next);
StateHasChanged();
@@ -475,22 +542,34 @@ else
private string GetSpeedButtonText(int level) => level switch
{
- 1 => "🐢 0.5x",
- 2 => "🚶 0.75x",
- 3 => "⚡ 1x",
- 4 => "🔥 1.5x",
- 5 => "🚀 2.5x",
- _ => "⚡ 1x"
+ -5 => "🐢 -5 (0.2x)",
+ -4 => "🐢 -4 (0.35x)",
+ -3 => "🚶 -3 (0.5x)",
+ -2 => "🚶 -2 (0.65x)",
+ -1 => "🍃 -1 (0.8x)",
+ 0 => "⚡ 0 (1x)",
+ 1 => "🔥 +1 (1.25x)",
+ 2 => "🔥 +2 (1.5x)",
+ 3 => "🚀 +3 (2x)",
+ 4 => "🚀 +4 (2.75x)",
+ 5 => "⚡ +5 (4x)",
+ _ => "⚡ 0 (1x)"
};
private string GetSpeedDescription(int level) => level switch
{
- 1 => "0.5x (Pomalá)",
- 2 => "0.75x (Pokojná)",
- 3 => "1x (Normálna - stredná)",
- 4 => "1.5x (Rýchla)",
- 5 => "2.5x (Turbo / Blesková)",
- _ => "1x (Normálna)"
+ -5 => "-5 (0.2x Zen / Extra pomalá)",
+ -4 => "-4 (0.35x Veľmi pomalá)",
+ -3 => "-3 (0.5x Pomalá)",
+ -2 => "-2 (0.65x Pokojná)",
+ -1 => "-1 (0.8x Zvoľnená)",
+ 0 => "0 (1x Normálna / Vyvážená)",
+ 1 => "+1 (1.25x Svižná)",
+ 2 => "+2 (1.5x Rýchla)",
+ 3 => "+3 (2x Veľmi rýchla)",
+ 4 => "+4 (2.75x Turbo)",
+ 5 => "+5 (4x Blesková)",
+ _ => "0 (1x Normálna)"
};
private void ToggleDeck()
@@ -647,6 +726,15 @@ else
if (Engine.State.TurnState == TurnState.Playing && Engine.State.CurrentPlayer.Type == PlayerType.AI)
{
+ if (Engine.State.Rules.AIHumanMode && Engine.State.TurnCount >= 28 && Engine.State.TurnCount % 16 == 0 && CurrentJoke == null)
+ {
+ var longJoke = AIDialogueService.GetContextualJoke(null, 0, Engine.State.TurnCount);
+ if (longJoke != null)
+ {
+ TriggerCardJoke(longJoke, Engine.State.CurrentPlayer);
+ }
+ }
+
_aiCts?.Cancel();
_aiCts = new System.Threading.CancellationTokenSource();
var token = _aiCts.Token;
@@ -655,12 +743,63 @@ else
{
try
{
- await Task.Delay(Engine.State.Rules.AIDelayMs, token);
- if (token.IsCancellationRequested || Engine == null) return;
-
var aiPlayer = Engine.State.CurrentPlayer;
if (aiPlayer.Type != PlayerType.AI) return;
+ if (Engine.State.Rules.AIHumanMode)
+ {
+ string thought = AIDialogueService.GetThinkingDialogue(aiPlayer, Engine.State);
+ int charDelay = Engine.State.Rules.SpeedLevel switch
+ {
+ -5 => 55,
+ -4 => 45,
+ -3 => 38,
+ -2 => 30,
+ -1 => 24,
+ 0 => 18,
+ 1 => 14,
+ 2 => 10,
+ 3 => 7,
+ 4 => 4,
+ 5 => 2,
+ _ => 18
+ };
+
+ // Postupné písanie myšlienky po písmenkách (Typewriter effect)
+ for (int i = 1; i <= thought.Length; i++)
+ {
+ if (token.IsCancellationRequested || Engine == null) return;
+ string partial = thought.Substring(0, i) + (i < thought.Length ? " ▌" : "");
+ await InvokeAsync(() => SetThoughtBubble(aiPlayer, partial, 5000));
+ await Task.Delay(charDelay, token);
+ }
+
+ // Dokončený text a krátka prirodzená pauza na prečítanie
+ await InvokeAsync(() => SetThoughtBubble(aiPlayer, thought, 4000));
+ int readPause = Engine.State.Rules.SpeedLevel switch
+ {
+ -5 => 1200,
+ -4 => 1000,
+ -3 => 800,
+ -2 => 650,
+ -1 => 500,
+ 0 => 400,
+ 1 => 300,
+ 2 => 220,
+ 3 => 150,
+ 4 => 90,
+ 5 => 40,
+ _ => 400
+ };
+ await Task.Delay(readPause, token);
+ }
+ else
+ {
+ await Task.Delay(Engine.State.Rules.AIDelayMs, token);
+ }
+
+ if (token.IsCancellationRequested || Engine == null) return;
+
var decision = AI.MakeDecision(Engine.State, aiPlayer);
await InvokeAsync(async () =>
@@ -719,6 +858,12 @@ else
}
else
{
+ if (Engine.State.Rules.AIHumanMode && Engine.State.PendingDrawCount == 0 && !Engine.State.PendingSkip)
+ {
+ var drawThought = AIDialogueService.GetMustDrawDialogue(aiPlayer);
+ SetThoughtBubble(aiPlayer, drawThought, 1800);
+ }
+
if (Engine.State.PendingDrawCount > 0)
{
int drawCount = Engine.State.PendingDrawCount;
@@ -752,6 +897,71 @@ else
}
}
+ private class PlayerThoughtInfo
+ {
+ public string Text { get; set; } = "";
+ public DateTime FadeOutAt { get; set; }
+ public DateTime Expiry { get; set; }
+ public bool IsFadingOut => DateTime.UtcNow >= FadeOutAt && DateTime.UtcNow <= Expiry;
+ public bool IsExpired => DateTime.UtcNow > Expiry;
+ }
+
+ private readonly Dictionary
PlayerThoughts = new();
+
+ private PlayerThoughtInfo? GetThoughtState(Player? player)
+ {
+ if (player == null || Engine == null || !Engine.State.Rules.AIHumanMode) return null;
+ if (PlayerThoughts.TryGetValue(player, out var info))
+ {
+ if (!info.IsExpired)
+ {
+ return info;
+ }
+ }
+ return null;
+ }
+
+ private void SetThoughtBubble(Player player, string text, int durationMs = 3200, int fadeDurationMs = 600)
+ {
+ if (string.IsNullOrEmpty(text)) return;
+ var now = DateTime.UtcNow;
+ var fadeAt = now.AddMilliseconds(Math.Max(200, durationMs - fadeDurationMs));
+ var expireAt = now.AddMilliseconds(durationMs);
+
+ var info = new PlayerThoughtInfo
+ {
+ Text = text,
+ FadeOutAt = fadeAt,
+ Expiry = expireAt
+ };
+ PlayerThoughts[player] = info;
+ StateHasChanged();
+
+ _ = Task.Run(async () =>
+ {
+ int delayToFade = (int)(fadeAt - DateTime.UtcNow).TotalMilliseconds;
+ if (delayToFade > 0)
+ {
+ await Task.Delay(delayToFade);
+ if (PlayerThoughts.TryGetValue(player, out var cur) && cur == info)
+ {
+ await InvokeAsync(StateHasChanged);
+ }
+ }
+
+ int delayToExpire = (int)(expireAt - DateTime.UtcNow).TotalMilliseconds;
+ if (delayToExpire > 0)
+ {
+ await Task.Delay(delayToExpire);
+ if (PlayerThoughts.TryGetValue(player, out var cur) && cur == info)
+ {
+ PlayerThoughts.Remove(player);
+ await InvokeAsync(StateHasChanged);
+ }
+ }
+ });
+ }
+
private IEnumerable<(Player Player, string PositionClass)> GetOpponents()
{
if (Engine == null) return Enumerable.Empty<(Player, string)>();
@@ -778,6 +988,65 @@ else
return result;
}
+ private class ActiveJokeInfo
+ {
+ public AIDialogueService.CardJoke Joke { get; set; } = default!;
+ public Player Storyteller { get; set; } = default!;
+ public DateTime FadeOutAt { get; set; }
+ public DateTime Expiry { get; set; }
+ public bool IsFadingOut => DateTime.UtcNow >= FadeOutAt && DateTime.UtcNow <= Expiry;
+ public bool IsExpired => DateTime.UtcNow > Expiry;
+ }
+
+ private ActiveJokeInfo? CurrentJoke;
+
+ private void TriggerCardJoke(AIDialogueService.CardJoke joke, Player storyteller, int durationMs = 7000, int fadeDurationMs = 700)
+ {
+ var now = DateTime.UtcNow;
+ var fadeAt = now.AddMilliseconds(Math.Max(200, durationMs - fadeDurationMs));
+ var expireAt = now.AddMilliseconds(durationMs);
+
+ var info = new ActiveJokeInfo
+ {
+ Joke = joke,
+ Storyteller = storyteller,
+ FadeOutAt = fadeAt,
+ Expiry = expireAt
+ };
+ CurrentJoke = info;
+ StateHasChanged();
+
+ _ = Task.Run(async () =>
+ {
+ int delayToFade = (int)(fadeAt - DateTime.UtcNow).TotalMilliseconds;
+ if (delayToFade > 0)
+ {
+ await Task.Delay(delayToFade);
+ if (CurrentJoke == info)
+ {
+ await InvokeAsync(StateHasChanged);
+ }
+ }
+
+ int delayToExpire = (int)(expireAt - DateTime.UtcNow).TotalMilliseconds;
+ if (delayToExpire > 0)
+ {
+ await Task.Delay(delayToExpire);
+ if (CurrentJoke == info)
+ {
+ CurrentJoke = null;
+ await InvokeAsync(StateHasChanged);
+ }
+ }
+ });
+ }
+
+ private void DismissJoke()
+ {
+ CurrentJoke = null;
+ StateHasChanged();
+ }
+
private async Task ToggleMute()
{
IsMuted = await Fx.ToggleMuteAsync();
@@ -785,12 +1054,16 @@ else
private void HandlePlayAgain()
{
+ PlayerThoughts.Clear();
+ CurrentJoke = null;
StartNewGame();
}
private void ReturnToLobby()
{
_aiCts?.Cancel();
+ PlayerThoughts.Clear();
+ CurrentJoke = null;
IsInLobby = true;
Engine = null;
}
diff --git a/Faraon.Client/wwwroot/css/app.css b/Faraon.Client/wwwroot/css/app.css
index c859763..ccb9ef5 100644
--- a/Faraon.Client/wwwroot/css/app.css
+++ b/Faraon.Client/wwwroot/css/app.css
@@ -310,43 +310,45 @@ body, html {
display: flex;
flex-direction: column;
align-items: center;
- background: rgba(10, 25, 18, 0.85);
- border: 2px solid rgba(255, 255, 255, 0.15);
- border-radius: 16px;
- padding: 6px 12px;
- backdrop-filter: blur(8px);
+ background: rgba(10, 25, 18, 0.88);
+ border: 2.5px solid rgba(255, 255, 255, 0.2);
+ border-radius: 20px;
+ padding: 10px 18px;
+ min-width: 110px;
+ backdrop-filter: blur(10px);
transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
}
.avatar-badge.active-turn {
border-color: var(--gold);
- 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);
+ box-shadow: 0 0 30px rgba(245, 176, 65, 0.95), inset 0 0 16px rgba(245, 176, 65, 0.4);
+ transform: scale(1.12);
animation: turn-pulse 1.8s infinite;
}
@keyframes turn-pulse {
- 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); }
+ 0%, 100% { box-shadow: 0 0 20px rgba(245, 176, 65, 0.75); }
+ 50% { box-shadow: 0 0 38px rgba(245, 176, 65, 1), 0 0 55px rgba(245, 176, 65, 0.6); }
}
/* TURN INDICATOR BADGE (NA ŤAHU) */
.turn-indicator-badge {
position: absolute;
- top: -14px;
+ top: -16px;
left: 50%;
transform: translateX(-50%);
display: inline-flex;
align-items: center;
- gap: 5px;
+ gap: 6px;
background: linear-gradient(135deg, #ffd700, #f5b041);
color: #11281d;
- font-size: 0.66rem;
+ font-size: 0.74rem;
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);
+ letter-spacing: 0.9px;
+ padding: 3px 12px;
+ border-radius: 14px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.6), 0 0 16px rgba(255, 215, 0, 0.85);
border: 1.5px solid #ffffff;
white-space: nowrap;
z-index: 20;
@@ -404,25 +406,28 @@ body, html {
}
.avatar-icon {
- font-size: 1.8rem;
+ font-size: 2.5rem;
line-height: 1;
}
.avatar-name {
- font-size: 0.85rem;
- font-weight: 700;
- margin-top: 2px;
+ font-size: 1.05rem;
+ font-weight: 800;
+ margin-top: 4px;
white-space: nowrap;
+ color: #ffffff;
+ text-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);
}
.avatar-card-count {
- font-size: 0.75rem;
- background: rgba(245, 176, 65, 0.2);
+ font-size: 0.86rem;
+ background: rgba(245, 176, 65, 0.22);
color: var(--gold);
- padding: 2px 8px;
- border-radius: 10px;
- margin-top: 2px;
- font-weight: 700;
+ padding: 3px 12px;
+ border-radius: 12px;
+ margin-top: 4px;
+ font-weight: 800;
+ border: 1px solid rgba(245, 176, 65, 0.35);
}
/* FROZEN / ICE STATUS STYLES */
@@ -671,18 +676,18 @@ body, html {
/* OPPONENT MINI CARDS */
.opponent-mini-cards {
display: flex;
- margin-top: 6px;
+ margin-top: 8px;
justify-content: center;
}
.mini-card-back {
- width: 22px;
- height: 32px;
+ width: 26px;
+ height: 38px;
background: linear-gradient(135deg, #8b1e1e, #4a0d0d);
border: 1px solid #ffd700;
- border-radius: 3px;
- margin-left: -12px;
- box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
+ border-radius: 4px;
+ margin-left: -14px;
+ box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.6);
transition: transform 0.2s ease;
}
@@ -1779,6 +1784,239 @@ body, html {
overflow: hidden;
}
+/* LOBBY COLLAPSIBLE SECTIONS */
+.lobby-accordion-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 14px;
+ background: rgba(0, 0, 0, 0.45);
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ border-radius: 12px;
+ cursor: pointer;
+ transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ user-select: none;
+}
+
+.lobby-accordion-header:hover {
+ background: rgba(245, 176, 65, 0.12);
+ border-color: rgba(245, 176, 65, 0.4);
+}
+
+.lobby-accordion-header.open {
+ border-color: rgba(245, 176, 65, 0.55);
+ background: rgba(245, 176, 65, 0.15);
+}
+
+.badge-rules-summary {
+ font-size: 0.72rem;
+ color: #cbd5e1;
+ background: rgba(255, 255, 255, 0.08);
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ padding: 2px 8px;
+ border-radius: 10px;
+ font-weight: 600;
+}
+
+.accordion-toggle-btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.78rem;
+ font-weight: 800;
+ color: var(--gold);
+ background: rgba(245, 176, 65, 0.12);
+ padding: 4px 10px;
+ border-radius: 8px;
+ border: 1px solid rgba(245, 176, 65, 0.3);
+ transition: all 0.2s ease;
+}
+
+.lobby-accordion-header:hover .accordion-toggle-btn {
+ background: rgba(245, 176, 65, 0.25);
+ border-color: var(--gold);
+}
+
+.accordion-arrow {
+ font-size: 0.7rem;
+}
+
+/* SELECTED DECK PREVIEW CARD */
+.selected-deck-summary-card {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ background: linear-gradient(135deg, rgba(245, 176, 65, 0.14) 0%, rgba(0, 0, 0, 0.6) 100%);
+ border: 2px solid rgba(245, 176, 65, 0.5);
+ border-radius: 14px;
+ padding: 10px 14px;
+ cursor: pointer;
+ transition: all 0.22s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ margin-top: 8px;
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
+}
+
+.selected-deck-summary-card:hover {
+ transform: translateY(-2px);
+ border-color: var(--gold);
+ box-shadow: 0 6px 20px rgba(245, 176, 65, 0.3);
+ background: linear-gradient(135deg, rgba(245, 176, 65, 0.22) 0%, rgba(0, 0, 0, 0.7) 100%);
+}
+
+.badge-active-deck {
+ font-size: 0.66rem;
+ font-weight: 900;
+ background: var(--gold);
+ color: #000;
+ padding: 2px 6px;
+ border-radius: 6px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.btn-deck-change-action {
+ font-size: 0.8rem;
+ font-weight: 800;
+ color: #fff;
+ background: rgba(255, 255, 255, 0.1);
+ padding: 6px 12px;
+ border-radius: 8px;
+ border: 1px solid rgba(255, 255, 255, 0.25);
+ white-space: nowrap;
+ transition: all 0.2s ease;
+}
+
+.selected-deck-summary-card:hover .btn-deck-change-action {
+ background: rgba(245, 176, 65, 0.25);
+ border-color: var(--gold);
+ color: var(--gold);
+}
+
+/* AI THOUGHT BUBBLE */
+.ai-thought-bubble {
+ position: absolute;
+ background: linear-gradient(135deg, rgba(15, 23, 42, 0.98) 0%, rgba(30, 41, 59, 0.98) 100%);
+ border: 2px solid #38bdf8;
+ color: #ffffff;
+ font-size: 0.98rem;
+ font-weight: 700;
+ line-height: 1.4;
+ padding: 10px 18px;
+ border-radius: 20px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.85), 0 0 22px rgba(56, 189, 248, 0.5);
+ z-index: 100;
+ max-width: 320px;
+ min-width: 170px;
+ text-align: center;
+ pointer-events: none;
+ animation: thought-pop 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ backdrop-filter: blur(10px);
+ transition: opacity 0.55s ease, transform 0.55s cubic-bezier(0.4, 0, 0.2, 1), filter 0.55s ease;
+ opacity: 1;
+ filter: blur(0);
+}
+
+@keyframes thought-pop {
+ 0% { transform: scale(0.7) translateY(8px); opacity: 0; }
+ 100% { transform: scale(1) translateY(0); opacity: 1; }
+}
+
+.ai-thought-bubble.fading-out {
+ opacity: 0 !important;
+ filter: blur(3px) !important;
+ pointer-events: none !important;
+}
+
+.thought-bubble-prefix {
+ display: inline-block;
+ margin-right: 6px;
+ font-size: 1.15rem;
+ vertical-align: -1px;
+}
+
+.thought-bubble-text {
+ vertical-align: middle;
+ color: #ffffff;
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
+}
+
+.thought-typing-cursor {
+ display: inline-block;
+ color: #38bdf8;
+ font-weight: 900;
+ margin-left: 2px;
+ animation: cursor-blink 0.6s infinite alternate;
+}
+
+@keyframes cursor-blink {
+ 0% { opacity: 0.15; }
+ 100% { opacity: 1; }
+}
+
+.ai-thought-bubble.slot-top {
+ top: 105%;
+ left: 50%;
+ transform: translateX(-50%);
+ margin-top: 10px;
+}
+
+.ai-thought-bubble.slot-top.fading-out {
+ transform: translateX(-50%) scale(0.9) translateY(-6px);
+}
+
+.ai-thought-bubble.slot-left {
+ top: 50%;
+ left: 110%;
+ transform: translateY(-50%);
+ margin-left: 10px;
+}
+
+.ai-thought-bubble.slot-left.fading-out {
+ transform: translateY(-50%) scale(0.9) translateX(-6px);
+}
+
+.ai-thought-bubble.slot-right {
+ top: 50%;
+ right: 110%;
+ transform: translateY(-50%);
+ margin-right: 10px;
+}
+
+.ai-thought-bubble.slot-right.fading-out {
+ transform: translateY(-50%) scale(0.9) translateX(6px);
+}
+
+.ai-thought-bubble.slot-top-left {
+ top: 105%;
+ left: 0%;
+ margin-top: 10px;
+}
+
+.ai-thought-bubble.slot-top-left.fading-out {
+ transform: scale(0.9) translateY(-6px);
+}
+
+.ai-thought-bubble.slot-top-right {
+ top: 105%;
+ right: 0%;
+ margin-top: 10px;
+}
+
+.ai-thought-bubble.slot-top-right.fading-out {
+ transform: scale(0.9) translateY(-6px);
+}
+
+.ai-thought-bubble.slot-bottom {
+ bottom: 110%;
+ left: 50%;
+ transform: translateX(-50%);
+ margin-bottom: 10px;
+}
+
+.ai-thought-bubble.slot-bottom.fading-out {
+ transform: translateX(-50%) scale(0.9) translateY(6px);
+}
+
/* Canvas Particle Layer */
#particle-canvas {
position: absolute;
@@ -1906,6 +2144,106 @@ body, html {
100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}
+/* AI SPECIAL CARD JOKE BANNER */
+.ai-joke-banner {
+ position: absolute;
+ top: 52px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: linear-gradient(135deg, rgba(35, 12, 50, 0.98) 0%, rgba(15, 23, 42, 0.98) 55%, rgba(45, 15, 25, 0.98) 100%);
+ border: 2px solid #ffd700;
+ border-radius: 20px;
+ box-shadow: 0 16px 45px rgba(0, 0, 0, 0.9), 0 0 35px rgba(255, 215, 0, 0.45), inset 0 0 20px rgba(255, 215, 0, 0.18);
+ padding: 14px 22px;
+ max-width: 580px;
+ width: 90%;
+ z-index: 300;
+ backdrop-filter: blur(12px);
+ animation: joke-entrance 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ transition: opacity 0.65s ease, transform 0.65s cubic-bezier(0.4, 0, 0.2, 1), filter 0.65s ease;
+ color: #ffffff;
+ opacity: 1;
+ filter: blur(0);
+}
+
+@keyframes joke-entrance {
+ 0% { transform: translateX(-50%) translateY(-30px) scale(0.85); opacity: 0; }
+ 100% { transform: translateX(-50%) translateY(0) scale(1); opacity: 1; }
+}
+
+.ai-joke-banner.fading-out {
+ opacity: 0 !important;
+ transform: translateX(-50%) translateY(-25px) scale(0.9) !important;
+ filter: blur(4px) !important;
+ pointer-events: none !important;
+}
+
+.joke-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 8px;
+ border-bottom: 1px solid rgba(255, 215, 0, 0.35);
+ padding-bottom: 6px;
+}
+
+.joke-header-badge {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 0.85rem;
+ font-weight: 900;
+ color: var(--gold);
+ letter-spacing: 0.8px;
+ text-transform: uppercase;
+}
+
+.joke-storyteller {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ background: rgba(245, 176, 65, 0.22);
+ padding: 2px 10px;
+ border-radius: 12px;
+ border: 1px solid rgba(245, 176, 65, 0.45);
+ color: #fff;
+ font-size: 0.78rem;
+ font-weight: 700;
+}
+
+.joke-close-btn {
+ background: none;
+ border: none;
+ color: #94a3b8;
+ font-size: 1.1rem;
+ cursor: pointer;
+ line-height: 1;
+ padding: 2px 6px;
+ border-radius: 6px;
+ transition: all 0.2s ease;
+}
+
+.joke-close-btn:hover {
+ color: #ffd700;
+ background: rgba(255, 255, 255, 0.15);
+}
+
+.joke-setup {
+ font-size: 1.02rem;
+ font-weight: 600;
+ line-height: 1.35;
+ color: #f1f5f9;
+ margin-bottom: 6px;
+}
+
+.joke-punchline {
+ font-size: 1.1rem;
+ font-weight: 800;
+ line-height: 1.35;
+ color: #ffd700;
+ text-shadow: 0 0 14px rgba(255, 215, 0, 0.6);
+}
+
/* HOLOGRAPHIC FOIL SHINE ON CARDS */
.game-card.playable::before {
content: '';
diff --git a/Faraon.Client/wwwroot/js/gameFx.js b/Faraon.Client/wwwroot/js/gameFx.js
index 6786523..26a6c12 100644
--- a/Faraon.Client/wwwroot/js/gameFx.js
+++ b/Faraon.Client/wwwroot/js/gameFx.js
@@ -11,12 +11,18 @@ window.FaraonFx = (function () {
function setSpeedLevel(level) {
switch (level) {
- case 1: speedMultiplier = 0.7; break; // 0.5x
- case 2: speedMultiplier = 0.85; break; // 0.75x
- case 4: speedMultiplier = 1.5; break; // 1.5x
- case 5: speedMultiplier = 2.4; break; // 2.5x Turbo
- case 3:
- default: speedMultiplier = 1.0; break;
+ case -5: speedMultiplier = 0.35; break; // 0.2x Zen
+ case -4: speedMultiplier = 0.45; break; // 0.35x Veľmi pomalá
+ case -3: speedMultiplier = 0.60; break; // 0.5x Pomalá
+ case -2: speedMultiplier = 0.75; break; // 0.65x Pokojná
+ case -1: speedMultiplier = 0.88; break; // 0.8x Zvoľnená
+ case 0: speedMultiplier = 1.0; break; // 1x Normálna
+ case 1: speedMultiplier = 1.25; break; // 1.25x Svižná
+ case 2: speedMultiplier = 1.55; break; // 1.5x Rýchla
+ case 3: speedMultiplier = 2.0; break; // 2x Veľmi rýchla
+ case 4: speedMultiplier = 2.75; break; // 2.75x Turbo
+ case 5: speedMultiplier = 3.8; break; // 4x Blesková
+ default: speedMultiplier = 1.0; break;
}
}
diff --git a/Faraon.Core/AI/AIDialogueService.cs b/Faraon.Core/AI/AIDialogueService.cs
new file mode 100644
index 0000000..981a8cf
--- /dev/null
+++ b/Faraon.Core/AI/AIDialogueService.cs
@@ -0,0 +1,284 @@
+namespace Faraon.Core.AI;
+
+using System;
+using System.Collections.Generic;
+using Faraon.Core.Models;
+
+public static class AIDialogueService
+{
+ private static readonly Random _rng = new();
+
+ // --- EASY (Začiatočník - prezrádza taktiku a neistotu) ---
+ private static readonly string[] EasyThinking =
+ [
+ "Mám ešte jedno eso, len neviem či ho dať teraz...",
+ "Keby mi radšej prišla zeleň, to by bolo super.",
+ "Dúfam, že po mne nikto nehodí sedmu!",
+ "Mám len samé vysoké karty, čo s tým...",
+ "Asi ti zmením farbu, táto sa mi veľmi nepáči.",
+ "Ktorú kartu dať skôr? Táto vyzerá celkom pekne.",
+ "Nemám veľa možností, toto snáď vyjde.",
+ "Mám tu jedného horníka, ale bojím sa ho použiť.",
+ "Ešteže mám túto záchrannú kartu...",
+ "Snáď nikto nezbadá, že mi ostala už len jedna karta!",
+ "Hm... pravidlá hovoria, že toto môžem zahrať, nie?",
+ "Tento ťah snáď nebude úplná katastrofa.",
+ "Čo ak zahrám toto a súper ma zničí?",
+ "Uff, dúfam, že nemáš sedmu na ruke."
+ ];
+
+ // --- MEDIUM (Taktik - realistické premýšľanie) ---
+ private static readonly string[] MediumThinking =
+ [
+ "Skúsim ti podržať farbu, nech nemôžeš vyložiť.",
+ "Túto sedmu si radšej pošetrím na neskôr.",
+ "Pozrime sa, čo s týmto urobíš.",
+ "Ak teraz zahrám túto kartu, ďalší ťah mám voľný.",
+ "Idem po istote, netreba zbytočne riskovať.",
+ "Vidím, že zbieraš túto farbu, tak ti ju zmením.",
+ "Rýchle combo by ťa teraz mohlo prekvapiť.",
+ "Mením taktiku na rýchle vyprázdnenie ruky.",
+ "Kľúčové bude neprepustiť ťah bez tlaku.",
+ "Opatrný ťah, ešte nie je čas na otvorené finále.",
+ "Počítam karty, toto by malo bezpečne prejsť.",
+ "Skúsim ťa prinútiť potiahnuť z balíčka."
+ ];
+
+ // --- HARD (Veľmajster - psychologické blufovanie a prevaha) ---
+ private static readonly string[] HardThinking =
+ [
+ "Presne do tejto pasce som ťa potreboval dostať.",
+ "Mám v ruke presne to, čo ťa bude bolieť.",
+ "Matematicky ti zostávajú maximálne dva ťahy.",
+ "Risk je zisk. Priprav sa na prekvapenie.",
+ "Túto hru mám už prečítanú od prvého kola.",
+ "Si si naozaj istý, že toto bol tvoj najlepší ťah?",
+ "Môj ďalší krok rozhodne o celom zápase.",
+ "Viem presne, aké karty ti ostali v ruke.",
+ "Pripravujem šach-mat, sleduj pozorne.",
+ "Všetko ide presne podľa môjho majstrovského plánu.",
+ "Tvoj osud je spečatený, žiadna záchrana nepríde.",
+ "Geniálny ťah si žiada trochu trpezlivosti."
+ ];
+
+ // --- POSLEDNÁ KARTA (1 karta) ---
+ private static readonly string[] LastCardThoughts =
+ [
+ "Už mi chýba len jedna jediná karta!",
+ "Cítim vôňu víťazstva... už len krok!",
+ "Dúfam, že ma nikto nezablokuje v poslednej chvíli...",
+ "Kto ma teraz nezastaví, ten prehral zápas!",
+ "Už len jeden ťah a titul je môj!"
+ ];
+
+ // --- ČELENIE SEDME (Facing Seven) ---
+ private static readonly string[] FacingSevenThoughts =
+ [
+ "Sedma?! Pozrime sa, či mám odpoveď...",
+ "Dúfam, že mám na ruke ďalšiu sedmu!",
+ "Toto bude bolieť, ak nemám obranu...",
+ "Ak mám Zeleného dolníka, tak sa vám vysmejem!"
+ ];
+
+ // --- ČELENIE ESU (Facing Ace) ---
+ private static readonly string[] FacingAceThoughts =
+ [
+ "Eso? Skúsim to prebiť vlastným esom!",
+ "Zamrznúť teraz by bola veľká škoda...",
+ "Hľadám v ruke eso na prebitie..."
+ ];
+
+ // --- BEŽNÉ LÍZANIE (Musí ťahať z balíčka) ---
+ private static readonly string[] MustDrawThoughts =
+ [
+ "Nič mi nesedí, musím líznuť z balíčka...",
+ "Snáď mi príde niečo poriadne...",
+ "Balíček, nesklam ma v tejto chvíli!",
+ "Škoda, tento ťah musím potiahnuť kartu."
+ ];
+
+ // --- REAKCIE NA UDALOSTI ---
+ private static readonly string[] FreezeReactions =
+ [
+ "Brrr, to je ale poriadna zima!",
+ "Zamrzol som na jedno celé kolo...",
+ "Kto tu pustil túto sibírsku fujavicu?!",
+ "Počkaj, hneď ako rozmrznem, ti to vrátim!",
+ "Stojím, ale môj čas ešte len príde.",
+ "Mráz mi nevadí, aspoň lepšie premyslím stratégiu."
+ ];
+
+ private static readonly string[] AceCounterReactions =
+ [
+ "Žiadne státie! Moje eso okamžite rozpúšťa ľad!",
+ "Na mňa si s mrazom neprídeš!",
+ "Vraciam ti to aj s úrokmi, stojíš ty!"
+ ];
+
+ private static readonly string[] SmallPenaltyDrawReactions =
+ [
+ "Ejha, +3 karty? To ešte hravo zvládnem.",
+ "No pekne, sedma ma neobišla...",
+ "Dobre, beriem 3, ale vrátim vám ich dvojnásobne!"
+ ];
+
+ private static readonly string[] BigPenaltyDrawReactions =
+ [
+ "Ejha, toľko kariet mi do ruky ani nevojde!",
+ "To je nejaká organizovaná pomsta?!",
+ "Kde mám teraz zohnať miesto na ruke pre toľko kariet?!",
+ "No výborne, zachvíľu budem mať celý balíček...",
+ "Tento masívny trest vám v ďalšom kole nezabudnem!"
+ ];
+
+ private static readonly string[] SevenCanceledReactions =
+ [
+ "Haha, Zelený dolník vás zachránil!",
+ "Štít aktivovaný, dnes žiadne ťahanie sediem!",
+ "Itel Reding hovorí jasné: Dnes nie!",
+ "Sedmy vymazané na nulu, ideme ďalej!"
+ ];
+
+ private static readonly string[] RevivedTargetReactions =
+ [
+ "Znova žijem! Pozor na mňa!",
+ "Červená sedma robí zázraky! Som späť v hre!",
+ "Ešte sme neskončili, návrat šampióna!"
+ ];
+
+ private static readonly string[] RevivedSourceReactions =
+ [
+ "Nikam neodchádzaš, ešte si s tebou zahrám!",
+ "Vraciam ťa späť do hry, zábava pokračuje!",
+ "Predčasná radosť, poď pekne naspäť k stolu!"
+ ];
+
+ private static readonly string[] SuitChangeReactions =
+ [
+ "Prepínam hru na moju obľúbenú farbu!",
+ "Uvidíme, či máte na túto farbu odpoveď...",
+ "Trochu to tu zamiešame a zmeníme smer!"
+ ];
+
+ private static readonly string[] BurnReactions =
+ [
+ "BUM! Všetko zhorelo na prach!",
+ "Spálené! A ťahám znova!",
+ "Dokonalé 4 karty, stôl je čistý!"
+ ];
+
+ public static string GetThinkingDialogue(Player player, GameState state)
+ {
+ if (player.Hand.Count == 1)
+ {
+ return PickRandom(LastCardThoughts);
+ }
+
+ if (state.PendingDrawCount > 0)
+ {
+ return PickRandom(FacingSevenThoughts);
+ }
+
+ if (state.PendingSkip)
+ {
+ return PickRandom(FacingAceThoughts);
+ }
+
+ return player.Difficulty switch
+ {
+ AIDifficulty.Easy => PickRandom(EasyThinking),
+ AIDifficulty.Hard => PickRandom(HardThinking),
+ _ => PickRandom(MediumThinking)
+ };
+ }
+
+ public static string GetMustDrawDialogue(Player player)
+ {
+ return PickRandom(MustDrawThoughts);
+ }
+
+ public static string GetEventReaction(GameEvent e, Player player)
+ {
+ return e.Type switch
+ {
+ GameEventType.AceSkipped when e.SourcePlayer == player => PickRandom(FreezeReactions),
+ GameEventType.AceCountered when e.SourcePlayer == player => PickRandom(AceCounterReactions),
+ GameEventType.SevenStacked when e.TargetPlayer == player || (e.ExtraData > 0 && e.SourcePlayer != player) =>
+ e.ExtraData >= 6 ? PickRandom(BigPenaltyDrawReactions) : PickRandom(SmallPenaltyDrawReactions),
+ GameEventType.SevenCanceled when e.SourcePlayer == player => PickRandom(SevenCanceledReactions),
+ GameEventType.PlayerRevived when e.TargetPlayer == player => PickRandom(RevivedTargetReactions),
+ GameEventType.PlayerRevived when e.SourcePlayer == player => PickRandom(RevivedSourceReactions),
+ GameEventType.SuitChanged when e.SourcePlayer == player => PickRandom(SuitChangeReactions),
+ GameEventType.CardsBurned when e.SourcePlayer == player => PickRandom(BurnReactions),
+ _ => ""
+ };
+ }
+
+ public record CardJoke(string Icon, string Category, string Setup, string Punchline);
+
+ // --- VTIPY: MASÍVNY TREST (9 - 12+ kariet) ---
+ private static readonly CardJoke[] MultiDrawJokes =
+ [
+ new("🎒", "KAUZÁLNY BATOH", "Pýta sa Faraón hráča pri stole: 'Prečo si prišiel na zápas s turistickým ruksakom?'", "– 'Aby sa mi tam zmestili všetky tie sedmičky z balíčka!'"),
+ new("🏢", "VRCHOL OPTIMIZMU", "Viete, čo je vrchol hráčskeho optimizmu?", "– Keď po 12 potiahnutých kartách zahlásiš: 'Počkajte, ešte dve kolá a končím!'"),
+ new("🏗️", "STAVEBNÝ ZÁMER", "Rozprávajú sa dvaja architekti pri stole:", "– 'Z toľkých kariet v ruke sa už dá postaviť pekný trojizbový byt aj s terasou!'"),
+ new("🍕", "RODINNÁ PIZZA", "Aký je rozdiel medzi rodinnou pizzou a hráčom s 12 kartami na ruke?", "– Rodinná pizza aspoň niekoho pri stole naozaj poteší!"),
+ new("📜", "DAŇ Z NEHNUTEĽNOSTI", "Dôležitý oznam od správcu turnaja:", "– Hráč s viac ako 10 kartami začína platiť daň za zastavanú plochu stola!")
+ ];
+
+ // --- VTIPY: SPÁLENIE (4 rovnaké karty) ---
+ private static readonly CardJoke[] BurnJokes =
+ [
+ new("🎩", "ILUZIONISTA", "Kúzelník na javisku: 'A teraz sledujte pozorne, nechám zmiznúť 4 karty!'", "– Hráč z rohu kričí: 'Nič nové, to robím v každom kole spálením!'"),
+ new("🚒", "POŽIARNY POPLACH", "Hasiči práve vyhlásili výjazd k nášmu hernému stolu!", "– Niekto tu práve odpálil 4 rovnaké karty a stôl ľahol popolom!"),
+ new("🧙", "LEGENDA V OHNI", "Stretnú sa 4 rovnaké karty v balíčku a jedna hovorí:", "– 'Dnes z nás bude buď slávna legenda, alebo čistý popol!'"),
+ new("🔥", "ČISTÁ POÉZIA", "Spáliť 4 karty naraz jedným ťahom nie je len obyčajné šťastie...", "– ...je to čisté vizuálne umenie a nočná mora pre súperov!")
+ ];
+
+ // --- VTIPY: DLHÁ HRA (TurnCount >= 28) ---
+ private static readonly CardJoke[] LongGameJokes =
+ [
+ new("⏳", "RODINNÝ DÔCHODOK", "Manželka volá kartárovi: 'Kedy už prídeš domov z toho zápasu?'", "– 'Drahá, ešte jedno kolo a môžeme ísť rovno spolu do dôchodku!'"),
+ new("🏛️", "PAMIATKOVÝ ÚRAD", "Táto partia trvá už tak neskutočne dlho...", "– ...že si na ďalší ťah budeme musieť pýtať povolenie od pamiatkárov!"),
+ new("🧔", "ČAS NA HOLENIE", "Začiatok zápasu: hladko oholení mladíci plní síl.", "– Aktuálny stav: fúzatí veteráni čakajúci na záchrannú zeleň!"),
+ new("🕰️", "ŠPORTOVÝ VÝKON", "Lekár sa pýta pacienta: 'Koľko hodín denne venujete športu?'", "– 'Už šiestu hodinu bez prestávky sedím pri napínavom Faraónovi!'")
+ ];
+
+ // --- VTIPY: OŽIVENIE (Červená sedma) ---
+ private static readonly CardJoke[] ReviveJokes =
+ [
+ new("🧟", "ZÁKAZ ODCHODU", "Lekár konštatuje: 'Hráč úspešne vyhral a odchádza domov.'", "– Červená sedma na to: 'Ešte nie, kamarát! Poď pekne naspäť k stolu!'"),
+ new("⚡", "VZKRIESENIE ŠAMPIÓNA", "Viete, čo je rýchlejšie ako ranný pondelkový budík?", "– Hráč vrátený do hry Červenou sedmou s chuťou po veľkej pomste!")
+ ];
+
+ public static CardJoke? GetContextualJoke(GameEventType? type, int extraData, int turnCount)
+ {
+ if (type == GameEventType.SevenStacked && extraData >= 9)
+ {
+ return PickRandom(MultiDrawJokes);
+ }
+
+ if (type == GameEventType.CardsBurned)
+ {
+ return PickRandom(BurnJokes);
+ }
+
+ if (type == GameEventType.PlayerRevived)
+ {
+ return PickRandom(ReviveJokes);
+ }
+
+ if (turnCount >= 28 && turnCount % 16 == 0)
+ {
+ return PickRandom(LongGameJokes);
+ }
+
+ return null;
+ }
+
+ private static T PickRandom(T[] options)
+ {
+ if (options == null || options.Length == 0) return default!;
+ return options[_rng.Next(options.Length)];
+ }
+}
\ No newline at end of file
diff --git a/Faraon.Core/Models/RuleOptions.cs b/Faraon.Core/Models/RuleOptions.cs
index 0287523..b144f1e 100644
--- a/Faraon.Core/Models/RuleOptions.cs
+++ b/Faraon.Core/Models/RuleOptions.cs
@@ -70,22 +70,29 @@ public class RuleOptions
public bool PassAndPlayPrivacyScreen { get; set; } = false;
///
- /// Úroveň rýchlosti hry (1 = 0.5x, 2 = 0.75x, 3 = 1x Normálna, 4 = 1.5x Rýchla, 5 = 2.5x Turbo).
+ /// Úroveň rýchlosti hry od -5 (Extra pomalá 0.2x) cez 0 (1x Normálna) až po +5 (Blesková 4x).
///
- public int SpeedLevel { get; set; } = 3;
+ public int SpeedLevel { get; set; } = 0;
///
- /// Rýchlosť AI ťahov v milisekundách podľa zvolenej úrovne rýchlosti.
+ /// Rýchlosť AI ťahov v milisekundách podľa zvolenej úrovne rýchlosti (-5 až +5).
///
public int AIDelayMs
{
get => SpeedLevel switch
{
- 1 => 1600,
- 2 => 1200,
- 4 => 500,
- 5 => 200,
- _ => 900
+ -5 => 3200,
+ -4 => 2600,
+ -3 => 2100,
+ -2 => 1700,
+ -1 => 1400,
+ 0 => 1100,
+ 1 => 850,
+ 2 => 650,
+ 3 => 450,
+ 4 => 300,
+ 5 => 150,
+ _ => 1100
};
set { }
}
@@ -100,4 +107,9 @@ public class RuleOptions
/// Hrá sa až kým nezostane iba jeden celkový víťaz.
///
public bool OlympicMode { get; set; } = false;
+
+ ///
+ /// AI Human Mode: AI hráči zobrazujú premýšľacie bubliny s taktickými a zábavnými hláškami a reagujú na herné udalosti.
+ ///
+ public bool AIHumanMode { get; set; } = false;
}
diff --git a/Faraon.Tests/GameEngineTests.cs b/Faraon.Tests/GameEngineTests.cs
index 2ef9be4..7d27cc1 100644
--- a/Faraon.Tests/GameEngineTests.cs
+++ b/Faraon.Tests/GameEngineTests.cs
@@ -431,4 +431,49 @@ public class GameEngineTests
// When facing penalty and disabled, Green Under Knave cannot be played
Assert.False(ActionValidator.CanPlayCard(greenUnderKnave, topSeven, null, 3, false, rules));
}
+
+ [Fact]
+ public void AIDialogueService_ReturnsAppropriateDialogues()
+ {
+ var easyAI = new Player("AI Easy", PlayerType.AI, "🦊", AIDifficulty.Easy);
+ var hardAI = new Player("AI Hard", PlayerType.AI, "🤖", AIDifficulty.Hard);
+ var state = new GameState();
+
+ easyAI.Hand.Add(new Card(Suit.Hearts, Rank.Eight));
+ easyAI.Hand.Add(new Card(Suit.Hearts, Rank.Nine));
+
+ // Thinking dialogue for easy AI
+ var easyThought = AIDialogueService.GetThinkingDialogue(easyAI, state);
+ Assert.False(string.IsNullOrWhiteSpace(easyThought));
+
+ // Thinking dialogue for hard AI
+ var hardThought = AIDialogueService.GetThinkingDialogue(hardAI, state);
+ Assert.False(string.IsNullOrWhiteSpace(hardThought));
+
+ // Event reactions
+ var freezeEvent = new GameEvent(GameEventType.AceSkipped, easyAI);
+ var freezeReaction = AIDialogueService.GetEventReaction(freezeEvent, easyAI);
+ Assert.False(string.IsNullOrWhiteSpace(freezeReaction));
+
+ var penaltyEvent = new GameEvent(GameEventType.SevenStacked, null, TargetPlayer: easyAI, ExtraData: 6);
+ var penaltyReaction = AIDialogueService.GetEventReaction(penaltyEvent, easyAI);
+ Assert.False(string.IsNullOrWhiteSpace(penaltyReaction));
+
+ var cancelEvent = new GameEvent(GameEventType.SevenCanceled, easyAI);
+ var cancelReaction = AIDialogueService.GetEventReaction(cancelEvent, easyAI);
+ Assert.False(string.IsNullOrWhiteSpace(cancelReaction));
+
+ // Card jokes
+ var multiDrawJoke = AIDialogueService.GetContextualJoke(GameEventType.SevenStacked, 12, 10);
+ Assert.NotNull(multiDrawJoke);
+ Assert.False(string.IsNullOrWhiteSpace(multiDrawJoke.Punchline));
+
+ var burnJoke = AIDialogueService.GetContextualJoke(GameEventType.CardsBurned, 0, 10);
+ Assert.NotNull(burnJoke);
+ Assert.False(string.IsNullOrWhiteSpace(burnJoke.Setup));
+
+ var longGameJoke = AIDialogueService.GetContextualJoke(null, 0, 32);
+ Assert.NotNull(longGameJoke);
+ Assert.False(string.IsNullOrWhiteSpace(longGameJoke.Punchline));
+ }
}