diff --git a/Baccarat/Engine/GameState.swift b/Baccarat/Engine/GameState.swift index 4971e1f..e55997d 100644 --- a/Baccarat/Engine/GameState.swift +++ b/Baccarat/Engine/GameState.swift @@ -140,6 +140,16 @@ final class GameState { self.settings = settings self.engine = BaccaratEngine(deckCount: settings.deckCount.rawValue) self.balance = settings.startingBalance + + // Sync sound settings with SoundManager + syncSoundSettings() + } + + /// Syncs sound settings from GameSettings to SoundManager. + private func syncSoundSettings() { + sound.soundEnabled = settings.soundEnabled + sound.hapticsEnabled = settings.hapticsEnabled + sound.volume = settings.soundVolume } // MARK: - Computed Properties for Bets @@ -228,13 +238,8 @@ final class GameState { balance -= amount - // Play chip placement sound - if settings.soundEnabled { - sound.play(.chipPlace) - } - if settings.hapticsEnabled { - sound.hapticLight() - } + // Play chip placement sound and haptic + sound.playChipPlace() } /// Clears all current bets and returns the amounts to balance. @@ -243,13 +248,8 @@ final class GameState { balance += totalBetAmount currentBets = [] - // Play clear bets sound - if settings.soundEnabled { - sound.play(.clearBets) - } - if settings.hapticsEnabled { - sound.hapticMedium() - } + // Play clear bets sound and haptic + sound.playClearBets() } /// Undoes the last bet placed. @@ -290,9 +290,7 @@ final class GameState { try? await Task.sleep(for: dealDelay) // Play card deal sound - if settings.soundEnabled { - sound.play(.cardDeal) - } + sound.playCardDeal() if index % 2 == 0 { visiblePlayerCards.append(card) @@ -307,9 +305,7 @@ final class GameState { try? await Task.sleep(for: flipDelay) // Play card flip sound - if settings.soundEnabled { - sound.play(.cardFlip) - } + sound.playCardFlip() // Flip all cards face up for i in 0..= 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() - } + sound.playWin(isBigWin: isBigWin) } else if totalWinnings < 0 { - if settings.soundEnabled { - sound.play(.lose) - } - if settings.hapticsEnabled { - sound.hapticError() - } + sound.playLose() } else { // Push (tie with main bet push) - if settings.soundEnabled { - sound.play(.push) - } - if settings.hapticsEnabled { - sound.hapticMedium() - } + sound.playPush() } // Record result in history @@ -470,9 +443,7 @@ final class GameState { guard currentPhase == .roundComplete else { return } // Play new round sound - if settings.soundEnabled { - sound.play(.newRound) - } + sound.playNewRound() // Dismiss result banner showResultBanner = false @@ -519,12 +490,7 @@ final class GameState { betResults = [] // Play new game sound - if settings.soundEnabled { - sound.play(.newRound) - } - if settings.hapticsEnabled { - sound.hapticMedium() - } + sound.playNewRound() } /// Applies new settings (call after settings change). diff --git a/CasinoKit/README.md b/CasinoKit/README.md index b921c26..bc1deb6 100644 --- a/CasinoKit/README.md +++ b/CasinoKit/README.md @@ -148,6 +148,66 @@ let image = IconRenderer.renderAppIcon(config: .baccarat, size: 1024) let allImages = IconRenderer.renderAllSizes(config: .baccarat) ``` +### 🔊 Audio & Haptics + +**SoundManager** - Manages game sounds and haptic feedback. + +```swift +// Access shared instance +let sound = SoundManager.shared + +// Configure settings +sound.soundEnabled = true +sound.hapticsEnabled = true +sound.volume = 1.0 + +// Play sounds +sound.play(.chipPlace) +sound.play(.cardDeal) +sound.play(.win) + +// Convenience methods (include haptics) +sound.playChipPlace() // Chip sound + light haptic +sound.playCardFlip() // Card sound + light haptic +sound.playWin() // Win sound + success haptic +sound.playLose() // Lose sound + error haptic +sound.playGameOver() // Game over sound + error haptic +``` + +**Available Sounds:** +| Sound | Description | +|-------|-------------| +| `.chipPlace` | Placing a bet | +| `.chipStack` | Stacking chips | +| `.cardDeal` | Dealing a card | +| `.cardFlip` | Flipping a card | +| `.cardShuffle` | Shuffling deck | +| `.win` | Player wins | +| `.lose` | Player loses | +| `.push` | Tie/push | +| `.bigWin` | Large payout | +| `.buttonTap` | UI tap | +| `.newRound` | Starting new round | +| `.clearBets` | Clearing bets | +| `.gameOver` | Out of chips | + +**Custom Sound Files:** + +The manager uses iOS system sounds as fallback. To use custom sounds: +1. Add `.mp3` files named: `chip_place.mp3`, `card_deal.mp3`, `win.mp3`, etc. +2. Add them to your app bundle's Resources folder +3. Files are automatically detected and used + +**Haptic Methods:** +```swift +sound.hapticLight() // Light tap +sound.hapticMedium() // Medium tap +sound.hapticHeavy() // Heavy tap +sound.hapticSuccess() // Success notification +sound.hapticError() // Error notification +sound.hapticWarning() // Warning notification +``` + ### 🎨 Design System **CasinoDesign** - Shared design constants. @@ -306,11 +366,15 @@ CasinoKit/ │ │ ├── AppIconView.swift │ │ ├── LaunchScreenView.swift │ │ └── IconRenderer.swift +│ ├── Audio/ +│ │ └── SoundManager.swift │ ├── Theme/ │ │ ├── CasinoTheme.swift │ │ └── CasinoDesign.swift │ └── Resources/ -│ └── Localizable.xcstrings +│ ├── Localizable.xcstrings +│ └── Sounds/ +│ └── README.md (+ .mp3 files) └── Tests/CasinoKitTests/ └── CasinoKitTests.swift ``` diff --git a/Baccarat/Audio/SoundManager.swift b/CasinoKit/Sources/CasinoKit/Audio/SoundManager.swift similarity index 68% rename from Baccarat/Audio/SoundManager.swift rename to CasinoKit/Sources/CasinoKit/Audio/SoundManager.swift index 2e01efd..a65f19a 100644 --- a/Baccarat/Audio/SoundManager.swift +++ b/CasinoKit/Sources/CasinoKit/Audio/SoundManager.swift @@ -1,16 +1,16 @@ // // SoundManager.swift -// Baccarat +// CasinoKit // -// Manages game sound effects. +// Manages game sound effects for casino games. // import AVFoundation import AudioToolbox import SwiftUI -/// Types of sound effects used in the game. -enum GameSound: String, CaseIterable { +/// Types of sound effects used in casino games. +public enum GameSound: String, CaseIterable, Sendable { case chipPlace = "chip_place" // When placing a bet case chipStack = "chip_stack" // Stacking multiple chips case cardDeal = "card_deal" // Dealing a card @@ -26,11 +26,11 @@ enum GameSound: String, CaseIterable { case gameOver = "game_over" // Out of chips / game over /// File extension for the sound file. - var fileExtension: String { "mp3" } + public 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 { + public var fallbackSystemSound: SystemSoundID { switch self { case .chipPlace: return 1104 // Key press click case .chipStack: return 1105 // Keyboard key @@ -49,7 +49,7 @@ enum GameSound: String, CaseIterable { } /// Display name for settings. - var displayName: String { + public var displayName: String { switch self { case .chipPlace: return "Chip Place" case .chipStack: return "Chip Stack" @@ -68,34 +68,48 @@ enum GameSound: String, CaseIterable { } } -/// Manages playing game sound effects. +/// Manages playing game sound effects for casino games. +/// +/// Use the shared instance and configure it with your app's settings: +/// ```swift +/// SoundManager.shared.soundEnabled = settings.soundEnabled +/// SoundManager.shared.volume = settings.soundVolume +/// ``` @MainActor @Observable -final class SoundManager { +public final class SoundManager { // MARK: - Singleton - static let shared = SoundManager() + public static let shared = SoundManager() // MARK: - Properties /// Whether sound effects are enabled. - var soundEnabled: Bool = true { + public var soundEnabled: Bool = true { didSet { - UserDefaults.standard.set(soundEnabled, forKey: "soundEnabled") + UserDefaults.standard.set(soundEnabled, forKey: "casinokit.soundEnabled") + } + } + + /// Whether haptic feedback is enabled. + public var hapticsEnabled: Bool = true { + didSet { + UserDefaults.standard.set(hapticsEnabled, forKey: "casinokit.hapticsEnabled") } } /// Master volume (0.0 to 1.0). - var volume: Float = 1.0 { + public var volume: Float = 1.0 { didSet { - UserDefaults.standard.set(volume, forKey: "soundVolume") + UserDefaults.standard.set(volume, forKey: "casinokit.soundVolume") updatePlayerVolumes() } } - /// Whether to use system sounds as fallback (true until custom sounds are added). - private var useSystemSoundsFallback: Bool = true + /// 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 /// Cache of audio players for quick playback. private var audioPlayers: [GameSound: AVAudioPlayer] = [:] @@ -104,8 +118,9 @@ final class SoundManager { private init() { // Load saved preferences - soundEnabled = UserDefaults.standard.object(forKey: "soundEnabled") as? Bool ?? true - volume = UserDefaults.standard.object(forKey: "soundVolume") as? Float ?? 1.0 + soundEnabled = UserDefaults.standard.object(forKey: "casinokit.soundEnabled") as? Bool ?? true + hapticsEnabled = UserDefaults.standard.object(forKey: "casinokit.hapticsEnabled") as? Bool ?? true + volume = UserDefaults.standard.object(forKey: "casinokit.soundVolume") as? Float ?? 1.0 // Configure audio session configureAudioSession() @@ -121,29 +136,26 @@ final class SoundManager { try AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default) try AVAudioSession.sharedInstance().setActive(true) } catch { - print("Failed to configure audio session: \(error)") + print("CasinoKit: Failed to configure audio session: \(error)") } } // MARK: - Preloading /// Preloads all sound files for faster playback. - private func preloadSounds() { - var foundCustomSound = false + /// Call this after setting `soundBundle` if you're using a custom bundle. + public func preloadSounds() { + audioPlayers.removeAll() 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( + guard let url = soundBundle.url( forResource: sound.rawValue, withExtension: sound.fileExtension ) else { @@ -157,7 +169,7 @@ final class SoundManager { player.volume = volume return player } catch { - print("Failed to create audio player for \(sound.rawValue): \(error)") + print("CasinoKit: Failed to create audio player for \(sound.rawValue): \(error)") return nil } } @@ -166,7 +178,7 @@ final class SoundManager { /// Plays a sound effect. /// - Parameter sound: The sound to play. - func play(_ sound: GameSound) { + public func play(_ sound: GameSound) { guard soundEnabled else { return } // Try custom sound first @@ -190,7 +202,7 @@ final class SoundManager { /// - Parameters: /// - sound: The sound to play. /// - delay: Delay in seconds before playing. - func play(_ sound: GameSound, delay: TimeInterval) { + public func play(_ sound: GameSound, delay: TimeInterval) { Task { try? await Task.sleep(for: .seconds(delay)) play(sound) @@ -201,7 +213,7 @@ final class SoundManager { /// - Parameters: /// - sounds: Array of sounds to play. /// - interval: Time between each sound. - func playSequence(_ sounds: [GameSound], interval: TimeInterval = 0.1) { + public func playSequence(_ sounds: [GameSound], interval: TimeInterval = 0.1) { for (index, sound) in sounds.enumerated() { play(sound, delay: TimeInterval(index) * interval) } @@ -218,33 +230,51 @@ final class SoundManager { // MARK: - Haptics /// Plays a light haptic feedback. - func hapticLight() { + public func hapticLight() { + guard hapticsEnabled else { return } let impact = UIImpactFeedbackGenerator(style: .light) impact.impactOccurred() } /// Plays a medium haptic feedback. - func hapticMedium() { + public func hapticMedium() { + guard hapticsEnabled else { return } let impact = UIImpactFeedbackGenerator(style: .medium) impact.impactOccurred() } + /// Plays a heavy haptic feedback. + public func hapticHeavy() { + guard hapticsEnabled else { return } + let impact = UIImpactFeedbackGenerator(style: .heavy) + impact.impactOccurred() + } + /// Plays a success haptic notification. - func hapticSuccess() { + public func hapticSuccess() { + guard hapticsEnabled else { return } let notification = UINotificationFeedbackGenerator() notification.notificationOccurred(.success) } /// Plays an error haptic notification. - func hapticError() { + public func hapticError() { + guard hapticsEnabled else { return } let notification = UINotificationFeedbackGenerator() notification.notificationOccurred(.error) } + + /// Plays a warning haptic notification. + public func hapticWarning() { + guard hapticsEnabled else { return } + let notification = UINotificationFeedbackGenerator() + notification.notificationOccurred(.warning) + } } // MARK: - Convenience Extensions -extension SoundManager { +public extension SoundManager { /// Plays chip placement sound with haptic. func playChipPlace() { @@ -257,7 +287,7 @@ extension SoundManager { play(.cardDeal) } - /// Plays card flip sound. + /// Plays card flip sound with haptic. func playCardFlip() { play(.cardFlip) hapticLight() @@ -275,7 +305,7 @@ extension SoundManager { hapticError() } - /// Plays push/tie sound. + /// Plays push/tie sound with haptic. func playPush() { play(.push) hapticMedium() @@ -286,4 +316,17 @@ extension SoundManager { play(.gameOver) hapticError() } + + /// Plays new round sound with haptic. + func playNewRound() { + play(.newRound) + hapticMedium() + } + + /// Plays clear bets sound with haptic. + func playClearBets() { + play(.clearBets) + hapticMedium() + } } + diff --git a/CasinoKit/Sources/CasinoKit/Exports.swift b/CasinoKit/Sources/CasinoKit/Exports.swift index f8f4b16..25b2beb 100644 --- a/CasinoKit/Sources/CasinoKit/Exports.swift +++ b/CasinoKit/Sources/CasinoKit/Exports.swift @@ -33,3 +33,7 @@ // - CasinoDesign (constants) // - Color.Sheet (sheet colors) +// MARK: - Audio +// - SoundManager +// - GameSound + diff --git a/Baccarat/Resources/Sounds/README.md b/CasinoKit/Sources/CasinoKit/Resources/Sounds/README.md similarity index 90% rename from Baccarat/Resources/Sounds/README.md rename to CasinoKit/Sources/CasinoKit/Resources/Sounds/README.md index 119bb89..a0ddccc 100644 --- a/Baccarat/Resources/Sounds/README.md +++ b/CasinoKit/Sources/CasinoKit/Resources/Sounds/README.md @@ -1,4 +1,4 @@ -# Sound Effects for Baccarat +# Sound Effects for CasinoKit ## Required Sound Files @@ -69,11 +69,12 @@ Add the following `.mp3` files to this folder: - Volume is controllable from Settings - Sounds respect the user's sound/haptic preferences -## Adding to Xcode Project +## Adding Sound Files -1. Add `.mp3` files to this `Sounds` folder -2. In Xcode, ensure files are included in the target +1. Add `.mp3` files to this `Sounds` folder in CasinoKit +2. The Package.swift already includes Resources, so files are automatically bundled 3. Build and run - sounds should work automatically +4. If sounds are in your app bundle instead, set `SoundManager.shared.soundBundle = .main` ## Testing diff --git a/Baccarat/Resources/Sounds/big_win.mp3 b/CasinoKit/Sources/CasinoKit/Resources/Sounds/big_win.mp3 similarity index 100% rename from Baccarat/Resources/Sounds/big_win.mp3 rename to CasinoKit/Sources/CasinoKit/Resources/Sounds/big_win.mp3 diff --git a/Baccarat/Resources/Sounds/button_tap.mp3 b/CasinoKit/Sources/CasinoKit/Resources/Sounds/button_tap.mp3 similarity index 100% rename from Baccarat/Resources/Sounds/button_tap.mp3 rename to CasinoKit/Sources/CasinoKit/Resources/Sounds/button_tap.mp3 diff --git a/Baccarat/Resources/Sounds/card_deal.mp3 b/CasinoKit/Sources/CasinoKit/Resources/Sounds/card_deal.mp3 similarity index 100% rename from Baccarat/Resources/Sounds/card_deal.mp3 rename to CasinoKit/Sources/CasinoKit/Resources/Sounds/card_deal.mp3 diff --git a/Baccarat/Resources/Sounds/card_flip.mp3 b/CasinoKit/Sources/CasinoKit/Resources/Sounds/card_flip.mp3 similarity index 100% rename from Baccarat/Resources/Sounds/card_flip.mp3 rename to CasinoKit/Sources/CasinoKit/Resources/Sounds/card_flip.mp3 diff --git a/Baccarat/Resources/Sounds/card_shuffle.mp3 b/CasinoKit/Sources/CasinoKit/Resources/Sounds/card_shuffle.mp3 similarity index 100% rename from Baccarat/Resources/Sounds/card_shuffle.mp3 rename to CasinoKit/Sources/CasinoKit/Resources/Sounds/card_shuffle.mp3 diff --git a/Baccarat/Resources/Sounds/chip_place.mp3 b/CasinoKit/Sources/CasinoKit/Resources/Sounds/chip_place.mp3 similarity index 100% rename from Baccarat/Resources/Sounds/chip_place.mp3 rename to CasinoKit/Sources/CasinoKit/Resources/Sounds/chip_place.mp3 diff --git a/Baccarat/Resources/Sounds/chip_stack.mp3 b/CasinoKit/Sources/CasinoKit/Resources/Sounds/chip_stack.mp3 similarity index 100% rename from Baccarat/Resources/Sounds/chip_stack.mp3 rename to CasinoKit/Sources/CasinoKit/Resources/Sounds/chip_stack.mp3 diff --git a/Baccarat/Resources/Sounds/clear_bets.mp3 b/CasinoKit/Sources/CasinoKit/Resources/Sounds/clear_bets.mp3 similarity index 100% rename from Baccarat/Resources/Sounds/clear_bets.mp3 rename to CasinoKit/Sources/CasinoKit/Resources/Sounds/clear_bets.mp3 diff --git a/Baccarat/Resources/Sounds/game_over.mp3 b/CasinoKit/Sources/CasinoKit/Resources/Sounds/game_over.mp3 similarity index 100% rename from Baccarat/Resources/Sounds/game_over.mp3 rename to CasinoKit/Sources/CasinoKit/Resources/Sounds/game_over.mp3 diff --git a/Baccarat/Resources/Sounds/lose.mp3 b/CasinoKit/Sources/CasinoKit/Resources/Sounds/lose.mp3 similarity index 100% rename from Baccarat/Resources/Sounds/lose.mp3 rename to CasinoKit/Sources/CasinoKit/Resources/Sounds/lose.mp3 diff --git a/Baccarat/Resources/Sounds/new_round.mp3 b/CasinoKit/Sources/CasinoKit/Resources/Sounds/new_round.mp3 similarity index 100% rename from Baccarat/Resources/Sounds/new_round.mp3 rename to CasinoKit/Sources/CasinoKit/Resources/Sounds/new_round.mp3 diff --git a/Baccarat/Resources/Sounds/push.mp3 b/CasinoKit/Sources/CasinoKit/Resources/Sounds/push.mp3 similarity index 100% rename from Baccarat/Resources/Sounds/push.mp3 rename to CasinoKit/Sources/CasinoKit/Resources/Sounds/push.mp3 diff --git a/Baccarat/Resources/Sounds/win.mp3 b/CasinoKit/Sources/CasinoKit/Resources/Sounds/win.mp3 similarity index 100% rename from Baccarat/Resources/Sounds/win.mp3 rename to CasinoKit/Sources/CasinoKit/Resources/Sounds/win.mp3