inial impl
@@ -0,0 +1,6 @@
|
||||
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
@@ -0,0 +1,34 @@
|
||||
@namespace Faraon.Client.Components
|
||||
|
||||
<div class="game-card marias-card @(IsBack ? "card-back" : "") @(IsPlayable ? "playable" : (IsInteractive && !IsPlayable ? "unplayable" : "")) @CustomClass"
|
||||
style="@CustomStyle"
|
||||
@onclick="HandleClick">
|
||||
|
||||
@if (IsBack || Card == null)
|
||||
{
|
||||
<img src="assets/cards/@(Deck)/Card_Back.webp" class="marias-card-img" alt="Rub karty" draggable="false" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<img src="assets/cards/@(Deck)/@(Card.Suit)_@(Card.Rank).webp" class="marias-card-img" alt="@Card.Name" draggable="false" />
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public Card? Card { get; set; }
|
||||
[Parameter] public string Deck { get; set; } = "original";
|
||||
[Parameter] public bool IsBack { get; set; } = false;
|
||||
[Parameter] public bool IsPlayable { get; set; } = false;
|
||||
[Parameter] public bool IsInteractive { get; set; } = false;
|
||||
[Parameter] public string CustomClass { get; set; } = "";
|
||||
[Parameter] public string CustomStyle { get; set; } = "";
|
||||
[Parameter] public EventCallback<Card> OnCardClicked { get; set; }
|
||||
|
||||
private async Task HandleClick()
|
||||
{
|
||||
if (IsInteractive && Card != null)
|
||||
{
|
||||
await OnCardClicked.InvokeAsync(Card);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
@namespace Faraon.Client.Components
|
||||
|
||||
<div class="game-over-overlay">
|
||||
<div class="game-over-card">
|
||||
<div class="game-over-title">Výsledky Hry 👑</div>
|
||||
|
||||
<div class="podium-list">
|
||||
@foreach (var player in Players.OrderBy(p => p.FinishedPosition ?? 99))
|
||||
{
|
||||
<div class="podium-row @(player.FinishedPosition == 1 ? "winner" : "")">
|
||||
<div class="podium-pos">
|
||||
@if (player.FinishedPosition == 1) { <span>🥇</span> }
|
||||
else if (player.FinishedPosition == 2) { <span>🥈</span> }
|
||||
else if (player.FinishedPosition == 3) { <span>🥉</span> }
|
||||
else { <span>#@(player.FinishedPosition ?? 4)</span> }
|
||||
</div>
|
||||
<div class="podium-player">
|
||||
<span>@player.Avatar</span>
|
||||
<span>@player.Name</span>
|
||||
@if (player.Type == PlayerType.AI)
|
||||
{
|
||||
<span style="font-size: 0.75rem; opacity: 0.7;">(AI)</span>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
@(player.FinishedPosition == 1 ? "Víťaz! 🎉" : $"{player.FinishedPosition}. miesto")
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 12px; justify-content: center;">
|
||||
<button class="btn-primary-glow" @onclick="OnPlayAgain">
|
||||
Hrať znova 🔄
|
||||
</button>
|
||||
<button class="btn-pill" style="padding: 12px 24px;" @onclick="OnBackToLobby">
|
||||
Do Lobby 🏠
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public List<Player> Players { get; set; } = new();
|
||||
[Parameter] public EventCallback OnPlayAgain { get; set; }
|
||||
[Parameter] public EventCallback OnBackToLobby { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
@namespace Faraon.Client.Components
|
||||
|
||||
<div class="lobby-screen">
|
||||
<div class="lobby-container">
|
||||
<div class="lobby-header">
|
||||
<h1 class="lobby-title">FARAÓN</h1>
|
||||
<div class="lobby-subtitle">Tradičná mariášová kartová hra s moderným nábojom</div>
|
||||
</div>
|
||||
|
||||
<!-- Výber počtu hráčov -->
|
||||
<div style="text-align: center; margin-bottom: 8px; font-weight: 700; font-size: 0.95rem; color: var(--gold);">
|
||||
POČET HRÁČOV:
|
||||
</div>
|
||||
<div class="player-count-picker">
|
||||
@for (int count = 2; count <= 4; count++)
|
||||
{
|
||||
var c = count;
|
||||
<button class="btn-pill @(SelectedPlayerCount == c ? "active" : "")" @onclick="() => SetPlayerCount(c)">
|
||||
@c Hráči
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Karty konfigurácie jednotlivých hráčov -->
|
||||
<div class="players-config-grid">
|
||||
@for (int i = 0; i < SelectedPlayerCount; i++)
|
||||
{
|
||||
var idx = i;
|
||||
var p = PlayerConfigs[idx];
|
||||
|
||||
<div class="player-setup-card">
|
||||
<!-- Avatar selektor -->
|
||||
<div style="cursor: pointer; font-size: 2.4rem;" title="Kliknite pre zmenu avatara" @onclick="() => CycleAvatar(idx)">
|
||||
@p.Avatar
|
||||
</div>
|
||||
|
||||
<!-- Meno hráča -->
|
||||
<input class="player-setup-input" type="text" @bind="p.Name" placeholder="Meno hráča" />
|
||||
|
||||
<!-- Typ hráča: Človek vs AI -->
|
||||
<select class="player-setup-select" @bind="p.Type">
|
||||
<option value="@PlayerType.Human">👤 Človek (Hráč)</option>
|
||||
<option value="@PlayerType.AI">🤖 AI Počítač</option>
|
||||
</select>
|
||||
|
||||
<!-- Obtiažnosť AI (ak je AI) -->
|
||||
@if (p.Type == PlayerType.AI)
|
||||
{
|
||||
<select class="player-setup-select" @bind="p.Difficulty">
|
||||
<option value="@AIDifficulty.Easy">🌱 Začiatočník (Ľahký)</option>
|
||||
<option value="@AIDifficulty.Medium">⚔️ Taktik (Stredný)</option>
|
||||
<option value="@AIDifficulty.Hard">🔥 Veľmajster (Ťažký)</option>
|
||||
</select>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</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 (+2, +4, +6...)</span>
|
||||
</label>
|
||||
<label class="rule-check-item">
|
||||
<input type="checkbox" @bind="Rules.CounterAceWithAce" />
|
||||
<span>Prebíjanie esa esom</span>
|
||||
</label>
|
||||
<label class="rule-check-item">
|
||||
<input type="checkbox" @bind="Rules.RedSevenRevives" />
|
||||
<span>Červená sedma oživuje víťaza</span>
|
||||
</label>
|
||||
<label class="rule-check-item">
|
||||
<input type="checkbox" @bind="Rules.FourOfAKindBurns" />
|
||||
<span>Spálenie (4 rovnaké karty)</span>
|
||||
</label>
|
||||
<label class="rule-check-item">
|
||||
<input type="checkbox" @bind="Rules.PassAndPlayPrivacyScreen" />
|
||||
<span>Pass & Play clona pre ľudí</span>
|
||||
</label>
|
||||
<div style="display: flex; align-items: center; gap: 8px; font-size: 0.9rem;">
|
||||
<span>Kariet do ruky:</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>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Výber balíčka kariet (Deck Theme) -->
|
||||
<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áš (Original)</div>
|
||||
<div class="deck-desc">Autentický dvojhlavý Tell vzor (Piatnik)</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 (Krabby Patty & postavičky)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Štart tlačidlo -->
|
||||
<div style="text-align: center;">
|
||||
<button class="btn-primary-glow" style="padding: 14px 45px; font-size: 1.25rem;" @onclick="StartGame">
|
||||
SPUSTIŤ HRU ⚔️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public EventCallback<(List<Player>, RuleOptions)> OnStartGame { get; set; }
|
||||
|
||||
private int SelectedPlayerCount = 4;
|
||||
private RuleOptions Rules = new();
|
||||
|
||||
private readonly string[] Avatars = ["👑", "🧙♂️", "🦊", "🦁", "🐯", "🤖", "🐉", "🥷", "👸", "🤠", "🦅", "🐺"];
|
||||
|
||||
private List<Player> PlayerConfigs = new()
|
||||
{
|
||||
new Player("Ty (Hráč 1)", PlayerType.Human, "👑"),
|
||||
new Player("AI Jano", PlayerType.AI, "🦊", AIDifficulty.Medium),
|
||||
new Player("AI Zuzka", PlayerType.AI, "👸", AIDifficulty.Hard),
|
||||
new Player("AI Mišo", PlayerType.AI, "🤖", AIDifficulty.Easy)
|
||||
};
|
||||
|
||||
private void SetPlayerCount(int count)
|
||||
{
|
||||
SelectedPlayerCount = count;
|
||||
}
|
||||
|
||||
private void CycleAvatar(int index)
|
||||
{
|
||||
var currentAvatar = PlayerConfigs[index].Avatar;
|
||||
int nextIdx = (Array.IndexOf(Avatars, currentAvatar) + 1) % Avatars.Length;
|
||||
PlayerConfigs[index].Avatar = Avatars[nextIdx];
|
||||
}
|
||||
|
||||
private async Task StartGame()
|
||||
{
|
||||
var activePlayers = PlayerConfigs.Take(SelectedPlayerCount).Select(p =>
|
||||
new Player(p.Name, p.Type, p.Avatar, p.Difficulty)
|
||||
).ToList();
|
||||
|
||||
await OnStartGame.InvokeAsync((activePlayers, Rules));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
@namespace Faraon.Client.Components
|
||||
|
||||
<div class="marias-pip-wrapper @CustomClass" style="@CustomStyle">
|
||||
@if (Suit == Suit.Hearts)
|
||||
{
|
||||
<!-- ČERVEŇ / SRDCE -->
|
||||
<svg viewBox="0 0 32 32" class="pip-svg">
|
||||
<defs>
|
||||
<radialGradient id="heartGrad" cx="35%" cy="35%" r="65%">
|
||||
<stop offset="0%" stop-color="#ff4d4d"/>
|
||||
<stop offset="70%" stop-color="#c1121f"/>
|
||||
<stop offset="100%" stop-color="#780000"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<path d="M16 28 C16 28 3 19 3 10 C3 5 7.5 2 12 2 C14.5 2 15.5 3.5 16 4.5 C16.5 3.5 17.5 2 20 2 C24.5 2 29 5 29 10 C29 19 16 28 16 28 Z"
|
||||
fill="url(#heartGrad)" stroke="#5e0000" stroke-width="1.2"/>
|
||||
<path d="M16 23 C16 23 7 16 7 10.5 C7 7 9 4.5 12 4.5 C14 4.5 15 5.5 16 7 C17 5.5 18 4.5 20 4.5 C23 4.5 25 7 25 10.5 C25 16 16 23 16 23 Z"
|
||||
fill="none" stroke="#ffd166" stroke-width="0.8" opacity="0.65"/>
|
||||
</svg>
|
||||
}
|
||||
else if (Suit == Suit.Leaves)
|
||||
{
|
||||
<!-- ZELEŇ / LIST -->
|
||||
<svg viewBox="0 0 32 32" class="pip-svg">
|
||||
<defs>
|
||||
<linearGradient id="leafGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#38b000"/>
|
||||
<stop offset="60%" stop-color="#1b4332"/>
|
||||
<stop offset="100%" stop-color="#081c15"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M16 2 C10 8 5 15 6 24 C7 27.5 11 29.5 16 30 C21 29.5 25 27.5 26 24 C27 15 22 8 16 2 Z"
|
||||
fill="url(#leafGrad)" stroke="#081c15" stroke-width="1.2"/>
|
||||
<path d="M16 4 L16 28" stroke="#ffdd00" stroke-width="1.4" stroke-linecap="round"/>
|
||||
<path d="M16 10 Q10 13 8 17 M16 16 Q10 19 8 23 M16 10 Q22 13 24 17 M16 16 Q22 19 24 23"
|
||||
stroke="#ffdd00" stroke-width="1.1" fill="none" stroke-linecap="round"/>
|
||||
</svg>
|
||||
}
|
||||
else if (Suit == Suit.Acorns)
|
||||
{
|
||||
<!-- ŽALUĎ -->
|
||||
<svg viewBox="0 0 32 32" class="pip-svg">
|
||||
<defs>
|
||||
<linearGradient id="acornNutGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#e9c46a"/>
|
||||
<stop offset="60%" stop-color="#bc6c25"/>
|
||||
<stop offset="100%" stop-color="#703604"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="acornCapGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#8c5835"/>
|
||||
<stop offset="100%" stop-color="#3d2110"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Stopka -->
|
||||
<path d="M16 2 Q15 5 16 7" stroke="#3d2110" stroke-width="2" stroke-linecap="round" fill="none"/>
|
||||
<!-- Čiapočka -->
|
||||
<path d="M8 12 C7 8 12 7 16 7 C20 7 25 8 24 12 Q16 14 8 12 Z"
|
||||
fill="url(#acornCapGrad)" stroke="#221107" stroke-width="1.2"/>
|
||||
<path d="M11 9 L21 11 M13 8 L19 12" stroke="#d4a373" stroke-width="0.8" opacity="0.6"/>
|
||||
<!-- Oriešok / Telo žaluďa -->
|
||||
<path d="M9 13 C9 22 13 29 16 30.5 C19 29 23 22 23 13 Q16 14.5 9 13 Z"
|
||||
fill="url(#acornNutGrad)" stroke="#432204" stroke-width="1.2"/>
|
||||
<path d="M12 15 C12 21 14 26 16 28.5" stroke="#fff3b0" stroke-width="1.1" fill="none" opacity="0.8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- GUĽA / ROLNIČKA -->
|
||||
<svg viewBox="0 0 32 32" class="pip-svg">
|
||||
<defs>
|
||||
<radialGradient id="bellGrad" cx="35%" cy="30%" r="70%">
|
||||
<stop offset="0%" stop-color="#fff07c"/>
|
||||
<stop offset="40%" stop-color="#f4a261"/>
|
||||
<stop offset="85%" stop-color="#e76f51"/>
|
||||
<stop offset="100%" stop-color="#8c2f16"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<!-- Očko na zavesenie -->
|
||||
<ellipse cx="16" cy="4.5" rx="3.5" ry="2.5" fill="none" stroke="#b07d10" stroke-width="1.5"/>
|
||||
<!-- Telo gule -->
|
||||
<circle cx="16" cy="18" r="12" fill="url(#bellGrad)" stroke="#66200e" stroke-width="1.2"/>
|
||||
<!-- Stredový červeno-zlatý pás -->
|
||||
<path d="M4.5 17 Q16 13 27.5 17" stroke="#c1121f" stroke-width="3" fill="none"/>
|
||||
<path d="M4.5 17 Q16 13 27.5 17" stroke="#ffe066" stroke-width="1" fill="none"/>
|
||||
<!-- Štrbina a stredová dierka -->
|
||||
<circle cx="16" cy="23.5" r="2" fill="#2b0d06"/>
|
||||
<path d="M10.5 23.5 L21.5 23.5" stroke="#2b0d06" stroke-width="1.6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public Suit Suit { get; set; } = Suit.Hearts;
|
||||
[Parameter] public string CustomClass { get; set; } = "";
|
||||
[Parameter] public string CustomStyle { get; set; } = "";
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
@namespace Faraon.Client.Components
|
||||
|
||||
<div class="pass-screen-overlay">
|
||||
<div class="pass-screen-card">
|
||||
<div class="pass-avatar">@Player.Avatar</div>
|
||||
<div class="pass-title">Na rade je @Player.Name!</div>
|
||||
<div class="pass-subtitle">Odovzdajte zariadenie hráčovi <strong>@Player.Name</strong> a uistite sa, že nikto iný nevidí obrazovku.</div>
|
||||
<button class="btn-primary-glow" @onclick="HandleContinue">
|
||||
Odkryť karty a hrať 🃏
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public Player Player { get; set; } = default!;
|
||||
[Parameter] public EventCallback OnContinue { get; set; }
|
||||
|
||||
private async Task HandleContinue()
|
||||
{
|
||||
await OnContinue.InvokeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
@namespace Faraon.Client.Components
|
||||
|
||||
<div class="player-hand-container">
|
||||
@if (SelectedCards.Count > 0 && IsMyTurn)
|
||||
{
|
||||
var firstSelected = SelectedCards[0];
|
||||
var lastSelected = SelectedCards[SelectedCards.Count - 1];
|
||||
var matchingHandCards = Player.Hand.Where(c => c.Rank == firstSelected.Rank).ToList();
|
||||
var canPlayCombo = CanPlayCards(SelectedCards);
|
||||
|
||||
<div class="hand-combo-action-bar animate-pop">
|
||||
<!-- Poradie vykladania kariet -->
|
||||
<div class="combo-sequence-container">
|
||||
<span class="combo-seq-label">Poradie:</span>
|
||||
<div class="combo-sequence-items">
|
||||
@for (int s = 0; s < SelectedCards.Count; s++)
|
||||
{
|
||||
var sc = SelectedCards[s];
|
||||
var isFinal = (s == SelectedCards.Count - 1 && SelectedCards.Count > 1);
|
||||
<span class="combo-seq-item @(isFinal ? "combo-seq-final" : "")" style="color: @sc.SuitColor;">
|
||||
<span class="seq-num">@(s + 1).</span>
|
||||
<span class="seq-symbol">@sc.SuitSymbol</span>
|
||||
<span class="seq-suit">@sc.SuitName</span>
|
||||
@if (isFinal)
|
||||
{
|
||||
<span class="seq-crown" title="Karta ostane na vrchu odhadzovacieho balíčka">👑</span>
|
||||
}
|
||||
</span>
|
||||
@if (s < SelectedCards.Count - 1)
|
||||
{
|
||||
<span class="combo-seq-arrow">➔</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tlačidlo na zahranie vybratých kariet -->
|
||||
@if (canPlayCombo)
|
||||
{
|
||||
<button class="btn btn-gold btn-combo-play" @onclick="PlaySelected">
|
||||
<span>⚡ Zahrať (@SelectedCards.Count @(SelectedCards.Count == 1 ? "kartu" : (SelectedCards.Count < 5 ? "karty" : "kariet")))</span>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="combo-invalid-badge">
|
||||
<span>Nemožno zahrať na stôl</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Možnosť otočiť poradie kariet -->
|
||||
@if (SelectedCards.Count > 1)
|
||||
{
|
||||
<button class="btn btn-dark btn-combo-reverse" @onclick="ReverseSelectionOrder" title="Otočiť poradie kariet (zmení vrchnú kartu na stole)">
|
||||
<span>⇄ Otočiť</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- Vybrať všetky karty danej hodnoty -->
|
||||
@if (matchingHandCards.Count > SelectedCards.Count)
|
||||
{
|
||||
<button class="btn btn-dark btn-combo-all" @onclick="SelectAllOfRank" title="Pridať všetky zostávajúce karty tejto hodnoty">
|
||||
<span>+ Všetky (@matchingHandCards.Count)</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- Zrušenie výberu -->
|
||||
<button class="btn btn-dark btn-combo-clear" @onclick="ClearSelection" title="Zrušiť výber">
|
||||
<span>✕</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="player-hand-fanning">
|
||||
@for (int i = 0; i < Player.Hand.Count; i++)
|
||||
{
|
||||
var card = Player.Hand[i];
|
||||
var selectedIdx = SelectedCards.IndexOf(card);
|
||||
var isSelected = selectedIdx >= 0;
|
||||
var isPlayable = IsMyTurn && (CanPlayCard(card) || (SelectedCards.Count > 0 && SelectedCards[0].Rank == card.Rank && CanPlayCards(new List<Card> { card })));
|
||||
|
||||
// Výpočet vejárovitého natočenia kariet v ruke
|
||||
int total = Player.Hand.Count;
|
||||
double mid = (total - 1) / 2.0;
|
||||
double offset = i - mid;
|
||||
double angle = total > 1 ? offset * (Math.Min(35.0 / total, 5.5)) : 0;
|
||||
double yOffset = Math.Abs(offset) * 4.0;
|
||||
double xOffset = offset * (total > 8 ? 26 : 38);
|
||||
|
||||
string fanSlotStyle = $"transform: translateX({xOffset}px) translateY({yOffset}px) rotate({angle}deg); z-index: {(isSelected ? 120 + i : i + 1)};";
|
||||
string selectedClass = isSelected ? "card-selected-combo" : "";
|
||||
|
||||
<div class="hand-card-slot" style="@fanSlotStyle">
|
||||
<CardView Card="card"
|
||||
Deck="@Deck"
|
||||
IsInteractive="@IsMyTurn"
|
||||
IsPlayable="@isPlayable"
|
||||
CustomClass="@selectedClass"
|
||||
OnCardClicked="HandleCardClick" />
|
||||
|
||||
@if (isSelected)
|
||||
{
|
||||
var orderNum = selectedIdx + 1;
|
||||
var isTopCard = (selectedIdx == SelectedCards.Count - 1 && SelectedCards.Count > 1);
|
||||
<div class="card-order-badge @(isTopCard ? "is-top-badge" : "")">
|
||||
<span class="order-number">@orderNum.</span>
|
||||
@if (isTopCard)
|
||||
{
|
||||
<span class="order-tag">👑 Vrch</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public Player Player { get; set; } = default!;
|
||||
[Parameter] public string Deck { get; set; } = "original";
|
||||
[Parameter] public bool IsMyTurn { get; set; } = false;
|
||||
[Parameter] public Func<Card, bool> CanPlayCardFunc { get; set; } = _ => false;
|
||||
[Parameter] public Func<List<Card>, bool> CanPlayCardsFunc { get; set; } = _ => false;
|
||||
[Parameter] public EventCallback<List<Card>> OnPlayCards { get; set; }
|
||||
|
||||
private List<Card> SelectedCards = new();
|
||||
|
||||
private bool CanPlayCard(Card card) => CanPlayCardFunc(card);
|
||||
private bool CanPlayCards(List<Card> cards) => CanPlayCardsFunc(cards);
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
// Vyčistíme karty, ktoré hráč už nemá v ruke
|
||||
SelectedCards.RemoveAll(c => !Player.Hand.Contains(c));
|
||||
}
|
||||
|
||||
private async Task HandleCardClick(Card card)
|
||||
{
|
||||
if (!IsMyTurn) return;
|
||||
|
||||
var rankCount = Player.Hand.Count(c => c.Rank == card.Rank);
|
||||
|
||||
// Ak už máme vybraté karty inej hodnoty, zresetujeme výber
|
||||
if (SelectedCards.Count > 0 && SelectedCards[0].Rank != card.Rank)
|
||||
{
|
||||
SelectedCards.Clear();
|
||||
}
|
||||
|
||||
// Ak má hráč v ruke len 1 kartu danej hodnoty a žiadna nie je vybratá:
|
||||
if (rankCount == 1 && SelectedCards.Count == 0)
|
||||
{
|
||||
if (CanPlayCard(card))
|
||||
{
|
||||
await OnPlayCards.InvokeAsync(new List<Card> { card });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ak je už karta v zozname vybratých -> odoberieme ju (deselect)
|
||||
if (SelectedCards.Contains(card))
|
||||
{
|
||||
SelectedCards.Remove(card);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pridáme na koniec zoznamu ako ďalšiu v poradí
|
||||
SelectedCards.Add(card);
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void ReverseSelectionOrder()
|
||||
{
|
||||
SelectedCards.Reverse();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void SelectAllOfRank()
|
||||
{
|
||||
if (SelectedCards.Count == 0) return;
|
||||
var rank = SelectedCards[0].Rank;
|
||||
foreach (var c in Player.Hand.Where(c => c.Rank == rank))
|
||||
{
|
||||
if (!SelectedCards.Contains(c))
|
||||
{
|
||||
SelectedCards.Add(c);
|
||||
}
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void ClearSelection()
|
||||
{
|
||||
SelectedCards.Clear();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task PlaySelected()
|
||||
{
|
||||
if (SelectedCards.Count > 0 && CanPlayCards(SelectedCards))
|
||||
{
|
||||
var cardsToPlay = new List<Card>(SelectedCards);
|
||||
SelectedCards.Clear();
|
||||
await OnPlayCards.InvokeAsync(cardsToPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
@namespace Faraon.Client.Components
|
||||
|
||||
<div class="player-slot @PositionClass">
|
||||
<div class="avatar-badge @(IsCurrentTurn ? "active-turn" : "") @(Player.IsSkipped ? "is-skipped" : "") @(Player.HasFinished ? "is-finished" : "")">
|
||||
@if (Player.HasFinished)
|
||||
{
|
||||
<div class="badge-finished-pos">
|
||||
@Player.FinishedPosition
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="avatar-icon">@Player.Avatar</div>
|
||||
<div class="avatar-name">
|
||||
@Player.Name
|
||||
@if (Player.Type == PlayerType.AI)
|
||||
{
|
||||
<span style="font-size: 0.7rem; opacity: 0.8;">(AI)</span>
|
||||
}
|
||||
</div>
|
||||
<div class="avatar-card-count">
|
||||
@if (Player.HasFinished)
|
||||
{
|
||||
<span>Skončil 🏆</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@Player.Hand.Count @(Player.Hand.Count == 1 ? "karta" : (Player.Hand.Count < 5 ? "karty" : "kariet"))</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mini karty pre vizuálny odpočet kariet v ruke -->
|
||||
@if (!Player.HasFinished)
|
||||
{
|
||||
<div class="opponent-mini-cards">
|
||||
@for (int i = 0; i < Math.Min(Player.Hand.Count, 8); i++)
|
||||
{
|
||||
<div class="mini-card-back"></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public Player Player { get; set; } = default!;
|
||||
[Parameter] public bool IsCurrentTurn { get; set; } = false;
|
||||
[Parameter] public string PositionClass { get; set; } = "slot-top";
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
@namespace Faraon.Client.Components
|
||||
|
||||
<div class="suit-picker-overlay">
|
||||
<div class="suit-picker-title">Zvoľte farbu (Horník)</div>
|
||||
<div class="suit-picker-grid">
|
||||
<button class="suit-btn" style="color: #d62828;" @onclick="() => SelectSuit(Suit.Hearts)">
|
||||
<div style="width: 50px; height: 50px;">
|
||||
<MariasPip Suit="Suit.Hearts" />
|
||||
</div>
|
||||
<span class="suit-btn-name">ČERVEŇ</span>
|
||||
</button>
|
||||
<button class="suit-btn" style="color: #2d6a4f;" @onclick="() => SelectSuit(Suit.Leaves)">
|
||||
<div style="width: 50px; height: 50px;">
|
||||
<MariasPip Suit="Suit.Leaves" />
|
||||
</div>
|
||||
<span class="suit-btn-name">ZELEŇ</span>
|
||||
</button>
|
||||
<button class="suit-btn" style="color: #8b5a2b;" @onclick="() => SelectSuit(Suit.Acorns)">
|
||||
<div style="width: 50px; height: 50px;">
|
||||
<MariasPip Suit="Suit.Acorns" />
|
||||
</div>
|
||||
<span class="suit-btn-name">ŽALUĎ</span>
|
||||
</button>
|
||||
<button class="suit-btn" style="color: #d48b1a;" @onclick="() => SelectSuit(Suit.Bells)">
|
||||
<div style="width: 50px; height: 50px;">
|
||||
<MariasPip Suit="Suit.Bells" />
|
||||
</div>
|
||||
<span class="suit-btn-name">GUĽA</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public EventCallback<Suit> OnSuitSelected { get; set; }
|
||||
|
||||
private async Task SelectSuit(Suit suit)
|
||||
{
|
||||
await OnSuitSelected.InvokeAsync(suit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.11" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.11" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Faraon.Core\Faraon.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<main>
|
||||
@Body
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
@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">
|
||||
<h1 class="game-title">FARAÓN</h1>
|
||||
<div style="font-size: 0.95rem; font-weight: 700; color: #a3b899;">
|
||||
Ťah #@Engine.State.TurnCount • Na rade:
|
||||
<span style="color: var(--gold); font-weight: 900;">
|
||||
@Engine.State.CurrentPlayer.Avatar @Engine.State.CurrentPlayer.Name
|
||||
</span>
|
||||
</div>
|
||||
<div class="game-controls">
|
||||
<!-- Rýchly prepínač balíčka kariet -->
|
||||
<button class="btn-game-icon" title="Zmeniť balíček kariet (Original / SpongeBob)" @onclick="ToggleDeck">
|
||||
@(Engine.State.Rules.SelectedDeck == "spongebob" ? "🧽" : "🎴")
|
||||
</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())
|
||||
{
|
||||
<PlayerSlot Player="@opp.Player"
|
||||
IsCurrentTurn="@(Engine.State.CurrentPlayer == opp.Player)"
|
||||
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 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>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spodný slot: Aktívny hráč v zobrazení -->
|
||||
<div class="slot-bottom">
|
||||
<div class="avatar-badge @(IsViewingPlayerTurn ? "active-turn" : "") @(ViewingPlayer.IsSkipped ? "is-skipped" : "")"
|
||||
style="margin-bottom: 8px;">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span class="avatar-icon">@ViewingPlayer.Avatar</span>
|
||||
<span class="avatar-name">@ViewingPlayer.Name</span>
|
||||
<span class="avatar-card-count">(@ViewingPlayer.Hand.Count kariet)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interaktívna ruka hráča -->
|
||||
<PlayerHandView Player="@ViewingPlayer"
|
||||
Deck="@Engine.State.Rules.SelectedDeck"
|
||||
IsMyTurn="@IsViewingPlayerTurn"
|
||||
CanPlayCardFunc="@(c => Engine.CanPlayCard(ViewingPlayer, c))"
|
||||
CanPlayCardsFunc="@(cards => Engine.CanPlayCards(ViewingPlayer, cards))"
|
||||
OnPlayCards="HandlePlayCards" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Akčné tlačidlá pre aktívneho živého hráča -->
|
||||
@if (IsViewingPlayerTurn && Engine.State.TurnState == TurnState.Playing)
|
||||
{
|
||||
<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.IsGameOver)
|
||||
{
|
||||
<GameOverModal Players="@Engine.State.Players"
|
||||
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 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 hráča v hre
|
||||
var firstHuman = Engine.State.Players.FirstOrDefault(p => p.Type == PlayerType.Human);
|
||||
return firstHuman ?? Engine.State.CurrentPlayer;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsViewingPlayerTurn =>
|
||||
Engine != null &&
|
||||
Engine.State.TurnState == TurnState.Playing &&
|
||||
Engine.State.CurrentPlayer == ViewingPlayer;
|
||||
|
||||
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);
|
||||
|
||||
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 / 2));
|
||||
break;
|
||||
case GameEventType.AceSkipped:
|
||||
case GameEventType.AceCountered:
|
||||
await Fx.PlayAceFreezeAsync();
|
||||
break;
|
||||
case GameEventType.SuitChanged:
|
||||
await Fx.PlayHornikChimeAsync();
|
||||
break;
|
||||
case GameEventType.CardsBurned:
|
||||
await Fx.PlayBurnExplosionAsync();
|
||||
break;
|
||||
case GameEventType.PlayerFinished:
|
||||
case GameEventType.GameOver:
|
||||
await Fx.PlayVictoryAsync();
|
||||
break;
|
||||
}
|
||||
|
||||
if (Engine != null && Engine.State.CurrentPlayer.Hand.Count == 1 && !Engine.State.CurrentPlayer.HasFinished)
|
||||
{
|
||||
await Fx.PlayHeartbeatAsync();
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
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 void ToggleDeck()
|
||||
{
|
||||
if (Engine == null) return;
|
||||
Engine.State.Rules.SelectedDeck = Engine.State.Rules.SelectedDeck == "spongebob" ? "original" : "spongebob";
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task HandlePlayCards(List<Card> cards)
|
||||
{
|
||||
if (Engine == null || cards.Count == 0) return;
|
||||
string pos = GetPlayerPosition(ViewingPlayer);
|
||||
string deck = Engine.State.Rules.SelectedDeck;
|
||||
|
||||
foreach (var card in cards)
|
||||
{
|
||||
_ = Fx.AnimateCardPlayAsync(pos, card, deck);
|
||||
if (cards.Count > 1) await Task.Delay(90);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
for (int i = 0; i < Math.Min(Engine.State.PendingDrawCount, 4); i++)
|
||||
{
|
||||
_ = Fx.AnimateCardDrawAsync(pos, deck);
|
||||
await Task.Delay(90);
|
||||
}
|
||||
}
|
||||
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 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;
|
||||
foreach (var c in cards)
|
||||
{
|
||||
_ = Fx.AnimateCardPlayAsync(pos, c, deck);
|
||||
await Task.Delay(75);
|
||||
}
|
||||
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)
|
||||
{
|
||||
foreach (var c in cardsToPlay)
|
||||
{
|
||||
_ = Fx.AnimateCardPlayAsync(pos, c, deck);
|
||||
if (cardsToPlay.Count > 1) await Task.Delay(85);
|
||||
}
|
||||
Engine.PlayCards(aiPlayer, cardsToPlay, decision.ChosenSuit);
|
||||
}
|
||||
}
|
||||
else if (decision.Type == AIDecisionType.BurnCards && decision.Cards != null)
|
||||
{
|
||||
foreach (var c in decision.Cards)
|
||||
{
|
||||
_ = Fx.AnimateCardPlayAsync(pos, c, deck);
|
||||
await Task.Delay(75);
|
||||
}
|
||||
Engine.BurnCards(aiPlayer, decision.Cards);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Engine.State.PendingDrawCount > 0)
|
||||
{
|
||||
for (int i = 0; i < Math.Min(Engine.State.PendingDrawCount, 4); i++)
|
||||
{
|
||||
_ = Fx.AnimateCardDrawAsync(pos, deck);
|
||||
await Task.Delay(90);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@page "/not-found"
|
||||
@layout MainLayout
|
||||
|
||||
<h3>Not Found</h3>
|
||||
<p>Sorry, the content you are looking for does not exist.</p>
|
||||
@@ -0,0 +1,15 @@
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Faraon.Client;
|
||||
using Faraon.Client.Services;
|
||||
using Faraon.Core.AI;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
builder.Services.AddScoped<FxService>();
|
||||
builder.Services.AddScoped<IFaraonAI, FaraonAI>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "http://localhost:5146",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "https://localhost:7244;http://localhost:5146",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
namespace Faraon.Client.Services;
|
||||
|
||||
using Microsoft.JSInterop;
|
||||
using Faraon.Core.Models;
|
||||
|
||||
public class FxService
|
||||
|
||||
{
|
||||
private readonly IJSRuntime _js;
|
||||
|
||||
public FxService(IJSRuntime js)
|
||||
{
|
||||
_js = js;
|
||||
}
|
||||
|
||||
public async ValueTask InitParticlesAsync(string canvasId = "particle-canvas")
|
||||
{
|
||||
try { await _js.InvokeVoidAsync("FaraonFx.initParticles", canvasId); } catch { }
|
||||
}
|
||||
|
||||
public async ValueTask PlayCardSnapAsync()
|
||||
{
|
||||
try { await _js.InvokeVoidAsync("FaraonFx.playCardSnap"); } catch { }
|
||||
}
|
||||
|
||||
public async ValueTask PlayCardDrawAsync()
|
||||
{
|
||||
try { await _js.InvokeVoidAsync("FaraonFx.playCardDraw"); } catch { }
|
||||
}
|
||||
|
||||
public async ValueTask PlaySevenStackAsync(int level)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _js.InvokeVoidAsync("FaraonFx.playSevenStack", level);
|
||||
await _js.InvokeVoidAsync("FaraonFx.triggerFireParticles", null, null, 25 * level);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public async ValueTask PlayAceFreezeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _js.InvokeVoidAsync("FaraonFx.playAceFreeze");
|
||||
await _js.InvokeVoidAsync("FaraonFx.triggerIceParticles", null, null, 35);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public async ValueTask PlayHornikChimeAsync()
|
||||
{
|
||||
try { await _js.InvokeVoidAsync("FaraonFx.playHornikChime"); } catch { }
|
||||
}
|
||||
|
||||
public async ValueTask PlayBurnExplosionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _js.InvokeVoidAsync("FaraonFx.playBurnExplosion");
|
||||
await _js.InvokeVoidAsync("FaraonFx.triggerBurnExplosion", null, null);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public async ValueTask PlayVictoryAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _js.InvokeVoidAsync("FaraonFx.playVictory");
|
||||
await _js.InvokeVoidAsync("FaraonFx.triggerConfetti");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public async ValueTask PlayHeartbeatAsync()
|
||||
{
|
||||
try { await _js.InvokeVoidAsync("FaraonFx.playHeartbeat"); } catch { }
|
||||
}
|
||||
|
||||
public async ValueTask TriggerScreenShakeAsync(int intensity = 5)
|
||||
{
|
||||
try { await _js.InvokeVoidAsync("FaraonFx.triggerScreenShake", intensity); } catch { }
|
||||
}
|
||||
|
||||
public async ValueTask AnimateCardPlayAsync(string sourcePosition, Card card, string deck = "original")
|
||||
{
|
||||
try
|
||||
{
|
||||
var cardInfo = new
|
||||
{
|
||||
deck = deck,
|
||||
suit = card.Suit.ToString(),
|
||||
rank = card.Rank.ToString(),
|
||||
shortName = card.ShortName,
|
||||
suitSymbol = card.SuitSymbol,
|
||||
suitColor = card.SuitColor,
|
||||
rankName = card.RankName,
|
||||
suitName = card.SuitName,
|
||||
isHornik = card.IsHornik,
|
||||
isRedSeven = card.IsRedSeven
|
||||
};
|
||||
await _js.InvokeVoidAsync("FaraonFx.animateCardPlay", sourcePosition, cardInfo);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public async ValueTask AnimateCardDrawAsync(string targetPosition, string deck = "original")
|
||||
{
|
||||
try
|
||||
{
|
||||
await _js.InvokeVoidAsync("FaraonFx.animateCardDraw", targetPosition, deck);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public async ValueTask<bool> ToggleMuteAsync()
|
||||
{
|
||||
try { return await _js.InvokeAsync<bool>("FaraonFx.toggleMute"); } catch { return false; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using Faraon.Client
|
||||
@using Faraon.Client.Layout
|
||||
@using Faraon.Client.Components
|
||||
@using Faraon.Client.Services
|
||||
@using Faraon.Core.Models
|
||||
@using Faraon.Core.Engine
|
||||
@using Faraon.Core.AI
|
||||
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 26 KiB |