@page "/" @using Faraon.Core.AI @inject FxService Fx @inject IFaraonAI AI @inject IJSRuntime JS @if (IsInLobby || Engine == null) { } else {

FARAÓN

@if (Engine.State.Rules.OlympicMode) { 🏅 KOLO #@Engine.State.RoundNumber }
@if (CurrentJoke != null) {
@CurrentJoke.Joke.Icon 🎭 @CurrentJoke.Joke.Category
@CurrentJoke.Storyteller.Avatar @CurrentJoke.Storyteller.Name
@CurrentJoke.Joke.Setup
@CurrentJoke.Joke.Punchline
}
@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); }
@Engine.State.DrawDeck.Count
@if (Engine.State.TopCard != null) { } @if (Engine.State.PendingSkip) {
❄️ ESO: ZMRAZENIE!
} @if (Engine.State.PendingDrawCount > 0) {
🔥 +@Engine.State.PendingDrawCount Kariet!
} @if (Engine.State.ActiveSuit.HasValue) { var dummy = new Card(Engine.State.ActiveSuit.Value, Rank.OverKnave);
Farba:
@dummy.SuitName
} @if (Engine.State.TopCard != null && Engine.State.TopCard.IsGreenUnderKnave && Engine.State.Rules.GreenUnderKnaveMode == GreenUnderKnaveMode.UniversalWildcard) {
🍃 ZELENÝ DOLNÍK: Daj čokoľvek!
}
@{ var isBottomFrozen = Engine.State.PendingSkip && IsViewingPlayerActiveTurn; var canBottomBeRevived = Engine.State.Rules.RedSevenRevives && Engine.State.LastFinishedPlayer == ViewingPlayer && Engine.State.ReviveTurnsRemaining > 0; }
@if (isBottomFrozen && !ViewingPlayer.HasFinished && !ViewingPlayer.IsEliminated) {
@(ViewingPlayer.Type == PlayerType.Human ? "❄️ STOJÍŠ TOTO KOLO" : "❄️ ZMRAZENÝ")
} else if (IsViewingPlayerActiveTurn && !ViewingPlayer.HasFinished && !ViewingPlayer.IsEliminated) {
@(ViewingPlayer.Type == PlayerType.Human ? "SI NA ŤAHU" : "NA ŤAHU")
} @if (ViewingPlayer.HasFinished) {
@ViewingPlayer.FinishedPosition
}
@ViewingPlayer.Avatar @ViewingPlayer.Name @if (ViewingPlayer.Type == PlayerType.AI) { (AI) } (@ViewingPlayer.Hand.Count @(ViewingPlayer.Hand.Count == 1 ? "karta" : (ViewingPlayer.Hand.Count < 5 ? "karty" : "kariet"))) @if (Engine.State.Rules.OlympicMode) { 🏅 @ViewingPlayer.OlympicCardsCount @(ViewingPlayer.OlympicCardsCount == 1 ? "karta" : (ViewingPlayer.OlympicCardsCount < 5 ? "karty" : "kariet")) }
@if (isBottomFrozen && !ViewingPlayer.HasFinished && !ViewingPlayer.IsEliminated) {
} @if (canBottomBeRevived && ViewingPlayer.HasFinished) {
❤️ @(Engine.State.ReviveRoundsRemaining > 1 ? $"Oživenie: {Engine.State.ReviveRoundsRemaining} kolá" : (Engine.State.ReviveTurnsRemaining <= 1 ? "Oživenie: 1 ťah" : "Oživenie: 1 kolo"))
} @{ var bottomThought = GetThoughtState(ViewingPlayer); } @if (bottomThought != null && !string.IsNullOrEmpty(bottomThought.Text)) {
💭 @bottomThought.Text
}
@if (!ViewingPlayer.IsEliminated) { } else {
☠️ Bol si vyradený z Olympijského turnaja. Sleduj zápas až do konca!
}
@if (IsViewingPlayerTurn && Engine.State.TurnState == TurnState.Playing && !ViewingPlayer.IsEliminated) {
@if (CheckFourOfAKindRank(ViewingPlayer) is Rank burnRank) { }
} @if (!string.IsNullOrEmpty(LastEventMessage)) {
@LastEventMessage
} @if (Engine.State.TurnState == TurnState.SelectingSuit && Engine.State.PendingHornikPlayer == ViewingPlayer) { } @if (Engine.State.TurnState == TurnState.PassScreen) { } @if (Engine.State.TurnState == TurnState.RoundOver) { } @if (Engine.State.IsGameOver) { }
} @code { private bool IsInLobby = true; private bool IsMuted = false; private GameEngine? Engine; private string LastEventMessage = ""; private System.Threading.CancellationTokenSource? _aiCts; private List 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 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.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(); } 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 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 (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 { decision.Card } : new List()); 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 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)>(); 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; StartNewGame(); } private void ReturnToLobby() { _aiCts?.Cancel(); PlayerThoughts.Clear(); CurrentJoke = null; IsInLobby = true; Engine = null; } }