mirror of https://github.com/flutter/pinball.git
commit
0b53c4eb3f
@ -1,2 +1,3 @@
|
|||||||
export 'bloc/leaderboard_bloc.dart';
|
export 'bloc/leaderboard_bloc.dart';
|
||||||
export 'models/leader_board_entry.dart';
|
export 'models/leader_board_entry.dart';
|
||||||
|
export 'view/leaderboard_page.dart';
|
||||||
|
@ -0,0 +1,306 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||||
|
import 'package:pinball/l10n/l10n.dart';
|
||||||
|
import 'package:pinball/leaderboard/leaderboard.dart';
|
||||||
|
import 'package:pinball/theme/theme.dart';
|
||||||
|
import 'package:pinball_theme/pinball_theme.dart';
|
||||||
|
|
||||||
|
class LeaderboardPage extends StatelessWidget {
|
||||||
|
const LeaderboardPage({Key? key, required this.theme}) : super(key: key);
|
||||||
|
|
||||||
|
final CharacterTheme theme;
|
||||||
|
|
||||||
|
static Route route({required CharacterTheme theme}) {
|
||||||
|
return MaterialPageRoute<void>(
|
||||||
|
builder: (_) => LeaderboardPage(theme: theme),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => LeaderboardBloc(
|
||||||
|
context.read<LeaderboardRepository>(),
|
||||||
|
)..add(const Top10Fetched()),
|
||||||
|
child: LeaderboardView(theme: theme),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LeaderboardView extends StatelessWidget {
|
||||||
|
const LeaderboardView({Key? key, required this.theme}) : super(key: key);
|
||||||
|
|
||||||
|
final CharacterTheme theme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 80),
|
||||||
|
Text(
|
||||||
|
l10n.leaderboard,
|
||||||
|
style: Theme.of(context).textTheme.headline3,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 80),
|
||||||
|
BlocBuilder<LeaderboardBloc, LeaderboardState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
switch (state.status) {
|
||||||
|
case LeaderboardStatus.loading:
|
||||||
|
return _LeaderboardLoading(theme: theme);
|
||||||
|
case LeaderboardStatus.success:
|
||||||
|
return _LeaderboardRanking(
|
||||||
|
ranking: state.leaderboard,
|
||||||
|
theme: theme,
|
||||||
|
);
|
||||||
|
case LeaderboardStatus.error:
|
||||||
|
return _LeaderboardError(theme: theme);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).push<void>(
|
||||||
|
CharacterSelectionPage.route(),
|
||||||
|
),
|
||||||
|
child: Text(l10n.retry),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LeaderboardLoading extends StatelessWidget {
|
||||||
|
const _LeaderboardLoading({Key? key, required this.theme}) : super(key: key);
|
||||||
|
|
||||||
|
final CharacterTheme theme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LeaderboardError extends StatelessWidget {
|
||||||
|
const _LeaderboardError({Key? key, required this.theme}) : super(key: key);
|
||||||
|
|
||||||
|
final CharacterTheme theme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Text(
|
||||||
|
'There was en error loading data!',
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.headline6?.copyWith(color: Colors.red),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LeaderboardRanking extends StatelessWidget {
|
||||||
|
const _LeaderboardRanking({
|
||||||
|
Key? key,
|
||||||
|
required this.ranking,
|
||||||
|
required this.theme,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final List<LeaderboardEntry> ranking;
|
||||||
|
final CharacterTheme theme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_LeaderboardHeaders(theme: theme),
|
||||||
|
_LeaderboardList(
|
||||||
|
ranking: ranking,
|
||||||
|
theme: theme,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LeaderboardHeaders extends StatelessWidget {
|
||||||
|
const _LeaderboardHeaders({Key? key, required this.theme}) : super(key: key);
|
||||||
|
|
||||||
|
final CharacterTheme theme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_LeaderboardHeaderItem(title: l10n.rank, theme: theme),
|
||||||
|
_LeaderboardHeaderItem(title: l10n.character, theme: theme),
|
||||||
|
_LeaderboardHeaderItem(title: l10n.username, theme: theme),
|
||||||
|
_LeaderboardHeaderItem(title: l10n.score, theme: theme),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LeaderboardHeaderItem extends StatelessWidget {
|
||||||
|
const _LeaderboardHeaderItem({
|
||||||
|
Key? key,
|
||||||
|
required this.title,
|
||||||
|
required this.theme,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final CharacterTheme theme;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Expanded(
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.ballColor,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: Theme.of(context).textTheme.headline5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LeaderboardList extends StatelessWidget {
|
||||||
|
const _LeaderboardList({
|
||||||
|
Key? key,
|
||||||
|
required this.ranking,
|
||||||
|
required this.theme,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final List<LeaderboardEntry> ranking;
|
||||||
|
final CharacterTheme theme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemBuilder: (_, index) => _LeaderBoardCompetitor(
|
||||||
|
entry: ranking[index],
|
||||||
|
theme: theme,
|
||||||
|
),
|
||||||
|
itemCount: ranking.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LeaderBoardCompetitor extends StatelessWidget {
|
||||||
|
const _LeaderBoardCompetitor({
|
||||||
|
Key? key,
|
||||||
|
required this.entry,
|
||||||
|
required this.theme,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final CharacterTheme theme;
|
||||||
|
|
||||||
|
final LeaderboardEntry entry;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_LeaderboardCompetitorField(
|
||||||
|
text: entry.rank,
|
||||||
|
theme: theme,
|
||||||
|
),
|
||||||
|
_LeaderboardCompetitorCharacter(
|
||||||
|
characterAsset: entry.character,
|
||||||
|
theme: theme,
|
||||||
|
),
|
||||||
|
_LeaderboardCompetitorField(
|
||||||
|
text: entry.playerInitials,
|
||||||
|
theme: theme,
|
||||||
|
),
|
||||||
|
_LeaderboardCompetitorField(
|
||||||
|
text: entry.score.toString(),
|
||||||
|
theme: theme,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LeaderboardCompetitorField extends StatelessWidget {
|
||||||
|
const _LeaderboardCompetitorField({
|
||||||
|
Key? key,
|
||||||
|
required this.text,
|
||||||
|
required this.theme,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final CharacterTheme theme;
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Expanded(
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.ballColor,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Text(text),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LeaderboardCompetitorCharacter extends StatelessWidget {
|
||||||
|
const _LeaderboardCompetitorCharacter({
|
||||||
|
Key? key,
|
||||||
|
required this.characterAsset,
|
||||||
|
required this.theme,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final CharacterTheme theme;
|
||||||
|
final AssetGenImage characterAsset;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Expanded(
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.ballColor,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 30,
|
||||||
|
child: characterAsset.image(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
export 'ball.dart';
|
export 'ball.dart';
|
||||||
|
export 'fire_effect.dart';
|
||||||
export 'initial_position.dart';
|
export 'initial_position.dart';
|
||||||
export 'layer.dart';
|
export 'layer.dart';
|
||||||
|
@ -0,0 +1,113 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flame/extensions.dart';
|
||||||
|
import 'package:flame/particles.dart';
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart' hide Particle;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
const _particleRadius = 0.25;
|
||||||
|
|
||||||
|
// TODO(erickzanardo): This component could just be a ParticleComponet,
|
||||||
|
/// unfortunately there is a Particle Component is not a PositionComponent,
|
||||||
|
/// which makes it hard to be used since we have camera transformations and on
|
||||||
|
// top of that, PositionComponent has a bug inside forge 2d games
|
||||||
|
///
|
||||||
|
/// https://github.com/flame-engine/flame/issues/1484
|
||||||
|
/// https://github.com/flame-engine/flame/issues/1484
|
||||||
|
|
||||||
|
/// {@template fire_effect}
|
||||||
|
/// A [BodyComponent] which creates a fire trail effect using the given
|
||||||
|
/// parameters
|
||||||
|
/// {@endtemplate}
|
||||||
|
class FireEffect extends BodyComponent {
|
||||||
|
/// {@macro fire_effect}
|
||||||
|
FireEffect({
|
||||||
|
required this.burstPower,
|
||||||
|
required this.position,
|
||||||
|
required this.direction,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// A [double] value that will define how "strong" the burst of particles
|
||||||
|
/// will be
|
||||||
|
final double burstPower;
|
||||||
|
|
||||||
|
/// The position of the burst
|
||||||
|
final Vector2 position;
|
||||||
|
|
||||||
|
/// Which direction the burst will aim
|
||||||
|
final Vector2 direction;
|
||||||
|
late Particle _particle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final bodyDef = BodyDef()..position = position;
|
||||||
|
|
||||||
|
final fixtureDef = FixtureDef(CircleShape()..radius = 0)..isSensor = true;
|
||||||
|
|
||||||
|
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
final children = [
|
||||||
|
...List.generate(4, (index) {
|
||||||
|
return CircleParticle(
|
||||||
|
radius: _particleRadius,
|
||||||
|
paint: Paint()..color = Colors.yellow.darken((index + 1) / 4),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
...List.generate(4, (index) {
|
||||||
|
return CircleParticle(
|
||||||
|
radius: _particleRadius,
|
||||||
|
paint: Paint()..color = Colors.red.darken((index + 1) / 4),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
...List.generate(4, (index) {
|
||||||
|
return CircleParticle(
|
||||||
|
radius: _particleRadius,
|
||||||
|
paint: Paint()..color = Colors.orange.darken((index + 1) / 4),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
final rng = math.Random();
|
||||||
|
final spreadTween = Tween<double>(begin: -0.2, end: 0.2);
|
||||||
|
|
||||||
|
_particle = Particle.generate(
|
||||||
|
count: (rng.nextDouble() * (burstPower * 10)).toInt(),
|
||||||
|
generator: (_) {
|
||||||
|
final spread = Vector2(
|
||||||
|
spreadTween.transform(rng.nextDouble()),
|
||||||
|
spreadTween.transform(rng.nextDouble()),
|
||||||
|
);
|
||||||
|
final finalDirection = Vector2(direction.x, -direction.y) + spread;
|
||||||
|
final speed = finalDirection * (burstPower * 20);
|
||||||
|
|
||||||
|
return AcceleratedParticle(
|
||||||
|
lifespan: 5 / burstPower,
|
||||||
|
position: Vector2.zero(),
|
||||||
|
speed: speed,
|
||||||
|
child: children[rng.nextInt(children.length)],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update(double dt) {
|
||||||
|
super.update(dt);
|
||||||
|
_particle.update(dt);
|
||||||
|
|
||||||
|
if (_particle.shouldRemove) {
|
||||||
|
removeFromParent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void render(Canvas canvas) {
|
||||||
|
super.render(canvas);
|
||||||
|
|
||||||
|
_particle.render(canvas);
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +0,0 @@
|
|||||||
<!--
|
|
||||||
Thanks for contributing!
|
|
||||||
|
|
||||||
Provide a description of your changes below and a general summary in the title
|
|
||||||
|
|
||||||
Please look at the following checklist to ensure that your PR can be accepted quickly:
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!--- Describe your changes in detail -->
|
|
||||||
|
|
||||||
## Type of Change
|
|
||||||
|
|
||||||
<!--- Put an `x` in all the boxes that apply: -->
|
|
||||||
|
|
||||||
- [ ] ✨ New feature (non-breaking change which adds functionality)
|
|
||||||
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
|
|
||||||
- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
|
|
||||||
- [ ] 🧹 Code refactor
|
|
||||||
- [ ] ✅ Build configuration change
|
|
||||||
- [ ] 📝 Documentation
|
|
||||||
- [ ] 🗑️ Chore
|
|
@ -1,10 +0,0 @@
|
|||||||
name: sandbox
|
|
||||||
|
|
||||||
on: [pull_request, push]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
|
|
||||||
with:
|
|
||||||
flutter_channel: stable
|
|
||||||
flutter_version: 2.10.0
|
|
@ -1,11 +1,2 @@
|
|||||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
export 'games.dart';
|
||||||
|
export 'methods.dart';
|
||||||
String buildSourceLink(String path) {
|
|
||||||
return 'https://github.com/VGVentures/pinball/tree/main/packages/pinball_components/sandbox/lib/stories/$path';
|
|
||||||
}
|
|
||||||
|
|
||||||
class BasicGame extends Forge2DGame {
|
|
||||||
BasicGame() {
|
|
||||||
images.prefix = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/input.dart';
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class BasicGame extends Forge2DGame {
|
||||||
|
BasicGame() {
|
||||||
|
images.prefix = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class LineGame extends BasicGame with PanDetector {
|
||||||
|
Vector2? _lineEnd;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
camera.followVector2(Vector2.zero());
|
||||||
|
unawaited(add(_PreviewLine()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onPanStart(DragStartInfo info) {
|
||||||
|
_lineEnd = info.eventPosition.game;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onPanUpdate(DragUpdateInfo info) {
|
||||||
|
_lineEnd = info.eventPosition.game;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onPanEnd(DragEndInfo info) {
|
||||||
|
if (_lineEnd != null) {
|
||||||
|
final line = _lineEnd! - Vector2.zero();
|
||||||
|
onLine(line);
|
||||||
|
_lineEnd = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onLine(Vector2 line);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PreviewLine extends PositionComponent with HasGameRef<LineGame> {
|
||||||
|
static final _previewLinePaint = Paint()
|
||||||
|
..color = Colors.pink
|
||||||
|
..strokeWidth = 0.2
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
Vector2? lineEnd;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update(double dt) {
|
||||||
|
super.update(dt);
|
||||||
|
|
||||||
|
lineEnd = gameRef._lineEnd?.clone()?..multiply(Vector2(1, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void render(Canvas canvas) {
|
||||||
|
super.render(canvas);
|
||||||
|
|
||||||
|
if (lineEnd != null) {
|
||||||
|
canvas.drawLine(
|
||||||
|
Vector2.zero().toOffset(),
|
||||||
|
lineEnd!.toOffset(),
|
||||||
|
_previewLinePaint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
String buildSourceLink(String path) {
|
||||||
|
return 'https://github.com/VGVentures/pinball/tree/main/packages/pinball_components/sandbox/lib/stories/$path';
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:sandbox/common/common.dart';
|
||||||
|
|
||||||
|
class BallBoosterExample extends LineGame {
|
||||||
|
static const info = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onLine(Vector2 line) {
|
||||||
|
final ball = Ball(baseColor: Colors.transparent);
|
||||||
|
add(ball);
|
||||||
|
|
||||||
|
ball.mounted.then((value) => ball.boost(line * -1 * 20));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import 'package:dashbook/dashbook.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:sandbox/common/common.dart';
|
||||||
|
import 'package:sandbox/stories/effects/fire_effect.dart';
|
||||||
|
|
||||||
|
void addEffectsStories(Dashbook dashbook) {
|
||||||
|
dashbook.storiesOf('Effects').add(
|
||||||
|
'Fire Effect',
|
||||||
|
(context) => GameWidget(game: FireEffectExample()),
|
||||||
|
codeLink: buildSourceLink('effects/fire_effect.dart'),
|
||||||
|
info: FireEffectExample.info,
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:sandbox/common/common.dart';
|
||||||
|
|
||||||
|
class FireEffectExample extends LineGame {
|
||||||
|
static const info = 'Demonstrate the fire trail effect '
|
||||||
|
'drag a line to define the trail direction';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onLine(Vector2 line) {
|
||||||
|
add(_EffectEmitter(line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EffectEmitter extends Component {
|
||||||
|
_EffectEmitter(this.line) {
|
||||||
|
_direction = line.normalized();
|
||||||
|
_force = line.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const _timerLimit = 2.0;
|
||||||
|
var _timer = _timerLimit;
|
||||||
|
|
||||||
|
final Vector2 line;
|
||||||
|
|
||||||
|
late Vector2 _direction;
|
||||||
|
late double _force;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update(double dt) {
|
||||||
|
super.update(dt);
|
||||||
|
|
||||||
|
if (_timer > 0) {
|
||||||
|
add(
|
||||||
|
FireEffect(
|
||||||
|
burstPower: (_timer / _timerLimit) * _force,
|
||||||
|
position: Vector2.zero(),
|
||||||
|
direction: _direction,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_timer -= dt;
|
||||||
|
} else {
|
||||||
|
removeFromParent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
import 'package:flame/input.dart';
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:sandbox/common/common.dart';
|
||||||
|
|
||||||
|
class BasicLayerGame extends BasicGame with TapDetector {
|
||||||
|
BasicLayerGame({required this.color});
|
||||||
|
|
||||||
|
static const info = '''
|
||||||
|
Basic example of how layers work with a Ball hitting other components,
|
||||||
|
tap anywhere on the screen to spawn a ball into the game.
|
||||||
|
''';
|
||||||
|
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await add(BigSquare()..initialPosition = Vector2(30, -40));
|
||||||
|
await add(SmallSquare()..initialPosition = Vector2(50, -40));
|
||||||
|
await add(UnlayeredSquare()..initialPosition = Vector2(60, -40));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onTapUp(TapUpInfo info) {
|
||||||
|
add(
|
||||||
|
Ball(baseColor: color)..initialPosition = info.eventPosition.game,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BigSquare extends BodyComponent with InitialPosition, Layered {
|
||||||
|
BigSquare() {
|
||||||
|
paint = Paint()
|
||||||
|
..color = const Color.fromARGB(255, 8, 218, 241)
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
layer = Layer.jetpack;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final shape = PolygonShape()..setAsBoxXY(16, 16);
|
||||||
|
final fixtureDef = FixtureDef(shape);
|
||||||
|
|
||||||
|
final bodyDef = BodyDef()..position = initialPosition;
|
||||||
|
|
||||||
|
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
await addAll(
|
||||||
|
[
|
||||||
|
UnlayeredSquare()..initialPosition = Vector2.all(4),
|
||||||
|
SmallSquare()..initialPosition = Vector2.all(-4),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SmallSquare extends BodyComponent with InitialPosition, Layered {
|
||||||
|
SmallSquare() {
|
||||||
|
paint = Paint()
|
||||||
|
..color = const Color.fromARGB(255, 27, 241, 8)
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
layer = Layer.board;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final shape = PolygonShape()..setAsBoxXY(2, 2);
|
||||||
|
final fixtureDef = FixtureDef(shape);
|
||||||
|
|
||||||
|
final bodyDef = BodyDef()..position = initialPosition;
|
||||||
|
|
||||||
|
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnlayeredSquare extends BodyComponent with InitialPosition {
|
||||||
|
UnlayeredSquare() {
|
||||||
|
paint = Paint()
|
||||||
|
..color = const Color.fromARGB(255, 241, 8, 8)
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final shape = PolygonShape()..setAsBoxXY(3, 3);
|
||||||
|
final fixtureDef = FixtureDef(shape);
|
||||||
|
|
||||||
|
final bodyDef = BodyDef()..position = initialPosition;
|
||||||
|
|
||||||
|
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import 'package:dashbook/dashbook.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sandbox/common/common.dart';
|
||||||
|
import 'package:sandbox/stories/layer/basic.dart';
|
||||||
|
|
||||||
|
void addLayerStories(Dashbook dashbook) {
|
||||||
|
dashbook.storiesOf('Layer').add(
|
||||||
|
'Layer',
|
||||||
|
(context) => GameWidget(
|
||||||
|
game: BasicLayerGame(
|
||||||
|
color: context.colorProperty('color', Colors.blue),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
codeLink: buildSourceLink('layer/basic.dart'),
|
||||||
|
info: BasicLayerGame.info,
|
||||||
|
);
|
||||||
|
}
|
@ -1 +1,2 @@
|
|||||||
export 'ball/ball.dart';
|
export 'ball/ball.dart';
|
||||||
|
export 'layer/layer.dart';
|
||||||
|
@ -1 +1,2 @@
|
|||||||
|
export 'mocks.dart';
|
||||||
export 'test_game.dart';
|
export 'test_game.dart';
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
class MockCanvas extends Mock implements Canvas {}
|
@ -0,0 +1,55 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
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 '../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final flameTester = FlameTester(TestGame.new);
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
registerFallbackValue(Offset.zero);
|
||||||
|
registerFallbackValue(Paint());
|
||||||
|
});
|
||||||
|
|
||||||
|
group('FireEffect', () {
|
||||||
|
flameTester.test('is removed once its particles are done', (game) async {
|
||||||
|
await game.ensureAdd(
|
||||||
|
FireEffect(
|
||||||
|
burstPower: 1,
|
||||||
|
position: Vector2.zero(),
|
||||||
|
direction: Vector2.all(2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await game.ready();
|
||||||
|
expect(game.children.whereType<FireEffect>().length, equals(1));
|
||||||
|
game.update(5);
|
||||||
|
|
||||||
|
await game.ready();
|
||||||
|
expect(game.children.whereType<FireEffect>().length, equals(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('render circles on the canvas', (game) async {
|
||||||
|
final effect = FireEffect(
|
||||||
|
burstPower: 1,
|
||||||
|
position: Vector2.zero(),
|
||||||
|
direction: Vector2.all(2),
|
||||||
|
);
|
||||||
|
await game.ensureAdd(effect);
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
final canvas = MockCanvas();
|
||||||
|
effect.render(canvas);
|
||||||
|
|
||||||
|
verify(() => canvas.drawCircle(any(), any(), any()))
|
||||||
|
.called(greaterThan(0));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
// ignore_for_file: prefer_const_constructors
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockingjay/mockingjay.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball/l10n/l10n.dart';
|
||||||
|
import 'package:pinball_theme/pinball_theme.dart';
|
||||||
|
|
||||||
|
import '../../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GameOverDialog', () {
|
||||||
|
testWidgets('renders correctly', (tester) async {
|
||||||
|
final l10n = await AppLocalizations.delegate.load(Locale('en'));
|
||||||
|
await tester.pumpApp(
|
||||||
|
const GameOverDialog(
|
||||||
|
theme: DashTheme(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text(l10n.gameOver), findsOneWidget);
|
||||||
|
expect(find.text(l10n.leaderboard), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('tapping on leaderboard button navigates to LeaderBoardPage',
|
||||||
|
(tester) async {
|
||||||
|
final l10n = await AppLocalizations.delegate.load(Locale('en'));
|
||||||
|
final navigator = MockNavigator();
|
||||||
|
when(() => navigator.push<void>(any())).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await tester.pumpApp(
|
||||||
|
const GameOverDialog(
|
||||||
|
theme: DashTheme(),
|
||||||
|
),
|
||||||
|
navigator: navigator,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.widgetWithText(TextButton, l10n.leaderboard));
|
||||||
|
|
||||||
|
verify(() => navigator.push<void>(any())).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
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);
|
||||||
|
}
|
@ -0,0 +1,150 @@
|
|||||||
|
// ignore_for_file: prefer_const_constructors
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||||
|
import 'package:mockingjay/mockingjay.dart';
|
||||||
|
import 'package:pinball/l10n/l10n.dart';
|
||||||
|
import 'package:pinball/leaderboard/leaderboard.dart';
|
||||||
|
import 'package:pinball_theme/pinball_theme.dart';
|
||||||
|
|
||||||
|
import '../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('LeaderboardPage', () {
|
||||||
|
testWidgets('renders LeaderboardView', (tester) async {
|
||||||
|
await tester.pumpApp(
|
||||||
|
LeaderboardPage(
|
||||||
|
theme: DashTheme(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(LeaderboardView), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('route returns a valid navigation route', (tester) async {
|
||||||
|
await expectNavigatesToRoute<LeaderboardPage>(
|
||||||
|
tester,
|
||||||
|
LeaderboardPage.route(
|
||||||
|
theme: DashTheme(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('LeaderboardView', () {
|
||||||
|
late LeaderboardBloc leaderboardBloc;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
leaderboardBloc = MockLeaderboardBloc();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('renders correctly', (tester) async {
|
||||||
|
final l10n = await AppLocalizations.delegate.load(Locale('en'));
|
||||||
|
when(() => leaderboardBloc.state).thenReturn(LeaderboardState.initial());
|
||||||
|
|
||||||
|
await tester.pumpApp(
|
||||||
|
BlocProvider.value(
|
||||||
|
value: leaderboardBloc,
|
||||||
|
child: LeaderboardView(
|
||||||
|
theme: DashTheme(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text(l10n.leaderboard), findsOneWidget);
|
||||||
|
expect(find.text(l10n.retry), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('renders loading view when bloc emits [loading]',
|
||||||
|
(tester) async {
|
||||||
|
when(() => leaderboardBloc.state).thenReturn(LeaderboardState.initial());
|
||||||
|
|
||||||
|
await tester.pumpApp(
|
||||||
|
BlocProvider.value(
|
||||||
|
value: leaderboardBloc,
|
||||||
|
child: LeaderboardView(
|
||||||
|
theme: DashTheme(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||||
|
expect(find.text('There was en error loading data!'), findsNothing);
|
||||||
|
expect(find.byType(ListView), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('renders error view when bloc emits [error]', (tester) async {
|
||||||
|
when(() => leaderboardBloc.state).thenReturn(
|
||||||
|
LeaderboardState.initial().copyWith(status: LeaderboardStatus.error),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpApp(
|
||||||
|
BlocProvider.value(
|
||||||
|
value: leaderboardBloc,
|
||||||
|
child: LeaderboardView(
|
||||||
|
theme: DashTheme(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||||
|
expect(find.text('There was en error loading data!'), findsOneWidget);
|
||||||
|
expect(find.byType(ListView), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('renders success view when bloc emits [success]',
|
||||||
|
(tester) async {
|
||||||
|
final l10n = await AppLocalizations.delegate.load(Locale('en'));
|
||||||
|
when(() => leaderboardBloc.state).thenReturn(
|
||||||
|
LeaderboardState(
|
||||||
|
status: LeaderboardStatus.success,
|
||||||
|
ranking: LeaderboardRanking(ranking: 0, outOf: 0),
|
||||||
|
leaderboard: [
|
||||||
|
LeaderboardEntry(
|
||||||
|
rank: '1',
|
||||||
|
playerInitials: 'ABC',
|
||||||
|
score: 10000,
|
||||||
|
character: DashTheme().characterAsset,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpApp(
|
||||||
|
BlocProvider.value(
|
||||||
|
value: leaderboardBloc,
|
||||||
|
child: LeaderboardView(
|
||||||
|
theme: DashTheme(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||||
|
expect(find.text('There was en error loading data!'), findsNothing);
|
||||||
|
expect(find.text(l10n.rank), findsOneWidget);
|
||||||
|
expect(find.text(l10n.character), findsOneWidget);
|
||||||
|
expect(find.text(l10n.username), findsOneWidget);
|
||||||
|
expect(find.text(l10n.score), findsOneWidget);
|
||||||
|
expect(find.byType(ListView), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('navigates to CharacterSelectionPage when retry is tapped',
|
||||||
|
(tester) async {
|
||||||
|
final navigator = MockNavigator();
|
||||||
|
when(() => navigator.push<void>(any())).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await tester.pumpApp(
|
||||||
|
LeaderboardPage(
|
||||||
|
theme: DashTheme(),
|
||||||
|
),
|
||||||
|
navigator: navigator,
|
||||||
|
);
|
||||||
|
await tester.ensureVisible(find.byType(TextButton));
|
||||||
|
await tester.tap(find.byType(TextButton));
|
||||||
|
|
||||||
|
verify(() => navigator.push<void>(any())).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in new issue