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.
351 lines
13 KiB
Dart
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'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|