From 034f2ead1cf05acba3c858519812ae005a5a99f5 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Mon, 6 Apr 2026 15:11:47 +0200 Subject: [PATCH] fix: correct blackjack PnL calculation and enhance UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- api/src/games/GameServer.ts | 9 +- panel/src/games/blackjack/BlackjackGame.tsx | 219 +++++++++++++------- panel/src/index.css | 26 +++ shared/games/blackjack/blackjack.plugin.ts | 10 +- 4 files changed, 178 insertions(+), 86 deletions(-) diff --git a/api/src/games/GameServer.ts b/api/src/games/GameServer.ts index 92e12cd..3b9b733 100644 --- a/api/src/games/GameServer.ts +++ b/api/src/games/GameServer.ts @@ -95,13 +95,14 @@ export class GameServer { const gameName = gameRegistry.get(room.gameSlug)?.name ?? "Game"; const payoutDetails: Record = {}; - 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)})`, ); diff --git a/panel/src/games/blackjack/BlackjackGame.tsx b/panel/src/games/blackjack/BlackjackGame.tsx index b536e4b..cd01278 100644 --- a/panel/src/games/blackjack/BlackjackGame.tsx +++ b/panel/src/games/blackjack/BlackjackGame.tsx @@ -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 ( - + {value} ); @@ -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 = { - 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 = { + blackjack: { + label: "Blackjack!", + color: "bg-gradient-to-r from-yellow-400 to-amber-500 text-white font-bold shadow-sm", + icon: + }, + win: { + label: "Win", + color: "bg-gradient-to-r from-emerald-500 to-emerald-600 text-white font-bold shadow-sm", + icon: + }, + push: { + label: "Push", + color: "bg-gradient-to-r from-blue-500 to-blue-600 text-white font-bold shadow-sm", + icon: + }, + lose: { + label: "Lose", + color: "bg-gradient-to-r from-red-500 to-red-600 text-white font-bold shadow-sm", + icon: + }, }; - 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 ( - - {c.label} + + {c.icon} + {c.label} ); } -// ── Bet chip indicator ── +// ── Bet chip indicator --- function BetChip({ bet, betAmount }: { bet: number; betAmount: number }) { if (betAmount <= 0) return null; const total = bet * betAmount; return ( -
+
{total}
); } -// ── 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 ( -
-
+
{hand.bet > 1 && } {hand.result && } @@ -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 ( -
- {/* Player label */} -
-
+
{playerName[0]?.toUpperCase() ?? "?"} + {isMe && }
- - {playerName}{isMe ? "" : ""} + {playerName}{isMe ? " (You)" : ""} {isActivePlayer && ( @@ -244,8 +274,10 @@ function Seat({ seat, playerName, isMe, isActivePlayer, activeHandIdx, betAmount {/* Bet status (during betting phase, no hands yet) */} {!hasHands && ( -
{hasBet ? <> Ready : "Waiting..."}
@@ -273,16 +305,18 @@ function Seat({ seat, playerName, isMe, isActivePlayer, activeHandIdx, betAmount {/* Cumulative PnL indicator */} {(seat.cumulativePnl !== 0 || hasHands) && ( -
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 && } {totalPnl < 0 && } - {totalPnl > 0 ? "+" : ""}{totalPnl} AU + 0 ? "text-emerald-400" : totalPnl < 0 ? "text-red-400" : "text-blue-300"}`}> + {totalPnl > 0 ? "+" : ""}{totalPnl} AU +
)}
@@ -320,24 +354,28 @@ function DealerArea({ dealerHand, visibleValue, fullValue }: { visibleValue: number; fullValue: number | null; }) { - if (dealerHand.length === 0) { - return ( -
- Dealer -
-
- ); - } - - const displayValue = fullValue ?? visibleValue; const isBust = fullValue !== null && fullValue > 21; return ( -
-
- Dealer - {displayValue > 0 && ( - +
+
= 17 + ? "bg-white/20 backdrop-blur-sm text-white" + : "bg-white/10 backdrop-blur-sm text-white/80" + }`}> + Dealer + {fullValue !== null && ( + + {fullValue} + )}
@@ -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 ( -
- Round {roundNumber} +
+ Round {roundNumber} +
{betAmount > 0 && ( - 0 ? "text-emerald-400" : roundNet < 0 ? "text-red-400" : "text-blue-300" +
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 - + + {roundNet > 0 ? "+" : ""}{roundNet} AU + +
)} -
- 0 ? "text-emerald-400" : currentBalance < 0 ? "text-red-400" : "text-blue-300" +
+
+ Total: +
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 - + {currentBalance > 0 && } + {currentBalance < 0 && } + 0 + ? "text-emerald-400" + : currentBalance < 0 + ? "text-red-400" + : "text-blue-300" + }`}> + {currentBalance > 0 ? "+" : ""}{currentBalance} AU + +
); @@ -504,25 +565,27 @@ export function BlackjackGame({ state, myPlayerId, isSpectator, onAction, player
{/* ── Mobile layout ( +
{/* Dealer */} - +
+ +
{/* Divider */} -
+
{/* Phase label */} {isBetting && ( -
+
Round {view.roundNumber} — Place your bets
)} {isResolved && ( -
+
Round {view.roundNumber} — Results
)} @@ -543,9 +606,9 @@ export function BlackjackGame({ state, myPlayerId, isSpectator, onAction, player return (
@@ -598,23 +661,23 @@ export function BlackjackGame({ state, myPlayerId, isSpectator, onAction, player {/* Playing phase: action buttons */} {isPlaying && playerView?.canAct && ( -
+
{playerView.canSplit && ( @@ -622,7 +685,7 @@ export function BlackjackGame({ state, myPlayerId, isSpectator, onAction, player {playerView.canDoubleDown && ( diff --git a/panel/src/index.css b/panel/src/index.css index 7cb2365..528a7c5 100644 --- a/panel/src/index.css +++ b/panel/src/index.css @@ -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; +} diff --git a/shared/games/blackjack/blackjack.plugin.ts b/shared/games/blackjack/blackjack.plugin.ts index 1725aa3..f7ea019 100644 --- a/shared/games/blackjack/blackjack.plugin.ts +++ b/shared/games/blackjack/blackjack.plugin.ts @@ -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,