Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
3021951390
commit
2b15e57c33
@ -11,8 +11,10 @@ import CasinoKit
|
|||||||
struct BaccaratApp: App {
|
struct BaccaratApp: App {
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
AppLaunchView(config: .baccarat) {
|
SherpaContainerView(configuration: .default) {
|
||||||
ContentView()
|
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" : {
|
"GAME STATS" : {
|
||||||
"comment" : "Section in the statistics sheet dedicated to displaying statistics specific to baccarat.",
|
"comment" : "Section in the statistics sheet dedicated to displaying statistics specific to baccarat.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -3620,6 +3643,7 @@
|
|||||||
},
|
},
|
||||||
"Results appear here, then in the road maps below" : {
|
"Results appear here, then in the road maps below" : {
|
||||||
"comment" : "Instructional text for new players.",
|
"comment" : "Instructional text for new players.",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -3736,6 +3760,7 @@
|
|||||||
},
|
},
|
||||||
"Select a chip and tap a bet zone" : {
|
"Select a chip and tap a bet zone" : {
|
||||||
"comment" : "Onboarding hint for placing bets.",
|
"comment" : "Onboarding hint for placing bets.",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"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." : {
|
"Set a budget and stick to it." : {
|
||||||
"comment" : "Tip for players to set a budget and stick to it when playing baccarat.",
|
"comment" : "Tip for players to set a budget and stick to it when playing baccarat.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -4238,6 +4401,7 @@
|
|||||||
},
|
},
|
||||||
"Tap Deal to start the round" : {
|
"Tap Deal to start the round" : {
|
||||||
"comment" : "Instructional text for new players.",
|
"comment" : "Instructional text for new players.",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"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" : {
|
"WIN" : {
|
||||||
"comment" : "The text that appears as a badge when a player wins a hand in baccarat.",
|
"comment" : "The text that appears as a badge when a player wins a hand in baccarat.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
@ -61,6 +61,7 @@ struct ActionButtonsView: View {
|
|||||||
onClear: onClear,
|
onClear: onClear,
|
||||||
onDeal: onDeal
|
onDeal: onDeal
|
||||||
)
|
)
|
||||||
|
.sherpaTag(BaccaratWalkthroughTags.dealButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import SwiftUI
|
|||||||
import CasinoKit
|
import CasinoKit
|
||||||
|
|
||||||
/// The main game table view containing all game elements.
|
/// The main game table view containing all game elements.
|
||||||
struct GameTableView: View {
|
struct GameTableView: View, SherpaDelegate {
|
||||||
@State private var settings = GameSettings()
|
@State private var settings = GameSettings()
|
||||||
@State private var gameState: GameState?
|
@State private var gameState: GameState?
|
||||||
@State private var selectedChip: ChipDenomination = .hundred
|
@State private var selectedChip: ChipDenomination = .hundred
|
||||||
@ -18,10 +18,10 @@ struct GameTableView: View {
|
|||||||
@State private var showStats = false
|
@State private var showStats = false
|
||||||
@State private var showWelcome = false
|
@State private var showWelcome = false
|
||||||
|
|
||||||
// MARK: - Onboarding State
|
// MARK: - Walkthrough State
|
||||||
|
|
||||||
/// Tooltip manager for contextual hints
|
/// Whether the Sherpa walkthrough is active
|
||||||
@State private var tooltipManager: TooltipManager?
|
@State private var isWalkthroughActive = false
|
||||||
|
|
||||||
/// Screen size for card sizing (measured from TableBackgroundView)
|
/// Screen size for card sizing (measured from TableBackgroundView)
|
||||||
@State private var screenSize: CGSize = CGSize(width: 375, height: 667)
|
@State private var screenSize: CGSize = CGSize(width: 375, height: 667)
|
||||||
@ -85,6 +85,17 @@ struct GameTableView: View {
|
|||||||
// Use global debug flag from Design constants
|
// Use global debug flag from Design constants
|
||||||
private var showDebugBorders: Bool { Design.showDebugBorders }
|
private var showDebugBorders: Bool { Design.showDebugBorders }
|
||||||
|
|
||||||
|
/// 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
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -109,9 +120,6 @@ struct GameTableView: View {
|
|||||||
if gameState == nil {
|
if gameState == nil {
|
||||||
gameState = GameState(settings: settings)
|
gameState = GameState(settings: settings)
|
||||||
}
|
}
|
||||||
if tooltipManager == nil {
|
|
||||||
tooltipManager = TooltipManager(onboarding: state.onboarding)
|
|
||||||
}
|
|
||||||
checkForWelcomeSheet()
|
checkForWelcomeSheet()
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showSettings) {
|
.sheet(isPresented: $showSettings) {
|
||||||
@ -161,30 +169,17 @@ struct GameTableView: View {
|
|||||||
],
|
],
|
||||||
onboarding: state.onboarding,
|
onboarding: state.onboarding,
|
||||||
onDismiss: { showWelcome = false },
|
onDismiss: { showWelcome = false },
|
||||||
onShowHints: checkOnboardingHints
|
onShowHints: startWalkthrough
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.onChange(of: showWelcome) { wasShowing, isShowing in
|
.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 {
|
if wasShowing && !isShowing && !state.onboarding.hasCompletedWelcome {
|
||||||
state.onboarding.skipOnboarding()
|
state.onboarding.skipOnboarding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: state.totalBetAmount) { _, newTotal in
|
// Sherpa walkthrough modifier
|
||||||
if newTotal > 0, state.onboarding.shouldShowHint("dealButton") {
|
.sherpa(isActive: isWalkthroughActive, tags: BaccaratWalkthroughTags.self, delegate: self)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Onboarding Helpers
|
// MARK: - Onboarding Helpers
|
||||||
@ -198,36 +193,29 @@ struct GameTableView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func checkOnboardingHints() {
|
/// Starts the Sherpa walkthrough when user taps "Show Me How"
|
||||||
if state.onboarding.shouldShowHint("bettingZone") {
|
private func startWalkthrough() {
|
||||||
tooltipManager?.show(
|
// Reset onboarding hints so walkthrough can be seen again
|
||||||
key: "bettingZone",
|
state.onboarding.reset()
|
||||||
message: String(localized: "Select a chip and tap a bet zone"),
|
state.onboarding.completeWelcome()
|
||||||
icon: "hand.tap.fill",
|
isWalkthroughActive = true
|
||||||
position: .bottom,
|
|
||||||
delay: 1.0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showDealHintWithDelay() {
|
// MARK: - SherpaDelegate
|
||||||
tooltipManager?.show(
|
|
||||||
key: "dealButton",
|
/// Returns nil to hide skip button and progress indicator
|
||||||
message: String(localized: "Tap Deal to start the round"),
|
func accessoryView(sherpa: Sherpa) -> AnyView? {
|
||||||
icon: "play.fill",
|
nil
|
||||||
position: .bottom,
|
|
||||||
delay: 0.5
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showResultHintWithDelay() {
|
func onWalkthroughComplete(sherpa: Sherpa) {
|
||||||
tooltipManager?.show(
|
isWalkthroughActive = false
|
||||||
key: "firstResult",
|
state.onboarding.completeWelcome()
|
||||||
message: String(localized: "Results appear here, then in the road maps below"),
|
}
|
||||||
icon: "chart.bar.fill",
|
|
||||||
position: .bottom,
|
func onWalkthroughSkipped(sherpa: Sherpa, atStep: Int, totalSteps: Int) {
|
||||||
delay: 2.0
|
isWalkthroughActive = false
|
||||||
)
|
state.onboarding.completeWelcome()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private Views
|
// MARK: - Private Views
|
||||||
@ -256,7 +244,8 @@ struct GameTableView: View {
|
|||||||
secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil,
|
secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil,
|
||||||
onSettings: { showSettings = true },
|
onSettings: { showSettings = true },
|
||||||
onHelp: { showRules = true },
|
onHelp: { showRules = true },
|
||||||
onStats: { showStats = true }
|
onStats: { showStats = true },
|
||||||
|
sherpaTags: topBarSherpaTags
|
||||||
)
|
)
|
||||||
.debugBorder(showDebugBorders, color: .cyan, label: "TopBar")
|
.debugBorder(showDebugBorders, color: .cyan, label: "TopBar")
|
||||||
|
|
||||||
@ -288,6 +277,7 @@ struct GameTableView: View {
|
|||||||
|
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
}
|
}
|
||||||
|
.sherpaTag(BaccaratWalkthroughTags.history)
|
||||||
.frame(width: 240)
|
.frame(width: 240)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
||||||
@ -341,6 +331,7 @@ struct GameTableView: View {
|
|||||||
secondaryInfo: hintInfo.secondaryText,
|
secondaryInfo: hintInfo.secondaryText,
|
||||||
style: hintInfo.style
|
style: hintInfo.style
|
||||||
)
|
)
|
||||||
|
.sherpaTag(BaccaratWalkthroughTags.bettingHint)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
.padding(.vertical, Design.Spacing.small)
|
.padding(.vertical, Design.Spacing.small)
|
||||||
.debugBorder(showDebugBorders, color: .purple, label: "Hint")
|
.debugBorder(showDebugBorders, color: .purple, label: "Hint")
|
||||||
@ -356,6 +347,7 @@ struct GameTableView: View {
|
|||||||
currentBet: state.minBetForChipSelector,
|
currentBet: state.minBetForChipSelector,
|
||||||
maxBet: state.maxBet
|
maxBet: state.maxBet
|
||||||
)
|
)
|
||||||
|
.sherpaTag(BaccaratWalkthroughTags.chipSelector)
|
||||||
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
||||||
.debugBorder(showDebugBorders, color: .pink, label: "ChipSelector")
|
.debugBorder(showDebugBorders, color: .pink, label: "ChipSelector")
|
||||||
}
|
}
|
||||||
@ -399,7 +391,8 @@ struct GameTableView: View {
|
|||||||
secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil,
|
secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil,
|
||||||
onSettings: { showSettings = true },
|
onSettings: { showSettings = true },
|
||||||
onHelp: { showRules = true },
|
onHelp: { showRules = true },
|
||||||
onStats: { showStats = true }
|
onStats: { showStats = true },
|
||||||
|
sherpaTags: topBarSherpaTags
|
||||||
)
|
)
|
||||||
.debugBorder(showDebugBorders, color: .cyan, label: "TopBar")
|
.debugBorder(showDebugBorders, color: .cyan, label: "TopBar")
|
||||||
|
|
||||||
@ -430,6 +423,7 @@ struct GameTableView: View {
|
|||||||
// Road map history - show in portrait before deal on larger screens
|
// Road map history - show in portrait before deal on larger screens
|
||||||
if settings.showHistory && !isSmallScreen && !isDealing {
|
if settings.showHistory && !isSmallScreen && !isDealing {
|
||||||
RoadMapView(results: state.recentResults)
|
RoadMapView(results: state.recentResults)
|
||||||
|
.sherpaTag(BaccaratWalkthroughTags.history)
|
||||||
.frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity)
|
.frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity)
|
||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
.debugBorder(showDebugBorders, color: .orange, label: "RoadMap")
|
.debugBorder(showDebugBorders, color: .orange, label: "RoadMap")
|
||||||
@ -454,6 +448,7 @@ struct GameTableView: View {
|
|||||||
secondaryInfo: hintInfo.secondaryText,
|
secondaryInfo: hintInfo.secondaryText,
|
||||||
style: hintInfo.style
|
style: hintInfo.style
|
||||||
)
|
)
|
||||||
|
.sherpaTag(BaccaratWalkthroughTags.bettingHint)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
.padding(.vertical, Design.Spacing.small)
|
.padding(.vertical, Design.Spacing.small)
|
||||||
.debugBorder(showDebugBorders, color: .purple, label: "Hint")
|
.debugBorder(showDebugBorders, color: .purple, label: "Hint")
|
||||||
@ -467,6 +462,7 @@ struct GameTableView: View {
|
|||||||
currentBet: state.minBetForChipSelector,
|
currentBet: state.minBetForChipSelector,
|
||||||
maxBet: state.maxBet
|
maxBet: state.maxBet
|
||||||
)
|
)
|
||||||
|
.sherpaTag(BaccaratWalkthroughTags.chipSelector)
|
||||||
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
||||||
.debugBorder(showDebugBorders, color: .pink, label: "ChipSelector")
|
.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.")
|
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(
|
RulePage(
|
||||||
title: String(localized: "Card Values"),
|
title: String(localized: "Card Values"),
|
||||||
icon: "suit.spade.fill",
|
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)
|
.shadow(color: .black.opacity(Design.Opacity.light), radius: Design.Shadow.radiusLarge, y: Design.Shadow.offsetLarge)
|
||||||
|
.sherpaTag(BaccaratWalkthroughTags.bettingZone)
|
||||||
}
|
}
|
||||||
.debugBorder(showDebugBorders, color: .orange, label: "BettingTable")
|
.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
|
### 🎓 First-Time User Experience
|
||||||
- **Welcome Sheet** — Interactive introduction on first launch
|
- **Welcome Sheet** — Interactive introduction on first launch
|
||||||
- **Tutorial Mode** — Optional guided walkthrough of your first round
|
- **Spotlight Walkthrough** — Guided tour with spotlight focus on key UI elements
|
||||||
- **Contextual Hints** — Tips appear at the right moment during gameplay
|
- **Sherpa Framework** — Modern walkthrough with progress indicators and haptic feedback
|
||||||
- **Never Intrusive** — All onboarding is skippable and shown only once
|
- **Never Intrusive** — All onboarding is skippable and shown only once
|
||||||
|
|
||||||
### 🎰 Authentic Punto Banco Gameplay
|
### 🎰 Authentic Punto Banco Gameplay
|
||||||
|
|||||||
@ -18,8 +18,10 @@ struct BlackjackApp: App {
|
|||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
AppLaunchView(config: .blackjack) {
|
SherpaContainerView(configuration: .default) {
|
||||||
ContentView()
|
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" : {
|
"Clear" : {
|
||||||
"localizations" : {
|
"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" : {
|
"GAME STYLE" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -6034,6 +6054,7 @@
|
|||||||
},
|
},
|
||||||
"Select a chip and tap the bet area" : {
|
"Select a chip and tap the bet area" : {
|
||||||
"comment" : "Onboarding hint for placing bets.",
|
"comment" : "Onboarding hint for placing bets.",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"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" : {
|
"Settings" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -7191,9 +7350,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Tap Deal to start the round" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"TAP TO BET" : {
|
"TAP TO BET" : {
|
||||||
"localizations" : {
|
"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" : {
|
"Win Rate" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@ -50,6 +50,7 @@ struct ActionButtonsView: View {
|
|||||||
onClear: { state.clearBet() },
|
onClear: { state.clearBet() },
|
||||||
onDeal: { Task { await state.deal() } }
|
onDeal: { Task { await state.deal() } }
|
||||||
)
|
)
|
||||||
|
.sherpaTag(BlackjackWalkthroughTags.dealButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Player Turn Buttons
|
// MARK: - Player Turn Buttons
|
||||||
@ -83,6 +84,7 @@ struct ActionButtonsView: View {
|
|||||||
.transition(.scale.combined(with: .opacity))
|
.transition(.scale.combined(with: .opacity))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sherpaTag(BlackjackWalkthroughTags.playerActions)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
animatedActions = availableActions
|
animatedActions = availableActions
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import CasinoKit
|
import CasinoKit
|
||||||
|
|
||||||
struct GameTableView: View {
|
struct GameTableView: View, SherpaDelegate {
|
||||||
@State private var settings = GameSettings()
|
@State private var settings = GameSettings()
|
||||||
@State private var gameState: GameState?
|
@State private var gameState: GameState?
|
||||||
@State private var selectedChip: ChipDenomination = .twentyFive
|
@State private var selectedChip: ChipDenomination = .twentyFive
|
||||||
@ -20,10 +20,10 @@ struct GameTableView: View {
|
|||||||
@State private var showStats = false
|
@State private var showStats = false
|
||||||
@State private var showWelcome = false
|
@State private var showWelcome = false
|
||||||
|
|
||||||
// MARK: - Onboarding State
|
// MARK: - Walkthrough State
|
||||||
|
|
||||||
/// Tooltip manager for contextual hints
|
/// Whether the Sherpa walkthrough is active
|
||||||
@State private var tooltipManager: TooltipManager?
|
@State private var isWalkthroughActive = false
|
||||||
|
|
||||||
/// Screen size for card sizing (measured from TableBackgroundView)
|
/// Screen size for card sizing (measured from TableBackgroundView)
|
||||||
@State private var screenSize: CGSize = CGSize(width: 375, height: 667)
|
@State private var screenSize: CGSize = CGSize(width: 375, height: 667)
|
||||||
@ -61,9 +61,6 @@ struct GameTableView: View {
|
|||||||
if gameState == nil {
|
if gameState == nil {
|
||||||
gameState = GameState(settings: settings)
|
gameState = GameState(settings: settings)
|
||||||
}
|
}
|
||||||
if tooltipManager == nil {
|
|
||||||
tooltipManager = TooltipManager(onboarding: state.onboarding)
|
|
||||||
}
|
|
||||||
checkForWelcomeSheet()
|
checkForWelcomeSheet()
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showSettings) {
|
.sheet(isPresented: $showSettings) {
|
||||||
@ -111,27 +108,17 @@ struct GameTableView: View {
|
|||||||
],
|
],
|
||||||
onboarding: state.onboarding,
|
onboarding: state.onboarding,
|
||||||
onDismiss: { showWelcome = false },
|
onDismiss: { showWelcome = false },
|
||||||
onShowHints: checkOnboardingHints
|
onShowHints: startWalkthrough
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.onChange(of: showWelcome) { wasShowing, isShowing in
|
.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 {
|
if wasShowing && !isShowing && !state.onboarding.hasCompletedWelcome {
|
||||||
state.onboarding.skipOnboarding()
|
state.onboarding.skipOnboarding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: state.currentBet) { _, newBet in
|
// Sherpa walkthrough modifier
|
||||||
if newBet > 0, state.onboarding.shouldShowHint("dealButton") {
|
.sherpa(isActive: isWalkthroughActive, tags: BlackjackWalkthroughTags.self, delegate: self)
|
||||||
showDealHintWithDelay()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: state.currentPhase) { oldPhase, newPhase in
|
|
||||||
if case .playerTurn = newPhase, oldPhase != newPhase {
|
|
||||||
if state.onboarding.shouldShowHint("playerActions") {
|
|
||||||
showActionsHintWithDelay()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use global debug flag from Design constants
|
// Use global debug flag from Design constants
|
||||||
@ -198,7 +185,14 @@ struct GameTableView: View {
|
|||||||
leadingButtons: hintToolbarButtons(for: state),
|
leadingButtons: hintToolbarButtons(for: state),
|
||||||
onSettings: { showSettings = true },
|
onSettings: { showSettings = true },
|
||||||
onHelp: { showRules = 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)
|
.frame(maxWidth: maxContentWidth)
|
||||||
.debugBorder(showDebugBorders, color: .cyan, label: "TopBar")
|
.debugBorder(showDebugBorders, color: .cyan, label: "TopBar")
|
||||||
@ -223,6 +217,7 @@ struct GameTableView: View {
|
|||||||
currentBet: state.minBetForChipSelector,
|
currentBet: state.minBetForChipSelector,
|
||||||
maxBet: state.settings.maxBet
|
maxBet: state.settings.maxBet
|
||||||
)
|
)
|
||||||
|
.sherpaTag(BlackjackWalkthroughTags.chipSelector)
|
||||||
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
||||||
.debugBorder(showDebugBorders, color: .pink, label: "ChipSelector")
|
.debugBorder(showDebugBorders, color: .pink, label: "ChipSelector")
|
||||||
}
|
}
|
||||||
@ -315,9 +310,6 @@ struct GameTableView: View {
|
|||||||
.onChange(of: state.balance) { oldBalance, newBalance in
|
.onChange(of: state.balance) { oldBalance, newBalance in
|
||||||
Design.debugLog("💰 Balance: \(oldBalance) → \(newBalance)")
|
Design.debugLog("💰 Balance: \(oldBalance) → \(newBalance)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamic tooltip display
|
|
||||||
.dynamicTooltip(tooltipManager ?? TooltipManager(onboarding: state.onboarding))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Onboarding Helpers
|
// MARK: - Onboarding Helpers
|
||||||
@ -332,39 +324,30 @@ struct GameTableView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func checkOnboardingHints() {
|
/// Starts the Sherpa walkthrough when user taps "Show Me How"
|
||||||
// Show betting hint if not yet shown
|
private func startWalkthrough() {
|
||||||
if state.onboarding.shouldShowHint("bettingZone") {
|
// Reset onboarding hints so walkthrough can be seen again
|
||||||
tooltipManager?.show(
|
state.onboarding.reset()
|
||||||
key: "bettingZone",
|
state.onboarding.completeWelcome()
|
||||||
message: String(localized: "Select a chip and tap the bet area"),
|
isWalkthroughActive = true
|
||||||
icon: "hand.tap.fill",
|
|
||||||
position: .bottom,
|
|
||||||
delay: 1.0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showDealHintWithDelay() {
|
// MARK: - SherpaDelegate
|
||||||
tooltipManager?.show(
|
|
||||||
key: "dealButton",
|
/// Returns nil to hide skip button and progress indicator
|
||||||
message: String(localized: "Tap Deal to start the round"),
|
func accessoryView(sherpa: Sherpa) -> AnyView? {
|
||||||
icon: "play.fill",
|
nil
|
||||||
position: .bottom,
|
|
||||||
delay: 0.5
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showActionsHintWithDelay() {
|
func onWalkthroughComplete(sherpa: Sherpa) {
|
||||||
tooltipManager?.show(
|
isWalkthroughActive = false
|
||||||
key: "playerActions",
|
state.onboarding.completeWelcome()
|
||||||
message: String(localized: "Choose your action based on the hint above"),
|
|
||||||
icon: "hand.point.up.left.fill",
|
|
||||||
position: .bottom,
|
|
||||||
delay: 1.0
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func onWalkthroughSkipped(sherpa: Sherpa, atStep: Int, totalSteps: Int) {
|
||||||
|
isWalkthroughActive = false
|
||||||
|
state.onboarding.completeWelcome()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Preview
|
// MARK: - Preview
|
||||||
|
|||||||
@ -22,6 +22,18 @@ struct RulesHelpView: View {
|
|||||||
String(localized: "If the dealer busts and you haven't, you win.")
|
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(
|
RulePage(
|
||||||
title: String(localized: "Card Values"),
|
title: String(localized: "Card Values"),
|
||||||
icon: "suit.spade.fill",
|
icon: "suit.spade.fill",
|
||||||
|
|||||||
@ -213,12 +213,14 @@ struct BlackjackTableView: View {
|
|||||||
state: state,
|
state: state,
|
||||||
selectedChip: selectedChip
|
selectedChip: selectedChip
|
||||||
)
|
)
|
||||||
|
.sherpaTag(BlackjackWalkthroughTags.bettingZone)
|
||||||
.transition(.scale.combined(with: .opacity))
|
.transition(.scale.combined(with: .opacity))
|
||||||
.debugBorder(showDebugBorders, color: .blue, label: "BetZone")
|
.debugBorder(showDebugBorders, color: .blue, label: "BetZone")
|
||||||
|
|
||||||
// Betting hint based on count (only when card counting enabled)
|
// Betting hint based on count (only when card counting enabled)
|
||||||
if let hint = state.bettingHint {
|
if let hint = state.bettingHint {
|
||||||
BlackjackBettingHintView(hint: hint, trueCount: state.engine.trueCount)
|
BlackjackBettingHintView(hint: hint, trueCount: state.engine.trueCount)
|
||||||
|
.sherpaTag(BlackjackWalkthroughTags.bettingHint)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
.debugBorder(showDebugBorders, color: .purple, label: "BetHint")
|
.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
|
### 🎓 First-Time User Experience
|
||||||
- **Welcome Sheet** — Interactive introduction on first launch
|
- **Welcome Sheet** — Interactive introduction on first launch
|
||||||
- **Tutorial Mode** — Optional guided walkthrough of your first hand
|
- **Spotlight Walkthrough** — Guided tour with spotlight focus on key UI elements
|
||||||
- **Contextual Hints** — Tips appear at the right moment during gameplay
|
- **Sherpa Framework** — Modern walkthrough with progress indicators and haptic feedback
|
||||||
- **Never Intrusive** — All onboarding is skippable and shown only once
|
- **Never Intrusive** — All onboarding is skippable and shown only once
|
||||||
|
|
||||||
### 🎰 Authentic Casino Gameplay
|
### 🎰 Authentic Casino Gameplay
|
||||||
|
|||||||
@ -16,9 +16,13 @@ let package = Package(
|
|||||||
targets: ["CasinoKit"]
|
targets: ["CasinoKit"]
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(url: "https://github.com/mbrucedogs/Sherpa.git", branch: "develop")
|
||||||
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
name: "CasinoKit",
|
name: "CasinoKit",
|
||||||
|
dependencies: ["Sherpa"],
|
||||||
resources: [
|
resources: [
|
||||||
.process("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
|
```swift
|
||||||
@State private var showBettingHint = false
|
// 1. Define walkthrough steps
|
||||||
|
enum MyTags: SherpaTags {
|
||||||
|
case bettingZone
|
||||||
|
case dealButton
|
||||||
|
|
||||||
var body: some View {
|
func makeCallout() -> Callout {
|
||||||
BettingZone(/* ... */)
|
switch self {
|
||||||
.contextualTooltip(
|
case .bettingZone:
|
||||||
"Tap chips, then tap here to bet",
|
return .localizedLabeled("walkthrough.bettingZone", systemImage: "hand.tap.fill")
|
||||||
icon: "hand.tap.fill",
|
case .dealButton:
|
||||||
position: .bottom,
|
return .localizedLabeled("walkthrough.dealButton", systemImage: "play.fill")
|
||||||
isShowing: $showBettingHint,
|
}
|
||||||
onDismiss: {
|
}
|
||||||
gameState.onboarding.markHintShown("bettingZone")
|
}
|
||||||
}
|
|
||||||
)
|
// 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 {
|
||||||
|
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
|
```swift
|
||||||
let onboarding = OnboardingState(gameIdentifier: "blackjack")
|
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
|
// Check if user has seen welcome
|
||||||
if !onboarding.hasCompletedWelcome {
|
if !onboarding.hasCompletedWelcome {
|
||||||
showWelcome = true
|
showWelcome = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable tutorial mode (shows all hints again)
|
// Mark welcome/walkthrough as completed
|
||||||
onboarding.startTutorialMode()
|
onboarding.completeWelcome()
|
||||||
|
|
||||||
|
// Skip onboarding entirely
|
||||||
|
onboarding.skipOnboarding()
|
||||||
|
|
||||||
// Reset onboarding (for testing)
|
// Reset onboarding (for testing)
|
||||||
onboarding.reset()
|
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.
|
**PulsingModifier** - Draw attention to interactive elements.
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
@ -776,7 +763,6 @@ CasinoKit/
|
|||||||
│ │ ├── ChipDenomination.swift
|
│ │ ├── ChipDenomination.swift
|
||||||
│ │ ├── TableLimits.swift # Betting limit presets
|
│ │ ├── TableLimits.swift # Betting limit presets
|
||||||
│ │ ├── OnboardingState.swift # Onboarding tracking
|
│ │ ├── OnboardingState.swift # Onboarding tracking
|
||||||
│ │ ├── TooltipManager.swift # Tooltip management
|
|
||||||
│ │ └── Session/
|
│ │ └── Session/
|
||||||
│ │ ├── GameSession.swift # Generic session with stats
|
│ │ ├── GameSession.swift # Generic session with stats
|
||||||
│ │ ├── GameSessionProtocol.swift # Session protocols
|
│ │ ├── GameSessionProtocol.swift # Session protocols
|
||||||
@ -794,7 +780,6 @@ CasinoKit/
|
|||||||
│ │ │ └── SheetContainerView.swift
|
│ │ │ └── SheetContainerView.swift
|
||||||
│ │ ├── Onboarding/
|
│ │ ├── Onboarding/
|
||||||
│ │ │ ├── WelcomeSheet.swift # First-launch welcome
|
│ │ │ ├── WelcomeSheet.swift # First-launch welcome
|
||||||
│ │ │ ├── ContextualTooltip.swift # In-game hints
|
|
||||||
│ │ │ └── PulsingModifier.swift # Attention-grabbing pulse
|
│ │ │ └── PulsingModifier.swift # Attention-grabbing pulse
|
||||||
│ │ ├── Branding/
|
│ │ ├── Branding/
|
||||||
│ │ │ ├── AppIconView.swift
|
│ │ │ ├── AppIconView.swift
|
||||||
|
|||||||
@ -8,13 +8,15 @@
|
|||||||
// This file ensures all public types are exported from the module.
|
// This file ensures all public types are exported from the module.
|
||||||
// Clients can simply `import CasinoKit` to access all components.
|
// Clients can simply `import CasinoKit` to access all components.
|
||||||
|
|
||||||
|
// Re-export Sherpa for walkthrough functionality
|
||||||
|
@_exported import Sherpa
|
||||||
|
|
||||||
// MARK: - Models
|
// MARK: - Models
|
||||||
// - Card, Suit, Rank
|
// - Card, Suit, Rank
|
||||||
// - Deck
|
// - Deck
|
||||||
// - ChipDenomination
|
// - ChipDenomination
|
||||||
// - TableLimits
|
// - TableLimits
|
||||||
// - OnboardingState
|
// - OnboardingState
|
||||||
// - TooltipManager, TooltipConfig
|
|
||||||
// - GameSettingsProtocol (shared settings interface)
|
// - GameSettingsProtocol (shared settings interface)
|
||||||
// - SettingsKeys, SettingsDefaults (persistence helpers)
|
// - SettingsKeys, SettingsDefaults (persistence helpers)
|
||||||
|
|
||||||
@ -26,9 +28,14 @@
|
|||||||
// - ChipStackView, ChipOnTableView
|
// - ChipStackView, ChipOnTableView
|
||||||
// - SheetContainerView, SheetSection
|
// - SheetContainerView, SheetSection
|
||||||
// - WelcomeSheet, WelcomeFeature
|
// - WelcomeSheet, WelcomeFeature
|
||||||
// - ContextualTooltip, ContextualTooltipModifier
|
|
||||||
// - PulsingModifier
|
// - PulsingModifier
|
||||||
|
|
||||||
|
// MARK: - Walkthrough (via Sherpa)
|
||||||
|
// Re-exported from Sherpa package:
|
||||||
|
// - SherpaContainerView, SherpaConfiguration
|
||||||
|
// - SherpaTags, Callout, Sherpa, SherpaDelegate
|
||||||
|
// - .sherpaTag(), .sherpa() view modifiers
|
||||||
|
|
||||||
// MARK: - Effects
|
// MARK: - Effects
|
||||||
// - ConfettiView, ConfettiPiece
|
// - 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" : {
|
"Global" : {
|
||||||
"comment" : "Title for the \"Global\" tab in the statistics view.",
|
"comment" : "Title for the \"Global\" tab in the statistics view.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
|
||||||
"Got it" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Hands" : {
|
"Hands" : {
|
||||||
"comment" : "Label for the number of hands played in the current session.",
|
"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.
|
/// A top bar showing balance and customizable toolbar buttons.
|
||||||
public struct TopBarView: View {
|
public struct TopBarView<Tags: SherpaTags>: View {
|
||||||
/// The current balance to display.
|
/// The current balance to display.
|
||||||
public let balance: Int
|
public let balance: Int
|
||||||
|
|
||||||
@ -55,6 +79,9 @@ public struct TopBarView: View {
|
|||||||
/// Action when stats is tapped.
|
/// Action when stats is tapped.
|
||||||
public let onStats: (() -> Void)?
|
public let onStats: (() -> Void)?
|
||||||
|
|
||||||
|
/// Optional Sherpa tags for walkthrough highlighting.
|
||||||
|
public let sherpaTags: TopBarSherpaTags<Tags>?
|
||||||
|
|
||||||
// MARK: - Font Sizes (fixed for top bar constraints)
|
// MARK: - Font Sizes (fixed for top bar constraints)
|
||||||
|
|
||||||
private let balanceFontSize: CGFloat = 24
|
private let balanceFontSize: CGFloat = 24
|
||||||
@ -71,7 +98,140 @@ public struct TopBarView: View {
|
|||||||
/// - onSettings: Settings button action.
|
/// - onSettings: Settings button action.
|
||||||
/// - onHelp: Help button action.
|
/// - onHelp: Help button action.
|
||||||
/// - onStats: Stats button action.
|
/// - onStats: Stats button action.
|
||||||
|
/// - sherpaTags: Optional Sherpa tags for walkthrough highlighting.
|
||||||
public init(
|
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,
|
balance: Int,
|
||||||
secondaryInfo: String? = nil,
|
secondaryInfo: String? = nil,
|
||||||
secondaryIcon: String? = nil,
|
secondaryIcon: String? = nil,
|
||||||
@ -87,72 +247,19 @@ public struct TopBarView: View {
|
|||||||
self.onSettings = onSettings
|
self.onSettings = onSettings
|
||||||
self.onHelp = onHelp
|
self.onHelp = onHelp
|
||||||
self.onStats = onStats
|
self.onStats = onStats
|
||||||
|
self.sherpaTags = nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public var body: some View {
|
/// A placeholder SherpaTags type for when no walkthrough is needed.
|
||||||
HStack {
|
public enum NoSherpaTags: SherpaTags {
|
||||||
// Balance display
|
public func makeCallout() -> Callout {
|
||||||
HStack(spacing: CasinoDesign.Spacing.xxSmall) {
|
.text("")
|
||||||
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 single toolbar button.
|
/// A single toolbar button.
|
||||||
private struct ToolbarButton: View {
|
struct ToolbarButton: View {
|
||||||
let icon: String
|
let icon: String
|
||||||
let action: () -> Void
|
let action: () -> Void
|
||||||
|
|
||||||
@ -172,7 +279,7 @@ private struct ToolbarButton: View {
|
|||||||
Color.CasinoTable.felt.ignoresSafeArea()
|
Color.CasinoTable.felt.ignoresSafeArea()
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
TopBarView(
|
TopBarView<NoSherpaTags>(
|
||||||
balance: 10_500,
|
balance: 10_500,
|
||||||
secondaryInfo: "411",
|
secondaryInfo: "411",
|
||||||
secondaryIcon: "rectangle.portrait.on.rectangle.portrait.fill",
|
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
|
## 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
|
- Tracks first-time user progress
|
||||||
- Manages which hints have been shown
|
|
||||||
- Persists state to UserDefaults
|
- Persists state to UserDefaults
|
||||||
- Supports tutorial mode (replay hints)
|
|
||||||
- Game-specific identifiers (separate state per game)
|
- Game-specific identifiers (separate state per game)
|
||||||
|
|
||||||
**Key Methods:**
|
**Key Methods:**
|
||||||
- `shouldShowHint(key:)` - Check if hint should be displayed
|
- `completeWelcome()` - Mark welcome/walkthrough as completed
|
||||||
- `markHintShown(key:)` - Mark hint as seen (persisted)
|
- `skipOnboarding()` - Skip onboarding entirely
|
||||||
- `completeWelcome()` - Mark welcome sheet as completed
|
|
||||||
- `startTutorialMode()` / `endTutorialMode()` - Enable/disable tutorial replay
|
|
||||||
- `reset()` - Clear all onboarding data (for testing)
|
- `reset()` - Clear all onboarding data (for testing)
|
||||||
|
|
||||||
### 2. WelcomeSheet.swift
|
### 2. WelcomeSheet.swift (CasinoKit)
|
||||||
- First-launch welcome screen
|
- First-launch welcome screen
|
||||||
- Lists key features with icons
|
- Lists key features with icons
|
||||||
- Two CTAs: "Show Me How" (tutorial) or "Start Playing" (skip)
|
- Two CTAs: "Show Me How" (starts walkthrough) or "Start Playing" (skip)
|
||||||
- Automatically shows game emoji (🃏 for Blackjack, 🎴 for Baccarat)
|
|
||||||
- Fully localized
|
- Fully localized
|
||||||
|
|
||||||
### 3. ContextualTooltip.swift
|
### 3. WalkthroughTags (Per Game)
|
||||||
- In-game hint tooltips
|
- Defines walkthrough steps for each game
|
||||||
- Appears at the right moment
|
- Provides localized callout content
|
||||||
- "Got it" button to dismiss
|
- Specifies icons and positioning
|
||||||
- Animated entry/exit
|
|
||||||
- Automatically marks hint as shown on dismiss
|
|
||||||
- View modifier for easy integration
|
|
||||||
|
|
||||||
### 4. PulsingModifier.swift
|
## Walkthrough Steps
|
||||||
- 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
|
|
||||||
|
|
||||||
### Blackjack
|
### Blackjack
|
||||||
|
|
||||||
**Welcome Sheet Features:**
|
| Step | Tag | Description |
|
||||||
1. Beat the Dealer - Get closer to 21
|
|------|-----|-------------|
|
||||||
2. Learn Strategy - Built-in hints
|
| 1 | `bettingZone` | Highlights betting area - "Select a chip and tap the bet area" |
|
||||||
3. Practice Free - Start with $1,000
|
| 2 | `dealButton` | Highlights deal button - "Tap Deal to start the round" |
|
||||||
4. Customize Rules - Change settings
|
| 3 | `playerActions` | Highlights action buttons - "Choose your action based on the hint above" |
|
||||||
|
|
||||||
**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)
|
|
||||||
|
|
||||||
### Baccarat
|
### Baccarat
|
||||||
|
|
||||||
**Welcome Sheet Features:**
|
| Step | Tag | Description |
|
||||||
1. Bet on Player, Banker, or Tie
|
|------|-----|-------------|
|
||||||
2. Track Patterns - Road maps show history
|
| 1 | `bettingZone` | Highlights betting table - "Select a chip and tap a bet zone" |
|
||||||
3. Practice Free - Start with $1,000
|
| 2 | `dealButton` | Highlights deal button - "Tap Deal to start the round" |
|
||||||
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
|
|
||||||
|
|
||||||
## User Flow
|
## User Flow
|
||||||
|
|
||||||
@ -96,72 +157,73 @@ Both games now default to more beginner-friendly settings:
|
|||||||
1. App loads
|
1. App loads
|
||||||
2. Welcome sheet appears automatically after 500ms delay
|
2. Welcome sheet appears automatically after 500ms delay
|
||||||
3. User chooses:
|
3. User chooses:
|
||||||
- **"Show Me How"**: Enables tutorial mode, shows contextual hints during first round
|
- **"Show Me How"**: Starts Sherpa walkthrough with spotlight focus
|
||||||
- **"Start Playing"**: Skips tutorial, hints still appear once naturally during gameplay
|
- **"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:
|
### Subsequent Launches:
|
||||||
- Welcome sheet never shows again
|
- Welcome sheet never shows again
|
||||||
- Contextual hints appear once at the right moment
|
- Walkthrough can be replayed via settings (if implemented)
|
||||||
- Each hint only shows once per hint key
|
|
||||||
- Completely transparent to returning users
|
|
||||||
|
|
||||||
### Tutorial Mode:
|
## Localization
|
||||||
- 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
|
|
||||||
|
|
||||||
## Technical Details
|
Walkthrough strings are stored in each game's `Localizable.xcstrings`:
|
||||||
|
|
||||||
### Hint Timing
|
| Key | English | Spanish (MX) | French (CA) |
|
||||||
- Hints are shown with delays using `Task.sleep(for:)` on MainActor
|
|-----|---------|--------------|-------------|
|
||||||
- Animations use spring duration for smooth transitions
|
| `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 |
|
||||||
- onChange modifiers trigger hints based on game state changes
|
| `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
|
## Benefits of Sherpa Migration
|
||||||
- Uses UserDefaults with game-specific keys
|
|
||||||
- Format: `"onboarding.{gameIdentifier}.{property}"`
|
|
||||||
- Examples: `"onboarding.blackjack.hasLaunched"`, `"onboarding.baccarat.hintsShown"`
|
|
||||||
|
|
||||||
### Thread Safety
|
| Before (TooltipManager) | After (Sherpa) |
|
||||||
- All onboarding state is @MainActor
|
|------------------------|----------------|
|
||||||
- Safe to use from SwiftUI views
|
| Simple tooltips without focus | Spotlight effect highlights elements |
|
||||||
- No threading concerns
|
| 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
|
### CasinoKit
|
||||||
- Added "First-Time User Experience" section at top of features
|
- `Package.swift` - Added Sherpa dependency
|
||||||
- Documents welcome sheet, tutorial mode, and contextual hints
|
- `Exports.swift` - Re-exports Sherpa
|
||||||
|
- Deleted: `TooltipManager.swift`, `ContextualTooltip.swift`
|
||||||
|
|
||||||
### Baccarat README
|
### Blackjack
|
||||||
- Added "First-Time User Experience" section at top of features
|
- Added: `Models/WalkthroughTags.swift`
|
||||||
- Same structure as Blackjack for consistency
|
- Modified: `BlackjackApp.swift`, `GameTableView.swift`, `ActionButtonsView.swift`
|
||||||
|
- Modified: `BlackjackTableView.swift` (added sherpaTag)
|
||||||
|
- Modified: `Localizable.xcstrings` (walkthrough strings)
|
||||||
|
|
||||||
### CasinoKit README
|
### Baccarat
|
||||||
- New "Onboarding & Tutorials" section with full API documentation
|
- Added: `Models/WalkthroughTags.swift`
|
||||||
- Code examples for WelcomeSheet, ContextualTooltip, OnboardingState
|
- Modified: `BaccaratApp.swift`, `GameTableView.swift`, `ActionButtonsView.swift`
|
||||||
- Updated file structure to show new components
|
- Modified: `BettingTableView.swift` (added sherpaTag)
|
||||||
- Added Blackjack to "Apps Using CasinoKit" list
|
- Modified: `Localizable.xcstrings` (walkthrough strings)
|
||||||
|
|
||||||
## Best Practices Followed
|
## Best Practices Followed
|
||||||
|
|
||||||
✅ **Non-intrusive**: All onboarding is skippable
|
✅ **Non-intrusive**: All onboarding is skippable
|
||||||
✅ **One-time only**: Hints never show twice (unless tutorial mode)
|
✅ **Sequential flow**: Clear progression through steps
|
||||||
✅ **Right moment**: Hints appear contextually, not all at once
|
✅ **Visual focus**: Spotlight draws attention to key elements
|
||||||
✅ **Short & visual**: Messages are concise with icons
|
✅ **Haptic feedback**: Tactile response on step changes
|
||||||
✅ **Localized**: All strings use String Catalog
|
✅ **Localized**: All strings use String Catalog
|
||||||
✅ **Accessible**: Uses standard SwiftUI components
|
✅ **Accessible**: VoiceOver announcements for each step
|
||||||
✅ **Persistent**: User's progress is saved
|
✅ **Persistent**: User's completion status is saved
|
||||||
✅ **Game-specific**: Each game has independent onboarding state
|
✅ **Game-specific**: Each game has independent walkthrough
|
||||||
|
|
||||||
## Testing Recommendations
|
## Testing
|
||||||
|
|
||||||
### 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)
|
|
||||||
|
|
||||||
### Reset for Testing:
|
### Reset for Testing:
|
||||||
Add this to a development menu if needed:
|
Add this to a development menu if needed:
|
||||||
@ -171,60 +233,9 @@ Button("Reset Onboarding") {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Future Enhancements (Not Implemented)
|
### Manual Testing:
|
||||||
|
1. Delete app, reinstall, verify welcome sheet appears
|
||||||
These could be added later if desired:
|
2. Tap "Show Me How", verify spotlight walkthrough starts
|
||||||
|
3. Navigate through all steps
|
||||||
1. **Progressive discovery hints**: Tips after 5/10/20 hands
|
4. Verify completion callback fires
|
||||||
- "Enable card counting in settings" (Blackjack)
|
5. Relaunch app, verify welcome doesn't show again
|
||||||
- "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.
|
|
||||||
|
|
||||||
|
|||||||
@ -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