Signed-off-by: Matt Bruce <matt.bruce1@toyota.com>
This commit is contained in:
parent
53edd3aa7c
commit
1daaf6ca22
@ -188,7 +188,7 @@
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 2600;
|
||||
LastUpgradeCheck = 2600;
|
||||
LastUpgradeCheck = 2610;
|
||||
TargetAttributes = {
|
||||
EAD890B62EF1E9CE006DBA80 = {
|
||||
CreatedOnToolsVersion = 26.0;
|
||||
@ -296,6 +296,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
@ -350,6 +351,7 @@
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@ -361,6 +363,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
@ -408,6 +411,7 @@
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
|
||||
@ -51,6 +51,9 @@ final class GameState {
|
||||
// MARK: - Settings
|
||||
let settings: GameSettings
|
||||
|
||||
// MARK: - Onboarding
|
||||
let onboarding: OnboardingState
|
||||
|
||||
// MARK: - Sound
|
||||
private let sound = SoundManager.shared
|
||||
|
||||
@ -343,6 +346,7 @@ final class GameState {
|
||||
self.settings = settings
|
||||
self.engine = BaccaratEngine(deckCount: settings.deckCount.rawValue)
|
||||
self.balance = settings.startingBalance
|
||||
self.onboarding = OnboardingState(gameIdentifier: "baccarat")
|
||||
|
||||
// Sync sound settings with SoundManager
|
||||
syncSoundSettings()
|
||||
|
||||
@ -188,13 +188,9 @@ final class GameSettings {
|
||||
|
||||
/// Whether iCloud is available.
|
||||
var iCloudAvailable: Bool {
|
||||
guard FileManager.default.ubiquityIdentityToken != nil else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Additional check: verify we can actually access ubiquity container
|
||||
let containerURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)
|
||||
return containerURL != nil
|
||||
// NSUbiquitousKeyValueStore only requires iCloud sign-in (token)
|
||||
// It does NOT require iCloud Drive/Documents to be enabled
|
||||
FileManager.default.ubiquityIdentityToken != nil
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
@ -1011,6 +1011,9 @@
|
||||
"Banker running hot (%lld%%)" : {
|
||||
"comment" : "A hint to place a bet on the Banker based on the calculated house edge. The argument is the percentage of the house edge.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Bet on Player, Banker, or Tie" : {
|
||||
|
||||
},
|
||||
"Bet on which hand will win: Player, Banker, or Tie." : {
|
||||
"comment" : "Text describing the objective of the baccarat game.",
|
||||
@ -1061,6 +1064,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Blue Circle (P): Player won the hand" : {
|
||||
"comment" : "Explains the blue circle icon in the history.",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Blue Circle (P): Player won the hand"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Círculo Azul (P): El jugador ganó la mano"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cercle Bleu (P) : Le joueur a gagné la main"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"BONUS" : {
|
||||
"comment" : "The text displayed in the center of the bonus zone.",
|
||||
"localizations" : {
|
||||
@ -1198,6 +1224,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Change table limits and display options" : {
|
||||
|
||||
},
|
||||
"Chips, cards, and result sounds" : {
|
||||
"comment" : "Subtitle describing sound effects toggle.",
|
||||
@ -1314,6 +1343,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Customize Settings" : {
|
||||
|
||||
},
|
||||
"DATA" : {
|
||||
"localizations" : {
|
||||
@ -1712,6 +1744,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Green Circle (T): Tie between Player and Banker" : {
|
||||
"comment" : "Explains the green circle icon in the history.",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Green Circle (T): Tie between Player and Banker"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Círculo Verde (T): Empate entre el jugador y el banquero"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cercle Vert (T) : Égalité entre le joueur et le banquier"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hand values use only the last digit (e.g., 7+8=15 → 5)." : {
|
||||
"comment" : "Explanation of how card values are determined in baccarat.",
|
||||
"localizations" : {
|
||||
@ -1804,6 +1859,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"History Display" : {
|
||||
"comment" : "Title of a section in the Rules Help view explaining the history feature.",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "History Display"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Visualización del historial"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Affichage de l'historique"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"historySummaryFormat" : {
|
||||
"comment" : "Format string used to create a summary of the user's game history, including the total number of rounds played, as well as the number of rounds won by the player, the banker, and as ties.",
|
||||
"localizations" : {
|
||||
@ -2899,6 +2977,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Practice Free" : {
|
||||
|
||||
},
|
||||
"Privacy Policy" : {
|
||||
"localizations" : {
|
||||
@ -2922,6 +3003,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Red Circle (B): Banker won the hand" : {
|
||||
"comment" : "Explains the red circle icon in the history.",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Red Circle (B): Banker won the hand"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Círculo Rojo (B): El banquero ganó la mano"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cercle Rouge (B) : Le banquier a gagné la main"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Reset Game" : {
|
||||
"comment" : "A button label that resets the game balance and reshuffles the deck.",
|
||||
"localizations" : {
|
||||
@ -2994,6 +3098,12 @@
|
||||
"Result distribution" : {
|
||||
"comment" : "A label describing the view that shows the distribution of betting results.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Results appear here, then in the road maps below" : {
|
||||
|
||||
},
|
||||
"Road maps show game history and trends" : {
|
||||
|
||||
},
|
||||
"Roulette" : {
|
||||
"comment" : "The name of a roulette game.",
|
||||
@ -3063,6 +3173,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Select a chip and tap a bet zone" : {
|
||||
|
||||
},
|
||||
"Set a budget and stick to it." : {
|
||||
"comment" : "Tip for players to set a budget and stick to it when playing baccarat.",
|
||||
@ -3158,6 +3271,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Show Welcome Again" : {
|
||||
|
||||
},
|
||||
"Side bet on Player or Banker winning by a margin." : {
|
||||
"comment" : "Title for a side bet where the player bets on which hand wins by a margin (e.g., Banker by 9 points).",
|
||||
@ -3272,6 +3388,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Start with $1,000 and play risk-free" : {
|
||||
|
||||
},
|
||||
"STARTING BALANCE" : {
|
||||
"comment" : "Section header for starting balance settings.",
|
||||
@ -3318,190 +3437,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"History Display" : {
|
||||
"comment" : "Title of a section in the Rules Help view explaining the history feature.",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "History Display"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Visualización del historial"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Affichage de l'historique"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"The History shows all previous round results at a glance." : {
|
||||
"comment" : "Explains the purpose of the history display.",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "The History shows all previous round results at a glance."
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "El Historial muestra todos los resultados de rondas anteriores de un vistazo."
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "L'historique affiche tous les résultats des tours précédents en un coup d'œil."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Blue Circle (P): Player won the hand" : {
|
||||
"comment" : "Explains the blue circle icon in the history.",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Blue Circle (P): Player won the hand"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Círculo Azul (P): El jugador ganó la mano"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cercle Bleu (P) : Le joueur a gagné la main"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Red Circle (B): Banker won the hand" : {
|
||||
"comment" : "Explains the red circle icon in the history.",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Red Circle (B): Banker won the hand"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Círculo Rojo (B): El banquero ganó la mano"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cercle Rouge (B) : Le banquier a gagné la main"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Green Circle (T): Tie between Player and Banker" : {
|
||||
"comment" : "Explains the green circle icon in the history.",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Green Circle (T): Tie between Player and Banker"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Círculo Verde (T): Empate entre el jugador y el banquero"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cercle Vert (T) : Égalité entre le joueur et le banquier"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Yellow Dot (bottom-left): A pair occurred in that hand" : {
|
||||
"comment" : "Explains the yellow dot marker in the history.",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Yellow Dot (bottom-left): A pair occurred in that hand"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Punto Amarillo (abajo-izquierda): Hubo un par en esa mano"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Point Jaune (en bas à gauche) : Une paire s'est produite dans cette main"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Yellow Star (top-right): Natural 8 or 9 win" : {
|
||||
"comment" : "Explains the yellow star marker in the history.",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Yellow Star (top-right): Natural 8 or 9 win"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Estrella Amarilla (arriba-derecha): Victoria natural con 8 o 9"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Étoile Jaune (en haut à droite) : Victoire naturelle avec 8 ou 9"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Use History to spot patterns and trends in the shoe." : {
|
||||
"comment" : "Advises the player on how to use the history feature.",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Use History to spot patterns and trends in the shoe."
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Usa el Historial para detectar patrones y tendencias en el zapato."
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Utilisez l'historique pour repérer les modèles et les tendances dans le sabot."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Strategy Tips" : {
|
||||
"comment" : "Title of a section in the Rules Help view focused on strategy tips.",
|
||||
"localizations" : {
|
||||
@ -3623,6 +3558,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Tap Deal to start the round" : {
|
||||
|
||||
},
|
||||
"The hand closest to 9 wins" : {
|
||||
|
||||
},
|
||||
"The hand closest to 9 wins." : {
|
||||
"comment" : "Explanation of how the hand closest to 9 wins in baccarat.",
|
||||
@ -3647,6 +3588,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"The History shows all previous round results at a glance." : {
|
||||
"comment" : "Explains the purpose of the history display.",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "The History shows all previous round results at a glance."
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "El Historial muestra todos los resultados de rondas anteriores de un vistazo."
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "L'historique affiche tous les résultats des tours précédents en un coup d'œil."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"There's no skill involved — just enjoy the game!" : {
|
||||
"comment" : "Tip for players on how to play baccarat without needing any skill.",
|
||||
"localizations" : {
|
||||
@ -3878,6 +3842,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Track Patterns" : {
|
||||
|
||||
},
|
||||
"Two Naturals of the same value result in a Tie." : {
|
||||
"comment" : "Text describing the outcome when two players both have a Natural (a total of 8 or 9 with two cards).",
|
||||
@ -3925,6 +3892,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Use History to spot patterns and trends in the shoe." : {
|
||||
"comment" : "Advises the player on how to use the history feature.",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Use History to spot patterns and trends in the shoe."
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Usa el Historial para detectar patrones y tendencias en el zapato."
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Utilisez l'historique pour repérer les modèles et les tendances dans le sabot."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Vibration for actions and results" : {
|
||||
"comment" : "Subtitle describing haptic feedback toggle.",
|
||||
"localizations" : {
|
||||
@ -4132,6 +4122,52 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Yellow Dot (bottom-left): A pair occurred in that hand" : {
|
||||
"comment" : "Explains the yellow dot marker in the history.",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Yellow Dot (bottom-left): A pair occurred in that hand"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Punto Amarillo (abajo-izquierda): Hubo un par en esa mano"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Point Jaune (en bas à gauche) : Une paire s'est produite dans cette main"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Yellow Star (top-right): Natural 8 or 9 win" : {
|
||||
"comment" : "Explains the yellow star marker in the history.",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Yellow Star (top-right): Natural 8 or 9 win"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Estrella Amarilla (arriba-derecha): Victoria natural con 8 o 9"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Étoile Jaune (en haut à droite) : Victoire naturelle avec 8 ou 9"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"You've run out of chips!" : {
|
||||
"comment" : "A message displayed when a player runs out of money in the game over screen.",
|
||||
"extractionState" : "stale",
|
||||
|
||||
@ -16,6 +16,12 @@ struct GameTableView: View {
|
||||
@State private var showSettings = false
|
||||
@State private var showRules = false
|
||||
@State private var showStats = false
|
||||
@State private var showWelcome = false
|
||||
|
||||
// MARK: - Onboarding State
|
||||
|
||||
/// Tooltip manager for contextual hints
|
||||
@State private var tooltipManager: TooltipManager?
|
||||
|
||||
/// Screen size for card sizing (measured from TableBackgroundView)
|
||||
@State private var screenSize: CGSize = CGSize(width: 375, height: 667)
|
||||
@ -103,6 +109,10 @@ struct GameTableView: View {
|
||||
if gameState == nil {
|
||||
gameState = GameState(settings: settings)
|
||||
}
|
||||
if tooltipManager == nil {
|
||||
tooltipManager = TooltipManager(onboarding: state.onboarding)
|
||||
}
|
||||
checkForWelcomeSheet()
|
||||
}
|
||||
.sheet(isPresented: $showSettings) {
|
||||
if let state = gameState {
|
||||
@ -111,12 +121,112 @@ struct GameTableView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: showSettings) { wasShowing, isShowing in
|
||||
// When settings sheet dismisses, check if we should show welcome
|
||||
if wasShowing && !isShowing {
|
||||
checkForWelcomeSheet()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showRules) {
|
||||
RulesHelpView()
|
||||
}
|
||||
.sheet(isPresented: $showStats) {
|
||||
StatisticsSheetView(results: state.roundHistory)
|
||||
}
|
||||
.sheet(isPresented: $showWelcome) {
|
||||
WelcomeSheet(
|
||||
gameName: "Baccarat",
|
||||
features: [
|
||||
WelcomeFeature(
|
||||
icon: "hand.raised.fill",
|
||||
title: String(localized: "Bet on Player, Banker, or Tie"),
|
||||
description: String(localized: "The hand closest to 9 wins")
|
||||
),
|
||||
WelcomeFeature(
|
||||
icon: "chart.line.uptrend.xyaxis",
|
||||
title: String(localized: "Track Patterns"),
|
||||
description: String(localized: "Road maps show game history and trends")
|
||||
),
|
||||
WelcomeFeature(
|
||||
icon: "dollarsign.circle",
|
||||
title: String(localized: "Practice Free"),
|
||||
description: String(localized: "Start with $1,000 and play risk-free")
|
||||
),
|
||||
WelcomeFeature(
|
||||
icon: "gearshape.fill",
|
||||
title: String(localized: "Customize Settings"),
|
||||
description: String(localized: "Change table limits and display options")
|
||||
)
|
||||
],
|
||||
onStartTutorial: {
|
||||
showWelcome = false
|
||||
state.onboarding.completeWelcome()
|
||||
checkOnboardingHints()
|
||||
},
|
||||
onStartPlaying: {
|
||||
showWelcome = false
|
||||
state.onboarding.completeWelcome()
|
||||
}
|
||||
)
|
||||
}
|
||||
.onChange(of: state.totalBetAmount) { _, newTotal in
|
||||
if newTotal > 0, state.onboarding.shouldShowHint("dealButton") {
|
||||
showDealHintWithDelay()
|
||||
}
|
||||
}
|
||||
.onChange(of: state.currentPhase) { oldPhase, newPhase in
|
||||
if newPhase == .showingResult, oldPhase != newPhase {
|
||||
if state.onboarding.shouldShowHint("firstResult") {
|
||||
showResultHintWithDelay()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic tooltip display
|
||||
.dynamicTooltip(tooltipManager ?? TooltipManager(onboarding: state.onboarding))
|
||||
}
|
||||
|
||||
// MARK: - Onboarding Helpers
|
||||
|
||||
private func checkForWelcomeSheet() {
|
||||
if !state.onboarding.hasCompletedWelcome {
|
||||
Task { @MainActor in
|
||||
try? await Task.sleep(for: .milliseconds(500))
|
||||
showWelcome = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func checkOnboardingHints() {
|
||||
if state.onboarding.shouldShowHint("bettingZone") {
|
||||
tooltipManager?.show(
|
||||
key: "bettingZone",
|
||||
message: String(localized: "Select a chip and tap a bet zone"),
|
||||
icon: "hand.tap.fill",
|
||||
position: .bottom,
|
||||
delay: 1.0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func showDealHintWithDelay() {
|
||||
tooltipManager?.show(
|
||||
key: "dealButton",
|
||||
message: String(localized: "Tap Deal to start the round"),
|
||||
icon: "play.fill",
|
||||
position: .bottom,
|
||||
delay: 0.5
|
||||
)
|
||||
}
|
||||
|
||||
private func showResultHintWithDelay() {
|
||||
tooltipManager?.show(
|
||||
key: "firstResult",
|
||||
message: String(localized: "Results appear here, then in the road maps below"),
|
||||
icon: "chart.bar.fill",
|
||||
position: .bottom,
|
||||
delay: 2.0
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Private Views
|
||||
|
||||
@ -329,7 +329,28 @@ struct SettingsView: View {
|
||||
.padding(.horizontal)
|
||||
.padding(.top, Design.Spacing.small)
|
||||
|
||||
// 9. App Version
|
||||
// 10. Show Welcome Again (Reset Onboarding)
|
||||
Button {
|
||||
gameState.onboarding.reset()
|
||||
dismiss()
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "hand.wave")
|
||||
Text(String(localized: "Show Welcome Again"))
|
||||
}
|
||||
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
||||
.fill(Color.white.opacity(Design.Opacity.subtle))
|
||||
)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top, Design.Spacing.xSmall)
|
||||
|
||||
// 11. App Version
|
||||
Text(appVersionString)
|
||||
.font(.system(size: Design.BaseFontSize.callout))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.light))
|
||||
|
||||
@ -8,6 +8,12 @@ A feature-rich Baccarat (Punto Banco) app for iOS built with SwiftUI. Experience
|
||||
|
||||
## Features
|
||||
|
||||
### 🎓 First-Time User Experience
|
||||
- **Welcome Sheet** — Interactive introduction on first launch
|
||||
- **Tutorial Mode** — Optional guided walkthrough of your first round
|
||||
- **Contextual Hints** — Tips appear at the right moment during gameplay
|
||||
- **Never Intrusive** — All onboarding is skippable and shown only once
|
||||
|
||||
### 🎰 Authentic Punto Banco Gameplay
|
||||
- Complete Baccarat rules with automatic third card logic
|
||||
- Natural detection (8 or 9 on initial deal)
|
||||
|
||||
@ -126,6 +126,9 @@ final class GameState {
|
||||
/// Game settings.
|
||||
let settings: GameSettings
|
||||
|
||||
/// Onboarding state for first-time users.
|
||||
let onboarding: OnboardingState
|
||||
|
||||
/// Sound manager.
|
||||
private let sound = SoundManager.shared
|
||||
|
||||
@ -323,6 +326,7 @@ final class GameState {
|
||||
self.settings = settings
|
||||
self.balance = settings.startingBalance
|
||||
self.engine = BlackjackEngine(settings: settings)
|
||||
self.onboarding = OnboardingState(gameIdentifier: "blackjack")
|
||||
self.persistence = CloudSyncManager<BlackjackGameData>()
|
||||
syncSoundSettings()
|
||||
loadSavedGame()
|
||||
|
||||
@ -1169,6 +1169,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Beat the Dealer" : {
|
||||
|
||||
},
|
||||
"Beat the dealer by getting a hand value closer to 21 without going over." : {
|
||||
"comment" : "Text for the objective of the game.",
|
||||
@ -1658,6 +1661,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Built-in hints show optimal plays based on basic strategy" : {
|
||||
|
||||
},
|
||||
"BUST" : {
|
||||
"localizations" : {
|
||||
@ -1856,6 +1862,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Change table limits, rules, and side bets in settings" : {
|
||||
|
||||
},
|
||||
"Chips, cards, and results" : {
|
||||
"localizations" : {
|
||||
@ -1878,6 +1887,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Choose your action based on the hint above" : {
|
||||
|
||||
},
|
||||
"Clear" : {
|
||||
"localizations" : {
|
||||
@ -2196,6 +2208,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Customize Rules" : {
|
||||
|
||||
},
|
||||
"DATA" : {
|
||||
"localizations" : {
|
||||
@ -3392,6 +3407,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Get closer to 21 than the dealer without going over" : {
|
||||
|
||||
},
|
||||
"H17 rule, increases house edge" : {
|
||||
"localizations" : {
|
||||
@ -4097,6 +4115,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Learn Strategy" : {
|
||||
|
||||
},
|
||||
"LEGAL" : {
|
||||
"localizations" : {
|
||||
@ -5100,6 +5121,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Practice Free" : {
|
||||
|
||||
},
|
||||
"Privacy Policy" : {
|
||||
"localizations" : {
|
||||
@ -5529,6 +5553,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Select a chip and tap the bet area" : {
|
||||
|
||||
},
|
||||
"SESSION SUMMARY" : {
|
||||
"localizations" : {
|
||||
@ -5737,6 +5764,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Show Welcome Again" : {
|
||||
|
||||
},
|
||||
"Side Bets" : {
|
||||
"localizations" : {
|
||||
@ -6167,6 +6197,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Start with $1,000 and play risk-free" : {
|
||||
|
||||
},
|
||||
"STARTING BALANCE" : {
|
||||
"localizations" : {
|
||||
@ -6573,6 +6606,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Tap Deal to start the round" : {
|
||||
|
||||
},
|
||||
"TAP TO BET" : {
|
||||
"localizations" : {
|
||||
|
||||
@ -18,6 +18,12 @@ struct GameTableView: View {
|
||||
@State private var showSettings = false
|
||||
@State private var showRules = false
|
||||
@State private var showStats = false
|
||||
@State private var showWelcome = false
|
||||
|
||||
// MARK: - Onboarding State
|
||||
|
||||
/// Tooltip manager for contextual hints
|
||||
@State private var tooltipManager: TooltipManager?
|
||||
|
||||
/// Screen size for card sizing (measured from TableBackgroundView)
|
||||
@State private var screenSize: CGSize = CGSize(width: 375, height: 667)
|
||||
@ -55,16 +61,74 @@ struct GameTableView: View {
|
||||
if gameState == nil {
|
||||
gameState = GameState(settings: settings)
|
||||
}
|
||||
if tooltipManager == nil {
|
||||
tooltipManager = TooltipManager(onboarding: state.onboarding)
|
||||
}
|
||||
checkForWelcomeSheet()
|
||||
}
|
||||
.sheet(isPresented: $showSettings) {
|
||||
SettingsView(settings: settings, gameState: gameState)
|
||||
}
|
||||
.onChange(of: showSettings) { wasShowing, isShowing in
|
||||
// When settings sheet dismisses, check if we should show welcome
|
||||
if wasShowing && !isShowing {
|
||||
checkForWelcomeSheet()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showRules) {
|
||||
RulesHelpView()
|
||||
}
|
||||
.sheet(isPresented: $showStats) {
|
||||
StatisticsSheetView(state: state)
|
||||
}
|
||||
.sheet(isPresented: $showWelcome) {
|
||||
WelcomeSheet(
|
||||
gameName: "Blackjack",
|
||||
features: [
|
||||
WelcomeFeature(
|
||||
icon: "target",
|
||||
title: String(localized: "Beat the Dealer"),
|
||||
description: String(localized: "Get closer to 21 than the dealer without going over")
|
||||
),
|
||||
WelcomeFeature(
|
||||
icon: "lightbulb.fill",
|
||||
title: String(localized: "Learn Strategy"),
|
||||
description: String(localized: "Built-in hints show optimal plays based on basic strategy")
|
||||
),
|
||||
WelcomeFeature(
|
||||
icon: "dollarsign.circle",
|
||||
title: String(localized: "Practice Free"),
|
||||
description: String(localized: "Start with $1,000 and play risk-free")
|
||||
),
|
||||
WelcomeFeature(
|
||||
icon: "gearshape.fill",
|
||||
title: String(localized: "Customize Rules"),
|
||||
description: String(localized: "Change table limits, rules, and side bets in settings")
|
||||
)
|
||||
],
|
||||
onStartTutorial: {
|
||||
showWelcome = false
|
||||
state.onboarding.completeWelcome()
|
||||
checkOnboardingHints()
|
||||
},
|
||||
onStartPlaying: {
|
||||
showWelcome = false
|
||||
state.onboarding.completeWelcome()
|
||||
}
|
||||
)
|
||||
}
|
||||
.onChange(of: state.currentBet) { _, newBet in
|
||||
if newBet > 0, state.onboarding.shouldShowHint("dealButton") {
|
||||
showDealHintWithDelay()
|
||||
}
|
||||
}
|
||||
.onChange(of: state.currentPhase) { oldPhase, newPhase in
|
||||
if case .playerTurn = newPhase, oldPhase != newPhase {
|
||||
if state.onboarding.shouldShowHint("playerActions") {
|
||||
showActionsHintWithDelay()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use global debug flag from Design constants
|
||||
@ -248,6 +312,54 @@ struct GameTableView: View {
|
||||
.onChange(of: state.balance) { oldBalance, newBalance in
|
||||
Design.debugLog("💰 Balance: \(oldBalance) → \(newBalance)")
|
||||
}
|
||||
|
||||
// Dynamic tooltip display
|
||||
.dynamicTooltip(tooltipManager ?? TooltipManager(onboarding: state.onboarding))
|
||||
}
|
||||
|
||||
// MARK: - Onboarding Helpers
|
||||
|
||||
private func checkForWelcomeSheet() {
|
||||
if !state.onboarding.hasCompletedWelcome {
|
||||
// Delay slightly so view has time to layout
|
||||
Task { @MainActor in
|
||||
try? await Task.sleep(for: .milliseconds(500))
|
||||
showWelcome = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func checkOnboardingHints() {
|
||||
// Show betting hint if not yet shown
|
||||
if state.onboarding.shouldShowHint("bettingZone") {
|
||||
tooltipManager?.show(
|
||||
key: "bettingZone",
|
||||
message: String(localized: "Select a chip and tap the bet area"),
|
||||
icon: "hand.tap.fill",
|
||||
position: .bottom,
|
||||
delay: 1.0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func showDealHintWithDelay() {
|
||||
tooltipManager?.show(
|
||||
key: "dealButton",
|
||||
message: String(localized: "Tap Deal to start the round"),
|
||||
icon: "play.fill",
|
||||
position: .bottom,
|
||||
delay: 0.5
|
||||
)
|
||||
}
|
||||
|
||||
private func showActionsHintWithDelay() {
|
||||
tooltipManager?.show(
|
||||
key: "playerActions",
|
||||
message: String(localized: "Choose your action based on the hint above"),
|
||||
icon: "hand.point.up.left.fill",
|
||||
position: .bottom,
|
||||
delay: 1.0
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -385,7 +385,30 @@ struct SettingsView: View {
|
||||
.padding(.horizontal)
|
||||
.padding(.top, Design.Spacing.small)
|
||||
|
||||
// 12. Version info
|
||||
// 12. Show Welcome Again (Reset Onboarding)
|
||||
if let state = gameState {
|
||||
Button {
|
||||
state.onboarding.reset()
|
||||
dismiss()
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "hand.wave")
|
||||
Text(String(localized: "Show Welcome Again"))
|
||||
}
|
||||
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
||||
.fill(Color.white.opacity(Design.Opacity.subtle))
|
||||
)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top, Design.Spacing.xSmall)
|
||||
}
|
||||
|
||||
// 13. Version info
|
||||
Text(appVersionString)
|
||||
.font(.system(size: Design.BaseFontSize.small))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.light))
|
||||
|
||||
@ -8,6 +8,12 @@ A feature-rich Blackjack app for iOS built with SwiftUI. Master basic strategy,
|
||||
|
||||
## Features
|
||||
|
||||
### 🎓 First-Time User Experience
|
||||
- **Welcome Sheet** — Interactive introduction on first launch
|
||||
- **Tutorial Mode** — Optional guided walkthrough of your first hand
|
||||
- **Contextual Hints** — Tips appear at the right moment during gameplay
|
||||
- **Never Intrusive** — All onboarding is skippable and shown only once
|
||||
|
||||
### 🎰 Authentic Casino Gameplay
|
||||
- Full Blackjack gameplay with all standard actions: Hit, Stand, Double Down, Split, Surrender
|
||||
- Insurance betting when dealer shows an Ace
|
||||
|
||||
@ -110,6 +110,125 @@ SheetSection(title: "SECTION TITLE", icon: "star.fill") {
|
||||
}
|
||||
```
|
||||
|
||||
### 🎓 Onboarding & Tutorials
|
||||
|
||||
**WelcomeSheet** - First-launch welcome screen with features list.
|
||||
|
||||
```swift
|
||||
WelcomeSheet(
|
||||
gameName: "Blackjack",
|
||||
features: [
|
||||
WelcomeFeature(
|
||||
icon: "target",
|
||||
title: "Beat the Dealer",
|
||||
description: "Get closer to 21 than the dealer without going over"
|
||||
),
|
||||
WelcomeFeature(
|
||||
icon: "lightbulb.fill",
|
||||
title: "Learn Strategy",
|
||||
description: "Built-in hints show optimal plays"
|
||||
)
|
||||
],
|
||||
onStartTutorial: {
|
||||
// Enable tutorial mode
|
||||
gameState.onboarding.startTutorialMode()
|
||||
},
|
||||
onStartPlaying: {
|
||||
// Skip to game
|
||||
gameState.onboarding.completeWelcome()
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**ContextualTooltip** - Show hints at the right moment.
|
||||
|
||||
```swift
|
||||
@State private var showBettingHint = false
|
||||
|
||||
var body: some View {
|
||||
BettingZone(/* ... */)
|
||||
.contextualTooltip(
|
||||
"Tap chips, then tap here to bet",
|
||||
icon: "hand.tap.fill",
|
||||
position: .bottom,
|
||||
isShowing: $showBettingHint,
|
||||
onDismiss: {
|
||||
gameState.onboarding.markHintShown("bettingZone")
|
||||
}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**OnboardingState** - Track which hints have been shown.
|
||||
|
||||
```swift
|
||||
let onboarding = OnboardingState(gameIdentifier: "blackjack")
|
||||
|
||||
// Check if a hint should be shown
|
||||
if onboarding.shouldShowHint("bettingZone") {
|
||||
showBettingHint = true
|
||||
}
|
||||
|
||||
// Mark hint as shown (persisted automatically)
|
||||
onboarding.markHintShown("bettingZone")
|
||||
|
||||
// Check if user has seen welcome
|
||||
if !onboarding.hasCompletedWelcome {
|
||||
showWelcome = true
|
||||
}
|
||||
|
||||
// Enable tutorial mode (shows all hints again)
|
||||
onboarding.startTutorialMode()
|
||||
|
||||
// Reset onboarding (for testing)
|
||||
onboarding.reset()
|
||||
```
|
||||
|
||||
**TooltipManager** - Generic, scalable tooltip management (recommended).
|
||||
|
||||
```swift
|
||||
@State private var tooltipManager: TooltipManager?
|
||||
|
||||
var body: some View {
|
||||
GameView()
|
||||
.onAppear {
|
||||
tooltipManager = TooltipManager(onboarding: gameState.onboarding)
|
||||
}
|
||||
.dynamicTooltip(tooltipManager ?? TooltipManager(onboarding: gameState.onboarding))
|
||||
}
|
||||
|
||||
// Show a tooltip with automatic dismiss tracking
|
||||
private func showBettingHint() {
|
||||
tooltipManager?.show(
|
||||
key: "bettingZone",
|
||||
message: "Select a chip and tap here to bet",
|
||||
icon: "hand.tap.fill",
|
||||
position: .bottom,
|
||||
delay: 1.0
|
||||
)
|
||||
}
|
||||
|
||||
// Dismiss current tooltip manually
|
||||
tooltipManager?.dismiss()
|
||||
```
|
||||
|
||||
The `TooltipManager` automatically:
|
||||
- Ensures only one tooltip shows at a time
|
||||
- Marks tooltips as shown when dismissed or replaced
|
||||
- Respects `OnboardingState.shouldShowHint()` checks
|
||||
- Handles delayed presentation with cancellation if needed
|
||||
|
||||
**PulsingModifier** - Draw attention to interactive elements.
|
||||
|
||||
```swift
|
||||
Button("Deal", action: deal)
|
||||
.pulsing(
|
||||
isActive: shouldHighlight,
|
||||
color: .white,
|
||||
scale: 1.3
|
||||
)
|
||||
```
|
||||
|
||||
### 🎲 Game Table Components
|
||||
|
||||
**TableBackgroundView** - Casino felt background with pattern.
|
||||
@ -569,7 +688,9 @@ CasinoKit/
|
||||
│ │ ├── Card.swift
|
||||
│ │ ├── Deck.swift
|
||||
│ │ ├── ChipDenomination.swift
|
||||
│ │ └── TableLimits.swift # Betting limit presets
|
||||
│ │ ├── TableLimits.swift # Betting limit presets
|
||||
│ │ ├── OnboardingState.swift # Onboarding tracking
|
||||
│ │ └── TooltipManager.swift # Tooltip management
|
||||
│ ├── Views/
|
||||
│ │ ├── Cards/
|
||||
│ │ │ ├── CardView.swift
|
||||
@ -581,6 +702,10 @@ CasinoKit/
|
||||
│ │ │ └── ChipOnTableView.swift
|
||||
│ │ ├── Sheets/
|
||||
│ │ │ └── SheetContainerView.swift
|
||||
│ │ ├── Onboarding/
|
||||
│ │ │ ├── WelcomeSheet.swift # First-launch welcome
|
||||
│ │ │ ├── ContextualTooltip.swift # In-game hints
|
||||
│ │ │ └── PulsingModifier.swift # Attention-grabbing pulse
|
||||
│ │ ├── Branding/
|
||||
│ │ │ ├── AppIconView.swift
|
||||
│ │ │ ├── LaunchScreenView.swift
|
||||
@ -663,7 +788,8 @@ private var fontSize: CGFloat = CasinoDesign.BaseFontSize.body
|
||||
|
||||
## Apps Using CasinoKit
|
||||
|
||||
- **Baccarat** - The classic casino card game
|
||||
- **Blackjack** - Classic 21 with basic strategy hints and card counting
|
||||
- **Baccarat** - The classic casino card game with road maps
|
||||
|
||||
## Version History
|
||||
|
||||
|
||||
@ -13,6 +13,8 @@
|
||||
// - Deck
|
||||
// - ChipDenomination
|
||||
// - TableLimits
|
||||
// - OnboardingState
|
||||
// - TooltipManager, TooltipConfig
|
||||
|
||||
// MARK: - Views
|
||||
// - CardView, CardFrontView, CardBackView, CardPlaceholderView
|
||||
@ -21,6 +23,9 @@
|
||||
// - ChipSelectorView
|
||||
// - ChipStackView, ChipOnTableView
|
||||
// - SheetContainerView, SheetSection
|
||||
// - WelcomeSheet, WelcomeFeature
|
||||
// - ContextualTooltip, ContextualTooltipModifier
|
||||
// - PulsingModifier
|
||||
|
||||
// MARK: - Effects
|
||||
// - ConfettiView, ConfettiPiece
|
||||
|
||||
96
CasinoKit/Sources/CasinoKit/Models/OnboardingState.swift
Normal file
96
CasinoKit/Sources/CasinoKit/Models/OnboardingState.swift
Normal file
@ -0,0 +1,96 @@
|
||||
//
|
||||
// OnboardingState.swift
|
||||
// CasinoKit
|
||||
//
|
||||
// Tracks first-time user onboarding progress and contextual hints.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/// Observable state for managing user onboarding and progressive feature discovery.
|
||||
@Observable
|
||||
@MainActor
|
||||
public final class OnboardingState {
|
||||
// MARK: - Properties
|
||||
|
||||
/// Whether the user has launched the app before.
|
||||
public var hasLaunchedBefore: Bool = false
|
||||
|
||||
/// Whether the user has completed the welcome sheet.
|
||||
public var hasCompletedWelcome: Bool = false
|
||||
|
||||
/// Whether the user is in tutorial mode (shows all contextual hints).
|
||||
public var isTutorialMode: Bool = false
|
||||
|
||||
/// Set of hint keys that have been shown to the user.
|
||||
public var hintsShown: Set<String> = []
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
private let persistenceKey: String
|
||||
|
||||
public init(gameIdentifier: String) {
|
||||
self.persistenceKey = "onboarding.\(gameIdentifier)"
|
||||
load()
|
||||
}
|
||||
|
||||
// MARK: - Hint Management
|
||||
|
||||
/// Marks a hint as shown and persists the state.
|
||||
public func markHintShown(_ key: String) {
|
||||
hintsShown.insert(key)
|
||||
save()
|
||||
}
|
||||
|
||||
/// Returns whether a specific hint should be shown.
|
||||
public func shouldShowHint(_ key: String) -> Bool {
|
||||
isTutorialMode || !hintsShown.contains(key)
|
||||
}
|
||||
|
||||
/// Marks the welcome sheet as completed.
|
||||
public func completeWelcome() {
|
||||
hasLaunchedBefore = true
|
||||
hasCompletedWelcome = true
|
||||
save()
|
||||
}
|
||||
|
||||
/// Enables tutorial mode (shows all hints again).
|
||||
public func startTutorialMode() {
|
||||
isTutorialMode = true
|
||||
}
|
||||
|
||||
/// Disables tutorial mode.
|
||||
public func endTutorialMode() {
|
||||
isTutorialMode = false
|
||||
}
|
||||
|
||||
/// Resets all onboarding state (for testing or user-requested reset).
|
||||
public func reset() {
|
||||
hasLaunchedBefore = false
|
||||
hasCompletedWelcome = false
|
||||
isTutorialMode = false
|
||||
hintsShown.removeAll()
|
||||
save()
|
||||
}
|
||||
|
||||
// MARK: - Persistence
|
||||
|
||||
private func load() {
|
||||
let defaults = UserDefaults.standard
|
||||
hasLaunchedBefore = defaults.bool(forKey: "\(persistenceKey).hasLaunched")
|
||||
hasCompletedWelcome = defaults.bool(forKey: "\(persistenceKey).hasCompletedWelcome")
|
||||
|
||||
if let hintsData = defaults.array(forKey: "\(persistenceKey).hintsShown") as? [String] {
|
||||
hintsShown = Set(hintsData)
|
||||
}
|
||||
}
|
||||
|
||||
private func save() {
|
||||
let defaults = UserDefaults.standard
|
||||
defaults.set(hasLaunchedBefore, forKey: "\(persistenceKey).hasLaunched")
|
||||
defaults.set(hasCompletedWelcome, forKey: "\(persistenceKey).hasCompletedWelcome")
|
||||
defaults.set(Array(hintsShown), forKey: "\(persistenceKey).hintsShown")
|
||||
}
|
||||
}
|
||||
|
||||
114
CasinoKit/Sources/CasinoKit/Models/TooltipManager.swift
Normal file
114
CasinoKit/Sources/CasinoKit/Models/TooltipManager.swift
Normal file
@ -0,0 +1,114 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1026,6 +1026,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Got it" : {
|
||||
|
||||
},
|
||||
"Hearts" : {
|
||||
"localizations" : {
|
||||
@ -1782,6 +1785,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Show Me How" : {
|
||||
|
||||
},
|
||||
"Six" : {
|
||||
"localizations" : {
|
||||
@ -1872,6 +1878,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Start Playing" : {
|
||||
|
||||
},
|
||||
"Statistics" : {
|
||||
"localizations" : {
|
||||
@ -2170,6 +2179,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Welcome to %@!" : {
|
||||
|
||||
},
|
||||
"WIN" : {
|
||||
"extractionState" : "stale",
|
||||
|
||||
@ -38,16 +38,10 @@ public final class CloudSyncManager<T: PersistableGameData> {
|
||||
let token = FileManager.default.ubiquityIdentityToken
|
||||
let available = token != nil
|
||||
|
||||
// Additional check: verify we can actually access ubiquity container
|
||||
// This prevents false positives in simulators
|
||||
if available {
|
||||
let containerURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)
|
||||
let actuallyAvailable = containerURL != nil
|
||||
CasinoDesign.debugLog("CloudSyncManager: iCloud token = \(String(describing: token)), container = \(String(describing: containerURL))")
|
||||
return actuallyAvailable
|
||||
}
|
||||
|
||||
CasinoDesign.debugLog("CloudSyncManager: iCloud available = \(available), token = \(String(describing: token))")
|
||||
// Note: NSUbiquitousKeyValueStore only requires iCloud sign-in (token)
|
||||
// It does NOT require iCloud Drive/Documents to be enabled
|
||||
// So we only check for the token, not the container
|
||||
CasinoDesign.debugLog("CloudSyncManager: iCloud available = \(available), token = \(token != nil ? "present" : "nil")")
|
||||
return available
|
||||
}
|
||||
|
||||
|
||||
164
CasinoKit/Sources/CasinoKit/Views/ContextualTooltip.swift
Normal file
164
CasinoKit/Sources/CasinoKit/Views/ContextualTooltip.swift
Normal file
@ -0,0 +1,164 @@
|
||||
//
|
||||
// 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)
|
||||
)
|
||||
}
|
||||
|
||||
70
CasinoKit/Sources/CasinoKit/Views/PulsingModifier.swift
Normal file
70
CasinoKit/Sources/CasinoKit/Views/PulsingModifier.swift
Normal file
@ -0,0 +1,70 @@
|
||||
//
|
||||
// PulsingModifier.swift
|
||||
// CasinoKit
|
||||
//
|
||||
// Visual hint modifier that pulses to draw attention.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// Modifier that adds a pulsing animation to draw attention to interactive elements.
|
||||
public struct PulsingModifier: ViewModifier {
|
||||
let isActive: Bool
|
||||
let color: Color
|
||||
let scale: CGFloat
|
||||
|
||||
@State private var animationAmount: CGFloat = 1.0
|
||||
|
||||
public func body(content: Content) -> some View {
|
||||
content
|
||||
.overlay {
|
||||
if isActive {
|
||||
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.large)
|
||||
.stroke(color, lineWidth: CasinoDesign.LineWidth.medium)
|
||||
.scaleEffect(animationAmount)
|
||||
.opacity(2 - animationAmount)
|
||||
.animation(
|
||||
.easeInOut(duration: 1.5)
|
||||
.repeatForever(autoreverses: false),
|
||||
value: animationAmount
|
||||
)
|
||||
.onAppear {
|
||||
animationAmount = scale
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
/// Adds a pulsing animation to draw attention to the view.
|
||||
func pulsing(
|
||||
isActive: Bool,
|
||||
color: Color = .white,
|
||||
scale: CGFloat = 1.3
|
||||
) -> some View {
|
||||
modifier(PulsingModifier(isActive: isActive, color: color, scale: scale))
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ZStack {
|
||||
Color.black
|
||||
|
||||
VStack(spacing: CasinoDesign.Spacing.xLarge) {
|
||||
Button("Tap Here") {
|
||||
// Action
|
||||
}
|
||||
.padding()
|
||||
.background(Color.Sheet.accent)
|
||||
.foregroundStyle(.black)
|
||||
.clipShape(.rect(cornerRadius: CasinoDesign.CornerRadius.medium))
|
||||
.pulsing(isActive: true, color: .white)
|
||||
|
||||
Text("Pulsing highlights interactive areas")
|
||||
.foregroundStyle(.white)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
174
CasinoKit/Sources/CasinoKit/Views/WelcomeSheet.swift
Normal file
174
CasinoKit/Sources/CasinoKit/Views/WelcomeSheet.swift
Normal file
@ -0,0 +1,174 @@
|
||||
//
|
||||
// WelcomeSheet.swift
|
||||
// CasinoKit
|
||||
//
|
||||
// First-launch welcome sheet for casino games.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// Welcome sheet shown on first launch of a game.
|
||||
public struct WelcomeSheet: View {
|
||||
let gameName: String
|
||||
let features: [WelcomeFeature]
|
||||
let onStartTutorial: () -> Void
|
||||
let onStartPlaying: () -> Void
|
||||
|
||||
@ScaledMetric(relativeTo: .largeTitle) private var titleSize: CGFloat = CasinoDesign.BaseFontSize.xxLarge
|
||||
@ScaledMetric(relativeTo: .title2) private var featureTitleSize: CGFloat = CasinoDesign.BaseFontSize.subheadline
|
||||
@ScaledMetric(relativeTo: .body) private var bodySize: CGFloat = CasinoDesign.BaseFontSize.body
|
||||
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = CasinoDesign.IconSize.large
|
||||
@ScaledMetric(relativeTo: .body) private var buttonPadding: CGFloat = CasinoDesign.Spacing.medium
|
||||
|
||||
public init(
|
||||
gameName: String,
|
||||
features: [WelcomeFeature],
|
||||
onStartTutorial: @escaping () -> Void,
|
||||
onStartPlaying: @escaping () -> Void
|
||||
) {
|
||||
self.gameName = gameName
|
||||
self.features = features
|
||||
self.onStartTutorial = onStartTutorial
|
||||
self.onStartPlaying = onStartPlaying
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
SheetContainerView(
|
||||
title: String(localized: "Welcome to \(gameName)!", bundle: .module),
|
||||
content: {
|
||||
ScrollView {
|
||||
VStack(spacing: CasinoDesign.Spacing.xLarge) {
|
||||
// Game icon/emoji
|
||||
Text(gameEmoji)
|
||||
.font(.system(size: 60))
|
||||
.padding(.top, CasinoDesign.Spacing.medium)
|
||||
|
||||
// Features list
|
||||
VStack(spacing: CasinoDesign.Spacing.large) {
|
||||
ForEach(features) { feature in
|
||||
FeatureRow(
|
||||
feature: feature,
|
||||
iconSize: iconSize,
|
||||
titleSize: featureTitleSize,
|
||||
bodySize: bodySize
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
VStack(spacing: CasinoDesign.Spacing.medium) {
|
||||
Button(action: onStartTutorial) {
|
||||
HStack {
|
||||
Image(systemName: "play.circle.fill")
|
||||
Text("Show Me How")
|
||||
}
|
||||
.font(.system(size: bodySize, weight: .semibold))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(buttonPadding)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.large)
|
||||
.fill(Color.Sheet.accent)
|
||||
)
|
||||
.foregroundStyle(.black)
|
||||
}
|
||||
|
||||
Button(action: onStartPlaying) {
|
||||
Text("Start Playing")
|
||||
.font(.system(size: bodySize, weight: .medium))
|
||||
.foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium))
|
||||
}
|
||||
}
|
||||
.padding(.top, CasinoDesign.Spacing.large)
|
||||
.padding(.bottom, CasinoDesign.Spacing.xLarge)
|
||||
}
|
||||
.padding(.horizontal, CasinoDesign.Spacing.large)
|
||||
}
|
||||
},
|
||||
onDone: {}
|
||||
)
|
||||
}
|
||||
|
||||
private var gameEmoji: String {
|
||||
switch gameName.lowercased() {
|
||||
case "blackjack": return "🃏"
|
||||
case "baccarat": return "🎴"
|
||||
default: return "🎰"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Feature Row
|
||||
|
||||
private struct FeatureRow: View {
|
||||
let feature: WelcomeFeature
|
||||
let iconSize: CGFloat
|
||||
let titleSize: CGFloat
|
||||
let bodySize: CGFloat
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: CasinoDesign.Spacing.medium) {
|
||||
// Icon
|
||||
Image(systemName: feature.icon)
|
||||
.font(.system(size: iconSize))
|
||||
.foregroundStyle(Color.Sheet.accent)
|
||||
.frame(width: 40, alignment: .center)
|
||||
|
||||
// Text
|
||||
VStack(alignment: .leading, spacing: CasinoDesign.Spacing.xxSmall) {
|
||||
Text(feature.title)
|
||||
.font(.system(size: titleSize, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
|
||||
Text(feature.description)
|
||||
.font(.system(size: bodySize))
|
||||
.foregroundStyle(.white.opacity(CasinoDesign.Opacity.strong))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Welcome Feature Model
|
||||
|
||||
public struct WelcomeFeature: Identifiable {
|
||||
public let id = UUID()
|
||||
let icon: String
|
||||
let title: String
|
||||
let description: String
|
||||
|
||||
public init(icon: String, title: String, description: String) {
|
||||
self.icon = icon
|
||||
self.title = title
|
||||
self.description = description
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
WelcomeSheet(
|
||||
gameName: "Blackjack",
|
||||
features: [
|
||||
WelcomeFeature(
|
||||
icon: "target",
|
||||
title: "Beat the Dealer",
|
||||
description: "Get closer to 21 than the dealer without going over"
|
||||
),
|
||||
WelcomeFeature(
|
||||
icon: "lightbulb.fill",
|
||||
title: "Learn Strategy",
|
||||
description: "Built-in hints show optimal plays based on basic strategy"
|
||||
),
|
||||
WelcomeFeature(
|
||||
icon: "dollarsign.circle",
|
||||
title: "Practice Free",
|
||||
description: "Start with $1,000 and play risk-free"
|
||||
)
|
||||
],
|
||||
onStartTutorial: {},
|
||||
onStartPlaying: {}
|
||||
)
|
||||
}
|
||||
|
||||
230
ONBOARDING_IMPLEMENTATION.md
Normal file
230
ONBOARDING_IMPLEMENTATION.md
Normal file
@ -0,0 +1,230 @@
|
||||
# Onboarding System Implementation Summary
|
||||
|
||||
## 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.
|
||||
|
||||
## Components Created (in CasinoKit)
|
||||
|
||||
### 1. OnboardingState.swift
|
||||
- Tracks first-time user progress
|
||||
- Manages which hints have been shown
|
||||
- Persists state to UserDefaults
|
||||
- Supports tutorial mode (replay hints)
|
||||
- Game-specific identifiers (separate state per game)
|
||||
|
||||
**Key Methods:**
|
||||
- `shouldShowHint(key:)` - Check if hint should be displayed
|
||||
- `markHintShown(key:)` - Mark hint as seen (persisted)
|
||||
- `completeWelcome()` - Mark welcome sheet as completed
|
||||
- `startTutorialMode()` / `endTutorialMode()` - Enable/disable tutorial replay
|
||||
- `reset()` - Clear all onboarding data (for testing)
|
||||
|
||||
### 2. WelcomeSheet.swift
|
||||
- First-launch welcome screen
|
||||
- Lists key features with icons
|
||||
- Two CTAs: "Show Me How" (tutorial) or "Start Playing" (skip)
|
||||
- Automatically shows game emoji (🃏 for Blackjack, 🎴 for Baccarat)
|
||||
- Fully localized
|
||||
|
||||
### 3. ContextualTooltip.swift
|
||||
- In-game hint tooltips
|
||||
- Appears at the right moment
|
||||
- "Got it" button to dismiss
|
||||
- Animated entry/exit
|
||||
- Automatically marks hint as shown on dismiss
|
||||
- View modifier for easy integration
|
||||
|
||||
### 4. PulsingModifier.swift
|
||||
- Visual pulse animation to draw attention
|
||||
- Optional enhancement for interactive elements
|
||||
- Configurable color and scale
|
||||
- Currently available but not actively used (can be added later)
|
||||
|
||||
## Integration in Games
|
||||
|
||||
### Blackjack
|
||||
|
||||
**Welcome Sheet Features:**
|
||||
1. Beat the Dealer - Get closer to 21
|
||||
2. Learn Strategy - Built-in hints
|
||||
3. Practice Free - Start with $1,000
|
||||
4. Customize Rules - Change settings
|
||||
|
||||
**Contextual Hints:**
|
||||
- **Betting Zone**: "Tap chips, then tap here to bet" (when entering betting phase for first time)
|
||||
- **Deal Button**: "Tap Deal to start the round" (after placing first bet)
|
||||
- **Player Actions**: "Choose your action based on the hint above" (during first player turn)
|
||||
|
||||
### Baccarat
|
||||
|
||||
**Welcome Sheet Features:**
|
||||
1. Bet on Player, Banker, or Tie
|
||||
2. Track Patterns - Road maps show history
|
||||
3. Practice Free - Start with $1,000
|
||||
4. Customize Settings - Change table limits
|
||||
|
||||
**Contextual Hints:**
|
||||
- **Betting Zone**: "Tap chips, then tap a betting zone" (when entering betting phase for first time)
|
||||
- **Deal Button**: "Tap Deal to start the round" (after placing first bet)
|
||||
- **Result Display**: "Results appear here, then in the road maps below" (after first round completes)
|
||||
|
||||
## Settings Changes
|
||||
|
||||
Both games now default to more beginner-friendly settings:
|
||||
|
||||
| Setting | Old Default | New Default |
|
||||
|---------|-------------|-------------|
|
||||
| Table Limits | Low Stakes ($10-$1,000) | Casual ($5-$500) |
|
||||
| Starting Balance | $10,000 | $1,000 |
|
||||
|
||||
**Rationale:**
|
||||
- Lower minimum bet ($5) is less intimidating for beginners
|
||||
- $1,000 balance creates more meaningful decisions (not too much/little)
|
||||
- Still allows ~200 hands at $5/hand for good learning experience
|
||||
- Users can always increase in settings
|
||||
|
||||
## Bug Fix
|
||||
|
||||
**Blackjack Late Surrender:**
|
||||
- Fixed inconsistency where `resetToDefaults()` set it to `true` but Vegas preset overrode to `false`
|
||||
- Now correctly defaults to `false` to match Vegas rules
|
||||
|
||||
## User Flow
|
||||
|
||||
### First Launch:
|
||||
1. App loads
|
||||
2. Welcome sheet appears automatically after 500ms delay
|
||||
3. User chooses:
|
||||
- **"Show Me How"**: Enables tutorial mode, shows contextual hints during first round
|
||||
- **"Start Playing"**: Skips tutorial, hints still appear once naturally during gameplay
|
||||
|
||||
### Subsequent Launches:
|
||||
- Welcome sheet never shows again
|
||||
- Contextual hints appear once at the right moment
|
||||
- Each hint only shows once per hint key
|
||||
- Completely transparent to returning users
|
||||
|
||||
### Tutorial Mode:
|
||||
- User can replay tutorial by tapping "Show Me How" on welcome
|
||||
- All hints re-enabled for that session
|
||||
- Tutorial mode doesn't persist across app launches
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Hint Timing
|
||||
- Hints are shown with delays using `Task.sleep(for:)` on MainActor
|
||||
- Animations use spring duration for smooth transitions
|
||||
- onChange modifiers trigger hints based on game state changes
|
||||
|
||||
### Persistence
|
||||
- Uses UserDefaults with game-specific keys
|
||||
- Format: `"onboarding.{gameIdentifier}.{property}"`
|
||||
- Examples: `"onboarding.blackjack.hasLaunched"`, `"onboarding.baccarat.hintsShown"`
|
||||
|
||||
### Thread Safety
|
||||
- All onboarding state is @MainActor
|
||||
- Safe to use from SwiftUI views
|
||||
- No threading concerns
|
||||
|
||||
## Documentation Updates
|
||||
|
||||
### Blackjack README
|
||||
- Added "First-Time User Experience" section at top of features
|
||||
- Documents welcome sheet, tutorial mode, and contextual hints
|
||||
|
||||
### Baccarat README
|
||||
- Added "First-Time User Experience" section at top of features
|
||||
- Same structure as Blackjack for consistency
|
||||
|
||||
### CasinoKit README
|
||||
- New "Onboarding & Tutorials" section with full API documentation
|
||||
- Code examples for WelcomeSheet, ContextualTooltip, OnboardingState
|
||||
- Updated file structure to show new components
|
||||
- Added Blackjack to "Apps Using CasinoKit" list
|
||||
|
||||
## Best Practices Followed
|
||||
|
||||
✅ **Non-intrusive**: All onboarding is skippable
|
||||
✅ **One-time only**: Hints never show twice (unless tutorial mode)
|
||||
✅ **Right moment**: Hints appear contextually, not all at once
|
||||
✅ **Short & visual**: Messages are concise with icons
|
||||
✅ **Localized**: All strings use String Catalog
|
||||
✅ **Accessible**: Uses standard SwiftUI components
|
||||
✅ **Persistent**: User's progress is saved
|
||||
✅ **Game-specific**: Each game has independent onboarding state
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
### Manual Testing:
|
||||
1. **Fresh install**: Delete app, reinstall, verify welcome sheet appears
|
||||
2. **Tutorial mode**: Tap "Show Me How", verify hints appear in sequence
|
||||
3. **Skip mode**: Tap "Start Playing", verify hints still appear once naturally
|
||||
4. **Second launch**: Relaunch app, verify welcome doesn't show again
|
||||
5. **Settings reset**: Check if onboarding can be reset (could add to settings)
|
||||
|
||||
### Reset for Testing:
|
||||
Add this to a development menu if needed:
|
||||
```swift
|
||||
Button("Reset Onboarding") {
|
||||
gameState.onboarding.reset()
|
||||
}
|
||||
```
|
||||
|
||||
## Future Enhancements (Not Implemented)
|
||||
|
||||
These could be added later if desired:
|
||||
|
||||
1. **Progressive discovery hints**: Tips after 5/10/20 hands
|
||||
- "Enable card counting in settings" (Blackjack)
|
||||
- "Side bets offer bigger payouts" (both games)
|
||||
|
||||
2. **Onboarding analytics**: Track which hints are most helpful
|
||||
|
||||
3. **User-requested replay**: "Show tutorial again" in settings
|
||||
|
||||
4. **Pulsing hints**: Use PulsingModifier on interactive elements during tutorial
|
||||
|
||||
5. **Accessibility announcements**: Post VoiceOver announcements for key moments
|
||||
|
||||
## Files Created
|
||||
|
||||
```
|
||||
CasinoKit/Sources/CasinoKit/
|
||||
├── Models/OnboardingState.swift
|
||||
└── Views/
|
||||
├── ContextualTooltip.swift
|
||||
├── WelcomeSheet.swift
|
||||
└── PulsingModifier.swift
|
||||
```
|
||||
|
||||
## Files Modified
|
||||
|
||||
```
|
||||
Blackjack/
|
||||
├── Models/GameSettings.swift (defaults changed, bug fix)
|
||||
├── Engine/GameState.swift (added onboarding property)
|
||||
└── Views/Game/GameTableView.swift (integrated onboarding UI)
|
||||
|
||||
Baccarat/
|
||||
├── Models/GameSettings.swift (defaults changed)
|
||||
├── Engine/GameState.swift (added onboarding property)
|
||||
└── Views/Game/GameTableView.swift (integrated onboarding UI)
|
||||
|
||||
README Files:
|
||||
├── Blackjack/README.md
|
||||
├── Baccarat/README.md
|
||||
└── CasinoKit/README.md
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
The onboarding system provides a polished first-time user experience that:
|
||||
- Helps new users understand the game quickly
|
||||
- Never annoys experienced players
|
||||
- Maintains the premium feel of the apps
|
||||
- Follows iOS best practices
|
||||
- Is fully reusable for future casino games
|
||||
|
||||
All code follows the project's Swift/SwiftUI guidelines, uses design constants, and includes proper accessibility support.
|
||||
|
||||
246
TOOLTIP_REFACTORING.md
Normal file
246
TOOLTIP_REFACTORING.md
Normal file
@ -0,0 +1,246 @@
|
||||
# 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