diff --git a/Blackjack/Blackjack/Engine/GameState.swift b/Blackjack/Blackjack/Engine/GameState.swift index a18243b..5632127 100644 --- a/Blackjack/Blackjack/Engine/GameState.swift +++ b/Blackjack/Blackjack/Engine/GameState.swift @@ -485,7 +485,11 @@ final class GameState { evaluateSideBets() // Check for insurance offer (only in American style with hole card) - if !settings.noHoleCard, let upCard = dealerUpCard, engine.shouldOfferInsurance(dealerUpCard: upCard) { + // Skip if user has opted out of insurance prompts + if !settings.noHoleCard, + !settings.neverAskInsurance, + let upCard = dealerUpCard, + engine.shouldOfferInsurance(dealerUpCard: upCard) { currentPhase = .insurance return } @@ -567,6 +571,12 @@ final class GameState { } } + /// Declines insurance and sets the "never ask again" preference. + func neverAskInsurance() { + settings.neverAskInsurance = true + declineInsurance() + } + // MARK: - Player Actions /// Player hits (takes another card). diff --git a/Blackjack/Blackjack/Models/GameSettings.swift b/Blackjack/Blackjack/Models/GameSettings.swift index 5234a77..69b6205 100644 --- a/Blackjack/Blackjack/Models/GameSettings.swift +++ b/Blackjack/Blackjack/Models/GameSettings.swift @@ -105,6 +105,9 @@ final class GameSettings { /// Whether insurance is offered. var insuranceAllowed: Bool = true { didSet { save() } } + /// Whether to skip the insurance prompt and auto-decline. + var neverAskInsurance: Bool = false { didSet { save() } } + /// Blackjack payout ratio (1.5 = 3:2, 1.2 = 6:5) var blackjackPayout: Double = 1.5 { didSet { save() } } @@ -237,6 +240,7 @@ final class GameSettings { self.noHoleCard = data.noHoleCard self.blackjackPayout = data.blackjackPayout self.insuranceAllowed = data.insuranceAllowed + self.neverAskInsurance = data.neverAskInsurance self.sideBetsEnabled = data.sideBetsEnabled self.showAnimations = data.showAnimations self.dealingSpeed = data.dealingSpeed @@ -263,6 +267,7 @@ final class GameSettings { noHoleCard: noHoleCard, blackjackPayout: blackjackPayout, insuranceAllowed: insuranceAllowed, + neverAskInsurance: neverAskInsurance, sideBetsEnabled: sideBetsEnabled, showAnimations: showAnimations, dealingSpeed: dealingSpeed, @@ -290,6 +295,7 @@ final class GameSettings { noHoleCard = false blackjackPayout = 1.5 insuranceAllowed = true + neverAskInsurance = false sideBetsEnabled = false showAnimations = true dealingSpeed = 1.0 diff --git a/Blackjack/Blackjack/Resources/Localizable.xcstrings b/Blackjack/Blackjack/Resources/Localizable.xcstrings index 2a62cb9..497fece 100644 --- a/Blackjack/Blackjack/Resources/Localizable.xcstrings +++ b/Blackjack/Blackjack/Resources/Localizable.xcstrings @@ -1057,6 +1057,28 @@ } } }, + "Auto-decline when dealer shows Ace" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auto-decline when dealer shows Ace" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rechazar automáticamente cuando el crupier muestra As" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Refuser automatiquement quand le croupier montre un As" + } + } + } + }, "Baccarat" : { "comment" : "The name of a casino game.", "isCommentAutoGenerated" : true, @@ -2625,6 +2647,28 @@ } } }, + "Don't Ask" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Don't Ask" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "No preguntar" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ne pas demander" + } + } + } + }, "Double" : { "localizations" : { "en" : { @@ -5800,6 +5844,28 @@ } } }, + "Skip Insurance Prompt" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skip Insurance Prompt" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Omitir aviso de seguro" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ignorer l'invitation d'assurance" + } + } + } + }, "Some games: Dealer hits on 'soft 17' (Ace + 6)." : { "comment" : "Description of a rule where the dealer must hit on a 'soft 17' (Ace + 6) in some blackjack games.", "isCommentAutoGenerated" : true, diff --git a/Blackjack/Blackjack/Storage/BlackjackGameData.swift b/Blackjack/Blackjack/Storage/BlackjackGameData.swift index 44ff6c7..fca8fc4 100644 --- a/Blackjack/Blackjack/Storage/BlackjackGameData.swift +++ b/Blackjack/Blackjack/Storage/BlackjackGameData.swift @@ -66,6 +66,7 @@ struct BlackjackSettingsData: PersistableGameData { noHoleCard: false, blackjackPayout: 1.5, insuranceAllowed: true, + neverAskInsurance: false, sideBetsEnabled: false, showAnimations: true, dealingSpeed: 1.0, @@ -90,6 +91,7 @@ struct BlackjackSettingsData: PersistableGameData { var noHoleCard: Bool var blackjackPayout: Double var insuranceAllowed: Bool + var neverAskInsurance: Bool var sideBetsEnabled: Bool var showAnimations: Bool var dealingSpeed: Double diff --git a/Blackjack/Blackjack/Theme/DesignConstants.swift b/Blackjack/Blackjack/Theme/DesignConstants.swift index 48fc592..c87e9ed 100644 --- a/Blackjack/Blackjack/Theme/DesignConstants.swift +++ b/Blackjack/Blackjack/Theme/DesignConstants.swift @@ -121,10 +121,15 @@ enum Design { // MARK: - Card Deal Animation enum DealAnimation { - /// Horizontal offset for card deal (from upper-right, simulating shoe) - static let offsetX: CGFloat = 150 - /// Vertical offset for card deal (from above the table) - static let offsetY: CGFloat = -200 + /// Horizontal offset for dealer cards (shoe is nearby, less horizontal travel) + static let dealerOffsetX: CGFloat = 120 + /// Vertical offset for dealer cards (small since near top) + static let dealerOffsetY: CGFloat = -80 + + /// Horizontal offset for player cards (shoe is far away, more horizontal travel) + static let playerOffsetX: CGFloat = 180 + /// Vertical offset for player cards (large since far from top) + static let playerOffsetY: CGFloat = -350 } } diff --git a/Blackjack/Blackjack/Views/Game/GameTableView.swift b/Blackjack/Blackjack/Views/Game/GameTableView.swift index fdf598e..b63175e 100644 --- a/Blackjack/Blackjack/Views/Game/GameTableView.swift +++ b/Blackjack/Blackjack/Views/Game/GameTableView.swift @@ -186,7 +186,8 @@ struct GameTableView: View { betAmount: state.currentBet / 2, balance: state.balance, onTake: { Task { await state.takeInsurance() } }, - onDecline: { state.declineInsurance() } + onDecline: { state.declineInsurance() }, + onNeverAsk: { state.neverAskInsurance() } ) } .ignoresSafeArea() diff --git a/Blackjack/Blackjack/Views/Sheets/SettingsView.swift b/Blackjack/Blackjack/Views/Sheets/SettingsView.swift index fd68e1a..811fa54 100644 --- a/Blackjack/Blackjack/Views/Sheets/SettingsView.swift +++ b/Blackjack/Blackjack/Views/Sheets/SettingsView.swift @@ -146,6 +146,15 @@ struct SettingsView: View { isOn: $settings.showCardsRemaining, accentColor: accent ) + + Divider().background(Color.white.opacity(Design.Opacity.hint)) + + SettingsToggle( + title: String(localized: "Skip Insurance Prompt"), + subtitle: String(localized: "Auto-decline when dealer shows Ace"), + isOn: $settings.neverAskInsurance, + accentColor: accent + ) } } diff --git a/Blackjack/Blackjack/Views/Table/BlackjackTableView.swift b/Blackjack/Blackjack/Views/Table/BlackjackTableView.swift index 78bd297..96d5f04 100644 --- a/Blackjack/Blackjack/Views/Table/BlackjackTableView.swift +++ b/Blackjack/Blackjack/Views/Table/BlackjackTableView.swift @@ -91,6 +91,8 @@ struct BlackjackTableView: View { hand: state.dealerHand, showHoleCard: state.shouldShowDealerHoleCard, showCardCount: showCardCount, + showAnimations: state.settings.showAnimations, + dealingSpeed: state.settings.dealingSpeed, cardWidth: cardWidth, cardSpacing: cardSpacing ) @@ -121,6 +123,8 @@ struct BlackjackTableView: View { activeHandIndex: state.activeHandIndex, isPlayerTurn: state.isPlayerTurn, showCardCount: showCardCount, + showAnimations: state.settings.showAnimations, + dealingSpeed: state.settings.dealingSpeed, cardWidth: cardWidth, cardSpacing: cardSpacing, currentHint: state.currentHint, diff --git a/Blackjack/Blackjack/Views/Table/DealerHandView.swift b/Blackjack/Blackjack/Views/Table/DealerHandView.swift index 7fe9744..88f1860 100644 --- a/Blackjack/Blackjack/Views/Table/DealerHandView.swift +++ b/Blackjack/Blackjack/Views/Table/DealerHandView.swift @@ -12,12 +12,19 @@ struct DealerHandView: View { let hand: BlackjackHand let showHoleCard: Bool let showCardCount: Bool + let showAnimations: Bool + let dealingSpeed: Double let cardWidth: CGFloat let cardSpacing: CGFloat @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize @ScaledMetric(relativeTo: .headline) private var badgeHeight: CGFloat = CasinoDesign.Size.valueBadge + /// Scaled animation duration based on dealing speed. + private var animationDuration: Double { + Design.Animation.springDuration * dealingSpeed + } + var body: some View { VStack(spacing: Design.Spacing.small) { @@ -62,12 +69,14 @@ struct DealerHandView: View { } .zIndex(Double(index)) .transition( - .asymmetric( - insertion: .offset(x: Design.DealAnimation.offsetX, y: Design.DealAnimation.offsetY) + showAnimations + ? .asymmetric( + insertion: .offset(x: Design.DealAnimation.dealerOffsetX, y: Design.DealAnimation.dealerOffsetY) .combined(with: .opacity) .combined(with: .scale(scale: Design.Scale.slightShrink)), removal: .scale.combined(with: .opacity) ) + : .identity ) } @@ -78,7 +87,12 @@ struct DealerHandView: View { } } } - .animation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce), value: hand.cards.count) + .animation( + showAnimations + ? .spring(duration: animationDuration, bounce: Design.Animation.springBounce) + : .none, + value: hand.cards.count + ) .overlay(alignment: .bottom) { // Result badge - overlayed so it doesn't add height to the view if let result = hand.cards.count >= 2 && showHoleCard ? handResultText : nil { @@ -136,6 +150,8 @@ struct DealerHandView: View { hand: BlackjackHand(), showHoleCard: false, showCardCount: false, + showAnimations: true, + dealingSpeed: 1.0, cardWidth: 60, cardSpacing: -20 ) @@ -152,6 +168,8 @@ struct DealerHandView: View { ]), showHoleCard: false, showCardCount: false, + showAnimations: true, + dealingSpeed: 1.0, cardWidth: 60, cardSpacing: -20 ) @@ -168,6 +186,8 @@ struct DealerHandView: View { ]), showHoleCard: true, showCardCount: true, + showAnimations: true, + dealingSpeed: 1.0, cardWidth: 60, cardSpacing: -20 ) diff --git a/Blackjack/Blackjack/Views/Table/InsurancePopupView.swift b/Blackjack/Blackjack/Views/Table/InsurancePopupView.swift index d5f17b7..eee9a49 100644 --- a/Blackjack/Blackjack/Views/Table/InsurancePopupView.swift +++ b/Blackjack/Blackjack/Views/Table/InsurancePopupView.swift @@ -13,6 +13,7 @@ struct InsurancePopupView: View { let balance: Int let onTake: () -> Void let onDecline: () -> Void + let onNeverAsk: () -> Void @State private var showContent = false @@ -47,39 +48,50 @@ struct InsurancePopupView: View { .padding(.bottom, Design.Spacing.small) // Buttons - HStack(spacing: Design.Spacing.large) { - // Decline button - Button(action: onDecline) { - Text(String(localized: "No Thanks")) - .font(.system(size: Design.BaseFontSize.medium, weight: .semibold)) - .foregroundStyle(.white) - .padding(.horizontal, Design.Spacing.xLarge) - .padding(.vertical, Design.Spacing.medium) - .background( - Capsule() - .fill(Color.red.opacity(Design.Opacity.heavy)) - ) - } - - // Accept button (only if can afford) - if balance >= betAmount { - Button(action: onTake) { - Text(String(localized: "Yes ($\(betAmount))")) - .font(.system(size: Design.BaseFontSize.medium, weight: .bold)) - .foregroundStyle(.black) + VStack(spacing: Design.Spacing.medium) { + HStack(spacing: Design.Spacing.large) { + // Decline button + Button(action: onDecline) { + Text(String(localized: "No Thanks")) + .font(.system(size: Design.BaseFontSize.medium, weight: .semibold)) + .foregroundStyle(.white) .padding(.horizontal, Design.Spacing.xLarge) .padding(.vertical, Design.Spacing.medium) .background( Capsule() - .fill( - LinearGradient( - colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark], - startPoint: .top, - endPoint: .bottom - ) - ) + .fill(Color.red.opacity(Design.Opacity.heavy)) ) } + + // Accept button (only if can afford) + if balance >= betAmount { + Button(action: onTake) { + Text(String(localized: "Yes ($\(betAmount))")) + .font(.system(size: Design.BaseFontSize.medium, weight: .bold)) + .foregroundStyle(.black) + .padding(.horizontal, Design.Spacing.xLarge) + .padding(.vertical, Design.Spacing.medium) + .background( + Capsule() + .fill( + LinearGradient( + colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark], + startPoint: .top, + endPoint: .bottom + ) + ) + ) + } + } + } + + // Don't Ask Again button + Button(action: onNeverAsk) { + Text(String(localized: "Don't Ask")) + .font(.system(size: Design.BaseFontSize.small, weight: .medium)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + .padding(.horizontal, Design.Spacing.large) + .padding(.vertical, Design.Spacing.small) } } } @@ -113,7 +125,8 @@ struct InsurancePopupView: View { betAmount: 500, balance: 4500, onTake: {}, - onDecline: {} + onDecline: {}, + onNeverAsk: {} ) } @@ -122,7 +135,8 @@ struct InsurancePopupView: View { betAmount: 500, balance: 200, onTake: {}, - onDecline: {} + onDecline: {}, + onNeverAsk: {} ) } diff --git a/Blackjack/Blackjack/Views/Table/PlayerHandView.swift b/Blackjack/Blackjack/Views/Table/PlayerHandView.swift index eb7b95e..a50193f 100644 --- a/Blackjack/Blackjack/Views/Table/PlayerHandView.swift +++ b/Blackjack/Blackjack/Views/Table/PlayerHandView.swift @@ -16,6 +16,8 @@ struct PlayerHandsView: View { let activeHandIndex: Int let isPlayerTurn: Bool let showCardCount: Bool + let showAnimations: Bool + let dealingSpeed: Double let cardWidth: CGFloat let cardSpacing: CGFloat @@ -43,6 +45,8 @@ struct PlayerHandsView: View { hand: hand, isActive: isActiveHand, showCardCount: showCardCount, + showAnimations: showAnimations, + dealingSpeed: dealingSpeed, // Hand numbers: rightmost (index 0) is Hand 1, played first handNumber: hands.count > 1 ? index + 1 : nil, cardWidth: cardWidth, @@ -95,6 +99,8 @@ struct PlayerHandView: View { let hand: BlackjackHand let isActive: Bool let showCardCount: Bool + let showAnimations: Bool + let dealingSpeed: Double let handNumber: Int? let cardWidth: CGFloat let cardSpacing: CGFloat @@ -105,6 +111,11 @@ struct PlayerHandView: View { /// Whether the hint toast should be visible. let showHintToast: Bool + /// Scaled animation duration based on dealing speed. + private var animationDuration: Double { + Design.Animation.springDuration * dealingSpeed + } + @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize @ScaledMetric(relativeTo: .caption) private var handNumberSize: CGFloat = Design.Size.handNumberFontSize @ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.Size.handIconSize @@ -132,17 +143,24 @@ struct PlayerHandView: View { } .zIndex(Double(index)) .transition( - .asymmetric( - insertion: .offset(x: Design.DealAnimation.offsetX, y: Design.DealAnimation.offsetY) + showAnimations + ? .asymmetric( + insertion: .offset(x: Design.DealAnimation.playerOffsetX, y: Design.DealAnimation.playerOffsetY) .combined(with: .opacity) .combined(with: .scale(scale: Design.Scale.slightShrink)), removal: .scale.combined(with: .opacity) ) + : .identity ) } } } - .animation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce), value: hand.cards.count) + .animation( + showAnimations + ? .spring(duration: animationDuration, bounce: Design.Animation.springBounce) + : .none, + value: hand.cards.count + ) .padding(.horizontal, Design.Spacing.medium) .padding(.vertical, Design.Spacing.medium) .background( @@ -246,6 +264,8 @@ struct PlayerHandView: View { activeHandIndex: 0, isPlayerTurn: true, showCardCount: false, + showAnimations: true, + dealingSpeed: 1.0, cardWidth: 60, cardSpacing: -20, currentHint: nil, @@ -265,6 +285,8 @@ struct PlayerHandView: View { activeHandIndex: 0, isPlayerTurn: true, showCardCount: false, + showAnimations: true, + dealingSpeed: 1.0, cardWidth: 60, cardSpacing: -20, currentHint: "Hit", @@ -298,6 +320,8 @@ struct PlayerHandView: View { activeHandIndex: 1, isPlayerTurn: true, showCardCount: true, + showAnimations: true, + dealingSpeed: 1.0, cardWidth: 60, cardSpacing: -20, currentHint: "Stand",