Files
flame_flip/lib/screens/game_screen_flame.dart

474 lines
13 KiB
Dart
Raw Permalink Normal View History

2026-06-10 18:23:39 +08:00
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,
),
),
),
);
}
}