feat: adding score text effects (#170)

* feat: adding score effects

* feat: adding score text effect

* feat: finishing score text effects

* fixing tests

* feat: pr suggestion

* feat: pr suggestion

* Update lib/game/components/score_effect_controller.dart

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

* Update packages/pinball_components/lib/src/components/score_text_effect.dart

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

* pr suggestions

* more pr suggestions

* nit

* Apply suggestions from code review

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

Co-authored-by: Alejandro Santiago <dev@alestiago.com>
pull/177/head
Erick 2 years ago committed by GitHub
parent d32192a4e3
commit 1fd6d9bf46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,6 +7,7 @@ export 'controlled_sparky_computer.dart';
export 'flutter_forest.dart';
export 'game_flow_controller.dart';
export 'plunger.dart';
export 'score_effect_controller.dart';
export 'score_points.dart';
export 'sparky_fire_zone.dart';
export 'wall.dart';

@ -0,0 +1,45 @@
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/flame/flame.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template score_effect_controller}
/// A [ComponentController] responsible for adding [ScoreText]s
/// on the game screen when the user earns points.
/// {@endtemplate}
class ScoreEffectController extends ComponentController<PinballGame>
with BlocComponent<GameBloc, GameState> {
/// {@macro score_effect_controller}
ScoreEffectController(PinballGame component) : super(component);
int _lastScore = 0;
final _random = Random();
double _noise() {
return _random.nextDouble() * 5 * (_random.nextBool() ? -1 : 1);
}
@override
bool listenWhen(GameState? previousState, GameState newState) {
return previousState?.score != newState.score;
}
@override
void onNewState(GameState state) {
final newScore = state.score - _lastScore;
_lastScore = state.score;
component.add(
ScoreText(
text: newScore.toString(),
position: Vector2(
_noise(),
_noise() + (-BoardDimensions.bounds.topCenter.dy + 10),
),
),
);
}
}

