Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
9081337dc1
commit
28eb33439e
@ -159,11 +159,9 @@ final class GameState {
|
|||||||
|
|
||||||
/// Handles data received from iCloud (e.g., after fresh install or from another device).
|
/// Handles data received from iCloud (e.g., after fresh install or from another device).
|
||||||
private func handleCloudDataReceived(_ cloudData: BaccaratGameData) {
|
private func handleCloudDataReceived(_ cloudData: BaccaratGameData) {
|
||||||
print("GameState: Received cloud data with \(cloudData.roundsPlayed) rounds")
|
|
||||||
|
|
||||||
// Only update if cloud has more progress than current state
|
// Only update if cloud has more progress than current state
|
||||||
guard cloudData.roundsPlayed > roundHistory.count else {
|
guard cloudData.roundsPlayed > roundHistory.count else {
|
||||||
print("GameState: Local data is newer, ignoring cloud data")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,8 +179,6 @@ final class GameState {
|
|||||||
bankerPair: saved.bankerPair
|
bankerPair: saved.bankerPair
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
print("GameState: Restored from cloud - \(cloudData.roundsPlayed) rounds, balance: \(cloudData.balance)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Persistence
|
// MARK: - Persistence
|
||||||
@ -208,8 +204,6 @@ final class GameState {
|
|||||||
bankerPair: saved.bankerPair
|
bankerPair: saved.bankerPair
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
print("GameState: Restored \(savedData.roundsPlayed) rounds, balance: \(savedData.balance)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Saves current game state to iCloud/local storage.
|
/// Saves current game state to iCloud/local storage.
|
||||||
|
|||||||
@ -295,8 +295,6 @@ final class GameSettings {
|
|||||||
if let volume = iCloudStore.object(forKey: Keys.soundVolume) as? Double {
|
if let volume = iCloudStore.object(forKey: Keys.soundVolume) as? Double {
|
||||||
self.soundVolume = Float(volume)
|
self.soundVolume = Float(volume)
|
||||||
}
|
}
|
||||||
|
|
||||||
print("GameSettings: Loaded from iCloud")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Saves settings to UserDefaults and iCloud.
|
/// Saves settings to UserDefaults and iCloud.
|
||||||
@ -327,7 +325,6 @@ final class GameSettings {
|
|||||||
iCloudStore.set(hapticsEnabled, forKey: Keys.hapticsEnabled)
|
iCloudStore.set(hapticsEnabled, forKey: Keys.hapticsEnabled)
|
||||||
iCloudStore.set(Double(soundVolume), forKey: Keys.soundVolume)
|
iCloudStore.set(Double(soundVolume), forKey: Keys.soundVolume)
|
||||||
iCloudStore.synchronize()
|
iCloudStore.synchronize()
|
||||||
print("GameSettings: Saved to iCloud")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1532,6 +1532,7 @@
|
|||||||
},
|
},
|
||||||
"Game Over" : {
|
"Game Over" : {
|
||||||
"comment" : "The title of the game over screen.",
|
"comment" : "The title of the game over screen.",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -1555,6 +1556,7 @@
|
|||||||
},
|
},
|
||||||
"GAME OVER" : {
|
"GAME OVER" : {
|
||||||
"comment" : "The title of the game over screen.",
|
"comment" : "The title of the game over screen.",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -2900,6 +2902,7 @@
|
|||||||
},
|
},
|
||||||
"Rounds Played" : {
|
"Rounds Played" : {
|
||||||
"comment" : "A label displayed next to the number of rounds played in the game over screen.",
|
"comment" : "A label displayed next to the number of rounds played in the game over screen.",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|||||||
@ -1,189 +0,0 @@
|
|||||||
//
|
|
||||||
// GameOverView.swift
|
|
||||||
// Baccarat
|
|
||||||
//
|
|
||||||
// Game over screen shown when player runs out of money.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import CasinoKit
|
|
||||||
|
|
||||||
/// Game over screen shown when player runs out of money.
|
|
||||||
struct GameOverView: View {
|
|
||||||
let roundsPlayed: Int
|
|
||||||
let onPlayAgain: () -> Void
|
|
||||||
|
|
||||||
@State private var showContent = false
|
|
||||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
||||||
|
|
||||||
/// Maximum width for the modal card on iPad
|
|
||||||
private var maxModalWidth: CGFloat {
|
|
||||||
horizontalSizeClass == .regular ? CasinoDesign.Size.maxModalWidth : .infinity
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Scaled Font Sizes (Dynamic Type)
|
|
||||||
|
|
||||||
@ScaledMetric(relativeTo: .largeTitle) private var iconSize: CGFloat = Design.BaseFontSize.display
|
|
||||||
@ScaledMetric(relativeTo: .largeTitle) private var titleFontSize: CGFloat = Design.BaseFontSize.largeTitle
|
|
||||||
@ScaledMetric(relativeTo: .body) private var messageFontSize: CGFloat = Design.BaseFontSize.xLarge
|
|
||||||
@ScaledMetric(relativeTo: .body) private var statsFontSize: CGFloat = 17
|
|
||||||
@ScaledMetric(relativeTo: .headline) private var buttonFontSize: CGFloat = Design.BaseFontSize.xLarge
|
|
||||||
|
|
||||||
// MARK: - Layout Constants
|
|
||||||
|
|
||||||
private let modalCornerRadius = Design.CornerRadius.xxxLarge
|
|
||||||
private let statsCornerRadius = Design.CornerRadius.large
|
|
||||||
private let cardPadding = Design.Spacing.xxxLarge
|
|
||||||
private let contentSpacing: CGFloat = 28
|
|
||||||
private let buttonHorizontalPadding: CGFloat = 48
|
|
||||||
private let buttonVerticalPadding: CGFloat = 18
|
|
||||||
|
|
||||||
// MARK: - Body
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
// Solid dark backdrop - fully opaque
|
|
||||||
Color.black
|
|
||||||
.ignoresSafeArea()
|
|
||||||
|
|
||||||
// Modal card
|
|
||||||
modalContent
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce)) {
|
|
||||||
showContent = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.accessibilityElement(children: .contain)
|
|
||||||
.accessibilityLabel(String(localized: "Game Over"))
|
|
||||||
.accessibilityAddTraits(.isModal)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Private Views
|
|
||||||
|
|
||||||
private var modalContent: some View {
|
|
||||||
VStack(spacing: contentSpacing) {
|
|
||||||
// Broke icon
|
|
||||||
Image(systemName: "creditcard.trianglebadge.exclamationmark")
|
|
||||||
.font(.system(size: iconSize))
|
|
||||||
.foregroundStyle(.red)
|
|
||||||
.symbolEffect(.pulse, options: .repeating)
|
|
||||||
|
|
||||||
// Title
|
|
||||||
Text("GAME OVER")
|
|
||||||
.font(.system(size: titleFontSize, weight: .black, design: .rounded))
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
|
|
||||||
// Message
|
|
||||||
Text("You've run out of chips!")
|
|
||||||
.font(.system(size: messageFontSize, weight: .medium))
|
|
||||||
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
|
||||||
|
|
||||||
// Stats card
|
|
||||||
statsCard
|
|
||||||
|
|
||||||
// Play Again button
|
|
||||||
playAgainButton
|
|
||||||
}
|
|
||||||
.padding(cardPadding)
|
|
||||||
.background(modalBackground)
|
|
||||||
.shadow(color: .red.opacity(Design.Opacity.hint), radius: Design.Shadow.radiusXXLarge)
|
|
||||||
.frame(maxWidth: maxModalWidth)
|
|
||||||
.padding(.horizontal, Design.Spacing.xxLarge)
|
|
||||||
.scaleEffect(showContent ? Design.Scale.normal : Design.Scale.slightShrink)
|
|
||||||
.opacity(showContent ? 1.0 : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var statsCard: some View {
|
|
||||||
VStack(spacing: Design.Spacing.medium) {
|
|
||||||
HStack {
|
|
||||||
Text("Rounds Played")
|
|
||||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
|
||||||
Spacer()
|
|
||||||
Text("\(roundsPlayed)")
|
|
||||||
.bold()
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.font(.system(size: statsFontSize))
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: statsCornerRadius)
|
|
||||||
.fill(Color.white.opacity(Design.Opacity.subtle))
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: statsCornerRadius)
|
|
||||||
.strokeBorder(Color.white.opacity(Design.Opacity.subtle), lineWidth: Design.LineWidth.thin)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.padding(.horizontal, Design.Spacing.xLarge)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var playAgainButton: some View {
|
|
||||||
Button {
|
|
||||||
onPlayAgain()
|
|
||||||
} label: {
|
|
||||||
HStack(spacing: Design.Spacing.small) {
|
|
||||||
Image(systemName: "arrow.counterclockwise")
|
|
||||||
Text("Play Again")
|
|
||||||
}
|
|
||||||
.font(.system(size: buttonFontSize, weight: .bold))
|
|
||||||
.foregroundStyle(.black)
|
|
||||||
.padding(.horizontal, buttonHorizontalPadding)
|
|
||||||
.padding(.vertical, buttonVerticalPadding)
|
|
||||||
.background(
|
|
||||||
Capsule()
|
|
||||||
.fill(
|
|
||||||
LinearGradient(
|
|
||||||
colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
|
|
||||||
startPoint: .top,
|
|
||||||
endPoint: .bottom
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusXLarge)
|
|
||||||
}
|
|
||||||
.padding(.top, Design.Spacing.medium)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var modalBackground: some View {
|
|
||||||
RoundedRectangle(cornerRadius: modalCornerRadius)
|
|
||||||
.fill(
|
|
||||||
LinearGradient(
|
|
||||||
colors: [Color.CasinoModal.backgroundLight, Color.CasinoModal.backgroundDark],
|
|
||||||
startPoint: .top,
|
|
||||||
endPoint: .bottom
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: modalCornerRadius)
|
|
||||||
.strokeBorder(
|
|
||||||
LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Color.red.opacity(Design.Opacity.medium),
|
|
||||||
Color.red.opacity(Design.Opacity.hint)
|
|
||||||
],
|
|
||||||
startPoint: .topLeading,
|
|
||||||
endPoint: .bottomTrailing
|
|
||||||
),
|
|
||||||
lineWidth: Design.LineWidth.medium
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Previews
|
|
||||||
|
|
||||||
#Preview("Game Over") {
|
|
||||||
GameOverView(
|
|
||||||
roundsPlayed: 42,
|
|
||||||
onPlayAgain: {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview("Few Rounds") {
|
|
||||||
GameOverView(
|
|
||||||
roundsPlayed: 3,
|
|
||||||
onPlayAgain: {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,168 +0,0 @@
|
|||||||
//
|
|
||||||
// BettingZone.swift
|
|
||||||
// CasinoKit
|
|
||||||
//
|
|
||||||
// A reusable betting zone for casino table layouts.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
/// A tappable betting zone with label and chip display.
|
|
||||||
public struct BettingZone: View {
|
|
||||||
/// The zone label (e.g., "PLAYER", "TIE", "INSURANCE").
|
|
||||||
public let label: String
|
|
||||||
|
|
||||||
/// Optional payout info (e.g., "1:1", "8:1").
|
|
||||||
public let payoutInfo: String?
|
|
||||||
|
|
||||||
/// Current bet amount (0 if no bet).
|
|
||||||
public let betAmount: Int
|
|
||||||
|
|
||||||
/// Whether the zone is enabled for betting.
|
|
||||||
public let isEnabled: Bool
|
|
||||||
|
|
||||||
/// Action when the zone is tapped.
|
|
||||||
public let onTap: () -> Void
|
|
||||||
|
|
||||||
/// Background color for the zone.
|
|
||||||
public let backgroundColor: Color
|
|
||||||
|
|
||||||
/// Text color.
|
|
||||||
public let textColor: Color
|
|
||||||
|
|
||||||
// Layout
|
|
||||||
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = 16
|
|
||||||
@ScaledMetric(relativeTo: .caption) private var payoutFontSize: CGFloat = 12
|
|
||||||
|
|
||||||
/// Creates a betting zone.
|
|
||||||
public init(
|
|
||||||
label: String,
|
|
||||||
payoutInfo: String? = nil,
|
|
||||||
betAmount: Int = 0,
|
|
||||||
isEnabled: Bool = true,
|
|
||||||
backgroundColor: Color = .blue.opacity(0.2),
|
|
||||||
textColor: Color = .white,
|
|
||||||
onTap: @escaping () -> Void
|
|
||||||
) {
|
|
||||||
self.label = label
|
|
||||||
self.payoutInfo = payoutInfo
|
|
||||||
self.betAmount = betAmount
|
|
||||||
self.isEnabled = isEnabled
|
|
||||||
self.backgroundColor = backgroundColor
|
|
||||||
self.textColor = textColor
|
|
||||||
self.onTap = onTap
|
|
||||||
}
|
|
||||||
|
|
||||||
public var body: some View {
|
|
||||||
Button(action: onTap) {
|
|
||||||
ZStack {
|
|
||||||
// Background
|
|
||||||
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.medium)
|
|
||||||
.fill(backgroundColor)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.medium)
|
|
||||||
.strokeBorder(
|
|
||||||
textColor.opacity(CasinoDesign.Opacity.light),
|
|
||||||
lineWidth: CasinoDesign.LineWidth.thin
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Content
|
|
||||||
VStack(spacing: CasinoDesign.Spacing.xxSmall) {
|
|
||||||
Text(label)
|
|
||||||
.font(.system(size: labelFontSize, weight: .bold))
|
|
||||||
.foregroundStyle(textColor)
|
|
||||||
|
|
||||||
if let payout = payoutInfo {
|
|
||||||
Text(payout)
|
|
||||||
.font(.system(size: payoutFontSize, weight: .medium))
|
|
||||||
.foregroundStyle(textColor.opacity(CasinoDesign.Opacity.medium))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chip badge for bet amount
|
|
||||||
if betAmount > 0 {
|
|
||||||
ChipBadge(amount: betAmount)
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
|
|
||||||
.padding(CasinoDesign.Spacing.xSmall)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.disabled(!isEnabled)
|
|
||||||
.accessibilityLabel(label)
|
|
||||||
.accessibilityValue(betAmount > 0 ? "$\(betAmount) bet" : "No bet")
|
|
||||||
.accessibilityHint(isEnabled ? "Double tap to place bet" : "Betting disabled")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A small chip badge showing bet amount.
|
|
||||||
public struct ChipBadge: View {
|
|
||||||
public let amount: Int
|
|
||||||
|
|
||||||
private let badgeSize: CGFloat = 28
|
|
||||||
private let fontSize: CGFloat = 10
|
|
||||||
|
|
||||||
public init(amount: Int) {
|
|
||||||
self.amount = amount
|
|
||||||
}
|
|
||||||
|
|
||||||
public var body: some View {
|
|
||||||
ZStack {
|
|
||||||
Circle()
|
|
||||||
.fill(Color.yellow)
|
|
||||||
.frame(width: badgeSize, height: badgeSize)
|
|
||||||
|
|
||||||
Circle()
|
|
||||||
.strokeBorder(Color.orange, lineWidth: 2)
|
|
||||||
.frame(width: badgeSize - 4, height: badgeSize - 4)
|
|
||||||
|
|
||||||
Text(formattedAmount)
|
|
||||||
.font(.system(size: fontSize, weight: .bold))
|
|
||||||
.foregroundStyle(.black)
|
|
||||||
.minimumScaleFactor(0.5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var formattedAmount: String {
|
|
||||||
if amount >= 1_000_000 {
|
|
||||||
return "\(amount / 1_000_000)M"
|
|
||||||
} else if amount >= 1_000 {
|
|
||||||
return "\(amount / 1_000)K"
|
|
||||||
}
|
|
||||||
return "\(amount)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ZStack {
|
|
||||||
Color.CasinoTable.felt.ignoresSafeArea()
|
|
||||||
|
|
||||||
HStack(spacing: 20) {
|
|
||||||
BettingZone(
|
|
||||||
label: "PLAYER",
|
|
||||||
payoutInfo: "1:1",
|
|
||||||
betAmount: 0,
|
|
||||||
backgroundColor: .blue.opacity(0.2)
|
|
||||||
) { }
|
|
||||||
.frame(width: 120, height: 80)
|
|
||||||
|
|
||||||
BettingZone(
|
|
||||||
label: "TIE",
|
|
||||||
payoutInfo: "8:1",
|
|
||||||
betAmount: 500,
|
|
||||||
backgroundColor: .green.opacity(0.2)
|
|
||||||
) { }
|
|
||||||
.frame(width: 80, height: 80)
|
|
||||||
|
|
||||||
BettingZone(
|
|
||||||
label: "BANKER",
|
|
||||||
payoutInfo: "0.95:1",
|
|
||||||
betAmount: 2500,
|
|
||||||
backgroundColor: .red.opacity(0.2)
|
|
||||||
) { }
|
|
||||||
.frame(width: 120, height: 80)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user