CasinoGames/Baccarat/Views/SettingsView.swift

330 lines
13 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
@Environment(\.dismiss) private var dismiss
let onApplyChanges: () -> Void
@State private var hasChanges = false
var body: some View {
SheetContainerView(
title: String(localized: "Settings"),
content: {
// Table Limits Section (First!)
SheetSection(title: "TABLE LIMITS", icon: "banknote") {
TableLimitsPicker(selection: $settings.tableLimits)
.onChange(of: settings.tableLimits) { _, _ in
hasChanges = true
}
}
// Deck Settings Section
SheetSection(title: "DECK SETTINGS", icon: "rectangle.portrait.on.rectangle.portrait") {
DeckCountPicker(selection: $settings.deckCount)
.onChange(of: settings.deckCount) { _, _ in
hasChanges = true
}
}
// Starting Balance Section
SheetSection(title: "STARTING BALANCE", icon: "dollarsign.circle") {
BalancePicker(balance: $settings.startingBalance)
.onChange(of: settings.startingBalance) { _, _ in
hasChanges = true
}
}
// Display Settings Section
SheetSection(title: "DISPLAY", icon: "eye") {
SettingsToggle(
title: "Show Cards Remaining",
subtitle: "Display deck counter at top",
isOn: $settings.showCardsRemaining
)
Divider()
.background(Color.white.opacity(Design.Opacity.subtle))
SettingsToggle(
title: "Show History",
subtitle: "Display result road map",
isOn: $settings.showHistory
)
}
// Animation Settings Section
SheetSection(title: "ANIMATIONS", icon: "sparkles") {
SettingsToggle(
title: "Card Animations",
subtitle: "Animate dealing and flipping",
isOn: $settings.showAnimations
)
if settings.showAnimations {
Divider()
.background(Color.white.opacity(Design.Opacity.subtle))
SpeedPicker(speed: $settings.dealingSpeed)
}
}
// Reset Button
Button {
settings.resetToDefaults()
hasChanges = true
} label: {
HStack {
Image(systemName: "arrow.counterclockwise")
Text("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")
)
}
}
/// 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("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)
}
}
}
}
#Preview {
SettingsView(settings: GameSettings()) { }
}