Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 200 KiB |
@ -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 'selected_character.dart';
|
||||
|
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,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,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,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));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
@ -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);
|
||||
}
|