diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 03adc4bd..a5663227 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -19,7 +19,7 @@ export 'kicker.dart'; export 'launch_ramp.dart'; export 'layer.dart'; export 'layer_sensor.dart'; -export 'multiball.dart'; +export 'multiball/multiball.dart'; export 'plunger.dart'; export 'render_priority.dart'; export 'rocket.dart'; diff --git a/packages/pinball_components/lib/src/components/multiball/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/multiball/behaviors/behaviors.dart new file mode 100644 index 00000000..052b4a4e --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'multiball_blinking_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.dart b/packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.dart new file mode 100644 index 00000000..c266e123 --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.dart @@ -0,0 +1,39 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template multiball_blinking_behavior} +/// Makes a [Multiball] blink back to [MultiballState.lit] when +/// [MultiballState.dimmed]. +/// {@endtemplate} +class MultiballBlinkingBehavior extends TimerComponent + with ParentIsA { + /// {@macro multiball_blinking_behavior} + MultiballBlinkingBehavior() : super(period: 0.05); + + void _onNewState(MultiballState state) { + switch (state) { + case MultiballState.lit: + break; + case MultiballState.dimmed: + timer + ..reset() + ..start(); + break; + } + } + + @override + Future onLoad() async { + await super.onLoad(); + timer.stop(); + parent.bloc.stream.listen(_onNewState); + } + + @override + void onTick() { + super.onTick(); + timer.stop(); + parent.bloc.onBlinked(); + } +} diff --git a/packages/pinball_components/lib/src/components/multiball/cubit/multiball_cubit.dart b/packages/pinball_components/lib/src/components/multiball/cubit/multiball_cubit.dart new file mode 100644 index 00000000..1fe37044 --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/cubit/multiball_cubit.dart @@ -0,0 +1,17 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; + +part 'multiball_state.dart'; + +class MultiballCubit extends Cubit { + MultiballCubit() : super(MultiballState.dimmed); + + void animate() { + emit(MultiballState.lit); + } + + void onBlinked() { + emit(MultiballState.dimmed); + } +} diff --git a/packages/pinball_components/lib/src/components/multiball/cubit/multiball_state.dart b/packages/pinball_components/lib/src/components/multiball/cubit/multiball_state.dart new file mode 100644 index 00000000..44b8bb80 --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/cubit/multiball_state.dart @@ -0,0 +1,9 @@ +// ignore_for_file: comment_references, public_member_api_docs + +part of 'multiball_cubit.dart'; + +/// Indicates the different sprite states for [MultiballSpriteGroupComponent]. +enum MultiballState { + lit, + dimmed, +} diff --git a/packages/pinball_components/lib/src/components/multiball/multiball.dart b/packages/pinball_components/lib/src/components/multiball/multiball.dart new file mode 100644 index 00000000..662e281e --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/multiball.dart @@ -0,0 +1,158 @@ +import 'dart:math' as math; +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/gen/assets.gen.dart'; +import 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart'; +import 'package:pinball_components/src/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/multiball_cubit.dart'; + +/// {@template multiball} +/// A [Component] for the multiball over the board. +/// {@endtemplate} +class Multiball extends Component { + /// {@macro multiball} + Multiball._({ + required Vector2 position, + required String onAssetPath, + required String offAssetPath, + double rotation = 0, + Iterable? children, + required this.bloc, + }) : super( + children: [ + MultiballBlinkingBehavior(), + MultiballSpriteGroupComponent( + position: position, + onAssetPath: onAssetPath, + offAssetPath: offAssetPath, + rotation: rotation, + state: bloc.state, + ), + ...?children, + ], + ); + + /// {@macro multiball} + Multiball.a({ + Iterable? children, + }) : this._( + position: Vector2(-23, 7.5), + onAssetPath: Assets.images.multiball.a.lit.keyName, + offAssetPath: Assets.images.multiball.a.dimmed.keyName, + rotation: -24 * math.pi / 180, + bloc: MultiballCubit(), + children: children, + ); + + /// {@macro multiball} + Multiball.b({ + Iterable? children, + }) : this._( + position: Vector2(-7, -6.5), + onAssetPath: Assets.images.multiball.b.lit.keyName, + offAssetPath: Assets.images.multiball.b.dimmed.keyName, + rotation: -5 * math.pi / 180, + bloc: MultiballCubit(), + children: children, + ); + + /// {@macro multiball} + Multiball.c({ + Iterable? children, + }) : this._( + position: Vector2(-0.5, -9.5), + onAssetPath: Assets.images.multiball.c.lit.keyName, + offAssetPath: Assets.images.multiball.c.dimmed.keyName, + rotation: 3 * math.pi / 180, + bloc: MultiballCubit(), + children: children, + ); + + /// {@macro multiball} + Multiball.d({ + Iterable? children, + }) : this._( + position: Vector2(15, 7), + onAssetPath: Assets.images.multiball.d.lit.keyName, + offAssetPath: Assets.images.multiball.d.dimmed.keyName, + rotation: 24 * math.pi / 180, + bloc: MultiballCubit(), + children: children, + ); + + /// Creates an [Multiball] without any children. + /// + /// This can be used for testing [Multiball]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + Multiball.test({ + required this.bloc, + }); + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final MultiballCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } + + /// Animates the [Multiball]. + Future animate() async { + final spriteGroupComponent = firstChild(); + + for (var i = 0; i < 5; i++) { + spriteGroupComponent?.current = MultiballState.lit; + await Future.delayed(const Duration(milliseconds: 100)); + spriteGroupComponent?.current = MultiballState.dimmed; + await Future.delayed(const Duration(milliseconds: 100)); + } + } +} + +/// {@template multiball_sprite_group_component} +/// A [SpriteGroupComponent] for the multiball over the board. +/// {@endtemplate} +@visibleForTesting +class MultiballSpriteGroupComponent extends SpriteGroupComponent + with HasGameRef, ParentIsA { + /// {@macro multiball_sprite_group_component} + MultiballSpriteGroupComponent({ + required Vector2 position, + required String onAssetPath, + required String offAssetPath, + required double rotation, + required MultiballState state, + }) : _onAssetPath = onAssetPath, + _offAssetPath = offAssetPath, + super( + anchor: Anchor.center, + position: position, + angle: rotation, + current: state, + ); + + final String _onAssetPath; + final String _offAssetPath; + + @override + Future onLoad() async { + await super.onLoad(); + parent.bloc.stream.listen((state) => current = state); + + final sprites = { + MultiballState.lit: Sprite(gameRef.images.fromCache(_onAssetPath)), + MultiballState.dimmed: Sprite(gameRef.images.fromCache(_offAssetPath)), + }; + this.sprites = sprites; + + current = MultiballState.dimmed; + size = sprites[current]!.originalSize / 10; + } +}