Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
d58f50b6ac
commit
e2785c3a48
@ -260,7 +260,6 @@ struct ResultBannerView: View {
|
|||||||
}
|
}
|
||||||
.accessibilityElement(children: .ignore)
|
.accessibilityElement(children: .ignore)
|
||||||
.accessibilityLabel(accessibilityDescription)
|
.accessibilityLabel(accessibilityDescription)
|
||||||
.accessibilityAddTraits(.updatesFrequently)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Accessibility
|
// MARK: - Accessibility
|
||||||
@ -401,61 +400,7 @@ private struct PairBadge: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Confetti particle for celebrations.
|
// Note: ConfettiView is now provided by CasinoKit
|
||||||
struct ConfettiPiece: View {
|
|
||||||
let color: Color
|
|
||||||
let containerSize: CGSize
|
|
||||||
|
|
||||||
@State private var position: CGPoint = .zero
|
|
||||||
@State private var rotation: Double = 0
|
|
||||||
@State private var opacity: Double = 1
|
|
||||||
|
|
||||||
private let confettiWidth: CGFloat = 8
|
|
||||||
private let confettiHeight: CGFloat = 12
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Rectangle()
|
|
||||||
.fill(color)
|
|
||||||
.frame(width: confettiWidth, height: confettiHeight)
|
|
||||||
.rotationEffect(.degrees(rotation))
|
|
||||||
.position(position)
|
|
||||||
.opacity(opacity)
|
|
||||||
.onAppear {
|
|
||||||
let startX = Double.random(in: 0...containerSize.width)
|
|
||||||
position = CGPoint(x: startX, y: -20)
|
|
||||||
|
|
||||||
withAnimation(.easeIn(duration: Double.random(in: 2...4))) {
|
|
||||||
position = CGPoint(
|
|
||||||
x: startX + Double.random(in: -100...100),
|
|
||||||
y: containerSize.height + 50
|
|
||||||
)
|
|
||||||
rotation = Double.random(in: 360...1080)
|
|
||||||
opacity = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A confetti celebration overlay.
|
|
||||||
struct ConfettiView: View {
|
|
||||||
let colors: [Color] = [.red, .blue, .green, .yellow, .orange, .purple, .pink]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
GeometryReader { geometry in
|
|
||||||
ZStack {
|
|
||||||
ForEach(0..<50, id: \.self) { _ in
|
|
||||||
ConfettiPiece(
|
|
||||||
color: colors.randomElement() ?? .yellow,
|
|
||||||
containerSize: geometry.size
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ignoresSafeArea()
|
|
||||||
.allowsHitTesting(false)
|
|
||||||
.accessibilityHidden(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview("Win") {
|
#Preview("Win") {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
// - Card, Suit, Rank
|
// - Card, Suit, Rank
|
||||||
// - Deck
|
// - Deck
|
||||||
// - ChipDenomination
|
// - ChipDenomination
|
||||||
|
// - TableLimits
|
||||||
|
|
||||||
// MARK: - Views
|
// MARK: - Views
|
||||||
// - CardView, CardFrontView, CardBackView, CardPlaceholderView
|
// - CardView, CardFrontView, CardBackView, CardPlaceholderView
|
||||||
@ -21,6 +22,27 @@
|
|||||||
// - ChipStackView, ChipOnTableView
|
// - ChipStackView, ChipOnTableView
|
||||||
// - SheetContainerView, SheetSection
|
// - SheetContainerView, SheetSection
|
||||||
|
|
||||||
|
// MARK: - Effects
|
||||||
|
// - ConfettiView, ConfettiPiece
|
||||||
|
|
||||||
|
// MARK: - Overlays
|
||||||
|
// - GameOverView
|
||||||
|
|
||||||
|
// MARK: - Table
|
||||||
|
// - TableBackgroundView, FeltPatternView
|
||||||
|
|
||||||
|
// MARK: - Bars
|
||||||
|
// - TopBarView
|
||||||
|
|
||||||
|
// MARK: - Badges
|
||||||
|
// - ValueBadge
|
||||||
|
|
||||||
|
// MARK: - Settings
|
||||||
|
// - SettingsToggle
|
||||||
|
// - SpeedPicker
|
||||||
|
// - VolumePicker
|
||||||
|
// - BalancePicker
|
||||||
|
|
||||||
// MARK: - Branding
|
// MARK: - Branding
|
||||||
// - AppIconView, AppIconConfig
|
// - AppIconView, AppIconConfig
|
||||||
// - LaunchScreenView, LaunchScreenConfig, StaticLaunchScreenView
|
// - LaunchScreenView, LaunchScreenConfig, StaticLaunchScreenView
|
||||||
@ -31,7 +53,7 @@
|
|||||||
// - DefaultCasinoTheme
|
// - DefaultCasinoTheme
|
||||||
// - ChipColorSet
|
// - ChipColorSet
|
||||||
// - CasinoDesign (constants)
|
// - CasinoDesign (constants)
|
||||||
// - Color.Sheet (sheet colors)
|
// - Color.Sheet, Color.Button, Color.Modal, Color.Table, Color.TopBar (colors)
|
||||||
|
|
||||||
// MARK: - Audio
|
// MARK: - Audio
|
||||||
// - SoundManager
|
// - SoundManager
|
||||||
|
|||||||
69
CasinoKit/Sources/CasinoKit/Models/TableLimits.swift
Normal file
69
CasinoKit/Sources/CasinoKit/Models/TableLimits.swift
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
//
|
||||||
|
// TableLimits.swift
|
||||||
|
// CasinoKit
|
||||||
|
//
|
||||||
|
// Preset table limits for casino games.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Preset table limits for betting.
|
||||||
|
public enum TableLimits: String, CaseIterable, Identifiable, Sendable {
|
||||||
|
case casual = "casual"
|
||||||
|
case low = "low"
|
||||||
|
case medium = "medium"
|
||||||
|
case high = "high"
|
||||||
|
case vip = "vip"
|
||||||
|
|
||||||
|
public var id: String { rawValue }
|
||||||
|
|
||||||
|
/// Display name for the limit tier.
|
||||||
|
public var displayName: String {
|
||||||
|
switch self {
|
||||||
|
case .casual: return String(localized: "Casual", bundle: .module)
|
||||||
|
case .low: return String(localized: "Low Stakes", bundle: .module)
|
||||||
|
case .medium: return String(localized: "Medium Stakes", bundle: .module)
|
||||||
|
case .high: return String(localized: "High Stakes", bundle: .module)
|
||||||
|
case .vip: return String(localized: "VIP", bundle: .module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimum bet for this limit tier.
|
||||||
|
public var minBet: Int {
|
||||||
|
switch self {
|
||||||
|
case .casual: return 5
|
||||||
|
case .low: return 10
|
||||||
|
case .medium: return 25
|
||||||
|
case .high: return 100
|
||||||
|
case .vip: return 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maximum bet for this limit tier.
|
||||||
|
public var maxBet: Int {
|
||||||
|
switch self {
|
||||||
|
case .casual: return 500
|
||||||
|
case .low: return 1_000
|
||||||
|
case .medium: return 5_000
|
||||||
|
case .high: return 10_000
|
||||||
|
case .vip: return 50_000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Short description showing the bet range.
|
||||||
|
public var description: String {
|
||||||
|
"$\(minBet) - $\(maxBet.formatted())"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detailed description of the limit tier.
|
||||||
|
public var detailedDescription: String {
|
||||||
|
switch self {
|
||||||
|
case .casual: return String(localized: "Perfect for learning", bundle: .module)
|
||||||
|
case .low: return String(localized: "Standard mini table", bundle: .module)
|
||||||
|
case .medium: return String(localized: "Regular casino table", bundle: .module)
|
||||||
|
case .high: return String(localized: "High roller table", bundle: .module)
|
||||||
|
case .vip: return String(localized: "Exclusive VIP room", bundle: .module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"sourceLanguage" : "en",
|
"sourceLanguage" : "en",
|
||||||
"strings" : {
|
"strings" : {
|
||||||
|
"%lld" : {
|
||||||
|
"comment" : "A badge displaying a numeric value. The argument is the numeric value to display.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"%lld dollar bet" : {
|
"%lld dollar bet" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -45,10 +49,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"%lld%%" : {
|
||||||
|
"comment" : "A text displaying the current volume percentage. The argument is a value between 0.0 (no volume) and 1.0 (full volume).",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"%lldpt" : {
|
"%lldpt" : {
|
||||||
"comment" : "A caption below an app icon that shows its size in points. The argument is the size of the icon in points.",
|
"comment" : "A caption below an app icon that shows its size in points. The argument is the size of the icon in points.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"$" : {
|
||||||
|
"comment" : "The dollar sign used in the top bar.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"$%@" : {
|
||||||
|
"comment" : "The value of the balance displayed in the top bar.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"1. Use Xcode's preview to screenshot these icons" : {
|
"1. Use Xcode's preview to screenshot these icons" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
@ -86,6 +102,28 @@
|
|||||||
"comment" : "A title for the preview section of the icon export view.",
|
"comment" : "A title for the preview section of the icon export view.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Balance" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Balance"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Saldo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Solde"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Card face down" : {
|
"Card face down" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -108,6 +146,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Casual" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Casual"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Casual"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Décontracté"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Chip selector" : {
|
"Chip selector" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -152,6 +212,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Dealing Speed" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Dealing Speed"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Velocidad de reparto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Vitesse de distribution"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Diamonds" : {
|
"Diamonds" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -240,6 +322,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Exclusive VIP room" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Exclusive VIP room"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Sala VIP exclusiva"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Salle VIP exclusive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Export Instructions" : {
|
"Export Instructions" : {
|
||||||
"comment" : "A section header describing how to export app icons.",
|
"comment" : "A section header describing how to export app icons.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -288,6 +392,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Game Over" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Game Over"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Fin del juego"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Fin de partie"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GAME OVER" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "GAME OVER"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "FIN DEL JUEGO"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "FIN DE PARTIE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Hearts" : {
|
"Hearts" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -310,6 +458,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"High roller table" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "High roller table"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mesa para grandes apostadores"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Table pour gros joueurs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"High Stakes" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "High Stakes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Apuestas altas"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Grosses mises"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Jack" : {
|
"Jack" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -354,6 +546,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Low Stakes" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Low Stakes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Apuestas bajas"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Petites mises"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"MAX" : {
|
"MAX" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -398,6 +612,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Medium Stakes" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Medium Stakes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Apuestas medias"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mises moyennes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Nine" : {
|
"Nine" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -420,6 +656,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Perfect for learning" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Perfect for learning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Perfecto para aprender"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Parfait pour apprendre"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Play Again" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Play Again"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Jugar de nuevo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Rejouer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Queen" : {
|
"Queen" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -442,6 +722,94 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Regular casino table" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Regular casino table"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mesa de casino regular"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Table de casino standard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Reset Game" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Reset Game"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Reiniciar juego"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Réinitialiser"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Rounds Played" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Rounds Played"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Rondas jugadas"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Parties jouées"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Rules" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Rules"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Reglas"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Règles"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Selected" : {
|
"Selected" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -464,6 +832,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Settings" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Settings"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Configuración"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Paramètres"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Seven" : {
|
"Seven" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -534,6 +924,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Standard mini table" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Standard mini table"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mesa mini estándar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Table mini standard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Statistics" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Statistics"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Estadísticas"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Statistiques"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Ten" : {
|
"Ten" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -599,6 +1033,72 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"VIP" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "VIP"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "VIP"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "VIP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Volume" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Volume"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Volumen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Volume"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"You've run out of chips!" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "You've run out of chips!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "¡Te quedaste sin fichas!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Vous n'avez plus de jetons!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version" : "1.1"
|
"version" : "1.1"
|
||||||
|
|||||||
@ -32,6 +32,7 @@ public enum CasinoDesign {
|
|||||||
public static let large: CGFloat = 16
|
public static let large: CGFloat = 16
|
||||||
public static let xLarge: CGFloat = 20
|
public static let xLarge: CGFloat = 20
|
||||||
public static let xxLarge: CGFloat = 24
|
public static let xxLarge: CGFloat = 24
|
||||||
|
public static let xxxLarge: CGFloat = 32
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Line Width
|
// MARK: - Line Width
|
||||||
@ -49,6 +50,8 @@ public enum CasinoDesign {
|
|||||||
public static let radiusSmall: CGFloat = 4
|
public static let radiusSmall: CGFloat = 4
|
||||||
public static let radiusMedium: CGFloat = 8
|
public static let radiusMedium: CGFloat = 8
|
||||||
public static let radiusLarge: CGFloat = 12
|
public static let radiusLarge: CGFloat = 12
|
||||||
|
public static let radiusXLarge: CGFloat = 16
|
||||||
|
public static let radiusXXLarge: CGFloat = 24
|
||||||
|
|
||||||
public static let offsetSmall: CGFloat = 1
|
public static let offsetSmall: CGFloat = 1
|
||||||
public static let offsetMedium: CGFloat = 2
|
public static let offsetMedium: CGFloat = 2
|
||||||
@ -58,8 +61,10 @@ public enum CasinoDesign {
|
|||||||
// MARK: - Opacity
|
// MARK: - Opacity
|
||||||
|
|
||||||
public enum Opacity {
|
public enum Opacity {
|
||||||
public static let subtle: CGFloat = 0.05
|
public static let verySubtle: CGFloat = 0.05
|
||||||
public static let light: CGFloat = 0.2
|
public static let subtle: CGFloat = 0.1
|
||||||
|
public static let hint: CGFloat = 0.2
|
||||||
|
public static let light: CGFloat = 0.3
|
||||||
public static let quarter: CGFloat = 0.25
|
public static let quarter: CGFloat = 0.25
|
||||||
public static let medium: CGFloat = 0.5
|
public static let medium: CGFloat = 0.5
|
||||||
public static let accent: CGFloat = 0.6
|
public static let accent: CGFloat = 0.6
|
||||||
@ -100,6 +105,19 @@ public enum CasinoDesign {
|
|||||||
/// Chip edge stripe dimensions.
|
/// Chip edge stripe dimensions.
|
||||||
public static let chipStripeWidth: CGFloat = 4
|
public static let chipStripeWidth: CGFloat = 4
|
||||||
public static let chipStripeInset: CGFloat = 2
|
public static let chipStripeInset: CGFloat = 2
|
||||||
|
|
||||||
|
/// iPad max widths for overlays and content.
|
||||||
|
public static let maxContentWidthPortrait: CGFloat = 500
|
||||||
|
public static let maxContentWidthLandscape: CGFloat = 800
|
||||||
|
public static let maxModalWidth: CGFloat = 450
|
||||||
|
|
||||||
|
/// Value badge size.
|
||||||
|
public static let valueBadge: CGFloat = 26
|
||||||
|
|
||||||
|
/// Icon sizes.
|
||||||
|
public static let iconSmall: CGFloat = 16
|
||||||
|
public static let iconMedium: CGFloat = 20
|
||||||
|
public static let iconLarge: CGFloat = 24
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Font Sizes (Base values for @ScaledMetric)
|
// MARK: - Font Sizes (Base values for @ScaledMetric)
|
||||||
@ -162,5 +180,45 @@ public extension Color {
|
|||||||
/// Cancel button color.
|
/// Cancel button color.
|
||||||
public static let cancelText = Color.white.opacity(CasinoDesign.Opacity.strong)
|
public static let cancelText = Color.white.opacity(CasinoDesign.Opacity.strong)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Button colors (gold gradient, destructive, etc.).
|
||||||
|
enum CasinoButton {
|
||||||
|
/// Light gold for button gradients.
|
||||||
|
public static let goldLight = Color(red: 1.0, green: 0.85, blue: 0.3)
|
||||||
|
/// Dark gold for button gradients.
|
||||||
|
public static let goldDark = Color(red: 0.9, green: 0.7, blue: 0.2)
|
||||||
|
/// Destructive button color (red).
|
||||||
|
public static let destructive = Color.red.opacity(0.8)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modal overlay colors.
|
||||||
|
enum CasinoModal {
|
||||||
|
/// Light background for modal cards.
|
||||||
|
public static let backgroundLight = Color(white: 0.15)
|
||||||
|
/// Dark background for modal cards.
|
||||||
|
public static let backgroundDark = Color(white: 0.08)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Table colors.
|
||||||
|
enum CasinoTable {
|
||||||
|
/// Casino table green felt.
|
||||||
|
public static let felt = Color(red: 0.05, green: 0.25, blue: 0.15)
|
||||||
|
/// Darker felt for gradients.
|
||||||
|
public static let feltDark = Color(red: 0.02, green: 0.15, blue: 0.08)
|
||||||
|
/// Table edge border.
|
||||||
|
public static let border = Color(red: 0.3, green: 0.2, blue: 0.1)
|
||||||
|
/// Gold accent for table elements.
|
||||||
|
public static let gold = Color(red: 0.85, green: 0.65, blue: 0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Top bar colors.
|
||||||
|
enum CasinoTopBar {
|
||||||
|
/// Balance text color.
|
||||||
|
public static let balanceText = Color.yellow
|
||||||
|
/// Secondary info color.
|
||||||
|
public static let secondaryText = Color.white.opacity(CasinoDesign.Opacity.medium)
|
||||||
|
/// Icon button color.
|
||||||
|
public static let iconButton = Color.white.opacity(CasinoDesign.Opacity.strong)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
63
CasinoKit/Sources/CasinoKit/Views/Badges/ValueBadge.swift
Normal file
63
CasinoKit/Sources/CasinoKit/Views/Badges/ValueBadge.swift
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
//
|
||||||
|
// ValueBadge.swift
|
||||||
|
// CasinoKit
|
||||||
|
//
|
||||||
|
// A circular badge showing a numeric value (hand value, score, etc.).
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A circular badge showing a numeric value.
|
||||||
|
public struct ValueBadge: View {
|
||||||
|
/// The value to display.
|
||||||
|
public let value: Int
|
||||||
|
|
||||||
|
/// The background color of the badge.
|
||||||
|
public let color: Color
|
||||||
|
|
||||||
|
/// The size of the badge (default: uses CasinoDesign.Size.valueBadge).
|
||||||
|
public let size: CGFloat?
|
||||||
|
|
||||||
|
// MARK: - Scaled Font Sizes (Dynamic Type)
|
||||||
|
|
||||||
|
@ScaledMetric(relativeTo: .headline) private var valueFontSize: CGFloat = 15
|
||||||
|
@ScaledMetric(relativeTo: .headline) private var badgeSize: CGFloat = CasinoDesign.Size.valueBadge
|
||||||
|
|
||||||
|
/// Creates a value badge.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - value: The numeric value to display.
|
||||||
|
/// - color: The background color.
|
||||||
|
/// - size: Optional custom size (overrides default).
|
||||||
|
public init(value: Int, color: Color, size: CGFloat? = nil) {
|
||||||
|
self.value = value
|
||||||
|
self.color = color
|
||||||
|
self.size = size
|
||||||
|
}
|
||||||
|
|
||||||
|
private var displaySize: CGFloat {
|
||||||
|
size ?? badgeSize
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Text("\(value)")
|
||||||
|
.font(.system(size: valueFontSize, weight: .black, design: .rounded))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(width: displaySize, height: displaySize)
|
||||||
|
.background(
|
||||||
|
Circle()
|
||||||
|
.fill(color)
|
||||||
|
)
|
||||||
|
.accessibilityLabel("\(value)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
HStack(spacing: 20) {
|
||||||
|
ValueBadge(value: 9, color: .blue)
|
||||||
|
ValueBadge(value: 8, color: .red)
|
||||||
|
ValueBadge(value: 0, color: .purple)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.black)
|
||||||
|
}
|
||||||
|
|
||||||
169
CasinoKit/Sources/CasinoKit/Views/Bars/TopBarView.swift
Normal file
169
CasinoKit/Sources/CasinoKit/Views/Bars/TopBarView.swift
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
//
|
||||||
|
// TopBarView.swift
|
||||||
|
// CasinoKit
|
||||||
|
//
|
||||||
|
// A reusable top bar for casino games showing balance and toolbar buttons.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A top bar showing balance and customizable toolbar buttons.
|
||||||
|
public struct TopBarView: View {
|
||||||
|
/// The current balance to display.
|
||||||
|
public let balance: Int
|
||||||
|
|
||||||
|
/// Optional secondary info (e.g., cards remaining).
|
||||||
|
public let secondaryInfo: String?
|
||||||
|
|
||||||
|
/// Icon for secondary info.
|
||||||
|
public let secondaryIcon: String?
|
||||||
|
|
||||||
|
/// Whether to show the reset button.
|
||||||
|
public let showReset: Bool
|
||||||
|
|
||||||
|
/// Action when reset is tapped.
|
||||||
|
public let onReset: (() -> Void)?
|
||||||
|
|
||||||
|
/// Action when settings is tapped.
|
||||||
|
public let onSettings: (() -> Void)?
|
||||||
|
|
||||||
|
/// Action when help/rules is tapped.
|
||||||
|
public let onHelp: (() -> Void)?
|
||||||
|
|
||||||
|
/// Action when stats is tapped.
|
||||||
|
public let onStats: (() -> Void)?
|
||||||
|
|
||||||
|
// MARK: - Font Sizes (fixed for top bar constraints)
|
||||||
|
|
||||||
|
private let balanceFontSize: CGFloat = 24
|
||||||
|
private let dollarFontSize: CGFloat = 14
|
||||||
|
private let secondaryFontSize: CGFloat = 14
|
||||||
|
private let iconSize: CGFloat = 20
|
||||||
|
|
||||||
|
/// Creates a top bar.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - balance: The current balance.
|
||||||
|
/// - secondaryInfo: Optional secondary info text.
|
||||||
|
/// - secondaryIcon: Optional SF Symbol for secondary info.
|
||||||
|
/// - showReset: Whether to show reset button.
|
||||||
|
/// - onReset: Reset button action.
|
||||||
|
/// - onSettings: Settings button action.
|
||||||
|
/// - onHelp: Help button action.
|
||||||
|
/// - onStats: Stats button action.
|
||||||
|
public init(
|
||||||
|
balance: Int,
|
||||||
|
secondaryInfo: String? = nil,
|
||||||
|
secondaryIcon: String? = nil,
|
||||||
|
showReset: Bool = true,
|
||||||
|
onReset: (() -> Void)? = nil,
|
||||||
|
onSettings: (() -> Void)? = nil,
|
||||||
|
onHelp: (() -> Void)? = nil,
|
||||||
|
onStats: (() -> Void)? = nil
|
||||||
|
) {
|
||||||
|
self.balance = balance
|
||||||
|
self.secondaryInfo = secondaryInfo
|
||||||
|
self.secondaryIcon = secondaryIcon
|
||||||
|
self.showReset = showReset
|
||||||
|
self.onReset = onReset
|
||||||
|
self.onSettings = onSettings
|
||||||
|
self.onHelp = onHelp
|
||||||
|
self.onStats = onStats
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
HStack {
|
||||||
|
// Balance display
|
||||||
|
HStack(spacing: CasinoDesign.Spacing.xxSmall) {
|
||||||
|
Text("$")
|
||||||
|
.font(.system(size: dollarFontSize, weight: .bold))
|
||||||
|
.foregroundStyle(Color.CasinoTopBar.balanceText)
|
||||||
|
|
||||||
|
Text(balance.formatted())
|
||||||
|
.font(.system(size: balanceFontSize, weight: .bold, design: .rounded))
|
||||||
|
.foregroundStyle(Color.CasinoTopBar.balanceText)
|
||||||
|
.contentTransition(.numericText())
|
||||||
|
}
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel(String(localized: "Balance", bundle: .module))
|
||||||
|
.accessibilityValue("$\(balance.formatted())")
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
// Secondary info (centered)
|
||||||
|
if let info = secondaryInfo {
|
||||||
|
HStack(spacing: CasinoDesign.Spacing.xSmall) {
|
||||||
|
if let icon = secondaryIcon {
|
||||||
|
Image(systemName: icon)
|
||||||
|
}
|
||||||
|
Text(info)
|
||||||
|
}
|
||||||
|
.font(.system(size: secondaryFontSize))
|
||||||
|
.foregroundStyle(Color.CasinoTopBar.secondaryText)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
// Toolbar buttons
|
||||||
|
HStack(spacing: CasinoDesign.Spacing.medium) {
|
||||||
|
if let onStats = onStats {
|
||||||
|
ToolbarButton(icon: "chart.bar.fill", action: onStats)
|
||||||
|
.accessibilityLabel(String(localized: "Statistics", bundle: .module))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let onHelp = onHelp {
|
||||||
|
ToolbarButton(icon: "info.circle", action: onHelp)
|
||||||
|
.accessibilityLabel(String(localized: "Rules", bundle: .module))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let onSettings = onSettings {
|
||||||
|
ToolbarButton(icon: "gearshape.fill", action: onSettings)
|
||||||
|
.accessibilityLabel(String(localized: "Settings", bundle: .module))
|
||||||
|
}
|
||||||
|
|
||||||
|
if showReset, let onReset = onReset {
|
||||||
|
ToolbarButton(icon: "arrow.counterclockwise", action: onReset)
|
||||||
|
.accessibilityLabel(String(localized: "Reset Game", bundle: .module))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, CasinoDesign.Spacing.large)
|
||||||
|
.padding(.vertical, CasinoDesign.Spacing.small)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single toolbar button.
|
||||||
|
private struct ToolbarButton: View {
|
||||||
|
let icon: String
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
private let iconSize: CGFloat = 20
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: action) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.system(size: iconSize))
|
||||||
|
.foregroundStyle(Color.CasinoTopBar.iconButton)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ZStack {
|
||||||
|
Color.CasinoTable.felt.ignoresSafeArea()
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
TopBarView(
|
||||||
|
balance: 10_500,
|
||||||
|
secondaryInfo: "411",
|
||||||
|
secondaryIcon: "rectangle.portrait.on.rectangle.portrait.fill",
|
||||||
|
onReset: {},
|
||||||
|
onSettings: {},
|
||||||
|
onHelp: {},
|
||||||
|
onStats: {}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
93
CasinoKit/Sources/CasinoKit/Views/Effects/ConfettiView.swift
Normal file
93
CasinoKit/Sources/CasinoKit/Views/Effects/ConfettiView.swift
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
//
|
||||||
|
// ConfettiView.swift
|
||||||
|
// CasinoKit
|
||||||
|
//
|
||||||
|
// A reusable confetti celebration effect for wins.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A single confetti particle that falls and rotates.
|
||||||
|
public struct ConfettiPiece: View {
|
||||||
|
let color: Color
|
||||||
|
let containerSize: CGSize
|
||||||
|
|
||||||
|
@State private var position: CGPoint = .zero
|
||||||
|
@State private var rotation: Double = 0
|
||||||
|
@State private var opacity: Double = 1
|
||||||
|
|
||||||
|
private let confettiWidth: CGFloat = 8
|
||||||
|
private let confettiHeight: CGFloat = 12
|
||||||
|
|
||||||
|
public init(color: Color, containerSize: CGSize) {
|
||||||
|
self.color = color
|
||||||
|
self.containerSize = containerSize
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Rectangle()
|
||||||
|
.fill(color)
|
||||||
|
.frame(width: confettiWidth, height: confettiHeight)
|
||||||
|
.rotationEffect(.degrees(rotation))
|
||||||
|
.position(position)
|
||||||
|
.opacity(opacity)
|
||||||
|
.onAppear {
|
||||||
|
let startX = Double.random(in: 0...containerSize.width)
|
||||||
|
position = CGPoint(x: startX, y: -20)
|
||||||
|
|
||||||
|
withAnimation(.easeIn(duration: Double.random(in: 2...4))) {
|
||||||
|
position = CGPoint(
|
||||||
|
x: startX + Double.random(in: -100...100),
|
||||||
|
y: containerSize.height + 50
|
||||||
|
)
|
||||||
|
rotation = Double.random(in: 360...1080)
|
||||||
|
opacity = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A confetti celebration overlay for wins.
|
||||||
|
public struct ConfettiView: View {
|
||||||
|
/// The colors to use for confetti pieces.
|
||||||
|
public let colors: [Color]
|
||||||
|
|
||||||
|
/// The number of confetti pieces to show.
|
||||||
|
public let count: Int
|
||||||
|
|
||||||
|
/// Creates a confetti view with custom colors.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - colors: The colors to randomly assign to confetti pieces.
|
||||||
|
/// - count: The number of confetti pieces (default: 50).
|
||||||
|
public init(
|
||||||
|
colors: [Color] = [.red, .blue, .green, .yellow, .orange, .purple, .pink],
|
||||||
|
count: Int = 50
|
||||||
|
) {
|
||||||
|
self.colors = colors
|
||||||
|
self.count = count
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
ZStack {
|
||||||
|
ForEach(0..<count, id: \.self) { _ in
|
||||||
|
ConfettiPiece(
|
||||||
|
color: colors.randomElement() ?? .yellow,
|
||||||
|
containerSize: geometry.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ZStack {
|
||||||
|
Color.black.ignoresSafeArea()
|
||||||
|
ConfettiView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
201
CasinoKit/Sources/CasinoKit/Views/Overlays/GameOverView.swift
Normal file
201
CasinoKit/Sources/CasinoKit/Views/Overlays/GameOverView.swift
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
//
|
||||||
|
// GameOverView.swift
|
||||||
|
// CasinoKit
|
||||||
|
//
|
||||||
|
// A reusable game over overlay for when the player runs out of chips.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A full-screen game over overlay with play again button.
|
||||||
|
public struct GameOverView: View {
|
||||||
|
/// The number of rounds played in this session.
|
||||||
|
public let roundsPlayed: Int
|
||||||
|
|
||||||
|
/// Additional stats to display (label: value pairs).
|
||||||
|
public let additionalStats: [(String, String)]
|
||||||
|
|
||||||
|
/// Action when the player taps "Play Again".
|
||||||
|
public let onPlayAgain: () -> Void
|
||||||
|
|
||||||
|
@State private var showContent = false
|
||||||
|
|
||||||
|
// MARK: - Scaled Font Sizes (Dynamic Type)
|
||||||
|
|
||||||
|
@ScaledMetric(relativeTo: .largeTitle) private var iconSize: CGFloat = 64
|
||||||
|
@ScaledMetric(relativeTo: .largeTitle) private var titleFontSize: CGFloat = 36
|
||||||
|
@ScaledMetric(relativeTo: .body) private var messageFontSize: CGFloat = 18
|
||||||
|
@ScaledMetric(relativeTo: .body) private var statsFontSize: CGFloat = 17
|
||||||
|
@ScaledMetric(relativeTo: .headline) private var buttonFontSize: CGFloat = 18
|
||||||
|
|
||||||
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
|
|
||||||
|
/// Maximum width for the modal card on iPad
|
||||||
|
private var maxModalWidth: CGFloat {
|
||||||
|
horizontalSizeClass == .regular ? CasinoDesign.Size.maxModalWidth : .infinity
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a game over view.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - roundsPlayed: Number of rounds played.
|
||||||
|
/// - additionalStats: Extra stats to show (default: empty).
|
||||||
|
/// - onPlayAgain: Action when tapping "Play Again".
|
||||||
|
public init(
|
||||||
|
roundsPlayed: Int,
|
||||||
|
additionalStats: [(String, String)] = [],
|
||||||
|
onPlayAgain: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
self.roundsPlayed = roundsPlayed
|
||||||
|
self.additionalStats = additionalStats
|
||||||
|
self.onPlayAgain = onPlayAgain
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
ZStack {
|
||||||
|
// Solid dark backdrop
|
||||||
|
Color.black
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
// Modal card
|
||||||
|
VStack(spacing: CasinoDesign.Spacing.xxLarge) {
|
||||||
|
// Broke icon
|
||||||
|
Image(systemName: "creditcard.trianglebadge.exclamationmark")
|
||||||
|
.font(.system(size: iconSize))
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
.symbolEffect(.pulse, options: .repeating)
|
||||||
|
|
||||||
|
// Title
|
||||||
|
Text(String(localized: "GAME OVER", bundle: .module))
|
||||||
|
.font(.system(size: titleFontSize, weight: .black, design: .rounded))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
|
// Message
|
||||||
|
Text(String(localized: "You've run out of chips!", bundle: .module))
|
||||||
|
.font(.system(size: messageFontSize, weight: .medium))
|
||||||
|
.foregroundStyle(.white.opacity(CasinoDesign.Opacity.strong))
|
||||||
|
|
||||||
|
// Stats card
|
||||||
|
VStack(spacing: CasinoDesign.Spacing.medium) {
|
||||||
|
StatRow(
|
||||||
|
label: String(localized: "Rounds Played", bundle: .module),
|
||||||
|
value: "\(roundsPlayed)",
|
||||||
|
fontSize: statsFontSize
|
||||||
|
)
|
||||||
|
|
||||||
|
ForEach(additionalStats.indices, id: \.self) { index in
|
||||||
|
StatRow(
|
||||||
|
label: additionalStats[index].0,
|
||||||
|
value: additionalStats[index].1,
|
||||||
|
fontSize: statsFontSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.large)
|
||||||
|
.fill(Color.white.opacity(CasinoDesign.Opacity.subtle))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.large)
|
||||||
|
.strokeBorder(Color.white.opacity(CasinoDesign.Opacity.subtle), lineWidth: CasinoDesign.LineWidth.thin)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(.horizontal, CasinoDesign.Spacing.xLarge)
|
||||||
|
|
||||||
|
// Play Again button
|
||||||
|
Button {
|
||||||
|
onPlayAgain()
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: CasinoDesign.Spacing.small) {
|
||||||
|
Image(systemName: "arrow.counterclockwise")
|
||||||
|
Text(String(localized: "Play Again", bundle: .module))
|
||||||
|
}
|
||||||
|
.font(.system(size: buttonFontSize, weight: .bold))
|
||||||
|
.foregroundStyle(.black)
|
||||||
|
.padding(.horizontal, CasinoDesign.Spacing.xxxLarge)
|
||||||
|
.padding(.vertical, CasinoDesign.Spacing.medium + CasinoDesign.Spacing.xxSmall)
|
||||||
|
.background(
|
||||||
|
Capsule()
|
||||||
|
.fill(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.shadow(color: .yellow.opacity(CasinoDesign.Opacity.light), radius: CasinoDesign.Shadow.radiusXLarge)
|
||||||
|
}
|
||||||
|
.padding(.top, CasinoDesign.Spacing.medium)
|
||||||
|
}
|
||||||
|
.padding(CasinoDesign.Spacing.xxxLarge)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.xxxLarge)
|
||||||
|
.fill(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [Color.CasinoModal.backgroundLight, Color.CasinoModal.backgroundDark],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.xxxLarge)
|
||||||
|
.strokeBorder(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color.red.opacity(CasinoDesign.Opacity.medium),
|
||||||
|
Color.red.opacity(CasinoDesign.Opacity.hint)
|
||||||
|
],
|
||||||
|
startPoint: .topLeading,
|
||||||
|
endPoint: .bottomTrailing
|
||||||
|
),
|
||||||
|
lineWidth: CasinoDesign.LineWidth.medium
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.shadow(color: .red.opacity(CasinoDesign.Opacity.hint), radius: CasinoDesign.Shadow.radiusXXLarge)
|
||||||
|
.frame(maxWidth: maxModalWidth)
|
||||||
|
.padding(.horizontal, CasinoDesign.Spacing.xxLarge)
|
||||||
|
.scaleEffect(showContent ? 1.0 : 0.8)
|
||||||
|
.opacity(showContent ? 1.0 : 0)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
withAnimation(.spring(duration: CasinoDesign.Animation.springDuration, bounce: CasinoDesign.Animation.springBounce)) {
|
||||||
|
showContent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.accessibilityElement(children: .contain)
|
||||||
|
.accessibilityLabel(String(localized: "Game Over", bundle: .module))
|
||||||
|
.accessibilityAddTraits(.isModal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single stat row for the game over view.
|
||||||
|
private struct StatRow: View {
|
||||||
|
let label: String
|
||||||
|
let value: String
|
||||||
|
let fontSize: CGFloat
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Text(label)
|
||||||
|
.foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium))
|
||||||
|
Spacer()
|
||||||
|
Text(value)
|
||||||
|
.bold()
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
}
|
||||||
|
.font(.system(size: fontSize))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
GameOverView(
|
||||||
|
roundsPlayed: 25,
|
||||||
|
additionalStats: [
|
||||||
|
("Biggest Win", "$5,000"),
|
||||||
|
("Biggest Loss", "-$2,500")
|
||||||
|
],
|
||||||
|
onPlayAgain: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,223 @@
|
|||||||
|
//
|
||||||
|
// SettingsComponents.swift
|
||||||
|
// CasinoKit
|
||||||
|
//
|
||||||
|
// Reusable settings UI components for casino games.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// MARK: - Settings Toggle
|
||||||
|
|
||||||
|
/// A toggle setting row with title and subtitle.
|
||||||
|
public struct SettingsToggle: View {
|
||||||
|
/// The main title text.
|
||||||
|
public let title: String
|
||||||
|
|
||||||
|
/// The subtitle/description text.
|
||||||
|
public let subtitle: String
|
||||||
|
|
||||||
|
/// Binding to the toggle state.
|
||||||
|
@Binding public var isOn: Bool
|
||||||
|
|
||||||
|
/// Creates a settings toggle.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - title: The main title.
|
||||||
|
/// - subtitle: The subtitle description.
|
||||||
|
/// - isOn: Binding to toggle state.
|
||||||
|
public init(title: String, subtitle: String, isOn: Binding<Bool>) {
|
||||||
|
self.title = title
|
||||||
|
self.subtitle = subtitle
|
||||||
|
self._isOn = isOn
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Toggle(isOn: $isOn) {
|
||||||
|
VStack(alignment: .leading, spacing: CasinoDesign.Spacing.xxSmall) {
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: CasinoDesign.BaseFontSize.medium, weight: .medium))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.system(size: CasinoDesign.BaseFontSize.body))
|
||||||
|
.foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tint(.yellow)
|
||||||
|
.padding(.vertical, CasinoDesign.Spacing.xSmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Speed Picker
|
||||||
|
|
||||||
|
/// A segmented picker for animation speed (Fast/Normal/Slow).
|
||||||
|
public struct SpeedPicker: View {
|
||||||
|
/// Binding to the speed value (0.5 = fast, 1.0 = normal, 2.0 = slow).
|
||||||
|
@Binding public var speed: Double
|
||||||
|
|
||||||
|
private let options: [(String, Double)] = [
|
||||||
|
("Fast", 0.5),
|
||||||
|
("Normal", 1.0),
|
||||||
|
("Slow", 2.0)
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Creates a speed picker.
|
||||||
|
/// - Parameter speed: Binding to the speed multiplier.
|
||||||
|
public init(speed: Binding<Double>) {
|
||||||
|
self._speed = speed
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: CasinoDesign.Spacing.small) {
|
||||||
|
Text(String(localized: "Dealing Speed", bundle: .module))
|
||||||
|
.font(.system(size: CasinoDesign.BaseFontSize.medium, weight: .medium))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
|
HStack(spacing: CasinoDesign.Spacing.small) {
|
||||||
|
ForEach(options, id: \.1) { option in
|
||||||
|
Button {
|
||||||
|
speed = option.1
|
||||||
|
} label: {
|
||||||
|
Text(option.0)
|
||||||
|
.font(.system(size: CasinoDesign.BaseFontSize.body, weight: .medium))
|
||||||
|
.foregroundStyle(speed == option.1 ? .black : .white.opacity(CasinoDesign.Opacity.strong))
|
||||||
|
.padding(.vertical, CasinoDesign.Spacing.small)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background(
|
||||||
|
Capsule()
|
||||||
|
.fill(speed == option.1 ? Color.yellow : Color.white.opacity(CasinoDesign.Opacity.subtle))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, CasinoDesign.Spacing.xSmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Volume Picker
|
||||||
|
|
||||||
|
/// A volume slider with speaker icons.
|
||||||
|
public struct VolumePicker: View {
|
||||||
|
/// Binding to the volume level (0.0 to 1.0).
|
||||||
|
@Binding public var volume: Float
|
||||||
|
|
||||||
|
/// Creates a volume picker.
|
||||||
|
/// - Parameter volume: Binding to volume (0.0-1.0).
|
||||||
|
public init(volume: Binding<Float>) {
|
||||||
|
self._volume = volume
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: CasinoDesign.Spacing.small) {
|
||||||
|
HStack {
|
||||||
|
Text(String(localized: "Volume", bundle: .module))
|
||||||
|
.font(.system(size: CasinoDesign.BaseFontSize.medium, weight: .medium))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("\(Int(volume * 100))%")
|
||||||
|
.font(.system(size: CasinoDesign.BaseFontSize.body, weight: .medium, design: .rounded))
|
||||||
|
.foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium))
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: CasinoDesign.Spacing.medium) {
|
||||||
|
Image(systemName: "speaker.fill")
|
||||||
|
.font(.system(size: CasinoDesign.BaseFontSize.body))
|
||||||
|
.foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium))
|
||||||
|
|
||||||
|
Slider(value: $volume, in: 0...1, step: 0.1)
|
||||||
|
.tint(.yellow)
|
||||||
|
|
||||||
|
Image(systemName: "speaker.wave.3.fill")
|
||||||
|
.font(.system(size: CasinoDesign.BaseFontSize.body))
|
||||||
|
.foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, CasinoDesign.Spacing.xSmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Balance Picker
|
||||||
|
|
||||||
|
/// A grid picker for selecting a starting balance.
|
||||||
|
public struct BalancePicker: View {
|
||||||
|
/// Binding to the selected balance.
|
||||||
|
@Binding public var balance: Int
|
||||||
|
|
||||||
|
/// The available balance options.
|
||||||
|
public let options: [Int]
|
||||||
|
|
||||||
|
/// Creates a balance picker.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - balance: Binding to selected balance.
|
||||||
|
/// - options: Available balance options (default: standard values).
|
||||||
|
public init(
|
||||||
|
balance: Binding<Int>,
|
||||||
|
options: [Int] = [1_000, 5_000, 10_000, 25_000, 50_000, 100_000]
|
||||||
|
) {
|
||||||
|
self._balance = balance
|
||||||
|
self.options = options
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
LazyVGrid(columns: [
|
||||||
|
GridItem(.flexible()),
|
||||||
|
GridItem(.flexible()),
|
||||||
|
GridItem(.flexible())
|
||||||
|
], spacing: CasinoDesign.Spacing.small) {
|
||||||
|
ForEach(options, id: \.self) { amount in
|
||||||
|
Button {
|
||||||
|
balance = amount
|
||||||
|
} label: {
|
||||||
|
Text(formattedAmount(amount))
|
||||||
|
.font(.system(size: CasinoDesign.BaseFontSize.body, weight: .bold))
|
||||||
|
.foregroundStyle(balance == amount ? .black : .white)
|
||||||
|
.padding(.vertical, CasinoDesign.Spacing.medium)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.small)
|
||||||
|
.fill(balance == amount ? Color.yellow : Color.white.opacity(CasinoDesign.Opacity.subtle))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formattedAmount(_ amount: Int) -> String {
|
||||||
|
if amount >= 1000 {
|
||||||
|
return "$\(amount / 1000)K"
|
||||||
|
}
|
||||||
|
return "$\(amount)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ScrollView {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
SettingsToggle(
|
||||||
|
title: "Sound Effects",
|
||||||
|
subtitle: "Play sounds for game events",
|
||||||
|
isOn: .constant(true)
|
||||||
|
)
|
||||||
|
|
||||||
|
Divider().background(Color.white.opacity(0.1))
|
||||||
|
|
||||||
|
SpeedPicker(speed: .constant(1.0))
|
||||||
|
|
||||||
|
Divider().background(Color.white.opacity(0.1))
|
||||||
|
|
||||||
|
VolumePicker(volume: .constant(0.8))
|
||||||
|
|
||||||
|
Divider().background(Color.white.opacity(0.1))
|
||||||
|
|
||||||
|
BalancePicker(balance: .constant(10_000))
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.background(Color.Sheet.background)
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
//
|
||||||
|
// TableBackgroundView.swift
|
||||||
|
// CasinoKit
|
||||||
|
//
|
||||||
|
// A reusable casino table background with felt pattern.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A casino table felt background with radial gradient.
|
||||||
|
public struct TableBackgroundView: View {
|
||||||
|
/// The primary felt color (center of gradient).
|
||||||
|
public let feltColor: Color
|
||||||
|
|
||||||
|
/// The darker edge color for the gradient.
|
||||||
|
public let edgeColor: Color
|
||||||
|
|
||||||
|
/// Whether to show the decorative felt pattern.
|
||||||
|
public let showPattern: Bool
|
||||||
|
|
||||||
|
/// Creates a table background.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - feltColor: The main felt color (default: casino green).
|
||||||
|
/// - edgeColor: The darker edge color (default: dark green).
|
||||||
|
/// - showPattern: Whether to show the decorative pattern (default: true).
|
||||||
|
public init(
|
||||||
|
feltColor: Color = Color.CasinoTable.felt,
|
||||||
|
edgeColor: Color = Color.CasinoTable.feltDark,
|
||||||
|
showPattern: Bool = true
|
||||||
|
) {
|
||||||
|
self.feltColor = feltColor
|
||||||
|
self.edgeColor = edgeColor
|
||||||
|
self.showPattern = showPattern
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
ZStack {
|
||||||
|
// Base gradient
|
||||||
|
RadialGradient(
|
||||||
|
colors: [feltColor, edgeColor],
|
||||||
|
center: .center,
|
||||||
|
startRadius: 50,
|
||||||
|
endRadius: 600
|
||||||
|
)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
// Optional pattern overlay
|
||||||
|
if showPattern {
|
||||||
|
FeltPatternView()
|
||||||
|
.opacity(CasinoDesign.Opacity.verySubtle)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A subtle decorative pattern for the felt.
|
||||||
|
public struct FeltPatternView: View {
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
Canvas { context, size in
|
||||||
|
let spacing = CasinoDesign.Size.patternSpacing
|
||||||
|
let diamondSize = CasinoDesign.Size.patternDiamondSize
|
||||||
|
|
||||||
|
for x in stride(from: 0, to: size.width, by: spacing) {
|
||||||
|
for y in stride(from: 0, to: size.height, by: spacing) {
|
||||||
|
let offsetX = Int(y / spacing).isMultiple(of: 2) ? spacing / 2 : 0
|
||||||
|
let rect = CGRect(
|
||||||
|
x: x + offsetX - diamondSize / 2,
|
||||||
|
y: y - diamondSize / 2,
|
||||||
|
width: diamondSize,
|
||||||
|
height: diamondSize
|
||||||
|
)
|
||||||
|
|
||||||
|
let path = Path { p in
|
||||||
|
p.move(to: CGPoint(x: rect.midX, y: rect.minY))
|
||||||
|
p.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
|
||||||
|
p.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
|
||||||
|
p.addLine(to: CGPoint(x: rect.minX, y: rect.midY))
|
||||||
|
p.closeSubpath()
|
||||||
|
}
|
||||||
|
|
||||||
|
context.fill(path, with: .color(.white))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
TableBackgroundView()
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user