improvements
This commit is contained in:
@@ -63,16 +63,40 @@
|
|||||||
<div class="rules-toggle-grid">
|
<div class="rules-toggle-grid">
|
||||||
<label class="rule-check-item">
|
<label class="rule-check-item">
|
||||||
<input type="checkbox" @bind="Rules.StackSevens" />
|
<input type="checkbox" @bind="Rules.StackSevens" />
|
||||||
<span>Kumulácia sediem (+2, +4, +6...)</span>
|
<span>Kumulácia sediem (+3, +6, +9...)</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="rule-check-item">
|
<label class="rule-check-item">
|
||||||
<input type="checkbox" @bind="Rules.CounterAceWithAce" />
|
<input type="checkbox" @bind="Rules.CounterAceWithAce" />
|
||||||
<span>Prebíjanie esa esom</span>
|
<span>Prebíjanie esa esom</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="rule-check-item">
|
<div style="grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; background: rgba(255, 23, 68, 0.08); padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(255, 23, 68, 0.25); gap: 12px;">
|
||||||
<input type="checkbox" @bind="Rules.RedSevenRevives" />
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
<span>Červená sedma oživuje víťaza</span>
|
<span style="font-size: 1.1rem;">❤️</span>
|
||||||
</label>
|
<div>
|
||||||
|
<div style="font-weight: 700; color: #ff5252;">Červená sedma (Oživenie / Resurrect)</div>
|
||||||
|
<div style="font-size: 0.76rem; color: #94a3b8;">Spôsob vrátenia skončeného hráča späť do hry</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<select class="player-setup-select" style="width: auto; min-width: 240px; padding: 4px 10px; font-weight: 600;" @bind="Rules.RedSevenMode">
|
||||||
|
<option value="@RedSevenMode.AnyCardOneRound">⚡ Voľná na čokoľvek (do 1 kola)</option>
|
||||||
|
<option value="@RedSevenMode.ColorOrSevenThreeRounds">🎴 Klasická na Srdce/7 (do 3 kôl)</option>
|
||||||
|
<option value="@RedSevenMode.Disabled">❌ Vypnuté (Bez oživenia)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; background: rgba(45, 106, 79, 0.12); padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(45, 106, 79, 0.35); gap: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<span style="font-size: 1.1rem;">🍃</span>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 700; color: #52b788;">Zelený dolník (Rušenie sediem)</div>
|
||||||
|
<div style="font-size: 0.76rem; color: #94a3b8;">Zelený dolník zruší trest zo sedmy & pravidlá vykladania</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<select class="player-setup-select" style="width: auto; min-width: 240px; padding: 4px 10px; font-weight: 600;" @bind="Rules.GreenUnderKnaveMode">
|
||||||
|
<option value="@GreenUnderKnaveMode.OnlyOnSevenOrUnderKnave">🌿 Ruší sedmy (Zeleň / Dolník)</option>
|
||||||
|
<option value="@GreenUnderKnaveMode.UniversalWildcard">✨ Voľný na čokoľvek + čokoľvek naňho</option>
|
||||||
|
<option value="@GreenUnderKnaveMode.Disabled">❌ Vypnuté (Bez rušenia sediem)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<label class="rule-check-item">
|
<label class="rule-check-item">
|
||||||
<input type="checkbox" @bind="Rules.FourOfAKindBurns" />
|
<input type="checkbox" @bind="Rules.FourOfAKindBurns" />
|
||||||
<span>Spálenie (4 rovnaké karty)</span>
|
<span>Spálenie (4 rovnaké karty)</span>
|
||||||
@@ -90,6 +114,16 @@
|
|||||||
<option value="6">6 kariet</option>
|
<option value="6">6 kariet</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; font-size: 0.9rem;">
|
||||||
|
<span>Rýchlosť hry:</span>
|
||||||
|
<select class="player-setup-select" style="width: auto; padding: 2px 8px;" @bind="Rules.SpeedLevel">
|
||||||
|
<option value="1">🐢 0.5x (Pomalá)</option>
|
||||||
|
<option value="2">🚶 0.75x (Pokojná)</option>
|
||||||
|
<option value="3">⚡ 1x (Normálna - štandard)</option>
|
||||||
|
<option value="4">🔥 1.5x (Rýchla)</option>
|
||||||
|
<option value="5">🚀 2.5x (Turbo / Blesková)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Výber balíčka kariet (Deck Theme) -->
|
<!-- Výber balíčka kariet (Deck Theme) -->
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
@namespace Faraon.Client.Components
|
@namespace Faraon.Client.Components
|
||||||
|
|
||||||
<div class="player-slot @PositionClass @(IsCurrentTurn ? "active-turn-slot" : "")">
|
<div class="player-slot @PositionClass @(IsFrozen ? "frozen-slot" : (IsCurrentTurn ? "active-turn-slot" : ""))">
|
||||||
<div class="avatar-badge @(IsCurrentTurn ? "active-turn" : "") @(Player.IsSkipped ? "is-skipped" : "") @(Player.HasFinished ? "is-finished" : "")">
|
<div class="avatar-badge @(IsFrozen ? "is-frozen" : (IsCurrentTurn ? "active-turn" : "")) @(Player.HasFinished ? "is-finished" : "") @(CanBeRevived && Player.HasFinished && ReviveTurnsRemaining > 0 ? "can-be-revived" : "")">
|
||||||
@if (IsCurrentTurn && !Player.HasFinished && !Player.IsEliminated)
|
@if (IsFrozen && !Player.HasFinished && !Player.IsEliminated)
|
||||||
|
{
|
||||||
|
<div class="frozen-indicator-badge">
|
||||||
|
<span class="frozen-indicator-dot"></span>
|
||||||
|
<span class="frozen-indicator-text">❄️ ZMRAZENÝ</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (IsCurrentTurn && !Player.HasFinished && !Player.IsEliminated)
|
||||||
{
|
{
|
||||||
<div class="turn-indicator-badge">
|
<div class="turn-indicator-badge">
|
||||||
<span class="turn-indicator-dot"></span>
|
<span class="turn-indicator-dot"></span>
|
||||||
@@ -45,6 +52,29 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (IsFrozen && !Player.HasFinished && !Player.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 (CanBeRevived && Player.HasFinished && ReviveTurnsRemaining > 0)
|
||||||
|
{
|
||||||
|
<div class="revive-countdown-badge" title="Tento hráč môže byť vrátený do hry Červenou sedmou!">
|
||||||
|
<span class="heartbeat-icon">❤️</span>
|
||||||
|
<span>
|
||||||
|
@(ReviveRoundsRemaining > 1 ? $"Oživenie: {ReviveRoundsRemaining} kolá" : (ReviveTurnsRemaining <= 1 ? "Oživenie: 1 ťah" : "Oživenie: 1 kolo"))
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mini karty pre vizuálny odpočet kariet v ruke -->
|
<!-- Mini karty pre vizuálny odpočet kariet v ruke -->
|
||||||
@@ -62,6 +92,10 @@
|
|||||||
@code {
|
@code {
|
||||||
[Parameter, EditorRequired] public Player Player { get; set; } = default!;
|
[Parameter, EditorRequired] public Player Player { get; set; } = default!;
|
||||||
[Parameter] public bool IsCurrentTurn { get; set; } = false;
|
[Parameter] public bool IsCurrentTurn { get; set; } = false;
|
||||||
|
[Parameter] public bool IsFrozen { get; set; } = false;
|
||||||
|
[Parameter] public bool CanBeRevived { get; set; } = false;
|
||||||
|
[Parameter] public int ReviveRoundsRemaining { get; set; } = 0;
|
||||||
|
[Parameter] public int ReviveTurnsRemaining { get; set; } = 0;
|
||||||
[Parameter] public bool IsOlympicMode { get; set; } = false;
|
[Parameter] public bool IsOlympicMode { get; set; } = false;
|
||||||
[Parameter] public string PositionClass { get; set; } = "slot-top";
|
[Parameter] public string PositionClass { get; set; } = "slot-top";
|
||||||
}
|
}
|
||||||
|
|||||||
+213
-27
@@ -31,6 +31,10 @@ else
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="game-controls">
|
<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 -->
|
<!-- Rýchly prepínač balíčka kariet -->
|
||||||
<button class="btn-game-icon" title="Zmeniť balíček kariet (@Engine.State.Rules.SelectedDeck)" @onclick="ToggleDeck">
|
<button class="btn-game-icon" title="Zmeniť balíček kariet (@Engine.State.Rules.SelectedDeck)" @onclick="ToggleDeck">
|
||||||
@GetDeckIcon(Engine.State.Rules.SelectedDeck)
|
@GetDeckIcon(Engine.State.Rules.SelectedDeck)
|
||||||
@@ -49,8 +53,14 @@ else
|
|||||||
<!-- Súperi na stole -->
|
<!-- Súperi na stole -->
|
||||||
@foreach (var opp in GetOpponents())
|
@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"
|
<PlayerSlot Player="@opp.Player"
|
||||||
IsCurrentTurn="@(Engine.State.CurrentPlayer == 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"
|
IsOlympicMode="@Engine.State.Rules.OlympicMode"
|
||||||
PositionClass="@opp.PositionClass" />
|
PositionClass="@opp.PositionClass" />
|
||||||
}
|
}
|
||||||
@@ -74,6 +84,14 @@ else
|
|||||||
CustomStyle="box-shadow: 0 10px 25px rgba(0,0,0,0.7);" />
|
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 -->
|
<!-- Indikátor trestu zo sedmy -->
|
||||||
@if (Engine.State.PendingDrawCount > 0)
|
@if (Engine.State.PendingDrawCount > 0)
|
||||||
{
|
{
|
||||||
@@ -94,23 +112,56 @@ else
|
|||||||
<span>@dummy.SuitName</span>
|
<span>@dummy.SuitName</span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Spodný slot: Aktívny hráč v zobrazení -->
|
<!-- Spodný slot: Aktívny hráč v zobrazení -->
|
||||||
<div class="slot-bottom @(IsViewingPlayerTurn ? "active-turn-slot" : "")">
|
@{
|
||||||
<div class="avatar-badge @(IsViewingPlayerTurn ? "active-turn" : "") @(ViewingPlayer.IsSkipped ? "is-skipped" : "")"
|
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;">
|
style="margin-bottom: 8px;">
|
||||||
@if (IsViewingPlayerTurn && !ViewingPlayer.HasFinished && !ViewingPlayer.IsEliminated)
|
@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">
|
<div class="turn-indicator-badge">
|
||||||
<span class="turn-indicator-dot"></span>
|
<span class="turn-indicator-dot"></span>
|
||||||
<span class="turn-indicator-text">SI NA ŤAHU</span>
|
<span class="turn-indicator-text">@(ViewingPlayer.Type == PlayerType.Human ? "SI NA ŤAHU" : "NA ŤAHU")</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (ViewingPlayer.HasFinished)
|
||||||
|
{
|
||||||
|
<div class="badge-finished-pos">
|
||||||
|
@ViewingPlayer.FinishedPosition
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
<span class="avatar-icon">@ViewingPlayer.Avatar</span>
|
<span class="avatar-icon">@ViewingPlayer.Avatar</span>
|
||||||
<span class="avatar-name">@ViewingPlayer.Name</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>
|
<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)
|
@if (Engine.State.Rules.OlympicMode)
|
||||||
{
|
{
|
||||||
@@ -119,6 +170,29 @@ else
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Interaktívna ruka hráča -->
|
<!-- Interaktívna ruka hráča -->
|
||||||
@@ -232,17 +306,24 @@ else
|
|||||||
return Engine.State.CurrentPlayer;
|
return Engine.State.CurrentPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inak primárne prvého živého hráča v hre
|
// Inak primárne prvého živého človeka v hre
|
||||||
var firstHuman = Engine.State.Players.FirstOrDefault(p => p.Type == PlayerType.Human);
|
var firstHuman = Engine.State.Players.FirstOrDefault(p => p.Type == PlayerType.Human);
|
||||||
return firstHuman ?? Engine.State.CurrentPlayer;
|
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 IsViewingPlayerTurn =>
|
private bool IsViewingPlayerActiveTurn =>
|
||||||
Engine != null &&
|
Engine != null &&
|
||||||
Engine.State.TurnState == TurnState.Playing &&
|
Engine.State.TurnState == TurnState.Playing &&
|
||||||
Engine.State.CurrentPlayer == ViewingPlayer;
|
Engine.State.CurrentPlayer == ViewingPlayer;
|
||||||
|
|
||||||
|
private bool IsViewingPlayerTurn =>
|
||||||
|
IsViewingPlayerActiveTurn &&
|
||||||
|
ViewingPlayer.Type == PlayerType.Human;
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
@@ -269,6 +350,7 @@ else
|
|||||||
Engine.StartGame(gamePlayers, ConfiguredRules);
|
Engine.StartGame(gamePlayers, ConfiguredRules);
|
||||||
|
|
||||||
_previousTurnPlayer = Engine.State.CurrentPlayer;
|
_previousTurnPlayer = Engine.State.CurrentPlayer;
|
||||||
|
_ = Fx.SetSpeedLevelAsync(Engine.State.Rules.SpeedLevel);
|
||||||
IsInLobby = false;
|
IsInLobby = false;
|
||||||
LastEventMessage = "Hra začala! Veľa šťastia.";
|
LastEventMessage = "Hra začala! Veľa šťastia.";
|
||||||
|
|
||||||
@@ -294,9 +376,29 @@ else
|
|||||||
await Fx.PlaySevenStackAsync(Math.Max(1, e.ExtraData / 3));
|
await Fx.PlaySevenStackAsync(Math.Max(1, e.ExtraData / 3));
|
||||||
await Fx.TriggerDopamineBannerAsync($"🔥 +{e.ExtraData} KARIET!", "Trestná reťaz sediem", "triple");
|
await Fx.TriggerDopamineBannerAsync($"🔥 +{e.ExtraData} KARIET!", "Trestná reťaz sediem", "triple");
|
||||||
break;
|
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:
|
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:
|
case GameEventType.AceCountered:
|
||||||
await Fx.TriggerFreezeEffectAsync();
|
if (e.SourcePlayer != null)
|
||||||
|
{
|
||||||
|
await Fx.TriggerIceShatterAsync(GetPlayerPosition(e.SourcePlayer));
|
||||||
|
await Fx.TriggerDopamineBannerAsync("⚡ ROZMRAZENÝ!", $"{e.SourcePlayer.Name} prebil Eso vlastným Esom!", "revive");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case GameEventType.PlayerRevived:
|
case GameEventType.PlayerRevived:
|
||||||
await Fx.TriggerReviveEffectAsync(e.TargetPlayer?.Name ?? e.SourcePlayer?.Name ?? "Hráč");
|
await Fx.TriggerReviveEffectAsync(e.TargetPlayer?.Name ?? e.SourcePlayer?.Name ?? "Hráč");
|
||||||
@@ -353,6 +455,44 @@ else
|
|||||||
|
|
||||||
private static readonly string[] AvailableDecks = new[] { "original", "janosik", "spongebob", "cyberpunk", "fantasy", "egypt", "pets", "pixel" };
|
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()
|
private void ToggleDeck()
|
||||||
{
|
{
|
||||||
if (Engine == null) return;
|
if (Engine == null) return;
|
||||||
@@ -393,10 +533,17 @@ else
|
|||||||
_ = Fx.TriggerComboSlamAsync(4, "👑 KVADRA / MEGA COMBO!");
|
_ = Fx.TriggerComboSlamAsync(4, "👑 KVADRA / MEGA COMBO!");
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var card in cards)
|
for (int i = 0; i < cards.Count; i++)
|
||||||
{
|
{
|
||||||
_ = Fx.AnimateCardPlayAsync(pos, card, deck);
|
if (i == cards.Count - 1)
|
||||||
if (cards.Count > 1) await Task.Delay(90);
|
{
|
||||||
|
await Fx.AnimateCardPlayAsync(pos, cards[i], deck);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_ = Fx.AnimateCardPlayAsync(pos, cards[i], deck);
|
||||||
|
await Task.Delay(MultiCardDelayMs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Engine.PlayCards(ViewingPlayer, cards);
|
Engine.PlayCards(ViewingPlayer, cards);
|
||||||
@@ -412,10 +559,19 @@ else
|
|||||||
|
|
||||||
if (Engine.State.PendingDrawCount > 0)
|
if (Engine.State.PendingDrawCount > 0)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < Math.Min(Engine.State.PendingDrawCount, 4); i++)
|
int drawCount = Engine.State.PendingDrawCount;
|
||||||
|
int staggerDelay = Math.Max(25, (int)(MultiCardDelayMs * 0.75));
|
||||||
|
for (int i = 0; i < drawCount; i++)
|
||||||
{
|
{
|
||||||
_ = Fx.AnimateCardDrawAsync(pos, deck);
|
if (i == drawCount - 1)
|
||||||
await Task.Delay(90);
|
{
|
||||||
|
await Fx.AnimateCardDrawAsync(pos, deck);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_ = Fx.AnimateCardDrawAsync(pos, deck);
|
||||||
|
await Task.Delay(staggerDelay);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!Engine.State.PendingSkip)
|
else if (!Engine.State.PendingSkip)
|
||||||
@@ -460,10 +616,17 @@ else
|
|||||||
{
|
{
|
||||||
string pos = GetPlayerPosition(ViewingPlayer);
|
string pos = GetPlayerPosition(ViewingPlayer);
|
||||||
string deck = Engine.State.Rules.SelectedDeck;
|
string deck = Engine.State.Rules.SelectedDeck;
|
||||||
foreach (var c in cards)
|
for (int i = 0; i < cards.Count; i++)
|
||||||
{
|
{
|
||||||
_ = Fx.AnimateCardPlayAsync(pos, c, deck);
|
if (i == cards.Count - 1)
|
||||||
await Task.Delay(75);
|
{
|
||||||
|
await Fx.AnimateCardPlayAsync(pos, cards[i], deck);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_ = Fx.AnimateCardPlayAsync(pos, cards[i], deck);
|
||||||
|
await Task.Delay(MultiCardDelayMs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Engine.BurnCards(ViewingPlayer, cards);
|
Engine.BurnCards(ViewingPlayer, cards);
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
@@ -523,20 +686,34 @@ else
|
|||||||
_ = Fx.TriggerComboSlamAsync(4, $"👑 {aiPlayer.Name}: KVADRA!");
|
_ = Fx.TriggerComboSlamAsync(4, $"👑 {aiPlayer.Name}: KVADRA!");
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var c in cardsToPlay)
|
for (int i = 0; i < cardsToPlay.Count; i++)
|
||||||
{
|
{
|
||||||
_ = Fx.AnimateCardPlayAsync(pos, c, deck);
|
if (i == cardsToPlay.Count - 1)
|
||||||
if (cardsToPlay.Count > 1) await Task.Delay(85);
|
{
|
||||||
|
await Fx.AnimateCardPlayAsync(pos, cardsToPlay[i], deck);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_ = Fx.AnimateCardPlayAsync(pos, cardsToPlay[i], deck);
|
||||||
|
await Task.Delay(MultiCardDelayMs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Engine.PlayCards(aiPlayer, cardsToPlay, decision.ChosenSuit);
|
Engine.PlayCards(aiPlayer, cardsToPlay, decision.ChosenSuit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (decision.Type == AIDecisionType.BurnCards && decision.Cards != null)
|
else if (decision.Type == AIDecisionType.BurnCards && decision.Cards != null)
|
||||||
{
|
{
|
||||||
foreach (var c in decision.Cards)
|
for (int i = 0; i < decision.Cards.Count; i++)
|
||||||
{
|
{
|
||||||
_ = Fx.AnimateCardPlayAsync(pos, c, deck);
|
if (i == decision.Cards.Count - 1)
|
||||||
await Task.Delay(75);
|
{
|
||||||
|
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);
|
Engine.BurnCards(aiPlayer, decision.Cards);
|
||||||
}
|
}
|
||||||
@@ -544,10 +721,19 @@ else
|
|||||||
{
|
{
|
||||||
if (Engine.State.PendingDrawCount > 0)
|
if (Engine.State.PendingDrawCount > 0)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < Math.Min(Engine.State.PendingDrawCount, 4); i++)
|
int drawCount = Engine.State.PendingDrawCount;
|
||||||
|
int staggerDelay = Math.Max(25, (int)(MultiCardDelayMs * 0.75));
|
||||||
|
for (int i = 0; i < drawCount; i++)
|
||||||
{
|
{
|
||||||
_ = Fx.AnimateCardDrawAsync(pos, deck);
|
if (i == drawCount - 1)
|
||||||
await Task.Delay(90);
|
{
|
||||||
|
await Fx.AnimateCardDrawAsync(pos, deck);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_ = Fx.AnimateCardDrawAsync(pos, deck);
|
||||||
|
await Task.Delay(staggerDelay);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!Engine.State.PendingSkip)
|
else if (!Engine.State.PendingSkip)
|
||||||
|
|||||||
@@ -48,11 +48,30 @@ public class FxService
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask TriggerIceShatterAsync(string position)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _js.InvokeVoidAsync("FaraonFx.triggerIceShatter", position);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask PlayHornikChimeAsync()
|
public async ValueTask PlayHornikChimeAsync()
|
||||||
{
|
{
|
||||||
try { await _js.InvokeVoidAsync("FaraonFx.playHornikChime"); } catch { }
|
try { await _js.InvokeVoidAsync("FaraonFx.playHornikChime"); } catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask PlayLeafShieldAsync(string? position = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _js.InvokeVoidAsync("FaraonFx.playLeafShield");
|
||||||
|
await _js.InvokeVoidAsync("FaraonFx.triggerLeafParticles", null, null, 40);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask PlayBurnExplosionAsync()
|
public async ValueTask PlayBurnExplosionAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -123,6 +142,15 @@ public class FxService
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask SetSpeedLevelAsync(int level)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _js.InvokeVoidAsync("FaraonFx.setSpeedLevel", level);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask PlaySelectStepAsync(int step)
|
public async ValueTask PlaySelectStepAsync(int step)
|
||||||
{
|
{
|
||||||
try { await _js.InvokeVoidAsync("FaraonFx.playSelectStep", step); } catch { }
|
try { await _js.InvokeVoidAsync("FaraonFx.playSelectStep", step); } catch { }
|
||||||
|
|||||||
@@ -156,6 +156,26 @@ body, html {
|
|||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-speed-control {
|
||||||
|
width: auto !important;
|
||||||
|
min-width: 60px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 20px !important;
|
||||||
|
font-size: 0.8rem !important;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
background: rgba(245, 176, 65, 0.15) !important;
|
||||||
|
border-color: rgba(245, 176, 65, 0.45) !important;
|
||||||
|
color: var(--gold) !important;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-speed-control:hover {
|
||||||
|
background: rgba(245, 176, 65, 0.35) !important;
|
||||||
|
border-color: var(--gold) !important;
|
||||||
|
box-shadow: 0 0 10px rgba(245, 176, 65, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
/* TABLE FELT & SLOTS */
|
/* TABLE FELT & SLOTS */
|
||||||
.poker-table-surface {
|
.poker-table-surface {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -405,14 +425,229 @@ body, html {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* FROZEN / ICE STATUS STYLES */
|
||||||
|
.avatar-badge.is-frozen,
|
||||||
.avatar-badge.is-skipped {
|
.avatar-badge.is-skipped {
|
||||||
border-color: var(--ice-blue);
|
background: linear-gradient(135deg, rgba(0, 229, 255, 0.45) 0%, rgba(255, 255, 255, 0.25) 45%, rgba(0, 150, 255, 0.4) 100%) !important;
|
||||||
box-shadow: 0 0 15px var(--ice-blue);
|
border-color: #00e5ff !important;
|
||||||
|
box-shadow: 0 0 26px rgba(0, 229, 255, 0.95), inset 0 0 16px rgba(255, 255, 255, 0.7) !important;
|
||||||
|
backdrop-filter: blur(8px) brightness(1.25) !important;
|
||||||
|
transform: scale(1.08);
|
||||||
|
overflow: visible;
|
||||||
|
animation: freeze-pulse 2s infinite ease-in-out alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes freeze-pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 18px rgba(0, 229, 255, 0.75), inset 0 0 12px rgba(255, 255, 255, 0.45);
|
||||||
|
border-color: #00e5ff;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 35px rgba(0, 229, 255, 1), 0 0 55px rgba(0, 229, 255, 0.5), inset 0 0 20px rgba(255, 255, 255, 0.85);
|
||||||
|
border-color: #e0ffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FROZEN ICE AURA (SLOT) */
|
||||||
|
.player-slot.frozen-slot::before,
|
||||||
|
.slot-bottom.frozen-slot::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 150%;
|
||||||
|
height: 150%;
|
||||||
|
background: radial-gradient(ellipse at center, rgba(0, 229, 255, 0.38) 0%, rgba(0, 229, 255, 0.12) 50%, transparent 75%) !important;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: ice-mist 2.4s infinite ease-in-out alternate !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ice-mist {
|
||||||
|
0% { transform: translate(-50%, -50%) scale(0.92); opacity: 0.7; }
|
||||||
|
100% { transform: translate(-50%, -50%) scale(1.18); opacity: 1; filter: drop-shadow(0 0 18px rgba(0, 229, 255, 0.6)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FROZEN BADGE (ZMRAZENÝ) */
|
||||||
|
.frozen-indicator-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -14px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
background: linear-gradient(135deg, #00e5ff, #80d8ff, #ffffff);
|
||||||
|
color: #003040;
|
||||||
|
font-size: 0.66rem;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
padding: 2px 9px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.6), 0 0 18px rgba(0, 229, 255, 0.9);
|
||||||
|
border: 1.5px solid #ffffff;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 25;
|
||||||
|
animation: frozen-badge-float 2s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes frozen-badge-float {
|
||||||
|
0% { transform: translateX(-50%) translateY(0); }
|
||||||
|
100% { transform: translateX(-50%) translateY(-3px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.frozen-indicator-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #00b0ff;
|
||||||
|
box-shadow: 0 0 6px #00e5ff;
|
||||||
|
animation: dot-blink 0.8s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ICICLES HANGING UNDER FROZEN BADGE */
|
||||||
|
.icicles-row {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -10px;
|
||||||
|
left: 4px;
|
||||||
|
right: 4px;
|
||||||
|
height: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icicle {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 3.5px solid transparent;
|
||||||
|
border-right: 3.5px solid transparent;
|
||||||
|
border-top: 9px solid rgba(224, 247, 250, 0.92);
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(0, 229, 255, 0.85));
|
||||||
|
}
|
||||||
|
|
||||||
|
.icicle:nth-child(even) {
|
||||||
|
border-top-width: 13px;
|
||||||
|
border-left-width: 4.5px;
|
||||||
|
border-right-width: 4.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icicle:nth-child(3n) {
|
||||||
|
border-top-width: 7px;
|
||||||
|
border-left-width: 2.5px;
|
||||||
|
border-right-width: 2.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DISCARD PILE ACE FREEZE BADGE */
|
||||||
|
.ace-freeze-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -18px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: linear-gradient(135deg, #00e5ff, #0091ea, #00b0ff);
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
padding: 3px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 0 22px rgba(0, 229, 255, 0.95), 0 4px 12px rgba(0, 0, 0, 0.7);
|
||||||
|
border: 2px solid #ffffff;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 25;
|
||||||
|
animation: ace-freeze-pulse 1.6s infinite alternate;
|
||||||
|
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ace-freeze-pulse {
|
||||||
|
0% { transform: translateX(-50%) scale(0.95); box-shadow: 0 0 16px rgba(0, 229, 255, 0.75); }
|
||||||
|
100% { transform: translateX(-50%) scale(1.06); box-shadow: 0 0 32px rgba(0, 229, 255, 1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DISCARD PILE GREEN WILDCARD BADGE */
|
||||||
|
.green-wildcard-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -18px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: linear-gradient(135deg, #1b4332, #2d6a4f, #52b788);
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
padding: 3px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 0 20px rgba(82, 183, 136, 0.9), 0 4px 12px rgba(0, 0, 0, 0.7);
|
||||||
|
border: 2px solid #b7e4c7;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 25;
|
||||||
|
animation: green-wildcard-pulse 1.6s infinite alternate;
|
||||||
|
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes green-wildcard-pulse {
|
||||||
|
0% { transform: translateX(-50%) scale(0.95); box-shadow: 0 0 14px rgba(82, 183, 136, 0.7); }
|
||||||
|
100% { transform: translateX(-50%) scale(1.06); box-shadow: 0 0 28px rgba(82, 183, 136, 1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-badge.is-finished {
|
.avatar-badge.is-finished {
|
||||||
opacity: 0.6;
|
opacity: 0.65;
|
||||||
filter: grayscale(0.5);
|
filter: grayscale(0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-badge.can-be-revived {
|
||||||
|
opacity: 0.95 !important;
|
||||||
|
filter: none !important;
|
||||||
|
border-color: #ff1744 !important;
|
||||||
|
box-shadow: 0 0 20px rgba(255, 23, 68, 0.7), inset 0 0 10px rgba(255, 23, 68, 0.3) !important;
|
||||||
|
animation: revive-target-pulse 1.4s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes revive-target-pulse {
|
||||||
|
0% { box-shadow: 0 0 12px rgba(255, 23, 68, 0.5); }
|
||||||
|
100% { box-shadow: 0 0 24px rgba(255, 23, 68, 0.95), 0 0 35px rgba(255, 82, 82, 0.4); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* REVIVE COUNTDOWN BADGE */
|
||||||
|
.revive-countdown-badge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -13px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: linear-gradient(135deg, #b71c1c, #d50000, #ff1744);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 2px 9px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 0 14px rgba(255, 23, 68, 0.85), 0 2px 6px rgba(0, 0, 0, 0.6);
|
||||||
|
border: 1.5px solid #ff8a80;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 25;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
animation: heartbeat-glow 1.2s infinite ease-in-out alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes heartbeat-glow {
|
||||||
|
0% { transform: translateX(-50%) scale(0.96); box-shadow: 0 0 8px rgba(255, 23, 68, 0.6); }
|
||||||
|
100% { transform: translateX(-50%) scale(1.05); box-shadow: 0 0 18px rgba(255, 23, 68, 1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.heartbeat-icon {
|
||||||
|
display: inline-block;
|
||||||
|
animation: heart-beat 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes heart-beat {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
15% { transform: scale(1.35); }
|
||||||
|
30% { transform: scale(1); }
|
||||||
|
45% { transform: scale(1.25); }
|
||||||
|
60% { transform: scale(1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-finished-pos {
|
.badge-finished-pos {
|
||||||
|
|||||||
@@ -3,11 +3,23 @@
|
|||||||
window.FaraonFx = (function () {
|
window.FaraonFx = (function () {
|
||||||
let audioCtx = null;
|
let audioCtx = null;
|
||||||
let isMuted = false;
|
let isMuted = false;
|
||||||
|
let speedMultiplier = 1.0;
|
||||||
let particleCanvas = null;
|
let particleCanvas = null;
|
||||||
let particleCtx = null;
|
let particleCtx = null;
|
||||||
let particles = [];
|
let particles = [];
|
||||||
let animationFrameId = null;
|
let animationFrameId = null;
|
||||||
|
|
||||||
|
function setSpeedLevel(level) {
|
||||||
|
switch (level) {
|
||||||
|
case 1: speedMultiplier = 0.7; break; // 0.5x
|
||||||
|
case 2: speedMultiplier = 0.85; break; // 0.75x
|
||||||
|
case 4: speedMultiplier = 1.5; break; // 1.5x
|
||||||
|
case 5: speedMultiplier = 2.4; break; // 2.5x Turbo
|
||||||
|
case 3:
|
||||||
|
default: speedMultiplier = 1.0; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getAudioContext() {
|
function getAudioContext() {
|
||||||
if (!audioCtx) {
|
if (!audioCtx) {
|
||||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||||
@@ -168,6 +180,79 @@ window.FaraonFx = (function () {
|
|||||||
triggerScreenShake(3);
|
triggerScreenShake(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function playIceCrack() {
|
||||||
|
if (isMuted) return;
|
||||||
|
const ctx = getAudioContext();
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const now = ctx.currentTime;
|
||||||
|
|
||||||
|
// Sharp ice snap noise
|
||||||
|
const bufferSize = ctx.sampleRate * 0.12;
|
||||||
|
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
|
||||||
|
const data = buffer.getChannelData(0);
|
||||||
|
for (let i = 0; i < bufferSize; i++) {
|
||||||
|
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (ctx.sampleRate * 0.02));
|
||||||
|
}
|
||||||
|
|
||||||
|
const noise = ctx.createBufferSource();
|
||||||
|
noise.buffer = buffer;
|
||||||
|
|
||||||
|
const filter = ctx.createBiquadFilter();
|
||||||
|
filter.type = 'highpass';
|
||||||
|
filter.frequency.setValueAtTime(3200, now);
|
||||||
|
|
||||||
|
const gain = ctx.createGain();
|
||||||
|
gain.gain.setValueAtTime(0.45, now);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12);
|
||||||
|
|
||||||
|
noise.connect(filter);
|
||||||
|
filter.connect(gain);
|
||||||
|
gain.connect(ctx.destination);
|
||||||
|
noise.start(now);
|
||||||
|
|
||||||
|
// Resonant high crystal ringing
|
||||||
|
[2400, 3600, 4800].forEach((freq, idx) => {
|
||||||
|
const osc = ctx.createOscillator();
|
||||||
|
const oscGain = ctx.createGain();
|
||||||
|
osc.type = 'sine';
|
||||||
|
osc.frequency.setValueAtTime(freq + Math.random() * 200, now + idx * 0.02);
|
||||||
|
oscGain.gain.setValueAtTime(0.12, now + idx * 0.02);
|
||||||
|
oscGain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.02 + 0.22);
|
||||||
|
|
||||||
|
osc.connect(oscGain);
|
||||||
|
oscGain.connect(ctx.destination);
|
||||||
|
osc.start(now + idx * 0.02);
|
||||||
|
osc.stop(now + idx * 0.02 + 0.23);
|
||||||
|
});
|
||||||
|
|
||||||
|
triggerScreenShake(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playLeafShield() {
|
||||||
|
if (isMuted) return;
|
||||||
|
const ctx = getAudioContext();
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const now = ctx.currentTime;
|
||||||
|
[523.25, 659.25, 783.99, 1046.50].forEach((freq, idx) => {
|
||||||
|
const osc = ctx.createOscillator();
|
||||||
|
const gain = ctx.createGain();
|
||||||
|
osc.type = 'triangle';
|
||||||
|
osc.frequency.setValueAtTime(freq, now + idx * 0.05);
|
||||||
|
|
||||||
|
gain.gain.setValueAtTime(0.2, now + idx * 0.05);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.05 + 0.35);
|
||||||
|
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(ctx.destination);
|
||||||
|
osc.start(now + idx * 0.05);
|
||||||
|
osc.stop(now + idx * 0.05 + 0.36);
|
||||||
|
});
|
||||||
|
|
||||||
|
triggerScreenShake(2);
|
||||||
|
}
|
||||||
|
|
||||||
function playHornikChime() {
|
function playHornikChime() {
|
||||||
if (isMuted) return;
|
if (isMuted) return;
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
@@ -386,7 +471,7 @@ window.FaraonFx = (function () {
|
|||||||
opacity: 1
|
opacity: 1
|
||||||
}
|
}
|
||||||
], {
|
], {
|
||||||
duration: 420,
|
duration: Math.round(420 / speedMultiplier),
|
||||||
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.2)'
|
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.2)'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -429,7 +514,7 @@ window.FaraonFx = (function () {
|
|||||||
{ transform: `translate(-50%, -50%) translate3d(${dx * 0.5}px, ${dy * 0.5 - 30}px, 0) scale(1.15) rotate(10deg)`, opacity: 1, offset: 0.5 },
|
{ transform: `translate(-50%, -50%) translate3d(${dx * 0.5}px, ${dy * 0.5 - 30}px, 0) scale(1.15) rotate(10deg)`, opacity: 1, offset: 0.5 },
|
||||||
{ transform: `translate(-50%, -50%) translate3d(${dx}px, ${dy}px, 0) scale(0.6) rotate(0deg)`, opacity: 0.2 }
|
{ transform: `translate(-50%, -50%) translate3d(${dx}px, ${dy}px, 0) scale(0.6) rotate(0deg)`, opacity: 0.2 }
|
||||||
], {
|
], {
|
||||||
duration: 380,
|
duration: Math.round(380 / speedMultiplier),
|
||||||
easing: 'cubic-bezier(0.25, 1, 0.5, 1)'
|
easing: 'cubic-bezier(0.25, 1, 0.5, 1)'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -513,7 +598,7 @@ window.FaraonFx = (function () {
|
|||||||
const midX = (startPos.x + endPos.x) / 2;
|
const midX = (startPos.x + endPos.x) / 2;
|
||||||
const midY = Math.min(startPos.y, endPos.y) - 35;
|
const midY = Math.min(startPos.y, endPos.y) - 35;
|
||||||
|
|
||||||
const duration = 380;
|
const duration = Math.round(380 / speedMultiplier);
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
||||||
function step(currentTime) {
|
function step(currentTime) {
|
||||||
@@ -693,6 +778,71 @@ window.FaraonFx = (function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function triggerIceShatter(position) {
|
||||||
|
if (!particleCanvas) return;
|
||||||
|
let x = window.innerWidth / 2;
|
||||||
|
let y = window.innerHeight / 2;
|
||||||
|
|
||||||
|
if (typeof position === 'string') {
|
||||||
|
const elem = getSlotElement(position);
|
||||||
|
const coords = elem ? getElementCenter(elem) : getPositionCoords(position);
|
||||||
|
x = coords.x;
|
||||||
|
y = coords.y;
|
||||||
|
} else if (position && position.x && position.y) {
|
||||||
|
x = position.x;
|
||||||
|
y = position.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
playIceCrack();
|
||||||
|
|
||||||
|
const colors = ['#00ffff', '#80d8ff', '#e0f7fa', '#ffffff', '#b2ebf2', '#87cefa'];
|
||||||
|
for (let i = 0; i < 40; i++) {
|
||||||
|
const angle = Math.random() * Math.PI * 2;
|
||||||
|
const speed = 3 + Math.random() * 10;
|
||||||
|
particles.push({
|
||||||
|
x: x + (Math.random() - 0.5) * 50,
|
||||||
|
y: y + (Math.random() - 0.5) * 40,
|
||||||
|
vx: Math.cos(angle) * speed,
|
||||||
|
vy: Math.sin(angle) * speed - 2.5,
|
||||||
|
gravity: 0.22,
|
||||||
|
size: 6 + Math.random() * 9,
|
||||||
|
aspect: 0.35 + Math.random() * 1.5,
|
||||||
|
color: colors[Math.floor(Math.random() * colors.length)],
|
||||||
|
shape: Math.random() > 0.35 ? 'rect' : 'spark',
|
||||||
|
life: 1.1,
|
||||||
|
decay: 0.022 + Math.random() * 0.02,
|
||||||
|
rotation: Math.random() * Math.PI * 2,
|
||||||
|
vRot: (Math.random() - 0.5) * 0.4
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerLeafParticles(x, y, count = 35) {
|
||||||
|
if (!particleCanvas) return;
|
||||||
|
const startX = x || window.innerWidth / 2;
|
||||||
|
const startY = y || window.innerHeight / 2;
|
||||||
|
const colors = ['#2d6a4f', '#52b788', '#74c69d', '#b7e4c7', '#d8f3dc', '#ffd700'];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const angle = Math.random() * Math.PI * 2;
|
||||||
|
const speed = 2.5 + Math.random() * 8;
|
||||||
|
particles.push({
|
||||||
|
x: startX + (Math.random() - 0.5) * 30,
|
||||||
|
y: startY + (Math.random() - 0.5) * 30,
|
||||||
|
vx: Math.cos(angle) * speed,
|
||||||
|
vy: Math.sin(angle) * speed - 1.5,
|
||||||
|
gravity: 0.12,
|
||||||
|
size: 5 + Math.random() * 7,
|
||||||
|
aspect: 0.4 + Math.random() * 1.5,
|
||||||
|
color: colors[Math.floor(Math.random() * colors.length)],
|
||||||
|
shape: 'spark',
|
||||||
|
life: 1.0,
|
||||||
|
decay: 0.025 + Math.random() * 0.02,
|
||||||
|
rotation: Math.random() * Math.PI * 2,
|
||||||
|
vRot: (Math.random() - 0.5) * 0.3
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function triggerBurnExplosion(x, y) {
|
function triggerBurnExplosion(x, y) {
|
||||||
if (!particleCanvas) return;
|
if (!particleCanvas) return;
|
||||||
const colors = ['#ff0000', '#ff4500', '#ffaa00', '#333333', '#ffffff'];
|
const colors = ['#ff0000', '#ff4500', '#ffaa00', '#333333', '#ffffff'];
|
||||||
@@ -1016,6 +1166,8 @@ window.FaraonFx = (function () {
|
|||||||
playCardDraw,
|
playCardDraw,
|
||||||
playSevenStack,
|
playSevenStack,
|
||||||
playAceFreeze,
|
playAceFreeze,
|
||||||
|
playIceCrack,
|
||||||
|
playLeafShield,
|
||||||
playHornikChime,
|
playHornikChime,
|
||||||
playBurnExplosion,
|
playBurnExplosion,
|
||||||
playVictory,
|
playVictory,
|
||||||
@@ -1029,6 +1181,8 @@ window.FaraonFx = (function () {
|
|||||||
initParticles,
|
initParticles,
|
||||||
triggerFireParticles,
|
triggerFireParticles,
|
||||||
triggerIceParticles,
|
triggerIceParticles,
|
||||||
|
triggerIceShatter,
|
||||||
|
triggerLeafParticles,
|
||||||
triggerBurnExplosion,
|
triggerBurnExplosion,
|
||||||
triggerConfetti,
|
triggerConfetti,
|
||||||
triggerCoinJackpot,
|
triggerCoinJackpot,
|
||||||
@@ -1037,6 +1191,7 @@ window.FaraonFx = (function () {
|
|||||||
animateCardPlay,
|
animateCardPlay,
|
||||||
animateCardDraw,
|
animateCardDraw,
|
||||||
animateTurnTransition,
|
animateTurnTransition,
|
||||||
|
setSpeedLevel,
|
||||||
toggleMute
|
toggleMute
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -124,11 +124,13 @@ public class FaraonAI : IFaraonAI
|
|||||||
|
|
||||||
private Card ChooseBestCardSmart(List<Card> playable, Player player, GameState state)
|
private Card ChooseBestCardSmart(List<Card> playable, Player player, GameState state)
|
||||||
{
|
{
|
||||||
// 1. Ak čelíme sedme, prioritne zahráme obyčajnú sedmu pred Červenou sedmou (ak je niekto vonku, šetríme ju)
|
// 1. Ak čelíme sedme, zahráme obyčajnú sedmu, Zeleného dolníka na zrušenie trestu, alebo Červenú sedmu
|
||||||
if (state.PendingDrawCount > 0)
|
if (state.PendingDrawCount > 0)
|
||||||
{
|
{
|
||||||
var regularSeven = playable.FirstOrDefault(c => c.IsSeven && !c.IsRedSeven);
|
var regularSeven = playable.FirstOrDefault(c => c.IsSeven && !c.IsRedSeven);
|
||||||
if (regularSeven != null) return regularSeven;
|
if (regularSeven != null) return regularSeven;
|
||||||
|
var greenUnderKnave = playable.FirstOrDefault(c => c.IsGreenUnderKnave);
|
||||||
|
if (greenUnderKnave != null) return greenUnderKnave;
|
||||||
var redSeven = playable.FirstOrDefault(c => c.IsRedSeven);
|
var redSeven = playable.FirstOrDefault(c => c.IsRedSeven);
|
||||||
if (redSeven != null) return redSeven;
|
if (redSeven != null) return redSeven;
|
||||||
}
|
}
|
||||||
@@ -147,7 +149,7 @@ public class FaraonAI : IFaraonAI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Ak má Červenú sedmu a na stole je niekto na oživenie (LastFinishedPlayer), oživíme ho!
|
// 4. Ak má Červenú sedmu a na stole je niekto na oživenie (LastFinishedPlayer), oživíme ho!
|
||||||
if (state.Rules.RedSevenRevives && state.LastFinishedPlayer != null)
|
if (state.Rules.RedSevenRevives && state.LastFinishedPlayer != null && state.ReviveTurnsRemaining > 0)
|
||||||
{
|
{
|
||||||
var redSeven = playable.FirstOrDefault(c => c.IsRedSeven);
|
var redSeven = playable.FirstOrDefault(c => c.IsRedSeven);
|
||||||
if (redSeven != null) return redSeven;
|
if (redSeven != null) return redSeven;
|
||||||
|
|||||||
@@ -12,9 +12,15 @@ public static class ActionValidator
|
|||||||
bool pendingSkip,
|
bool pendingSkip,
|
||||||
RuleOptions rules)
|
RuleOptions rules)
|
||||||
{
|
{
|
||||||
// 1. Ak je na stole trest zo sedmy (napr. +2, +4, +6)
|
// 1. Ak je na stole trest zo sedmy (napr. +3, +6, +9)
|
||||||
if (pendingDrawCount > 0)
|
if (pendingDrawCount > 0)
|
||||||
{
|
{
|
||||||
|
// Zelený dolník ruší sedmičky
|
||||||
|
if (rules.GreenUnderKnaveCancelsSevens && card.IsGreenUnderKnave)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Ak je povolené prebíjanie sediem, musí zahrať sedmu
|
// Ak je povolené prebíjanie sediem, musí zahrať sedmu
|
||||||
return rules.StackSevens && card.IsSeven;
|
return rules.StackSevens && card.IsSeven;
|
||||||
}
|
}
|
||||||
@@ -38,6 +44,21 @@ public static class ActionValidator
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4b. Červená sedma v režime AnyCardOneRound sa môže zahrať na ľubovoľnú kartu
|
||||||
|
if (card.IsRedSeven && rules.RedSevenMode == RedSevenMode.AnyCardOneRound)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4c. Zelený dolník v režime UniversalWildcard ide na čokoľvek a naňho ide čokoľvek
|
||||||
|
if (rules.GreenUnderKnaveMode == GreenUnderKnaveMode.UniversalWildcard)
|
||||||
|
{
|
||||||
|
if (card.IsGreenUnderKnave || topCard.IsGreenUnderKnave)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 5. Ak bola zmenená farba Horníkom
|
// 5. Ak bola zmenená farba Horníkom
|
||||||
if (activeSuit.HasValue)
|
if (activeSuit.HasValue)
|
||||||
{
|
{
|
||||||
@@ -66,6 +87,10 @@ public static class ActionValidator
|
|||||||
// 1. Ak je na stole trest zo sedmy
|
// 1. Ak je na stole trest zo sedmy
|
||||||
if (pendingDrawCount > 0)
|
if (pendingDrawCount > 0)
|
||||||
{
|
{
|
||||||
|
if (rules.GreenUnderKnaveCancelsSevens && cards.Any(c => c.IsGreenUnderKnave))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return rules.StackSevens && cards.All(c => c.IsSeven);
|
return rules.StackSevens && cards.All(c => c.IsSeven);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +112,21 @@ public static class ActionValidator
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4b. Combo so Srdcovou/Červenou sedmou v režime AnyCardOneRound sa dá vyložiť na čokoľvek
|
||||||
|
if (firstRank == Rank.Seven && cards.Any(c => c.IsRedSeven) && rules.RedSevenMode == RedSevenMode.AnyCardOneRound)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4c. Zelený dolník v režime UniversalWildcard
|
||||||
|
if (rules.GreenUnderKnaveMode == GreenUnderKnaveMode.UniversalWildcard)
|
||||||
|
{
|
||||||
|
if (cards.Any(c => c.IsGreenUnderKnave) || topCard.IsGreenUnderKnave)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 5. Ak bola zmenená farba Horníkom
|
// 5. Ak bola zmenená farba Horníkom
|
||||||
if (activeSuit.HasValue)
|
if (activeSuit.HasValue)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ public class GameEngine
|
|||||||
State.RoundLoser = null;
|
State.RoundLoser = null;
|
||||||
State.RoundNumber = 1;
|
State.RoundNumber = 1;
|
||||||
State.LastFinishedPlayer = null;
|
State.LastFinishedPlayer = null;
|
||||||
|
State.ReviveTurnsRemaining = 0;
|
||||||
|
State.ReviveRoundsRemaining = 0;
|
||||||
|
State.ReviveWindowInitialTurns = 0;
|
||||||
State.TurnCount = 1;
|
State.TurnCount = 1;
|
||||||
State.PendingDrawCount = 0;
|
State.PendingDrawCount = 0;
|
||||||
State.PendingSkip = false;
|
State.PendingSkip = false;
|
||||||
@@ -205,11 +208,13 @@ public class GameEngine
|
|||||||
State.PendingDrawCount += addedPenalty;
|
State.PendingDrawCount += addedPenalty;
|
||||||
|
|
||||||
var redSeven = cards.FirstOrDefault(c => c.IsRedSeven);
|
var redSeven = cards.FirstOrDefault(c => c.IsRedSeven);
|
||||||
if (redSeven != null && State.Rules.RedSevenRevives && State.LastFinishedPlayer != null && State.LastFinishedPlayer != player)
|
if (redSeven != null && State.Rules.RedSevenRevives && State.LastFinishedPlayer != null && State.LastFinishedPlayer != player && State.ReviveTurnsRemaining > 0)
|
||||||
{
|
{
|
||||||
var revived = State.LastFinishedPlayer;
|
var revived = State.LastFinishedPlayer;
|
||||||
revived.FinishedPosition = null;
|
revived.FinishedPosition = null;
|
||||||
State.LastFinishedPlayer = null;
|
State.LastFinishedPlayer = null;
|
||||||
|
State.ReviveTurnsRemaining = 0;
|
||||||
|
State.ReviveRoundsRemaining = 0;
|
||||||
DrawCardsForPlayer(revived, 3);
|
DrawCardsForPlayer(revived, 3);
|
||||||
|
|
||||||
EmitEvent(new GameEvent(
|
EmitEvent(new GameEvent(
|
||||||
@@ -257,6 +262,20 @@ public class GameEngine
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Efekt Zeleného dolníka (rušenie sediem)
|
||||||
|
else if (firstRank == Rank.UnderKnave && cards.Any(c => c.IsGreenUnderKnave) && State.Rules.GreenUnderKnaveCancelsSevens && State.PendingDrawCount > 0)
|
||||||
|
{
|
||||||
|
int canceled = State.PendingDrawCount;
|
||||||
|
State.PendingDrawCount = 0;
|
||||||
|
|
||||||
|
EmitEvent(new GameEvent(
|
||||||
|
GameEventType.SevenCanceled,
|
||||||
|
player,
|
||||||
|
Cards: cards,
|
||||||
|
ExtraData: canceled,
|
||||||
|
Message: $"🍃 {player.Name} zahral Zeleného dolníka a ZRUŠIL trest {canceled} kariet!"
|
||||||
|
));
|
||||||
|
}
|
||||||
// Ostatné karty
|
// Ostatné karty
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -328,11 +347,13 @@ public class GameEngine
|
|||||||
State.PendingDrawCount += 3;
|
State.PendingDrawCount += 3;
|
||||||
|
|
||||||
// Červená sedma vracia do hry
|
// Červená sedma vracia do hry
|
||||||
if (card.IsRedSeven && State.Rules.RedSevenRevives && State.LastFinishedPlayer != null && State.LastFinishedPlayer != player)
|
if (card.IsRedSeven && State.Rules.RedSevenRevives && State.LastFinishedPlayer != null && State.LastFinishedPlayer != player && State.ReviveTurnsRemaining > 0)
|
||||||
{
|
{
|
||||||
var revived = State.LastFinishedPlayer;
|
var revived = State.LastFinishedPlayer;
|
||||||
revived.FinishedPosition = null;
|
revived.FinishedPosition = null;
|
||||||
State.LastFinishedPlayer = null;
|
State.LastFinishedPlayer = null;
|
||||||
|
State.ReviveTurnsRemaining = 0;
|
||||||
|
State.ReviveRoundsRemaining = 0;
|
||||||
|
|
||||||
// Doberie 3 karty
|
// Doberie 3 karty
|
||||||
DrawCardsForPlayer(revived, 3);
|
DrawCardsForPlayer(revived, 3);
|
||||||
@@ -382,6 +403,21 @@ public class GameEngine
|
|||||||
State.PendingSkip = true;
|
State.PendingSkip = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Efekt Zeleného dolníka (rušenie sediem)
|
||||||
|
if (card.IsGreenUnderKnave && State.Rules.GreenUnderKnaveCancelsSevens && State.PendingDrawCount > 0)
|
||||||
|
{
|
||||||
|
int canceled = State.PendingDrawCount;
|
||||||
|
State.PendingDrawCount = 0;
|
||||||
|
|
||||||
|
EmitEvent(new GameEvent(
|
||||||
|
GameEventType.SevenCanceled,
|
||||||
|
player,
|
||||||
|
Card: card,
|
||||||
|
ExtraData: canceled,
|
||||||
|
Message: $"🍃 {player.Name} zahral Zeleného dolníka a ZRUŠIL trest {canceled} kariet!"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// Kontrola, či hráč vyhral / skončil
|
// Kontrola, či hráč vyhral / skončil
|
||||||
CheckPlayerFinish(player);
|
CheckPlayerFinish(player);
|
||||||
|
|
||||||
@@ -538,7 +574,22 @@ public class GameEngine
|
|||||||
{
|
{
|
||||||
int nextPos = State.Players.Count(p => p.HasFinished) + 1;
|
int nextPos = State.Players.Count(p => p.HasFinished) + 1;
|
||||||
player.FinishedPosition = nextPos;
|
player.FinishedPosition = nextPos;
|
||||||
State.LastFinishedPlayer = player;
|
|
||||||
|
if (State.Rules.RedSevenRevives)
|
||||||
|
{
|
||||||
|
State.LastFinishedPlayer = player;
|
||||||
|
int maxRounds = State.Rules.RedSevenMode == RedSevenMode.ColorOrSevenThreeRounds ? 3 : 1;
|
||||||
|
int activeCount = Math.Max(1, State.ActivePlayersCount);
|
||||||
|
State.ReviveRoundsRemaining = maxRounds;
|
||||||
|
State.ReviveTurnsRemaining = maxRounds * activeCount;
|
||||||
|
State.ReviveWindowInitialTurns = State.ReviveTurnsRemaining;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
State.LastFinishedPlayer = null;
|
||||||
|
State.ReviveRoundsRemaining = 0;
|
||||||
|
State.ReviveTurnsRemaining = 0;
|
||||||
|
}
|
||||||
|
|
||||||
EmitEvent(new GameEvent(
|
EmitEvent(new GameEvent(
|
||||||
GameEventType.PlayerFinished,
|
GameEventType.PlayerFinished,
|
||||||
@@ -682,6 +733,20 @@ public class GameEngine
|
|||||||
{
|
{
|
||||||
if (State.IsGameOver || State.IsRoundOver) return;
|
if (State.IsGameOver || State.IsRoundOver) return;
|
||||||
|
|
||||||
|
// Odpočet lehoty na oživenie
|
||||||
|
if (State.LastFinishedPlayer != null && State.ReviveTurnsRemaining > 0)
|
||||||
|
{
|
||||||
|
State.ReviveTurnsRemaining--;
|
||||||
|
int activeCount = Math.Max(1, State.ActivePlayersCount);
|
||||||
|
State.ReviveRoundsRemaining = (int)Math.Ceiling((double)State.ReviveTurnsRemaining / activeCount);
|
||||||
|
|
||||||
|
if (State.ReviveTurnsRemaining <= 0)
|
||||||
|
{
|
||||||
|
State.LastFinishedPlayer = null;
|
||||||
|
State.ReviveRoundsRemaining = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int attempts = 0;
|
int attempts = 0;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -126,4 +126,5 @@ public record Card(Suit Suit, Rank Rank)
|
|||||||
public bool IsAce => Rank == Rank.Ace;
|
public bool IsAce => Rank == Rank.Ace;
|
||||||
public bool IsHornik => Rank == Rank.OverKnave;
|
public bool IsHornik => Rank == Rank.OverKnave;
|
||||||
public bool IsRedSeven => Rank == Rank.Seven && Suit == Suit.Hearts;
|
public bool IsRedSeven => Rank == Rank.Seven && Suit == Suit.Hearts;
|
||||||
|
public bool IsGreenUnderKnave => Rank == Rank.UnderKnave && Suit == Suit.Leaves;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ public enum GameEventType
|
|||||||
CardsBurned,
|
CardsBurned,
|
||||||
CardDrawn,
|
CardDrawn,
|
||||||
SevenStacked,
|
SevenStacked,
|
||||||
|
SevenCanceled,
|
||||||
AceSkipped,
|
AceSkipped,
|
||||||
AceCountered,
|
AceCountered,
|
||||||
SuitChanged,
|
SuitChanged,
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ public class GameState
|
|||||||
public RuleOptions Rules { get; set; } = new();
|
public RuleOptions Rules { get; set; } = new();
|
||||||
public bool IsGameOver { get; set; } = false;
|
public bool IsGameOver { get; set; } = false;
|
||||||
public Player? LastFinishedPlayer { get; set; }
|
public Player? LastFinishedPlayer { get; set; }
|
||||||
|
public int ReviveTurnsRemaining { get; set; } = 0;
|
||||||
|
public int ReviveRoundsRemaining { get; set; } = 0;
|
||||||
|
public int ReviveWindowInitialTurns { get; set; } = 0;
|
||||||
public int TurnCount { get; set; } = 0;
|
public int TurnCount { get; set; } = 0;
|
||||||
public TurnState TurnState { get; set; } = TurnState.Playing;
|
public TurnState TurnState { get; set; } = TurnState.Playing;
|
||||||
public Player? PendingHornikPlayer { get; set; }
|
public Player? PendingHornikPlayer { get; set; }
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
namespace Faraon.Core.Models;
|
namespace Faraon.Core.Models;
|
||||||
|
|
||||||
|
public enum RedSevenMode
|
||||||
|
{
|
||||||
|
Disabled,
|
||||||
|
AnyCardOneRound,
|
||||||
|
ColorOrSevenThreeRounds
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum GreenUnderKnaveMode
|
||||||
|
{
|
||||||
|
Disabled,
|
||||||
|
OnlyOnSevenOrUnderKnave, // Ruší sedmy, bežné pravidlá (Zeleň / Dolník)
|
||||||
|
UniversalWildcard // Ruší sedmy, ide na čokoľvek a súper naňho môže dať čokoľvek
|
||||||
|
}
|
||||||
|
|
||||||
public class RuleOptions
|
public class RuleOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -18,9 +32,32 @@ public class RuleOptions
|
|||||||
public bool CounterAceWithAce { get; set; } = true;
|
public bool CounterAceWithAce { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Červená sedma vracia do hry hráča, ktorý sa už zbavil všetkých kariet (posledného víťaza).
|
/// Režim oživovania Červenou sedmou (Voľná do 1 kola, Klasická do 3 kôl, Vypnuté).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool RedSevenRevives { get; set; } = true;
|
public RedSevenMode RedSevenMode { get; set; } = RedSevenMode.ColorOrSevenThreeRounds;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Či je oživovanie Červenou sedmou povolené.
|
||||||
|
/// </summary>
|
||||||
|
public bool RedSevenRevives
|
||||||
|
{
|
||||||
|
get => RedSevenMode != RedSevenMode.Disabled;
|
||||||
|
set => RedSevenMode = value ? (RedSevenMode == RedSevenMode.Disabled ? RedSevenMode.ColorOrSevenThreeRounds : RedSevenMode) : RedSevenMode.Disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Režim pre Zeleného dolníka (rušenie sediem a univerzálne vykladanie).
|
||||||
|
/// </summary>
|
||||||
|
public GreenUnderKnaveMode GreenUnderKnaveMode { get; set; } = GreenUnderKnaveMode.OnlyOnSevenOrUnderKnave;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Či Zelený dolník ruší trest zo sediem.
|
||||||
|
/// </summary>
|
||||||
|
public bool GreenUnderKnaveCancelsSevens
|
||||||
|
{
|
||||||
|
get => GreenUnderKnaveMode != GreenUnderKnaveMode.Disabled;
|
||||||
|
set => GreenUnderKnaveMode = value ? (GreenUnderKnaveMode == GreenUnderKnaveMode.Disabled ? GreenUnderKnaveMode.OnlyOnSevenOrUnderKnave : GreenUnderKnaveMode) : GreenUnderKnaveMode.Disabled;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Zahranie 4 rovnakých kariet naraz ("Spálenie"). Spáli kôpku a hráč hrá znova.
|
/// Zahranie 4 rovnakých kariet naraz ("Spálenie"). Spáli kôpku a hráč hrá znova.
|
||||||
@@ -33,9 +70,25 @@ public class RuleOptions
|
|||||||
public bool PassAndPlayPrivacyScreen { get; set; } = false;
|
public bool PassAndPlayPrivacyScreen { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rýchlosť AI ťahov v milisekundách (pre plynulé sledovanie animácií).
|
/// Úroveň rýchlosti hry (1 = 0.5x, 2 = 0.75x, 3 = 1x Normálna, 4 = 1.5x Rýchla, 5 = 2.5x Turbo).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int AIDelayMs { get; set; } = 900;
|
public int SpeedLevel { get; set; } = 3;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rýchlosť AI ťahov v milisekundách podľa zvolenej úrovne rýchlosti.
|
||||||
|
/// </summary>
|
||||||
|
public int AIDelayMs
|
||||||
|
{
|
||||||
|
get => SpeedLevel switch
|
||||||
|
{
|
||||||
|
1 => 1600,
|
||||||
|
2 => 1200,
|
||||||
|
4 => 500,
|
||||||
|
5 => 200,
|
||||||
|
_ => 900
|
||||||
|
};
|
||||||
|
set { }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Zvolený vizuálny štýl balíčka ("original" alebo "spongebob").
|
/// Zvolený vizuálny štýl balíčka ("original" alebo "spongebob").
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ public class GameEngineTests
|
|||||||
p3.Hand.Clear();
|
p3.Hand.Clear();
|
||||||
p3.FinishedPosition = 1;
|
p3.FinishedPosition = 1;
|
||||||
engine.State.LastFinishedPlayer = p3;
|
engine.State.LastFinishedPlayer = p3;
|
||||||
|
engine.State.ReviveTurnsRemaining = 4;
|
||||||
|
engine.State.ReviveRoundsRemaining = 2;
|
||||||
|
|
||||||
p1.Hand.Clear();
|
p1.Hand.Clear();
|
||||||
var redSeven = new Card(Suit.Hearts, Rank.Seven);
|
var redSeven = new Card(Suit.Hearts, Rank.Seven);
|
||||||
@@ -126,6 +128,64 @@ public class GameEngineTests
|
|||||||
Assert.Equal(3, p3.Hand.Count);
|
Assert.Equal(3, p3.Hand.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RedSeven_AnyCardOneRound_CanPlayOnAnyCard()
|
||||||
|
{
|
||||||
|
var rules = new RuleOptions { RedSevenMode = RedSevenMode.AnyCardOneRound };
|
||||||
|
var redSeven = new Card(Suit.Hearts, Rank.Seven);
|
||||||
|
var topCard = new Card(Suit.Acorns, Rank.King); // completely different suit and rank
|
||||||
|
|
||||||
|
var canPlay = ActionValidator.CanPlayCard(redSeven, topCard, null, 0, false, rules);
|
||||||
|
Assert.True(canPlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RedSeven_ColorOrSeven_OnlyOnMatchingSuitOrSeven()
|
||||||
|
{
|
||||||
|
var rules = new RuleOptions { RedSevenMode = RedSevenMode.ColorOrSevenThreeRounds };
|
||||||
|
var redSeven = new Card(Suit.Hearts, Rank.Seven);
|
||||||
|
var topAcornsKing = new Card(Suit.Acorns, Rank.King);
|
||||||
|
var topHeartsKing = new Card(Suit.Hearts, Rank.King);
|
||||||
|
var topBellsSeven = new Card(Suit.Bells, Rank.Seven);
|
||||||
|
|
||||||
|
Assert.False(ActionValidator.CanPlayCard(redSeven, topAcornsKing, null, 0, false, rules));
|
||||||
|
Assert.True(ActionValidator.CanPlayCard(redSeven, topHeartsKing, null, 0, false, rules));
|
||||||
|
Assert.True(ActionValidator.CanPlayCard(redSeven, topBellsSeven, null, 0, false, rules));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RedSeven_ReviveExpiresAfterTurns()
|
||||||
|
{
|
||||||
|
var engine = new GameEngine(seed: 42);
|
||||||
|
var p1 = new Player("Hráč 1", PlayerType.Human);
|
||||||
|
var p2 = new Player("Hráč 2", PlayerType.Human);
|
||||||
|
var p3 = new Player("Hráč 3", PlayerType.Human);
|
||||||
|
var rules = new RuleOptions { RedSevenMode = RedSevenMode.AnyCardOneRound };
|
||||||
|
engine.StartGame(new List<Player> { p1, p2, p3 }, rules);
|
||||||
|
|
||||||
|
p3.Hand.Clear();
|
||||||
|
p3.Hand.Add(new Card(Suit.Leaves, Rank.Eight));
|
||||||
|
engine.State.DiscardPile.Clear();
|
||||||
|
engine.State.DiscardPile.Add(new Card(Suit.Leaves, Rank.Seven));
|
||||||
|
|
||||||
|
// p3 finishes
|
||||||
|
engine.State.CurrentPlayerIndex = 2;
|
||||||
|
var played = engine.PlayCard(p3, p3.Hand[0]);
|
||||||
|
Assert.True(played);
|
||||||
|
Assert.True(p3.HasFinished);
|
||||||
|
Assert.Equal(p3, engine.State.LastFinishedPlayer);
|
||||||
|
Assert.True(engine.State.ReviveTurnsRemaining > 0);
|
||||||
|
|
||||||
|
// Advance turns until window expires
|
||||||
|
while (engine.State.ReviveTurnsRemaining > 0)
|
||||||
|
{
|
||||||
|
engine.DrawOrAcknowledge(engine.State.CurrentPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Null(engine.State.LastFinishedPlayer);
|
||||||
|
Assert.Equal(0, engine.State.ReviveTurnsRemaining);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FourOfAKind_BurnsAndAllowsAnotherMove()
|
public void FourOfAKind_BurnsAndAllowsAnotherMove()
|
||||||
{
|
{
|
||||||
@@ -314,4 +374,61 @@ public class GameEngineTests
|
|||||||
Assert.Equal(p1, engine.State.MatchWinner);
|
Assert.Equal(p1, engine.State.MatchWinner);
|
||||||
Assert.Equal(TurnState.GameOver, engine.State.TurnState);
|
Assert.Equal(TurnState.GameOver, engine.State.TurnState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GreenUnderKnave_CancelsSevenPenalty()
|
||||||
|
{
|
||||||
|
var engine = new GameEngine(seed: 42);
|
||||||
|
var p1 = new Player("Hráč 1", PlayerType.Human);
|
||||||
|
var p2 = new Player("Hráč 2", PlayerType.Human);
|
||||||
|
var rules = new RuleOptions { GreenUnderKnaveMode = GreenUnderKnaveMode.OnlyOnSevenOrUnderKnave };
|
||||||
|
engine.StartGame(new List<Player> { p1, p2 }, rules);
|
||||||
|
|
||||||
|
// p1 plays 7 -> penalty 3
|
||||||
|
p1.Hand.Clear();
|
||||||
|
p1.Hand.Add(new Card(Suit.Bells, Rank.Seven));
|
||||||
|
p1.Hand.Add(new Card(Suit.Leaves, Rank.Eight));
|
||||||
|
engine.State.DiscardPile.Clear();
|
||||||
|
engine.State.DiscardPile.Add(new Card(Suit.Bells, Rank.King));
|
||||||
|
|
||||||
|
engine.PlayCard(p1, p1.Hand[0]);
|
||||||
|
Assert.Equal(3, engine.State.PendingDrawCount);
|
||||||
|
|
||||||
|
// p2 plays Green Under Knave -> penalty becomes 0
|
||||||
|
p2.Hand.Clear();
|
||||||
|
var greenUnderKnave = new Card(Suit.Leaves, Rank.UnderKnave);
|
||||||
|
p2.Hand.Add(greenUnderKnave);
|
||||||
|
p2.Hand.Add(new Card(Suit.Hearts, Rank.Nine));
|
||||||
|
|
||||||
|
var played = engine.PlayCard(p2, greenUnderKnave);
|
||||||
|
Assert.True(played);
|
||||||
|
Assert.Equal(0, engine.State.PendingDrawCount);
|
||||||
|
Assert.Equal(greenUnderKnave, engine.State.TopCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GreenUnderKnave_UniversalWildcard_AllowsAnyPlay()
|
||||||
|
{
|
||||||
|
var rules = new RuleOptions { GreenUnderKnaveMode = GreenUnderKnaveMode.UniversalWildcard };
|
||||||
|
var greenUnderKnave = new Card(Suit.Leaves, Rank.UnderKnave);
|
||||||
|
var topAcornsKing = new Card(Suit.Acorns, Rank.King);
|
||||||
|
var arbitraryCard = new Card(Suit.Hearts, Rank.Ten);
|
||||||
|
|
||||||
|
// Green Under Knave can be played on Acorns King
|
||||||
|
Assert.True(ActionValidator.CanPlayCard(greenUnderKnave, topAcornsKing, null, 0, false, rules));
|
||||||
|
|
||||||
|
// Arbitrary card (Hearts Ten) can be played on Green Under Knave
|
||||||
|
Assert.True(ActionValidator.CanPlayCard(arbitraryCard, greenUnderKnave, null, 0, false, rules));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GreenUnderKnave_Disabled_DoesNotCancelPenalty()
|
||||||
|
{
|
||||||
|
var rules = new RuleOptions { GreenUnderKnaveMode = GreenUnderKnaveMode.Disabled };
|
||||||
|
var greenUnderKnave = new Card(Suit.Leaves, Rank.UnderKnave);
|
||||||
|
var topSeven = new Card(Suit.Hearts, Rank.Seven);
|
||||||
|
|
||||||
|
// When facing penalty and disabled, Green Under Knave cannot be played
|
||||||
|
Assert.False(ActionValidator.CanPlayCard(greenUnderKnave, topSeven, null, 3, false, rules));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user