Merge branch 'main' into fix/plunger-pull-not-working-on-devices

pull/321/head
Tom Arra 3 years ago committed by GitHub
commit 04c2a6f335
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,3 +13,4 @@ jobs:
flutter_channel: stable
flutter_version: 2.10.0
coverage_excludes: "lib/gen/*.dart"
test_optimization: false

@ -1,11 +1,33 @@
{
"firestore": {
"rules": "firestore.rules"
},
"hosting": {
"public": "build/web",
"site": "ashehwkdkdjruejdnensjsjdne",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"headers": [
{
"source": "**/*.@(jpg|jpeg|gif|png)",
"headers": [
{
"key": "Cache-Control",
"value": "max-age=3600"
}
]
},
{
"source": "**",
"headers": [
{
"key": "Cache-Control",
"value": "no-cache, no-store, must-revalidate"
}
]
}
]
},
"storage": {
"rules": "storage.rules"
}
}

@ -0,0 +1,29 @@
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /leaderboard/{userId} {
function prohibited(initials) {
let prohibitedInitials = get(/databases/$(database)/documents/prohibitedInitials/list).data.prohibitedInitials;
return initials in prohibitedInitials;
}
function inCharLimit(initials) {
return initials.size() < 4;
}
function isAuthedUser(auth) {
return request.auth.uid != null && auth.token.firebase.sign_in_provider == "anonymous"
}
// Leaderboard can be read if it doesn't contain any prohibited initials
allow read: if !prohibited(resource.data.playerInitials);
// A leaderboard entry can be created if the user is authenticated,
// it's 3 characters long, and not a prohibited combination.
allow create: if isAuthedUser(request.auth) &&
inCharLimit(request.resource.data.playerInitials) &&
!prohibited(request.resource.data.playerInitials);
}
}
}

@ -8,6 +8,7 @@ import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart';
@ -34,8 +35,11 @@ class App extends StatelessWidget {
RepositoryProvider.value(value: _leaderboardRepository),
RepositoryProvider.value(value: _pinballAudio),
],
child: BlocProvider(
create: (context) => CharacterThemeCubit(),
child: MultiBlocProvider(
providers: [
BlocProvider(create: (_) => CharacterThemeCubit()),
BlocProvider(create: (_) => StartGameBloc()),
],
child: MaterialApp(
title: 'I/O Pinball',
theme: PinballTheme.standard,

@ -0,0 +1,2 @@
export 'bumper_noisy_behavior.dart';
export 'scoring_behavior.dart';

@ -0,0 +1,14 @@
// ignore_for_file: public_member_api_docs
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/pinball_game.dart';
import 'package:pinball_flame/pinball_flame.dart';
class BumperNoisyBehavior extends ContactBehavior with HasGameRef<PinballGame> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
gameRef.audio.bumper();
}
}

@ -0,0 +1,78 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template scoring_behavior}
/// Adds [_points] to the score and shows a text effect.
///
/// The behavior removes itself after the duration.
/// {@endtemplate}
class ScoringBehavior extends Component with HasGameRef<PinballGame> {
/// {@macto scoring_behavior}
ScoringBehavior({
required Points points,
required Vector2 position,
double duration = 1,
}) : _points = points,
_position = position,
_effectController = EffectController(
duration: duration,
);
final Points _points;
final Vector2 _position;
final EffectController _effectController;
@override
void update(double dt) {
super.update(dt);
if (_effectController.completed) {
removeFromParent();
}
}
@override
Future<void> onLoad() async {
gameRef.read<GameBloc>().add(Scored(points: _points.value));
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
await canvas.add(
ScoreComponent(
points: _points,
position: _position,
effectController: _effectController,
),
);
}
}
/// {@template scoring_contact_behavior}
/// Adds points to the score when the [Ball] contacts the [parent].
/// {@endtemplate}
class ScoringContactBehavior extends ContactBehavior
with HasGameRef<PinballGame> {
/// {@macro scoring_contact_behavior}
ScoringContactBehavior({
required Points points,
}) : _points = points;
final Points _points;
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.add(
ScoringBehavior(
points: _points,
position: other.body.position,
),
);
}
}

@ -17,12 +17,13 @@ class GameBloc extends Bloc<GameEvent, GameState> {
}
void _onRoundLost(RoundLost event, Emitter emit) {
final score = state.score * state.multiplier;
final score = state.totalScore + state.roundScore * state.multiplier;
final roundsLeft = math.max(state.rounds - 1, 0);
emit(
state.copyWith(
score: score,
totalScore: score,
roundScore: 0,
multiplier: 1,
rounds: roundsLeft,
),
@ -32,7 +33,7 @@ class GameBloc extends Bloc<GameEvent, GameState> {
void _onScored(Scored event, Emitter emit) {
if (!state.isGameOver) {
emit(
state.copyWith(score: state.score + event.points),
state.copyWith(roundScore: state.roundScore + event.points),
);
}
}

@ -26,22 +26,32 @@ enum GameBonus {
class GameState extends Equatable {
/// {@macro game_state}
const GameState({
required this.score,
required this.totalScore,
required this.roundScore,
required this.multiplier,
required this.rounds,
required this.bonusHistory,
}) : assert(score >= 0, "Score can't be negative"),
}) : assert(totalScore >= 0, "TotalScore can't be negative"),
assert(roundScore >= 0, "Round score can't be negative"),
assert(multiplier > 0, 'Multiplier must be greater than zero'),
assert(rounds >= 0, "Number of rounds can't be negative");
const GameState.initial()
: score = 0,
: totalScore = 0,
roundScore = 0,
multiplier = 1,
rounds = 3,
bonusHistory = const [];
/// The current score of the game.
final int score;
/// The score for the current round of the game.
///
/// Multipliers are only applied to the score for the current round once is
/// lost. Then the [roundScore] is added to the [totalScore] and reset to 0
/// for the next round.
final int roundScore;
/// The total score of the game.
final int totalScore;
/// The current multiplier for the score.
final int multiplier;
@ -58,20 +68,25 @@ class GameState extends Equatable {
/// Determines when the game is over.
bool get isGameOver => rounds == 0;
/// The score displayed at the game.
int get displayScore => roundScore + totalScore;
GameState copyWith({
int? score,
int? totalScore,
int? roundScore,
int? multiplier,
int? balls,
int? rounds,
List<GameBonus>? bonusHistory,
}) {
assert(
score == null || score >= this.score,
"Score can't be decreased",
totalScore == null || totalScore >= this.totalScore,
"Total score can't be decreased",
);
return GameState(
score: score ?? this.score,
totalScore: totalScore ?? this.totalScore,
roundScore: roundScore ?? this.roundScore,
multiplier: multiplier ?? this.multiplier,
rounds: rounds ?? this.rounds,
bonusHistory: bonusHistory ?? this.bonusHistory,
@ -80,7 +95,8 @@ class GameState extends Equatable {
@override
List<Object?> get props => [
score,
totalScore,
roundScore,
multiplier,
rounds,
bonusHistory,

@ -2,8 +2,8 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template android_acres}
@ -15,27 +15,39 @@ class AndroidAcres extends Component {
AndroidAcres()
: super(
children: [
SpaceshipRamp(),
SpaceshipRamp(
children: [
RampShotBehavior(
points: Points.fiveThousand,
),
RampBonusBehavior(
points: Points.oneMillion,
),
],
),
SpaceshipRail(),
AndroidSpaceship(position: Vector2(-26.5, -28.5)),
AndroidAnimatronic(
children: [
ScoringBehavior(points: Points.twoHundredThousand),
ScoringContactBehavior(points: Points.twoHundredThousand),
],
)..initialPosition = Vector2(-26, -28.25),
AndroidBumper.a(
children: [
BumperScoringBehavior(points: Points.twentyThousand),
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
],
)..initialPosition = Vector2(-25, 1.3),
AndroidBumper.b(
children: [
BumperScoringBehavior(points: Points.twentyThousand),
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
],
)..initialPosition = Vector2(-32.8, -9.2),
AndroidBumper.cow(
children: [
BumperScoringBehavior(points: Points.twentyThousand),
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
],
)..initialPosition = Vector2(-20.5, -13.8),
AndroidSpaceshipBonusBehavior(),

@ -1 +1,3 @@
export 'android_spaceship_bonus_behavior.dart';
export 'ramp_bonus_behavior.dart';
export 'ramp_shot_behavior.dart';

@ -0,0 +1,62 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ramp_bonus_behavior}
/// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp].
/// {@endtemplate}
class RampBonusBehavior extends Component
with ParentIsA<SpaceshipRamp>, HasGameRef<PinballGame> {
/// {@macro ramp_bonus_behavior}
RampBonusBehavior({
required Points points,
}) : _points = points,
super();
/// Creates a [RampBonusBehavior].
///
/// This can be used for testing [RampBonusBehavior] in isolation.
@visibleForTesting
RampBonusBehavior.test({
required Points points,
required this.subscription,
}) : _points = points,
super();
final Points _points;
/// Subscription to [SpaceshipRampState] at [SpaceshipRamp].
@visibleForTesting
StreamSubscription? subscription;
@override
void onMount() {
super.onMount();
subscription = subscription ??
parent.bloc.stream.listen((state) {
final achievedOneMillionPoints = state.hits % 10 == 0;
if (achievedOneMillionPoints) {
parent.add(
ScoringBehavior(
points: _points,
position: Vector2(0, -60),
duration: 2,
),
);
}
});
}
@override
void onRemove() {
subscription?.cancel();
super.onRemove();
}
}

@ -0,0 +1,63 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/cupertino.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ramp_shot_behavior}
/// Increases the score when a [Ball] is shot into the [SpaceshipRamp].
/// {@endtemplate}
class RampShotBehavior extends Component
with ParentIsA<SpaceshipRamp>, HasGameRef<PinballGame> {
/// {@macro ramp_shot_behavior}
RampShotBehavior({
required Points points,
}) : _points = points,
super();
/// Creates a [RampShotBehavior].
///
/// This can be used for testing [RampShotBehavior] in isolation.
@visibleForTesting
RampShotBehavior.test({
required Points points,
required this.subscription,
}) : _points = points,
super();
final Points _points;
/// Subscription to [SpaceshipRampState] at [SpaceshipRamp].
@visibleForTesting
StreamSubscription? subscription;
@override
void onMount() {
super.onMount();
subscription = subscription ??
parent.bloc.stream.listen((state) {
final achievedOneMillionPoints = state.hits % 10 == 0;
if (!achievedOneMillionPoints) {
gameRef.read<GameBloc>().add(const MultiplierIncreased());
parent.add(
ScoringBehavior(
points: _points,
position: Vector2(0, -45),
),
);
}
});
}
@override
void onRemove() {
subscription?.cancel();
super.onRemove();
}
}

@ -0,0 +1,56 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:pinball/game/components/backbox/displays/displays.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template backbox}
/// The [Backbox] of the pinball machine.
/// {@endtemplate}
class Backbox extends PositionComponent with HasGameRef, ZIndex {
/// {@macro backbox}
Backbox()
: super(
position: Vector2(0, -87),
anchor: Anchor.bottomCenter,
children: [
_BackboxSpriteComponent(),
],
) {
zIndex = ZIndexes.backbox;
}
/// Puts [InitialsInputDisplay] on the [Backbox].
Future<void> initialsInput({
required int score,
required String characterIconPath,
InitialsOnSubmit? onSubmit,
}) async {
removeAll(children.where((child) => child is! _BackboxSpriteComponent));
await add(
InitialsInputDisplay(
score: score,
characterIconPath: characterIconPath,
onSubmit: onSubmit,
),
);
}
}
class _BackboxSpriteComponent extends SpriteComponent with HasGameRef {
_BackboxSpriteComponent() : super(anchor: Anchor.bottomCenter);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.backbox.marquee.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 20;
}
}

@ -0,0 +1 @@
export 'initials_input_display.dart';

@ -0,0 +1,387 @@
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// Signature for the callback called when the used has
/// submitted their initials on the [InitialsInputDisplay].
typedef InitialsOnSubmit = void Function(String);
final _bodyTextPaint = TextPaint(
style: const TextStyle(
fontSize: 3,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
final _subtitleTextPaint = TextPaint(
style: const TextStyle(
fontSize: 1.8,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
/// {@template initials_input_display}
/// Display that handles the user input on the game over view.
/// {@endtemplate}
// TODO(allisonryan0002): add mobile input buttons.
class InitialsInputDisplay extends Component with HasGameRef {
/// {@macro initials_input_display}
InitialsInputDisplay({
required int score,
required String characterIconPath,
InitialsOnSubmit? onSubmit,
}) : _onSubmit = onSubmit,
super(
children: [
_ScoreLabelTextComponent(),
_ScoreTextComponent(score.formatScore()),
_NameLabelTextComponent(),
_CharacterIconSpriteComponent(characterIconPath),
_DividerSpriteComponent(),
_InstructionsComponent(),
],
);
final InitialsOnSubmit? _onSubmit;
@override
Future<void> onLoad() async {
for (var i = 0; i < 3; i++) {
await add(
InitialsLetterPrompt(
position: Vector2(
11.4 + (2.3 * i),
-20,
),
hasFocus: i == 0,
),
);
}
await add(
KeyboardInputController(
keyUp: {
LogicalKeyboardKey.arrowLeft: () => _movePrompt(true),
LogicalKeyboardKey.arrowRight: () => _movePrompt(false),
LogicalKeyboardKey.enter: _submit,
},
),
);
}
/// Returns the current inputed initials
String get initials => children
.whereType<InitialsLetterPrompt>()
.map((prompt) => prompt.char)
.join();
bool _submit() {
_onSubmit?.call(initials);
return true;
}
bool _movePrompt(bool left) {
final prompts = children.whereType<InitialsLetterPrompt>().toList();
final current = prompts.firstWhere((prompt) => prompt.hasFocus)
..hasFocus = false;
var index = prompts.indexOf(current) + (left ? -1 : 1);
index = min(max(0, index), prompts.length - 1);
prompts[index].hasFocus = true;
return false;
}
}
class _ScoreLabelTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_ScoreLabelTextComponent()
: super(
anchor: Anchor.centerLeft,
position: Vector2(-16.9, -24),
textRenderer: _bodyTextPaint.copyWith(
(style) => style.copyWith(
color: PinballColors.red,
),
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.score;
}
}
class _ScoreTextComponent extends TextComponent {
_ScoreTextComponent(String score)
: super(
text: score,
anchor: Anchor.centerLeft,
position: Vector2(-16.9, -20),
textRenderer: _bodyTextPaint,
);
}
class _NameLabelTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_NameLabelTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(11.4, -24),
textRenderer: _bodyTextPaint.copyWith(
(style) => style.copyWith(
color: PinballColors.red,
),
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.name;
}
}
class _CharacterIconSpriteComponent extends SpriteComponent with HasGameRef {
_CharacterIconSpriteComponent(String characterIconPath)
: _characterIconPath = characterIconPath,
super(
anchor: Anchor.center,
position: Vector2(8.4, -20),
);
final String _characterIconPath;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(gameRef.images.fromCache(_characterIconPath));
this.sprite = sprite;
size = sprite.originalSize / 20;
}
}
/// {@template initials_input_display}
/// Display that handles the user input on the game over view.
/// {@endtemplate}
@visibleForTesting
class InitialsLetterPrompt extends PositionComponent {
/// {@macro initials_input_display}
InitialsLetterPrompt({
required Vector2 position,
bool hasFocus = false,
}) : _hasFocus = hasFocus,
super(
position: position,
);
static const _alphabetCode = 65;
static const _alphabetLength = 25;
var _charIndex = 0;
bool _hasFocus;
late RectangleComponent _underscore;
late TextComponent _input;
late TimerComponent _underscoreBlinker;
@override
Future<void> onLoad() async {
_underscore = RectangleComponent(
size: Vector2(1.9, 0.4),
anchor: Anchor.center,
position: Vector2(-0.1, 1.8),
);
await add(_underscore);
_input = TextComponent(
text: 'A',
textRenderer: _bodyTextPaint,
anchor: Anchor.center,
);
await add(_input);
_underscoreBlinker = TimerComponent(
period: 0.6,
repeat: true,
autoStart: _hasFocus,
onTick: () {
_underscore.paint.color = (_underscore.paint.color == Colors.white)
? Colors.transparent
: Colors.white;
},
);
await add(_underscoreBlinker);
await add(
KeyboardInputController(
keyUp: {
LogicalKeyboardKey.arrowUp: () => _cycle(true),
LogicalKeyboardKey.arrowDown: () => _cycle(false),
},
),
);
}
/// Returns the current selected character
String get char => String.fromCharCode(_alphabetCode + _charIndex);
bool _cycle(bool up) {
if (_hasFocus) {
final newCharCode =
min(max(_charIndex + (up ? 1 : -1), 0), _alphabetLength);
_input.text = String.fromCharCode(_alphabetCode + newCharCode);
_charIndex = newCharCode;
return false;
}
return true;
}
/// Returns if this prompt has focus on it
bool get hasFocus => _hasFocus;
/// Updates this prompt focus
set hasFocus(bool hasFocus) {
if (hasFocus) {
_underscoreBlinker.timer.resume();
} else {
_underscoreBlinker.timer.pause();
}
_underscore.paint.color = Colors.white;
_hasFocus = hasFocus;
}
}
class _DividerSpriteComponent extends SpriteComponent with HasGameRef {
_DividerSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, -17),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(Assets.images.backbox.displayDivider.keyName),
);
this.sprite = sprite;
size = sprite.originalSize / 20;
}
}
class _InstructionsComponent extends PositionComponent with HasGameRef {
_InstructionsComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, -12.3),
children: [
_EnterInitialsTextComponent(),
_ArrowsTextComponent(),
_AndPressTextComponent(),
_EnterReturnTextComponent(),
_ToSubmitTextComponent(),
],
);
}
class _EnterInitialsTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_EnterInitialsTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, -2.4),
textRenderer: _subtitleTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.enterInitials;
}
}
class _ArrowsTextComponent extends TextComponent with HasGameRef<PinballGame> {
_ArrowsTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(-13.2, 0),
textRenderer: _subtitleTextPaint.copyWith(
(style) => style.copyWith(
fontWeight: FontWeight.bold,
),
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.arrows;
}
}
class _AndPressTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_AndPressTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(-3.7, 0),
textRenderer: _subtitleTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.andPress;
}
}
class _EnterReturnTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_EnterReturnTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(10, 0),
textRenderer: _subtitleTextPaint.copyWith(
(style) => style.copyWith(
fontWeight: FontWeight.bold,
),
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.enterReturn;
}
}
class _ToSubmitTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_ToSubmitTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, 2.4),
textRenderer: _subtitleTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.toSubmit;
}
}

