improvements

This commit is contained in:
Michal Vrba
2026-09-04 10:01:22 +02:00
parent 9005b4b9be
commit 7ca0e08637
15 changed files with 715 additions and 65 deletions
+31 -9
View File
@@ -1,8 +1,12 @@
@namespace Faraon.Client.Components
@using Faraon.Core.AI
<div class="lobby-screen">
<div class="lobby-container">
<div class="lobby-header">
<div class="lobby-logo-wrapper">
<img src="assets/logo/logo_2_modern.jpg" class="lobby-logo-img" alt="FARAÓN Logo" />
</div>
<h1 class="lobby-title">FARAÓN</h1>
<div class="lobby-subtitle">Tradičná mariášová kartová hra s moderným nábojom</div>
</div>
@@ -43,13 +47,14 @@
<option value="@PlayerType.AI">🤖 AI Počítač</option>
</select>
<!-- Obtiažnosť AI (ak je AI) -->
<!-- Výber AI osobnosti a obtiažnosti (ak je AI) -->
@if (p.Type == PlayerType.AI)
{
<select class="player-setup-select" @bind="p.Difficulty">
<option value="@AIDifficulty.Easy">🌱 Začiatočník (Ľahký)</option>
<option value="@AIDifficulty.Medium">⚔️ Taktik (Stredný)</option>
<option value="@AIDifficulty.Hard">🔥 Veľmajster (Ťažký)</option>
<select class="player-setup-select" style="font-weight: 700; color: var(--gold);" value="@(p.PersonalityId ?? "fox")" @onchange="(e) => SelectPersonality(idx, e.Value?.ToString())">
@foreach (var pers in AIPersonality.All)
{
<option value="@pers.Id">@pers.BaseAvatar @pers.Name (@GetDiffLabel(pers.Difficulty))</option>
}
</select>
}
</div>
@@ -255,9 +260,9 @@
private List<Player> 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));
+8 -2
View File
@@ -1,7 +1,7 @@
@namespace Faraon.Client.Components
<div class="player-slot @PositionClass @(IsFrozen ? "frozen-slot" : (IsCurrentTurn ? "active-turn-slot" : ""))">
<div class="avatar-badge @(IsFrozen ? "is-frozen" : (IsCurrentTurn ? "active-turn" : "")) @(Player.HasFinished ? "is-finished" : "") @(CanBeRevived && Player.HasFinished && ReviveTurnsRemaining > 0 ? "can-be-revived" : "")">
<div class="avatar-badge @(IsFrozen ? "is-frozen" : (IsCurrentTurn ? "active-turn" : "")) @(Player.HasFinished ? "is-finished" : "") @(CanBeRevived && Player.HasFinished && ReviveTurnsRemaining > 0 ? "can-be-revived" : "") emotion-aura-@Player.CurrentEmotion.ToString().ToLowerInvariant()">
@if (IsFrozen && !Player.HasFinished && !Player.IsEliminated)
{
<div class="frozen-indicator-badge">
@@ -24,7 +24,13 @@
</div>
}
<div class="avatar-icon">@Player.Avatar</div>
<div class="avatar-icon-wrapper emotion-anim-@Player.CurrentEmotion.ToString().ToLowerInvariant()">
<span class="avatar-icon">@Player.GetLiveAvatar()</span>
@if (!Player.HasFinished && !Player.IsEliminated && Player.GetEmotionBadge() != null)
{
<span class="avatar-emotion-dot-badge">@Player.GetEmotionBadge()</span>
}
</div>
<div class="avatar-name">
@Player.Name
@if (Player.Type == PlayerType.AI)
+82 -6
View File
@@ -17,6 +17,7 @@ else
<!-- Horná lišta -->
<header class="game-header">
<div style="display: flex; align-items: center; gap: 10px;">
<img src="assets/logo/logo_2_modern.jpg" style="width: 34px; height: 34px; border-radius: 50%; border: 1.5px solid #00e5ff; object-fit: cover; box-shadow: 0 0 12px rgba(0,229,255,0.6);" alt="Logo" />
<h1 class="game-title">FARAÓN</h1>
@if (Engine.State.Rules.OlympicMode)
{
@@ -25,20 +26,21 @@ else
</span>
}
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<div style="display: flex; align-items: center; gap: 8px;">
<!-- Rýchlosť hry -->
<button class="btn-speed-control" @onclick="CycleSpeed" title="Zmeniť rýchlosť hry">
@GetSpeedButtonText(Engine.State.Rules.SpeedLevel)
</button>
<!-- Zvuk -->
<button class="btn-header-action" @onclick="ToggleMute" title="@(IsMuted ? "Zapnúť zvuk" : "Stlmiť zvuk")">
@(IsMuted ? "🔇" : "🔊")
<button class="btn-header-action @(IsMuted ? "btn-sound-muted" : "")" @onclick="ToggleMute" title="@(IsMuted ? "Zapnúť zvuk" : "Stlmiť zvuk")">
<span>@(IsMuted ? "🔇" : "🔊")</span>
</button>
<!-- Nová hra -->
<!-- Menu -->
<button class="btn-header-action" @onclick="ReturnToLobby" title="Späť do menu">
⚙️ Menu
<span style="font-size: 0.9rem;">⚙️</span>
<span>Menu</span>
</button>
</div>
</header>
@@ -174,7 +176,7 @@ else
}
<div style="display: flex; align-items: center; gap: 8px;">
<span class="avatar-icon">@ViewingPlayer.Avatar</span>
<span class="avatar-icon">@ViewingPlayer.GetLiveAvatar()</span>
<span class="avatar-name">
@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)>();
Binary file not shown.

After

Width:  |  Height:  |  Size: 956 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 KiB

+279 -44
View File
@@ -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;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 765 KiB

+11 -3
View File
@@ -11,15 +11,23 @@
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@600;800;900&family=Outfit:wght@400;600;700;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="css/app.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="icon" type="image/jpeg" href="assets/logo/logo_2_modern.jpg" />
<link rel="apple-touch-icon" href="assets/logo/logo_2_modern.jpg" />
<link href="Faraon.Client.styles.css" rel="stylesheet" />
</head>
<body>
<div id="app">
<div class="app-loader">
<div class="loader-card">🃏</div>
<div class="loader-text">Načítavam Faraóna...</div>
<div class="loader-logo-container">
<img src="assets/logo/logo_2_modern.jpg" class="loader-logo-img" alt="FARAÓN" />
<div class="loader-spinner-ring"></div>
</div>
<div class="loader-title">FARAÓN</div>
<div class="loader-text">Pripravujem stôl a miešam karty...</div>
<div class="loader-progress-bar">
<div class="loader-progress-fill"></div>
</div>
</div>
</div>
+243
View File
@@ -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<PlayerEmotion, string> 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;
}
}
+33 -1
View File
@@ -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<Card> 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;
}
}
+28
View File
@@ -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());
}
}