CasinoGames/CasinoKit/Sources/CasinoKit/Views/Chips/ChipSelectorView.swift

122 lines
4.4 KiB
Swift

//
// ChipSelectorView.swift
// CasinoKit
//
// Horizontal chip selector for choosing bet denomination.
//
import SwiftUI
/// A horizontal scrollable chip selector.
/// Shows chips based on current balance - higher denomination chips unlock as you win more!
/// Chips are disabled when:
/// - Balance is less than the chip value
/// - Adding the chip would exceed the max bet (considering current bet)
public struct ChipSelectorView: View {
@Binding var selectedChip: ChipDenomination
let balance: Int
let currentBet: Int
let maxBet: Int
let theme: any CasinoTheme
/// Remaining room before hitting max bet
private var remainingBetRoom: Int {
maxBet - currentBet
}
public init(
selectedChip: Binding<ChipDenomination>,
balance: Int,
currentBet: Int = 0,
maxBet: Int = 100_000,
theme: any CasinoTheme = DefaultCasinoTheme()
) {
self._selectedChip = selectedChip
self.balance = balance
self.currentBet = currentBet
self.maxBet = maxBet
self.theme = theme
}
/// Chips that are unlocked based on current balance.
private var availableChips: [ChipDenomination] {
ChipDenomination.availableChips(forBalance: balance)
.filter { $0.rawValue <= maxBet } // Don't show chips larger than max bet
}
/// Whether a chip can be used (affordable and within bet limit)
private func canUseChip(_ denomination: ChipDenomination) -> Bool {
balance >= denomination.rawValue && denomination.rawValue <= remainingBetRoom
}
public var body: some View {
ScrollView(.horizontal) {
HStack(spacing: CasinoDesign.Spacing.medium) {
ForEach(availableChips) { denomination in
Button {
selectedChip = denomination
} label: {
ChipView(
denomination: denomination,
size: CasinoDesign.Size.chipLarge,
isSelected: selectedChip == denomination,
theme: theme
)
}
.buttonStyle(.plain)
.opacity(canUseChip(denomination) ? 1.0 : CasinoDesign.Opacity.medium)
.disabled(!canUseChip(denomination))
}
}
.padding(.horizontal, CasinoDesign.Spacing.large)
.padding(.vertical, CasinoDesign.Spacing.medium) // Extra padding for selection scale effect
}
.scrollIndicators(.hidden)
.frame(maxWidth: .infinity) // Center the scroll content
.accessibilityElement(children: .contain)
.accessibilityLabel(String(localized: "Chip selector", bundle: .module))
.accessibilityHint(String(localized: "Double tap a chip to select bet amount", bundle: .module))
.onChange(of: balance) { _, newBalance in
// Auto-select highest affordable chip if current selection is now too expensive
autoSelectAffordableChip(forBalance: newBalance)
}
.onChange(of: currentBet) { _, _ in
// Auto-select when remaining bet room changes
autoSelectAffordableChip(forBalance: balance)
}
}
/// Auto-selects the highest affordable chip that fits within remaining bet room
private func autoSelectAffordableChip(forBalance newBalance: Int) {
let newRemainingRoom = maxBet - currentBet
// If current selection is unaffordable or exceeds remaining room, find a better chip
if newBalance < selectedChip.rawValue || selectedChip.rawValue > newRemainingRoom {
if let affordable = availableChips.last(where: {
newBalance >= $0.rawValue && $0.rawValue <= newRemainingRoom
}) {
selectedChip = affordable
}
}
}
}
#Preview {
ZStack {
Color(red: 0.05, green: 0.35, blue: 0.15)
.ignoresSafeArea()
VStack(spacing: CasinoDesign.Spacing.xLarge) {
Text("Balance: $50,000 | Bet: $1,000 | Max: $5,000")
.foregroundStyle(.white)
ChipSelectorView(
selectedChip: .constant(.fiveHundred),
balance: 50_000,
currentBet: 1_000,
maxBet: 5_000
)
}
}
}