bubble_spawner.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import 'dart:math';
  2. import 'package:flame/components.dart';
  3. import 'bubble.dart';
  4. class BubbleSpawner extends Component with HasGameReference {
  5. static const double baseSpawnInterval = 1.5; // base seconds
  6. static const double spawnVariation = 1.0; // +/- variation in seconds
  7. // Bubble rules constants
  8. static const double bubbleMaxDiameter = 50.0;
  9. static const double bubbleMinSpacing = 10.0;
  10. static const double screenMargin = 120.0;
  11. static const double uiTopMargin = 100.0;
  12. // Dynamic max bubbles based on screen size
  13. int _maxBubbles = 8;
  14. int get maxBubbles => _maxBubbles;
  15. double _timeSinceLastSpawn = 0;
  16. double _nextSpawnTime = 0;
  17. final List<Bubble> _activeBubbles = [];
  18. final Random _random = Random();
  19. Function(Bubble, bool)? onBubblePopped;
  20. bool isActive = true;
  21. // Screen capacity calculation cache
  22. Vector2 _lastCalculatedScreenSize = Vector2.zero();
  23. // Shake-enhanced spawning
  24. bool _isShakeMode = false;
  25. double _shakeModeEndTime = 0;
  26. static const double shakeModeSpeedMultiplier = 4.0; // 4x faster during shake
  27. static const double shakeModeDuration = 3.0; // seconds
  28. BubbleSpawner({this.onBubblePopped}) {
  29. _calculateNextSpawnTime();
  30. }
  31. double _lastCleanupTime = 0;
  32. static const double cleanupInterval = 0.5; // Clean up bubbles every 0.5 seconds instead of every frame
  33. @override
  34. void update(double dt) {
  35. super.update(dt);
  36. if (!isActive) return;
  37. // Update max bubbles if screen size changed
  38. _updateMaxBubblesIfNeeded();
  39. _timeSinceLastSpawn += dt;
  40. _lastCleanupTime += dt;
  41. // Check if shake mode should end
  42. if (_isShakeMode && _shakeModeEndTime <= DateTime.now().millisecondsSinceEpoch / 1000.0) {
  43. _isShakeMode = false;
  44. }
  45. // Spawn new bubble if conditions are met
  46. if (_timeSinceLastSpawn >= _nextSpawnTime && _activeBubbles.length < maxBubbles) {
  47. _spawnBubble();
  48. _timeSinceLastSpawn = 0;
  49. _calculateNextSpawnTime();
  50. }
  51. // Clean up popped bubbles less frequently to improve performance
  52. if (_lastCleanupTime >= cleanupInterval) {
  53. _activeBubbles.removeWhere((bubble) => !bubble.isMounted);
  54. _lastCleanupTime = 0;
  55. }
  56. }
  57. void _spawnBubble({bool isAutoSpawned = true}) {
  58. if (game.size.x == 0 || game.size.y == 0) return;
  59. final spawnPosition = _getValidSpawnPosition();
  60. if (spawnPosition == null) return;
  61. final bubble = Bubble(
  62. position: spawnPosition,
  63. onPop: _onBubblePopped,
  64. isAutoSpawned: isAutoSpawned,
  65. );
  66. _activeBubbles.add(bubble);
  67. game.add(bubble);
  68. }
  69. Vector2? _getValidSpawnPosition() {
  70. // Calculate safe spawn area (avoiding edges and UI elements)
  71. // Increased margin to account for larger bubble sprites
  72. const margin = 120.0;
  73. const maxAttempts = 20; // Increased attempts
  74. final minX = margin;
  75. final maxX = game.size.x - margin;
  76. final minY = margin + 100; // Extra margin for score display
  77. final maxY = game.size.y - margin;
  78. if (maxX <= minX || maxY <= minY) return null;
  79. // Try to find a position that doesn't overlap with existing bubbles
  80. for (int attempt = 0; attempt < maxAttempts; attempt++) {
  81. final position = Vector2(
  82. minX + _random.nextDouble() * (maxX - minX),
  83. minY + _random.nextDouble() * (maxY - minY),
  84. );
  85. // Check if position is safe from all existing bubbles
  86. bool isSafe = true;
  87. for (final bubble in _activeBubbles) {
  88. if (!bubble.isPopping) {
  89. // Calculate required distance based on bubble sizes
  90. final bubbleRadius = (bubbleMaxDiameter / 2) * bubble.scale.x;
  91. final newBubbleRadius = bubbleMaxDiameter / 2; // New bubble starts small
  92. final requiredDistance = bubbleRadius + newBubbleRadius + 20.0; // 20px padding
  93. final distance = position.distanceTo(bubble.position);
  94. if (distance < requiredDistance) {
  95. isSafe = false;
  96. break;
  97. }
  98. }
  99. }
  100. if (isSafe) {
  101. return position;
  102. }
  103. }
  104. // If no safe position found after max attempts, return null
  105. return null;
  106. }
  107. void _onBubblePopped(Bubble bubble, bool userTriggered) {
  108. _activeBubbles.remove(bubble);
  109. onBubblePopped?.call(bubble, userTriggered);
  110. }
  111. void spawnBubbleAt(Vector2 position) {
  112. // Find a safe position near the requested position
  113. final safePosition = _findSafePositionNear(position);
  114. if (safePosition == null) return; // Don't spawn if no safe position found
  115. final bubble = Bubble(
  116. position: safePosition,
  117. onPop: _onBubblePopped,
  118. isAutoSpawned: false, // User-triggered bubbles
  119. );
  120. _activeBubbles.add(bubble);
  121. game.add(bubble);
  122. }
  123. /// Find a safe position near the requested position that doesn't overlap with existing bubbles
  124. Vector2? _findSafePositionNear(Vector2 requestedPosition) {
  125. const maxAttempts = 15; // Increased attempts
  126. const searchRadius = 100.0; // How far to search around requested position
  127. // First, ensure the requested position is within bounds
  128. final clampedPosition = _clampPositionToBounds(requestedPosition);
  129. // Try the exact position first
  130. if (_isPositionSafeImproved(clampedPosition)) {
  131. return clampedPosition;
  132. }
  133. // If not safe, try positions in a circle around the requested position
  134. for (int attempt = 0; attempt < maxAttempts; attempt++) {
  135. final angle = _random.nextDouble() * 2 * pi; // Random angle
  136. final distance = _random.nextDouble() * searchRadius; // Random distance
  137. final testPosition = Vector2(
  138. clampedPosition.x + distance * cos(angle),
  139. clampedPosition.y + distance * sin(angle),
  140. );
  141. final boundedPosition = _clampPositionToBounds(testPosition);
  142. if (_isPositionSafeImproved(boundedPosition)) {
  143. return boundedPosition;
  144. }
  145. }
  146. return null; // No safe position found
  147. }
  148. /// Check if a position is safe (far enough from all existing bubbles)
  149. /// This improved version accounts for bubble sizes and scaling
  150. bool _isPositionSafe(Vector2 position, [double? fixedMinDistance]) {
  151. for (final bubble in _activeBubbles) {
  152. if (!bubble.isPopping) {
  153. double requiredDistance;
  154. if (fixedMinDistance != null) {
  155. // Use fixed distance for backwards compatibility
  156. requiredDistance = fixedMinDistance;
  157. } else {
  158. // Calculate required distance based on bubble sizes
  159. final bubbleRadius = (bubbleMaxDiameter / 2) * bubble.scale.x;
  160. final newBubbleRadius = bubbleMaxDiameter / 2; // New bubble starts small
  161. requiredDistance = bubbleRadius + newBubbleRadius + 20.0; // 20px padding
  162. }
  163. final distance = position.distanceTo(bubble.position);
  164. if (distance < requiredDistance) {
  165. return false;
  166. }
  167. }
  168. }
  169. return true;
  170. }
  171. /// Improved position safety check that accounts for bubble sizes
  172. bool _isPositionSafeImproved(Vector2 position) {
  173. return _isPositionSafe(position); // Use the improved version by default
  174. }
  175. Vector2 _clampPositionToBounds(Vector2 position) {
  176. const margin = 120.0;
  177. final minX = margin;
  178. final maxX = game.size.x - margin;
  179. final minY = margin + 100;
  180. final maxY = game.size.y - margin;
  181. if (maxX <= minX || maxY <= minY) return position;
  182. return Vector2(
  183. position.x.clamp(minX, maxX),
  184. position.y.clamp(minY, maxY),
  185. );
  186. }
  187. void clearAllBubbles() {
  188. for (final bubble in _activeBubbles) {
  189. if (bubble.isMounted) {
  190. bubble.removeFromParent();
  191. }
  192. }
  193. _activeBubbles.clear();
  194. }
  195. void setActive(bool active) {
  196. isActive = active;
  197. }
  198. void _calculateNextSpawnTime() {
  199. // Base spawn time calculation
  200. double baseTime = baseSpawnInterval + (_random.nextDouble() - 0.5) * 2 * spawnVariation;
  201. // Apply shake mode speed multiplier if active
  202. if (_isShakeMode) {
  203. baseTime /= shakeModeSpeedMultiplier;
  204. }
  205. _nextSpawnTime = baseTime.clamp(0.1, double.infinity); // Minimum 0.1 seconds for shake mode
  206. }
  207. int get activeBubbleCount => _activeBubbles.length;
  208. /// Get list of active bubbles for performance optimization
  209. List<Bubble> getActiveBubbles() => List.unmodifiable(_activeBubbles);
  210. /// Activate shake mode for faster bubble generation
  211. void activateShakeMode() {
  212. _isShakeMode = true;
  213. _shakeModeEndTime = (DateTime.now().millisecondsSinceEpoch / 1000.0) + shakeModeDuration;
  214. // Immediately recalculate spawn time for current bubble
  215. _calculateNextSpawnTime();
  216. }
  217. /// Check if shake mode is currently active
  218. bool get isShakeModeActive => _isShakeMode;
  219. /// Update max bubbles calculation if screen size changed
  220. void _updateMaxBubblesIfNeeded() {
  221. if (game.size != _lastCalculatedScreenSize) {
  222. _maxBubbles = calculateMaxBubblesForScreen();
  223. _lastCalculatedScreenSize = game.size.clone();
  224. }
  225. }
  226. /// Calculate maximum number of full-size bubbles that can fit on screen
  227. /// based on bubble size, spacing, and screen dimensions
  228. int calculateMaxBubblesForScreen() {
  229. if (game.size.x == 0 || game.size.y == 0) return 8; // fallback
  230. return calculateMaxBubblesForScreenSize(game.size);
  231. }
  232. /// Static method to calculate max bubbles for given screen size
  233. /// This can be used for testing without requiring a game instance
  234. static int calculateMaxBubblesForScreenSize(Vector2 screenSize) {
  235. if (screenSize.x == 0 || screenSize.y == 0) return 8; // fallback
  236. // Calculate available screen area
  237. final availableWidth = screenSize.x - (2 * screenMargin);
  238. final availableHeight = screenSize.y - (2 * screenMargin) - uiTopMargin;
  239. if (availableWidth <= 0 || availableHeight <= 0) return 4; // minimum fallback
  240. // Calculate effective bubble area (including spacing)
  241. final effectiveBubbleSize = bubbleMaxDiameter + bubbleMinSpacing;
  242. final effectiveBubbleArea = effectiveBubbleSize * effectiveBubbleSize;
  243. // Calculate total available area
  244. final availableArea = availableWidth * availableHeight;
  245. // Calculate theoretical max bubbles
  246. final theoreticalMax = (availableArea / effectiveBubbleArea).floor();
  247. // Apply practical limits for performance and gameplay
  248. final practicalMax = theoreticalMax.clamp(4, 15); // between 4-15 bubbles
  249. return practicalMax;
  250. }
  251. /// Get current screen capacity utilization as percentage
  252. double getScreenCapacityUtilization() {
  253. if (maxBubbles == 0) return 0.0;
  254. return (_activeBubbles.length / maxBubbles).clamp(0.0, 1.0);
  255. }
  256. }