fix: correct blackjack PnL calculation and enhance UI
Some checks failed
Deploy to Production / test (push) Failing after 35s
Some checks failed
Deploy to Production / test (push) Failing after 35s
- Fix incorrect PnL calculations where multipliers were treated as amounts - Add proper net profit calculation: (multiplier × bet) - bet - Update UI with gradient backgrounds, icons, and improved animations - Add slide-in and pulse-slow keyframe animations - Enhance dealer area with status-based styling - Improve action buttons with colored gradients and hover effects - Revise round result banner with better visual hierarchy
This commit is contained in:
@@ -95,13 +95,14 @@ export class GameServer {
|
||||
const gameName = gameRegistry.get(room.gameSlug)?.name ?? "Game";
|
||||
const payoutDetails: Record<string, { net: number }> = {};
|
||||
|
||||
for (const [playerId, grossPayout] of Object.entries(roundPayouts)) {
|
||||
// roundPayout is already the gross payout amount (not a multiplier)
|
||||
const netProfit = Math.floor(grossPayout) - betAmount; // Calculate net profit (gross payout minus original bet)
|
||||
for (const [playerId, multiplier] of Object.entries(roundPayouts)) {
|
||||
// roundPayout contains multipliers (e.g., win=2, blackjack=2.5, push=1)
|
||||
const grossAmount = Math.floor(betAmount * multiplier);
|
||||
const netProfit = grossAmount - betAmount;
|
||||
try {
|
||||
await economyService.modifyUserBalance(
|
||||
playerId,
|
||||
BigInt(grossPayout),
|
||||
BigInt(grossAmount),
|
||||
TransactionType.GAME_WIN,
|
||||
`${gameName} round payout (room ${roomId.slice(0, 8)})`,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState, useEffect, useCallback } from "react";
|
||||
import type { GameUIProps } from "../registry";
|
||||
import { Hand, Square, ArrowLeftRight, ChevronsUp, LogOut, Check, Coins } from "lucide-react";
|
||||
import { Hand, Square, ArrowLeftRight, ChevronsUp, LogOut, Check, Coins, Trophy, AlertCircle } from "lucide-react";
|
||||
|
||||
// ── Types matching server views ──
|
||||
|
||||
@@ -127,18 +127,18 @@ function CardFan({ cards, faceDown, size = "normal" }: {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Value badge ──
|
||||
// ── Value badge ---
|
||||
|
||||
function ValueBadge({ value, status }: { value: number; status?: string }) {
|
||||
const colorClass =
|
||||
value === 21 || status === "blackjack"
|
||||
? "bg-emerald-500/20 text-emerald-400 font-bold"
|
||||
? "bg-gradient-to-r from-emerald-500 to-emerald-600 text-white font-bold shadow-sm"
|
||||
: value > 21 || status === "bust"
|
||||
? "bg-red-500/20 text-red-400 font-bold"
|
||||
: "bg-black/30 text-white/80";
|
||||
? "bg-gradient-to-r from-red-500 to-red-600 text-white font-bold shadow-sm"
|
||||
: "bg-white/10 backdrop-blur-sm text-white border border-white/20";
|
||||
|
||||
return (
|
||||
<span className={`text-[10px] font-mono px-1.5 py-0.5 rounded ${colorClass}`}>
|
||||
<span className={`text-[10px] font-mono px-2 py-0.5 rounded-full ${colorClass}`}>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
@@ -147,35 +147,52 @@ function ValueBadge({ value, status }: { value: number; status?: string }) {
|
||||
// ── Result badge ──
|
||||
|
||||
function ResultBadge({ result, reason }: { result: string; reason: string | null }) {
|
||||
const config: Record<string, { label: string; color: string }> = {
|
||||
blackjack: { label: "Blackjack!", color: "bg-yellow-500/20 text-yellow-300" },
|
||||
win: { label: "Win", color: "bg-emerald-500/20 text-emerald-400" },
|
||||
push: { label: "Push", color: "bg-blue-500/20 text-blue-400" },
|
||||
lose: { label: "Lose", color: "bg-red-500/20 text-red-400" },
|
||||
const config: Record<string, { label: string; color: string; icon?: React.ReactNode }> = {
|
||||
blackjack: {
|
||||
label: "Blackjack!",
|
||||
color: "bg-gradient-to-r from-yellow-400 to-amber-500 text-white font-bold shadow-sm",
|
||||
icon: <Trophy className="w-3 h-3" />
|
||||
},
|
||||
win: {
|
||||
label: "Win",
|
||||
color: "bg-gradient-to-r from-emerald-500 to-emerald-600 text-white font-bold shadow-sm",
|
||||
icon: <ChevronsUp className="w-3 h-3" />
|
||||
},
|
||||
push: {
|
||||
label: "Push",
|
||||
color: "bg-gradient-to-r from-blue-500 to-blue-600 text-white font-bold shadow-sm",
|
||||
icon: <Check className="w-3 h-3" />
|
||||
},
|
||||
lose: {
|
||||
label: "Lose",
|
||||
color: "bg-gradient-to-r from-red-500 to-red-600 text-white font-bold shadow-sm",
|
||||
icon: <AlertCircle className="w-3 h-3" />
|
||||
},
|
||||
};
|
||||
const c = config[result] ?? { label: "-", color: "bg-black/20 text-white/50" };
|
||||
const c = config[result] ?? { label: "-", color: "bg-white/10 text-white/50" };
|
||||
|
||||
return (
|
||||
<span className={`rounded px-1.5 py-0.5 text-[10px] font-semibold ${c.color}`} title={reason ?? undefined}>
|
||||
{c.label}
|
||||
<span className={`flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] ${c.color}`} title={reason ?? undefined}>
|
||||
{c.icon}
|
||||
<span>{c.label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Bet chip indicator ──
|
||||
// ── Bet chip indicator ---
|
||||
|
||||
function BetChip({ bet, betAmount }: { bet: number; betAmount: number }) {
|
||||
if (betAmount <= 0) return null;
|
||||
const total = bet * betAmount;
|
||||
return (
|
||||
<div className="flex items-center gap-0.5 bg-yellow-500/20 text-yellow-300 rounded px-1.5 py-0.5">
|
||||
<div className="flex items-center gap-1 bg-gradient-to-r from-yellow-500 to-yellow-600 text-white rounded-full px-2 py-0.5 shadow-md border border-yellow-400/30 animate-pulse-slow">
|
||||
<Coins className="w-2.5 h-2.5" />
|
||||
<span className="text-[9px] font-bold">{total}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Single hand display ──
|
||||
// ── Single hand display ---
|
||||
|
||||
function HandDisplay({ hand, isActive, betAmount, size = "normal" }: {
|
||||
hand: PlayerHandView;
|
||||
@@ -184,11 +201,11 @@ function HandDisplay({ hand, isActive, betAmount, size = "normal" }: {
|
||||
size?: "small" | "normal";
|
||||
}) {
|
||||
return (
|
||||
<div className={`flex flex-col items-center gap-1 rounded-lg px-1.5 py-1 transition-all ${
|
||||
isActive ? "ring-1 ring-primary/40 bg-primary/5" : ""
|
||||
<div className={`flex flex-col items-center gap-2 rounded-xl px-1.5 py-2 transition-all duration-300 ${
|
||||
isActive ? "ring-2 ring-primary/50 bg-white/5 scale-[1.02] shadow-lg" : "ring-1 ring-white/10 bg-white/[0.02]"
|
||||
}`}>
|
||||
<CardFan cards={hand.cards} size={size} />
|
||||
<div className="flex items-center gap-1 flex-wrap justify-center">
|
||||
<div className="flex items-center gap-2 flex-wrap justify-center">
|
||||
<ValueBadge value={hand.value} status={hand.status} />
|
||||
{hand.bet > 1 && <BetChip bet={hand.bet} betAmount={betAmount} />}
|
||||
{hand.result && <ResultBadge result={hand.result} reason={hand.resultReason} />}
|
||||
@@ -221,21 +238,34 @@ function Seat({ seat, playerName, isMe, isActivePlayer, activeHandIdx, betAmount
|
||||
}, 0);
|
||||
const totalPnl = seat.cumulativePnl + currentRoundPnl;
|
||||
|
||||
// Determine PnL color theme
|
||||
const getPnlTheme = () => {
|
||||
if (totalPnl > 0) return "emerald";
|
||||
if (totalPnl < 0) return "red";
|
||||
return "blue";
|
||||
};
|
||||
|
||||
const pnlColor = getPnlTheme();
|
||||
const isMeStyle = isActivePlayer && isMe ? "ring-2 ring-primary/50 bg-gradient-to-br from-white/10 to-white/5" : isActivePlayer ? "ring-1 ring-primary/30 bg-primary/5" : "ring-1 ring-white/10 bg-white/[0.02]";
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col items-center gap-1.5 transition-all ${
|
||||
<div className={`flex flex-col items-center gap-2 transition-all duration-300 ${
|
||||
isActivePlayer ? "scale-105" : ""
|
||||
}`}>
|
||||
{/* Player label */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold ${
|
||||
isActivePlayer ? "bg-primary/30 text-primary ring-2 ring-primary/50" : "bg-white/10 text-white/70"
|
||||
{/* Player label with avatar and active indicator */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`relative w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold ${
|
||||
isActivePlayer
|
||||
? "bg-gradient-to-br from-primary/40 to-primary/60 ring-2 ring-primary/70 shadow-lg"
|
||||
: "bg-white/10 text-white/70 ring-1 ring-white/20"
|
||||
}`}>
|
||||
{playerName[0]?.toUpperCase() ?? "?"}
|
||||
{isMe && <span className="absolute -top-1 -right-1 w-2 h-2 bg-primary rounded-full animate-pulse" />}
|
||||
</div>
|
||||
<span className={`text-xs font-medium truncate max-w-20 ${
|
||||
isActivePlayer ? "text-primary" : "text-white/70"
|
||||
<span className={`text-xs font-medium truncate max-w-24 ${
|
||||
isActivePlayer ? "text-primary font-semibold" : isMe ? "text-white font-medium" : "text-white/60"
|
||||
}`}>
|
||||
{playerName}{isMe ? "" : ""}
|
||||
{playerName}{isMe ? " (You)" : ""}
|
||||
</span>
|
||||
{isActivePlayer && (
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />
|
||||
@@ -244,8 +274,10 @@ function Seat({ seat, playerName, isMe, isActivePlayer, activeHandIdx, betAmount
|
||||
|
||||
{/* Bet status (during betting phase, no hands yet) */}
|
||||
{!hasHands && (
|
||||
<div className={`flex items-center gap-1 text-[10px] rounded-full px-2 py-0.5 ${
|
||||
hasBet ? "bg-emerald-500/20 text-emerald-400" : "bg-white/5 text-white/40"
|
||||
<div className={`flex items-center gap-1 text-[9px] rounded-full px-2 py-0.5 ${
|
||||
hasBet
|
||||
? "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30"
|
||||
: "bg-white/5 text-white/40 border border-white/10"
|
||||
}`}>
|
||||
{hasBet ? <><Check className="w-2.5 h-2.5" /> Ready</> : "Waiting..."}
|
||||
</div>
|
||||
@@ -273,16 +305,18 @@ function Seat({ seat, playerName, isMe, isActivePlayer, activeHandIdx, betAmount
|
||||
|
||||
{/* Cumulative PnL indicator */}
|
||||
{(seat.cumulativePnl !== 0 || hasHands) && (
|
||||
<div className={`flex items-center gap-1 text-xs font-bold rounded px-2 py-0.5 ${
|
||||
<div className={`flex items-center gap-1.5 text-xs font-bold rounded-full px-3 py-1 border ${
|
||||
totalPnl > 0
|
||||
? "bg-emerald-500/20 text-emerald-400"
|
||||
? `bg-emerald-500/10 border-emerald-500/30 text-emerald-400`
|
||||
: totalPnl < 0
|
||||
? "bg-red-500/20 text-red-400"
|
||||
: "bg-blue-500/20 text-blue-300"
|
||||
? `bg-red-500/10 border-red-500/30 text-red-400`
|
||||
: `bg-blue-500/10 border-blue-500/30 text-blue-400`
|
||||
}`}>
|
||||
{totalPnl > 0 && <ChevronsUp className="w-3.5 h-3.5" />}
|
||||
{totalPnl < 0 && <ChevronsUp className="w-3.5 h-3.5 rotate-180" />}
|
||||
<span>{totalPnl > 0 ? "+" : ""}{totalPnl} AU</span>
|
||||
<span className={`font-mono ${totalPnl > 0 ? "text-emerald-400" : totalPnl < 0 ? "text-red-400" : "text-blue-300"}`}>
|
||||
{totalPnl > 0 ? "+" : ""}{totalPnl} AU
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -320,24 +354,28 @@ function DealerArea({ dealerHand, visibleValue, fullValue }: {
|
||||
visibleValue: number;
|
||||
fullValue: number | null;
|
||||
}) {
|
||||
if (dealerHand.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
<span className="text-xs font-semibold text-white/50">Dealer</span>
|
||||
<div className="w-12 h-[4.25rem] sm:w-14 sm:h-[5rem] rounded-md border border-dashed border-white/10" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const displayValue = fullValue ?? visibleValue;
|
||||
const isBust = fullValue !== null && fullValue > 21;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs font-semibold text-white/60">Dealer</span>
|
||||
{displayValue > 0 && (
|
||||
<ValueBadge value={displayValue} status={isBust ? "bust" : undefined} />
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className={`flex items-center gap-2 px-3 py-1 rounded-full ${
|
||||
fullValue === 21
|
||||
? "bg-gradient-to-r from-yellow-500 to-amber-600 text-white shadow-lg"
|
||||
: fullValue !== null && fullValue >= 17
|
||||
? "bg-white/20 backdrop-blur-sm text-white"
|
||||
: "bg-white/10 backdrop-blur-sm text-white/80"
|
||||
}`}>
|
||||
<span className="text-xs font-semibold">Dealer</span>
|
||||
{fullValue !== null && (
|
||||
<span className={`text-xs font-bold px-2 py-0.5 rounded-full ${
|
||||
fullValue === 21
|
||||
? "bg-white text-black font-bold"
|
||||
: isBust
|
||||
? "bg-red-500 text-white animate-bounce"
|
||||
: "bg-black/30 text-white"
|
||||
}`}>
|
||||
{fullValue}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<CardFan cards={dealerHand} />
|
||||
@@ -345,7 +383,7 @@ function DealerArea({ dealerHand, visibleValue, fullValue }: {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Round result banner ──
|
||||
// ── Round result banner ---
|
||||
|
||||
function RoundResultBanner({ roundNumber, roundResult, myPlayerId, betAmount, myCumulativePnl }: {
|
||||
roundNumber: number;
|
||||
@@ -361,22 +399,45 @@ function RoundResultBanner({ roundNumber, roundResult, myPlayerId, betAmount, my
|
||||
const currentBalance = myCumulativePnl;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-3 bg-black/30 rounded-xl px-4 py-2">
|
||||
<span className="text-xs text-white/50">Round {roundNumber}</span>
|
||||
<div className="flex items-center justify-between gap-3 bg-gradient-to-r from-black/50 to-black/40 rounded-xl px-4 py-3 shadow-xl border border-white/10 animate-in fade-in slide-in-from-bottom-4 duration-300">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-semibold text-white/50">Round {roundNumber}</span>
|
||||
<div className="h-4 w-px bg-white/20" />
|
||||
{betAmount > 0 && (
|
||||
<span className={`text-xs font-semibold ${
|
||||
roundNet > 0 ? "text-emerald-400" : roundNet < 0 ? "text-red-400" : "text-blue-300"
|
||||
<div className={`flex items-center gap-1.5 text-xs font-semibold px-2 py-0.5 rounded-full ${
|
||||
roundNet > 0
|
||||
? "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30"
|
||||
: roundNet < 0
|
||||
? "bg-red-500/20 text-red-400 border border-red-500/30"
|
||||
: "bg-blue-500/20 text-blue-400 border border-blue-500/30"
|
||||
}`}>
|
||||
Round: {roundNet > 0 ? "+" : ""}{roundNet} AU
|
||||
</span>
|
||||
<span className="font-mono">
|
||||
{roundNet > 0 ? "+" : ""}{roundNet} AU
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-px h-4 bg-white/20" />
|
||||
<span className={`text-xs font-bold ${
|
||||
currentBalance > 0 ? "text-emerald-400" : currentBalance < 0 ? "text-red-400" : "text-blue-300"
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-white/40">Total:</span>
|
||||
<div className={`flex items-center gap-1.5 px-3 py-1 rounded-full border ${
|
||||
currentBalance > 0
|
||||
? "bg-emerald-500/10 border-emerald-500/30"
|
||||
: currentBalance < 0
|
||||
? "bg-red-500/10 border-red-500/30"
|
||||
: "bg-blue-500/10 border-blue-500/30"
|
||||
}`}>
|
||||
Total: {currentBalance > 0 ? "+" : ""}{currentBalance} AU
|
||||
</span>
|
||||
{currentBalance > 0 && <ChevronsUp className="w-4 h-4 text-emerald-500" />}
|
||||
{currentBalance < 0 && <ChevronsUp className="w-4 h-4 text-red-500 rotate-180" />}
|
||||
<span className={`font-bold font-mono ${
|
||||
currentBalance > 0
|
||||
? "text-emerald-400"
|
||||
: currentBalance < 0
|
||||
? "text-red-400"
|
||||
: "text-blue-300"
|
||||
}`}>
|
||||
{currentBalance > 0 ? "+" : ""}{currentBalance} AU
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -504,25 +565,27 @@ export function BlackjackGame({ state, myPlayerId, isSpectator, onAction, player
|
||||
</div>
|
||||
|
||||
{/* ── Mobile layout (<md): vertical stack ── */}
|
||||
<div className="md:hidden flex flex-col gap-4 p-4">
|
||||
<div className="md:hidden flex flex-col gap-4 p-3 md:p-4">
|
||||
{/* Dealer */}
|
||||
<DealerArea
|
||||
dealerHand={view.dealerHand}
|
||||
visibleValue={view.dealerVisibleValue}
|
||||
fullValue={view.dealerFullValue}
|
||||
/>
|
||||
<div className="bg-white/5 rounded-xl p-3">
|
||||
<DealerArea
|
||||
dealerHand={view.dealerHand}
|
||||
visibleValue={view.dealerVisibleValue}
|
||||
fullValue={view.dealerFullValue}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="h-px bg-white/10" />
|
||||
<div className="h-px bg-white/10 mx-3" />
|
||||
|
||||
{/* Phase label */}
|
||||
{isBetting && (
|
||||
<div className="text-center text-white/30 text-xs">
|
||||
<div className="text-center text-white/40 text-xs font-medium">
|
||||
Round {view.roundNumber} — Place your bets
|
||||
</div>
|
||||
)}
|
||||
{isResolved && (
|
||||
<div className="text-center text-white/30 text-xs">
|
||||
<div className="text-center text-white/40 text-xs font-medium">
|
||||
Round {view.roundNumber} — Results
|
||||
</div>
|
||||
)}
|
||||
@@ -543,9 +606,9 @@ export function BlackjackGame({ state, myPlayerId, isSpectator, onAction, player
|
||||
return (
|
||||
<div
|
||||
key={slot.playerId}
|
||||
className={`rounded-xl px-3 py-2 transition-colors ${
|
||||
className={`rounded-xl px-3 py-3 transition-all duration-200 ${
|
||||
view.activePlayerId === slot.playerId
|
||||
? "bg-white/5 ring-1 ring-primary/20"
|
||||
? "ring-1 ring-primary/30 bg-white/[0.05]"
|
||||
: "bg-white/[0.02]"
|
||||
}`}
|
||||
>
|
||||
@@ -598,23 +661,23 @@ export function BlackjackGame({ state, myPlayerId, isSpectator, onAction, player
|
||||
|
||||
{/* Playing phase: action buttons */}
|
||||
{isPlaying && playerView?.canAct && (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={handleHit}
|
||||
className="flex-1 flex items-center justify-center gap-2 rounded-xl bg-primary text-on-primary px-4 py-3 text-sm font-label font-semibold hover:opacity-90 transition-colors"
|
||||
className="flex-1 min-w-[80px] flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-blue-600 to-blue-700 text-white px-4 py-3.5 text-sm font-label font-semibold shadow-lg hover:shadow-blue-500/20 transition-all active:scale-[0.98]"
|
||||
>
|
||||
<Hand className="w-4 h-4" /> Hit
|
||||
</button>
|
||||
<button
|
||||
onClick={handleStand}
|
||||
className="flex-1 flex items-center justify-center gap-2 rounded-xl bg-raised text-foreground px-4 py-3 text-sm font-label font-semibold hover:bg-surface-container-high transition-colors"
|
||||
className="flex-1 min-w-[80px] flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-gray-600 to-gray-700 text-white px-4 py-3.5 text-sm font-label font-semibold shadow-lg hover:shadow-gray-500/20 transition-all active:scale-[0.98]"
|
||||
>
|
||||
<Square className="w-3.5 h-3.5" /> Stand
|
||||
</button>
|
||||
{playerView.canSplit && (
|
||||
<button
|
||||
onClick={handleSplit}
|
||||
className="flex items-center justify-center gap-2 rounded-xl bg-raised text-foreground px-4 py-3 text-sm font-label font-semibold hover:bg-surface-container-high transition-colors"
|
||||
className="flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-purple-600 to-indigo-700 text-white px-4 py-3.5 text-sm font-label font-semibold shadow-lg hover:shadow-purple-500/20 transition-all active:scale-[0.98]"
|
||||
>
|
||||
<ArrowLeftRight className="w-4 h-4" /> Split
|
||||
</button>
|
||||
@@ -622,7 +685,7 @@ export function BlackjackGame({ state, myPlayerId, isSpectator, onAction, player
|
||||
{playerView.canDoubleDown && (
|
||||
<button
|
||||
onClick={handleDouble}
|
||||
className="flex items-center justify-center gap-2 rounded-xl bg-raised text-foreground px-4 py-3 text-sm font-label font-semibold hover:bg-surface-container-high transition-colors"
|
||||
className="flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-orange-500 to-red-600 text-white px-4 py-3.5 text-sm font-label font-semibold shadow-lg hover:shadow-orange-500/20 transition-all active:scale-[0.98]"
|
||||
>
|
||||
<ChevronsUp className="w-4 h-4" /> Double
|
||||
</button>
|
||||
|
||||
@@ -77,3 +77,29 @@ body {
|
||||
* {
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
/* Custom Animations */
|
||||
@keyframes pulse-slow {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.animate-pulse-slow {
|
||||
animation: pulse-slow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
/* Slide-in animation for result banner */
|
||||
@keyframes slideInFromBottom {
|
||||
from {
|
||||
transform: translateY(1rem);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-in.slide-in-from-bottom-4 {
|
||||
animation: slideInFromBottom 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
@@ -194,12 +194,14 @@ function finishPlayerTurns(state: BlackjackState): BlackjackState {
|
||||
// First resolve the hands
|
||||
const resolvedHands = seat.hands.map(h => resolveHand(h, dealerHand));
|
||||
|
||||
// Then calculate PnL based on resolved hands
|
||||
// Calculate round payouts as multipliers
|
||||
const roundPayout = calculateRoundPayouts({ [id]: { ...seat, hands: resolvedHands } });
|
||||
const roundPayoutMoney = roundPayout[id] ?? 0;
|
||||
// Subtract the total bet amount to get net profit/loss
|
||||
const multiplier = roundPayout[id] ?? 0;
|
||||
// Calculate total bet amount for this player
|
||||
const roundBetTotal = seat.hands.reduce((sum, h) => sum + (h.bet * state.betAmount), 0);
|
||||
const roundNetPnl = roundPayoutMoney ? roundPayoutMoney - roundBetTotal : -roundBetTotal;
|
||||
// Calculate actual payout amount and net profit
|
||||
const roundPayoutAmount = Math.round(multiplier * state.betAmount);
|
||||
const roundNetPnl = roundBetTotal > 0 ? roundPayoutAmount - roundBetTotal : 0;
|
||||
|
||||
resolvedSeats[id] = {
|
||||
...seat,
|
||||
|
||||
Reference in New Issue
Block a user