diff --git a/Baccarat/Baccarat/Engine/GameState.swift b/Baccarat/Baccarat/Engine/GameState.swift index 567a6cf..ebe0562 100644 --- a/Baccarat/Baccarat/Engine/GameState.swift +++ b/Baccarat/Baccarat/Engine/GameState.swift @@ -610,8 +610,14 @@ final class GameState { bankerHadPair = false betResults = [] - // Deal initial cards + // Change to dealing phase - triggers layout animation (horizontal to vertical) currentPhase = .dealingInitial + + // Wait for layout animation to complete before dealing cards + if settings.showAnimations { + try? await Task.sleep(for: .seconds(1)) + } + let initialCards = engine.dealInitialCards() // Check if animations are enabled diff --git a/Baccarat/Baccarat/Views/Game/GameTableView.swift b/Baccarat/Baccarat/Views/Game/GameTableView.swift index 6368141..c871d78 100644 --- a/Baccarat/Baccarat/Views/Game/GameTableView.swift +++ b/Baccarat/Baccarat/Views/Game/GameTableView.swift @@ -17,6 +17,9 @@ struct GameTableView: View { @State private var showRules = false @State private var showStats = false + /// Screen size for card sizing (measured from TableBackgroundView) + @State private var screenSize: CGSize = CGSize(width: 375, height: 667) + @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.verticalSizeClass) private var verticalSizeClass @@ -76,8 +79,13 @@ struct GameTableView: View { var body: some View { GeometryReader { geometry in ZStack { - // Table background (from CasinoKit) + // Table background - measures screen size for card sizing TableBackgroundView() + .onGeometryChange(for: CGSize.self) { proxy in + proxy.size + } action: { size in + screenSize = size + } // Main content mainContent(geometry: geometry) @@ -192,7 +200,8 @@ struct GameTableView: View { showAnimations: settings.showAnimations, dealingSpeed: settings.dealingSpeed, bettedOnPlayer: state.bettedOnPlayer, - isDealing: isDealing + isDealing: isDealing, + screenSize: screenSize ) .frame(maxWidth: maxContentWidth) .padding(.horizontal, Design.Spacing.medium) @@ -289,7 +298,8 @@ struct GameTableView: View { showAnimations: settings.showAnimations, dealingSpeed: settings.dealingSpeed, bettedOnPlayer: state.bettedOnPlayer, - isDealing: isDealing + isDealing: isDealing, + screenSize: screenSize ) .frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity) .padding(.horizontal, Design.Spacing.medium) diff --git a/Baccarat/Baccarat/Views/Table/CardsDisplayArea.swift b/Baccarat/Baccarat/Views/Table/CardsDisplayArea.swift index 06ddfbe..58202e0 100644 --- a/Baccarat/Baccarat/Views/Table/CardsDisplayArea.swift +++ b/Baccarat/Baccarat/Views/Table/CardsDisplayArea.swift @@ -3,8 +3,7 @@ // Baccarat // // The cards display area showing both Player and Banker hands. -// Defaults to side-by-side during betting, animates to vertical during dealing -// with the betted hand on bottom. +// Animates from side-by-side (betting) to vertical stack (dealing). // import SwiftUI @@ -12,7 +11,7 @@ import CasinoKit /// The cards display area showing both hands. /// - Betting phase: Horizontal side-by-side layout (Player | Banker) -/// - Dealing/Result phases: Vertical layout with betted hand on bottom +/// - Dealing phase: Vertical stack with betted hand on bottom struct CardsDisplayArea: View { let playerCards: [Card] let bankerCards: [Card] @@ -26,39 +25,48 @@ struct CardsDisplayArea: View { let showAnimations: Bool let dealingSpeed: Double /// Which main bet is placed - nil if no main bet, true if Player, false if Banker. - /// Determines layout ordering in vertical mode: betted hand appears on bottom. let bettedOnPlayer: Bool? /// Whether the game is in dealing/result phase (vertical layout) or betting phase (horizontal). let isDealing: Bool + /// Full screen size for calculating card width in dealing mode. + let screenSize: CGSize // MARK: - State @State private var containerWidth: CGFloat = 300 + @Namespace private var animation // MARK: - Environment @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @Environment(\.verticalSizeClass) private var verticalSizeClass // MARK: - Computed Properties - /// Whether we're on a large screen (iPad) private var isLargeScreen: Bool { horizontalSizeClass == .regular } - // Use global debug flag from Design constants + /// Whether we're in landscape mode + private var isLandscape: Bool { + verticalSizeClass == .compact || (isLargeScreen && screenSize.width > screenSize.height) + } + private var showDebugBorders: Bool { Design.showDebugBorders } - /// Label font size - larger on iPad private var labelFontSize: CGFloat { isLargeScreen ? 18 : Design.Size.labelFontSize } - /// Minimum height for label row - larger on iPad private var labelRowMinHeight: CGFloat { isLargeScreen ? 40 : Design.Size.labelRowHeight } + /// Whether Player hand should be on bottom in vertical mode. + private var playerOnBottom: Bool { + bettedOnPlayer ?? true + } + /// Spacing between hands private var handsSpacing: CGFloat { if isDealing { @@ -68,13 +76,35 @@ struct CardsDisplayArea: View { } } - /// Card section width - larger in vertical mode (more horizontal space) + // MARK: - Card Width Calculations + + /// Card section width for horizontal (betting) mode + private var horizontalHandWidth: CGFloat { + let spacing = isLargeScreen ? Design.Spacing.xxxLarge : Design.Spacing.large + return max(100, (containerWidth - spacing) / 2) + } + + /// Card section width for vertical (dealing) mode - matches Blackjack sizing + /// Uses screen height as base (like Blackjack), which is smaller in landscape + private var verticalHandWidth: CGFloat { + // Use screen height (smaller dimension in landscape = smaller cards) + let height = screenSize.height + guard height > 100 else { return horizontalHandWidth } + + // Blackjack uses 0.18, but in landscape we may need slightly smaller + let percentage: CGFloat = isLandscape ? 0.14 : 0.18 + let cardWidth = height * percentage + + // CompactHandView: cardWidth = containerWidth / divisor + let overlapRatio: CGFloat = -0.45 + let maxCards: CGFloat = 3 + let divisor = 1 + (maxCards - 1) * (1 + overlapRatio) + return cardWidth * divisor + } + + /// Current hand section width based on mode private var handSectionWidth: CGFloat { - if isDealing { - return containerWidth * 0.7 - } else { - return (containerWidth - handsSpacing) / 2 - } + isDealing ? verticalHandWidth : horizontalHandWidth } // MARK: - Accessibility @@ -111,49 +141,35 @@ struct CardsDisplayArea: View { return visibleCards.joined(separator: ", ") + ". " + String(format: format, bankerValue) } - /// Whether Player hand should be on bottom (true) or top (false) in vertical mode. - /// Defaults to Player on bottom if no bet placed. - private var playerOnBottom: Bool { - bettedOnPlayer ?? true - } - - /// The layout to use - HStack for horizontal, VStack for vertical - private var layout: AnyLayout { - isDealing ? AnyLayout(VStackLayout(spacing: handsSpacing)) : AnyLayout(HStackLayout(spacing: handsSpacing)) - } - // MARK: - Body var body: some View { - layout { - // First position: Player in horizontal, or top hand in vertical - if isDealing && !playerOnBottom { - // Vertical mode, player on top - playerHandSection(width: handSectionWidth) - .debugBorder(showDebugBorders, color: .blue, label: "Player") - } else if isDealing && playerOnBottom { - // Vertical mode, banker on top - bankerHandSection(width: handSectionWidth) - .debugBorder(showDebugBorders, color: .red, label: "Banker") + // Use different layouts but keep view identity with matchedGeometryEffect + Group { + if isDealing { + // Vertical layout + VStack(spacing: handsSpacing) { + // Top position + if playerOnBottom { + bankerHandSection(width: handSectionWidth) + .matchedGeometryEffect(id: "banker", in: animation) + playerHandSection(width: handSectionWidth) + .matchedGeometryEffect(id: "player", in: animation) + } else { + playerHandSection(width: handSectionWidth) + .matchedGeometryEffect(id: "player", in: animation) + bankerHandSection(width: handSectionWidth) + .matchedGeometryEffect(id: "banker", in: animation) + } + } } else { - // Horizontal mode - player always on left - playerHandSection(width: handSectionWidth) - .debugBorder(showDebugBorders, color: .blue, label: "Player") - } - - // Second position: Banker in horizontal, or bottom hand in vertical - if isDealing && !playerOnBottom { - // Vertical mode, banker on bottom - bankerHandSection(width: handSectionWidth) - .debugBorder(showDebugBorders, color: .red, label: "Banker") - } else if isDealing && playerOnBottom { - // Vertical mode, player on bottom - playerHandSection(width: handSectionWidth) - .debugBorder(showDebugBorders, color: .blue, label: "Player") - } else { - // Horizontal mode - banker always on right - bankerHandSection(width: handSectionWidth) - .debugBorder(showDebugBorders, color: .red, label: "Banker") + // Horizontal layout - Player left, Banker right + HStack(spacing: handsSpacing) { + playerHandSection(width: handSectionWidth) + .matchedGeometryEffect(id: "player", in: animation) + bankerHandSection(width: handSectionWidth) + .matchedGeometryEffect(id: "banker", in: animation) + } } } .frame(maxWidth: .infinity) @@ -176,15 +192,14 @@ struct CardsDisplayArea: View { .accessibilityHidden(true) ) .debugBorder(showDebugBorders, color: .mint, label: "HandsContainer") - .animation(.spring(duration: 0.5, bounce: 0.2), value: isDealing) - .animation(.spring(duration: 0.4, bounce: 0.15), value: playerOnBottom) + .animation(.spring(duration: 0.6, bounce: 0.2), value: isDealing) + .animation(.spring(duration: 0.5, bounce: 0.15), value: playerOnBottom) } // MARK: - Private Views private func playerHandSection(width: CGFloat) -> some View { VStack(spacing: Design.Spacing.small) { - // Label with value HStack(spacing: Design.Spacing.small) { Text("PLAYER") .font(.system(size: labelFontSize, weight: .bold, design: .rounded)) @@ -196,7 +211,6 @@ struct CardsDisplayArea: View { } .frame(minHeight: labelRowMinHeight) - // Cards CompactHandView( cards: playerCards, cardsFaceUp: playerCardsFaceUp, @@ -214,7 +228,6 @@ struct CardsDisplayArea: View { private func bankerHandSection(width: CGFloat) -> some View { VStack(spacing: Design.Spacing.small) { - // Label with value HStack(spacing: Design.Spacing.small) { Text("BANKER") .font(.system(size: labelFontSize, weight: .bold, design: .rounded)) @@ -226,7 +239,6 @@ struct CardsDisplayArea: View { } .frame(minHeight: labelRowMinHeight) - // Cards CompactHandView( cards: bankerCards, cardsFaceUp: bankerCardsFaceUp, @@ -261,7 +273,8 @@ struct CardsDisplayArea: View { showAnimations: true, dealingSpeed: 1.0, bettedOnPlayer: nil, - isDealing: false + isDealing: false, + screenSize: CGSize(width: 400, height: 800) ) } } @@ -288,7 +301,8 @@ struct CardsDisplayArea: View { showAnimations: true, dealingSpeed: 1.0, bettedOnPlayer: true, - isDealing: true + isDealing: true, + screenSize: CGSize(width: 400, height: 800) ) } } @@ -317,7 +331,8 @@ struct CardsDisplayArea: View { showAnimations: true, dealingSpeed: 1.0, bettedOnPlayer: false, - isDealing: true + isDealing: true, + screenSize: CGSize(width: 400, height: 800) ) } }