CasinoGames/Blackjack/Views/Game/ActionButtonsView.swift

301 lines
8.6 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
@ViewBuilder
private var bettingButtons: some View {
if state.currentBet > 0 {
VStack(spacing: Design.Spacing.small) {
// Show hint if bet is below minimum
if state.isBetBelowMinimum {
Text(String(localized: "Add $\(state.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
)
}
}