bubble_spawner.dart 11 KB

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