zentap_game.dart 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. import 'package:flame/components.dart';
  2. import 'package:flame/effects.dart';
  3. import 'package:flame/game.dart';
  4. import 'package:flutter/material.dart';
  5. import 'dart:math';
  6. import '../utils/colors.dart';
  7. import '../utils/score_manager.dart';
  8. import '../utils/tilt_detector.dart';
  9. import 'components/bubble.dart';
  10. import 'components/bubble_spawner.dart';
  11. import 'audio/audio_manager.dart';
  12. class ZenTapGame extends FlameGame with HasCollisionDetection {
  13. static const String routeName = '/game';
  14. TextComponent? scoreText;
  15. TextComponent? timerText;
  16. int score = 0;
  17. bool isZenMode = false;
  18. double gameTime = 0.0;
  19. bool gameActive = true;
  20. BubbleSpawner? bubbleSpawner;
  21. final AudioManager audioManager = AudioManager();
  22. final TiltDetector _tiltDetector = TiltDetector();
  23. final TiltDetector tiltDetector = TiltDetector();
  24. // Performance optimization: cache last displayed second to avoid unnecessary text updates
  25. int _lastDisplayedSecond = -1;
  26. @override
  27. Color backgroundColor() => Colors.transparent;
  28. @override
  29. Future<void> onLoad() async {
  30. await super.onLoad();
  31. // Initialize score manager and audio
  32. await ScoreManager.initialize();
  33. await audioManager.initialize();
  34. // Initialize score display
  35. scoreText = TextComponent(
  36. text: isZenMode ? 'Zen Mode' : 'Relaxation Points: $score',
  37. textRenderer: TextPaint(
  38. style: const TextStyle(
  39. color: ZenColors.scoreText,
  40. fontSize: 24,
  41. fontWeight: FontWeight.w500,
  42. ),
  43. ),
  44. position: Vector2(20, 50),
  45. );
  46. add(scoreText!);
  47. // Initialize timer display (only in regular mode)
  48. if (!isZenMode) {
  49. timerText = TextComponent(
  50. text: 'Time: ${_formatTime(gameTime)}',
  51. textRenderer: TextPaint(
  52. style: const TextStyle(
  53. color: ZenColors.timerText,
  54. fontSize: 18,
  55. ),
  56. ),
  57. position: Vector2(20, 80),
  58. );
  59. add(timerText!);
  60. }
  61. // Add initial game elements
  62. await _initializeGame();
  63. // Start background music after all initialization is complete
  64. await audioManager.playBackgroundMusic();
  65. // Start tilt detection
  66. _initializeTiltDetection();
  67. }
  68. void _initializeTiltDetection() {
  69. _tiltDetector.startListening(
  70. onTiltChanged: (tiltAngle) {
  71. _applyTiltToAllBubbles(_tiltDetector.normalizedTilt);
  72. },
  73. );
  74. }
  75. void _applyTiltToAllBubbles(double tiltStrength) {
  76. if (bubbleSpawner == null) return;
  77. final activeBubbles = bubbleSpawner!.getActiveBubbles();
  78. for (final bubble in activeBubbles) {
  79. bubble.applyTiltForce(tiltStrength);
  80. }
  81. }
  82. Future<void> _initializeGame() async {
  83. // Initialize bubble spawner
  84. bubbleSpawner = BubbleSpawner(
  85. onBubblePopped: _onBubblePopped,
  86. );
  87. add(bubbleSpawner!);
  88. // Instruction text removed for cleaner interface
  89. // Background music is now started in onLoad() after all initialization
  90. }
  91. @override
  92. void update(double dt) {
  93. super.update(dt);
  94. if (gameActive && !isZenMode) {
  95. gameTime += dt;
  96. // Performance optimization: only update timer text when the second changes
  97. final currentSecond = gameTime.toInt();
  98. if (currentSecond != _lastDisplayedSecond) {
  99. _lastDisplayedSecond = currentSecond;
  100. _updateTimer();
  101. }
  102. }
  103. }
  104. @override
  105. void onGameResize(Vector2 size) {
  106. super.onGameResize(size);
  107. // Update text positions when screen size changes
  108. if (scoreText != null) {
  109. scoreText!.position = Vector2(20, 50);
  110. }
  111. if (timerText != null) {
  112. timerText!.position = Vector2(20, 80);
  113. }
  114. // Reposition bubbles that might be off-screen
  115. _repositionBubblesInBounds();
  116. }
  117. void _repositionBubblesInBounds() {
  118. if (bubbleSpawner == null) return;
  119. const margin = 120.0;
  120. final minX = margin;
  121. final maxX = size.x - margin;
  122. final minY = margin + 100;
  123. final maxY = size.y - margin;
  124. if (maxX <= minX || maxY <= minY) return;
  125. // Performance optimization: use bubble spawner's cached active bubbles
  126. final activeBubbles = bubbleSpawner!.getActiveBubbles();
  127. for (final bubble in activeBubbles) {
  128. if (!bubble.isPopping) {
  129. bool needsRepositioning = false;
  130. Vector2 newPosition = bubble.position.clone();
  131. if (bubble.position.x < minX) {
  132. newPosition.x = minX;
  133. needsRepositioning = true;
  134. } else if (bubble.position.x > maxX) {
  135. newPosition.x = maxX;
  136. needsRepositioning = true;
  137. }
  138. if (bubble.position.y < minY) {
  139. newPosition.y = minY;
  140. needsRepositioning = true;
  141. } else if (bubble.position.y > maxY) {
  142. newPosition.y = maxY;
  143. needsRepositioning = true;
  144. }
  145. if (needsRepositioning) {
  146. bubble.position = newPosition;
  147. }
  148. }
  149. }
  150. }
  151. void handleTap(Vector2 position) {
  152. if (!gameActive) return;
  153. // Create a bubble at tap position if in zen mode or no bubble was hit
  154. if (isZenMode) {
  155. bubbleSpawner?.spawnBubbleAt(position);
  156. }
  157. // Add visual feedback at tap position
  158. _addTapEffect(position);
  159. }
  160. void _onBubblePopped(Bubble bubble, bool userTriggered) {
  161. // Play pop sound and haptic feedback
  162. audioManager.playBubblePop();
  163. if (!isZenMode && userTriggered) {
  164. // Only increment score for user-triggered pops in regular mode
  165. score += 10; // 10 points per bubble
  166. ScoreManager.addScore(10);
  167. _updateScore();
  168. }
  169. }
  170. void _updateScore() {
  171. scoreText?.text = 'Relaxation Points: $score';
  172. }
  173. void _updateTimer() {
  174. timerText?.text = 'Time: ${_formatTime(gameTime)}';
  175. }
  176. // Helper method to format time as MM:SS
  177. String _formatTime(double seconds) {
  178. final minutes = (seconds / 60).floor();
  179. final remainingSeconds = (seconds % 60).floor();
  180. return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}';
  181. }
  182. void _addTapEffect(Vector2 position) {
  183. // Create a simple tap effect circle with animation
  184. final tapEffect = CircleComponent(
  185. radius: 15,
  186. paint: Paint()
  187. ..color = ZenColors.bubblePopEffect.withValues(alpha: 0.6)
  188. ..style = PaintingStyle.stroke
  189. ..strokeWidth = 2,
  190. position: position,
  191. anchor: Anchor.center,
  192. );
  193. add(tapEffect);
  194. // Add scaling animation
  195. tapEffect.add(
  196. ScaleEffect.to(
  197. Vector2.all(2.0),
  198. EffectController(duration: 0.3),
  199. ),
  200. );
  201. // Add fade animation
  202. tapEffect.add(
  203. OpacityEffect.to(
  204. 0.0,
  205. EffectController(duration: 0.3),
  206. ),
  207. );
  208. // Remove the effect after animation
  209. Future.delayed(const Duration(milliseconds: 300), () {
  210. if (tapEffect.isMounted) {
  211. remove(tapEffect);
  212. }
  213. });
  214. }
  215. void setZenMode(bool zenMode) {
  216. isZenMode = zenMode;
  217. if (scoreText != null) {
  218. if (zenMode) {
  219. scoreText!.text = 'Zen Mode';
  220. score = 0;
  221. // Hide timer in zen mode
  222. timerText?.removeFromParent();
  223. timerText = null;
  224. } else {
  225. scoreText!.text = 'Relaxation Points: $score';
  226. // Show timer in regular mode
  227. if (timerText == null) {
  228. timerText = TextComponent(
  229. text: 'Time: ${_formatTime(gameTime)}',
  230. textRenderer: TextPaint(
  231. style: const TextStyle(
  232. color: ZenColors.timerText,
  233. fontSize: 18,
  234. ),
  235. ),
  236. position: Vector2(20, 80),
  237. );
  238. add(timerText!);
  239. }
  240. }
  241. }
  242. // Update bubble spawner behavior
  243. bubbleSpawner?.setActive(!zenMode);
  244. }
  245. void resetGame() {
  246. score = 0;
  247. gameTime = 0.0;
  248. gameActive = true;
  249. _updateScore();
  250. _updateTimer();
  251. // Clear all bubbles
  252. bubbleSpawner?.clearAllBubbles();
  253. }
  254. void pauseGame() {
  255. paused = true;
  256. gameActive = false;
  257. audioManager.pauseBackgroundMusic();
  258. _tiltDetector.stopListening();
  259. }
  260. void resumeGame() {
  261. paused = false;
  262. gameActive = true;
  263. audioManager.resumeBackgroundMusic();
  264. _initializeTiltDetection();
  265. }
  266. void handleShake() {
  267. if (!gameActive) return;
  268. // Spawn 2-4 extra bubbles on shake
  269. final random = Random();
  270. final bubbleCount = 2 + random.nextInt(3); // 2-4 bubbles
  271. for (int i = 0; i < bubbleCount; i++) {
  272. final position = Vector2(
  273. 50 + random.nextDouble() * (size.x - 100),
  274. 100 + random.nextDouble() * (size.y - 200),
  275. );
  276. bubbleSpawner?.spawnBubbleAt(position);
  277. }
  278. // Shake all existing bubbles
  279. _shakeAllBubbles();
  280. }
  281. void _shakeAllBubbles() {
  282. // Performance optimization: use bubble spawner's cached active bubbles
  283. if (bubbleSpawner == null) return;
  284. final activeBubbles = bubbleSpawner!.getActiveBubbles();
  285. for (final bubble in activeBubbles) {
  286. if (!bubble.isPopping) {
  287. bubble.addShakeEffect();
  288. }
  289. }
  290. }
  291. @override
  292. void onRemove() {
  293. audioManager.stopBackgroundMusic();
  294. _tiltDetector.stopListening();
  295. super.onRemove();
  296. }
  297. }