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

This commit is contained in:
Matt Bruce 2025-12-17 09:51:36 -06:00
parent deedc08b85
commit 7803a561ae
18 changed files with 179 additions and 101 deletions

View File

@ -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..<playerCardsFaceUp.count {
@ -344,15 +340,11 @@ final class GameState {
if let playerThird = engine.drawPlayerThirdCard() {
if settings.showAnimations {
try? await Task.sleep(for: dealDelay)
if settings.soundEnabled {
sound.play(.cardDeal)
}
sound.playCardDeal()
visiblePlayerCards.append(playerThird)
playerCardsFaceUp.append(false)
try? await Task.sleep(for: shortDelay)
if settings.soundEnabled {
sound.play(.cardFlip)
}
sound.playCardFlip()
playerCardsFaceUp[2] = true
try? await Task.sleep(for: flipDelay)
} else {
@ -366,15 +358,11 @@ final class GameState {
if let bankerThird = engine.drawBankerThirdCard() {
if settings.showAnimations {
try? await Task.sleep(for: dealDelay)
if settings.soundEnabled {
sound.play(.cardDeal)
}
sound.playCardDeal()
visibleBankerCards.append(bankerThird)
bankerCardsFaceUp.append(false)
try? await Task.sleep(for: shortDelay)
if settings.soundEnabled {
sound.play(.cardFlip)
}
sound.playCardFlip()
bankerCardsFaceUp[2] = true
try? await Task.sleep(for: dealDelay)
} else {
@ -427,27 +415,12 @@ final class GameState {
// 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()
}
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).

View File

@ -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
```

View File

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

View File

@ -33,3 +33,7 @@
// - CasinoDesign (constants)
// - Color.Sheet (sheet colors)
// MARK: - Audio
// - SoundManager
// - GameSound

View File

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