Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
2b7b770d3a
commit
c846ef05ac
117
Baccarat/Models/ChipDenomination.swift
Normal file
117
Baccarat/Models/ChipDenomination.swift
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -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..<stripeCount {
|
|
||||||
let angle = Double(i) * (360.0 / Double(stripeCount)) * .pi / 180
|
|
||||||
|
|
||||||
let innerRadius = radius * 0.75
|
|
||||||
let outerRadius = radius * 0.98
|
|
||||||
|
|
||||||
let startX = center.x + cos(angle) * innerRadius
|
|
||||||
let startY = center.y + sin(angle) * innerRadius
|
|
||||||
let endX = center.x + cos(angle) * outerRadius
|
|
||||||
let endY = center.y + sin(angle) * outerRadius
|
|
||||||
|
|
||||||
var path = Path()
|
|
||||||
path.move(to: CGPoint(x: startX, y: startY))
|
|
||||||
path.addLine(to: CGPoint(x: endX, y: endY))
|
|
||||||
|
|
||||||
context.stroke(
|
|
||||||
path,
|
|
||||||
with: .color(stripeColor),
|
|
||||||
lineWidth: stripeWidth
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A stack of chips showing a bet amount.
|
|
||||||
struct ChipStackView: View {
|
|
||||||
let amount: Int
|
|
||||||
let maxChips: Int = 5
|
|
||||||
|
|
||||||
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: 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
62
Baccarat/Views/Chips/ChipEdgePattern.swift
Normal file
62
Baccarat/Views/Chips/ChipEdgePattern.swift
Normal file
@ -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..<stripeCount {
|
||||||
|
let angle = Double(i) * (360.0 / Double(stripeCount)) * .pi / 180
|
||||||
|
|
||||||
|
let innerRadius = radius * innerRadiusRatio
|
||||||
|
let outerRadius = radius * outerRadiusRatio
|
||||||
|
|
||||||
|
let startX = center.x + cos(angle) * innerRadius
|
||||||
|
let startY = center.y + sin(angle) * innerRadius
|
||||||
|
let endX = center.x + cos(angle) * outerRadius
|
||||||
|
let endY = center.y + sin(angle) * outerRadius
|
||||||
|
|
||||||
|
var path = Path()
|
||||||
|
path.move(to: CGPoint(x: startX, y: startY))
|
||||||
|
path.addLine(to: CGPoint(x: endX, y: endY))
|
||||||
|
|
||||||
|
context.stroke(
|
||||||
|
path,
|
||||||
|
with: .color(stripeColor),
|
||||||
|
lineWidth: stripeWidth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ZStack {
|
||||||
|
Color.Table.preview
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
ChipEdgePattern(stripeColor: .white)
|
||||||
|
.frame(width: 100, height: 100)
|
||||||
|
.clipShape(.circle)
|
||||||
|
.background(Circle().fill(.red))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
104
Baccarat/Views/Chips/ChipOnTable.swift
Normal file
104
Baccarat/Views/Chips/ChipOnTable.swift
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
//
|
||||||
|
// ChipOnTable.swift
|
||||||
|
// Baccarat
|
||||||
|
//
|
||||||
|
// A simplified chip display for showing bets on the table.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A simplified chip for displaying bets on the table.
|
||||||
|
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.preview
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
HStack(spacing: Design.Spacing.xLarge) {
|
||||||
|
ChipOnTable(amount: 25)
|
||||||
|
ChipOnTable(amount: 100)
|
||||||
|
ChipOnTable(amount: 500)
|
||||||
|
ChipOnTable(amount: 1000)
|
||||||
|
ChipOnTable(amount: 5000, showMax: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
64
Baccarat/Views/Chips/ChipStackView.swift
Normal file
64
Baccarat/Views/Chips/ChipStackView.swift
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
116
Baccarat/Views/Chips/ChipView.swift
Normal file
116
Baccarat/Views/Chips/ChipView.swift
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -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 {
|
#Preview {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.Table.baseDark
|
Color.Table.baseDark
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user