Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
3021951390
commit
2b15e57c33
@ -11,9 +11,11 @@ import CasinoKit
|
||||
struct BaccaratApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
SherpaContainerView(configuration: .default) {
|
||||
AppLaunchView(config: .baccarat) {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
189
Baccarat/Baccarat/Models/WalkthroughTags.swift
Normal file
189
Baccarat/Baccarat/Models/WalkthroughTags.swift
Normal file
@ -0,0 +1,189 @@
|
||||
//
|
||||
// WalkthroughTags.swift
|
||||
// Baccarat
|
||||
//
|
||||
// Defines the walkthrough steps for onboarding new players.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CasinoKit
|
||||
|
||||
/// Walkthrough steps for Baccarat onboarding.
|
||||
enum BaccaratWalkthroughTags: SherpaTags {
|
||||
// MARK: - Top Bar Elements
|
||||
|
||||
/// Shows the current balance
|
||||
case balance
|
||||
|
||||
/// Shows remaining cards in the shoe
|
||||
case cardsRemaining
|
||||
|
||||
/// Opens statistics sheet
|
||||
case statsButton
|
||||
|
||||
/// Opens rules/help sheet
|
||||
case rulesButton
|
||||
|
||||
/// Opens settings sheet
|
||||
case settingsButton
|
||||
|
||||
/// Shows the road map history display
|
||||
case history
|
||||
|
||||
// MARK: - Gameplay Elements
|
||||
|
||||
/// Introduce the betting zones (Player, Banker, Tie)
|
||||
case bettingZone
|
||||
|
||||
/// Explain the betting hints showing patterns and tips
|
||||
case bettingHint
|
||||
|
||||
/// Explain the chip selector for choosing bet amounts
|
||||
case chipSelector
|
||||
|
||||
/// Show the deal button to start the round
|
||||
case dealButton
|
||||
|
||||
func makeCallout() -> Callout {
|
||||
switch self {
|
||||
// Top Bar
|
||||
case .balance:
|
||||
return .custom(edge: .bottom) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "dollarsign.circle.fill",
|
||||
text: String(localized: "walkthrough.balance"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
case .cardsRemaining:
|
||||
return .custom(edge: .bottom) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "rectangle.portrait.on.rectangle.portrait.fill",
|
||||
text: String(localized: "walkthrough.cardsRemaining"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
case .statsButton:
|
||||
return .custom(edge: .bottom) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "chart.bar.fill",
|
||||
text: String(localized: "walkthrough.statsButton"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
case .rulesButton:
|
||||
return .custom(edge: .bottom) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "info.circle",
|
||||
text: String(localized: "walkthrough.rulesButton"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
case .settingsButton:
|
||||
return .custom(edge: .bottom) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "gearshape.fill",
|
||||
text: String(localized: "walkthrough.settingsButton"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
case .history:
|
||||
return .custom(edge: .bottom) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "clock.fill",
|
||||
text: String(localized: "walkthrough.history"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
// Gameplay
|
||||
case .bettingZone:
|
||||
return .custom(edge: .top) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "hand.tap.fill",
|
||||
text: String(localized: "walkthrough.bettingZone"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
case .bettingHint:
|
||||
return .custom(edge: .top) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "lightbulb.fill",
|
||||
text: String(localized: "walkthrough.bettingHint"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
case .chipSelector:
|
||||
return .custom(edge: .top) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "circle.grid.2x2.fill",
|
||||
text: String(localized: "walkthrough.chipSelector"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
case .dealButton:
|
||||
return .custom(edge: .top) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "play.fill",
|
||||
text: String(localized: "walkthrough.dealButton"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Custom Callout View
|
||||
|
||||
/// Fully custom callout view with dark background and white text
|
||||
struct WalkthroughCalloutView: View {
|
||||
let icon: String
|
||||
let text: String
|
||||
let onTap: () -> Void
|
||||
|
||||
// Maximum width to prevent edge clipping
|
||||
private let maxCalloutWidth: CGFloat = 320
|
||||
|
||||
var body: some View {
|
||||
Button(action: onTap) {
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: Design.BaseFontSize.large, weight: .semibold))
|
||||
.foregroundStyle(.yellow)
|
||||
|
||||
Text(text)
|
||||
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
|
||||
.foregroundStyle(.white)
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(2)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
Text(String(localized: "walkthrough.close"))
|
||||
.font(.system(size: Design.BaseFontSize.small, weight: .semibold))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
}
|
||||
.padding(Design.Spacing.medium)
|
||||
.frame(maxWidth: maxCalloutWidth)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
||||
.fill(Color.black.opacity(Design.Opacity.almostFull))
|
||||
.shadow(
|
||||
color: .black.opacity(Design.Opacity.medium),
|
||||
radius: Design.Shadow.radiusLarge,
|
||||
y: Design.Shadow.offsetMedium
|
||||
)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.horizontal, Design.Spacing.xLarge)
|
||||
}
|
||||
}
|
||||
@ -1983,6 +1983,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Game Sessions" : {
|
||||
"comment" : "Title for the Game Sessions help page",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Game Sessions"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sesiones de Juego"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sessions de Jeu"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"GAME STATS" : {
|
||||
"comment" : "Section in the statistics sheet dedicated to displaying statistics specific to baccarat.",
|
||||
"localizations" : {
|
||||
@ -3620,6 +3643,7 @@
|
||||
},
|
||||
"Results appear here, then in the road maps below" : {
|
||||
"comment" : "Instructional text for new players.",
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@ -3736,6 +3760,7 @@
|
||||
},
|
||||
"Select a chip and tap a bet zone" : {
|
||||
"comment" : "Onboarding hint for placing bets.",
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@ -3803,6 +3828,144 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessions.autoStart" : {
|
||||
"comment" : "Game Sessions help - auto start explanation",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "A new session starts automatically when you begin playing."
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Una nueva sesión comienza automáticamente cuando empiezas a jugar."
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Une nouvelle session démarre automatiquement lorsque vous commencez à jouer."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessions.endSession" : {
|
||||
"comment" : "Game Sessions help - end session explanation",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "End your session anytime from the Statistics screen."
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Termina tu sesión en cualquier momento desde la pantalla de Estadísticas."
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Terminez votre session à tout moment depuis l'écran Statistiques."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessions.history" : {
|
||||
"comment" : "Game Sessions help - history explanation",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "View your complete session history to see past performance."
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Consulta tu historial completo de sesiones para ver tu rendimiento pasado."
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Consultez votre historique complet de sessions pour voir vos performances passées."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessions.likeRealCasino" : {
|
||||
"comment" : "Game Sessions help - like real casino",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Track your play just like at a real casino table!"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "¡Rastrea tu juego como en una mesa de casino real!"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Suivez votre jeu comme à une vraie table de casino !"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessions.trackProgress" : {
|
||||
"comment" : "Game Sessions help - track progress",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sessions track your playing time, hands, wins, losses, and chip balance."
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Las sesiones rastrean tu tiempo de juego, manos, victorias, derrotas y saldo de fichas."
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Les sessions suivent votre temps de jeu, mains, victoires, défaites et solde de jetons."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessions.viewStats" : {
|
||||
"comment" : "Game Sessions help - view stats",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Tap the chart icon to view your current session stats."
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Toca el ícono de gráfico para ver las estadísticas de tu sesión actual."
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Appuyez sur l'icône de graphique pour voir les statistiques de votre session actuelle."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Set a budget and stick to it." : {
|
||||
"comment" : "Tip for players to set a budget and stick to it when playing baccarat.",
|
||||
"localizations" : {
|
||||
@ -4238,6 +4401,7 @@
|
||||
},
|
||||
"Tap Deal to start the round" : {
|
||||
"comment" : "Instructional text for new players.",
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@ -4787,6 +4951,259 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.balance" : {
|
||||
"comment" : "Walkthrough hint for the balance display",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Your current balance is shown here"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Tu saldo actual se muestra aquí"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Votre solde actuel est affiché ici"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.bettingHint" : {
|
||||
"comment" : "Walkthrough hint for the betting hint display",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Tips based on patterns and trends"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Consejos según patrones y tendencias"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Conseils selon les tendances"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.bettingZone" : {
|
||||
"comment" : "Walkthrough hint for the betting zone",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Select a chip and tap a bet zone"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Selecciona una ficha y toca una zona de apuesta"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sélectionnez un jeton et touchez une zone de mise"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.cardsRemaining" : {
|
||||
"comment" : "Walkthrough hint for cards remaining display",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cards remaining in the shoe"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cartas restantes en el zapato"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cartes restantes dans le sabot"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.chipSelector" : {
|
||||
"comment" : "Walkthrough hint for the chip selector",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Choose a chip value to bet"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Elige el valor de la ficha para apostar"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Choisissez une valeur de jeton pour miser"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.close" : {
|
||||
"comment" : "Walkthrough button to close/advance to next step",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Close"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cerrar"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Fermer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.dealButton" : {
|
||||
"comment" : "Walkthrough hint for the deal button",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Tap Deal to start the round"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Toca Repartir para comenzar la ronda"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Touchez Distribuer pour commencer la manche"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.history" : {
|
||||
"comment" : "Walkthrough hint for the road map history display",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Track game results and spot trends"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Rastrea resultados y detecta tendencias"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Suivez les résultats et repérez les tendances"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.rulesButton" : {
|
||||
"comment" : "Walkthrough hint for the rules/help button",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Learn the rules and how to play"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Aprende las reglas y cómo jugar"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Apprenez les règles et comment jouer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.settingsButton" : {
|
||||
"comment" : "Walkthrough hint for the settings button",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Customize game rules and options"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Personaliza las reglas y opciones del juego"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Personnalisez les règles et options du jeu"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.statsButton" : {
|
||||
"comment" : "Walkthrough hint for the statistics button",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "View session stats and history"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Ver estadísticas e historial"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Voir statistiques et historique"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"WIN" : {
|
||||
"comment" : "The text that appears as a badge when a player wins a hand in baccarat.",
|
||||
"localizations" : {
|
||||
|
||||
@ -61,6 +61,7 @@ struct ActionButtonsView: View {
|
||||
onClear: onClear,
|
||||
onDeal: onDeal
|
||||
)
|
||||
.sherpaTag(BaccaratWalkthroughTags.dealButton)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
@ -9,7 +9,7 @@ import SwiftUI
|
||||
import CasinoKit
|
||||
|
||||
/// The main game table view containing all game elements.
|
||||
struct GameTableView: View {
|
||||
struct GameTableView: View, SherpaDelegate {
|
||||
@State private var settings = GameSettings()
|
||||
@State private var gameState: GameState?
|
||||
@State private var selectedChip: ChipDenomination = .hundred
|
||||
@ -18,10 +18,10 @@ struct GameTableView: View {
|
||||
@State private var showStats = false
|
||||
@State private var showWelcome = false
|
||||
|
||||
// MARK: - Onboarding State
|
||||
// MARK: - Walkthrough State
|
||||
|
||||
/// Tooltip manager for contextual hints
|
||||
@State private var tooltipManager: TooltipManager?
|
||||
/// Whether the Sherpa walkthrough is active
|
||||
@State private var isWalkthroughActive = false
|
||||
|
||||
/// Screen size for card sizing (measured from TableBackgroundView)
|
||||
@State private var screenSize: CGSize = CGSize(width: 375, height: 667)
|
||||
@ -85,6 +85,17 @@ struct GameTableView: View {
|
||||
// Use global debug flag from Design constants
|
||||
private var showDebugBorders: Bool { Design.showDebugBorders }
|
||||
|
||||
/// Sherpa tags for the top bar elements
|
||||
private var topBarSherpaTags: TopBarSherpaTags<BaccaratWalkthroughTags> {
|
||||
TopBarSherpaTags(
|
||||
balance: .balance,
|
||||
cardsRemaining: .cardsRemaining,
|
||||
stats: .statsButton,
|
||||
rules: .rulesButton,
|
||||
settings: .settingsButton
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
@ -109,9 +120,6 @@ struct GameTableView: View {
|
||||
if gameState == nil {
|
||||
gameState = GameState(settings: settings)
|
||||
}
|
||||
if tooltipManager == nil {
|
||||
tooltipManager = TooltipManager(onboarding: state.onboarding)
|
||||
}
|
||||
checkForWelcomeSheet()
|
||||
}
|
||||
.sheet(isPresented: $showSettings) {
|
||||
@ -161,30 +169,17 @@ struct GameTableView: View {
|
||||
],
|
||||
onboarding: state.onboarding,
|
||||
onDismiss: { showWelcome = false },
|
||||
onShowHints: checkOnboardingHints
|
||||
onShowHints: startWalkthrough
|
||||
)
|
||||
}
|
||||
.onChange(of: showWelcome) { wasShowing, isShowing in
|
||||
// Handle swipe-down dismissal: treat as "Start Playing" (no tooltips)
|
||||
// Handle swipe-down dismissal: treat as "Start Playing" (no walkthrough)
|
||||
if wasShowing && !isShowing && !state.onboarding.hasCompletedWelcome {
|
||||
state.onboarding.skipOnboarding()
|
||||
}
|
||||
}
|
||||
.onChange(of: state.totalBetAmount) { _, newTotal in
|
||||
if newTotal > 0, state.onboarding.shouldShowHint("dealButton") {
|
||||
showDealHintWithDelay()
|
||||
}
|
||||
}
|
||||
.onChange(of: state.currentPhase) { oldPhase, newPhase in
|
||||
if newPhase == .showingResult, oldPhase != newPhase {
|
||||
if state.onboarding.shouldShowHint("firstResult") {
|
||||
showResultHintWithDelay()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic tooltip display
|
||||
.dynamicTooltip(tooltipManager ?? TooltipManager(onboarding: state.onboarding))
|
||||
// Sherpa walkthrough modifier
|
||||
.sherpa(isActive: isWalkthroughActive, tags: BaccaratWalkthroughTags.self, delegate: self)
|
||||
}
|
||||
|
||||
// MARK: - Onboarding Helpers
|
||||
@ -198,36 +193,29 @@ struct GameTableView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func checkOnboardingHints() {
|
||||
if state.onboarding.shouldShowHint("bettingZone") {
|
||||
tooltipManager?.show(
|
||||
key: "bettingZone",
|
||||
message: String(localized: "Select a chip and tap a bet zone"),
|
||||
icon: "hand.tap.fill",
|
||||
position: .bottom,
|
||||
delay: 1.0
|
||||
)
|
||||
}
|
||||
/// Starts the Sherpa walkthrough when user taps "Show Me How"
|
||||
private func startWalkthrough() {
|
||||
// Reset onboarding hints so walkthrough can be seen again
|
||||
state.onboarding.reset()
|
||||
state.onboarding.completeWelcome()
|
||||
isWalkthroughActive = true
|
||||
}
|
||||
|
||||
private func showDealHintWithDelay() {
|
||||
tooltipManager?.show(
|
||||
key: "dealButton",
|
||||
message: String(localized: "Tap Deal to start the round"),
|
||||
icon: "play.fill",
|
||||
position: .bottom,
|
||||
delay: 0.5
|
||||
)
|
||||
// MARK: - SherpaDelegate
|
||||
|
||||
/// Returns nil to hide skip button and progress indicator
|
||||
func accessoryView(sherpa: Sherpa) -> AnyView? {
|
||||
nil
|
||||
}
|
||||
|
||||
private func showResultHintWithDelay() {
|
||||
tooltipManager?.show(
|
||||
key: "firstResult",
|
||||
message: String(localized: "Results appear here, then in the road maps below"),
|
||||
icon: "chart.bar.fill",
|
||||
position: .bottom,
|
||||
delay: 2.0
|
||||
)
|
||||
func onWalkthroughComplete(sherpa: Sherpa) {
|
||||
isWalkthroughActive = false
|
||||
state.onboarding.completeWelcome()
|
||||
}
|
||||
|
||||
func onWalkthroughSkipped(sherpa: Sherpa, atStep: Int, totalSteps: Int) {
|
||||
isWalkthroughActive = false
|
||||
state.onboarding.completeWelcome()
|
||||
}
|
||||
|
||||
// MARK: - Private Views
|
||||
@ -256,7 +244,8 @@ struct GameTableView: View {
|
||||
secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil,
|
||||
onSettings: { showSettings = true },
|
||||
onHelp: { showRules = true },
|
||||
onStats: { showStats = true }
|
||||
onStats: { showStats = true },
|
||||
sherpaTags: topBarSherpaTags
|
||||
)
|
||||
.debugBorder(showDebugBorders, color: .cyan, label: "TopBar")
|
||||
|
||||
@ -288,6 +277,7 @@ struct GameTableView: View {
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.sherpaTag(BaccaratWalkthroughTags.history)
|
||||
.frame(width: 240)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
||||
@ -341,6 +331,7 @@ struct GameTableView: View {
|
||||
secondaryInfo: hintInfo.secondaryText,
|
||||
style: hintInfo.style
|
||||
)
|
||||
.sherpaTag(BaccaratWalkthroughTags.bettingHint)
|
||||
.transition(.opacity)
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
.debugBorder(showDebugBorders, color: .purple, label: "Hint")
|
||||
@ -356,6 +347,7 @@ struct GameTableView: View {
|
||||
currentBet: state.minBetForChipSelector,
|
||||
maxBet: state.maxBet
|
||||
)
|
||||
.sherpaTag(BaccaratWalkthroughTags.chipSelector)
|
||||
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
||||
.debugBorder(showDebugBorders, color: .pink, label: "ChipSelector")
|
||||
}
|
||||
@ -399,7 +391,8 @@ struct GameTableView: View {
|
||||
secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil,
|
||||
onSettings: { showSettings = true },
|
||||
onHelp: { showRules = true },
|
||||
onStats: { showStats = true }
|
||||
onStats: { showStats = true },
|
||||
sherpaTags: topBarSherpaTags
|
||||
)
|
||||
.debugBorder(showDebugBorders, color: .cyan, label: "TopBar")
|
||||
|
||||
@ -430,6 +423,7 @@ struct GameTableView: View {
|
||||
// Road map history - show in portrait before deal on larger screens
|
||||
if settings.showHistory && !isSmallScreen && !isDealing {
|
||||
RoadMapView(results: state.recentResults)
|
||||
.sherpaTag(BaccaratWalkthroughTags.history)
|
||||
.frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
.debugBorder(showDebugBorders, color: .orange, label: "RoadMap")
|
||||
@ -454,6 +448,7 @@ struct GameTableView: View {
|
||||
secondaryInfo: hintInfo.secondaryText,
|
||||
style: hintInfo.style
|
||||
)
|
||||
.sherpaTag(BaccaratWalkthroughTags.bettingHint)
|
||||
.transition(.opacity)
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
.debugBorder(showDebugBorders, color: .purple, label: "Hint")
|
||||
@ -467,6 +462,7 @@ struct GameTableView: View {
|
||||
currentBet: state.minBetForChipSelector,
|
||||
maxBet: state.maxBet
|
||||
)
|
||||
.sherpaTag(BaccaratWalkthroughTags.chipSelector)
|
||||
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
||||
.debugBorder(showDebugBorders, color: .pink, label: "ChipSelector")
|
||||
}
|
||||
|
||||
@ -23,6 +23,18 @@ struct RulesHelpView: View {
|
||||
String(localized: "Baccarat has one of the lowest house edges in the casino.")
|
||||
]
|
||||
),
|
||||
RulePage(
|
||||
title: String(localized: "Game Sessions"),
|
||||
icon: "clock.badge.checkmark.fill",
|
||||
content: [
|
||||
String(localized: "sessions.trackProgress"),
|
||||
String(localized: "sessions.autoStart"),
|
||||
String(localized: "sessions.viewStats"),
|
||||
String(localized: "sessions.endSession"),
|
||||
String(localized: "sessions.history"),
|
||||
String(localized: "sessions.likeRealCasino")
|
||||
]
|
||||
),
|
||||
RulePage(
|
||||
title: String(localized: "Card Values"),
|
||||
icon: "suit.spade.fill",
|
||||
|
||||
@ -163,6 +163,7 @@ struct BettingTableView: View {
|
||||
)
|
||||
)
|
||||
.shadow(color: .black.opacity(Design.Opacity.light), radius: Design.Shadow.radiusLarge, y: Design.Shadow.offsetLarge)
|
||||
.sherpaTag(BaccaratWalkthroughTags.bettingZone)
|
||||
}
|
||||
.debugBorder(showDebugBorders, color: .orange, label: "BettingTable")
|
||||
}
|
||||
|
||||
@ -10,8 +10,8 @@ A feature-rich Baccarat (Punto Banco) app for iOS built with SwiftUI. Experience
|
||||
|
||||
### 🎓 First-Time User Experience
|
||||
- **Welcome Sheet** — Interactive introduction on first launch
|
||||
- **Tutorial Mode** — Optional guided walkthrough of your first round
|
||||
- **Contextual Hints** — Tips appear at the right moment during gameplay
|
||||
- **Spotlight Walkthrough** — Guided tour with spotlight focus on key UI elements
|
||||
- **Sherpa Framework** — Modern walkthrough with progress indicators and haptic feedback
|
||||
- **Never Intrusive** — All onboarding is skippable and shown only once
|
||||
|
||||
### 🎰 Authentic Punto Banco Gameplay
|
||||
|
||||
@ -18,9 +18,11 @@ struct BlackjackApp: App {
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
SherpaContainerView(configuration: .default) {
|
||||
AppLaunchView(config: .blackjack) {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
189
Blackjack/Blackjack/Models/WalkthroughTags.swift
Normal file
189
Blackjack/Blackjack/Models/WalkthroughTags.swift
Normal file
@ -0,0 +1,189 @@
|
||||
//
|
||||
// WalkthroughTags.swift
|
||||
// Blackjack
|
||||
//
|
||||
// Defines the walkthrough steps for onboarding new players.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CasinoKit
|
||||
|
||||
/// Walkthrough steps for Blackjack onboarding.
|
||||
enum BlackjackWalkthroughTags: SherpaTags {
|
||||
// MARK: - Top Bar Elements
|
||||
|
||||
/// Shows the current balance
|
||||
case balance
|
||||
|
||||
/// Shows remaining cards in the shoe
|
||||
case cardsRemaining
|
||||
|
||||
/// Opens statistics sheet
|
||||
case statsButton
|
||||
|
||||
/// Opens rules/help sheet
|
||||
case rulesButton
|
||||
|
||||
/// Opens settings sheet
|
||||
case settingsButton
|
||||
|
||||
// MARK: - Gameplay Elements
|
||||
|
||||
/// Introduce the betting zone where chips are placed
|
||||
case bettingZone
|
||||
|
||||
/// Explain the betting hints based on card count
|
||||
case bettingHint
|
||||
|
||||
/// Explain the chip selector for choosing bet amounts
|
||||
case chipSelector
|
||||
|
||||
/// Show the deal button to start the round
|
||||
case dealButton
|
||||
|
||||
/// Explain player actions during the hand
|
||||
case playerActions
|
||||
|
||||
func makeCallout() -> Callout {
|
||||
switch self {
|
||||
// Top Bar
|
||||
case .balance:
|
||||
return .custom(edge: .bottom) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "dollarsign.circle.fill",
|
||||
text: String(localized: "walkthrough.balance"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
case .cardsRemaining:
|
||||
return .custom(edge: .bottom) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "rectangle.portrait.on.rectangle.portrait.fill",
|
||||
text: String(localized: "walkthrough.cardsRemaining"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
case .statsButton:
|
||||
return .custom(edge: .bottom) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "chart.bar.fill",
|
||||
text: String(localized: "walkthrough.statsButton"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
case .rulesButton:
|
||||
return .custom(edge: .bottom) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "info.circle",
|
||||
text: String(localized: "walkthrough.rulesButton"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
case .settingsButton:
|
||||
return .custom(edge: .bottom) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "gearshape.fill",
|
||||
text: String(localized: "walkthrough.settingsButton"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
// Gameplay
|
||||
case .bettingZone:
|
||||
return .custom(edge: .top) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "hand.tap.fill",
|
||||
text: String(localized: "walkthrough.bettingZone"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
case .bettingHint:
|
||||
return .custom(edge: .top) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "lightbulb.fill",
|
||||
text: String(localized: "walkthrough.bettingHint"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
case .chipSelector:
|
||||
return .custom(edge: .top) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "circle.grid.2x2.fill",
|
||||
text: String(localized: "walkthrough.chipSelector"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
case .dealButton:
|
||||
return .custom(edge: .top) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "play.fill",
|
||||
text: String(localized: "walkthrough.dealButton"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
|
||||
case .playerActions:
|
||||
return .custom(edge: .top) { onTap in
|
||||
WalkthroughCalloutView(
|
||||
icon: "hand.point.up.left.fill",
|
||||
text: String(localized: "walkthrough.playerActions"),
|
||||
onTap: onTap
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Custom Callout View
|
||||
|
||||
/// Fully custom callout view with dark background and white text
|
||||
struct WalkthroughCalloutView: View {
|
||||
let icon: String
|
||||
let text: String
|
||||
let onTap: () -> Void
|
||||
|
||||
// Maximum width to prevent edge clipping
|
||||
private let maxCalloutWidth: CGFloat = 320
|
||||
|
||||
var body: some View {
|
||||
Button(action: onTap) {
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: Design.BaseFontSize.large, weight: .semibold))
|
||||
.foregroundStyle(.yellow)
|
||||
|
||||
Text(text)
|
||||
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
|
||||
.foregroundStyle(.white)
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(2)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
Text(String(localized: "walkthrough.close"))
|
||||
.font(.system(size: Design.BaseFontSize.small, weight: .semibold))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
}
|
||||
.padding(Design.Spacing.medium)
|
||||
.frame(maxWidth: maxCalloutWidth)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
||||
.fill(Color.black.opacity(Design.Opacity.almostFull))
|
||||
.shadow(
|
||||
color: .black.opacity(Design.Opacity.medium),
|
||||
radius: Design.Shadow.radiusLarge,
|
||||
y: Design.Shadow.offsetMedium
|
||||
)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.horizontal, Design.Spacing.xLarge)
|
||||
}
|
||||
}
|
||||
@ -2010,9 +2010,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Choose your action based on the hint above" : {
|
||||
|
||||
},
|
||||
"Clear" : {
|
||||
"localizations" : {
|
||||
@ -3552,6 +3549,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Game Sessions" : {
|
||||
"comment" : "Title for the Game Sessions help page",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Game Sessions"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sesiones de Juego"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sessions de Jeu"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"GAME STYLE" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -6034,6 +6054,7 @@
|
||||
},
|
||||
"Select a chip and tap the bet area" : {
|
||||
"comment" : "Onboarding hint for placing bets.",
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@ -6124,6 +6145,144 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessions.autoStart" : {
|
||||
"comment" : "Game Sessions help - auto start explanation",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "A new session starts automatically when you begin playing."
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Una nueva sesión comienza automáticamente cuando empiezas a jugar."
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Une nouvelle session démarre automatiquement lorsque vous commencez à jouer."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessions.endSession" : {
|
||||
"comment" : "Game Sessions help - end session explanation",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "End your session anytime from the Statistics screen."
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Termina tu sesión en cualquier momento desde la pantalla de Estadísticas."
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Terminez votre session à tout moment depuis l'écran Statistiques."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessions.history" : {
|
||||
"comment" : "Game Sessions help - history explanation",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "View your complete session history to see past performance."
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Consulta tu historial completo de sesiones para ver tu rendimiento pasado."
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Consultez votre historique complet de sessions pour voir vos performances passées."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessions.likeRealCasino" : {
|
||||
"comment" : "Game Sessions help - like real casino",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Track your play just like at a real casino table!"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "¡Rastrea tu juego como en una mesa de casino real!"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Suivez votre jeu comme à une vraie table de casino !"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessions.trackProgress" : {
|
||||
"comment" : "Game Sessions help - track progress",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sessions track your playing time, hands, wins, losses, and chip balance."
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Las sesiones rastrean tu tiempo de juego, manos, victorias, derrotas y saldo de fichas."
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Les sessions suivent votre temps de jeu, mains, victoires, défaites et solde de jetons."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessions.viewStats" : {
|
||||
"comment" : "Game Sessions help - view stats",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Tap the chart icon to view your current session stats."
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Toca el ícono de gráfico para ver las estadísticas de tu sesión actual."
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Appuyez sur l'icône de graphique pour voir les statistiques de votre session actuelle."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Settings" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -7191,9 +7350,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Tap Deal to start the round" : {
|
||||
|
||||
},
|
||||
"TAP TO BET" : {
|
||||
"localizations" : {
|
||||
@ -7608,6 +7764,259 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.balance" : {
|
||||
"comment" : "Walkthrough hint for the balance display",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Your current balance is shown here"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Tu saldo actual se muestra aquí"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Votre solde actuel est affiché ici"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.bettingHint" : {
|
||||
"comment" : "Walkthrough hint for the betting hint display",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Betting tips based on card count"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Consejos de apuesta según el conteo"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Conseils de mise selon le comptage"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.bettingZone" : {
|
||||
"comment" : "Walkthrough hint for the betting zone",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Select a chip and tap the bet area"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Selecciona una ficha y toca el área de apuesta"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sélectionnez un jeton et touchez la zone de mise"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.cardsRemaining" : {
|
||||
"comment" : "Walkthrough hint for cards remaining display",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cards remaining in the shoe"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cartas restantes en el zapato"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cartes restantes dans le sabot"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.close" : {
|
||||
"comment" : "Walkthrough button to close/advance to next step",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Close"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cerrar"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Fermer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.chipSelector" : {
|
||||
"comment" : "Walkthrough hint for the chip selector",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Choose a chip value to bet"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Elige el valor de la ficha para apostar"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Choisissez une valeur de jeton pour miser"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.dealButton" : {
|
||||
"comment" : "Walkthrough hint for the deal button",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Tap Deal to start the round"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Toca Repartir para comenzar la ronda"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Touchez Distribuer pour commencer la manche"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.playerActions" : {
|
||||
"comment" : "Walkthrough hint for player action buttons",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Choose your action based on the hint above"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Elige tu acción según la sugerencia de arriba"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Choisissez votre action selon l'indice ci-dessus"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.rulesButton" : {
|
||||
"comment" : "Walkthrough hint for the rules/help button",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Learn the rules and how to play"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Aprende las reglas y cómo jugar"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Apprenez les règles et comment jouer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.settingsButton" : {
|
||||
"comment" : "Walkthrough hint for the settings button",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Customize game rules and options"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Personaliza las reglas y opciones del juego"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Personnalisez les règles et options du jeu"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"walkthrough.statsButton" : {
|
||||
"comment" : "Walkthrough hint for the statistics button",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "View session stats and history"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Ver estadísticas e historial"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Voir statistiques et historique"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Win Rate" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
|
||||
@ -50,6 +50,7 @@ struct ActionButtonsView: View {
|
||||
onClear: { state.clearBet() },
|
||||
onDeal: { Task { await state.deal() } }
|
||||
)
|
||||
.sherpaTag(BlackjackWalkthroughTags.dealButton)
|
||||
}
|
||||
|
||||
// MARK: - Player Turn Buttons
|
||||
@ -83,6 +84,7 @@ struct ActionButtonsView: View {
|
||||
.transition(.scale.combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
.sherpaTag(BlackjackWalkthroughTags.playerActions)
|
||||
.onAppear {
|
||||
animatedActions = availableActions
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
import SwiftUI
|
||||
import CasinoKit
|
||||
|
||||
struct GameTableView: View {
|
||||
struct GameTableView: View, SherpaDelegate {
|
||||
@State private var settings = GameSettings()
|
||||
@State private var gameState: GameState?
|
||||
@State private var selectedChip: ChipDenomination = .twentyFive
|
||||
@ -20,10 +20,10 @@ struct GameTableView: View {
|
||||
@State private var showStats = false
|
||||
@State private var showWelcome = false
|
||||
|
||||
// MARK: - Onboarding State
|
||||
// MARK: - Walkthrough State
|
||||
|
||||
/// Tooltip manager for contextual hints
|
||||
@State private var tooltipManager: TooltipManager?
|
||||
/// Whether the Sherpa walkthrough is active
|
||||
@State private var isWalkthroughActive = false
|
||||
|
||||
/// Screen size for card sizing (measured from TableBackgroundView)
|
||||
@State private var screenSize: CGSize = CGSize(width: 375, height: 667)
|
||||
@ -61,9 +61,6 @@ struct GameTableView: View {
|
||||
if gameState == nil {
|
||||
gameState = GameState(settings: settings)
|
||||
}
|
||||
if tooltipManager == nil {
|
||||
tooltipManager = TooltipManager(onboarding: state.onboarding)
|
||||
}
|
||||
checkForWelcomeSheet()
|
||||
}
|
||||
.sheet(isPresented: $showSettings) {
|
||||
@ -111,27 +108,17 @@ struct GameTableView: View {
|
||||
],
|
||||
onboarding: state.onboarding,
|
||||
onDismiss: { showWelcome = false },
|
||||
onShowHints: checkOnboardingHints
|
||||
onShowHints: startWalkthrough
|
||||
)
|
||||
}
|
||||
.onChange(of: showWelcome) { wasShowing, isShowing in
|
||||
// Handle swipe-down dismissal: treat as "Start Playing" (no tooltips)
|
||||
// Handle swipe-down dismissal: treat as "Start Playing" (no walkthrough)
|
||||
if wasShowing && !isShowing && !state.onboarding.hasCompletedWelcome {
|
||||
state.onboarding.skipOnboarding()
|
||||
}
|
||||
}
|
||||
.onChange(of: state.currentBet) { _, newBet in
|
||||
if newBet > 0, state.onboarding.shouldShowHint("dealButton") {
|
||||
showDealHintWithDelay()
|
||||
}
|
||||
}
|
||||
.onChange(of: state.currentPhase) { oldPhase, newPhase in
|
||||
if case .playerTurn = newPhase, oldPhase != newPhase {
|
||||
if state.onboarding.shouldShowHint("playerActions") {
|
||||
showActionsHintWithDelay()
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sherpa walkthrough modifier
|
||||
.sherpa(isActive: isWalkthroughActive, tags: BlackjackWalkthroughTags.self, delegate: self)
|
||||
}
|
||||
|
||||
// Use global debug flag from Design constants
|
||||
@ -198,7 +185,14 @@ struct GameTableView: View {
|
||||
leadingButtons: hintToolbarButtons(for: state),
|
||||
onSettings: { showSettings = true },
|
||||
onHelp: { showRules = true },
|
||||
onStats: { showStats = true }
|
||||
onStats: { showStats = true },
|
||||
sherpaTags: TopBarSherpaTags(
|
||||
balance: BlackjackWalkthroughTags.balance,
|
||||
cardsRemaining: BlackjackWalkthroughTags.cardsRemaining,
|
||||
stats: BlackjackWalkthroughTags.statsButton,
|
||||
rules: BlackjackWalkthroughTags.rulesButton,
|
||||
settings: BlackjackWalkthroughTags.settingsButton
|
||||
)
|
||||
)
|
||||
.frame(maxWidth: maxContentWidth)
|
||||
.debugBorder(showDebugBorders, color: .cyan, label: "TopBar")
|
||||
@ -223,6 +217,7 @@ struct GameTableView: View {
|
||||
currentBet: state.minBetForChipSelector,
|
||||
maxBet: state.settings.maxBet
|
||||
)
|
||||
.sherpaTag(BlackjackWalkthroughTags.chipSelector)
|
||||
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
||||
.debugBorder(showDebugBorders, color: .pink, label: "ChipSelector")
|
||||
}
|
||||
@ -315,9 +310,6 @@ struct GameTableView: View {
|
||||
.onChange(of: state.balance) { oldBalance, newBalance in
|
||||
Design.debugLog("💰 Balance: \(oldBalance) → \(newBalance)")
|
||||
}
|
||||
|
||||
// Dynamic tooltip display
|
||||
.dynamicTooltip(tooltipManager ?? TooltipManager(onboarding: state.onboarding))
|
||||
}
|
||||
|
||||
// MARK: - Onboarding Helpers
|
||||
@ -332,39 +324,30 @@ struct GameTableView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func checkOnboardingHints() {
|
||||
// Show betting hint if not yet shown
|
||||
if state.onboarding.shouldShowHint("bettingZone") {
|
||||
tooltipManager?.show(
|
||||
key: "bettingZone",
|
||||
message: String(localized: "Select a chip and tap the bet area"),
|
||||
icon: "hand.tap.fill",
|
||||
position: .bottom,
|
||||
delay: 1.0
|
||||
)
|
||||
}
|
||||
/// Starts the Sherpa walkthrough when user taps "Show Me How"
|
||||
private func startWalkthrough() {
|
||||
// Reset onboarding hints so walkthrough can be seen again
|
||||
state.onboarding.reset()
|
||||
state.onboarding.completeWelcome()
|
||||
isWalkthroughActive = true
|
||||
}
|
||||
|
||||
private func showDealHintWithDelay() {
|
||||
tooltipManager?.show(
|
||||
key: "dealButton",
|
||||
message: String(localized: "Tap Deal to start the round"),
|
||||
icon: "play.fill",
|
||||
position: .bottom,
|
||||
delay: 0.5
|
||||
)
|
||||
// MARK: - SherpaDelegate
|
||||
|
||||
/// Returns nil to hide skip button and progress indicator
|
||||
func accessoryView(sherpa: Sherpa) -> AnyView? {
|
||||
nil
|
||||
}
|
||||
|
||||
private func showActionsHintWithDelay() {
|
||||
tooltipManager?.show(
|
||||
key: "playerActions",
|
||||
message: String(localized: "Choose your action based on the hint above"),
|
||||
icon: "hand.point.up.left.fill",
|
||||
position: .bottom,
|
||||
delay: 1.0
|
||||
)
|
||||
func onWalkthroughComplete(sherpa: Sherpa) {
|
||||
isWalkthroughActive = false
|
||||
state.onboarding.completeWelcome()
|
||||
}
|
||||
|
||||
func onWalkthroughSkipped(sherpa: Sherpa, atStep: Int, totalSteps: Int) {
|
||||
isWalkthroughActive = false
|
||||
state.onboarding.completeWelcome()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
@ -22,6 +22,18 @@ struct RulesHelpView: View {
|
||||
String(localized: "If the dealer busts and you haven't, you win.")
|
||||
]
|
||||
),
|
||||
RulePage(
|
||||
title: String(localized: "Game Sessions"),
|
||||
icon: "clock.badge.checkmark.fill",
|
||||
content: [
|
||||
String(localized: "sessions.trackProgress"),
|
||||
String(localized: "sessions.autoStart"),
|
||||
String(localized: "sessions.viewStats"),
|
||||
String(localized: "sessions.endSession"),
|
||||
String(localized: "sessions.history"),
|
||||
String(localized: "sessions.likeRealCasino")
|
||||
]
|
||||
),
|
||||
RulePage(
|
||||
title: String(localized: "Card Values"),
|
||||
icon: "suit.spade.fill",
|
||||
|
||||
@ -213,12 +213,14 @@ struct BlackjackTableView: View {
|
||||
state: state,
|
||||
selectedChip: selectedChip
|
||||
)
|
||||
.sherpaTag(BlackjackWalkthroughTags.bettingZone)
|
||||
.transition(.scale.combined(with: .opacity))
|
||||
.debugBorder(showDebugBorders, color: .blue, label: "BetZone")
|
||||
|
||||
// Betting hint based on count (only when card counting enabled)
|
||||
if let hint = state.bettingHint {
|
||||
BlackjackBettingHintView(hint: hint, trueCount: state.engine.trueCount)
|
||||
.sherpaTag(BlackjackWalkthroughTags.bettingHint)
|
||||
.transition(.opacity)
|
||||
.padding(.vertical, 10)
|
||||
.debugBorder(showDebugBorders, color: .purple, label: "BetHint")
|
||||
|
||||
@ -10,8 +10,8 @@ A feature-rich Blackjack app for iOS built with SwiftUI. Master basic strategy,
|
||||
|
||||
### 🎓 First-Time User Experience
|
||||
- **Welcome Sheet** — Interactive introduction on first launch
|
||||
- **Tutorial Mode** — Optional guided walkthrough of your first hand
|
||||
- **Contextual Hints** — Tips appear at the right moment during gameplay
|
||||
- **Spotlight Walkthrough** — Guided tour with spotlight focus on key UI elements
|
||||
- **Sherpa Framework** — Modern walkthrough with progress indicators and haptic feedback
|
||||
- **Never Intrusive** — All onboarding is skippable and shown only once
|
||||
|
||||
### 🎰 Authentic Casino Gameplay
|
||||
|
||||
@ -16,9 +16,13 @@ let package = Package(
|
||||
targets: ["CasinoKit"]
|
||||
)
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/mbrucedogs/Sherpa.git", branch: "develop")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "CasinoKit",
|
||||
dependencies: ["Sherpa"],
|
||||
resources: [
|
||||
.process("Resources")
|
||||
]
|
||||
|
||||
@ -140,84 +140,71 @@ WelcomeSheet(
|
||||
)
|
||||
```
|
||||
|
||||
**ContextualTooltip** - Show hints at the right moment.
|
||||
**Sherpa Walkthrough** - Guided spotlight walkthrough (re-exported from Sherpa package).
|
||||
|
||||
```swift
|
||||
@State private var showBettingHint = false
|
||||
// 1. Define walkthrough steps
|
||||
enum MyTags: SherpaTags {
|
||||
case bettingZone
|
||||
case dealButton
|
||||
|
||||
func makeCallout() -> Callout {
|
||||
switch self {
|
||||
case .bettingZone:
|
||||
return .localizedLabeled("walkthrough.bettingZone", systemImage: "hand.tap.fill")
|
||||
case .dealButton:
|
||||
return .localizedLabeled("walkthrough.dealButton", systemImage: "play.fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Wrap app in SherpaContainerView (in @main App)
|
||||
SherpaContainerView(configuration: .default) {
|
||||
ContentView()
|
||||
}
|
||||
|
||||
// 3. Tag views and activate walkthrough
|
||||
struct GameTableView: View, SherpaDelegate {
|
||||
@State private var isWalkthroughActive = false
|
||||
|
||||
var body: some View {
|
||||
BettingZone(/* ... */)
|
||||
.contextualTooltip(
|
||||
"Tap chips, then tap here to bet",
|
||||
icon: "hand.tap.fill",
|
||||
position: .bottom,
|
||||
isShowing: $showBettingHint,
|
||||
onDismiss: {
|
||||
gameState.onboarding.markHintShown("bettingZone")
|
||||
VStack {
|
||||
BettingZone()
|
||||
.sherpaTag(MyTags.bettingZone)
|
||||
|
||||
DealButton()
|
||||
.sherpaTag(MyTags.dealButton)
|
||||
}
|
||||
.sherpa(isActive: isWalkthroughActive, tags: MyTags.self, delegate: self)
|
||||
}
|
||||
|
||||
func onWalkthroughComplete(sherpa: Sherpa) {
|
||||
isWalkthroughActive = false
|
||||
onboarding.completeWelcome()
|
||||
}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**OnboardingState** - Track which hints have been shown.
|
||||
**OnboardingState** - Track onboarding completion status.
|
||||
|
||||
```swift
|
||||
let onboarding = OnboardingState(gameIdentifier: "blackjack")
|
||||
|
||||
// Check if a hint should be shown
|
||||
if onboarding.shouldShowHint("bettingZone") {
|
||||
showBettingHint = true
|
||||
}
|
||||
|
||||
// Mark hint as shown (persisted automatically)
|
||||
onboarding.markHintShown("bettingZone")
|
||||
|
||||
// Check if user has seen welcome
|
||||
if !onboarding.hasCompletedWelcome {
|
||||
showWelcome = true
|
||||
}
|
||||
|
||||
// Enable tutorial mode (shows all hints again)
|
||||
onboarding.startTutorialMode()
|
||||
// Mark welcome/walkthrough as completed
|
||||
onboarding.completeWelcome()
|
||||
|
||||
// Skip onboarding entirely
|
||||
onboarding.skipOnboarding()
|
||||
|
||||
// Reset onboarding (for testing)
|
||||
onboarding.reset()
|
||||
```
|
||||
|
||||
**TooltipManager** - Generic, scalable tooltip management (recommended).
|
||||
|
||||
```swift
|
||||
@State private var tooltipManager: TooltipManager?
|
||||
|
||||
var body: some View {
|
||||
GameView()
|
||||
.onAppear {
|
||||
tooltipManager = TooltipManager(onboarding: gameState.onboarding)
|
||||
}
|
||||
.dynamicTooltip(tooltipManager ?? TooltipManager(onboarding: gameState.onboarding))
|
||||
}
|
||||
|
||||
// Show a tooltip with automatic dismiss tracking
|
||||
private func showBettingHint() {
|
||||
tooltipManager?.show(
|
||||
key: "bettingZone",
|
||||
message: "Select a chip and tap here to bet",
|
||||
icon: "hand.tap.fill",
|
||||
position: .bottom,
|
||||
delay: 1.0
|
||||
)
|
||||
}
|
||||
|
||||
// Dismiss current tooltip manually
|
||||
tooltipManager?.dismiss()
|
||||
```
|
||||
|
||||
The `TooltipManager` automatically:
|
||||
- Ensures only one tooltip shows at a time
|
||||
- Marks tooltips as shown when dismissed or replaced
|
||||
- Respects `OnboardingState.shouldShowHint()` checks
|
||||
- Handles delayed presentation with cancellation if needed
|
||||
|
||||
**PulsingModifier** - Draw attention to interactive elements.
|
||||
|
||||
```swift
|
||||
@ -776,7 +763,6 @@ CasinoKit/
|
||||
│ │ ├── ChipDenomination.swift
|
||||
│ │ ├── TableLimits.swift # Betting limit presets
|
||||
│ │ ├── OnboardingState.swift # Onboarding tracking
|
||||
│ │ ├── TooltipManager.swift # Tooltip management
|
||||
│ │ └── Session/
|
||||
│ │ ├── GameSession.swift # Generic session with stats
|
||||
│ │ ├── GameSessionProtocol.swift # Session protocols
|
||||
@ -794,7 +780,6 @@ CasinoKit/
|
||||
│ │ │ └── SheetContainerView.swift
|
||||
│ │ ├── Onboarding/
|
||||
│ │ │ ├── WelcomeSheet.swift # First-launch welcome
|
||||
│ │ │ ├── ContextualTooltip.swift # In-game hints
|
||||
│ │ │ └── PulsingModifier.swift # Attention-grabbing pulse
|
||||
│ │ ├── Branding/
|
||||
│ │ │ ├── AppIconView.swift
|
||||
|
||||
@ -8,13 +8,15 @@
|
||||
// This file ensures all public types are exported from the module.
|
||||
// Clients can simply `import CasinoKit` to access all components.
|
||||
|
||||
// Re-export Sherpa for walkthrough functionality
|
||||
@_exported import Sherpa
|
||||
|
||||
// MARK: - Models
|
||||
// - Card, Suit, Rank
|
||||
// - Deck
|
||||
// - ChipDenomination
|
||||
// - TableLimits
|
||||
// - OnboardingState
|
||||
// - TooltipManager, TooltipConfig
|
||||
// - GameSettingsProtocol (shared settings interface)
|
||||
// - SettingsKeys, SettingsDefaults (persistence helpers)
|
||||
|
||||
@ -26,9 +28,14 @@
|
||||
// - ChipStackView, ChipOnTableView
|
||||
// - SheetContainerView, SheetSection
|
||||
// - WelcomeSheet, WelcomeFeature
|
||||
// - ContextualTooltip, ContextualTooltipModifier
|
||||
// - PulsingModifier
|
||||
|
||||
// MARK: - Walkthrough (via Sherpa)
|
||||
// Re-exported from Sherpa package:
|
||||
// - SherpaContainerView, SherpaConfiguration
|
||||
// - SherpaTags, Callout, Sherpa, SherpaDelegate
|
||||
// - .sherpaTag(), .sherpa() view modifiers
|
||||
|
||||
// MARK: - Effects
|
||||
// - ConfettiView, ConfettiPiece
|
||||
|
||||
|
||||
@ -1,114 +0,0 @@
|
||||
//
|
||||
// TooltipManager.swift
|
||||
// CasinoKit
|
||||
//
|
||||
// Manages contextual tooltips for onboarding.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// Configuration for a contextual tooltip
|
||||
public struct TooltipConfig: Equatable {
|
||||
public let key: String
|
||||
public let message: String
|
||||
public let icon: String
|
||||
public let position: ContextualTooltip.TooltipPosition
|
||||
|
||||
public init(key: String, message: String, icon: String, position: ContextualTooltip.TooltipPosition) {
|
||||
self.key = key
|
||||
self.message = message
|
||||
self.icon = icon
|
||||
self.position = position
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages the display of contextual tooltips with automatic dismissal and persistence
|
||||
@Observable
|
||||
@MainActor
|
||||
public final class TooltipManager {
|
||||
private let onboarding: OnboardingState
|
||||
|
||||
/// Currently showing tooltip (nil if none showing)
|
||||
public var currentTooltip: TooltipConfig? {
|
||||
didSet {
|
||||
// Mark the old tooltip as shown when replaced by a new one
|
||||
if let oldValue = oldValue, oldValue.key != currentTooltip?.key {
|
||||
onboarding.markHintShown(oldValue.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init(onboarding: OnboardingState) {
|
||||
self.onboarding = onboarding
|
||||
}
|
||||
|
||||
/// Shows a tooltip with a delay
|
||||
public func show(
|
||||
key: String,
|
||||
message: String,
|
||||
icon: String = "lightbulb.fill",
|
||||
position: ContextualTooltip.TooltipPosition = .bottom,
|
||||
delay: TimeInterval = 1.0
|
||||
) {
|
||||
Task { @MainActor in
|
||||
try? await Task.sleep(for: .seconds(delay))
|
||||
// Double-check hint hasn't been shown while we were waiting
|
||||
guard onboarding.shouldShowHint(key) else { return }
|
||||
withAnimation(.spring(duration: CasinoDesign.Animation.springDuration)) {
|
||||
currentTooltip = TooltipConfig(
|
||||
key: key,
|
||||
message: message,
|
||||
icon: icon,
|
||||
position: position
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dismisses the current tooltip and marks it as shown
|
||||
public func dismiss() {
|
||||
if let current = currentTooltip {
|
||||
onboarding.markHintShown(current.key)
|
||||
}
|
||||
withAnimation(.spring(duration: CasinoDesign.Animation.springDuration)) {
|
||||
currentTooltip = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - View Modifier
|
||||
|
||||
public extension View {
|
||||
/// Displays the current tooltip from a TooltipManager
|
||||
func dynamicTooltip(_ manager: TooltipManager) -> some View {
|
||||
self.overlay {
|
||||
if let tooltip = manager.currentTooltip {
|
||||
VStack {
|
||||
if tooltip.position == .bottom {
|
||||
Spacer()
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
|
||||
ContextualTooltip(
|
||||
message: tooltip.message,
|
||||
icon: tooltip.icon,
|
||||
position: tooltip.position,
|
||||
onDismiss: {
|
||||
manager.dismiss()
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, CasinoDesign.Spacing.medium)
|
||||
.transition(.move(edge: tooltip.position == .bottom ? .bottom : .top).combined(with: .opacity))
|
||||
|
||||
if tooltip.position == .top {
|
||||
Spacer()
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.zIndex(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1130,9 +1130,6 @@
|
||||
"Global" : {
|
||||
"comment" : "Title for the \"Global\" tab in the statistics view.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Got it" : {
|
||||
|
||||
},
|
||||
"Hands" : {
|
||||
"comment" : "Label for the number of hands played in the current session.",
|
||||
|
||||
@ -32,8 +32,32 @@ public struct TopBarButton: Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional Sherpa tags for TopBarView elements.
|
||||
/// Use this to enable walkthrough highlighting of individual top bar elements.
|
||||
public struct TopBarSherpaTags<Tags: SherpaTags> {
|
||||
public let balance: Tags?
|
||||
public let cardsRemaining: Tags?
|
||||
public let stats: Tags?
|
||||
public let rules: Tags?
|
||||
public let settings: Tags?
|
||||
|
||||
public init(
|
||||
balance: Tags? = nil,
|
||||
cardsRemaining: Tags? = nil,
|
||||
stats: Tags? = nil,
|
||||
rules: Tags? = nil,
|
||||
settings: Tags? = nil
|
||||
) {
|
||||
self.balance = balance
|
||||
self.cardsRemaining = cardsRemaining
|
||||
self.stats = stats
|
||||
self.rules = rules
|
||||
self.settings = settings
|
||||
}
|
||||
}
|
||||
|
||||
/// A top bar showing balance and customizable toolbar buttons.
|
||||
public struct TopBarView: View {
|
||||
public struct TopBarView<Tags: SherpaTags>: View {
|
||||
/// The current balance to display.
|
||||
public let balance: Int
|
||||
|
||||
@ -55,6 +79,9 @@ public struct TopBarView: View {
|
||||
/// Action when stats is tapped.
|
||||
public let onStats: (() -> Void)?
|
||||
|
||||
/// Optional Sherpa tags for walkthrough highlighting.
|
||||
public let sherpaTags: TopBarSherpaTags<Tags>?
|
||||
|
||||
// MARK: - Font Sizes (fixed for top bar constraints)
|
||||
|
||||
private let balanceFontSize: CGFloat = 24
|
||||
@ -71,7 +98,140 @@ public struct TopBarView: View {
|
||||
/// - onSettings: Settings button action.
|
||||
/// - onHelp: Help button action.
|
||||
/// - onStats: Stats button action.
|
||||
/// - sherpaTags: Optional Sherpa tags for walkthrough highlighting.
|
||||
public init(
|
||||
balance: Int,
|
||||
secondaryInfo: String? = nil,
|
||||
secondaryIcon: String? = nil,
|
||||
leadingButtons: [TopBarButton] = [],
|
||||
onSettings: (() -> Void)? = nil,
|
||||
onHelp: (() -> Void)? = nil,
|
||||
onStats: (() -> Void)? = nil,
|
||||
sherpaTags: TopBarSherpaTags<Tags>? = nil
|
||||
) {
|
||||
self.balance = balance
|
||||
self.secondaryInfo = secondaryInfo
|
||||
self.secondaryIcon = secondaryIcon
|
||||
self.leadingButtons = leadingButtons
|
||||
self.onSettings = onSettings
|
||||
self.onHelp = onHelp
|
||||
self.onStats = onStats
|
||||
self.sherpaTags = sherpaTags
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
HStack {
|
||||
// Balance display
|
||||
balanceView
|
||||
|
||||
Spacer()
|
||||
|
||||
// Secondary info (centered)
|
||||
if let info = secondaryInfo {
|
||||
secondaryInfoView(info: info)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Toolbar buttons
|
||||
toolbarButtonsView
|
||||
}
|
||||
.padding(.horizontal, CasinoDesign.Spacing.large)
|
||||
.padding(.vertical, CasinoDesign.Spacing.small)
|
||||
}
|
||||
|
||||
// MARK: - Private Views
|
||||
|
||||
@ViewBuilder
|
||||
private var balanceView: some View {
|
||||
let view = HStack(spacing: CasinoDesign.Spacing.xxSmall) {
|
||||
Text("$")
|
||||
.font(.system(size: dollarFontSize, weight: .bold))
|
||||
.foregroundStyle(Color.CasinoTopBar.balanceText)
|
||||
|
||||
Text(balance.formatted())
|
||||
.font(.system(size: balanceFontSize, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(Color.CasinoTopBar.balanceText)
|
||||
.contentTransition(.numericText())
|
||||
}
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(String(localized: "Balance", bundle: .module))
|
||||
.accessibilityValue("$\(balance.formatted())")
|
||||
|
||||
if let tag = sherpaTags?.balance {
|
||||
view.sherpaTag(tag)
|
||||
} else {
|
||||
view
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func secondaryInfoView(info: String) -> some View {
|
||||
let view = HStack(spacing: CasinoDesign.Spacing.xSmall) {
|
||||
if let icon = secondaryIcon {
|
||||
Image(systemName: icon)
|
||||
}
|
||||
Text(info)
|
||||
}
|
||||
.font(.system(size: secondaryFontSize))
|
||||
.foregroundStyle(Color.CasinoTopBar.secondaryText)
|
||||
|
||||
if let tag = sherpaTags?.cardsRemaining {
|
||||
view.sherpaTag(tag)
|
||||
} else {
|
||||
view
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var toolbarButtonsView: some View {
|
||||
HStack(spacing: CasinoDesign.Spacing.medium) {
|
||||
// Custom leading buttons (game-specific)
|
||||
ForEach(leadingButtons) { button in
|
||||
ToolbarButton(icon: button.icon, action: button.action)
|
||||
.accessibilityLabel(button.accessibilityLabel)
|
||||
}
|
||||
|
||||
if let onStats = onStats {
|
||||
let statsButton = ToolbarButton(icon: "chart.bar.fill", action: onStats)
|
||||
.accessibilityLabel(String(localized: "Statistics", bundle: .module))
|
||||
|
||||
if let tag = sherpaTags?.stats {
|
||||
statsButton.sherpaTag(tag)
|
||||
} else {
|
||||
statsButton
|
||||
}
|
||||
}
|
||||
|
||||
if let onHelp = onHelp {
|
||||
let helpButton = ToolbarButton(icon: "info.circle", action: onHelp)
|
||||
.accessibilityLabel(String(localized: "Rules", bundle: .module))
|
||||
|
||||
if let tag = sherpaTags?.rules {
|
||||
helpButton.sherpaTag(tag)
|
||||
} else {
|
||||
helpButton
|
||||
}
|
||||
}
|
||||
|
||||
if let onSettings = onSettings {
|
||||
let settingsButton = ToolbarButton(icon: "gearshape.fill", action: onSettings)
|
||||
.accessibilityLabel(String(localized: "Settings", bundle: .module))
|
||||
|
||||
if let tag = sherpaTags?.settings {
|
||||
settingsButton.sherpaTag(tag)
|
||||
} else {
|
||||
settingsButton
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience initializer for TopBarView without Sherpa tags.
|
||||
public extension TopBarView where Tags == NoSherpaTags {
|
||||
/// Creates a top bar without Sherpa walkthrough support.
|
||||
init(
|
||||
balance: Int,
|
||||
secondaryInfo: String? = nil,
|
||||
secondaryIcon: String? = nil,
|
||||
@ -87,72 +247,19 @@ public struct TopBarView: View {
|
||||
self.onSettings = onSettings
|
||||
self.onHelp = onHelp
|
||||
self.onStats = onStats
|
||||
self.sherpaTags = nil
|
||||
}
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
HStack {
|
||||
// Balance display
|
||||
HStack(spacing: CasinoDesign.Spacing.xxSmall) {
|
||||
Text("$")
|
||||
.font(.system(size: dollarFontSize, weight: .bold))
|
||||
.foregroundStyle(Color.CasinoTopBar.balanceText)
|
||||
|
||||
Text(balance.formatted())
|
||||
.font(.system(size: balanceFontSize, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(Color.CasinoTopBar.balanceText)
|
||||
.contentTransition(.numericText())
|
||||
}
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(String(localized: "Balance", bundle: .module))
|
||||
.accessibilityValue("$\(balance.formatted())")
|
||||
|
||||
Spacer()
|
||||
|
||||
// Secondary info (centered)
|
||||
if let info = secondaryInfo {
|
||||
HStack(spacing: CasinoDesign.Spacing.xSmall) {
|
||||
if let icon = secondaryIcon {
|
||||
Image(systemName: icon)
|
||||
}
|
||||
Text(info)
|
||||
}
|
||||
.font(.system(size: secondaryFontSize))
|
||||
.foregroundStyle(Color.CasinoTopBar.secondaryText)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Toolbar buttons
|
||||
HStack(spacing: CasinoDesign.Spacing.medium) {
|
||||
// Custom leading buttons (game-specific)
|
||||
ForEach(leadingButtons) { button in
|
||||
ToolbarButton(icon: button.icon, action: button.action)
|
||||
.accessibilityLabel(button.accessibilityLabel)
|
||||
}
|
||||
|
||||
if let onStats = onStats {
|
||||
ToolbarButton(icon: "chart.bar.fill", action: onStats)
|
||||
.accessibilityLabel(String(localized: "Statistics", bundle: .module))
|
||||
}
|
||||
|
||||
if let onHelp = onHelp {
|
||||
ToolbarButton(icon: "info.circle", action: onHelp)
|
||||
.accessibilityLabel(String(localized: "Rules", bundle: .module))
|
||||
}
|
||||
|
||||
if let onSettings = onSettings {
|
||||
ToolbarButton(icon: "gearshape.fill", action: onSettings)
|
||||
.accessibilityLabel(String(localized: "Settings", bundle: .module))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, CasinoDesign.Spacing.large)
|
||||
.padding(.vertical, CasinoDesign.Spacing.small)
|
||||
/// A placeholder SherpaTags type for when no walkthrough is needed.
|
||||
public enum NoSherpaTags: SherpaTags {
|
||||
public func makeCallout() -> Callout {
|
||||
.text("")
|
||||
}
|
||||
}
|
||||
|
||||
/// A single toolbar button.
|
||||
private struct ToolbarButton: View {
|
||||
struct ToolbarButton: View {
|
||||
let icon: String
|
||||
let action: () -> Void
|
||||
|
||||
@ -172,7 +279,7 @@ private struct ToolbarButton: View {
|
||||
Color.CasinoTable.felt.ignoresSafeArea()
|
||||
|
||||
VStack {
|
||||
TopBarView(
|
||||
TopBarView<NoSherpaTags>(
|
||||
balance: 10_500,
|
||||
secondaryInfo: "411",
|
||||
secondaryIcon: "rectangle.portrait.on.rectangle.portrait.fill",
|
||||
|
||||
@ -1,164 +0,0 @@
|
||||
//
|
||||
// ContextualTooltip.swift
|
||||
// CasinoKit
|
||||
//
|
||||
// Contextual tooltip for showing hints and tips to users.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A tooltip that appears to guide users through features.
|
||||
public struct ContextualTooltip: View {
|
||||
let message: String
|
||||
let icon: String
|
||||
let position: TooltipPosition
|
||||
let onDismiss: () -> Void
|
||||
|
||||
@ScaledMetric(relativeTo: .body) private var bodyFontSize: CGFloat = CasinoDesign.BaseFontSize.body
|
||||
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = CasinoDesign.IconSize.small
|
||||
|
||||
public enum TooltipPosition {
|
||||
case top, bottom, leading, trailing
|
||||
}
|
||||
|
||||
public init(
|
||||
message: String,
|
||||
icon: String = "lightbulb.fill",
|
||||
position: TooltipPosition = .top,
|
||||
onDismiss: @escaping () -> Void
|
||||
) {
|
||||
self.message = message
|
||||
self.icon = icon
|
||||
self.position = position
|
||||
self.onDismiss = onDismiss
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
HStack(spacing: CasinoDesign.Spacing.small) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: iconSize))
|
||||
.foregroundStyle(Color.Sheet.accent)
|
||||
|
||||
Text(message)
|
||||
.font(.system(size: bodyFontSize))
|
||||
.foregroundStyle(.white)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
Button {
|
||||
onDismiss()
|
||||
} label: {
|
||||
Text("Got it")
|
||||
.font(.system(size: bodyFontSize, weight: .semibold))
|
||||
.foregroundStyle(Color.Sheet.accent)
|
||||
}
|
||||
}
|
||||
.padding(CasinoDesign.Spacing.medium)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.medium)
|
||||
.fill(Color.black.opacity(CasinoDesign.Opacity.almostFull))
|
||||
.shadow(
|
||||
color: .black.opacity(CasinoDesign.Opacity.medium),
|
||||
radius: CasinoDesign.Shadow.radiusLarge,
|
||||
y: CasinoDesign.Shadow.offsetMedium
|
||||
)
|
||||
)
|
||||
.padding(.horizontal, CasinoDesign.Spacing.medium)
|
||||
.transition(.move(edge: edgeForPosition).combined(with: .opacity))
|
||||
}
|
||||
|
||||
private var edgeForPosition: Edge {
|
||||
switch position {
|
||||
case .top: return .top
|
||||
case .bottom: return .bottom
|
||||
case .leading: return .leading
|
||||
case .trailing: return .trailing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Modifier to show a contextual tooltip above a view.
|
||||
public struct ContextualTooltipModifier: ViewModifier {
|
||||
let message: String
|
||||
let icon: String
|
||||
let position: ContextualTooltip.TooltipPosition
|
||||
@Binding var isShowing: Bool
|
||||
let onDismiss: () -> Void
|
||||
|
||||
public func body(content: Content) -> some View {
|
||||
ZStack(alignment: alignmentForPosition) {
|
||||
content
|
||||
|
||||
if isShowing {
|
||||
ContextualTooltip(
|
||||
message: message,
|
||||
icon: icon,
|
||||
position: position,
|
||||
onDismiss: {
|
||||
withAnimation(.spring(duration: CasinoDesign.Animation.quick)) {
|
||||
isShowing = false
|
||||
}
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
.offset(offsetForPosition)
|
||||
.zIndex(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var alignmentForPosition: Alignment {
|
||||
switch position {
|
||||
case .top: return .top
|
||||
case .bottom: return .bottom
|
||||
case .leading: return .leading
|
||||
case .trailing: return .trailing
|
||||
}
|
||||
}
|
||||
|
||||
private var offsetForPosition: CGSize {
|
||||
switch position {
|
||||
case .top: return CGSize(width: 0, height: -CasinoDesign.Spacing.small)
|
||||
case .bottom: return CGSize(width: 0, height: CasinoDesign.Spacing.small)
|
||||
case .leading: return CGSize(width: -CasinoDesign.Spacing.small, height: 0)
|
||||
case .trailing: return CGSize(width: CasinoDesign.Spacing.small, height: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
/// Shows a contextual tooltip when the binding is true.
|
||||
func contextualTooltip(
|
||||
_ message: String,
|
||||
icon: String = "lightbulb.fill",
|
||||
position: ContextualTooltip.TooltipPosition = .top,
|
||||
isShowing: Binding<Bool>,
|
||||
onDismiss: @escaping () -> Void = {}
|
||||
) -> some View {
|
||||
modifier(ContextualTooltipModifier(
|
||||
message: message,
|
||||
icon: icon,
|
||||
position: position,
|
||||
isShowing: isShowing,
|
||||
onDismiss: onDismiss
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ZStack {
|
||||
Color.black
|
||||
|
||||
VStack {
|
||||
Text("Some Content")
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
.contextualTooltip(
|
||||
"This is a helpful tip that appears at the right moment!",
|
||||
position: .top,
|
||||
isShowing: .constant(true)
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,93 +2,154 @@
|
||||
|
||||
## Overview
|
||||
|
||||
A comprehensive, non-intrusive onboarding system has been implemented for both Blackjack and Baccarat games. The system provides a great first-time user experience without annoying experienced players.
|
||||
A comprehensive, non-intrusive onboarding system has been implemented for both Blackjack and Baccarat games using the **Sherpa** walkthrough framework. The system provides a great first-time user experience without annoying experienced players.
|
||||
|
||||
## Components Created (in CasinoKit)
|
||||
## Architecture
|
||||
|
||||
### 1. OnboardingState.swift
|
||||
The onboarding system uses:
|
||||
- **Sherpa** - A SwiftUI walkthrough framework for spotlight-based guided tours
|
||||
- **OnboardingState** - Tracks completion status and hint visibility
|
||||
- **WelcomeSheet** - First-launch welcome screen with feature highlights
|
||||
|
||||
## Sherpa Integration
|
||||
|
||||
### Package Dependency
|
||||
|
||||
Sherpa is added as a dependency in `CasinoKit/Package.swift`:
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/mbrucedogs/Sherpa.git", branch: "develop")
|
||||
]
|
||||
```
|
||||
|
||||
It's re-exported from CasinoKit, so games only need to `import CasinoKit`.
|
||||
|
||||
### App Structure
|
||||
|
||||
Each game wraps its content in `SherpaContainerView`:
|
||||
|
||||
```swift
|
||||
@main
|
||||
struct BlackjackApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
SherpaContainerView(configuration: .default) {
|
||||
AppLaunchView(config: .blackjack) {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Walkthrough Tags
|
||||
|
||||
Each game defines walkthrough steps using `SherpaTags`:
|
||||
|
||||
```swift
|
||||
enum BlackjackWalkthroughTags: SherpaTags {
|
||||
case bettingZone
|
||||
case dealButton
|
||||
case playerActions
|
||||
|
||||
func makeCallout() -> Callout {
|
||||
switch self {
|
||||
case .bettingZone:
|
||||
return .localizedLabeled(
|
||||
"walkthrough.bettingZone",
|
||||
systemImage: "hand.tap.fill",
|
||||
edge: .top
|
||||
)
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### View Tagging
|
||||
|
||||
Views are tagged for spotlight highlighting:
|
||||
|
||||
```swift
|
||||
BettingZoneView(state: state, selectedChip: selectedChip)
|
||||
.sherpaTag(BlackjackWalkthroughTags.bettingZone)
|
||||
|
||||
BettingActionsView(...)
|
||||
.sherpaTag(BlackjackWalkthroughTags.dealButton)
|
||||
```
|
||||
|
||||
### Activation
|
||||
|
||||
The walkthrough is activated when the user taps "Show Me How":
|
||||
|
||||
```swift
|
||||
struct GameTableView: View, SherpaDelegate {
|
||||
@State private var isWalkthroughActive = false
|
||||
|
||||
var body: some View {
|
||||
mainGameView(state: state)
|
||||
.sherpa(isActive: isWalkthroughActive, tags: BlackjackWalkthroughTags.self, delegate: self)
|
||||
}
|
||||
|
||||
private func startWalkthrough() {
|
||||
isWalkthroughActive = true
|
||||
}
|
||||
|
||||
// MARK: - SherpaDelegate
|
||||
|
||||
func onWalkthroughComplete(sherpa: Sherpa) {
|
||||
isWalkthroughActive = false
|
||||
state.onboarding.completeWelcome()
|
||||
}
|
||||
|
||||
func onWalkthroughSkipped(sherpa: Sherpa, atStep: Int, totalSteps: Int) {
|
||||
isWalkthroughActive = false
|
||||
state.onboarding.completeWelcome()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### 1. OnboardingState.swift (CasinoKit)
|
||||
- Tracks first-time user progress
|
||||
- Manages which hints have been shown
|
||||
- Persists state to UserDefaults
|
||||
- Supports tutorial mode (replay hints)
|
||||
- Game-specific identifiers (separate state per game)
|
||||
|
||||
**Key Methods:**
|
||||
- `shouldShowHint(key:)` - Check if hint should be displayed
|
||||
- `markHintShown(key:)` - Mark hint as seen (persisted)
|
||||
- `completeWelcome()` - Mark welcome sheet as completed
|
||||
- `startTutorialMode()` / `endTutorialMode()` - Enable/disable tutorial replay
|
||||
- `completeWelcome()` - Mark welcome/walkthrough as completed
|
||||
- `skipOnboarding()` - Skip onboarding entirely
|
||||
- `reset()` - Clear all onboarding data (for testing)
|
||||
|
||||
### 2. WelcomeSheet.swift
|
||||
### 2. WelcomeSheet.swift (CasinoKit)
|
||||
- First-launch welcome screen
|
||||
- Lists key features with icons
|
||||
- Two CTAs: "Show Me How" (tutorial) or "Start Playing" (skip)
|
||||
- Automatically shows game emoji (🃏 for Blackjack, 🎴 for Baccarat)
|
||||
- Two CTAs: "Show Me How" (starts walkthrough) or "Start Playing" (skip)
|
||||
- Fully localized
|
||||
|
||||
### 3. ContextualTooltip.swift
|
||||
- In-game hint tooltips
|
||||
- Appears at the right moment
|
||||
- "Got it" button to dismiss
|
||||
- Animated entry/exit
|
||||
- Automatically marks hint as shown on dismiss
|
||||
- View modifier for easy integration
|
||||
### 3. WalkthroughTags (Per Game)
|
||||
- Defines walkthrough steps for each game
|
||||
- Provides localized callout content
|
||||
- Specifies icons and positioning
|
||||
|
||||
### 4. PulsingModifier.swift
|
||||
- Visual pulse animation to draw attention
|
||||
- Optional enhancement for interactive elements
|
||||
- Configurable color and scale
|
||||
- Currently available but not actively used (can be added later)
|
||||
|
||||
## Integration in Games
|
||||
## Walkthrough Steps
|
||||
|
||||
### Blackjack
|
||||
|
||||
**Welcome Sheet Features:**
|
||||
1. Beat the Dealer - Get closer to 21
|
||||
2. Learn Strategy - Built-in hints
|
||||
3. Practice Free - Start with $1,000
|
||||
4. Customize Rules - Change settings
|
||||
|
||||
**Contextual Hints:**
|
||||
- **Betting Zone**: "Tap chips, then tap here to bet" (when entering betting phase for first time)
|
||||
- **Deal Button**: "Tap Deal to start the round" (after placing first bet)
|
||||
- **Player Actions**: "Choose your action based on the hint above" (during first player turn)
|
||||
| Step | Tag | Description |
|
||||
|------|-----|-------------|
|
||||
| 1 | `bettingZone` | Highlights betting area - "Select a chip and tap the bet area" |
|
||||
| 2 | `dealButton` | Highlights deal button - "Tap Deal to start the round" |
|
||||
| 3 | `playerActions` | Highlights action buttons - "Choose your action based on the hint above" |
|
||||
|
||||
### Baccarat
|
||||
|
||||
**Welcome Sheet Features:**
|
||||
1. Bet on Player, Banker, or Tie
|
||||
2. Track Patterns - Road maps show history
|
||||
3. Practice Free - Start with $1,000
|
||||
4. Customize Settings - Change table limits
|
||||
|
||||
**Contextual Hints:**
|
||||
- **Betting Zone**: "Tap chips, then tap a betting zone" (when entering betting phase for first time)
|
||||
- **Deal Button**: "Tap Deal to start the round" (after placing first bet)
|
||||
- **Result Display**: "Results appear here, then in the road maps below" (after first round completes)
|
||||
|
||||
## Settings Changes
|
||||
|
||||
Both games now default to more beginner-friendly settings:
|
||||
|
||||
| Setting | Old Default | New Default |
|
||||
|---------|-------------|-------------|
|
||||
| Table Limits | Low Stakes ($10-$1,000) | Casual ($5-$500) |
|
||||
| Starting Balance | $10,000 | $1,000 |
|
||||
|
||||
**Rationale:**
|
||||
- Lower minimum bet ($5) is less intimidating for beginners
|
||||
- $1,000 balance creates more meaningful decisions (not too much/little)
|
||||
- Still allows ~200 hands at $5/hand for good learning experience
|
||||
- Users can always increase in settings
|
||||
|
||||
## Bug Fix
|
||||
|
||||
**Blackjack Late Surrender:**
|
||||
- Fixed inconsistency where `resetToDefaults()` set it to `true` but Vegas preset overrode to `false`
|
||||
- Now correctly defaults to `false` to match Vegas rules
|
||||
| Step | Tag | Description |
|
||||
|------|-----|-------------|
|
||||
| 1 | `bettingZone` | Highlights betting table - "Select a chip and tap a bet zone" |
|
||||
| 2 | `dealButton` | Highlights deal button - "Tap Deal to start the round" |
|
||||
|
||||
## User Flow
|
||||
|
||||
@ -96,72 +157,73 @@ Both games now default to more beginner-friendly settings:
|
||||
1. App loads
|
||||
2. Welcome sheet appears automatically after 500ms delay
|
||||
3. User chooses:
|
||||
- **"Show Me How"**: Enables tutorial mode, shows contextual hints during first round
|
||||
- **"Start Playing"**: Skips tutorial, hints still appear once naturally during gameplay
|
||||
- **"Show Me How"**: Starts Sherpa walkthrough with spotlight focus
|
||||
- **"Start Playing"**: Skips walkthrough, marks onboarding complete
|
||||
|
||||
### Walkthrough Experience:
|
||||
1. Screen dims with spotlight on highlighted element
|
||||
2. Callout tooltip explains the element
|
||||
3. User taps to advance to next step
|
||||
4. Progress indicator shows current step
|
||||
5. Skip button available at all times
|
||||
|
||||
### Subsequent Launches:
|
||||
- Welcome sheet never shows again
|
||||
- Contextual hints appear once at the right moment
|
||||
- Each hint only shows once per hint key
|
||||
- Completely transparent to returning users
|
||||
- Walkthrough can be replayed via settings (if implemented)
|
||||
|
||||
### Tutorial Mode:
|
||||
- User can replay tutorial by tapping "Show Me How" on welcome
|
||||
- All hints re-enabled for that session
|
||||
- Tutorial mode doesn't persist across app launches
|
||||
## Localization
|
||||
|
||||
## Technical Details
|
||||
Walkthrough strings are stored in each game's `Localizable.xcstrings`:
|
||||
|
||||
### Hint Timing
|
||||
- Hints are shown with delays using `Task.sleep(for:)` on MainActor
|
||||
- Animations use spring duration for smooth transitions
|
||||
- onChange modifiers trigger hints based on game state changes
|
||||
| Key | English | Spanish (MX) | French (CA) |
|
||||
|-----|---------|--------------|-------------|
|
||||
| `walkthrough.bettingZone` | Select a chip and tap the bet area | Selecciona una ficha y toca el área de apuesta | Sélectionnez un jeton et touchez la zone de mise |
|
||||
| `walkthrough.dealButton` | Tap Deal to start the round | Toca Repartir para comenzar la ronda | Touchez Distribuer pour commencer la manche |
|
||||
| `walkthrough.playerActions` | Choose your action based on the hint above | Elige tu acción según la sugerencia de arriba | Choisissez votre action selon l'indice ci-dessus |
|
||||
|
||||
### Persistence
|
||||
- Uses UserDefaults with game-specific keys
|
||||
- Format: `"onboarding.{gameIdentifier}.{property}"`
|
||||
- Examples: `"onboarding.blackjack.hasLaunched"`, `"onboarding.baccarat.hintsShown"`
|
||||
## Benefits of Sherpa Migration
|
||||
|
||||
### Thread Safety
|
||||
- All onboarding state is @MainActor
|
||||
- Safe to use from SwiftUI views
|
||||
- No threading concerns
|
||||
| Before (TooltipManager) | After (Sherpa) |
|
||||
|------------------------|----------------|
|
||||
| Simple tooltips without focus | Spotlight effect highlights elements |
|
||||
| Manual positioning | Smart auto-positioning |
|
||||
| No navigation controls | Back/Next/Skip controls |
|
||||
| No progress indicator | Step indicator dots |
|
||||
| Basic animations | Smooth transitions with haptics |
|
||||
| Custom implementation | Well-tested framework |
|
||||
| 10+ languages | 10 languages built-in |
|
||||
|
||||
## Documentation Updates
|
||||
## Files Changed
|
||||
|
||||
### Blackjack README
|
||||
- Added "First-Time User Experience" section at top of features
|
||||
- Documents welcome sheet, tutorial mode, and contextual hints
|
||||
### CasinoKit
|
||||
- `Package.swift` - Added Sherpa dependency
|
||||
- `Exports.swift` - Re-exports Sherpa
|
||||
- Deleted: `TooltipManager.swift`, `ContextualTooltip.swift`
|
||||
|
||||
### Baccarat README
|
||||
- Added "First-Time User Experience" section at top of features
|
||||
- Same structure as Blackjack for consistency
|
||||
### Blackjack
|
||||
- Added: `Models/WalkthroughTags.swift`
|
||||
- Modified: `BlackjackApp.swift`, `GameTableView.swift`, `ActionButtonsView.swift`
|
||||
- Modified: `BlackjackTableView.swift` (added sherpaTag)
|
||||
- Modified: `Localizable.xcstrings` (walkthrough strings)
|
||||
|
||||
### CasinoKit README
|
||||
- New "Onboarding & Tutorials" section with full API documentation
|
||||
- Code examples for WelcomeSheet, ContextualTooltip, OnboardingState
|
||||
- Updated file structure to show new components
|
||||
- Added Blackjack to "Apps Using CasinoKit" list
|
||||
### Baccarat
|
||||
- Added: `Models/WalkthroughTags.swift`
|
||||
- Modified: `BaccaratApp.swift`, `GameTableView.swift`, `ActionButtonsView.swift`
|
||||
- Modified: `BettingTableView.swift` (added sherpaTag)
|
||||
- Modified: `Localizable.xcstrings` (walkthrough strings)
|
||||
|
||||
## Best Practices Followed
|
||||
|
||||
✅ **Non-intrusive**: All onboarding is skippable
|
||||
✅ **One-time only**: Hints never show twice (unless tutorial mode)
|
||||
✅ **Right moment**: Hints appear contextually, not all at once
|
||||
✅ **Short & visual**: Messages are concise with icons
|
||||
✅ **Sequential flow**: Clear progression through steps
|
||||
✅ **Visual focus**: Spotlight draws attention to key elements
|
||||
✅ **Haptic feedback**: Tactile response on step changes
|
||||
✅ **Localized**: All strings use String Catalog
|
||||
✅ **Accessible**: Uses standard SwiftUI components
|
||||
✅ **Persistent**: User's progress is saved
|
||||
✅ **Game-specific**: Each game has independent onboarding state
|
||||
✅ **Accessible**: VoiceOver announcements for each step
|
||||
✅ **Persistent**: User's completion status is saved
|
||||
✅ **Game-specific**: Each game has independent walkthrough
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
### Manual Testing:
|
||||
1. **Fresh install**: Delete app, reinstall, verify welcome sheet appears
|
||||
2. **Tutorial mode**: Tap "Show Me How", verify hints appear in sequence
|
||||
3. **Skip mode**: Tap "Start Playing", verify hints still appear once naturally
|
||||
4. **Second launch**: Relaunch app, verify welcome doesn't show again
|
||||
5. **Settings reset**: Check if onboarding can be reset (could add to settings)
|
||||
## Testing
|
||||
|
||||
### Reset for Testing:
|
||||
Add this to a development menu if needed:
|
||||
@ -171,60 +233,9 @@ Button("Reset Onboarding") {
|
||||
}
|
||||
```
|
||||
|
||||
## Future Enhancements (Not Implemented)
|
||||
|
||||
These could be added later if desired:
|
||||
|
||||
1. **Progressive discovery hints**: Tips after 5/10/20 hands
|
||||
- "Enable card counting in settings" (Blackjack)
|
||||
- "Side bets offer bigger payouts" (both games)
|
||||
|
||||
2. **Onboarding analytics**: Track which hints are most helpful
|
||||
|
||||
3. **User-requested replay**: "Show tutorial again" in settings
|
||||
|
||||
4. **Pulsing hints**: Use PulsingModifier on interactive elements during tutorial
|
||||
|
||||
5. **Accessibility announcements**: Post VoiceOver announcements for key moments
|
||||
|
||||
## Files Created
|
||||
|
||||
```
|
||||
CasinoKit/Sources/CasinoKit/
|
||||
├── Models/OnboardingState.swift
|
||||
└── Views/
|
||||
├── ContextualTooltip.swift
|
||||
├── WelcomeSheet.swift
|
||||
└── PulsingModifier.swift
|
||||
```
|
||||
|
||||
## Files Modified
|
||||
|
||||
```
|
||||
Blackjack/
|
||||
├── Models/GameSettings.swift (defaults changed, bug fix)
|
||||
├── Engine/GameState.swift (added onboarding property)
|
||||
└── Views/Game/GameTableView.swift (integrated onboarding UI)
|
||||
|
||||
Baccarat/
|
||||
├── Models/GameSettings.swift (defaults changed)
|
||||
├── Engine/GameState.swift (added onboarding property)
|
||||
└── Views/Game/GameTableView.swift (integrated onboarding UI)
|
||||
|
||||
README Files:
|
||||
├── Blackjack/README.md
|
||||
├── Baccarat/README.md
|
||||
└── CasinoKit/README.md
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
The onboarding system provides a polished first-time user experience that:
|
||||
- Helps new users understand the game quickly
|
||||
- Never annoys experienced players
|
||||
- Maintains the premium feel of the apps
|
||||
- Follows iOS best practices
|
||||
- Is fully reusable for future casino games
|
||||
|
||||
All code follows the project's Swift/SwiftUI guidelines, uses design constants, and includes proper accessibility support.
|
||||
|
||||
### Manual Testing:
|
||||
1. Delete app, reinstall, verify welcome sheet appears
|
||||
2. Tap "Show Me How", verify spotlight walkthrough starts
|
||||
3. Navigate through all steps
|
||||
4. Verify completion callback fires
|
||||
5. Relaunch app, verify welcome doesn't show again
|
||||
|
||||
@ -1,246 +0,0 @@
|
||||
# Tooltip System Refactoring
|
||||
|
||||
## Overview
|
||||
|
||||
The tooltip management system has been refactored from game-specific implementations to a generic, reusable system in CasinoKit.
|
||||
|
||||
## Problem
|
||||
|
||||
The original implementation had several issues:
|
||||
- **Verbose**: Each game had duplicate `TooltipConfig` structs and complex state management
|
||||
- **Not scalable**: Adding new tooltips required significant boilerplate
|
||||
- **Repetitive logic**: Similar code duplicated across Blackjack and Baccarat
|
||||
- **Difficult to maintain**: Changes required updates in multiple places
|
||||
|
||||
## Solution
|
||||
|
||||
Created a centralized `TooltipManager` class in CasinoKit that:
|
||||
- Manages tooltip state generically
|
||||
- Automatically handles dismiss tracking
|
||||
- Ensures only one tooltip shows at a time
|
||||
- Integrates seamlessly with `OnboardingState`
|
||||
|
||||
## New Components
|
||||
|
||||
### 1. `TooltipConfig` (CasinoKit)
|
||||
|
||||
A simple struct defining tooltip properties:
|
||||
|
||||
```swift
|
||||
public struct TooltipConfig: Equatable {
|
||||
public let key: String
|
||||
public let message: String
|
||||
public let icon: String
|
||||
public let position: ContextualTooltip.TooltipPosition
|
||||
}
|
||||
```
|
||||
|
||||
### 2. `TooltipManager` (CasinoKit)
|
||||
|
||||
An `@Observable` class that manages tooltip lifecycle:
|
||||
|
||||
```swift
|
||||
@Observable
|
||||
@MainActor
|
||||
public final class TooltipManager {
|
||||
private let onboarding: OnboardingState
|
||||
public var currentTooltip: TooltipConfig?
|
||||
|
||||
public func show(
|
||||
key: String,
|
||||
message: String,
|
||||
icon: String = "lightbulb.fill",
|
||||
position: ContextualTooltip.TooltipPosition = .bottom,
|
||||
delay: TimeInterval = 1.0
|
||||
)
|
||||
|
||||
public func dismiss()
|
||||
}
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- Automatic dismiss tracking via `didSet` on `currentTooltip`
|
||||
- Delayed presentation with cancellation if hint already shown
|
||||
- Respects `OnboardingState.shouldShowHint()` checks
|
||||
- Single source of truth for current tooltip
|
||||
|
||||
### 3. `dynamicTooltip` View Modifier (CasinoKit)
|
||||
|
||||
A convenient view modifier that displays tooltips:
|
||||
|
||||
```swift
|
||||
public extension View {
|
||||
func dynamicTooltip(_ manager: TooltipManager) -> some View
|
||||
}
|
||||
```
|
||||
|
||||
## Usage in Games
|
||||
|
||||
### Before (Verbose)
|
||||
|
||||
```swift
|
||||
// State variables for each tooltip
|
||||
@State private var showBettingHint = false
|
||||
@State private var showDealHint = false
|
||||
@State private var showActionsHint = false
|
||||
|
||||
// Separate struct definition
|
||||
private struct TooltipConfig {
|
||||
let key: String
|
||||
let message: String
|
||||
let icon: String
|
||||
let position: ContextualTooltip.TooltipPosition
|
||||
}
|
||||
|
||||
// Complex state with tracking
|
||||
@State private var currentTooltip: TooltipConfig? {
|
||||
didSet {
|
||||
if let oldValue = oldValue, oldValue.key != currentTooltip?.key {
|
||||
state.onboarding.markHintShown(oldValue.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generic helper with lots of parameters
|
||||
private func showTooltip(key: String, message: String, icon: String, position: ContextualTooltip.TooltipPosition, delay: TimeInterval) {
|
||||
Task { @MainActor in
|
||||
try? await Task.sleep(for: .seconds(delay))
|
||||
guard state.onboarding.shouldShowHint(key) else { return }
|
||||
withAnimation(.spring(duration: Design.Animation.springDuration)) {
|
||||
currentTooltip = TooltipConfig(key: key, message: message, icon: icon, position: position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Complex overlay in view body
|
||||
.overlay {
|
||||
if let tooltip = currentTooltip {
|
||||
VStack {
|
||||
// ... positioning logic
|
||||
ContextualTooltip(
|
||||
message: tooltip.message,
|
||||
icon: tooltip.icon,
|
||||
position: tooltip.position,
|
||||
onDismiss: {
|
||||
state.onboarding.markHintShown(tooltip.key)
|
||||
withAnimation(.spring(duration: Design.Animation.springDuration)) {
|
||||
currentTooltip = nil
|
||||
}
|
||||
}
|
||||
)
|
||||
// ... more positioning
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### After (Clean)
|
||||
|
||||
```swift
|
||||
// Single state variable
|
||||
@State private var tooltipManager: TooltipManager?
|
||||
|
||||
// Initialize in onAppear
|
||||
.onAppear {
|
||||
tooltipManager = TooltipManager(onboarding: state.onboarding)
|
||||
}
|
||||
|
||||
// Simple show calls
|
||||
private func showBettingHint() {
|
||||
tooltipManager?.show(
|
||||
key: "bettingZone",
|
||||
message: "Select a chip and tap here to bet",
|
||||
icon: "hand.tap.fill",
|
||||
position: .bottom,
|
||||
delay: 1.0
|
||||
)
|
||||
}
|
||||
|
||||
// One-line view modifier
|
||||
.dynamicTooltip(tooltipManager ?? TooltipManager(onboarding: state.onboarding))
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Reduced code**: ~70 lines of boilerplate eliminated per game
|
||||
2. **Better scalability**: Adding new tooltips is now trivial
|
||||
3. **Single source of truth**: All tooltip logic in CasinoKit
|
||||
4. **Easier maintenance**: Changes only need to be made in one place
|
||||
5. **Reusable**: Any new game can use `TooltipManager` with minimal setup
|
||||
|
||||
## Files Changed
|
||||
|
||||
### CasinoKit
|
||||
- **Created**: `Sources/CasinoKit/Models/TooltipManager.swift`
|
||||
- **Updated**: `Sources/CasinoKit/Exports.swift` (added TooltipManager export)
|
||||
- **Updated**: `README.md` (added TooltipManager documentation)
|
||||
|
||||
### Blackjack
|
||||
- **Updated**: `Views/Game/GameTableView.swift`
|
||||
- Removed local `TooltipConfig` struct
|
||||
- Removed `currentTooltip` @State with didSet
|
||||
- Removed generic `showTooltip()` helper
|
||||
- Added `@State private var tooltipManager: TooltipManager?`
|
||||
- Simplified tooltip show methods
|
||||
- Replaced complex overlay with `.dynamicTooltip()` modifier
|
||||
|
||||
### Baccarat
|
||||
- **Updated**: `Views/Game/GameTableView.swift`
|
||||
- Same changes as Blackjack
|
||||
|
||||
## Migration Guide
|
||||
|
||||
To use `TooltipManager` in a new game:
|
||||
|
||||
1. **Add state variable:**
|
||||
```swift
|
||||
@State private var tooltipManager: TooltipManager?
|
||||
```
|
||||
|
||||
2. **Initialize in onAppear:**
|
||||
```swift
|
||||
.onAppear {
|
||||
tooltipManager = TooltipManager(onboarding: gameState.onboarding)
|
||||
}
|
||||
```
|
||||
|
||||
3. **Apply view modifier:**
|
||||
```swift
|
||||
.dynamicTooltip(tooltipManager ?? TooltipManager(onboarding: gameState.onboarding))
|
||||
```
|
||||
|
||||
4. **Show tooltips:**
|
||||
```swift
|
||||
tooltipManager?.show(
|
||||
key: "uniqueKey",
|
||||
message: "Your hint text",
|
||||
icon: "lightbulb.fill",
|
||||
position: .bottom,
|
||||
delay: 1.0
|
||||
)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Both Blackjack and Baccarat have been built and tested successfully with the new system.
|
||||
|
||||
```bash
|
||||
# Blackjack
|
||||
xcodebuild -workspace CasinoGames.xcworkspace -scheme Blackjack \
|
||||
-configuration Debug -destination 'platform=iOS Simulator,id=...' build
|
||||
# ✅ BUILD SUCCEEDED
|
||||
|
||||
# Baccarat
|
||||
xcodebuild -workspace CasinoGames.xcworkspace -scheme Baccarat \
|
||||
-configuration Debug -destination 'platform=iOS Simulator,id=...' build
|
||||
# ✅ BUILD SUCCEEDED
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
- **Tooltip queue**: Allow queueing multiple tooltips
|
||||
- **Positioning hints**: Auto-detect best position based on screen space
|
||||
- **Animation customization**: Per-tooltip animation styles
|
||||
- **Analytics**: Track which tooltips are most helpful
|
||||
|
||||
Loading…
Reference in New Issue
Block a user