@ -38,6 +38,7 @@ class PinballGame extends Forge2DGame
Future<void> onLoad() async {
_addContactCallbacks();
unawaited(add(ScoreEffectController(this)));
unawaited(add(gameFlowController = GameFlowController(this)));
unawaited(add(CameraController(this)));
unawaited(add(Backboard(position: Vector2(0, -88))));

@ -3,8 +3,6 @@
/// FlutterGen
/// *****************************************************
// ignore_for_file: directives_ordering,unnecessary_import
import 'package:flutter/widgets.dart';
class $AssetsImagesGen {
@ -17,11 +15,8 @@ class $AssetsImagesGen {
class $AssetsImagesComponentsGen {
const $AssetsImagesComponentsGen();
/// File path: assets/images/components/background.png
AssetGenImage get background =>
const AssetGenImage('assets/images/components/background.png');
/// File path: assets/images/components/plunger.png
AssetGenImage get plunger =>
const AssetGenImage('assets/images/components/plunger.png');
}

@ -0,0 +1,11 @@
/// GENERATED CODE - DO NOT MODIFY BY HAND
/// *****************************************************
/// FlutterGen
/// *****************************************************
class FontFamily {
FontFamily._();
static const String pixeloidMono = 'PixeloidMono';
static const String pixeloidSans = 'PixeloidSans';
}

@ -0,0 +1,16 @@
import 'package:pinball_components/gen/fonts.gen.dart';
String _prefixFont(String font) {
return 'packages/pinball_components/$font';
}
/// Class with the fonts available on the pinball game
class PinballFonts {
PinballFonts._();
/// Mono variation of the Pixeloid font
static final String pixeloidMono = _prefixFont(FontFamily.pixeloidMono);
/// Sans variation of the Pixeloid font
static final String pixeloidSans = _prefixFont(FontFamily.pixeloidMono);
}

@ -1,4 +1,5 @@
library pinball_components;
export 'gen/assets.gen.dart';
export 'gen/pinball_fonts.dart';
export 'src/pinball_components.dart';

@ -17,6 +17,7 @@ export 'kicker.dart';
export 'launch_ramp.dart';
export 'layer.dart';
export 'ramp_opening.dart';
export 'score_text.dart';
export 'shapes/shapes.dart';
export 'slingshot.dart';
export 'spaceship.dart';

@ -0,0 +1,55 @@
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';
/// {@template score_text}
/// A [TextComponent] that spawns at a given [position] with a moving animation.
/// {@endtemplate}
class ScoreText extends TextComponent {
/// {@macro score_text}
ScoreText({
required String text,
required Vector2 position,
this.color = Colors.black,
}) : super(
text: text,
position: position,
anchor: Anchor.center,
priority: 100,
);
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();
}
}
}

@ -11,7 +11,7 @@ dependencies:
flame_forge2d: ^0.10.0
flutter:
sdk: flutter
geometry:
geometry:
path: ../geometry
@ -24,6 +24,16 @@ dev_dependencies:
flutter:
generate: true
fonts:
- family: PixeloidSans
fonts:
- asset: fonts/PixeloidSans-nR3g1.ttf
- asset: fonts/PixeloidSansBold-RpeJo.ttf
weight: 700
- family: PixeloidMono
fonts:
- asset: fonts/PixeloidMono-1G8ae.ttf
assets:
- assets/images/
- assets/images/baseboard/

@ -28,5 +28,7 @@ void main() {
addSpaceshipRampStories(dashbook);
addSpaceshipRailStories(dashbook);
addLaunchRampStories(dashbook);
addScoreTextStories(dashbook);
runApp(dashbook);
}

@ -0,0 +1,32 @@
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 ScoreTextBasicGame extends BasicGame with TapDetector {
static const info = '''
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)),
),
);
}
}

@ -0,0 +1,15 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/score_text/basic.dart';
void addScoreTextStories(Dashbook dashbook) {
dashbook.storiesOf('ScoreText').add(
'Basic',
(context) => GameWidget(
game: ScoreTextBasicGame(),
),
codeLink: buildSourceLink('score_text/basic.dart'),
info: ScoreTextBasicGame.info,
);
}

@ -7,6 +7,7 @@ export 'flipper/stories.dart';
export 'flutter_forest/stories.dart';
export 'launch_ramp/stories.dart';
export 'layer/stories.dart';
export 'score_text/stories.dart';
export 'slingshot/stories.dart';
export 'spaceship/stories.dart';
export 'spaceship_rail/stories.dart';

@ -0,0 +1,75 @@
// 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,115 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
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 '../../helpers/helpers.dart';
void main() {
group('ScoreEffectController', () {
late ScoreEffectController controller;
late PinballGame game;
setUpAll(() {
registerFallbackValue(Component());
});
setUp(() {
game = MockPinballGame();
when(() => game.add(any())).thenAnswer((_) async {});
controller = ScoreEffectController(game);
});
group('listenWhen', () {
test('returns true when the user has earned points', () {
const previous = GameState.initial();
const current = GameState(
score: 10,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [],
activatedDashNests: {},
);
expect(controller.listenWhen(previous, current), isTrue);
});
test(
'returns true when the user has earned points and there was no '
'previous state',
() {
const current = GameState(
score: 10,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [],
activatedDashNests: {},
);
expect(controller.listenWhen(null, current), isTrue);
},
);
test(
'returns false when no points were earned',
() {
const current = GameState.initial();
const previous = GameState.initial();
expect(controller.listenWhen(previous, current), isFalse);
},
);
});
group('onNewState', () {
test(
'adds a ScoreText with the correct score for the '
'first time',
() {
const state = GameState(
score: 10,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [],
activatedDashNests: {},
);
controller.onNewState(state);
final effect =
verify(() => game.add(captureAny())).captured.first as ScoreText;
expect(effect.text, equals('10'));
},
);
test('adds a ScoreTextEffect with the correct score', () {
controller.onNewState(
const GameState(
score: 10,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [],
activatedDashNests: {},
),
);
controller.onNewState(
const GameState(
score: 14,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [],
activatedDashNests: {},
),
);
final effect =
verify(() => game.add(captureAny())).captured.last as ScoreText;
expect(effect.text, equals('4'));
});
});
});
}
Loading…
Cancel
Save