AI human mode
This commit is contained in:
@@ -56,11 +56,30 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Nastavenia pravidiel -->
|
||||
<div style="font-weight: 700; margin-bottom: 8px; color: var(--gold); font-size: 0.95rem;">
|
||||
PRAVIDLÁ A NASTAVENIA:
|
||||
<!-- Nastavenia pravidiel (Collapsible - by default schované) -->
|
||||
<div style="margin-bottom: 18px;">
|
||||
<div class="lobby-accordion-header @(ShowRules ? "open" : "")" @onclick="() => ShowRules = !ShowRules">
|
||||
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
|
||||
<span style="font-size: 1.15rem;">⚙️</span>
|
||||
<span style="font-weight: 800; color: var(--gold); font-size: 0.95rem; letter-spacing: 0.5px;">
|
||||
PRAVIDLÁ A NASTAVENIA HRY
|
||||
</span>
|
||||
@if (!ShowRules)
|
||||
{
|
||||
<span class="badge-rules-summary">
|
||||
@(Rules.InitialCardCount) kariet • Sedmy (+3) • @(Rules.SpeedLevel)x Rýchlosť
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="rules-toggle-grid">
|
||||
<div class="accordion-toggle-btn">
|
||||
<span>@(ShowRules ? "Zbaliť" : "Rozbaliť")</span>
|
||||
<span class="accordion-arrow">@(ShowRules ? "▲" : "▼")</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (ShowRules)
|
||||
{
|
||||
<div class="rules-toggle-grid" style="margin-top: 10px; animation: fadeIn 0.25s ease;">
|
||||
<label class="rule-check-item">
|
||||
<input type="checkbox" @bind="Rules.StackSevens" />
|
||||
<span>Kumulácia sediem (+3, +6, +9...)</span>
|
||||
@@ -106,6 +125,11 @@
|
||||
<span style="font-weight: 800; color: var(--gold);">🏅 Olympijský Faraón (Turnajový režim)</span>
|
||||
<span style="font-size: 0.8rem; color: #94a3b8; margin-left: 6px;">— porazený má v ďalšom kole o 1 kartu menej</span>
|
||||
</label>
|
||||
<label class="rule-check-item" style="grid-column: 1 / -1; background: rgba(56, 189, 248, 0.12); padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(56, 189, 248, 0.35);">
|
||||
<input type="checkbox" @bind="Rules.AIHumanMode" />
|
||||
<span style="font-weight: 800; color: #38bdf8;">🤖💭 AI Human Mode (Premýšľacie bubliny & hlášky)</span>
|
||||
<span style="font-size: 0.8rem; color: #94a3b8; margin-left: 6px;">— boti hovoria svoje myšlienky, blufujú a reagujú na hru</span>
|
||||
</label>
|
||||
<div style="display: flex; align-items: center; gap: 8px; font-size: 0.9rem;">
|
||||
<span>Počiatočný počet kariet:</span>
|
||||
<select class="player-setup-select" style="width: auto; padding: 2px 8px;" @bind="Rules.InitialCardCount">
|
||||
@@ -117,117 +141,75 @@
|
||||
<div style="display: flex; align-items: center; gap: 8px; font-size: 0.9rem;">
|
||||
<span>Rýchlosť hry:</span>
|
||||
<select class="player-setup-select" style="width: auto; padding: 2px 8px;" @bind="Rules.SpeedLevel">
|
||||
<option value="1">🐢 0.5x (Pomalá)</option>
|
||||
<option value="2">🚶 0.75x (Pokojná)</option>
|
||||
<option value="3">⚡ 1x (Normálna - štandard)</option>
|
||||
<option value="4">🔥 1.5x (Rýchla)</option>
|
||||
<option value="5">🚀 2.5x (Turbo / Blesková)</option>
|
||||
<option value="-5">🐢 -5 (0.2x Zen / Extra pomalá)</option>
|
||||
<option value="-4">🐢 -4 (0.35x Veľmi pomalá)</option>
|
||||
<option value="-3">🚶 -3 (0.5x Pomalá)</option>
|
||||
<option value="-2">🚶 -2 (0.65x Pokojná)</option>
|
||||
<option value="-1">🍃 -1 (0.8x Zvoľnená)</option>
|
||||
<option value="0">⚡ 0 (1x Normálna / Vyvážená)</option>
|
||||
<option value="1">🔥 +1 (1.25x Svižná)</option>
|
||||
<option value="2">🔥 +2 (1.5x Rýchla)</option>
|
||||
<option value="3">🚀 +3 (2x Veľmi rýchla)</option>
|
||||
<option value="4">🚀 +4 (2.75x Turbo)</option>
|
||||
<option value="5">⚡ +5 (4x Blesková)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Výber balíčka kariet (Deck Theme) -->
|
||||
<!-- Výber balíčka kariet (Collapsible - by default iba zvolený deck) -->
|
||||
<div style="margin-bottom: 24px;">
|
||||
<h3 style="font-size: 1.05rem; color: var(--gold); margin-bottom: 12px; text-transform: uppercase; letter-spacing: 1px;">
|
||||
🎨 Vizuálny štýl balíčka kariet
|
||||
</h3>
|
||||
<div class="deck-selector-grid">
|
||||
<div class="deck-option-card @(Rules.SelectedDeck == "original" ? "selected-deck" : "")"
|
||||
@onclick='() => Rules.SelectedDeck = "original"'>
|
||||
<div class="deck-preview-img-box">
|
||||
<img src="assets/cards/original/Hearts_Seven.webp" class="deck-preview-thumb" alt="Original Mariáš" />
|
||||
<img src="assets/cards/original/Card_Back.webp" class="deck-preview-thumb-back" alt="Rub" />
|
||||
<div class="lobby-accordion-header @(ShowDeckSelector ? "open" : "")" @onclick="() => ShowDeckSelector = !ShowDeckSelector">
|
||||
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
|
||||
<span style="font-size: 1.15rem;">🎨</span>
|
||||
<span style="font-weight: 800; color: var(--gold); font-size: 0.95rem; letter-spacing: 0.5px;">
|
||||
VIZUÁLNY ŠTÝL BALÍČKA
|
||||
</span>
|
||||
</div>
|
||||
<div class="deck-info">
|
||||
<div class="deck-name">Tradičný Mariáš 🎴</div>
|
||||
<div class="deck-desc">Autentický dvojhlavý Tell vzor (Piatnik)</div>
|
||||
<div class="accordion-toggle-btn">
|
||||
<span>@(ShowDeckSelector ? "Zavrieť galériu" : "Zmeniť balíček")</span>
|
||||
<span class="accordion-arrow">@(ShowDeckSelector ? "▲" : "▼")</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="deck-option-card @(Rules.SelectedDeck == "janosik" ? "selected-deck" : "")"
|
||||
@onclick='() => Rules.SelectedDeck = "janosik"'>
|
||||
<!-- Aktuálne zvolený balíček (Default náhľad po kliknutí rozbalí výber) -->
|
||||
<div class="selected-deck-summary-card" @onclick="() => ShowDeckSelector = !ShowDeckSelector" title="Klikni pre výber a zmenu balíčka kariet">
|
||||
<div class="deck-preview-img-box">
|
||||
<img src="assets/cards/janosik/Hearts_King.webp" class="deck-preview-thumb" alt="Zbojníci" />
|
||||
<img src="assets/cards/janosik/Card_Back.webp" class="deck-preview-thumb-back" alt="Rub" />
|
||||
<img src="@CurrentDeck.Thumb" class="deck-preview-thumb" alt="@CurrentDeck.Name" />
|
||||
<img src="@CurrentDeck.Back" class="deck-preview-thumb-back" alt="Rub" />
|
||||
</div>
|
||||
<div class="deck-info">
|
||||
<div class="deck-name">Zbojníci & Legendy 🪓</div>
|
||||
<div class="deck-desc">Jánošík, Uhorčík, výšivky a tatranský folklór</div>
|
||||
<div class="deck-info" style="flex: 1;">
|
||||
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
||||
<span class="deck-name" style="color: var(--gold); font-size: 0.98rem;">@CurrentDeck.Name</span>
|
||||
<span class="badge-active-deck">Vybraný</span>
|
||||
</div>
|
||||
<div class="deck-desc">@CurrentDeck.Desc</div>
|
||||
</div>
|
||||
<div class="btn-deck-change-action">
|
||||
@(ShowDeckSelector ? "▲ Skryť" : "🎨 Zmeniť štýl ▾")
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="deck-option-card @(Rules.SelectedDeck == "spongebob" ? "selected-deck" : "")"
|
||||
@onclick='() => Rules.SelectedDeck = "spongebob"'>
|
||||
@if (ShowDeckSelector)
|
||||
{
|
||||
<div class="deck-selector-grid" style="margin-top: 12px; animation: fadeIn 0.25s ease;">
|
||||
@foreach (var d in AvailableDecks)
|
||||
{
|
||||
<div class="deck-option-card @(Rules.SelectedDeck == d.Id ? "selected-deck" : "")"
|
||||
@onclick="() => SelectDeck(d.Id)">
|
||||
<div class="deck-preview-img-box">
|
||||
<img src="assets/cards/spongebob/Hearts_OverKnave.webp" class="deck-preview-thumb" alt="SpongeBob Cartoon" />
|
||||
<img src="assets/cards/spongebob/Card_Back.webp" class="deck-preview-thumb-back" alt="Rub" />
|
||||
<img src="@d.Thumb" class="deck-preview-thumb" alt="@d.Name" />
|
||||
<img src="@d.Back" class="deck-preview-thumb-back" alt="Rub" />
|
||||
</div>
|
||||
<div class="deck-info">
|
||||
<div class="deck-name">SpongeBob Cartoon 🧽</div>
|
||||
<div class="deck-desc">Bikini Bottom edícia (postavičky a veľké symboly)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="deck-option-card @(Rules.SelectedDeck == "cyberpunk" ? "selected-deck" : "")"
|
||||
@onclick='() => Rules.SelectedDeck = "cyberpunk"'>
|
||||
<div class="deck-preview-img-box">
|
||||
<img src="assets/cards/cyberpunk/Leaves_King.webp" class="deck-preview-thumb" alt="Cyberpunk 2077" />
|
||||
<img src="assets/cards/cyberpunk/Card_Back.webp" class="deck-preview-thumb-back" alt="Rub" />
|
||||
</div>
|
||||
<div class="deck-info">
|
||||
<div class="deck-name">Cyberpunk 2077 ⚡</div>
|
||||
<div class="deck-desc">Neónový holografický štýl a kybernetickí hrdinovia</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="deck-option-card @(Rules.SelectedDeck == "fantasy" ? "selected-deck" : "")"
|
||||
@onclick='() => Rules.SelectedDeck = "fantasy"'>
|
||||
<div class="deck-preview-img-box">
|
||||
<img src="assets/cards/fantasy/Acorns_OverKnave.webp" class="deck-preview-thumb" alt="Fantasy RPG" />
|
||||
<img src="assets/cards/fantasy/Card_Back.webp" class="deck-preview-thumb-back" alt="Rub" />
|
||||
</div>
|
||||
<div class="deck-info">
|
||||
<div class="deck-name">Fantasy RPG 🐉</div>
|
||||
<div class="deck-desc">Dračí lordi, mágovia, runy a rytieri</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="deck-option-card @(Rules.SelectedDeck == "egypt" ? "selected-deck" : "")"
|
||||
@onclick='() => Rules.SelectedDeck = "egypt"'>
|
||||
<div class="deck-preview-img-box">
|
||||
<img src="assets/cards/egypt/Bells_King.webp" class="deck-preview-thumb" alt="Staroveký Egypt" />
|
||||
<img src="assets/cards/egypt/Card_Back.webp" class="deck-preview-thumb-back" alt="Rub" />
|
||||
</div>
|
||||
<div class="deck-info">
|
||||
<div class="deck-name">Staroveký Egypt ⚔️</div>
|
||||
<div class="deck-desc">Zlatý papyrus, Tutanchamón, Anubis a pyramídy</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="deck-option-card @(Rules.SelectedDeck == "pets" ? "selected-deck" : "")"
|
||||
@onclick='() => Rules.SelectedDeck = "pets"'>
|
||||
<div class="deck-preview-img-box">
|
||||
<img src="assets/cards/pets/Hearts_OverKnave.webp" class="deck-preview-thumb" alt="Kráľovské zvieratká" />
|
||||
<img src="assets/cards/pets/Card_Back.webp" class="deck-preview-thumb-back" alt="Rub" />
|
||||
</div>
|
||||
<div class="deck-info">
|
||||
<div class="deck-name">Kráľovské Zvieratká 🐱</div>
|
||||
<div class="deck-desc">Mačičky a psíkovia v kráľovských korunkách</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="deck-option-card @(Rules.SelectedDeck == "pixel" ? "selected-deck" : "")"
|
||||
@onclick='() => Rules.SelectedDeck = "pixel"'>
|
||||
<div class="deck-preview-img-box">
|
||||
<img src="assets/cards/pixel/Leaves_Seven.webp" class="deck-preview-thumb" alt="Retro Pixel" />
|
||||
<img src="assets/cards/pixel/Card_Back.webp" class="deck-preview-thumb-back" alt="Rub" />
|
||||
</div>
|
||||
<div class="deck-info">
|
||||
<div class="deck-name">Retro 8-Bit Pixel 🕹️</div>
|
||||
<div class="deck-desc">Nostalgický arkádový štýl klasických hier</div>
|
||||
<div class="deck-name">@d.Name</div>
|
||||
<div class="deck-desc">@d.Desc</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Štart tlačidlo -->
|
||||
@@ -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 = ["👑", "🧙♂️", "🦊", "🦁", "🐯", "🤖", "🐉", "🥷", "👸", "🤠", "🦅", "🐺"];
|
||||
|
||||
|
||||
@@ -75,6 +75,14 @@
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(ThoughtBubble))
|
||||
{
|
||||
<div class="ai-thought-bubble @PositionClass @(IsThoughtFadingOut ? "fading-out" : "")">
|
||||
<span class="thought-bubble-prefix">💭</span>
|
||||
<span class="thought-bubble-text">@ThoughtBubble</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Mini karty pre vizuálny odpočet kariet v ruke -->
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
+310
-37
@@ -1,4 +1,5 @@
|
||||
@page "/"
|
||||
@using Faraon.Core.AI
|
||||
@inject FxService Fx
|
||||
@inject IFaraonAI AI
|
||||
@inject IJSRuntime JS
|
||||
@@ -24,30 +25,46 @@ else
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="turn-header-info">
|
||||
Ťah #@Engine.State.TurnCount • Na rade:
|
||||
<span class="active-turn-header-player">
|
||||
@Engine.State.CurrentPlayer.Avatar @Engine.State.CurrentPlayer.Name
|
||||
</span>
|
||||
</div>
|
||||
<div class="game-controls">
|
||||
<!-- Rýchly prepínač rýchlosti hry (5 úrovní) -->
|
||||
<button class="btn-game-icon btn-speed-control" title="Rýchlosť hry: @GetSpeedDescription(Engine.State.Rules.SpeedLevel) (Klikni pre zmenu)" @onclick="CycleSpeed">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<!-- Rýchlosť hry -->
|
||||
<button class="btn-speed-control" @onclick="CycleSpeed" title="Zmeniť rýchlosť hry">
|
||||
@GetSpeedButtonText(Engine.State.Rules.SpeedLevel)
|
||||
</button>
|
||||
<!-- Rýchly prepínač balíčka kariet -->
|
||||
<button class="btn-game-icon" title="Zmeniť balíček kariet (@Engine.State.Rules.SelectedDeck)" @onclick="ToggleDeck">
|
||||
@GetDeckIcon(Engine.State.Rules.SelectedDeck)
|
||||
</button>
|
||||
<button class="btn-game-icon" title="Stlmiť / Zapnúť zvuk" @onclick="ToggleMute">
|
||||
|
||||
<!-- Zvuk -->
|
||||
<button class="btn-header-action" @onclick="ToggleMute" title="@(IsMuted ? "Zapnúť zvuk" : "Stlmiť zvuk")">
|
||||
@(IsMuted ? "🔇" : "🔊")
|
||||
</button>
|
||||
<button class="btn-game-icon" title="Návrat do Lobby" @onclick="ReturnToLobby">
|
||||
🏠
|
||||
|
||||
<!-- Nová hra -->
|
||||
<button class="btn-header-action" @onclick="ReturnToLobby" title="Späť do menu">
|
||||
⚙️ Menu
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Špeciálny banner kartového vtipu (AI Human Mode) -->
|
||||
@if (CurrentJoke != null)
|
||||
{
|
||||
<div class="ai-joke-banner @(CurrentJoke.IsFadingOut ? "fading-out" : "")">
|
||||
<div class="joke-header">
|
||||
<div class="joke-header-badge">
|
||||
<span>@CurrentJoke.Joke.Icon</span>
|
||||
<span>🎭 @CurrentJoke.Joke.Category</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span class="joke-storyteller">
|
||||
<span>@CurrentJoke.Storyteller.Avatar</span>
|
||||
<span>@CurrentJoke.Storyteller.Name</span>
|
||||
</span>
|
||||
<button class="joke-close-btn" @onclick="DismissJoke" title="Zavrieť vtip">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="joke-setup">@CurrentJoke.Joke.Setup</div>
|
||||
<div class="joke-punchline">@CurrentJoke.Joke.Punchline</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Herný stôl -->
|
||||
<div class="poker-table-surface">
|
||||
<!-- Súperi na stole -->
|
||||
@@ -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);
|
||||
<PlayerSlot Player="@opp.Player"
|
||||
IsCurrentTurn="@(Engine.State.CurrentPlayer == opp.Player)"
|
||||
IsFrozen="@isOppFrozen"
|
||||
CanBeRevived="@canOppBeRevived"
|
||||
ReviveRoundsRemaining="@Engine.State.ReviveRoundsRemaining"
|
||||
ReviveTurnsRemaining="@Engine.State.ReviveTurnsRemaining"
|
||||
ThoughtBubble="@thought?.Text"
|
||||
IsThoughtFadingOut="@(thought?.IsFadingOut ?? false)"
|
||||
IsOlympicMode="@Engine.State.Rules.OlympicMode"
|
||||
PositionClass="@opp.PositionClass" />
|
||||
}
|
||||
@@ -193,6 +213,17 @@ else
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@{
|
||||
var bottomThought = GetThoughtState(ViewingPlayer);
|
||||
}
|
||||
@if (bottomThought != null && !string.IsNullOrEmpty(bottomThought.Text))
|
||||
{
|
||||
<div class="ai-thought-bubble slot-bottom @(bottomThought.IsFadingOut ? "fading-out" : "")">
|
||||
<span class="thought-bubble-prefix">💭</span>
|
||||
<span class="thought-bubble-text">@bottomThought.Text</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Interaktívna ruka hráča -->
|
||||
@@ -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<Player, PlayerThoughtInfo> 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;
|
||||
}
|
||||
|
||||
@@ -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: '';
|
||||
|
||||
@@ -11,11 +11,17 @@ 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:
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>(T[] options)
|
||||
{
|
||||
if (options == null || options.Length == 0) return default!;
|
||||
return options[_rng.Next(options.Length)];
|
||||
}
|
||||
}
|
||||
@@ -70,22 +70,29 @@ public class RuleOptions
|
||||
public bool PassAndPlayPrivacyScreen { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Ú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).
|
||||
/// </summary>
|
||||
public int SpeedLevel { get; set; } = 3;
|
||||
public int SpeedLevel { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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.
|
||||
/// </summary>
|
||||
public bool OlympicMode { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// AI Human Mode: AI hráči zobrazujú premýšľacie bubliny s taktickými a zábavnými hláškami a reagujú na herné udalosti.
|
||||
/// </summary>
|
||||
public bool AIHumanMode { get; set; } = false;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user