You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pinball/lib/game/pinball_game.dart

331 lines
10 KiB

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<LeaderboardEntryData>? _entries;
Future<void> 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<void> onLoad() async {
await add(
FlameMultiBlocProvider(
providers: [
FlameBlocProvider<GameBloc, GameState>.value(
value: _gameBloc,
),
FlameBlocProvider<CharacterThemeCubit, CharacterThemeState>.value(
value: _characterThemeBloc,
),
],
children: [
MultiFlameProvider(
providers: [
FlameProvider<PinballAudioPlayer>.value(_audioPlayer),
FlameProvider<LeaderboardRepository>.value(leaderboardRepository),
FlameProvider<ShareRepository>.value(shareRepository),
FlameProvider<AppLocalizations>.value(_l10n),
FlameProvider<PlatformHelper>.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 = <int, BoardSide>{};
@override
void onTapDown(int pointerId, TapDownInfo info) {
if (info.raw.kind == PointerDeviceKind.touch &&
_gameBloc.state.status.isPlaying) {
final rocket = descendants().whereType<RocketSpriteComponent>().first;
final bounds = rocket.topLeftPosition & rocket.size;
final tappedRocket = bounds.contains(info.eventPosition.game.toOffset());
if (tappedRocket) {
descendants()
.whereType<FlameBlocProvider<PlungerCubit, PlungerState>>()
.first
.bloc
.autoPulled();
} else {
final tappedLeftSide = info.eventPosition.widget.x < canvasSize.x / 2;
focusedBoardSide[pointerId] =
tappedLeftSide ? BoardSide.left : BoardSide.right;
final flippers = descendants()
.whereType<Flipper>()
.where((flipper) => flipper.side == focusedBoardSide[pointerId]);
for (final flipper in flippers) {
flipper
.descendants()
.whereType<FlameBlocProvider<FlipperCubit, FlipperState>>()
.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<Flipper>()
.where((flipper) => flipper.side == focusedBoardSide[pointerId]);
for (final flipper in flippers) {
flipper
.descendants()
.whereType<FlameBlocProvider<FlipperCubit, FlipperState>>()
.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<void> 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<ZCanvasComponent>().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<ZCanvasComponent>().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<DebugPinballGame> {
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<DebugPinballGame> {
@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<Ball>().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