386 lines
16 KiB
Swift
386 lines
16 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 {
|
|
NavigationStack {
|
|
ZStack {
|
|
// Background
|
|
Color.Settings.background
|
|
.ignoresSafeArea()
|
|
|
|
ScrollView {
|
|
VStack(spacing: Design.Spacing.xxLarge) {
|
|
// Table Limits Section (First!)
|
|
SettingsSection(title: "TABLE LIMITS", icon: "banknote") {
|
|
TableLimitsPicker(selection: $settings.tableLimits)
|
|
.onChange(of: settings.tableLimits) { _, _ in
|
|
hasChanges = true
|
|
}
|
|
}
|
|
|
|
// Deck Settings Section
|
|
SettingsSection(title: "DECK SETTINGS", icon: "rectangle.portrait.on.rectangle.portrait") {
|
|
DeckCountPicker(selection: $settings.deckCount)
|
|
.onChange(of: settings.deckCount) { _, _ in
|
|
hasChanges = true
|
|
}
|
|
}
|
|
|
|
// Starting Balance Section
|
|
SettingsSection(title: "STARTING BALANCE", icon: "dollarsign.circle") {
|
|
BalancePicker(balance: $settings.startingBalance)
|
|
.onChange(of: settings.startingBalance) { _, _ in
|
|
hasChanges = true
|
|
}
|
|
}
|
|
|
|
// Display Settings Section
|
|
SettingsSection(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
|
|
SettingsSection(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)
|
|
}
|
|
.padding(.vertical)
|
|
}
|
|
}
|
|
.navigationTitle("Settings")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbarBackground(Color.Settings.background, for: .navigationBar)
|
|
.toolbarBackground(.visible, for: .navigationBar)
|
|
.toolbarColorScheme(.dark, for: .navigationBar)
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarLeading) {
|
|
Button("Cancel") {
|
|
settings.load() // Revert changes
|
|
dismiss()
|
|
}
|
|
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
|
}
|
|
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button("Done") {
|
|
settings.save()
|
|
if hasChanges {
|
|
onApplyChanges()
|
|
}
|
|
dismiss()
|
|
}
|
|
.bold()
|
|
.foregroundStyle(.yellow)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A settings section with a title and content.
|
|
struct SettingsSection<Content: View>: View {
|
|
let title: String
|
|
let icon: String
|
|
@ViewBuilder let content: Content
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
|
// Header
|
|
HStack(spacing: Design.Spacing.small) {
|
|
Image(systemName: icon)
|
|
.font(.system(size: Design.BaseFontSize.body, weight: .semibold))
|
|
.foregroundStyle(.yellow.opacity(Design.Opacity.heavy))
|
|
|
|
Text(title)
|
|
.font(.system(size: Design.BaseFontSize.body, weight: .bold, design: .rounded))
|
|
.tracking(1)
|
|
.foregroundStyle(.white.opacity(Design.Opacity.accent))
|
|
}
|
|
.padding(.horizontal, Design.Spacing.xSmall)
|
|
|
|
// Content card
|
|
VStack(spacing: 0) {
|
|
content
|
|
}
|
|
.padding()
|
|
.background(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
|
.fill(Color.white.opacity(Design.Opacity.verySubtle))
|
|
)
|
|
}
|
|
.padding(.horizontal)
|
|
}
|
|
}
|
|
|
|
/// 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()) { }
|
|
}
|
|
|