311 lines
8.9 KiB
Swift
311 lines
8.9 KiB
Swift
//
|
|
// ActionButtonsView.swift
|
|
// Blackjack
|
|
//
|
|
// Container for game action buttons (betting, player turn).
|
|
//
|
|
|
|
import SwiftUI
|
|
import CasinoKit
|
|
|
|
struct ActionButtonsView: View {
|
|
@Bindable var state: GameState
|
|
|
|
// Scaled metrics
|
|
@ScaledMetric(relativeTo: .headline) private var buttonFontSize: CGFloat = Design.BaseFontSize.large
|
|
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.IconSize.large
|
|
|
|
// Scaled container height - base 60pt, scales with accessibility
|
|
@ScaledMetric(relativeTo: .body) private var containerHeight: CGFloat = 60
|
|
|
|
var body: some View {
|
|
VStack(spacing: Design.Spacing.small) {
|
|
// Primary actions
|
|
HStack(spacing: Design.Spacing.medium) {
|
|
switch state.currentPhase {
|
|
case .betting:
|
|
bettingButtons
|
|
case .playerTurn:
|
|
playerTurnButtons
|
|
case .roundComplete:
|
|
// Empty - handled by result banner
|
|
EmptyView()
|
|
default:
|
|
// Dealing, dealer turn - show nothing
|
|
EmptyView()
|
|
}
|
|
}
|
|
.animation(.spring(duration: Design.Animation.quick), value: state.currentPhase)
|
|
}
|
|
.frame(minHeight: containerHeight)
|
|
.padding(.horizontal, Design.Spacing.large)
|
|
}
|
|
|
|
// MARK: - Betting Phase Buttons
|
|
|
|
/// Whether the current bet meets the minimum requirement
|
|
private var isBetBelowMinimum: Bool {
|
|
state.currentBet > 0 && state.currentBet < state.settings.minBet
|
|
}
|
|
|
|
/// Amount needed to reach minimum bet
|
|
private var amountNeededForMinimum: Int {
|
|
state.settings.minBet - state.currentBet
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var bettingButtons: some View {
|
|
if state.currentBet > 0 {
|
|
VStack(spacing: Design.Spacing.small) {
|
|
// Show hint if bet is below minimum
|
|
if isBetBelowMinimum {
|
|
Text(String(localized: "Add $\(amountNeededForMinimum) more to meet minimum"))
|
|
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
|
|
.foregroundStyle(.orange)
|
|
.transition(.opacity)
|
|
}
|
|
|
|
HStack(spacing: Design.Spacing.medium) {
|
|
ActionButton(
|
|
String(localized: "Clear"),
|
|
icon: "xmark.circle",
|
|
style: .destructive
|
|
) {
|
|
state.clearBet()
|
|
}
|
|
|
|
// Always show Deal button, but disable if below minimum
|
|
ActionButton(
|
|
String(localized: "Deal"),
|
|
icon: "play.fill",
|
|
style: .primary
|
|
) {
|
|
Task { await state.deal() }
|
|
}
|
|
.opacity(state.canDeal ? 1.0 : Design.Opacity.medium)
|
|
.disabled(!state.canDeal)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Player Turn Buttons
|
|
|
|
@ViewBuilder
|
|
private var playerTurnButtons: some View {
|
|
// All player actions in a single row
|
|
HStack(spacing: Design.Spacing.medium) {
|
|
if state.canHit {
|
|
ActionButton(
|
|
String(localized: "Hit"),
|
|
style: .custom(Color.Button.hit)
|
|
) {
|
|
Task { await state.hit() }
|
|
}
|
|
}
|
|
|
|
if state.canStand {
|
|
ActionButton(
|
|
String(localized: "Stand"),
|
|
style: .custom(Color.Button.stand)
|
|
) {
|
|
Task { await state.stand() }
|
|
}
|
|
}
|
|
|
|
if state.canDouble {
|
|
ActionButton(
|
|
String(localized: "Double"),
|
|
style: .custom(Color.Button.doubleDown)
|
|
) {
|
|
Task { await state.doubleDown() }
|
|
}
|
|
}
|
|
|
|
if state.canSplit {
|
|
ActionButton(
|
|
String(localized: "Split"),
|
|
style: .custom(Color.Button.split)
|
|
) {
|
|
Task { await state.split() }
|
|
}
|
|
}
|
|
|
|
if state.canSurrender {
|
|
ActionButton(
|
|
String(localized: "Surrender"),
|
|
style: .custom(Color.Button.surrender)
|
|
) {
|
|
Task { await state.surrender() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Previews
|
|
|
|
#Preview("Betting Phase - No Bet") {
|
|
ZStack {
|
|
Color.Table.felt.ignoresSafeArea()
|
|
ActionButtonsView(state: {
|
|
let state = GameState(settings: GameSettings())
|
|
return state
|
|
}())
|
|
}
|
|
}
|
|
|
|
#Preview("Betting Phase - With Bet") {
|
|
ZStack {
|
|
Color.Table.felt.ignoresSafeArea()
|
|
ActionButtonsView(state: {
|
|
let state = GameState(settings: GameSettings())
|
|
state.placeBet(amount: 100)
|
|
return state
|
|
}())
|
|
}
|
|
}
|
|
|
|
#Preview("Betting Phase - Below Minimum") {
|
|
ZStack {
|
|
Color.Table.felt.ignoresSafeArea()
|
|
ActionButtonsView(state: {
|
|
let state = GameState(settings: GameSettings())
|
|
state.placeBet(amount: 25)
|
|
return state
|
|
}())
|
|
}
|
|
}
|
|
|
|
// MARK: - Player Turn Button Previews
|
|
|
|
/// Preview helper that shows player turn button layouts without needing real game state.
|
|
private struct PlayerTurnButtonsPreview: View {
|
|
let showHit: Bool
|
|
let showStand: Bool
|
|
let showDouble: Bool
|
|
let showSplit: Bool
|
|
let showSurrender: Bool
|
|
|
|
private let containerHeight: CGFloat = 120
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Color.clear
|
|
.frame(height: containerHeight)
|
|
|
|
// Single row of buttons matching actual game layout
|
|
HStack(spacing: Design.Spacing.medium) {
|
|
if showHit {
|
|
ActionButton("Hit", style: .custom(Color.Button.hit)) {}
|
|
}
|
|
if showStand {
|
|
ActionButton("Stand", style: .custom(Color.Button.stand)) {}
|
|
}
|
|
if showDouble {
|
|
ActionButton("Double", style: .custom(Color.Button.doubleDown)) {}
|
|
}
|
|
if showSplit {
|
|
ActionButton("Split", style: .custom(Color.Button.split)) {}
|
|
}
|
|
if showSurrender {
|
|
ActionButton("Surrender", style: .custom(Color.Button.surrender)) {}
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, Design.Spacing.large)
|
|
}
|
|
}
|
|
|
|
#Preview("Player Turn - Hit & Stand Only") {
|
|
ZStack {
|
|
Color.Table.felt.ignoresSafeArea()
|
|
PlayerTurnButtonsPreview(
|
|
showHit: true,
|
|
showStand: true,
|
|
showDouble: false,
|
|
showSplit: false,
|
|
showSurrender: false
|
|
)
|
|
}
|
|
}
|
|
|
|
#Preview("Player Turn - With Double") {
|
|
ZStack {
|
|
Color.Table.felt.ignoresSafeArea()
|
|
PlayerTurnButtonsPreview(
|
|
showHit: true,
|
|
showStand: true,
|
|
showDouble: true,
|
|
showSplit: false,
|
|
showSurrender: false
|
|
)
|
|
}
|
|
}
|
|
|
|
#Preview("Player Turn - With Split") {
|
|
ZStack {
|
|
Color.Table.felt.ignoresSafeArea()
|
|
PlayerTurnButtonsPreview(
|
|
showHit: true,
|
|
showStand: true,
|
|
showDouble: true,
|
|
showSplit: true,
|
|
showSurrender: false
|
|
)
|
|
}
|
|
}
|
|
|
|
#Preview("Player Turn - With Surrender") {
|
|
ZStack {
|
|
Color.Table.felt.ignoresSafeArea()
|
|
PlayerTurnButtonsPreview(
|
|
showHit: true,
|
|
showStand: true,
|
|
showDouble: true,
|
|
showSplit: false,
|
|
showSurrender: true
|
|
)
|
|
}
|
|
}
|
|
|
|
#Preview("Player Turn - All Options") {
|
|
ZStack {
|
|
Color.Table.felt.ignoresSafeArea()
|
|
PlayerTurnButtonsPreview(
|
|
showHit: true,
|
|
showStand: true,
|
|
showDouble: true,
|
|
showSplit: true,
|
|
showSurrender: true
|
|
)
|
|
}
|
|
}
|
|
|
|
#Preview("Player Turn - After Hit (No Double/Split)") {
|
|
ZStack {
|
|
Color.Table.felt.ignoresSafeArea()
|
|
PlayerTurnButtonsPreview(
|
|
showHit: true,
|
|
showStand: true,
|
|
showDouble: false,
|
|
showSplit: false,
|
|
showSurrender: false
|
|
)
|
|
}
|
|
}
|
|
|
|
#Preview("Player Turn - Split Hand (No Resplit)") {
|
|
ZStack {
|
|
Color.Table.felt.ignoresSafeArea()
|
|
PlayerTurnButtonsPreview(
|
|
showHit: true,
|
|
showStand: true,
|
|
showDouble: true,
|
|
showSplit: false,
|
|
showSurrender: false
|
|
)
|
|
}
|
|
}
|
|
|