diff --git a/lib/game/components/android_acres/behaviors/behaviors.dart b/lib/game/components/android_acres/behaviors/behaviors.dart index 91b1e132..2c3335e5 100644 --- a/lib/game/components/android_acres/behaviors/behaviors.dart +++ b/lib/game/components/android_acres/behaviors/behaviors.dart @@ -1,3 +1,4 @@ export 'android_spaceship_bonus_behavior.dart'; export 'ramp_bonus_behavior.dart'; +export 'ramp_progress_behavior.dart'; export 'ramp_shot_behavior.dart'; diff --git a/lib/game/components/android_acres/behaviors/ramp_progress_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_progress_behavior.dart new file mode 100644 index 00000000..6171b31f --- /dev/null +++ b/lib/game/components/android_acres/behaviors/ramp_progress_behavior.dart @@ -0,0 +1,69 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.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 ramp_progress_behavior} +/// Increases the score when a [Ball] is shot into the [SpaceshipRamp]. +/// {@endtemplate} +class RampProgressBehavior extends Component + with ParentIsA, FlameBlocReader { + /// {@macro ramp_shot_behavior} + RampProgressBehavior() : super(); + + /// Creates a [RampProgressBehavior]. + /// + /// This can be used for testing [RampProgressBehavior] in isolation. + @visibleForTesting + RampProgressBehavior.test({ + required this.subscription, + }) : super(); + + /// Subscription to [SpaceshipRampState] at [SpaceshipRamp]. + @visibleForTesting + StreamSubscription? subscription; + + @override + void onMount() { + super.onMount(); + + final spaceshipRamp = parent.children.whereType().first; + + var previousState = const SpaceshipRampState.initial(); + + subscription = subscription ?? + parent.bloc.stream.listen((state) { + final listenWhen = + previousState.hits != state.hits && state.hits != 0; + if (listenWhen) { + var fullArrowLit = spaceshipRamp.bloc.isFullyProgressed(); + var isMaxMultiplier = bloc.state.multiplier == 6; + final canProgress = + !isMaxMultiplier || (isMaxMultiplier && !fullArrowLit); + + if (canProgress) { + spaceshipRamp.bloc.onProgressed(); + } + + fullArrowLit = spaceshipRamp.bloc.isFullyProgressed(); + isMaxMultiplier = bloc.state.multiplier == 6; + + if (fullArrowLit && !isMaxMultiplier) { + spaceshipRamp.bloc.onAnimate(); + } + + previousState = state; + } + }); + } + + @override + void onRemove() { + subscription?.cancel(); + super.onRemove(); + } +} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart index 1f9b6284..9eae19f0 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart @@ -1 +1,2 @@ +export 'ramp_arrow_blinking_behavior.dart'; export 'ramp_ball_ascending_contact_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_arrow_blinking_behavior.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_arrow_blinking_behavior.dart new file mode 100644 index 00000000..dee9b436 --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_arrow_blinking_behavior.dart @@ -0,0 +1,77 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template ramp_arrow_blinking_behavior} +/// Makes a [SpaceshipRampArrowSpriteComponent] blink between [ArrowLightState.values]. +/// {@endtemplate} +class RampArrowBlinkingBehavior extends TimerComponent + with ParentIsA { + /// {@macro ramp_arrow_blinking_behavior} + RampArrowBlinkingBehavior() : super(period: 0.05); + + final _maxBlinks = 15; + + int _blinksCounter = 0; + + bool _isAnimating = false; + + void _onNewState(SpaceshipRampState state) { + final animationEnabled = + state.animationState == ArrowAnimationState.blinking; + final canBlink = _blinksCounter < _maxBlinks; + + if (animationEnabled && canBlink) { + _start(); + } else { + _stop(); + } + } + + void _start() { + if (!_isAnimating) { + _isAnimating = true; + timer + ..reset() + ..start(); + _animate(); + } + } + + void _animate() { + parent.bloc.onBlink(); + _blinksCounter++; + } + + void _stop() { + if (_isAnimating) { + _isAnimating = false; + timer.stop(); + _blinksCounter = 0; + parent.bloc.onStop(); + } + } + + @override + Future onLoad() async { + await super.onLoad(); + parent.bloc.stream.listen(_onNewState); + } + + @override + void onTick() { + super.onTick(); + if (!_isAnimating) { + timer.stop(); + } else { + if (_blinksCounter < _maxBlinks) { + _animate(); + timer + ..reset() + ..start(); + } else { + timer.stop(); + } + } + } +} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart index c3dc9e3e..484bea99 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart @@ -11,4 +11,79 @@ class SpaceshipRampCubit extends Cubit { state.copyWith(hits: state.hits + 1), ); } + + void onProgressed() { + final index = ArrowLightState.values.indexOf(state.lightState); + + emit( + state.copyWith( + lightState: + ArrowLightState.values[(index + 1) % ArrowLightState.values.length], + animationState: ArrowAnimationState.idle, + ), + ); + } + + bool isFullyProgressed() => + state.lightState == ArrowLightState.active5 && + state.animationState == ArrowAnimationState.idle; + + void onReset() { + emit( + state.copyWith( + lightState: ArrowLightState.inactive, + animationState: ArrowAnimationState.idle, + ), + ); + } + + void onAnimate() { + emit( + state.copyWith(animationState: ArrowAnimationState.blinking), + ); + } + + void onStop() { + emit( + state.copyWith( + lightState: ArrowLightState.inactive, + animationState: ArrowAnimationState.idle, + ), + ); + } + + void onBlink() { + switch (state.lightState) { + case ArrowLightState.inactive: + emit( + state.copyWith(lightState: ArrowLightState.active1), + ); + break; + case ArrowLightState.active1: + emit( + state.copyWith(lightState: ArrowLightState.active2), + ); + break; + case ArrowLightState.active2: + emit( + state.copyWith(lightState: ArrowLightState.active3), + ); + break; + case ArrowLightState.active3: + emit( + state.copyWith(lightState: ArrowLightState.active4), + ); + break; + case ArrowLightState.active4: + emit( + state.copyWith(lightState: ArrowLightState.active5), + ); + break; + case ArrowLightState.active5: + emit( + state.copyWith(lightState: ArrowLightState.inactive), + ); + break; + } + } } diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart index 2979f05f..bc5a3a9c 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart @@ -1,22 +1,64 @@ +// ignore_for_file: comment_references + part of 'spaceship_ramp_cubit.dart'; class SpaceshipRampState extends Equatable { const SpaceshipRampState({ required this.hits, + required this.lightState, + required this.animationState, }) : assert(hits >= 0, "Hits can't be negative"); - const SpaceshipRampState.initial() : this(hits: 0); + const SpaceshipRampState.initial() + : this( + hits: 0, + lightState: ArrowLightState.inactive, + animationState: ArrowAnimationState.idle, + ); final int hits; + final ArrowLightState lightState; + final ArrowAnimationState animationState; SpaceshipRampState copyWith({ int? hits, + ArrowLightState? lightState, + ArrowAnimationState? animationState, }) { return SpaceshipRampState( hits: hits ?? this.hits, + lightState: lightState ?? this.lightState, + animationState: animationState ?? this.animationState, ); } @override - List get props => [hits]; + List get props => [hits, lightState, animationState]; +} + +/// Indicates the state of the arrow on the [SpaceshipRamp]. +enum ArrowLightState { + /// Arrow with no dashes lit up. + inactive, + + /// Arrow with 1 light lit up. + active1, + + /// Arrow with 2 lights lit up. + active2, + + /// Arrow with 3 lights lit up. + active3, + + /// Arrow with 4 lights lit up. + active4, + + /// Arrow with all 5 lights lit up. + active5, +} + +// Indicates if the blinking animation is running. +enum ArrowAnimationState { + idle, + blinking, } diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart index 07a5e79b..92119cf3 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart @@ -40,8 +40,9 @@ class SpaceshipRamp extends Component { SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5), _SpaceshipRampBackgroundRailingSpriteComponent(), SpaceshipRampArrowSpriteComponent( - current: bloc.state.hits, + current: bloc.state.lightState, ), + RampArrowBlinkingBehavior(), ...?children, ], ); @@ -167,11 +168,12 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent /// Lights progressively whenever a [Ball] gets into [SpaceshipRamp]. /// {@endtemplate} @visibleForTesting -class SpaceshipRampArrowSpriteComponent extends SpriteGroupComponent +class SpaceshipRampArrowSpriteComponent + extends SpriteGroupComponent with HasGameRef, ParentIsA, ZIndex { /// {@macro spaceship_ramp_arrow_sprite_component} SpaceshipRampArrowSpriteComponent({ - required int current, + required ArrowLightState current, }) : super( anchor: Anchor.center, position: Vector2(-3.9, -56.5), @@ -184,58 +186,38 @@ class SpaceshipRampArrowSpriteComponent extends SpriteGroupComponent Future onLoad() async { await super.onLoad(); parent.bloc.stream.listen((state) { - current = state.hits % SpaceshipRampArrowSpriteState.values.length; + print("STATE $state"); + current = state.lightState; }); - final sprites = {}; + final sprites = {}; this.sprites = sprites; - for (final spriteState in SpaceshipRampArrowSpriteState.values) { - sprites[spriteState.index] = Sprite( + for (final spriteState in ArrowLightState.values) { + print("SPRITE $spriteState"); + sprites[spriteState] = Sprite( gameRef.images.fromCache(spriteState.path), ); } - current = 0; + current = ArrowLightState.inactive; size = sprites[current]!.originalSize / 10; } } -/// Indicates the state of the arrow on the [SpaceshipRamp]. -@visibleForTesting -enum SpaceshipRampArrowSpriteState { - /// Arrow with no dashes lit up. - inactive, - - /// Arrow with 1 light lit up. - active1, - - /// Arrow with 2 lights lit up. - active2, - - /// Arrow with 3 lights lit up. - active3, - - /// Arrow with 4 lights lit up. - active4, - - /// Arrow with all 5 lights lit up. - active5, -} - -extension on SpaceshipRampArrowSpriteState { +extension on ArrowLightState { String get path { switch (this) { - case SpaceshipRampArrowSpriteState.inactive: + case ArrowLightState.inactive: return Assets.images.android.ramp.arrow.inactive.keyName; - case SpaceshipRampArrowSpriteState.active1: + case ArrowLightState.active1: return Assets.images.android.ramp.arrow.active1.keyName; - case SpaceshipRampArrowSpriteState.active2: + case ArrowLightState.active2: return Assets.images.android.ramp.arrow.active2.keyName; - case SpaceshipRampArrowSpriteState.active3: + case ArrowLightState.active3: return Assets.images.android.ramp.arrow.active3.keyName; - case SpaceshipRampArrowSpriteState.active4: + case ArrowLightState.active4: return Assets.images.android.ramp.arrow.active4.keyName; - case SpaceshipRampArrowSpriteState.active5: + case ArrowLightState.active5: return Assets.images.android.ramp.arrow.active5.keyName; } }