feat: add alien bumper (#166)

* feat: added alien bumpers

* test: tests for alien bumpers

* feat: sandbox for alien bumpers

* refactor: changed alien bumper ellipses

* feat: added alien bumpers zone to game

* test: tests for alien zone

* refactor: final size and positions for alien bumpers

* feat: added new alien zone bumpers

* test: changed tests for alien

* chore: api doc

* refactor: alien sandbox traceable

* Update lib/game/components/alien_zone.dart

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* refactor: moved alien from board to pinball and fixed sprites

* test: fixed test for alien at pinball

* Update packages/pinball_components/sandbox/lib/stories/alien_bumper/alien_bumper_game.dart

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

* test: clean flamebloc test

* refactor: ControlledAlienBumper visible for testing

* chore: removed unused file

* test: fixed alien test in pinball

* refactor: refactored alien dashbook stories

* chore: analysis error

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>
Co-authored-by: Alejandro Santiago <dev@alestiago.com>
pull/186/head
Rui Miguel Alonso 3 years ago committed by GitHub
parent b424f0a008
commit aafc254ad3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,95 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/flame/flame.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template alien_zone}
/// Area positioned below [Spaceship] where the [Ball]
/// can bounce off [AlienBumper]s.
///
/// When a [Ball] hits [AlienBumper]s, they toggle between activated and
/// deactivated states.
/// {@endtemplate}
class AlienZone extends Component with HasGameRef<PinballGame> {
/// {@macro alien_zone}
AlienZone();
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(_ControlledAlienBumperBallContactCallback());
final lowerBumper = ControlledAlienBumper.a()
..initialPosition = Vector2(-32.52, 9.34);
final upperBumper = ControlledAlienBumper.b()
..initialPosition = Vector2(-22.89, 17.43);
await addAll([
lowerBumper,
upperBumper,
]);
}
}
/// {@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);
}
/// {@macro controlled_alien_bumper}
ControlledAlienBumper.b() : super.b() {
controller = _AlienBumperController(this);
}
@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> {
@override
void begin(
Controls<_AlienBumperController> controlledAlienBumper,
Ball _,
Contact __,
) {
controlledAlienBumper.controller.hit();
}
}

@ -1,3 +1,4 @@
export 'alien_zone.dart';
export 'board.dart';
export 'bonus_word.dart';
export 'camera_controller.dart';

@ -53,6 +53,7 @@ class PinballGame extends Forge2DGame
await add(plunger);
unawaited(add(Board()));
unawaited(add(AlienZone()));
unawaited(add(SparkyFireZone()));
unawaited(addFromBlueprint(Slingshots()));
unawaited(addFromBlueprint(DinoWalls()));

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

