Merge branch 'main' into fix/game-hud-position

pull/289/head
arturplaczek 3 years ago committed by GitHub
commit ab229a1488
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

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

@ -40,7 +40,8 @@ class ScoringBehavior extends Component with HasGameRef<PinballGame> {
@override
Future<void> onLoad() async {
gameRef.read<GameBloc>().add(Scored(points: _points.value));
await gameRef.firstChild<ZCanvasComponent>()!.add(
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
await canvas.add(
ScoreComponent(
points: _points,
position: _position,

@ -15,7 +15,16 @@ class AndroidAcres extends Component {
AndroidAcres()
: super(
children: [
SpaceshipRamp(),
SpaceshipRamp(
children: [
RampShotBehavior(
points: Points.fiveThousand,
),
RampBonusBehavior(
points: Points.oneMillion,
),
],
),
SpaceshipRail(),
AndroidSpaceship(position: Vector2(-26.5, -28.5)),
AndroidAnimatronic(

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

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

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

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

@ -36,12 +36,14 @@ class DinoDesert extends Component {
}
class _BarrierBehindDino extends BodyComponent {
_BarrierBehindDino() : super(renderBody: false);
@override
Body createBody() {
final shape = EdgeShape()
..set(
Vector2(25, -14.2),
Vector2(25, -7.7),
Vector2(25.3, -14.2),
Vector2(25.3, -7.7),
);
return world.createBody(BodyDef())..createFixtureFromShape(shape);

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

@ -13,7 +13,6 @@ extension PinballGameAssetsX on PinballGame {
return [
images.load(components.Assets.images.boardBackground.keyName),
images.load(components.Assets.images.ball.ball.keyName),
images.load(components.Assets.images.ball.flameEffect.keyName),
images.load(components.Assets.images.signpost.inactive.keyName),
images.load(components.Assets.images.signpost.active1.keyName),
@ -132,10 +131,18 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.flapper.backSupport.keyName),
images.load(components.Assets.images.flapper.frontSupport.keyName),
images.load(components.Assets.images.flapper.flap.keyName),
images.load(components.Assets.images.skillShot.decal.keyName),
images.load(components.Assets.images.skillShot.pin.keyName),
images.load(components.Assets.images.skillShot.lit.keyName),
images.load(components.Assets.images.skillShot.dimmed.keyName),
images.load(dashTheme.leaderboardIcon.keyName),
images.load(sparkyTheme.leaderboardIcon.keyName),
images.load(androidTheme.leaderboardIcon.keyName),
images.load(dinoTheme.leaderboardIcon.keyName),
images.load(androidTheme.ball.keyName),
images.load(dashTheme.ball.keyName),
images.load(dinoTheme.ball.keyName),
images.load(sparkyTheme.ball.keyName),
];
}
}

@ -7,6 +7,7 @@ import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_audio/pinball_audio.dart';
@ -57,6 +58,11 @@ class PinballGame extends PinballForge2DGame
GoogleWord(position: Vector2(-4.25, 1.8)),
Multipliers(),
Multiballs(),
SkillShot(
children: [
ScoringContactBehavior(points: Points.oneMillion),
],
),
];
final characterAreas = [
AndroidAcres(),
@ -66,6 +72,13 @@ class PinballGame extends PinballForge2DGame
];
await add(
CanvasComponent(
onSpritePainted: (paint) {
if (paint.filterQuality != FilterQuality.medium) {
paint.filterQuality = FilterQuality.medium;
}
},
children: [
ZCanvasComponent(
children: [
...machine,
@ -76,6 +89,8 @@ class PinballGame extends PinballForge2DGame
Launcher(),
],
),
],
),
);
await super.onLoad();
@ -163,7 +178,7 @@ class _GameBallsController extends ComponentController<PinballGame>
plunger.body.position.x,
plunger.body.position.y - Ball.size.y,
);
component.firstChild<ZCanvasComponent>()?.add(ball);
component.descendants().whereType<ZCanvasComponent>().single.add(ball);
});
}
}
@ -197,9 +212,10 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
super.onTapUp(pointerId, info);
if (info.raw.kind == PointerDeviceKind.mouse) {
final canvas = descendants().whereType<ZCanvasComponent>().single;
final ball = ControlledBall.debug()
..initialPosition = info.eventPosition.game;
firstChild<ZCanvasComponent>()?.add(ball);
canvas.add(ball);
}
}
@ -224,10 +240,11 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
}
void _turboChargeBall(Vector2 line) {
final canvas = descendants().whereType<ZCanvasComponent>().single;
final ball = ControlledBall.debug()..initialPosition = lineStart!;
final impulse = line * -1 * 10;
ball.add(BallTurboChargingBehavior(impulse: impulse));
firstChild<ZCanvasComponent>()?.add(ball);
canvas.add(ball);
}
}

