diff --git a/Baccarat/Views/MiniBaccaratTableView.swift b/Baccarat/Views/MiniBaccaratTableView.swift index d35da41..ca5ad6d 100644 --- a/Baccarat/Views/MiniBaccaratTableView.swift +++ b/Baccarat/Views/MiniBaccaratTableView.swift @@ -2,13 +2,13 @@ // MiniBaccaratTableView.swift // Baccarat // -// A realistic mini baccarat table layout for single player with side bets. +// A modern baccarat table layout with all betting options. // import SwiftUI import CasinoKit -/// The semi-circular mini baccarat betting table layout. +/// The baccarat betting table layout with main bets and side bets. struct MiniBaccaratTableView: View { @Bindable var gameState: GameState let selectedChip: ChipDenomination @@ -52,7 +52,7 @@ struct MiniBaccaratTableView: View { // MARK: - Body var body: some View { - VStack(spacing: Design.Spacing.xSmall) { + VStack(spacing: Design.Spacing.small) { // Table limits label Text(tableLimitsText) .font(.system(size: tableLimitsFontSize, weight: .bold, design: .rounded)) @@ -61,88 +61,79 @@ struct MiniBaccaratTableView: View { .lineLimit(1) .minimumScaleFactor(Design.MinScaleFactor.comfortable) - // Main table - ZStack { - // Table felt background with arc shape - TableFeltShape() - .fill( - LinearGradient( - colors: [Color.Table.feltLight, Color.Table.feltDark], - startPoint: .top, - endPoint: .bottom - ) - ) + // Main betting table + VStack(spacing: 0) { + // Top row: B PAIR | TIE | P PAIR + TopBettingRow( + bankerPairAmount: betAmount(for: .bankerPair), + tieAmount: betAmount(for: .tie), + playerPairAmount: betAmount(for: .playerPair), + canBetBankerPair: canAddBet(for: .bankerPair), + canBetTie: canAddBet(for: .tie), + canBetPlayerPair: canAddBet(for: .playerPair), + isBankerPairAtMax: isAtMax(for: .bankerPair), + isTieAtMax: isAtMax(for: .tie), + isPlayerPairAtMax: isAtMax(for: .playerPair), + onBankerPair: { gameState.placeBet(type: .bankerPair, amount: selectedChip.rawValue) }, + onTie: { gameState.placeBet(type: .tie, amount: selectedChip.rawValue) }, + onPlayerPair: { gameState.placeBet(type: .playerPair, amount: selectedChip.rawValue) } + ) - // Gold border - TableFeltShape() + // Divider + Rectangle() + .fill(Color.Border.gold.opacity(0.5)) + .frame(height: 1) + + // Middle row: BANKER | BONUS + MainBetRow( + title: "BANKER", + payoutText: "0.95 : 1", + mainBetAmount: betAmount(for: .banker), + bonusBetAmount: betAmount(for: .dragonBonusBanker), + isSelected: isBankerSelected, + canBetMain: canAddBet(for: .banker), + canBetBonus: canAddBet(for: .dragonBonusBanker), + isMainAtMax: isAtMax(for: .banker), + isBonusAtMax: isAtMax(for: .dragonBonusBanker), + mainColor: Color.BettingZone.bankerDark, + onMain: { gameState.placeBet(type: .banker, amount: selectedChip.rawValue) }, + onBonus: { gameState.placeBet(type: .dragonBonusBanker, amount: selectedChip.rawValue) } + ) + + // Divider + Rectangle() + .fill(Color.Border.gold.opacity(0.5)) + .frame(height: 1) + + // Bottom row: PLAYER | BONUS + MainBetRow( + title: "PLAYER", + payoutText: "1 : 1", + mainBetAmount: betAmount(for: .player), + bonusBetAmount: betAmount(for: .dragonBonusPlayer), + isSelected: isPlayerSelected, + canBetMain: canAddBet(for: .player), + canBetBonus: canAddBet(for: .dragonBonusPlayer), + isMainAtMax: isAtMax(for: .player), + isBonusAtMax: isAtMax(for: .dragonBonusPlayer), + mainColor: Color.BettingZone.playerDark, + onMain: { gameState.placeBet(type: .player, amount: selectedChip.rawValue) }, + onBonus: { gameState.placeBet(type: .dragonBonusPlayer, amount: selectedChip.rawValue) } + ) + } + .clipShape(RoundedRectangle(cornerRadius: Design.CornerRadius.large)) + .overlay( + RoundedRectangle(cornerRadius: Design.CornerRadius.large) .strokeBorder( LinearGradient( colors: [Color.Border.goldLight, Color.Border.goldDark], startPoint: .top, endPoint: .bottom ), - lineWidth: Design.LineWidth.heavy + lineWidth: Design.LineWidth.medium ) - - // Betting zones layout - VStack(spacing: Design.Spacing.xSmall) { - // Top row: B PAIR | TIE | P PAIR - TopBettingRow( - bankerPairAmount: betAmount(for: .bankerPair), - tieAmount: betAmount(for: .tie), - playerPairAmount: betAmount(for: .playerPair), - canBetBankerPair: canAddBet(for: .bankerPair), - canBetTie: canAddBet(for: .tie), - canBetPlayerPair: canAddBet(for: .playerPair), - isBankerPairAtMax: isAtMax(for: .bankerPair), - isTieAtMax: isAtMax(for: .tie), - isPlayerPairAtMax: isAtMax(for: .playerPair), - onBankerPair: { gameState.placeBet(type: .bankerPair, amount: selectedChip.rawValue) }, - onTie: { gameState.placeBet(type: .tie, amount: selectedChip.rawValue) }, - onPlayerPair: { gameState.placeBet(type: .playerPair, amount: selectedChip.rawValue) } - ) - .padding(.horizontal, Design.Spacing.medium) - .padding(.top, Design.Spacing.large) - - // Middle row: BANKER | DRAGON BONUS - MainBetRow( - title: "BANKER", - payoutText: "PAYS 0.95 TO 1", - mainBetAmount: betAmount(for: .banker), - bonusBetAmount: betAmount(for: .dragonBonusBanker), - isSelected: isBankerSelected, - canBetMain: canAddBet(for: .banker), - canBetBonus: canAddBet(for: .dragonBonusBanker), - isMainAtMax: isAtMax(for: .banker), - isBonusAtMax: isAtMax(for: .dragonBonusBanker), - mainColor: Color.BettingZone.bankerLight, - mainColorDark: Color.BettingZone.bankerDark, - onMain: { gameState.placeBet(type: .banker, amount: selectedChip.rawValue) }, - onBonus: { gameState.placeBet(type: .dragonBonusBanker, amount: selectedChip.rawValue) } - ) - .padding(.horizontal, Design.Spacing.medium) - - // Bottom row: PLAYER | DRAGON BONUS - MainBetRow( - title: "PLAYER", - payoutText: "PAYS 1 TO 1", - mainBetAmount: betAmount(for: .player), - bonusBetAmount: betAmount(for: .dragonBonusPlayer), - isSelected: isPlayerSelected, - canBetMain: canAddBet(for: .player), - canBetBonus: canAddBet(for: .dragonBonusPlayer), - isMainAtMax: isAtMax(for: .player), - isBonusAtMax: isAtMax(for: .dragonBonusPlayer), - mainColor: Color.BettingZone.playerLight, - mainColorDark: Color.BettingZone.playerDark, - onMain: { gameState.placeBet(type: .player, amount: selectedChip.rawValue) }, - onBonus: { gameState.placeBet(type: .dragonBonusPlayer, amount: selectedChip.rawValue) } - ) - .padding(.horizontal, Design.Spacing.medium) - .padding(.bottom, Design.Spacing.large) - } - } - .aspectRatio(Design.Size.tableAspectRatio, contentMode: .fit) + ) + .shadow(color: .black.opacity(0.3), radius: 10, y: 5) } } } @@ -163,21 +154,25 @@ private struct TopBettingRow: View { let onTie: () -> Void let onPlayerPair: () -> Void - private let rowHeight: CGFloat = 48 - private let cornerRadius = Design.CornerRadius.small + private let rowHeight: CGFloat = 52 var body: some View { - HStack(spacing: Design.Spacing.xSmall) { + HStack(spacing: 0) { // B PAIR PairBetZone( title: "B PAIR", betAmount: bankerPairAmount, isEnabled: canBetBankerPair, isAtMax: isBankerPairAtMax, - color: Color.BettingZone.bankerLight, + color: Color.BettingZone.bankerDark.opacity(0.6), action: onBankerPair ) + // Vertical divider + Rectangle() + .fill(Color.Border.gold.opacity(0.5)) + .frame(width: 1) + // TIE TieBetZone( betAmount: tieAmount, @@ -186,13 +181,18 @@ private struct TopBettingRow: View { action: onTie ) + // Vertical divider + Rectangle() + .fill(Color.Border.gold.opacity(0.5)) + .frame(width: 1) + // P PAIR PairBetZone( title: "P PAIR", betAmount: playerPairAmount, isEnabled: canBetPlayerPair, isAtMax: isPlayerPairAtMax, - color: Color.BettingZone.playerLight, + color: Color.BettingZone.playerDark.opacity(0.6), action: onPlayerPair ) } @@ -210,9 +210,8 @@ private struct PairBetZone: View { let color: Color let action: () -> Void - private let cornerRadius = Design.CornerRadius.small - private let titleFontSize: CGFloat = 11 - private let payoutFontSize: CGFloat = 9 + private let titleFontSize: CGFloat = 12 + private let payoutFontSize: CGFloat = 10 var body: some View { Button { @@ -220,33 +219,29 @@ private struct PairBetZone: View { } label: { ZStack { // Background - RoundedRectangle(cornerRadius: cornerRadius) - .fill(color.opacity(0.6)) - - // Border - RoundedRectangle(cornerRadius: cornerRadius) - .strokeBorder(isAtMax ? Color.Border.silver : Color.Border.gold, lineWidth: Design.LineWidth.thin) + Rectangle() + .fill(color) // Content - VStack(spacing: 1) { + VStack(spacing: 2) { Text(title) - .font(.system(size: titleFontSize, weight: .bold, design: .rounded)) + .font(.system(size: titleFontSize, weight: .heavy, design: .rounded)) .foregroundStyle(.yellow) - Text("11:1") - .font(.system(size: payoutFontSize, weight: .medium)) - .foregroundStyle(.white.opacity(0.8)) + Text("11 : 1") + .font(.system(size: payoutFontSize, weight: .medium, design: .rounded)) + .foregroundStyle(.white.opacity(0.7)) } // Chip indicator if betAmount > 0 { - SmallChipIndicator(amount: betAmount, isMax: isAtMax) - .offset(x: 0, y: 12) + ChipBadge(amount: betAmount, isMax: isAtMax) + .offset(y: 16) } } } .buttonStyle(.plain) - .opacity(isEnabled ? 1.0 : 0.6) + .opacity(isEnabled ? 1.0 : 0.5) .accessibilityElement(children: .ignore) .accessibilityLabel("\(title) bet, pays 11 to 1" + (betAmount > 0 ? ", current bet $\(betAmount)" : "")) .accessibilityAddTraits(.isButton) @@ -261,9 +256,8 @@ private struct TieBetZone: View { let isAtMax: Bool let action: () -> Void - private let cornerRadius = Design.CornerRadius.small - private let titleFontSize: CGFloat = 13 - private let payoutFontSize: CGFloat = 9 + private let titleFontSize: CGFloat = 14 + private let payoutFontSize: CGFloat = 10 var body: some View { Button { @@ -271,34 +265,30 @@ private struct TieBetZone: View { } label: { ZStack { // Background - RoundedRectangle(cornerRadius: cornerRadius) + Rectangle() .fill(isAtMax ? Color.BettingZone.tieMax : Color.BettingZone.tie) - // Border - RoundedRectangle(cornerRadius: cornerRadius) - .strokeBorder(isAtMax ? Color.Border.silver : Color.Border.gold, lineWidth: Design.LineWidth.thin) - // Content - VStack(spacing: 1) { + VStack(spacing: 2) { Text("TIE") .font(.system(size: titleFontSize, weight: .black, design: .rounded)) .tracking(1) - Text("8 TO 1") - .font(.system(size: payoutFontSize, weight: .medium)) - .opacity(0.8) + Text("8 : 1") + .font(.system(size: payoutFontSize, weight: .medium, design: .rounded)) + .opacity(0.7) } .foregroundStyle(.white) // Chip indicator if betAmount > 0 { - SmallChipIndicator(amount: betAmount, isMax: isAtMax) - .offset(x: 0, y: 12) + ChipBadge(amount: betAmount, isMax: isAtMax) + .offset(y: 16) } } } .buttonStyle(.plain) - .opacity(isEnabled ? 1.0 : 0.6) + .opacity(isEnabled ? 1.0 : 0.5) .accessibilityElement(children: .ignore) .accessibilityLabel("Tie bet, pays 8 to 1" + (betAmount > 0 ? ", current bet $\(betAmount)" : "")) .accessibilityAddTraits(.isButton) @@ -318,16 +308,14 @@ private struct MainBetRow: View { let isMainAtMax: Bool let isBonusAtMax: Bool let mainColor: Color - let mainColorDark: Color let onMain: () -> Void let onBonus: () -> Void - private let rowHeight: CGFloat = 55 - private let bonusWidth: CGFloat = 70 - private let cornerRadius = Design.CornerRadius.medium + private let rowHeight: CGFloat = 65 + private let bonusWidth: CGFloat = 80 var body: some View { - HStack(spacing: Design.Spacing.xSmall) { + HStack(spacing: 0) { // Main bet zone (BANKER or PLAYER) MainBetZone( title: title, @@ -336,11 +324,15 @@ private struct MainBetRow: View { isSelected: isSelected, isEnabled: canBetMain, isAtMax: isMainAtMax, - colorLight: mainColor, - colorDark: mainColorDark, + color: mainColor, action: onMain ) + // Vertical divider + Rectangle() + .fill(Color.Border.gold.opacity(0.5)) + .frame(width: 1) + // Dragon Bonus zone DragonBonusZone( betAmount: bonusBetAmount, @@ -363,68 +355,59 @@ private struct MainBetZone: View { let isSelected: Bool let isEnabled: Bool let isAtMax: Bool - let colorLight: Color - let colorDark: Color + let color: Color let action: () -> Void - private let cornerRadius = Design.CornerRadius.medium - private let titleFontSize: CGFloat = 18 - private let payoutFontSize: CGFloat = 9 + private let titleFontSize: CGFloat = 20 + private let payoutFontSize: CGFloat = 11 var body: some View { Button { if isEnabled { action() } } label: { ZStack { - // Background gradient - RoundedRectangle(cornerRadius: cornerRadius) - .fill( - LinearGradient( - colors: isAtMax ? [colorLight.opacity(0.5), colorDark.opacity(0.5)] : [colorLight, colorDark], - startPoint: .top, - endPoint: .bottom - ) - ) + // Background + Rectangle() + .fill(color) - // Selection glow + // Selection highlight if isSelected { - RoundedRectangle(cornerRadius: cornerRadius) - .strokeBorder(Color.yellow, lineWidth: Design.LineWidth.thick) - .shadow(color: .yellow.opacity(0.5), radius: 6) + Rectangle() + .fill(Color.yellow.opacity(0.15)) + + Rectangle() + .strokeBorder(Color.yellow, lineWidth: 3) } - // Border - RoundedRectangle(cornerRadius: cornerRadius) - .strokeBorder(isAtMax ? Color.Border.silver : Color.Border.gold, lineWidth: Design.LineWidth.medium) - // Content - VStack(spacing: 2) { - Text(title) - .font(.system(size: titleFontSize, weight: .black, design: .rounded)) - .tracking(2) + HStack { + Spacer() - Text(payoutText) - .font(.system(size: payoutFontSize, weight: .medium)) - .opacity(0.8) - } - .foregroundStyle(.white) - .lineLimit(1) - .minimumScaleFactor(0.7) - - // Chip indicator - if betAmount > 0 { - HStack { - Spacer() + VStack(spacing: 3) { + Text(title) + .font(.system(size: titleFontSize, weight: .black, design: .rounded)) + .tracking(2) + + Text(payoutText) + .font(.system(size: payoutFontSize, weight: .semibold, design: .rounded)) + .opacity(0.7) + } + .foregroundStyle(.white) + + Spacer() + + // Chip indicator + if betAmount > 0 { ChipOnTableView(amount: betAmount, showMax: isAtMax) - .padding(.trailing, Design.Spacing.small) + .padding(.trailing, Design.Spacing.medium) } } } } .buttonStyle(.plain) - .opacity(isEnabled ? 1.0 : 0.6) + .opacity(isEnabled ? 1.0 : 0.5) .accessibilityElement(children: .ignore) - .accessibilityLabel("\(title) bet, \(payoutText)" + (isSelected ? ", selected" : "") + (betAmount > 0 ? ", current bet $\(betAmount)" : "")) + .accessibilityLabel("\(title) bet, pays \(payoutText)" + (isSelected ? ", selected" : "") + (betAmount > 0 ? ", current bet $\(betAmount)" : "")) .accessibilityAddTraits(.isButton) } } @@ -437,48 +420,58 @@ private struct DragonBonusZone: View { let isAtMax: Bool let action: () -> Void - private let cornerRadius = Design.CornerRadius.medium - private let titleFontSize: CGFloat = 10 - private let diamondSize: CGFloat = 20 + private let titleFontSize: CGFloat = 9 + private let diamondSize: CGFloat = 24 var body: some View { Button { if isEnabled { action() } } label: { ZStack { - // Background - RoundedRectangle(cornerRadius: cornerRadius) - .fill(Color.purple.opacity(0.5)) - - // Border - RoundedRectangle(cornerRadius: cornerRadius) - .strokeBorder(isAtMax ? Color.Border.silver : Color.Border.gold, lineWidth: Design.LineWidth.thin) + // Background - darker purple/blue tint + Rectangle() + .fill( + LinearGradient( + colors: [ + Color(red: 0.25, green: 0.3, blue: 0.45), + Color(red: 0.15, green: 0.2, blue: 0.35) + ], + startPoint: .top, + endPoint: .bottom + ) + ) // Content - VStack(spacing: 4) { + VStack(spacing: 5) { // Diamond shape DiamondShape() - .fill(Color.purple.opacity(0.8)) + .fill( + LinearGradient( + colors: [Color.purple.opacity(0.8), Color.purple.opacity(0.5)], + startPoint: .top, + endPoint: .bottom + ) + ) .frame(width: diamondSize, height: diamondSize) .overlay( DiamondShape() - .strokeBorder(Color.white.opacity(0.5), lineWidth: 1) + .strokeBorder(Color.white.opacity(0.4), lineWidth: 1) ) Text("BONUS") .font(.system(size: titleFontSize, weight: .bold, design: .rounded)) - .foregroundStyle(.white) + .foregroundStyle(.white.opacity(0.9)) } // Chip indicator if betAmount > 0 { - SmallChipIndicator(amount: betAmount, isMax: isAtMax) - .offset(y: 18) + ChipBadge(amount: betAmount, isMax: isAtMax) + .offset(y: 22) } } } .buttonStyle(.plain) - .opacity(isEnabled ? 1.0 : 0.6) + .opacity(isEnabled ? 1.0 : 0.5) .accessibilityElement(children: .ignore) .accessibilityLabel("Dragon Bonus, pays up to 30 to 1" + (betAmount > 0 ? ", current bet $\(betAmount)" : "")) .accessibilityAddTraits(.isButton) @@ -510,9 +503,9 @@ private struct DiamondShape: InsettableShape { } } -// MARK: - Small Chip Indicator +// MARK: - Chip Badge (small indicator) -private struct SmallChipIndicator: View { +private struct ChipBadge: View { let amount: Int let isMax: Bool @@ -520,77 +513,29 @@ private struct SmallChipIndicator: View { ZStack { Circle() .fill(isMax ? Color.gray : Color.yellow) - .frame(width: 18, height: 18) + .frame(width: 20, height: 20) Circle() - .strokeBorder(Color.white, lineWidth: 1) - .frame(width: 18, height: 18) + .strokeBorder(Color.white.opacity(0.8), lineWidth: 1) + .frame(width: 20, height: 20) if isMax { Text("M") - .font(.system(size: 8, weight: .bold)) + .font(.system(size: 9, weight: .bold)) .foregroundStyle(.white) } else { - Text(amount.formatted(.number.notation(.compactName))) - .font(.system(size: 6, weight: .bold)) + Text(formatCompact(amount)) + .font(.system(size: 7, weight: .bold)) .foregroundStyle(.black) } } } -} - -// MARK: - Table Felt Shape - -struct TableFeltShape: InsettableShape { - var insetAmount: CGFloat = 0 - private let shapeCornerRadius = Design.CornerRadius.xxLarge - - func path(in rect: CGRect) -> Path { - var path = Path() - - let insetRect = rect.insetBy(dx: insetAmount, dy: insetAmount) - let height = insetRect.height - let cornerRadius = shapeCornerRadius - - // Start from bottom left - path.move(to: CGPoint(x: insetRect.minX + cornerRadius, y: insetRect.maxY)) - - // Bottom edge - path.addLine(to: CGPoint(x: insetRect.maxX - cornerRadius, y: insetRect.maxY)) - - // Bottom right corner - path.addQuadCurve( - to: CGPoint(x: insetRect.maxX, y: insetRect.maxY - cornerRadius), - control: CGPoint(x: insetRect.maxX, y: insetRect.maxY) - ) - - // Right edge going up with curve - path.addLine(to: CGPoint(x: insetRect.maxX, y: insetRect.minY + height * 0.3)) - - // Top arc - path.addQuadCurve( - to: CGPoint(x: insetRect.minX, y: insetRect.minY + height * 0.3), - control: CGPoint(x: insetRect.midX, y: insetRect.minY - height * 0.1) - ) - - // Left edge going down - path.addLine(to: CGPoint(x: insetRect.minX, y: insetRect.maxY - cornerRadius)) - - // Bottom left corner - path.addQuadCurve( - to: CGPoint(x: insetRect.minX + cornerRadius, y: insetRect.maxY), - control: CGPoint(x: insetRect.minX, y: insetRect.maxY) - ) - - path.closeSubpath() - return path - } - - func inset(by amount: CGFloat) -> some InsettableShape { - var shape = self - shape.insetAmount += amount - return shape + private func formatCompact(_ value: Int) -> String { + if value >= 1000 { + return "\(value / 1000)K" + } + return "\(value)" } }