Merge branch 'main' into feat/spaceship-ramp-logic

feat/spaceship-ramp-logic
RuiAlonso 3 years ago
commit 536c34484f

@ -0,0 +1,23 @@
name: pinball_ui
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
paths:
- "packages/pinball_ui/**"
- ".github/workflows/pinball_ui.yaml"
pull_request:
paths:
- "packages/pinball_ui/**"
- ".github/workflows/pinball_ui.yaml"
jobs:
build:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
with:
working_directory: packages/pinball_ui
coverage_excludes: "lib/gen/*.dart"

@ -1,86 +0,0 @@
import 'package:flame/components.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template board}
/// The main flat surface of the [PinballGame].
/// {endtemplate}
class Board extends Component {
/// {@macro board}
// TODO(alestiago): Make Board a Blueprint and sort out priorities.
Board() : super(priority: 1);
@override
Future<void> onLoad() async {
// TODO(allisonryan0002): add bottom group and flutter forest to pinball
// game directly. Then remove board.
final bottomGroup = _BottomGroup();
final flutterForest = FlutterForest();
await addAll([
bottomGroup,
flutterForest,
]);
}
}
/// {@template bottom_group}
/// Grouping of the board's bottom [Component]s.
///
/// The [_BottomGroup] consists of[Flipper]s, [Baseboard]s and [Kicker]s.
/// {@endtemplate}
// TODO(alestiago): Consider renaming once entire Board is defined.
class _BottomGroup extends Component {
/// {@macro bottom_group}
_BottomGroup() : super(priority: RenderPriority.bottomGroup);
@override
Future<void> onLoad() async {
final rightSide = _BottomGroupSide(
side: BoardSide.right,
);
final leftSide = _BottomGroupSide(
side: BoardSide.left,
);
await addAll([rightSide, leftSide]);
}
}
/// {@template bottom_group_side}
/// Group with one side of [_BottomGroup]'s symmetric [Component]s.
///
/// For example, [Flipper]s are symmetric components.
/// {@endtemplate}
class _BottomGroupSide extends Component {
/// {@macro bottom_group_side}
_BottomGroupSide({
required BoardSide side,
}) : _side = side;
final BoardSide _side;
@override
Future<void> onLoad() async {
final direction = _side.direction;
final centerXAdjustment = _side.isLeft ? 0 : -6.5;
final flipper = ControlledFlipper(
side: _side,
)..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, 43.6);
final baseboard = Baseboard(side: _side)
..initialPosition = Vector2(
(25.58 * direction) + centerXAdjustment,
28.69,
);
final kicker = Kicker(
side: _side,
)..initialPosition = Vector2(
(22.4 * direction) + centerXAdjustment,
25,
);
await addAll([flipper, baseboard, kicker]);
}
}

@ -0,0 +1,58 @@
import 'package:flame/components.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template bottom_group}
/// Grouping of the board's symmetrical bottom [Component]s.
///
/// The [BottomGroup] consists of [Flipper]s, [Baseboard]s and [Kicker]s.
/// {@endtemplate}
// TODO(allisonryan0002): Consider renaming.
class BottomGroup extends Component {
/// {@macro bottom_group}
BottomGroup()
: super(
children: [
_BottomGroupSide(side: BoardSide.right),
_BottomGroupSide(side: BoardSide.left),
],
);
}
/// {@template bottom_group_side}
/// Group with one side of [BottomGroup]'s symmetric [Component]s.
///
/// For example, [Flipper]s are symmetric components.
/// {@endtemplate}
class _BottomGroupSide extends Component {
/// {@macro bottom_group_side}
_BottomGroupSide({
required BoardSide side,
}) : _side = side,
super(priority: RenderPriority.bottomGroup);
final BoardSide _side;
@override
Future<void> onLoad() async {
final direction = _side.direction;
final centerXAdjustment = _side.isLeft ? 0 : -6.5;
final flipper = ControlledFlipper(
side: _side,
)..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, 43.6);
final baseboard = Baseboard(side: _side)
..initialPosition = Vector2(
(25.58 * direction) + centerXAdjustment,
28.69,
);
final kicker = Kicker(
side: _side,
)..initialPosition = Vector2(
(22.4 * direction) + centerXAdjustment,
25,
);
await addAll([flipper, baseboard, kicker]);
}
}

@ -1,9 +1,11 @@
export 'android_acres.dart';
export 'board.dart';
export 'bottom_group.dart';
export 'camera_controller.dart';
export 'controlled_ball.dart';
export 'controlled_flipper.dart';
export 'controlled_plunger.dart';
export 'dino_desert.dart';
export 'drain.dart';
export 'flutter_forest/flutter_forest.dart';
export 'game_flow_controller.dart';
export 'google_word/google_word.dart';
@ -11,4 +13,3 @@ export 'launcher.dart';
export 'scoring_behavior.dart';
export 'spaceship_ramp/android_ramp.dart';
export 'sparky_fire_zone.dart';
export 'wall.dart';

@ -1,3 +1,5 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';

