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
|
||||
let settings: GameSettings
|
||||
|
||||
// MARK: - Sound
|
||||
private let sound = SoundManager.shared
|
||||
|
||||
// MARK: - Game Engine
|
||||
private(set) var engine: BaccaratEngine
|
||||
|
||||
@ -224,13 +227,29 @@ final class GameState {
|
||||
}
|
||||
|
||||
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.
|
||||
func clearBets() {
|
||||
guard canPlaceBet else { return }
|
||||
guard canPlaceBet, !currentBets.isEmpty else { return }
|
||||
balance += totalBetAmount
|
||||
currentBets = []
|
||||
|
||||
// Play clear bets sound
|
||||
if settings.soundEnabled {
|
||||
sound.play(.clearBets)
|
||||
}
|
||||
if settings.hapticsEnabled {
|
||||
sound.hapticMedium()
|
||||
}
|
||||
}
|
||||
|
||||
/// Undoes the last bet placed.
|
||||
@ -270,6 +289,11 @@ final class GameState {
|
||||
for (index, card) in initialCards.enumerated() {
|
||||
try? await Task.sleep(for: dealDelay)
|
||||
|
||||
// Play card deal sound
|
||||
if settings.soundEnabled {
|
||||
sound.play(.cardDeal)
|
||||
}
|
||||
|
||||
if index % 2 == 0 {
|
||||
visiblePlayerCards.append(card)
|
||||
playerCardsFaceUp.append(false)
|
||||
@ -282,6 +306,11 @@ final class GameState {
|
||||
// Brief pause then flip cards
|
||||
try? await Task.sleep(for: flipDelay)
|
||||
|
||||
// Play card flip sound
|
||||
if settings.soundEnabled {
|
||||
sound.play(.cardFlip)
|
||||
}
|
||||
|
||||
// Flip all cards face up
|
||||
for i in 0..<playerCardsFaceUp.count {
|
||||
playerCardsFaceUp[i] = true
|
||||
@ -315,9 +344,15 @@ final class GameState {
|
||||
if let playerThird = engine.drawPlayerThirdCard() {
|
||||
if settings.showAnimations {
|
||||
try? await Task.sleep(for: dealDelay)
|
||||
if settings.soundEnabled {
|
||||
sound.play(.cardDeal)
|
||||
}
|
||||
visiblePlayerCards.append(playerThird)
|
||||
playerCardsFaceUp.append(false)
|
||||
try? await Task.sleep(for: shortDelay)
|
||||
if settings.soundEnabled {
|
||||
sound.play(.cardFlip)
|
||||
}
|
||||
playerCardsFaceUp[2] = true
|
||||
try? await Task.sleep(for: flipDelay)
|
||||
} else {
|
||||
@ -331,9 +366,15 @@ final class GameState {
|
||||
if let bankerThird = engine.drawBankerThirdCard() {
|
||||
if settings.showAnimations {
|
||||
try? await Task.sleep(for: dealDelay)
|
||||
if settings.soundEnabled {
|
||||
sound.play(.cardDeal)
|
||||
}
|
||||
visibleBankerCards.append(bankerThird)
|
||||
bankerCardsFaceUp.append(false)
|
||||
try? await Task.sleep(for: shortDelay)
|
||||
if settings.soundEnabled {
|
||||
sound.play(.cardFlip)
|
||||
}
|
||||
bankerCardsFaceUp[2] = true
|
||||
try? await Task.sleep(for: dealDelay)
|
||||
} else {
|
||||
@ -381,6 +422,34 @@ final class GameState {
|
||||
betResults = results
|
||||
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
|
||||
roundHistory.append(RoundResult(
|
||||
result: result,
|
||||
@ -400,6 +469,11 @@ final class GameState {
|
||||
func newRound() {
|
||||
guard currentPhase == .roundComplete else { return }
|
||||
|
||||
// Play new round sound
|
||||
if settings.soundEnabled {
|
||||
sound.play(.newRound)
|
||||
}
|
||||
|
||||
// Dismiss result banner
|
||||
showResultBanner = false
|
||||
|
||||
@ -443,6 +517,14 @@ final class GameState {
|
||||
playerHadPair = false
|
||||
bankerHadPair = false
|
||||
betResults = []
|
||||
|
||||
// Play new game sound
|
||||
if settings.soundEnabled {
|
||||
sound.play(.newRound)
|
||||
}
|
||||
if settings.hapticsEnabled {
|
||||
sound.hapticMedium()
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies new settings (call after settings change).
|
||||
|
||||
@ -133,6 +133,17 @@ final class GameSettings {
|
||||
/// Whether to show the history road map.
|
||||
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
|
||||
|
||||
private enum Keys {
|
||||
@ -143,6 +154,9 @@ final class GameSettings {
|
||||
static let dealingSpeed = "settings.dealingSpeed"
|
||||
static let showCardsRemaining = "settings.showCardsRemaining"
|
||||
static let showHistory = "settings.showHistory"
|
||||
static let soundEnabled = "settings.soundEnabled"
|
||||
static let hapticsEnabled = "settings.hapticsEnabled"
|
||||
static let soundVolume = "settings.soundVolume"
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
@ -186,6 +200,18 @@ final class GameSettings {
|
||||
if defaults.object(forKey: Keys.showHistory) != nil {
|
||||
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.
|
||||
@ -198,6 +224,9 @@ final class GameSettings {
|
||||
defaults.set(dealingSpeed, forKey: Keys.dealingSpeed)
|
||||
defaults.set(showCardsRemaining, forKey: Keys.showCardsRemaining)
|
||||
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.
|
||||
@ -209,6 +238,9 @@ final class GameSettings {
|
||||
dealingSpeed = 1.0
|
||||
showCardsRemaining = true
|
||||
showHistory = true
|
||||
soundEnabled = true
|
||||
hapticsEnabled = true
|
||||
soundVolume = 1.0
|
||||
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.",
|
||||
"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" : {
|
||||
"comment" : "A text label displaying the size of the app icon. The argument is the size of the icon in pixels.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -412,6 +416,88 @@
|
||||
"comment" : "A section header that suggests using an online tool to generate app icon sizes.",
|
||||
"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" : {
|
||||
"comment" : "A label displayed above the preview of the app icon.",
|
||||
"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" : {
|
||||
"comment" : "A heading that explains the values of playing cards.",
|
||||
"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" : {
|
||||
"comment" : "The label of a button that clears all current bets in the game.",
|
||||
"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" : {
|
||||
"comment" : "The text for a button that confirms and saves settings.",
|
||||
"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" : {
|
||||
"comment" : "The label of a button that shows help information.",
|
||||
"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" : {
|
||||
"localizations" : {
|
||||
"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" : {
|
||||
"comment" : "Format string for table limits display. First argument is min bet, second is max bet.",
|
||||
"extractionState" : "stale",
|
||||
@ -3225,6 +3844,47 @@
|
||||
"comment" : "A description of an alternative method for generating app icons.",
|
||||
"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" : {
|
||||
"comment" : "A hint that appears when hovering over the \"Statistics\" button, explaining its function.",
|
||||
"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" : {
|
||||
"comment" : "The text that appears as a badge when a player wins a hand in baccarat.",
|
||||
"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,
|
||||
playerHadPair: state.playerHadPair,
|
||||
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)
|
||||
|
||||
|
||||
@ -15,7 +15,15 @@ struct ResultBannerView: View {
|
||||
let betResults: [BetResult]
|
||||
var playerHadPair: Bool = false
|
||||
var bankerHadPair: Bool = false
|
||||
let currentBalance: Int
|
||||
let minBet: Int
|
||||
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 showText = false
|
||||
@ -113,11 +121,47 @@ struct ResultBannerView: View {
|
||||
.opacity(showTotal ? Design.Scale.normal : 0)
|
||||
}
|
||||
|
||||
// New Round button
|
||||
// Game Over message or New Round button
|
||||
if isGameOver {
|
||||
// Game Over - show message and restart button
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
Text(String(localized: "You've run out of chips!"))
|
||||
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
||||
.foregroundStyle(.red.opacity(Design.Opacity.heavy))
|
||||
|
||||
Button {
|
||||
onGameOver()
|
||||
} label: {
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
Image(systemName: "arrow.counterclockwise")
|
||||
Text(String(localized: "Play Again"))
|
||||
}
|
||||
.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)
|
||||
} else {
|
||||
// Normal - New Round button
|
||||
Button {
|
||||
onNewRound()
|
||||
} label: {
|
||||
Text("New Round")
|
||||
Text(String(localized: "New Round"))
|
||||
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
|
||||
.foregroundStyle(.black)
|
||||
.padding(.horizontal, Design.Spacing.xxxLarge + Design.Spacing.small)
|
||||
@ -138,6 +182,7 @@ struct ResultBannerView: View {
|
||||
.opacity(showButton ? Design.Scale.normal : 0)
|
||||
.padding(.top, Design.Spacing.small)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, Design.Spacing.xLarge)
|
||||
.padding(.vertical, Design.Spacing.xxLarge)
|
||||
.background(
|
||||
@ -193,6 +238,14 @@ struct ResultBannerView: View {
|
||||
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
|
||||
announceResult()
|
||||
}
|
||||
@ -388,7 +441,7 @@ struct ConfettiView: View {
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
#Preview("Win") {
|
||||
ZStack {
|
||||
Color.Table.preview
|
||||
.ignoresSafeArea()
|
||||
@ -404,7 +457,31 @@ struct ConfettiView: View {
|
||||
],
|
||||
playerHadPair: true,
|
||||
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"),
|
||||
content: {
|
||||
// Table Limits Section (First!)
|
||||
SheetSection(title: "TABLE LIMITS", icon: "banknote") {
|
||||
SheetSection(title: String(localized: "TABLE LIMITS"), icon: "banknote") {
|
||||
TableLimitsPicker(selection: $settings.tableLimits)
|
||||
.onChange(of: settings.tableLimits) { _, _ in
|
||||
hasChanges = true
|
||||
@ -29,7 +29,7 @@ struct SettingsView: View {
|
||||
}
|
||||
|
||||
// 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)
|
||||
.onChange(of: settings.deckCount) { _, _ in
|
||||
hasChanges = true
|
||||
@ -37,7 +37,7 @@ struct SettingsView: View {
|
||||
}
|
||||
|
||||
// Starting Balance Section
|
||||
SheetSection(title: "STARTING BALANCE", icon: "dollarsign.circle") {
|
||||
SheetSection(title: String(localized: "STARTING BALANCE"), icon: "dollarsign.circle") {
|
||||
BalancePicker(balance: $settings.startingBalance)
|
||||
.onChange(of: settings.startingBalance) { _, _ in
|
||||
hasChanges = true
|
||||
@ -45,10 +45,10 @@ struct SettingsView: View {
|
||||
}
|
||||
|
||||
// Display Settings Section
|
||||
SheetSection(title: "DISPLAY", icon: "eye") {
|
||||
SheetSection(title: String(localized: "DISPLAY"), icon: "eye") {
|
||||
SettingsToggle(
|
||||
title: "Show Cards Remaining",
|
||||
subtitle: "Display deck counter at top",
|
||||
title: String(localized: "Show Cards Remaining"),
|
||||
subtitle: String(localized: "Display deck counter at top"),
|
||||
isOn: $settings.showCardsRemaining
|
||||
)
|
||||
|
||||
@ -56,17 +56,17 @@ struct SettingsView: View {
|
||||
.background(Color.white.opacity(Design.Opacity.subtle))
|
||||
|
||||
SettingsToggle(
|
||||
title: "Show History",
|
||||
subtitle: "Display result road map",
|
||||
title: String(localized: "Show History"),
|
||||
subtitle: String(localized: "Display result road map"),
|
||||
isOn: $settings.showHistory
|
||||
)
|
||||
}
|
||||
|
||||
// Animation Settings Section
|
||||
SheetSection(title: "ANIMATIONS", icon: "sparkles") {
|
||||
SheetSection(title: String(localized: "ANIMATIONS"), icon: "sparkles") {
|
||||
SettingsToggle(
|
||||
title: "Card Animations",
|
||||
subtitle: "Animate dealing and flipping",
|
||||
title: String(localized: "Card Animations"),
|
||||
subtitle: String(localized: "Animate dealing and flipping"),
|
||||
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
|
||||
Button {
|
||||
settings.resetToDefaults()
|
||||
@ -85,7 +121,7 @@ struct SettingsView: View {
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "arrow.counterclockwise")
|
||||
Text("Reset to Defaults")
|
||||
Text(String(localized: "Reset to Defaults"))
|
||||
}
|
||||
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
||||
.foregroundStyle(.red.opacity(Design.Opacity.heavy))
|
||||
@ -234,7 +270,7 @@ struct SpeedPicker: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
Text("Dealing Speed")
|
||||
Text(String(localized: "Dealing Speed"))
|
||||
.font(.system(size: Design.BaseFontSize.subheadline, weight: .medium))
|
||||
.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 {
|
||||
SettingsView(settings: GameSettings()) { }
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user