Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2025-12-22 09:33:41 -06:00
parent 644127ae40
commit 50f569d137
10 changed files with 164 additions and 74 deletions

View File

@ -719,6 +719,28 @@
} }
} }
}, },
"Add $%lld more to meet minimum" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Add $%lld more to meet minimum"
}
},
"es-MX" : {
"stringUnit" : {
"state" : "translated",
"value" : "Añade $%lld más para alcanzar el mínimo"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ajoutez %lld$ de plus pour atteindre le minimum"
}
}
}
},
"Clear" : { "Clear" : {
"localizations" : { "localizations" : {
"en" : { "en" : {

View File

@ -31,10 +31,44 @@ enum Design {
// MARK: - Blackjack-Specific Component Sizes // MARK: - Blackjack-Specific Component Sizes
enum Size { enum Size {
// Cards - slightly larger than medium for better visibility // Hand scaling factor (1.5 = 50% larger hands)
static let cardWidth: CGFloat = 60 static let handScale: CGFloat = 1.5
// Cards - scaled for better visibility
static let cardWidth: CGFloat = 60 * handScale // 90pt at 1.5x
static let cardWidthSmall: CGFloat = CasinoDesign.Size.cardWidthSmall static let cardWidthSmall: CGFloat = CasinoDesign.Size.cardWidthSmall
static let cardOverlap: CGFloat = CasinoDesign.Size.cardOverlap static let cardOverlap: CGFloat = CasinoDesign.Size.cardOverlap * handScale // Scaled overlap
// Player hands container height (accommodates larger cards)
static let playerHandsHeight: CGFloat = 180 * handScale // 270pt at 1.5x
// Hand label font sizes (scaled)
static let handLabelFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * handScale
static let handNumberFontSize: CGFloat = CasinoDesign.BaseFontSize.small * handScale
static let handValueFontSize: CGFloat = CasinoDesign.BaseFontSize.xLarge * handScale
// Hint font size (scaled to match hands)
static let hintFontSize: CGFloat = CasinoDesign.BaseFontSize.small * handScale
static let hintIconSize: CGFloat = CasinoDesign.IconSize.medium * handScale
static let hintPaddingH: CGFloat = CasinoDesign.Spacing.medium * handScale
static let hintPaddingV: CGFloat = CasinoDesign.Spacing.small * handScale
// Hand icons (scaled)
static let handIconSize: CGFloat = CasinoDesign.IconSize.medium * handScale
// Hi-Lo count badge (scaled)
static let countBadgeFontSize: CGFloat = CasinoDesign.BaseFontSize.xxSmall * handScale
static let countBadgePaddingH: CGFloat = CasinoDesign.Spacing.xSmall * handScale
static let countBadgePaddingV: CGFloat = CasinoDesign.Spacing.xxxSmall * handScale
static let countBadgeOffset: CGFloat = CasinoDesign.Spacing.xSmall * handScale
// Betting zone (chip scales, but zone height stays reasonable)
static let bettingChipSize: CGFloat = 36 * handScale // 54pt at 1.5x
static let bettingZoneHeightScaled: CGFloat = CasinoDesign.Size.bettingZoneHeight // Keep original height to save space
// Card count display (scaled)
static let cardCountLabelSize: CGFloat = CasinoDesign.BaseFontSize.xSmall * handScale
static let cardCountValueSize: CGFloat = CasinoDesign.BaseFontSize.large * handScale
// Chips - use CasinoDesign values // Chips - use CasinoDesign values
static let chipBadgeSize: CGFloat = CasinoDesign.Size.chipBadge static let chipBadgeSize: CGFloat = CasinoDesign.Size.chipBadge

View File

@ -14,7 +14,7 @@ struct BettingZoneView: View {
let maxBet: Int let maxBet: Int
let onTap: () -> Void let onTap: () -> Void
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.BaseFontSize.large @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
private var isAtMax: Bool { private var isAtMax: Bool {
betAmount >= maxBet betAmount >= maxBet
@ -33,8 +33,8 @@ struct BettingZoneView: View {
// Content // Content
if betAmount > 0 { if betAmount > 0 {
// Show chip with amount // Show chip with amount (scaled)
ChipOnTableView(amount: betAmount, showMax: isAtMax) ChipOnTableView(amount: betAmount, showMax: isAtMax, size: Design.Size.bettingChipSize)
} else { } else {
// Empty state // Empty state
VStack(spacing: Design.Spacing.small) { VStack(spacing: Design.Spacing.small) {
@ -44,18 +44,18 @@ struct BettingZoneView: View {
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
Text(String(localized: "Min: $\(minBet)")) Text(String(localized: "Min: $\(minBet)"))
.font(.system(size: Design.BaseFontSize.small, weight: .medium)) .font(.system(size: Design.Size.handNumberFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.light)) .foregroundStyle(.white.opacity(Design.Opacity.light))
Text(String(localized: "Max: $\(maxBet.formatted())")) Text(String(localized: "Max: $\(maxBet.formatted())"))
.font(.system(size: Design.BaseFontSize.small, weight: .medium)) .font(.system(size: Design.Size.handNumberFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.light)) .foregroundStyle(.white.opacity(Design.Opacity.light))
} }
} }
} }
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.frame(height: Design.Size.bettingZoneHeight) .frame(height: Design.Size.bettingZoneHeightScaled)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel(betAmount > 0 ? "$\(betAmount) bet" + (isAtMax ? ", maximum" : "") : "Place bet") .accessibilityLabel(betAmount > 0 ? "$\(betAmount) bet" + (isAtMax ? ", maximum" : "") : "Place bet")

View File

@ -39,7 +39,8 @@ struct BlackjackTableView: View {
Spacer() Spacer()
// Player hands area // Player hands area - only show when there are cards dealt
if state.playerHands.first?.cards.isEmpty == false {
PlayerHandsView( PlayerHandsView(
hands: state.playerHands, hands: state.playerHands,
activeHandIndex: state.activeHandIndex, activeHandIndex: state.activeHandIndex,
@ -48,6 +49,8 @@ struct BlackjackTableView: View {
cardWidth: cardWidth, cardWidth: cardWidth,
cardSpacing: cardSpacing cardSpacing: cardSpacing
) )
.transition(.opacity)
}
// Betting zone (when betting) // Betting zone (when betting)
if state.currentPhase == .betting { if state.currentPhase == .betting {

View File

@ -15,7 +15,7 @@ struct DealerHandView: View {
let cardWidth: CGFloat let cardWidth: CGFloat
let cardSpacing: CGFloat let cardSpacing: CGFloat
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.BaseFontSize.medium @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
var body: some View { var body: some View {
VStack(spacing: Design.Spacing.small) { VStack(spacing: Design.Spacing.small) {

View File

@ -111,18 +111,18 @@ struct GameTableView: View {
Spacer() Spacer()
// Chip selector - only interactive during betting phase // Chip selector - only shown during betting phase
// During gameplay, show chips as they were (balance + currentBet) but dimmed if state.currentPhase == .betting {
ChipSelectorView( ChipSelectorView(
selectedChip: $selectedChip, selectedChip: $selectedChip,
balance: state.currentPhase == .betting ? state.balance : (state.balance + state.currentBet), balance: state.balance,
currentBet: state.currentPhase == .betting ? state.currentBet : 0, currentBet: state.currentBet,
maxBet: state.settings.maxBet maxBet: state.settings.maxBet
) )
.frame(maxWidth: maxContentWidth) .frame(maxWidth: maxContentWidth)
.padding(.bottom, Design.Spacing.small) .padding(.bottom, Design.Spacing.small)
.opacity(state.currentPhase == .betting ? 1.0 : Design.Opacity.medium) .transition(.opacity.combined(with: .move(edge: .bottom)))
.allowsHitTesting(state.currentPhase == .betting) // Disable interaction when not betting }
// Action buttons // Action buttons
ActionButtonsView(state: state) ActionButtonsView(state: state)
@ -216,9 +216,29 @@ struct ActionButtonsView: View {
// MARK: - Betting Phase Buttons // 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 @ViewBuilder
private var bettingButtons: some View { private var bettingButtons: some View {
if state.currentBet > 0 { 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( ActionButton(
String(localized: "Clear"), String(localized: "Clear"),
icon: "xmark.circle", icon: "xmark.circle",
@ -227,7 +247,7 @@ struct ActionButtonsView: View {
state.clearBet() state.clearBet()
} }
if state.canDeal { // Always show Deal button, but disable if below minimum
ActionButton( ActionButton(
String(localized: "Deal"), String(localized: "Deal"),
icon: "play.fill", icon: "play.fill",
@ -235,6 +255,9 @@ struct ActionButtonsView: View {
) { ) {
Task { await state.deal() } Task { await state.deal() }
} }
.opacity(state.canDeal ? 1.0 : Design.Opacity.medium)
.disabled(!state.canDeal)
}
} }
} }
} }
@ -379,11 +402,11 @@ struct CardCountView: View {
// Running count // Running count
VStack(spacing: Design.Spacing.xxSmall) { VStack(spacing: Design.Spacing.xxSmall) {
Text("Running") Text("Running")
.font(.system(size: Design.BaseFontSize.xSmall, weight: .medium)) .font(.system(size: Design.Size.cardCountLabelSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium))
Text(runningCount >= 0 ? "+\(runningCount)" : "\(runningCount)") Text(runningCount >= 0 ? "+\(runningCount)" : "\(runningCount)")
.font(.system(size: Design.BaseFontSize.large, weight: .bold, design: .monospaced)) .font(.system(size: Design.Size.cardCountValueSize, weight: .bold, design: .monospaced))
.foregroundStyle(countColor(for: runningCount)) .foregroundStyle(countColor(for: runningCount))
} }
@ -394,11 +417,11 @@ struct CardCountView: View {
// True count // True count
VStack(spacing: Design.Spacing.xxSmall) { VStack(spacing: Design.Spacing.xxSmall) {
Text("True") Text("True")
.font(.system(size: Design.BaseFontSize.xSmall, weight: .medium)) .font(.system(size: Design.Size.cardCountLabelSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium))
Text(trueCount >= 0 ? "+\(trueCount, format: .number.precision(.fractionLength(1)))" : "\(trueCount, format: .number.precision(.fractionLength(1)))") Text(trueCount >= 0 ? "+\(trueCount, format: .number.precision(.fractionLength(1)))" : "\(trueCount, format: .number.precision(.fractionLength(1)))")
.font(.system(size: Design.BaseFontSize.large, weight: .bold, design: .monospaced)) .font(.system(size: Design.Size.cardCountValueSize, weight: .bold, design: .monospaced))
.foregroundStyle(countColor(for: Int(trueCount.rounded()))) .foregroundStyle(countColor(for: Int(trueCount.rounded())))
} }
} }

View File

@ -14,15 +14,15 @@ struct HiLoCountBadge: View {
var body: some View { var body: some View {
Text(card.hiLoDisplayText) Text(card.hiLoDisplayText)
.font(.system(size: Design.BaseFontSize.xxSmall, weight: .bold, design: .rounded)) .font(.system(size: Design.Size.countBadgeFontSize, weight: .bold, design: .rounded))
.foregroundStyle(badgeTextColor) .foregroundStyle(badgeTextColor)
.padding(.horizontal, Design.Spacing.xSmall) .padding(.horizontal, Design.Size.countBadgePaddingH)
.padding(.vertical, Design.Spacing.xxxSmall) .padding(.vertical, Design.Size.countBadgePaddingV)
.background( .background(
Capsule() Capsule()
.fill(badgeBackgroundColor) .fill(badgeBackgroundColor)
) )
.offset(x: -Design.Spacing.xSmall, y: Design.Spacing.xSmall) .offset(x: -Design.Size.countBadgeOffset, y: Design.Size.countBadgeOffset)
} }
private var badgeBackgroundColor: Color { private var badgeBackgroundColor: Color {

View File

@ -17,13 +17,14 @@ struct HintView: View {
var body: some View { var body: some View {
HStack(spacing: Design.Spacing.small) { HStack(spacing: Design.Spacing.small) {
Image(systemName: "lightbulb.fill") Image(systemName: "lightbulb.fill")
.font(.system(size: Design.Size.hintIconSize))
.foregroundStyle(.yellow) .foregroundStyle(.yellow)
Text(String(localized: "Hint: \(hint)")) Text(String(localized: "Hint: \(hint)"))
.font(.system(size: Design.BaseFontSize.small, weight: .medium)) .font(.system(size: Design.Size.hintFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.strong)) .foregroundStyle(.white.opacity(Design.Opacity.strong))
} }
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Size.hintPaddingH)
.padding(.vertical, Design.Spacing.small) .padding(.vertical, Design.Size.hintPaddingV)
.background( .background(
Capsule() Capsule()
.fill(Color.black.opacity(Design.Opacity.light)) .fill(Color.black.opacity(Design.Opacity.light))
@ -66,13 +67,14 @@ struct BettingHintView: View {
var body: some View { var body: some View {
HStack(spacing: Design.Spacing.small) { HStack(spacing: Design.Spacing.small) {
Image(systemName: icon) Image(systemName: icon)
.font(.system(size: Design.Size.hintIconSize))
.foregroundStyle(hintColor) .foregroundStyle(hintColor)
Text(hint) Text(hint)
.font(.system(size: Design.BaseFontSize.small, weight: .medium)) .font(.system(size: Design.Size.hintFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.strong)) .foregroundStyle(.white.opacity(Design.Opacity.strong))
} }
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Size.hintPaddingH)
.padding(.vertical, Design.Spacing.small) .padding(.vertical, Design.Size.hintPaddingV)
.background( .background(
Capsule() Capsule()
.fill(Color.black.opacity(Design.Opacity.light)) .fill(Color.black.opacity(Design.Opacity.light))

View File

@ -56,7 +56,7 @@ struct PlayerHandsView: View {
} }
} }
} }
.frame(height: 180) .frame(height: Design.Size.playerHandsHeight)
} }
} }
@ -71,8 +71,8 @@ struct PlayerHandView: View {
let cardWidth: CGFloat let cardWidth: CGFloat
let cardSpacing: CGFloat let cardSpacing: CGFloat
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.BaseFontSize.medium @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
@ScaledMetric(relativeTo: .caption) private var handNumberSize: CGFloat = Design.BaseFontSize.small @ScaledMetric(relativeTo: .caption) private var handNumberSize: CGFloat = Design.Size.handNumberFontSize
var body: some View { var body: some View {
VStack(spacing: Design.Spacing.small) { VStack(spacing: Design.Spacing.small) {
@ -129,7 +129,7 @@ struct PlayerHandView: View {
if hand.isDoubledDown { if hand.isDoubledDown {
Image(systemName: "xmark.circle.fill") Image(systemName: "xmark.circle.fill")
.font(.system(size: handNumberSize)) .font(.system(size: Design.Size.handIconSize))
.foregroundStyle(.purple) .foregroundStyle(.purple)
} }
} }
@ -139,8 +139,8 @@ struct PlayerHandView: View {
Text(result.displayText) Text(result.displayText)
.font(.system(size: labelFontSize, weight: .black)) .font(.system(size: labelFontSize, weight: .black))
.foregroundStyle(result.color) .foregroundStyle(result.color)
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Size.hintPaddingH)
.padding(.vertical, Design.Spacing.xSmall) .padding(.vertical, Design.Size.hintPaddingV)
.background( .background(
Capsule() Capsule()
.fill(result.color.opacity(Design.Opacity.hint)) .fill(result.color.opacity(Design.Opacity.hint))
@ -151,6 +151,7 @@ struct PlayerHandView: View {
if hand.bet > 0 { if hand.bet > 0 {
HStack(spacing: Design.Spacing.xSmall) { HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "dollarsign.circle.fill") Image(systemName: "dollarsign.circle.fill")
.font(.system(size: Design.Size.handIconSize))
.foregroundStyle(.yellow) .foregroundStyle(.yellow)
Text("\(hand.bet * (hand.isDoubledDown ? 2 : 1))") Text("\(hand.bet * (hand.isDoubledDown ? 2 : 1))")
.font(.system(size: handNumberSize, weight: .bold, design: .rounded)) .font(.system(size: handNumberSize, weight: .bold, design: .rounded))

View File

@ -62,26 +62,29 @@ public struct ChipStackView: View {
public struct ChipOnTableView: View { public struct ChipOnTableView: View {
let amount: Int let amount: Int
let showMax: Bool let showMax: Bool
let size: CGFloat
let theme: any CasinoTheme let theme: any CasinoTheme
public init( public init(
amount: Int, amount: Int,
showMax: Bool = false, showMax: Bool = false,
size: CGFloat = 36,
theme: any CasinoTheme = DefaultCasinoTheme() theme: any CasinoTheme = DefaultCasinoTheme()
) { ) {
self.amount = amount self.amount = amount
self.showMax = showMax self.showMax = showMax
self.size = size
self.theme = theme self.theme = theme
} }
// MARK: - Layout Constants // MARK: - Layout Constants (relative to size)
private let chipSize: CGFloat = 36 private var chipSize: CGFloat { size }
private let innerRingSize: CGFloat = 26 private var innerRingSize: CGFloat { size * 0.72 } // 26/36
private let gradientEndRadius: CGFloat = 20 private var gradientEndRadius: CGFloat { size * 0.56 } // 20/36
private let maxBadgeFontSize: CGFloat = 8 private var maxBadgeFontSize: CGFloat { size * 0.22 } // 8/36
private let maxBadgeOffsetX: CGFloat = 6 private var maxBadgeOffsetX: CGFloat { size * 0.17 } // 6/36
private let maxBadgeOffsetY: CGFloat = -4 private var maxBadgeOffsetY: CGFloat { size * -0.11 } // -4/36
// MARK: - Computed Properties // MARK: - Computed Properties
@ -95,8 +98,10 @@ public struct ChipOnTableView: View {
amount >= 1000 ? "\(amount / 1000)K" : "\(amount)" amount >= 1000 ? "\(amount / 1000)K" : "\(amount)"
} }
/// Text font size scales with chip size
private var textFontSize: CGFloat { private var textFontSize: CGFloat {
amount >= 1000 ? CasinoDesign.BaseFontSize.xSmall : CasinoDesign.BaseFontSize.xSmall + 1 let baseFontSize = amount >= 1000 ? size * 0.25 : size * 0.30
return baseFontSize
} }
// MARK: - Accessibility // MARK: - Accessibility