Files
FaraonCardGame/Faraon.Client/Pages/Home.razor
T
2026-09-03 17:54:36 +02:00

798 lines
32 KiB
Plaintext

@page "/"
@inject FxService Fx
@inject IFaraonAI AI
@inject IJSRuntime JS
@if (IsInLobby || Engine == null)
{
<Lobby OnStartGame="HandleStartGame" />
}
else
{
<div id="game-table-container">
<!-- 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;">
<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 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">
@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">
@(IsMuted ? "🔇" : "🔊")
</button>
<button class="btn-game-icon" title="Návrat do Lobby" @onclick="ReturnToLobby">
🏠
</button>
</div>
</header>
<!-- 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;
<PlayerSlot Player="@opp.Player"
IsCurrentTurn="@(Engine.State.CurrentPlayer == opp.Player)"
IsFrozen="@isOppFrozen"
CanBeRevived="@canOppBeRevived"
ReviveRoundsRemaining="@Engine.State.ReviveRoundsRemaining"
ReviveTurnsRemaining="@Engine.State.ReviveTurnsRemaining"
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.Avatar</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>
}
</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"
OnPlayAgain="HandlePlayAgain"
OnBackToLobby="ReturnToLobby" />
}
</div>
}
@code {
private bool IsInLobby = true;
private bool IsMuted = false;
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)).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.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
{
1 => 130,
2 => 105,
4 => 50,
5 => 25,
_ => 80
};
private async Task CycleSpeed()
{
if (Engine == null) return;
int next = (Engine.State.Rules.SpeedLevel % 5) + 1;
Engine.State.Rules.SpeedLevel = next;
await Fx.SetSpeedLevelAsync(next);
StateHasChanged();
}
private string GetSpeedButtonText(int level) => level switch
{
1 => "🐢 0.5x",
2 => "🚶 0.75x",
3 => "⚡ 1x",
4 => "🔥 1.5x",
5 => "🚀 2.5x",
_ => "⚡ 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)"
};
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)
{
_aiCts?.Cancel();
_aiCts = new System.Threading.CancellationTokenSource();
var token = _aiCts.Token;
_ = Task.Run(async () =>
{
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;
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.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 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 async Task ToggleMute()
{
IsMuted = await Fx.ToggleMuteAsync();
}
private void HandlePlayAgain()
{
StartNewGame();
}
private void ReturnToLobby()
{
_aiCts?.Cancel();
IsInLobby = true;
Engine = null;
}
}