CasinoGames/Baccarat/Views/SettingsView.swift

525 lines
23 KiB
Swift

//
// SettingsView.swift
// Baccarat
//
// Settings screen for game customization.
//
import SwiftUI
import CasinoKit
/// The settings screen for customizing game options.
struct SettingsView: View {
@Bindable var settings: GameSettings
let gameState: GameState
@Environment(\.dismiss) private var dismiss
let onApplyChanges: () -> Void
@State private var hasChanges = false
@State private var showClearDataAlert = false
var body: some View {
SheetContainerView(
title: String(localized: "Settings"),
content: {
// Table Limits Section (First!)
SheetSection(title: String(localized: "TABLE LIMITS"), icon: "banknote") {
TableLimitsPicker(selection: $settings.tableLimits)
.onChange(of: settings.tableLimits) { _, _ in
hasChanges = true
}
}
// Deck Settings Section
SheetSection(title: String(localized: "DECK SETTINGS"), icon: "rectangle.portrait.on.rectangle.portrait") {
DeckCountPicker(selection: $settings.deckCount)
.onChange(of: settings.deckCount) { _, _ in
hasChanges = true
}
}
// Starting Balance Section
SheetSection(title: String(localized: "STARTING BALANCE"), icon: "dollarsign.circle") {
BalancePicker(balance: $settings.startingBalance)
.onChange(of: settings.startingBalance) { _, _ in
hasChanges = true
}
}
// Display Settings Section
SheetSection(title: String(localized: "DISPLAY"), icon: "eye") {
SettingsToggle(
title: String(localized: "Show Cards Remaining"),
subtitle: String(localized: "Display deck counter at top"),
isOn: $settings.showCardsRemaining
)
Divider()
.background(Color.white.opacity(Design.Opacity.subtle))
SettingsToggle(
title: String(localized: "Show History"),
subtitle: String(localized: "Display result road map"),
isOn: $settings.showHistory
)
}
// Animation Settings Section
SheetSection(title: String(localized: "ANIMATIONS"), icon: "sparkles") {
SettingsToggle(
title: String(localized: "Card Animations"),
subtitle: String(localized: "Animate dealing and flipping"),
isOn: $settings.showAnimations
)
if settings.showAnimations {
Divider()
.background(Color.white.opacity(Design.Opacity.subtle))
SpeedPicker(speed: $settings.dealingSpeed)
}
}
// 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
}
}
// iCloud Sync Section
SheetSection(title: String(localized: "CLOUD SYNC"), icon: "icloud") {
if gameState.iCloudAvailable {
Toggle(isOn: Binding(
get: { gameState.iCloudEnabled },
set: { gameState.iCloudEnabled = $0 }
)) {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(String(localized: "iCloud Sync"))
.font(.system(size: Design.BaseFontSize.subheadline, weight: .medium))
.foregroundStyle(.white)
Text(String(localized: "Sync progress across devices"))
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
}
.tint(.yellow)
if gameState.iCloudEnabled {
Divider()
.background(Color.white.opacity(Design.Opacity.subtle))
HStack {
Text(String(localized: "Last Synced"))
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
Spacer()
if let lastSync = gameState.lastSyncDate {
Text(lastSync, style: .relative)
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
} else {
Text(String(localized: "Never"))
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
}
Divider()
.background(Color.white.opacity(Design.Opacity.subtle))
Button {
gameState.syncWithCloud()
} label: {
HStack {
Image(systemName: "arrow.triangle.2.circlepath")
Text(String(localized: "Sync Now"))
}
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.yellow)
}
}
} else {
HStack {
Image(systemName: "icloud.slash")
.foregroundStyle(.white.opacity(Design.Opacity.medium))
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(String(localized: "iCloud Unavailable"))
.font(.system(size: Design.BaseFontSize.subheadline, weight: .medium))
.foregroundStyle(.white)
Text(String(localized: "Sign in to iCloud to sync progress"))
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
}
}
}
// Data Section
SheetSection(title: String(localized: "DATA"), icon: "externaldrive") {
HStack {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(String(localized: "Lifetime Rounds"))
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
Text("\(gameState.lifetimeStats.roundsPlayed)")
.font(.system(size: Design.BaseFontSize.xxLarge, weight: .bold, design: .rounded))
.foregroundStyle(.white)
}
Spacer()
VStack(alignment: .trailing, spacing: Design.Spacing.xxSmall) {
Text(String(localized: "Total Winnings"))
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
let winnings = gameState.lifetimeStats.totalWinnings
Text(winnings >= 0 ? "+$\(winnings)" : "-$\(abs(winnings))")
.font(.system(size: Design.BaseFontSize.xxLarge, weight: .bold, design: .rounded))
.foregroundStyle(winnings >= 0 ? .green : .red)
}
}
Divider()
.background(Color.white.opacity(Design.Opacity.subtle))
Button(role: .destructive) {
showClearDataAlert = true
} label: {
HStack {
Image(systemName: "trash")
Text(String(localized: "Clear All Data"))
}
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.red)
}
}
// Reset Button
Button {
settings.resetToDefaults()
hasChanges = true
} label: {
HStack {
Image(systemName: "arrow.counterclockwise")
Text(String(localized: "Reset to Defaults"))
}
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
.foregroundStyle(.red.opacity(Design.Opacity.heavy))
.padding()
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.fill(Color.red.opacity(Design.Opacity.subtle))
)
}
.padding(.horizontal)
.padding(.top, Design.Spacing.small)
},
onCancel: {
settings.load() // Revert changes
dismiss()
},
onDone: {
settings.save()
if hasChanges {
onApplyChanges()
}
dismiss()
},
doneButtonText: String(localized: "Done"),
cancelButtonText: String(localized: "Cancel")
)
.alert(String(localized: "Clear All Data?"), isPresented: $showClearDataAlert) {
Button(String(localized: "Cancel"), role: .cancel) { }
Button(String(localized: "Clear"), role: .destructive) {
gameState.clearAllData()
}
} message: {
Text(String(localized: "This will delete all saved progress, statistics, and reset your balance. This cannot be undone."))
}
}
}
/// Deck count picker with visual options.
struct DeckCountPicker: View {
@Binding var selection: DeckCount
var body: some View {
VStack(spacing: Design.Spacing.medium) {
ForEach(DeckCount.allCases) { count in
Button {
selection = count
} label: {
HStack {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(count.displayName)
.font(.system(size: Design.BaseFontSize.large, weight: .semibold))
.foregroundStyle(.white)
Text(count.description)
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
Spacer()
if selection == count {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: Design.Size.checkmark))
.foregroundStyle(.yellow)
} else {
Circle()
.strokeBorder(Color.white.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.medium)
.frame(width: Design.Size.checkmark, height: Design.Size.checkmark)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(selection == count ? Color.yellow.opacity(Design.Opacity.subtle) : Color.clear)
)
.overlay(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.strokeBorder(
selection == count ? Color.yellow.opacity(Design.Opacity.medium) : Color.white.opacity(Design.Opacity.subtle),
lineWidth: Design.LineWidth.thin
)
)
}
.buttonStyle(.plain)
}
}
}
}
/// Starting balance picker.
struct BalancePicker: View {
@Binding var balance: Int
private let options = [1_000, 5_000, 10_000, 25_000, 50_000, 100_000]
var body: some View {
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
], spacing: Design.Spacing.small) {
ForEach(options, id: \.self) { amount in
Button {
balance = amount
} label: {
Text("$\(amount / 1000)K")
.font(.system(size: Design.BaseFontSize.medium, weight: .bold))
.foregroundStyle(balance == amount ? .black : .white)
.padding(.vertical, Design.Spacing.medium)
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
.fill(balance == amount ? Color.yellow : Color.white.opacity(Design.Opacity.subtle))
)
}
.buttonStyle(.plain)
}
}
}
}
/// A toggle setting row.
struct SettingsToggle: View {
let title: String
let subtitle: String
@Binding var isOn: Bool
var body: some View {
Toggle(isOn: $isOn) {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(title)
.font(.system(size: Design.BaseFontSize.subheadline, weight: .medium))
.foregroundStyle(.white)
Text(subtitle)
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
}
.tint(.yellow)
}
}
/// Animation speed picker.
struct SpeedPicker: View {
@Binding var speed: Double
private let options: [(String, Double)] = [
("Fast", 0.5),
("Normal", 1.0),
("Slow", 2.0)
]
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(String(localized: "Dealing Speed"))
.font(.system(size: Design.BaseFontSize.subheadline, weight: .medium))
.foregroundStyle(.white)
HStack(spacing: Design.Spacing.small) {
ForEach(options, id: \.1) { option in
Button {
speed = option.1
} label: {
Text(option.0)
.font(.system(size: Design.BaseFontSize.callout, weight: .medium))
.foregroundStyle(speed == option.1 ? .black : .white.opacity(Design.Opacity.strong))
.padding(.vertical, Design.Spacing.small)
.frame(maxWidth: .infinity)
.background(
Capsule()
.fill(speed == option.1 ? Color.yellow : Color.white.opacity(Design.Opacity.subtle))
)
}
.buttonStyle(.plain)
}
}
}
}
}
/// Table limits picker for min/max bets.
struct TableLimitsPicker: View {
@Binding var selection: TableLimits
var body: some View {
VStack(spacing: Design.Spacing.small) {
ForEach(TableLimits.allCases) { limit in
Button {
selection = limit
} label: {
HStack {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(limit.displayName)
.font(.system(size: Design.BaseFontSize.large, weight: .semibold))
.foregroundStyle(.white)
Text(limit.detailedDescription)
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
Spacer()
// Limits badge
Text(limit.description)
.font(.system(size: Design.BaseFontSize.body, weight: .bold, design: .rounded))
.foregroundStyle(selection == limit ? .black : .yellow)
.padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xSmall)
.background(
Capsule()
.fill(selection == limit ? Color.yellow : Color.yellow.opacity(Design.Opacity.hint))
)
if selection == limit {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: Design.Size.checkmark - 2))
.foregroundStyle(.yellow)
} else {
Circle()
.strokeBorder(Color.white.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.medium)
.frame(width: Design.Size.checkmark - 2, height: Design.Size.checkmark - 2)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(selection == limit ? Color.yellow.opacity(Design.Opacity.subtle) : Color.clear)
)
.overlay(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.strokeBorder(
selection == limit ? Color.yellow.opacity(Design.Opacity.medium) : Color.white.opacity(Design.Opacity.subtle),
lineWidth: Design.LineWidth.thin
)
)
}
.buttonStyle(.plain)
}
}
}
}
/// 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(), gameState: GameState()) { }
}