Merge branch 'main' into feat/dino-mechanics

pull/277/head
Allison Ryan 3 years ago
commit db22bb4a40

@ -1,5 +1,9 @@
name: geometry
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
paths:

@ -1,5 +1,9 @@
name: leaderboard_repository
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
paths:

@ -1,5 +1,9 @@
name: pinball
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on: [pull_request, push]
jobs:

@ -1,5 +1,9 @@
name: pinball_audio
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
paths:

@ -1,5 +1,9 @@
name: pinball_components
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
paths:

@ -1,5 +1,9 @@
name: pinball_flame
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
paths:

@ -1,5 +1,9 @@
name: pinball_theme
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
paths:

Before

Width:  |  Height:  |  Size: 306 KiB

After

Width:  |  Height:  |  Size: 306 KiB

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@ -13,7 +13,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart';
class App extends StatelessWidget {
@ -36,7 +36,7 @@ class App extends StatelessWidget {
RepositoryProvider.value(value: _pinballAudio),
],
child: BlocProvider(
create: (context) => ThemeCubit(),
create: (context) => CharacterThemeCubit(),
child: const MaterialApp(
title: 'I/O Pinball',
localizationsDelegates: [

@ -1,5 +1,5 @@
// ignore_for_file: public_member_api_docs
import 'dart:math' as math;
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
@ -9,19 +9,41 @@ part 'game_state.dart';
class GameBloc extends Bloc<GameEvent, GameState> {
GameBloc() : super(const GameState.initial()) {
on<BallLost>(_onBallLost);
on<RoundLost>(_onRoundLost);
on<Scored>(_onScored);
on<MultiplierIncreased>(_onIncreasedMultiplier);
on<BonusActivated>(_onBonusActivated);
on<SparkyTurboChargeActivated>(_onSparkyTurboChargeActivated);
}
void _onBallLost(BallLost event, Emitter emit) {
emit(state.copyWith(balls: state.balls - 1));
void _onRoundLost(RoundLost event, Emitter emit) {
final score = state.score * state.multiplier;
final roundsLeft = math.max(state.rounds - 1, 0);
emit(
state.copyWith(
score: score,
multiplier: 1,
rounds: roundsLeft,
),
);
}
void _onScored(Scored event, Emitter emit) {
if (!state.isGameOver) {
emit(state.copyWith(score: state.score + event.points));
emit(
state.copyWith(score: state.score + event.points),
);
}
}
void _onIncreasedMultiplier(MultiplierIncreased event, Emitter emit) {
if (!state.isGameOver) {
emit(
state.copyWith(
multiplier: math.min(state.multiplier + 1, 6),
),
);
}
}

@ -7,12 +7,12 @@ abstract class GameEvent extends Equatable {
const GameEvent();
}
/// {@template ball_lost_game_event}
/// Event added when a user drops a ball off the screen.
/// {@template round_lost_game_event}
/// Event added when a user drops all balls off the screen and loses a round.
/// {@endtemplate}
class BallLost extends GameEvent {
/// {@macro ball_lost_game_event}
const BallLost();
class RoundLost extends GameEvent {
/// {@macro round_lost_game_event}
const RoundLost();
@override
List<Object?> get props => [];
@ -48,3 +48,14 @@ class SparkyTurboChargeActivated extends GameEvent {
@override
List<Object?> get props => [];
}
/// {@template multiplier_increased_game_event}
/// Added when a multiplier is gained.
/// {@endtemplate}
class MultiplierIncreased extends GameEvent {
/// {@macro multiplier_increased_game_event}
const MultiplierIncreased();
@override
List<Object?> get props => [];
}

@ -12,6 +12,12 @@ enum GameBonus {
/// Bonus achieved when a ball enters Sparky's computer.
sparkyTurboCharge,
/// Bonus achieved when the ball goes in the dino mouth.
dinoChomp,
/// Bonus achieved when a ball enters the android spaceship.
androidSpaceship,
}
/// {@template game_state}
@ -21,34 +27,42 @@ class GameState extends Equatable {
/// {@macro game_state}
const GameState({
required this.score,
required this.balls,
required this.multiplier,
required this.rounds,
required this.bonusHistory,
}) : assert(score >= 0, "Score can't be negative"),
assert(balls >= 0, "Number of balls 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,
balls = 3,
multiplier = 1,
rounds = 3,
bonusHistory = const [];
/// The current score of the game.
final int score;
/// The number of balls left in the game.
/// The current multiplier for the score.
final int multiplier;
/// The number of rounds left in the game.
///
/// When the number of balls is 0, the game is over.
final int balls;
/// When the number of rounds is 0, the game is over.
final int rounds;
/// Holds the history of all the [GameBonus]es earned by the player during a
/// PinballGame.
final List<GameBonus> bonusHistory;
/// Determines when the game is over.
bool get isGameOver => balls == 0;
bool get isGameOver => rounds == 0;
GameState copyWith({
int? score,
int? multiplier,
int? balls,
int? rounds,
List<GameBonus>? bonusHistory,
}) {
assert(
@ -58,7 +72,8 @@ class GameState extends Equatable {
return GameState(
score: score ?? this.score,
balls: balls ?? this.balls,
multiplier: multiplier ?? this.multiplier,
rounds: rounds ?? this.rounds,
bonusHistory: bonusHistory ?? this.bonusHistory,
);
}
@ -66,7 +81,8 @@ class GameState extends Equatable {
@override
List<Object?> get props => [
score,
balls,
multiplier,
rounds,
bonusHistory,
];
}

@ -1,60 +0,0 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template alien_zone}
/// Area positioned below [Spaceship] where the [Ball]
/// can bounce off [AlienBumper]s.
///
/// When a [Ball] hits an [AlienBumper], the bumper animates.
/// {@endtemplate}
class AlienZone extends Component with HasGameRef<PinballGame> {
/// {@macro alien_zone}
AlienZone();
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(AlienBumperBallContactCallback());
final lowerBumper = _AlienBumper.a()
..initialPosition = Vector2(-32.52, -9.1);
final upperBumper = _AlienBumper.b()
..initialPosition = Vector2(-22.89, -17.35);
await addAll([
lowerBumper,
upperBumper,
]);
}
}
// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
// ContactCallback process is enhanced.
class _AlienBumper extends AlienBumper with ScorePoints {
_AlienBumper.a() : super.a();
_AlienBumper.b() : super.b();
@override
int get points => 20;
}
/// Listens when a [Ball] bounces against an [AlienBumper].
@visibleForTesting
class AlienBumperBallContactCallback
extends ContactCallback<AlienBumper, Ball> {
@override
void begin(
AlienBumper alienBumper,
Ball _,
Contact __,
) {
alienBumper.animate();
}
}

@ -0,0 +1,34 @@
// ignore_for_file: avoid_renaming_method_parameters
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 android_acres}
/// Area positioned on the left side of the board containing the [Spaceship],
/// [SpaceshipRamp], [SpaceshipRail], and [AndroidBumper]s.
/// {@endtemplate}
class AndroidAcres extends Blueprint {
/// {@macro android_acres}
AndroidAcres()
: super(
components: [
AndroidBumper.a(
children: [
ScoringBehavior(points: 20),
],
)..initialPosition = Vector2(-32.52, -9.1),
AndroidBumper.b(
children: [
ScoringBehavior(points: 20),
],
)..initialPosition = Vector2(-22.89, -17.35),
],
blueprints: [
SpaceshipRamp(),
Spaceship(position: Vector2(-26.5, -28.5)),
SpaceshipRail(),
],
);
}

@ -1,13 +1,13 @@
export 'alien_zone.dart';
export 'android_acres.dart';
export 'board.dart';
export 'camera_controller.dart';
export 'controlled_ball.dart';
export 'controlled_flipper.dart';
export 'controlled_plunger.dart';
export 'flutter_forest.dart';
export 'flutter_forest/flutter_forest.dart';
export 'game_flow_controller.dart';
export 'google_word.dart';
export 'google_word/google_word.dart';
export 'launcher.dart';
export 'score_points.dart';
export 'scoring_behavior.dart';
export 'sparky_fire_zone.dart';
export 'wall.dart';

@ -1,5 +1,4 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/forge2d_game.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
@ -8,15 +7,15 @@ import 'package:pinball_theme/pinball_theme.dart';
/// {@template controlled_ball}
/// A [Ball] with a [BallController] attached.
///
/// When a [Ball] is lost, if there aren't more [Ball]s in play and the game is
/// not over, a new [Ball] will be spawned.
/// {@endtemplate}
class ControlledBall extends Ball with Controls<BallController> {
/// A [Ball] that launches from the [Plunger].
///
/// When a launched [Ball] is lost, it will decrease the [GameState.balls]
/// count, and a new [Ball] is spawned.
ControlledBall.launch({
required PinballTheme theme,
}) : super(baseColor: theme.characterTheme.ballColor) {
required CharacterTheme characterTheme,
}) : super(baseColor: characterTheme.ballColor) {
controller = BallController(this);
priority = RenderPriority.ballOnLaunchRamp;
layer = Layer.launcher;
@ -24,19 +23,17 @@ class ControlledBall extends Ball with Controls<BallController> {
/// {@template bonus_ball}
/// {@macro controlled_ball}
///
/// When a bonus [Ball] is lost, the [GameState.balls] doesn't change.
/// {@endtemplate}
ControlledBall.bonus({
required PinballTheme theme,
}) : super(baseColor: theme.characterTheme.ballColor) {
required CharacterTheme characterTheme,
}) : super(baseColor: characterTheme.ballColor) {
controller = BallController(this);
priority = RenderPriority.ballOnBoard;
}
/// [Ball] used in [DebugPinballGame].
ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) {
controller = DebugBallController(this);
controller = BallController(this);
priority = RenderPriority.ballOnBoard;
}
}
@ -49,10 +46,8 @@ class BallController extends ComponentController<Ball>
/// {@macro ball_controller}
BallController(Ball ball) : super(ball);
/// Removes the [Ball] from a [PinballGame].
///
/// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into
/// a [BottomWall].
/// Event triggered when the ball is lost.
// TODO(alestiago): Refactor using behaviors.
void lost() {
component.shouldRemove = true;
}
@ -76,15 +71,9 @@ class BallController extends ComponentController<Ball>
@override
void onRemove() {
super.onRemove();
gameRef.read<GameBloc>().add(const BallLost());
final noBallsLeft = gameRef.descendants().whereType<Ball>().isEmpty;
if (noBallsLeft) {
gameRef.read<GameBloc>().add(const RoundLost());
}
}
}
/// {@macro ball_controller}
class DebugBallController extends BallController {
/// {@macro ball_controller}
DebugBallController(Ball<Forge2DGame> component) : super(component);
@override
void onRemove() {}
}

@ -1,102 +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 flutter_forest}
/// Area positioned at the top right of the [Board] where the [Ball]
/// can bounce off [DashNestBumper]s.
///
/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest]
/// is awarded, and the [DashNestBumper.main] releases a new [Ball].
/// {@endtemplate}
class FlutterForest extends Component
with Controls<_FlutterForestController>, HasGameRef<PinballGame> {
/// {@macro flutter_forest}
FlutterForest() {
controller = _FlutterForestController(this);
}
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(_DashNestBumperBallContactCallback());
final signpost = Signpost()..initialPosition = Vector2(8.35, -58.3);
final bigNest = _DashNestBumper.main()
..initialPosition = Vector2(18.55, -59.35);
final smallLeftNest = _DashNestBumper.a()
..initialPosition = Vector2(8.95, -51.95);
final smallRightNest = _DashNestBumper.b()
..initialPosition = Vector2(23.3, -46.75);
final dashAnimatronic = DashAnimatronic()..position = Vector2(20, -66);
await addAll([
signpost,
smallLeftNest,
smallRightNest,
bigNest,
dashAnimatronic,
]);
}
}
class _FlutterForestController extends ComponentController<FlutterForest>
with HasGameRef<PinballGame> {
_FlutterForestController(FlutterForest flutterForest) : super(flutterForest);
final _activatedBumpers = <DashNestBumper>{};
void activateBumper(DashNestBumper dashNestBumper) {
if (!_activatedBumpers.add(dashNestBumper)) return;
dashNestBumper.activate();
final activatedBonus = _activatedBumpers.length == 3;
if (activatedBonus) {
_addBonusBall();
gameRef.read<GameBloc>().add(const BonusActivated(GameBonus.dashNest));
_activatedBumpers
..forEach((bumper) => bumper.deactivate())
..clear();
component.firstChild<DashAnimatronic>()?.playing = true;
}
}
Future<void> _addBonusBall() async {
await gameRef.add(
ControlledBall.bonus(theme: gameRef.theme)
..initialPosition = Vector2(17.2, -52.7),
);
}
}
// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
// ContactCallback process is enhanced.
class _DashNestBumper extends DashNestBumper with ScorePoints {
_DashNestBumper.main() : super.main();
_DashNestBumper.a() : super.a();
_DashNestBumper.b() : super.b();
@override
int get points => 20;
}
class _DashNestBumperBallContactCallback
extends ContactCallback<DashNestBumper, Ball> {
@override
void begin(DashNestBumper dashNestBumper, _, __) {
final parent = dashNestBumper.parent;
if (parent is FlutterForest) {
parent.controller.activateBumper(dashNestBumper);
}
}
}

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

@ -0,0 +1,41 @@
import 'package:flame/components.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest]
/// is awarded, and the [DashNestBumper.main] releases a new [Ball].
class FlutterForestBonusBehavior extends Component
with ParentIsA<FlutterForest>, HasGameRef<PinballGame> {
@override
void onMount() {
super.onMount();
final bumpers = parent.children.whereType<DashNestBumper>();
for (final bumper in bumpers) {
// TODO(alestiago): Refactor subscription management once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1538
bumper.bloc.stream.listen((state) {
final achievedBonus = bumpers.every(
(bumper) => bumper.bloc.state == DashNestBumperState.active,
);
if (achievedBonus) {
gameRef
.read<GameBloc>()
.add(const BonusActivated(GameBonus.dashNest));
gameRef.add(
ControlledBall.bonus(characterTheme: gameRef.characterTheme)
..initialPosition = Vector2(17.2, -52.7),
);
parent.firstChild<DashAnimatronic>()?.playing = true;
for (final bumper in bumpers) {
bumper.bloc.onReset();
}
}
});
}
}
}

@ -0,0 +1,49 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template flutter_forest}
/// Area positioned at the top right of the [Board] where the [Ball] can bounce
/// off [DashNestBumper]s.
/// {@endtemplate}
class FlutterForest extends Component {
/// {@macro flutter_forest}
FlutterForest()
: super(
priority: RenderPriority.flutterForest,
children: [
Signpost(
children: [
ScoringBehavior(points: 20),
],
)..initialPosition = Vector2(8.35, -58.3),
DashNestBumper.main(
children: [
ScoringBehavior(points: 20),
],
)..initialPosition = Vector2(18.55, -59.35),
DashNestBumper.a(
children: [
ScoringBehavior(points: 20),
],
)..initialPosition = Vector2(8.95, -51.95),
DashNestBumper.b(
children: [
ScoringBehavior(points: 20),
],
)..initialPosition = Vector2(23.3, -46.75),
DashAnimatronic()..position = Vector2(20, -66),
FlutterForestBonusBehavior(),
],
);
/// Creates a [FlutterForest] without any children.
///
/// This can be used for testing [FlutterForest]'s behaviors in isolation.
@visibleForTesting
FlutterForest.test();
}

@ -32,8 +32,7 @@ class GameFlowController extends ComponentController<PinballGame>
// next page
component.firstChild<Backboard>()?.gameOverMode(
score: state?.score ?? 0,
characterIconPath:
component.theme.characterTheme.leaderboardIcon.keyName,
characterIconPath: component.characterTheme.leaderboardIcon.keyName,
);
component.firstChild<CameraController>()?.focusOnBackboard();
}

@ -1,83 +0,0 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'dart:async';
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 google_word}
/// Loads all [GoogleLetter]s to compose a [GoogleWord].
/// {@endtemplate}
class GoogleWord extends Component
with HasGameRef<PinballGame>, Controls<_GoogleWordController> {
/// {@macro google_word}
GoogleWord({
required Vector2 position,
}) : _position = position {
controller = _GoogleWordController(this);
}
final Vector2 _position;
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(_GoogleLetterBallContactCallback());
final offsets = [
Vector2(-12.92, 1.82),
Vector2(-8.33, -0.65),
Vector2(-2.88, -1.75),
Vector2(2.88, -1.75),
Vector2(8.33, -0.65),
Vector2(12.92, 1.82),
];
final letters = <GoogleLetter>[];
for (var index = 0; index < offsets.length; index++) {
letters.add(
GoogleLetter(index)..initialPosition = _position + offsets[index],
);
}
await addAll(letters);
}
}
class _GoogleWordController extends ComponentController<GoogleWord>
with HasGameRef<PinballGame> {
_GoogleWordController(GoogleWord googleWord) : super(googleWord);
final _activatedLetters = <GoogleLetter>{};
void activate(GoogleLetter googleLetter) {
if (!_activatedLetters.add(googleLetter)) return;
googleLetter.activate();
final activatedBonus = _activatedLetters.length == 6;
if (activatedBonus) {
gameRef.audio.googleBonus();
gameRef.read<GameBloc>().add(const BonusActivated(GameBonus.googleWord));
component.children.whereType<GoogleLetter>().forEach(
(letter) => letter.deactivate(),
);
_activatedLetters.clear();
}
}
}
/// Activates a [GoogleLetter] when it contacts with a [Ball].
class _GoogleLetterBallContactCallback
extends ContactCallback<GoogleLetter, Ball> {
@override
void begin(GoogleLetter googleLetter, _, __) {
final parent = googleLetter.parent;
if (parent is GoogleWord) {
parent.controller.activate(googleLetter);
}
}
}

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

