game_screen.dart 12 KB

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