194 lines
6.9 KiB
Swift
194 lines
6.9 KiB
Swift
//
|
|
// BettingZoneView.swift
|
|
// Blackjack
|
|
//
|
|
// The betting area where players place their bets.
|
|
//
|
|
|
|
import SwiftUI
|
|
import CasinoKit
|
|
|
|
/// Main betting zone view - adapts layout based on whether side bets are enabled.
|
|
/// Follows Baccarat's betting pattern exactly.
|
|
struct BettingZoneView: View {
|
|
@Bindable var state: GameState
|
|
let selectedChip: ChipDenomination
|
|
|
|
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
|
|
@ScaledMetric(relativeTo: .caption) private var detailFontSize: CGFloat = Design.Size.handNumberFontSize
|
|
@ScaledMetric(relativeTo: .body) private var chipSize: CGFloat = Design.Size.bettingChipSize
|
|
@ScaledMetric(relativeTo: .body) private var zoneHeight: CGFloat = CasinoDesign.Size.bettingZoneHeight
|
|
|
|
// MARK: - Computed Properties (matches Baccarat's pattern)
|
|
|
|
/// Whether a bet can be added to main bet (matches Baccarat's canAddBet)
|
|
private var canAddMainBet: Bool {
|
|
state.canPlaceBet &&
|
|
state.balance >= selectedChip.rawValue &&
|
|
state.canAddToMainBet(amount: selectedChip.rawValue)
|
|
}
|
|
|
|
/// Whether a bet can be added to Perfect Pairs
|
|
private var canAddPerfectPairs: Bool {
|
|
state.canPlaceBet &&
|
|
state.balance >= selectedChip.rawValue &&
|
|
state.canAddToSideBet(type: .perfectPairs, amount: selectedChip.rawValue)
|
|
}
|
|
|
|
/// Whether a bet can be added to 21+3
|
|
private var canAddTwentyOnePlusThree: Bool {
|
|
state.canPlaceBet &&
|
|
state.balance >= selectedChip.rawValue &&
|
|
state.canAddToSideBet(type: .twentyOnePlusThree, amount: selectedChip.rawValue)
|
|
}
|
|
|
|
private var isMainAtMax: Bool {
|
|
state.currentBet >= state.settings.maxBet
|
|
}
|
|
|
|
private var isPPAtMax: Bool {
|
|
state.perfectPairsBet >= state.settings.maxBet
|
|
}
|
|
|
|
private var is21PlusThreeAtMax: Bool {
|
|
state.twentyOnePlusThreeBet >= state.settings.maxBet
|
|
}
|
|
|
|
var body: some View {
|
|
if state.settings.sideBetsEnabled {
|
|
// Horizontal layout: PP | Main Bet | 21+3
|
|
HStack(spacing: Design.Spacing.small) {
|
|
// Perfect Pairs
|
|
SideBetZoneView(
|
|
betType: .perfectPairs,
|
|
betAmount: state.perfectPairsBet,
|
|
isEnabled: canAddPerfectPairs,
|
|
isAtMax: isPPAtMax,
|
|
onTap: { state.placeSideBet(type: .perfectPairs, amount: selectedChip.rawValue) }
|
|
)
|
|
.frame(width: Design.Size.sideBetZoneWidth)
|
|
|
|
// Main bet (center, takes remaining space)
|
|
mainBetZone
|
|
|
|
// 21+3
|
|
SideBetZoneView(
|
|
betType: .twentyOnePlusThree,
|
|
betAmount: state.twentyOnePlusThreeBet,
|
|
isEnabled: canAddTwentyOnePlusThree,
|
|
isAtMax: is21PlusThreeAtMax,
|
|
onTap: { state.placeSideBet(type: .twentyOnePlusThree, amount: selectedChip.rawValue) }
|
|
)
|
|
.frame(width: Design.Size.sideBetZoneWidth)
|
|
}
|
|
.frame(height: zoneHeight)
|
|
} else {
|
|
// Simple layout: just main bet
|
|
mainBetZone
|
|
.frame(height: zoneHeight)
|
|
}
|
|
}
|
|
|
|
private var mainBetZone: some View {
|
|
Button {
|
|
if canAddMainBet {
|
|
state.placeBet(amount: selectedChip.rawValue)
|
|
}
|
|
} label: {
|
|
ZStack {
|
|
// Background
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
|
.fill(Color.BettingZone.main)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
|
.strokeBorder(Color.BettingZone.mainBorder, lineWidth: Design.LineWidth.medium)
|
|
)
|
|
|
|
// Content
|
|
if state.currentBet > 0 {
|
|
// Show chip with amount (scaled)
|
|
ChipOnTableView(amount: state.currentBet, showMax: isMainAtMax, size: chipSize)
|
|
} else {
|
|
// Empty state
|
|
VStack(spacing: Design.Spacing.small) {
|
|
Text(String(localized: "TAP TO BET"))
|
|
.font(.system(size: labelFontSize, weight: .bold))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
|
|
|
HStack(spacing: Design.Spacing.medium) {
|
|
Text(String(localized: "Min: $\(state.settings.minBet)"))
|
|
.font(.system(size: detailFontSize, weight: .medium))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.light))
|
|
|
|
Text(String(localized: "Max: $\(state.settings.maxBet.formatted())"))
|
|
.font(.system(size: detailFontSize, weight: .medium))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.light))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.accessibilityLabel(state.currentBet > 0 ? "$\(state.currentBet) bet" + (isMainAtMax ? ", maximum" : "") : "Place bet")
|
|
.accessibilityHint("Double tap to add chips")
|
|
}
|
|
}
|
|
|
|
// MARK: - Design Constants Extension
|
|
|
|
extension Design.Size {
|
|
/// Width of side bet zones
|
|
static let sideBetZoneWidth: CGFloat = 70
|
|
}
|
|
|
|
// MARK: - Previews
|
|
|
|
#Preview("Empty - No Side Bets") {
|
|
ZStack {
|
|
Color.Table.felt.ignoresSafeArea()
|
|
BettingZoneView(
|
|
state: GameState(settings: GameSettings()),
|
|
selectedChip: .hundred
|
|
)
|
|
.padding()
|
|
}
|
|
}
|
|
|
|
#Preview("With Side Bets") {
|
|
ZStack {
|
|
Color.Table.felt.ignoresSafeArea()
|
|
BettingZoneView(
|
|
state: {
|
|
let settings = GameSettings()
|
|
settings.sideBetsEnabled = true
|
|
let state = GameState(settings: settings)
|
|
state.placeBet(amount: 100)
|
|
return state
|
|
}(),
|
|
selectedChip: .twentyFive
|
|
)
|
|
.padding()
|
|
}
|
|
}
|
|
|
|
#Preview("All Bets Placed") {
|
|
ZStack {
|
|
Color.Table.felt.ignoresSafeArea()
|
|
BettingZoneView(
|
|
state: {
|
|
let settings = GameSettings()
|
|
settings.sideBetsEnabled = true
|
|
let state = GameState(settings: settings)
|
|
state.placeBet(amount: 250)
|
|
state.placeSideBet(type: .perfectPairs, amount: 25)
|
|
state.placeSideBet(type: .twentyOnePlusThree, amount: 50)
|
|
return state
|
|
}(),
|
|
selectedChip: .hundred
|
|
)
|
|
.padding()
|
|
}
|
|
}
|
|
|