diff --git a/Faraon.Client/Components/Lobby.razor b/Faraon.Client/Components/Lobby.razor index 9cca206..add81c0 100644 --- a/Faraon.Client/Components/Lobby.razor +++ b/Faraon.Client/Components/Lobby.razor @@ -1,8 +1,12 @@ @namespace Faraon.Client.Components +@using Faraon.Core.AI
+
+ FARAÓN Logo +

FARAÓN

Tradičná mariášová kartová hra s moderným nábojom
@@ -43,13 +47,14 @@ - + @if (p.Type == PlayerType.AI) { - + @foreach (var pers in AIPersonality.All) + { + + } }
@@ -255,9 +260,9 @@ private List PlayerConfigs = new() { new Player("Ty (Hráč 1)", PlayerType.Human, "👑"), - new Player("AI Jano", PlayerType.AI, "🦊", AIDifficulty.Medium), - new Player("AI Zuzka", PlayerType.AI, "👸", AIDifficulty.Hard), - new Player("AI Mišo", PlayerType.AI, "🤖", AIDifficulty.Easy) + new Player(AIPersonality.Fox.Name, PlayerType.AI, AIPersonality.Fox.BaseAvatar, AIPersonality.Fox.Difficulty, AIPersonality.Fox.Id), + new Player(AIPersonality.Queen.Name, PlayerType.AI, AIPersonality.Queen.BaseAvatar, AIPersonality.Queen.Difficulty, AIPersonality.Queen.Id), + new Player(AIPersonality.Robot.Name, PlayerType.AI, AIPersonality.Robot.BaseAvatar, AIPersonality.Robot.Difficulty, AIPersonality.Robot.Id) }; private void SetPlayerCount(int count) @@ -272,10 +277,27 @@ PlayerConfigs[index].Avatar = Avatars[nextIdx]; } + private void SelectPersonality(int index, string? personalityId) + { + if (string.IsNullOrEmpty(personalityId)) return; + var pers = AIPersonality.GetById(personalityId); + PlayerConfigs[index].PersonalityId = pers.Id; + PlayerConfigs[index].Name = pers.Name; + PlayerConfigs[index].Avatar = pers.BaseAvatar; + PlayerConfigs[index].Difficulty = pers.Difficulty; + } + + private string GetDiffLabel(AIDifficulty diff) => diff switch + { + AIDifficulty.Easy => "Ľahký", + AIDifficulty.Hard => "Veľmajster", + _ => "Taktik" + }; + private async Task StartGame() { var activePlayers = PlayerConfigs.Take(SelectedPlayerCount).Select(p => - new Player(p.Name, p.Type, p.Avatar, p.Difficulty) + new Player(p.Name, p.Type, p.Avatar, p.Difficulty, p.PersonalityId) ).ToList(); await OnStartGame.InvokeAsync((activePlayers, Rules)); diff --git a/Faraon.Client/Components/PlayerSlot.razor b/Faraon.Client/Components/PlayerSlot.razor index 6cd4b6a..59d1ebc 100644 --- a/Faraon.Client/Components/PlayerSlot.razor +++ b/Faraon.Client/Components/PlayerSlot.razor @@ -1,7 +1,7 @@ @namespace Faraon.Client.Components
-
0 ? "can-be-revived" : "")"> +
0 ? "can-be-revived" : "") emotion-aura-@Player.CurrentEmotion.ToString().ToLowerInvariant()"> @if (IsFrozen && !Player.HasFinished && !Player.IsEliminated) {
@@ -24,7 +24,13 @@
} -
@Player.Avatar
+
+ @Player.GetLiveAvatar() + @if (!Player.HasFinished && !Player.IsEliminated && Player.GetEmotionBadge() != null) + { + @Player.GetEmotionBadge() + } +
@Player.Name @if (Player.Type == PlayerType.AI) diff --git a/Faraon.Client/Pages/Home.razor b/Faraon.Client/Pages/Home.razor index 9860701..f338ebb 100644 --- a/Faraon.Client/Pages/Home.razor +++ b/Faraon.Client/Pages/Home.razor @@ -17,6 +17,7 @@ else
+ Logo

FARAÓN

