Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
a898da7664
commit
acd0064776
@ -64,6 +64,70 @@ If SwiftData is configured to use CloudKit:
|
||||
- All relationships must be marked optional.
|
||||
|
||||
|
||||
## Localization instructions
|
||||
|
||||
- Use **String Catalogs** (`.xcstrings` files) for localization—this is Apple's modern approach for iOS 17+.
|
||||
- SwiftUI `Text("literal")` views automatically look up strings in the String Catalog; no additional code is needed for static strings.
|
||||
- For strings outside of `Text` views or with dynamic content, use `String(localized:)` or create a helper extension:
|
||||
```swift
|
||||
extension String {
|
||||
static func localized(_ key: String) -> String {
|
||||
String(localized: String.LocalizationValue(key))
|
||||
}
|
||||
static func localized(_ key: String, _ arguments: CVarArg...) -> String {
|
||||
let format = String(localized: String.LocalizationValue(key))
|
||||
return String(format: format, arguments: arguments)
|
||||
}
|
||||
}
|
||||
```
|
||||
- For format strings with interpolation (e.g., "Balance: $%@"), define a key in the String Catalog and use `String.localized("key", value)`.
|
||||
- Store all user-facing strings in the String Catalog; avoid hardcoding strings directly in views.
|
||||
- Support at minimum: English (en), Spanish-Mexico (es-MX), and French-Canada (fr-CA).
|
||||
- Never use `NSLocalizedString`; prefer the modern `String(localized:)` API.
|
||||
|
||||
|
||||
## 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 small: CGFloat = 8
|
||||
static let medium: CGFloat = 12
|
||||
static let large: CGFloat = 16
|
||||
}
|
||||
enum CornerRadius {
|
||||
static let small: CGFloat = 8
|
||||
static let medium: CGFloat = 12
|
||||
}
|
||||
enum FontSize {
|
||||
static let body: CGFloat = 14
|
||||
static let title: CGFloat = 24
|
||||
}
|
||||
}
|
||||
```
|
||||
- For colors used across the app, extend `Color` with semantic color definitions:
|
||||
```swift
|
||||
extension Color {
|
||||
enum Primary {
|
||||
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)
|
||||
}
|
||||
}
|
||||
```
|
||||
- Within each view, extract view-specific magic numbers to private constants at the top of the struct:
|
||||
```swift
|
||||
struct MyView: View {
|
||||
private let cardWidth: CGFloat = 45
|
||||
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, FontSize, IconSize, Size, Animation, Opacity, LineWidth, Shadow.
|
||||
|
||||
|
||||
## Project structure
|
||||
|
||||
- Use a consistent project structure, with folder layout determined by app features.
|
||||
|
||||
@ -14,12 +14,12 @@ enum GameResult: Equatable {
|
||||
case bankerWins
|
||||
case tie
|
||||
|
||||
/// Display text for the result.
|
||||
/// Display text for the result (localized).
|
||||
var displayText: String {
|
||||
switch self {
|
||||
case .playerWins: return "Player Wins!"
|
||||
case .bankerWins: return "Banker Wins!"
|
||||
case .tie: return "Tie!"
|
||||
case .playerWins: return String.localized("PLAYER WINS")
|
||||
case .bankerWins: return String.localized("BANKER WINS")
|
||||
case .tie: return String.localized("TIE GAME")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1287
Baccarat/Resources/Localizable.xcstrings
Normal file
1287
Baccarat/Resources/Localizable.xcstrings
Normal file
File diff suppressed because it is too large
Load Diff
203
Baccarat/Theme/DesignConstants.swift
Normal file
203
Baccarat/Theme/DesignConstants.swift
Normal file
@ -0,0 +1,203 @@
|
||||
//
|
||||
// DesignConstants.swift
|
||||
// Baccarat
|
||||
//
|
||||
// Design system constants for consistent styling across the app.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// Design constants for the Baccarat app.
|
||||
enum Design {
|
||||
|
||||
// MARK: - Spacing
|
||||
|
||||
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
|
||||
static let xxLarge: CGFloat = 24
|
||||
static let xxxLarge: CGFloat = 32
|
||||
}
|
||||
|
||||
// MARK: - Corner Radii
|
||||
|
||||
enum CornerRadius {
|
||||
static let small: CGFloat = 8
|
||||
static let medium: CGFloat = 10
|
||||
static let large: CGFloat = 12
|
||||
static let xLarge: CGFloat = 14
|
||||
static let xxLarge: CGFloat = 20
|
||||
static let xxxLarge: CGFloat = 28
|
||||
}
|
||||
|
||||
// MARK: - Font Sizes
|
||||
|
||||
enum FontSize {
|
||||
static let xxSmall: CGFloat = 7
|
||||
static let xSmall: CGFloat = 9
|
||||
static let small: CGFloat = 10
|
||||
static let body: CGFloat = 12
|
||||
static let medium: CGFloat = 14
|
||||
static let large: CGFloat = 16
|
||||
static let xLarge: CGFloat = 18
|
||||
static let xxLarge: CGFloat = 20
|
||||
static let title: CGFloat = 32
|
||||
static let largeTitle: CGFloat = 36
|
||||
static let display: CGFloat = 70
|
||||
}
|
||||
|
||||
// MARK: - Icon Sizes
|
||||
|
||||
enum IconSize {
|
||||
static let small: CGFloat = 12
|
||||
static let medium: CGFloat = 16
|
||||
static let large: CGFloat = 22
|
||||
static let xLarge: CGFloat = 60
|
||||
static let xxLarge: CGFloat = 70
|
||||
}
|
||||
|
||||
// MARK: - Component Sizes
|
||||
|
||||
enum Size {
|
||||
static let chipSmall: CGFloat = 36
|
||||
static let chipMedium: CGFloat = 50
|
||||
static let cardWidthSmall: CGFloat = 45
|
||||
static let cardWidthMedium: CGFloat = 55
|
||||
static let cardWidthLarge: CGFloat = 65
|
||||
static let valueBadge: CGFloat = 26
|
||||
static let checkmark: CGFloat = 22
|
||||
static let tableAspectRatio: CGFloat = 1.6
|
||||
}
|
||||
|
||||
// MARK: - Animation
|
||||
|
||||
enum Animation {
|
||||
static let springDuration: Double = 0.4
|
||||
static let springBounce: Double = 0.3
|
||||
static let fadeInDuration: Double = 0.3
|
||||
static let cardFlipDuration: Double = 0.5
|
||||
}
|
||||
|
||||
// MARK: - Opacity
|
||||
|
||||
enum Opacity {
|
||||
static let disabled: Double = 0.5
|
||||
static let subtle: Double = 0.1
|
||||
static let light: Double = 0.3
|
||||
static let medium: Double = 0.5
|
||||
static let strong: Double = 0.7
|
||||
static let heavy: Double = 0.8
|
||||
static let nearOpaque: Double = 0.85
|
||||
}
|
||||
|
||||
// MARK: - Line Widths
|
||||
|
||||
enum LineWidth {
|
||||
static let thin: CGFloat = 1
|
||||
static let medium: CGFloat = 2
|
||||
static let thick: CGFloat = 3
|
||||
static let heavy: CGFloat = 4
|
||||
}
|
||||
|
||||
// MARK: - Shadow
|
||||
|
||||
enum Shadow {
|
||||
static let radiusSmall: CGFloat = 3
|
||||
static let radiusMedium: CGFloat = 6
|
||||
static let radiusLarge: CGFloat = 10
|
||||
static let radiusXLarge: CGFloat = 12
|
||||
static let radiusXXLarge: CGFloat = 30
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - App Colors
|
||||
|
||||
extension Color {
|
||||
|
||||
// MARK: - Table Colors
|
||||
|
||||
enum Table {
|
||||
static let feltDark = Color(red: 0.0, green: 0.28, blue: 0.12)
|
||||
static let feltLight = Color(red: 0.0, green: 0.35, blue: 0.18)
|
||||
static let backgroundDark = Color(red: 0.01, green: 0.12, blue: 0.06)
|
||||
static let backgroundLight = Color(red: 0.03, green: 0.25, blue: 0.12)
|
||||
static let baseDark = Color(red: 0.02, green: 0.15, blue: 0.08)
|
||||
}
|
||||
|
||||
// MARK: - Border Colors
|
||||
|
||||
enum Border {
|
||||
static let goldLight = Color(red: 0.85, green: 0.7, blue: 0.35)
|
||||
static let goldDark = Color(red: 0.65, green: 0.5, blue: 0.2)
|
||||
static let gold = Color(red: 0.7, green: 0.55, blue: 0.25)
|
||||
static let silver = Color(red: 0.6, green: 0.6, blue: 0.65)
|
||||
}
|
||||
|
||||
// MARK: - Betting Zone Colors
|
||||
|
||||
enum BettingZone {
|
||||
// Player (Blue)
|
||||
static let playerLight = Color(red: 0.1, green: 0.25, blue: 0.55)
|
||||
static let playerDark = Color(red: 0.05, green: 0.15, blue: 0.4)
|
||||
static let playerMaxLight = Color(red: 0.08, green: 0.18, blue: 0.4)
|
||||
static let playerMaxDark = Color(red: 0.04, green: 0.1, blue: 0.28)
|
||||
|
||||
// Banker (Red)
|
||||
static let bankerLight = Color(red: 0.55, green: 0.12, blue: 0.12)
|
||||
static let bankerDark = Color(red: 0.4, green: 0.08, blue: 0.08)
|
||||
static let bankerMaxLight = Color(red: 0.4, green: 0.1, blue: 0.1)
|
||||
static let bankerMaxDark = Color(red: 0.28, green: 0.06, blue: 0.06)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// MARK: - Button Colors
|
||||
|
||||
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)
|
||||
static let destructive = Color(red: 0.6, green: 0.2, blue: 0.2)
|
||||
}
|
||||
|
||||
// MARK: - Chip Colors
|
||||
|
||||
enum Chip {
|
||||
static let gold = Color(red: 0.8, green: 0.65, blue: 0.2)
|
||||
}
|
||||
|
||||
// MARK: - Modal Colors
|
||||
|
||||
enum Modal {
|
||||
static let backgroundLight = Color(red: 0.12, green: 0.12, blue: 0.14)
|
||||
static let backgroundDark = Color(red: 0.08, green: 0.08, blue: 0.1)
|
||||
}
|
||||
|
||||
// MARK: - Settings Colors
|
||||
|
||||
enum Settings {
|
||||
static let background = Color(red: 0.08, green: 0.12, blue: 0.08)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Localized Strings Helper
|
||||
|
||||
extension String {
|
||||
|
||||
/// Returns a localized string for the given key.
|
||||
static func localized(_ key: String) -> String {
|
||||
String(localized: String.LocalizationValue(key))
|
||||
}
|
||||
|
||||
/// Returns a localized string with format arguments.
|
||||
static func localized(_ key: String, _ arguments: CVarArg...) -> String {
|
||||
let format = String(localized: String.LocalizationValue(key))
|
||||
return String(format: format, arguments: arguments)
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,6 +148,22 @@ struct GameOverView: View {
|
||||
|
||||
@State private var showContent = false
|
||||
|
||||
// MARK: - Layout Constants
|
||||
|
||||
private let iconSize = Design.FontSize.display
|
||||
private let titleFontSize = Design.FontSize.largeTitle
|
||||
private let messageFontSize = Design.FontSize.xLarge
|
||||
private let statsFontSize: CGFloat = 17
|
||||
private let buttonFontSize = Design.FontSize.xLarge
|
||||
private let modalCornerRadius = Design.CornerRadius.xxxLarge
|
||||
private let statsCornerRadius = Design.CornerRadius.large
|
||||
private let cardPadding = Design.Spacing.xxxLarge
|
||||
private let contentSpacing: CGFloat = 28
|
||||
private let buttonHorizontalPadding: CGFloat = 48
|
||||
private let buttonVerticalPadding: CGFloat = 18
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Solid dark backdrop - fully opaque
|
||||
@ -155,110 +171,104 @@ struct GameOverView: View {
|
||||
.ignoresSafeArea()
|
||||
|
||||
// Modal card
|
||||
VStack(spacing: 28) {
|
||||
VStack(spacing: contentSpacing) {
|
||||
// Broke icon
|
||||
Image(systemName: "creditcard.trianglebadge.exclamationmark")
|
||||
.font(.system(size: 70))
|
||||
.font(.system(size: iconSize))
|
||||
.foregroundStyle(.red)
|
||||
.symbolEffect(.pulse, options: .repeating)
|
||||
|
||||
// Title
|
||||
Text("GAME OVER")
|
||||
.font(.system(size: 36, weight: .black, design: .rounded))
|
||||
.font(.system(size: titleFontSize, weight: .black, design: .rounded))
|
||||
.foregroundStyle(.white)
|
||||
|
||||
// Message
|
||||
Text("You've run out of chips!")
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundStyle(.white.opacity(0.7))
|
||||
.font(.system(size: messageFontSize, weight: .medium))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
||||
|
||||
// Stats card
|
||||
VStack(spacing: 12) {
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
HStack {
|
||||
Text("Rounds Played")
|
||||
.foregroundStyle(.white.opacity(0.6))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
Spacer()
|
||||
Text("\(roundsPlayed)")
|
||||
.bold()
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
.font(.system(size: 17))
|
||||
.font(.system(size: statsFontSize))
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
RoundedRectangle(cornerRadius: statsCornerRadius)
|
||||
.fill(Color.white.opacity(0.08))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.strokeBorder(Color.white.opacity(0.1), lineWidth: 1)
|
||||
RoundedRectangle(cornerRadius: statsCornerRadius)
|
||||
.strokeBorder(Color.white.opacity(Design.Opacity.subtle), lineWidth: Design.LineWidth.thin)
|
||||
)
|
||||
)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.horizontal, Design.Spacing.xLarge)
|
||||
|
||||
// Play Again button
|
||||
Button {
|
||||
onPlayAgain()
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
Image(systemName: "arrow.counterclockwise")
|
||||
Text("Play Again")
|
||||
}
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.font(.system(size: buttonFontSize, weight: .bold))
|
||||
.foregroundStyle(.black)
|
||||
.padding(.horizontal, 48)
|
||||
.padding(.vertical, 18)
|
||||
.padding(.horizontal, buttonHorizontalPadding)
|
||||
.padding(.vertical, buttonVerticalPadding)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color(red: 1.0, green: 0.85, blue: 0.3),
|
||||
Color(red: 0.9, green: 0.7, blue: 0.2)
|
||||
],
|
||||
colors: [Color.Button.goldLight, Color.Button.goldDark],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
)
|
||||
.shadow(color: .yellow.opacity(0.4), radius: 12)
|
||||
.shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusXLarge)
|
||||
}
|
||||
.padding(.top, 12)
|
||||
.padding(.top, Design.Spacing.medium)
|
||||
}
|
||||
.padding(32)
|
||||
.padding(cardPadding)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 28)
|
||||
RoundedRectangle(cornerRadius: modalCornerRadius)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color(red: 0.12, green: 0.12, blue: 0.14),
|
||||
Color(red: 0.08, green: 0.08, blue: 0.1)
|
||||
],
|
||||
colors: [Color.Modal.backgroundLight, Color.Modal.backgroundDark],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 28)
|
||||
RoundedRectangle(cornerRadius: modalCornerRadius)
|
||||
.strokeBorder(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color.red.opacity(0.5),
|
||||
Color.red.opacity(Design.Opacity.medium),
|
||||
Color.red.opacity(0.2)
|
||||
],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
),
|
||||
lineWidth: 2
|
||||
lineWidth: Design.LineWidth.medium
|
||||
)
|
||||
)
|
||||
)
|
||||
.shadow(color: .red.opacity(0.2), radius: 30)
|
||||
.padding(.horizontal, 24)
|
||||
.shadow(color: .red.opacity(0.2), radius: Design.Shadow.radiusXXLarge)
|
||||
.padding(.horizontal, Design.Spacing.xxLarge)
|
||||
.scaleEffect(showContent ? 1.0 : 0.8)
|
||||
.opacity(showContent ? 1.0 : 0)
|
||||
}
|
||||
.onAppear {
|
||||
withAnimation(.spring(duration: 0.4, bounce: 0.3)) {
|
||||
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce)) {
|
||||
showContent = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,20 @@ struct MiniBaccaratTableView: View {
|
||||
@Bindable var gameState: GameState
|
||||
let selectedChip: ChipDenomination
|
||||
|
||||
// MARK: - Layout Constants
|
||||
|
||||
private let tableLimitsFontSize = Design.FontSize.small
|
||||
private let tieZoneHeight: CGFloat = 55
|
||||
private let mainZoneHeight: CGFloat = 60
|
||||
private let tieHorizontalPadding: CGFloat = 50
|
||||
private let bankerHorizontalPadding: CGFloat = 30
|
||||
private let playerHorizontalPadding: CGFloat = 20
|
||||
private let zoneTopPadding = Design.Spacing.medium
|
||||
private let zoneBottomPadding = Design.Spacing.medium
|
||||
private let minSpacerLength = Design.Spacing.small
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
private func betAmount(for type: BetType) -> Int {
|
||||
gameState.betAmount(for: type)
|
||||
}
|
||||
@ -34,12 +48,22 @@ struct MiniBaccaratTableView: View {
|
||||
gameState.mainBet?.type == .banker
|
||||
}
|
||||
|
||||
private var tableLimitsText: String {
|
||||
String.localized(
|
||||
"tableLimitsFormat",
|
||||
gameState.minBet.formatted(),
|
||||
gameState.maxBet.formatted()
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 4) {
|
||||
VStack(spacing: Design.Spacing.xSmall) {
|
||||
// Table limits label
|
||||
Text("TABLE LIMITS: $\(gameState.minBet) - $\(gameState.maxBet.formatted())")
|
||||
.font(.system(size: 10, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(.white.opacity(0.5))
|
||||
Text(tableLimitsText)
|
||||
.font(.system(size: tableLimitsFontSize, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
.tracking(1)
|
||||
|
||||
ZStack {
|
||||
@ -47,10 +71,7 @@ struct MiniBaccaratTableView: View {
|
||||
TableFeltShape()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color(red: 0.0, green: 0.35, blue: 0.18),
|
||||
Color(red: 0.0, green: 0.28, blue: 0.12)
|
||||
],
|
||||
colors: [Color.Table.feltLight, Color.Table.feltDark],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
@ -60,14 +81,11 @@ struct MiniBaccaratTableView: View {
|
||||
TableFeltShape()
|
||||
.strokeBorder(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color(red: 0.85, green: 0.7, blue: 0.35),
|
||||
Color(red: 0.65, green: 0.5, blue: 0.2)
|
||||
],
|
||||
colors: [Color.Border.goldLight, Color.Border.goldDark],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
),
|
||||
lineWidth: 4
|
||||
lineWidth: Design.LineWidth.heavy
|
||||
)
|
||||
|
||||
// Betting zones layout
|
||||
@ -80,11 +98,11 @@ struct MiniBaccaratTableView: View {
|
||||
) {
|
||||
gameState.placeBet(type: .tie, amount: selectedChip.rawValue)
|
||||
}
|
||||
.frame(height: 55)
|
||||
.padding(.horizontal, 50)
|
||||
.padding(.top, 12)
|
||||
.frame(height: tieZoneHeight)
|
||||
.padding(.horizontal, tieHorizontalPadding)
|
||||
.padding(.top, zoneTopPadding)
|
||||
|
||||
Spacer(minLength: 8)
|
||||
Spacer(minLength: minSpacerLength)
|
||||
|
||||
// BANKER zone in middle
|
||||
BankerBettingZone(
|
||||
@ -95,10 +113,10 @@ struct MiniBaccaratTableView: View {
|
||||
) {
|
||||
gameState.placeBet(type: .banker, amount: selectedChip.rawValue)
|
||||
}
|
||||
.frame(height: 60)
|
||||
.padding(.horizontal, 30)
|
||||
.frame(height: mainZoneHeight)
|
||||
.padding(.horizontal, bankerHorizontalPadding)
|
||||
|
||||
Spacer(minLength: 8)
|
||||
Spacer(minLength: minSpacerLength)
|
||||
|
||||
// PLAYER zone at bottom
|
||||
PlayerBettingZone(
|
||||
@ -109,12 +127,12 @@ struct MiniBaccaratTableView: View {
|
||||
) {
|
||||
gameState.placeBet(type: .player, amount: selectedChip.rawValue)
|
||||
}
|
||||
.frame(height: 60)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, 12)
|
||||
.frame(height: mainZoneHeight)
|
||||
.padding(.horizontal, playerHorizontalPadding)
|
||||
.padding(.bottom, zoneBottomPadding)
|
||||
}
|
||||
}
|
||||
.aspectRatio(1.6, contentMode: .fit)
|
||||
.aspectRatio(Design.Size.tableAspectRatio, contentMode: .fit)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -123,12 +141,14 @@ struct MiniBaccaratTableView: View {
|
||||
struct TableFeltShape: InsettableShape {
|
||||
var insetAmount: CGFloat = 0
|
||||
|
||||
private let shapeCornerRadius = Design.CornerRadius.xxLarge
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
|
||||
let insetRect = rect.insetBy(dx: insetAmount, dy: insetAmount)
|
||||
let height = insetRect.height
|
||||
let cornerRadius: CGFloat = 20
|
||||
let cornerRadius = shapeCornerRadius
|
||||
|
||||
// Start from bottom left
|
||||
path.move(to: CGPoint(x: insetRect.minX + cornerRadius, y: insetRect.maxY))
|
||||
@ -178,22 +198,25 @@ struct TieBettingZone: View {
|
||||
var isAtMax: Bool = false
|
||||
let action: () -> Void
|
||||
|
||||
// MARK: - Layout Constants
|
||||
|
||||
private let cornerRadius = Design.CornerRadius.small
|
||||
private let titleFontSize = Design.FontSize.medium
|
||||
private let subtitleFontSize = Design.FontSize.xSmall
|
||||
private let chipTrailingPadding = Design.Spacing.small
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
private var backgroundColor: Color {
|
||||
if isAtMax {
|
||||
// Darker/muted green when at max
|
||||
return Color(red: 0.08, green: 0.32, blue: 0.18)
|
||||
}
|
||||
return Color(red: 0.1, green: 0.45, blue: 0.25)
|
||||
isAtMax ? Color.BettingZone.tieMax : Color.BettingZone.tie
|
||||
}
|
||||
|
||||
private var borderColor: Color {
|
||||
if isAtMax {
|
||||
// Silver border when at max
|
||||
return Color(red: 0.6, green: 0.6, blue: 0.65)
|
||||
}
|
||||
return Color(red: 0.7, green: 0.55, blue: 0.25)
|
||||
isAtMax ? Color.Border.silver : Color.Border.gold
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
if isEnabled {
|
||||
@ -202,22 +225,22 @@ struct TieBettingZone: View {
|
||||
} label: {
|
||||
ZStack {
|
||||
// Background
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(backgroundColor)
|
||||
|
||||
// Border
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.strokeBorder(borderColor, lineWidth: 2)
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.strokeBorder(borderColor, lineWidth: Design.LineWidth.medium)
|
||||
|
||||
// Centered text content
|
||||
VStack(spacing: 2) {
|
||||
VStack(spacing: Design.Spacing.xxSmall) {
|
||||
Text("TIE")
|
||||
.font(.system(size: 14, weight: .black, design: .rounded))
|
||||
.font(.system(size: titleFontSize, weight: .black, design: .rounded))
|
||||
.tracking(2)
|
||||
|
||||
Text("PAYS 8 TO 1")
|
||||
.font(.system(size: 9, weight: .medium))
|
||||
.opacity(0.8)
|
||||
.font(.system(size: subtitleFontSize, weight: .medium))
|
||||
.opacity(Design.Opacity.heavy)
|
||||
}
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
@ -225,7 +248,7 @@ struct TieBettingZone: View {
|
||||
.overlay(alignment: .trailing) {
|
||||
if betAmount > 0 {
|
||||
ChipOnTable(amount: betAmount, showMax: isAtMax)
|
||||
.padding(.trailing, 8)
|
||||
.padding(.trailing, chipTrailingPadding)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -241,27 +264,28 @@ struct BankerBettingZone: View {
|
||||
var isAtMax: Bool = false
|
||||
let action: () -> Void
|
||||
|
||||
// MARK: - Layout Constants
|
||||
|
||||
private let cornerRadius = Design.CornerRadius.medium
|
||||
private let titleFontSize = Design.FontSize.large
|
||||
private let subtitleFontSize = Design.FontSize.xSmall
|
||||
private let chipTrailingPadding = Design.Spacing.medium
|
||||
private let selectionShadowRadius = Design.Shadow.radiusSmall
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
private var backgroundColors: [Color] {
|
||||
if isAtMax {
|
||||
// Darker/muted red when at max
|
||||
return [
|
||||
Color(red: 0.4, green: 0.1, blue: 0.1),
|
||||
Color(red: 0.28, green: 0.06, blue: 0.06)
|
||||
]
|
||||
}
|
||||
return [
|
||||
Color(red: 0.55, green: 0.12, blue: 0.12),
|
||||
Color(red: 0.4, green: 0.08, blue: 0.08)
|
||||
]
|
||||
isAtMax
|
||||
? [Color.BettingZone.bankerMaxLight, Color.BettingZone.bankerMaxDark]
|
||||
: [Color.BettingZone.bankerLight, Color.BettingZone.bankerDark]
|
||||
}
|
||||
|
||||
private var borderColor: Color {
|
||||
if isAtMax {
|
||||
return Color(red: 0.6, green: 0.6, blue: 0.65)
|
||||
}
|
||||
return Color(red: 0.7, green: 0.55, blue: 0.25)
|
||||
isAtMax ? Color.Border.silver : Color.Border.gold
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
if isEnabled {
|
||||
@ -270,7 +294,7 @@ struct BankerBettingZone: View {
|
||||
} label: {
|
||||
ZStack {
|
||||
// Background
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: backgroundColors,
|
||||
@ -281,24 +305,24 @@ struct BankerBettingZone: View {
|
||||
|
||||
// Selection glow
|
||||
if isSelected {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.strokeBorder(Color.yellow, lineWidth: 3)
|
||||
.shadow(color: .yellow.opacity(0.5), radius: 8)
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.strokeBorder(Color.yellow, lineWidth: Design.LineWidth.thick)
|
||||
.shadow(color: .yellow.opacity(Design.Opacity.medium), radius: selectionShadowRadius)
|
||||
}
|
||||
|
||||
// Border
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.strokeBorder(borderColor, lineWidth: 2)
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.strokeBorder(borderColor, lineWidth: Design.LineWidth.medium)
|
||||
|
||||
// Centered text content
|
||||
VStack(spacing: 2) {
|
||||
VStack(spacing: Design.Spacing.xxSmall) {
|
||||
Text("BANKER")
|
||||
.font(.system(size: 16, weight: .black, design: .rounded))
|
||||
.font(.system(size: titleFontSize, weight: .black, design: .rounded))
|
||||
.tracking(3)
|
||||
|
||||
Text("PAYS 0.95 TO 1")
|
||||
.font(.system(size: 9, weight: .medium))
|
||||
.opacity(0.8)
|
||||
.font(.system(size: subtitleFontSize, weight: .medium))
|
||||
.opacity(Design.Opacity.heavy)
|
||||
}
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
@ -306,7 +330,7 @@ struct BankerBettingZone: View {
|
||||
.overlay(alignment: .trailing) {
|
||||
if betAmount > 0 {
|
||||
ChipOnTable(amount: betAmount, showMax: isAtMax)
|
||||
.padding(.trailing, 12)
|
||||
.padding(.trailing, chipTrailingPadding)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -322,27 +346,28 @@ struct PlayerBettingZone: View {
|
||||
var isAtMax: Bool = false
|
||||
let action: () -> Void
|
||||
|
||||
// MARK: - Layout Constants
|
||||
|
||||
private let cornerRadius = Design.CornerRadius.medium
|
||||
private let titleFontSize = Design.FontSize.large
|
||||
private let subtitleFontSize = Design.FontSize.xSmall
|
||||
private let chipTrailingPadding = Design.Spacing.medium
|
||||
private let selectionShadowRadius = Design.Shadow.radiusSmall
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
private var backgroundColors: [Color] {
|
||||
if isAtMax {
|
||||
// Darker/muted blue when at max
|
||||
return [
|
||||
Color(red: 0.08, green: 0.18, blue: 0.4),
|
||||
Color(red: 0.04, green: 0.1, blue: 0.28)
|
||||
]
|
||||
}
|
||||
return [
|
||||
Color(red: 0.1, green: 0.25, blue: 0.55),
|
||||
Color(red: 0.05, green: 0.15, blue: 0.4)
|
||||
]
|
||||
isAtMax
|
||||
? [Color.BettingZone.playerMaxLight, Color.BettingZone.playerMaxDark]
|
||||
: [Color.BettingZone.playerLight, Color.BettingZone.playerDark]
|
||||
}
|
||||
|
||||
private var borderColor: Color {
|
||||
if isAtMax {
|
||||
return Color(red: 0.6, green: 0.6, blue: 0.65)
|
||||
}
|
||||
return Color(red: 0.7, green: 0.55, blue: 0.25)
|
||||
isAtMax ? Color.Border.silver : Color.Border.gold
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
if isEnabled {
|
||||
@ -351,7 +376,7 @@ struct PlayerBettingZone: View {
|
||||
} label: {
|
||||
ZStack {
|
||||
// Background
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: backgroundColors,
|
||||
@ -362,24 +387,24 @@ struct PlayerBettingZone: View {
|
||||
|
||||
// Selection glow
|
||||
if isSelected {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.strokeBorder(Color.yellow, lineWidth: 3)
|
||||
.shadow(color: .yellow.opacity(0.5), radius: 8)
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.strokeBorder(Color.yellow, lineWidth: Design.LineWidth.thick)
|
||||
.shadow(color: .yellow.opacity(Design.Opacity.medium), radius: selectionShadowRadius)
|
||||
}
|
||||
|
||||
// Border
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.strokeBorder(borderColor, lineWidth: 2)
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.strokeBorder(borderColor, lineWidth: Design.LineWidth.medium)
|
||||
|
||||
// Centered text content
|
||||
VStack(spacing: 2) {
|
||||
VStack(spacing: Design.Spacing.xxSmall) {
|
||||
Text("PLAYER")
|
||||
.font(.system(size: 16, weight: .black, design: .rounded))
|
||||
.font(.system(size: titleFontSize, weight: .black, design: .rounded))
|
||||
.tracking(3)
|
||||
|
||||
Text("PAYS 1 TO 1")
|
||||
.font(.system(size: 9, weight: .medium))
|
||||
.opacity(0.8)
|
||||
.font(.system(size: subtitleFontSize, weight: .medium))
|
||||
.opacity(Design.Opacity.heavy)
|
||||
}
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
@ -387,7 +412,7 @@ struct PlayerBettingZone: View {
|
||||
.overlay(alignment: .trailing) {
|
||||
if betAmount > 0 {
|
||||
ChipOnTable(amount: betAmount, showMax: isAtMax)
|
||||
.padding(.trailing, 12)
|
||||
.padding(.trailing, chipTrailingPadding)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -400,24 +425,37 @@ struct ChipOnTable: View {
|
||||
let amount: Int
|
||||
var showMax: Bool = false
|
||||
|
||||
// MARK: - Layout Constants
|
||||
|
||||
private let chipSize = Design.Size.chipSmall
|
||||
private let innerRingSize: CGFloat = 26
|
||||
private let gradientEndRadius: CGFloat = 20
|
||||
private let maxBadgeFontSize = Design.FontSize.xxSmall
|
||||
private let maxBadgeOffsetX: CGFloat = 6
|
||||
private let maxBadgeOffsetY: CGFloat = -4
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
private var chipColor: Color {
|
||||
switch amount {
|
||||
case 0..<50: return .blue
|
||||
case 50..<100: return .orange
|
||||
case 100..<500: return .black
|
||||
case 500..<1000: return .purple
|
||||
default: return Color(red: 0.8, green: 0.65, blue: 0.2)
|
||||
default: return Color.Chip.gold
|
||||
}
|
||||
}
|
||||
|
||||
private var displayText: String {
|
||||
if amount >= 1000 {
|
||||
return "\(amount / 1000)K"
|
||||
} else {
|
||||
return "\(amount)"
|
||||
}
|
||||
amount >= 1000 ? "\(amount / 1000)K" : "\(amount)"
|
||||
}
|
||||
|
||||
private var textFontSize: CGFloat {
|
||||
amount >= 1000 ? Design.FontSize.small : 11
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
@ -426,36 +464,36 @@ struct ChipOnTable: View {
|
||||
colors: [chipColor.opacity(0.9), chipColor],
|
||||
center: .topLeading,
|
||||
startRadius: 0,
|
||||
endRadius: 20
|
||||
endRadius: gradientEndRadius
|
||||
)
|
||||
)
|
||||
.frame(width: 36, height: 36)
|
||||
.frame(width: chipSize, height: chipSize)
|
||||
|
||||
Circle()
|
||||
.strokeBorder(Color.white.opacity(0.8), lineWidth: 2)
|
||||
.frame(width: 36, height: 36)
|
||||
.strokeBorder(Color.white.opacity(Design.Opacity.heavy), lineWidth: Design.LineWidth.medium)
|
||||
.frame(width: chipSize, height: chipSize)
|
||||
|
||||
Circle()
|
||||
.strokeBorder(Color.white.opacity(0.4), lineWidth: 1)
|
||||
.frame(width: 26, height: 26)
|
||||
.strokeBorder(Color.white.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.thin)
|
||||
.frame(width: innerRingSize, height: innerRingSize)
|
||||
|
||||
Text(displayText)
|
||||
.font(.system(size: amount >= 1000 ? 10 : 11, weight: .bold))
|
||||
.font(.system(size: textFontSize, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.shadow(color: .black.opacity(0.4), radius: 3, x: 1, y: 2)
|
||||
.shadow(color: .black.opacity(Design.Opacity.light), radius: Design.Shadow.radiusSmall, x: 1, y: 2)
|
||||
.overlay(alignment: .topTrailing) {
|
||||
if showMax {
|
||||
Text("MAX")
|
||||
.font(.system(size: 7, weight: .black))
|
||||
.font(.system(size: maxBadgeFontSize, weight: .black))
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 4)
|
||||
.padding(.vertical, 2)
|
||||
.padding(.horizontal, Design.Spacing.xSmall)
|
||||
.padding(.vertical, Design.Spacing.xxSmall)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(Color.red)
|
||||
)
|
||||
.offset(x: 6, y: -4)
|
||||
.offset(x: maxBadgeOffsetX, y: maxBadgeOffsetY)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -463,7 +501,7 @@ struct ChipOnTable: View {
|
||||
|
||||
#Preview {
|
||||
ZStack {
|
||||
Color(red: 0.05, green: 0.2, blue: 0.1)
|
||||
Color.Table.baseDark
|
||||
.ignoresSafeArea()
|
||||
|
||||
MiniBaccaratTableView(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user