game_screen.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. import 'package:flame/game.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:shake/shake.dart';
  5. import '../l10n/app_localizations.dart';
  6. import '../game/zentap_game.dart';
  7. import '../utils/colors.dart';
  8. import '../utils/settings_manager.dart';
  9. import '../utils/haptic_utils.dart';
  10. import '../utils/theme_notifier.dart';
  11. import 'components/animated_background.dart';
  12. import '../utils/app_lifecycle_manager.dart';
  13. class GameScreen extends StatefulWidget {
  14. final bool isZenMode;
  15. const GameScreen({
  16. super.key,
  17. required this.isZenMode,
  18. });
  19. @override
  20. State<GameScreen> createState() => _GameScreenState();
  21. }
  22. class _GameScreenState extends State<GameScreen> {
  23. late ZenTapGame game;
  24. bool _isPaused = false;
  25. ShakeDetector? _shakeDetector;
  26. final GlobalKey _backgroundKey = GlobalKey();
  27. @override
  28. void initState() {
  29. super.initState();
  30. game = ZenTapGame();
  31. game.setZenMode(widget.isZenMode);
  32. // Register the game instance for app lifecycle management
  33. AppLifecycleManager.instance.setCurrentGame(game);
  34. // Hide system UI for immersive experience
  35. SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
  36. }
  37. @override
  38. void didChangeDependencies() {
  39. super.didChangeDependencies();
  40. // Set localized strings for the game
  41. final l10n = AppLocalizations.of(context)!;
  42. game.setLocalizedStrings(
  43. zenMode: l10n.zenModeGame,
  44. relaxationPoints: l10n.relaxationPoints,
  45. time: l10n.time,
  46. );
  47. // Initialize shake detector only on mobile platforms
  48. _initializeShakeDetector();
  49. }
  50. void _initializeShakeDetector() async {
  51. if (_shakeDetector != null) return; // Already initialized
  52. try {
  53. // Only initialize shake detector on mobile platforms
  54. if (Theme.of(context).platform == TargetPlatform.android ||
  55. Theme.of(context).platform == TargetPlatform.iOS) {
  56. _shakeDetector = ShakeDetector.autoStart(
  57. onPhoneShake: (ShakeEvent event) => _onPhoneShake(),
  58. minimumShakeCount: 1,
  59. shakeSlopTimeMS: 500,
  60. shakeCountResetTime: 3000,
  61. shakeThresholdGravity: 2.7,
  62. );
  63. }
  64. } catch (e) {
  65. // Shake detection not supported on this platform
  66. print('Shake detection not supported: $e');
  67. }
  68. }
  69. @override
  70. void dispose() {
  71. try {
  72. _shakeDetector?.stopListening();
  73. } catch (e) {
  74. // Shake detector might not be initialized on desktop
  75. }
  76. // Unregister the game instance from lifecycle management
  77. AppLifecycleManager.instance.setCurrentGame(null);
  78. // Restore system UI
  79. SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
  80. super.dispose();
  81. }
  82. void _onPhoneShake() async {
  83. if (!_isPaused) {
  84. // Trigger background shake animation
  85. final backgroundState = _backgroundKey.currentState;
  86. if (backgroundState != null) {
  87. (backgroundState as dynamic).triggerShake();
  88. }
  89. // Trigger game shake effects (spawn bubbles and shake existing ones)
  90. game.handleShake();
  91. // Haptic feedback
  92. if (SettingsManager.isHapticsEnabled) {
  93. await HapticUtils.vibrate(duration: 100);
  94. }
  95. }
  96. }
  97. @override
  98. Widget build(BuildContext context) {
  99. return ThemeAwareBuilder(
  100. builder: (context, theme) {
  101. return Scaffold(
  102. backgroundColor: ZenColors.currentAppBackground,
  103. body: OrientationBuilder(
  104. builder: (context, orientation) {
  105. return KeyboardListener(
  106. focusNode: FocusNode()..requestFocus(),
  107. onKeyEvent: (event) {
  108. // Add keyboard shortcut for shake simulation on desktop
  109. if (event.logicalKey == LogicalKeyboardKey.space && event is KeyDownEvent) {
  110. _onPhoneShake();
  111. }
  112. },
  113. child: Stack(
  114. children: [
  115. // Animated Background
  116. AnimatedBackground(
  117. key: _backgroundKey,
  118. isZenMode: widget.isZenMode,
  119. onShake: () {}, // Background handles its own shake animation
  120. child: Container(), // Empty container just for background
  121. ),
  122. // Game Widget with tap detection (transparent background)
  123. GestureDetector(
  124. onTapDown: (details) async {
  125. // Add haptic feedback on tap
  126. if (SettingsManager.isHapticsEnabled) {
  127. await HapticUtils.vibrate(duration: 30);
  128. }
  129. // Convert screen position to game position
  130. final position = Vector2(
  131. details.localPosition.dx,
  132. details.localPosition.dy,
  133. );
  134. game.handleTap(position);
  135. },
  136. child: GameWidget<ZenTapGame>.controlled(
  137. gameFactory: () => game,
  138. ),
  139. ),
  140. // Top UI Overlay - adaptive to orientation
  141. SafeArea(
  142. child: Padding(
  143. padding: const EdgeInsets.all(16.0),
  144. child: orientation == Orientation.portrait
  145. ? _buildPortraitUI()
  146. : _buildLandscapeUI(),
  147. ),
  148. ),
  149. // Pause Overlay
  150. if (_isPaused) _buildPauseOverlay(),
  151. ],
  152. ),
  153. );
  154. },
  155. ),
  156. );
  157. },
  158. );
  159. }
  160. Widget _buildPortraitUI() {
  161. return Row(
  162. children: [
  163. // Back Button
  164. IconButton(
  165. onPressed: _showExitDialog,
  166. icon: Icon(
  167. Icons.arrow_back,
  168. color: ZenColors.currentPrimaryText,
  169. size: 28,
  170. ),
  171. style: IconButton.styleFrom(
  172. backgroundColor: ZenColors.black.withValues(alpha: 0.3),
  173. shape: const CircleBorder(),
  174. ),
  175. ),
  176. const Spacer(),
  177. // Mode Indicator
  178. Container(
  179. padding: const EdgeInsets.symmetric(
  180. horizontal: 16,
  181. vertical: 8,
  182. ),
  183. decoration: BoxDecoration(
  184. color: ZenColors.black.withValues(alpha: 0.3),
  185. borderRadius: BorderRadius.circular(20),
  186. ),
  187. child: Text(
  188. widget.isZenMode
  189. ? AppLocalizations.of(context)!.zenModeGame.toUpperCase()
  190. : AppLocalizations.of(context)!.playModeGame.toUpperCase(),
  191. style: TextStyle(
  192. color: ZenColors.currentPrimaryText,
  193. fontSize: 14,
  194. fontWeight: FontWeight.w600,
  195. letterSpacing: 1.0,
  196. ),
  197. ),
  198. ),
  199. const Spacer(),
  200. // Pause Button
  201. IconButton(
  202. onPressed: _togglePause,
  203. icon: Icon(
  204. _isPaused ? Icons.play_arrow : Icons.pause,
  205. color: ZenColors.currentPrimaryText,
  206. size: 28,
  207. ),
  208. style: IconButton.styleFrom(
  209. backgroundColor: ZenColors.black.withValues(alpha: 0.3),
  210. shape: const CircleBorder(),
  211. ),
  212. ),
  213. ],
  214. );
  215. }
  216. Widget _buildLandscapeUI() {
  217. return Row(
  218. children: [
  219. // Back Button
  220. IconButton(
  221. onPressed: _showExitDialog,
  222. icon: Icon(
  223. Icons.arrow_back,
  224. color: ZenColors.currentPrimaryText,
  225. size: 24,
  226. ),
  227. style: IconButton.styleFrom(
  228. backgroundColor: ZenColors.black.withValues(alpha: 0.3),
  229. shape: const CircleBorder(),
  230. ),
  231. ),
  232. const SizedBox(width: 16),
  233. // Mode Indicator (smaller in landscape)
  234. Container(
  235. padding: const EdgeInsets.symmetric(
  236. horizontal: 12,
  237. vertical: 6,
  238. ),
  239. decoration: BoxDecoration(
  240. color: ZenColors.black.withValues(alpha: 0.3),
  241. borderRadius: BorderRadius.circular(16),
  242. ),
  243. child: Text(
  244. widget.isZenMode
  245. ? AppLocalizations.of(context)!.zenModeShort
  246. : AppLocalizations.of(context)!.playModeShort,
  247. style: TextStyle(
  248. color: ZenColors.currentPrimaryText,
  249. fontSize: 12,
  250. fontWeight: FontWeight.w600,
  251. letterSpacing: 1.0,
  252. ),
  253. ),
  254. ),
  255. const Spacer(),
  256. // Pause Button
  257. IconButton(
  258. onPressed: _togglePause,
  259. icon: Icon(
  260. _isPaused ? Icons.play_arrow : Icons.pause,
  261. color: ZenColors.currentPrimaryText,
  262. size: 24,
  263. ),
  264. style: IconButton.styleFrom(
  265. backgroundColor: ZenColors.black.withValues(alpha: 0.3),
  266. shape: const CircleBorder(),
  267. ),
  268. ),
  269. ],
  270. );
  271. }
  272. Widget _buildPauseOverlay() {
  273. return Container(
  274. color: ZenColors.black.withValues(alpha: 0.8),
  275. child: Center(
  276. child: Container(
  277. margin: const EdgeInsets.all(40),
  278. padding: const EdgeInsets.all(30),
  279. decoration: BoxDecoration(
  280. color: ZenColors.currentUiElements,
  281. borderRadius: BorderRadius.circular(20),
  282. ),
  283. child: Column(
  284. mainAxisSize: MainAxisSize.min,
  285. children: [
  286. Text(
  287. AppLocalizations.of(context)!.paused,
  288. style: TextStyle(
  289. color: ZenColors.currentPrimaryText,
  290. fontSize: 32,
  291. fontWeight: FontWeight.bold,
  292. ),
  293. ),
  294. const SizedBox(height: 20),
  295. Text(
  296. AppLocalizations.of(context)!.takeAMomentToBreathe,
  297. style: TextStyle(
  298. color: ZenColors.currentSecondaryText,
  299. fontSize: 16,
  300. ),
  301. ),
  302. const SizedBox(height: 30),
  303. // Resume Button
  304. ElevatedButton(
  305. onPressed: _togglePause,
  306. style: ElevatedButton.styleFrom(
  307. backgroundColor: ZenColors.currentButtonBackground,
  308. foregroundColor: ZenColors.currentButtonText,
  309. padding: const EdgeInsets.symmetric(
  310. horizontal: 40,
  311. vertical: 15,
  312. ),
  313. shape: RoundedRectangleBorder(
  314. borderRadius: BorderRadius.circular(12),
  315. ),
  316. ),
  317. child: Text(
  318. AppLocalizations.of(context)!.resume,
  319. style: TextStyle(
  320. fontSize: 18,
  321. fontWeight: FontWeight.w600,
  322. ),
  323. ),
  324. ),
  325. ],
  326. ),
  327. ),
  328. ),
  329. );
  330. }
  331. void _togglePause() async {
  332. if (SettingsManager.isHapticsEnabled) {
  333. await HapticUtils.vibrate(duration: 50);
  334. }
  335. setState(() {
  336. _isPaused = !_isPaused;
  337. if (_isPaused) {
  338. game.pauseGame();
  339. } else {
  340. game.resumeGame();
  341. }
  342. });
  343. }
  344. void _showExitDialog() async {
  345. if (!mounted) return;
  346. if (SettingsManager.isHapticsEnabled) {
  347. await HapticUtils.vibrate(duration: 50);
  348. }
  349. if (!mounted) return;
  350. showDialog(
  351. context: context,
  352. builder: (BuildContext context) {
  353. return AlertDialog(
  354. backgroundColor: ZenColors.currentUiElements,
  355. title: Text(
  356. AppLocalizations.of(context)!.leaveGame,
  357. style: TextStyle(color: ZenColors.currentPrimaryText),
  358. ),
  359. content: Text(
  360. AppLocalizations.of(context)!.leaveGameConfirm,
  361. style: TextStyle(color: ZenColors.currentSecondaryText),
  362. ),
  363. actions: [
  364. TextButton(
  365. onPressed: () => Navigator.of(context).pop(),
  366. child: Text(
  367. AppLocalizations.of(context)!.cancel,
  368. style: TextStyle(color: ZenColors.currentLinks),
  369. ),
  370. ),
  371. ElevatedButton(
  372. onPressed: () {
  373. Navigator.of(context).pop(); // Close dialog
  374. Navigator.of(context).pop(true); // Return to main menu with result
  375. },
  376. style: ElevatedButton.styleFrom(
  377. backgroundColor: ZenColors.red,
  378. foregroundColor: ZenColors.white,
  379. ),
  380. child: Text(AppLocalizations.of(context)!.leave),
  381. ),
  382. ],
  383. );
  384. },
  385. );
  386. }
  387. }