// ignore_for_file: public_member_api_docs 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:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_theme/pinball_theme.dart'; class PinballGame extends PinballForge2DGame with FlameBloc, HasKeyboardHandlerComponents, Controls<_GameBallsController>, MultiTouchTapDetector { PinballGame({ required this.characterTheme, required this.audio, required this.l10n, }) : super(gravity: Vector2(0, 30)) { images.prefix = ''; controller = _GameBallsController(this); } /// Identifier of the play button overlay static const playButtonOverlay = 'play_button'; @override Color backgroundColor() => Colors.transparent; final CharacterTheme characterTheme; final PinballAudio audio; final AppLocalizations l10n; late final GameFlowController gameFlowController; @override Future onLoad() async { await add(gameFlowController = GameFlowController(this)); await add(CameraController(this)); final machine = [ BoardBackgroundSpriteComponent(), Boundaries(), Backbox(), ]; final decals = [ GoogleWord(position: Vector2(-4.25, 1.8)), Multipliers(), Multiballs(), ]; final characterAreas = [ AndroidAcres(), DinoDesert(), FlutterForest(), SparkyScorch(), ]; await add( ZCanvasComponent( children: [ ...machine, ...decals, ...characterAreas, Drain(), BottomGroup(), Launcher(), ], ), ); await super.onLoad(); } final focusedBoardSide = {}; @override void onTapDown(int pointerId, TapDownInfo info) { if (info.raw.kind == PointerDeviceKind.touch) { final rocket = descendants().whereType().first; final bounds = rocket.topLeftPosition & rocket.size; // NOTE(wolfen): As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually. if (bounds.contains(info.eventPosition.game.toOffset())) { descendants().whereType().single.pullFor(2); } else { final leftSide = info.eventPosition.widget.x < canvasSize.x / 2; focusedBoardSide[pointerId] = leftSide ? BoardSide.left : BoardSide.right; final flippers = descendants().whereType().where((flipper) { return flipper.side == focusedBoardSide[pointerId]; }); flippers.first.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) { return flipper.side == focusedBoardSide[pointerId]; }); flippers.first.moveDown(); focusedBoardSide.remove(pointerId); } } } class _GameBallsController extends ComponentController with BlocComponent { _GameBallsController(PinballGame game) : super(game); @override bool listenWhen(GameState? previousState, GameState newState) { final noBallsLeft = component.descendants().whereType().isEmpty; final notGameOver = !newState.isGameOver; return noBallsLeft && notGameOver; } @override void onNewState(GameState state) { super.onNewState(state); spawnBall(); } @override Future onLoad() async { await super.onLoad(); spawnBall(); } void spawnBall() { // TODO(alestiago): Refactor with behavioural pattern. component.ready().whenComplete(() { final plunger = parent!.descendants().whereType().single; final ball = ControlledBall.launch( characterTheme: component.characterTheme, )..initialPosition = Vector2( plunger.body.position.x, plunger.body.position.y - Ball.size.y, ); component.firstChild()?.add(ball); }); } } class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { DebugPinballGame({ required CharacterTheme characterTheme, required PinballAudio audio, required AppLocalizations l10n, }) : super( characterTheme: characterTheme, audio: audio, l10n: l10n, ) { controller = _GameBallsController(this); } Vector2? lineStart; Vector2? lineEnd; @override Future onLoad() async { await super.onLoad(); await add(PreviewLine()); await add(_DebugInformation()); } @override void onTapUp(int pointerId, TapUpInfo info) { super.onTapUp(pointerId, info); if (info.raw.kind == PointerDeviceKind.mouse) { final ball = ControlledBall.debug() ..initialPosition = info.eventPosition.game; firstChild()?.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 ball = ControlledBall.debug()..initialPosition = lineStart!; final impulse = line * -1 * 10; ball.add(BallTurboChargingBehavior(impulse: impulse)); firstChild()?.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, ); } } } // TODO(wolfenrain): investigate this CI failure. 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