diff --git a/Faraon.Client/Components/Lobby.razor b/Faraon.Client/Components/Lobby.razor index fec83c6..9cca206 100644 --- a/Faraon.Client/Components/Lobby.razor +++ b/Faraon.Client/Components/Lobby.razor @@ -56,178 +56,160 @@ } - -
- PRAVIDLÁ A NASTAVENIA: -
-
- - -
-
- ❤️ -
-
Červená sedma (Oživenie / Resurrect)
-
Spôsob vrátenia skončeného hráča späť do hry
+ +
+
+
+ ⚙️ + + PRAVIDLÁ A NASTAVENIA HRY + + @if (!ShowRules) + { + + @(Rules.InitialCardCount) kariet • Sedmy (+3) • @(Rules.SpeedLevel)x Rýchlosť + + } +
+
+ @(ShowRules ? "Zbaliť" : "Rozbaliť") + @(ShowRules ? "▲" : "▼") +
+
+ + @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 -

-
-
-
- Original Mariáš - Rub -
-
-
Tradičný Mariáš 🎴
-
Autentický dvojhlavý Tell vzor (Piatnik)
-
+
+
+ 🎨 + + VIZUÁLNY ŠTÝL BALÍČKA +
- -
-
- Zbojníci - Rub -
-
-
Zbojníci & Legendy 🪓
-
Jánošík, Uhorčík, výšivky a tatranský folklór
-
-
- -
-
- SpongeBob Cartoon - Rub -
-
-
SpongeBob Cartoon 🧽
-
Bikini Bottom edícia (postavičky a veľké symboly)
-
-
- -
-
- Cyberpunk 2077 - Rub -
-
-
Cyberpunk 2077 ⚡
-
Neónový holografický štýl a kybernetickí hrdinovia
-
-
- -
-
- Fantasy RPG - Rub -
-
-
Fantasy RPG 🐉
-
Dračí lordi, mágovia, runy a rytieri
-
-
- -
-
- Staroveký Egypt - Rub -
-
-
Staroveký Egypt ⚔️
-
Zlatý papyrus, Tutanchamón, Anubis a pyramídy
-
-
- -
-
- Kráľovské zvieratká - Rub -
-
-
Kráľovské Zvieratká 🐱
-
Mačičky a psíkovia v kráľovských korunkách
-
-
- -
-
- Retro Pixel - Rub -
-
-
Retro 8-Bit Pixel 🕹️
-
Nostalgický arkádový štýl klasických hier
-
+
+ @(ShowDeckSelector ? "Zavrieť galériu" : "Zmeniť balíček") + @(ShowDeckSelector ? "▲" : "▼")
+ + +
+
+ @CurrentDeck.Name + Rub +
+
+
+ @CurrentDeck.Name + Vybraný +
+
@CurrentDeck.Desc
+
+
+ @(ShowDeckSelector ? "▲ Skryť" : "🎨 Zmeniť štýl ▾") +
+
+ + @if (ShowDeckSelector) + { +
+ @foreach (var d in AvailableDecks) + { +
+
+ @d.Name + Rub +
+
+
@d.Name
+
@d.Desc
+
+
+ } +
+ }
@@ -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 }
-
- Ťah #@Engine.State.TurnCount • Na rade: - - @Engine.State.CurrentPlayer.Avatar @Engine.State.CurrentPlayer.Name - -
-
- - - - - -
+ + @if (CurrentJoke != null) + { +
+
+
+ @CurrentJoke.Joke.Icon + 🎭 @CurrentJoke.Joke.Category +
+
+ + @CurrentJoke.Storyteller.Avatar + @CurrentJoke.Storyteller.Name + + +
+
+
@CurrentJoke.Joke.Setup
+
@CurrentJoke.Joke.Punchline
+
+ } +
@@ -55,12 +72,15 @@ else { var isOppFrozen = Engine.State.PendingSkip && Engine.State.CurrentPlayer == opp.Player; var canOppBeRevived = Engine.State.Rules.RedSevenRevives && Engine.State.LastFinishedPlayer == opp.Player && Engine.State.ReviveTurnsRemaining > 0; + var thought = GetThoughtState(opp.Player); } @@ -193,6 +213,17 @@ else
} + + @{ + var bottomThought = GetThoughtState(ViewingPlayer); + } + @if (bottomThought != null && !string.IsNullOrEmpty(bottomThought.Text)) + { +
+ 💭 + @bottomThought.Text +
+ }
@@ -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)); + } }