- Add Level model with constraints, objectives, and star ratings - Create LevelService for loading levels from JSON configuration - Implement LevelSelectionScreen with visual progress tracking - Update GameBloc to handle level-based gameplay - Add 10 predefined levels with varying difficulty and objectives - Integrate level progression system into game flow
219 lines
6.4 KiB
Dart
219 lines
6.4 KiB
Dart
import 'package:flame/components.dart';
|
|
import 'package:match_three/bloc/game_bloc.dart';
|
|
import 'package:match_three/game/systems/physics_system.dart';
|
|
import '../models/grid.dart';
|
|
import '../models/gem.dart';
|
|
import '../match_three_game.dart';
|
|
import 'gem_component.dart';
|
|
import '../../utils/constants.dart';
|
|
|
|
class GridComponent extends PositionComponent
|
|
with HasGameReference<MatchThreeGame> {
|
|
late GameGrid gameGrid;
|
|
final List<List<GemComponent?>> gemComponents = [];
|
|
final MatchThreeGame game;
|
|
int? levelId;
|
|
bool canInterract = false;
|
|
|
|
GridComponent(this.game, this.levelId) : super();
|
|
|
|
@override
|
|
Future<void> onLoad() async {
|
|
if (levelId == null) {
|
|
game.gameBloc.add(StartGame());
|
|
} else {
|
|
game.gameBloc.add(StartLevel(levelId!));
|
|
}
|
|
}
|
|
|
|
void _createGemComponents() {
|
|
// Clear existing components
|
|
for (final row in gemComponents) {
|
|
for (final gemComponent in row) {
|
|
gemComponent?.removeFromParent();
|
|
}
|
|
row.clear();
|
|
}
|
|
gemComponents.clear();
|
|
for (final child in children) {
|
|
if (child is GemComponent) {
|
|
child.removeFromParent();
|
|
}
|
|
}
|
|
|
|
for (int row = 0; row < GameConstants.gridHeight; row++) {
|
|
gemComponents.add(<GemComponent?>[]);
|
|
for (int col = 0; col < GameConstants.gridWidth; col++) {
|
|
final gem = gameGrid.getGem(row, col);
|
|
if (gem != null) {
|
|
final gemComponent = GemComponent(gem: gem, gridComponent: this);
|
|
gemComponent.position = Vector2(
|
|
col * GameConstants.gemSize + GameConstants.gridPadding,
|
|
row * GameConstants.gemSize + GameConstants.gridPadding,
|
|
);
|
|
gemComponents[row].add(gemComponent);
|
|
add(gemComponent);
|
|
} else {
|
|
gemComponents[row].add(null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void updateGrid(GameState state) async {
|
|
// Work only with relevant events
|
|
if (state is! GamePlaying && 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,
|
|
);
|
|
}
|
|
if (state is GameLevelSwap) {
|
|
await _swapGems(
|
|
state.grid,
|
|
state.gem1,
|
|
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) {
|
|
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();
|
|
}
|
|
}
|
|
|
|
GemComponent? _findGemComponent(Gem gem) {
|
|
return gemComponents[gem.row][gem.col];
|
|
}
|
|
|
|
_swapGems(GameGrid newGrid, Gem gem1, Gem gem2) async {
|
|
final gemComp1 = _findGemComponent(gem1);
|
|
final gemComp2 = _findGemComponent(gem2);
|
|
|
|
if (gemComp1 == null || gemComp2 == null) {
|
|
print("!! Gem not found");
|
|
return;
|
|
}
|
|
|
|
await PhysicsSystem.animateSwap(gemComp1, gemComp2);
|
|
}
|
|
|
|
_animateMatch(GameGrid newGrid, List<Gem> matches, [int combo = 0]) async {
|
|
final effects = <Future>[];
|
|
final toRemove = <GemComponent>[];
|
|
|
|
// Animate explosion
|
|
for (final gem in matches) {
|
|
final gemComponent = _findGemComponent(gem);
|
|
if (gemComponent != null) {
|
|
toRemove.add(gemComponent);
|
|
effects.add(gemComponent.animateMatch());
|
|
if (combo > 0) {
|
|
gemComponent.animateCombo();
|
|
}
|
|
}
|
|
}
|
|
await Future.wait(effects);
|
|
for (final gemComponent in toRemove) {
|
|
gemComponent.removeFromParent();
|
|
}
|
|
}
|
|
|
|
_animateDrop(GameGrid newGrid) async {
|
|
final effects = <Future>[];
|
|
|
|
for (int col = GameConstants.gridWidth - 1; col >= 0; col--) {
|
|
for (int row = GameConstants.gridHeight - 1; row >= 0; row--) {
|
|
final gem = gameGrid.getGem(row, col);
|
|
|
|
// if gem is removed - animate fall effect from top
|
|
if (gem == null) {
|
|
int fallToRow = row;
|
|
while (row >= 0) {
|
|
final gemAbove = gameGrid.getGem(row, col);
|
|
final gemComponent = gemComponents[row][col];
|
|
if (gemAbove != null) {
|
|
final targetPosition = Vector2(
|
|
col * GameConstants.gemSize + GameConstants.gridPadding,
|
|
fallToRow * GameConstants.gemSize + GameConstants.gridPadding,
|
|
);
|
|
print(
|
|
"@@ Fall $row, $col -> $fallToRow, $col ${gemComponent == null ? "NULL" : "OK"}");
|
|
effects.add(gemComponent?.animateFall(targetPosition));
|
|
fallToRow--;
|
|
}
|
|
row--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
await Future.wait(effects);
|
|
}
|
|
|
|
_animateNewGemsFall(GameGrid newGrid, List<Gem> newGems) async {
|
|
final effects = <Future>[];
|
|
|
|
for (final gem in newGems) {
|
|
// Create gem component from gem and animate falling effect to proper location
|
|
final gemComponent = GemComponent(gem: gem, gridComponent: this);
|
|
final targetPosition = Vector2(
|
|
gem.col * GameConstants.gemSize + GameConstants.gridPadding,
|
|
gem.row * GameConstants.gemSize + GameConstants.gridPadding,
|
|
);
|
|
|
|
gemComponent.position.x = targetPosition.x;
|
|
gemComponent.position.y = -GameConstants.gemSize;
|
|
add(gemComponent);
|
|
effects.add(gemComponent.animateFall(targetPosition));
|
|
}
|
|
|
|
await Future.wait(effects);
|
|
}
|
|
|
|
void onGemTapped(Gem gem) {
|
|
if (canInterract) {
|
|
game.selectGem(gem);
|
|
}
|
|
}
|
|
}
|