diff --git a/lib/bloc/game_bloc.dart b/lib/bloc/game_bloc.dart index 9958938..88b3ce1 100644 --- a/lib/bloc/game_bloc.dart +++ b/lib/bloc/game_bloc.dart @@ -16,8 +16,6 @@ abstract class GameEvent extends Equatable { 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); @@ -58,118 +56,8 @@ abstract class GameState extends Equatable { 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]; -} - // Level-based states class GameLevelPlaying extends GameState { final GameGrid grid; @@ -368,10 +256,8 @@ class GameBloc extends Bloc { final LevelService _levelService = LevelService.instance; GameBloc() : super(GameInitial()) { - on(_onStartGame); on(_onSwapGems); on(_onProcessMatches); - on(_onStartLevel); on(_onUpdateTimer); on(_onCompleteLevel); @@ -384,84 +270,9 @@ class GameBloc extends Bloc { return super.close(); } - 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 { - // Handle regular game states - 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; - } - } - // Handle level-based states - else if (state is GameLevelPlaying) { + // Handle only level-based states (including Free Play) + if (state is GameLevelPlaying) { final currentState = state as GameLevelPlaying; if (!MatchDetector.isValidSwap( @@ -555,87 +366,8 @@ class GameBloc extends Bloc { } void _onProcessMatches(ProcessMatches event, Emitter emit) async { - // Handle regular game matches - 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)); - } - } - // Handle level-based matches - else if (state is _GameLevelPlayingMatch) { + // Handle only level-based matches (including Free Play) + if (state is _GameLevelPlayingMatch) { final currentState = state as _GameLevelPlayingMatch; // Calculate score diff --git a/lib/game/components/grid_component.dart b/lib/game/components/grid_component.dart index 0315168..16e112c 100644 --- a/lib/game/components/grid_component.dart +++ b/lib/game/components/grid_component.dart @@ -19,11 +19,9 @@ class GridComponent extends PositionComponent @override Future onLoad() async { - if (levelId == null) { - game.gameBloc.add(StartGame()); - } else { - game.gameBloc.add(StartLevel(levelId!)); - } + // Always use StartLevel - Free Play is level ID 0 + final id = levelId ?? 0; // Default to Free Play if no level specified + game.gameBloc.add(StartLevel(id)); } void _createGemComponents() { @@ -61,21 +59,14 @@ class GridComponent extends PositionComponent } void updateGrid(GameState state) async { - // Work only with relevant events - if (state is! GamePlaying && state is! GameLevelPlaying) { + // Work only with level-based states (including Free Play) + if (state is! GameLevelPlaying) { return; } canInterract = false; print("Update event with state ${state.runtimeType.toString()}"); - // Handle regular game states - if (state is GamePlayingSwap) { - await _swapGems( - state.grid, - state.gem1, - state.gem2, - ); - } + // Handle level-based states if (state is GameLevelSwap) { await _swapGems( state.grid, @@ -83,43 +74,27 @@ class GridComponent extends PositionComponent state.gem2, ); } - if (state is GamePlayingMatch) { - await _animateMatch(state.grid, state.matches); - } if (state is GameLevelMatch) { await _animateMatch(state.grid, state.matches); } - if (state is GamePlayingDrop) { - await _animateDrop(state.grid); - } if (state is GameLevelDrop) { await _animateDrop(state.grid); } - if (state is GamePlayingNewGems) { - await _animateNewGemsFall(state.grid, state.gems); - } if (state is GameLevelNewGems) { await _animateNewGemsFall(state.grid, state.gems); } - if (state is GamePlayingIdle || state is GameLevelIdle) { + if (state is GameLevelIdle) { canInterract = true; } - if (state is GamePlaying) { - gameGrid = state.grid; - gameGrid.printGrid(); - _createGemComponents(); - await Future.delayed(const Duration(milliseconds: 100)); - print("Updated state ${state.runtimeType.toString()}"); - state.done(); - } else { - state = state as GameLevelPlaying; - gameGrid = state.grid; - gameGrid.printGrid(); - _createGemComponents(); - await Future.delayed(const Duration(milliseconds: 100)); - print("Updated state ${state.runtimeType.toString()}"); - state.done(); - } + + // Update grid and create components for all GameLevelPlaying states + final levelState = state as GameLevelPlaying; + gameGrid = levelState.grid; + gameGrid.printGrid(); + _createGemComponents(); + await Future.delayed(const Duration(milliseconds: 100)); + print("Updated state ${state.runtimeType.toString()}"); + levelState.done(); } GemComponent? _findGemComponent(Gem gem) { diff --git a/lib/screens/game_screen.dart b/lib/screens/game_screen.dart index eb48fb3..c47bcec 100644 --- a/lib/screens/game_screen.dart +++ b/lib/screens/game_screen.dart @@ -30,7 +30,7 @@ class _GameScreenState extends State { body: BlocListener( listener: (context, state) { if (game.gridComponent == null) return; - if (state is GameLevelPlaying || state is GamePlaying) { + if (state is GameLevelPlaying) { game.gridComponent!.updateGrid(state); } else if (state is GameLevelCompleted) { _showLevelCompletedDialog(context, state); @@ -48,52 +48,6 @@ class _GameScreenState extends State { GameWidget.controlled( gameFactory: () => game, ), - if (state is GamePlaying) - Positioned( - top: 50, - left: 20, - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Score: ${state.score}', - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - Text( - 'Moves: ${state.moves}', - style: const TextStyle( - color: Colors.white, - fontSize: 16, - ), - ), - Text( - 'Combo x ${state is GamePlayingMatch ? (state.combo + 1) : "-"}', - style: const TextStyle( - color: Colors.white, - fontSize: 16, - ), - ), - Text( - 'Last state: ${state.runtimeType}', - style: const TextStyle( - color: Colors.blue, - fontSize: 16, - ), - ), - ], - ), - ), - ), if (state is GameLevelPlaying) Positioned( top: 50, diff --git a/lib/screens/menu_screen.dart b/lib/screens/menu_screen.dart index 29e7392..f4a43b8 100644 --- a/lib/screens/menu_screen.dart +++ b/lib/screens/menu_screen.dart @@ -53,9 +53,16 @@ class MenuScreen extends StatelessWidget { const SizedBox(height: 20), ElevatedButton( onPressed: () { + // Start Free Play level (ID: 0) + context.read().add(StartLevel(0)); Navigator.push( context, - MaterialPageRoute(builder: (context) => GameScreen()), + MaterialPageRoute( + builder: (context) => BlocProvider.value( + value: context.read(), + child: GameScreen(), + ), + ), ); }, style: ElevatedButton.styleFrom( diff --git a/lib/services/level_service.dart b/lib/services/level_service.dart index 055e995..dc525f2 100644 --- a/lib/services/level_service.dart +++ b/lib/services/level_service.dart @@ -6,6 +6,7 @@ import '../game/models/level.dart'; class LevelService { static const String _progressKey = 'level_progress'; + static const int freePlayLevelId = 0; // Special ID for Free Play static LevelService? _instance; static LevelService get instance => _instance ??= LevelService._(); @@ -14,6 +15,25 @@ class LevelService { List? _levels; Map? _progress; + /// Get the hardcoded Free Play level + Level get freePlayLevel => const Level( + id: freePlayLevelId, + name: 'Free Play', + description: 'Play without constraints - match gems and score points!', + constraints: LevelConstraints(), // No constraints + objectives: LevelObjectives(), // No objectives + starRating: StarRating( + criteria: StarCriteria.SCORE, + thresholds: StarThresholds( + oneStar: 1000, + twoStar: 5000, + threeStar: 10000, + ), + ), + availableGemTypes: [0, 1, 2, 3, 4, 5], // All gem types available + unlocked: true, // Always unlocked + ); + /// Load all levels from JSON configuration Future> loadLevels() async { if (_levels != null) return _levels!; @@ -35,6 +55,11 @@ class LevelService { /// Get a specific level by ID Future getLevel(int levelId) async { + // Return Free Play level for ID 0 + if (levelId == freePlayLevelId) { + return freePlayLevel; + } + final levels = await loadLevels(); try { return levels.firstWhere((level) => level.id == levelId); @@ -181,7 +206,7 @@ class LevelService { if (!objectives.hasGemTypeObjectives) { return true; } - + for (final entry in objectives.clearGemTypes.entries) { final requiredCount = entry.value; final clearedCount = gemsCleared[entry.key] ?? 0; @@ -196,8 +221,9 @@ class LevelService { bool isLevelCompleted(Level level, int score, int moves, int timeUsed, Map gemsCleared) { // Check score constraint - if (level.constraints.hasScoreTarget && - score < level.constraints.targetScore!) { + if (level.id == 0 || + (level.constraints.hasScoreTarget && + score < level.constraints.targetScore!)) { return false; }