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';
|
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 'bottom_group.dart';
|
||||||
export 'camera_controller.dart';
|
export 'camera_controller.dart';
|
||||||
export 'controlled_ball.dart';
|
export 'controlled_ball.dart';
|
||||||
export 'controlled_flipper.dart';
|
export 'controlled_flipper.dart';
|
||||||
export 'controlled_plunger.dart';
|
export 'controlled_plunger.dart';
|
||||||
export 'dino_desert.dart';
|
export 'dino_desert/dino_desert.dart';
|
||||||
export 'drain.dart';
|
export 'drain.dart';
|
||||||
export 'flutter_forest/flutter_forest.dart';
|
export 'flutter_forest/flutter_forest.dart';
|
||||||
export 'game_flow_controller.dart';
|
export 'game_flow_controller.dart';
|
||||||
export 'google_word/google_word.dart';
|
export 'google_word/google_word.dart';
|
||||||
export 'launcher.dart';
|
export 'launcher.dart';
|
||||||
|
export 'multiballs/multiballs.dart';
|
||||||
export 'multipliers/multipliers.dart';
|
export 'multipliers/multipliers.dart';
|
||||||
export 'scoring_behavior.dart';
|
export 'scoring_behavior.dart';
|
||||||
export 'sparky_scorch.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 '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;
|
||||||
|
}
|
||||||
|
}
|