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

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

@ -5,14 +5,12 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template sparky_fire_zone} /// {@template sparky_fire_zone}
/// Area positioned at the top left of the [Board] where the [Ball] /// Area positioned at the top left of the [Board] where the [Ball]
/// can bounce off [SparkyBumper]s. /// can bounce off [SparkyBumper]s.
/// ///
/// When a [Ball] hits [SparkyBumper]s, they toggle between activated and /// When a [Ball] hits [SparkyBumper]s, the bumper animates.
/// deactivated states.
/// {@endtemplate} /// {@endtemplate}
class SparkyFireZone extends Component with HasGameRef<PinballGame> { class SparkyFireZone extends Component with HasGameRef<PinballGame> {
/// {@macro sparky_fire_zone} /// {@macro sparky_fire_zone}
@ -22,14 +20,14 @@ class SparkyFireZone extends Component with HasGameRef<PinballGame> {
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
gameRef.addContactCallback(_ControlledSparkyBumperBallContactCallback()); gameRef.addContactCallback(SparkyBumperBallContactCallback());
final lowerLeftBumper = ControlledSparkyBumper.a() final lowerLeftBumper = _SparkyBumper.a()
..initialPosition = Vector2(-23.15, -41.65); ..initialPosition = Vector2(-22.9, -41.65);
final upperLeftBumper = ControlledSparkyBumper.b() final upperLeftBumper = _SparkyBumper.b()
..initialPosition = Vector2(-21.25, -58.15); ..initialPosition = Vector2(-21.25, -57.9);
final rightBumper = ControlledSparkyBumper.c() final rightBumper = _SparkyBumper.c()
..initialPosition = Vector2(-3.56, -53.051); ..initialPosition = Vector2(-3.3, -52.55);
await addAll([ await addAll([
lowerLeftBumper, lowerLeftBumper,
@ -39,65 +37,29 @@ class SparkyFireZone extends Component with HasGameRef<PinballGame> {
} }
} }
/// {@template controlled_sparky_bumper} // TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
/// [SparkyBumper] with [_SparkyBumperController] attached. // ContactCallback process is enhanced.
/// {@endtemplate} class _SparkyBumper extends SparkyBumper with ScorePoints {
@visibleForTesting _SparkyBumper.a() : super.a();
class ControlledSparkyBumper extends SparkyBumper
with Controls<_SparkyBumperController>, ScorePoints {
///{@macro controlled_sparky_bumper}
ControlledSparkyBumper.a() : super.a() {
controller = _SparkyBumperController(this);
}
///{@macro controlled_sparky_bumper} _SparkyBumper.b() : super.b();
ControlledSparkyBumper.b() : super.b() {
controller = _SparkyBumperController(this);
}
///{@macro controlled_sparky_bumper} _SparkyBumper.c() : super.c();
ControlledSparkyBumper.c() : super.c() {
controller = _SparkyBumperController(this);
}
@override @override
int get points => 20; 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]. /// Listens when a [Ball] bounces bounces against a [SparkyBumper].
class _ControlledSparkyBumperBallContactCallback @visibleForTesting
extends ContactCallback<Controls<_SparkyBumperController>, Ball> { class SparkyBumperBallContactCallback
extends ContactCallback<SparkyBumper, Ball> {
@override @override
void begin( void begin(
Controls<_SparkyBumperController> controlledSparkyBumper, SparkyBumper sparkyBumper,
Ball _, Ball _,
Contact __, 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.main.keyName),
images.load(components.Assets.images.spaceship.rail.foreground.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.mouth.keyName),
images.load(components.Assets.images.chromeDino.head.keyName), images.load(components.Assets.images.chromeDino.head.keyName),
images.load(components.Assets.images.plunger.plunger.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/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template alien_bumper} /// {@template alien_bumper}
/// Bumper for Alien area. /// Bumper for area under the [Spaceship].
/// {@endtemplate} /// {@endtemplate}
// TODO(ruimiguel): refactor later to unify with DashBumpers.
class AlienBumper extends BodyComponent with InitialPosition { class AlienBumper extends BodyComponent with InitialPosition {
/// {@macro alien_bumper} /// {@macro alien_bumper}
AlienBumper._({ AlienBumper._({
required double majorRadius, required double majorRadius,
required double minorRadius, required double minorRadius,
required String activeAssetPath, required String onAssetPath,
required String inactiveAssetPath, required String offAssetPath,
required SpriteComponent spriteComponent,
}) : _majorRadius = majorRadius, }) : _majorRadius = majorRadius,
_minorRadius = minorRadius, _minorRadius = minorRadius,
_activeAssetPath = activeAssetPath, super(
_inactiveAssetPath = inactiveAssetPath, priority: RenderPriority.alienBumper,
_spriteComponent = spriteComponent, children: [
super(priority: RenderPriority.alienBumper); _AlienBumperSpriteGroupComponent(
onAssetPath: onAssetPath,
offAssetPath: offAssetPath,
),
],
) {
renderBody = false;
}
/// {@macro alien_bumper} /// {@macro alien_bumper}
AlienBumper.a() AlienBumper.a()
: this._( : this._(
majorRadius: 3.52, majorRadius: 3.52,
minorRadius: 2.97, minorRadius: 2.97,
activeAssetPath: Assets.images.alienBumper.a.active.keyName, onAssetPath: Assets.images.alienBumper.a.active.keyName,
inactiveAssetPath: Assets.images.alienBumper.a.inactive.keyName, offAssetPath: Assets.images.alienBumper.a.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.1),
),
); );
/// {@macro alien_bumper} /// {@macro alien_bumper}
@ -39,32 +43,12 @@ class AlienBumper extends BodyComponent with InitialPosition {
: this._( : this._(
majorRadius: 3.19, majorRadius: 3.19,
minorRadius: 2.79, minorRadius: 2.79,
activeAssetPath: Assets.images.alienBumper.b.active.keyName, onAssetPath: Assets.images.alienBumper.b.active.keyName,
inactiveAssetPath: Assets.images.alienBumper.b.inactive.keyName, offAssetPath: Assets.images.alienBumper.b.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.1),
),
); );
final double _majorRadius; final double _majorRadius;
final double _minorRadius; 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 @override
Body createBody() { Body createBody() {
@ -85,25 +69,53 @@ class AlienBumper extends BodyComponent with InitialPosition {
return world.createBody(bodyDef)..createFixture(fixtureDef); return world.createBody(bodyDef)..createFixture(fixtureDef);
} }
Future<void> _loadSprites() async { /// Animates the [AlienBumper].
// TODO(alestiago): I think ideally we would like to do: Future<void> animate() async {
// Sprite(path).load so we don't require to store the activeAssetPath and final spriteGroupComponent = firstChild<_AlienBumperSpriteGroupComponent>()
// the inactive assetPath. ?..current = AlienBumperSpriteState.inactive;
_inactiveSprite = await gameRef.loadSprite(_inactiveAssetPath); await Future<void>.delayed(const Duration(milliseconds: 50));
_activeSprite = await gameRef.loadSprite(_activeAssetPath); spriteGroupComponent?.current = AlienBumperSpriteState.active;
} }
}
/// Activates the [AlienBumper]. /// Indicates the [AlienBumper]'s current sprite state.
void activate() { @visibleForTesting
_spriteComponent enum AlienBumperSpriteState {
..sprite = _activeSprite /// A lit up bumper.
..size = _activeSprite.originalSize / 10; 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]. current = AlienBumperSpriteState.active;
void deactivate() { size = sprites[current]!.originalSize / 10;
_spriteComponent
..sprite = _inactiveSprite
..size = _inactiveSprite.originalSize / 10;
} }
} }

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

@ -2,139 +2,76 @@ import 'dart:math' as math;
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template dash_nest_bumper} /// {@template dash_nest_bumper}
/// Bumper with a nest appearance. /// Bumper with a nest appearance.
/// {@endtemplate} /// {@endtemplate}
abstract class DashNestBumper extends BodyComponent with InitialPosition { class DashNestBumper extends BodyComponent with InitialPosition {
/// {@macro dash_nest_bumper} /// {@macro dash_nest_bumper}
DashNestBumper._({ DashNestBumper._({
required double majorRadius,
required double minorRadius,
required String activeAssetPath, required String activeAssetPath,
required String inactiveAssetPath, required String inactiveAssetPath,
required SpriteComponent spriteComponent, required Vector2 spritePosition,
}) : _activeAssetPath = activeAssetPath, }) : _majorRadius = majorRadius,
_inactiveAssetPath = inactiveAssetPath, _minorRadius = minorRadius,
_spriteComponent = spriteComponent, super(
super(priority: RenderPriority.dashBumper); priority: RenderPriority.dashBumper,
children: [
final String _activeAssetPath; _DashNestBumperSpriteGroupComponent(
late final Sprite _activeSprite; activeAssetPath: activeAssetPath,
final String _inactiveAssetPath; inactiveAssetPath: inactiveAssetPath,
late final Sprite _inactiveSprite; position: spritePosition,
final SpriteComponent _spriteComponent; ),
],
Future<void> _loadSprites() async { ) {
// TODO(alestiago): I think ideally we would like to do: renderBody = false;
// 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;
} }
/// 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} /// {@macro dash_nest_bumper}
BigDashNestBumper() DashNestBumper.main()
: super._( : this._(
majorRadius: 5.1,
minorRadius: 3.75,
activeAssetPath: Assets.images.dash.bumper.main.active.keyName, activeAssetPath: Assets.images.dash.bumper.main.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName, inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName,
spriteComponent: SpriteComponent( spritePosition: Vector2(0, -0.3),
anchor: Anchor.center,
position: 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} /// {@macro dash_nest_bumper}
SmallDashNestBumper._({ DashNestBumper.a()
required String activeAssetPath,
required String inactiveAssetPath,
required SpriteComponent spriteComponent,
}) : super._(
activeAssetPath: activeAssetPath,
inactiveAssetPath: inactiveAssetPath,
spriteComponent: spriteComponent,
);
/// {@macro dash_nest_bumper}
SmallDashNestBumper.a()
: this._( : this._(
majorRadius: 3,
minorRadius: 2.5,
activeAssetPath: Assets.images.dash.bumper.a.active.keyName, activeAssetPath: Assets.images.dash.bumper.a.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName, inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName,
spriteComponent: SpriteComponent( spritePosition: Vector2(0.35, -1.2),
anchor: Anchor.center,
position: Vector2(0.35, -1.2),
),
); );
/// {@macro dash_nest_bumper} /// {@macro dash_nest_bumper}
SmallDashNestBumper.b() DashNestBumper.b()
: this._( : this._(
majorRadius: 3,
minorRadius: 2.5,
activeAssetPath: Assets.images.dash.bumper.b.active.keyName, activeAssetPath: Assets.images.dash.bumper.b.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName, inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName,
spriteComponent: SpriteComponent( spritePosition: Vector2(0.35, -1.2),
anchor: Anchor.center,
position: Vector2(0.35, -1.2),
),
); );
final double _majorRadius;
final double _minorRadius;
@override @override
Body createBody() { Body createBody() {
final shape = EllipseShape( final shape = EllipseShape(
center: Vector2.zero(), center: Vector2.zero(),
majorRadius: 3, majorRadius: _majorRadius,
minorRadius: 2.25, minorRadius: _minorRadius,
)..rotate(math.pi / 2); )..rotate(math.pi / 1.9);
final fixtureDef = FixtureDef( final fixtureDef = FixtureDef(shape, restitution: 4);
shape,
restitution: 4,
);
final bodyDef = BodyDef( final bodyDef = BodyDef(
position: initialPosition, position: initialPosition,
userData: this, userData: this,
@ -142,4 +79,58 @@ class SmallDashNestBumper extends DashNestBumper {
return world.createBody(bodyDef)..createFixture(fixtureDef); 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>{}; final sprites = <SignpostSpriteState, Sprite>{};
this.sprites = sprites; this.sprites = sprites;
for (final spriteState in SignpostSpriteState.values) { for (final spriteState in SignpostSpriteState.values) {
// TODO(allisonryan0002): Support caching sprites[spriteState] = Sprite(
// https://github.com/VGVentures/pinball/pull/204 gameRef.images.fromCache(spriteState.path),
// sprites[spriteState] = Sprite( );
// gameRef.images.fromCache(spriteState.path),
// );
sprites[spriteState] = await gameRef.loadSprite(spriteState.path);
} }
current = SignpostSpriteState.inactive; current = SignpostSpriteState.inactive;

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

@ -17,6 +17,11 @@ class AlienBumperAGame extends BasicBallGame {
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); 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 center = screenToWorld(camera.viewport.canvasSize! / 2);
final alienBumperA = AlienBumper.a() final alienBumperA = AlienBumper.a()
..initialPosition = Vector2(center.x - 20, center.y - 20) ..initialPosition = Vector2(center.x - 20, center.y - 20)

@ -17,6 +17,11 @@ class AlienBumperBGame extends BasicBallGame {
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); 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 center = screenToWorld(camera.viewport.canvasSize! / 2);
final alienBumperB = AlienBumper.b() final alienBumperB = AlienBumper.b()
..initialPosition = Vector2(center.x - 10, center.y + 10) ..initialPosition = Vector2(center.x - 10, center.y + 10)

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

@ -17,8 +17,15 @@ class SignpostGame extends BasicBallGame with Traceable, TapDetector {
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); 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()); camera.followVector2(Vector2.zero());
await add(Signpost()..priority = 1); await add(Signpost());
await traceAllBodies(); await traceAllBodies();
} }

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

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

@ -16,12 +16,21 @@ class SparkyBumperGame extends BasicBallGame with Traceable {
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); 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 center = screenToWorld(camera.viewport.canvasSize! / 2);
final sparkyBumperA = SparkyBumper.a() final sparkyBumperA = SparkyBumper.a()
..initialPosition = Vector2(center.x - 20, center.y - 20) ..initialPosition = Vector2(center.x - 20, center.y + 20)
..priority = 1; ..priority = 1;
final sparkyBumperB = SparkyBumper.b() final sparkyBumperB = SparkyBumper.b()
..initialPosition = Vector2(center.x - 10, center.y + 10) ..initialPosition = Vector2(center.x - 10, center.y - 10)
..priority = 1; ..priority = 1;
final sparkyBumperC = SparkyBumper.c() final sparkyBumperC = SparkyBumper.c()
..initialPosition = Vector2(center.x + 20, center.y) ..initialPosition = Vector2(center.x + 20, center.y)

@ -2,9 +2,19 @@ import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
class TestGame extends Forge2DGame { class TestGame extends Forge2DGame {
TestGame() { TestGame([List<String>? assets]) : _assets = assets {
images.prefix = ''; 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 {} class KeyboardTestGame extends TestGame with HasKeyboardHandlerComponents {}

@ -9,7 +9,13 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); 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', () { group('AlienBumper', () {
flameTester.test('"a" loads correctly', (game) async { flameTester.test('"a" loads correctly', (game) async {
@ -25,43 +31,30 @@ void main() {
expect(game.contains(bumper), isTrue); 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(); final bumper = AlienBumper.a();
await game.ensureAdd(bumper); await game.ensureAdd(bumper);
expect(bumper.activate, returnsNormally); final spriteGroupComponent = bumper.firstChild<SpriteGroupComponent>()!;
});
flameTester.test('deactivate returns normally', (game) async {
final bumper = AlienBumper.a();
await game.ensureAdd(bumper);
expect(bumper.deactivate, returnsNormally);
});
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( expect(
spriteComponent.sprite, spriteGroupComponent.current,
isNot(equals(deactivatedSprite)), equals(AlienBumperSpriteState.active),
); );
final activatedSprite = spriteComponent.sprite; final future = bumper.animate();
bumper.deactivate();
expect( expect(
spriteComponent.sprite, spriteGroupComponent.current,
isNot(equals(activatedSprite)), equals(AlienBumperSpriteState.inactive),
); );
await future;
expect( expect(
activatedSprite, spriteGroupComponent.current,
isNot(equals(deactivatedSprite)), equals(AlienBumperSpriteState.active),
); );
}); });
}); });

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

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

@ -9,7 +9,15 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); 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', () { group('SparkyBumper', () {
flameTester.test('"a" loads correctly', (game) async { flameTester.test('"a" loads correctly', (game) async {
@ -31,43 +39,30 @@ void main() {
expect(game.contains(bumper), isTrue); 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(); final bumper = SparkyBumper.a();
await game.ensureAdd(bumper); await game.ensureAdd(bumper);
expect(bumper.activate, returnsNormally); final spriteGroupComponent = bumper.firstChild<SpriteGroupComponent>()!;
});
flameTester.test('deactivate returns normally', (game) async {
final bumper = SparkyBumper.a();
await game.ensureAdd(bumper);
expect(bumper.deactivate, returnsNormally);
});
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( expect(
spriteComponent.sprite, spriteGroupComponent.current,
isNot(equals(deactivatedSprite)), equals(SparkyBumperSpriteState.active),
); );
final activatedSprite = spriteComponent.sprite; final future = bumper.animate();
bumper.deactivate();
expect( expect(
spriteComponent.sprite, spriteGroupComponent.current,
isNot(equals(activatedSprite)), equals(SparkyBumperSpriteState.inactive),
); );
await future;
expect( expect(
activatedSprite, spriteGroupComponent.current,
isNot(equals(deactivatedSprite)), equals(SparkyBumperSpriteState.active),
); );
}); });
}); });

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

