Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
baf9d88601
commit
bde7b0bd3f
@ -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.
|
||||
- Place view logic into view models or similar, so it can be tested.
|
||||
- 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.
|
||||
|
||||
|
||||
@ -86,25 +87,97 @@ If SwiftData is configured to use CloudKit:
|
||||
- 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
|
||||
|
||||
- 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:
|
||||
```swift
|
||||
enum Design {
|
||||
enum Spacing {
|
||||
static let xxSmall: CGFloat = 2
|
||||
static let xSmall: CGFloat = 4
|
||||
static let small: CGFloat = 8
|
||||
static let medium: CGFloat = 12
|
||||
static let large: CGFloat = 16
|
||||
static let xLarge: CGFloat = 20
|
||||
}
|
||||
enum CornerRadius {
|
||||
static let small: CGFloat = 8
|
||||
static let medium: CGFloat = 12
|
||||
static let large: CGFloat = 16
|
||||
}
|
||||
enum BaseFontSize {
|
||||
static let small: CGFloat = 10
|
||||
static let body: CGFloat = 14
|
||||
static let large: CGFloat = 18
|
||||
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:
|
||||
@ -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 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
|
||||
struct MyView: View {
|
||||
// Layout: fixed card dimensions for consistent appearance
|
||||
private let cardWidth: CGFloat = 45
|
||||
// Typography: constrained space requires fixed size
|
||||
private let headerFontSize: CGFloat = 18
|
||||
// ...
|
||||
}
|
||||
```
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
@ -19,6 +19,31 @@ enum GamePhase: Equatable {
|
||||
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.
|
||||
@Observable
|
||||
@MainActor
|
||||
@ -38,10 +63,10 @@ final class GameState {
|
||||
var lastResult: GameResult?
|
||||
var lastWinnings: Int = 0
|
||||
|
||||
// MARK: - Side Bet Results
|
||||
// MARK: - Bet Results
|
||||
var playerHadPair: Bool = false
|
||||
var bankerHadPair: Bool = false
|
||||
var dragonBonusPayouts: [BetType: Int] = [:]
|
||||
var betResults: [BetResult] = []
|
||||
|
||||
// MARK: - Card Display State (for animations)
|
||||
var visiblePlayerCards: [Card] = []
|
||||
@ -224,7 +249,7 @@ final class GameState {
|
||||
isAnimating = true
|
||||
engine.prepareNewRound()
|
||||
|
||||
// Clear visible cards and side bet results
|
||||
// Clear visible cards and bet results
|
||||
visiblePlayerCards = []
|
||||
visibleBankerCards = []
|
||||
playerCardsFaceUp = []
|
||||
@ -233,7 +258,7 @@ final class GameState {
|
||||
showResultBanner = false
|
||||
playerHadPair = false
|
||||
bankerHadPair = false
|
||||
dragonBonusPayouts = [:]
|
||||
betResults = []
|
||||
|
||||
// Deal initial cards
|
||||
currentPhase = .dealingInitial
|
||||
@ -331,16 +356,16 @@ final class GameState {
|
||||
playerHadPair = engine.playerHasPair
|
||||
bankerHadPair = engine.bankerHasPair
|
||||
|
||||
// Calculate and apply payouts
|
||||
// Calculate and apply payouts, track individual results
|
||||
var totalWinnings = 0
|
||||
var results: [BetResult] = []
|
||||
|
||||
for bet in currentBets {
|
||||
let payout = engine.calculatePayout(bet: bet, result: result)
|
||||
totalWinnings += payout
|
||||
|
||||
// Track dragon bonus payouts for display
|
||||
if bet.type == .dragonBonusPlayer || bet.type == .dragonBonusBanker {
|
||||
dragonBonusPayouts[bet.type] = payout
|
||||
}
|
||||
// Track individual bet result
|
||||
results.append(BetResult(type: bet.type, amount: bet.amount, payout: payout))
|
||||
|
||||
// Return original bet if not a loss
|
||||
if payout >= 0 {
|
||||
@ -353,6 +378,7 @@ final class GameState {
|
||||
}
|
||||
}
|
||||
|
||||
betResults = results
|
||||
lastWinnings = totalWinnings
|
||||
|
||||
// Record result in history
|
||||
@ -385,7 +411,7 @@ final class GameState {
|
||||
lastWinnings = 0
|
||||
playerHadPair = false
|
||||
bankerHadPair = false
|
||||
dragonBonusPayouts = [:]
|
||||
betResults = []
|
||||
currentPhase = .betting
|
||||
}
|
||||
|
||||
@ -415,7 +441,7 @@ final class GameState {
|
||||
showResultBanner = false
|
||||
playerHadPair = false
|
||||
bankerHadPair = false
|
||||
dragonBonusPayouts = [:]
|
||||
betResults = []
|
||||
}
|
||||
|
||||
/// Applies new settings (call after settings change).
|
||||
|
||||
@ -40,6 +40,10 @@
|
||||
"comment" : "A bullet point used to list items in a rule section.",
|
||||
"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 \"$\".",
|
||||
"localizations" : {
|
||||
@ -114,10 +118,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"8 TO 1" : {
|
||||
"8 : 1" : {
|
||||
"comment" : "The payout ratio for a tie bet.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"11 : 1" : {
|
||||
"comment" : "The payout ratio for a pair bet.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"11:1" : {
|
||||
"comment" : "The payout ratio for a pair bonus bet.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -624,17 +632,8 @@
|
||||
"comment" : "A heading for important information related to a section of a view.",
|
||||
"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" : {
|
||||
"comment" : "A label displayed as a badge on top-right of a chip to indicate it's the maximum bet.",
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
@ -1223,6 +1222,10 @@
|
||||
"comment" : "A section header for tips related to pair bonuses.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"TOTAL" : {
|
||||
"comment" : "A label displayed next to the total winnings in the result banner.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"WIN" : {
|
||||
"comment" : "The text that appears as a badge when a player wins a hand in baccarat.",
|
||||
"localizations" : {
|
||||
@ -1262,10 +1265,6 @@
|
||||
"comment" : "A description of the player's hand, including its value and whether they won.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"wonAmountFormat" : {
|
||||
"comment" : "Format string used to describe the amount won in a localized manner.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"You've run out of chips!" : {
|
||||
"comment" : "A message displayed when a player runs out of money in the game over screen.",
|
||||
"localizations" : {
|
||||
|
||||
@ -70,6 +70,8 @@ enum Design {
|
||||
static let chipSmall: CGFloat = 36
|
||||
static let chipMedium: CGFloat = 50
|
||||
static let chipSelector: CGFloat = 50
|
||||
static let chipBadge: CGFloat = 32
|
||||
static let chipBadgeInner: CGFloat = 28
|
||||
static let cardWidthSmall: CGFloat = 45
|
||||
static let cardWidthMedium: CGFloat = 55
|
||||
static let cardWidthLarge: CGFloat = 65
|
||||
@ -77,6 +79,10 @@ enum Design {
|
||||
static let checkmark: CGFloat = 22
|
||||
static let tableAspectRatio: CGFloat = 1.6
|
||||
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
|
||||
@ -89,8 +95,9 @@ enum Design {
|
||||
static let fadeInDuration: Double = 0.3
|
||||
static let cardFlipDuration: Double = 0.5
|
||||
static let selectionDuration: Double = 0.2
|
||||
static let staggerDelay1: Double = 0.2
|
||||
static let staggerDelay2: Double = 0.4
|
||||
static let staggerDelay1: Double = 0.1
|
||||
static let staggerDelay2: Double = 0.25
|
||||
static let staggerDelay3: Double = 0.4
|
||||
}
|
||||
|
||||
// MARK: - Opacity
|
||||
@ -98,12 +105,15 @@ enum Design {
|
||||
enum Opacity {
|
||||
static let verySubtle: Double = 0.05
|
||||
static let subtle: Double = 0.1
|
||||
static let selection: Double = 0.15
|
||||
static let hint: Double = 0.2
|
||||
static let quarter: Double = 0.25
|
||||
static let light: Double = 0.3
|
||||
static let overlay: Double = 0.4
|
||||
static let medium: Double = 0.5
|
||||
static let secondary: Double = 0.5
|
||||
static let disabled: Double = 0.5
|
||||
static let accent: Double = 0.6
|
||||
static let strong: Double = 0.7
|
||||
static let heavy: Double = 0.8
|
||||
static let nearOpaque: Double = 0.85
|
||||
@ -140,11 +150,15 @@ enum Design {
|
||||
// MARK: - Shadow
|
||||
|
||||
enum Shadow {
|
||||
static let radiusSmall: CGFloat = 3
|
||||
static let radiusSmall: CGFloat = 2
|
||||
static let radiusMedium: CGFloat = 6
|
||||
static let radiusLarge: CGFloat = 10
|
||||
static let radiusXLarge: CGFloat = 12
|
||||
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)
|
||||
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)
|
||||
|
||||
// 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
|
||||
|
||||
@ -32,29 +32,6 @@ struct GameTableView: View {
|
||||
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 {
|
||||
ZStack {
|
||||
// Table background
|
||||
@ -134,10 +111,10 @@ struct GameTableView: View {
|
||||
if state.showResultBanner, let result = state.lastResult {
|
||||
ResultBannerView(
|
||||
result: result,
|
||||
winnings: state.lastWinnings,
|
||||
totalWinnings: state.lastWinnings,
|
||||
betResults: state.betResults,
|
||||
playerHadPair: state.playerHadPair,
|
||||
bankerHadPair: state.bankerHadPair,
|
||||
sideBetWinnings: buildSideBetDescriptions(state: state)
|
||||
bankerHadPair: state.bankerHadPair
|
||||
)
|
||||
.transition(.opacity)
|
||||
|
||||
@ -238,7 +215,7 @@ struct GameOverView: View {
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: statsCornerRadius)
|
||||
.fill(Color.white.opacity(0.08))
|
||||
.fill(Color.white.opacity(Design.Opacity.subtle))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: statsCornerRadius)
|
||||
.strokeBorder(Color.white.opacity(Design.Opacity.subtle), lineWidth: Design.LineWidth.thin)
|
||||
@ -288,7 +265,7 @@ struct GameOverView: View {
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color.red.opacity(Design.Opacity.medium),
|
||||
Color.red.opacity(0.2)
|
||||
Color.red.opacity(Design.Opacity.hint)
|
||||
],
|
||||
startPoint: .topLeading,
|
||||
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)
|
||||
.scaleEffect(showContent ? Design.Scale.normal : Design.Scale.slightShrink)
|
||||
.opacity(showContent ? 1.0 : 0)
|
||||
@ -421,7 +398,7 @@ struct CardsDisplayArea: View {
|
||||
.padding(.horizontal, Design.Spacing.xLarge)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge)
|
||||
.fill(Color.black.opacity(0.25))
|
||||
.fill(Color.black.opacity(Design.Opacity.quarter))
|
||||
.accessibilityHidden(true)
|
||||
)
|
||||
.padding(.horizontal)
|
||||
@ -588,7 +565,7 @@ struct TopBarView: View {
|
||||
HStack(spacing: Design.Spacing.xSmall) {
|
||||
Text("$")
|
||||
.font(.system(size: currencyFontSize, weight: .bold))
|
||||
.foregroundStyle(.yellow.opacity(0.8))
|
||||
.foregroundStyle(.yellow.opacity(Design.Opacity.heavy))
|
||||
|
||||
Text(balance, format: .number)
|
||||
.font(.system(size: balanceFontSize, weight: .black, design: .rounded))
|
||||
@ -630,7 +607,7 @@ struct TopBarView: View {
|
||||
Button("Help", systemImage: "info.circle.fill", action: onHelp)
|
||||
.labelStyle(.iconOnly)
|
||||
.font(.system(size: buttonFontSize))
|
||||
.foregroundStyle(.white.opacity(0.6))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.accent))
|
||||
.padding(Design.Spacing.small)
|
||||
.background(
|
||||
Circle()
|
||||
@ -641,7 +618,7 @@ struct TopBarView: View {
|
||||
Button("Settings", systemImage: "gearshape.fill", action: onSettings)
|
||||
.labelStyle(.iconOnly)
|
||||
.font(.system(size: buttonFontSize))
|
||||
.foregroundStyle(.white.opacity(0.6))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.accent))
|
||||
.padding(Design.Spacing.small)
|
||||
.background(
|
||||
Circle()
|
||||
@ -652,7 +629,7 @@ struct TopBarView: View {
|
||||
Button("Reset", systemImage: "arrow.counterclockwise", action: onReset)
|
||||
.labelStyle(.iconOnly)
|
||||
.font(.system(size: buttonFontSize))
|
||||
.foregroundStyle(.white.opacity(0.6))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.accent))
|
||||
.padding(Design.Spacing.small)
|
||||
.background(
|
||||
Circle()
|
||||
|
||||
@ -81,8 +81,8 @@ struct MiniBaccaratTableView: View {
|
||||
|
||||
// Divider
|
||||
Rectangle()
|
||||
.fill(Color.Border.gold.opacity(0.5))
|
||||
.frame(height: 1)
|
||||
.fill(Color.Border.gold.opacity(Design.Opacity.medium))
|
||||
.frame(height: Design.LineWidth.thin)
|
||||
|
||||
// Middle row: BANKER | BONUS
|
||||
MainBetRow(
|
||||
@ -102,8 +102,8 @@ struct MiniBaccaratTableView: View {
|
||||
|
||||
// Divider
|
||||
Rectangle()
|
||||
.fill(Color.Border.gold.opacity(0.5))
|
||||
.frame(height: 1)
|
||||
.fill(Color.Border.gold.opacity(Design.Opacity.medium))
|
||||
.frame(height: Design.LineWidth.thin)
|
||||
|
||||
// Bottom row: PLAYER | BONUS
|
||||
MainBetRow(
|
||||
@ -133,7 +133,7 @@ struct MiniBaccaratTableView: View {
|
||||
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 onPlayerPair: () -> Void
|
||||
|
||||
private let rowHeight: CGFloat = 52
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
// B PAIR
|
||||
@ -164,14 +162,14 @@ private struct TopBettingRow: View {
|
||||
betAmount: bankerPairAmount,
|
||||
isEnabled: canBetBankerPair,
|
||||
isAtMax: isBankerPairAtMax,
|
||||
color: Color.BettingZone.bankerDark.opacity(0.6),
|
||||
color: Color.BettingZone.bankerDark.opacity(Design.Opacity.accent),
|
||||
action: onBankerPair
|
||||
)
|
||||
|
||||
// Vertical divider
|
||||
Rectangle()
|
||||
.fill(Color.Border.gold.opacity(0.5))
|
||||
.frame(width: 1)
|
||||
.fill(Color.Border.gold.opacity(Design.Opacity.medium))
|
||||
.frame(width: Design.LineWidth.thin)
|
||||
|
||||
// TIE
|
||||
TieBetZone(
|
||||
@ -183,8 +181,8 @@ private struct TopBettingRow: View {
|
||||
|
||||
// Vertical divider
|
||||
Rectangle()
|
||||
.fill(Color.Border.gold.opacity(0.5))
|
||||
.frame(width: 1)
|
||||
.fill(Color.Border.gold.opacity(Design.Opacity.medium))
|
||||
.frame(width: Design.LineWidth.thin)
|
||||
|
||||
// P PAIR
|
||||
PairBetZone(
|
||||
@ -192,11 +190,11 @@ private struct TopBettingRow: View {
|
||||
betAmount: playerPairAmount,
|
||||
isEnabled: canBetPlayerPair,
|
||||
isAtMax: isPlayerPairAtMax,
|
||||
color: Color.BettingZone.playerDark.opacity(0.6),
|
||||
color: Color.BettingZone.playerDark.opacity(Design.Opacity.accent),
|
||||
action: onPlayerPair
|
||||
)
|
||||
}
|
||||
.frame(height: rowHeight)
|
||||
.frame(height: Design.Size.topBetRowHeight)
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,9 +208,6 @@ private struct PairBetZone: View {
|
||||
let color: Color
|
||||
let action: () -> Void
|
||||
|
||||
private let titleFontSize: CGFloat = 12
|
||||
private let payoutFontSize: CGFloat = 10
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
if isEnabled { action() }
|
||||
@ -223,25 +218,27 @@ private struct PairBetZone: View {
|
||||
.fill(color)
|
||||
|
||||
// Content
|
||||
VStack(spacing: 2) {
|
||||
VStack(spacing: Design.Spacing.xxSmall) {
|
||||
Text(title)
|
||||
.font(.system(size: titleFontSize, weight: .heavy, design: .rounded))
|
||||
.font(.system(size: Design.BaseFontSize.body, weight: .heavy, design: .rounded))
|
||||
.foregroundStyle(.yellow)
|
||||
|
||||
Text("11 : 1")
|
||||
.font(.system(size: payoutFontSize, weight: .medium, design: .rounded))
|
||||
.foregroundStyle(.white.opacity(0.7))
|
||||
.font(.system(size: Design.BaseFontSize.small, weight: .medium, design: .rounded))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
||||
}
|
||||
|
||||
// Chip indicator
|
||||
// Chip indicator - center right with padding
|
||||
if betAmount > 0 {
|
||||
ChipBadge(amount: betAmount, isMax: isAtMax)
|
||||
.offset(y: 16)
|
||||
HStack {
|
||||
Spacer()
|
||||
ChipBadge(amount: betAmount, isMax: isAtMax)
|
||||
.padding(.trailing, Design.Spacing.xSmall + Design.Spacing.xxSmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.opacity(isEnabled ? 1.0 : 0.5)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel("\(title) bet, pays 11 to 1" + (betAmount > 0 ? ", current bet $\(betAmount)" : ""))
|
||||
.accessibilityAddTraits(.isButton)
|
||||
@ -256,9 +253,6 @@ private struct TieBetZone: View {
|
||||
let isAtMax: Bool
|
||||
let action: () -> Void
|
||||
|
||||
private let titleFontSize: CGFloat = 14
|
||||
private let payoutFontSize: CGFloat = 10
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
if isEnabled { action() }
|
||||
@ -269,26 +263,28 @@ private struct TieBetZone: View {
|
||||
.fill(isAtMax ? Color.BettingZone.tieMax : Color.BettingZone.tie)
|
||||
|
||||
// Content
|
||||
VStack(spacing: 2) {
|
||||
VStack(spacing: Design.Spacing.xxSmall) {
|
||||
Text("TIE")
|
||||
.font(.system(size: titleFontSize, weight: .black, design: .rounded))
|
||||
.font(.system(size: Design.BaseFontSize.medium, weight: .black, design: .rounded))
|
||||
.tracking(1)
|
||||
|
||||
Text("8 : 1")
|
||||
.font(.system(size: payoutFontSize, weight: .medium, design: .rounded))
|
||||
.opacity(0.7)
|
||||
.font(.system(size: Design.BaseFontSize.small, weight: .medium, design: .rounded))
|
||||
.opacity(Design.Opacity.strong)
|
||||
}
|
||||
.foregroundStyle(.white)
|
||||
|
||||
// Chip indicator
|
||||
// Chip indicator - center right with padding
|
||||
if betAmount > 0 {
|
||||
ChipBadge(amount: betAmount, isMax: isAtMax)
|
||||
.offset(y: 16)
|
||||
HStack {
|
||||
Spacer()
|
||||
ChipBadge(amount: betAmount, isMax: isAtMax)
|
||||
.padding(.trailing, Design.Spacing.xSmall + Design.Spacing.xxSmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.opacity(isEnabled ? 1.0 : 0.5)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel("Tie bet, pays 8 to 1" + (betAmount > 0 ? ", current bet $\(betAmount)" : ""))
|
||||
.accessibilityAddTraits(.isButton)
|
||||
@ -311,9 +307,6 @@ private struct MainBetRow: View {
|
||||
let onMain: () -> Void
|
||||
let onBonus: () -> Void
|
||||
|
||||
private let rowHeight: CGFloat = 65
|
||||
private let bonusWidth: CGFloat = 80
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
// Main bet zone (BANKER or PLAYER)
|
||||
@ -330,8 +323,8 @@ private struct MainBetRow: View {
|
||||
|
||||
// Vertical divider
|
||||
Rectangle()
|
||||
.fill(Color.Border.gold.opacity(0.5))
|
||||
.frame(width: 1)
|
||||
.fill(Color.Border.gold.opacity(Design.Opacity.medium))
|
||||
.frame(width: Design.LineWidth.thin)
|
||||
|
||||
// Dragon Bonus zone
|
||||
DragonBonusZone(
|
||||
@ -340,9 +333,9 @@ private struct MainBetRow: View {
|
||||
isAtMax: isBonusAtMax,
|
||||
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 action: () -> Void
|
||||
|
||||
private let titleFontSize: CGFloat = 20
|
||||
private let payoutFontSize: CGFloat = 11
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
if isEnabled { action() }
|
||||
@ -373,39 +363,35 @@ private struct MainBetZone: View {
|
||||
// Selection highlight
|
||||
if isSelected {
|
||||
Rectangle()
|
||||
.fill(Color.yellow.opacity(0.15))
|
||||
.fill(Color.yellow.opacity(Design.Opacity.selection))
|
||||
|
||||
Rectangle()
|
||||
.strokeBorder(Color.yellow, lineWidth: 3)
|
||||
.strokeBorder(Color.yellow, lineWidth: Design.LineWidth.thick)
|
||||
}
|
||||
|
||||
// Content
|
||||
HStack {
|
||||
Spacer()
|
||||
// Content - always centered
|
||||
VStack(spacing: Design.Spacing.xxSmall + 1) {
|
||||
Text(title)
|
||||
.font(.system(size: Design.BaseFontSize.xxLarge, weight: .black, design: .rounded))
|
||||
.tracking(2)
|
||||
|
||||
VStack(spacing: 3) {
|
||||
Text(title)
|
||||
.font(.system(size: titleFontSize, weight: .black, design: .rounded))
|
||||
.tracking(2)
|
||||
|
||||
Text(payoutText)
|
||||
.font(.system(size: payoutFontSize, weight: .semibold, design: .rounded))
|
||||
.opacity(0.7)
|
||||
}
|
||||
.foregroundStyle(.white)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Chip indicator
|
||||
if betAmount > 0 {
|
||||
Text(payoutText)
|
||||
.font(.system(size: Design.BaseFontSize.callout - 2, weight: .semibold, design: .rounded))
|
||||
.opacity(Design.Opacity.strong)
|
||||
}
|
||||
.foregroundStyle(.white)
|
||||
|
||||
// Chip indicator - overlaid on right, doesn't affect centering
|
||||
if betAmount > 0 {
|
||||
HStack {
|
||||
Spacer()
|
||||
ChipOnTableView(amount: betAmount, showMax: isAtMax)
|
||||
.padding(.trailing, Design.Spacing.medium)
|
||||
.padding(.trailing, Design.Spacing.small)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.opacity(isEnabled ? 1.0 : 0.5)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel("\(title) bet, pays \(payoutText)" + (isSelected ? ", selected" : "") + (betAmount > 0 ? ", current bet $\(betAmount)" : ""))
|
||||
.accessibilityAddTraits(.isButton)
|
||||
@ -420,9 +406,6 @@ private struct DragonBonusZone: View {
|
||||
let isAtMax: Bool
|
||||
let action: () -> Void
|
||||
|
||||
private let titleFontSize: CGFloat = 9
|
||||
private let diamondSize: CGFloat = 24
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
if isEnabled { action() }
|
||||
@ -433,8 +416,8 @@ private struct DragonBonusZone: View {
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color(red: 0.25, green: 0.3, blue: 0.45),
|
||||
Color(red: 0.15, green: 0.2, blue: 0.35)
|
||||
Color.BettingZone.dragonBonusLight,
|
||||
Color.BettingZone.dragonBonusDark
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
@ -442,36 +425,38 @@ private struct DragonBonusZone: View {
|
||||
)
|
||||
|
||||
// Content
|
||||
VStack(spacing: 5) {
|
||||
VStack(spacing: Design.Spacing.xSmall) {
|
||||
// Diamond shape
|
||||
DiamondShape()
|
||||
.fill(
|
||||
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,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
.frame(width: diamondSize, height: diamondSize)
|
||||
.frame(width: Design.Size.diamondIcon, height: Design.Size.diamondIcon)
|
||||
.overlay(
|
||||
DiamondShape()
|
||||
.strokeBorder(Color.white.opacity(0.4), lineWidth: 1)
|
||||
.strokeBorder(Color.white.opacity(Design.Opacity.overlay), lineWidth: Design.LineWidth.thin)
|
||||
)
|
||||
|
||||
Text("BONUS")
|
||||
.font(.system(size: titleFontSize, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(.white.opacity(0.9))
|
||||
.font(.system(size: Design.BaseFontSize.xSmall, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.almostFull))
|
||||
}
|
||||
|
||||
// Chip indicator
|
||||
// Chip indicator - center right with padding (same as top row)
|
||||
if betAmount > 0 {
|
||||
ChipBadge(amount: betAmount, isMax: isAtMax)
|
||||
.offset(y: 22)
|
||||
HStack {
|
||||
Spacer()
|
||||
ChipBadge(amount: betAmount, isMax: isAtMax)
|
||||
.padding(.trailing, Design.Spacing.xSmall + Design.Spacing.xxSmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.opacity(isEnabled ? 1.0 : 0.5)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel("Dragon Bonus, pays up to 30 to 1" + (betAmount > 0 ? ", current bet $\(betAmount)" : ""))
|
||||
.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 {
|
||||
let amount: Int
|
||||
@ -511,24 +496,28 @@ private struct ChipBadge: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Outer ring
|
||||
Circle()
|
||||
.fill(isMax ? Color.gray : Color.yellow)
|
||||
.frame(width: 20, height: 20)
|
||||
.frame(width: Design.Size.chipBadge, height: Design.Size.chipBadge)
|
||||
|
||||
// Inner decoration
|
||||
Circle()
|
||||
.strokeBorder(Color.white.opacity(0.8), lineWidth: 1)
|
||||
.frame(width: 20, height: 20)
|
||||
.strokeBorder(Color.white.opacity(Design.Opacity.almostFull), lineWidth: Design.LineWidth.standard)
|
||||
.frame(width: Design.Size.chipBadgeInner, height: Design.Size.chipBadgeInner)
|
||||
|
||||
// Text
|
||||
if isMax {
|
||||
Text("M")
|
||||
.font(.system(size: 9, weight: .bold))
|
||||
Text("MAX")
|
||||
.font(.system(size: Design.BaseFontSize.xSmall, weight: .black))
|
||||
.foregroundStyle(.white)
|
||||
} else {
|
||||
Text(formatCompact(amount))
|
||||
.font(.system(size: 7, weight: .bold))
|
||||
.font(.system(size: Design.BaseFontSize.body, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(.black)
|
||||
}
|
||||
}
|
||||
.shadow(color: .black.opacity(Design.Opacity.light), radius: Design.Shadow.radiusSmall, y: Design.Shadow.offsetSmall)
|
||||
}
|
||||
|
||||
private func formatCompact(_ value: Int) -> String {
|
||||
|
||||
@ -2,30 +2,44 @@
|
||||
// ResultBannerView.swift
|
||||
// Baccarat
|
||||
//
|
||||
// Animated result banner showing the winner and winnings.
|
||||
// Animated result banner showing the winner and itemized bet results.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CasinoKit
|
||||
|
||||
/// An animated banner showing the round result.
|
||||
/// An animated banner showing the round result with bet breakdown.
|
||||
struct ResultBannerView: View {
|
||||
let result: GameResult
|
||||
let winnings: Int
|
||||
let totalWinnings: Int
|
||||
let betResults: [BetResult]
|
||||
var playerHadPair: Bool = false
|
||||
var bankerHadPair: Bool = false
|
||||
var sideBetWinnings: [String] = [] // List of side bet win descriptions
|
||||
|
||||
@State private var showBanner = false
|
||||
@State private var showText = false
|
||||
@State private var showWinnings = false
|
||||
@State private var showSideBets = false
|
||||
@State private var showBreakdown = false
|
||||
@State private var showTotal = false
|
||||
|
||||
// MARK: - Scaled Font Sizes (Dynamic Type)
|
||||
|
||||
@ScaledMetric(relativeTo: .largeTitle) private var resultFontSize: CGFloat = Design.BaseFontSize.largeTitle
|
||||
@ScaledMetric(relativeTo: .title2) private var winningsFontSize: CGFloat = 28
|
||||
@ScaledMetric(relativeTo: .body) private var sideBetFontSize: CGFloat = 14
|
||||
@ScaledMetric(relativeTo: .title3) private var totalFontSize: CGFloat = Design.BaseFontSize.xxLarge + Design.Spacing.xSmall
|
||||
@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 {
|
||||
ZStack {
|
||||
@ -60,47 +74,47 @@ struct ResultBannerView: View {
|
||||
PairBadge(label: "B PAIR", color: .red)
|
||||
}
|
||||
}
|
||||
.scaleEffect(showSideBets ? Design.Scale.normal : Design.Scale.shrunk)
|
||||
.opacity(showSideBets ? Design.Scale.normal : 0)
|
||||
.scaleEffect(showBreakdown ? Design.Scale.normal : Design.Scale.shrunk)
|
||||
.opacity(showBreakdown ? Design.Scale.normal : 0)
|
||||
}
|
||||
|
||||
// Winnings display
|
||||
if winnings != 0 {
|
||||
// Bet breakdown
|
||||
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) {
|
||||
if winnings > 0 {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
Text("\(winnings)")
|
||||
Text("TOTAL")
|
||||
.font(.system(size: Design.BaseFontSize.body, weight: .bold))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.accent))
|
||||
|
||||
if totalWinnings > 0 {
|
||||
Text("+\(totalWinnings)")
|
||||
.font(.system(size: totalFontSize, weight: .black, design: .rounded))
|
||||
.foregroundStyle(.green)
|
||||
} else {
|
||||
Image(systemName: "minus.circle.fill")
|
||||
.foregroundStyle(.red)
|
||||
Text("\(abs(winnings))")
|
||||
Text("\(totalWinnings)")
|
||||
.font(.system(size: totalFontSize, weight: .black, design: .rounded))
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
.font(.system(size: winningsFontSize, weight: .bold, design: .rounded))
|
||||
.scaleEffect(showWinnings ? Design.Scale.normal : Design.Scale.shrunk)
|
||||
.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)
|
||||
.scaleEffect(showTotal ? Design.Scale.normal : Design.Scale.shrunk)
|
||||
.opacity(showTotal ? Design.Scale.normal : 0)
|
||||
}
|
||||
}
|
||||
.padding(Design.Spacing.xxxLarge + Design.Spacing.small)
|
||||
.padding(.horizontal, Design.Spacing.xLarge)
|
||||
.padding(.vertical, Design.Spacing.xxLarge)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge + Design.Spacing.xSmall)
|
||||
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
@ -112,7 +126,7 @@ struct ResultBannerView: View {
|
||||
)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge + Design.Spacing.xSmall)
|
||||
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge)
|
||||
.strokeBorder(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
@ -140,11 +154,11 @@ struct ResultBannerView: View {
|
||||
}
|
||||
|
||||
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)) {
|
||||
showSideBets = true
|
||||
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce).delay(Design.Animation.staggerDelay3)) {
|
||||
showTotal = true
|
||||
}
|
||||
|
||||
// Announce result to VoiceOver users
|
||||
@ -168,25 +182,25 @@ struct ResultBannerView: View {
|
||||
description += ". Banker pair"
|
||||
}
|
||||
|
||||
// Add winnings
|
||||
if winnings > 0 {
|
||||
let format = String(localized: "wonAmountFormat")
|
||||
description += ". " + String(format: format, winnings.formatted())
|
||||
} else if winnings < 0 {
|
||||
let format = String(localized: "lostAmountFormat")
|
||||
description += ". " + String(format: format, abs(winnings).formatted())
|
||||
// Add bet results
|
||||
for bet in winningBets {
|
||||
description += ". \(bet.displayName) won \(bet.payout)"
|
||||
}
|
||||
for bet in losingBets {
|
||||
description += ". \(bet.displayName) lost \(abs(bet.payout))"
|
||||
}
|
||||
|
||||
// Add side bet descriptions
|
||||
for sideBet in sideBetWinnings {
|
||||
description += ". \(sideBet)"
|
||||
// Add total
|
||||
if totalWinnings > 0 {
|
||||
description += ". Total winnings: \(totalWinnings)"
|
||||
} else if totalWinnings < 0 {
|
||||
description += ". Total loss: \(abs(totalWinnings))"
|
||||
}
|
||||
|
||||
return description
|
||||
}
|
||||
|
||||
private func announceResult() {
|
||||
// Post accessibility announcement for screen reader users
|
||||
Task { @MainActor in
|
||||
try? await Task.sleep(for: .milliseconds(500))
|
||||
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 {
|
||||
let label: String
|
||||
let color: Color
|
||||
|
||||
var body: some View {
|
||||
Text(label)
|
||||
.font(.system(size: 11, weight: .bold))
|
||||
.font(.system(size: Design.BaseFontSize.callout - Design.Spacing.xxSmall, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, Design.Spacing.small)
|
||||
.padding(.vertical, Design.Spacing.xxSmall)
|
||||
@ -257,7 +352,7 @@ struct ConfettiView: View {
|
||||
}
|
||||
}
|
||||
.allowsHitTesting(false)
|
||||
.accessibilityHidden(true) // Decorative element
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -267,11 +362,16 @@ struct ConfettiView: View {
|
||||
.ignoresSafeArea()
|
||||
|
||||
ResultBannerView(
|
||||
result: .playerWins,
|
||||
winnings: 500,
|
||||
result: .playerWins,
|
||||
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,
|
||||
bankerHadPair: false,
|
||||
sideBetWinnings: ["Dragon Bonus +300"]
|
||||
bankerHadPair: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ struct RoadMapView: View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
|
||||
Text("HISTORY")
|
||||
.font(.system(size: historyFontSize, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(.white.opacity(0.6))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.accent))
|
||||
.tracking(1)
|
||||
|
||||
ScrollView(.horizontal) {
|
||||
|
||||
@ -43,7 +43,7 @@ struct RulesHelpView: View {
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Background
|
||||
Color.black.opacity(0.9)
|
||||
Color.black.opacity(Design.Opacity.almostFull)
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
@ -72,10 +72,10 @@ struct RulesHelpView: View {
|
||||
Image(systemName: "suit.heart.fill")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
.font(.system(size: 32))
|
||||
.font(.system(size: Design.BaseFontSize.title))
|
||||
|
||||
Text("BACCARAT")
|
||||
.font(.system(size: 28, weight: .black, design: .rounded))
|
||||
.font(.system(size: Design.BaseFontSize.title - Design.Spacing.xSmall, weight: .black, design: .rounded))
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [.yellow, .orange],
|
||||
@ -92,7 +92,7 @@ struct RulesHelpView: View {
|
||||
VStack(spacing: 0) {
|
||||
// Page 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)
|
||||
.padding(.top, Design.Spacing.large)
|
||||
.padding(.bottom, Design.Spacing.medium)
|
||||
@ -130,13 +130,13 @@ struct RulesHelpView: View {
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
// Previous button
|
||||
Button {
|
||||
withAnimation(.spring(duration: 0.3)) {
|
||||
withAnimation(.spring(duration: Design.Animation.quick)) {
|
||||
goToPreviousPage()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "chevron.left.circle.fill")
|
||||
.font(.system(size: 36))
|
||||
.foregroundStyle(currentPage.rawValue > 0 ? .yellow : .gray.opacity(0.5))
|
||||
.font(.system(size: Design.BaseFontSize.largeTitle))
|
||||
.foregroundStyle(currentPage.rawValue > 0 ? .yellow : .gray.opacity(Design.Opacity.medium))
|
||||
}
|
||||
.disabled(currentPage.rawValue == 0)
|
||||
|
||||
@ -145,7 +145,7 @@ struct RulesHelpView: View {
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("BACK TO GAME")
|
||||
.font(.system(size: 16, weight: .bold))
|
||||
.font(.system(size: Design.BaseFontSize.large, weight: .bold))
|
||||
.foregroundStyle(.black)
|
||||
.padding(.horizontal, Design.Spacing.xLarge)
|
||||
.padding(.vertical, Design.Spacing.medium)
|
||||
@ -163,13 +163,13 @@ struct RulesHelpView: View {
|
||||
|
||||
// Next button
|
||||
Button {
|
||||
withAnimation(.spring(duration: 0.3)) {
|
||||
withAnimation(.spring(duration: Design.Animation.quick)) {
|
||||
goToNextPage()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "chevron.right.circle.fill")
|
||||
.font(.system(size: 36))
|
||||
.foregroundStyle(currentPage.rawValue < RulesPage.allCases.count - 1 ? .green : .gray.opacity(0.5))
|
||||
.font(.system(size: Design.BaseFontSize.largeTitle))
|
||||
.foregroundStyle(currentPage.rawValue < RulesPage.allCases.count - 1 ? .green : .gray.opacity(Design.Opacity.medium))
|
||||
}
|
||||
.disabled(currentPage.rawValue >= RulesPage.allCases.count - 1)
|
||||
}
|
||||
@ -204,10 +204,10 @@ private struct BasicRulesContent: View {
|
||||
])
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.3))
|
||||
.background(Color.white.opacity(Design.Opacity.light))
|
||||
|
||||
Text("Card Values")
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
|
||||
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.")
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.3))
|
||||
.background(Color.white.opacity(Design.Opacity.light))
|
||||
|
||||
Text("Natural Win")
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
|
||||
.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.")
|
||||
@ -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.")
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.3))
|
||||
.background(Color.white.opacity(Design.Opacity.light))
|
||||
|
||||
Text("Player Rules")
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
|
||||
RuleSection(items: [
|
||||
@ -250,10 +250,10 @@ private struct ThirdCardRulesContent: View {
|
||||
])
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.3))
|
||||
.background(Color.white.opacity(Design.Opacity.light))
|
||||
|
||||
Text("Banker Rules")
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
|
||||
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()
|
||||
.background(
|
||||
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 action: String
|
||||
|
||||
private let labelWidth: CGFloat = 80
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text("Banker \(bankerTotal):")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.font(.system(size: Design.BaseFontSize.callout, weight: .semibold))
|
||||
.foregroundStyle(.yellow)
|
||||
.frame(width: 80, alignment: .leading)
|
||||
.frame(width: labelWidth, alignment: .leading)
|
||||
|
||||
Text(action)
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.white.opacity(0.9))
|
||||
.font(.system(size: Design.BaseFontSize.callout))
|
||||
.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.")
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.3))
|
||||
.background(Color.white.opacity(Design.Opacity.light))
|
||||
|
||||
Text("Payout Table")
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
|
||||
VStack(spacing: Design.Spacing.xSmall) {
|
||||
@ -321,14 +323,14 @@ private struct DragonBonusContent: View {
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
||||
.fill(Color.black.opacity(0.2))
|
||||
.fill(Color.black.opacity(Design.Opacity.hint))
|
||||
)
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.3))
|
||||
.background(Color.white.opacity(Design.Opacity.light))
|
||||
|
||||
Text("Important")
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
|
||||
RuleSection(items: [
|
||||
@ -347,13 +349,13 @@ private struct PayoutRow: View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(condition)
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(.white.opacity(0.9))
|
||||
.font(.system(size: Design.BaseFontSize.medium))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.almostFull))
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(payout)
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.font(.system(size: Design.BaseFontSize.medium, weight: .bold))
|
||||
.foregroundStyle(.yellow)
|
||||
}
|
||||
.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.")
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.3))
|
||||
.background(Color.white.opacity(Design.Opacity.light))
|
||||
|
||||
Text("Payout")
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
|
||||
HStack {
|
||||
VStack {
|
||||
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(
|
||||
LinearGradient(
|
||||
colors: [.yellow, .orange],
|
||||
@ -387,18 +389,18 @@ private struct PairBonusContent: View {
|
||||
)
|
||||
|
||||
Text("Pair Pays")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundStyle(.white.opacity(0.7))
|
||||
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding(.vertical, Design.Spacing.medium)
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.3))
|
||||
.background(Color.white.opacity(Design.Opacity.light))
|
||||
|
||||
Text("Examples")
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
|
||||
RuleSection(items: [
|
||||
@ -410,10 +412,10 @@ private struct PairBonusContent: View {
|
||||
RuleSection(text: "Note: Suits are disregarded. Only the rank matters for a pair.")
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.3))
|
||||
.background(Color.white.opacity(Design.Opacity.light))
|
||||
|
||||
Text("Tips")
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
|
||||
RuleSection(items: [
|
||||
@ -436,14 +438,14 @@ private struct RuleSection: View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
if let title = title {
|
||||
Text(title)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.font(.system(size: Design.BaseFontSize.large, weight: .semibold))
|
||||
.foregroundStyle(.yellow)
|
||||
}
|
||||
|
||||
if let text = text {
|
||||
Text(text)
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(.white.opacity(0.9))
|
||||
.font(.system(size: Design.BaseFontSize.medium))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.almostFull))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
@ -453,9 +455,9 @@ private struct RuleSection: View {
|
||||
Text("•")
|
||||
.foregroundStyle(.yellow)
|
||||
Text(item)
|
||||
.foregroundStyle(.white.opacity(0.9))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.almostFull))
|
||||
}
|
||||
.font(.system(size: 14))
|
||||
.font(.system(size: Design.BaseFontSize.medium))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,7 +118,7 @@ struct SettingsView: View {
|
||||
settings.load() // Revert changes
|
||||
dismiss()
|
||||
}
|
||||
.foregroundStyle(.white.opacity(0.7))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
@ -154,7 +154,7 @@ struct SettingsSection<Content: View>: View {
|
||||
Text(title)
|
||||
.font(.system(size: Design.BaseFontSize.body, weight: .bold, design: .rounded))
|
||||
.tracking(1)
|
||||
.foregroundStyle(.white.opacity(0.6))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.accent))
|
||||
}
|
||||
.padding(.horizontal, Design.Spacing.xSmall)
|
||||
|
||||
@ -165,7 +165,7 @@ struct SettingsSection<Content: View>: View {
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
||||
.fill(Color.white.opacity(0.05))
|
||||
.fill(Color.white.opacity(Design.Opacity.verySubtle))
|
||||
)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
@ -347,7 +347,7 @@ struct TableLimitsPicker: View {
|
||||
.padding(.vertical, Design.Spacing.xSmall)
|
||||
.background(
|
||||
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 {
|
||||
|
||||
@ -49,6 +49,10 @@ public enum CasinoDesign {
|
||||
public static let radiusSmall: CGFloat = 4
|
||||
public static let radiusMedium: CGFloat = 8
|
||||
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
|
||||
@ -56,7 +60,10 @@ public enum CasinoDesign {
|
||||
public enum Opacity {
|
||||
public static let subtle: CGFloat = 0.05
|
||||
public static let light: CGFloat = 0.2
|
||||
public static let quarter: CGFloat = 0.25
|
||||
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 nearOpaque: CGFloat = 0.95
|
||||
}
|
||||
@ -83,6 +90,16 @@ public enum CasinoDesign {
|
||||
|
||||
/// Card aspect ratio (height = width * this value).
|
||||
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)
|
||||
|
||||
@ -150,8 +150,8 @@ public struct CardFrontView: View {
|
||||
.shadow(
|
||||
color: .black.opacity(CasinoDesign.Opacity.light),
|
||||
radius: CasinoDesign.Shadow.radiusSmall,
|
||||
x: 2,
|
||||
y: 2
|
||||
x: CasinoDesign.Shadow.offsetMedium,
|
||||
y: CasinoDesign.Shadow.offsetMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -210,8 +210,8 @@ public struct CardBackView: View {
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
theme.cardBackPrimaryColor.opacity(0.8),
|
||||
theme.cardBackSecondaryColor.opacity(0.8)
|
||||
theme.cardBackPrimaryColor.opacity(CasinoDesign.Opacity.heavy),
|
||||
theme.cardBackSecondaryColor.opacity(CasinoDesign.Opacity.heavy)
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
@ -244,8 +244,8 @@ public struct CardBackView: View {
|
||||
.shadow(
|
||||
color: .black.opacity(CasinoDesign.Opacity.light),
|
||||
radius: CasinoDesign.Shadow.radiusSmall,
|
||||
x: 2,
|
||||
y: 2
|
||||
x: CasinoDesign.Shadow.offsetMedium,
|
||||
y: CasinoDesign.Shadow.offsetMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -256,8 +256,8 @@ public struct DiamondPatternView: View {
|
||||
|
||||
public var body: some View {
|
||||
Canvas { context, size in
|
||||
let spacing: CGFloat = 12
|
||||
let diamondSize: CGFloat = 6
|
||||
let spacing = CasinoDesign.Size.patternSpacing
|
||||
let diamondSize = CasinoDesign.Size.patternDiamondSize
|
||||
|
||||
for row in stride(from: 0, to: size.height, by: spacing) {
|
||||
let offset = Int(row / spacing) % 2 == 0 ? 0 : spacing / 2
|
||||
@ -293,7 +293,10 @@ public struct CardPlaceholderView: View {
|
||||
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.small)
|
||||
.strokeBorder(
|
||||
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)
|
||||
.accessibilityLabel(String(localized: "Empty card slot", bundle: .module))
|
||||
|
||||
@ -96,7 +96,7 @@ public struct ChipOnTableView: View {
|
||||
}
|
||||
|
||||
private var textFontSize: CGFloat {
|
||||
amount >= 1000 ? 10 : 11
|
||||
amount >= 1000 ? CasinoDesign.BaseFontSize.xSmall : CasinoDesign.BaseFontSize.xSmall + 1
|
||||
}
|
||||
|
||||
// MARK: - Accessibility
|
||||
@ -116,7 +116,7 @@ public struct ChipOnTableView: View {
|
||||
Circle()
|
||||
.fill(
|
||||
RadialGradient(
|
||||
colors: [chipColor.opacity(0.9), chipColor],
|
||||
colors: [chipColor.opacity(CasinoDesign.Opacity.nearOpaque), chipColor],
|
||||
center: .topLeading,
|
||||
startRadius: 0,
|
||||
endRadius: gradientEndRadius
|
||||
@ -145,8 +145,8 @@ public struct ChipOnTableView: View {
|
||||
.shadow(
|
||||
color: .black.opacity(CasinoDesign.Opacity.light),
|
||||
radius: CasinoDesign.Shadow.radiusSmall,
|
||||
x: 1,
|
||||
y: 2
|
||||
x: CasinoDesign.Shadow.offsetSmall,
|
||||
y: CasinoDesign.Shadow.offsetMedium
|
||||
)
|
||||
.overlay(alignment: .topTrailing) {
|
||||
if showMax {
|
||||
|
||||
@ -31,9 +31,7 @@ public struct ChipView: View {
|
||||
private let innerCircleRatio: CGFloat = 0.65
|
||||
private let innerGradientRatio: CGFloat = 0.4
|
||||
private let textSizeRatio: CGFloat = 0.25
|
||||
private let selectionGlowPadding: CGFloat = 6
|
||||
private let shadowOffset: CGFloat = 2
|
||||
private let shadowOffsetY: CGFloat = 3
|
||||
private let selectionGlowPadding: CGFloat = CasinoDesign.Spacing.xSmall + CasinoDesign.Spacing.xxSmall
|
||||
|
||||
private var colors: ChipColorSet {
|
||||
theme.chipColors(for: denomination)
|
||||
@ -90,8 +88,8 @@ public struct ChipView: View {
|
||||
.shadow(
|
||||
color: .black.opacity(CasinoDesign.Opacity.light),
|
||||
radius: CasinoDesign.LineWidth.thin,
|
||||
x: 1,
|
||||
y: 1
|
||||
x: CasinoDesign.Shadow.offsetSmall,
|
||||
y: CasinoDesign.Shadow.offsetSmall
|
||||
)
|
||||
|
||||
// Outer border
|
||||
@ -119,8 +117,8 @@ public struct ChipView: View {
|
||||
.shadow(
|
||||
color: .black.opacity(CasinoDesign.Opacity.medium),
|
||||
radius: isSelected ? CasinoDesign.Shadow.radiusSmall * 2 : CasinoDesign.Shadow.radiusSmall,
|
||||
x: shadowOffset,
|
||||
y: shadowOffsetY
|
||||
x: CasinoDesign.Shadow.offsetMedium,
|
||||
y: CasinoDesign.Shadow.offsetLarge
|
||||
)
|
||||
.scaleEffect(isSelected ? CasinoDesign.Scale.selected : CasinoDesign.Scale.normal)
|
||||
.animation(.spring(duration: CasinoDesign.Animation.quick), value: isSelected)
|
||||
@ -138,19 +136,23 @@ public struct ChipEdgePattern: View {
|
||||
self.stripeColor = stripeColor
|
||||
}
|
||||
|
||||
// MARK: - Pattern Constants
|
||||
|
||||
private let stripeCount = 8
|
||||
private let stripeLengthRatio: CGFloat = 0.2
|
||||
|
||||
public var body: some View {
|
||||
Canvas { context, size in
|
||||
let center = CGPoint(x: size.width / 2, y: size.height / 2)
|
||||
let radius = min(size.width, size.height) / 2
|
||||
let stripeCount = 8
|
||||
let stripeWidth: CGFloat = 4
|
||||
let stripeLength: CGFloat = radius * 0.2
|
||||
let stripeWidth = CasinoDesign.Size.chipStripeWidth
|
||||
let stripeLength = radius * stripeLengthRatio
|
||||
|
||||
for i in 0..<stripeCount {
|
||||
let angle = (Double(i) / Double(stripeCount)) * 2 * .pi
|
||||
|
||||
let innerRadius = radius - stripeLength
|
||||
let outerRadius = radius - 2
|
||||
let outerRadius = radius - CasinoDesign.Size.chipStripeInset
|
||||
|
||||
let startX = center.x + cos(angle) * innerRadius
|
||||
let startY = center.y + sin(angle) * innerRadius
|
||||
|
||||
Loading…
Reference in New Issue
Block a user