Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cecd89f6d | |||
| 410182cf7d | |||
| 42deeaf242 | |||
| 1695684fc9 | |||
| fa2784519b |
+105
-44
@@ -4,44 +4,72 @@ PODS:
|
|||||||
- audioplayers_darwin (0.0.1):
|
- audioplayers_darwin (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- Firebase/CoreOnly (12.14.0):
|
- Firebase/CoreOnly (12.15.0):
|
||||||
- FirebaseCore (~> 12.14.0)
|
- FirebaseCore (~> 12.15.0)
|
||||||
|
- Firebase/Crashlytics (12.15.0):
|
||||||
|
- Firebase/CoreOnly
|
||||||
|
- FirebaseCrashlytics (~> 12.15.0)
|
||||||
- firebase_analytics (12.4.2):
|
- firebase_analytics (12.4.2):
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- FirebaseAnalytics (= 12.14.0)
|
- FirebaseAnalytics (= 12.15.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_core (4.10.0):
|
- firebase_core (4.11.0):
|
||||||
- Firebase/CoreOnly (= 12.14.0)
|
- Firebase/CoreOnly (= 12.15.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- FirebaseAnalytics (12.14.0):
|
- firebase_crashlytics (5.2.4):
|
||||||
- FirebaseAnalytics/Default (= 12.14.0)
|
- Firebase/Crashlytics (= 12.15.0)
|
||||||
- FirebaseCore (~> 12.14.0)
|
- firebase_core
|
||||||
- FirebaseInstallations (~> 12.14.0)
|
- Flutter
|
||||||
|
- FirebaseAnalytics (12.15.0):
|
||||||
|
- FirebaseAnalytics/Default (= 12.15.0)
|
||||||
|
- FirebaseCore (~> 12.15.0)
|
||||||
|
- FirebaseInstallations (~> 12.15.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseAnalytics/Default (12.14.0):
|
- FirebaseAnalytics/Default (12.15.0):
|
||||||
- FirebaseCore (~> 12.14.0)
|
- FirebaseCore (~> 12.15.0)
|
||||||
- FirebaseInstallations (~> 12.14.0)
|
- FirebaseInstallations (~> 12.15.0)
|
||||||
- GoogleAppMeasurement/Default (= 12.14.0)
|
- GoogleAppMeasurement/Default (= 12.15.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseCore (12.14.0):
|
- FirebaseCore (12.15.0):
|
||||||
- FirebaseCoreInternal (~> 12.14.0)
|
- FirebaseCoreInternal (~> 12.15.0)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/Logger (~> 8.1)
|
- GoogleUtilities/Logger (~> 8.1)
|
||||||
- FirebaseCoreInternal (12.14.0):
|
- FirebaseCoreExtension (12.15.0):
|
||||||
|
- FirebaseCore (~> 12.15.0)
|
||||||
|
- FirebaseCoreInternal (12.15.0):
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- FirebaseInstallations (12.14.0):
|
- FirebaseCrashlytics (12.15.0):
|
||||||
- FirebaseCore (~> 12.14.0)
|
- FirebaseCore (~> 12.15.0)
|
||||||
|
- FirebaseInstallations (~> 12.15.0)
|
||||||
|
- FirebaseRemoteConfigInterop (~> 12.15.0)
|
||||||
|
- FirebaseSessions (~> 12.15.0)
|
||||||
|
- GoogleDataTransport (~> 10.1)
|
||||||
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
|
- nanopb (~> 3.30910.0)
|
||||||
|
- PromisesObjC (~> 2.4)
|
||||||
|
- FirebaseInstallations (12.15.0):
|
||||||
|
- FirebaseCore (~> 12.15.0)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
- PromisesObjC (~> 2.4)
|
- PromisesObjC (~> 2.4)
|
||||||
|
- FirebaseRemoteConfigInterop (12.15.0)
|
||||||
|
- FirebaseSessions (12.15.0):
|
||||||
|
- FirebaseCore (~> 12.15.0)
|
||||||
|
- FirebaseCoreExtension (~> 12.15.0)
|
||||||
|
- FirebaseInstallations (~> 12.15.0)
|
||||||
|
- GoogleDataTransport (~> 10.1)
|
||||||
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
|
- nanopb (~> 3.30910.0)
|
||||||
|
- PromisesSwift (~> 2.1)
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- Google-Mobile-Ads-SDK (12.14.0):
|
- Google-Mobile-Ads-SDK (12.14.0):
|
||||||
- GoogleUserMessagingPlatform (>= 1.1)
|
- GoogleUserMessagingPlatform (>= 1.1)
|
||||||
@@ -54,59 +82,64 @@ PODS:
|
|||||||
- GoogleUtilities/Logger (~> 8.1)
|
- GoogleUtilities/Logger (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/Core (12.14.0):
|
- GoogleAppMeasurement/Core (12.15.0):
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/Default (12.14.0):
|
- GoogleAppMeasurement/Default (12.15.0):
|
||||||
- GoogleAdsOnDeviceConversion (~> 3.6.0)
|
- GoogleAdsOnDeviceConversion (~> 3.6.0)
|
||||||
- GoogleAppMeasurement/Core (= 12.14.0)
|
- GoogleAppMeasurement/Core (= 12.15.0)
|
||||||
- GoogleAppMeasurement/IdentitySupport (= 12.14.0)
|
- GoogleAppMeasurement/IdentitySupport (= 12.15.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/IdentitySupport (12.14.0):
|
- GoogleAppMeasurement/IdentitySupport (12.15.0):
|
||||||
- GoogleAppMeasurement/Core (= 12.14.0)
|
- GoogleAppMeasurement/Core (= 12.15.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
|
- GoogleDataTransport (10.1.0):
|
||||||
|
- nanopb (~> 3.30910.0)
|
||||||
|
- PromisesObjC (~> 2.4)
|
||||||
- GoogleUserMessagingPlatform (3.1.0)
|
- GoogleUserMessagingPlatform (3.1.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
|
- GoogleUtilities/AppDelegateSwizzler (8.1.1):
|
||||||
- GoogleUtilities/Environment
|
- GoogleUtilities/Environment
|
||||||
- GoogleUtilities/Logger
|
- GoogleUtilities/Logger
|
||||||
- GoogleUtilities/Network
|
- GoogleUtilities/Network
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- GoogleUtilities/Environment (8.1.0):
|
- GoogleUtilities/Environment (8.1.1):
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- GoogleUtilities/Logger (8.1.0):
|
- GoogleUtilities/Logger (8.1.1):
|
||||||
- GoogleUtilities/Environment
|
- GoogleUtilities/Environment
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- GoogleUtilities/MethodSwizzler (8.1.0):
|
- GoogleUtilities/MethodSwizzler (8.1.1):
|
||||||
- GoogleUtilities/Logger
|
- GoogleUtilities/Logger
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- GoogleUtilities/Network (8.1.0):
|
- GoogleUtilities/Network (8.1.1):
|
||||||
- GoogleUtilities/Logger
|
- GoogleUtilities/Logger
|
||||||
- "GoogleUtilities/NSData+zlib"
|
- "GoogleUtilities/NSData+zlib"
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- GoogleUtilities/Reachability
|
- GoogleUtilities/Reachability
|
||||||
- "GoogleUtilities/NSData+zlib (8.1.0)":
|
- "GoogleUtilities/NSData+zlib (8.1.1)":
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- GoogleUtilities/Privacy (8.1.0)
|
- GoogleUtilities/Privacy (8.1.1)
|
||||||
- GoogleUtilities/Reachability (8.1.0):
|
- GoogleUtilities/Reachability (8.1.1):
|
||||||
- GoogleUtilities/Logger
|
- GoogleUtilities/Logger
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- GoogleUtilities/UserDefaults (8.1.0):
|
- GoogleUtilities/UserDefaults (8.1.1):
|
||||||
- GoogleUtilities/Logger
|
- GoogleUtilities/Logger
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- in_app_purchase_storekit (0.0.1):
|
- in_app_purchase_storekit (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- in_app_review (2.0.0):
|
||||||
|
- Flutter
|
||||||
- nanopb (3.30910.0):
|
- nanopb (3.30910.0):
|
||||||
- nanopb/decode (= 3.30910.0)
|
- nanopb/decode (= 3.30910.0)
|
||||||
- nanopb/encode (= 3.30910.0)
|
- nanopb/encode (= 3.30910.0)
|
||||||
@@ -115,10 +148,14 @@ PODS:
|
|||||||
- path_provider_foundation (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.1)
|
||||||
|
- PromisesSwift (2.4.1):
|
||||||
|
- PromisesObjC (= 2.4.1)
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- url_launcher_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
- webview_flutter_wkwebview (0.0.1):
|
- webview_flutter_wkwebview (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -128,11 +165,14 @@ DEPENDENCIES:
|
|||||||
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
|
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
|
||||||
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
|
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
|
||||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
||||||
|
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- google_mobile_ads (from `.symlinks/plugins/google_mobile_ads/ios`)
|
- google_mobile_ads (from `.symlinks/plugins/google_mobile_ads/ios`)
|
||||||
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
|
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
|
||||||
|
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
|
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
@@ -140,15 +180,21 @@ SPEC REPOS:
|
|||||||
- Firebase
|
- Firebase
|
||||||
- FirebaseAnalytics
|
- FirebaseAnalytics
|
||||||
- FirebaseCore
|
- FirebaseCore
|
||||||
|
- FirebaseCoreExtension
|
||||||
- FirebaseCoreInternal
|
- FirebaseCoreInternal
|
||||||
|
- FirebaseCrashlytics
|
||||||
- FirebaseInstallations
|
- FirebaseInstallations
|
||||||
|
- FirebaseRemoteConfigInterop
|
||||||
|
- FirebaseSessions
|
||||||
- Google-Mobile-Ads-SDK
|
- Google-Mobile-Ads-SDK
|
||||||
- GoogleAdsOnDeviceConversion
|
- GoogleAdsOnDeviceConversion
|
||||||
- GoogleAppMeasurement
|
- GoogleAppMeasurement
|
||||||
|
- GoogleDataTransport
|
||||||
- GoogleUserMessagingPlatform
|
- GoogleUserMessagingPlatform
|
||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
- nanopb
|
- nanopb
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
|
- PromisesSwift
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
app_tracking_transparency:
|
app_tracking_transparency:
|
||||||
@@ -159,41 +205,56 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/firebase_analytics/ios"
|
:path: ".symlinks/plugins/firebase_analytics/ios"
|
||||||
firebase_core:
|
firebase_core:
|
||||||
:path: ".symlinks/plugins/firebase_core/ios"
|
:path: ".symlinks/plugins/firebase_core/ios"
|
||||||
|
firebase_crashlytics:
|
||||||
|
:path: ".symlinks/plugins/firebase_crashlytics/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
google_mobile_ads:
|
google_mobile_ads:
|
||||||
:path: ".symlinks/plugins/google_mobile_ads/ios"
|
:path: ".symlinks/plugins/google_mobile_ads/ios"
|
||||||
in_app_purchase_storekit:
|
in_app_purchase_storekit:
|
||||||
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
|
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
|
||||||
|
in_app_review:
|
||||||
|
:path: ".symlinks/plugins/in_app_review/ios"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
|
url_launcher_ios:
|
||||||
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
webview_flutter_wkwebview:
|
webview_flutter_wkwebview:
|
||||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
|
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
app_tracking_transparency: 3d84f147f67ca82d3c15355c36b1fa6b66ca7c92
|
app_tracking_transparency: 3d84f147f67ca82d3c15355c36b1fa6b66ca7c92
|
||||||
audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
|
audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
|
||||||
Firebase: 7cc10425300768ec86292688af5cb228f0604bde
|
Firebase: a8539b633d474fbeb654c7043f9c1649e274045b
|
||||||
firebase_analytics: 9ea6c22e60e1d37ee76772de57b4addddb03e276
|
firebase_analytics: e0a17f792099472235f9ec7f31d1d3a0730d4891
|
||||||
firebase_core: 383e19b49a08df5d7a6cf5017616de6a357ed7af
|
firebase_core: fc23178af8ea070194d09031ae4198a9608a3d22
|
||||||
FirebaseAnalytics: 99364329a3ea4d1ab4a3744b5464371b7351c481
|
firebase_crashlytics: 344bb168f55aee1086c6cdd0b105a9db018cd344
|
||||||
FirebaseCore: 4939b340b9c598dc1f965d68f8fe57e630b65407
|
FirebaseAnalytics: 9c9fa7915fc52ea03077000d5a7b6a8947b2d76e
|
||||||
FirebaseCoreInternal: 090369a5fffd7423cf88006ab4d2ccc2173a8db9
|
FirebaseCore: 2e86a4ea1684d4381707069e4a6d89ac808e901e
|
||||||
FirebaseInstallations: 7cdc919e29dc54306edeffdbdc1eed1a40d7d1e7
|
FirebaseCoreExtension: 10d2a627977b39418759ad88ada80fbbd34f1c4f
|
||||||
|
FirebaseCoreInternal: 6ab6a02c94446c026d2cf35cf5383842ebaa4992
|
||||||
|
FirebaseCrashlytics: 87e76cc33259b076dd1f96cd829db76849338e08
|
||||||
|
FirebaseInstallations: eb29ccbf64eaedf86fd5b2ccc7fabde567660b52
|
||||||
|
FirebaseRemoteConfigInterop: 7e3d57ce4b1e958bb1d15403faa7178f46bbb5b7
|
||||||
|
FirebaseSessions: acfe7eadca47cda94ac86592737204581bb1abf6
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
Google-Mobile-Ads-SDK: 4534fd2dfcd3f705c5485a6633c5188d03d4eed2
|
Google-Mobile-Ads-SDK: 4534fd2dfcd3f705c5485a6633c5188d03d4eed2
|
||||||
google_mobile_ads: df3008bafbe1f2ad6862f87334e560d2f047f902
|
google_mobile_ads: df3008bafbe1f2ad6862f87334e560d2f047f902
|
||||||
GoogleAdsOnDeviceConversion: 80ce443fa1b4b5750913d53a04ecda644ff57744
|
GoogleAdsOnDeviceConversion: 80ce443fa1b4b5750913d53a04ecda644ff57744
|
||||||
GoogleAppMeasurement: 1987ffa55055dfb22c52e363c31aa50c1e11d349
|
GoogleAppMeasurement: a6d37949071d456e9147dac6789c4342e0e7a8c5
|
||||||
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUserMessagingPlatform: befe603da6501006420c206222acd449bba45a9c
|
GoogleUserMessagingPlatform: befe603da6501006420c206222acd449bba45a9c
|
||||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
GoogleUtilities: 4f2618a4a1e762a1ee134a1e2323bba9843e06da
|
||||||
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
|
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
|
||||||
|
in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: 752c3227f599e3467650e47ea36f433eeb10c273
|
||||||
|
PromisesSwift: 217dea0fd5d2ad65222a109c48698add13cc1c5b
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
|
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||||
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
|
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
|
||||||
|
|
||||||
PODFILE CHECKSUM: 7a0c05f8aeb53a8c858ca08a4666afaa242f0eb1
|
PODFILE CHECKSUM: 7a0c05f8aeb53a8c858ca08a4666afaa242f0eb1
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ class SaveRepository {
|
|||||||
_reviewRequested = (json['flags']
|
_reviewRequested = (json['flags']
|
||||||
as Map<String, dynamic>?)?['reviewRequested'] as bool? ??
|
as Map<String, dynamic>?)?['reviewRequested'] as bool? ??
|
||||||
false;
|
false;
|
||||||
|
_boostersSeeded = (json['flags']
|
||||||
|
as Map<String, dynamic>?)?['boostersSeeded'] as bool? ??
|
||||||
|
false;
|
||||||
final boosters = json['boosters'] as Map<String, dynamic>? ?? {};
|
final boosters = json['boosters'] as Map<String, dynamic>? ?? {};
|
||||||
for (final t in BoosterType.values) {
|
for (final t in BoosterType.values) {
|
||||||
_boosters[t] = boosters[t.name] as int? ?? 0;
|
_boosters[t] = boosters[t.name] as int? ?? 0;
|
||||||
@@ -76,6 +79,7 @@ class SaveRepository {
|
|||||||
bool _soundEnabled = true;
|
bool _soundEnabled = true;
|
||||||
bool _musicEnabled = true;
|
bool _musicEnabled = true;
|
||||||
bool _reviewRequested = false;
|
bool _reviewRequested = false;
|
||||||
|
bool _boostersSeeded = false;
|
||||||
final Map<BoosterType, int> _boosters = {
|
final Map<BoosterType, int> _boosters = {
|
||||||
for (final t in BoosterType.values) t: 0,
|
for (final t in BoosterType.values) t: 0,
|
||||||
};
|
};
|
||||||
@@ -138,6 +142,18 @@ class SaveRepository {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Grants one of each booster the first time it ever runs, so a new player
|
||||||
|
/// can try every booster. Idempotent for the app's lifetime via a persisted
|
||||||
|
/// flag — safe to call on every launch.
|
||||||
|
Future<void> seedInitialBoostersIfNeeded() async {
|
||||||
|
if (_boostersSeeded) return;
|
||||||
|
_boostersSeeded = true;
|
||||||
|
for (final t in BoosterType.values) {
|
||||||
|
_boosters[t] = (_boosters[t] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
await _flush();
|
||||||
|
}
|
||||||
|
|
||||||
String? get dailyLastClaimedYmd => _dailyLastClaimedYmd;
|
String? get dailyLastClaimedYmd => _dailyLastClaimedYmd;
|
||||||
int get dailyCalendarDay => _dailyCalendarDay;
|
int get dailyCalendarDay => _dailyCalendarDay;
|
||||||
|
|
||||||
@@ -221,6 +237,7 @@ class SaveRepository {
|
|||||||
'soundEnabled': _soundEnabled,
|
'soundEnabled': _soundEnabled,
|
||||||
'musicEnabled': _musicEnabled,
|
'musicEnabled': _musicEnabled,
|
||||||
'reviewRequested': _reviewRequested,
|
'reviewRequested': _reviewRequested,
|
||||||
|
'boostersSeeded': _boostersSeeded,
|
||||||
},
|
},
|
||||||
'endless': {'best': _endlessBest},
|
'endless': {'best': _endlessBest},
|
||||||
'boosters': {for (final t in BoosterType.values) t.name: _boosters[t]},
|
'boosters': {for (final t in BoosterType.values) t.name: _boosters[t]},
|
||||||
|
|||||||
@@ -217,19 +217,22 @@ class GameEngine {
|
|||||||
_phase = GamePhase.lost;
|
_phase = GamePhase.lost;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Booster: empties one filled cell. No move/score/combo/objective effect.
|
/// Booster: empties one filled cell. No move/score/combo effect, but a
|
||||||
/// Allowed only mid-attempt (playing or stuck). Returns false on an empty
|
/// hammered gem DOES count toward a clear-gems objective (and can win the
|
||||||
/// cell or finished attempt so the caller keeps the booster.
|
/// stage). Allowed only mid-attempt. Returns false on an empty cell or a
|
||||||
|
/// finished attempt so the caller keeps the booster.
|
||||||
bool useHammer(int x, int y) {
|
bool useHammer(int x, int y) {
|
||||||
if (_phase == GamePhase.won || _phase == GamePhase.lost) return false;
|
if (_phase == GamePhase.won || _phase == GamePhase.lost) return false;
|
||||||
if (!_grid.isOccupied(x, y)) return false;
|
if (!_grid.isOccupied(x, y)) return false;
|
||||||
|
final wasGem = _grid.cellAt(x, y).type == CellType.gem;
|
||||||
_grid = _grid.withCell(x, y, const Cell(CellType.empty));
|
_grid = _grid.withCell(x, y, const Cell(CellType.empty));
|
||||||
_checkStuck();
|
_resolveAfterBooster(
|
||||||
|
wasGem ? const LinesCleared(lines: 0, gems: 1) : null);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Booster: re-deals the tray. No move/score effect. Re-checks stuck so a
|
/// Booster: re-deals the tray. No move/score/objective effect. Re-checks
|
||||||
/// dead board with a hopeless tray can become playable again.
|
/// stuck so a dead board with a hopeless tray can become playable again.
|
||||||
bool useShuffle() {
|
bool useShuffle() {
|
||||||
if (_phase == GamePhase.won || _phase == GamePhase.lost) return false;
|
if (_phase == GamePhase.won || _phase == GamePhase.lost) return false;
|
||||||
_tray = _generator.nextTray(_grid);
|
_tray = _generator.nextTray(_grid);
|
||||||
@@ -237,17 +240,35 @@ class GameEngine {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Booster: empties one row or one column (exactly one of [row]/[col]).
|
/// Booster: empties one row or one column (exactly one of [row]/[col]). No
|
||||||
/// No move/score/objective effect. Re-checks stuck.
|
/// move/score effect, but it counts as clearing one line plus any gems on
|
||||||
|
/// that line, toward the stage objectives (and can win the stage).
|
||||||
bool useLineBomb({int? row, int? col}) {
|
bool useLineBomb({int? row, int? col}) {
|
||||||
if (_phase == GamePhase.won || _phase == GamePhase.lost) return false;
|
if (_phase == GamePhase.won || _phase == GamePhase.lost) return false;
|
||||||
if ((row == null) == (col == null)) return false; // need exactly one
|
if ((row == null) == (col == null)) return false; // need exactly one
|
||||||
|
var gems = 0;
|
||||||
for (var i = 0; i < GridState.size; i++) {
|
for (var i = 0; i < GridState.size; i++) {
|
||||||
final x = col ?? i;
|
final x = col ?? i;
|
||||||
final y = row ?? i;
|
final y = row ?? i;
|
||||||
|
if (_grid.cellAt(x, y).type == CellType.gem) gems++;
|
||||||
_grid = _grid.withCell(x, y, const Cell(CellType.empty));
|
_grid = _grid.withCell(x, y, const Cell(CellType.empty));
|
||||||
}
|
}
|
||||||
_checkStuck();
|
_resolveAfterBooster(LinesCleared(lines: 1, gems: gems));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// After a booster mutates the grid, feed any [cleared] event through the
|
||||||
|
/// objectives so booster-cleared gems/lines count, then resolve the phase:
|
||||||
|
/// a completed objective wins the stage, otherwise re-check stuck. Score and
|
||||||
|
/// the move counter stay untouched — boosters are help, not a placement.
|
||||||
|
void _resolveAfterBooster(LinesCleared? cleared) {
|
||||||
|
if (cleared != null) {
|
||||||
|
_objectives = [for (final obj in _objectives) obj.onEvent(cleared)];
|
||||||
|
}
|
||||||
|
if (!_stage.endless && _objectives.every((o) => o.isComplete)) {
|
||||||
|
_phase = GamePhase.won;
|
||||||
|
} else {
|
||||||
|
_checkStuck();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ const contentBaseUrl = String.fromEnvironment(
|
|||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
final saveRepository = await SaveRepository.open();
|
final saveRepository = await SaveRepository.open();
|
||||||
|
// New players start with one of each booster (idempotent after the first run).
|
||||||
|
await saveRepository.seedInitialBoostersIfNeeded();
|
||||||
|
|
||||||
// Analytics: real GA4 traffic flows only from release builds so development
|
// Analytics: real GA4 traffic flows only from release builds so development
|
||||||
// never pollutes production. If Firebase init fails (e.g. missing native
|
// never pollutes production. If Firebase init fails (e.g. missing native
|
||||||
|
|||||||
@@ -25,11 +25,14 @@ class DailyRewardNotifier extends Notifier<DailyResolution> {
|
|||||||
final r = state;
|
final r = state;
|
||||||
if (!r.claimable) return;
|
if (!r.claimable) return;
|
||||||
final reward = _cal.rewardFor(r.day);
|
final reward = _cal.rewardFor(r.day);
|
||||||
|
// Record the claim BEFORE granting, so a crash mid-claim forfeits at most
|
||||||
|
// one reward rather than leaving the day unrecorded (which would let the
|
||||||
|
// player re-claim and farm boosters on the next launch).
|
||||||
|
await _save.recordDailyClaim(_cal.ymd(_now()), r.day);
|
||||||
final inv = ref.read(boosterInventoryProvider.notifier);
|
final inv = ref.read(boosterInventoryProvider.notifier);
|
||||||
for (final entry in reward.entries) {
|
for (final entry in reward.entries) {
|
||||||
await inv.grant(entry.key, entry.value * (doubled ? 2 : 1));
|
await inv.grant(entry.key, entry.value * (doubled ? 2 : 1));
|
||||||
}
|
}
|
||||||
await _save.recordDailyClaim(_cal.ymd(_now()), r.day);
|
|
||||||
ref.read(analyticsProvider).dailyRewardClaimed(day: r.day, doubled: doubled);
|
ref.read(analyticsProvider).dailyRewardClaimed(day: r.day, doubled: doubled);
|
||||||
for (final e in reward.entries) {
|
for (final e in reward.entries) {
|
||||||
ref.read(analyticsProvider).boosterGranted(
|
ref.read(analyticsProvider).boosterGranted(
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import '../widgets/board_geometry.dart';
|
|||||||
import '../widgets/board_painter.dart';
|
import '../widgets/board_painter.dart';
|
||||||
import '../widgets/board_widget.dart';
|
import '../widgets/board_widget.dart';
|
||||||
import '../widgets/booster_bar.dart';
|
import '../widgets/booster_bar.dart';
|
||||||
|
import '../widgets/booster_hint.dart';
|
||||||
import '../widgets/effects_overlay.dart';
|
import '../widgets/effects_overlay.dart';
|
||||||
import '../widgets/hud_widget.dart';
|
import '../widgets/hud_widget.dart';
|
||||||
import '../widgets/piece_painter.dart';
|
import '../widgets/piece_painter.dart';
|
||||||
@@ -143,18 +144,9 @@ class _GameScreenState extends ConsumerState<GameScreen>
|
|||||||
await ref.read(gameSessionProvider.notifier).useShuffle();
|
await ref.read(gameSessionProvider.notifier).useShuffle();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// hammer / lineBomb need a board target.
|
// hammer / lineBomb need a board target; the floating BoosterHint above
|
||||||
|
// the board shows the prompt (and lets the player cancel) while armed.
|
||||||
setState(() => _arming = type);
|
setState(() => _arming = type);
|
||||||
final l10n = AppLocalizations.of(context)!;
|
|
||||||
final hint =
|
|
||||||
type == BoosterType.hammer ? l10n.boosterTapTarget : l10n.boosterTapLine;
|
|
||||||
if (!mounted) return;
|
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
..hideCurrentSnackBar()
|
|
||||||
..showSnackBar(SnackBar(
|
|
||||||
content: Text(hint),
|
|
||||||
duration: const Duration(seconds: 3),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a board tap into a cell, then applies the armed booster.
|
/// Converts a board tap into a cell, then applies the armed booster.
|
||||||
@@ -169,10 +161,13 @@ class _GameScreenState extends ConsumerState<GameScreen>
|
|||||||
final y = (local.dy / cell).floor();
|
final y = (local.dy / cell).floor();
|
||||||
if (x < 0 || x >= GridState.size || y < 0 || y >= GridState.size) return;
|
if (x < 0 || x >= GridState.size || y < 0 || y >= GridState.size) return;
|
||||||
|
|
||||||
|
// Disarm synchronously, before any await, so a rapid second tap on the
|
||||||
|
// board is a no-op rather than a redundant booster use / stacked dialog.
|
||||||
|
setState(() => _arming = null);
|
||||||
|
|
||||||
final session = ref.read(gameSessionProvider.notifier);
|
final session = ref.read(gameSessionProvider.notifier);
|
||||||
if (armed == BoosterType.hammer) {
|
if (armed == BoosterType.hammer) {
|
||||||
await session.useHammer(x, y);
|
await session.useHammer(x, y);
|
||||||
if (mounted) setState(() => _arming = null);
|
|
||||||
} else if (armed == BoosterType.lineBomb) {
|
} else if (armed == BoosterType.lineBomb) {
|
||||||
final axis = await _chooseLineAxis();
|
final axis = await _chooseLineAxis();
|
||||||
if (axis == _LineAxis.row) {
|
if (axis == _LineAxis.row) {
|
||||||
@@ -180,8 +175,6 @@ class _GameScreenState extends ConsumerState<GameScreen>
|
|||||||
} else if (axis == _LineAxis.col) {
|
} else if (axis == _LineAxis.col) {
|
||||||
await session.useLineBomb(col: x);
|
await session.useLineBomb(col: x);
|
||||||
}
|
}
|
||||||
// A dismissed chooser cancels the use but still clears the armed state.
|
|
||||||
if (mounted) setState(() => _arming = null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,32 +402,54 @@ class _GameScreenState extends ConsumerState<GameScreen>
|
|||||||
),
|
),
|
||||||
HudWidget(view: view),
|
HudWidget(view: view),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Center(
|
child: Stack(
|
||||||
child: AnimatedBuilder(
|
children: [
|
||||||
animation: _shake,
|
Positioned.fill(
|
||||||
builder: (context, child) {
|
child: Center(
|
||||||
final t = _shake.value;
|
child: AnimatedBuilder(
|
||||||
final dx =
|
animation: _shake,
|
||||||
math.sin(t * math.pi * 10) * 6 * (1 - t);
|
builder: (context, child) {
|
||||||
return Transform.translate(
|
final t = _shake.value;
|
||||||
offset: Offset(dx, 0), child: child);
|
final dx = math.sin(t * math.pi * 10) *
|
||||||
},
|
6 *
|
||||||
// While a targeted booster is armed, taps on the
|
(1 - t);
|
||||||
// board pick a cell. When not arming, onTapUp
|
return Transform.translate(
|
||||||
// returns immediately so it never steals the
|
offset: Offset(dx, 0), child: child);
|
||||||
// tray-drag placement gestures.
|
},
|
||||||
child: GestureDetector(
|
// While a targeted booster is armed, taps on
|
||||||
behavior: HitTestBehavior.deferToChild,
|
// the board pick a cell. When not arming,
|
||||||
onTapUp: _arming == null
|
// onTapUp returns immediately so it never
|
||||||
? null
|
// steals the tray-drag placement gestures.
|
||||||
: (details) => _onBoardTapUp(details),
|
child: GestureDetector(
|
||||||
child: BoardWidget(
|
behavior: HitTestBehavior.deferToChild,
|
||||||
key: _boardKey,
|
onTapUp: _arming == null
|
||||||
view: view,
|
? null
|
||||||
ghost: ghost,
|
: (details) => _onBoardTapUp(details),
|
||||||
|
child: BoardWidget(
|
||||||
|
key: _boardKey,
|
||||||
|
view: view,
|
||||||
|
ghost: ghost,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
// Floating targeting prompt, in the empty space above
|
||||||
|
// the centered board so it never covers cells.
|
||||||
|
Positioned(
|
||||||
|
top: 4,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Center(
|
||||||
|
child: BoosterHint(
|
||||||
|
arming: _arming,
|
||||||
|
accent: ThemeColors(theme).accent,
|
||||||
|
onCancel: () =>
|
||||||
|
setState(() => _arming = null),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TrayWidget(
|
TrayWidget(
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
// lib/ui/widgets/booster_hint.dart
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../game/models/booster.dart';
|
||||||
|
import '../../l10n/gen/app_localizations.dart';
|
||||||
|
|
||||||
|
/// A small floating pill that appears above the board while a targeted booster
|
||||||
|
/// (hammer / line-bomb) is armed, telling the player what to tap. Glows with a
|
||||||
|
/// gentle pulse in the season accent colour, slides/scales in, and cancels the
|
||||||
|
/// armed state when tapped. Renders (invisibly, ignoring pointer) when nothing
|
||||||
|
/// is armed so it can animate in/out in place without layout jumps.
|
||||||
|
class BoosterHint extends StatefulWidget {
|
||||||
|
const BoosterHint({
|
||||||
|
super.key,
|
||||||
|
required this.arming,
|
||||||
|
required this.accent,
|
||||||
|
required this.onCancel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BoosterType? arming;
|
||||||
|
final Color accent;
|
||||||
|
final VoidCallback onCancel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BoosterHint> createState() => _BoosterHintState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BoosterHintState extends State<BoosterHint>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _pulse = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 1100),
|
||||||
|
);
|
||||||
|
|
||||||
|
static const _icons = {
|
||||||
|
BoosterType.hammer: Icons.gavel,
|
||||||
|
BoosterType.shuffle: Icons.shuffle,
|
||||||
|
BoosterType.lineBomb: Icons.clear_all,
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_syncPulse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(BoosterHint oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
_syncPulse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pulse only while a booster is armed — idle (no live ticker) otherwise, so
|
||||||
|
/// it never spins during normal play or pins a timer open in widget tests.
|
||||||
|
void _syncPulse() {
|
||||||
|
if (widget.arming != null) {
|
||||||
|
if (!_pulse.isAnimating) _pulse.repeat(reverse: true);
|
||||||
|
} else if (_pulse.isAnimating) {
|
||||||
|
_pulse
|
||||||
|
..stop()
|
||||||
|
..value = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pulse.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final arming = widget.arming;
|
||||||
|
final visible = arming != null;
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final label = arming == BoosterType.lineBomb
|
||||||
|
? l10n.boosterTapLine
|
||||||
|
: l10n.boosterTapTarget;
|
||||||
|
final accent = widget.accent;
|
||||||
|
|
||||||
|
return IgnorePointer(
|
||||||
|
ignoring: !visible,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: visible ? 1 : 0,
|
||||||
|
duration: const Duration(milliseconds: 160),
|
||||||
|
child: AnimatedSlide(
|
||||||
|
offset: visible ? Offset.zero : const Offset(0, -0.4),
|
||||||
|
duration: const Duration(milliseconds: 220),
|
||||||
|
curve: Curves.easeOutBack,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: widget.onCancel,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _pulse,
|
||||||
|
builder: (context, child) {
|
||||||
|
final t = _pulse.value; // 0..1, breathing
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: accent,
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: accent.withValues(alpha: 0.22 + 0.28 * t),
|
||||||
|
blurRadius: 10 + 12 * t,
|
||||||
|
spreadRadius: 1 + 3 * t,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(7, 6, 12, 6),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 26,
|
||||||
|
height: 26,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withValues(alpha: 0.22),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
_icons[arming ?? BoosterType.hammer],
|
||||||
|
size: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Icon(Icons.close, size: 16, color: Colors.white70),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.0.0+3
|
version: 1.1.0+4
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.2
|
sdk: ^3.9.2
|
||||||
|
|||||||
@@ -34,4 +34,21 @@ void main() {
|
|||||||
expect(repo.boosterCount(BoosterType.shuffle), 0);
|
expect(repo.boosterCount(BoosterType.shuffle), 0);
|
||||||
expect(await repo.consumeBooster(BoosterType.shuffle), isFalse);
|
expect(await repo.consumeBooster(BoosterType.shuffle), isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('seedInitialBoosters grants 1 of each once, then is idempotent',
|
||||||
|
() async {
|
||||||
|
final repo = await fresh();
|
||||||
|
await repo.seedInitialBoostersIfNeeded();
|
||||||
|
for (final t in BoosterType.values) {
|
||||||
|
expect(repo.boosterCount(t), 1, reason: t.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A second call (and a reload) must not grant again — the flag persists.
|
||||||
|
await repo.seedInitialBoostersIfNeeded();
|
||||||
|
final reloaded = SaveRepository(await SharedPreferences.getInstance());
|
||||||
|
await reloaded.seedInitialBoostersIfNeeded();
|
||||||
|
for (final t in BoosterType.values) {
|
||||||
|
expect(reloaded.boosterCount(t), 1, reason: '${t.name} after reload');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,56 @@ StageConfig _stage() => StageConfig.fromJson({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const _stars = {
|
||||||
|
'two': {'movesLeft': 5},
|
||||||
|
'three': {'movesLeft': 10},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Two gems plus one plain filled cell; objective is to clear both gems.
|
||||||
|
StageConfig _gemStage() => StageConfig.fromJson({
|
||||||
|
'id': 'b_gem',
|
||||||
|
'seed': 1,
|
||||||
|
'moveLimit': 20,
|
||||||
|
'preset': [
|
||||||
|
{'x': 0, 'y': 0, 't': 'gem'},
|
||||||
|
{'x': 3, 'y': 3, 't': 'gem'},
|
||||||
|
{'x': 5, 'y': 5, 't': 'filled', 'c': 2},
|
||||||
|
],
|
||||||
|
'objectives': [
|
||||||
|
{'type': 'clearGems', 'count': 2},
|
||||||
|
],
|
||||||
|
'stars': _stars,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Two gems sitting on row 2; objective is to clear both gems.
|
||||||
|
StageConfig _gemRowStage() => StageConfig.fromJson({
|
||||||
|
'id': 'b_gemrow',
|
||||||
|
'seed': 1,
|
||||||
|
'moveLimit': 20,
|
||||||
|
'preset': [
|
||||||
|
{'x': 1, 'y': 2, 't': 'gem'},
|
||||||
|
{'x': 4, 'y': 2, 't': 'gem'},
|
||||||
|
],
|
||||||
|
'objectives': [
|
||||||
|
{'type': 'clearGems', 'count': 2},
|
||||||
|
],
|
||||||
|
'stars': _stars,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Objective is to clear one line.
|
||||||
|
StageConfig _lineStage() => StageConfig.fromJson({
|
||||||
|
'id': 'b_line',
|
||||||
|
'seed': 1,
|
||||||
|
'moveLimit': 20,
|
||||||
|
'preset': [
|
||||||
|
{'x': 0, 'y': 0, 't': 'filled', 'c': 1},
|
||||||
|
],
|
||||||
|
'objectives': [
|
||||||
|
{'type': 'clearLines', 'count': 1},
|
||||||
|
],
|
||||||
|
'stars': _stars,
|
||||||
|
});
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('useHammer empties a filled cell without scoring or spending a move', () {
|
test('useHammer empties a filled cell without scoring or spending a move', () {
|
||||||
final e = GameEngine(_stage());
|
final e = GameEngine(_stage());
|
||||||
@@ -101,4 +151,43 @@ void main() {
|
|||||||
e.declineAndLose();
|
e.declineAndLose();
|
||||||
expect(e.useLineBomb(row: 0), isFalse);
|
expect(e.useLineBomb(row: 0), isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Boosters count toward objectives (owner decision 2026-06-18) ---
|
||||||
|
|
||||||
|
test('useHammer on a gem counts toward the gem objective and wins the stage '
|
||||||
|
'when it clears the last one', () {
|
||||||
|
final e = GameEngine(_gemStage()); // 2 gems, objective clearGems(2)
|
||||||
|
expect(e.objectives.first.current, 0);
|
||||||
|
|
||||||
|
expect(e.useHammer(0, 0), isTrue); // first gem
|
||||||
|
expect(e.objectives.first.current, 1);
|
||||||
|
expect(e.phase, GamePhase.playing);
|
||||||
|
|
||||||
|
expect(e.useHammer(3, 3), isTrue); // last gem -> objective complete
|
||||||
|
expect(e.objectives.first.current, 2);
|
||||||
|
expect(e.phase, GamePhase.won, reason: 'clearing the last gem wins');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('useHammer on a plain filled cell does not change the gem objective', () {
|
||||||
|
final e = GameEngine(_gemStage());
|
||||||
|
expect(e.useHammer(5, 5), isTrue); // a non-gem filled cell
|
||||||
|
expect(e.objectives.first.current, 0);
|
||||||
|
expect(e.phase, GamePhase.playing);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('useLineBomb counts the gems in the cleared line toward the objective',
|
||||||
|
() {
|
||||||
|
final e = GameEngine(_gemRowStage()); // 2 gems on row 2, clearGems(2)
|
||||||
|
expect(e.useLineBomb(row: 2), isTrue);
|
||||||
|
expect(e.objectives.first.current, 2);
|
||||||
|
expect(e.phase, GamePhase.won);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('useLineBomb counts as a cleared line toward the line objective', () {
|
||||||
|
final e = GameEngine(_lineStage()); // clearLines(1)
|
||||||
|
expect(e.objectives.first.current, 0);
|
||||||
|
expect(e.useLineBomb(row: 0), isTrue);
|
||||||
|
expect(e.objectives.first.current, 1);
|
||||||
|
expect(e.phase, GamePhase.won);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:block_seasons/game/models/booster.dart';
|
||||||
|
import 'package:block_seasons/l10n/gen/app_localizations.dart';
|
||||||
|
import 'package:block_seasons/ui/widgets/booster_hint.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
Widget wrap(Widget child) => MaterialApp(
|
||||||
|
locale: const Locale('en'),
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(body: Center(child: child)),
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('shows the cell hint for hammer and cancels on tap',
|
||||||
|
(tester) async {
|
||||||
|
var cancelled = false;
|
||||||
|
await tester.pumpWidget(wrap(BoosterHint(
|
||||||
|
arming: BoosterType.hammer,
|
||||||
|
accent: const Color(0xFF5B7FFF),
|
||||||
|
onCancel: () => cancelled = true,
|
||||||
|
)));
|
||||||
|
await tester.pump(const Duration(milliseconds: 250)); // settle entrance
|
||||||
|
|
||||||
|
expect(find.text('Tap a cell'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.byType(BoosterHint));
|
||||||
|
expect(cancelled, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows the line hint for the line bomb', (tester) async {
|
||||||
|
await tester.pumpWidget(wrap(BoosterHint(
|
||||||
|
arming: BoosterType.lineBomb,
|
||||||
|
accent: const Color(0xFF5B7FFF),
|
||||||
|
onCancel: () {},
|
||||||
|
)));
|
||||||
|
await tester.pump(const Duration(milliseconds: 250));
|
||||||
|
|
||||||
|
expect(find.text('Tap a row or column'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('ignores taps when nothing is armed', (tester) async {
|
||||||
|
var cancelled = false;
|
||||||
|
await tester.pumpWidget(wrap(BoosterHint(
|
||||||
|
arming: null,
|
||||||
|
accent: const Color(0xFF5B7FFF),
|
||||||
|
onCancel: () => cancelled = true,
|
||||||
|
)));
|
||||||
|
await tester.pump(const Duration(milliseconds: 250));
|
||||||
|
|
||||||
|
await tester.tap(find.byType(BoosterHint), warnIfMissed: false);
|
||||||
|
expect(cancelled, isFalse);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user