// // BlackjackTableView.swift // Blackjack // // The main table layout showing dealer and player hands. // import SwiftUI import CasinoKit struct BlackjackTableView: View { @Bindable var state: GameState let selectedChip: ChipDenomination /// Full screen size passed from parent (stable - measured from TableBackgroundView) let fullScreenSize: CGSize // MARK: - Environment @Environment(\.verticalSizeClass) private var verticalSizeClass // MARK: - Computed from stable screen size private var screenWidth: CGFloat { fullScreenSize.width } private var screenHeight: CGFloat { fullScreenSize.height } /// Whether to show Hi-Lo card count values on cards. var showCardCount: Bool { state.settings.showCardCount } // MARK: - Scaled Metrics @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.BaseFontSize.medium @ScaledMetric(relativeTo: .title) private var valueFontSize: CGFloat = Design.BaseFontSize.xLarge // MARK: - Dynamic Card Sizing /// Whether we're in landscape mode /// - iPhones: use verticalSizeClass == .compact /// - iPads: use screen dimensions (since iPads always report .regular) private var isLandscape: Bool { if DeviceInfo.isPad { return screenWidth > screenHeight } return verticalSizeClass == .compact } /// Card width based on full screen height (stable - doesn't change with content) private var cardWidth: CGFloat { let maxDimension = screenHeight let percentage: CGFloat = 0.18 // ~10% of screen return maxDimension * percentage } /// Card overlap scales proportionally with card width private var cardSpacing: CGFloat { // Overlap ratio: roughly -55% of card width cardWidth * -0.55 } // Use global debug flag from Design constants private var showDebugBorders: Bool { Design.showDebugBorders } // MARK: - Hint Toast Helper /// Shows the hint toast with auto-dismiss timer. private func showHintToastWithTimer(state: GameState) { // Generate new ID to invalidate any pending dismiss tasks let currentID = UUID() state.hintDisplayID = currentID withAnimation(.spring(duration: Design.Animation.springDuration)) { state.showHintToast = true } // Auto-dismiss after delay, but only if this is still the active hint session Task { @MainActor in try? await Task.sleep(for: Design.Toast.duration) // Only dismiss if no newer hint has arrived if state.hintDisplayID == currentID { withAnimation(.spring(duration: Design.Animation.springDuration)) { state.showHintToast = false } } } } var body: some View { VStack(spacing: 0) { // Dealer area DealerHandView( hand: state.dealerHand, showHoleCard: state.shouldShowDealerHoleCard, showCardCount: showCardCount, showAnimations: state.settings.showAnimations, dealingSpeed: state.settings.dealingSpeed, cardWidth: cardWidth, cardSpacing: cardSpacing, visibleCardCount: state.dealerVisibleCardCount ) .debugBorder(showDebugBorders, color: .red, label: "Dealer") // Top spacer Spacer(minLength: Design.Spacing.small) .debugBorder(showDebugBorders, color: .yellow, label: "TopSpacer") // Card count view centered between dealer and player if showCardCount { CardCountView( runningCount: state.engine.runningCount, trueCount: state.engine.trueCount ) .debugBorder(showDebugBorders, color: .mint, label: "CardCount") } // Bottom spacer Spacer(minLength: Design.Spacing.small) .debugBorder(showDebugBorders, color: .yellow, label: "BottomSpacer") // Player hands area - only show when there are cards dealt if state.playerHands.first?.cards.isEmpty == false { ZStack { PlayerHandsContainer( hands: state.playerHands, activeHandIndex: state.activeHandIndex, isPlayerTurn: state.isPlayerTurn, showCardCount: showCardCount, showAnimations: state.settings.showAnimations, dealingSpeed: state.settings.dealingSpeed, cardWidth: cardWidth, cardSpacing: cardSpacing, visibleCardCounts: state.playerHandsVisibleCardCount, currentHint: state.currentHint, showHintToast: state.showHintToast ) // Side bet toasts (positioned on left/right sides to not cover cards) if state.settings.sideBetsEnabled && state.showSideBetToasts { HStack { // PP on left if state.perfectPairsBet > 0, let ppResult = state.perfectPairsResult { SideBetToastView( title: "PP", result: ppResult.displayName, isWin: ppResult.isWin, amount: ppResult.isWin ? state.perfectPairsBet * ppResult.payout : -state.perfectPairsBet, showOnLeft: true ) } Spacer() // 21+3 on right if state.twentyOnePlusThreeBet > 0, let topResult = state.twentyOnePlusThreeResult { SideBetToastView( title: "21+3", result: topResult.displayName, isWin: topResult.isWin, amount: topResult.isWin ? state.twentyOnePlusThreeBet * topResult.payout : -state.twentyOnePlusThreeBet, showOnLeft: false ) } } .padding(.horizontal, Design.Spacing.small) } } .onChange(of: state.currentHint) { oldHint, newHint in // Show toast when a new hint appears if let hint = newHint, hint != oldHint { showHintToastWithTimer(state: state) } else if newHint == nil { // Hide immediately when no hint state.hintDisplayID = UUID() // Invalidate any pending dismiss tasks withAnimation(.spring(duration: Design.Animation.springDuration)) { state.showHintToast = false } } } .onChange(of: state.playerHands.count) { _, _ in // Show hint when hands are added (split occurred) if state.currentHint != nil { showHintToastWithTimer(state: state) } } .onChange(of: state.activeHandIndex) { _, _ in // Show hint when active hand changes (moved to next hand after split) if state.currentHint != nil { showHintToastWithTimer(state: state) } } .onChange(of: state.activeHand?.cards.count) { _, newCount in // Show hint when a card is added to the active hand (after hit) guard let count = newCount, count > 2, state.currentHint != nil else { return } // Small delay to let card animation settle before showing hint Task { @MainActor in try? await Task.sleep(for: .milliseconds(250)) if state.currentHint != nil { showHintToastWithTimer(state: state) } } } .padding(.bottom, 5) .transition(.opacity) .debugBorder(showDebugBorders, color: .green, label: "Player") } // Betting zone (when betting) if state.currentPhase == .betting { Spacer() .debugBorder(showDebugBorders, color: .yellow, label: "Spacer2") BettingZoneView( state: state, selectedChip: selectedChip ) .transition(.scale.combined(with: .opacity)) .debugBorder(showDebugBorders, color: .blue, label: "BetZone") // Betting hint based on count (only when card counting enabled) if let hint = state.bettingHint { BlackjackBettingHintView(hint: hint, trueCount: state.engine.trueCount) .transition(.opacity) .padding(.vertical, 10) .debugBorder(showDebugBorders, color: .purple, label: "BetHint") } } } .padding(.horizontal, Design.Spacing.large) .debugBorder(showDebugBorders, color: .white, label: "TableView") .animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase) } } // MARK: - Previews #Preview { ZStack { Color.Table.felt.ignoresSafeArea() Text("Use GameTableView for full preview") .foregroundStyle(.white) } }