feat: add sparky animatronic (#194)

* feat: add sparky animatronic

* refactor: move computer into sparky fire zone

* test: sparky animatronic and controller

* chore: removed old test

* test: remove test for sparky fire zone component

* refactor: use onComplete

* chore: included SparkyTurboChargeSensor

* feat: specified priority

* fix: specified render priority

* refactor: spacing changes

* refactor: used children param

* feat: made SparkyFireZone a Blueprint

* feat: updated assets

* refactor: moved SparkyComputerSensor

* test: fix contact callback tests

* test: updated SparkyComputer loading test

* test: updated golden animatronic tests

* feat: modified turboCharge duration

* fix: included missing cached image

* fix: coverage

* refactor: removed unused HasGameRef mixin

* fix: included missing plunger assets

* Update test/game/components/sparky_fire_zone_test.dart

* fix: unused import

Co-authored-by: alestiago <dev@alestiago.com>
pull/228/head
Allison Ryan 3 years ago committed by GitHub
parent ad536bba17
commit 7cc41231a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,7 +4,6 @@ export 'camera_controller.dart';
export 'controlled_ball.dart';
export 'controlled_flipper.dart';
export 'controlled_plunger.dart';
export 'controlled_sparky_computer.dart';
export 'flutter_forest.dart';
export 'game_flow_controller.dart';
export 'google_word.dart';

@ -62,10 +62,13 @@ class BallController extends ComponentController<Ball>
Future<void> turboCharge() async {
gameRef.read<GameBloc>().add(const SparkyTurboChargeActivated());
// TODO(allisonryan0002): adjust delay to match animation duration once
// given animations.
component.stop();
await Future<void>.delayed(const Duration(seconds: 1));
// TODO(alestiago): Refactor this hard coded duration once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1564
await Future<void>.delayed(
const Duration(milliseconds: 2583),
);
component.resume();
await component.boost(Vector2(40, 110));
}

@ -1,52 +0,0 @@
// 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/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template controlled_sparky_computer}
/// [SparkyComputer] with a [SparkyComputerController] attached.
/// {@endtemplate}
class ControlledSparkyComputer extends SparkyComputer
with Controls<SparkyComputerController>, HasGameRef<Forge2DGame> {
/// {@macro controlled_sparky_computer}
ControlledSparkyComputer() : super() {
controller = SparkyComputerController(this);
}
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(SparkyComputerSensorBallContactCallback());
}
}
/// {@template sparky_computer_controller}
/// Controller attached to a [SparkyComputer] that handles its game related
/// logic.
/// {@endtemplate}
// TODO(allisonryan0002): listen for turbo charge game bonus and animate Sparky.
class SparkyComputerController
extends ComponentController<ControlledSparkyComputer> {
/// {@macro sparky_computer_controller}
SparkyComputerController(ControlledSparkyComputer controlledComputer)
: super(controlledComputer);
}
/// {@template sparky_computer_sensor_ball_contact_callback}
/// Turbo charges the [Ball] when it enters the [SparkyComputer]
/// {@endtemplate}
@visibleForTesting
class SparkyComputerSensorBallContactCallback
extends ContactCallback<SparkyComputerSensor, ControlledBall> {
/// {@macro sparky_computer_sensor_ball_contact_callback}
SparkyComputerSensorBallContactCallback();
@override
void begin(_, ControlledBall ball, __) {
ball.controller.turboCharge();
}
}

