first commit
This commit is contained in:
473
lib/screens/game_screen_flame.dart
Normal file
473
lib/screens/game_screen_flame.dart
Normal file
@@ -0,0 +1,473 @@
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../components/result_modal.dart';
|
||||
import '../flame/memory_match_flame_game.dart';
|
||||
import '../game/levels.dart';
|
||||
|
||||
class GameScreenFlame extends StatefulWidget {
|
||||
const GameScreenFlame({super.key, this.levelId = 1});
|
||||
|
||||
final int levelId;
|
||||
|
||||
@override
|
||||
State<GameScreenFlame> createState() => _GameScreenFlameState();
|
||||
}
|
||||
|
||||
class _GameScreenFlameState extends State<GameScreenFlame> {
|
||||
late int currentLevelId;
|
||||
late MemoryMatchFlameGame flameGame;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
currentLevelId = widget.levelId;
|
||||
flameGame = _buildGame(currentLevelId);
|
||||
}
|
||||
|
||||
MemoryMatchFlameGame _buildGame(int levelId) {
|
||||
final level = levels[(levelId - 1).clamp(0, levels.length - 1)];
|
||||
return MemoryMatchFlameGame(
|
||||
level: level,
|
||||
onStateChanged: () {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
onResult: (status, score) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _restartSameLevel() async {
|
||||
await flameGame.restartSameLevel();
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _goNext() async {
|
||||
if (currentLevelId >= levels.length) {
|
||||
await _restartSameLevel();
|
||||
return;
|
||||
}
|
||||
currentLevelId += 1;
|
||||
await flameGame.nextLevel(levels[currentLevelId - 1]);
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isReady = flameGame.isReady;
|
||||
final scoreText = isReady ? '${flameGame.controller.score}' : '0';
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
GameWidget(game: flameGame),
|
||||
SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width >= 768
|
||||
? 22
|
||||
: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
child: _TopHud(
|
||||
flameGame: flameGame,
|
||||
onPause: () {
|
||||
flameGame.togglePause();
|
||||
},
|
||||
onMusic: () {
|
||||
flameGame.toggleMusic();
|
||||
},
|
||||
onSfx: () {
|
||||
flameGame.toggleSfx();
|
||||
},
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Level ${flameGame.level.id}',
|
||||
style: const TextStyle(
|
||||
fontSize: 34,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color(0xFF56F0FF),
|
||||
letterSpacing: 1,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Color.fromRGBO(80, 255, 255, 0.55),
|
||||
blurRadius: 12,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width >= 768
|
||||
? 22
|
||||
: 18,
|
||||
),
|
||||
child: _BottomHud(
|
||||
flameGame: flameGame,
|
||||
scoreText: scoreText,
|
||||
onRetry: () {
|
||||
_restartSameLevel();
|
||||
},
|
||||
),
|
||||
),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
// child: Text(
|
||||
// 'Moves: $moves Mistakes: $mistakes${flameGame.level.maxMistakes != null ? ' / ${flameGame.level.maxMistakes}' : ''} Grid: ${flameGame.level.rowPattern.join('-')}',
|
||||
// style: const TextStyle(
|
||||
// color: Color.fromRGBO(255, 255, 255, 0.85),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (flameGame.pausedByUser)
|
||||
Positioned.fill(
|
||||
child: _PauseOverlay(
|
||||
onResume: () {
|
||||
flameGame.togglePause();
|
||||
},
|
||||
onRetry: () {
|
||||
_restartSameLevel();
|
||||
},
|
||||
onHome: () {
|
||||
Navigator.of(context).pushReplacementNamed('/start');
|
||||
},
|
||||
),
|
||||
),
|
||||
ResultModal(
|
||||
visible: flameGame.showResult,
|
||||
status: flameGame.resultStatus,
|
||||
score: flameGame.finalScore,
|
||||
bestScore: flameGame.bestScore,
|
||||
hasNext: currentLevelId + 1 <= levels.length,
|
||||
onRetry: _restartSameLevel,
|
||||
onNext: _goNext,
|
||||
showAddTime: flameGame.resultStatus == 'lost',
|
||||
addTimeSeconds: 30,
|
||||
onRequestRewardAddTime: (grant) async {
|
||||
await Future.delayed(const Duration(milliseconds: 600));
|
||||
final sec = await flameGame.reviveWithReward();
|
||||
grant(sec);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TopHud extends StatelessWidget {
|
||||
const _TopHud({
|
||||
required this.flameGame,
|
||||
required this.onPause,
|
||||
required this.onMusic,
|
||||
required this.onSfx,
|
||||
});
|
||||
|
||||
final MemoryMatchFlameGame flameGame;
|
||||
final VoidCallback onPause;
|
||||
final VoidCallback onMusic;
|
||||
final VoidCallback onSfx;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final compact = constraints.maxWidth < 380;
|
||||
final iconSize = compact ? 42.0 : 46.0;
|
||||
final itemGap = compact ? 6.0 : 10.0;
|
||||
final groupGap = compact ? 8.0 : 12.0;
|
||||
final topBadgeHeight = compact ? 52.0 : 58.0;
|
||||
final rightGroupWidth = iconSize * 2 + itemGap;
|
||||
final timerWidth =
|
||||
(constraints.maxWidth - iconSize - rightGroupWidth - groupGap * 2)
|
||||
.clamp(118.0, 176.0);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
_NeonIconButton(
|
||||
iconAsset: flameGame.pausedByUser
|
||||
? 'assets/images/0f3f8a4a21cdb16db2f080e4d7695cdc.png'
|
||||
: 'assets/images/e86bdd5d8908079d1ed4c06015f37eb0.png',
|
||||
onTap: onPause,
|
||||
size: iconSize,
|
||||
),
|
||||
SizedBox(width: groupGap),
|
||||
_HudValueBadge(
|
||||
backgroundAsset: 'assets/images/b5bf27b2555de44e3df2230080db5a1d.png',
|
||||
text: flameGame.timeText,
|
||||
width: timerWidth,
|
||||
height: topBadgeHeight,
|
||||
textLeftInset: compact ? 64 : 76,
|
||||
textRightInset: compact ? 12 : 16,
|
||||
textStyle: TextStyle(
|
||||
color: const Color(0xFF69F6FF),
|
||||
fontSize: compact ? 16 : 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: compact ? 0.5 : 1,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
_NeonIconButton(
|
||||
iconAsset: flameGame.mutedMusic
|
||||
? 'assets/images/5efe427a4e20b0ab12afe98c3cb50a60.png'
|
||||
: 'assets/images/5efe427a4e20b0ab12afe98c3cb50a60.png',
|
||||
active: !flameGame.mutedMusic,
|
||||
onTap: onMusic,
|
||||
size: iconSize,
|
||||
),
|
||||
SizedBox(width: itemGap),
|
||||
_NeonIconButton(
|
||||
iconAsset: flameGame.mutedSfx
|
||||
? 'assets/images/849ade26e1b6da2ed96d00db639a0985.png'
|
||||
: 'assets/images/1548af1c94ad45584324df8f08baf227.png',
|
||||
active: !flameGame.mutedSfx,
|
||||
onTap: onSfx,
|
||||
size: iconSize,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BottomHud extends StatelessWidget {
|
||||
const _BottomHud({
|
||||
required this.flameGame,
|
||||
required this.scoreText,
|
||||
required this.onRetry,
|
||||
});
|
||||
|
||||
final MemoryMatchFlameGame flameGame;
|
||||
final String scoreText;
|
||||
final VoidCallback onRetry;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_HudValueBadge(
|
||||
backgroundAsset: 'assets/images/5e36941b3d856737e81516acd45edc50.png',
|
||||
text: scoreText,
|
||||
width: 118,
|
||||
height: 44,
|
||||
textLeftInset: 54,
|
||||
textRightInset: 10,
|
||||
textStyle: const TextStyle(
|
||||
color: Color(0xFF56F0FF),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
_NeonIconButton(
|
||||
iconAsset: 'assets/images/161747ec4dc9f55f1760195593742232.png',
|
||||
onTap: onRetry,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HudValueBadge extends StatelessWidget {
|
||||
const _HudValueBadge({
|
||||
required this.backgroundAsset,
|
||||
required this.text,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.textLeftInset,
|
||||
required this.textRightInset,
|
||||
required this.textStyle,
|
||||
});
|
||||
|
||||
final String backgroundAsset;
|
||||
final String text;
|
||||
final double width;
|
||||
final double height;
|
||||
final double textLeftInset;
|
||||
final double textRightInset;
|
||||
final TextStyle textStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset(backgroundAsset, fit: BoxFit.fill),
|
||||
),
|
||||
Positioned.fill(
|
||||
left: textLeftInset,
|
||||
right: textRightInset,
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(text, maxLines: 1, style: textStyle),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NeonIconButton extends StatelessWidget {
|
||||
const _NeonIconButton({
|
||||
required this.iconAsset,
|
||||
required this.onTap,
|
||||
this.active = true,
|
||||
this.size = 46,
|
||||
});
|
||||
|
||||
final String iconAsset;
|
||||
final VoidCallback onTap;
|
||||
final bool active;
|
||||
final double size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: Center(
|
||||
child: Opacity(
|
||||
opacity: active ? 1 : 0.45,
|
||||
child: Image.asset(iconAsset, width: size, height: size),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PauseOverlay extends StatelessWidget {
|
||||
const _PauseOverlay({
|
||||
required this.onResume,
|
||||
required this.onRetry,
|
||||
required this.onHome,
|
||||
});
|
||||
|
||||
final VoidCallback onResume;
|
||||
final VoidCallback onRetry;
|
||||
final VoidCallback onHome;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: const Color.fromRGBO(0, 0, 0, 0.65),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width.clamp(0, 420).toDouble(),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color.fromRGBO(20, 24, 60, 0.96),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(
|
||||
color: const Color.fromRGBO(120, 255, 255, 0.25),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Paused',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_PauseButton(
|
||||
text: 'Resume',
|
||||
color: const Color.fromRGBO(50, 160, 255, 0.9),
|
||||
onTap: onResume,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_PauseButton(
|
||||
text: 'Retry',
|
||||
color: const Color.fromRGBO(255, 255, 255, 0.1),
|
||||
onTap: onRetry,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_PauseButton(
|
||||
text: 'Home',
|
||||
color: const Color.fromRGBO(255, 120, 120, 0.22),
|
||||
onTap: onHome,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PauseButton extends StatelessWidget {
|
||||
const _PauseButton({
|
||||
required this.text,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: color,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
side: const BorderSide(
|
||||
color: Color.fromRGBO(255, 255, 255, 0.22),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
onPressed: onTap,
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user