@ -1,4 +1,5 @@
import 'package:flame/components.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
@ -51,7 +52,8 @@ class _BottomGroupSide extends Component {
final kicker = Kicker(
side: _side,
children: [
ScoringBehavior(points: Points.fiveThousand)..applyTo(['bouncy_edge']),
ScoringContactBehavior(points: Points.fiveThousand)
..applyTo(['bouncy_edge']),
],
)..initialPosition = Vector2(
(22.64 * direction) + centerXAdjustment,

@ -3,15 +3,15 @@ import 'package:flame/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Adds helpers methods to Flame's [Camera]
/// Adds helpers methods to Flame's [Camera].
extension CameraX on Camera {
/// Instantly apply the point of focus to the [Camera]
/// Instantly apply the point of focus to the [Camera].
void snapToFocus(FocusData data) {
followVector2(data.position);
zoom = data.zoom;
}
/// Returns a [CameraZoom] that can be added to a [FlameGame]
/// Returns a [CameraZoom] that can be added to a [FlameGame].
CameraZoom focusToCameraZoom(FocusData data) {
final zoom = CameraZoom(value: data.zoom);
zoom.completed.then((_) {
@ -22,7 +22,7 @@ extension CameraX on Camera {
}
/// {@template focus_data}
/// Model class that defines a focus point of the camera
/// Model class that defines a focus point of the camera.
/// {@endtemplate}
class FocusData {
/// {@template focus_data}
@ -31,50 +31,63 @@ class FocusData {
required this.position,
});
/// The amount of zoom
/// The amount of zoom.
final double zoom;
/// The position of the camera
/// The position of the camera.
final Vector2 position;
}
/// {@template camera_controller}
/// A [Component] that controls its game camera focus
/// A [Component] that controls its game camera focus.
/// {@endtemplate}
class CameraController extends ComponentController<FlameGame> {
/// {@macro camera_controller}
CameraController(FlameGame component) : super(component) {
final gameZoom = component.size.y / 16;
final backboardZoom = component.size.y / 18;
final waitingBackboxZoom = component.size.y / 18;
final gameOverBackboxZoom = component.size.y / 10;
gameFocus = FocusData(
zoom: gameZoom,
position: Vector2(0, -7.8),
);
backboardFocus = FocusData(
zoom: backboardZoom,
position: Vector2(0, -100.8),
waitingBackboxFocus = FocusData(
zoom: waitingBackboxZoom,
position: Vector2(0, -112),
);
gameOverBackboxFocus = FocusData(
zoom: gameOverBackboxZoom,
position: Vector2(0, -111),
);
// Game starts with the camera focused on the panel
// Game starts with the camera focused on the [Backbox].
component.camera
..speed = 100
..snapToFocus(backboardFocus);
..snapToFocus(waitingBackboxFocus);
}
/// Holds the data for the game focus point
/// Holds the data for the game focus point.
late final FocusData gameFocus;
/// Holds the data for the backboard focus point
late final FocusData backboardFocus;
/// Holds the data for the waiting backbox focus point.
late final FocusData waitingBackboxFocus;
/// Holds the data for the game over backbox focus point.
late final FocusData gameOverBackboxFocus;
/// Move the camera focus to the game board
/// Move the camera focus to the game board.
void focusOnGame() {
component.add(component.camera.focusToCameraZoom(gameFocus));
}
/// Move the camera focus to the backboard
void focusOnBackboard() {
component.add(component.camera.focusToCameraZoom(backboardFocus));
/// Move the camera focus to the waiting backbox.
void focusOnWaitingBackbox() {
component.add(component.camera.focusToCameraZoom(waitingBackboxFocus));
}
/// Move the camera focus to the game over backbox.
void focusOnGameOverBackbox() {
component.add(component.camera.focusToCameraZoom(gameOverBackboxFocus));
}
}

@ -1,4 +1,5 @@
export 'android_acres/android_acres.dart';
export 'backbox/backbox.dart';
export 'bottom_group.dart';
export 'camera_controller.dart';
export 'controlled_ball.dart';
@ -12,5 +13,4 @@ export 'google_word/google_word.dart';
export 'launcher.dart';
export 'multiballs/multiballs.dart';
export 'multipliers/multipliers.dart';
export 'scoring_behavior.dart';
export 'sparky_scorch.dart';

@ -1,7 +1,6 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
@ -17,7 +16,7 @@ class ControlledBall extends Ball with Controls<BallController> {
/// A [Ball] that launches from the [Plunger].
ControlledBall.launch({
required CharacterTheme characterTheme,
}) : super(baseColor: characterTheme.ballColor) {
}) : super(assetPath: characterTheme.ball.keyName) {
controller = BallController(this);
layer = Layer.launcher;
zIndex = ZIndexes.ballOnLaunchRamp;
@ -28,13 +27,13 @@ class ControlledBall extends Ball with Controls<BallController> {
/// {@endtemplate}
ControlledBall.bonus({
required CharacterTheme characterTheme,
}) : super(baseColor: characterTheme.ballColor) {
}) : super(assetPath: characterTheme.ball.keyName) {
controller = BallController(this);
zIndex = ZIndexes.ballOnBoard;
}
/// [Ball] used in [DebugPinballGame].
ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) {
ControlledBall.debug() : super() {
controller = BallController(this);
zIndex = ZIndexes.ballOnBoard;
}
@ -67,7 +66,9 @@ class BallController extends ComponentController<Ball>
const Duration(milliseconds: 2583),
);
component.resume();
await component.boost(Vector2(40, 110));
await component.add(
BallTurboChargingBehavior(impulse: Vector2(40, 110)),
);
}
@override

@ -1,6 +1,7 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/dino_desert/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
@ -16,7 +17,7 @@ class DinoDesert extends Component {
children: [
ChromeDino(
children: [
ScoringBehavior(points: Points.twoHundredThousand)
ScoringContactBehavior(points: Points.twoHundredThousand)
..applyTo(['inside_mouth']),
],
)..initialPosition = Vector2(12.6, -6.9),
@ -35,12 +36,14 @@ class DinoDesert extends Component {
}
class _BarrierBehindDino extends BodyComponent {
_BarrierBehindDino() : super(renderBody: false);
@override
Body createBody() {
final shape = EdgeShape()
..set(
Vector2(25, -14.2),
Vector2(25, -7.7),
Vector2(25.3, -14.2),
Vector2(25.3, -7.7),
);
return world.createBody(BodyDef())..createFixtureFromShape(shape);

@ -17,7 +17,7 @@ class FlutterForestBonusBehavior extends Component
final bumpers = parent.children.whereType<DashNestBumper>();
final signpost = parent.firstChild<Signpost>()!;
final animatronic = parent.firstChild<DashAnimatronic>()!;
final canvas = gameRef.firstChild<ZCanvasComponent>()!;
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
for (final bumper in bumpers) {
// TODO(alestiago): Refactor subscription management once the following is

@ -2,8 +2,8 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
@ -18,22 +18,26 @@ class FlutterForest extends Component with ZIndex {
children: [
Signpost(
children: [
BumperScoringBehavior(points: Points.fiveThousand),
ScoringContactBehavior(points: Points.fiveThousand),
BumperNoisyBehavior(),
],
)..initialPosition = Vector2(8.35, -58.3),
DashNestBumper.main(
children: [
BumperScoringBehavior(points: Points.twoHundredThousand),
ScoringContactBehavior(points: Points.twoHundredThousand),
BumperNoisyBehavior(),
],
)..initialPosition = Vector2(18.55, -59.35),
DashNestBumper.a(
children: [
BumperScoringBehavior(points: Points.twentyThousand),
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
],
)..initialPosition = Vector2(8.95, -51.95),
DashNestBumper.b(
children: [
BumperScoringBehavior(points: Points.twentyThousand),
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
],
)..initialPosition = Vector2(22.3, -46.75),
DashAnimatronic()..position = Vector2(20, -66),

@ -1,7 +1,6 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template game_flow_controller}
@ -20,27 +19,26 @@ class GameFlowController extends ComponentController<PinballGame>
@override
void onNewState(GameState state) {
if (state.isGameOver) {
gameOver();
_initialsInput();
} else {
start();
}
}
/// Puts the game on a game over state
void gameOver() {
/// Puts the game in the initials input state.
void _initialsInput() {
// TODO(erickzanardo): implement score submission and "navigate" to the
// next page
component.firstChild<Backboard>()?.gameOverMode(
score: state?.score ?? 0,
component.descendants().whereType<Backbox>().first.initialsInput(
score: state?.displayScore ?? 0,
characterIconPath: component.characterTheme.leaderboardIcon.keyName,
);
component.firstChild<CameraController>()?.focusOnBackboard();
component.firstChild<CameraController>()!.focusOnGameOverBackbox();
}
/// Puts the game on a playing state
/// Puts the game in the playing state.
void start() {
component.audio.backgroundMusic();
component.firstChild<Backboard>()?.waitingMode();
component.firstChild<CameraController>()?.focusOnGame();
component.overlays.remove(PinballGame.playButtonOverlay);
}

@ -1,7 +1,7 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/scoring_behavior.dart';
import 'package:pinball/game/components/google_word/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
@ -16,27 +16,27 @@ class GoogleWord extends Component with ZIndex {
children: [
GoogleLetter(
0,
children: [ScoringBehavior(points: Points.fiveThousand)],
children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-13.1, 1.72),
GoogleLetter(
1,
children: [ScoringBehavior(points: Points.fiveThousand)],
children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-8.33, -0.75),
GoogleLetter(
2,
children: [ScoringBehavior(points: Points.fiveThousand)],
children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-2.88, -1.85),
GoogleLetter(
3,
children: [ScoringBehavior(points: Points.fiveThousand)],
children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(2.88, -1.85),
GoogleLetter(
4,
children: [ScoringBehavior(points: Points.fiveThousand)],
children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(8.33, -0.75),
GoogleLetter(
5,
children: [ScoringBehavior(points: Points.fiveThousand)],
children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(13.1, 1.72),
GoogleWordBonusBehavior(),
],

@ -1,53 +0,0 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template scoring_behavior}
/// Adds points to the score when the ball contacts the [parent].
/// {@endtemplate}
class ScoringBehavior extends ContactBehavior with HasGameRef<PinballGame> {
/// {@macro scoring_behavior}
ScoringBehavior({
required Points points,
}) : _points = points;
final Points _points;
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
gameRef.read<GameBloc>().add(Scored(points: _points.value));
gameRef.firstChild<ZCanvasComponent>()!.add(
ScoreComponent(
points: _points,
position: other.body.position,
),
);
}
}
/// {@template bumper_scoring_behavior}
/// A specific [ScoringBehavior] used for Bumpers.
/// In addition to its parent logic, also plays the
/// SFX for bumpers
/// {@endtemplate}
class BumperScoringBehavior extends ScoringBehavior {
/// {@macro bumper_scoring_behavior}
BumperScoringBehavior({
required Points points,
}) : super(points: points);
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
gameRef.audio.bumper();
}
}

@ -2,7 +2,8 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/components.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template sparky_scorch}
@ -16,17 +17,20 @@ class SparkyScorch extends Component {
children: [
SparkyBumper.a(
children: [
BumperScoringBehavior(points: Points.twentyThousand),
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
],
)..initialPosition = Vector2(-22.9, -41.65),
SparkyBumper.b(
children: [
BumperScoringBehavior(points: Points.twentyThousand),
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
],
)..initialPosition = Vector2(-21.25, -57.9),
SparkyBumper.c(
children: [
BumperScoringBehavior(points: Points.twentyThousand),
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
],
)..initialPosition = Vector2(-3.3, -52.55),
SparkyComputerSensor()..initialPosition = Vector2(-13, -49.9),
@ -47,7 +51,7 @@ class SparkyComputerSensor extends BodyComponent
: super(
renderBody: false,
children: [
ScoringBehavior(points: Points.twentyThousand),
ScoringContactBehavior(points: Points.twentyThousand),
],
);

@ -13,7 +13,6 @@ extension PinballGameAssetsX on PinballGame {
return [
images.load(components.Assets.images.boardBackground.keyName),
images.load(components.Assets.images.ball.ball.keyName),
images.load(components.Assets.images.ball.flameEffect.keyName),
images.load(components.Assets.images.signpost.inactive.keyName),
images.load(components.Assets.images.signpost.active1.keyName),
@ -99,8 +98,8 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.sparky.bumper.b.dimmed.keyName),
images.load(components.Assets.images.sparky.bumper.c.lit.keyName),
images.load(components.Assets.images.sparky.bumper.c.dimmed.keyName),
images.load(components.Assets.images.backboard.backboardScores.keyName),
images.load(components.Assets.images.backboard.backboardGameOver.keyName),
images.load(components.Assets.images.backbox.marquee.keyName),
images.load(components.Assets.images.backbox.displayDivider.keyName),
images.load(components.Assets.images.googleWord.letter1.lit.keyName),
images.load(components.Assets.images.googleWord.letter1.dimmed.keyName),
images.load(components.Assets.images.googleWord.letter2.lit.keyName),
@ -113,7 +112,6 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.googleWord.letter5.dimmed.keyName),
images.load(components.Assets.images.googleWord.letter6.lit.keyName),
images.load(components.Assets.images.googleWord.letter6.dimmed.keyName),
images.load(components.Assets.images.backboard.display.keyName),
images.load(components.Assets.images.multiball.lit.keyName),
images.load(components.Assets.images.multiball.dimmed.keyName),
images.load(components.Assets.images.multiplier.x2.lit.keyName),
@ -133,10 +131,18 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.flapper.backSupport.keyName),
images.load(components.Assets.images.flapper.frontSupport.keyName),
images.load(components.Assets.images.flapper.flap.keyName),
images.load(components.Assets.images.skillShot.decal.keyName),
images.load(components.Assets.images.skillShot.pin.keyName),
images.load(components.Assets.images.skillShot.lit.keyName),
images.load(components.Assets.images.skillShot.dimmed.keyName),
images.load(dashTheme.leaderboardIcon.keyName),
images.load(sparkyTheme.leaderboardIcon.keyName),
images.load(androidTheme.leaderboardIcon.keyName),
images.load(dinoTheme.leaderboardIcon.keyName),
images.load(androidTheme.ball.keyName),
images.load(dashTheme.ball.keyName),
images.load(dinoTheme.ball.keyName),
images.load(sparkyTheme.ball.keyName),
];
}
}

@ -7,7 +7,9 @@ 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/behaviors/behaviors.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';
@ -22,6 +24,7 @@ class PinballGame extends PinballForge2DGame
PinballGame({
required this.characterTheme,
required this.audio,
required this.l10n,
}) : super(gravity: Vector2(0, 30)) {
images.prefix = '';
controller = _GameBallsController(this);
@ -37,6 +40,8 @@ class PinballGame extends PinballForge2DGame
final PinballAudio audio;
final AppLocalizations l10n;
late final GameFlowController gameFlowController;
@override
@ -47,12 +52,17 @@ class PinballGame extends PinballForge2DGame
final machine = [
BoardBackgroundSpriteComponent(),
Boundaries(),
Backboard.waiting(position: Vector2(0, -88)),
Backbox(),
];
final decals = [
GoogleWord(position: Vector2(-4.25, 1.8)),
Multipliers(),
Multiballs(),
SkillShot(
children: [
ScoringContactBehavior(points: Points.oneMillion),
],
),
];
final characterAreas = [
AndroidAcres(),
@ -62,14 +72,23 @@ class PinballGame extends PinballForge2DGame
];
await add(
ZCanvasComponent(
CanvasComponent(
onSpritePainted: (paint) {
if (paint.filterQuality != FilterQuality.medium) {
paint.filterQuality = FilterQuality.medium;
}
},
children: [
...machine,
...decals,
...characterAreas,
Drain(),
BottomGroup(),
Launcher(),
ZCanvasComponent(
children: [
...machine,
...decals,
...characterAreas,
Drain(),
BottomGroup(),
Launcher(),
],
),
],
),
);
@ -77,7 +96,7 @@ class PinballGame extends PinballForge2DGame
await super.onLoad();
}
BoardSide? focusedBoardSide;
final focusedBoardSide = <int, BoardSide>{};
@override
void onTapDown(int pointerId, TapDownInfo info) {
@ -90,9 +109,10 @@ class PinballGame extends PinballForge2DGame
descendants().whereType<Plunger>().single.pullFor(2);
} else {
final leftSide = info.eventPosition.widget.x < canvasSize.x / 2;
focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right;
focusedBoardSide[pointerId] =
leftSide ? BoardSide.left : BoardSide.right;
final flippers = descendants().whereType<Flipper>().where((flipper) {
return flipper.side == focusedBoardSide;
return flipper.side == focusedBoardSide[pointerId];
});
flippers.first.moveUp();
}
@ -103,23 +123,23 @@ class PinballGame extends PinballForge2DGame
@override
void onTapUp(int pointerId, TapUpInfo info) {
_moveFlippersDown();
_moveFlippersDown(pointerId);
super.onTapUp(pointerId, info);
}
@override
void onTapCancel(int pointerId) {
_moveFlippersDown();
_moveFlippersDown(pointerId);
super.onTapCancel(pointerId);
}
void _moveFlippersDown() {
if (focusedBoardSide != null) {
void _moveFlippersDown(int pointerId) {
if (focusedBoardSide[pointerId] != null) {
final flippers = descendants().whereType<Flipper>().where((flipper) {
return flipper.side == focusedBoardSide;
return flipper.side == focusedBoardSide[pointerId];
});
flippers.first.moveDown();
focusedBoardSide = null;
focusedBoardSide.remove(pointerId);
}
}
}
@ -158,25 +178,32 @@ class _GameBallsController extends ComponentController<PinballGame>
plunger.body.position.x,
plunger.body.position.y - Ball.size.y,
);
component.firstChild<ZCanvasComponent>()?.add(ball);
component.descendants().whereType<ZCanvasComponent>().single.add(ball);
});
}
}
class DebugPinballGame extends PinballGame with FPSCounter {
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<void> onLoad() async {
await super.onLoad();
await add(PreviewLine());
await add(_DebugInformation());
}
@ -185,15 +212,64 @@ class DebugPinballGame extends PinballGame with FPSCounter {
super.onTapUp(pointerId, info);
if (info.raw.kind == PointerDeviceKind.mouse) {
final canvas = descendants().whereType<ZCanvasComponent>().single;
final ball = ControlledBall.debug()
..initialPosition = info.eventPosition.game;
firstChild<ZCanvasComponent>()?.add(ball);
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 = ControlledBall.debug()..initialPosition = lineStart!;
final impulse = line * -1 * 10;
ball.add(BallTurboChargingBehavior(impulse: impulse));
canvas.add(ball);
}
}
// TODO(wolfenrain): investigate this CI failure.
// 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,
);
}
}
}
// TODO(wolfenrain): investigate this CI failure.
class _DebugInformation extends Component with HasGameRef<DebugPinballGame> {
@override
PositionType get positionType => PositionType.widget;

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart';
@ -39,8 +40,16 @@ class PinballGamePage extends StatelessWidget {
final pinballAudio = context.read<PinballAudio>();
final game = isDebugMode
? DebugPinballGame(characterTheme: characterTheme, audio: audio)
: PinballGame(characterTheme: characterTheme, audio: audio);
? DebugPinballGame(
characterTheme: characterTheme,
audio: audio,
l10n: context.l10n,
)
: PinballGame(
characterTheme: characterTheme,
audio: audio,
l10n: context.l10n,
);
final loadables = [
...game.preLoadAssets(),
@ -51,7 +60,6 @@ class PinballGamePage extends StatelessWidget {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => StartGameBloc(game: game)),
BlocProvider(create: (_) => GameBloc()),
BlocProvider(create: (_) => AssetsManagerCubit(loadables)..load()),
],
@ -96,36 +104,44 @@ class PinballGameLoadedView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isPlaying = context.select(
(StartGameBloc bloc) => bloc.state.status == StartGameStatus.play,
);
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
final screenWidth = MediaQuery.of(context).size.width;
final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8);
final clampedMargin = leftMargin > 0 ? leftMargin : 0.0;
return Stack(
children: [
Positioned.fill(
child: GameWidget<PinballGame>(
game: game,
initialActiveOverlays: const [PinballGame.playButtonOverlay],
overlayBuilderMap: {
PinballGame.playButtonOverlay: (context, game) {
return Positioned(
bottom: 20,
right: 0,
left: 0,
child: PlayButtonOverlay(game: game),
);
return StartGameListener(
game: game,
child: Stack(
children: [
Positioned.fill(
child: GameWidget<PinballGame>(
game: game,
initialActiveOverlays: const [PinballGame.playButtonOverlay],
overlayBuilderMap: {
PinballGame.playButtonOverlay: (context, game) {
return const Positioned(
bottom: 20,
right: 0,
left: 0,
child: PlayButtonOverlay(),
);
},
},
},
),
),
),
// TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc
// status
Positioned(
top: 16,
left: leftMargin,
child: const GameHud(),
),
],
Positioned(
top: 0,
left: clampedMargin,
child: Visibility(
visible: isPlaying,
child: const GameHud(),
),
),
],
),
);
}
}

@ -7,8 +7,8 @@ import 'package:pinball_ui/pinball_ui.dart';
/// {@template game_hud}
/// Overlay on the [PinballGame].
///
/// Displays the current [GameState.score], [GameState.rounds] and animates when
/// the player gets a [GameBonus].
/// Displays the current [GameState.displayScore], [GameState.rounds] and
/// animates when the player gets a [GameBonus].
/// {@endtemplate}
class GameHud extends StatefulWidget {
/// {@macro game_hud}
@ -23,16 +23,17 @@ class _GameHudState extends State<GameHud> {
/// Ratio from sprite frame (width 500, height 144) w / h = ratio
static const _ratio = 3.47;
static const _width = 265.0;
@override
Widget build(BuildContext context) {
final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver);
final height = _calculateHeight(context);
return _ScoreViewDecoration(
child: SizedBox(
height: _width / _ratio,
width: _width,
height: height,
width: height * _ratio,
child: BlocListener<GameBloc, GameState>(
listenWhen: (previous, current) =>
previous.bonusHistory.length != current.bonusHistory.length,
@ -53,6 +54,17 @@ class _GameHudState extends State<GameHud> {
),
);
}
double _calculateHeight(BuildContext context) {
final height = MediaQuery.of(context).size.height * 0.09;
if (height > 90) {
return 90;
} else if (height < 60) {
return 60;
} else {
return height;
}
}
}
class _ScoreViewDecoration extends StatelessWidget {

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:pinball/game/pinball_game.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template play_button_overlay}
@ -9,13 +9,7 @@ import 'package:pinball_ui/pinball_ui.dart';
/// {@endtemplate}
class PlayButtonOverlay extends StatelessWidget {
/// {@macro play_button_overlay}
const PlayButtonOverlay({
Key? key,
required PinballGame game,
}) : _game = game,
super(key: key);
final PinballGame _game;
const PlayButtonOverlay({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -23,9 +17,8 @@ class PlayButtonOverlay extends StatelessWidget {
return PinballButton(
text: l10n.play,
onTap: () async {
_game.gameFlowController.start();
await showCharacterSelectionDialog(context);
onTap: () {
context.read<StartGameBloc>().add(const PlayTapped());
},
);
}

@ -18,7 +18,7 @@ class ScoreView extends StatelessWidget {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
vertical: 2,
),
child: AnimatedSwitcher(
duration: kThemeAnimationDuration,
@ -69,11 +69,13 @@ class _ScoreText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final score = context.select((GameBloc bloc) => bloc.state.score);
final score = context.select((GameBloc bloc) => bloc.state.displayScore);
return Text(
score.formatScore(),
style: Theme.of(context).textTheme.headline1,
return FittedBox(
child: Text(
score.formatScore(),
style: Theme.of(context).textTheme.headline2,
),
);
}
}

@ -3,10 +3,8 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart';
import 'package:platform_helper/platform_helper.dart';
@ -51,24 +49,16 @@ extension on Control {
}
}
Future<void> showHowToPlayDialog(BuildContext context) {
final audio = context.read<PinballAudio>();
return showDialog<void>(
context: context,
builder: (_) => HowToPlayDialog(),
).then((_) {
audio.ioPinballVoiceOver();
});
}
class HowToPlayDialog extends StatefulWidget {
HowToPlayDialog({
Key? key,
required this.onDismissCallback,
@visibleForTesting PlatformHelper? platformHelper,
}) : platformHelper = platformHelper ?? PlatformHelper(),
super(key: key);
final PlatformHelper platformHelper;
final VoidCallback onDismissCallback;
@override
State<HowToPlayDialog> createState() => _HowToPlayDialogState();
@ -82,6 +72,7 @@ class _HowToPlayDialogState extends State<HowToPlayDialog> {
closeTimer = Timer(const Duration(seconds: 3), () {
if (mounted) {
Navigator.of(context).pop();
widget.onDismissCallback.call();
}
});
}
@ -96,10 +87,19 @@ class _HowToPlayDialogState extends State<HowToPlayDialog> {
Widget build(BuildContext context) {
final isMobile = widget.platformHelper.isMobile;
final l10n = context.l10n;
return PinballDialog(
title: l10n.howToPlay,
subtitle: l10n.tipsForFlips,
child: isMobile ? const _MobileBody() : const _DesktopBody(),
return WillPopScope(
onWillPop: () {
widget.onDismissCallback.call();
return Future.value(true);
},
child: PinballDialog(
title: l10n.howToPlay,
subtitle: l10n.tipsForFlips,
child: FittedBox(
child: isMobile ? const _MobileBody() : const _DesktopBody(),
),
),
);
}
}
@ -111,18 +111,16 @@ class _MobileBody extends StatelessWidget {
Widget build(BuildContext context) {
final paddingWidth = MediaQuery.of(context).size.width * 0.15;
final paddingHeight = MediaQuery.of(context).size.height * 0.075;
return FittedBox(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: paddingWidth,
),
child: Column(
children: [
const _MobileLaunchControls(),
SizedBox(height: paddingHeight),
const _MobileFlipperControls(),
],
),
return Padding(
padding: EdgeInsets.symmetric(
horizontal: paddingWidth,
),
child: Column(
children: [
const _MobileLaunchControls(),
SizedBox(height: paddingHeight),
const _MobileFlipperControls(),
],
),
);
}
@ -191,13 +189,15 @@ class _DesktopBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
children: const [
SizedBox(height: 16),
_DesktopLaunchControls(),
SizedBox(height: 16),
_DesktopFlipperControls(),
],
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: const [
_DesktopLaunchControls(),
SizedBox(height: 16),
_DesktopFlipperControls(),
],
),
);
}
}

@ -64,49 +64,45 @@
"@gameOver": {
"description": "Text displayed on the ending dialog when game finishes"
},
"leaderboard": "Leaderboard",
"@leaderboard": {
"description": "Text displayed on the ending dialog leaderboard button"
"rounds": "Ball Ct:",
"@rounds": {
"description": "Text displayed on the scoreboard widget to indicate rounds left"
},
"rank": "Rank",
"@rank": {
"description": "Text displayed on the leaderboard page header rank column"
"topPlayers": "Top Players",
"@topPlayers": {
"description": "Title text displayed on leaderboard screen"
},
"character": "Character",
"@character": {
"description": "Text displayed on the leaderboard page header character column"
"rank": "rank",
"@rank": {
"description": "Label text displayed above player's rank"
},
"username": "Username",
"@username": {
"description": "Text displayed on the leaderboard page header userName column"
"name": "name",
"@name": {
"description": "Label text displayed above player's initials"
},
"score": "Score",
"score": "score",
"@score": {
"description": "Text displayed on the leaderboard page header score column"
},
"retry": "Retry",
"@retry": {
"description": "Text displayed on the retry button leaders board page"
"description": "Label text displayed above player's score"
},
"addUser": "Add User",
"@addUser": {
"description": "Text displayed on the add user button at ending dialog"
"enterInitials": "Enter your initials using the",
"@enterInitials": {
"description": "Informational text displayed on initials input screen"
},
"error": "Error",
"@error": {
"description": "Text displayed on the ending dialog when there is any error on sending user"
"arrows": "arrows",
"@arrows": {
"description": "Text displayed on initials input screen indicating arrow keys"
},
"yourScore": "Your score is",
"@yourScore": {
"description": "Text displayed on the ending dialog when game finishes to show the final score"
"andPress": "and press",
"@andPress": {
"description": "Connecting text displayed on initials input screen informational text span"
},
"enterInitials": "Enter your initials",
"@enterInitials": {
"description": "Text displayed on the ending dialog when game finishes to ask the user for his initials"
"enterReturn": "enter/return",
"@enterReturn": {
"description": "Text displayed on initials input screen indicating return key"
},
"rounds": "Ball Ct:",
"@rounds": {
"description": "Text displayed on the scoreboard widget to indicate rounds left"
"toSubmit": "to submit",
"@toSubmit": {
"description": "Ending text displayed on initials input screen informational text span"
},
"footerMadeWithText": "Made with ",
"@footerMadeWithText": {

@ -1,20 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/how_to_play/how_to_play.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_theme/pinball_theme.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// Inflates [CharacterSelectionDialog] using [showDialog].
Future<void> showCharacterSelectionDialog(BuildContext context) {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => const CharacterSelectionDialog(),
);
}
/// {@template character_selection_dialog}
/// Dialog used to select the playing character of the game.
/// {@endtemplate character_selection_dialog}
@ -59,7 +50,7 @@ class _SelectCharacterButton extends StatelessWidget {
return PinballButton(
onTap: () async {
Navigator.of(context).pop();
await showHowToPlayDialog(context);
context.read<StartGameBloc>().add(const CharacterSelected());
},
text: l10n.select,
);
@ -74,36 +65,40 @@ class _CharacterGrid extends StatelessWidget {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
_Character(
key: const Key('sparky_character_selection'),
character: const SparkyTheme(),
isSelected: state.isSparkySelected,
),
const SizedBox(height: 6),
_Character(
key: const Key('android_character_selection'),
character: const AndroidTheme(),
isSelected: state.isAndroidSelected,
),
],
Expanded(
child: Column(
children: [
_Character(
key: const Key('sparky_character_selection'),
character: const SparkyTheme(),
isSelected: state.isSparkySelected,
),
const SizedBox(height: 6),
_Character(
key: const Key('android_character_selection'),
character: const AndroidTheme(),
isSelected: state.isAndroidSelected,
),
],
),
),
const SizedBox(width: 6),
Column(
children: [
_Character(
key: const Key('dash_character_selection'),
character: const DashTheme(),
isSelected: state.isDashSelected,
),
const SizedBox(height: 6),
_Character(
key: const Key('dino_character_selection'),
character: const DinoTheme(),
isSelected: state.isDinoSelected,
),
],
Expanded(
child: Column(
children: [
_Character(
key: const Key('dash_character_selection'),
character: const DashTheme(),
isSelected: state.isDashSelected,
),
const SizedBox(height: 6),
_Character(
key: const Key('dino_character_selection'),
character: const DinoTheme(),
isSelected: state.isDinoSelected,
),
],
),
),
],
);

@ -1,6 +1,5 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:pinball/game/game.dart';
part 'start_game_event.dart';
part 'start_game_state.dart';
@ -10,23 +9,16 @@ part 'start_game_state.dart';
/// {@endtemplate}
class StartGameBloc extends Bloc<StartGameEvent, StartGameState> {
/// {@macro start_game_bloc}
StartGameBloc({
required PinballGame game,
}) : _game = game,
super(const StartGameState.initial()) {
StartGameBloc() : super(const StartGameState.initial()) {
on<PlayTapped>(_onPlayTapped);
on<CharacterSelected>(_onCharacterSelected);
on<HowToPlayFinished>(_onHowToPlayFinished);
}
final PinballGame _game;
void _onPlayTapped(
PlayTapped event,
Emitter<StartGameState> emit,
) {
_game.gameFlowController.start();
emit(
state.copyWith(
status: StartGameStatus.selectCharacter,

@ -1 +1,2 @@
export 'bloc/start_game_bloc.dart';
export 'widgets/start_game_listener.dart';

@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/how_to_play/how_to_play.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template start_game_listener}
/// Listener that manages the display of dialogs for [StartGameStatus].
///
/// It's responsible for starting the game after pressing play button
/// and playing a sound after the 'how to play' dialog.
/// {@endtemplate}
class StartGameListener extends StatelessWidget {
/// {@macro start_game_listener}
const StartGameListener({
Key? key,
required Widget child,
required PinballGame game,
}) : _child = child,
_game = game,
super(key: key);
final Widget _child;
final PinballGame _game;
@override
Widget build(BuildContext context) {
return BlocListener<StartGameBloc, StartGameState>(
listener: (context, state) {
switch (state.status) {
case StartGameStatus.initial:
break;
case StartGameStatus.selectCharacter:
_onSelectCharacter(context);
_game.gameFlowController.start();
break;
case StartGameStatus.howToPlay:
_onHowToPlay(context);
break;
case StartGameStatus.play:
break;
}
},
child: _child,
);
}
void _onSelectCharacter(BuildContext context) {
_showPinballDialog(
context: context,
child: const CharacterSelectionDialog(),
barrierDismissible: false,
);
}
void _onHowToPlay(BuildContext context) {
final audio = context.read<PinballAudio>();
_showPinballDialog(
context: context,
child: HowToPlayDialog(
onDismissCallback: () {
context.read<StartGameBloc>().add(const HowToPlayFinished());
audio.ioPinballVoiceOver();
},
),
);
}
void _showPinballDialog({
required BuildContext context,
required Widget child,
bool barrierDismissible = true,
}) {
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
showDialog<void>(
context: context,
barrierColor: PinballColors.transparent,
barrierDismissible: barrierDismissible,
builder: (_) {
return Center(
child: SizedBox(
height: gameWidgetWidth * 0.87,
width: gameWidgetWidth,
child: child,
),
);
},
);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 616 KiB

After

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 735 KiB

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 955 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 886 KiB

After

Width:  |  Height:  |  Size: 1012 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 569 KiB

@ -11,7 +11,7 @@ class $AssetsImagesGen {
const $AssetsImagesGen();
$AssetsImagesAndroidGen get android => const $AssetsImagesAndroidGen();
$AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen();
$AssetsImagesBackboxGen get backbox => const $AssetsImagesBackboxGen();
$AssetsImagesBallGen get ball => const $AssetsImagesBallGen();
$AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen();
@ -35,6 +35,7 @@ class $AssetsImagesGen {
$AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
$AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen();
$AssetsImagesSkillShotGen get skillShot => const $AssetsImagesSkillShotGen();
$AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen();
$AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen();
}
@ -50,20 +51,16 @@ class $AssetsImagesAndroidGen {
const $AssetsImagesAndroidSpaceshipGen();
}
class $AssetsImagesBackboardGen {
const $AssetsImagesBackboardGen();
class $AssetsImagesBackboxGen {
const $AssetsImagesBackboxGen();
/// File path: assets/images/backboard/backboard_game_over.png
AssetGenImage get backboardGameOver =>
const AssetGenImage('assets/images/backboard/backboard_game_over.png');
/// File path: assets/images/backbox/display-divider.png
AssetGenImage get displayDivider =>
const AssetGenImage('assets/images/backbox/display-divider.png');
/// File path: assets/images/backboard/backboard_scores.png
AssetGenImage get backboardScores =>
const AssetGenImage('assets/images/backboard/backboard_scores.png');
/// File path: assets/images/backboard/display.png
AssetGenImage get display =>
const AssetGenImage('assets/images/backboard/display.png');
/// File path: assets/images/backbox/marquee.png
AssetGenImage get marquee =>
const AssetGenImage('assets/images/backbox/marquee.png');
}
class $AssetsImagesBallGen {
@ -276,6 +273,26 @@ class $AssetsImagesSignpostGen {
const AssetGenImage('assets/images/signpost/inactive.png');
}
class $AssetsImagesSkillShotGen {
const $AssetsImagesSkillShotGen();
/// File path: assets/images/skill_shot/decal.png
AssetGenImage get decal =>
const AssetGenImage('assets/images/skill_shot/decal.png');
/// File path: assets/images/skill_shot/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/skill_shot/dimmed.png');
/// File path: assets/images/skill_shot/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/skill_shot/lit.png');
/// File path: assets/images/skill_shot/pin.png
AssetGenImage get pin =>
const AssetGenImage('assets/images/skill_shot/pin.png');
}
class $AssetsImagesSlingshotGen {
const $AssetsImagesSlingshotGen();

@ -78,11 +78,11 @@ class AndroidBumper extends BodyComponent with InitialPosition, ZIndex {
AndroidBumper.cow({
Iterable<Component>? children,
}) : this._(
majorRadius: 3.4,
minorRadius: 2.9,
majorRadius: 3.45,
minorRadius: 3.11,
litAssetPath: Assets.images.android.bumper.cow.lit.keyName,
dimmedAssetPath: Assets.images.android.bumper.cow.dimmed.keyName,
spritePosition: Vector2(0, -0.68),
spritePosition: Vector2(0, -0.35),
bloc: AndroidBumperCubit(),
children: [
...?children,

@ -122,7 +122,7 @@ class _SpaceshipSaucerSpriteAnimationComponent extends SpriteAnimationComponent
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
stepTime: 1 / 12,
textureSize: textureSize,
),
);

@ -1,79 +0,0 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
export 'backboard_game_over.dart';
export 'backboard_letter_prompt.dart';
export 'backboard_waiting.dart';
/// {@template backboard}
/// The [Backboard] of the pinball machine.
/// {@endtemplate}
class Backboard extends PositionComponent with HasGameRef {
/// {@macro backboard}
Backboard({
required Vector2 position,
}) : super(
position: position,
anchor: Anchor.bottomCenter,
);
/// {@macro backboard}
///
/// Returns a [Backboard] initialized in the waiting mode
factory Backboard.waiting({
required Vector2 position,
}) {
return Backboard(position: position)..waitingMode();
}
/// {@macro backboard}
///
/// Returns a [Backboard] initialized in the game over mode
factory Backboard.gameOver({
required Vector2 position,
required String characterIconPath,
required int score,
required BackboardOnSubmit onSubmit,
}) {
return Backboard(position: position)
..gameOverMode(
score: score,
characterIconPath: characterIconPath,
onSubmit: onSubmit,
);
}
/// [TextPaint] used on the [Backboard]
static final textPaint = TextPaint(
style: const TextStyle(
fontSize: 6,
color: Colors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
/// Puts the Backboard in waiting mode, where the scoreboard is shown.
Future<void> waitingMode() async {
children.removeWhere((_) => true);
await add(BackboardWaiting());
}
/// Puts the Backboard in game over mode, where the score input is shown.
Future<void> gameOverMode({
required int score,
required String characterIconPath,
BackboardOnSubmit? onSubmit,
}) async {
children.removeWhere((_) => true);
await add(
BackboardGameOver(
score: score,
characterIconPath: characterIconPath,
onSubmit: onSubmit,
),
);
}
}

@ -1,144 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Signature for the callback called when the used has
/// submettied their initials on the [BackboardGameOver]
typedef BackboardOnSubmit = void Function(String);
/// {@template backboard_game_over}
/// [PositionComponent] that handles the user input on the
/// game over display view.
/// {@endtemplate}
class BackboardGameOver extends PositionComponent with HasGameRef {
/// {@macro backboard_game_over}
BackboardGameOver({
required int score,
required String characterIconPath,
BackboardOnSubmit? onSubmit,
}) : _onSubmit = onSubmit,
super(
children: [
_BackboardSpriteComponent(),
_BackboardDisplaySpriteComponent(),
_ScoreTextComponent(score.formatScore()),
_CharacterIconSpriteComponent(characterIconPath),
],
);
final BackboardOnSubmit? _onSubmit;
@override
Future<void> onLoad() async {
for (var i = 0; i < 3; i++) {
await add(
BackboardLetterPrompt(
position: Vector2(
24.3 + (4.5 * i),
-45,
),
hasFocus: i == 0,
),
);
}
await add(
KeyboardInputController(
keyUp: {
LogicalKeyboardKey.arrowLeft: () => _movePrompt(true),
LogicalKeyboardKey.arrowRight: () => _movePrompt(false),
LogicalKeyboardKey.enter: _submit,
},
),
);
}
/// Returns the current inputed initials
String get initials => children
.whereType<BackboardLetterPrompt>()
.map((prompt) => prompt.char)
.join();
bool _submit() {
_onSubmit?.call(initials);
return true;
}
bool _movePrompt(bool left) {
final prompts = children.whereType<BackboardLetterPrompt>().toList();
final current = prompts.firstWhere((prompt) => prompt.hasFocus)
..hasFocus = false;
var index = prompts.indexOf(current) + (left ? -1 : 1);
index = min(max(0, index), prompts.length - 1);
prompts[index].hasFocus = true;
return false;
}
}
class _BackboardSpriteComponent extends SpriteComponent with HasGameRef {
_BackboardSpriteComponent() : super(anchor: Anchor.bottomCenter);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.backboard.backboardGameOver.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}
class _BackboardDisplaySpriteComponent extends SpriteComponent with HasGameRef {
_BackboardDisplaySpriteComponent()
: super(
anchor: Anchor.bottomCenter,
position: Vector2(0, -11.5),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.backboard.display.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}
class _ScoreTextComponent extends TextComponent {
_ScoreTextComponent(String score)
: super(
text: score,
anchor: Anchor.centerLeft,
position: Vector2(-34, -45),
textRenderer: Backboard.textPaint,
);
}
class _CharacterIconSpriteComponent extends SpriteComponent with HasGameRef {
_CharacterIconSpriteComponent(String characterIconPath)
: _characterIconPath = characterIconPath,
super(
anchor: Anchor.center,
position: Vector2(18.4, -45),
);
final String _characterIconPath;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(gameRef.images.fromCache(_characterIconPath));
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}

@ -1,102 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template backboard_letter_prompt}
/// A [PositionComponent] that renders a letter prompt used
/// on the [BackboardGameOver]
/// {@endtemplate}
class BackboardLetterPrompt extends PositionComponent {
/// {@macro backboard_letter_prompt}
BackboardLetterPrompt({
required Vector2 position,
bool hasFocus = false,
}) : _hasFocus = hasFocus,
super(
position: position,
);
static const _alphabetCode = 65;
static const _alphabetLength = 25;
var _charIndex = 0;
bool _hasFocus;
late RectangleComponent _underscore;
late TextComponent _input;
late TimerComponent _underscoreBlinker;
@override
Future<void> onLoad() async {
_underscore = RectangleComponent(
size: Vector2(3.8, 0.8),
anchor: Anchor.center,
position: Vector2(-0.3, 4),
);
await add(_underscore);
_input = TextComponent(
text: 'A',
textRenderer: Backboard.textPaint,
anchor: Anchor.center,
);
await add(_input);
_underscoreBlinker = TimerComponent(
period: 0.6,
repeat: true,
autoStart: _hasFocus,
onTick: () {
_underscore.paint.color = (_underscore.paint.color == Colors.white)
? Colors.transparent
: Colors.white;
},
);
await add(_underscoreBlinker);
await add(
KeyboardInputController(
keyUp: {
LogicalKeyboardKey.arrowUp: () => _cycle(true),
LogicalKeyboardKey.arrowDown: () => _cycle(false),
},
),
);
}
/// Returns the current selected character
String get char => String.fromCharCode(_alphabetCode + _charIndex);
bool _cycle(bool up) {
if (_hasFocus) {
final newCharCode =
min(max(_charIndex + (up ? 1 : -1), 0), _alphabetLength);
_input.text = String.fromCharCode(_alphabetCode + newCharCode);
_charIndex = newCharCode;
return false;
}
return true;
}
/// Returns if this prompt has focus on it
bool get hasFocus => _hasFocus;
/// Updates this prompt focus
set hasFocus(bool hasFocus) {
if (hasFocus) {
_underscoreBlinker.timer.resume();
} else {
_underscoreBlinker.timer.pause();
}
_underscore.paint.color = Colors.white;
_hasFocus = hasFocus;
}
}

@ -1,17 +0,0 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
/// [PositionComponent] that shows the leaderboard while the player
/// has not started the game yet.
class BackboardWaiting extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
final sprite = await gameRef.loadSprite(
Assets.images.backboard.backboardScores.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.bottomCenter;
}
}

@ -1,13 +1,13 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/ball/behaviors/ball_gravitating_behavior.dart';
import 'package:pinball_components/src/components/ball/behaviors/ball_scaling_behavior.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
export 'behaviors/behaviors.dart';
/// {@template ball}
/// A solid, [BodyType.dynamic] sphere that rolls and bounces around.
@ -15,11 +15,11 @@ import 'package:pinball_flame/pinball_flame.dart';
class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
/// {@macro ball}
Ball({
required this.baseColor,
String? assetPath,
}) : super(
renderBody: false,
children: [
_BallSpriteComponent()..tint(baseColor.withOpacity(0.5)),
_BallSpriteComponent(assetPath: assetPath),
BallScalingBehavior(),
BallGravitatingBehavior(),
],
@ -36,7 +36,7 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
///
/// This can be used for testing [Ball]'s behaviors in isolation.
@visibleForTesting
Ball.test({required this.baseColor})
Ball.test()
: super(
children: [_BallSpriteComponent()],
);
@ -44,23 +44,16 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
/// The size of the [Ball].
static final Vector2 size = Vector2.all(4.13);
/// The base [Color] used to tint this [Ball].
final Color baseColor;
@override
Body createBody() {
final shape = CircleShape()..radius = size.x / 2;
final fixtureDef = FixtureDef(
shape,
density: 1,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
type: BodyType.dynamic,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
return world.createBody(bodyDef)..createFixtureFromShape(shape, 1);
}
/// Immediatly and completly [stop]s the ball.
@ -81,75 +74,25 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
void resume() {
body.gravityScale = Vector2(1, 1);
}
/// Applies a boost and [_TurboChargeSpriteAnimationComponent] on this [Ball].
Future<void> boost(Vector2 impulse) async {
body.linearVelocity = impulse;
await add(_TurboChargeSpriteAnimationComponent());
}
}
class _BallSpriteComponent extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.ball.ball.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
}
}
class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent
with HasGameRef, ZIndex {
_TurboChargeSpriteAnimationComponent()
: super(
anchor: const Anchor(0.53, 0.72),
removeOnFinish: true,
) {
zIndex = ZIndexes.turboChargeFlame;
}
_BallSpriteComponent({
this.assetPath,
}) : super(
anchor: Anchor.center,
);
late final Vector2 _textureSize;
final String? assetPath;
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = await gameRef.images.load(
Assets.images.ball.flameEffect.keyName,
);
const amountPerRow = 8;
const amountPerColumn = 4;
_textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
final sprite = Sprite(
gameRef.images
.fromCache(assetPath ?? theme.Assets.images.dash.ball.keyName),
);
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: _textureSize,
loop: false,
),
);
}
@override
void update(double dt) {
super.update(dt);
if (parent != null) {
final body = (parent! as BodyComponent).body;
final direction = -body.linearVelocity.normalized();
angle = math.atan2(direction.x, -direction.y);
size = (_textureSize / 45) * body.fixtures.first.shape.radius;
}
this.sprite = sprite;
size = sprite.originalSize / 12.5;
}
}

@ -0,0 +1,81 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ball_turbo_charging_behavior}
/// Puts the [Ball] in flames and [_impulse]s it.
/// {@endtemplate}
class BallTurboChargingBehavior extends TimerComponent with ParentIsA<Ball> {
/// {@macro ball_turbo_charging_behavior}
BallTurboChargingBehavior({
required Vector2 impulse,
}) : _impulse = impulse,
super(period: 5, removeOnFinish: true);
final Vector2 _impulse;
@override
Future<void> onLoad() async {
await super.onLoad();
parent.body.linearVelocity = _impulse;
await parent.add(_TurboChargeSpriteAnimationComponent());
}
@override
void onRemove() {
parent
.firstChild<_TurboChargeSpriteAnimationComponent>()!
.removeFromParent();
super.onRemove();
}
}
class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent
with HasGameRef, ZIndex, ParentIsA<Ball> {
_TurboChargeSpriteAnimationComponent()
: super(
anchor: const Anchor(0.53, 0.72),
) {
zIndex = ZIndexes.turboChargeFlame;
}
late final Vector2 _textureSize;
@override
void update(double dt) {
super.update(dt);
final direction = -parent.body.linearVelocity.normalized();
angle = math.atan2(direction.x, -direction.y);
size = (_textureSize / 45) * parent.body.fixtures.first.shape.radius;
}
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = await gameRef.images.load(
Assets.images.ball.flameEffect.keyName,
);
const amountPerRow = 8;
const amountPerColumn = 4;
_textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: _textureSize,
),
);
}
}

@ -1,2 +1,3 @@
export 'ball_gravitating_behavior.dart';
export 'ball_scaling_behavior.dart';
export 'ball_turbo_charging_behavior.dart';

@ -7,7 +7,7 @@ import 'package:pinball_components/pinball_components.dart';
part 'chrome_dino_state.dart';
class ChromeDinoCubit extends Cubit<ChromeDinoState> {
ChromeDinoCubit() : super(const ChromeDinoState.inital());
ChromeDinoCubit() : super(const ChromeDinoState.initial());
void onOpenMouth() {
emit(state.copyWith(isMouthOpen: true));

@ -14,7 +14,7 @@ class ChromeDinoState extends Equatable {
this.ball,
});
const ChromeDinoState.inital()
const ChromeDinoState.initial()
: this(
status: ChromeDinoStatus.idle,
isMouthOpen: false,

@ -1,7 +1,6 @@
export 'android_animatronic.dart';
export 'android_bumper/android_bumper.dart';
export 'android_spaceship/android_spaceship.dart';
export 'backboard/backboard.dart';
export 'ball/ball.dart';
export 'baseboard.dart';
export 'board_background_sprite_component.dart';
@ -22,7 +21,7 @@ export 'joint_anchor.dart';
export 'kicker/kicker.dart';
export 'launch_ramp.dart';
export 'layer.dart';
export 'layer_sensor.dart';
export 'layer_sensor/layer_sensor.dart';
export 'multiball/multiball.dart';
export 'multiplier/multiplier.dart';
export 'plunger.dart';
@ -30,9 +29,10 @@ export 'rocket.dart';
export 'score_component.dart';
export 'shapes/shapes.dart';
export 'signpost/signpost.dart';
export 'skill_shot/skill_shot.dart';
export 'slingshot.dart';
export 'spaceship_rail.dart';
export 'spaceship_ramp.dart';
export 'spaceship_ramp/spaceship_ramp.dart';
export 'sparky_animatronic.dart';
export 'sparky_bumper/sparky_bumper.dart';
export 'sparky_computer.dart';

@ -60,10 +60,10 @@ class DashNestBumper extends BodyComponent with InitialPosition {
Iterable<Component>? children,
}) : this._(
majorRadius: 3,
minorRadius: 2.5,
minorRadius: 2.2,
activeAssetPath: Assets.images.dash.bumper.a.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName,
spritePosition: Vector2(0.35, -1.2),
spritePosition: Vector2(0.3, -1.3),
bloc: DashNestBumperCubit(),
children: [
...?children,
@ -75,11 +75,11 @@ class DashNestBumper extends BodyComponent with InitialPosition {
DashNestBumper.b({
Iterable<Component>? children,
}) : this._(
majorRadius: 3,
minorRadius: 2.5,
majorRadius: 3.1,
minorRadius: 2.2,
activeAssetPath: Assets.images.dash.bumper.b.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName,
spritePosition: Vector2(0.35, -1.2),
spritePosition: Vector2(0.4, -1.2),
bloc: DashNestBumperCubit(),
children: [
...?children,

@ -1,90 +0,0 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template layer_entrance_orientation}
/// Determines if a layer entrance is oriented [up] or [down] on the board.
/// {@endtemplate}
enum LayerEntranceOrientation {
/// Facing up on the Board.
up,
/// Facing down on the Board.
down,
}
/// {@template layer_sensor}
/// [BodyComponent] located at the entrance and exit of a [Layer].
///
/// By default the base [layer] is set to [Layer.board] and the
/// [_outsideZIndex] is set to [ZIndexes.ballOnBoard].
/// {@endtemplate}
abstract class LayerSensor extends BodyComponent
with InitialPosition, Layered, ContactCallbacks {
/// {@macro layer_sensor}
LayerSensor({
required Layer insideLayer,
Layer? outsideLayer,
required int insideZIndex,
int? outsideZIndex,
required this.orientation,
}) : _insideLayer = insideLayer,
_outsideLayer = outsideLayer ?? Layer.board,
_insideZIndex = insideZIndex,
_outsideZIndex = outsideZIndex ?? ZIndexes.ballOnBoard,
super(renderBody: false) {
layer = Layer.opening;
}
final Layer _insideLayer;
final Layer _outsideLayer;
final int _insideZIndex;
final int _outsideZIndex;
/// The [Shape] of the [LayerSensor].
Shape get shape;
/// {@macro layer_entrance_orientation}
// TODO(ruimiguel): Try to remove the need of [LayerEntranceOrientation] for
// collision calculations.
final LayerEntranceOrientation orientation;
@override
Body createBody() {
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
if (other.layer != _insideLayer) {
final isBallEnteringOpening =
(orientation == LayerEntranceOrientation.down &&
other.body.linearVelocity.y < 0) ||
(orientation == LayerEntranceOrientation.up &&
other.body.linearVelocity.y > 0);
if (isBallEnteringOpening) {
other
..layer = _insideLayer
..zIndex = _insideZIndex;
}
} else {
other
..layer = _outsideLayer
..zIndex = _outsideZIndex;
}
}
}

@ -0,0 +1,2 @@
export 'behaviors.dart';
export 'layer_filtering_behavior.dart';

@ -0,0 +1,31 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class LayerFilteringBehavior extends ContactBehavior<LayerSensor> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
if (other.layer != parent.insideLayer) {
final isBallEnteringOpening =
(parent.orientation == LayerEntranceOrientation.down &&
other.body.linearVelocity.y < 0) ||
(parent.orientation == LayerEntranceOrientation.up &&
other.body.linearVelocity.y > 0);
if (isBallEnteringOpening) {
other
..layer = parent.insideLayer
..zIndex = parent.insideZIndex;
}
} else {
other
..layer = parent.outsideLayer
..zIndex = parent.outsideZIndex;
}
}
}

@ -0,0 +1,66 @@
// ignore_for_file: avoid_renaming_method_parameters, public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/layer_sensor/behaviors/layer_filtering_behavior.dart';
/// {@template layer_entrance_orientation}
/// Determines if a layer entrance is oriented [up] or [down] on the board.
/// {@endtemplate}
enum LayerEntranceOrientation {
/// Facing up on the Board.
up,
/// Facing down on the Board.
down,
}
/// {@template layer_sensor}
/// [BodyComponent] located at the entrance and exit of a [Layer].
///
/// By default the base [layer] is set to [Layer.board] and the
/// [outsideZIndex] is set to [ZIndexes.ballOnBoard].
/// {@endtemplate}
abstract class LayerSensor extends BodyComponent with InitialPosition, Layered {
/// {@macro layer_sensor}
LayerSensor({
required this.insideLayer,
Layer? outsideLayer,
required this.insideZIndex,
int? outsideZIndex,
required this.orientation,
}) : outsideLayer = outsideLayer ?? Layer.board,
outsideZIndex = outsideZIndex ?? ZIndexes.ballOnBoard,
super(
renderBody: false,
children: [LayerFilteringBehavior()],
) {
layer = Layer.opening;
}
final Layer insideLayer;
final Layer outsideLayer;
final int insideZIndex;
final int outsideZIndex;
/// The [Shape] of the [LayerSensor].
Shape get shape;
/// {@macro layer_entrance_orientation}
// TODO(ruimiguel): Try to remove the need of [LayerEntranceOrientation] for
// collision calculations.
final LayerEntranceOrientation orientation;
@override
Body createBody() {
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(position: initialPosition);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -23,16 +23,20 @@ class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex {
ScoreComponent({
required this.points,
required Vector2 position,
}) : super(
required EffectController effectController,
}) : _effectController = effectController,
super(
position: position,
anchor: Anchor.center,
) {
zIndex = ZIndexes.score;
}
late Points points;
late final Effect _effect;
late Points points;
final EffectController _effectController;
@override
Future<void> onLoad() async {
@ -46,7 +50,7 @@ class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex {
await add(
_effect = MoveEffect.by(
Vector2(0, -5),
EffectController(duration: 1),
_effectController,
),
);
}

@ -0,0 +1,2 @@
export 'skill_shot_ball_contact_behavior.dart';
export 'skill_shot_blinking_behavior.dart';

@ -0,0 +1,16 @@
// ignore_for_file: public_member_api_docs
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class SkillShotBallContactBehavior extends ContactBehavior<SkillShot> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.bloc.onBallContacted();
parent.firstChild<SpriteAnimationComponent>()?.playing = true;
}
}

@ -0,0 +1,44 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template skill_shot_blinking_behavior}
/// Makes a [SkillShot] blink between [SkillShotSpriteState.lit] and
/// [SkillShotSpriteState.dimmed] for a set amount of blinks.
/// {@endtemplate}
class SkillShotBlinkingBehavior extends TimerComponent
with ParentIsA<SkillShot> {
/// {@macro skill_shot_blinking_behavior}
SkillShotBlinkingBehavior() : super(period: 0.15);
final _maxBlinks = 4;
int _blinks = 0;
void _onNewState(SkillShotState state) {
if (state.isBlinking) {
timer
..reset()
..start();
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
timer.stop();
parent.bloc.stream.listen(_onNewState);
}
@override
void onTick() {
super.onTick();
if (_blinks != _maxBlinks * 2) {
parent.bloc.switched();
_blinks++;
} else {
_blinks = 0;
timer.stop();
parent.bloc.onBlinkingFinished();
}
}
}

@ -0,0 +1,39 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'skill_shot_state.dart';
class SkillShotCubit extends Cubit<SkillShotState> {
SkillShotCubit() : super(const SkillShotState.initial());
void onBallContacted() {
emit(
const SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
),
);
}
void switched() {
switch (state.spriteState) {
case SkillShotSpriteState.lit:
emit(state.copyWith(spriteState: SkillShotSpriteState.dimmed));
break;
case SkillShotSpriteState.dimmed:
emit(state.copyWith(spriteState: SkillShotSpriteState.lit));
break;
}
}
void onBlinkingFinished() {
emit(
const SkillShotState(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: false,
),
);
}
}

@ -0,0 +1,37 @@
// ignore_for_file: public_member_api_docs
part of 'skill_shot_cubit.dart';
enum SkillShotSpriteState {
lit,
dimmed,
}
class SkillShotState extends Equatable {
const SkillShotState({
required this.spriteState,
required this.isBlinking,
});
const SkillShotState.initial()
: this(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: false,
);
final SkillShotSpriteState spriteState;
final bool isBlinking;
SkillShotState copyWith({
SkillShotSpriteState? spriteState,
bool? isBlinking,
}) =>
SkillShotState(
spriteState: spriteState ?? this.spriteState,
isBlinking: isBlinking ?? this.isBlinking,
);
@override
List<Object?> get props => [spriteState, isBlinking];
}

@ -0,0 +1,169 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/skill_shot/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/skill_shot_cubit.dart';
/// {@template skill_shot}
/// Rollover awarding extra points.
/// {@endtemplate}
class SkillShot extends BodyComponent with ZIndex {
/// {@macro skill_shot}
SkillShot({Iterable<Component>? children})
: this._(
children: children,
bloc: SkillShotCubit(),
);
SkillShot._({
Iterable<Component>? children,
required this.bloc,
}) : super(
renderBody: false,
children: [
SkillShotBallContactBehavior(),
SkillShotBlinkingBehavior(),
_RolloverDecalSpriteComponent(),
PinSpriteAnimationComponent(),
_TextDecalSpriteGroupComponent(state: bloc.state.spriteState),
...?children,
],
) {
zIndex = ZIndexes.decal;
}
/// Creates a [SkillShot] without any children.
///
/// This can be used for testing [SkillShot]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting
SkillShot.test({
required this.bloc,
});
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
final SkillShotCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
@override
Body createBody() {
final shape = PolygonShape()
..setAsBox(
0.1,
3.7,
Vector2(-31.9, 9.1),
0.11,
);
final fixtureDef = FixtureDef(shape, isSensor: true);
return world.createBody(BodyDef())..createFixture(fixtureDef);
}
}
class _RolloverDecalSpriteComponent extends SpriteComponent with HasGameRef {
_RolloverDecalSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(-31.9, 9.1),
angle: 0.11,
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.skillShot.decal.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 20;
}
}
/// {@template pin_sprite_animation_component}
/// Animation for pin in [SkillShot] rollover.
/// {@endtemplate}
@visibleForTesting
class PinSpriteAnimationComponent extends SpriteAnimationComponent
with HasGameRef {
/// {@macro pin_sprite_animation_component}
PinSpriteAnimationComponent()
: super(
anchor: Anchor.center,
position: Vector2(-31.9, 9.1),
angle: 0,
playing: false,
);
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = gameRef.images.fromCache(
Assets.images.skillShot.pin.keyName,
);
const amountPerRow = 3;
const amountPerColumn = 1;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
loop: false,
),
)..onComplete = () {
animation?.reset();
playing = false;
};
}
}
class _TextDecalSpriteGroupComponent
extends SpriteGroupComponent<SkillShotSpriteState>
with HasGameRef, ParentIsA<SkillShot> {
_TextDecalSpriteGroupComponent({
required SkillShotSpriteState state,
}) : super(
anchor: Anchor.center,
position: Vector2(-35.55, 3.59),
current: state,
);
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen((state) => current = state.spriteState);
final sprites = {
SkillShotSpriteState.lit: Sprite(
gameRef.images.fromCache(Assets.images.skillShot.lit.keyName),
),
SkillShotSpriteState.dimmed: Sprite(
gameRef.images.fromCache(Assets.images.skillShot.dimmed.keyName),
),
};
this.sprites = sprites;
size = sprites[current]!.originalSize / 10;
}
}

@ -0,0 +1,24 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ramp_ball_ascending_contact_behavior}
/// Detects an ascending [Ball] that enters into the [SpaceshipRamp].
///
/// The [Ball] can hit with sensor to recognize if a [Ball] goes into or out of
/// the [SpaceshipRamp].
/// {@endtemplate}
class RampBallAscendingContactBehavior
extends ContactBehavior<RampScoringSensor> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
if (other.body.linearVelocity.y < 0) {
parent.parent.bloc.onAscendingBallEntered();
}
}
}

@ -0,0 +1,16 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'spaceship_ramp_state.dart';
class SpaceshipRampCubit extends Cubit<SpaceshipRampState> {
SpaceshipRampCubit() : super(const SpaceshipRampState.initial());
void onAscendingBallEntered() {
emit(
state.copyWith(hits: state.hits + 1),
);
}
}

@ -0,0 +1,24 @@
// ignore_for_file: public_member_api_docs
part of 'spaceship_ramp_cubit.dart';
class SpaceshipRampState extends Equatable {
const SpaceshipRampState({
required this.hits,
}) : assert(hits >= 0, "Hits can't be negative");
const SpaceshipRampState.initial() : this(hits: 0);
final int hits;
SpaceshipRampState copyWith({
int? hits,
}) {
return SpaceshipRampState(
hits: hits ?? this.hits,
);
}
@override
List<Object?> get props => [hits];
}

@ -5,16 +5,35 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/spaceship_ramp_cubit.dart';
/// {@template spaceship_ramp}
/// Ramp leading into the [AndroidSpaceship].
/// {@endtemplate}
class SpaceshipRamp extends Component {
/// {@macro spaceship_ramp}
SpaceshipRamp()
: super(
SpaceshipRamp({
Iterable<Component>? children,
}) : this._(
children: children,
bloc: SpaceshipRampCubit(),
);
SpaceshipRamp._({
Iterable<Component>? children,
required this.bloc,
}) : super(
children: [
// TODO(ruimiguel): refactor RampScoringSensor and
// _SpaceshipRampOpening to be in only one sensor if possible.
RampScoringSensor(
children: [
RampBallAscendingContactBehavior(),
],
)..initialPosition = Vector2(1.7, -20.4),
_SpaceshipRampOpening(
outsidePriority: ZIndexes.ballOnBoard,
rotation: math.pi,
@ -34,60 +53,30 @@ class SpaceshipRamp extends Component {
_SpaceshipRampForegroundRailing(),
_SpaceshipRampBase()..initialPosition = Vector2(1.7, -20),
_SpaceshipRampBackgroundRailingSpriteComponent(),
_SpaceshipRampArrowSpriteComponent(),
SpaceshipRampArrowSpriteComponent(
current: bloc.state.hits,
),
...?children,
],
);
/// Forwards the sprite to the next [SpaceshipRampArrowSpriteState].
/// Creates a [SpaceshipRamp] without any children.
///
/// If the current state is the last one it cycles back to the initial state.
void progress() =>
firstChild<_SpaceshipRampArrowSpriteComponent>()?.progress();
}
/// Indicates the state of the arrow on the [SpaceshipRamp].
@visibleForTesting
enum SpaceshipRampArrowSpriteState {
/// Arrow with no dashes lit up.
inactive,
/// Arrow with 1 light lit up.
active1,
/// This can be used for testing [SpaceshipRamp]'s behaviors in isolation.
@visibleForTesting
SpaceshipRamp.test({
required this.bloc,
}) : super();
/// Arrow with 2 lights lit up.
active2,
/// Arrow with 3 lights lit up.
active3,
/// Arrow with 4 lights lit up.
active4,
/// Arrow with all 5 lights lit up.
active5,
}
extension on SpaceshipRampArrowSpriteState {
String get path {
switch (this) {
case SpaceshipRampArrowSpriteState.inactive:
return Assets.images.android.ramp.arrow.inactive.keyName;
case SpaceshipRampArrowSpriteState.active1:
return Assets.images.android.ramp.arrow.active1.keyName;
case SpaceshipRampArrowSpriteState.active2:
return Assets.images.android.ramp.arrow.active2.keyName;
case SpaceshipRampArrowSpriteState.active3:
return Assets.images.android.ramp.arrow.active3.keyName;
case SpaceshipRampArrowSpriteState.active4:
return Assets.images.android.ramp.arrow.active4.keyName;
case SpaceshipRampArrowSpriteState.active5:
return Assets.images.android.ramp.arrow.active5.keyName;
}
}
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
final SpaceshipRampCubit bloc;
SpaceshipRampArrowSpriteState get next {
return SpaceshipRampArrowSpriteState
.values[(index + 1) % SpaceshipRampArrowSpriteState.values.length];
@override
void onRemove() {
bloc.close();
super.onRemove();
}
}
@ -194,37 +183,81 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent
///
/// Lights progressively whenever a [Ball] gets into [SpaceshipRamp].
/// {@endtemplate}
class _SpaceshipRampArrowSpriteComponent
extends SpriteGroupComponent<SpaceshipRampArrowSpriteState>
with HasGameRef, ZIndex {
@visibleForTesting
class SpaceshipRampArrowSpriteComponent extends SpriteGroupComponent<int>
with HasGameRef, ParentIsA<SpaceshipRamp>, ZIndex {
/// {@macro spaceship_ramp_arrow_sprite_component}
_SpaceshipRampArrowSpriteComponent()
: super(
SpaceshipRampArrowSpriteComponent({
required int current,
}) : super(
anchor: Anchor.center,
position: Vector2(-3.9, -56.5),
current: current,
) {
zIndex = ZIndexes.spaceshipRampArrow;
}
/// Changes arrow image to the next [Sprite].
void progress() => current = current?.next;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprites = <SpaceshipRampArrowSpriteState, Sprite>{};
parent.bloc.stream.listen((state) {
current = state.hits % SpaceshipRampArrowSpriteState.values.length;
});
final sprites = <int, Sprite>{};
this.sprites = sprites;
for (final spriteState in SpaceshipRampArrowSpriteState.values) {
sprites[spriteState] = Sprite(
sprites[spriteState.index] = Sprite(
gameRef.images.fromCache(spriteState.path),
);
}
current = SpaceshipRampArrowSpriteState.inactive;
current = 0;
size = sprites[current]!.originalSize / 10;
}
}
/// Indicates the state of the arrow on the [SpaceshipRamp].
@visibleForTesting
enum SpaceshipRampArrowSpriteState {
/// Arrow with no dashes lit up.
inactive,
/// Arrow with 1 light lit up.
active1,
/// Arrow with 2 lights lit up.
active2,
/// Arrow with 3 lights lit up.
active3,
/// Arrow with 4 lights lit up.
active4,
/// Arrow with all 5 lights lit up.
active5,
}
extension on SpaceshipRampArrowSpriteState {
String get path {
switch (this) {
case SpaceshipRampArrowSpriteState.inactive:
return Assets.images.android.ramp.arrow.inactive.keyName;
case SpaceshipRampArrowSpriteState.active1:
return Assets.images.android.ramp.arrow.active1.keyName;
case SpaceshipRampArrowSpriteState.active2:
return Assets.images.android.ramp.arrow.active2.keyName;
case SpaceshipRampArrowSpriteState.active3:
return Assets.images.android.ramp.arrow.active3.keyName;
case SpaceshipRampArrowSpriteState.active4:
return Assets.images.android.ramp.arrow.active4.keyName;
case SpaceshipRampArrowSpriteState.active5:
return Assets.images.android.ramp.arrow.active5.keyName;
}
}
}
class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex {
_SpaceshipRampBoardOpeningSpriteComponent() : super(anchor: Anchor.center) {
@ -373,3 +406,47 @@ class _SpaceshipRampOpening extends LayerSensor {
);
}
}
/// {@template ramp_scoring_sensor}
/// Small sensor body used to detect when a ball has entered the
/// [SpaceshipRamp].
/// {@endtemplate}
class RampScoringSensor extends BodyComponent
with ParentIsA<SpaceshipRamp>, InitialPosition, Layered {
/// {@macro ramp_scoring_sensor}
RampScoringSensor({
Iterable<Component>? children,
}) : super(
children: children,
renderBody: false,
) {
layer = Layer.spaceshipEntranceRamp;
}
/// Creates a [RampScoringSensor] without any children.
///
@visibleForTesting
RampScoringSensor.test();
@override
Body createBody() {
final shape = PolygonShape()
..setAsBox(
2.6,
.5,
initialPosition,
-5 * math.pi / 180,
);
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -32,18 +32,18 @@ class _ComputerBase extends BodyComponent with InitialPosition, ZIndex {
List<FixtureDef> _createFixtureDefs() {
final leftEdge = EdgeShape()
..set(
Vector2(-14.9, -46),
Vector2(-15.3, -49.6),
Vector2(-15.1, -45.9),
Vector2(-15.5, -49.5),
);
final topEdge = EdgeShape()
..set(
Vector2(-15.3, -49.6),
Vector2(-10.7, -50.6),
leftEdge.vertex2,
Vector2(-10.9, -50.5),
);
final rightEdge = EdgeShape()
..set(
Vector2(-10.7, -50.6),
Vector2(-9, -47.2),
topEdge.vertex2,
Vector2(-9.2, -47.1),
);
return [
@ -67,7 +67,7 @@ class _ComputerBaseSpriteComponent extends SpriteComponent with HasGameRef {
_ComputerBaseSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(-12.1, -48.15),
position: Vector2(-12.24, -48.15),
);
@override
@ -89,7 +89,7 @@ class _ComputerTopSpriteComponent extends SpriteComponent
_ComputerTopSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(-12.52, -49.37),
position: Vector2(-12.66, -49.37),
) {
zIndex = ZIndexes.computerTop;
}
@ -113,9 +113,9 @@ class _ComputerGlowSpriteComponent extends SpriteComponent
_ComputerGlowSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(7.4, 10),
position: Vector2(4.2, 11),
) {
zIndex = ZIndexes.computerGlow;
zIndex = ZIndexes.computerGlow + 4;
}
@override

@ -114,5 +114,10 @@ abstract class ZIndexes {
static const score = _above + spaceshipRampForegroundRailing;
// Debug information
static const debugInfo = _above + score;
// Backbox
static const backbox = _below + outerBoundary;
}

@ -73,7 +73,6 @@ flutter:
- assets/images/sparky/bumper/a/
- assets/images/sparky/bumper/b/
- assets/images/sparky/bumper/c/
- assets/images/backboard/
- assets/images/google_word/letter1/
- assets/images/google_word/letter2/
- assets/images/google_word/letter3/
@ -88,7 +87,9 @@ flutter:
- assets/images/multiplier/x5/
- assets/images/multiplier/x6/
- assets/images/score/
- assets/images/backbox/
- assets/images/flapper/
- assets/images/skill_shot/
flutter_gen:
line_length: 80

@ -24,6 +24,14 @@ abstract class AssetsGame extends Forge2DGame {
}
abstract class LineGame extends AssetsGame with PanDetector {
LineGame({
List<String>? imagesFileNames,
}) : super(
imagesFileNames: [
if (imagesFileNames != null) ...imagesFileNames,
],
);
Vector2? _lineEnd;
@override

@ -18,7 +18,6 @@ void main() {
addGoogleWordStories(dashbook);
addLaunchRampStories(dashbook);
addScoreStories(dashbook);
addBackboardStories(dashbook);
addMultiballStories(dashbook);
addMultipliersStories(dashbook);

@ -7,7 +7,6 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart';
class AndroidBumperAGame extends BallGame {
AndroidBumperAGame()
: super(
color: const Color(0xFF0000FF),
imagesFileNames: [
Assets.images.android.bumper.a.lit.keyName,
Assets.images.android.bumper.a.dimmed.keyName,

@ -7,7 +7,6 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart';
class AndroidBumperBGame extends BallGame {
AndroidBumperBGame()
: super(
color: const Color(0xFF0000FF),
imagesFileNames: [
Assets.images.android.bumper.b.lit.keyName,
Assets.images.android.bumper.b.dimmed.keyName,

@ -1,14 +1,12 @@
import 'dart:async';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SpaceshipRailGame extends BallGame {
SpaceshipRailGame()
: super(
color: Colors.blue,
ballPriority: ZIndexes.ballOnSpaceshipRail,
ballLayer: Layer.spaceshipExitRail,
imagesFileNames: [

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save