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

This commit is contained in:
Matt Bruce 2025-12-22 23:50:09 -06:00
parent a2ecfcd580
commit 017000b197
16 changed files with 344 additions and 313 deletions

View File

@ -21,8 +21,8 @@
"value" : "-%lld $"
}
}
}
},
}
},
"%lld" : {
"comment" : "The number of rounds a player has played in the game.",
"localizations" : {
@ -67,8 +67,8 @@
"value" : "%lld."
}
}
}
},
}
},
"%lldpx" : {
"comment" : "A text label displaying the size of the app icon. The argument is the size of the icon in pixels.",
"localizations" : {
@ -113,8 +113,8 @@
"value" : "•"
}
}
}
},
}
},
"• Add to Assets.xcassets/AppIcon" : {
"comment" : "A step in the process of exporting app icons.",
"localizations" : {
@ -158,8 +158,8 @@
"value" : "• Appeler IconRenderer.renderAppIcon(config: .baccarat)"
}
}
}
},
}
},
"• Run the preview in Xcode" : {
"localizations" : {
"en" : {
@ -203,8 +203,8 @@
"value" : "• Enregistrer l'UIImage résultante dans les fichiers"
}
}
}
},
}
},
"• Screenshot the 1024px icon" : {
"comment" : "A step in the process of exporting app icons, describing how to take a screenshot of a 1024px icon.",
"localizations" : {
@ -249,8 +249,8 @@
"value" : "• Utiliser un outil en ligne pour générer toutes les tailles"
}
}
}
},
}
},
"↓ then →" : {
"comment" : "A textual instruction for using the road map in the game.",
"localizations" : {
@ -295,8 +295,8 @@
"value" : "+%lld"
}
}
}
},
}
},
"+$%lld" : {
"localizations" : {
"en" : {
@ -340,8 +340,8 @@
"value" : "2-9: Valeur faciale"
}
}
}
},
}
},
"8 : 1" : {
"comment" : "The payout ratio for a tie bet.",
"localizations" : {
@ -386,8 +386,8 @@
"value" : "10, Valet, Dame, Roi: 0 point"
}
}
}
},
}
},
"11 : 1" : {
"comment" : "The payout ratio for a pair bet.",
"localizations" : {
@ -432,8 +432,8 @@
"value" : "Un Naturel termine la manche immédiatement."
}
}
}
},
}
},
"Ace: 1 point" : {
"comment" : "Card value description for an Ace.",
"localizations" : {
@ -477,8 +477,8 @@
"value" : "Ajoutez %lld$ de plus pour atteindre le minimum"
}
}
}
},
}
},
"After generating:" : {
"comment" : "A heading for the instructions section of the icon generator view.",
"localizations" : {
@ -523,8 +523,8 @@
"value" : "Toutes les tailles"
}
}
}
},
}
},
"Alternative: Use an online tool" : {
"comment" : "A section header that suggests using an online tool to generate app icon sizes.",
"localizations" : {
@ -569,8 +569,8 @@
"value" : "Misez toujours sur le Banquier — il a les meilleures chances (1.06% d'avantage)."
}
}
}
},
}
},
"Animate dealing and flipping" : {
"comment" : "Subtitle for card animations toggle.",
"localizations" : {
@ -615,8 +615,8 @@
"value" : "Icône de l'app"
}
}
}
},
}
},
"App Icon Preview" : {
"comment" : "A header describing the preview of the app icon.",
"localizations" : {
@ -661,8 +661,8 @@
"value" : "Évitez la mise sur Égalité — 14.4% d'avantage maison!"
}
}
}
},
}
},
"B Pair" : {
"localizations" : {
"en" : {
@ -706,8 +706,8 @@
"value" : "Le Baccarat a l'un des plus faibles avantages maison du casino."
}
}
}
},
}
},
"Banker" : {
"localizations" : {
"en" : {
@ -751,8 +751,8 @@
"value" : "BANQUE"
}
}
}
},
}
},
"Banker 0-2: Always draws" : {
"comment" : "Description of the third card rule for the Banker when the Player's third card is 0-2.",
"localizations" : {
@ -797,8 +797,8 @@
"value" : "Banquier 3: Tire sauf si la 3e du Joueur était 8"
}
}
}
},
}
},
"Banker 4: Draws if Player's 3rd was 2-7" : {
"comment" : "Side bet rule for the Banker when the Player's third card is 4 and it falls between 2 and 7.",
"localizations" : {
@ -843,8 +843,8 @@
"value" : "Banquier 5: Tire si la 3e du Joueur était 4-7"
}
}
}
},
}
},
"Banker 6: Draws if Player's 3rd was 6-7" : {
"comment" : "Description of the betting strategy for the Banker when the Player's third card is 6-7.",
"localizations" : {
@ -958,8 +958,8 @@
"value" : "Main du banquier"
}
}
}
},
}
},
"Bet on which hand will win: Player, Banker, or Tie." : {
"comment" : "Text describing the objective of the baccarat game.",
"localizations" : {
@ -1004,8 +1004,8 @@
"value" : "Blackjack"
}
}
}
},
}
},
"BONUS" : {
"comment" : "The text displayed in the center of the bonus zone.",
"localizations" : {
@ -1232,8 +1232,8 @@
"value" : "Effacer toutes les données?"
}
}
}
},
}
},
"CLOUD SYNC" : {
"localizations" : {
"en" : {
@ -1276,8 +1276,8 @@
"value" : "DONNÉES"
}
}
}
},
}
},
"Deal" : {
"comment" : "The label of a button that deals cards in a game.",
"localizations" : {
@ -1322,8 +1322,8 @@
"value" : "Distribution..."
}
}
}
},
}
},
"DECK SETTINGS" : {
"comment" : "Section header for deck configuration settings.",
"localizations" : {
@ -1391,8 +1391,8 @@
"value" : "Afficher le compteur de cartes en haut"
}
}
}
},
}
},
"Display result road map" : {
"comment" : "Subtitle for show history toggle.",
"localizations" : {
@ -1437,8 +1437,8 @@
"value" : "Terminé"
}
}
}
},
}
},
"Dragon Bonus" : {
"localizations" : {
"en" : {
@ -1482,8 +1482,8 @@
"value" : "Le Bonus Dragon est amusant mais a ~2.7% d'avantage maison."
}
}
}
},
}
},
"Example: 5♥ + 5♣ = Pair (wins!)" : {
"comment" : "Example of a pair bet winning.",
"localizations" : {
@ -1528,8 +1528,8 @@
"value" : "Historique"
}
}
}
},
}
},
"Game Over" : {
"comment" : "The title of the game over screen.",
"localizations" : {
@ -1574,8 +1574,8 @@
"value" : "FIN DE PARTIE"
}
}
}
},
}
},
"Generate & Save Icons" : {
"comment" : "A button label that triggers the generation of app icons.",
"localizations" : {
@ -1620,8 +1620,8 @@
"value" : "Icônes générées:"
}
}
}
},
}
},
"Generating..." : {
"comment" : "A text that appears while generating icons.",
"localizations" : {
@ -1666,8 +1666,8 @@
"value" : "Les valeurs de main n'utilisent que le dernier chiffre (ex., 7+8=15 → 5)."
}
}
}
},
}
},
"handValueFormat" : {
"comment" : "Format for displaying hand value. The argument is the numeric value of the hand.",
"localizations" : {
@ -1712,8 +1712,8 @@
"value" : "Retour Haptique"
}
}
}
},
}
},
"HISTORY" : {
"comment" : "A label displayed above the road map view, indicating that it shows a history of past game results.",
"localizations" : {
@ -1758,8 +1758,8 @@
"value" : "%lld parties : %lld joueur, %lld banquier, %lld égalités"
}
}
}
},
}
},
"How to Export Icons" : {
"comment" : "A section header explaining how to export app icons.",
"localizations" : {
@ -1804,8 +1804,8 @@
"value" : "Comment jouer"
}
}
}
},
}
},
"iCloud Sync" : {
"localizations" : {
"en" : {
@ -1848,8 +1848,8 @@
"value" : "iCloud non disponible"
}
}
}
},
}
},
"Icon" : {
"comment" : "The title for the tab that displays the app icon preview.",
"localizations" : {
@ -1894,8 +1894,8 @@
"value" : "Générateur d'icônes"
}
}
}
},
}
},
"If either hand totals 8 or 9 with two cards, it's a 'Natural'." : {
"comment" : "Description of the 'Natural' hand in baccarat, explaining when it occurs and its significance.",
"localizations" : {
@ -1940,8 +1940,8 @@
"value" : "Si aucune main n'a un Naturel, les règles de la troisième carte s'appliquent."
}
}
}
},
}
},
"If Player draws, Banker's action depends on Player's third card:" : {
"comment" : "Explanation of the third card decision for the Banker in the Rules Help view.",
"localizations" : {
@ -1986,8 +1986,8 @@
"value" : "Si le Joueur reste, le Banquier tire sur 0-5, reste sur 6-7."
}
}
}
},
}
},
"Independent of the main game result." : {
"comment" : "Note about the independence of the Pair Bonus from the main game result in the Rules Help view.",
"localizations" : {
@ -2098,8 +2098,8 @@
"value" : "Tours joués"
}
}
}
},
}
},
"Main Bets" : {
"comment" : "Title of a rule page in the \"Rules\" help view, describing the main bets available in baccarat.",
"localizations" : {
@ -2144,8 +2144,8 @@
"value" : "MAX"
}
}
}
},
}
},
"Natural 9 beats Natural 8." : {
"comment" : "Explanation of the payout for a Baccarat hand that is a Natural 9, compared to one that is a Natural 8.",
"localizations" : {
@ -2190,8 +2190,8 @@
"value" : "Main naturelle"
}
}
}
},
}
},
"Natural Win: 1:1" : {
"comment" : "Description of the payout for a 'Natural Win' in the Rules Help view.",
"localizations" : {
@ -2371,8 +2371,8 @@
"value" : "Objectif"
}
}
}
},
}
},
"Only the rank matters (suits are ignored)." : {
"comment" : "Explanation of how to determine if the first two cards in a hand form a pair, focusing on the rank rather than the suit.",
"localizations" : {
@ -2645,8 +2645,8 @@
"value" : "Joueur"
}
}
}
},
}
},
"PLAYER" : {
"comment" : "The label for the player's hand in the cards display area.",
"localizations" : {
@ -2737,8 +2737,8 @@
"value" : "Joueur avec 0-5: Tire une troisième carte"
}
}
}
},
}
},
"Player with 6-7: Stands" : {
"comment" : "Description of the action for the Banker when the Player draws a third card.",
"localizations" : {
@ -2783,8 +2783,8 @@
"value" : "Joueur avec 8-9: Naturel (pas de troisième carte)"
}
}
}
},
}
},
"Poker" : {
"comment" : "The name of a poker game.",
"localizations" : {
@ -2828,8 +2828,8 @@
"value" : "Politique de confidentialité"
}
}
}
},
}
},
"Reset to Defaults" : {
"comment" : "A button label that resets game settings to their default values.",
"localizations" : {
@ -2874,8 +2874,8 @@
"value" : "Roulette"
}
}
}
},
}
},
"Rounds" : {
"localizations" : {
"en" : {
@ -2988,8 +2988,8 @@
"value" : "Afficher Cartes Restantes"
}
}
}
},
}
},
"Show History" : {
"comment" : "Toggle label for showing game history.",
"localizations" : {
@ -3034,8 +3034,8 @@
"value" : "Mise secondaire sur le Joueur ou Banquier gagnant par une marge."
}
}
}
},
}
},
"Side bet on the first two cards being a pair." : {
"comment" : "Description of a side bet where the player bets on whether the first two cards dealt in a hand are a pair.",
"localizations" : {
@ -3079,8 +3079,8 @@
"value" : "Connectez-vous à iCloud pour synchroniser"
}
}
}
},
}
},
"SOUND & HAPTICS" : {
"comment" : "Section header for sound and haptic settings.",
"localizations" : {
@ -3125,8 +3125,8 @@
"value" : "Effets Sonores"
}
}
}
},
}
},
"STARTING BALANCE" : {
"comment" : "Section header for starting balance settings.",
"localizations" : {
@ -3170,8 +3170,8 @@
"value" : "Statistiques"
}
}
}
},
}
},
"Strategy Tips" : {
"comment" : "Title of a section in the Rules Help view focused on strategy tips.",
"localizations" : {
@ -3284,8 +3284,8 @@
"value" : "%@ $ - %@ $"
}
}
}
},
}
},
"The hand closest to 9 wins." : {
"comment" : "Explanation of how the hand closest to 9 wins in baccarat.",
"localizations" : {
@ -3330,8 +3330,8 @@
"value" : "Aucune compétence requise — profitez simplement du jeu!"
}
}
}
},
}
},
"These show how the same pattern works for other games" : {
"comment" : "A description below the section of the view that previews icons for other games.",
"localizations" : {
@ -3443,8 +3443,8 @@
"value" : "Égalité"
}
}
}
},
}
},
"TIE" : {
"comment" : "The text displayed in the TIE betting zone.",
"localizations" : {
@ -3489,8 +3489,8 @@
"value" : "Mise Égalité: Paie 8:1"
}
}
}
},
}
},
"TOTAL" : {
"comment" : "A label displayed next to the total winnings in the result banner.",
"localizations" : {

View File

@ -119,11 +119,11 @@ struct RulesHelpView: View {
var body: some View {
NavigationStack {
ZStack {
ZStack {
Color.Sheet.background
.ignoresSafeArea()
VStack(spacing: 0) {
.ignoresSafeArea()
VStack(spacing: 0) {
// Page content
TabView(selection: $currentPage) {
ForEach(pages.indices, id: \.self) { index in
@ -132,15 +132,15 @@ struct RulesHelpView: View {
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
// Page indicator
HStack(spacing: Design.Spacing.small) {
ForEach(pages.indices, id: \.self) { index in
Circle()
.fill(index == currentPage ? Color.Sheet.accent : Color.white.opacity(Design.Opacity.light))
.frame(width: 8, height: 8)
}
}
}
}
.padding(.vertical, Design.Spacing.medium)
}
}
@ -149,7 +149,7 @@ struct RulesHelpView: View {
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button(String(localized: "Done")) {
dismiss()
dismiss()
}
.foregroundStyle(Color.Sheet.accent)
}
@ -190,10 +190,10 @@ struct RulePageView: View {
// Title
Text(page.title)
.font(.system(size: titleSize, weight: .bold))
.foregroundStyle(.white)
.foregroundStyle(.white)
// Content
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
ForEach(page.content.indices, id: \.self) { index in
HStack(alignment: .top, spacing: Design.Spacing.medium) {
Text("")

View File

@ -17,7 +17,7 @@ enum Design {
// MARK: - Debug
/// Set to true to show layout debug borders on views
static let showDebugBorders = false
static let showDebugBorders = true
// MARK: - Shared Constants (from CasinoKit)
@ -32,73 +32,45 @@ enum Design {
typealias BaseFontSize = CasinoDesign.BaseFontSize
typealias IconSize = CasinoDesign.IconSize
// MARK: - Blackjack-Specific Component Sizes
// MARK: - Blackjack-Specific Sizes (use CasinoDesign.Size for shared values)
enum Size {
// Cards
static let cardWidth: CGFloat = 90
static let cardOverlap: CGFloat = -50 // Negative = more overlap
// Use shared scaling from CasinoKit
static var handScale: CGFloat { CasinoDesign.Size.handScale }
static var fontScale: CGFloat { CasinoDesign.Size.fontScale }
// Cards - scaled for better visibility
static let cardWidth: CGFloat = 60 * handScale // 90pt at 1.5x
static let cardWidthSmall: CGFloat = CasinoDesign.Size.cardWidthSmall
// Player hands
static let playerHandsHeight: CGFloat = 240
/// Card overlap (negative = cards stack left over right).
/// More negative = more overlap (less card visible).
/// With 90pt cards: -40 = ~44% overlap, -50 = ~55% overlap
static let cardOverlap: CGFloat = -50
// Hand labels
static let handLabelFontSize: CGFloat = 14
static let handNumberFontSize: CGFloat = 12
static let handValueFontSize: CGFloat = 18
// Player hands container height (accommodates larger cards + labels)
// Reduced from 180 to fit content more snugly
static let playerHandsHeight: CGFloat = 160 * handScale // 240pt at 1.5x
// Hints
static let hintFontSize: CGFloat = 15
static let hintIconSize: CGFloat = 24
static let hintPaddingH: CGFloat = 18
static let hintPaddingV: CGFloat = 12
// Hand label font sizes (scaled)
static let handLabelFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * fontScale
static let handNumberFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * fontScale // Same as label
static let handValueFontSize: CGFloat = CasinoDesign.BaseFontSize.xLarge * fontScale
// Hand icons
static let handIconSize: CGFloat = 18
// Hint font size (scaled to match hands)
static let hintFontSize: CGFloat = CasinoDesign.BaseFontSize.small * fontScale
static let hintIconSize: CGFloat = CasinoDesign.IconSize.medium * handScale
static let hintPaddingH: CGFloat = CasinoDesign.Spacing.medium * handScale
static let hintPaddingV: CGFloat = CasinoDesign.Spacing.small * handScale
// Hi-Lo count badge
static let countBadgeFontSize: CGFloat = 10
static let countBadgePaddingH: CGFloat = 6
static let countBadgePaddingV: CGFloat = 2
static let countBadgeOffset: CGFloat = 6
// Hand icons (scaled)
static let handIconSize: CGFloat = CasinoDesign.IconSize.medium * handScale
// Betting
static let bettingChipSize: CGFloat = 54
// Hi-Lo count badge (scaled)
static let countBadgeFontSize: CGFloat = CasinoDesign.BaseFontSize.xxSmall * fontScale
static let countBadgePaddingH: CGFloat = CasinoDesign.Spacing.xSmall * handScale
static let countBadgePaddingV: CGFloat = CasinoDesign.Spacing.xxxSmall * handScale
static let countBadgeOffset: CGFloat = CasinoDesign.Spacing.xSmall * handScale
// Card count display
static let cardCountLabelSize: CGFloat = 11
static let cardCountValueSize: CGFloat = 18
// 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 * fontScale
static let cardCountValueSize: CGFloat = CasinoDesign.BaseFontSize.large * handScale
// Chips - use CasinoDesign values
static let chipBadgeSize: CGFloat = CasinoDesign.Size.chipBadge
// Buttons - use CasinoDesign values
static let actionButtonHeight: CGFloat = CasinoDesign.Size.actionButtonHeight
static let actionButtonMinWidth: CGFloat = CasinoDesign.Size.actionButtonMinWidth
static let bettingZoneHeight: CGFloat = CasinoDesign.Size.bettingZoneHeight
// Responsive - use CasinoDesign values
static let maxContentWidthPortrait: CGFloat = CasinoDesign.Size.maxContentWidthPortrait
static let maxContentWidthLandscape: CGFloat = CasinoDesign.Size.maxContentWidthLandscape
static let maxModalWidth: CGFloat = CasinoDesign.Size.maxModalWidth
// Blackjack-specific
// Table
static let tableHeight: CGFloat = 280
// Settings - use CasinoDesign values
static let checkmark: CGFloat = CasinoDesign.Size.checkmark
}
}
@ -138,7 +110,7 @@ extension Color {
static let blackjack = Color.yellow
}
// MARK: - Action Button Colors
// MARK: - Action Button Colors (Blackjack-specific)
enum Button {
static let hit = Color(red: 0.2, green: 0.6, blue: 0.3)
@ -147,25 +119,6 @@ extension Color {
static let split = Color(red: 0.3, green: 0.5, blue: 0.7)
static let surrender = Color(red: 0.6, green: 0.3, blue: 0.3)
static let insurance = Color(red: 0.7, green: 0.6, blue: 0.2)
static let goldLight = Color(red: 1.0, green: 0.85, blue: 0.3)
static let goldDark = Color(red: 0.9, green: 0.7, blue: 0.2)
}
// MARK: - Settings Colors
enum Settings {
static let background = Color(red: 0.08, green: 0.12, blue: 0.18)
static let cardBackground = Color.white.opacity(CasinoDesign.Opacity.verySubtle)
static let accent = Color(red: 0.9, green: 0.75, blue: 0.3)
}
// MARK: - Modal Colors
enum Modal {
static let background = Color(red: 0.12, green: 0.18, blue: 0.25)
static let backgroundLight = Color(red: 0.15, green: 0.2, blue: 0.3)
static let backgroundDark = Color(red: 0.1, green: 0.15, blue: 0.25)
}
// MARK: - TopBar Colors

View File

@ -61,7 +61,7 @@ struct ActionButton: View {
Capsule()
.fill(
LinearGradient(
colors: [Color.Button.goldLight, Color.Button.goldDark],
colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
startPoint: .top,
endPoint: .bottom
)

View File

@ -13,16 +13,19 @@ struct CardCountView: View {
let runningCount: Int
let trueCount: Double
@ScaledMetric(relativeTo: .caption) private var labelSize: CGFloat = Design.Size.cardCountLabelSize
@ScaledMetric(relativeTo: .title) private var valueSize: CGFloat = Design.Size.cardCountValueSize
var body: some View {
HStack(spacing: Design.Spacing.large) {
// Running count
VStack(spacing: Design.Spacing.xxSmall) {
Text("Running")
.font(.system(size: Design.Size.cardCountLabelSize, weight: .medium))
.font(.system(size: labelSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
Text(runningCount >= 0 ? "+\(runningCount)" : "\(runningCount)")
.font(.system(size: Design.Size.cardCountValueSize, weight: .bold, design: .monospaced))
.font(.system(size: valueSize, weight: .bold, design: .monospaced))
.foregroundStyle(countColor(for: runningCount))
}
@ -33,11 +36,11 @@ struct CardCountView: View {
// True count
VStack(spacing: Design.Spacing.xxSmall) {
Text("True")
.font(.system(size: Design.Size.cardCountLabelSize, weight: .medium))
.font(.system(size: labelSize, 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.Size.cardCountValueSize, weight: .bold, design: .monospaced))
.font(.system(size: valueSize, weight: .bold, design: .monospaced))
.foregroundStyle(countColor(for: Int(trueCount.rounded())))
}
}

View File

@ -33,8 +33,8 @@ struct GameTableView: View {
private var maxContentWidth: CGFloat {
if isIPad {
return verticalSizeClass == .compact
? Design.Size.maxContentWidthLandscape
: Design.Size.maxContentWidthPortrait
? CasinoDesign.Size.maxContentWidthLandscape
: CasinoDesign.Size.maxContentWidthPortrait
}
return .infinity
}
@ -72,18 +72,16 @@ struct GameTableView: View {
@ViewBuilder
private func mainGameView(state: GameState) -> some View {
GeometryReader { geometry in
ZStack {
// Background
TableBackgroundView()
mainContent(state: state, screenHeight: geometry.size.height)
}
ZStack {
// Background
TableBackgroundView()
mainContent(state: state)
}
}
@ViewBuilder
private func mainContent(state: GameState, screenHeight: CGFloat) -> some View {
private func mainContent(state: GameState) -> some View {
ZStack {
VStack(spacing: 0) {
// Top bar
@ -119,8 +117,7 @@ struct GameTableView: View {
// Table layout - fills available space
BlackjackTableView(
state: state,
onPlaceBet: { placeBet(state: state) },
screenHeight: screenHeight
onPlaceBet: { placeBet(state: state) }
)
.frame(maxWidth: maxContentWidth)

View File

@ -121,7 +121,7 @@ struct ResultBannerView: View {
Capsule()
.fill(
LinearGradient(
colors: [Color.Button.goldLight, Color.Button.goldDark],
colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
startPoint: .top,
endPoint: .bottom
)
@ -144,7 +144,7 @@ struct ResultBannerView: View {
Capsule()
.fill(
LinearGradient(
colors: [Color.Button.goldLight, Color.Button.goldDark],
colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
startPoint: .top,
endPoint: .bottom
)
@ -158,7 +158,7 @@ struct ResultBannerView: View {
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge)
.fill(
LinearGradient(
colors: [Color.Modal.backgroundLight, Color.Modal.backgroundDark],
colors: [Color.CasinoModal.backgroundLight, Color.CasinoModal.backgroundDark],
startPoint: .top,
endPoint: .bottom
)
@ -172,7 +172,7 @@ struct ResultBannerView: View {
)
)
.shadow(color: mainResultColor.opacity(Design.Opacity.hint), radius: Design.Shadow.radiusXLarge)
.frame(maxWidth: Design.Size.maxModalWidth)
.frame(maxWidth: CasinoDesign.Size.maxModalWidth)
.padding(.horizontal, Design.Spacing.large) // Prevent clipping on sides
.scaleEffect(showContent ? 1.0 : 0.8)
.opacity(showContent ? 1.0 : 0)

View File

@ -191,7 +191,7 @@ struct RulesHelpView: View {
var body: some View {
NavigationStack {
ZStack {
Color.Settings.background
Color.Sheet.background
.ignoresSafeArea()
VStack(spacing: 0) {
@ -208,7 +208,7 @@ struct RulesHelpView: View {
HStack(spacing: Design.Spacing.small) {
ForEach(pages.indices, id: \.self) { index in
Circle()
.fill(index == currentPage ? Color.Settings.accent : Color.white.opacity(Design.Opacity.light))
.fill(index == currentPage ? Color.Sheet.accent : Color.white.opacity(Design.Opacity.light))
.frame(width: 8, height: 8)
}
}
@ -222,10 +222,10 @@ struct RulesHelpView: View {
Button(String(localized: "Done")) {
dismiss()
}
.foregroundStyle(Color.Settings.accent)
.foregroundStyle(Color.Sheet.accent)
}
}
.toolbarBackground(Color.Settings.background, for: .navigationBar)
.toolbarBackground(Color.Sheet.background, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar)
}
}
@ -255,7 +255,7 @@ struct RulePageView: View {
// Icon
Image(systemName: page.icon)
.font(.system(size: iconSize))
.foregroundStyle(Color.Settings.accent)
.foregroundStyle(Color.Sheet.accent)
.padding(.top, Design.Spacing.xxLarge)
// Title
@ -268,7 +268,7 @@ struct RulePageView: View {
ForEach(page.content.indices, id: \.self) { index in
HStack(alignment: .top, spacing: Design.Spacing.medium) {
Text("")
.foregroundStyle(Color.Settings.accent)
.foregroundStyle(Color.Sheet.accent)
Text(page.content[index])
.font(.system(size: bodySize))

View File

@ -24,7 +24,7 @@ struct SettingsView: View {
}
/// Accent color for settings components
private let accent = Color.Settings.accent
private let accent = Color.Sheet.accent
var body: some View {
SheetContainerView(
@ -248,7 +248,7 @@ struct SettingsView: View {
}
}
.padding(.vertical, Design.Spacing.xSmall)
}
}
}
}
@ -335,10 +335,10 @@ struct SettingsView: View {
// 12. Version info
Text(appVersionString)
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.light))
.frame(maxWidth: .infinity)
.padding(.top, Design.Spacing.large)
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.light))
.frame(maxWidth: .infinity)
.padding(.top, Design.Spacing.large)
.padding(.bottom, Design.Spacing.medium)
},
onCancel: nil,
@ -377,7 +377,7 @@ struct GameStylePicker: View {
title: style.displayName,
subtitle: style.description,
isSelected: selection == style,
accentColor: Color.Settings.accent,
accentColor: Color.Sheet.accent,
action: { selection = style }
)
}
@ -397,7 +397,7 @@ struct DeckCountPicker: View {
title: count.displayName,
subtitle: count.description,
isSelected: selection == count,
accentColor: Color.Settings.accent,
accentColor: Color.Sheet.accent,
action: { selection = count }
)
}
@ -417,8 +417,8 @@ struct TableLimitsPicker: View {
title: limit.displayName,
subtitle: limit.detailedDescription,
isSelected: selection == limit,
accentColor: Color.Settings.accent,
badge: { BadgePill(text: limit.description, isSelected: selection == limit, accentColor: Color.Settings.accent) },
accentColor: Color.Sheet.accent,
badge: { BadgePill(text: limit.description, isSelected: selection == limit, accentColor: Color.Sheet.accent) },
action: { selection = limit }
)
}

View File

@ -72,7 +72,7 @@ struct StatisticsSheetView: View {
StatBox(title: String(localized: "Rounds"), value: "\(totalRounds)", color: .white)
StatBox(title: String(localized: "Win Rate"), value: formatPercent(winRate), color: winRate >= 50 ? .green : .orange)
StatBox(title: String(localized: "Net"), value: formatMoney(totalWinnings), color: totalWinnings >= 0 ? .green : .red)
StatBox(title: String(localized: "Balance"), value: "$\(state.balance)", color: Color.Settings.accent)
StatBox(title: String(localized: "Balance"), value: "$\(state.balance)", color: Color.Sheet.accent)
}
}

View File

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

View File

@ -12,8 +12,15 @@ struct BlackjackTableView: View {
@Bindable var state: GameState
let onPlaceBet: () -> Void
/// Screen height passed from parent for responsive sizing
var screenHeight: CGFloat = 800
// MARK: - Environment
@Environment(\.verticalSizeClass) private var verticalSizeClass
// MARK: - State
/// Screen dimensions measured from container for responsive sizing
@State private var screenHeight: CGFloat = 800
@State private var screenWidth: CGFloat = 375
/// Whether to show Hi-Lo card count values on cards.
var showCardCount: Bool { state.settings.showCardCount }
@ -24,10 +31,44 @@ struct BlackjackTableView: View {
@ScaledMetric(relativeTo: .title) private var valueFontSize: CGFloat = Design.BaseFontSize.xLarge
@ScaledMetric(relativeTo: .caption) private var hintFontSize: CGFloat = Design.BaseFontSize.small
// MARK: - Layout
// MARK: - Dynamic Card Sizing
private let cardWidth: CGFloat = Design.Size.cardWidth
private let cardSpacing: CGFloat = Design.Size.cardOverlap
/// Whether we're in landscape mode
/// - iPhones: use verticalSizeClass == .compact
/// - iPads: use screen dimensions (since iPads always report .regular)
private var isLandscape: Bool {
if DeviceInfo.isPad {
return screenWidth > screenHeight
}
return verticalSizeClass == .compact
}
/// Dynamic card width based on screen width
/// - iPhone SE (375pt): ~80pt
/// - iPhone 16 Pro (393pt): ~95pt
/// - iPhone 16 Pro Max (430pt): ~120pt
/// - iPad mini (landscape): ~90pt (reduced for smaller tablet screen)
/// - iPad: ~140pt (capped)
private var cardWidth: CGFloat {
// iPad mini in landscape gets smaller cards to fit better
if DeviceInfo.isPadMini && isLandscape {
return 90
}
let baseWidth: CGFloat = 80 // Minimum card width for smallest screens
let baseScreen: CGFloat = 375 // iPhone SE width
let scale: CGFloat = 0.7 // Scale factor (70% of width increase goes to cards)
let maxWidth: CGFloat = 140 // Cap for large screens
let calculated = baseWidth + (screenWidth - baseScreen) * scale
return min(maxWidth, max(baseWidth, calculated))
}
/// Card overlap scales proportionally with card width
private var cardSpacing: CGFloat {
// Overlap ratio: roughly -55% of card width
cardWidth * -0.55
}
/// Fixed height for the hint area to prevent layout shifts
private let hintAreaHeight: CGFloat = 44
@ -116,6 +157,19 @@ struct BlackjackTableView: View {
}
.padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.medium)
.background(
GeometryReader { geometry in
Color.clear
.onAppear {
screenHeight = geometry.size.height
screenWidth = geometry.size.width
}
.onChange(of: geometry.size) { _, newSize in
screenHeight = newSize.height
screenWidth = newSize.width
}
}
)
.debugBorder(showDebugBorders, color: .white, label: "TableView")
.animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase)
}

View File

@ -12,17 +12,22 @@ import CasinoKit
struct HiLoCountBadge: View {
let card: Card
@ScaledMetric(relativeTo: .caption2) private var fontSize: CGFloat = Design.Size.countBadgeFontSize
@ScaledMetric(relativeTo: .caption2) private var paddingH: CGFloat = Design.Size.countBadgePaddingH
@ScaledMetric(relativeTo: .caption2) private var paddingV: CGFloat = Design.Size.countBadgePaddingV
@ScaledMetric(relativeTo: .caption2) private var offsetAmount: CGFloat = Design.Size.countBadgeOffset
var body: some View {
Text(card.hiLoDisplayText)
.font(.system(size: Design.Size.countBadgeFontSize, weight: .bold, design: .rounded))
.font(.system(size: fontSize, weight: .bold, design: .rounded))
.foregroundStyle(badgeTextColor)
.padding(.horizontal, Design.Size.countBadgePaddingH)
.padding(.vertical, Design.Size.countBadgePaddingV)
.padding(.horizontal, paddingH)
.padding(.vertical, paddingV)
.background(
Capsule()
.fill(badgeBackgroundColor)
)
.offset(x: -Design.Size.countBadgeOffset, y: Design.Size.countBadgeOffset)
.offset(x: -offsetAmount, y: offsetAmount)
}
private var badgeBackgroundColor: Color {

View File

@ -14,17 +14,22 @@ import CasinoKit
struct HintView: View {
let hint: String
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.Size.hintIconSize
@ScaledMetric(relativeTo: .body) private var fontSize: CGFloat = Design.Size.hintFontSize
@ScaledMetric(relativeTo: .body) private var paddingH: CGFloat = Design.Size.hintPaddingH
@ScaledMetric(relativeTo: .body) private var paddingV: CGFloat = Design.Size.hintPaddingV
var body: some View {
HStack(spacing: Design.Spacing.small) {
Image(systemName: "lightbulb.fill")
.font(.system(size: Design.Size.hintIconSize))
.font(.system(size: iconSize))
.foregroundStyle(.yellow)
Text(String(localized: "Hint: \(hint)"))
.font(.system(size: Design.Size.hintFontSize, weight: .medium))
.font(.system(size: fontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.strong))
}
.padding(.horizontal, Design.Size.hintPaddingH)
.padding(.vertical, Design.Size.hintPaddingV)
.padding(.horizontal, paddingH)
.padding(.vertical, paddingV)
.background(
Capsule()
.fill(Color.black.opacity(Design.Opacity.light))
@ -42,6 +47,11 @@ struct BettingHintView: View {
let hint: String
let trueCount: Double
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.Size.hintIconSize
@ScaledMetric(relativeTo: .body) private var fontSize: CGFloat = Design.Size.hintFontSize
@ScaledMetric(relativeTo: .body) private var paddingH: CGFloat = Design.Size.hintPaddingH
@ScaledMetric(relativeTo: .body) private var paddingV: CGFloat = Design.Size.hintPaddingV
private var hintColor: Color {
let tc = Int(trueCount.rounded())
if tc >= 2 {
@ -67,14 +77,14 @@ struct BettingHintView: View {
var body: some View {
HStack(spacing: Design.Spacing.small) {
Image(systemName: icon)
.font(.system(size: Design.Size.hintIconSize))
.font(.system(size: iconSize))
.foregroundStyle(hintColor)
Text(hint)
.font(.system(size: Design.Size.hintFontSize, weight: .medium))
.font(.system(size: fontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.strong))
}
.padding(.horizontal, Design.Size.hintPaddingH)
.padding(.vertical, Design.Size.hintPaddingV)
.padding(.horizontal, paddingH)
.padding(.vertical, paddingV)
.background(
Capsule()
.fill(Color.black.opacity(Design.Opacity.light))

View File

@ -71,7 +71,7 @@ struct InsurancePopupView: View {
Capsule()
.fill(
LinearGradient(
colors: [Color.Button.goldLight, Color.Button.goldDark],
colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
startPoint: .top,
endPoint: .bottom
)
@ -84,7 +84,7 @@ struct InsurancePopupView: View {
.padding(Design.Spacing.xLarge)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge)
.fill(Color.Modal.background)
.fill(Color.CasinoModal.backgroundDark)
.shadow(color: .black.opacity(Design.Opacity.medium), radius: Design.Shadow.radiusXLarge)
)
.overlay(

View File

@ -25,48 +25,47 @@ struct PlayerHandsView: View {
}
var body: some View {
GeometryReader { geometry in
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(hands.indices.reversed(), id: \.self) { index in
PlayerHandView(
hand: hands[index],
isActive: index == activeHandIndex && isPlayerTurn,
showCardCount: showCardCount,
// Hand numbers: rightmost (index 0) is Hand 1, played first
handNumber: hands.count > 1 ? index + 1 : nil,
cardWidth: cardWidth,
cardSpacing: cardSpacing
)
.id(index)
}
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(hands.indices.reversed(), id: \.self) { index in
PlayerHandView(
hand: hands[index],
isActive: index == activeHandIndex && isPlayerTurn,
showCardCount: showCardCount,
// Hand numbers: rightmost (index 0) is Hand 1, played first
handNumber: hands.count > 1 ? index + 1 : nil,
cardWidth: cardWidth,
cardSpacing: cardSpacing
)
.id(index)
}
.padding(.horizontal, Design.Spacing.large)
.frame(minWidth: geometry.size.width)
}
.scrollClipDisabled()
.scrollBounceBehavior(.basedOnSize)
.onChange(of: activeHandIndex) { _, newIndex in
scrollToHand(proxy: proxy, index: newIndex)
}
.onChange(of: totalCardCount) { _, _ in
// Scroll to active hand when cards are added (hit)
scrollToHand(proxy: proxy, index: activeHandIndex)
}
.onChange(of: hands.count) { _, _ in
// Scroll to active hand when split occurs
scrollToHand(proxy: proxy, index: activeHandIndex)
}
.onAppear {
scrollToHand(proxy: proxy, index: activeHandIndex)
.padding(.horizontal, Design.Spacing.large)
.containerRelativeFrame(.horizontal) { length, _ in
length // Ensures content fills container width for centering
}
}
.scrollClipDisabled()
.scrollBounceBehavior(.basedOnSize)
.onChange(of: activeHandIndex) { _, newIndex in
scrollToHand(proxy: proxy, index: newIndex)
}
.onChange(of: totalCardCount) { _, _ in
// Scroll to active hand when cards are added (hit)
scrollToHand(proxy: proxy, index: activeHandIndex)
}
.onChange(of: hands.count) { _, _ in
// Scroll to active hand when split occurs
scrollToHand(proxy: proxy, index: activeHandIndex)
}
.onAppear {
scrollToHand(proxy: proxy, index: activeHandIndex)
}
}
.frame(height: Design.Size.playerHandsHeight)
}
private func scrollToHand(proxy: ScrollViewProxy, index: Int) {
@ -89,6 +88,9 @@ struct PlayerHandView: View {
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
@ScaledMetric(relativeTo: .caption) private var handNumberSize: CGFloat = Design.Size.handNumberFontSize
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.Size.handIconSize
@ScaledMetric(relativeTo: .body) private var hintPaddingH: CGFloat = Design.Size.hintPaddingH
@ScaledMetric(relativeTo: .body) private var hintPaddingV: CGFloat = Design.Size.hintPaddingV
var body: some View {
VStack(spacing: Design.Spacing.small) {
@ -126,8 +128,25 @@ struct PlayerHandView: View {
)
)
)
.overlay {
// Result badge - centered on cards
if let result = hand.result {
Text(result.displayText)
.font(.system(size: labelFontSize, weight: .black))
.foregroundStyle(.white)
.padding(.horizontal, hintPaddingH)
.padding(.vertical, hintPaddingV)
.background(
Capsule()
.fill(result.color)
.shadow(color: .black.opacity(Design.Opacity.medium), radius: Design.Shadow.radiusMedium)
)
.transition(.scale.combined(with: .opacity))
}
}
.contentShape(Rectangle())
.animation(.easeInOut(duration: Design.Animation.quick), value: isActive)
.animation(.spring(duration: Design.Animation.springDuration), value: hand.result != nil)
// Hand info
HStack(spacing: Design.Spacing.small) {
@ -145,29 +164,16 @@ struct PlayerHandView: View {
if hand.isDoubledDown {
Image(systemName: "xmark.circle.fill")
.font(.system(size: Design.Size.handIconSize))
.font(.system(size: iconSize))
.foregroundStyle(.purple)
}
}
// Result badge
if let result = hand.result {
Text(result.displayText)
.font(.system(size: labelFontSize, weight: .black))
.foregroundStyle(result.color)
.padding(.horizontal, Design.Size.hintPaddingH)
.padding(.vertical, Design.Size.hintPaddingV)
.background(
Capsule()
.fill(result.color.opacity(Design.Opacity.hint))
)
}
// Bet amount
if hand.bet > 0 {
HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "dollarsign.circle.fill")
.font(.system(size: Design.Size.handIconSize))
.font(.system(size: iconSize))
.foregroundStyle(.yellow)
Text("\(hand.bet * (hand.isDoubledDown ? 2 : 1))")
.font(.system(size: handNumberSize, weight: .bold, design: .rounded))