diff --git a/Blackjack/Blackjack/Resources/Localizable.xcstrings b/Blackjack/Blackjack/Resources/Localizable.xcstrings index c4bc264..52f9db1 100644 --- a/Blackjack/Blackjack/Resources/Localizable.xcstrings +++ b/Blackjack/Blackjack/Resources/Localizable.xcstrings @@ -53,9 +53,21 @@ "localizations" : { "en" : { "stringUnit" : { - "state" : "new", + "state" : "translated", "value" : "%1$@ bet, pays up to %2$@" } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apuesta %1$@, paga hasta %2$@" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pari %1$@, paie jusqu'à %2$@" + } } } }, @@ -633,7 +645,27 @@ }, "21+3" : { "comment" : "Description of the 21+3 side bet type.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "21+3" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "21+3" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "21+3" + } + } + } }, "21+3: Poker hand from your cards + dealer's upcard." : { @@ -1814,7 +1846,27 @@ }, "Colored Pair" : { "comment" : "Description of a Perfect Pairs side bet result when the first two cards form a pair of the same rank but different suit.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Colored Pair" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Par de Color" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paire Colorée" + } + } + } }, "Colored Pair (same color): 12:1" : { @@ -2827,7 +2879,26 @@ }, "Enable Side Bets" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enable Side Bets" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activar Apuestas Laterales" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activer les Paris Annexes" + } + } + } }, "European" : { "localizations" : { @@ -2906,7 +2977,27 @@ }, "Flush" : { "comment" : "Description of a 21+3 side bet outcome when the user has a flush.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Flush" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Color" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Couleur" + } + } + } }, "Flush (same suit): 5:1" : { @@ -3918,7 +4009,27 @@ }, "MAX" : { "comment" : "The text displayed in the center of the chip badge when it represents the maximum bet.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "MAX" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "MÁX" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "MAX" + } + } + } }, "Max: $%@" : { "comment" : "A label displaying the maximum bet amount. The argument is the maximum bet amount.", @@ -3992,7 +4103,27 @@ }, "Mixed Pair" : { "comment" : "Description of a Perfect Pairs side bet outcome when the first two cards have the same rank but different color.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mixed Pair" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Par Mixto" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paire Mixte" + } + } + } }, "Mixed Pair (diff. color): 6:1" : { @@ -4131,7 +4262,27 @@ }, "No Hand" : { "comment" : "Description of a 21+3 side bet outcome when there is no qualifying hand.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Hand" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin Mano" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pas de Main" + } + } + } }, "No hole card, dealer stands on soft 17, no surrender" : { "localizations" : { @@ -4178,8 +4329,27 @@ } }, "No Pair" : { - "comment" : "Description of a 21+3 side bet outcome when there is no qualifying hand.", - "isCommentAutoGenerated" : true + "comment" : "Description of a Perfect Pairs side bet outcome when there is no pair.", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Pair" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin Par" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pas de Paire" + } + } + } }, "No surrender option." : { "localizations" : { @@ -4440,14 +4610,54 @@ }, "Perfect Pair" : { "comment" : "Name of the side bet outcome where the first two cards form a perfect pair.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Perfect Pair" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Par Perfecto" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paire Parfaite" + } + } + } }, "Perfect Pair (same suit): 25:1" : { }, "Perfect Pairs" : { "comment" : "Name of the Perfect Pairs side bet type.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Perfect Pairs" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pares Perfectos" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paires Parfaites" + } + } + } }, "Perfect Pairs (25:1) and 21+3 (100:1)" : { @@ -5136,7 +5346,26 @@ } }, "Side Bets" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Side Bets" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apuestas Laterales" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paris Annexes" + } + } + } }, "SIDE BETS" : { @@ -5604,21 +5833,81 @@ }, "Straight" : { "comment" : "Name of a 21+3 result when the user has three consecutive ranks.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Straight" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escalera" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suite" + } + } + } }, "Straight (consecutive): 10:1" : { }, "Straight Flush" : { "comment" : "Description of a 21+3 side bet outcome when the player has a straight flush.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Straight Flush" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escalera de Color" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quinte Flush" + } + } + } }, "Straight Flush: 40:1" : { }, "Suited Trips" : { "comment" : "Name of a 21+3 side bet outcome when the user has three cards of the same rank and suit.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suited Trips" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trío del Mismo Palo" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brelan Assorti" + } + } + } }, "Suited Trips (same rank & suit): 100:1" : { @@ -5917,7 +6206,27 @@ }, "Three of a Kind" : { "comment" : "Description of a 21+3 side bet outcome when they have three of a kind.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Three of a Kind" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trío" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brelan" + } + } + } }, "Three of a Kind: 30:1" : { diff --git a/Blackjack/Blackjack/Theme/DesignConstants.swift b/Blackjack/Blackjack/Theme/DesignConstants.swift index 978e7f2..dfa22c2 100644 --- a/Blackjack/Blackjack/Theme/DesignConstants.swift +++ b/Blackjack/Blackjack/Theme/DesignConstants.swift @@ -126,4 +126,13 @@ extension Color { enum TopBar { static let balance = Color(red: 0.95, green: 0.85, blue: 0.4) } + + // MARK: - Side Bet Colors + + enum SideBet { + /// Perfect Pairs - purple theme + static let perfectPairs = Color(red: 0.4, green: 0.2, blue: 0.5) + /// 21+3 - teal/cyan theme + static let twentyOnePlusThree = Color(red: 0.1, green: 0.4, blue: 0.5) + } } diff --git a/Blackjack/Blackjack/Views/Table/SideBetToastView.swift b/Blackjack/Blackjack/Views/Table/SideBetToastView.swift index 55e0a25..655a8cd 100644 --- a/Blackjack/Blackjack/Views/Table/SideBetToastView.swift +++ b/Blackjack/Blackjack/Views/Table/SideBetToastView.swift @@ -70,7 +70,10 @@ struct SideBetToastView: View { .shadow(color: borderColor.opacity(Design.Opacity.medium), radius: Design.Shadow.radiusMedium) .scaleEffect(isShowing ? 1.0 : 0.5) .opacity(isShowing ? 1.0 : 0) - .offset(x: isShowing ? 0 : (showOnLeft ? -50 : 50)) + .offset(x: isShowing ? 0 : (showOnLeft ? -Design.Spacing.toastSlide : Design.Spacing.toastSlide)) + .accessibilityElement(children: .combine) + .accessibilityLabel("\(title): \(result)") + .accessibilityValue(amountText) .onAppear { withAnimation(.spring(duration: Design.Animation.springDuration, bounce: 0.4).delay(showOnLeft ? 0 : Design.Animation.staggerDelay1)) { isShowing = true diff --git a/Blackjack/Blackjack/Views/Table/SideBetZoneView.swift b/Blackjack/Blackjack/Views/Table/SideBetZoneView.swift index fecf366..adce23c 100644 --- a/Blackjack/Blackjack/Views/Table/SideBetZoneView.swift +++ b/Blackjack/Blackjack/Views/Table/SideBetZoneView.swift @@ -100,7 +100,7 @@ struct ChipBadgeView: View { .frame(width: CasinoDesign.Size.chipBadgeInner, height: CasinoDesign.Size.chipBadgeInner) if isMax { - Text("MAX") + Text(String(localized: "MAX")) .font(.system(size: Design.BaseFontSize.xxSmall, weight: .black)) .foregroundStyle(.white) } else { @@ -120,17 +120,6 @@ struct ChipBadgeView: View { } } -// MARK: - Side Bet Colors - -extension Color { - enum SideBet { - /// Perfect Pairs - purple theme - static let perfectPairs = Color(red: 0.4, green: 0.2, blue: 0.5) - /// 21+3 - teal/cyan theme - static let twentyOnePlusThree = Color(red: 0.1, green: 0.4, blue: 0.5) - } -} - // MARK: - Previews #Preview("Perfect Pairs") { diff --git a/CasinoKit/Sources/CasinoKit/Audio/SoundManager.swift b/CasinoKit/Sources/CasinoKit/Audio/SoundManager.swift index a65f19a..31a12e0 100644 --- a/CasinoKit/Sources/CasinoKit/Audio/SoundManager.swift +++ b/CasinoKit/Sources/CasinoKit/Audio/SoundManager.swift @@ -99,7 +99,7 @@ public final class SoundManager { } } - /// Master volume (0.0 to 1.0). + /// Master volume (0.0 to 1.0). This is the linear slider value. public var volume: Float = 1.0 { didSet { UserDefaults.standard.set(volume, forKey: "casinokit.soundVolume") @@ -107,6 +107,15 @@ public final class SoundManager { } } + /// Perceived volume using an exponential curve. + /// Human hearing is logarithmic, so linear volume feels wrong. + /// This curve makes 50% on the slider sound like 50% to human ears. + private var perceivedVolume: Float { + // Using a power of 3 gives a natural-feeling curve + // 0.0 -> 0.0, 0.5 -> 0.125, 1.0 -> 1.0 + pow(volume, 3) + } + /// The bundle to load sound files from. Defaults to CasinoKit's bundle. /// Set this to `.main` if sounds are in your app bundle instead. public var soundBundle: Bundle = .module @@ -166,7 +175,7 @@ public final class SoundManager { do { let player = try AVAudioPlayer(contentsOf: url) player.prepareToPlay() - player.volume = volume + player.volume = perceivedVolume return player } catch { print("CasinoKit: Failed to create audio player for \(sound.rawValue): \(error)") @@ -193,8 +202,8 @@ public final class SoundManager { /// Plays a system sound by ID. private func playSystemSound(_ soundID: SystemSoundID) { - // Only play if volume is above threshold - guard volume > 0.1 else { return } + // Only play if volume is above threshold (system sounds can't be volume-adjusted) + guard perceivedVolume > 0.01 else { return } AudioServicesPlaySystemSound(soundID) } @@ -223,7 +232,7 @@ public final class SoundManager { private func updatePlayerVolumes() { for player in audioPlayers.values { - player.volume = volume + player.volume = perceivedVolume } } diff --git a/CasinoKit/Sources/CasinoKit/Theme/CasinoDesign.swift b/CasinoKit/Sources/CasinoKit/Theme/CasinoDesign.swift index 4c98c63..ac09420 100644 --- a/CasinoKit/Sources/CasinoKit/Theme/CasinoDesign.swift +++ b/CasinoKit/Sources/CasinoKit/Theme/CasinoDesign.swift @@ -22,6 +22,9 @@ public enum CasinoDesign { public static let xLarge: CGFloat = 20 public static let xxLarge: CGFloat = 24 public static let xxxLarge: CGFloat = 32 + + /// Slide distance for toast animations + public static let toastSlide: CGFloat = 50 } // MARK: - Corner Radius