refactor: bumper animation logic (#204)

* chore: update assets

* refactor: bumpers to use group components

* refactor: update dash nest games

* refactor: load assets in test games

* test: bumper changes

* fix: analyzer warning

* refactor: load assets in sandbox stories

* refactor: back to active/inactive naming

* refactor: sprite sizing

* refactor: use loadAll

* refactor: loadAll in sandbox

* refactor: comments and sizing

* fix: preload assets for pinball page test

* refactor: load signpost assets from cache

* refactor: signpost tests from cache

* refactor: loadAll sprites
pull/211/head
Allison Ryan 2 years ago committed by GitHub
parent 00e006bde3
commit 58324adad6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -5,14 +5,12 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template alien_zone}
/// Area positioned below [Spaceship] where the [Ball]
/// can bounce off [AlienBumper]s.
///
/// When a [Ball] hits [AlienBumper]s, they toggle between activated and
/// deactivated states.
/// When a [Ball] hits an [AlienBumper], the bumper animates.
/// {@endtemplate}
class AlienZone extends Component with HasGameRef<PinballGame> {
/// {@macro alien_zone}
@ -22,12 +20,12 @@ class AlienZone extends Component with HasGameRef<PinballGame> {
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(_ControlledAlienBumperBallContactCallback());
gameRef.addContactCallback(AlienBumperBallContactCallback());
final lowerBumper = ControlledAlienBumper.a()
..initialPosition = Vector2(-32.52, -9.34);
final upperBumper = ControlledAlienBumper.b()
..initialPosition = Vector2(-22.89, -17.43);
final lowerBumper = _AlienBumper.a()
..initialPosition = Vector2(-32.52, -9.1);
final upperBumper = _AlienBumper.b()
..initialPosition = Vector2(-22.89, -17.35);
await addAll([
lowerBumper,
@ -36,60 +34,27 @@ class AlienZone extends Component with HasGameRef<PinballGame> {
}
}
/// {@template controlled_alien_bumper}
/// [AlienBumper] with [_AlienBumperController] attached.
/// {@endtemplate}
@visibleForTesting
class ControlledAlienBumper extends AlienBumper
with Controls<_AlienBumperController>, ScorePoints {
/// {@macro controlled_alien_bumper}
ControlledAlienBumper.a() : super.a() {
controller = _AlienBumperController(this);
}
// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
// ContactCallback process is enhanced.
class _AlienBumper extends AlienBumper with ScorePoints {
_AlienBumper.a() : super.a();
/// {@macro controlled_alien_bumper}
ControlledAlienBumper.b() : super.b() {
controller = _AlienBumperController(this);
}
_AlienBumper.b() : super.b();
@override
// TODO(ruimiguel): change points when get final points map.
int get points => 20;
}
/// {@template alien_bumper_controller}
/// Controls a [AlienBumper].
/// {@endtemplate}
class _AlienBumperController extends ComponentController<AlienBumper>
with HasGameRef<PinballGame> {
/// {@macro alien_bumper_controller}
_AlienBumperController(AlienBumper alienBumper) : super(alienBumper);
/// Flag for activated state of the [AlienBumper].
///
/// Used to toggle [AlienBumper]s' state between activated and deactivated.
bool isActivated = false;
/// Registers when a [AlienBumper] is hit by a [Ball].
void hit() {
if (isActivated) {
component.deactivate();
} else {
component.activate();
}
isActivated = !isActivated;
}
}
/// Listens when a [Ball] bounces bounces against a [AlienBumper].
class _ControlledAlienBumperBallContactCallback
extends ContactCallback<Controls<_AlienBumperController>, Ball> {
/// Listens when a [Ball] bounces against an [AlienBumper].
@visibleForTesting
class AlienBumperBallContactCallback
extends ContactCallback<AlienBumper, Ball> {
@override
void begin(
Controls<_AlienBumperController> controlledAlienBumper,
AlienBumper alienBumper,
Ball _,
Contact __,
) {
controlledAlienBumper.controller.hit();
alienBumper.animate();
}
}

@ -11,7 +11,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// can bounce off [DashNestBumper]s.
///
/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest]
/// is awarded, and the [BigDashNestBumper] releases a new [Ball].
/// is awarded, and the [DashNestBumper.main] releases a new [Ball].
/// {@endtemplate}
class FlutterForest extends Component
with Controls<_FlutterForestController>, HasGameRef<PinballGame> {
@ -27,11 +27,11 @@ class FlutterForest extends Component
final signpost = Signpost()..initialPosition = Vector2(8.35, -58.3);
final bigNest = _BigDashNestBumper()
final bigNest = _DashNestBumper.main()
..initialPosition = Vector2(18.55, -59.35);
final smallLeftNest = _SmallDashNestBumper.a()
final smallLeftNest = _DashNestBumper.a()
..initialPosition = Vector2(8.95, -51.95);
final smallRightNest = _SmallDashNestBumper.b()
final smallRightNest = _DashNestBumper.b()
..initialPosition = Vector2(23.3, -46.75);
final dashAnimatronic = DashAnimatronic()..position = Vector2(20, -66);
@ -81,15 +81,12 @@ class _FlutterForestController extends ComponentController<FlutterForest>
// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
// ContactCallback process is enhanced.
class _BigDashNestBumper extends BigDashNestBumper with ScorePoints {
@override
int get points => 20;
}
class _DashNestBumper extends DashNestBumper with ScorePoints {
_DashNestBumper.main() : super.main();
class _SmallDashNestBumper extends SmallDashNestBumper with ScorePoints {
_SmallDashNestBumper.a() : super.a();
_DashNestBumper.a() : super.a();
_SmallDashNestBumper.b() : super.b();
_DashNestBumper.b() : super.b();
@override
int get points => 20;

@ -5,14 +5,12 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template sparky_fire_zone}
/// Area positioned at the top left of the [Board] where the [Ball]
/// can bounce off [SparkyBumper]s.
///
/// When a [Ball] hits [SparkyBumper]s, they toggle between activated and
/// deactivated states.
/// When a [Ball] hits [SparkyBumper]s, the bumper animates.
/// {@endtemplate}
class SparkyFireZone extends Component with HasGameRef<PinballGame> {
/// {@macro sparky_fire_zone}
@ -22,14 +20,14 @@ class SparkyFireZone extends Component with HasGameRef<PinballGame> {
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(_ControlledSparkyBumperBallContactCallback());
gameRef.addContactCallback(SparkyBumperBallContactCallback());
final lowerLeftBumper = ControlledSparkyBumper.a()
..initialPosition = Vector2(-23.15, -41.65);
final upperLeftBumper = ControlledSparkyBumper.b()
..initialPosition = Vector2(-21.25, -58.15);
final rightBumper = ControlledSparkyBumper.c()
..initialPosition = Vector2(-3.56, -53.051);
final lowerLeftBumper = _SparkyBumper.a()
..initialPosition = Vector2(-22.9, -41.65);
final upperLeftBumper = _SparkyBumper.b()
..initialPosition = Vector2(-21.25, -57.9);
final rightBumper = _SparkyBumper.c()
..initialPosition = Vector2(-3.3, -52.55);
await addAll([
lowerLeftBumper,
@ -39,65 +37,29 @@ class SparkyFireZone extends Component with HasGameRef<PinballGame> {
}
}
/// {@template controlled_sparky_bumper}
/// [SparkyBumper] with [_SparkyBumperController] attached.
/// {@endtemplate}
@visibleForTesting
class ControlledSparkyBumper extends SparkyBumper
with Controls<_SparkyBumperController>, ScorePoints {
///{@macro controlled_sparky_bumper}
ControlledSparkyBumper.a() : super.a() {
controller = _SparkyBumperController(this);
}
// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
// ContactCallback process is enhanced.
class _SparkyBumper extends SparkyBumper with ScorePoints {
_SparkyBumper.a() : super.a();
///{@macro controlled_sparky_bumper}
ControlledSparkyBumper.b() : super.b() {
controller = _SparkyBumperController(this);
}
_SparkyBumper.b() : super.b();
///{@macro controlled_sparky_bumper}
ControlledSparkyBumper.c() : super.c() {
controller = _SparkyBumperController(this);
}
_SparkyBumper.c() : super.c();
@override
int get points => 20;
}
/// {@template sparky_bumper_controller}
/// Controls a [SparkyBumper].
/// {@endtemplate}
class _SparkyBumperController extends ComponentController<SparkyBumper>
with HasGameRef<PinballGame> {
/// {@macro sparky_bumper_controller}
_SparkyBumperController(ControlledSparkyBumper controlledSparkyBumper)
: super(controlledSparkyBumper);
/// Flag for activated state of the [SparkyBumper].
///
/// Used to toggle [SparkyBumper]s' state between activated and deactivated.
bool isActivated = false;
/// Registers when a [SparkyBumper] is hit by a [Ball].
void hit() {
if (isActivated) {
component.deactivate();
} else {
component.activate();
}
isActivated = !isActivated;
}
}
/// Listens when a [Ball] bounces bounces against a [SparkyBumper].
class _ControlledSparkyBumperBallContactCallback
extends ContactCallback<Controls<_SparkyBumperController>, Ball> {
@visibleForTesting
class SparkyBumperBallContactCallback
extends ContactCallback<SparkyBumper, Ball> {
@override
void begin(
Controls<_SparkyBumperController> controlledSparkyBumper,
SparkyBumper sparkyBumper,
Ball _,
Contact __,
) {
controlledSparkyBumper.controller.hit();
sparkyBumper.animate();
}
}

@ -51,6 +51,10 @@ extension PinballGameAssetsX on PinballGame {
),
images.load(components.Assets.images.spaceship.rail.main.keyName),
images.load(components.Assets.images.spaceship.rail.foreground.keyName),
images.load(components.Assets.images.alienBumper.a.active.keyName),
images.load(components.Assets.images.alienBumper.a.inactive.keyName),
images.load(components.Assets.images.alienBumper.b.active.keyName),
images.load(components.Assets.images.alienBumper.b.inactive.keyName),
images.load(components.Assets.images.chromeDino.mouth.keyName),
images.load(components.Assets.images.chromeDino.head.keyName),
images.load(components.Assets.images.plunger.plunger.keyName),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

@ -1,37 +1,41 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template alien_bumper}
/// Bumper for Alien area.
/// Bumper for area under the [Spaceship].
/// {@endtemplate}
// TODO(ruimiguel): refactor later to unify with DashBumpers.
class AlienBumper extends BodyComponent with InitialPosition {
/// {@macro alien_bumper}
AlienBumper._({
required double majorRadius,
required double minorRadius,
required String activeAssetPath,
required String inactiveAssetPath,
required SpriteComponent spriteComponent,
required String onAssetPath,
required String offAssetPath,
}) : _majorRadius = majorRadius,
_minorRadius = minorRadius,
_activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath,
_spriteComponent = spriteComponent,
super(priority: RenderPriority.alienBumper);
super(
priority: RenderPriority.alienBumper,
children: [
_AlienBumperSpriteGroupComponent(
onAssetPath: onAssetPath,
offAssetPath: offAssetPath,
),
],
) {
renderBody = false;
}
/// {@macro alien_bumper}
AlienBumper.a()
: this._(
majorRadius: 3.52,
minorRadius: 2.97,
activeAssetPath: Assets.images.alienBumper.a.active.keyName,
inactiveAssetPath: Assets.images.alienBumper.a.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.1),
),
onAssetPath: Assets.images.alienBumper.a.active.keyName,
offAssetPath: Assets.images.alienBumper.a.inactive.keyName,
);
/// {@macro alien_bumper}
@ -39,32 +43,12 @@ class AlienBumper extends BodyComponent with InitialPosition {
: this._(
majorRadius: 3.19,
minorRadius: 2.79,
activeAssetPath: Assets.images.alienBumper.b.active.keyName,
inactiveAssetPath: Assets.images.alienBumper.b.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.1),
),
onAssetPath: Assets.images.alienBumper.b.active.keyName,
offAssetPath: Assets.images.alienBumper.b.inactive.keyName,
);
final double _majorRadius;
final double _minorRadius;
final String _activeAssetPath;
late final Sprite _activeSprite;
final String _inactiveAssetPath;
late final Sprite _inactiveSprite;
final SpriteComponent _spriteComponent;
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await _loadSprites();
deactivate();
await add(_spriteComponent);
}
@override
Body createBody() {
@ -85,25 +69,53 @@ class AlienBumper extends BodyComponent with InitialPosition {
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
Future<void> _loadSprites() async {
// TODO(alestiago): I think ideally we would like to do:
// Sprite(path).load so we don't require to store the activeAssetPath and
// the inactive assetPath.
_inactiveSprite = await gameRef.loadSprite(_inactiveAssetPath);
_activeSprite = await gameRef.loadSprite(_activeAssetPath);
/// Animates the [AlienBumper].
Future<void> animate() async {
final spriteGroupComponent = firstChild<_AlienBumperSpriteGroupComponent>()
?..current = AlienBumperSpriteState.inactive;
await Future<void>.delayed(const Duration(milliseconds: 50));
spriteGroupComponent?.current = AlienBumperSpriteState.active;
}
}
/// Activates the [AlienBumper].
void activate() {
_spriteComponent
..sprite = _activeSprite
..size = _activeSprite.originalSize / 10;
}
/// Indicates the [AlienBumper]'s current sprite state.
@visibleForTesting
enum AlienBumperSpriteState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}
class _AlienBumperSpriteGroupComponent
extends SpriteGroupComponent<AlienBumperSpriteState> with HasGameRef {
_AlienBumperSpriteGroupComponent({
required String onAssetPath,
required String offAssetPath,
}) : _onAssetPath = onAssetPath,
_offAssetPath = offAssetPath,
super(
anchor: Anchor.center,
position: Vector2(0, -0.1),
);
final String _onAssetPath;
final String _offAssetPath;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprites = {
AlienBumperSpriteState.active:
Sprite(gameRef.images.fromCache(_onAssetPath)),
AlienBumperSpriteState.inactive:
Sprite(gameRef.images.fromCache(_offAssetPath)),
};
this.sprites = sprites;
/// Deactivates the [AlienBumper].
void deactivate() {
_spriteComponent
..sprite = _inactiveSprite
..size = _inactiveSprite.originalSize / 10;
current = AlienBumperSpriteState.active;
size = sprites[current]!.originalSize / 10;
}
}

@ -2,7 +2,7 @@ import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template dash_animatronic}
/// Animated Dash that sits on top of the [BigDashNestBumper].
/// Animated Dash that sits on top of the [DashNestBumper.main].
/// {@endtemplate}
class DashAnimatronic extends SpriteAnimationComponent with HasGameRef {
/// {@macro dash_animatronic}

@ -2,139 +2,76 @@ import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template dash_nest_bumper}
/// Bumper with a nest appearance.
/// {@endtemplate}
abstract class DashNestBumper extends BodyComponent with InitialPosition {
class DashNestBumper extends BodyComponent with InitialPosition {
/// {@macro dash_nest_bumper}
DashNestBumper._({
required double majorRadius,
required double minorRadius,
required String activeAssetPath,
required String inactiveAssetPath,
required SpriteComponent spriteComponent,
}) : _activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath,
_spriteComponent = spriteComponent,
super(priority: RenderPriority.dashBumper);
final String _activeAssetPath;
late final Sprite _activeSprite;
final String _inactiveAssetPath;
late final Sprite _inactiveSprite;
final SpriteComponent _spriteComponent;
Future<void> _loadSprites() async {
// TODO(alestiago): I think ideally we would like to do:
// Sprite(path).load so we don't require to store the activeAssetPath and
// the inactive assetPath.
_inactiveSprite = await gameRef.loadSprite(_inactiveAssetPath);
_activeSprite = await gameRef.loadSprite(_activeAssetPath);
}
/// Activates the [DashNestBumper].
void activate() {
_spriteComponent
..sprite = _activeSprite
..size = _activeSprite.originalSize / 10;
required Vector2 spritePosition,
}) : _majorRadius = majorRadius,
_minorRadius = minorRadius,
super(
priority: RenderPriority.dashBumper,
children: [
_DashNestBumperSpriteGroupComponent(
activeAssetPath: activeAssetPath,
inactiveAssetPath: inactiveAssetPath,
position: spritePosition,
),
],
) {
renderBody = false;
}
/// Deactivates the [DashNestBumper].
void deactivate() {
_spriteComponent
..sprite = _inactiveSprite
..size = _inactiveSprite.originalSize / 10;
}
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprites();
// TODO(erickzanardo): Look into using onNewState instead.
// Currently doing: onNewState(gameRef.read<GameState>()) will throw an
// `Exception: build context is not available yet`
deactivate();
await add(_spriteComponent);
}
}
/// {@macro dash_nest_bumper}
class BigDashNestBumper extends DashNestBumper {
/// {@macro dash_nest_bumper}
BigDashNestBumper()
: super._(
DashNestBumper.main()
: this._(
majorRadius: 5.1,
minorRadius: 3.75,
activeAssetPath: Assets.images.dash.bumper.main.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.3),
),
spritePosition: Vector2(0, -0.3),
);
@override
Body createBody() {
final shape = EllipseShape(
center: Vector2.zero(),
majorRadius: 5.1,
minorRadius: 3.75,
)..rotate(math.pi / 1.9);
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
/// {@macro dash_nest_bumper}
class SmallDashNestBumper extends DashNestBumper {
/// {@macro dash_nest_bumper}
SmallDashNestBumper._({
required String activeAssetPath,
required String inactiveAssetPath,
required SpriteComponent spriteComponent,
}) : super._(
activeAssetPath: activeAssetPath,
inactiveAssetPath: inactiveAssetPath,
spriteComponent: spriteComponent,
);
/// {@macro dash_nest_bumper}
SmallDashNestBumper.a()
DashNestBumper.a()
: this._(
majorRadius: 3,
minorRadius: 2.5,
activeAssetPath: Assets.images.dash.bumper.a.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0.35, -1.2),
),
spritePosition: Vector2(0.35, -1.2),
);
/// {@macro dash_nest_bumper}
SmallDashNestBumper.b()
DashNestBumper.b()
: this._(
majorRadius: 3,
minorRadius: 2.5,
activeAssetPath: Assets.images.dash.bumper.b.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0.35, -1.2),
),
spritePosition: Vector2(0.35, -1.2),
);
final double _majorRadius;
final double _minorRadius;
@override
Body createBody() {
final shape = EllipseShape(
center: Vector2.zero(),
majorRadius: 3,
minorRadius: 2.25,
)..rotate(math.pi / 2);
final fixtureDef = FixtureDef(
shape,
restitution: 4,
);
majorRadius: _majorRadius,
minorRadius: _minorRadius,
)..rotate(math.pi / 1.9);
final fixtureDef = FixtureDef(shape, restitution: 4);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
@ -142,4 +79,58 @@ class SmallDashNestBumper extends DashNestBumper {
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
/// Activates the [DashNestBumper].
void activate() {
firstChild<_DashNestBumperSpriteGroupComponent>()?.current =
DashNestBumperSpriteState.active;
}
/// Deactivates the [DashNestBumper].
void deactivate() {
firstChild<_DashNestBumperSpriteGroupComponent>()?.current =
DashNestBumperSpriteState.inactive;
}
}
/// Indicates the [DashNestBumper]'s current sprite state.
@visibleForTesting
enum DashNestBumperSpriteState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}
class _DashNestBumperSpriteGroupComponent
extends SpriteGroupComponent<DashNestBumperSpriteState> with HasGameRef {
_DashNestBumperSpriteGroupComponent({
required String activeAssetPath,
required String inactiveAssetPath,
required Vector2 position,
}) : _activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath,
super(
anchor: Anchor.center,
position: position,
);
final String _activeAssetPath;
final String _inactiveAssetPath;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprites = {
DashNestBumperSpriteState.active:
Sprite(gameRef.images.fromCache(_activeAssetPath)),
DashNestBumperSpriteState.inactive:
Sprite(gameRef.images.fromCache(_inactiveAssetPath)),
};
this.sprites = sprites;
current = DashNestBumperSpriteState.inactive;
size = sprites[current]!.originalSize / 10;
}
}

@ -88,12 +88,9 @@ class _SignpostSpriteComponent extends SpriteGroupComponent<SignpostSpriteState>
final sprites = <SignpostSpriteState, Sprite>{};
this.sprites = sprites;
for (final spriteState in SignpostSpriteState.values) {
// TODO(allisonryan0002): Support caching
// https://github.com/VGVentures/pinball/pull/204
// sprites[spriteState] = Sprite(
// gameRef.images.fromCache(spriteState.path),
// );
sprites[spriteState] = await gameRef.loadSprite(spriteState.path);
sprites[spriteState] = Sprite(
gameRef.images.fromCache(spriteState.path),
);
}
current = SignpostSpriteState.inactive;

@ -2,38 +2,43 @@ import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template sparky_bumper}
/// Bumper for Sparky area.
/// {@endtemplate}
// TODO(ruimiguel): refactor later to unify with DashBumpers.
class SparkyBumper extends BodyComponent with InitialPosition {
/// {@macro sparky_bumper}
SparkyBumper._({
required double majorRadius,
required double minorRadius,
required String activeAssetPath,
required String inactiveAssetPath,
required SpriteComponent spriteComponent,
required String onAssetPath,
required String offAssetPath,
required Vector2 spritePosition,
}) : _majorRadius = majorRadius,
_minorRadius = minorRadius,
_activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath,
_spriteComponent = spriteComponent,
super(priority: RenderPriority.sparkyBumper);
super(
priority: RenderPriority.sparkyBumper,
children: [
_SparkyBumperSpriteGroupComponent(
onAssetPath: onAssetPath,
offAssetPath: offAssetPath,
position: spritePosition,
),
],
) {
renderBody = false;
}
/// {@macro sparky_bumper}
SparkyBumper.a()
: this._(
majorRadius: 2.9,
minorRadius: 2.1,
activeAssetPath: Assets.images.sparky.bumper.a.active.keyName,
inactiveAssetPath: Assets.images.sparky.bumper.a.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.25),
),
onAssetPath: Assets.images.sparky.bumper.a.active.keyName,
offAssetPath: Assets.images.sparky.bumper.a.inactive.keyName,
spritePosition: Vector2(0, -0.25),
);
/// {@macro sparky_bumper}
@ -41,12 +46,9 @@ class SparkyBumper extends BodyComponent with InitialPosition {
: this._(
majorRadius: 2.85,
minorRadius: 2,
activeAssetPath: Assets.images.sparky.bumper.b.active.keyName,
inactiveAssetPath: Assets.images.sparky.bumper.b.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.35),
),
onAssetPath: Assets.images.sparky.bumper.b.active.keyName,
offAssetPath: Assets.images.sparky.bumper.b.inactive.keyName,
spritePosition: Vector2(0, -0.35),
);
/// {@macro sparky_bumper}
@ -54,33 +56,13 @@ class SparkyBumper extends BodyComponent with InitialPosition {
: this._(
majorRadius: 3,
minorRadius: 2.2,
activeAssetPath: Assets.images.sparky.bumper.c.active.keyName,
inactiveAssetPath: Assets.images.sparky.bumper.c.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.4),
),
onAssetPath: Assets.images.sparky.bumper.c.active.keyName,
offAssetPath: Assets.images.sparky.bumper.c.inactive.keyName,
spritePosition: Vector2(0, -0.4),
);
final double _majorRadius;
final double _minorRadius;
final String _activeAssetPath;
late final Sprite _activeSprite;
final String _inactiveAssetPath;
late final Sprite _inactiveSprite;
final SpriteComponent _spriteComponent;
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprites();
// TODO(erickzanardo): Look into using onNewState instead.
// Currently doing: onNewState(gameRef.read<GameState>()) will throw an
// `Exception: build context is not available yet`
deactivate();
await add(_spriteComponent);
}
@override
Body createBody() {
@ -102,25 +84,53 @@ class SparkyBumper extends BodyComponent with InitialPosition {
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
Future<void> _loadSprites() async {
// TODO(alestiago): I think ideally we would like to do:
// Sprite(path).load so we don't require to store the activeAssetPath and
// the inactive assetPath.
_inactiveSprite = await gameRef.loadSprite(_inactiveAssetPath);
_activeSprite = await gameRef.loadSprite(_activeAssetPath);
/// Animates the [DashNestBumper].
Future<void> animate() async {
final spriteGroupComponent = firstChild<_SparkyBumperSpriteGroupComponent>()
?..current = SparkyBumperSpriteState.inactive;
await Future<void>.delayed(const Duration(milliseconds: 50));
spriteGroupComponent?.current = SparkyBumperSpriteState.active;
}
}
/// Activates the [DashNestBumper].
void activate() {
_spriteComponent
..sprite = _activeSprite
..size = _activeSprite.originalSize / 10;
}
/// Indicates the [SparkyBumper]'s current sprite state.
@visibleForTesting
enum SparkyBumperSpriteState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}
class _SparkyBumperSpriteGroupComponent
extends SpriteGroupComponent<SparkyBumperSpriteState> with HasGameRef {
_SparkyBumperSpriteGroupComponent({
required String onAssetPath,
required String offAssetPath,
required Vector2 position,
}) : _onAssetPath = onAssetPath,
_offAssetPath = offAssetPath,
super(
anchor: Anchor.center,
position: position,
);
final String _onAssetPath;
final String _offAssetPath;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprites = {
SparkyBumperSpriteState.active:
Sprite(gameRef.images.fromCache(_onAssetPath)),
SparkyBumperSpriteState.inactive:
Sprite(gameRef.images.fromCache(_offAssetPath)),
};
this.sprites = sprites;
/// Deactivates the [DashNestBumper].
void deactivate() {
_spriteComponent
..sprite = _inactiveSprite
..size = _inactiveSprite.originalSize / 10;
current = SparkyBumperSpriteState.active;
size = sprites[current]!.originalSize / 10;
}
}

@ -17,6 +17,11 @@ class AlienBumperAGame extends BasicBallGame {
Future<void> onLoad() async {
await super.onLoad();
await images.loadAll([
Assets.images.alienBumper.a.active.keyName,
Assets.images.alienBumper.a.inactive.keyName,
]);
final center = screenToWorld(camera.viewport.canvasSize! / 2);
final alienBumperA = AlienBumper.a()
..initialPosition = Vector2(center.x - 20, center.y - 20)

@ -17,6 +17,11 @@ class AlienBumperBGame extends BasicBallGame {
Future<void> onLoad() async {
await super.onLoad();
await images.loadAll([
Assets.images.alienBumper.b.active.keyName,
Assets.images.alienBumper.b.inactive.keyName,
]);
final center = screenToWorld(camera.viewport.canvasSize! / 2);
final alienBumperB = AlienBumper.b()
..initialPosition = Vector2(center.x - 10, center.y + 10)

@ -15,9 +15,14 @@ class BigDashNestBumperGame extends BasicBallGame with Traceable {
@override
Future<void> onLoad() async {
await super.onLoad();
await images.loadAll([
Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
]);
camera.followVector2(Vector2.zero());
await add(BigDashNestBumper()..priority = 1);
await traceAllBodies();
await add(DashNestBumper.main()..priority = 1);
await traceAllBodies();
}
}

@ -17,8 +17,15 @@ class SignpostGame extends BasicBallGame with Traceable, TapDetector {
Future<void> onLoad() async {
await super.onLoad();
await images.loadAll([
Assets.images.signpost.inactive.keyName,
Assets.images.signpost.active1.keyName,
Assets.images.signpost.active2.keyName,
Assets.images.signpost.active3.keyName,
]);
camera.followVector2(Vector2.zero());
await add(Signpost()..priority = 1);
await add(Signpost());
await traceAllBodies();
}

@ -15,9 +15,14 @@ class SmallDashNestBumperAGame extends BasicBallGame with Traceable {
@override
Future<void> onLoad() async {
await super.onLoad();
await images.loadAll([
Assets.images.dash.bumper.a.active.keyName,
Assets.images.dash.bumper.a.inactive.keyName,
]);
camera.followVector2(Vector2.zero());
await add(SmallDashNestBumper.a()..priority = 1);
await traceAllBodies();
await add(DashNestBumper.a()..priority = 1);
await traceAllBodies();
}
}

@ -15,9 +15,14 @@ class SmallDashNestBumperBGame extends BasicBallGame with Traceable {
@override
Future<void> onLoad() async {
await super.onLoad();
await images.loadAll([
Assets.images.dash.bumper.b.active.keyName,
Assets.images.dash.bumper.b.inactive.keyName,
]);
camera.followVector2(Vector2.zero());
await add(SmallDashNestBumper.b()..priority = 1);
await traceAllBodies();
await add(DashNestBumper.b()..priority = 1);
await traceAllBodies();
}
}

@ -16,12 +16,21 @@ class SparkyBumperGame extends BasicBallGame with Traceable {
Future<void> onLoad() async {
await super.onLoad();
await images.loadAll([
Assets.images.sparky.bumper.a.active.keyName,
Assets.images.sparky.bumper.a.inactive.keyName,
Assets.images.sparky.bumper.b.active.keyName,
Assets.images.sparky.bumper.b.inactive.keyName,
Assets.images.sparky.bumper.c.active.keyName,
Assets.images.sparky.bumper.c.inactive.keyName,
]);
final center = screenToWorld(camera.viewport.canvasSize! / 2);
final sparkyBumperA = SparkyBumper.a()
..initialPosition = Vector2(center.x - 20, center.y - 20)
..initialPosition = Vector2(center.x - 20, center.y + 20)
..priority = 1;
final sparkyBumperB = SparkyBumper.b()
..initialPosition = Vector2(center.x - 10, center.y + 10)
..initialPosition = Vector2(center.x - 10, center.y - 10)
..priority = 1;
final sparkyBumperC = SparkyBumper.c()
..initialPosition = Vector2(center.x + 20, center.y)

@ -2,9 +2,19 @@ import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
class TestGame extends Forge2DGame {
TestGame() {
TestGame([List<String>? assets]) : _assets = assets {
images.prefix = '';
}
final List<String>? _assets;
@override
Future<void> onLoad() async {
if (_assets != null) {
await images.loadAll(_assets!);
}
await super.onLoad();
}
}
class KeyboardTestGame extends TestGame with HasKeyboardHandlerComponents {}

@ -9,7 +9,13 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
final assets = [
Assets.images.alienBumper.a.active.keyName,
Assets.images.alienBumper.a.inactive.keyName,
Assets.images.alienBumper.b.active.keyName,
Assets.images.alienBumper.b.inactive.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('AlienBumper', () {
flameTester.test('"a" loads correctly', (game) async {
@ -25,43 +31,30 @@ void main() {
expect(game.contains(bumper), isTrue);
});
flameTester.test('activate returns normally', (game) async {
flameTester.test('animate switches between on and off sprites',
(game) async {
final bumper = AlienBumper.a();
await game.ensureAdd(bumper);
expect(bumper.activate, returnsNormally);
});
flameTester.test('deactivate returns normally', (game) async {
final bumper = AlienBumper.a();
await game.ensureAdd(bumper);
expect(bumper.deactivate, returnsNormally);
});
final spriteGroupComponent = bumper.firstChild<SpriteGroupComponent>()!;
flameTester.test('changes sprite', (game) async {
final bumper = AlienBumper.a();
await game.ensureAdd(bumper);
final spriteComponent = bumper.firstChild<SpriteComponent>()!;
final deactivatedSprite = spriteComponent.sprite;
bumper.activate();
expect(
spriteComponent.sprite,
isNot(equals(deactivatedSprite)),
spriteGroupComponent.current,
equals(AlienBumperSpriteState.active),
);
final activatedSprite = spriteComponent.sprite;
bumper.deactivate();
final future = bumper.animate();
expect(
spriteComponent.sprite,
isNot(equals(activatedSprite)),
spriteGroupComponent.current,
equals(AlienBumperSpriteState.inactive),
);
await future;
expect(
activatedSprite,
isNot(equals(deactivatedSprite)),
spriteGroupComponent.current,
equals(AlienBumperSpriteState.active),
);
});
});

@ -9,107 +9,68 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group('BigDashNestBumper', () {
flameTester.test('loads correctly', (game) async {
final bumper = BigDashNestBumper();
group('DashNestBumper', () {
final assets = [
Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
Assets.images.dash.bumper.a.active.keyName,
Assets.images.dash.bumper.a.inactive.keyName,
Assets.images.dash.bumper.b.active.keyName,
Assets.images.dash.bumper.b.inactive.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
flameTester.test('"main" loads correctly', (game) async {
final bumper = DashNestBumper.main();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('activate returns normally', (game) async {
final bumper = BigDashNestBumper();
flameTester.test('"a" loads correctly', (game) async {
final bumper = DashNestBumper.a();
await game.ensureAdd(bumper);
expect(bumper.activate, returnsNormally);
expect(game.contains(bumper), isTrue);
});
flameTester.test('deactivate returns normally', (game) async {
final bumper = BigDashNestBumper();
flameTester.test('"b" loads correctly', (game) async {
final bumper = DashNestBumper.b();
await game.ensureAdd(bumper);
expect(bumper.deactivate, returnsNormally);
expect(game.contains(bumper), isTrue);
});
flameTester.test('changes sprite', (game) async {
final bumper = BigDashNestBumper();
flameTester.test('activate switches to active sprite', (game) async {
final bumper = DashNestBumper.main();
await game.ensureAdd(bumper);
final spriteComponent = bumper.firstChild<SpriteComponent>()!;
final spriteGroupComponent = bumper.firstChild<SpriteGroupComponent>()!;
final deactivatedSprite = spriteComponent.sprite;
bumper.activate();
expect(
spriteComponent.sprite,
isNot(equals(deactivatedSprite)),
spriteGroupComponent.current,
equals(DashNestBumperSpriteState.inactive),
);
final activatedSprite = spriteComponent.sprite;
bumper.deactivate();
expect(
spriteComponent.sprite,
isNot(equals(activatedSprite)),
);
bumper.activate();
expect(
activatedSprite,
isNot(equals(deactivatedSprite)),
spriteGroupComponent.current,
equals(DashNestBumperSpriteState.active),
);
});
});
group('SmallDashNestBumper', () {
flameTester.test('"a" loads correctly', (game) async {
final bumper = SmallDashNestBumper.a();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"b" loads correctly', (game) async {
final bumper = SmallDashNestBumper.b();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('activate returns normally', (game) async {
final bumper = SmallDashNestBumper.a();
await game.ensureAdd(bumper);
expect(bumper.activate, returnsNormally);
});
flameTester.test('deactivate returns normally', (game) async {
final bumper = SmallDashNestBumper.a();
await game.ensureAdd(bumper);
expect(bumper.deactivate, returnsNormally);
});
flameTester.test('changes sprite', (game) async {
final bumper = SmallDashNestBumper.a();
flameTester.test('deactivate switches to inactive sprite', (game) async {
final bumper = DashNestBumper.main();
await game.ensureAdd(bumper);
final spriteComponent = bumper.firstChild<SpriteComponent>()!;
final spriteGroupComponent = bumper.firstChild<SpriteGroupComponent>()!
..current = DashNestBumperSpriteState.active;
final deactivatedSprite = spriteComponent.sprite;
bumper.activate();
expect(
spriteComponent.sprite,
isNot(equals(deactivatedSprite)),
);
final activatedSprite = spriteComponent.sprite;
bumper.deactivate();
expect(
spriteComponent.sprite,
isNot(equals(activatedSprite)),
);
expect(
activatedSprite,
isNot(equals(deactivatedSprite)),
spriteGroupComponent.current,
equals(DashNestBumperSpriteState.inactive),
);
});
});

@ -9,7 +9,13 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
final assets = [
Assets.images.signpost.inactive.keyName,
Assets.images.signpost.active1.keyName,
Assets.images.signpost.active2.keyName,
Assets.images.signpost.active3.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('Signpost', () {
flameTester.test(
@ -27,8 +33,10 @@ void main() {
flameTester.testGameWidget(
'inactive sprite',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final signpost = Signpost();
await game.ensureAdd(signpost);
await tester.pump();
expect(
signpost.firstChild<SpriteGroupComponent>()!.current,
@ -48,9 +56,11 @@ void main() {
flameTester.testGameWidget(
'active1 sprite',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final signpost = Signpost();
await game.ensureAdd(signpost);
signpost.progress();
await tester.pump();
expect(
signpost.firstChild<SpriteGroupComponent>()!.current,
@ -70,11 +80,13 @@ void main() {
flameTester.testGameWidget(
'active2 sprite',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final signpost = Signpost();
await game.ensureAdd(signpost);
signpost
..progress()
..progress();
await tester.pump();
expect(
signpost.firstChild<SpriteGroupComponent>()!.current,
@ -94,12 +106,14 @@ void main() {
flameTester.testGameWidget(
'active3 sprite',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final signpost = Signpost();
await game.ensureAdd(signpost);
signpost
..progress()
..progress()
..progress();
await tester.pump();
expect(
signpost.firstChild<SpriteGroupComponent>()!.current,

@ -9,7 +9,15 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
final assets = [
Assets.images.sparky.bumper.a.active.keyName,
Assets.images.sparky.bumper.a.inactive.keyName,
Assets.images.sparky.bumper.b.active.keyName,
Assets.images.sparky.bumper.b.inactive.keyName,
Assets.images.sparky.bumper.c.active.keyName,
Assets.images.sparky.bumper.c.inactive.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('SparkyBumper', () {
flameTester.test('"a" loads correctly', (game) async {
@ -31,43 +39,30 @@ void main() {
expect(game.contains(bumper), isTrue);
});
flameTester.test('activate returns normally', (game) async {
flameTester.test('animate switches between on and off sprites',
(game) async {
final bumper = SparkyBumper.a();
await game.ensureAdd(bumper);
expect(bumper.activate, returnsNormally);
});
flameTester.test('deactivate returns normally', (game) async {
final bumper = SparkyBumper.a();
await game.ensureAdd(bumper);
expect(bumper.deactivate, returnsNormally);
});
final spriteGroupComponent = bumper.firstChild<SpriteGroupComponent>()!;
flameTester.test('changes sprite', (game) async {
final bumper = SparkyBumper.a();
await game.ensureAdd(bumper);
final spriteComponent = bumper.firstChild<SpriteComponent>()!;
final deactivatedSprite = spriteComponent.sprite;
bumper.activate();
expect(
spriteComponent.sprite,
isNot(equals(deactivatedSprite)),
spriteGroupComponent.current,
equals(SparkyBumperSpriteState.active),
);
final activatedSprite = spriteComponent.sprite;
bumper.deactivate();
final future = bumper.animate();
expect(
spriteComponent.sprite,
isNot(equals(activatedSprite)),
spriteGroupComponent.current,
equals(SparkyBumperSpriteState.inactive),
);
await future;
expect(
activatedSprite,
isNot(equals(deactivatedSprite)),
spriteGroupComponent.current,
equals(SparkyBumperSpriteState.active),
);
});
});

@ -13,13 +13,18 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballTestGame.new);
final assets = [
Assets.images.alienBumper.a.active.keyName,
Assets.images.alienBumper.a.inactive.keyName,
Assets.images.alienBumper.b.active.keyName,
Assets.images.alienBumper.b.inactive.keyName,
];
final flameTester = FlameTester(() => EmptyPinballTestGame(assets));
group('AlienZone', () {
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final alienZone = AlienZone();
await game.ensureAdd(alienZone);
@ -31,7 +36,6 @@ void main() {
flameTester.test(
'two AlienBumper',
(game) async {
await game.ready();
final alienZone = AlienZone();
await game.ensureAdd(alienZone);
@ -44,55 +48,40 @@ void main() {
});
group('bumpers', () {
late ControlledAlienBumper controlledAlienBumper;
late GameBloc gameBloc;
setUp(() {
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
flameTester.testGameWidget(
'activate when deactivated bumper is hit',
setUp: (game, tester) async {
controlledAlienBumper = ControlledAlienBumper.a();
await game.ensureAdd(controlledAlienBumper);
flameTester.test('call animate on contact', (game) async {
final contactCallback = AlienBumperBallContactCallback();
final bumper = MockAlienBumper();
final ball = MockBall();
controlledAlienBumper.controller.hit();
},
verify: (game, tester) async {
expect(controlledAlienBumper.controller.isActivated, isTrue);
},
);
when(bumper.animate).thenAnswer((_) async {});
flameTester.testGameWidget(
'deactivate when activated bumper is hit',
setUp: (game, tester) async {
controlledAlienBumper = ControlledAlienBumper.a();
await game.ensureAdd(controlledAlienBumper);
contactCallback.begin(bumper, ball, MockContact());
controlledAlienBumper.controller.hit();
controlledAlienBumper.controller.hit();
},
verify: (game, tester) async {
expect(controlledAlienBumper.controller.isActivated, isFalse);
},
);
verify(bumper.animate).called(1);
});
flameBlocTester.testGameWidget(
'add Scored event',
setUp: (game, tester) async {
final ball = Ball(baseColor: const Color(0xFF00FFFF));
final alienZone = AlienZone();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
await game.ensureAdd(alienZone);
await game.ensureAdd(ball);

@ -9,7 +9,19 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballTestGame.new);
final assets = [
Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
Assets.images.dash.bumper.a.active.keyName,
Assets.images.dash.bumper.a.inactive.keyName,
Assets.images.dash.bumper.b.active.keyName,
Assets.images.dash.bumper.b.inactive.keyName,
Assets.images.signpost.inactive.keyName,
Assets.images.signpost.active1.keyName,
Assets.images.signpost.active2.keyName,
Assets.images.signpost.active3.keyName,
];
final flameTester = FlameTester(() => EmptyPinballTestGame(assets));
group('Board', () {
flameTester.test(

@ -12,7 +12,19 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballTestGame.new);
final assets = [
Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
Assets.images.dash.bumper.a.active.keyName,
Assets.images.dash.bumper.a.inactive.keyName,
Assets.images.dash.bumper.b.active.keyName,
Assets.images.dash.bumper.b.inactive.keyName,
Assets.images.signpost.inactive.keyName,
Assets.images.signpost.active1.keyName,
Assets.images.signpost.active2.keyName,
Assets.images.signpost.active3.keyName,
];
final flameTester = FlameTester(() => EmptyPinballTestGame(assets));
group('FlutterForest', () {
flameTester.test(
@ -53,27 +65,14 @@ void main() {
);
flameTester.test(
'a BigDashNestBumper',
'three DashNestBumper',
(game) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect(
flutterForest.descendants().whereType<BigDashNestBumper>().length,
equals(1),
);
},
);
flameTester.test(
'two SmallDashNestBumper',
(game) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect(
flutterForest.descendants().whereType<SmallDashNestBumper>().length,
equals(2),
flutterForest.descendants().whereType<DashNestBumper>().length,
equals(3),
);
},
);
@ -88,13 +87,14 @@ void main() {
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
gameBuilder: () => EmptyPinballTestGame(assets),
blocBuilder: () {
gameBloc = MockGameBloc();
const state = GameState.initial();
whenListen(gameBloc, Stream.value(state), initialState: state);
return gameBloc;
},
assets: assets,
);
flameBlocTester.testGameWidget(

@ -13,13 +13,20 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballTestGame.new);
final assets = [
Assets.images.sparky.bumper.a.active.keyName,
Assets.images.sparky.bumper.a.inactive.keyName,
Assets.images.sparky.bumper.b.active.keyName,
Assets.images.sparky.bumper.b.inactive.keyName,
Assets.images.sparky.bumper.c.active.keyName,
Assets.images.sparky.bumper.c.inactive.keyName,
];
final flameTester = FlameTester(() => EmptyPinballTestGame(assets));
group('SparkyFireZone', () {
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final sparkyFireZone = SparkyFireZone();
await game.ensureAdd(sparkyFireZone);
@ -31,7 +38,6 @@ void main() {
flameTester.test(
'three SparkyBumper',
(game) async {
await game.ready();
final sparkyFireZone = SparkyFireZone();
await game.ensureAdd(sparkyFireZone);
@ -44,12 +50,9 @@ void main() {
});
group('bumpers', () {
late ControlledSparkyBumper controlledSparkyBumper;
late Ball ball;
late GameBloc gameBloc;
setUp(() {
ball = Ball(baseColor: const Color(0xFF00FFFF));
gameBloc = MockGameBloc();
whenListen(
gameBloc,
@ -58,41 +61,28 @@ void main() {
);
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
flameTester.testGameWidget(
'activate when deactivated bumper is hit',
setUp: (game, tester) async {
controlledSparkyBumper = ControlledSparkyBumper.a();
await game.ensureAdd(controlledSparkyBumper);
flameTester.test('call animate on contact', (game) async {
final contactCallback = SparkyBumperBallContactCallback();
final bumper = MockSparkyBumper();
final ball = MockBall();
controlledSparkyBumper.controller.hit();
},
verify: (game, tester) async {
expect(controlledSparkyBumper.controller.isActivated, isTrue);
},
);
when(bumper.animate).thenAnswer((_) async {});
flameTester.testGameWidget(
'deactivate when activated bumper is hit',
setUp: (game, tester) async {
controlledSparkyBumper = ControlledSparkyBumper.a();
await game.ensureAdd(controlledSparkyBumper);
contactCallback.begin(bumper, ball, MockContact());
controlledSparkyBumper.controller.hit();
controlledSparkyBumper.controller.hit();
},
verify: (game, tester) async {
expect(controlledSparkyBumper.controller.isActivated, isFalse);
},
);
verify(bumper.animate).called(1);
});
flameBlocTester.testGameWidget(
'add Scored event',
setUp: (game, tester) async {
final ball = Ball(baseColor: const Color(0xFF00FFFF));
final sparkyFireZone = SparkyFireZone();
await game.ensureAdd(sparkyFireZone);
await game.ensureAdd(ball);

@ -12,8 +12,30 @@ import '../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballTestGame.new);
final debugModeFlameTester = FlameTester(DebugPinballTestGame.new);
final assets = [
Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
Assets.images.dash.bumper.a.active.keyName,
Assets.images.dash.bumper.a.inactive.keyName,
Assets.images.dash.bumper.b.active.keyName,
Assets.images.dash.bumper.b.inactive.keyName,
Assets.images.signpost.inactive.keyName,
Assets.images.signpost.active1.keyName,
Assets.images.signpost.active2.keyName,
Assets.images.signpost.active3.keyName,
Assets.images.alienBumper.a.active.keyName,
Assets.images.alienBumper.a.inactive.keyName,
Assets.images.alienBumper.b.active.keyName,
Assets.images.alienBumper.b.inactive.keyName,
Assets.images.sparky.bumper.a.active.keyName,
Assets.images.sparky.bumper.a.inactive.keyName,
Assets.images.sparky.bumper.b.active.keyName,
Assets.images.sparky.bumper.b.inactive.keyName,
Assets.images.sparky.bumper.c.active.keyName,
Assets.images.sparky.bumper.c.inactive.keyName,
];
final flameTester = FlameTester(() => PinballTestGame(assets));
final debugModeFlameTester = FlameTester(() => DebugPinballTestGame(assets));
group('PinballGame', () {
// TODO(alestiago): test if [PinballGame] registers
@ -90,6 +112,7 @@ void main() {
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
// assets: assets,
);
flameBlocTester.testGameWidget(
@ -208,6 +231,7 @@ void main() {
FlameBlocTester<DebugPinballGame, GameBloc>(
gameBuilder: DebugPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
debugModeFlameBlocTester.testGameWidget(

@ -16,7 +16,8 @@ void main() {
late ThemeCubit themeCubit;
late GameBloc gameBloc;
setUp(() {
setUp(() async {
await Future.wait<void>(game.preLoadAssets());
themeCubit = MockThemeCubit();
gameBloc = MockGameBloc();

@ -1,16 +1,23 @@
import 'package:flame/src/game/flame_game.dart';
import 'package:flame/game.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
class FlameBlocTester<T extends FlameGame, B extends Bloc<dynamic, dynamic>>
extends FlameTester<T> {
FlameBlocTester({
required GameCreateFunction<T> gameBuilder,
required B Function() blocBuilder,
// TODO(allisonryan0002): find alternative for testGameWidget. Loading
// assets in onLoad fails because the game loads after
List<String>? assets,
List<RepositoryProvider> Function()? repositories,
}) : super(
gameBuilder,
pumpWidget: (gameWidget, tester) async {
if (assets != null) {
await Future.wait(assets.map(gameWidget.game.images.load));
}
await tester.pumpWidget(
BlocProvider.value(
value: blocBuilder(),

@ -82,3 +82,7 @@ class MockActiveOverlaysNotifier extends Mock
implements ActiveOverlaysNotifier {}
class MockGameFlowController extends Mock implements GameFlowController {}
class MockAlienBumper extends Mock implements AlienBumper {}
class MockSparkyBumper extends Mock implements SparkyBumper {}

@ -1,5 +1,7 @@
// ignore_for_file: must_call_super
import 'dart:async';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
@ -14,26 +16,53 @@ class TestGame extends Forge2DGame with FlameBloc {
}
class PinballTestGame extends PinballGame {
PinballTestGame()
: super(
PinballTestGame([List<String>? assets])
: _assets = assets,
super(
audio: MockPinballAudio(),
theme: const PinballTheme(
characterTheme: DashTheme(),
),
);
final List<String>? _assets;
@override
Future<void> onLoad() async {
if (_assets != null) {
await images.loadAll(_assets!);
}
await super.onLoad();
}
}
class DebugPinballTestGame extends DebugPinballGame {
DebugPinballTestGame()
: super(
DebugPinballTestGame([List<String>? assets])
: _assets = assets,
super(
audio: MockPinballAudio(),
theme: const PinballTheme(
characterTheme: DashTheme(),
),
);
final List<String>? _assets;
@override
Future<void> onLoad() async {
if (_assets != null) {
await images.loadAll(_assets!);
}
await super.onLoad();
}
}
class EmptyPinballTestGame extends PinballTestGame {
EmptyPinballTestGame([List<String>? assets]) : super(assets);
@override
Future<void> onLoad() async {}
Future<void> onLoad() async {
if (_assets != null) {
await images.loadAll(_assets!);
}
}
}

Loading…
Cancel
Save