Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
6ae84c02eb
commit
d2b9688b30
@ -83,6 +83,11 @@ enum Design {
|
|||||||
static let topBetRowHeight: CGFloat = 52
|
static let topBetRowHeight: CGFloat = 52
|
||||||
static let mainBetRowHeight: CGFloat = 65
|
static let mainBetRowHeight: CGFloat = 65
|
||||||
static let bonusZoneWidth: CGFloat = 80
|
static let bonusZoneWidth: CGFloat = 80
|
||||||
|
|
||||||
|
// iPad max widths
|
||||||
|
static let maxContentWidthPortrait: CGFloat = 500
|
||||||
|
static let maxContentWidthLandscape: CGFloat = 800
|
||||||
|
static let maxModalWidth: CGFloat = 450
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Animation
|
// MARK: - Animation
|
||||||
|
|||||||
@ -17,6 +17,24 @@ struct GameTableView: View {
|
|||||||
@State private var showRules = false
|
@State private var showRules = false
|
||||||
@State private var showStats = false
|
@State private var showStats = false
|
||||||
|
|
||||||
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
|
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||||
|
|
||||||
|
/// Whether we're on iPad or large screen
|
||||||
|
private var isLargeScreen: Bool {
|
||||||
|
horizontalSizeClass == .regular
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether we're in landscape mode (compact vertical on iPad)
|
||||||
|
private var isLandscape: Bool {
|
||||||
|
verticalSizeClass == .compact
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maximum width for game content on large screens
|
||||||
|
private var maxContentWidth: CGFloat {
|
||||||
|
isLandscape ? Design.Size.maxContentWidthLandscape : Design.Size.maxContentWidthPortrait
|
||||||
|
}
|
||||||
|
|
||||||
private var state: GameState {
|
private var state: GameState {
|
||||||
gameState ?? GameState(settings: settings)
|
gameState ?? GameState(settings: settings)
|
||||||
}
|
}
|
||||||
@ -53,7 +71,7 @@ struct GameTableView: View {
|
|||||||
|
|
||||||
Spacer(minLength: Design.Spacing.xSmall)
|
Spacer(minLength: Design.Spacing.xSmall)
|
||||||
|
|
||||||
// Cards display area
|
// Cards display area - constrained width on iPad
|
||||||
CardsDisplayArea(
|
CardsDisplayArea(
|
||||||
playerCards: state.visiblePlayerCards,
|
playerCards: state.visiblePlayerCards,
|
||||||
bankerCards: state.visibleBankerCards,
|
bankerCards: state.visibleBankerCards,
|
||||||
@ -65,36 +83,40 @@ struct GameTableView: View {
|
|||||||
bankerIsWinner: bankerIsWinner,
|
bankerIsWinner: bankerIsWinner,
|
||||||
isTie: isTie
|
isTie: isTie
|
||||||
)
|
)
|
||||||
|
.frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity)
|
||||||
|
|
||||||
Spacer(minLength: Design.Spacing.xSmall)
|
Spacer(minLength: Design.Spacing.xSmall)
|
||||||
|
|
||||||
// Road map history
|
// Road map history - constrained width on iPad
|
||||||
if settings.showHistory && !state.roundHistory.isEmpty {
|
if settings.showHistory && !state.roundHistory.isEmpty {
|
||||||
RoadMapView(results: state.recentResults)
|
RoadMapView(results: state.recentResults)
|
||||||
|
.frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(minLength: Design.Spacing.small)
|
Spacer(minLength: Design.Spacing.small)
|
||||||
|
|
||||||
// Mini Baccarat betting table
|
// Mini Baccarat betting table - constrained width on iPad
|
||||||
MiniBaccaratTableView(
|
MiniBaccaratTableView(
|
||||||
gameState: state,
|
gameState: state,
|
||||||
selectedChip: selectedChip
|
selectedChip: selectedChip
|
||||||
)
|
)
|
||||||
|
.frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity)
|
||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
Spacer(minLength: Design.Spacing.medium)
|
Spacer(minLength: Design.Spacing.medium)
|
||||||
|
|
||||||
// Chip selector - shows higher chips as you win more!
|
// Chip selector - constrained width on iPad
|
||||||
ChipSelectorView(
|
ChipSelectorView(
|
||||||
selectedChip: $selectedChip,
|
selectedChip: $selectedChip,
|
||||||
balance: state.balance,
|
balance: state.balance,
|
||||||
maxBet: state.maxBet
|
maxBet: state.maxBet
|
||||||
)
|
)
|
||||||
|
.frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity)
|
||||||
|
|
||||||
Spacer(minLength: Design.Spacing.small)
|
Spacer(minLength: Design.Spacing.small)
|
||||||
|
|
||||||
// Action buttons
|
// Action buttons - constrained width on iPad
|
||||||
ActionButtonsView(
|
ActionButtonsView(
|
||||||
gameState: state,
|
gameState: state,
|
||||||
onDeal: {
|
onDeal: {
|
||||||
@ -105,12 +127,13 @@ struct GameTableView: View {
|
|||||||
onClear: { state.clearBets() },
|
onClear: { state.clearBets() },
|
||||||
onNewRound: { state.newRound() }
|
onNewRound: { state.newRound() }
|
||||||
)
|
)
|
||||||
|
.frame(maxWidth: isLargeScreen ? maxContentWidth * 0.8 : .infinity)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.padding(.bottom, Design.Spacing.xSmall)
|
.padding(.bottom, Design.Spacing.xSmall)
|
||||||
}
|
}
|
||||||
.safeAreaPadding(.bottom)
|
.safeAreaPadding(.bottom)
|
||||||
|
|
||||||
// Result banner overlay
|
// Result banner overlay (handles its own iPad sizing)
|
||||||
if state.showResultBanner, let result = state.lastResult {
|
if state.showResultBanner, let result = state.lastResult {
|
||||||
ResultBannerView(
|
ResultBannerView(
|
||||||
result: result,
|
result: result,
|
||||||
@ -134,7 +157,7 @@ struct GameTableView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Game Over overlay when broke
|
// Game Over overlay (handles its own iPad sizing)
|
||||||
if state.balance == 0 && state.currentBets.isEmpty && !state.isAnimating {
|
if state.balance == 0 && state.currentBets.isEmpty && !state.isAnimating {
|
||||||
GameOverView(
|
GameOverView(
|
||||||
roundsPlayed: state.roundHistory.count,
|
roundsPlayed: state.roundHistory.count,
|
||||||
@ -171,6 +194,12 @@ struct GameOverView: View {
|
|||||||
let onPlayAgain: () -> Void
|
let onPlayAgain: () -> Void
|
||||||
|
|
||||||
@State private var showContent = false
|
@State private var showContent = false
|
||||||
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
|
|
||||||
|
/// Maximum width for the modal card on iPad
|
||||||
|
private var maxModalWidth: CGFloat {
|
||||||
|
horizontalSizeClass == .regular ? Design.Size.maxModalWidth : .infinity
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Scaled Font Sizes (Dynamic Type)
|
// MARK: - Scaled Font Sizes (Dynamic Type)
|
||||||
|
|
||||||
@ -290,6 +319,7 @@ struct GameOverView: View {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.shadow(color: .red.opacity(Design.Opacity.hint), radius: Design.Shadow.radiusXXLarge)
|
.shadow(color: .red.opacity(Design.Opacity.hint), radius: Design.Shadow.radiusXXLarge)
|
||||||
|
.frame(maxWidth: maxModalWidth)
|
||||||
.padding(.horizontal, Design.Spacing.xxLarge)
|
.padding(.horizontal, Design.Spacing.xxLarge)
|
||||||
.scaleEffect(showContent ? Design.Scale.normal : Design.Scale.slightShrink)
|
.scaleEffect(showContent ? Design.Scale.normal : Design.Scale.slightShrink)
|
||||||
.opacity(showContent ? 1.0 : 0)
|
.opacity(showContent ? 1.0 : 0)
|
||||||
@ -434,10 +464,31 @@ struct CompactHandView: View {
|
|||||||
// Fixed size: cards have strict visual constraints
|
// Fixed size: cards have strict visual constraints
|
||||||
|
|
||||||
private let cardWidth: CGFloat = 45
|
private let cardWidth: CGFloat = 45
|
||||||
|
private let cardHeight: CGFloat = 63 // Standard card aspect ratio ~1.4
|
||||||
private let cardOverlap: CGFloat = -12
|
private let cardOverlap: CGFloat = -12
|
||||||
private let placeholderSpacing: CGFloat = 8
|
private let placeholderSpacing: CGFloat = 8
|
||||||
|
|
||||||
|
/// Fixed container width to prevent resizing during deal
|
||||||
|
/// Calculated as: 3 cards with overlap + padding
|
||||||
|
/// = cardWidth + (cardWidth + overlap) * 2 + padding * 2
|
||||||
|
private var fixedContainerWidth: CGFloat {
|
||||||
|
// Max 3 cards: first card full width + 2 more with overlap
|
||||||
|
let cardsWidth = cardWidth + (cardWidth + cardOverlap) * 2
|
||||||
|
return cardsWidth + Design.Spacing.xSmall * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fixed container height
|
||||||
|
private var fixedContainerHeight: CGFloat {
|
||||||
|
cardHeight + Design.Spacing.xSmall * 2
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
// Fixed-size container
|
||||||
|
Color.clear
|
||||||
|
.frame(width: fixedContainerWidth, height: fixedContainerHeight)
|
||||||
|
|
||||||
|
// Cards content centered in fixed container
|
||||||
HStack(spacing: cards.isEmpty ? placeholderSpacing : cardOverlap) {
|
HStack(spacing: cards.isEmpty ? placeholderSpacing : cardOverlap) {
|
||||||
if cards.isEmpty {
|
if cards.isEmpty {
|
||||||
// Placeholders - no overlap, just side by side
|
// Placeholders - no overlap, just side by side
|
||||||
@ -456,7 +507,7 @@ struct CompactHandView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(Design.Spacing.xSmall)
|
}
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
|
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
|
||||||
.strokeBorder(
|
.strokeBorder(
|
||||||
@ -692,14 +743,23 @@ struct ActionButtonsView: View {
|
|||||||
private let iconSize: CGFloat = Design.BaseFontSize.xxLarge + Design.Spacing.xSmall
|
private let iconSize: CGFloat = Design.BaseFontSize.xxLarge + Design.Spacing.xSmall
|
||||||
private let statusFontSize: CGFloat = Design.BaseFontSize.medium
|
private let statusFontSize: CGFloat = Design.BaseFontSize.medium
|
||||||
|
|
||||||
|
/// Fixed height to prevent layout shifts when buttons change
|
||||||
|
private let containerHeight: CGFloat = 50
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: Design.Spacing.medium) {
|
ZStack {
|
||||||
|
// Fixed height container to prevent layout shifts
|
||||||
|
Color.clear
|
||||||
|
.frame(height: containerHeight)
|
||||||
|
|
||||||
|
// Content changes with animation
|
||||||
|
Group {
|
||||||
if gameState.currentPhase == .betting {
|
if gameState.currentPhase == .betting {
|
||||||
// Clear bets button - icon only at accessibility sizes
|
// Clear bets button - icon only at accessibility sizes
|
||||||
|
HStack(spacing: Design.Spacing.medium) {
|
||||||
clearButton
|
clearButton
|
||||||
|
|
||||||
// Deal button - icon only at accessibility sizes
|
|
||||||
dealButton
|
dealButton
|
||||||
|
}
|
||||||
} else if gameState.currentPhase == .roundComplete && !gameState.showResultBanner {
|
} else if gameState.currentPhase == .roundComplete && !gameState.showResultBanner {
|
||||||
// New round button - only shown after banner is dismissed
|
// New round button - only shown after banner is dismissed
|
||||||
// (The banner itself has a New Round button)
|
// (The banner itself has a New Round button)
|
||||||
@ -720,6 +780,9 @@ struct ActionButtonsView: View {
|
|||||||
.padding(.vertical, Design.Spacing.medium)
|
.padding(.vertical, Design.Spacing.medium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.animation(.easeInOut(duration: Design.Animation.quick), value: gameState.currentPhase)
|
||||||
|
.animation(.easeInOut(duration: Design.Animation.quick), value: gameState.showResultBanner)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|||||||
@ -31,6 +31,13 @@ struct ResultBannerView: View {
|
|||||||
@State private var showTotal = false
|
@State private var showTotal = false
|
||||||
@State private var showButton = 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 ? Design.Size.maxModalWidth : .infinity
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Scaled Font Sizes (Dynamic Type)
|
// MARK: - Scaled Font Sizes (Dynamic Type)
|
||||||
|
|
||||||
@ScaledMetric(relativeTo: .largeTitle) private var resultFontSize: CGFloat = Design.BaseFontSize.largeTitle
|
@ScaledMetric(relativeTo: .largeTitle) private var resultFontSize: CGFloat = Design.BaseFontSize.largeTitle
|
||||||
@ -185,6 +192,7 @@ struct ResultBannerView: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, Design.Spacing.xLarge)
|
.padding(.horizontal, Design.Spacing.xLarge)
|
||||||
.padding(.vertical, Design.Spacing.xxLarge)
|
.padding(.vertical, Design.Spacing.xxLarge)
|
||||||
|
.frame(maxWidth: maxBannerWidth)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge)
|
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge)
|
||||||
.fill(
|
.fill(
|
||||||
@ -213,6 +221,7 @@ struct ResultBannerView: View {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.shadow(color: result.color.opacity(Design.Opacity.light), radius: Design.Shadow.radiusXXLarge)
|
.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)
|
.scaleEffect(showBanner ? Design.Scale.normal : Design.Scale.slightShrink)
|
||||||
.opacity(showBanner ? Design.Scale.normal : 0)
|
.opacity(showBanner ? Design.Scale.normal : 0)
|
||||||
}
|
}
|
||||||
@ -395,6 +404,8 @@ private struct PairBadge: View {
|
|||||||
/// Confetti particle for celebrations.
|
/// Confetti particle for celebrations.
|
||||||
struct ConfettiPiece: View {
|
struct ConfettiPiece: View {
|
||||||
let color: Color
|
let color: Color
|
||||||
|
let containerSize: CGSize
|
||||||
|
|
||||||
@State private var position: CGPoint = .zero
|
@State private var position: CGPoint = .zero
|
||||||
@State private var rotation: Double = 0
|
@State private var rotation: Double = 0
|
||||||
@State private var opacity: Double = 1
|
@State private var opacity: Double = 1
|
||||||
@ -410,14 +421,13 @@ struct ConfettiPiece: View {
|
|||||||
.position(position)
|
.position(position)
|
||||||
.opacity(opacity)
|
.opacity(opacity)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
let screenWidth = 400.0
|
let startX = Double.random(in: 0...containerSize.width)
|
||||||
let startX = Double.random(in: 0...screenWidth)
|
|
||||||
position = CGPoint(x: startX, y: -20)
|
position = CGPoint(x: startX, y: -20)
|
||||||
|
|
||||||
withAnimation(.easeIn(duration: Double.random(in: 2...4))) {
|
withAnimation(.easeIn(duration: Double.random(in: 2...4))) {
|
||||||
position = CGPoint(
|
position = CGPoint(
|
||||||
x: startX + Double.random(in: -100...100),
|
x: startX + Double.random(in: -100...100),
|
||||||
y: 800
|
y: containerSize.height + 50
|
||||||
)
|
)
|
||||||
rotation = Double.random(in: 360...1080)
|
rotation = Double.random(in: 360...1080)
|
||||||
opacity = 0
|
opacity = 0
|
||||||
@ -431,11 +441,17 @@ struct ConfettiView: View {
|
|||||||
let colors: [Color] = [.red, .blue, .green, .yellow, .orange, .purple, .pink]
|
let colors: [Color] = [.red, .blue, .green, .yellow, .orange, .purple, .pink]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
ZStack {
|
ZStack {
|
||||||
ForEach(0..<50, id: \.self) { _ in
|
ForEach(0..<50, id: \.self) { _ in
|
||||||
ConfettiPiece(color: colors.randomElement() ?? .yellow)
|
ConfettiPiece(
|
||||||
|
color: colors.randomElement() ?? .yellow,
|
||||||
|
containerSize: geometry.size
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.ignoresSafeArea()
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -134,6 +134,7 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(.yellow)
|
.tint(.yellow)
|
||||||
|
.padding(.vertical, Design.Spacing.xSmall)
|
||||||
|
|
||||||
if gameState.iCloudEnabled {
|
if gameState.iCloudEnabled {
|
||||||
Divider()
|
Divider()
|
||||||
@ -186,6 +187,7 @@ struct SettingsView: View {
|
|||||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.vertical, Design.Spacing.xSmall)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,6 +382,7 @@ struct SettingsToggle: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(.yellow)
|
.tint(.yellow)
|
||||||
|
.padding(.vertical, Design.Spacing.xSmall)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,6 +421,7 @@ struct SpeedPicker: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.vertical, Design.Spacing.xSmall)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -515,6 +519,7 @@ struct VolumePicker: View {
|
|||||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.vertical, Design.Spacing.xSmall)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user