Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 200 KiB |
@ -0,0 +1 @@
|
|||||||
|
export 'multiballs_behavior.dart';
|
@ -0,0 +1,28 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_bloc/flame_bloc.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
/// Toggle each [Multiball] when there is a bonus ball.
|
||||||
|
class MultiballsBehavior extends Component
|
||||||
|
with
|
||||||
|
HasGameRef<PinballGame>,
|
||||||
|
ParentIsA<Multiballs>,
|
||||||
|
BlocComponent<GameBloc, GameState> {
|
||||||
|
@override
|
||||||
|
bool listenWhen(GameState? previousState, GameState newState) {
|
||||||
|
final hasChanged = previousState?.bonusHistory != newState.bonusHistory;
|
||||||
|
final lastBonusIsMultiball = newState.bonusHistory.isNotEmpty &&
|
||||||
|
newState.bonusHistory.last == GameBonus.dashNest;
|
||||||
|
|
||||||
|
return hasChanged && lastBonusIsMultiball;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onNewState(GameState state) {
|
||||||
|
parent.children.whereType<Multiball>().forEach((multiball) {
|
||||||
|
multiball.bloc.onAnimate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball/game/components/multiballs/behaviors/behaviors.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
/// {@template multiballs_component}
|
||||||
|
/// A [SpriteGroupComponent] for the multiball over the board.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class Multiballs extends Component with ZIndex {
|
||||||
|
/// {@macro multiballs_component}
|
||||||
|
Multiballs()
|
||||||
|
: super(
|
||||||
|
children: [
|
||||||
|
Multiball.a(),
|
||||||
|
Multiball.b(),
|
||||||
|
Multiball.c(),
|
||||||
|
Multiball.d(),
|
||||||
|
MultiballsBehavior(),
|
||||||
|
],
|
||||||
|
) {
|
||||||
|
zIndex = ZIndexes.decal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [Multiballs] without any children.
|
||||||
|
///
|
||||||
|
/// This can be used for testing [Multiballs]'s behaviors in isolation.
|
||||||
|
@visibleForTesting
|
||||||
|
Multiballs.test();
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/flame.dart';
|
||||||
|
import 'package:flame/sprite.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
import 'package:pinball_theme/pinball_theme.dart';
|
||||||
|
|
||||||
|
/// {@template selected_character}
|
||||||
|
/// Shows an animated version of the character currently selected.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class SelectedCharacter extends StatefulWidget {
|
||||||
|
/// {@macro selected_character}
|
||||||
|
const SelectedCharacter({
|
||||||
|
Key? key,
|
||||||
|
required this.currentCharacter,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The character that is selected at the moment.
|
||||||
|
final CharacterTheme currentCharacter;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SelectedCharacter> createState() => _SelectedCharacterState();
|
||||||
|
|
||||||
|
/// Returns a list of assets to be loaded.
|
||||||
|
static List<Future> loadAssets() {
|
||||||
|
return [
|
||||||
|
Flame.images.load(const DashTheme().animation.keyName),
|
||||||
|
Flame.images.load(const AndroidTheme().animation.keyName),
|
||||||
|
Flame.images.load(const DinoTheme().animation.keyName),
|
||||||
|
Flame.images.load(const SparkyTheme().animation.keyName),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectedCharacterState extends State<SelectedCharacter>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
SpriteAnimationController? _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_setupCharacterAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant SelectedCharacter oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
_setupCharacterAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.currentCharacter.name,
|
||||||
|
style: Theme.of(context).textTheme.headline2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Expanded(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SizedBox(
|
||||||
|
width: constraints.maxWidth,
|
||||||
|
height: constraints.maxHeight,
|
||||||
|
child: SpriteAnimationWidget(
|
||||||
|
controller: _controller!,
|
||||||
|
anchor: Anchor.center,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setupCharacterAnimation() {
|
||||||
|
final spriteSheet = SpriteSheet.fromColumnsAndRows(
|
||||||
|
image: Flame.images.fromCache(widget.currentCharacter.animation.keyName),
|
||||||
|
columns: 12,
|
||||||
|
rows: 6,
|
||||||
|
);
|
||||||
|
final animation = spriteSheet.createAnimation(
|
||||||
|
row: 0,
|
||||||
|
stepTime: 1 / 24,
|
||||||
|
to: spriteSheet.rows * spriteSheet.columns,
|
||||||
|
);
|
||||||
|
if (_controller != null) _controller?.dispose();
|
||||||
|
_controller = SpriteAnimationController(vsync: this, animation: animation)
|
||||||
|
..forward()
|
||||||
|
..repeat();
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1,2 @@
|
|||||||
export 'character_selection_page.dart';
|
export 'character_selection_page.dart';
|
||||||
|
export 'selected_character.dart';
|
||||||
|
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 19 KiB |
@ -0,0 +1 @@
|
|||||||
|
export 'multiball_blinking_behavior.dart';
|
@ -0,0 +1,78 @@
|
|||||||
|
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 [MultiballLightState.lit] when
|
||||||
|
/// [MultiballLightState.dimmed].
|
||||||
|
/// {@endtemplate}
|
||||||
|
class MultiballBlinkingBehavior extends TimerComponent
|
||||||
|
with ParentIsA<Multiball> {
|
||||||
|
/// {@macro multiball_blinking_behavior}
|
||||||
|
MultiballBlinkingBehavior() : super(period: 0.1);
|
||||||
|
|
||||||
|
final _maxBlinks = 10;
|
||||||
|
|
||||||
|
int _blinksCounter = 0;
|
||||||
|
|
||||||
|
bool _isAnimating = false;
|
||||||
|
|
||||||
|
void _onNewState(MultiballState state) {
|
||||||
|
final animationEnabled =
|
||||||
|
state.animationState == MultiballAnimationState.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<void> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
part 'multiball_state.dart';
|
||||||
|
|
||||||
|
class MultiballCubit extends Cubit<MultiballState> {
|
||||||
|
MultiballCubit() : super(const MultiballState.initial());
|
||||||
|
|
||||||
|
void onAnimate() {
|
||||||
|
emit(
|
||||||
|
state.copyWith(animationState: MultiballAnimationState.blinking),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onStop() {
|
||||||
|
emit(
|
||||||
|
state.copyWith(animationState: MultiballAnimationState.idle),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onBlink() {
|
||||||
|
switch (state.lightState) {
|
||||||
|
case MultiballLightState.lit:
|
||||||
|
emit(
|
||||||
|
state.copyWith(lightState: MultiballLightState.dimmed),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case MultiballLightState.dimmed:
|
||||||
|
emit(
|
||||||
|
state.copyWith(lightState: MultiballLightState.lit),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
// ignore_for_file: comment_references, public_member_api_docs
|
||||||
|
|
||||||
|
part of 'multiball_cubit.dart';
|
||||||
|
|
||||||
|
/// Indicates the different sprite states for [MultiballSpriteGroupComponent].
|
||||||
|
enum MultiballLightState {
|
||||||
|
lit,
|
||||||
|
dimmed,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indicates if the blinking animation is running.
|
||||||
|
enum MultiballAnimationState {
|
||||||
|
idle,
|
||||||
|
blinking,
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiballState extends Equatable {
|
||||||
|
const MultiballState({
|
||||||
|
required this.lightState,
|
||||||
|
required this.animationState,
|
||||||
|
});
|
||||||
|
|
||||||
|
const MultiballState.initial()
|
||||||
|
: this(
|
||||||
|
lightState: MultiballLightState.dimmed,
|
||||||
|
animationState: MultiballAnimationState.idle,
|
||||||
|
);
|
||||||
|
|
||||||
|
final MultiballLightState lightState;
|
||||||
|
final MultiballAnimationState animationState;
|
||||||
|
|
||||||
|
MultiballState copyWith({
|
||||||
|
MultiballLightState? lightState,
|
||||||
|
MultiballAnimationState? animationState,
|
||||||
|
}) {
|
||||||
|
return MultiballState(
|
||||||
|
lightState: lightState ?? this.lightState,
|
||||||
|
animationState: animationState ?? this.animationState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [lightState, animationState];
|
||||||
|
}
|
@ -0,0 +1,138 @@
|
|||||||
|
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 lighting decals on the board.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class Multiball extends Component {
|
||||||
|
/// {@macro multiball}
|
||||||
|
Multiball._({
|
||||||
|
required Vector2 position,
|
||||||
|
double rotation = 0,
|
||||||
|
Iterable<Component>? children,
|
||||||
|
required this.bloc,
|
||||||
|
}) : super(
|
||||||
|
children: [
|
||||||
|
MultiballBlinkingBehavior(),
|
||||||
|
MultiballSpriteGroupComponent(
|
||||||
|
position: position,
|
||||||
|
litAssetPath: Assets.images.multiball.lit.keyName,
|
||||||
|
dimmedAssetPath: Assets.images.multiball.dimmed.keyName,
|
||||||
|
rotation: rotation,
|
||||||
|
state: bloc.state.lightState,
|
||||||
|
),
|
||||||
|
...?children,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@macro multiball}
|
||||||
|
Multiball.a({
|
||||||
|
Iterable<Component>? children,
|
||||||
|
}) : this._(
|
||||||
|
position: Vector2(-23, 7.5),
|
||||||
|
rotation: -24 * math.pi / 180,
|
||||||
|
bloc: MultiballCubit(),
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@macro multiball}
|
||||||
|
Multiball.b({
|
||||||
|
Iterable<Component>? children,
|
||||||
|
}) : this._(
|
||||||
|
position: Vector2(-7.2, -6.2),
|
||||||
|
rotation: -5 * math.pi / 180,
|
||||||
|
bloc: MultiballCubit(),
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@macro multiball}
|
||||||
|
Multiball.c({
|
||||||
|
Iterable<Component>? children,
|
||||||
|
}) : this._(
|
||||||
|
position: Vector2(-0.7, -9.3),
|
||||||
|
rotation: 2.7 * math.pi / 180,
|
||||||
|
bloc: MultiballCubit(),
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@macro multiball}
|
||||||
|
Multiball.d({
|
||||||
|
Iterable<Component>? children,
|
||||||
|
}) : this._(
|
||||||
|
position: Vector2(15, 7),
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template multiball_sprite_group_component}
|
||||||
|
/// A [SpriteGroupComponent] for the multiball over the board.
|
||||||
|
/// {@endtemplate}
|
||||||
|
@visibleForTesting
|
||||||
|
class MultiballSpriteGroupComponent
|
||||||
|
extends SpriteGroupComponent<MultiballLightState>
|
||||||
|
with HasGameRef, ParentIsA<Multiball> {
|
||||||
|
/// {@macro multiball_sprite_group_component}
|
||||||
|
MultiballSpriteGroupComponent({
|
||||||
|
required Vector2 position,
|
||||||
|
required String litAssetPath,
|
||||||
|
required String dimmedAssetPath,
|
||||||
|
required double rotation,
|
||||||
|
required MultiballLightState state,
|
||||||
|
}) : _litAssetPath = litAssetPath,
|
||||||
|
_dimmedAssetPath = dimmedAssetPath,
|
||||||
|
super(
|
||||||
|
anchor: Anchor.center,
|
||||||
|
position: position,
|
||||||
|
angle: rotation,
|
||||||
|
current: state,
|
||||||
|
);
|
||||||
|
|
||||||
|
final String _litAssetPath;
|
||||||
|
final String _dimmedAssetPath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
parent.bloc.stream.listen((state) => current = state.lightState);
|
||||||
|
|
||||||
|
final sprites = {
|
||||||
|
MultiballLightState.lit: Sprite(
|
||||||
|
gameRef.images.fromCache(_litAssetPath),
|
||||||
|
),
|
||||||
|
MultiballLightState.dimmed:
|
||||||
|
Sprite(gameRef.images.fromCache(_dimmedAssetPath)),
|
||||||
|
};
|
||||||
|
this.sprites = sprites;
|
||||||
|
size = sprites[current]!.originalSize / 10;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/effects.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
enum Points {
|
||||||
|
fiveThousand,
|
||||||
|
twentyThousand,
|
||||||
|
twoHundredThousand,
|
||||||
|
oneMillion,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template score_component}
|
||||||
|
/// A [ScoreComponent] that spawns at a given [position] with a moving
|
||||||
|
/// animation.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex {
|
||||||
|
/// {@macro score_component}
|
||||||
|
ScoreComponent({
|
||||||
|
required this.points,
|
||||||
|
required Vector2 position,
|
||||||
|
}) : super(
|
||||||
|
position: position,
|
||||||
|
anchor: Anchor.center,
|
||||||
|
) {
|
||||||
|
zIndex = ZIndexes.score;
|
||||||
|
}
|
||||||
|
|
||||||
|
late final Effect _effect;
|
||||||
|
|
||||||
|
late Points points;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
final sprite = Sprite(
|
||||||
|
gameRef.images.fromCache(points.asset),
|
||||||
|
);
|
||||||
|
this.sprite = sprite;
|
||||||
|
size = sprite.originalSize / 55;
|
||||||
|
|
||||||
|
await add(
|
||||||
|
_effect = MoveEffect.by(
|
||||||
|
Vector2(0, -5),
|
||||||
|
EffectController(duration: 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update(double dt) {
|
||||||
|
super.update(dt);
|
||||||
|
|
||||||
|
if (_effect.controller.completed) {
|
||||||
|
removeFromParent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PointsX on Points {
|
||||||
|
int get value {
|
||||||
|
switch (this) {
|
||||||
|
case Points.fiveThousand:
|
||||||
|
return 5000;
|
||||||
|
case Points.twentyThousand:
|
||||||
|
return 20000;
|
||||||
|
case Points.twoHundredThousand:
|
||||||
|
return 200000;
|
||||||
|
case Points.oneMillion:
|
||||||
|
return 1000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on Points {
|
||||||
|
String get asset {
|
||||||
|
switch (this) {
|
||||||
|
case Points.fiveThousand:
|
||||||
|
return Assets.images.score.fiveThousand.keyName;
|
||||||
|
case Points.twentyThousand:
|
||||||
|
return Assets.images.score.twentyThousand.keyName;
|
||||||
|
case Points.twoHundredThousand:
|
||||||
|
return Assets.images.score.twoHundredThousand.keyName;
|
||||||
|
case Points.oneMillion:
|
||||||
|
return Assets.images.score.oneMillion.keyName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,57 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flame/effects.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart';
|
|
||||||
import 'package:pinball_flame/pinball_flame.dart';
|
|
||||||
|
|
||||||
/// {@template score_text}
|
|
||||||
/// A [TextComponent] that spawns at a given [position] with a moving animation.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class ScoreText extends TextComponent with ZIndex {
|
|
||||||
/// {@macro score_text}
|
|
||||||
ScoreText({
|
|
||||||
required String text,
|
|
||||||
required Vector2 position,
|
|
||||||
this.color = Colors.black,
|
|
||||||
}) : super(
|
|
||||||
text: text,
|
|
||||||
position: position,
|
|
||||||
anchor: Anchor.center,
|
|
||||||
) {
|
|
||||||
zIndex = ZIndexes.scoreText;
|
|
||||||
}
|
|
||||||
|
|
||||||
late final Effect _effect;
|
|
||||||
|
|
||||||
/// The [text]'s [Color].
|
|
||||||
final Color color;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
textRenderer = TextPaint(
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: PinballFonts.pixeloidMono,
|
|
||||||
color: color,
|
|
||||||
fontSize: 4,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await add(
|
|
||||||
_effect = MoveEffect.by(
|
|
||||||
Vector2(0, -5),
|
|
||||||
EffectController(duration: 1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void update(double dt) {
|
|
||||||
super.update(dt);
|
|
||||||
|
|
||||||
if (_effect.controller.completed) {
|
|
||||||
removeFromParent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,56 @@
|
|||||||
|
import 'package:flame/input.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:sandbox/stories/ball/basic_ball_game.dart';
|
||||||
|
|
||||||
|
class MultiballGame extends BallGame with KeyboardEvents {
|
||||||
|
MultiballGame()
|
||||||
|
: super(
|
||||||
|
imagesFileNames: [
|
||||||
|
Assets.images.multiball.lit.keyName,
|
||||||
|
Assets.images.multiball.dimmed.keyName,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
static const description = '''
|
||||||
|
Shows how the Multiball are rendered.
|
||||||
|
|
||||||
|
- Tap anywhere on the screen to spawn a ball into the game.
|
||||||
|
- Press space bar to animate multiballs.
|
||||||
|
''';
|
||||||
|
|
||||||
|
final List<Multiball> multiballs = [
|
||||||
|
Multiball.a(),
|
||||||
|
Multiball.b(),
|
||||||
|
Multiball.c(),
|
||||||
|
Multiball.d(),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
camera.followVector2(Vector2.zero());
|
||||||
|
|
||||||
|
await addAll(multiballs);
|
||||||
|
await traceAllBodies();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
KeyEventResult onKeyEvent(
|
||||||
|
RawKeyEvent event,
|
||||||
|
Set<LogicalKeyboardKey> keysPressed,
|
||||||
|
) {
|
||||||
|
if (event is RawKeyDownEvent &&
|
||||||
|
event.logicalKey == LogicalKeyboardKey.space) {
|
||||||
|
for (final multiball in multiballs) {
|
||||||
|
multiball.bloc.onBlink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import 'package:dashbook/dashbook.dart';
|
||||||
|
import 'package:sandbox/common/common.dart';
|
||||||
|
import 'package:sandbox/stories/multiball/multiball_game.dart';
|
||||||
|
|
||||||
|
void addMultiballStories(Dashbook dashbook) {
|
||||||
|
dashbook.storiesOf('Multiball').addGame(
|
||||||
|
title: 'Assets',
|
||||||
|
description: MultiballGame.description,
|
||||||
|
gameBuilder: (_) => MultiballGame(),
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flame/input.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:sandbox/common/common.dart';
|
||||||
|
|
||||||
|
class ScoreGame extends AssetsGame with TapDetector {
|
||||||
|
ScoreGame()
|
||||||
|
: super(
|
||||||
|
imagesFileNames: [
|
||||||
|
Assets.images.score.fiveThousand.keyName,
|
||||||
|
Assets.images.score.twentyThousand.keyName,
|
||||||
|
Assets.images.score.twoHundredThousand.keyName,
|
||||||
|
Assets.images.score.oneMillion.keyName,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
static const description = '''
|
||||||
|
Simple game to show how score component works,
|
||||||
|
|
||||||
|
- Tap anywhere on the screen to spawn an image on the given location.
|
||||||
|
''';
|
||||||
|
|
||||||
|
final random = Random();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
camera.followVector2(Vector2.zero());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onTapUp(TapUpInfo info) {
|
||||||
|
final index = random.nextInt(Points.values.length);
|
||||||
|
final score = Points.values[index];
|
||||||
|
|
||||||
|
add(
|
||||||
|
ScoreComponent(
|
||||||
|
points: score,
|
||||||
|
position: info.eventPosition.game..multiply(Vector2(1, -1)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import 'package:dashbook/dashbook.dart';
|
||||||
|
import 'package:sandbox/common/common.dart';
|
||||||
|
import 'package:sandbox/stories/score/score_game.dart';
|
||||||
|
|
||||||
|
void addScoreStories(Dashbook dashbook) {
|
||||||
|
dashbook.storiesOf('Score').addGame(
|
||||||
|
title: 'Basic',
|
||||||
|
description: ScoreGame.description,
|
||||||
|
gameBuilder: (_) => ScoreGame(),
|
||||||
|
);
|
||||||
|
}
|
@ -1,32 +0,0 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flame/input.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart';
|
|
||||||
import 'package:sandbox/common/common.dart';
|
|
||||||
|
|
||||||
class ScoreTextGame extends AssetsGame with TapDetector {
|
|
||||||
static const description = '''
|
|
||||||
Simple game to show how score text works,
|
|
||||||
|
|
||||||
- Tap anywhere on the screen to spawn an text on the given location.
|
|
||||||
''';
|
|
||||||
|
|
||||||
final random = Random();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
camera.followVector2(Vector2.zero());
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onTapUp(TapUpInfo info) {
|
|
||||||
add(
|
|
||||||
ScoreText(
|
|
||||||
text: random.nextInt(100000).toString(),
|
|
||||||
color: Colors.white,
|
|
||||||
position: info.eventPosition.game..multiply(Vector2(1, -1)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
import 'package:dashbook/dashbook.dart';
|
|
||||||
import 'package:sandbox/common/common.dart';
|
|
||||||
import 'package:sandbox/stories/score_text/score_text_game.dart';
|
|
||||||
|
|
||||||
void addScoreTextStories(Dashbook dashbook) {
|
|
||||||
dashbook.storiesOf('ScoreText').addGame(
|
|
||||||
title: 'Basic',
|
|
||||||
description: ScoreTextGame.description,
|
|
||||||
gameBuilder: (_) => ScoreTextGame(),
|
|
||||||
);
|
|
||||||
}
|
|
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 27 KiB |
@ -0,0 +1,158 @@
|
|||||||
|
// ignore_for_file: prefer_const_constructors, cascade_invocations
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
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_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart';
|
||||||
|
|
||||||
|
import '../../../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final flameTester = FlameTester(TestGame.new);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'MultiballBlinkingBehavior',
|
||||||
|
() {
|
||||||
|
flameTester.testGameWidget(
|
||||||
|
'calls onBlink every 0.1 seconds when animation state is animated',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
final behavior = MultiballBlinkingBehavior();
|
||||||
|
final bloc = MockMultiballCubit();
|
||||||
|
final streamController = StreamController<MultiballState>();
|
||||||
|
whenListen(
|
||||||
|
bloc,
|
||||||
|
streamController.stream,
|
||||||
|
initialState: MultiballState.initial(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final multiball = Multiball.test(bloc: bloc);
|
||||||
|
await multiball.add(behavior);
|
||||||
|
await game.ensureAdd(multiball);
|
||||||
|
|
||||||
|
streamController.add(
|
||||||
|
MultiballState(
|
||||||
|
animationState: MultiballAnimationState.blinking,
|
||||||
|
lightState: MultiballLightState.lit,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
game.update(0);
|
||||||
|
|
||||||
|
verify(bloc.onBlink).called(1);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
game.update(0.1);
|
||||||
|
|
||||||
|
await streamController.close();
|
||||||
|
verify(bloc.onBlink).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.testGameWidget(
|
||||||
|
'calls onStop when animation state is stopped',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
final behavior = MultiballBlinkingBehavior();
|
||||||
|
final bloc = MockMultiballCubit();
|
||||||
|
final streamController = StreamController<MultiballState>();
|
||||||
|
whenListen(
|
||||||
|
bloc,
|
||||||
|
streamController.stream,
|
||||||
|
initialState: MultiballState.initial(),
|
||||||
|
);
|
||||||
|
when(bloc.onBlink).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
final multiball = Multiball.test(bloc: bloc);
|
||||||
|
await multiball.add(behavior);
|
||||||
|
await game.ensureAdd(multiball);
|
||||||
|
|
||||||
|
streamController.add(
|
||||||
|
MultiballState(
|
||||||
|
animationState: MultiballAnimationState.blinking,
|
||||||
|
lightState: MultiballLightState.lit,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
streamController.add(
|
||||||
|
MultiballState(
|
||||||
|
animationState: MultiballAnimationState.idle,
|
||||||
|
lightState: MultiballLightState.lit,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await streamController.close();
|
||||||
|
verify(bloc.onStop).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.testGameWidget(
|
||||||
|
'onTick stops when there is no animation',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
final behavior = MultiballBlinkingBehavior();
|
||||||
|
final bloc = MockMultiballCubit();
|
||||||
|
final streamController = StreamController<MultiballState>();
|
||||||
|
whenListen(
|
||||||
|
bloc,
|
||||||
|
streamController.stream,
|
||||||
|
initialState: MultiballState.initial(),
|
||||||
|
);
|
||||||
|
when(bloc.onBlink).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
final multiball = Multiball.test(bloc: bloc);
|
||||||
|
await multiball.add(behavior);
|
||||||
|
await game.ensureAdd(multiball);
|
||||||
|
|
||||||
|
streamController.add(
|
||||||
|
MultiballState(
|
||||||
|
animationState: MultiballAnimationState.idle,
|
||||||
|
lightState: MultiballLightState.lit,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
behavior.onTick();
|
||||||
|
|
||||||
|
expect(behavior.timer.isRunning(), false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.testGameWidget(
|
||||||
|
'onTick stops after 10 blinks repetitions',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
final behavior = MultiballBlinkingBehavior();
|
||||||
|
final bloc = MockMultiballCubit();
|
||||||
|
final streamController = StreamController<MultiballState>();
|
||||||
|
whenListen(
|
||||||
|
bloc,
|
||||||
|
streamController.stream,
|
||||||
|
initialState: MultiballState.initial(),
|
||||||
|
);
|
||||||
|
when(bloc.onBlink).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
final multiball = Multiball.test(bloc: bloc);
|
||||||
|
await multiball.add(behavior);
|
||||||
|
await game.ensureAdd(multiball);
|
||||||
|
|
||||||
|
streamController.add(
|
||||||
|
MultiballState(
|
||||||
|
animationState: MultiballAnimationState.blinking,
|
||||||
|
lightState: MultiballLightState.dimmed,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
for (var i = 0; i < 10; i++) {
|
||||||
|
behavior.onTick();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(behavior.timer.isRunning(), false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
import 'package:bloc_test/bloc_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group(
|
||||||
|
'MultiballCubit',
|
||||||
|
() {
|
||||||
|
blocTest<MultiballCubit, MultiballState>(
|
||||||
|
'onAnimate emits animationState [animate]',
|
||||||
|
build: MultiballCubit.new,
|
||||||
|
act: (bloc) => bloc.onAnimate(),
|
||||||
|
expect: () => [
|
||||||
|
isA<MultiballState>()
|
||||||
|
..having(
|
||||||
|
(state) => state.animationState,
|
||||||
|
'animationState',
|
||||||
|
MultiballAnimationState.blinking,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
blocTest<MultiballCubit, MultiballState>(
|
||||||
|
'onStop emits animationState [stopped]',
|
||||||
|
build: MultiballCubit.new,
|
||||||
|
act: (bloc) => bloc.onStop(),
|
||||||
|
expect: () => [
|
||||||
|
isA<MultiballState>()
|
||||||
|
..having(
|
||||||
|
(state) => state.animationState,
|
||||||
|
'animationState',
|
||||||
|
MultiballAnimationState.idle,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
blocTest<MultiballCubit, MultiballState>(
|
||||||
|
'onBlink emits lightState [lit, dimmed, lit]',
|
||||||
|
build: MultiballCubit.new,
|
||||||
|
act: (bloc) => bloc
|
||||||
|
..onBlink()
|
||||||
|
..onBlink()
|
||||||
|
..onBlink(),
|
||||||
|
expect: () => [
|
||||||
|
isA<MultiballState>()
|
||||||
|
..having(
|
||||||
|
(state) => state.lightState,
|
||||||
|
'lightState',
|
||||||
|
MultiballLightState.lit,
|
||||||
|
),
|
||||||
|
isA<MultiballState>()
|
||||||
|
..having(
|
||||||
|
(state) => state.lightState,
|
||||||
|
'lightState',
|
||||||
|
MultiballLightState.dimmed,
|
||||||
|
),
|
||||||
|
isA<MultiballState>()
|
||||||
|
..having(
|
||||||
|
(state) => state.lightState,
|
||||||
|
'lightState',
|
||||||
|
MultiballLightState.lit,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
// ignore_for_file: prefer_const_constructors
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball_components/src/pinball_components.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('MultiballState', () {
|
||||||
|
test('supports value equality', () {
|
||||||
|
expect(
|
||||||
|
MultiballState(
|
||||||
|
animationState: MultiballAnimationState.idle,
|
||||||
|
lightState: MultiballLightState.dimmed,
|
||||||
|
),
|
||||||
|
equals(
|
||||||
|
MultiballState(
|
||||||
|
animationState: MultiballAnimationState.idle,
|
||||||
|
lightState: MultiballLightState.dimmed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('constructor', () {
|
||||||
|
test('can be instantiated', () {
|
||||||
|
expect(
|
||||||
|
MultiballState(
|
||||||
|
animationState: MultiballAnimationState.idle,
|
||||||
|
lightState: MultiballLightState.dimmed,
|
||||||
|
),
|
||||||
|
isNotNull,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('copyWith', () {
|
||||||
|
test(
|
||||||
|
'copies correctly '
|
||||||
|
'when no argument specified',
|
||||||
|
() {
|
||||||
|
final multiballState = MultiballState(
|
||||||
|
animationState: MultiballAnimationState.idle,
|
||||||
|
lightState: MultiballLightState.dimmed,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
multiballState.copyWith(),
|
||||||
|
equals(multiballState),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'copies correctly '
|
||||||
|
'when all arguments specified',
|
||||||
|
() {
|
||||||
|
final multiballState = MultiballState(
|
||||||
|
animationState: MultiballAnimationState.idle,
|
||||||
|
lightState: MultiballLightState.dimmed,
|
||||||
|
);
|
||||||
|
final otherMultiballState = MultiballState(
|
||||||
|
animationState: MultiballAnimationState.blinking,
|
||||||
|
lightState: MultiballLightState.lit,
|
||||||
|
);
|
||||||
|
expect(multiballState, isNot(equals(otherMultiballState)));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
multiballState.copyWith(
|
||||||
|
animationState: MultiballAnimationState.blinking,
|
||||||
|
lightState: MultiballLightState.lit,
|
||||||
|
),
|
||||||
|
equals(otherMultiballState),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:bloc_test/bloc_test.dart';
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart';
|
||||||
|
|
||||||
|
import '../../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final assets = [
|
||||||
|
Assets.images.multiball.lit.keyName,
|
||||||
|
Assets.images.multiball.dimmed.keyName,
|
||||||
|
];
|
||||||
|
final flameTester = FlameTester(() => TestGame(assets));
|
||||||
|
|
||||||
|
group('Multiball', () {
|
||||||
|
group('loads correctly', () {
|
||||||
|
flameTester.test('"a"', (game) async {
|
||||||
|
final multiball = Multiball.a();
|
||||||
|
await game.ensureAdd(multiball);
|
||||||
|
|
||||||
|
expect(game.contains(multiball), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('"b"', (game) async {
|
||||||
|
final multiball = Multiball.b();
|
||||||
|
await game.ensureAdd(multiball);
|
||||||
|
expect(game.contains(multiball), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('"c"', (game) async {
|
||||||
|
final multiball = Multiball.c();
|
||||||
|
await game.ensureAdd(multiball);
|
||||||
|
|
||||||
|
expect(game.contains(multiball), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('"d"', (game) async {
|
||||||
|
final multiball = Multiball.d();
|
||||||
|
await game.ensureAdd(multiball);
|
||||||
|
expect(game.contains(multiball), isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'closes bloc when removed',
|
||||||
|
(game) async {
|
||||||
|
final bloc = MockMultiballCubit();
|
||||||
|
whenListen(
|
||||||
|
bloc,
|
||||||
|
const Stream<MultiballLightState>.empty(),
|
||||||
|
initialState: MultiballLightState.dimmed,
|
||||||
|
);
|
||||||
|
when(bloc.close).thenAnswer((_) async {});
|
||||||
|
final multiball = Multiball.test(bloc: bloc);
|
||||||
|
|
||||||
|
await game.ensureAdd(multiball);
|
||||||
|
game.remove(multiball);
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
verify(bloc.close).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('adds', () {
|
||||||
|
flameTester.test('new children', (game) async {
|
||||||
|
final component = Component();
|
||||||
|
final multiball = Multiball.a(
|
||||||
|
children: [component],
|
||||||
|
);
|
||||||
|
await game.ensureAdd(multiball);
|
||||||
|
expect(multiball.children, contains(component));
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('a MultiballBlinkingBehavior', (game) async {
|
||||||
|
final multiball = Multiball.a();
|
||||||
|
await game.ensureAdd(multiball);
|
||||||
|
expect(
|
||||||
|
multiball.children.whereType<MultiballBlinkingBehavior>().single,
|
||||||
|
isNotNull,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,202 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/effects.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 assets = [
|
||||||
|
Assets.images.score.fiveThousand.keyName,
|
||||||
|
Assets.images.score.twentyThousand.keyName,
|
||||||
|
Assets.images.score.twoHundredThousand.keyName,
|
||||||
|
Assets.images.score.oneMillion.keyName,
|
||||||
|
];
|
||||||
|
final flameTester = FlameTester(() => TestGame(assets));
|
||||||
|
|
||||||
|
group('ScoreComponent', () {
|
||||||
|
flameTester.testGameWidget(
|
||||||
|
'loads correctly',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
await game.images.loadAll(assets);
|
||||||
|
game.camera.followVector2(Vector2.zero());
|
||||||
|
await game.ensureAdd(
|
||||||
|
ScoreComponent(
|
||||||
|
points: Points.oneMillion,
|
||||||
|
position: Vector2.zero(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
verify: (game, tester) async {
|
||||||
|
final texts = game.descendants().whereType<SpriteComponent>().length;
|
||||||
|
expect(texts, equals(1));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.testGameWidget(
|
||||||
|
'has a movement effect',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
await game.images.loadAll(assets);
|
||||||
|
game.camera.followVector2(Vector2.zero());
|
||||||
|
await game.ensureAdd(
|
||||||
|
ScoreComponent(
|
||||||
|
points: Points.oneMillion,
|
||||||
|
position: Vector2.zero(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
game.update(0.5);
|
||||||
|
await tester.pump();
|
||||||
|
},
|
||||||
|
verify: (game, tester) async {
|
||||||
|
final text = game.descendants().whereType<SpriteComponent>().first;
|
||||||
|
expect(text.firstChild<MoveEffect>(), isNotNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.testGameWidget(
|
||||||
|
'is removed once finished',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
await game.images.loadAll(assets);
|
||||||
|
game.camera.followVector2(Vector2.zero());
|
||||||
|
await game.ensureAdd(
|
||||||
|
ScoreComponent(
|
||||||
|
points: Points.oneMillion,
|
||||||
|
position: Vector2.zero(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
game.update(1);
|
||||||
|
game.update(0); // Ensure all component removals
|
||||||
|
await tester.pump();
|
||||||
|
},
|
||||||
|
verify: (game, tester) async {
|
||||||
|
expect(game.children.length, equals(0));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('renders correctly', () {
|
||||||
|
flameTester.testGameWidget(
|
||||||
|
'5000 points',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
await game.images.loadAll(assets);
|
||||||
|
await game.ensureAdd(
|
||||||
|
ScoreComponent(
|
||||||
|
points: Points.fiveThousand,
|
||||||
|
position: Vector2.zero(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
game.camera
|
||||||
|
..followVector2(Vector2.zero())
|
||||||
|
..zoom = 8;
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
},
|
||||||
|
verify: (game, tester) async {
|
||||||
|
await expectLater(
|
||||||
|
find.byGame<TestGame>(),
|
||||||
|
matchesGoldenFile('golden/score/5k.png'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.testGameWidget(
|
||||||
|
'20000 points',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
await game.images.loadAll(assets);
|
||||||
|
await game.ensureAdd(
|
||||||
|
ScoreComponent(
|
||||||
|
points: Points.twentyThousand,
|
||||||
|
position: Vector2.zero(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
game.camera
|
||||||
|
..followVector2(Vector2.zero())
|
||||||
|
..zoom = 8;
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
},
|
||||||
|
verify: (game, tester) async {
|
||||||
|
await expectLater(
|
||||||
|
find.byGame<TestGame>(),
|
||||||
|
matchesGoldenFile('golden/score/20k.png'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.testGameWidget(
|
||||||
|
'200000 points',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
await game.images.loadAll(assets);
|
||||||
|
await game.ensureAdd(
|
||||||
|
ScoreComponent(
|
||||||
|
points: Points.twoHundredThousand,
|
||||||
|
position: Vector2.zero(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
game.camera
|
||||||
|
..followVector2(Vector2.zero())
|
||||||
|
..zoom = 8;
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
},
|
||||||
|
verify: (game, tester) async {
|
||||||
|
await expectLater(
|
||||||
|
find.byGame<TestGame>(),
|
||||||
|
matchesGoldenFile('golden/score/200k.png'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.testGameWidget(
|
||||||
|
'1000000 points',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
await game.images.loadAll(assets);
|
||||||
|
await game.ensureAdd(
|
||||||
|
ScoreComponent(
|
||||||
|
points: Points.oneMillion,
|
||||||
|
position: Vector2.zero(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
game.camera
|
||||||
|
..followVector2(Vector2.zero())
|
||||||
|
..zoom = 8;
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
},
|
||||||
|
verify: (game, tester) async {
|
||||||
|
await expectLater(
|
||||||
|
find.byGame<TestGame>(),
|
||||||
|
matchesGoldenFile('golden/score/1m.png'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('PointsX', () {
|
||||||
|
test('5k value return 5000', () {
|
||||||
|
expect(Points.fiveThousand.value, 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('20k value return 20000', () {
|
||||||
|
expect(Points.twentyThousand.value, 20000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('200k value return 200000', () {
|
||||||
|
expect(Points.twoHundredThousand.value, 200000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('1m value return 1000000', () {
|
||||||
|
expect(Points.oneMillion.value, 1000000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -1,75 +0,0 @@
|
|||||||
// ignore_for_file: cascade_invocations
|
|
||||||
|
|
||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flame/effects.dart';
|
|
||||||
import 'package:flame_test/flame_test.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart';
|
|
||||||
|
|
||||||
import '../../helpers/helpers.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('ScoreText', () {
|
|
||||||
final flameTester = FlameTester(TestGame.new);
|
|
||||||
|
|
||||||
flameTester.testGameWidget(
|
|
||||||
'renders correctly',
|
|
||||||
setUp: (game, tester) async {
|
|
||||||
game.camera.followVector2(Vector2.zero());
|
|
||||||
await game.ensureAdd(
|
|
||||||
ScoreText(
|
|
||||||
text: '123',
|
|
||||||
position: Vector2.zero(),
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
verify: (game, tester) async {
|
|
||||||
final texts = game.descendants().whereType<TextComponent>().length;
|
|
||||||
expect(texts, equals(1));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
flameTester.testGameWidget(
|
|
||||||
'has a movement effect',
|
|
||||||
setUp: (game, tester) async {
|
|
||||||
game.camera.followVector2(Vector2.zero());
|
|
||||||
await game.ensureAdd(
|
|
||||||
ScoreText(
|
|
||||||
text: '123',
|
|
||||||
position: Vector2.zero(),
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
game.update(0.5);
|
|
||||||
await tester.pump();
|
|
||||||
},
|
|
||||||
verify: (game, tester) async {
|
|
||||||
final text = game.descendants().whereType<TextComponent>().first;
|
|
||||||
expect(text.firstChild<MoveEffect>(), isNotNull);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
flameTester.testGameWidget(
|
|
||||||
'is removed once finished',
|
|
||||||
setUp: (game, tester) async {
|
|
||||||
game.camera.followVector2(Vector2.zero());
|
|
||||||
await game.ensureAdd(
|
|
||||||
ScoreText(
|
|
||||||
text: '123',
|
|
||||||
position: Vector2.zero(),
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
game.update(1);
|
|
||||||
game.update(0); // Ensure all component removals
|
|
||||||
},
|
|
||||||
verify: (game, tester) async {
|
|
||||||
expect(game.children.length, equals(0));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
@ -0,0 +1,9 @@
|
|||||||
|
rules_version = '2';
|
||||||
|
service firebase.storage {
|
||||||
|
match /b/{bucket}/o {
|
||||||
|
match /{folder}/{imageId} {
|
||||||
|
allow read: if imageId.matches(".*\\.png") || imageId.matches(".*\\.jpg");
|
||||||
|
allow write: if false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,136 @@
|
|||||||
|
// ignore_for_file: cascade_invocations, prefer_const_constructors
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
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/components/multiballs/behaviors/behaviors.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
import '../../../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final assets = [
|
||||||
|
Assets.images.multiball.lit.keyName,
|
||||||
|
Assets.images.multiball.dimmed.keyName,
|
||||||
|
];
|
||||||
|
|
||||||
|
group('MultiballsBehavior', () {
|
||||||
|
late GameBloc gameBloc;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
gameBloc = MockGameBloc();
|
||||||
|
whenListen(
|
||||||
|
gameBloc,
|
||||||
|
const Stream<GameState>.empty(),
|
||||||
|
initialState: const GameState.initial(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
|
||||||
|
gameBuilder: EmptyPinballTestGame.new,
|
||||||
|
blocBuilder: () => gameBloc,
|
||||||
|
assets: assets,
|
||||||
|
);
|
||||||
|
|
||||||
|
group('listenWhen', () {
|
||||||
|
test(
|
||||||
|
'is true when the bonusHistory has changed '
|
||||||
|
'with a new GameBonus.dashNest', () {
|
||||||
|
final previous = GameState.initial();
|
||||||
|
final state = previous.copyWith(
|
||||||
|
bonusHistory: [GameBonus.dashNest],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
MultiballsBehavior().listenWhen(previous, state),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'is false when the bonusHistory has changed '
|
||||||
|
'with a bonus different than GameBonus.dashNest', () {
|
||||||
|
final previous =
|
||||||
|
GameState.initial().copyWith(bonusHistory: [GameBonus.dashNest]);
|
||||||
|
final state = previous.copyWith(
|
||||||
|
bonusHistory: [...previous.bonusHistory, GameBonus.androidSpaceship],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
MultiballsBehavior().listenWhen(previous, state),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('is false when the bonusHistory state is the same', () {
|
||||||
|
final previous = GameState.initial();
|
||||||
|
final state = GameState(
|
||||||
|
score: 10,
|
||||||
|
multiplier: 1,
|
||||||
|
rounds: 0,
|
||||||
|
bonusHistory: const [],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
MultiballsBehavior().listenWhen(previous, state),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('onNewState', () {
|
||||||
|
flameBlocTester.testGameWidget(
|
||||||
|
"calls 'onAnimate' once for every multiball",
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
final behavior = MultiballsBehavior();
|
||||||
|
final parent = Multiballs.test();
|
||||||
|
final multiballCubit = MockMultiballCubit();
|
||||||
|
final otherMultiballCubit = MockMultiballCubit();
|
||||||
|
final multiballs = [
|
||||||
|
Multiball.test(
|
||||||
|
bloc: multiballCubit,
|
||||||
|
),
|
||||||
|
Multiball.test(
|
||||||
|
bloc: otherMultiballCubit,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
whenListen(
|
||||||
|
multiballCubit,
|
||||||
|
const Stream<MultiballState>.empty(),
|
||||||
|
initialState: MultiballState.initial(),
|
||||||
|
);
|
||||||
|
when(multiballCubit.onAnimate).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
whenListen(
|
||||||
|
otherMultiballCubit,
|
||||||
|
const Stream<MultiballState>.empty(),
|
||||||
|
initialState: MultiballState.initial(),
|
||||||
|
);
|
||||||
|
when(otherMultiballCubit.onAnimate).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await parent.addAll(multiballs);
|
||||||
|
await game.ensureAdd(parent);
|
||||||
|
await parent.ensureAdd(behavior);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
behavior.onNewState(
|
||||||
|
GameState.initial().copyWith(bonusHistory: [GameBonus.dashNest]),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final multiball in multiballs) {
|
||||||
|
verify(
|
||||||
|
multiball.bloc.onAnimate,
|
||||||
|
).called(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
import '../../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final assets = [
|
||||||
|
Assets.images.multiball.lit.keyName,
|
||||||
|
Assets.images.multiball.dimmed.keyName,
|
||||||
|
];
|
||||||
|
late GameBloc gameBloc;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
gameBloc = GameBloc();
|
||||||
|
});
|
||||||
|
|
||||||
|
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
|
||||||
|
gameBuilder: EmptyPinballTestGame.new,
|
||||||
|
blocBuilder: () => gameBloc,
|
||||||
|
assets: assets,
|
||||||
|
);
|
||||||
|
|
||||||
|
group('Multiballs', () {
|
||||||
|
flameBlocTester.testGameWidget(
|
||||||
|
'loads correctly',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
final multiballs = Multiballs();
|
||||||
|
await game.ensureAdd(multiballs);
|
||||||
|
|
||||||
|
expect(game.contains(multiballs), isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('loads', () {
|
||||||
|
flameBlocTester.testGameWidget(
|
||||||
|
'four Multiball',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
final multiballs = Multiballs();
|
||||||
|
await game.ensureAdd(multiballs);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
multiballs.descendants().whereType<Multiball>().length,
|
||||||
|
equals(4),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'helpers.dart';
|
|
||||||
|
|
||||||
Future<void> expectNavigatesToRoute<Type>(
|
|
||||||
WidgetTester tester,
|
|
||||||
Route route, {
|
|
||||||
bool hasFlameGameInside = false,
|
|
||||||
}) async {
|
|
||||||
// ignore: avoid_dynamic_calls
|
|
||||||
await tester.pumpApp(
|
|
||||||
Scaffold(
|
|
||||||
body: Builder(
|
|
||||||
builder: (context) {
|
|
||||||
return ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).push<void>(route);
|
|
||||||
},
|
|
||||||
child: const Text('Tap me'),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.tap(find.text('Tap me'));
|
|
||||||
if (hasFlameGameInside) {
|
|
||||||
// We can't use pumpAndSettle here because the page renders a Flame game
|
|
||||||
// which is an infinity animation, so it will timeout
|
|
||||||
await tester.pump(); // Runs the button action
|
|
||||||
await tester.pump(); // Runs the navigation
|
|
||||||
} else {
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(find.byType(Type), findsOneWidget);
|
|
||||||
}
|
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 917 B |