Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 200 KiB |
Before Width: | Height: | Size: 2.2 MiB |
@ -1,8 +1 @@
|
||||
// Copyright (c) 2021, 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.
|
||||
|
||||
export 'view/app.dart';
|
||||
|
@ -0,0 +1,2 @@
|
||||
export 'cubit/assets_manager_cubit.dart';
|
||||
export 'views/views.dart';
|
@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:pinball/assets_manager/assets_manager.dart';
|
||||
import 'package:pinball/l10n/l10n.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
/// {@template assets_loading_page}
|
||||
/// Widget used to indicate the loading progress of the different assets used
|
||||
/// in the game
|
||||
/// {@endtemplate}
|
||||
class AssetsLoadingPage extends StatelessWidget {
|
||||
/// {@macro assets_loading_page}
|
||||
const AssetsLoadingPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final headline1 = Theme.of(context).textTheme.headline1;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
l10n.ioPinball,
|
||||
style: headline1!.copyWith(fontSize: 80),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
AnimatedEllipsisText(
|
||||
l10n.loading,
|
||||
style: headline1,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
FractionallySizedBox(
|
||||
widthFactor: 0.8,
|
||||
child: BlocBuilder<AssetsManagerCubit, AssetsManagerState>(
|
||||
builder: (context, state) {
|
||||
return PinballLoadingIndicator(value: state.progress);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export 'assets_loading_page.dart';
|
@ -0,0 +1,27 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// Adds a [GameBonus.androidSpaceship] when [AndroidSpaceship] has a bonus.
|
||||
class AndroidSpaceshipBonusBehavior extends Component
|
||||
with HasGameRef<PinballGame>, ParentIsA<AndroidAcres> {
|
||||
@override
|
||||
void onMount() {
|
||||
super.onMount();
|
||||
final androidSpaceship = parent.firstChild<AndroidSpaceship>()!;
|
||||
|
||||
// TODO(alestiago): Refactor subscription management once the following is
|
||||
// merged:
|
||||
// https://github.com/flame-engine/flame/pull/1538
|
||||
androidSpaceship.bloc.stream.listen((state) {
|
||||
final listenWhen = state == AndroidSpaceshipState.withBonus;
|
||||
if (!listenWhen) return;
|
||||
|
||||
gameRef
|
||||
.read<GameBloc>()
|
||||
.add(const BonusActivated(GameBonus.androidSpaceship));
|
||||
androidSpaceship.bloc.onBonusAwarded();
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export 'android_spaceship_bonus_behavior.dart';
|
@ -1,15 +1,16 @@
|
||||
export 'android_acres.dart';
|
||||
export 'android_acres/android_acres.dart';
|
||||
export 'bottom_group.dart';
|
||||
export 'camera_controller.dart';
|
||||
export 'controlled_ball.dart';
|
||||
export 'controlled_flipper.dart';
|
||||
export 'controlled_plunger.dart';
|
||||
export 'dino_desert.dart';
|
||||
export 'dino_desert/dino_desert.dart';
|
||||
export 'drain.dart';
|
||||
export 'flutter_forest/flutter_forest.dart';
|
||||
export 'game_flow_controller.dart';
|
||||
export 'google_word/google_word.dart';
|
||||
export 'launcher.dart';
|
||||
export 'multiballs/multiballs.dart';
|
||||
export 'multipliers/multipliers.dart';
|
||||
export 'scoring_behavior.dart';
|
||||
export 'sparky_scorch.dart';
|
||||
|
@ -0,0 +1 @@
|
||||
export 'chrome_dino_bonus_behavior.dart';
|
@ -0,0 +1,24 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// Adds a [GameBonus.dinoChomp] when a [Ball] is chomped by the [ChromeDino].
|
||||
class ChromeDinoBonusBehavior extends Component
|
||||
with HasGameRef<PinballGame>, ParentIsA<DinoDesert> {
|
||||
@override
|
||||
void onMount() {
|
||||
super.onMount();
|
||||
final chromeDino = parent.firstChild<ChromeDino>()!;
|
||||
|
||||
// TODO(alestiago): Refactor subscription management once the following is
|
||||
// merged:
|
||||
// https://github.com/flame-engine/flame/pull/1538
|
||||
chromeDino.bloc.stream.listen((state) {
|
||||
final listenWhen = state.status == ChromeDinoStatus.chomping;
|
||||
if (!listenWhen) return;
|
||||
|
||||
gameRef.read<GameBloc>().add(const BonusActivated(GameBonus.dinoChomp));
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export 'multiballs_behavior.dart';
|
@ -0,0 +1,28 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_bloc/flame_bloc.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// Toggle each [Multiball] when there is a bonus ball.
|
||||
class MultiballsBehavior extends Component
|
||||
with
|
||||
HasGameRef<PinballGame>,
|
||||
ParentIsA<Multiballs>,
|
||||
BlocComponent<GameBloc, GameState> {
|
||||
@override
|
||||
bool listenWhen(GameState? previousState, GameState newState) {
|
||||
final hasChanged = previousState?.bonusHistory != newState.bonusHistory;
|
||||
final lastBonusIsMultiball = newState.bonusHistory.isNotEmpty &&
|
||||
newState.bonusHistory.last == GameBonus.dashNest;
|
||||
|
||||
return hasChanged && lastBonusIsMultiball;
|
||||
}
|
||||
|
||||
@override
|
||||
void onNewState(GameState state) {
|
||||
parent.children.whereType<Multiball>().forEach((multiball) {
|
||||
multiball.bloc.onAnimate();
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinball/game/components/multiballs/behaviors/behaviors.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template multiballs_component}
|
||||
/// A [SpriteGroupComponent] for the multiball over the board.
|
||||
/// {@endtemplate}
|
||||
class Multiballs extends Component with ZIndex {
|
||||
/// {@macro multiballs_component}
|
||||
Multiballs()
|
||||
: super(
|
||||
children: [
|
||||
Multiball.a(),
|
||||
Multiball.b(),
|
||||
Multiball.c(),
|
||||
Multiball.d(),
|
||||
MultiballsBehavior(),
|
||||
],
|
||||
) {
|
||||
zIndex = ZIndexes.decal;
|
||||
}
|
||||
|
||||
/// Creates a [Multiballs] without any children.
|
||||
///
|
||||
/// This can be used for testing [Multiballs]'s behaviors in isolation.
|
||||
@visibleForTesting
|
||||
Multiballs.test();
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/flame.dart';
|
||||
import 'package:flame/sprite.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
import 'package:pinball_theme/pinball_theme.dart';
|
||||
|
||||
/// {@template selected_character}
|
||||
/// Shows an animated version of the character currently selected.
|
||||
/// {@endtemplate}
|
||||
class SelectedCharacter extends StatefulWidget {
|
||||
/// {@macro selected_character}
|
||||
const SelectedCharacter({
|
||||
Key? key,
|
||||
required this.currentCharacter,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The character that is selected at the moment.
|
||||
final CharacterTheme currentCharacter;
|
||||
|
||||
@override
|
||||
State<SelectedCharacter> createState() => _SelectedCharacterState();
|
||||
|
||||
/// Returns a list of assets to be loaded.
|
||||
static List<Future> loadAssets() {
|
||||
return [
|
||||
Flame.images.load(const DashTheme().animation.keyName),
|
||||
Flame.images.load(const AndroidTheme().animation.keyName),
|
||||
Flame.images.load(const DinoTheme().animation.keyName),
|
||||
Flame.images.load(const SparkyTheme().animation.keyName),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectedCharacterState extends State<SelectedCharacter>
|
||||
with TickerProviderStateMixin {
|
||||
SpriteAnimationController? _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupCharacterAnimation();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant SelectedCharacter oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_setupCharacterAnimation();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
widget.currentCharacter.name,
|
||||
style: Theme.of(context).textTheme.headline2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
width: constraints.maxWidth,
|
||||
height: constraints.maxHeight,
|
||||
child: SpriteAnimationWidget(
|
||||
controller: _controller!,
|
||||
anchor: Anchor.center,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _setupCharacterAnimation() {
|
||||
final spriteSheet = SpriteSheet.fromColumnsAndRows(
|
||||
image: Flame.images.fromCache(widget.currentCharacter.animation.keyName),
|
||||
columns: 12,
|
||||
rows: 6,
|
||||
);
|
||||
final animation = spriteSheet.createAnimation(
|
||||
row: 0,
|
||||
stepTime: 1 / 24,
|
||||
to: spriteSheet.rows * spriteSheet.columns,
|
||||
);
|
||||
if (_controller != null) _controller?.dispose();
|
||||
_controller = SpriteAnimationController(vsync: this, animation: animation)
|
||||
..forward()
|
||||
..repeat();
|
||||
}
|
||||
}
|
@ -1 +1,2 @@
|
||||
export 'character_selection_page.dart';
|
||||
export 'selected_character.dart';
|
||||
|
@ -1 +0,0 @@
|
||||
export 'mocks.dart';
|
@ -1,34 +0,0 @@
|
||||
// ignore_for_file: one_member_abstracts
|
||||
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:flame_audio/audio_pool.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
abstract class _CreateAudioPoolStub {
|
||||
Future<AudioPool> onCall(
|
||||
String sound, {
|
||||
bool? repeating,
|
||||
int? maxPlayers,
|
||||
int? minPlayers,
|
||||
String? prefix,
|
||||
});
|
||||
}
|
||||
|
||||
class CreateAudioPoolStub extends Mock implements _CreateAudioPoolStub {}
|
||||
|
||||
abstract class _ConfigureAudioCacheStub {
|
||||
void onCall(AudioCache cache);
|
||||
}
|
||||
|
||||
class ConfigureAudioCacheStub extends Mock implements _ConfigureAudioCacheStub {
|
||||
}
|
||||
|
||||
abstract class _PlaySingleAudioStub {
|
||||
Future<void> onCall(String url);
|
||||
}
|
||||
|
||||
class PlaySingleAudioStub extends Mock implements _PlaySingleAudioStub {}
|
||||
|
||||
class MockAudioPool extends Mock implements AudioPool {}
|
||||
|
||||
class MockAudioCache extends Mock implements AudioCache {}
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 588 B |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 45 KiB |
@ -0,0 +1,71 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template android_animatronic}
|
||||
/// Animated Android that sits on top of the [AndroidSpaceship].
|
||||
/// {@endtemplate}
|
||||
class AndroidAnimatronic extends BodyComponent
|
||||
with InitialPosition, Layered, ZIndex {
|
||||
/// {@macro android_animatronic}
|
||||
AndroidAnimatronic({Iterable<Component>? children})
|
||||
: super(
|
||||
children: [
|
||||
_AndroidAnimatronicSpriteAnimationComponent(),
|
||||
...?children,
|
||||
],
|
||||
renderBody: false,
|
||||
) {
|
||||
layer = Layer.spaceship;
|
||||
zIndex = ZIndexes.androidHead;
|
||||
}
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final shape = EllipseShape(
|
||||
center: Vector2.zero(),
|
||||
majorRadius: 3.1,
|
||||
minorRadius: 2,
|
||||
)..rotate(1.4);
|
||||
final bodyDef = BodyDef(position: initialPosition);
|
||||
|
||||
return world.createBody(bodyDef)..createFixtureFromShape(shape);
|
||||
}
|
||||
}
|
||||
|
||||
class _AndroidAnimatronicSpriteAnimationComponent
|
||||
extends SpriteAnimationComponent with HasGameRef {
|
||||
_AndroidAnimatronicSpriteAnimationComponent()
|
||||
: 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
class AndroidSpaceshipEntranceBallContactBehavior
|
||||
extends ContactBehavior<AndroidSpaceshipEntrance> {
|
||||
@override
|
||||
void beginContact(Object other, Contact contact) {
|
||||
super.beginContact(other, contact);
|
||||
if (other is! Ball) return;
|
||||
|
||||
parent.parent.bloc.onBallEntered();
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export 'android_spaceship_entrance_ball_contact_behavior.dart.dart';
|
@ -0,0 +1,13 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
|
||||
part 'android_spaceship_state.dart';
|
||||
|
||||
class AndroidSpaceshipCubit extends Cubit<AndroidSpaceshipState> {
|
||||
AndroidSpaceshipCubit() : super(AndroidSpaceshipState.withoutBonus);
|
||||
|
||||
void onBallEntered() => emit(AndroidSpaceshipState.withBonus);
|
||||
|
||||
void onBonusAwarded() => emit(AndroidSpaceshipState.withoutBonus);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
part of 'android_spaceship_cubit.dart';
|
||||
|
||||
enum AndroidSpaceshipState {
|
||||
withoutBonus,
|
||||
withBonus,
|
||||
}
|
@ -1,186 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.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, ZIndex {
|
||||
/// {@macro ball}
|
||||
Ball({
|
||||
required this.baseColor,
|
||||
}) : super(
|
||||
renderBody: false,
|
||||
children: [
|
||||
_BallSpriteComponent()..tint(baseColor.withOpacity(0.5)),
|
||||
],
|
||||
) {
|
||||
// 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].
|
||||
static final Vector2 size = Vector2.all(4.13);
|
||||
|
||||
/// The base [Color] used to tint this [Ball].
|
||||
final Color baseColor;
|
||||
|
||||
@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.
|
||||
// TODO(allisonryan0002): prevent motion from contact with other balls.
|
||||
void stop() {
|
||||
body
|
||||
..gravityScale = Vector2.zero()
|
||||
..linearVelocity = Vector2.zero()
|
||||
..angularVelocity = 0;
|
||||
}
|
||||
|
||||
/// Allows the [Ball] to be affected by forces.
|
||||
///
|
||||
/// If previously [stop]ped, the previous ball's velocity is not kept.
|
||||
void resume() {
|
||||
body.gravityScale = Vector2(1, 1);
|
||||
}
|
||||
|
||||
/// Applies a boost and [_TurboChargeSpriteAnimationComponent] on this [Ball].
|
||||
Future<void> boost(Vector2 impulse) async {
|
||||
body.linearVelocity = impulse;
|
||||
await add(_TurboChargeSpriteAnimationComponent());
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
|
||||
_rescaleSize();
|
||||
_setPositionalGravity();
|
||||
}
|
||||
|
||||
void _rescaleSize() {
|
||||
final boardHeight = BoardDimensions.bounds.height;
|
||||
const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor;
|
||||
|
||||
final standardizedYPosition = body.position.y + (boardHeight / 2);
|
||||
|
||||
final scaleFactor = maxShrinkValue +
|
||||
((standardizedYPosition / boardHeight) * (1 - maxShrinkValue));
|
||||
|
||||
body.fixtures.first.shape.radius = (size.x / 2) * scaleFactor;
|
||||
|
||||
// TODO(alestiago): Revisit and see if there's a better way to do this.
|
||||
final spriteComponent = firstChild<_BallSpriteComponent>();
|
||||
spriteComponent?.scale = Vector2.all(scaleFactor);
|
||||
}
|
||||
|
||||
void _setPositionalGravity() {
|
||||
final defaultGravity = gameRef.world.gravity.y;
|
||||
final maxXDeviationFromCenter = BoardDimensions.bounds.width / 2;
|
||||
const maxXGravityPercentage =
|
||||
(1 - BoardDimensions.perspectiveShrinkFactor) / 2;
|
||||
final xDeviationFromCenter = body.position.x;
|
||||
|
||||
final positionalXForce = ((xDeviationFromCenter / maxXDeviationFromCenter) *
|
||||
maxXGravityPercentage) *
|
||||
defaultGravity;
|
||||
|
||||
final positionalYForce = math.sqrt(
|
||||
math.pow(defaultGravity, 2) - math.pow(positionalXForce, 2),
|
||||
);
|
||||
|
||||
body.gravityOverride = Vector2(positionalXForce, positionalYForce);
|
||||
}
|
||||
}
|
||||
|
||||
class _BallSpriteComponent extends SpriteComponent with HasGameRef {
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
final sprite = await gameRef.loadSprite(
|
||||
Assets.images.ball.ball.keyName,
|
||||
);
|
||||
this.sprite = sprite;
|
||||
size = sprite.originalSize / 10;
|
||||
anchor = Anchor.center;
|
||||
}
|
||||
}
|
||||
|
||||
class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent
|
||||
with HasGameRef, ZIndex {
|
||||
_TurboChargeSpriteAnimationComponent()
|
||||
: super(
|
||||
anchor: const Anchor(0.53, 0.72),
|
||||
removeOnFinish: true,
|
||||
) {
|
||||
zIndex = ZIndexes.turboChargeFlame;
|
||||
}
|
||||
|
||||
late final Vector2 _textureSize;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
final spriteSheet = await gameRef.images.load(
|
||||
Assets.images.ball.flameEffect.keyName,
|
||||
);
|
||||
|
||||
const amountPerRow = 8;
|
||||
const amountPerColumn = 4;
|
||||
_textureSize = Vector2(
|
||||
spriteSheet.width / amountPerRow,
|
||||
spriteSheet.height / amountPerColumn,
|
||||
);
|
||||
|
||||
animation = SpriteAnimation.fromFrameData(
|
||||
spriteSheet,
|
||||
SpriteAnimationData.sequenced(
|
||||
amount: amountPerRow * amountPerColumn,
|
||||
amountPerRow: amountPerRow,
|
||||
stepTime: 1 / 24,
|
||||
textureSize: _textureSize,
|
||||
loop: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
|
||||
if (parent != null) {
|
||||
final body = (parent! as BodyComponent).body;
|
||||
final direction = -body.linearVelocity.normalized();
|
||||
angle = math.atan2(direction.x, -direction.y);
|
||||
size = (_textureSize / 45) * body.fixtures.first.shape.radius;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
export 'behaviors/behaviors.dart';
|
||||
|
||||
/// {@template ball}
|
||||
/// A solid, [BodyType.dynamic] sphere that rolls and bounces around.
|
||||
/// {@endtemplate}
|
||||
class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
|
||||
/// {@macro ball}
|
||||
Ball({
|
||||
required this.baseColor,
|
||||
}) : super(
|
||||
renderBody: false,
|
||||
children: [
|
||||
_BallSpriteComponent()..tint(baseColor.withOpacity(0.5)),
|
||||
BallScalingBehavior(),
|
||||
BallGravitatingBehavior(),
|
||||
],
|
||||
) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// Creates a [Ball] without any behaviors.
|
||||
///
|
||||
/// This can be used for testing [Ball]'s behaviors in isolation.
|
||||
@visibleForTesting
|
||||
Ball.test({required this.baseColor})
|
||||
: super(
|
||||
children: [_BallSpriteComponent()],
|
||||
);
|
||||
|
||||
/// The size of the [Ball].
|
||||
static final Vector2 size = Vector2.all(4.13);
|
||||
|
||||
/// The base [Color] used to tint this [Ball].
|
||||
final Color baseColor;
|
||||
|
||||
@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.
|
||||
// TODO(allisonryan0002): prevent motion from contact with other balls.
|
||||
void stop() {
|
||||
body
|
||||
..gravityScale = Vector2.zero()
|
||||
..linearVelocity = Vector2.zero()
|
||||
..angularVelocity = 0;
|
||||
}
|
||||
|
||||
/// Allows the [Ball] to be affected by forces.
|
||||
///
|
||||
/// If previously [stop]ped, the previous ball's velocity is not kept.
|
||||
void resume() {
|
||||
body.gravityScale = Vector2(1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
class _BallSpriteComponent extends SpriteComponent with HasGameRef {
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
final sprite = await gameRef.loadSprite(
|
||||
Assets.images.ball.ball.keyName,
|
||||
);
|
||||
this.sprite = sprite;
|
||||
size = sprite.originalSize / 10;
|
||||
anchor = Anchor.center;
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// Scales the ball's gravity according to its position on the board.
|
||||
class BallGravitatingBehavior extends Component
|
||||
with ParentIsA<Ball>, HasGameRef<Forge2DGame> {
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
final defaultGravity = gameRef.world.gravity.y;
|
||||
|
||||
final maxXDeviationFromCenter = BoardDimensions.bounds.width / 2;
|
||||
const maxXGravityPercentage =
|
||||
(1 - BoardDimensions.perspectiveShrinkFactor) / 2;
|
||||
final xDeviationFromCenter = parent.body.position.x;
|
||||
|
||||
final positionalXForce = ((xDeviationFromCenter / maxXDeviationFromCenter) *
|
||||
maxXGravityPercentage) *
|
||||
defaultGravity;
|
||||
final positionalYForce = math.sqrt(
|
||||
math.pow(defaultGravity, 2) - math.pow(positionalXForce, 2),
|
||||
);
|
||||
|
||||
final gravityOverride = parent.body.gravityOverride;
|
||||
if (gravityOverride != null) {
|
||||
gravityOverride.setValues(positionalXForce, positionalYForce);
|
||||
} else {
|
||||
parent.body.gravityOverride = Vector2(positionalXForce, positionalYForce);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// Scales the ball's body and sprite according to its position on the board.
|
||||
class BallScalingBehavior extends Component with ParentIsA<Ball> {
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
final boardHeight = BoardDimensions.bounds.height;
|
||||
const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor;
|
||||
|
||||
final standardizedYPosition = parent.body.position.y + (boardHeight / 2);
|
||||
final scaleFactor = maxShrinkValue +
|
||||
((standardizedYPosition / boardHeight) * (1 - maxShrinkValue));
|
||||
|
||||
parent.body.fixtures.first.shape.radius = (Ball.size.x / 2) * scaleFactor;
|
||||
|
||||
parent.firstChild<SpriteComponent>()!.scale.setValues(
|
||||
scaleFactor,
|
||||
scaleFactor,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template ball_turbo_charging_behavior}
|
||||
/// Puts the [Ball] in flames and [_impulse]s it.
|
||||
/// {@endtemplate}
|
||||
class BallTurboChargingBehavior extends TimerComponent with ParentIsA<Ball> {
|
||||
/// {@macro ball_turbo_charging_behavior}
|
||||
BallTurboChargingBehavior({
|
||||
required Vector2 impulse,
|
||||
}) : _impulse = impulse,
|
||||
super(period: 5, removeOnFinish: true);
|
||||
|
||||
final Vector2 _impulse;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
parent.body.linearVelocity = _impulse;
|
||||
await parent.add(_TurboChargeSpriteAnimationComponent());
|
||||
}
|
||||
|
||||
@override
|
||||
void onRemove() {
|
||||
parent
|
||||
.firstChild<_TurboChargeSpriteAnimationComponent>()!
|
||||
.removeFromParent();
|
||||
super.onRemove();
|
||||
}
|
||||
}
|
||||
|
||||
class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent
|
||||
with HasGameRef, ZIndex, ParentIsA<Ball> {
|
||||
_TurboChargeSpriteAnimationComponent()
|
||||
: super(
|
||||
anchor: const Anchor(0.53, 0.72),
|
||||
) {
|
||||
zIndex = ZIndexes.turboChargeFlame;
|
||||
}
|
||||
|
||||
late final Vector2 _textureSize;
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
|
||||
final direction = -parent.body.linearVelocity.normalized();
|
||||
angle = math.atan2(direction.x, -direction.y);
|
||||
size = (_textureSize / 45) * parent.body.fixtures.first.shape.radius;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
final spriteSheet = await gameRef.images.load(
|
||||
Assets.images.ball.flameEffect.keyName,
|
||||
);
|
||||
|
||||
const amountPerRow = 8;
|
||||
const amountPerColumn = 4;
|
||||
_textureSize = Vector2(
|
||||
spriteSheet.width / amountPerRow,
|
||||
spriteSheet.height / amountPerColumn,
|
||||
);
|
||||
|
||||
animation = SpriteAnimation.fromFrameData(
|
||||
spriteSheet,
|
||||
SpriteAnimationData.sequenced(
|
||||
amount: amountPerRow * amountPerColumn,
|
||||
amountPerRow: amountPerRow,
|
||||
stepTime: 1 / 24,
|
||||
textureSize: _textureSize,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export 'ball_gravitating_behavior.dart';
|
||||
export 'ball_scaling_behavior.dart';
|
||||
export 'ball_turbo_charging_behavior.dart';
|
@ -0,0 +1 @@
|
||||
export 'flapper_spinning_behavior.dart';
|
@ -0,0 +1,15 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
class FlapperSpinningBehavior extends ContactBehavior<FlapperEntrance> {
|
||||
@override
|
||||
void beginContact(Object other, Contact contact) {
|
||||
super.beginContact(other, contact);
|
||||
if (other is! Ball) return;
|
||||
parent.parent?.firstChild<SpriteAnimationComponent>()?.playing = true;
|
||||
}
|
||||
}
|
@ -0,0 +1,215 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_components/src/components/flapper/behaviors/behaviors.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template flapper}
|
||||
/// Flap to let a [Ball] out of the [LaunchRamp] and to prevent [Ball]s from
|
||||
/// going back in.
|
||||
/// {@endtemplate}
|
||||
class Flapper extends Component {
|
||||
/// {@macro flapper}
|
||||
Flapper()
|
||||
: super(
|
||||
children: [
|
||||
FlapperEntrance(
|
||||
children: [
|
||||
FlapperSpinningBehavior(),
|
||||
],
|
||||
)..initialPosition = Vector2(4, -69.3),
|
||||
_FlapperStructure(),
|
||||
_FlapperExit()..initialPosition = Vector2(-0.6, -33.8),
|
||||
_BackSupportSpriteComponent(),
|
||||
_FrontSupportSpriteComponent(),
|
||||
FlapSpriteAnimationComponent(),
|
||||
],
|
||||
);
|
||||
|
||||
/// Creates a [Flapper] without any children.
|
||||
///
|
||||
/// This can be used for testing [Flapper]'s behaviors in isolation.
|
||||
@visibleForTesting
|
||||
Flapper.test();
|
||||
}
|
||||
|
||||
/// {@template flapper_entrance}
|
||||
/// Sensor used in [FlapperSpinningBehavior] to animate
|
||||
/// [FlapSpriteAnimationComponent].
|
||||
/// {@endtemplate}
|
||||
class FlapperEntrance extends BodyComponent with InitialPosition, Layered {
|
||||
/// {@macro flapper_entrance}
|
||||
FlapperEntrance({
|
||||
Iterable<Component>? children,
|
||||
}) : super(
|
||||
children: children,
|
||||
renderBody: false,
|
||||
) {
|
||||
layer = Layer.launcher;
|
||||
}
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final shape = EdgeShape()
|
||||
..set(
|
||||
Vector2.zero(),
|
||||
Vector2(0, 3.2),
|
||||
);
|
||||
final fixtureDef = FixtureDef(
|
||||
shape,
|
||||
isSensor: true,
|
||||
);
|
||||
final bodyDef = BodyDef(position: initialPosition);
|
||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||
}
|
||||
}
|
||||
|
||||
class _FlapperStructure extends BodyComponent with Layered {
|
||||
_FlapperStructure() : super(renderBody: false) {
|
||||
layer = Layer.board;
|
||||
}
|
||||
|
||||
List<FixtureDef> _createFixtureDefs() {
|
||||
final leftEdgeShape = EdgeShape()
|
||||
..set(
|
||||
Vector2(1.9, -69.3),
|
||||
Vector2(1.9, -66),
|
||||
);
|
||||
|
||||
final bottomEdgeShape = EdgeShape()
|
||||
..set(
|
||||
leftEdgeShape.vertex2,
|
||||
Vector2(3.9, -66),
|
||||
);
|
||||
|
||||
return [
|
||||
FixtureDef(leftEdgeShape),
|
||||
FixtureDef(bottomEdgeShape),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final body = world.createBody(BodyDef());
|
||||
_createFixtureDefs().forEach(body.createFixture);
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
class _FlapperExit extends LayerSensor {
|
||||
_FlapperExit()
|
||||
: super(
|
||||
insideLayer: Layer.launcher,
|
||||
outsideLayer: Layer.board,
|
||||
orientation: LayerEntranceOrientation.down,
|
||||
insideZIndex: ZIndexes.ballOnLaunchRamp,
|
||||
outsideZIndex: ZIndexes.ballOnBoard,
|
||||
) {
|
||||
layer = Layer.launcher;
|
||||
}
|
||||
|
||||
@override
|
||||
Shape get shape => PolygonShape()
|
||||
..setAsBox(
|
||||
1.7,
|
||||
0.1,
|
||||
initialPosition,
|
||||
1.5708,
|
||||
);
|
||||
}
|
||||
|
||||
/// {@template flap_sprite_animation_component}
|
||||
/// Flap suspended between supports that animates to let the [Ball] exit the
|
||||
/// [LaunchRamp].
|
||||
/// {@endtemplate}
|
||||
@visibleForTesting
|
||||
class FlapSpriteAnimationComponent extends SpriteAnimationComponent
|
||||
with HasGameRef, ZIndex {
|
||||
/// {@macro flap_sprite_animation_component}
|
||||
FlapSpriteAnimationComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(2.8, -70.7),
|
||||
playing: false,
|
||||
) {
|
||||
zIndex = ZIndexes.flapper;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
final spriteSheet = gameRef.images.fromCache(
|
||||
Assets.images.flapper.flap.keyName,
|
||||
);
|
||||
|
||||
const amountPerRow = 14;
|
||||
const amountPerColumn = 1;
|
||||
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,
|
||||
loop: false,
|
||||
),
|
||||
)..onComplete = () {
|
||||
animation?.reset();
|
||||
playing = false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _BackSupportSpriteComponent extends SpriteComponent
|
||||
with HasGameRef, ZIndex {
|
||||
_BackSupportSpriteComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(2.95, -70.6),
|
||||
) {
|
||||
zIndex = ZIndexes.flapperBack;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
final sprite = Sprite(
|
||||
gameRef.images.fromCache(
|
||||
Assets.images.flapper.backSupport.keyName,
|
||||
),
|
||||
);
|
||||
this.sprite = sprite;
|
||||
size = sprite.originalSize / 10;
|
||||
}
|
||||
}
|
||||
|
||||
class _FrontSupportSpriteComponent extends SpriteComponent
|
||||
with HasGameRef, ZIndex {
|
||||
_FrontSupportSpriteComponent()
|
||||
: super(
|
||||
anchor: Anchor.center,
|
||||
position: Vector2(2.9, -67.6),
|
||||
) {
|
||||
zIndex = ZIndexes.flapperFront;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
final sprite = Sprite(
|
||||
gameRef.images.fromCache(
|
||||
Assets.images.flapper.frontSupport.keyName,
|
||||
),
|
||||
);
|
||||
this.sprite = sprite;
|
||||
size = sprite.originalSize / 10;
|
||||
}
|
||||
}
|