@ -0,0 +1,23 @@
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';
/// {@template dino_desert}
/// Area located next to the [Launcher] containing the [ChromeDino] and
/// [DinoWalls].
/// {@endtemplate}
// TODO(allisonryan0002): use a controller to initiate dino bonus when dino is
// fully implemented.
class DinoDesert extends Blueprint {
/// {@macro dino_desert}
DinoDesert()
: super(
components: [
ChromeDino()..initialPosition = Vector2(12.3, -6.9),
],
blueprints: [
DinoWalls(),
],
);
}

@ -0,0 +1,34 @@
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template drain}
/// Area located at the bottom of the board to detect when a [Ball] is lost.
/// {@endtemplate}
// TODO(allisonryan0002): move to components package when possible.
class Drain extends BodyComponent with ContactCallbacks {
/// {@macro drain}
Drain() : super(renderBody: false);
@override
Body createBody() {
final shape = EdgeShape()
..set(
BoardDimensions.bounds.bottomLeft.toVector2(),
BoardDimensions.bounds.bottomRight.toVector2(),
);
final fixtureDef = FixtureDef(shape, isSensor: true);
final bodyDef = BodyDef(userData: this);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
// TODO(allisonryan0002): move this to ball.dart when BallLost is removed.
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! ControlledBall) return;
other.controller.lost();
}
}

@ -7,7 +7,7 @@ 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
/// Area positioned at the top right of the board where the [Ball] can bounce
/// off [DashNestBumper]s.
/// {@endtemplate}
class FlutterForest extends Component {

@ -6,7 +6,7 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template sparky_fire_zone}
/// Area positioned at the top left of the [Board] where the [Ball]
/// Area positioned at the top left of the board where the [Ball]
/// can bounce off [SparkyBumper]s.
///
/// When a [Ball] hits [SparkyBumper]s, the bumper animates.

@ -1,60 +0,0 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
/// {@template wall}
/// A continuous generic and [BodyType.static] barrier that divides a game area.
/// {@endtemplate}
// TODO(alestiago): Remove [Wall] for [Pathway.straight].
class Wall extends BodyComponent {
/// {@macro wall}
Wall({
required this.start,
required this.end,
});
/// The [start] of the [Wall].
final Vector2 start;
/// The [end] of the [Wall].
final Vector2 end;
@override
Body createBody() {
final shape = EdgeShape()..set(start, end);
final fixtureDef = FixtureDef(shape)
..restitution = 0.1
..friction = 0;
final bodyDef = BodyDef()
..userData = this
..position = Vector2.zero()
..type = BodyType.static;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
/// {@template bottom_wall}
/// [Wall] located at the bottom of the board.
///
/// {@endtemplate}
class BottomWall extends Wall with ContactCallbacks {
/// {@macro bottom_wall}
BottomWall()
: super(
start: BoardDimensions.bounds.bottomLeft.toVector2(),
end: BoardDimensions.bounds.bottomRight.toVector2(),
);
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! ControlledBall) return;
other.controller.lost();
}
}

