CasinoGames/CasinoKit/Sources/CasinoKit/Views/Overlays/GameOverView.swift

202 lines
7.9 KiB
Swift

//
// 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) {
GameOverStatRow(
label: String(localized: "Rounds Played", bundle: .module),
value: "\(roundsPlayed)",
fontSize: statsFontSize
)
ForEach(additionalStats.indices, id: \.self) { index in
GameOverStatRow(
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 GameOverStatRow: 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: {}
)
}