432 lines
15 KiB
Swift
432 lines
15 KiB
Swift
//
|
|
// GameSettings.swift
|
|
// Baccarat
|
|
//
|
|
// User-configurable game settings.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftUI
|
|
import CasinoKit
|
|
|
|
/// The number of decks available for the shoe.
|
|
enum DeckCount: Int, CaseIterable, Identifiable {
|
|
case one = 1
|
|
case six = 6
|
|
case eight = 8
|
|
|
|
var id: Int { rawValue }
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .one: return "1 Deck"
|
|
case .six: return "6 Decks"
|
|
case .eight: return "8 Decks (Standard)"
|
|
}
|
|
}
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .one: return "Rare, for private games"
|
|
case .six: return "Common in mini baccarat"
|
|
case .eight: return "Casino standard"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The style used to reveal cards.
|
|
enum RevealStyle: String, CaseIterable, Codable, Identifiable {
|
|
case auto // Automatically reveal (original behavior)
|
|
case tap // Tap each card to reveal
|
|
case squeeze // Tap + Hold / Drag for pressure-based reveal
|
|
|
|
var id: String { rawValue } // Keep Identifiable conformance for SwiftUI
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .auto: return String(localized: "Auto Reveal")
|
|
case .tap: return String(localized: "Tap Reveal")
|
|
case .squeeze: return String(localized: "Squeeze Reveal")
|
|
}
|
|
}
|
|
|
|
var helpText: String {
|
|
switch self {
|
|
case .auto: return String(localized: "Cards flip automatically")
|
|
case .tap: return String(localized: "Tap each card to reveal")
|
|
case .squeeze: return String(localized: "Squeeze card to peek at value")
|
|
}
|
|
}
|
|
|
|
var iconName: String {
|
|
switch self {
|
|
case .auto: return "bolt.fill"
|
|
case .tap: return "hand.tap.fill"
|
|
case .squeeze: return "hand.point.up.left.and.text"
|
|
}
|
|
}
|
|
}
|
|
|
|
// TableLimits is now provided by CasinoKit
|
|
|
|
/// Observable settings class for Baccarat configuration.
|
|
/// Conforms to GameSettingsProtocol for shared settings behavior.
|
|
@Observable
|
|
@MainActor
|
|
final class GameSettings: GameSettingsProtocol {
|
|
// MARK: - Deck Settings
|
|
|
|
/// Number of decks in the shoe.
|
|
var deckCount: DeckCount = .eight
|
|
|
|
// MARK: - Betting Limits
|
|
|
|
/// The table limits preset.
|
|
var tableLimits: TableLimits = .casual
|
|
|
|
/// Minimum bet amount.
|
|
var minBet: Int {
|
|
tableLimits.minBet
|
|
}
|
|
|
|
/// Maximum bet amount per betting spot.
|
|
var maxBet: Int {
|
|
tableLimits.maxBet
|
|
}
|
|
|
|
// MARK: - Starting Balance
|
|
|
|
/// The starting balance for new games.
|
|
var startingBalance: Int = 1_000
|
|
|
|
// MARK: - Animation Settings
|
|
|
|
/// Whether to show dealing animations.
|
|
var showAnimations: Bool = true
|
|
|
|
/// Speed of card dealing (uses CasinoDesign.DealingSpeed constants)
|
|
var dealingSpeed: Double = CasinoDesign.DealingSpeed.normal
|
|
|
|
/// The style used to reveal cards.
|
|
var revealStyle: RevealStyle = .auto
|
|
|
|
// MARK: - Display Settings
|
|
|
|
/// Whether to show the cards remaining indicator.
|
|
var showCardsRemaining: Bool = true
|
|
|
|
/// Whether to show the history road map.
|
|
var showHistory: Bool = true
|
|
|
|
/// Whether to show betting hints and recommendations.
|
|
var showHints: Bool = true
|
|
|
|
// MARK: - Road Map Settings
|
|
|
|
/// The preferred road type to display.
|
|
var preferredRoadType: RoadType = .big
|
|
|
|
/// Whether to show streak alerts.
|
|
var showStreakAlerts: 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 {
|
|
static let deckCount = "settings.deckCount"
|
|
static let tableLimits = "settings.tableLimits"
|
|
static let startingBalance = "settings.startingBalance"
|
|
static let showAnimations = "settings.showAnimations"
|
|
static let dealingSpeed = "settings.dealingSpeed"
|
|
static let showCardsRemaining = "settings.showCardsRemaining"
|
|
static let showHistory = "settings.showHistory"
|
|
static let showHints = "settings.showHints"
|
|
static let soundEnabled = "settings.soundEnabled"
|
|
static let hapticsEnabled = "settings.hapticsEnabled"
|
|
static let soundVolume = "settings.soundVolume"
|
|
static let revealStyle = "settings.revealStyle"
|
|
static let preferredRoadType = "settings.preferredRoadType"
|
|
static let showStreakAlerts = "settings.showStreakAlerts"
|
|
}
|
|
|
|
// MARK: - iCloud
|
|
|
|
/// Cached reference to iCloud store, lazily initialized
|
|
private var _iCloudStore: NSUbiquitousKeyValueStore?
|
|
private var _iCloudStoreInitialized = false
|
|
|
|
private var iCloudStore: NSUbiquitousKeyValueStore? {
|
|
// Return cached value if already attempted initialization
|
|
if _iCloudStoreInitialized {
|
|
return _iCloudStore
|
|
}
|
|
|
|
_iCloudStoreInitialized = true
|
|
|
|
// Only access the store if iCloud is actually available
|
|
guard iCloudAvailable else {
|
|
return nil
|
|
}
|
|
|
|
_iCloudStore = NSUbiquitousKeyValueStore.default
|
|
return _iCloudStore
|
|
}
|
|
|
|
/// Whether iCloud is available.
|
|
var iCloudAvailable: Bool {
|
|
// NSUbiquitousKeyValueStore only requires iCloud sign-in (token)
|
|
// It does NOT require iCloud Drive/Documents to be enabled
|
|
FileManager.default.ubiquityIdentityToken != nil
|
|
}
|
|
|
|
// MARK: - Initialization
|
|
|
|
init() {
|
|
load()
|
|
|
|
// Register for iCloud changes (only if available)
|
|
if iCloudAvailable, let store = iCloudStore {
|
|
NotificationCenter.default.addObserver(
|
|
forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
|
|
object: store,
|
|
queue: .main
|
|
) { [weak self] _ in
|
|
// Already on main queue, safe to call
|
|
self?.loadFromiCloud()
|
|
}
|
|
|
|
// Trigger iCloud sync
|
|
store.synchronize()
|
|
}
|
|
}
|
|
|
|
// MARK: - Persistence
|
|
|
|
/// Loads settings from UserDefaults, then checks iCloud for newer settings.
|
|
func load() {
|
|
// First load from local UserDefaults
|
|
loadFromLocal()
|
|
|
|
// Then check iCloud for potentially newer settings
|
|
if iCloudAvailable {
|
|
loadFromiCloud()
|
|
}
|
|
}
|
|
|
|
/// Loads settings from local UserDefaults.
|
|
private func loadFromLocal() {
|
|
let defaults = UserDefaults.standard
|
|
|
|
if let rawDeckCount = defaults.object(forKey: Keys.deckCount) as? Int,
|
|
let deckCount = DeckCount(rawValue: rawDeckCount) {
|
|
self.deckCount = deckCount
|
|
}
|
|
|
|
if let rawTableLimits = defaults.string(forKey: Keys.tableLimits),
|
|
let tableLimits = TableLimits(rawValue: rawTableLimits) {
|
|
self.tableLimits = tableLimits
|
|
}
|
|
|
|
if let balance = defaults.object(forKey: Keys.startingBalance) as? Int {
|
|
self.startingBalance = Self.validatedStartingBalance(balance)
|
|
}
|
|
|
|
if defaults.object(forKey: Keys.showAnimations) != nil {
|
|
self.showAnimations = defaults.bool(forKey: Keys.showAnimations)
|
|
}
|
|
|
|
if let speed = defaults.object(forKey: Keys.dealingSpeed) as? Double {
|
|
self.dealingSpeed = Self.validatedDealingSpeed(speed)
|
|
}
|
|
|
|
if defaults.object(forKey: Keys.showCardsRemaining) != nil {
|
|
self.showCardsRemaining = defaults.bool(forKey: Keys.showCardsRemaining)
|
|
}
|
|
|
|
if defaults.object(forKey: Keys.showHistory) != nil {
|
|
self.showHistory = defaults.bool(forKey: Keys.showHistory)
|
|
}
|
|
|
|
if defaults.object(forKey: Keys.showHints) != nil {
|
|
self.showHints = defaults.bool(forKey: Keys.showHints)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if let rawStyle = defaults.string(forKey: Keys.revealStyle),
|
|
let style = RevealStyle(rawValue: rawStyle) {
|
|
self.revealStyle = style
|
|
}
|
|
|
|
if let rawRoadType = defaults.string(forKey: Keys.preferredRoadType),
|
|
let roadType = RoadType(rawValue: rawRoadType) {
|
|
self.preferredRoadType = roadType
|
|
}
|
|
|
|
if defaults.object(forKey: Keys.showStreakAlerts) != nil {
|
|
self.showStreakAlerts = defaults.bool(forKey: Keys.showStreakAlerts)
|
|
}
|
|
}
|
|
|
|
/// Loads settings from iCloud.
|
|
private func loadFromiCloud() {
|
|
guard iCloudAvailable, let store = iCloudStore else { return }
|
|
|
|
if let rawDeckCount = store.object(forKey: Keys.deckCount) as? Int,
|
|
let deckCount = DeckCount(rawValue: rawDeckCount) {
|
|
self.deckCount = deckCount
|
|
}
|
|
|
|
if let rawTableLimits = store.string(forKey: Keys.tableLimits),
|
|
let tableLimits = TableLimits(rawValue: rawTableLimits) {
|
|
self.tableLimits = tableLimits
|
|
}
|
|
|
|
if let balance = store.object(forKey: Keys.startingBalance) as? Int {
|
|
self.startingBalance = Self.validatedStartingBalance(balance)
|
|
}
|
|
|
|
if store.object(forKey: Keys.showAnimations) != nil {
|
|
self.showAnimations = store.bool(forKey: Keys.showAnimations)
|
|
}
|
|
|
|
if let speed = store.object(forKey: Keys.dealingSpeed) as? Double {
|
|
self.dealingSpeed = Self.validatedDealingSpeed(speed)
|
|
}
|
|
|
|
if store.object(forKey: Keys.showCardsRemaining) != nil {
|
|
self.showCardsRemaining = store.bool(forKey: Keys.showCardsRemaining)
|
|
}
|
|
|
|
if store.object(forKey: Keys.showHistory) != nil {
|
|
self.showHistory = store.bool(forKey: Keys.showHistory)
|
|
}
|
|
|
|
if store.object(forKey: Keys.showHints) != nil {
|
|
self.showHints = store.bool(forKey: Keys.showHints)
|
|
}
|
|
|
|
if store.object(forKey: Keys.soundEnabled) != nil {
|
|
self.soundEnabled = store.bool(forKey: Keys.soundEnabled)
|
|
}
|
|
|
|
if store.object(forKey: Keys.hapticsEnabled) != nil {
|
|
self.hapticsEnabled = store.bool(forKey: Keys.hapticsEnabled)
|
|
}
|
|
|
|
if let volume = store.object(forKey: Keys.soundVolume) as? Double {
|
|
self.soundVolume = Float(volume)
|
|
}
|
|
|
|
if let rawStyle = store.string(forKey: Keys.revealStyle),
|
|
let style = RevealStyle(rawValue: rawStyle) {
|
|
self.revealStyle = style
|
|
}
|
|
|
|
if let rawRoadType = store.string(forKey: Keys.preferredRoadType),
|
|
let roadType = RoadType(rawValue: rawRoadType) {
|
|
self.preferredRoadType = roadType
|
|
}
|
|
|
|
if store.object(forKey: Keys.showStreakAlerts) != nil {
|
|
self.showStreakAlerts = store.bool(forKey: Keys.showStreakAlerts)
|
|
}
|
|
}
|
|
|
|
/// Saves settings to UserDefaults and iCloud.
|
|
func save() {
|
|
// Save to local UserDefaults
|
|
let defaults = UserDefaults.standard
|
|
defaults.set(deckCount.rawValue, forKey: Keys.deckCount)
|
|
defaults.set(tableLimits.rawValue, forKey: Keys.tableLimits)
|
|
defaults.set(startingBalance, forKey: Keys.startingBalance)
|
|
defaults.set(showAnimations, forKey: Keys.showAnimations)
|
|
defaults.set(dealingSpeed, forKey: Keys.dealingSpeed)
|
|
defaults.set(showCardsRemaining, forKey: Keys.showCardsRemaining)
|
|
defaults.set(showHistory, forKey: Keys.showHistory)
|
|
defaults.set(showHints, forKey: Keys.showHints)
|
|
defaults.set(soundEnabled, forKey: Keys.soundEnabled)
|
|
defaults.set(hapticsEnabled, forKey: Keys.hapticsEnabled)
|
|
defaults.set(soundVolume, forKey: Keys.soundVolume)
|
|
defaults.set(revealStyle.rawValue, forKey: Keys.revealStyle)
|
|
defaults.set(preferredRoadType.rawValue, forKey: Keys.preferredRoadType)
|
|
defaults.set(showStreakAlerts, forKey: Keys.showStreakAlerts)
|
|
|
|
// Also save to iCloud
|
|
if iCloudAvailable, let store = iCloudStore {
|
|
store.set(deckCount.rawValue, forKey: Keys.deckCount)
|
|
store.set(tableLimits.rawValue, forKey: Keys.tableLimits)
|
|
store.set(startingBalance, forKey: Keys.startingBalance)
|
|
store.set(showAnimations, forKey: Keys.showAnimations)
|
|
store.set(dealingSpeed, forKey: Keys.dealingSpeed)
|
|
store.set(showCardsRemaining, forKey: Keys.showCardsRemaining)
|
|
store.set(showHistory, forKey: Keys.showHistory)
|
|
store.set(showHints, forKey: Keys.showHints)
|
|
store.set(soundEnabled, forKey: Keys.soundEnabled)
|
|
store.set(hapticsEnabled, forKey: Keys.hapticsEnabled)
|
|
store.set(Double(soundVolume), forKey: Keys.soundVolume)
|
|
store.set(revealStyle.rawValue, forKey: Keys.revealStyle)
|
|
store.set(preferredRoadType.rawValue, forKey: Keys.preferredRoadType)
|
|
store.set(showStreakAlerts, forKey: Keys.showStreakAlerts)
|
|
store.synchronize()
|
|
}
|
|
}
|
|
|
|
/// Validates a dealing speed value, returning normal if invalid.
|
|
/// This handles legacy data that may have been saved with incorrect values.
|
|
private static func validatedDealingSpeed(_ speed: Double) -> Double {
|
|
let validSpeeds = [
|
|
CasinoDesign.DealingSpeed.fast,
|
|
CasinoDesign.DealingSpeed.normal,
|
|
CasinoDesign.DealingSpeed.slow
|
|
]
|
|
return validSpeeds.contains(speed) ? speed : CasinoDesign.DealingSpeed.normal
|
|
}
|
|
|
|
/// Validates a starting balance value, returning default if invalid.
|
|
/// This handles legacy data that may have been saved with incorrect values.
|
|
private static func validatedStartingBalance(_ balance: Int) -> Int {
|
|
let validBalances = [1_000, 5_000, 10_000, 25_000, 50_000, 100_000]
|
|
return validBalances.contains(balance) ? balance : 1_000
|
|
}
|
|
|
|
/// Resets all settings to defaults.
|
|
func resetToDefaults() {
|
|
deckCount = .eight
|
|
tableLimits = .casual
|
|
startingBalance = 1_000
|
|
showAnimations = true
|
|
dealingSpeed = CasinoDesign.DealingSpeed.normal
|
|
showCardsRemaining = true
|
|
showHistory = true
|
|
showHints = true
|
|
soundEnabled = true
|
|
hapticsEnabled = true
|
|
soundVolume = 1.0
|
|
revealStyle = .auto
|
|
preferredRoadType = .big
|
|
showStreakAlerts = true
|
|
save()
|
|
}
|
|
}
|