Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
deedc08b85
commit
7803a561ae
@ -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).
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,3 +33,7 @@
|
||||
// - CasinoDesign (constants)
|
||||
// - Color.Sheet (sheet colors)
|
||||
|
||||
// MARK: - Audio
|
||||
// - SoundManager
|
||||
// - GameSound
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user