import 'package:flame/components.dart'; import 'package:flame/effects.dart'; import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'dart:math'; import '../utils/colors.dart'; import '../utils/score_manager.dart'; import '../utils/tilt_detector.dart'; import '../utils/google_play_games_manager.dart'; import 'components/bubble.dart'; import 'components/bubble_spawner.dart'; import 'audio/audio_manager.dart'; class ZenTapGame extends FlameGame with HasCollisionDetection { static const String routeName = '/game'; TextComponent? scoreText; TextComponent? timerText; int score = 0; bool isZenMode = false; double gameTime = 0.0; bool gameActive = true; BubbleSpawner? bubbleSpawner; final AudioManager audioManager = AudioManager(); final TiltDetector _tiltDetector = TiltDetector(); final TiltDetector tiltDetector = TiltDetector(); final GooglePlayGamesManager _gamesManager = GooglePlayGamesManager.instance; // Google Play Games tracking int _totalBubblesPopped = 0; double _sessionStartTime = 0.0; bool _perfectSession = true; // No missed bubbles // Performance optimization: cache last displayed second to avoid unnecessary text updates int _lastDisplayedSecond = -1; @override Color backgroundColor() => Colors.transparent; @override Future onLoad() async { await super.onLoad(); // Initialize Google Play Games await _gamesManager.initialize(); // Track session start time _sessionStartTime = DateTime.now().millisecondsSinceEpoch / 1000.0; // Initialize score manager and audio await ScoreManager.initialize(); await audioManager.initialize(); // Initialize score display scoreText = TextComponent( text: isZenMode ? 'Zen Mode' : 'Relaxation Points: $score', textRenderer: TextPaint( style: const TextStyle( color: ZenColors.scoreText, fontSize: 24, fontWeight: FontWeight.w500, ), ), position: Vector2(20, 50), ); add(scoreText!); // Initialize timer display (only in regular mode) if (!isZenMode) { timerText = TextComponent( text: 'Time: ${_formatTime(gameTime)}', textRenderer: TextPaint( style: const TextStyle( color: ZenColors.timerText, fontSize: 18, ), ), position: Vector2(20, 80), ); add(timerText!); } // Add initial game elements await _initializeGame(); // Start background music after all initialization is complete await audioManager.playBackgroundMusic(); // Start tilt detection _initializeTiltDetection(); } void _initializeTiltDetection() { _tiltDetector.startListening( onTiltChanged: (tiltAngle) { _applyTiltToAllBubbles(_tiltDetector.normalizedTilt); }, ); } void _applyTiltToAllBubbles(double tiltStrength) { if (bubbleSpawner == null) return; final activeBubbles = bubbleSpawner!.getActiveBubbles(); for (final bubble in activeBubbles) { bubble.applyTiltForce(tiltStrength); } } Future _initializeGame() async { // Initialize bubble spawner bubbleSpawner = BubbleSpawner( onBubblePopped: _onBubblePopped, ); add(bubbleSpawner!); // Instruction text removed for cleaner interface // Background music is now started in onLoad() after all initialization } @override void update(double dt) { super.update(dt); if (gameActive && !isZenMode) { gameTime += dt; // Performance optimization: only update timer text when the second changes final currentSecond = gameTime.toInt(); if (currentSecond != _lastDisplayedSecond) { _lastDisplayedSecond = currentSecond; _updateTimer(); } } } @override void onGameResize(Vector2 size) { super.onGameResize(size); // Update text positions when screen size changes if (scoreText != null) { scoreText!.position = Vector2(20, 50); } if (timerText != null) { timerText!.position = Vector2(20, 80); } // Reposition bubbles that might be off-screen _repositionBubblesInBounds(); } void _repositionBubblesInBounds() { if (bubbleSpawner == null) return; const margin = 120.0; final minX = margin; final maxX = size.x - margin; final minY = margin + 100; final maxY = size.y - margin; if (maxX <= minX || maxY <= minY) return; // Performance optimization: use bubble spawner's cached active bubbles final activeBubbles = bubbleSpawner!.getActiveBubbles(); for (final bubble in activeBubbles) { if (!bubble.isPopping) { bool needsRepositioning = false; Vector2 newPosition = bubble.position.clone(); if (bubble.position.x < minX) { newPosition.x = minX; needsRepositioning = true; } else if (bubble.position.x > maxX) { newPosition.x = maxX; needsRepositioning = true; } if (bubble.position.y < minY) { newPosition.y = minY; needsRepositioning = true; } else if (bubble.position.y > maxY) { newPosition.y = maxY; needsRepositioning = true; } if (needsRepositioning) { bubble.position = newPosition; } } } } void handleTap(Vector2 position) { if (!gameActive) return; // Create a bubble at tap position if in zen mode or no bubble was hit if (isZenMode) { bubbleSpawner?.spawnBubbleAt(position); } // Add visual feedback at tap position _addTapEffect(position); } void _onBubblePopped(Bubble bubble, bool userTriggered) { // Play pop sound and haptic feedback audioManager.playBubblePop(); // Track Google Play Games statistics if (userTriggered) { _totalBubblesPopped++; // Check for perfect session achievement after significant bubbles if (_totalBubblesPopped >= 50 && _perfectSession) { _gamesManager.unlockAchievement( GooglePlayGamesManager.achievementPerfectSession ); } } else { // User missed a bubble (it timed out) _perfectSession = false; } if (!isZenMode && userTriggered) { // Only increment score for user-triggered pops in regular mode score += 10; // 10 points per bubble ScoreManager.addScore(10); _updateScore(); // Check for speed achievements _gamesManager.checkScoreAchievements(score, gameTime); } // Check bubble-based achievements _gamesManager.checkBubbleAchievements(_totalBubblesPopped); } void _updateScore() { scoreText?.text = 'Relaxation Points: $score'; } void _updateTimer() { timerText?.text = 'Time: ${_formatTime(gameTime)}'; } // Helper method to format time as MM:SS String _formatTime(double seconds) { final minutes = (seconds / 60).floor(); final remainingSeconds = (seconds % 60).floor(); return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}'; } void _addTapEffect(Vector2 position) { // Create a simple tap effect circle with animation final tapEffect = CircleComponent( radius: 15, paint: Paint() ..color = ZenColors.bubblePopEffect.withValues(alpha: 0.6) ..style = PaintingStyle.stroke ..strokeWidth = 2, position: position, anchor: Anchor.center, ); add(tapEffect); // Add scaling animation tapEffect.add( ScaleEffect.to( Vector2.all(2.0), EffectController(duration: 0.3), ), ); // Add fade animation tapEffect.add( OpacityEffect.to( 0.0, EffectController(duration: 0.3), ), ); // Remove the effect after animation Future.delayed(const Duration(milliseconds: 300), () { if (tapEffect.isMounted) { remove(tapEffect); } }); } void setZenMode(bool zenMode) { isZenMode = zenMode; if (scoreText != null) { if (zenMode) { scoreText!.text = 'Zen Mode'; score = 0; // Hide timer in zen mode timerText?.removeFromParent(); timerText = null; } else { scoreText!.text = 'Relaxation Points: $score'; // Show timer in regular mode if (timerText == null) { timerText = TextComponent( text: 'Time: ${_formatTime(gameTime)}', textRenderer: TextPaint( style: const TextStyle( color: ZenColors.timerText, fontSize: 18, ), ), position: Vector2(20, 80), ); add(timerText!); } } } // Update bubble spawner behavior bubbleSpawner?.setActive(!zenMode); } void resetGame() { score = 0; gameTime = 0.0; gameActive = true; _updateScore(); _updateTimer(); // Clear all bubbles bubbleSpawner?.clearAllBubbles(); } void pauseGame() { paused = true; gameActive = false; audioManager.pauseBackgroundMusic(); _tiltDetector.stopListening(); } void resumeGame() { paused = false; gameActive = true; audioManager.resumeBackgroundMusic(); _initializeTiltDetection(); } /// End the game session and submit results to Google Play Games Future endGameSession() async { gameActive = false; final currentTime = DateTime.now().millisecondsSinceEpoch / 1000.0; final sessionLength = currentTime - _sessionStartTime; // Check for zen mode achievements if (isZenMode) { await _gamesManager.checkZenModeAchievements(sessionLength); } // Submit final results to Google Play Games await _gamesManager.submitGameResults( finalScore: score, isZenMode: isZenMode, totalBubblesPopped: _totalBubblesPopped, sessionLength: sessionLength, ); // Score is already tracked by ScoreManager.addScore() calls during gameplay } void handleShake() { if (!gameActive) return; // Spawn 2-4 extra bubbles on shake final random = Random(); final bubbleCount = 2 + random.nextInt(3); // 2-4 bubbles for (int i = 0; i < bubbleCount; i++) { final position = Vector2( 50 + random.nextDouble() * (size.x - 100), 100 + random.nextDouble() * (size.y - 200), ); bubbleSpawner?.spawnBubbleAt(position); } // Shake all existing bubbles _shakeAllBubbles(); } void _shakeAllBubbles() { // Performance optimization: use bubble spawner's cached active bubbles if (bubbleSpawner == null) return; final activeBubbles = bubbleSpawner!.getActiveBubbles(); for (final bubble in activeBubbles) { if (!bubble.isPopping) { bubble.addShakeEffect(); } } } @override void onRemove() { // End the game session and submit results endGameSession(); audioManager.stopBackgroundMusic(); _tiltDetector.stopListening(); super.onRemove(); } }