|
|
@@ -8,16 +8,25 @@ 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
|
|
|
+ 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 lifecycleDuration = 12.0; // Same lifecycle duration for all bubbles
|
|
|
+
|
|
|
+ // Triple collision rule constants
|
|
|
+ static const int maxCollisions = 3;
|
|
|
+ static const double collisionGracePeriod = 2.0; // seconds
|
|
|
|
|
|
late String bubbleImagePath;
|
|
|
late Color bubbleColor;
|
|
|
bool isPopping = false;
|
|
|
bool poppedByUser = false;
|
|
|
+ bool isAutoSpawned = true; // Whether bubble was auto-spawned or user-triggered
|
|
|
Function(Bubble, bool)? onPop;
|
|
|
|
|
|
+ // Collision tracking for triple collision rule
|
|
|
+ int _collisionCount = 0;
|
|
|
+ double _lastCollisionTime = 0.0;
|
|
|
+
|
|
|
double _age = 0.0;
|
|
|
double _lifecycleProgress = 0.0;
|
|
|
late double _targetSize;
|
|
|
@@ -32,13 +41,14 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
Bubble({
|
|
|
required Vector2 position,
|
|
|
this.onPop,
|
|
|
+ this.isAutoSpawned = true,
|
|
|
}) : 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
|
|
|
+ // All bubbles have the same target size regardless of type
|
|
|
+ _targetSize = maxSize;
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
@@ -53,9 +63,9 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
|
|
|
await super.onLoad();
|
|
|
|
|
|
- // Add circular collision hitbox
|
|
|
+ // Add circular collision hitbox - will be updated dynamically
|
|
|
add(CircleHitbox(
|
|
|
- radius: _targetSize / 2,
|
|
|
+ radius: startSize / 2, // Start with initial size
|
|
|
anchor: Anchor.center,
|
|
|
));
|
|
|
|
|
|
@@ -87,9 +97,16 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
_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 scale based on lifecycle progress - grow from startSize to maxSize
|
|
|
+ final currentScale = _lifecycleProgress * (_targetSize / startSize);
|
|
|
+ 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
|
|
|
+ final currentRadius = (startSize / 2) * scale.x; // Use scale.x since it's uniform scaling
|
|
|
+ final hitbox = children.whereType<CircleHitbox>().firstOrNull;
|
|
|
+ if (hitbox != null) {
|
|
|
+ hitbox.radius = currentRadius;
|
|
|
+ }
|
|
|
|
|
|
// Update opacity based on lifecycle (fade out towards the end)
|
|
|
final opacityFactor = 1.0 - (_lifecycleProgress * 0.6); // Fade to 40% opacity
|
|
|
@@ -101,6 +118,9 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
// Keep bubble within screen bounds
|
|
|
_keepInBounds();
|
|
|
|
|
|
+ // Update collision tracking
|
|
|
+ _updateCollisionTracking(dt);
|
|
|
+
|
|
|
// Auto-pop when lifecycle is complete
|
|
|
if (_lifecycleProgress >= 1.0) {
|
|
|
pop(userTriggered: false);
|
|
|
@@ -189,18 +209,10 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
}
|
|
|
|
|
|
void _addEntranceAnimation() {
|
|
|
- // Smooth scale-in animation with bounce effect
|
|
|
- add(
|
|
|
- ScaleEffect.to(
|
|
|
- Vector2.all(1.0),
|
|
|
- EffectController(
|
|
|
- duration: 0.6,
|
|
|
- curve: Curves.elasticOut,
|
|
|
- ),
|
|
|
- ),
|
|
|
- );
|
|
|
+ // Start at scale 1.0 (startSize) and grow gradually throughout lifecycle
|
|
|
+ // The growth will be handled in the update method based on lifecycle progress
|
|
|
|
|
|
- // Optional fade-in animation
|
|
|
+ // Simple fade-in animation only
|
|
|
opacity = 0.0;
|
|
|
add(
|
|
|
OpacityEffect.to(
|
|
|
@@ -377,53 +389,62 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // Track if shake effect is currently active to prevent overlapping effects
|
|
|
+ bool _isShakeEffectActive = false;
|
|
|
+
|
|
|
/// Add shake effect to this bubble (called when phone is shaken)
|
|
|
+ /// Fixed version that only adds visual effects without movement or size flickering
|
|
|
void addShakeEffect() {
|
|
|
- if (isPopping) return;
|
|
|
+ if (isPopping || _isShakeEffectActive) return;
|
|
|
|
|
|
+ _isShakeEffectActive = true;
|
|
|
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
|
|
|
- );
|
|
|
+ // Store current scale to return to it after effect
|
|
|
+ final currentScale = scale.clone();
|
|
|
+ final targetScale = currentScale * 1.1; // Only 10% bigger than current size
|
|
|
|
|
|
- // Add shake movement effect
|
|
|
- add(
|
|
|
- MoveEffect.by(
|
|
|
- shakeDirection,
|
|
|
- EffectController(
|
|
|
- duration: 0.4,
|
|
|
- alternate: true,
|
|
|
- curve: Curves.elasticOut,
|
|
|
- ),
|
|
|
+ // Use absolute scaling to prevent flickering with lifecycle scaling
|
|
|
+ final scaleUpEffect = ScaleEffect.to(
|
|
|
+ targetScale,
|
|
|
+ EffectController(
|
|
|
+ duration: 0.1, // Very short duration
|
|
|
+ curve: Curves.easeOut,
|
|
|
),
|
|
|
);
|
|
|
|
|
|
- // Add scale bounce effect
|
|
|
- add(
|
|
|
- ScaleEffect.by(
|
|
|
- Vector2.all(0.3),
|
|
|
- EffectController(
|
|
|
- duration: 0.2,
|
|
|
- alternate: true,
|
|
|
- curve: Curves.elasticOut,
|
|
|
- ),
|
|
|
+ final scaleDownEffect = ScaleEffect.to(
|
|
|
+ currentScale,
|
|
|
+ EffectController(
|
|
|
+ duration: 0.1, // Very short duration
|
|
|
+ curve: Curves.easeIn,
|
|
|
),
|
|
|
);
|
|
|
|
|
|
- // Add slight rotation effect
|
|
|
- add(
|
|
|
- RotateEffect.by(
|
|
|
- (random.nextDouble() - 0.5) * 0.4, // Random rotation
|
|
|
- EffectController(
|
|
|
- duration: 0.3,
|
|
|
- alternate: true,
|
|
|
- curve: Curves.elasticOut,
|
|
|
- ),
|
|
|
+ // Add slight rotation effect - reduced intensity
|
|
|
+ final rotateEffect = RotateEffect.by(
|
|
|
+ (random.nextDouble() - 0.5) * 0.15, // Further reduced rotation
|
|
|
+ EffectController(
|
|
|
+ duration: 0.15,
|
|
|
+ alternate: true,
|
|
|
+ curve: Curves.easeInOut,
|
|
|
),
|
|
|
);
|
|
|
+
|
|
|
+ // Chain the scale effects
|
|
|
+ add(scaleUpEffect);
|
|
|
+ scaleUpEffect.onComplete = () {
|
|
|
+ if (isMounted && !isPopping) {
|
|
|
+ add(scaleDownEffect);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ add(rotateEffect);
|
|
|
+
|
|
|
+ // Set completion callback to reset shake effect flag
|
|
|
+ Future.delayed(const Duration(milliseconds: 200), () {
|
|
|
+ _isShakeEffectActive = false;
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
// Collision detection methods
|
|
|
@@ -440,34 +461,91 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, Collis
|
|
|
void _handleBubbleCollision(Bubble other, Set<Vector2> intersectionPoints) {
|
|
|
if (intersectionPoints.isEmpty) return;
|
|
|
|
|
|
+ // Track collision for triple collision rule
|
|
|
+ _incrementCollisionCount();
|
|
|
+ other._incrementCollisionCount();
|
|
|
+
|
|
|
// Calculate collision direction
|
|
|
- final direction = (position - other.position).normalized();
|
|
|
+ final direction = (position - other.position);
|
|
|
+ final distance = direction.length;
|
|
|
+
|
|
|
+ // Prevent division by zero
|
|
|
+ if (distance < 0.1) {
|
|
|
+ // If bubbles are too close, separate them with a random direction
|
|
|
+ final random = Random();
|
|
|
+ direction.setFrom(Vector2(
|
|
|
+ (random.nextDouble() - 0.5) * 2,
|
|
|
+ (random.nextDouble() - 0.5) * 2,
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ direction.normalize();
|
|
|
+
|
|
|
+ // Calculate required separation based on current bubble sizes
|
|
|
+ final thisRadius = (startSize / 2) * scale.x;
|
|
|
+ final otherRadius = (startSize / 2) * other.scale.x;
|
|
|
+ final requiredDistance = thisRadius + otherRadius + 5.0; // 5px padding
|
|
|
+
|
|
|
+ // If bubbles are overlapping, separate them immediately
|
|
|
+ if (distance < requiredDistance) {
|
|
|
+ final separationNeeded = requiredDistance - distance;
|
|
|
+ final separationVector = direction * (separationNeeded / 2);
|
|
|
+
|
|
|
+ // Move both bubbles away from each other
|
|
|
+ position.add(separationVector);
|
|
|
+ other.position.sub(separationVector);
|
|
|
+
|
|
|
+ // Ensure bubbles stay within bounds after separation
|
|
|
+ _keepInBounds();
|
|
|
+ other._keepInBounds();
|
|
|
+ }
|
|
|
|
|
|
// Apply collision response - bubbles bounce off each other
|
|
|
- final collisionForce = 30.0;
|
|
|
+ final collisionForce = 25.0; // Reduced from 30.0 for gentler bouncing
|
|
|
velocity.add(direction * collisionForce);
|
|
|
other.velocity.add(direction * -collisionForce);
|
|
|
|
|
|
- // Apply damping to prevent infinite acceleration
|
|
|
- velocity.scale(collisionDamping);
|
|
|
- other.velocity.scale(collisionDamping);
|
|
|
+ // Apply stronger damping to prevent infinite acceleration
|
|
|
+ velocity.scale(0.6); // Increased damping from 0.7 to 0.6
|
|
|
+ other.velocity.scale(0.6);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Update collision tracking and handle grace period reset
|
|
|
+ void _updateCollisionTracking(double dt) {
|
|
|
+ final currentTime = DateTime.now().millisecondsSinceEpoch / 1000.0;
|
|
|
|
|
|
- // Add slight separation to prevent sticking
|
|
|
- final separation = direction * 2.0;
|
|
|
- position.add(separation);
|
|
|
- other.position.sub(separation);
|
|
|
+ // Reset collision count if grace period has passed
|
|
|
+ if (currentTime - _lastCollisionTime > collisionGracePeriod) {
|
|
|
+ _collisionCount = 0;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+ /// Increment collision count and check for auto-pop
|
|
|
+ void _incrementCollisionCount() {
|
|
|
+ if (isPopping) return;
|
|
|
+
|
|
|
+ _collisionCount++;
|
|
|
+ _lastCollisionTime = DateTime.now().millisecondsSinceEpoch / 1000.0;
|
|
|
+
|
|
|
+ // Auto-pop after max collisions
|
|
|
+ if (_collisionCount >= maxCollisions) {
|
|
|
+ pop(userTriggered: false); // Auto-pop due to collision rule
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Get current collision count (for debugging/testing)
|
|
|
+ int get collisionCount => _collisionCount;
|
|
|
+
|
|
|
/// Apply tilt force to bubble based on device orientation
|
|
|
void applyTiltForce(double tiltStrength) {
|
|
|
if (isPopping) return;
|
|
|
|
|
|
+ // Reduced tilt force to prevent excessive movement during shaking
|
|
|
// Apply force opposite to tilt direction
|
|
|
// If device tilts right, bubbles move left and vice versa
|
|
|
- final tiltForce = -tiltStrength * 15.0; // Adjust force strength
|
|
|
+ final tiltForce = -tiltStrength * 8.0; // Reduced from 15.0 to 8.0
|
|
|
velocity.x += tiltForce;
|
|
|
|
|
|
// Add slight vertical component for more natural movement
|
|
|
- velocity.y += tiltForce * 0.3;
|
|
|
+ velocity.y += tiltForce * 0.2; // Reduced from 0.3 to 0.2
|
|
|
}
|
|
|
}
|