@ -0,0 +1,22 @@
|
||||
name: authentication_repository
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "packages/authentication_repository/**"
|
||||
- ".github/workflows/authentication_repository.yaml"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "packages/authentication_repository/**"
|
||||
- ".github/workflows/authentication_repository.yaml"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
|
||||
with:
|
||||
working_directory: packages/authentication_repository
|
@ -0,0 +1,76 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinball/l10n/l10n.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
/// {@template footer}
|
||||
/// Footer widget with links to the main tech stack.
|
||||
/// {@endtemplate}
|
||||
class Footer extends StatelessWidget {
|
||||
/// {@macro footer}
|
||||
const Footer({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(50, 0, 50, 32),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: const [
|
||||
_MadeWithFlutterAndFirebase(),
|
||||
_GoogleIO(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GoogleIO extends StatelessWidget {
|
||||
const _GoogleIO({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final theme = Theme.of(context);
|
||||
return Text(
|
||||
l10n.footerGoogleIOText,
|
||||
style: theme.textTheme.bodyText1!.copyWith(color: PinballColors.white),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MadeWithFlutterAndFirebase extends StatelessWidget {
|
||||
const _MadeWithFlutterAndFirebase({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final theme = Theme.of(context);
|
||||
return RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: l10n.footerMadeWithText,
|
||||
style: theme.textTheme.bodyText1!.copyWith(color: PinballColors.white),
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: l10n.footerFlutterLinkText,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => openLink('https://flutter.dev'),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: ' & '),
|
||||
TextSpan(
|
||||
text: l10n.footerFirebaseLinkText,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => openLink('https://firebase.google.com'),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
{
|
||||
"@@locale": "es",
|
||||
"play": "Jugar",
|
||||
"@play": {
|
||||
"description": "Text displayed on the landing page play button"
|
||||
},
|
||||
"start": "Comienzo",
|
||||
"@start": {
|
||||
"description": "Text displayed on the character selection page start button"
|
||||
},
|
||||
"characterSelectionTitle": "¡Elige a tu personaje!",
|
||||
"@characterSelectionTitle": {
|
||||
"description": "Title text displayed on the character selection page"
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export 'app_colors.dart';
|
||||
export 'app_text_style.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 @@
|
||||
# authentication_repository
|
||||
|
||||
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
|
||||
[![License: MIT][license_badge]][license_link]
|
||||
|
||||
Repository to manage user authentication.
|
||||
|
||||
[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 @@
|
||||
include: package:very_good_analysis/analysis_options.2.4.0.yaml
|
@ -0,0 +1,3 @@
|
||||
library authentication_repository;
|
||||
|
||||
export 'src/authentication_repository.dart';
|
@ -0,0 +1,36 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
|
||||
/// {@template authentication_exception}
|
||||
/// Exception for authentication repository failures.
|
||||
/// {@endtemplate}
|
||||
class AuthenticationException implements Exception {
|
||||
/// {@macro authentication_exception}
|
||||
const AuthenticationException(this.error, this.stackTrace);
|
||||
|
||||
/// The error that was caught.
|
||||
final Object error;
|
||||
|
||||
/// The Stacktrace associated with the [error].
|
||||
final StackTrace stackTrace;
|
||||
}
|
||||
|
||||
/// {@template authentication_repository}
|
||||
/// Repository to manage user authentication.
|
||||
/// {@endtemplate}
|
||||
class AuthenticationRepository {
|
||||
/// {@macro authentication_repository}
|
||||
AuthenticationRepository(this._firebaseAuth);
|
||||
|
||||
final FirebaseAuth _firebaseAuth;
|
||||
|
||||
/// Sign in the existing user anonymously using [FirebaseAuth]. If the
|
||||
/// authentication process can't be completed, it will throw an
|
||||
/// [AuthenticationException].
|
||||
Future<void> authenticateAnonymously() async {
|
||||
try {
|
||||
await _firebaseAuth.signInAnonymously();
|
||||
} on Exception catch (error, stackTrace) {
|
||||
throw AuthenticationException(error, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
name: authentication_repository
|
||||
description: Repository to manage user authentication.
|
||||
version: 1.0.0+1
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=2.16.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
firebase_auth: ^3.3.16
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
mocktail: ^0.2.0
|
||||
very_good_analysis: ^2.4.0
|
@ -0,0 +1,40 @@
|
||||
import 'package:authentication_repository/authentication_repository.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockFirebaseAuth extends Mock implements FirebaseAuth {}
|
||||
|
||||
class MockUserCredential extends Mock implements UserCredential {}
|
||||
|
||||
void main() {
|
||||
late FirebaseAuth firebaseAuth;
|
||||
late UserCredential userCredential;
|
||||
late AuthenticationRepository authenticationRepository;
|
||||
|
||||
group('AuthenticationRepository', () {
|
||||
setUp(() {
|
||||
firebaseAuth = MockFirebaseAuth();
|
||||
userCredential = MockUserCredential();
|
||||
authenticationRepository = AuthenticationRepository(firebaseAuth);
|
||||
});
|
||||
|
||||
group('authenticateAnonymously', () {
|
||||
test('completes if no exception is thrown', () async {
|
||||
when(() => firebaseAuth.signInAnonymously())
|
||||
.thenAnswer((_) async => userCredential);
|
||||
await authenticationRepository.authenticateAnonymously();
|
||||
verify(() => firebaseAuth.signInAnonymously()).called(1);
|
||||
});
|
||||
|
||||
test('throws AuthenticationException when firebase auth fails', () async {
|
||||
when(() => firebaseAuth.signInAnonymously())
|
||||
.thenThrow(Exception('oops'));
|
||||
expect(
|
||||
() => authenticationRepository.authenticateAnonymously(),
|
||||
throwsA(isA<AuthenticationException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 616 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 735 KiB |
After Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 231 KiB |
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 156 KiB |
@ -0,0 +1,209 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball_components/gen/assets.gen.dart';
|
||||
import 'package:pinball_components/pinball_components.dart' hide Assets;
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
class AndroidSpaceship extends Blueprint {
|
||||
AndroidSpaceship({required Vector2 position})
|
||||
: super(
|
||||
components: [
|
||||
_SpaceshipSaucer()..initialPosition = position,
|
||||
_SpaceshipSaucerSpriteAnimationComponent()..position = position,
|
||||
_LightBeamSpriteComponent()..position = position + Vector2(2.5, 5),
|
||||
_AndroidHead()..initialPosition = position + Vector2(0.5, 0.25),
|
||||
_SpaceshipHole(
|
||||
outsideLayer: Layer.spaceshipExitRail,
|
||||
outsidePriority: RenderPriority.ballOnSpaceshipRail,
|
||||
)..initialPosition = position - Vector2(5.3, -5.4),
|
||||
_SpaceshipHole(
|
||||
outsideLayer: Layer.board,
|
||||
outsidePriority: RenderPriority.ballOnBoard,
|
||||
)..initialPosition = position - Vector2(-7.5, -1.1),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
|
||||
_SpaceshipSaucer() : super(renderBody: false) {
|
||||
layer = Layer.spaceship;
|
||||
}
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final shape = _SpaceshipSaucerShape();
|
||||
final bodyDef = BodyDef(
|
||||
position: initialPosition,
|
||||
userData: this,
|
||||
angle: -1.7,
|
||||
);
|
||||
|
||||
return world.createBody(bodyDef)..createFixtureFromShape(shape);
|
||||
}
|
||||
}
|
||||
|
||||
class _SpaceshipSaucerShape extends ChainShape {
|
||||
_SpaceshipSaucerShape() {
|
||||
const minorRadius = 9.75;
|
||||
const majorRadius = 11.9;
|
||||
|
||||
createChain(
|
||||
[
|
||||
for (var angle = 0.2618; angle <= 6.0214; angle += math.pi / 180)
|
||||
Vector2(
|
||||
minorRadius * math.cos(angle),
|
||||
majorRadius * math.sin(angle),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SpaceshipSaucerSpriteAnimationComponent extends SpriteAnimationComponent
|
||||
with HasGameRef {
|
||||
_SpaceshipSaucerSpriteAnimationComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
priority: RenderPriority.spaceshipSaucer,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
final spriteSheet = gameRef.images.fromCache(
|
||||
Assets.images.android.spaceship.saucer.keyName,
|
||||
);
|
||||
|
||||
const amountPerRow = 5;
|
||||
const amountPerColumn = 3;
|
||||
final textureSize = Vector2(
|
||||
spriteSheet.width / amountPerRow,
|
||||
spriteSheet.height / amountPerColumn,
|
||||
);
|
||||
size = textureSize / 10;
|
||||
|
||||
animation = SpriteAnimation.fromFrameData(
|
||||
spriteSheet,
|
||||
SpriteAnimationData.sequenced(
|
||||
amount: amountPerRow * amountPerColumn,
|
||||
amountPerRow: amountPerRow,
|
||||
stepTime: 1 / 24,
|
||||
textureSize: textureSize,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(allisonryan0002): add pulsing behavior.
|
||||
class _LightBeamSpriteComponent extends SpriteComponent with HasGameRef {
|
||||
_LightBeamSpriteComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
priority: RenderPriority.spaceshipLightBeam,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
final sprite = Sprite(
|
||||
gameRef.images.fromCache(
|
||||
Assets.images.android.spaceship.lightBeam.keyName,
|
||||
),
|
||||
);
|
||||
this.sprite = sprite;
|
||||
size = sprite.originalSize / 10;
|
||||
}
|
||||
}
|
||||
|
||||
class _AndroidHead extends BodyComponent with InitialPosition, Layered {
|
||||
_AndroidHead()
|
||||
: super(
|
||||
priority: RenderPriority.androidHead,
|
||||
children: [_AndroidHeadSpriteAnimationComponent()],
|
||||
renderBody: false,
|
||||
) {
|
||||
layer = Layer.spaceship;
|
||||
}
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final shape = EllipseShape(
|
||||
center: Vector2.zero(),
|
||||
majorRadius: 3.1,
|
||||
minorRadius: 2,
|
||||
)..rotate(1.4);
|
||||
// TODO(allisonryan0002): use bumping behavior.
|
||||
final fixtureDef = FixtureDef(
|
||||
shape,
|
||||
restitution: 0.1,
|
||||
);
|
||||
final bodyDef = BodyDef(position: initialPosition);
|
||||
|
||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||
}
|
||||
}
|
||||
|
||||
class _AndroidHeadSpriteAnimationComponent extends SpriteAnimationComponent
|
||||
with HasGameRef {
|
||||
_AndroidHeadSpriteAnimationComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(-0.24, -2.6),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
final spriteSheet = gameRef.images.fromCache(
|
||||
Assets.images.android.spaceship.animatronic.keyName,
|
||||
);
|
||||
|
||||
const amountPerRow = 18;
|
||||
const amountPerColumn = 4;
|
||||
final textureSize = Vector2(
|
||||
spriteSheet.width / amountPerRow,
|
||||
spriteSheet.height / amountPerColumn,
|
||||
);
|
||||
size = textureSize / 10;
|
||||
|
||||
animation = SpriteAnimation.fromFrameData(
|
||||
spriteSheet,
|
||||
SpriteAnimationData.sequenced(
|
||||
amount: amountPerRow * amountPerColumn,
|
||||
amountPerRow: amountPerRow,
|
||||
stepTime: 1 / 24,
|
||||
textureSize: textureSize,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SpaceshipHole extends LayerSensor {
|
||||
_SpaceshipHole({required Layer outsideLayer, required int outsidePriority})
|
||||
: super(
|
||||
insideLayer: Layer.spaceship,
|
||||
outsideLayer: outsideLayer,
|
||||
orientation: LayerEntranceOrientation.down,
|
||||
insidePriority: RenderPriority.ballOnSpaceship,
|
||||
outsidePriority: outsidePriority,
|
||||
) {
|
||||
layer = Layer.spaceship;
|
||||
}
|
||||
|
||||
@override
|
||||
Shape get shape {
|
||||
return ArcShape(
|
||||
center: Vector2(0, -3.2),
|
||||
arcRadius: 5,
|
||||
angle: 1,
|
||||
rotation: -2,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
class BoardBackgroundSpriteComponent extends SpriteComponent with HasGameRef {
|
||||
BoardBackgroundSpriteComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
priority: RenderPriority.boardBackground,
|
||||
position: Vector2(0, -1),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
final sprite = Sprite(
|
||||
gameRef.images.fromCache(
|
||||
Assets.images.boardBackground.keyName,
|
||||
),
|
||||
);
|
||||
this.sprite = sprite;
|
||||
size = sprite.originalSize / 10;
|
||||
}
|
||||
}
|
@ -1,246 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball_components/gen/assets.gen.dart';
|
||||
import 'package:pinball_components/pinball_components.dart' hide Assets;
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template spaceship}
|
||||
/// A [Blueprint] which creates the spaceship feature.
|
||||
/// {@endtemplate}
|
||||
class Spaceship extends Blueprint {
|
||||
/// {@macro spaceship}
|
||||
Spaceship({required Vector2 position})
|
||||
: super(
|
||||
components: [
|
||||
SpaceshipSaucer()..initialPosition = position,
|
||||
_SpaceshipEntrance()..initialPosition = position,
|
||||
AndroidHead()..initialPosition = position,
|
||||
_SpaceshipHole(
|
||||
outsideLayer: Layer.spaceshipExitRail,
|
||||
outsidePriority: RenderPriority.ballOnSpaceshipRail,
|
||||
)..initialPosition = position - Vector2(5.2, -4.8),
|
||||
_SpaceshipHole(
|
||||
outsideLayer: Layer.board,
|
||||
outsidePriority: RenderPriority.ballOnBoard,
|
||||
)..initialPosition = position - Vector2(-7.2, -0.8),
|
||||
SpaceshipWall()..initialPosition = position,
|
||||
],
|
||||
);
|
||||
|
||||
/// Total size of the spaceship.
|
||||
static final size = Vector2(25, 19);
|
||||
}
|
||||
|
||||
/// {@template spaceship_saucer}
|
||||
/// A [BodyComponent] for the base, or the saucer of the spaceship
|
||||
/// {@endtemplate}
|
||||
class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
|
||||
/// {@macro spaceship_saucer}
|
||||
SpaceshipSaucer()
|
||||
: super(
|
||||
priority: RenderPriority.spaceshipSaucer,
|
||||
renderBody: false,
|
||||
children: [
|
||||
_SpaceshipSaucerSpriteComponent(),
|
||||
],
|
||||
) {
|
||||
layer = Layer.spaceship;
|
||||
}
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final shape = CircleShape()..radius = 3;
|
||||
final fixtureDef = FixtureDef(
|
||||
shape,
|
||||
isSensor: true,
|
||||
);
|
||||
final bodyDef = BodyDef(
|
||||
position: initialPosition,
|
||||
userData: this,
|
||||
);
|
||||
|
||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||
}
|
||||
}
|
||||
|
||||
class _SpaceshipSaucerSpriteComponent extends SpriteComponent with HasGameRef {
|
||||
_SpaceshipSaucerSpriteComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
// TODO(alestiago): Refactor to use sprite orignial size instead.
|
||||
size: Spaceship.size,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
// TODO(alestiago): Use cached sprite.
|
||||
sprite = await gameRef.loadSprite(
|
||||
Assets.images.spaceship.saucer.keyName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// {@template spaceship_bridge}
|
||||
/// A [BodyComponent] that provides both the collision and the rotation
|
||||
/// animation for the bridge.
|
||||
/// {@endtemplate}
|
||||
class AndroidHead extends BodyComponent with InitialPosition, Layered {
|
||||
/// {@macro spaceship_bridge}
|
||||
AndroidHead()
|
||||
: super(
|
||||
priority: RenderPriority.androidHead,
|
||||
children: [_AndroidHeadSpriteAnimation()],
|
||||
renderBody: false,
|
||||
) {
|
||||
layer = Layer.spaceship;
|
||||
}
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final circleShape = CircleShape()..radius = 2;
|
||||
|
||||
final bodyDef = BodyDef(
|
||||
position: initialPosition,
|
||||
userData: this,
|
||||
);
|
||||
|
||||
return world.createBody(bodyDef)
|
||||
..createFixture(
|
||||
FixtureDef(circleShape)..restitution = 0.4,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AndroidHeadSpriteAnimation extends SpriteAnimationComponent
|
||||
with HasGameRef {
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
final image = await gameRef.images.load(
|
||||
Assets.images.spaceship.bridge.keyName,
|
||||
);
|
||||
size = Vector2(8.2, 10);
|
||||
position = Vector2(0, -2);
|
||||
anchor = Anchor.center;
|
||||
|
||||
final data = SpriteAnimationData.sequenced(
|
||||
amount: 72,
|
||||
amountPerRow: 24,
|
||||
stepTime: 0.05,
|
||||
textureSize: size * 10,
|
||||
);
|
||||
animation = SpriteAnimation.fromFrameData(image, data);
|
||||
}
|
||||
}
|
||||
|
||||
class _SpaceshipEntrance extends LayerSensor {
|
||||
_SpaceshipEntrance()
|
||||
: super(
|
||||
insideLayer: Layer.spaceship,
|
||||
orientation: LayerEntranceOrientation.up,
|
||||
insidePriority: RenderPriority.ballOnSpaceship,
|
||||
) {
|
||||
layer = Layer.spaceship;
|
||||
}
|
||||
|
||||
@override
|
||||
Shape get shape {
|
||||
final radius = Spaceship.size.y / 2;
|
||||
return PolygonShape()
|
||||
..setAsEdge(
|
||||
Vector2(
|
||||
radius * cos(20 * pi / 180),
|
||||
radius * sin(20 * pi / 180),
|
||||
)..rotate(90 * pi / 180),
|
||||
Vector2(
|
||||
radius * cos(340 * pi / 180),
|
||||
radius * sin(340 * pi / 180),
|
||||
)..rotate(90 * pi / 180),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SpaceshipHole extends LayerSensor {
|
||||
_SpaceshipHole({required Layer outsideLayer, required int outsidePriority})
|
||||
: super(
|
||||
insideLayer: Layer.spaceship,
|
||||
outsideLayer: outsideLayer,
|
||||
orientation: LayerEntranceOrientation.down,
|
||||
insidePriority: RenderPriority.ballOnSpaceship,
|
||||
outsidePriority: outsidePriority,
|
||||
) {
|
||||
layer = Layer.spaceship;
|
||||
}
|
||||
|
||||
@override
|
||||
Shape get shape {
|
||||
return ArcShape(
|
||||
center: Vector2(0, -3.2),
|
||||
arcRadius: 5,
|
||||
angle: 1,
|
||||
rotation: -2,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// {@template spaceship_wall_shape}
|
||||
/// The [ChainShape] that defines the shape of the [SpaceshipWall].
|
||||
/// {@endtemplate}
|
||||
class _SpaceshipWallShape extends ChainShape {
|
||||
/// {@macro spaceship_wall_shape}
|
||||
_SpaceshipWallShape() {
|
||||
final minorRadius = (Spaceship.size.y - 2) / 2;
|
||||
final majorRadius = (Spaceship.size.x - 2) / 2;
|
||||
|
||||
createChain(
|
||||
[
|
||||
// TODO(alestiago): Try converting this logic to radian.
|
||||
for (var angle = 20; angle <= 340; angle++)
|
||||
Vector2(
|
||||
minorRadius * cos(angle * pi / 180),
|
||||
majorRadius * sin(angle * pi / 180),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// {@template spaceship_wall}
|
||||
/// A [BodyComponent] that provides the collision for the wall
|
||||
/// surrounding the spaceship.
|
||||
///
|
||||
/// It has a small opening to allow the [Ball] to get inside the spaceship
|
||||
/// saucer.
|
||||
///
|
||||
/// It also contains the [SpriteComponent] for the lower wall
|
||||
/// {@endtemplate}
|
||||
class SpaceshipWall extends BodyComponent with InitialPosition, Layered {
|
||||
/// {@macro spaceship_wall}
|
||||
SpaceshipWall()
|
||||
: super(
|
||||
priority: RenderPriority.spaceshipSaucerWall,
|
||||
renderBody: false,
|
||||
) {
|
||||
layer = Layer.spaceship;
|
||||
}
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final shape = _SpaceshipWallShape();
|
||||
final fixtureDef = FixtureDef(shape);
|
||||
|
||||
final bodyDef = BodyDef(
|
||||
position: initialPosition,
|
||||
userData: this,
|
||||
angle: -1.7,
|
||||
);
|
||||
|
||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||
}
|
||||
}
|
@ -1,10 +1,8 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
part of 'sparky_bumper_cubit.dart';
|
||||
|
||||
/// Indicates the [SparkyBumperCubit]'s current state.
|
||||
enum SparkyBumperState {
|
||||
/// A lit up bumper.
|
||||
active,
|
||||
|
||||
/// A dimmed bumper.
|
||||
inactive,
|
||||
lit,
|
||||
dimmed,
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/input.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
import 'package:sandbox/stories/ball/basic_ball_game.dart';
|
||||
|
||||
class AndroidSpaceshipGame extends BallGame {
|
||||
AndroidSpaceshipGame()
|
||||
: super(
|
||||
ballPriority: RenderPriority.ballOnSpaceship,
|
||||
ballLayer: Layer.spaceship,
|
||||
imagesFileNames: [
|
||||
Assets.images.android.spaceship.saucer.keyName,
|
||||
Assets.images.android.spaceship.animatronic.keyName,
|
||||
Assets.images.android.spaceship.lightBeam.keyName,
|
||||
],
|
||||
);
|
||||
|
||||
static const description = '''
|
||||
Shows how the AndroidSpaceship is rendered.
|
||||
|
||||
- Activate the "trace" parameter to overlay the body.
|
||||
- Tap anywhere on the screen to spawn a Ball into the game.
|
||||
''';
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
camera.followVector2(Vector2.zero());
|
||||
await addFromBlueprint(
|
||||
AndroidSpaceship(position: Vector2.zero()),
|
||||
);
|
||||
|
||||
await traceAllBodies();
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/input.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
|
||||
class SpaceshipGame extends AssetsGame with TapDetector {
|
||||
static const description = '''
|
||||
Shows how a Spaceship works.
|
||||
|
||||
- Tap anywhere on the screen to spawn a Ball into the game.
|
||||
''';
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
camera.followVector2(Vector2.zero());
|
||||
await addFromBlueprint(
|
||||
Spaceship(position: Vector2.zero()),
|
||||
);
|
||||
await ready();
|
||||
}
|
||||
|
||||
@override
|
||||
void onTapUp(TapUpInfo info) {
|
||||
add(
|
||||
Ball(baseColor: Colors.blue)
|
||||
..initialPosition = info.eventPosition.game
|
||||
..layer = Layer.spaceshipEntranceRamp,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import 'package:dashbook/dashbook.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
import 'package:sandbox/stories/sparky_bumper/sparky_bumper_game.dart';
|
||||
|
||||
void addSparkyBumperStories(Dashbook dashbook) {
|
||||
dashbook.storiesOf('Sparky Bumpers').addGame(
|
||||
title: 'Traced',
|
||||
description: SparkyBumperGame.description,
|
||||
gameBuilder: (_) => SparkyBumperGame(),
|
||||
);
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/input.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
import 'package:sandbox/stories/ball/basic_ball_game.dart';
|
||||
|
||||
class SparkyComputerGame extends BallGame {
|
||||
static const description = '''
|
||||
Shows how the SparkyComputer is rendered.
|
||||
|
||||
- Activate the "trace" parameter to overlay the body.
|
||||
- Tap anywhere on the screen to spawn a ball into the game.
|
||||
''';
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
await images.loadAll([
|
||||
Assets.images.sparky.computer.base.keyName,
|
||||
Assets.images.sparky.computer.top.keyName,
|
||||
Assets.images.sparky.computer.glow.keyName,
|
||||
]);
|
||||
|
||||
camera.followVector2(Vector2(-10, -40));
|
||||
await addFromBlueprint(SparkyComputer());
|
||||
await ready();
|
||||
await traceAllBodies();
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import 'package:dashbook/dashbook.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
import 'package:sandbox/stories/sparky_scorch/sparky_bumper_game.dart';
|
||||
import 'package:sandbox/stories/sparky_scorch/sparky_computer_game.dart';
|
||||
|
||||
void addSparkyScorchStories(Dashbook dashbook) {
|
||||
dashbook.storiesOf('Sparky Scorch')
|
||||
..addGame(
|
||||
title: 'Sparky Computer',
|
||||
description: SparkyComputerGame.description,
|
||||
gameBuilder: (_) => SparkyComputerGame(),
|
||||
)
|
||||
..addGame(
|
||||
title: 'Sparky Bumper',
|
||||
description: SparkyBumperGame.description,
|
||||
gameBuilder: (_) => SparkyBumperGame(),
|
||||
);
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
// ignore_for_file: cascade_invocations
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
import '../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('AndroidSpaceship', () {
|
||||
group('Spaceship', () {
|
||||
final assets = [
|
||||
Assets.images.android.spaceship.saucer.keyName,
|
||||
Assets.images.android.spaceship.animatronic.keyName,
|
||||
Assets.images.android.spaceship.lightBeam.keyName,
|
||||
];
|
||||
final flameTester = FlameTester(() => TestGame(assets));
|
||||
|
||||
flameTester.test('loads correctly', (game) async {
|
||||
await game.addFromBlueprint(AndroidSpaceship(position: Vector2.zero()));
|
||||
await game.ready();
|
||||
});
|
||||
|
||||
flameTester.testGameWidget(
|
||||
'renders correctly',
|
||||
setUp: (game, tester) async {
|
||||
await game.images.loadAll(assets);
|
||||
await game
|
||||
.addFromBlueprint(AndroidSpaceship(position: Vector2.zero()));
|
||||
game.camera.followVector2(Vector2.zero());
|
||||
await game.ready();
|
||||
await tester.pump();
|
||||
},
|
||||
verify: (game, tester) async {
|
||||
final animationDuration = game
|
||||
.descendants()
|
||||
.whereType<SpriteAnimationComponent>()
|
||||
.last
|
||||
.animation!
|
||||
.totalDuration();
|
||||
|
||||
await expectLater(
|
||||
find.byGame<TestGame>(),
|
||||
matchesGoldenFile('golden/android_spaceship/start.png'),
|
||||
);
|
||||
|
||||
game.update(animationDuration * 0.5);
|
||||
await tester.pump();
|
||||
await expectLater(
|
||||
find.byGame<TestGame>(),
|
||||
matchesGoldenFile('golden/android_spaceship/middle.png'),
|
||||
);
|
||||
|
||||
game.update(animationDuration * 0.5);
|
||||
await tester.pump();
|
||||
await expectLater(
|
||||
find.byGame<TestGame>(),
|
||||
matchesGoldenFile('golden/android_spaceship/end.png'),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
// ignore_for_file: cascade_invocations
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
import '../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final assets = [
|
||||
Assets.images.boardBackground.keyName,
|
||||
];
|
||||
final flameTester = FlameTester(() => TestGame(assets));
|
||||
|
||||
group('BoardBackgroundSpriteComponent', () {
|
||||
flameTester.test(
|
||||
'loads correctly',
|
||||
(game) async {
|
||||
final boardBackground = BoardBackgroundSpriteComponent();
|
||||
await game.ensureAdd(boardBackground);
|
||||
|
||||
expect(game.contains(boardBackground), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.testGameWidget(
|
||||
'renders correctly',
|
||||
setUp: (game, tester) async {
|
||||
await game.images.loadAll(assets);
|
||||
final boardBackground = BoardBackgroundSpriteComponent();
|
||||
await game.ensureAdd(boardBackground);
|
||||
await tester.pump();
|
||||
|
||||
game.camera
|
||||
..followVector2(Vector2.zero())
|
||||
..zoom = 3.7;
|
||||
},
|
||||
verify: (game, tester) async {
|
||||
await expectLater(
|
||||
find.byGame<TestGame>(),
|
||||
matchesGoldenFile('golden/board-background.png'),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
After Width: | Height: | Size: 121 KiB |
After Width: | Height: | Size: 121 KiB |
After Width: | Height: | Size: 121 KiB |
After Width: | Height: | Size: 1.5 MiB |
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 155 KiB |
Before Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 209 KiB |