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

This commit is contained in:
Matt Bruce 2025-12-22 15:39:27 -06:00
parent 7e16e67826
commit 12715cb646
9 changed files with 864 additions and 746 deletions

View File

@ -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" : {

View File

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

View File

@ -103,7 +103,7 @@ struct GameTableView: View {
}
}
}
.fullScreenCover(isPresented: $showRules) {
.sheet(isPresented: $showRules) {
RulesHelpView()
}
.sheet(isPresented: $showStats) {

View File

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

View File

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

View File

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

View File

@ -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" : {

View File

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

View File

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