AI human mode

This commit is contained in:
Michal Vrba
2026-09-03 21:11:25 +02:00
parent 035da18584
commit 9005b4b9be
8 changed files with 1220 additions and 247 deletions
+169 -164
View File
@@ -56,178 +56,160 @@
}
</div>
<!-- Nastavenia pravidiel -->
<div style="font-weight: 700; margin-bottom: 8px; color: var(--gold); font-size: 0.95rem;">
PRAVIDLÁ A NASTAVENIA:
</div>
<div class="rules-toggle-grid">
<label class="rule-check-item">
<input type="checkbox" @bind="Rules.StackSevens" />
<span>Kumulácia sediem (+3, +6, +9...)</span>
</label>
<label class="rule-check-item">
<input type="checkbox" @bind="Rules.CounterAceWithAce" />
<span>Prebíjanie esa esom</span>
</label>
<div style="grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; background: rgba(255, 23, 68, 0.08); padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(255, 23, 68, 0.25); gap: 12px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.1rem;">❤️</span>
<div>
<div style="font-weight: 700; color: #ff5252;">Červená sedma (Oživenie / Resurrect)</div>
<div style="font-size: 0.76rem; color: #94a3b8;">Spôsob vrátenia skončeného hráča späť do hry</div>
<!-- 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="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>
</label>
<label class="rule-check-item">
<input type="checkbox" @bind="Rules.CounterAceWithAce" />
<span>Prebíjanie esa esom</span>
</label>
<div style="grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; background: rgba(255, 23, 68, 0.08); padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(255, 23, 68, 0.25); gap: 12px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.1rem;">❤️</span>
<div>
<div style="font-weight: 700; color: #ff5252;">Červená sedma (Oživenie / Resurrect)</div>
<div style="font-size: 0.76rem; color: #94a3b8;">Spôsob vrátenia skončeného hráča späť do hry</div>
</div>
</div>
<select class="player-setup-select" style="width: auto; min-width: 240px; padding: 4px 10px; font-weight: 600;" @bind="Rules.RedSevenMode">
<option value="@RedSevenMode.AnyCardOneRound">⚡ Voľná na čokoľvek (do 1 kola)</option>
<option value="@RedSevenMode.ColorOrSevenThreeRounds">🎴 Klasická na Srdce/7 (do 3 kôl)</option>
<option value="@RedSevenMode.Disabled">❌ Vypnuté (Bez oživenia)</option>
</select>
</div>
<div style="grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; background: rgba(45, 106, 79, 0.12); padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(45, 106, 79, 0.35); gap: 12px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.1rem;">🍃</span>
<div>
<div style="font-weight: 700; color: #52b788;">Zelený dolník (Rušenie sediem)</div>
<div style="font-size: 0.76rem; color: #94a3b8;">Zelený dolník zruší trest zo sedmy & pravidlá vykladania</div>
</div>
</div>
<select class="player-setup-select" style="width: auto; min-width: 240px; padding: 4px 10px; font-weight: 600;" @bind="Rules.GreenUnderKnaveMode">
<option value="@GreenUnderKnaveMode.OnlyOnSevenOrUnderKnave">🌿 Ruší sedmy (Zeleň / Dolník)</option>
<option value="@GreenUnderKnaveMode.UniversalWildcard">✨ Voľný na čokoľvek + čokoľvek naňho</option>
<option value="@GreenUnderKnaveMode.Disabled">❌ Vypnuté (Bez rušenia sediem)</option>
</select>
</div>
<label class="rule-check-item">
<input type="checkbox" @bind="Rules.FourOfAKindBurns" />
<span>Spálenie (4 rovnaké karty)</span>
</label>
<label class="rule-check-item" style="grid-column: 1 / -1; background: rgba(245, 176, 65, 0.12); padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(245, 176, 65, 0.3);">
<input type="checkbox" @bind="Rules.OlympicMode" />
<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">
<option value="4">4 karty</option>
<option value="5">5 kariet</option>
<option value="6">6 kariet</option>
</select>
</div>
<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="-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>
<select class="player-setup-select" style="width: auto; min-width: 240px; padding: 4px 10px; font-weight: 600;" @bind="Rules.RedSevenMode">
<option value="@RedSevenMode.AnyCardOneRound">⚡ Voľná na čokoľvek (do 1 kola)</option>
<option value="@RedSevenMode.ColorOrSevenThreeRounds">🎴 Klasická na Srdce/7 (do 3 kôl)</option>
<option value="@RedSevenMode.Disabled">❌ Vypnuté (Bez oživenia)</option>
</select>
</div>
<div style="grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; background: rgba(45, 106, 79, 0.12); padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(45, 106, 79, 0.35); gap: 12px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.1rem;">🍃</span>
<div>
<div style="font-weight: 700; color: #52b788;">Zelený dolník (Rušenie sediem)</div>
<div style="font-size: 0.76rem; color: #94a3b8;">Zelený dolník zruší trest zo sedmy & pravidlá vykladania</div>
</div>
</div>
<select class="player-setup-select" style="width: auto; min-width: 240px; padding: 4px 10px; font-weight: 600;" @bind="Rules.GreenUnderKnaveMode">
<option value="@GreenUnderKnaveMode.OnlyOnSevenOrUnderKnave">🌿 Ruší sedmy (Zeleň / Dolník)</option>
<option value="@GreenUnderKnaveMode.UniversalWildcard">✨ Voľný na čokoľvek + čokoľvek naňho</option>
<option value="@GreenUnderKnaveMode.Disabled">❌ Vypnuté (Bez rušenia sediem)</option>
</select>
</div>
<label class="rule-check-item">
<input type="checkbox" @bind="Rules.FourOfAKindBurns" />
<span>Spálenie (4 rovnaké karty)</span>
</label>
<label class="rule-check-item" style="grid-column: 1 / -1; background: rgba(245, 176, 65, 0.12); padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(245, 176, 65, 0.3);">
<input type="checkbox" @bind="Rules.OlympicMode" />
<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>
<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">
<option value="4">4 karty</option>
<option value="5">5 kariet</option>
<option value="6">6 kariet</option>
</select>
</div>
<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>
</select>
</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>
<div class="deck-info">
<div class="deck-name">Tradičný Mariáš 🎴</div>
<div class="deck-desc">Autentický dvojhlavý Tell vzor (Piatnik)</div>
</div>
<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-option-card @(Rules.SelectedDeck == "janosik" ? "selected-deck" : "")"
@onclick='() => Rules.SelectedDeck = "janosik"'>
<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" />
</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>
</div>
<div class="deck-option-card @(Rules.SelectedDeck == "spongebob" ? "selected-deck" : "")"
@onclick='() => Rules.SelectedDeck = "spongebob"'>
<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" />
</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>
<div class="accordion-toggle-btn">
<span>@(ShowDeckSelector ? "Zavrieť galériu" : "Zmeniť balíček")</span>
<span class="accordion-arrow">@(ShowDeckSelector ? "▲" : "▼")</span>
</div>
</div>
<!-- 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="@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" 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>
@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="@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">@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 = ["👑", "🧙‍♂️", "🦊", "🦁", "🐯", "🤖", "🐉", "🥷", "👸", "🤠", "🦅", "🐺"];
+10
View File
@@ -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
View File
@@ -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 &bull; 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;
}
+370 -32
View File
@@ -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: '';
+12 -6
View File
@@ -11,12 +11,18 @@ window.FaraonFx = (function () {
function setSpeedLevel(level) {
switch (level) {
case 1: speedMultiplier = 0.7; break; // 0.5x
case 2: speedMultiplier = 0.85; break; // 0.75x
case 4: speedMultiplier = 1.5; break; // 1.5x
case 5: speedMultiplier = 2.4; break; // 2.5x Turbo
case 3:
default: speedMultiplier = 1.0; break;
case -5: speedMultiplier = 0.35; break; // 0.2x Zen
case -4: speedMultiplier = 0.45; break; // 0.35x Veľmi pomalá
case -3: speedMultiplier = 0.60; break; // 0.5x Pomalá
case -2: speedMultiplier = 0.75; break; // 0.65x Pokojná
case -1: speedMultiplier = 0.88; break; // 0.8x Zvoľnená
case 0: speedMultiplier = 1.0; break; // 1x Normálna
case 1: speedMultiplier = 1.25; break; // 1.25x Svižná
case 2: speedMultiplier = 1.55; break; // 1.5x Rýchla
case 3: speedMultiplier = 2.0; break; // 2x Veľmi rýchla
case 4: speedMultiplier = 2.75; break; // 2.75x Turbo
case 5: speedMultiplier = 3.8; break; // 4x Blesková
default: speedMultiplier = 1.0; break;
}
}