320 lines
7.7 KiB
Dart
320 lines
7.7 KiB
Dart
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<Object> 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<Object> get props => [row1, col1, row2, col2];
|
|
}
|
|
|
|
class ProcessMatches extends GameEvent {
|
|
final int combo;
|
|
ProcessMatches({this.combo = 0});
|
|
@override
|
|
List<Object> get props => [combo];
|
|
}
|
|
|
|
// States
|
|
typedef DoneCallback = void Function([FutureOr<dynamic>?]);
|
|
|
|
abstract class GameState extends Equatable {
|
|
@override
|
|
List<Object> get props => [];
|
|
}
|
|
|
|
// Internal match state event
|
|
class _GamePlayingMatch extends GameState {
|
|
final GameGrid grid;
|
|
final int score;
|
|
final int moves;
|
|
final List<Gem> matches;
|
|
|
|
_GamePlayingMatch({
|
|
required this.grid,
|
|
this.score = 0,
|
|
this.moves = 0,
|
|
this.matches = const [],
|
|
});
|
|
|
|
@override
|
|
List<Object> 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<dynamic>? _]) {}
|
|
|
|
@override
|
|
List<Object> 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<Object> get props => [grid, score, moves, gem1, gem2];
|
|
}
|
|
|
|
class GamePlayingNewGems extends GamePlaying {
|
|
final List<Gem> gems;
|
|
|
|
GamePlayingNewGems(
|
|
{required super.grid,
|
|
super.score,
|
|
super.moves,
|
|
super.done,
|
|
required this.gems});
|
|
|
|
@override
|
|
List<Object> get props => [grid, score, moves, gems];
|
|
}
|
|
|
|
class GamePlayingMatch extends GamePlaying {
|
|
final List<Gem> matches;
|
|
final int combo;
|
|
|
|
GamePlayingMatch({
|
|
required super.grid,
|
|
super.score,
|
|
super.moves,
|
|
super.done,
|
|
this.combo = 0,
|
|
this.matches = const [],
|
|
});
|
|
|
|
@override
|
|
List<Object> get props => [grid, score, moves, matches];
|
|
}
|
|
|
|
// Bloc
|
|
class GameBloc extends Bloc<GameEvent, GameState> {
|
|
GameBloc() : super(GameInitial()) {
|
|
on<StartGame>(_onStartGame);
|
|
on<SwapGems>(_onSwapGems);
|
|
on<ProcessMatches>(_onProcessMatches);
|
|
}
|
|
|
|
void _onStartGame(StartGame event, Emitter<GameState> 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<GameState> 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<GameState> 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));
|
|
}
|
|
}
|
|
}
|
|
}
|