Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
1dc64ebf69
commit
2cd2946a80
@ -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
@ -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
|
||||
|
||||
@ -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)")
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user