@ -1,10 +1,10 @@
// 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/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]
@ -12,29 +12,21 @@ import 'package:pinball_components/pinball_components.dart';
///
/// When a [Ball] hits [SparkyBumper]s, the bumper animates.
/// {@endtemplate}
class SparkyFireZone extends Component with HasGameRef<PinballGame> {
class SparkyFireZone extends Blueprint {
/// {@macro sparky_fire_zone}
SparkyFireZone();
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(SparkyBumperBallContactCallback());
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,
upperLeftBumper,
rightBumper,
]);
}
SparkyFireZone()
: super(
components: [
_SparkyBumper.a()..initialPosition = Vector2(-22.9, -41.65),
_SparkyBumper.b()..initialPosition = Vector2(-21.25, -57.9),
_SparkyBumper.c()..initialPosition = Vector2(-3.3, -52.55),
SparkyComputerSensor()..initialPosition = Vector2(-13, -49.8),
SparkyAnimatronic()..position = Vector2(-13.8, -58.2),
],
blueprints: [
SparkyComputer(),
],
);
}
// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
@ -48,6 +40,14 @@ class _SparkyBumper extends SparkyBumper with ScorePoints {
@override
int get points => 20;
@override
Future<void> onLoad() async {
await super.onLoad();
// TODO(alestiago): Revisit once this has been merged:
// https://github.com/flame-engine/flame/pull/1547
gameRef.addContactCallback(SparkyBumperBallContactCallback());
}
}
/// Listens when a [Ball] bounces bounces against a [SparkyBumper].
@ -63,3 +63,48 @@ class SparkyBumperBallContactCallback
sparkyBumper.animate();
}
}
/// {@template sparky_computer_sensor}
/// Small sensor body used to detect when a ball has entered the
/// [SparkyComputer].
/// {@endtemplate}
// TODO(alestiago): Revisit once this has been merged:
// https://github.com/flame-engine/flame/pull/1547
class SparkyComputerSensor extends BodyComponent with InitialPosition {
/// {@macro sparky_computer_sensor}
SparkyComputerSensor() {
renderBody = false;
}
@override
Body createBody() {
final shape = CircleShape()..radius = 0.1;
final fixtureDef = FixtureDef(shape, isSensor: true);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
@override
Future<void> onLoad() async {
await super.onLoad();
// TODO(alestiago): Revisit once this has been merged:
// https://github.com/flame-engine/flame/pull/1547
gameRef.addContactCallback(SparkyComputerSensorBallContactCallback());
}
}
@visibleForTesting
// TODO(alestiago): Revisit once this has been merged:
// https://github.com/flame-engine/flame/pull/1547
// ignore: public_member_api_docs
class SparkyComputerSensorBallContactCallback
extends ContactCallback<SparkyComputerSensor, ControlledBall> {
@override
void begin(_, ControlledBall controlledBall, __) {
controlledBall.controller.turboCharge();
controlledBall.gameRef.firstChild<SparkyAnimatronic>()?.playing = true;
}
}

@ -43,6 +43,8 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.dash.bumper.b.inactive.keyName),
images.load(components.Assets.images.dash.bumper.main.active.keyName),
images.load(components.Assets.images.dash.bumper.main.inactive.keyName),
images.load(components.Assets.images.plunger.plunger.keyName),
images.load(components.Assets.images.plunger.rocket.keyName),
images.load(components.Assets.images.boundary.bottom.keyName),
images.load(components.Assets.images.boundary.outer.keyName),
images.load(components.Assets.images.spaceship.saucer.keyName),
@ -80,12 +82,11 @@ extension PinballGameAssetsX on PinballGame {
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),
images.load(components.Assets.images.plunger.rocket.keyName),
images.load(components.Assets.images.sparky.computer.base.keyName),
images.load(components.Assets.images.sparky.computer.top.keyName),
images.load(components.Assets.images.sparky.bumper.a.active.keyName),
images.load(components.Assets.images.sparky.computer.base.keyName),
images.load(components.Assets.images.sparky.animatronic.keyName),
images.load(components.Assets.images.sparky.bumper.a.inactive.keyName),
images.load(components.Assets.images.sparky.bumper.a.active.keyName),
images.load(components.Assets.images.sparky.bumper.b.active.keyName),
images.load(components.Assets.images.sparky.bumper.b.inactive.keyName),
images.load(components.Assets.images.sparky.bumper.c.active.keyName),

@ -49,13 +49,13 @@ class PinballGame extends Forge2DGame
// TODO(allisonryan0002): banish Wall and Board classes in later PR.
await add(BottomWall());
unawaited(addFromBlueprint(Boundaries()));
unawaited(addFromBlueprint(ControlledSparkyComputer()));
unawaited(addFromBlueprint(LaunchRamp()));
final launcher = Launcher();
unawaited(addFromBlueprint(launcher));
unawaited(add(Board()));
unawaited(add(AlienZone()));
unawaited(add(SparkyFireZone()));
await addFromBlueprint(SparkyFireZone());
unawaited(addFromBlueprint(Slingshots()));
unawaited(addFromBlueprint(DinoWalls()));
unawaited(_addBonusWord());

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 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: 4.5 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

@ -257,6 +257,8 @@ class $AssetsImagesSpaceshipGen {
class $AssetsImagesSparkyGen {
const $AssetsImagesSparkyGen();
AssetGenImage get animatronic =>
const AssetGenImage('assets/images/sparky/animatronic.png');
$AssetsImagesSparkyBumperGen get bumper =>
const $AssetsImagesSparkyBumperGen();
$AssetsImagesSparkyComputerGen get computer =>

@ -29,5 +29,6 @@ export 'slingshot.dart';
export 'spaceship.dart';
export 'spaceship_rail.dart';
export 'spaceship_ramp.dart';
export 'sparky_animatronic.dart';
export 'sparky_bumper.dart';
export 'sparky_computer.dart';

@ -38,17 +38,9 @@ class DashAnimatronic extends SpriteAnimationComponent with HasGameRef {
textureSize: textureSize,
loop: false,
),
);
}
@override
void update(double dt) {
super.update(dt);
if (animation != null) {
if (animation!.isLastFrame) {
animation!.reset();
)..onComplete = () {
animation?.reset();
playing = false;
}
}
};
}
}

