diff --git a/Baccarat/Baccarat.xcodeproj/project.pbxproj b/Baccarat/Baccarat.xcodeproj/project.pbxproj index 4c670fb..19a6905 100644 --- a/Baccarat/Baccarat.xcodeproj/project.pbxproj +++ b/Baccarat/Baccarat.xcodeproj/project.pbxproj @@ -34,22 +34,9 @@ EAD890CE2EF1E9CF006DBA80 /* BaccaratUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BaccaratUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ -/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - EAD891062EF1F51E006DBA80 /* Exceptions for "Baccarat" folder in "Baccarat" target */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - Agents.md, - ); - target = EAD890B62EF1E9CE006DBA80 /* Baccarat */; - }; -/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ - /* Begin PBXFileSystemSynchronizedRootGroup section */ EAD890B92EF1E9CE006DBA80 /* Baccarat */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - EAD891062EF1F51E006DBA80 /* Exceptions for "Baccarat" folder in "Baccarat" target */, - ); path = Baccarat; sourceTree = ""; }; diff --git a/Baccarat/Baccarat/Theme/DesignConstants.swift b/Baccarat/Baccarat/Theme/DesignConstants.swift index ab3d9ba..fc95199 100644 --- a/Baccarat/Baccarat/Theme/DesignConstants.swift +++ b/Baccarat/Baccarat/Theme/DesignConstants.swift @@ -29,15 +29,50 @@ enum Design { // MARK: - Baccarat-Specific Component Sizes enum Size { - // Cards - use CasinoDesign values + // MARK: - Hand Scaling + + /// Hand scaling factor for cards and related elements. + /// 1.0 = original size, 1.5 = 50% larger, 2.0 = double size. + /// Adjust this value to change card sizes across the app. + static let handScale: CGFloat = 1.5 + + /// Scale multiplier for small screens (iPhone SE, etc). + /// Applied instead of handScale on screens narrower than smallScreenThreshold. + static let smallScreenScale: CGFloat = 1.2 + + /// Screen width threshold for small screen detection (iPhone SE is 375pt) + static let smallScreenThreshold: CGFloat = 390 + + /// Additional scale multiplier for large screens (iPad). + /// Applied on top of handScale when on regular size class. + static let largeScreenMultiplier: CGFloat = 1.2 + + // Cards - base values from CasinoDesign static let cardWidthSmall: CGFloat = CasinoDesign.Size.cardWidthSmall static let cardWidthMedium: CGFloat = CasinoDesign.Size.cardWidthMedium static let cardWidthLarge: CGFloat = CasinoDesign.Size.cardWidthLarge static let cardAspectRatio: CGFloat = CasinoDesign.Size.cardAspectRatio - static let cardOverlap: CGFloat = CasinoDesign.Size.cardOverlap - // Baccarat table cards (smaller for compact layout) - static let cardWidthTable: CGFloat = 45 + /// Base card width before scaling (for reference) + private static let cardWidthTableBase: CGFloat = 45 + + /// Card overlap scaled with hand size (standard iPhone) + static let cardOverlap: CGFloat = CasinoDesign.Size.cardOverlap * handScale + + /// Card overlap for small screens + static let cardOverlapSmall: CGFloat = CasinoDesign.Size.cardOverlap * smallScreenScale + + // Baccarat table cards - scaled for better visibility (standard iPhone) + static let cardWidthTable: CGFloat = cardWidthTableBase * handScale + + /// Card width for small screens (iPhone SE, etc) + static let cardWidthTableSmall: CGFloat = cardWidthTableBase * smallScreenScale + + /// Card width for large screens (iPad) - applies additional multiplier + static let cardWidthTableLarge: CGFloat = cardWidthTableBase * handScale * largeScreenMultiplier + + /// Card overlap for large screens + static let cardOverlapLarge: CGFloat = CasinoDesign.Size.cardOverlap * handScale * largeScreenMultiplier // Chips - use CasinoDesign values static let chipSmall: CGFloat = CasinoDesign.Size.chipSmall diff --git a/Baccarat/Baccarat/Views/GameTableView.swift b/Baccarat/Baccarat/Views/GameTableView.swift index a43fc8e..0dcb260 100644 --- a/Baccarat/Baccarat/Views/GameTableView.swift +++ b/Baccarat/Baccarat/Views/GameTableView.swift @@ -52,38 +52,40 @@ struct GameTableView: View { } var body: some View { - ZStack { - // Table background - TableBackgroundView() - - // Main content - VStack(spacing: 0) { - // Top bar with balance and info - TopBarView( - balance: state.balance, - cardsRemaining: state.engine.shoe.cardsRemaining, - showCardsRemaining: settings.showCardsRemaining, - onReset: { state.resetGame() }, - onSettings: { showSettings = true }, - onHelp: { showRules = true }, - onStats: { showStats = true } - ) + GeometryReader { geometry in + ZStack { + // Table background + TableBackgroundView() - Spacer(minLength: Design.Spacing.xSmall) - - // Cards display area - constrained width on iPad - CardsDisplayArea( - playerCards: state.visiblePlayerCards, - bankerCards: state.visibleBankerCards, - playerCardsFaceUp: state.playerCardsFaceUp, - bankerCardsFaceUp: state.bankerCardsFaceUp, - playerValue: state.playerHandValue, - bankerValue: state.bankerHandValue, - playerIsWinner: playerIsWinner, - bankerIsWinner: bankerIsWinner, - isTie: isTie - ) - .frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity) + // Main content + VStack(spacing: 0) { + // Top bar with balance and info + TopBarView( + balance: state.balance, + cardsRemaining: state.engine.shoe.cardsRemaining, + showCardsRemaining: settings.showCardsRemaining, + onReset: { state.resetGame() }, + onSettings: { showSettings = true }, + onHelp: { showRules = true }, + onStats: { showStats = true } + ) + + Spacer(minLength: Design.Spacing.xSmall) + + // Cards display area - constrained width on iPad + CardsDisplayArea( + playerCards: state.visiblePlayerCards, + bankerCards: state.visibleBankerCards, + playerCardsFaceUp: state.playerCardsFaceUp, + bankerCardsFaceUp: state.bankerCardsFaceUp, + playerValue: state.playerHandValue, + bankerValue: state.bankerHandValue, + playerIsWinner: playerIsWinner, + bankerIsWinner: bankerIsWinner, + isTie: isTie, + screenWidth: geometry.size.width + ) + .frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity) Spacer(minLength: Design.Spacing.xSmall) @@ -134,37 +136,38 @@ struct GameTableView: View { } .safeAreaPadding(.bottom) - // Result banner overlay (handles its own iPad sizing) - if state.showResultBanner, let result = state.lastResult { - ResultBannerView( - result: result, - totalWinnings: state.lastWinnings, - betResults: state.betResults, - playerHadPair: state.playerHadPair, - bankerHadPair: state.bankerHadPair, - currentBalance: state.balance, - minBet: state.minBet, - onNewRound: { state.newRound() }, - onGameOver: { - // Reset game (sound already played when banner appeared) - state.resetGame() + // Result banner overlay (handles its own iPad sizing) + if state.showResultBanner, let result = state.lastResult { + ResultBannerView( + result: result, + totalWinnings: state.lastWinnings, + betResults: state.betResults, + playerHadPair: state.playerHadPair, + bankerHadPair: state.bankerHadPair, + currentBalance: state.balance, + minBet: state.minBet, + onNewRound: { state.newRound() }, + onGameOver: { + // Reset game (sound already played when banner appeared) + state.resetGame() + } + ) + .transition(.opacity) + + // Confetti for wins + if state.lastWinnings > 0 { + ConfettiView() } - ) - .transition(.opacity) - - // Confetti for wins - if state.lastWinnings > 0 { - ConfettiView() } - } - - // Game Over overlay (handles its own iPad sizing) - if state.balance == 0 && state.currentBets.isEmpty && !state.isAnimating { - GameOverView( - roundsPlayed: state.roundHistory.count, - onPlayAgain: { state.resetGame() } - ) - .transition(.opacity) + + // Game Over overlay (handles its own iPad sizing) + if state.balance == 0 && state.currentBets.isEmpty && !state.isAnimating { + GameOverView( + roundsPlayed: state.roundHistory.count, + onPlayAgain: { state.resetGame() } + ) + .transition(.opacity) + } } } .onAppear { @@ -347,11 +350,27 @@ struct CardsDisplayArea: View { let playerIsWinner: Bool let bankerIsWinner: Bool let isTie: Bool + /// Screen width for responsive card sizing + var screenWidth: CGFloat = 400 - // MARK: - Fixed font sizes for card area - // Fixed because the card display has strict layout constraints + // MARK: - Environment - private let labelFontSize: CGFloat = 14 + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + // MARK: - Scaled font sizes for card area + // Scales with hand size for proportional appearance + + /// Whether we're on a large screen (iPad) + private var isLargeScreen: Bool { + horizontalSizeClass == .regular + } + + /// Label font size - only scales on iPad to avoid clipping on small iPhones + private var labelFontSize: CGFloat { + let baseSize: CGFloat = 14 + // Only apply scaling on large screens; keep original size on iPhone + return isLargeScreen ? baseSize * Design.Size.handScale * Design.Size.largeScreenMultiplier : baseSize + } // MARK: - Accessibility @@ -387,8 +406,25 @@ struct CardsDisplayArea: View { return visibleCards.joined(separator: ", ") + ". " + String(format: format, bankerValue) } + /// Minimum height for label row - only scales on iPad + private var labelRowMinHeight: CGFloat { + let baseHeight: CGFloat = 30 + // Only apply scaling on large screens; keep original size on iPhone + return isLargeScreen ? baseHeight * Design.Size.handScale * Design.Size.largeScreenMultiplier : baseHeight + } + + /// Spacing between PLAYER and BANKER hands - reduced on smaller screens + private var handsSpacing: CGFloat { + isLargeScreen ? Design.Spacing.xxxLarge : Design.Spacing.medium + } + + /// Horizontal padding inside the container - reduced on smaller screens + private var containerPaddingH: CGFloat { + isLargeScreen ? Design.Spacing.xLarge : Design.Spacing.small + } + var body: some View { - HStack(spacing: Design.Spacing.xxxLarge) { + HStack(spacing: handsSpacing) { // Player side VStack(spacing: Design.Spacing.small) { // Label with value @@ -401,13 +437,14 @@ struct CardsDisplayArea: View { ValueBadge(value: playerValue, color: .blue) } } - .frame(minHeight: 30) + .frame(minHeight: labelRowMinHeight) // Cards CompactHandView( cards: playerCards, cardsFaceUp: playerCardsFaceUp, - isWinner: playerIsWinner + isWinner: playerIsWinner, + screenWidth: screenWidth ) } .accessibilityElement(children: .ignore) @@ -426,28 +463,29 @@ struct CardsDisplayArea: View { ValueBadge(value: bankerValue, color: .red) } } - .frame(minHeight: 30) + .frame(minHeight: labelRowMinHeight) // Cards CompactHandView( cards: bankerCards, cardsFaceUp: bankerCardsFaceUp, - isWinner: bankerIsWinner + isWinner: bankerIsWinner, + screenWidth: screenWidth ) } .accessibilityElement(children: .ignore) .accessibilityLabel(String(localized: "Banker hand")) .accessibilityValue(bankerHandDescription + (bankerIsWinner ? ", " + String(localized: "Winner") : "")) } - .padding(.top, Design.Spacing.large) - .padding(.bottom, Design.Spacing.xLarge) - .padding(.horizontal, Design.Spacing.xLarge) + .padding(.top, Design.Spacing.medium) + .padding(.bottom, Design.Spacing.large) + .padding(.horizontal, containerPaddingH) .background( RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge) .fill(Color.black.opacity(Design.Opacity.quarter)) .accessibilityHidden(true) ) - .padding(.horizontal) + .padding(.horizontal, Design.Spacing.small) } } @@ -456,17 +494,59 @@ struct CompactHandView: View { let cards: [Card] let cardsFaceUp: [Bool] let isWinner: Bool + /// Screen width passed from parent for responsive sizing + var screenWidth: CGFloat = 400 - // MARK: - Scaled Font Sizes (Dynamic Type) + // MARK: - Environment - @ScaledMetric(relativeTo: .caption) private var winBadgeFontSize: CGFloat = 10 + @Environment(\.horizontalSizeClass) private var horizontalSizeClass // MARK: - Layout Constants - // Fixed size: cards have strict visual constraints + // Responsive sizing based on device + + /// Whether we're on a large screen (iPad) + private var isLargeScreen: Bool { + horizontalSizeClass == .regular + } + + /// Whether we're on a small screen (iPhone SE, etc) + private var isSmallScreen: Bool { + !isLargeScreen && screenWidth < Design.Size.smallScreenThreshold + } + + /// WIN badge font size - only scales on iPad + private var winBadgeFontSize: CGFloat { + let baseSize: CGFloat = 10 + return isLargeScreen ? baseSize * Design.Size.handScale * Design.Size.largeScreenMultiplier : baseSize + } + + /// Card width - responsive based on screen size + private var cardWidth: CGFloat { + if isLargeScreen { + return Design.Size.cardWidthTableLarge + } else if isSmallScreen { + return Design.Size.cardWidthTableSmall + } else { + return Design.Size.cardWidthTable + } + } + + /// Card height based on aspect ratio + private var cardHeight: CGFloat { + cardWidth * Design.Size.cardAspectRatio + } + + /// Card overlap - scaled with card size + private var cardOverlap: CGFloat { + if isLargeScreen { + return Design.Size.cardOverlapLarge + } else if isSmallScreen { + return Design.Size.cardOverlapSmall + } else { + return Design.Size.cardOverlap + } + } - private let cardWidth: CGFloat = Design.Size.cardWidthTable - private let cardHeight: CGFloat = Design.Size.cardWidthTable * Design.Size.cardAspectRatio - private let cardOverlap: CGFloat = Design.Size.cardOverlap private let placeholderSpacing: CGFloat = Design.Spacing.small /// Fixed container width to prevent resizing during deal @@ -538,10 +618,27 @@ struct ValueBadge: View { let value: Int let color: Color + // MARK: - Environment + + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + // MARK: - Scaled Font Sizes (Dynamic Type) - @ScaledMetric(relativeTo: .headline) private var valueFontSize: CGFloat = 15 - @ScaledMetric(relativeTo: .headline) private var badgeSize: CGFloat = 26 + /// Whether we're on a large screen (iPad) + private var isLargeScreen: Bool { + horizontalSizeClass == .regular + } + + /// Scale factor for badge sizing - only applies on iPad to avoid clipping on iPhone + private var scale: CGFloat { + isLargeScreen ? Design.Size.handScale * Design.Size.largeScreenMultiplier : 1.0 + } + + @ScaledMetric(relativeTo: .headline) private var baseValueFontSize: CGFloat = 15 + @ScaledMetric(relativeTo: .headline) private var baseBadgeSize: CGFloat = 26 + + private var valueFontSize: CGFloat { baseValueFontSize * scale } + private var badgeSize: CGFloat { baseBadgeSize * scale } var body: some View { Text("\(value)")