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

This commit is contained in:
Matt Bruce 2025-12-16 22:06:55 -06:00
parent baf9d88601
commit bde7b0bd3f
14 changed files with 504 additions and 290 deletions

View File

@ -51,7 +51,8 @@ You are a **Senior iOS Engineer**, specializing in SwiftUI, SwiftData, and relat
- When hiding scroll view indicators, use the `.scrollIndicators(.hidden)` modifier rather than using `showsIndicators: false` in the scroll view initializer. - When hiding scroll view indicators, use the `.scrollIndicators(.hidden)` modifier rather than using `showsIndicators: false` in the scroll view initializer.
- Place view logic into view models or similar, so it can be tested. - Place view logic into view models or similar, so it can be tested.
- Avoid `AnyView` unless it is absolutely required. - Avoid `AnyView` unless it is absolutely required.
- Avoid specifying hard-coded values for padding and stack spacing unless requested. - **Never use raw numeric literals** for padding, spacing, opacity, font sizes, dimensions, corner radii, shadows, or animation durations—always use Design constants (see "No magic numbers" section).
- **Never use inline `Color(red:green:blue:)` or hex colors**—define all colors in the `Color` extension in `DesignConstants.swift` with semantic names.
- Avoid using UIKit colors in SwiftUI code. - Avoid using UIKit colors in SwiftUI code.
@ -86,25 +87,97 @@ If SwiftData is configured to use CloudKit:
- Never use `NSLocalizedString`; prefer the modern `String(localized:)` API. - Never use `NSLocalizedString`; prefer the modern `String(localized:)` API.
## No magic numbers or hardcoded values
**Never use raw numeric literals or hardcoded colors directly in views.** All values must be extracted to named constants, enums, or variables. This applies to:
### Values that MUST be constants:
- **Spacing & Padding**: `.padding(Design.Spacing.medium)` not `.padding(12)`
- **Corner Radii**: `Design.CornerRadius.large` not `cornerRadius: 16`
- **Font Sizes**: `Design.BaseFontSize.body` not `size: 14`
- **Opacity Values**: `Design.Opacity.strong` not `.opacity(0.7)`
- **Colors**: `Color.Primary.accent` not `Color(red: 0.8, green: 0.6, blue: 0.2)`
- **Line Widths**: `Design.LineWidth.medium` not `lineWidth: 2`
- **Shadow Values**: `Design.Shadow.radiusLarge` not `radius: 10`
- **Animation Durations**: `Design.Animation.quick` not `duration: 0.3`
- **Component Sizes**: `Design.Size.chipBadge` not `frame(width: 32)`
### What to do when you see a magic number:
1. Check if an appropriate constant already exists in `DesignConstants.swift`
2. If not, add a new constant with a semantic name
3. Use the constant in place of the raw value
4. If it's truly view-specific and used only once, extract to a `private let` at the top of the view struct
### Examples of violations:
```swift
// ❌ BAD - Magic numbers everywhere
.padding(16)
.opacity(0.6)
.frame(width: 80, height: 52)
.shadow(radius: 10, y: 5)
Color(red: 0.25, green: 0.3, blue: 0.45)
// ✅ GOOD - Named constants
.padding(Design.Spacing.large)
.opacity(Design.Opacity.accent)
.frame(width: Design.Size.bonusZoneWidth, height: Design.Size.topBetRowHeight)
.shadow(radius: Design.Shadow.radiusLarge, y: Design.Shadow.offsetLarge)
Color.BettingZone.dragonBonusLight
```
## Design constants instructions ## Design constants instructions
- Avoid magic numbers for layout values (padding, spacing, corner radii, font sizes, etc.).
- Create a centralized design constants file (e.g., `DesignConstants.swift`) using enums for namespacing: - Create a centralized design constants file (e.g., `DesignConstants.swift`) using enums for namespacing:
```swift ```swift
enum Design { enum Design {
enum Spacing { enum Spacing {
static let xxSmall: CGFloat = 2
static let xSmall: CGFloat = 4
static let small: CGFloat = 8 static let small: CGFloat = 8
static let medium: CGFloat = 12 static let medium: CGFloat = 12
static let large: CGFloat = 16 static let large: CGFloat = 16
static let xLarge: CGFloat = 20
} }
enum CornerRadius { enum CornerRadius {
static let small: CGFloat = 8 static let small: CGFloat = 8
static let medium: CGFloat = 12 static let medium: CGFloat = 12
static let large: CGFloat = 16
} }
enum BaseFontSize { enum BaseFontSize {
static let small: CGFloat = 10
static let body: CGFloat = 14 static let body: CGFloat = 14
static let large: CGFloat = 18
static let title: CGFloat = 24 static let title: CGFloat = 24
} }
enum Opacity {
static let subtle: Double = 0.1
static let hint: Double = 0.2
static let light: Double = 0.3
static let medium: Double = 0.5
static let accent: Double = 0.6
static let strong: Double = 0.7
static let heavy: Double = 0.8
static let almostFull: Double = 0.9
}
enum LineWidth {
static let thin: CGFloat = 1
static let medium: CGFloat = 2
static let thick: CGFloat = 3
}
enum Shadow {
static let radiusSmall: CGFloat = 2
static let radiusMedium: CGFloat = 6
static let radiusLarge: CGFloat = 10
static let offsetSmall: CGFloat = 1
static let offsetMedium: CGFloat = 3
}
enum Animation {
static let quick: Double = 0.3
static let springDuration: Double = 0.4
static let staggerDelay1: Double = 0.1
static let staggerDelay2: Double = 0.25
}
} }
``` ```
- For colors used across the app, extend `Color` with semantic color definitions: - For colors used across the app, extend `Color` with semantic color definitions:
@ -114,18 +187,26 @@ If SwiftData is configured to use CloudKit:
static let background = Color(red: 0.1, green: 0.2, blue: 0.3) static let background = Color(red: 0.1, green: 0.2, blue: 0.3)
static let accent = Color(red: 0.8, green: 0.6, blue: 0.2) static let accent = Color(red: 0.8, green: 0.6, blue: 0.2)
} }
enum Button {
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)
}
} }
``` ```
- Within each view, extract view-specific magic numbers to private constants at the top of the struct: - Within each view, extract view-specific magic numbers to private constants at the top of the struct with a comment explaining why they're local:
```swift ```swift
struct MyView: View { struct MyView: View {
// Layout: fixed card dimensions for consistent appearance
private let cardWidth: CGFloat = 45 private let cardWidth: CGFloat = 45
// Typography: constrained space requires fixed size
private let headerFontSize: CGFloat = 18 private let headerFontSize: CGFloat = 18
// ... // ...
} }
``` ```
- Reference design constants in views: `Design.Spacing.medium`, `Design.CornerRadius.large`, `Color.Primary.accent`. - Reference design constants in views: `Design.Spacing.medium`, `Design.CornerRadius.large`, `Color.Primary.accent`.
- Keep design constants organized by category: Spacing, CornerRadius, BaseFontSize, IconSize, Size, Animation, Opacity, LineWidth, Shadow. - Keep design constants organized by category: Spacing, CornerRadius, BaseFontSize, IconSize, Size, Animation, Opacity, LineWidth, Shadow.
- When adding new features, check existing constants first before creating new ones.
- Name constants semantically (what they represent) not literally (their value): `accent` not `pointSix`, `large` not `sixteen`.
## Dynamic Type instructions ## Dynamic Type instructions

View File

