import 'dart:async'; import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; import 'package:platform_helper/platform_helper.dart'; import 'package:share_repository/share_repository.dart'; class PinballGame extends PinballForge2DGame with HasKeyboardHandlerComponents, MultiTouchTapDetector, HasTappables { PinballGame({ required CharacterThemeCubit characterThemeBloc, required this.leaderboardRepository, required this.shareRepository, required GameBloc gameBloc, required AppLocalizations l10n, required PinballAudioPlayer audioPlayer, required this.platformHelper, }) : focusNode = FocusNode(), _gameBloc = gameBloc, _audioPlayer = audioPlayer, _characterThemeBloc = characterThemeBloc, _l10n = l10n, super( gravity: Vector2(0, 30), ) { images.prefix = ''; } /// Identifier of the play button overlay. static const playButtonOverlay = 'play_button'; /// Identifier of the replay button overlay. static const replayButtonOverlay = 'replay_button'; /// Identifier of the mobile controls overlay. static const mobileControlsOverlay = 'mobile_controls'; @override Color backgroundColor() => Colors.transparent; final FocusNode focusNode; final CharacterThemeCubit _characterThemeBloc; final PinballAudioPlayer _audioPlayer; final LeaderboardRepository leaderboardRepository; final ShareRepository shareRepository; final AppLocalizations _l10n; final PlatformHelper platformHelper; final GameBloc _gameBloc; List? _entries; Future preFetchLeaderboard() async { try { _entries = await leaderboardRepository.fetchTop10Leaderboard(); } catch (_) { // An initial null leaderboard means that we couldn't fetch // the entries for the [Backbox] and it will show the relevant display. _entries = null; } } @override Future onLoad() async { await add( FlameMultiBlocProvider( providers: [ FlameBlocProvider.value( value: _gameBloc, ), FlameBlocProvider.value( value: _characterThemeBloc, ), ], children: [ MultiFlameProvider( providers: [ FlameProvider.value(_audioPlayer), FlameProvider.value(leaderboardRepository), FlameProvider.value(shareRepository), FlameProvider.value(_l10n), FlameProvider.value(platformHelper), ], children: [ BonusNoiseBehavior(), GameBlocStatusListener(), BallSpawningBehavior(), CharacterSelectionBehavior(), CameraFocusingBehavior(), CanvasComponent( onSpritePainted: (paint) { if (paint.filterQuality != FilterQuality.medium) { paint.filterQuality = FilterQuality.medium; } }, children: [ ZCanvasComponent( children: [ if (!platformHelper.isMobile) ArcadeBackground(), BoardBackgroundSpriteComponent(), Boundaries(), Backbox( leaderboardRepository: leaderboardRepository, shareRepository: shareRepository, entries: _entries, ), GoogleGallery(), Multipliers(), Multiballs(), SkillShot( children: [ ScoringContactBehavior(points: Points.oneMillion), RolloverNoiseBehavior(), ], ), AndroidAcres(), DinoDesert(), FlutterForest(), SparkyScorch(), Drain(), BottomGroup(), Launcher(), ], ), ], ), ], ), ], ), ); await super.onLoad(); } final focusedBoardSide = {}; @override void onTapDown(int pointerId, TapDownInfo info) { if (info.raw.kind == PointerDeviceKind.touch && _gameBloc.state.status.isPlaying) { final rocket = descendants().whereType().first; final bounds = rocket.topLeftPosition & rocket.size; final tappedRocket = bounds.contains(info.eventPosition.game.toOffset()); if (tappedRocket) { descendants() .whereType>() .first .bloc .autoPulled(); } else { final tappedLeftSide = info.eventPosition.widget.x < canvasSize.x / 2; focusedBoardSide[pointerId] = tappedLeftSide ? BoardSide.left : BoardSide.right; final flippers = descendants() .whereType() .where((flipper) => flipper.side == focusedBoardSide[pointerId]); for (final flipper in flippers) { flipper .descendants() .whereType>() .forEach((provider) => provider.bloc.moveUp()); } } } super.onTapDown(pointerId, info); } @override void onTapUp(int pointerId, TapUpInfo info) { _moveFlippersDown(pointerId); super.onTapUp(pointerId, info); } @override void onTapCancel(int pointerId) { _moveFlippersDown(pointerId); super.onTapCancel(pointerId); } void _moveFlippersDown(int pointerId) { if (focusedBoardSide[pointerId] != null) { final flippers = descendants() .whereType() .where((flipper) => flipper.side == focusedBoardSide[pointerId]); for (final flipper in flippers) { flipper .descendants() .whereType>() .forEach((provider) => provider.bloc.moveDown()); } } } } class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { DebugPinballGame({ required CharacterThemeCubit characterThemeBloc, required LeaderboardRepository leaderboardRepository, required ShareRepository shareRepository, required AppLocalizations l10n, required PinballAudioPlayer audioPlayer, required PlatformHelper platformHelper, required GameBloc gameBloc, }) : super( characterThemeBloc: characterThemeBloc, audioPlayer: audioPlayer, leaderboardRepository: leaderboardRepository, shareRepository: shareRepository, l10n: l10n, platformHelper: platformHelper, gameBloc: gameBloc, ); Vector2? lineStart; Vector2? lineEnd; @override Future onLoad() async { await super.onLoad(); await addAll([PreviewLine(), _DebugInformation()]); } @override void onTapUp(int pointerId, TapUpInfo info) { super.onTapUp(pointerId, info); if (info.raw.kind == PointerDeviceKind.mouse) { final canvas = descendants().whereType().single; final ball = Ball()..initialPosition = info.eventPosition.game; canvas.add(ball); } } @override void onPanStart(DragStartInfo info) => lineStart = info.eventPosition.game; @override void onPanUpdate(DragUpdateInfo info) => lineEnd = info.eventPosition.game; @override void onPanEnd(DragEndInfo info) { if (lineEnd != null) { final line = lineEnd! - lineStart!; _turboChargeBall(line); lineEnd = null; lineStart = null; } } void _turboChargeBall(Vector2 line) { final canvas = descendants().whereType().single; final ball = Ball()..initialPosition = lineStart!; final impulse = line * -1 * 10; ball.add(BallTurboChargingBehavior(impulse: impulse)); canvas.add(ball); } } // coverage:ignore-start class PreviewLine extends PositionComponent with HasGameRef { static final _previewLinePaint = Paint() ..color = Colors.pink ..strokeWidth = 0.4 ..style = PaintingStyle.stroke; @override void render(Canvas canvas) { super.render(canvas); if (gameRef.lineEnd != null) { canvas.drawLine( gameRef.lineStart!.toOffset(), gameRef.lineEnd!.toOffset(), _previewLinePaint, ); } } } class _DebugInformation extends Component with HasGameRef { @override PositionType get positionType => PositionType.widget; final _debugTextPaint = TextPaint( style: const TextStyle( color: Colors.green, fontSize: 10, ), ); final _debugBackgroundPaint = Paint()..color = Colors.white; @override void render(Canvas canvas) { final debugText = [ 'FPS: ${gameRef.fps().toStringAsFixed(1)}', 'BALLS: ${gameRef.descendants().whereType().length}', ].join(' | '); final height = _debugTextPaint.measureTextHeight(debugText); final position = Vector2(0, gameRef.camera.canvasSize.y - height); canvas.drawRect( position & Vector2(gameRef.camera.canvasSize.x, height), _debugBackgroundPaint, ); _debugTextPaint.render(canvas, debugText, position); } } // coverage:ignore-end