match-three/lib/game/components/grid_component.dart
savinmax ea3f0c4e18 Add level system with progression and selection screen
- 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
2025-09-21 17:08:44 +02:00

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);
}
}
}