@ -10,6 +10,8 @@ import 'package:flutter/widgets.dart';
class $AssetsImagesGen {
const $AssetsImagesGen();
$AssetsImagesAlienBumperGen get alienBumper =>
const $AssetsImagesAlienBumperGen();
$AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen();
/// File path: assets/images/ball.png
@ -38,6 +40,13 @@ class $AssetsImagesGen {
$AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen();
}
class $AssetsImagesAlienBumperGen {
const $AssetsImagesAlienBumperGen();
$AssetsImagesAlienBumperAGen get a => const $AssetsImagesAlienBumperAGen();
$AssetsImagesAlienBumperBGen get b => const $AssetsImagesAlienBumperBGen();
}
class $AssetsImagesBackboardGen {
const $AssetsImagesBackboardGen();
@ -226,6 +235,30 @@ class $AssetsImagesSparkyGen {
const $AssetsImagesSparkyComputerGen();
}
class $AssetsImagesAlienBumperAGen {
const $AssetsImagesAlienBumperAGen();
/// File path: assets/images/alien_bumper/a/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/alien_bumper/a/active.png');
/// File path: assets/images/alien_bumper/a/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/alien_bumper/a/inactive.png');
}
class $AssetsImagesAlienBumperBGen {
const $AssetsImagesAlienBumperBGen();
/// File path: assets/images/alien_bumper/b/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/alien_bumper/b/active.png');
/// File path: assets/images/alien_bumper/b/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/alien_bumper/b/inactive.png');
}
class $AssetsImagesDashBumperGen {
const $AssetsImagesDashBumperGen();
@ -283,42 +316,6 @@ class $AssetsImagesSparkyComputerGen {
const AssetGenImage('assets/images/sparky/computer/top.png');
}
class $AssetsImagesSparkyBumperAGen {
const $AssetsImagesSparkyBumperAGen();
/// File path: assets/images/sparky/bumper/a/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/a/active.png');
/// File path: assets/images/sparky/bumper/a/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/a/inactive.png');
}
class $AssetsImagesSparkyBumperBGen {
const $AssetsImagesSparkyBumperBGen();
/// File path: assets/images/sparky/bumper/b/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/b/active.png');
/// File path: assets/images/sparky/bumper/b/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/b/inactive.png');
}
class $AssetsImagesSparkyBumperCGen {
const $AssetsImagesSparkyBumperCGen();
/// File path: assets/images/sparky/bumper/c/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/c/active.png');
/// File path: assets/images/sparky/bumper/c/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/c/inactive.png');
}
class $AssetsImagesDashBumperAGen {
const $AssetsImagesDashBumperAGen();
@ -355,6 +352,42 @@ class $AssetsImagesDashBumperMainGen {
const AssetGenImage('assets/images/dash/bumper/main/inactive.png');
}
class $AssetsImagesSparkyBumperAGen {
const $AssetsImagesSparkyBumperAGen();
/// File path: assets/images/sparky/bumper/a/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/a/active.png');
/// File path: assets/images/sparky/bumper/a/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/a/inactive.png');
}
class $AssetsImagesSparkyBumperBGen {
const $AssetsImagesSparkyBumperBGen();
/// File path: assets/images/sparky/bumper/b/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/b/active.png');
/// File path: assets/images/sparky/bumper/b/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/b/inactive.png');
}
class $AssetsImagesSparkyBumperCGen {
const $AssetsImagesSparkyBumperCGen();
/// File path: assets/images/sparky/bumper/c/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/c/active.png');
/// File path: assets/images/sparky/bumper/c/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/c/inactive.png');
}
class Assets {
Assets._();

@ -0,0 +1,109 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template alien_bumper}
/// Bumper for Alien area.
/// {@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,
}) : _majorRadius = majorRadius,
_minorRadius = minorRadius,
_activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath,
_spriteComponent = spriteComponent;
/// {@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),
),
);
/// {@macro alien_bumper}
AlienBumper.b()
: 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),
),
);
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() {
final shape = EllipseShape(
center: Vector2.zero(),
majorRadius: _majorRadius,
minorRadius: _minorRadius,
)..rotate(15.9 * math.pi / 180);
final fixtureDef = FixtureDef(shape)
..friction = 0
..restitution = 4;
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
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);
}
/// Activates the [AlienBumper].
void activate() {
_spriteComponent
..sprite = _activeSprite
..size = _activeSprite.originalSize / 10;
}
/// Deactivates the [AlienBumper].
void deactivate() {
_spriteComponent
..sprite = _inactiveSprite
..size = _inactiveSprite.originalSize / 10;
}
}

@ -1,3 +1,4 @@
export 'alien_bumper.dart';
export 'backboard.dart';
export 'ball.dart';
export 'baseboard.dart';

@ -52,6 +52,8 @@ flutter:
- assets/images/kicker/
- assets/images/plunger/
- assets/images/slingshot/
- assets/images/alien_bumper/a/
- assets/images/alien_bumper/b/
- assets/images/sparky/computer/
- assets/images/sparky/bumper/a/
- assets/images/sparky/bumper/b/

@ -24,6 +24,7 @@ void main() {
addPlungerStories(dashbook);
addSlingshotStories(dashbook);
addSparkyBumperStories(dashbook);
addAlienZoneStories(dashbook);
addZoomStories(dashbook);
addBoundariesStories(dashbook);
addGoogleWordStories(dashbook);

@ -0,0 +1,28 @@
import 'dart:async';
import 'package:flame/extensions.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class AlienBumperAGame extends BasicBallGame {
AlienBumperAGame() : super(color: const Color(0xFF0000FF));
static const info = '''
Shows how a AlienBumperA is rendered.
- Activate the "trace" parameter to overlay the body.
''';
@override
Future<void> onLoad() async {
await super.onLoad();
final center = screenToWorld(camera.viewport.canvasSize! / 2);
final alienBumperA = AlienBumper.a()
..initialPosition = Vector2(center.x - 20, center.y - 20)
..priority = 1;
await add(alienBumperA);
await traceAllBodies();
}
}

