mirror of https://github.com/flutter/pinball.git
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
parent
d32192a4e3
commit
1fd6d9bf46
@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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';
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
@ -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…
Reference in new issue