Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
21e5d901c7
commit
71b27b671b
@ -56,6 +56,10 @@ final class GameState {
|
|||||||
/// Whether to show the gameplay hint toast.
|
/// Whether to show the gameplay hint toast.
|
||||||
var showHintToast: Bool = false
|
var showHintToast: Bool = false
|
||||||
|
|
||||||
|
/// Tracks the current hint display session to prevent race conditions.
|
||||||
|
/// When a new hint arrives or is shown, increment this so old dismiss tasks become stale.
|
||||||
|
var hintDisplayID: UUID = UUID()
|
||||||
|
|
||||||
/// Whether a reshuffle notification should be shown.
|
/// Whether a reshuffle notification should be shown.
|
||||||
var showReshuffleNotification: Bool = false
|
var showReshuffleNotification: Bool = false
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ enum Design {
|
|||||||
static let showDebugBorders = false
|
static let showDebugBorders = false
|
||||||
|
|
||||||
/// Set to true to show debug log statements
|
/// Set to true to show debug log statements
|
||||||
static let showDebugLogs = false
|
static let showDebugLogs = true
|
||||||
|
|
||||||
/// Debug logger - only prints when showDebugLogs is true
|
/// Debug logger - only prints when showDebugLogs is true
|
||||||
static func debugLog(_ message: String) {
|
static func debugLog(_ message: String) {
|
||||||
@ -84,7 +84,7 @@ enum Design {
|
|||||||
|
|
||||||
// Result banner
|
// Result banner
|
||||||
static let resultRowAmountWidth: CGFloat = 70
|
static let resultRowAmountWidth: CGFloat = 70
|
||||||
static let resultRowResultWidth: CGFloat = 150
|
static let resultRowResultWidth: CGFloat = 120
|
||||||
|
|
||||||
// Side bet zones
|
// Side bet zones
|
||||||
static let sideBetLabelFontSize: CGFloat = 13
|
static let sideBetLabelFontSize: CGFloat = 13
|
||||||
|
|||||||
@ -87,15 +87,22 @@ struct GameTableView: View {
|
|||||||
icon: "lightbulb.fill",
|
icon: "lightbulb.fill",
|
||||||
accessibilityLabel: String(localized: "Show Hint")
|
accessibilityLabel: String(localized: "Show Hint")
|
||||||
) {
|
) {
|
||||||
|
// Generate new ID to invalidate any pending dismiss tasks
|
||||||
|
let currentID = UUID()
|
||||||
|
state.hintDisplayID = currentID
|
||||||
|
|
||||||
// Show the toast with animation
|
// Show the toast with animation
|
||||||
withAnimation(.spring(duration: Design.Animation.springDuration)) {
|
withAnimation(.spring(duration: Design.Animation.springDuration)) {
|
||||||
state.showHintToast = true
|
state.showHintToast = true
|
||||||
}
|
}
|
||||||
// Auto-dismiss after delay (same as auto-show behavior)
|
// Auto-dismiss after delay, but only if this is still the active hint session
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
try? await Task.sleep(for: Design.Toast.duration)
|
try? await Task.sleep(for: Design.Toast.duration)
|
||||||
withAnimation(.spring(duration: Design.Animation.springDuration)) {
|
// Only dismiss if no newer hint has arrived
|
||||||
state.showHintToast = false
|
if state.hintDisplayID == currentID {
|
||||||
|
withAnimation(.spring(duration: Design.Animation.springDuration)) {
|
||||||
|
state.showHintToast = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ struct BlackjackTableView: View {
|
|||||||
|
|
||||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Computed from stable screen size
|
// MARK: - Computed from stable screen size
|
||||||
|
|
||||||
private var screenWidth: CGFloat { fullScreenSize.width }
|
private var screenWidth: CGFloat { fullScreenSize.width }
|
||||||
@ -152,18 +153,26 @@ struct BlackjackTableView: View {
|
|||||||
.onChange(of: state.currentHint) { oldHint, newHint in
|
.onChange(of: state.currentHint) { oldHint, newHint in
|
||||||
// Show toast when a new hint appears
|
// Show toast when a new hint appears
|
||||||
if let hint = newHint, hint != oldHint {
|
if let hint = newHint, hint != oldHint {
|
||||||
|
// Generate new ID to invalidate any pending dismiss tasks
|
||||||
|
let currentID = UUID()
|
||||||
|
state.hintDisplayID = currentID
|
||||||
|
|
||||||
withAnimation(.spring(duration: Design.Animation.springDuration)) {
|
withAnimation(.spring(duration: Design.Animation.springDuration)) {
|
||||||
state.showHintToast = true
|
state.showHintToast = true
|
||||||
}
|
}
|
||||||
// Auto-dismiss after delay
|
// Auto-dismiss after delay, but only if this is still the active hint session
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
try? await Task.sleep(for: Design.Toast.duration)
|
try? await Task.sleep(for: Design.Toast.duration)
|
||||||
withAnimation(.spring(duration: Design.Animation.springDuration)) {
|
// Only dismiss if no newer hint has arrived
|
||||||
state.showHintToast = false
|
if state.hintDisplayID == currentID {
|
||||||
|
withAnimation(.spring(duration: Design.Animation.springDuration)) {
|
||||||
|
state.showHintToast = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if newHint == nil {
|
} else if newHint == nil {
|
||||||
// Hide immediately when no hint
|
// Hide immediately when no hint
|
||||||
|
state.hintDisplayID = UUID() // Invalidate any pending dismiss tasks
|
||||||
withAnimation(.spring(duration: Design.Animation.springDuration)) {
|
withAnimation(.spring(duration: Design.Animation.springDuration)) {
|
||||||
state.showHintToast = false
|
state.showHintToast = false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,27 +16,32 @@ struct DealerHandView: View {
|
|||||||
let cardSpacing: CGFloat
|
let cardSpacing: CGFloat
|
||||||
|
|
||||||
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
|
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
|
||||||
|
@ScaledMetric(relativeTo: .headline) private var badgeHeight: CGFloat = CasinoDesign.Size.valueBadge
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
VStack(spacing: Design.Spacing.small) {
|
VStack(spacing: Design.Spacing.small) {
|
||||||
// Label and value
|
// Label and value - fixed height prevents vertical layout shift
|
||||||
HStack(spacing: Design.Spacing.small) {
|
HStack(spacing: Design.Spacing.small) {
|
||||||
Text(String(localized: "DEALER"))
|
Text(String(localized: "DEALER"))
|
||||||
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
// Show value: full value when hole card visible, otherwise just the face-up card's value
|
// Badge animates in when cards are dealt
|
||||||
if !hand.cards.isEmpty {
|
if !hand.cards.isEmpty {
|
||||||
if showHoleCard {
|
if showHoleCard {
|
||||||
// All cards visible - show total hand value
|
// All cards visible - show total hand value
|
||||||
ValueBadge(value: hand.value, color: Color.Hand.dealer)
|
ValueBadge(value: hand.value, color: Color.Hand.dealer)
|
||||||
|
.transition(.scale.combined(with: .opacity))
|
||||||
} else {
|
} else {
|
||||||
// Hole card hidden - show only the first (face-up) card's value
|
// Hole card hidden - show only the first (face-up) card's value
|
||||||
ValueBadge(value: hand.cards[0].blackjackValue, color: Color.Hand.dealer)
|
ValueBadge(value: hand.cards[0].blackjackValue, color: Color.Hand.dealer)
|
||||||
|
.transition(.scale.combined(with: .opacity))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(minHeight: badgeHeight) // Reserve consistent height
|
||||||
|
.animation(.spring(duration: Design.Animation.springDuration), value: hand.cards.isEmpty)
|
||||||
// Cards
|
// Cards
|
||||||
HStack(spacing: hand.cards.isEmpty ? Design.Spacing.small : cardSpacing) {
|
HStack(spacing: hand.cards.isEmpty ? Design.Spacing.small : cardSpacing) {
|
||||||
if hand.cards.isEmpty {
|
if hand.cards.isEmpty {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user