From 172e224a427bf695323f77a22a7eca5b1d42da10 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sun, 28 Dec 2025 18:01:37 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- .../Views/Game/ActionButtonsView.swift | 115 ++++++++++++------ 1 file changed, 75 insertions(+), 40 deletions(-) diff --git a/Blackjack/Blackjack/Views/Game/ActionButtonsView.swift b/Blackjack/Blackjack/Views/Game/ActionButtonsView.swift index ae4aa35..665422c 100644 --- a/Blackjack/Blackjack/Views/Game/ActionButtonsView.swift +++ b/Blackjack/Blackjack/Views/Game/ActionButtonsView.swift @@ -21,8 +21,10 @@ struct ActionButtonsView: View { switch state.currentPhase { case .betting: bettingButtons + .transition(.opacity.combined(with: .scale(scale: 0.9))) case .playerTurn: playerTurnButtons + .transition(.opacity.combined(with: .scale(scale: 0.9))) case .roundComplete: // Empty - handled by result banner EmptyView() @@ -31,6 +33,7 @@ struct ActionButtonsView: View { EmptyView() } } + .animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase) } .frame(height: containerHeight) .padding(.horizontal, Design.Spacing.large) @@ -51,56 +54,88 @@ struct ActionButtonsView: View { // MARK: - Player Turn Buttons + /// Available player actions based on current game state. + private var availableActions: [PlayerAction] { + PlayerAction.allCases.filter { action in + switch action { + case .hit: state.canHit + case .stand: state.canStand + case .double: state.canDouble + case .split: state.canSplit + case .surrender: state.canSurrender + } + } + } + + /// Tracks available actions for animation purposes. + @State private var animatedActions: [PlayerAction] = [] + @ViewBuilder private var playerTurnButtons: some View { - // All player actions in a single row HStack(spacing: Design.Spacing.medium) { - if state.canHit { + ForEach(animatedActions) { action in ActionButton( - String(localized: "Hit"), - style: .custom(Color.Button.hit) + action.title, + style: .custom(action.color) ) { - Task { await state.hit() } + Task { await performAction(action) } } + .transition(.scale.combined(with: .opacity)) } - - 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() } - } + } + .onAppear { + animatedActions = availableActions + } + .onChange(of: availableActions) { _, newActions in + withAnimation(.spring(duration: Design.Animation.springDuration, bounce: 0.15)) { + animatedActions = newActions } } } + + /// Performs the given player action. + private func performAction(_ action: PlayerAction) async { + switch action { + case .hit: await state.hit() + case .stand: await state.stand() + case .double: await state.doubleDown() + case .split: await state.split() + case .surrender: await state.surrender() + } + } +} + +// MARK: - Player Action + +/// Represents a player action button during their turn. +private enum PlayerAction: String, CaseIterable, Identifiable { + case hit + case stand + case double + case split + case surrender + + var id: String { rawValue } + + var title: String { + switch self { + case .hit: String(localized: "Hit") + case .stand: String(localized: "Stand") + case .double: String(localized: "Double") + case .split: String(localized: "Split") + case .surrender: String(localized: "Surrender") + } + } + + var color: Color { + switch self { + case .hit: Color.Button.hit + case .stand: Color.Button.stand + case .double: Color.Button.doubleDown + case .split: Color.Button.split + case .surrender: Color.Button.surrender + } + } } // MARK: - Previews