CasinoGames/Blackjack/Blackjack/Views/Table/PlayerHandsContainer.swift

149 lines
5.3 KiB
Swift

//
// PlayerHandsContainer.swift
// Blackjack
//
// Scrollable container for player hands (supports split hands).
//
import SwiftUI
import CasinoKit
/// Horizontally scrollable container that displays one or more player hands.
/// Handles split hands by showing them side-by-side with auto-scrolling to the active hand.
struct PlayerHandsContainer: View {
let hands: [BlackjackHand]
let activeHandIndex: Int
let isPlayerTurn: Bool
let showCardCount: Bool
let showAnimations: Bool
let dealingSpeed: Double
let cardWidth: CGFloat
let cardSpacing: CGFloat
/// Number of visible cards for each hand (completed animations)
let visibleCardCounts: [Int]
/// Current hint to display (shown on active hand only).
let currentHint: String?
/// Whether the hint toast should be visible.
let showHintToast: Bool
/// Total card count across all hands - used to trigger scroll when hitting
private var totalCardCount: Int {
hands.reduce(0) { $0 + $1.cards.count }
}
var body: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Design.Spacing.large) {
// Display hands in reverse order (right to left play order)
// Visual order: Hand 3, Hand 2, Hand 1 (left to right)
// Play order: Hand 1 played first (rightmost), then Hand 2, etc.
ForEach(Array(hands.enumerated()).reversed(), id: \.element.id) { index, hand in
let isActiveHand = index == activeHandIndex && isPlayerTurn
let visibleCount = index < visibleCardCounts.count ? visibleCardCounts[index] : 0
PlayerHandView(
hand: hand,
isActive: isActiveHand,
showCardCount: showCardCount,
showAnimations: showAnimations,
dealingSpeed: dealingSpeed,
// Hand numbers: rightmost (index 0) is Hand 1, played first
handNumber: hands.count > 1 ? index + 1 : nil,
cardWidth: cardWidth,
cardSpacing: cardSpacing,
visibleCardCount: visibleCount,
// Only show hint on the active hand
currentHint: isActiveHand ? currentHint : nil,
showHintToast: isActiveHand && showHintToast
)
.id(hand.id)
.transition(.scale.combined(with: .opacity))
}
}
.animation(.spring(duration: Design.Animation.springDuration), value: hands.count)
.padding(.horizontal, Design.Spacing.xxLarge)
}
.scrollClipDisabled()
.scrollBounceBehavior(.always)
.defaultScrollAnchor(.center)
.onChange(of: activeHandIndex) { _, _ in
scrollToActiveHand(proxy: proxy)
}
.onChange(of: totalCardCount) { _, _ in
scrollToActiveHand(proxy: proxy)
}
.onChange(of: hands.count) { _, _ in
scrollToActiveHand(proxy: proxy)
}
.onAppear {
scrollToActiveHand(proxy: proxy)
}
}
.frame(maxWidth: .infinity)
}
private func scrollToActiveHand(proxy: ScrollViewProxy) {
guard activeHandIndex < hands.count else { return }
let activeHandId = hands[activeHandIndex].id
withAnimation(.easeInOut(duration: Design.Animation.quick)) {
proxy.scrollTo(activeHandId, anchor: .center)
}
}
}
// MARK: - Previews
#Preview("Single Hand") {
ZStack {
Color.Table.felt.ignoresSafeArea()
PlayerHandsContainer(
hands: [BlackjackHand(cards: [
Card(suit: .hearts, rank: .ace),
Card(suit: .spades, rank: .king)
], bet: 100)],
activeHandIndex: 0,
isPlayerTurn: true,
showCardCount: false,
showAnimations: true,
dealingSpeed: 1.0,
cardWidth: 60,
cardSpacing: -20,
visibleCardCounts: [2],
currentHint: "Stand",
showHintToast: true
)
}
}
#Preview("Split Hands") {
ZStack {
Color.Table.felt.ignoresSafeArea()
PlayerHandsContainer(
hands: [
BlackjackHand(cards: [
Card(suit: .clubs, rank: .eight),
Card(suit: .spades, rank: .jack)
], bet: 100),
BlackjackHand(cards: [
Card(suit: .hearts, rank: .eight),
Card(suit: .diamonds, rank: .five)
], bet: 100)
],
activeHandIndex: 1,
isPlayerTurn: true,
showCardCount: true,
showAnimations: true,
dealingSpeed: 1.0,
cardWidth: 60,
cardSpacing: -20,
visibleCardCounts: [2, 2],
currentHint: "Hit",
showHintToast: true
)
}
}