Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
d2b93a019f
commit
deedc08b85
289
Baccarat/Audio/SoundManager.swift
Normal file
289
Baccarat/Audio/SoundManager.swift
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
//
|
||||||
|
// SoundManager.swift
|
||||||
|
// Baccarat
|
||||||
|
//
|
||||||
|
// Manages game sound effects.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AVFoundation
|
||||||
|
import AudioToolbox
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Types of sound effects used in the game.
|
||||||
|
enum GameSound: String, CaseIterable {
|
||||||
|
case chipPlace = "chip_place" // When placing a bet
|
||||||
|
case chipStack = "chip_stack" // Stacking multiple chips
|
||||||
|
case cardDeal = "card_deal" // Dealing a card
|
||||||
|
case cardFlip = "card_flip" // Flipping a card face up
|
||||||
|
case cardShuffle = "card_shuffle" // Shuffling the deck
|
||||||
|
case win = "win" // Player wins
|
||||||
|
case lose = "lose" // Player loses
|
||||||
|
case push = "push" // Tie/push result
|
||||||
|
case bigWin = "big_win" // Large payout
|
||||||
|
case buttonTap = "button_tap" // UI button tap
|
||||||
|
case newRound = "new_round" // Starting a new round
|
||||||
|
case clearBets = "clear_bets" // Clearing all bets
|
||||||
|
case gameOver = "game_over" // Out of chips / game over
|
||||||
|
|
||||||
|
/// File extension for the sound file.
|
||||||
|
var fileExtension: String { "mp3" }
|
||||||
|
|
||||||
|
/// System sound ID to use as fallback when custom sound file is missing.
|
||||||
|
/// These are built-in iOS sounds that approximate the intended effect.
|
||||||
|
var fallbackSystemSound: SystemSoundID {
|
||||||
|
switch self {
|
||||||
|
case .chipPlace: return 1104 // Key press click
|
||||||
|
case .chipStack: return 1105 // Keyboard key
|
||||||
|
case .cardDeal: return 1306 // Swoosh
|
||||||
|
case .cardFlip: return 1104 // Click
|
||||||
|
case .cardShuffle: return 1110 // Swish
|
||||||
|
case .win: return 1025 // Success chime
|
||||||
|
case .lose: return 1053 // Error tone
|
||||||
|
case .push: return 1057 // Neutral beep
|
||||||
|
case .bigWin: return 1026 // Fanfare
|
||||||
|
case .buttonTap: return 1104 // Tap
|
||||||
|
case .newRound: return 1113 // Begin
|
||||||
|
case .clearBets: return 1155 // Sweep
|
||||||
|
case .gameOver: return 1073 // Sad trombone / failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display name for settings.
|
||||||
|
var displayName: String {
|
||||||
|
switch self {
|
||||||
|
case .chipPlace: return "Chip Place"
|
||||||
|
case .chipStack: return "Chip Stack"
|
||||||
|
case .cardDeal: return "Card Deal"
|
||||||
|
case .cardFlip: return "Card Flip"
|
||||||
|
case .cardShuffle: return "Card Shuffle"
|
||||||
|
case .win: return "Win"
|
||||||
|
case .lose: return "Lose"
|
||||||
|
case .push: return "Push/Tie"
|
||||||
|
case .bigWin: return "Big Win"
|
||||||
|
case .buttonTap: return "Button Tap"
|
||||||
|
case .newRound: return "New Round"
|
||||||
|
case .clearBets: return "Clear Bets"
|
||||||
|
case .gameOver: return "Game Over"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manages playing game sound effects.
|
||||||
|
@MainActor
|
||||||
|
@Observable
|
||||||
|
final class SoundManager {
|
||||||
|
|
||||||
|
// MARK: - Singleton
|
||||||
|
|
||||||
|
static let shared = SoundManager()
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
|
||||||
|
/// Whether sound effects are enabled.
|
||||||
|
var soundEnabled: Bool = true {
|
||||||
|
didSet {
|
||||||
|
UserDefaults.standard.set(soundEnabled, forKey: "soundEnabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Master volume (0.0 to 1.0).
|
||||||
|
var volume: Float = 1.0 {
|
||||||
|
didSet {
|
||||||
|
UserDefaults.standard.set(volume, forKey: "soundVolume")
|
||||||
|
updatePlayerVolumes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether to use system sounds as fallback (true until custom sounds are added).
|
||||||
|
private var useSystemSoundsFallback: Bool = true
|
||||||
|
|
||||||
|
/// Cache of audio players for quick playback.
|
||||||
|
private var audioPlayers: [GameSound: AVAudioPlayer] = [:]
|
||||||
|
|
||||||
|
// MARK: - Initialization
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
// Load saved preferences
|
||||||
|
soundEnabled = UserDefaults.standard.object(forKey: "soundEnabled") as? Bool ?? true
|
||||||
|
volume = UserDefaults.standard.object(forKey: "soundVolume") as? Float ?? 1.0
|
||||||
|
|
||||||
|
// Configure audio session
|
||||||
|
configureAudioSession()
|
||||||
|
|
||||||
|
// Preload sounds
|
||||||
|
preloadSounds()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Audio Session
|
||||||
|
|
||||||
|
private func configureAudioSession() {
|
||||||
|
do {
|
||||||
|
try AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default)
|
||||||
|
try AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
} catch {
|
||||||
|
print("Failed to configure audio session: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Preloading
|
||||||
|
|
||||||
|
/// Preloads all sound files for faster playback.
|
||||||
|
private func preloadSounds() {
|
||||||
|
var foundCustomSound = false
|
||||||
|
|
||||||
|
for sound in GameSound.allCases {
|
||||||
|
if let player = createPlayer(for: sound) {
|
||||||
|
audioPlayers[sound] = player
|
||||||
|
foundCustomSound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any custom sounds are found, disable fallback for all sounds
|
||||||
|
useSystemSoundsFallback = !foundCustomSound
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createPlayer(for sound: GameSound) -> AVAudioPlayer? {
|
||||||
|
guard let url = Bundle.main.url(
|
||||||
|
forResource: sound.rawValue,
|
||||||
|
withExtension: sound.fileExtension
|
||||||
|
) else {
|
||||||
|
// Sound file not found - will use system sound fallback
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let player = try AVAudioPlayer(contentsOf: url)
|
||||||
|
player.prepareToPlay()
|
||||||
|
player.volume = volume
|
||||||
|
return player
|
||||||
|
} catch {
|
||||||
|
print("Failed to create audio player for \(sound.rawValue): \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Playback
|
||||||
|
|
||||||
|
/// Plays a sound effect.
|
||||||
|
/// - Parameter sound: The sound to play.
|
||||||
|
func play(_ sound: GameSound) {
|
||||||
|
guard soundEnabled else { return }
|
||||||
|
|
||||||
|
// Try custom sound first
|
||||||
|
if let player = audioPlayers[sound] {
|
||||||
|
player.currentTime = 0
|
||||||
|
player.play()
|
||||||
|
} else {
|
||||||
|
// Fall back to system sound
|
||||||
|
playSystemSound(sound.fallbackSystemSound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays a system sound by ID.
|
||||||
|
private func playSystemSound(_ soundID: SystemSoundID) {
|
||||||
|
// Only play if volume is above threshold
|
||||||
|
guard volume > 0.1 else { return }
|
||||||
|
AudioServicesPlaySystemSound(soundID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays a sound effect with a delay.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - sound: The sound to play.
|
||||||
|
/// - delay: Delay in seconds before playing.
|
||||||
|
func play(_ sound: GameSound, delay: TimeInterval) {
|
||||||
|
Task {
|
||||||
|
try? await Task.sleep(for: .seconds(delay))
|
||||||
|
play(sound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays multiple sounds in sequence.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - sounds: Array of sounds to play.
|
||||||
|
/// - interval: Time between each sound.
|
||||||
|
func playSequence(_ sounds: [GameSound], interval: TimeInterval = 0.1) {
|
||||||
|
for (index, sound) in sounds.enumerated() {
|
||||||
|
play(sound, delay: TimeInterval(index) * interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Volume
|
||||||
|
|
||||||
|
private func updatePlayerVolumes() {
|
||||||
|
for player in audioPlayers.values {
|
||||||
|
player.volume = volume
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Haptics
|
||||||
|
|
||||||
|
/// Plays a light haptic feedback.
|
||||||
|
func hapticLight() {
|
||||||
|
let impact = UIImpactFeedbackGenerator(style: .light)
|
||||||
|
impact.impactOccurred()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays a medium haptic feedback.
|
||||||
|
func hapticMedium() {
|
||||||
|
let impact = UIImpactFeedbackGenerator(style: .medium)
|
||||||
|
impact.impactOccurred()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays a success haptic notification.
|
||||||
|
func hapticSuccess() {
|
||||||
|
let notification = UINotificationFeedbackGenerator()
|
||||||
|
notification.notificationOccurred(.success)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays an error haptic notification.
|
||||||
|
func hapticError() {
|
||||||
|
let notification = UINotificationFeedbackGenerator()
|
||||||
|
notification.notificationOccurred(.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Convenience Extensions
|
||||||
|
|
||||||
|
extension SoundManager {
|
||||||
|
|
||||||
|
/// Plays chip placement sound with haptic.
|
||||||
|
func playChipPlace() {
|
||||||
|
play(.chipPlace)
|
||||||
|
hapticLight()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays card deal sound.
|
||||||
|
func playCardDeal() {
|
||||||
|
play(.cardDeal)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays card flip sound.
|
||||||
|
func playCardFlip() {
|
||||||
|
play(.cardFlip)
|
||||||
|
hapticLight()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays win sound with haptic.
|
||||||
|
func playWin(isBigWin: Bool = false) {
|
||||||
|
play(isBigWin ? .bigWin : .win)
|
||||||
|
hapticSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays lose sound with haptic.
|
||||||
|
func playLose() {
|
||||||
|
play(.lose)
|
||||||
|
hapticError()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays push/tie sound.
|
||||||
|
func playPush() {
|
||||||
|
play(.push)
|
||||||
|
hapticMedium()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays game over sound with haptic.
|
||||||
|
func playGameOver() {
|
||||||
|
play(.gameOver)
|
||||||
|
hapticError()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -51,6 +51,9 @@ final class GameState {
|
|||||||
// MARK: - Settings
|
// MARK: - Settings
|
||||||
let settings: GameSettings
|
let settings: GameSettings
|
||||||
|
|
||||||
|
// MARK: - Sound
|
||||||
|
private let sound = SoundManager.shared
|
||||||
|
|
||||||
// MARK: - Game Engine
|
// MARK: - Game Engine
|
||||||
private(set) var engine: BaccaratEngine
|
private(set) var engine: BaccaratEngine
|
||||||
|
|
||||||
@ -224,13 +227,29 @@ final class GameState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
balance -= amount
|
balance -= amount
|
||||||
|
|
||||||
|
// Play chip placement sound
|
||||||
|
if settings.soundEnabled {
|
||||||
|
sound.play(.chipPlace)
|
||||||
|
}
|
||||||
|
if settings.hapticsEnabled {
|
||||||
|
sound.hapticLight()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears all current bets and returns the amounts to balance.
|
/// Clears all current bets and returns the amounts to balance.
|
||||||
func clearBets() {
|
func clearBets() {
|
||||||
guard canPlaceBet else { return }
|
guard canPlaceBet, !currentBets.isEmpty else { return }
|
||||||
balance += totalBetAmount
|
balance += totalBetAmount
|
||||||
currentBets = []
|
currentBets = []
|
||||||
|
|
||||||
|
// Play clear bets sound
|
||||||
|
if settings.soundEnabled {
|
||||||
|
sound.play(.clearBets)
|
||||||
|
}
|
||||||
|
if settings.hapticsEnabled {
|
||||||
|
sound.hapticMedium()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Undoes the last bet placed.
|
/// Undoes the last bet placed.
|
||||||
@ -270,6 +289,11 @@ final class GameState {
|
|||||||
for (index, card) in initialCards.enumerated() {
|
for (index, card) in initialCards.enumerated() {
|
||||||
try? await Task.sleep(for: dealDelay)
|
try? await Task.sleep(for: dealDelay)
|
||||||
|
|
||||||
|
// Play card deal sound
|
||||||
|
if settings.soundEnabled {
|
||||||
|
sound.play(.cardDeal)
|
||||||
|
}
|
||||||
|
|
||||||
if index % 2 == 0 {
|
if index % 2 == 0 {
|
||||||
visiblePlayerCards.append(card)
|
visiblePlayerCards.append(card)
|
||||||
playerCardsFaceUp.append(false)
|
playerCardsFaceUp.append(false)
|
||||||
@ -282,6 +306,11 @@ final class GameState {
|
|||||||
// Brief pause then flip cards
|
// Brief pause then flip cards
|
||||||
try? await Task.sleep(for: flipDelay)
|
try? await Task.sleep(for: flipDelay)
|
||||||
|
|
||||||
|
// Play card flip sound
|
||||||
|
if settings.soundEnabled {
|
||||||
|
sound.play(.cardFlip)
|
||||||
|
}
|
||||||
|
|
||||||
// Flip all cards face up
|
// Flip all cards face up
|
||||||
for i in 0..<playerCardsFaceUp.count {
|
for i in 0..<playerCardsFaceUp.count {
|
||||||
playerCardsFaceUp[i] = true
|
playerCardsFaceUp[i] = true
|
||||||
@ -315,9 +344,15 @@ final class GameState {
|
|||||||
if let playerThird = engine.drawPlayerThirdCard() {
|
if let playerThird = engine.drawPlayerThirdCard() {
|
||||||
if settings.showAnimations {
|
if settings.showAnimations {
|
||||||
try? await Task.sleep(for: dealDelay)
|
try? await Task.sleep(for: dealDelay)
|
||||||
|
if settings.soundEnabled {
|
||||||
|
sound.play(.cardDeal)
|
||||||
|
}
|
||||||
visiblePlayerCards.append(playerThird)
|
visiblePlayerCards.append(playerThird)
|
||||||
playerCardsFaceUp.append(false)
|
playerCardsFaceUp.append(false)
|
||||||
try? await Task.sleep(for: shortDelay)
|
try? await Task.sleep(for: shortDelay)
|
||||||
|
if settings.soundEnabled {
|
||||||
|
sound.play(.cardFlip)
|
||||||
|
}
|
||||||
playerCardsFaceUp[2] = true
|
playerCardsFaceUp[2] = true
|
||||||
try? await Task.sleep(for: flipDelay)
|
try? await Task.sleep(for: flipDelay)
|
||||||
} else {
|
} else {
|
||||||
@ -331,9 +366,15 @@ final class GameState {
|
|||||||
if let bankerThird = engine.drawBankerThirdCard() {
|
if let bankerThird = engine.drawBankerThirdCard() {
|
||||||
if settings.showAnimations {
|
if settings.showAnimations {
|
||||||
try? await Task.sleep(for: dealDelay)
|
try? await Task.sleep(for: dealDelay)
|
||||||
|
if settings.soundEnabled {
|
||||||
|
sound.play(.cardDeal)
|
||||||
|
}
|
||||||
visibleBankerCards.append(bankerThird)
|
visibleBankerCards.append(bankerThird)
|
||||||
bankerCardsFaceUp.append(false)
|
bankerCardsFaceUp.append(false)
|
||||||
try? await Task.sleep(for: shortDelay)
|
try? await Task.sleep(for: shortDelay)
|
||||||
|
if settings.soundEnabled {
|
||||||
|
sound.play(.cardFlip)
|
||||||
|
}
|
||||||
bankerCardsFaceUp[2] = true
|
bankerCardsFaceUp[2] = true
|
||||||
try? await Task.sleep(for: dealDelay)
|
try? await Task.sleep(for: dealDelay)
|
||||||
} else {
|
} else {
|
||||||
@ -381,6 +422,34 @@ final class GameState {
|
|||||||
betResults = results
|
betResults = results
|
||||||
lastWinnings = totalWinnings
|
lastWinnings = totalWinnings
|
||||||
|
|
||||||
|
// Play result sound
|
||||||
|
if totalWinnings > 0 {
|
||||||
|
// Determine if it's a big win (>= 5x any bet amount or >= 500)
|
||||||
|
let maxBetAmount = currentBets.map(\.amount).max() ?? 0
|
||||||
|
let isBigWin = totalWinnings >= maxBetAmount * 5 || totalWinnings >= 500
|
||||||
|
if settings.soundEnabled {
|
||||||
|
sound.play(isBigWin ? .bigWin : .win)
|
||||||
|
}
|
||||||
|
if settings.hapticsEnabled {
|
||||||
|
sound.hapticSuccess()
|
||||||
|
}
|
||||||
|
} else if totalWinnings < 0 {
|
||||||
|
if settings.soundEnabled {
|
||||||
|
sound.play(.lose)
|
||||||
|
}
|
||||||
|
if settings.hapticsEnabled {
|
||||||
|
sound.hapticError()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Push (tie with main bet push)
|
||||||
|
if settings.soundEnabled {
|
||||||
|
sound.play(.push)
|
||||||
|
}
|
||||||
|
if settings.hapticsEnabled {
|
||||||
|
sound.hapticMedium()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Record result in history
|
// Record result in history
|
||||||
roundHistory.append(RoundResult(
|
roundHistory.append(RoundResult(
|
||||||
result: result,
|
result: result,
|
||||||
@ -400,6 +469,11 @@ final class GameState {
|
|||||||
func newRound() {
|
func newRound() {
|
||||||
guard currentPhase == .roundComplete else { return }
|
guard currentPhase == .roundComplete else { return }
|
||||||
|
|
||||||
|
// Play new round sound
|
||||||
|
if settings.soundEnabled {
|
||||||
|
sound.play(.newRound)
|
||||||
|
}
|
||||||
|
|
||||||
// Dismiss result banner
|
// Dismiss result banner
|
||||||
showResultBanner = false
|
showResultBanner = false
|
||||||
|
|
||||||
@ -443,6 +517,14 @@ final class GameState {
|
|||||||
playerHadPair = false
|
playerHadPair = false
|
||||||
bankerHadPair = false
|
bankerHadPair = false
|
||||||
betResults = []
|
betResults = []
|
||||||
|
|
||||||
|
// Play new game sound
|
||||||
|
if settings.soundEnabled {
|
||||||
|
sound.play(.newRound)
|
||||||
|
}
|
||||||
|
if settings.hapticsEnabled {
|
||||||
|
sound.hapticMedium()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Applies new settings (call after settings change).
|
/// Applies new settings (call after settings change).
|
||||||
|
|||||||
@ -133,6 +133,17 @@ final class GameSettings {
|
|||||||
/// Whether to show the history road map.
|
/// Whether to show the history road map.
|
||||||
var showHistory: Bool = true
|
var showHistory: Bool = true
|
||||||
|
|
||||||
|
// MARK: - Sound Settings
|
||||||
|
|
||||||
|
/// Whether sound effects are enabled.
|
||||||
|
var soundEnabled: Bool = true
|
||||||
|
|
||||||
|
/// Whether haptic feedback is enabled.
|
||||||
|
var hapticsEnabled: Bool = true
|
||||||
|
|
||||||
|
/// Volume level for sound effects (0.0 to 1.0).
|
||||||
|
var soundVolume: Float = 1.0
|
||||||
|
|
||||||
// MARK: - Persistence Keys
|
// MARK: - Persistence Keys
|
||||||
|
|
||||||
private enum Keys {
|
private enum Keys {
|
||||||
@ -143,6 +154,9 @@ final class GameSettings {
|
|||||||
static let dealingSpeed = "settings.dealingSpeed"
|
static let dealingSpeed = "settings.dealingSpeed"
|
||||||
static let showCardsRemaining = "settings.showCardsRemaining"
|
static let showCardsRemaining = "settings.showCardsRemaining"
|
||||||
static let showHistory = "settings.showHistory"
|
static let showHistory = "settings.showHistory"
|
||||||
|
static let soundEnabled = "settings.soundEnabled"
|
||||||
|
static let hapticsEnabled = "settings.hapticsEnabled"
|
||||||
|
static let soundVolume = "settings.soundVolume"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
@ -186,6 +200,18 @@ final class GameSettings {
|
|||||||
if defaults.object(forKey: Keys.showHistory) != nil {
|
if defaults.object(forKey: Keys.showHistory) != nil {
|
||||||
self.showHistory = defaults.bool(forKey: Keys.showHistory)
|
self.showHistory = defaults.bool(forKey: Keys.showHistory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if defaults.object(forKey: Keys.soundEnabled) != nil {
|
||||||
|
self.soundEnabled = defaults.bool(forKey: Keys.soundEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if defaults.object(forKey: Keys.hapticsEnabled) != nil {
|
||||||
|
self.hapticsEnabled = defaults.bool(forKey: Keys.hapticsEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let volume = defaults.object(forKey: Keys.soundVolume) as? Float {
|
||||||
|
self.soundVolume = volume
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Saves settings to UserDefaults.
|
/// Saves settings to UserDefaults.
|
||||||
@ -198,6 +224,9 @@ final class GameSettings {
|
|||||||
defaults.set(dealingSpeed, forKey: Keys.dealingSpeed)
|
defaults.set(dealingSpeed, forKey: Keys.dealingSpeed)
|
||||||
defaults.set(showCardsRemaining, forKey: Keys.showCardsRemaining)
|
defaults.set(showCardsRemaining, forKey: Keys.showCardsRemaining)
|
||||||
defaults.set(showHistory, forKey: Keys.showHistory)
|
defaults.set(showHistory, forKey: Keys.showHistory)
|
||||||
|
defaults.set(soundEnabled, forKey: Keys.soundEnabled)
|
||||||
|
defaults.set(hapticsEnabled, forKey: Keys.hapticsEnabled)
|
||||||
|
defaults.set(soundVolume, forKey: Keys.soundVolume)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets all settings to defaults.
|
/// Resets all settings to defaults.
|
||||||
@ -209,6 +238,9 @@ final class GameSettings {
|
|||||||
dealingSpeed = 1.0
|
dealingSpeed = 1.0
|
||||||
showCardsRemaining = true
|
showCardsRemaining = true
|
||||||
showHistory = true
|
showHistory = true
|
||||||
|
soundEnabled = true
|
||||||
|
hapticsEnabled = true
|
||||||
|
soundVolume = 1.0
|
||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,6 +46,10 @@
|
|||||||
"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.",
|
"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
|
"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" : {
|
"%lldpx" : {
|
||||||
"comment" : "A text label displaying the size of the app icon. The argument is the size of the icon in pixels.",
|
"comment" : "A text label displaying the size of the app icon. The argument is the size of the icon in pixels.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -412,6 +416,88 @@
|
|||||||
"comment" : "A section header that suggests using an online tool to generate app icon sizes.",
|
"comment" : "A section header that suggests using an online tool to generate app icon sizes.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Animate dealing and flipping" : {
|
||||||
|
"comment" : "Subtitle for card animations toggle.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Animate dealing and flipping"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Animar reparto y volteo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Animar reparto y volteo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Animar reparto y volteo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Animer la distribution et le retournement"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Animer la distribution et le retournement"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ANIMATIONS" : {
|
||||||
|
"comment" : "Section header for animation settings.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "ANIMATIONS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "ANIMACIONES"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "ANIMACIONES"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "ANIMACIONES"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "ANIMATIONS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "ANIMATIONS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"App Icon" : {
|
"App Icon" : {
|
||||||
"comment" : "A label displayed above the preview of the app icon.",
|
"comment" : "A label displayed above the preview of the app icon.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -998,6 +1084,47 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Card Animations" : {
|
||||||
|
"comment" : "Toggle label for card animation setting.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Card Animations"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Animaciones de Cartas"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Animaciones de Cartas"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Animaciones de Cartas"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Animations des Cartes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Animations des Cartes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Card Values" : {
|
"Card Values" : {
|
||||||
"comment" : "A heading that explains the values of playing cards.",
|
"comment" : "A heading that explains the values of playing cards.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -1121,6 +1248,47 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Chips, cards, and result sounds" : {
|
||||||
|
"comment" : "Subtitle describing sound effects toggle.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Chips, cards, and result sounds"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Sonidos de fichas, cartas y resultados"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Sonidos de fichas, cartas y resultados"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Sonidos de fichas, cartas y resultados"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Sons des jetons, cartes et résultats"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Sons des jetons, cartes et résultats"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Clear" : {
|
"Clear" : {
|
||||||
"comment" : "The label of a button that clears all current bets in the game.",
|
"comment" : "The label of a button that clears all current bets in the game.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -1285,6 +1453,170 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"DECK SETTINGS" : {
|
||||||
|
"comment" : "Section header for deck configuration settings.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "DECK SETTINGS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "CONFIGURACIÓN DE BARAJA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "CONFIGURACIÓN DE BARAJA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "CONFIGURACIÓN DE BARAJA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "PARAMÈTRES DU JEU"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "PARAMÈTRES DU JEU"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DISPLAY" : {
|
||||||
|
"comment" : "Section header for display settings.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "DISPLAY"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "PANTALLA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "PANTALLA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "PANTALLA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "AFFICHAGE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "AFFICHAGE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Display deck counter at top" : {
|
||||||
|
"comment" : "Subtitle for show cards remaining toggle.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Display deck counter at top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mostrar contador de baraja arriba"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mostrar contador de baraja arriba"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mostrar contador de baraja arriba"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Afficher le compteur de cartes en haut"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Afficher le compteur de cartes en haut"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Display result road map" : {
|
||||||
|
"comment" : "Subtitle for show history toggle.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Display result road map"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mostrar historial de resultados"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mostrar historial de resultados"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mostrar historial de resultados"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Afficher l'historique des résultats"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Afficher l'historique des résultats"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Done" : {
|
"Done" : {
|
||||||
"comment" : "The text for a button that confirms and saves settings.",
|
"comment" : "The text for a button that confirms and saves settings.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -1582,6 +1914,47 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Haptic Feedback" : {
|
||||||
|
"comment" : "Toggle label for haptic feedback setting.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Haptic Feedback"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Retroalimentación Háptica"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Retroalimentación Háptica"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Retroalimentación Háptica"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Retour Haptique"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Retour Haptique"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Help" : {
|
"Help" : {
|
||||||
"comment" : "The label of a button that shows help information.",
|
"comment" : "The label of a button that shows help information.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -2890,6 +3263,211 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Show Cards Remaining" : {
|
||||||
|
"comment" : "Toggle label for showing cards remaining.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Show Cards Remaining"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mostrar Cartas Restantes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mostrar Cartas Restantes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mostrar Cartas Restantes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Afficher Cartes Restantes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Afficher Cartes Restantes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Show History" : {
|
||||||
|
"comment" : "Toggle label for showing game history.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Show History"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mostrar Historial"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mostrar Historial"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mostrar Historial"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Afficher l'Historique"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Afficher l'Historique"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SOUND & HAPTICS" : {
|
||||||
|
"comment" : "Section header for sound and haptic settings.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "SOUND & HAPTICS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "SONIDO Y HÁPTICOS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "SONIDO Y HÁPTICOS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "SONIDO Y HÁPTICOS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "SON ET HAPTIQUE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "SON ET HAPTIQUE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Sound Effects" : {
|
||||||
|
"comment" : "Toggle label for enabling sound effects.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Sound Effects"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Efectos de Sonido"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Efectos de Sonido"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Efectos de Sonido"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Effets Sonores"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Effets Sonores"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"STARTING BALANCE" : {
|
||||||
|
"comment" : "Section header for starting balance settings.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "STARTING BALANCE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "SALDO INICIAL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "SALDO INICIAL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "SALDO INICIAL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "SOLDE DE DÉPART"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "SOLDE DE DÉPART"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Statistics" : {
|
"Statistics" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -2930,6 +3508,47 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"TABLE LIMITS" : {
|
||||||
|
"comment" : "Section header for table limits settings.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "TABLE LIMITS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "LÍMITES DE MESA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "LÍMITES DE MESA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "LÍMITES DE MESA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "LIMITES DE TABLE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "LIMITES DE TABLE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"tableLimitsFormat" : {
|
"tableLimitsFormat" : {
|
||||||
"comment" : "Format string for table limits display. First argument is min bet, second is max bet.",
|
"comment" : "Format string for table limits display. First argument is min bet, second is max bet.",
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
@ -3225,6 +3844,47 @@
|
|||||||
"comment" : "A description of an alternative method for generating app icons.",
|
"comment" : "A description of an alternative method for generating app icons.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Vibration for actions and results" : {
|
||||||
|
"comment" : "Subtitle describing haptic feedback toggle.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Vibration for actions and results"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Vibración para acciones y resultados"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Vibración para acciones y resultados"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Vibración para acciones y resultados"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Vibration pour les actions et résultats"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Vibration pour les actions et résultats"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"View detailed game statistics" : {
|
"View detailed game statistics" : {
|
||||||
"comment" : "A hint that appears when hovering over the \"Statistics\" button, explaining its function.",
|
"comment" : "A hint that appears when hovering over the \"Statistics\" button, explaining its function.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -3266,6 +3926,47 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Volume" : {
|
||||||
|
"comment" : "Label for volume slider.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Volume"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Volumen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Volumen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Volumen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Volume"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Volume"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"WIN" : {
|
"WIN" : {
|
||||||
"comment" : "The text that appears as a badge when a player wins a hand in baccarat.",
|
"comment" : "The text that appears as a badge when a player wins a hand in baccarat.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
86
Baccarat/Resources/Sounds/README.md
Normal file
86
Baccarat/Resources/Sounds/README.md
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# Sound Effects for Baccarat
|
||||||
|
|
||||||
|
## Required Sound Files
|
||||||
|
|
||||||
|
Add the following `.mp3` files to this folder:
|
||||||
|
|
||||||
|
| Filename | Description | When Played |
|
||||||
|
|----------|-------------|-------------|
|
||||||
|
| `chip_place.mp3` | Chip clicking sound | When placing a bet |
|
||||||
|
| `chip_stack.mp3` | Multiple chips clinking | When stacking chips |
|
||||||
|
| `card_deal.mp3` | Card sliding/dealing sound | When each card is dealt |
|
||||||
|
| `card_flip.mp3` | Card flip/reveal sound | When cards are turned face up |
|
||||||
|
| `card_shuffle.mp3` | Deck shuffling sound | When deck is shuffled (optional) |
|
||||||
|
| `win.mp3` | Victory/success sound | When player wins |
|
||||||
|
| `lose.mp3` | Loss sound (subtle) | When player loses |
|
||||||
|
| `push.mp3` | Neutral/tie sound | When result is a tie/push |
|
||||||
|
| `big_win.mp3` | Big win celebration | For large payouts |
|
||||||
|
| `button_tap.mp3` | UI tap feedback | When buttons are tapped |
|
||||||
|
| `new_round.mp3` | Fresh start sound | When starting a new round |
|
||||||
|
| `clear_bets.mp3` | Chips sliding/removing | When clearing bets |
|
||||||
|
| `game_over.mp3` | Sad/failure sound | When player runs out of chips |
|
||||||
|
|
||||||
|
## Free Sound Resources
|
||||||
|
|
||||||
|
### Casino & Card Game Sounds
|
||||||
|
|
||||||
|
1. **Freesound.org** (Free, requires attribution)
|
||||||
|
- Search: "casino chips", "card dealing", "poker"
|
||||||
|
- High quality community-contributed sounds
|
||||||
|
- https://freesound.org
|
||||||
|
|
||||||
|
2. **Mixkit** (Free, no attribution required)
|
||||||
|
- Casino and game sound effects
|
||||||
|
- https://mixkit.co/free-sound-effects/casino/
|
||||||
|
|
||||||
|
3. **Zapsplat** (Free with account)
|
||||||
|
- Large library of casino sounds
|
||||||
|
- https://www.zapsplat.com
|
||||||
|
|
||||||
|
4. **Soundsnap** (Subscription/paid)
|
||||||
|
- Professional quality sounds
|
||||||
|
- https://www.soundsnap.com
|
||||||
|
|
||||||
|
5. **Epidemic Sound** (Subscription)
|
||||||
|
- High-quality licensed sounds
|
||||||
|
- https://www.epidemicsound.com
|
||||||
|
|
||||||
|
### Recommended Search Terms
|
||||||
|
|
||||||
|
- "casino chip" or "poker chip"
|
||||||
|
- "card deal" or "card slide"
|
||||||
|
- "card flip" or "card turn"
|
||||||
|
- "win jingle" or "success sound"
|
||||||
|
- "game over" or "fail sound"
|
||||||
|
- "casino ambience" (for background)
|
||||||
|
|
||||||
|
## Tips for Good Sound Selection
|
||||||
|
|
||||||
|
1. **Keep it subtle** - Sounds should enhance, not distract
|
||||||
|
2. **Short duration** - 0.2-1.5 seconds is ideal for most effects
|
||||||
|
3. **Consistent volume** - Normalize all sounds to similar levels
|
||||||
|
4. **High quality** - Use 44.1kHz sample rate minimum
|
||||||
|
5. **Mobile-friendly** - MP3 format, reasonable file sizes (<500KB each)
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
- The `SoundManager` class preloads all sounds at app launch
|
||||||
|
- Missing sound files are handled gracefully (no crash)
|
||||||
|
- Volume is controllable from Settings
|
||||||
|
- Sounds respect the user's sound/haptic preferences
|
||||||
|
|
||||||
|
## Adding to Xcode Project
|
||||||
|
|
||||||
|
1. Add `.mp3` files to this `Sounds` folder
|
||||||
|
2. In Xcode, ensure files are included in the target
|
||||||
|
3. Build and run - sounds should work automatically
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run the app and:
|
||||||
|
1. Place a bet → hear chip sound
|
||||||
|
2. Deal cards → hear deal/flip sounds
|
||||||
|
3. See result → hear win/lose/push sound
|
||||||
|
4. New round → hear new round sound
|
||||||
|
5. Clear bets → hear clear sound
|
||||||
|
|
||||||
BIN
Baccarat/Resources/Sounds/big_win.mp3
Normal file
BIN
Baccarat/Resources/Sounds/big_win.mp3
Normal file
Binary file not shown.
BIN
Baccarat/Resources/Sounds/button_tap.mp3
Normal file
BIN
Baccarat/Resources/Sounds/button_tap.mp3
Normal file
Binary file not shown.
BIN
Baccarat/Resources/Sounds/card_deal.mp3
Normal file
BIN
Baccarat/Resources/Sounds/card_deal.mp3
Normal file
Binary file not shown.
BIN
Baccarat/Resources/Sounds/card_flip.mp3
Normal file
BIN
Baccarat/Resources/Sounds/card_flip.mp3
Normal file
Binary file not shown.
BIN
Baccarat/Resources/Sounds/card_shuffle.mp3
Normal file
BIN
Baccarat/Resources/Sounds/card_shuffle.mp3
Normal file
Binary file not shown.
BIN
Baccarat/Resources/Sounds/chip_place.mp3
Normal file
BIN
Baccarat/Resources/Sounds/chip_place.mp3
Normal file
Binary file not shown.
BIN
Baccarat/Resources/Sounds/chip_stack.mp3
Normal file
BIN
Baccarat/Resources/Sounds/chip_stack.mp3
Normal file
Binary file not shown.
BIN
Baccarat/Resources/Sounds/clear_bets.mp3
Normal file
BIN
Baccarat/Resources/Sounds/clear_bets.mp3
Normal file
Binary file not shown.
BIN
Baccarat/Resources/Sounds/game_over.mp3
Normal file
BIN
Baccarat/Resources/Sounds/game_over.mp3
Normal file
Binary file not shown.
BIN
Baccarat/Resources/Sounds/lose.mp3
Normal file
BIN
Baccarat/Resources/Sounds/lose.mp3
Normal file
Binary file not shown.
BIN
Baccarat/Resources/Sounds/new_round.mp3
Normal file
BIN
Baccarat/Resources/Sounds/new_round.mp3
Normal file
Binary file not shown.
BIN
Baccarat/Resources/Sounds/push.mp3
Normal file
BIN
Baccarat/Resources/Sounds/push.mp3
Normal file
Binary file not shown.
BIN
Baccarat/Resources/Sounds/win.mp3
Normal file
BIN
Baccarat/Resources/Sounds/win.mp3
Normal file
Binary file not shown.
@ -118,7 +118,13 @@ struct GameTableView: View {
|
|||||||
betResults: state.betResults,
|
betResults: state.betResults,
|
||||||
playerHadPair: state.playerHadPair,
|
playerHadPair: state.playerHadPair,
|
||||||
bankerHadPair: state.bankerHadPair,
|
bankerHadPair: state.bankerHadPair,
|
||||||
onNewRound: { state.newRound() }
|
currentBalance: state.balance,
|
||||||
|
minBet: state.minBet,
|
||||||
|
onNewRound: { state.newRound() },
|
||||||
|
onGameOver: {
|
||||||
|
// Reset game (sound already played when banner appeared)
|
||||||
|
state.resetGame()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,15 @@ struct ResultBannerView: View {
|
|||||||
let betResults: [BetResult]
|
let betResults: [BetResult]
|
||||||
var playerHadPair: Bool = false
|
var playerHadPair: Bool = false
|
||||||
var bankerHadPair: Bool = false
|
var bankerHadPair: Bool = false
|
||||||
|
let currentBalance: Int
|
||||||
|
let minBet: Int
|
||||||
let onNewRound: () -> Void
|
let onNewRound: () -> Void
|
||||||
|
let onGameOver: () -> Void
|
||||||
|
|
||||||
|
/// Whether the player is out of money and can't continue.
|
||||||
|
private var isGameOver: Bool {
|
||||||
|
currentBalance < minBet
|
||||||
|
}
|
||||||
|
|
||||||
@State private var showBanner = false
|
@State private var showBanner = false
|
||||||
@State private var showText = false
|
@State private var showText = false
|
||||||
@ -113,30 +121,67 @@ struct ResultBannerView: View {
|
|||||||
.opacity(showTotal ? Design.Scale.normal : 0)
|
.opacity(showTotal ? Design.Scale.normal : 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New Round button
|
// Game Over message or New Round button
|
||||||
Button {
|
if isGameOver {
|
||||||
onNewRound()
|
// Game Over - show message and restart button
|
||||||
} label: {
|
VStack(spacing: Design.Spacing.medium) {
|
||||||
Text("New Round")
|
Text(String(localized: "You've run out of chips!"))
|
||||||
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
|
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
||||||
.foregroundStyle(.black)
|
.foregroundStyle(.red.opacity(Design.Opacity.heavy))
|
||||||
.padding(.horizontal, Design.Spacing.xxxLarge + Design.Spacing.small)
|
|
||||||
.padding(.vertical, Design.Spacing.medium + Design.Spacing.xxSmall)
|
Button {
|
||||||
.background(
|
onGameOver()
|
||||||
Capsule()
|
} label: {
|
||||||
.fill(
|
HStack(spacing: Design.Spacing.small) {
|
||||||
LinearGradient(
|
Image(systemName: "arrow.counterclockwise")
|
||||||
colors: [Color.Button.goldLight, Color.Button.goldDark],
|
Text(String(localized: "Play Again"))
|
||||||
startPoint: .top,
|
}
|
||||||
endPoint: .bottom
|
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
|
||||||
|
.foregroundStyle(.black)
|
||||||
|
.padding(.horizontal, Design.Spacing.xxxLarge + Design.Spacing.small)
|
||||||
|
.padding(.vertical, Design.Spacing.medium + Design.Spacing.xxSmall)
|
||||||
|
.background(
|
||||||
|
Capsule()
|
||||||
|
.fill(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [Color.Button.goldLight, Color.Button.goldDark],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
.shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium)
|
||||||
.shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium)
|
}
|
||||||
|
}
|
||||||
|
.scaleEffect(showButton ? Design.Scale.normal : Design.Scale.shrunk)
|
||||||
|
.opacity(showButton ? Design.Scale.normal : 0)
|
||||||
|
.padding(.top, Design.Spacing.small)
|
||||||
|
} else {
|
||||||
|
// Normal - New Round button
|
||||||
|
Button {
|
||||||
|
onNewRound()
|
||||||
|
} label: {
|
||||||
|
Text(String(localized: "New Round"))
|
||||||
|
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
|
||||||
|
.foregroundStyle(.black)
|
||||||
|
.padding(.horizontal, Design.Spacing.xxxLarge + Design.Spacing.small)
|
||||||
|
.padding(.vertical, Design.Spacing.medium + Design.Spacing.xxSmall)
|
||||||
|
.background(
|
||||||
|
Capsule()
|
||||||
|
.fill(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [Color.Button.goldLight, Color.Button.goldDark],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium)
|
||||||
|
}
|
||||||
|
.scaleEffect(showButton ? Design.Scale.normal : Design.Scale.shrunk)
|
||||||
|
.opacity(showButton ? Design.Scale.normal : 0)
|
||||||
|
.padding(.top, Design.Spacing.small)
|
||||||
}
|
}
|
||||||
.scaleEffect(showButton ? Design.Scale.normal : Design.Scale.shrunk)
|
|
||||||
.opacity(showButton ? Design.Scale.normal : 0)
|
|
||||||
.padding(.top, Design.Spacing.small)
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, Design.Spacing.xLarge)
|
.padding(.horizontal, Design.Spacing.xLarge)
|
||||||
.padding(.vertical, Design.Spacing.xxLarge)
|
.padding(.vertical, Design.Spacing.xxLarge)
|
||||||
@ -193,6 +238,14 @@ struct ResultBannerView: View {
|
|||||||
showButton = true
|
showButton = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Play game over sound if out of chips (after a short delay so it doesn't overlap with lose sound)
|
||||||
|
if isGameOver {
|
||||||
|
Task {
|
||||||
|
try? await Task.sleep(for: .seconds(1))
|
||||||
|
SoundManager.shared.play(.gameOver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Announce result to VoiceOver users
|
// Announce result to VoiceOver users
|
||||||
announceResult()
|
announceResult()
|
||||||
}
|
}
|
||||||
@ -388,7 +441,7 @@ struct ConfettiView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview("Win") {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.Table.preview
|
Color.Table.preview
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
@ -404,7 +457,31 @@ struct ConfettiView: View {
|
|||||||
],
|
],
|
||||||
playerHadPair: true,
|
playerHadPair: true,
|
||||||
bankerHadPair: false,
|
bankerHadPair: false,
|
||||||
onNewRound: {}
|
currentBalance: 5000,
|
||||||
|
minBet: 10,
|
||||||
|
onNewRound: {},
|
||||||
|
onGameOver: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Game Over") {
|
||||||
|
ZStack {
|
||||||
|
Color.Table.preview
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
ResultBannerView(
|
||||||
|
result: .bankerWins,
|
||||||
|
totalWinnings: -1000,
|
||||||
|
betResults: [
|
||||||
|
BetResult(type: .player, amount: 1000, payout: -1000)
|
||||||
|
],
|
||||||
|
playerHadPair: false,
|
||||||
|
bankerHadPair: false,
|
||||||
|
currentBalance: 0,
|
||||||
|
minBet: 10,
|
||||||
|
onNewRound: {},
|
||||||
|
onGameOver: {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ struct SettingsView: View {
|
|||||||
title: String(localized: "Settings"),
|
title: String(localized: "Settings"),
|
||||||
content: {
|
content: {
|
||||||
// Table Limits Section (First!)
|
// Table Limits Section (First!)
|
||||||
SheetSection(title: "TABLE LIMITS", icon: "banknote") {
|
SheetSection(title: String(localized: "TABLE LIMITS"), icon: "banknote") {
|
||||||
TableLimitsPicker(selection: $settings.tableLimits)
|
TableLimitsPicker(selection: $settings.tableLimits)
|
||||||
.onChange(of: settings.tableLimits) { _, _ in
|
.onChange(of: settings.tableLimits) { _, _ in
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
@ -29,7 +29,7 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Deck Settings Section
|
// Deck Settings Section
|
||||||
SheetSection(title: "DECK SETTINGS", icon: "rectangle.portrait.on.rectangle.portrait") {
|
SheetSection(title: String(localized: "DECK SETTINGS"), icon: "rectangle.portrait.on.rectangle.portrait") {
|
||||||
DeckCountPicker(selection: $settings.deckCount)
|
DeckCountPicker(selection: $settings.deckCount)
|
||||||
.onChange(of: settings.deckCount) { _, _ in
|
.onChange(of: settings.deckCount) { _, _ in
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
@ -37,7 +37,7 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Starting Balance Section
|
// Starting Balance Section
|
||||||
SheetSection(title: "STARTING BALANCE", icon: "dollarsign.circle") {
|
SheetSection(title: String(localized: "STARTING BALANCE"), icon: "dollarsign.circle") {
|
||||||
BalancePicker(balance: $settings.startingBalance)
|
BalancePicker(balance: $settings.startingBalance)
|
||||||
.onChange(of: settings.startingBalance) { _, _ in
|
.onChange(of: settings.startingBalance) { _, _ in
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
@ -45,10 +45,10 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Display Settings Section
|
// Display Settings Section
|
||||||
SheetSection(title: "DISPLAY", icon: "eye") {
|
SheetSection(title: String(localized: "DISPLAY"), icon: "eye") {
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Show Cards Remaining",
|
title: String(localized: "Show Cards Remaining"),
|
||||||
subtitle: "Display deck counter at top",
|
subtitle: String(localized: "Display deck counter at top"),
|
||||||
isOn: $settings.showCardsRemaining
|
isOn: $settings.showCardsRemaining
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -56,17 +56,17 @@ struct SettingsView: View {
|
|||||||
.background(Color.white.opacity(Design.Opacity.subtle))
|
.background(Color.white.opacity(Design.Opacity.subtle))
|
||||||
|
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Show History",
|
title: String(localized: "Show History"),
|
||||||
subtitle: "Display result road map",
|
subtitle: String(localized: "Display result road map"),
|
||||||
isOn: $settings.showHistory
|
isOn: $settings.showHistory
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Animation Settings Section
|
// Animation Settings Section
|
||||||
SheetSection(title: "ANIMATIONS", icon: "sparkles") {
|
SheetSection(title: String(localized: "ANIMATIONS"), icon: "sparkles") {
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Card Animations",
|
title: String(localized: "Card Animations"),
|
||||||
subtitle: "Animate dealing and flipping",
|
subtitle: String(localized: "Animate dealing and flipping"),
|
||||||
isOn: $settings.showAnimations
|
isOn: $settings.showAnimations
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -78,6 +78,42 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sound & Haptics Section
|
||||||
|
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
|
||||||
|
)
|
||||||
|
.onChange(of: settings.soundEnabled) { _, newValue in
|
||||||
|
SoundManager.shared.soundEnabled = newValue
|
||||||
|
hasChanges = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.soundEnabled {
|
||||||
|
Divider()
|
||||||
|
.background(Color.white.opacity(Design.Opacity.subtle))
|
||||||
|
|
||||||
|
VolumePicker(volume: $settings.soundVolume)
|
||||||
|
.onChange(of: settings.soundVolume) { _, newValue in
|
||||||
|
SoundManager.shared.volume = newValue
|
||||||
|
hasChanges = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.background(Color.white.opacity(Design.Opacity.subtle))
|
||||||
|
|
||||||
|
SettingsToggle(
|
||||||
|
title: String(localized: "Haptic Feedback"),
|
||||||
|
subtitle: String(localized: "Vibration for actions and results"),
|
||||||
|
isOn: $settings.hapticsEnabled
|
||||||
|
)
|
||||||
|
.onChange(of: settings.hapticsEnabled) { _, _ in
|
||||||
|
hasChanges = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reset Button
|
// Reset Button
|
||||||
Button {
|
Button {
|
||||||
settings.resetToDefaults()
|
settings.resetToDefaults()
|
||||||
@ -85,7 +121,7 @@ struct SettingsView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "arrow.counterclockwise")
|
Image(systemName: "arrow.counterclockwise")
|
||||||
Text("Reset to Defaults")
|
Text(String(localized: "Reset to Defaults"))
|
||||||
}
|
}
|
||||||
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
||||||
.foregroundStyle(.red.opacity(Design.Opacity.heavy))
|
.foregroundStyle(.red.opacity(Design.Opacity.heavy))
|
||||||
@ -234,7 +270,7 @@ struct SpeedPicker: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
Text("Dealing Speed")
|
Text(String(localized: "Dealing Speed"))
|
||||||
.font(.system(size: Design.BaseFontSize.subheadline, weight: .medium))
|
.font(.system(size: Design.BaseFontSize.subheadline, weight: .medium))
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
@ -323,6 +359,40 @@ 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
SettingsView(settings: GameSettings()) { }
|
SettingsView(settings: GameSettings()) { }
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user