122 lines
4.4 KiB
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
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|