Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2025-12-24 12:19:59 -06:00
parent 1dc64ebf69
commit 2cd2946a80
6 changed files with 4638 additions and 4606 deletions

View File

@ -53,6 +53,9 @@ final class GameState {
/// Whether to show side bet toast notifications.
var showSideBetToasts: Bool = false
/// Whether to show the gameplay hint toast.
var showHintToast: Bool = false
/// Whether a reshuffle notification should be shown.
var showReshuffleNotification: Bool = false
@ -787,7 +790,7 @@ final class GameState {
// Auto-hide toasts after delay
Task {
try? await Task.sleep(for: .seconds(3))
try? await Task.sleep(for: Design.Toast.duration)
showSideBetToasts = false
}
}

File diff suppressed because it is too large Load Diff

View File

@ -109,6 +109,13 @@ enum Design {
/// Bounce for side bet toast animations
static let toastBounce: Double = 0.4
}
// MARK: - Toast Configuration
enum Toast {
/// Duration all toasts stay visible (in seconds).
static let duration: Duration = .seconds(2)
}
}
// MARK: - Blackjack App Colors

View File

@ -71,6 +71,31 @@ struct GameTableView: View {
// Use global debug flag from Design constants
private var showDebugBorders: Bool { Design.showDebugBorders }
// MARK: - Toolbar Buttons
/// Returns hint toolbar button when a hint is available.
private func hintToolbarButtons(for state: GameState) -> [TopBarButton] {
guard state.currentHint != nil else { return [] }
return [
TopBarButton(
icon: "lightbulb.fill",
accessibilityLabel: String(localized: "Show Hint")
) {
// Show the toast with animation
withAnimation(.spring(duration: Design.Animation.springDuration)) {
state.showHintToast = true
}
// Auto-dismiss after delay (same as auto-show behavior)
Task { @MainActor in
try? await Task.sleep(for: Design.Toast.duration)
withAnimation(.spring(duration: Design.Animation.springDuration)) {
state.showHintToast = false
}
}
}
]
}
// MARK: - Main Game View
@ViewBuilder
@ -97,6 +122,7 @@ struct GameTableView: View {
balance: state.balance,
secondaryInfo: settings.showCardsRemaining ? "\(state.engine.cardsRemaining)" : nil,
secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil,
leadingButtons: hintToolbarButtons(for: state),
onSettings: { showSettings = true },
onHelp: { showRules = true },
onStats: { showStats = true }
@ -143,7 +169,7 @@ struct GameTableView: View {
.padding(.bottom, Design.Spacing.small)
.debugBorder(showDebugBorders, color: .blue, label: "ActionBtns")
}
.frame(maxWidth: .infinity, alignment: .top)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.zIndex(1)
.onChange(of: state.currentPhase) { oldPhase, newPhase in
Design.debugLog("🔄 Phase changed: \(oldPhase)\(newPhase)")
@ -208,6 +234,7 @@ struct GameTableView: View {
.allowsHitTesting(true)
.zIndex(100)
}
}
.onChange(of: state.playerHands.count) { oldCount, newCount in
Design.debugLog("👥 Player hands count: \(oldCount)\(newCount)")

View File

@ -31,7 +31,6 @@ struct BlackjackTableView: View {
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.BaseFontSize.medium
@ScaledMetric(relativeTo: .title) private var valueFontSize: CGFloat = Design.BaseFontSize.xLarge
@ScaledMetric(relativeTo: .caption) private var hintFontSize: CGFloat = Design.BaseFontSize.small
// MARK: - Dynamic Card Sizing
@ -48,7 +47,7 @@ struct BlackjackTableView: View {
/// Card width based on full screen height (stable - doesn't change with content)
private var cardWidth: CGFloat {
let maxDimension = screenHeight
let percentage: CGFloat = 0.15 // ~10% of screen
let percentage: CGFloat = 0.18 // ~10% of screen
return maxDimension * percentage
}
@ -58,32 +57,12 @@ struct BlackjackTableView: View {
cardWidth * -0.55
}
/// Fixed height for the hint area to prevent layout shifts
private let hintAreaHeight: CGFloat = 44
// Use global debug flag from Design constants
private var showDebugBorders: Bool { Design.showDebugBorders }
/// Dynamic spacer height based on screen size.
/// Formula: spacing = clamp((screenHeight - baseline) * scale, min, max)
/// This produces smooth scaling across all device sizes:
/// - iPhone SE (~667pt): ~20pt
/// - iPhone Pro Max (~932pt): ~76pt
/// - iPad Mini (~1024pt): ~95pt
/// - iPad Pro 12.9" (~1366pt): ~150pt (capped)
private var dealerPlayerSpacing: CGFloat {
let baseline: CGFloat = 550 // Below this, use minimum
let scale: CGFloat = 0.18 // 20% of height above baseline
let minSpacing: CGFloat = 10 // Floor for smallest screens
let maxSpacing: CGFloat = 150 // Ceiling for largest screens
let calculated = (screenHeight - baseline) * scale
return min(maxSpacing, max(minSpacing, calculated))
}
var body: some View {
VStack(spacing: Design.Spacing.small) {
// Dealer area
VStack(spacing: 0) {
// Dealer area - pushed to top
DealerHandView(
hand: state.dealerHand,
showHoleCard: state.shouldShowDealerHoleCard,
@ -93,33 +72,23 @@ struct BlackjackTableView: View {
)
.debugBorder(showDebugBorders, color: .red, label: "Dealer")
// Space between dealer and player - contains card count if enabled
// Top spacer pushes dealer up
Spacer(minLength: Design.Spacing.small)
.debugBorder(showDebugBorders, color: .yellow, label: "TopSpacer")
// Card count view centered between dealer and player
if showCardCount {
// Card count view fills the space between hands
VStack {
// Top spacer for larger screens
if screenHeight > 700 {
Spacer(minLength: Design.Spacing.small)
}
CardCountView(
runningCount: state.engine.runningCount,
trueCount: state.engine.trueCount
)
// Bottom spacer for larger screens
if screenHeight > 700 {
Spacer(minLength: Design.Spacing.small)
}
}
.frame(minHeight: dealerPlayerSpacing)
CardCountView(
runningCount: state.engine.runningCount,
trueCount: state.engine.trueCount
)
.debugBorder(showDebugBorders, color: .mint, label: "CardCount")
} else {
// No card count - just use flexible spacer
Spacer(minLength: dealerPlayerSpacing)
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer \(Int(dealerPlayerSpacing))")
}
// Bottom spacer pushes player down
Spacer(minLength: Design.Spacing.small)
.debugBorder(showDebugBorders, color: .yellow, label: "BottomSpacer")
// Player hands area - only show when there are cards dealt
if state.playerHands.first?.cards.isEmpty == false {
ZStack {
@ -162,6 +131,33 @@ struct BlackjackTableView: View {
.padding(.horizontal, Design.Spacing.small)
}
}
.overlay {
// Hint toast overlaying player hands (auto-dismisses)
if state.showHintToast, let hint = state.currentHint {
HintView(hint: hint)
.transition(.scale.combined(with: .opacity))
}
}
.onChange(of: state.currentHint) { oldHint, newHint in
// Show toast when a new hint appears
if let hint = newHint, hint != oldHint {
withAnimation(.spring(duration: Design.Animation.springDuration)) {
state.showHintToast = true
}
// Auto-dismiss after delay
Task { @MainActor in
try? await Task.sleep(for: Design.Toast.duration)
withAnimation(.spring(duration: Design.Animation.springDuration)) {
state.showHintToast = false
}
}
} else if newHint == nil {
// Hide immediately when no hint
withAnimation(.spring(duration: Design.Animation.springDuration)) {
state.showHintToast = false
}
}
}
.padding(.bottom, 5)
.transition(.opacity)
.debugBorder(showDebugBorders, color: .green, label: "Player")
@ -183,22 +179,12 @@ struct BlackjackTableView: View {
if let hint = state.bettingHint {
BettingHintView(hint: hint, trueCount: state.engine.trueCount)
.transition(.opacity)
.padding(.vertical, 10)
.debugBorder(showDebugBorders, color: .purple, label: "BetHint")
}
} else {
// Fixed-height hint area to prevent layout shifts during player turn
ZStack {
if let hint = state.currentHint {
HintView(hint: hint)
.transition(.opacity)
}
}
.frame(height: hintAreaHeight)
.debugBorder(showDebugBorders, color: .orange, label: "HintArea")
}
}
.padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.medium)
.debugBorder(showDebugBorders, color: .white, label: "TableView")
.animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase)
}

View File

@ -26,7 +26,7 @@ struct HintView: View {
.foregroundStyle(.yellow)
Text(String(localized: "Hint: \(hint)"))
.font(.system(size: fontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.strong))
.foregroundStyle(.white)
.lineLimit(1)
.minimumScaleFactor(Design.MinScaleFactor.comfortable)
}
@ -34,8 +34,13 @@ struct HintView: View {
.padding(.vertical, paddingV)
.background(
Capsule()
.fill(Color.black.opacity(Design.Opacity.light))
.fill(Color.black.opacity(Design.Opacity.heavy))
.overlay(
Capsule()
.strokeBorder(Color.yellow.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.thin)
)
)
.shadow(color: .black.opacity(Design.Opacity.medium), radius: Design.Shadow.radiusMedium)
.accessibilityElement(children: .ignore)
.accessibilityLabel(String(localized: "Hint"))
.accessibilityValue(hint)