@ -0,0 +1,19 @@
|
||||
name: pinball_components
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "packages/pinball_components/**"
|
||||
- ".github/workflows/pinball_components.yaml"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "packages/pinball_components/**"
|
||||
- ".github/workflows/pinball_components.yaml"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
|
||||
with:
|
||||
working_directory: packages/pinball_components
|
||||
coverage_excludes: "lib/gen/*.dart"
|
Before Width: | Height: | Size: 7.7 KiB |
@ -0,0 +1,131 @@
|
||||
// ignore_for_file: avoid_renaming_method_parameters
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_bloc/flame_bloc.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinball/flame/blueprint.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
/// {@template flutter_forest}
|
||||
/// Area positioned at the top right of the [Board] where the [Ball]
|
||||
/// can bounce off [DashNestBumper]s.
|
||||
///
|
||||
/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest]
|
||||
/// is awarded, and the [BigDashNestBumper] releases a new [Ball].
|
||||
/// {@endtemplate}
|
||||
// TODO(alestiago): Make a [Blueprint] once nesting [Blueprint] is implemented.
|
||||
class FlutterForest extends Component
|
||||
with HasGameRef<PinballGame>, BlocComponent<GameBloc, GameState> {
|
||||
/// {@macro flutter_forest}
|
||||
FlutterForest({
|
||||
required this.position,
|
||||
});
|
||||
|
||||
/// The position of the [FlutterForest] on the [Board].
|
||||
final Vector2 position;
|
||||
|
||||
@override
|
||||
bool listenWhen(GameState? previousState, GameState newState) {
|
||||
return (previousState?.bonusHistory.length ?? 0) <
|
||||
newState.bonusHistory.length &&
|
||||
newState.bonusHistory.last == GameBonus.dashNest;
|
||||
}
|
||||
|
||||
@override
|
||||
void onNewState(GameState state) {
|
||||
super.onNewState(state);
|
||||
gameRef.addFromBlueprint(BallBlueprint(position: position));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
gameRef.addContactCallback(DashNestBumperBallContactCallback());
|
||||
|
||||
// TODO(alestiago): adjust positioning once sprites are added.
|
||||
final smallLeftNest = SmallDashNestBumper(id: 'small_left_nest')
|
||||
..initialPosition = position + Vector2(-4.8, 2.8);
|
||||
final smallRightNest = SmallDashNestBumper(id: 'small_right_nest')
|
||||
..initialPosition = position + Vector2(0.5, -5.5);
|
||||
final bigNest = BigDashNestBumper(id: 'big_nest')
|
||||
..initialPosition = position;
|
||||
|
||||
await addAll([
|
||||
smallLeftNest,
|
||||
smallRightNest,
|
||||
bigNest,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/// {@template dash_nest_bumper}
|
||||
/// Bumper located in the [FlutterForest].
|
||||
/// {@endtemplate}
|
||||
@visibleForTesting
|
||||
abstract class DashNestBumper extends BodyComponent<PinballGame>
|
||||
with ScorePoints, InitialPosition {
|
||||
/// {@macro dash_nest_bumper}
|
||||
DashNestBumper({required this.id});
|
||||
|
||||
/// Unique identifier for this [DashNestBumper].
|
||||
///
|
||||
/// Used to identify [DashNestBumper]s in [GameState.activatedDashNests].
|
||||
final String id;
|
||||
}
|
||||
|
||||
/// Listens when a [Ball] bounces bounces against a [DashNestBumper].
|
||||
@visibleForTesting
|
||||
class DashNestBumperBallContactCallback
|
||||
extends ContactCallback<DashNestBumper, Ball> {
|
||||
@override
|
||||
void begin(DashNestBumper dashNestBumper, Ball ball, Contact _) {
|
||||
dashNestBumper.gameRef.read<GameBloc>().add(
|
||||
DashNestActivated(dashNestBumper.id),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// {@macro dash_nest_bumper}
|
||||
@visibleForTesting
|
||||
class BigDashNestBumper extends DashNestBumper {
|
||||
/// {@macro dash_nest_bumper}
|
||||
BigDashNestBumper({required String id}) : super(id: id);
|
||||
|
||||
@override
|
||||
int get points => 20;
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final shape = CircleShape()..radius = 2.5;
|
||||
final fixtureDef = FixtureDef(shape);
|
||||
|
||||
final bodyDef = BodyDef()
|
||||
..position = initialPosition
|
||||
..userData = this;
|
||||
|
||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||
}
|
||||
}
|
||||
|
||||
/// {@macro dash_nest_bumper}
|
||||
@visibleForTesting
|
||||
class SmallDashNestBumper extends DashNestBumper {
|
||||
/// {@macro dash_nest_bumper}
|
||||
SmallDashNestBumper({required String id}) : super(id: id);
|
||||
|
||||
@override
|
||||
int get points => 10;
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final shape = CircleShape()..radius = 1;
|
||||
final fixtureDef = FixtureDef(shape);
|
||||
|
||||
final bodyDef = BodyDef()
|
||||
..position = initialPosition
|
||||
..userData = this;
|
||||
|
||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||
}
|
||||
}
|
@ -1,168 +0,0 @@
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geometry/geometry.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
|
||||
/// {@template pathway}
|
||||
/// [Pathway] creates lines of various shapes.
|
||||
///
|
||||
/// [BodyComponent]s such as a Ball can collide and move along a [Pathway].
|
||||
/// {@endtemplate}
|
||||
class Pathway extends BodyComponent with InitialPosition, Layered {
|
||||
Pathway._({
|
||||
// TODO(ruialonso): remove color when assets added.
|
||||
Color? color,
|
||||
required List<List<Vector2>> paths,
|
||||
}) : _paths = paths {
|
||||
paint = Paint()
|
||||
..color = color ?? const Color.fromARGB(0, 0, 0, 0)
|
||||
..style = PaintingStyle.stroke;
|
||||
}
|
||||
|
||||
/// Creates a uniform unidirectional (straight) [Pathway].
|
||||
///
|
||||
/// Does so with two [ChainShape] separated by a [width]. Can
|
||||
/// be rotated by a given [rotation] in radians.
|
||||
///
|
||||
/// If [singleWall] is true, just one [ChainShape] is created.
|
||||
factory Pathway.straight({
|
||||
Color? color,
|
||||
required Vector2 start,
|
||||
required Vector2 end,
|
||||
required double width,
|
||||
double rotation = 0,
|
||||
bool singleWall = false,
|
||||
}) {
|
||||
final paths = <List<Vector2>>[];
|
||||
|
||||
// TODO(ruialonso): Refactor repetitive logic
|
||||
final firstWall = [
|
||||
start.clone(),
|
||||
end.clone(),
|
||||
].map((vector) => vector..rotate(rotation)).toList();
|
||||
paths.add(firstWall);
|
||||
|
||||
if (!singleWall) {
|
||||
final secondWall = [
|
||||
start + Vector2(width, 0),
|
||||
end + Vector2(width, 0),
|
||||
].map((vector) => vector..rotate(rotation)).toList();
|
||||
paths.add(secondWall);
|
||||
}
|
||||
|
||||
return Pathway._(
|
||||
color: color,
|
||||
paths: paths,
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates an arc [Pathway].
|
||||
///
|
||||
/// The [angle], in radians, specifies the size of the arc. For example, two
|
||||
/// pi returns a complete circumference.
|
||||
///
|
||||
/// Does so with two [ChainShape] separated by a [width]. Which can be
|
||||
/// rotated by a given [rotation] in radians.
|
||||
///
|
||||
/// The outer radius is specified by [radius], whilst the inner one is
|
||||
/// equivalent to the [radius] minus the [width].
|
||||
///
|
||||
/// If [singleWall] is true, just one [ChainShape] is created.
|
||||
factory Pathway.arc({
|
||||
Color? color,
|
||||
required Vector2 center,
|
||||
required double width,
|
||||
required double radius,
|
||||
required double angle,
|
||||
double rotation = 0,
|
||||
bool singleWall = false,
|
||||
}) {
|
||||
final paths = <List<Vector2>>[];
|
||||
|
||||
// TODO(ruialonso): Refactor repetitive logic
|
||||
final outerWall = calculateArc(
|
||||
center: center,
|
||||
radius: radius,
|
||||
angle: angle,
|
||||
offsetAngle: rotation,
|
||||
);
|
||||
paths.add(outerWall);
|
||||
|
||||
if (!singleWall) {
|
||||
final innerWall = calculateArc(
|
||||
center: center,
|
||||
radius: radius - width,
|
||||
angle: angle,
|
||||
offsetAngle: rotation,
|
||||
);
|
||||
paths.add(innerWall);
|
||||
}
|
||||
|
||||
return Pathway._(
|
||||
color: color,
|
||||
paths: paths,
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a bezier curve [Pathway].
|
||||
///
|
||||
/// Does so with two [ChainShape] separated by a [width]. Which can be
|
||||
/// rotated by a given [rotation] in radians.
|
||||
///
|
||||
/// First and last [controlPoints] set the beginning and end of the curve,
|
||||
/// inner points between them set its final shape.
|
||||
///
|
||||
/// If [singleWall] is true, just one [ChainShape] is created.
|
||||
factory Pathway.bezierCurve({
|
||||
Color? color,
|
||||
required List<Vector2> controlPoints,
|
||||
required double width,
|
||||
double rotation = 0,
|
||||
bool singleWall = false,
|
||||
}) {
|
||||
final paths = <List<Vector2>>[];
|
||||
|
||||
// TODO(ruialonso): Refactor repetitive logic
|
||||
final firstWall = calculateBezierCurve(controlPoints: controlPoints)
|
||||
.map((vector) => vector..rotate(rotation))
|
||||
.toList();
|
||||
paths.add(firstWall);
|
||||
|
||||
if (!singleWall) {
|
||||
final secondWall = calculateBezierCurve(
|
||||
controlPoints: controlPoints
|
||||
.map((vector) => vector + Vector2(width, -width))
|
||||
.toList(),
|
||||
).map((vector) => vector..rotate(rotation)).toList();
|
||||
paths.add(secondWall);
|
||||
}
|
||||
|
||||
return Pathway._(
|
||||
color: color,
|
||||
paths: paths,
|
||||
);
|
||||
}
|
||||
|
||||
final List<List<Vector2>> _paths;
|
||||
|
||||
List<FixtureDef> _createFixtureDefs() {
|
||||
final fixturesDef = <FixtureDef>[];
|
||||
|
||||
for (final path in _paths) {
|
||||
final chain = ChainShape()..createChain(path);
|
||||
fixturesDef.add(FixtureDef(chain));
|
||||
}
|
||||
|
||||
return fixturesDef;
|
||||
}
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final bodyDef = BodyDef()..position = initialPosition;
|
||||
final body = world.createBody(bodyDef);
|
||||
_createFixtureDefs().forEach(body.createFixture);
|
||||
|
||||
return body;
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
|
||||
/// {@template round_bumper}
|
||||
/// Circular body that repels a [Ball] on contact, increasing the score.
|
||||
/// {@endtemplate}
|
||||
class RoundBumper extends BodyComponent with ScorePoints, InitialPosition {
|
||||
/// {@macro round_bumper}
|
||||
RoundBumper({
|
||||
required double radius,
|
||||
required int points,
|
||||
}) : _radius = radius,
|
||||
_points = points;
|
||||
|
||||
/// The radius of the [RoundBumper].
|
||||
final double _radius;
|
||||
|
||||
/// Points awarded from hitting this [RoundBumper].
|
||||
final int _points;
|
||||
|
||||
@override
|
||||
int get points => _points;
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final shape = CircleShape()..radius = _radius;
|
||||
|
||||
final fixtureDef = FixtureDef(shape)..restitution = 1;
|
||||
|
||||
final bodyDef = BodyDef()..position = initialPosition;
|
||||
|
||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||
}
|
||||
}
|
@ -1 +1,3 @@
|
||||
export 'bloc/leaderboard_bloc.dart';
|
||||
export 'models/leader_board_entry.dart';
|
||||
export 'view/leaderboard_page.dart';
|
||||
|
@ -0,0 +1,80 @@
|
||||
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||
import 'package:pinball_theme/pinball_theme.dart';
|
||||
|
||||
/// {@template leaderboard_entry}
|
||||
/// A model representing a leaderboard entry containing the ranking position,
|
||||
/// player's initials, score, and chosen character.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class LeaderboardEntry {
|
||||
/// {@macro leaderboard_entry}
|
||||
LeaderboardEntry({
|
||||
required this.rank,
|
||||
required this.playerInitials,
|
||||
required this.score,
|
||||
required this.character,
|
||||
});
|
||||
|
||||
/// Ranking position for [LeaderboardEntry].
|
||||
final String rank;
|
||||
|
||||
/// Player's chosen initials for [LeaderboardEntry].
|
||||
final String playerInitials;
|
||||
|
||||
/// Score for [LeaderboardEntry].
|
||||
final int score;
|
||||
|
||||
/// [CharacterTheme] for [LeaderboardEntry].
|
||||
final AssetGenImage character;
|
||||
}
|
||||
|
||||
/// Converts [LeaderboardEntryData] from repository to [LeaderboardEntry].
|
||||
extension LeaderboardEntryDataX on LeaderboardEntryData {
|
||||
/// Conversion method to [LeaderboardEntry]
|
||||
LeaderboardEntry toEntry(int position) {
|
||||
return LeaderboardEntry(
|
||||
rank: position.toString(),
|
||||
playerInitials: playerInitials,
|
||||
score: score,
|
||||
character: character.toTheme.characterAsset,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts [CharacterType] to [CharacterTheme] to show on UI character theme
|
||||
/// from repository.
|
||||
extension CharacterTypeX on CharacterType {
|
||||
/// Conversion method to [CharacterTheme]
|
||||
CharacterTheme get toTheme {
|
||||
switch (this) {
|
||||
case CharacterType.dash:
|
||||
return const DashTheme();
|
||||
case CharacterType.sparky:
|
||||
return const SparkyTheme();
|
||||
case CharacterType.android:
|
||||
return const AndroidTheme();
|
||||
case CharacterType.dino:
|
||||
return const DinoTheme();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts [CharacterTheme] to [CharacterType] to persist at repository the
|
||||
/// character theme from UI.
|
||||
extension CharacterThemeX on CharacterTheme {
|
||||
/// Conversion method to [CharacterType]
|
||||
CharacterType get toType {
|
||||
switch (runtimeType) {
|
||||
case DashTheme:
|
||||
return CharacterType.dash;
|
||||
case SparkyTheme:
|
||||
return CharacterType.sparky;
|
||||
case AndroidTheme:
|
||||
return CharacterType.android;
|
||||
case DinoTheme:
|
||||
return CharacterType.dino;
|
||||
default:
|
||||
return CharacterType.dash;
|
||||
}
|
||||
}
|
||||
}
|
@ -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,19 +1,19 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'leaderboard_entry.dart';
|
||||
part of 'leaderboard_entry_data.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
LeaderboardEntry _$LeaderboardEntryFromJson(Map<String, dynamic> json) =>
|
||||
LeaderboardEntry(
|
||||
LeaderboardEntryData _$LeaderboardEntryFromJson(Map<String, dynamic> json) =>
|
||||
LeaderboardEntryData(
|
||||
playerInitials: json['playerInitials'] as String,
|
||||
score: json['score'] as int,
|
||||
character: $enumDecode(_$CharacterTypeEnumMap, json['character']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LeaderboardEntryToJson(LeaderboardEntry instance) =>
|
||||
Map<String, dynamic> _$LeaderboardEntryToJson(LeaderboardEntryData instance) =>
|
||||
<String, dynamic>{
|
||||
'playerInitials': instance.playerInitials,
|
||||
'score': instance.score,
|
@ -1,2 +1,2 @@
|
||||
export 'leaderboard_entry.dart';
|
||||
export 'leaderboard_entry_data.dart';
|
||||
export 'leaderboard_ranking.dart';
|
||||
|
@ -0,0 +1,39 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# VSCode related
|
||||
.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Web related
|
||||
lib/generated_plugin_registrant.dart
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
@ -0,0 +1,11 @@
|
||||
# pinball_components
|
||||
|
||||
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
|
||||
[![License: MIT][license_badge]][license_link]
|
||||
|
||||
Package with the UI game components for the Pinball Game
|
||||
|
||||
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[license_link]: https://opensource.org/licenses/MIT
|
||||
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
|
||||
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis
|
@ -0,0 +1,4 @@
|
||||
include: package:very_good_analysis/analysis_options.2.4.0.yaml
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/**/*.gen.dart
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
@ -0,0 +1,68 @@
|
||||
/// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
/// *****************************************************
|
||||
/// FlutterGen
|
||||
/// *****************************************************
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class $AssetsImagesGen {
|
||||
const $AssetsImagesGen();
|
||||
|
||||
AssetGenImage get ball => const AssetGenImage('assets/images/ball.png');
|
||||
}
|
||||
|
||||
class Assets {
|
||||
Assets._();
|
||||
|
||||
static const $AssetsImagesGen images = $AssetsImagesGen();
|
||||
}
|
||||
|
||||
class AssetGenImage extends AssetImage {
|
||||
const AssetGenImage(String assetName)
|
||||
: super(assetName, package: 'pinball_components');
|
||||
|
||||
Image image({
|
||||
Key? key,
|
||||
ImageFrameBuilder? frameBuilder,
|
||||
ImageLoadingBuilder? loadingBuilder,
|
||||
ImageErrorWidgetBuilder? errorBuilder,
|
||||
String? semanticLabel,
|
||||
bool excludeFromSemantics = false,
|
||||
double? width,
|
||||
double? height,
|
||||
Color? color,
|
||||
BlendMode? colorBlendMode,
|
||||
BoxFit? fit,
|
||||
AlignmentGeometry alignment = Alignment.center,
|
||||
ImageRepeat repeat = ImageRepeat.noRepeat,
|
||||
Rect? centerSlice,
|
||||
bool matchTextDirection = false,
|
||||
bool gaplessPlayback = false,
|
||||
bool isAntiAlias = false,
|
||||
FilterQuality filterQuality = FilterQuality.low,
|
||||
}) {
|
||||
return Image(
|
||||
key: key,
|
||||
image: this,
|
||||
frameBuilder: frameBuilder,
|
||||
loadingBuilder: loadingBuilder,
|
||||
errorBuilder: errorBuilder,
|
||||
semanticLabel: semanticLabel,
|
||||
excludeFromSemantics: excludeFromSemantics,
|
||||
width: width,
|
||||
height: height,
|
||||
color: color,
|
||||
colorBlendMode: colorBlendMode,
|
||||
fit: fit,
|
||||
alignment: alignment,
|
||||
repeat: repeat,
|
||||
centerSlice: centerSlice,
|
||||
matchTextDirection: matchTextDirection,
|
||||
gaplessPlayback: gaplessPlayback,
|
||||
isAntiAlias: isAntiAlias,
|
||||
filterQuality: filterQuality,
|
||||
);
|
||||
}
|
||||
|
||||
String get path => assetName;
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
library pinball_components;
|
||||
|
||||
export 'gen/assets.gen.dart';
|
||||
export 'src/pinball_components.dart';
|
@ -0,0 +1,98 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
/// {@template ball}
|
||||
/// A solid, [BodyType.dynamic] sphere that rolls and bounces around
|
||||
/// {@endtemplate}
|
||||
class Ball<T extends Forge2DGame> extends BodyComponent<T>
|
||||
with Layered, InitialPosition {
|
||||
/// {@macro ball_body}
|
||||
Ball({
|
||||
required this.baseColor,
|
||||
}) {
|
||||
// TODO(ruimiguel): while developing Ball can be launched by clicking mouse,
|
||||
// and default layer is Layer.all. But on final game Ball will be always be
|
||||
// be launched from Plunger and LauncherRamp will modify it to Layer.board.
|
||||
// We need to see what happens if Ball appears from other place like nest
|
||||
// bumper, it will need to explicit change layer to Layer.board then.
|
||||
layer = Layer.board;
|
||||
}
|
||||
|
||||
/// The size of the [Ball]
|
||||
final Vector2 size = Vector2.all(3);
|
||||
|
||||
/// The base [Color] used to tint this [Ball]
|
||||
final Color baseColor;
|
||||
|
||||
double _boostTimer = 0;
|
||||
static const _boostDuration = 2.0;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
final sprite = await gameRef.loadSprite(Assets.images.ball.keyName);
|
||||
final tint = baseColor.withOpacity(0.5);
|
||||
await add(
|
||||
SpriteComponent(
|
||||
sprite: sprite,
|
||||
size: size,
|
||||
anchor: Anchor.center,
|
||||
)..tint(tint),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final shape = CircleShape()..radius = size.x / 2;
|
||||
|
||||
final fixtureDef = FixtureDef(shape)..density = 1;
|
||||
|
||||
final bodyDef = BodyDef()
|
||||
..position = initialPosition
|
||||
..userData = this
|
||||
..type = BodyType.dynamic;
|
||||
|
||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||
}
|
||||
|
||||
/// Immediatly and completly [stop]s the ball.
|
||||
///
|
||||
/// The [Ball] will no longer be affected by any forces, including it's
|
||||
/// weight and those emitted from collisions.
|
||||
void stop() {
|
||||
body.setType(BodyType.static);
|
||||
}
|
||||
|
||||
/// Allows the [Ball] to be affected by forces.
|
||||
///
|
||||
/// If previously [stop]ed, the previous ball's velocity is not kept.
|
||||
void resume() {
|
||||
body.setType(BodyType.dynamic);
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
if (_boostTimer > 0) {
|
||||
_boostTimer -= dt;
|
||||
final direction = body.linearVelocity.normalized();
|
||||
final effect = FireEffect(
|
||||
burstPower: _boostTimer,
|
||||
direction: direction,
|
||||
position: body.position,
|
||||
);
|
||||
|
||||
unawaited(gameRef.add(effect));
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a boost on this [Ball]
|
||||
void boost(Vector2 impulse) {
|
||||
body.applyLinearImpulse(impulse);
|
||||
_boostTimer = _boostDuration;
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
export 'ball.dart';
|
||||
export 'fire_effect.dart';
|
||||
export 'initial_position.dart';
|
||||
export 'layer.dart';
|
||||
export 'shapes/shapes.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);
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:geometry/geometry.dart';
|
||||
|
||||
/// {@template arc_shape}
|
||||
/// Creates an arc.
|
||||
/// {@endtemplate}
|
||||
class ArcShape extends ChainShape {
|
||||
/// {@macro arc_shape}
|
||||
ArcShape({
|
||||
required this.center,
|
||||
required this.arcRadius,
|
||||
required this.angle,
|
||||
this.rotation = 0,
|
||||
}) {
|
||||
createChain(
|
||||
calculateArc(
|
||||
center: center,
|
||||
radius: arcRadius,
|
||||
angle: angle,
|
||||
offsetAngle: rotation,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The center of the arc.
|
||||
final Vector2 center;
|
||||
|
||||
/// The radius of the arc.
|
||||
// TODO(alestiago): Check if modifying the parent radius makes sense.
|
||||
final double arcRadius;
|
||||
|
||||
/// Specifies the size of the arc, in radians.
|
||||
///
|
||||
/// For example, two pi returns a complete circumference.
|
||||
final double angle;
|
||||
|
||||
/// Angle in radians to rotate the arc around its [center].
|
||||
final double rotation;
|
||||
|
||||
ArcShape copyWith({
|
||||
Vector2? center,
|
||||
double? arcRadius,
|
||||
double? angle,
|
||||
double? rotation,
|
||||
}) =>
|
||||
ArcShape(
|
||||
center: center ?? this.center,
|
||||
arcRadius: arcRadius ?? this.arcRadius,
|
||||
angle: angle ?? this.angle,
|
||||
rotation: rotation ?? this.rotation,
|
||||
);
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:geometry/geometry.dart';
|
||||
|
||||
/// {@template bezier_curve_shape}
|
||||
/// Creates a bezier curve.
|
||||
/// {@endtemplate}
|
||||
class BezierCurveShape extends ChainShape {
|
||||
/// {@macro bezier_curve_shape}
|
||||
BezierCurveShape({
|
||||
required this.controlPoints,
|
||||
}) {
|
||||
createChain(calculateBezierCurve(controlPoints: controlPoints));
|
||||
}
|
||||
|
||||
/// Specifies the control points of the curve.
|
||||
///
|
||||
/// First and last [controlPoints] set the beginning and end of the curve,
|
||||
/// inner points between them set its final shape.
|
||||
final List<Vector2> controlPoints;
|
||||
|
||||
/// Rotates the bezier curve by a given [angle] in radians.
|
||||
void rotate(double angle) {
|
||||
vertices.map((vector) => vector..rotate(angle)).toList();
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:geometry/geometry.dart';
|
||||
|
||||
/// {@template ellipse_shape}
|
||||
/// Creates an ellipse.
|
||||
/// {@endtemplate}
|
||||
class EllipseShape extends ChainShape {
|
||||
/// {@macro ellipse_shape}
|
||||
EllipseShape({
|
||||
required this.center,
|
||||
required this.majorRadius,
|
||||
required this.minorRadius,
|
||||
}) {
|
||||
createChain(
|
||||
calculateEllipse(
|
||||
center: center,
|
||||
majorRadius: majorRadius,
|
||||
minorRadius: minorRadius,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The top left corner of the ellipse.
|
||||
///
|
||||
/// Where the initial painting begins.
|
||||
// TODO(ruialonso): Change to use appropiate center.
|
||||
final Vector2 center;
|
||||
|
||||
/// Major radius is specified by [majorRadius].
|
||||
final double majorRadius;
|
||||
|
||||
/// Minor radius is specified by [minorRadius].
|
||||
final double minorRadius;
|
||||
|
||||
/// Rotates the ellipse by a given [angle] in radians.
|
||||
void rotate(double angle) {
|
||||
vertices.map((vector) => vector..rotate(angle)).toList();
|
||||
}
|
||||
|
||||
EllipseShape copyWith({
|
||||
Vector2? center,
|
||||
double? majorRadius,
|
||||
double? minorRadius,
|
||||
}) =>
|
||||
EllipseShape(
|
||||
center: center ?? this.center,
|
||||
majorRadius: majorRadius ?? this.majorRadius,
|
||||
minorRadius: minorRadius ?? this.minorRadius,
|
||||
);
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export 'arc_shape.dart';
|
||||
export 'bezier_curve_shape.dart';
|
||||
export 'ellipse_shape.dart';
|
@ -0,0 +1 @@
|
||||
export 'components/components.dart';
|
@ -0,0 +1,33 @@
|
||||
name: pinball_components
|
||||
description: Package with the UI game components for the Pinball Game
|
||||
version: 1.0.0+1
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=2.16.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flame: ^1.1.0-releasecandidate.6
|
||||
flame_forge2d: ^0.9.0-releasecandidate.6
|
||||
flutter:
|
||||
sdk: flutter
|
||||
geometry:
|
||||
path: ../geometry
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
flame_test: ^1.1.0
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
mocktail: ^0.2.0
|
||||
very_good_analysis: ^2.4.0
|
||||
|
||||
flutter:
|
||||
generate: true
|
||||
assets:
|
||||
- assets/images/
|
||||
|
||||
flutter_gen:
|
||||
line_length: 80
|
||||
assets:
|
||||
package_parameter_enabled: true
|
@ -0,0 +1,127 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.lock
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/*
|
||||
|
||||
# Visual Studio Code related
|
||||
.classpath
|
||||
.project
|
||||
.settings/
|
||||
.vscode/*
|
||||
|
||||
# Flutter repo-specific
|
||||
/bin/cache/
|
||||
/bin/mingit/
|
||||
/dev/benchmarks/mega_gallery/
|
||||
/dev/bots/.recipe_deps
|
||||
/dev/bots/android_tools/
|
||||
/dev/docs/doc/
|
||||
/dev/docs/flutter.docs.zip
|
||||
/dev/docs/lib/
|
||||
/dev/docs/pubspec.yaml
|
||||
/dev/integration_tests/**/xcuserdata
|
||||
/dev/integration_tests/**/Pods
|
||||
/packages/flutter/coverage/
|
||||
version
|
||||
|
||||
# packages file containing multi-root paths
|
||||
.packages.generated
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
build/
|
||||
flutter_*.png
|
||||
linked_*.ds
|
||||
unlinked.ds
|
||||
unlinked_spec.ds
|
||||
.fvm/
|
||||
|
||||
# Android related
|
||||
**/android/**/gradle-wrapper.jar
|
||||
**/android/.gradle
|
||||
**/android/captures/
|
||||
**/android/gradlew
|
||||
**/android/gradlew.bat
|
||||
**/android/local.properties
|
||||
**/android/**/GeneratedPluginRegistrant.java
|
||||
**/android/key.properties
|
||||
**/android/.idea/
|
||||
*.jks
|
||||
|
||||
# iOS/XCode related
|
||||
**/ios/**/*.mode1v3
|
||||
**/ios/**/*.mode2v3
|
||||
**/ios/**/*.moved-aside
|
||||
**/ios/**/*.pbxuser
|
||||
**/ios/**/*.perspectivev3
|
||||
**/ios/**/*sync/
|
||||
**/ios/**/.sconsign.dblite
|
||||
**/ios/**/.tags*
|
||||
**/ios/**/.vagrant/
|
||||
**/ios/**/DerivedData/
|
||||
**/ios/**/Icon?
|
||||
**/ios/**/Pods/
|
||||
**/ios/**/.symlinks/
|
||||
**/ios/**/profile
|
||||
**/ios/**/xcuserdata
|
||||
**/ios/.generated/
|
||||
**/ios/Flutter/App.framework
|
||||
**/ios/Flutter/Flutter.framework
|
||||
**/ios/Flutter/Flutter.podspec
|
||||
**/ios/Flutter/Generated.xcconfig
|
||||
**/ios/Flutter/app.flx
|
||||
**/ios/Flutter/app.zip
|
||||
**/ios/Flutter/.last_build_id
|
||||
**/ios/Flutter/flutter_assets/
|
||||
**/ios/Flutter/flutter_export_environment.sh
|
||||
**/ios/ServiceDefinitions.json
|
||||
**/ios/Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Coverage
|
||||
coverage/
|
||||
|
||||
# Submodules
|
||||
!pubspec.lock
|
||||
packages/**/pubspec.lock
|
||||
|
||||
# Web related
|
||||
lib/generated_plugin_registrant.dart
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Exceptions to the above rules.
|
||||
!**/ios/**/default.mode1v3
|
||||
!**/ios/**/default.mode2v3
|
||||
!**/ios/**/default.pbxuser
|
||||
!**/ios/**/default.perspectivev3
|
||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||
!/dev/ci/**/Gemfile.lock
|
||||
!.vscode/extensions.json
|
||||
!.vscode/launch.json
|
||||
!.idea/codeStyles/
|
||||
!.idea/dictionaries/
|
||||
!.idea/runConfigurations/
|
@ -0,0 +1,10 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6
|
||||
channel: stable
|
||||
|
||||
project_type: app
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Very Good Ventures
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,164 @@
|
||||
# Sandbox
|
||||
|
||||
![coverage][coverage_badge]
|
||||
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
|
||||
[![License: MIT][license_badge]][license_link]
|
||||
|
||||
Generated by the [Very Good CLI][very_good_cli_link] 🤖
|
||||
|
||||
A sanbox application where components are showcased and developed in an isolated way
|
||||
|
||||
---
|
||||
|
||||
## Getting Started 🚀
|
||||
|
||||
This project contains 3 flavors:
|
||||
|
||||
- development
|
||||
- staging
|
||||
- production
|
||||
|
||||
To run the desired flavor either use the launch configuration in VSCode/Android Studio or use the following commands:
|
||||
|
||||
```sh
|
||||
# Development
|
||||
$ flutter run --flavor development --target lib/main_development.dart
|
||||
|
||||
# Staging
|
||||
$ flutter run --flavor staging --target lib/main_staging.dart
|
||||
|
||||
# Production
|
||||
$ flutter run --flavor production --target lib/main_production.dart
|
||||
```
|
||||
|
||||
_\*Sandbox works on iOS, Android, Web, and Windows._
|
||||
|
||||
---
|
||||
|
||||
## Running Tests 🧪
|
||||
|
||||
To run all unit and widget tests use the following command:
|
||||
|
||||
```sh
|
||||
$ flutter test --coverage --test-randomize-ordering-seed random
|
||||
```
|
||||
|
||||
To view the generated coverage report you can use [lcov](https://github.com/linux-test-project/lcov).
|
||||
|
||||
```sh
|
||||
# Generate Coverage Report
|
||||
$ genhtml coverage/lcov.info -o coverage/
|
||||
|
||||
# Open Coverage Report
|
||||
$ open coverage/index.html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Working with Translations 🌐
|
||||
|
||||
This project relies on [flutter_localizations][flutter_localizations_link] and follows the [official internationalization guide for Flutter][internationalization_link].
|
||||
|
||||
### Adding Strings
|
||||
|
||||
1. To add a new localizable string, open the `app_en.arb` file at `lib/l10n/arb/app_en.arb`.
|
||||
|
||||
```arb
|
||||
{
|
||||
"@@locale": "en",
|
||||
"counterAppBarTitle": "Counter",
|
||||
"@counterAppBarTitle": {
|
||||
"description": "Text shown in the AppBar of the Counter Page"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Then add a new key/value and description
|
||||
|
||||
```arb
|
||||
{
|
||||
"@@locale": "en",
|
||||
"counterAppBarTitle": "Counter",
|
||||
"@counterAppBarTitle": {
|
||||
"description": "Text shown in the AppBar of the Counter Page"
|
||||
},
|
||||
"helloWorld": "Hello World",
|
||||
"@helloWorld": {
|
||||
"description": "Hello World Text"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Use the new string
|
||||
|
||||
```dart
|
||||
import 'package:sandbox/l10n/l10n.dart';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Text(l10n.helloWorld);
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Supported Locales
|
||||
|
||||
Update the `CFBundleLocalizations` array in the `Info.plist` at `ios/Runner/Info.plist` to include the new locale.
|
||||
|
||||
```xml
|
||||
...
|
||||
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
<string>es</string>
|
||||
</array>
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
### Adding Translations
|
||||
|
||||
1. For each supported locale, add a new ARB file in `lib/l10n/arb`.
|
||||
|
||||
```
|
||||
├── l10n
|
||||
│ ├── arb
|
||||
│ │ ├── app_en.arb
|
||||
│ │ └── app_es.arb
|
||||
```
|
||||
|
||||
2. Add the translated strings to each `.arb` file:
|
||||
|
||||
`app_en.arb`
|
||||
|
||||
```arb
|
||||
{
|
||||
"@@locale": "en",
|
||||
"counterAppBarTitle": "Counter",
|
||||
"@counterAppBarTitle": {
|
||||
"description": "Text shown in the AppBar of the Counter Page"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`app_es.arb`
|
||||
|
||||
```arb
|
||||
{
|
||||
"@@locale": "es",
|
||||
"counterAppBarTitle": "Contador",
|
||||
"@counterAppBarTitle": {
|
||||
"description": "Texto mostrado en la AppBar de la página del contador"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[coverage_badge]: coverage_badge.svg
|
||||
[flutter_localizations_link]: https://api.flutter.dev/flutter/flutter_localizations/flutter_localizations-library.html
|
||||
[internationalization_link]: https://flutter.dev/docs/development/accessibility-and-localization/internationalization
|
||||
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[license_link]: https://opensource.org/licenses/MIT
|
||||
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
|
||||
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis
|
||||
[very_good_cli_link]: https://github.com/VeryGoodOpenSource/very_good_cli
|
@ -0,0 +1,4 @@
|
||||
include: package:very_good_analysis/analysis_options.2.4.0.yaml
|
||||
linter:
|
||||
rules:
|
||||
public_member_api_docs: false
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,2 @@
|
||||
export 'games.dart';
|
||||
export 'methods.dart';
|
@ -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,19 @@
|
||||
// Copyright (c) 2022, Very Good Ventures
|
||||
// https://verygood.ventures
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
import 'package:dashbook/dashbook.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sandbox/stories/effects/effects.dart';
|
||||
import 'package:sandbox/stories/stories.dart';
|
||||
|
||||
void main() {
|
||||
final dashbook = Dashbook(theme: ThemeData.dark());
|
||||
|
||||
addBallStories(dashbook);
|
||||
addLayerStories(dashbook);
|
||||
addEffectsStories(dashbook);
|
||||
runApp(dashbook);
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
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/ball/ball_booster.dart';
|
||||
import 'package:sandbox/stories/ball/basic.dart';
|
||||
|
||||
void addBallStories(Dashbook dashbook) {
|
||||
dashbook.storiesOf('Ball')
|
||||
..add(
|
||||
'Basic',
|
||||
(context) => GameWidget(
|
||||
game: BasicBallGame(
|
||||
color: context.colorProperty('color', Colors.blue),
|
||||
),
|
||||
),
|
||||
codeLink: buildSourceLink('ball/basic.dart'),
|
||||
info: BasicBallGame.info,
|
||||
)
|
||||
..add(
|
||||
'Booster',
|
||||
(context) => GameWidget(
|
||||
game: BallBoosterExample(),
|
||||
),
|
||||
codeLink: buildSourceLink('ball/ball_booster.dart'),
|
||||
info: BallBoosterExample.info,
|
||||
);
|
||||
}
|
@ -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,22 @@
|
||||
import 'package:flame/input.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
|
||||
class BasicBallGame extends BasicGame with TapDetector {
|
||||
BasicBallGame({required this.color});
|
||||
|
||||
static const info = '''
|
||||
Basic example of how a Ball works, tap anywhere on the
|
||||
screen to spawn a ball into the game.
|
||||
''';
|
||||
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
void onTapUp(TapUpInfo info) {
|
||||
add(
|
||||
Ball(baseColor: color)..initialPosition = info.eventPosition.game,
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export 'ball/ball.dart';
|
||||
export 'layer/layer.dart';
|
@ -0,0 +1,453 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.2"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
dashbook:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dashbook
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.7"
|
||||
device_frame:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_frame
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
flame:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flame
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0-releasecandidate.6"
|
||||
flame_forge2d:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flame_forge2d
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.0-releasecandidate.6"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_colorpicker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_colorpicker
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
flutter_markdown:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_markdown
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.9"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
forge2d:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: forge2d
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.0"
|
||||
freezed_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: freezed_annotation
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
geometry:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "../../geometry"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.0.0+1"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.3"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.4.0"
|
||||
markdown:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: markdown
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.11"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
ordered_set:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ordered_set
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
pinball_components:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: ".."
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.0.0+1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.2.4"
|
||||
shared_preferences:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.13"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.11"
|
||||
shared_preferences_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_ios
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
shared_preferences_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.8"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
url_launcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.20"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.15"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.15"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.9"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
very_good_analysis:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: very_good_analysis
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0+1"
|
||||
sdks:
|
||||
dart: ">=2.16.0 <3.0.0"
|
||||
flutter: ">=2.10.0"
|
@ -0,0 +1,24 @@
|
||||
name: sandbox
|
||||
description: A sanbox application where components are showcased and developed in an isolated way
|
||||
version: 1.0.0+1
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=2.16.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
dashbook: ^0.1.7
|
||||
flame: ^1.1.0-releasecandidate.6
|
||||
flame_forge2d: ^0.9.0-releasecandidate.6
|
||||
flutter:
|
||||
sdk: flutter
|
||||
pinball_components:
|
||||
path: ../
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
very_good_analysis: ^2.4.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
After Width: | Height: | Size: 917 B |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 8.1 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 20 KiB |
@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
|
||||
The path provided below has to start and end with a slash "/" in order for
|
||||
it to work correctly.
|
||||
|
||||
For more details:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||
|
||||
This is a placeholder for base href that will be replaced by the value of
|
||||
the `--base-href` argument provided to `flutter build`.
|
||||
-->
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="sandbox">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>sandbox</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<!-- This script installs service_worker.js to provide PWA functionality to
|
||||
application. For more information, see:
|
||||
https://developers.google.com/web/fundamentals/primers/service-workers -->
|
||||
<script>
|
||||
var serviceWorkerVersion = null;
|
||||
var scriptLoaded = false;
|
||||
function loadMainDartJs() {
|
||||
if (scriptLoaded) {
|
||||
return;
|
||||
}
|
||||
scriptLoaded = true;
|
||||
var scriptTag = document.createElement('script');
|
||||
scriptTag.src = 'main.dart.js';
|
||||
scriptTag.type = 'application/javascript';
|
||||
document.body.append(scriptTag);
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
// Service workers are supported. Use them.
|
||||
window.addEventListener('load', function () {
|
||||
// Wait for registration to finish before dropping the <script> tag.
|
||||
// Otherwise, the browser will load the script multiple times,
|
||||
// potentially different versions.
|
||||
var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
|
||||
navigator.serviceWorker.register(serviceWorkerUrl)
|
||||
.then((reg) => {
|
||||
function waitForActivation(serviceWorker) {
|
||||
serviceWorker.addEventListener('statechange', () => {
|
||||
if (serviceWorker.state == 'activated') {
|
||||
console.log('Installed new service worker.');
|
||||
loadMainDartJs();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!reg.active && (reg.installing || reg.waiting)) {
|
||||
// No active web worker and we have installed or are installing
|
||||
// one for the first time. Simply wait for it to activate.
|
||||
waitForActivation(reg.installing || reg.waiting);
|
||||
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
|
||||
// When the app updates the serviceWorkerVersion changes, so we
|
||||
// need to ask the service worker to update.
|
||||
console.log('New service worker available.');
|
||||
reg.update();
|
||||
waitForActivation(reg.installing);
|
||||
} else {
|
||||
// Existing service worker is still good.
|
||||
console.log('Loading app from service worker.');
|
||||
loadMainDartJs();
|
||||
}
|
||||
});
|
||||
|
||||
// If service worker doesn't succeed in a reasonable amount of time,
|
||||
// fallback to plaint <script> tag.
|
||||
setTimeout(() => {
|
||||
if (!scriptLoaded) {
|
||||
console.warn(
|
||||
'Failed to load app from service worker. Falling back to plain <script> tag.',
|
||||
);
|
||||
loadMainDartJs();
|
||||
}
|
||||
}, 4000);
|
||||
});
|
||||
} else {
|
||||
// Service workers not supported. Just drop the <script> tag.
|
||||
loadMainDartJs();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "sandbox",
|
||||
"short_name": "sandbox",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "A new Flutter project.",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export 'mocks.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,7 @@
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
|
||||
class TestGame extends Forge2DGame {
|
||||
TestGame() {
|
||||
images.prefix = '';
|
||||
}
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
// ignore_for_file: cascade_invocations
|
||||
|
||||
import 'package:flame_forge2d/flame_forge2d.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() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final flameTester = FlameTester(TestGame.new);
|
||||
|
||||
group('Ball', () {
|
||||
flameTester.test(
|
||||
'loads correctly',
|
||||
(game) async {
|
||||
final ball = Ball(baseColor: Colors.blue);
|
||||
await game.ready();
|
||||
await game.ensureAdd(ball);
|
||||
|
||||
expect(game.contains(ball), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
group('body', () {
|
||||
flameTester.test(
|
||||
'is dynamic',
|
||||
(game) async {
|
||||
final ball = Ball(baseColor: Colors.blue);
|
||||
await game.ensureAdd(ball);
|
||||
|
||||
expect(ball.body.bodyType, equals(BodyType.dynamic));
|
||||
},
|
||||
);
|
||||
|
||||
group('can be moved', () {
|
||||
flameTester.test('by its weight', (game) async {
|
||||
final ball = Ball(baseColor: Colors.blue);
|
||||
await game.ensureAdd(ball);
|
||||
|
||||
game.update(1);
|
||||
expect(ball.body.position, isNot(equals(ball.initialPosition)));
|
||||
});
|
||||
|
||||
flameTester.test('by applying velocity', (game) async {
|
||||
final ball = Ball(baseColor: Colors.blue);
|
||||
await game.ensureAdd(ball);
|
||||
|
||||
ball.body.gravityScale = 0;
|
||||
ball.body.linearVelocity.setValues(10, 10);
|
||||
game.update(1);
|
||||
expect(ball.body.position, isNot(equals(ball.initialPosition)));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('fixture', () {
|
||||
flameTester.test(
|
||||
'exists',
|
||||
(game) async {
|
||||
final ball = Ball(baseColor: Colors.blue);
|
||||
await game.ensureAdd(ball);
|
||||
|
||||
expect(ball.body.fixtures[0], isA<Fixture>());
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.test(
|
||||
'is dense',
|
||||
(game) async {
|
||||
final ball = Ball(baseColor: Colors.blue);
|
||||
await game.ensureAdd(ball);
|
||||
|
||||
final fixture = ball.body.fixtures[0];
|
||||
expect(fixture.density, greaterThan(0));
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.test(
|
||||
'shape is circular',
|
||||
(game) async {
|
||||
final ball = Ball(baseColor: Colors.blue);
|
||||
await game.ensureAdd(ball);
|
||||
|
||||
final fixture = ball.body.fixtures[0];
|
||||
expect(fixture.shape.shapeType, equals(ShapeType.circle));
|
||||
expect(fixture.shape.radius, equals(1.5));
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.test(
|
||||
'has Layer.all as default filter maskBits',
|
||||
(game) async {
|
||||
final ball = Ball(baseColor: Colors.blue);
|
||||
await game.ready();
|
||||
await game.ensureAdd(ball);
|
||||
await game.ready();
|
||||
|
||||
final fixture = ball.body.fixtures[0];
|
||||
expect(fixture.filterData.maskBits, equals(Layer.board.maskBits));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('stop', () {
|
||||
group("can't be moved", () {
|
||||
flameTester.test('by its weight', (game) async {
|
||||
final ball = Ball(baseColor: Colors.blue);
|
||||
await game.ensureAdd(ball);
|
||||
ball.stop();
|
||||
|
||||
game.update(1);
|
||||
expect(ball.body.position, equals(ball.initialPosition));
|
||||
});
|
||||
});
|
||||
|
||||
flameTester.test('by applying velocity', (game) async {
|
||||
final ball = Ball(baseColor: Colors.blue);
|
||||
await game.ensureAdd(ball);
|
||||
ball.stop();
|
||||
|
||||
ball.body.linearVelocity.setValues(10, 10);
|
||||
game.update(1);
|
||||
expect(ball.body.position, equals(ball.initialPosition));
|
||||
});
|
||||
});
|
||||
|
||||
group('resume', () {
|
||||
group('can move', () {
|
||||
flameTester.test(
|
||||
'by its weight when previously stopped',
|
||||
(game) async {
|
||||
final ball = Ball(baseColor: Colors.blue);
|
||||
await game.ensureAdd(ball);
|
||||
ball.stop();
|
||||
ball.resume();
|
||||
|
||||
game.update(1);
|
||||
expect(ball.body.position, isNot(equals(ball.initialPosition)));
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.test(
|
||||
'by applying velocity when previously stopped',
|
||||
(game) async {
|
||||
final ball = Ball(baseColor: Colors.blue);
|
||||
await game.ensureAdd(ball);
|
||||
ball.stop();
|
||||
ball.resume();
|
||||
|
||||
ball.body.gravityScale = 0;
|
||||
ball.body.linearVelocity.setValues(10, 10);
|
||||
game.update(1);
|
||||
expect(ball.body.position, isNot(equals(ball.initialPosition)));
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('boost', () {
|
||||
flameTester.test('applies an impulse to the ball', (game) async {
|
||||
final ball = Ball(baseColor: Colors.blue);
|
||||
await game.ensureAdd(ball);
|
||||
|
||||
expect(ball.body.linearVelocity, equals(Vector2.zero()));
|
||||
|
||||
ball.boost(Vector2.all(10));
|
||||
expect(ball.body.linearVelocity.x, greaterThan(0));
|
||||
expect(ball.body.linearVelocity.y, greaterThan(0));
|
||||
});
|
||||
|
||||
flameTester.test('adds fire effect components to the game', (game) async {
|
||||
final ball = Ball(baseColor: Colors.blue);
|
||||
await game.ensureAdd(ball);
|
||||
|
||||
ball.boost(Vector2.all(10));
|
||||
game.update(0);
|
||||
await game.ready();
|
||||
|
||||
expect(game.children.whereType<FireEffect>().length, greaterThan(0));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -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,66 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_components/src/components/components.dart';
|
||||
|
||||
void main() {
|
||||
group('ArcShape', () {
|
||||
test('can be instantiated', () {
|
||||
expect(
|
||||
ArcShape(
|
||||
center: Vector2.zero(),
|
||||
arcRadius: 10,
|
||||
angle: 2 * math.pi,
|
||||
),
|
||||
isNotNull,
|
||||
);
|
||||
});
|
||||
|
||||
group('copyWith', () {
|
||||
test(
|
||||
'copies correctly '
|
||||
'when no argument specified', () {
|
||||
final arcShape = ArcShape(
|
||||
center: Vector2.zero(),
|
||||
arcRadius: 10,
|
||||
angle: 2 * math.pi,
|
||||
);
|
||||
final arcShapeCopied = arcShape.copyWith();
|
||||
|
||||
for (var index = 0; index < arcShape.vertices.length; index++) {
|
||||
expect(
|
||||
arcShape.vertices[index],
|
||||
equals(arcShapeCopied.vertices[index]),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'copies correctly '
|
||||
'when all arguments specified', () {
|
||||
final arcShapeExpected = ArcShape(
|
||||
center: Vector2.all(10),
|
||||
arcRadius: 15,
|
||||
angle: 2 * math.pi,
|
||||
);
|
||||
final arcShapeCopied = ArcShape(
|
||||
center: Vector2.zero(),
|
||||
arcRadius: 10,
|
||||
angle: math.pi,
|
||||
).copyWith(
|
||||
center: Vector2.all(10),
|
||||
arcRadius: 15,
|
||||
angle: 2 * math.pi,
|
||||
);
|
||||
|
||||
for (var index = 0; index < arcShapeCopied.vertices.length; index++) {
|
||||
expect(
|
||||
arcShapeCopied.vertices[index],
|
||||
equals(arcShapeExpected.vertices[index]),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_components/src/components/components.dart';
|
||||
|
||||
void main() {
|
||||
group('BezierCurveShape', () {
|
||||
final controlPoints = [
|
||||
Vector2(0, 0),
|
||||
Vector2(10, 0),
|
||||
Vector2(0, 10),
|
||||
Vector2(10, 10),
|
||||
];
|
||||
|
||||
test('can be instantiated', () {
|
||||
expect(
|
||||
BezierCurveShape(
|
||||
controlPoints: controlPoints,
|
||||
),
|
||||
isNotNull,
|
||||
);
|
||||
});
|
||||
|
||||
group('rotate', () {
|
||||
test('returns vertices rotated', () {
|
||||
const rotationAngle = 2 * math.pi;
|
||||
final controlPoints = [
|
||||
Vector2(0, 0),
|
||||
Vector2(10, 0),
|
||||
Vector2(0, 10),
|
||||
Vector2(10, 10),
|
||||
];
|
||||
|
||||
final bezierCurveShape = BezierCurveShape(
|
||||
controlPoints: controlPoints,
|
||||
);
|
||||
final bezierCurveShapeRotated = BezierCurveShape(
|
||||
controlPoints: controlPoints,
|
||||
)..rotate(rotationAngle);
|
||||
|
||||
for (var index = 0; index < bezierCurveShape.vertices.length; index++) {
|
||||
expect(
|
||||
bezierCurveShape.vertices[index]..rotate(rotationAngle),
|
||||
equals(bezierCurveShapeRotated.vertices[index]),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_components/src/components/components.dart';
|
||||
|
||||
void main() {
|
||||
group('EllipseShape', () {
|
||||
test('can be instantiated', () {
|
||||
expect(
|
||||
EllipseShape(
|
||||
center: Vector2.zero(),
|
||||
majorRadius: 10,
|
||||
minorRadius: 8,
|
||||
),
|
||||
isNotNull,
|
||||
);
|
||||
});
|
||||
|
||||
group('rotate', () {
|
||||
test('returns vertices rotated', () {
|
||||
const rotationAngle = 2 * math.pi;
|
||||
final ellipseShape = EllipseShape(
|
||||
center: Vector2.zero(),
|
||||
majorRadius: 10,
|
||||
minorRadius: 8,
|
||||
);
|
||||
final ellipseShapeRotated = EllipseShape(
|
||||
center: Vector2.zero(),
|
||||
majorRadius: 10,
|
||||
minorRadius: 8,
|
||||
)..rotate(rotationAngle);
|
||||
|
||||
for (var index = 0; index < ellipseShape.vertices.length; index++) {
|
||||
expect(
|
||||
ellipseShape.vertices[index]..rotate(rotationAngle),
|
||||
equals(ellipseShapeRotated.vertices[index]),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('copyWith', () {
|
||||
test('returns same shape when no properties are passed', () {
|
||||
final ellipseShape = EllipseShape(
|
||||
center: Vector2.zero(),
|
||||
majorRadius: 10,
|
||||
minorRadius: 8,
|
||||
);
|
||||
final ellipseShapeCopied = ellipseShape.copyWith();
|
||||
|
||||
for (var index = 0; index < ellipseShape.vertices.length; index++) {
|
||||
expect(
|
||||
ellipseShape.vertices[index],
|
||||
equals(ellipseShapeCopied.vertices[index]),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('returns object with updated properties when are passed', () {
|
||||
final ellipseShapeExpected = EllipseShape(
|
||||
center: Vector2.all(10),
|
||||
majorRadius: 10,
|
||||
minorRadius: 8,
|
||||
);
|
||||
final ellipseShapeCopied = EllipseShape(
|
||||
center: Vector2.zero(),
|
||||
majorRadius: 10,
|
||||
minorRadius: 8,
|
||||
).copyWith(center: Vector2.all(10));
|
||||
|
||||
for (var index = 0;
|
||||
index < ellipseShapeCopied.vertices.length;
|
||||
index++) {
|
||||
expect(
|
||||
ellipseShapeCopied.vertices[index],
|
||||
equals(ellipseShapeExpected.vertices[index]),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|