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" : {
"localizations" : {
"en" : {

View File

@ -31,10 +31,44 @@ enum Design {
// MARK: - Blackjack-Specific Component Sizes
enum Size {
// Cards - slightly larger than medium for better visibility
static let cardWidth: CGFloat = 60
// Hand scaling factor (1.5 = 50% larger hands)
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 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
static let chipBadgeSize: CGFloat = CasinoDesign.Size.chipBadge

View File

@ -14,7 +14,7 @@ struct BettingZoneView: View {
let maxBet: Int
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 {
betAmount >= maxBet
@ -33,8 +33,8 @@ struct BettingZoneView: View {
// Content
if betAmount > 0 {
// Show chip with amount
ChipOnTableView(amount: betAmount, showMax: isAtMax)
// Show chip with amount (scaled)
ChipOnTableView(amount: betAmount, showMax: isAtMax, size: Design.Size.bettingChipSize)
} else {
// Empty state
VStack(spacing: Design.Spacing.small) {
@ -44,18 +44,18 @@ struct BettingZoneView: View {
HStack(spacing: Design.Spacing.medium) {
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))
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))
}
}
}
}
.frame(maxWidth: .infinity)
.frame(height: Design.Size.bettingZoneHeight)
.frame(height: Design.Size.bettingZoneHeightScaled)
}
.buttonStyle(.plain)
.accessibilityLabel(betAmount > 0 ? "$\(betAmount) bet" + (isAtMax ? ", maximum" : "") : "Place bet")

View File

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

View File

@ -15,7 +15,7 @@ struct DealerHandView: View {
let cardWidth: 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 {
VStack(spacing: Design.Spacing.small) {

View File

@ -111,18 +111,18 @@ struct GameTableView: View {
Spacer()
// Chip selector - only interactive during betting phase
// During gameplay, show chips as they were (balance + currentBet) but dimmed
ChipSelectorView(
selectedChip: $selectedChip,
balance: state.currentPhase == .betting ? state.balance : (state.balance + state.currentBet),
currentBet: state.currentPhase == .betting ? state.currentBet : 0,
maxBet: state.settings.maxBet
)
.frame(maxWidth: maxContentWidth)
.padding(.bottom, Design.Spacing.small)
.opacity(state.currentPhase == .betting ? 1.0 : Design.Opacity.medium)
.allowsHitTesting(state.currentPhase == .betting) // Disable interaction when not betting
// Chip selector - only shown during betting phase
if state.currentPhase == .betting {
ChipSelectorView(
selectedChip: $selectedChip,
balance: state.balance,
currentBet: state.currentBet,
maxBet: state.settings.maxBet
)
.frame(maxWidth: maxContentWidth)
.padding(.bottom, Design.Spacing.small)
.transition(.opacity.combined(with: .move(edge: .bottom)))
}
// Action buttons
ActionButtonsView(state: state)
@ -216,24 +216,47 @@ struct ActionButtonsView: View {
// 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 {
ActionButton(
String(localized: "Clear"),
icon: "xmark.circle",
style: .destructive
) {
state.clearBet()
}
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)
}
if state.canDeal {
ActionButton(
String(localized: "Deal"),
icon: "play.fill",
style: .primary
) {
Task { await state.deal() }
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)
}
}
}
@ -379,11 +402,11 @@ struct CardCountView: View {
// Running count
VStack(spacing: Design.Spacing.xxSmall) {
Text("Running")
.font(.system(size: Design.BaseFontSize.xSmall, weight: .medium))
.font(.system(size: Design.Size.cardCountLabelSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
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))
}
@ -394,11 +417,11 @@ struct CardCountView: View {
// True count
VStack(spacing: Design.Spacing.xxSmall) {
Text("True")
.font(.system(size: Design.BaseFontSize.xSmall, weight: .medium))
.font(.system(size: Design.Size.cardCountLabelSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
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())))
}
}

View File

@ -14,15 +14,15 @@ struct HiLoCountBadge: View {
var body: some View {
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)
.padding(.horizontal, Design.Spacing.xSmall)
.padding(.vertical, Design.Spacing.xxxSmall)
.padding(.horizontal, Design.Size.countBadgePaddingH)
.padding(.vertical, Design.Size.countBadgePaddingV)
.background(
Capsule()
.fill(badgeBackgroundColor)
)
.offset(x: -Design.Spacing.xSmall, y: Design.Spacing.xSmall)
.offset(x: -Design.Size.countBadgeOffset, y: Design.Size.countBadgeOffset)
}
private var badgeBackgroundColor: Color {

View File

@ -17,13 +17,14 @@ struct HintView: View {
var body: some View {
HStack(spacing: Design.Spacing.small) {
Image(systemName: "lightbulb.fill")
.font(.system(size: Design.Size.hintIconSize))
.foregroundStyle(.yellow)
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))
}
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
.padding(.horizontal, Design.Size.hintPaddingH)
.padding(.vertical, Design.Size.hintPaddingV)
.background(
Capsule()
.fill(Color.black.opacity(Design.Opacity.light))
@ -66,13 +67,14 @@ struct BettingHintView: View {
var body: some View {
HStack(spacing: Design.Spacing.small) {
Image(systemName: icon)
.font(.system(size: Design.Size.hintIconSize))
.foregroundStyle(hintColor)
Text(hint)
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
.font(.system(size: Design.Size.hintFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.strong))
}
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
.padding(.horizontal, Design.Size.hintPaddingH)
.padding(.vertical, Design.Size.hintPaddingV)
.background(
Capsule()
.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 cardSpacing: CGFloat
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.BaseFontSize.medium
@ScaledMetric(relativeTo: .caption) private var handNumberSize: CGFloat = Design.BaseFontSize.small
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
@ScaledMetric(relativeTo: .caption) private var handNumberSize: CGFloat = Design.Size.handNumberFontSize
var body: some View {
VStack(spacing: Design.Spacing.small) {
@ -129,7 +129,7 @@ struct PlayerHandView: View {
if hand.isDoubledDown {
Image(systemName: "xmark.circle.fill")
.font(.system(size: handNumberSize))
.font(.system(size: Design.Size.handIconSize))
.foregroundStyle(.purple)
}
}
@ -139,8 +139,8 @@ struct PlayerHandView: View {
Text(result.displayText)
.font(.system(size: labelFontSize, weight: .black))
.foregroundStyle(result.color)
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.xSmall)
.padding(.horizontal, Design.Size.hintPaddingH)
.padding(.vertical, Design.Size.hintPaddingV)
.background(
Capsule()
.fill(result.color.opacity(Design.Opacity.hint))
@ -151,6 +151,7 @@ struct PlayerHandView: View {
if hand.bet > 0 {
HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "dollarsign.circle.fill")
.font(.system(size: Design.Size.handIconSize))
.foregroundStyle(.yellow)
Text("\(hand.bet * (hand.isDoubledDown ? 2 : 1))")
.font(.system(size: handNumberSize, weight: .bold, design: .rounded))

View File

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