From c846ef05ac78c5d1d99246c75d87929f0d9171da Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 16 Dec 2025 20:17:21 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- Baccarat/Models/ChipDenomination.swift | 117 +++++++ Baccarat/Views/ChipView.swift | 291 ------------------ Baccarat/Views/Chips/ChipEdgePattern.swift | 62 ++++ Baccarat/Views/Chips/ChipOnTable.swift | 104 +++++++ .../Views/{ => Chips}/ChipSelectorView.swift | 0 Baccarat/Views/Chips/ChipStackView.swift | 64 ++++ Baccarat/Views/Chips/ChipView.swift | 116 +++++++ Baccarat/Views/MiniBaccaratTableView.swift | 80 ----- 8 files changed, 463 insertions(+), 371 deletions(-) create mode 100644 Baccarat/Models/ChipDenomination.swift delete mode 100644 Baccarat/Views/ChipView.swift create mode 100644 Baccarat/Views/Chips/ChipEdgePattern.swift create mode 100644 Baccarat/Views/Chips/ChipOnTable.swift rename Baccarat/Views/{ => Chips}/ChipSelectorView.swift (100%) create mode 100644 Baccarat/Views/Chips/ChipStackView.swift create mode 100644 Baccarat/Views/Chips/ChipView.swift diff --git a/Baccarat/Models/ChipDenomination.swift b/Baccarat/Models/ChipDenomination.swift new file mode 100644 index 0000000..22c092e --- /dev/null +++ b/Baccarat/Models/ChipDenomination.swift @@ -0,0 +1,117 @@ +// +// ChipDenomination.swift +// Baccarat +// +// The available chip denominations with their properties. +// + +import SwiftUI + +/// The available chip denominations. +enum ChipDenomination: Int, CaseIterable, Identifiable { + case ten = 10 + case twentyFive = 25 + case fifty = 50 + case hundred = 100 + case fiveHundred = 500 + case thousand = 1_000 + case fiveThousand = 5_000 + case tenThousand = 10_000 + case twentyFiveThousand = 25_000 + case fiftyThousand = 50_000 + case hundredThousand = 100_000 + + var id: Int { rawValue } + + /// The display text for this denomination. + var displayText: String { + switch self { + case .ten: return "10" + case .twentyFive: return "25" + case .fifty: return "50" + case .hundred: return "100" + case .fiveHundred: return "500" + case .thousand: return "1K" + case .fiveThousand: return "5K" + case .tenThousand: return "10K" + case .twentyFiveThousand: return "25K" + case .fiftyThousand: return "50K" + case .hundredThousand: return "100K" + } + } + + /// The minimum balance required to show this chip in the selector. + /// Higher chips unlock as you win more! + var unlockBalance: Int { + switch self { + case .ten, .twentyFive, .fifty, .hundred: return 0 // Always available + case .fiveHundred: return 500 + case .thousand: return 1_000 + case .fiveThousand: return 5_000 + case .tenThousand: return 10_000 + case .twentyFiveThousand: return 25_000 + case .fiftyThousand: return 50_000 + case .hundredThousand: return 100_000 + } + } + + /// Whether this chip should be shown based on the player's balance. + func isUnlocked(forBalance balance: Int) -> Bool { + balance >= unlockBalance + } + + /// Returns chips that should be visible for a given balance. + static func availableChips(forBalance balance: Int) -> [ChipDenomination] { + allCases.filter { $0.isUnlocked(forBalance: balance) } + } + + /// The primary color for this chip. + var primaryColor: Color { + switch self { + case .ten: return Color.Chip.tenBase + case .twentyFive: return Color.Chip.twentyFiveBase + case .fifty: return Color.Chip.fiftyBase + case .hundred: return Color.Chip.hundredBase + case .fiveHundred: return Color.Chip.fiveHundredBase + case .thousand: return Color.Chip.thousandBase + case .fiveThousand: return Color.Chip.fiveThousandBase + case .tenThousand: return Color.Chip.tenThousandBase + case .twentyFiveThousand: return Color.Chip.twentyFiveThousandBase + case .fiftyThousand: return Color.Chip.fiftyThousandBase + case .hundredThousand: return Color.Chip.hundredThousandBase + } + } + + /// The secondary/accent color for this chip. + var secondaryColor: Color { + switch self { + case .ten: return Color.Chip.tenHighlight + case .twentyFive: return Color.Chip.twentyFiveHighlight + case .fifty: return Color.Chip.fiftyHighlight + case .hundred: return Color.Chip.hundredHighlight + case .fiveHundred: return Color.Chip.fiveHundredHighlight + case .thousand: return Color.Chip.thousandHighlight + case .fiveThousand: return Color.Chip.fiveThousandHighlight + case .tenThousand: return Color.Chip.tenThousandHighlight + case .twentyFiveThousand: return Color.Chip.twentyFiveThousandHighlight + case .fiftyThousand: return Color.Chip.fiftyThousandHighlight + case .hundredThousand: return Color.Chip.hundredThousandHighlight + } + } + + /// The edge stripe color for this chip. + var stripeColor: Color { + switch self { + case .ten, .twentyFive, .fifty: return .white + case .hundred: return Color.Chip.goldStripe + case .fiveHundred: return .white + case .thousand: return .black + case .fiveThousand: return Color.Chip.goldStripe + case .tenThousand: return .white + case .twentyFiveThousand: return Color.Chip.goldStripe + case .fiftyThousand: return Color.Chip.darkStripe + case .hundredThousand: return Color.Chip.goldRubyStripe + } + } +} + diff --git a/Baccarat/Views/ChipView.swift b/Baccarat/Views/ChipView.swift deleted file mode 100644 index 0a9eb46..0000000 --- a/Baccarat/Views/ChipView.swift +++ /dev/null @@ -1,291 +0,0 @@ -// -// ChipView.swift -// Baccarat -// -// Casino-style betting chips with realistic design. -// - -import SwiftUI - -/// The available chip denominations. -enum ChipDenomination: Int, CaseIterable, Identifiable { - case ten = 10 - case twentyFive = 25 - case fifty = 50 - case hundred = 100 - case fiveHundred = 500 - case thousand = 1_000 - case fiveThousand = 5_000 - case tenThousand = 10_000 - case twentyFiveThousand = 25_000 - case fiftyThousand = 50_000 - case hundredThousand = 100_000 - - var id: Int { rawValue } - - /// The display text for this denomination. - var displayText: String { - switch self { - case .ten: return "10" - case .twentyFive: return "25" - case .fifty: return "50" - case .hundred: return "100" - case .fiveHundred: return "500" - case .thousand: return "1K" - case .fiveThousand: return "5K" - case .tenThousand: return "10K" - case .twentyFiveThousand: return "25K" - case .fiftyThousand: return "50K" - case .hundredThousand: return "100K" - } - } - - /// The minimum balance required to show this chip in the selector. - /// Higher chips unlock as you win more! - var unlockBalance: Int { - switch self { - case .ten, .twentyFive, .fifty, .hundred: return 0 // Always available - case .fiveHundred: return 500 - case .thousand: return 1_000 - case .fiveThousand: return 5_000 - case .tenThousand: return 10_000 - case .twentyFiveThousand: return 25_000 - case .fiftyThousand: return 50_000 - case .hundredThousand: return 100_000 - } - } - - /// Whether this chip should be shown based on the player's balance. - func isUnlocked(forBalance balance: Int) -> Bool { - balance >= unlockBalance - } - - /// Returns chips that should be visible for a given balance. - static func availableChips(forBalance balance: Int) -> [ChipDenomination] { - allCases.filter { $0.isUnlocked(forBalance: balance) } - } - - /// The primary color for this chip. - var primaryColor: Color { - switch self { - case .ten: return Color.Chip.tenBase - case .twentyFive: return Color.Chip.twentyFiveBase - case .fifty: return Color.Chip.fiftyBase - case .hundred: return Color.Chip.hundredBase - case .fiveHundred: return Color.Chip.fiveHundredBase - case .thousand: return Color.Chip.thousandBase - case .fiveThousand: return Color.Chip.fiveThousandBase - case .tenThousand: return Color.Chip.tenThousandBase - case .twentyFiveThousand: return Color.Chip.twentyFiveThousandBase - case .fiftyThousand: return Color.Chip.fiftyThousandBase - case .hundredThousand: return Color.Chip.hundredThousandBase - } - } - - /// The secondary/accent color for this chip. - var secondaryColor: Color { - switch self { - case .ten: return Color.Chip.tenHighlight - case .twentyFive: return Color.Chip.twentyFiveHighlight - case .fifty: return Color.Chip.fiftyHighlight - case .hundred: return Color.Chip.hundredHighlight - case .fiveHundred: return Color.Chip.fiveHundredHighlight - case .thousand: return Color.Chip.thousandHighlight - case .fiveThousand: return Color.Chip.fiveThousandHighlight - case .tenThousand: return Color.Chip.tenThousandHighlight - case .twentyFiveThousand: return Color.Chip.twentyFiveThousandHighlight - case .fiftyThousand: return Color.Chip.fiftyThousandHighlight - case .hundredThousand: return Color.Chip.hundredThousandHighlight - } - } - - /// The edge stripe color for this chip. - var stripeColor: Color { - switch self { - case .ten, .twentyFive, .fifty: return .white - case .hundred: return Color.Chip.goldStripe - case .fiveHundred: return .white - case .thousand: return .black - case .fiveThousand: return Color.Chip.goldStripe - case .tenThousand: return .white - case .twentyFiveThousand: return Color.Chip.goldStripe - case .fiftyThousand: return Color.Chip.darkStripe - case .hundredThousand: return Color.Chip.goldRubyStripe - } - } -} - -/// A realistic casino-style betting chip. -struct ChipView: View { - let denomination: ChipDenomination - let size: CGFloat - var isSelected: Bool = false - - init(denomination: ChipDenomination, size: CGFloat = 60, isSelected: Bool = false) { - self.denomination = denomination - self.size = size - self.isSelected = isSelected - } - - var body: some View { - ZStack { - // Base circle with gradient - Circle() - .fill( - RadialGradient( - colors: [ - denomination.secondaryColor, - denomination.primaryColor, - denomination.primaryColor.opacity(0.8) - ], - center: .topLeading, - startRadius: 0, - endRadius: size - ) - ) - - // Edge stripes pattern - ChipEdgePattern(stripeColor: denomination.stripeColor) - .clipShape(.circle) - - // Inner circle - Circle() - .fill( - RadialGradient( - colors: [ - denomination.secondaryColor, - denomination.primaryColor - ], - center: .topLeading, - startRadius: 0, - endRadius: size * 0.4 - ) - ) - .frame(width: size * 0.65, height: size * 0.65) - - // Inner border - Circle() - .strokeBorder( - denomination.stripeColor.opacity(0.8), - lineWidth: 2 - ) - .frame(width: size * 0.65, height: size * 0.65) - - // Denomination text - Text(denomination.displayText) - .font(.system(size: size * 0.25, weight: .heavy, design: .rounded)) - .foregroundStyle(denomination.stripeColor) - .shadow(color: .black.opacity(0.3), radius: 1, x: 1, y: 1) - - // Outer border - Circle() - .strokeBorder( - LinearGradient( - colors: [ - Color.white.opacity(0.4), - Color.black.opacity(0.3) - ], - startPoint: .topLeading, - endPoint: .bottomTrailing - ), - lineWidth: 2 - ) - - // Selection glow - if isSelected { - Circle() - .strokeBorder(Color.yellow, lineWidth: 3) - .frame(width: size + 6, height: size + 6) - } - } - .frame(width: size, height: size) - .shadow(color: .black.opacity(0.4), radius: isSelected ? 8 : 4, x: 2, y: 3) - .scaleEffect(isSelected ? 1.1 : 1.0) - .animation(.spring(duration: 0.2), value: isSelected) - } -} - -/// The edge stripe pattern for chips. -struct ChipEdgePattern: View { - let stripeColor: Color - - 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 = 16 - let stripeWidth: CGFloat = 6 - - for i in 0.. 0 { - result.append((denom, min(count, maxChips))) - remaining -= count * denom.rawValue - } - } - - return result - } - - var body: some View { - ZStack { - ForEach(chipBreakdown.indices, id: \.self) { index in - let (denom, _) = chipBreakdown[index] - ChipView(denomination: denom, size: 40) - .offset(y: CGFloat(-index * 4)) - } - } - } -} - -#Preview { - ZStack { - Color.Table.preview - .ignoresSafeArea() - - VStack(spacing: Design.Spacing.xxxLarge) { - HStack(spacing: Design.Spacing.xLarge) { - ForEach(ChipDenomination.allCases) { denom in - ChipView(denomination: denom, size: Design.Size.chipSelector) - } - } - - ChipView(denomination: .hundred, size: 80, isSelected: true) - } - } -} - diff --git a/Baccarat/Views/Chips/ChipEdgePattern.swift b/Baccarat/Views/Chips/ChipEdgePattern.swift new file mode 100644 index 0000000..e7ceb34 --- /dev/null +++ b/Baccarat/Views/Chips/ChipEdgePattern.swift @@ -0,0 +1,62 @@ +// +// ChipEdgePattern.swift +// Baccarat +// +// The edge stripe pattern for casino chips. +// + +import SwiftUI + +/// The edge stripe pattern for chips. +struct ChipEdgePattern: View { + let stripeColor: Color + + // MARK: - Layout Constants + + private let stripeCount = 16 + private let stripeWidth: CGFloat = 6 + private let innerRadiusRatio: CGFloat = 0.75 + private let outerRadiusRatio: CGFloat = 0.98 + + 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 + + for i in 0..= 1000 ? "\(amount / 1000)K" : "\(amount)" + } + + private var textFontSize: CGFloat { + amount >= 1000 ? Design.BaseFontSize.small : 11 + } + + // MARK: - Body + + var body: some View { + ZStack { + Circle() + .fill( + RadialGradient( + colors: [chipColor.opacity(0.9), chipColor], + center: .topLeading, + startRadius: 0, + endRadius: gradientEndRadius + ) + ) + .frame(width: chipSize, height: chipSize) + + Circle() + .strokeBorder(Color.white.opacity(Design.Opacity.heavy), lineWidth: Design.LineWidth.medium) + .frame(width: chipSize, height: chipSize) + + Circle() + .strokeBorder(Color.white.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.thin) + .frame(width: innerRingSize, height: innerRingSize) + + Text(displayText) + .font(.system(size: textFontSize, weight: .bold)) + .foregroundStyle(.white) + } + .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: maxBadgeFontSize, weight: .black)) + .foregroundStyle(.white) + .padding(.horizontal, Design.Spacing.xSmall) + .padding(.vertical, Design.Spacing.xxSmall) + .background( + Capsule() + .fill(Color.red) + ) + .offset(x: maxBadgeOffsetX, y: maxBadgeOffsetY) + } + } + } +} + +#Preview { + ZStack { + Color.Table.preview + .ignoresSafeArea() + + HStack(spacing: Design.Spacing.xLarge) { + ChipOnTable(amount: 25) + ChipOnTable(amount: 100) + ChipOnTable(amount: 500) + ChipOnTable(amount: 1000) + ChipOnTable(amount: 5000, showMax: true) + } + } +} + diff --git a/Baccarat/Views/ChipSelectorView.swift b/Baccarat/Views/Chips/ChipSelectorView.swift similarity index 100% rename from Baccarat/Views/ChipSelectorView.swift rename to Baccarat/Views/Chips/ChipSelectorView.swift diff --git a/Baccarat/Views/Chips/ChipStackView.swift b/Baccarat/Views/Chips/ChipStackView.swift new file mode 100644 index 0000000..d4b2d78 --- /dev/null +++ b/Baccarat/Views/Chips/ChipStackView.swift @@ -0,0 +1,64 @@ +// +// ChipStackView.swift +// Baccarat +// +// A stacked display of chips representing a bet amount. +// + +import SwiftUI + +/// A stack of chips showing a bet amount. +struct ChipStackView: View { + let amount: Int + let maxChips: Int + + // MARK: - Layout Constants + + private let chipSize: CGFloat = 40 + private let stackOffset: CGFloat = 4 + + init(amount: Int, maxChips: Int = 5) { + self.amount = amount + self.maxChips = maxChips + } + + private var chipBreakdown: [(ChipDenomination, Int)] { + var remaining = amount + var result: [(ChipDenomination, Int)] = [] + + for denom in ChipDenomination.allCases.reversed() { + let count = remaining / denom.rawValue + if count > 0 { + result.append((denom, min(count, maxChips))) + remaining -= count * denom.rawValue + } + } + + return result + } + + var body: some View { + ZStack { + ForEach(chipBreakdown.indices, id: \.self) { index in + let (denom, _) = chipBreakdown[index] + ChipView(denomination: denom, size: chipSize) + .offset(y: CGFloat(-index) * stackOffset) + } + } + } +} + +#Preview { + ZStack { + Color.Table.preview + .ignoresSafeArea() + + HStack(spacing: Design.Spacing.xxxLarge) { + ChipStackView(amount: 100) + ChipStackView(amount: 550) + ChipStackView(amount: 1500) + ChipStackView(amount: 10_000) + } + } +} + diff --git a/Baccarat/Views/Chips/ChipView.swift b/Baccarat/Views/Chips/ChipView.swift new file mode 100644 index 0000000..9b41f0a --- /dev/null +++ b/Baccarat/Views/Chips/ChipView.swift @@ -0,0 +1,116 @@ +// +// ChipView.swift +// Baccarat +// +// A realistic casino-style betting chip. +// + +import SwiftUI + +/// A realistic casino-style betting chip. +struct ChipView: View { + let denomination: ChipDenomination + let size: CGFloat + var isSelected: Bool = false + + init(denomination: ChipDenomination, size: CGFloat = 60, isSelected: Bool = false) { + self.denomination = denomination + self.size = size + self.isSelected = isSelected + } + + var body: some View { + ZStack { + // Base circle with gradient + Circle() + .fill( + RadialGradient( + colors: [ + denomination.secondaryColor, + denomination.primaryColor, + denomination.primaryColor.opacity(0.8) + ], + center: .topLeading, + startRadius: 0, + endRadius: size + ) + ) + + // Edge stripes pattern + ChipEdgePattern(stripeColor: denomination.stripeColor) + .clipShape(.circle) + + // Inner circle + Circle() + .fill( + RadialGradient( + colors: [ + denomination.secondaryColor, + denomination.primaryColor + ], + center: .topLeading, + startRadius: 0, + endRadius: size * 0.4 + ) + ) + .frame(width: size * 0.65, height: size * 0.65) + + // Inner border + Circle() + .strokeBorder( + denomination.stripeColor.opacity(0.8), + lineWidth: Design.LineWidth.medium + ) + .frame(width: size * 0.65, height: size * 0.65) + + // Denomination text + Text(denomination.displayText) + .font(.system(size: size * 0.25, weight: .heavy, design: .rounded)) + .foregroundStyle(denomination.stripeColor) + .shadow(color: .black.opacity(Design.Opacity.light), radius: 1, x: 1, y: 1) + + // Outer border + Circle() + .strokeBorder( + LinearGradient( + colors: [ + Color.white.opacity(Design.Opacity.overlay), + Color.black.opacity(Design.Opacity.light) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: Design.LineWidth.medium + ) + + // Selection glow + if isSelected { + Circle() + .strokeBorder(Color.yellow, lineWidth: Design.LineWidth.thick) + .frame(width: size + 6, height: size + 6) + } + } + .frame(width: size, height: size) + .shadow(color: .black.opacity(Design.Opacity.overlay), radius: isSelected ? 8 : 4, x: 2, y: 3) + .scaleEffect(isSelected ? 1.1 : 1.0) + .animation(.spring(duration: 0.2), value: isSelected) + } +} + +#Preview { + ZStack { + Color.Table.preview + .ignoresSafeArea() + + VStack(spacing: Design.Spacing.xxxLarge) { + HStack(spacing: Design.Spacing.xLarge) { + ForEach(ChipDenomination.allCases) { denom in + ChipView(denomination: denom, size: Design.Size.chipSelector) + } + } + + ChipView(denomination: .hundred, size: 80, isSelected: true) + } + } +} + diff --git a/Baccarat/Views/MiniBaccaratTableView.swift b/Baccarat/Views/MiniBaccaratTableView.swift index 5a3b415..e0ff9af 100644 --- a/Baccarat/Views/MiniBaccaratTableView.swift +++ b/Baccarat/Views/MiniBaccaratTableView.swift @@ -450,86 +450,6 @@ struct PlayerBettingZone: View { } } -/// A chip displayed on the table showing bet amount. -struct ChipOnTable: View { - let amount: Int - var showMax: Bool = false - - // MARK: - Layout Constants - // Fixed sizes: chip face has strict space constraints - - private let chipSize = Design.Size.chipSmall - private let innerRingSize: CGFloat = 26 - private let gradientEndRadius: CGFloat = 20 - private let maxBadgeFontSize = Design.BaseFontSize.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.Chip.gold - } - } - - private var displayText: String { - amount >= 1000 ? "\(amount / 1000)K" : "\(amount)" - } - - private var textFontSize: CGFloat { - amount >= 1000 ? Design.BaseFontSize.small : 11 - } - - // MARK: - Body - - var body: some View { - ZStack { - Circle() - .fill( - RadialGradient( - colors: [chipColor.opacity(0.9), chipColor], - center: .topLeading, - startRadius: 0, - endRadius: gradientEndRadius - ) - ) - .frame(width: chipSize, height: chipSize) - - Circle() - .strokeBorder(Color.white.opacity(Design.Opacity.heavy), lineWidth: Design.LineWidth.medium) - .frame(width: chipSize, height: chipSize) - - Circle() - .strokeBorder(Color.white.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.thin) - .frame(width: innerRingSize, height: innerRingSize) - - Text(displayText) - .font(.system(size: textFontSize, weight: .bold)) - .foregroundStyle(.white) - } - .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: maxBadgeFontSize, weight: .black)) - .foregroundStyle(.white) - .padding(.horizontal, Design.Spacing.xSmall) - .padding(.vertical, Design.Spacing.xxSmall) - .background( - Capsule() - .fill(Color.red) - ) - .offset(x: maxBadgeOffsetX, y: maxBadgeOffsetY) - } - } - } -} - #Preview { ZStack { Color.Table.baseDark