diff --git a/lib/game/components/alien_zone/alien_bumper/behaviors/alien_bumper_contact_behavior.dart b/lib/game/components/alien_zone/alien_bumper/behaviors/alien_bumper_contact_behavior.dart new file mode 100644 index 00000000..80187f40 --- /dev/null +++ b/lib/game/components/alien_zone/alien_bumper/behaviors/alien_bumper_contact_behavior.dart @@ -0,0 +1,36 @@ +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class AlienBumperContactBehavior extends Component with ContactCallbacks { + @override + Future onLoad() async { + await super.onLoad(); + + // TODO(alestiago): Refactor once the following is merged: + // https://github.com/flame-engine/flame/pull/1566 + final parent = this.parent; + if (parent is! AlienBumper) return; + + final userData = parent.body.userData; + if (userData is ContactCallbacksNotifer) { + userData.addCallback(this); + } else { + parent.body.userData = this; + } + } + + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + + // TODO(alestiago): Refactor once the following is merged: + // https://github.com/flame-engine/flame/pull/1566 + final parent = this.parent; + if (parent is! AlienBumper) return; + + parent.state = AlienBumperState.inactive; + } +} diff --git a/lib/game/components/alien_zone/alien_bumper/behaviors/alien_bumper_sprite_behavior.dart b/lib/game/components/alien_zone/alien_bumper/behaviors/alien_bumper_sprite_behavior.dart new file mode 100644 index 00000000..011872b0 --- /dev/null +++ b/lib/game/components/alien_zone/alien_bumper/behaviors/alien_bumper_sprite_behavior.dart @@ -0,0 +1,51 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template alien_bumper_sprite_behavior} +/// +/// {@endtemplate} +class AlienBumperSpriteBehavior extends TimerComponent { + /// {@macro alien_bumper_sprite_behavior} + AlienBumperSpriteBehavior() + : super( + period: 0.05, + removeOnFinish: false, + ); + + void _onNewState(AlienBumperState state) { + switch (state) { + case AlienBumperState.active: + timer.stop(); + break; + case AlienBumperState.inactive: + timer + ..reset() + ..start(); + break; + } + } + + @override + Future onLoad() async { + await super.onLoad(); + + // TODO(alestiago): Refactor once the following is merged: + // https://github.com/flame-engine/flame/pull/1566 + final parent = this.parent; + if (parent is! AlienBumper) return; + + timer.stop(); + parent.stream.listen(_onNewState); + } + + @override + void onTick() { + super.onTick(); + // TODO(alestiago): Refactor once the following is merged: + // https://github.com/flame-engine/flame/pull/1566 + final parent = this.parent; + if (parent is! AlienBumper) return; + + parent.state = AlienBumperState.active; + } +} diff --git a/lib/game/components/alien_zone/alien_bumper/behaviors/behaviors.dart b/lib/game/components/alien_zone/alien_bumper/behaviors/behaviors.dart new file mode 100644 index 00000000..d17b3b0e --- /dev/null +++ b/lib/game/components/alien_zone/alien_bumper/behaviors/behaviors.dart @@ -0,0 +1,2 @@ +export 'alien_bumper_contact_behavior.dart'; +export 'alien_bumper_sprite_behavior.dart'; diff --git a/lib/game/components/alien_zone.dart b/lib/game/components/alien_zone/alien_zone.dart similarity index 69% rename from lib/game/components/alien_zone.dart rename to lib/game/components/alien_zone/alien_zone.dart index 5603e09a..eebc6cbf 100644 --- a/lib/game/components/alien_zone.dart +++ b/lib/game/components/alien_zone/alien_zone.dart @@ -1,7 +1,7 @@ // ignore_for_file: avoid_renaming_method_parameters -import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/components/alien_zone/alien_bumper/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; @@ -20,28 +20,17 @@ class AlienZone extends Blueprint { AlienBumper.a( children: [ ScoringBehaviour(points: 20), + AlienBumperContactBehavior(), + AlienBumperSpriteBehavior(), ], )..initialPosition = Vector2(-32.52, -9.1), AlienBumper.b( children: [ ScoringBehaviour(points: 20), + AlienBumperContactBehavior(), + AlienBumperSpriteBehavior(), ], )..initialPosition = Vector2(-22.89, -17.35), ], ); } - -/// Listens when a [Ball] bounces against an [AlienBumper]. -// TODO(alestiago): Add Bumper animation behaviour. -// @visibleForTesting -// class AlienBumperBallContactCallback -// extends ContactCallback { -// @override -// void begin( -// AlienBumper alienBumper, -// Ball _, -// Contact __, -// ) { -// alienBumper.animate(); -// } -// } diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 95e3b697..64500501 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,4 +1,4 @@ -export 'alien_zone.dart'; +export 'alien_zone/alien_zone.dart'; export 'board.dart'; export 'camera_controller.dart'; export 'controlled_ball.dart'; diff --git a/lib/game/components/scoring_behaviour.dart b/lib/game/components/scoring_behaviour.dart index b645a8b2..fad8ce04 100644 --- a/lib/game/components/scoring_behaviour.dart +++ b/lib/game/components/scoring_behaviour.dart @@ -10,7 +10,7 @@ import 'package:pinball_flame/pinball_flame.dart'; /// /// {@endtemplate} class ScoringBehaviour extends Component - with ContactCallbacks, HasGameRef { + with ContactCallbacksNotifer, HasGameRef { /// {@macro scoring_behaviour} ScoringBehaviour({ required int points, @@ -28,8 +28,8 @@ class ScoringBehaviour extends Component if (parent is! BodyComponent) return; final userData = parent.body.userData; - if (userData is ContactCallbacks) { - userData.add(this); + if (userData is ContactCallbacksNotifer) { + userData.addCallback(this); } else { parent.body.userData = this; } @@ -42,7 +42,6 @@ class ScoringBehaviour extends Component gameRef.read().add(Scored(points: _points)); gameRef.audio.score(); - gameRef.add( ScoreText( text: _points.toString(), diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 9a79dafd..ea9d904f 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -53,7 +53,8 @@ class PinballGame extends Forge2DGame final launcher = Launcher(); unawaited(addFromBlueprint(launcher)); unawaited(add(Board())); - unawaited(add(AlienZone())); + await addFromBlueprint(AlienZone()); + await addFromBlueprint(SparkyFireZone()); unawaited(addFromBlueprint(Slingshots())); unawaited(addFromBlueprint(DinoWalls())); diff --git a/packages/pinball_components/lib/src/components/alien_bumper.dart b/packages/pinball_components/lib/src/components/alien_bumper.dart index 219f99bb..0b8a19b4 100644 --- a/packages/pinball_components/lib/src/components/alien_bumper.dart +++ b/packages/pinball_components/lib/src/components/alien_bumper.dart @@ -2,13 +2,39 @@ import 'dart:async'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; +abstract class State { + // TODO(alestiago): Investigate approaches to avoid having this as late. + late T _state; + + T get state => _state; + + set state(T value) { + if (value == _state) return; + _state = value; + _stream.sink.add(value); + } + + final StreamController _stream = StreamController.broadcast(); + + Stream get stream => _stream.stream; +} + +/// Indicates the [AlienBumper]'s current sprite state. +enum AlienBumperState { + /// A lit up bumper. + active, + + /// A dimmed bumper. + inactive, +} + /// {@template alien_bumper} /// Bumper for area under the [Spaceship]. /// {@endtemplate} -class AlienBumper extends BodyComponent with InitialPosition { +class AlienBumper extends BodyComponent + with InitialPosition, State { /// {@macro alien_bumper} AlienBumper._({ required double majorRadius, @@ -21,14 +47,17 @@ class AlienBumper extends BodyComponent with InitialPosition { super( priority: RenderPriority.alienBumper, children: [ - _AlienBumperSpriteGroupComponent( - onAssetPath: onAssetPath, - offAssetPath: offAssetPath, - ), if (children != null) ...children, ], renderBody: false, - ); + ) { + _state = AlienBumperState.active; + _spriteGroupComponent = _AlienBumperSpriteGroupComponent( + offAssetPath: offAssetPath, + onAssetPath: onAssetPath, + state: state, + ); + } /// {@macro alien_bumper} AlienBumper.a({ @@ -53,8 +82,17 @@ class AlienBumper extends BodyComponent with InitialPosition { ); final double _majorRadius; + final double _minorRadius; + late final _AlienBumperSpriteGroupComponent _spriteGroupComponent; + + @override + Future onLoad() async { + await super.onLoad(); + await add(_spriteGroupComponent); + } + @override Body createBody() { final shape = EllipseShape( @@ -68,41 +106,24 @@ class AlienBumper extends BodyComponent with InitialPosition { ); final bodyDef = BodyDef( position: initialPosition, - userData: this, ); return world.createBody(bodyDef)..createFixture(fixtureDef); } - - /// Animates the [AlienBumper]. - Future animate() async { - final spriteGroupComponent = firstChild<_AlienBumperSpriteGroupComponent>() - ?..current = AlienBumperSpriteState.inactive; - await Future.delayed(const Duration(milliseconds: 50)); - spriteGroupComponent?.current = AlienBumperSpriteState.active; - } -} - -/// Indicates the [AlienBumper]'s current sprite state. -@visibleForTesting -enum AlienBumperSpriteState { - /// A lit up bumper. - active, - - /// A dimmed bumper. - inactive, } class _AlienBumperSpriteGroupComponent - extends SpriteGroupComponent with HasGameRef { + extends SpriteGroupComponent with HasGameRef { _AlienBumperSpriteGroupComponent({ required String onAssetPath, required String offAssetPath, + required AlienBumperState state, }) : _onAssetPath = onAssetPath, _offAssetPath = offAssetPath, super( anchor: Anchor.center, position: Vector2(0, -0.1), + current: state, ); final String _onAssetPath; @@ -113,14 +134,20 @@ class _AlienBumperSpriteGroupComponent await super.onLoad(); final sprites = { - AlienBumperSpriteState.active: - Sprite(gameRef.images.fromCache(_onAssetPath)), - AlienBumperSpriteState.inactive: + AlienBumperState.active: Sprite( + gameRef.images.fromCache(_onAssetPath), + ), + AlienBumperState.inactive: Sprite(gameRef.images.fromCache(_offAssetPath)), }; this.sprites = sprites; - - current = AlienBumperSpriteState.active; size = sprites[current]!.originalSize / 10; + + // TODO(alestiago): Refactor once the following is merged: + // https://github.com/flame-engine/flame/pull/1566 + final parent = this.parent; + if (parent is! AlienBumper) return; + + parent.stream.listen((state) => current = state); } } diff --git a/packages/pinball_components/test/src/components/alien_bumper_test.dart b/packages/pinball_components/test/src/components/alien_bumper_test.dart index c6384759..50701da0 100644 --- a/packages/pinball_components/test/src/components/alien_bumper_test.dart +++ b/packages/pinball_components/test/src/components/alien_bumper_test.dart @@ -40,21 +40,17 @@ void main() { expect( spriteGroupComponent.current, - equals(AlienBumperSpriteState.active), + equals(AlienBumperState.active), ); - final future = bumper.animate(); - expect( spriteGroupComponent.current, - equals(AlienBumperSpriteState.inactive), + equals(AlienBumperState.inactive), ); - await future; - expect( spriteGroupComponent.current, - equals(AlienBumperSpriteState.active), + equals(AlienBumperState.active), ); }); }); diff --git a/packages/pinball_flame/lib/src/contacts_callbacks_adder.dart b/packages/pinball_flame/lib/src/contacts_callbacks_adder.dart index 2d4b5dc8..c0029883 100644 --- a/packages/pinball_flame/lib/src/contacts_callbacks_adder.dart +++ b/packages/pinball_flame/lib/src/contacts_callbacks_adder.dart @@ -1,38 +1,60 @@ import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; -/// {@template contact_callbacks_adder} -/// -/// {@endtemplate} -// TODO(alestiago): Consider adding streams to [ContactCallbacks]. -extension ContactCallbacksAdder on ContactCallbacks { - /// {@macro contact_callbacks_adder} - void add(ContactCallbacks contactCallbacks) { - if (contactCallbacks.onBeginContact != null) { - onBeginContact = (other, contact) { - onBeginContact?.call(other, contact); - contactCallbacks.beginContact(other, contact); - }; +class ContactCallbacksNotifer implements ContactCallbacks { + final List _callbacks = []; + + @override + @mustCallSuper + void beginContact(Object other, Contact contact) { + onBeginContact?.call(other, contact); + for (final callback in _callbacks) { + callback.beginContact(other, contact); } + } - if (contactCallbacks.onEndContact != null) { - onEndContact = (other, contact) { - onEndContact?.call(other, contact); - contactCallbacks.endContact(other, contact); - }; + @override + @mustCallSuper + void endContact(Object other, Contact contact) { + onEndContact?.call(other, contact); + for (final callback in _callbacks) { + callback.endContact(other, contact); } + } - if (contactCallbacks.onPreSolve != null) { - onPreSolve = (other, contact, oldManifold) { - onPreSolve?.call(other, contact, oldManifold); - contactCallbacks.preSolve(other, contact, oldManifold); - }; + @override + @mustCallSuper + void preSolve(Object other, Contact contact, Manifold oldManifold) { + onPreSolve?.call(other, contact, oldManifold); + for (final callback in _callbacks) { + callback.preSolve(other, contact, oldManifold); } + } - if (contactCallbacks.onPostSolve != null) { - onPostSolve = (other, contact, impulse) { - onPostSolve?.call(other, contact, impulse); - contactCallbacks.postSolve(other, contact, impulse); - }; + @override + @mustCallSuper + void postSolve(Object other, Contact contact, ContactImpulse impulse) { + onPostSolve?.call(other, contact, impulse); + for (final callback in _callbacks) { + callback.postSolve(other, contact, impulse); } } + + void addCallback(ContactCallbacks callback) { + _callbacks.add(callback); + } + + @override + void Function(Object other, Contact contact)? onBeginContact; + + @override + void Function(Object other, Contact contact)? onEndContact; + + @override + void Function(Object other, Contact contact, ContactImpulse impulse)? + onPostSolve; + + @override + void Function(Object other, Contact contact, Manifold oldManifold)? + onPreSolve; }