@ -0,0 +1,46 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template sparky_animatronic}
/// Animated Sparky that sits on top of the [SparkyComputer].
/// {@endtemplate}
class SparkyAnimatronic extends SpriteAnimationComponent with HasGameRef {
/// {@macro sparky_animatronic}
SparkyAnimatronic()
: super(
anchor: Anchor.center,
playing: false,
priority: RenderPriority.sparkyAnimatronic,
);
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = gameRef.images.fromCache(
Assets.images.sparky.animatronic.keyName,
);
const amountPerRow = 9;
const amountPerColumn = 7;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: (amountPerRow * amountPerColumn) - 1,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
loop: false,
),
)..onComplete = () {
animation?.reset();
playing = false;
};
}
}

@ -7,9 +7,6 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@template sparky_computer}
/// A computer owned by Sparky.
///
/// Register a [ContactCallback] for [SparkyComputerSensor] to listen when
/// something enters the [SparkyComputer].
/// {@endtemplate}
class SparkyComputer extends Blueprint {
/// {@macro sparky_computer}
@ -18,7 +15,6 @@ class SparkyComputer extends Blueprint {
components: [
_ComputerBase(),
_ComputerTopSpriteComponent(),
SparkyComputerSensor(),
],
);
}
@ -104,24 +100,3 @@ class _ComputerTopSpriteComponent extends SpriteComponent with HasGameRef {
size = sprite.originalSize / 10;
}
}
/// {@template sparky_computer_sensor}
/// Small sensor body used to detect when a ball has entered the
/// [SparkyComputer].
/// {@endtemplate}
class SparkyComputerSensor extends BodyComponent with InitialPosition {
/// {@macro sparky_computer_sensor}
SparkyComputerSensor() {
renderBody = false;
}
@override
Body createBody() {
final shape = CircleShape()..radius = 0.1;
final fixtureDef = FixtureDef(shape, isSensor: true);
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -61,6 +61,7 @@ flutter:
- assets/images/slingshot/
- assets/images/alien_bumper/a/
- assets/images/alien_bumper/b/
- assets/images/sparky/
- assets/images/sparky/computer/
- assets/images/sparky/bumper/a/
- assets/images/sparky/bumper/b/

@ -22,7 +22,9 @@ void main() {
await tester.pump();
},
verify: (game, tester) async {
const animationDuration = 3.25;
final animationDuration =
game.firstChild<DashAnimatronic>()!.animation!.totalDuration();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/dash_animatronic/start.png'),
@ -60,8 +62,7 @@ void main() {
await game.ensureAdd(dashAnimatronic);
dashAnimatronic.playing = true;
dashAnimatronic.animation?.setToLast();
game.update(1);
game.update(4);
expect(dashAnimatronic.playing, isFalse);
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

@ -0,0 +1,76 @@
// ignore_for_file: cascade_invocations
import 'package:flame/extensions.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();
group('SparkyAnimatronic', () {
final asset = Assets.images.sparky.animatronic.keyName;
final flameTester = FlameTester(() => TestGame([asset]));
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.images.load(asset);
await game.ensureAdd(SparkyAnimatronic()..playing = true);
await tester.pump();
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
final animationDuration =
game.firstChild<SparkyAnimatronic>()!.animation!.totalDuration();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/sparky_animatronic/start.png'),
);
game.update(animationDuration * 0.25);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/sparky_animatronic/middle.png'),
);
game.update(animationDuration * 0.75);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/sparky_animatronic/end.png'),
);
},
);
flameTester.test(
'loads correctly',
(game) async {
final sparkyAnimatronic = SparkyAnimatronic();
await game.ensureAdd(sparkyAnimatronic);
expect(game.contains(sparkyAnimatronic), isTrue);
},
);
flameTester.test(
'stops animating after animation completes',
(game) async {
final sparkyAnimatronic = SparkyAnimatronic();
await game.ensureAdd(sparkyAnimatronic);
sparkyAnimatronic.playing = true;
final animationDuration =
game.firstChild<SparkyAnimatronic>()!.animation!.totalDuration();
game.update(animationDuration);
expect(sparkyAnimatronic.playing, isFalse);
},
);
});
}

@ -1,38 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
group('ControlledSparkyComputer', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballTestGame.new);
flameTester.test('loads correctly', (game) async {
final sparkyComputer = ControlledSparkyComputer();
await game.ensureAdd(sparkyComputer);
expect(game.children, contains(sparkyComputer));
});
flameTester.testGameWidget(
'SparkyTurboChargeSensorBallContactCallback turbo charges the ball',
setUp: (game, tester) async {
final contackCallback = SparkyComputerSensorBallContactCallback();
final sparkyTurboChargeSensor = MockSparkyComputerSensor();
final ball = MockControlledBall();
final controller = MockBallController();
when(() => ball.controller).thenReturn(controller);
when(controller.turboCharge).thenAnswer((_) async {});
contackCallback.begin(sparkyTurboChargeSensor, ball, MockContact());
verify(() => ball.controller.turboCharge()).called(1);
},
);
});
}

