202 lines
7.9 KiB
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: {}
|
|
)
|
|
}
|
|
|