match-three/lib/screens/game_screen.dart
savinmax 64053d1d58 Refactor game state management to use level-based architecture
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.
2025-09-21 17:26:51 +02:00

351 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flame/game.dart';
import 'package:match_three/game/models/gem.dart';
import '../game/match_three_game.dart';
import '../bloc/game_bloc.dart';
// ignore: must_be_immutable
class GameScreen extends StatefulWidget {
int? levelId;
GameScreen({super.key, this.levelId});
@override
State<GameScreen> createState() => _GameScreenState();
}
class _GameScreenState extends State<GameScreen> {
late MatchThreeGame game;
@override
void initState() {
super.initState();
game = MatchThreeGame(widget.levelId);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocListener<GameBloc, GameState>(
listener: (context, state) {
if (game.gridComponent == null) return;
if (state is GameLevelPlaying) {
game.gridComponent!.updateGrid(state);
} else if (state is GameLevelCompleted) {
_showLevelCompletedDialog(context, state);
} else if (state is GameLevelFailed) {
_showLevelFailedDialog(context, state);
}
},
child: BlocBuilder<GameBloc, GameState>(
builder: (context, state) {
// Set game bloc reference
game.setGameBloc(context.read<GameBloc>());
return Stack(
children: [
GameWidget<MatchThreeGame>.controlled(
gameFactory: () => game,
),
if (state is GameLevelPlaying)
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(
state.level.name,
style: const TextStyle(
color: Colors.amber,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Score: ${state.score}',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
if (state.level.constraints.hasScoreTarget)
Text(
'Target: ${state.level.constraints.targetScore}',
style: const TextStyle(
color: Colors.green,
fontSize: 14,
),
),
Text(
'Moves: ${state.moves}',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
if (state.level.constraints.hasMoveLimit)
Text(
'Limit: ${state.level.constraints.maxMoves}',
style: TextStyle(
color: state.moves >=
state.level.constraints.maxMoves!
? Colors.red
: Colors.orange,
fontSize: 14,
),
),
if (state.level.constraints.hasTimeLimit)
Text(
'Time: ${state.level.constraints.timeLimit! - state.timeElapsed}s',
style: TextStyle(
color: (state.level.constraints.timeLimit! -
state.timeElapsed) <=
10
? Colors.red
: Colors.cyan,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
if (state.level.objectives.hasGemTypeObjectives) ...[
const SizedBox(height: 4),
const Text(
'Objectives:',
style: TextStyle(
color: Colors.yellow,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
...state.level.objectives.clearGemTypes.entries
.map((entry) {
final gemType = Gem.getName(entry.key);
final required = entry.value;
final cleared = state.gemsCleared[entry.key] ?? 0;
return Text(
'Gem $gemType: $cleared/$required',
style: TextStyle(
color: cleared >= required
? Colors.green
: Colors.white,
fontSize: 12,
),
);
}),
],
if (state is GameLevelMatch)
Text(
'Combo x ${state.combo + 1}',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
],
),
),
),
Positioned(
top: 50,
right: 20,
child: IconButton(
onPressed: () => Navigator.pop(context),
icon:
const Icon(Icons.close, color: Colors.white, size: 30),
),
),
],
);
},
),
),
);
}
void _showLevelCompletedDialog(
BuildContext context, GameLevelCompleted state) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: const Text(
'Level Complete!',
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
state.level.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(3, (index) {
return Icon(
index < state.stars ? Icons.star : Icons.star_border,
color: index < state.stars ? Colors.amber : Colors.grey,
size: 32,
);
}),
),
const SizedBox(height: 16),
Text(
'Final Score: ${state.score}',
style: const TextStyle(fontSize: 16),
),
Text(
'Moves Used: ${state.moves}',
style: const TextStyle(fontSize: 16),
),
if (state.level.constraints.hasTimeLimit)
Text(
'Time: ${state.timeElapsed}s',
style: const TextStyle(fontSize: 16),
),
if (state.level.objectives.hasGemTypeObjectives) ...[
const SizedBox(height: 8),
const Text(
'Objectives Completed:',
style: TextStyle(fontWeight: FontWeight.bold),
),
...state.level.objectives.clearGemTypes.entries.map((entry) {
final gemType = entry.key;
final required = entry.value;
final cleared = state.gemsCleared[gemType] ?? 0;
return Text(
'Gem $gemType: $cleared/$required',
style: const TextStyle(color: Colors.green),
);
}),
],
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(); // Close dialog
Navigator.of(context).pop(); // Return to level selection
},
child: const Text('Continue'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(); // Close dialog
context.read<GameBloc>().add(StartLevel(state.level.id));
},
child: const Text('Replay'),
),
],
);
},
);
}
void _showLevelFailedDialog(BuildContext context, GameLevelFailed state) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: const Text(
'Level Failed',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
state.level.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
state.reason,
style: const TextStyle(
fontSize: 16,
color: Colors.red,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
'Final Score: ${state.score}',
style: const TextStyle(fontSize: 16),
),
Text(
'Moves Used: ${state.moves}',
style: const TextStyle(fontSize: 16),
),
if (state.level.constraints.hasTimeLimit)
Text(
'Time: ${state.timeElapsed}s',
style: const TextStyle(fontSize: 16),
),
if (state.level.objectives.hasGemTypeObjectives) ...[
const SizedBox(height: 8),
const Text(
'Objectives Progress:',
style: TextStyle(fontWeight: FontWeight.bold),
),
...state.level.objectives.clearGemTypes.entries.map((entry) {
final gemType = entry.key;
final required = entry.value;
final cleared = state.gemsCleared[gemType] ?? 0;
final completed = cleared >= required;
return Text(
'Gem $gemType: $cleared/$required ${completed ? "" : ""}',
style: TextStyle(
color: completed ? Colors.green : Colors.red,
),
);
}),
],
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(); // Close dialog
Navigator.of(context).pop(); // Return to level selection
},
child: const Text('Give Up'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(); // Close dialog
context.read<GameBloc>().add(StartLevel(state.level.id));
},
child: const Text('Try Again'),
),
],
);
},
);
}
}