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)
|
||||
.accessibilityLabel(accessibilityDescription)
|
||||
.accessibilityAddTraits(.updatesFrequently)
|
||||
}
|
||||
|
||||
// MARK: - Accessibility
|
||||
@ -401,61 +400,7 @@ private struct PairBadge: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Confetti particle for celebrations.
|
||||
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)
|
||||
}
|
||||
}
|
||||
// Note: ConfettiView is now provided by CasinoKit
|
||||
|
||||
#Preview("Win") {
|
||||
ZStack {
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
// - Card, Suit, Rank
|
||||
// - Deck
|
||||
// - ChipDenomination
|
||||
// - TableLimits
|
||||
|
||||
// MARK: - Views
|
||||
// - CardView, CardFrontView, CardBackView, CardPlaceholderView
|
||||
@ -21,6 +22,27 @@
|
||||
// - ChipStackView, ChipOnTableView
|
||||
// - 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
|
||||
// - AppIconView, AppIconConfig
|
||||
// - LaunchScreenView, LaunchScreenConfig, StaticLaunchScreenView
|
||||
@ -31,7 +53,7 @@
|
||||
// - DefaultCasinoTheme
|
||||
// - ChipColorSet
|
||||
// - CasinoDesign (constants)
|
||||
// - Color.Sheet (sheet colors)
|
||||
// - Color.Sheet, Color.Button, Color.Modal, Color.Table, Color.TopBar (colors)
|
||||
|
||||
// MARK: - Audio
|
||||
// - 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",
|
||||
"strings" : {
|
||||
"%lld" : {
|
||||
"comment" : "A badge displaying a numeric value. The argument is the numeric value to display.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"%lld dollar bet" : {
|
||||
"localizations" : {
|
||||
"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" : {
|
||||
"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
|
||||
},
|
||||
"$" : {
|
||||
"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" : {
|
||||
|
||||
},
|
||||
@ -86,6 +102,28 @@
|
||||
"comment" : "A title for the preview section of the icon export view.",
|
||||
"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" : {
|
||||
"localizations" : {
|
||||
"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" : {
|
||||
"localizations" : {
|
||||
"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" : {
|
||||
"localizations" : {
|
||||
"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" : {
|
||||
"comment" : "A section header describing how to export app icons.",
|
||||
"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" : {
|
||||
"localizations" : {
|
||||
"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" : {
|
||||
"localizations" : {
|
||||
"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" : {
|
||||
"localizations" : {
|
||||
"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" : {
|
||||
"localizations" : {
|
||||
"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" : {
|
||||
"localizations" : {
|
||||
"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" : {
|
||||
"localizations" : {
|
||||
"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" : {
|
||||
"localizations" : {
|
||||
"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" : {
|
||||
"localizations" : {
|
||||
"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"
|
||||
|
||||
@ -32,6 +32,7 @@ public enum CasinoDesign {
|
||||
public static let large: CGFloat = 16
|
||||
public static let xLarge: CGFloat = 20
|
||||
public static let xxLarge: CGFloat = 24
|
||||
public static let xxxLarge: CGFloat = 32
|
||||
}
|
||||
|
||||
// MARK: - Line Width
|
||||
@ -49,6 +50,8 @@ public enum CasinoDesign {
|
||||
public static let radiusSmall: CGFloat = 4
|
||||
public static let radiusMedium: CGFloat = 8
|
||||
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 offsetMedium: CGFloat = 2
|
||||
@ -58,8 +61,10 @@ public enum CasinoDesign {
|
||||
// MARK: - Opacity
|
||||
|
||||
public enum Opacity {
|
||||
public static let subtle: CGFloat = 0.05
|
||||
public static let light: CGFloat = 0.2
|
||||
public static let verySubtle: CGFloat = 0.05
|
||||
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 medium: CGFloat = 0.5
|
||||
public static let accent: CGFloat = 0.6
|
||||
@ -100,6 +105,19 @@ public enum CasinoDesign {
|
||||
/// Chip edge stripe dimensions.
|
||||
public static let chipStripeWidth: CGFloat = 4
|
||||
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)
|
||||
@ -162,5 +180,45 @@ public extension Color {
|
||||
/// Cancel button color.
|
||||
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