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;
|
library pinball_components;
|
||||||
|
|
||||||
export 'gen/assets.gen.dart';
|
export 'gen/assets.gen.dart';
|
||||||
|
export 'gen/pinball_fonts.dart';
|
||||||
export 'src/pinball_components.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