@ -9,7 +9,19 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); 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', () { group('Board', () {
flameTester.test( flameTester.test(

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

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

@ -12,8 +12,30 @@ import '../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballTestGame.new); final assets = [
final debugModeFlameTester = FlameTester(DebugPinballTestGame.new); 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', () { group('PinballGame', () {
// TODO(alestiago): test if [PinballGame] registers // TODO(alestiago): test if [PinballGame] registers
@ -90,6 +112,7 @@ void main() {
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>( final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new, gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc, blocBuilder: () => gameBloc,
// assets: assets,
); );
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(
@ -208,6 +231,7 @@ void main() {
FlameBlocTester<DebugPinballGame, GameBloc>( FlameBlocTester<DebugPinballGame, GameBloc>(
gameBuilder: DebugPinballTestGame.new, gameBuilder: DebugPinballTestGame.new,
blocBuilder: () => gameBloc, blocBuilder: () => gameBloc,
assets: assets,
); );
debugModeFlameBlocTester.testGameWidget( debugModeFlameBlocTester.testGameWidget(

@ -16,7 +16,8 @@ void main() {
late ThemeCubit themeCubit; late ThemeCubit themeCubit;
late GameBloc gameBloc; late GameBloc gameBloc;
setUp(() { setUp(() async {
await Future.wait<void>(game.preLoadAssets());
themeCubit = MockThemeCubit(); themeCubit = MockThemeCubit();
gameBloc = MockGameBloc(); 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:flame_test/flame_test.dart';
import 'package:flutter_bloc/flutter_bloc.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>> class FlameBlocTester<T extends FlameGame, B extends Bloc<dynamic, dynamic>>
extends FlameTester<T> { extends FlameTester<T> {
FlameBlocTester({ FlameBlocTester({
required GameCreateFunction<T> gameBuilder, required GameCreateFunction<T> gameBuilder,
required B Function() blocBuilder, 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, List<RepositoryProvider> Function()? repositories,
}) : super( }) : super(
gameBuilder, gameBuilder,
pumpWidget: (gameWidget, tester) async { pumpWidget: (gameWidget, tester) async {
if (assets != null) {
await Future.wait(assets.map(gameWidget.game.images.load));
}
await tester.pumpWidget( await tester.pumpWidget(
BlocProvider.value( BlocProvider.value(
value: blocBuilder(), value: blocBuilder(),

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

Loading…
Cancel
Save