From 12715cb646befa69181bdb23c992310082f4529e Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 22 Dec 2025 15:39:27 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- .../Baccarat/Resources/Localizable.xcstrings | 233 ++++++- Baccarat/Baccarat/Theme/DesignConstants.swift | 4 +- .../Baccarat/Views/Game/GameTableView.swift | 2 +- .../Baccarat/Views/Sheets/RulesHelpView.swift | 639 ++++++------------ .../Baccarat/Views/Sheets/SettingsView.swift | 212 ++---- Blackjack/Blackjack/Models/GameSettings.swift | 25 + .../Blackjack/Resources/Localizable.xcstrings | 173 +++-- .../Blackjack/Views/Sheets/SettingsView.swift | 278 ++++++-- .../Views/Settings/SettingsComponents.swift | 44 +- 9 files changed, 864 insertions(+), 746 deletions(-) diff --git a/Baccarat/Baccarat/Resources/Localizable.xcstrings b/Baccarat/Baccarat/Resources/Localizable.xcstrings index c503532..f00ed69 100644 --- a/Baccarat/Baccarat/Resources/Localizable.xcstrings +++ b/Baccarat/Baccarat/Resources/Localizable.xcstrings @@ -49,10 +49,6 @@ "comment" : "A numbered list item with a callout number and accompanying text. The first argument is the number of the item. The second argument is the text of the item.", "isCommentAutoGenerated" : true }, - "%lld%%" : { - "comment" : "A label showing the current volume percentage. The argument is the current volume as a percentage (e.g. \"50%\").", - "isCommentAutoGenerated" : true - }, "%lldpx" : { "comment" : "A text label displaying the size of the app icon. The argument is the size of the icon in pixels.", "isCommentAutoGenerated" : true @@ -254,6 +250,7 @@ }, "$%lldK" : { "comment" : "A button that allows the user to select a starting balance for the game.", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -293,6 +290,10 @@ } } }, + "2-9: Face value" : { + "comment" : "Description of the card values for cards with values from 2 to 9.", + "isCommentAutoGenerated" : true + }, "8 : 1" : { "comment" : "The payout ratio for a tie bet.", "localizations" : { @@ -334,6 +335,10 @@ } } }, + "10, Jack, Queen, King: 0 points" : { + "comment" : "Card value description for the face cards (Jack, Queen, King).", + "isCommentAutoGenerated" : true + }, "11 : 1" : { "comment" : "The payout ratio for a pair bet.", "localizations" : { @@ -377,6 +382,7 @@ }, "11:1" : { "comment" : "The payout ratio for a pair bonus bet.", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -416,6 +422,14 @@ } } }, + "A Natural ends the round immediately." : { + "comment" : "Explanation of what happens when a Natural (either Player or Banker) is dealt in a Baccarat round.", + "isCommentAutoGenerated" : true + }, + "Ace: 1 point" : { + "comment" : "Card value description for an Ace.", + "isCommentAutoGenerated" : true + }, "After generating:" : { "comment" : "A heading for the instructions section of the icon generator view.", "isCommentAutoGenerated" : true @@ -428,6 +442,10 @@ "comment" : "A section header that suggests using an online tool to generate app icon sizes.", "isCommentAutoGenerated" : true }, + "Always bet Banker — it has the best odds (1.06% edge)." : { + "comment" : "Advice for playing baccarat that emphasizes the advantage of betting on the Banker.", + "isCommentAutoGenerated" : true + }, "Animate dealing and flipping" : { "comment" : "Subtitle for card animations toggle.", "localizations" : { @@ -471,6 +489,7 @@ }, "ANIMATIONS" : { "comment" : "Section header for animation settings.", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -518,6 +537,10 @@ "comment" : "A header describing the preview of the app icon.", "isCommentAutoGenerated" : true }, + "Avoid the Tie bet — 14.4% house edge!" : { + "comment" : "Tip for avoiding the Tie bet in baccarat, highlighting its low house edge.", + "isCommentAutoGenerated" : true + }, "B" : { "comment" : "The letter \"B\" displayed in the center of the playing card's back.", "extractionState" : "stale", @@ -601,6 +624,7 @@ } }, "BACCARAT" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -640,8 +664,13 @@ } } }, + "Baccarat has one of the lowest house edges in the casino." : { + "comment" : "Description of the house edge of baccarat.", + "isCommentAutoGenerated" : true + }, "BACK TO GAME" : { "comment" : "A button label that takes the user back to the main game screen.", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -848,6 +877,7 @@ }, "Banker %@:" : { "comment" : "A label displaying the total for the banker and an action to take based on that total. The first argument is the banker's total. The second argument is a string describing the action to take based on that total", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -887,6 +917,38 @@ } } }, + "Banker 0-2: Always draws" : { + "comment" : "Description of the third card rule for the Banker when the Player's third card is 0-2.", + "isCommentAutoGenerated" : true + }, + "Banker 3: Draws unless Player's 3rd was 8" : { + "comment" : "Description of the third card rule for the Banker when the Player's third card is 3.", + "isCommentAutoGenerated" : true + }, + "Banker 4: Draws if Player's 3rd was 2-7" : { + "comment" : "Side bet rule for the Banker when the Player's third card is 4 and it falls between 2 and 7.", + "isCommentAutoGenerated" : true + }, + "Banker 5: Draws if Player's 3rd was 4-7" : { + "comment" : "Description of the third card rule for the Banker when the Player's third card is 4-7.", + "isCommentAutoGenerated" : true + }, + "Banker 6: Draws if Player's 3rd was 6-7" : { + "comment" : "Description of the betting strategy for the Banker when the Player's third card is 6-7.", + "isCommentAutoGenerated" : true + }, + "Banker 7: Always stands" : { + "comment" : "Description of the action a banker should take if their third card is a 7 in the \"Third Card - Banker\" rule.", + "isCommentAutoGenerated" : true + }, + "Banker bet has the lowest house edge (1.06%)." : { + "comment" : "Description of the house edge for the Banker bet in the Rules Help view.", + "isCommentAutoGenerated" : true + }, + "Banker Bet: Pays 0.95:1 (5% commission)" : { + "comment" : "Description of the payout for a Banker bet in the Rules Help view.", + "isCommentAutoGenerated" : true + }, "Banker hand" : { "comment" : "A label displayed above the banker's hand.", "localizations" : { @@ -930,6 +992,7 @@ }, "Banker Rules" : { "comment" : "A section header for the banker's rules in the third card rules content.", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1011,6 +1074,10 @@ } } }, + "Bet on which hand will win: Player, Banker, or Tie." : { + "comment" : "Text describing the objective of the baccarat game.", + "isCommentAutoGenerated" : true + }, "Blackjack" : { "comment" : "The name of a blackjack game.", "isCommentAutoGenerated" : true @@ -1179,6 +1246,10 @@ } } }, + "Cards are dealt automatically — no decisions to make!" : { + "comment" : "Description of the baccarat rules page, emphasizing that no decisions need to be made during the deal.", + "isCommentAutoGenerated" : true + }, "Cards face down" : { "comment" : "Voiceover description of the player's hand when no cards are visible.", "localizations" : { @@ -1547,6 +1618,7 @@ }, "Dealing Speed" : { "comment" : "A label describing the speed at which cards are dealt in the game.", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1872,7 +1944,16 @@ } } }, + "Dragon Bonus is fun but has ~2.7% house edge." : { + "comment" : "Description of the Dragon Bonus side bet, highlighting its fun aspect and its house edge.", + "isCommentAutoGenerated" : true + }, + "Example: 5♥ + 5♣ = Pair (wins!)" : { + "comment" : "Example of a pair bet winning.", + "isCommentAutoGenerated" : true + }, "Examples" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2047,6 +2128,10 @@ "comment" : "A text that appears while generating icons.", "isCommentAutoGenerated" : true }, + "Hand values use only the last digit (e.g., 7+8=15 → 5)." : { + "comment" : "Explanation of how card values are determined in baccarat.", + "isCommentAutoGenerated" : true + }, "handValueFormat" : { "comment" : "Format for displaying hand value. The argument is the numeric value of the hand.", "localizations" : { @@ -2386,8 +2471,25 @@ "comment" : "The title of the Icon Generator view.", "isCommentAutoGenerated" : true }, + "If either hand totals 8 or 9 with two cards, it's a 'Natural'." : { + "comment" : "Description of the 'Natural' hand in baccarat, explaining when it occurs and its significance.", + "isCommentAutoGenerated" : true + }, + "If neither hand has a Natural, third card rules apply." : { + "comment" : "Explanation of the third card rule for the Player hand.", + "isCommentAutoGenerated" : true + }, + "If Player draws, Banker's action depends on Player's third card:" : { + "comment" : "Explanation of the third card decision for the Banker in the Rules Help view.", + "isCommentAutoGenerated" : true + }, + "If Player stands, Banker draws on 0-5, stands on 6-7." : { + "comment" : "Description of the third card rules for the Banker in the Rules Help view.", + "isCommentAutoGenerated" : true + }, "Important" : { "comment" : "A heading for important information related to a section of a view.", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2427,6 +2529,10 @@ } } }, + "Independent of the main game result." : { + "comment" : "Note about the independence of the Pair Bonus from the main game result in the Rules Help view.", + "isCommentAutoGenerated" : true + }, "Last Synced" : { "localizations" : { "en" : { @@ -2511,6 +2617,10 @@ } } }, + "Main Bets" : { + "comment" : "Title of a rule page in the \"Rules\" help view, describing the main bets available in baccarat.", + "isCommentAutoGenerated" : true + }, "MAX" : { "comment" : "A label displayed as a badge on top-right of a chip to indicate it's the maximum bet.", "localizations" : { @@ -2552,6 +2662,10 @@ } } }, + "Natural 9 beats Natural 8." : { + "comment" : "Explanation of the payout for a Baccarat hand that is a Natural 9, compared to one that is a Natural 8.", + "isCommentAutoGenerated" : true + }, "Natural Win" : { "comment" : "A section header for information about winning with a natural hand.", "localizations" : { @@ -2593,6 +2707,10 @@ } } }, + "Natural Win: 1:1" : { + "comment" : "Description of the payout for a 'Natural Win' in the Rules Help view.", + "isCommentAutoGenerated" : true + }, "Naturals" : { "localizations" : { "en" : { @@ -2836,6 +2954,14 @@ } } }, + "Objective" : { + "comment" : "Title of a rule page in the \"Rules\" help view, describing the objective of the game.", + "isCommentAutoGenerated" : true + }, + "Only the rank matters (suits are ignored)." : { + "comment" : "Explanation of how to determine if the first two cards in a hand form a pair, focusing on the rank rather than the suit.", + "isCommentAutoGenerated" : true + }, "Option 1: Screenshot from Preview" : { "comment" : "A description of one method for exporting app icons.", "isCommentAutoGenerated" : true @@ -2892,6 +3018,10 @@ } } }, + "Pair bets have ~10% house edge." : { + "comment" : "Description of the house edge of a pair bet in the Rules Help view.", + "isCommentAutoGenerated" : true + }, "Pair Bonus" : { "comment" : "Title of the page explaining the pair bonus in Baccarat.", "localizations" : { @@ -2935,6 +3065,7 @@ }, "Pair Pays" : { "comment" : "The text that appears above the payout value for a pair bonus bet.", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2974,7 +3105,16 @@ } } }, + "Pair Pays: 11:1" : { + "comment" : "Side bet payout for a pair of cards.", + "isCommentAutoGenerated" : true + }, + "Pairs occur roughly once every 15 hands." : { + "comment" : "Explanation of how often pairs occur in a typical game of baccarat.", + "isCommentAutoGenerated" : true + }, "Payout" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3016,6 +3156,7 @@ }, "Payout Table" : { "comment" : "The title of a table that lists possible payouts for a dragon bonus bet.", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3303,6 +3444,10 @@ } } }, + "Player Bet: Pays 1:1 (even money)" : { + "comment" : "Description of the payout for a Player Bet in the Rules Help view.", + "isCommentAutoGenerated" : true + }, "Player hand" : { "comment" : "An accessibility label for the player's hand in the cards display area.", "localizations" : { @@ -3345,6 +3490,7 @@ } }, "Player Rules" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3426,6 +3572,18 @@ } } }, + "Player with 0-5: Draws a third card" : { + "comment" : "Description of the action for the Player when their third card is 0-5.", + "isCommentAutoGenerated" : true + }, + "Player with 6-7: Stands" : { + "comment" : "Description of the action for the Banker when the Player draws a third card.", + "isCommentAutoGenerated" : true + }, + "Player with 8-9: Natural (no third card)" : { + "comment" : "Text for the third card rule when the player's hand totals 8 or 9.", + "isCommentAutoGenerated" : true + }, "Poker" : { "comment" : "The name of a poker game.", "isCommentAutoGenerated" : true @@ -3598,6 +3756,10 @@ } } }, + "Set a budget and stick to it." : { + "comment" : "Tip for players to set a budget and stick to it when playing baccarat.", + "isCommentAutoGenerated" : true + }, "Settings" : { "comment" : "The label of a button that navigates to the settings screen.", "localizations" : { @@ -3721,6 +3883,14 @@ } } }, + "Side bet on Player or Banker winning by a margin." : { + "comment" : "Title for a side bet where the player bets on which hand wins by a margin (e.g., Banker by 9 points).", + "isCommentAutoGenerated" : true + }, + "Side bet on the first two cards being a pair." : { + "comment" : "Description of a side bet where the player bets on whether the first two cards dealt in a hand are a pair.", + "isCommentAutoGenerated" : true + }, "Sign in to iCloud to sync progress" : { "localizations" : { "en" : { @@ -3924,6 +4094,10 @@ } } }, + "Strategy Tips" : { + "comment" : "Title of a section in the Rules Help view focused on strategy tips.", + "isCommentAutoGenerated" : true + }, "Sync Now" : { "localizations" : { "en" : { @@ -4087,11 +4261,28 @@ } } }, + "The hand closest to 9 wins." : { + "comment" : "Explanation of how the hand closest to 9 wins in baccarat.", + "isCommentAutoGenerated" : true + }, + "There's no skill involved — just enjoy the game!" : { + "comment" : "Tip for players on how to play baccarat without needing any skill.", + "isCommentAutoGenerated" : true + }, "These show how the same pattern works for other games" : { "comment" : "A description below the section of the view that previews icons for other games.", "isCommentAutoGenerated" : true }, + "Third Card - Banker" : { + "comment" : "Content for a rule page in the \"Rules\" help view, detailing the action of the Banker based on the Player's third card.", + "isCommentAutoGenerated" : true + }, + "Third Card - Player" : { + "comment" : "Description of the third card rule for the player in the Rules Help view.", + "isCommentAutoGenerated" : true + }, "Third Card Rules" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -4252,6 +4443,10 @@ } } }, + "Tie Bet: Pays 8:1" : { + "comment" : "Description of a baccarat side bet where the bettor wins 8:1 if both hands have the same value.", + "isCommentAutoGenerated" : true + }, "TIE GAME" : { "comment" : "Result banner text when the game is a tie.", "extractionState" : "stale", @@ -4296,6 +4491,7 @@ }, "Tips" : { "comment" : "A section header for tips related to pair bonuses.", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -4416,6 +4612,10 @@ } } }, + "Two Naturals of the same value result in a Tie." : { + "comment" : "Text describing the outcome when two players both have a Natural (a total of 8 or 9 with two cards).", + "isCommentAutoGenerated" : true + }, "Upload the 1024px icon to appicon.co or makeappicon.com to generate all sizes automatically." : { "comment" : "A description of an alternative method for generating app icons.", "isCommentAutoGenerated" : true @@ -4505,6 +4705,7 @@ }, "Volume" : { "comment" : "Label for volume slider.", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -4585,6 +4786,30 @@ } } }, + "Win by 4 points: 1:1" : { + "comment" : "Description of the payout for a Baccarat \"Pair\" side bet when the player's two cards have the same rank.", + "isCommentAutoGenerated" : true + }, + "Win by 5 points: 2:1" : { + "comment" : "Description of the Dragon Bonus side bet, specifically for a win by 5 points.", + "isCommentAutoGenerated" : true + }, + "Win by 6 points: 4:1" : { + "comment" : "Description of the Dragon Bonus side bet, specifically for the \"Win by 6 points\" option.", + "isCommentAutoGenerated" : true + }, + "Win by 7 points: 6:1" : { + "comment" : "Description of the Dragon Bonus side bet, specifically for a win by 7 points.", + "isCommentAutoGenerated" : true + }, + "Win by 8 points: 10:1" : { + "comment" : "Description of a baccarat side bet where the player bets on the banker winning by 8 points, offering a payout of 10:1.", + "isCommentAutoGenerated" : true + }, + "Win by 9 points: 30:1" : { + "comment" : "Description of a side bet where the player bets on the banker winning by 9 points, offering a payout of 30:1.", + "isCommentAutoGenerated" : true + }, "Winner" : { "comment" : "A description of the player's hand, including its value and whether they won.", "localizations" : { diff --git a/Baccarat/Baccarat/Theme/DesignConstants.swift b/Baccarat/Baccarat/Theme/DesignConstants.swift index 3dbfce9..bfb7d2e 100644 --- a/Baccarat/Baccarat/Theme/DesignConstants.swift +++ b/Baccarat/Baccarat/Theme/DesignConstants.swift @@ -215,7 +215,9 @@ extension Color { // MARK: - Settings Colors enum Settings { - static let background = Color(red: 0.08, green: 0.12, blue: 0.08) + static let background = Color(red: 0.08, green: 0.12, blue: 0.18) + static let cardBackground = Color.white.opacity(CasinoDesign.Opacity.verySubtle) + static let accent = Color(red: 0.9, green: 0.75, blue: 0.3) } } diff --git a/Baccarat/Baccarat/Views/Game/GameTableView.swift b/Baccarat/Baccarat/Views/Game/GameTableView.swift index 6fee835..c69a78a 100644 --- a/Baccarat/Baccarat/Views/Game/GameTableView.swift +++ b/Baccarat/Baccarat/Views/Game/GameTableView.swift @@ -103,7 +103,7 @@ struct GameTableView: View { } } } - .fullScreenCover(isPresented: $showRules) { + .sheet(isPresented: $showRules) { RulesHelpView() } .sheet(isPresented: $showStats) { diff --git a/Baccarat/Baccarat/Views/Sheets/RulesHelpView.swift b/Baccarat/Baccarat/Views/Sheets/RulesHelpView.swift index 672c5b1..90b36e6 100644 --- a/Baccarat/Baccarat/Views/Sheets/RulesHelpView.swift +++ b/Baccarat/Baccarat/Views/Sheets/RulesHelpView.swift @@ -2,465 +2,213 @@ // RulesHelpView.swift // Baccarat // -// A paginated help view explaining game rules and side bets. +// Game rules and how to play guide. // import SwiftUI import CasinoKit -/// The available rule pages. -enum RulesPage: Int, CaseIterable, Identifiable { - case basicRules = 0 - case thirdCardRules = 1 - case dragonBonus = 2 - case pairBonus = 3 - - var id: Int { rawValue } - - var title: String { - switch self { - case .basicRules: return String(localized: "How to Play") - case .thirdCardRules: return String(localized: "Third Card Rules") - case .dragonBonus: return String(localized: "Dragon Bonus") - case .pairBonus: return String(localized: "Pair Bonus") - } - } -} - -/// A multi-page help view explaining baccarat rules. struct RulesHelpView: View { @Environment(\.dismiss) private var dismiss - @State private var currentPage: RulesPage = .basicRules + @State private var currentPage = 0 - // MARK: - Layout Constants - - private let modalCornerRadius = Design.CornerRadius.xxxLarge - private let contentCornerRadius = Design.CornerRadius.xLarge - private let contentPadding = Design.Spacing.large - private let buttonSize: CGFloat = 44 - - // MARK: - Body - - var body: some View { - ZStack { - // Background - same as other sheets - Color.Settings.background - .ignoresSafeArea() - - VStack(spacing: Design.Spacing.medium) { - // Header with logo - headerView - - // Content card - contentCard - .padding(.horizontal) - - // Navigation - navigationView - .padding(.bottom, Design.Spacing.large) - } - } - } - - // MARK: - Subviews - - private var headerView: some View { - VStack(spacing: Design.Spacing.small) { - // Cards icon - HStack(spacing: -Design.Spacing.small) { - Image(systemName: "suit.spade.fill") - .foregroundStyle(.white) - Image(systemName: "suit.heart.fill") - .foregroundStyle(.red) - } - .font(.system(size: Design.BaseFontSize.title)) - - Text("BACCARAT") - .font(.system(size: Design.BaseFontSize.title - Design.Spacing.xSmall, weight: .black, design: .rounded)) - .foregroundStyle( - LinearGradient( - colors: [.yellow, .orange], - startPoint: .top, - endPoint: .bottom - ) - ) - .tracking(3) - } - .padding(.top, Design.Spacing.xLarge) - } - - private var contentCard: some View { - VStack(spacing: 0) { - // Page title - Text(currentPage.title) - .font(.system(size: Design.BaseFontSize.xxLarge + Design.Spacing.xxSmall, weight: .bold, design: .rounded)) - .foregroundStyle(.yellow) - .padding(.top, Design.Spacing.large) - .padding(.bottom, Design.Spacing.medium) - - // Scrollable content - ScrollView { - pageContent - .padding(.horizontal, contentPadding) - .padding(.bottom, Design.Spacing.large) - } - } - .frame(maxWidth: .infinity) - .background( - RoundedRectangle(cornerRadius: contentCornerRadius) - .fill(Color.white.opacity(Design.Opacity.verySubtle)) + private let pages: [RulePage] = [ + RulePage( + title: String(localized: "Objective"), + icon: "target", + content: [ + String(localized: "Bet on which hand will win: Player, Banker, or Tie."), + String(localized: "The hand closest to 9 wins."), + String(localized: "Cards are dealt automatically — no decisions to make!"), + String(localized: "Baccarat has one of the lowest house edges in the casino.") + ] + ), + RulePage( + title: String(localized: "Card Values"), + icon: "suit.spade.fill", + content: [ + String(localized: "Ace: 1 point"), + String(localized: "2-9: Face value"), + String(localized: "10, Jack, Queen, King: 0 points"), + String(localized: "Hand values use only the last digit (e.g., 7+8=15 → 5).") + ] + ), + RulePage( + title: String(localized: "Natural Win"), + icon: "star.fill", + content: [ + String(localized: "If either hand totals 8 or 9 with two cards, it's a 'Natural'."), + String(localized: "A Natural ends the round immediately."), + String(localized: "Natural 9 beats Natural 8."), + String(localized: "Two Naturals of the same value result in a Tie.") + ] + ), + RulePage( + title: String(localized: "Main Bets"), + icon: "dollarsign.circle.fill", + content: [ + String(localized: "Player Bet: Pays 1:1 (even money)"), + String(localized: "Banker Bet: Pays 0.95:1 (5% commission)"), + String(localized: "Tie Bet: Pays 8:1"), + String(localized: "Banker bet has the lowest house edge (1.06%).") + ] + ), + RulePage( + title: String(localized: "Third Card - Player"), + icon: "hand.draw.fill", + content: [ + String(localized: "If neither hand has a Natural, third card rules apply."), + String(localized: "Player with 0-5: Draws a third card"), + String(localized: "Player with 6-7: Stands"), + String(localized: "Player with 8-9: Natural (no third card)") + ] + ), + RulePage( + title: String(localized: "Third Card - Banker"), + icon: "person.fill", + content: [ + String(localized: "If Player stands, Banker draws on 0-5, stands on 6-7."), + String(localized: "If Player draws, Banker's action depends on Player's third card:"), + String(localized: "Banker 0-2: Always draws"), + String(localized: "Banker 3: Draws unless Player's 3rd was 8"), + String(localized: "Banker 4: Draws if Player's 3rd was 2-7"), + String(localized: "Banker 5: Draws if Player's 3rd was 4-7"), + String(localized: "Banker 6: Draws if Player's 3rd was 6-7"), + String(localized: "Banker 7: Always stands") + ] + ), + RulePage( + title: String(localized: "Dragon Bonus"), + icon: "flame.fill", + content: [ + String(localized: "Side bet on Player or Banker winning by a margin."), + String(localized: "Natural Win: 1:1"), + String(localized: "Win by 9 points: 30:1"), + String(localized: "Win by 8 points: 10:1"), + String(localized: "Win by 7 points: 6:1"), + String(localized: "Win by 6 points: 4:1"), + String(localized: "Win by 5 points: 2:1"), + String(localized: "Win by 4 points: 1:1") + ] + ), + RulePage( + title: String(localized: "Pair Bonus"), + icon: "square.on.square.fill", + content: [ + String(localized: "Side bet on the first two cards being a pair."), + String(localized: "Pair Pays: 11:1"), + String(localized: "Only the rank matters (suits are ignored)."), + String(localized: "Example: 5♥ + 5♣ = Pair (wins!)"), + String(localized: "Pairs occur roughly once every 15 hands."), + String(localized: "Independent of the main game result.") + ] + ), + RulePage( + title: String(localized: "Strategy Tips"), + icon: "lightbulb.fill", + content: [ + String(localized: "Always bet Banker — it has the best odds (1.06% edge)."), + String(localized: "Avoid the Tie bet — 14.4% house edge!"), + String(localized: "Dragon Bonus is fun but has ~2.7% house edge."), + String(localized: "Pair bets have ~10% house edge."), + String(localized: "There's no skill involved — just enjoy the game!"), + String(localized: "Set a budget and stick to it.") + ] ) - .clipShape(RoundedRectangle(cornerRadius: contentCornerRadius)) - } - - @ViewBuilder - private var pageContent: some View { - switch currentPage { - case .basicRules: - BasicRulesContent() - case .thirdCardRules: - ThirdCardRulesContent() - case .dragonBonus: - DragonBonusContent() - case .pairBonus: - PairBonusContent() - } - } - - private var navigationView: some View { - HStack(spacing: Design.Spacing.medium) { - // Previous button - Button { - withAnimation(.spring(duration: Design.Animation.quick)) { - goToPreviousPage() - } - } label: { - Image(systemName: "chevron.left.circle.fill") - .font(.system(size: Design.BaseFontSize.largeTitle)) - .foregroundStyle(currentPage.rawValue > 0 ? .yellow : .gray.opacity(Design.Opacity.medium)) - } - .disabled(currentPage.rawValue == 0) - - // Back to game button - Button { - dismiss() - } label: { - Text("BACK TO GAME") - .font(.system(size: Design.BaseFontSize.large, weight: .bold)) - .foregroundStyle(.black) - .padding(.horizontal, Design.Spacing.xLarge) - .padding(.vertical, Design.Spacing.medium) - .background( - Capsule() - .fill( - LinearGradient( - colors: [Color.Button.goldLight, Color.Button.goldDark], - startPoint: .top, - endPoint: .bottom - ) - ) - ) - } - - // Next button - Button { - withAnimation(.spring(duration: Design.Animation.quick)) { - goToNextPage() - } - } label: { - Image(systemName: "chevron.right.circle.fill") - .font(.system(size: Design.BaseFontSize.largeTitle)) - .foregroundStyle(currentPage.rawValue < RulesPage.allCases.count - 1 ? .green : .gray.opacity(Design.Opacity.medium)) - } - .disabled(currentPage.rawValue >= RulesPage.allCases.count - 1) - } - } - - // MARK: - Navigation - - private func goToPreviousPage() { - if currentPage.rawValue > 0 { - currentPage = RulesPage(rawValue: currentPage.rawValue - 1) ?? .basicRules - } - } - - private func goToNextPage() { - if currentPage.rawValue < RulesPage.allCases.count - 1 { - currentPage = RulesPage(rawValue: currentPage.rawValue + 1) ?? .pairBonus - } - } -} - -// MARK: - Basic Rules Content - -private struct BasicRulesContent: View { - var body: some View { - VStack(alignment: .leading, spacing: Design.Spacing.medium) { - RuleSection(text: "Two hands are dealt: one for the Player and one for the Banker. You may bet on which hand will win, or that they will tie.") - - RuleSection(title: "Payouts", items: [ - "Player wins: pays 1 to 1", - "Banker wins: pays 0.95 to 1 (5% commission)", - "Tie: pays 8 to 1" - ]) - - Divider() - .background(Color.white.opacity(Design.Opacity.light)) - - Text("Card Values") - .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold)) - .foregroundStyle(.white) - - RuleSection(items: [ - "Aces = 1 point", - "2-9 = Face value", - "10, J, Q, K = 0 points" - ]) - - RuleSection(text: "Hand values are the sum of cards, keeping only the last digit. For example: 7 + 8 = 15, so the hand value is 5.") - - Divider() - .background(Color.white.opacity(Design.Opacity.light)) - - Text("Natural Win") - .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold)) - .foregroundStyle(.white) - - RuleSection(text: "If either hand totals 8 or 9 with the first two cards, it's a \"Natural\" and the round ends immediately.") - } - } -} - -// MARK: - Third Card Rules Content - -private struct ThirdCardRulesContent: View { - var body: some View { - VStack(alignment: .leading, spacing: Design.Spacing.medium) { - RuleSection(text: "If neither hand has a Natural, additional cards may be drawn according to fixed rules.") - - Divider() - .background(Color.white.opacity(Design.Opacity.light)) - - Text("Player Rules") - .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold)) - .foregroundStyle(.white) - - RuleSection(items: [ - "0-5: Player draws a third card", - "6-7: Player stands" - ]) - - Divider() - .background(Color.white.opacity(Design.Opacity.light)) - - Text("Banker Rules") - .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold)) - .foregroundStyle(.white) - - RuleSection(text: "If Player stood (6-7), Banker draws on 0-5 and stands on 6-7.") - - RuleSection(text: "If Player drew a third card, Banker's action depends on both the Banker's total and the Player's third card:") - - VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { - BankerRuleRow(bankerTotal: "0-2", action: "Always draws") - BankerRuleRow(bankerTotal: "3", action: "Draws unless Player's 3rd was 8") - BankerRuleRow(bankerTotal: "4", action: "Draws if Player's 3rd was 2-7") - BankerRuleRow(bankerTotal: "5", action: "Draws if Player's 3rd was 4-7") - BankerRuleRow(bankerTotal: "6", action: "Draws if Player's 3rd was 6-7") - BankerRuleRow(bankerTotal: "7", action: "Always stands") - } - .padding() - .background( - RoundedRectangle(cornerRadius: Design.CornerRadius.medium) - .fill(Color.black.opacity(Design.Opacity.hint)) - ) - } - } -} - -private struct BankerRuleRow: View { - let bankerTotal: String - let action: String - - private let labelWidth: CGFloat = 80 + ] var body: some View { - HStack { - Text("Banker \(bankerTotal):") - .font(.system(size: Design.BaseFontSize.callout, weight: .semibold)) - .foregroundStyle(.yellow) - .frame(width: labelWidth, alignment: .leading) - - Text(action) - .font(.system(size: Design.BaseFontSize.callout)) - .foregroundStyle(.white.opacity(Design.Opacity.almostFull)) - } - } -} - -// MARK: - Dragon Bonus Content - -private struct DragonBonusContent: View { - var body: some View { - VStack(alignment: .leading, spacing: Design.Spacing.medium) { - RuleSection(text: "The Dragon Bonus is a side bet available for both Player and Banker. It pays based on how the winning hand wins.") - - Divider() - .background(Color.white.opacity(Design.Opacity.light)) - - Text("Payout Table") - .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold)) - .foregroundStyle(.white) - - VStack(spacing: Design.Spacing.xSmall) { - PayoutRow(condition: "Natural Win (8 or 9)", payout: "1 to 1") - PayoutRow(condition: "Win by 9 points", payout: "30 to 1") - PayoutRow(condition: "Win by 8 points", payout: "10 to 1") - PayoutRow(condition: "Win by 7 points", payout: "6 to 1") - PayoutRow(condition: "Win by 6 points", payout: "4 to 1") - PayoutRow(condition: "Win by 5 points", payout: "2 to 1") - PayoutRow(condition: "Win by 4 points", payout: "1 to 1") - } - .padding() - .background( - RoundedRectangle(cornerRadius: Design.CornerRadius.medium) - .fill(Color.black.opacity(Design.Opacity.hint)) - ) - - Divider() - .background(Color.white.opacity(Design.Opacity.light)) - - Text("Important") - .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold)) - .foregroundStyle(.white) - - RuleSection(items: [ - "Dragon Bonus loses if your side doesn't win", - "Dragon Bonus loses on a tie", - "Wins by less than 4 points also lose" - ]) - } - } -} - -private struct PayoutRow: View { - let condition: String - let payout: String - - var body: some View { - HStack { - Text(condition) - .font(.system(size: Design.BaseFontSize.medium)) - .foregroundStyle(.white.opacity(Design.Opacity.almostFull)) - - Spacer() - - Text(payout) - .font(.system(size: Design.BaseFontSize.medium, weight: .bold)) - .foregroundStyle(.yellow) - } - .padding(.vertical, Design.Spacing.xxSmall) - } -} - -// MARK: - Pair Bonus Content - -private struct PairBonusContent: View { - var body: some View { - VStack(alignment: .leading, spacing: Design.Spacing.medium) { - RuleSection(text: "Pair Bonus bets are available for both Player and Banker. They pay when the first two cards dealt to that hand form a pair.") - - Divider() - .background(Color.white.opacity(Design.Opacity.light)) - - Text("Payout") - .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold)) - .foregroundStyle(.white) - - HStack { - VStack { - Text("11:1") - .font(.system(size: Design.BaseFontSize.largeTitle + Design.Spacing.medium, weight: .black, design: .rounded)) - .foregroundStyle( - LinearGradient( - colors: [.yellow, .orange], - startPoint: .top, - endPoint: .bottom - ) - ) - - Text("Pair Pays") - .font(.system(size: Design.BaseFontSize.medium, weight: .medium)) - .foregroundStyle(.white.opacity(Design.Opacity.strong)) - } - .frame(maxWidth: .infinity) - } - .padding(.vertical, Design.Spacing.medium) - - Divider() - .background(Color.white.opacity(Design.Opacity.light)) - - Text("Examples") - .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold)) - .foregroundStyle(.white) - - RuleSection(items: [ - "5♥ + 5♣ = Pair (wins 11:1)", - "J♦ + J♠ = Pair (wins 11:1)", - "A♥ + A♥ = Pair (wins 11:1)" - ]) - - RuleSection(text: "Note: Suits are disregarded. Only the rank matters for a pair.") - - Divider() - .background(Color.white.opacity(Design.Opacity.light)) - - Text("Tips") - .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold)) - .foregroundStyle(.white) - - RuleSection(items: [ - "Pair bets are independent of the main game result", - "You can bet on Player Pair, Banker Pair, or both", - "Pairs occur roughly once every 15 hands" - ]) - } - } -} - -// MARK: - Helper Views - -private struct RuleSection: View { - var title: String? = nil - var text: String? = nil - var items: [String]? = nil - - var body: some View { - VStack(alignment: .leading, spacing: Design.Spacing.small) { - if let title = title { - Text(title) - .font(.system(size: Design.BaseFontSize.large, weight: .semibold)) - .foregroundStyle(.yellow) - } - - if let text = text { - Text(text) - .font(.system(size: Design.BaseFontSize.medium)) - .foregroundStyle(.white.opacity(Design.Opacity.almostFull)) - .fixedSize(horizontal: false, vertical: true) - } - - if let items = items { - ForEach(items, id: \.self) { item in - HStack(alignment: .top, spacing: Design.Spacing.small) { - Text("•") - .foregroundStyle(.yellow) - Text(item) - .foregroundStyle(.white.opacity(Design.Opacity.almostFull)) + NavigationStack { + ZStack { + Color.Settings.background + .ignoresSafeArea() + + VStack(spacing: 0) { + // Page content + TabView(selection: $currentPage) { + ForEach(pages.indices, id: \.self) { index in + RulePageView(page: pages[index]) + .tag(index) + } } - .font(.system(size: Design.BaseFontSize.medium)) + .tabViewStyle(.page(indexDisplayMode: .never)) + + // Page indicator + HStack(spacing: Design.Spacing.small) { + ForEach(pages.indices, id: \.self) { index in + Circle() + .fill(index == currentPage ? Color.Settings.accent : Color.white.opacity(Design.Opacity.light)) + .frame(width: 8, height: 8) + } + } + .padding(.vertical, Design.Spacing.medium) } } + .navigationTitle(String(localized: "How to Play")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button(String(localized: "Done")) { + dismiss() + } + .foregroundStyle(Color.Settings.accent) + } + } + .toolbarBackground(Color.Settings.background, for: .navigationBar) + .toolbarColorScheme(.dark, for: .navigationBar) + } + } +} + +// MARK: - Rule Page Model + +struct RulePage: Identifiable { + let id = UUID() + let title: String + let icon: String + let content: [String] +} + +// MARK: - Rule Page View + +struct RulePageView: View { + let page: RulePage + + @ScaledMetric(relativeTo: .title) private var iconSize: CGFloat = Design.BaseFontSize.display + @ScaledMetric(relativeTo: .title) private var titleSize: CGFloat = Design.BaseFontSize.title + @ScaledMetric(relativeTo: .body) private var bodySize: CGFloat = Design.BaseFontSize.body + + var body: some View { + ScrollView { + VStack(spacing: Design.Spacing.xLarge) { + // Icon + Image(systemName: page.icon) + .font(.system(size: iconSize)) + .foregroundStyle(Color.Settings.accent) + .padding(.top, Design.Spacing.xxLarge) + + // Title + Text(page.title) + .font(.system(size: titleSize, weight: .bold)) + .foregroundStyle(.white) + + // Content + VStack(alignment: .leading, spacing: Design.Spacing.medium) { + ForEach(page.content.indices, id: \.self) { index in + HStack(alignment: .top, spacing: Design.Spacing.medium) { + Text("•") + .foregroundStyle(Color.Settings.accent) + + Text(page.content[index]) + .font(.system(size: bodySize)) + .foregroundStyle(.white.opacity(Design.Opacity.heavy)) + } + } + } + .padding(.horizontal, Design.Spacing.xxLarge) + + Spacer() + } } } } @@ -468,4 +216,3 @@ private struct RuleSection: View { #Preview { RulesHelpView() } - diff --git a/Baccarat/Baccarat/Views/Sheets/SettingsView.swift b/Baccarat/Baccarat/Views/Sheets/SettingsView.swift index 03de157..cdcb2d7 100644 --- a/Baccarat/Baccarat/Views/Sheets/SettingsView.swift +++ b/Baccarat/Baccarat/Views/Sheets/SettingsView.swift @@ -25,11 +25,14 @@ struct SettingsView: View { return "Baccarat v\(version) (\(build))" } + /// Accent color for settings components + private let accent = Color.Settings.accent + var body: some View { SheetContainerView( title: String(localized: "Settings"), content: { - // Table Limits Section (First!) + // 1. Table Limits SheetSection(title: String(localized: "TABLE LIMITS"), icon: "banknote") { TableLimitsPicker(selection: $settings.tableLimits) .onChange(of: settings.tableLimits) { _, _ in @@ -37,7 +40,7 @@ struct SettingsView: View { } } - // Deck Settings Section + // 2. Deck Settings SheetSection(title: String(localized: "DECK SETTINGS"), icon: "rectangle.portrait.on.rectangle.portrait") { DeckCountPicker(selection: $settings.deckCount) .onChange(of: settings.deckCount) { _, _ in @@ -45,20 +48,38 @@ struct SettingsView: View { } } - // Starting Balance Section + // 3. Starting Balance SheetSection(title: String(localized: "STARTING BALANCE"), icon: "dollarsign.circle") { - BalancePicker(balance: $settings.startingBalance) + BalancePicker(balance: $settings.startingBalance, accentColor: accent) .onChange(of: settings.startingBalance) { _, _ in hasChanges = true } } - // Display Settings Section + // 4. Display (includes animations) SheetSection(title: String(localized: "DISPLAY"), icon: "eye") { + SettingsToggle( + title: String(localized: "Card Animations"), + subtitle: String(localized: "Animate dealing and flipping"), + isOn: $settings.showAnimations, + accentColor: accent + ) + + if settings.showAnimations { + Divider() + .background(Color.white.opacity(Design.Opacity.subtle)) + + SpeedPicker(speed: $settings.dealingSpeed, accentColor: accent) + } + + Divider() + .background(Color.white.opacity(Design.Opacity.subtle)) + SettingsToggle( title: String(localized: "Show Cards Remaining"), subtitle: String(localized: "Display deck counter at top"), - isOn: $settings.showCardsRemaining + isOn: $settings.showCardsRemaining, + accentColor: accent ) Divider() @@ -67,32 +88,18 @@ struct SettingsView: View { SettingsToggle( title: String(localized: "Show History"), subtitle: String(localized: "Display result road map"), - isOn: $settings.showHistory + isOn: $settings.showHistory, + accentColor: accent ) } - // Animation Settings Section - SheetSection(title: String(localized: "ANIMATIONS"), icon: "sparkles") { - SettingsToggle( - title: String(localized: "Card Animations"), - subtitle: String(localized: "Animate dealing and flipping"), - isOn: $settings.showAnimations - ) - - if settings.showAnimations { - Divider() - .background(Color.white.opacity(Design.Opacity.subtle)) - - SpeedPicker(speed: $settings.dealingSpeed) - } - } - - // Sound & Haptics Section + // 5. Sound & Haptics SheetSection(title: String(localized: "SOUND & HAPTICS"), icon: "speaker.wave.2") { SettingsToggle( title: String(localized: "Sound Effects"), subtitle: String(localized: "Chips, cards, and result sounds"), - isOn: $settings.soundEnabled + isOn: $settings.soundEnabled, + accentColor: accent ) .onChange(of: settings.soundEnabled) { _, newValue in SoundManager.shared.soundEnabled = newValue @@ -103,7 +110,7 @@ struct SettingsView: View { Divider() .background(Color.white.opacity(Design.Opacity.subtle)) - VolumePicker(volume: $settings.soundVolume) + VolumePicker(volume: $settings.soundVolume, accentColor: accent) .onChange(of: settings.soundVolume) { _, newValue in SoundManager.shared.volume = newValue hasChanges = true @@ -116,14 +123,15 @@ struct SettingsView: View { SettingsToggle( title: String(localized: "Haptic Feedback"), subtitle: String(localized: "Vibration for actions and results"), - isOn: $settings.hapticsEnabled + isOn: $settings.hapticsEnabled, + accentColor: accent ) .onChange(of: settings.hapticsEnabled) { _, _ in hasChanges = true } } - // iCloud Sync Section + // 6. Cloud Sync SheetSection(title: String(localized: "CLOUD SYNC"), icon: "icloud") { if gameState.iCloudAvailable { Toggle(isOn: Binding( @@ -140,7 +148,7 @@ struct SettingsView: View { .foregroundStyle(.white.opacity(Design.Opacity.medium)) } } - .tint(.yellow) + .tint(accent) .padding(.vertical, Design.Spacing.xSmall) if gameState.iCloudEnabled { @@ -176,7 +184,7 @@ struct SettingsView: View { Text(String(localized: "Sync Now")) } .font(.system(size: Design.BaseFontSize.body, weight: .medium)) - .foregroundStyle(.yellow) + .foregroundStyle(accent) } } } else { @@ -198,7 +206,7 @@ struct SettingsView: View { } } - // Data Section + // 7. Data SheetSection(title: String(localized: "DATA"), icon: "externaldrive") { HStack { VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { @@ -240,7 +248,7 @@ struct SettingsView: View { } } - // Reset Button + // 8. Reset to Defaults Button { settings.resetToDefaults() hasChanges = true @@ -261,7 +269,7 @@ struct SettingsView: View { .padding(.horizontal) .padding(.top, Design.Spacing.small) - // App Version + // 9. App Version Text(appVersionString) .font(.system(size: Design.BaseFontSize.callout)) .foregroundStyle(.white.opacity(Design.Opacity.light)) @@ -294,7 +302,8 @@ struct SettingsView: View { } } -/// Deck count picker with visual options. +// MARK: - Deck Count Picker (Baccarat-specific) + struct DeckCountPicker: View { @Binding var selection: DeckCount @@ -305,6 +314,7 @@ struct DeckCountPicker: View { title: count.displayName, subtitle: count.description, isSelected: selection == count, + accentColor: Color.Settings.accent, action: { selection = count } ) } @@ -312,101 +322,8 @@ struct DeckCountPicker: View { } } -/// Starting balance picker. -struct BalancePicker: View { - @Binding var balance: Int - - private let options = [1_000, 5_000, 10_000, 25_000, 50_000, 100_000] - - var body: some View { - LazyVGrid(columns: [ - GridItem(.flexible()), - GridItem(.flexible()), - GridItem(.flexible()) - ], spacing: Design.Spacing.small) { - ForEach(options, id: \.self) { amount in - Button { - balance = amount - } label: { - Text("$\(amount / 1000)K") - .font(.system(size: Design.BaseFontSize.medium, weight: .bold)) - .foregroundStyle(balance == amount ? .black : .white) - .padding(.vertical, Design.Spacing.medium) - .frame(maxWidth: .infinity) - .background( - RoundedRectangle(cornerRadius: Design.CornerRadius.small) - .fill(balance == amount ? Color.yellow : Color.white.opacity(Design.Opacity.subtle)) - ) - } - .buttonStyle(.plain) - } - } - } -} +// MARK: - Table Limits Picker (Baccarat-specific) -/// A toggle setting row. -struct SettingsToggle: View { - let title: String - let subtitle: String - @Binding var isOn: Bool - - var body: some View { - Toggle(isOn: $isOn) { - VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { - Text(title) - .font(.system(size: Design.BaseFontSize.subheadline, weight: .medium)) - .foregroundStyle(.white) - - Text(subtitle) - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - } - .tint(.yellow) - .padding(.vertical, Design.Spacing.xSmall) - } -} - -/// Animation speed picker. -struct SpeedPicker: View { - @Binding var speed: Double - - private let options: [(String, Double)] = [ - ("Fast", 0.5), - ("Normal", 1.0), - ("Slow", 2.0) - ] - - var body: some View { - VStack(alignment: .leading, spacing: Design.Spacing.small) { - Text(String(localized: "Dealing Speed")) - .font(.system(size: Design.BaseFontSize.subheadline, weight: .medium)) - .foregroundStyle(.white) - - HStack(spacing: Design.Spacing.small) { - ForEach(options, id: \.1) { option in - Button { - speed = option.1 - } label: { - Text(option.0) - .font(.system(size: Design.BaseFontSize.callout, weight: .medium)) - .foregroundStyle(speed == option.1 ? .black : .white.opacity(Design.Opacity.strong)) - .padding(.vertical, Design.Spacing.small) - .frame(maxWidth: .infinity) - .background( - Capsule() - .fill(speed == option.1 ? Color.yellow : Color.white.opacity(Design.Opacity.subtle)) - ) - } - .buttonStyle(.plain) - } - } - } - .padding(.vertical, Design.Spacing.xSmall) - } -} - -/// Table limits picker for min/max bets. struct TableLimitsPicker: View { @Binding var selection: TableLimits @@ -417,7 +334,8 @@ struct TableLimitsPicker: View { title: limit.displayName, subtitle: limit.detailedDescription, isSelected: selection == limit, - badge: { BadgePill(text: limit.description, isSelected: selection == limit) }, + accentColor: Color.Settings.accent, + badge: { BadgePill(text: limit.description, isSelected: selection == limit, accentColor: Color.Settings.accent) }, action: { selection = limit } ) } @@ -425,42 +343,6 @@ struct TableLimitsPicker: View { } } -/// Volume slider for sound effects. -struct VolumePicker: View { - @Binding var volume: Float - - var body: some View { - VStack(alignment: .leading, spacing: Design.Spacing.small) { - HStack { - Text(String(localized: "Volume")) - .font(.system(size: Design.BaseFontSize.subheadline, weight: .medium)) - .foregroundStyle(.white) - - Spacer() - - Text("\(Int(volume * 100))%") - .font(.system(size: Design.BaseFontSize.body, weight: .medium, design: .rounded)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - - HStack(spacing: Design.Spacing.medium) { - Image(systemName: "speaker.fill") - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - - Slider(value: $volume, in: 0...1, step: 0.1) - .tint(.yellow) - - Image(systemName: "speaker.wave.3.fill") - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - } - .padding(.vertical, Design.Spacing.xSmall) - } -} - #Preview { SettingsView(settings: GameSettings(), gameState: GameState()) { } } - diff --git a/Blackjack/Blackjack/Models/GameSettings.swift b/Blackjack/Blackjack/Models/GameSettings.swift index e812825..b1459ea 100644 --- a/Blackjack/Blackjack/Models/GameSettings.swift +++ b/Blackjack/Blackjack/Models/GameSettings.swift @@ -269,5 +269,30 @@ final class GameSettings { ) persistence.save(data) } + + /// Resets all settings to defaults. + func resetToDefaults() { + gameStyle = .vegas + deckCount = .six + tableLimits = .low + startingBalance = 10_000 + dealerHitsSoft17 = false + doubleAfterSplit = true + resplitAces = false + lateSurrender = true + noHoleCard = false + blackjackPayout = 1.5 + insuranceAllowed = true + showAnimations = true + dealingSpeed = 1.0 + showCardsRemaining = true + showHistory = true + showHints = true + showCardCount = false + soundEnabled = true + hapticsEnabled = true + soundVolume = 1.0 + save() + } } diff --git a/Blackjack/Blackjack/Resources/Localizable.xcstrings b/Blackjack/Blackjack/Resources/Localizable.xcstrings index 9e31dfb..1bac0d2 100644 --- a/Blackjack/Blackjack/Resources/Localizable.xcstrings +++ b/Blackjack/Blackjack/Resources/Localizable.xcstrings @@ -1,6 +1,9 @@ { "sourceLanguage" : "en", "strings" : { + "-$%lld" : { + + }, "%@" : { "comment" : "A label displaying the current true count for a card counting practice session. The argument is the true count, formatted to one decimal place.", "isCommentAutoGenerated" : true @@ -50,6 +53,9 @@ "+%lld" : { "comment" : "A label that displays the running count of cards in a Hi-Lo card counting practice session. The text inside the label changes color based on whether the count is positive or negative.", "isCommentAutoGenerated" : true + }, + "+$%lld" : { + }, "1 Deck: Lowest house edge (~0.17%), rare to find." : { @@ -175,6 +181,28 @@ } } }, + "Add $%lld more to meet minimum" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add $%lld more to meet minimum" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añade $%lld más para alcanzar el mínimo" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajoutez %lld$ de plus pour atteindre le minimum" + } + } + } + }, "After generating:" : { "comment" : "A heading for instructions on how to use the IconGeneratorView.", "isCommentAutoGenerated" : true @@ -586,6 +614,9 @@ } } } + }, + "Cancel" : { + }, "Card Count" : { "localizations" : { @@ -719,28 +750,6 @@ } } }, - "Add $%lld more to meet minimum" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Add $%lld more to meet minimum" - } - }, - "es-MX" : { - "stringUnit" : { - "state" : "translated", - "value" : "Añade $%lld más para alcanzar el mínimo" - } - }, - "fr-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ajoutez %lld$ de plus pour atteindre le minimum" - } - } - } - }, "Clear" : { "localizations" : { "en" : { @@ -762,6 +771,15 @@ } } } + }, + "Clear All Data" : { + + }, + "Clear All Data?" : { + + }, + "CLOUD SYNC" : { + }, "Common shoe game" : { "comment" : "Description of a deck count option when the user selects 4 decks.", @@ -843,6 +861,9 @@ } } } + }, + "DATA" : { + }, "Deal" : { "localizations" : { @@ -1089,22 +1110,6 @@ } } }, - "Double (Count: 9v2 at TC≥+1)" : { - "comment" : "Explanation of a count-based deviation from the basic strategy, recommending to double down.", - "isCommentAutoGenerated" : true - }, - "Double (Count: 9v7 at TC≥+3)" : { - "comment" : "Strategy recommendation to double down when the player has a value of 9, is soft, has two cards, and the dealer's upcard is 7, with a true count of 3 or higher.", - "isCommentAutoGenerated" : true - }, - "Double (Count: 10v10 at TC≥+4)" : { - "comment" : "Explanation of a recommended Blackjack move based on a count-adjusted strategy, specifically for a pair of 10s against a 10.", - "isCommentAutoGenerated" : true - }, - "Double (Count: 10vA at TC≥+4)" : { - "comment" : "Text displayed in a notification when a user should double their bet in a Blackjack game.", - "isCommentAutoGenerated" : true - }, "Double After Split" : { "localizations" : { "en" : { @@ -1168,6 +1173,18 @@ "comment" : "Action available in Blackjack when the player wants to double their bet, take one more card, and then stand.", "isCommentAutoGenerated" : true }, + "Double instead of Hit (TC %@, deck favors doubling)" : { + "comment" : "Text explaining a Blackjack strategy recommendation to double down when the true count is favorable. The argument is the formatted true count.", + "isCommentAutoGenerated" : true + }, + "Double instead of Hit (TC %@, high cards favor you)" : { + "comment" : "A hint to double down when the true count and the dealer's upcard favor it. The argument is the formatted true count.", + "isCommentAutoGenerated" : true + }, + "Double instead of Hit (TC %@, slight edge to double)" : { + "comment" : "Text explaining a situation where doubling is recommended in Blackjack, based on the true count and the dealer's upcard. The argument is the formatted true count.", + "isCommentAutoGenerated" : true + }, "Double on 9, 10, or 11 only (some venues)." : { }, @@ -1408,13 +1425,12 @@ } } }, - "Hit (Count: 12v4 at TC<0)" : { - "comment" : "Explanation of a Blackjack hand value and true count that leads to recommending to hit.", + "Hit instead of Stand (TC %@, deck is poor)" : { + "comment" : "A hint to the player based on the true count, suggesting they should hit instead of stand. The argument is the true count, displayed with a plus sign if positive.", "isCommentAutoGenerated" : true }, - "Hit (Count: 13v2 at TC<-1)" : { - "comment" : "Explanation of a Blackjack hand value and the recommended action based on the true count.", - "isCommentAutoGenerated" : true + "Hit instead of Stand (TC %@, deck is very poor)" : { + }, "Hit on soft 17 or less." : { @@ -1448,6 +1464,12 @@ } } } + }, + "iCloud Sync" : { + + }, + "iCloud Unavailable" : { + }, "Icon" : { "comment" : "The label for the tab item representing the app icon preview.", @@ -1567,6 +1589,9 @@ "Jack, Queen, King: 10" : { "comment" : "Card value description for face cards (Jack, Queen, King).", "isCommentAutoGenerated" : true + }, + "Last Synced" : { + }, "Late Surrender" : { "localizations" : { @@ -1749,11 +1774,15 @@ } } } + }, + "Never" : { + }, "Never split 10s or 5s." : { }, "NEW GAME" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2075,6 +2104,9 @@ }, "Re-split Aces: Reduces house edge by ~0.05%." : { + }, + "Reset to Defaults" : { + }, "Roulette" : { "comment" : "The name of a roulette card.", @@ -2105,6 +2137,9 @@ } } } + }, + "Rounds Played" : { + }, "Rule Variations" : { @@ -2303,6 +2338,9 @@ } } } + }, + "Sign in to iCloud to sync progress" : { + }, "Single deck, higher variance" : { "comment" : "Description of a deck count option when the user selects one deck.", @@ -2378,14 +2416,6 @@ } } }, - "Split (Count: 10,10v5 at TC≥+5)" : { - "comment" : "Description of a strategy recommendation to split a pair of 10s when the true count is greater than or equal to 5.", - "isCommentAutoGenerated" : true - }, - "Split (Count: 10,10v6 at TC≥+4)" : { - "comment" : "Description of a count-based deviation from the basic strategy, specifically for a pair of 10s against a 6.", - "isCommentAutoGenerated" : true - }, "Split Hand" : { "extractionState" : "stale", "localizations" : { @@ -2409,6 +2439,10 @@ } } }, + "Split instead of Stand (TC %@, dealer very likely to bust)" : { + "comment" : "A hint to split a pair of 10s when the true count is high enough.", + "isCommentAutoGenerated" : true + }, "Split up to 4 hands, but not aces." : { }, @@ -2438,25 +2472,20 @@ } } }, - "Stand (Count: 12v2 at TC≥+3)" : { - "comment" : "Explanation of a count-based deviation from the basic strategy, including the count and the recommended action.", + "Stand instead of Hit (TC %@, dealer likely to bust)" : { + "comment" : "Text explaining a Blackjack strategy recommendation based on the true count. The argument is the formatted true count.", "isCommentAutoGenerated" : true }, - "Stand (Count: 12v3 at TC≥+2)" : { - "comment" : "Explanation of a count-adjusted strategy recommendation to stand in a 12-3 situation with a true count of 2.", + "Stand instead of Hit (TC %@, deck is extremely rich)" : { + "comment" : "Text provided in the \"Count Adjusted\" hint section of the Blackjack game UI, explaining a recommended action based on the true count and the current game state. The argument is the formatted true count.", "isCommentAutoGenerated" : true }, - "Stand (Count: 15v10 at TC≥+4)" : { - "comment" : "Explanation of a count-based deviation from the basic strategy.", + "Stand instead of Hit (TC %@, deck is neutral/rich)" : { + "comment" : "Explanation of a count-based deviation from the basic strategy, including the true count and a description of the deck situation.", "isCommentAutoGenerated" : true }, - "Stand (Count: 16v9 at TC≥+5)" : { - "comment" : "Explanation of a Blackjack game hint based on a count-adjusted strategy recommendation.", - "isCommentAutoGenerated" : true - }, - "Stand (Count: 16v10 at TC≥0)" : { - "comment" : "Explanation of a count-based deviation from the basic strategy, indicating that the recommended action is to stand.", - "isCommentAutoGenerated" : true + "Stand instead of Hit (TC %@, deck is very rich)" : { + }, "Stand on 17+ always." : { @@ -2471,6 +2500,9 @@ }, "Standard rules on the East Coast." : { + }, + "STARTING BALANCE" : { + }, "Statistics" : { "localizations" : { @@ -2592,6 +2624,12 @@ } } } + }, + "Sync Now" : { + + }, + "Sync progress across devices" : { + }, "TABLE LIMITS" : { "localizations" : { @@ -2640,6 +2678,12 @@ "These show how the same pattern works for other games" : { "comment" : "A description below the section of the view that previews icons for other games.", "isCommentAutoGenerated" : true + }, + "This will delete all saved progress, statistics, and reset your balance. This cannot be undone." : { + + }, + "Total Winnings" : { + }, "Traditional European casino style." : { @@ -2722,6 +2766,7 @@ } }, "Version %@ (%@)" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { diff --git a/Blackjack/Blackjack/Views/Sheets/SettingsView.swift b/Blackjack/Blackjack/Views/Sheets/SettingsView.swift index c600736..4242cde 100644 --- a/Blackjack/Blackjack/Views/Sheets/SettingsView.swift +++ b/Blackjack/Blackjack/Views/Sheets/SettingsView.swift @@ -13,38 +13,36 @@ struct SettingsView: View { let gameState: GameState? @Environment(\.dismiss) private var dismiss + @State private var showClearDataAlert = false + + /// App version string from bundle info + private var appVersionString: String { + let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0" + let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1" + return "Blackjack v\(version) (\(build))" + } + + /// Accent color for settings components + private let accent = Color.Settings.accent var body: some View { SheetContainerView( title: String(localized: "Settings"), content: { - // Game Style + // 1. Game Style (Blackjack-specific) SheetSection(title: String(localized: "GAME STYLE"), icon: "suit.club.fill") { GameStylePicker(selection: $settings.gameStyle) } - // Deck Settings - SheetSection(title: String(localized: "DECK SETTINGS"), icon: "rectangle.portrait.on.rectangle.portrait") { - DeckCountPicker(selection: $settings.deckCount) - } - .onChange(of: settings.deckCount) { _, _ in - // Reshuffle with new deck count - gameState?.applyDeckCountChange() - } - - // Table Limits - SheetSection(title: String(localized: "TABLE LIMITS"), icon: "banknote") { - TableLimitsPicker(selection: $settings.tableLimits) - } - - // Rule Options (for custom style) + // 2. Rules (Blackjack-specific, custom only) if settings.gameStyle == .custom { SheetSection(title: String(localized: "RULES"), icon: "list.bullet.clipboard") { VStack(spacing: Design.Spacing.small) { SettingsToggle( title: String(localized: "Dealer Hits Soft 17"), subtitle: String(localized: "H17 rule, increases house edge"), - isOn: $settings.dealerHitsSoft17 + isOn: $settings.dealerHitsSoft17, + accentColor: accent ) Divider().background(Color.white.opacity(Design.Opacity.hint)) @@ -52,7 +50,8 @@ struct SettingsView: View { SettingsToggle( title: String(localized: "Double After Split"), subtitle: String(localized: "Allow doubling on split hands"), - isOn: $settings.doubleAfterSplit + isOn: $settings.doubleAfterSplit, + accentColor: accent ) Divider().background(Color.white.opacity(Design.Opacity.hint)) @@ -60,7 +59,8 @@ struct SettingsView: View { SettingsToggle( title: String(localized: "Re-split Aces"), subtitle: String(localized: "Allow splitting aces again"), - isOn: $settings.resplitAces + isOn: $settings.resplitAces, + accentColor: accent ) Divider().background(Color.white.opacity(Design.Opacity.hint)) @@ -68,27 +68,54 @@ struct SettingsView: View { SettingsToggle( title: String(localized: "Late Surrender"), subtitle: String(localized: "Surrender after dealer checks for blackjack"), - isOn: $settings.lateSurrender + isOn: $settings.lateSurrender, + accentColor: accent ) } } } - // Display + // 3. Table Limits + SheetSection(title: String(localized: "TABLE LIMITS"), icon: "banknote") { + TableLimitsPicker(selection: $settings.tableLimits) + } + + // 4. Deck Settings + SheetSection(title: String(localized: "DECK SETTINGS"), icon: "rectangle.portrait.on.rectangle.portrait") { + DeckCountPicker(selection: $settings.deckCount) + } + .onChange(of: settings.deckCount) { _, _ in + gameState?.applyDeckCountChange() + } + + // 5. Starting Balance + SheetSection(title: String(localized: "STARTING BALANCE"), icon: "dollarsign.circle") { + BalancePicker(balance: $settings.startingBalance, accentColor: accent) + } + + // 6. Display SheetSection(title: String(localized: "DISPLAY"), icon: "eye") { VStack(spacing: Design.Spacing.small) { SettingsToggle( title: String(localized: "Show Animations"), subtitle: String(localized: "Card dealing animations"), - isOn: $settings.showAnimations + isOn: $settings.showAnimations, + accentColor: accent ) + if settings.showAnimations { + Divider().background(Color.white.opacity(Design.Opacity.hint)) + + SpeedPicker(speed: $settings.dealingSpeed, accentColor: accent) + } + Divider().background(Color.white.opacity(Design.Opacity.hint)) SettingsToggle( title: String(localized: "Show Hints"), subtitle: String(localized: "Basic strategy suggestions"), - isOn: $settings.showHints + isOn: $settings.showHints, + accentColor: accent ) Divider().background(Color.white.opacity(Design.Opacity.hint)) @@ -96,7 +123,8 @@ struct SettingsView: View { SettingsToggle( title: String(localized: "Card Count"), subtitle: String(localized: "Show Hi-Lo running count & card values"), - isOn: $settings.showCardCount + isOn: $settings.showCardCount, + accentColor: accent ) Divider().background(Color.white.opacity(Design.Opacity.hint)) @@ -104,61 +132,196 @@ struct SettingsView: View { SettingsToggle( title: String(localized: "Cards Remaining"), subtitle: String(localized: "Show cards left in shoe"), - isOn: $settings.showCardsRemaining + isOn: $settings.showCardsRemaining, + accentColor: accent ) - - Divider().background(Color.white.opacity(Design.Opacity.hint)) - - SpeedPicker(speed: $settings.dealingSpeed) } } - // Sound & Haptics + // 7. Sound & Haptics SheetSection(title: String(localized: "SOUND & HAPTICS"), icon: "speaker.wave.2") { VStack(spacing: Design.Spacing.small) { SettingsToggle( title: String(localized: "Sound Effects"), subtitle: String(localized: "Chips, cards, and results"), - isOn: $settings.soundEnabled + isOn: $settings.soundEnabled, + accentColor: accent ) .onChange(of: settings.soundEnabled) { _, newValue in SoundManager.shared.soundEnabled = newValue } + if settings.soundEnabled { + Divider().background(Color.white.opacity(Design.Opacity.hint)) + + VolumePicker(volume: $settings.soundVolume, accentColor: accent) + .onChange(of: settings.soundVolume) { _, newValue in + SoundManager.shared.volume = newValue + } + } + Divider().background(Color.white.opacity(Design.Opacity.hint)) SettingsToggle( title: String(localized: "Haptic Feedback"), subtitle: String(localized: "Vibration on actions"), - isOn: $settings.hapticsEnabled + isOn: $settings.hapticsEnabled, + accentColor: accent ) .onChange(of: settings.hapticsEnabled) { _, newValue in SoundManager.shared.hapticsEnabled = newValue } - - Divider().background(Color.white.opacity(Design.Opacity.hint)) - - VolumePicker(volume: $settings.soundVolume) - .onChange(of: settings.soundVolume) { _, newValue in - SoundManager.shared.volume = newValue - } } } - // Starting Balance - SheetSection(title: String(localized: "NEW GAME"), icon: "dollarsign.circle") { - BalancePicker(balance: $settings.startingBalance) + // 8. Cloud Sync + if let state = gameState { + SheetSection(title: String(localized: "CLOUD SYNC"), icon: "icloud") { + if state.persistence.iCloudAvailable { + Toggle(isOn: Binding( + get: { state.persistence.iCloudEnabled }, + set: { state.persistence.iCloudEnabled = $0 } + )) { + VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { + Text(String(localized: "iCloud Sync")) + .font(.system(size: Design.BaseFontSize.subheadline, weight: .medium)) + .foregroundStyle(.white) + + Text(String(localized: "Sync progress across devices")) + .font(.system(size: Design.BaseFontSize.body)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + } + } + .tint(accent) + .padding(.vertical, Design.Spacing.xSmall) + + if state.persistence.iCloudEnabled { + Divider() + .background(Color.white.opacity(Design.Opacity.subtle)) + + HStack { + Text(String(localized: "Last Synced")) + .font(.system(size: Design.BaseFontSize.body)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + + Spacer() + + if let lastSync = state.persistence.lastSyncDate { + Text(lastSync, style: .relative) + .font(.system(size: Design.BaseFontSize.body)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + } else { + Text(String(localized: "Never")) + .font(.system(size: Design.BaseFontSize.body)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + } + } + + Divider() + .background(Color.white.opacity(Design.Opacity.subtle)) + + Button { + state.persistence.sync() + } label: { + HStack { + Image(systemName: "arrow.triangle.2.circlepath") + Text(String(localized: "Sync Now")) + } + .font(.system(size: Design.BaseFontSize.body, weight: .medium)) + .foregroundStyle(accent) + } + } + } else { + HStack { + Image(systemName: "icloud.slash") + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + + VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { + Text(String(localized: "iCloud Unavailable")) + .font(.system(size: Design.BaseFontSize.subheadline, weight: .medium)) + .foregroundStyle(.white) + + Text(String(localized: "Sign in to iCloud to sync progress")) + .font(.system(size: Design.BaseFontSize.body)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + } + } + .padding(.vertical, Design.Spacing.xSmall) + } + } } - // Version info - if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, - let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { - Text(String(localized: "Version \(version) (\(build))")) - .font(.system(size: Design.BaseFontSize.small)) - .foregroundStyle(.white.opacity(Design.Opacity.light)) - .frame(maxWidth: .infinity) - .padding(.top, Design.Spacing.large) + // 9. Data + if let state = gameState { + SheetSection(title: String(localized: "DATA"), icon: "externaldrive") { + HStack { + VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { + Text(String(localized: "Rounds Played")) + .font(.system(size: Design.BaseFontSize.body)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + + Text("\(state.roundsPlayed)") + .font(.system(size: Design.BaseFontSize.xxLarge, weight: .bold, design: .rounded)) + .foregroundStyle(.white) + } + + Spacer() + + VStack(alignment: .trailing, spacing: Design.Spacing.xxSmall) { + Text(String(localized: "Total Winnings")) + .font(.system(size: Design.BaseFontSize.body)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + + let winnings = state.totalWinnings + Text(winnings >= 0 ? "+$\(winnings)" : "-$\(abs(winnings))") + .font(.system(size: Design.BaseFontSize.xxLarge, weight: .bold, design: .rounded)) + .foregroundStyle(winnings >= 0 ? .green : .red) + } + } + + Divider() + .background(Color.white.opacity(Design.Opacity.subtle)) + + Button(role: .destructive) { + showClearDataAlert = true + } label: { + HStack { + Image(systemName: "trash") + Text(String(localized: "Clear All Data")) + } + .font(.system(size: Design.BaseFontSize.body, weight: .medium)) + .foregroundStyle(.red) + } + } } + + // 10. Reset to Defaults + Button { + settings.resetToDefaults() + } label: { + HStack { + Image(systemName: "arrow.counterclockwise") + Text(String(localized: "Reset to Defaults")) + } + .font(.system(size: Design.BaseFontSize.medium, weight: .medium)) + .foregroundStyle(.red.opacity(Design.Opacity.heavy)) + .padding() + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: Design.CornerRadius.large) + .fill(Color.red.opacity(Design.Opacity.subtle)) + ) + } + .padding(.horizontal) + .padding(.top, Design.Spacing.small) + + // 11. Version info + Text(appVersionString) + .font(.system(size: Design.BaseFontSize.small)) + .foregroundStyle(.white.opacity(Design.Opacity.light)) + .frame(maxWidth: .infinity) + .padding(.top, Design.Spacing.large) + .padding(.bottom, Design.Spacing.medium) }, onCancel: nil, onDone: { @@ -167,10 +330,18 @@ struct SettingsView: View { }, doneButtonText: String(localized: "Done") ) + .alert(String(localized: "Clear All Data?"), isPresented: $showClearDataAlert) { + Button(String(localized: "Cancel"), role: .cancel) { } + Button(String(localized: "Clear"), role: .destructive) { + gameState?.clearAllData() + } + } message: { + Text(String(localized: "This will delete all saved progress, statistics, and reset your balance. This cannot be undone.")) + } } } -// MARK: - Game Style Picker +// MARK: - Game Style Picker (Blackjack-specific) struct GameStylePicker: View { @Binding var selection: BlackjackStyle @@ -190,7 +361,7 @@ struct GameStylePicker: View { } } -// MARK: - Deck Count Picker +// MARK: - Deck Count Picker (Blackjack-specific) struct DeckCountPicker: View { @Binding var selection: DeckCount @@ -210,7 +381,7 @@ struct DeckCountPicker: View { } } -// MARK: - Table Limits Picker +// MARK: - Table Limits Picker (Blackjack-specific) struct TableLimitsPicker: View { @Binding var selection: TableLimits @@ -234,4 +405,3 @@ struct TableLimitsPicker: View { #Preview { SettingsView(settings: GameSettings(), gameState: nil) } - diff --git a/CasinoKit/Sources/CasinoKit/Views/Settings/SettingsComponents.swift b/CasinoKit/Sources/CasinoKit/Views/Settings/SettingsComponents.swift index dbdd7ff..b2c63f4 100644 --- a/CasinoKit/Sources/CasinoKit/Views/Settings/SettingsComponents.swift +++ b/CasinoKit/Sources/CasinoKit/Views/Settings/SettingsComponents.swift @@ -20,15 +20,20 @@ public struct SettingsToggle: View { /// Binding to the toggle state. @Binding public var isOn: Bool + /// The accent color for the toggle. + public let accentColor: Color + /// Creates a settings toggle. /// - Parameters: /// - title: The main title. /// - subtitle: The subtitle description. /// - isOn: Binding to toggle state. - public init(title: String, subtitle: String, isOn: Binding) { + /// - accentColor: The accent color (default: yellow). + public init(title: String, subtitle: String, isOn: Binding, accentColor: Color = .yellow) { self.title = title self.subtitle = subtitle self._isOn = isOn + self.accentColor = accentColor } public var body: some View { @@ -43,7 +48,7 @@ public struct SettingsToggle: View { .foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium)) } } - .tint(.yellow) + .tint(accentColor) .padding(.vertical, CasinoDesign.Spacing.xSmall) } } @@ -55,6 +60,9 @@ public struct SpeedPicker: View { /// Binding to the speed value (0.5 = fast, 1.0 = normal, 2.0 = slow). @Binding public var speed: Double + /// The accent color for the selected button. + public let accentColor: Color + private let options: [(String, Double)] = [ ("Fast", 0.5), ("Normal", 1.0), @@ -62,9 +70,12 @@ public struct SpeedPicker: View { ] /// Creates a speed picker. - /// - Parameter speed: Binding to the speed multiplier. - public init(speed: Binding) { + /// - Parameters: + /// - speed: Binding to the speed multiplier. + /// - accentColor: The accent color (default: yellow). + public init(speed: Binding, accentColor: Color = .yellow) { self._speed = speed + self.accentColor = accentColor } public var body: some View { @@ -85,7 +96,7 @@ public struct SpeedPicker: View { .frame(maxWidth: .infinity) .background( Capsule() - .fill(speed == option.1 ? Color.yellow : Color.white.opacity(CasinoDesign.Opacity.subtle)) + .fill(speed == option.1 ? accentColor : Color.white.opacity(CasinoDesign.Opacity.subtle)) ) } .buttonStyle(.plain) @@ -103,10 +114,16 @@ public struct VolumePicker: View { /// Binding to the volume level (0.0 to 1.0). @Binding public var volume: Float + /// The accent color for the slider. + public let accentColor: Color + /// Creates a volume picker. - /// - Parameter volume: Binding to volume (0.0-1.0). - public init(volume: Binding) { + /// - Parameters: + /// - volume: Binding to volume (0.0-1.0). + /// - accentColor: The accent color (default: yellow). + public init(volume: Binding, accentColor: Color = .yellow) { self._volume = volume + self.accentColor = accentColor } public var body: some View { @@ -129,7 +146,7 @@ public struct VolumePicker: View { .foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium)) Slider(value: $volume, in: 0...1, step: 0.1) - .tint(.yellow) + .tint(accentColor) Image(systemName: "speaker.wave.3.fill") .font(.system(size: CasinoDesign.BaseFontSize.body)) @@ -332,16 +349,22 @@ public struct BalancePicker: View { /// The available balance options. public let options: [Int] + /// The accent color for the selected button. + public let accentColor: Color + /// Creates a balance picker. /// - Parameters: /// - balance: Binding to selected balance. /// - options: Available balance options (default: standard values). + /// - accentColor: The accent color (default: yellow). public init( balance: Binding, - options: [Int] = [1_000, 5_000, 10_000, 25_000, 50_000, 100_000] + options: [Int] = [1_000, 5_000, 10_000, 25_000, 50_000, 100_000], + accentColor: Color = .yellow ) { self._balance = balance self.options = options + self.accentColor = accentColor } public var body: some View { @@ -361,7 +384,7 @@ public struct BalancePicker: View { .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.small) - .fill(balance == amount ? Color.yellow : Color.white.opacity(CasinoDesign.Opacity.subtle)) + .fill(balance == amount ? accentColor : Color.white.opacity(CasinoDesign.Opacity.subtle)) ) } .buttonStyle(.plain) @@ -430,4 +453,3 @@ public struct BalancePicker: View { } .background(Color.Sheet.background) } -