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.
|
/// Whether to show side bet toast notifications.
|
||||||
var showSideBetToasts: Bool = false
|
var showSideBetToasts: Bool = false
|
||||||
|
|
||||||
|
/// Whether to show the gameplay hint toast.
|
||||||
|
var showHintToast: Bool = false
|
||||||
|
|
||||||
/// Whether a reshuffle notification should be shown.
|
/// Whether a reshuffle notification should be shown.
|
||||||
var showReshuffleNotification: Bool = false
|
var showReshuffleNotification: Bool = false
|
||||||
|
|
||||||
@ -787,7 +790,7 @@ final class GameState {
|
|||||||
|
|
||||||
// Auto-hide toasts after delay
|
// Auto-hide toasts after delay
|
||||||
Task {
|
Task {
|
||||||
try? await Task.sleep(for: .seconds(3))
|
try? await Task.sleep(for: Design.Toast.duration)
|
||||||
showSideBetToasts = false
|
showSideBetToasts = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5230,6 +5230,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Reset Game" : {
|
||||||
|
"comment" : "Button to reset game balance and reshuffle cards.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Reset Game"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Reiniciar Juego"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Réinitialiser le Jeu"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Reset to Defaults" : {
|
"Reset to Defaults" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -5252,6 +5275,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Restore starting balance and reshuffle" : {
|
||||||
|
"comment" : "Subtitle for Reset Game button explaining what it does.",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Restore starting balance and reshuffle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Restaurar saldo inicial y barajar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Restaurer le solde initial et remélanger"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Roulette" : {
|
"Roulette" : {
|
||||||
"comment" : "The name of a roulette card.",
|
"comment" : "The name of a roulette card.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@ -5434,52 +5480,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Reset Game": {
|
|
||||||
"comment": "Button to reset game balance and reshuffle cards.",
|
|
||||||
"localizations": {
|
|
||||||
"en": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Reset Game"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es-MX": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Reiniciar Juego"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr-CA": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Réinitialiser le Jeu"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Restore starting balance and reshuffle": {
|
|
||||||
"comment": "Subtitle for Reset Game button explaining what it does.",
|
|
||||||
"localizations": {
|
|
||||||
"en": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Restore starting balance and reshuffle"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es-MX": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Restaurar saldo inicial y barajar"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr-CA": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Restaurer le solde initial et remélanger"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Running Count: Sum of all card values seen." : {
|
"Running Count: Sum of all card values seen." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -5684,6 +5684,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Show Hint" : {
|
||||||
|
"comment" : "Label for a toolbar button that shows a hint.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Show Hints" : {
|
"Show Hints" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@ -109,6 +109,13 @@ enum Design {
|
|||||||
/// Bounce for side bet toast animations
|
/// Bounce for side bet toast animations
|
||||||
static let toastBounce: Double = 0.4
|
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
|
// MARK: - Blackjack App Colors
|
||||||
|
|||||||
@ -71,6 +71,31 @@ struct GameTableView: View {
|
|||||||
// Use global debug flag from Design constants
|
// Use global debug flag from Design constants
|
||||||
private var showDebugBorders: Bool { Design.showDebugBorders }
|
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
|
// MARK: - Main Game View
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@ -97,6 +122,7 @@ struct GameTableView: View {
|
|||||||
balance: state.balance,
|
balance: state.balance,
|
||||||
secondaryInfo: settings.showCardsRemaining ? "\(state.engine.cardsRemaining)" : nil,
|
secondaryInfo: settings.showCardsRemaining ? "\(state.engine.cardsRemaining)" : nil,
|
||||||
secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil,
|
secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil,
|
||||||
|
leadingButtons: hintToolbarButtons(for: state),
|
||||||
onSettings: { showSettings = true },
|
onSettings: { showSettings = true },
|
||||||
onHelp: { showRules = true },
|
onHelp: { showRules = true },
|
||||||
onStats: { showStats = true }
|
onStats: { showStats = true }
|
||||||
@ -143,7 +169,7 @@ struct GameTableView: View {
|
|||||||
.padding(.bottom, Design.Spacing.small)
|
.padding(.bottom, Design.Spacing.small)
|
||||||
.debugBorder(showDebugBorders, color: .blue, label: "ActionBtns")
|
.debugBorder(showDebugBorders, color: .blue, label: "ActionBtns")
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
.zIndex(1)
|
.zIndex(1)
|
||||||
.onChange(of: state.currentPhase) { oldPhase, newPhase in
|
.onChange(of: state.currentPhase) { oldPhase, newPhase in
|
||||||
Design.debugLog("🔄 Phase changed: \(oldPhase) → \(newPhase)")
|
Design.debugLog("🔄 Phase changed: \(oldPhase) → \(newPhase)")
|
||||||
@ -208,6 +234,7 @@ struct GameTableView: View {
|
|||||||
.allowsHitTesting(true)
|
.allowsHitTesting(true)
|
||||||
.zIndex(100)
|
.zIndex(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
.onChange(of: state.playerHands.count) { oldCount, newCount in
|
.onChange(of: state.playerHands.count) { oldCount, newCount in
|
||||||
Design.debugLog("👥 Player hands count: \(oldCount) → \(newCount)")
|
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: .headline) private var labelFontSize: CGFloat = Design.BaseFontSize.medium
|
||||||
@ScaledMetric(relativeTo: .title) private var valueFontSize: CGFloat = Design.BaseFontSize.xLarge
|
@ScaledMetric(relativeTo: .title) private var valueFontSize: CGFloat = Design.BaseFontSize.xLarge
|
||||||
@ScaledMetric(relativeTo: .caption) private var hintFontSize: CGFloat = Design.BaseFontSize.small
|
|
||||||
|
|
||||||
// MARK: - Dynamic Card Sizing
|
// MARK: - Dynamic Card Sizing
|
||||||
|
|
||||||
@ -48,7 +47,7 @@ struct BlackjackTableView: View {
|
|||||||
/// Card width based on full screen height (stable - doesn't change with content)
|
/// Card width based on full screen height (stable - doesn't change with content)
|
||||||
private var cardWidth: CGFloat {
|
private var cardWidth: CGFloat {
|
||||||
let maxDimension = screenHeight
|
let maxDimension = screenHeight
|
||||||
let percentage: CGFloat = 0.15 // ~10% of screen
|
let percentage: CGFloat = 0.18 // ~10% of screen
|
||||||
return maxDimension * percentage
|
return maxDimension * percentage
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,32 +57,12 @@ struct BlackjackTableView: View {
|
|||||||
cardWidth * -0.55
|
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
|
// Use global debug flag from Design constants
|
||||||
private var showDebugBorders: Bool { Design.showDebugBorders }
|
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 {
|
var body: some View {
|
||||||
VStack(spacing: Design.Spacing.small) {
|
VStack(spacing: 0) {
|
||||||
// Dealer area
|
// Dealer area - pushed to top
|
||||||
DealerHandView(
|
DealerHandView(
|
||||||
hand: state.dealerHand,
|
hand: state.dealerHand,
|
||||||
showHoleCard: state.shouldShowDealerHoleCard,
|
showHoleCard: state.shouldShowDealerHoleCard,
|
||||||
@ -93,33 +72,23 @@ struct BlackjackTableView: View {
|
|||||||
)
|
)
|
||||||
.debugBorder(showDebugBorders, color: .red, label: "Dealer")
|
.debugBorder(showDebugBorders, color: .red, label: "Dealer")
|
||||||
|
|
||||||
// Space between dealer and player - contains card count if enabled
|
// Top spacer pushes dealer up
|
||||||
if showCardCount {
|
|
||||||
// Card count view fills the space between hands
|
|
||||||
VStack {
|
|
||||||
// Top spacer for larger screens
|
|
||||||
if screenHeight > 700 {
|
|
||||||
Spacer(minLength: Design.Spacing.small)
|
Spacer(minLength: Design.Spacing.small)
|
||||||
}
|
.debugBorder(showDebugBorders, color: .yellow, label: "TopSpacer")
|
||||||
|
|
||||||
|
// Card count view centered between dealer and player
|
||||||
|
if showCardCount {
|
||||||
CardCountView(
|
CardCountView(
|
||||||
runningCount: state.engine.runningCount,
|
runningCount: state.engine.runningCount,
|
||||||
trueCount: state.engine.trueCount
|
trueCount: state.engine.trueCount
|
||||||
)
|
)
|
||||||
|
|
||||||
// Bottom spacer for larger screens
|
|
||||||
if screenHeight > 700 {
|
|
||||||
Spacer(minLength: Design.Spacing.small)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(minHeight: dealerPlayerSpacing)
|
|
||||||
.debugBorder(showDebugBorders, color: .mint, label: "CardCount")
|
.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
|
// Player hands area - only show when there are cards dealt
|
||||||
if state.playerHands.first?.cards.isEmpty == false {
|
if state.playerHands.first?.cards.isEmpty == false {
|
||||||
ZStack {
|
ZStack {
|
||||||
@ -162,6 +131,33 @@ struct BlackjackTableView: View {
|
|||||||
.padding(.horizontal, Design.Spacing.small)
|
.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)
|
.padding(.bottom, 5)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
.debugBorder(showDebugBorders, color: .green, label: "Player")
|
.debugBorder(showDebugBorders, color: .green, label: "Player")
|
||||||
@ -183,22 +179,12 @@ struct BlackjackTableView: View {
|
|||||||
if let hint = state.bettingHint {
|
if let hint = state.bettingHint {
|
||||||
BettingHintView(hint: hint, trueCount: state.engine.trueCount)
|
BettingHintView(hint: hint, trueCount: state.engine.trueCount)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
|
.padding(.vertical, 10)
|
||||||
.debugBorder(showDebugBorders, color: .purple, label: "BetHint")
|
.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(.horizontal, Design.Spacing.large)
|
||||||
.padding(.vertical, Design.Spacing.medium)
|
|
||||||
.debugBorder(showDebugBorders, color: .white, label: "TableView")
|
.debugBorder(showDebugBorders, color: .white, label: "TableView")
|
||||||
.animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase)
|
.animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ struct HintView: View {
|
|||||||
.foregroundStyle(.yellow)
|
.foregroundStyle(.yellow)
|
||||||
Text(String(localized: "Hint: \(hint)"))
|
Text(String(localized: "Hint: \(hint)"))
|
||||||
.font(.system(size: fontSize, weight: .medium))
|
.font(.system(size: fontSize, weight: .medium))
|
||||||
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
.foregroundStyle(.white)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.minimumScaleFactor(Design.MinScaleFactor.comfortable)
|
.minimumScaleFactor(Design.MinScaleFactor.comfortable)
|
||||||
}
|
}
|
||||||
@ -34,8 +34,13 @@ struct HintView: View {
|
|||||||
.padding(.vertical, paddingV)
|
.padding(.vertical, paddingV)
|
||||||
.background(
|
.background(
|
||||||
Capsule()
|
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)
|
.accessibilityElement(children: .ignore)
|
||||||
.accessibilityLabel(String(localized: "Hint"))
|
.accessibilityLabel(String(localized: "Hint"))
|
||||||
.accessibilityValue(hint)
|
.accessibilityValue(hint)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user