Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
645602b2de
commit
9ac5320782
@ -2606,6 +2606,7 @@
|
|||||||
},
|
},
|
||||||
"Play Again" : {
|
"Play Again" : {
|
||||||
"comment" : "A button label that says \"Play Again\".",
|
"comment" : "A button label that says \"Play Again\".",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -2855,29 +2856,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Restore starting balance and reshuffle" : {
|
|
||||||
"comment" : "Description for the reset game button explaining what it does.",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Restore starting balance and reshuffle"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es-MX" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Restaurar saldo inicial y barajar"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr-CA" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Restaurer le solde initial et mélanger"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Reset to Defaults" : {
|
"Reset to Defaults" : {
|
||||||
"comment" : "A button label that resets game settings to their default values.",
|
"comment" : "A button label that resets game settings to their default values.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -2901,6 +2879,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Restore starting balance and reshuffle" : {
|
||||||
|
"comment" : "Description for the reset game button explaining what it does.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Restore starting balance and reshuffle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Restaurar saldo inicial y barajar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Restaurer le solde initial et mélanger"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Roulette" : {
|
"Roulette" : {
|
||||||
"comment" : "The name of a roulette game.",
|
"comment" : "The name of a roulette game.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -3542,6 +3543,7 @@
|
|||||||
},
|
},
|
||||||
"TOTAL" : {
|
"TOTAL" : {
|
||||||
"comment" : "A label displayed next to the total winnings in the result banner.",
|
"comment" : "A label displayed next to the total winnings in the result banner.",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -3840,6 +3842,7 @@
|
|||||||
},
|
},
|
||||||
"You've run out of chips!" : {
|
"You've run out of chips!" : {
|
||||||
"comment" : "A message displayed when a player runs out of money in the game over screen.",
|
"comment" : "A message displayed when a player runs out of money in the game over screen.",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|||||||
@ -3,12 +3,14 @@
|
|||||||
// Baccarat
|
// Baccarat
|
||||||
//
|
//
|
||||||
// Animated result banner showing the winner and itemized bet results.
|
// Animated result banner showing the winner and itemized bet results.
|
||||||
|
// Uses the shared ResultBannerView from CasinoKit.
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import CasinoKit
|
import CasinoKit
|
||||||
|
|
||||||
/// An animated banner showing the round result with bet breakdown.
|
/// An animated banner showing the round result with bet breakdown.
|
||||||
|
/// This is a wrapper around CasinoKit's shared ResultBannerView.
|
||||||
struct ResultBannerView: View {
|
struct ResultBannerView: View {
|
||||||
let result: GameResult
|
let result: GameResult
|
||||||
let totalWinnings: Int
|
let totalWinnings: Int
|
||||||
@ -20,30 +22,6 @@ struct ResultBannerView: View {
|
|||||||
let onNewRound: () -> Void
|
let onNewRound: () -> Void
|
||||||
let onGameOver: () -> Void
|
let onGameOver: () -> Void
|
||||||
|
|
||||||
/// Whether the player is out of money and can't continue.
|
|
||||||
private var isGameOver: Bool {
|
|
||||||
currentBalance < minBet
|
|
||||||
}
|
|
||||||
|
|
||||||
@State private var showBanner = false
|
|
||||||
@State private var showText = false
|
|
||||||
@State private var showBreakdown = false
|
|
||||||
@State private var showTotal = false
|
|
||||||
@State private var showButton = false
|
|
||||||
|
|
||||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
||||||
|
|
||||||
/// Maximum width for the banner card on iPad
|
|
||||||
private var maxBannerWidth: CGFloat {
|
|
||||||
horizontalSizeClass == .regular ? CasinoDesign.Size.maxModalWidth : .infinity
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Scaled Font Sizes (Dynamic Type)
|
|
||||||
|
|
||||||
@ScaledMetric(relativeTo: .largeTitle) private var resultFontSize: CGFloat = Design.BaseFontSize.largeTitle
|
|
||||||
@ScaledMetric(relativeTo: .title3) private var totalFontSize: CGFloat = Design.BaseFontSize.xxLarge + Design.Spacing.xSmall
|
|
||||||
@ScaledMetric(relativeTo: .body) private var itemFontSize: CGFloat = Design.BaseFontSize.medium
|
|
||||||
|
|
||||||
// MARK: - Computed Properties
|
// MARK: - Computed Properties
|
||||||
|
|
||||||
private var winningBets: [BetResult] {
|
private var winningBets: [BetResult] {
|
||||||
@ -59,28 +37,13 @@ struct ResultBannerView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
CasinoResultBannerView(
|
||||||
// Background overlay
|
resultText: result.displayText,
|
||||||
Color.black.opacity(showBanner ? Design.Opacity.medium : 0)
|
resultColor: result.color,
|
||||||
.ignoresSafeArea()
|
totalWinnings: totalWinnings,
|
||||||
.animation(.easeIn(duration: Design.Animation.fadeInDuration), value: showBanner)
|
currentBalance: currentBalance,
|
||||||
|
minBet: minBet,
|
||||||
// Banner
|
breakdownContent: {
|
||||||
VStack(spacing: Design.Spacing.medium) {
|
|
||||||
// Result text
|
|
||||||
Text(result.displayText)
|
|
||||||
.font(.system(size: resultFontSize, weight: .black, design: .rounded))
|
|
||||||
.foregroundStyle(
|
|
||||||
LinearGradient(
|
|
||||||
colors: [.white, result.color],
|
|
||||||
startPoint: .top,
|
|
||||||
endPoint: .bottom
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.shadow(color: result.color.opacity(Design.Opacity.heavy), radius: Design.Shadow.radiusLarge)
|
|
||||||
.scaleEffect(showText ? Design.Scale.normal : Design.Scale.shrunk)
|
|
||||||
.opacity(showText ? Design.Scale.normal : 0)
|
|
||||||
|
|
||||||
// Pair indicators
|
// Pair indicators
|
||||||
if playerHadPair || bankerHadPair {
|
if playerHadPair || bankerHadPair {
|
||||||
HStack(spacing: Design.Spacing.large) {
|
HStack(spacing: Design.Spacing.large) {
|
||||||
@ -91,8 +54,6 @@ struct ResultBannerView: View {
|
|||||||
PairBadge(label: "B PAIR", color: .red)
|
PairBadge(label: "B PAIR", color: .red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.scaleEffect(showBreakdown ? Design.Scale.normal : Design.Scale.shrunk)
|
|
||||||
.opacity(showBreakdown ? Design.Scale.normal : 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bet breakdown
|
// Bet breakdown
|
||||||
@ -100,204 +61,13 @@ struct ResultBannerView: View {
|
|||||||
BetBreakdownView(
|
BetBreakdownView(
|
||||||
winningBets: winningBets,
|
winningBets: winningBets,
|
||||||
losingBets: losingBets,
|
losingBets: losingBets,
|
||||||
pushBets: pushBets,
|
pushBets: pushBets
|
||||||
fontSize: itemFontSize
|
|
||||||
)
|
)
|
||||||
.scaleEffect(showBreakdown ? Design.Scale.normal : Design.Scale.shrunk)
|
|
||||||
.opacity(showBreakdown ? Design.Scale.normal : 0)
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
// Total
|
onNewRound: onNewRound,
|
||||||
if totalWinnings != 0 {
|
onPlayAgain: onGameOver
|
||||||
HStack(spacing: Design.Spacing.small) {
|
|
||||||
Text("TOTAL")
|
|
||||||
.font(.system(size: Design.BaseFontSize.body, weight: .bold))
|
|
||||||
.foregroundStyle(.white.opacity(Design.Opacity.accent))
|
|
||||||
|
|
||||||
if totalWinnings > 0 {
|
|
||||||
Text("+\(totalWinnings)")
|
|
||||||
.font(.system(size: totalFontSize, weight: .black, design: .rounded))
|
|
||||||
.foregroundStyle(.green)
|
|
||||||
} else {
|
|
||||||
Text("\(totalWinnings)")
|
|
||||||
.font(.system(size: totalFontSize, weight: .black, design: .rounded))
|
|
||||||
.foregroundStyle(.red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.scaleEffect(showTotal ? Design.Scale.normal : Design.Scale.shrunk)
|
|
||||||
.opacity(showTotal ? Design.Scale.normal : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Game Over message or New Round button
|
|
||||||
if isGameOver {
|
|
||||||
// Game Over - show message and restart button
|
|
||||||
VStack(spacing: Design.Spacing.medium) {
|
|
||||||
Text(String(localized: "You've run out of chips!"))
|
|
||||||
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
|
||||||
.foregroundStyle(.red.opacity(Design.Opacity.heavy))
|
|
||||||
|
|
||||||
Button {
|
|
||||||
onGameOver()
|
|
||||||
} label: {
|
|
||||||
HStack(spacing: Design.Spacing.small) {
|
|
||||||
Image(systemName: "arrow.counterclockwise")
|
|
||||||
Text(String(localized: "Play Again"))
|
|
||||||
}
|
|
||||||
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
|
|
||||||
.foregroundStyle(.black)
|
|
||||||
.padding(.horizontal, Design.Spacing.xxxLarge + Design.Spacing.small)
|
|
||||||
.padding(.vertical, Design.Spacing.medium + Design.Spacing.xxSmall)
|
|
||||||
.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.radiusMedium)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.scaleEffect(showButton ? Design.Scale.normal : Design.Scale.shrunk)
|
|
||||||
.opacity(showButton ? Design.Scale.normal : 0)
|
|
||||||
.padding(.top, Design.Spacing.small)
|
|
||||||
} else {
|
|
||||||
// Normal - New Round button
|
|
||||||
Button {
|
|
||||||
onNewRound()
|
|
||||||
} label: {
|
|
||||||
Text(String(localized: "New Round"))
|
|
||||||
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
|
|
||||||
.foregroundStyle(.black)
|
|
||||||
.padding(.horizontal, Design.Spacing.xxxLarge + Design.Spacing.small)
|
|
||||||
.padding(.vertical, Design.Spacing.medium + Design.Spacing.xxSmall)
|
|
||||||
.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.radiusMedium)
|
|
||||||
}
|
|
||||||
.scaleEffect(showButton ? Design.Scale.normal : Design.Scale.shrunk)
|
|
||||||
.opacity(showButton ? Design.Scale.normal : 0)
|
|
||||||
.padding(.top, Design.Spacing.small)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, Design.Spacing.xLarge)
|
|
||||||
.padding(.vertical, Design.Spacing.xxLarge)
|
|
||||||
.frame(maxWidth: maxBannerWidth)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge)
|
|
||||||
.fill(
|
|
||||||
LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Color(white: 0.15),
|
|
||||||
Color(white: 0.08)
|
|
||||||
],
|
|
||||||
startPoint: .top,
|
|
||||||
endPoint: .bottom
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge)
|
|
||||||
.strokeBorder(
|
|
||||||
LinearGradient(
|
|
||||||
colors: [
|
|
||||||
result.color.opacity(Design.Opacity.heavy),
|
|
||||||
result.color.opacity(Design.Opacity.light)
|
|
||||||
],
|
|
||||||
startPoint: .topLeading,
|
|
||||||
endPoint: .bottomTrailing
|
|
||||||
),
|
|
||||||
lineWidth: Design.LineWidth.thick
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.shadow(color: result.color.opacity(Design.Opacity.light), radius: Design.Shadow.radiusXXLarge)
|
|
||||||
.padding(.horizontal, Design.Spacing.large)
|
|
||||||
.scaleEffect(showBanner ? Design.Scale.normal : Design.Scale.slightShrink)
|
|
||||||
.opacity(showBanner ? Design.Scale.normal : 0)
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce)) {
|
|
||||||
showBanner = true
|
|
||||||
}
|
|
||||||
|
|
||||||
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce).delay(Design.Animation.staggerDelay1)) {
|
|
||||||
showText = true
|
|
||||||
}
|
|
||||||
|
|
||||||
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce).delay(Design.Animation.staggerDelay2)) {
|
|
||||||
showBreakdown = true
|
|
||||||
}
|
|
||||||
|
|
||||||
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce).delay(Design.Animation.staggerDelay3)) {
|
|
||||||
showTotal = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show button after everything else
|
|
||||||
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce).delay(Design.Animation.staggerDelay3 + Design.Animation.staggerDelay1)) {
|
|
||||||
showButton = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Play game over sound if out of chips (after a short delay so it doesn't overlap with lose sound)
|
|
||||||
if isGameOver {
|
|
||||||
Task {
|
|
||||||
try? await Task.sleep(for: .seconds(1))
|
|
||||||
SoundManager.shared.play(.gameOver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Announce result to VoiceOver users
|
|
||||||
announceResult()
|
|
||||||
}
|
|
||||||
.accessibilityElement(children: .ignore)
|
|
||||||
.accessibilityLabel(accessibilityDescription)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Accessibility
|
|
||||||
|
|
||||||
private var accessibilityDescription: String {
|
|
||||||
var description = result.displayText
|
|
||||||
|
|
||||||
// Add pair information
|
|
||||||
if playerHadPair {
|
|
||||||
description += ". Player pair"
|
|
||||||
}
|
|
||||||
if bankerHadPair {
|
|
||||||
description += ". Banker pair"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add bet results
|
|
||||||
for bet in winningBets {
|
|
||||||
description += ". \(bet.displayName) won \(bet.payout)"
|
|
||||||
}
|
|
||||||
for bet in losingBets {
|
|
||||||
description += ". \(bet.displayName) lost \(abs(bet.payout))"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add total
|
|
||||||
if totalWinnings > 0 {
|
|
||||||
description += ". Total winnings: \(totalWinnings)"
|
|
||||||
} else if totalWinnings < 0 {
|
|
||||||
description += ". Total loss: \(abs(totalWinnings))"
|
|
||||||
}
|
|
||||||
|
|
||||||
return description
|
|
||||||
}
|
|
||||||
|
|
||||||
private func announceResult() {
|
|
||||||
Task { @MainActor in
|
|
||||||
try? await Task.sleep(for: .milliseconds(500))
|
|
||||||
AccessibilityNotification.Announcement(accessibilityDescription).post()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,10 +77,11 @@ private struct BetBreakdownView: View {
|
|||||||
let winningBets: [BetResult]
|
let winningBets: [BetResult]
|
||||||
let losingBets: [BetResult]
|
let losingBets: [BetResult]
|
||||||
let pushBets: [BetResult]
|
let pushBets: [BetResult]
|
||||||
let fontSize: CGFloat
|
|
||||||
|
@ScaledMetric(relativeTo: .body) private var fontSize: CGFloat = Design.BaseFontSize.medium
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: Design.Spacing.xSmall) {
|
ResultBreakdownCard {
|
||||||
// Winning bets
|
// Winning bets
|
||||||
ForEach(winningBets) { bet in
|
ForEach(winningBets) { bet in
|
||||||
BetResultRow(bet: bet, fontSize: fontSize)
|
BetResultRow(bet: bet, fontSize: fontSize)
|
||||||
@ -326,12 +97,6 @@ private struct BetBreakdownView: View {
|
|||||||
BetResultRow(bet: bet, fontSize: fontSize)
|
BetResultRow(bet: bet, fontSize: fontSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, Design.Spacing.medium)
|
|
||||||
.padding(.vertical, Design.Spacing.small)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
|
||||||
.fill(Color.white.opacity(Design.Opacity.verySubtle))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,7 +165,7 @@ private struct PairBadge: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: ConfettiView is now provided by CasinoKit
|
// MARK: - Previews
|
||||||
|
|
||||||
#Preview("Win") {
|
#Preview("Win") {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|||||||
@ -2625,28 +2625,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Done" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Done"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es-MX" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Listo"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr-CA" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Terminé"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Don't Ask" : {
|
"Don't Ask" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -2669,6 +2647,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Done" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Done"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Listo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Terminé"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Double" : {
|
"Double" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -4480,6 +4480,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"New Round" : {
|
"New Round" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -4983,6 +4984,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Play Again" : {
|
"Play Again" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -5346,6 +5348,7 @@
|
|||||||
},
|
},
|
||||||
"Round result: %@" : {
|
"Round result: %@" : {
|
||||||
"comment" : "An accessibility label for the round result banner, describing the main hand result.",
|
"comment" : "An accessibility label for the round result banner, describing the main hand result.",
|
||||||
|
"extractionState" : "stale",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -7019,6 +7022,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"You've run out of chips!" : {
|
"You've run out of chips!" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|||||||
@ -3,11 +3,13 @@
|
|||||||
// Blackjack
|
// Blackjack
|
||||||
//
|
//
|
||||||
// Displays the result of a round with breakdown.
|
// Displays the result of a round with breakdown.
|
||||||
|
// Uses the shared ResultBannerView from CasinoKit.
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import CasinoKit
|
import CasinoKit
|
||||||
|
|
||||||
|
/// Result banner for Blackjack using the shared CasinoKit component.
|
||||||
struct ResultBannerView: View {
|
struct ResultBannerView: View {
|
||||||
let result: RoundResult
|
let result: RoundResult
|
||||||
let currentBalance: Int
|
let currentBalance: Int
|
||||||
@ -15,22 +17,9 @@ struct ResultBannerView: View {
|
|||||||
let onNewRound: () -> Void
|
let onNewRound: () -> Void
|
||||||
let onPlayAgain: () -> Void
|
let onPlayAgain: () -> Void
|
||||||
|
|
||||||
@State private var showContent = false
|
|
||||||
|
|
||||||
// MARK: - Scaled Metrics
|
|
||||||
|
|
||||||
@ScaledMetric(relativeTo: .largeTitle) private var titleFontSize: CGFloat = Design.BaseFontSize.largeTitle
|
|
||||||
@ScaledMetric(relativeTo: .title) private var resultFontSize: CGFloat = Design.BaseFontSize.title
|
|
||||||
@ScaledMetric(relativeTo: .headline) private var amountFontSize: CGFloat = Design.BaseFontSize.xLarge
|
|
||||||
@ScaledMetric(relativeTo: .body) private var buttonFontSize: CGFloat = Design.BaseFontSize.medium
|
|
||||||
|
|
||||||
// MARK: - Computed
|
// MARK: - Computed
|
||||||
|
|
||||||
private var isGameOver: Bool {
|
/// Overall result text based on total winnings
|
||||||
currentBalance < minBet
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Overall result based on total winnings (what the player actually cares about)
|
|
||||||
private var overallResultText: String {
|
private var overallResultText: String {
|
||||||
if result.totalWinnings > 0 {
|
if result.totalWinnings > 0 {
|
||||||
return String(localized: "WIN!")
|
return String(localized: "WIN!")
|
||||||
@ -48,60 +37,35 @@ struct ResultBannerView: View {
|
|||||||
return .blue
|
return .blue
|
||||||
}
|
}
|
||||||
|
|
||||||
private var winningsText: String {
|
|
||||||
if result.totalWinnings > 0 {
|
|
||||||
return "+$\(result.totalWinnings)"
|
|
||||||
} else if result.totalWinnings < 0 {
|
|
||||||
return "-$\(abs(result.totalWinnings))"
|
|
||||||
} else {
|
|
||||||
return "$0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var winningsColor: Color {
|
|
||||||
if result.totalWinnings > 0 { return .green }
|
|
||||||
if result.totalWinnings < 0 { return .red }
|
|
||||||
return .blue
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
CasinoResultBannerView(
|
||||||
// Full screen dark background
|
resultText: overallResultText,
|
||||||
Color.black.opacity(Design.Opacity.strong)
|
resultColor: overallResultColor,
|
||||||
|
totalWinnings: result.totalWinnings,
|
||||||
// Content card
|
currentBalance: currentBalance,
|
||||||
VStack(spacing: Design.Spacing.xLarge) {
|
minBet: minBet,
|
||||||
// Overall result based on total winnings
|
breakdownContent: {
|
||||||
Text(overallResultText)
|
ResultBreakdownCard {
|
||||||
.font(.system(size: titleFontSize, weight: .black, design: .rounded))
|
// Hand results
|
||||||
.foregroundStyle(overallResultColor)
|
|
||||||
|
|
||||||
// Winnings
|
|
||||||
Text(winningsText)
|
|
||||||
.font(.system(size: amountFontSize, weight: .bold, design: .rounded))
|
|
||||||
.foregroundStyle(winningsColor)
|
|
||||||
|
|
||||||
// Breakdown - all hands with amounts for splits
|
|
||||||
VStack(spacing: Design.Spacing.small) {
|
|
||||||
ForEach(result.handResults.indices, id: \.self) { index in
|
ForEach(result.handResults.indices, id: \.self) { index in
|
||||||
let handResult = result.handResults[index]
|
let handResult = result.handResults[index]
|
||||||
let handWinnings = index < result.handWinnings.count ? result.handWinnings[index] : nil
|
let handWinnings = index < result.handWinnings.count ? result.handWinnings[index] : nil
|
||||||
// Hand numbering: index 0 = Hand 1 (played first, displayed rightmost)
|
|
||||||
let handLabel = result.handResults.count > 1
|
let handLabel = result.handResults.count > 1
|
||||||
? String(localized: "Hand \(index + 1)")
|
? String(localized: "Hand \(index + 1)")
|
||||||
: String(localized: "Main Hand")
|
: String(localized: "Main Hand")
|
||||||
// Show amounts for split hands, or for single hand if there are winnings
|
|
||||||
let showAmount = result.hadSplit && handWinnings != nil
|
let showAmount = result.hadSplit && handWinnings != nil
|
||||||
ResultRow(
|
|
||||||
|
HandResultRow(
|
||||||
label: handLabel,
|
label: handLabel,
|
||||||
result: handResult,
|
result: handResult,
|
||||||
amount: showAmount ? handWinnings : nil
|
amount: showAmount ? handWinnings : nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insurance result
|
||||||
if let insuranceResult = result.insuranceResult {
|
if let insuranceResult = result.insuranceResult {
|
||||||
let showInsAmount = result.insuranceWinnings != 0
|
let showInsAmount = result.insuranceWinnings != 0
|
||||||
ResultRow(
|
HandResultRow(
|
||||||
label: String(localized: "Insurance"),
|
label: String(localized: "Insurance"),
|
||||||
result: insuranceResult,
|
result: insuranceResult,
|
||||||
amount: showInsAmount ? result.insuranceWinnings : nil
|
amount: showInsAmount ? result.insuranceWinnings : nil
|
||||||
@ -127,124 +91,44 @@ struct ResultBannerView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(Design.Spacing.medium)
|
},
|
||||||
.background(
|
onNewRound: onNewRound,
|
||||||
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
onPlayAgain: onPlayAgain
|
||||||
.fill(Color.white.opacity(Design.Opacity.subtle))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Game over message
|
|
||||||
if isGameOver {
|
|
||||||
VStack(spacing: Design.Spacing.small) {
|
|
||||||
Text(String(localized: "You've run out of chips!"))
|
|
||||||
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
|
||||||
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
|
||||||
|
|
||||||
Button(action: onPlayAgain) {
|
|
||||||
HStack(spacing: Design.Spacing.small) {
|
|
||||||
Image(systemName: "arrow.counterclockwise")
|
|
||||||
Text(String(localized: "Play Again"))
|
|
||||||
}
|
|
||||||
.font(.system(size: buttonFontSize, weight: .bold))
|
|
||||||
.foregroundStyle(.black)
|
|
||||||
.padding(.horizontal, Design.Spacing.xxLarge)
|
|
||||||
.padding(.vertical, Design.Spacing.medium)
|
|
||||||
.background(
|
|
||||||
Capsule()
|
|
||||||
.fill(
|
|
||||||
LinearGradient(
|
|
||||||
colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
|
|
||||||
startPoint: .top,
|
|
||||||
endPoint: .bottom
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// New Round button
|
|
||||||
Button(action: onNewRound) {
|
|
||||||
HStack(spacing: Design.Spacing.small) {
|
|
||||||
Image(systemName: "arrow.clockwise")
|
|
||||||
Text(String(localized: "New Round"))
|
|
||||||
}
|
|
||||||
.font(.system(size: buttonFontSize, weight: .bold))
|
|
||||||
.foregroundStyle(.black)
|
|
||||||
.padding(.horizontal, Design.Spacing.xxLarge)
|
|
||||||
.padding(.vertical, Design.Spacing.medium)
|
|
||||||
.background(
|
|
||||||
Capsule()
|
|
||||||
.fill(
|
|
||||||
LinearGradient(
|
|
||||||
colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
|
|
||||||
startPoint: .top,
|
|
||||||
endPoint: .bottom
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(Design.Spacing.xxLarge)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge)
|
|
||||||
.fill(
|
|
||||||
LinearGradient(
|
|
||||||
colors: [Color.CasinoModal.backgroundLight, Color.CasinoModal.backgroundDark],
|
|
||||||
startPoint: .top,
|
|
||||||
endPoint: .bottom
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge)
|
|
||||||
.strokeBorder(
|
|
||||||
overallResultColor.opacity(Design.Opacity.medium),
|
|
||||||
lineWidth: Design.LineWidth.medium
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.shadow(color: overallResultColor.opacity(Design.Opacity.hint), radius: Design.Shadow.radiusXLarge)
|
|
||||||
.frame(maxWidth: CasinoDesign.Size.maxModalWidth)
|
|
||||||
.padding(.horizontal, Design.Spacing.large) // Prevent clipping on sides
|
|
||||||
.scaleEffect(showContent ? Design.Scale.normal : Design.Scale.slightShrink)
|
|
||||||
.opacity(showContent ? 1.0 : 0)
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce)) {
|
|
||||||
showContent = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Play game over sound if out of chips (after a delay so it doesn't overlap with result sound)
|
|
||||||
if isGameOver {
|
|
||||||
Task {
|
|
||||||
try? await Task.sleep(for: .seconds(Design.Delay.gameOverSound))
|
|
||||||
SoundManager.shared.play(.gameOver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.accessibilityElement(children: .contain)
|
|
||||||
.accessibilityLabel(String(localized: "Round result: \(result.mainHandResult.displayText)"))
|
|
||||||
.accessibilityAddTraits(AccessibilityTraits.isModal)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Result Row
|
// MARK: - Hand Result Row
|
||||||
|
|
||||||
struct ResultRow: View {
|
/// Row displaying a single hand result with optional amount.
|
||||||
|
private struct HandResultRow: View {
|
||||||
let label: String
|
let label: String
|
||||||
let result: HandResult
|
let result: HandResult
|
||||||
var amount: Int? = nil
|
var amount: Int? = nil
|
||||||
|
|
||||||
private var showDebugBorders: Bool { Design.showDebugBorders }
|
@ScaledMetric(relativeTo: .body) private var fontSize: CGFloat = Design.BaseFontSize.medium
|
||||||
|
|
||||||
|
private var statusIcon: String {
|
||||||
|
switch result {
|
||||||
|
case .blackjack, .win, .insuranceWin:
|
||||||
|
return "checkmark.circle.fill"
|
||||||
|
case .lose, .bust, .insuranceLose:
|
||||||
|
return "xmark.circle.fill"
|
||||||
|
case .push:
|
||||||
|
return "arrow.left.arrow.right.circle.fill"
|
||||||
|
case .surrender:
|
||||||
|
return "flag.circle.fill"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var amountText: String? {
|
private var amountText: String? {
|
||||||
guard let amount = amount else { return nil }
|
guard let amount = amount else { return nil }
|
||||||
if amount > 0 {
|
if amount > 0 {
|
||||||
return "+$\(amount)"
|
return "+\(amount)"
|
||||||
} else if amount < 0 {
|
} else if amount < 0 {
|
||||||
return "-$\(abs(amount))"
|
return "\(amount)"
|
||||||
} else {
|
} else {
|
||||||
return "$0"
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,51 +140,56 @@ struct ResultRow: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack(spacing: Design.Spacing.small) {
|
||||||
|
// Status icon
|
||||||
|
Image(systemName: statusIcon)
|
||||||
|
.font(.system(size: fontSize))
|
||||||
|
.foregroundStyle(result.color)
|
||||||
|
|
||||||
|
// Label
|
||||||
Text(label)
|
Text(label)
|
||||||
.font(.system(size: Design.BaseFontSize.medium))
|
.font(.system(size: fontSize, weight: .medium))
|
||||||
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
.foregroundStyle(.white.opacity(Design.Opacity.heavy))
|
||||||
.debugBorder(showDebugBorders, color: .blue, label: "Label")
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer")
|
|
||||||
|
|
||||||
// Show amount if provided
|
// Amount (if provided)
|
||||||
if let amountText = amountText {
|
if let amountText = amountText {
|
||||||
Text(amountText)
|
Text(amountText)
|
||||||
.font(.system(size: Design.BaseFontSize.medium, weight: .semibold, design: .rounded))
|
.font(.system(size: fontSize, weight: .semibold, design: .rounded))
|
||||||
.foregroundStyle(amountColor)
|
.foregroundStyle(amountColor)
|
||||||
.frame(width: Design.Size.resultRowAmountWidth, alignment: .trailing)
|
|
||||||
.debugBorder(showDebugBorders, color: .green, label: "Amount")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Result text
|
||||||
Text(result.displayText)
|
Text(result.displayText)
|
||||||
.font(.system(size: Design.BaseFontSize.large, weight: .bold))
|
.font(.system(size: fontSize + 2, weight: .bold))
|
||||||
.foregroundStyle(result.color)
|
.foregroundStyle(result.color)
|
||||||
.frame(width: Design.Size.resultRowResultWidth, alignment: .trailing)
|
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.minimumScaleFactor(Design.MinScaleFactor.comfortable)
|
.minimumScaleFactor(Design.MinScaleFactor.comfortable)
|
||||||
.debugBorder(showDebugBorders, color: .red, label: "Result")
|
|
||||||
}
|
}
|
||||||
.debugBorder(showDebugBorders, color: .white, label: "ResultRow")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Side Bet Result Row
|
// MARK: - Side Bet Result Row
|
||||||
|
|
||||||
struct SideBetResultRow: View {
|
/// Row displaying a side bet result.
|
||||||
|
private struct SideBetResultRow: View {
|
||||||
let label: String
|
let label: String
|
||||||
let resultText: String
|
let resultText: String
|
||||||
let isWin: Bool
|
let isWin: Bool
|
||||||
let amount: Int
|
let amount: Int
|
||||||
|
|
||||||
private var showDebugBorders: Bool { Design.showDebugBorders }
|
@ScaledMetric(relativeTo: .body) private var fontSize: CGFloat = Design.BaseFontSize.medium
|
||||||
|
|
||||||
|
private var statusIcon: String {
|
||||||
|
isWin ? "checkmark.circle.fill" : "xmark.circle.fill"
|
||||||
|
}
|
||||||
|
|
||||||
private var amountText: String {
|
private var amountText: String {
|
||||||
if amount > 0 {
|
if amount > 0 {
|
||||||
return "+$\(amount)"
|
return "+\(amount)"
|
||||||
} else if amount < 0 {
|
} else if amount < 0 {
|
||||||
return "-$\(abs(amount))"
|
return "\(amount)"
|
||||||
} else {
|
} else {
|
||||||
return "$0"
|
return "$0"
|
||||||
}
|
}
|
||||||
@ -317,32 +206,35 @@ struct SideBetResultRow: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack(spacing: Design.Spacing.small) {
|
||||||
|
// Status icon
|
||||||
|
Image(systemName: statusIcon)
|
||||||
|
.font(.system(size: fontSize))
|
||||||
|
.foregroundStyle(resultColor)
|
||||||
|
|
||||||
|
// Label
|
||||||
Text(label)
|
Text(label)
|
||||||
.font(.system(size: Design.BaseFontSize.medium))
|
.font(.system(size: fontSize, weight: .medium))
|
||||||
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
.foregroundStyle(.white.opacity(Design.Opacity.heavy))
|
||||||
.debugBorder(showDebugBorders, color: .blue, label: "Label")
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer")
|
|
||||||
|
|
||||||
|
// Amount
|
||||||
Text(amountText)
|
Text(amountText)
|
||||||
.font(.system(size: Design.BaseFontSize.medium, weight: .semibold, design: .rounded))
|
.font(.system(size: fontSize, weight: .semibold, design: .rounded))
|
||||||
.foregroundStyle(amountColor)
|
.foregroundStyle(amountColor)
|
||||||
.frame(width: Design.Size.resultRowAmountWidth, alignment: .trailing)
|
|
||||||
.debugBorder(showDebugBorders, color: .green, label: "Amount")
|
|
||||||
|
|
||||||
|
// Result text
|
||||||
Text(resultText)
|
Text(resultText)
|
||||||
.font(.system(size: Design.BaseFontSize.large, weight: .bold))
|
.font(.system(size: fontSize + 2, weight: .bold))
|
||||||
.foregroundStyle(resultColor)
|
.foregroundStyle(resultColor)
|
||||||
.frame(width: Design.Size.resultRowResultWidth, alignment: .trailing)
|
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.minimumScaleFactor(Design.MinScaleFactor.comfortable)
|
.minimumScaleFactor(Design.MinScaleFactor.comfortable)
|
||||||
.debugBorder(showDebugBorders, color: .red, label: "Result")
|
|
||||||
}
|
|
||||||
.debugBorder(showDebugBorders, color: .white, label: "SideBetRow")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
|
||||||
#Preview("Single Hand") {
|
#Preview("Single Hand") {
|
||||||
ResultBannerView(
|
ResultBannerView(
|
||||||
@ -416,3 +308,19 @@ struct SideBetResultRow: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Preview("Game Over") {
|
||||||
|
ResultBannerView(
|
||||||
|
result: RoundResult(
|
||||||
|
handResults: [.bust],
|
||||||
|
handWinnings: [-100],
|
||||||
|
insuranceResult: nil,
|
||||||
|
insuranceWinnings: 0,
|
||||||
|
totalWinnings: -100,
|
||||||
|
wasBlackjack: false
|
||||||
|
),
|
||||||
|
currentBalance: 0,
|
||||||
|
minBet: 10,
|
||||||
|
onNewRound: {},
|
||||||
|
onPlayAgain: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -27,6 +27,9 @@
|
|||||||
|
|
||||||
// MARK: - Overlays
|
// MARK: - Overlays
|
||||||
// - GameOverView
|
// - GameOverView
|
||||||
|
// - CasinoResultBannerView (animated result banner with staggered animations)
|
||||||
|
// - ResultBreakdownCard (styled container for bet breakdown)
|
||||||
|
// - ResultItemRow (generic result row with icon, label, amount)
|
||||||
|
|
||||||
// MARK: - Table
|
// MARK: - Table
|
||||||
// - TableBackgroundView, FeltPatternView
|
// - TableBackgroundView, FeltPatternView
|
||||||
|
|||||||
@ -165,6 +165,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"+%lld" : {
|
||||||
|
"comment" : "A label displaying the total winnings amount in the result banner. The argument is the total winnings amount.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"$" : {
|
"$" : {
|
||||||
"comment" : "The dollar sign used in the top bar.",
|
"comment" : "The dollar sign used in the top bar.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@ -916,6 +920,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Game over. You've run out of chips." : {
|
||||||
|
"comment" : "Accessibility label for the result banner when the game is over and the user has run out of chips.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Game progress and statistics are stored locally on your device" : {
|
"Game progress and statistics are stored locally on your device" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -1334,6 +1342,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"New Round" : {
|
||||||
|
"comment" : "A button label that initiates a new round of a casino game.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Nine" : {
|
"Nine" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -1890,6 +1902,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"TOTAL" : {
|
||||||
|
"comment" : "A label displayed alongside the total winnings in the result banner.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Total loss: %lld" : {
|
||||||
|
"comment" : "A string that describes the total loss in a casino game. The argument is the absolute value of the total loss, formatted in U.S. Dollars.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Total winnings: %lld" : {
|
||||||
|
"comment" : "A description of the result text that includes the total winnings. The argument is the total winnings, formatted as currency.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Two" : {
|
"Two" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@ -0,0 +1,504 @@
|
|||||||
|
//
|
||||||
|
// ResultBannerView.swift
|
||||||
|
// CasinoKit
|
||||||
|
//
|
||||||
|
// A reusable animated result banner for casino games.
|
||||||
|
// Each game provides its own breakdown content via a @ViewBuilder closure.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A reusable result banner with staggered animations.
|
||||||
|
///
|
||||||
|
/// This component provides the shared structure for result displays:
|
||||||
|
/// - Animated dark overlay
|
||||||
|
/// - Result text with gradient and glow
|
||||||
|
/// - Game-specific breakdown content (provided via @ViewBuilder)
|
||||||
|
/// - Total winnings display
|
||||||
|
/// - New Round / Play Again button
|
||||||
|
/// - Game over handling
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// ```swift
|
||||||
|
/// CasinoResultBannerView(
|
||||||
|
/// resultText: "WIN!",
|
||||||
|
/// resultColor: .green,
|
||||||
|
/// totalWinnings: 500,
|
||||||
|
/// currentBalance: 10500,
|
||||||
|
/// minBet: 10,
|
||||||
|
/// breakdownContent: {
|
||||||
|
/// // Your game-specific breakdown views here
|
||||||
|
/// ForEach(betResults) { bet in
|
||||||
|
/// BetResultRow(bet: bet)
|
||||||
|
/// }
|
||||||
|
/// },
|
||||||
|
/// onNewRound: { startNewRound() },
|
||||||
|
/// onPlayAgain: { resetGame() }
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
public struct CasinoResultBannerView<Content: View>: View {
|
||||||
|
|
||||||
|
// MARK: - Parameters
|
||||||
|
|
||||||
|
/// The result text to display (e.g., "WIN!", "PLAYER WINS", "BLACKJACK!")
|
||||||
|
public let resultText: String
|
||||||
|
|
||||||
|
/// The color associated with the result (green for win, red for loss, etc.)
|
||||||
|
public let resultColor: Color
|
||||||
|
|
||||||
|
/// Total winnings/losses for this round
|
||||||
|
public let totalWinnings: Int
|
||||||
|
|
||||||
|
/// Current balance to determine if game over
|
||||||
|
public let currentBalance: Int
|
||||||
|
|
||||||
|
/// Minimum bet to determine if player can continue
|
||||||
|
public let minBet: Int
|
||||||
|
|
||||||
|
/// Game-specific breakdown content (bet results, hand results, etc.)
|
||||||
|
@ViewBuilder public let breakdownContent: Content
|
||||||
|
|
||||||
|
/// Action when "New Round" is tapped
|
||||||
|
public let onNewRound: () -> Void
|
||||||
|
|
||||||
|
/// Action when "Play Again" is tapped (game over state)
|
||||||
|
public let onPlayAgain: () -> Void
|
||||||
|
|
||||||
|
// MARK: - Animation States
|
||||||
|
|
||||||
|
@State private var showBanner = false
|
||||||
|
@State private var showText = false
|
||||||
|
@State private var showBreakdown = false
|
||||||
|
@State private var showTotal = false
|
||||||
|
@State private var showButton = false
|
||||||
|
|
||||||
|
// MARK: - Environment
|
||||||
|
|
||||||
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
|
|
||||||
|
// MARK: - Scaled Font Sizes (Dynamic Type)
|
||||||
|
|
||||||
|
@ScaledMetric(relativeTo: .largeTitle) private var resultFontSize: CGFloat = CasinoDesign.BaseFontSize.largeTitle
|
||||||
|
@ScaledMetric(relativeTo: .title3) private var totalFontSize: CGFloat = CasinoDesign.BaseFontSize.xxLarge + CasinoDesign.Spacing.xSmall
|
||||||
|
@ScaledMetric(relativeTo: .headline) private var buttonFontSize: CGFloat = CasinoDesign.BaseFontSize.xLarge
|
||||||
|
|
||||||
|
// MARK: - Computed Properties
|
||||||
|
|
||||||
|
/// Whether the player is out of money and can't continue.
|
||||||
|
private var isGameOver: Bool {
|
||||||
|
currentBalance < minBet
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maximum width for the banner card on iPad
|
||||||
|
private var maxBannerWidth: CGFloat {
|
||||||
|
horizontalSizeClass == .regular ? CasinoDesign.Size.maxModalWidth : .infinity
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
/// Creates a result banner view.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - resultText: The main result text to display.
|
||||||
|
/// - resultColor: The color for the result (affects text gradient, border, shadow).
|
||||||
|
/// - totalWinnings: The total amount won or lost this round.
|
||||||
|
/// - currentBalance: The player's current balance (for game over detection).
|
||||||
|
/// - minBet: The minimum bet amount (for game over detection).
|
||||||
|
/// - breakdownContent: Game-specific content showing bet/hand breakdown.
|
||||||
|
/// - onNewRound: Action when "New Round" button is tapped.
|
||||||
|
/// - onPlayAgain: Action when "Play Again" button is tapped (game over state).
|
||||||
|
public init(
|
||||||
|
resultText: String,
|
||||||
|
resultColor: Color,
|
||||||
|
totalWinnings: Int,
|
||||||
|
currentBalance: Int,
|
||||||
|
minBet: Int,
|
||||||
|
@ViewBuilder breakdownContent: () -> Content,
|
||||||
|
onNewRound: @escaping () -> Void,
|
||||||
|
onPlayAgain: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
self.resultText = resultText
|
||||||
|
self.resultColor = resultColor
|
||||||
|
self.totalWinnings = totalWinnings
|
||||||
|
self.currentBalance = currentBalance
|
||||||
|
self.minBet = minBet
|
||||||
|
self.breakdownContent = breakdownContent()
|
||||||
|
self.onNewRound = onNewRound
|
||||||
|
self.onPlayAgain = onPlayAgain
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
ZStack {
|
||||||
|
// Background overlay
|
||||||
|
Color.black.opacity(showBanner ? CasinoDesign.Opacity.medium : 0)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.animation(.easeIn(duration: CasinoDesign.Animation.fadeInDuration), value: showBanner)
|
||||||
|
|
||||||
|
// Banner card
|
||||||
|
VStack(spacing: CasinoDesign.Spacing.medium) {
|
||||||
|
// Result text with gradient and glow
|
||||||
|
Text(resultText)
|
||||||
|
.font(.system(size: resultFontSize, weight: .black, design: .rounded))
|
||||||
|
.foregroundStyle(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [.white, resultColor],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.shadow(color: resultColor.opacity(CasinoDesign.Opacity.heavy), radius: CasinoDesign.Shadow.radiusLarge)
|
||||||
|
.scaleEffect(showText ? CasinoDesign.Scale.normal : CasinoDesign.Scale.shrunk)
|
||||||
|
.opacity(showText ? CasinoDesign.Scale.normal : 0)
|
||||||
|
|
||||||
|
// Game-specific breakdown content
|
||||||
|
breakdownContent
|
||||||
|
.scaleEffect(showBreakdown ? CasinoDesign.Scale.normal : CasinoDesign.Scale.shrunk)
|
||||||
|
.opacity(showBreakdown ? CasinoDesign.Scale.normal : 0)
|
||||||
|
|
||||||
|
// Total winnings
|
||||||
|
if totalWinnings != 0 {
|
||||||
|
totalWinningsView
|
||||||
|
.scaleEffect(showTotal ? CasinoDesign.Scale.normal : CasinoDesign.Scale.shrunk)
|
||||||
|
.opacity(showTotal ? CasinoDesign.Scale.normal : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button section
|
||||||
|
buttonSection
|
||||||
|
.scaleEffect(showButton ? CasinoDesign.Scale.normal : CasinoDesign.Scale.shrunk)
|
||||||
|
.opacity(showButton ? CasinoDesign.Scale.normal : 0)
|
||||||
|
.padding(.top, CasinoDesign.Spacing.small)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, CasinoDesign.Spacing.xLarge)
|
||||||
|
.padding(.vertical, CasinoDesign.Spacing.xxLarge)
|
||||||
|
.frame(maxWidth: maxBannerWidth)
|
||||||
|
.background(bannerBackground)
|
||||||
|
.shadow(color: resultColor.opacity(CasinoDesign.Opacity.light), radius: CasinoDesign.Shadow.radiusXXLarge)
|
||||||
|
.padding(.horizontal, CasinoDesign.Spacing.large)
|
||||||
|
.scaleEffect(showBanner ? CasinoDesign.Scale.normal : CasinoDesign.Scale.slightShrink)
|
||||||
|
.opacity(showBanner ? CasinoDesign.Scale.normal : 0)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
startAnimations()
|
||||||
|
}
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel(accessibilityDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Subviews
|
||||||
|
|
||||||
|
private var totalWinningsView: some View {
|
||||||
|
HStack(spacing: CasinoDesign.Spacing.small) {
|
||||||
|
Text(String(localized: "TOTAL", bundle: .module))
|
||||||
|
.font(.system(size: CasinoDesign.BaseFontSize.body, weight: .bold))
|
||||||
|
.foregroundStyle(.white.opacity(CasinoDesign.Opacity.accent))
|
||||||
|
|
||||||
|
if totalWinnings > 0 {
|
||||||
|
Text("+\(totalWinnings)")
|
||||||
|
.font(.system(size: totalFontSize, weight: .black, design: .rounded))
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
} else {
|
||||||
|
Text("\(totalWinnings)")
|
||||||
|
.font(.system(size: totalFontSize, weight: .black, design: .rounded))
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var buttonSection: some View {
|
||||||
|
if isGameOver {
|
||||||
|
// Game Over - show message and restart button
|
||||||
|
VStack(spacing: CasinoDesign.Spacing.medium) {
|
||||||
|
Text(String(localized: "You've run out of chips!", bundle: .module))
|
||||||
|
.font(.system(size: CasinoDesign.BaseFontSize.medium, weight: .medium))
|
||||||
|
.foregroundStyle(.red.opacity(CasinoDesign.Opacity.heavy))
|
||||||
|
|
||||||
|
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 + CasinoDesign.Spacing.small)
|
||||||
|
.padding(.vertical, CasinoDesign.Spacing.medium + CasinoDesign.Spacing.xxSmall)
|
||||||
|
.background(goldButtonBackground)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normal - New Round button
|
||||||
|
Button {
|
||||||
|
onNewRound()
|
||||||
|
} label: {
|
||||||
|
Text(String(localized: "New Round", bundle: .module))
|
||||||
|
.font(.system(size: buttonFontSize, weight: .bold))
|
||||||
|
.foregroundStyle(.black)
|
||||||
|
.padding(.horizontal, CasinoDesign.Spacing.xxxLarge + CasinoDesign.Spacing.small)
|
||||||
|
.padding(.vertical, CasinoDesign.Spacing.medium + CasinoDesign.Spacing.xxSmall)
|
||||||
|
.background(goldButtonBackground)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var goldButtonBackground: some View {
|
||||||
|
Capsule()
|
||||||
|
.fill(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.shadow(color: .yellow.opacity(CasinoDesign.Opacity.light), radius: CasinoDesign.Shadow.radiusMedium)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var bannerBackground: some View {
|
||||||
|
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.xxLarge)
|
||||||
|
.fill(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color.CasinoModal.backgroundLight,
|
||||||
|
Color.CasinoModal.backgroundDark
|
||||||
|
],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.xxLarge)
|
||||||
|
.strokeBorder(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [
|
||||||
|
resultColor.opacity(CasinoDesign.Opacity.heavy),
|
||||||
|
resultColor.opacity(CasinoDesign.Opacity.light)
|
||||||
|
],
|
||||||
|
startPoint: .topLeading,
|
||||||
|
endPoint: .bottomTrailing
|
||||||
|
),
|
||||||
|
lineWidth: CasinoDesign.LineWidth.thick
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Animations
|
||||||
|
|
||||||
|
private func startAnimations() {
|
||||||
|
withAnimation(.spring(duration: CasinoDesign.Animation.springDuration, bounce: CasinoDesign.Animation.springBounce)) {
|
||||||
|
showBanner = true
|
||||||
|
}
|
||||||
|
|
||||||
|
withAnimation(.spring(duration: CasinoDesign.Animation.springDuration, bounce: CasinoDesign.Animation.springBounce).delay(CasinoDesign.Animation.staggerDelay1)) {
|
||||||
|
showText = true
|
||||||
|
}
|
||||||
|
|
||||||
|
withAnimation(.spring(duration: CasinoDesign.Animation.springDuration, bounce: CasinoDesign.Animation.springBounce).delay(CasinoDesign.Animation.staggerDelay2)) {
|
||||||
|
showBreakdown = true
|
||||||
|
}
|
||||||
|
|
||||||
|
withAnimation(.spring(duration: CasinoDesign.Animation.springDuration, bounce: CasinoDesign.Animation.springBounce).delay(CasinoDesign.Animation.staggerDelay3)) {
|
||||||
|
showTotal = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show button after everything else
|
||||||
|
withAnimation(.spring(duration: CasinoDesign.Animation.springDuration, bounce: CasinoDesign.Animation.springBounce).delay(CasinoDesign.Animation.staggerDelay3 + CasinoDesign.Animation.staggerDelay1)) {
|
||||||
|
showButton = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play game over sound if out of chips
|
||||||
|
if isGameOver {
|
||||||
|
Task {
|
||||||
|
try? await Task.sleep(for: .seconds(1))
|
||||||
|
SoundManager.shared.play(.gameOver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Announce result to VoiceOver users
|
||||||
|
announceResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Accessibility
|
||||||
|
|
||||||
|
private var accessibilityDescription: String {
|
||||||
|
var description = resultText
|
||||||
|
|
||||||
|
if totalWinnings > 0 {
|
||||||
|
description += ". " + String(localized: "Total winnings: \(totalWinnings)", bundle: .module)
|
||||||
|
} else if totalWinnings < 0 {
|
||||||
|
description += ". " + String(localized: "Total loss: \(abs(totalWinnings))", bundle: .module)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isGameOver {
|
||||||
|
description += ". " + String(localized: "Game over. You've run out of chips.", bundle: .module)
|
||||||
|
}
|
||||||
|
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
|
||||||
|
private func announceResult() {
|
||||||
|
Task { @MainActor in
|
||||||
|
try? await Task.sleep(for: .milliseconds(500))
|
||||||
|
AccessibilityNotification.Announcement(accessibilityDescription).post()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Result Breakdown Card
|
||||||
|
|
||||||
|
/// A styled container for bet/hand breakdown content.
|
||||||
|
/// Provides consistent styling across all casino games.
|
||||||
|
public struct ResultBreakdownCard<Content: View>: View {
|
||||||
|
@ViewBuilder public let content: Content
|
||||||
|
|
||||||
|
public init(@ViewBuilder content: () -> Content) {
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
VStack(spacing: CasinoDesign.Spacing.xSmall) {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
.padding(.horizontal, CasinoDesign.Spacing.medium)
|
||||||
|
.padding(.vertical, CasinoDesign.Spacing.small)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.medium)
|
||||||
|
.fill(Color.white.opacity(CasinoDesign.Opacity.verySubtle))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Result Row
|
||||||
|
|
||||||
|
/// A generic row for displaying a result item with label, optional amount, and status.
|
||||||
|
public struct ResultItemRow: View {
|
||||||
|
public let label: String
|
||||||
|
public let statusText: String
|
||||||
|
public let statusColor: Color
|
||||||
|
public var amount: Int?
|
||||||
|
public var statusIcon: String?
|
||||||
|
|
||||||
|
@ScaledMetric(relativeTo: .body) private var fontSize: CGFloat = CasinoDesign.BaseFontSize.medium
|
||||||
|
|
||||||
|
public init(
|
||||||
|
label: String,
|
||||||
|
statusText: String,
|
||||||
|
statusColor: Color,
|
||||||
|
amount: Int? = nil,
|
||||||
|
statusIcon: String? = nil
|
||||||
|
) {
|
||||||
|
self.label = label
|
||||||
|
self.statusText = statusText
|
||||||
|
self.statusColor = statusColor
|
||||||
|
self.amount = amount
|
||||||
|
self.statusIcon = statusIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
private var amountText: String? {
|
||||||
|
guard let amount = amount else { return nil }
|
||||||
|
if amount > 0 {
|
||||||
|
return "+\(amount)"
|
||||||
|
} else if amount < 0 {
|
||||||
|
return "\(amount)"
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var amountColor: Color {
|
||||||
|
guard let amount = amount else { return .white }
|
||||||
|
if amount > 0 { return .green }
|
||||||
|
if amount < 0 { return .red }
|
||||||
|
return .blue
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
HStack(spacing: CasinoDesign.Spacing.small) {
|
||||||
|
// Status icon (if provided)
|
||||||
|
if let icon = statusIcon {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.system(size: fontSize))
|
||||||
|
.foregroundStyle(statusColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label
|
||||||
|
Text(label)
|
||||||
|
.font(.system(size: fontSize, weight: .medium))
|
||||||
|
.foregroundStyle(.white.opacity(CasinoDesign.Opacity.heavy))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
// Amount (if provided)
|
||||||
|
if let amountText = amountText {
|
||||||
|
Text(amountText)
|
||||||
|
.font(.system(size: fontSize, weight: .semibold, design: .rounded))
|
||||||
|
.foregroundStyle(amountColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status text
|
||||||
|
Text(statusText)
|
||||||
|
.font(.system(size: fontSize + 2, weight: .bold))
|
||||||
|
.foregroundStyle(statusColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Preview
|
||||||
|
|
||||||
|
#Preview("Win Result") {
|
||||||
|
ZStack {
|
||||||
|
Color.CasinoTable.felt
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
CasinoResultBannerView(
|
||||||
|
resultText: "WIN!",
|
||||||
|
resultColor: .green,
|
||||||
|
totalWinnings: 500,
|
||||||
|
currentBalance: 10500,
|
||||||
|
minBet: 10,
|
||||||
|
breakdownContent: {
|
||||||
|
ResultBreakdownCard {
|
||||||
|
ResultItemRow(
|
||||||
|
label: "Main Hand",
|
||||||
|
statusText: "WIN",
|
||||||
|
statusColor: .green,
|
||||||
|
amount: 500,
|
||||||
|
statusIcon: "checkmark.circle.fill"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNewRound: {},
|
||||||
|
onPlayAgain: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Game Over") {
|
||||||
|
ZStack {
|
||||||
|
Color.CasinoTable.felt
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
CasinoResultBannerView(
|
||||||
|
resultText: "LOSE",
|
||||||
|
resultColor: .red,
|
||||||
|
totalWinnings: -100,
|
||||||
|
currentBalance: 0,
|
||||||
|
minBet: 10,
|
||||||
|
breakdownContent: {
|
||||||
|
ResultBreakdownCard {
|
||||||
|
ResultItemRow(
|
||||||
|
label: "Main Hand",
|
||||||
|
statusText: "BUST",
|
||||||
|
statusColor: .red,
|
||||||
|
amount: -100,
|
||||||
|
statusIcon: "xmark.circle.fill"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNewRound: {},
|
||||||
|
onPlayAgain: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user