@ -0,0 +1,34 @@
import 'package:flame/components.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Adds a [GameBonus.googleWord] when all [GoogleLetter]s are activated.
class GoogleWordBonusBehavior extends Component
with HasGameRef<PinballGame>, ParentIsA<GoogleWord> {
@override
void onMount() {
super.onMount();
final googleLetters = parent.children.whereType<GoogleLetter>();
for (final letter in googleLetters) {
// TODO(alestiago): Refactor subscription management once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1538
letter.bloc.stream.listen((_) {
final achievedBonus = googleLetters
.every((letter) => letter.bloc.state == GoogleLetterState.active);
if (achievedBonus) {
gameRef.audio.googleBonus();
gameRef
.read<GameBloc>()
.add(const BonusActivated(GameBonus.googleWord));
for (final letter in googleLetters) {
letter.bloc.onReset();
}
}
});
}
}
}

@ -0,0 +1,30 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/components/google_word/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template google_word}
/// Loads all [GoogleLetter]s to compose a [GoogleWord].
/// {@endtemplate}
class GoogleWord extends Component {
/// {@macro google_word}
GoogleWord({
required Vector2 position,
}) : super(
children: [
GoogleLetter(0)..initialPosition = position + Vector2(-12.92, 1.82),
GoogleLetter(1)..initialPosition = position + Vector2(-8.33, -0.65),
GoogleLetter(2)..initialPosition = position + Vector2(-2.88, -1.75),
GoogleLetter(3)..initialPosition = position + Vector2(2.88, -1.75),
GoogleLetter(4)..initialPosition = position + Vector2(8.33, -0.65),
GoogleLetter(5)..initialPosition = position + Vector2(12.92, 1.82),
GoogleWordBonusBehavior(),
],
);
/// Creates a [GoogleWord] without any children.
///
/// This can be used for testing [GoogleWord]'s behaviors in isolation.
@visibleForTesting
GoogleWord.test();
}

@ -1,47 +0,0 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template score_points}
/// Specifies the amount of points received on [Ball] collision.
/// {@endtemplate}
mixin ScorePoints<T extends Forge2DGame> on BodyComponent<T> {
/// {@macro score_points}
int get points;
@override
Future<void> onLoad() async {
await super.onLoad();
body.userData = this;
}
}
/// {@template ball_score_points_callbacks}
/// Adds points to the score when a [Ball] collides with a [BodyComponent] that
/// implements [ScorePoints].
/// {@endtemplate}
class BallScorePointsCallback extends ContactCallback<Ball, ScorePoints> {
/// {@macro ball_score_points_callbacks}
BallScorePointsCallback(PinballGame game) : _gameRef = game;
final PinballGame _gameRef;
@override
void begin(
Ball ball,
ScorePoints scorePoints,
Contact _,
) {
_gameRef.read<GameBloc>().add(Scored(points: scorePoints.points));
_gameRef.audio.score();
_gameRef.add(
ScoreText(
text: scorePoints.points.toString(),
position: ball.body.position,
),
);
}
}

@ -0,0 +1,34 @@
// 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 int points,
}) : _points = points;
final int _points;
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
gameRef.read<GameBloc>().add(Scored(points: _points));
gameRef.audio.score();
gameRef.add(
ScoreText(
text: _points.toString(),
position: other.body.position,
),
);
}
}