@ -6,6 +6,7 @@ import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart';
@ -18,7 +19,8 @@ class PinballGame extends Forge2DGame
with
FlameBloc,
HasKeyboardHandlerComponents,
Controls<_GameBallsController> {
Controls<_GameBallsController>,
TapDetector {
PinballGame({
required this.characterTheme,
required this.audio,
@ -44,20 +46,18 @@ class PinballGame extends Forge2DGame
unawaited(add(gameFlowController = GameFlowController(this)));
unawaited(add(CameraController(this)));
unawaited(add(Backboard.waiting(position: Vector2(0, -88))));
// TODO(allisonryan0002): banish Wall and Board classes in later PR.
await add(BottomWall());
await add(Drain());
await add(BottomGroup());
unawaited(addFromBlueprint(Boundaries()));
unawaited(addFromBlueprint(LaunchRamp()));
final launcher = Launcher();
unawaited(addFromBlueprint(launcher));
unawaited(add(Board()));
await add(FlutterForest());
await addFromBlueprint(SparkyFireZone());
await addFromBlueprint(AndroidAcres());
await addFromBlueprint(DinoDesert());
unawaited(addFromBlueprint(Slingshots()));
unawaited(addFromBlueprint(DinoWalls()));
await add(ChromeDino()..initialPosition = Vector2(12.3, -6.9));
await add(
GoogleWord(
position: Vector2(
@ -70,6 +70,61 @@ class PinballGame extends Forge2DGame
controller.attachTo(launcher.components.whereType<Plunger>().first);
await super.onLoad();
}
BoardSide? focusedBoardSide;
@override
void onTapDown(TapDownInfo info) {
if (info.raw.kind == PointerDeviceKind.touch) {
final rocket = children.whereType<RocketSpriteComponent>().first;
final bounds = rocket.topLeftPosition & rocket.size;
// NOTE(wolfen): As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually.
if (bounds.contains(info.eventPosition.game.toOffset())) {
children.whereType<Plunger>().first.pull();
} else {
final leftSide = info.eventPosition.widget.x < canvasSize.x / 2;
focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right;
final flippers = descendants().whereType<Flipper>().where((flipper) {
return flipper.side == focusedBoardSide;
});
flippers.first.moveUp();
}
}
super.onTapDown(info);
}
@override
void onTapUp(TapUpInfo info) {
final rocket = descendants().whereType<RocketSpriteComponent>().first;
final bounds = rocket.topLeftPosition & rocket.size;
if (bounds.contains(info.eventPosition.game.toOffset())) {
children.whereType<Plunger>().first.release();
} else {
_moveFlippersDown();
}
super.onTapUp(info);
}
@override
void onTapCancel() {
children.whereType<Plunger>().first.release();
_moveFlippersDown();
super.onTapCancel();
}
void _moveFlippersDown() {
if (focusedBoardSide != null) {
final flippers = descendants().whereType<Flipper>().where((flipper) {
return flipper.side == focusedBoardSide;
});
flippers.first.moveDown();
focusedBoardSide = null;
}
}
}
class _GameBallsController extends ComponentController<PinballGame>
@ -116,7 +171,7 @@ class _GameBallsController extends ComponentController<PinballGame>
}
}
class DebugPinballGame extends PinballGame with FPSCounter, TapDetector {
class DebugPinballGame extends PinballGame with FPSCounter {
DebugPinballGame({
required CharacterTheme characterTheme,
required PinballAudio audio,
@ -153,9 +208,11 @@ class DebugPinballGame extends PinballGame with FPSCounter, TapDetector {
@override
void onTapUp(TapUpInfo info) {
add(
ControlledBall.debug()..initialPosition = info.eventPosition.game,
);
super.onTapUp(info);
if (info.raw.kind == PointerDeviceKind.mouse) {
add(ControlledBall.debug()..initialPosition = info.eventPosition.game);
}
}
}

@ -66,14 +66,14 @@ class _ScoreViewDecoration extends StatelessWidget {
@override
Widget build(BuildContext context) {
const radius = BorderRadius.all(Radius.circular(12));
const boardWidth = 5.0;
const borderWidth = 5.0;
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: radius,
border: Border.all(
color: AppColors.white,
width: boardWidth,
width: borderWidth,
),
image: DecorationImage(
fit: BoxFit.cover,
@ -83,7 +83,7 @@ class _ScoreViewDecoration extends StatelessWidget {
),
),
child: Padding(
padding: const EdgeInsets.all(boardWidth - 1),
padding: const EdgeInsets.all(borderWidth - 1),
child: ClipRRect(
borderRadius: radius,
child: child,

@ -28,6 +28,7 @@ class PlayButtonOverlay extends StatelessWidget {
context: context,
barrierDismissible: false,
builder: (_) {
// TODO(arturplaczek): remove after merge StarBlocListener
final height = MediaQuery.of(context).size.height * 0.5;
return Center(

@ -6,6 +6,7 @@ import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_theme/pinball_theme.dart';
import 'package:pinball_ui/pinball_ui.dart';
class CharacterSelectionDialog extends StatelessWidget {
const CharacterSelectionDialog({Key? key}) : super(key: key);
@ -32,25 +33,32 @@ class CharacterSelectionView extends StatelessWidget {
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
return PixelatedDecoration(
header: Text(
l10n.characterSelectionTitle,
style: Theme.of(context).textTheme.headline3,
),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 80),
Text(
l10n.characterSelectionTitle,
style: Theme.of(context).textTheme.headline3,
),
const SizedBox(height: 80),
const _CharacterSelectionGridView(),
const SizedBox(height: 20),
TextButton(
onPressed: () {
Navigator.of(context).pop();
// TODO(arturplaczek): remove after merge StarBlocListener
final height = MediaQuery.of(context).size.height * 0.5;
showDialog<void>(
context: context,
builder: (_) => const HowToPlayDialog(),
builder: (_) => Center(
child: SizedBox(
height: height,
width: height * 1.4,
child: const HowToPlayDialog(),
),
),
);
},
child: Text(l10n.start),

@ -2,6 +2,7 @@
import 'package:flutter/material.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_ui/pinball_ui.dart';
class HowToPlayDialog extends StatelessWidget {
const HowToPlayDialog({Key? key}) : super(key: key);
@ -11,20 +12,16 @@ class HowToPlayDialog extends StatelessWidget {
final l10n = context.l10n;
const spacing = SizedBox(height: 16);
return Dialog(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.howToPlay),
return PixelatedDecoration(
header: Text(l10n.howToPlay),
body: ListView(
children: const [
spacing,
const _LaunchControls(),
_LaunchControls(),
spacing,
const _FlipperControls(),
_FlipperControls(),
],
),
),
);
}
}
@ -41,9 +38,7 @@ class _LaunchControls extends StatelessWidget {
children: [
Text(l10n.launchControls),
const SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
Wrap(
children: const [
KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_down),
spacing,
@ -81,9 +76,7 @@ class _FlipperControls extends StatelessWidget {
],
),
const SizedBox(height: 8),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
Wrap(
children: const [
KeyIndicator.fromKeyName(keyName: 'A'),
rowSpacing,

@ -55,7 +55,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
);
final joint = _FlipperJoint(jointDef);
world.createJoint(joint);
unawaited(mounted.whenComplete(joint.unlock));
}
List<FixtureDef> _createFixtureDefs() {
@ -132,6 +131,15 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
return body;
}
@override
void onMount() {
super.onMount();
gameRef.ready().whenComplete(
() => body.joints.whereType<_FlipperJoint>().first.unlock(),
);
}
}
class _FlipperSpriteComponent extends SpriteComponent with HasGameRef {
@ -215,11 +223,8 @@ class _FlipperJoint extends RevoluteJoint {
/// The joint is locked when initialized in order to force the [Flipper]
/// at its resting position.
void lock() {
const angle = _halfSweepingAngle;
setLimits(
angle * side.direction,
angle * side.direction,
);
final angle = _halfSweepingAngle * side.direction;
setLimits(angle, angle);
}
/// Unlocks the [Flipper] from its resting position.

@ -27,7 +27,7 @@ abstract class RenderPriority {
static const int ballOnSpaceshipRail = _above + spaceshipRail;
/// Render priority for the [Ball] while it's on the [LaunchRamp].
static const int ballOnLaunchRamp = _above + launchRamp;
static const int ballOnLaunchRamp = launchRamp;
// Background
@ -51,7 +51,7 @@ abstract class RenderPriority {
static const int launchRamp = _above + outerBoundary;
static const int launchRampForegroundRailing = _below + ballOnBoard;
static const int launchRampForegroundRailing = ballOnBoard;
static const int plunger = _above + launchRamp;

@ -6,7 +6,6 @@
// https://opensource.org/licenses/MIT.
import 'package:dashbook/dashbook.dart';
import 'package:flutter/material.dart';
import 'package:sandbox/stories/kicker/stories.dart';
import 'package:sandbox/stories/stories.dart';
void main() {
@ -15,11 +14,9 @@ void main() {
addBallStories(dashbook);
addLayerStories(dashbook);
addEffectsStories(dashbook);
addFlipperStories(dashbook);
addBaseboardStories(dashbook);
addChromeDinoStories(dashbook);
addDashNestBumperStories(dashbook);
addKickerStories(dashbook);
addFlutterForestStories(dashbook);
addBottomGroupStories(dashbook);
addPlungerStories(dashbook);
addSlingshotStories(dashbook);
addSparkyBumperStories(dashbook);

@ -1,11 +0,0 @@
import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/baseboard/baseboard_game.dart';
void addBaseboardStories(Dashbook dashbook) {
dashbook.storiesOf('Baseboard').addGame(
title: 'Traced',
description: BaseboardGame.description,
gameBuilder: (_) => BaseboardGame(),
);
}

@ -0,0 +1,24 @@
import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/bottom_group/baseboard_game.dart';
import 'package:sandbox/stories/bottom_group/flipper_game.dart';
import 'package:sandbox/stories/bottom_group/kicker_game.dart';
void addBottomGroupStories(Dashbook dashbook) {
dashbook.storiesOf('Bottom Group')
..addGame(
title: 'Flipper',
description: FlipperGame.description,
gameBuilder: (_) => FlipperGame(),
)
..addGame(
title: 'Kicker',
description: KickerGame.description,
gameBuilder: (_) => KickerGame(),
)
..addGame(
title: 'Baseboard',
description: BaseboardGame.description,
gameBuilder: (_) => BaseboardGame(),
);
}

@ -1,11 +0,0 @@
import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/flipper/flipper_game.dart';
void addFlipperStories(Dashbook dashbook) {
dashbook.storiesOf('Flipper').addGame(
title: 'Traced',
description: FlipperGame.description,
gameBuilder: (_) => FlipperGame(),
);
}

@ -5,7 +5,7 @@ import 'package:sandbox/stories/flutter_forest/signpost_game.dart';
import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_a_game.dart';
import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_b_game.dart';
void addDashNestBumperStories(Dashbook dashbook) {
void addFlutterForestStories(Dashbook dashbook) {
dashbook.storiesOf('Flutter Forest')
..addGame(
title: 'Signpost',

@ -1,11 +0,0 @@
import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/kicker/kicker_game.dart';
void addKickerStories(Dashbook dashbook) {
dashbook.storiesOf('Kickers').addGame(
title: 'Traced',
description: KickerGame.description,
gameBuilder: (_) => KickerGame(),
);
}

@ -1,12 +1,11 @@
export 'android_acres/stories.dart';
export 'backboard/stories.dart';
export 'ball/stories.dart';
export 'baseboard/stories.dart';
export 'bottom_group/stories.dart';
export 'boundaries/stories.dart';
export 'chrome_dino/stories.dart';
export 'dino_wall/stories.dart';
export 'effects/stories.dart';
export 'flipper/stories.dart';
export 'flutter_forest/stories.dart';
export 'google_word/stories.dart';
export 'launch_ramp/stories.dart';

@ -0,0 +1,39 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# VSCode related
.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json

@ -0,0 +1,11 @@
# pinball_ui
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
[![License: MIT][license_badge]][license_link]
UI Toolkit for the Pinball Flutter Application
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
[license_link]: https://opensource.org/licenses/MIT
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis

@ -0,0 +1,4 @@
include: package:very_good_analysis/analysis_options.2.4.0.yaml
analyzer:
exclude:
- lib/**/*.gen.dart

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@ -0,0 +1,78 @@
/// GENERATED CODE - DO NOT MODIFY BY HAND
/// *****************************************************
/// FlutterGen
/// *****************************************************
// ignore_for_file: directives_ordering,unnecessary_import
import 'package:flutter/widgets.dart';
class $AssetsImagesGen {
const $AssetsImagesGen();
$AssetsImagesDialogGen get dialog => const $AssetsImagesDialogGen();
}
class $AssetsImagesDialogGen {
const $AssetsImagesDialogGen();
/// File path: assets/images/dialog/background.png
AssetGenImage get background =>
const AssetGenImage('assets/images/dialog/background.png');
}
class Assets {
Assets._();
static const $AssetsImagesGen images = $AssetsImagesGen();
}
class AssetGenImage extends AssetImage {
const AssetGenImage(String assetName)
: super(assetName, package: 'pinball_ui');
Image image({
Key? key,
ImageFrameBuilder? frameBuilder,
ImageLoadingBuilder? loadingBuilder,
ImageErrorWidgetBuilder? errorBuilder,
String? semanticLabel,
bool excludeFromSemantics = false,
double? width,
double? height,
Color? color,
BlendMode? colorBlendMode,
BoxFit? fit,
AlignmentGeometry alignment = Alignment.center,
ImageRepeat repeat = ImageRepeat.noRepeat,
Rect? centerSlice,
bool matchTextDirection = false,
bool gaplessPlayback = false,
bool isAntiAlias = false,
FilterQuality filterQuality = FilterQuality.low,
}) {
return Image(
key: key,
image: this,
frameBuilder: frameBuilder,
loadingBuilder: loadingBuilder,
errorBuilder: errorBuilder,
semanticLabel: semanticLabel,
excludeFromSemantics: excludeFromSemantics,
width: width,
height: height,
color: color,
colorBlendMode: colorBlendMode,
fit: fit,
alignment: alignment,
repeat: repeat,
centerSlice: centerSlice,
matchTextDirection: matchTextDirection,
gaplessPlayback: gaplessPlayback,
isAntiAlias: isAntiAlias,
filterQuality: filterQuality,
);
}
String get path => assetName;
}

@ -0,0 +1 @@
export 'assets.gen.dart';

@ -0,0 +1,3 @@
library pinball_ui;
export 'src/dialog/dialog.dart';

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

@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:pinball_ui/gen/gen.dart';
/// {@template pixelated_decoration}
/// Widget with pixelated background and layout defined for dialog displays.
/// {@endtemplate}
class PixelatedDecoration extends StatelessWidget {
/// {@macro pixelated_decoration}
const PixelatedDecoration({
Key? key,
required Widget header,
required Widget body,
}) : _header = header,
_body = body,
super(key: key);
final Widget _header;
final Widget _body;
@override
Widget build(BuildContext context) {
const radius = BorderRadius.all(Radius.circular(12));
return Material(
borderRadius: radius,
child: Padding(
padding: const EdgeInsets.all(5),
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: radius,
image: DecorationImage(
fit: BoxFit.fill,
image: AssetImage(Assets.images.dialog.background.keyName),
),
),
child: ClipRRect(
borderRadius: radius,
child: Column(
children: [
Expanded(
child: Center(
child: _header,
),
),
Expanded(
flex: 4,
child: _body,
),
],
),
),
),
),
);
}
}

@ -0,0 +1,29 @@
name: pinball_ui
description: UI Toolkit for the Pinball Flutter Application
version: 1.0.0+1
publish_to: none
environment:
sdk: ">=2.16.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
test: ^1.19.2
very_good_analysis: ^2.4.0
flutter:
uses-material-design: true
generate: true
assets:
- assets/images/dialog/
flutter_gen:
line_length: 80
assets:
package_parameter_enabled: true

@ -0,0 +1,26 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_ui/pinball_ui.dart';
void main() {
group('PixelatedDecoration', () {
testWidgets('renders header and body', (tester) async {
const headerText = 'header';
const bodyText = 'body';
await tester.pumpWidget(
MaterialApp(
home: PixelatedDecoration(
header: Text(headerText),
body: Text(bodyText),
),
),
);
expect(find.text(headerText), findsOneWidget);
expect(find.text(bodyText), findsOneWidget);
});
});
}

@ -499,6 +499,13 @@ packages:
relative: true
source: path
version: "1.0.0+1"
pinball_ui:
dependency: "direct main"
description:
path: "packages/pinball_ui"
relative: true
source: path
version: "1.0.0+1"
platform:
dependency: transitive
description:

@ -35,6 +35,8 @@ dependencies:
path: packages/pinball_flame
pinball_theme:
path: packages/pinball_theme
pinball_ui:
path: packages/pinball_ui
dev_dependencies:
bloc_test: ^9.0.2

@ -1,110 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
Assets.images.dash.bumper.a.active.keyName,
Assets.images.dash.bumper.a.inactive.keyName,
Assets.images.dash.bumper.b.active.keyName,
Assets.images.dash.bumper.b.inactive.keyName,
Assets.images.dash.animatronic.keyName,
Assets.images.signpost.inactive.keyName,
Assets.images.signpost.active1.keyName,
Assets.images.signpost.active2.keyName,
Assets.images.signpost.active3.keyName,
Assets.images.baseboard.left.keyName,
Assets.images.baseboard.right.keyName,
Assets.images.flipper.left.keyName,
Assets.images.flipper.right.keyName,
];
final flameTester = FlameTester(
() => EmptyPinballTestGame(assets: assets),
);
group('Board', () {
flameTester.test(
'loads correctly',
(game) async {
final board = Board();
await game.ready();
await game.ensureAdd(board);
expect(game.contains(board), isTrue);
},
);
group('loads', () {
flameTester.test(
'one left flipper',
(game) async {
final board = Board();
await game.ready();
await game.ensureAdd(board);
final leftFlippers = board.descendants().whereType<Flipper>().where(
(flipper) => flipper.side.isLeft,
);
expect(leftFlippers.length, equals(1));
},
);
flameTester.test(
'one right flipper',
(game) async {
final board = Board();
await game.ready();
await game.ensureAdd(board);
final rightFlippers = board.descendants().whereType<Flipper>().where(
(flipper) => flipper.side.isRight,
);
expect(rightFlippers.length, equals(1));
},
);
flameTester.test(
'two Baseboards',
(game) async {
final board = Board();
await game.ready();
await game.ensureAdd(board);
final baseboards = board.descendants().whereType<Baseboard>();
expect(baseboards.length, equals(2));
},
);
flameTester.test(
'two Kickers',
(game) async {
final board = Board();
await game.ready();
await game.ensureAdd(board);
final kickers = board.descendants().whereType<Kicker>();
expect(kickers.length, equals(2));
},
);
flameTester.test(
'one FlutterForest',
(game) async {
final board = Board();
await game.ready();
await game.ensureAdd(board);
final flutterForest = board.descendants().whereType<FlutterForest>();
expect(flutterForest.length, equals(1));
},
);
});
});
}

@ -0,0 +1,86 @@
// ignore_for_file: cascade_invocations
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.baseboard.left.keyName,
Assets.images.baseboard.right.keyName,
Assets.images.flipper.left.keyName,
Assets.images.flipper.right.keyName,
];
final flameTester = FlameTester(
() => EmptyPinballTestGame(assets: assets),
);
group('BottomGroup', () {
flameTester.test(
'loads correctly',
(game) async {
final bottomGroup = BottomGroup();
await game.ensureAdd(bottomGroup);
expect(game.contains(bottomGroup), isTrue);
},
);
group('loads', () {
flameTester.test(
'one left flipper',
(game) async {
final bottomGroup = BottomGroup();
await game.ensureAdd(bottomGroup);
final leftFlippers =
bottomGroup.descendants().whereType<Flipper>().where(
(flipper) => flipper.side.isLeft,
);
expect(leftFlippers.length, equals(1));
},
);
flameTester.test(
'one right flipper',
(game) async {
final bottomGroup = BottomGroup();
await game.ensureAdd(bottomGroup);
final rightFlippers =
bottomGroup.descendants().whereType<Flipper>().where(
(flipper) => flipper.side.isRight,
);
expect(rightFlippers.length, equals(1));
},
);
flameTester.test(
'two Baseboards',
(game) async {
final bottomGroup = BottomGroup();
await game.ensureAdd(bottomGroup);
final basebottomGroups =
bottomGroup.descendants().whereType<Baseboard>();
expect(basebottomGroups.length, equals(2));
},
);
flameTester.test(
'two Kickers',
(game) async {
final bottomGroup = BottomGroup();
await game.ensureAdd(bottomGroup);
final kickers = bottomGroup.descendants().whereType<Kicker>();
expect(kickers.length, equals(2));
},
);
});
});
}

@ -0,0 +1,60 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group('Drain', () {
flameTester.test(
'loads correctly',
(game) async {
final drain = Drain();
await game.ensureAdd(drain);
expect(game.contains(drain), isTrue);
},
);
flameTester.test(
'body is static',
(game) async {
final drain = Drain();
await game.ensureAdd(drain);
expect(drain.body.bodyType, equals(BodyType.static));
},
);
flameTester.test(
'is sensor',
(game) async {
final drain = Drain();
await game.ensureAdd(drain);
expect(drain.body.fixtures.first.isSensor, isTrue);
},
);
test(
'calls lost on contact with ball',
() async {
final drain = Drain();
final ball = MockControlledBall();
final controller = MockBallController();
when(() => ball.controller).thenReturn(controller);
drain.beginContact(ball, MockContact());
verify(controller.lost).called(1);
},
);
});
}

@ -1,165 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballTestGame.new);
group('Wall', () {
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final wall = Wall(
start: Vector2.zero(),
end: Vector2(100, 0),
);
await game.ensureAdd(wall);
expect(game.contains(wall), isTrue);
},
);
group('body', () {
flameTester.test(
'positions correctly',
(game) async {
final wall = Wall(
start: Vector2.zero(),
end: Vector2(100, 0),
);
await game.ensureAdd(wall);
game.contains(wall);
expect(wall.body.position, Vector2.zero());
},
);
flameTester.test(
'is static',
(game) async {
final wall = Wall(
start: Vector2.zero(),
end: Vector2(100, 0),
);
await game.ensureAdd(wall);
expect(wall.body.bodyType, equals(BodyType.static));
},
);
});
group('fixture', () {
flameTester.test(
'exists',
(game) async {
final wall = Wall(
start: Vector2.zero(),
end: Vector2(100, 0),
);
await game.ensureAdd(wall);
expect(wall.body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
'has restitution',
(game) async {
final wall = Wall(
start: Vector2.zero(),
end: Vector2(100, 0),
);
await game.ensureAdd(wall);
final fixture = wall.body.fixtures[0];
expect(fixture.restitution, greaterThan(0));
},
);
flameTester.test(
'has no friction',
(game) async {
final wall = Wall(
start: Vector2.zero(),
end: Vector2(100, 0),
);
await game.ensureAdd(wall);
final fixture = wall.body.fixtures[0];
expect(fixture.friction, equals(0));
},
);
});
});
group(
'BottomWall',
() {
group('removes ball on contact', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = GameBloc();
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
);
flameBlocTester.testGameWidget(
'when ball is launch',
setUp: (game, tester) async {
final ball = ControlledBall.launch(
characterTheme: game.characterTheme,
);
final wall = BottomWall();
await game.ensureAddAll([ball, wall]);
beginContact(game, ball, wall);
await game.ready();
expect(game.contains(ball), isFalse);
},
);
flameBlocTester.testGameWidget(
'when ball is bonus',
setUp: (game, tester) async {
final ball = ControlledBall.bonus(
characterTheme: game.characterTheme,
);
final wall = BottomWall();
await game.ensureAddAll([ball, wall]);
beginContact(game, ball, wall);
await game.ready();
expect(game.contains(ball), isFalse);
},
);
flameBlocTester.testGameWidget(
'when ball is debug',
setUp: (game, tester) async {
final ball = ControlledBall.debug();
final wall = BottomWall();
await game.ensureAddAll([ball, wall]);
beginContact(game, ball, wall);
await game.ready();
expect(game.contains(ball), isFalse);
},
);
});
},
);
}

@ -3,6 +3,7 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
@ -103,38 +104,47 @@ void main() {
// TODO(alestiago): tests that Blueprints get added once the Blueprint
// class is removed.
flameTester.test(
'has only one BottomWall',
'has only one Drain',
(game) async {
await game.ready();
expect(
game.children.whereType<BottomWall>().length,
game.children.whereType<Drain>().length,
equals(1),
);
},
);
flameTester.test(
'has only one Plunger',
'has only one BottomGroup',
(game) async {
await game.ready();
expect(
game.children.whereType<Plunger>().length,
game.children.whereType<BottomGroup>().length,
equals(1),
);
},
);
flameTester.test(
'has one Board',
'has only one Plunger',
(game) async {
await game.ready();
expect(
game.children.whereType<Board>().length,
game.children.whereType<Plunger>().length,
equals(1),
);
},
);
flameTester.test('has one FlutterForest', (game) async {
await game.ready();
expect(
game.children.whereType<FlutterForest>().length,
equals(1),
);
});
flameTester.test(
'one GoogleWord',
(game) async {
@ -229,6 +239,181 @@ void main() {
);
});
});
group('flipper control', () {
flameTester.test('tap down moves left flipper up', (game) async {
await game.ready();
final eventPosition = MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2.zero());
when(() => eventPosition.widget).thenReturn(Vector2.zero());
final raw = MockTapDownDetails();
when(() => raw.kind).thenReturn(PointerDeviceKind.touch);
final tapDownEvent = MockTapDownInfo();
when(() => tapDownEvent.eventPosition).thenReturn(eventPosition);
when(() => tapDownEvent.raw).thenReturn(raw);
final flippers = game.descendants().whereType<Flipper>().where(
(flipper) => flipper.side == BoardSide.left,
);
game.onTapDown(tapDownEvent);
expect(flippers.first.body.linearVelocity.y, isNegative);
});
flameTester.test('tap down moves right flipper up', (game) async {
await game.ready();
final eventPosition = MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2.zero());
when(() => eventPosition.widget).thenReturn(game.canvasSize);
final raw = MockTapDownDetails();
when(() => raw.kind).thenReturn(PointerDeviceKind.touch);
final tapDownEvent = MockTapDownInfo();
when(() => tapDownEvent.eventPosition).thenReturn(eventPosition);
when(() => tapDownEvent.raw).thenReturn(raw);
final flippers = game.descendants().whereType<Flipper>().where(
(flipper) => flipper.side == BoardSide.right,
);
game.onTapDown(tapDownEvent);
expect(flippers.first.body.linearVelocity.y, isNegative);
});
flameTester.test('tap up moves flipper down', (game) async {
await game.ready();
final eventPosition = MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2.zero());
when(() => eventPosition.widget).thenReturn(Vector2.zero());
final raw = MockTapDownDetails();
when(() => raw.kind).thenReturn(PointerDeviceKind.touch);
final tapDownEvent = MockTapDownInfo();
when(() => tapDownEvent.eventPosition).thenReturn(eventPosition);
when(() => tapDownEvent.raw).thenReturn(raw);
final flippers = game.descendants().whereType<Flipper>().where(
(flipper) => flipper.side == BoardSide.left,
);
game.onTapDown(tapDownEvent);
expect(flippers.first.body.linearVelocity.y, isNegative);
final tapUpEvent = MockTapUpInfo();
when(() => tapUpEvent.eventPosition).thenReturn(eventPosition);
game.onTapUp(tapUpEvent);
await game.ready();
expect(flippers.first.body.linearVelocity.y, isPositive);
});
flameTester.test('tap cancel moves flipper down', (game) async {
await game.ready();
final eventPosition = MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2.zero());
when(() => eventPosition.widget).thenReturn(Vector2.zero());
final raw = MockTapDownDetails();
when(() => raw.kind).thenReturn(PointerDeviceKind.touch);
final tapDownEvent = MockTapDownInfo();
when(() => tapDownEvent.eventPosition).thenReturn(eventPosition);
when(() => tapDownEvent.raw).thenReturn(raw);
final flippers = game.descendants().whereType<Flipper>().where(
(flipper) => flipper.side == BoardSide.left,
);
game.onTapDown(tapDownEvent);
expect(flippers.first.body.linearVelocity.y, isNegative);
game.onTapCancel();
expect(flippers.first.body.linearVelocity.y, isPositive);
});
});
group('plunger control', () {
flameTester.test('tap down moves plunger down', (game) async {
await game.ready();
final eventPosition = MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2(40, 60));
final raw = MockTapDownDetails();
when(() => raw.kind).thenReturn(PointerDeviceKind.touch);
final tapDownEvent = MockTapDownInfo();
when(() => tapDownEvent.eventPosition).thenReturn(eventPosition);
when(() => tapDownEvent.raw).thenReturn(raw);
final plunger = game.descendants().whereType<Plunger>().first;
game.onTapDown(tapDownEvent);
expect(plunger.body.linearVelocity.y, equals(7));
});
flameTester.test('tap up releases plunger', (game) async {
final eventPosition = MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2(40, 60));
final raw = MockTapDownDetails();
when(() => raw.kind).thenReturn(PointerDeviceKind.touch);
final tapDownEvent = MockTapDownInfo();
when(() => tapDownEvent.eventPosition).thenReturn(eventPosition);
when(() => tapDownEvent.raw).thenReturn(raw);
final plunger = game.descendants().whereType<Plunger>().first;
game.onTapDown(tapDownEvent);
expect(plunger.body.linearVelocity.y, equals(7));
final tapUpEvent = MockTapUpInfo();
when(() => tapUpEvent.eventPosition).thenReturn(eventPosition);
game.onTapUp(tapUpEvent);
expect(plunger.body.linearVelocity.y, equals(0));
});
flameTester.test('tap cancel releases plunger', (game) async {
await game.ready();
final eventPosition = MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2(40, 60));
final raw = MockTapDownDetails();
when(() => raw.kind).thenReturn(PointerDeviceKind.touch);
final tapDownEvent = MockTapDownInfo();
when(() => tapDownEvent.eventPosition).thenReturn(eventPosition);
when(() => tapDownEvent.raw).thenReturn(raw);
final plunger = game.descendants().whereType<Plunger>().first;
game.onTapDown(tapDownEvent);
expect(plunger.body.linearVelocity.y, equals(7));
game.onTapCancel();
expect(plunger.body.linearVelocity.y, equals(0));
});
});
});
group('DebugPinballGame', () {
@ -238,8 +423,12 @@ void main() {
final eventPosition = MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2.all(10));
final raw = MockTapUpDetails();
when(() => raw.kind).thenReturn(PointerDeviceKind.mouse);
final tapUpEvent = MockTapUpInfo();
when(() => tapUpEvent.eventPosition).thenReturn(eventPosition);
when(() => tapUpEvent.raw).thenReturn(raw);
final previousBalls =
game.descendants().whereType<ControlledBall>().toList();

@ -2,7 +2,7 @@ import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart';
@ -15,9 +15,7 @@ import 'package:pinball_components/pinball_components.dart';
class MockPinballGame extends Mock implements PinballGame {}
class MockWall extends Mock implements Wall {}
class MockBottomWall extends Mock implements BottomWall {}
class MockDrain extends Mock implements Drain {}
class MockBody extends Mock implements Body {}
@ -55,8 +53,14 @@ class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent {
}
}
class MockTapDownInfo extends Mock implements TapDownInfo {}
class MockTapDownDetails extends Mock implements TapDownDetails {}
class MockTapUpInfo extends Mock implements TapUpInfo {}
class MockTapUpDetails extends Mock implements TapUpDetails {}
class MockEventPosition extends Mock implements EventPosition {}
class MockFilter extends Mock implements Filter {}

@ -2,16 +2,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/start_game/start_game.dart';
import '../../helpers/helpers.dart';
void main() {
group('HowToPlayDialog', () {
testWidgets('displays dialog', (tester) async {
testWidgets('displays content', (tester) async {
final l10n = await AppLocalizations.delegate.load(Locale('en'));
await tester.pumpApp(HowToPlayDialog());
expect(find.byType(Dialog), findsOneWidget);
expect(find.text(l10n.launchControls), findsOneWidget);
});
});

Loading…
Cancel
Save