238 lines
8.9 KiB
Swift
238 lines
8.9 KiB
Swift
//
|
|
// ResultBannerView.swift
|
|
// Blackjack
|
|
//
|
|
// Displays the result of a round with breakdown.
|
|
//
|
|
|
|
import SwiftUI
|
|
import CasinoKit
|
|
|
|
struct ResultBannerView: View {
|
|
let result: RoundResult
|
|
let currentBalance: Int
|
|
let minBet: Int
|
|
let onNewRound: () -> 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
|
|
|
|
private var isGameOver: Bool {
|
|
currentBalance < minBet
|
|
}
|
|
|
|
private var mainResultColor: Color {
|
|
result.mainHandResult.color
|
|
}
|
|
|
|
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 {
|
|
ZStack {
|
|
// Full screen dark background
|
|
Color.black.opacity(Design.Opacity.strong)
|
|
.ignoresSafeArea()
|
|
|
|
// Content card
|
|
VStack(spacing: Design.Spacing.xLarge) {
|
|
// Main result
|
|
Text(result.mainHandResult.displayText)
|
|
.font(.system(size: titleFontSize, weight: .black, design: .rounded))
|
|
.foregroundStyle(mainResultColor)
|
|
|
|
// Winnings
|
|
Text(winningsText)
|
|
.font(.system(size: amountFontSize, weight: .bold, design: .rounded))
|
|
.foregroundStyle(winningsColor)
|
|
|
|
// Breakdown - all hands
|
|
VStack(spacing: Design.Spacing.small) {
|
|
ForEach(result.handResults.indices, id: \.self) { index in
|
|
let handResult = result.handResults[index]
|
|
// Hand numbering: index 0 = Hand 1 (played first, displayed rightmost)
|
|
let handLabel = result.handResults.count > 1
|
|
? String(localized: "Hand \(index + 1)")
|
|
: String(localized: "Main Hand")
|
|
ResultRow(label: handLabel, result: handResult)
|
|
}
|
|
|
|
if let insuranceResult = result.insuranceResult {
|
|
ResultRow(label: String(localized: "Insurance"), result: insuranceResult)
|
|
}
|
|
}
|
|
.padding(Design.Spacing.medium)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
|
.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.Button.goldLight, Color.Button.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.Button.goldLight, Color.Button.goldDark],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
.padding(Design.Spacing.xxLarge)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge)
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [Color.Modal.backgroundLight, Color.Modal.backgroundDark],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge)
|
|
.strokeBorder(
|
|
mainResultColor.opacity(Design.Opacity.medium),
|
|
lineWidth: Design.LineWidth.medium
|
|
)
|
|
)
|
|
)
|
|
.shadow(color: mainResultColor.opacity(Design.Opacity.hint), radius: Design.Shadow.radiusXLarge)
|
|
.frame(maxWidth: Design.Size.maxModalWidth)
|
|
.padding(.horizontal, Design.Spacing.large) // Prevent clipping on sides
|
|
.scaleEffect(showContent ? 1.0 : 0.8)
|
|
.opacity(showContent ? 1.0 : 0)
|
|
}
|
|
.onAppear {
|
|
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: 0.3)) {
|
|
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(1))
|
|
SoundManager.shared.play(.gameOver)
|
|
}
|
|
}
|
|
}
|
|
.accessibilityElement(children: .contain)
|
|
.accessibilityLabel(String(localized: "Round result: \(result.mainHandResult.displayText)"))
|
|
.accessibilityAddTraits(AccessibilityTraits.isModal)
|
|
}
|
|
}
|
|
|
|
// MARK: - Result Row
|
|
|
|
struct ResultRow: View {
|
|
let label: String
|
|
let result: HandResult
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Text(label)
|
|
.font(.system(size: Design.BaseFontSize.body))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
|
|
|
Spacer()
|
|
|
|
Text(result.displayText)
|
|
.font(.system(size: Design.BaseFontSize.body, weight: .bold))
|
|
.foregroundStyle(result.color)
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview("Single Hand") {
|
|
ResultBannerView(
|
|
result: RoundResult(
|
|
handResults: [.blackjack],
|
|
insuranceResult: nil,
|
|
totalWinnings: 150,
|
|
wasBlackjack: true
|
|
),
|
|
currentBalance: 10150,
|
|
minBet: 10,
|
|
onNewRound: {},
|
|
onPlayAgain: {}
|
|
)
|
|
}
|
|
|
|
#Preview("Multiple Split Hands") {
|
|
ResultBannerView(
|
|
result: RoundResult(
|
|
handResults: [.bust, .win, .push],
|
|
insuranceResult: nil,
|
|
totalWinnings: 25,
|
|
wasBlackjack: false
|
|
),
|
|
currentBalance: 1025,
|
|
minBet: 10,
|
|
onNewRound: {},
|
|
onPlayAgain: {}
|
|
)
|
|
}
|
|
|