336 lines
15 KiB
Swift
336 lines
15 KiB
Swift
//
|
|
// SettingsView.swift
|
|
// Blackjack
|
|
//
|
|
// Game settings and rule configuration.
|
|
//
|
|
|
|
import SwiftUI
|
|
import CasinoKit
|
|
|
|
struct SettingsView: View {
|
|
@Bindable var settings: GameSettings
|
|
let gameState: GameState?
|
|
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
var body: some View {
|
|
SheetContainerView(
|
|
title: String(localized: "Settings"),
|
|
content: {
|
|
// Game Style
|
|
SheetSection(title: String(localized: "GAME STYLE"), icon: "suit.club.fill") {
|
|
GameStylePicker(selection: $settings.gameStyle)
|
|
}
|
|
|
|
// Deck Settings
|
|
SheetSection(title: String(localized: "DECK SETTINGS"), icon: "rectangle.portrait.on.rectangle.portrait") {
|
|
DeckCountPicker(selection: $settings.deckCount)
|
|
}
|
|
|
|
// Table Limits
|
|
SheetSection(title: String(localized: "TABLE LIMITS"), icon: "banknote") {
|
|
TableLimitsPicker(selection: $settings.tableLimits)
|
|
}
|
|
|
|
// Rule Options (for custom style)
|
|
if settings.gameStyle == .custom {
|
|
SheetSection(title: String(localized: "RULES"), icon: "list.bullet.clipboard") {
|
|
VStack(spacing: Design.Spacing.small) {
|
|
SettingsToggle(
|
|
title: String(localized: "Dealer Hits Soft 17"),
|
|
subtitle: String(localized: "H17 rule, increases house edge"),
|
|
isOn: $settings.dealerHitsSoft17
|
|
)
|
|
|
|
Divider().background(Color.white.opacity(Design.Opacity.hint))
|
|
|
|
SettingsToggle(
|
|
title: String(localized: "Double After Split"),
|
|
subtitle: String(localized: "Allow doubling on split hands"),
|
|
isOn: $settings.doubleAfterSplit
|
|
)
|
|
|
|
Divider().background(Color.white.opacity(Design.Opacity.hint))
|
|
|
|
SettingsToggle(
|
|
title: String(localized: "Re-split Aces"),
|
|
subtitle: String(localized: "Allow splitting aces again"),
|
|
isOn: $settings.resplitAces
|
|
)
|
|
|
|
Divider().background(Color.white.opacity(Design.Opacity.hint))
|
|
|
|
SettingsToggle(
|
|
title: String(localized: "Late Surrender"),
|
|
subtitle: String(localized: "Surrender after dealer checks for blackjack"),
|
|
isOn: $settings.lateSurrender
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Display
|
|
SheetSection(title: String(localized: "DISPLAY"), icon: "eye") {
|
|
VStack(spacing: Design.Spacing.small) {
|
|
SettingsToggle(
|
|
title: String(localized: "Show Animations"),
|
|
subtitle: String(localized: "Card dealing animations"),
|
|
isOn: $settings.showAnimations
|
|
)
|
|
|
|
Divider().background(Color.white.opacity(Design.Opacity.hint))
|
|
|
|
SettingsToggle(
|
|
title: String(localized: "Show Hints"),
|
|
subtitle: String(localized: "Basic strategy suggestions"),
|
|
isOn: $settings.showHints
|
|
)
|
|
|
|
Divider().background(Color.white.opacity(Design.Opacity.hint))
|
|
|
|
SettingsToggle(
|
|
title: String(localized: "Cards Remaining"),
|
|
subtitle: String(localized: "Show cards left in shoe"),
|
|
isOn: $settings.showCardsRemaining
|
|
)
|
|
|
|
Divider().background(Color.white.opacity(Design.Opacity.hint))
|
|
|
|
SpeedPicker(speed: $settings.dealingSpeed)
|
|
}
|
|
}
|
|
|
|
// Sound & Haptics
|
|
SheetSection(title: String(localized: "SOUND & HAPTICS"), icon: "speaker.wave.2") {
|
|
VStack(spacing: Design.Spacing.small) {
|
|
SettingsToggle(
|
|
title: String(localized: "Sound Effects"),
|
|
subtitle: String(localized: "Chips, cards, and results"),
|
|
isOn: $settings.soundEnabled
|
|
)
|
|
.onChange(of: settings.soundEnabled) { _, newValue in
|
|
SoundManager.shared.soundEnabled = newValue
|
|
}
|
|
|
|
Divider().background(Color.white.opacity(Design.Opacity.hint))
|
|
|
|
SettingsToggle(
|
|
title: String(localized: "Haptic Feedback"),
|
|
subtitle: String(localized: "Vibration on actions"),
|
|
isOn: $settings.hapticsEnabled
|
|
)
|
|
.onChange(of: settings.hapticsEnabled) { _, newValue in
|
|
SoundManager.shared.hapticsEnabled = newValue
|
|
}
|
|
|
|
Divider().background(Color.white.opacity(Design.Opacity.hint))
|
|
|
|
VolumePicker(volume: $settings.soundVolume)
|
|
.onChange(of: settings.soundVolume) { _, newValue in
|
|
SoundManager.shared.volume = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
// Starting Balance
|
|
SheetSection(title: String(localized: "NEW GAME"), icon: "dollarsign.circle") {
|
|
BalancePicker(balance: $settings.startingBalance)
|
|
}
|
|
|
|
// Version info
|
|
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
|
|
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
|
Text(String(localized: "Version \(version) (\(build))"))
|
|
.font(.system(size: Design.BaseFontSize.small))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.light))
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.top, Design.Spacing.large)
|
|
}
|
|
},
|
|
onCancel: nil,
|
|
onDone: {
|
|
settings.save()
|
|
dismiss()
|
|
},
|
|
doneButtonText: String(localized: "Done")
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Game Style Picker
|
|
|
|
struct GameStylePicker: View {
|
|
@Binding var selection: BlackjackStyle
|
|
|
|
var body: some View {
|
|
VStack(spacing: Design.Spacing.small) {
|
|
ForEach(BlackjackStyle.allCases) { style in
|
|
Button {
|
|
selection = style
|
|
} label: {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
|
Text(style.displayName)
|
|
.font(.system(size: Design.BaseFontSize.large, weight: .semibold))
|
|
.foregroundStyle(.white)
|
|
|
|
Text(style.description)
|
|
.font(.system(size: Design.BaseFontSize.body))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
|
.lineLimit(2)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
if selection == style {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.font(.system(size: Design.Size.checkmark))
|
|
.foregroundStyle(Color.Settings.accent)
|
|
} 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 == style ? Color.Settings.accent.opacity(Design.Opacity.subtle) : Color.clear)
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
|
.strokeBorder(
|
|
selection == style ? Color.Settings.accent.opacity(Design.Opacity.medium) : Color.white.opacity(Design.Opacity.subtle),
|
|
lineWidth: Design.LineWidth.thin
|
|
)
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Deck Count Picker
|
|
|
|
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(Color.Settings.accent)
|
|
} 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.Settings.accent.opacity(Design.Opacity.subtle) : Color.clear)
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
|
.strokeBorder(
|
|
selection == count ? Color.Settings.accent.opacity(Design.Opacity.medium) : Color.white.opacity(Design.Opacity.subtle),
|
|
lineWidth: Design.LineWidth.thin
|
|
)
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Table Limits Picker
|
|
|
|
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 pill
|
|
Text(limit.description)
|
|
.font(.system(size: Design.BaseFontSize.body, weight: .bold, design: .rounded))
|
|
.foregroundStyle(selection == limit ? .black : Color.Settings.accent)
|
|
.padding(.horizontal, Design.Spacing.small)
|
|
.padding(.vertical, Design.Spacing.xSmall)
|
|
.background(
|
|
Capsule()
|
|
.fill(selection == limit ? Color.Settings.accent : Color.Settings.accent.opacity(Design.Opacity.hint))
|
|
)
|
|
|
|
if selection == limit {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.font(.system(size: Design.Size.checkmark - 2))
|
|
.foregroundStyle(Color.Settings.accent)
|
|
} 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.Settings.accent.opacity(Design.Opacity.subtle) : Color.clear)
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
|
.strokeBorder(
|
|
selection == limit ? Color.Settings.accent.opacity(Design.Opacity.medium) : Color.white.opacity(Design.Opacity.subtle),
|
|
lineWidth: Design.LineWidth.thin
|
|
)
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
SettingsView(settings: GameSettings(), gameState: nil)
|
|
}
|
|
|