@if (Engine.State.Rules.OlympicMode) { @@ -25,20 +26,21 @@ else }
-
+
- - +
@@ -174,7 +176,7 @@ else }
- @ViewingPlayer.Avatar + @ViewingPlayer.GetLiveAvatar() @ViewingPlayer.Name @if (ViewingPlayer.Type == PlayerType.AI) @@ -475,6 +477,40 @@ else } } + // Dynamické emócie hráčov na herné udalosti + if (e.SourcePlayer != null) + { + switch (e.Type) + { + case GameEventType.AceSkipped: + SetPlayerEmotion(e.SourcePlayer, PlayerEmotion.Freezing, 3500); + break; + case GameEventType.AceCountered: + case GameEventType.SevenCanceled: + SetPlayerEmotion(e.SourcePlayer, PlayerEmotion.Relieved, 3000); + break; + case GameEventType.CardsBurned: + SetPlayerEmotion(e.SourcePlayer, PlayerEmotion.Confident, 3500); + break; + case GameEventType.PlayerFinished: + case GameEventType.MatchWon: + SetPlayerEmotion(e.SourcePlayer, PlayerEmotion.Celebrating, 10000); + break; + } + } + if (e.TargetPlayer != null) + { + switch (e.Type) + { + case GameEventType.SevenStacked: + SetPlayerEmotion(e.TargetPlayer, e.ExtraData >= 6 ? PlayerEmotion.Shocked : PlayerEmotion.Angry, 3500); + break; + case GameEventType.PlayerRevived: + SetPlayerEmotion(e.TargetPlayer, PlayerEmotion.Confident, 3500); + break; + } + } + if (Engine != null && Engine.State.CurrentPlayer.Hand.Count == 1 && !Engine.State.CurrentPlayer.HasFinished) { await Fx.PlayHeartbeatAsync(); @@ -746,6 +782,27 @@ else var aiPlayer = Engine.State.CurrentPlayer; if (aiPlayer.Type != PlayerType.AI) return; + if (aiPlayer.Hand.Count <= 2) + { + SetPlayerEmotion(aiPlayer, PlayerEmotion.Confident, 3500); + } + else if (aiPlayer.Hand.Count >= 8) + { + SetPlayerEmotion(aiPlayer, PlayerEmotion.Desperate, 3500); + } + else if (Engine.State.PendingSkip) + { + SetPlayerEmotion(aiPlayer, PlayerEmotion.Freezing, 3500); + } + else if (Engine.State.PendingDrawCount > 0) + { + SetPlayerEmotion(aiPlayer, PlayerEmotion.Shocked, 3500); + } + else + { + SetPlayerEmotion(aiPlayer, PlayerEmotion.Thinking, 3500); + } + if (Engine.State.Rules.AIHumanMode) { string thought = AIDialogueService.GetThinkingDialogue(aiPlayer, Engine.State); @@ -962,6 +1019,25 @@ else }); } + private void SetPlayerEmotion(Player player, PlayerEmotion emotion, int durationMs = 3000) + { + player.CurrentEmotion = emotion; + StateHasChanged(); + + if (emotion != PlayerEmotion.Neutral && emotion != PlayerEmotion.Celebrating) + { + _ = Task.Run(async () => + { + await Task.Delay(durationMs); + if (player.CurrentEmotion == emotion) + { + player.CurrentEmotion = PlayerEmotion.Neutral; + await InvokeAsync(StateHasChanged); + } + }); + } + } + private IEnumerable<(Player Player, string PositionClass)> GetOpponents() { if (Engine == null) return Enumerable.Empty<(Player, string)>(); diff --git a/Faraon.Client/wwwroot/assets/logo/logo_1_royal.jpg b/Faraon.Client/wwwroot/assets/logo/logo_1_royal.jpg new file mode 100644 index 0000000..307cde7 Binary files /dev/null and b/Faraon.Client/wwwroot/assets/logo/logo_1_royal.jpg differ diff --git a/Faraon.Client/wwwroot/assets/logo/logo_2_modern.jpg b/Faraon.Client/wwwroot/assets/logo/logo_2_modern.jpg new file mode 100644 index 0000000..7378d31 Binary files /dev/null and b/Faraon.Client/wwwroot/assets/logo/logo_2_modern.jpg differ diff --git a/Faraon.Client/wwwroot/assets/logo/logo_3_vintage.jpg b/Faraon.Client/wwwroot/assets/logo/logo_3_vintage.jpg new file mode 100644 index 0000000..ece18cd Binary files /dev/null and b/Faraon.Client/wwwroot/assets/logo/logo_3_vintage.jpg differ diff --git a/Faraon.Client/wwwroot/assets/logo/logo_4_pyramid.jpg b/Faraon.Client/wwwroot/assets/logo/logo_4_pyramid.jpg new file mode 100644 index 0000000..633045d Binary files /dev/null and b/Faraon.Client/wwwroot/assets/logo/logo_4_pyramid.jpg differ diff --git a/Faraon.Client/wwwroot/assets/logo/logo_5_shield.jpg b/Faraon.Client/wwwroot/assets/logo/logo_5_shield.jpg new file mode 100644 index 0000000..745598f Binary files /dev/null and b/Faraon.Client/wwwroot/assets/logo/logo_5_shield.jpg differ diff --git a/Faraon.Client/wwwroot/assets/logo/logo_6_mascot.jpg b/Faraon.Client/wwwroot/assets/logo/logo_6_mascot.jpg new file mode 100644 index 0000000..1f62176 Binary files /dev/null and b/Faraon.Client/wwwroot/assets/logo/logo_6_mascot.jpg differ diff --git a/Faraon.Client/wwwroot/css/app.css b/Faraon.Client/wwwroot/css/app.css index ccb9ef5..d31b4fd 100644 --- a/Faraon.Client/wwwroot/css/app.css +++ b/Faraon.Client/wwwroot/css/app.css @@ -57,32 +57,103 @@ body, html { color: #ffffff; } -/* App Loader */ +/* App Loader (WASM modern esports Pharaoh loader) */ .app-loader { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; - background: radial-gradient(circle at center, #1b4d3e 0%, #050b08 100%); + background: radial-gradient(circle at center, #0f2b38 0%, #061118 60%, #02060a 100%); + color: #ffffff; + user-select: none; } -.loader-card { - font-size: 4rem; - animation: bounce 1.2s infinite alternate ease-in-out; +.loader-logo-container { + position: relative; + width: 140px; + height: 140px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 24px; +} + +.loader-logo-img { + width: 120px; + height: 120px; + border-radius: 50%; + object-fit: cover; + border: 3px solid #00e5ff; + box-shadow: 0 0 30px rgba(0, 229, 255, 0.6), 0 0 60px rgba(255, 215, 0, 0.3); + animation: loader-pulse 2.2s infinite ease-in-out alternate; + z-index: 2; +} + +.loader-spinner-ring { + position: absolute; + top: 0; + left: 0; + width: 140px; + height: 140px; + border-radius: 50%; + border: 3px solid transparent; + border-top-color: #00e5ff; + border-right-color: #ffd700; + animation: loader-spin 1.4s linear infinite; + z-index: 1; +} + +@keyframes loader-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@keyframes loader-pulse { + 0% { transform: scale(0.96); box-shadow: 0 0 20px rgba(0, 229, 255, 0.5); } + 100% { transform: scale(1.04); box-shadow: 0 0 45px rgba(0, 229, 255, 0.85), 0 0 70px rgba(255, 215, 0, 0.4); } +} + +.loader-title { + font-family: 'Cinzel', serif; + font-size: 2.2rem; + font-weight: 900; + letter-spacing: 4px; + color: var(--gold); + text-shadow: 0 0 25px rgba(255, 215, 0, 0.7), 0 0 45px rgba(0, 229, 255, 0.5); + margin-bottom: 6px; } .loader-text { - margin-top: 1rem; - font-family: 'Cinzel', serif; - font-size: 1.5rem; - letter-spacing: 2px; - color: var(--gold); + font-family: 'Outfit', sans-serif; + font-size: 1.05rem; + color: #94a3b8; + letter-spacing: 1px; + margin-bottom: 20px; } -@keyframes bounce { - 0% { transform: translateY(0) rotate(-5deg); } - 100% { transform: translateY(-20px) rotate(5deg); } +.loader-progress-bar { + width: 240px; + height: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + overflow: hidden; + position: relative; + border: 1px solid rgba(0, 229, 255, 0.3); +} + +.loader-progress-fill { + width: 100%; + height: 100%; + background: linear-gradient(90deg, #00e5ff, #ffd700, #00e5ff); + background-size: 200% 100%; + animation: loader-progress-anim 1.8s infinite linear; + border-radius: 10px; +} + +@keyframes loader-progress-anim { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } } /* MAIN GAME CONTAINER */ @@ -135,45 +206,63 @@ body, html { gap: 8px; } -.btn-game-icon { - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - color: #fff; - border-radius: 50%; - width: 38px; - height: 38px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.2s ease; - font-size: 1.1rem; +/* Top Game Header Controls */ +.btn-header-action, +.btn-speed-control { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + gap: 6px !important; + height: 32px !important; + padding: 0 14px !important; + border-radius: 18px !important; + font-family: 'Outfit', sans-serif !important; + font-size: 0.85rem !important; + font-weight: 700 !important; + color: #e2e8f0 !important; + background: rgba(10, 25, 20, 0.85) !important; + border: 1.5px solid rgba(245, 176, 65, 0.45) !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5), inset 0 1px 1px rgba(255, 255, 255, 0.1) !important; + backdrop-filter: blur(8px) !important; + cursor: pointer !important; + transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; + user-select: none !important; + text-decoration: none !important; + white-space: nowrap !important; + outline: none !important; + line-height: 1 !important; } -.btn-game-icon:hover { - background: rgba(245, 176, 65, 0.3); - border-color: var(--gold); - transform: scale(1.1); +.btn-header-action:hover, +.btn-speed-control:hover { + background: rgba(245, 176, 65, 0.25) !important; + border-color: var(--gold) !important; + color: #ffffff !important; + box-shadow: 0 0 14px rgba(245, 176, 65, 0.55), 0 4px 12px rgba(0, 0, 0, 0.6) !important; + transform: translateY(-2px) scale(1.04) !important; +} + +.btn-header-action:active, +.btn-speed-control:active { + transform: translateY(0) scale(0.97) !important; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5) !important; } .btn-speed-control { - width: auto !important; - min-width: 60px; - padding: 0 10px; - border-radius: 20px !important; - font-size: 0.8rem !important; - font-weight: 800; - letter-spacing: 0.3px; - background: rgba(245, 176, 65, 0.15) !important; - border-color: rgba(245, 176, 65, 0.45) !important; color: var(--gold) !important; - white-space: nowrap; + font-weight: 800 !important; + letter-spacing: 0.3px !important; } -.btn-speed-control:hover { - background: rgba(245, 176, 65, 0.35) !important; - border-color: var(--gold) !important; - box-shadow: 0 0 10px rgba(245, 176, 65, 0.4); +.btn-header-action.btn-sound-muted { + border-color: rgba(239, 68, 68, 0.5) !important; + color: #f87171 !important; +} + +.btn-header-action.btn-sound-muted:hover { + border-color: #ef4444 !important; + background: rgba(239, 68, 68, 0.22) !important; + box-shadow: 0 0 14px rgba(239, 68, 68, 0.55) !important; } /* TABLE FELT & SLOTS */ @@ -405,9 +494,134 @@ body, html { 100% { transform: translate(-50%, -50%) scale(1.25); } } +.avatar-icon-wrapper { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + .avatar-icon { font-size: 2.5rem; line-height: 1; + display: inline-block; + transition: transform 0.25s ease; +} + +.avatar-emotion-dot-badge { + position: absolute; + bottom: -4px; + right: -10px; + font-size: 1.15rem; + filter: drop-shadow(0 2px 5px rgba(0, 0, 0, 0.85)); + animation: badge-pop 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); + z-index: 15; +} + +@keyframes badge-pop { + 0% { transform: scale(0) rotate(-15deg); opacity: 0; } + 100% { transform: scale(1) rotate(0deg); opacity: 1; } +} + +/* AVATAR EMOTION MICRO-ANIMATIONS */ +.emotion-anim-thinking { + animation: anim-thinking 1.8s ease-in-out infinite alternate; +} + +@keyframes anim-thinking { + 0% { transform: translateY(0) rotate(-2deg); } + 100% { transform: translateY(-4px) rotate(3deg); } +} + +.emotion-anim-confident { + animation: anim-confident 1.4s ease-in-out infinite alternate; +} + +@keyframes anim-confident { + 0% { transform: scale(1); } + 100% { transform: scale(1.14); } +} + +.emotion-anim-shocked { + animation: anim-shocked 0.35s ease-in-out infinite alternate; +} + +@keyframes anim-shocked { + 0% { transform: translateX(-4px) scale(1.1); } + 100% { transform: translateX(4px) scale(1.1); } +} + +.emotion-anim-freezing { + animation: anim-freezing 0.12s ease-in-out infinite; +} + +@keyframes anim-freezing { + 0% { transform: translate(-2px, -1px); } + 50% { transform: translate(2px, 1px); } + 100% { transform: translate(-1px, 2px); } +} + +.emotion-anim-angry { + animation: anim-angry 0.22s ease-in-out infinite alternate; +} + +@keyframes anim-angry { + 0% { transform: rotate(-5deg) scale(1.06); } + 100% { transform: rotate(5deg) scale(1.06); } +} + +.emotion-anim-relieved { + animation: anim-relieved 1.4s ease-in-out infinite alternate; +} + +@keyframes anim-relieved { + 0% { transform: translateY(0); } + 100% { transform: translateY(-3px); } +} + +.emotion-anim-desperate { + animation: anim-desperate 1.8s ease-in-out infinite alternate; +} + +@keyframes anim-desperate { + 0% { transform: translateY(0) scale(0.95); } + 100% { transform: translateY(4px) scale(0.88); } +} + +.emotion-anim-celebrating { + animation: anim-celebrating 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275) infinite alternate; +} + +@keyframes anim-celebrating { + 0% { transform: translateY(0) rotate(-7deg) scale(1.1); } + 100% { transform: translateY(-8px) rotate(7deg) scale(1.22); } +} + +/* EMOTION AURAS ON AVATAR BADGE */ +.avatar-badge.emotion-aura-angry { + border-color: #ef4444 !important; + box-shadow: 0 0 25px rgba(239, 68, 68, 0.85), inset 0 0 14px rgba(239, 68, 68, 0.4) !important; +} + +.avatar-badge.emotion-aura-shocked { + border-color: #f59e0b !important; + box-shadow: 0 0 25px rgba(245, 158, 11, 0.85), inset 0 0 14px rgba(245, 158, 11, 0.4) !important; +} + +.avatar-badge.emotion-aura-confident { + border-color: #eab308 !important; + box-shadow: 0 0 25px rgba(234, 179, 8, 0.85), inset 0 0 14px rgba(234, 179, 8, 0.4) !important; +} + +.avatar-badge.emotion-aura-celebrating { + border-color: #ffd700 !important; + box-shadow: 0 0 35px rgba(255, 215, 0, 1), inset 0 0 20px rgba(255, 215, 0, 0.6) !important; +} + +.avatar-badge.emotion-aura-relieved { + border-color: #22c55e !important; + box-shadow: 0 0 25px rgba(34, 197, 94, 0.85), inset 0 0 14px rgba(34, 197, 94, 0.4) !important; } .avatar-name { @@ -1557,6 +1771,27 @@ body, html { margin-bottom: 24px; } +.lobby-logo-wrapper { + display: flex; + justify-content: center; + margin-bottom: 12px; +} + +.lobby-logo-img { + width: 96px; + height: 96px; + border-radius: 50%; + object-fit: cover; + border: 3px solid #00e5ff; + box-shadow: 0 0 25px rgba(0, 229, 255, 0.65), 0 0 45px rgba(255, 215, 0, 0.35), 0 8px 24px rgba(0, 0, 0, 0.85); + animation: logo-glow 3s infinite ease-in-out alternate; +} + +@keyframes logo-glow { + 0% { transform: scale(1); box-shadow: 0 0 20px rgba(0, 229, 255, 0.5), 0 0 35px rgba(255, 215, 0, 0.25); } + 100% { transform: scale(1.05); box-shadow: 0 0 40px rgba(0, 229, 255, 0.9), 0 0 60px rgba(255, 215, 0, 0.5); } +} + .lobby-title { font-family: 'Cinzel', serif; font-size: 2.5rem; diff --git a/Faraon.Client/wwwroot/favicon.png b/Faraon.Client/wwwroot/favicon.png index 8422b59..7378d31 100644 Binary files a/Faraon.Client/wwwroot/favicon.png and b/Faraon.Client/wwwroot/favicon.png differ diff --git a/Faraon.Client/wwwroot/index.html b/Faraon.Client/wwwroot/index.html index 1794858..b139590 100644 --- a/Faraon.Client/wwwroot/index.html +++ b/Faraon.Client/wwwroot/index.html @@ -11,15 +11,23 @@ - + +
-
🃏
-
Načítavam Faraóna...
+
+ FARAÓN +
+
+
FARAÓN
+
Pripravujem stôl a miešam karty...
+
+
+
diff --git a/Faraon.Core/AI/AIPersonality.cs b/Faraon.Core/AI/AIPersonality.cs new file mode 100644 index 0000000..b1ab852 --- /dev/null +++ b/Faraon.Core/AI/AIPersonality.cs @@ -0,0 +1,243 @@ +namespace Faraon.Core.AI; + +using System.Collections.Generic; +using Faraon.Core.Models; + +public enum PlayerEmotion +{ + Neutral = 0, + Thinking = 1, + Confident = 2, + Shocked = 3, + Freezing = 4, + Angry = 5, + Relieved = 6, + Desperate = 7, + Celebrating = 8 +} + +public class AIPersonality +{ + public string Id { get; init; } = ""; + public string Name { get; init; } = ""; + public string Title { get; init; } = ""; + public string BaseAvatar { get; init; } = "👤"; + public string Description { get; init; } = ""; + public AIDifficulty Difficulty { get; init; } = AIDifficulty.Medium; + public string ColorTheme { get; init; } = "#f5b041"; + public Dictionary EmotionAvatars { get; init; } = new(); + + public string GetAvatarForEmotion(PlayerEmotion emotion) + { + if (EmotionAvatars.TryGetValue(emotion, out var av)) + { + return av; + } + return BaseAvatar; + } + + public static readonly AIPersonality Fox = new() + { + Id = "fox", + Name = "Fero 'Lišiak'", + Title = "Prefíkaný blufér", + BaseAvatar = "🦊", + Description = "Rád vyčkáva a nečakane mení farbu na stole. Keď sa mu darí, uškŕňa sa.", + Difficulty = AIDifficulty.Hard, + ColorTheme = "#ea580c", + EmotionAvatars = new() + { + [PlayerEmotion.Neutral] = "🦊", + [PlayerEmotion.Thinking] = "🧐", + [PlayerEmotion.Confident] = "😏", + [PlayerEmotion.Shocked] = "🙀", + [PlayerEmotion.Freezing] = "🥶", + [PlayerEmotion.Angry] = "😾", + [PlayerEmotion.Relieved] = "😸", + [PlayerEmotion.Desperate] = "😿", + [PlayerEmotion.Celebrating] = "🥳" + } + }; + + public static readonly AIPersonality Queen = new() + { + Id = "queen", + Name = "Zuzka 'Kráľovná'", + Title = "Chladnokrvná taktička", + BaseAvatar = "👸", + Description = "Vždy si stráži horníkov a premyslene kontroluje priebeh každej partie.", + Difficulty = AIDifficulty.Hard, + ColorTheme = "#ec4899", + EmotionAvatars = new() + { + [PlayerEmotion.Neutral] = "👸", + [PlayerEmotion.Thinking] = "🤔", + [PlayerEmotion.Confident] = "💅", + [PlayerEmotion.Shocked] = "😱", + [PlayerEmotion.Freezing] = "🥶", + [PlayerEmotion.Angry] = "😡", + [PlayerEmotion.Relieved] = "😌", + [PlayerEmotion.Desperate] = "😭", + [PlayerEmotion.Celebrating] = "👑" + } + }; + + public static readonly AIPersonality Robot = new() + { + Id = "robot", + Name = "Robo 'Algoritmus'", + Title = "Kvantový analytik", + BaseAvatar = "🤖", + Description = "Počíta každú zahratú kartu. Neznáša chyby a emócie vyjadruje v bleskoch a iskričkách.", + Difficulty = AIDifficulty.Hard, + ColorTheme = "#06b6d4", + EmotionAvatars = new() + { + [PlayerEmotion.Neutral] = "🤖", + [PlayerEmotion.Thinking] = "🧠", + [PlayerEmotion.Confident] = "⚡", + [PlayerEmotion.Shocked] = "💥", + [PlayerEmotion.Freezing] = "🧊", + [PlayerEmotion.Angry] = "🛑", + [PlayerEmotion.Relieved] = "🔋", + [PlayerEmotion.Desperate] = "🫠", + [PlayerEmotion.Celebrating] = "🚀" + } + }; + + public static readonly AIPersonality Grandpa = new() + { + Id = "grandpa", + Name = "Dedo Jozef", + Title = "Starý mariášnik", + BaseAvatar = "👴", + Description = "Hrá poctivú starú školu, žiadne zbytočné výmysly. Pri hre si rád dá virtuálne pivo.", + Difficulty = AIDifficulty.Medium, + ColorTheme = "#84cc16", + EmotionAvatars = new() + { + [PlayerEmotion.Neutral] = "👴", + [PlayerEmotion.Thinking] = "👓", + [PlayerEmotion.Confident] = "🍻", + [PlayerEmotion.Shocked] = "😵", + [PlayerEmotion.Freezing] = "🧣", + [PlayerEmotion.Angry] = "🤬", + [PlayerEmotion.Relieved] = "👴", + [PlayerEmotion.Desperate] = "🤦‍♂️", + [PlayerEmotion.Celebrating] = "🏆" + } + }; + + public static readonly AIPersonality CyberKid = new() + { + Id = "cyberkid", + Name = "CyberKid 'Gamer'", + Title = "Turbo útočník", + BaseAvatar = "👾", + Description = "Rýchly a agresívny hráč. Hádže sedmy a esá bez váhania.", + Difficulty = AIDifficulty.Medium, + ColorTheme = "#a855f7", + EmotionAvatars = new() + { + [PlayerEmotion.Neutral] = "👾", + [PlayerEmotion.Thinking] = "📡", + [PlayerEmotion.Confident] = "😎", + [PlayerEmotion.Shocked] = "🤯", + [PlayerEmotion.Freezing] = "❄️", + [PlayerEmotion.Angry] = "👿", + [PlayerEmotion.Relieved] = "🎮", + [PlayerEmotion.Desperate] = "💀", + [PlayerEmotion.Celebrating] = "🌟" + } + }; + + public static readonly AIPersonality Cat = new() + { + Id = "cat", + Name = "Mačka Minda", + Title = "Hravá šibalica", + BaseAvatar = "🐱", + Description = "Hravá a nevyspytateľná. Občas spôsobí chaos, ale teší sa z každej vyhratej rybičky.", + Difficulty = AIDifficulty.Easy, + ColorTheme = "#f59e0b", + EmotionAvatars = new() + { + [PlayerEmotion.Neutral] = "🐱", + [PlayerEmotion.Thinking] = "🐾", + [PlayerEmotion.Confident] = "😸", + [PlayerEmotion.Shocked] = "🙀", + [PlayerEmotion.Freezing] = "😿", + [PlayerEmotion.Angry] = "😾", + [PlayerEmotion.Relieved] = "😻", + [PlayerEmotion.Desperate] = "😿", + [PlayerEmotion.Celebrating] = "🐟" + } + }; + + public static readonly AIPersonality Wizard = new() + { + Id = "wizard", + Name = "Čarodej Baltazár", + Title = "Tajomný iluzionista", + BaseAvatar = "🧙", + Description = "Verí v magické spálenie 4 kariet a zázračné obraty v poslednej chvíli.", + Difficulty = AIDifficulty.Medium, + ColorTheme = "#6366f1", + EmotionAvatars = new() + { + [PlayerEmotion.Neutral] = "🧙", + [PlayerEmotion.Thinking] = "🔮", + [PlayerEmotion.Confident] = "✨", + [PlayerEmotion.Shocked] = "👁️", + [PlayerEmotion.Freezing] = "🧊", + [PlayerEmotion.Angry] = "🌩️", + [PlayerEmotion.Relieved] = "🧙‍♂️", + [PlayerEmotion.Desperate] = "🥀", + [PlayerEmotion.Celebrating] = "🪄" + } + }; + + public static readonly AIPersonality Bear = new() + { + Id = "bear", + Name = "Medveď Mišo", + Title = "Tatranský silák", + BaseAvatar = "🐻", + Description = "Keď dostane veľa sediem, hrozivo zúri. No keď vyloží eso, trasie sa celý stôl.", + Difficulty = AIDifficulty.Easy, + ColorTheme = "#b45309", + EmotionAvatars = new() + { + [PlayerEmotion.Neutral] = "🐻", + [PlayerEmotion.Thinking] = "🍯", + [PlayerEmotion.Confident] = "😁", + [PlayerEmotion.Shocked] = "😳", + [PlayerEmotion.Freezing] = "🥶", + [PlayerEmotion.Angry] = "🐻‍❄️", + [PlayerEmotion.Relieved] = "🐻", + [PlayerEmotion.Desperate] = "😭", + [PlayerEmotion.Celebrating] = "🍖" + } + }; + + public static readonly AIPersonality[] All = + [ + Fox, + Queen, + Robot, + Grandpa, + CyberKid, + Cat, + Wizard, + Bear + ]; + + public static AIPersonality GetById(string id) + { + foreach (var p in All) + { + if (p.Id.Equals(id, System.StringComparison.OrdinalIgnoreCase)) return p; + } + return Fox; + } +} \ No newline at end of file diff --git a/Faraon.Core/Models/Player.cs b/Faraon.Core/Models/Player.cs index 002b83c..354d90a 100644 --- a/Faraon.Core/Models/Player.cs +++ b/Faraon.Core/Models/Player.cs @@ -1,5 +1,7 @@ namespace Faraon.Core.Models; +using Faraon.Core.AI; + public enum PlayerType { Human = 0, @@ -20,6 +22,8 @@ public class Player public PlayerType Type { get; set; } = PlayerType.Human; public AIDifficulty Difficulty { get; set; } = AIDifficulty.Medium; public string Avatar { get; set; } = "👑"; + public string? PersonalityId { get; set; } + public PlayerEmotion CurrentEmotion { get; set; } = PlayerEmotion.Neutral; public List Hand { get; } = new(); public int? FinishedPosition { get; set; } @@ -39,12 +43,39 @@ public class Player public Player() { } - public Player(string name, PlayerType type, string avatar = "👤", AIDifficulty difficulty = AIDifficulty.Medium) + public Player(string name, PlayerType type, string avatar = "👤", AIDifficulty difficulty = AIDifficulty.Medium, string? personalityId = null) { Name = name; Type = type; Avatar = avatar; Difficulty = difficulty; + PersonalityId = personalityId; + } + + public string GetLiveAvatar() + { + if (Type == PlayerType.AI && !string.IsNullOrEmpty(PersonalityId)) + { + var p = AIPersonality.GetById(PersonalityId); + return p.GetAvatarForEmotion(CurrentEmotion); + } + return Avatar; + } + + public string? GetEmotionBadge() + { + return CurrentEmotion switch + { + PlayerEmotion.Thinking => "💡", + PlayerEmotion.Confident => "✨", + PlayerEmotion.Shocked => "💧", + PlayerEmotion.Freezing => "❄️", + PlayerEmotion.Angry => "🔥", + PlayerEmotion.Relieved => "🍀", + PlayerEmotion.Desperate => "💀", + PlayerEmotion.Celebrating => "🎉", + _ => null + }; } public void Reset() @@ -52,5 +83,6 @@ public class Player Hand.Clear(); FinishedPosition = null; IsSkipped = false; + CurrentEmotion = PlayerEmotion.Neutral; } } diff --git a/Faraon.Tests/GameEngineTests.cs b/Faraon.Tests/GameEngineTests.cs index 7d27cc1..31f69ef 100644 --- a/Faraon.Tests/GameEngineTests.cs +++ b/Faraon.Tests/GameEngineTests.cs @@ -476,4 +476,32 @@ public class GameEngineTests Assert.NotNull(longGameJoke); Assert.False(string.IsNullOrWhiteSpace(longGameJoke.Punchline)); } + + [Fact] + public void AIPersonality_ChangesLiveAvatar_BasedOnEmotion() + { + var fox = AIPersonality.Fox; + Assert.Equal("fox", fox.Id); + Assert.Equal("🦊", fox.GetAvatarForEmotion(PlayerEmotion.Neutral)); + Assert.Equal("🧐", fox.GetAvatarForEmotion(PlayerEmotion.Thinking)); + Assert.Equal("🙀", fox.GetAvatarForEmotion(PlayerEmotion.Shocked)); + Assert.Equal("🥶", fox.GetAvatarForEmotion(PlayerEmotion.Freezing)); + Assert.Equal("😾", fox.GetAvatarForEmotion(PlayerEmotion.Angry)); + Assert.Equal("🥳", fox.GetAvatarForEmotion(PlayerEmotion.Celebrating)); + + var player = new Player(fox.Name, PlayerType.AI, fox.BaseAvatar, fox.Difficulty, fox.Id); + Assert.Equal("🦊", player.GetLiveAvatar()); + + player.CurrentEmotion = PlayerEmotion.Shocked; + Assert.Equal("🙀", player.GetLiveAvatar()); + Assert.Equal("💧", player.GetEmotionBadge()); + + player.CurrentEmotion = PlayerEmotion.Freezing; + Assert.Equal("🥶", player.GetLiveAvatar()); + Assert.Equal("❄️", player.GetEmotionBadge()); + + player.CurrentEmotion = PlayerEmotion.Angry; + Assert.Equal("😾", player.GetLiveAvatar()); + Assert.Equal("🔥", player.GetEmotionBadge()); + } }