|
@@ -7,46 +7,46 @@ import 'package:flame/collisions.dart';
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
import '../../utils/colors.dart';
|
|
import '../../utils/colors.dart';
|
|
|
|
|
|
|
|
-class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, CollisionCallbacks {
|
|
|
|
|
|
|
+class Bubble extends SpriteComponent
|
|
|
|
|
+ with HasGameReference, TapCallbacks, CollisionCallbacks {
|
|
|
static const double startSize = 40.0; // Small starting size for all bubbles
|
|
static const double startSize = 40.0; // Small starting size for all bubbles
|
|
|
static const double maxSize = 50.0; // Same final size for all bubbles
|
|
static const double maxSize = 50.0; // Same final size for all bubbles
|
|
|
- static const double lifecycleDuration = 12.0; // Same lifecycle duration for all bubbles
|
|
|
|
|
-
|
|
|
|
|
|
|
+ static const double lifecycleDuration =
|
|
|
|
|
+ 12.0; // Same lifecycle duration for all bubbles
|
|
|
|
|
+
|
|
|
// Triple collision rule constants
|
|
// Triple collision rule constants
|
|
|
static const int maxCollisions = 3;
|
|
static const int maxCollisions = 3;
|
|
|
static const double collisionGracePeriod = 2.0; // seconds
|
|
static const double collisionGracePeriod = 2.0; // seconds
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
late String bubbleImagePath;
|
|
late String bubbleImagePath;
|
|
|
late Color bubbleColor;
|
|
late Color bubbleColor;
|
|
|
bool isPopping = false;
|
|
bool isPopping = false;
|
|
|
bool poppedByUser = false;
|
|
bool poppedByUser = false;
|
|
|
- bool isAutoSpawned = true; // Whether bubble was auto-spawned or user-triggered
|
|
|
|
|
|
|
+ bool isAutoSpawned =
|
|
|
|
|
+ true; // Whether bubble was auto-spawned or user-triggered
|
|
|
Function(Bubble, bool)? onPop;
|
|
Function(Bubble, bool)? onPop;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Collision tracking for triple collision rule
|
|
// Collision tracking for triple collision rule
|
|
|
int _collisionCount = 0;
|
|
int _collisionCount = 0;
|
|
|
double _lastCollisionTime = 0.0;
|
|
double _lastCollisionTime = 0.0;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
double _age = 0.0;
|
|
double _age = 0.0;
|
|
|
double _lifecycleProgress = 0.0;
|
|
double _lifecycleProgress = 0.0;
|
|
|
late double _targetSize;
|
|
late double _targetSize;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Physics properties for collision and movement
|
|
// Physics properties for collision and movement
|
|
|
Vector2 velocity = Vector2.zero();
|
|
Vector2 velocity = Vector2.zero();
|
|
|
static const double maxSpeed = 50.0;
|
|
static const double maxSpeed = 50.0;
|
|
|
static const double friction = 0.98;
|
|
static const double friction = 0.98;
|
|
|
static const double collisionDamping = 0.7;
|
|
static const double collisionDamping = 0.7;
|
|
|
double _floatTimer = 0.0;
|
|
double _floatTimer = 0.0;
|
|
|
-
|
|
|
|
|
- Bubble({
|
|
|
|
|
- required Vector2 position,
|
|
|
|
|
- this.onPop,
|
|
|
|
|
- this.isAutoSpawned = true,
|
|
|
|
|
- }) : super(
|
|
|
|
|
- size: Vector2.all(startSize),
|
|
|
|
|
- position: position,
|
|
|
|
|
- anchor: Anchor.center,
|
|
|
|
|
- ) {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ Bubble({required Vector2 position, this.onPop, this.isAutoSpawned = true})
|
|
|
|
|
+ : super(
|
|
|
|
|
+ size: Vector2.all(startSize),
|
|
|
|
|
+ position: position,
|
|
|
|
|
+ anchor: Anchor.center,
|
|
|
|
|
+ ) {
|
|
|
// All bubbles have the same target size regardless of type
|
|
// All bubbles have the same target size regardless of type
|
|
|
_targetSize = maxSize;
|
|
_targetSize = maxSize;
|
|
|
}
|
|
}
|
|
@@ -57,28 +57,30 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
final bubbleData = _getRandomBubbleData();
|
|
final bubbleData = _getRandomBubbleData();
|
|
|
bubbleImagePath = bubbleData['path'];
|
|
bubbleImagePath = bubbleData['path'];
|
|
|
bubbleColor = bubbleData['color'];
|
|
bubbleColor = bubbleData['color'];
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Load the sprite
|
|
// Load the sprite
|
|
|
sprite = await Sprite.load(bubbleImagePath);
|
|
sprite = await Sprite.load(bubbleImagePath);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
await super.onLoad();
|
|
await super.onLoad();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Add circular collision hitbox - will be updated dynamically
|
|
// Add circular collision hitbox - will be updated dynamically
|
|
|
- add(CircleHitbox(
|
|
|
|
|
- radius: startSize / 2, // Start with initial size
|
|
|
|
|
- anchor: Anchor.center,
|
|
|
|
|
- ));
|
|
|
|
|
-
|
|
|
|
|
|
|
+ add(
|
|
|
|
|
+ CircleHitbox(
|
|
|
|
|
+ radius: startSize / 2, // Start with initial size
|
|
|
|
|
+ anchor: Anchor.center,
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
// Start with zero scale for smooth entrance animation
|
|
// Start with zero scale for smooth entrance animation
|
|
|
scale = Vector2.zero();
|
|
scale = Vector2.zero();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Add smooth entrance animation
|
|
// Add smooth entrance animation
|
|
|
_addEntranceAnimation();
|
|
_addEntranceAnimation();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Add varied, organic floating animations
|
|
// Add varied, organic floating animations
|
|
|
_addRandomFloatingAnimation();
|
|
_addRandomFloatingAnimation();
|
|
|
_addRandomRotationAnimation();
|
|
_addRandomRotationAnimation();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Initialize random velocity for natural movement
|
|
// Initialize random velocity for natural movement
|
|
|
final random = Random();
|
|
final random = Random();
|
|
|
velocity = Vector2(
|
|
velocity = Vector2(
|
|
@@ -90,50 +92,58 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
@override
|
|
@override
|
|
|
void update(double dt) {
|
|
void update(double dt) {
|
|
|
super.update(dt);
|
|
super.update(dt);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if (isPopping) return;
|
|
if (isPopping) return;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Update age and lifecycle
|
|
// Update age and lifecycle
|
|
|
_age += dt;
|
|
_age += dt;
|
|
|
_lifecycleProgress = (_age / lifecycleDuration).clamp(0.0, 1.0);
|
|
_lifecycleProgress = (_age / lifecycleDuration).clamp(0.0, 1.0);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Update scale based on lifecycle progress - grow from startSize to maxSize
|
|
// Update scale based on lifecycle progress - grow from startSize to maxSize
|
|
|
final currentScale = _lifecycleProgress * (_targetSize / startSize);
|
|
final currentScale = _lifecycleProgress * (_targetSize / startSize);
|
|
|
- scale = Vector2.all(1.0 + currentScale * 3.0); // Scale from 1.0 to 4.0 over lifetime
|
|
|
|
|
-
|
|
|
|
|
|
|
+ scale = Vector2.all(
|
|
|
|
|
+ 1.0 + currentScale * 3.0,
|
|
|
|
|
+ ); // Scale from 1.0 to 4.0 over lifetime
|
|
|
|
|
+
|
|
|
// Update collision hitbox radius to match current visual size
|
|
// Update collision hitbox radius to match current visual size
|
|
|
- final currentRadius = (startSize / 2) * scale.x; // Use scale.x since it's uniform scaling
|
|
|
|
|
|
|
+ // Use a slightly smaller radius for collision detection to prevent edge cases
|
|
|
|
|
+ final currentRadius =
|
|
|
|
|
+ ((startSize / 2) * scale.x) * 0.9; // 90% of visual size
|
|
|
final hitbox = children.whereType<CircleHitbox>().firstOrNull;
|
|
final hitbox = children.whereType<CircleHitbox>().firstOrNull;
|
|
|
if (hitbox != null) {
|
|
if (hitbox != null) {
|
|
|
hitbox.radius = currentRadius;
|
|
hitbox.radius = currentRadius;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Update opacity based on lifecycle (fade out towards the end)
|
|
// Update opacity based on lifecycle (fade out towards the end)
|
|
|
- final opacityFactor = 1.0 - (_lifecycleProgress * 0.6); // Fade to 40% opacity
|
|
|
|
|
|
|
+ final opacityFactor =
|
|
|
|
|
+ 1.0 - (_lifecycleProgress * 0.6); // Fade to 40% opacity
|
|
|
opacity = opacityFactor.clamp(0.4, 1.0);
|
|
opacity = opacityFactor.clamp(0.4, 1.0);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Physics-based movement
|
|
// Physics-based movement
|
|
|
_updatePhysics(dt);
|
|
_updatePhysics(dt);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Keep bubble within screen bounds
|
|
// Keep bubble within screen bounds
|
|
|
_keepInBounds();
|
|
_keepInBounds();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Update collision tracking
|
|
// Update collision tracking
|
|
|
_updateCollisionTracking(dt);
|
|
_updateCollisionTracking(dt);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Periodic overlap check to prevent stuck overlaps
|
|
|
|
|
+ _preventOverlapSticking(dt);
|
|
|
|
|
+
|
|
|
// Auto-pop when lifecycle is complete
|
|
// Auto-pop when lifecycle is complete
|
|
|
if (_lifecycleProgress >= 1.0) {
|
|
if (_lifecycleProgress >= 1.0) {
|
|
|
pop(userTriggered: false);
|
|
pop(userTriggered: false);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
void _updatePhysics(double dt) {
|
|
void _updatePhysics(double dt) {
|
|
|
// Apply velocity to position
|
|
// Apply velocity to position
|
|
|
position.add(velocity * dt);
|
|
position.add(velocity * dt);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Apply friction
|
|
// Apply friction
|
|
|
velocity.scale(friction);
|
|
velocity.scale(friction);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Add periodic floating effects
|
|
// Add periodic floating effects
|
|
|
_floatTimer += dt;
|
|
_floatTimer += dt;
|
|
|
if (_floatTimer > 2.0) {
|
|
if (_floatTimer > 2.0) {
|
|
@@ -145,18 +155,18 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
);
|
|
);
|
|
|
velocity.add(floatForce);
|
|
velocity.add(floatForce);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Clamp velocity to max speed
|
|
// Clamp velocity to max speed
|
|
|
if (velocity.length > maxSpeed) {
|
|
if (velocity.length > maxSpeed) {
|
|
|
velocity.normalize();
|
|
velocity.normalize();
|
|
|
velocity.scale(maxSpeed);
|
|
velocity.scale(maxSpeed);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
void _keepInBounds() {
|
|
void _keepInBounds() {
|
|
|
const margin = 60.0;
|
|
const margin = 60.0;
|
|
|
final gameSize = game.size;
|
|
final gameSize = game.size;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Bounce off screen edges
|
|
// Bounce off screen edges
|
|
|
if (position.x < margin) {
|
|
if (position.x < margin) {
|
|
|
position.x = margin;
|
|
position.x = margin;
|
|
@@ -165,8 +175,9 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
position.x = gameSize.x - margin;
|
|
position.x = gameSize.x - margin;
|
|
|
velocity.x = -velocity.x.abs(); // Bounce left
|
|
velocity.x = -velocity.x.abs(); // Bounce left
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- if (position.y < margin + 100) { // Account for UI at top
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (position.y < margin + 100) {
|
|
|
|
|
+ // Account for UI at top
|
|
|
position.y = margin + 100;
|
|
position.y = margin + 100;
|
|
|
velocity.y = velocity.y.abs(); // Bounce down
|
|
velocity.y = velocity.y.abs(); // Bounce down
|
|
|
} else if (position.y > gameSize.y - margin) {
|
|
} else if (position.y > gameSize.y - margin) {
|
|
@@ -178,32 +189,20 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
Map<String, dynamic> _getRandomBubbleData() {
|
|
Map<String, dynamic> _getRandomBubbleData() {
|
|
|
final random = Random();
|
|
final random = Random();
|
|
|
final bubbleTypes = [
|
|
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,
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ {'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)];
|
|
return bubbleTypes[random.nextInt(bubbleTypes.length)];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void _addRandomFloatingAnimation() {
|
|
void _addRandomFloatingAnimation() {
|
|
|
final random = Random();
|
|
final random = Random();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Add a small upward bias to the initial velocity
|
|
// Add a small upward bias to the initial velocity
|
|
|
velocity.y += -5 - random.nextDouble() * 10; // -5 to -15 upward bias
|
|
velocity.y += -5 - random.nextDouble() * 10; // -5 to -15 upward bias
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// The floating effect will now be handled by the physics system
|
|
// The floating effect will now be handled by the physics system
|
|
|
// and periodic velocity changes in the update method
|
|
// and periodic velocity changes in the update method
|
|
|
}
|
|
}
|
|
@@ -211,34 +210,31 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
void _addEntranceAnimation() {
|
|
void _addEntranceAnimation() {
|
|
|
// Start at scale 1.0 (startSize) and grow gradually throughout lifecycle
|
|
// Start at scale 1.0 (startSize) and grow gradually throughout lifecycle
|
|
|
// The growth will be handled in the update method based on lifecycle progress
|
|
// The growth will be handled in the update method based on lifecycle progress
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Simple fade-in animation only
|
|
// Simple fade-in animation only
|
|
|
opacity = 0.0;
|
|
opacity = 0.0;
|
|
|
add(
|
|
add(
|
|
|
OpacityEffect.to(
|
|
OpacityEffect.to(
|
|
|
1.0,
|
|
1.0,
|
|
|
- EffectController(
|
|
|
|
|
- duration: 0.4,
|
|
|
|
|
- curve: Curves.easeOut,
|
|
|
|
|
- ),
|
|
|
|
|
|
|
+ EffectController(duration: 0.4, curve: Curves.easeOut),
|
|
|
),
|
|
),
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void _addRandomRotationAnimation() {
|
|
void _addRandomRotationAnimation() {
|
|
|
final random = Random();
|
|
final random = Random();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 30% chance for rotation animation
|
|
// 30% chance for rotation animation
|
|
|
if (random.nextDouble() < 0.3) {
|
|
if (random.nextDouble() < 0.3) {
|
|
|
// Small random rotation between -0.2 and 0.2 radians
|
|
// Small random rotation between -0.2 and 0.2 radians
|
|
|
final rotationAngle = (random.nextDouble() - 0.5) * 0.4;
|
|
final rotationAngle = (random.nextDouble() - 0.5) * 0.4;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Random rotation duration between 3 and 8 seconds
|
|
// Random rotation duration between 3 and 8 seconds
|
|
|
final rotationDuration = 3.0 + random.nextDouble() * 5.0;
|
|
final rotationDuration = 3.0 + random.nextDouble() * 5.0;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Random delay
|
|
// Random delay
|
|
|
final delay = random.nextDouble() * 3.0;
|
|
final delay = random.nextDouble() * 3.0;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
Future.delayed(Duration(milliseconds: (delay * 1000).round()), () {
|
|
Future.delayed(Duration(milliseconds: (delay * 1000).round()), () {
|
|
|
if (isMounted) {
|
|
if (isMounted) {
|
|
|
add(
|
|
add(
|
|
@@ -257,7 +253,6 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-
|
|
|
|
|
@override
|
|
@override
|
|
|
bool onTapDown(TapDownEvent event) {
|
|
bool onTapDown(TapDownEvent event) {
|
|
|
if (!isPopping) {
|
|
if (!isPopping) {
|
|
@@ -268,42 +263,42 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
|
|
|
|
|
void pop({bool userTriggered = false}) {
|
|
void pop({bool userTriggered = false}) {
|
|
|
if (isPopping) return;
|
|
if (isPopping) return;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
isPopping = true;
|
|
isPopping = true;
|
|
|
poppedByUser = userTriggered;
|
|
poppedByUser = userTriggered;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Create exciting multi-stage pop animation
|
|
// Create exciting multi-stage pop animation
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Stage 1: Quick expand (0.1s)
|
|
// Stage 1: Quick expand (0.1s)
|
|
|
final expandEffect = ScaleEffect.to(
|
|
final expandEffect = ScaleEffect.to(
|
|
|
Vector2.all(1.8),
|
|
Vector2.all(1.8),
|
|
|
EffectController(duration: 0.1, curve: Curves.easeOut),
|
|
EffectController(duration: 0.1, curve: Curves.easeOut),
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Stage 2: Slight compression (0.05s)
|
|
// Stage 2: Slight compression (0.05s)
|
|
|
final compressEffect = ScaleEffect.to(
|
|
final compressEffect = ScaleEffect.to(
|
|
|
Vector2.all(0.8),
|
|
Vector2.all(0.8),
|
|
|
EffectController(duration: 0.05, curve: Curves.easeIn),
|
|
EffectController(duration: 0.05, curve: Curves.easeIn),
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Stage 3: Final explosion (0.15s)
|
|
// Stage 3: Final explosion (0.15s)
|
|
|
final explodeEffect = ScaleEffect.to(
|
|
final explodeEffect = ScaleEffect.to(
|
|
|
Vector2.all(2.5),
|
|
Vector2.all(2.5),
|
|
|
EffectController(duration: 0.15, curve: Curves.easeOut),
|
|
EffectController(duration: 0.15, curve: Curves.easeOut),
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Fade out effect
|
|
// Fade out effect
|
|
|
final fadeEffect = OpacityEffect.to(
|
|
final fadeEffect = OpacityEffect.to(
|
|
|
0.0,
|
|
0.0,
|
|
|
EffectController(duration: 0.3, curve: Curves.easeIn),
|
|
EffectController(duration: 0.3, curve: Curves.easeIn),
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Rotation effect for more dynamic feel
|
|
// Rotation effect for more dynamic feel
|
|
|
final rotateEffect = RotateEffect.by(
|
|
final rotateEffect = RotateEffect.by(
|
|
|
0.5, // Half rotation
|
|
0.5, // Half rotation
|
|
|
EffectController(duration: 0.3),
|
|
EffectController(duration: 0.3),
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Chain the scale effects
|
|
// Chain the scale effects
|
|
|
add(expandEffect);
|
|
add(expandEffect);
|
|
|
expandEffect.onComplete = () {
|
|
expandEffect.onComplete = () {
|
|
@@ -316,16 +311,16 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
add(fadeEffect);
|
|
add(fadeEffect);
|
|
|
add(rotateEffect);
|
|
add(rotateEffect);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Create enhanced particle effects
|
|
// Create enhanced particle effects
|
|
|
_createExcitingPopParticles();
|
|
_createExcitingPopParticles();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Notify parent and remove bubble
|
|
// Notify parent and remove bubble
|
|
|
onPop?.call(this, userTriggered);
|
|
onPop?.call(this, userTriggered);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
Future.delayed(const Duration(milliseconds: 300), () {
|
|
Future.delayed(const Duration(milliseconds: 300), () {
|
|
|
if (isMounted) {
|
|
if (isMounted) {
|
|
|
removeFromParent();
|
|
removeFromParent();
|
|
@@ -336,42 +331,46 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
void _createExcitingPopParticles() {
|
|
void _createExcitingPopParticles() {
|
|
|
final random = Random();
|
|
final random = Random();
|
|
|
const particleCount = 12; // Further reduced for better performance
|
|
const particleCount = 12; // Further reduced for better performance
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Use object pooling approach - create fewer, simpler particles
|
|
// Use object pooling approach - create fewer, simpler particles
|
|
|
for (int i = 0; i < particleCount; i++) {
|
|
for (int i = 0; i < particleCount; i++) {
|
|
|
final angle = (i / particleCount) * 2 * pi;
|
|
final angle = (i / particleCount) * 2 * pi;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Simplified particle creation
|
|
// Simplified particle creation
|
|
|
final baseSpeed = 50 + random.nextDouble() * 30; // 50-80 speed
|
|
final baseSpeed = 50 + random.nextDouble() * 30; // 50-80 speed
|
|
|
final particleVelocity = Vector2(
|
|
final particleVelocity = Vector2(
|
|
|
cos(angle) * baseSpeed,
|
|
cos(angle) * baseSpeed,
|
|
|
sin(angle) * baseSpeed,
|
|
sin(angle) * baseSpeed,
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Fixed particle size for performance
|
|
// Fixed particle size for performance
|
|
|
const particleSize = 3.0;
|
|
const particleSize = 3.0;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Simplified particle color
|
|
// Simplified particle color
|
|
|
final particleColor = bubbleColor.withValues(alpha: 0.8);
|
|
final particleColor = bubbleColor.withValues(alpha: 0.8);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
final particle = CircleComponent(
|
|
final particle = CircleComponent(
|
|
|
radius: particleSize,
|
|
radius: particleSize,
|
|
|
position: position.clone(),
|
|
position: position.clone(),
|
|
|
- paint: Paint()
|
|
|
|
|
- ..color = particleColor
|
|
|
|
|
- ..style = PaintingStyle.fill,
|
|
|
|
|
|
|
+ paint:
|
|
|
|
|
+ Paint()
|
|
|
|
|
+ ..color = particleColor
|
|
|
|
|
+ ..style = PaintingStyle.fill,
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
parent?.add(particle);
|
|
parent?.add(particle);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Combine effects for better performance
|
|
// Combine effects for better performance
|
|
|
final combinedEffect = MoveEffect.by(
|
|
final combinedEffect = MoveEffect.by(
|
|
|
particleVelocity + Vector2(0, 40), // Combined movement + gravity
|
|
particleVelocity + Vector2(0, 40), // Combined movement + gravity
|
|
|
- EffectController(duration: 0.3, curve: Curves.easeOut), // Faster animation
|
|
|
|
|
|
|
+ EffectController(
|
|
|
|
|
+ duration: 0.3,
|
|
|
|
|
+ curve: Curves.easeOut,
|
|
|
|
|
+ ), // Faster animation
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
particle.add(combinedEffect);
|
|
particle.add(combinedEffect);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Single fade effect instead of multiple effects
|
|
// Single fade effect instead of multiple effects
|
|
|
particle.add(
|
|
particle.add(
|
|
|
OpacityEffect.to(
|
|
OpacityEffect.to(
|
|
@@ -379,7 +378,7 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
EffectController(duration: 0.3, curve: Curves.easeIn),
|
|
EffectController(duration: 0.3, curve: Curves.easeIn),
|
|
|
),
|
|
),
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Faster cleanup
|
|
// Faster cleanup
|
|
|
Future.delayed(const Duration(milliseconds: 300), () {
|
|
Future.delayed(const Duration(milliseconds: 300), () {
|
|
|
if (particle.isMounted) {
|
|
if (particle.isMounted) {
|
|
@@ -391,19 +390,19 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
|
|
|
|
|
// Track if shake effect is currently active to prevent overlapping effects
|
|
// Track if shake effect is currently active to prevent overlapping effects
|
|
|
bool _isShakeEffectActive = false;
|
|
bool _isShakeEffectActive = false;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// Add shake effect to this bubble (called when phone is shaken)
|
|
/// Add shake effect to this bubble (called when phone is shaken)
|
|
|
/// Fixed version that only adds visual effects without movement or size flickering
|
|
/// Fixed version that only adds visual effects without movement or size flickering
|
|
|
void addShakeEffect() {
|
|
void addShakeEffect() {
|
|
|
if (isPopping || _isShakeEffectActive) return;
|
|
if (isPopping || _isShakeEffectActive) return;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
_isShakeEffectActive = true;
|
|
_isShakeEffectActive = true;
|
|
|
final random = Random();
|
|
final random = Random();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Store current scale to return to it after effect
|
|
// Store current scale to return to it after effect
|
|
|
final currentScale = scale.clone();
|
|
final currentScale = scale.clone();
|
|
|
final targetScale = currentScale * 1.1; // Only 10% bigger than current size
|
|
final targetScale = currentScale * 1.1; // Only 10% bigger than current size
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Use absolute scaling to prevent flickering with lifecycle scaling
|
|
// Use absolute scaling to prevent flickering with lifecycle scaling
|
|
|
final scaleUpEffect = ScaleEffect.to(
|
|
final scaleUpEffect = ScaleEffect.to(
|
|
|
targetScale,
|
|
targetScale,
|
|
@@ -412,7 +411,7 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
curve: Curves.easeOut,
|
|
curve: Curves.easeOut,
|
|
|
),
|
|
),
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
final scaleDownEffect = ScaleEffect.to(
|
|
final scaleDownEffect = ScaleEffect.to(
|
|
|
currentScale,
|
|
currentScale,
|
|
|
EffectController(
|
|
EffectController(
|
|
@@ -420,7 +419,7 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
curve: Curves.easeIn,
|
|
curve: Curves.easeIn,
|
|
|
),
|
|
),
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Add slight rotation effect - reduced intensity
|
|
// Add slight rotation effect - reduced intensity
|
|
|
final rotateEffect = RotateEffect.by(
|
|
final rotateEffect = RotateEffect.by(
|
|
|
(random.nextDouble() - 0.5) * 0.15, // Further reduced rotation
|
|
(random.nextDouble() - 0.5) * 0.15, // Further reduced rotation
|
|
@@ -430,7 +429,7 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
curve: Curves.easeInOut,
|
|
curve: Curves.easeInOut,
|
|
|
),
|
|
),
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Chain the scale effects
|
|
// Chain the scale effects
|
|
|
add(scaleUpEffect);
|
|
add(scaleUpEffect);
|
|
|
scaleUpEffect.onComplete = () {
|
|
scaleUpEffect.onComplete = () {
|
|
@@ -438,9 +437,9 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
add(scaleDownEffect);
|
|
add(scaleDownEffect);
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
add(rotateEffect);
|
|
add(rotateEffect);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Set completion callback to reset shake effect flag
|
|
// Set completion callback to reset shake effect flag
|
|
|
Future.delayed(const Duration(milliseconds: 200), () {
|
|
Future.delayed(const Duration(milliseconds: 200), () {
|
|
|
_isShakeEffectActive = false;
|
|
_isShakeEffectActive = false;
|
|
@@ -451,101 +450,152 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
@override
|
|
@override
|
|
|
bool onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
|
|
bool onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
|
|
|
super.onCollision(intersectionPoints, other);
|
|
super.onCollision(intersectionPoints, other);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if (other is Bubble && !isPopping && !other.isPopping) {
|
|
if (other is Bubble && !isPopping && !other.isPopping) {
|
|
|
_handleBubbleCollision(other, intersectionPoints);
|
|
_handleBubbleCollision(other, intersectionPoints);
|
|
|
}
|
|
}
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
void _handleBubbleCollision(Bubble other, Set<Vector2> intersectionPoints) {
|
|
void _handleBubbleCollision(Bubble other, Set<Vector2> intersectionPoints) {
|
|
|
if (intersectionPoints.isEmpty) return;
|
|
if (intersectionPoints.isEmpty) return;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Track collision for triple collision rule
|
|
// Track collision for triple collision rule
|
|
|
_incrementCollisionCount();
|
|
_incrementCollisionCount();
|
|
|
other._incrementCollisionCount();
|
|
other._incrementCollisionCount();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Calculate collision direction
|
|
// Calculate collision direction
|
|
|
final direction = (position - other.position);
|
|
final direction = (position - other.position);
|
|
|
final distance = direction.length;
|
|
final distance = direction.length;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Prevent division by zero
|
|
// Prevent division by zero
|
|
|
if (distance < 0.1) {
|
|
if (distance < 0.1) {
|
|
|
// If bubbles are too close, separate them with a random direction
|
|
// If bubbles are too close, separate them with a random direction
|
|
|
final random = Random();
|
|
final random = Random();
|
|
|
- direction.setFrom(Vector2(
|
|
|
|
|
- (random.nextDouble() - 0.5) * 2,
|
|
|
|
|
- (random.nextDouble() - 0.5) * 2,
|
|
|
|
|
- ));
|
|
|
|
|
|
|
+ direction.setFrom(
|
|
|
|
|
+ Vector2(
|
|
|
|
|
+ (random.nextDouble() - 0.5) * 2,
|
|
|
|
|
+ (random.nextDouble() - 0.5) * 2,
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
direction.normalize();
|
|
direction.normalize();
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Calculate required separation based on current bubble sizes
|
|
// Calculate required separation based on current bubble sizes
|
|
|
final thisRadius = (startSize / 2) * scale.x;
|
|
final thisRadius = (startSize / 2) * scale.x;
|
|
|
final otherRadius = (startSize / 2) * other.scale.x;
|
|
final otherRadius = (startSize / 2) * other.scale.x;
|
|
|
- final requiredDistance = thisRadius + otherRadius + 5.0; // 5px padding
|
|
|
|
|
-
|
|
|
|
|
- // If bubbles are overlapping, separate them immediately
|
|
|
|
|
|
|
+ final requiredDistance =
|
|
|
|
|
+ thisRadius + otherRadius + 10.0; // Increased padding to 10px
|
|
|
|
|
+
|
|
|
|
|
+ // If bubbles are overlapping, separate them immediately with more aggressive separation
|
|
|
if (distance < requiredDistance) {
|
|
if (distance < requiredDistance) {
|
|
|
- final separationNeeded = requiredDistance - distance;
|
|
|
|
|
|
|
+ final separationNeeded =
|
|
|
|
|
+ requiredDistance - distance + 5.0; // Extra 5px buffer
|
|
|
final separationVector = direction * (separationNeeded / 2);
|
|
final separationVector = direction * (separationNeeded / 2);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Move both bubbles away from each other
|
|
// Move both bubbles away from each other
|
|
|
position.add(separationVector);
|
|
position.add(separationVector);
|
|
|
other.position.sub(separationVector);
|
|
other.position.sub(separationVector);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Ensure bubbles stay within bounds after separation
|
|
// Ensure bubbles stay within bounds after separation
|
|
|
_keepInBounds();
|
|
_keepInBounds();
|
|
|
other._keepInBounds();
|
|
other._keepInBounds();
|
|
|
|
|
+
|
|
|
|
|
+ // Additional check: if still overlapping after separation, apply emergency separation
|
|
|
|
|
+ final newDistance = (position - other.position).length;
|
|
|
|
|
+ if (newDistance < requiredDistance) {
|
|
|
|
|
+ final emergencyVector =
|
|
|
|
|
+ direction * ((requiredDistance - newDistance + 10.0) / 2);
|
|
|
|
|
+ position.add(emergencyVector);
|
|
|
|
|
+ other.position.sub(emergencyVector);
|
|
|
|
|
+ _keepInBounds();
|
|
|
|
|
+ other._keepInBounds();
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Apply collision response - bubbles bounce off each other
|
|
// Apply collision response - bubbles bounce off each other
|
|
|
final collisionForce = 25.0; // Reduced from 30.0 for gentler bouncing
|
|
final collisionForce = 25.0; // Reduced from 30.0 for gentler bouncing
|
|
|
velocity.add(direction * collisionForce);
|
|
velocity.add(direction * collisionForce);
|
|
|
other.velocity.add(direction * -collisionForce);
|
|
other.velocity.add(direction * -collisionForce);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Apply stronger damping to prevent infinite acceleration
|
|
// Apply stronger damping to prevent infinite acceleration
|
|
|
velocity.scale(0.6); // Increased damping from 0.7 to 0.6
|
|
velocity.scale(0.6); // Increased damping from 0.7 to 0.6
|
|
|
other.velocity.scale(0.6);
|
|
other.velocity.scale(0.6);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// Update collision tracking and handle grace period reset
|
|
/// Update collision tracking and handle grace period reset
|
|
|
void _updateCollisionTracking(double dt) {
|
|
void _updateCollisionTracking(double dt) {
|
|
|
final currentTime = DateTime.now().millisecondsSinceEpoch / 1000.0;
|
|
final currentTime = DateTime.now().millisecondsSinceEpoch / 1000.0;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Reset collision count if grace period has passed
|
|
// Reset collision count if grace period has passed
|
|
|
if (currentTime - _lastCollisionTime > collisionGracePeriod) {
|
|
if (currentTime - _lastCollisionTime > collisionGracePeriod) {
|
|
|
_collisionCount = 0;
|
|
_collisionCount = 0;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// Increment collision count and check for auto-pop
|
|
/// Increment collision count and check for auto-pop
|
|
|
void _incrementCollisionCount() {
|
|
void _incrementCollisionCount() {
|
|
|
if (isPopping) return;
|
|
if (isPopping) return;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
_collisionCount++;
|
|
_collisionCount++;
|
|
|
_lastCollisionTime = DateTime.now().millisecondsSinceEpoch / 1000.0;
|
|
_lastCollisionTime = DateTime.now().millisecondsSinceEpoch / 1000.0;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Auto-pop after max collisions
|
|
// Auto-pop after max collisions
|
|
|
if (_collisionCount >= maxCollisions) {
|
|
if (_collisionCount >= maxCollisions) {
|
|
|
pop(userTriggered: false); // Auto-pop due to collision rule
|
|
pop(userTriggered: false); // Auto-pop due to collision rule
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// Get current collision count (for debugging/testing)
|
|
/// Get current collision count (for debugging/testing)
|
|
|
int get collisionCount => _collisionCount;
|
|
int get collisionCount => _collisionCount;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// Apply tilt force to bubble based on device orientation
|
|
/// Apply tilt force to bubble based on device orientation
|
|
|
void applyTiltForce(double tiltStrength) {
|
|
void applyTiltForce(double tiltStrength) {
|
|
|
if (isPopping) return;
|
|
if (isPopping) return;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Reduced tilt force to prevent excessive movement during shaking
|
|
// Reduced tilt force to prevent excessive movement during shaking
|
|
|
// Apply force opposite to tilt direction
|
|
// Apply force opposite to tilt direction
|
|
|
// If device tilts right, bubbles move left and vice versa
|
|
// If device tilts right, bubbles move left and vice versa
|
|
|
final tiltForce = -tiltStrength * 8.0; // Reduced from 15.0 to 8.0
|
|
final tiltForce = -tiltStrength * 8.0; // Reduced from 15.0 to 8.0
|
|
|
velocity.x += tiltForce;
|
|
velocity.x += tiltForce;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Add slight vertical component for more natural movement
|
|
// Add slight vertical component for more natural movement
|
|
|
velocity.y += tiltForce * 0.2; // Reduced from 0.3 to 0.2
|
|
velocity.y += tiltForce * 0.2; // Reduced from 0.3 to 0.2
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
|
|
+
|
|
|
|
|
+ double _lastOverlapCheck = 0.0;
|
|
|
|
|
+ static const double overlapCheckInterval = 0.1; // Check every 100ms
|
|
|
|
|
+
|
|
|
|
|
+ /// Periodic check to prevent bubbles from getting stuck overlapping
|
|
|
|
|
+ void _preventOverlapSticking(double dt) {
|
|
|
|
|
+ _lastOverlapCheck += dt;
|
|
|
|
|
+
|
|
|
|
|
+ // Only check periodically to avoid performance issues
|
|
|
|
|
+ if (_lastOverlapCheck < overlapCheckInterval) return;
|
|
|
|
|
+ _lastOverlapCheck = 0.0;
|
|
|
|
|
+
|
|
|
|
|
+ // Get all other bubbles from the game
|
|
|
|
|
+ final allBubbles = game.children.whereType<Bubble>();
|
|
|
|
|
+
|
|
|
|
|
+ for (final otherBubble in allBubbles) {
|
|
|
|
|
+ if (otherBubble == this || otherBubble.isPopping || isPopping) continue;
|
|
|
|
|
+
|
|
|
|
|
+ final distance = position.distanceTo(otherBubble.position);
|
|
|
|
|
+ final thisRadius = (startSize / 2) * scale.x;
|
|
|
|
|
+ final otherRadius = (startSize / 2) * otherBubble.scale.x;
|
|
|
|
|
+ final requiredDistance = thisRadius + otherRadius + 8.0; // 8px padding
|
|
|
|
|
+
|
|
|
|
|
+ // If bubbles are overlapping, apply gentle separation force
|
|
|
|
|
+ if (distance < requiredDistance && distance > 0.1) {
|
|
|
|
|
+ final direction = (position - otherBubble.position).normalized();
|
|
|
|
|
+ final separationForce =
|
|
|
|
|
+ (requiredDistance - distance) * 0.5; // Gentle force
|
|
|
|
|
+
|
|
|
|
|
+ // Apply separation force to velocities instead of direct position changes
|
|
|
|
|
+ // to make the movement more natural
|
|
|
|
|
+ velocity.add(direction * separationForce);
|
|
|
|
|
+ otherBubble.velocity.add(direction * -separationForce);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|