Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2025-12-25 09:07:28 -06:00
parent 889e91a8ca
commit 98d72d0db8
11 changed files with 202 additions and 41 deletions

View File

@ -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).

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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
}
}

View File

@ -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()

View File

@ -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
)
}
}

View File

@ -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,

View File

@ -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
)

View File

@ -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,6 +48,7 @@ struct InsurancePopupView: View {
.padding(.bottom, Design.Spacing.small)
// Buttons
VStack(spacing: Design.Spacing.medium) {
HStack(spacing: Design.Spacing.large) {
// Decline button
Button(action: onDecline) {
@ -82,6 +84,16 @@ struct InsurancePopupView: View {
}
}
}
// 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)
}
}
}
.padding(Design.Spacing.xLarge)
.background(
@ -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: {}
)
}

View File

@ -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",