@ -65,7 +65,8 @@ class _CharacterGrid extends StatelessWidget {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
Expanded(
child: Column(
children: [
_Character(
key: const Key('sparky_character_selection'),
@ -80,8 +81,10 @@ class _CharacterGrid extends StatelessWidget {
),
],
),
),
const SizedBox(width: 6),
Column(
Expanded(
child: Column(
children: [
_Character(
key: const Key('dash_character_selection'),
@ -96,6 +99,7 @@ class _CharacterGrid extends StatelessWidget {
),
],
),
),
],
);
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -35,6 +35,7 @@ class $AssetsImagesGen {
$AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
$AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen();
$AssetsImagesSkillShotGen get skillShot => const $AssetsImagesSkillShotGen();
$AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen();
$AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen();
}
@ -272,6 +273,26 @@ class $AssetsImagesSignpostGen {
const AssetGenImage('assets/images/signpost/inactive.png');
}
class $AssetsImagesSkillShotGen {
const $AssetsImagesSkillShotGen();
/// File path: assets/images/skill_shot/decal.png
AssetGenImage get decal =>
const AssetGenImage('assets/images/skill_shot/decal.png');
/// File path: assets/images/skill_shot/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/skill_shot/dimmed.png');
/// File path: assets/images/skill_shot/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/skill_shot/lit.png');
/// File path: assets/images/skill_shot/pin.png
AssetGenImage get pin =>
const AssetGenImage('assets/images/skill_shot/pin.png');
}
class $AssetsImagesSlingshotGen {
const $AssetsImagesSlingshotGen();

@ -2,9 +2,10 @@ import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
export 'behaviors/behaviors.dart';
@ -14,11 +15,11 @@ export 'behaviors/behaviors.dart';
class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
/// {@macro ball}
Ball({
required this.baseColor,
String? assetPath,
}) : super(
renderBody: false,
children: [
_BallSpriteComponent()..tint(baseColor.withOpacity(0.5)),
_BallSpriteComponent(assetPath: assetPath),
BallScalingBehavior(),
BallGravitatingBehavior(),
],
@ -35,7 +36,7 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
///
/// This can be used for testing [Ball]'s behaviors in isolation.
@visibleForTesting
Ball.test({required this.baseColor})
Ball.test()
: super(
children: [_BallSpriteComponent()],
);
@ -43,9 +44,6 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
/// The size of the [Ball].
static final Vector2 size = Vector2.all(4.13);
/// The base [Color] used to tint this [Ball].
final Color baseColor;
@override
Body createBody() {
final shape = CircleShape()..radius = size.x / 2;
@ -79,14 +77,22 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
}
class _BallSpriteComponent extends SpriteComponent with HasGameRef {
_BallSpriteComponent({
this.assetPath,
}) : super(
anchor: Anchor.center,
);
final String? assetPath;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.ball.ball.keyName,
final sprite = Sprite(
gameRef.images
.fromCache(assetPath ?? theme.Assets.images.dash.ball.keyName),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
size = sprite.originalSize / 12.5;
}
}

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

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

@ -21,7 +21,7 @@ export 'joint_anchor.dart';
export 'kicker/kicker.dart';
export 'launch_ramp.dart';
export 'layer.dart';
export 'layer_sensor.dart';
export 'layer_sensor/layer_sensor.dart';
export 'multiball/multiball.dart';
export 'multiplier/multiplier.dart';
export 'plunger.dart';
@ -29,9 +29,10 @@ export 'rocket.dart';
export 'score_component.dart';
export 'shapes/shapes.dart';
export 'signpost/signpost.dart';
export 'skill_shot/skill_shot.dart';
export 'slingshot.dart';
export 'spaceship_rail.dart';
export 'spaceship_ramp.dart';
export 'spaceship_ramp/spaceship_ramp.dart';
export 'sparky_animatronic.dart';
export 'sparky_bumper/sparky_bumper.dart';
export 'sparky_computer.dart';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -89,6 +89,7 @@ flutter:
- assets/images/score/
- assets/images/backbox/
- assets/images/flapper/
- assets/images/skill_shot/
flutter_gen:
line_length: 80

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

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

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

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

@ -9,7 +9,6 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SpaceshipRampGame extends BallGame with KeyboardEvents {
SpaceshipRampGame()
: super(
color: Colors.blue,
ballPriority: ZIndexes.ballOnSpaceshipRamp,
ballLayer: Layer.spaceshipEntranceRamp,
imagesFileNames: [
@ -54,7 +53,7 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents {
) {
if (event is RawKeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.space) {
_spaceshipRamp.progress();
_spaceshipRamp.bloc.onAscendingBallEntered();
return KeyEventResult.handled;
}

@ -1,9 +1,20 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import 'package:sandbox/common/common.dart';
class BallBoosterGame extends LineGame {
BallBoosterGame()
: super(
imagesFileNames: [
theme.Assets.images.android.ball.keyName,
theme.Assets.images.dash.ball.keyName,
theme.Assets.images.dino.ball.keyName,
theme.Assets.images.sparky.ball.keyName,
Assets.images.ball.flameEffect.keyName,
],
);
static const description = '''
Shows how a Ball with a boost works.
@ -12,7 +23,7 @@ class BallBoosterGame extends LineGame {
@override
void onLine(Vector2 line) {
final ball = Ball(baseColor: Colors.transparent);
final ball = Ball();
final impulse = line * -1 * 20;
ball.add(BallTurboChargingBehavior(impulse: impulse));

@ -1,17 +1,20 @@
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import 'package:sandbox/common/common.dart';
class BallGame extends AssetsGame with TapDetector, Traceable {
BallGame({
this.color = Colors.blue,
this.ballPriority = 0,
this.ballLayer = Layer.all,
this.character,
List<String>? imagesFileNames,
}) : super(
imagesFileNames: [
Assets.images.ball.ball.keyName,
theme.Assets.images.android.ball.keyName,
theme.Assets.images.dash.ball.keyName,
theme.Assets.images.dino.ball.keyName,
theme.Assets.images.sparky.ball.keyName,
if (imagesFileNames != null) ...imagesFileNames,
],
);
@ -22,14 +25,23 @@ class BallGame extends AssetsGame with TapDetector, Traceable {
- Tap anywhere on the screen to spawn a ball into the game.
''';
final Color color;
static final characterBallPaths = <String, String>{
'Dash': theme.Assets.images.dash.ball.keyName,
'Sparky': theme.Assets.images.sparky.ball.keyName,
'Android': theme.Assets.images.android.ball.keyName,
'Dino': theme.Assets.images.dino.ball.keyName,
};
final int ballPriority;
final Layer ballLayer;
final String? character;
@override
void onTapUp(TapUpInfo info) {
add(
Ball(baseColor: color)
Ball(
assetPath: characterBallPaths[character],
)
..initialPosition = info.eventPosition.game
..layer = ballLayer
..priority = ballPriority,

@ -1,5 +1,4 @@
import 'package:dashbook/dashbook.dart';
import 'package:flutter/material.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/ball_booster_game.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
@ -7,10 +6,14 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart';
void addBallStories(Dashbook dashbook) {
dashbook.storiesOf('Ball')
..addGame(
title: 'Colored',
title: 'Themed',
description: BallGame.description,
gameBuilder: (context) => BallGame(
color: context.colorProperty('color', Colors.blue),
character: context.listProperty(
'Character',
BallGame.characterBallPaths.keys.first,
BallGame.characterBallPaths.keys.toList(),
),
),
)
..addGame(

@ -1,14 +1,10 @@
import 'dart:ui';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class GoogleLetterGame extends BallGame {
GoogleLetterGame()
: super(
color: const Color(0xFF009900),
imagesFileNames: [
Assets.images.googleWord.letter1.lit.keyName,
Assets.images.googleWord.letter1.dimmed.keyName,

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

@ -6,8 +6,6 @@ import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class PlungerGame extends BallGame with KeyboardEvents, Traceable {
PlungerGame() : super(color: const Color(0xFFFF0000));
static const description = '''
Shows how Plunger is rendered.

@ -2,31 +2,36 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
final assets = [
theme.Assets.images.android.ball.keyName,
theme.Assets.images.dash.ball.keyName,
theme.Assets.images.dino.ball.keyName,
theme.Assets.images.sparky.ball.keyName,
];
group('Ball', () {
const baseColor = Color(0xFFFFFFFF);
final flameTester = FlameTester(() => TestGame(assets));
group('Ball', () {
test(
'can be instantiated',
() {
expect(Ball(baseColor: baseColor), isA<Ball>());
expect(Ball.test(baseColor: baseColor), isA<Ball>());
expect(Ball(), isA<Ball>());
expect(Ball.test(), isA<Ball>());
},
);
flameTester.test(
'loads correctly',
(game) async {
final ball = Ball(baseColor: baseColor);
final ball = Ball();
await game.ready();
await game.ensureAdd(ball);
@ -36,7 +41,7 @@ void main() {
group('adds', () {
flameTester.test('a BallScalingBehavior', (game) async {
final ball = Ball(baseColor: baseColor);
final ball = Ball();
await game.ensureAdd(ball);
expect(
ball.descendants().whereType<BallScalingBehavior>().length,
@ -45,7 +50,7 @@ void main() {
});
flameTester.test('a BallGravitatingBehavior', (game) async {
final ball = Ball(baseColor: baseColor);
final ball = Ball();
await game.ensureAdd(ball);
expect(
ball.descendants().whereType<BallGravitatingBehavior>().length,
@ -58,7 +63,7 @@ void main() {
flameTester.test(
'is dynamic',
(game) async {
final ball = Ball(baseColor: baseColor);
final ball = Ball();
await game.ensureAdd(ball);
expect(ball.body.bodyType, equals(BodyType.dynamic));
@ -67,7 +72,7 @@ void main() {
group('can be moved', () {
flameTester.test('by its weight', (game) async {
final ball = Ball(baseColor: baseColor);
final ball = Ball();
await game.ensureAdd(ball);
game.update(1);
@ -75,7 +80,7 @@ void main() {
});
flameTester.test('by applying velocity', (game) async {
final ball = Ball(baseColor: baseColor);
final ball = Ball();
await game.ensureAdd(ball);
ball.body.gravityScale = Vector2.zero();
@ -90,7 +95,7 @@ void main() {
flameTester.test(
'exists',
(game) async {
final ball = Ball(baseColor: baseColor);
final ball = Ball();
await game.ensureAdd(ball);
expect(ball.body.fixtures[0], isA<Fixture>());
@ -100,7 +105,7 @@ void main() {
flameTester.test(
'is dense',
(game) async {
final ball = Ball(baseColor: baseColor);
final ball = Ball();
await game.ensureAdd(ball);
final fixture = ball.body.fixtures[0];
@ -111,7 +116,7 @@ void main() {
flameTester.test(
'shape is circular',
(game) async {
final ball = Ball(baseColor: baseColor);
final ball = Ball();
await game.ensureAdd(ball);
final fixture = ball.body.fixtures[0];
@ -123,7 +128,7 @@ void main() {
flameTester.test(
'has Layer.all as default filter maskBits',
(game) async {
final ball = Ball(baseColor: baseColor);
final ball = Ball();
await game.ready();
await game.ensureAdd(ball);
await game.ready();
@ -137,7 +142,7 @@ void main() {
group('stop', () {
group("can't be moved", () {
flameTester.test('by its weight', (game) async {
final ball = Ball(baseColor: baseColor);
final ball = Ball();
await game.ensureAdd(ball);
ball.stop();
@ -152,7 +157,7 @@ void main() {
flameTester.test(
'by its weight when previously stopped',
(game) async {
final ball = Ball(baseColor: baseColor);
final ball = Ball();
await game.ensureAdd(ball);
ball.stop();
ball.resume();
@ -165,7 +170,7 @@ void main() {
flameTester.test(
'by applying velocity when previously stopped',
(game) async {
final ball = Ball(baseColor: baseColor);
final ball = Ball();
await game.ensureAdd(ball);
ball.stop();
ball.resume();

@ -1,21 +1,19 @@
// ignore_for_file: cascade_invocations
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final asset = Assets.images.ball.ball.keyName;
final asset = theme.Assets.images.dash.ball.keyName;
final flameTester = FlameTester(() => TestGame([asset]));
group('BallGravitatingBehavior', () {
const baseColor = Color(0xFFFFFFFF);
test('can be instantiated', () {
expect(
BallGravitatingBehavior(),
@ -24,7 +22,7 @@ void main() {
});
flameTester.test('can be loaded', (game) async {
final ball = Ball.test(baseColor: baseColor);
final ball = Ball.test();
final behavior = BallGravitatingBehavior();
await ball.add(behavior);
await game.ensureAdd(ball);
@ -37,12 +35,10 @@ void main() {
flameTester.test(
"overrides the body's horizontal gravity symmetrically",
(game) async {
final ball1 = Ball.test(baseColor: baseColor)
..initialPosition = Vector2(10, 0);
final ball1 = Ball.test()..initialPosition = Vector2(10, 0);
await ball1.add(BallGravitatingBehavior());
final ball2 = Ball.test(baseColor: baseColor)
..initialPosition = Vector2(-10, 0);
final ball2 = Ball.test()..initialPosition = Vector2(-10, 0);
await ball2.add(BallGravitatingBehavior());
await game.ensureAddAll([ball1, ball2]);

@ -1,21 +1,19 @@
// ignore_for_file: cascade_invocations
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final asset = Assets.images.ball.ball.keyName;
final asset = theme.Assets.images.dash.ball.keyName;
final flameTester = FlameTester(() => TestGame([asset]));
group('BallScalingBehavior', () {
const baseColor = Color(0xFFFFFFFF);
test('can be instantiated', () {
expect(
BallScalingBehavior(),
@ -24,7 +22,7 @@ void main() {
});
flameTester.test('can be loaded', (game) async {
final ball = Ball.test(baseColor: baseColor);
final ball = Ball.test();
final behavior = BallScalingBehavior();
await ball.add(behavior);
await game.ensureAdd(ball);
@ -35,12 +33,10 @@ void main() {
});
flameTester.test('scales the shape radius', (game) async {
final ball1 = Ball.test(baseColor: baseColor)
..initialPosition = Vector2(0, 10);
final ball1 = Ball.test()..initialPosition = Vector2(0, 10);
await ball1.add(BallScalingBehavior());
final ball2 = Ball.test(baseColor: baseColor)
..initialPosition = Vector2(0, -10);
final ball2 = Ball.test()..initialPosition = Vector2(0, -10);
await ball2.add(BallScalingBehavior());
await game.ensureAddAll([ball1, ball2]);
@ -57,12 +53,10 @@ void main() {
flameTester.test(
'scales the sprite',
(game) async {
final ball1 = Ball.test(baseColor: baseColor)
..initialPosition = Vector2(0, 10);
final ball1 = Ball.test()..initialPosition = Vector2(0, 10);
await ball1.add(BallScalingBehavior());
final ball2 = Ball.test(baseColor: baseColor)
..initialPosition = Vector2(0, -10);
final ball2 = Ball.test()..initialPosition = Vector2(0, -10);
await ball2.add(BallScalingBehavior());
await game.ensureAddAll([ball1, ball2]);

@ -1,12 +1,10 @@
// ignore_for_file: cascade_invocations
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../../helpers/helpers.dart';
@ -16,9 +14,8 @@ void main() {
group(
'BallTurboChargingBehavior',
() {
final assets = [Assets.images.ball.ball.keyName];
final flameTester = FlameTester(() => TestGame(assets));
const baseColor = Color(0xFFFFFFFF);
final asset = theme.Assets.images.dash.ball.keyName;
final flameTester = FlameTester(() => TestGame([asset]));
test('can be instantiated', () {
expect(
@ -28,7 +25,7 @@ void main() {
});
flameTester.test('can be loaded', (game) async {
final ball = Ball.test(baseColor: baseColor);
final ball = Ball.test();
final behavior = BallTurboChargingBehavior(impulse: Vector2.zero());
await ball.add(behavior);
await game.ensureAdd(ball);
@ -41,7 +38,7 @@ void main() {
flameTester.test(
'impulses the ball velocity when loaded',
(game) async {
final ball = Ball.test(baseColor: baseColor);
final ball = Ball.test();
await game.ensureAdd(ball);
final impulse = Vector2.all(1);
final behavior = BallTurboChargingBehavior(impulse: impulse);
@ -59,7 +56,7 @@ void main() {
);
flameTester.test('adds sprite', (game) async {
final ball = Ball(baseColor: baseColor);
final ball = Ball();
await game.ensureAdd(ball);
await ball.ensureAdd(
@ -73,7 +70,7 @@ void main() {
});
flameTester.test('removes sprite after it finishes', (game) async {
final ball = Ball(baseColor: baseColor);
final ball = Ball();
await game.ensureAdd(ball);
final behavior = BallTurboChargingBehavior(impulse: Vector2.zero());

@ -4,11 +4,11 @@ import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/chrome_dino/behaviors/behaviors.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../../helpers/helpers.dart';
@ -20,7 +20,10 @@ class _MockFixture extends Mock implements Fixture {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
final assets = [
theme.Assets.images.dash.ball.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group(
'ChromeDinoChompingBehavior',
@ -35,7 +38,7 @@ void main() {
flameTester.test(
'beginContact sets ball sprite to be invisible and calls onChomp',
(game) async {
final ball = Ball(baseColor: Colors.red);
final ball = Ball();
final behavior = ChromeDinoChompingBehavior();
final bloc = _MockChromeDinoCubit();
whenListen(

@ -5,11 +5,11 @@ import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/chrome_dino/behaviors/behaviors.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../../helpers/helpers.dart';
@ -17,7 +17,10 @@ class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
final assets = [
theme.Assets.images.dash.ball.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group(
'ChromeDinoSpittingBehavior',
@ -33,7 +36,7 @@ void main() {
flameTester.test(
'sets ball sprite to visible and sets a linear velocity',
(game) async {
final ball = Ball(baseColor: Colors.red);
final ball = Ball();
final behavior = ChromeDinoSpittingBehavior();
final bloc = _MockChromeDinoCubit();
final streamController = StreamController<ChromeDinoState>();
@ -71,7 +74,7 @@ void main() {
flameTester.test(
'calls onSpit',
(game) async {
final ball = Ball(baseColor: Colors.red);
final ball = Ball();
final behavior = ChromeDinoSpittingBehavior();
final bloc = _MockChromeDinoCubit();
final streamController = StreamController<ChromeDinoState>();

@ -36,7 +36,7 @@ void main() {
whenListen(
bloc,
const Stream<ChromeDinoState>.empty(),
initialState: const ChromeDinoState.inital(),
initialState: const ChromeDinoState.initial(),
);
final chromeDino = ChromeDino.test(bloc: bloc);
@ -58,7 +58,7 @@ void main() {
whenListen(
bloc,
const Stream<ChromeDinoState>.empty(),
initialState: const ChromeDinoState.inital(),
initialState: const ChromeDinoState.initial(),
);
final chromeDino = ChromeDino.test(bloc: bloc);
@ -91,7 +91,7 @@ void main() {
bloc,
const Stream<ChromeDinoState>.empty(),
initialState:
const ChromeDinoState.inital().copyWith(isMouthOpen: true),
const ChromeDinoState.initial().copyWith(isMouthOpen: true),
);
final chromeDino = ChromeDino.test(bloc: bloc);
@ -120,7 +120,7 @@ void main() {
bloc,
const Stream<ChromeDinoState>.empty(),
initialState:
const ChromeDinoState.inital().copyWith(isMouthOpen: false),
const ChromeDinoState.initial().copyWith(isMouthOpen: false),
);
final chromeDino = ChromeDino.test(bloc: bloc);
@ -148,7 +148,7 @@ void main() {
bloc,
const Stream<ChromeDinoState>.empty(),
initialState:
const ChromeDinoState.inital().copyWith(isMouthOpen: false),
const ChromeDinoState.initial().copyWith(isMouthOpen: false),
);
final chromeDino = ChromeDino.test(bloc: bloc);

@ -79,7 +79,7 @@ void main() {
whenListen(
bloc,
const Stream<ChromeDinoState>.empty(),
initialState: const ChromeDinoState.inital(),
initialState: const ChromeDinoState.initial(),
);
when(bloc.close).thenAnswer((_) async {});
final chromeDino = ChromeDino.test(bloc: bloc);

@ -1,5 +1,4 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
@ -7,7 +6,7 @@ void main() {
group(
'ChromeDinoCubit',
() {
final ball = Ball(baseColor: Colors.red);
final ball = Ball();
blocTest<ChromeDinoCubit, ChromeDinoState>(
'onOpenMouth emits true',
@ -58,7 +57,7 @@ void main() {
blocTest<ChromeDinoCubit, ChromeDinoState>(
'onChomp emits nothing when the ball is already in the mouth',
build: ChromeDinoCubit.new,
seed: () => const ChromeDinoState.inital().copyWith(ball: ball),
seed: () => const ChromeDinoState.initial().copyWith(ball: ball),
act: (bloc) => bloc.onChomp(ball),
expect: () => <ChromeDinoState>[],
);

@ -1,6 +1,5 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
@ -37,7 +36,7 @@ void main() {
status: ChromeDinoStatus.idle,
isMouthOpen: false,
);
expect(ChromeDinoState.inital(), equals(initialState));
expect(ChromeDinoState.initial(), equals(initialState));
});
});
@ -61,7 +60,7 @@ void main() {
'copies correctly '
'when all arguments specified',
() {
final ball = Ball(baseColor: Colors.red);
final ball = Ball();
const chromeDinoState = ChromeDinoState(
status: ChromeDinoStatus.chomping,
isMouthOpen: true,

@ -2,9 +2,9 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../helpers/helpers.dart';
@ -13,6 +13,7 @@ void main() {
final assets = [
Assets.images.flipper.left.keyName,
Assets.images.flipper.right.keyName,
theme.Assets.images.dash.ball.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
@ -89,7 +90,7 @@ void main() {
'has greater mass than Ball',
(game) async {
final flipper = Flipper(side: BoardSide.left);
final ball = Ball(baseColor: Colors.white);
final ball = Ball();
await game.ready();
await game.ensureAddAll([flipper, ball]);

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

@ -0,0 +1,136 @@
// 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_components/pinball_components.dart';
import 'package:pinball_components/src/components/layer_sensor/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _TestLayerSensor extends LayerSensor {
_TestLayerSensor({
required LayerEntranceOrientation orientation,
required int insideZIndex,
required Layer insideLayer,
}) : super(
insideLayer: insideLayer,
insideZIndex: insideZIndex,
orientation: orientation,
);
@override
Shape get shape => PolygonShape()..setAsBoxXY(1, 1);
}
class _MockBall extends Mock implements Ball {}
class _MockBody extends Mock implements Body {}
class _MockContact extends Mock implements Contact {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'LayerSensorBehavior',
() {
test('can be instantiated', () {
expect(
LayerFilteringBehavior(),
isA<LayerFilteringBehavior>(),
);
});
flameTester.test(
'loads',
(game) async {
final behavior = LayerFilteringBehavior();
final parent = _TestLayerSensor(
orientation: LayerEntranceOrientation.down,
insideZIndex: 1,
insideLayer: Layer.spaceshipEntranceRamp,
);
await parent.add(behavior);
await game.ensureAdd(parent);
expect(game.contains(parent), isTrue);
},
);
group('beginContact', () {
late Ball ball;
late Body body;
late int insideZIndex;
late Layer insideLayer;
setUp(() {
ball = _MockBall();
body = _MockBody();
insideZIndex = 1;
insideLayer = Layer.spaceshipEntranceRamp;
when(() => ball.body).thenReturn(body);
when(() => ball.layer).thenReturn(Layer.board);
});
flameTester.test(
'changes ball layer and zIndex '
'when a ball enters and exits a downward oriented LayerSensor',
(game) async {
final parent = _TestLayerSensor(
orientation: LayerEntranceOrientation.down,
insideZIndex: 1,
insideLayer: insideLayer,
)..initialPosition = Vector2(0, 10);
final behavior = LayerFilteringBehavior();
await parent.add(behavior);
await game.ensureAdd(parent);
when(() => body.linearVelocity).thenReturn(Vector2(0, -1));
behavior.beginContact(ball, _MockContact());
verify(() => ball.layer = insideLayer).called(1);
verify(() => ball.zIndex = insideZIndex).called(1);
when(() => ball.layer).thenReturn(insideLayer);
behavior.beginContact(ball, _MockContact());
verify(() => ball.layer = Layer.board);
verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1);
});
flameTester.test(
'changes ball layer and zIndex '
'when a ball enters and exits an upward oriented LayerSensor',
(game) async {
final parent = _TestLayerSensor(
orientation: LayerEntranceOrientation.up,
insideZIndex: 1,
insideLayer: insideLayer,
)..initialPosition = Vector2(0, 10);
final behavior = LayerFilteringBehavior();
await parent.add(behavior);
await game.ensureAdd(parent);
when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
behavior.beginContact(ball, _MockContact());
verify(() => ball.layer = insideLayer).called(1);
verify(() => ball.zIndex = 1).called(1);
when(() => ball.layer).thenReturn(insideLayer);
behavior.beginContact(ball, _MockContact());
verify(() => ball.layer = Layer.board);
verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1);
});
});
},
);
}

@ -2,16 +2,10 @@
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_components/pinball_components.dart';
import 'package:pinball_components/src/components/layer_sensor/behaviors/behaviors.dart';
import '../../helpers/helpers.dart';
class _MockBall extends Mock implements Ball {}
class _MockBody extends Mock implements Body {}
class _MockContact extends Mock implements Contact {}
import '../../../helpers/helpers.dart';
class TestLayerSensor extends LayerSensor {
TestLayerSensor({
@ -112,68 +106,22 @@ void main() {
);
});
});
});
group('beginContact', () {
late Ball ball;
late Body body;
late int insideZIndex;
late Layer insideLayer;
setUp(() {
ball = _MockBall();
body = _MockBody();
insideZIndex = 1;
insideLayer = Layer.spaceshipEntranceRamp;
when(() => ball.body).thenReturn(body);
when(() => ball.layer).thenReturn(Layer.board);
});
flameTester.test(
'changes ball layer and zIndex '
'when a ball enters and exits a downward oriented LayerSensor',
'adds a LayerFilteringBehavior',
(game) async {
final sensor = TestLayerSensor(
final layerSensor = TestLayerSensor(
orientation: LayerEntranceOrientation.down,
insideZIndex: insidePriority,
insideLayer: insideLayer,
)..initialPosition = Vector2(0, 10);
when(() => body.linearVelocity).thenReturn(Vector2(0, -1));
sensor.beginContact(ball, _MockContact());
verify(() => ball.layer = insideLayer).called(1);
verify(() => ball.zIndex = insideZIndex).called(1);
when(() => ball.layer).thenReturn(insideLayer);
sensor.beginContact(ball, _MockContact());
verify(() => ball.layer = Layer.board);
verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1);
});
flameTester.test(
'changes ball layer and zIndex '
'when a ball enters and exits an upward oriented LayerSensor',
(game) async {
final sensor = TestLayerSensor(
orientation: LayerEntranceOrientation.up,
insideZIndex: insidePriority,
insideLayer: insideLayer,
)..initialPosition = Vector2(0, 10);
when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
sensor.beginContact(ball, _MockContact());
verify(() => ball.layer = insideLayer).called(1);
verify(() => ball.zIndex = insidePriority).called(1);
when(() => ball.layer).thenReturn(insideLayer);
insideLayer: Layer.spaceshipEntranceRamp,
);
await game.ensureAdd(layerSensor);
sensor.beginContact(ball, _MockContact());
verify(() => ball.layer = Layer.board);
verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1);
});
expect(
layerSensor.children.whereType<LayerFilteringBehavior>().length,
equals(1),
);
},
);
});
}

@ -0,0 +1,62 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
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_components/pinball_components.dart';
import 'package:pinball_components/src/components/skill_shot/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _MockBall extends Mock implements Ball {}
class _MockContact extends Mock implements Contact {}
class _MockSkillShotCubit extends Mock implements SkillShotCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'SkillShotBallContactBehavior',
() {
test('can be instantiated', () {
expect(
SkillShotBallContactBehavior(),
isA<SkillShotBallContactBehavior>(),
);
});
flameTester.testGameWidget(
'beginContact animates pin and calls onBallContacted '
'when contacts with a ball',
setUp: (game, tester) async {
await game.images.load(Assets.images.skillShot.pin.keyName);
final behavior = SkillShotBallContactBehavior();
final bloc = _MockSkillShotCubit();
whenListen(
bloc,
const Stream<SkillShotState>.empty(),
initialState: const SkillShotState.initial(),
);
final skillShot = SkillShot.test(bloc: bloc);
await skillShot.addAll([behavior, PinSpriteAnimationComponent()]);
await game.ensureAdd(skillShot);
behavior.beginContact(_MockBall(), _MockContact());
await tester.pump();
expect(
skillShot.firstChild<PinSpriteAnimationComponent>()!.playing,
isTrue,
);
verify(skillShot.bloc.onBallContacted).called(1);
},
);
},
);
}

@ -0,0 +1,125 @@
// ignore_for_file: cascade_invocations
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/skill_shot/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _MockSkillShotCubit extends Mock implements SkillShotCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'SkillShotBlinkingBehavior',
() {
flameTester.testGameWidget(
'calls switched after 0.15 seconds when isBlinking and lit',
setUp: (game, tester) async {
final behavior = SkillShotBlinkingBehavior();
final bloc = _MockSkillShotCubit();
final streamController = StreamController<SkillShotState>();
whenListen(
bloc,
streamController.stream,
initialState: const SkillShotState.initial(),
);
final skillShot = SkillShot.test(bloc: bloc);
await skillShot.add(behavior);
await game.ensureAdd(skillShot);
streamController.add(
const SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
),
);
await tester.pump();
game.update(0.15);
await streamController.close();
verify(bloc.switched).called(1);
},
);
flameTester.testGameWidget(
'calls switched after 0.15 seconds when isBlinking and dimmed',
setUp: (game, tester) async {
final behavior = SkillShotBlinkingBehavior();
final bloc = _MockSkillShotCubit();
final streamController = StreamController<SkillShotState>();
whenListen(
bloc,
streamController.stream,
initialState: const SkillShotState.initial(),
);
final skillShot = SkillShot.test(bloc: bloc);
await skillShot.add(behavior);
await game.ensureAdd(skillShot);
streamController.add(
const SkillShotState(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: true,
),
);
await tester.pump();
game.update(0.15);
await streamController.close();
verify(bloc.switched).called(1);
},
);
flameTester.testGameWidget(
'calls onBlinkingFinished after all blinks complete',
setUp: (game, tester) async {
final behavior = SkillShotBlinkingBehavior();
final bloc = _MockSkillShotCubit();
final streamController = StreamController<SkillShotState>();
whenListen(
bloc,
streamController.stream,
initialState: const SkillShotState.initial(),
);
final skillShot = SkillShot.test(bloc: bloc);
await skillShot.add(behavior);
await game.ensureAdd(skillShot);
for (var i = 0; i <= 8; i++) {
if (i.isEven) {
streamController.add(
const SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
),
);
} else {
streamController.add(
const SkillShotState(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: true,
),
);
}
await tester.pump();
game.update(0.15);
}
await streamController.close();
verify(bloc.onBlinkingFinished).called(1);
},
);
},
);
}

@ -0,0 +1,66 @@
// ignore_for_file: prefer_const_constructors
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'SkillShotCubit',
() {
blocTest<SkillShotCubit, SkillShotState>(
'onBallContacted emits lit and true',
build: SkillShotCubit.new,
act: (bloc) => bloc.onBallContacted(),
expect: () => [
SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
),
],
);
blocTest<SkillShotCubit, SkillShotState>(
'switched emits lit when dimmed',
build: SkillShotCubit.new,
act: (bloc) => bloc.switched(),
expect: () => [
isA<SkillShotState>().having(
(state) => state.spriteState,
'spriteState',
SkillShotSpriteState.lit,
)
],
);
blocTest<SkillShotCubit, SkillShotState>(
'switched emits dimmed when lit',
build: SkillShotCubit.new,
seed: () => SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: false,
),
act: (bloc) => bloc.switched(),
expect: () => [
isA<SkillShotState>().having(
(state) => state.spriteState,
'spriteState',
SkillShotSpriteState.dimmed,
)
],
);
blocTest<SkillShotCubit, SkillShotState>(
'onBlinkingFinished emits dimmed and false',
build: SkillShotCubit.new,
act: (bloc) => bloc.onBlinkingFinished(),
expect: () => [
SkillShotState(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: false,
),
],
);
},
);
}

@ -0,0 +1,84 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group('SkillShotState', () {
test('supports value equality', () {
expect(
SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
),
equals(
const SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
),
),
);
});
group('constructor', () {
test('can be instantiated', () {
expect(
const SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
),
isNotNull,
);
});
test('initial is idle with mouth closed', () {
const initialState = SkillShotState(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: false,
);
expect(SkillShotState.initial(), equals(initialState));
});
});
group('copyWith', () {
test(
'copies correctly '
'when no argument specified',
() {
const chromeDinoState = SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
);
expect(
chromeDinoState.copyWith(),
equals(chromeDinoState),
);
},
);
test(
'copies correctly '
'when all arguments specified',
() {
const chromeDinoState = SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
);
final otherSkillShotState = SkillShotState(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: false,
);
expect(chromeDinoState, isNot(equals(otherSkillShotState)));
expect(
chromeDinoState.copyWith(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: false,
),
equals(otherSkillShotState),
);
},
);
});
});
}

@ -0,0 +1,99 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/skill_shot/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart';
class _MockSkillShotCubit extends Mock implements SkillShotCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.skillShot.decal.keyName,
Assets.images.skillShot.pin.keyName,
Assets.images.skillShot.lit.keyName,
Assets.images.skillShot.dimmed.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('SkillShot', () {
flameTester.test('loads correctly', (game) async {
final skillShot = SkillShot();
await game.ensureAdd(skillShot);
expect(game.contains(skillShot), isTrue);
});
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockSkillShotCubit();
whenListen(
bloc,
const Stream<SkillShotState>.empty(),
initialState: const SkillShotState.initial(),
);
when(bloc.close).thenAnswer((_) async {});
final skillShot = SkillShot.test(bloc: bloc);
await game.ensureAdd(skillShot);
game.remove(skillShot);
await game.ready();
verify(bloc.close).called(1);
});
group('adds', () {
flameTester.test('new children', (game) async {
final component = Component();
final skillShot = SkillShot(
children: [component],
);
await game.ensureAdd(skillShot);
expect(skillShot.children, contains(component));
});
flameTester.test('a SkillShotBallContactBehavior', (game) async {
final skillShot = SkillShot();
await game.ensureAdd(skillShot);
expect(
skillShot.children.whereType<SkillShotBallContactBehavior>().single,
isNotNull,
);
});
flameTester.test('a SkillShotBlinkingBehavior', (game) async {
final skillShot = SkillShot();
await game.ensureAdd(skillShot);
expect(
skillShot.children.whereType<SkillShotBlinkingBehavior>().single,
isNotNull,
);
});
});
flameTester.test(
'pin stops animating after animation completes',
(game) async {
final skillShot = SkillShot();
await game.ensureAdd(skillShot);
final pinSpriteAnimationComponent =
skillShot.firstChild<PinSpriteAnimationComponent>()!;
pinSpriteAnimationComponent.playing = true;
game.update(
pinSpriteAnimationComponent.animation!.totalDuration() + 0.1,
);
expect(pinSpriteAnimationComponent.playing, isFalse);
},
);
});
}

@ -0,0 +1,117 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
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_components/pinball_components.dart';
import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart';
import '../../../../helpers/helpers.dart';
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
class _MockBall extends Mock implements Ball {}
class _MockBody extends Mock implements Body {}
class _MockContact extends Mock implements Contact {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group(
'RampBallAscendingContactBehavior',
() {
test('can be instantiated', () {
expect(
RampBallAscendingContactBehavior(),
isA<RampBallAscendingContactBehavior>(),
);
});
group('beginContact', () {
late Ball ball;
late Body body;
setUp(() {
ball = _MockBall();
body = _MockBody();
when(() => ball.body).thenReturn(body);
});
flameTester.test(
"calls 'onAscendingBallEntered' when a ball enters into the ramp",
(game) async {
final behavior = RampBallAscendingContactBehavior();
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: const SpaceshipRampState.initial(),
);
final rampSensor = RampScoringSensor.test();
final spaceshipRamp = SpaceshipRamp.test(
bloc: bloc,
);
when(() => body.linearVelocity).thenReturn(Vector2(0, -1));
await spaceshipRamp.add(rampSensor);
await game.ensureAddAll([spaceshipRamp, ball]);
await rampSensor.add(behavior);
behavior.beginContact(ball, _MockContact());
verify(bloc.onAscendingBallEntered).called(1);
},
);
flameTester.test(
"doesn't call 'onAscendingBallEntered' when a ball goes out the ramp",
(game) async {
final behavior = RampBallAscendingContactBehavior();
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: const SpaceshipRampState.initial(),
);
final rampSensor = RampScoringSensor.test();
final spaceshipRamp = SpaceshipRamp.test(
bloc: bloc,
);
when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
await spaceshipRamp.add(rampSensor);
await game.ensureAddAll([spaceshipRamp, ball]);
await rampSensor.add(behavior);
behavior.beginContact(ball, _MockContact());
verifyNever(bloc.onAscendingBallEntered);
},
);
});
},
);
}

@ -0,0 +1,25 @@
// ignore_for_file: prefer_const_constructors
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group('SpaceshipRampCubit', () {
group('onAscendingBallEntered', () {
blocTest<SpaceshipRampCubit, SpaceshipRampState>(
'emits hits incremented and arrow goes to the next value',
build: SpaceshipRampCubit.new,
act: (bloc) => bloc
..onAscendingBallEntered()
..onAscendingBallEntered()
..onAscendingBallEntered(),
expect: () => [
SpaceshipRampState(hits: 1),
SpaceshipRampState(hits: 2),
SpaceshipRampState(hits: 3),
],
);
});
});
}

@ -0,0 +1,78 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/src/components/components.dart';
void main() {
group('SpaceshipRampState', () {
test('supports value equality', () {
expect(
SpaceshipRampState(hits: 0),
equals(
SpaceshipRampState(hits: 0),
),
);
});
group('constructor', () {
test('can be instantiated', () {
expect(
SpaceshipRampState(hits: 0),
isNotNull,
);
});
});
test(
'throws AssertionError '
'when hits is negative',
() {
expect(
() => SpaceshipRampState(hits: -1),
throwsAssertionError,
);
},
);
group('copyWith', () {
test(
'throws AssertionError '
'when hits is decreased',
() {
const rampState = SpaceshipRampState(hits: 0);
expect(
() => rampState.copyWith(hits: rampState.hits - 1),
throwsAssertionError,
);
},
);
test(
'copies correctly '
'when no argument specified',
() {
const rampState = SpaceshipRampState(hits: 0);
expect(
rampState.copyWith(),
equals(rampState),
);
},
);
test(
'copies correctly '
'when all arguments specified',
() {
const rampState = SpaceshipRampState(hits: 0);
final otherRampState = SpaceshipRampState(hits: rampState.hits + 1);
expect(rampState, isNot(equals(otherRampState)));
expect(
rampState.copyWith(hits: rampState.hits + 1),
equals(otherRampState),
);
},
);
});
});
}

@ -1,12 +1,16 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import '../../helpers/helpers.dart';
import '../../../helpers/helpers.dart';
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@ -25,28 +29,35 @@ void main() {
final flameTester = FlameTester(() => TestGame(assets));
group('SpaceshipRamp', () {
flameTester.test('loads correctly', (game) async {
final component = SpaceshipRamp();
await game.ensureAdd(component);
expect(game.contains(component), isTrue);
});
flameTester.test(
'loads correctly',
(game) async {
final spaceshipRamp = SpaceshipRamp();
await game.ensureAdd(spaceshipRamp);
expect(game.children, contains(spaceshipRamp));
},
);
group('renders correctly', () {
const goldenFilePath = 'golden/spaceship_ramp/';
const goldenFilePath = '../golden/spaceship_ramp/';
final centerForSpaceshipRamp = Vector2(-13, -55);
flameTester.testGameWidget(
'inactive sprite',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final component = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]);
final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas);
await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect(
component.children.whereType<SpriteGroupComponent>().first.current,
SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.inactive,
);
@ -64,15 +75,21 @@ void main() {
'active1 sprite',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final component = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]);
final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas);
component.progress();
ramp.bloc.onAscendingBallEntered();
await game.ready();
await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect(
component.children.whereType<SpriteGroupComponent>().first.current,
SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active1,
);
@ -90,17 +107,23 @@ void main() {
'active2 sprite',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final component = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]);
final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas);
component
..progress()
..progress();
ramp.bloc
..onAscendingBallEntered()
..onAscendingBallEntered();
await game.ready();
await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect(
component.children.whereType<SpriteGroupComponent>().first.current,
SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active2,
);
@ -118,18 +141,24 @@ void main() {
'active3 sprite',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final component = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]);
final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas);
component
..progress()
..progress()
..progress();
ramp.bloc
..onAscendingBallEntered()
..onAscendingBallEntered()
..onAscendingBallEntered();
await game.ready();
await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect(
component.children.whereType<SpriteGroupComponent>().first.current,
SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active3,
);
@ -147,19 +176,25 @@ void main() {
'active4 sprite',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final component = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]);
final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas);
component
..progress()
..progress()
..progress()
..progress();
ramp.bloc
..onAscendingBallEntered()
..onAscendingBallEntered()
..onAscendingBallEntered()
..onAscendingBallEntered();
await game.ready();
await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect(
component.children.whereType<SpriteGroupComponent>().first.current,
SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active4,
);
@ -177,20 +212,26 @@ void main() {
'active5 sprite',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final component = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [component]);
final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas);
component
..progress()
..progress()
..progress()
..progress()
..progress();
ramp.bloc
..onAscendingBallEntered()
..onAscendingBallEntered()
..onAscendingBallEntered()
..onAscendingBallEntered()
..onAscendingBallEntered();
await game.ready();
await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect(
component.children.whereType<SpriteGroupComponent>().first.current,
SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active5,
);
@ -204,5 +245,34 @@ void main() {
},
);
});
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: const SpaceshipRampState.initial(),
);
when(bloc.close).thenAnswer((_) async {});
final ramp = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ramp);
game.remove(ramp);
await game.ready();
verify(bloc.close).called(1);
});
group('adds', () {
flameTester.test('new children', (game) async {
final component = Component();
final ramp = SpaceshipRamp(children: [component]);
await game.ensureAdd(ramp);
expect(ramp.children, contains(component));
});
});
});
}

@ -1,9 +1,9 @@
library pinball_flame;
export 'src/canvas/canvas.dart';
export 'src/component_controller.dart';
export 'src/contact_behavior.dart';
export 'src/keyboard_input_controller.dart';
export 'src/parent_is_a.dart';
export 'src/pinball_forge2d_game.dart';
export 'src/sprite_animation.dart';
export 'src/z_canvas_component.dart';

@ -0,0 +1,2 @@
export 'canvas_component.dart';
export 'z_canvas_component.dart';

@ -0,0 +1,47 @@
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:pinball_flame/src/canvas/canvas_wrapper.dart';
/// Called right before [Canvas.drawImageRect] is called.
///
/// This is useful since [Sprite.render] uses [Canvas.drawImageRect] to draw
/// the [Sprite].
typedef PaintFunction = void Function(Paint);
/// {@template canvas_component}
/// Allows listening before the rendering of [Sprite]s.
///
/// The existance of this class is to hack around the fact that Flame doesn't
/// provide a global way to modify the default [Paint] before rendering a
/// [Sprite].
/// {@endtemplate}
class CanvasComponent extends Component {
/// {@macro canvas_component}
CanvasComponent({
PaintFunction? onSpritePainted,
Iterable<Component>? children,
}) : _canvas = _Canvas(onSpritePainted: onSpritePainted),
super(children: children);
final _Canvas _canvas;
@override
void renderTree(Canvas canvas) {
_canvas.canvas = canvas;
super.renderTree(_canvas);
}
}
class _Canvas extends CanvasWrapper {
_Canvas({PaintFunction? onSpritePainted})
: _onSpritePainted = onSpritePainted;
final PaintFunction? _onSpritePainted;
@override
void drawImageRect(Image image, Rect src, Rect dst, Paint paint) {
_onSpritePainted?.call(paint);
super.drawImageRect(image, src, dst, paint);
}
}

@ -1,85 +1,11 @@
// ignore_for_file: public_member_api_docs
import 'dart:typed_data';
import 'dart:ui';
import 'package:flame/components.dart';
/// {@template z_canvas_component}
/// Draws [ZIndex] components after the all non-[ZIndex] components have been
/// drawn.
/// {@endtemplate}
class ZCanvasComponent extends Component {
/// {@macro z_canvas_component}
ZCanvasComponent({
Iterable<Component>? children,
}) : _zCanvas = ZCanvas(),
super(children: children);
final ZCanvas _zCanvas;
@override
void renderTree(Canvas canvas) {
_zCanvas.canvas = canvas;
super.renderTree(_zCanvas);
_zCanvas.render();
}
}
/// Apply to any [Component] that will be rendered according to a
/// [ZIndex.zIndex].
///
/// [ZIndex] components must be descendants of a [ZCanvasComponent].
///
/// {@macro z_canvas.render}
mixin ZIndex on Component {
/// The z-index of this component.
///
/// The higher the value, the later the component will be drawn. Hence,
/// rendering in front of [Component]s with lower [zIndex] values.
int zIndex = 0;
@override
void renderTree(
Canvas canvas,
) {
if (canvas is ZCanvas) {
canvas.buffer(this);
} else {
super.renderTree(canvas);
}
}
}
/// The [ZCanvas] allows to postpone the rendering of [ZIndex] components.
///
/// You should not use this class directly.
class ZCanvas implements Canvas {
/// The [Canvas] to render to.
///
/// This is set by [ZCanvasComponent] when rendering.
class CanvasWrapper implements Canvas {
late Canvas canvas;
final List<ZIndex> _zBuffer = [];
/// Postpones the rendering of [ZIndex] component and its children.
void buffer(ZIndex component) => _zBuffer.add(component);
/// Renders all [ZIndex] components and their children.
///
/// {@template z_canvas.render}
/// The rendering order is defined by the parent [ZIndex]. The children of
/// the same parent are rendered in the order they were added.
///
/// If two [Component]s ever overlap each other, and have the same
/// [ZIndex.zIndex], there is no guarantee that the first one will be rendered
/// before the second one.
/// {@endtemplate}
void render() => _zBuffer
..sort((a, b) => a.zIndex.compareTo(b.zIndex))
..whereType<Component>().forEach(_render)
..clear();
void _render(Component component) => component.renderTree(canvas);
@override
void clipPath(Path path, {bool doAntiAlias = true}) =>
canvas.clipPath(path, doAntiAlias: doAntiAlias);

@ -0,0 +1,77 @@
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:pinball_flame/src/canvas/canvas_wrapper.dart';
/// {@template z_canvas_component}
/// Draws [ZIndex] components after the all non-[ZIndex] components have been
/// drawn.
/// {@endtemplate}
class ZCanvasComponent extends Component {
/// {@macro z_canvas_component}
ZCanvasComponent({
Iterable<Component>? children,
}) : _zCanvas = _ZCanvas(),
super(children: children);
final _ZCanvas _zCanvas;
@override
void renderTree(Canvas canvas) {
_zCanvas.canvas = canvas;
super.renderTree(_zCanvas);
_zCanvas.render();
}
}
/// Apply to any [Component] that will be rendered according to a
/// [ZIndex.zIndex].
///
/// [ZIndex] components must be descendants of a [ZCanvasComponent].
///
/// {@macro z_canvas.render}
mixin ZIndex on Component {
/// The z-index of this component.
///
/// The higher the value, the later the component will be drawn. Hence,
/// rendering in front of [Component]s with lower [zIndex] values.
int zIndex = 0;
@override
void renderTree(
Canvas canvas,
) {
if (canvas is _ZCanvas) {
canvas.buffer(this);
} else {
super.renderTree(canvas);
}
}
}
/// The [_ZCanvas] allows to postpone the rendering of [ZIndex] components.
///
/// You should not use this class directly.
class _ZCanvas extends CanvasWrapper {
final List<ZIndex> _zBuffer = [];
/// Postpones the rendering of [ZIndex] component and its children.
void buffer(ZIndex component) => _zBuffer.add(component);
/// Renders all [ZIndex] components and their children.
///
/// {@template z_canvas.render}
/// The rendering order is defined by the parent [ZIndex]. The children of
/// the same parent are rendered in the order they were added.
///
/// If two [Component]s ever overlap each other, and have the same
/// [ZIndex.zIndex], there is no guarantee that the first one will be rendered
/// before the second one.
/// {@endtemplate}
void render() => _zBuffer
..sort((a, b) => a.zIndex.compareTo(b.zIndex))
..whereType<Component>().forEach(_render)
..clear();
void _render(Component component) => component.renderTree(canvas);
}

@ -0,0 +1,144 @@
// ignore_for_file: cascade_invocations
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_flame/src/canvas/canvas_component.dart';
class _TestSpriteComponent extends SpriteComponent {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('CanvasComponent', () {
final flameTester = FlameTester(FlameGame.new);
test('can be instantiated', () {
expect(
CanvasComponent(),
isA<CanvasComponent>(),
);
});
flameTester.test('loads correctly', (game) async {
final component = CanvasComponent();
await game.ensureAdd(component);
expect(game.contains(component), isTrue);
});
flameTester.test(
'adds children',
(game) async {
final component = Component();
final canvas = CanvasComponent(
onSpritePainted: (paint) => paint.filterQuality = FilterQuality.high,
children: [component],
);
await game.ensureAdd(canvas);
expect(
canvas.children.contains(component),
isTrue,
);
},
);
flameTester.testGameWidget(
'calls onSpritePainted when paiting a sprite',
setUp: (game, tester) async {
final spriteComponent = _TestSpriteComponent();
final completer = Completer<Image>();
decodeImageFromList(
Uint8List.fromList(_image),
completer.complete,
);
spriteComponent.sprite = Sprite(await completer.future);
var calls = 0;
final canvas = CanvasComponent(
onSpritePainted: (paint) => calls++,
children: [spriteComponent],
);
await game.ensureAdd(canvas);
await tester.pump();
expect(calls, equals(1));
},
);
});
}
const List<int> _image = <int>[
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
0x00,
0x00,
0x00,
0x0D,
0x49,
0x48,
0x44,
0x52,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x01,
0x08,
0x06,
0x00,
0x00,
0x00,
0x1F,
0x15,
0xC4,
0x89,
0x00,
0x00,
0x00,
0x0A,
0x49,
0x44,
0x41,
0x54,
0x78,
0x9C,
0x63,
0x00,
0x01,
0x00,
0x00,
0x05,
0x00,
0x01,
0x0D,
0x0A,
0x2D,
0xB4,
0x00,
0x00,
0x00,
0x00,
0x49,
0x45,
0x4E,
0x44,
0xAE,
];

@ -0,0 +1,353 @@
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/material.dart' hide Image;
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_flame/src/canvas/canvas_wrapper.dart';
class _MockCanvas extends Mock implements Canvas {}
class _MockImage extends Mock implements Image {}
class _MockPicture extends Mock implements Picture {}
class _MockParagraph extends Mock implements Paragraph {}
class _MockVertices extends Mock implements Vertices {}
void main() {
group('CanvasWrapper', () {
group('CanvasWrapper', () {
late Canvas canvas;
late Path path;
late RRect rRect;
late Rect rect;
late Paint paint;
late Image atlas;
late BlendMode blendMode;
late Color color;
late Offset offset;
late Float64List float64list;
late Float32List float32list;
late Int32List int32list;
late Picture picture;
late Paragraph paragraph;
late Vertices vertices;
setUp(() {
canvas = _MockCanvas();
path = Path();
rRect = RRect.zero;
rect = Rect.zero;
paint = Paint();
atlas = _MockImage();
blendMode = BlendMode.clear;
color = Colors.black;
offset = Offset.zero;
float64list = Float64List(1);
float32list = Float32List(1);
int32list = Int32List(1);
picture = _MockPicture();
paragraph = _MockParagraph();
vertices = _MockVertices();
});
test("clipPath calls Canvas's clipPath", () {
CanvasWrapper()
..canvas = canvas
..clipPath(path, doAntiAlias: false);
verify(
() => canvas.clipPath(path, doAntiAlias: false),
).called(1);
});
test("clipRRect calls Canvas's clipRRect", () {
CanvasWrapper()
..canvas = canvas
..clipRRect(rRect, doAntiAlias: false);
verify(
() => canvas.clipRRect(rRect, doAntiAlias: false),
).called(1);
});
test("clipRect calls Canvas's clipRect", () {
CanvasWrapper()
..canvas = canvas
..clipRect(rect, doAntiAlias: false);
verify(
() => canvas.clipRect(rect, doAntiAlias: false),
).called(1);
});
test("drawArc calls Canvas's drawArc", () {
CanvasWrapper()
..canvas = canvas
..drawArc(rect, 0, 1, false, paint);
verify(
() => canvas.drawArc(rect, 0, 1, false, paint),
).called(1);
});
test("drawAtlas calls Canvas's drawAtlas", () {
CanvasWrapper()
..canvas = canvas
..drawAtlas(atlas, [], [], [], blendMode, rect, paint);
verify(
() => canvas.drawAtlas(atlas, [], [], [], blendMode, rect, paint),
).called(1);
});
test("drawCircle calls Canvas's drawCircle", () {
CanvasWrapper()
..canvas = canvas
..drawCircle(offset, 0, paint);
verify(
() => canvas.drawCircle(offset, 0, paint),
).called(1);
});
test("drawColor calls Canvas's drawColor", () {
CanvasWrapper()
..canvas = canvas
..drawColor(color, blendMode);
verify(
() => canvas.drawColor(color, blendMode),
).called(1);
});
test("drawDRRect calls Canvas's drawDRRect", () {
CanvasWrapper()
..canvas = canvas
..drawDRRect(rRect, rRect, paint);
verify(
() => canvas.drawDRRect(rRect, rRect, paint),
).called(1);
});
test("drawImage calls Canvas's drawImage", () {
CanvasWrapper()
..canvas = canvas
..drawImage(atlas, offset, paint);
verify(
() => canvas.drawImage(atlas, offset, paint),
).called(1);
});
test("drawImageNine calls Canvas's drawImageNine", () {
CanvasWrapper()
..canvas = canvas
..drawImageNine(atlas, rect, rect, paint);
verify(
() => canvas.drawImageNine(atlas, rect, rect, paint),
).called(1);
});
test("drawImageRect calls Canvas's drawImageRect", () {
CanvasWrapper()
..canvas = canvas
..drawImageRect(atlas, rect, rect, paint);
verify(
() => canvas.drawImageRect(atlas, rect, rect, paint),
).called(1);
});
test("drawLine calls Canvas's drawLine", () {
CanvasWrapper()
..canvas = canvas
..drawLine(offset, offset, paint);
verify(
() => canvas.drawLine(offset, offset, paint),
).called(1);
});
test("drawOval calls Canvas's drawOval", () {
CanvasWrapper()
..canvas = canvas
..drawOval(rect, paint);
verify(
() => canvas.drawOval(rect, paint),
).called(1);
});
test("drawPaint calls Canvas's drawPaint", () {
CanvasWrapper()
..canvas = canvas
..drawPaint(paint);
verify(
() => canvas.drawPaint(paint),
).called(1);
});
test("drawParagraph calls Canvas's drawParagraph", () {
CanvasWrapper()
..canvas = canvas
..drawParagraph(paragraph, offset);
verify(
() => canvas.drawParagraph(paragraph, offset),
).called(1);
});
test("drawPath calls Canvas's drawPath", () {
CanvasWrapper()
..canvas = canvas
..drawPath(path, paint);
verify(
() => canvas.drawPath(path, paint),
).called(1);
});
test("drawPicture calls Canvas's drawPicture", () {
CanvasWrapper()
..canvas = canvas
..drawPicture(picture);
verify(
() => canvas.drawPicture(picture),
).called(1);
});
test("drawPoints calls Canvas's drawPoints", () {
CanvasWrapper()
..canvas = canvas
..drawPoints(PointMode.points, [offset], paint);
verify(
() => canvas.drawPoints(PointMode.points, [offset], paint),
).called(1);
});
test("drawRRect calls Canvas's drawRRect", () {
CanvasWrapper()
..canvas = canvas
..drawRRect(rRect, paint);
verify(
() => canvas.drawRRect(rRect, paint),
).called(1);
});
test("drawRawAtlas calls Canvas's drawRawAtlas", () {
CanvasWrapper()
..canvas = canvas
..drawRawAtlas(
atlas,
float32list,
float32list,
int32list,
BlendMode.clear,
rect,
paint,
);
verify(
() => canvas.drawRawAtlas(
atlas,
float32list,
float32list,
int32list,
BlendMode.clear,
rect,
paint,
),
).called(1);
});
test("drawRawPoints calls Canvas's drawRawPoints", () {
CanvasWrapper()
..canvas = canvas
..drawRawPoints(PointMode.points, float32list, paint);
verify(
() => canvas.drawRawPoints(PointMode.points, float32list, paint),
).called(1);
});
test("drawRect calls Canvas's drawRect", () {
CanvasWrapper()
..canvas = canvas
..drawRect(rect, paint);
verify(
() => canvas.drawRect(rect, paint),
).called(1);
});
test("drawShadow calls Canvas's drawShadow", () {
CanvasWrapper()
..canvas = canvas
..drawShadow(path, color, 0, false);
verify(
() => canvas.drawShadow(path, color, 0, false),
).called(1);
});
test("drawVertices calls Canvas's drawVertices", () {
CanvasWrapper()
..canvas = canvas
..drawVertices(vertices, blendMode, paint);
verify(
() => canvas.drawVertices(vertices, blendMode, paint),
).called(1);
});
test("getSaveCount calls Canvas's getSaveCount", () {
final canvasWrapper = CanvasWrapper()..canvas = canvas;
when(() => canvas.getSaveCount()).thenReturn(1);
canvasWrapper.getSaveCount();
verify(() => canvas.getSaveCount()).called(1);
expect(canvasWrapper.getSaveCount(), 1);
});
test("restore calls Canvas's restore", () {
CanvasWrapper()
..canvas = canvas
..restore();
verify(() => canvas.restore()).called(1);
});
test("rotate calls Canvas's rotate", () {
CanvasWrapper()
..canvas = canvas
..rotate(0);
verify(() => canvas.rotate(0)).called(1);
});
test("save calls Canvas's save", () {
CanvasWrapper()
..canvas = canvas
..save();
verify(() => canvas.save()).called(1);
});
test("saveLayer calls Canvas's saveLayer", () {
CanvasWrapper()
..canvas = canvas
..saveLayer(rect, paint);
verify(() => canvas.saveLayer(rect, paint)).called(1);
});
test("scale calls Canvas's scale", () {
CanvasWrapper()
..canvas = canvas
..scale(0, 0);
verify(() => canvas.scale(0, 0)).called(1);
});
test("skew calls Canvas's skew", () {
CanvasWrapper()
..canvas = canvas
..skew(0, 0);
verify(() => canvas.skew(0, 0)).called(1);
});
test("transform calls Canvas's transform", () {
CanvasWrapper()
..canvas = canvas
..transform(float64list);
verify(() => canvas.transform(float64list)).called(1);
});
test("translate calls Canvas's translate", () {
CanvasWrapper()
..canvas = canvas
..translate(0, 0);
verify(() => canvas.translate(0, 0)).called(1);
});
});
});
}

@ -0,0 +1,80 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart' hide Image;
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_flame/pinball_flame.dart';
class _TestCircleComponent extends CircleComponent with ZIndex {
_TestCircleComponent(Color color)
: super(
paint: Paint()..color = color,
radius: 10,
);
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('ZCanvasComponent', () {
final flameTester = FlameTester(FlameGame.new);
const goldensFilePath = '../goldens/rendering/';
test('can be instantiated', () {
expect(
ZCanvasComponent(),
isA<ZCanvasComponent>(),
);
});
flameTester.test('loads correctly', (game) async {
final component = ZCanvasComponent();
await game.ensureAdd(component);
expect(game.contains(component), isTrue);
});
flameTester.testGameWidget(
'red circle renders behind blue circle',
setUp: (game, tester) async {
final canvas = ZCanvasComponent(
children: [
_TestCircleComponent(Colors.blue)..zIndex = 1,
_TestCircleComponent(Colors.red)..zIndex = 0,
],
);
await game.ensureAdd(canvas);
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
await expectLater(
find.byGame<FlameGame>(),
matchesGoldenFile('${goldensFilePath}red_blue.png'),
);
},
);
flameTester.testGameWidget(
'blue circle renders behind red circle',
setUp: (game, tester) async {
final canvas = ZCanvasComponent(
children: [
_TestCircleComponent(Colors.blue)..zIndex = 0,
_TestCircleComponent(Colors.red)..zIndex = 1
],
);
await game.ensureAdd(canvas);
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
await expectLater(
find.byGame<FlameGame>(),
matchesGoldenFile('${goldensFilePath}blue_red.png'),
);
},
);
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

@ -1,385 +0,0 @@
// ignore_for_file: cascade_invocations
import 'dart:typed_data';
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart' hide Image;
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_flame/pinball_flame.dart';
class _TestCircleComponent extends CircleComponent with ZIndex {
_TestCircleComponent(Color color)
: super(
paint: Paint()..color = color,
radius: 10,
);
}
class _MockCanvas extends Mock implements Canvas {}
class _MockImage extends Mock implements Image {}
class _MockPicture extends Mock implements Picture {}
class _MockParagraph extends Mock implements Paragraph {}
class _MockVertices extends Mock implements Vertices {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(FlameGame.new);
const goldenPrefix = 'golden/rendering/';
group('ZCanvasComponent', () {
flameTester.test('loads correctly', (game) async {
final component = ZCanvasComponent();
await game.ensureAdd(component);
expect(game.contains(component), isTrue);
});
flameTester.testGameWidget(
'red circle renders behind blue circle',
setUp: (game, tester) async {
final canvas = ZCanvasComponent(
children: [
_TestCircleComponent(Colors.blue)..zIndex = 1,
_TestCircleComponent(Colors.red)..zIndex = 0,
],
);
await game.ensureAdd(canvas);
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
await expectLater(
find.byGame<FlameGame>(),
matchesGoldenFile('${goldenPrefix}red_blue.png'),
);
},
);
flameTester.testGameWidget(
'blue circle renders behind red circle',
setUp: (game, tester) async {
final canvas = ZCanvasComponent(
children: [
_TestCircleComponent(Colors.blue)..zIndex = 0,
_TestCircleComponent(Colors.red)..zIndex = 1
],
);
await game.ensureAdd(canvas);
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
await expectLater(
find.byGame<FlameGame>(),
matchesGoldenFile('${goldenPrefix}blue_red.png'),
);
},
);
});
group('ZCanvas', () {
late Canvas canvas;
late Path path;
late RRect rRect;
late Rect rect;
late Paint paint;
late Image atlas;
late BlendMode blendMode;
late Color color;
late Offset offset;
late Float64List float64list;
late Float32List float32list;
late Int32List int32list;
late Picture picture;
late Paragraph paragraph;
late Vertices vertices;
setUp(() {
canvas = _MockCanvas();
path = Path();
rRect = RRect.zero;
rect = Rect.zero;
paint = Paint();
atlas = _MockImage();
blendMode = BlendMode.clear;
color = Colors.black;
offset = Offset.zero;
float64list = Float64List(1);
float32list = Float32List(1);
int32list = Int32List(1);
picture = _MockPicture();
paragraph = _MockParagraph();
vertices = _MockVertices();
});
test("clipPath calls Canvas's clipPath", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.clipPath(path, doAntiAlias: false);
verify(
() => canvas.clipPath(path, doAntiAlias: false),
).called(1);
});
test("clipRRect calls Canvas's clipRRect", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.clipRRect(rRect, doAntiAlias: false);
verify(
() => canvas.clipRRect(rRect, doAntiAlias: false),
).called(1);
});
test("clipRect calls Canvas's clipRect", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.clipRect(rect, doAntiAlias: false);
verify(
() => canvas.clipRect(rect, doAntiAlias: false),
).called(1);
});
test("drawArc calls Canvas's drawArc", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawArc(rect, 0, 1, false, paint);
verify(
() => canvas.drawArc(rect, 0, 1, false, paint),
).called(1);
});
test("drawAtlas calls Canvas's drawAtlas", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawAtlas(atlas, [], [], [], blendMode, rect, paint);
verify(
() => canvas.drawAtlas(atlas, [], [], [], blendMode, rect, paint),
).called(1);
});
test("drawCircle calls Canvas's drawCircle", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawCircle(offset, 0, paint);
verify(
() => canvas.drawCircle(offset, 0, paint),
).called(1);
});
test("drawColor calls Canvas's drawColor", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawColor(color, blendMode);
verify(
() => canvas.drawColor(color, blendMode),
).called(1);
});
test("drawDRRect calls Canvas's drawDRRect", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawDRRect(rRect, rRect, paint);
verify(
() => canvas.drawDRRect(rRect, rRect, paint),
).called(1);
});
test("drawImage calls Canvas's drawImage", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawImage(atlas, offset, paint);
verify(
() => canvas.drawImage(atlas, offset, paint),
).called(1);
});
test("drawImageNine calls Canvas's drawImageNine", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawImageNine(atlas, rect, rect, paint);
verify(
() => canvas.drawImageNine(atlas, rect, rect, paint),
).called(1);
});
test("drawImageRect calls Canvas's drawImageRect", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawImageRect(atlas, rect, rect, paint);
verify(
() => canvas.drawImageRect(atlas, rect, rect, paint),
).called(1);
});
test("drawLine calls Canvas's drawLine", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawLine(offset, offset, paint);
verify(
() => canvas.drawLine(offset, offset, paint),
).called(1);
});
test("drawOval calls Canvas's drawOval", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawOval(rect, paint);
verify(
() => canvas.drawOval(rect, paint),
).called(1);
});
test("drawPaint calls Canvas's drawPaint", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawPaint(paint);
verify(
() => canvas.drawPaint(paint),
).called(1);
});
test("drawParagraph calls Canvas's drawParagraph", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawParagraph(paragraph, offset);
verify(
() => canvas.drawParagraph(paragraph, offset),
).called(1);
});
test("drawPath calls Canvas's drawPath", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawPath(path, paint);
verify(
() => canvas.drawPath(path, paint),
).called(1);
});
test("drawPicture calls Canvas's drawPicture", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawPicture(picture);
verify(
() => canvas.drawPicture(picture),
).called(1);
});
test("drawPoints calls Canvas's drawPoints", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawPoints(PointMode.points, [offset], paint);
verify(
() => canvas.drawPoints(PointMode.points, [offset], paint),
).called(1);
});
test("drawRRect calls Canvas's drawRRect", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawRRect(rRect, paint);
verify(
() => canvas.drawRRect(rRect, paint),
).called(1);
});
test("drawRawAtlas calls Canvas's drawRawAtlas", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawRawAtlas(
atlas,
float32list,
float32list,
int32list,
BlendMode.clear,
rect,
paint,
);
verify(
() => canvas.drawRawAtlas(
atlas,
float32list,
float32list,
int32list,
BlendMode.clear,
rect,
paint,
),
).called(1);
});
test("drawRawPoints calls Canvas's drawRawPoints", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawRawPoints(PointMode.points, float32list, paint);
verify(
() => canvas.drawRawPoints(PointMode.points, float32list, paint),
).called(1);
});
test("drawRect calls Canvas's drawRect", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawRect(rect, paint);
verify(
() => canvas.drawRect(rect, paint),
).called(1);
});
test("drawShadow calls Canvas's drawShadow", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawShadow(path, color, 0, false);
verify(
() => canvas.drawShadow(path, color, 0, false),
).called(1);
});
test("drawVertices calls Canvas's drawVertices", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.drawVertices(vertices, blendMode, paint);
verify(
() => canvas.drawVertices(vertices, blendMode, paint),
).called(1);
});
test("getSaveCount calls Canvas's getSaveCount", () {
final zcanvas = ZCanvas()..canvas = canvas;
when(() => canvas.getSaveCount()).thenReturn(1);
zcanvas.getSaveCount();
verify(() => canvas.getSaveCount()).called(1);
});
test("restore calls Canvas's restore", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.restore();
verify(() => canvas.restore()).called(1);
});
test("rotate calls Canvas's rotate", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.rotate(0);
verify(() => canvas.rotate(0)).called(1);
});
test("save calls Canvas's save", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.save();
verify(() => canvas.save()).called(1);
});
test("saveLayer calls Canvas's saveLayer", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.saveLayer(rect, paint);
verify(() => canvas.saveLayer(rect, paint)).called(1);
});
test("scale calls Canvas's scale", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.scale(0, 0);
verify(() => canvas.scale(0, 0)).called(1);
});
test("skew calls Canvas's skew", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.skew(0, 0);
verify(() => canvas.skew(0, 0)).called(1);
});
test("transform calls Canvas's transform", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.transform(float64list);
verify(() => canvas.transform(float64list)).called(1);
});
test("translate calls Canvas's translate", () {
final zcanvas = ZCanvas()..canvas = canvas;
zcanvas.translate(0, 0);
verify(() => canvas.translate(0, 0)).called(1);
});
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

@ -36,9 +36,9 @@ class $AssetsImagesAndroidGen {
AssetGenImage get background =>
const AssetGenImage('assets/images/android/background.png');
/// File path: assets/images/android/character.png
AssetGenImage get character =>
const AssetGenImage('assets/images/android/character.png');
/// File path: assets/images/android/ball.png
AssetGenImage get ball =>
const AssetGenImage('assets/images/android/ball.png');
/// File path: assets/images/android/icon.png
AssetGenImage get icon =>
@ -60,9 +60,8 @@ class $AssetsImagesDashGen {
AssetGenImage get background =>
const AssetGenImage('assets/images/dash/background.png');
/// File path: assets/images/dash/character.png
AssetGenImage get character =>
const AssetGenImage('assets/images/dash/character.png');
/// File path: assets/images/dash/ball.png
AssetGenImage get ball => const AssetGenImage('assets/images/dash/ball.png');
/// File path: assets/images/dash/icon.png
AssetGenImage get icon => const AssetGenImage('assets/images/dash/icon.png');
@ -83,9 +82,8 @@ class $AssetsImagesDinoGen {
AssetGenImage get background =>
const AssetGenImage('assets/images/dino/background.png');
/// File path: assets/images/dino/character.png
AssetGenImage get character =>
const AssetGenImage('assets/images/dino/character.png');
/// File path: assets/images/dino/ball.png
AssetGenImage get ball => const AssetGenImage('assets/images/dino/ball.png');
/// File path: assets/images/dino/icon.png
AssetGenImage get icon => const AssetGenImage('assets/images/dino/icon.png');
@ -106,9 +104,9 @@ class $AssetsImagesSparkyGen {
AssetGenImage get background =>
const AssetGenImage('assets/images/sparky/background.png');
/// File path: assets/images/sparky/character.png
AssetGenImage get character =>
const AssetGenImage('assets/images/sparky/character.png');
/// File path: assets/images/sparky/ball.png
AssetGenImage get ball =>
const AssetGenImage('assets/images/sparky/ball.png');
/// File path: assets/images/sparky/icon.png
AssetGenImage get icon =>

@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template android_theme}
@ -12,7 +11,7 @@ class AndroidTheme extends CharacterTheme {
String get name => 'Android';
@override
Color get ballColor => Colors.green;
AssetGenImage get ball => Assets.images.android.ball;
@override
AssetGenImage get background => Assets.images.android.background;

@ -1,5 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template character_theme}
@ -15,8 +14,8 @@ abstract class CharacterTheme extends Equatable {
/// Name of character.
String get name;
/// Ball color for this theme.
Color get ballColor;
/// Asset for the ball.
AssetGenImage get ball;
/// Asset for the background.
AssetGenImage get background;
@ -33,7 +32,7 @@ abstract class CharacterTheme extends Equatable {
@override
List<Object?> get props => [
name,
ballColor,
ball,
background,
icon,
leaderboardIcon,

@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template dash_theme}
@ -12,7 +11,7 @@ class DashTheme extends CharacterTheme {
String get name => 'Dash';
@override
Color get ballColor => Colors.blue;
AssetGenImage get ball => Assets.images.dash.ball;
@override
AssetGenImage get background => Assets.images.dash.background;

@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template dino_theme}
@ -12,7 +11,7 @@ class DinoTheme extends CharacterTheme {
String get name => 'Dino';
@override
Color get ballColor => Colors.grey;
AssetGenImage get ball => Assets.images.dino.ball;
@override
AssetGenImage get background => Assets.images.dino.background;

@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template sparky_theme}
@ -9,7 +8,7 @@ class SparkyTheme extends CharacterTheme {
const SparkyTheme();
@override
Color get ballColor => Colors.orange;
AssetGenImage get ball => Assets.images.sparky.ball;
@override
String get name => 'Sparky';

@ -0,0 +1,152 @@
// ignore_for_file: cascade_invocations, prefer_const_constructors
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import '../../../../helpers/helpers.dart';
class _MockGameBloc extends Mock implements GameBloc {}
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
class _MockStreamSubscription extends Mock
implements StreamSubscription<SpaceshipRampState> {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
Assets.images.android.rail.main.keyName,
Assets.images.android.rail.exit.keyName,
Assets.images.score.oneMillion.keyName,
];
group('RampBonusBehavior', () {
const shotPoints = Points.oneMillion;
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
flameBlocTester.testGameWidget(
'when hits are multiples of 10 times adds a ScoringBehavior',
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: SpaceshipRampState(hits: 9),
);
final behavior = RampBonusBehavior(
points: shotPoints,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 10));
final scores = game.descendants().whereType<ScoringBehavior>();
await game.ready();
expect(scores.length, 1);
},
);
flameBlocTester.testGameWidget(
"when hits are not multiple of 10 times doesn't add any ScoringBehavior",
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: SpaceshipRampState.initial(),
);
final behavior = RampBonusBehavior(
points: shotPoints,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 1));
final scores = game.descendants().whereType<ScoringBehavior>();
await game.ready();
expect(scores.length, 0);
},
);
flameBlocTester.testGameWidget(
'closes subscription when removed',
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: SpaceshipRampState.initial(),
);
when(bloc.close).thenAnswer((_) async {});
final subscription = _MockStreamSubscription();
when(subscription.cancel).thenAnswer((_) async {});
final behavior = RampBonusBehavior.test(
points: shotPoints,
subscription: subscription,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
parent.remove(behavior);
await game.ready();
verify(subscription.cancel).called(1);
},
);
});
}

@ -0,0 +1,156 @@
// ignore_for_file: cascade_invocations, prefer_const_constructors
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import '../../../../helpers/helpers.dart';
class _MockGameBloc extends Mock implements GameBloc {}
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
class _MockStreamSubscription extends Mock
implements StreamSubscription<SpaceshipRampState> {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
Assets.images.android.rail.main.keyName,
Assets.images.android.rail.exit.keyName,
Assets.images.score.fiveThousand.keyName,
];
group('RampShotBehavior', () {
const shotPoints = Points.fiveThousand;
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
flameBlocTester.testGameWidget(
'when hits are not multiple of 10 times '
'increases multiplier and adds a ScoringBehavior',
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: SpaceshipRampState.initial(),
);
final behavior = RampShotBehavior(
points: shotPoints,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 1));
final scores = game.descendants().whereType<ScoringBehavior>();
await game.ready();
verify(() => gameBloc.add(MultiplierIncreased())).called(1);
expect(scores.length, 1);
},
);
flameBlocTester.testGameWidget(
'when hits multiple of 10 times '
"doesn't increase multiplier, neither ScoringBehavior",
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: SpaceshipRampState(hits: 9),
);
final behavior = RampShotBehavior(
points: shotPoints,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 10));
final scores = game.children.whereType<ScoringBehavior>();
await game.ready();
verifyNever(() => gameBloc.add(MultiplierIncreased()));
expect(scores.length, 0);
},
);
flameBlocTester.testGameWidget(
'closes subscription when removed',
setUp: (game, tester) async {
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: SpaceshipRampState.initial(),
);
when(bloc.close).thenAnswer((_) async {});
final subscription = _MockStreamSubscription();
when(subscription.cancel).thenAnswer((_) async {});
final behavior = RampShotBehavior.test(
points: shotPoints,
subscription: subscription,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ZCanvasComponent(children: [parent]));
await parent.ensureAdd(behavior);
parent.remove(behavior);
await game.ready();
verify(subscription.cancel).called(1);
},
);
});
}

@ -2,13 +2,12 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame/extensions.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 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../helpers/helpers.dart';
@ -33,13 +32,16 @@ class _MockBall extends Mock implements Ball {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
theme.Assets.images.dash.ball.keyName,
];
group('BallController', () {
late Ball ball;
late GameBloc gameBloc;
setUp(() {
ball = Ball(baseColor: const Color(0xFF00FFFF));
ball = Ball();
gameBloc = _MockGameBloc();
whenListen(
gameBloc,
@ -51,6 +53,7 @@ void main() {
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
test('can be instantiated', () {
@ -68,7 +71,7 @@ void main() {
await ball.add(controller);
await game.ensureAdd(ball);
final otherBall = Ball(baseColor: const Color(0xFF00FFFF));
final otherBall = Ball();
final otherController = BallController(otherBall);
await otherBall.add(otherController);
await game.ensureAdd(otherBall);
@ -106,6 +109,7 @@ void main() {
flameBlocTester.testGameWidget(
'adds TurboChargeActivated',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final controller = BallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);

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

Loading…
Cancel
Save