import 'dart:async'; import 'package:flame/game.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import '../game/models/grid.dart'; import '../game/models/gem.dart'; import '../game/systems/match_detector.dart'; import '../game/systems/gravity_system.dart'; import '../utils/constants.dart'; // Events abstract class GameEvent extends Equatable { @override List get props => []; } class StartGame extends GameEvent {} class SwapGems extends GameEvent { final int row1, col1, row2, col2; SwapGems(this.row1, this.col1, this.row2, this.col2); @override List get props => [row1, col1, row2, col2]; } class ProcessMatches extends GameEvent { final int combo; ProcessMatches({this.combo = 0}); @override List get props => [combo]; } // States typedef DoneCallback = void Function([FutureOr?]); abstract class GameState extends Equatable { @override List get props => []; } // Internal match state event class _GamePlayingMatch extends GameState { final GameGrid grid; final int score; final int moves; final List matches; _GamePlayingMatch({ required this.grid, this.score = 0, this.moves = 0, this.matches = const [], }); @override List get props => [grid, score, moves, matches]; } class GameInitial extends GameState {} class GamePlaying extends GameState { final GameGrid grid; final DoneCallback done; final int score; final int moves; GamePlaying({ required this.grid, this.done = _defaultDoneCallback, this.score = 0, this.moves = 0, }); static void _defaultDoneCallback([FutureOr? _]) {} @override List get props => [grid, score, moves]; } class GamePlayingStart extends GamePlaying { GamePlayingStart({ required super.grid, super.done, super.score, super.moves, }); } class GamePlayingIdle extends GamePlaying { GamePlayingIdle({ required super.grid, super.done, super.score, super.moves, }); } class GamePlayingDrop extends GamePlaying { GamePlayingDrop({ required super.grid, super.done, super.score, super.moves, }); } class GamePlayingSwap extends GamePlaying { final Gem gem1, gem2; GamePlayingSwap( {required super.grid, super.score, super.moves, super.done, required this.gem1, required this.gem2}); @override List get props => [grid, score, moves, gem1, gem2]; } class GamePlayingNewGems extends GamePlaying { final List gems; GamePlayingNewGems( {required super.grid, super.score, super.moves, super.done, required this.gems}); @override List get props => [grid, score, moves, gems]; } class GamePlayingMatch extends GamePlaying { final List matches; final int combo; GamePlayingMatch({ required super.grid, super.score, super.moves, super.done, this.combo = 0, this.matches = const [], }); @override List get props => [grid, score, moves, matches]; } // Bloc class GameBloc extends Bloc { GameBloc() : super(GameInitial()) { on(_onStartGame); on(_onSwapGems); on(_onProcessMatches); } void _onStartGame(StartGame event, Emitter emit) async { final grid = GameGrid(); final done = Completer(); emit(GamePlayingStart(grid: grid, done: done.complete)); await done.future; final matches = MatchDetector.findMatches(grid); emit(_GamePlayingMatch(grid: grid, matches: matches)); add(ProcessMatches()); } void _onSwapGems(SwapGems event, Emitter emit) async { if (state is GamePlaying) { final currentState = state as GamePlaying; if (!MatchDetector.isValidSwap( currentState.grid, event.row1, event.col1, event.row2, event.col2)) { return; } final 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) { newGrid.setGem(event.row1, event.col1, gem2.copyWith(row: event.row1, col: event.col1)); newGrid.setGem(event.row2, event.col2, gem1.copyWith(row: event.row2, col: event.col2)); // Emit state for swap animation var done = Completer(); emit(GamePlayingSwap( grid: newGrid, score: currentState.score, moves: currentState.moves, gem1: gem1, gem2: gem2, done: done.complete, )); // Wait for swap animation await done.future; // Check for matches final matches = MatchDetector.findMatches(newGrid); if (matches.isNotEmpty) { emit(_GamePlayingMatch( grid: newGrid, score: currentState.score, moves: currentState.moves + 1, matches: matches, )); add(ProcessMatches()); } else { // Revert swap if no matches newGrid.setGem(event.row1, event.col1, gem1); newGrid.setGem(event.row2, event.col2, gem2); var done = Completer(); emit(GamePlayingSwap( grid: newGrid, score: currentState.score, moves: currentState.moves, gem1: gem2, gem2: gem1, done: done.complete, )); } // Wait for swap animation await done.future; } } } void _onProcessMatches(ProcessMatches event, Emitter emit) async { if (state is _GamePlayingMatch) { final currentState = state as _GamePlayingMatch; // Calculate score int newScore = currentState.score + currentState.matches.length * GameConstants.baseScore; if (event.combo > 0) { newScore += GameConstants.comboMultiplier * event.combo; } var newGrid = currentState.grid.clone(); // Mark matched gems and emit state for animation for (final gem in currentState.matches) { newGrid.setGem(gem.row, gem.col, null); } var done = Completer(); emit(GamePlayingMatch( grid: newGrid, score: newScore, moves: currentState.moves, matches: currentState.matches, combo: event.combo, done: done.complete, )); // Wait for match animations to complete await done.future; newGrid = newGrid.clone(); // Apply gravity GravitySystem.applyGravity(newGrid); // Emit state for drop columns done = Completer(); emit(GamePlayingDrop( grid: newGrid, score: newScore, moves: currentState.moves, done: done.complete, )); await done.future; newGrid = newGrid.clone(); // Apply gravity final newGems = GravitySystem.generateGems(newGrid); // Emit state for drop columns done = Completer(); emit(GamePlayingNewGems( grid: newGrid, score: newScore, moves: currentState.moves, gems: newGems, done: done.complete, )); // Wait for fall animations await done.future; newGrid = newGrid.clone(); // Check for new matches final newMatches = MatchDetector.findMatches(newGrid); if (newMatches.isNotEmpty) { emit(_GamePlayingMatch( grid: newGrid, score: newScore, moves: currentState.moves, matches: newMatches, )); add(ProcessMatches(combo: event.combo + 1)); } else { emit(GamePlayingIdle( grid: newGrid, score: newScore, moves: currentState.moves)); } } } }