game_screen.dart 13 KB

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