// // ResultBannerView.swift // Blackjack // // Displays the result of a round with breakdown. // import SwiftUI import CasinoKit struct ResultBannerView: View { let result: RoundResult let currentBalance: Int let minBet: Int let onNewRound: () -> Void let onPlayAgain: () -> Void @State private var showContent = false // MARK: - Scaled Metrics @ScaledMetric(relativeTo: .largeTitle) private var titleFontSize: CGFloat = Design.BaseFontSize.largeTitle @ScaledMetric(relativeTo: .title) private var resultFontSize: CGFloat = Design.BaseFontSize.title @ScaledMetric(relativeTo: .headline) private var amountFontSize: CGFloat = Design.BaseFontSize.xLarge @ScaledMetric(relativeTo: .body) private var buttonFontSize: CGFloat = Design.BaseFontSize.medium // MARK: - Computed private var isGameOver: Bool { currentBalance < minBet } private var mainResultColor: Color { result.mainHandResult.color } private var winningsText: String { if result.totalWinnings > 0 { return "+$\(result.totalWinnings)" } else if result.totalWinnings < 0 { return "-$\(abs(result.totalWinnings))" } else { return "$0" } } private var winningsColor: Color { if result.totalWinnings > 0 { return .green } if result.totalWinnings < 0 { return .red } return .blue } var body: some View { ZStack { // Full screen dark background Color.black.opacity(Design.Opacity.strong) .ignoresSafeArea() // Content card VStack(spacing: Design.Spacing.xLarge) { // Main result Text(result.mainHandResult.displayText) .font(.system(size: titleFontSize, weight: .black, design: .rounded)) .foregroundStyle(mainResultColor) // Winnings Text(winningsText) .font(.system(size: amountFontSize, weight: .bold, design: .rounded)) .foregroundStyle(winningsColor) // Breakdown - all hands VStack(spacing: Design.Spacing.small) { ForEach(result.handResults.indices, id: \.self) { index in let handResult = result.handResults[index] // Hand numbering: index 0 = Hand 1 (played first, displayed rightmost) let handLabel = result.handResults.count > 1 ? String(localized: "Hand \(index + 1)") : String(localized: "Main Hand") ResultRow(label: handLabel, result: handResult) } if let insuranceResult = result.insuranceResult { ResultRow(label: String(localized: "Insurance"), result: insuranceResult) } } .padding(Design.Spacing.medium) .background( RoundedRectangle(cornerRadius: Design.CornerRadius.medium) .fill(Color.white.opacity(Design.Opacity.subtle)) ) // Game over message if isGameOver { VStack(spacing: Design.Spacing.small) { Text(String(localized: "You've run out of chips!")) .font(.system(size: Design.BaseFontSize.medium, weight: .medium)) .foregroundStyle(.white.opacity(Design.Opacity.strong)) Button(action: onPlayAgain) { HStack(spacing: Design.Spacing.small) { Image(systemName: "arrow.counterclockwise") Text(String(localized: "Play Again")) } .font(.system(size: buttonFontSize, weight: .bold)) .foregroundStyle(.black) .padding(.horizontal, Design.Spacing.xxLarge) .padding(.vertical, Design.Spacing.medium) .background( Capsule() .fill( LinearGradient( colors: [Color.Button.goldLight, Color.Button.goldDark], startPoint: .top, endPoint: .bottom ) ) ) } } } else { // New Round button Button(action: onNewRound) { HStack(spacing: Design.Spacing.small) { Image(systemName: "arrow.clockwise") Text(String(localized: "New Round")) } .font(.system(size: buttonFontSize, weight: .bold)) .foregroundStyle(.black) .padding(.horizontal, Design.Spacing.xxLarge) .padding(.vertical, Design.Spacing.medium) .background( Capsule() .fill( LinearGradient( colors: [Color.Button.goldLight, Color.Button.goldDark], startPoint: .top, endPoint: .bottom ) ) ) } } } .padding(Design.Spacing.xxLarge) .background( RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge) .fill( LinearGradient( colors: [Color.Modal.backgroundLight, Color.Modal.backgroundDark], startPoint: .top, endPoint: .bottom ) ) .overlay( RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge) .strokeBorder( mainResultColor.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.medium ) ) ) .shadow(color: mainResultColor.opacity(Design.Opacity.hint), radius: Design.Shadow.radiusXLarge) .frame(maxWidth: Design.Size.maxModalWidth) .padding(.horizontal, Design.Spacing.large) // Prevent clipping on sides .scaleEffect(showContent ? 1.0 : 0.8) .opacity(showContent ? 1.0 : 0) } .onAppear { withAnimation(.spring(duration: Design.Animation.springDuration, bounce: 0.3)) { showContent = true } // Play game over sound if out of chips (after a delay so it doesn't overlap with result sound) if isGameOver { Task { try? await Task.sleep(for: .seconds(1)) SoundManager.shared.play(.gameOver) } } } .accessibilityElement(children: .contain) .accessibilityLabel(String(localized: "Round result: \(result.mainHandResult.displayText)")) .accessibilityAddTraits(AccessibilityTraits.isModal) } } // MARK: - Result Row struct ResultRow: View { let label: String let result: HandResult var body: some View { HStack { Text(label) .font(.system(size: Design.BaseFontSize.body)) .foregroundStyle(.white.opacity(Design.Opacity.strong)) Spacer() Text(result.displayText) .font(.system(size: Design.BaseFontSize.body, weight: .bold)) .foregroundStyle(result.color) } } } #Preview("Single Hand") { ResultBannerView( result: RoundResult( handResults: [.blackjack], insuranceResult: nil, totalWinnings: 150, wasBlackjack: true ), currentBalance: 10150, minBet: 10, onNewRound: {}, onPlayAgain: {} ) } #Preview("Multiple Split Hands") { ResultBannerView( result: RoundResult( handResults: [.bust, .win, .push], insuranceResult: nil, totalWinnings: 25, wasBlackjack: false ), currentBalance: 1025, minBet: 10, onNewRound: {}, onPlayAgain: {} ) }