|
@@ -1,11 +1,13 @@
|
|
|
import 'dart:math';
|
|
import 'dart:math';
|
|
|
|
|
+import 'dart:async';
|
|
|
import 'package:flame/components.dart';
|
|
import 'package:flame/components.dart';
|
|
|
import 'package:flame/effects.dart';
|
|
import 'package:flame/effects.dart';
|
|
|
import 'package:flame/events.dart';
|
|
import 'package:flame/events.dart';
|
|
|
|
|
+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 {
|
|
|
|
|
|
|
+class Bubble extends SpriteComponent with HasGameReference, TapCallbacks, CollisionCallbacks {
|
|
|
static const double startSize = 60.0; // Increased from 40.0
|
|
static const double startSize = 60.0; // Increased from 40.0
|
|
|
static const double maxSize = 120.0; // Increased from 100.0
|
|
static const double maxSize = 120.0; // Increased from 100.0
|
|
|
static const double lifecycleDuration = 12.0; // seconds
|
|
static const double lifecycleDuration = 12.0; // seconds
|
|
@@ -20,6 +22,13 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks {
|
|
|
double _lifecycleProgress = 0.0;
|
|
double _lifecycleProgress = 0.0;
|
|
|
late double _targetSize;
|
|
late double _targetSize;
|
|
|
|
|
|
|
|
|
|
+ // Physics properties for collision and movement
|
|
|
|
|
+ Vector2 velocity = Vector2.zero();
|
|
|
|
|
+ static const double maxSpeed = 50.0;
|
|
|
|
|
+ static const double friction = 0.98;
|
|
|
|
|
+ static const double collisionDamping = 0.7;
|
|
|
|
|
+ double _floatTimer = 0.0;
|
|
|
|
|
+
|
|
|
Bubble({
|
|
Bubble({
|
|
|
required Vector2 position,
|
|
required Vector2 position,
|
|
|
this.onPop,
|
|
this.onPop,
|
|
@@ -44,6 +53,12 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks {
|
|
|
|
|
|
|
|
await super.onLoad();
|
|
await super.onLoad();
|
|
|
|
|
|
|
|
|
|
+ // Add circular collision hitbox
|
|
|
|
|
+ add(CircleHitbox(
|
|
|
|
|
+ radius: _targetSize / 2,
|
|
|
|
|
+ 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();
|
|
|
|
|
|
|
@@ -53,6 +68,13 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks {
|
|
|
// Add varied, organic floating animations
|
|
// Add varied, organic floating animations
|
|
|
_addRandomFloatingAnimation();
|
|
_addRandomFloatingAnimation();
|
|
|
_addRandomRotationAnimation();
|
|
_addRandomRotationAnimation();
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize random velocity for natural movement
|
|
|
|
|
+ final random = Random();
|
|
|
|
|
+ velocity = Vector2(
|
|
|
|
|
+ (random.nextDouble() - 0.5) * 20, // -10 to +10
|
|
|
|
|
+ (random.nextDouble() - 0.5) * 20, // -10 to +10
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
@override
|
|
@@ -73,11 +95,65 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks {
|
|
|
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
|
|
|
|
|
+ _updatePhysics(dt);
|
|
|
|
|
+
|
|
|
|
|
+ // Keep bubble within screen bounds
|
|
|
|
|
+ _keepInBounds();
|
|
|
|
|
+
|
|
|
// 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) {
|
|
|
|
|
+ // Apply velocity to position
|
|
|
|
|
+ position.add(velocity * dt);
|
|
|
|
|
+
|
|
|
|
|
+ // Apply friction
|
|
|
|
|
+ velocity.scale(friction);
|
|
|
|
|
+
|
|
|
|
|
+ // Add periodic floating effects
|
|
|
|
|
+ _floatTimer += dt;
|
|
|
|
|
+ if (_floatTimer > 2.0) {
|
|
|
|
|
+ _floatTimer = 0.0;
|
|
|
|
|
+ final random = Random();
|
|
|
|
|
+ final floatForce = Vector2(
|
|
|
|
|
+ (random.nextDouble() - 0.5) * 8, // -4 to +4 horizontal
|
|
|
|
|
+ (random.nextDouble() - 0.5) * 8, // -4 to +4 vertical
|
|
|
|
|
+ );
|
|
|
|
|
+ velocity.add(floatForce);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Clamp velocity to max speed
|
|
|
|
|
+ if (velocity.length > maxSpeed) {
|
|
|
|
|
+ velocity.normalize();
|
|
|
|
|
+ velocity.scale(maxSpeed);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void _keepInBounds() {
|
|
|
|
|
+ const margin = 60.0;
|
|
|
|
|
+ final gameSize = game.size;
|
|
|
|
|
+
|
|
|
|
|
+ // Bounce off screen edges
|
|
|
|
|
+ if (position.x < margin) {
|
|
|
|
|
+ position.x = margin;
|
|
|
|
|
+ velocity.x = velocity.x.abs(); // Bounce right
|
|
|
|
|
+ } else if (position.x > gameSize.x - margin) {
|
|
|
|
|
+ position.x = gameSize.x - margin;
|
|
|
|
|
+ velocity.x = -velocity.x.abs(); // Bounce left
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (position.y < margin + 100) { // Account for UI at top
|
|
|
|
|
+ position.y = margin + 100;
|
|
|
|
|
+ velocity.y = velocity.y.abs(); // Bounce down
|
|
|
|
|
+ } else if (position.y > gameSize.y - margin) {
|
|
|
|
|
+ position.y = gameSize.y - margin;
|
|
|
|
|
+ velocity.y = -velocity.y.abs(); // Bounce up
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
Map<String, dynamic> _getRandomBubbleData() {
|
|
Map<String, dynamic> _getRandomBubbleData() {
|
|
|
final random = Random();
|
|
final random = Random();
|
|
@@ -105,26 +181,11 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks {
|
|
|
void _addRandomFloatingAnimation() {
|
|
void _addRandomFloatingAnimation() {
|
|
|
final random = Random();
|
|
final random = Random();
|
|
|
|
|
|
|
|
- // Random floating direction and amplitude
|
|
|
|
|
- final floatDirection = Vector2(
|
|
|
|
|
- (random.nextDouble() - 0.5) * 30, // -15 to +15 horizontal
|
|
|
|
|
- -5 - random.nextDouble() * 15, // -5 to -20 vertical (upward bias)
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- // Random duration between 2.5 and 6 seconds
|
|
|
|
|
- final duration = 2.5 + random.nextDouble() * 3.5;
|
|
|
|
|
|
|
+ // Add a small upward bias to the initial velocity
|
|
|
|
|
+ velocity.y += -5 - random.nextDouble() * 10; // -5 to -15 upward bias
|
|
|
|
|
|
|
|
- add(
|
|
|
|
|
- MoveEffect.by(
|
|
|
|
|
- floatDirection,
|
|
|
|
|
- EffectController(
|
|
|
|
|
- duration: duration,
|
|
|
|
|
- alternate: true,
|
|
|
|
|
- infinite: true,
|
|
|
|
|
- curve: Curves.easeInOut,
|
|
|
|
|
- ),
|
|
|
|
|
- ),
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ // The floating effect will now be handled by the physics system
|
|
|
|
|
+ // and periodic velocity changes in the update method
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void _addEntranceAnimation() {
|
|
void _addEntranceAnimation() {
|
|
@@ -381,4 +442,49 @@ class Bubble extends SpriteComponent with HasGameReference, TapCallbacks {
|
|
|
),
|
|
),
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // Collision detection methods
|
|
|
|
|
+ @override
|
|
|
|
|
+ bool onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
|
|
|
|
|
+ super.onCollision(intersectionPoints, other);
|
|
|
|
|
+
|
|
|
|
|
+ if (other is Bubble && !isPopping && !other.isPopping) {
|
|
|
|
|
+ _handleBubbleCollision(other, intersectionPoints);
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void _handleBubbleCollision(Bubble other, Set<Vector2> intersectionPoints) {
|
|
|
|
|
+ if (intersectionPoints.isEmpty) return;
|
|
|
|
|
+
|
|
|
|
|
+ // Calculate collision direction
|
|
|
|
|
+ final direction = (position - other.position).normalized();
|
|
|
|
|
+
|
|
|
|
|
+ // Apply collision response - bubbles bounce off each other
|
|
|
|
|
+ final collisionForce = 30.0;
|
|
|
|
|
+ velocity.add(direction * collisionForce);
|
|
|
|
|
+ other.velocity.add(direction * -collisionForce);
|
|
|
|
|
+
|
|
|
|
|
+ // Apply damping to prevent infinite acceleration
|
|
|
|
|
+ velocity.scale(collisionDamping);
|
|
|
|
|
+ other.velocity.scale(collisionDamping);
|
|
|
|
|
+
|
|
|
|
|
+ // Add slight separation to prevent sticking
|
|
|
|
|
+ final separation = direction * 2.0;
|
|
|
|
|
+ position.add(separation);
|
|
|
|
|
+ other.position.sub(separation);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// Apply tilt force to bubble based on device orientation
|
|
|
|
|
+ void applyTiltForce(double tiltStrength) {
|
|
|
|
|
+ if (isPopping) return;
|
|
|
|
|
+
|
|
|
|
|
+ // Apply force opposite to tilt direction
|
|
|
|
|
+ // If device tilts right, bubbles move left and vice versa
|
|
|
|
|
+ final tiltForce = -tiltStrength * 15.0; // Adjust force strength
|
|
|
|
|
+ velocity.x += tiltForce;
|
|
|
|
|
+
|
|
|
|
|
+ // Add slight vertical component for more natural movement
|
|
|
|
|
+ velocity.y += tiltForce * 0.3;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|