Refactor gem swap logic to handle invalid moves and animations
- Add proper validation for null gems during swap operations - Implement swap-back animation for invalid moves (no matches) - Restructure swap flow to emit animation states before match detection - Add move limit validation before processing matches - Improve error handling and logging for edge cases
This commit is contained in:
parent
3f12ce8d3f
commit
eaa6947e93
@ -279,13 +279,16 @@ class GameBloc extends Bloc<GameEvent, GameState> {
|
||||
currentState.grid, event.row1, event.col1, event.row2, event.col2)) {
|
||||
return;
|
||||
}
|
||||
final newGrid = currentState.grid.clone();
|
||||
GameGrid newGrid = currentState.grid.clone();
|
||||
|
||||
// Perform swap
|
||||
final gem1 = newGrid.getGem(event.row1, event.col1);
|
||||
final gem2 = newGrid.getGem(event.row2, event.col2);
|
||||
|
||||
if (gem1 != null && gem2 != null) {
|
||||
if (gem1 == null || gem2 == null) {
|
||||
print("Gem1 or Gem2 is null, that should not be so");
|
||||
return;
|
||||
}
|
||||
newGrid.setGem(event.row1, event.col1,
|
||||
gem2.copyWith(row: event.row1, col: event.col1));
|
||||
newGrid.setGem(event.row2, event.col2,
|
||||
@ -343,6 +346,7 @@ class GameBloc extends Bloc<GameEvent, GameState> {
|
||||
add(ProcessMatches());
|
||||
} else {
|
||||
// Revert swap if no matches
|
||||
newGrid = newGrid.clone();
|
||||
newGrid.setGem(event.row1, event.col1, gem1);
|
||||
newGrid.setGem(event.row2, event.col2, gem2);
|
||||
var done = Completer();
|
||||
@ -358,8 +362,19 @@ class GameBloc extends Bloc<GameEvent, GameState> {
|
||||
gem2: gem1,
|
||||
done: done.complete,
|
||||
));
|
||||
}
|
||||
// Wait for swap animation
|
||||
// Wait for swap-back animation
|
||||
await done.future;
|
||||
|
||||
done = Completer();
|
||||
emit(GameLevelIdle(
|
||||
grid: newGrid,
|
||||
level: currentState.level,
|
||||
score: currentState.score,
|
||||
moves: currentState.moves,
|
||||
timeElapsed: currentState.timeElapsed,
|
||||
gemsCleared: currentState.gemsCleared,
|
||||
done: done.complete,
|
||||
));
|
||||
await done.future;
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ class GemComponent extends RectangleComponent with TapCallbacks {
|
||||
parent?.add(explosion);
|
||||
|
||||
// Animate gem destruction
|
||||
PhysicsSystem.animateMatch(this);
|
||||
await PhysicsSystem.animateMatch(this);
|
||||
|
||||
// Remove after animation
|
||||
await Future.delayed(
|
||||
|
||||
@ -9,11 +9,12 @@ class ParticleSystem {
|
||||
return ParticleSystemComponent(
|
||||
priority: 1,
|
||||
particle: Particle.generate(
|
||||
count: 15,
|
||||
count: 50,
|
||||
lifespan: 1.0,
|
||||
generator: (i) => AcceleratedParticle(
|
||||
acceleration: Vector2(0, 200),
|
||||
speed: Vector2.random(Random()) * 100,
|
||||
acceleration:
|
||||
(Vector2.random(Random()) - Vector2.random(Random())) * 300,
|
||||
speed: (Vector2.random(Random()) - Vector2.random(Random())) * 500,
|
||||
position: Vector2.zero(),
|
||||
child: CircleParticle(
|
||||
radius: Random().nextDouble() * 3 + 2,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:match_three/game/components/gem_component.dart';
|
||||
import 'package:match_three/game/systems/match_detector.dart';
|
||||
import 'components/grid_component.dart';
|
||||
import 'components/background_component.dart';
|
||||
@ -51,23 +52,32 @@ class MatchThreeGame extends FlameGame {
|
||||
selectedGem = gem;
|
||||
return;
|
||||
}
|
||||
if (MatchDetector.isValidSwap(gridComponent!.gameGrid, selectedGem!.row,
|
||||
selectedGem!.col, gem.row, gem.col)) {
|
||||
var theGem = selectedGem;
|
||||
selectedGem = null;
|
||||
if (MatchDetector.isValidSwap(
|
||||
gridComponent!.gameGrid, theGem!.row, theGem.col, gem.row, gem.col)) {
|
||||
// Attempt swap
|
||||
print(
|
||||
")) Valid swapping ${selectedGem!.row}, ${selectedGem!.col} with ${gem.row}, ${gem.col}");
|
||||
")) Valid swapping ${theGem.row}, ${theGem.col} with ${gem.row}, ${gem.col}");
|
||||
|
||||
gameBloc.add(SwapGems(
|
||||
selectedGem!.row,
|
||||
selectedGem!.col,
|
||||
theGem.row,
|
||||
theGem.col,
|
||||
gem.row,
|
||||
gem.col,
|
||||
));
|
||||
} else {
|
||||
print(
|
||||
"(( Invalid swapping ${selectedGem!.row}, ${selectedGem!.col} with ${gem.row}, ${gem.col}");
|
||||
"(( Invalid swapping ${theGem.row}, ${theGem.col} with ${gem.row}, ${gem.col}");
|
||||
}
|
||||
if (gridComponent == null) {
|
||||
return;
|
||||
}
|
||||
for (var comp in gridComponent!.children) {
|
||||
if (comp is GemComponent && comp.gem != gem) {
|
||||
comp.isSelected = false;
|
||||
}
|
||||
}
|
||||
selectedGem = null;
|
||||
}
|
||||
|
||||
void setGameBloc(GameBloc bloc) {
|
||||
|
||||
@ -45,7 +45,7 @@ class GameGrid {
|
||||
return row >= 0 && row < height && col >= 0 && col < width;
|
||||
}
|
||||
|
||||
clone() {
|
||||
GameGrid clone() {
|
||||
final clonedGrid = GameGrid(width: width, height: height);
|
||||
for (int row = 0; row < _grid.length; row++) {
|
||||
for (int col = 0; col < _grid[row].length; col++) {
|
||||
|
||||
@ -11,7 +11,7 @@ class MatchDetector {
|
||||
for (int row = 0; row < grid.height; row++) {
|
||||
for (int col = 0; col < grid.width; col++) {
|
||||
final gem = grid.getGem(row, col);
|
||||
if (gem == null || matches.contains(gem)) {
|
||||
if (gem == null) {
|
||||
continue;
|
||||
}
|
||||
currentMatch.add(gem);
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/effects.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -38,17 +40,23 @@ class PhysicsSystem {
|
||||
await Future.delayed(Duration(milliseconds: (fallTime * 1000).toInt()));
|
||||
}
|
||||
|
||||
static void animateMatch(GemComponent gem) {
|
||||
static Future<void> animateMatch(GemComponent gem) async {
|
||||
final scaleDone = Completer();
|
||||
final opacityDone = Completer();
|
||||
// Scale down and fade out
|
||||
gem.add(ScaleEffect.to(
|
||||
Vector2.zero(),
|
||||
EffectController(duration: GameConstants.matchDuration),
|
||||
onComplete: () => scaleDone.complete(),
|
||||
));
|
||||
|
||||
gem.add(OpacityEffect.to(
|
||||
0.0,
|
||||
EffectController(duration: GameConstants.matchDuration),
|
||||
onComplete: () => opacityDone.complete(),
|
||||
));
|
||||
|
||||
await Future.wait([scaleDone.future, opacityDone.future]);
|
||||
}
|
||||
|
||||
static void animatePop(GemComponent gem) {
|
||||
|
||||
@ -19,6 +19,7 @@ class GameScreen extends StatefulWidget {
|
||||
|
||||
class _GameScreenState extends State<GameScreen> {
|
||||
late MatchThreeGame game;
|
||||
final bool isDebug = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -75,6 +76,16 @@ class _GameScreenState extends State<GameScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (isDebug)
|
||||
Text(
|
||||
'State: ${state.runtimeType}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Score: ${state.score}',
|
||||
style: const TextStyle(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user