Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
ec484d8574
commit
740ef6a73b
@ -14,6 +14,10 @@ import CasinoKit
|
|||||||
/// Design constants for the Blackjack app.
|
/// Design constants for the Blackjack app.
|
||||||
/// Shared constants are imported from CasinoDesign; game-specific values are defined here.
|
/// Shared constants are imported from CasinoDesign; game-specific values are defined here.
|
||||||
enum Design {
|
enum Design {
|
||||||
|
// MARK: - Debug
|
||||||
|
|
||||||
|
/// Set to true to show layout debug borders on views
|
||||||
|
static let showDebugBorders = false
|
||||||
|
|
||||||
// MARK: - Shared Constants (from CasinoKit)
|
// MARK: - Shared Constants (from CasinoKit)
|
||||||
|
|
||||||
@ -39,12 +43,13 @@ enum Design {
|
|||||||
static let cardWidthSmall: CGFloat = CasinoDesign.Size.cardWidthSmall
|
static let cardWidthSmall: CGFloat = CasinoDesign.Size.cardWidthSmall
|
||||||
static let cardOverlap: CGFloat = CasinoDesign.Size.cardOverlap * handScale // Scaled overlap
|
static let cardOverlap: CGFloat = CasinoDesign.Size.cardOverlap * handScale // Scaled overlap
|
||||||
|
|
||||||
// Player hands container height (accommodates larger cards)
|
// Player hands container height (accommodates larger cards + labels)
|
||||||
static let playerHandsHeight: CGFloat = 180 * handScale // 270pt at 1.5x
|
// Reduced from 180 to fit content more snugly
|
||||||
|
static let playerHandsHeight: CGFloat = 160 * handScale // 240pt at 1.5x
|
||||||
|
|
||||||
// Hand label font sizes (scaled)
|
// Hand label font sizes (scaled)
|
||||||
static let handLabelFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * handScale
|
static let handLabelFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * handScale
|
||||||
static let handNumberFontSize: CGFloat = CasinoDesign.BaseFontSize.small * handScale
|
static let handNumberFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * handScale // Same as label
|
||||||
static let handValueFontSize: CGFloat = CasinoDesign.BaseFontSize.xLarge * handScale
|
static let handValueFontSize: CGFloat = CasinoDesign.BaseFontSize.xLarge * handScale
|
||||||
|
|
||||||
// Hint font size (scaled to match hands)
|
// Hint font size (scaled to match hands)
|
||||||
|
|||||||
@ -15,33 +15,29 @@ struct ActionButtonsView: View {
|
|||||||
@ScaledMetric(relativeTo: .headline) private var buttonFontSize: CGFloat = Design.BaseFontSize.large
|
@ScaledMetric(relativeTo: .headline) private var buttonFontSize: CGFloat = Design.BaseFontSize.large
|
||||||
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.IconSize.large
|
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.IconSize.large
|
||||||
|
|
||||||
// Fixed height to prevent layout shifts
|
// Scaled container height - base 60pt, scales with accessibility
|
||||||
private let containerHeight: CGFloat = 120
|
@ScaledMetric(relativeTo: .body) private var containerHeight: CGFloat = 60
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
VStack(spacing: Design.Spacing.small) {
|
||||||
Color.clear
|
// Primary actions
|
||||||
.frame(height: containerHeight)
|
HStack(spacing: Design.Spacing.medium) {
|
||||||
|
switch state.currentPhase {
|
||||||
VStack(spacing: Design.Spacing.medium) {
|
case .betting:
|
||||||
// Primary actions
|
bettingButtons
|
||||||
HStack(spacing: Design.Spacing.medium) {
|
case .playerTurn:
|
||||||
switch state.currentPhase {
|
playerTurnButtons
|
||||||
case .betting:
|
case .roundComplete:
|
||||||
bettingButtons
|
// Empty - handled by result banner
|
||||||
case .playerTurn:
|
EmptyView()
|
||||||
playerTurnButtons
|
default:
|
||||||
case .roundComplete:
|
// Dealing, dealer turn - show nothing
|
||||||
// Empty - handled by result banner
|
EmptyView()
|
||||||
EmptyView()
|
|
||||||
default:
|
|
||||||
// Dealing, dealer turn - show nothing
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.animation(.spring(duration: Design.Animation.quick), value: state.currentPhase)
|
|
||||||
}
|
}
|
||||||
|
.animation(.spring(duration: Design.Animation.quick), value: state.currentPhase)
|
||||||
}
|
}
|
||||||
|
.frame(minHeight: containerHeight)
|
||||||
.padding(.horizontal, Design.Spacing.large)
|
.padding(.horizontal, Design.Spacing.large)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -65,6 +65,9 @@ struct GameTableView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use global debug flag from Design constants
|
||||||
|
private var showDebugBorders: Bool { Design.showDebugBorders }
|
||||||
|
|
||||||
// MARK: - Main Game View
|
// MARK: - Main Game View
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@ -85,6 +88,7 @@ struct GameTableView: View {
|
|||||||
onStats: { showStats = true }
|
onStats: { showStats = true }
|
||||||
)
|
)
|
||||||
.frame(maxWidth: maxContentWidth)
|
.frame(maxWidth: maxContentWidth)
|
||||||
|
.debugBorder(showDebugBorders, color: .cyan, label: "TopBar")
|
||||||
|
|
||||||
// Card count display (when enabled)
|
// Card count display (when enabled)
|
||||||
if settings.showCardCount {
|
if settings.showCardCount {
|
||||||
@ -93,6 +97,7 @@ struct GameTableView: View {
|
|||||||
trueCount: state.engine.trueCount
|
trueCount: state.engine.trueCount
|
||||||
)
|
)
|
||||||
.frame(maxWidth: maxContentWidth)
|
.frame(maxWidth: maxContentWidth)
|
||||||
|
.debugBorder(showDebugBorders, color: .mint, label: "CardCount")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reshuffle notification
|
// Reshuffle notification
|
||||||
@ -102,17 +107,18 @@ struct GameTableView: View {
|
|||||||
.transition(.move(edge: .top).combined(with: .opacity))
|
.transition(.move(edge: .top).combined(with: .opacity))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table layout
|
// Table layout - fills available space
|
||||||
BlackjackTableView(
|
BlackjackTableView(
|
||||||
state: state,
|
state: state,
|
||||||
onPlaceBet: { placeBet(state: state) }
|
onPlaceBet: { placeBet(state: state) }
|
||||||
)
|
)
|
||||||
.frame(maxWidth: maxContentWidth)
|
.frame(maxWidth: maxContentWidth)
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// Chip selector - only shown during betting phase
|
// Chip selector - only shown during betting phase
|
||||||
if state.currentPhase == .betting {
|
if state.currentPhase == .betting {
|
||||||
|
Spacer()
|
||||||
|
.debugBorder(showDebugBorders, color: .yellow, label: "ChipSpacer")
|
||||||
|
|
||||||
ChipSelectorView(
|
ChipSelectorView(
|
||||||
selectedChip: $selectedChip,
|
selectedChip: $selectedChip,
|
||||||
balance: state.balance,
|
balance: state.balance,
|
||||||
@ -120,14 +126,15 @@ struct GameTableView: View {
|
|||||||
maxBet: state.settings.maxBet
|
maxBet: state.settings.maxBet
|
||||||
)
|
)
|
||||||
.frame(maxWidth: maxContentWidth)
|
.frame(maxWidth: maxContentWidth)
|
||||||
.padding(.bottom, Design.Spacing.small)
|
|
||||||
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
||||||
|
.debugBorder(showDebugBorders, color: .pink, label: "ChipSelector")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action buttons
|
// Action buttons - minimal spacing during player turn
|
||||||
ActionButtonsView(state: state)
|
ActionButtonsView(state: state)
|
||||||
.frame(maxWidth: maxContentWidth)
|
.frame(maxWidth: maxContentWidth)
|
||||||
.padding(.bottom, Design.Spacing.medium)
|
.padding(.bottom, Design.Spacing.small)
|
||||||
|
.debugBorder(showDebugBorders, color: .blue, label: "ActionBtns")
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
|
|||||||
@ -26,8 +26,14 @@ struct BlackjackTableView: View {
|
|||||||
private let cardWidth: CGFloat = Design.Size.cardWidth
|
private let cardWidth: CGFloat = Design.Size.cardWidth
|
||||||
private let cardSpacing: CGFloat = Design.Size.cardOverlap
|
private let cardSpacing: CGFloat = Design.Size.cardOverlap
|
||||||
|
|
||||||
|
/// 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 }
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: Design.Spacing.large) {
|
VStack(spacing: Design.Spacing.small) {
|
||||||
// Dealer area
|
// Dealer area
|
||||||
DealerHandView(
|
DealerHandView(
|
||||||
hand: state.dealerHand,
|
hand: state.dealerHand,
|
||||||
@ -36,8 +42,11 @@ struct BlackjackTableView: View {
|
|||||||
cardWidth: cardWidth,
|
cardWidth: cardWidth,
|
||||||
cardSpacing: cardSpacing
|
cardSpacing: cardSpacing
|
||||||
)
|
)
|
||||||
|
.debugBorder(showDebugBorders, color: .red, label: "Dealer")
|
||||||
|
|
||||||
Spacer()
|
// Flexible space between dealer and player (minimum 60pt)
|
||||||
|
Spacer(minLength: 60)
|
||||||
|
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer")
|
||||||
|
|
||||||
// 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 {
|
||||||
@ -50,10 +59,14 @@ struct BlackjackTableView: View {
|
|||||||
cardSpacing: cardSpacing
|
cardSpacing: cardSpacing
|
||||||
)
|
)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
|
.debugBorder(showDebugBorders, color: .green, label: "Player")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Betting zone (when betting)
|
// Betting zone (when betting)
|
||||||
if state.currentPhase == .betting {
|
if state.currentPhase == .betting {
|
||||||
|
Spacer()
|
||||||
|
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer2")
|
||||||
|
|
||||||
BettingZoneView(
|
BettingZoneView(
|
||||||
betAmount: state.currentBet,
|
betAmount: state.currentBet,
|
||||||
minBet: state.settings.minBet,
|
minBet: state.settings.minBet,
|
||||||
@ -61,22 +74,29 @@ struct BlackjackTableView: View {
|
|||||||
onTap: onPlaceBet
|
onTap: onPlaceBet
|
||||||
)
|
)
|
||||||
.transition(.scale.combined(with: .opacity))
|
.transition(.scale.combined(with: .opacity))
|
||||||
|
.debugBorder(showDebugBorders, color: .blue, label: "BetZone")
|
||||||
|
|
||||||
// Betting hint based on count (only when card counting enabled)
|
// Betting hint based on count (only when card counting enabled)
|
||||||
if showCardCount, let bettingHint = bettingHint {
|
if showCardCount, let bettingHint = bettingHint {
|
||||||
BettingHintView(hint: bettingHint, trueCount: state.engine.trueCount)
|
BettingHintView(hint: bettingHint, trueCount: state.engine.trueCount)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
|
.debugBorder(showDebugBorders, color: .purple, label: "BetHint")
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
// Fixed-height hint area to prevent layout shifts during player turn
|
||||||
// Hint (when enabled and player turn)
|
ZStack {
|
||||||
if state.settings.showHints && isPlayerTurn, let hint = currentHint {
|
if state.settings.showHints && isPlayerTurn, let hint = currentHint {
|
||||||
HintView(hint: hint)
|
HintView(hint: hint)
|
||||||
.transition(.opacity)
|
.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)
|
.padding(.vertical, Design.Spacing.medium)
|
||||||
|
.debugBorder(showDebugBorders, color: .white, label: "TableView")
|
||||||
.animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase)
|
.animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,11 @@ struct PlayerHandsView: View {
|
|||||||
let cardWidth: CGFloat
|
let cardWidth: CGFloat
|
||||||
let cardSpacing: CGFloat
|
let cardSpacing: CGFloat
|
||||||
|
|
||||||
|
/// Total card count across all hands - used to trigger scroll when hitting
|
||||||
|
private var totalCardCount: Int {
|
||||||
|
hands.reduce(0) { $0 + $1.cards.count }
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
@ -40,24 +45,35 @@ struct PlayerHandsView: View {
|
|||||||
.id(index)
|
.id(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.large)
|
||||||
.frame(minWidth: geometry.size.width)
|
.frame(minWidth: geometry.size.width)
|
||||||
}
|
}
|
||||||
.scrollClipDisabled()
|
.scrollClipDisabled()
|
||||||
|
.scrollBounceBehavior(.basedOnSize)
|
||||||
.onChange(of: activeHandIndex) { _, newIndex in
|
.onChange(of: activeHandIndex) { _, newIndex in
|
||||||
withAnimation(.easeInOut(duration: Design.Animation.quick)) {
|
scrollToHand(proxy: proxy, index: newIndex)
|
||||||
proxy.scrollTo(newIndex, anchor: .center)
|
}
|
||||||
}
|
.onChange(of: totalCardCount) { _, _ in
|
||||||
|
// Scroll to active hand when cards are added (hit)
|
||||||
|
scrollToHand(proxy: proxy, index: activeHandIndex)
|
||||||
|
}
|
||||||
|
.onChange(of: hands.count) { _, _ in
|
||||||
|
// Scroll to active hand when split occurs
|
||||||
|
scrollToHand(proxy: proxy, index: activeHandIndex)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if hands.count > 1 {
|
scrollToHand(proxy: proxy, index: activeHandIndex)
|
||||||
proxy.scrollTo(activeHandIndex, anchor: .center)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(height: Design.Size.playerHandsHeight)
|
.frame(height: Design.Size.playerHandsHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func scrollToHand(proxy: ScrollViewProxy, index: Int) {
|
||||||
|
withAnimation(.easeInOut(duration: Design.Animation.quick)) {
|
||||||
|
proxy.scrollTo(index, anchor: .center)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Single Player Hand
|
// MARK: - Single Player Hand
|
||||||
|
|||||||
@ -76,3 +76,6 @@
|
|||||||
// - CloudSyncManager
|
// - CloudSyncManager
|
||||||
// - PersistableGameData (protocol)
|
// - PersistableGameData (protocol)
|
||||||
|
|
||||||
|
// MARK: - Debug
|
||||||
|
// - debugBorder(_:color:label:) View modifier
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,39 @@
|
|||||||
|
//
|
||||||
|
// DebugBorderModifier.swift
|
||||||
|
// CasinoKit
|
||||||
|
//
|
||||||
|
// Debug view modifier for visualizing layout boundaries.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// MARK: - Debug Border Modifier
|
||||||
|
|
||||||
|
public extension View {
|
||||||
|
/// Adds a colored border and label to a view for debugging layout.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - show: Whether to show the debug border.
|
||||||
|
/// - color: The color of the border and label.
|
||||||
|
/// - label: The label text to display in the corner.
|
||||||
|
/// - Returns: The view with an optional debug border overlay.
|
||||||
|
@ViewBuilder
|
||||||
|
func debugBorder(_ show: Bool, color: Color, label: String) -> some View {
|
||||||
|
if show {
|
||||||
|
self
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.xSmall)
|
||||||
|
.strokeBorder(color, lineWidth: CasinoDesign.LineWidth.thin)
|
||||||
|
)
|
||||||
|
.overlay(alignment: .topLeading) {
|
||||||
|
Text(label)
|
||||||
|
.font(.system(size: 8, weight: .bold))
|
||||||
|
.foregroundStyle(color)
|
||||||
|
.padding(2)
|
||||||
|
.background(Color.black.opacity(CasinoDesign.Opacity.strong))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user