Remove legacy GamePlaying states and consolidate all gameplay logic to use the GameLevelPlaying state system. This simplifies state management by eliminating duplicate code paths and ensures consistent behavior across all game modes including Free Play.
680 lines
18 KiB
Dart
680 lines
18 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:equatable/equatable.dart';
|
|
import '../game/models/grid.dart';
|
|
import '../game/models/gem.dart';
|
|
import '../game/models/level.dart';
|
|
import '../game/systems/match_detector.dart';
|
|
import '../game/systems/gravity_system.dart';
|
|
import '../services/level_service.dart';
|
|
import '../utils/constants.dart';
|
|
|
|
// Events
|
|
abstract class GameEvent extends Equatable {
|
|
@override
|
|
List<Object> get props => [];
|
|
}
|
|
|
|
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];
|
|
}
|
|
|
|
class StartLevel extends GameEvent {
|
|
final int levelId;
|
|
StartLevel(this.levelId);
|
|
@override
|
|
List<Object> get props => [levelId];
|
|
}
|
|
|
|
class UpdateTimer extends GameEvent {
|
|
final int timeElapsed;
|
|
UpdateTimer(this.timeElapsed);
|
|
@override
|
|
List<Object> get props => [timeElapsed];
|
|
}
|
|
|
|
class CompleteLevel extends GameEvent {}
|
|
|
|
class FailLevel extends GameEvent {}
|
|
|
|
// States
|
|
typedef DoneCallback = void Function([FutureOr<dynamic>?]);
|
|
|
|
abstract class GameState extends Equatable {
|
|
@override
|
|
List<Object> get props => [];
|
|
}
|
|
|
|
class GameInitial extends GameState {}
|
|
|
|
// Level-based states
|
|
class GameLevelPlaying extends GameState {
|
|
final GameGrid grid;
|
|
final DoneCallback done;
|
|
final int score;
|
|
final int moves;
|
|
final Level level;
|
|
final int timeElapsed;
|
|
final Map<int, int> gemsCleared;
|
|
|
|
GameLevelPlaying({
|
|
required this.grid,
|
|
required this.level,
|
|
this.done = _defaultDoneCallback,
|
|
this.score = 0,
|
|
this.moves = 0,
|
|
this.timeElapsed = 0,
|
|
this.gemsCleared = const {},
|
|
});
|
|
|
|
static void _defaultDoneCallback([FutureOr<dynamic>? _]) {}
|
|
|
|
@override
|
|
List<Object> get props =>
|
|
[grid, score, moves, level, timeElapsed, gemsCleared];
|
|
}
|
|
|
|
class GameLevelStart extends GameLevelPlaying {
|
|
GameLevelStart({
|
|
required super.grid,
|
|
required super.level,
|
|
super.done,
|
|
super.score,
|
|
super.moves,
|
|
super.timeElapsed,
|
|
super.gemsCleared,
|
|
});
|
|
}
|
|
|
|
class GameLevelIdle extends GameLevelPlaying {
|
|
GameLevelIdle({
|
|
required super.grid,
|
|
required super.level,
|
|
super.done,
|
|
super.score,
|
|
super.moves,
|
|
super.timeElapsed,
|
|
super.gemsCleared,
|
|
});
|
|
}
|
|
|
|
class GameLevelSwap extends GameLevelPlaying {
|
|
final Gem gem1, gem2;
|
|
|
|
GameLevelSwap({
|
|
required super.grid,
|
|
required super.level,
|
|
super.score,
|
|
super.moves,
|
|
super.done,
|
|
super.timeElapsed,
|
|
super.gemsCleared,
|
|
required this.gem1,
|
|
required this.gem2,
|
|
});
|
|
|
|
@override
|
|
List<Object> get props =>
|
|
[grid, score, moves, level, timeElapsed, gemsCleared, gem1, gem2];
|
|
}
|
|
|
|
class GameLevelMatch extends GameLevelPlaying {
|
|
final List<Gem> matches;
|
|
final int combo;
|
|
|
|
GameLevelMatch({
|
|
required super.grid,
|
|
required super.level,
|
|
super.score,
|
|
super.moves,
|
|
super.done,
|
|
super.timeElapsed,
|
|
super.gemsCleared,
|
|
this.combo = 0,
|
|
this.matches = const [],
|
|
});
|
|
|
|
@override
|
|
List<Object> get props =>
|
|
[grid, score, moves, level, timeElapsed, gemsCleared, matches, combo];
|
|
}
|
|
|
|
class GameLevelDrop extends GameLevelPlaying {
|
|
GameLevelDrop({
|
|
required super.grid,
|
|
required super.level,
|
|
super.done,
|
|
super.score,
|
|
super.moves,
|
|
super.timeElapsed,
|
|
super.gemsCleared,
|
|
});
|
|
}
|
|
|
|
class GameLevelNewGems extends GameLevelPlaying {
|
|
final List<Gem> gems;
|
|
|
|
GameLevelNewGems({
|
|
required super.grid,
|
|
required super.level,
|
|
super.score,
|
|
super.moves,
|
|
super.done,
|
|
super.timeElapsed,
|
|
super.gemsCleared,
|
|
required this.gems,
|
|
});
|
|
|
|
@override
|
|
List<Object> get props =>
|
|
[grid, score, moves, level, timeElapsed, gemsCleared, gems];
|
|
}
|
|
|
|
class GameLevelCompleted extends GameState {
|
|
final Level level;
|
|
final int score;
|
|
final int moves;
|
|
final int timeElapsed;
|
|
final int stars;
|
|
final Map<int, int> gemsCleared;
|
|
|
|
GameLevelCompleted({
|
|
required this.level,
|
|
required this.score,
|
|
required this.moves,
|
|
required this.timeElapsed,
|
|
required this.stars,
|
|
required this.gemsCleared,
|
|
});
|
|
|
|
@override
|
|
List<Object> get props =>
|
|
[level, score, moves, timeElapsed, stars, gemsCleared];
|
|
}
|
|
|
|
class GameLevelFailed extends GameState {
|
|
final Level level;
|
|
final int score;
|
|
final int moves;
|
|
final int timeElapsed;
|
|
final Map<int, int> gemsCleared;
|
|
final String reason;
|
|
|
|
GameLevelFailed({
|
|
required this.level,
|
|
required this.score,
|
|
required this.moves,
|
|
required this.timeElapsed,
|
|
required this.gemsCleared,
|
|
required this.reason,
|
|
});
|
|
|
|
@override
|
|
List<Object> get props =>
|
|
[level, score, moves, timeElapsed, gemsCleared, reason];
|
|
}
|
|
|
|
// Internal level match state
|
|
class _GameLevelPlayingMatch extends GameState {
|
|
final GameGrid grid;
|
|
final Level level;
|
|
final int score;
|
|
final int moves;
|
|
final int timeElapsed;
|
|
final Map<int, int> gemsCleared;
|
|
final List<Gem> matches;
|
|
|
|
_GameLevelPlayingMatch({
|
|
required this.grid,
|
|
required this.level,
|
|
this.score = 0,
|
|
this.moves = 0,
|
|
this.timeElapsed = 0,
|
|
this.gemsCleared = const {},
|
|
this.matches = const [],
|
|
});
|
|
|
|
@override
|
|
List<Object> get props =>
|
|
[grid, level, score, moves, timeElapsed, gemsCleared, matches];
|
|
}
|
|
|
|
// Bloc
|
|
class GameBloc extends Bloc<GameEvent, GameState> {
|
|
Timer? _gameTimer;
|
|
final LevelService _levelService = LevelService.instance;
|
|
|
|
GameBloc() : super(GameInitial()) {
|
|
on<SwapGems>(_onSwapGems);
|
|
on<ProcessMatches>(_onProcessMatches);
|
|
on<StartLevel>(_onStartLevel);
|
|
on<UpdateTimer>(_onUpdateTimer);
|
|
on<CompleteLevel>(_onCompleteLevel);
|
|
on<FailLevel>(_onFailLevel);
|
|
}
|
|
|
|
@override
|
|
Future<void> close() {
|
|
_gameTimer?.cancel();
|
|
return super.close();
|
|
}
|
|
|
|
void _onSwapGems(SwapGems event, Emitter<GameState> emit) async {
|
|
// Handle only level-based states (including Free Play)
|
|
if (state is GameLevelPlaying) {
|
|
final currentState = state as GameLevelPlaying;
|
|
|
|
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(GameLevelSwap(
|
|
grid: newGrid,
|
|
level: currentState.level,
|
|
score: currentState.score,
|
|
moves: currentState.moves,
|
|
timeElapsed: currentState.timeElapsed,
|
|
gemsCleared: currentState.gemsCleared,
|
|
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) {
|
|
final newMoves = currentState.moves + 1;
|
|
|
|
// Check if move limit exceeded
|
|
if (currentState.level.constraints.hasMoveLimit &&
|
|
newMoves >= currentState.level.constraints.maxMoves!) {
|
|
// This is the last move, check if level can be completed
|
|
final updatedGemsCleared =
|
|
_updateGemsCleared(currentState.gemsCleared, matches);
|
|
if (!_levelService.isLevelCompleted(
|
|
currentState.level,
|
|
currentState.score,
|
|
newMoves,
|
|
currentState.timeElapsed,
|
|
updatedGemsCleared,
|
|
)) {
|
|
add(FailLevel());
|
|
return;
|
|
}
|
|
}
|
|
|
|
emit(_GameLevelPlayingMatch(
|
|
grid: newGrid,
|
|
level: currentState.level,
|
|
score: currentState.score,
|
|
moves: newMoves,
|
|
timeElapsed: currentState.timeElapsed,
|
|
gemsCleared: currentState.gemsCleared,
|
|
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(GameLevelSwap(
|
|
grid: newGrid,
|
|
level: currentState.level,
|
|
score: currentState.score,
|
|
moves: currentState.moves,
|
|
timeElapsed: currentState.timeElapsed,
|
|
gemsCleared: currentState.gemsCleared,
|
|
gem1: gem2,
|
|
gem2: gem1,
|
|
done: done.complete,
|
|
));
|
|
}
|
|
// Wait for swap animation
|
|
await done.future;
|
|
}
|
|
}
|
|
}
|
|
|
|
void _onProcessMatches(ProcessMatches event, Emitter<GameState> emit) async {
|
|
// Handle only level-based matches (including Free Play)
|
|
if (state is _GameLevelPlayingMatch) {
|
|
final currentState = state as _GameLevelPlayingMatch;
|
|
|
|
// Calculate score
|
|
int newScore = currentState.score +
|
|
currentState.matches.length * GameConstants.baseScore;
|
|
|
|
if (event.combo > 0) {
|
|
newScore += GameConstants.comboMultiplier * event.combo;
|
|
}
|
|
|
|
// Update gems cleared count
|
|
final updatedGemsCleared =
|
|
_updateGemsCleared(currentState.gemsCleared, currentState.matches);
|
|
|
|
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(GameLevelMatch(
|
|
grid: newGrid,
|
|
level: currentState.level,
|
|
score: newScore,
|
|
moves: currentState.moves,
|
|
timeElapsed: currentState.timeElapsed,
|
|
gemsCleared: updatedGemsCleared,
|
|
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(GameLevelDrop(
|
|
grid: newGrid,
|
|
level: currentState.level,
|
|
score: newScore,
|
|
moves: currentState.moves,
|
|
timeElapsed: currentState.timeElapsed,
|
|
gemsCleared: updatedGemsCleared,
|
|
done: done.complete,
|
|
));
|
|
await done.future;
|
|
newGrid = newGrid.clone();
|
|
|
|
// Apply gravity
|
|
final newGems = GravitySystem.generateGems(newGrid);
|
|
|
|
// Emit state for drop columns
|
|
done = Completer();
|
|
emit(GameLevelNewGems(
|
|
grid: newGrid,
|
|
level: currentState.level,
|
|
score: newScore,
|
|
moves: currentState.moves,
|
|
timeElapsed: currentState.timeElapsed,
|
|
gemsCleared: updatedGemsCleared,
|
|
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(_GameLevelPlayingMatch(
|
|
grid: newGrid,
|
|
level: currentState.level,
|
|
score: newScore,
|
|
moves: currentState.moves,
|
|
timeElapsed: currentState.timeElapsed,
|
|
gemsCleared: updatedGemsCleared,
|
|
matches: newMatches,
|
|
));
|
|
add(ProcessMatches(combo: event.combo + 1));
|
|
} else {
|
|
final finalState = GameLevelIdle(
|
|
grid: newGrid,
|
|
level: currentState.level,
|
|
score: newScore,
|
|
moves: currentState.moves,
|
|
timeElapsed: currentState.timeElapsed,
|
|
gemsCleared: updatedGemsCleared,
|
|
);
|
|
|
|
emit(finalState);
|
|
|
|
// Check level completion/failure after processing matches
|
|
_checkLevelStatus(finalState);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _onStartLevel(StartLevel event, Emitter<GameState> emit) async {
|
|
try {
|
|
final level = await _levelService.getLevel(event.levelId);
|
|
if (level == null) {
|
|
emit(GameLevelFailed(
|
|
level: Level(
|
|
id: event.levelId,
|
|
name: 'Unknown Level',
|
|
description: '',
|
|
constraints: const LevelConstraints(),
|
|
objectives: const LevelObjectives(),
|
|
starRating: const StarRating(
|
|
criteria: StarCriteria.SCORE,
|
|
thresholds: StarThresholds(oneStar: 0, twoStar: 0, threeStar: 0),
|
|
),
|
|
availableGemTypes: [],
|
|
),
|
|
score: 0,
|
|
moves: 0,
|
|
timeElapsed: 0,
|
|
gemsCleared: {},
|
|
reason: 'Level not found',
|
|
));
|
|
return;
|
|
}
|
|
|
|
final grid = GameGrid();
|
|
|
|
// Start timer if level has time limit
|
|
if (level.constraints.hasTimeLimit) {
|
|
_gameTimer?.cancel();
|
|
_gameTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
|
add(UpdateTimer(timer.tick));
|
|
});
|
|
}
|
|
|
|
final done = Completer();
|
|
emit(GameLevelStart(
|
|
grid: grid,
|
|
level: level,
|
|
done: done.complete,
|
|
));
|
|
await done.future;
|
|
|
|
final matches = MatchDetector.findMatches(grid);
|
|
emit(_GameLevelPlayingMatch(
|
|
grid: grid,
|
|
level: level,
|
|
matches: matches,
|
|
));
|
|
add(ProcessMatches());
|
|
} catch (e) {
|
|
emit(GameLevelFailed(
|
|
level: Level(
|
|
id: event.levelId,
|
|
name: 'Error',
|
|
description: '',
|
|
constraints: const LevelConstraints(),
|
|
objectives: const LevelObjectives(),
|
|
starRating: const StarRating(
|
|
criteria: StarCriteria.SCORE,
|
|
thresholds: StarThresholds(oneStar: 0, twoStar: 0, threeStar: 0),
|
|
),
|
|
availableGemTypes: [],
|
|
),
|
|
score: 0,
|
|
moves: 0,
|
|
timeElapsed: 0,
|
|
gemsCleared: {},
|
|
reason: 'Failed to load level: $e',
|
|
));
|
|
}
|
|
}
|
|
|
|
void _onUpdateTimer(UpdateTimer event, Emitter<GameState> emit) {
|
|
if (state is GameLevelPlaying) {
|
|
final currentState = state as GameLevelPlaying;
|
|
|
|
// Check if time limit exceeded
|
|
if (currentState.level.constraints.hasTimeLimit &&
|
|
event.timeElapsed >= currentState.level.constraints.timeLimit!) {
|
|
_gameTimer?.cancel();
|
|
add(FailLevel());
|
|
return;
|
|
}
|
|
|
|
// Update the current state with new time
|
|
if (currentState is GameLevelIdle) {
|
|
emit(GameLevelIdle(
|
|
grid: currentState.grid,
|
|
level: currentState.level,
|
|
score: currentState.score,
|
|
moves: currentState.moves,
|
|
timeElapsed: event.timeElapsed,
|
|
gemsCleared: currentState.gemsCleared,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
void _onCompleteLevel(CompleteLevel event, Emitter<GameState> emit) async {
|
|
if (state is GameLevelPlaying) {
|
|
final currentState = state as GameLevelPlaying;
|
|
_gameTimer?.cancel();
|
|
|
|
// Calculate stars
|
|
final stars = _levelService.calculateStars(
|
|
currentState.level,
|
|
currentState.score,
|
|
currentState.moves,
|
|
currentState.timeElapsed,
|
|
currentState.gemsCleared,
|
|
);
|
|
|
|
// Update progress
|
|
final currentProgress =
|
|
await _levelService.getLevelProgress(currentState.level.id);
|
|
final newProgress = currentProgress.copyWith(
|
|
completed: true,
|
|
stars: stars > currentProgress.stars ? stars : currentProgress.stars,
|
|
bestScore: currentState.score > currentProgress.bestScore
|
|
? currentState.score
|
|
: currentProgress.bestScore,
|
|
);
|
|
|
|
await _levelService.updateLevelProgress(newProgress);
|
|
|
|
emit(GameLevelCompleted(
|
|
level: currentState.level,
|
|
score: currentState.score,
|
|
moves: currentState.moves,
|
|
timeElapsed: currentState.timeElapsed,
|
|
stars: stars,
|
|
gemsCleared: currentState.gemsCleared,
|
|
));
|
|
}
|
|
}
|
|
|
|
void _onFailLevel(FailLevel event, Emitter<GameState> emit) {
|
|
if (state is GameLevelPlaying) {
|
|
final currentState = state as GameLevelPlaying;
|
|
_gameTimer?.cancel();
|
|
|
|
String reason = 'Level failed';
|
|
if (currentState.level.constraints.hasMoveLimit &&
|
|
currentState.moves >= currentState.level.constraints.maxMoves!) {
|
|
reason = 'No moves remaining';
|
|
} else if (currentState.level.constraints.hasTimeLimit &&
|
|
currentState.timeElapsed >=
|
|
currentState.level.constraints.timeLimit!) {
|
|
reason = 'Time limit exceeded';
|
|
}
|
|
|
|
emit(GameLevelFailed(
|
|
level: currentState.level,
|
|
score: currentState.score,
|
|
moves: currentState.moves,
|
|
timeElapsed: currentState.timeElapsed,
|
|
gemsCleared: currentState.gemsCleared,
|
|
reason: reason,
|
|
));
|
|
}
|
|
}
|
|
|
|
// Helper method to update gems cleared count
|
|
Map<int, int> _updateGemsCleared(Map<int, int> current, List<Gem> matches) {
|
|
final updated = Map<int, int>.from(current);
|
|
for (final gem in matches) {
|
|
updated[gem.type] = (updated[gem.type] ?? 0) + 1;
|
|
}
|
|
return updated;
|
|
}
|
|
|
|
// Helper method to check level completion/failure
|
|
void _checkLevelStatus(GameLevelPlaying currentState) {
|
|
final level = currentState.level;
|
|
|
|
// Check if level is completed
|
|
if (_levelService.isLevelCompleted(
|
|
level,
|
|
currentState.score,
|
|
currentState.moves,
|
|
currentState.timeElapsed,
|
|
currentState.gemsCleared,
|
|
)) {
|
|
add(CompleteLevel());
|
|
return;
|
|
}
|
|
|
|
// Check if level is failed
|
|
if (_levelService.isLevelFailed(
|
|
level,
|
|
currentState.score,
|
|
currentState.moves,
|
|
currentState.timeElapsed,
|
|
currentState.gemsCleared,
|
|
)) {
|
|
add(FailLevel());
|
|
return;
|
|
}
|
|
}
|
|
}
|