@ -1,7 +1,6 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame_forge2d/flame_forge2d.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,9 +16,21 @@ class SparkyFireZone extends Blueprint {
SparkyFireZone()
: super(
components: [
_SparkyBumper.a()..initialPosition = Vector2(-22.9, -41.65),
_SparkyBumper.b()..initialPosition = Vector2(-21.25, -57.9),
_SparkyBumper.c()..initialPosition = Vector2(-3.3, -52.55),
SparkyBumper.a(
children: [
ScoringBehavior(points: 20),
],
)..initialPosition = Vector2(-22.9, -41.65),
SparkyBumper.b(
children: [
ScoringBehavior(points: 20),
],
)..initialPosition = Vector2(-21.25, -57.9),
SparkyBumper.c(
children: [
ScoringBehavior(points: 20),
],
)..initialPosition = Vector2(-3.3, -52.55),
SparkyComputerSensor()..initialPosition = Vector2(-13, -49.8),
SparkyAnimatronic()..position = Vector2(-13.8, -58.2),
],
@ -29,52 +40,14 @@ class SparkyFireZone extends Blueprint {
);
}
// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
// ContactCallback process is enhanced.
class _SparkyBumper extends SparkyBumper with ScorePoints {
_SparkyBumper.a() : super.a();
_SparkyBumper.b() : super.b();
_SparkyBumper.c() : super.c();
@override
int get points => 20;
@override
Future<void> onLoad() async {
await super.onLoad();
// TODO(alestiago): Revisit once this has been merged:
// https://github.com/flame-engine/flame/pull/1547
gameRef.addContactCallback(SparkyBumperBallContactCallback());
}
}
/// Listens when a [Ball] bounces bounces against a [SparkyBumper].
@visibleForTesting
class SparkyBumperBallContactCallback
extends ContactCallback<SparkyBumper, Ball> {
@override
void begin(
SparkyBumper sparkyBumper,
Ball _,
Contact __,
) {
sparkyBumper.animate();
}
}
/// {@template sparky_computer_sensor}
/// Small sensor body used to detect when a ball has entered the
/// [SparkyComputer].
/// {@endtemplate}
// TODO(alestiago): Revisit once this has been merged:
// https://github.com/flame-engine/flame/pull/1547
class SparkyComputerSensor extends BodyComponent with InitialPosition {
class SparkyComputerSensor extends BodyComponent
with InitialPosition, ContactCallbacks {
/// {@macro sparky_computer_sensor}
SparkyComputerSensor() {
renderBody = false;
}
SparkyComputerSensor() : super(renderBody: false);
@override
Body createBody() {
@ -88,23 +61,11 @@ class SparkyComputerSensor extends BodyComponent with InitialPosition {
}
@override
Future<void> onLoad() async {
await super.onLoad();
// TODO(alestiago): Revisit once this has been merged:
// https://github.com/flame-engine/flame/pull/1547
gameRef.addContactCallback(SparkyComputerSensorBallContactCallback());
}
}
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! ControlledBall) return;
@visibleForTesting
// TODO(alestiago): Revisit once this has been merged:
// https://github.com/flame-engine/flame/pull/1547
// ignore: public_member_api_docs
class SparkyComputerSensorBallContactCallback
extends ContactCallback<SparkyComputerSensor, ControlledBall> {
@override
void begin(_, ControlledBall controlledBall, __) {
controlledBall.controller.turboCharge();
controlledBall.gameRef.firstChild<SparkyAnimatronic>()?.playing = true;
other.controller.turboCharge();
gameRef.firstChild<SparkyAnimatronic>()?.playing = true;
}
}

@ -42,25 +42,19 @@ class Wall extends BodyComponent {
/// {@template bottom_wall}
/// [Wall] located at the bottom of the board.
///
/// Collisions with [BottomWall] are listened by
/// [BottomWallBallContactCallback].
/// {@endtemplate}
class BottomWall extends Wall {
class BottomWall extends Wall with ContactCallbacks {
/// {@macro bottom_wall}
BottomWall()
: super(
start: BoardDimensions.bounds.bottomLeft.toVector2(),
end: BoardDimensions.bounds.bottomRight.toVector2(),
);
}
/// {@template bottom_wall_ball_contact_callback}
/// Listens when a [ControlledBall] falls into a [BottomWall].
/// {@endtemplate}
class BottomWallBallContactCallback
extends ContactCallback<ControlledBall, BottomWall> {
@override
void begin(ControlledBall ball, BottomWall wall, Contact contact) {
ball.controller.lost();
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! ControlledBall) return;
other.controller.lost();
}
}

@ -78,11 +78,11 @@ extension PinballGameAssetsX on PinballGame {
components.Assets.images.spaceship.ramp.arrow.active5.keyName,
),
images.load(components.Assets.images.spaceship.rail.main.keyName),
images.load(components.Assets.images.spaceship.rail.foreground.keyName),
images.load(components.Assets.images.alienBumper.a.active.keyName),
images.load(components.Assets.images.alienBumper.a.inactive.keyName),
images.load(components.Assets.images.alienBumper.b.active.keyName),
images.load(components.Assets.images.alienBumper.b.inactive.keyName),
images.load(components.Assets.images.spaceship.rail.exit.keyName),
images.load(components.Assets.images.androidBumper.a.lit.keyName),
images.load(components.Assets.images.androidBumper.a.dimmed.keyName),
images.load(components.Assets.images.androidBumper.b.lit.keyName),
images.load(components.Assets.images.androidBumper.b.dimmed.keyName),
images.load(components.Assets.images.sparky.computer.top.keyName),
images.load(components.Assets.images.sparky.computer.base.keyName),
images.load(components.Assets.images.sparky.animatronic.keyName),

@ -20,7 +20,7 @@ class PinballGame extends Forge2DGame
HasKeyboardHandlerComponents,
Controls<_GameBallsController> {
PinballGame({
required this.theme,
required this.characterTheme,
required this.audio,
}) {
images.prefix = '';
@ -33,7 +33,7 @@ class PinballGame extends Forge2DGame
@override
Color backgroundColor() => Colors.transparent;
final PinballTheme theme;
final CharacterTheme characterTheme;
final PinballAudio audio;
@ -41,8 +41,6 @@ class PinballGame extends Forge2DGame
@override
Future<void> onLoad() async {
_addContactCallbacks();
unawaited(add(gameFlowController = GameFlowController(this)));
unawaited(add(CameraController(this)));
unawaited(add(Backboard.waiting(position: Vector2(0, -88))));
@ -55,32 +53,11 @@ class PinballGame extends Forge2DGame
final launcher = Launcher();
unawaited(addFromBlueprint(launcher));
unawaited(add(Board()));
unawaited(add(AlienZone()));
await addFromBlueprint(SparkyFireZone());
await addFromBlueprint(AndroidAcres());
unawaited(addFromBlueprint(Slingshots()));
unawaited(addFromBlueprint(DinoWalls()));
await add(ChromeDino()..initialPosition = Vector2(12.3, -6.9));
unawaited(_addBonusWord());
unawaited(addFromBlueprint(SpaceshipRamp()));
unawaited(
addFromBlueprint(
Spaceship(
position: Vector2(-26.5, -28.5),
),
),
);
unawaited(addFromBlueprint(SpaceshipRail()));
controller.attachTo(launcher.components.whereType<Plunger>().first);
await super.onLoad();
}
void _addContactCallbacks() {
addContactCallback(BallScorePointsCallback(this));
addContactCallback(BottomWallBallContactCallback());
}
Future<void> _addBonusWord() async {
await add(
GoogleWord(
position: Vector2(
@ -89,11 +66,14 @@ class PinballGame extends Forge2DGame
),
),
);
controller.attachTo(launcher.components.whereType<Plunger>().first);
await super.onLoad();
}
}
class _GameBallsController extends ComponentController<PinballGame>
with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
with BlocComponent<GameBloc, GameState> {
_GameBallsController(PinballGame game) : super(game);
late final Plunger _plunger;
@ -101,9 +81,9 @@ class _GameBallsController extends ComponentController<PinballGame>
@override
bool listenWhen(GameState? previousState, GameState newState) {
final noBallsLeft = component.descendants().whereType<Ball>().isEmpty;
final canBallRespawn = newState.balls > 0;
final notGameOver = !newState.isGameOver;
return noBallsLeft && canBallRespawn;
return noBallsLeft && notGameOver;
}
@override
@ -120,7 +100,7 @@ class _GameBallsController extends ComponentController<PinballGame>
void _spawnBall() {
final ball = ControlledBall.launch(
theme: gameRef.theme,
characterTheme: component.characterTheme,
)..initialPosition = Vector2(
_plunger.body.position.x,
_plunger.body.position.y - Ball.size.y,
@ -138,10 +118,10 @@ class _GameBallsController extends ComponentController<PinballGame>
class DebugPinballGame extends PinballGame with FPSCounter, TapDetector {
DebugPinballGame({
required PinballTheme theme,
required CharacterTheme characterTheme,
required PinballAudio audio,
}) : super(
theme: theme,
characterTheme: characterTheme,
audio: audio,
) {
controller = _DebugGameBallsController(this);
@ -181,20 +161,10 @@ class DebugPinballGame extends PinballGame with FPSCounter, TapDetector {
class _DebugGameBallsController extends _GameBallsController {
_DebugGameBallsController(PinballGame game) : super(game);
@override
bool listenWhen(GameState? previousState, GameState newState) {
final noBallsLeft = component
.descendants()
.whereType<ControlledBall>()
.where((ball) => ball.controller is! DebugBallController)
.isEmpty;
final canBallRespawn = newState.balls > 0;
return noBallsLeft && canBallRespawn;
}
}
// TODO(wolfenrain): investigate this CI failure.
// coverage:ignore-start
class _DebugInformation extends Component with HasGameRef<DebugPinballGame> {
_DebugInformation() : super(priority: RenderPriority.debugInfo);
@ -226,3 +196,4 @@ class _DebugInformation extends Component with HasGameRef<DebugPinballGame> {
_debugTextPaint.render(canvas, debugText, position);
}
}
// coverage:ignore-end

@ -5,8 +5,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball/theme/theme.dart';
import 'package:pinball_audio/pinball_audio.dart';
class PinballGamePage extends StatelessWidget {
@ -31,17 +31,19 @@ class PinballGamePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = context.read<ThemeCubit>().state.theme;
final characterTheme =
context.read<CharacterThemeCubit>().state.characterTheme;
final audio = context.read<PinballAudio>();
final pinballAudio = context.read<PinballAudio>();
final game = isDebugMode
? DebugPinballGame(theme: theme, audio: audio)
: PinballGame(theme: theme, audio: audio);
? DebugPinballGame(characterTheme: characterTheme, audio: audio)
: PinballGame(characterTheme: characterTheme, audio: audio);
final loadables = [
...game.preLoadAssets(),
pinballAudio.load(),
...BonusAnimation.loadAssets(),
];
return MultiBlocProvider(
@ -112,6 +114,10 @@ class PinballGameLoadedView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
final screenWidth = MediaQuery.of(context).size.width;
final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8);
return Stack(
children: [
Positioned.fill(
@ -130,10 +136,12 @@ class PinballGameLoadedView extends StatelessWidget {
},
),
),
const Positioned(
top: 8,
left: 8,
child: GameHud(),
// TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc
// status
Positioned(
top: 16,
left: leftMargin,
child: const GameHud(),
),
],
);

@ -1,19 +1,23 @@
// ignore_for_file: public_member_api_docs
import 'package:flame/flame.dart';
import 'package:flame/sprite.dart';
import 'package:flame/widgets.dart';
import 'package:flutter/material.dart' hide Image;
import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_flame/pinball_flame.dart';
class BonusAnimation extends StatelessWidget {
/// {@template bonus_animation}
/// [Widget] that displays bonus animations.
/// {@endtemplate}
class BonusAnimation extends StatefulWidget {
/// {@macro bonus_animation}
const BonusAnimation._(
this.imagePath, {
String imagePath, {
VoidCallback? onCompleted,
Key? key,
}) : _onCompleted = onCompleted,
}) : _imagePath = imagePath,
_onCompleted = onCompleted,
super(key: key);
/// [Widget] that displays the dash nest animation.
BonusAnimation.dashNest({
Key? key,
VoidCallback? onCompleted,
@ -23,6 +27,7 @@ class BonusAnimation extends StatelessWidget {
key: key,
);
/// [Widget] that displays the sparky turbo charge animation.
BonusAnimation.sparkyTurboCharge({
Key? key,
VoidCallback? onCompleted,
@ -32,56 +37,94 @@ class BonusAnimation extends StatelessWidget {
key: key,
);
BonusAnimation.dino({
/// [Widget] that displays the dino chomp animation.
BonusAnimation.dinoChomp({
Key? key,
VoidCallback? onCompleted,
}) : this._(
Assets.images.bonusAnimation.dino.keyName,
Assets.images.bonusAnimation.dinoChomp.keyName,
onCompleted: onCompleted,
key: key,
);
BonusAnimation.android({
/// [Widget] that displays the android spaceship animation.
BonusAnimation.androidSpaceship({
Key? key,
VoidCallback? onCompleted,
}) : this._(
Assets.images.bonusAnimation.android.keyName,
Assets.images.bonusAnimation.androidSpaceship.keyName,
onCompleted: onCompleted,
key: key,
);
BonusAnimation.google({
/// [Widget] that displays the google word animation.
BonusAnimation.googleWord({
Key? key,
VoidCallback? onCompleted,
}) : this._(
Assets.images.bonusAnimation.google.keyName,
Assets.images.bonusAnimation.googleWord.keyName,
onCompleted: onCompleted,
key: key,
);
final String imagePath;
final String _imagePath;
final VoidCallback? _onCompleted;
static Future<void> loadAssets() {
/// Returns a list of assets to be loaded for animations.
static List<Future> loadAssets() {
Flame.images.prefix = '';
return Flame.images.loadAll([
Assets.images.bonusAnimation.dashNest.keyName,
Assets.images.bonusAnimation.sparkyTurboCharge.keyName,
Assets.images.bonusAnimation.dino.keyName,
Assets.images.bonusAnimation.android.keyName,
Assets.images.bonusAnimation.google.keyName,
]);
return [
Flame.images.load(Assets.images.bonusAnimation.dashNest.keyName),
Flame.images.load(Assets.images.bonusAnimation.sparkyTurboCharge.keyName),
Flame.images.load(Assets.images.bonusAnimation.dinoChomp.keyName),
Flame.images.load(Assets.images.bonusAnimation.androidSpaceship.keyName),
Flame.images.load(Assets.images.bonusAnimation.googleWord.keyName),
];
}
@override
State<BonusAnimation> createState() => _BonusAnimationState();
}
class _BonusAnimationState extends State<BonusAnimation>
with TickerProviderStateMixin {
late SpriteAnimationController controller;
late SpriteAnimation animation;
bool shouldRunBuildCallback = true;
@override
void dispose() {
controller.dispose();
super.dispose();
}
// When the animation is overwritten by another animation, we need to stop
// the callback in the build method as it will break the new animation.
// Otherwise we need to set up a new callback when a new animation starts to
// show the score view at the end of the animation.
@override
void didUpdateWidget(BonusAnimation oldWidget) {
shouldRunBuildCallback = oldWidget._imagePath == widget._imagePath;
Future<void>.delayed(
Duration(seconds: animation.totalDuration().ceil()),
() {
widget._onCompleted?.call();
},
);
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
final spriteSheet = SpriteSheet.fromColumnsAndRows(
image: Flame.images.fromCache(imagePath),
image: Flame.images.fromCache(widget._imagePath),
columns: 8,
rows: 9,
);
final animation = spriteSheet.createAnimation(
animation = spriteSheet.createAnimation(
row: 0,
stepTime: 1 / 24,
to: spriteSheet.rows * spriteSheet.columns,
@ -91,15 +134,22 @@ class BonusAnimation extends StatelessWidget {
Future<void>.delayed(
Duration(seconds: animation.totalDuration().ceil()),
() {
_onCompleted?.call();
if (shouldRunBuildCallback) {
widget._onCompleted?.call();
}
},
);
controller = SpriteAnimationController(
animation: animation,
vsync: this,
)..forward();
return SizedBox(
width: double.infinity,
height: double.infinity,
child: SpriteAnimationWidget(
animation: animation,
controller: controller,
),
);
}

@ -1,46 +1,122 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/theme/app_colors.dart';
/// {@template game_hud}
/// Overlay of a [PinballGame] that displays the current [GameState.score] and
/// [GameState.balls].
/// Overlay on the [PinballGame].
///
/// Displays the current [GameState.score], [GameState.rounds] and animates when
/// the player gets a [GameBonus].
/// {@endtemplate}
class GameHud extends StatelessWidget {
class GameHud extends StatefulWidget {
/// {@macro game_hud}
const GameHud({Key? key}) : super(key: key);
@override
State<GameHud> createState() => _GameHudState();
}
class _GameHudState extends State<GameHud> {
bool showAnimation = false;
/// 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 state = context.watch<GameBloc>().state;
return Container(
color: Colors.redAccent,
width: 200,
height: 100,
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${state.score}',
style: Theme.of(context).textTheme.headline3,
final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver);
return _ScoreViewDecoration(
child: SizedBox(
height: _width / _ratio,
width: _width,
child: BlocListener<GameBloc, GameState>(
listenWhen: (previous, current) =>
previous.bonusHistory.length != current.bonusHistory.length,
listener: (_, __) => setState(() => showAnimation = true),
child: AnimatedSwitcher(
duration: kThemeAnimationDuration,
child: showAnimation && !isGameOver
? _AnimationView(
onComplete: () {
if (mounted) {
setState(() => showAnimation = false);
}
},
)
: const ScoreView(),
),
Wrap(
direction: Axis.vertical,
children: [
for (var i = 0; i < state.balls; i++)
const Padding(
padding: EdgeInsets.only(top: 6, right: 6),
child: CircleAvatar(
radius: 8,
backgroundColor: Colors.black,
),
),
],
),
),
);
}
}
class _ScoreViewDecoration extends StatelessWidget {
const _ScoreViewDecoration({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
const radius = BorderRadius.all(Radius.circular(12));
const boardWidth = 5.0;
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: radius,
border: Border.all(
color: AppColors.white,
width: boardWidth,
),
image: DecorationImage(
fit: BoxFit.cover,
image: AssetImage(
Assets.images.score.miniScoreBackground.path,
),
],
),
),
child: Padding(
padding: const EdgeInsets.all(boardWidth - 1),
child: ClipRRect(
borderRadius: radius,
child: child,
),
),
);
}
}
class _AnimationView extends StatelessWidget {
const _AnimationView({
Key? key,
required this.onComplete,
}) : super(key: key);
final VoidCallback onComplete;
@override
Widget build(BuildContext context) {
final lastBonus = context.select(
(GameBloc bloc) => bloc.state.bonusHistory.last,
);
switch (lastBonus) {
case GameBonus.dashNest:
return BonusAnimation.dashNest(onCompleted: onComplete);
case GameBonus.sparkyTurboCharge:
return BonusAnimation.sparkyTurboCharge(onCompleted: onComplete);
case GameBonus.dinoChomp:
return BonusAnimation.dinoChomp(onCompleted: onComplete);
case GameBonus.googleWord:
return BonusAnimation.googleWord(onCompleted: onComplete);
case GameBonus.androidSpaceship:
return BonusAnimation.androidSpaceship(onCompleted: onComplete);
}
}
}

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:pinball/game/pinball_game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart';
import 'package:pinball/select_character/select_character.dart';
/// {@template play_button_overlay}
/// [Widget] that renders the button responsible to starting the game

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart';
/// {@template round_count_display}
/// Colored square indicating if a round is available.
/// {@endtemplate}
class RoundCountDisplay extends StatelessWidget {
/// {@macro round_count_display}
const RoundCountDisplay({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final rounds = context.select((GameBloc bloc) => bloc.state.rounds);
return Row(
children: [
Text(
l10n.rounds,
style: AppTextStyle.subtitle1.copyWith(
color: AppColors.orange,
),
),
const SizedBox(width: 8),
Row(
children: [
RoundIndicator(isActive: rounds >= 1),
RoundIndicator(isActive: rounds >= 2),
RoundIndicator(isActive: rounds >= 3),
],
),
],
);
}
}
/// {@template round_indicator}
/// [Widget] that displays the round indicator.
/// {@endtemplate}
@visibleForTesting
class RoundIndicator extends StatelessWidget {
/// {@macro round_indicator}
const RoundIndicator({
Key? key,
required this.isActive,
}) : super(key: key);
/// A value that describes whether the indicator is active.
final bool isActive;
@override
Widget build(BuildContext context) {
final color = isActive ? AppColors.orange : AppColors.orange.withAlpha(128);
const size = 8.0;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Container(
color: color,
height: size,
width: size,
),
);
}
}

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template score_view}
/// [Widget] that displays the score.
/// {@endtemplate}
class ScoreView extends StatelessWidget {
/// {@macro score_view}
const ScoreView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: AnimatedSwitcher(
duration: kThemeAnimationDuration,
child: isGameOver ? const _GameOver() : const _ScoreDisplay(),
),
);
}
}
class _GameOver extends StatelessWidget {
const _GameOver({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Text(
l10n.gameOver,
style: AppTextStyle.headline1.copyWith(
color: AppColors.white,
),
);
}
}
class _ScoreDisplay extends StatelessWidget {
const _ScoreDisplay({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
l10n.score.toLowerCase(),
style: AppTextStyle.subtitle1.copyWith(
color: AppColors.orange,
),
),
const _ScoreText(),
const RoundCountDisplay(),
],
);
}
}
class _ScoreText extends StatelessWidget {
const _ScoreText({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final score = context.select((GameBloc bloc) => bloc.state.score);
return Text(
score.formatScore(),
style: AppTextStyle.headline1.copyWith(
color: AppColors.white,
),
);
}
}

@ -1,3 +1,5 @@
export 'bonus_animation.dart';
export 'game_hud.dart';
export 'play_button_overlay.dart';
export 'round_count_display.dart';
export 'score_view.dart';

@ -14,26 +14,27 @@ class $AssetsImagesGen {
const $AssetsImagesBonusAnimationGen();
$AssetsImagesComponentsGen get components =>
const $AssetsImagesComponentsGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
}
class $AssetsImagesBonusAnimationGen {
const $AssetsImagesBonusAnimationGen();
/// File path: assets/images/bonus_animation/android.png
AssetGenImage get android =>
const AssetGenImage('assets/images/bonus_animation/android.png');
/// File path: assets/images/bonus_animation/android_spaceship.png
AssetGenImage get androidSpaceship => const AssetGenImage(
'assets/images/bonus_animation/android_spaceship.png');
/// File path: assets/images/bonus_animation/dash_nest.png
AssetGenImage get dashNest =>
const AssetGenImage('assets/images/bonus_animation/dash_nest.png');
/// File path: assets/images/bonus_animation/dino.png
AssetGenImage get dino =>
const AssetGenImage('assets/images/bonus_animation/dino.png');
/// File path: assets/images/bonus_animation/dino_chomp.png
AssetGenImage get dinoChomp =>
const AssetGenImage('assets/images/bonus_animation/dino_chomp.png');
/// File path: assets/images/bonus_animation/google.png
AssetGenImage get google =>
const AssetGenImage('assets/images/bonus_animation/google.png');
/// File path: assets/images/bonus_animation/google_word.png
AssetGenImage get googleWord =>
const AssetGenImage('assets/images/bonus_animation/google_word.png');
/// File path: assets/images/bonus_animation/sparky_turbo_charge.png
AssetGenImage get sparkyTurboCharge => const AssetGenImage(
@ -48,6 +49,14 @@ class $AssetsImagesComponentsGen {
const AssetGenImage('assets/images/components/background.png');
}
class $AssetsImagesScoreGen {
const $AssetsImagesScoreGen();
/// File path: assets/images/score/mini_score_background.png
AssetGenImage get miniScoreBackground =>
const AssetGenImage('assets/images/score/mini_score_background.png');
}
class Assets {
Assets._();

@ -75,5 +75,9 @@
"enterInitials": "Enter your initials",
"@enterInitials": {
"description": "Text displayed on the ending dialog when game finishes to ask the user for his initials"
},
"rounds": "Ball Ct:",
"@rounds": {
"description": "Text displayed on the scoreboard widget to indicate rounds left"
}
}

@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/leaderboard/leaderboard.dart';
import 'package:pinball/theme/theme.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_theme/pinball_theme.dart';
class LeaderboardPage extends StatelessWidget {

@ -5,12 +5,12 @@ import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:pinball_theme/pinball_theme.dart';
part 'theme_state.dart';
part 'character_theme_state.dart';
class ThemeCubit extends Cubit<ThemeState> {
ThemeCubit() : super(const ThemeState.initial());
class CharacterThemeCubit extends Cubit<CharacterThemeState> {
CharacterThemeCubit() : super(const CharacterThemeState.initial());
void characterSelected(CharacterTheme characterTheme) {
emit(ThemeState(PinballTheme(characterTheme: characterTheme)));
emit(CharacterThemeState(characterTheme));
}
}

@ -0,0 +1,15 @@
// ignore_for_file: public_member_api_docs
// TODO(allisonryan0002): Document this section when the API is stable.
part of 'character_theme_cubit.dart';
class CharacterThemeState extends Equatable {
const CharacterThemeState(this.characterTheme);
const CharacterThemeState.initial() : characterTheme = const DashTheme();
final CharacterTheme characterTheme;
@override
List<Object> get props => [characterTheme];
}

@ -0,0 +1,2 @@
export 'cubit/character_theme_cubit.dart';
export 'view/view.dart';

@ -3,8 +3,8 @@
import 'package:flutter/material.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/theme/theme.dart';
import 'package:pinball_theme/pinball_theme.dart';
class CharacterSelectionDialog extends StatelessWidget {
@ -19,7 +19,7 @@ class CharacterSelectionDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ThemeCubit(),
create: (_) => CharacterThemeCubit(),
child: const CharacterSelectionView(),
);
}
@ -109,12 +109,14 @@ class CharacterImageButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final currentCharacterTheme = context.select<ThemeCubit, CharacterTheme>(
(cubit) => cubit.state.theme.characterTheme,
final currentCharacterTheme =
context.select<CharacterThemeCubit, CharacterTheme>(
(cubit) => cubit.state.characterTheme,
);
return GestureDetector(
onTap: () => context.read<ThemeCubit>().characterSelected(characterTheme),
onTap: () =>
context.read<CharacterThemeCubit>().characterSelected(characterTheme),
child: DecoratedBox(
decoration: BoxDecoration(
color: (currentCharacterTheme == characterTheme)

@ -5,7 +5,7 @@ import 'package:pinball/theme/theme.dart';
import 'package:pinball_components/pinball_components.dart';
const _fontPackage = 'pinball_components';
const _primaryFontFamily = PinballFonts.pixeloidSans;
const _primaryFontFamily = FontFamily.pixeloidSans;
abstract class AppTextStyle {
static const headline1 = TextStyle(

@ -1,16 +0,0 @@
// ignore_for_file: public_member_api_docs
// TODO(allisonryan0002): Document this section when the API is stable.
part of 'theme_cubit.dart';
class ThemeState extends Equatable {
const ThemeState(this.theme);
const ThemeState.initial()
: theme = const PinballTheme(characterTheme: DashTheme());
final PinballTheme theme;
@override
List<Object> get props => [theme];
}

@ -1,4 +1,2 @@
export 'app_colors.dart';
export 'app_text_style.dart';
export 'cubit/theme_cubit.dart';
export 'view/view.dart';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 825 KiB

After

Width:  |  Height:  |  Size: 886 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 61 KiB

@ -10,8 +10,8 @@ import 'package:flutter/widgets.dart';
class $AssetsImagesGen {
const $AssetsImagesGen();
$AssetsImagesAlienBumperGen get alienBumper =>
const $AssetsImagesAlienBumperGen();
$AssetsImagesAndroidBumperGen get androidBumper =>
const $AssetsImagesAndroidBumperGen();
$AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen();
$AssetsImagesBallGen get ball => const $AssetsImagesBallGen();
$AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen();
@ -31,11 +31,13 @@ class $AssetsImagesGen {
$AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen();
}
class $AssetsImagesAlienBumperGen {
const $AssetsImagesAlienBumperGen();
class $AssetsImagesAndroidBumperGen {
const $AssetsImagesAndroidBumperGen();
$AssetsImagesAlienBumperAGen get a => const $AssetsImagesAlienBumperAGen();
$AssetsImagesAlienBumperBGen get b => const $AssetsImagesAlienBumperBGen();
$AssetsImagesAndroidBumperAGen get a =>
const $AssetsImagesAndroidBumperAGen();
$AssetsImagesAndroidBumperBGen get b =>
const $AssetsImagesAndroidBumperBGen();
}
class $AssetsImagesBackboardGen {
@ -258,28 +260,28 @@ class $AssetsImagesSparkyGen {
const $AssetsImagesSparkyComputerGen();
}
class $AssetsImagesAlienBumperAGen {
const $AssetsImagesAlienBumperAGen();
class $AssetsImagesAndroidBumperAGen {
const $AssetsImagesAndroidBumperAGen();
/// File path: assets/images/alien_bumper/a/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/alien_bumper/a/active.png');
/// File path: assets/images/android_bumper/a/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/android_bumper/a/dimmed.png');
/// File path: assets/images/alien_bumper/a/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/alien_bumper/a/inactive.png');
/// File path: assets/images/android_bumper/a/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/android_bumper/a/lit.png');
}
class $AssetsImagesAlienBumperBGen {
const $AssetsImagesAlienBumperBGen();
class $AssetsImagesAndroidBumperBGen {
const $AssetsImagesAndroidBumperBGen();
/// File path: assets/images/alien_bumper/b/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/alien_bumper/b/active.png');
/// File path: assets/images/android_bumper/b/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/android_bumper/b/dimmed.png');
/// File path: assets/images/alien_bumper/b/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/alien_bumper/b/inactive.png');
/// File path: assets/images/android_bumper/b/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/android_bumper/b/lit.png');
}
class $AssetsImagesDashBumperGen {
@ -306,9 +308,9 @@ class $AssetsImagesDinoAnimatronicGen {
class $AssetsImagesSpaceshipRailGen {
const $AssetsImagesSpaceshipRailGen();
/// File path: assets/images/spaceship/rail/foreground.png
AssetGenImage get foreground =>
const AssetGenImage('assets/images/spaceship/rail/foreground.png');
/// File path: assets/images/spaceship/rail/exit.png
AssetGenImage get exit =>
const AssetGenImage('assets/images/spaceship/rail/exit.png');
/// File path: assets/images/spaceship/rail/main.png
AssetGenImage get main =>

@ -1,2 +1,3 @@
export 'assets.gen.dart';
export 'fonts.gen.dart';
export 'pinball_fonts.dart';

@ -1,121 +0,0 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template alien_bumper}
/// Bumper for area under the [Spaceship].
/// {@endtemplate}
class AlienBumper extends BodyComponent with InitialPosition {
/// {@macro alien_bumper}
AlienBumper._({
required double majorRadius,
required double minorRadius,
required String onAssetPath,
required String offAssetPath,
}) : _majorRadius = majorRadius,
_minorRadius = minorRadius,
super(
priority: RenderPriority.alienBumper,
children: [
_AlienBumperSpriteGroupComponent(
onAssetPath: onAssetPath,
offAssetPath: offAssetPath,
),
],
) {
renderBody = false;
}
/// {@macro alien_bumper}
AlienBumper.a()
: this._(
majorRadius: 3.52,
minorRadius: 2.97,
onAssetPath: Assets.images.alienBumper.a.active.keyName,
offAssetPath: Assets.images.alienBumper.a.inactive.keyName,
);
/// {@macro alien_bumper}
AlienBumper.b()
: this._(
majorRadius: 3.19,
minorRadius: 2.79,
onAssetPath: Assets.images.alienBumper.b.active.keyName,
offAssetPath: Assets.images.alienBumper.b.inactive.keyName,
);
final double _majorRadius;
final double _minorRadius;
@override
Body createBody() {
final shape = EllipseShape(
center: Vector2.zero(),
majorRadius: _majorRadius,
minorRadius: _minorRadius,
)..rotate(1.29);
final fixtureDef = FixtureDef(
shape,
restitution: 4,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
/// Animates the [AlienBumper].
Future<void> animate() async {
final spriteGroupComponent = firstChild<_AlienBumperSpriteGroupComponent>()
?..current = AlienBumperSpriteState.inactive;
await Future<void>.delayed(const Duration(milliseconds: 50));
spriteGroupComponent?.current = AlienBumperSpriteState.active;
}
}
/// Indicates the [AlienBumper]'s current sprite state.
@visibleForTesting
enum AlienBumperSpriteState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}
class _AlienBumperSpriteGroupComponent
extends SpriteGroupComponent<AlienBumperSpriteState> with HasGameRef {
_AlienBumperSpriteGroupComponent({
required String onAssetPath,
required String offAssetPath,
}) : _onAssetPath = onAssetPath,
_offAssetPath = offAssetPath,
super(
anchor: Anchor.center,
position: Vector2(0, -0.1),
);
final String _onAssetPath;
final String _offAssetPath;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprites = {
AlienBumperSpriteState.active:
Sprite(gameRef.images.fromCache(_onAssetPath)),
AlienBumperSpriteState.inactive:
Sprite(gameRef.images.fromCache(_offAssetPath)),
};
this.sprites = sprites;
current = AlienBumperSpriteState.active;
size = sprites[current]!.originalSize / 10;
}
}

@ -0,0 +1,143 @@
import 'dart:async';
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/android_bumper/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/android_bumper_cubit.dart';
/// {@template android_bumper}
/// Bumper for area under the [Spaceship].
/// {@endtemplate}
class AndroidBumper extends BodyComponent with InitialPosition {
/// {@macro android_bumper}
AndroidBumper._({
required double majorRadius,
required double minorRadius,
required String litAssetPath,
required String dimmedAssetPath,
Iterable<Component>? children,
required this.bloc,
}) : _majorRadius = majorRadius,
_minorRadius = minorRadius,
super(
priority: RenderPriority.androidBumper,
renderBody: false,
children: [
AndroidBumperBallContactBehavior(),
AndroidBumperBlinkingBehavior(),
_AndroidBumperSpriteGroupComponent(
dimmedAssetPath: dimmedAssetPath,
litAssetPath: litAssetPath,
state: bloc.state,
),
...?children,
],
);
/// {@macro android_bumper}
AndroidBumper.a({
Iterable<Component>? children,
}) : this._(
majorRadius: 3.52,
minorRadius: 2.97,
litAssetPath: Assets.images.androidBumper.a.lit.keyName,
dimmedAssetPath: Assets.images.androidBumper.a.dimmed.keyName,
bloc: AndroidBumperCubit(),
children: children,
);
/// {@macro android_bumper}
AndroidBumper.b({
Iterable<Component>? children,
}) : this._(
majorRadius: 3.19,
minorRadius: 2.79,
litAssetPath: Assets.images.androidBumper.b.lit.keyName,
dimmedAssetPath: Assets.images.androidBumper.b.dimmed.keyName,
bloc: AndroidBumperCubit(),
children: children,
);
/// Creates an [AndroidBumper] without any children.
///
/// This can be used for testing [AndroidBumper]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting
AndroidBumper.test({
required this.bloc,
}) : _majorRadius = 3.52,
_minorRadius = 2.97;
final double _majorRadius;
final double _minorRadius;
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
final AndroidBumperCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
@override
Body createBody() {
final shape = EllipseShape(
center: Vector2.zero(),
majorRadius: _majorRadius,
minorRadius: _minorRadius,
)..rotate(1.29);
final fixtureDef = FixtureDef(
shape,
restitution: 4,
);
final bodyDef = BodyDef(
position: initialPosition,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
class _AndroidBumperSpriteGroupComponent
extends SpriteGroupComponent<AndroidBumperState>
with HasGameRef, ParentIsA<AndroidBumper> {
_AndroidBumperSpriteGroupComponent({
required String litAssetPath,
required String dimmedAssetPath,
required AndroidBumperState state,
}) : _litAssetPath = litAssetPath,
_dimmedAssetPath = dimmedAssetPath,
super(
anchor: Anchor.center,
position: Vector2(0, -0.1),
current: state,
);
final String _litAssetPath;
final String _dimmedAssetPath;
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen((state) => current = state);
final sprites = {
AndroidBumperState.lit: Sprite(
gameRef.images.fromCache(_litAssetPath),
),
AndroidBumperState.dimmed:
Sprite(gameRef.images.fromCache(_dimmedAssetPath)),
};
this.sprites = sprites;
size = sprites[current]!.originalSize / 10;
}
}

@ -0,0 +1,14 @@
// 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 AndroidBumperBallContactBehavior extends ContactBehavior<AndroidBumper> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.bloc.onBallContacted();
}
}

@ -0,0 +1,39 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template android_bumper_blinking_behavior}
/// Makes an [AndroidBumper] blink back to [AndroidBumperState.lit] when
/// [AndroidBumperState.dimmed].
/// {@endtemplate}
class AndroidBumperBlinkingBehavior extends TimerComponent
with ParentIsA<AndroidBumper> {
/// {@macro android_bumper_blinking_behavior}
AndroidBumperBlinkingBehavior() : super(period: 0.05);
void _onNewState(AndroidBumperState state) {
switch (state) {
case AndroidBumperState.lit:
break;
case AndroidBumperState.dimmed:
timer
..reset()
..start();
break;
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
timer.stop();
parent.bloc.stream.listen(_onNewState);
}
@override
void onTick() {
super.onTick();
timer.stop();
parent.bloc.onBlinked();
}
}

@ -0,0 +1,2 @@
export 'android_bumper_ball_contact_behavior.dart';
export 'android_bumper_blinking_behavior.dart';

@ -0,0 +1,17 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
part 'android_bumper_state.dart';
class AndroidBumperCubit extends Cubit<AndroidBumperState> {
AndroidBumperCubit() : super(AndroidBumperState.dimmed);
void onBallContacted() {
emit(AndroidBumperState.dimmed);
}
void onBlinked() {
emit(AndroidBumperState.lit);
}
}

@ -0,0 +1,8 @@
// ignore_for_file: public_member_api_docs
part of 'android_bumper_cubit.dart';
enum AndroidBumperState {
lit,
dimmed,
}

@ -16,6 +16,7 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
Ball({
required this.baseColor,
}) : super(
renderBody: false,
children: [
_BallSpriteComponent()..tint(baseColor.withOpacity(0.5)),
],
@ -26,7 +27,6 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
// We need to see what happens if Ball appears from other place like nest
// bumper, it will need to explicit change layer to Layer.board then.
layer = Layer.board;
renderBody = false;
}
/// The size of the [Ball].

@ -13,10 +13,9 @@ class Baseboard extends BodyComponent with InitialPosition {
required BoardSide side,
}) : _side = side,
super(
renderBody: false,
children: [_BaseboardSpriteComponent(side: side)],
) {
renderBody = false;
}
);
/// Whether the [Baseboard] is on the left or right side of the board.
final BoardSide _side;

@ -26,11 +26,10 @@ class _BottomBoundary extends BodyComponent with InitialPosition {
/// {@macro bottom_boundary}
_BottomBoundary()
: super(
renderBody: false,
priority: RenderPriority.bottomBoundary,
children: [_BottomBoundarySpriteComponent()],
) {
renderBody = false;
}
);
List<FixtureDef> _createFixtureDefs() {
final bottomLeftCurve = BezierCurveShape(
@ -92,13 +91,10 @@ class _OuterBoundary extends BodyComponent with InitialPosition {
/// {@macro outer_boundary}
_OuterBoundary()
: super(
renderBody: false,
priority: RenderPriority.outerBoundary,
children: [
_OuterBoundarySpriteComponent(),
],
) {
renderBody = false;
}
children: [_OuterBoundarySpriteComponent()],
);
List<FixtureDef> _createFixtureDefs() {
final topWall = EdgeShape()
@ -106,28 +102,59 @@ class _OuterBoundary extends BodyComponent with InitialPosition {
Vector2(3.6, -70.2),
Vector2(-14.1, -70.2),
);
final topWallFixtureDef = FixtureDef(topWall);
final topLeftCurve = BezierCurveShape(
controlPoints: [
Vector2(-32.3, -57.2),
topWall.vertex1,
Vector2(-31.5, -69.9),
Vector2(-14.1, -70.2),
Vector2(-32.3, -57.2),
],
);
final topLeftCurveFixtureDef = FixtureDef(topLeftCurve);
final leftWall = EdgeShape()
final topLeftWall = EdgeShape()
..set(
Vector2(-32.3, -57.2),
topLeftCurve.vertices.last,
Vector2(-33.5, -44),
);
final upperLeftWallCurve = BezierCurveShape(
controlPoints: [
topLeftWall.vertex1,
Vector2(-33.9, -40.7),
Vector2(-32.5, -39),
],
);
final middleLeftWallCurve = BezierCurveShape(
controlPoints: [
upperLeftWallCurve.vertices.last,
Vector2(-23.2, -31.4),
Vector2(-33.9, -21.8),
],
);
final lowerLeftWallCurve = BezierCurveShape(
controlPoints: [
middleLeftWallCurve.vertices.last,
Vector2(-32.4, -17.6),
Vector2(-37.3, -11),
],
);
final bottomLeftWall = EdgeShape()
..set(
lowerLeftWallCurve.vertices.last,
Vector2(-43.9, 41.8),
);
final leftWallFixtureDef = FixtureDef(leftWall);
return [
topWallFixtureDef,
topLeftCurveFixtureDef,
leftWallFixtureDef,
FixtureDef(topWall),
FixtureDef(topLeftCurve),
FixtureDef(topLeftWall),
FixtureDef(upperLeftWallCurve),
FixtureDef(middleLeftWallCurve),
FixtureDef(lowerLeftWallCurve),
FixtureDef(bottomLeftWall),
];
}

@ -0,0 +1,25 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template bumping_behavior}
/// Makes any [BodyComponent] that contacts with [parent] bounce off.
/// {@endtemplate}
class BumpingBehavior extends ContactBehavior {
/// {@macro bumping_behavior}
BumpingBehavior({required double strength}) : _strength = strength;
/// Determines how strong the bump is.
final double _strength;
@override
void postSolve(Object other, Contact contact, ContactImpulse impulse) {
super.postSolve(other, contact, impulse);
if (other is! BodyComponent) return;
other.body.applyLinearImpulse(
contact.manifold.localPoint
..normalize()
..multiply(Vector2.all(other.body.mass * _strength)),
);
}
}

@ -12,9 +12,11 @@ import 'package:pinball_components/pinball_components.dart';
/// {@endtemplate}
class ChromeDino extends BodyComponent with InitialPosition {
/// {@macro chrome_dino}
ChromeDino() : super(priority: RenderPriority.dino) {
renderBody = false;
}
ChromeDino()
: super(
priority: RenderPriority.dino,
renderBody: false,
);
/// The size of the dinosaur mouth.
static final size = Vector2(5.5, 5);
@ -22,8 +24,10 @@ class ChromeDino extends BodyComponent with InitialPosition {
/// Anchors the [ChromeDino] to the [RevoluteJoint] that controls its arc
/// motion.
Future<_ChromeDinoJoint> _anchorToJoint() async {
// TODO(allisonryan0002): try moving to anchor after new body is defined.
final anchor = _ChromeDinoAnchor()
..initialPosition = initialPosition + Vector2(9, -4);
await add(anchor);
final jointDef = _ChromeDinoAnchorRevoluteJointDef(
@ -40,9 +44,11 @@ class ChromeDino extends BodyComponent with InitialPosition {
Future<void> onLoad() async {
await super.onLoad();
final joint = await _anchorToJoint();
const framesInAnimation = 98;
const animationFPS = 1 / 24;
await add(
TimerComponent(
period: 98 / 48,
period: (framesInAnimation / 2) * animationFPS,
onTick: joint._swivel,
repeat: true,
),
@ -81,13 +87,11 @@ class ChromeDino extends BodyComponent with InitialPosition {
}
}
/// {@template chrome_dino_anchor}
/// [JointAnchor] positioned at the back of the [ChromeDino].
/// {@endtemplate}
class _ChromeDinoAnchor extends JointAnchor {
/// {@macro chrome_dino_anchor}
_ChromeDinoAnchor();
// TODO(allisonryan0002): if these aren't moved when fixing the rendering, see
// if the joint can be created in onMount to resolve render syncing.
@override
Future<void> onLoad() async {
await super.onLoad();
@ -113,9 +117,8 @@ class _ChromeDinoAnchorRevoluteJointDef extends RevoluteJointDef {
chromeDino.body.position + anchor.body.position,
);
enableLimit = true;
const angle = _ChromeDinoJoint._halfSweepingAngle;
lowerAngle = -angle;
upperAngle = angle;
lowerAngle = -_ChromeDinoJoint._halfSweepingAngle;
upperAngle = _ChromeDinoJoint._halfSweepingAngle;
enableMotor = true;
maxMotorTorque = chromeDino.body.mass * 255;
@ -126,7 +129,6 @@ class _ChromeDinoAnchorRevoluteJointDef extends RevoluteJointDef {
class _ChromeDinoJoint extends RevoluteJoint {
_ChromeDinoJoint(_ChromeDinoAnchorRevoluteJointDef def) : super(def);
/// Half the angle of the arc motion.
static const _halfSweepingAngle = 0.1143;
/// Sweeps the [ChromeDino] up and down repeatedly.

@ -1,4 +1,4 @@
export 'alien_bumper.dart';
export 'android_bumper/android_bumper.dart';
export 'backboard/backboard.dart';
export 'ball.dart';
export 'baseboard.dart';
@ -8,11 +8,11 @@ export 'boundaries.dart';
export 'camera_zoom.dart';
export 'chrome_dino.dart';
export 'dash_animatronic.dart';
export 'dash_nest_bumper.dart';
export 'dash_nest_bumper/dash_nest_bumper.dart';
export 'dino_walls.dart';
export 'fire_effect.dart';
export 'flipper.dart';
export 'google_letter.dart';
export 'google_letter/google_letter.dart';
export 'initial_position.dart';
export 'joint_anchor.dart';
export 'kicker.dart';
@ -30,5 +30,5 @@ export 'spaceship.dart';
export 'spaceship_rail.dart';
export 'spaceship_ramp.dart';
export 'sparky_animatronic.dart';
export 'sparky_bumper.dart';
export 'sparky_bumper/sparky_bumper.dart';
export 'sparky_computer.dart';

@ -10,7 +10,6 @@ class DashAnimatronic extends SpriteAnimationComponent with HasGameRef {
: super(
anchor: Anchor.center,
playing: false,
priority: RenderPriority.dashAnimatronic,
);
@override

@ -0,0 +1,15 @@
// 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 DashNestBumperBallContactBehavior
extends ContactBehavior<DashNestBumper> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.bloc.onBallContacted();
}
}

@ -0,0 +1,19 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
part 'dash_nest_bumper_state.dart';
class DashNestBumperCubit extends Cubit<DashNestBumperState> {
DashNestBumperCubit() : super(DashNestBumperState.inactive);
/// Event added when the bumper contacts with a ball.
void onBallContacted() {
emit(DashNestBumperState.active);
}
/// Event added when the bumper should return to its initial configuration.
void onReset() {
emit(DashNestBumperState.inactive);
}
}

@ -0,0 +1,10 @@
part of 'dash_nest_bumper_cubit.dart';
/// Indicates the [DashNestBumperCubit]'s current state.
enum DashNestBumperState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}

@ -4,6 +4,10 @@ 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/dash_nest_bumper/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/dash_nest_bumper_cubit.dart';
/// {@template dash_nest_bumper}
/// Bumper with a nest appearance.
@ -16,54 +20,87 @@ class DashNestBumper extends BodyComponent with InitialPosition {
required String activeAssetPath,
required String inactiveAssetPath,
required Vector2 spritePosition,
Iterable<Component>? children,
required this.bloc,
}) : _majorRadius = majorRadius,
_minorRadius = minorRadius,
super(
priority: RenderPriority.dashBumper,
renderBody: false,
children: [
_DashNestBumperSpriteGroupComponent(
activeAssetPath: activeAssetPath,
inactiveAssetPath: inactiveAssetPath,
position: spritePosition,
current: bloc.state,
),
DashNestBumperBallContactBehavior(),
...?children,
],
) {
renderBody = false;
}
);
/// {@macro dash_nest_bumper}
DashNestBumper.main()
: this._(
DashNestBumper.main({
Iterable<Component>? children,
}) : this._(
majorRadius: 5.1,
minorRadius: 3.75,
activeAssetPath: Assets.images.dash.bumper.main.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName,
spritePosition: Vector2(0, -0.3),
children: children,
bloc: DashNestBumperCubit(),
);
/// {@macro dash_nest_bumper}
DashNestBumper.a()
: this._(
DashNestBumper.a({
Iterable<Component>? children,
}) : this._(
majorRadius: 3,
minorRadius: 2.5,
activeAssetPath: Assets.images.dash.bumper.a.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName,
spritePosition: Vector2(0.35, -1.2),
children: children,
bloc: DashNestBumperCubit(),
);
/// {@macro dash_nest_bumper}
DashNestBumper.b()
: this._(
DashNestBumper.b({
Iterable<Component>? children,
}) : this._(
majorRadius: 3,
minorRadius: 2.5,
activeAssetPath: Assets.images.dash.bumper.b.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName,
spritePosition: Vector2(0.35, -1.2),
children: children,
bloc: DashNestBumperCubit(),
);
/// Creates an [DashNestBumper] without any children.
///
/// This can be used for testing [DashNestBumper]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting
DashNestBumper.test({required this.bloc})
: _majorRadius = 3,
_minorRadius = 2.5;
final double _majorRadius;
final double _minorRadius;
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
final DashNestBumperCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
@override
Body createBody() {
final shape = EllipseShape(
@ -79,41 +116,22 @@ class DashNestBumper extends BodyComponent with InitialPosition {
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
/// Activates the [DashNestBumper].
void activate() {
firstChild<_DashNestBumperSpriteGroupComponent>()?.current =
DashNestBumperSpriteState.active;
}
/// Deactivates the [DashNestBumper].
void deactivate() {
firstChild<_DashNestBumperSpriteGroupComponent>()?.current =
DashNestBumperSpriteState.inactive;
}
}
/// Indicates the [DashNestBumper]'s current sprite state.
@visibleForTesting
enum DashNestBumperSpriteState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}
class _DashNestBumperSpriteGroupComponent
extends SpriteGroupComponent<DashNestBumperSpriteState> with HasGameRef {
extends SpriteGroupComponent<DashNestBumperState>
with HasGameRef, ParentIsA<DashNestBumper> {
_DashNestBumperSpriteGroupComponent({
required String activeAssetPath,
required String inactiveAssetPath,
required Vector2 position,
required DashNestBumperState current,
}) : _activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath,
super(
anchor: Anchor.center,
position: position,
current: current,
);
final String _activeAssetPath;
@ -122,15 +140,15 @@ class _DashNestBumperSpriteGroupComponent
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen((state) => current = state);
final sprites = {
DashNestBumperSpriteState.active:
DashNestBumperState.active:
Sprite(gameRef.images.fromCache(_activeAssetPath)),
DashNestBumperSpriteState.inactive:
DashNestBumperState.inactive:
Sprite(gameRef.images.fromCache(_inactiveAssetPath)),
};
this.sprites = sprites;
current = DashNestBumperSpriteState.inactive;
size = sprites[current]!.originalSize / 10;
}
}

@ -29,9 +29,8 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
: super(
priority: RenderPriority.dinoTopWall,
children: [_DinoTopWallSpriteComponent()],
) {
renderBody = false;
}
renderBody: false,
);
List<FixtureDef> _createFixtureDefs() {
final topStraightShape = EdgeShape()
@ -39,7 +38,6 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
Vector2(28.65, -34.3),
Vector2(29.5, -34.3),
);
final topStraightFixtureDef = FixtureDef(topStraightShape);
final topCurveShape = BezierCurveShape(
controlPoints: [
@ -48,7 +46,6 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
Vector2(26.6, -20.2),
],
);
final topCurveFixtureDef = FixtureDef(topCurveShape);
final middleCurveShape = BezierCurveShape(
controlPoints: [
@ -57,7 +54,6 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
Vector2(26.8, -18.7),
],
);
final middleCurveFixtureDef = FixtureDef(middleCurveShape);
final bottomCurveShape = BezierCurveShape(
controlPoints: [
@ -66,21 +62,19 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
Vector2(27, -14.2),
],
);
final bottomCurveFixtureDef = FixtureDef(bottomCurveShape);
final bottomStraightShape = EdgeShape()
..set(
bottomCurveShape.vertices.last,
Vector2(31, -13.7),
);
final bottomStraightFixtureDef = FixtureDef(bottomStraightShape);
return [
topStraightFixtureDef,
topCurveFixtureDef,
middleCurveFixtureDef,
bottomCurveFixtureDef,
bottomStraightFixtureDef,
FixtureDef(topStraightShape),
FixtureDef(topCurveShape),
FixtureDef(middleCurveShape),
FixtureDef(bottomCurveShape),
FixtureDef(bottomStraightShape),
];
}
@ -128,22 +122,15 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
: super(
priority: RenderPriority.dinoBottomWall,
children: [_DinoBottomWallSpriteComponent()],
) {
renderBody = false;
}
renderBody: false,
);
List<FixtureDef> _createFixtureDefs() {
const restitution = 1.0;
final topStraightShape = EdgeShape()
..set(
Vector2(32.4, -8.8),
Vector2(25, -7.7),
);
final topStraightFixtureDef = FixtureDef(
topStraightShape,
restitution: restitution,
);
final topLeftCurveShape = BezierCurveShape(
controlPoints: [
@ -152,36 +139,24 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
Vector2(29.8, 13.8),
],
);
final topLeftCurveFixtureDef = FixtureDef(
topLeftCurveShape,
restitution: restitution,
);
final bottomLeftStraightShape = EdgeShape()
..set(
topLeftCurveShape.vertices.last,
Vector2(31.9, 44.1),
);
final bottomLeftStraightFixtureDef = FixtureDef(
bottomLeftStraightShape,
restitution: restitution,
);
final bottomStraightShape = EdgeShape()
..set(
bottomLeftStraightShape.vertex2,
Vector2(37.8, 44.1),
);
final bottomStraightFixtureDef = FixtureDef(
bottomStraightShape,
restitution: restitution,
);
return [
topStraightFixtureDef,
topLeftCurveFixtureDef,
bottomLeftStraightFixtureDef,
bottomStraightFixtureDef,
FixtureDef(topStraightShape),
FixtureDef(topLeftCurveShape),
FixtureDef(bottomLeftStraightShape),
FixtureDef(bottomStraightShape),
];
}

@ -14,10 +14,9 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
Flipper({
required this.side,
}) : super(
renderBody: false,
children: [_FlipperSpriteComponent(side: side)],
) {
renderBody = false;
}
);
/// The size of the [Flipper].
static final size = Vector2(13.5, 4.3);

@ -0,0 +1,14 @@
// 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 GoogleLetterBallContactBehavior extends ContactBehavior<GoogleLetter> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.bloc.onBallContacted();
}
}

@ -0,0 +1,17 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
part 'google_letter_state.dart';
class GoogleLetterCubit extends Cubit<GoogleLetterState> {
GoogleLetterCubit() : super(GoogleLetterState.inactive);
void onBallContacted() {
emit(GoogleLetterState.active);
}
void onReset() {
emit(GoogleLetterState.inactive);
}
}

@ -0,0 +1,10 @@
part of 'google_letter_cubit.dart';
/// Indicates the [GoogleLetterCubit]'s current state.
enum GoogleLetterState {
/// A lit up letter.
active,
/// A dimmed letter.
inactive,
}

@ -1,33 +1,46 @@
import 'package:flame/components.dart';
import 'package:flame/effects.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/google_letter/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/google_letter_cubit.dart';
/// {@template google_letter}
/// Circular sensor that represents a letter in "GOOGLE" for a given index.
/// {@endtemplate}
class GoogleLetter extends BodyComponent with InitialPosition {
/// {@macro google_letter}
GoogleLetter(int index)
: _sprite = _GoogleLetterSprite(
_GoogleLetterSprite.spritePaths[index],
GoogleLetter(
int index,
) : bloc = GoogleLetterCubit(),
super(
children: [
GoogleLetterBallContactBehavior(),
_GoogleLetterSprite(_GoogleLetterSprite.spritePaths[index])
],
);
final _GoogleLetterSprite _sprite;
/// Activates this [GoogleLetter].
// TODO(alestiago): Improve doc comment once activate and deactivate
// are implemented with the actual assets.
Future<void> activate() => _sprite.activate();
/// Creates a [GoogleLetter] without any children.
///
/// This can be used for testing [GoogleLetter]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting
GoogleLetter.test({
required this.bloc,
});
/// Deactivates this [GoogleLetter].
Future<void> deactivate() => _sprite.deactivate();
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
final GoogleLetterCubit bloc;
@override
Future<void> onLoad() async {
await super.onLoad();
await add(_sprite);
void onRemove() {
bloc.close();
super.onRemove();
}
@override
@ -46,8 +59,11 @@ class GoogleLetter extends BodyComponent with InitialPosition {
}
}
class _GoogleLetterSprite extends SpriteComponent with HasGameRef {
_GoogleLetterSprite(String path) : _path = path;
class _GoogleLetterSprite extends SpriteComponent
with HasGameRef, ParentIsA<GoogleLetter> {
_GoogleLetterSprite(String path)
: _path = path,
super(anchor: Anchor.center);
static final spritePaths = [
Assets.images.googleWord.letter1.keyName,
@ -60,39 +76,16 @@ class _GoogleLetterSprite extends SpriteComponent with HasGameRef {
final String _path;
// TODO(alestiago): Correctly implement activate and deactivate once the
// assets are provided.
Future<void> activate() async {
await add(
_GoogleLetterColorEffect(color: Colors.green),
);
}
Future<void> deactivate() async {
await add(
_GoogleLetterColorEffect(color: Colors.red),
);
}
@override
Future<void> onLoad() async {
await super.onLoad();
// TODO(alisonryan2002): Make SpriteGroupComponent.
// parent.bloc.stream.listen();
// TODO(alestiago): Used cached assets.
final sprite = await gameRef.loadSprite(_path);
this.sprite = sprite;
// TODO(alestiago): Size correctly once the assets are provided.
size = sprite.originalSize / 5;
anchor = Anchor.center;
}
}
class _GoogleLetterColorEffect extends ColorEffect {
_GoogleLetterColorEffect({
required Color color,
}) : super(
color,
const Offset(0, 1),
EffectController(duration: 0.25),
);
}

@ -19,9 +19,8 @@ class Kicker extends BodyComponent with InitialPosition {
}) : _side = side,
super(
children: [_KickerSpriteComponent(side: side)],
) {
renderBody = false;
}
renderBody: false,
);
/// The size of the [Kicker] body.
static final Vector2 size = Vector2(4.4, 15);

@ -32,13 +32,13 @@ class _LaunchRampBase extends BodyComponent with Layered {
_LaunchRampBase()
: super(
priority: RenderPriority.launchRamp,
renderBody: false,
children: [
_LaunchRampBackgroundRailingSpriteComponent(),
_LaunchRampBaseSpriteComponent(),
],
) {
layer = Layer.launcher;
renderBody = false;
}
// TODO(ruimiguel): final asset differs slightly from the current shape. We
@ -107,13 +107,6 @@ class _LaunchRampBase extends BodyComponent with Layered {
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef
.addContactCallback(LayerSensorBallContactCallback<_LaunchRampExit>());
}
}
class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef {
@ -157,9 +150,8 @@ class _LaunchRampForegroundRailing extends BodyComponent {
: super(
priority: RenderPriority.launchRampForegroundRailing,
children: [_LaunchRampForegroundRailingSpriteComponent()],
) {
renderBody = false;
}
renderBody: false,
);
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
@ -218,9 +210,8 @@ class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent
}
class _LaunchRampCloseWall extends BodyComponent with InitialPosition, Layered {
_LaunchRampCloseWall() {
_LaunchRampCloseWall() : super(renderBody: false) {
layer = Layer.board;
renderBody = false;
}
@override
@ -252,7 +243,6 @@ class _LaunchRampExit extends LayerSensor {
outsidePriority: RenderPriority.ballOnBoard,
) {
layer = Layer.launcher;
renderBody = false;
}
static final Vector2 _size = Vector2(1.6, 0.1);

@ -17,13 +17,11 @@ enum LayerEntranceOrientation {
/// {@template layer_sensor}
/// [BodyComponent] located at the entrance and exit of a [Layer].
///
/// [LayerSensorBallContactCallback] detects when a [Ball] passes
/// through this sensor.
///
/// By default the base [layer] is set to [Layer.board] and the
/// [outsidePriority] is set to the lowest possible [Layer].
/// {@endtemplate}
abstract class LayerSensor extends BodyComponent with InitialPosition, Layered {
abstract class LayerSensor extends BodyComponent
with InitialPosition, Layered, ContactCallbacks {
/// {@macro layer_sensor}
LayerSensor({
required Layer insideLayer,
@ -34,7 +32,8 @@ abstract class LayerSensor extends BodyComponent with InitialPosition, Layered {
}) : _insideLayer = insideLayer,
_outsideLayer = outsideLayer ?? Layer.board,
_insidePriority = insidePriority,
_outsidePriority = outsidePriority ?? RenderPriority.ballOnBoard {
_outsidePriority = outsidePriority ?? RenderPriority.ballOnBoard,
super(renderBody: false) {
layer = Layer.opening;
}
final Layer _insideLayer;
@ -75,35 +74,29 @@ abstract class LayerSensor extends BodyComponent with InitialPosition, Layered {
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
/// {@template layer_sensor_ball_contact_callback}
/// Detects when a [Ball] enters or exits a [Layer] through a [LayerSensor].
///
/// Modifies [Ball]'s [Layer] and render priority depending on whether the
/// [Ball] is on or outside of a [Layer].
/// {@endtemplate}
class LayerSensorBallContactCallback<LayerEntrance extends LayerSensor>
extends ContactCallback<Ball, LayerEntrance> {
@override
void begin(Ball ball, LayerEntrance layerEntrance, Contact _) {
if (ball.layer != layerEntrance.insideLayer) {
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
if (other.layer != insideLayer) {
final isBallEnteringOpening =
(layerEntrance.orientation == LayerEntranceOrientation.down &&
ball.body.linearVelocity.y < 0) ||
(layerEntrance.orientation == LayerEntranceOrientation.up &&
ball.body.linearVelocity.y > 0);
(orientation == LayerEntranceOrientation.down &&
other.body.linearVelocity.y < 0) ||
(orientation == LayerEntranceOrientation.up &&
other.body.linearVelocity.y > 0);
if (isBallEnteringOpening) {
ball
..layer = layerEntrance.insideLayer
..priority = layerEntrance.insidePriority
other
..layer = insideLayer
..priority = insidePriority
..reorderChildren();
}
} else {
ball
..layer = layerEntrance.outsideLayer
..priority = layerEntrance.outsidePriority
other
..layer = outsideLayer
..priority = outsidePriority
..reorderChildren();
}
}

@ -14,9 +14,11 @@ class Plunger extends BodyComponent with InitialPosition, Layered {
required this.compressionDistance,
// TODO(ruimiguel): set to priority +1 over LaunchRamp once all priorities
// are fixed.
}) : super(priority: RenderPriority.plunger) {
}) : super(
priority: RenderPriority.plunger,
renderBody: false,
) {
layer = Layer.launcher;
renderBody = false;
}
/// Distance the plunger can lower.

@ -24,7 +24,7 @@ abstract class RenderPriority {
static const int ballOnSpaceship = _above + spaceshipSaucer;
/// Render priority for the [Ball] while it's on the [SpaceshipRail].
static const int ballOnSpaceshipRail = _below + spaceshipSaucer;
static const int ballOnSpaceshipRail = _above + spaceshipRail;
/// Render priority for the [Ball] while it's on the [LaunchRamp].
static const int ballOnLaunchRamp = _above + launchRamp;
@ -69,11 +69,7 @@ abstract class RenderPriority {
// Flutter Forest
static const int signpost = _above + launchRampForegroundRailing;
static const int dashBumper = _above + ballOnBoard;
static const int dashAnimatronic = 2 * _above + launchRamp;
static const int flutterForest = _above + launchRampForegroundRailing;
// Sparky Fire Zone
@ -87,13 +83,13 @@ abstract class RenderPriority {
static const int turboChargeFlame = _above + ballOnBoard;
// Android Spaceship
// Android Acres
static const int spaceshipRail = _above + bottomGroup;
static const int spaceshipRailForeground = _above + spaceshipRail;
static const int spaceshipRailExit = _above + ballOnSpaceshipRail;
static const int spaceshipSaucer = _above + spaceshipRail;
static const int spaceshipSaucer = _above + ballOnSpaceshipRail;
static const int spaceshipSaucerWall = _above + spaceshipSaucer;
@ -110,7 +106,7 @@ abstract class RenderPriority {
static const int spaceshipRampBoardOpening = _below + ballOnBoard;
static const int alienBumper = _above + ballOnBoard;
static const int androidBumper = _above + ballOnBoard;
// Score Text

@ -46,13 +46,15 @@ extension on SignpostSpriteState {
/// {@endtemplate}
class Signpost extends BodyComponent with InitialPosition {
/// {@macro signpost}
Signpost()
: super(
priority: RenderPriority.signpost,
children: [_SignpostSpriteComponent()],
) {
renderBody = false;
}
Signpost({
Iterable<Component>? children,
}) : super(
renderBody: false,
children: [
_SignpostSpriteComponent(),
...?children,
],
);
/// Forwards the sprite to the next [SignpostSpriteState].
///

@ -40,9 +40,8 @@ class Slingshot extends BodyComponent with InitialPosition {
super(
priority: RenderPriority.slingshot,
children: [_SlinghsotSpriteComponent(spritePath, angle: angle)],
) {
renderBody = false;
}
renderBody: false,
);
final double _length;

@ -42,25 +42,12 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
SpaceshipSaucer()
: super(
priority: RenderPriority.spaceshipSaucer,
renderBody: false,
children: [
_SpaceshipSaucerSpriteComponent(),
],
) {
layer = Layer.spaceship;
renderBody = false;
}
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef
..addContactCallback(
LayerSensorBallContactCallback<_SpaceshipEntrance>(),
)
..addContactCallback(
LayerSensorBallContactCallback<_SpaceshipHole>(),
);
}
@override
@ -108,8 +95,8 @@ class AndroidHead extends BodyComponent with InitialPosition, Layered {
: super(
priority: RenderPriority.androidHead,
children: [_AndroidHeadSpriteAnimation()],
renderBody: false,
) {
renderBody = false;
layer = Layer.spaceship;
}
@ -164,7 +151,6 @@ class _SpaceshipEntrance extends LayerSensor {
@override
Shape get shape {
renderBody = false;
final radius = Spaceship.size.y / 2;
return PolygonShape()
..setAsEdge(
@ -189,7 +175,6 @@ class _SpaceshipHole extends LayerSensor {
insidePriority: RenderPriority.ballOnSpaceship,
outsidePriority: outsidePriority,
) {
renderBody = false;
layer = Layer.spaceship;
}
@ -237,14 +222,16 @@ class _SpaceshipWallShape extends ChainShape {
/// {@endtemplate}
class SpaceshipWall extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_wall}
SpaceshipWall() : super(priority: RenderPriority.spaceshipSaucerWall) {
SpaceshipWall()
: super(
priority: RenderPriority.spaceshipSaucerWall,
renderBody: false,
) {
layer = Layer.spaceship;
}
@override
Body createBody() {
renderBody = false;
final shape = _SpaceshipWallShape();
final fixtureDef = FixtureDef(shape);

@ -2,88 +2,71 @@ import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template spaceship_rail}
/// A [Blueprint] for the spaceship drop tube.
/// A [Blueprint] for the rail exiting the [Spaceship].
/// {@endtemplate}
class SpaceshipRail extends Blueprint {
/// {@macro spaceship_rail}
SpaceshipRail()
: super(
components: [
_SpaceshipRailRamp(),
_SpaceshipRail(),
_SpaceshipRailExit(),
_SpaceshipRailBase(radius: 0.55)
..initialPosition = Vector2(-26.15, -18.65),
_SpaceshipRailBase(radius: 0.8)
..initialPosition = Vector2(-25.5, 12.9),
_SpaceshipRailForeground()
_SpaceshipRailExitSpriteComponent()
],
);
}
class _SpaceshipRailRamp extends BodyComponent with Layered {
_SpaceshipRailRamp()
class _SpaceshipRail extends BodyComponent with Layered {
_SpaceshipRail()
: super(
priority: RenderPriority.spaceshipRail,
children: [_SpaceshipRailRampSpriteComponent()],
children: [_SpaceshipRailSpriteComponent()],
renderBody: false,
) {
layer = Layer.spaceshipExitRail;
renderBody = false;
}
List<FixtureDef> _createFixtureDefs() {
final fixturesDefs = <FixtureDef>[];
final topArcShape = ArcShape(
center: Vector2(-35.5, -30.9),
center: Vector2(-35.1, -30.9),
arcRadius: 2.5,
angle: math.pi,
rotation: 0.2,
);
final topArcFixtureDef = FixtureDef(topArcShape);
fixturesDefs.add(topArcFixtureDef);
final topLeftCurveShape = BezierCurveShape(
controlPoints: [
Vector2(-37.9, -30.4),
Vector2(-38, -23.9),
Vector2(-37.6, -30.4),
Vector2(-37.8, -23.9),
Vector2(-30.93, -18.2),
],
);
final topLeftCurveFixtureDef = FixtureDef(topLeftCurveShape);
fixturesDefs.add(topLeftCurveFixtureDef);
final middleLeftCurveShape = BezierCurveShape(
controlPoints: [
topLeftCurveShape.vertices.last,
Vector2(-22.6, -10.3),
Vector2(-30, -0.2),
Vector2(-29.5, -0.2),
],
);
final middleLeftCurveFixtureDef = FixtureDef(middleLeftCurveShape);
fixturesDefs.add(middleLeftCurveFixtureDef);
final bottomLeftCurveShape = BezierCurveShape(
controlPoints: [
middleLeftCurveShape.vertices.last,
Vector2(-36, 8.6),
Vector2(-32.04, 18.3),
Vector2(-35.6, 8.6),
Vector2(-31.3, 18.3),
],
);
final bottomLeftCurveFixtureDef = FixtureDef(bottomLeftCurveShape);
fixturesDefs.add(bottomLeftCurveFixtureDef);
final topRightStraightShape = EdgeShape()
..set(
Vector2(-33, -31.3),
Vector2(-27.2, -21.3),
Vector2(-33, -31.3),
);
final topRightStraightFixtureDef = FixtureDef(topRightStraightShape);
fixturesDefs.add(topRightStraightFixtureDef);
final middleRightCurveShape = BezierCurveShape(
controlPoints: [
@ -92,8 +75,6 @@ class _SpaceshipRailRamp extends BodyComponent with Layered {
Vector2(-25.29, 1.7),
],
);
final middleRightCurveFixtureDef = FixtureDef(middleRightCurveShape);
fixturesDefs.add(middleRightCurveFixtureDef);
final bottomRightCurveShape = BezierCurveShape(
controlPoints: [
@ -102,10 +83,16 @@ class _SpaceshipRailRamp extends BodyComponent with Layered {
Vector2(-26.8, 15.7),
],
);
final bottomRightCurveFixtureDef = FixtureDef(bottomRightCurveShape);
fixturesDefs.add(bottomRightCurveFixtureDef);
return fixturesDefs;
return [
FixtureDef(topArcShape),
FixtureDef(topLeftCurveShape),
FixtureDef(middleLeftCurveShape),
FixtureDef(bottomLeftCurveShape),
FixtureDef(topRightStraightShape),
FixtureDef(middleRightCurveShape),
FixtureDef(bottomRightCurveShape),
];
}
@override
@ -114,67 +101,49 @@ class _SpaceshipRailRamp extends BodyComponent with Layered {
_createFixtureDefs().forEach(body.createFixture);
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(
LayerSensorBallContactCallback<_SpaceshipRailExit>(),
);
}
}
class _SpaceshipRailRampSpriteComponent extends SpriteComponent
with HasGameRef {
class _SpaceshipRailSpriteComponent extends SpriteComponent with HasGameRef {
_SpaceshipRailSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(-29.4, -5.7),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.spaceship.rail.main.keyName,
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.spaceship.rail.main.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
position = Vector2(-29.4, -5.7);
}
}
class _SpaceshipRailForeground extends SpriteComponent with HasGameRef {
_SpaceshipRailForeground()
: super(priority: RenderPriority.spaceshipRailForeground);
class _SpaceshipRailExitSpriteComponent extends SpriteComponent
with HasGameRef {
_SpaceshipRailExitSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(-28, 19.4),
priority: RenderPriority.spaceshipRailExit,
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.spaceship.rail.foreground.keyName,
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.spaceship.rail.exit.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
position = Vector2(-28.5, 19.7);
}
}
/// Represents the ground bases of the [_SpaceshipRailRamp].
class _SpaceshipRailBase extends BodyComponent with InitialPosition {
_SpaceshipRailBase({required this.radius}) {
renderBody = false;
}
final double radius;
@override
Body createBody() {
final shape = CircleShape()..radius = radius;
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef(
position: initialPosition,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
@ -185,7 +154,6 @@ class _SpaceshipRailExit extends LayerSensor {
insideLayer: Layer.spaceshipExitRail,
insidePriority: RenderPriority.ballOnSpaceshipRail,
) {
renderBody = false;
layer = Layer.spaceshipExitRail;
}

@ -98,12 +98,12 @@ class _SpaceshipRampBackground extends BodyComponent
_SpaceshipRampBackground()
: super(
priority: RenderPriority.spaceshipRamp,
renderBody: false,
children: [
_SpaceshipRampBackgroundRampSpriteComponent(),
],
) {
layer = Layer.spaceshipEntranceRamp;
renderBody = false;
}
/// Width between walls of the ramp.
@ -145,14 +145,6 @@ class _SpaceshipRampBackground extends BodyComponent
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(
LayerSensorBallContactCallback<_SpaceshipRampOpening>(),
);
}
}
class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent
@ -255,10 +247,10 @@ class _SpaceshipRampForegroundRailing extends BodyComponent
_SpaceshipRampForegroundRailing()
: super(
priority: RenderPriority.spaceshipRampForegroundRailing,
renderBody: false,
children: [_SpaceshipRampForegroundRailingSpriteComponent()],
) {
layer = Layer.spaceshipEntranceRamp;
renderBody = false;
}
List<FixtureDef> _createFixtureDefs() {
@ -321,8 +313,7 @@ class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent
}
class _SpaceshipRampBase extends BodyComponent with InitialPosition, Layered {
_SpaceshipRampBase() {
renderBody = false;
_SpaceshipRampBase() : super(renderBody: false) {
layer = Layer.board;
}
@ -363,9 +354,7 @@ class _SpaceshipRampOpening extends LayerSensor {
orientation: LayerEntranceOrientation.down,
insidePriority: RenderPriority.ballOnSpaceshipRamp,
outsidePriority: outsidePriority,
) {
renderBody = false;
}
);
final double _rotation;

@ -0,0 +1,2 @@
export 'sparky_bumper_ball_contact_behavior.dart';
export 'sparky_bumper_blinking_behavior.dart';

@ -0,0 +1,14 @@
// 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 SparkyBumperBallContactBehavior extends ContactBehavior<SparkyBumper> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.bloc.onBallContacted();
}
}

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

Loading…
Cancel
Save