1226 lines
49 KiB
Plaintext
1226 lines
49 KiB
Plaintext
@page "/"
|
|
@using Faraon.Core.AI
|
|
@implements IDisposable
|
|
@inject FxService Fx
|
|
@inject IFaraonAI AI
|
|
@inject IJSRuntime JS
|
|
|
|
@if (IsInLobby || Engine == null)
|
|
{
|
|
<Lobby OnStartGame="HandleStartGame" />
|
|
}
|
|
else
|
|
{
|
|
<div id="game-table-container" class="theme-@Engine.State.Rules.TableTheme.ToString().ToLowerInvariant()">
|
|
<!-- Canvas pre časticové efekty (oheň, ľad, konfety) -->
|
|
<canvas id="particle-canvas"></canvas>
|
|
|
|
<!-- Horná lišta -->
|
|
<header class="game-header">
|
|
<div style="display: flex; align-items: center; gap: 10px;">
|
|
<img src="assets/logo/logo_2_modern.jpg" style="width: 34px; height: 34px; border-radius: 50%; border: 1.5px solid #00e5ff; object-fit: cover; box-shadow: 0 0 12px rgba(0,229,255,0.6);" alt="Logo" />
|
|
<h1 class="game-title">FARAÓN</h1>
|
|
@if (Engine.State.Rules.OlympicMode)
|
|
{
|
|
<span style="background: linear-gradient(135deg, #f5b041, #d4ac0d); color: #1a1a1a; font-weight: 900; font-size: 0.78rem; padding: 2px 10px; border-radius: 12px; letter-spacing: 0.5px;">
|
|
🏅 KOLO #@Engine.State.RoundNumber
|
|
</span>
|
|
}
|
|
</div>
|
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
<!-- Prepínač témy stola -->
|
|
<button class="btn-header-action" @onclick="CycleTableTheme" title="Téma stola: @GetThemeName(Engine.State.Rules.TableTheme)">
|
|
<span>@GetThemeIcon(Engine.State.Rules.TableTheme)</span>
|
|
</button>
|
|
|
|
<!-- Rýchlosť hry -->
|
|
<button class="btn-speed-control" @onclick="CycleSpeed" title="Zmeniť rýchlosť hry">
|
|
@GetSpeedButtonText(Engine.State.Rules.SpeedLevel)
|
|
</button>
|
|
|
|
<!-- Zvuk -->
|
|
<button class="btn-header-action @(IsMuted ? "btn-sound-muted" : "")" @onclick="ToggleMute" title="@(IsMuted ? "Zapnúť zvuk" : "Stlmiť zvuk")">
|
|
<span>@(IsMuted ? "🔇" : "🔊")</span>
|
|
</button>
|
|
|
|
<!-- Menu -->
|
|
<button class="btn-header-action" @onclick="ReturnToLobby" title="Späť do menu">
|
|
<span style="font-size: 0.9rem;">⚙️</span>
|
|
<span>Menu</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Živý radar & Záznamník ťahov -->
|
|
<div class="game-log-radar @(IsLogRadarMinimized ? "minimized" : "")">
|
|
<button class="log-radar-toggle-btn" @onclick="() => IsLogRadarMinimized = !IsLogRadarMinimized" title="Záznamník ťahov">
|
|
<span>📜 Ťahy</span>
|
|
<span style="font-size: 0.65rem;">@(IsLogRadarMinimized ? "▼" : "▲")</span>
|
|
</button>
|
|
@if (!IsLogRadarMinimized && Engine.State.MoveLog.Count > 0)
|
|
{
|
|
<div class="log-radar-items-container">
|
|
@foreach (var log in Engine.State.MoveLog.TakeLast(5).Reverse())
|
|
{
|
|
<div class="log-radar-item" style="border-left-color: @(log.AccentColor ?? "var(--gold)");">
|
|
<span class="log-radar-icon">@log.Icon</span>
|
|
<div class="log-radar-text">
|
|
<div>@log.Text</div>
|
|
@if (!string.IsNullOrEmpty(log.SubText))
|
|
{
|
|
<div class="log-radar-sub">@log.SubText</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Š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 -->
|
|
@foreach (var opp in GetOpponents())
|
|
{
|
|
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" />
|
|
}
|
|
|
|
<!-- Stred stola: Lízací balíček a Odhadzovacia kôpka -->
|
|
<div class="slot-center">
|
|
<!-- Lízací balíček -->
|
|
<div class="deck-card-container" @onclick="HandleDeckClick" title="Potiahnuť kartu">
|
|
<CardView IsBack="true" Deck="@Engine.State.Rules.SelectedDeck" CustomStyle="box-shadow: 0 10px 25px rgba(0,0,0,0.6);" />
|
|
<div class="deck-count-badge">
|
|
@Engine.State.DrawDeck.Count
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Odhadzovacia kôpka -->
|
|
<div class="discard-pile-container">
|
|
@if (Engine.State.TopCard != null)
|
|
{
|
|
<CardView Card="@Engine.State.TopCard"
|
|
Deck="@Engine.State.Rules.SelectedDeck"
|
|
CustomStyle="box-shadow: 0 10px 25px rgba(0,0,0,0.7);" />
|
|
}
|
|
|
|
<!-- Indikátor zmrazenia z Esa -->
|
|
@if (Engine.State.PendingSkip)
|
|
{
|
|
<div class="ace-freeze-badge">
|
|
❄️ ESO: ZMRAZENIE!
|
|
</div>
|
|
}
|
|
|
|
<!-- Indikátor trestu zo sedmy -->
|
|
@if (Engine.State.PendingDrawCount > 0)
|
|
{
|
|
<div class="seven-penalty-badge">
|
|
🔥 +@Engine.State.PendingDrawCount Kariet!
|
|
</div>
|
|
}
|
|
|
|
<!-- Indikátor zvolenej farby z Horníka -->
|
|
@if (Engine.State.ActiveSuit.HasValue)
|
|
{
|
|
var dummy = new Card(Engine.State.ActiveSuit.Value, Rank.OverKnave);
|
|
<div class="active-suit-badge" style="color: @dummy.SuitColor;">
|
|
<span>Farba:</span>
|
|
<div style="width: 20px; height: 20px; display: inline-flex; vertical-align: middle;">
|
|
<MariasPip Suit="@Engine.State.ActiveSuit.Value" />
|
|
</div>
|
|
<span>@dummy.SuitName</span>
|
|
</div>
|
|
}
|
|
|
|
<!-- Indikátor Zeleného dolníka (univerzálna karta) -->
|
|
@if (Engine.State.TopCard != null && Engine.State.TopCard.IsGreenUnderKnave && Engine.State.Rules.GreenUnderKnaveMode == GreenUnderKnaveMode.UniversalWildcard)
|
|
{
|
|
<div class="green-wildcard-badge">
|
|
🍃 ZELENÝ DOLNÍK: Daj čokoľvek!
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Spodný slot: Aktívny hráč v zobrazení -->
|
|
@{
|
|
var isBottomFrozen = Engine.State.PendingSkip && IsViewingPlayerActiveTurn;
|
|
var canBottomBeRevived = Engine.State.Rules.RedSevenRevives && Engine.State.LastFinishedPlayer == ViewingPlayer && Engine.State.ReviveTurnsRemaining > 0;
|
|
}
|
|
<div class="slot-bottom @(isBottomFrozen ? "frozen-slot" : (IsViewingPlayerActiveTurn ? "active-turn-slot" : ""))">
|
|
<div class="avatar-badge @(isBottomFrozen ? "is-frozen" : (IsViewingPlayerActiveTurn ? "active-turn" : "")) @(ViewingPlayer.HasFinished ? "is-finished" : "") @(canBottomBeRevived && ViewingPlayer.HasFinished ? "can-be-revived" : "")"
|
|
style="margin-bottom: 8px;">
|
|
@if (isBottomFrozen && !ViewingPlayer.HasFinished && !ViewingPlayer.IsEliminated)
|
|
{
|
|
<div class="frozen-indicator-badge">
|
|
<span class="frozen-indicator-dot"></span>
|
|
<span class="frozen-indicator-text">@(ViewingPlayer.Type == PlayerType.Human ? "❄️ STOJÍŠ TOTO KOLO" : "❄️ ZMRAZENÝ")</span>
|
|
</div>
|
|
}
|
|
else if (IsViewingPlayerActiveTurn && !ViewingPlayer.HasFinished && !ViewingPlayer.IsEliminated)
|
|
{
|
|
<div class="turn-indicator-badge">
|
|
<span class="turn-indicator-dot"></span>
|
|
<span class="turn-indicator-text">@(ViewingPlayer.Type == PlayerType.Human ? "SI NA ŤAHU" : "NA ŤAHU")</span>
|
|
</div>
|
|
}
|
|
|
|
@if (ViewingPlayer.HasFinished)
|
|
{
|
|
<div class="badge-finished-pos">
|
|
@ViewingPlayer.FinishedPosition
|
|
</div>
|
|
}
|
|
|
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
<span class="avatar-icon">@ViewingPlayer.GetLiveAvatar()</span>
|
|
<span class="avatar-name">
|
|
@ViewingPlayer.Name
|
|
@if (ViewingPlayer.Type == PlayerType.AI)
|
|
{
|
|
<span style="font-size: 0.7rem; opacity: 0.8;">(AI)</span>
|
|
}
|
|
</span>
|
|
<span class="avatar-card-count">(@ViewingPlayer.Hand.Count @(ViewingPlayer.Hand.Count == 1 ? "karta" : (ViewingPlayer.Hand.Count < 5 ? "karty" : "kariet")))</span>
|
|
@if (Engine.State.Rules.OlympicMode)
|
|
{
|
|
<span style="font-size: 0.75rem; color: var(--gold); background: rgba(0,0,0,0.4); padding: 2px 8px; border-radius: 10px; border: 1px solid rgba(245, 176, 65, 0.4);">
|
|
🏅 @ViewingPlayer.OlympicCardsCount @(ViewingPlayer.OlympicCardsCount == 1 ? "karta" : (ViewingPlayer.OlympicCardsCount < 5 ? "karty" : "kariet"))
|
|
</span>
|
|
}
|
|
</div>
|
|
|
|
@if (isBottomFrozen && !ViewingPlayer.HasFinished && !ViewingPlayer.IsEliminated)
|
|
{
|
|
<div class="icicles-row">
|
|
<span class="icicle"></span>
|
|
<span class="icicle"></span>
|
|
<span class="icicle"></span>
|
|
<span class="icicle"></span>
|
|
<span class="icicle"></span>
|
|
<span class="icicle"></span>
|
|
<span class="icicle"></span>
|
|
</div>
|
|
}
|
|
|
|
@if (canBottomBeRevived && ViewingPlayer.HasFinished)
|
|
{
|
|
<div class="revive-countdown-badge" title="Tento hráč môže byť vrátený do hry Červenou sedmou!">
|
|
<span class="heartbeat-icon">❤️</span>
|
|
<span>
|
|
@(Engine.State.ReviveRoundsRemaining > 1 ? $"Oživenie: {Engine.State.ReviveRoundsRemaining} kolá" : (Engine.State.ReviveTurnsRemaining <= 1 ? "Oživenie: 1 ťah" : "Oživenie: 1 kolo"))
|
|
</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 -->
|
|
@if (!ViewingPlayer.IsEliminated)
|
|
{
|
|
<PlayerHandView Player="@ViewingPlayer"
|
|
Deck="@Engine.State.Rules.SelectedDeck"
|
|
IsMyTurn="@IsViewingPlayerTurn"
|
|
CanPlayCardFunc="@(c => Engine.CanPlayCard(ViewingPlayer, c))"
|
|
CanPlayCardsFunc="@(cards => Engine.CanPlayCards(ViewingPlayer, cards))"
|
|
OnPlayCards="HandlePlayCards" />
|
|
}
|
|
else
|
|
{
|
|
<div style="padding: 20px; color: #ef4444; font-weight: 800; font-size: 1.1rem; text-shadow: 0 2px 8px rgba(0,0,0,0.8);">
|
|
☠️ Bol si vyradený z Olympijského turnaja. Sleduj zápas až do konca!
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Akčné tlačidlá pre aktívneho živého hráča -->
|
|
@if (IsViewingPlayerTurn && Engine.State.TurnState == TurnState.Playing && !ViewingPlayer.IsEliminated)
|
|
{
|
|
<div style="position: absolute; bottom: 18px; right: 24px; z-index: 60; display: flex; gap: 10px;">
|
|
@if (CheckFourOfAKindRank(ViewingPlayer) is Rank burnRank)
|
|
{
|
|
<button class="btn-primary-glow" style="background: linear-gradient(135deg, #ff4500, #ff8c00); box-shadow: 0 0 20px #ff4500;"
|
|
@onclick="() => HandleBurn(burnRank)">
|
|
💥 Spáliť 4x @burnRank!
|
|
</button>
|
|
}
|
|
|
|
<button class="btn-pill" style="background: rgba(0,0,0,0.6); border: 2px solid var(--gold);"
|
|
@onclick="HandleDeckClick">
|
|
@if (Engine.State.PendingDrawCount > 0)
|
|
{
|
|
<span>Ťahať +@Engine.State.PendingDrawCount 🃏</span>
|
|
}
|
|
else if (Engine.State.PendingSkip)
|
|
{
|
|
<span>Stáť toto kolo ❄️</span>
|
|
}
|
|
else
|
|
{
|
|
<span>Líznúť kartu 📥</span>
|
|
}
|
|
</button>
|
|
</div>
|
|
}
|
|
|
|
<!-- Toast / Ticker poslednej akcie -->
|
|
@if (!string.IsNullOrEmpty(LastEventMessage))
|
|
{
|
|
<div class="event-ticker">
|
|
@LastEventMessage
|
|
</div>
|
|
}
|
|
|
|
<!-- Modálne okná -->
|
|
@if (Engine.State.TurnState == TurnState.SelectingSuit && Engine.State.PendingHornikPlayer == ViewingPlayer)
|
|
{
|
|
<SuitPickerModal OnSuitSelected="HandleSuitSelected" />
|
|
}
|
|
|
|
@if (Engine.State.TurnState == TurnState.PassScreen)
|
|
{
|
|
<PassScreenModal Player="@Engine.State.CurrentPlayer" OnContinue="HandlePassContinue" />
|
|
}
|
|
|
|
@if (Engine.State.TurnState == TurnState.RoundOver)
|
|
{
|
|
<RoundOverModal RoundNumber="@Engine.State.RoundNumber"
|
|
RoundLoser="@Engine.State.RoundLoser"
|
|
Players="@Engine.State.Players"
|
|
OnNextRound="HandleStartNextRound"
|
|
OnBackToLobby="ReturnToLobby" />
|
|
}
|
|
|
|
@if (Engine.State.IsGameOver)
|
|
{
|
|
<GameOverModal Players="@Engine.State.Players"
|
|
IsOlympicMode="@Engine.State.Rules.OlympicMode"
|
|
MatchWinner="@Engine.State.MatchWinner"
|
|
Stats="@Engine.State.Stats"
|
|
OnPlayAgain="HandlePlayAgain"
|
|
OnBackToLobby="ReturnToLobby" />
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
private bool IsInLobby = true;
|
|
private bool IsMuted = false;
|
|
private bool IsLogRadarMinimized = true;
|
|
private GameEngine? Engine;
|
|
private string LastEventMessage = "";
|
|
private System.Threading.CancellationTokenSource? _aiCts;
|
|
|
|
private List<Player> ConfiguredPlayers = new();
|
|
private RuleOptions ConfiguredRules = new();
|
|
private Player? _previousTurnPlayer;
|
|
|
|
private Player ViewingPlayer
|
|
{
|
|
get
|
|
{
|
|
if (Engine == null || Engine.State.Players.Count == 0) return new Player();
|
|
|
|
// Ak je Pass & Play a aktuálny hráč je človek, zobrazujeme jeho ruku
|
|
if (Engine.State.CurrentPlayer.Type == PlayerType.Human)
|
|
{
|
|
return Engine.State.CurrentPlayer;
|
|
}
|
|
|
|
// Inak primárne prvého živého človeka v hre
|
|
var firstHuman = Engine.State.Players.FirstOrDefault(p => p.Type == PlayerType.Human);
|
|
if (firstHuman != null) return firstHuman;
|
|
|
|
// Ak sú všetci hráči AI, spodný hráč je PEVNE prvý hráč (Players[0]), aby sa stôl a rozmiestnenie nemenili
|
|
return Engine.State.Players[0];
|
|
}
|
|
}
|
|
|
|
private bool IsViewingPlayerActiveTurn =>
|
|
Engine != null &&
|
|
Engine.State.TurnState == TurnState.Playing &&
|
|
Engine.State.CurrentPlayer == ViewingPlayer;
|
|
|
|
private bool IsViewingPlayerTurn =>
|
|
IsViewingPlayerActiveTurn &&
|
|
ViewingPlayer.Type == PlayerType.Human;
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender)
|
|
{
|
|
await Fx.InitParticlesAsync();
|
|
}
|
|
}
|
|
|
|
private void HandleStartGame((List<Player> players, RuleOptions rules) setup)
|
|
{
|
|
ConfiguredPlayers = setup.players;
|
|
ConfiguredRules = setup.rules;
|
|
|
|
StartNewGame();
|
|
}
|
|
|
|
private void StartNewGame()
|
|
{
|
|
// Klony hráčov pre čistú hru
|
|
var gamePlayers = ConfiguredPlayers.Select(p => new Player(p.Name, p.Type, p.Avatar, p.Difficulty, p.PersonalityId)).ToList();
|
|
|
|
Engine = new GameEngine();
|
|
Engine.OnGameEvent += HandleGameEvent;
|
|
Engine.StartGame(gamePlayers, ConfiguredRules);
|
|
|
|
_previousTurnPlayer = Engine.State.CurrentPlayer;
|
|
_ = Fx.SetSpeedLevelAsync(Engine.State.Rules.SpeedLevel);
|
|
IsInLobby = false;
|
|
LastEventMessage = "Hra začala! Veľa šťastia.";
|
|
|
|
StateHasChanged();
|
|
CheckTriggerAI();
|
|
}
|
|
|
|
private void HandleGameEvent(GameEvent e)
|
|
{
|
|
LastEventMessage = e.Message;
|
|
|
|
_ = InvokeAsync(async () =>
|
|
{
|
|
switch (e.Type)
|
|
{
|
|
case GameEventType.CardPlayed:
|
|
await Fx.PlayCardSnapAsync();
|
|
break;
|
|
case GameEventType.CardDrawn:
|
|
await Fx.PlayCardDrawAsync();
|
|
break;
|
|
case GameEventType.SevenStacked:
|
|
await Fx.PlaySevenStackAsync(Math.Max(1, e.ExtraData / 3));
|
|
await Fx.TriggerDopamineBannerAsync($"🔥 +{e.ExtraData} KARIET!", "Trestná reťaz sediem", "triple");
|
|
break;
|
|
case GameEventType.SevenCanceled:
|
|
await Fx.PlayLeafShieldAsync();
|
|
await Fx.TriggerDopamineBannerAsync("🍃 TREST ZRUŠENÝ!", $"{e.SourcePlayer?.Name ?? "Hráč"} zrušil trest Zeleným dolníkom!", "triple");
|
|
break;
|
|
case GameEventType.AceSkipped:
|
|
if (e.Message.Contains("stojí toto kolo"))
|
|
{
|
|
if (e.SourcePlayer != null)
|
|
{
|
|
await Fx.TriggerIceShatterAsync(GetPlayerPosition(e.SourcePlayer));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
await Fx.TriggerFreezeEffectAsync();
|
|
}
|
|
break;
|
|
case GameEventType.AceCountered:
|
|
if (e.SourcePlayer != null)
|
|
{
|
|
await Fx.TriggerIceShatterAsync(GetPlayerPosition(e.SourcePlayer));
|
|
await Fx.TriggerDopamineBannerAsync("⚡ ROZMRAZENÝ!", $"{e.SourcePlayer.Name} prebil Eso vlastným Esom!", "revive");
|
|
}
|
|
break;
|
|
case GameEventType.PlayerRevived:
|
|
await Fx.TriggerReviveEffectAsync(e.TargetPlayer?.Name ?? e.SourcePlayer?.Name ?? "Hráč");
|
|
break;
|
|
case GameEventType.SuitChanged:
|
|
await Fx.PlayHornikChimeAsync();
|
|
break;
|
|
case GameEventType.CardsBurned:
|
|
await Fx.PlayBurnExplosionAsync();
|
|
break;
|
|
case GameEventType.PlayerFinished:
|
|
case GameEventType.MatchWon:
|
|
case GameEventType.GameOver:
|
|
await Fx.TriggerJackpotWinAsync(e.SourcePlayer?.Name ?? "Víťaz");
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Dynamické emócie hráčov na herné udalosti
|
|
if (e.SourcePlayer != null)
|
|
{
|
|
switch (e.Type)
|
|
{
|
|
case GameEventType.AceSkipped:
|
|
SetPlayerEmotion(e.SourcePlayer, PlayerEmotion.Freezing, 3500);
|
|
break;
|
|
case GameEventType.AceCountered:
|
|
case GameEventType.SevenCanceled:
|
|
SetPlayerEmotion(e.SourcePlayer, PlayerEmotion.Relieved, 3000);
|
|
break;
|
|
case GameEventType.CardsBurned:
|
|
SetPlayerEmotion(e.SourcePlayer, PlayerEmotion.Confident, 3500);
|
|
break;
|
|
case GameEventType.PlayerFinished:
|
|
case GameEventType.MatchWon:
|
|
SetPlayerEmotion(e.SourcePlayer, PlayerEmotion.Celebrating, 10000);
|
|
break;
|
|
}
|
|
}
|
|
if (e.TargetPlayer != null)
|
|
{
|
|
switch (e.Type)
|
|
{
|
|
case GameEventType.SevenStacked:
|
|
SetPlayerEmotion(e.TargetPlayer, e.ExtraData >= 6 ? PlayerEmotion.Shocked : PlayerEmotion.Angry, 3500);
|
|
break;
|
|
case GameEventType.PlayerRevived:
|
|
SetPlayerEmotion(e.TargetPlayer, PlayerEmotion.Confident, 3500);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (Engine != null && Engine.State.CurrentPlayer.Hand.Count == 1 && !Engine.State.CurrentPlayer.HasFinished)
|
|
{
|
|
await Fx.PlayHeartbeatAsync();
|
|
}
|
|
|
|
await CheckTurnTransitionAsync();
|
|
StateHasChanged();
|
|
});
|
|
}
|
|
|
|
private async Task CheckTurnTransitionAsync()
|
|
{
|
|
if (Engine == null || Engine.State.Players.Count == 0 || Engine.State.IsGameOver) return;
|
|
var current = Engine.State.CurrentPlayer;
|
|
if (_previousTurnPlayer != null && _previousTurnPlayer != current)
|
|
{
|
|
var prev = _previousTurnPlayer;
|
|
_previousTurnPlayer = current;
|
|
await Fx.AnimateTurnTransitionAsync(GetPlayerPosition(prev), GetPlayerPosition(current));
|
|
}
|
|
else
|
|
{
|
|
_previousTurnPlayer = current;
|
|
}
|
|
}
|
|
|
|
private string GetPlayerPosition(Player player)
|
|
{
|
|
if (player == ViewingPlayer) return "bottom";
|
|
var opp = GetOpponents().FirstOrDefault(o => o.Player == player);
|
|
if (!string.IsNullOrEmpty(opp.PositionClass))
|
|
{
|
|
return opp.PositionClass.Replace("slot-", "");
|
|
}
|
|
return "top";
|
|
}
|
|
|
|
private static readonly string[] AvailableDecks = new[] { "original", "janosik", "spongebob", "cyberpunk", "fantasy", "egypt", "pets", "pixel" };
|
|
|
|
private int MultiCardDelayMs => Engine?.State.Rules.SpeedLevel switch
|
|
{
|
|
-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 current = Engine.State.Rules.SpeedLevel;
|
|
int next = current >= 5 ? -5 : current + 1;
|
|
Engine.State.Rules.SpeedLevel = next;
|
|
await Fx.SetSpeedLevelAsync(next);
|
|
StateHasChanged();
|
|
}
|
|
|
|
private string GetSpeedButtonText(int level) => level switch
|
|
{
|
|
-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
|
|
{
|
|
-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()
|
|
{
|
|
if (Engine == null) return;
|
|
int currentIdx = Array.IndexOf(AvailableDecks, Engine.State.Rules.SelectedDeck);
|
|
int nextIdx = (currentIdx + 1) % AvailableDecks.Length;
|
|
Engine.State.Rules.SelectedDeck = AvailableDecks[nextIdx];
|
|
StateHasChanged();
|
|
}
|
|
|
|
private string GetDeckIcon(string deck) => deck switch
|
|
{
|
|
"janosik" => "🪓",
|
|
"spongebob" => "🧽",
|
|
"cyberpunk" => "⚡",
|
|
"fantasy" => "🐉",
|
|
"egypt" => "⚔️",
|
|
"pets" => "🐱",
|
|
"pixel" => "🕹️",
|
|
_ => "🎴"
|
|
};
|
|
|
|
private async Task HandlePlayCards(List<Card> cards)
|
|
{
|
|
if (Engine == null || cards.Count == 0) return;
|
|
string pos = GetPlayerPosition(ViewingPlayer);
|
|
string deck = Engine.State.Rules.SelectedDeck;
|
|
|
|
if (cards.Count == 2)
|
|
{
|
|
_ = Fx.TriggerComboSlamAsync(2, "⚡ DVOJIČKA!");
|
|
}
|
|
else if (cards.Count == 3)
|
|
{
|
|
_ = Fx.TriggerComboSlamAsync(3, "🔥 TROJITÝ ÚDER!");
|
|
}
|
|
else if (cards.Count >= 4)
|
|
{
|
|
_ = Fx.TriggerComboSlamAsync(4, "👑 KVADRA / MEGA COMBO!");
|
|
}
|
|
|
|
for (int i = 0; i < cards.Count; i++)
|
|
{
|
|
if (i == cards.Count - 1)
|
|
{
|
|
await Fx.AnimateCardPlayAsync(pos, cards[i], deck);
|
|
}
|
|
else
|
|
{
|
|
_ = Fx.AnimateCardPlayAsync(pos, cards[i], deck);
|
|
await Task.Delay(MultiCardDelayMs);
|
|
}
|
|
}
|
|
|
|
Engine.PlayCards(ViewingPlayer, cards);
|
|
StateHasChanged();
|
|
CheckTriggerAI();
|
|
}
|
|
|
|
private async Task HandleDeckClick()
|
|
{
|
|
if (Engine == null || !IsViewingPlayerTurn) return;
|
|
string pos = GetPlayerPosition(ViewingPlayer);
|
|
string deck = Engine.State.Rules.SelectedDeck;
|
|
|
|
if (Engine.State.PendingDrawCount > 0)
|
|
{
|
|
int drawCount = Engine.State.PendingDrawCount;
|
|
int staggerDelay = Math.Max(25, (int)(MultiCardDelayMs * 0.75));
|
|
for (int i = 0; i < drawCount; i++)
|
|
{
|
|
if (i == drawCount - 1)
|
|
{
|
|
await Fx.AnimateCardDrawAsync(pos, deck);
|
|
}
|
|
else
|
|
{
|
|
_ = Fx.AnimateCardDrawAsync(pos, deck);
|
|
await Task.Delay(staggerDelay);
|
|
}
|
|
}
|
|
}
|
|
else if (!Engine.State.PendingSkip)
|
|
{
|
|
await Fx.AnimateCardDrawAsync(pos, deck);
|
|
}
|
|
|
|
Engine.DrawOrAcknowledge(ViewingPlayer);
|
|
StateHasChanged();
|
|
CheckTriggerAI();
|
|
}
|
|
|
|
private void HandleSuitSelected(Suit suit)
|
|
{
|
|
if (Engine == null) return;
|
|
Engine.ChooseSuit(ViewingPlayer, suit);
|
|
StateHasChanged();
|
|
CheckTriggerAI();
|
|
}
|
|
|
|
private void HandlePassContinue()
|
|
{
|
|
if (Engine == null) return;
|
|
Engine.ContinueFromPassScreen();
|
|
StateHasChanged();
|
|
CheckTriggerAI();
|
|
}
|
|
|
|
private void HandleStartNextRound()
|
|
{
|
|
if (Engine == null) return;
|
|
Engine.StartNextRound();
|
|
StateHasChanged();
|
|
CheckTriggerAI();
|
|
}
|
|
|
|
private async Task HandleBurn(Rank rank)
|
|
{
|
|
if (Engine == null) return;
|
|
var cards = ViewingPlayer.Hand.Where(c => c.Rank == rank).ToList();
|
|
if (cards.Count == 4)
|
|
{
|
|
string pos = GetPlayerPosition(ViewingPlayer);
|
|
string deck = Engine.State.Rules.SelectedDeck;
|
|
for (int i = 0; i < cards.Count; i++)
|
|
{
|
|
if (i == cards.Count - 1)
|
|
{
|
|
await Fx.AnimateCardPlayAsync(pos, cards[i], deck);
|
|
}
|
|
else
|
|
{
|
|
_ = Fx.AnimateCardPlayAsync(pos, cards[i], deck);
|
|
await Task.Delay(MultiCardDelayMs);
|
|
}
|
|
}
|
|
Engine.BurnCards(ViewingPlayer, cards);
|
|
StateHasChanged();
|
|
CheckTriggerAI();
|
|
}
|
|
}
|
|
|
|
private Rank? CheckFourOfAKindRank(Player player)
|
|
{
|
|
if (Engine == null || !Engine.State.Rules.FourOfAKindBurns) return null;
|
|
var group = player.Hand.GroupBy(c => c.Rank).FirstOrDefault(g => g.Count() == 4);
|
|
return group?.Key;
|
|
}
|
|
|
|
private void CheckTriggerAI()
|
|
{
|
|
if (Engine == null || Engine.State.IsGameOver) return;
|
|
|
|
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;
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
var aiPlayer = Engine.State.CurrentPlayer;
|
|
if (aiPlayer.Type != PlayerType.AI) return;
|
|
|
|
if (aiPlayer.Hand.Count <= 2)
|
|
{
|
|
SetPlayerEmotion(aiPlayer, PlayerEmotion.Confident, 3500);
|
|
}
|
|
else if (aiPlayer.Hand.Count >= 8)
|
|
{
|
|
SetPlayerEmotion(aiPlayer, PlayerEmotion.Desperate, 3500);
|
|
}
|
|
else if (Engine.State.PendingSkip)
|
|
{
|
|
SetPlayerEmotion(aiPlayer, PlayerEmotion.Freezing, 3500);
|
|
}
|
|
else if (Engine.State.PendingDrawCount > 0)
|
|
{
|
|
SetPlayerEmotion(aiPlayer, PlayerEmotion.Shocked, 3500);
|
|
}
|
|
else
|
|
{
|
|
SetPlayerEmotion(aiPlayer, PlayerEmotion.Thinking, 3500);
|
|
}
|
|
|
|
if (Engine.State.Rules.AIHumanMode)
|
|
{
|
|
string thought = AIDialogueService.GetThinkingDialogue(aiPlayer, Engine.State);
|
|
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 () =>
|
|
{
|
|
string pos = GetPlayerPosition(aiPlayer);
|
|
string deck = Engine.State.Rules.SelectedDeck;
|
|
|
|
if (decision.Type == AIDecisionType.PlayCard)
|
|
{
|
|
var cardsToPlay = decision.Cards ?? (decision.Card != null ? new List<Card> { decision.Card } : new List<Card>());
|
|
if (cardsToPlay.Count > 0)
|
|
{
|
|
if (cardsToPlay.Count == 2)
|
|
{
|
|
_ = Fx.TriggerComboSlamAsync(2, $"⚡ {aiPlayer.Name}: DVOJIČKA!");
|
|
}
|
|
else if (cardsToPlay.Count == 3)
|
|
{
|
|
_ = Fx.TriggerComboSlamAsync(3, $"🔥 {aiPlayer.Name}: TROJITÝ ÚDER!");
|
|
}
|
|
else if (cardsToPlay.Count >= 4)
|
|
{
|
|
_ = Fx.TriggerComboSlamAsync(4, $"👑 {aiPlayer.Name}: KVADRA!");
|
|
}
|
|
|
|
for (int i = 0; i < cardsToPlay.Count; i++)
|
|
{
|
|
if (i == cardsToPlay.Count - 1)
|
|
{
|
|
await Fx.AnimateCardPlayAsync(pos, cardsToPlay[i], deck);
|
|
}
|
|
else
|
|
{
|
|
_ = Fx.AnimateCardPlayAsync(pos, cardsToPlay[i], deck);
|
|
await Task.Delay(MultiCardDelayMs);
|
|
}
|
|
}
|
|
Engine.PlayCards(aiPlayer, cardsToPlay, decision.ChosenSuit);
|
|
}
|
|
}
|
|
else if (decision.Type == AIDecisionType.BurnCards && decision.Cards != null)
|
|
{
|
|
for (int i = 0; i < decision.Cards.Count; i++)
|
|
{
|
|
if (i == decision.Cards.Count - 1)
|
|
{
|
|
await Fx.AnimateCardPlayAsync(pos, decision.Cards[i], deck);
|
|
}
|
|
else
|
|
{
|
|
_ = Fx.AnimateCardPlayAsync(pos, decision.Cards[i], deck);
|
|
await Task.Delay(MultiCardDelayMs);
|
|
}
|
|
}
|
|
Engine.BurnCards(aiPlayer, decision.Cards);
|
|
}
|
|
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;
|
|
int staggerDelay = Math.Max(25, (int)(MultiCardDelayMs * 0.75));
|
|
for (int i = 0; i < drawCount; i++)
|
|
{
|
|
if (i == drawCount - 1)
|
|
{
|
|
await Fx.AnimateCardDrawAsync(pos, deck);
|
|
}
|
|
else
|
|
{
|
|
_ = Fx.AnimateCardDrawAsync(pos, deck);
|
|
await Task.Delay(staggerDelay);
|
|
}
|
|
}
|
|
}
|
|
else if (!Engine.State.PendingSkip)
|
|
{
|
|
await Fx.AnimateCardDrawAsync(pos, deck);
|
|
}
|
|
Engine.DrawOrAcknowledge(aiPlayer);
|
|
}
|
|
|
|
StateHasChanged();
|
|
CheckTriggerAI();
|
|
});
|
|
}
|
|
catch (TaskCanceledException) { }
|
|
}, token);
|
|
}
|
|
}
|
|
|
|
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 void SetPlayerEmotion(Player player, PlayerEmotion emotion, int durationMs = 3000)
|
|
{
|
|
player.CurrentEmotion = emotion;
|
|
StateHasChanged();
|
|
|
|
if (player.Type == PlayerType.AI && !string.IsNullOrEmpty(player.PersonalityId))
|
|
{
|
|
_ = Fx.PlayBotSfxAsync(player.PersonalityId, emotion.ToString().ToLowerInvariant());
|
|
}
|
|
|
|
if (emotion != PlayerEmotion.Neutral && emotion != PlayerEmotion.Celebrating)
|
|
{
|
|
_ = Task.Run(async () =>
|
|
{
|
|
await Task.Delay(durationMs);
|
|
if (player.CurrentEmotion == emotion)
|
|
{
|
|
player.CurrentEmotion = PlayerEmotion.Neutral;
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private void CycleTableTheme()
|
|
{
|
|
if (Engine == null) return;
|
|
Engine.State.Rules.TableTheme = Engine.State.Rules.TableTheme switch
|
|
{
|
|
TableTheme.ClassicGreen => TableTheme.RoyalBurgundy,
|
|
TableTheme.RoyalBurgundy => TableTheme.WoodCabin,
|
|
TableTheme.WoodCabin => TableTheme.CyberNeon,
|
|
TableTheme.CyberNeon => TableTheme.ClassicGreen,
|
|
_ => TableTheme.ClassicGreen
|
|
};
|
|
StateHasChanged();
|
|
}
|
|
|
|
private string GetThemeName(TableTheme theme) => theme switch
|
|
{
|
|
TableTheme.ClassicGreen => "Zelené plátno",
|
|
TableTheme.RoyalBurgundy => "Kráľovský bord",
|
|
TableTheme.WoodCabin => "Drevený zrub",
|
|
TableTheme.CyberNeon => "Cyberpunk neón",
|
|
_ => "Zelené plátno"
|
|
};
|
|
|
|
private string GetThemeIcon(TableTheme theme) => theme switch
|
|
{
|
|
TableTheme.ClassicGreen => "🍀",
|
|
TableTheme.RoyalBurgundy => "👑",
|
|
TableTheme.WoodCabin => "🪵",
|
|
TableTheme.CyberNeon => "⚡",
|
|
_ => "🍀"
|
|
};
|
|
|
|
private IEnumerable<(Player Player, string PositionClass)> GetOpponents()
|
|
{
|
|
if (Engine == null) return Enumerable.Empty<(Player, string)>();
|
|
|
|
var opponents = Engine.State.Players.Where(p => p != ViewingPlayer).ToList();
|
|
var result = new List<(Player, string)>();
|
|
|
|
if (opponents.Count == 1)
|
|
{
|
|
result.Add((opponents[0], "slot-top"));
|
|
}
|
|
else if (opponents.Count == 2)
|
|
{
|
|
result.Add((opponents[0], "slot-left"));
|
|
result.Add((opponents[1], "slot-right"));
|
|
}
|
|
else if (opponents.Count >= 3)
|
|
{
|
|
result.Add((opponents[0], "slot-left"));
|
|
result.Add((opponents[1], "slot-top"));
|
|
result.Add((opponents[2], "slot-right"));
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
private void HandlePlayAgain()
|
|
{
|
|
PlayerThoughts.Clear();
|
|
CurrentJoke = null;
|
|
IsLogRadarMinimized = true;
|
|
StartNewGame();
|
|
}
|
|
|
|
private void ReturnToLobby()
|
|
{
|
|
_aiCts?.Cancel();
|
|
PlayerThoughts.Clear();
|
|
CurrentJoke = null;
|
|
IsLogRadarMinimized = true;
|
|
IsInLobby = true;
|
|
Engine = null;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_aiCts?.Cancel();
|
|
_aiCts?.Dispose();
|
|
}
|
|
}
|