diff --git a/Baccarat/Baccarat/Resources/Localizable.xcstrings b/Baccarat/Baccarat/Resources/Localizable.xcstrings index 9f09dc7..68062db 100644 --- a/Baccarat/Baccarat/Resources/Localizable.xcstrings +++ b/Baccarat/Baccarat/Resources/Localizable.xcstrings @@ -4443,10 +4443,6 @@ } } }, - "TAP" : { - "comment" : "Text displayed as an overlay on the card during the interactive reveal state, instructing the user to tap.", - "isCommentAutoGenerated" : true - }, "Tap Deal to start the round" : { "comment" : "Instructional text for new players.", "extractionState" : "stale", diff --git a/Baccarat/Baccarat/Views/Game/GameTableView.swift b/Baccarat/Baccarat/Views/Game/GameTableView.swift index b1ebf46..f3c7b9d 100644 --- a/Baccarat/Baccarat/Views/Game/GameTableView.swift +++ b/Baccarat/Baccarat/Views/Game/GameTableView.swift @@ -312,6 +312,7 @@ struct GameTableView: View, SherpaDelegate { bettedOnPlayer: state.bettedOnPlayer, isDealing: isDealing, screenSize: screenSize, + totalBetAmount: state.totalBetAmount, onReveal: { state.revealCurrentCard() }, onUpdateProgress: { state.updateRevealProgress($0) } ) @@ -321,28 +322,28 @@ struct GameTableView: View, SherpaDelegate { Spacer(minLength: 0) - // Betting table - BettingTableView( - gameState: state, - selectedChip: selectedChip - ) - .frame(maxWidth: maxContentWidth) - .padding(.horizontal, Design.Spacing.medium) - .opacity(isDealing ? (state.isWaitingForReveal ? 0.1 : 0.3) : 1.0) - .blur(radius: isDealing ? (state.isWaitingForReveal ? 3 : 1.5) : 0) - .scaleEffect(isDealing && state.isWaitingForReveal ? 0.95 : 1.0) - .debugBorder(showDebugBorders, color: .blue, label: "BetTable") - - // Betting hint (static, below table, above chips) - if let hintInfo = state.currentHintInfo { - BettingHintView( - hint: hintInfo.text, - secondaryInfo: hintInfo.secondaryText, - style: hintInfo.style + // Betting table - completely hidden during dealing + if !isDealing { + BettingTableView( + gameState: state, + selectedChip: selectedChip ) + .frame(maxWidth: maxContentWidth) + .padding(.horizontal, Design.Spacing.medium) .transition(.opacity) - .padding(.vertical, Design.Spacing.small) - .debugBorder(showDebugBorders, color: .purple, label: "Hint") + .debugBorder(showDebugBorders, color: .blue, label: "BetTable") + + // Betting hint (static, below table, above chips) + if let hintInfo = state.currentHintInfo { + BettingHintView( + hint: hintInfo.text, + secondaryInfo: hintInfo.secondaryText, + style: hintInfo.style + ) + .transition(.opacity) + .padding(.vertical, Design.Spacing.small) + .debugBorder(showDebugBorders, color: .purple, label: "Hint") + } } Spacer(minLength: Design.Spacing.xSmall) @@ -424,6 +425,7 @@ struct GameTableView: View, SherpaDelegate { bettedOnPlayer: state.bettedOnPlayer, isDealing: isDealing, screenSize: screenSize, + totalBetAmount: state.totalBetAmount, onReveal: { state.revealCurrentCard() }, onUpdateProgress: { state.updateRevealProgress($0) } ) @@ -446,28 +448,28 @@ struct GameTableView: View, SherpaDelegate { .debugBorder(showDebugBorders, color: .yellow, label: "Spacer3") } - // Betting table - BettingTableView( - gameState: state, - selectedChip: selectedChip - ) - .frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity) - .padding(.horizontal, Design.Spacing.medium) - .opacity(isDealing ? (state.isWaitingForReveal ? 0.1 : 0.3) : 1.0) - .blur(radius: isDealing ? (state.isWaitingForReveal ? 3 : 1.5) : 0) - .scaleEffect(isDealing && state.isWaitingForReveal ? 0.95 : 1.0) - .debugBorder(showDebugBorders, color: .blue, label: "BetTable") - - // Betting hint (static, below table, above chips) - if let hintInfo = state.currentHintInfo { - BettingHintView( - hint: hintInfo.text, - secondaryInfo: hintInfo.secondaryText, - style: hintInfo.style + // Betting table - completely hidden during dealing + if !isDealing { + BettingTableView( + gameState: state, + selectedChip: selectedChip ) + .frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity) + .padding(.horizontal, Design.Spacing.medium) .transition(.opacity) - .padding(.vertical, Design.Spacing.small) - .debugBorder(showDebugBorders, color: .purple, label: "Hint") + .debugBorder(showDebugBorders, color: .blue, label: "BetTable") + + // Betting hint (static, below table, above chips) + if let hintInfo = state.currentHintInfo { + BettingHintView( + hint: hintInfo.text, + secondaryInfo: hintInfo.secondaryText, + style: hintInfo.style + ) + .transition(.opacity) + .padding(.vertical, Design.Spacing.small) + .debugBorder(showDebugBorders, color: .purple, label: "Hint") + } } // Chip selector - only shown during betting phase diff --git a/Baccarat/Baccarat/Views/Table/CardsDisplayArea.swift b/Baccarat/Baccarat/Views/Table/CardsDisplayArea.swift index cab7fd2..46077f3 100644 --- a/Baccarat/Baccarat/Views/Table/CardsDisplayArea.swift +++ b/Baccarat/Baccarat/Views/Table/CardsDisplayArea.swift @@ -34,6 +34,8 @@ struct CardsDisplayArea: View { let isDealing: Bool /// Full screen size for calculating card width in dealing mode. let screenSize: CGSize + /// Total bet amount to display below the bottom hand during dealing. + let totalBetAmount: Int let onReveal: () -> Void let onUpdateProgress: (Double) -> Void @@ -164,6 +166,9 @@ struct CardsDisplayArea: View { playerHandSection(width: handSectionWidth) .matchedGeometryEffect(id: "player", in: animation) + + // Bet amount below the bottom hand + betAmountDisplay } else { playerHandSection(width: handSectionWidth) .matchedGeometryEffect(id: "player", in: animation) @@ -173,6 +178,9 @@ struct CardsDisplayArea: View { bankerHandSection(width: handSectionWidth) .matchedGeometryEffect(id: "banker", in: animation) + + // Bet amount below the bottom hand + betAmountDisplay } } } else { @@ -199,18 +207,39 @@ struct CardsDisplayArea: View { } } ) + // Only show background in betting phase (horizontal layout) .background( - RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge) - .fill(Color.black.opacity(isWaitingForReveal ? 0.05 : Design.Opacity.quarter)) - .accessibilityHidden(true) + Group { + if !isDealing { + RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge) + .fill(Color.black.opacity(Design.Opacity.quarter)) + .accessibilityHidden(true) + } + } ) .debugBorder(showDebugBorders, color: .mint, label: "HandsContainer") - .animation(.spring(duration: 0.6, bounce: 0.2), value: isDealing) - .animation(.spring(duration: 0.5, bounce: 0.15), value: playerOnBottom) + .animation(.easeOut(duration: 0.5), value: isDealing) + .animation(.easeOut(duration: 0.4), value: playerOnBottom) } // MARK: - Private Views + /// Bet amount display shown below the bottom hand during dealing. + @ViewBuilder + private var betAmountDisplay: some View { + if totalBetAmount > 0 { + HStack(spacing: Design.Spacing.xSmall) { + Image(systemName: "dollarsign.circle.fill") + .font(.system(size: Design.BaseFontSize.xLarge)) + .foregroundStyle(.yellow) + Text("\(totalBetAmount)") + .font(.system(size: Design.BaseFontSize.medium, weight: .bold, design: .rounded)) + .foregroundStyle(.yellow) + } + .padding(.top, Design.Spacing.medium) + } + } + private func playerHandSection(width: CGFloat) -> some View { // Calculate value from face-up cards let visibleValue: Int = { @@ -336,6 +365,7 @@ struct CardsDisplayArea: View { bettedOnPlayer: nil, isDealing: false, screenSize: CGSize(width: 400, height: 800), + totalBetAmount: 0, onReveal: {}, onUpdateProgress: { _ in } ) @@ -370,6 +400,7 @@ struct CardsDisplayArea: View { bettedOnPlayer: true, isDealing: true, screenSize: CGSize(width: 400, height: 800), + totalBetAmount: 150, onReveal: {}, onUpdateProgress: { _ in } ) @@ -406,6 +437,7 @@ struct CardsDisplayArea: View { bettedOnPlayer: false, isDealing: true, screenSize: CGSize(width: 400, height: 800), + totalBetAmount: 250, onReveal: {}, onUpdateProgress: { _ in } ) diff --git a/Baccarat/Baccarat/Views/Table/CompactHandView.swift b/Baccarat/Baccarat/Views/Table/CompactHandView.swift index 5fd6311..514a0d9 100644 --- a/Baccarat/Baccarat/Views/Table/CompactHandView.swift +++ b/Baccarat/Baccarat/Views/Table/CompactHandView.swift @@ -148,6 +148,7 @@ struct CompactHandView: View { } .frame(height: cardHeight) .frame(maxWidth: .infinity) + .padding(.vertical, isWinner ? Design.Spacing.small : 0) .background(winnerBorder) .overlay(alignment: .bottom) { winBadge @@ -184,6 +185,7 @@ struct CompactHandView: View { revealStyle: revealStyle, isWaiting: isWaitingForReveal && currentRevealIndex == revealIndex, progress: currentRevealIndex == revealIndex ? revealProgress : 0.0, + isBottomHand: isBottom, onReveal: onReveal, onUpdateProgress: onUpdateProgress ) diff --git a/Baccarat/Baccarat/Views/Table/InteractiveCardView.swift b/Baccarat/Baccarat/Views/Table/InteractiveCardView.swift index fd5e064..ecaf97c 100644 --- a/Baccarat/Baccarat/Views/Table/InteractiveCardView.swift +++ b/Baccarat/Baccarat/Views/Table/InteractiveCardView.swift @@ -15,9 +15,19 @@ struct InteractiveCardView: View { let revealStyle: RevealStyle let isWaiting: Bool let progress: Double + /// Whether this card is in the bottom (Home) hand - used for glow effect + let isBottomHand: Bool let onReveal: () -> Void let onUpdateProgress: (Double) -> Void + /// Whether to show the glowing animation around this card + private var showGlow: Bool { + isWaiting && !isFaceUp && isBottomHand && revealStyle != .auto + } + + /// Animation state for the pulsing glow + @State private var glowPulse = false + var body: some View { ZStack { if revealStyle == .squeeze && !isFaceUp && isWaiting { @@ -26,14 +36,11 @@ struct InteractiveCardView: View { width: cardWidth, onReveal: onReveal ) + .modifier(GlowModifier(isActive: showGlow, glowPulse: glowPulse, cardWidth: cardWidth)) } else { // Standard CardView (handles its own flip animation) CardView(card: card, isFaceUp: isFaceUp, cardWidth: cardWidth) - .overlay { - if !isFaceUp && isWaiting && revealStyle == .tap { - revealInstructionOverlay(text: String(localized: "TAP")) - } - } + .modifier(GlowModifier(isActive: showGlow, glowPulse: glowPulse, cardWidth: cardWidth)) .onTapGesture { if !isFaceUp && isWaiting && revealStyle == .tap { onReveal() @@ -41,25 +48,68 @@ struct InteractiveCardView: View { } } } - } - - private func revealInstructionOverlay(text: String) -> some View { - Text(text) - .font(.system(size: Design.BaseFontSize.small, weight: .black, design: .rounded)) - .foregroundStyle(.white) - .padding(.horizontal, Design.Spacing.small) - .padding(.vertical, Design.Spacing.xxSmall) - .background( - Capsule() - .fill(Color.black.opacity(Design.Opacity.heavy)) - .overlay( - Capsule() - .strokeBorder(.white.opacity(Design.Opacity.medium), lineWidth: 1) - ) - ) - .scaleEffect(isWaiting ? 1.0 : 0.8) - .opacity(isWaiting ? 1.0 : 0) - .animation(.interactiveSpring().repeatForever(), value: isWaiting) - .allowsHitTesting(false) + .onAppear { + if showGlow { + glowPulse = true + } + } + .onChange(of: showGlow) { _, newValue in + glowPulse = newValue + } + } +} + +// MARK: - Glow Modifier + +/// A view modifier that adds a pulsing glow effect around a card. +private struct GlowModifier: ViewModifier { + let isActive: Bool + let glowPulse: Bool + let cardWidth: CGFloat + + /// Corner radius matching CasinoKit's CardView + private var cornerRadius: CGFloat { + cardWidth * 0.08 + } + + func body(content: Content) -> some View { + if isActive { + content + .overlay { + // Glow effect using multiple stroke layers + ZStack { + // Outer glow strokes (largest to smallest for layered glow) + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(Color.orange.opacity(glowPulse ? 0.6 : 0.2), lineWidth: glowPulse ? 20 : 10) + .blur(radius: 10) + + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(Color.yellow.opacity(glowPulse ? 0.8 : 0.3), lineWidth: glowPulse ? 12 : 6) + .blur(radius: 6) + + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(Color.yellow.opacity(glowPulse ? 1.0 : 0.5), lineWidth: glowPulse ? 6 : 3) + .blur(radius: 3) + + // Bright animated border (sharp, on top) + RoundedRectangle(cornerRadius: cornerRadius) + .strokeBorder( + LinearGradient( + colors: [.yellow, .white, .yellow], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: glowPulse ? 4 : 3 + ) + } + } + .scaleEffect(glowPulse ? 1.02 : 1.0) + .animation( + .easeInOut(duration: 0.7).repeatForever(autoreverses: true), + value: glowPulse + ) + } else { + content + } } } diff --git a/Baccarat/README.md b/Baccarat/README.md index 44c297a..4f1a4a9 100644 --- a/Baccarat/README.md +++ b/Baccarat/README.md @@ -21,6 +21,11 @@ A feature-rich Baccarat (Punto Banco) app for iOS built with SwiftUI. Experience - Animated card dealing with sound effects and haptics - Automatic shoe reshuffling with burn card +### ⚙️ Settings & Customization +- Dealing animations toggle and adjustable dealing speed +- Card reveal style: Auto (instant flip), Tap (per-card), Squeeze (peek/drag reveal) +- Persistent preferences via iCloud sync + ### 💰 Betting Options #### Main Bets