@ -8,6 +8,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import '../../helpers/helpers.dart';
@ -20,29 +21,50 @@ void main() {
Assets.images.sparky.bumper.b.inactive.keyName,
Assets.images.sparky.bumper.c.active.keyName,
Assets.images.sparky.bumper.c.inactive.keyName,
Assets.images.sparky.animatronic.keyName,
];
final flameTester = FlameTester(() => EmptyPinballTestGame(assets));
group('SparkyFireZone', () {
flameTester.test('loads correctly', (game) async {
final sparkyFireZone = SparkyFireZone();
await game.ensureAdd(sparkyFireZone);
});
group('loads', () {
flameTester.test(
'loads correctly',
'a SparkyComputer',
(game) async {
expect(
SparkyFireZone().blueprints.whereType<SparkyComputer>().single,
isNotNull,
);
},
);
flameTester.test(
'a SparkyAnimatronic',
(game) async {
final sparkyFireZone = SparkyFireZone();
await game.ensureAdd(sparkyFireZone);
await game.addFromBlueprint(sparkyFireZone);
await game.ready();
expect(game.contains(sparkyFireZone), isTrue);
expect(
game.descendants().whereType<SparkyAnimatronic>().single,
isNotNull,
);
},
);
group('loads', () {
flameTester.test(
'three SparkyBumper',
(game) async {
final sparkyFireZone = SparkyFireZone();
await game.ensureAdd(sparkyFireZone);
await game.addFromBlueprint(sparkyFireZone);
await game.ready();
expect(
sparkyFireZone.descendants().whereType<SparkyBumper>().length,
game.descendants().whereType<SparkyBumper>().length,
equals(3),
);
},
@ -84,11 +106,11 @@ void main() {
setUp: (game, tester) async {
final ball = Ball(baseColor: const Color(0xFF00FFFF));
final sparkyFireZone = SparkyFireZone();
await game.ensureAdd(sparkyFireZone);
await game.addFromBlueprint(sparkyFireZone);
await game.ensureAdd(ball);
game.addContactCallback(BallScorePointsCallback(game));
final bumpers = sparkyFireZone.descendants().whereType<ScorePoints>();
final bumpers = sparkyFireZone.components.whereType<ScorePoints>();
for (final bumper in bumpers) {
beginContact(game, bumper, ball);
@ -102,4 +124,40 @@ void main() {
);
});
});
group('SparkyTurboChargeSensorBallContactCallback', () {
flameTester.test('calls turboCharge', (game) async {
final callback = SparkyComputerSensorBallContactCallback();
final ball = MockControlledBall();
final controller = MockBallController();
when(() => ball.controller).thenReturn(controller);
when(() => ball.gameRef).thenReturn(game);
when(controller.turboCharge).thenAnswer((_) async {});
callback.begin(MockSparkyComputerSensor(), ball, MockContact());
verify(() => ball.controller.turboCharge()).called(1);
});
flameTester.test('plays SparkyAnimatronic', (game) async {
final callback = SparkyComputerSensorBallContactCallback();
final ball = MockControlledBall();
final controller = MockBallController();
when(() => ball.controller).thenReturn(controller);
when(() => ball.gameRef).thenReturn(game);
when(controller.turboCharge).thenAnswer((_) async {});
final sparkyFireZone = SparkyFireZone();
await game.addFromBlueprint(sparkyFireZone);
await game.ready();
final sparkyAnimatronic =
sparkyFireZone.components.whereType<SparkyAnimatronic>().single;
expect(sparkyAnimatronic.playing, isFalse);
callback.begin(MockSparkyComputerSensor(), ball, MockContact());
expect(sparkyAnimatronic.playing, isTrue);
});
});
}

@ -34,6 +34,7 @@ void main() {
Assets.images.sparky.bumper.b.inactive.keyName,
Assets.images.sparky.bumper.c.active.keyName,
Assets.images.sparky.bumper.c.inactive.keyName,
Assets.images.sparky.animatronic.keyName,
Assets.images.spaceship.ramp.boardOpening.keyName,
Assets.images.spaceship.ramp.railingForeground.keyName,
Assets.images.spaceship.ramp.railingBackground.keyName,
@ -92,14 +93,6 @@ void main() {
);
});
flameTester.test(
'one SparkyFireZone',
(game) async {
await game.ready();
expect(game.children.whereType<SparkyFireZone>().length, equals(1));
},
);
flameTester.test(
'one AlienZone',
(game) async {

Loading…
Cancel
Save