| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- import 'dart:math';
- import 'dart:async';
- import 'package:flame/components.dart';
- import 'package:flame/effects.dart';
- import 'package:flame/events.dart';
- import 'package:flame/collisions.dart';
- import 'package:flutter/material.dart';
- import '../../utils/colors.dart';
- class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, CollisionCallbacks {
- static const double startSize = 60.0; // Increased from 40.0
- static const double maxSize = 120.0; // Increased from 100.0
- static const double lifecycleDuration = 12.0; // seconds
-
- late String bubbleImagePath;
- late Color bubbleColor;
- bool isPopping = false;
- bool poppedByUser = false;
- Function(Bubble, bool)? onPop;
-
- double _age = 0.0;
- double _lifecycleProgress = 0.0;
- late double _targetSize;
-
- // Physics properties for collision and movement
- Vector2 velocity = Vector2.zero();
- static const double maxSpeed = 50.0;
- static const double friction = 0.98;
- static const double collisionDamping = 0.7;
- double _floatTimer = 0.0;
-
- Bubble({
- required Vector2 position,
- this.onPop,
- }) : super(
- size: Vector2.all(startSize),
- position: position,
- anchor: Anchor.center,
- ) {
- final random = Random();
- _targetSize = (maxSize * 0.6) + (random.nextDouble() * maxSize * 0.4); // 60-100% of maxSize
- }
- @override
- Future<void> onLoad() async {
- // Choose random bubble image and set corresponding color
- final bubbleData = _getRandomBubbleData();
- bubbleImagePath = bubbleData['path'];
- bubbleColor = bubbleData['color'];
-
- // Load the sprite
- sprite = await Sprite.load(bubbleImagePath);
-
- await super.onLoad();
-
- // Add circular collision hitbox
- add(CircleHitbox(
- radius: _targetSize / 2,
- anchor: Anchor.center,
- ));
-
- // Start with zero scale for smooth entrance animation
- scale = Vector2.zero();
-
- // Add smooth entrance animation
- _addEntranceAnimation();
-
- // Add varied, organic floating animations
- _addRandomFloatingAnimation();
- _addRandomRotationAnimation();
-
- // Initialize random velocity for natural movement
- final random = Random();
- velocity = Vector2(
- (random.nextDouble() - 0.5) * 20, // -10 to +10
- (random.nextDouble() - 0.5) * 20, // -10 to +10
- );
- }
- @override
- void update(double dt) {
- super.update(dt);
-
- if (isPopping) return;
-
- // Update age and lifecycle
- _age += dt;
- _lifecycleProgress = (_age / lifecycleDuration).clamp(0.0, 1.0);
-
- // Update size based on lifecycle (grow from startSize to targetSize)
- final currentSize = startSize + (_targetSize - startSize) * _lifecycleProgress;
- size = Vector2.all(currentSize);
-
- // Update opacity based on lifecycle (fade out towards the end)
- final opacityFactor = 1.0 - (_lifecycleProgress * 0.6); // Fade to 40% opacity
- opacity = opacityFactor.clamp(0.4, 1.0);
-
- // Physics-based movement
- _updatePhysics(dt);
-
- // Keep bubble within screen bounds
- _keepInBounds();
-
- // Auto-pop when lifecycle is complete
- if (_lifecycleProgress >= 1.0) {
- pop(userTriggered: false);
- }
- }
-
- void _updatePhysics(double dt) {
- // Apply velocity to position
- position.add(velocity * dt);
-
- // Apply friction
- velocity.scale(friction);
-
- // Add periodic floating effects
- _floatTimer += dt;
- if (_floatTimer > 2.0) {
- _floatTimer = 0.0;
- final random = Random();
- final floatForce = Vector2(
- (random.nextDouble() - 0.5) * 8, // -4 to +4 horizontal
- (random.nextDouble() - 0.5) * 8, // -4 to +4 vertical
- );
- velocity.add(floatForce);
- }
-
- // Clamp velocity to max speed
- if (velocity.length > maxSpeed) {
- velocity.normalize();
- velocity.scale(maxSpeed);
- }
- }
-
- void _keepInBounds() {
- const margin = 60.0;
- final gameSize = game.size;
-
- // Bounce off screen edges
- if (position.x < margin) {
- position.x = margin;
- velocity.x = velocity.x.abs(); // Bounce right
- } else if (position.x > gameSize.x - margin) {
- position.x = gameSize.x - margin;
- velocity.x = -velocity.x.abs(); // Bounce left
- }
-
- if (position.y < margin + 100) { // Account for UI at top
- position.y = margin + 100;
- velocity.y = velocity.y.abs(); // Bounce down
- } else if (position.y > gameSize.y - margin) {
- position.y = gameSize.y - margin;
- velocity.y = -velocity.y.abs(); // Bounce up
- }
- }
- Map<String, dynamic> _getRandomBubbleData() {
- final random = Random();
- final bubbleTypes = [
- {
- 'path': 'bubble_blue.png',
- 'color': ZenColors.defaultLink,
- },
- {
- 'path': 'bubble_cyan.png',
- 'color': ZenColors.buttonBackground,
- },
- {
- 'path': 'bubble_green.png',
- 'color': const Color(0xFF00FF80),
- },
- {
- 'path': 'bubble_purple.png',
- 'color': ZenColors.lightModeHover,
- },
- ];
- return bubbleTypes[random.nextInt(bubbleTypes.length)];
- }
- void _addRandomFloatingAnimation() {
- final random = Random();
-
- // Add a small upward bias to the initial velocity
- velocity.y += -5 - random.nextDouble() * 10; // -5 to -15 upward bias
-
- // The floating effect will now be handled by the physics system
- // and periodic velocity changes in the update method
- }
- void _addEntranceAnimation() {
- // Smooth scale-in animation with bounce effect
- add(
- ScaleEffect.to(
- Vector2.all(1.0),
- EffectController(
- duration: 0.6,
- curve: Curves.elasticOut,
- ),
- ),
- );
-
- // Optional fade-in animation
- opacity = 0.0;
- add(
- OpacityEffect.to(
- 1.0,
- EffectController(
- duration: 0.4,
- curve: Curves.easeOut,
- ),
- ),
- );
- }
- void _addRandomRotationAnimation() {
- final random = Random();
-
- // 30% chance for rotation animation
- if (random.nextDouble() < 0.3) {
- // Small random rotation between -0.2 and 0.2 radians
- final rotationAngle = (random.nextDouble() - 0.5) * 0.4;
-
- // Random rotation duration between 3 and 8 seconds
- final rotationDuration = 3.0 + random.nextDouble() * 5.0;
-
- // Random delay
- final delay = random.nextDouble() * 3.0;
-
- Future.delayed(Duration(milliseconds: (delay * 1000).round()), () {
- if (isMounted) {
- add(
- RotateEffect.by(
- rotationAngle,
- EffectController(
- duration: rotationDuration,
- alternate: true,
- infinite: true,
- curve: Curves.easeInOut,
- ),
- ),
- );
- }
- });
- }
- }
- @override
- bool onTapDown(TapDownEvent event) {
- if (!isPopping) {
- pop(userTriggered: true);
- }
- return true;
- }
- void pop({bool userTriggered = false}) {
- if (isPopping) return;
-
- isPopping = true;
- poppedByUser = userTriggered;
-
- // Create exciting multi-stage pop animation
-
- // Stage 1: Quick expand (0.1s)
- final expandEffect = ScaleEffect.to(
- Vector2.all(1.8),
- EffectController(duration: 0.1, curve: Curves.easeOut),
- );
-
- // Stage 2: Slight compression (0.05s)
- final compressEffect = ScaleEffect.to(
- Vector2.all(0.8),
- EffectController(duration: 0.05, curve: Curves.easeIn),
- );
-
- // Stage 3: Final explosion (0.15s)
- final explodeEffect = ScaleEffect.to(
- Vector2.all(2.5),
- EffectController(duration: 0.15, curve: Curves.easeOut),
- );
-
- // Fade out effect
- final fadeEffect = OpacityEffect.to(
- 0.0,
- EffectController(duration: 0.3, curve: Curves.easeIn),
- );
-
- // Rotation effect for more dynamic feel
- final rotateEffect = RotateEffect.by(
- 0.5, // Half rotation
- EffectController(duration: 0.3),
- );
-
- // Chain the scale effects
- add(expandEffect);
- expandEffect.onComplete = () {
- if (isMounted) {
- add(compressEffect);
- compressEffect.onComplete = () {
- if (isMounted) {
- add(explodeEffect);
- }
- };
- }
- };
-
- add(fadeEffect);
- add(rotateEffect);
-
- // Create enhanced particle effects
- _createExcitingPopParticles();
-
- // Notify parent and remove bubble
- onPop?.call(this, userTriggered);
-
- Future.delayed(const Duration(milliseconds: 300), () {
- if (isMounted) {
- removeFromParent();
- }
- });
- }
- void _createExcitingPopParticles() {
- final random = Random();
- const particleCount = 4; // Further reduced for better performance
-
- // Use object pooling approach - create fewer, simpler particles
- for (int i = 0; i < particleCount; i++) {
- final angle = (i / particleCount) * 2 * pi;
-
- // Simplified particle creation
- final baseSpeed = 50 + random.nextDouble() * 30; // 50-80 speed
- final particleVelocity = Vector2(
- cos(angle) * baseSpeed,
- sin(angle) * baseSpeed,
- );
-
- // Fixed particle size for performance
- const particleSize = 3.0;
-
- // Simplified particle color
- final particleColor = bubbleColor.withValues(alpha: 0.8);
-
- final particle = CircleComponent(
- radius: particleSize,
- position: position.clone(),
- paint: Paint()
- ..color = particleColor
- ..style = PaintingStyle.fill,
- );
-
- parent?.add(particle);
-
- // Combine effects for better performance
- final combinedEffect = MoveEffect.by(
- particleVelocity + Vector2(0, 40), // Combined movement + gravity
- EffectController(duration: 0.3, curve: Curves.easeOut), // Faster animation
- );
-
- particle.add(combinedEffect);
-
- // Single fade effect instead of multiple effects
- particle.add(
- OpacityEffect.to(
- 0.0,
- EffectController(duration: 0.3, curve: Curves.easeIn),
- ),
- );
-
- // Faster cleanup
- Future.delayed(const Duration(milliseconds: 300), () {
- if (particle.isMounted) {
- particle.removeFromParent();
- }
- });
- }
- }
- /// Add shake effect to this bubble (called when phone is shaken)
- void addShakeEffect() {
- if (isPopping) return;
-
- final random = Random();
-
- // Random shake direction
- final shakeDirection = Vector2(
- (random.nextDouble() - 0.5) * 60, // -30 to +30
- (random.nextDouble() - 0.5) * 60, // -30 to +30
- );
-
- // Add shake movement effect
- add(
- MoveEffect.by(
- shakeDirection,
- EffectController(
- duration: 0.4,
- alternate: true,
- curve: Curves.elasticOut,
- ),
- ),
- );
-
- // Add scale bounce effect
- add(
- ScaleEffect.by(
- Vector2.all(0.3),
- EffectController(
- duration: 0.2,
- alternate: true,
- curve: Curves.elasticOut,
- ),
- ),
- );
-
- // Add slight rotation effect
- add(
- RotateEffect.by(
- (random.nextDouble() - 0.5) * 0.4, // Random rotation
- EffectController(
- duration: 0.3,
- alternate: true,
- curve: Curves.elasticOut,
- ),
- ),
- );
- }
- // Collision detection methods
- @override
- bool onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
- super.onCollision(intersectionPoints, other);
-
- if (other is Bubble && !isPopping && !other.isPopping) {
- _handleBubbleCollision(other, intersectionPoints);
- }
- return true;
- }
-
- void _handleBubbleCollision(Bubble other, Set<Vector2> intersectionPoints) {
- if (intersectionPoints.isEmpty) return;
-
- // Calculate collision direction
- final direction = (position - other.position).normalized();
-
- // Apply collision response - bubbles bounce off each other
- final collisionForce = 30.0;
- velocity.add(direction * collisionForce);
- other.velocity.add(direction * -collisionForce);
-
- // Apply damping to prevent infinite acceleration
- velocity.scale(collisionDamping);
- other.velocity.scale(collisionDamping);
-
- // Add slight separation to prevent sticking
- final separation = direction * 2.0;
- position.add(separation);
- other.position.sub(separation);
- }
-
- /// Apply tilt force to bubble based on device orientation
- void applyTiltForce(double tiltStrength) {
- if (isPopping) return;
-
- // Apply force opposite to tilt direction
- // If device tilts right, bubbles move left and vice versa
- final tiltForce = -tiltStrength * 15.0; // Adjust force strength
- velocity.x += tiltForce;
-
- // Add slight vertical component for more natural movement
- velocity.y += tiltForce * 0.3;
- }
- }
|