bubble.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. import 'dart:math';
  2. import 'dart:async';
  3. import 'package:flame/components.dart';
  4. import 'package:flame/effects.dart';
  5. import 'package:flame/events.dart';
  6. import 'package:flame/collisions.dart';
  7. import 'package:flutter/material.dart';
  8. import '../../utils/colors.dart';
  9. class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, CollisionCallbacks {
  10. static const double startSize = 60.0; // Increased from 40.0
  11. static const double maxSize = 120.0; // Increased from 100.0
  12. static const double lifecycleDuration = 12.0; // seconds
  13. late String bubbleImagePath;
  14. late Color bubbleColor;
  15. bool isPopping = false;
  16. bool poppedByUser = false;
  17. Function(Bubble, bool)? onPop;
  18. double _age = 0.0;
  19. double _lifecycleProgress = 0.0;
  20. late double _targetSize;
  21. // Physics properties for collision and movement
  22. Vector2 velocity = Vector2.zero();
  23. static const double maxSpeed = 50.0;
  24. static const double friction = 0.98;
  25. static const double collisionDamping = 0.7;
  26. double _floatTimer = 0.0;
  27. Bubble({
  28. required Vector2 position,
  29. this.onPop,
  30. }) : super(
  31. size: Vector2.all(startSize),
  32. position: position,
  33. anchor: Anchor.center,
  34. ) {
  35. final random = Random();
  36. _targetSize = (maxSize * 0.6) + (random.nextDouble() * maxSize * 0.4); // 60-100% of maxSize
  37. }
  38. @override
  39. Future<void> onLoad() async {
  40. // Choose random bubble image and set corresponding color
  41. final bubbleData = _getRandomBubbleData();
  42. bubbleImagePath = bubbleData['path'];
  43. bubbleColor = bubbleData['color'];
  44. // Load the sprite
  45. sprite = await Sprite.load(bubbleImagePath);
  46. await super.onLoad();
  47. // Add circular collision hitbox
  48. add(CircleHitbox(
  49. radius: _targetSize / 2,
  50. anchor: Anchor.center,
  51. ));
  52. // Start with zero scale for smooth entrance animation
  53. scale = Vector2.zero();
  54. // Add smooth entrance animation
  55. _addEntranceAnimation();
  56. // Add varied, organic floating animations
  57. _addRandomFloatingAnimation();
  58. _addRandomRotationAnimation();
  59. // Initialize random velocity for natural movement
  60. final random = Random();
  61. velocity = Vector2(
  62. (random.nextDouble() - 0.5) * 20, // -10 to +10
  63. (random.nextDouble() - 0.5) * 20, // -10 to +10
  64. );
  65. }
  66. @override
  67. void update(double dt) {
  68. super.update(dt);
  69. if (isPopping) return;
  70. // Update age and lifecycle
  71. _age += dt;
  72. _lifecycleProgress = (_age / lifecycleDuration).clamp(0.0, 1.0);
  73. // Update size based on lifecycle (grow from startSize to targetSize)
  74. final currentSize = startSize + (_targetSize - startSize) * _lifecycleProgress;
  75. size = Vector2.all(currentSize);
  76. // Update opacity based on lifecycle (fade out towards the end)
  77. final opacityFactor = 1.0 - (_lifecycleProgress * 0.6); // Fade to 40% opacity
  78. opacity = opacityFactor.clamp(0.4, 1.0);
  79. // Physics-based movement
  80. _updatePhysics(dt);
  81. // Keep bubble within screen bounds
  82. _keepInBounds();
  83. // Auto-pop when lifecycle is complete
  84. if (_lifecycleProgress >= 1.0) {
  85. pop(userTriggered: false);
  86. }
  87. }
  88. void _updatePhysics(double dt) {
  89. // Apply velocity to position
  90. position.add(velocity * dt);
  91. // Apply friction
  92. velocity.scale(friction);
  93. // Add periodic floating effects
  94. _floatTimer += dt;
  95. if (_floatTimer > 2.0) {
  96. _floatTimer = 0.0;
  97. final random = Random();
  98. final floatForce = Vector2(
  99. (random.nextDouble() - 0.5) * 8, // -4 to +4 horizontal
  100. (random.nextDouble() - 0.5) * 8, // -4 to +4 vertical
  101. );
  102. velocity.add(floatForce);
  103. }
  104. // Clamp velocity to max speed
  105. if (velocity.length > maxSpeed) {
  106. velocity.normalize();
  107. velocity.scale(maxSpeed);
  108. }
  109. }
  110. void _keepInBounds() {
  111. const margin = 60.0;
  112. final gameSize = game.size;
  113. // Bounce off screen edges
  114. if (position.x < margin) {
  115. position.x = margin;
  116. velocity.x = velocity.x.abs(); // Bounce right
  117. } else if (position.x > gameSize.x - margin) {
  118. position.x = gameSize.x - margin;
  119. velocity.x = -velocity.x.abs(); // Bounce left
  120. }
  121. if (position.y < margin + 100) { // Account for UI at top
  122. position.y = margin + 100;
  123. velocity.y = velocity.y.abs(); // Bounce down
  124. } else if (position.y > gameSize.y - margin) {
  125. position.y = gameSize.y - margin;
  126. velocity.y = -velocity.y.abs(); // Bounce up
  127. }
  128. }
  129. Map<String, dynamic> _getRandomBubbleData() {
  130. final random = Random();
  131. final bubbleTypes = [
  132. {
  133. 'path': 'bubble_blue.png',
  134. 'color': ZenColors.defaultLink,
  135. },
  136. {
  137. 'path': 'bubble_cyan.png',
  138. 'color': ZenColors.buttonBackground,
  139. },
  140. {
  141. 'path': 'bubble_green.png',
  142. 'color': const Color(0xFF00FF80),
  143. },
  144. {
  145. 'path': 'bubble_purple.png',
  146. 'color': ZenColors.lightModeHover,
  147. },
  148. ];
  149. return bubbleTypes[random.nextInt(bubbleTypes.length)];
  150. }
  151. void _addRandomFloatingAnimation() {
  152. final random = Random();
  153. // Add a small upward bias to the initial velocity
  154. velocity.y += -5 - random.nextDouble() * 10; // -5 to -15 upward bias
  155. // The floating effect will now be handled by the physics system
  156. // and periodic velocity changes in the update method
  157. }
  158. void _addEntranceAnimation() {
  159. // Smooth scale-in animation with bounce effect
  160. add(
  161. ScaleEffect.to(
  162. Vector2.all(1.0),
  163. EffectController(
  164. duration: 0.6,
  165. curve: Curves.elasticOut,
  166. ),
  167. ),
  168. );
  169. // Optional fade-in animation
  170. opacity = 0.0;
  171. add(
  172. OpacityEffect.to(
  173. 1.0,
  174. EffectController(
  175. duration: 0.4,
  176. curve: Curves.easeOut,
  177. ),
  178. ),
  179. );
  180. }
  181. void _addRandomRotationAnimation() {
  182. final random = Random();
  183. // 30% chance for rotation animation
  184. if (random.nextDouble() < 0.3) {
  185. // Small random rotation between -0.2 and 0.2 radians
  186. final rotationAngle = (random.nextDouble() - 0.5) * 0.4;
  187. // Random rotation duration between 3 and 8 seconds
  188. final rotationDuration = 3.0 + random.nextDouble() * 5.0;
  189. // Random delay
  190. final delay = random.nextDouble() * 3.0;
  191. Future.delayed(Duration(milliseconds: (delay * 1000).round()), () {
  192. if (isMounted) {
  193. add(
  194. RotateEffect.by(
  195. rotationAngle,
  196. EffectController(
  197. duration: rotationDuration,
  198. alternate: true,
  199. infinite: true,
  200. curve: Curves.easeInOut,
  201. ),
  202. ),
  203. );
  204. }
  205. });
  206. }
  207. }
  208. @override
  209. bool onTapDown(TapDownEvent event) {
  210. if (!isPopping) {
  211. pop(userTriggered: true);
  212. }
  213. return true;
  214. }
  215. void pop({bool userTriggered = false}) {
  216. if (isPopping) return;
  217. isPopping = true;
  218. poppedByUser = userTriggered;
  219. // Create exciting multi-stage pop animation
  220. // Stage 1: Quick expand (0.1s)
  221. final expandEffect = ScaleEffect.to(
  222. Vector2.all(1.8),
  223. EffectController(duration: 0.1, curve: Curves.easeOut),
  224. );
  225. // Stage 2: Slight compression (0.05s)
  226. final compressEffect = ScaleEffect.to(
  227. Vector2.all(0.8),
  228. EffectController(duration: 0.05, curve: Curves.easeIn),
  229. );
  230. // Stage 3: Final explosion (0.15s)
  231. final explodeEffect = ScaleEffect.to(
  232. Vector2.all(2.5),
  233. EffectController(duration: 0.15, curve: Curves.easeOut),
  234. );
  235. // Fade out effect
  236. final fadeEffect = OpacityEffect.to(
  237. 0.0,
  238. EffectController(duration: 0.3, curve: Curves.easeIn),
  239. );
  240. // Rotation effect for more dynamic feel
  241. final rotateEffect = RotateEffect.by(
  242. 0.5, // Half rotation
  243. EffectController(duration: 0.3),
  244. );
  245. // Chain the scale effects
  246. add(expandEffect);
  247. expandEffect.onComplete = () {
  248. if (isMounted) {
  249. add(compressEffect);
  250. compressEffect.onComplete = () {
  251. if (isMounted) {
  252. add(explodeEffect);
  253. }
  254. };
  255. }
  256. };
  257. add(fadeEffect);
  258. add(rotateEffect);
  259. // Create enhanced particle effects
  260. _createExcitingPopParticles();
  261. // Notify parent and remove bubble
  262. onPop?.call(this, userTriggered);
  263. Future.delayed(const Duration(milliseconds: 300), () {
  264. if (isMounted) {
  265. removeFromParent();
  266. }
  267. });
  268. }
  269. void _createExcitingPopParticles() {
  270. final random = Random();
  271. const particleCount = 4; // Further reduced for better performance
  272. // Use object pooling approach - create fewer, simpler particles
  273. for (int i = 0; i < particleCount; i++) {
  274. final angle = (i / particleCount) * 2 * pi;
  275. // Simplified particle creation
  276. final baseSpeed = 50 + random.nextDouble() * 30; // 50-80 speed
  277. final particleVelocity = Vector2(
  278. cos(angle) * baseSpeed,
  279. sin(angle) * baseSpeed,
  280. );
  281. // Fixed particle size for performance
  282. const particleSize = 3.0;
  283. // Simplified particle color
  284. final particleColor = bubbleColor.withValues(alpha: 0.8);
  285. final particle = CircleComponent(
  286. radius: particleSize,
  287. position: position.clone(),
  288. paint: Paint()
  289. ..color = particleColor
  290. ..style = PaintingStyle.fill,
  291. );
  292. parent?.add(particle);
  293. // Combine effects for better performance
  294. final combinedEffect = MoveEffect.by(
  295. particleVelocity + Vector2(0, 40), // Combined movement + gravity
  296. EffectController(duration: 0.3, curve: Curves.easeOut), // Faster animation
  297. );
  298. particle.add(combinedEffect);
  299. // Single fade effect instead of multiple effects
  300. particle.add(
  301. OpacityEffect.to(
  302. 0.0,
  303. EffectController(duration: 0.3, curve: Curves.easeIn),
  304. ),
  305. );
  306. // Faster cleanup
  307. Future.delayed(const Duration(milliseconds: 300), () {
  308. if (particle.isMounted) {
  309. particle.removeFromParent();
  310. }
  311. });
  312. }
  313. }
  314. /// Add shake effect to this bubble (called when phone is shaken)
  315. void addShakeEffect() {
  316. if (isPopping) return;
  317. final random = Random();
  318. // Random shake direction
  319. final shakeDirection = Vector2(
  320. (random.nextDouble() - 0.5) * 60, // -30 to +30
  321. (random.nextDouble() - 0.5) * 60, // -30 to +30
  322. );
  323. // Add shake movement effect
  324. add(
  325. MoveEffect.by(
  326. shakeDirection,
  327. EffectController(
  328. duration: 0.4,
  329. alternate: true,
  330. curve: Curves.elasticOut,
  331. ),
  332. ),
  333. );
  334. // Add scale bounce effect
  335. add(
  336. ScaleEffect.by(
  337. Vector2.all(0.3),
  338. EffectController(
  339. duration: 0.2,
  340. alternate: true,
  341. curve: Curves.elasticOut,
  342. ),
  343. ),
  344. );
  345. // Add slight rotation effect
  346. add(
  347. RotateEffect.by(
  348. (random.nextDouble() - 0.5) * 0.4, // Random rotation
  349. EffectController(
  350. duration: 0.3,
  351. alternate: true,
  352. curve: Curves.elasticOut,
  353. ),
  354. ),
  355. );
  356. }
  357. // Collision detection methods
  358. @override
  359. bool onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
  360. super.onCollision(intersectionPoints, other);
  361. if (other is Bubble && !isPopping && !other.isPopping) {
  362. _handleBubbleCollision(other, intersectionPoints);
  363. }
  364. return true;
  365. }
  366. void _handleBubbleCollision(Bubble other, Set<Vector2> intersectionPoints) {
  367. if (intersectionPoints.isEmpty) return;
  368. // Calculate collision direction
  369. final direction = (position - other.position).normalized();
  370. // Apply collision response - bubbles bounce off each other
  371. final collisionForce = 30.0;
  372. velocity.add(direction * collisionForce);
  373. other.velocity.add(direction * -collisionForce);
  374. // Apply damping to prevent infinite acceleration
  375. velocity.scale(collisionDamping);
  376. other.velocity.scale(collisionDamping);
  377. // Add slight separation to prevent sticking
  378. final separation = direction * 2.0;
  379. position.add(separation);
  380. other.position.sub(separation);
  381. }
  382. /// Apply tilt force to bubble based on device orientation
  383. void applyTiltForce(double tiltStrength) {
  384. if (isPopping) return;
  385. // Apply force opposite to tilt direction
  386. // If device tilts right, bubbles move left and vice versa
  387. final tiltForce = -tiltStrength * 15.0; // Adjust force strength
  388. velocity.x += tiltForce;
  389. // Add slight vertical component for more natural movement
  390. velocity.y += tiltForce * 0.3;
  391. }
  392. }