Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
7e16e67826
commit
12715cb646
@ -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" : {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -103,7 +103,7 @@ struct GameTableView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showRules) {
|
||||
.sheet(isPresented: $showRules) {
|
||||
RulesHelpView()
|
||||
}
|
||||
.sheet(isPresented: $showStats) {
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
@ -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()) { }
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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" : {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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<Bool>) {
|
||||
/// - accentColor: The accent color (default: yellow).
|
||||
public init(title: String, subtitle: String, isOn: Binding<Bool>, 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<Double>) {
|
||||
/// - Parameters:
|
||||
/// - speed: Binding to the speed multiplier.
|
||||
/// - accentColor: The accent color (default: yellow).
|
||||
public init(speed: Binding<Double>, 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<Float>) {
|
||||
/// - Parameters:
|
||||
/// - volume: Binding to volume (0.0-1.0).
|
||||
/// - accentColor: The accent color (default: yellow).
|
||||
public init(volume: Binding<Float>, 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<Int>,
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user