@ -0,0 +1,28 @@
import 'dart:async';
import 'package:flame/extensions.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class AlienBumperBGame extends BasicBallGame {
AlienBumperBGame() : super(color: const Color(0xFF0000FF));
static const info = '''
Shows how a AlienBumperB is rendered.
- Activate the "trace" parameter to overlay the body.
''';
@override
Future<void> onLoad() async {
await super.onLoad();
final center = screenToWorld(camera.viewport.canvasSize! / 2);
final alienBumperB = AlienBumper.b()
..initialPosition = Vector2(center.x - 10, center.y + 10)
..priority = 1;
await add(alienBumperB);
await traceAllBodies();
}
}

@ -0,0 +1,25 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/alien_zone/alien_bumper_a_game.dart';
import 'package:sandbox/stories/alien_zone/alien_bumper_b_game.dart';
void addAlienZoneStories(Dashbook dashbook) {
dashbook.storiesOf('Alien Zone')
..add(
'Alien Bumper A',
(context) => GameWidget(
game: AlienBumperAGame()..trace = context.boolProperty('Trace', true),
),
codeLink: buildSourceLink('alien_zone/alien_bumper_a.dart'),
info: AlienBumperAGame.info,
)
..add(
'Alien Bumper B',
(context) => GameWidget(
game: AlienBumperBGame()..trace = context.boolProperty('Trace', true),
),
codeLink: buildSourceLink('alien_zone/alien_bumper_b.dart'),
info: AlienBumperAGame.info,
);
}

@ -1,3 +1,4 @@
export 'alien_zone/stories.dart';
export 'ball/stories.dart';
export 'baseboard/stories.dart';
export 'boundaries/stories.dart';

@ -0,0 +1,68 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group('AlienBumper', () {
flameTester.test('"a" loads correctly', (game) async {
final bumper = AlienBumper.a();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"b" loads correctly', (game) async {
final bumper = AlienBumper.b();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('activate returns normally', (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);
});
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)),
);
final activatedSprite = spriteComponent.sprite;
bumper.deactivate();
expect(
spriteComponent.sprite,
isNot(equals(activatedSprite)),
);
expect(
activatedSprite,
isNot(equals(deactivatedSprite)),
);
});
});
}

@ -0,0 +1,115 @@
// ignore_for_file: cascade_invocations
import 'dart:ui';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new);
group('AlienZone', () {
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final alienZone = AlienZone();
await game.ensureAdd(alienZone);
expect(game.contains(alienZone), isTrue);
},
);
group('loads', () {
flameTester.test(
'two AlienBumper',
(game) async {
await game.ready();
final alienZone = AlienZone();
await game.ensureAdd(alienZone);
expect(
alienZone.descendants().whereType<AlienBumper>().length,
equals(2),
);
},
);
});
group('bumpers', () {
late ControlledAlienBumper controlledAlienBumper;
late GameBloc gameBloc;
setUp(() {
gameBloc = MockGameBloc();
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballGameTest.new,
blocBuilder: () => gameBloc,
);
flameTester.testGameWidget(
'activate when deactivated bumper is hit',
setUp: (game, tester) async {
controlledAlienBumper = ControlledAlienBumper.a();
await game.ensureAdd(controlledAlienBumper);
controlledAlienBumper.controller.hit();
},
verify: (game, tester) async {
expect(controlledAlienBumper.controller.isActivated, isTrue);
},
);
flameTester.testGameWidget(
'deactivate when activated bumper is hit',
setUp: (game, tester) async {
controlledAlienBumper = ControlledAlienBumper.a();
await game.ensureAdd(controlledAlienBumper);
controlledAlienBumper.controller.hit();
controlledAlienBumper.controller.hit();
},
verify: (game, tester) async {
expect(controlledAlienBumper.controller.isActivated, isFalse);
},
);
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);
game.addContactCallback(BallScorePointsCallback(game));
final bumpers = alienZone.descendants().whereType<ScorePoints>();
for (final bumper in bumpers) {
beginContact(game, bumper, ball);
verify(
() => gameBloc.add(
Scored(points: bumper.points),
),
).called(1);
}
},
);
});
});
}

@ -70,6 +70,14 @@ void main() {
},
);
flameTester.test(
'one AlienZone',
(game) async {
await game.ready();
expect(game.children.whereType<AlienZone>().length, equals(1));
},
);
group('controller', () {
// TODO(alestiago): Write test to be controller agnostic.
group('listenWhen', () {

Loading…
Cancel
Save