@ -19,6 +19,31 @@ enum GamePhase: Equatable {
case roundComplete case roundComplete
} }
/// Result of an individual bet after a round.
struct BetResult: Identifiable {
let id = UUID()
let type: BetType
let amount: Int
let payout: Int // Net winnings (positive) or loss (negative)
var isWin: Bool { payout > 0 }
var isLoss: Bool { payout < 0 }
var isPush: Bool { payout == 0 }
/// Display name for the bet type
var displayName: String {
switch type {
case .player: return "Player"
case .banker: return "Banker"
case .tie: return "Tie"
case .playerPair: return "P Pair"
case .bankerPair: return "B Pair"
case .dragonBonusPlayer: return "Dragon P"
case .dragonBonusBanker: return "Dragon B"
}
}
}
/// Main observable game state class managing all game logic and UI state. /// Main observable game state class managing all game logic and UI state.
@Observable @Observable
@MainActor @MainActor
@ -38,10 +63,10 @@ final class GameState {
var lastResult: GameResult? var lastResult: GameResult?
var lastWinnings: Int = 0 var lastWinnings: Int = 0
// MARK: - Side Bet Results // MARK: - Bet Results
var playerHadPair: Bool = false var playerHadPair: Bool = false
var bankerHadPair: Bool = false var bankerHadPair: Bool = false
var dragonBonusPayouts: [BetType: Int] = [:] var betResults: [BetResult] = []
// MARK: - Card Display State (for animations) // MARK: - Card Display State (for animations)
var visiblePlayerCards: [Card] = [] var visiblePlayerCards: [Card] = []
@ -224,7 +249,7 @@ final class GameState {
isAnimating = true isAnimating = true
engine.prepareNewRound() engine.prepareNewRound()
// Clear visible cards and side bet results // Clear visible cards and bet results
visiblePlayerCards = [] visiblePlayerCards = []
visibleBankerCards = [] visibleBankerCards = []
playerCardsFaceUp = [] playerCardsFaceUp = []
@ -233,7 +258,7 @@ final class GameState {
showResultBanner = false showResultBanner = false
playerHadPair = false playerHadPair = false
bankerHadPair = false bankerHadPair = false
dragonBonusPayouts = [:] betResults = []
// Deal initial cards // Deal initial cards
currentPhase = .dealingInitial currentPhase = .dealingInitial
@ -331,16 +356,16 @@ final class GameState {
playerHadPair = engine.playerHasPair playerHadPair = engine.playerHasPair
bankerHadPair = engine.bankerHasPair bankerHadPair = engine.bankerHasPair
// Calculate and apply payouts // Calculate and apply payouts, track individual results
var totalWinnings = 0 var totalWinnings = 0
var results: [BetResult] = []
for bet in currentBets { for bet in currentBets {
let payout = engine.calculatePayout(bet: bet, result: result) let payout = engine.calculatePayout(bet: bet, result: result)
totalWinnings += payout totalWinnings += payout
// Track dragon bonus payouts for display // Track individual bet result
if bet.type == .dragonBonusPlayer || bet.type == .dragonBonusBanker { results.append(BetResult(type: bet.type, amount: bet.amount, payout: payout))
dragonBonusPayouts[bet.type] = payout
}
// Return original bet if not a loss // Return original bet if not a loss
if payout >= 0 { if payout >= 0 {
@ -353,6 +378,7 @@ final class GameState {
} }
} }
betResults = results
lastWinnings = totalWinnings lastWinnings = totalWinnings
// Record result in history // Record result in history
@ -385,7 +411,7 @@ final class GameState {
lastWinnings = 0 lastWinnings = 0
playerHadPair = false playerHadPair = false
bankerHadPair = false bankerHadPair = false
dragonBonusPayouts = [:] betResults = []
currentPhase = .betting currentPhase = .betting
} }
@ -415,7 +441,7 @@ final class GameState {
showResultBanner = false showResultBanner = false
playerHadPair = false playerHadPair = false
bankerHadPair = false bankerHadPair = false
dragonBonusPayouts = [:] betResults = []
} }
/// Applies new settings (call after settings change). /// Applies new settings (call after settings change).

View File

@ -40,6 +40,10 @@
"comment" : "A bullet point used to list items in a rule section.", "comment" : "A bullet point used to list items in a rule section.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"+%lld" : {
"comment" : "A text element displaying the total winnings in the round, prefixed by a plus sign. The argument is the total winnings amount.",
"isCommentAutoGenerated" : true
},
"$" : { "$" : {
"comment" : "The currency symbol \"$\".", "comment" : "The currency symbol \"$\".",
"localizations" : { "localizations" : {
@ -114,10 +118,14 @@
} }
} }
}, },
"8 TO 1" : { "8 : 1" : {
"comment" : "The payout ratio for a tie bet.", "comment" : "The payout ratio for a tie bet.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"11 : 1" : {
"comment" : "The payout ratio for a pair bet.",
"isCommentAutoGenerated" : true
},
"11:1" : { "11:1" : {
"comment" : "The payout ratio for a pair bonus bet.", "comment" : "The payout ratio for a pair bonus bet.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -624,17 +632,8 @@
"comment" : "A heading for important information related to a section of a view.", "comment" : "A heading for important information related to a section of a view.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"lostAmountFormat" : {
"comment" : "Format string used to describe the amount lost in a round.",
"isCommentAutoGenerated" : true
},
"M" : {
"comment" : "The letter \"M\" displayed on a mini chip indicator to represent the maximum bet.",
"isCommentAutoGenerated" : true
},
"MAX" : { "MAX" : {
"comment" : "A label displayed as a badge on top-right of a chip to indicate it's the maximum bet.", "comment" : "A label displayed as a badge on top-right of a chip to indicate it's the maximum bet.",
"extractionState" : "stale",
"localizations" : { "localizations" : {
"es" : { "es" : {
"stringUnit" : { "stringUnit" : {
@ -1223,6 +1222,10 @@
"comment" : "A section header for tips related to pair bonuses.", "comment" : "A section header for tips related to pair bonuses.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"TOTAL" : {
"comment" : "A label displayed next to the total winnings in the result banner.",
"isCommentAutoGenerated" : true
},
"WIN" : { "WIN" : {
"comment" : "The text that appears as a badge when a player wins a hand in baccarat.", "comment" : "The text that appears as a badge when a player wins a hand in baccarat.",
"localizations" : { "localizations" : {
@ -1262,10 +1265,6 @@
"comment" : "A description of the player's hand, including its value and whether they won.", "comment" : "A description of the player's hand, including its value and whether they won.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"wonAmountFormat" : {
"comment" : "Format string used to describe the amount won in a localized manner.",
"isCommentAutoGenerated" : true
},
"You've run out of chips!" : { "You've run out of chips!" : {
"comment" : "A message displayed when a player runs out of money in the game over screen.", "comment" : "A message displayed when a player runs out of money in the game over screen.",
"localizations" : { "localizations" : {

View File

@ -70,6 +70,8 @@ enum Design {
static let chipSmall: CGFloat = 36 static let chipSmall: CGFloat = 36
static let chipMedium: CGFloat = 50 static let chipMedium: CGFloat = 50
static let chipSelector: CGFloat = 50 static let chipSelector: CGFloat = 50
static let chipBadge: CGFloat = 32
static let chipBadgeInner: CGFloat = 28
static let cardWidthSmall: CGFloat = 45 static let cardWidthSmall: CGFloat = 45
static let cardWidthMedium: CGFloat = 55 static let cardWidthMedium: CGFloat = 55
static let cardWidthLarge: CGFloat = 65 static let cardWidthLarge: CGFloat = 65
@ -77,6 +79,10 @@ enum Design {
static let checkmark: CGFloat = 22 static let checkmark: CGFloat = 22
static let tableAspectRatio: CGFloat = 1.6 static let tableAspectRatio: CGFloat = 1.6
static let roadMapCell: CGFloat = 16 static let roadMapCell: CGFloat = 16
static let diamondIcon: CGFloat = 24
static let topBetRowHeight: CGFloat = 52
static let mainBetRowHeight: CGFloat = 65
static let bonusZoneWidth: CGFloat = 80
} }
// MARK: - Animation // MARK: - Animation
@ -89,8 +95,9 @@ enum Design {
static let fadeInDuration: Double = 0.3 static let fadeInDuration: Double = 0.3
static let cardFlipDuration: Double = 0.5 static let cardFlipDuration: Double = 0.5
static let selectionDuration: Double = 0.2 static let selectionDuration: Double = 0.2
static let staggerDelay1: Double = 0.2 static let staggerDelay1: Double = 0.1
static let staggerDelay2: Double = 0.4 static let staggerDelay2: Double = 0.25
static let staggerDelay3: Double = 0.4
} }
// MARK: - Opacity // MARK: - Opacity
@ -98,12 +105,15 @@ enum Design {
enum Opacity { enum Opacity {
static let verySubtle: Double = 0.05 static let verySubtle: Double = 0.05
static let subtle: Double = 0.1 static let subtle: Double = 0.1
static let selection: Double = 0.15
static let hint: Double = 0.2 static let hint: Double = 0.2
static let quarter: Double = 0.25
static let light: Double = 0.3 static let light: Double = 0.3
static let overlay: Double = 0.4 static let overlay: Double = 0.4
static let medium: Double = 0.5 static let medium: Double = 0.5
static let secondary: Double = 0.5 static let secondary: Double = 0.5
static let disabled: Double = 0.5 static let disabled: Double = 0.5
static let accent: Double = 0.6
static let strong: Double = 0.7 static let strong: Double = 0.7
static let heavy: Double = 0.8 static let heavy: Double = 0.8
static let nearOpaque: Double = 0.85 static let nearOpaque: Double = 0.85
@ -140,11 +150,15 @@ enum Design {
// MARK: - Shadow // MARK: - Shadow
enum Shadow { enum Shadow {
static let radiusSmall: CGFloat = 3 static let radiusSmall: CGFloat = 2
static let radiusMedium: CGFloat = 6 static let radiusMedium: CGFloat = 6
static let radiusLarge: CGFloat = 10 static let radiusLarge: CGFloat = 10
static let radiusXLarge: CGFloat = 12 static let radiusXLarge: CGFloat = 12
static let radiusXXLarge: CGFloat = 30 static let radiusXXLarge: CGFloat = 30
static let offsetSmall: CGFloat = 1
static let offsetMedium: CGFloat = 3
static let offsetLarge: CGFloat = 5
} }
} }
@ -190,6 +204,10 @@ extension Color {
// Tie (Green) // Tie (Green)
static let tie = Color(red: 0.1, green: 0.45, blue: 0.25) static let tie = Color(red: 0.1, green: 0.45, blue: 0.25)
static let tieMax = Color(red: 0.08, green: 0.32, blue: 0.18) static let tieMax = Color(red: 0.08, green: 0.32, blue: 0.18)
// Dragon Bonus (Purple/Blue gradient)
static let dragonBonusLight = Color(red: 0.25, green: 0.3, blue: 0.45)
static let dragonBonusDark = Color(red: 0.15, green: 0.2, blue: 0.35)
} }
// MARK: - Button Colors // MARK: - Button Colors

View File

@ -32,29 +32,6 @@ struct GameTableView: View {
state.lastResult == .tie state.lastResult == .tie
} }
/// Builds descriptions for side bet wins to display in the result banner.
private func buildSideBetDescriptions(state: GameState) -> [String] {
var descriptions: [String] = []
// Check pair bets
if state.playerHadPair && state.bet(for: .playerPair) != nil {
descriptions.append("Player Pair Win!")
}
if state.bankerHadPair && state.bet(for: .bankerPair) != nil {
descriptions.append("Banker Pair Win!")
}
// Check dragon bonus payouts
if let payout = state.dragonBonusPayouts[.dragonBonusPlayer], payout > 0 {
descriptions.append("Dragon Player +\(payout)")
}
if let payout = state.dragonBonusPayouts[.dragonBonusBanker], payout > 0 {
descriptions.append("Dragon Banker +\(payout)")
}
return descriptions
}
var body: some View { var body: some View {
ZStack { ZStack {
// Table background // Table background
@ -134,10 +111,10 @@ struct GameTableView: View {
if state.showResultBanner, let result = state.lastResult { if state.showResultBanner, let result = state.lastResult {
ResultBannerView( ResultBannerView(
result: result, result: result,
winnings: state.lastWinnings, totalWinnings: state.lastWinnings,
betResults: state.betResults,
playerHadPair: state.playerHadPair, playerHadPair: state.playerHadPair,
bankerHadPair: state.bankerHadPair, bankerHadPair: state.bankerHadPair
sideBetWinnings: buildSideBetDescriptions(state: state)
) )
.transition(.opacity) .transition(.opacity)
@ -238,7 +215,7 @@ struct GameOverView: View {
.padding() .padding()
.background( .background(
RoundedRectangle(cornerRadius: statsCornerRadius) RoundedRectangle(cornerRadius: statsCornerRadius)
.fill(Color.white.opacity(0.08)) .fill(Color.white.opacity(Design.Opacity.subtle))
.overlay( .overlay(
RoundedRectangle(cornerRadius: statsCornerRadius) RoundedRectangle(cornerRadius: statsCornerRadius)
.strokeBorder(Color.white.opacity(Design.Opacity.subtle), lineWidth: Design.LineWidth.thin) .strokeBorder(Color.white.opacity(Design.Opacity.subtle), lineWidth: Design.LineWidth.thin)
@ -288,7 +265,7 @@ struct GameOverView: View {
LinearGradient( LinearGradient(
colors: [ colors: [
Color.red.opacity(Design.Opacity.medium), Color.red.opacity(Design.Opacity.medium),
Color.red.opacity(0.2) Color.red.opacity(Design.Opacity.hint)
], ],
startPoint: .topLeading, startPoint: .topLeading,
endPoint: .bottomTrailing endPoint: .bottomTrailing
@ -297,7 +274,7 @@ struct GameOverView: View {
) )
) )
) )
.shadow(color: .red.opacity(0.2), radius: Design.Shadow.radiusXXLarge) .shadow(color: .red.opacity(Design.Opacity.hint), radius: Design.Shadow.radiusXXLarge)
.padding(.horizontal, Design.Spacing.xxLarge) .padding(.horizontal, Design.Spacing.xxLarge)
.scaleEffect(showContent ? Design.Scale.normal : Design.Scale.slightShrink) .scaleEffect(showContent ? Design.Scale.normal : Design.Scale.slightShrink)
.opacity(showContent ? 1.0 : 0) .opacity(showContent ? 1.0 : 0)
@ -421,7 +398,7 @@ struct CardsDisplayArea: View {
.padding(.horizontal, Design.Spacing.xLarge) .padding(.horizontal, Design.Spacing.xLarge)
.background( .background(
RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge) RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge)
.fill(Color.black.opacity(0.25)) .fill(Color.black.opacity(Design.Opacity.quarter))
.accessibilityHidden(true) .accessibilityHidden(true)
) )
.padding(.horizontal) .padding(.horizontal)
@ -588,7 +565,7 @@ struct TopBarView: View {
HStack(spacing: Design.Spacing.xSmall) { HStack(spacing: Design.Spacing.xSmall) {
Text("$") Text("$")
.font(.system(size: currencyFontSize, weight: .bold)) .font(.system(size: currencyFontSize, weight: .bold))
.foregroundStyle(.yellow.opacity(0.8)) .foregroundStyle(.yellow.opacity(Design.Opacity.heavy))
Text(balance, format: .number) Text(balance, format: .number)
.font(.system(size: balanceFontSize, weight: .black, design: .rounded)) .font(.system(size: balanceFontSize, weight: .black, design: .rounded))
@ -630,7 +607,7 @@ struct TopBarView: View {
Button("Help", systemImage: "info.circle.fill", action: onHelp) Button("Help", systemImage: "info.circle.fill", action: onHelp)
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
.font(.system(size: buttonFontSize)) .font(.system(size: buttonFontSize))
.foregroundStyle(.white.opacity(0.6)) .foregroundStyle(.white.opacity(Design.Opacity.accent))
.padding(Design.Spacing.small) .padding(Design.Spacing.small)
.background( .background(
Circle() Circle()
@ -641,7 +618,7 @@ struct TopBarView: View {
Button("Settings", systemImage: "gearshape.fill", action: onSettings) Button("Settings", systemImage: "gearshape.fill", action: onSettings)
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
.font(.system(size: buttonFontSize)) .font(.system(size: buttonFontSize))
.foregroundStyle(.white.opacity(0.6)) .foregroundStyle(.white.opacity(Design.Opacity.accent))
.padding(Design.Spacing.small) .padding(Design.Spacing.small)
.background( .background(
Circle() Circle()
@ -652,7 +629,7 @@ struct TopBarView: View {
Button("Reset", systemImage: "arrow.counterclockwise", action: onReset) Button("Reset", systemImage: "arrow.counterclockwise", action: onReset)
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
.font(.system(size: buttonFontSize)) .font(.system(size: buttonFontSize))
.foregroundStyle(.white.opacity(0.6)) .foregroundStyle(.white.opacity(Design.Opacity.accent))
.padding(Design.Spacing.small) .padding(Design.Spacing.small)
.background( .background(
Circle() Circle()

View File

@ -81,8 +81,8 @@ struct MiniBaccaratTableView: View {
// Divider // Divider
Rectangle() Rectangle()
.fill(Color.Border.gold.opacity(0.5)) .fill(Color.Border.gold.opacity(Design.Opacity.medium))
.frame(height: 1) .frame(height: Design.LineWidth.thin)
// Middle row: BANKER | BONUS // Middle row: BANKER | BONUS
MainBetRow( MainBetRow(
@ -102,8 +102,8 @@ struct MiniBaccaratTableView: View {
// Divider // Divider
Rectangle() Rectangle()
.fill(Color.Border.gold.opacity(0.5)) .fill(Color.Border.gold.opacity(Design.Opacity.medium))
.frame(height: 1) .frame(height: Design.LineWidth.thin)
// Bottom row: PLAYER | BONUS // Bottom row: PLAYER | BONUS
MainBetRow( MainBetRow(
@ -133,7 +133,7 @@ struct MiniBaccaratTableView: View {
lineWidth: Design.LineWidth.medium lineWidth: Design.LineWidth.medium
) )
) )
.shadow(color: .black.opacity(0.3), radius: 10, y: 5) .shadow(color: .black.opacity(Design.Opacity.light), radius: Design.Shadow.radiusLarge, y: Design.Shadow.offsetLarge)
} }
} }
} }
@ -154,8 +154,6 @@ private struct TopBettingRow: View {
let onTie: () -> Void let onTie: () -> Void
let onPlayerPair: () -> Void let onPlayerPair: () -> Void
private let rowHeight: CGFloat = 52
var body: some View { var body: some View {
HStack(spacing: 0) { HStack(spacing: 0) {
// B PAIR // B PAIR
@ -164,14 +162,14 @@ private struct TopBettingRow: View {
betAmount: bankerPairAmount, betAmount: bankerPairAmount,
isEnabled: canBetBankerPair, isEnabled: canBetBankerPair,
isAtMax: isBankerPairAtMax, isAtMax: isBankerPairAtMax,
color: Color.BettingZone.bankerDark.opacity(0.6), color: Color.BettingZone.bankerDark.opacity(Design.Opacity.accent),
action: onBankerPair action: onBankerPair
) )
// Vertical divider // Vertical divider
Rectangle() Rectangle()
.fill(Color.Border.gold.opacity(0.5)) .fill(Color.Border.gold.opacity(Design.Opacity.medium))
.frame(width: 1) .frame(width: Design.LineWidth.thin)
// TIE // TIE
TieBetZone( TieBetZone(
@ -183,8 +181,8 @@ private struct TopBettingRow: View {
// Vertical divider // Vertical divider
Rectangle() Rectangle()
.fill(Color.Border.gold.opacity(0.5)) .fill(Color.Border.gold.opacity(Design.Opacity.medium))
.frame(width: 1) .frame(width: Design.LineWidth.thin)
// P PAIR // P PAIR
PairBetZone( PairBetZone(
@ -192,11 +190,11 @@ private struct TopBettingRow: View {
betAmount: playerPairAmount, betAmount: playerPairAmount,
isEnabled: canBetPlayerPair, isEnabled: canBetPlayerPair,
isAtMax: isPlayerPairAtMax, isAtMax: isPlayerPairAtMax,
color: Color.BettingZone.playerDark.opacity(0.6), color: Color.BettingZone.playerDark.opacity(Design.Opacity.accent),
action: onPlayerPair action: onPlayerPair
) )
} }
.frame(height: rowHeight) .frame(height: Design.Size.topBetRowHeight)
} }
} }
@ -210,9 +208,6 @@ private struct PairBetZone: View {
let color: Color let color: Color
let action: () -> Void let action: () -> Void
private let titleFontSize: CGFloat = 12
private let payoutFontSize: CGFloat = 10
var body: some View { var body: some View {
Button { Button {
if isEnabled { action() } if isEnabled { action() }
@ -223,25 +218,27 @@ private struct PairBetZone: View {
.fill(color) .fill(color)
// Content // Content
VStack(spacing: 2) { VStack(spacing: Design.Spacing.xxSmall) {
Text(title) Text(title)
.font(.system(size: titleFontSize, weight: .heavy, design: .rounded)) .font(.system(size: Design.BaseFontSize.body, weight: .heavy, design: .rounded))
.foregroundStyle(.yellow) .foregroundStyle(.yellow)
Text("11 : 1") Text("11 : 1")
.font(.system(size: payoutFontSize, weight: .medium, design: .rounded)) .font(.system(size: Design.BaseFontSize.small, weight: .medium, design: .rounded))
.foregroundStyle(.white.opacity(0.7)) .foregroundStyle(.white.opacity(Design.Opacity.strong))
} }
// Chip indicator // Chip indicator - center right with padding
if betAmount > 0 { if betAmount > 0 {
ChipBadge(amount: betAmount, isMax: isAtMax) HStack {
.offset(y: 16) Spacer()
ChipBadge(amount: betAmount, isMax: isAtMax)
.padding(.trailing, Design.Spacing.xSmall + Design.Spacing.xxSmall)
}
} }
} }
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.opacity(isEnabled ? 1.0 : 0.5)
.accessibilityElement(children: .ignore) .accessibilityElement(children: .ignore)
.accessibilityLabel("\(title) bet, pays 11 to 1" + (betAmount > 0 ? ", current bet $\(betAmount)" : "")) .accessibilityLabel("\(title) bet, pays 11 to 1" + (betAmount > 0 ? ", current bet $\(betAmount)" : ""))
.accessibilityAddTraits(.isButton) .accessibilityAddTraits(.isButton)
@ -256,9 +253,6 @@ private struct TieBetZone: View {
let isAtMax: Bool let isAtMax: Bool
let action: () -> Void let action: () -> Void
private let titleFontSize: CGFloat = 14
private let payoutFontSize: CGFloat = 10
var body: some View { var body: some View {
Button { Button {
if isEnabled { action() } if isEnabled { action() }
@ -269,26 +263,28 @@ private struct TieBetZone: View {
.fill(isAtMax ? Color.BettingZone.tieMax : Color.BettingZone.tie) .fill(isAtMax ? Color.BettingZone.tieMax : Color.BettingZone.tie)
// Content // Content
VStack(spacing: 2) { VStack(spacing: Design.Spacing.xxSmall) {
Text("TIE") Text("TIE")
.font(.system(size: titleFontSize, weight: .black, design: .rounded)) .font(.system(size: Design.BaseFontSize.medium, weight: .black, design: .rounded))
.tracking(1) .tracking(1)
Text("8 : 1") Text("8 : 1")
.font(.system(size: payoutFontSize, weight: .medium, design: .rounded)) .font(.system(size: Design.BaseFontSize.small, weight: .medium, design: .rounded))
.opacity(0.7) .opacity(Design.Opacity.strong)
} }
.foregroundStyle(.white) .foregroundStyle(.white)
// Chip indicator // Chip indicator - center right with padding
if betAmount > 0 { if betAmount > 0 {
ChipBadge(amount: betAmount, isMax: isAtMax) HStack {
.offset(y: 16) Spacer()
ChipBadge(amount: betAmount, isMax: isAtMax)
.padding(.trailing, Design.Spacing.xSmall + Design.Spacing.xxSmall)
}
} }
} }
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.opacity(isEnabled ? 1.0 : 0.5)
.accessibilityElement(children: .ignore) .accessibilityElement(children: .ignore)
.accessibilityLabel("Tie bet, pays 8 to 1" + (betAmount > 0 ? ", current bet $\(betAmount)" : "")) .accessibilityLabel("Tie bet, pays 8 to 1" + (betAmount > 0 ? ", current bet $\(betAmount)" : ""))
.accessibilityAddTraits(.isButton) .accessibilityAddTraits(.isButton)
@ -311,9 +307,6 @@ private struct MainBetRow: View {
let onMain: () -> Void let onMain: () -> Void
let onBonus: () -> Void let onBonus: () -> Void
private let rowHeight: CGFloat = 65
private let bonusWidth: CGFloat = 80
var body: some View { var body: some View {
HStack(spacing: 0) { HStack(spacing: 0) {
// Main bet zone (BANKER or PLAYER) // Main bet zone (BANKER or PLAYER)
@ -330,8 +323,8 @@ private struct MainBetRow: View {
// Vertical divider // Vertical divider
Rectangle() Rectangle()
.fill(Color.Border.gold.opacity(0.5)) .fill(Color.Border.gold.opacity(Design.Opacity.medium))
.frame(width: 1) .frame(width: Design.LineWidth.thin)
// Dragon Bonus zone // Dragon Bonus zone
DragonBonusZone( DragonBonusZone(
@ -340,9 +333,9 @@ private struct MainBetRow: View {
isAtMax: isBonusAtMax, isAtMax: isBonusAtMax,
action: onBonus action: onBonus
) )
.frame(width: bonusWidth) .frame(width: Design.Size.bonusZoneWidth)
} }
.frame(height: rowHeight) .frame(height: Design.Size.mainBetRowHeight)
} }
} }
@ -358,9 +351,6 @@ private struct MainBetZone: View {
let color: Color let color: Color
let action: () -> Void let action: () -> Void
private let titleFontSize: CGFloat = 20
private let payoutFontSize: CGFloat = 11
var body: some View { var body: some View {
Button { Button {
if isEnabled { action() } if isEnabled { action() }
@ -373,39 +363,35 @@ private struct MainBetZone: View {
// Selection highlight // Selection highlight
if isSelected { if isSelected {
Rectangle() Rectangle()
.fill(Color.yellow.opacity(0.15)) .fill(Color.yellow.opacity(Design.Opacity.selection))
Rectangle() Rectangle()
.strokeBorder(Color.yellow, lineWidth: 3) .strokeBorder(Color.yellow, lineWidth: Design.LineWidth.thick)
} }
// Content // Content - always centered
HStack { VStack(spacing: Design.Spacing.xxSmall + 1) {
Spacer() Text(title)
.font(.system(size: Design.BaseFontSize.xxLarge, weight: .black, design: .rounded))
.tracking(2)
VStack(spacing: 3) { Text(payoutText)
Text(title) .font(.system(size: Design.BaseFontSize.callout - 2, weight: .semibold, design: .rounded))
.font(.system(size: titleFontSize, weight: .black, design: .rounded)) .opacity(Design.Opacity.strong)
.tracking(2) }
.foregroundStyle(.white)
Text(payoutText) // Chip indicator - overlaid on right, doesn't affect centering
.font(.system(size: payoutFontSize, weight: .semibold, design: .rounded)) if betAmount > 0 {
.opacity(0.7) HStack {
} Spacer()
.foregroundStyle(.white)
Spacer()
// Chip indicator
if betAmount > 0 {
ChipOnTableView(amount: betAmount, showMax: isAtMax) ChipOnTableView(amount: betAmount, showMax: isAtMax)
.padding(.trailing, Design.Spacing.medium) .padding(.trailing, Design.Spacing.small)
} }
} }
} }
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.opacity(isEnabled ? 1.0 : 0.5)
.accessibilityElement(children: .ignore) .accessibilityElement(children: .ignore)
.accessibilityLabel("\(title) bet, pays \(payoutText)" + (isSelected ? ", selected" : "") + (betAmount > 0 ? ", current bet $\(betAmount)" : "")) .accessibilityLabel("\(title) bet, pays \(payoutText)" + (isSelected ? ", selected" : "") + (betAmount > 0 ? ", current bet $\(betAmount)" : ""))
.accessibilityAddTraits(.isButton) .accessibilityAddTraits(.isButton)
@ -420,9 +406,6 @@ private struct DragonBonusZone: View {
let isAtMax: Bool let isAtMax: Bool
let action: () -> Void let action: () -> Void
private let titleFontSize: CGFloat = 9
private let diamondSize: CGFloat = 24
var body: some View { var body: some View {
Button { Button {
if isEnabled { action() } if isEnabled { action() }
@ -433,8 +416,8 @@ private struct DragonBonusZone: View {
.fill( .fill(
LinearGradient( LinearGradient(
colors: [ colors: [
Color(red: 0.25, green: 0.3, blue: 0.45), Color.BettingZone.dragonBonusLight,
Color(red: 0.15, green: 0.2, blue: 0.35) Color.BettingZone.dragonBonusDark
], ],
startPoint: .top, startPoint: .top,
endPoint: .bottom endPoint: .bottom
@ -442,36 +425,38 @@ private struct DragonBonusZone: View {
) )
// Content // Content
VStack(spacing: 5) { VStack(spacing: Design.Spacing.xSmall) {
// Diamond shape // Diamond shape
DiamondShape() DiamondShape()
.fill( .fill(
LinearGradient( LinearGradient(
colors: [Color.purple.opacity(0.8), Color.purple.opacity(0.5)], colors: [Color.purple.opacity(Design.Opacity.heavy), Color.purple.opacity(Design.Opacity.medium)],
startPoint: .top, startPoint: .top,
endPoint: .bottom endPoint: .bottom
) )
) )
.frame(width: diamondSize, height: diamondSize) .frame(width: Design.Size.diamondIcon, height: Design.Size.diamondIcon)
.overlay( .overlay(
DiamondShape() DiamondShape()
.strokeBorder(Color.white.opacity(0.4), lineWidth: 1) .strokeBorder(Color.white.opacity(Design.Opacity.overlay), lineWidth: Design.LineWidth.thin)
) )
Text("BONUS") Text("BONUS")
.font(.system(size: titleFontSize, weight: .bold, design: .rounded)) .font(.system(size: Design.BaseFontSize.xSmall, weight: .bold, design: .rounded))
.foregroundStyle(.white.opacity(0.9)) .foregroundStyle(.white.opacity(Design.Opacity.almostFull))
} }
// Chip indicator // Chip indicator - center right with padding (same as top row)
if betAmount > 0 { if betAmount > 0 {
ChipBadge(amount: betAmount, isMax: isAtMax) HStack {
.offset(y: 22) Spacer()
ChipBadge(amount: betAmount, isMax: isAtMax)
.padding(.trailing, Design.Spacing.xSmall + Design.Spacing.xxSmall)
}
} }
} }
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.opacity(isEnabled ? 1.0 : 0.5)
.accessibilityElement(children: .ignore) .accessibilityElement(children: .ignore)
.accessibilityLabel("Dragon Bonus, pays up to 30 to 1" + (betAmount > 0 ? ", current bet $\(betAmount)" : "")) .accessibilityLabel("Dragon Bonus, pays up to 30 to 1" + (betAmount > 0 ? ", current bet $\(betAmount)" : ""))
.accessibilityAddTraits(.isButton) .accessibilityAddTraits(.isButton)
@ -503,7 +488,7 @@ private struct DiamondShape: InsettableShape {
} }
} }
// MARK: - Chip Badge (small indicator) // MARK: - Chip Badge (indicator for side bets)
private struct ChipBadge: View { private struct ChipBadge: View {
let amount: Int let amount: Int
@ -511,24 +496,28 @@ private struct ChipBadge: View {
var body: some View { var body: some View {
ZStack { ZStack {
// Outer ring
Circle() Circle()
.fill(isMax ? Color.gray : Color.yellow) .fill(isMax ? Color.gray : Color.yellow)
.frame(width: 20, height: 20) .frame(width: Design.Size.chipBadge, height: Design.Size.chipBadge)
// Inner decoration
Circle() Circle()
.strokeBorder(Color.white.opacity(0.8), lineWidth: 1) .strokeBorder(Color.white.opacity(Design.Opacity.almostFull), lineWidth: Design.LineWidth.standard)
.frame(width: 20, height: 20) .frame(width: Design.Size.chipBadgeInner, height: Design.Size.chipBadgeInner)
// Text
if isMax { if isMax {
Text("M") Text("MAX")
.font(.system(size: 9, weight: .bold)) .font(.system(size: Design.BaseFontSize.xSmall, weight: .black))
.foregroundStyle(.white) .foregroundStyle(.white)
} else { } else {
Text(formatCompact(amount)) Text(formatCompact(amount))
.font(.system(size: 7, weight: .bold)) .font(.system(size: Design.BaseFontSize.body, weight: .bold, design: .rounded))
.foregroundStyle(.black) .foregroundStyle(.black)
} }
} }
.shadow(color: .black.opacity(Design.Opacity.light), radius: Design.Shadow.radiusSmall, y: Design.Shadow.offsetSmall)
} }
private func formatCompact(_ value: Int) -> String { private func formatCompact(_ value: Int) -> String {

View File

@ -2,30 +2,44 @@
// ResultBannerView.swift // ResultBannerView.swift
// Baccarat // Baccarat
// //
// Animated result banner showing the winner and winnings. // Animated result banner showing the winner and itemized bet results.
// //
import SwiftUI import SwiftUI
import CasinoKit import CasinoKit
/// An animated banner showing the round result. /// An animated banner showing the round result with bet breakdown.
struct ResultBannerView: View { struct ResultBannerView: View {
let result: GameResult let result: GameResult
let winnings: Int let totalWinnings: Int
let betResults: [BetResult]
var playerHadPair: Bool = false var playerHadPair: Bool = false
var bankerHadPair: Bool = false var bankerHadPair: Bool = false
var sideBetWinnings: [String] = [] // List of side bet win descriptions
@State private var showBanner = false @State private var showBanner = false
@State private var showText = false @State private var showText = false
@State private var showWinnings = false @State private var showBreakdown = false
@State private var showSideBets = false @State private var showTotal = false
// MARK: - Scaled Font Sizes (Dynamic Type) // MARK: - Scaled Font Sizes (Dynamic Type)
@ScaledMetric(relativeTo: .largeTitle) private var resultFontSize: CGFloat = Design.BaseFontSize.largeTitle @ScaledMetric(relativeTo: .largeTitle) private var resultFontSize: CGFloat = Design.BaseFontSize.largeTitle
@ScaledMetric(relativeTo: .title2) private var winningsFontSize: CGFloat = 28 @ScaledMetric(relativeTo: .title3) private var totalFontSize: CGFloat = Design.BaseFontSize.xxLarge + Design.Spacing.xSmall
@ScaledMetric(relativeTo: .body) private var sideBetFontSize: CGFloat = 14 @ScaledMetric(relativeTo: .body) private var itemFontSize: CGFloat = Design.BaseFontSize.medium
// MARK: - Computed Properties
private var winningBets: [BetResult] {
betResults.filter { $0.isWin }
}
private var losingBets: [BetResult] {
betResults.filter { $0.isLoss }
}
private var pushBets: [BetResult] {
betResults.filter { $0.isPush }
}
var body: some View { var body: some View {
ZStack { ZStack {
@ -60,47 +74,47 @@ struct ResultBannerView: View {
PairBadge(label: "B PAIR", color: .red) PairBadge(label: "B PAIR", color: .red)
} }
} }
.scaleEffect(showSideBets ? Design.Scale.normal : Design.Scale.shrunk) .scaleEffect(showBreakdown ? Design.Scale.normal : Design.Scale.shrunk)
.opacity(showSideBets ? Design.Scale.normal : 0) .opacity(showBreakdown ? Design.Scale.normal : 0)
} }
// Winnings display // Bet breakdown
if winnings != 0 { if !betResults.isEmpty {
BetBreakdownView(
winningBets: winningBets,
losingBets: losingBets,
pushBets: pushBets,
fontSize: itemFontSize
)
.scaleEffect(showBreakdown ? Design.Scale.normal : Design.Scale.shrunk)
.opacity(showBreakdown ? Design.Scale.normal : 0)
}
// Total
if totalWinnings != 0 {
HStack(spacing: Design.Spacing.small) { HStack(spacing: Design.Spacing.small) {
if winnings > 0 { Text("TOTAL")
Image(systemName: "plus.circle.fill") .font(.system(size: Design.BaseFontSize.body, weight: .bold))
.foregroundStyle(.green) .foregroundStyle(.white.opacity(Design.Opacity.accent))
Text("\(winnings)")
if totalWinnings > 0 {
Text("+\(totalWinnings)")
.font(.system(size: totalFontSize, weight: .black, design: .rounded))
.foregroundStyle(.green) .foregroundStyle(.green)
} else { } else {
Image(systemName: "minus.circle.fill") Text("\(totalWinnings)")
.foregroundStyle(.red) .font(.system(size: totalFontSize, weight: .black, design: .rounded))
Text("\(abs(winnings))")
.foregroundStyle(.red) .foregroundStyle(.red)
} }
} }
.font(.system(size: winningsFontSize, weight: .bold, design: .rounded)) .scaleEffect(showTotal ? Design.Scale.normal : Design.Scale.shrunk)
.scaleEffect(showWinnings ? Design.Scale.normal : Design.Scale.shrunk) .opacity(showTotal ? Design.Scale.normal : 0)
.opacity(showWinnings ? Design.Scale.normal : 0)
}
// Side bet win descriptions
if !sideBetWinnings.isEmpty {
VStack(spacing: Design.Spacing.xSmall) {
ForEach(sideBetWinnings, id: \.self) { description in
Text(description)
.font(.system(size: sideBetFontSize, weight: .semibold))
.foregroundStyle(.yellow)
}
}
.padding(.top, Design.Spacing.xSmall)
.scaleEffect(showSideBets ? Design.Scale.normal : Design.Scale.shrunk)
.opacity(showSideBets ? Design.Scale.normal : 0)
} }
} }
.padding(Design.Spacing.xxxLarge + Design.Spacing.small) .padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.xxLarge)
.background( .background(
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge + Design.Spacing.xSmall) RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge)
.fill( .fill(
LinearGradient( LinearGradient(
colors: [ colors: [
@ -112,7 +126,7 @@ struct ResultBannerView: View {
) )
) )
.overlay( .overlay(
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge + Design.Spacing.xSmall) RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge)
.strokeBorder( .strokeBorder(
LinearGradient( LinearGradient(
colors: [ colors: [
@ -140,11 +154,11 @@ struct ResultBannerView: View {
} }
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce).delay(Design.Animation.staggerDelay2)) { withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce).delay(Design.Animation.staggerDelay2)) {
showWinnings = true showBreakdown = true
} }
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce).delay(Design.Animation.staggerDelay2 + 0.1)) { withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce).delay(Design.Animation.staggerDelay3)) {
showSideBets = true showTotal = true
} }
// Announce result to VoiceOver users // Announce result to VoiceOver users
@ -168,25 +182,25 @@ struct ResultBannerView: View {
description += ". Banker pair" description += ". Banker pair"
} }
// Add winnings // Add bet results
if winnings > 0 { for bet in winningBets {
let format = String(localized: "wonAmountFormat") description += ". \(bet.displayName) won \(bet.payout)"
description += ". " + String(format: format, winnings.formatted()) }
} else if winnings < 0 { for bet in losingBets {
let format = String(localized: "lostAmountFormat") description += ". \(bet.displayName) lost \(abs(bet.payout))"
description += ". " + String(format: format, abs(winnings).formatted())
} }
// Add side bet descriptions // Add total
for sideBet in sideBetWinnings { if totalWinnings > 0 {
description += ". \(sideBet)" description += ". Total winnings: \(totalWinnings)"
} else if totalWinnings < 0 {
description += ". Total loss: \(abs(totalWinnings))"
} }
return description return description
} }
private func announceResult() { private func announceResult() {
// Post accessibility announcement for screen reader users
Task { @MainActor in Task { @MainActor in
try? await Task.sleep(for: .milliseconds(500)) try? await Task.sleep(for: .milliseconds(500))
AccessibilityNotification.Announcement(accessibilityDescription).post() AccessibilityNotification.Announcement(accessibilityDescription).post()
@ -194,14 +208,95 @@ struct ResultBannerView: View {
} }
} }
/// A small badge showing pair result. // MARK: - Bet Breakdown View
private struct BetBreakdownView: View {
let winningBets: [BetResult]
let losingBets: [BetResult]
let pushBets: [BetResult]
let fontSize: CGFloat
var body: some View {
VStack(spacing: Design.Spacing.xSmall) {
// Winning bets
ForEach(winningBets) { bet in
BetResultRow(bet: bet, fontSize: fontSize)
}
// Push bets
ForEach(pushBets) { bet in
BetResultRow(bet: bet, fontSize: fontSize)
}
// Losing bets
ForEach(losingBets) { bet in
BetResultRow(bet: bet, fontSize: fontSize)
}
}
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(Color.white.opacity(Design.Opacity.verySubtle))
)
}
}
// MARK: - Bet Result Row
private struct BetResultRow: View {
let bet: BetResult
let fontSize: CGFloat
private var statusColor: Color {
if bet.isWin { return .green }
if bet.isLoss { return .red }
return .yellow // Push
}
private var statusIcon: String {
if bet.isWin { return "checkmark.circle.fill" }
if bet.isLoss { return "xmark.circle.fill" }
return "arrow.left.arrow.right.circle.fill" // Push
}
private var payoutText: String {
if bet.isWin { return "+\(bet.payout)" }
if bet.isLoss { return "\(bet.payout)" }
return "PUSH"
}
var body: some View {
HStack(spacing: Design.Spacing.small) {
// Status icon
Image(systemName: statusIcon)
.font(.system(size: fontSize))
.foregroundStyle(statusColor)
// Bet name
Text(bet.displayName)
.font(.system(size: fontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.heavy))
Spacer()
// Payout
Text(payoutText)
.font(.system(size: fontSize, weight: .bold, design: .rounded))
.foregroundStyle(statusColor)
}
}
}
// MARK: - Pair Badge
private struct PairBadge: View { private struct PairBadge: View {
let label: String let label: String
let color: Color let color: Color
var body: some View { var body: some View {
Text(label) Text(label)
.font(.system(size: 11, weight: .bold)) .font(.system(size: Design.BaseFontSize.callout - Design.Spacing.xxSmall, weight: .bold))
.foregroundStyle(.white) .foregroundStyle(.white)
.padding(.horizontal, Design.Spacing.small) .padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xxSmall) .padding(.vertical, Design.Spacing.xxSmall)
@ -257,7 +352,7 @@ struct ConfettiView: View {
} }
} }
.allowsHitTesting(false) .allowsHitTesting(false)
.accessibilityHidden(true) // Decorative element .accessibilityHidden(true)
} }
} }
@ -268,10 +363,15 @@ struct ConfettiView: View {
ResultBannerView( ResultBannerView(
result: .playerWins, result: .playerWins,
winnings: 500, totalWinnings: 1500,
betResults: [
BetResult(type: .player, amount: 1000, payout: 1000),
BetResult(type: .playerPair, amount: 100, payout: 1100),
BetResult(type: .dragonBonusPlayer, amount: 100, payout: 200),
BetResult(type: .tie, amount: 500, payout: -500)
],
playerHadPair: true, playerHadPair: true,
bankerHadPair: false, bankerHadPair: false
sideBetWinnings: ["Dragon Bonus +300"]
) )
} }
} }

View File

@ -36,7 +36,7 @@ struct RoadMapView: View {
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
Text("HISTORY") Text("HISTORY")
.font(.system(size: historyFontSize, weight: .bold, design: .rounded)) .font(.system(size: historyFontSize, weight: .bold, design: .rounded))
.foregroundStyle(.white.opacity(0.6)) .foregroundStyle(.white.opacity(Design.Opacity.accent))
.tracking(1) .tracking(1)
ScrollView(.horizontal) { ScrollView(.horizontal) {

View File

@ -43,7 +43,7 @@ struct RulesHelpView: View {
var body: some View { var body: some View {
ZStack { ZStack {
// Background // Background
Color.black.opacity(0.9) Color.black.opacity(Design.Opacity.almostFull)
.ignoresSafeArea() .ignoresSafeArea()
VStack(spacing: Design.Spacing.medium) { VStack(spacing: Design.Spacing.medium) {
@ -72,10 +72,10 @@ struct RulesHelpView: View {
Image(systemName: "suit.heart.fill") Image(systemName: "suit.heart.fill")
.foregroundStyle(.red) .foregroundStyle(.red)
} }
.font(.system(size: 32)) .font(.system(size: Design.BaseFontSize.title))
Text("BACCARAT") Text("BACCARAT")
.font(.system(size: 28, weight: .black, design: .rounded)) .font(.system(size: Design.BaseFontSize.title - Design.Spacing.xSmall, weight: .black, design: .rounded))
.foregroundStyle( .foregroundStyle(
LinearGradient( LinearGradient(
colors: [.yellow, .orange], colors: [.yellow, .orange],
@ -92,7 +92,7 @@ struct RulesHelpView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
// Page title // Page title
Text(currentPage.title) Text(currentPage.title)
.font(.system(size: 22, weight: .bold, design: .rounded)) .font(.system(size: Design.BaseFontSize.xxLarge + Design.Spacing.xxSmall, weight: .bold, design: .rounded))
.foregroundStyle(.yellow) .foregroundStyle(.yellow)
.padding(.top, Design.Spacing.large) .padding(.top, Design.Spacing.large)
.padding(.bottom, Design.Spacing.medium) .padding(.bottom, Design.Spacing.medium)
@ -130,13 +130,13 @@ struct RulesHelpView: View {
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
// Previous button // Previous button
Button { Button {
withAnimation(.spring(duration: 0.3)) { withAnimation(.spring(duration: Design.Animation.quick)) {
goToPreviousPage() goToPreviousPage()
} }
} label: { } label: {
Image(systemName: "chevron.left.circle.fill") Image(systemName: "chevron.left.circle.fill")
.font(.system(size: 36)) .font(.system(size: Design.BaseFontSize.largeTitle))
.foregroundStyle(currentPage.rawValue > 0 ? .yellow : .gray.opacity(0.5)) .foregroundStyle(currentPage.rawValue > 0 ? .yellow : .gray.opacity(Design.Opacity.medium))
} }
.disabled(currentPage.rawValue == 0) .disabled(currentPage.rawValue == 0)
@ -145,7 +145,7 @@ struct RulesHelpView: View {
dismiss() dismiss()
} label: { } label: {
Text("BACK TO GAME") Text("BACK TO GAME")
.font(.system(size: 16, weight: .bold)) .font(.system(size: Design.BaseFontSize.large, weight: .bold))
.foregroundStyle(.black) .foregroundStyle(.black)
.padding(.horizontal, Design.Spacing.xLarge) .padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.medium) .padding(.vertical, Design.Spacing.medium)
@ -163,13 +163,13 @@ struct RulesHelpView: View {
// Next button // Next button
Button { Button {
withAnimation(.spring(duration: 0.3)) { withAnimation(.spring(duration: Design.Animation.quick)) {
goToNextPage() goToNextPage()
} }
} label: { } label: {
Image(systemName: "chevron.right.circle.fill") Image(systemName: "chevron.right.circle.fill")
.font(.system(size: 36)) .font(.system(size: Design.BaseFontSize.largeTitle))
.foregroundStyle(currentPage.rawValue < RulesPage.allCases.count - 1 ? .green : .gray.opacity(0.5)) .foregroundStyle(currentPage.rawValue < RulesPage.allCases.count - 1 ? .green : .gray.opacity(Design.Opacity.medium))
} }
.disabled(currentPage.rawValue >= RulesPage.allCases.count - 1) .disabled(currentPage.rawValue >= RulesPage.allCases.count - 1)
} }
@ -204,10 +204,10 @@ private struct BasicRulesContent: View {
]) ])
Divider() Divider()
.background(Color.white.opacity(0.3)) .background(Color.white.opacity(Design.Opacity.light))
Text("Card Values") Text("Card Values")
.font(.system(size: 18, weight: .bold)) .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white) .foregroundStyle(.white)
RuleSection(items: [ RuleSection(items: [
@ -219,10 +219,10 @@ private struct BasicRulesContent: View {
RuleSection(text: "Hand values are the sum of cards, keeping only the last digit. For example: 7 + 8 = 15, so the hand value is 5.") RuleSection(text: "Hand values are the sum of cards, keeping only the last digit. For example: 7 + 8 = 15, so the hand value is 5.")
Divider() Divider()
.background(Color.white.opacity(0.3)) .background(Color.white.opacity(Design.Opacity.light))
Text("Natural Win") Text("Natural Win")
.font(.system(size: 18, weight: .bold)) .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white) .foregroundStyle(.white)
RuleSection(text: "If either hand totals 8 or 9 with the first two cards, it's a \"Natural\" and the round ends immediately.") RuleSection(text: "If either hand totals 8 or 9 with the first two cards, it's a \"Natural\" and the round ends immediately.")
@ -238,10 +238,10 @@ private struct ThirdCardRulesContent: View {
RuleSection(text: "If neither hand has a Natural, additional cards may be drawn according to fixed rules.") RuleSection(text: "If neither hand has a Natural, additional cards may be drawn according to fixed rules.")
Divider() Divider()
.background(Color.white.opacity(0.3)) .background(Color.white.opacity(Design.Opacity.light))
Text("Player Rules") Text("Player Rules")
.font(.system(size: 18, weight: .bold)) .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white) .foregroundStyle(.white)
RuleSection(items: [ RuleSection(items: [
@ -250,10 +250,10 @@ private struct ThirdCardRulesContent: View {
]) ])
Divider() Divider()
.background(Color.white.opacity(0.3)) .background(Color.white.opacity(Design.Opacity.light))
Text("Banker Rules") Text("Banker Rules")
.font(.system(size: 18, weight: .bold)) .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white) .foregroundStyle(.white)
RuleSection(text: "If Player stood (6-7), Banker draws on 0-5 and stands on 6-7.") RuleSection(text: "If Player stood (6-7), Banker draws on 0-5 and stands on 6-7.")
@ -271,7 +271,7 @@ private struct ThirdCardRulesContent: View {
.padding() .padding()
.background( .background(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium) RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(Color.black.opacity(0.2)) .fill(Color.black.opacity(Design.Opacity.hint))
) )
} }
} }
@ -281,16 +281,18 @@ private struct BankerRuleRow: View {
let bankerTotal: String let bankerTotal: String
let action: String let action: String
private let labelWidth: CGFloat = 80
var body: some View { var body: some View {
HStack { HStack {
Text("Banker \(bankerTotal):") Text("Banker \(bankerTotal):")
.font(.system(size: 13, weight: .semibold)) .font(.system(size: Design.BaseFontSize.callout, weight: .semibold))
.foregroundStyle(.yellow) .foregroundStyle(.yellow)
.frame(width: 80, alignment: .leading) .frame(width: labelWidth, alignment: .leading)
Text(action) Text(action)
.font(.system(size: 13)) .font(.system(size: Design.BaseFontSize.callout))
.foregroundStyle(.white.opacity(0.9)) .foregroundStyle(.white.opacity(Design.Opacity.almostFull))
} }
} }
} }
@ -303,10 +305,10 @@ private struct DragonBonusContent: View {
RuleSection(text: "The Dragon Bonus is a side bet available for both Player and Banker. It pays based on how the winning hand wins.") RuleSection(text: "The Dragon Bonus is a side bet available for both Player and Banker. It pays based on how the winning hand wins.")
Divider() Divider()
.background(Color.white.opacity(0.3)) .background(Color.white.opacity(Design.Opacity.light))
Text("Payout Table") Text("Payout Table")
.font(.system(size: 18, weight: .bold)) .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white) .foregroundStyle(.white)
VStack(spacing: Design.Spacing.xSmall) { VStack(spacing: Design.Spacing.xSmall) {
@ -321,14 +323,14 @@ private struct DragonBonusContent: View {
.padding() .padding()
.background( .background(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium) RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(Color.black.opacity(0.2)) .fill(Color.black.opacity(Design.Opacity.hint))
) )
Divider() Divider()
.background(Color.white.opacity(0.3)) .background(Color.white.opacity(Design.Opacity.light))
Text("Important") Text("Important")
.font(.system(size: 18, weight: .bold)) .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white) .foregroundStyle(.white)
RuleSection(items: [ RuleSection(items: [
@ -347,13 +349,13 @@ private struct PayoutRow: View {
var body: some View { var body: some View {
HStack { HStack {
Text(condition) Text(condition)
.font(.system(size: 14)) .font(.system(size: Design.BaseFontSize.medium))
.foregroundStyle(.white.opacity(0.9)) .foregroundStyle(.white.opacity(Design.Opacity.almostFull))
Spacer() Spacer()
Text(payout) Text(payout)
.font(.system(size: 14, weight: .bold)) .font(.system(size: Design.BaseFontSize.medium, weight: .bold))
.foregroundStyle(.yellow) .foregroundStyle(.yellow)
} }
.padding(.vertical, Design.Spacing.xxSmall) .padding(.vertical, Design.Spacing.xxSmall)
@ -368,16 +370,16 @@ private struct PairBonusContent: View {
RuleSection(text: "Pair Bonus bets are available for both Player and Banker. They pay when the first two cards dealt to that hand form a pair.") RuleSection(text: "Pair Bonus bets are available for both Player and Banker. They pay when the first two cards dealt to that hand form a pair.")
Divider() Divider()
.background(Color.white.opacity(0.3)) .background(Color.white.opacity(Design.Opacity.light))
Text("Payout") Text("Payout")
.font(.system(size: 18, weight: .bold)) .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white) .foregroundStyle(.white)
HStack { HStack {
VStack { VStack {
Text("11:1") Text("11:1")
.font(.system(size: 48, weight: .black, design: .rounded)) .font(.system(size: Design.BaseFontSize.largeTitle + Design.Spacing.medium, weight: .black, design: .rounded))
.foregroundStyle( .foregroundStyle(
LinearGradient( LinearGradient(
colors: [.yellow, .orange], colors: [.yellow, .orange],
@ -387,18 +389,18 @@ private struct PairBonusContent: View {
) )
Text("Pair Pays") Text("Pair Pays")
.font(.system(size: 14, weight: .medium)) .font(.system(size: Design.BaseFontSize.medium, weight: .medium))
.foregroundStyle(.white.opacity(0.7)) .foregroundStyle(.white.opacity(Design.Opacity.strong))
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
.padding(.vertical, Design.Spacing.medium) .padding(.vertical, Design.Spacing.medium)
Divider() Divider()
.background(Color.white.opacity(0.3)) .background(Color.white.opacity(Design.Opacity.light))
Text("Examples") Text("Examples")
.font(.system(size: 18, weight: .bold)) .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white) .foregroundStyle(.white)
RuleSection(items: [ RuleSection(items: [
@ -410,10 +412,10 @@ private struct PairBonusContent: View {
RuleSection(text: "Note: Suits are disregarded. Only the rank matters for a pair.") RuleSection(text: "Note: Suits are disregarded. Only the rank matters for a pair.")
Divider() Divider()
.background(Color.white.opacity(0.3)) .background(Color.white.opacity(Design.Opacity.light))
Text("Tips") Text("Tips")
.font(.system(size: 18, weight: .bold)) .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white) .foregroundStyle(.white)
RuleSection(items: [ RuleSection(items: [
@ -436,14 +438,14 @@ private struct RuleSection: View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
if let title = title { if let title = title {
Text(title) Text(title)
.font(.system(size: 16, weight: .semibold)) .font(.system(size: Design.BaseFontSize.large, weight: .semibold))
.foregroundStyle(.yellow) .foregroundStyle(.yellow)
} }
if let text = text { if let text = text {
Text(text) Text(text)
.font(.system(size: 14)) .font(.system(size: Design.BaseFontSize.medium))
.foregroundStyle(.white.opacity(0.9)) .foregroundStyle(.white.opacity(Design.Opacity.almostFull))
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
} }
@ -453,9 +455,9 @@ private struct RuleSection: View {
Text("") Text("")
.foregroundStyle(.yellow) .foregroundStyle(.yellow)
Text(item) Text(item)
.foregroundStyle(.white.opacity(0.9)) .foregroundStyle(.white.opacity(Design.Opacity.almostFull))
} }
.font(.system(size: 14)) .font(.system(size: Design.BaseFontSize.medium))
} }
} }
} }

View File

@ -118,7 +118,7 @@ struct SettingsView: View {
settings.load() // Revert changes settings.load() // Revert changes
dismiss() dismiss()
} }
.foregroundStyle(.white.opacity(0.7)) .foregroundStyle(.white.opacity(Design.Opacity.strong))
} }
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
@ -154,7 +154,7 @@ struct SettingsSection<Content: View>: View {
Text(title) Text(title)
.font(.system(size: Design.BaseFontSize.body, weight: .bold, design: .rounded)) .font(.system(size: Design.BaseFontSize.body, weight: .bold, design: .rounded))
.tracking(1) .tracking(1)
.foregroundStyle(.white.opacity(0.6)) .foregroundStyle(.white.opacity(Design.Opacity.accent))
} }
.padding(.horizontal, Design.Spacing.xSmall) .padding(.horizontal, Design.Spacing.xSmall)
@ -165,7 +165,7 @@ struct SettingsSection<Content: View>: View {
.padding() .padding()
.background( .background(
RoundedRectangle(cornerRadius: Design.CornerRadius.large) RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.fill(Color.white.opacity(0.05)) .fill(Color.white.opacity(Design.Opacity.verySubtle))
) )
} }
.padding(.horizontal) .padding(.horizontal)
@ -347,7 +347,7 @@ struct TableLimitsPicker: View {
.padding(.vertical, Design.Spacing.xSmall) .padding(.vertical, Design.Spacing.xSmall)
.background( .background(
Capsule() Capsule()
.fill(selection == limit ? Color.yellow : Color.yellow.opacity(0.2)) .fill(selection == limit ? Color.yellow : Color.yellow.opacity(Design.Opacity.hint))
) )
if selection == limit { if selection == limit {

View File

@ -49,6 +49,10 @@ public enum CasinoDesign {
public static let radiusSmall: CGFloat = 4 public static let radiusSmall: CGFloat = 4
public static let radiusMedium: CGFloat = 8 public static let radiusMedium: CGFloat = 8
public static let radiusLarge: CGFloat = 12 public static let radiusLarge: CGFloat = 12
public static let offsetSmall: CGFloat = 1
public static let offsetMedium: CGFloat = 2
public static let offsetLarge: CGFloat = 3
} }
// MARK: - Opacity // MARK: - Opacity
@ -56,7 +60,10 @@ public enum CasinoDesign {
public enum Opacity { public enum Opacity {
public static let subtle: CGFloat = 0.05 public static let subtle: CGFloat = 0.05
public static let light: CGFloat = 0.2 public static let light: CGFloat = 0.2
public static let quarter: CGFloat = 0.25
public static let medium: CGFloat = 0.5 public static let medium: CGFloat = 0.5
public static let accent: CGFloat = 0.6
public static let strong: CGFloat = 0.7
public static let heavy: CGFloat = 0.8 public static let heavy: CGFloat = 0.8
public static let nearOpaque: CGFloat = 0.95 public static let nearOpaque: CGFloat = 0.95
} }
@ -83,6 +90,16 @@ public enum CasinoDesign {
/// Card aspect ratio (height = width * this value). /// Card aspect ratio (height = width * this value).
public static let cardAspectRatio: CGFloat = 1.4 public static let cardAspectRatio: CGFloat = 1.4
/// Pattern dimensions for decorative elements.
public static let patternSpacing: CGFloat = 12
public static let patternDiamondSize: CGFloat = 6
public static let dashLength: CGFloat = 8
public static let dashGap: CGFloat = 4
/// Chip edge stripe dimensions.
public static let chipStripeWidth: CGFloat = 4
public static let chipStripeInset: CGFloat = 2
} }
// MARK: - Font Sizes (Base values for @ScaledMetric) // MARK: - Font Sizes (Base values for @ScaledMetric)

View File

@ -150,8 +150,8 @@ public struct CardFrontView: View {
.shadow( .shadow(
color: .black.opacity(CasinoDesign.Opacity.light), color: .black.opacity(CasinoDesign.Opacity.light),
radius: CasinoDesign.Shadow.radiusSmall, radius: CasinoDesign.Shadow.radiusSmall,
x: 2, x: CasinoDesign.Shadow.offsetMedium,
y: 2 y: CasinoDesign.Shadow.offsetMedium
) )
} }
} }
@ -210,8 +210,8 @@ public struct CardBackView: View {
.fill( .fill(
LinearGradient( LinearGradient(
colors: [ colors: [
theme.cardBackPrimaryColor.opacity(0.8), theme.cardBackPrimaryColor.opacity(CasinoDesign.Opacity.heavy),
theme.cardBackSecondaryColor.opacity(0.8) theme.cardBackSecondaryColor.opacity(CasinoDesign.Opacity.heavy)
], ],
startPoint: .top, startPoint: .top,
endPoint: .bottom endPoint: .bottom
@ -244,8 +244,8 @@ public struct CardBackView: View {
.shadow( .shadow(
color: .black.opacity(CasinoDesign.Opacity.light), color: .black.opacity(CasinoDesign.Opacity.light),
radius: CasinoDesign.Shadow.radiusSmall, radius: CasinoDesign.Shadow.radiusSmall,
x: 2, x: CasinoDesign.Shadow.offsetMedium,
y: 2 y: CasinoDesign.Shadow.offsetMedium
) )
} }
} }
@ -256,8 +256,8 @@ public struct DiamondPatternView: View {
public var body: some View { public var body: some View {
Canvas { context, size in Canvas { context, size in
let spacing: CGFloat = 12 let spacing = CasinoDesign.Size.patternSpacing
let diamondSize: CGFloat = 6 let diamondSize = CasinoDesign.Size.patternDiamondSize
for row in stride(from: 0, to: size.height, by: spacing) { for row in stride(from: 0, to: size.height, by: spacing) {
let offset = Int(row / spacing) % 2 == 0 ? 0 : spacing / 2 let offset = Int(row / spacing) % 2 == 0 ? 0 : spacing / 2
@ -293,7 +293,10 @@ public struct CardPlaceholderView: View {
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.small) RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.small)
.strokeBorder( .strokeBorder(
Color.white.opacity(CasinoDesign.Opacity.light), Color.white.opacity(CasinoDesign.Opacity.light),
style: StrokeStyle(lineWidth: CasinoDesign.LineWidth.medium, dash: [8, 4]) style: StrokeStyle(
lineWidth: CasinoDesign.LineWidth.medium,
dash: [CasinoDesign.Size.dashLength, CasinoDesign.Size.dashGap]
)
) )
.frame(width: width, height: height) .frame(width: width, height: height)
.accessibilityLabel(String(localized: "Empty card slot", bundle: .module)) .accessibilityLabel(String(localized: "Empty card slot", bundle: .module))

View File

@ -96,7 +96,7 @@ public struct ChipOnTableView: View {
} }
private var textFontSize: CGFloat { private var textFontSize: CGFloat {
amount >= 1000 ? 10 : 11 amount >= 1000 ? CasinoDesign.BaseFontSize.xSmall : CasinoDesign.BaseFontSize.xSmall + 1
} }
// MARK: - Accessibility // MARK: - Accessibility
@ -116,7 +116,7 @@ public struct ChipOnTableView: View {
Circle() Circle()
.fill( .fill(
RadialGradient( RadialGradient(
colors: [chipColor.opacity(0.9), chipColor], colors: [chipColor.opacity(CasinoDesign.Opacity.nearOpaque), chipColor],
center: .topLeading, center: .topLeading,
startRadius: 0, startRadius: 0,
endRadius: gradientEndRadius endRadius: gradientEndRadius
@ -145,8 +145,8 @@ public struct ChipOnTableView: View {
.shadow( .shadow(
color: .black.opacity(CasinoDesign.Opacity.light), color: .black.opacity(CasinoDesign.Opacity.light),
radius: CasinoDesign.Shadow.radiusSmall, radius: CasinoDesign.Shadow.radiusSmall,
x: 1, x: CasinoDesign.Shadow.offsetSmall,
y: 2 y: CasinoDesign.Shadow.offsetMedium
) )
.overlay(alignment: .topTrailing) { .overlay(alignment: .topTrailing) {
if showMax { if showMax {

View File

@ -31,9 +31,7 @@ public struct ChipView: View {
private let innerCircleRatio: CGFloat = 0.65 private let innerCircleRatio: CGFloat = 0.65
private let innerGradientRatio: CGFloat = 0.4 private let innerGradientRatio: CGFloat = 0.4
private let textSizeRatio: CGFloat = 0.25 private let textSizeRatio: CGFloat = 0.25
private let selectionGlowPadding: CGFloat = 6 private let selectionGlowPadding: CGFloat = CasinoDesign.Spacing.xSmall + CasinoDesign.Spacing.xxSmall
private let shadowOffset: CGFloat = 2
private let shadowOffsetY: CGFloat = 3
private var colors: ChipColorSet { private var colors: ChipColorSet {
theme.chipColors(for: denomination) theme.chipColors(for: denomination)
@ -90,8 +88,8 @@ public struct ChipView: View {
.shadow( .shadow(
color: .black.opacity(CasinoDesign.Opacity.light), color: .black.opacity(CasinoDesign.Opacity.light),
radius: CasinoDesign.LineWidth.thin, radius: CasinoDesign.LineWidth.thin,
x: 1, x: CasinoDesign.Shadow.offsetSmall,
y: 1 y: CasinoDesign.Shadow.offsetSmall
) )
// Outer border // Outer border
@ -119,8 +117,8 @@ public struct ChipView: View {
.shadow( .shadow(
color: .black.opacity(CasinoDesign.Opacity.medium), color: .black.opacity(CasinoDesign.Opacity.medium),
radius: isSelected ? CasinoDesign.Shadow.radiusSmall * 2 : CasinoDesign.Shadow.radiusSmall, radius: isSelected ? CasinoDesign.Shadow.radiusSmall * 2 : CasinoDesign.Shadow.radiusSmall,
x: shadowOffset, x: CasinoDesign.Shadow.offsetMedium,
y: shadowOffsetY y: CasinoDesign.Shadow.offsetLarge
) )
.scaleEffect(isSelected ? CasinoDesign.Scale.selected : CasinoDesign.Scale.normal) .scaleEffect(isSelected ? CasinoDesign.Scale.selected : CasinoDesign.Scale.normal)
.animation(.spring(duration: CasinoDesign.Animation.quick), value: isSelected) .animation(.spring(duration: CasinoDesign.Animation.quick), value: isSelected)
@ -138,19 +136,23 @@ public struct ChipEdgePattern: View {
self.stripeColor = stripeColor self.stripeColor = stripeColor
} }
// MARK: - Pattern Constants
private let stripeCount = 8
private let stripeLengthRatio: CGFloat = 0.2
public var body: some View { public var body: some View {
Canvas { context, size in Canvas { context, size in
let center = CGPoint(x: size.width / 2, y: size.height / 2) let center = CGPoint(x: size.width / 2, y: size.height / 2)
let radius = min(size.width, size.height) / 2 let radius = min(size.width, size.height) / 2
let stripeCount = 8 let stripeWidth = CasinoDesign.Size.chipStripeWidth
let stripeWidth: CGFloat = 4 let stripeLength = radius * stripeLengthRatio
let stripeLength: CGFloat = radius * 0.2
for i in 0..<stripeCount { for i in 0..<stripeCount {
let angle = (Double(i) / Double(stripeCount)) * 2 * .pi let angle = (Double(i) / Double(stripeCount)) * 2 * .pi
let innerRadius = radius - stripeLength let innerRadius = radius - stripeLength
let outerRadius = radius - 2 let outerRadius = radius - CasinoDesign.Size.chipStripeInset
let startX = center.x + cos(angle) * innerRadius let startX = center.x + cos(angle) * innerRadius
let startY = center.y + sin(angle) * innerRadius let startY = center.y + sin(angle) * innerRadius